// invoice-view.js — ES Module v4 // Fixes: No Paid for drafts, payment modal, UTC dates, persistent settings let invoices = []; let filterCustomer = localStorage.getItem('inv_filterCustomer') || ''; let filterStatus = localStorage.getItem('inv_filterStatus') || 'unpaid'; let groupBy = localStorage.getItem('inv_groupBy') || 'none'; const OVERDUE_DAYS = 30; // ============================================================ // Date Helpers — KEIN new Date('YYYY-MM-DD') wegen UTC-Bug! // ============================================================ function parseLocalDate(dateStr) { if (!dateStr) return null; const str = String(dateStr).split('T')[0]; const parts = str.split('-'); if (parts.length !== 3) return null; return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); } function formatDate(date) { if (!date) return '—'; const d = parseLocalDate(date); if (!d) return '—'; return `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}/${d.getFullYear()}`; } function daysSince(date) { const d = parseLocalDate(date); if (!d) return 0; const now = new Date(); now.setHours(0, 0, 0, 0); return Math.floor((now - d) / 86400000); } function getWeekNumber(date) { const d = parseLocalDate(date); if (!d) return { year: 0, week: 0 }; const copy = new Date(d.getTime()); copy.setHours(0, 0, 0, 0); copy.setDate(copy.getDate() + 3 - ((copy.getDay() + 6) % 7)); const week1 = new Date(copy.getFullYear(), 0, 4); return { year: copy.getFullYear(), week: 1 + Math.round(((copy - 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); const fmt = (d) => `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}/${d.getFullYear()}`; return { start: fmt(monday), end: fmt(sunday) }; } function getMonthName(i) { return ['January','February','March','April','May','June','July','August','September','October','November','December'][i]; } function isPaid(inv) { return !!inv.paid_date; } function isDraft(inv) { return !inv.qbo_id; } function isOverdue(inv) { return !isPaid(inv) && daysSince(inv.invoice_date) > OVERDUE_DAYS; } function saveSettings() { localStorage.setItem('inv_filterStatus', filterStatus); localStorage.setItem('inv_groupBy', groupBy); localStorage.setItem('inv_filterCustomer', filterCustomer); } // ============================================================ // Data // ============================================================ 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; } // ============================================================ // Filter / Sort / Group // ============================================================ function getFilteredInvoices() { let f = [...invoices]; if (filterStatus === 'unpaid') f = f.filter(i => !isPaid(i)); else if (filterStatus === 'paid') f = f.filter(i => isPaid(i)); else if (filterStatus === 'overdue') f = f.filter(i => isOverdue(i)); else if (filterStatus === 'draft') f = f.filter(i => isDraft(i) && !isPaid(i)); if (filterCustomer.trim()) { const s = filterCustomer.toLowerCase(); f = f.filter(i => (i.customer_name || '').toLowerCase().includes(s)); } f.sort((a, b) => (parseLocalDate(b.invoice_date) || 0) - (parseLocalDate(a.invoice_date) || 0)); return f; } function groupInvoices(filtered) { if (groupBy === 'none') return null; const groups = new Map(); filtered.forEach(inv => { const d = parseLocalDate(inv.invoice_date); if (!d) return; 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 { key = `${d.getFullYear()}-${String(d.getMonth()).padStart(2, '0')}`; label = `${getMonthName(d.getMonth())} ${d.getFullYear()}`; } if (!groups.has(key)) groups.set(key, { label, invoices: [], total: 0 }); const g = groups.get(key); g.invoices.push(inv); g.total += parseFloat(inv.total) || 0; }); for (const g of groups.values()) { g.invoices.sort((a, b) => (parseLocalDate(b.invoice_date) || 0) - (parseLocalDate(a.invoice_date) || 0)); } return new Map([...groups.entries()].sort((a, b) => b[0].localeCompare(a[0]))); } // ============================================================ // Render // ============================================================ function renderInvoiceRow(invoice) { const hasQbo = !!invoice.qbo_id; const paid = isPaid(invoice); const overdue = isOverdue(invoice); const draft = isDraft(invoice); const invNumDisplay = invoice.invoice_number ? invoice.invoice_number : `Draft`; let statusBadge = ''; if (paid) statusBadge = `Paid`; else if (overdue) statusBadge = `Overdue`; // Send Date let sendDateDisplay = '—'; if (invoice.scheduled_send_date) { const sendDate = parseLocalDate(invoice.scheduled_send_date); const today = new Date(); today.setHours(0, 0, 0, 0); const daysUntil = Math.floor((sendDate - today) / 86400000); sendDateDisplay = formatDate(invoice.scheduled_send_date); if (!paid) { if (daysUntil < 0) sendDateDisplay += ` (${Math.abs(daysUntil)}d ago)`; else if (daysUntil === 0) sendDateDisplay += ` (today)`; else if (daysUntil <= 3) sendDateDisplay += ` (in ${daysUntil}d)`; } } // --- BUTTONS (Edit | QBO | PDF HTML | Payment | Del) --- const editBtn = ``; const qboBtn = hasQbo ? `✓ QBO` : ``; const pdfBtn = draft ? `PDF` : ``; const htmlBtn = ``; // PAYMENT BUTTON — NUR wenn in QBO. Drafts bekommen KEINEN Button. let paidBtn = ''; if (paid) { paidBtn = ``; } else if (hasQbo) { paidBtn = ``; } // Kein Button für Drafts (!hasQbo && !paid) const delBtn = ``; const rowClass = paid ? 'bg-green-50/50' : overdue ? 'bg-red-50/50' : ''; return `