//SHARED // Инициализация Flatpickr document.addEventListener('DOMContentLoaded', () => { // Установка сегодняшней даты по умолчанию const today = new Date().toISOString().split('T')[0]; document.getElementById("reportDate").value = today; flatpickr("#reportDate", { locale: "es", dateFormat: "Y-m-d", maxDate: "today", altInput: true, altFormat: "j F Y", ariaDateFormat: "j F Y" }); const demoHint = document.getElementById('demoAccountsHint'); if (demoHint && window.GENERATE_DEMO_DATA === 'true') { demoHint.removeAttribute('hidden'); } }); //helper to destroyChart on change function destroyChart(instance) { if (instance) instance.destroy(); } // Система уведомлений // function showNotification(message, type = "success") { // const notification = document.getElementById("notification"); // const notificationText = document.getElementById("notificationText"); // if (!notification || !notificationText) return; // notificationText.textContent = message; // notification.className = `fixed top-4 right-4 z-50 animate-fade-in`; // if (type === "error") { // notification.innerHTML = ` //
//
// // ${message} //
//
// `; // } else { // notification.innerHTML = ` //
//
// // ${message} //
//
// `; // } // notification.classList.remove("hidden"); // setTimeout(() => { // notification.classList.add("hidden"); // }, 3000); // } let notificationTimeout = null; // At top of your script! function showNotification(message, type = "success") { const notification = document.getElementById("notification"); if (!notification) return; notification.innerHTML = `
${message}
`; notification.className = "fixed top-4 right-4 z-50 animate-fade-in"; notification.classList.remove("hidden"); notification.style.display = "block"; // previous timeout exists -> clear it! if (notificationTimeout) { clearTimeout(notificationTimeout); } notificationTimeout = setTimeout(() => { notification.classList.add("hidden"); notification.style.display = "none"; notificationTimeout = null; }, 3000); } // Gestión de ventanas modales con desplazamiento corregido function showModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.classList.add("show"); // Bloquear desplazamiento del cuerpo document.body.classList.add("modal-open"); } } function hideModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.classList.remove("show"); // Разблокируем прокрутку body document.body.classList.remove("modal-open"); } } // Настройка вкладок администратора function setupAdminTabs() { if (appState.adminTabsInitialized) return; appState.adminTabsInitialized = true; const tabButtons = document.querySelectorAll(".admin-tab-btn"); const tabContents = document.querySelectorAll(".admin-tab-content"); tabButtons.forEach((button) => { button.addEventListener("click", () => { const tabId = button.dataset.tab; // Переключение активной вкладки tabButtons.forEach((btn) => { btn.className = "admin-tab-btn px-6 py-3 font-medium text-gray-600 hover:text-blue-600 transition-colors border-b-2 border-transparent hover:border-blue-500"; }); button.className = "admin-tab-btn px-6 py-3 font-medium transition-colors border-b-2 border-blue-500 text-blue-600"; // Показ/скрытие содержимого tabContents.forEach((content) => { content.classList.add("hidden"); }); document.getElementById(tabId + "Tab").classList.remove("hidden"); if (tabId === "dashboard") updateDashboard(); // Optionally, reload todos for TODO tab if (tabId === "todo") loadTodos(); // Загрузка данных при переключении // switch (tabId) { // case "dashboard": // updateDashboard(); // break; // case "reports": // loadReports(); // break; // case "users": // loadUsers(); // break; // case "stores": // loadStores(); // break; // case "todo": // loadTodos(); // break; // } }); }); } // Закрытие модального окна при клике вне его document.addEventListener("click", (e) => { if (e.target.classList.contains("modal")) { hideModal(e.target.id); } }); // Глобальные функции для onclick обработчиков window.viewReport = viewReport; window.deleteReport = deleteReport; window.editUser = editUser; window.deleteUser = deleteUser; window.editStore = editStore; window.deleteStore = deleteStore; window.editTodo = editTodo; window.deleteTodo = deleteTodo; window.toggleTodo = toggleTodo; //AUTH // Обработчики авторизации document.getElementById("loginForm").addEventListener("submit", async (e) => { e.preventDefault(); const username = document.getElementById("username").value; const password = document.getElementById("password").value; // Call backend login const result = await loginUser(username, password); if (result.success) { // Save token for later API calls localStorage.setItem("token", result.token); // Save user info in global state appState.currentUser = result.user; // save globally reports of today for user if they are presented in what login giving back appState.todaysReports = result.todaysReports || []; // Hide login, show correct UI document.getElementById("loginScreen").classList.add("hidden"); if (result.user.role === "admin") { showAdminInterface(); } else { showUserInterface(); } showNotification("¡Autenticación exitosa!"); } else { const errorDiv = document.getElementById("loginError"); errorDiv.textContent = result.error || "Usuario o contraseña incorrectos"; errorDiv.classList.remove("hidden"); } }); // Показать интерфейс пользователя async function showUserInterface() { document.getElementById("userInterface").classList.remove("hidden"); document.getElementById( "userWelcome" ).textContent = `Bienvenido, ${appState.currentUser.username}!`; loadUserStores(); setupFormCalculations(); await loadReports(); } // Показать интерфейс администратора async function showAdminInterface() { document.getElementById("adminInterface").classList.remove("hidden"); document.getElementById( "adminWelcome" ).textContent = `Bienvenido, ${appState.currentUser.username}!`; await loadStores(); await loadUsers(); await loadReports(); updateDashboard(); loadTodos(); setupAdminTabs(); // Activate first tab (dashboard) document.querySelector('.admin-tab-btn[data-tab="dashboard"]').click(); } // Обработчики выхода document.getElementById("logoutBtn").addEventListener("click", logout); document.getElementById("adminLogoutBtn").addEventListener("click", logout); function logout() { appState.currentUser = null; appState.editingReportId = null; appState.usersList = []; appState.storesList = []; appState.reportsList = []; // database.users = []; // database.reports = []; // database.stores = []; // Remove all admin tab event listeners by replacing each node document.querySelectorAll(".admin-tab-btn").forEach((btn) => { btn.replaceWith(btn.cloneNode(true)); }); appState.adminTabsInitialized = false; document.getElementById("loginScreen").classList.remove("hidden"); document.getElementById("userInterface").classList.add("hidden"); document.getElementById("adminInterface").classList.add("hidden"); const reportForm = document.getElementById("reportForm"); if (reportForm) { reportForm.reset(); document.getElementById("wagesContainer").innerHTML = ""; addWageRow(); // Add one empty row document.getElementById("expensesContainer").innerHTML = ""; addExpenseRow(); } document.getElementById("loginForm").reset(); document.getElementById("loginError").classList.add("hidden"); showNotification("¡Sesión cerrada!"); } //DASHBOARD // Обновление дашборда function updateDashboard() { const reports = appState.reportsList || []; const users = appState.usersList || []; // Расчет статистики const totalRevenue = reports.reduce((sum, r) => sum + r.totalIncome, 0); const totalExpenses = reports.reduce((sum, r) => sum + r.totalExpenses, 0); const totalReports = reports.length; const totalUsers = users.length; // Обновление карточек document.getElementById( "totalRevenueCard" ).textContent = `€${totalRevenue.toFixed(2)}`; document.getElementById( "totalExpensesCard" ).textContent = `€${totalExpenses.toFixed(2)}`; document.getElementById("totalReportsCard").textContent = totalReports; document.getElementById("totalUsersCard").textContent = totalUsers; // Создание графиков createCharts(); } // Создание графиков function createCharts() { // График доходов по дням const reports = appState.reportsList || []; const stores = appState.storesList || []; const revenueCtx = document.getElementById("revenueChart"); if (revenueCtx) { destroyChart(appState.revenueChartInstance); const last7Days = []; const revenueData = []; for (let i = 6; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - i); const dateStr = date.toISOString().split("T")[0]; last7Days.push(dateStr); const dayReports = reports.filter( (r) => (r.reportDate || r.date) === dateStr ); const dayRevenue = dayReports.reduce( (sum, r) => sum + (Number(r.totalIncome) || 0), 0 ); revenueData.push(dayRevenue); } appState.revenueChartInstance = new Chart(revenueCtx, { type: "line", data: { labels: last7Days, datasets: [ { label: "Доходы", data: revenueData, borderColor: "rgb(59, 130, 246)", backgroundColor: "rgba(59, 130, 246, 0.1)", tension: 0.4, }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false, }, }, scales: { y: { beginAtZero: true, ticks: { callback: function (value) { return "€" + value.toFixed(0); }, }, }, }, }, }); } // Круговая диаграмма расходов const expensesCtx = document.getElementById("expensesChart"); if (expensesCtx) { destroyChart(appState.expensesChartInstance); const totalWages = reports.reduce( (sum, r) => sum + (Number(r.totalWages) || 0), 0 ); const totalInternal = reports.reduce( (sum, r) => sum + (Number(r.totalExpenses) || 0), 0 ); const totalEnvelope = reports.reduce( (sum, r) => sum + (Number(r.envelope) || 0), 0 ); appState.expensesChartInstance = new Chart(expensesCtx, { type: "doughnut", data: { labels: ["Salarios", "Otros gastos", "Envelope"], datasets: [ { data: [totalWages, totalInternal, totalEnvelope], backgroundColor: ["#F59E0B", "#EF4444", "#3B82F6"], }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: "bottom", }, }, }, }); } // График по магазинам const storesCtx = document.getElementById("storesChart"); if (storesCtx) { destroyChart(appState.storesChartInstance); const storeData = stores.map((store) => { const storeReports = reports.filter((r) => r.storeId === store.id); const revenue = storeReports.reduce((sum, r) => sum + r.totalIncome, 0); return { name: store.name, revenue }; }); // console.log("storeData for bar chart:", storeData); appState.storesChartInstance = new Chart(storesCtx, { type: "bar", data: { labels: storeData.map((s) => s.name), datasets: [ { label: "Ingresos por tienda", data: storeData.map((s) => s.revenue), backgroundColor: "#10B981", }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false, }, }, scales: { y: { beginAtZero: true, ticks: { callback: function (value) { return "€" + value.toFixed(0); }, }, }, }, }, }); } // Тренды продаж const trendsCtx = document.getElementById("trendsChart"); if (trendsCtx) { destroyChart(appState.trendsChartInstance); const last30Days = []; const profitData = []; for (let i = 29; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - i); const dateStr = date.toISOString().split("T")[0]; last30Days.push(dateStr); const dayReports = reports.filter( (r) => (r.reportDate || r.date) === dateStr ); const dayProfit = dayReports.reduce( (sum, r) => sum + ((Number(r.totalIncome) || 0) - (Number(r.totalExpenses) || 0)), 0 ); profitData.push(dayProfit); } appState.trendsChartInstance = new Chart(trendsCtx, { type: "line", data: { labels: last30Days, datasets: [ { label: "Beneficio", data: profitData, borderColor: "#8B5CF6", backgroundColor: "rgba(139, 92, 246, 0.1)", tension: 0.4, fill: true, }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false, }, }, scales: { y: { beginAtZero: true, ticks: { callback: function (value) { return "€" + value.toFixed(0); }, }, }, }, }, }); } } //REPORTS async function loadReports() { if (!appState.storesList || appState.storesList.length === 0) { await loadStores(); return; } const tbody = document.getElementById("reportsTableBody"); const filterStore = document.getElementById("filterStore"); const result = await getReports(); // console.log("getReports() result:", result); appState.reportsList = result.success ? result.reports : []; if (!result.success) { showNotification(result.error || "Error al cargar informes", "error"); return; } // extract reports array const reports = result.reports; const stores = appState.storesList || []; // parse wages/expenses from JSON string to array --- reports.forEach((report) => { // parse wages if it's a string (backend gives string, we need array) if (typeof report.wages === "string") { try { report.wages = JSON.parse(report.wages); } catch { report.wages = []; } } // parse expenses if it's a string if (typeof report.expenses === "string") { try { report.expenses = JSON.parse(report.expenses); } catch { report.expenses = []; } } }); filterStore.innerHTML = ` ${stores .map((store) => ``) .join("")} `; // render table rows tbody.innerHTML = ""; reports.forEach((report) => { const profit = (Number(report.totalIncome) || 0) - (Number(report.totalExpenses) || 0); const store = stores.find((s) => Number(s.id) === Number(report.storeId)); const storeName = store ? store.name : report.storeName || report.storeId; const row = document.createElement("tr"); row.className = "hover:bg-gray-50"; row.innerHTML = ` ${ report.reportDate || report.date || "" } ${storeName} €${Number( report.totalIncome ).toFixed(2)} €${Number( report.totalExpenses ).toFixed(2)} €${profit.toFixed(2)} ${ report.username || report.userId } ${report.isVerified ? "Verificado" : "No verificado"} `; tbody.appendChild(row); }); setupReportsFilters(); } // Use global array for backend reports function viewReport(reportId) { if (!appState.reportsList) { showNotification("Reports not loaded yet!", "error"); return; } const report = appState.reportsList.find((r) => r.id === reportId); if (report) { showReportModal(report, true); } else { showNotification("Report not found!", "error"); } } function editReport(report) { // console.log("editReport() called with:", report); appState.editingReportId = report.id; const content = document.getElementById("reportViewContent"); const buttons = document.getElementById("reportModalButtons"); const shopName = (report.storeName || report.storeId || "").replace( /"/g, """ ); content.innerHTML = `

Основная информация

Доходы

Статус

`; buttons.innerHTML = ` `; document .getElementById("saveReportBtn") .addEventListener("click", async (e) => { e.preventDefault(); await saveEditedReport(report.id); }); document.getElementById("cancelEditBtn").addEventListener("click", (e) => { e.preventDefault(); showReportModal(report, true); }); } async function saveEditedReport(reportId) { // Get values from the form const storeId = document.getElementById("editStoreSelect").value; const reportDate = document.getElementById("editDate").value; const incomeVal = document.getElementById("editIncome").value; const initialCashVal = document.getElementById("editCajaInicial").value; const envelopeVal = document.getElementById("editEnvelope").value; const isVerified = document.getElementById("editVerified").checked ? 1 : 0; // Build the payload dynamically const data = {}; if (storeId) data.storeId = parseInt(storeId, 10); if (reportDate) data.reportDate = reportDate; if (incomeVal !== "") data.income = parseFloat(incomeVal); if (initialCashVal !== "") data.initialCash = parseFloat(initialCashVal); if (envelopeVal !== "") data.envelope = parseFloat(envelopeVal); if (data.income !== undefined && data.initialCash !== undefined) { data.totalIncome = data.income + data.initialCash; } data.isVerified = isVerified; // REMOVE any NaN values Object.keys(data).forEach((k) => { if (typeof data[k] === "number" && isNaN(data[k])) delete data[k]; }); await updateReport(reportId, data); } // Показ модального окна отчета с исправленной прокруткой function showReportModal(report, isAdmin = false) { // --- Parse wages/expenses as arrays if needed --- let wages = report.wages; let expenses = report.expenses; if (typeof wages === "string") { try { wages = JSON.parse(wages); } catch { wages = []; } } if (typeof expenses === "string") { try { expenses = JSON.parse(expenses); } catch { expenses = []; } } let totalExpensesInternal = 0; if (Array.isArray(expenses)) { totalExpensesInternal = expenses.reduce( (sum, e) => sum + (Number(e.amount) || 0), 0 ); } const modal = document.getElementById("reportViewModal"); const content = document.getElementById("reportViewContent"); const buttons = document.getElementById("reportModalButtons"); const title = document.getElementById("reportModalTitle"); title.textContent = `Informe del ${report.reportDate || report.date} - ${ report.storeName || report.storeId || "Tienda desconocida" }`; content.innerHTML = `

Información básica

Fecha: ${ report.date || report.reportDate || "" }
Tienda: ${ report.storeName || report.storeId }
Usuario: ${ report.username || report.fullName || report.userId }
Estado: ${ report.isVerified || report.verified ? "Verificado" : "No verificado" }

Ingresos

Ingresos del día: €${safeToFixed(report.income)}
Caja inicial: €${safeToFixed( report.cajaInicial || report.initialCash )}
Ingresos totales: €${safeToFixed( report.totalIncome )}

Salarios

${ Array.isArray(wages) && wages.length > 0 ? `
${wages .map( (w) => `
${w.name} €${safeToFixed(w.amount)}
` ) .join("")}
Total salarios: €${safeToFixed(report.totalWages)}
` : '

No hay datos de salarios

' }

Gastos

${ Array.isArray(expenses) && expenses.length > 0 ? `
${expenses .map( (e) => `
${e.name} €${safeToFixed(e.amount)}
` ) .join("")}
Total gastos internos: €${safeToFixed(totalExpensesInternal)}
` : '

No hay datos de gastos

' }

Resumen final

Ingresos totales: €${safeToFixed( report.totalIncome )}
Gastos totales: €${safeToFixed( report.totalExpenses )}
Envelope: €${safeToFixed( report.envelope )}
Caja final: €${safeToFixed( report.cajaFinal || report.finalCash )}
`; buttons.innerHTML = ""; if (isAdmin) { if (report.isVerified || report.verified) { // Verified: only unverify + close buttons.innerHTML = `
Informe verificado.
Para hacer cambios, retire la verificación.
`; document .getElementById("unverifyReportBtn") .addEventListener("click", () => { unverifyReport(report.id); }); } else { buttons.innerHTML = ` `; document.getElementById("editReportBtn").addEventListener("click", () => { editReport(report); }); document .getElementById("verifyReportBtn") .addEventListener("click", () => { verifyReport(report.id); }); } } else { if (!report.isVerified && !report.verified) { buttons.innerHTML = ` `; document .getElementById("editReportUserBtn") .addEventListener("click", () => { appState.editingReportId = report.id; fillFormWithReport(report); hideModal("reportViewModal"); }); } else { buttons.innerHTML = `
El informe está verificado y no se puede modificar. Contacte al administrador para cambios.
`; } } document .getElementById("closeReportModalBtn") .addEventListener("click", () => { hideModal("reportViewModal"); }); showModal("reportViewModal"); } // helpder of the modal of showReportModal - unverify report for ADMIN async function unverifyReport(reportId) { await updateReport(reportId, { isVerified: 0 }); showNotification("Verificación retirada. Ahora se puede editar."); await loadReports(); hideModal("reportViewModal"); } // Настройка фильтров отчетов function setupReportsFilters() { document .getElementById("applyFilters") .addEventListener("click", applyReportsFilters); document .getElementById("exportExcel") .addEventListener("click", exportToExcel); } function applyReportsFilters() { const storeFilter = document.getElementById("filterStore").value; const dateFrom = document.getElementById("filterDateFrom").value; const dateTo = document.getElementById("filterDateTo").value; let filteredReports = appState.reportsList || []; if (storeFilter) { filteredReports = filteredReports.filter( (r) => String(r.storeId) === String(storeFilter) ); } if (dateFrom) { filteredReports = filteredReports.filter((r) => r.reportDate >= dateFrom); } if (dateTo) { filteredReports = filteredReports.filter((r) => r.reportDate <= dateTo); } // Обновление таблицы с отфильтрованными данными const tbody = document.getElementById("reportsTableBody"); tbody.innerHTML = ""; filteredReports.forEach((report) => { const storeName = report.storeName || report.storeId; const username = report.username || report.userId; const profit = (Number(report.totalIncome) || 0) - (Number(report.totalExpenses) || 0); const row = document.createElement("tr"); row.className = "hover:bg-gray-50"; row.innerHTML = ` ${ report.reportDate || report.date || "" } ${storeName} €${report.totalIncome.toFixed( 2 )} €${report.totalExpenses.toFixed( 2 )} €${profit.toFixed(2)} ${username} ${report.isVerified ? "Verificado" : "No verificado"} `; tbody.appendChild(row); }); showNotification(`Se encontraron ${filteredReports.length} informes`); } function deleteReport(reportId) { showConfirmModal("¿Estás seguro de que deseas eliminar este informe?", () => { apiDeleteReport(reportId); loadReports(); updateDashboard(); showNotification("¡Informe eliminado!"); }); } //USER BEHAVIOUR INSIDE REPORTS // Заполнение формы данными отчета (для пользователя) function fillFormWithReport(report) { appState.editingReportId = report.id; document.getElementById("storeSelect").value = report.storeId; document.getElementById("income").value = report.income || 0; document.getElementById("cajaInicial").value = report.cajaInicial || report.initialCash || 0; document.getElementById("envelope").value = report.envelope; document.getElementById("displayTotalIncome").value = report.totalIncome; // document.getElementById("reportDate").value = // report.reportDate || report.date || ""; // --- Deal with date change tooltip --- // Set the date value const dateInput = document.getElementById("reportDate"); const originalDate = report.reportDate || report.date || ""; dateInput.value = originalDate; // Store the original date for later check dateInput.dataset.originalDate = originalDate; // Hide warning by default document.getElementById("dateEditWarning").classList.add("hidden"); // Set up change listener dateInput.oninput = function () { if (dateInput.value && dateInput.value !== dateInput.dataset.originalDate) { document.getElementById("dateEditWarning").classList.remove("hidden"); } else { document.getElementById("dateEditWarning").classList.add("hidden"); } }; // --- Parse wages/expenses if they are string --- let wages = report.wages; let expenses = report.expenses; if (typeof wages === "string") { try { wages = JSON.parse(wages); } catch { wages = []; } } if (typeof expenses === "string") { try { expenses = JSON.parse(expenses); } catch { expenses = []; } } // --- Fill Wages --- const wagesContainer = document.getElementById("wagesContainer"); wagesContainer.innerHTML = ""; if (Array.isArray(wages) && wages.length > 0) { wages.forEach((wage) => { const row = document.createElement("div"); row.className = "wage-row grid grid-cols-1 md:grid-cols-3 gap-4 mb-3"; row.innerHTML = ` `; wagesContainer.appendChild(row); }); } else { addWageRow(); } // --- Fill Expenses --- const expensesContainer = document.getElementById("expensesContainer"); expensesContainer.innerHTML = ""; if (Array.isArray(expenses) && expenses.length > 0) { expenses.forEach((expense) => { const row = document.createElement("div"); row.className = "expense-row grid grid-cols-1 md:grid-cols-3 gap-4 mb-3"; row.innerHTML = ` `; expensesContainer.appendChild(row); }); } else { addExpenseRow(); } updateTotals(); } // Экспорт в Excel function exportToExcel() { // Use backend data const reports = appState.reportsList || []; const stores = appState.storesList || []; const users = appState.usersList || []; if (!reports.length) { showNotification("No hay informes para exportar", "info"); return; } const data = reports.map((report) => { // Find store and user by id const store = stores.find((s) => Number(s.id) === Number(report.storeId)) || {}; const user = users.find((u) => Number(u.id) === Number(report.userId)) || {}; const profit = (Number(report.totalIncome) || 0) - (Number(report.totalExpenses) || 0); return { Дата: report.reportDate || report.date || "", Tienda: store.name || report.storeName || "Desconocido", Доход: `€${Number(report.totalIncome || 0).toFixed(2)}`, Расходы: `€${Number(report.totalExpenses || 0).toFixed(2)}`, Прибыль: `€${profit.toFixed(2)}`, Usuario: user.username || report.username || "Desconocido", Estado: report.isVerified ? "Verificado" : report.verified ? "Verificado" : "No verificado", }; }); if (!data.length) { showNotification("No hay informes para exportar", "info"); return; } // Create CSV const headers = Object.keys(data[0]); const csvContent = [ headers.join(","), ...data.map((row) => headers.map((header) => `"${row[header]}"`).join(",")), ].join("\n"); // Download file const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;", }); const link = document.createElement("a"); const url = URL.createObjectURL(blob); link.setAttribute("href", url); link.setAttribute( "download", `cash_reports_${new Date().toISOString().split("T")[0]}.csv` ); link.style.visibility = "hidden"; document.body.appendChild(link); link.click(); document.body.removeChild(link); showNotification("¡Informe exportado!"); } //REPORTS FORM LOGIC // Загрузка магазинов для пользователя function loadUserStores() { const select = document.getElementById("storeSelect"); if (!select) return; select.innerHTML = ''; if (!appState.currentUser) return; // For admin: show all if (appState.currentUser.role === "admin") { (appState.storesList || []).forEach((store) => { const option = document.createElement("option"); option.value = store.id; option.textContent = store.name; select.appendChild(option); }); } else { // For employee: only their own stores (appState.currentUser.stores || []).forEach((storeObj) => { // storeObj could be an object ({id, name, ...}) or just an ID; check backend API let store = storeObj; if (typeof storeObj === "number") { // If backend sends just IDs, look up in appState.storesList store = (appState.storesList || []).find((s) => s.id === storeObj); } if (store) { const option = document.createElement("option"); option.value = store.id; option.textContent = store.name; select.appendChild(option); } }); } } // Настройка автоматических расчетов в форме function setupFormCalculations() { const incomeInput = document.getElementById("income"); const cajaInicialInput = document.getElementById("cajaInicial"); const envelopeInput = document.getElementById("envelope"); function updateCalculations() { const income = parseFloat(incomeInput.value) || 0; const cajaInicial = parseFloat(cajaInicialInput.value) || 0; const envelope = parseFloat(envelopeInput.value) || 0; const totalIncome = income + cajaInicial; document.getElementById("totalIncome").value = totalIncome.toFixed(2); document.getElementById("displayTotalIncome").value = totalIncome.toFixed(2); const totalWages = calculateTotalWages(); const totalExpensesInternal = calculateTotalExpenses(); const totalExpenses = totalWages + totalExpensesInternal; document.getElementById("totalExpenses").value = totalExpenses.toFixed(2); const cajaFinal = totalIncome - totalExpenses - envelope; document.getElementById("cajaFinal").textContent = cajaFinal.toFixed(2); } incomeInput.addEventListener("input", updateCalculations); cajaInicialInput.addEventListener("input", updateCalculations); envelopeInput.addEventListener("input", updateCalculations); // setupDynamicRows(); } document.addEventListener("DOMContentLoaded", setupDynamicRows); // Настройка динамических строк let dynamicRowsInitialized = false; function setupDynamicRows() { // Only run the global listeners ONCE! if (!dynamicRowsInitialized) { document.addEventListener("input", (e) => { if ( e.target.classList.contains("wage-amount") || e.target.classList.contains("expense-amount") ) { updateTotals(); } }); document.addEventListener("click", (e) => { if (e.target.classList.contains("remove-wage")) { e.target.closest(".wage-row").remove(); updateTotals(); } else if (e.target.classList.contains("remove-expense")) { e.target.closest(".expense-row").remove(); updateTotals(); } }); dynamicRowsInitialized = true; } // Always attach these because they might be removed and re-injected const addWageBtn = document.getElementById("addWage"); if (addWageBtn && !addWageBtn.hasListener) { addWageBtn.addEventListener("click", () => addWageRow()); addWageBtn.hasListener = true; } const addExpenseBtn = document.getElementById("addExpense"); if (addExpenseBtn && !addExpenseBtn.hasListener) { addExpenseBtn.addEventListener("click", () => addExpenseRow()); addExpenseBtn.hasListener = true; } } document.addEventListener("DOMContentLoaded", setupDynamicRows); function addWageRow() { const container = document.getElementById("wagesContainer"); const row = document.createElement("div"); row.className = "wage-row grid grid-cols-1 md:grid-cols-3 gap-4 mb-3"; row.innerHTML = ` `; container.appendChild(row); } function addExpenseRow() { const container = document.getElementById("expensesContainer"); const row = document.createElement("div"); row.className = "expense-row grid grid-cols-1 md:grid-cols-3 gap-4 mb-3"; row.innerHTML = ` `; container.appendChild(row); } function calculateTotalWages() { const amounts = document.querySelectorAll(".wage-amount"); let total = 0; amounts.forEach((amount) => { total += parseFloat(amount.value) || 0; }); return total; } function calculateTotalExpenses() { const amounts = document.querySelectorAll(".expense-amount"); let total = 0; amounts.forEach((amount) => { total += parseFloat(amount.value) || 0; }); return total; } function updateTotals() { const totalWages = calculateTotalWages(); const totalExpensesInternal = calculateTotalExpenses(); document.getElementById("totalWages").textContent = totalWages.toFixed(2); document.getElementById("totalExpensesInternal").textContent = totalExpensesInternal.toFixed(2); const totalExpenses = totalWages + totalExpensesInternal; document.getElementById("totalExpenses").value = totalExpenses.toFixed(2); // Обновить Caja Final const income = parseFloat(document.getElementById("income").value) || 0; const cajaInicial = parseFloat(document.getElementById("cajaInicial").value) || 0; const envelope = parseFloat(document.getElementById("envelope").value) || 0; const totalIncome = income + cajaInicial; const cajaFinal = totalIncome - totalExpenses - envelope; document.getElementById("totalIncome").value = totalIncome.toFixed(2); document.getElementById("cajaFinal").textContent = cajaFinal.toFixed(2); } function collectWages() { const wages = []; document.querySelectorAll(".wage-row").forEach((row) => { const name = row.querySelector(".wage-name").value; const amount = parseFloat(row.querySelector(".wage-amount").value) || 0; if (name && amount > 0) wages.push({ name, amount }); }); return wages; } function collectExpenses() { const expenses = []; document.querySelectorAll(".expense-row").forEach((row) => { const name = row.querySelector(".expense-name").value; const amount = parseFloat(row.querySelector(".expense-amount").value) || 0; if (name && amount > 0) expenses.push({ name, amount }); }); return expenses; } function safeToFixed(value, digits = 2) { return (Number(value) || 0).toFixed(digits); } function upsertTodaysReport(report) { const today = new Date().toISOString().split("T")[0]; const storeId = String(report.storeId); appState.todaysReports = (appState.todaysReports || []).filter((r) => { return ( !(r.id === report.id) && !( String(r.storeId) === storeId && (r.reportDate || r.date) === today && (r.userId == report.userId || r.username === report.username) ) ); }); // Add again only if the edited report is for today if ((report.reportDate || report.date) === today) { appState.todaysReports.push(report); } } // save report as user-worker document.getElementById("reportForm").addEventListener("submit", async (e) => { e.preventDefault(); const incomeValue = parseFloat(document.getElementById("income").value); const cajaInicialValue = parseFloat( document.getElementById("cajaInicial").value ); const envelopeValue = parseFloat(document.getElementById("envelope").value); const totalIncomeValue = incomeValue + cajaInicialValue; const totalWagesValue = calculateTotalWages(); const totalExpensesInternalValue = calculateTotalExpenses(); const totalExpensesValue = totalExpensesInternalValue + totalWagesValue; const finalCashValue = totalIncomeValue - totalExpensesValue - envelopeValue; const formData = { storeId: parseInt(document.getElementById("storeSelect").value), // reportDate: new Date().toISOString().split("T")[0], reportDate: document.getElementById("reportDate").value || new Date().toISOString().split("T")[0], income: isNaN(incomeValue) ? 0 : incomeValue, initialCash: isNaN(cajaInicialValue) ? 0 : cajaInicialValue, totalIncome: isNaN(totalIncomeValue) ? 0 : totalIncomeValue, wages: JSON.stringify(collectWages()), expenses: JSON.stringify(collectExpenses()), totalWages: isNaN(totalWagesValue) ? 0 : totalWagesValue, totalExpensesInternal: isNaN(totalExpensesInternalValue) ? 0 : totalExpensesInternalValue, totalExpenses: isNaN(totalExpensesValue) ? 0 : totalExpensesValue, envelope: isNaN(envelopeValue) ? 0 : envelopeValue, finalCash: isNaN(finalCashValue) ? 0 : finalCashValue, }; let result; if (appState.editingReportId) { // EDIT mode: update existing report // Update! result = await updateReport(appState.editingReportId, formData); } else { // CREATE mode: new report result = await createReport(formData); } // if result is missing --> error if (!result) { showNotification("No hay respuesta del servidor. Inténtalo de nuevo.", "error"); return; } // if (result.success && (!appState.editingReportId || result.id)) { if ( (appState.editingReportId && result.success) || // edit: only need success (!appState.editingReportId && result.success && result.id) // create: need id ) { resetReportForm(); const wasEdit = !!appState.editingReportId; const reportId = appState.editingReportId || result.id; appState.editingReportId = null; window.scrollTo({ top: 0, behavior: "smooth" }); const newReport = { ...formData, id: reportId, userId: appState.currentUser.id, username: appState.currentUser.username, }; upsertTodaysReport(newReport); showNotification( wasEdit ? "¡Informe editado con éxito!" : "¡Informe creado con éxito!" ); await loadReports(); } else { resetReportForm(); window.scrollTo({ top: 0, behavior: "smooth" }); showNotification(result.error || "Error al crear el informe", "error"); } }); function resetReportForm() { document.getElementById("reportForm").reset(); document.getElementById("wagesContainer").innerHTML = ""; addWageRow(); document.getElementById("expensesContainer").innerHTML = ""; addExpenseRow(); document.getElementById("totalWages").textContent = "0.00"; document.getElementById("totalExpensesInternal").textContent = "0.00"; document.getElementById("cajaFinal").textContent = "0.00"; // today const today = new Date().toISOString().split("T")[0]; // left date blank document.getElementById("reportDate").value = ""; document.getElementById("reportDate").setAttribute("max", today); // OR // Set date field to today and restrict to today or earlier // document.getElementById("reportDate").value = today; // document.getElementById("reportDate").setAttribute("max", today); //clean tooltip about changing date on the report document.getElementById("dateEditWarning").classList.add("hidden"); } // Отчет за сегодня для пользователя document.getElementById("todayReportBtn").addEventListener("click", () => { const storeId = document.getElementById("storeSelect").value; if (!storeId) { showNotification("¡Por favor seleccione una tienda!", "error"); return; } const todaysReport = (appState.todaysReports || []).find( (r) => String(r.storeId) === storeId ); console.log(todaysReport); console.log("Matching today's report:", todaysReport); if (todaysReport) { showReportModal(todaysReport, false); } else { showNotification("El informe de hoy aún no se ha creado", "error"); document.getElementById("reportForm").reset(); document.getElementById("storeSelect").value = storeId; document.getElementById("wagesContainer").innerHTML = ""; addWageRow(); document.getElementById("expensesContainer").innerHTML = ""; addExpenseRow(); } }); //USERS async function loadUsers() { // console.log("Loading users..."); const tbody = document.getElementById("usersTableBody"); tbody.innerHTML = ""; const result = await getAllUsers(); // console.log("getAllUsers result:", result); if (!result.success) { showNotification(result.error, "error"); return; } const users = result.users; appState.usersList = users; users.forEach((user) => { const userStores = user.role === "admin" ? "Все магазины" : user.stores .map((storeId) => { const store = (appState.storesList || []).find( (s) => s.id === storeId ); return store ? store.name : "Sin acceso"; }) .join(", ") || "Sin acceso"; const row = document.createElement("tr"); row.className = "hover:bg-gray-50"; row.innerHTML = ` ${user.id} ${user.username} ${user.role === "admin" ? "Administrador" : "Empleado"} ${userStores} `; tbody.appendChild(row); }); if (users.length === 0) { showNotification("No hay usuarios para mostrar", "info"); } } // Редактирование пользователя function editUser(userId) { appState.editingUserId = userId; const user = appState.usersList.find((u) => u.id === userId); showUserEditModal(user); } // Показ модального окна редактирования пользователя function showUserEditModal(user) { const modal = document.getElementById("userEditModal"); const title = document.getElementById("userModalTitle"); const form = document.getElementById("userEditForm"); // If appState.editingUserId, it's an edit; else, it's add title.textContent = appState.editingUserId ? "Editar usuario" : "Agregar usuario"; document.getElementById("userLogin").value = (user && user.username) || ""; const loginInput = document.getElementById("userLogin"); loginInput.value = (user && user.username) || ""; // if (appState.editingUserId) { // loginInput.disabled = true; // } else { // loginInput.disabled = false; // } document.getElementById("userPassword").value = user && user.plaintextPassword ? user.plaintextPassword : ""; const pwInput = document.getElementById("userPassword"); pwInput.value = user && user.plaintextPassword ? user.plaintextPassword : ""; pwInput.type = "password"; // Always set hidden on open! const eye = document.getElementById("eyeIcon"); if (eye) eye.textContent = "👁️"; document.getElementById("userRole").value = (user && user.role) || "employee"; // Загрузка чекбоксов магазинов const storesContainer = document.getElementById("userStoresAccess"); storesContainer.innerHTML = ""; (appState.storesList || []).forEach((store) => { const isChecked = user && user.stores && user.stores.includes(store.id); const checkbox = document.createElement("label"); checkbox.className = "flex items-center"; checkbox.innerHTML = ` ${store.name} `; storesContainer.appendChild(checkbox); }); showModal("userEditModal"); } //eye toggle for the password document.getElementById("togglePasswordBtn").onclick = function () { const pw = document.getElementById("userPassword"); const eye = document.getElementById("eyeIcon"); if (pw.type === "password") { pw.type = "text"; eye.textContent = "🙈"; } else { pw.type = "password"; eye.textContent = "👁️"; } }; //save user for create and update async function saveUser() { const login = document.getElementById("userLogin").value.trim(); const password = document.getElementById("userPassword").value; const role = document.getElementById("userRole").value; const selectedStores = Array.from( document.querySelectorAll( '#userStoresAccess input[type="checkbox"]:checked' ) ).map((cb) => parseInt(cb.value)); if (!login) { showNotification("¡Complete el nombre de usuario!", "error"); return; } // Always build userData const userData = { username: login, role: role, storeIds: selectedStores, }; if (password) userData.password = password; // Determine CREATE or EDIT let result; if (!appState.editingUserId) { if (!password) { showNotification("¡Especifique una contraseña para el nuevo usuario!", "error"); return; } // CREATE result = await createUser(userData); if (result.success) { showNotification("¡Usuario agregado!"); } } else { // EDIT result = await updateUser(appState.editingUserId, userData); if (result.success) { showNotification("¡Usuario actualizado!"); } appState.editingUserId = null; } // After save: UI update or error if (result && result.success) { hideModal("userEditModal"); loadUsers(); updateDashboard(); loadReports(); } else if (result) { showNotification(result.error || "Error de operación", "error"); } } //UI trigger: delete user with modal function deleteUser(userId) { showConfirmModal("¿Está seguro de que desea eliminar este usuario?", () => apiDeleteUser(userId) ); } // Добавление пользователя document.addEventListener("DOMContentLoaded", function () { const addUserBtn = document.getElementById("addUserBtn"); if (addUserBtn) { addUserBtn.addEventListener("click", () => { appState.editingUserId = null; showUserEditModal(); }); } }); // Сохранение пользователя document.addEventListener("DOMContentLoaded", function () { const saveUserBtn = document.getElementById("saveUserBtn"); const cancelUserBtn = document.getElementById("cancelUserBtn"); if (saveUserBtn) { saveUserBtn.addEventListener("click", saveUser); } if (cancelUserBtn) { cancelUserBtn.addEventListener("click", () => { hideModal("userEditModal"); }); } }); //SHOPS // Загрузка магазинов async function loadStores() { console.log("loadStores CALLED"); const tbody = document.getElementById("storesTableBody"); tbody.innerHTML = ""; const result = await getStores(); if (!result.success) { showNotification(result.error || "Error al cargar tiendas", "error"); appState.storesList = result.stores; return; } const stores = result.stores; appState.storesList = stores; console.log("Fetched stores from API:", result.stores); console.log("appState.storesList:", appState.storesList); stores.forEach((store) => { console.log("Rendering store:", store.name); const row = document.createElement("tr"); row.className = "hover:bg-gray-50"; row.innerHTML = ` ${store.id} ${store.name} ${ store.reportsCount || 0 } `; tbody.appendChild(row); }); if (stores.length === 0) { showNotification("No hay tiendas para mostrar", "info"); } } // Редактирование магазина function editStore(storeId) { appState.editingStoreId = storeId; showStoreEditModal(); } // Показ модального окна редактирования магазина function showStoreEditModal() { const modal = document.getElementById("storeEditModal"); const title = document.getElementById("storeModalTitle"); const form = document.getElementById("storeEditForm"); const store = appState.storesList.find( (s) => s.id === appState.editingStoreId ); if (!store) { showNotification("¡Tienda no encontrada!", "error"); return; } title.textContent = "Editar tienda"; document.getElementById("storeName").value = store.name || ""; showModal("storeEditModal"); } async function saveStore() { const name = document.getElementById("storeName").value.trim(); if (!name) { showNotification("¡Complete el nombre de la tienda!", "error"); return; } let result; if (appState.editingStoreId == null) { // Add result = await createStore({ name }); if (result.success) { showNotification("¡Tienda agregada!"); hideModal("storeEditModal"); await loadStores(); await loadReports(); if (typeof loadUsers === "function") loadUsers(); if (typeof loadUserStores === "function") loadUserStores(); if (typeof updateDashboard === "function") updateDashboard(); } } else { // Edit result = await updateStore(appState.editingStoreId, { name }); if (result.success) { showNotification("¡Tienda actualizada!"); hideModal("storeEditModal"); await loadStores(); await loadReports(); if (typeof loadUsers === "function") loadUsers(); if (typeof loadUserStores === "function") loadUserStores(); if (typeof updateDashboard === "function") updateDashboard(); } } if (result && !result.success) { if ( result.status === 409 || (result.error && result.error.includes("already exists")) ) { showNotification("¡Ya existe una tienda con este nombre!", "error"); } else { showNotification(result.error || "Error al guardar tienda", "error"); } return; } hideModal("storeEditModal"); appState.editingStoreId = null; await loadStores(); if (typeof updateDashboard === "function") updateDashboard(); } // Удаление магазина function deleteStore(storeId) { const store = appState.storesList.find((s) => s.id === storeId); let message = `¿Está seguro de que desea eliminar esta tienda "${store.name}"?`; if (store.reportsCount && store.reportsCount > 0) { message += `\n\n¡Atención! Esta tienda tiene ${store.reportsCount} informes asociados. También serán eliminados.`; } showConfirmModal(message, () => handleDeleteStore(storeId)); } async function handleDeleteStore(storeId) { const result = await deleteStoreApi(storeId); if (result.success) { showNotification("Shop and related reports deleted!"); await loadStores(); if (typeof loadReports === "function") loadReports(); if (typeof updateDashboard === "function") updateDashboard(); if (typeof loadUsers === "function") loadUsers(); } else { showNotification(result.error || "Failed to delete shop", "error"); } } // Добавление магазина document.getElementById("addStoreBtn").onclick = () => { appState.editingStoreId = null; document.getElementById("storeModalTitle").textContent = "Agregar tienda"; document.getElementById("storeName").value = ""; showModal("storeEditModal"); }; // Сохранение магазина document.addEventListener("DOMContentLoaded", function () { const saveStoreBtn = document.getElementById("saveStoreBtn"); const cancelStoreBtn = document.getElementById("cancelStoreBtn"); if (saveStoreBtn) saveStoreBtn.onclick = saveStore; if (cancelStoreBtn) cancelStoreBtn.onclick = () => { hideModal("storeEditModal"); appState.editingStoreId = null; document.getElementById("storeEditForm").reset(); }; }); //TODO // Загрузка TODO function loadTodos() { const container = document.getElementById("todoList"); container.innerHTML = ""; database.todos.forEach((todo) => { const todoItem = document.createElement("div"); todoItem.className = `p-4 border rounded-lg ${ todo.completed ? "bg-green-50 border-green-200" : "bg-white border-gray-200" }`; const priorityColors = { low: "bg-blue-100 text-blue-800", medium: "bg-yellow-100 text-yellow-800", high: "bg-red-100 text-red-800", }; const priorityText = { low: "Bajo", medium: "Medio", high: "Alto", }; todoItem.innerHTML = `

${todo.title}

${ todo.description }

${priorityText[todo.priority]} ${ todo.createdAt }
`; container.appendChild(todoItem); }); } // Переключение статуса TODO function toggleTodo(todoId) { const todoIndex = database.todos.findIndex((t) => t.id === todoId); database.todos[todoIndex].completed = !database.todos[todoIndex].completed; loadTodos(); showNotification("¡Estado de tarea actualizado!"); } // Редактирование TODO function editTodo(todoId) { appState.editingTodoId = todoId; showTodoEditModal(); } // Показ модального окна редактирования TODO function showTodoEditModal() { const modal = document.getElementById("todoEditModal"); const title = document.getElementById("todoModalTitle"); const form = document.getElementById("todoEditForm"); if (appState.editingTodoId) { const todo = database.todos.find((t) => t.id === appState.editingTodoId); title.textContent = "Editar tarea"; document.getElementById("todoTitle").value = todo.title; document.getElementById("todoDescription").value = todo.description; document.getElementById("todoPriority").value = todo.priority; } else { title.textContent = "Agregar tarea"; form.reset(); } showModal("todoEditModal"); } function saveTodo() { const title = document.getElementById("todoTitle").value; const description = document.getElementById("todoDescription").value; const priority = document.getElementById("todoPriority").value; if (!title) { showNotification("¡Complete el título!", "error"); return; } if (appState.editingTodoId) { // Редактирование const todoIndex = database.todos.findIndex( (t) => t.id === appState.editingTodoId ); database.todos[todoIndex].title = title; database.todos[todoIndex].description = description; database.todos[todoIndex].priority = priority; showNotification("¡Tarea actualizada!"); } else { // Добавление const newId = Math.max(...database.todos.map((t) => t.id)) + 1; database.todos.push({ id: newId, title: title, description: description, priority: priority, completed: false, createdAt: new Date().toISOString().split("T")[0], }); showNotification("¡Tarea agregada!"); } hideModal("todoEditModal"); loadTodos(); } // Удаление TODO function deleteTodo(todoId) { if (confirm("¿Está seguro de que desea eliminar esta tarea?")) { database.todos = database.todos.filter((t) => t.id !== todoId); loadTodos(); showNotification("¡Tarea eliminada!"); } } // Добавление TODO document.addEventListener("DOMContentLoaded", function () { const addTodoBtn = document.getElementById("addTodoBtn"); if (addTodoBtn) { addTodoBtn.addEventListener("click", () => { appState.editingTodoId = null; showTodoEditModal(); }); } }); // Сохранение TODO document.addEventListener("DOMContentLoaded", function () { const saveTodoBtn = document.getElementById("saveTodoBtn"); const cancelTodoBtn = document.getElementById("cancelTodoBtn"); if (saveTodoBtn) { saveTodoBtn.addEventListener("click", saveTodo); } if (cancelTodoBtn) { cancelTodoBtn.addEventListener("click", () => { hideModal("todoEditModal"); }); } }); // ####################MOCK########### // База данных (симуляция) let database = { users: [ { id: 1, username: "admin", password: "admin123", role: "admin", stores: [], }, { id: 2, username: "employee", password: "password123", role: "employee", stores: [1, 2], }, { id: 3, username: "cashier1", password: "password123", role: "employee", stores: [1, 2], }, { id: 4, username: "cashier2", password: "password123", role: "employee", stores: [3], }, ], stores: [ { id: 1, name: "Магазин 1" }, { id: 2, name: "Магазин 2" }, { id: 3, name: "Магазин 3" }, { id: 4, name: "Магазин 4" }, ], reports: [], todos: [ { id: 1, title: "Исправить модальные окна", description: "Исправить прокрутку и видимость кнопок в модальных окнах", completed: true, priority: "high", createdAt: "2024-01-15", }, { id: 2, title: "Добавить экспорт в PDF", description: "Реализовать функцию экспорта отчетов в PDF формат", completed: false, priority: "medium", createdAt: "2024-01-16", }, { id: 3, title: "Улучшить графики", description: "Добавить больше интерактивности в графики Dashboard", completed: false, priority: "low", createdAt: "2024-01-17", }, ], }; // Генерация тестовых данных для отчетов function generateTestData() { const reports = []; const today = new Date(); for (let i = 0; i < 30; i++) { const date = new Date(today); date.setDate(date.getDate() - i); const storeId = Math.floor(Math.random() * 4) + 1; const userId = Math.floor(Math.random() * 3) + 2; // employee пользователи const income = Math.floor(Math.random() * 2000) + 500; const cajaInicial = Math.floor(Math.random() * 300) + 100; const totalIncome = income + cajaInicial; const wages = Math.floor(Math.random() * 400) + 100; const expenses = Math.floor(Math.random() * 200) + 50; const totalExpenses = wages + expenses; const envelope = Math.floor(Math.random() * 200) + 100; const cajaFinal = totalIncome - totalExpenses - envelope; reports.push({ id: i + 1, date: date.toISOString().split("T")[0], storeId: storeId, userId: userId, income: income, cajaInicial: cajaInicial, totalIncome: totalIncome, wages: [ { name: "Сотрудник " + (Math.floor(Math.random() * 3) + 1), amount: wages, }, ], expenses: [ { name: "Расходы " + (Math.floor(Math.random() * 3) + 1), amount: expenses, }, ], totalWages: wages, totalExpensesInternal: expenses, totalExpenses: totalExpenses, envelope: envelope, cajaFinal: cajaFinal, verified: Math.random() > 0.3, createdAt: date.toISOString(), }); } database.reports = reports; } // Инициализация тестовых данных generateTestData(); // ####################################