From 6065339bcd9546adaa5ff2f48a9f0391606e4a9d Mon Sep 17 00:00:00 2001 From: Angie Date: Fri, 1 Aug 2025 06:09:35 +0200 Subject: [PATCH] feat: updated user UI behaviour with report --- frontend/api.js | 74 ++++++- frontend/script.js | 469 +++++++++++++++++++++++++++++++++------------ 2 files changed, 408 insertions(+), 135 deletions(-) diff --git a/frontend/api.js b/frontend/api.js index 7c9c5b8..53652fb 100644 --- a/frontend/api.js +++ b/frontend/api.js @@ -25,6 +25,7 @@ window.appState = createReactiveState( usersList: [], reportsList: [], storesList: [], + todaysReports: [], editingReportId: null, editingUserId: null, editingStoreId: null, @@ -34,6 +35,7 @@ window.appState = createReactiveState( storesChartInstance: null, trendsChartInstance: null, adminTabsInitialized: false, + initialEditReportFormData: null, }, function (prop, value) { // React to changes in critical state @@ -88,6 +90,9 @@ function logout() { document.getElementById("adminInterface").classList.add("hidden"); document.getElementById("loginForm").reset(); document.getElementById("loginError").classList.add("hidden"); + document.getElementById("reportForm").reset(); + if (document.getElementById("storeSelect")) + document.getElementById("storeSelect").selectedIndex = 0; showNotification("Вы вышли из системы", "info"); } @@ -317,6 +322,24 @@ async function getReports() { } } +//GET single report by ID +async function getReportById(reportId) { + const token = localStorage.getItem("token"); + try { + const response = await fetch(`${API_BASE_URL}/reports/${reportId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + const data = await response.json(); + if (response.ok && data.report) { + return { success: true, report: data.report }; + } else { + return { success: false, error: data.error || "Ошибка получения отчета" }; + } + } catch (err) { + return { success: false, error: "Нет соединения с сервером" }; + } +} + //edit report async function updateReport(reportId, data) { const token = localStorage.getItem("token"); @@ -405,7 +428,6 @@ async function apiDeleteReport(reportId) { //REPORTS WORKER //create report -// api.js async function createReport(data) { const token = localStorage.getItem("token"); try { @@ -417,19 +439,53 @@ async function createReport(data) { }, body: JSON.stringify(data), }); - const result = await response.json(); + + let result = {}; + try { + result = await response.json(); + } catch (e) { + // In case response is not JSON + console.error("Could not parse JSON from server:", e); + result = {}; + } + + // Debug: Log response status and result + console.log( + "createReport response.status:", + response.status, + "result:", + result + ); + if (response.ok && result.id) { + // Successfully created return { success: true, id: result.id }; } else { - return { - success: false, - error: - result.error || - (result.errors && result.errors[0]?.msg) || - "Ошибка создания отчета", - }; + // Extract error + let errorMsg = + result.error || + (Array.isArray(result.errors) && result.errors[0]?.msg) || + response.statusText || + "Ошибка создания отчета"; + + if (response.status === 409) { + errorMsg = + "Отчет за этот магазин и дату уже был отправлен этим пользователем."; + } + + // Debug: Log error case + console.warn( + "createReport failed:", + errorMsg, + "Status:", + response.status, + result + ); + + return { success: false, error: errorMsg }; } } catch (err) { + console.error("Network/server error in createReport:", err); return { success: false, error: "Нет соединения с сервером" }; } } diff --git a/frontend/script.js b/frontend/script.js index 0469c1d..a7d0160 100644 --- a/frontend/script.js +++ b/frontend/script.js @@ -6,38 +6,70 @@ function destroyChart(instance) { } // Система уведомлений +// 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"); - 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} -
-
- `; - } + 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"; - setTimeout(() => { + // previous timeout exists -> clear it! + if (notificationTimeout) { + clearTimeout(notificationTimeout); + } + notificationTimeout = setTimeout(() => { notification.classList.add("hidden"); + notification.style.display = "none"; + notificationTimeout = null; }, 3000); } @@ -142,10 +174,19 @@ document.getElementById("loginForm").addEventListener("submit", async (e) => { const result = await loginUser(username, password); if (result.success) { - // Save user/token in JS (or localStorage) - appState.currentUser = result.user; + // Save token for later API calls localStorage.setItem("token", result.token); + // Save user info in global state + appState.currentUser = result.user; + + // Save today's reports if present (for workers), or empty for admin === + if (result.todaysReports) { + appState.todaysReports = result.todaysReports; + } else { + appState.todaysReports = []; + } + // Hide login, show correct UI document.getElementById("loginScreen").classList.add("hidden"); if (result.user.role === "admin") { @@ -157,8 +198,7 @@ document.getElementById("loginForm").addEventListener("submit", async (e) => { showNotification("Успешная авторизация!"); } else { const errorDiv = document.getElementById("loginError"); - // errorDiv.textContent = result.error; - errorDiv.textContent = "Неверный логин или пароль"; + errorDiv.textContent = result.error || "Неверный логин или пароль"; errorDiv.classList.remove("hidden"); } }); @@ -220,6 +260,15 @@ function logout() { 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"); @@ -716,6 +765,24 @@ async function saveEditedReport(reportId) { // Показ модального окна отчета с исправленной прокруткой 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 = []; + } + } + const modal = document.getElementById("reportViewModal"); const content = document.getElementById("reportViewContent"); const buttons = document.getElementById("reportModalButtons"); @@ -772,10 +839,10 @@ function showReportModal(report, isAdmin = false) {

Зарплаты (Wages)

${ - Array.isArray(report.wages) && report.wages.length > 0 + Array.isArray(wages) && wages.length > 0 ? `
- ${report.wages + ${wages .map( (w) => `
@@ -800,10 +867,10 @@ function showReportModal(report, isAdmin = false) {

Расходы (Expenses)

${ - Array.isArray(report.expenses) && report.expenses.length > 0 + Array.isArray(expenses) && expenses.length > 0 ? `
- ${report.expenses + ${expenses .map( (e) => `
@@ -848,9 +915,25 @@ function showReportModal(report, isAdmin = false) { `; buttons.innerHTML = ""; - if (isAdmin) { - buttons.innerHTML = ` + if (report.isVerified || report.verified) { + // Verified: only unverify + close + buttons.innerHTML = ` +
Отчет подтвержден. Для внесения изменений снимите подтверждение.
+ + + `; + document + .getElementById("unverifyReportBtn") + .addEventListener("click", () => { + unverifyReport(report.id); + }); + } else { + buttons.innerHTML = ` @@ -861,37 +944,39 @@ function showReportModal(report, isAdmin = false) { Закрыть `; - - document.getElementById("editReportBtn").addEventListener("click", () => { - editReport(report); - }); - - document.getElementById("verifyReportBtn").addEventListener("click", () => { - verifyReport(report.id); - }); + 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 = ` - - `; +
Отчет подтвержден и не может быть изменен. Обратитесь к администратору для изменений.
+ + `; } } @@ -904,6 +989,14 @@ function showReportModal(report, isAdmin = false) { showModal("reportViewModal"); } +// helpder of the modal of showReportModal - unverify report for ADMIN +async function unverifyReport(reportId) { + await updateReport(reportId, { isVerified: 0 }); + showNotification("Подтверждение снято. Теперь можно редактировать."); + await loadReports(); + hideModal("reportViewModal"); +} + // Настройка фильтров отчетов function setupReportsFilters() { document @@ -999,49 +1092,69 @@ function deleteReport(reportId) { }); } +//USER BEHAVIOUR INSIDE REPORTS // Заполнение формы данными отчета (для пользователя) 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("income").value = report.income || 0; + document.getElementById("cajaInicial").value = + report.cajaInicial || report.initialCash || 0; document.getElementById("envelope").value = report.envelope; - // Заполнение зарплат + // --- 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 (report.wages && report.wages.length > 0) { - report.wages.forEach((wage) => { + 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 (report.expenses && report.expenses.length > 0) { - report.expenses.forEach((expense) => { + 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 { @@ -1186,41 +1299,86 @@ function setupFormCalculations() { cajaInicialInput.addEventListener("input", updateCalculations); envelopeInput.addEventListener("input", updateCalculations); - setupDynamicRows(); + // setupDynamicRows(); } +document.addEventListener("DOMContentLoaded", 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(); +// } +// }); +// } + +let dynamicRowsInitialized = false; + function setupDynamicRows() { - document.getElementById("addWage").addEventListener("click", () => { - addWageRow(); - }); + // 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.getElementById("addExpense").addEventListener("click", () => { - addExpenseRow(); - }); + 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(); + } + }); - // Обновление при изменении значений - document.addEventListener("input", (e) => { - if ( - e.target.classList.contains("wage-amount") || - e.target.classList.contains("expense-amount") - ) { - updateTotals(); - } - }); + dynamicRowsInitialized = true; + } - // Удаление строк - 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(); - } - }); + // 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"); @@ -1286,6 +1444,7 @@ function updateTotals() { const totalIncome = income + cajaInicial; const cajaFinal = totalIncome - totalExpenses - envelope; + document.getElementById("totalIncome").value = totalIncome.toFixed(2); // <-- Add this line! document.getElementById("cajaFinal").textContent = cajaFinal.toFixed(2); } @@ -1312,47 +1471,104 @@ function safeToFixed(value, digits = 2) { return (Number(value) || 0).toFixed(digits); } +// 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 totalExpensesValue = calculateTotalExpenses() + totalWagesValue; + const finalCashValue = totalIncomeValue - totalExpensesValue - envelopeValue; + 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), + reportDate: 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: calculateTotalWages(), - totalExpenses: calculateTotalExpenses() + calculateTotalWages(), - envelope: parseFloat(document.getElementById("envelope").value), - finalCash: - parseFloat(document.getElementById("totalIncome").value) - - (calculateTotalWages() + calculateTotalExpenses()) - - parseFloat(document.getElementById("envelope").value), + totalWages: isNaN(totalWagesValue) ? 0 : totalWagesValue, + totalExpenses: isNaN(totalExpensesValue) ? 0 : totalExpensesValue, + envelope: isNaN(envelopeValue) ? 0 : envelopeValue, + finalCash: isNaN(finalCashValue) ? 0 : finalCashValue, }; - // console.log("Sending report:", formData); - const result = await createReport(formData); - if (result.success) { - showNotification("Отчет успешно создан!"); - await loadReports(); - document.getElementById("reportForm").reset(); + + 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("Нет ответа от сервера. Попробуйте еще раз.", "error"); + return; + } + + if (result.success) { + const wasEdit = !!appState.editingReportId; + appState.editingReportId = null; + window.scrollTo({ top: 0, behavior: "smooth" }); + document.getElementById("reportForm").reset(); + document.getElementById("wagesContainer").innerHTML = ""; + addWageRow(); + + document.getElementById("expensesContainer").innerHTML = ""; + addExpenseRow(); + + showNotification( + wasEdit ? "Отчет успешно отредактирован!" : "Отчет успешно создан!" + ); + await loadReports(); + refreshTodaysReports(); + console.log("Updated todaysReports:", appState.todaysReports); + } else { + window.scrollTo({ top: 0, behavior: "smooth" }); + document.getElementById("reportForm").reset(); showNotification(result.error || "Ошибка создания отчета", "error"); } }); +//helper for USER UI +function refreshTodaysReports() { + const today = new Date().toISOString().split("T")[0]; + appState.todaysReports = (appState.reportsList || []).filter( + (r) => (r.reportDate || r.date) === today + ); +} + // Отчет за сегодня для пользователя 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 + const storeId = document.getElementById("storeSelect").value; + if (!storeId) { + showNotification("Пожалуйста, выберите магазин!", "error"); + return; + } + const report = (appState.todaysReports || []).find( + (r) => String(r.storeId) === String(storeId) ); - - if (todayReport) { - showReportModal(todayReport, false); // false = не админ режим + if (report) { + showReportModal(report, false); } else { - showNotification("Отчет за сегодня не найден", "error"); + showNotification("Сегодняшний отчет еще не создан", "error"); + document.getElementById("reportForm").reset(); + document.getElementById("storeSelect").value = storeId; + + document.getElementById("wagesContainer").innerHTML = ""; + addWageRow(); + + document.getElementById("expensesContainer").innerHTML = ""; + addExpenseRow(); } }); @@ -1545,6 +1761,7 @@ async function saveUser() { hideModal("userEditModal"); loadUsers(); updateDashboard(); + loadReports(); } else if (result) { showNotification(result.error || "Ошибка операции", "error"); }