567 lines
22 KiB
JavaScript
Executable File
567 lines
22 KiB
JavaScript
Executable File
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*/`
|
|
<div class="facturas-header">
|
|
<h1>Facturas</h1>
|
|
</div>
|
|
|
|
<div class="facturas-filters">
|
|
<div class="search-wrapper">
|
|
<span class="search-icon">${icons.search}</span>
|
|
<input type="search" placeholder="Buscar facturas..." class="search-input search-input--with-icon" />
|
|
</div>
|
|
<select class="filter-status">
|
|
<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-quarterly" id="btn-quarterly">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
|
Resumen trimestral
|
|
</button>
|
|
<button class="btn-new-invoice">${icons.plus} <span>Nueva Factura</span></button>
|
|
</div>
|
|
|
|
<div class="quarterly-view" id="quarterly-view" style="display:none">
|
|
<div class="quarterly-table-wrap" id="quarterly-content">
|
|
<p class="quarterly-loading">Calculando...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bulk-action-bar" id="bulk-bar" style="display:none">
|
|
<span class="bulk-count" id="bulk-count">0 seleccionadas</span>
|
|
<div class="bulk-actions">
|
|
<button class="btn-bulk btn-bulk-duplicate" id="btn-duplicate">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
Duplicar
|
|
</button>
|
|
<button class="btn-bulk btn-bulk-delete" id="btn-bulk-delete">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
|
Eliminar
|
|
</button>
|
|
<button class="btn-bulk btn-bulk-cancel" id="btn-deselect">✕ Cancelar selección</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="invoices-table-container">
|
|
<table class="invoices-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="invoice-select-col">
|
|
<input type="checkbox" class="invoice-check-all" id="check-all" title="Seleccionar todo" />
|
|
</th>
|
|
<th class="invoice-number sortable" data-sort="number">
|
|
Número <span class="sort-icon"></span>
|
|
</th>
|
|
<th class="invoice-status sortable" data-sort="status">
|
|
Estado <span class="sort-icon"></span>
|
|
</th>
|
|
<th class="invoice-client sortable" data-sort="clientName">
|
|
Cliente <span class="sort-icon"></span>
|
|
</th>
|
|
<th class="invoice-date sortable" data-sort="date">
|
|
Fecha <span class="sort-icon"></span>
|
|
</th>
|
|
<th class="invoice-total sortable" data-sort="total">
|
|
Total <span class="sort-icon"></span>
|
|
</th>
|
|
<th class="invoice-remain sortable" data-sort="remainToPay">
|
|
Pendiente <span class="sort-icon"></span>
|
|
</th>
|
|
<th class="invoice-actions">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="invoices-list">
|
|
<tr><td colspan="8" class="loading">Cargando facturas...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="invoices-summary" style="display:none">
|
|
<div class="summary-item">
|
|
<span class="summary-label">Facturas</span>
|
|
<span class="summary-value" data-summary="count">—</span>
|
|
</div>
|
|
<div class="summary-divider"></div>
|
|
<div class="summary-item">
|
|
<span class="summary-label">Total facturado</span>
|
|
<span class="summary-value" data-summary="total">—</span>
|
|
</div>
|
|
<div class="summary-item summary-item--paid">
|
|
<span class="summary-label">Pagado</span>
|
|
<span class="summary-value" data-summary="paid">—</span>
|
|
</div>
|
|
<div class="summary-item summary-item--pending">
|
|
<span class="summary-label">Pendiente</span>
|
|
<span class="summary-value" data-summary="pending">—</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pagination">
|
|
<button class="btn-pagination btn-prev">← Anterior</button>
|
|
<div class="pagination-info">
|
|
<span class="current-page">1</span> / <span class="total-pages">1</span>
|
|
</div>
|
|
<button class="btn-pagination btn-next">Siguiente →</button>
|
|
</div>
|
|
`;
|
|
|
|
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 = `<tr><td colspan="8"><div class="empty-state">${icons.emptyInvoices}<h3>No hay facturas</h3><p>Crea tu primera factura para comenzar.</p></div></td></tr>`;
|
|
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 = '<tr><td colspan="8" class="loading">Cargando facturas...</td></tr>';
|
|
|
|
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 = `<tr><td colspan="8"><div class="empty-state">${icons.error}<h3>Error al cargar</h3><p>No se pudieron cargar las facturas. Inténtalo de nuevo.</p></div></td></tr>`;
|
|
}
|
|
}
|
|
|
|
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 = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg> 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 = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> 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 = '<p class="quarterly-empty">No hay facturas para mostrar</p>';
|
|
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 += `
|
|
<div class="quarterly-year-block">
|
|
<div class="quarterly-year-header">
|
|
<span class="quarterly-year-label">${year}</span>
|
|
<span class="quarterly-year-total">${fmt(totals.total)} · ${totals.count} facturas</span>
|
|
</div>
|
|
<table class="quarterly-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Trimestre</th>
|
|
<th>Facturas</th>
|
|
<th>Base imponible</th>
|
|
<th>IVA</th>
|
|
<th>Total</th>
|
|
<th>Cobrado</th>
|
|
<th>Pendiente</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${yearRows.map(r => {
|
|
return `<tr>
|
|
<td class="quarterly-q-label">T${r.q} · <span>${qLabel(r.q)}</span></td>
|
|
<td class="quarterly-count">${r.count}</td>
|
|
<td class="quarterly-num">${fmt(r.ht)}</td>
|
|
<td class="quarterly-num quarterly-tax">${fmt(r.tax)}</td>
|
|
<td class="quarterly-num ${r.total > 0 ? 'quarterly-paid' : r.total < 0 ? 'quarterly-pending' : ''}">${fmt(r.total)}</td>
|
|
<td class="quarterly-num ${r.paid > 0 ? 'quarterly-paid' : r.paid < 0 ? 'quarterly-pending' : 'quarterly-zero'}"${r.paid < 0 ? ' title="Incluye facturas rectificativas (abonos)"' : ''}>${fmt(r.paid)}</td>
|
|
<td class="quarterly-num ${r.pending > 0.01 ? 'quarterly-pending' : 'quarterly-zero'}">${fmt(r.pending)}</td>
|
|
</tr>`;
|
|
}).join('')}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr class="quarterly-footer-row">
|
|
<td>Total ${year}</td>
|
|
<td>${totals.count}</td>
|
|
<td>${fmt(totals.ht)}</td>
|
|
<td>${fmt(totals.tax)}</td>
|
|
<td class="${totals.total > 0 ? 'quarterly-paid' : totals.total < 0 ? 'quarterly-pending' : ''}">${fmt(totals.total)}</td>
|
|
<td class="${totals.paid > 0 ? 'quarterly-paid' : totals.paid < 0 ? 'quarterly-pending' : 'quarterly-zero'}"${totals.paid < 0 ? ' title="Incluye facturas rectificativas (abonos)"' : ''}>${fmt(totals.paid)}</td>
|
|
<td class="${totals.pending > 0.01 ? 'quarterly-pending' : 'quarterly-zero'}">${fmt(totals.pending)}</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>`;
|
|
});
|
|
|
|
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 = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Cerrar resumen`;
|
|
renderQuarterlySummary();
|
|
} else {
|
|
view.style.display = 'none';
|
|
table.style.display = '';
|
|
btn.classList.remove('btn-quarterly--active');
|
|
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> 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;
|
|
} |