Merge remote-tracking branch 'origin/main' into Josep
This commit is contained in:
commit
a9cd1da63d
|
|
@ -1,13 +1,17 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<head>
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<title>doli-front</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
</head>
|
<title>doli-front</title>
|
||||||
<body>
|
<link rel="stylesheet" href="/src/styles/modal.css" />
|
||||||
<div id="app"></div>
|
</head>
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
<body>
|
||||||
</html>
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,73 @@
|
||||||
|
export function InvoiceItem(invoice, onView) {
|
||||||
|
const item = document.createElement('tr');
|
||||||
|
item.className = 'invoice-item';
|
||||||
|
|
||||||
|
// Formatear fecha
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Obtener clase de estado
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'draft': 'status-draft',
|
||||||
|
'validated': 'status-validated',
|
||||||
|
'paid': 'status-paid',
|
||||||
|
'unpaid': 'status-unpaid',
|
||||||
|
'canceled': 'status-canceled'
|
||||||
|
};
|
||||||
|
return statusMap[status] || 'status-default';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Obtener texto de estado
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusTextMap = {
|
||||||
|
'draft': 'Borrador',
|
||||||
|
'validated': 'Validada',
|
||||||
|
'paid': 'Pagada',
|
||||||
|
'unpaid': 'Impagada',
|
||||||
|
'canceled': 'Cancelada'
|
||||||
|
};
|
||||||
|
return statusTextMap[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
item.innerHTML = /*html*/`
|
||||||
|
<td class="invoice-number">${invoice.number}</td>
|
||||||
|
<td class="invoice-status">
|
||||||
|
<span class="status-badge ${getStatusClass(invoice.status)}">
|
||||||
|
${getStatusText(invoice.status)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="invoice-client">
|
||||||
|
<span class="client-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></span>
|
||||||
|
${invoice.clientName || 'Sin nombre'}
|
||||||
|
</td>
|
||||||
|
<td class="invoice-date">${formatDate(invoice.date)}</td>
|
||||||
|
<td class="invoice-total">${formatCurrency(invoice.total)}</td>
|
||||||
|
<td class="invoice-remain">${formatCurrency(invoice.remainToPay)}</td>
|
||||||
|
<td class="invoice-actions">
|
||||||
|
<button class="btn-action btn-view" data-invoice-id="${invoice.id}" title="Ver/Editar detalles">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Event listener para el botón de ver
|
||||||
|
const viewBtn = item.querySelector('.btn-view');
|
||||||
|
viewBtn.addEventListener('click', () => {
|
||||||
|
if (onView) {
|
||||||
|
onView(invoice.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,465 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 995 B After Width: | Height: | Size: 995 B |
|
|
@ -0,0 +1,243 @@
|
||||||
|
export function renderCreateInvoicePage() {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'create-invoice-page';
|
||||||
|
|
||||||
|
container.innerHTML = /*html*/`
|
||||||
|
<div class="invoice-header-compact">
|
||||||
|
<h1>Nueva Factura</h1>
|
||||||
|
<button class="btn-back" id="back-btn">← Volver</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="invoice-form-compact" id="invoice-form">
|
||||||
|
<div class="form-grid-compact">
|
||||||
|
<div class="form-field-compact full-width">
|
||||||
|
<label>Cliente</label>
|
||||||
|
<select id="clientId" name="clientId" required disabled>
|
||||||
|
<option value="">Cargando...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field-compact">
|
||||||
|
<label>Fecha</label>
|
||||||
|
<input type="date" id="date" name="date" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field-compact">
|
||||||
|
<label>Vencimiento</label>
|
||||||
|
<input type="date" id="expireDate" name="expireDate" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notes-grid-compact">
|
||||||
|
<div class="form-field-compact">
|
||||||
|
<label>Nota pública</label>
|
||||||
|
<textarea id="notePublic" name="notePublic" rows="2" placeholder="Visible para el cliente (opcional)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field-compact">
|
||||||
|
<label>Nota privada</label>
|
||||||
|
<textarea id="notePrivate" name="notePrivate" rows="2" placeholder="Interna (opcional)"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lines-section-compact">
|
||||||
|
<div class="lines-header-compact">
|
||||||
|
<h3>Líneas de Factura</h3>
|
||||||
|
<button type="button" class="btn-add-compact" id="add-line-btn">+ Añadir</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lines-table-compact" id="invoice-lines-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions-compact">
|
||||||
|
<button type="button" class="btn-cancel-compact" id="cancel-btn">Cancelar</button>
|
||||||
|
<button type="submit" class="btn-submit-compact">Crear Factura</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const clientSelect = container.querySelector('#clientId');
|
||||||
|
|
||||||
|
async function loadClients() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/Clients?limit=1000`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Error al cargar clientes');
|
||||||
|
|
||||||
|
const clients = await response.json();
|
||||||
|
|
||||||
|
clientSelect.innerHTML = '<option value="">Seleccionar cliente...</option>';
|
||||||
|
clients.forEach(client => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = client.id;
|
||||||
|
option.textContent = client.name;
|
||||||
|
clientSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
clientSelect.disabled = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
clientSelect.innerHTML = '<option value="">Error al cargar</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadClients();
|
||||||
|
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
container.querySelector('#date').value = today;
|
||||||
|
|
||||||
|
const expireDate = new Date();
|
||||||
|
expireDate.setDate(expireDate.getDate() + 30);
|
||||||
|
container.querySelector('#expireDate').value = expireDate.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
let lineCounter = 0;
|
||||||
|
|
||||||
|
function createInvoiceLine() {
|
||||||
|
lineCounter++;
|
||||||
|
const lineDiv = document.createElement('div');
|
||||||
|
lineDiv.className = 'line-row-compact';
|
||||||
|
|
||||||
|
lineDiv.innerHTML = /*html*/`
|
||||||
|
<input type="text" class="line-desc-compact" required placeholder="Descripción" />
|
||||||
|
<input type="number" class="line-qty-compact" required min="0.01" step="0.01" value="1" placeholder="Cant" />
|
||||||
|
<input type="number" class="line-price-compact" required min="0" step="0.01" placeholder="Precio" />
|
||||||
|
<input type="number" class="line-tax-compact" required min="0" max="100" step="0.01" value="21" placeholder="IVA %" />
|
||||||
|
<div class="line-total-compact">0.00 €</div>
|
||||||
|
<button type="button" class="btn-delete-compact" data-line-id="${lineCounter}">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const descInput = lineDiv.querySelector('.line-desc-compact');
|
||||||
|
const qtyInput = lineDiv.querySelector('.line-qty-compact');
|
||||||
|
const priceInput = lineDiv.querySelector('.line-price-compact');
|
||||||
|
const taxInput = lineDiv.querySelector('.line-tax-compact');
|
||||||
|
const totalDiv = lineDiv.querySelector('.line-total-compact');
|
||||||
|
|
||||||
|
function updateTotal() {
|
||||||
|
const qty = parseFloat(qtyInput.value) || 0;
|
||||||
|
const price = parseFloat(priceInput.value) || 0;
|
||||||
|
const tax = parseFloat(taxInput.value) || 0;
|
||||||
|
const subtotal = qty * price;
|
||||||
|
const total = subtotal * (1 + tax / 100);
|
||||||
|
totalDiv.textContent = `${total.toFixed(2)} €`;
|
||||||
|
}
|
||||||
|
|
||||||
|
qtyInput.addEventListener('input', updateTotal);
|
||||||
|
priceInput.addEventListener('input', updateTotal);
|
||||||
|
taxInput.addEventListener('input', updateTotal);
|
||||||
|
|
||||||
|
lineDiv.querySelector('.btn-delete-compact').addEventListener('click', () => {
|
||||||
|
lineDiv.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
return lineDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linesContainer = container.querySelector('#invoice-lines-container');
|
||||||
|
linesContainer.appendChild(createInvoiceLine());
|
||||||
|
linesContainer.appendChild(createInvoiceLine());
|
||||||
|
|
||||||
|
container.querySelector('#add-line-btn').addEventListener('click', () => {
|
||||||
|
linesContainer.appendChild(createInvoiceLine());
|
||||||
|
});
|
||||||
|
|
||||||
|
container.querySelector('#back-btn').addEventListener('click', () => {
|
||||||
|
window.location.hash = '#invoices';
|
||||||
|
});
|
||||||
|
|
||||||
|
container.querySelector('#cancel-btn').addEventListener('click', () => {
|
||||||
|
if (confirm('¿Cancelar? Se perderán los cambios.')) {
|
||||||
|
window.location.hash = '#invoices';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.querySelector('#invoice-form').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const clientIdValue = parseInt(container.querySelector('#clientId').value);
|
||||||
|
|
||||||
|
if (!clientIdValue || isNaN(clientIdValue) || clientIdValue < 1) {
|
||||||
|
alert('Selecciona un cliente');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = container.querySelectorAll('.line-row-compact');
|
||||||
|
if (lines.length === 0) {
|
||||||
|
alert('Añade al menos una línea');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
clientId: clientIdValue,
|
||||||
|
date: container.querySelector('#date').value + 'T00:00:00',
|
||||||
|
expireDate: container.querySelector('#expireDate').value + 'T00:00:00',
|
||||||
|
notePublic: container.querySelector('#notePublic').value || null,
|
||||||
|
notePrivate: container.querySelector('#notePrivate').value || null,
|
||||||
|
lines: []
|
||||||
|
};
|
||||||
|
|
||||||
|
let hasEmptyLine = false;
|
||||||
|
lines.forEach(line => {
|
||||||
|
const desc = line.querySelector('.line-desc-compact').value.trim();
|
||||||
|
const qty = parseFloat(line.querySelector('.line-qty-compact').value);
|
||||||
|
const price = parseFloat(line.querySelector('.line-price-compact').value);
|
||||||
|
const tax = parseFloat(line.querySelector('.line-tax-compact').value);
|
||||||
|
|
||||||
|
if (!desc || !qty || !price) {
|
||||||
|
hasEmptyLine = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.lines.push({
|
||||||
|
description: desc,
|
||||||
|
quantity: qty,
|
||||||
|
unitPrice: price,
|
||||||
|
taxRate: tax
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasEmptyLine) {
|
||||||
|
alert('Completa todos los campos de las líneas');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const submitBtn = container.querySelector('.btn-submit-compact');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.textContent = 'Guardando...';
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/Invoices`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(error || 'Error al crear');
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoiceId = await response.json();
|
||||||
|
alert(`Factura creada (ID: ${invoiceId})`);
|
||||||
|
window.location.hash = '#invoices';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert(error.message || 'Error al crear la factura');
|
||||||
|
|
||||||
|
const submitBtn = container.querySelector('.btn-submit-compact');
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = 'Crear Factura';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
import { InvoiceItem } from '../components/InvoiceItem.js';
|
||||||
|
import { InvoiceModal } from '../components/InvoiceModal.js';
|
||||||
|
|
||||||
|
export function renderFacturasPage() {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'facturas-page';
|
||||||
|
|
||||||
|
// Estado de paginación
|
||||||
|
let currentPage = 1;
|
||||||
|
let totalPages = 1;
|
||||||
|
const pageSize = 20;
|
||||||
|
let allInvoices = []; // Almacenar todas las facturas
|
||||||
|
let filteredInvoices = []; // Facturas filtradas
|
||||||
|
let currentFilter = ''; // Filtro actual de estado
|
||||||
|
let searchTerm = ''; // Término de búsqueda actual
|
||||||
|
|
||||||
|
container.innerHTML = /*html*/`
|
||||||
|
<div class="facturas-header">
|
||||||
|
<h1>Facturas</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="facturas-filters">
|
||||||
|
<input type="search" placeholder="Buscar facturas..." class="search-input" />
|
||||||
|
<select class="filter-status">
|
||||||
|
<option value="">Todos los estados</option>
|
||||||
|
<option value="draft">Borrador</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-container">
|
||||||
|
<table class="invoices-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="invoice-number">Número</th>
|
||||||
|
<th class="invoice-status">Estado</th>
|
||||||
|
<th class="invoice-client">Cliente</th>
|
||||||
|
<th class="invoice-date">Fecha</th>
|
||||||
|
<th class="invoice-total">Total</th>
|
||||||
|
<th class="invoice-remain">Pendiente</th>
|
||||||
|
<th class="invoice-actions">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="invoices-list">
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="loading">Cargando facturas...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<button class="btn-pagination btn-prev">← Anterior</button>
|
||||||
|
<div class="pagination-info">
|
||||||
|
<span class="current-page">1</span> / <span class="total-pages">1</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn-pagination btn-next">Siguiente →</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Función para aplicar filtros combinados
|
||||||
|
function applyFilters() {
|
||||||
|
// Ya no filtra localmente, hace llamada a la API
|
||||||
|
loadAllInvoices();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para renderizar la página actual
|
||||||
|
function renderPage(page = 1) {
|
||||||
|
const invoicesList = container.querySelector('.invoices-list');
|
||||||
|
|
||||||
|
currentPage = page;
|
||||||
|
totalPages = Math.ceil(filteredInvoices.length / pageSize);
|
||||||
|
|
||||||
|
// Calcular índices para la página
|
||||||
|
const startIndex = (page - 1) * pageSize;
|
||||||
|
const endIndex = Math.min(startIndex + pageSize, filteredInvoices.length);
|
||||||
|
const invoices = filteredInvoices.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
console.log(`Página ${currentPage}/${totalPages}, Facturas en esta página: ${invoices.length}, Total filtrado: ${filteredInvoices.length}`);
|
||||||
|
|
||||||
|
// Actualizar UI de paginación
|
||||||
|
updatePaginationUI();
|
||||||
|
|
||||||
|
// Limpiar lista
|
||||||
|
invoicesList.innerHTML = '';
|
||||||
|
|
||||||
|
// Si no hay facturas
|
||||||
|
if (invoices.length === 0) {
|
||||||
|
invoicesList.innerHTML = '<tr><td colspan="7" class="no-invoices">No hay facturas disponibles</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderizar cada factura
|
||||||
|
invoices.forEach(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');
|
||||||
|
|
||||||
|
try {
|
||||||
|
invoicesList.innerHTML = '<tr><td colspan="7" class="loading">Cargando facturas...</td></tr>';
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
// Construir URL con parámetros
|
||||||
|
let url = `${import.meta.env.VITE_API_BASE_URL}/api/Invoices?limit=1000`;
|
||||||
|
|
||||||
|
if (currentFilter) {
|
||||||
|
url += `&status=${currentFilter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
url += `&search=${encodeURIComponent(searchTerm)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Error al cargar facturas');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Manejo de respuesta - obtener todas las facturas
|
||||||
|
allInvoices = Array.isArray(data) ? data : data.data || data.invoices || [];
|
||||||
|
filteredInvoices = [...allInvoices];
|
||||||
|
|
||||||
|
console.log(`Total de facturas cargadas: ${allInvoices.length}`);
|
||||||
|
|
||||||
|
// Renderizar la primera página
|
||||||
|
renderPage(1);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar facturas:', error);
|
||||||
|
invoicesList.innerHTML = '<tr><td colspan="7" class="error">Error al cargar las facturas. Por favor, intenta de nuevo.</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para actualizar la UI de paginación
|
||||||
|
function updatePaginationUI() {
|
||||||
|
const paginationDiv = container.querySelector('.pagination');
|
||||||
|
const currentPageSpan = container.querySelector('.current-page');
|
||||||
|
const totalPagesSpan = container.querySelector('.total-pages');
|
||||||
|
const btnPrev = container.querySelector('.btn-prev');
|
||||||
|
const btnNext = container.querySelector('.btn-next');
|
||||||
|
|
||||||
|
currentPageSpan.textContent = currentPage;
|
||||||
|
totalPagesSpan.textContent = totalPages;
|
||||||
|
|
||||||
|
// Mostrar u ocultar la paginación según el número de páginas
|
||||||
|
if (totalPages <= 1) {
|
||||||
|
paginationDiv.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
paginationDiv.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deshabilitar botones según corresponda
|
||||||
|
btnPrev.disabled = currentPage === 1;
|
||||||
|
btnNext.disabled = currentPage === totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
const newInvoiceBtn = container.querySelector('.btn-new-invoice');
|
||||||
|
newInvoiceBtn.addEventListener('click', () => {
|
||||||
|
window.location.hash = '#create-invoice';
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput = container.querySelector('.search-input');
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
const inputValue = e.target.value.trim();
|
||||||
|
// Debounce para no hacer muchas llamadas
|
||||||
|
clearTimeout(searchInput.debounceTimer);
|
||||||
|
searchInput.debounceTimer = setTimeout(() => {
|
||||||
|
// Validación mínima: al menos 2 caracteres para activar búsqueda
|
||||||
|
if (inputValue.length >= 2) {
|
||||||
|
searchTerm = inputValue;
|
||||||
|
} else {
|
||||||
|
searchTerm = ''; // Si es muy corto, mostrar todas
|
||||||
|
}
|
||||||
|
applyFilters();
|
||||||
|
}, 500); // Espera 500ms después de que el usuario deja de escribir
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterStatus = container.querySelector('.filter-status');
|
||||||
|
filterStatus.addEventListener('change', (e) => {
|
||||||
|
currentFilter = e.target.value;
|
||||||
|
console.log(`Filtro aplicado: ${currentFilter || 'Todos'}`);
|
||||||
|
applyFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listeners de paginación
|
||||||
|
const btnPrev = container.querySelector('.btn-prev');
|
||||||
|
const btnNext = container.querySelector('.btn-next');
|
||||||
|
|
||||||
|
btnPrev.addEventListener('click', () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
renderPage(currentPage - 1);
|
||||||
|
scrollToTop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnNext.addEventListener('click', () => {
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
renderPage(currentPage + 1);
|
||||||
|
scrollToTop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Función para scroll al inicio
|
||||||
|
function scrollToTop() {
|
||||||
|
container.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar todas las facturas al montar el componente
|
||||||
|
loadAllInvoices();
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
@ -1,40 +1,37 @@
|
||||||
import { renderDashboard } from './DashboardPage.js';
|
import { renderDashboard } from './DashboardPage.js';
|
||||||
import { renderTestPage } from './test.js';
|
import { renderFacturasPage } from './Facturas.js';
|
||||||
|
import { renderCreateInvoicePage } from './CreateInvoicePage.js';
|
||||||
|
|
||||||
// Registry of all pages available in the application
|
// Registry of all pages available in the application
|
||||||
export const pagesRegistry = [
|
export const pagesRegistry = [
|
||||||
{
|
{
|
||||||
route: 'dashboard',
|
route: 'dashboard',
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
icon: '🏠',
|
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
showInSidebar: true,
|
showInSidebar: true,
|
||||||
render: renderDashboard
|
render: renderDashboard
|
||||||
},
|
},
|
||||||
{
|
|
||||||
route: 'test',
|
|
||||||
name: 'Test Page',
|
|
||||||
icon: '🧪',
|
|
||||||
requiresAuth: true,
|
|
||||||
showInSidebar: true,
|
|
||||||
render: renderTestPage
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
route: 'invoices',
|
route: 'invoices',
|
||||||
name: 'Facturas',
|
name: 'Facturas',
|
||||||
icon: '📄',
|
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
showInSidebar: true,
|
showInSidebar: true,
|
||||||
render: () => {
|
render: renderFacturasPage
|
||||||
const div = document.createElement('div');
|
},
|
||||||
div.innerHTML = '<h1>Facturas</h1><p>Página en construcción...</p>';
|
{
|
||||||
return div;
|
route: 'create-invoice',
|
||||||
}
|
name: 'Nueva Factura',
|
||||||
|
icon: '+',
|
||||||
|
requiresAuth: true,
|
||||||
|
showInSidebar: false, // No mostrar en sidebar
|
||||||
|
render: renderCreateInvoicePage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
route: 'clients',
|
route: 'clients',
|
||||||
name: 'Clientes',
|
name: 'Clientes',
|
||||||
icon: '👥',
|
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
showInSidebar: true,
|
showInSidebar: true,
|
||||||
render: () => {
|
render: () => {
|
||||||
|
|
@ -46,7 +43,7 @@ export const pagesRegistry = [
|
||||||
{
|
{
|
||||||
route: 'settings',
|
route: 'settings',
|
||||||
name: 'Configuración',
|
name: 'Configuración',
|
||||||
icon: '⚙️',
|
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24"/></svg>',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
showInSidebar: true,
|
showInSidebar: true,
|
||||||
render: () => {
|
render: () => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
color: var(--primary);
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--primary-hover);
|
color: var(--primary-hover);
|
||||||
}
|
}
|
||||||
|
|
@ -57,9 +58,11 @@ h1 {
|
||||||
will-change: filter;
|
will-change: filter;
|
||||||
transition: filter 300ms;
|
transition: filter 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo:hover {
|
.logo:hover {
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo.vanilla:hover {
|
.logo.vanilla:hover {
|
||||||
filter: drop-shadow(0 0 2em #f7df1eaa);
|
filter: drop-shadow(0 0 2em #f7df1eaa);
|
||||||
}
|
}
|
||||||
|
|
@ -84,9 +87,11 @@ button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.25s, border-color 0.25s;
|
transition: background-color 0.25s, border-color 0.25s;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: var(--primary-hover);
|
background-color: var(--primary-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus,
|
button:focus,
|
||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
|
@ -97,6 +102,7 @@ button:focus-visible {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
background-color: #f8fafc;
|
background-color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--primary-hover);
|
color: var(--primary-hover);
|
||||||
}
|
}
|
||||||
|
|
@ -228,22 +234,22 @@ button:focus-visible {
|
||||||
|
|
||||||
.status-pending {
|
.status-pending {
|
||||||
background-color: #fef3c7;
|
background-color: #fef3c7;
|
||||||
color: #d97706;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-paid {
|
.status-paid {
|
||||||
background-color: #d1fae5;
|
background-color: #d1fae5;
|
||||||
color: #059669;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-overdue {
|
.status-overdue {
|
||||||
background-color: #fee2e2;
|
background-color: #fee2e2;
|
||||||
color: #dc2626;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-draft {
|
.status-draft {
|
||||||
background-color: #f1f5f9;
|
background-color: #f1f5f9;
|
||||||
color: #64748b;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount-display {
|
.amount-display {
|
||||||
|
|
@ -404,7 +410,7 @@ button:focus-visible {
|
||||||
background-color: #f8fafc;
|
background-color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content > div {
|
.main-content>div {
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
|
@ -431,38 +437,347 @@ button:focus-visible {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Test Page Styles */
|
/* Facturas Page Styles */
|
||||||
.test-page {
|
.facturas-page {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-page h1 {
|
.facturas-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facturas-header h1 {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-content {
|
.btn-new-invoice {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-new-invoice:hover {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facturas-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input,
|
||||||
|
.filter-status {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-status {
|
||||||
|
min-width: 200px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Invoices Table Styles */
|
||||||
|
.invoices-table-container {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
margin-top: 1rem;
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-button {
|
.invoices-table {
|
||||||
background-color: var(--success);
|
width: 100%;
|
||||||
color: white;
|
border-collapse: collapse;
|
||||||
padding: 0.5rem 1rem;
|
}
|
||||||
|
|
||||||
|
.invoices-table thead {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoices-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoices-table tbody tr {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoices-table tbody tr:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoices-table tbody tr:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoices-table td {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-number {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-status .status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-draft {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-validated {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-paid {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-unpaid {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-canceled {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #000000;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-client {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-date,
|
||||||
|
.invoice-total,
|
||||||
|
.invoice-remain {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-total,
|
||||||
|
.invoice-remain {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view {
|
||||||
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
padding: 0.25rem;
|
||||||
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-top: 1rem;
|
color: var(--text-secondary);
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-button:hover {
|
.btn-view:hover {
|
||||||
background-color: var(--success-hover);
|
color: var(--primary);
|
||||||
border-color: transparent;
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.error,
|
||||||
|
.no-invoices {
|
||||||
|
padding: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination Styles */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info .current-page,
|
||||||
|
.pagination-info .total-pages {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary);
|
||||||
|
min-width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pagination {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pagination:hover:not(:disabled) {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pagination:disabled {
|
||||||
|
background-color: #cbd5e1;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pagination:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pagination {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pagination {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info {
|
||||||
|
order: 3;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
|
||||||
|
.invoices-header,
|
||||||
|
.invoice-item {
|
||||||
|
grid-template-columns: 40px 120px 100px 50px 130px 100px 100px 100px 70px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.facturas-page {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoices-header,
|
||||||
|
.invoice-item {
|
||||||
|
grid-template-columns: 40px 100px 90px 130px 90px 90px 60px;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-id,
|
||||||
|
.invoice-remain {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.facturas-filters {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoices-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-item {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-item>div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-item>div::before {
|
||||||
|
content: attr(class);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adjust dashboard and login for new layout */
|
/* Adjust dashboard and login for new layout */
|
||||||
|
|
@ -473,3 +788,314 @@ button:focus-visible {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Create Invoice Page - Ultra Compact */
|
||||||
|
.create-invoice-page {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-header-compact {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-header-compact h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-form-compact {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid-compact {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-grid-compact {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-compact {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-compact.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-compact label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-compact input,
|
||||||
|
.form-field-compact select,
|
||||||
|
.form-field-compact textarea {
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: #ffffff;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 38px;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-compact input:focus,
|
||||||
|
.form-field-compact select:focus,
|
||||||
|
.form-field-compact textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lines-section-compact {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lines-header-compact {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lines-header-compact h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-compact {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-compact:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lines-table-compact {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-row-compact {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 4fr 0.8fr 1.2fr 0.8fr 1.2fr auto;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-row-compact:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-desc-compact,
|
||||||
|
.line-qty-compact,
|
||||||
|
.line-price-compact,
|
||||||
|
.line-tax-compact {
|
||||||
|
padding: 0.5rem 0.625rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
background: white;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-desc-compact {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-desc-compact:focus,
|
||||||
|
.line-qty-compact:focus,
|
||||||
|
.line-price-compact:focus,
|
||||||
|
.line-tax-compact:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-total-compact {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete-compact {
|
||||||
|
background: transparent;
|
||||||
|
color: #94a3b8;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete-compact:hover {
|
||||||
|
color: #ef4444;
|
||||||
|
background: #fee2e2;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions-compact {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel-compact {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.625rem 1.25rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel-compact:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit-compact {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit-compact:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit-compact:disabled {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #94a3b8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.create-invoice-page {
|
||||||
|
padding: 0.5rem;
|
||||||
|
height: calc(100vh - 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid-compact {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-compact.full-width {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-row-compact {
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-desc-compact {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-qty-compact,
|
||||||
|
.line-price-compact,
|
||||||
|
.line-tax-compact,
|
||||||
|
.line-total-compact {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete-compact {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,393 @@
|
||||||
|
/* 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;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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