import { load, save, remove, KEY_DATA, KEY_SETTINGS, KEY_CLIENTS } from './storage.js'; import { renderTicketAndPrint } from './print.js?v=1.8'; // --- UTILITIES --- function escapeHTML(str) { if (str === null || str === undefined) { return ''; } return str.toString() .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function construirFechaCita() { const dia = document.getElementById('m-cita-dia').value; const mes = document.getElementById('m-cita-mes').value; const año = document.getElementById('m-cita-año').value; if (!dia || !mes || !año) { return ''; } // Formatear con ceros a la izquierda const diaStr = dia.padStart(2, '0'); const mesStr = mes.padStart(2, '0'); // Retornar en formato YYYY-MM-DD para compatibilidad return `${año}-${mesStr}-${diaStr}`; } // Sistema dinámico de productos y descuentos let selectedProducts = []; let currentSubtotal = 0; let currentDiscount = 0; function initializeDynamicSystem() { const articuloSelect = document.getElementById('m-articulo'); const categoriaSelect = document.getElementById('m-categoria'); const addProductBtn = document.getElementById('add-product-btn'); const discountType = document.getElementById('discount-type'); const discountValue = document.getElementById('discount-value'); const discountReason = document.getElementById('discount-reason'); const clienteInput = document.getElementById('m-cliente'); // Listener para cambio de categoría (servicio/curso) if (categoriaSelect) { categoriaSelect.addEventListener('change', function() { populateArticuloDropdown(this.value); }); } // Botón para agregar productos if (addProductBtn) { addProductBtn.addEventListener('click', addCurrentProduct); } // Sistema de descuentos colapsable const discountToggle = document.getElementById('discount-toggle'); const discountContainer = document.getElementById('discount-container'); const discountSymbol = document.getElementById('discount-symbol'); if (discountToggle && discountContainer) { discountToggle.addEventListener('change', function() { if (this.checked) { discountContainer.style.display = 'block'; // Habilitar campos cuando se abre la sección if (discountType.value) { discountValue.disabled = false; discountReason.disabled = false; } } else { discountContainer.style.display = 'none'; // Limpiar y deshabilitar campos cuando se cierra discountType.value = ''; discountValue.value = ''; discountReason.value = ''; discountValue.disabled = true; discountReason.disabled = true; calculateTotals(); } }); } if (discountType) { discountType.addEventListener('change', function() { const isDiscountSelected = this.value !== ''; discountValue.disabled = !isDiscountSelected; discountReason.disabled = !isDiscountSelected; // Actualizar símbolo según el tipo if (discountSymbol) { if (this.value === 'percentage') { discountSymbol.textContent = '%'; } else if (this.value === 'amount') { discountSymbol.textContent = '$'; } else if (this.value === 'warrior') { discountSymbol.textContent = '🎗️'; } else { discountSymbol.textContent = '%'; } } if (!isDiscountSelected) { discountValue.value = ''; discountReason.value = ''; } calculateTotals(); }); } if (discountValue) { discountValue.addEventListener('input', calculateTotals); } // Detección automática de pacientes oncológicos para descuento Warrior if (clienteInput) { clienteInput.addEventListener('blur', function() { const clienteNombre = this.value.trim(); if (clienteNombre) { const client = clients.find(c => c.nombre.toLowerCase() === clienteNombre.toLowerCase()); if (client && client.esOncologico) { // Activar automáticamente el descuento Warrior activateWarriorDiscount(); } // Cargar anticipos disponibles del cliente loadClientAnticipos(clienteNombre); } else { // Si no hay cliente, ocultar anticipos document.getElementById('anticipos-section').style.display = 'none'; } }); } } function activateWarriorDiscount() { const discountToggle = document.getElementById('discount-toggle'); const discountContainer = document.getElementById('discount-container'); const discountType = document.getElementById('discount-type'); const discountValue = document.getElementById('discount-value'); const discountReason = document.getElementById('discount-reason'); // Activar la sección de descuentos if (discountToggle && !discountToggle.checked) { discountToggle.checked = true; if (discountContainer) { discountContainer.style.display = 'block'; } } // Seleccionar descuento Warrior if (discountType) { discountType.value = 'warrior'; discountType.dispatchEvent(new Event('change')); } // Establecer valores automáticamente if (discountValue) { discountValue.value = 100; discountValue.disabled = true; } if (discountReason) { discountReason.value = 'Paciente Oncológico'; discountReason.disabled = true; } // Calcular totales calculateTotals(); } function showDynamicSections() { // Show the product selection area and totals const selectedProducts = document.getElementById('selected-products'); const totalsSection = document.querySelector('.totals-section'); if (selectedProducts) selectedProducts.style.display = 'block'; if (totalsSection) totalsSection.style.display = 'block'; } function hideDynamicSections() { const selectedProductsEl = document.getElementById('selected-products'); const totalsSection = document.querySelector('.totals-section'); if (selectedProductsEl) selectedProductsEl.style.display = 'none'; if (totalsSection) totalsSection.style.display = 'none'; selectedProducts = []; renderSelectedProducts(); } function addCurrentProduct() { const articuloSelect = document.getElementById('m-articulo'); const categoriaSelect = document.getElementById('m-categoria'); const quantityInput = document.getElementById('product-quantity'); if (!categoriaSelect.value) { alert('Selecciona el tipo (servicio, curso o anticipo) primero'); return; } if (!articuloSelect.value) { alert('Selecciona un producto primero'); return; } const quantity = parseInt(quantityInput.value) || 1; // Manejar anticipos de forma especial if (categoriaSelect.value === 'anticipo') { let anticipoAmount = prompt('Ingresa el monto del anticipo:', ''); if (anticipoAmount === null) return; // Usuario canceló anticipoAmount = parseFloat(anticipoAmount); if (isNaN(anticipoAmount) || anticipoAmount <= 0) { alert('Por favor ingresa un monto válido para el anticipo'); return; } const clienteInput = document.getElementById('m-cliente'); const clienteName = clienteInput.value.trim(); let anticipoName = 'Anticipo'; if (clienteName) { anticipoName = `Anticipo - ${clienteName}`; } const existingIndex = selectedProducts.findIndex(p => p.name === anticipoName); if (existingIndex >= 0) { selectedProducts[existingIndex].quantity += quantity; selectedProducts[existingIndex].price += anticipoAmount; // Acumular el monto } else { selectedProducts.push({ id: 'anticipo-' + Date.now(), name: anticipoName, price: anticipoAmount, quantity: quantity, type: 'anticipo' }); } } else { // Manejar servicios y cursos como antes const productData = products.find(p => p.name === articuloSelect.value && p.type === categoriaSelect.value); if (productData) { const existingIndex = selectedProducts.findIndex(p => p.name === productData.name); if (existingIndex >= 0) { selectedProducts[existingIndex].quantity += quantity; } else { selectedProducts.push({ id: productData.id, name: productData.name, price: parseFloat(productData.price), quantity: quantity, type: categoriaSelect.value }); } } else { alert('Producto no encontrado'); return; } } renderSelectedProducts(); calculateTotals(); quantityInput.value = 1; articuloSelect.value = ''; // Mostrar descuento inmediatamente showDiscountSection(); } function removeProduct(productName) { selectedProducts = selectedProducts.filter(p => p.name !== productName); renderSelectedProducts(); calculateTotals(); } function renderSelectedProducts() { const container = document.getElementById('selected-products'); if (!container) return; if (selectedProducts.length === 0) { container.innerHTML = '
No hay productos seleccionados
'; return; } const html = selectedProducts.map(product => `No hay citas próximas.
'; } } } catch (error) { console.error('Error al cargar el dashboard:', error); } finally { isDashboardLoading = false; } } function generateFolio() { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < 5; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } async function addMovement(mov) { try { const response = await fetch('/api/movements', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ movement: mov }) }); if (response.ok) { movements.unshift(mov); renderTable(); } else { throw new Error('Failed to save movement'); } } catch (error) { console.error('Error saving movement:', error); alert('Error al guardar el movimiento'); } } async function deleteMovement(id) { if (confirm('¿Estás seguro de que quieres eliminar este movimiento?')) { try { const response = await fetch(`/api/movements/${id}`, { method: 'DELETE' }); if (response.ok) { movements = movements.filter(m => m.id !== id); renderTable(); } else { throw new Error('Failed to delete movement'); } } catch (error) { console.error('Error deleting movement:', error); alert('Error al eliminar el movimiento'); } } } async function saveClient(clientData) { let clientToSave; let isUpdate = false; if (clientData) { clientToSave = clientData; } else { isUpdate = !!document.getElementById('c-id').value; const id = isUpdate ? document.getElementById('c-id').value : crypto.randomUUID(); clientToSave = { id: id, nombre: document.getElementById('c-nombre').value, telefono: document.getElementById('c-telefono').value, genero: document.getElementById('c-genero').value, cumpleaños: document.getElementById('c-cumple').value, consentimiento: document.getElementById('c-consent').checked, esOncologico: document.getElementById('c-pacienteOncologico').checked, oncologoAprueba: document.getElementById('c-oncologoAprueba').checked, nombreMedico: document.getElementById('c-nombreMedico').value, telefonoMedico: document.getElementById('c-telefonoMedico').value, cedulaMedico: document.getElementById('c-cedulaMedico').value, pruebaAprobacion: document.getElementById('c-pruebaAprobacion').checked, }; } try { const response = await fetch('/api/clients', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client: clientToSave }) }); if (!response.ok) { throw new Error('Failed to save client'); } } catch (error) { console.error('Error saving client:', error); alert('Error al guardar el cliente'); return; } if (isUpdate) { const index = clients.findIndex(c => c.id === clientToSave.id); if (index > -1) clients[index] = clientToSave; } else { clients.unshift(clientToSave); } renderClientsTable(); updateClientDatalist(); if (!clientData) { document.getElementById('formClient').reset(); document.getElementById('c-id').value = ''; document.getElementById('oncologico-fields').classList.add('hidden'); } } async function deleteClient(id) { if (confirm('¿Estás seguro de que quieres eliminar este cliente? Se conservarán sus recibos históricos.')) { try { const response = await fetch(`/api/clients/${id}`, { method: 'DELETE' }); if (response.ok) { clients = clients.filter(c => c.id !== id); renderClientsTable(); updateClientDatalist(); clearClientRecord(); } else { throw new Error('Failed to delete client'); } } catch (error) { console.error('Error deleting client:', error); alert('Error al eliminar el cliente'); } } } function exportCSV() { const headers = 'folio,fechaISO,cliente,tipo,monto,metodo,concepto,staff,notas,fechaCita,horaCita'; const rows = movements.map(m => { const client = clients.find(c => c.id === m.clienteId); return [ m.folio, m.fechaISO, client ? client.nombre : 'N/A', m.tipo, m.monto, m.metodo || '', m.concepto || '', m.staff || '', m.notas || '', m.fechaCita || '', m.horaCita || '' ].map(val => `"${String(val).replace(/"/g, '""')}"`).join(','); }); const csvContent = `data:text/csv;charset=utf-8,${headers}\n${rows.join('\n')}`; const encodedUri = encodeURI(csvContent); const link = document.createElement('a'); link.setAttribute('href', encodedUri); link.setAttribute('download', 'movimientos.csv'); document.body.appendChild(link); link.click(); document.body.removeChild(link); } // --- RENDERIZADO --- function renderSettings() { document.getElementById('s-negocio').value = settings.negocio || ''; document.getElementById('s-tagline').value = settings.tagline || ''; document.getElementById('s-calle').value = settings.calle || ''; document.getElementById('s-colonia-cp').value = settings.colonia && settings.cp ? `${settings.colonia}, ${settings.cp}` : ''; document.getElementById('s-rfc').value = settings.rfc || ''; document.getElementById('s-tel').value = settings.tel || ''; document.getElementById('s-leyenda').value = settings.leyenda || ''; document.getElementById('s-folioPrefix').value = settings.folioPrefix || ''; } function renderTable() { if (!tblMovesBody) return; tblMovesBody.innerHTML = ''; movements.forEach(mov => { const client = clients.find(c => c.id === mov.clienteId); const tr = tblMovesBody.insertRow(); const tipoServicio = mov.subtipo ? `${escapeHTML(mov.tipo)} (${escapeHTML(mov.subtipo)})` : escapeHTML(mov.tipo); const folioCell = tr.insertCell(); const folioLink = document.createElement('a'); folioLink.href = '#'; folioLink.className = 'action-btn'; folioLink.dataset.id = mov.id; folioLink.dataset.action = 'reprint'; folioLink.textContent = mov.folio; folioCell.appendChild(folioLink); tr.insertCell().textContent = formatDate(mov.fechaISO); tr.insertCell().textContent = `${formatDate(mov.fechaCita)} ${mov.horaCita || ''}`.trim(); tr.insertCell().textContent = client ? escapeHTML(client.nombre) : 'Cliente Eliminado'; tr.insertCell().textContent = tipoServicio; tr.insertCell().textContent = Number(mov.monto).toFixed(2); const actionsCell = tr.insertCell(); const deleteButton = document.createElement('button'); deleteButton.className = 'action-btn'; deleteButton.dataset.id = mov.id; deleteButton.dataset.action = 'delete'; deleteButton.textContent = 'Eliminar'; actionsCell.appendChild(deleteButton); }); } function renderClientsTable(clientList = clients) { if (!tblClientsBody) return; tblClientsBody.innerHTML = ''; clientList.forEach(c => { const tr = tblClientsBody.insertRow(); tr.dataset.id = c.id; if (c.id === currentClientId) { tr.classList.add('selected'); } tr.insertCell().textContent = escapeHTML(c.nombre); tr.insertCell().textContent = escapeHTML(c.telefono || ''); }); } function renderUsersTable() { if (!tblUsersBody) return; tblUsersBody.innerHTML = ''; users.forEach(u => { const tr = tblUsersBody.insertRow(); tr.insertCell().textContent = escapeHTML(u.name); tr.insertCell().textContent = escapeHTML(u.username); tr.insertCell().textContent = u.role === 'admin' ? 'Administrador' : 'Usuario'; const actionsCell = tr.insertCell(); const editButton = document.createElement('button'); editButton.className = 'action-btn'; editButton.dataset.id = u.id; editButton.dataset.action = 'edit-user'; editButton.textContent = 'Editar'; actionsCell.appendChild(editButton); if (u.id !== currentUser.id) { const deleteButton = document.createElement('button'); deleteButton.className = 'action-btn'; deleteButton.dataset.id = u.id; deleteButton.dataset.action = 'delete-user'; deleteButton.textContent = 'Eliminar'; actionsCell.appendChild(deleteButton); } }); } function renderProductTables() { const tblServicesBody = document.getElementById('tblServices')?.querySelector('tbody'); const tblCoursesBody = document.getElementById('tblCourses')?.querySelector('tbody'); if (!tblServicesBody || !tblCoursesBody) return; tblServicesBody.innerHTML = ''; tblCoursesBody.innerHTML = ''; products.forEach(p => { const tableBody = p.type === 'service' ? tblServicesBody : tblCoursesBody; const tr = tableBody.insertRow(); tr.insertCell().textContent = escapeHTML(p.name); tr.insertCell().textContent = Number(p.price || 0).toFixed(2); const actionsCell = tr.insertCell(); const editButton = document.createElement('button'); editButton.className = 'action-btn'; editButton.dataset.id = p.id; editButton.dataset.action = 'edit-product'; editButton.textContent = 'Editar'; actionsCell.appendChild(editButton); const deleteButton = document.createElement('button'); deleteButton.className = 'action-btn'; deleteButton.dataset.id = p.id; deleteButton.dataset.action = 'delete-product'; deleteButton.textContent = 'Eliminar'; actionsCell.appendChild(deleteButton); }); } function updateClientDatalist() { if (!clientDatalist) return; clientDatalist.innerHTML = ''; clients.forEach(c => { const option = document.createElement('option'); option.value = c.nombre; clientDatalist.appendChild(option); }); } function populateArticuloDropdown(category) { const articuloSelect = document.getElementById('m-articulo'); if (!articuloSelect) return; // Clear existing options except the first default option if (category) { let placeholder = ''; if (category === 'service') placeholder = 'servicio'; else if (category === 'course') placeholder = 'curso'; else if (category === 'anticipo') placeholder = 'anticipo'; articuloSelect.innerHTML = ``; if (category === 'anticipo') { // Para anticipos, permitir búsqueda automática o ingreso manual const option = document.createElement('option'); option.value = 'Anticipo'; option.textContent = 'Anticipo - $0.00 (Ingreso manual)'; articuloSelect.appendChild(option); } else { const items = products.filter(p => p.type === category); items.forEach(i => { const option = document.createElement('option'); option.value = i.name; option.textContent = `${i.name} - $${parseFloat(i.price).toFixed(2)}`; articuloSelect.appendChild(option); }); } } else { articuloSelect.innerHTML = ''; } } // --- MANEJADORES DE EVENTOS --- async function handleSaveSettings(e) { e.preventDefault(); settings.negocio = document.getElementById('s-negocio').value; settings.tagline = document.getElementById('s-tagline').value; settings.calle = document.getElementById('s-calle').value; const coloniaCp = document.getElementById('s-colonia-cp').value.split(','); settings.colonia = coloniaCp[0]?.trim() || ''; settings.cp = coloniaCp[1]?.trim() || ''; settings.rfc = document.getElementById('s-rfc').value; settings.tel = document.getElementById('s-tel').value; settings.leyenda = document.getElementById('s-leyenda').value; settings.folioPrefix = document.getElementById('s-folioPrefix').value; try { const response = await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings }) }); if (response.ok) { alert('Configuración guardada.'); } else { throw new Error('Failed to save settings'); } } catch (error) { console.error('Error saving settings:', error); alert('Error al guardar la configuración'); } } async function handleSaveCredentials(e) { e.preventDefault(); const name = document.getElementById('s-name').value; const username = document.getElementById('s-username').value; const password = document.getElementById('s-password').value; const body = { username, name }; if (password) { body.password = password; } try { const response = await fetch('/api/user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (response.ok) { alert('Credenciales actualizadas.'); currentUser.name = name; currentUser.username = username; document.getElementById('s-password').value = ''; } else { const error = await response.json(); alert(`Error: ${error.error}`); } } catch (error) { alert('Error de conexión al guardar credenciales.'); } } async function handleAddOrUpdateUser(e) { e.preventDefault(); const id = document.getElementById('u-id').value; const name = document.getElementById('u-name').value; const username = document.getElementById('u-username').value; const password = document.getElementById('u-password').value; const role = document.getElementById('u-role').value; const isUpdate = !!id; const url = isUpdate ? `/api/users/${id}` : '/api/users'; const method = isUpdate ? 'PUT' : 'POST'; const body = { name, username, role }; if (password || !isUpdate) { if (!password && !isUpdate) { alert('La contraseña es obligatoria para nuevos usuarios.'); return; } body.password = password; } try { const response = await fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const result = await response.json(); if (response.ok) { alert(`Usuario ${isUpdate ? 'actualizado' : 'creado'} exitosamente.`); if (isUpdate) { const index = users.findIndex(u => u.id === parseInt(id)); if (index > -1) { users[index] = { ...users[index], name, username, role }; } } else { users.push(result); } renderUsersTable(); formAddUser.reset(); document.getElementById('u-id').value = ''; } else { alert(`Error: ${result.error}`); } } catch (error) { alert('Error de conexión al guardar el usuario.'); } } async function deleteUser(id) { if (confirm('¿Estás seguro de que quieres eliminar este usuario?')) { try { const response = await fetch(`/api/users/${id}`, { method: 'DELETE' }); if (response.ok) { users = users.filter(u => u.id !== id); renderUsersTable(); } else { const error = await response.json(); alert(`Error: ${error.error}`); } } catch (error) { alert('Error de conexión al eliminar usuario.'); } } } async function handleAddOrUpdateProduct(e) { e.preventDefault(); const id = document.getElementById('p-id').value; const name = document.getElementById('p-name').value; const type = document.getElementById('p-type').value; const price = document.getElementById('p-price').value; const isUpdate = !!id; const url = isUpdate ? `/api/products/${id}` : '/api/products'; const method = isUpdate ? 'PUT' : 'POST'; const body = { name, type, price }; try { const response = await fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const result = await response.json(); if (response.ok) { alert(`Producto ${isUpdate ? 'actualizado' : 'creado'} exitosamente.`); if (isUpdate) { const index = products.findIndex(p => p.id === parseInt(id)); if (index > -1) { products[index] = { ...products[index], name, type, price }; } } else { products.push(result); } renderProductTables(); formProduct.reset(); document.getElementById('p-id').value = ''; } else { alert(`Error: ${result.error}`); } } catch (error) { alert('Error de conexión al guardar el producto.'); } } async function deleteProduct(id) { if (confirm('¿Estás seguro de que quieres eliminar este producto?')) { try { const response = await fetch(`/api/products/${id}`, { method: 'DELETE' }); if (response.ok) { products = products.filter(p => p.id !== id); renderProductTables(); } } catch (error) { alert('Error de conexión al eliminar el producto.'); } } } function showAddCourseModal(clientId) { const courses = products.filter(p => p.type === 'course'); const courseOptions = courses.map(c => ``).join(''); const modalHTML = `Nombre: ${escapeHTML(client.nombre)}
Teléfono: ${escapeHTML(client.telefono || 'N/A')}
Cumpleaños: ${escapeHTML(formatDate(client.cumpleaños) || 'N/A')}
Género: ${escapeHTML(client.genero || 'N/A')}
Oncológico: ${client.esOncologico ? 'Sí' : 'No'}
`; try { const [historyResponse, coursesResponse] = await Promise.all([ fetch(`/api/clients/${client.id}/history`), fetch(`/api/clients/${client.id}/courses`) ]); const history = await historyResponse.json(); const courses = await coursesResponse.json(); clientHistoryTableBody.innerHTML = ''; if (history.length > 0) { history.forEach(mov => { const tr = clientHistoryTableBody.insertRow(); const servicio = mov.subtipo ? `${escapeHTML(mov.tipo)} (${escapeHTML(mov.subtipo)})` : escapeHTML(mov.tipo); tr.insertCell().textContent = mov.folio; tr.insertCell().textContent = formatDate(mov.fechaISO); tr.insertCell().textContent = servicio; tr.insertCell().textContent = Number(mov.monto).toFixed(2); }); } else { clientHistoryTableBody.innerHTML = 'No hay cursos registrados para este cliente.
'; } } catch (error) { console.error('Error al cargar el historial del cliente:', error); clientHistoryTableBody.innerHTML = 'Error al cargar historial de cursos.
'; } clientRecordContent.classList.remove('hidden'); clientRecordPlaceholder.classList.add('hidden'); } function clearClientRecord() { currentClientId = null; const clientRecordContent = document.getElementById('client-record-content'); const clientRecordPlaceholder = document.getElementById('client-record-placeholder'); clientRecordContent.classList.add('hidden'); clientRecordPlaceholder.classList.remove('hidden'); renderClientsTable(); } function handleTableClick(e) { const target = e.target; const row = target.closest('tr'); if (!row) return; const tableId = row.closest('table')?.id; if (tableId === 'tblClients') { const clientId = row.dataset.id; showClientRecord(clientId); return; } const actionBtn = target.closest('.action-btn'); if (actionBtn) { e.preventDefault(); const id = actionBtn.dataset.id; const action = actionBtn.dataset.action; if (action === 'reprint' || action === 'delete') { const movement = movements.find(m => m.id === id); if (movement) { if (action === 'reprint') { const client = clients.find(c => c.id === movement.clienteId); renderTicketAndPrint({ ...movement, client }, settings); } else if (action === 'delete') { deleteMovement(id); } } } else if (action === 'edit-user') { const user = users.find(u => u.id === parseInt(id)); if (user) { document.getElementById('u-id').value = user.id; document.getElementById('u-name').value = user.name; document.getElementById('u-username').value = user.username; document.getElementById('u-role').value = user.role; document.getElementById('u-password').value = ''; document.getElementById('u-password').placeholder = 'Dejar en blanco para no cambiar'; } } else if (action === 'delete-user') { deleteUser(parseInt(id, 10)); } else if (action === 'edit-product') { const product = products.find(p => p.id === parseInt(id)); if (product) { document.getElementById('p-id').value = product.id; document.getElementById('p-name').value = product.name; document.getElementById('p-type').value = product.type; document.getElementById('p-price').value = product.price; } } else if (action === 'delete-product') { deleteProduct(parseInt(id, 10)); } } } async function handleClientForm(e) { e.preventDefault(); await saveClient(); activateClientSubTab('sub-tab-consult'); } function activateClientSubTab(subTabId) { if (!subTabId) return; 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')); const tabButton = document.querySelector(`[data-subtab="${subTabId}"]`); const tabContent = document.getElementById(subTabId); if (tabButton) { tabButton.classList.add('active'); } if (tabContent) { tabContent.classList.add('active'); } } function handleClientTabChange(e) { const subTabButton = e.target.closest('.sub-tab-link'); if (!subTabButton) return; e.preventDefault(); const subTabId = subTabButton.dataset.subtab; activateClientSubTab(subTabId); } function activateTab(tabId) { if (!tabId) return; document.querySelectorAll('.tab-link').forEach(tab => tab.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); const tabButton = document.querySelector(`[data-tab="${tabId}"]`); const tabContent = document.getElementById(tabId); if (tabButton) { tabButton.classList.add('active'); } if (tabContent) { tabContent.classList.add('active'); } if (tabId === 'tab-dashboard') { if (!incomeChart) { const ctx = document.getElementById('incomeChart').getContext('2d'); incomeChart = new Chart(ctx, { type: 'pie', data: { labels: [], datasets: [{ label: 'Ingresos por Servicio', data: [], backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40'], }] }, options: { responsive: true, maintainAspectRatio: false, animation: false } }); } if (!paymentMethodChart) { const ctx = document.getElementById('paymentMethodChart').getContext('2d'); paymentMethodChart = new Chart(ctx, { type: 'doughnut', data: { labels: [], datasets: [{ label: 'Ingresos por Método de Pago', data: [], backgroundColor: ['#4BC0C0', '#9966FF', '#FF9F40', '#FF6384', '#36A2EB', '#FFCE56'], }] }, options: { responsive: true, maintainAspectRatio: false, animation: false } }); } loadDashboardData(); } } function handleTabChange(e) { const tabButton = e.target.closest('.tab-link'); if (!tabButton) return; if (tabButton.dataset.tab) { e.preventDefault(); const tabId = tabButton.dataset.tab; activateTab(tabId); } } function handleTestTicket() { 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, settings); } function setupUIForRole(role) { const dashboardTab = document.querySelector('[data-tab="dashboard"]'); const settingsTab = document.querySelector('[data-tab="settings"]'); const userManagementSection = document.getElementById('user-management-section'); const staffInput = document.getElementById('m-staff'); const dbInfoIcon = document.getElementById('db-info-icon'); if (role === 'admin') { if (dashboardTab) dashboardTab.style.display = 'block'; if (settingsTab) settingsTab.style.display = 'block'; if (userManagementSection) userManagementSection.style.display = 'block'; if (dbInfoIcon) dbInfoIcon.style.display = 'inline-block'; fetch('/api/users') .then(res => { if (!res.ok) throw new Error('Failed to fetch users list'); return res.json(); }) .then(data => { users = data; renderUsersTable(); }) .catch(err => console.error(err)); } else { if (dashboardTab) dashboardTab.style.display = 'block'; if (settingsTab) settingsTab.style.display = 'block'; if (userManagementSection) userManagementSection.style.display = 'none'; if (dbInfoIcon) dbInfoIcon.style.display = 'none'; } if (staffInput) { staffInput.disabled = true; } } function populateFooter() { // Footer elements removed - no longer needed } // Make removeProduct globally accessible window.removeProduct = removeProduct; // --- INICIALIZACIÓN --- async function initializeApp() { let userResponse; try { userResponse = await fetch('/api/user'); if (!userResponse.ok) { window.location.href = '/login.html'; return; } const contentType = userResponse.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { console.error('La respuesta del servidor no es JSON. Redirigiendo al login.'); window.location.href = '/login.html'; return; } currentUser = await userResponse.json(); } catch (error) { console.error('Error de conexión al verificar la autenticación. Redirigiendo al login.', error); window.location.href = '/login.html'; return; } const tabs = document.querySelector('.tabs'); const btnLogout = document.getElementById('btnLogout'); const btnCancelEditUser = document.getElementById('btnCancelEditUser'); const tipoServicioSelect = document.getElementById('m-tipo'); const clientSubTabs = document.querySelector('#tab-clients .sub-tabs'); const dbInfoIcon = document.getElementById('db-info-icon'); formSettings?.addEventListener('submit', handleSaveSettings); formCredentials?.addEventListener('submit', handleSaveCredentials); formMove?.addEventListener('submit', handleNewMovement); tblMovesBody?.addEventListener('click', handleTableClick); tblClientsBody?.addEventListener('click', handleTableClick); tblServicesBody?.addEventListener('click', handleTableClick); tblCoursesBody?.addEventListener('click', handleTableClick); appointmentsList?.addEventListener('click', handleTableClick); btnExport?.addEventListener('click', exportCSV); btnTestTicket?.addEventListener('click', handleTestTicket); formClient?.addEventListener('submit', handleClientForm); formProduct?.addEventListener('submit', handleAddOrUpdateProduct); tabs?.addEventListener('click', handleTabChange); clientSubTabs?.addEventListener('click', handleClientTabChange); if (currentUser.role === 'admin') { formAddUser?.addEventListener('submit', handleAddOrUpdateUser); tblUsersBody?.addEventListener('click', handleTableClick); dbInfoIcon?.addEventListener('click', () => { document.getElementById('db-instructions').classList.toggle('hidden'); }); } btnLogout?.addEventListener('click', async () => { await fetch('/api/logout', { method: 'POST' }); window.location.href = '/login.html'; }); document.getElementById('btnCancelEditClient')?.addEventListener('click', () => { formClient.reset(); document.getElementById('c-id').value = ''; document.getElementById('oncologico-fields').classList.add('hidden'); }); document.getElementById('c-pacienteOncologico')?.addEventListener('change', (e) => { const oncologicoFields = document.getElementById('oncologico-fields'); if (oncologicoFields) { oncologicoFields.classList.toggle('hidden', !e.target.checked); oncologicoFields.classList.toggle('active', e.target.checked); } }); btnCancelEditUser?.addEventListener('click', (e) => { e.preventDefault(); formAddUser.reset(); document.getElementById('u-id').value = ''; document.getElementById('u-password').placeholder = 'Contraseña'; }); const searchClientInput = document.getElementById('search-client'); searchClientInput?.addEventListener('input', (e) => { const searchTerm = e.target.value.toLowerCase(); const filteredClients = clients.filter(c => c.nombre.toLowerCase().includes(searchTerm) || c.telefono?.toLowerCase().includes(searchTerm) ); renderClientsTable(filteredClients); }); const categoriaSelect = document.getElementById('m-categoria'); categoriaSelect?.addEventListener('change', (e) => { populateArticuloDropdown(e.target.value); }); tipoServicioSelect?.addEventListener('change', (e) => { const subtipoContainer = document.getElementById('m-subtipo-container'); const servicesWithSubtype = ['Microblading', 'Lashes', 'Nail Art', 'Lash Lifting']; subtipoContainer.classList.toggle('hidden', !servicesWithSubtype.includes(e.target.value)); }); document.getElementById('btn-edit-client')?.addEventListener('click', () => { if (!currentClientId) return; const client = clients.find(c => c.id === currentClientId); if (client) { document.getElementById('c-id').value = client.id; document.getElementById('c-nombre').value = client.nombre; document.getElementById('c-telefono').value = client.telefono || ''; document.getElementById('c-genero').value = client.genero || ''; document.getElementById('c-cumple').value = client.cumpleaños; document.getElementById('c-consent').checked = client.consentimiento; const esOncologicoCheckbox = document.getElementById('c-pacienteOncologico'); const oncologicoFields = document.getElementById('oncologico-fields'); if (esOncologicoCheckbox) { esOncologicoCheckbox.checked = client.esOncologico; } if (oncologicoFields) { oncologicoFields.classList.toggle('hidden', !client.esOncologico); oncologicoFields.classList.toggle('active', client.esOncologico); } document.getElementById('c-oncologoAprueba').checked = client.oncologoAprueba; document.getElementById('c-nombreMedico').value = client.nombreMedico || ''; document.getElementById('c-telefonoMedico').value = client.telefonoMedico || ''; document.getElementById('c-cedulaMedico').value = client.cedulaMedico || ''; document.getElementById('c-pruebaAprobacion').checked = client.pruebaAprobacion; activateClientSubTab('sub-tab-register'); } }); document.getElementById('btn-delete-client')?.addEventListener('click', () => { if (!currentClientId) return; deleteClient(currentClientId); }); document.getElementById('btnAddCourseToClient')?.addEventListener('click', () => { const clientId = document.getElementById('c-id').value; if (!clientId) { alert('Por favor, primero guarda el cliente antes de añadir un curso.'); return; } showAddCourseModal(clientId); }); Promise.all([ fetch('/api/settings').then(res => res.json()).catch(() => DEFAULT_SETTINGS), fetch('/api/movements').then(res => res.json()).catch(() => []), fetch('/api/clients').then(res => res.json()).catch(() => []), fetch('/api/products').then(res => res.json()).catch(() => []), ]).then(values => { console.log('Initial data loaded:', values); [settings, movements, clients, products] = values; console.log('Rendering settings...'); renderSettings(); console.log('Rendering movements table...'); renderTable(); console.log('Rendering clients table...'); renderClientsTable(); console.log('Rendering products table...'); renderProductTables(); console.log('Updating client datalist...'); updateClientDatalist(); populateArticuloDropdown(''); if (currentUser) { console.log('Setting user info in form...'); document.getElementById('s-name').value = currentUser.name || ''; document.getElementById('s-username').value = currentUser.username; document.getElementById('m-staff').value = currentUser.name || ''; } console.log('Setting up UI for role...'); setupUIForRole(currentUser.role); console.log('Activating initial tab...'); activateTab('tab-dashboard'); console.log('Activating client sub-tab...'); activateClientSubTab('sub-tab-register'); console.log('Clearing client record...'); clearClientRecord(); console.log('Populating footer...'); populateFooter(); console.log('Initializing dynamic system...'); initializeDynamicSystem(); console.log('Initialization complete.'); }).catch(error => { console.error('CRITICAL: Failed to load initial data.', error); alert('Error Crítico: No se pudieron cargar los datos del servidor.'); }); } document.addEventListener('DOMContentLoaded', initializeApp);