feat: QBO sync schedules (2AM + 4PM) + Sync QBO button on mobile finance page
- 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
This commit is contained in:
parent
8cd8a94412
commit
44847ccf21
1 changed files with 67 additions and 6 deletions
|
|
@ -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<string | null>(null);
|
||||
const [lastSync, setLastSync] = useState<string | null>(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 (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
|
|
@ -62,11 +108,26 @@ export default function MobileFinance() {
|
|||
return (
|
||||
<div className="p-4 space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">Finance</h1>
|
||||
<button onClick={load} className="p-2 rounded-full hover:bg-accent">
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Finance</h1>
|
||||
{lastSync && <p className="text-[11px] text-muted-foreground mt-0.5">Last sync {lastSync}</p>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={load} disabled={loading} className="p-2 rounded-full hover:bg-accent disabled:opacity-40" title="Refresh display">
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<button onClick={syncAndRefresh} disabled={syncing} className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-primary text-primary-foreground text-xs font-medium disabled:opacity-50 hover:bg-primary/90 transition-colors" title="Sync from QuickBooks then refresh">
|
||||
<CloudDownload className={`w-3.5 h-3.5 ${syncing ? 'animate-pulse' : ''}`} />
|
||||
{syncing ? 'Syncing…' : 'Sync QBO'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{syncMsg && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-xl bg-muted text-xs text-muted-foreground">
|
||||
<RefreshCw className="w-3 h-3 animate-spin shrink-0" />
|
||||
{syncMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── AR Hero ─────────────────────────────────────────── */}
|
||||
<div className="rounded-2xl border bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 p-5">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue