//SHARED //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); } // Управление модальными окнами с исправленной прокруткой function showModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.classList.add("show"); // Блокируем прокрутку body 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 user/token in JS (or localStorage) appState.currentUser = result.user; localStorage.setItem("token", result.token); document.getElementById("loginScreen").classList.add("hidden"); if (result.user.role === "admin") { showAdminInterface(); } else { showUserInterface(); } showNotification("Успешная авторизация!"); } else { const errorDiv = document.getElementById("loginError"); // errorDiv.textContent = result.error; errorDiv.textContent = "Неверный логин или пароль"; errorDiv.classList.remove("hidden"); } }); // Показать интерфейс пользователя function showUserInterface() { document.getElementById("userInterface").classList.remove("hidden"); document.getElementById( "userWelcome" ).textContent = `Добро пожаловать, ${appState.currentUser.username}!`; loadUserStores(); setupFormCalculations(); } // Показать интерфейс администратора async function showAdminInterface() { document.getElementById("adminInterface").classList.remove("hidden"); document.getElementById( "adminWelcome" ).textContent = `Добро пожаловать, ${appState.currentUser.username}!`; await loadUsers(); // Load users await loadStores(); // Load stores (first and only time!) await loadReports(); // Only now load reports (uses storesList) 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"); document.getElementById("loginForm").reset(); document.getElementById("loginError").classList.add("hidden"); showNotification("Вы вышли из системы"); } //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 ); appState.expensesChartInstance = new Chart(expensesCtx, { type: "doughnut", data: { labels: ["Зарплаты", "Прочие расходы"], datasets: [ { data: [totalWages, totalInternal], backgroundColor: ["#F59E0B", "#EF4444"], }, ], }, 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: "Доходы по магазинам", 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: "Прибыль", 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"); return; } const reports = result.reports; const stores = appState.storesList || []; filterStore.innerHTML = ` ${stores .map((store) => ``) .join("")} `; 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 ? "Проверен" : "Не проверен"} `; 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); 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) { const modal = document.getElementById("reportViewModal"); const content = document.getElementById("reportViewContent"); const buttons = document.getElementById("reportModalButtons"); const title = document.getElementById("reportModalTitle"); title.textContent = `Отчет от ${report.reportDate || report.date} - ${ report.storeName || report.storeId || "Неизвестный магазин" }`; content.innerHTML = `

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

Дата: ${ report.date || report.reportDate || "" }
Магазин: ${ report.storeName || report.storeId }
Пользователь: ${ report.username || report.fullName || report.userId }
Статус: ${ report.isVerified || report.verified ? "Проверен" : "Не проверен" }

Доходы (Ingresos)

Income: €${safeToFixed(report.income)}
Caja inicial: €${safeToFixed( report.cajaInicial || report.initialCash )}
Total income: €${safeToFixed( report.totalIncome )}

Зарплаты (Wages)

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

Нет данных о зарплатах

' }

Расходы (Expenses)

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

Нет данных о расходах

' }

Итоговые расчеты

Total income: €${safeToFixed( report.totalIncome )}
Total expenses: €${safeToFixed( report.totalExpenses )}
Envelope: €${safeToFixed( report.envelope )}
Caja final: €${safeToFixed( report.cajaFinal || report.finalCash )}
`; buttons.innerHTML = ""; if (isAdmin) { 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", () => { fillFormWithReport(report); hideModal("reportViewModal"); }); } else { buttons.innerHTML = ` `; } } document .getElementById("closeReportModalBtn") .addEventListener("click", () => { hideModal("reportViewModal"); }); showModal("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 ? "Проверен" : "Не проверен"} `; tbody.appendChild(row); }); showNotification(`Найдено ${filteredReports.length} отчетов`); } function deleteReport(reportId) { showConfirmModal("Вы уверены, что хотите удалить этот отчет?", () => { apiDeleteReport(reportId); loadReports(); updateDashboard(); showNotification("Отчет удален!"); }); } // Заполнение формы данными отчета (для пользователя) function fillFormWithReport(report) { appState.editingReportId = report.id; document.getElementById("storeSelect").value = report.storeId; document.getElementById("income").value = report.income; document.getElementById("cajaInicial").value = report.cajaInicial; document.getElementById("envelope").value = report.envelope; // Заполнение зарплат const wagesContainer = document.getElementById("wagesContainer"); wagesContainer.innerHTML = ""; if (report.wages && report.wages.length > 0) { report.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(); } // Заполнение расходов const expensesContainer = document.getElementById("expensesContainer"); expensesContainer.innerHTML = ""; if (report.expenses && report.expenses.length > 0) { report.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("Нет отчетов для экспорта", "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 || "", Магазин: store.name || report.storeName || "Неизвестно", Доход: `€${Number(report.totalIncome || 0).toFixed(2)}`, Расходы: `€${Number(report.totalExpenses || 0).toFixed(2)}`, Прибыль: `€${profit.toFixed(2)}`, Пользователь: user.username || report.username || "Неизвестно", Статус: report.isVerified ? "Проверен" : report.verified ? "Проверен" : "Не проверен", }; }); if (!data.length) { showNotification("Нет отчетов для экспорта", "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("Отчет экспортирован!"); } //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(); } // Настройка динамических строк function setupDynamicRows() { document.getElementById("addWage").addEventListener("click", () => { addWageRow(); }); document.getElementById("addExpense").addEventListener("click", () => { addExpenseRow(); }); // Обновление при изменении значений 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(); } }); } 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("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); } document.getElementById("reportForm").addEventListener("submit", async (e) => { e.preventDefault(); const formData = { storeId: parseInt(document.getElementById("storeSelect").value), reportDate: new Date().toISOString().split("T")[0], // Or get from input if user chooses date income: parseFloat(document.getElementById("income").value), initialCash: parseFloat(document.getElementById("cajaInicial").value), totalIncome: parseFloat(document.getElementById("totalIncome").value), wages: JSON.stringify(collectWages()), expenses: JSON.stringify(collectExpenses()), totalWages: calculateTotalWages(), totalExpenses: calculateTotalExpenses() + calculateTotalWages(), envelope: parseFloat(document.getElementById("envelope").value), finalCash: parseFloat(document.getElementById("totalIncome").value) - (calculateTotalWages() + calculateTotalExpenses()) - parseFloat(document.getElementById("envelope").value), }; // console.log("Sending report:", formData); const result = await createReport(formData); if (result.success) { showNotification("Отчет успешно создан!"); await loadReports(); document.getElementById("reportForm").reset(); } else { showNotification(result.error || "Ошибка создания отчета", "error"); } }); // Отчет за сегодня для пользователя document.getElementById("todayReportBtn").addEventListener("click", () => { const today = new Date().toISOString().split("T")[0]; const todayReport = database.reports.find( (r) => r.date === today && r.userId === appState.currentUser.id ); if (todayReport) { showReportModal(todayReport, false); // false = не админ режим } else { showNotification("Отчет за сегодня не найден", "error"); } }); //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.stores .map((storeId) => { const store = (appState.storesList || []).find( (s) => s.id === storeId ); return store ? store.name : "Нет доступа"; }) .join(", ") || "Нет доступа"; const row = document.createElement("tr"); row.className = "hover:bg-gray-50"; row.innerHTML = ` ${user.id} ${user.username} ${user.role === "admin" ? "Администратор" : "Сотрудник"} ${userStores} `; tbody.appendChild(row); }); if (users.length === 0) { showNotification("Нет пользователей для отображения", "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 ? "Редактирование пользователя" : "Добавление пользователя"; 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 = ""; 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"); } //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("Заполните логин!", "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("Укажите пароль для нового пользователя!", "error"); return; } // CREATE result = await createUser(userData); if (result.success) { showNotification("Пользователь добавлен!"); } } else { // EDIT result = await updateUser(appState.editingUserId, userData); if (result.success) { showNotification("Пользователь обновлен!"); } appState.editingUserId = null; } // After save: UI update or error if (result && result.success) { hideModal("userEditModal"); loadUsers(); updateDashboard(); } else if (result) { showNotification(result.error || "Ошибка операции", "error"); } } //UI trigger: delete user with modal function deleteUser(userId) { showConfirmModal("Вы уверены, что хотите удалить этого пользователя?", () => 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"); 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("Нет магазинов для отображения", "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("Магазин не найден!", "error"); return; } title.textContent = "Редактирование магазина"; document.getElementById("storeName").value = store.name || ""; showModal("storeEditModal"); } async function saveStore() { const name = document.getElementById("storeName").value.trim(); if (!name) { showNotification("Заполните название магазина!", "error"); return; } let result; if (appState.editingStoreId == null) { // Add result = await createStore({ name }); if (result.success) { showNotification("Магазин добавлен!"); 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("Магазин обновлен!"); 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("Магазин с таким названием уже существует!", "error"); } else { showNotification(result.error || "Ошибка сохранения магазина", "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 = `Вы уверены, что хотите удалить этот магазин "${store.name}"?`; if (store.reportsCount && store.reportsCount > 0) { message += `\n\nОбратите внимание! У этого магазина ${store.reportsCount} есть связынные с ним отчеты. Они также будут удалены.`; } 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 = "Добавление магазина"; 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: "Низкий", medium: "Средний", high: "Высокий", }; 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("Статус задачи обновлен!"); } // Редактирование 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 = "Редактирование задачи"; document.getElementById("todoTitle").value = todo.title; document.getElementById("todoDescription").value = todo.description; document.getElementById("todoPriority").value = todo.priority; } else { title.textContent = "Добавление задачи"; 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("Заполните заголовок!", "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("Задача обновлена!"); } 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("Задача добавлена!"); } hideModal("todoEditModal"); loadTodos(); } // Удаление TODO function deleteTodo(todoId) { if (confirm("Вы уверены, что хотите удалить эту задачу?")) { database.todos = database.todos.filter((t) => t.id !== todoId); loadTodos(); showNotification("Задача удалена!"); } } // Добавление 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(); // ####################################