Files
cash-report-system/frontend/api.js

818 lines
25 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.
//API
const API_BASE_URL = "http://localhost:3001/api";
//Login
async function loginUser(username, password) {
try {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (response.ok) {
return { success: true, ...data };
} else {
return {
success: false,
error:
data.error ||
(data.errors && data.errors[0]?.msg) ||
"Ошибка авторизации",
};
}
} catch (err) {
return { success: false, error: "Нет соединения с сервером" };
}
}
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)
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");
}
});
document.addEventListener("DOMContentLoaded", async () => {
const token = localStorage.getItem("token");
if (token) {
// Try to get user info from backend
try {
const response = await fetch(`${API_BASE_URL}/auth/me`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
currentUser = data.user;
document.getElementById("loginScreen").classList.add("hidden");
if (currentUser.role === "admin") {
showAdminInterface();
} else {
showUserInterface();
}
} else {
// Token invalid/expired
localStorage.removeItem("token");
document.getElementById("loginScreen").classList.remove("hidden");
}
} catch (err) {
showNotification("Нет соединения с сервером", "error");
}
} else {
// No token, show login screen
document.getElementById("loginScreen").classList.remove("hidden");
}
});
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
//GET all users (admin only)
async function getAllUsers() {
const token = localStorage.getItem("token");
try {
const response = await fetch(`${API_BASE_URL}/users`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const data = await response.json();
if (response.ok) {
return { success: true, users: data.users };
} else {
return {
success: false,
error: data.error || data.message || "Ошибка получения пользователей",
};
}
} catch (err) {
return { success: false, error: "Нет соединения с сервером" };
}
}
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;
usersList = users;
users.forEach((user) => {
const userStores =
user.stores
.map((storeId) => {
const store = database.stores.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");
}
}
//add user
async function createUser(userData) {
const token = localStorage.getItem("token");
try {
const response = await fetch(`${API_BASE_URL}/users`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(userData),
});
const data = await response.json();
if (response.ok) {
return { success: true, ...data };
} else {
return {
success: false,
error:
data.error ||
(data.errors && data.errors[0]?.msg) ||
"Ошибка создания пользователя",
};
}
} catch {
return { success: false, error: "Нет соединения с сервером" };
}
}
//edit user
async function updateUser(userId, userData) {
const token = localStorage.getItem("token");
try {
const response = await fetch(`${API_BASE_URL}/users/${userId}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(userData),
});
const data = await response.json();
if (response.ok) {
return { success: true };
} else {
return {
success: false,
error:
data.error ||
(data.errors && data.errors[0]?.msg) ||
"Ошибка обновления пользователя",
};
}
} catch {
return { success: false, error: "Нет соединения с сервером" };
}
}
// 1. Create modal on-demand if missing
function ensureConfirmModal() {
if (document.getElementById("confirmModal")) return;
const modal = document.createElement("div");
modal.id = "confirmModal";
modal.className =
"hidden fixed inset-0 z-50 bg-black bg-opacity-30 flex items-center justify-center";
modal.innerHTML = `
<div class="bg-white rounded-lg shadow-lg p-8 max-w-sm w-full relative">
<div id="confirmModalText" class="text-lg text-gray-700 mb-6"></div>
<div class="flex justify-end space-x-4">
<button id="confirmModalOk" class="px-5 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700">Удалить</button>
<button id="confirmModalCancel" class="px-5 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400">Отмена</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
// 2. Show confirm modal
function showConfirmModal(message, onConfirm) {
ensureConfirmModal();
const modal = document.getElementById("confirmModal");
const text = document.getElementById("confirmModalText");
const okBtn = document.getElementById("confirmModalOk");
const cancelBtn = document.getElementById("confirmModalCancel");
text.textContent = message;
modal.classList.remove("hidden");
function cleanup() {
modal.classList.add("hidden");
okBtn.removeEventListener("click", okHandler);
cancelBtn.removeEventListener("click", cancelHandler);
}
function okHandler() {
cleanup();
onConfirm();
}
function cancelHandler() {
cleanup();
}
okBtn.addEventListener("click", okHandler);
cancelBtn.addEventListener("click", cancelHandler);
}
// 3. API call to delete user
async function apiDeleteUser(userId) {
const token = localStorage.getItem("token");
try {
const response = await fetch(`${API_BASE_URL}/users/${userId}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const data = await response.json();
if (response.ok) {
showNotification("Пользователь удален!");
loadUsers();
updateDashboard();
} else {
showNotification(data.error || "Ошибка удаления пользователя", "error");
}
} catch {
showNotification("Нет соединения с сервером", "error");
}
}
// 4. UI trigger: delete user with modal
function deleteUser(userId) {
showConfirmModal("Вы уверены, что хотите удалить этого пользователя?", () =>
apiDeleteUser(userId)
);
}
//Reports
//GET all reports
async function getReports() {
const token = localStorage.getItem("token");
try {
const response = await fetch(`${API_BASE_URL}/reports`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const data = await response.json();
console.log(data);
if (response.ok) {
return { success: true, reports: data.reports };
} else {
return {
success: false,
error: data.error || data.message || "Ошибка получения отчетов",
};
}
} catch (err) {
return { success: false, error: "Нет соединения с сервером" };
}
}
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");
try {
const res = await fetch(`${API_BASE_URL}/reports/${reportId}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await res.json();
if (res.ok && result.updated) {
showNotification("Отчет обновлен!", "success");
hideModal("reportViewModal");
await loadReports();
console.log(data);
if (typeof updateDashboard === "function") updateDashboard();
return { success: true };
} else {
showNotification(result.error || "Ошибка обновления отчета", "error");
return { success: false, error: result.error };
}
} catch (err) {
showNotification("Ошибка сети. Попробуйте еще раз.", "error");
return { success: false, error: err.message };
}
}
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");
try {
const response = await fetch(`${API_BASE_URL}/reports/${reportId}/verify`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const data = await response.json();
if (response.ok) {
showNotification("Отчет успешно подтвержден!", "success");
hideModal("reportViewModal");
await loadReports();
if (typeof updateDashboard === "function") {
updateDashboard(); // Refresh dashboard if it exists
}
return { success: true, ...data };
} else {
showNotification(data.error || "Ошибка подтверждения отчета", "error");
return { success: false, error: data.error || "Ошибка" };
}
} catch (err) {
showNotification("Нет соединения с сервером", "error");
return { success: false, error: "Нет соединения с сервером" };
}
}
//delete report (admin only)
async function apiDeleteReport(reportId) {
const token = localStorage.getItem("token");
try {
const response = await fetch(`${API_BASE_URL}/reports/${reportId}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const data = await response.json();
if (response.ok && data.deleted) {
showNotification("Отчет удален!");
await loadReports();
if (typeof updateDashboard === "function") updateDashboard();
} else {
showNotification(data.error || "Ошибка удаления отчета", "error");
}
} catch {
showNotification("Нет соединения с сервером", "error");
}
}
//worker
//create report
// api.js
async function createReport(data) {
const token = localStorage.getItem("token");
try {
const response = await fetch(`${API_BASE_URL}/reports`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await response.json();
if (response.ok && result.id) {
return { success: true, id: result.id };
} else {
return {
success: false,
error:
result.error ||
(result.errors && result.errors[0]?.msg) ||
"Ошибка создания отчета",
};
}
} catch (err) {
return { success: false, error: "Нет соединения с сервером" };
}
}
//stores (for admin)
// 1. Get all stores
async function getStores() {
const token = localStorage.getItem("token");
try {
const response = await fetch(`${API_BASE_URL}/stores`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const data = await response.json();
if (response.ok) {
return { success: true, stores: data.stores };
} else {
return {
success: false,
error: data.error || "Ошибка получения магазинов",
};
}
} catch {
return { success: false, error: "Нет соединения с сервером" };
}
}
// 2. Create new store (admin only)
async function createStore(storeData) {
const token = localStorage.getItem("token");
try {
const response = await fetch(`${API_BASE_URL}/stores`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(storeData),
});
const data = await response.json();
if (response.ok && data.id) {
return { success: true, id: data.id };
} else {
return {
success: false,
error: data.error || "Ошибка создания магазина",
};
}
} catch {
return { success: false, error: "Нет соединения с сервером" };
}
}
// 3. Edit store (admin only)
async function updateStore(storeId, storeData) {
const token = localStorage.getItem("token");
try {
const response = await fetch(`${API_BASE_URL}/stores/${storeId}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(storeData),
});
const data = await response.json();
return {
success: response.ok && data.updated,
error: data.error || "",
status: response.status,
};
} catch {
return { success: false, error: "Нет соединения с сервером" };
}
}
// 4. Delete store (admin only)
async function deleteStoreApi(storeId) {
const token = localStorage.getItem("token");
try {
const response = await fetch(`${API_BASE_URL}/stores/${storeId}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const data = await response.json();
if (response.ok && data.deleted) {
return { success: true };
} else {
return {
success: false,
error: data.error || "Ошибка удаления магазина",
};
}
} catch {
return { success: false, error: "Нет соединения с сервером" };
}
}