Add Invoice Modal component and integrate with invoice management
This commit is contained in:
parent
f7b376e10f
commit
84c99b8155
26
index.html
26
index.html
|
|
@ -1,13 +1,17 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>doli-front</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>doli-front</title>
|
||||
<link rel="stylesheet" href="/src/styles/modal.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export function InvoiceItem(invoice) {
|
||||
export function InvoiceItem(invoice, onView) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'invoice-item';
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ export function InvoiceItem(invoice) {
|
|||
<div class="invoice-total">${formatCurrency(invoice.total)}</div>
|
||||
<div class="invoice-remain">${formatCurrency(invoice.remainToPay)}</div>
|
||||
<div class="invoice-actions">
|
||||
<button class="btn-view" data-invoice-id="${invoice.id}" title="Ver detalles">
|
||||
<button class="btn-action btn-view" data-invoice-id="${invoice.id}" title="Ver/Editar detalles">
|
||||
👁️
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -67,9 +67,9 @@ export function InvoiceItem(invoice) {
|
|||
// Event listener para el botón de ver
|
||||
const viewBtn = item.querySelector('.btn-view');
|
||||
viewBtn.addEventListener('click', () => {
|
||||
console.log('Ver factura:', invoice.id);
|
||||
// Aquí puedes agregar la lógica para ver los detalles
|
||||
alert(`Ver detalles de factura: ${invoice.number}`);
|
||||
if (onView) {
|
||||
onView(invoice.id);
|
||||
}
|
||||
});
|
||||
|
||||
return item;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,470 @@
|
|||
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="notePublic"
|
||||
rows="3"
|
||||
>${invoice.notePublic || ''}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group full-width">
|
||||
<label>Nota Privada</label>
|
||||
<textarea
|
||||
name="notePrivate"
|
||||
rows="3"
|
||||
>${invoice.notePrivate || ''}</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">✓ 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);
|
||||
|
||||
const data = {
|
||||
number: formData.get('number') || undefined,
|
||||
expireDate: formData.get('expireDate') || undefined,
|
||||
notePublic: formData.get('notePublic') || undefined,
|
||||
notePrivate: formData.get('notePrivate') || 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.textContent = '✓ 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;
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { InvoiceItem } from '../components/InvoiceItem.js';
|
||||
import { InvoiceModal } from '../components/InvoiceModal.js';
|
||||
|
||||
export function renderFacturasPage() {
|
||||
const container = document.createElement('div');
|
||||
|
|
@ -16,7 +17,6 @@ export function renderFacturasPage() {
|
|||
container.innerHTML = /*html*/`
|
||||
<div class="facturas-header">
|
||||
<h1>Facturas</h1>
|
||||
<button class="btn-new-invoice">+ Nueva Factura</button>
|
||||
</div>
|
||||
|
||||
<div class="facturas-filters">
|
||||
|
|
@ -24,11 +24,11 @@ export function renderFacturasPage() {
|
|||
<select class="filter-status">
|
||||
<option value="">Todos los estados</option>
|
||||
<option value="draft">Borrador</option>
|
||||
<option value="validated">Validada</option>
|
||||
<option value="paid">Pagada</option>
|
||||
<option value="unpaid">Impagada</option>
|
||||
<option value="canceled">Cancelada</option>
|
||||
</select>
|
||||
<button class="btn-new-invoice">+ Nueva Factura</button>
|
||||
</div>
|
||||
|
||||
<div class="invoices-table">
|
||||
|
|
@ -92,11 +92,20 @@ export function renderFacturasPage() {
|
|||
|
||||
// Renderizar cada factura
|
||||
invoices.forEach(invoice => {
|
||||
const invoiceItem = InvoiceItem(invoice);
|
||||
const invoiceItem = InvoiceItem(invoice, handleViewInvoice);
|
||||
invoicesList.appendChild(invoiceItem);
|
||||
});
|
||||
}
|
||||
|
||||
// Manejar ver/editar factura
|
||||
function handleViewInvoice(invoiceId) {
|
||||
const modal = InvoiceModal(invoiceId, null, () => {
|
||||
// Callback cuando se actualiza la factura
|
||||
loadAllInvoices();
|
||||
});
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// Función para cargar todas las facturas
|
||||
async function loadAllInvoices() {
|
||||
const invoicesList = container.querySelector('.invoices-list');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
function getAuthHeaders() {
|
||||
const token = localStorage.getItem('token');
|
||||
return {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
export async function getInvoiceById(id) {
|
||||
const response = await fetch(`${API_BASE_URL}/api/Invoices/${id}`, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Error al obtener la factura');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function updateInvoice(id, data) {
|
||||
const response = await fetch(`${API_BASE_URL}/api/Invoices/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new Error(error.detail || 'Error al actualizar la factura');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function validateInvoice(id) {
|
||||
const response = await fetch(`${API_BASE_URL}/api/Invoices/${id}/validate`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new Error(error.detail || 'Error al validar la factura');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function updateInvoiceStatus(id, status) {
|
||||
const response = await fetch(`${API_BASE_URL}/api/Invoices/${id}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ status })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new Error(error.detail || 'Error al actualizar el estado');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function addInvoiceLine(id, lineData) {
|
||||
const response = await fetch(`${API_BASE_URL}/api/Invoices/${id}/lines`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(lineData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new Error(error.detail || 'Error al agregar línea');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function deleteInvoice(id) {
|
||||
// Nota: No hay endpoint DELETE en el OpenAPI spec,
|
||||
// pero podríamos usar el endpoint de cambio de estado a "canceled"
|
||||
// o implementarlo si está disponible en el backend
|
||||
const response = await fetch(`${API_BASE_URL}/api/Invoices/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Si no existe DELETE, intentar cancelar
|
||||
return await updateInvoiceStatus(id, 'canceled');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ a {
|
|||
color: var(--primary);
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
|
@ -57,9 +58,11 @@ h1 {
|
|||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
|
||||
.logo.vanilla:hover {
|
||||
filter: drop-shadow(0 0 2em #f7df1eaa);
|
||||
}
|
||||
|
|
@ -84,9 +87,11 @@ button {
|
|||
cursor: pointer;
|
||||
transition: background-color 0.25s, border-color 0.25s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
|
|
@ -97,6 +102,7 @@ button:focus-visible {
|
|||
color: var(--text-primary);
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
|
@ -405,7 +411,7 @@ button:focus-visible {
|
|||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.main-content > div {
|
||||
.main-content>div {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
|
@ -440,9 +446,6 @@ button:focus-visible {
|
|||
}
|
||||
|
||||
.facturas-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
|
|
@ -461,6 +464,7 @@ button:focus-visible {
|
|||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.2s;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn-new-invoice:hover {
|
||||
|
|
@ -471,6 +475,7 @@ button:focus-visible {
|
|||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input,
|
||||
|
|
@ -714,6 +719,7 @@ button:focus-visible {
|
|||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
|
||||
.invoices-header,
|
||||
.invoice-item {
|
||||
grid-template-columns: 40px 120px 100px 50px 130px 100px 100px 100px 70px;
|
||||
|
|
@ -758,13 +764,13 @@ button:focus-visible {
|
|||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.invoice-item > div {
|
||||
.invoice-item>div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.invoice-item > div::before {
|
||||
.invoice-item>div::before {
|
||||
content: attr(class);
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
|
|
@ -815,6 +821,7 @@ button:focus-visible {
|
|||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Create Invoice Page - Ultra Compact */
|
||||
.create-invoice-page {
|
||||
padding: 1rem;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,392 @@
|
|||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.modal-title h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
background-color: #f1f5f9;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 1.5rem 2rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.footer-actions-left,
|
||||
.footer-actions-right {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Form Styles in Modal */
|
||||
.form-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.125rem;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
padding: 0.625rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.9375rem;
|
||||
transition: all 0.2s;
|
||||
font-family: inherit;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.form-group input:disabled,
|
||||
.form-group textarea:disabled,
|
||||
.form-group select:disabled {
|
||||
background-color: #f8fafc;
|
||||
color: var(--text-secondary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-group input[readonly] {
|
||||
background-color: #f8fafc;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Add Line Form */
|
||||
.add-line-form {
|
||||
background-color: #f8fafc;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
border: 2px dashed var(--border-color);
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* Invoice Lines Table */
|
||||
.invoice-lines {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.lines-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.lines-table thead {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.lines-table th {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.lines-table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.lines-table tbody tr:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.no-lines {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Invoice Totals */
|
||||
.invoice-totals {
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
max-width: 300px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.total-row.pending {
|
||||
font-size: 1.125rem;
|
||||
color: var(--primary);
|
||||
padding-top: 0.75rem;
|
||||
border-top: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.btn-small {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: var(--success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: var(--success-hover);
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: transparent;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background-color: #f8fafc;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Invoice Actions Buttons */
|
||||
.invoice-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
background-color: #f1f5f9;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background-color: #fee2e2;
|
||||
}
|
||||
|
||||
/* Loading and Error States */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* Responsive Modal */
|
||||
@media (max-width: 768px) {
|
||||
.modal-overlay {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
max-width: 100%;
|
||||
max-height: 100vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-body,
|
||||
.modal-footer {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.invoice-totals {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.footer-actions-left,
|
||||
.footer-actions-right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer-actions-left button,
|
||||
.footer-actions-right button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue