From 44847ccf21c2be45478967625858eaa93272dc7f Mon Sep 17 00:00:00 2001 From: lorentz Date: Mon, 23 Mar 2026 12:36:02 -0400 Subject: [PATCH] feat: QBO sync schedules (2AM + 4PM) + Sync QBO button on mobile finance page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DB: inserted qbo-sync-2am (0 2 * * *) and qbo-sync-4pm (0 16 * * *) schedules - Mobile finance: 'Sync QBO' button triggers POST /api/qbo/sync incremental, polls /api/qbo/sync GET until lastSync timestamp changes (max 90s), then reloads finance data - Shows last sync timestamp below page title - Separate refresh-only button (↻) for quick display refresh without re-syncing - Sync status message shown during polling --- app/mobile/finance/page.tsx | 73 ++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/app/mobile/finance/page.tsx b/app/mobile/finance/page.tsx index 85708cd..b7b726b 100644 --- a/app/mobile/finance/page.tsx +++ b/app/mobile/finance/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { RefreshCw, TrendingUp, AlertTriangle, CheckCircle2, ChevronDown, ChevronRight } from 'lucide-react'; +import { RefreshCw, TrendingUp, AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, CloudDownload } from 'lucide-react'; interface AgingBucket { balance: number; count: number; } interface FinanceData { @@ -32,6 +32,9 @@ export default function MobileFinance() { const [invoicesOpen, setInvoicesOpen] = useState(false); const [paymentsOpen, setPaymentsOpen] = useState(false); const [tab, setTab] = useState<'open' | 'overdue'>('overdue'); + const [syncing, setSyncing] = useState(false); + const [syncMsg, setSyncMsg] = useState(null); + const [lastSync, setLastSync] = useState(null); const load = async () => { setLoading(true); setError(null); @@ -43,7 +46,50 @@ export default function MobileFinance() { finally { setLoading(false); } }; - useEffect(() => { load(); }, []); + const loadLastSync = async () => { + try { + const r = await fetch('/api/qbo/sync'); + if (r.ok) { + const d = await r.json(); + const ts = d.lastSync?.invoices; + setLastSync(ts ? new Date(ts).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : null); + } + } catch {} + }; + + const syncAndRefresh = async () => { + if (syncing) return; + setSyncing(true); + setSyncMsg('Starting sync…'); + try { + const r = await fetch('/api/qbo/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ syncType: 'incremental', triggeredBy: 'mobile-finance' }) }); + if (r.status === 409) { setSyncMsg('Sync already in progress — refreshing data…'); } + else if (!r.ok) { setSyncMsg('Sync failed'); setSyncing(false); return; } + else { setSyncMsg('Syncing with QuickBooks…'); } + // Poll until sync completes (max 90s) + const start = Date.now(); + let lastTs: string | null = null; + while (Date.now() - start < 90_000) { + await new Promise(res => setTimeout(res, 4000)); + const s = await fetch('/api/qbo/sync'); + if (s.ok) { + const sd = await s.json(); + const ts = sd.lastSync?.invoices; + if (ts && ts !== lastTs) { lastTs = ts; break; } + } + } + setSyncMsg('Refreshing data…'); + await load(); + await loadLastSync(); + setSyncMsg(null); + } catch (e) { + setSyncMsg('Error: ' + String(e)); + } finally { + setSyncing(false); + } + }; + + useEffect(() => { load(); loadLastSync(); }, []); if (loading) return (
@@ -62,11 +108,26 @@ export default function MobileFinance() { return (
-

Finance

- +
+

Finance

+ {lastSync &&

Last sync {lastSync}

} +
+
+ + +
+ {syncMsg && ( +
+ + {syncMsg} +
+ )} {/* ── AR Hero ─────────────────────────────────────────── */}