// invoice-view.js — ES Module für die Invoice View // Features: Status Filter (all/unpaid/paid/overdue), Customer Filter, // Group by (none/week/month), Sortierung neueste zuerst, Mark Paid/Unpaid // ============================================================ // State // ============================================================ let invoices = []; let filterCustomer = ''; let filterStatus = 'unpaid'; // 'all' | 'unpaid' | 'paid' | 'overdue' let groupBy = 'none'; // 'none' | 'week' | 'month' const OVERDUE_DAYS = 30; // ============================================================ // Helpers // ============================================================ function formatDate(date) { const d = new Date(date); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); const year = d.getFullYear(); return `${month}/${day}/${year}`; } function daysSince(date) { const d = new Date(date); const now = new Date(); return Math.floor((now - d) / 86400000); } function getWeekNumber(date) { const d = new Date(date); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() + 3 - ((d.getDay() + 6) % 7)); const week1 = new Date(d.getFullYear(), 0, 4); return { year: d.getFullYear(), week: 1 + Math.round(((d - week1) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7) }; } function getWeekRange(year, weekNum) { const jan4 = new Date(year, 0, 4); const dayOfWeek = jan4.getDay() || 7; const monday = new Date(jan4); monday.setDate(jan4.getDate() - dayOfWeek + 1 + (weekNum - 1) * 7); const sunday = new Date(monday); sunday.setDate(monday.getDate() + 6); return { start: formatDate(monday), end: formatDate(sunday) }; } function getMonthName(monthIndex) { return ['January','February','March','April','May','June', 'July','August','September','October','November','December'][monthIndex]; } function isPaid(inv) { return !!inv.paid_date; } function isOverdue(inv) { return !isPaid(inv) && daysSince(inv.invoice_date) > OVERDUE_DAYS; } // ============================================================ // Data Loading // ============================================================ export async function loadInvoices() { try { const response = await fetch('/api/invoices'); invoices = await response.json(); renderInvoiceView(); } catch (error) { console.error('Error loading invoices:', error); } } export function getInvoicesData() { return invoices; } // ============================================================ // Filtering & Sorting & Grouping // ============================================================ function getFilteredInvoices() { let filtered = [...invoices]; // Status Filter if (filterStatus === 'unpaid') { filtered = filtered.filter(inv => !isPaid(inv)); } else if (filterStatus === 'paid') { filtered = filtered.filter(inv => isPaid(inv)); } else if (filterStatus === 'overdue') { filtered = filtered.filter(inv => isOverdue(inv)); } // 'all' → kein Filter // Customer Filter if (filterCustomer.trim()) { const search = filterCustomer.toLowerCase(); filtered = filtered.filter(inv => (inv.customer_name || '').toLowerCase().includes(search) ); } // Sortierung: neueste zuerst filtered.sort((a, b) => new Date(b.invoice_date) - new Date(a.invoice_date)); return filtered; } function groupInvoices(filtered) { if (groupBy === 'none') return null; const groups = new Map(); filtered.forEach(inv => { const d = new Date(inv.invoice_date); let key, label; if (groupBy === 'week') { const wk = getWeekNumber(inv.invoice_date); key = `${wk.year}-W${String(wk.week).padStart(2, '0')}`; const range = getWeekRange(wk.year, wk.week); label = `Week ${wk.week}, ${wk.year} (${range.start} – ${range.end})`; } else if (groupBy === 'month') { const month = d.getMonth(); const year = d.getFullYear(); key = `${year}-${String(month).padStart(2, '0')}`; label = `${getMonthName(month)} ${year}`; } if (!groups.has(key)) { groups.set(key, { label, invoices: [], total: 0 }); } const group = groups.get(key); group.invoices.push(inv); group.total += parseFloat(inv.total) || 0; }); // Innerhalb jeder Gruppe nochmal nach Datum sortieren (neueste zuerst) for (const group of groups.values()) { group.invoices.sort((a, b) => new Date(b.invoice_date) - new Date(a.invoice_date)); } // Gruppen nach Key sortieren (neueste zuerst) return new Map([...groups.entries()].sort((a, b) => b[0].localeCompare(a[0]))); } // ============================================================ // Rendering // ============================================================ function renderInvoiceRow(invoice) { const hasQbo = !!invoice.qbo_id; const paid = isPaid(invoice); const overdue = isOverdue(invoice); // QBO Button const qboButton = hasQbo ? `✓ QBO` : ``; // Paid/Unpaid Toggle Button const paidButton = paid ? `` : ``; // Status Badge let statusBadge = ''; if (paid) { statusBadge = `Paid`; } else if (overdue) { statusBadge = `Overdue`; } // Row styling const rowClass = paid ? 'bg-green-50/50' : overdue ? 'bg-red-50/50' : ''; return ` ${invoice.invoice_number} ${statusBadge} ${invoice.customer_name || 'N/A'} ${formatDate(invoice.invoice_date)} ${invoice.terms} $${parseFloat(invoice.total).toFixed(2)} ${qboButton} ${paidButton} `; } function renderGroupHeader(label) { return ` 📅 ${label} `; } function renderGroupFooter(total, count) { return ` Group Total (${count} invoices): $${total.toFixed(2)} `; } export function renderInvoiceView() { const tbody = document.getElementById('invoices-list'); if (!tbody) return; const filtered = getFilteredInvoices(); const groups = groupInvoices(filtered); let html = ''; let grandTotal = 0; if (groups) { for (const [key, group] of groups) { html += renderGroupHeader(group.label); group.invoices.forEach(inv => { html += renderInvoiceRow(inv); }); html += renderGroupFooter(group.total, group.invoices.length); grandTotal += group.total; } if (groups.size > 1) { html += ` Grand Total (${filtered.length} invoices): $${grandTotal.toFixed(2)} `; } } else { filtered.forEach(inv => { html += renderInvoiceRow(inv); grandTotal += parseFloat(inv.total) || 0; }); if (filtered.length > 0) { html += ` Total (${filtered.length} invoices): $${grandTotal.toFixed(2)} `; } } if (filtered.length === 0) { html = `No invoices found.`; } tbody.innerHTML = html; // Update count badge const countEl = document.getElementById('invoice-count'); if (countEl) countEl.textContent = filtered.length; // Update status button active states updateStatusButtons(); } function updateStatusButtons() { document.querySelectorAll('[data-status-filter]').forEach(btn => { const status = btn.getAttribute('data-status-filter'); if (status === filterStatus) { btn.classList.remove('bg-white', 'text-gray-600'); btn.classList.add('bg-blue-600', 'text-white'); } else { btn.classList.remove('bg-blue-600', 'text-white'); btn.classList.add('bg-white', 'text-gray-600'); } }); // Update overdue count badge const overdueCount = invoices.filter(inv => isOverdue(inv)).length; const overdueBadge = document.getElementById('overdue-badge'); if (overdueBadge) { if (overdueCount > 0) { overdueBadge.textContent = overdueCount; overdueBadge.classList.remove('hidden'); } else { overdueBadge.classList.add('hidden'); } } // Update unpaid count const unpaidCount = invoices.filter(inv => !isPaid(inv)).length; const unpaidBadge = document.getElementById('unpaid-badge'); if (unpaidBadge) { unpaidBadge.textContent = unpaidCount; } } // ============================================================ // Toolbar HTML // ============================================================ export function injectToolbar() { const container = document.getElementById('invoice-toolbar'); if (!container) return; container.innerHTML = `
0 invoices
`; // Event Listeners document.getElementById('invoice-filter-customer').addEventListener('input', (e) => { filterCustomer = e.target.value; renderInvoiceView(); }); document.getElementById('invoice-group-by').addEventListener('change', (e) => { groupBy = e.target.value; renderInvoiceView(); }); } // ============================================================ // Actions // ============================================================ export function setStatus(status) { filterStatus = status; renderInvoiceView(); } export function viewPDF(id) { window.open(`/api/invoices/${id}/pdf`, '_blank'); } export function viewHTML(id) { window.open(`/api/invoices/${id}/html`, '_blank'); } export async function exportToQBO(id) { if (!confirm('Rechnung wirklich an QuickBooks Online senden?')) return; const btn = event.target; const originalText = btn.textContent; btn.textContent = "⏳..."; btn.disabled = true; try { const response = await fetch(`/api/invoices/${id}/export`, { method: 'POST' }); const result = await response.json(); if (response.ok) { alert(`✅ Erfolg! QBO ID: ${result.qbo_id}, Rechnungsnr: ${result.qbo_doc_number}`); loadInvoices(); } else { alert(`❌ Fehler: ${result.error}`); } } catch (error) { console.error(error); alert('Netzwerkfehler beim Export.'); } finally { btn.textContent = originalText; btn.disabled = false; } } export async function markPaid(id) { try { const response = await fetch(`/api/invoices/${id}/mark-paid`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paid_date: new Date().toISOString().split('T')[0] }) }); if (response.ok) { loadInvoices(); } else { const err = await response.json(); alert('Error: ' + (err.error || 'Unknown')); } } catch (error) { console.error('Error marking paid:', error); } } export async function markUnpaid(id) { try { const response = await fetch(`/api/invoices/${id}/mark-unpaid`, { method: 'PATCH' }); if (response.ok) { loadInvoices(); } else { const err = await response.json(); alert('Error: ' + (err.error || 'Unknown')); } } catch (error) { console.error('Error marking unpaid:', error); } } export async function edit(id) { if (typeof window.openInvoiceModal === 'function') { await window.openInvoiceModal(id); } } export async function remove(id) { if (!confirm('Are you sure you want to delete this invoice?')) return; try { const response = await fetch(`/api/invoices/${id}`, { method: 'DELETE' }); if (response.ok) { loadInvoices(); } else { alert('Error deleting invoice'); } } catch (error) { console.error('Error:', error); } } // ============================================================ // Expose to window // ============================================================ window.invoiceView = { viewPDF, viewHTML, exportToQBO, markPaid, markUnpaid, edit, remove, loadInvoices, renderInvoiceView, setStatus };