BillTracker/client/pages/DataPage.jsx

297 lines
11 KiB
React
Raw Normal View History

import React, { useState, useEffect, useCallback } from 'react';
import { toast } from 'sonner';
2026-05-03 19:51:57 -05:00
import { api } from '@/api';
import { cn } from '@/lib/utils';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import BankSyncSection from '@/components/data/BankSyncSection';
import BillRulesManager from '@/components/BillRulesManager';
import ImportTransactionCsvSection from '@/components/data/ImportTransactionCsvSection';
import TransactionMatchingSection from '@/components/data/TransactionMatchingSection';
import ImportSpreadsheetSection from '@/components/data/ImportSpreadsheetSection';
import ImportMyDataSection from '@/components/data/ImportMyDataSection';
import SeedDemoDataSection from '@/components/data/SeedDemoDataSection';
import DownloadMyDataSection from '@/components/data/DownloadMyDataSection';
import ImportHistorySection from '@/components/data/ImportHistorySection';
2026-05-09 13:03:36 -05:00
const DATA_TAB_STORAGE_KEY = 'billtracker:data.activeTab';
const DATA_TABS = [
{
id: 'sync',
label: 'Sync & Match',
description: 'Bank connections, synced transactions, and CSV transaction imports.',
},
{
id: 'import',
label: 'Import Data',
description: 'Bring in spreadsheet history, restore an app export, or seed demo data.',
},
{
id: 'export',
label: 'Export & History',
description: 'Download your data and review previous import activity.',
},
];
function useStoredTab() {
const [activeTab, setActiveTabState] = useState(() => {
if (typeof window === 'undefined') return DATA_TABS[0].id;
const stored = window.localStorage.getItem(DATA_TAB_STORAGE_KEY);
return DATA_TABS.some(tab => tab.id === stored) ? stored : DATA_TABS[0].id;
});
const setActiveTab = useCallback((tab) => {
setActiveTabState(tab);
if (typeof window !== 'undefined') {
window.localStorage.setItem(DATA_TAB_STORAGE_KEY, tab);
}
}, []);
return [activeTab, setActiveTab];
}
function DataStatusStrip({ history, historyLoading, simplefinConn, syncLoading }) {
const syncStatus = simplefinConn
? (simplefinConn.last_error ? 'Needs attention' : 'Connected')
: syncLoading ? 'Loading…' : 'Not connected';
const syncTone = simplefinConn?.last_error
? 'text-amber-600 dark:text-amber-300'
: simplefinConn
? 'text-emerald-600 dark:text-emerald-300'
: 'text-muted-foreground';
return (
<div className="grid gap-2 sm:grid-cols-3">
<div className="rounded-lg border border-border/70 bg-card/80 px-4 py-3">
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground cursor-default w-fit">SimpleFIN</p>
</TooltipTrigger>
<TooltipContent>Open standard for syncing bank transactions</TooltipContent>
</Tooltip>
{simplefinConn?.last_error ? (
<Tooltip>
<TooltipTrigger asChild>
<p className={cn('mt-1 truncate text-sm font-semibold cursor-default', syncTone)}>{syncStatus}</p>
</TooltipTrigger>
<TooltipContent>{simplefinConn.last_error}</TooltipContent>
</Tooltip>
) : (
<p className={cn('mt-1 truncate text-sm font-semibold', syncTone)}>{syncStatus}</p>
)}
</TooltipProvider>
</div>
<div className="rounded-lg border border-border/70 bg-card/80 px-4 py-3">
<p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">Last Sync</p>
<p className="mt-1 truncate text-sm font-semibold text-foreground">
{simplefinConn?.last_sync_at ? new Date(simplefinConn.last_sync_at).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—'}
</p>
</div>
<div className="rounded-lg border border-border/70 bg-card/80 px-4 py-3">
<p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">Import History</p>
<p className="mt-1 truncate text-sm font-semibold text-foreground">
{historyLoading ? 'Loading…' : `${history?.length || 0} record${(history?.length || 0) === 1 ? '' : 's'}`}
</p>
</div>
</div>
);
}
2026-05-03 19:51:57 -05:00
export default function DataPage() {
const [history, setHistory] = useState(null);
2026-05-04 23:34:24 -05:00
const [historyLoading, setHistoryLoading] = useState(true);
const [transactionRefreshKey, setTransactionRefreshKey] = useState(0);
const [simplefinConn, setSimplefinConn] = useState(null);
const [syncLoading, setSyncLoading] = useState(true);
const [activeTab, setActiveTab] = useStoredTab();
2026-05-04 23:34:24 -05:00
const loadHistory = async () => {
setHistoryLoading(true);
try {
const { history } = await api.importHistory();
setHistory(history);
} catch (err) {
2026-05-04 23:34:24 -05:00
setHistory([]);
toast.error(err.message || 'Failed to load import history.');
2026-05-04 23:34:24 -05:00
} finally {
setHistoryLoading(false);
}
};
const loadSimplefinSummary = useCallback(async () => {
setSyncLoading(true);
try {
const [status, sources] = await Promise.all([
api.simplefinStatus(),
api.dataSources({ type: 'provider_sync' }),
]);
if (!status.enabled) {
setSimplefinConn(null);
return;
}
const conns = Array.isArray(sources) ? sources.filter(source => source.provider === 'simplefin') : [];
setSimplefinConn(conns[0] || null);
} catch {
setSimplefinConn(null);
} finally {
setSyncLoading(false);
}
}, []);
useEffect(() => {
loadHistory();
loadSimplefinSummary();
}, [loadSimplefinSummary]);
2026-05-04 23:34:24 -05:00
const handleTransactionImportComplete = () => {
loadHistory();
setTransactionRefreshKey(key => key + 1);
};
// Called by BankSyncSection when connection state changes (connect/sync/disconnect)
const handleConnectionChange = useCallback((conn) => {
setSimplefinConn(conn || null);
setSyncLoading(false);
setTransactionRefreshKey(key => key + 1);
}, []);
2026-05-04 23:34:24 -05:00
return (
<div className="mx-auto w-full max-w-6xl space-y-5">
<div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Data</h1>
<p className="text-sm text-muted-foreground mt-0.5">
Import, export, and review your user-owned bill tracker records.
</p>
</div>
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<div className="rounded-full border border-border/70 bg-card/80 px-3 py-1.5 text-xs font-medium text-muted-foreground cursor-default">
User data only
</div>
</TooltipTrigger>
<TooltipContent>This page only manages your own records other users' data is not accessible here</TooltipContent>
</Tooltip>
</TooltipProvider>
2026-05-04 23:34:24 -05:00
</div>
<DataStatusStrip history={history} historyLoading={historyLoading} simplefinConn={simplefinConn} syncLoading={syncLoading} />
<div className="rounded-lg border border-border/70 bg-card/70 p-1">
<div className="grid gap-1 md:grid-cols-3">
{DATA_TABS.map(tab => {
const active = activeTab === tab.id;
return (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
className={cn(
'rounded-md px-4 py-3 text-left transition-colors',
active ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground',
)}
>
<span className="block text-sm font-semibold">{tab.label}</span>
<span className={cn('mt-0.5 block text-xs', active ? 'text-primary-foreground/75' : 'text-muted-foreground')}>
{tab.description}
</span>
</button>
);
})}
</div>
2026-05-04 23:34:24 -05:00
</div>
{activeTab === 'sync' && (
<div className="space-y-5">
<BankSyncSection
onConnectionChange={handleConnectionChange}
cardProps={{
collapsible: true,
defaultOpen: true,
storageKey: 'billtracker:data.card.bankSync',
summary: simplefinConn ? `Connected as ${simplefinConn.name || 'SimpleFIN'}` : 'Connect or manage SimpleFIN bank sync.',
}}
/>
<TransactionMatchingSection
refreshKey={transactionRefreshKey}
simplefinConn={simplefinConn}
cardProps={{
collapsible: true,
defaultOpen: true,
storageKey: 'billtracker:data.card.transactions',
summary: 'Review transaction matches and create bill payments.',
}}
/>
<BillRulesManager />
<ImportTransactionCsvSection
onHistoryRefresh={handleTransactionImportComplete}
cardProps={{
collapsible: true,
defaultOpen: false,
storageKey: 'billtracker:data.card.transactionCsv',
summary: 'Upload bank or credit-card CSV transaction files.',
}}
/>
</div>
)}
{activeTab === 'import' && (
<div className="space-y-5">
<ImportSpreadsheetSection
onHistoryRefresh={loadHistory}
cardProps={{
collapsible: true,
defaultOpen: false,
storageKey: 'billtracker:data.card.spreadsheet',
summary: 'Import bill/payment history from an XLSX workbook.',
}}
/>
<ImportMyDataSection
onHistoryRefresh={loadHistory}
cardProps={{
collapsible: true,
defaultOpen: false,
storageKey: 'billtracker:data.card.sqliteImport',
summary: 'Restore data from a user SQLite export.',
}}
/>
<SeedDemoDataSection
onSeeded={loadHistory}
cardProps={{
collapsible: true,
defaultOpen: false,
storageKey: 'billtracker:data.card.demo',
summary: 'Seed or clear demo data for testing.',
}}
/>
</div>
)}
{activeTab === 'export' && (
<div className="space-y-5">
<DownloadMyDataSection
cardProps={{
collapsible: true,
defaultOpen: true,
storageKey: 'billtracker:data.card.download',
summary: 'Download SQLite or Excel exports of your records.',
}}
/>
<ImportHistorySection
history={history}
loading={historyLoading}
onRefresh={loadHistory}
cardProps={{
collapsible: true,
defaultOpen: false,
storageKey: 'billtracker:data.card.history',
summary: historyLoading ? 'Loading import activity…' : `${history?.length || 0} import record${(history?.length || 0) === 1 ? '' : 's'}.`,
}}
/>
</div>
)}
2026-05-04 23:34:24 -05:00
</div>
);
2026-05-03 19:51:57 -05:00
}