Refactor invoice status labels and update navigation toast functionality

This commit is contained in:
javiermengual 2026-02-11 13:56:28 +01:00
parent 4719bf61e3
commit 23c79cc7e4
6 changed files with 429 additions and 414 deletions

View File

@ -35,7 +35,7 @@ export function showBlockedNavigationToast(message = 'Guarda los cambios antes d
setTimeout(() => { setTimeout(() => {
toast.classList.remove('visible'); toast.classList.remove('visible');
toast.classList.add('hiding'); toast.classList.add('hiding');
setTimeout(() => { setTimeout(() => {
toast.remove(); toast.remove();
}, 300); }, 300);

View File

@ -15,8 +15,8 @@ export function ConfirmExitModal(options = {}) {
message = '¿Estás seguro de que quieres salir? Los cambios no guardados se perderán.', message = '¿Estás seguro de que quieres salir? Los cambios no guardados se perderán.',
confirmText = 'Salir sin guardar', confirmText = 'Salir sin guardar',
cancelText = 'Quedarse', cancelText = 'Quedarse',
onConfirm = () => {}, onConfirm = () => { },
onCancel = () => {} onCancel = () => { }
} = options; } = options;
const modal = document.createElement('div'); const modal = document.createElement('div');
@ -123,7 +123,7 @@ export class FormChangeTracker {
*/ */
getFormState(container) { getFormState(container) {
const state = {}; const state = {};
// Inputs de texto, date, number, etc. // Inputs de texto, date, number, etc.
container.querySelectorAll('input:not([type="button"]):not([type="submit"])').forEach(input => { container.querySelectorAll('input:not([type="button"]):not([type="submit"])').forEach(input => {
const key = input.name || input.id || input.className; const key = input.name || input.id || input.className;
@ -162,9 +162,9 @@ export class FormChangeTracker {
*/ */
checkForChanges(container) { checkForChanges(container) {
if (!this.initialState) return false; if (!this.initialState) return false;
const currentState = this.getFormState(container); const currentState = this.getFormState(container);
// Comparar estados // Comparar estados
const initialKeys = Object.keys(this.initialState); const initialKeys = Object.keys(this.initialState);
const currentKeys = Object.keys(currentState); const currentKeys = Object.keys(currentState);

View File

@ -20,7 +20,7 @@ export function InvoiceItem(invoice, onView) {
const getStatusClass = (status) => { const getStatusClass = (status) => {
const statusMap = { const statusMap = {
'draft': 'status-draft', 'draft': 'status-draft',
'validated': 'status-validated', 'validated': 'status-unpaid',
'paid': 'status-paid', 'paid': 'status-paid',
'unpaid': 'status-unpaid', 'unpaid': 'status-unpaid',
'canceled': 'status-canceled' 'canceled': 'status-canceled'
@ -32,9 +32,9 @@ export function InvoiceItem(invoice, onView) {
const getStatusText = (status) => { const getStatusText = (status) => {
const statusTextMap = { const statusTextMap = {
'draft': 'Borrador', 'draft': 'Borrador',
'validated': 'Validada', 'validated': 'Pte. Pago',
'paid': 'Pagada', 'paid': 'Pagada',
'unpaid': 'Impagada', 'unpaid': 'Pte. Pago',
'canceled': 'Cancelada' 'canceled': 'Cancelada'
}; };
return statusTextMap[status] || status; return statusTextMap[status] || status;

View File

@ -2,57 +2,57 @@ import { getInvoiceById, updateInvoice, validateInvoice, addInvoiceLine, deleteI
import { showConfirmExitModal, FormChangeTracker } from './ConfirmExitModal.js'; import { showConfirmExitModal, FormChangeTracker } from './ConfirmExitModal.js';
export function InvoiceModal(invoiceId, onClose, onUpdate) { export function InvoiceModal(invoiceId, onClose, onUpdate) {
const modal = document.createElement('div'); const modal = document.createElement('div');
modal.className = 'modal-overlay'; modal.className = 'modal-overlay';
let invoice = null; let invoice = null;
let payments = []; let payments = [];
let isLoading = true; let isLoading = true;
// Tracker de cambios
const changeTracker = new FormChangeTracker();
let savedSuccessfully = false;
// Formatear fecha para input type="date" // Tracker de cambios
const formatDateForInput = (dateString) => { const changeTracker = new FormChangeTracker();
if (!dateString) return ''; let savedSuccessfully = false;
const date = new Date(dateString);
return date.toISOString().split('T')[0]; // Formatear fecha para input type="date"
const formatDateForInput = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toISOString().split('T')[0];
};
// Formatear fecha para mostrar
const formatDate = (dateString) => {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('es-ES');
};
// Formatear moneda
const formatCurrency = (amount) => {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: 'EUR'
}).format(amount || 0);
};
// Obtener texto de estado
const getStatusText = (status) => {
const statusTextMap = {
'draft': 'Borrador',
'validated': 'Pte. Pago',
'paid': 'Pagada',
'unpaid': 'Pte. Pago',
'canceled': 'Cancelada'
}; };
return statusTextMap[status] || status;
};
// Formatear fecha para mostrar // Renderizar contenido del modal
const formatDate = (dateString) => { const renderContent = () => {
if (!dateString) return '-'; const modalContent = modal.querySelector('.modal-content');
const date = new Date(dateString);
return date.toLocaleDateString('es-ES');
};
// Formatear moneda if (isLoading) {
const formatCurrency = (amount) => { modalContent.innerHTML = `
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: 'EUR'
}).format(amount || 0);
};
// Obtener texto de estado
const getStatusText = (status) => {
const statusTextMap = {
'draft': 'Borrador',
'validated': 'Validada',
'paid': 'Pagada',
'unpaid': 'Impagada',
'canceled': 'Cancelada'
};
return statusTextMap[status] || status;
};
// Renderizar contenido del modal
const renderContent = () => {
const modalContent = modal.querySelector('.modal-content');
if (isLoading) {
modalContent.innerHTML = `
<div class="modal-header"> <div class="modal-header">
<h2>Cargando...</h2> <h2>Cargando...</h2>
<button class="btn-close">×</button> <button class="btn-close">×</button>
@ -61,11 +61,11 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
<div class="loading">Cargando detalles de la factura...</div> <div class="loading">Cargando detalles de la factura...</div>
</div> </div>
`; `;
return; return;
} }
if (!invoice) { if (!invoice) {
modalContent.innerHTML = ` modalContent.innerHTML = `
<div class="modal-header"> <div class="modal-header">
<h2>Error</h2> <h2>Error</h2>
<button class="btn-close">×</button> <button class="btn-close">×</button>
@ -74,13 +74,13 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
<div class="error">No se pudo cargar la factura</div> <div class="error">No se pudo cargar la factura</div>
</div> </div>
`; `;
return; return;
} }
const isDraft = invoice.status === 'draft'; const isDraft = invoice.status === 'draft';
const canEdit = isDraft || invoice.status === 'validated' || invoice.status === 'unpaid'; const canEdit = isDraft || invoice.status === 'validated' || invoice.status === 'unpaid';
modalContent.innerHTML = ` modalContent.innerHTML = `
<div class="modal-header"> <div class="modal-header">
<div class="modal-title"> <div class="modal-title">
<h2>${invoice.number || 'Nueva Factura'}</h2> <h2>${invoice.number || 'Nueva Factura'}</h2>
@ -144,14 +144,14 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
<div class="form-row"> <div class="form-row">
<div class="form-group full-width"> <div class="form-group full-width">
<label>Nota Pública</label> <label>Nota Pública</label>
<textarea name="note_public" rows="3">${invoice.note_public || ''}</textarea> <textarea name="note_public" rows="3">${invoice.notePublic || ''}</textarea>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group full-width"> <div class="form-group full-width">
<label>Nota Privada</label> <label>Nota Privada</label>
<textarea name="note_private" rows="3">${invoice.note_private || ''}</textarea> <textarea name="note_private" rows="3">${invoice.notePrivate || ''}</textarea>
</div> </div>
</div> </div>
</div> </div>
@ -208,7 +208,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
</thead> </thead>
<tbody> <tbody>
${invoice.lines && invoice.lines.length > 0 ${invoice.lines && invoice.lines.length > 0
? invoice.lines.map(line => ` ? invoice.lines.map(line => `
<tr data-line-id="${line.id}"> <tr data-line-id="${line.id}">
<td>${line.description || ''}</td> <td>${line.description || ''}</td>
<td>${line.quantity || 0}</td> <td>${line.quantity || 0}</td>
@ -227,8 +227,8 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
` : ''} ` : ''}
</tr> </tr>
`).join('') `).join('')
: `<tr><td colspan="${isDraft ? 6 : 5}" class="no-lines">No hay líneas en esta factura</td></tr>` : `<tr><td colspan="${isDraft ? 6 : 5}" class="no-lines">No hay líneas en esta factura</td></tr>`
} }
</tbody> </tbody>
</table> </table>
</div> </div>
@ -328,389 +328,389 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
</div> </div>
`; `;
attachEventListeners(); attachEventListeners();
// Capturar estado inicial después de renderizar // Capturar estado inicial después de renderizar
setTimeout(() => { setTimeout(() => {
const modalContent = modal.querySelector('.modal-content'); const modalContent = modal.querySelector('.modal-content');
if (modalContent && !isLoading) { if (modalContent && !isLoading) {
changeTracker.captureInitialState(modalContent); changeTracker.captureInitialState(modalContent);
changeTracker.setupAutoTracking(modalContent); changeTracker.setupAutoTracking(modalContent);
} }
}, 100); }, 100);
};
// Adjuntar event listeners
const attachEventListeners = () => {
// Botón cerrar (X) - muestra modal si hay cambios
const closeBtn = modal.querySelector('.btn-close');
closeBtn?.addEventListener('click', handleClose);
// Botón cancelar modal - muestra modal si hay cambios
const cancelBtn = modal.querySelector('.btn-cancel-modal');
cancelBtn?.addEventListener('click', handleClose);
// Botón guardar
const saveBtn = modal.querySelector('.btn-save');
saveBtn?.addEventListener('click', handleSave);
// Botón validar
const validateBtn = modal.querySelector('.btn-validate');
validateBtn?.addEventListener('click', handleValidate);
// Botón añadir línea
const addLineBtn = modal.querySelector('.btn-add-line');
addLineBtn?.addEventListener('click', toggleAddLineForm);
// Botón guardar línea
const saveLineBtn = modal.querySelector('.btn-save-line');
saveLineBtn?.addEventListener('click', handleSaveLine);
// Botón cancelar línea
const cancelLineBtn = modal.querySelector('.btn-cancel-line');
cancelLineBtn?.addEventListener('click', () => {
const form = modal.querySelector('.add-line-form');
if (form) form.style.display = 'none';
});
// Botones eliminar línea
const deleteLineBtns = modal.querySelectorAll('.btn-delete-line');
deleteLineBtns.forEach(btn => {
btn.addEventListener('click', () => {
const lineId = parseInt(btn.dataset.lineId);
handleDeleteLine(lineId);
});
});
// Botón registrar pago
const payBtn = modal.querySelector('#btn-pay');
payBtn?.addEventListener('click', handlePayment);
// Click fuera del modal - muestra modal si hay cambios
modal.addEventListener('click', (e) => {
if (e.target === modal) {
handleClose();
}
});
};
// Manejar cierre
const handleClose = async () => {
// Si ya se guardó exitosamente, cerrar directamente
if (savedSuccessfully) {
changeTracker.cleanup();
modal.remove();
if (onClose) onClose();
return;
}
// Verificar si hay cambios sin guardar
const modalContent = modal.querySelector('.modal-content');
if (modalContent) {
changeTracker.checkForChanges(modalContent);
}
if (changeTracker.hasChanges) {
// Mostrar modal de confirmación
const confirmed = await showConfirmExitModal({
title: 'Cambios sin guardar',
message: 'Tienes cambios sin guardar en la factura. ¿Seguro que quieres cerrar?',
confirmText: 'Cerrar sin guardar',
cancelText: 'Seguir editando'
});
if (!confirmed) {
return; // No cerrar el modal
}
}
changeTracker.cleanup();
modal.remove();
if (onClose) onClose();
};
// Manejar guardado
const handleSave = async () => {
const form = modal.querySelector('#invoice-form');
const formData = new FormData(form);
// La API espera camelCase en el PUT (notePublic/notePrivate)
const data = {
number: formData.get('number') || undefined,
expireDate: formData.get('expireDate') || undefined,
notePublic: formData.get('note_public') || undefined,
notePrivate: formData.get('note_private') || undefined
}; };
// Adjuntar event listeners // Filtrar valores undefined
const attachEventListeners = () => { Object.keys(data).forEach(key => {
// Botón cerrar (X) - muestra modal si hay cambios if (data[key] === undefined || data[key] === '') {
const closeBtn = modal.querySelector('.btn-close'); delete data[key];
closeBtn?.addEventListener('click', handleClose); }
});
// Botón cancelar modal - muestra modal si hay cambios try {
const cancelBtn = modal.querySelector('.btn-cancel-modal'); const saveBtn = modal.querySelector('.btn-save');
cancelBtn?.addEventListener('click', handleClose); saveBtn.disabled = true;
saveBtn.textContent = 'Guardando...';
// Botón guardar await updateInvoice(invoiceId, data);
const saveBtn = modal.querySelector('.btn-save');
saveBtn?.addEventListener('click', handleSave);
// Botón validar // Marcar como guardado exitosamente
const validateBtn = modal.querySelector('.btn-validate'); savedSuccessfully = true;
validateBtn?.addEventListener('click', handleValidate); changeTracker.markAsSaved();
// Botón añadir línea alert('Factura actualizada correctamente');
const addLineBtn = modal.querySelector('.btn-add-line'); if (onUpdate) onUpdate();
addLineBtn?.addEventListener('click', toggleAddLineForm); handleClose();
} catch (error) {
console.error('Error al guardar:', error);
alert('Error al guardar la factura: ' + error.message);
} finally {
const saveBtn = modal.querySelector('.btn-save');
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.textContent = 'Guardar Cambios';
}
}
};
// Botón guardar línea // Manejar validación
const saveLineBtn = modal.querySelector('.btn-save-line'); const handleValidate = async () => {
saveLineBtn?.addEventListener('click', handleSaveLine); if (!confirm('¿Estás seguro de que quieres validar esta factura? No podrás editarla completamente después.')) {
return;
}
// Botón cancelar línea try {
const cancelLineBtn = modal.querySelector('.btn-cancel-line'); const validateBtn = modal.querySelector('.btn-validate');
cancelLineBtn?.addEventListener('click', () => { validateBtn.disabled = true;
const form = modal.querySelector('.add-line-form'); validateBtn.textContent = 'Validando...';
if (form) form.style.display = 'none';
});
// Botones eliminar línea await validateInvoice(invoiceId);
const deleteLineBtns = modal.querySelectorAll('.btn-delete-line');
deleteLineBtns.forEach(btn => {
btn.addEventListener('click', () => {
const lineId = parseInt(btn.dataset.lineId);
handleDeleteLine(lineId);
});
});
// Botón registrar pago // Marcar como guardado para no mostrar el modal de confirmación
const payBtn = modal.querySelector('#btn-pay'); savedSuccessfully = true;
payBtn?.addEventListener('click', handlePayment); changeTracker.markAsSaved();
// Click fuera del modal - muestra modal si hay cambios alert('Factura validada correctamente');
modal.addEventListener('click', (e) => { if (onUpdate) onUpdate();
if (e.target === modal) {
handleClose();
}
});
};
// Manejar cierre // Recargar la factura para mostrar el nuevo estado
const handleClose = async () => { await loadInvoice();
// Si ya se guardó exitosamente, cerrar directamente
if (savedSuccessfully) {
changeTracker.cleanup();
modal.remove();
if (onClose) onClose();
return;
}
// Verificar si hay cambios sin guardar // Recapturar estado inicial después de recargar
const modalContent = modal.querySelector('.modal-content'); savedSuccessfully = false;
if (modalContent) { } catch (error) {
changeTracker.checkForChanges(modalContent); console.error('Error al validar:', error);
} alert('Error al validar la factura: ' + error.message);
if (changeTracker.hasChanges) { const validateBtn = modal.querySelector('.btn-validate');
// Mostrar modal de confirmación if (validateBtn) {
const confirmed = await showConfirmExitModal({ validateBtn.disabled = false;
title: 'Cambios sin guardar', validateBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 4px;"><polyline points="20 6 9 17 4 12"/></svg>Validar Factura';
message: 'Tienes cambios sin guardar en la factura. ¿Seguro que quieres cerrar?', }
confirmText: 'Cerrar sin guardar', }
cancelText: 'Seguir editando' };
});
if (!confirmed) { // Mostrar/ocultar formulario de añadir línea
return; // No cerrar el modal const toggleAddLineForm = () => {
} const form = modal.querySelector('.add-line-form');
} if (form) {
const isVisible = form.style.display !== 'none';
form.style.display = isVisible ? 'none' : 'block';
changeTracker.cleanup(); if (!isVisible) {
modal.remove(); // Limpiar campos
if (onClose) onClose(); modal.querySelector('#line-description').value = '';
}; modal.querySelector('#line-quantity').value = '1';
modal.querySelector('#line-price').value = '0';
modal.querySelector('#line-tax').value = '21';
}
}
};
// Manejar guardado // Guardar nueva línea
const handleSave = async () => { const handleSaveLine = async () => {
const form = modal.querySelector('#invoice-form'); const description = modal.querySelector('#line-description').value.trim();
const formData = new FormData(form); const quantity = parseFloat(modal.querySelector('#line-quantity').value);
const unitPrice = parseFloat(modal.querySelector('#line-price').value);
const taxRate = parseFloat(modal.querySelector('#line-tax').value);
// La API espera camelCase en el PUT (notePublic/notePrivate) if (!description) {
const data = { alert('La descripción es obligatoria');
number: formData.get('number') || undefined, return;
expireDate: formData.get('expireDate') || undefined, }
notePublic: formData.get('note_public') || undefined,
notePrivate: formData.get('note_private') || undefined
};
// Filtrar valores undefined if (!quantity || quantity <= 0) {
Object.keys(data).forEach(key => { alert('La cantidad debe ser mayor que 0');
if (data[key] === undefined || data[key] === '') { return;
delete data[key]; }
}
});
try { if (unitPrice < 0) {
const saveBtn = modal.querySelector('.btn-save'); alert('El precio no puede ser negativo');
saveBtn.disabled = true; return;
saveBtn.textContent = 'Guardando...'; }
await updateInvoice(invoiceId, data); if (taxRate < 0 || taxRate > 100) {
alert('El IVA debe estar entre 0 y 100');
return;
}
// Marcar como guardado exitosamente try {
savedSuccessfully = true; const saveBtn = modal.querySelector('.btn-save-line');
changeTracker.markAsSaved(); saveBtn.disabled = true;
saveBtn.textContent = 'Guardando...';
alert('Factura actualizada correctamente'); await addInvoiceLine(invoiceId, {
if (onUpdate) onUpdate(); description,
handleClose(); quantity,
} catch (error) { unitPrice,
console.error('Error al guardar:', error); taxRate
alert('Error al guardar la factura: ' + error.message); });
} finally {
const saveBtn = modal.querySelector('.btn-save');
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.textContent = 'Guardar Cambios';
}
}
};
// Manejar validación alert('Línea añadida correctamente');
const handleValidate = async () => { toggleAddLineForm();
if (!confirm('¿Estás seguro de que quieres validar esta factura? No podrás editarla completamente después.')) {
return;
}
try { // Recargar y recapturar estado inicial
const validateBtn = modal.querySelector('.btn-validate'); await loadInvoice();
validateBtn.disabled = true; changeTracker.markAsSaved();
validateBtn.textContent = 'Validando...'; } catch (error) {
console.error('Error al añadir línea:', error);
alert('Error al añadir línea: ' + error.message);
} finally {
const saveBtn = modal.querySelector('.btn-save-line');
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.textContent = 'Guardar Línea';
}
}
};
await validateInvoice(invoiceId); // Eliminar línea de factura
const handleDeleteLine = async (lineId) => {
if (!confirm('¿Estás seguro de que quieres eliminar esta línea?')) {
return;
}
// Marcar como guardado para no mostrar el modal de confirmación try {
savedSuccessfully = true; // Deshabilitar el botón mientras se elimina
changeTracker.markAsSaved(); const btn = modal.querySelector(`.btn-delete-line[data-line-id="${lineId}"]`);
if (btn) {
btn.disabled = true;
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="spin"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>';
}
alert('Factura validada correctamente'); await deleteInvoiceLine(invoiceId, lineId);
if (onUpdate) onUpdate();
// Recargar la factura para mostrar el nuevo estado if (onUpdate) onUpdate();
await loadInvoice(); await loadInvoice();
changeTracker.markAsSaved();
// Recapturar estado inicial después de recargar } catch (error) {
savedSuccessfully = false; console.error('Error al eliminar línea:', error);
} catch (error) { alert('Error al eliminar línea: ' + error.message);
console.error('Error al validar:', error);
alert('Error al validar la factura: ' + error.message);
const validateBtn = modal.querySelector('.btn-validate'); const btn = modal.querySelector(`.btn-delete-line[data-line-id="${lineId}"]`);
if (validateBtn) { if (btn) {
validateBtn.disabled = false; btn.disabled = false;
validateBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 4px;"><polyline points="20 6 9 17 4 12"/></svg>Validar Factura'; btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
} }
} }
}; };
// Mostrar/ocultar formulario de añadir línea // Manejar pago de factura
const toggleAddLineForm = () => { const handlePayment = async () => {
const form = modal.querySelector('.add-line-form'); const amountInput = modal.querySelector('#payment-amount');
if (form) { const dateInput = modal.querySelector('#payment-date');
const isVisible = form.style.display !== 'none'; const refInput = modal.querySelector('#payment-ref');
form.style.display = isVisible ? 'none' : 'block';
if (!isVisible) { if (!amountInput || !dateInput) return;
// Limpiar campos
modal.querySelector('#line-description').value = '';
modal.querySelector('#line-quantity').value = '1';
modal.querySelector('#line-price').value = '0';
modal.querySelector('#line-tax').value = '21';
}
}
};
// Guardar nueva línea const rawAmount = parseFloat(amountInput.value);
const handleSaveLine = async () => { const remainToPay = invoice.remainToPay;
const description = modal.querySelector('#line-description').value.trim();
const quantity = parseFloat(modal.querySelector('#line-quantity').value);
const unitPrice = parseFloat(modal.querySelector('#line-price').value);
const taxRate = parseFloat(modal.querySelector('#line-tax').value);
if (!description) { // Si vacío o 0, pago total (enviar null)
alert('La descripción es obligatoria'); let amount = null;
return; if (!isNaN(rawAmount) && rawAmount > 0) {
} if (rawAmount > remainToPay) {
alert(`La cantidad no puede superar el pendiente de pago (${remainToPay.toFixed(2)} €)`);
return;
}
amount = rawAmount;
}
if (!quantity || quantity <= 0) { const paymentDate = dateInput.value;
alert('La cantidad debe ser mayor que 0'); if (!paymentDate) {
return; alert('La fecha de pago es obligatoria');
} return;
}
if (unitPrice < 0) { const paymentRef = refInput?.value?.trim() || undefined;
alert('El precio no puede ser negativo');
return;
}
if (taxRate < 0 || taxRate > 100) { try {
alert('El IVA debe estar entre 0 y 100'); const payBtn = modal.querySelector('#btn-pay');
return; payBtn.disabled = true;
} payBtn.textContent = 'Procesando...';
try { await addPayment(invoiceId, {
const saveBtn = modal.querySelector('.btn-save-line'); amount: amount,
saveBtn.disabled = true; paymentDate: paymentDate,
saveBtn.textContent = 'Guardando...'; paymentModeId: 4,
closePaidInvoices: "yes",
accountId: 1,
numPayment: paymentRef
});
await addInvoiceLine(invoiceId, { savedSuccessfully = true;
description, changeTracker.markAsSaved();
quantity,
unitPrice,
taxRate
});
alert('Línea añadida correctamente'); const displayAmount = amount ? `${amount.toFixed(2)} €` : `${remainToPay.toFixed(2)} € (total)`;
toggleAddLineForm(); alert(`Pago de ${displayAmount} registrado correctamente`);
// Recargar y recapturar estado inicial
await loadInvoice();
changeTracker.markAsSaved();
} catch (error) {
console.error('Error al añadir línea:', error);
alert('Error al añadir línea: ' + error.message);
} finally {
const saveBtn = modal.querySelector('.btn-save-line');
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.textContent = 'Guardar Línea';
}
}
};
// Eliminar línea de factura if (onUpdate) onUpdate();
const handleDeleteLine = async (lineId) => { await loadInvoice();
if (!confirm('¿Estás seguro de que quieres eliminar esta línea?')) { savedSuccessfully = false;
return; } catch (error) {
} console.error('Error al registrar pago:', error);
alert('Error al registrar el pago: ' + error.message);
} finally {
const payBtn = modal.querySelector('#btn-pay');
if (payBtn) {
payBtn.disabled = false;
payBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 4px;"><path d="M12 1v22M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>Registrar Pago';
}
}
};
try { // Cargar factura
// Deshabilitar el botón mientras se elimina const loadInvoice = async () => {
const btn = modal.querySelector(`.btn-delete-line[data-line-id="${lineId}"]`); try {
if (btn) { isLoading = true;
btn.disabled = true; renderContent();
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="spin"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>';
}
await deleteInvoiceLine(invoiceId, lineId); invoice = await getInvoiceById(invoiceId);
try {
payments = await getPayments(invoiceId);
} catch (e) {
payments = [];
}
isLoading = false;
renderContent();
} catch (error) {
console.error('Error al cargar factura:', error);
isLoading = false;
invoice = null;
renderContent();
}
};
if (onUpdate) onUpdate(); // Inicializar modal
await loadInvoice(); modal.innerHTML = `
changeTracker.markAsSaved();
} catch (error) {
console.error('Error al eliminar línea:', error);
alert('Error al eliminar línea: ' + error.message);
const btn = modal.querySelector(`.btn-delete-line[data-line-id="${lineId}"]`);
if (btn) {
btn.disabled = false;
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
}
}
};
// Manejar pago de factura
const handlePayment = async () => {
const amountInput = modal.querySelector('#payment-amount');
const dateInput = modal.querySelector('#payment-date');
const refInput = modal.querySelector('#payment-ref');
if (!amountInput || !dateInput) return;
const rawAmount = parseFloat(amountInput.value);
const remainToPay = invoice.remainToPay;
// Si vacío o 0, pago total (enviar null)
let amount = null;
if (!isNaN(rawAmount) && rawAmount > 0) {
if (rawAmount > remainToPay) {
alert(`La cantidad no puede superar el pendiente de pago (${remainToPay.toFixed(2)} €)`);
return;
}
amount = rawAmount;
}
const paymentDate = dateInput.value;
if (!paymentDate) {
alert('La fecha de pago es obligatoria');
return;
}
const paymentRef = refInput?.value?.trim() || undefined;
try {
const payBtn = modal.querySelector('#btn-pay');
payBtn.disabled = true;
payBtn.textContent = 'Procesando...';
await addPayment(invoiceId, {
amount: amount,
paymentDate: paymentDate,
paymentModeId: 4,
closePaidInvoices: "yes",
accountId: 1,
numPayment: paymentRef
});
savedSuccessfully = true;
changeTracker.markAsSaved();
const displayAmount = amount ? `${amount.toFixed(2)} €` : `${remainToPay.toFixed(2)} € (total)`;
alert(`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);
} finally {
const payBtn = modal.querySelector('#btn-pay');
if (payBtn) {
payBtn.disabled = false;
payBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 4px;"><path d="M12 1v22M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>Registrar Pago';
}
}
};
// Cargar factura
const loadInvoice = async () => {
try {
isLoading = true;
renderContent();
invoice = await getInvoiceById(invoiceId);
try {
payments = await getPayments(invoiceId);
} catch (e) {
payments = [];
}
isLoading = false;
renderContent();
} catch (error) {
console.error('Error al cargar factura:', error);
isLoading = false;
invoice = null;
renderContent();
}
};
// Inicializar modal
modal.innerHTML = `
<div class="modal-content invoice-modal"> <div class="modal-content invoice-modal">
</div> </div>
`; `;
// Cargar factura // Cargar factura
loadInvoice(); loadInvoice();
return modal; return modal;
} }

View File

@ -24,9 +24,8 @@ export function renderFacturasPage() {
<select class="filter-status"> <select class="filter-status">
<option value="">Todos los estados</option> <option value="">Todos los estados</option>
<option value="draft">Borrador</option> <option value="draft">Borrador</option>
<option value="unpaid">Pte. Pago</option>
<option value="paid">Pagada</option> <option value="paid">Pagada</option>
<option value="unpaid">Impagada</option>
<option value="canceled">Cancelada</option>
</select> </select>
<button class="btn-new-invoice">+ Nueva Factura</button> <button class="btn-new-invoice">+ Nueva Factura</button>
</div> </div>

View File

@ -95,7 +95,9 @@
/* ============================================ /* ============================================
Reset & Base Reset & Base
============================================ */ ============================================ */
*, *::before, *::after { *,
*::before,
*::after {
box-sizing: border-box; box-sizing: border-box;
} }
@ -115,7 +117,12 @@ a:hover {
color: var(--primary-hover); color: var(--primary-hover);
} }
h1, h2, h3, h4, h5, h6 { h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0; margin: 0;
font-weight: 600; font-weight: 600;
line-height: 1.3; line-height: 1.3;
@ -123,9 +130,17 @@ h1, h2, h3, h4, h5, h6 {
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }
h1 { font-size: 1.5rem; } h1 {
h2 { font-size: 1.25rem; } font-size: 1.5rem;
h3 { font-size: 1rem; } }
h2 {
font-size: 1.25rem;
}
h3 {
font-size: 1rem;
}
#app { #app {
width: 100%; width: 100%;
@ -406,7 +421,7 @@ button:disabled {
min-width: 0; min-width: 0;
} }
.main-content > div { .main-content>div {
max-width: 1280px; max-width: 1280px;
margin: 0 auto; margin: 0 auto;
padding: var(--space-6); padding: var(--space-6);
@ -1622,6 +1637,7 @@ button:disabled {
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
.clientes-page, .clientes-page,
.facturas-page { .facturas-page {
padding: var(--space-4); padding: var(--space-4);
@ -1693,4 +1709,4 @@ button:disabled {
.dashboard-card-desc { .dashboard-card-desc {
font-size: 13px; font-size: 13px;
color: var(--text-secondary); color: var(--text-secondary);
} }