cash-report-system/frontend/script.js

2378 lines
76 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//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 = `
// <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");
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";
// 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 = `
<option value="">Все магазины</option>
${stores
.map((store) => `<option value="${store.id}">${store.name}</option>`)
.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 = `
<td class="px-6 py-4 text-sm text-gray-900">${
report.reportDate || report.date || ""
}</td>
<td class="px-6 py-4 text-sm text-gray-900">${storeName}</td>
<td class="px-6 py-4 text-sm text-gray-900">€${Number(
report.totalIncome
).toFixed(2)}</td>
<td class="px-6 py-4 text-sm text-gray-900">€${Number(
report.totalExpenses
).toFixed(2)}</td>
<td class="px-6 py-4 text-sm ${
profit >= 0 ? "text-green-600" : "text-red-600"
}">€${profit.toFixed(2)}</td>
<td class="px-6 py-4 text-sm text-gray-900">${
report.username || report.userId
}</td>
<td class="px-6 py-4">
<span class="px-2 py-1 text-xs rounded-full ${
report.isVerified
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}">
${report.isVerified ? "Verificado" : "No verificado"}
</span>
</td>
<td class="px-6 py-4 text-sm">
<button class="text-blue-600 hover:text-blue-900 mr-2" onclick="viewReport(${
report.id
})">
<i class="fas fa-eye"></i> Ver
</button>
<button class="text-red-600 hover:text-red-900" onclick="deleteReport(${
report.id
})">
<i class="fas fa-trash"></i> Eliminar
</button>
</td>
`;
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,
"&quot;"
);
content.innerHTML = `
<form id="editReportForm" class="space-y-6">
<!-- Основная информация -->
<div class="report-section">
<h4 class="font-bold text-gray-700 mb-3"><i class="fas fa-info-circle mr-2"></i>Основная информация</h4>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">Магазин</label>
<input
type="text"
value="${shopName}"
class="form-input w-full px-3 py-2 rounded-lg bg-gray-100 text-gray-900 cursor-not-allowed"
readonly
disabled
>
<input type="hidden" id="editStoreSelect" value="${report.storeId}">
</div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">Дата</label>
<input
type="date"
id="editDate"
value="${report.reportDate || report.date || ""}"
class="form-input w-full px-3 py-2 rounded-lg bg-gray-100 text-gray-900 cursor-not-allowed"
readonly
disabled
>
</div>
</div>
</div>
<!-- Доходы -->
<div class="report-section income">
<h4 class="font-bold text-green-700 mb-3"><i class="fas fa-arrow-up mr-2"></i>Доходы</h4>
<div class="grid grid-cols-3 gap-4">
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">Income</label>
<input type="number" id="editIncome" value="${
report.income || ""
}" step="0.01" class="form-input w-full px-3 py-2 rounded-lg">
</div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">Caja inicial</label>
<input type="number" id="editCajaInicial" value="${
report.initialCash || report.cajaInicial || ""
}" step="0.01" class="form-input w-full px-3 py-2 rounded-lg">
</div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">Envelope</label>
<input type="number" id="editEnvelope" value="${
report.envelope || ""
}" step="0.01" class="form-input w-full px-3 py-2 rounded-lg">
</div>
</div>
</div>
<!-- Статус проверки -->
<div class="report-section">
<h4 class="font-bold text-gray-700 mb-3"><i class="fas fa-check mr-2"></i>Статус</h4>
<label class="flex items-center">
<input type="checkbox" id="editVerified" ${
report.isVerified || report.verified ? "checked" : ""
} class="mr-2">
<span>Отчет проверен</span>
</label>
</div>
</form>
`;
buttons.innerHTML = `
<button id="saveReportBtn" class="btn-success text-white px-4 py-2 rounded-lg">
<i class="fas fa-save mr-2"></i>Guardar
</button>
<button id="cancelEditBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg">
<i class="fas fa-times mr-2"></i>Cancelar
</button>
`;
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 = `
<div class="space-y-6">
<!-- Información básica -->
<div class="report-section">
<h4 class="font-bold text-gray-700 mb-3"><i class="fas fa-info-circle mr-2"></i>Información básica</h4>
<div class="grid grid-cols-2 gap-4 text-sm">
<div><strong>Fecha:</strong> ${
report.date || report.reportDate || ""
}</div>
<div><strong>Tienda:</strong> ${
report.storeName || report.storeId
}</div>
<div><strong>Usuario:</strong> ${
report.username || report.fullName || report.userId
}</div>
<div><strong>Estado:</strong>
<span class="px-2 py-1 rounded text-xs ${
report.isVerified || report.verified
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}">
${
report.isVerified || report.verified
? "Verificado"
: "No verificado"
}
</span>
</div>
</div>
</div>
<!-- Ingresos -->
<div class="report-section income">
<h4 class="font-bold text-green-700 mb-3"><i class="fas fa-arrow-up mr-2"></i>Ingresos</h4>
<div class="grid grid-cols-3 gap-4 text-sm">
<div><strong>Ingresos del día:</strong> €${safeToFixed(report.income)}</div>
<div><strong>Caja inicial:</strong> €${safeToFixed(
report.cajaInicial || report.initialCash
)}</div>
<div><strong>Ingresos totales:</strong> €${safeToFixed(
report.totalIncome
)}</div>
</div>
</div>
<!-- Salarios -->
<div class="report-section wages">
<h4 class="font-bold text-yellow-700 mb-3"><i class="fas fa-users mr-2"></i>Salarios</h4>
${
Array.isArray(wages) && wages.length > 0
? `
<div class="space-y-2">
${wages
.map(
(w) => `
<div class="flex justify-between text-sm">
<span>${w.name}</span>
<span>€${safeToFixed(w.amount)}</span>
</div>
`
)
.join("")}
<div class="border-t pt-2 font-bold">
<div class="flex justify-between">
<span>Total salarios:</span>
<span>€${safeToFixed(report.totalWages)}</span>
</div>
</div>
</div>
`
: '<p class="text-gray-500 text-sm">No hay datos de salarios</p>'
}
</div>
<!-- Gastos -->
<div class="report-section expenses">
<h4 class="font-bold text-red-700 mb-3"><i class="fas fa-arrow-down mr-2"></i>Gastos</h4>
${
Array.isArray(expenses) && expenses.length > 0
? `
<div class="space-y-2">
${expenses
.map(
(e) => `
<div class="flex justify-between text-sm">
<span>${e.name}</span>
<span>€${safeToFixed(e.amount)}</span>
</div>
`
)
.join("")}
<div class="border-t pt-2 font-bold">
<div class="flex justify-between">
<span>Total gastos internos:</span>
<span>€${safeToFixed(totalExpensesInternal)}</span>
</div>
</div>
</div>
`
: '<p class="text-gray-500 text-sm">No hay datos de gastos</p>'
}
</div>
<!-- Resumen final -->
<div class="report-section">
<h4 class="font-bold text-gray-700 mb-3"><i class="fas fa-calculator mr-2"></i>Resumen final</h4>
<div class="space-y-2">
<div class="flex justify-between"><strong>Ingresos totales:</strong> <span>€${safeToFixed(
report.totalIncome
)}</span></div>
<div class="flex justify-between"><strong>Gastos totales:</strong> <span>€${safeToFixed(
report.totalExpenses
)}</span></div>
<div class="flex justify-between"><strong>Envelope:</strong> <span>€${safeToFixed(
report.envelope
)}</span></div>
<div class="flex justify-between border-t pt-2 text-lg font-bold text-blue-700">
<strong>Caja final:</strong> <span>€${safeToFixed(
report.cajaFinal || report.finalCash
)}</span>
</div>
</div>
</div>
</div>
`;
buttons.innerHTML = "";
if (isAdmin) {
if (report.isVerified || report.verified) {
// Verified: only unverify + close
buttons.innerHTML = `
<div class="grid grid-cols-2 items-center justify-between gap-4">
<div class="text-yellow-700 font-medium text-sm">
Informe verificado.<br>Para hacer cambios, retire la verificación.
</div>
<div class="flex justify-end gap-3">
<button id="unverifyReportBtn" class="bg-yellow-500 text-white px-4 py-2 rounded-lg hover:bg-yellow-600 flex items-center">
<i class="fas fa-undo mr-2"></i>Retirar verificación
</button>
<button id="closeReportModalBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg flex items-center">
<i class="fas fa-times mr-2"></i>Cerrar
</button>
</div>
</div>
`;
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>Editar
</button>
<button id="verifyReportBtn" class="btn-success text-white px-4 py-2 rounded-lg">
<i class="fas fa-check mr-2"></i>Verificar informe
</button>
<button id="closeReportModalBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg">
<i class="fas fa-times mr-2"></i>Cerrar
</button>
`;
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>Editar
</button>
<button id="closeReportModalBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg">
<i class="fas fa-times mr-2"></i>Cerrar
</button>
`;
document
.getElementById("editReportUserBtn")
.addEventListener("click", () => {
appState.editingReportId = report.id;
fillFormWithReport(report);
hideModal("reportViewModal");
});
} else {
buttons.innerHTML = `
<div class="mb-2 text-yellow-700 font-medium text-sm">El informe está verificado y no se puede modificar. Contacte al administrador para cambios.</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>
`;
}
}
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 = `
<td class="px-6 py-4 text-sm text-gray-900">${
report.reportDate || report.date || ""
}</td>
<td class="px-6 py-4 text-sm text-gray-900">${storeName}</td>
<td class="px-6 py-4 text-sm text-gray-900">€${report.totalIncome.toFixed(
2
)}</td>
<td class="px-6 py-4 text-sm text-gray-900">€${report.totalExpenses.toFixed(
2
)}</td>
<td class="px-6 py-4 text-sm ${
profit >= 0 ? "text-green-600" : "text-red-600"
}">€${profit.toFixed(2)}</td>
<td class="px-6 py-4 text-sm text-gray-900">${username}</td>
<td class="px-6 py-4">
<span class="px-2 py-1 text-xs rounded-full ${
report.isVerified
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}">
${report.isVerified ? "Verificado" : "No verificado"}
</span>
</td>
<td class="px-6 py-4 text-sm">
<button class="text-blue-600 hover:text-blue-900 mr-2" onclick="viewReport(${
report.id
})">
<i class="fas fa-eye"></i> Ver
</button>
<button class="text-red-600 hover:text-red-900" onclick="deleteReport(${
report.id
})">
<i class="fas fa-trash"></i> Eliminar
</button>
</td>
`;
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 = `
<input type="text" placeholder="Nombre del empleado" value="${wage.name}" class="wage-name form-input px-3 py-2 rounded-lg">
<input type="number" step="0.01" placeholder="Cantidad €" 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 (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="Nombre del gasto" value="${expense.name}" class="expense-name form-input px-3 py-2 rounded-lg">
<input type="number" step="0.01" placeholder="Cantidad €" 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 {
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 =
'<option value="" disabled selected hidden>Seleccione tienda</option>';
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 = `
<input type="text" placeholder="Nombre del empleado" class="wage-name form-input px-3 py-2 rounded-lg">
<input type="number" step="0.01" placeholder="Cantidad €" 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>
`;
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 = `
<input type="text" placeholder="Nombre del gasto" class="expense-name form-input px-3 py-2 rounded-lg">
<input type="number" step="0.01" placeholder="Cantidad €" 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>
`;
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 = `
<td class="px-6 py-4 text-sm text-gray-900">${user.id}</td>
<td class="px-6 py-4 text-sm text-gray-900">${user.username}</td>
<td class="px-6 py-4 text-sm">
<span class="px-2 py-1 text-xs rounded-full ${
user.role === "admin"
? "bg-purple-100 text-purple-800"
: "bg-blue-100 text-blue-800"
}">
${user.role === "admin" ? "Administrador" : "Empleado"}
</span>
</td>
<td class="px-6 py-4 text-sm text-gray-900">${userStores}</td>
<td class="px-6 py-4 text-sm">
<button class="text-blue-600 hover:text-blue-900 mr-2" onclick="editUser(${
user.id
})">
<i class="fas fa-edit"></i> Editar
</button>
<button class="text-red-600 hover:text-red-900" onclick="deleteUser(${
user.id
})">
<i class="fas fa-trash"></i> Eliminar
</button>
</td>
`;
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 = `
<input type="checkbox" value="${store.id}" ${
isChecked ? "checked" : ""
} class="mr-2">
<span>${store.name}</span>
`;
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 = `
<td class="px-6 py-4 text-sm text-gray-900">${store.id}</td>
<td class="px-6 py-4 text-sm text-gray-900">${store.name}</td>
<td class="px-6 py-4 text-sm text-gray-900">${
store.reportsCount || 0
}</td>
<td class="px-6 py-4 text-sm">
<button class="text-blue-600 hover:text-blue-900 mr-2" onclick="editStore(${
store.id
})">
<i class="fas fa-edit"></i> Editar
</button>
<button class="text-red-600 hover:text-red-900" onclick="deleteStore(${
store.id
})">
<i class="fas fa-trash"></i> Eliminar
</button>
</td>
`;
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 = `
<div class="flex items-start justify-between">
<div class="flex items-start space-x-3 flex-1">
<input type="checkbox" ${
todo.completed ? "checked" : ""
}
onchange="toggleTodo(${todo.id})"
class="mt-1">
<div class="flex-1">
<h4 class="font-medium ${
todo.completed
? "line-through text-gray-500"
: "text-gray-900"
}">${todo.title}</h4>
<p class="text-sm text-gray-600 mt-1">${
todo.description
}</p>
<div class="flex items-center space-x-2 mt-2">
<span class="px-2 py-1 text-xs rounded-full ${
priorityColors[todo.priority]
}">
${priorityText[todo.priority]}
</span>
<span class="text-xs text-gray-500">${
todo.createdAt
}</span>
</div>
</div>
</div>
<div class="flex space-x-2">
<button onclick="editTodo(${
todo.id
})" class="text-blue-600 hover:text-blue-900">
<i class="fas fa-edit"></i>
</button>
<button onclick="deleteTodo(${
todo.id
})" class="text-red-600 hover:text-red-900">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
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();
// ####################################