diff --git a/ap-pos/Dockerfile b/ap-pos/Dockerfile
index f5f806b..b1bead0 100644
--- a/ap-pos/Dockerfile
+++ b/ap-pos/Dockerfile
@@ -1,20 +1,27 @@
-# Usar una imagen base de Node.js ligera
+# Usar una imagen base de Node.js
FROM node:18-alpine
-# Establecer el directorio de trabajo dentro del contenedor
+# Establecer el directorio de trabajo en el contenedor
WORKDIR /usr/src/app
-# Copiar los archivos de definición de paquetes y dependencias
+# Copiar package.json y package-lock.json
COPY package*.json ./
-# Instalar las dependencias de producción
-RUN npm install --production
+# Instalar las dependencias de la aplicación
+RUN npm install
# Copiar el resto de los archivos de la aplicación
COPY . .
+# Crear un directorio para la base de datos persistente y definirlo como volumen
+RUN mkdir -p /usr/src/app/data
+VOLUME /usr/src/app/data
+
+# Establecer la ruta de la base de datos a través de una variable de entorno
+ENV DB_PATH /usr/src/app/data/ap-pos.db
+
# Exponer el puerto en el que corre la aplicación
EXPOSE 3000
-# Definir el comando para iniciar la aplicación
+# Comando para iniciar la aplicación
CMD [ "node", "server.js" ]
diff --git a/ap-pos/README.md b/ap-pos/README.md
index e51f40b..09582ec 100644
--- a/ap-pos/README.md
+++ b/ap-pos/README.md
@@ -60,11 +60,11 @@ Este es un sistema de punto de venta (POS) simple y moderno basado en la web, di
```
2. **Ejecutar el Contenedor:**
- Para ejecutar la aplicación en un contenedor, usa el siguiente comando. Esto mapeará el puerto 3000 y montará un volumen para que la base de datos persista fuera del contenedor.
+ Para ejecutar la aplicación en un contenedor, usa el siguiente comando. Esto mapeará el puerto 3000 y montará un volumen para que la base de datos persista fuera del contenedor, en una nueva carpeta `data` que se creará en tu directorio actual.
```bash
- docker run -p 3000:3000 -v $(pwd)/data:/usr/src/app ap-pos-app
+ docker run -p 3000:3000 -v $(pwd)/data:/usr/src/app/data ap-pos-app
```
- *Nota: El comando anterior crea un directorio `data` en tu carpeta actual para almacenar `ap-pos.db`.*
+ *Nota: La primera vez que ejecutes esto, se creará un directorio `data` en tu carpeta actual para almacenar `ap-pos.db`.*
## Autores
- **Gemini**
diff --git a/ap-pos/app.js b/ap-pos/app.js
index 66af944..d495c5e 100644
--- a/ap-pos/app.js
+++ b/ap-pos/app.js
@@ -20,6 +20,7 @@ let movements = [];
let clients = [];
let users = [];
let incomeChart = null;
+let paymentMethodChart = null;
let currentUser = {};
// --- DOM ELEMENTS ---
@@ -34,6 +35,7 @@ const clientDatalist = document.getElementById('client-list');
const formCredentials = document.getElementById('formCredentials');
const formAddUser = document.getElementById('formAddUser');
const tblUsersBody = document.getElementById('tblUsers')?.querySelector('tbody');
+const appointmentsList = document.getElementById('upcoming-appointments-list');
let isDashboardLoading = false;
@@ -41,7 +43,7 @@ let isDashboardLoading = false;
async function loadDashboardData() {
// Guardia para prevenir ejecuciones múltiples y re-entradas.
- if (currentUser.role !== 'admin' || !incomeChart || isDashboardLoading) {
+ if (currentUser.role !== 'admin' || isDashboardLoading) {
return;
}
isDashboardLoading = true;
@@ -68,12 +70,38 @@ async function loadDashboardData() {
document.getElementById('stat-total-income').textContent = `${Number(data.totalIncome || 0).toFixed(2)}`;
document.getElementById('stat-total-movements').textContent = data.totalMovements || 0;
- // Actualizar datos del gráfico
- incomeChart.data.labels = data.incomeByService.map(item => item.tipo);
- incomeChart.data.datasets[0].data = data.incomeByService.map(item => item.total);
+ // Actualizar datos del gráfico de ingresos
+ if (incomeChart) {
+ incomeChart.data.labels = data.incomeByService.map(item => item.tipo);
+ incomeChart.data.datasets[0].data = data.incomeByService.map(item => item.total);
+ incomeChart.update('none');
+ }
+
+ // Actualizar datos del gráfico de método de pago
+ if (paymentMethodChart) {
+ paymentMethodChart.data.labels = data.incomeByPaymentMethod.map(item => item.metodo);
+ paymentMethodChart.data.datasets[0].data = data.incomeByPaymentMethod.map(item => item.total);
+ paymentMethodChart.update('none');
+ }
- // Usar 'none' para el modo de actualización previene bucles de renderizado por animación/responsividad.
- incomeChart.update('none');
+ // Renderizar próximas citas
+ if (appointmentsList) {
+ appointmentsList.innerHTML = '';
+ if (data.upcomingAppointments.length > 0) {
+ data.upcomingAppointments.forEach(appt => {
+ const item = document.createElement('div');
+ item.className = 'appointment-item';
+ const fechaCita = new Date(appt.fechaCita + 'T00:00:00').toLocaleDateString('es-MX', { day: '2-digit', month: 'long' });
+ item.innerHTML = `
+ ${appt.clienteNombre}
+ ${fechaCita} - ${appt.horaCita}
+ `;
+ appointmentsList.appendChild(item);
+ });
+ } else {
+ appointmentsList.innerHTML = '
No hay citas próximas.
';
+ }
+ }
} catch (error) {
console.error('Error al cargar el dashboard:', error);
@@ -218,16 +246,17 @@ function renderTable() {
});
}
-function renderClientsTable() {
+function renderClientsTable(clientList = clients) {
if (!tblClientsBody) return;
tblClientsBody.innerHTML = '';
- clients.forEach(c => {
+ clientList.forEach(c => {
const tr = document.createElement('tr');
+ tr.dataset.id = c.id; // Importante para la función de expandir
+ tr.style.cursor = 'pointer'; // Indicar que la fila es clickeable
tr.innerHTML = `
${c.nombre}
${c.telefono || ''}
-
${c.cumpleaños ? new Date(c.cumpleaños).toLocaleDateString('es-MX') : ''}