feat: updated user UI behaviour with report

This commit is contained in:
Angie
2025-08-01 06:09:35 +02:00
parent 3fed754570
commit 6065339bcd
2 changed files with 408 additions and 135 deletions

View File

@@ -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: "Нет соединения с сервером" };
}
}

View File

@@ -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 = `
// <div class="bg-red-500 text-white px-6 py-4 rounded-lg shadow-lg">
// <div class="flex items-center">
// <i class="fas fa-exclamation-triangle mr-2"></i>
// <span>${message}</span>
// </div>
// </div>
// `;
// } else {
// notification.innerHTML = `
// <div class="bg-green-500 text-white px-6 py-4 rounded-lg shadow-lg">
// <div class="flex items-center">
// <i class="fas fa-check-circle mr-2"></i>
// <span>${message}</span>
// </div>
// </div>
// `;
// }
// 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 = `
<div class="bg-red-500 text-white px-6 py-4 rounded-lg shadow-lg">
<div class="flex items-center">
<i class="fas fa-exclamation-triangle mr-2"></i>
<span>${message}</span>
</div>
</div>
`;
} else {
notification.innerHTML = `
<div class="bg-green-500 text-white px-6 py-4 rounded-lg shadow-lg">
<div class="flex items-center">
<i class="fas fa-check-circle mr-2"></i>
<span>${message}</span>
</div>
</div>
`;
}
if (!notification) return;
notification.innerHTML = `
<div class="bg-${
type === "error" ? "red" : "green"
}-500 text-white px-6 py-4 rounded-lg shadow-lg">
<div class="flex items-center">
<i class="fas ${
type === "error" ? "fa-exclamation-triangle" : "fa-check-circle"
} mr-2"></i>
<span>${message}</span>
</div>
</div>
`;
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) {
<div class="report-section wages">
<h4 class="font-bold text-yellow-700 mb-3"><i class="fas fa-users mr-2"></i>Зарплаты (Wages)</h4>
${
Array.isArray(report.wages) && report.wages.length > 0
Array.isArray(wages) && wages.length > 0
? `
<div class="space-y-2">
${report.wages
${wages
.map(
(w) => `
<div class="flex justify-between text-sm">
@@ -800,10 +867,10 @@ function showReportModal(report, isAdmin = false) {
<div class="report-section expenses">
<h4 class="font-bold text-red-700 mb-3"><i class="fas fa-arrow-down mr-2"></i>Расходы (Expenses)</h4>
${
Array.isArray(report.expenses) && report.expenses.length > 0
Array.isArray(expenses) && expenses.length > 0
? `
<div class="space-y-2">
${report.expenses
${expenses
.map(
(e) => `
<div class="flex justify-between text-sm">
@@ -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 = `
<div class="mb-2 text-yellow-700 font-medium text-sm">Отчет подтвержден. Для внесения изменений снимите подтверждение.</div>
<button id="unverifyReportBtn" class="bg-yellow-500 text-white px-4 py-2 rounded-lg hover:bg-yellow-600">
<i class="fas fa-undo mr-2"></i>Снять подтверждение
</button>
<button id="closeReportModalBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg">
<i class="fas fa-times mr-2"></i>Закрыть
</button>
`;
document
.getElementById("unverifyReportBtn")
.addEventListener("click", () => {
unverifyReport(report.id);
});
} else {
buttons.innerHTML = `
<button id="editReportBtn" class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
<i class="fas fa-edit mr-2"></i>Редактировать
</button>
@@ -861,37 +944,39 @@ function showReportModal(report, isAdmin = false) {
<i class="fas fa-times mr-2"></i>Закрыть
</button>
`;
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 = `
<button id="editReportUserBtn" class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
<i class="fas fa-edit mr-2"></i>Редактировать
</button>
<button id="closeReportModalBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg">
<i class="fas fa-times mr-2"></i>Закрыть
</button>
`;
<button id="editReportUserBtn" class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
<i class="fas fa-edit mr-2"></i>Редактировать
</button>
<button id="closeReportModalBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg">
<i class="fas fa-times mr-2"></i>Закрыть
</button>
`;
document
.getElementById("editReportUserBtn")
.addEventListener("click", () => {
appState.editingReportId = report.id;
fillFormWithReport(report);
hideModal("reportViewModal");
});
} else {
buttons.innerHTML = `
<button id="closeReportModalBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg">
<i class="fas fa-times mr-2"></i>Закрыть
</button>
`;
<div class="mb-2 text-yellow-700 font-medium text-sm">Отчет подтвержден и не может быть изменен. Обратитесь к администратору для изменений.</div>
<button id="closeReportModalBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg">
<i class="fas fa-times mr-2"></i>Закрыть
</button>
`;
}
}
@@ -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 = `
<input type="text" placeholder="Имя сотрудника" value="${wage.name}" class="wage-name form-input px-3 py-2 rounded-lg">
<input type="number" step="0.01" placeholder="Сумма €" value="${wage.amount}" class="wage-amount form-input px-3 py-2 rounded-lg">
<button type="button" class="remove-wage bg-red-500 text-white px-3 py-2 rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-times"></i>
</button>
`;
<input type="text" placeholder="Имя сотрудника" value="${wage.name}" class="wage-name form-input px-3 py-2 rounded-lg">
<input type="number" step="0.01" placeholder="Сумма €" value="${wage.amount}" class="wage-amount form-input px-3 py-2 rounded-lg">
<button type="button" class="remove-wage bg-red-500 text-white px-3 py-2 rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-times"></i>
</button>
`;
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 = `
<input type="text" placeholder="Название расхода" value="${expense.name}" class="expense-name form-input px-3 py-2 rounded-lg">
<input type="number" step="0.01" placeholder="Сумма €" value="${expense.amount}" class="expense-amount form-input px-3 py-2 rounded-lg">
<button type="button" class="remove-expense bg-red-500 text-white px-3 py-2 rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-times"></i>
</button>
`;
<input type="text" placeholder="Название расхода" value="${expense.name}" class="expense-name form-input px-3 py-2 rounded-lg">
<input type="number" step="0.01" placeholder="Сумма €" value="${expense.amount}" class="expense-amount form-input px-3 py-2 rounded-lg">
<button type="button" class="remove-expense bg-red-500 text-white px-3 py-2 rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-times"></i>
</button>
`;
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");
}