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:
parent
b638189cb0
commit
672f17b7f9
35 changed files with 2801 additions and 92 deletions
|
|
@ -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) },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue