hotfix rapido de crear facturas

This commit is contained in:
Алекс 2026-02-06 20:46:31 +01:00
parent 76912ec0d7
commit c24564dc35
2 changed files with 176 additions and 29 deletions

View File

@ -60,10 +60,34 @@ export function renderCreateInvoicePage() {
<div class="lines-section-compact"> <div class="lines-section-compact">
<div class="lines-header-compact"> <div class="lines-header-compact">
<h3>Líneas de Factura</h3> <h3>Líneas de Factura</h3>
<button type="button" class="btn-add-compact" id="add-line-btn">+ Añadir</button> <button type="button" class="btn-add-compact" id="add-line-btn">+ Añadir línea</button>
</div>
<div class="lines-columns-header">
<span class="col-desc">Descripción</span>
<span class="col-qty">Cantidad</span>
<span class="col-price">Precio</span>
<span class="col-tax">IVA %</span>
<span class="col-total">Total</span>
<span class="col-actions"></span>
</div> </div>
<div class="lines-table-compact" id="invoice-lines-container"></div> <div class="lines-table-compact" id="invoice-lines-container"></div>
<div class="invoice-grand-total" id="invoice-grand-total">
<div class="grand-total-row">
<span>Subtotal (sin IVA):</span>
<strong id="grand-subtotal">0,00 €</strong>
</div>
<div class="grand-total-row">
<span>IVA:</span>
<strong id="grand-tax">0,00 €</strong>
</div>
<div class="grand-total-row grand-total-final">
<span>Total:</span>
<strong id="grand-total-value">0,00 €</strong>
</div>
</div>
</div> </div>
<div class="form-actions-compact"> <div class="form-actions-compact">
@ -118,6 +142,34 @@ export function renderCreateInvoicePage() {
}); });
}); });
// Formatear moneda
function formatCurrency(amount) {
return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(amount);
}
// Actualizar total general
function updateGrandTotal() {
const lines = container.querySelectorAll('.line-row-compact');
let subtotal = 0;
let totalTax = 0;
lines.forEach(line => {
const qty = parseFloat(line.querySelector('.line-qty-compact').value) || 0;
const price = parseFloat(line.querySelector('.line-price-compact').value) || 0;
const tax = parseFloat(line.querySelector('.line-tax-compact').value) || 0;
const lineSubtotal = qty * price;
subtotal += lineSubtotal;
totalTax += lineSubtotal * (tax / 100);
});
const grandSubtotalEl = container.querySelector('#grand-subtotal');
const grandTaxEl = container.querySelector('#grand-tax');
const grandTotalEl = container.querySelector('#grand-total-value');
if (grandSubtotalEl) grandSubtotalEl.textContent = formatCurrency(subtotal);
if (grandTaxEl) grandTaxEl.textContent = formatCurrency(totalTax);
if (grandTotalEl) grandTotalEl.textContent = formatCurrency(subtotal + totalTax);
}
let lineCounter = 0; let lineCounter = 0;
function createInvoiceLine() { function createInvoiceLine() {
@ -126,15 +178,33 @@ export function renderCreateInvoicePage() {
lineDiv.className = 'line-row-compact'; lineDiv.className = 'line-row-compact';
lineDiv.innerHTML = /*html*/` lineDiv.innerHTML = /*html*/`
<input type="text" class="line-desc-compact" required placeholder="Descripción" /> <div class="line-field line-field-desc">
<input type="number" class="line-qty-compact" required min="0.01" step="0.01" value="1" placeholder="Cant" /> <label class="line-label-mobile">Descripción</label>
<input type="number" class="line-price-compact" required min="0" step="0.01" placeholder="Precio" /> <input type="text" class="line-desc-compact" required placeholder="Producto o servicio..." />
<input type="number" class="line-tax-compact" required min="0" max="100" step="0.01" value="21" placeholder="IVA %" /> </div>
<div class="line-total-compact">0.00 €</div> <div class="line-field line-field-qty">
<button type="button" class="btn-delete-compact" data-line-id="${lineCounter}">×</button> <label class="line-label-mobile">Cant.</label>
<input type="number" class="line-qty-compact" required min="0.01" step="0.01" value="1" />
</div>
<div class="line-field line-field-price">
<label class="line-label-mobile">Precio (€)</label>
<input type="number" class="line-price-compact" required min="0" max="2147483647" step="0.01" placeholder="0.00" />
</div>
<div class="line-field line-field-tax">
<label class="line-label-mobile">IVA %</label>
<input type="number" class="line-tax-compact" required min="0" max="100" step="0.01" value="21" />
</div>
<div class="line-field line-field-total">
<label class="line-label-mobile">Total</label>
<div class="line-total-compact">0,00 €</div>
</div>
<div class="line-field line-field-actions">
<button type="button" class="btn-delete-compact" data-line-id="${lineCounter}" title="Eliminar línea">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</div>
`; `;
const descInput = lineDiv.querySelector('.line-desc-compact');
const qtyInput = lineDiv.querySelector('.line-qty-compact'); const qtyInput = lineDiv.querySelector('.line-qty-compact');
const priceInput = lineDiv.querySelector('.line-price-compact'); const priceInput = lineDiv.querySelector('.line-price-compact');
const taxInput = lineDiv.querySelector('.line-tax-compact'); const taxInput = lineDiv.querySelector('.line-tax-compact');
@ -146,7 +216,8 @@ export function renderCreateInvoicePage() {
const tax = parseFloat(taxInput.value) || 0; const tax = parseFloat(taxInput.value) || 0;
const subtotal = qty * price; const subtotal = qty * price;
const total = subtotal * (1 + tax / 100); const total = subtotal * (1 + tax / 100);
totalDiv.textContent = `${total.toFixed(2)} €`; totalDiv.textContent = formatCurrency(total);
updateGrandTotal();
} }
qtyInput.addEventListener('input', updateTotal); qtyInput.addEventListener('input', updateTotal);
@ -155,6 +226,7 @@ export function renderCreateInvoicePage() {
lineDiv.querySelector('.btn-delete-compact').addEventListener('click', () => { lineDiv.querySelector('.btn-delete-compact').addEventListener('click', () => {
lineDiv.remove(); lineDiv.remove();
updateGrandTotal();
}); });
return lineDiv; return lineDiv;

View File

@ -1029,23 +1029,49 @@ button:focus-visible {
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem;
}
/* Column headers */
.lines-columns-header {
display: grid;
grid-template-columns: 4fr 0.8fr 1.2fr 0.8fr 1.2fr 40px;
gap: 0.75rem; gap: 0.75rem;
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 2px solid var(--border-color);
margin-bottom: 0.25rem;
} }
.line-row-compact { .line-row-compact {
display: grid; display: grid;
grid-template-columns: 4fr 0.8fr 1.2fr 0.8fr 1.2fr auto; grid-template-columns: 4fr 0.8fr 1.2fr 0.8fr 1.2fr 40px;
gap: 0.75rem; gap: 0.75rem;
align-items: center; align-items: center;
background: #f8fafc; background: #f8fafc;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 6px; border-radius: 8px;
padding: 0.75rem; padding: 0.75rem;
transition: all 0.2s; transition: all 0.2s;
} }
.line-row-compact:hover { .line-row-compact:hover {
background: #f1f5f9; background: #f1f5f9;
border-color: #cbd5e1;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.line-label-mobile {
display: none;
}
.line-field {
display: flex;
flex-direction: column;
} }
.line-desc-compact, .line-desc-compact,
@ -1076,23 +1102,22 @@ button:focus-visible {
.line-total-compact { .line-total-compact {
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--primary);
font-size: 0.9375rem; font-size: 0.9375rem;
text-align: right; text-align: right;
padding-right: 0.5rem; padding: 0.5rem 0.25rem;
background: rgba(37, 99, 235, 0.04);
border-radius: 4px;
} }
.btn-delete-compact { .btn-delete-compact {
background: transparent; background: transparent;
color: #94a3b8; color: #94a3b8;
border: 1px solid transparent; border: 1px solid transparent;
width: 32px; width: 36px;
height: 32px; height: 36px;
border-radius: 4px; border-radius: 6px;
cursor: pointer; cursor: pointer;
font-size: 1.5rem;
font-weight: 300;
line-height: 1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -1103,7 +1128,45 @@ button:focus-visible {
.btn-delete-compact:hover { .btn-delete-compact:hover {
color: #ef4444; color: #ef4444;
background: #fee2e2; background: #fee2e2;
border-color: transparent; border-color: #fecaca;
}
/* Grand total section */
.invoice-grand-total {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 1rem;
padding: 1rem 1.25rem;
background: #f8fafc;
border: 1px solid var(--border-color);
border-radius: 8px;
max-width: 300px;
margin-left: auto;
}
.grand-total-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
color: var(--text-secondary);
}
.grand-total-row strong {
color: var(--text-primary);
font-weight: 600;
}
.grand-total-final {
padding-top: 0.5rem;
border-top: 2px solid var(--border-color);
font-size: 1.05rem;
}
.grand-total-final strong {
color: var(--primary);
font-size: 1.125rem;
} }
.form-actions-compact { .form-actions-compact {
@ -1170,25 +1233,37 @@ button:focus-visible {
grid-column: 1; grid-column: 1;
} }
.lines-columns-header {
display: none;
}
.line-row-compact { .line-row-compact {
grid-template-columns: 1fr auto; grid-template-columns: 1fr 1fr;
gap: 0.5rem; gap: 0.5rem;
padding: 0.75rem; padding: 0.75rem;
} }
.line-desc-compact { .line-label-mobile {
grid-column: 1; display: block;
font-size: 0.7rem;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 0.2rem;
} }
.line-qty-compact, .line-field-desc {
.line-price-compact, grid-column: 1 / -1;
.line-tax-compact,
.line-total-compact {
width: 60px;
} }
.btn-delete-compact { .line-field-actions {
grid-column: 2; grid-column: 2;
justify-self: end;
}
.invoice-grand-total {
max-width: 100%;
} }
} }