Translate Spain

This commit is contained in:
NW
2025-08-09 14:36:22 +00:00
parent d9de5fb098
commit 220c2f8d62
5 changed files with 387 additions and 283 deletions

View File

@@ -73,7 +73,7 @@ router.post(
) {
return res.status(409).json({
error:
"Отчет за этот магазин и дату уже был отправлен этим пользователем.",
"Ya existe un informe para esta tienda y fecha enviado por este usuario.",
});
}
console.error("DB error:", err);
@@ -195,7 +195,7 @@ router.put(
) {
return res
.status(403)
.json({ error: "Запрещено редактировать подтвержденный отчет" });
.json({ error: "No se permite editar un informe verificado" });
}
//check for duplicate report, excluding the current, use new values if provided, else keep old ones from original report
@@ -215,7 +215,7 @@ router.put(
if (duplicate) {
return res.status(409).json({
error:
"Отчет за этот магазин и дату уже был отправлен этим пользователем.",
"Ya existe un informe para esta tienda y fecha enviado por este usuario.",
});
}

View File

@@ -1,5 +1,8 @@
//API
const API_BASE_URL = "http://195.209.214.159/api";
//const API_BASE_URL = "http://195.209.214.159/api";
//API Product
const API_BASE_URL = "https://csr.bbox.wtf/api";
//API local
// const API_BASE_URL = "http://localhost:3001/api";
@@ -72,11 +75,11 @@ async function loginUser(username, password) {
error:
data.error ||
(data.errors && data.errors[0]?.msg) ||
"Ошибка авторизации",
"Error de autenticación",
};
}
} catch (err) {
return { success: false, error: "Нет соединения с сервером" };
return { success: false, error: "Sin conexión con el servidor" };
}
}
@@ -94,7 +97,7 @@ function logout() {
if (document.getElementById("storeSelect"))
document.getElementById("storeSelect").selectedIndex = 0;
showNotification("Вы вышли из системы", "info");
showNotification("Sesión cerrada", "info");
}
document.addEventListener("DOMContentLoaded", async () => {
@@ -123,7 +126,7 @@ document.addEventListener("DOMContentLoaded", async () => {
document.getElementById("loginScreen").classList.remove("hidden");
}
} catch (err) {
showNotification("Нет соединения с сервером", "error");
showNotification("Sin conexión con el servidor", "error");
}
} else {
// No token, show login screen
@@ -153,11 +156,11 @@ async function getAllUsers() {
} else {
return {
success: false,
error: data.error || data.message || "Ошибка получения пользователей",
error: data.error || data.message || "Error al obtener usuarios",
};
}
} catch (err) {
return { success: false, error: "Нет соединения с сервером" };
return { success: false, error: "Sin conexión con el servidor" };
}
}
@@ -184,11 +187,11 @@ async function createUser(userData) {
error:
data.error ||
(data.errors && data.errors[0]?.msg) ||
"Ошибка создания пользователя",
"Error al crear usuario",
};
}
} catch {
return { success: false, error: "Нет соединения с сервером" };
return { success: false, error: "Sin conexión con el servidor" };
}
}
@@ -213,11 +216,11 @@ async function updateUser(userId, userData) {
error:
data.error ||
(data.errors && data.errors[0]?.msg) ||
"Ошибка обновления пользователя",
"Error al actualizar usuario",
};
}
} catch {
return { success: false, error: "Нет соединения с сервером" };
return { success: false, error: "Sin conexión con el servidor" };
}
}
@@ -233,8 +236,8 @@ function ensureConfirmModal() {
<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>
<button id="confirmModalOk" class="px-5 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700">Eliminar</button>
<button id="confirmModalCancel" class="px-5 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400">Cancelar</button>
</div>
</div>
`;
@@ -285,14 +288,14 @@ async function apiDeleteUser(userId) {
});
const data = await response.json();
if (response.ok) {
showNotification("Пользователь удален!");
showNotification("Usuario eliminado!");
loadUsers();
updateDashboard();
} else {
showNotification(data.error || "Ошибка удаления пользователя", "error");
showNotification(data.error || "Error al eliminar usuario", "error");
}
} catch {
showNotification("Нет соединения с сервером", "error");
showNotification("Sin conexión con el servidor", "error");
}
}
@@ -315,11 +318,11 @@ async function getReports() {
} else {
return {
success: false,
error: data.error || data.message || "Ошибка получения отчетов",
error: data.error || data.message || "Error al obtener informes",
};
}
} catch (err) {
return { success: false, error: "Нет соединения с сервером" };
return { success: false, error: "Sin conexión con el servidor" };
}
}
@@ -334,10 +337,10 @@ async function getReportById(reportId) {
if (response.ok && data.report) {
return { success: true, report: data.report };
} else {
return { success: false, error: data.error || "Ошибка получения отчета" };
return { success: false, error: data.error || "Error al obtener informe" };
}
} catch (err) {
return { success: false, error: "Нет соединения с сервером" };
return { success: false, error: "Sin conexión con el servidor" };
}
}
@@ -357,16 +360,16 @@ async function updateReport(reportId, data) {
// handler for duplicates error before parsing
if (res.status === 409) {
showNotification(
"Отчет за этот магазин и дату уже был отправлен этим пользователем.",
"Ya existe un informe para esta tienda y fecha.",
"error"
);
return { success: false, error: "Дубликат отчета" };
return { success: false, error: "Informe duplicado" };
}
const result = await res.json();
if (res.ok && result.updated) {
showNotification("Отчет обновлен!", "success");
showNotification("Informe actualizado!", "success");
hideModal("reportViewModal");
await loadReports();
@@ -375,11 +378,11 @@ async function updateReport(reportId, data) {
if (typeof updateDashboard === "function") updateDashboard();
return { success: true };
} else {
showNotification(result.error || "Ошибка обновления отчета", "error");
showNotification(result.error || "Error al actualizar informe", "error");
return { success: false, error: result.error };
}
} catch (err) {
showNotification("Ошибка сети. Попробуйте еще раз.", "error");
showNotification("Error de red. Intente nuevamente.", "error");
return { success: false, error: err.message };
}
}
@@ -397,7 +400,7 @@ async function verifyReport(reportId) {
});
const data = await response.json();
if (response.ok) {
showNotification("Отчет успешно подтвержден!", "success");
showNotification("Informe verificado exitosamente!", "success");
hideModal("reportViewModal");
await loadReports();
if (typeof updateDashboard === "function") {
@@ -405,12 +408,12 @@ async function verifyReport(reportId) {
}
return { success: true, ...data };
} else {
showNotification(data.error || "Ошибка подтверждения отчета", "error");
return { success: false, error: data.error || "Ошибка" };
showNotification(data.error || "Error al verificar informe", "error");
return { success: false, error: data.error || "Error" };
}
} catch (err) {
showNotification("Нет соединения с сервером", "error");
return { success: false, error: "Нет соединения с сервером" };
showNotification("Sin conexión con el servidor", "error");
return { success: false, error: "Sin conexión con el servidor" };
}
}
@@ -427,14 +430,14 @@ async function apiDeleteReport(reportId) {
});
const data = await response.json();
if (response.ok && data.deleted) {
showNotification("Отчет удален!");
showNotification("Informe eliminado!");
await loadReports();
if (typeof updateDashboard === "function") updateDashboard();
} else {
showNotification(data.error || "Ошибка удаления отчета", "error");
showNotification(data.error || "Error al eliminar informe", "error");
}
} catch {
showNotification("Нет соединения с сервером", "error");
showNotification("Sin conexión con el servidor", "error");
}
}
@@ -478,11 +481,11 @@ async function createReport(data) {
result.error ||
(Array.isArray(result.errors) && result.errors[0]?.msg) ||
response.statusText ||
"Ошибка создания отчета";
"Error al crear informe";
if (response.status === 409) {
errorMsg =
"Отчет за этот магазин и дату уже был отправлен этим пользователем.";
"Ya existe un informe para esta tienda y fecha.";
}
// Debug: Log error case
@@ -498,7 +501,7 @@ async function createReport(data) {
}
} catch (err) {
console.error("Network/server error in createReport:", err);
return { success: false, error: "Нет соединения с сервером" };
return { success: false, error: "Sin conexión con el servidor" };
}
}
@@ -520,11 +523,11 @@ async function getStores() {
} else {
return {
success: false,
error: data.error || "Ошибка получения магазинов",
error: data.error || "Error al obtener tiendas",
};
}
} catch {
return { success: false, error: "Нет соединения с сервером" };
return { success: false, error: "Sin conexión con el servidor" };
}
}
@@ -546,11 +549,11 @@ async function createStore(storeData) {
} else {
return {
success: false,
error: data.error || "Ошибка создания магазина",
error: data.error || "Error al crear tienda",
};
}
} catch {
return { success: false, error: "Нет соединения с сервером" };
return { success: false, error: "Sin conexión con el servidor" };
}
}
@@ -573,7 +576,7 @@ async function updateStore(storeId, storeData) {
status: response.status,
};
} catch {
return { success: false, error: "Нет соединения с сервером" };
return { success: false, error: "Sin conexión con el servidor" };
}
}
@@ -594,10 +597,10 @@ async function deleteStoreApi(storeId) {
} else {
return {
success: false,
error: data.error || "Ошибка удаления магазина",
error: data.error || "Error al eliminar tienda",
};
}
} catch {
return { success: false, error: "Нет соединения с сервером" };
return { success: false, error: "Sin conexión con el servidor" };
}
}

View File

@@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="ru">
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Система учета ежедневной кассовой статистики</title>
<title>Sistema de registro de estadísticas diarias de caja</title>
<link
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
rel="stylesheet"
@@ -13,10 +13,16 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css"
/>
<!-- Flatpickr CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<!-- Flatpickr JS -->
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<!-- Localización española -->
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/es.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body class="bg-gray-100">
<!-- Контейнер приложения -->
<!-- Contenedor de la aplicación -->
<div id="app" class="min-h-screen">
<!-- Экран авторизации -->
<div
@@ -27,15 +33,15 @@
<div class="text-center mb-6">
<i class="fas fa-cash-register text-4xl text-blue-600 mb-4"></i>
<h1 class="text-2xl font-bold text-gray-800">
Система учета кассовой статистики
Sistema de registro de estadísticas de caja
</h1>
<p class="text-gray-600 mt-2">Войдите в систему</p>
<p class="text-gray-600 mt-2">Iniciar sesión</p>
</div>
<form id="loginForm">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
<i class="fas fa-user mr-2"></i>Логин
<i class="fas fa-user mr-2"></i>Usuario
</label>
<input
type="text"
@@ -47,7 +53,7 @@
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2">
<i class="fas fa-lock mr-2"></i>Пароль
<i class="fas fa-lock mr-2"></i>Contraseña
</label>
<input
type="password"
@@ -61,14 +67,14 @@
type="submit"
class="btn-primary w-full text-white font-bold py-2 px-4 rounded-lg"
>
<i class="fas fa-sign-in-alt mr-2"></i>Войти
<i class="fas fa-sign-in-alt mr-2"></i>Acceder
</button>
<div id="demoAccountsHint" hidden>
<div class="mt-4 text-sm text-gray-600 text-center">
<div class="mb-2"><strong>Тестовые аккаунты:</strong></div>
<div>Админ: admin / admin123</div>
<div>Кассир: cashier1 / password123</div>
<div class="mb-2"><strong>Cuentas de prueba:</strong></div>
<div>Administrador: admin / admin123</div>
<div>Cajero: cashier1 / password123</div>
</div>
</div>
</form>
@@ -87,7 +93,7 @@
<div class="container mx-auto flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold">
<i class="fas fa-cash-register mr-2"></i>Кассовый отчет
<i class="fas fa-cash-register mr-2"></i>Reporte de caja
</h1>
<p id="userWelcome" class="text-blue-100"></p>
</div>
@@ -96,13 +102,13 @@
id="todayReportBtn"
class="bg-white text-blue-600 px-4 py-2 rounded-lg hover:bg-blue-50 transition-colors"
>
<i class="fas fa-file-alt mr-2"></i>Отчет за сегодня
<i class="fas fa-file-alt mr-2"></i>Informe de hoy
</button>
<button
id="logoutBtn"
class="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600 transition-colors"
>
<i class="fas fa-sign-out-alt mr-2"></i>Выйти
<i class="fas fa-sign-out-alt mr-2"></i>Salir
</button>
</div>
</div>
@@ -114,7 +120,7 @@
<!-- Дата отчета -->
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2">
<i class="fas fa-calendar-alt mr-2"></i>Дата отчета *
<i class="fas fa-calendar-alt mr-2"></i>Fecha del informe *
</label>
<input
type="date"
@@ -128,34 +134,33 @@
class="hidden mt-2 text-yellow-800 bg-yellow-100 rounded px-4 py-2 text-sm"
>
<i class="fas fa-exclamation-triangle mr-2"></i>
Внимание: изменение даты отчета может повлиять на доступность
редактирования.
Advertencia: cambiar la fecha del informe puede afectar la disponibilidad de edición.
</div>
<!-- Выбор магазина -->
<!-- Selección de tienda -->
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2">
<i class="fas fa-store mr-2"></i>Магазин *
<i class="fas fa-store mr-2"></i>Tienda *
</label>
<select
id="storeSelect"
class="form-input w-full px-3 py-2 rounded-lg"
required
>
<option value="">Выберите магазин</option>
<option value="">Seleccionar tienda</option>
</select>
</div>
<!-- Доходы -->
<div class="report-section income">
<h3 class="text-lg font-bold text-green-700 mb-4">
<i class="fas fa-arrow-up mr-2"></i>Доходы (Ingresos)
<i class="fas fa-arrow-up mr-2"></i>Ingresos
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-gray-700 text-sm font-bold mb-2"
>Income (Выручка за день) * €</label
>Ingresos del día * €</label
>
<input
type="number"
@@ -168,7 +173,7 @@
<div>
<label class="block text-gray-700 text-sm font-bold mb-2"
>Caja inicial (Остаток со вчера) * €</label
>Caja inicial * €</label
>
<input
type="number"
@@ -181,7 +186,7 @@
<div>
<label class="block text-gray-700 text-sm font-bold mb-2"
>Total income (Общий доход)</label
>Ingresos totales</label
>
<input
type="number"
@@ -196,7 +201,7 @@
<!-- Зарплаты -->
<div class="report-section wages">
<h3 class="text-lg font-bold text-yellow-700 mb-4">
<i class="fas fa-users mr-2"></i>Зарплаты (Wages)
<i class="fas fa-users mr-2"></i>Salarios
</h3>
<div id="wagesContainer">
@@ -205,13 +210,13 @@
>
<input
type="text"
placeholder="Имя сотрудника"
placeholder="Nombre del empleado"
class="wage-name form-input px-3 py-2 rounded-lg"
/>
<input
type="number"
step="0.01"
placeholder="Сумма €"
placeholder="Monto €"
class="wage-amount form-input px-3 py-2 rounded-lg"
/>
<button
@@ -228,7 +233,7 @@
id="addWage"
class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors mr-4"
>
<i class="fas fa-plus mr-2"></i>Добавить сотрудника
<i class="fas fa-plus mr-2"></i>Añadir empleado
</button>
<div class="mt-4 text-right">
@@ -238,10 +243,10 @@
</div>
</div>
<!-- Расходы -->
<!-- Gastos -->
<div class="report-section expenses">
<h3 class="text-lg font-bold text-red-700 mb-4">
<i class="fas fa-arrow-down mr-2"></i>Расходы (Expenses)
<i class="fas fa-arrow-down mr-2"></i>Gastos
</h3>
<div id="expensesContainer">
@@ -250,13 +255,13 @@
>
<input
type="text"
placeholder="Название расхода"
placeholder="Nombre del gasto"
class="expense-name form-input px-3 py-2 rounded-lg"
/>
<input
type="number"
step="0.01"
placeholder="Сумма €"
placeholder="Monto €"
class="expense-amount form-input px-3 py-2 rounded-lg"
/>
<button
@@ -273,7 +278,7 @@
id="addExpense"
class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors mr-4"
>
<i class="fas fa-plus mr-2"></i>Добавить расход
<i class="fas fa-plus mr-2"></i>Añadir gasto
</button>
<div class="mt-4 text-right">
@@ -285,16 +290,16 @@
</div>
</div>
<!-- Итоги -->
<!-- Resumen -->
<div class="report-section">
<h3 class="text-lg font-bold text-gray-700 mb-4">
<i class="fas fa-calculator mr-2"></i>Итого
<i class="fas fa-calculator mr-2"></i>Resumen
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label class="block text-gray-700 text-sm font-bold mb-2"
>Total income</label
>Ingresos totales</label
>
<input
type="number"
@@ -306,7 +311,7 @@
<div>
<label class="block text-gray-700 text-sm font-bold mb-2"
>Total expenses €</label
>Gastos totales €</label
>
<input
type="number"
@@ -318,7 +323,7 @@
<div>
<label class="block text-gray-700 text-sm font-bold mb-2"
>Envelope (в конверт) * €</label
>Sobre * €</label
>
<input
type="number"
@@ -332,7 +337,7 @@
<div class="text-center bg-blue-50 p-4 rounded-lg">
<span class="text-xl font-bold text-blue-700"
>Caja final (остаток на завтра): €<span id="cajaFinal"
>Caja final: €<span id="cajaFinal"
>0.00</span
></span
>
@@ -344,7 +349,7 @@
type="submit"
class="btn-primary text-white font-bold py-3 px-8 rounded-lg text-lg"
>
<i class="fas fa-save mr-2"></i>Сохранить отчет
<i class="fas fa-save mr-2"></i>Guardar informe
</button>
</div>
</form>
@@ -358,7 +363,7 @@
<div class="container mx-auto flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold">
<i class="fas fa-chart-line mr-2"></i>Админ панель
<i class="fas fa-chart-line mr-2"></i>Panel de administración
</h1>
<p id="adminWelcome" class="text-blue-100"></p>
</div>
@@ -366,12 +371,12 @@
id="adminLogoutBtn"
class="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600 transition-colors"
>
<i class="fas fa-sign-out-alt mr-2"></i>Выйти
<i class="fas fa-sign-out-alt mr-2"></i>Salir
</button>
</div>
</header>
<!-- Навигация -->
<!-- Navegación -->
<div class="bg-white shadow">
<div class="container mx-auto">
<div class="flex space-x-0">
@@ -385,19 +390,19 @@
class="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"
data-tab="reports"
>
<i class="fas fa-file-alt mr-2"></i>Отчеты
<i class="fas fa-file-alt mr-2"></i>Informes
</button>
<button
class="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"
data-tab="users"
>
<i class="fas fa-users mr-2"></i>Пользователи
<i class="fas fa-users mr-2"></i>Usuarios
</button>
<button
class="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"
data-tab="stores"
>
<i class="fas fa-store mr-2"></i>Магазины
<i class="fas fa-store mr-2"></i>Tiendas
</button>
<button
class="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"
@@ -409,7 +414,7 @@
</div>
</div>
<!-- Контент вкладок -->
<!-- Contenido de pestañas -->
<div class="container mx-auto p-6">
<!-- Dashboard -->
<div id="dashboardTab" class="admin-tab-content">
@@ -423,7 +428,7 @@
<p
class="text-white-contrast text-sm font-medium uppercase tracking-wide"
>
Общий доход
Ingresos totales
</p>
<p
class="text-white-contrast text-3xl font-bold"
@@ -448,7 +453,7 @@
<p
class="text-white-contrast text-sm font-medium uppercase tracking-wide"
>
Общие расходы
Gastos totales
</p>
<p
class="text-white-contrast text-3xl font-bold"
@@ -473,7 +478,7 @@
<p
class="text-white-contrast text-sm font-medium uppercase tracking-wide"
>
Количество отчетов
Número de informes
</p>
<p
class="text-white-contrast text-3xl font-bold"
@@ -514,12 +519,12 @@
</div>
</div>
<!-- Графики -->
<!-- Gráficos -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white p-6 rounded-lg shadow-lg">
<h3 class="text-lg font-bold text-gray-800 mb-4">
<i class="fas fa-chart-line mr-2 text-blue-600"></i>Доходы по
дням
<i class="fas fa-chart-line mr-2 text-blue-600"></i>Ingresos por día
</h3>
<div style="height: 300px">
<canvas id="revenueChart"></canvas>
@@ -528,8 +533,7 @@
<div class="bg-white p-6 rounded-lg shadow-lg">
<h3 class="text-lg font-bold text-gray-800 mb-4">
<i class="fas fa-chart-pie mr-2 text-red-600"></i>Расходы по
категориям
<i class="fas fa-chart-pie mr-2 text-red-600"></i>Gastos por categoría
</h3>
<div style="height: 300px">
<canvas id="expensesChart"></canvas>
@@ -538,8 +542,7 @@
<div class="bg-white p-6 rounded-lg shadow-lg">
<h3 class="text-lg font-bold text-gray-800 mb-4">
<i class="fas fa-chart-bar mr-2 text-green-600"></i>Сравнение
по магазинам
<i class="fas fa-chart-bar mr-2 text-green-600"></i>Comparación por tiendas
</h3>
<div style="height: 300px">
<canvas id="storesChart"></canvas>
@@ -548,8 +551,7 @@
<div class="bg-white p-6 rounded-lg shadow-lg">
<h3 class="text-lg font-bold text-gray-800 mb-4">
<i class="fas fa-chart-area mr-2 text-purple-600"></i>Тренды
продаж
<i class="fas fa-chart-area mr-2 text-purple-600"></i>Tendencias de ventas
</h3>
<div style="height: 300px">
<canvas id="trendsChart"></canvas>
@@ -563,27 +565,27 @@
<div class="bg-white rounded-lg shadow-lg">
<div class="p-6 border-b">
<h2 class="text-xl font-bold text-gray-800">
<i class="fas fa-file-alt mr-2"></i>Управление отчетами
<i class="fas fa-file-alt mr-2"></i>Gestión de informes
</h2>
</div>
<!-- Фильтры -->
<!-- Filtros -->
<div class="p-6 bg-gray-50 border-b">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"
>Магазин</label
>Tienda</label
>
<select
id="filterStore"
class="form-input w-full px-3 py-2 rounded-lg"
>
<option value="">Все магазины</option>
<option value="">Todas las tiendas</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"
>Дата от</label
>Fecha desde</label
>
<input
type="date"
@@ -593,7 +595,7 @@
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"
>Дата до</label
>Fecha hasta</label
>
<input
type="date"
@@ -606,7 +608,7 @@
id="applyFilters"
class="btn-primary text-white px-4 py-2 rounded-lg mr-2"
>
<i class="fas fa-search mr-2"></i>Поиск
<i class="fas fa-search mr-2"></i>Buscar
</button>
<button
id="exportExcel"
@@ -618,7 +620,7 @@
</div>
</div>
<!-- Таблица отчетов -->
<!-- Tabla de informes -->
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-100">
@@ -626,42 +628,42 @@
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Дата
Fecha
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Магазин
Tienda
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Доход
Ingresos
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Расходы
Gastos
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Прибыль
Beneficio
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Пользователь
Usuario
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Статус
Estado
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Действия
Acciones
</th>
</tr>
</thead>
@@ -679,13 +681,13 @@
<div class="bg-white rounded-lg shadow-lg">
<div class="p-6 border-b flex justify-between items-center">
<h2 class="text-xl font-bold text-gray-800">
<i class="fas fa-users mr-2"></i>Управление пользователями
<i class="fas fa-users mr-2"></i>Gestión de usuarios
</h2>
<button
id="addUserBtn"
class="btn-primary text-white px-4 py-2 rounded-lg"
>
<i class="fas fa-plus mr-2"></i>Добавить пользователя
<i class="fas fa-plus mr-2"></i>Añadir usuario
</button>
</div>
@@ -701,22 +703,22 @@
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Логин
Usuario
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Роль
Rol
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Доступные магазины
Tiendas disponibles
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Действия
Acciones
</th>
</tr>
</thead>
@@ -734,13 +736,13 @@
<div class="bg-white rounded-lg shadow-lg">
<div class="p-6 border-b flex justify-between items-center">
<h2 class="text-xl font-bold text-gray-800">
<i class="fas fa-store mr-2"></i>Управление магазинами
<i class="fas fa-store mr-2"></i>Gestión de tiendas
</h2>
<button
id="addStoreBtn"
class="btn-primary text-white px-4 py-2 rounded-lg"
>
<i class="fas fa-plus mr-2"></i>Добавить магазин
<i class="fas fa-plus mr-2"></i>Añadir tienda
</button>
</div>
@@ -756,17 +758,17 @@
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Название
Nombre
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Количество отчетов
Número de informes
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Действия
Acciones
</th>
</tr>
</thead>
@@ -784,14 +786,14 @@
<div class="bg-white rounded-lg shadow-lg">
<div class="p-6 border-b flex justify-between items-center">
<h2 class="text-xl font-bold text-gray-800">
<i class="fas fa-tasks mr-2"></i>TODO лист - Отслеживание
<i class="fas fa-tasks mr-2"></i>Lista TODO - Seguimiento
изменений
</h2>
<button
id="addTodoBtn"
class="btn-primary text-white px-4 py-2 rounded-lg"
>
<i class="fas fa-plus mr-2"></i>Добавить задачу
<i class="fas fa-plus mr-2"></i>Añadir tarea
</button>
</div>
@@ -808,7 +810,7 @@
<div class="modal-content">
<div class="modal-header">
<h3 class="text-xl font-bold text-gray-800" id="reportModalTitle">
Просмотр отчета
Ver informe
</h3>
</div>
<div class="modal-body">
@@ -829,14 +831,14 @@
<div class="modal-content">
<div class="modal-header">
<h3 class="text-xl font-bold text-gray-800" id="userModalTitle">
Пользователь
Usuario
</h3>
</div>
<div class="modal-body">
<form id="userEditForm">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2"
>Логин</label
>Usuario</label
>
<input
type="text"
@@ -848,7 +850,7 @@
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2"
>Пароль</label
>Contraseña</label
>
<div class="relative">
<input
@@ -861,34 +863,34 @@
id="togglePasswordBtn"
class="absolute right-3 top-1/4 -translate-y-1/2 text-xl text-gray-500 hover:text-gray-700"
tabindex="-1"
aria-label="Показать пароль"
aria-label="Mostrar contraseña"
>
<span id="eyeIcon">👁️</span>
</button>
</div>
<small class="text-gray-500"
>Если не хотите менять пароль, оставьте поле без
изменений</small
>Si no desea cambiar la contraseña, deje este campo sin
cambios</small
>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2"
>Роль</label
>Rol</label
>
<select
id="userRole"
class="form-input w-full px-3 py-2 rounded-lg"
required
>
<option value="employee">Сотрудник</option>
<option value="admin">Администратор</option>
<option value="employee">Empleado</option>
<option value="admin">Administrador</option>
</select>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2"
>Доступные магазины</label
>Tiendas disponibles</label
>
<div id="userStoresAccess" class="space-y-2">
<!-- Чекбоксы магазинов -->
@@ -901,13 +903,13 @@
id="saveUserBtn"
class="btn-success text-white px-4 py-2 rounded-lg mr-2"
>
<i class="fas fa-save mr-2"></i>Сохранить
<i class="fas fa-save mr-2"></i>Guardar
</button>
<button
id="cancelUserBtn"
class="bg-gray-500 text-white px-4 py-2 rounded-lg"
>
<i class="fas fa-times mr-2"></i>Отмена
<i class="fas fa-times mr-2"></i>Cancelar
</button>
</div>
</div>
@@ -918,14 +920,14 @@
<div class="modal-content">
<div class="modal-header">
<h3 class="text-xl font-bold text-gray-800" id="storeModalTitle">
Магазин
Tienda
</h3>
</div>
<div class="modal-body">
<form id="storeEditForm">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2"
>Название магазина</label
>Nombre de la tienda</label
>
<input
type="text"
@@ -942,13 +944,13 @@
id="saveStoreBtn"
class="btn-success text-white px-4 py-2 rounded-lg mr-2"
>
<i class="fas fa-save mr-2"></i>Сохранить
<i class="fas fa-save mr-2"></i>Guardar
</button>
<button
id="cancelStoreBtn"
class="bg-gray-500 text-white px-4 py-2 rounded-lg"
>
<i class="fas fa-times mr-2"></i>Отмена
<i class="fas fa-times mr-2"></i>Cancelar
</button>
</div>
</div>
@@ -959,14 +961,14 @@
<div class="modal-content">
<div class="modal-header">
<h3 class="text-xl font-bold text-gray-800" id="todoModalTitle">
TODO задача
Tarea TODO
</h3>
</div>
<div class="modal-body">
<form id="todoEditForm">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2"
>Заголовок</label
>Título</label
>
<input
type="text"
@@ -978,7 +980,7 @@
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2"
>Описание</label
>Descripción</label
>
<textarea
id="todoDescription"
@@ -989,15 +991,15 @@
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2"
>Приоритет</label
>Prioridad</label
>
<select
id="todoPriority"
class="form-input w-full px-3 py-2 rounded-lg"
>
<option value="low">Низкий</option>
<option value="medium">Средний</option>
<option value="high">Высокий</option>
<option value="low">Baja</option>
<option value="medium">Media</option>
<option value="high">Alta</option>
</select>
</div>
</form>
@@ -1007,13 +1009,13 @@
id="saveTodoBtn"
class="btn-success text-white px-4 py-2 rounded-lg mr-2"
>
<i class="fas fa-save mr-2"></i>Сохранить
<i class="fas fa-save mr-2"></i>Guardar
</button>
<button
id="cancelTodoBtn"
class="bg-gray-500 text-white px-4 py-2 rounded-lg"
>
<i class="fas fa-times mr-2"></i>Отмена
<i class="fas fa-times mr-2"></i>Cancelar
</button>
</div>
</div>
@@ -1024,7 +1026,7 @@
<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 id="notificationText">Успешно сохранено!</span>
<span id="notificationText">Guardado exitosamente!</span>
</div>
</div>
</div>

86
frontend/locales/es.json Normal file
View File

@@ -0,0 +1,86 @@
{
"errors": {
"server_connection": "Sin conexión con el servidor",
"auth_error": "Error de autenticación",
"user_creation": "Error al crear usuario",
"user_update": "Error al actualizar usuario",
"user_delete": "Error al eliminar usuario",
"report_creation": "Error al crear informe",
"report_update": "Error al actualizar informe",
"report_delete": "Error al eliminar informe",
"report_duplicate": "Ya existe un informe para esta tienda y fecha",
"store_creation": "Error al crear tienda",
"store_update": "Error al actualizar tienda",
"store_delete": "Error al eliminar tienda",
"data_loading": "Error al cargar datos"
},
"notifications": {
"login_success": "Autenticación exitosa",
"logout": "Sesión cerrada",
"report_created": "Informe creado exitosamente",
"report_updated": "Informe actualizado",
"report_deleted": "Informe eliminado",
"report_verified": "Informe verificado",
"user_created": "Usuario creado",
"user_updated": "Usuario actualizado",
"user_deleted": "Usuario eliminado",
"store_created": "Tienda creada",
"store_updated": "Tienda actualizada",
"store_deleted": "Tienda eliminada",
"todo_created": "Tarea creada",
"todo_updated": "Tarea actualizada",
"todo_deleted": "Tarea eliminada"
},
"ui": {
"login": {
"title": "Sistema de estadísticas de caja diaria",
"subtitle": "Iniciar sesión",
"username": "Usuario",
"password": "Contraseña",
"submit": "Entrar",
"demo_accounts": "Cuentas de prueba:",
"admin": "Admin: admin / admin123",
"cashier": "Cajero: cashier1 / password123"
},
"buttons": {
"add_field": "Añadir campo adicional",
"add_employee": "Añadir empleado",
"add_expense": "Añadir gasto"
},
"user": {
"welcome": "Bienvenido, {username}!",
"logout": "Salir",
"today_report": "Informe de hoy"
},
"admin": {
"welcome": "Bienvenido, {username}!",
"logout": "Salir",
"dashboard": "Panel",
"reports": "Informes",
"users": "Usuarios",
"stores": "Tiendas",
"todos": "Tareas",
"users_management": "Gestión de usuarios",
"stores_management": "Gestión de tiendas",
"reports_management": "Gestión de informes"
},
"report": {
"title": "Informe de caja",
"date": "Fecha",
"store": "Tienda",
"income": "Ingresos",
"expenses": "Gastos",
"profit": "Beneficio",
"status": "Estado",
"verified": "Verificado",
"unverified": "No verificado",
"view": "Ver",
"edit": "Editar",
"delete": "Eliminar",
"verify": "Verificar",
"unverify": "Desverificar",
"save": "Guardar",
"cancel": "Cancelar"
}
}
}

View File

@@ -1,6 +1,19 @@
//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');
@@ -81,12 +94,12 @@ function showNotification(message, type = "success") {
}, 3000);
}
// Управление модальными окнами с исправленной прокруткой
// Gestión de ventanas modales con desplazamiento corregido
function showModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add("show");
// Блокируем прокрутку body
// Bloquear desplazamiento del cuerpo
document.body.classList.add("modal-open");
}
}
@@ -200,10 +213,10 @@ document.getElementById("loginForm").addEventListener("submit", async (e) => {
showUserInterface();
}
showNotification("Успешная авторизация!");
showNotification("¡Autenticación exitosa!");
} else {
const errorDiv = document.getElementById("loginError");
errorDiv.textContent = result.error || "Неверный логин или пароль";
errorDiv.textContent = result.error || "Usuario o contraseña incorrectos";
errorDiv.classList.remove("hidden");
}
});
@@ -213,7 +226,7 @@ async function showUserInterface() {
document.getElementById("userInterface").classList.remove("hidden");
document.getElementById(
"userWelcome"
).textContent = `Добро пожаловать, ${appState.currentUser.username}!`;
).textContent = `Bienvenido, ${appState.currentUser.username}!`;
loadUserStores();
setupFormCalculations();
@@ -226,7 +239,7 @@ async function showAdminInterface() {
document.getElementById("adminInterface").classList.remove("hidden");
document.getElementById(
"adminWelcome"
).textContent = `Добро пожаловать, ${appState.currentUser.username}!`;
).textContent = `Bienvenido, ${appState.currentUser.username}!`;
await loadStores();
await loadUsers();
@@ -279,7 +292,7 @@ function logout() {
document.getElementById("loginForm").reset();
document.getElementById("loginError").classList.add("hidden");
showNotification("Вы вышли из системы");
showNotification("¡Sesión cerrada!");
}
//DASHBOARD
@@ -392,7 +405,7 @@ function createCharts() {
appState.expensesChartInstance = new Chart(expensesCtx, {
type: "doughnut",
data: {
labels: ["Зарплаты", "Прочие расходы"],
labels: ["Salarios", "Otros gastos"],
datasets: [
{
data: [totalWages, totalInternal],
@@ -429,7 +442,7 @@ function createCharts() {
labels: storeData.map((s) => s.name),
datasets: [
{
label: "Доходы по магазинам",
label: "Ingresos por tienda",
data: storeData.map((s) => s.revenue),
backgroundColor: "#10B981",
},
@@ -487,7 +500,7 @@ function createCharts() {
labels: last30Days,
datasets: [
{
label: "Прибыль",
label: "Beneficio",
data: profitData,
borderColor: "#8B5CF6",
backgroundColor: "rgba(139, 92, 246, 0.1)",
@@ -536,7 +549,7 @@ async function loadReports() {
appState.reportsList = result.success ? result.reports : [];
if (!result.success) {
showNotification(result.error || "Ошибка загрузки отчетов", "error");
showNotification(result.error || "Error al cargar informes", "error");
return;
}
@@ -604,19 +617,19 @@ async function loadReports() {
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}">
${report.isVerified ? "Проверен" : "Не проверен"}
${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> Просмотр
<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> Удалить
<i class="fas fa-trash"></i> Eliminar
</button>
</td>
`;
@@ -720,10 +733,10 @@ function editReport(report) {
buttons.innerHTML = `
<button id="saveReportBtn" class="btn-success text-white px-4 py-2 rounded-lg">
<i class="fas fa-save mr-2"></i>Сохранить
<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>Отмена
<i class="fas fa-times mr-2"></i>Cancelar
</button>
`;
@@ -804,26 +817,26 @@ function showReportModal(report, isAdmin = false) {
const buttons = document.getElementById("reportModalButtons");
const title = document.getElementById("reportModalTitle");
title.textContent = `Отчет от ${report.reportDate || report.date} - ${
report.storeName || report.storeId || "Неизвестный магазин"
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>Основная информация</h4>
<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>Дата:</strong> ${
<div><strong>Fecha:</strong> ${
report.date || report.reportDate || ""
}</div>
<div><strong>Магазин:</strong> ${
<div><strong>Tienda:</strong> ${
report.storeName || report.storeId
}</div>
<div><strong>Пользователь:</strong> ${
<div><strong>Usuario:</strong> ${
report.username || report.fullName || report.userId
}</div>
<div><strong>Статус:</strong>
<div><strong>Estado:</strong>
<span class="px-2 py-1 rounded text-xs ${
report.isVerified || report.verified
? "bg-green-100 text-green-800"
@@ -831,29 +844,29 @@ function showReportModal(report, isAdmin = false) {
}">
${
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>
<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>Ingresos del día:</strong> €${safeToFixed(report.income)}</div>
<div><strong>Caja inicial:</strong> €${safeToFixed(
report.cajaInicial || report.initialCash
)}</div>
<div><strong>Total income:</strong> €${safeToFixed(
<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>Зарплаты (Wages)</h4>
<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
? `
@@ -870,18 +883,18 @@ function showReportModal(report, isAdmin = false) {
.join("")}
<div class="border-t pt-2 font-bold">
<div class="flex justify-between">
<span>Total wages:</span>
<span>Total salarios:</span>
<span>€${safeToFixed(report.totalWages)}</span>
</div>
</div>
</div>
`
: '<p class="text-gray-500 text-sm">Нет данных о зарплатах</p>'
: '<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>Расходы (Expenses)</h4>
<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
? `
@@ -898,26 +911,26 @@ function showReportModal(report, isAdmin = false) {
.join("")}
<div class="border-t pt-2 font-bold">
<div class="flex justify-between">
<span>Total expenses internal:</span>
<span>Total gastos internos:</span>
<span>€${safeToFixed(totalExpensesInternal)}</span>
</div>
</div>
</div>
`
: '<p class="text-gray-500 text-sm">Нет данных о расходах</p>'
: '<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>Итоговые расчеты</h4>
<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>Total income:</strong> <span>€${safeToFixed(
<div class="flex justify-between"><strong>Ingresos totales:</strong> <span>€${safeToFixed(
report.totalIncome
)}</span></div>
<div class="flex justify-between"><strong>Total expenses:</strong> <span>€${safeToFixed(
<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(
<div class="flex justify-between"><strong>Sobre:</strong> <span>€${safeToFixed(
report.envelope
)}</span></div>
<div class="flex justify-between border-t pt-2 text-lg font-bold text-blue-700">
@@ -937,14 +950,14 @@ function showReportModal(report, isAdmin = false) {
buttons.innerHTML = `
<div class="grid grid-cols-2 items-center justify-between gap-4">
<div class="text-yellow-700 font-medium text-sm">
Отчет подтвержден.<br>Для внесения изменений снимите подтверждение.
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>Снять подтверждение
<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>Закрыть
<i class="fas fa-times mr-2"></i>Cerrar
</button>
</div>
</div>
@@ -957,13 +970,13 @@ function showReportModal(report, isAdmin = false) {
} 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>Редактировать
<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>Подтвердить отчет
<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>Закрыть
<i class="fas fa-times mr-2"></i>Cerrar
</button>
`;
document.getElementById("editReportBtn").addEventListener("click", () => {
@@ -979,10 +992,10 @@ function showReportModal(report, isAdmin = false) {
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>Редактировать
<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>Закрыть
<i class="fas fa-times mr-2"></i>Cerrar
</button>
`;
document
@@ -994,7 +1007,7 @@ function showReportModal(report, isAdmin = false) {
});
} else {
buttons.innerHTML = `
<div class="mb-2 text-yellow-700 font-medium text-sm">Отчет подтвержден и не может быть изменен. Обратитесь к администратору для изменений.</div>
<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>
@@ -1014,7 +1027,7 @@ function showReportModal(report, isAdmin = false) {
// helpder of the modal of showReportModal - unverify report for ADMIN
async function unverifyReport(reportId) {
await updateReport(reportId, { isVerified: 0 });
showNotification("Подтверждение снято. Теперь можно редактировать.");
showNotification("Verificación retirada. Ahora se puede editar.");
await loadReports();
hideModal("reportViewModal");
}
@@ -1083,34 +1096,34 @@ function applyReportsFilters() {
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}">
${report.isVerified ? "Проверен" : "Не проверен"}
${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> Просмотр
<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> Удалить
<i class="fas fa-trash"></i> Eliminar
</button>
</td>
`;
tbody.appendChild(row);
});
showNotification(`Найдено ${filteredReports.length} отчетов`);
showNotification(`Se encontraron ${filteredReports.length} informes`);
}
function deleteReport(reportId) {
showConfirmModal("Вы уверены, что хотите удалить этот отчет?", () => {
showConfirmModal("¿Estás seguro de que deseas eliminar este informe?", () => {
apiDeleteReport(reportId);
loadReports();
updateDashboard();
showNotification("Отчет удален!");
showNotification("¡Informe eliminado!");
});
}
@@ -1174,8 +1187,8 @@ function fillFormWithReport(report) {
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">
<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>
@@ -1194,8 +1207,8 @@ function fillFormWithReport(report) {
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">
<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>
@@ -1217,7 +1230,7 @@ function exportToExcel() {
const users = appState.usersList || [];
if (!reports.length) {
showNotification("Нет отчетов для экспорта", "info");
showNotification("No hay informes para exportar", "info");
return;
}
@@ -1233,21 +1246,21 @@ function exportToExcel() {
return {
Дата: report.reportDate || report.date || "",
Магазин: store.name || report.storeName || "Неизвестно",
Tienda: store.name || report.storeName || "Desconocido",
Доход: `${Number(report.totalIncome || 0).toFixed(2)}`,
Расходы: `${Number(report.totalExpenses || 0).toFixed(2)}`,
Прибыль: `${profit.toFixed(2)}`,
Пользователь: user.username || report.username || "Неизвестно",
Статус: report.isVerified
? "Проверен"
Usuario: user.username || report.username || "Desconocido",
Estado: report.isVerified
? "Verificado"
: report.verified
? "Проверен"
: "Не проверен",
? "Verificado"
: "No verificado",
};
});
if (!data.length) {
showNotification("Нет отчетов для экспорта", "info");
showNotification("No hay informes para exportar", "info");
return;
}
@@ -1274,7 +1287,7 @@ function exportToExcel() {
link.click();
document.body.removeChild(link);
showNotification("Отчет экспортирован!");
showNotification("¡Informe exportado!");
}
//REPORTS FORM LOGIC
@@ -1284,7 +1297,7 @@ function loadUserStores() {
const select = document.getElementById("storeSelect");
if (!select) return;
select.innerHTML =
'<option value="" disabled selected hidden>Выберите магазин</option>';
'<option value="" disabled selected hidden>Seleccione tienda</option>';
if (!appState.currentUser) return;
// For admin: show all
@@ -1398,8 +1411,8 @@ function addWageRow() {
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">
<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>
@@ -1412,8 +1425,8 @@ function addExpenseRow() {
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">
<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>
@@ -1553,7 +1566,7 @@ document.getElementById("reportForm").addEventListener("submit", async (e) => {
}
// if result is missing --> error
if (!result) {
showNotification("Нет ответа от сервера. Попробуйте еще раз.", "error");
showNotification("No hay respuesta del servidor. Inténtalo de nuevo.", "error");
return;
}
@@ -1578,14 +1591,14 @@ document.getElementById("reportForm").addEventListener("submit", async (e) => {
upsertTodaysReport(newReport);
showNotification(
wasEdit ? "Отчет успешно отредактирован!" : "Отчет успешно создан!"
wasEdit ? "¡Informe editado con éxito!" : "¡Informe creado con éxito!"
);
await loadReports();
} else {
resetReportForm();
window.scrollTo({ top: 0, behavior: "smooth" });
showNotification(result.error || "Ошибка создания отчета", "error");
showNotification(result.error || "Error al crear el informe", "error");
}
});
@@ -1619,7 +1632,7 @@ function resetReportForm() {
document.getElementById("todayReportBtn").addEventListener("click", () => {
const storeId = document.getElementById("storeSelect").value;
if (!storeId) {
showNotification("Пожалуйста, выберите магазин!", "error");
showNotification("¡Por favor seleccione una tienda!", "error");
return;
}
@@ -1633,7 +1646,7 @@ document.getElementById("todayReportBtn").addEventListener("click", () => {
if (todaysReport) {
showReportModal(todaysReport, false);
} else {
showNotification("Сегодняшний отчет еще не создан", "error");
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 = "";
@@ -1671,9 +1684,9 @@ async function loadUsers() {
const store = (appState.storesList || []).find(
(s) => s.id === storeId
);
return store ? store.name : "Нет доступа";
return store ? store.name : "Sin acceso";
})
.join(", ") || "Нет доступа";
.join(", ") || "Sin acceso";
const row = document.createElement("tr");
row.className = "hover:bg-gray-50";
@@ -1686,7 +1699,7 @@ async function loadUsers() {
? "bg-purple-100 text-purple-800"
: "bg-blue-100 text-blue-800"
}">
${user.role === "admin" ? "Администратор" : "Сотрудник"}
${user.role === "admin" ? "Administrador" : "Empleado"}
</span>
</td>
<td class="px-6 py-4 text-sm text-gray-900">${userStores}</td>
@@ -1694,12 +1707,12 @@ async function loadUsers() {
<button class="text-blue-600 hover:text-blue-900 mr-2" onclick="editUser(${
user.id
})">
<i class="fas fa-edit"></i> Редактировать
<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> Удалить
<i class="fas fa-trash"></i> Eliminar
</button>
</td>
`;
@@ -1707,7 +1720,7 @@ async function loadUsers() {
});
if (users.length === 0) {
showNotification("Нет пользователей для отображения", "info");
showNotification("No hay usuarios para mostrar", "info");
}
}
@@ -1726,8 +1739,8 @@ function showUserEditModal(user) {
// 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");
@@ -1794,7 +1807,7 @@ async function saveUser() {
).map((cb) => parseInt(cb.value));
if (!login) {
showNotification("Заполните логин!", "error");
showNotification("¡Complete el nombre de usuario!", "error");
return;
}
@@ -1810,19 +1823,19 @@ async function saveUser() {
let result;
if (!appState.editingUserId) {
if (!password) {
showNotification("Укажите пароль для нового пользователя!", "error");
showNotification("¡Especifique una contraseña para el nuevo usuario!", "error");
return;
}
// CREATE
result = await createUser(userData);
if (result.success) {
showNotification("Пользователь добавлен!");
showNotification("¡Usuario agregado!");
}
} else {
// EDIT
result = await updateUser(appState.editingUserId, userData);
if (result.success) {
showNotification("Пользователь обновлен!");
showNotification("¡Usuario actualizado!");
}
appState.editingUserId = null;
}
@@ -1834,13 +1847,13 @@ async function saveUser() {
updateDashboard();
loadReports();
} else if (result) {
showNotification(result.error || "Ошибка операции", "error");
showNotification(result.error || "Error de operación", "error");
}
}
//UI trigger: delete user with modal
function deleteUser(userId) {
showConfirmModal("Вы уверены, что хотите удалить этого пользователя?", () =>
showConfirmModal("¿Está seguro de que desea eliminar este usuario?", () =>
apiDeleteUser(userId)
);
}
@@ -1882,7 +1895,7 @@ async function loadStores() {
const result = await getStores();
if (!result.success) {
showNotification(result.error || "Ошибка загрузки магазинов", "error");
showNotification(result.error || "Error al cargar tiendas", "error");
appState.storesList = result.stores;
return;
}
@@ -1907,12 +1920,12 @@ async function loadStores() {
<button class="text-blue-600 hover:text-blue-900 mr-2" onclick="editStore(${
store.id
})">
<i class="fas fa-edit"></i> Редактировать
<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> Удалить
<i class="fas fa-trash"></i> Eliminar
</button>
</td>
`;
@@ -1920,7 +1933,7 @@ async function loadStores() {
});
if (stores.length === 0) {
showNotification("Нет магазинов для отображения", "info");
showNotification("No hay tiendas para mostrar", "info");
}
}
@@ -1941,11 +1954,11 @@ function showStoreEditModal() {
);
if (!store) {
showNotification("Магазин не найден!", "error");
showNotification("¡Tienda no encontrada!", "error");
return;
}
title.textContent = "Редактирование магазина";
title.textContent = "Editar tienda";
document.getElementById("storeName").value = store.name || "";
showModal("storeEditModal");
}
@@ -1953,7 +1966,7 @@ function showStoreEditModal() {
async function saveStore() {
const name = document.getElementById("storeName").value.trim();
if (!name) {
showNotification("Заполните название магазина!", "error");
showNotification("¡Complete el nombre de la tienda!", "error");
return;
}
@@ -1962,7 +1975,7 @@ async function saveStore() {
// Add
result = await createStore({ name });
if (result.success) {
showNotification("Магазин добавлен!");
showNotification("¡Tienda agregada!");
hideModal("storeEditModal");
await loadStores();
await loadReports();
@@ -1974,7 +1987,7 @@ async function saveStore() {
// Edit
result = await updateStore(appState.editingStoreId, { name });
if (result.success) {
showNotification("Магазин обновлен!");
showNotification("¡Tienda actualizada!");
hideModal("storeEditModal");
await loadStores();
await loadReports();
@@ -1989,9 +2002,9 @@ async function saveStore() {
result.status === 409 ||
(result.error && result.error.includes("already exists"))
) {
showNotification("Магазин с таким названием уже существует!", "error");
showNotification("¡Ya existe una tienda con este nombre!", "error");
} else {
showNotification(result.error || "Ошибка сохранения магазина", "error");
showNotification(result.error || "Error al guardar tienda", "error");
}
return;
}
@@ -2005,10 +2018,10 @@ async function saveStore() {
// Удаление магазина
function deleteStore(storeId) {
const store = appState.storesList.find((s) => s.id === storeId);
let message = `Вы уверены, что хотите удалить этот магазин "${store.name}"?`;
let message = `¿Está seguro de que desea eliminar esta tienda "${store.name}"?`;
if (store.reportsCount && store.reportsCount > 0) {
message += `\n\nОбратите внимание! У этого магазина ${store.reportsCount} есть связынные с ним отчеты. Они также будут удалены.`;
message += `\n\n¡Atención! Esta tienda tiene ${store.reportsCount} informes asociados. También serán eliminados.`;
}
showConfirmModal(message, () => handleDeleteStore(storeId));
@@ -2031,7 +2044,7 @@ async function handleDeleteStore(storeId) {
document.getElementById("addStoreBtn").onclick = () => {
appState.editingStoreId = null;
document.getElementById("storeModalTitle").textContent =
"Добавление магазина";
"Agregar tienda";
document.getElementById("storeName").value = "";
showModal("storeEditModal");
};
@@ -2071,9 +2084,9 @@ function loadTodos() {
};
const priorityText = {
low: "Низкий",
medium: "Средний",
high: "Высокий",
low: "Bajo",
medium: "Medio",
high: "Alto",
};
todoItem.innerHTML = `
@@ -2128,7 +2141,7 @@ function toggleTodo(todoId) {
const todoIndex = database.todos.findIndex((t) => t.id === todoId);
database.todos[todoIndex].completed = !database.todos[todoIndex].completed;
loadTodos();
showNotification("Статус задачи обновлен!");
showNotification("¡Estado de tarea actualizado!");
}
// Редактирование TODO
@@ -2145,12 +2158,12 @@ function showTodoEditModal() {
if (appState.editingTodoId) {
const todo = database.todos.find((t) => t.id === appState.editingTodoId);
title.textContent = "Редактирование задачи";
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 = "Добавление задачи";
title.textContent = "Agregar tarea";
form.reset();
}
@@ -2163,7 +2176,7 @@ function saveTodo() {
const priority = document.getElementById("todoPriority").value;
if (!title) {
showNotification("Заполните заголовок!", "error");
showNotification("¡Complete el título!", "error");
return;
}
@@ -2175,7 +2188,7 @@ function saveTodo() {
database.todos[todoIndex].title = title;
database.todos[todoIndex].description = description;
database.todos[todoIndex].priority = priority;
showNotification("Задача обновлена!");
showNotification("¡Tarea actualizada!");
} else {
// Добавление
const newId = Math.max(...database.todos.map((t) => t.id)) + 1;
@@ -2187,7 +2200,7 @@ function saveTodo() {
completed: false,
createdAt: new Date().toISOString().split("T")[0],
});
showNotification("Задача добавлена!");
showNotification("¡Tarea agregada!");
}
hideModal("todoEditModal");
@@ -2196,10 +2209,10 @@ function saveTodo() {
// Удаление TODO
function deleteTodo(todoId) {
if (confirm("Вы уверены, что хотите удалить эту задачу?")) {
if (confirm("¿Está seguro de que desea eliminar esta tarea?")) {
database.todos = database.todos.filter((t) => t.id !== todoId);
loadTodos();
showNotification("Задача удалена!");
showNotification("¡Tarea eliminada!");
}
}