Merge pull request 'dev' (#7) from dev into main

Reviewed-on: https://git.softuniq.eu/AnzhelaK/cash_report_system/pulls/7
This commit is contained in:
AnzhelaK 2025-07-30 19:03:16 +00:00
commit 282c573752
2 changed files with 1241 additions and 1649 deletions

View File

@ -1,7 +1,60 @@
//API
const API_BASE_URL = "http://195.209.214.159/api";
//Login
//API local
// const API_BASE_URL = "http://localhost:3001/api";
//SHARED
//REACTIVITY (experimental)
function createReactiveState(stateObj, onChange) {
return new Proxy(stateObj, {
set(target, prop, value) {
target[prop] = value;
onChange(prop, value);
return true;
},
});
}
//universal global state
window.appState = createReactiveState(
window.appState || {
currentUser: null,
usersList: [],
reportsList: [],
storesList: [],
editingReportId: null,
editingUserId: null,
editingStoreId: null,
editingTodoId: null,
revenueChartInstance: null,
expensesChartInstance: null,
storesChartInstance: null,
trendsChartInstance: null,
adminTabsInitialized: false,
},
function (prop, value) {
// React to changes in critical state
if (prop === "storesList") {
// if (typeof loadStores === "function") loadStores();
if (typeof loadUserStores === "function") loadUserStores();
if (typeof updateDashboard === "function") updateDashboard();
}
if (prop === "usersList") {
// if (typeof loadUsers === "function") loadUsers();
if (typeof updateDashboard === "function") updateDashboard();
}
if (prop === "reportsList") {
// if (typeof loadReports === "function") loadReports();
if (typeof updateDashboard === "function") updateDashboard();
}
}
);
//AUTH
//Login & Logout
async function loginUser(username, password) {
try {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
@ -28,35 +81,19 @@ async function loginUser(username, password) {
}
}
document.getElementById("loginForm").addEventListener("submit", async (e) => {
e.preventDefault();
function logout() {
appState.currentUser = null;
localStorage.removeItem("token");
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
// Show login screen, hide other interfaces
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");
// Call backend login
const result = await loginUser(username, password);
if (result.success) {
// Save user/token in JS (or localStorage)
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.classList.remove("hidden");
}
});
showNotification("Вы вышли из системы", "info");
}
document.addEventListener("DOMContentLoaded", async () => {
const token = localStorage.getItem("token");
@ -70,9 +107,9 @@ document.addEventListener("DOMContentLoaded", async () => {
});
if (response.ok) {
const data = await response.json();
currentUser = data.user;
appState.currentUser = data.user;
document.getElementById("loginScreen").classList.add("hidden");
if (currentUser.role === "admin") {
if (appState.currentUser.role === "admin") {
showAdminInterface();
} else {
showUserInterface();
@ -91,24 +128,10 @@ document.addEventListener("DOMContentLoaded", async () => {
}
});
function logout() {
currentUser = null;
localStorage.removeItem("token");
// Show login screen, hide other interfaces
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("Вы вышли из системы", "info");
}
document.getElementById("logoutBtn").addEventListener("click", logout);
document.getElementById("adminLogoutBtn").addEventListener("click", logout);
//Users
//USERS
//GET all users (admin only)
async function getAllUsers() {
@ -135,68 +158,7 @@ async function getAllUsers() {
}
}
let usersList = [];
async function loadUsers() {
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;
window.usersList = users;
users.forEach((user) => {
const userStores =
user.stores
.map((storeId) => {
const store = (window.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");
}
}
appState.usersList = [];
//add user
async function createUser(userData) {
@ -331,14 +293,7 @@ async function apiDeleteUser(userId) {
}
}
// 4. UI trigger: delete user with modal
function deleteUser(userId) {
showConfirmModal("Вы уверены, что хотите удалить этого пользователя?", () =>
apiDeleteUser(userId)
);
}
//Reports
//REPORTS ADMIN
//GET all reports
async function getReports() {
const token = localStorage.getItem("token");
@ -365,109 +320,6 @@ async function getReports() {
}
}
async function loadReports() {
const tbody = document.getElementById("reportsTableBody");
const filterStore = document.getElementById("filterStore");
const result = await getReports();
console.log("getReports() result:", result);
window.reportsList = result.success ? result.reports : [];
if (!result.success) {
showNotification(result.error || "Ошибка загрузки отчетов", "error");
return;
}
const reports = result.reports;
// Build a map storeId => storeName from all reports
const storeMap = {};
reports.forEach((r) => {
if (r.storeId && r.storeName) {
storeMap[r.storeId] = r.storeName;
}
});
// Get unique [storeId, storeName] pairs sorted alphabetically
const storesWithReports = Object.entries(storeMap)
.map(([id, name]) => ({ id, name }))
.sort((a, b) => a.name.localeCompare(b.name));
filterStore.innerHTML = `
<option value="">Все магазины</option>
${storesWithReports
.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 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">${
report.storeName || report.storeId
}</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 (!window.reportsList) {
showNotification("Reports not loaded yet!", "error");
return;
}
const report = window.reportsList.find((r) => r.id === reportId);
if (report) {
showReportModal(report, true);
} else {
showNotification("Report not found!", "error");
}
}
//edit report
async function updateReport(reportId, data) {
const token = localStorage.getItem("token");
@ -500,136 +352,6 @@ async function updateReport(reportId, data) {
}
}
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);
}
//accept report (admin only)
async function verifyReport(reportId) {
const token = localStorage.getItem("token");
@ -684,7 +406,7 @@ async function apiDeleteReport(reportId) {
}
}
//worker
//REPORTS WORKER
//create report
// api.js
async function createReport(data) {
@ -715,8 +437,7 @@ async function createReport(data) {
}
}
//stores (for admin)
//STORES (for admin)
// 1. Get all stores
async function getStores() {
const token = localStorage.getItem("token");

File diff suppressed because it is too large Load Diff