import React, { useState, useEffect, useCallback } from 'react';
import { toast } from 'sonner';
import { Building2, Eye, EyeOff, ExternalLink, Link2Off, Loader2, RefreshCw } from 'lucide-react';
import { api } from '@/api';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { SectionCard } from './dataShared';
function TokenInput({ value, onChange, disabled }) {
const [show, setShow] = useState(false);
const tail = value.slice(-4);
return (
{value && (
setShow(v => !v)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
>
{show ? : }
)}
{value && !show && (
···{tail}
)}
);
}
export default function BankSyncSection({ onConnectionChange }) {
const [enabled, setEnabled] = useState(null);
const [connections, setConnections] = useState([]);
const [loadError, setLoadError] = useState('');
const [setupToken, setSetupToken] = useState('');
const [connecting, setConnecting] = useState(false);
const [syncing, setSyncing] = useState(null);
const [disconnectTarget, setDisconnectTarget] = useState(null);
const [disconnecting, setDisconnecting] = useState(false);
const load = useCallback(async () => {
setLoadError('');
try {
const [status, sources] = await Promise.all([
api.simplefinStatus(),
api.dataSources({ type: 'provider_sync' }),
]);
setEnabled(status.enabled);
const conns = Array.isArray(sources) ? sources.filter(s => s.provider === 'simplefin') : [];
setConnections(conns);
onConnectionChange?.(conns[0] || null);
} catch (err) {
setEnabled(false);
setLoadError(err.message || 'Failed to load bank sync status');
}
}, [onConnectionChange]);
useEffect(() => { load(); }, [load]);
const handleConnect = async () => {
const token = setupToken.trim();
if (!token) { toast.error('Paste your SimpleFIN setup token first.'); return; }
setConnecting(true);
try {
const result = await api.connectSimplefin(token);
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');
} finally {
setConnecting(false);
}
};
const handleSync = async (id) => {
setSyncing(id);
try {
const result = await api.syncDataSource(id);
toast.success(`Synced — ${result.transactionsNew} new transaction(s).`);
await load();
} catch (err) {
toast.error(err.message || 'Sync failed');
await load();
} finally {
setSyncing(null);
}
};
const handleDisconnect = async () => {
if (!disconnectTarget) return;
setDisconnecting(true);
try {
await api.deleteDataSource(disconnectTarget.id);
toast.success('SimpleFIN disconnected.');
setDisconnectTarget(null);
await load();
} catch (err) {
toast.error(err.message || 'Failed to disconnect');
} finally {
setDisconnecting(false);
}
};
function fmtDate(iso) {
if (!iso) return '—';
return new Date(iso).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
}
if (enabled === null) {
return (
Loading…
);
}
return (
<>
{!enabled ? (
Bank sync is not enabled on this server. Ask your administrator to enable it in the Admin panel.
) : (
<>
{loadError && (
{loadError}
)}
{connections.length > 0 && connections.map(conn => (
{conn.name}
{conn.account_count} account{conn.account_count !== 1 ? 's' : ''} ·{' '}
{conn.transaction_count} transaction{conn.transaction_count !== 1 ? 's' : ''}
handleSync(conn.id)}
disabled={syncing === conn.id}
className="h-8 text-xs gap-1.5"
>
{syncing === conn.id
? <> Syncing…>
: <> Sync Now>}
setDisconnectTarget(conn)}
className="h-8 text-xs gap-1.5 text-muted-foreground hover:text-destructive"
>
Disconnect
Last sync
{fmtDate(conn.last_sync_at)}
Status
{conn.last_error ? conn.last_error : (conn.status === 'active' ? 'Active' : conn.status)}
))}
{connections.length === 0 && (
Connect a SimpleFIN Bridge account
Paste your SimpleFIN setup token below. BillTracker only stores an encrypted access URL — no bank credentials are saved.
Need a token?{' '}
Open SimpleFIN Bridge
setSetupToken(e.target.value)}
disabled={connecting}
/>
{connecting ? <> Connecting…> : 'Connect'}
)}
{connections.length > 0 && (
setSetupToken(e.target.value)}
disabled={connecting}
/>
{connecting ? <> Connecting…> : 'Connect'}
)}
>
)}
{ if (!open) setDisconnectTarget(null); }}>
Disconnect SimpleFIN?
This removes the connection and deletes synced accounts. Previously synced transactions
are kept but will no longer be associated with a data source.
Cancel
{disconnecting ? 'Disconnecting…' : 'Disconnect'}
>
);
}