mirror of
https://github.com/marcogll/ap_pos.git
synced 2026-01-13 13:15:16 +00:00
agrega funcion de users y dashboard
This commit is contained in:
@@ -18,6 +18,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
let settings = {};
|
let settings = {};
|
||||||
let movements = [];
|
let movements = [];
|
||||||
let clients = [];
|
let clients = [];
|
||||||
|
let incomeChart = null;
|
||||||
|
|
||||||
// --- DOM ELEMENTS ---
|
// --- DOM ELEMENTS ---
|
||||||
const formSettings = document.getElementById('formSettings');
|
const formSettings = document.getElementById('formSettings');
|
||||||
@@ -31,6 +32,53 @@ const clientDatalist = document.getElementById('client-list');
|
|||||||
|
|
||||||
// --- LÓGICA DE NEGOCIO ---
|
// --- LÓGICA DE NEGOCIO ---
|
||||||
|
|
||||||
|
async function loadDashboardData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/dashboard');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch dashboard data');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Update stat cards
|
||||||
|
document.getElementById('stat-total-income').textContent = `${Number(data.totalIncome || 0).toFixed(2)}`;
|
||||||
|
document.getElementById('stat-total-movements').textContent = data.totalMovements || 0;
|
||||||
|
|
||||||
|
// Update chart
|
||||||
|
const ctx = document.getElementById('incomeChart').getContext('2d');
|
||||||
|
const chartData = {
|
||||||
|
labels: data.incomeByService.map(item => item.tipo),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Ingresos por Servicio',
|
||||||
|
data: data.incomeByService.map(item => item.total),
|
||||||
|
backgroundColor: [
|
||||||
|
'#FF6384',
|
||||||
|
'#36A2EB',
|
||||||
|
'#FFCE56',
|
||||||
|
'#4BC0C0',
|
||||||
|
'#9966FF',
|
||||||
|
'#FF9F40'
|
||||||
|
],
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (incomeChart) {
|
||||||
|
incomeChart.data = chartData;
|
||||||
|
incomeChart.update();
|
||||||
|
} else {
|
||||||
|
incomeChart = new Chart(ctx, {
|
||||||
|
type: 'pie',
|
||||||
|
data: chartData,
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading dashboard:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function generateFolio() {
|
function generateFolio() {
|
||||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
let result = '';
|
let result = '';
|
||||||
@@ -302,6 +350,10 @@ function handleTabChange(e) {
|
|||||||
const tabId = tabButton.dataset.tab;
|
const tabId = tabButton.dataset.tab;
|
||||||
tabButton.classList.add('active');
|
tabButton.classList.add('active');
|
||||||
document.getElementById(tabId)?.classList.add('active');
|
document.getElementById(tabId)?.classList.add('active');
|
||||||
|
|
||||||
|
if (tabId === 'tab-dashboard') {
|
||||||
|
loadDashboardData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTestTicket() {
|
function handleTestTicket() {
|
||||||
@@ -322,8 +374,23 @@ function handleTestTicket() {
|
|||||||
|
|
||||||
// --- INICIALIZACIÓN ---
|
// --- INICIALIZACIÓN ---
|
||||||
|
|
||||||
function initializeApp() {
|
async function initializeApp() {
|
||||||
|
// Primero, verificar la autenticación
|
||||||
|
try {
|
||||||
|
const authResponse = await fetch('/api/check-auth');
|
||||||
|
const authData = await authResponse.json();
|
||||||
|
if (!authData.isAuthenticated) {
|
||||||
|
window.location.href = '/login.html';
|
||||||
|
return; // Detener la inicialización si no está autenticado
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Authentication check failed', error);
|
||||||
|
window.location.href = '/login.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tabs = document.querySelector('.tabs');
|
const tabs = document.querySelector('.tabs');
|
||||||
|
const btnLogout = document.getElementById('btnLogout');
|
||||||
|
|
||||||
formSettings?.addEventListener('submit', handleSaveSettings);
|
formSettings?.addEventListener('submit', handleSaveSettings);
|
||||||
formMove?.addEventListener('submit', handleNewMovement);
|
formMove?.addEventListener('submit', handleNewMovement);
|
||||||
@@ -333,6 +400,12 @@ function initializeApp() {
|
|||||||
btnTestTicket?.addEventListener('click', handleTestTicket);
|
btnTestTicket?.addEventListener('click', handleTestTicket);
|
||||||
formClient?.addEventListener('submit', handleClientForm);
|
formClient?.addEventListener('submit', handleClientForm);
|
||||||
tabs?.addEventListener('click', handleTabChange);
|
tabs?.addEventListener('click', handleTabChange);
|
||||||
|
|
||||||
|
btnLogout?.addEventListener('click', async () => {
|
||||||
|
await fetch('/api/logout', { method: 'POST' });
|
||||||
|
window.location.href = '/login.html';
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('btnCancelEditClient')?.addEventListener('click', () => {
|
document.getElementById('btnCancelEditClient')?.addEventListener('click', () => {
|
||||||
formClient.reset();
|
formClient.reset();
|
||||||
document.getElementById('c-id').value = '';
|
document.getElementById('c-id').value = '';
|
||||||
@@ -348,10 +421,13 @@ function initializeApp() {
|
|||||||
renderTable();
|
renderTable();
|
||||||
renderClientsTable();
|
renderClientsTable();
|
||||||
updateClientDatalist();
|
updateClientDatalist();
|
||||||
|
// Cargar datos del dashboard al inicio
|
||||||
|
loadDashboardData();
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('CRITICAL: Failed to load initial data. The app may not function correctly.', error);
|
console.error('CRITICAL: Failed to load initial data. The app may not function correctly.', error);
|
||||||
alert('Error Crítico: No se pudieron cargar los datos del servidor. Asegúrate de que el servidor (npm start) esté corriendo y que no haya errores en la terminal del servidor.');
|
alert('Error Crítico: No se pudieron cargar los datos del servidor. Asegúrate de que el servidor (npm start) esté corriendo y que no haya errores en la terminal del servidor.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initializeApp);
|
document.addEventListener('DOMContentLoaded', initializeApp);
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>AP-POS — v0.2.1</title>
|
<title>AP-POS — v0.2.1</title>
|
||||||
<link rel="stylesheet" href="styles.css?v=1.1" />
|
<link rel="stylesheet" href="styles.css?v=1.1" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="container">
|
<main class="container">
|
||||||
@@ -12,14 +13,37 @@
|
|||||||
<!-- Logo del negocio en lugar de texto -->
|
<!-- Logo del negocio en lugar de texto -->
|
||||||
<img src="src/logo.png" alt="Ale Ponce" class="header-logo">
|
<img src="src/logo.png" alt="Ale Ponce" class="header-logo">
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<button type="button" class="tab-link active" data-tab="tab-movements">Recibos</button>
|
<button type="button" class="tab-link active" data-tab="tab-dashboard">Dashboard</button>
|
||||||
|
<button type="button" class="tab-link" data-tab="tab-movements">Recibos</button>
|
||||||
<button type="button" class="tab-link" data-tab="tab-clients">Clientes</button>
|
<button type="button" class="tab-link" data-tab="tab-clients">Clientes</button>
|
||||||
<button type="button" class="tab-link" data-tab="tab-settings">Configuración</button>
|
<button type="button" class="tab-link" data-tab="tab-settings">Configuración</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
<button type="button" id="btnLogout" class="btn-danger">Cerrar Sesión</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Pestaña de Dashboard -->
|
||||||
|
<div id="tab-dashboard" class="tab-content active">
|
||||||
|
<div class="section">
|
||||||
|
<h2>Dashboard</h2>
|
||||||
|
<div class="dashboard-stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Ingresos Totales</h3>
|
||||||
|
<p id="stat-total-income">$0.00</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Servicios Realizados</h3>
|
||||||
|
<p id="stat-total-movements">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-chart">
|
||||||
|
<h3>Ingresos por Servicio</h3>
|
||||||
|
<canvas id="incomeChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Pestaña de Movimientos/Recibos -->
|
<!-- Pestaña de Movimientos/Recibos -->
|
||||||
<div id="tab-movements" class="tab-content active">
|
<div id="tab-movements" class="tab-content">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Nuevo Movimiento</h2>
|
<h2>Nuevo Movimiento</h2>
|
||||||
<form id="formMove">
|
<form id="formMove">
|
||||||
@@ -34,6 +58,7 @@
|
|||||||
<option value="Microblading">Microblading</option>
|
<option value="Microblading">Microblading</option>
|
||||||
<option value="Lashes">Lashes</option>
|
<option value="Lashes">Lashes</option>
|
||||||
<option value="Nail Art">Nail Art</option>
|
<option value="Nail Art">Nail Art</option>
|
||||||
|
<option value="Pago">Pago</option>
|
||||||
</select>
|
</select>
|
||||||
<label>Fecha de Cita:</label>
|
<label>Fecha de Cita:</label>
|
||||||
<input type="date" id="m-fecha-cita" />
|
<input type="date" id="m-fecha-cita" />
|
||||||
|
|||||||
59
ap-pos/login.html
Normal file
59
ap-pos/login.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>AP-POS — Iniciar Sesión</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f4f4f9;
|
||||||
|
}
|
||||||
|
.login-container {
|
||||||
|
padding: 2rem;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.login-container h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
#error-message {
|
||||||
|
color: var(--color-danger);
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="login-container">
|
||||||
|
<h1>Iniciar Sesión</h1>
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="form-grid">
|
||||||
|
<label for="username">Usuario:</label>
|
||||||
|
<input type="text" id="username" required autocomplete="username" />
|
||||||
|
<label for="password">Contraseña:</label>
|
||||||
|
<input type="password" id="password" required autocomplete="current-password" />
|
||||||
|
</div>
|
||||||
|
<div class="form-actions" style="margin-top: 1.5rem;">
|
||||||
|
<button type="submit">Entrar</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p id="error-message"></p>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="login.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
43
ap-pos/login.js
Normal file
43
ap-pos/login.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const loginForm = document.getElementById('loginForm');
|
||||||
|
const errorMessage = document.getElementById('error-message');
|
||||||
|
|
||||||
|
// Redirigir si ya está autenticado
|
||||||
|
fetch('/api/check-auth')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.isAuthenticated) {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loginForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
errorMessage.style.display = 'none';
|
||||||
|
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
window.location.href = '/'; // Redirigir a la página principal
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
errorMessage.textContent = errorData.error || 'Error al iniciar sesión.';
|
||||||
|
errorMessage.style.display = 'block';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.textContent = 'No se pudo conectar con el servidor.';
|
||||||
|
errorMessage.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
78
ap-pos/package-lock.json
generated
78
ap-pos/package-lock.json
generated
@@ -9,8 +9,10 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"express-session": "^1.18.0",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -176,6 +178,12 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||||
|
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/bindings": {
|
"node_modules/bindings": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
@@ -648,6 +656,46 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-session": {
|
||||||
|
"version": "1.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
|
||||||
|
"integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "0.7.2",
|
||||||
|
"cookie-signature": "1.0.7",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"on-headers": "~1.1.0",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"uid-safe": "~2.1.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express-session/node_modules/cookie-signature": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/express-session/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express-session/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-uri-to-path": {
|
"node_modules/file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
@@ -1441,6 +1489,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/on-headers": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
@@ -1579,6 +1636,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/random-bytes": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/range-parser": {
|
"node_modules/range-parser": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
@@ -2115,6 +2181,18 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uid-safe": {
|
||||||
|
"version": "2.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||||
|
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"random-bytes": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unique-filename": {
|
"node_modules/unique-filename": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"express-session": "^1.18.0",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
205
ap-pos/server.js
205
ap-pos/server.js
@@ -1,24 +1,28 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const session = require('express-session');
|
||||||
const sqlite3 = require('sqlite3').verbose();
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3000;
|
const port = 3000;
|
||||||
|
|
||||||
|
// --- MIDDLEWARE ---
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(express.static(__dirname)); // Servir archivos estáticos como CSS, JS, etc.
|
||||||
|
|
||||||
// Servir archivos estáticos (CSS, JS, imágenes)
|
// Session Middleware
|
||||||
app.use(express.static(__dirname));
|
app.use(session({
|
||||||
|
secret: 'your-very-secret-key-change-it', // Cambia esto por una clave secreta real
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
cookie: { secure: false, httpOnly: true, maxAge: 24 * 60 * 60 * 1000 } // `secure: true` en producción con HTTPS
|
||||||
|
}));
|
||||||
|
|
||||||
// Ruta principal para servir el index.html
|
// --- DATABASE INITIALIZATION ---
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.sendFile(path.join(__dirname, 'index.html'));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize SQLite database
|
|
||||||
const db = new sqlite3.Database('./ap-pos.db', (err) => {
|
const db = new sqlite3.Database('./ap-pos.db', (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err.message);
|
console.error(err.message);
|
||||||
@@ -26,42 +30,113 @@ const db = new sqlite3.Database('./ap-pos.db', (err) => {
|
|||||||
console.log('Connected to the ap-pos.db database.');
|
console.log('Connected to the ap-pos.db database.');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create tables if they don't exist
|
// --- AUTHENTICATION LOGIC ---
|
||||||
|
const SALT_ROUNDS = 10;
|
||||||
|
|
||||||
|
// Crear tabla de usuarios y usuario admin por defecto si no existen
|
||||||
db.serialize(() => {
|
db.serialize(() => {
|
||||||
db.run(`CREATE TABLE IF NOT EXISTS settings (
|
db.run(`CREATE TABLE IF NOT EXISTS users (
|
||||||
key TEXT PRIMARY KEY,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
value TEXT
|
username TEXT UNIQUE,
|
||||||
)`);
|
password TEXT
|
||||||
|
)`, (err) => {
|
||||||
|
if (err) return;
|
||||||
|
// Solo intentar insertar si la tabla fue creada o ya existía
|
||||||
|
const adminUsername = 'admin';
|
||||||
|
const defaultPassword = 'password';
|
||||||
|
|
||||||
db.run(`CREATE TABLE IF NOT EXISTS clients (
|
db.get('SELECT * FROM users WHERE username = ?', [adminUsername], (err, row) => {
|
||||||
id TEXT PRIMARY KEY,
|
if (err) return;
|
||||||
nombre TEXT,
|
if (!row) {
|
||||||
telefono TEXT,
|
bcrypt.hash(defaultPassword, SALT_ROUNDS, (err, hash) => {
|
||||||
cumpleaños TEXT,
|
if (err) return;
|
||||||
consentimiento INTEGER
|
db.run('INSERT INTO users (username, password) VALUES (?, ?)', [adminUsername, hash], (err) => {
|
||||||
)`);
|
if (!err) {
|
||||||
|
console.log(`Default user '${adminUsername}' created with password '${defaultPassword}'. Please change it.`);
|
||||||
db.run(`CREATE TABLE IF NOT EXISTS movements (
|
}
|
||||||
id TEXT PRIMARY KEY,
|
});
|
||||||
folio TEXT,
|
});
|
||||||
fechaISO TEXT,
|
}
|
||||||
clienteId TEXT,
|
});
|
||||||
tipo TEXT,
|
|
||||||
monto REAL,
|
|
||||||
metodo TEXT,
|
|
||||||
concepto TEXT,
|
|
||||||
staff TEXT,
|
|
||||||
notas TEXT,
|
|
||||||
fechaCita TEXT,
|
|
||||||
horaCita TEXT,
|
|
||||||
FOREIGN KEY (clienteId) REFERENCES clients (id)
|
|
||||||
)`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// API routes will go here
|
// Tablas existentes
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)`);
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS clients (id TEXT PRIMARY KEY, nombre TEXT, telefono TEXT, cumpleaños TEXT, consentimiento INTEGER)`);
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS movements (id TEXT PRIMARY KEY, folio TEXT, fechaISO TEXT, clienteId TEXT, tipo TEXT, monto REAL, metodo TEXT, concepto TEXT, staff TEXT, notas TEXT, fechaCita TEXT, horaCita TEXT, FOREIGN KEY (clienteId) REFERENCES clients (id))`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Middleware para verificar si el usuario está autenticado
|
||||||
|
const isAuthenticated = (req, res, next) => {
|
||||||
|
if (req.session.userId) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
// Para peticiones de API, devolver un error 401
|
||||||
|
if (req.path.startsWith('/api/')) {
|
||||||
|
return res.status(401).json({ error: 'Not authenticated' });
|
||||||
|
}
|
||||||
|
// Para otras peticiones, redirigir al login
|
||||||
|
res.redirect('/login.html');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- AUTH API ROUTES ---
|
||||||
|
|
||||||
|
app.post('/api/login', (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({ error: 'Username and password are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => {
|
||||||
|
if (err || !user) {
|
||||||
|
return res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
bcrypt.compare(password, user.password, (err, isMatch) => {
|
||||||
|
if (err || !isMatch) {
|
||||||
|
return res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
req.session.userId = user.id;
|
||||||
|
res.json({ message: 'Login successful' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/logout', (req, res) => {
|
||||||
|
req.session.destroy(err => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({ error: 'Could not log out' });
|
||||||
|
}
|
||||||
|
res.clearCookie('connect.sid'); // Limpiar la cookie de sesión
|
||||||
|
res.json({ message: 'Logout successful' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Endpoint para verificar el estado de la autenticación en el frontend
|
||||||
|
app.get('/api/check-auth', (req, res) => {
|
||||||
|
if (req.session.userId) {
|
||||||
|
res.json({ isAuthenticated: true });
|
||||||
|
} else {
|
||||||
|
res.json({ isAuthenticated: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- PROTECTED APPLICATION ROUTES ---
|
||||||
|
|
||||||
|
// La ruta principal ahora está protegida
|
||||||
|
app.get('/', isAuthenticated, (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Proteger todas las rutas de la API
|
||||||
|
const apiRouter = express.Router();
|
||||||
|
apiRouter.use(isAuthenticated);
|
||||||
|
|
||||||
|
|
||||||
// --- Settings ---
|
// --- Settings ---
|
||||||
app.get('/api/settings', (req, res) => {
|
apiRouter.get('/settings', (req, res) => {
|
||||||
db.get("SELECT value FROM settings WHERE key = 'settings'", (err, row) => {
|
db.get("SELECT value FROM settings WHERE key = 'settings'", (err, row) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
@@ -71,7 +146,7 @@ app.get('/api/settings', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/settings', (req, res) => {
|
apiRouter.post('/settings', (req, res) => {
|
||||||
const { settings } = req.body;
|
const { settings } = req.body;
|
||||||
const value = JSON.stringify(settings);
|
const value = JSON.stringify(settings);
|
||||||
db.run(`INSERT OR REPLACE INTO settings (key, value) VALUES ('settings', ?)`, [value], function(err) {
|
db.run(`INSERT OR REPLACE INTO settings (key, value) VALUES ('settings', ?)`, [value], function(err) {
|
||||||
@@ -84,7 +159,7 @@ app.post('/api/settings', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Clients ---
|
// --- Clients ---
|
||||||
app.get('/api/clients', (req, res) => {
|
apiRouter.get('/clients', (req, res) => {
|
||||||
db.all("SELECT * FROM clients", [], (err, rows) => {
|
db.all("SELECT * FROM clients", [], (err, rows) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
@@ -94,7 +169,7 @@ app.get('/api/clients', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/clients', (req, res) => {
|
apiRouter.post('/clients', (req, res) => {
|
||||||
const { client } = req.body;
|
const { client } = req.body;
|
||||||
const { id, nombre, telefono, cumpleaños, consentimiento } = client;
|
const { id, nombre, telefono, cumpleaños, consentimiento } = client;
|
||||||
db.run(`INSERT OR REPLACE INTO clients (id, nombre, telefono, cumpleaños, consentimiento) VALUES (?, ?, ?, ?, ?)`,
|
db.run(`INSERT OR REPLACE INTO clients (id, nombre, telefono, cumpleaños, consentimiento) VALUES (?, ?, ?, ?, ?)`,
|
||||||
@@ -107,7 +182,7 @@ app.post('/api/clients', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/api/clients/:id', (req, res) => {
|
apiRouter.delete('/clients/:id', (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
db.run(`DELETE FROM clients WHERE id = ?`, id, function(err) {
|
db.run(`DELETE FROM clients WHERE id = ?`, id, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -120,7 +195,7 @@ app.delete('/api/clients/:id', (req, res) => {
|
|||||||
|
|
||||||
|
|
||||||
// --- Movements ---
|
// --- Movements ---
|
||||||
app.get('/api/movements', (req, res) => {
|
apiRouter.get('/movements', (req, res) => {
|
||||||
db.all("SELECT * FROM movements ORDER BY fechaISO DESC", [], (err, rows) => {
|
db.all("SELECT * FROM movements ORDER BY fechaISO DESC", [], (err, rows) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
@@ -130,7 +205,7 @@ app.get('/api/movements', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/movements', (req, res) => {
|
apiRouter.post('/movements', (req, res) => {
|
||||||
const { movement } = req.body;
|
const { movement } = req.body;
|
||||||
const { id, folio, fechaISO, clienteId, tipo, monto, metodo, concepto, staff, notas, fechaCita, horaCita } = movement;
|
const { id, folio, fechaISO, clienteId, tipo, monto, metodo, concepto, staff, notas, fechaCita, horaCita } = movement;
|
||||||
db.run(`INSERT INTO movements (id, folio, fechaISO, clienteId, tipo, monto, metodo, concepto, staff, notas, fechaCita, horaCita)
|
db.run(`INSERT INTO movements (id, folio, fechaISO, clienteId, tipo, monto, metodo, concepto, staff, notas, fechaCita, horaCita)
|
||||||
@@ -144,7 +219,7 @@ app.post('/api/movements', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/api/movements/:id', (req, res) => {
|
apiRouter.delete('/movements/:id', (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
db.run(`DELETE FROM movements WHERE id = ?`, id, function(err) {
|
db.run(`DELETE FROM movements WHERE id = ?`, id, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -155,6 +230,46 @@ app.delete('/api/movements/:id', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Registrar el router de la API protegida
|
||||||
|
app.use('/api', apiRouter);
|
||||||
|
|
||||||
|
// --- Dashboard Route ---
|
||||||
|
apiRouter.get('/dashboard', (req, res) => {
|
||||||
|
const queries = {
|
||||||
|
totalIncome: "SELECT SUM(monto) as total FROM movements WHERE tipo = 'Pago'",
|
||||||
|
totalMovements: "SELECT COUNT(*) as total FROM movements",
|
||||||
|
incomeByService: "SELECT tipo, SUM(monto) as total FROM movements WHERE tipo = 'Pago' GROUP BY tipo"
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = {};
|
||||||
|
const promises = Object.keys(queries).map(key => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.all(queries[key], [], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
if (key === 'totalIncome' || key === 'totalMovements') {
|
||||||
|
resolve({ key, value: rows[0] ? rows[0].total : 0 });
|
||||||
|
} else {
|
||||||
|
resolve({ key, value: rows });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(promises)
|
||||||
|
.then(allResults => {
|
||||||
|
allResults.forEach(result => {
|
||||||
|
results[result.key] = result.value;
|
||||||
|
});
|
||||||
|
res.json(results);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Server running at http://localhost:${port}`);
|
console.log(`Server running at http://localhost:${port}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -248,10 +248,57 @@ button.action-btn {
|
|||||||
|
|
||||||
/* Logo en header */
|
/* Logo en header */
|
||||||
.main-header .header-logo {
|
.main-header .header-logo {
|
||||||
max-height: 50px;
|
height: 50px;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
margin-right: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Styles */
|
||||||
|
.dashboard-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h3 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-chart {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user