feat: Agregar funcionalidad de citas

Se agrega la capacidad de agendar citas para los servicios.

- Se añaden campos de fecha y hora de cita en el formulario de nuevo movimiento.

- Se actualiza la tabla de movimientos para mostrar la información de la cita.

- Se modifica la base de datos para almacenar la fecha y hora de la cita.

- Se ajusta la exportación a CSV para incluir los nuevos campos.

- Se reemplaza la generación de folio secuencial por uno aleatorio.
This commit is contained in:
Marco Gallegos
2025-08-12 21:57:46 -06:00
parent dc7dcf84ce
commit f3ef5952d2
4 changed files with 38 additions and 30 deletions

View File

@@ -31,11 +31,13 @@ const clientDatalist = document.getElementById('client-list');
// --- LÓGICA DE NEGOCIO --- // --- LÓGICA DE NEGOCIO ---
async function getNextFolio() { function generateFolio() {
const folio = `${settings.folioPrefix || ''}${String(settings.folioSeq).padStart(6, '0')}`; const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
settings.folioSeq += 1; let result = '';
await save(KEY_SETTINGS, settings); for (let i = 0; i < 5; i++) {
return folio; result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
} }
async function addMovement(mov) { async function addMovement(mov) {
@@ -57,10 +59,8 @@ async function saveClient(clientData) {
let isUpdate = false; let isUpdate = false;
if (clientData) { if (clientData) {
// Data is passed directly (e.g., from new movement creation)
clientToSave = clientData; clientToSave = clientData;
} else { } else {
// Read from the client form
isUpdate = !!document.getElementById('c-id').value; isUpdate = !!document.getElementById('c-id').value;
const id = isUpdate ? document.getElementById('c-id').value : crypto.randomUUID(); const id = isUpdate ? document.getElementById('c-id').value : crypto.randomUUID();
clientToSave = { clientToSave = {
@@ -80,7 +80,6 @@ async function saveClient(clientData) {
clients[index] = clientToSave; clients[index] = clientToSave;
} }
} else { } else {
// Avoid duplicates if client was already added optimistically
if (!clients.some(c => c.id === clientToSave.id)) { if (!clients.some(c => c.id === clientToSave.id)) {
clients.push(clientToSave); clients.push(clientToSave);
} }
@@ -89,7 +88,6 @@ async function saveClient(clientData) {
renderClientsTable(); renderClientsTable();
updateClientDatalist(); updateClientDatalist();
// Only reset the form if we were using it
if (!clientData) { if (!clientData) {
document.getElementById('formClient').reset(); document.getElementById('formClient').reset();
document.getElementById('c-id').value = ''; document.getElementById('c-id').value = '';
@@ -106,12 +104,13 @@ async function deleteClient(id) {
} }
function exportCSV() { function exportCSV() {
const headers = 'folio,fechaISO,cliente,tipo,monto,metodo,concepto,staff,notas'; const headers = 'folio,fechaISO,cliente,tipo,monto,metodo,concepto,staff,notas,fechaCita,horaCita';
const rows = movements.map(m => { const rows = movements.map(m => {
const client = clients.find(c => c.id === m.clienteId); const client = clients.find(c => c.id === m.clienteId);
return [ return [
m.folio, m.fechaISO, client ? client.nombre : 'N/A', m.tipo, m.monto, m.folio, m.fechaISO, client ? client.nombre : 'N/A', m.tipo, m.monto,
m.metodo || '', m.concepto || '', m.staff || '', m.notas || '' m.metodo || '', m.concepto || '', m.staff || '', m.notas || '',
m.fechaCita || '', m.horaCita || ''
].map(val => `"${String(val).replace(/"/g, '""')}"`).join(','); ].map(val => `"${String(val).replace(/"/g, '""')}"`).join(',');
}); });
@@ -144,9 +143,11 @@ function renderTable() {
movements.forEach(mov => { movements.forEach(mov => {
const client = clients.find(c => c.id === mov.clienteId); const client = clients.find(c => c.id === mov.clienteId);
const tr = document.createElement('tr'); const tr = document.createElement('tr');
const fechaCita = mov.fechaCita ? new Date(mov.fechaCita + 'T00:00:00').toLocaleDateString('es-MX') : '';
tr.innerHTML = ` tr.innerHTML = `
<td><a href="#" class="action-btn" data-id="${mov.id}" data-action="reprint">${mov.folio}</a></td> <td><a href="#" class="action-btn" data-id="${mov.id}" data-action="reprint">${mov.folio}</a></td>
<td>${new Date(mov.fechaISO).toLocaleDateString('es-MX')}</td> <td>${new Date(mov.fechaISO).toLocaleDateString('es-MX')}</td>
<td>${fechaCita} ${mov.horaCita || ''}</td>
<td>${client ? client.nombre : 'Cliente Eliminado'}</td> <td>${client ? client.nombre : 'Cliente Eliminado'}</td>
<td>${mov.tipo}</td> <td>${mov.tipo}</td>
<td>${Number(mov.monto).toFixed(2)}</td> <td>${Number(mov.monto).toFixed(2)}</td>
@@ -221,16 +222,16 @@ async function handleNewMovement(e) {
cumpleaños: '', cumpleaños: '',
consentimiento: false consentimiento: false
}; };
await saveClient(newClient); // This now works correctly await saveClient(newClient);
client = newClient; client = newClient;
} else { } else {
return; // Do not create movement if client is not created return;
} }
} }
const newMovement = { const newMovement = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
folio: await getNextFolio(), folio: generateFolio(),
fechaISO: new Date().toISOString(), fechaISO: new Date().toISOString(),
clienteId: client.id, clienteId: client.id,
tipo: document.getElementById('m-tipo').value, tipo: document.getElementById('m-tipo').value,
@@ -239,13 +240,15 @@ async function handleNewMovement(e) {
concepto: document.getElementById('m-concepto').value, concepto: document.getElementById('m-concepto').value,
staff: document.getElementById('m-staff').value, staff: document.getElementById('m-staff').value,
notas: document.getElementById('m-notas').value, notas: document.getElementById('m-notas').value,
fechaCita: document.getElementById('m-fecha-cita').value,
horaCita: document.getElementById('m-hora-cita').value,
}; };
await addMovement(newMovement); await addMovement(newMovement);
const movementForTicket = { ...newMovement, cliente: client.nombre }; const movementForTicket = { ...newMovement, cliente: client.nombre };
renderTicketAndPrint(movementForTicket, settings); renderTicketAndPrint(movementForTicket, settings);
form.reset(); form.reset();
document.getElementById('m-cliente').focus(); // Poner el foco en el campo de cliente document.getElementById('m-cliente').focus();
} }
function handleTableClick(e) { function handleTableClick(e) {
@@ -289,15 +292,13 @@ async function handleClientForm(e) {
function handleTabChange(e) { function handleTabChange(e) {
const tabButton = e.target.closest('.tab-link'); const tabButton = e.target.closest('.tab-link');
if (!tabButton) return; // Si el clic no fue en un botón de pestaña, no hacer nada. if (!tabButton) return;
e.preventDefault(); e.preventDefault();
// Quitar la clase 'active' de 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 correcta y su contenido.
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');
@@ -324,7 +325,6 @@ function handleTestTicket() {
function initializeApp() { function initializeApp() {
const tabs = document.querySelector('.tabs'); const tabs = document.querySelector('.tabs');
// Conectar eventos
formSettings?.addEventListener('submit', handleSaveSettings); formSettings?.addEventListener('submit', handleSaveSettings);
formMove?.addEventListener('submit', handleNewMovement); formMove?.addEventListener('submit', handleNewMovement);
tblMovesBody?.addEventListener('click', handleTableClick); tblMovesBody?.addEventListener('click', handleTableClick);
@@ -338,7 +338,6 @@ function initializeApp() {
document.getElementById('c-id').value = ''; document.getElementById('c-id').value = '';
}); });
// Cargar datos y renderizar
Promise.all([ Promise.all([
load(KEY_SETTINGS, DEFAULT_SETTINGS), load(KEY_SETTINGS, DEFAULT_SETTINGS),
load(KEY_DATA, []), load(KEY_DATA, []),
@@ -355,5 +354,4 @@ function initializeApp() {
}); });
} }
// Esperar a que el DOM esté completamente cargado para iniciar la app document.addEventListener('DOMContentLoaded', initializeApp);
document.addEventListener('DOMContentLoaded', initializeApp);

View File

@@ -29,11 +29,16 @@
<input type="text" id="m-cliente" list="client-list" required autocomplete="off" /> <input type="text" id="m-cliente" list="client-list" required autocomplete="off" />
<datalist id="client-list"></datalist> <datalist id="client-list"></datalist>
</div> </div>
<label>Tipo:</label> <label>Servicio:</label>
<select id="m-tipo" required> <select id="m-tipo" required>
<option value="Pago">Pago</option> <option value="Microblading">Microblading</option>
<option value="Anticipo">Anticipo</option> <option value="Lashes">Lashes</option>
<option value="Nail Art">Nail Art</option>
</select> </select>
<label>Fecha de Cita:</label>
<input type="date" id="m-fecha-cita" />
<label>Hora de Cita:</label>
<input type="time" id="m-hora-cita" />
<label>Monto (MXN):</label><input type="number" id="m-monto" step="0.01" min="0" required /> <label>Monto (MXN):</label><input type="number" id="m-monto" step="0.01" min="0" required />
<label>Método:</label> <label>Método:</label>
<select id="m-metodo"> <select id="m-metodo">
@@ -64,8 +69,9 @@
<tr> <tr>
<th>Folio</th> <th>Folio</th>
<th>Fecha</th> <th>Fecha</th>
<th>Cita</th>
<th>Cliente</th> <th>Cliente</th>
<th>Tipo</th> <th>Servicio</th>
<th>Monto</th> <th>Monto</th>
<th>Acciones</th> <th>Acciones</th>
</tr> </tr>

View File

@@ -52,6 +52,8 @@ db.serialize(() => {
concepto TEXT, concepto TEXT,
staff TEXT, staff TEXT,
notas TEXT, notas TEXT,
fechaCita TEXT,
horaCita TEXT,
FOREIGN KEY (clienteId) REFERENCES clients (id) FOREIGN KEY (clienteId) REFERENCES clients (id)
)`); )`);
}); });
@@ -130,10 +132,10 @@ app.get('/api/movements', (req, res) => {
app.post('/api/movements', (req, res) => { app.post('/api/movements', (req, res) => {
const { movement } = req.body; const { movement } = req.body;
const { id, folio, fechaISO, clienteId, tipo, monto, metodo, concepto, staff, notas } = 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) db.run(`INSERT INTO movements (id, folio, fechaISO, clienteId, tipo, monto, metodo, concepto, staff, notas, fechaCita, horaCita)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[id, folio, fechaISO, clienteId, tipo, monto, metodo, concepto, staff, notas], function(err) { [id, folio, fechaISO, clienteId, tipo, monto, metodo, concepto, staff, notas, fechaCita, horaCita], function(err) {
if (err) { if (err) {
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
return; return;

View File

@@ -63,6 +63,8 @@ label {
input[type="text"], input[type="text"],
input[type="number"], input[type="number"],
input[type="date"],
input[type="time"],
select, select,
textarea { textarea {
width: 100%; width: 100%;