doli-front/src/components/InvoiceModal.js

466 lines
16 KiB
JavaScript
Raw Normal View History

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;
}