doli-front/src/components/InvoiceModal.js

466 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { getInvoiceById, updateInvoice, validateInvoice, addInvoiceLine } from '../services/invoices.js';
export function InvoiceModal(invoiceId, onClose, onUpdate) {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
let invoice = null;
let isLoading = true;
// 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': '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">
<h2>Cargando...</h2>
<button class="btn-close">×</button>
</div>
<div class="modal-body">
<div class="loading">Cargando detalles de la factura...</div>
</div>
`;
return;
}
if (!invoice) {
modalContent.innerHTML = `
<div class="modal-header">
<h2>Error</h2>
<button class="btn-close">×</button>
</div>
<div class="modal-body">
<div class="error">No se pudo cargar la factura</div>
</div>
`;
return;
}
const isDraft = invoice.status === 'draft';
const canEdit = isDraft || invoice.status === 'validated' || invoice.status === 'unpaid';
modalContent.innerHTML = `
<div class="modal-header">
<div class="modal-title">
<h2>${invoice.number || 'Nueva Factura'}</h2>
<span class="status-badge status-${invoice.status}">
${getStatusText(invoice.status)}
</span>
</div>
<button class="btn-close">×</button>
</div>
<div class="modal-body">
<form id="invoice-form">
<!-- Información básica -->
<div class="form-section">
<h3>Información de la Factura</h3>
<div class="form-row">
<div class="form-group">
<label>Número de Factura</label>
<input
type="text"
name="number"
value="${invoice.number || ''}"
disabled
/>
</div>
<div class="form-group">
<label>Fecha</label>
<input
type="date"
name="date"
value="${formatDateForInput(invoice.date)}"
disabled
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Fecha de Vencimiento</label>
<input
type="date"
name="expireDate"
value="${formatDateForInput(invoice.expireDate)}"
${!canEdit ? 'disabled' : ''}
/>
</div>
<div class="form-group">
<label>Cliente ID</label>
<input
type="text"
name="clientId"
value="${invoice.clientId || ''}"
disabled
/>
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label>Nota Pública</label>
<textarea name="note_public" rows="3">${invoice.note_public || ''}</textarea>
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label>Nota Privada</label>
<textarea name="note_private" rows="3">${invoice.note_private || ''}</textarea>
</div>
</div>
</div>
<!-- Líneas de factura -->
<div class="form-section">
<div class="section-header">
<h3>Líneas de Factura</h3>
${isDraft ? '<button type="button" class="btn-add-line btn-small">+ Añadir Línea</button>' : ''}
</div>
${isDraft ? `
<div class="add-line-form" style="display: none;">
<div class="form-row">
<div class="form-group full-width">
<label>Descripción</label>
<input type="text" id="line-description" placeholder="Descripción del producto/servicio" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Cantidad</label>
<input type="number" id="line-quantity" value="1" min="0.01" step="0.01" />
</div>
<div class="form-group">
<label>Precio Unitario (€)</label>
<input type="number" id="line-price" value="0" min="0" step="0.01" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>IVA (%)</label>
<input type="number" id="line-tax" value="21" min="0" max="100" step="0.01" />
</div>
<div class="form-group" style="display: flex; align-items: flex-end; gap: 0.5rem;">
<button type="button" class="btn-save-line btn-small btn-success">Guardar Línea</button>
<button type="button" class="btn-cancel-line btn-small btn-cancel">Cancelar</button>
</div>
</div>
</div>
` : ''}
<div class="invoice-lines">
<table class="lines-table">
<thead>
<tr>
<th>Descripción</th>
<th>Cantidad</th>
<th>Precio Unitario</th>
<th>IVA %</th>
<th>Total</th>
</tr>
</thead>
<tbody>
${invoice.lines && invoice.lines.length > 0
? invoice.lines.map(line => `
<tr>
<td>${line.description || ''}</td>
<td>${line.quantity || 0}</td>
<td>${formatCurrency(line.unitPrice)}</td>
<td>${line.taxRate || 0}%</td>
<td>${formatCurrency(line.total)}</td>
</tr>
`).join('')
: '<tr><td colspan="5" class="no-lines">No hay líneas en esta factura</td></tr>'
}
</tbody>
</table>
</div>
<!-- Totales -->
<div class="invoice-totals">
<div class="total-row">
<span>Total:</span>
<strong>${formatCurrency(invoice.total)}</strong>
</div>
<div class="total-row pending">
<span>Pendiente:</span>
<strong>${formatCurrency(invoice.remainToPay)}</strong>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<div class="footer-actions-left">
${isDraft ? '<button type="button" class="btn-validate btn-success"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 4px;"><polyline points="20 6 9 17 4 12"/></svg>Validar Factura</button>' : ''}
</div>
<div class="footer-actions-right">
<button type="button" class="btn-cancel-modal">Cancelar</button>
${canEdit ? '<button type="button" class="btn-save btn-primary">Guardar Cambios</button>' : ''}
</div>
</div>
`;
attachEventListeners();
};
// Adjuntar event listeners
const attachEventListeners = () => {
// Botón cerrar
const closeBtn = modal.querySelector('.btn-close');
closeBtn?.addEventListener('click', handleClose);
// Botón cancelar modal
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';
});
// Click fuera del modal
modal.addEventListener('click', (e) => {
if (e.target === modal) {
handleClose();
}
});
};
// Manejar cierre
const handleClose = () => {
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
};
// Filtrar valores undefined
Object.keys(data).forEach(key => {
if (data[key] === undefined || data[key] === '') {
delete data[key];
}
});
try {
const saveBtn = modal.querySelector('.btn-save');
saveBtn.disabled = true;
saveBtn.textContent = 'Guardando...';
await updateInvoice(invoiceId, data);
alert('Factura actualizada correctamente');
if (onUpdate) onUpdate();
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';
}
}
};
// 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;
}
try {
const validateBtn = modal.querySelector('.btn-validate');
validateBtn.disabled = true;
validateBtn.textContent = 'Validando...';
await validateInvoice(invoiceId);
alert('Factura validada correctamente');
if (onUpdate) onUpdate();
// Recargar la factura para mostrar el nuevo estado
await loadInvoice();
} catch (error) {
console.error('Error al validar:', error);
alert('Error al validar la factura: ' + error.message);
const validateBtn = modal.querySelector('.btn-validate');
if (validateBtn) {
validateBtn.disabled = false;
validateBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 4px;"><polyline points="20 6 9 17 4 12"/></svg>Validar Factura';
}
}
};
// Mostrar/ocultar formulario de añadir línea
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) {
// 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 handleSaveLine = async () => {
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) {
alert('La descripción es obligatoria');
return;
}
if (!quantity || quantity <= 0) {
alert('La cantidad debe ser mayor que 0');
return;
}
if (unitPrice < 0) {
alert('El precio no puede ser negativo');
return;
}
if (taxRate < 0 || taxRate > 100) {
alert('El IVA debe estar entre 0 y 100');
return;
}
try {
const saveBtn = modal.querySelector('.btn-save-line');
saveBtn.disabled = true;
saveBtn.textContent = 'Guardando...';
await addInvoiceLine(invoiceId, {
description,
quantity,
unitPrice,
taxRate
});
alert('Línea añadida correctamente');
toggleAddLineForm();
await loadInvoice();
} 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';
}
}
};
// Cargar factura
const loadInvoice = async () => {
try {
isLoading = true;
renderContent();
invoice = await getInvoiceById(invoiceId);
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>
`;
// Cargar factura
loadInvoice();
return modal;
}