From 605e73e3044aa3329db51b0467259eff0e487a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81?= Date: Sat, 9 May 2026 00:59:54 +0200 Subject: [PATCH] Mejora simple de UI/UX --- src/components/InvoiceItem.js | 13 +- src/components/InvoiceModal.js | 72 +++++++---- src/components/Sidebar.js | 30 +++++ src/main.js | 1 + src/pages/Facturas.js | 224 ++++++++++++++++++++------------- src/services/toast.js | 54 ++++++++ src/styles/create-invoice.css | 38 +++++- src/styles/facturas.css | 95 ++++++++++++++ src/styles/modal.css | 97 ++++++++++++++ src/styles/sidebar.css | 86 +++++++++++++ src/styles/toast.css | 127 +++++++++++++++++++ 11 files changed, 719 insertions(+), 118 deletions(-) create mode 100644 src/services/toast.js create mode 100644 src/styles/toast.css diff --git a/src/components/InvoiceItem.js b/src/components/InvoiceItem.js index 6c0c272..4f480da 100644 --- a/src/components/InvoiceItem.js +++ b/src/components/InvoiceItem.js @@ -41,7 +41,9 @@ export function InvoiceItem(invoice, onView) { }; item.innerHTML = /*html*/` - ${invoice.number} + + + ${getStatusText(invoice.status)} @@ -61,13 +63,12 @@ export function InvoiceItem(invoice, onView) { `; + // Invoice number — also opens modal + item.querySelector('.btn-invoice-num').addEventListener('click', () => onView?.(invoice.id)); + // Event listener para el botón de ver const viewBtn = item.querySelector('.btn-view'); - viewBtn.addEventListener('click', () => { - if (onView) { - onView(invoice.id); - } - }); + viewBtn.addEventListener('click', () => onView?.(invoice.id)); return item; } diff --git a/src/components/InvoiceModal.js b/src/components/InvoiceModal.js index 6f76f20..c43e41b 100644 --- a/src/components/InvoiceModal.js +++ b/src/components/InvoiceModal.js @@ -1,5 +1,6 @@ import { getInvoiceById, updateInvoice, validateInvoice, addInvoiceLine, deleteInvoiceLine, addPayment, getPayments, downloadInvoicePdf } from '../services/invoices.js'; import { showConfirmExitModal, FormChangeTracker } from './ConfirmExitModal.js'; +import { toast } from '../services/toast.js'; export function InvoiceModal(invoiceId, onClose, onUpdate) { const modal = document.createElement('div'); @@ -475,12 +476,12 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { savedSuccessfully = true; changeTracker.markAsSaved(); - alert('Factura actualizada correctamente'); + toast.success('Factura actualizada correctamente'); if (onUpdate) onUpdate(); handleClose(); } catch (error) { console.error('Error al guardar:', error); - alert('Error al guardar la factura: ' + error.message); + toast.error('Error al guardar la factura: ' + error.message); } finally { const saveBtn = modal.querySelector('.btn-save'); if (saveBtn) { @@ -492,9 +493,13 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { // Manejar validación const handleValidate = async () => { - if (!confirm('¿Estás seguro de que quieres validar esta factura? No podrás editarla completamente después.')) { - return; - } + const ok = await showConfirmExitModal({ + title: 'Validar factura', + message: '¿Seguro que quieres validar esta factura? No podrás editarla completamente después.', + confirmText: 'Validar', + cancelText: 'Cancelar' + }); + if (!ok) return; try { const validateBtn = modal.querySelector('.btn-validate'); @@ -507,7 +512,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { savedSuccessfully = true; changeTracker.markAsSaved(); - alert('Factura validada correctamente'); + toast.success('Factura validada correctamente'); if (onUpdate) onUpdate(); // Recargar la factura para mostrar el nuevo estado @@ -517,7 +522,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { savedSuccessfully = false; } catch (error) { console.error('Error al validar:', error); - alert('Error al validar la factura: ' + error.message); + toast.error('Error al validar la factura: ' + error.message); const validateBtn = modal.querySelector('.btn-validate'); if (validateBtn) { @@ -530,7 +535,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { // Manejar descarga del PDF const handleDownloadPdf = async () => { if (!invoice?.number) { - alert('No se puede descargar la factura porque no tiene número.'); + toast.warning('No se puede descargar la factura porque no tiene número.'); return; } @@ -554,7 +559,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { URL.revokeObjectURL(blobUrl); } catch (error) { console.error('Error al descargar factura:', error); - alert('Error al descargar la factura: ' + error.message); + toast.error('Error al descargar la factura: ' + error.message); } finally { if (downloadBtn) { downloadBtn.disabled = false; @@ -566,7 +571,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { // Manejar vista previa del PDF const handlePreviewPdf = async () => { if (!invoice?.number) { - alert('No se puede previsualizar la factura porque no tiene número.'); + toast.warning('No se puede previsualizar la factura porque no tiene número.'); return; } @@ -575,7 +580,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { const previewWindow = window.open('', '_blank'); if (!previewWindow) { - alert('El navegador bloqueó la ventana emergente. Permite popups para ver la vista previa.'); + toast.warning('El navegador bloqueó la ventana emergente. Permite popups para ver la vista previa.'); return; } @@ -600,7 +605,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { } catch (error) { console.error('Error al previsualizar factura:', error); previewWindow.close(); - alert('Error al abrir la vista previa: ' + error.message); + toast.error('Error al abrir la vista previa: ' + error.message); } finally { if (previewBtn) { previewBtn.disabled = false; @@ -634,22 +639,22 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { const taxRate = parseFloat(modal.querySelector('#line-tax').value); if (!description) { - alert('La descripción es obligatoria'); + toast.warning('La descripción es obligatoria'); return; } if (!quantity || quantity <= 0) { - alert('La cantidad debe ser mayor que 0'); + toast.warning('La cantidad debe ser mayor que 0'); return; } if (unitPrice < 0) { - alert('El precio no puede ser negativo'); + toast.warning('El precio no puede ser negativo'); return; } if (taxRate < 0 || taxRate > 100) { - alert('El IVA debe estar entre 0 y 100'); + toast.warning('El IVA debe estar entre 0 y 100'); return; } @@ -665,7 +670,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { taxRate }); - alert('Línea añadida correctamente'); + toast.success('Línea añadida correctamente'); toggleAddLineForm(); // Recargar y recapturar estado inicial @@ -673,7 +678,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { changeTracker.markAsSaved(); } catch (error) { console.error('Error al añadir línea:', error); - alert('Error al añadir línea: ' + error.message); + toast.error('Error al añadir línea: ' + error.message); } finally { const saveBtn = modal.querySelector('.btn-save-line'); if (saveBtn) { @@ -685,9 +690,13 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { // Eliminar línea de factura const handleDeleteLine = async (lineId) => { - if (!confirm('¿Estás seguro de que quieres eliminar esta línea?')) { - return; - } + const ok = await showConfirmExitModal({ + title: 'Eliminar línea', + message: '¿Seguro que quieres eliminar esta línea de la factura?', + confirmText: 'Eliminar', + cancelText: 'Cancelar' + }); + if (!ok) return; try { // Deshabilitar el botón mientras se elimina @@ -704,7 +713,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { changeTracker.markAsSaved(); } catch (error) { console.error('Error al eliminar línea:', error); - alert('Error al eliminar línea: ' + error.message); + toast.error('Error al eliminar línea: ' + error.message); const btn = modal.querySelector(`.btn-delete-line[data-line-id="${lineId}"]`); if (btn) { @@ -729,7 +738,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { let amount = null; if (!isNaN(rawAmount) && rawAmount > 0) { if (rawAmount > remainToPay) { - alert(`La cantidad no puede superar el pendiente de pago (${remainToPay.toFixed(2)} €)`); + toast.warning(`La cantidad no puede superar el pendiente de pago (${remainToPay.toFixed(2)} €)`); return; } amount = rawAmount; @@ -737,7 +746,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { const paymentDate = dateInput.value; if (!paymentDate) { - alert('La fecha de pago es obligatoria'); + toast.warning('La fecha de pago es obligatoria'); return; } @@ -761,14 +770,14 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { changeTracker.markAsSaved(); const displayAmount = amount ? `${amount.toFixed(2)} €` : `${remainToPay.toFixed(2)} € (total)`; - alert(`Pago de ${displayAmount} registrado correctamente`); + toast.success(`Pago de ${displayAmount} registrado correctamente`); if (onUpdate) onUpdate(); await loadInvoice(); savedSuccessfully = false; } catch (error) { console.error('Error al registrar pago:', error); - alert('Error al registrar el pago: ' + error.message); + toast.error('Error al registrar el pago: ' + error.message); } finally { const payBtn = modal.querySelector('#btn-pay'); if (payBtn) { @@ -806,6 +815,17 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { `; + // Escape key closes modal + const handleEscapeKey = (e) => { + if (e.key !== 'Escape') return; + if (!modal.isConnected) { + document.removeEventListener('keydown', handleEscapeKey); + return; + } + handleClose(); + }; + document.addEventListener('keydown', handleEscapeKey); + // Cargar factura loadInvoice(); diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js index 7b87261..22abea8 100755 --- a/src/components/Sidebar.js +++ b/src/components/Sidebar.js @@ -1,3 +1,5 @@ +import { auth } from '../services/auth.js'; + export function createSidebar(pages, currentPage) { const sidebar = document.createElement('aside'); sidebar.className = 'sidebar'; @@ -48,6 +50,34 @@ export function createSidebar(pages, currentPage) { sidebar.appendChild(sidebarHeader); sidebar.appendChild(nav); + // User footer + const user = auth.getUser(); + const identifier = user?.identifier || user?.name || user?.login || '?'; + const initials = identifier.slice(0, 2).toUpperCase(); + + const sidebarFooter = document.createElement('div'); + sidebarFooter.className = 'sidebar-footer'; + sidebarFooter.innerHTML = /*html*/` + + + `; + + sidebarFooter.querySelector('.sidebar-logout-btn').addEventListener('click', () => { + auth.logout(); + window.location.hash = '#login'; + }); + + sidebar.appendChild(sidebarFooter); + const toggleBtn = sidebarHeader.querySelector('#sidebar-toggle'); toggleBtn.addEventListener('click', () => { if (window.matchMedia('(max-width: 768px)').matches) { diff --git a/src/main.js b/src/main.js index cc968f9..a818d32 100755 --- a/src/main.js +++ b/src/main.js @@ -7,6 +7,7 @@ import './styles/clients.css' import './styles/create-invoice.css' import './styles/settings.css' import './styles/voice-assistant.css' +import './styles/toast.css' import { initRouter } from './router.js' import { initSessionManager } from './services/session.js' import { initTheme } from './services/theme.js' diff --git a/src/pages/Facturas.js b/src/pages/Facturas.js index 2000588..7db6462 100755 --- a/src/pages/Facturas.js +++ b/src/pages/Facturas.js @@ -10,16 +10,20 @@ export function renderFacturasPage() { let currentPage = 1; let totalPages = 1; const pageSize = 20; - let allInvoices = []; // Almacenar todas las facturas - let filteredInvoices = []; // Facturas filtradas - let currentFilter = ''; // Filtro actual de estado - let searchTerm = ''; // Término de búsqueda actual + let allInvoices = []; + let filteredInvoices = []; + let currentFilter = ''; + let searchTerm = ''; + + // Estado de ordenación + let sortKey = null; + let sortDir = 'asc'; container.innerHTML = /*html*/`

Facturas

- +