chore: check in pending work — queue preferences, QBO AR diagnostics, mobile engagement fixes, ops scripts

Bundles several in-progress efforts that were sitting uncommitted:
- User queue-preferences (migration 087, API route, popover component)
- QBO invoice soft-delete (migration 088) and AR diagnostics route
- Dashboard/mobile engagement route and page adjustments
- Docker Compose log-rotation config
- One-off ticket/RMM investigation scripts (scripts/)
- Planning docs: phase verification/pattern notes, mobile shell design spec
- .gitignore: exclude local scratch financial/inventory data and Claude Code
  worktree/local-settings runtime state (never meant for version control)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6RuWdiUiXrPK6FLBHjtpY
This commit is contained in:
lorentz 2026-07-18 06:34:57 -04:00
parent b638189cb0
commit 672f17b7f9
35 changed files with 2801 additions and 92 deletions

View file

@ -13,11 +13,11 @@ export async function GET() {
if (error) return error;
const tz = getUserTimezone(session);
const [summary, aging, topCustomers, overdueInvoices, recentPayments, monthlyRevenue] = await Promise.all([
const [summary, aging, topCustomers, overdueInvoices, recentPayments, monthlyRevenue, credits] = await Promise.all([
postgresClient.query(
`
SELECT
SUM(balance) FILTER (WHERE status IN ('Open','Overdue')) as total_ar,
SUM(balance) FILTER (WHERE status IN ('Open','Overdue')) as total_ar_gross,
COUNT(*) FILTER (WHERE status IN ('Open','Overdue')) as total_ar_count,
SUM(balance) FILTER (WHERE status = 'Open') as current_balance,
COUNT(*) FILTER (WHERE status = 'Open') as current_count,
@ -26,6 +26,7 @@ export async function GET() {
SUM(total_amt) FILTER (WHERE status = 'Paid' AND (txn_date AT TIME ZONE 'UTC' AT TIME ZONE $1) >= DATE_TRUNC('month', NOW() AT TIME ZONE $1)) as paid_mtd,
SUM(total_amt) FILTER (WHERE status = 'Paid' AND (txn_date AT TIME ZONE 'UTC' AT TIME ZONE $1) >= DATE_TRUNC('year', NOW() AT TIME ZONE $1)) as paid_ytd
FROM qbo_invoices
WHERE is_deleted = false
`,
[tz],
),
@ -41,22 +42,43 @@ export async function GET() {
SUM(balance) FILTER (WHERE status = 'Overdue' AND due_date < (NOW() AT TIME ZONE $1)::date - 60) as days_60_plus,
COUNT(*) FILTER (WHERE status = 'Overdue' AND due_date < (NOW() AT TIME ZONE $1)::date - 60) as cnt_60_plus
FROM qbo_invoices
WHERE is_deleted = false
`,
[tz],
),
// Top customers — net out any unapplied payment credits the customer is
// sitting on, so the number matches QBO's customer A/R view.
postgresClient.query(`
SELECT customer_ref_name, SUM(balance) as balance, COUNT(*) as invoice_count
FROM qbo_invoices
WHERE status IN ('Open','Overdue')
GROUP BY customer_ref_name
ORDER BY balance DESC LIMIT 8
WITH inv AS (
SELECT customer_ref_id, customer_ref_name,
SUM(balance) AS gross_balance,
COUNT(*) AS invoice_count
FROM qbo_invoices
WHERE is_deleted = false
AND status IN ('Open','Overdue')
GROUP BY customer_ref_id, customer_ref_name
),
cred AS (
SELECT customer_ref_id, SUM(unapplied_amt) AS unapplied
FROM qbo_payments
WHERE unapplied_amt > 0
GROUP BY customer_ref_id
)
SELECT inv.customer_ref_name,
GREATEST(inv.gross_balance - COALESCE(cred.unapplied, 0), 0) AS balance,
inv.invoice_count
FROM inv
LEFT JOIN cred ON cred.customer_ref_id = inv.customer_ref_id
ORDER BY balance DESC
LIMIT 8
`),
postgresClient.query(
`
SELECT id, doc_number, txn_date, due_date, customer_ref_name, total_amt, balance, status,
(NOW() AT TIME ZONE $1)::date - due_date::date as days_overdue
FROM qbo_invoices
WHERE status IN ('Open','Overdue')
WHERE is_deleted = false
AND status IN ('Open','Overdue')
ORDER BY status DESC, balance DESC LIMIT 30
`,
[tz],
@ -72,23 +94,39 @@ export async function GET() {
SELECT DATE_TRUNC('month', txn_date) as month,
SUM(total_amt) as revenue, COUNT(*) as invoice_count
FROM qbo_invoices
WHERE status = 'Paid' AND txn_date >= NOW() - INTERVAL '12 months'
WHERE is_deleted = false
AND status = 'Paid'
AND txn_date >= NOW() - INTERVAL '12 months'
GROUP BY 1 ORDER BY 1 ASC
`),
// Unapplied customer credits — payments not yet linked to an invoice.
// QBO nets these against A/R in its customer balance reports.
postgresClient.query(`
SELECT COALESCE(SUM(unapplied_amt), 0) AS total_unapplied
FROM qbo_payments
WHERE unapplied_amt > 0
`),
]);
const s = summary.rows[0];
const a = aging.rows[0];
const unappliedCredits = parseFloat(credits.rows[0]?.total_unapplied ?? 0);
const grossAr = parseFloat(s.total_ar_gross ?? 0);
// QBO's A/R reports net unapplied customer credits against the gross
// invoice balance. Mirror that so the dashboard headline matches QBO.
const netAr = Math.max(grossAr - unappliedCredits, 0);
return NextResponse.json({
summary: {
total_ar: parseFloat(s.total_ar ?? 0),
total_ar_count: parseInt(s.total_ar_count ?? 0),
current_balance: parseFloat(s.current_balance ?? 0),
current_count: parseInt(s.current_count ?? 0),
overdue_balance: parseFloat(s.overdue_balance ?? 0),
overdue_count: parseInt(s.overdue_count ?? 0),
paid_mtd: parseFloat(s.paid_mtd ?? 0),
paid_ytd: parseFloat(s.paid_ytd ?? 0),
total_ar: netAr,
total_ar_gross: grossAr,
unapplied_credits: unappliedCredits,
total_ar_count: parseInt(s.total_ar_count ?? 0),
current_balance: parseFloat(s.current_balance ?? 0),
current_count: parseInt(s.current_count ?? 0),
overdue_balance: parseFloat(s.overdue_balance ?? 0),
overdue_count: parseInt(s.overdue_count ?? 0),
paid_mtd: parseFloat(s.paid_mtd ?? 0),
paid_ytd: parseFloat(s.paid_ytd ?? 0),
},
aging: {
days_1_30: { balance: parseFloat(a.days_1_30 ?? 0), count: parseInt(a.cnt_1_30 ?? 0) },