feat: Estandarizar fechas y mejorar UI

Se realizan los siguientes cambios:

- Se estandariza el formato de fecha a dd/mm/aaaa en toda la aplicación (tablas, reportes, tickets) para consistencia.
- Se ajusta la versión de la aplicación a 1.3.0.
- Se reduce el tamaño del logo principal para mejorar la proporción visual.
- Se corrige el comportamiento de las pestañas de navegación para que en estado inactivo solo muestren el ícono.
This commit is contained in:
Marco Gallegos
2025-08-25 08:16:17 -06:00
parent 4d027bc393
commit e549a2db9e
5 changed files with 97 additions and 80 deletions

69
app.js
View File

@@ -14,7 +14,18 @@ function escapeHTML(str) {
.replace(/'/g, '''); .replace(/'/g, ''');
} }
const APP_VERSION = '1.0.0'; function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const userTimezoneOffset = date.getTimezoneOffset() * 60000;
const adjustedDate = new Date(date.getTime() + userTimezoneOffset);
const day = String(adjustedDate.getDate()).padStart(2, '0');
const month = String(adjustedDate.getMonth() + 1).padStart(2, '0');
const year = adjustedDate.getFullYear();
return `${day}/${month}/${year}`;
}
const APP_VERSION = '1.3.0';
// --- ESTADO Y DATOS --- // --- ESTADO Y DATOS ---
const DEFAULT_SETTINGS = { const DEFAULT_SETTINGS = {
@@ -62,7 +73,6 @@ let isDashboardLoading = false;
// --- LÓGICA DE NEGOCIO --- // --- LÓGICA DE NEGOCIO ---
async function loadDashboardData() { async function loadDashboardData() {
// Guardia para prevenir ejecuciones múltiples y re-entradas.
if (currentUser.role !== 'admin' || isDashboardLoading) { if (currentUser.role !== 'admin' || isDashboardLoading) {
return; return;
} }
@@ -76,45 +86,39 @@ async function loadDashboardData() {
} else { } else {
throw new Error('Falló la carga de datos del dashboard'); throw new Error('Falló la carga de datos del dashboard');
} }
return; // Salir aquí después de manejar el error return;
} }
const data = await response.json(); const data = await response.json();
// Antes de actualizar, verificar que el dashboard sigue activo.
const dashboardTab = document.getElementById('tab-dashboard'); const dashboardTab = document.getElementById('tab-dashboard');
if (!dashboardTab.classList.contains('active')) { if (!dashboardTab.classList.contains('active')) {
return; return;
} }
// Actualizar tarjetas de estadísticas
document.getElementById('stat-total-income').textContent = `${Number(data.totalIncome || 0).toFixed(2)}`; document.getElementById('stat-total-income').textContent = `${Number(data.totalIncome || 0).toFixed(2)}`;
document.getElementById('stat-total-movements').textContent = data.totalMovements || 0; document.getElementById('stat-total-movements').textContent = data.totalMovements || 0;
// Actualizar datos del gráfico de ingresos
if (incomeChart) { if (incomeChart) {
incomeChart.data.labels = data.incomeByService.map(item => item.tipo); incomeChart.data.labels = data.incomeByService.map(item => item.tipo);
incomeChart.data.datasets[0].data = data.incomeByService.map(item => item.total); incomeChart.data.datasets[0].data = data.incomeByService.map(item => item.total);
incomeChart.update('none'); incomeChart.update('none');
} }
// Actualizar datos del gráfico de método de pago
if (paymentMethodChart) { if (paymentMethodChart) {
paymentMethodChart.data.labels = data.incomeByPaymentMethod.map(item => item.metodo); paymentMethodChart.data.labels = data.incomeByPaymentMethod.map(item => item.metodo);
paymentMethodChart.data.datasets[0].data = data.incomeByPaymentMethod.map(item => item.total); paymentMethodChart.data.datasets[0].data = data.incomeByPaymentMethod.map(item => item.total);
paymentMethodChart.update('none'); paymentMethodChart.update('none');
} }
// Renderizar próximas citas
if (appointmentsList) { if (appointmentsList) {
appointmentsList.innerHTML = ''; appointmentsList.innerHTML = '';
if (data.upcomingAppointments.length > 0) { if (data.upcomingAppointments.length > 0) {
data.upcomingAppointments.forEach(appt => { data.upcomingAppointments.forEach(appt => {
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'appointment-item'; item.className = 'appointment-item';
const fechaCita = new Date(appt.fechaCita + 'T00:00:00').toLocaleDateString('es-MX', { day: '2-digit', month: 'long' });
item.innerHTML = ` item.innerHTML = `
<a href="#" data-id="${appt.id}" data-action="reprint">${appt.clienteNombre}</a> <a href="#" data-id="${appt.id}" data-action="reprint">${appt.clienteNombre}</a>
<span class="date">${fechaCita} - ${appt.horaCita}</span> <span class="date">${formatDate(appt.fechaCita)} - ${appt.horaCita}</span>
`; `;
appointmentsList.appendChild(item); appointmentsList.appendChild(item);
}); });
@@ -126,7 +130,6 @@ async function loadDashboardData() {
} catch (error) { } catch (error) {
console.error('Error al cargar el dashboard:', error); console.error('Error al cargar el dashboard:', error);
} finally { } finally {
// Asegurar que el bloqueo se libere sin importar el resultado.
isDashboardLoading = false; isDashboardLoading = false;
} }
} }
@@ -182,12 +185,11 @@ async function saveClient(clientData) {
await save('clients', { client: clientToSave }); await save('clients', { client: clientToSave });
// Optimización: en lugar de recargar, actualizamos el estado local.
if (isUpdate) { if (isUpdate) {
const index = clients.findIndex(c => c.id === clientToSave.id); const index = clients.findIndex(c => c.id === clientToSave.id);
if (index > -1) clients[index] = clientToSave; if (index > -1) clients[index] = clientToSave;
} else { } else {
clients.unshift(clientToSave); // Añadir al principio para que aparezca primero clients.unshift(clientToSave);
} }
renderClientsTable(); renderClientsTable();
@@ -251,7 +253,6 @@ function renderTable() {
const client = clients.find(c => c.id === mov.clienteId); const client = clients.find(c => c.id === mov.clienteId);
const tr = tblMovesBody.insertRow(); const tr = tblMovesBody.insertRow();
const fechaCita = mov.fechaCita ? new Date(mov.fechaCita + 'T00:00:00').toLocaleDateString('es-MX') : '';
const tipoServicio = mov.subtipo ? `${escapeHTML(mov.tipo)} (${escapeHTML(mov.subtipo)})` : escapeHTML(mov.tipo); const tipoServicio = mov.subtipo ? `${escapeHTML(mov.tipo)} (${escapeHTML(mov.subtipo)})` : escapeHTML(mov.tipo);
const folioCell = tr.insertCell(); const folioCell = tr.insertCell();
@@ -263,8 +264,8 @@ function renderTable() {
folioLink.textContent = mov.folio; folioLink.textContent = mov.folio;
folioCell.appendChild(folioLink); folioCell.appendChild(folioLink);
tr.insertCell().textContent = new Date(mov.fechaISO).toLocaleDateString('es-MX'); tr.insertCell().textContent = formatDate(mov.fechaISO);
tr.insertCell().textContent = `${fechaCita} ${mov.horaCita || ''}`; tr.insertCell().textContent = `${formatDate(mov.fechaCita)} ${mov.horaCita || ''}`.trim();
tr.insertCell().textContent = client ? escapeHTML(client.nombre) : 'Cliente Eliminado'; tr.insertCell().textContent = client ? escapeHTML(client.nombre) : 'Cliente Eliminado';
tr.insertCell().textContent = tipoServicio; tr.insertCell().textContent = tipoServicio;
tr.insertCell().textContent = Number(mov.monto).toFixed(2); tr.insertCell().textContent = Number(mov.monto).toFixed(2);
@@ -416,7 +417,7 @@ async function handleSaveCredentials(e) {
if (response.ok) { if (response.ok) {
alert('Credenciales actualizadas.'); alert('Credenciales actualizadas.');
currentUser.name = name; // Actualizar el nombre en el estado local currentUser.name = name;
currentUser.username = username; currentUser.username = username;
document.getElementById('s-password').value = ''; document.getElementById('s-password').value = '';
} else { } else {
@@ -680,7 +681,7 @@ async function handleNewMovement(e) {
monto: Number(monto.toFixed(2)), monto: Number(monto.toFixed(2)),
metodo: document.getElementById('m-metodo').value, metodo: document.getElementById('m-metodo').value,
concepto: document.getElementById('m-articulo').value, concepto: document.getElementById('m-articulo').value,
staff: currentUser.name, // Usar el nombre del usuario actual staff: currentUser.name,
notas: document.getElementById('m-notas').value, notas: document.getElementById('m-notas').value,
fechaCita: document.getElementById('m-fecha-cita').value, fechaCita: document.getElementById('m-fecha-cita').value,
horaCita: document.getElementById('m-hora-cita').value, horaCita: document.getElementById('m-hora-cita').value,
@@ -696,11 +697,10 @@ async function handleNewMovement(e) {
function exportClientHistoryCSV(client, history) { function exportClientHistoryCSV(client, history) {
const headers = 'Folio,Fecha,Servicio,Monto'; const headers = 'Folio,Fecha,Servicio,Monto';
const rows = history.map(mov => { const rows = history.map(mov => {
const fecha = new Date(mov.fechaISO).toLocaleDateString('es-MX');
const servicio = mov.subtipo ? `${mov.tipo} (${mov.subtipo})` : mov.tipo; const servicio = mov.subtipo ? `${mov.tipo} (${mov.subtipo})` : mov.tipo;
return [ return [
mov.folio, mov.folio,
fecha, formatDate(mov.fechaISO),
`"${servicio}"`, `"${servicio}"`,
Number(mov.monto).toFixed(2) Number(mov.monto).toFixed(2)
].join(','); ].join(',');
@@ -732,11 +732,10 @@ async function showClientRecord(clientId) {
const clientHistoryTableBody = document.getElementById('client-history-table').querySelector('tbody'); const clientHistoryTableBody = document.getElementById('client-history-table').querySelector('tbody');
const clientCoursesContainer = document.getElementById('client-courses-history-container'); const clientCoursesContainer = document.getElementById('client-courses-history-container');
// Sanitize client details before rendering
clientDetails.innerHTML = ` clientDetails.innerHTML = `
<p><strong>Nombre:</strong> ${escapeHTML(client.nombre)}</p> <p><strong>Nombre:</strong> ${escapeHTML(client.nombre)}</p>
<p><strong>Teléfono:</strong> ${escapeHTML(client.telefono || 'N/A')}</p> <p><strong>Teléfono:</strong> ${escapeHTML(client.telefono || 'N/A')}</p>
<p><strong>Cumpleaños:</strong> ${escapeHTML(client.cumpleaños ? new Date(client.cumpleaños + 'T00:00:00').toLocaleDateString('es-MX') : 'N/A')}</p> <p><strong>Cumpleaños:</strong> ${escapeHTML(formatDate(client.cumpleaños) || 'N/A')}</p>
<p><strong>Género:</strong> ${escapeHTML(client.genero || 'N/A')}</p> <p><strong>Género:</strong> ${escapeHTML(client.genero || 'N/A')}</p>
<p><strong>Oncológico:</strong> ${client.esOncologico ? 'Sí' : 'No'}</p> <p><strong>Oncológico:</strong> ${client.esOncologico ? 'Sí' : 'No'}</p>
`; `;
@@ -754,10 +753,9 @@ async function showClientRecord(clientId) {
if (history.length > 0) { if (history.length > 0) {
history.forEach(mov => { history.forEach(mov => {
const tr = clientHistoryTableBody.insertRow(); const tr = clientHistoryTableBody.insertRow();
const fecha = new Date(mov.fechaISO).toLocaleDateString('es-MX');
const servicio = mov.subtipo ? `${escapeHTML(mov.tipo)} (${escapeHTML(mov.subtipo)})` : escapeHTML(mov.tipo); const servicio = mov.subtipo ? `${escapeHTML(mov.tipo)} (${escapeHTML(mov.subtipo)})` : escapeHTML(mov.tipo);
tr.insertCell().textContent = mov.folio; tr.insertCell().textContent = mov.folio;
tr.insertCell().textContent = fecha; tr.insertCell().textContent = formatDate(mov.fechaISO);
tr.insertCell().textContent = servicio; tr.insertCell().textContent = servicio;
tr.insertCell().textContent = Number(mov.monto).toFixed(2); tr.insertCell().textContent = Number(mov.monto).toFixed(2);
}); });
@@ -784,7 +782,7 @@ async function showClientRecord(clientId) {
${courses.map(course => ` ${courses.map(course => `
<tr> <tr>
<td>${escapeHTML(course.course_name)}</td> <td>${escapeHTML(course.course_name)}</td>
<td>${escapeHTML(course.fecha_curso)}</td> <td>${escapeHTML(formatDate(course.fecha_curso))}</td>
<td>${escapeHTML(course.score_general)}</td> <td>${escapeHTML(course.score_general)}</td>
<td>${course.completo_presencial ? 'Sí' : 'No'}</td> <td>${course.completo_presencial ? 'Sí' : 'No'}</td>
<td>${course.completo_online ? 'Sí' : 'No'}</td> <td>${course.completo_online ? 'Sí' : 'No'}</td>
@@ -876,18 +874,15 @@ function handleTableClick(e) {
async function handleClientForm(e) { async function handleClientForm(e) {
e.preventDefault(); e.preventDefault();
await saveClient(); await saveClient();
// Después de guardar, cambiar a la pestaña de consulta
activateClientSubTab('sub-tab-consult'); activateClientSubTab('sub-tab-consult');
} }
function activateClientSubTab(subTabId) { function activateClientSubTab(subTabId) {
if (!subTabId) return; if (!subTabId) return;
// Desactivar todas las sub-pestañas y contenidos de clientes
document.querySelectorAll('#tab-clients .sub-tab-link').forEach(tab => tab.classList.remove('active')); document.querySelectorAll('#tab-clients .sub-tab-link').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('#tab-clients .sub-tab-content').forEach(content => content.classList.remove('active')); document.querySelectorAll('#tab-clients .sub-tab-content').forEach(content => content.classList.remove('active'));
// Activar la sub-pestaña y el contenido correctos
const tabButton = document.querySelector(`[data-subtab="${subTabId}"]`); const tabButton = document.querySelector(`[data-subtab="${subTabId}"]`);
const tabContent = document.getElementById(subTabId); const tabContent = document.getElementById(subTabId);
@@ -910,11 +905,9 @@ function handleClientTabChange(e) {
function activateTab(tabId) { function activateTab(tabId) {
if (!tabId) return; if (!tabId) return;
// Desactivar todas las pestañas y contenidos
document.querySelectorAll('.tab-link').forEach(tab => tab.classList.remove('active')); document.querySelectorAll('.tab-link').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// Activar la pestaña y el contenido correctos
const tabButton = document.querySelector(`[data-tab="${tabId}"]`); const tabButton = document.querySelector(`[data-tab="${tabId}"]`);
const tabContent = document.getElementById(tabId); const tabContent = document.getElementById(tabId);
@@ -925,9 +918,7 @@ function activateTab(tabId) {
tabContent.classList.add('active'); tabContent.classList.add('active');
} }
// Cargar datos dinámicos si es la pestaña del dashboard
if (tabId === 'tab-dashboard' && currentUser.role === 'admin') { if (tabId === 'tab-dashboard' && currentUser.role === 'admin') {
// Si es la primera vez que se visita la pestaña, inicializar el gráfico
if (!incomeChart) { if (!incomeChart) {
const ctx = document.getElementById('incomeChart').getContext('2d'); const ctx = document.getElementById('incomeChart').getContext('2d');
incomeChart = new Chart(ctx, { incomeChart = new Chart(ctx, {
@@ -966,7 +957,6 @@ function activateTab(tabId) {
} }
}); });
} }
// Cargar (o recargar) los datos del dashboard
loadDashboardData(); loadDashboardData();
} }
} }
@@ -975,13 +965,11 @@ function handleTabChange(e) {
const tabButton = e.target.closest('.tab-link'); const tabButton = e.target.closest('.tab-link');
if (!tabButton) return; if (!tabButton) return;
// Solo prevenir el comportamiento por defecto si es un botón para cambiar de pestaña
if (tabButton.dataset.tab) { if (tabButton.dataset.tab) {
e.preventDefault(); e.preventDefault();
const tabId = tabButton.dataset.tab; const tabId = tabButton.dataset.tab;
activateTab(tabId); activateTab(tabId);
} }
// Si no tiene data-tab (es un enlace normal como el de Clientes), no hacer nada y permitir la navegación.
} }
function handleTestTicket() { function handleTestTicket() {
@@ -1043,7 +1031,7 @@ function populateFooter() {
const versionElement = document.getElementById('footer-version'); const versionElement = document.getElementById('footer-version');
if (dateElement) { if (dateElement) {
dateElement.textContent = new Date().toLocaleDateString('es-MX', { year: 'numeric', month: 'long', day: 'numeric' }); dateElement.textContent = formatDate(new Date().toISOString());
} }
if (versionElement) { if (versionElement) {
versionElement.textContent = `Versión ${APP_VERSION}`; versionElement.textContent = `Versión ${APP_VERSION}`;
@@ -1054,17 +1042,14 @@ function populateFooter() {
// --- INICIALIZACIÓN --- // --- INICIALIZACIÓN ---
async function initializeApp() { async function initializeApp() {
// 1. Verificar autenticación y obtener datos del usuario.
let userResponse; let userResponse;
try { try {
userResponse = await fetch('/api/user'); userResponse = await fetch('/api/user');
if (!userResponse.ok) { if (!userResponse.ok) {
// Si la respuesta no es 2xx, el usuario no está autenticado o hay un error.
window.location.href = '/login.html'; window.location.href = '/login.html';
return; return;
} }
// Verificar que la respuesta sea JSON antes de procesarla.
const contentType = userResponse.headers.get('content-type'); const contentType = userResponse.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) { if (!contentType || !contentType.includes('application/json')) {
console.error('La respuesta del servidor no es JSON. Redirigiendo al login.'); console.error('La respuesta del servidor no es JSON. Redirigiendo al login.');
@@ -1072,17 +1057,14 @@ async function initializeApp() {
return; return;
} }
// 2. Procesar datos del usuario.
currentUser = await userResponse.json(); currentUser = await userResponse.json();
} catch (error) { } catch (error) {
// Si hay un error de red, es probable que el servidor no esté corriendo.
console.error('Error de conexión al verificar la autenticación. Redirigiendo al login.', error); console.error('Error de conexión al verificar la autenticación. Redirigiendo al login.', error);
window.location.href = '/login.html'; window.location.href = '/login.html';
return; return;
} }
// 3. Añadir manejadores de eventos.
const tabs = document.querySelector('.tabs'); const tabs = document.querySelector('.tabs');
const btnLogout = document.getElementById('btnLogout'); const btnLogout = document.getElementById('btnLogout');
const btnCancelEditUser = document.getElementById('btnCancelEditUser'); const btnCancelEditUser = document.getElementById('btnCancelEditUser');
@@ -1193,7 +1175,6 @@ async function initializeApp() {
showAddCourseModal(clientId); showAddCourseModal(clientId);
}); });
// 4. Cargar el resto de los datos de la aplicación.
Promise.all([ Promise.all([
load(KEY_SETTINGS, DEFAULT_SETTINGS), load(KEY_SETTINGS, DEFAULT_SETTINGS),
load(KEY_DATA, []), load(KEY_DATA, []),
@@ -1213,7 +1194,6 @@ async function initializeApp() {
renderProductTables(); renderProductTables();
console.log('Updating client datalist...'); console.log('Updating client datalist...');
updateClientDatalist(); updateClientDatalist();
// Initial population of the articulo dropdown
populateArticuloDropdown(document.getElementById('m-categoria').value); populateArticuloDropdown(document.getElementById('m-categoria').value);
if (currentUser) { if (currentUser) {
@@ -1247,5 +1227,4 @@ async function initializeApp() {
}); });
} }
document.addEventListener('DOMContentLoaded', initializeApp); document.addEventListener('DOMContentLoaded', initializeApp);

View File

@@ -2,6 +2,18 @@ import { load, save, remove, KEY_CLIENTS } from './storage.js';
let clients = []; let clients = [];
// --- UTILITIES ---
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const userTimezoneOffset = date.getTimezoneOffset() * 60000;
const adjustedDate = new Date(date.getTime() + userTimezoneOffset);
const day = String(adjustedDate.getDate()).padStart(2, '0');
const month = String(adjustedDate.getMonth() + 1).padStart(2, '0');
const year = adjustedDate.getFullYear();
return `${day}/${month}/${year}`;
}
// --- DOM ELEMENTS --- // --- DOM ELEMENTS ---
const formClient = document.getElementById('formClient'); const formClient = document.getElementById('formClient');
const tblClientsBody = document.getElementById('tblClients')?.querySelector('tbody'); const tblClientsBody = document.getElementById('tblClients')?.querySelector('tbody');
@@ -64,11 +76,10 @@ async function deleteClient(id) {
function exportClientHistoryCSV(client, history) { function exportClientHistoryCSV(client, history) {
const headers = 'Folio,Fecha,Servicio,Monto'; const headers = 'Folio,Fecha,Servicio,Monto';
const rows = history.map(mov => { const rows = history.map(mov => {
const fecha = new Date(mov.fechaISO).toLocaleDateString('es-MX');
const servicio = mov.subtipo ? `${mov.tipo} (${mov.subtipo})` : mov.tipo; const servicio = mov.subtipo ? `${mov.tipo} (${mov.subtipo})` : mov.tipo;
return [ return [
mov.folio, mov.folio,
fecha, formatDate(mov.fechaISO),
`"${servicio}"`, // Corrected: escaped inner quotes for CSV compatibility `"${servicio}"`, // Corrected: escaped inner quotes for CSV compatibility
Number(mov.monto).toFixed(2) Number(mov.monto).toFixed(2)
].join(','); ].join(',');
@@ -101,19 +112,18 @@ async function toggleClientHistory(row, client) {
historyRow.id = historyRowId; historyRow.id = historyRowId;
historyRow.className = 'client-history-row'; historyRow.className = 'client-history-row';
let historyHtml = ' let historyHtml = `
<div class="client-history-header"> <div class="client-history-header">
<h4>Historial de Servicios</h4> <h4>Historial de Servicios</h4>
<button class="action-btn" id="btn-export-history-' + client˚.id + '">Exportar CSV</button> <button class="action-btn" id="btn-export-history-${client.id}">Exportar CSV</button>
</div> </div>
'; `;
if (history.length > 0) { if (history.length > 0) {
historyHtml += '<table><thead><tr><th>Folio</th><th>Fecha</th><th>Servicio</th><th>Monto</th></tr></thead><tbody>'; historyHtml += '<table><thead><tr><th>Folio</th><th>Fecha</th><th>Servicio</th><th>Monto</th></tr></thead><tbody>';
history.forEach(mov => { history.forEach(mov => {
const fecha = new Date(mov.fechaISO).toLocaleDateString('es-MX');
const servicio = mov.subtipo ? `${mov.tipo} (${mov.subtipo})` : mov.tipo; const servicio = mov.subtipo ? `${mov.tipo} (${mov.subtipo})` : mov.tipo;
historyHtml += `<tr><td>${mov.folio}</td><td>${fecha}</td><td>${servicio}</td><td>${Number(mov.monto).toFixed(2)}</td></tr>`; historyHtml += `<tr><td>${mov.folio}</td><td>${formatDate(mov.fechaISO)}</td><td>${servicio}</td><td>${Number(mov.monto).toFixed(2)}</td></tr>`;
}); });
historyHtml += '</tbody></table>'; historyHtml += '</tbody></table>';
} else { } else {
@@ -145,16 +155,16 @@ function renderClientsTable(clientList = clients) {
clientList.forEach(c => { clientList.forEach(c => {
const tr = document.createElement('tr'); const tr = document.createElement('tr');
tr.dataset.id = c.id; tr.dataset.id = c.id;
tr.innerHTML = ' tr.innerHTML = `
<td>' + c.nombre + '</td> <td>${c.nombre}</td>
<td>' + (c.telefono || '') + '</td> <td>${c.telefono || ''}</td>
<td>' + (c.esOncologico ? 'Sí' : 'No') + '</td> <td>${c.esOncologico ? 'Sí' : 'No'}</td>
<td> <td>
<button class="action-btn" data-id="' + c.id + '" data-action="view-history">Historial</button> <button class="action-btn" data-id="${c.id}" data-action="view-history">Historial</button>
<button class="action-btn" data-id="' + c.id + '" data-action="edit-client">Editar</button> <button class="action-btn" data-id="${c.id}" data-action="edit-client">Editar</button>
<button class="action-btn" data-id="' + c.id + '" data-action="delete-client">Eliminar</button> <button class="action-btn" data-id="${c.id}" data-action="delete-client">Eliminar</button>
</td> </td>
'; `;
tblClientsBody.appendChild(tr); tblClientsBody.appendChild(tr);
}); });
} }
@@ -209,7 +219,6 @@ async function handleClientForm(e) {
// --- INICIALIZACIÓN --- // --- INICIALIZACIÓN ---
async function initializeClientsPage() { async function initializeClientsPage() {
// 1. Verificar autenticación
try { try {
const response = await fetch('/api/check-auth'); const response = await fetch('/api/check-auth');
const auth = await response.json(); const auth = await response.json();
@@ -223,11 +232,9 @@ async function initializeClientsPage() {
return; return;
} }
// 2. Cargar clientes
clients = await load(KEY_CLIENTS, []); clients = await load(KEY_CLIENTS, []);
renderClientsTable(); renderClientsTable();
// 3. Añadir manejadores de eventos
formClient?.addEventListener('submit', handleClientForm); formClient?.addEventListener('submit', handleClientForm);
tblClientsBody?.addEventListener('click', handleTableClick); tblClientsBody?.addEventListener('click', handleTableClick);

View File

@@ -20,7 +20,7 @@
<header class="main-header"> <header class="main-header">
<!-- 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" data-tab="tab-dashboard"> <button type="button" class="tab-link" data-tab="tab-dashboard">
<span class="material-icons-outlined">dashboard</span><span>Dashboard</span> <span class="material-icons-outlined">dashboard</span><span>Dashboard</span>
</button> </button>

View File

@@ -4,15 +4,28 @@
* @returns {string} El string escapado. * @returns {string} El string escapado.
*/ */
function esc(str) { function esc(str) {
return String(str || '').replace(/[&<>"']/g, c => ({ return String(str || '').replace(/[&<>"'/]/g, c => ({
"&": "&amp;", "&": "&amp;",
"<": "&lt;", "<": "&lt;",
">": "&gt;", ">": "&gt;",
"\"": "&quot;", '"': '&quot;',
"'": "&#39;" "'": "&#39;"
}[c])); }[c]));
} }
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const userTimezoneOffset = date.getTimezoneOffset() * 60000;
const adjustedDate = new Date(date.getTime() + userTimezoneOffset);
const day = String(adjustedDate.getDate()).padStart(2, '0');
const month = String(adjustedDate.getMonth() + 1).padStart(2, '0');
const year = adjustedDate.getFullYear();
const hours = String(adjustedDate.getHours()).padStart(2, '0');
const minutes = String(adjustedDate.getMinutes()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`;
}
/** /**
* Genera el HTML para un ticket de movimiento. * Genera el HTML para un ticket de movimiento.
* @param {object} mov El objeto del movimiento. * @param {object} mov El objeto del movimiento.
@@ -20,8 +33,7 @@ function esc(str) {
* @returns {string} El HTML del ticket. * @returns {string} El HTML del ticket.
*/ */
function templateTicket(mov, settings) { function templateTicket(mov, settings) {
const dt = new Date(mov.fechaISO || Date.now()); const fechaLocal = formatDate(mov.fechaISO || Date.now());
const fechaLocal = dt.toLocaleString('es-MX', { dateStyle: 'medium', timeStyle: 'short' });
const montoFormateado = Number(mov.monto).toFixed(2); const montoFormateado = Number(mov.monto).toFixed(2);
const tipoServicio = mov.subtipo === 'Retoque' ? `Retoque de ${mov.tipo}` : mov.tipo; const tipoServicio = mov.subtipo === 'Retoque' ? `Retoque de ${mov.tipo}` : mov.tipo;
@@ -50,7 +62,6 @@ function templateTicket(mov, settings) {
lines.push('<div class="t-divider"></div>'); lines.push('<div class="t-divider"></div>');
lines.push(`<div class="t-row t-bold"><span>Total</span><span>${montoFormateado}</span></div>`); lines.push(`<div class="t-row t-bold"><span>Total</span><span>${montoFormateado}</span></div>`);
// Sección de consentimientos
if (mov.client && (mov.client.esOncologico || mov.client.consentimiento)) { if (mov.client && (mov.client.esOncologico || mov.client.consentimiento)) {
lines.push('<div class="t-divider"></div>'); lines.push('<div class="t-divider"></div>');
if (mov.client.esOncologico) { if (mov.client.esOncologico) {
@@ -66,7 +77,6 @@ function templateTicket(mov, settings) {
if (settings.leyenda) lines.push(`<div class="t-footer t-center t-small">${esc(settings.leyenda)}</div>`); if (settings.leyenda) lines.push(`<div class="t-footer t-center t-small">${esc(settings.leyenda)}</div>`);
// Sección de Encuesta con QR
lines.push('<div class="t-qr-section">'); lines.push('<div class="t-qr-section">');
lines.push('<div class="t-small t-bold">¡Tu opinión es muy importante!</div>'); lines.push('<div class="t-small t-bold">¡Tu opinión es muy importante!</div>');
lines.push('<div class="t-small">Escanea el código QR para darnos tu feedback.</div>'); lines.push('<div class="t-small">Escanea el código QR para darnos tu feedback.</div>');
@@ -91,24 +101,18 @@ export async function renderTicketAndPrint(mov, settings) {
} }
try { try {
// 1. Renderizar la estructura HTML del ticket
printArea.innerHTML = templateTicket(mov, settings); printArea.innerHTML = templateTicket(mov, settings);
// 2. Encontrar el elemento canvas para el QR
const canvas = document.getElementById('qr-canvas'); const canvas = document.getElementById('qr-canvas');
if (!canvas) { if (!canvas) {
console.error("El canvas del QR #qr-canvas no se encontró. Se imprimirá sin QR."); console.error("El canvas del QR #qr-canvas no se encontró. Se imprimirá sin QR.");
window.print(); // Imprimir sin QR de inmediato window.print();
return; return;
} }
// 3. Generar el código QR
const qrUrl = 'http://vanityexperience.mx/qr'; const qrUrl = 'http://vanityexperience.mx/qr';
await QRCode.toCanvas(canvas, qrUrl, { width: 140, margin: 1 }); await QRCode.toCanvas(canvas, qrUrl, { width: 140, margin: 1 });
// 4. Llamar a la impresión.
// El `await` anterior asegura que el QR ya está renderizado.
// Un pequeño timeout puede seguir siendo útil para asegurar que el navegador "pinte" el canvas en la pantalla.
requestAnimationFrame(() => window.print()); requestAnimationFrame(() => window.print());
} catch (error) { } catch (error) {
@@ -116,3 +120,31 @@ export async function renderTicketAndPrint(mov, settings) {
alert(`Ocurrió un error al preparar la impresión: ${error.message}. Revise la consola para más detalles.`); alert(`Ocurrió un error al preparar la impresión: ${error.message}. Revise la consola para más detalles.`);
} }
} }
document.addEventListener('DOMContentLoaded', () => {
const btnTestTicket = document.getElementById('btnTestTicket');
if (btnTestTicket) {
btnTestTicket.addEventListener('click', () => {
const demoMovement = {
id: 'demo',
folio: 'DEMO-000001',
fechaISO: new Date().toISOString(),
client: {
nombre: 'Cliente de Prueba',
esOncologico: true,
nombreMedico: 'Dr. Juan Pérez',
telefonoMedico: '5512345678',
cedulaMedico: '1234567'
},
tipo: 'Pago',
monto: 123.45,
metodo: 'Efectivo',
concepto: 'Producto de demostración',
staff: 'Admin',
notas: 'Esta es una impresión de prueba.'
};
renderTicketAndPrint(demoMovement, window.settings || {});
});
}
});

View File

@@ -281,7 +281,6 @@ button.action-btn {
.header-logo { .header-logo {
max-width: 20%; max-width: 20%;
height: auto; height: auto;
padding-right: 50px;
} }
.tabs { .tabs {