doli-front/src/pages/FacturasProveedores.js

795 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { icons } from '../services/icons.js';
import {
getSupplierInvoices, getSupplierInvoiceById, createSupplierInvoice,
updateSupplierInvoice, deleteSupplierInvoice, changeSupplierInvoiceStatus,
addSupplierInvoiceLine, updateSupplierInvoiceLine, deleteSupplierInvoiceLine,
getSupplierInvoicePayments, addSupplierInvoicePayment
} from '../services/supplierInvoices.js';
import { showToast } from '../services/toast.js';
import { apiGet } from '../services/apiClient.js';
import { getPaymentTypes } from '../services/setup.js';
export function renderFacturasProveedoresPage() {
const container = document.createElement('div');
container.className = 'facturas-page page-enter';
let allInvoices = [];
let filteredInvoices = [];
let currentFilter = '';
let searchTerm = '';
let currentInvoice = null;
let payments = [];
let paymentTypes = [];
const fmt = (n) => new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(n || 0);
const fmtDate = (d) => d ? new Date(d).toLocaleDateString('es-ES') : '—';
const fmtDateInput = (d) => d ? new Date(d).toISOString().split('T')[0] : '';
const STATUS_TEXT = {
draft: 'Borrador',
unpaid: 'Pte. Pago',
paid: 'Pagada',
cancelled: 'Cancelada',
unknown: 'Desconocido'
};
const STATUS_CLASS = {
draft: 'status-draft',
unpaid: 'status-unpaid',
paid: 'status-paid',
cancelled: 'status-canceled',
unknown: 'status-draft'
};
function escHtml(str) {
if (str == null) return '';
const d = document.createElement('div');
d.textContent = String(str);
return d.innerHTML;
}
container.innerHTML = /*html*/`
<div class="facturas-header">
<h1>Facturas de Proveedores</h1>
</div>
<div class="facturas-filters">
<div class="search-wrapper">
<span class="search-icon">${icons.search}</span>
<input type="search" placeholder="Buscar proveedor o referencia..." class="search-input search-input--with-icon" id="sup-search" />
</div>
<select class="filter-status" id="sup-filter">
<option value="">Todos los estados</option>
<option value="draft">Borrador</option>
<option value="unpaid">Pte. Pago</option>
<option value="paid">Pagada</option>
</select>
<button class="btn-new-invoice" id="btn-new-sup">${icons.plus} <span>Nueva factura</span></button>
</div>
<div class="invoices-summary" id="sup-summary" style="display:none">
<div class="summary-item">
<span class="summary-label">Facturas</span>
<span class="summary-value" id="sum-count">—</span>
</div>
<div class="summary-divider"></div>
<div class="summary-item">
<span class="summary-label">Base imponible</span>
<span class="summary-value" id="sum-ht">—</span>
</div>
<div class="summary-divider"></div>
<div class="summary-item">
<span class="summary-label">IVA soportado</span>
<span class="summary-value" id="sum-tax">—</span>
</div>
<div class="summary-divider"></div>
<div class="summary-item">
<span class="summary-label">Total</span>
<span class="summary-value" id="sum-total">—</span>
</div>
<div class="summary-divider"></div>
<div class="summary-item summary-item--pending">
<span class="summary-label">Pendiente</span>
<span class="summary-value" id="sum-pending">—</span>
</div>
</div>
<div class="invoices-table-container">
<table class="invoices-table">
<thead>
<tr>
<th>Número</th>
<th>Estado</th>
<th>Proveedor</th>
<th>Fecha</th>
<th class="text-right">Total</th>
<th class="text-right">Pendiente</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="sup-tbody">
<tr><td colspan="7" class="facturas-empty">Cargando...</td></tr>
</tbody>
</table>
</div>
<!-- Modal detalle -->
<div class="modal-overlay sup-detail-overlay" id="sup-detail-overlay" style="display:none">
<div class="modal-content sup-detail-modal" id="sup-detail-modal">
<div class="modal-header">
<h2 id="sup-detail-title">Detalle</h2>
<button class="btn-close" id="sup-detail-close">×</button>
</div>
<div class="modal-body" id="sup-detail-body">
<div class="loading">Cargando...</div>
</div>
</div>
</div>
<!-- Modal crear factura proveedor -->
<div class="modal-overlay sup-detail-overlay" id="sup-create-overlay" style="display:none">
<div class="modal-content sup-detail-modal" id="sup-create-modal">
<div class="modal-header">
<h2>Nueva factura de proveedor</h2>
<button class="btn-close" id="sup-create-close">×</button>
</div>
<div class="modal-body">
<form id="sup-create-form">
<div class="invoice-detail-grid" style="margin-bottom:1rem">
<div class="form-field-compact full-width" style="grid-column:1/-1">
<label>Proveedor *</label>
<select id="sup-supplier-select" required style="width:100%;padding:0.5rem;border:1px solid var(--border-color);border-radius:6px;background:var(--card-bg);color:var(--text-primary)">
<option value="">Cargando proveedores...</option>
</select>
</div>
<div class="form-field-compact">
<label>Ref. proveedor *</label>
<input type="text" id="sup-ref-supplier" required placeholder="Ej: FAC-2024-001"
style="width:100%;padding:0.5rem;border:1px solid var(--border-color);border-radius:6px;background:var(--bg-primary);color:var(--text-primary)" />
</div>
<div class="form-field-compact">
<label>Fecha *</label>
<input type="date" id="sup-date" required
style="width:100%;padding:0.5rem;border:1px solid var(--border-color);border-radius:6px;background:var(--bg-primary);color:var(--text-primary)" />
</div>
<div class="form-field-compact">
<label>Vencimiento</label>
<input type="date" id="sup-expire"
style="width:100%;padding:0.5rem;border:1px solid var(--border-color);border-radius:6px;background:var(--bg-primary);color:var(--text-primary)" />
</div>
<div class="form-field-compact">
<label>Nota pública</label>
<input type="text" id="sup-note-public" placeholder="Opcional"
style="width:100%;padding:0.5rem;border:1px solid var(--border-color);border-radius:6px;background:var(--bg-primary);color:var(--text-primary)" />
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
<h3 class="lines-section-title" style="margin:0">Líneas *</h3>
<button type="button" class="btn-add-compact" id="sup-add-line">+ Añadir línea</button>
</div>
<div id="sup-lines-container"></div>
<div class="form-actions-compact" style="margin-top:1.5rem">
<button type="button" class="btn-cancel-compact" id="sup-create-cancel">Cancelar</button>
<button type="submit" class="btn-submit-compact" id="sup-create-submit">Crear factura</button>
</div>
</form>
</div>
</div>
</div>
`;
const tbody = container.querySelector('#sup-tbody');
const searchInput = container.querySelector('#sup-search');
const filterSelect = container.querySelector('#sup-filter');
const summary = container.querySelector('#sup-summary');
const overlay = container.querySelector('#sup-detail-overlay');
const detailBody = container.querySelector('#sup-detail-body');
const detailTitle = container.querySelector('#sup-detail-title');
container.querySelector('#sup-detail-close').addEventListener('click', closeDetail);
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDetail(); });
function applyFilters() {
filteredInvoices = allInvoices.filter(inv => {
const matchStatus = !currentFilter || inv.status === currentFilter;
const q = searchTerm.toLowerCase();
const matchSearch = !q
|| (inv.number || '').toLowerCase().includes(q)
|| (inv.supplierRef || '').toLowerCase().includes(q)
|| (inv.supplierName || '').toLowerCase().includes(q);
return matchStatus && matchSearch;
});
renderTable();
renderSummary();
}
function renderTable() {
if (filteredInvoices.length === 0) {
tbody.innerHTML = `<tr><td colspan="7" class="facturas-empty">No hay facturas de proveedores</td></tr>`;
return;
}
tbody.innerHTML = filteredInvoices.map(inv => `
<tr class="invoice-item" data-id="${inv.id}">
<td class="invoice-number">
<button class="btn-invoice-num" data-id="${inv.id}">${escHtml(inv.number)}</button>
</td>
<td class="invoice-status">
<span class="status-badge ${STATUS_CLASS[inv.status] || 'status-draft'}">
${STATUS_TEXT[inv.status] || inv.status}
</span>
</td>
<td class="invoice-client">
<span class="client-icon">${icons.user}</span>
${escHtml(inv.supplierName || 'Sin nombre')}
</td>
<td class="invoice-date">${fmtDate(inv.date)}</td>
<td class="invoice-total amount-positive">${fmt(inv.total)}</td>
<td class="invoice-remain ${parseFloat(inv.remainToPay) > 0.009 ? 'amount-pending' : 'amount-paid'}">
${fmt(inv.remainToPay)}
</td>
<td class="invoice-actions">
<button class="btn-action btn-view" data-id="${inv.id}" title="Ver detalle">
${icons.eye}
</button>
</td>
</tr>
`).join('');
tbody.querySelectorAll('[data-id]').forEach(el => {
el.addEventListener('click', () => openDetail(Number(el.dataset.id)));
});
}
function renderSummary() {
if (filteredInvoices.length === 0) { summary.style.display = 'none'; return; }
summary.style.display = 'flex';
const totalHt = filteredInvoices.reduce((s, i) => s + (i.totalHt || 0), 0);
const totalTax = filteredInvoices.reduce((s, i) => s + (i.totalTax || 0), 0);
const total = filteredInvoices.reduce((s, i) => s + (i.total || 0), 0);
const pending = filteredInvoices.reduce((s, i) => s + (i.remainToPay || 0), 0);
container.querySelector('#sum-count').textContent = filteredInvoices.length;
container.querySelector('#sum-ht').textContent = fmt(totalHt);
container.querySelector('#sum-tax').textContent = fmt(totalTax);
container.querySelector('#sum-total').textContent = fmt(total);
container.querySelector('#sum-pending').textContent = fmt(pending);
}
async function openDetail(id) {
overlay.style.display = 'flex';
detailTitle.textContent = 'Cargando...';
detailBody.innerHTML = '<div class="loading">Cargando detalle...</div>';
payments = [];
// Load payment types once per page instance
if (paymentTypes.length === 0) {
try { paymentTypes = await getPaymentTypes(); } catch { paymentTypes = []; }
}
try {
const inv = await getSupplierInvoiceById(id);
currentInvoice = inv;
detailTitle.textContent = `Factura ${inv.number}`;
await loadPayments(id);
renderDetail(inv);
} catch (err) {
detailBody.innerHTML = `<div class="error">No se pudo cargar el detalle: ${escHtml(err.message)}</div>`;
}
}
async function refreshDetail() {
if (!currentInvoice) return;
try {
const inv = await getSupplierInvoiceById(currentInvoice.id);
currentInvoice = inv;
detailTitle.textContent = `Factura ${inv.number}`;
await loadPayments(inv.id);
renderDetail(inv);
// Refresh list row too
const idx = allInvoices.findIndex(x => x.id === inv.id);
if (idx >= 0) allInvoices[idx] = { ...allInvoices[idx], ...inv };
applyFilters();
} catch (err) {
showToast(`Error al recargar: ${err.message}`, 'error');
}
}
async function loadPayments(id) {
try {
payments = await getSupplierInvoicePayments(id);
} catch {
payments = [];
}
}
function renderDetail(inv) {
const isDraft = inv.status === 'draft';
const isUnpaid = inv.status === 'unpaid';
const lines = inv.lines || [];
detailBody.innerHTML = /*html*/`
<!-- Header info -->
<div class="invoice-detail-grid" id="sup-detail-info">
<div class="detail-row">
<span class="detail-label">Proveedor</span>
<span class="detail-value">${escHtml(inv.supplierName || '—')}</span>
</div>
<div class="detail-row">
<span class="detail-label">Ref. interna</span>
<span class="detail-value">${escHtml(inv.number || '—')}</span>
</div>
<div class="detail-row">
<span class="detail-label">Ref. proveedor</span>
<span class="detail-value" id="sup-d-ref">${escHtml(inv.supplierRef || '—')}</span>
</div>
<div class="detail-row">
<span class="detail-label">Estado</span>
<span class="detail-value">
<span class="status-badge ${STATUS_CLASS[inv.status] || 'status-draft'}">
${STATUS_TEXT[inv.status] || inv.status}
</span>
</span>
</div>
<div class="detail-row">
<span class="detail-label">Fecha</span>
<span class="detail-value">${fmtDate(inv.date)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Vencimiento</span>
<span class="detail-value" id="sup-d-expire">${fmtDate(inv.expireDate)}</span>
</div>
${inv.notePublic ? `<div class="detail-row full-width"><span class="detail-label">Nota pública</span><span class="detail-value" id="sup-d-note">${escHtml(inv.notePublic)}</span></div>` : ''}
</div>
<!-- Actions bar -->
<div style="display:flex;flex-wrap:wrap;gap:0.5rem;margin:1rem 0">
${isDraft ? `<button class="btn-submit-compact" id="sup-btn-validate">Validar factura</button>` : ''}
${isUnpaid ? `<button class="btn-submit-compact" id="sup-btn-paid" style="background:var(--success,#5dd39e);color:#000">Marcar como pagada</button>` : ''}
${inv.status === 'paid' || isUnpaid ? `<button class="btn-cancel-compact" id="sup-btn-draft">Volver a borrador</button>` : ''}
${isDraft ? `<button class="btn-cancel-compact" id="sup-btn-edit">Editar</button>` : ''}
${isDraft ? `<button class="btn-cancel-compact" id="sup-btn-delete" style="color:var(--danger)">Eliminar</button>` : ''}
</div>
<!-- Lines -->
<h3 class="lines-section-title">Líneas</h3>
<div class="invoice-lines-table-wrap">
<table class="invoice-lines-table">
<thead>
<tr>
<th>Descripción</th>
<th class="text-right">Cant.</th>
<th class="text-right">P. Unit.</th>
<th class="text-right">IVA %</th>
<th class="text-right">Total</th>
${isDraft ? '<th></th>' : ''}
</tr>
</thead>
<tbody id="sup-lines-tbody">
${lines.length === 0
? `<tr><td colspan="${isDraft ? 6 : 5}" class="no-lines">Sin líneas</td></tr>`
: lines.map(l => `
<tr data-line-id="${l.id}">
<td>${escHtml(l.description || '—')}</td>
<td class="text-right">${l.quantity}</td>
<td class="text-right">${fmt(l.unitPrice)}</td>
<td class="text-right">${l.taxRate}%</td>
<td class="text-right amount-positive">${fmt(l.total)}</td>
${isDraft ? `<td style="white-space:nowrap">
<button class="btn-action" data-edit-line="${l.id}" title="Editar">${icons.eye}</button>
<button class="btn-action" data-del-line="${l.id}" title="Eliminar" style="color:var(--danger)">${icons.close}</button>
</td>` : ''}
</tr>
`).join('')}
</tbody>
</table>
</div>
${isDraft ? `
<div id="sup-add-line-form" style="margin-top:0.75rem;display:none">
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center">
<input type="text" id="nl-desc" placeholder="Descripción *" style="flex:2;min-width:140px;padding:0.4rem 0.5rem;border:1px solid var(--border-color);border-radius:5px;background:var(--bg-primary);color:var(--text-primary)" />
<input type="number" id="nl-qty" placeholder="Cant." value="1" min="0.01" step="0.01" style="width:70px;padding:0.4rem 0.5rem;border:1px solid var(--border-color);border-radius:5px;background:var(--bg-primary);color:var(--text-primary)" />
<input type="number" id="nl-price" placeholder="P.Unit." min="0" step="0.01" style="width:90px;padding:0.4rem 0.5rem;border:1px solid var(--border-color);border-radius:5px;background:var(--bg-primary);color:var(--text-primary)" />
<input type="number" id="nl-tax" placeholder="IVA%" value="21" min="0" max="100" step="0.01" style="width:70px;padding:0.4rem 0.5rem;border:1px solid var(--border-color);border-radius:5px;background:var(--bg-primary);color:var(--text-primary)" />
<button type="button" class="btn-submit-compact" id="nl-save">Añadir</button>
<button type="button" class="btn-cancel-compact" id="nl-cancel">Cancelar</button>
</div>
</div>
<button type="button" class="btn-add-compact" id="sup-show-add-line" style="margin-top:0.5rem">+ Añadir línea</button>
` : ''}
<!-- Totals -->
<div class="invoice-totals">
${inv.totalHt != null ? `<div class="total-row subtotal"><span>Base imponible:</span><strong>${fmt(inv.totalHt)}</strong></div>` : ''}
${inv.totalTax != null ? `<div class="total-row subtotal"><span>IVA soportado:</span><strong>${fmt(inv.totalTax)}</strong></div>` : ''}
<div class="total-row"><span>Total:</span><strong class="amount-positive">${fmt(inv.total)}</strong></div>
<div class="total-row paid"><span>Pagado:</span><strong class="amount-paid">${fmt((inv.total || 0) - (inv.remainToPay || 0))}</strong></div>
<div class="total-row pending"><span>Pendiente:</span><strong class="amount-pending">${fmt(inv.remainToPay)}</strong></div>
</div>
<!-- Payments -->
<h3 class="lines-section-title" style="margin-top:1.5rem">Pagos</h3>
<div class="invoice-lines-table-wrap">
<table class="invoice-lines-table">
<thead>
<tr>
<th>Fecha</th>
<th>Referencia</th>
<th>Tipo</th>
<th class="text-right">Importe</th>
</tr>
</thead>
<tbody id="sup-payments-tbody">
${payments.length === 0
? `<tr><td colspan="4" class="no-lines">Sin pagos registrados</td></tr>`
: payments.map(p => `
<tr>
<td>${p.paymentDate ? new Date(p.paymentDate).toLocaleDateString('es-ES') : '—'}</td>
<td>${escHtml(p.ref || '—')}</td>
<td>${escHtml(p.type || '—')}</td>
<td class="text-right amount-paid">${fmt(p.amount)}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
${isUnpaid ? `
<div id="sup-pay-form" style="margin-top:1rem;display:none">
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center">
<input type="date" id="pay-date" style="padding:0.4rem 0.5rem;border:1px solid var(--border-color);border-radius:5px;background:var(--bg-primary);color:var(--text-primary)" />
<input type="number" id="pay-amount" placeholder="Importe (vacío=total)" min="0.01" step="0.01" style="width:130px;padding:0.4rem 0.5rem;border:1px solid var(--border-color);border-radius:5px;background:var(--bg-primary);color:var(--text-primary)" />
<select id="pay-type" style="padding:0.4rem 0.5rem;border:1px solid var(--border-color);border-radius:5px;background:var(--card-bg);color:var(--text-primary)">
${paymentTypes.length
? paymentTypes.map(t => `<option value="${t.id}">${escHtml(t.label || t.code || t.id)}</option>`).join('')
: `<option value="6">Transferencia</option><option value="4">Cheque</option><option value="7">Efectivo</option>`}
</select>
<button type="button" class="btn-submit-compact" id="pay-save">Registrar pago</button>
<button type="button" class="btn-cancel-compact" id="pay-cancel">Cancelar</button>
</div>
</div>
<button type="button" class="btn-add-compact" id="sup-show-pay-form" style="margin-top:0.5rem">+ Registrar pago</button>
` : ''}
`;
// Wire up status buttons
detailBody.querySelector('#sup-btn-validate')?.addEventListener('click', () => changeStatus('unpaid'));
detailBody.querySelector('#sup-btn-paid')?.addEventListener('click', () => changeStatus('paid'));
detailBody.querySelector('#sup-btn-draft')?.addEventListener('click', () => changeStatus('draft'));
detailBody.querySelector('#sup-btn-delete')?.addEventListener('click', handleDelete);
detailBody.querySelector('#sup-btn-edit')?.addEventListener('click', () => renderEditForm(inv));
// Wire up line actions
detailBody.querySelectorAll('[data-del-line]').forEach(btn => {
btn.addEventListener('click', () => handleDeleteLine(parseInt(btn.dataset.delLine)));
});
detailBody.querySelectorAll('[data-edit-line]').forEach(btn => {
btn.addEventListener('click', () => openEditLineForm(parseInt(btn.dataset.editLine), inv));
});
// Add line form
const showAddLine = detailBody.querySelector('#sup-show-add-line');
const addLineForm = detailBody.querySelector('#sup-add-line-form');
showAddLine?.addEventListener('click', () => {
addLineForm.style.display = 'flex';
showAddLine.style.display = 'none';
detailBody.querySelector('#nl-desc').focus();
});
detailBody.querySelector('#nl-cancel')?.addEventListener('click', () => {
addLineForm.style.display = 'none';
showAddLine.style.display = '';
});
detailBody.querySelector('#nl-save')?.addEventListener('click', handleAddLine);
// Payment form
const showPayForm = detailBody.querySelector('#sup-show-pay-form');
const payForm = detailBody.querySelector('#sup-pay-form');
showPayForm?.addEventListener('click', () => {
payForm.style.display = 'flex';
showPayForm.style.display = 'none';
const dateInput = detailBody.querySelector('#pay-date');
if (dateInput) dateInput.value = new Date().toISOString().split('T')[0];
});
detailBody.querySelector('#pay-cancel')?.addEventListener('click', () => {
payForm.style.display = 'none';
showPayForm.style.display = '';
});
detailBody.querySelector('#pay-save')?.addEventListener('click', handleAddPayment);
}
function renderEditForm(inv) {
const editSection = detailBody.querySelector('#sup-detail-info');
if (!editSection) return;
const actionsBar = detailBody.querySelector('div[style*="flex-wrap"]');
// Replace info grid with edit form
const form = document.createElement('form');
form.id = 'sup-edit-form';
form.innerHTML = /*html*/`
<div class="invoice-detail-grid" style="margin-bottom:1rem">
<div class="form-field-compact">
<label>Ref. proveedor</label>
<input type="text" id="ed-ref" value="${escHtml(inv.supplierRef || '')}"
style="width:100%;padding:0.5rem;border:1px solid var(--border-color);border-radius:6px;background:var(--bg-primary);color:var(--text-primary)" />
</div>
<div class="form-field-compact">
<label>Vencimiento</label>
<input type="date" id="ed-expire" value="${fmtDateInput(inv.expireDate)}"
style="width:100%;padding:0.5rem;border:1px solid var(--border-color);border-radius:6px;background:var(--bg-primary);color:var(--text-primary)" />
</div>
<div class="form-field-compact full-width" style="grid-column:1/-1">
<label>Nota pública</label>
<input type="text" id="ed-note" value="${escHtml(inv.notePublic || '')}"
style="width:100%;padding:0.5rem;border:1px solid var(--border-color);border-radius:6px;background:var(--bg-primary);color:var(--text-primary)" />
</div>
</div>
<div class="form-actions-compact">
<button type="button" class="btn-cancel-compact" id="ed-cancel">Cancelar</button>
<button type="submit" class="btn-submit-compact" id="ed-save">Guardar cambios</button>
</div>
`;
detailBody.querySelector('#sup-detail-info').replaceWith(form);
if (actionsBar) actionsBar.style.display = 'none';
form.querySelector('#ed-cancel').addEventListener('click', () => renderDetail(inv));
form.addEventListener('submit', async (e) => {
e.preventDefault();
const saveBtn = form.querySelector('#ed-save');
saveBtn.disabled = true;
saveBtn.textContent = 'Guardando...';
try {
await updateSupplierInvoice(inv.id, {
supplierRef: form.querySelector('#ed-ref').value.trim() || undefined,
expireDate: form.querySelector('#ed-expire').value || undefined,
notePublic: form.querySelector('#ed-note').value.trim() || undefined,
});
showToast('Factura actualizada', 'success');
await refreshDetail();
} catch (err) {
showToast(`Error: ${err.message}`, 'error');
saveBtn.disabled = false;
saveBtn.textContent = 'Guardar cambios';
}
});
}
async function changeStatus(status) {
try {
await changeSupplierInvoiceStatus(currentInvoice.id, status);
showToast(`Estado actualizado a ${STATUS_TEXT[status] || status}`, 'success');
await refreshDetail();
} catch (err) {
showToast(`Error: ${err.message}`, 'error');
}
}
async function handleDelete() {
if (!confirm(`¿Eliminar la factura ${currentInvoice?.number}? Esta acción no se puede deshacer.`)) return;
try {
await deleteSupplierInvoice(currentInvoice.id);
showToast('Factura eliminada', 'success');
closeDetail();
allInvoices = allInvoices.filter(i => i.id !== currentInvoice.id);
applyFilters();
} catch (err) {
showToast(`Error: ${err.message}`, 'error');
}
}
async function handleAddLine() {
const desc = detailBody.querySelector('#nl-desc').value.trim();
const qty = parseFloat(detailBody.querySelector('#nl-qty').value) || 1;
const price = parseFloat(detailBody.querySelector('#nl-price').value) || 0;
const tax = parseFloat(detailBody.querySelector('#nl-tax').value) || 0;
if (!desc) { showToast('La descripción es obligatoria', 'error'); return; }
const saveBtn = detailBody.querySelector('#nl-save');
saveBtn.disabled = true;
try {
await addSupplierInvoiceLine(currentInvoice.id, {
description: desc, quantity: qty, unitPrice: price, taxRate: tax
});
showToast('Línea añadida', 'success');
await refreshDetail();
} catch (err) {
showToast(`Error: ${err.message}`, 'error');
saveBtn.disabled = false;
}
}
function openEditLineForm(lineId, inv) {
const line = inv.lines?.find(l => l.id === lineId);
if (!line) return;
const row = detailBody.querySelector(`[data-line-id="${lineId}"]`);
if (!row) return;
row.innerHTML = /*html*/`
<td><input type="text" value="${escHtml(line.description || '')}" class="el-desc" style="width:100%;padding:0.3rem;border:1px solid var(--border-color);border-radius:4px;background:var(--bg-primary);color:var(--text-primary)" /></td>
<td><input type="number" value="${line.quantity}" class="el-qty" min="0.01" step="0.01" style="width:70px;padding:0.3rem;border:1px solid var(--border-color);border-radius:4px;background:var(--bg-primary);color:var(--text-primary)" /></td>
<td><input type="number" value="${line.unitPrice}" class="el-price" min="0" step="0.01" style="width:90px;padding:0.3rem;border:1px solid var(--border-color);border-radius:4px;background:var(--bg-primary);color:var(--text-primary)" /></td>
<td><input type="number" value="${line.taxRate}" class="el-tax" min="0" max="100" step="0.01" style="width:70px;padding:0.3rem;border:1px solid var(--border-color);border-radius:4px;background:var(--bg-primary);color:var(--text-primary)" /></td>
<td></td>
<td style="white-space:nowrap">
<button class="btn-submit-compact el-save" style="padding:0.25rem 0.5rem;font-size:0.8rem">OK</button>
<button class="btn-cancel-compact el-cancel" style="padding:0.25rem 0.5rem;font-size:0.8rem">✕</button>
</td>
`;
row.querySelector('.el-cancel').addEventListener('click', () => renderDetail(inv));
row.querySelector('.el-save').addEventListener('click', async () => {
const btn = row.querySelector('.el-save');
btn.disabled = true;
try {
await updateSupplierInvoiceLine(currentInvoice.id, lineId, {
description: row.querySelector('.el-desc').value.trim() || undefined,
quantity: parseFloat(row.querySelector('.el-qty').value) || undefined,
unitPrice: parseFloat(row.querySelector('.el-price').value) || undefined,
taxRate: parseFloat(row.querySelector('.el-tax').value) ?? undefined,
});
showToast('Línea actualizada', 'success');
await refreshDetail();
} catch (err) {
showToast(`Error: ${err.message}`, 'error');
btn.disabled = false;
}
});
}
async function handleDeleteLine(lineId) {
if (!confirm('¿Eliminar esta línea?')) return;
try {
await deleteSupplierInvoiceLine(currentInvoice.id, lineId);
showToast('Línea eliminada', 'success');
await refreshDetail();
} catch (err) {
showToast(`Error: ${err.message}`, 'error');
}
}
async function handleAddPayment() {
const date = detailBody.querySelector('#pay-date').value;
const amountRaw = detailBody.querySelector('#pay-amount').value;
const paymentModeId = parseInt(detailBody.querySelector('#pay-type').value) || 6;
if (!date) { showToast('Selecciona una fecha de pago', 'error'); return; }
const saveBtn = detailBody.querySelector('#pay-save');
saveBtn.disabled = true;
try {
await addSupplierInvoicePayment(currentInvoice.id, {
paymentDate: date,
amount: amountRaw ? parseFloat(amountRaw) : undefined,
paymentModeId,
closePaidInvoices: 'yes',
accountId: 1,
});
showToast('Pago registrado', 'success');
await refreshDetail();
} catch (err) {
showToast(`Error: ${err.message}`, 'error');
saveBtn.disabled = false;
}
}
function closeDetail() {
overlay.style.display = 'none';
currentInvoice = null;
}
async function load() {
try {
allInvoices = await getSupplierInvoices({ limit: 200 });
applyFilters();
} catch (err) {
tbody.innerHTML = `<tr><td colspan="7" class="facturas-empty">Error al cargar: ${escHtml(err.message)}</td></tr>`;
showToast('Error al cargar facturas de proveedores', 'error');
}
}
searchInput.addEventListener('input', () => { searchTerm = searchInput.value; applyFilters(); });
filterSelect.addEventListener('change', () => { currentFilter = filterSelect.value; applyFilters(); });
// ===== CREAR FACTURA =====
const createOverlay = container.querySelector('#sup-create-overlay');
const supplierSelect = container.querySelector('#sup-supplier-select');
const linesContainer = container.querySelector('#sup-lines-container');
let lineCount = 0;
function addLine() {
lineCount++;
const row = document.createElement('div');
row.className = 'line-row-compact';
row.dataset.line = lineCount;
row.innerHTML = `
<input type="text" placeholder="Descripción *" class="line-desc" required style="flex:2;padding:0.4rem 0.5rem;border:1px solid var(--border-color);border-radius:5px;background:var(--bg-primary);color:var(--text-primary)" />
<input type="number" placeholder="Cant." class="line-qty" value="1" min="0.01" step="0.01" required style="width:70px;padding:0.4rem 0.5rem;border:1px solid var(--border-color);border-radius:5px;background:var(--bg-primary);color:var(--text-primary)" />
<input type="number" placeholder="P.Unit." class="line-price" min="0" step="0.01" required style="width:90px;padding:0.4rem 0.5rem;border:1px solid var(--border-color);border-radius:5px;background:var(--bg-primary);color:var(--text-primary)" />
<input type="number" placeholder="IVA%" class="line-tax" value="21" min="0" max="100" step="0.01" required style="width:70px;padding:0.4rem 0.5rem;border:1px solid var(--border-color);border-radius:5px;background:var(--bg-primary);color:var(--text-primary)" />
<button type="button" class="btn-remove-line" title="Eliminar" style="background:none;border:none;color:var(--danger);cursor:pointer;font-size:1.1rem;padding:0 0.25rem">✕</button>
`;
row.querySelector('.btn-remove-line').addEventListener('click', () => {
row.remove();
if (linesContainer.children.length === 0) addLine();
});
linesContainer.appendChild(row);
}
function openCreateModal() {
createOverlay.style.display = 'flex';
linesContainer.innerHTML = '';
lineCount = 0;
container.querySelector('#sup-ref-supplier').value = '';
container.querySelector('#sup-date').value = new Date().toISOString().split('T')[0];
container.querySelector('#sup-expire').value = '';
container.querySelector('#sup-note-public').value = '';
addLine();
}
function closeCreate() {
createOverlay.style.display = 'none';
}
async function loadSuppliers() {
try {
const clients = await apiGet('/api/Clients?limit=500&supplier=true');
supplierSelect.innerHTML = '<option value="">Seleccionar proveedor...</option>';
clients.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.name;
supplierSelect.appendChild(opt);
});
} catch {
supplierSelect.innerHTML = '<option value="">Error al cargar</option>';
}
}
container.querySelector('#btn-new-sup').addEventListener('click', () => {
openCreateModal();
loadSuppliers();
});
container.querySelector('#sup-create-close').addEventListener('click', closeCreate);
container.querySelector('#sup-create-cancel').addEventListener('click', closeCreate);
createOverlay.addEventListener('click', (e) => { if (e.target === createOverlay) closeCreate(); });
container.querySelector('#sup-add-line').addEventListener('click', addLine);
container.querySelector('#sup-create-form').addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = container.querySelector('#sup-create-submit');
const supplierId = parseInt(supplierSelect.value);
if (!supplierId) { showToast('Selecciona un proveedor', 'error'); return; }
const lines = [...linesContainer.querySelectorAll('.line-row-compact')].map(row => ({
description: row.querySelector('.line-desc').value.trim(),
quantity: parseFloat(row.querySelector('.line-qty').value) || 1,
unitPrice: parseFloat(row.querySelector('.line-price').value) || 0,
taxRate: parseFloat(row.querySelector('.line-tax').value) || 0,
}));
if (lines.some(l => !l.description)) { showToast('Rellena la descripción de todas las líneas', 'error'); return; }
submitBtn.disabled = true;
submitBtn.textContent = 'Creando...';
try {
await createSupplierInvoice({
supplierId,
supplierRef: container.querySelector('#sup-ref-supplier').value.trim(),
date: container.querySelector('#sup-date').value,
expireDate: container.querySelector('#sup-expire').value || null,
notePublic: container.querySelector('#sup-note-public').value.trim() || null,
lines
});
showToast('Factura de proveedor creada', 'success');
closeCreate();
allInvoices = await getSupplierInvoices({ limit: 200 });
applyFilters();
} catch (err) {
showToast(`Error: ${err.message}`, 'error');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Crear factura';
}
});
load();
return container;
}