refactor(ts): convert BankSyncSection to TypeScript
SimpleFIN connect/sync/backfill/accounts + bank-budget-tracking + auto-categorize. Branded Cents on account/tx amounts, Dollars on financial-account balances. Dropped unused sourceId/bills props on AccountRow. typecheck 0, build green.
This commit is contained in:
parent
5a2e37fd61
commit
0cc5cbd957
|
|
@ -1,12 +1,12 @@
|
|||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
AlertTriangle, Building2, ChevronDown, ChevronRight,
|
||||
Eye, EyeOff, ExternalLink, History, Landmark, Link2Off, Loader2, RefreshCw, Unlink,
|
||||
} from 'lucide-react';
|
||||
import { api } from '@/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { formatUSD, formatCentsUSD } from '@/lib/money';
|
||||
import { cn, errMessage } from '@/lib/utils';
|
||||
import { formatUSD, formatCentsUSD, type Cents, type Dollars } from '@/lib/money';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
|
|
@ -19,10 +19,62 @@ import {
|
|||
} from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { SectionCard } from './dataShared';
|
||||
import { SectionCard, type SectionCardProps } from './dataShared';
|
||||
import AutoMatchReview from './AutoMatchReview';
|
||||
import type { Bill } from '@/types';
|
||||
|
||||
function TokenInput({ value, onChange, disabled }) {
|
||||
interface BankTx {
|
||||
id: number | string;
|
||||
posted_date?: string | null;
|
||||
transacted_at?: string | null;
|
||||
payee?: string | null;
|
||||
description?: string | null;
|
||||
amount: Cents;
|
||||
match_status?: string | null;
|
||||
matched_bill_id?: number | null;
|
||||
matched_bill_name?: string | null;
|
||||
}
|
||||
|
||||
interface BankAccount {
|
||||
id: number | string;
|
||||
name: string;
|
||||
org_name?: string | null;
|
||||
monitored?: boolean;
|
||||
balance: Cents;
|
||||
transaction_count?: number;
|
||||
transactions: BankTx[];
|
||||
}
|
||||
|
||||
interface Connection {
|
||||
id: number | string;
|
||||
name?: string;
|
||||
provider?: string;
|
||||
status?: string;
|
||||
last_error?: string | null;
|
||||
last_sync_at?: string | null;
|
||||
account_count?: number;
|
||||
transaction_count?: number;
|
||||
}
|
||||
|
||||
interface FinAccount {
|
||||
id: number | string;
|
||||
name: string;
|
||||
org_name?: string | null;
|
||||
balance_dollars?: Dollars | null;
|
||||
}
|
||||
|
||||
interface BtPatch {
|
||||
enabled?: boolean;
|
||||
accountId?: string;
|
||||
pendingDays?: number;
|
||||
lateGraceDays?: number;
|
||||
}
|
||||
|
||||
function TokenInput({ value, onChange, disabled }: {
|
||||
value: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const [show, setShow] = useState(false);
|
||||
const tail = value.slice(-4);
|
||||
return (
|
||||
|
|
@ -57,31 +109,32 @@ function TokenInput({ value, onChange, disabled }) {
|
|||
);
|
||||
}
|
||||
|
||||
function parseUtc(str) {
|
||||
function parseUtc(str: string | null | undefined): Date | null {
|
||||
if (!str) return null;
|
||||
const normalized = str.includes('T') ? str : str.replace(' ', 'T') + 'Z';
|
||||
return new Date(normalized);
|
||||
}
|
||||
|
||||
function fmtDate(iso) {
|
||||
if (!iso) return '—';
|
||||
return parseUtc(iso).toLocaleString(undefined, {
|
||||
function fmtDate(iso: string | null | undefined): string {
|
||||
const d = parseUtc(iso);
|
||||
if (!d) return '—';
|
||||
return d.toLocaleString(undefined, {
|
||||
month: 'short', day: 'numeric', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function fmtShortDate(date) {
|
||||
function fmtShortDate(date: string | null | undefined): string {
|
||||
if (!date) return '—';
|
||||
const d = new Date(`${date}T00:00:00`);
|
||||
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
function fmtDollars(cents) {
|
||||
function fmtDollars(cents: Cents): string {
|
||||
return formatCentsUSD(cents, { dash: true });
|
||||
}
|
||||
|
||||
function MatchBadge({ status, billName }) {
|
||||
function MatchBadge({ status, billName }: { status: string; billName?: string | null }) {
|
||||
if (status === 'matched') {
|
||||
return (
|
||||
<span className="text-[10px] font-medium text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 px-1.5 py-0.5 rounded-full max-w-[120px] truncate inline-block" title={billName || 'matched'}>
|
||||
|
|
@ -95,9 +148,16 @@ function MatchBadge({ status, billName }) {
|
|||
return <span className="text-[10px] font-medium text-muted-foreground bg-muted/60 px-1.5 py-0.5 rounded-full">unmatched</span>;
|
||||
}
|
||||
|
||||
function BillPickerDialog({ open, onClose, transaction, bills, onConfirm, busy }) {
|
||||
function BillPickerDialog({ open, onClose, transaction, bills, onConfirm, busy }: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
transaction?: BankTx | null;
|
||||
bills: Bill[];
|
||||
onConfirm: (billId: number | string | null) => void;
|
||||
busy: boolean;
|
||||
}) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
const [selectedId, setSelectedId] = useState<number | string | null>(null);
|
||||
|
||||
useEffect(() => { if (open) { setSearch(''); setSelectedId(null); } }, [open]);
|
||||
|
||||
|
|
@ -165,7 +225,18 @@ function BillPickerDialog({ open, onClose, transaction, bills, onConfirm, busy }
|
|||
);
|
||||
}
|
||||
|
||||
function AccountRow({ account, sourceId, expanded, onToggleExpand, onToggleMonitored, toggling, bills, onMatch, onUnmatch, matchingTxId }) {
|
||||
interface AccountRowProps {
|
||||
account: BankAccount;
|
||||
expanded: boolean;
|
||||
onToggleExpand: () => void;
|
||||
onToggleMonitored: (accountId: number | string, monitored: boolean) => void;
|
||||
toggling: boolean;
|
||||
onMatch: (tx: BankTx) => void;
|
||||
onUnmatch: (tx: BankTx) => void;
|
||||
matchingTxId: number | string | null;
|
||||
}
|
||||
|
||||
function AccountRow({ account, expanded, onToggleExpand, onToggleMonitored, toggling, onMatch, onUnmatch, matchingTxId }: AccountRowProps) {
|
||||
const txDate = account.transactions?.[0]?.posted_date || account.transactions?.[0]?.transacted_at?.slice(0, 10);
|
||||
|
||||
return (
|
||||
|
|
@ -183,7 +254,7 @@ function AccountRow({ account, sourceId, expanded, onToggleExpand, onToggleMonit
|
|||
{/* Monitored toggle */}
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
<Switch
|
||||
checked={account.monitored}
|
||||
checked={!!account.monitored}
|
||||
onCheckedChange={v => onToggleMonitored(account.id, v)}
|
||||
disabled={toggling}
|
||||
aria-label={`Monitor ${account.name}`}
|
||||
|
|
@ -292,27 +363,30 @@ function AccountRow({ account, sourceId, expanded, onToggleExpand, onToggleMonit
|
|||
);
|
||||
}
|
||||
|
||||
export default function BankSyncSection({ onConnectionChange, cardProps = {} }) {
|
||||
const [enabled, setEnabled] = useState(null);
|
||||
export default function BankSyncSection({ onConnectionChange, cardProps = {} }: {
|
||||
onConnectionChange?: (conn: Connection | null) => void;
|
||||
cardProps?: Partial<SectionCardProps>;
|
||||
}) {
|
||||
const [enabled, setEnabled] = useState<boolean | null>(null);
|
||||
const [syncDays, setSyncDays] = useState(30);
|
||||
const [seedDays, setSeedDays] = useState(44);
|
||||
const [serverTz, setServerTz] = useState(null);
|
||||
const [connections, setConnections] = useState([]);
|
||||
const [accountsBySource, setAccountsBySource] = useState({});
|
||||
const [accountsLoading, setAccountsLoading] = useState({});
|
||||
const [accountsErrorBySource, setAccountsError] = useState({});
|
||||
const [serverTz, setServerTz] = useState<string | null>(null);
|
||||
const [connections, setConnections] = useState<Connection[]>([]);
|
||||
const [accountsBySource, setAccountsBySource] = useState<Record<string, BankAccount[]>>({});
|
||||
const [accountsLoading, setAccountsLoading] = useState<Record<string, boolean>>({});
|
||||
const [accountsErrorBySource, setAccountsError] = useState<Record<string, string>>({});
|
||||
const [loadError, setLoadError] = useState('');
|
||||
const [setupToken, setSetupToken] = useState('');
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [syncing, setSyncing] = useState(null);
|
||||
const [backfilling, setBackfilling] = useState(null);
|
||||
const [disconnectTarget, setDisconnectTarget] = useState(null);
|
||||
const [syncing, setSyncing] = useState<number | string | null>(null);
|
||||
const [backfilling, setBackfilling] = useState<number | string | null>(null);
|
||||
const [disconnectTarget, setDisconnectTarget] = useState<Connection | null>(null);
|
||||
const [disconnecting, setDisconnecting] = useState(false);
|
||||
const [expandedAccount, setExpandedAccount] = useState(null);
|
||||
const [togglingAccount, setTogglingAccount] = useState(null);
|
||||
const [bills, setBills] = useState([]);
|
||||
const [matchTarget, setMatchTarget] = useState(null); // { sourceId, tx }
|
||||
const [matchingTxId, setMatchingTxId] = useState(null);
|
||||
const [expandedAccount, setExpandedAccount] = useState<number | string | null>(null);
|
||||
const [togglingAccount, setTogglingAccount] = useState<number | string | null>(null);
|
||||
const [bills, setBills] = useState<Bill[]>([]);
|
||||
const [matchTarget, setMatchTarget] = useState<{ sourceId: number | string; tx: BankTx } | null>(null);
|
||||
const [matchingTxId, setMatchingTxId] = useState<number | string | null>(null);
|
||||
const [autoMatchRefreshKey, setAutoMatchRefreshKey] = useState(0);
|
||||
|
||||
// Bank tracking state
|
||||
|
|
@ -320,22 +394,22 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
const [btAccountId, setBtAccountId] = useState('');
|
||||
const [btPendingDays, setBtPendingDays] = useState(3);
|
||||
const [btLateGraceDays, setBtLateGraceDays] = useState(0);
|
||||
const [btAccounts, setBtAccounts] = useState([]);
|
||||
const [btAccounts, setBtAccounts] = useState<FinAccount[]>([]);
|
||||
const [btSaving, setBtSaving] = useState(false);
|
||||
|
||||
// Auto-categorization state
|
||||
const [acmEnabled, setAcmEnabled] = useState(true);
|
||||
const [acmSaving, setAcmSaving] = useState(false);
|
||||
|
||||
const loadAccounts = useCallback(async (conns) => {
|
||||
const loadAccounts = useCallback(async (conns: Connection[]) => {
|
||||
for (const conn of conns) {
|
||||
setAccountsLoading(prev => ({ ...prev, [conn.id]: true }));
|
||||
setAccountsError(prev => ({ ...prev, [conn.id]: '' }));
|
||||
try {
|
||||
const accounts = await api.dataSourceAccounts(conn.id);
|
||||
const accounts = await api.dataSourceAccounts(conn.id) as BankAccount[];
|
||||
setAccountsBySource(prev => ({ ...prev, [conn.id]: accounts }));
|
||||
} catch (err) {
|
||||
setAccountsError(prev => ({ ...prev, [conn.id]: err.message || 'Failed to load accounts' }));
|
||||
setAccountsError(prev => ({ ...prev, [conn.id]: errMessage(err, 'Failed to load accounts') }));
|
||||
} finally {
|
||||
setAccountsLoading(prev => ({ ...prev, [conn.id]: false }));
|
||||
}
|
||||
|
|
@ -346,21 +420,21 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
const loadBankTracking = useCallback(async () => {
|
||||
try {
|
||||
const [settings, accounts] = await Promise.all([
|
||||
api.settings(),
|
||||
api.settings() as Promise<Record<string, string>>,
|
||||
api.allFinancialAccounts().catch(() => []),
|
||||
]);
|
||||
setBtEnabled(settings.bank_tracking_enabled === 'true');
|
||||
setBtAccountId(settings.bank_tracking_account_id || '');
|
||||
setBtPendingDays(parseInt(settings.bank_tracking_pending_days, 10) || 3);
|
||||
setBtLateGraceDays(parseInt(settings.bank_late_attribution_days, 10) || 0);
|
||||
setBtAccounts(Array.isArray(accounts) ? accounts : []);
|
||||
setBtPendingDays(parseInt(settings.bank_tracking_pending_days || '', 10) || 3);
|
||||
setBtLateGraceDays(parseInt(settings.bank_late_attribution_days || '', 10) || 0);
|
||||
setBtAccounts(Array.isArray(accounts) ? accounts as FinAccount[] : []);
|
||||
setAcmEnabled(settings.bank_auto_categorize_merchants !== 'false');
|
||||
} catch {
|
||||
// non-fatal — bank tracking section just won't populate
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleBtSave = useCallback(async (patch) => {
|
||||
const handleBtSave = useCallback(async (patch: BtPatch) => {
|
||||
setBtSaving(true);
|
||||
try {
|
||||
const next = {
|
||||
|
|
@ -376,20 +450,20 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
if (patch.lateGraceDays !== undefined) setBtLateGraceDays(patch.lateGraceDays);
|
||||
toast.success('Bank tracking settings saved');
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to save bank tracking settings');
|
||||
toast.error(errMessage(err, 'Failed to save bank tracking settings'));
|
||||
} finally {
|
||||
setBtSaving(false);
|
||||
}
|
||||
}, [btEnabled, btAccountId, btPendingDays, btLateGraceDays]);
|
||||
|
||||
const handleAcmSave = useCallback(async (next) => {
|
||||
const handleAcmSave = useCallback(async (next: boolean) => {
|
||||
setAcmSaving(true);
|
||||
try {
|
||||
await api.saveSettings({ bank_auto_categorize_merchants: String(next) });
|
||||
setAcmEnabled(next);
|
||||
toast.success('Auto-categorization setting saved');
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to save auto-categorization setting');
|
||||
toast.error(errMessage(err, 'Failed to save auto-categorization setting'));
|
||||
} finally {
|
||||
setAcmSaving(false);
|
||||
}
|
||||
|
|
@ -399,20 +473,20 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
setLoadError('');
|
||||
try {
|
||||
const [status, sources] = await Promise.all([
|
||||
api.simplefinStatus(),
|
||||
api.simplefinStatus() as Promise<{ enabled?: boolean; sync_days?: number; seed_days?: number; timezone?: string | null }>,
|
||||
api.dataSources({ type: 'provider_sync' }),
|
||||
]);
|
||||
setEnabled(status.enabled);
|
||||
setEnabled(!!status.enabled);
|
||||
setSyncDays(status.sync_days ?? 30);
|
||||
setSeedDays(status.seed_days ?? 44);
|
||||
setServerTz(status.timezone || null);
|
||||
const conns = Array.isArray(sources) ? sources.filter(s => s.provider === 'simplefin') : [];
|
||||
const conns = Array.isArray(sources) ? (sources as Connection[]).filter(s => s.provider === 'simplefin') : [];
|
||||
setConnections(conns);
|
||||
onConnectionChange?.(conns[0] || null);
|
||||
if (conns.length > 0) loadAccounts(conns);
|
||||
} catch (err) {
|
||||
setEnabled(false);
|
||||
setLoadError(err.message || 'Failed to load bank sync status');
|
||||
setLoadError(errMessage(err, 'Failed to load bank sync status'));
|
||||
}
|
||||
}, [onConnectionChange, loadAccounts]);
|
||||
|
||||
|
|
@ -426,7 +500,7 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
}
|
||||
}, [connections, bills.length]);
|
||||
|
||||
function updateTxInState(sourceId, txId, updates) {
|
||||
function updateTxInState(sourceId: number | string, txId: number | string, updates: Partial<BankTx>) {
|
||||
setAccountsBySource(prev => ({
|
||||
...prev,
|
||||
[sourceId]: (prev[sourceId] || []).map(acc => ({
|
||||
|
|
@ -436,14 +510,14 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
}));
|
||||
}
|
||||
|
||||
const handleMatch = (sourceId, tx) => setMatchTarget({ sourceId, tx });
|
||||
const handleMatch = (sourceId: number | string, tx: BankTx) => setMatchTarget({ sourceId, tx });
|
||||
|
||||
const handleConfirmMatch = async (billId) => {
|
||||
const handleConfirmMatch = async (billId: number | string | null) => {
|
||||
if (!matchTarget || !billId) return;
|
||||
const { sourceId, tx } = matchTarget;
|
||||
setMatchingTxId(tx.id);
|
||||
try {
|
||||
const { transaction } = await api.confirmTransactionMatch(tx.id, billId);
|
||||
const { transaction } = await api.confirmTransactionMatch(tx.id, billId) as { transaction: BankTx };
|
||||
updateTxInState(sourceId, tx.id, {
|
||||
match_status: transaction.match_status,
|
||||
matched_bill_id: transaction.matched_bill_id,
|
||||
|
|
@ -452,20 +526,20 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
setMatchTarget(null);
|
||||
toast.success(`Matched to "${transaction.matched_bill_name}" — payment recorded for ${fmtShortDate(tx.posted_date || tx.transacted_at?.slice(0, 10))}.`);
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to match transaction.');
|
||||
toast.error(errMessage(err, 'Failed to match transaction.'));
|
||||
} finally {
|
||||
setMatchingTxId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnmatch = async (sourceId, tx) => {
|
||||
const handleUnmatch = async (sourceId: number | string, tx: BankTx) => {
|
||||
setMatchingTxId(tx.id);
|
||||
try {
|
||||
await api.unmatchTransaction(tx.id);
|
||||
updateTxInState(sourceId, tx.id, { match_status: 'unmatched', matched_bill_id: null, matched_bill_name: null });
|
||||
toast.success('Match removed.');
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to remove match.');
|
||||
toast.error(errMessage(err, 'Failed to remove match.'));
|
||||
} finally {
|
||||
setMatchingTxId(null);
|
||||
}
|
||||
|
|
@ -476,21 +550,21 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
if (!token) { toast.error('Paste your SimpleFIN setup token first.'); return; }
|
||||
setConnecting(true);
|
||||
try {
|
||||
const result = await api.connectSimplefin(token);
|
||||
const result = await api.connectSimplefin(token) as { accountsUpserted?: number; transactionsNew?: number };
|
||||
toast.success(`Connected — ${result.accountsUpserted} account(s), ${result.transactionsNew} new transaction(s).`);
|
||||
setSetupToken('');
|
||||
await load();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to connect SimpleFIN');
|
||||
toast.error(errMessage(err, 'Failed to connect SimpleFIN'));
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async (id) => {
|
||||
const handleSync = async (id: number | string) => {
|
||||
setSyncing(id);
|
||||
try {
|
||||
const result = await api.syncDataSource(id);
|
||||
const result = await api.syncDataSource(id) as { errlist?: string; transactionsNew?: number };
|
||||
if (result.errlist) {
|
||||
toast.warning(`Synced ${result.transactionsNew} new transaction(s), but some connections need attention: ${result.errlist}`);
|
||||
} else {
|
||||
|
|
@ -499,17 +573,17 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
setAutoMatchRefreshKey(k => k + 1);
|
||||
await load();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Sync failed');
|
||||
toast.error(errMessage(err, 'Sync failed'));
|
||||
await load();
|
||||
} finally {
|
||||
setSyncing(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackfill = async (id) => {
|
||||
const handleBackfill = async (id: number | string) => {
|
||||
setBackfilling(id);
|
||||
try {
|
||||
const result = await api.backfillDataSource(id);
|
||||
const result = await api.backfillDataSource(id) as { errlist?: string; transactionsNew?: number };
|
||||
if (result.errlist) {
|
||||
toast.warning(`Backfill complete — ${result.transactionsNew} new transaction(s). Some connections need attention: ${result.errlist}`);
|
||||
} else {
|
||||
|
|
@ -518,7 +592,7 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
setAutoMatchRefreshKey(k => k + 1);
|
||||
await load();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Backfill failed');
|
||||
toast.error(errMessage(err, 'Backfill failed'));
|
||||
await load();
|
||||
} finally {
|
||||
setBackfilling(null);
|
||||
|
|
@ -534,13 +608,13 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
setDisconnectTarget(null);
|
||||
await load();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to disconnect');
|
||||
toast.error(errMessage(err, 'Failed to disconnect'));
|
||||
} finally {
|
||||
setDisconnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleMonitored = async (sourceId, accountId, monitored) => {
|
||||
const handleToggleMonitored = async (sourceId: number | string, accountId: number | string, monitored: boolean) => {
|
||||
setTogglingAccount(accountId);
|
||||
// Optimistic update
|
||||
setAccountsBySource(prev => ({
|
||||
|
|
@ -559,13 +633,13 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
a.id === accountId ? { ...a, monitored: !monitored } : a
|
||||
),
|
||||
}));
|
||||
toast.error(err.message || 'Failed to update account');
|
||||
toast.error(errMessage(err, 'Failed to update account'));
|
||||
} finally {
|
||||
setTogglingAccount(null);
|
||||
}
|
||||
};
|
||||
|
||||
function connWarning(conn) {
|
||||
function connWarning(conn: Connection): { kind: string; label: string } | null {
|
||||
if (!conn.last_error) return null;
|
||||
if (conn.status === 'error') return { kind: 'error', label: 'Sync error' };
|
||||
// Partial errlist: sync succeeded but some bank connections need attention
|
||||
|
|
@ -729,12 +803,10 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
<AccountRow
|
||||
key={account.id}
|
||||
account={account}
|
||||
sourceId={conn.id}
|
||||
expanded={expandedAccount === account.id}
|
||||
onToggleExpand={() => setExpandedAccount(prev => prev === account.id ? null : account.id)}
|
||||
onToggleMonitored={(accountId, monitored) => handleToggleMonitored(conn.id, accountId, monitored)}
|
||||
toggling={togglingAccount === account.id}
|
||||
bills={bills}
|
||||
onMatch={tx => handleMatch(conn.id, tx)}
|
||||
onUnmatch={tx => handleUnmatch(conn.id, tx)}
|
||||
matchingTxId={matchingTxId}
|
||||
|
|
@ -863,7 +935,7 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
|||
<div className="flex items-center gap-2">
|
||||
<Landmark className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span>{acc.org_name ? `${acc.org_name} — ` : ''}{acc.name}</span>
|
||||
{acc.balance_dollars !== null && (
|
||||
{acc.balance_dollars != null && (
|
||||
<span className="ml-auto text-xs text-muted-foreground tabular-nums">
|
||||
{formatUSD(acc.balance_dollars)}
|
||||
</span>
|
||||
Loading…
Reference in New Issue