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 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';
// 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 [unmatchedCount, setUnmatchedCount] = useState(0);
const [txnTotal, setTxnTotal] = useState(null);
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);
}
}, []);
// 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 });
setUnmatchedCount(data?.summary?.unmatched || 0);
setTxnTotal(data?.summary?.total ?? null);
} catch {
setUnmatchedCount(0);
}
}, []);
useEffect(() => { loadHistory(); loadSimplefinSummary(); }, [loadHistory, loadSimplefinSummary]);
useEffect(() => { loadTxnStats(); }, [loadTxnStats, transactionRefreshKey]);
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 } };
// 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 =>
s.id === 'bank-sync' ? { ...s, dot: bankDot }
: s.id === 'transactions' ? { ...s, badge: unmatchedCount || undefined }
: s,
);
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
);
}