// invoice-view.js — ES Module für die Invoice View
// Features: Status Filter, Customer Filter, Group by, Send Date, Mark Paid, Reset QBO
// ============================================================
// 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) {
if (!date) return '—';
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));
}
// 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;
});
for (const group of groups.values()) {
group.invoices.sort((a, b) => new Date(b.invoice_date) - new Date(a.invoice_date));
}
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);
// Invoice Number Display
const invNumDisplay = invoice.invoice_number
? invoice.invoice_number
: `Draft`;
// QBO Button — if already in QBO, show checkmark + optional reset
const qboButton = hasQbo
? `✓ QBO`
: ``;
// Paid/Unpaid Toggle
const paidButton = paid
? ``
: ``;
// 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 = new Date(invoice.scheduled_send_date);
const today = new Date();
today.setHours(0,0,0,0);
sendDate.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)`;
}
}
}
const rowClass = paid ? 'bg-green-50/50' : overdue ? 'bg-red-50/50' : '';
return `
${invNumDisplay} ${statusBadge}
${invoice.customer_name || 'N/A'}
${formatDate(invoice.invoice_date)}
${sendDateDisplay}
${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 += `