import { InvoiceItem } from '../components/InvoiceItem.js'; import { InvoiceModal } from '../components/InvoiceModal.js'; import { apiDelete, apiGet, apiPost } from '../services/apiClient.js'; import { icons } from '../services/icons.js'; import { showToast } from '../services/toast.js'; export function renderFacturasPage() { const container = document.createElement('div'); container.className = 'facturas-page page-enter'; let currentPage = 1; let totalPages = 1; const pageSize = 20; let allInvoices = []; let filteredInvoices = []; let currentFilter = ''; let searchTerm = ''; let sortKey = null; let sortDir = 'asc'; let selectedIds = new Set(); container.innerHTML = /*html*/`

Facturas

${icons.search}
Número Estado Cliente Fecha Total Pendiente Acciones
Cargando facturas...
`; const formatCurrency = (n) => new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(n || 0); function sortInvoices(invoices) { if (!sortKey) return invoices; return [...invoices].sort((a, b) => { let va = a[sortKey], vb = b[sortKey]; if (sortKey === 'total' || sortKey === 'remainToPay') { va = parseFloat(va) || 0; vb = parseFloat(vb) || 0; } else if (sortKey === 'date') { va = new Date(va).getTime() || 0; vb = new Date(vb).getTime() || 0; } else { va = String(va ?? '').toLowerCase(); vb = String(vb ?? '').toLowerCase(); } if (va < vb) return sortDir === 'asc' ? -1 : 1; if (va > vb) return sortDir === 'asc' ? 1 : -1; return 0; }); } function updateSortIcons() { container.querySelectorAll('th.sortable').forEach(th => { const key = th.dataset.sort; const icon = th.querySelector('.sort-icon'); if (key === sortKey) { icon.textContent = sortDir === 'asc' ? ' ↑' : ' ↓'; th.classList.add('sort-active'); } else { icon.textContent = ''; th.classList.remove('sort-active'); } }); } function updateSummary(invoices) { const summaryEl = container.querySelector('.invoices-summary'); if (!invoices.length) { summaryEl.style.display = 'none'; return; } const total = invoices.reduce((s, i) => s + (parseFloat(i.total) || 0), 0); const pending = invoices.reduce((s, i) => s + (parseFloat(i.remainToPay) || 0), 0); const paid = total - pending; container.querySelector('[data-summary="count"]').textContent = invoices.length; container.querySelector('[data-summary="total"]').textContent = formatCurrency(total); container.querySelector('[data-summary="paid"]').textContent = formatCurrency(paid); container.querySelector('[data-summary="pending"]').textContent = formatCurrency(pending); summaryEl.style.display = 'flex'; } function renderPage(page = 1) { const invoicesList = container.querySelector('.invoices-list'); currentPage = page; const sorted = sortInvoices(filteredInvoices); totalPages = Math.ceil(sorted.length / pageSize); const startIndex = (page - 1) * pageSize; const invoices = sorted.slice(startIndex, startIndex + pageSize); updatePaginationUI(); updateSortIcons(); updateSummary(filteredInvoices); invoicesList.innerHTML = ''; if (invoices.length === 0) { invoicesList.innerHTML = `
${icons.emptyInvoices}

No hay facturas

Crea tu primera factura para comenzar.

`; return; } invoices.forEach(invoice => { const invoiceItem = InvoiceItem(invoice, handleViewInvoice); // Restore checked state for already-selected rows const chk = invoiceItem.querySelector('.invoice-check'); if (chk && selectedIds.has(String(invoice.id))) chk.checked = true; invoicesList.appendChild(invoiceItem); }); // Sync select-all state syncSelectAll(); } function handleViewInvoice(invoiceId) { const modal = InvoiceModal(invoiceId, null, () => { loadAllInvoices(); }); document.body.appendChild(modal); } async function loadAllInvoices() { const invoicesList = container.querySelector('.invoices-list'); selectedIds.clear(); updateBulkBar(); try { invoicesList.innerHTML = 'Cargando facturas...'; let url = '/api/Invoices?limit=1000'; if (currentFilter) url += `&status=${currentFilter}`; if (searchTerm) url += `&search=${encodeURIComponent(searchTerm)}`; const data = await apiGet(url); allInvoices = Array.isArray(data) ? data : data.data || data.invoices || []; filteredInvoices = [...allInvoices].reverse(); renderPage(1); } catch (error) { console.error('Error al cargar facturas:', error); invoicesList.innerHTML = `
${icons.error}

Error al cargar

No se pudieron cargar las facturas. Inténtalo de nuevo.

`; } } function applyFilters() { loadAllInvoices(); } 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; paginationDiv.style.display = totalPages <= 1 ? 'none' : 'flex'; btnPrev.disabled = currentPage === 1; btnNext.disabled = currentPage === totalPages; } container.querySelectorAll('th.sortable').forEach(th => { th.style.cursor = 'pointer'; th.addEventListener('click', () => { const key = th.dataset.sort; if (sortKey === key) { sortDir = sortDir === 'asc' ? 'desc' : 'asc'; } else { sortKey = key; sortDir = 'asc'; } renderPage(1); }); }); // ── Bulk selection ────────────────────────────────────────────────── function updateBulkBar() { const bar = container.querySelector('#bulk-bar'); const count = container.querySelector('#bulk-count'); if (selectedIds.size > 0) { bar.style.display = 'flex'; count.textContent = `${selectedIds.size} seleccionada${selectedIds.size !== 1 ? 's' : ''}`; } else { bar.style.display = 'none'; } } function syncSelectAll() { const checkAll = container.querySelector('#check-all'); if (!checkAll) return; const pageCheckboxes = [...container.querySelectorAll('.invoice-check')]; if (pageCheckboxes.length === 0) { checkAll.checked = false; checkAll.indeterminate = false; return; } const checkedCount = pageCheckboxes.filter(c => c.checked).length; checkAll.checked = checkedCount === pageCheckboxes.length; checkAll.indeterminate = checkedCount > 0 && checkedCount < pageCheckboxes.length; } // Event delegation for row checkboxes container.querySelector('.invoices-list').addEventListener('change', (e) => { if (!e.target.classList.contains('invoice-check')) return; const id = String(e.target.value); if (e.target.checked) selectedIds.add(id); else selectedIds.delete(id); syncSelectAll(); updateBulkBar(); }); container.querySelector('#check-all').addEventListener('change', (e) => { const checkboxes = container.querySelectorAll('.invoice-check'); checkboxes.forEach(chk => { chk.checked = e.target.checked; if (e.target.checked) selectedIds.add(String(chk.value)); else selectedIds.delete(String(chk.value)); }); updateBulkBar(); }); container.querySelector('#btn-deselect').addEventListener('click', () => { selectedIds.clear(); container.querySelectorAll('.invoice-check').forEach(c => { c.checked = false; }); syncSelectAll(); updateBulkBar(); }); container.querySelector('#btn-bulk-delete').addEventListener('click', async () => { if (selectedIds.size === 0) return; const confirmed = window.confirm(`¿Eliminar ${selectedIds.size} factura${selectedIds.size !== 1 ? 's' : ''}? Esta acción no se puede deshacer.`); if (!confirmed) return; const btn = container.querySelector('#btn-bulk-delete'); btn.disabled = true; btn.textContent = 'Eliminando...'; let ok = 0, fail = 0; await Promise.allSettled([...selectedIds].map(async id => { try { await apiDelete(`/api/Invoices/${id}`); ok++; } catch { fail++; } })); btn.disabled = false; btn.innerHTML = ` Eliminar`; if (ok > 0) showToast(`${ok} factura${ok !== 1 ? 's' : ''} eliminada${ok !== 1 ? 's' : ''}`, 'success'); if (fail > 0) showToast(`${fail} factura${fail !== 1 ? 's no pudieron' : ' no pudo'} eliminarse`, 'error'); loadAllInvoices(); }); container.querySelector('#btn-duplicate').addEventListener('click', async () => { if (selectedIds.size === 0) return; const btn = container.querySelector('#btn-duplicate'); btn.disabled = true; btn.textContent = 'Duplicando...'; const today = new Date().toISOString().split('T')[0]; const expire = new Date(); expire.setDate(expire.getDate() + 30); const expireStr = expire.toISOString().split('T')[0]; let ok = 0, fail = 0; await Promise.allSettled([...selectedIds].map(async id => { try { const inv = await apiGet(`/api/Invoices/${id}`); const lines = (inv.lines || []).map(l => ({ description: l.description, quantity: l.quantity, unitPrice: l.unitPrice, taxRate: l.taxRate ?? l.tax ?? 0, })); await apiPost('/api/Invoices', { clientId: inv.clientId, date: today + 'T00:00:00', expireDate: expireStr + 'T00:00:00', notePublic: inv.notePublic || null, notePrivate: inv.notePrivate || null, lines, }); ok++; } catch { fail++; } })); btn.disabled = false; btn.innerHTML = ` Duplicar`; if (ok > 0) showToast(`${ok} factura${ok !== 1 ? 's' : ''} duplicada${ok !== 1 ? 's' : ''}`, 'success'); if (fail > 0) showToast(`${fail} factura${fail !== 1 ? 's no pudieron' : ' no pudo'} duplicarse`, 'error'); loadAllInvoices(); }); // ── Quarterly summary ───────────────────────────────────────────── let quarterlyVisible = false; function buildQuarterlySummary(invoices) { const quarters = {}; invoices.forEach(inv => { if (!inv.date) return; const d = new Date(inv.date); const year = d.getFullYear(); const q = Math.floor(d.getMonth() / 3) + 1; const key = `${year}-Q${q}`; if (!quarters[key]) quarters[key] = { year, q, ht: 0, tax: 0, total: 0, paid: 0, pending: 0, count: 0 }; const ttc = parseFloat(inv.total) || 0; quarters[key].ht += parseFloat(inv.totalHt) || 0; quarters[key].tax += parseFloat(inv.totalTax) || 0; quarters[key].total += ttc; if (inv.status === 'paid') quarters[key].paid += ttc; else if (inv.status === 'unpaid') quarters[key].pending += ttc; quarters[key].count += 1; }); return Object.values(quarters).sort((a, b) => b.year !== a.year ? b.year - a.year : b.q - a.q ); } function renderQuarterlySummary() { const content = container.querySelector('#quarterly-content'); const rows = buildQuarterlySummary(allInvoices); if (rows.length === 0) { content.innerHTML = '

No hay facturas para mostrar

'; return; } // Group by year const byYear = {}; rows.forEach(r => { if (!byYear[r.year]) byYear[r.year] = []; byYear[r.year].push(r); }); const fmt = (n) => new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(n); const qLabel = (q) => [`1er`, `2º`, `3er`, `4º`][q - 1] + ` trimestre`; let html = ''; Object.keys(byYear).sort((a,b) => b - a).forEach(year => { const yearRows = byYear[year]; const totals = yearRows.reduce((acc, r) => ({ ht: acc.ht + r.ht, tax: acc.tax + r.tax, total: acc.total + r.total, paid: acc.paid + r.paid, pending: acc.pending + r.pending, count: acc.count + r.count }), { ht: 0, tax: 0, total: 0, paid: 0, pending: 0, count: 0 }); html += `
${year} ${fmt(totals.total)} · ${totals.count} facturas
${yearRows.map(r => { return ``; }).join('')}
Trimestre Facturas Base imponible IVA Total Cobrado Pendiente
T${r.q} · ${qLabel(r.q)} ${r.count} ${fmt(r.ht)} ${fmt(r.tax)} ${fmt(r.total)} ${fmt(r.paid)} ${fmt(r.pending)}
`; }); content.innerHTML = html; } function toggleQuarterly() { quarterlyVisible = !quarterlyVisible; const view = container.querySelector('#quarterly-view'); const table = container.querySelector('.invoices-table-container'); const pag = container.querySelector('.pagination'); const summary = container.querySelector('.invoices-summary'); const btn = container.querySelector('#btn-quarterly'); const bulk = container.querySelector('#bulk-bar'); if (quarterlyVisible) { view.style.display = 'block'; table.style.display = 'none'; if (pag) pag.style.display = 'none'; if (summary) summary.style.display = 'none'; if (bulk) bulk.style.display = 'none'; btn.classList.add('btn-quarterly--active'); btn.innerHTML = ` Cerrar resumen`; renderQuarterlySummary(); } else { view.style.display = 'none'; table.style.display = ''; btn.classList.remove('btn-quarterly--active'); btn.innerHTML = ` Resumen trimestral`; updatePaginationUI(); updateSummary(filteredInvoices); } } container.querySelector('#btn-quarterly').addEventListener('click', toggleQuarterly); container.querySelector('.btn-new-invoice').addEventListener('click', () => { window.location.hash = '#create-invoice'; }); const searchInput = container.querySelector('.search-input'); searchInput.addEventListener('input', (e) => { const inputValue = e.target.value.trim(); clearTimeout(searchInput._debounceTimer); searchInput._debounceTimer = setTimeout(() => { searchTerm = /\d/.test(inputValue) ? inputValue : ''; applyFilters(); }, 500); }); container.querySelector('.filter-status').addEventListener('change', (e) => { currentFilter = e.target.value; applyFilters(); }); container.querySelector('.btn-prev').addEventListener('click', () => { if (currentPage > 1) { renderPage(currentPage - 1); scrollToTop(); } }); container.querySelector('.btn-next').addEventListener('click', () => { if (currentPage < totalPages) { renderPage(currentPage + 1); scrollToTop(); } }); function scrollToTop() { container.scrollIntoView({ behavior: 'smooth', block: 'start' }); } loadAllInvoices(); return container; }