From 42e67dfdeb4a0fbcc13cabeb66ee3b88b31e4f46 Mon Sep 17 00:00:00 2001 From: null Date: Sat, 4 Jul 2026 22:21:48 -0500 Subject: [PATCH] =?UTF-8?q?refactor(ts):=20DataPage=20=E2=86=92=20TypeScri?= =?UTF-8?q?pt=20(section=20routing=20+=20data-section=20orchestration)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/pages/{DataPage.jsx => DataPage.tsx} | 65 +++++++++++++-------- 1 file changed, 41 insertions(+), 24 deletions(-) rename client/pages/{DataPage.jsx => DataPage.tsx} (85%) diff --git a/client/pages/DataPage.jsx b/client/pages/DataPage.tsx similarity index 85% rename from client/pages/DataPage.jsx rename to client/pages/DataPage.tsx index 74c60c1..06d6235 100644 --- a/client/pages/DataPage.jsx +++ b/client/pages/DataPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useRef, Suspense, lazy } from 'react'; +import { useState, useEffect, useCallback, useRef, Suspense, lazy, type ComponentType, type ReactNode } from 'react'; import { useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import { motion, AnimatePresence, useReducedMotion } from 'framer-motion'; @@ -7,9 +7,10 @@ import { FileSpreadsheet, FileText, FlaskConical, Download, RotateCcw, History, ShieldAlert, } from 'lucide-react'; import { api } from '@/api'; +import { errMessage } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import ConnectionHero from '@/components/data/ConnectionHero'; -import DataNav from '@/components/data/DataNav'; +import DataNav, { type DataNavSection } from '@/components/data/DataNav'; import BankSyncSection from '@/components/data/BankSyncSection'; import BillRulesManager from '@/components/BillRulesManager'; import ImportTransactionCsvSection from '@/components/data/ImportTransactionCsvSection'; @@ -17,28 +18,44 @@ import ImportOfxSection from '@/components/data/ImportOfxSection'; import TransactionMatchingSection from '@/components/data/TransactionMatchingSection'; import SeedDemoDataSection from '@/components/data/SeedDemoDataSection'; import DownloadMyDataSection from '@/components/data/DownloadMyDataSection'; -import ImportHistorySection from '@/components/data/ImportHistorySection'; +import ImportHistorySection, { type ImportHistoryRow } from '@/components/data/ImportHistorySection'; import EraseDataSection from '@/components/data/EraseDataSection'; // Heavy panes (XLSX parsing / SQLite restore) — code-split, loaded on demand. const ImportSpreadsheetSection = lazy(() => import('@/components/data/ImportSpreadsheetSection')); const ImportMyDataSection = lazy(() => import('@/components/data/ImportMyDataSection')); -const SECTIONS = [ +interface Conn { + id: number | string; + name?: string; + provider?: string; + status?: string; + last_error?: string | null; + last_sync_at?: string | null; +} + +interface Section { + id: string; + label: string; + description: string; + icon: ComponentType<{ className?: string }>; +} + +const SECTIONS: Section[] = [ { id: 'bank-sync', label: 'Bank sync', description: 'Connect & sync your bank', icon: Landmark }, { id: 'transactions', label: 'Transactions', description: 'Review & match', icon: ArrowRightLeft }, { id: 'import', label: 'Import', description: 'Bring in existing data', icon: Upload }, { id: 'export', label: 'Export & backups', description: 'Download & restore', icon: DatabaseBackup }, ]; const SECTION_IDS = SECTIONS.map(s => s.id); -const DEFAULT_SECTION = SECTIONS[0].id; +const DEFAULT_SECTION = SECTIONS[0]!.id; const SECTION_KEY = 'billtracker:data.section'; const LEGACY_KEY = 'billtracker:data.activeTab'; // old 3-tab key → migrate -function storedSection() { +function storedSection(): string | null { if (typeof window === 'undefined') return null; const s = window.localStorage.getItem(SECTION_KEY); - return SECTION_IDS.includes(s) ? s : null; + return s && SECTION_IDS.includes(s) ? s : null; } function PaneSkeleton() { @@ -52,25 +69,25 @@ function PaneSkeleton() { export default function DataPage() { const [searchParams, setSearchParams] = useSearchParams(); - const [history, setHistory] = useState(null); + const [history, setHistory] = useState(null); const [historyLoading, setHistoryLoading] = useState(true); const [transactionRefreshKey, setTransactionRefreshKey] = useState(0); - const [simplefinConn, setSimplefinConn] = useState(null); + const [simplefinConn, setSimplefinConn] = useState(null); const [syncEnabled, setSyncEnabled] = useState(true); const [hasConnections, setHasConnections] = useState(false); const [syncLoading, setSyncLoading] = useState(true); const [syncError, setSyncError] = useState(false); const [unmatchedCount, setUnmatchedCount] = useState(0); - const [txnTotal, setTxnTotal] = useState(null); + const [txnTotal, setTxnTotal] = useState(null); const reduceMotion = useReducedMotion(); - const paneRef = useRef(null); + const paneRef = useRef(null); const firstRender = useRef(true); // Active section: URL ?section= is source of truth → localStorage → default. const urlSection = searchParams.get('section'); - const activeSection = SECTION_IDS.includes(urlSection) ? urlSection : (storedSection() || DEFAULT_SECTION); + const activeSection = (urlSection && SECTION_IDS.includes(urlSection)) ? urlSection : (storedSection() || DEFAULT_SECTION); - const goTo = useCallback((id) => { + const goTo = useCallback((id: string) => { if (!SECTION_IDS.includes(id)) return; window.localStorage.setItem(SECTION_KEY, id); setSearchParams(prev => { @@ -83,9 +100,9 @@ export default function DataPage() { // Reflect the resolved section into the URL once, so refresh/back-button work // (and migrate the old 3-tab key). Runs only when the URL lacks a valid section. useEffect(() => { - if (SECTION_IDS.includes(urlSection)) return; + if (urlSection && SECTION_IDS.includes(urlSection)) return; const legacy = window.localStorage.getItem(LEGACY_KEY); - const migrated = { sync: 'bank-sync', import: 'import', export: 'export' }[legacy]; + const migrated = legacy ? ({ sync: 'bank-sync', import: 'import', export: 'export' } as Record)[legacy] : undefined; const target = storedSection() || migrated || DEFAULT_SECTION; setSearchParams(prev => { const p = new URLSearchParams(prev); @@ -104,11 +121,11 @@ export default function DataPage() { const loadHistory = useCallback(async () => { setHistoryLoading(true); try { - const { history } = await api.importHistory(); - setHistory(history); + const res = await api.importHistory() as { history?: ImportHistoryRow[] }; + setHistory(res.history ?? []); } catch (err) { setHistory([]); - toast.error(err.message || 'Failed to load import history.'); + toast.error(errMessage(err, 'Failed to load import history.')); } finally { setHistoryLoading(false); } @@ -119,12 +136,12 @@ export default function DataPage() { setSyncError(false); try { const [status, sources] = await Promise.all([ - api.simplefinStatus(), + api.simplefinStatus() as Promise<{ enabled?: boolean; has_connections?: boolean }>, api.dataSources({ type: 'provider_sync' }), ]); setSyncEnabled(Boolean(status.enabled)); setHasConnections(Boolean(status.has_connections)); - const conns = Array.isArray(sources) ? sources.filter(s => s.provider === 'simplefin') : []; + const conns = Array.isArray(sources) ? (sources as Conn[]).filter(s => s.provider === 'simplefin') : []; setSimplefinConn(conns[0] || null); } catch { setSyncError(true); @@ -137,7 +154,7 @@ export default function DataPage() { // Cheap "N to review" + transaction count (summary is aggregate, so limit:1 is enough). const loadTxnStats = useCallback(async () => { try { - const data = await api.bankTransactionsLedger({ limit: 1 }); + const data = await api.bankTransactionsLedger({ limit: 1 }) as { summary?: { unmatched?: number; total?: number } }; setUnmatchedCount(data?.summary?.unmatched || 0); setTxnTotal(data?.summary?.total ?? null); } catch { @@ -154,7 +171,7 @@ export default function DataPage() { }, [loadHistory]); // BankSyncSection reports connect/sync/disconnect changes. - const handleConnectionChange = useCallback((conn) => { + const handleConnectionChange = useCallback((conn: Conn | null) => { setSimplefinConn(conn || null); setHasConnections(Boolean(conn)); setSyncLoading(false); @@ -172,7 +189,7 @@ export default function DataPage() { setTransactionRefreshKey(k => k + 1); }, [loadHistory, loadSimplefinSummary]); - const renderPane = () => { + const renderPane = (): ReactNode => { switch (activeSection) { case 'bank-sync': return ( @@ -250,7 +267,7 @@ export default function DataPage() { // Health dot for Bank sync + "N to review" badge for Transactions. const bankDot = (syncError || !syncEnabled || !hasConnections) ? 'gray' : simplefinConn?.last_error ? 'amber' : 'green'; - const navSections = SECTIONS.map(s => + const navSections: DataNavSection[] = SECTIONS.map(s => s.id === 'bank-sync' ? { ...s, dot: bankDot } : s.id === 'transactions' ? { ...s, badge: unmatchedCount || undefined } : s,