Refactor invoice status labels and update navigation toast functionality
This commit is contained in:
parent
4719bf61e3
commit
23c79cc7e4
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Tracker de cambios
|
||||||
const changeTracker = new FormChangeTracker();
|
const changeTracker = new FormChangeTracker();
|
||||||
let savedSuccessfully = false;
|
let savedSuccessfully = false;
|
||||||
|
|
||||||
// Formatear fecha para input type="date"
|
// Formatear fecha para input type="date"
|
||||||
const formatDateForInput = (dateString) => {
|
const formatDateForInput = (dateString) => {
|
||||||
if (!dateString) return '';
|
if (!dateString) return '';
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toISOString().split('T')[0];
|
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();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al eliminar línea:', error);
|
||||||
|
alert('Error al eliminar línea: ' + error.message);
|
||||||
|
|
||||||
// Recapturar estado inicial después de recargar
|
const btn = modal.querySelector(`.btn-delete-line[data-line-id="${lineId}"]`);
|
||||||
savedSuccessfully = false;
|
if (btn) {
|
||||||
} catch (error) {
|
btn.disabled = false;
|
||||||
console.error('Error al validar:', error);
|
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>';
|
||||||
alert('Error al validar la factura: ' + error.message);
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const validateBtn = modal.querySelector('.btn-validate');
|
// Manejar pago de factura
|
||||||
if (validateBtn) {
|
const handlePayment = async () => {
|
||||||
validateBtn.disabled = false;
|
const amountInput = modal.querySelector('#payment-amount');
|
||||||
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';
|
const dateInput = modal.querySelector('#payment-date');
|
||||||
}
|
const refInput = modal.querySelector('#payment-ref');
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mostrar/ocultar formulario de añadir línea
|
if (!amountInput || !dateInput) return;
|
||||||
const toggleAddLineForm = () => {
|
|
||||||
const form = modal.querySelector('.add-line-form');
|
|
||||||
if (form) {
|
|
||||||
const isVisible = form.style.display !== 'none';
|
|
||||||
form.style.display = isVisible ? 'none' : 'block';
|
|
||||||
|
|
||||||
if (!isVisible) {
|
const rawAmount = parseFloat(amountInput.value);
|
||||||
// Limpiar campos
|
const remainToPay = invoice.remainToPay;
|
||||||
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
|
// Si vacío o 0, pago total (enviar null)
|
||||||
const handleSaveLine = async () => {
|
let amount = null;
|
||||||
const description = modal.querySelector('#line-description').value.trim();
|
if (!isNaN(rawAmount) && rawAmount > 0) {
|
||||||
const quantity = parseFloat(modal.querySelector('#line-quantity').value);
|
if (rawAmount > remainToPay) {
|
||||||
const unitPrice = parseFloat(modal.querySelector('#line-price').value);
|
alert(`La cantidad no puede superar el pendiente de pago (${remainToPay.toFixed(2)} €)`);
|
||||||
const taxRate = parseFloat(modal.querySelector('#line-tax').value);
|
return;
|
||||||
|
}
|
||||||
|
amount = rawAmount;
|
||||||
|
}
|
||||||
|
|
||||||
if (!description) {
|
const paymentDate = dateInput.value;
|
||||||
alert('La descripción es obligatoria');
|
if (!paymentDate) {
|
||||||
return;
|
alert('La fecha de pago es obligatoria');
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!quantity || quantity <= 0) {
|
const paymentRef = refInput?.value?.trim() || undefined;
|
||||||
alert('La cantidad debe ser mayor que 0');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unitPrice < 0) {
|
try {
|
||||||
alert('El precio no puede ser negativo');
|
const payBtn = modal.querySelector('#btn-pay');
|
||||||
return;
|
payBtn.disabled = true;
|
||||||
}
|
payBtn.textContent = 'Procesando...';
|
||||||
|
|
||||||
if (taxRate < 0 || taxRate > 100) {
|
await addPayment(invoiceId, {
|
||||||
alert('El IVA debe estar entre 0 y 100');
|
amount: amount,
|
||||||
return;
|
paymentDate: paymentDate,
|
||||||
}
|
paymentModeId: 4,
|
||||||
|
closePaidInvoices: "yes",
|
||||||
|
accountId: 1,
|
||||||
|
numPayment: paymentRef
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
savedSuccessfully = true;
|
||||||
const saveBtn = modal.querySelector('.btn-save-line');
|
changeTracker.markAsSaved();
|
||||||
saveBtn.disabled = true;
|
|
||||||
saveBtn.textContent = 'Guardando...';
|
|
||||||
|
|
||||||
await addInvoiceLine(invoiceId, {
|
const displayAmount = amount ? `${amount.toFixed(2)} €` : `${remainToPay.toFixed(2)} € (total)`;
|
||||||
description,
|
alert(`Pago de ${displayAmount} registrado correctamente`);
|
||||||
quantity,
|
|
||||||
unitPrice,
|
|
||||||
taxRate
|
|
||||||
});
|
|
||||||
|
|
||||||
alert('Línea añadida correctamente');
|
if (onUpdate) onUpdate();
|
||||||
toggleAddLineForm();
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Recargar y recapturar estado inicial
|
// Cargar factura
|
||||||
await loadInvoice();
|
const loadInvoice = async () => {
|
||||||
changeTracker.markAsSaved();
|
try {
|
||||||
} catch (error) {
|
isLoading = true;
|
||||||
console.error('Error al añadir línea:', error);
|
renderContent();
|
||||||
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
|
invoice = await getInvoiceById(invoiceId);
|
||||||
const handleDeleteLine = async (lineId) => {
|
try {
|
||||||
if (!confirm('¿Estás seguro de que quieres eliminar esta línea?')) {
|
payments = await getPayments(invoiceId);
|
||||||
return;
|
} catch (e) {
|
||||||
}
|
payments = [];
|
||||||
|
}
|
||||||
|
isLoading = false;
|
||||||
|
renderContent();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar factura:', error);
|
||||||
|
isLoading = false;
|
||||||
|
invoice = null;
|
||||||
|
renderContent();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
// Inicializar modal
|
||||||
// Deshabilitar el botón mientras se elimina
|
modal.innerHTML = `
|
||||||
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>';
|
|
||||||
}
|
|
||||||
|
|
||||||
await deleteInvoiceLine(invoiceId, lineId);
|
|
||||||
|
|
||||||
if (onUpdate) onUpdate();
|
|
||||||
await loadInvoice();
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue