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
+ —
+
+
+
+ Total facturado
+ —
+
+
+ Pagado
+ —
+
+
+ Pendiente
+ —
+
+
+