hotfix rapido de crear facturas
This commit is contained in:
parent
76912ec0d7
commit
c24564dc35
|
|
@ -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;
|
||||||
|
|
|
||||||
115
src/style.css
115
src/style.css
|
|
@ -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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue