mirror of
https://github.com/marcogll/ap_pos.git
synced 2026-01-13 13:15:16 +00:00
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:
@@ -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);
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
|||||||
Reference in New Issue
Block a user