import React, { useState, useEffect, useCallback, useRef, Suspense, lazy } from 'react'; import { useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import { motion, AnimatePresence, useReducedMotion } from 'framer-motion'; import { Landmark, ArrowRightLeft, Upload, DatabaseBackup, FileSpreadsheet, FileText, FlaskConical, Download, RotateCcw, History, } from 'lucide-react'; import { api } from '@/api'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import ConnectionHero from '@/components/data/ConnectionHero'; import DataNav from '@/components/data/DataNav'; import BankSyncSection from '@/components/data/BankSyncSection'; import BillRulesManager from '@/components/BillRulesManager'; import ImportTransactionCsvSection from '@/components/data/ImportTransactionCsvSection'; import TransactionMatchingSection from '@/components/data/TransactionMatchingSection'; import SeedDemoDataSection from '@/components/data/SeedDemoDataSection'; import DownloadMyDataSection from '@/components/data/DownloadMyDataSection'; import ImportHistorySection from '@/components/data/ImportHistorySection'; // 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 = [ { 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 SECTION_KEY = 'billtracker:data.section'; const LEGACY_KEY = 'billtracker:data.activeTab'; // old 3-tab key → migrate function storedSection() { if (typeof window === 'undefined') return null; const s = window.localStorage.getItem(SECTION_KEY); return SECTION_IDS.includes(s) ? s : null; } function PaneSkeleton() { return (
); } export default function DataPage() { const [searchParams, setSearchParams] = useSearchParams(); const [history, setHistory] = useState(null); const [historyLoading, setHistoryLoading] = useState(true); const [transactionRefreshKey, setTransactionRefreshKey] = useState(0); 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 reduceMotion = useReducedMotion(); 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 goTo = useCallback((id) => { if (!SECTION_IDS.includes(id)) return; window.localStorage.setItem(SECTION_KEY, id); setSearchParams(prev => { const p = new URLSearchParams(prev); p.set('section', id); return p; }); }, [setSearchParams]); // 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; const legacy = window.localStorage.getItem(LEGACY_KEY); const migrated = { sync: 'bank-sync', import: 'import', export: 'export' }[legacy]; const target = storedSection() || migrated || DEFAULT_SECTION; setSearchParams(prev => { const p = new URLSearchParams(prev); p.set('section', target); return p; }, { replace: true }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Move focus to the pane on section change (keyboard/SR), but not on first load. useEffect(() => { if (firstRender.current) { firstRender.current = false; return; } paneRef.current?.focus?.(); }, [activeSection]); const loadHistory = useCallback(async () => { setHistoryLoading(true); try { const { history } = await api.importHistory(); setHistory(history); } catch (err) { setHistory([]); toast.error(err.message || 'Failed to load import history.'); } finally { setHistoryLoading(false); } }, []); const loadSimplefinSummary = useCallback(async () => { setSyncLoading(true); setSyncError(false); try { const [status, sources] = await Promise.all([ api.simplefinStatus(), 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') : []; setSimplefinConn(conns[0] || null); } catch { setSyncError(true); setSimplefinConn(null); } finally { setSyncLoading(false); } }, []); useEffect(() => { loadHistory(); loadSimplefinSummary(); }, [loadHistory, loadSimplefinSummary]); const handleTransactionImportComplete = useCallback(() => { loadHistory(); setTransactionRefreshKey(k => k + 1); }, [loadHistory]); // BankSyncSection reports connect/sync/disconnect changes. const handleConnectionChange = useCallback((conn) => { setSimplefinConn(conn || null); setHasConnections(Boolean(conn)); setSyncLoading(false); setTransactionRefreshKey(k => k + 1); }, []); const handleSynced = useCallback(() => { loadSimplefinSummary(); setTransactionRefreshKey(k => k + 1); }, [loadSimplefinSummary]); const renderPane = () => { switch (activeSection) { case 'bank-sync': return ( ); case 'transactions': return (
); case 'import': return (
}>
); case 'export': return (
}>
); default: return null; } }; const motionProps = reduceMotion ? {} : { initial: { opacity: 0, y: 6 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -6 }, transition: { duration: 0.15 } }; return (

Data

Manage how your bills, payments & transactions get in and out.

User data only
This page only manages your own records — other users' data is not accessible here
{renderPane()}
); }