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*/`
0 seleccionadas
Facturas
—
Total facturado
—
Pagado
—
Pendiente
—
`;
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 facturasCrea 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 cargarNo 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 += `
| Trimestre |
Facturas |
Base imponible |
IVA |
Total |
Cobrado |
Pendiente |
${yearRows.map(r => {
return `
| T${r.q} · ${qLabel(r.q)} |
${r.count} |
${fmt(r.ht)} |
${fmt(r.tax)} |
${fmt(r.total)} |
${fmt(r.paid)} |
${fmt(r.pending)} |
`;
}).join('')}
`;
});
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;
}