diff --git a/public/invoice-view.js b/public/invoice-view.js
index 1b504e4..b881948 100644
--- a/public/invoice-view.js
+++ b/public/invoice-view.js
@@ -1,5 +1,5 @@
-// invoice-view.js — ES Module v4
-// Fixes: No Paid for drafts, payment modal, UTC dates, persistent settings
+// invoice-view.js — ES Module v5
+// Sync from QBO, Paid/Deposited/Partial badges, no Unpaid button
let invoices = [];
let filterCustomer = localStorage.getItem('inv_filterCustomer') || '';
@@ -9,7 +9,7 @@ let groupBy = localStorage.getItem('inv_groupBy') || 'none';
const OVERDUE_DAYS = 30;
// ============================================================
-// Date Helpers — KEIN new Date('YYYY-MM-DD') wegen UTC-Bug!
+// Date Helpers
// ============================================================
function parseLocalDate(dateStr) {
@@ -27,6 +27,13 @@ function formatDate(date) {
return `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}/${d.getFullYear()}`;
}
+function formatDateTime(isoStr) {
+ if (!isoStr) return 'Never';
+ const d = new Date(isoStr);
+ return d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) +
+ ', ' + d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
+}
+
function daysSince(date) {
const d = parseLocalDate(date);
if (!d) return 0;
@@ -64,7 +71,12 @@ function getMonthName(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 isOverdue(inv) { return !isPaid(inv) && !isPartiallyPaid(inv) && daysSince(inv.invoice_date) > OVERDUE_DAYS; }
+function isPartiallyPaid(inv) {
+ const amountPaid = parseFloat(inv.amount_paid) || 0;
+ const balance = parseFloat(inv.balance) ?? ((parseFloat(inv.total) || 0) - amountPaid);
+ return !inv.paid_date && amountPaid > 0 && balance > 0;
+}
function saveSettings() {
localStorage.setItem('inv_filterStatus', filterStatus);
@@ -81,9 +93,19 @@ export async function loadInvoices() {
const response = await fetch('/api/invoices');
invoices = await response.json();
renderInvoiceView();
+ loadLastSync();
} catch (error) { console.error('Error loading invoices:', error); }
}
+async function loadLastSync() {
+ try {
+ const res = await fetch('/api/qbo/last-sync');
+ const data = await res.json();
+ const el = document.getElementById('last-sync-time');
+ if (el) el.textContent = data.last_sync ? `Last synced: ${formatDateTime(data.last_sync)}` : 'Never synced';
+ } catch (e) { /* ignore */ }
+}
+
export function getInvoicesData() { return invoices; }
// ============================================================
@@ -96,6 +118,7 @@ function getFilteredInvoices() {
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));
+ else if (filterStatus === 'partial') f = f.filter(i => isPartiallyPaid(i));
if (filterCustomer.trim()) {
const s = filterCustomer.toLowerCase();
@@ -141,19 +164,25 @@ function renderInvoiceRow(invoice) {
const paid = isPaid(invoice);
const overdue = isOverdue(invoice);
const draft = isDraft(invoice);
+ const amountPaid = parseFloat(invoice.amount_paid) || 0;
+ const balance = parseFloat(invoice.balance) ?? ((parseFloat(invoice.total) || 0) - amountPaid);
+ const partial = isPartiallyPaid(invoice);
const invNumDisplay = invoice.invoice_number
? invoice.invoice_number
: `Draft`;
+ // Status Badge
let statusBadge = '';
- const amountPaid = parseFloat(invoice.amount_paid) || 0;
- const balance = parseFloat(invoice.balance) ?? (parseFloat(invoice.total) - amountPaid);
- const isPartiallyPaid = !paid && amountPaid > 0 && balance > 0;
-
- if (paid) statusBadge = `Paid`;
- else if (isPartiallyPaid) statusBadge = `Partial $${amountPaid.toFixed(2)}`;
- else if (overdue) statusBadge = `Overdue`;
+ if (paid && invoice.payment_status === 'Deposited') {
+ statusBadge = `Deposited`;
+ } else if (paid) {
+ statusBadge = `Paid`;
+ } else if (partial) {
+ statusBadge = `Partial $${amountPaid.toFixed(2)}`;
+ } else if (overdue) {
+ statusBadge = `Overdue`;
+ }
// Send Date
let sendDateDisplay = '—';
@@ -169,14 +198,20 @@ function renderInvoiceRow(invoice) {
}
}
- // --- BUTTONS (Edit | QBO | PDF HTML | Payment | Del) ---
+ // Amount column — show balance when partially paid
+ let amountDisplay;
+ if (partial) {
+ amountDisplay = `$${balance.toFixed(2)} $${parseFloat(invoice.total).toFixed(2)}`;
+ } else {
+ amountDisplay = `$${parseFloat(invoice.total).toFixed(2)}`;
+ }
+
+ // --- BUTTONS: Edit | QBO | PDF HTML | Payment | Del ---
const editBtn = ``;
- // QBO Button — Export oder Sync
const customerHasQbo = !!invoice.customer_qbo_id;
let qboBtn;
if (hasQbo) {
- // Already in QBO — show sync button + reset option
qboBtn = ``;
} else if (!customerHasQbo) {
qboBtn = `QBO ⚠`;
@@ -185,21 +220,19 @@ function renderInvoiceRow(invoice) {
}
const pdfBtn = draft
- ? `PDF`
+ ? `PDF`
: ``;
const htmlBtn = ``;
- // PAYMENT BUTTON — NUR wenn in QBO. Drafts bekommen KEINEN Button.
+ // Payment button — only for QBO invoices that are not fully paid
let paidBtn = '';
- if (paid) {
- paidBtn = ``;
- } else if (hasQbo) {
+ if (!paid && hasQbo) {
paidBtn = ``;
}
- // Kein Button für Drafts (!hasQbo && !paid)
const delBtn = ``;
- const rowClass = paid ? 'bg-green-50/50' : overdue ? 'bg-red-50/50' : '';
+
+ const rowClass = paid ? (invoice.payment_status === 'Deposited' ? 'bg-blue-50/50' : 'bg-green-50/50') : partial ? 'bg-yellow-50/30' : overdue ? 'bg-red-50/50' : '';
return `
@@ -208,12 +241,7 @@ function renderInvoiceRow(invoice) {
| ${formatDate(invoice.invoice_date)} |
${sendDateDisplay} |
${invoice.terms} |
-
- ${isPartiallyPaid
- ? `$${balance.toFixed(2)} $${parseFloat(invoice.total).toFixed(2)}`
- : `$${parseFloat(invoice.total).toFixed(2)}`
- }
- |
+ ${amountDisplay} |
${editBtn} ${qboBtn} ${pdfBtn} ${htmlBtn} ${paidBtn} ${delBtn}
|
@@ -285,6 +313,10 @@ function updateStatusButtons() {
const unpaidCount = invoices.filter(i => !isPaid(i)).length;
const ub = document.getElementById('unpaid-badge');
if (ub) ub.textContent = unpaidCount;
+
+ const partialCount = invoices.filter(i => isPartiallyPaid(i)).length;
+ const pb = document.getElementById('partial-badge');
+ if (pb) { pb.textContent = partialCount; pb.classList.toggle('hidden', partialCount === 0); }
}
// ============================================================
@@ -302,6 +334,9 @@ export function injectToolbar() {
+