'use client'; import { useState, useEffect } from 'react'; import { toast } from 'sonner'; import { CheckCircle2, Circle, Loader2, AlertTriangle, CalendarDays } from 'lucide-react'; import { api } from '@/api'; import { cn, fmt, fmtDate } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from '@/components/ui/dialog'; const STATUS_META = { unmatched: { label: 'Unmatched', className: 'text-muted-foreground', icon: null }, matched_this_bill:{ label: 'Already linked', className: 'text-emerald-600 dark:text-emerald-400', icon: CheckCircle2 }, matched_other_bill:{ label: null, className: 'text-amber-600 dark:text-amber-400', icon: AlertTriangle }, payment_exists: { label: 'Payment exists', className: 'text-emerald-600 dark:text-emerald-400', icon: CheckCircle2 }, }; function StatusChip({ candidate }) { const meta = STATUS_META[candidate.status] ?? STATUS_META.unmatched; const Icon = meta.icon; const label = candidate.status === 'matched_other_bill' ? `Matched to ${candidate.matched_bill_name || 'another bill'}` : meta.label; if (!label) return null; return ( {Icon && } {label} ); } // ── Main dialog ─────────────────────────────────────────────────────────────── export default function BillHistoricalImportDialog({ billId, billName, open, onClose, onImported }) { const [step, setStep] = useState('choice'); // 'choice' | 'pick' const [candidates, setCandidates] = useState([]); const [loading, setLoading] = useState(true); const [selected, setSelected] = useState(new Set()); const [importing, setImporting] = useState(false); // Load candidates whenever the dialog opens useEffect(() => { if (!open || !billId) return; setStep('choice'); setSelected(new Set()); setLoading(true); api.merchantRuleCandidates(billId) .then(data => { // Pre-select importable candidates (not already a payment for this bill) const importable = (data.candidates || []).filter(c => c.status !== 'payment_exists' && c.status !== 'matched_this_bill'); setCandidates(data.candidates || []); setSelected(new Set(importable.map(c => c.id))); }) .catch(() => setCandidates([])) .finally(() => setLoading(false)); }, [open, billId]); const importable = candidates.filter(c => c.status !== 'payment_exists' && c.status !== 'matched_this_bill'); const alreadyDone = candidates.filter(c => c.status === 'payment_exists' || c.status === 'matched_this_bill'); async function doImport(ids) { if (ids.length === 0) { onClose(); return; } setImporting(true); try { const result = await api.importHistoricalPayments(billId, ids); toast.success(`${result.imported} payment${result.imported === 1 ? '' : 's'} imported for ${billName}`); onImported?.(result); onClose(); } catch (err) { toast.error(err.message || 'Import failed'); } finally { setImporting(false); } } function toggleAll(checked) { setSelected(checked ? new Set(importable.map(c => c.id)) : new Set()); } function toggle(id) { setSelected(prev => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); } const allSelected = importable.length > 0 && importable.every(c => selected.has(c.id)); // ── Choice step ────────────────────────────────────────────────────────────── if (step === 'choice') { return ( { if (!v) onClose(); }}> {!loading && importable.length === 0 && alreadyDone.length === 0 ? 'No past payments found' : !loading && importable.length === 0 ? 'Already up to date' : 'Import past payments'} {loading ? 'Searching your bank history…' : importable.length === 0 && alreadyDone.length > 0 ? `All matching transactions for ${billName} are already linked — nothing left to import.` : importable.length === 0 ? `No past bank transactions found matching ${billName}.` : `Found ${importable.length} past transaction${importable.length === 1 ? '' : 's'} matching ${billName}. What would you like to do?`} {loading && ( )} {!loading && importable.length > 0 && ( {/* Populate all */} doImport(importable.map(c => c.id))} disabled={importing} className="w-full rounded-xl border border-primary/30 bg-primary/5 p-3.5 text-left transition-colors hover:bg-primary/10 disabled:opacity-50" > Import all {importable.length} payments Treat your bank as the source of truth — import every matching transaction as a payment. {/* Pick one by one */} setStep('pick')} disabled={importing} className="w-full rounded-xl border border-border/60 bg-muted/20 p-3.5 text-left transition-colors hover:bg-muted/40 disabled:opacity-50" > Choose which ones Review the list and select exactly which transactions to import. {/* Skip */} Skip — future only Only import new transactions going forward. Past history stays as-is. )} {!loading && importable.length === 0 && alreadyDone.length > 0 && ( All matching transactions are already linked or have payments. Nothing to import. )} {importing && ( Importing… )} {(!loading || importing) && ( Cancel )} ); } // ── Pick step ──────────────────────────────────────────────────────────────── return ( { if (!v) onClose(); }}> Choose transactions to import Select which past transactions to import as payments for {billName}. {/* Select all toggle */} toggleAll(!allSelected)} className="flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors" > {allSelected ? : } {allSelected ? 'Deselect all' : 'Select all'} {selected.size} selected {/* Transaction list */} {importable.map(c => ( toggle(c.id)} className={cn( 'w-full flex items-center gap-3 rounded-lg border px-3 py-2.5 text-left transition-colors', selected.has(c.id) ? 'border-primary/30 bg-primary/5' : 'border-border/50 bg-background hover:bg-muted/30', )} > {selected.has(c.id) ? : } {c.payee} {fmtDate(c.paid_date)} {fmt(c.amount)} ))} {/* Already-done items (dimmed, informational) */} {alreadyDone.length > 0 && ( Already handled {alreadyDone.map(c => ( {c.payee} {fmt(c.amount)} ))} )} setStep('choice')} disabled={importing}>Back doImport([...selected])} disabled={importing || selected.size === 0} > {importing ? <>Importing…> : `Import ${selected.size} payment${selected.size === 1 ? '' : 's'}`} ); }
Import all {importable.length} payments
Treat your bank as the source of truth — import every matching transaction as a payment.
Choose which ones
Review the list and select exactly which transactions to import.
Skip — future only
Only import new transactions going forward. Past history stays as-is.
All matching transactions are already linked or have payments. Nothing to import.
{c.payee}
Already handled