// invoice-view.js — ES Module für die Invoice View // v3: UTC date fix, Draft filter, persistent settings, PDF disabled for drafts // ============================================================ // State // ============================================================ 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; // ============================================================ // Helpers // ============================================================ // KRITISCHER FIX: Datum-String 'YYYY-MM-DD' OHNE Timezone-Conversion parsen // new Date('2026-02-16') interpretiert als UTC → in CST wird's der 15. // Stattdessen: manuell parsen oder 'T12:00:00' anhängen function parseLocalDate(dateStr) { if (!dateStr) return null; // Wenn es schon ein T enthält (ISO mit Zeit), den Datumsteil nehmen const str = String(dateStr).split('T')[0]; const parts = str.split('-'); if (parts.length !== 3) return null; // Monat ist 0-basiert in JS 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 '—'; 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 = 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) => { const m = String(d.getMonth() + 1).padStart(2, '0'); const dy = String(d.getDate()).padStart(2, '0'); return `${m}/${dy}/${d.getFullYear()}`; }; return { start: fmt(monday), end: fmt(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 isDraft(inv) { return !inv.qbo_id; } function isOverdue(inv) { return !isPaid(inv) && daysSince(inv.invoice_date) > OVERDUE_DAYS; } // Save state to localStorage function saveSettings() { localStorage.setItem('inv_filterStatus', filterStatus); localStorage.setItem('inv_groupBy', groupBy); localStorage.setItem('inv_filterCustomer', filterCustomer); } // ============================================================ // 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)); } else if (filterStatus === 'draft') { filtered = filtered.filter(inv => isDraft(inv) && !isPaid(inv)); } // 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) => { const da = parseLocalDate(a.invoice_date); const db = parseLocalDate(b.invoice_date); return (db || 0) - (da || 0); }); return filtered; } 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 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; }); for (const group of groups.values()) { group.invoices.sort((a, b) => { const da = parseLocalDate(a.invoice_date); const db = parseLocalDate(b.invoice_date); return (db || 0) - (da || 0); }); } 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); const draft = isDraft(invoice); // Invoice Number Display const invNumDisplay = invoice.invoice_number ? invoice.invoice_number : `Draft`; // Status Badge let statusBadge = ''; if (paid) { statusBadge = `Paid`; } else if (overdue) { statusBadge = `Overdue`; } // Send Date display 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)`; } } } // --- ACTION BUTTONS (Reihenfolge: Edit | QBO | PDF HTML | Paid | Del) --- // Edit const editBtn = ``; // QBO const qboBtn = hasQbo ? `✓ QBO` : ``; // PDF + HTML — PDF deaktiviert wenn Draft (keine Rechnungsnummer) const pdfBtn = draft ? `PDF` : ``; const htmlBtn = ``; // Paid/Unpaid — wenn in QBO: Payment-Modal öffnen, sonst lokal markieren let paidBtn; if (paid) { paidBtn = ``; } else if (hasQbo && window.paymentModal) { paidBtn = ``; } else { paidBtn = ``; } // Delete const delBtn = ``; const rowClass = paid ? 'bg-green-50/50' : overdue ? 'bg-red-50/50' : ''; return `