2378 lines
76 KiB
JavaScript
2378 lines
76 KiB
JavaScript
//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,
|
||
"""
|
||
);
|
||
|
||
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();
|
||
|
||
// ####################################
|