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*/`

Facturas de Proveedores

${icons.search}
Número Estado Proveedor Fecha Total Pendiente Acciones
Cargando...
`; 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 = `No hay facturas de proveedores`; return; } tbody.innerHTML = filteredInvoices.map(inv => ` ${STATUS_TEXT[inv.status] || inv.status} ${icons.user} ${escHtml(inv.supplierName || 'Sin nombre')} ${fmtDate(inv.date)} ${fmt(inv.total)} ${fmt(inv.remainToPay)} `).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 = '
Cargando detalle...
'; 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 = `
No se pudo cargar el detalle: ${escHtml(err.message)}
`; } } 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*/`
Proveedor ${escHtml(inv.supplierName || '—')}
Ref. interna ${escHtml(inv.number || '—')}
Ref. proveedor ${escHtml(inv.supplierRef || '—')}
Estado ${STATUS_TEXT[inv.status] || inv.status}
Fecha ${fmtDate(inv.date)}
Vencimiento ${fmtDate(inv.expireDate)}
${inv.notePublic ? `
Nota pública${escHtml(inv.notePublic)}
` : ''}
${isDraft ? `` : ''} ${isUnpaid ? `` : ''} ${inv.status === 'paid' || isUnpaid ? `` : ''} ${isDraft ? `` : ''} ${isDraft ? `` : ''}

Líneas

${isDraft ? '' : ''} ${lines.length === 0 ? `` : lines.map(l => ` ${isDraft ? `` : ''} `).join('')}
Descripción Cant. P. Unit. IVA % Total
Sin líneas
${escHtml(l.description || '—')} ${l.quantity} ${fmt(l.unitPrice)} ${l.taxRate}% ${fmt(l.total)}
${isDraft ? ` ` : ''}
${inv.totalHt != null ? `
Base imponible:${fmt(inv.totalHt)}
` : ''} ${inv.totalTax != null ? `
IVA soportado:${fmt(inv.totalTax)}
` : ''}
Total:${fmt(inv.total)}
Pendiente:${fmt(inv.remainToPay)}

Pagos

${payments.length === 0 ? `` : payments.map(p => ` `).join('')}
Fecha Referencia Tipo Importe
Sin pagos registrados
${p.paymentDate ? new Date(p.paymentDate).toLocaleDateString('es-ES') : '—'} ${escHtml(p.ref || '—')} ${escHtml(p.type || '—')} ${fmt(p.amount)}
${isUnpaid ? ` ` : ''} `; // 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*/`
`; 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*/` `; 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 = `Error al cargar: ${escHtml(err.message)}`; 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 = ` `; 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 = ''; clients.forEach(c => { const opt = document.createElement('option'); opt.value = c.id; opt.textContent = c.name; supplierSelect.appendChild(opt); }); } catch { supplierSelect.innerHTML = ''; } } 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; }