cash-report-system/frontend/script.js

2028 lines
67 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
//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);
}
// Управление модальными окнами с исправленной прокруткой
function showModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add("show");
// Блокируем прокрутку body
document.body.classList.add("modal-open");
}
}
function hideModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove("show");
// Разблокируем прокрутку body
document.body.classList.remove("modal-open");
}
}
// Настройка вкладок администратора
function setupAdminTabs() {
if (appState.adminTabsInitialized) return;
appState.adminTabsInitialized = true;
const tabButtons = document.querySelectorAll(".admin-tab-btn");
const tabContents = document.querySelectorAll(".admin-tab-content");
tabButtons.forEach((button) => {
button.addEventListener("click", () => {
const tabId = button.dataset.tab;
// Переключение активной вкладки
tabButtons.forEach((btn) => {
btn.className =
"admin-tab-btn px-6 py-3 font-medium text-gray-600 hover:text-blue-600 transition-colors border-b-2 border-transparent hover:border-blue-500";
});
button.className =
"admin-tab-btn px-6 py-3 font-medium transition-colors border-b-2 border-blue-500 text-blue-600";
// Показ/скрытие содержимого
tabContents.forEach((content) => {
content.classList.add("hidden");
});
document.getElementById(tabId + "Tab").classList.remove("hidden");
if (tabId === "dashboard") updateDashboard();
// Optionally, reload todos for TODO tab
if (tabId === "todo") loadTodos();
// Загрузка данных при переключении
// switch (tabId) {
// case "dashboard":
// updateDashboard();
// break;
// case "reports":
// loadReports();
// break;
// case "users":
// loadUsers();
// break;
// case "stores":
// loadStores();
// break;
// case "todo":
// loadTodos();
// break;
// }
});
});
}
// Закрытие модального окна при клике вне его
document.addEventListener("click", (e) => {
if (e.target.classList.contains("modal")) {
hideModal(e.target.id);
}
});
// Глобальные функции для onclick обработчиков
window.viewReport = viewReport;
window.deleteReport = deleteReport;
window.editUser = editUser;
window.deleteUser = deleteUser;
window.editStore = editStore;
window.deleteStore = deleteStore;
window.editTodo = editTodo;
window.deleteTodo = deleteTodo;
window.toggleTodo = toggleTodo;
//AUTH
// Обработчики авторизации
document.getElementById("loginForm").addEventListener("submit", async (e) => {
e.preventDefault();
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
// Call backend login
const result = await loginUser(username, password);
if (result.success) {
// Save user/token in JS (or localStorage)
appState.currentUser = result.user;
localStorage.setItem("token", result.token);
document.getElementById("loginScreen").classList.add("hidden");
if (result.user.role === "admin") {
showAdminInterface();
} else {
showUserInterface();
}
showNotification("Успешная авторизация!");
} else {
const errorDiv = document.getElementById("loginError");
// errorDiv.textContent = result.error;
errorDiv.textContent = "Неверный логин или пароль";
errorDiv.classList.remove("hidden");
}
});
// Показать интерфейс пользователя
function showUserInterface() {
document.getElementById("userInterface").classList.remove("hidden");
document.getElementById(
"userWelcome"
).textContent = `Добро пожаловать, ${appState.currentUser.username}!`;
loadUserStores();
setupFormCalculations();
}
// Показать интерфейс администратора
async function showAdminInterface() {
document.getElementById("adminInterface").classList.remove("hidden");
document.getElementById(
"adminWelcome"
).textContent = `Добро пожаловать, ${appState.currentUser.username}!`;
await Promise.all([loadUsers(), loadStores(), 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");
document.getElementById("loginForm").reset();
document.getElementById("loginError").classList.add("hidden");
showNotification("Вы вышли из системы");
}
//DASHBOARD
// Обновление дашборда
function updateDashboard() {
const reports = appState.reportsList || [];
const users = appState.usersList || [];
// Расчет статистики
const totalRevenue = reports.reduce((sum, r) => sum + r.totalIncome, 0);
const totalExpenses = reports.reduce((sum, r) => sum + r.totalExpenses, 0);
const totalReports = reports.length;
const totalUsers = users.length;
// Обновление карточек
document.getElementById(
"totalRevenueCard"
).textContent = `${totalRevenue.toFixed(2)}`;
document.getElementById(
"totalExpensesCard"
).textContent = `${totalExpenses.toFixed(2)}`;
document.getElementById("totalReportsCard").textContent = totalReports;
document.getElementById("totalUsersCard").textContent = totalUsers;
// Создание графиков
createCharts();
}
// Создание графиков
function createCharts() {
// График доходов по дням
const reports = appState.reportsList || [];
const stores = appState.storesList || [];
const revenueCtx = document.getElementById("revenueChart");
if (revenueCtx) {
destroyChart(appState.revenueChartInstance);
const last7Days = [];
const revenueData = [];
for (let i = 6; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split("T")[0];
last7Days.push(dateStr);
const dayReports = reports.filter(
(r) => (r.reportDate || r.date) === dateStr
);
const dayRevenue = dayReports.reduce(
(sum, r) => sum + (Number(r.totalIncome) || 0),
0
);
revenueData.push(dayRevenue);
}
appState.revenueChartInstance = new Chart(revenueCtx, {
type: "line",
data: {
labels: last7Days,
datasets: [
{
label: "Доходы",
data: revenueData,
borderColor: "rgb(59, 130, 246)",
backgroundColor: "rgba(59, 130, 246, 0.1)",
tension: 0.4,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function (value) {
return "€" + value.toFixed(0);
},
},
},
},
},
});
}
// Круговая диаграмма расходов
const expensesCtx = document.getElementById("expensesChart");
if (expensesCtx) {
destroyChart(appState.expensesChartInstance);
const totalWages = reports.reduce(
(sum, r) => sum + (Number(r.totalWages) || 0),
0
);
const totalInternal = reports.reduce(
(sum, r) => sum + (Number(r.totalExpenses) || 0),
0
);
appState.expensesChartInstance = new Chart(expensesCtx, {
type: "doughnut",
data: {
labels: ["Зарплаты", "Прочие расходы"],
datasets: [
{
data: [totalWages, totalInternal],
backgroundColor: ["#F59E0B", "#EF4444"],
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "bottom",
},
},
},
});
}
// График по магазинам
const storesCtx = document.getElementById("storesChart");
if (storesCtx) {
destroyChart(appState.storesChartInstance);
const storeData = stores.map((store) => {
const storeReports = reports.filter((r) => r.storeId === store.id);
const revenue = storeReports.reduce((sum, r) => sum + r.totalIncome, 0);
return { name: store.name, revenue };
});
console.log("storeData for bar chart:", storeData);
appState.storesChartInstance = new Chart(storesCtx, {
type: "bar",
data: {
labels: storeData.map((s) => s.name),
datasets: [
{
label: "Доходы по магазинам",
data: storeData.map((s) => s.revenue),
backgroundColor: "#10B981",
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function (value) {
return "€" + value.toFixed(0);
},
},
},
},
},
});
}
// Тренды продаж
const trendsCtx = document.getElementById("trendsChart");
if (trendsCtx) {
destroyChart(appState.trendsChartInstance);
const last30Days = [];
const profitData = [];
for (let i = 29; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split("T")[0];
last30Days.push(dateStr);
const dayReports = reports.filter(
(r) => (r.reportDate || r.date) === dateStr
);
const dayProfit = dayReports.reduce(
(sum, r) =>
sum + ((Number(r.totalIncome) || 0) - (Number(r.totalExpenses) || 0)),
0
);
profitData.push(dayProfit);
}
appState.trendsChartInstance = new Chart(trendsCtx, {
type: "line",
data: {
labels: last30Days,
datasets: [
{
label: "Прибыль",
data: profitData,
borderColor: "#8B5CF6",
backgroundColor: "rgba(139, 92, 246, 0.1)",
tension: 0.4,
fill: true,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function (value) {
return "€" + value.toFixed(0);
},
},
},
},
},
});
}
}
//REPORTS
async function loadReports() {
if (!appState.storesList || appState.storesList.length === 0) {
await loadStores();
}
const tbody = document.getElementById("reportsTableBody");
const filterStore = document.getElementById("filterStore");
const result = await getReports();
console.log("getReports() result:", result);
appState.reportsList = result.success ? result.reports : [];
if (!result.success) {
showNotification(result.error || "Ошибка загрузки отчетов", "error");
return;
}
const reports = result.reports;
const stores = appState.storesList || [];
filterStore.innerHTML = `
<option value="">Все магазины</option>
${stores
.map((store) => `<option value="${store.id}">${store.name}</option>`)
.join("")}
`;
tbody.innerHTML = "";
reports.forEach((report) => {
const profit =
(Number(report.totalIncome) || 0) - (Number(report.totalExpenses) || 0);
const store = stores.find((s) => Number(s.id) === Number(report.storeId));
const storeName = store ? store.name : report.storeName || report.storeId;
const row = document.createElement("tr");
row.className = "hover:bg-gray-50";
row.innerHTML = `
<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 ? "Проверен" : "Не проверен"}
</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> Просмотр
</button>
<button class="text-red-600 hover:text-red-900" onclick="deleteReport(${
report.id
})">
<i class="fas fa-trash"></i> Удалить
</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);
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>Сохранить
</button>
<button id="cancelEditBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg">
<i class="fas fa-times mr-2"></i>Отмена
</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) {
const modal = document.getElementById("reportViewModal");
const content = document.getElementById("reportViewContent");
const buttons = document.getElementById("reportModalButtons");
const title = document.getElementById("reportModalTitle");
title.textContent = `Отчет от ${report.reportDate || report.date} - ${
report.storeName || report.storeId || "Неизвестный магазин"
}`;
content.innerHTML = `
<div 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 text-sm">
<div><strong>Дата:</strong> ${
report.date || report.reportDate || ""
}</div>
<div><strong>Магазин:</strong> ${
report.storeName || report.storeId
}</div>
<div><strong>Пользователь:</strong> ${
report.username || report.fullName || report.userId
}</div>
<div><strong>Статус:</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
? "Проверен"
: "Не проверен"
}
</span>
</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>Доходы (Ingresos)</h4>
<div class="grid grid-cols-3 gap-4 text-sm">
<div><strong>Income:</strong> €${safeToFixed(report.income)}</div>
<div><strong>Caja inicial:</strong> €${safeToFixed(
report.cajaInicial || report.initialCash
)}</div>
<div><strong>Total income:</strong> €${safeToFixed(
report.totalIncome
)}</div>
</div>
</div>
<!-- Зарплаты -->
<div class="report-section wages">
<h4 class="font-bold text-yellow-700 mb-3"><i class="fas fa-users mr-2"></i>Зарплаты (Wages)</h4>
${
Array.isArray(report.wages) && report.wages.length > 0
? `
<div class="space-y-2">
${report.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 wages:</span>
<span>€${safeToFixed(report.totalWages)}</span>
</div>
</div>
</div>
`
: '<p class="text-gray-500 text-sm">Нет данных о зарплатах</p>'
}
</div>
<!-- Расходы -->
<div class="report-section expenses">
<h4 class="font-bold text-red-700 mb-3"><i class="fas fa-arrow-down mr-2"></i>Расходы (Expenses)</h4>
${
Array.isArray(report.expenses) && report.expenses.length > 0
? `
<div class="space-y-2">
${report.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 expenses internal:</span>
<span>€${safeToFixed(report.totalExpensesInternal)}</span>
</div>
</div>
</div>
`
: '<p class="text-gray-500 text-sm">Нет данных о расходах</p>'
}
</div>
<!-- Итоговые расчеты -->
<div class="report-section">
<h4 class="font-bold text-gray-700 mb-3"><i class="fas fa-calculator mr-2"></i>Итоговые расчеты</h4>
<div class="space-y-2">
<div class="flex justify-between"><strong>Total income:</strong> <span>€${safeToFixed(
report.totalIncome
)}</span></div>
<div class="flex justify-between"><strong>Total expenses:</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) {
buttons.innerHTML = `
<button id="editReportBtn" class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
<i class="fas fa-edit mr-2"></i>Редактировать
</button>
<button id="verifyReportBtn" class="btn-success text-white px-4 py-2 rounded-lg">
<i class="fas fa-check mr-2"></i>Подтвердить отчет
</button>
<button id="closeReportModalBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg">
<i class="fas fa-times mr-2"></i>Закрыть
</button>
`;
document.getElementById("editReportBtn").addEventListener("click", () => {
editReport(report);
});
document.getElementById("verifyReportBtn").addEventListener("click", () => {
verifyReport(report.id);
});
} else {
if (!report.isVerified && !report.verified) {
buttons.innerHTML = `
<button id="editReportUserBtn" class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
<i class="fas fa-edit mr-2"></i>Редактировать
</button>
<button id="closeReportModalBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg">
<i class="fas fa-times mr-2"></i>Закрыть
</button>
`;
document
.getElementById("editReportUserBtn")
.addEventListener("click", () => {
fillFormWithReport(report);
hideModal("reportViewModal");
});
} else {
buttons.innerHTML = `
<button id="closeReportModalBtn" class="bg-gray-500 text-white px-4 py-2 rounded-lg">
<i class="fas fa-times mr-2"></i>Закрыть
</button>
`;
}
}
document
.getElementById("closeReportModalBtn")
.addEventListener("click", () => {
hideModal("reportViewModal");
});
showModal("reportViewModal");
}
// Настройка фильтров отчетов
function setupReportsFilters() {
document
.getElementById("applyFilters")
.addEventListener("click", applyReportsFilters);
document
.getElementById("exportExcel")
.addEventListener("click", exportToExcel);
}
function applyReportsFilters() {
const storeFilter = document.getElementById("filterStore").value;
const dateFrom = document.getElementById("filterDateFrom").value;
const dateTo = document.getElementById("filterDateTo").value;
let filteredReports = appState.reportsList || [];
if (storeFilter) {
filteredReports = filteredReports.filter(
(r) => String(r.storeId) === String(storeFilter)
);
}
if (dateFrom) {
filteredReports = filteredReports.filter((r) => r.reportDate >= dateFrom);
}
if (dateTo) {
filteredReports = filteredReports.filter((r) => r.reportDate <= dateTo);
}
// Обновление таблицы с отфильтрованными данными
const tbody = document.getElementById("reportsTableBody");
tbody.innerHTML = "";
filteredReports.forEach((report) => {
const storeName = report.storeName || report.storeId;
const username = report.username || report.userId;
const profit =
(Number(report.totalIncome) || 0) - (Number(report.totalExpenses) || 0);
const row = document.createElement("tr");
row.className = "hover:bg-gray-50";
row.innerHTML = `
<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 ? "Проверен" : "Не проверен"}
</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> Просмотр
</button>
<button class="text-red-600 hover:text-red-900" onclick="deleteReport(${
report.id
})">
<i class="fas fa-trash"></i> Удалить
</button>
</td>
`;
tbody.appendChild(row);
});
showNotification(`Найдено ${filteredReports.length} отчетов`);
}
function deleteReport(reportId) {
showConfirmModal("Вы уверены, что хотите удалить этот отчет?", () => {
apiDeleteReport(reportId);
loadReports();
updateDashboard();
showNotification("Отчет удален!");
});
}
// Заполнение формы данными отчета (для пользователя)
function fillFormWithReport(report) {
appState.editingReportId = report.id;
document.getElementById("storeSelect").value = report.storeId;
document.getElementById("income").value = report.income;
document.getElementById("cajaInicial").value = report.cajaInicial;
document.getElementById("envelope").value = report.envelope;
// Заполнение зарплат
const wagesContainer = document.getElementById("wagesContainer");
wagesContainer.innerHTML = "";
if (report.wages && report.wages.length > 0) {
report.wages.forEach((wage) => {
const row = document.createElement("div");
row.className = "wage-row grid grid-cols-1 md:grid-cols-3 gap-4 mb-3";
row.innerHTML = `
<input type="text" placeholder="Имя сотрудника" value="${wage.name}" class="wage-name form-input px-3 py-2 rounded-lg">
<input type="number" step="0.01" placeholder="Сумма €" value="${wage.amount}" class="wage-amount form-input px-3 py-2 rounded-lg">
<button type="button" class="remove-wage bg-red-500 text-white px-3 py-2 rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-times"></i>
</button>
`;
wagesContainer.appendChild(row);
});
} else {
addWageRow();
}
// Заполнение расходов
const expensesContainer = document.getElementById("expensesContainer");
expensesContainer.innerHTML = "";
if (report.expenses && report.expenses.length > 0) {
report.expenses.forEach((expense) => {
const row = document.createElement("div");
row.className = "expense-row grid grid-cols-1 md:grid-cols-3 gap-4 mb-3";
row.innerHTML = `
<input type="text" placeholder="Название расхода" value="${expense.name}" class="expense-name form-input px-3 py-2 rounded-lg">
<input type="number" step="0.01" placeholder="Сумма €" value="${expense.amount}" class="expense-amount form-input px-3 py-2 rounded-lg">
<button type="button" class="remove-expense bg-red-500 text-white px-3 py-2 rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-times"></i>
</button>
`;
expensesContainer.appendChild(row);
});
} else {
addExpenseRow();
}
updateTotals();
}
// Экспорт в Excel
function exportToExcel() {
// Use backend data
const reports = appState.reportsList || [];
const stores = appState.storesList || [];
const users = appState.usersList || [];
if (!reports.length) {
showNotification("Нет отчетов для экспорта", "info");
return;
}
const data = reports.map((report) => {
// Find store and user by id
const store =
stores.find((s) => Number(s.id) === Number(report.storeId)) || {};
const user =
users.find((u) => Number(u.id) === Number(report.userId)) || {};
const profit =
(Number(report.totalIncome) || 0) - (Number(report.totalExpenses) || 0);
return {
Дата: report.reportDate || report.date || "",
Магазин: store.name || report.storeName || "Неизвестно",
Доход: `${Number(report.totalIncome || 0).toFixed(2)}`,
Расходы: `${Number(report.totalExpenses || 0).toFixed(2)}`,
Прибыль: `${profit.toFixed(2)}`,
Пользователь: user.username || report.username || "Неизвестно",
Статус: report.isVerified
? "Проверен"
: report.verified
? "Проверен"
: "Не проверен",
};
});
if (!data.length) {
showNotification("Нет отчетов для экспорта", "info");
return;
}
// Create CSV
const headers = Object.keys(data[0]);
const csvContent = [
headers.join(","),
...data.map((row) => headers.map((header) => `"${row[header]}"`).join(",")),
].join("\n");
// Download file
const blob = new Blob([csvContent], {
type: "text/csv;charset=utf-8;",
});
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute(
"download",
`cash_reports_${new Date().toISOString().split("T")[0]}.csv`
);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showNotification("Отчет экспортирован!");
}
//REPORTS FORM LOGIC
// Загрузка магазинов для пользователя
function loadUserStores() {
const select = document.getElementById("storeSelect");
if (!select) return;
select.innerHTML =
'<option value="" disabled selected hidden>Выберите магазин</option>';
// For admin: show all
if (appState.currentUser.role === "admin") {
(appState.storesList || []).forEach((store) => {
const option = document.createElement("option");
option.value = store.id;
option.textContent = store.name;
select.appendChild(option);
});
} else {
// For employee: only their own stores
(appState.currentUser.stores || []).forEach((storeObj) => {
// storeObj could be an object ({id, name, ...}) or just an ID; check backend API
let store = storeObj;
if (typeof storeObj === "number") {
// If backend sends just IDs, look up in appState.storesList
store = (appState.storesList || []).find((s) => s.id === storeObj);
}
if (store) {
const option = document.createElement("option");
option.value = store.id;
option.textContent = store.name;
select.appendChild(option);
}
});
}
}
// Настройка автоматических расчетов в форме
function setupFormCalculations() {
const incomeInput = document.getElementById("income");
const cajaInicialInput = document.getElementById("cajaInicial");
const envelopeInput = document.getElementById("envelope");
function updateCalculations() {
const income = parseFloat(incomeInput.value) || 0;
const cajaInicial = parseFloat(cajaInicialInput.value) || 0;
const envelope = parseFloat(envelopeInput.value) || 0;
const totalIncome = income + cajaInicial;
document.getElementById("totalIncome").value = totalIncome.toFixed(2);
document.getElementById("displayTotalIncome").value =
totalIncome.toFixed(2);
const totalWages = calculateTotalWages();
const totalExpensesInternal = calculateTotalExpenses();
const totalExpenses = totalWages + totalExpensesInternal;
document.getElementById("totalExpenses").value = totalExpenses.toFixed(2);
const cajaFinal = totalIncome - totalExpenses - envelope;
document.getElementById("cajaFinal").textContent = cajaFinal.toFixed(2);
}
incomeInput.addEventListener("input", updateCalculations);
cajaInicialInput.addEventListener("input", updateCalculations);
envelopeInput.addEventListener("input", updateCalculations);
setupDynamicRows();
}
// Настройка динамических строк
function setupDynamicRows() {
document.getElementById("addWage").addEventListener("click", () => {
addWageRow();
});
document.getElementById("addExpense").addEventListener("click", () => {
addExpenseRow();
});
// Обновление при изменении значений
document.addEventListener("input", (e) => {
if (
e.target.classList.contains("wage-amount") ||
e.target.classList.contains("expense-amount")
) {
updateTotals();
}
});
// Удаление строк
document.addEventListener("click", (e) => {
if (e.target.classList.contains("remove-wage")) {
e.target.closest(".wage-row").remove();
updateTotals();
} else if (e.target.classList.contains("remove-expense")) {
e.target.closest(".expense-row").remove();
updateTotals();
}
});
}
function addWageRow() {
const container = document.getElementById("wagesContainer");
const row = document.createElement("div");
row.className = "wage-row grid grid-cols-1 md:grid-cols-3 gap-4 mb-3";
row.innerHTML = `
<input type="text" placeholder="Имя сотрудника" class="wage-name form-input px-3 py-2 rounded-lg">
<input type="number" step="0.01" placeholder="Сумма €" 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="Название расхода" class="expense-name form-input px-3 py-2 rounded-lg">
<input type="number" step="0.01" placeholder="Сумма €" 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("cajaFinal").textContent = cajaFinal.toFixed(2);
}
function collectWages() {
const wages = [];
document.querySelectorAll(".wage-row").forEach((row) => {
const name = row.querySelector(".wage-name").value;
const amount = parseFloat(row.querySelector(".wage-amount").value) || 0;
if (name && amount > 0) wages.push({ name, amount });
});
return wages;
}
function collectExpenses() {
const expenses = [];
document.querySelectorAll(".expense-row").forEach((row) => {
const name = row.querySelector(".expense-name").value;
const amount = parseFloat(row.querySelector(".expense-amount").value) || 0;
if (name && amount > 0) expenses.push({ name, amount });
});
return expenses;
}
function safeToFixed(value, digits = 2) {
return (Number(value) || 0).toFixed(digits);
}
document.getElementById("reportForm").addEventListener("submit", async (e) => {
e.preventDefault();
const formData = {
storeId: parseInt(document.getElementById("storeSelect").value),
reportDate: new Date().toISOString().split("T")[0], // Or get from input if user chooses date
income: parseFloat(document.getElementById("income").value),
initialCash: parseFloat(document.getElementById("cajaInicial").value),
totalIncome: parseFloat(document.getElementById("totalIncome").value),
wages: JSON.stringify(collectWages()),
expenses: JSON.stringify(collectExpenses()),
totalWages: calculateTotalWages(),
totalExpenses: calculateTotalExpenses() + calculateTotalWages(),
envelope: parseFloat(document.getElementById("envelope").value),
finalCash:
parseFloat(document.getElementById("totalIncome").value) -
(calculateTotalWages() + calculateTotalExpenses()) -
parseFloat(document.getElementById("envelope").value),
};
console.log("Sending report:", formData);
const result = await createReport(formData);
if (result.success) {
showNotification("Отчет успешно создан!");
await loadReports();
document.getElementById("reportForm").reset();
} else {
showNotification(result.error || "Ошибка создания отчета", "error");
}
});
// Отчет за сегодня для пользователя
document.getElementById("todayReportBtn").addEventListener("click", () => {
const today = new Date().toISOString().split("T")[0];
const todayReport = database.reports.find(
(r) => r.date === today && r.userId === appState.currentUser.id
);
if (todayReport) {
showReportModal(todayReport, false); // false = не админ режим
} else {
showNotification("Отчет за сегодня не найден", "error");
}
});
//USERS
async function loadUsers() {
console.log("Loading users...");
const tbody = document.getElementById("usersTableBody");
tbody.innerHTML = "";
const result = await getAllUsers();
// console.log("getAllUsers result:", result);
if (!result.success) {
showNotification(result.error, "error");
return;
}
const users = result.users;
appState.usersList = users;
users.forEach((user) => {
const userStores =
user.stores
.map((storeId) => {
const store = (appState.storesList || []).find(
(s) => s.id === storeId
);
return store ? store.name : "Нет доступа";
})
.join(", ") || "Нет доступа";
const row = document.createElement("tr");
row.className = "hover:bg-gray-50";
row.innerHTML = `
<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" ? "Администратор" : "Сотрудник"}
</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> Редактировать
</button>
<button class="text-red-600 hover:text-red-900" onclick="deleteUser(${
user.id
})">
<i class="fas fa-trash"></i> Удалить
</button>
</td>
`;
tbody.appendChild(row);
});
if (users.length === 0) {
showNotification("Нет пользователей для отображения", "info");
}
}
// Редактирование пользователя
function editUser(userId) {
appState.editingUserId = userId;
const user = appState.usersList.find((u) => u.id === userId);
showUserEditModal(user);
}
// Показ модального окна редактирования пользователя
function showUserEditModal(user) {
const modal = document.getElementById("userEditModal");
const title = document.getElementById("userModalTitle");
const form = document.getElementById("userEditForm");
// If appState.editingUserId, it's an edit; else, it's add
title.textContent = appState.editingUserId
? "Редактирование пользователя"
: "Добавление пользователя";
document.getElementById("userLogin").value = (user && user.username) || "";
const loginInput = document.getElementById("userLogin");
loginInput.value = (user && user.username) || "";
if (appState.editingUserId) {
loginInput.disabled = true;
} else {
loginInput.disabled = false;
}
document.getElementById("userPassword").value = "";
document.getElementById("userRole").value = (user && user.role) || "employee";
// Загрузка чекбоксов магазинов
const storesContainer = document.getElementById("userStoresAccess");
storesContainer.innerHTML = "";
(appState.storesList || []).forEach((store) => {
const isChecked = user && user.stores && user.stores.includes(store.id);
const checkbox = document.createElement("label");
checkbox.className = "flex items-center";
checkbox.innerHTML = `
<input type="checkbox" value="${store.id}" ${
isChecked ? "checked" : ""
} class="mr-2">
<span>${store.name}</span>
`;
storesContainer.appendChild(checkbox);
});
showModal("userEditModal");
}
//save user for create and update
async function saveUser() {
const login = document.getElementById("userLogin").value.trim();
const password = document.getElementById("userPassword").value;
const role = document.getElementById("userRole").value;
const selectedStores = Array.from(
document.querySelectorAll(
'#userStoresAccess input[type="checkbox"]:checked'
)
).map((cb) => parseInt(cb.value));
if (!login) {
showNotification("Заполните логин!", "error");
return;
}
// Always build userData
const userData = {
username: login,
role: role,
storeIds: selectedStores,
};
if (password) userData.password = password;
// Determine CREATE or EDIT
let result;
if (!appState.editingUserId) {
if (!password) {
showNotification("Укажите пароль для нового пользователя!", "error");
return;
}
// CREATE
result = await createUser(userData);
if (result.success) {
showNotification("Пользователь добавлен!");
}
} else {
// EDIT
result = await updateUser(appState.editingUserId, userData);
if (result.success) {
showNotification("Пользователь обновлен!");
}
appState.editingUserId = null;
}
// After save: UI update or error
if (result && result.success) {
hideModal("userEditModal");
loadUsers();
updateDashboard();
} else if (result) {
showNotification(result.error || "Ошибка операции", "error");
}
}
//UI trigger: delete user with modal
function deleteUser(userId) {
showConfirmModal("Вы уверены, что хотите удалить этого пользователя?", () =>
apiDeleteUser(userId)
);
}
// Добавление пользователя
document.addEventListener("DOMContentLoaded", function () {
const addUserBtn = document.getElementById("addUserBtn");
if (addUserBtn) {
addUserBtn.addEventListener("click", () => {
appState.editingUserId = null;
showUserEditModal();
});
}
});
// Сохранение пользователя
document.addEventListener("DOMContentLoaded", function () {
const saveUserBtn = document.getElementById("saveUserBtn");
const cancelUserBtn = document.getElementById("cancelUserBtn");
if (saveUserBtn) {
saveUserBtn.addEventListener("click", saveUser);
}
if (cancelUserBtn) {
cancelUserBtn.addEventListener("click", () => {
hideModal("userEditModal");
});
}
});
//SHOPS
// Загрузка магазинов
async function loadStores() {
const tbody = document.getElementById("storesTableBody");
tbody.innerHTML = "";
const token = localStorage.getItem("token");
try {
const response = await fetch(`${API_BASE_URL}/stores`, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const data = await response.json();
if (!response.ok || !data.stores) {
showNotification(data.error || "Ошибка загрузки магазинов", "error");
return;
}
const stores = data.stores;
// appState.usersList = users;
appState.storesList = stores;
stores.forEach((store) => {
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> Редактировать
</button>
<button class="text-red-600 hover:text-red-900" onclick="deleteStore(${
store.id
})">
<i class="fas fa-trash"></i> Удалить
</button>
</td>
`;
tbody.appendChild(row);
});
if (stores.length === 0) {
showNotification("Нет магазинов для отображения", "info");
}
} catch (err) {
showNotification("Нет соединения с сервером", "error");
}
}
// Редактирование магазина
function editStore(storeId) {
appState.editingStoreId = storeId;
showStoreEditModal();
}
// Показ модального окна редактирования магазина
function showStoreEditModal() {
const modal = document.getElementById("storeEditModal");
const title = document.getElementById("storeModalTitle");
const form = document.getElementById("storeEditForm");
const store = appState.storesList.find(
(s) => s.id === appState.editingStoreId
);
if (!store) {
showNotification("Магазин не найден!", "error");
return;
}
title.textContent = "Редактирование магазина";
document.getElementById("storeName").value = store.name || "";
showModal("storeEditModal");
}
async function saveStore() {
const name = document.getElementById("storeName").value.trim();
if (!name) {
showNotification("Заполните название магазина!", "error");
return;
}
let result;
if (appState.editingStoreId == null) {
// Add
result = await createStore({ name });
if (result.success) {
showNotification("Магазин добавлен!");
hideModal("storeEditModal");
await loadStores();
if (typeof loadUsers === "function") loadUsers();
if (typeof loadUserStores === "function") loadUserStores();
if (typeof updateDashboard === "function") updateDashboard();
}
} else {
// Edit
result = await updateStore(appState.editingStoreId, { name });
if (result.success) {
showNotification("Магазин обновлен!");
hideModal("storeEditModal");
await loadStores();
await loadReports();
if (typeof loadUsers === "function") loadUsers();
if (typeof loadUserStores === "function") loadUserStores();
if (typeof updateDashboard === "function") updateDashboard();
}
}
if (result && !result.success) {
if (
result.status === 409 ||
(result.error && result.error.includes("already exists"))
) {
showNotification("Магазин с таким названием уже существует!", "error");
} else {
showNotification(result.error || "Ошибка сохранения магазина", "error");
}
return;
}
hideModal("storeEditModal");
appState.editingStoreId = null;
await loadStores();
if (typeof updateDashboard === "function") updateDashboard();
}
// Удаление магазина
function deleteStore(storeId) {
const store = appState.storesList.find((s) => s.id === storeId);
let message = `Вы уверены, что хотите удалить этот магазин "${store.name}"?`;
if (store.reportsCount && store.reportsCount > 0) {
message += `\n\nОбратите внимание! У этого магазина ${store.reportsCount} есть связынные с ним отчеты. Они также будут удалены.`;
}
showConfirmModal(message, () => handleDeleteStore(storeId));
}
async function handleDeleteStore(storeId) {
const result = await deleteStoreApi(storeId);
if (result.success) {
showNotification("Shop and related reports deleted!");
await loadStores();
if (typeof loadReports === "function") loadReports();
if (typeof updateDashboard === "function") updateDashboard();
if (typeof loadUsers === "function") loadUsers();
} else {
showNotification(result.error || "Failed to delete shop", "error");
}
}
// Добавление магазина
document.getElementById("addStoreBtn").onclick = () => {
appState.editingStoreId = null;
document.getElementById("storeModalTitle").textContent =
"Добавление магазина";
document.getElementById("storeName").value = "";
showModal("storeEditModal");
};
// Сохранение магазина
document.addEventListener("DOMContentLoaded", function () {
const saveStoreBtn = document.getElementById("saveStoreBtn");
const cancelStoreBtn = document.getElementById("cancelStoreBtn");
if (saveStoreBtn) saveStoreBtn.onclick = saveStore;
if (cancelStoreBtn)
cancelStoreBtn.onclick = () => {
hideModal("storeEditModal");
appState.editingStoreId = null;
document.getElementById("storeEditForm").reset();
};
});
//TODO
// Загрузка TODO
function loadTodos() {
const container = document.getElementById("todoList");
container.innerHTML = "";
database.todos.forEach((todo) => {
const todoItem = document.createElement("div");
todoItem.className = `p-4 border rounded-lg ${
todo.completed
? "bg-green-50 border-green-200"
: "bg-white border-gray-200"
}`;
const priorityColors = {
low: "bg-blue-100 text-blue-800",
medium: "bg-yellow-100 text-yellow-800",
high: "bg-red-100 text-red-800",
};
const priorityText = {
low: "Низкий",
medium: "Средний",
high: "Высокий",
};
todoItem.innerHTML = `
<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("Статус задачи обновлен!");
}
// Редактирование TODO
function editTodo(todoId) {
appState.editingTodoId = todoId;
showTodoEditModal();
}
// Показ модального окна редактирования TODO
function showTodoEditModal() {
const modal = document.getElementById("todoEditModal");
const title = document.getElementById("todoModalTitle");
const form = document.getElementById("todoEditForm");
if (appState.editingTodoId) {
const todo = database.todos.find((t) => t.id === appState.editingTodoId);
title.textContent = "Редактирование задачи";
document.getElementById("todoTitle").value = todo.title;
document.getElementById("todoDescription").value = todo.description;
document.getElementById("todoPriority").value = todo.priority;
} else {
title.textContent = "Добавление задачи";
form.reset();
}
showModal("todoEditModal");
}
function saveTodo() {
const title = document.getElementById("todoTitle").value;
const description = document.getElementById("todoDescription").value;
const priority = document.getElementById("todoPriority").value;
if (!title) {
showNotification("Заполните заголовок!", "error");
return;
}
if (appState.editingTodoId) {
// Редактирование
const todoIndex = database.todos.findIndex(
(t) => t.id === appState.editingTodoId
);
database.todos[todoIndex].title = title;
database.todos[todoIndex].description = description;
database.todos[todoIndex].priority = priority;
showNotification("Задача обновлена!");
} else {
// Добавление
const newId = Math.max(...database.todos.map((t) => t.id)) + 1;
database.todos.push({
id: newId,
title: title,
description: description,
priority: priority,
completed: false,
createdAt: new Date().toISOString().split("T")[0],
});
showNotification("Задача добавлена!");
}
hideModal("todoEditModal");
loadTodos();
}
// Удаление TODO
function deleteTodo(todoId) {
if (confirm("Вы уверены, что хотите удалить эту задачу?")) {
database.todos = database.todos.filter((t) => t.id !== todoId);
loadTodos();
showNotification("Задача удалена!");
}
}
// Добавление TODO
document.addEventListener("DOMContentLoaded", function () {
const addTodoBtn = document.getElementById("addTodoBtn");
if (addTodoBtn) {
addTodoBtn.addEventListener("click", () => {
appState.editingTodoId = null;
showTodoEditModal();
});
}
});
// Сохранение TODO
document.addEventListener("DOMContentLoaded", function () {
const saveTodoBtn = document.getElementById("saveTodoBtn");
const cancelTodoBtn = document.getElementById("cancelTodoBtn");
if (saveTodoBtn) {
saveTodoBtn.addEventListener("click", saveTodo);
}
if (cancelTodoBtn) {
cancelTodoBtn.addEventListener("click", () => {
hideModal("todoEditModal");
});
}
});
// ####################MOCK###########
// База данных (симуляция)
let database = {
users: [
{
id: 1,
username: "admin",
password: "admin123",
role: "admin",
stores: [],
},
{
id: 2,
username: "employee",
password: "password123",
role: "employee",
stores: [1, 2],
},
{
id: 3,
username: "cashier1",
password: "password123",
role: "employee",
stores: [1, 2],
},
{
id: 4,
username: "cashier2",
password: "password123",
role: "employee",
stores: [3],
},
],
stores: [
{ id: 1, name: "Магазин 1" },
{ id: 2, name: "Магазин 2" },
{ id: 3, name: "Магазин 3" },
{ id: 4, name: "Магазин 4" },
],
reports: [],
todos: [
{
id: 1,
title: "Исправить модальные окна",
description: "Исправить прокрутку и видимость кнопок в модальных окнах",
completed: true,
priority: "high",
createdAt: "2024-01-15",
},
{
id: 2,
title: "Добавить экспорт в PDF",
description: "Реализовать функцию экспорта отчетов в PDF формат",
completed: false,
priority: "medium",
createdAt: "2024-01-16",
},
{
id: 3,
title: "Улучшить графики",
description: "Добавить больше интерактивности в графики Dashboard",
completed: false,
priority: "low",
createdAt: "2024-01-17",
},
],
};
// Генерация тестовых данных для отчетов
function generateTestData() {
const reports = [];
const today = new Date();
for (let i = 0; i < 30; i++) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const storeId = Math.floor(Math.random() * 4) + 1;
const userId = Math.floor(Math.random() * 3) + 2; // employee пользователи
const income = Math.floor(Math.random() * 2000) + 500;
const cajaInicial = Math.floor(Math.random() * 300) + 100;
const totalIncome = income + cajaInicial;
const wages = Math.floor(Math.random() * 400) + 100;
const expenses = Math.floor(Math.random() * 200) + 50;
const totalExpenses = wages + expenses;
const envelope = Math.floor(Math.random() * 200) + 100;
const cajaFinal = totalIncome - totalExpenses - envelope;
reports.push({
id: i + 1,
date: date.toISOString().split("T")[0],
storeId: storeId,
userId: userId,
income: income,
cajaInicial: cajaInicial,
totalIncome: totalIncome,
wages: [
{
name: "Сотрудник " + (Math.floor(Math.random() * 3) + 1),
amount: wages,
},
],
expenses: [
{
name: "Расходы " + (Math.floor(Math.random() * 3) + 1),
amount: expenses,
},
],
totalWages: wages,
totalExpensesInternal: expenses,
totalExpenses: totalExpenses,
envelope: envelope,
cajaFinal: cajaFinal,
verified: Math.random() > 0.3,
createdAt: date.toISOString(),
});
}
database.reports = reports;
}
// Инициализация тестовых данных
generateTestData();
// ####################################