refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
import React, { useState, useEffect, useMemo } from 'react';
|
|
|
|
|
import { toast } from 'sonner';
|
|
|
|
|
import {
|
|
|
|
|
Sparkles, CheckCircle2, Loader2, RefreshCw, Link2, Link2Off,
|
2026-06-07 20:07:27 -05:00
|
|
|
XCircle, Eye, EyeOff, Search, Plus, Clock,
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
} 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 {
|
|
|
|
|
Dialog, DialogContent, DialogDescription, DialogFooter,
|
|
|
|
|
DialogHeader, DialogTitle,
|
|
|
|
|
} from '@/components/ui/dialog';
|
|
|
|
|
import { SectionCard } from './dataShared';
|
2026-05-29 18:06:12 -05:00
|
|
|
import BillModal from '@/components/BillModal';
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
|
|
|
|
|
const TRANSACTION_FILTERS = [
|
|
|
|
|
{ id: 'open', label: 'Open', params: { ignored: 'false' } },
|
|
|
|
|
{ id: 'unmatched', label: 'Unmatched', params: { match_status: 'unmatched', ignored: 'false' } },
|
|
|
|
|
{ id: 'matched', label: 'Matched', params: { match_status: 'matched', ignored: 'false' } },
|
|
|
|
|
{ id: 'ignored', label: 'Ignored', params: { match_status: 'ignored', ignored: 'true' } },
|
|
|
|
|
{ id: 'all', label: 'All', params: { ignored: 'all' } },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function transactionStatus(tx) {
|
|
|
|
|
if (tx?.ignored) return 'ignored';
|
|
|
|
|
return tx?.match_status || 'unmatched';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function TransactionStatusBadge({ tx }) {
|
|
|
|
|
const status = transactionStatus(tx);
|
|
|
|
|
const styles = {
|
|
|
|
|
matched: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600',
|
|
|
|
|
ignored: 'border-muted-foreground/30 bg-muted/40 text-muted-foreground',
|
|
|
|
|
unmatched: 'border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<span className={cn(
|
|
|
|
|
'inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase',
|
|
|
|
|
styles[status] || styles.unmatched,
|
|
|
|
|
)}>
|
|
|
|
|
{status}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatTransactionAmount(amount, currency = 'USD') {
|
|
|
|
|
const value = Math.abs(Number(amount || 0)) / 100;
|
|
|
|
|
const sign = Number(amount || 0) < 0 ? '-' : '+';
|
|
|
|
|
return `${sign}${new Intl.NumberFormat(undefined, {
|
|
|
|
|
style: 'currency',
|
|
|
|
|
currency: currency || 'USD',
|
|
|
|
|
}).format(value)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function transactionDate(tx) {
|
|
|
|
|
return tx?.posted_date || String(tx?.transacted_at || '').slice(0, 10) || '—';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function transactionTitle(tx) {
|
|
|
|
|
return tx?.payee || tx?.description || tx?.memo || 'Transaction';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function matchScoreTone(score) {
|
|
|
|
|
const value = Number(score) || 0;
|
|
|
|
|
if (value >= 80) return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400';
|
|
|
|
|
if (value >= 55) return 'border-sky-500/30 bg-sky-500/10 text-sky-600 dark:text-sky-400';
|
|
|
|
|
return 'border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SuggestedMatchesPanel({ suggestions, loading, actionId, onAccept, onReject }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="rounded-lg border border-sky-500/20 bg-sky-500/[0.035]">
|
|
|
|
|
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-sky-500/10 px-4 py-3">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-sky-500/20 bg-sky-500/10 text-sky-600 dark:text-sky-400">
|
|
|
|
|
<Sparkles className="h-4 w-4" />
|
|
|
|
|
</span>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-semibold">Suggested matches</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">{loading ? 'Checking transactions' : `${suggestions.length} ready for review`}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="px-4 py-6 text-sm text-muted-foreground">
|
|
|
|
|
<Loader2 className="mr-2 inline h-4 w-4 animate-spin" />
|
|
|
|
|
Finding likely bill matches...
|
|
|
|
|
</div>
|
|
|
|
|
) : suggestions.length === 0 ? (
|
|
|
|
|
<div className="px-4 py-6 text-sm text-muted-foreground">
|
|
|
|
|
No suggested matches right now.
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="grid gap-2 p-3 xl:grid-cols-2">
|
|
|
|
|
{suggestions.map(suggestion => {
|
|
|
|
|
const tx = suggestion.transaction || {};
|
|
|
|
|
const bill = suggestion.bill || {};
|
|
|
|
|
const acceptBusy = actionId === `suggestion-match:${suggestion.id}`;
|
|
|
|
|
const rejectBusy = actionId === `suggestion-reject:${suggestion.id}`;
|
|
|
|
|
const busy = acceptBusy || rejectBusy;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={suggestion.id}
|
|
|
|
|
className="rounded-lg border border-border/60 bg-background/80 p-3 shadow-sm transition-colors hover:border-sky-500/30"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-start justify-between gap-3">
|
|
|
|
|
<div className="min-w-0 space-y-1">
|
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
|
|
|
<span className={cn('rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase', matchScoreTone(suggestion.score))}>
|
|
|
|
|
{suggestion.score}
|
|
|
|
|
</span>
|
|
|
|
|
<p className="truncate text-sm font-medium">{transactionTitle(tx)}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="truncate text-xs text-muted-foreground">
|
|
|
|
|
{transactionDate(tx)} · {tx.source_label || tx.source_type_label || 'Transaction'}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<p className={cn(
|
|
|
|
|
'shrink-0 text-sm font-semibold tabular-nums',
|
|
|
|
|
Number(tx.amount) < 0 ? 'text-destructive' : 'text-emerald-600',
|
|
|
|
|
)}>
|
|
|
|
|
{formatTransactionAmount(tx.amount, tx.currency)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mt-3 flex items-center gap-2 rounded-md border border-emerald-500/15 bg-emerald-500/[0.045] px-2.5 py-2">
|
|
|
|
|
<Link2 className="h-3.5 w-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" />
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="truncate text-sm font-medium">{bill.name || `Bill ${suggestion.billId}`}</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Expected ${Number(bill.expected_amount || 0).toFixed(2)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{suggestion.reasons?.length > 0 && (
|
|
|
|
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
|
|
|
{suggestion.reasons.slice(0, 4).map(reason => (
|
|
|
|
|
<span key={reason} className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[10px] text-muted-foreground">
|
|
|
|
|
{reason}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="mt-3 flex justify-end gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
disabled={busy}
|
|
|
|
|
onClick={() => onReject(suggestion)}
|
|
|
|
|
className="h-8 text-xs text-muted-foreground hover:text-destructive"
|
|
|
|
|
>
|
|
|
|
|
{rejectBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <XCircle className="h-3.5 w-3.5" />}
|
|
|
|
|
<span className="ml-1.5">Reject</span>
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
disabled={busy}
|
|
|
|
|
onClick={() => onAccept(suggestion)}
|
|
|
|
|
className="h-8 gap-1.5 text-xs"
|
|
|
|
|
>
|
|
|
|
|
{acceptBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <CheckCircle2 className="h-3.5 w-3.5" />}
|
|
|
|
|
Match
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 18:06:12 -05:00
|
|
|
function MatchBillDialog({ open, onOpenChange, transaction, bills, onConfirm, loading, onCreateBill }) {
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
const [query, setQuery] = useState('');
|
|
|
|
|
const [selectedBillId, setSelectedBillId] = useState('');
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open) {
|
|
|
|
|
setQuery('');
|
|
|
|
|
setSelectedBillId(transaction?.matched_bill_id ? String(transaction.matched_bill_id) : '');
|
|
|
|
|
}
|
|
|
|
|
}, [open, transaction?.id, transaction?.matched_bill_id]);
|
|
|
|
|
|
|
|
|
|
const filteredBills = useMemo(() => {
|
|
|
|
|
const q = query.trim().toLowerCase();
|
|
|
|
|
if (!q) return bills.slice(0, 40);
|
|
|
|
|
return bills
|
|
|
|
|
.filter(bill => String(bill.name || '').toLowerCase().includes(q))
|
|
|
|
|
.slice(0, 40);
|
|
|
|
|
}, [bills, query]);
|
|
|
|
|
|
|
|
|
|
const selectedBill = bills.find(bill => String(bill.id) === String(selectedBillId));
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
<DialogContent className="sm:max-w-2xl">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>Match Transaction</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
Choose the bill this transaction paid. Nothing changes until you confirm.
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
{transaction && (
|
|
|
|
|
<div className="rounded-lg border border-border/60 bg-muted/25 p-3">
|
|
|
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="truncate text-sm font-medium">{transactionTitle(transaction)}</p>
|
|
|
|
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
|
|
|
{transactionDate(transaction)} · {transaction.source_label || transaction.source_type_label || 'Transaction'}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<p className={cn(
|
|
|
|
|
'text-sm font-semibold tabular-nums',
|
|
|
|
|
Number(transaction.amount) < 0 ? 'text-destructive' : 'text-emerald-600',
|
|
|
|
|
)}>
|
|
|
|
|
{formatTransactionAmount(transaction.amount, transaction.currency)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
{transaction.description && transaction.description !== transactionTitle(transaction) && (
|
|
|
|
|
<p className="mt-2 text-xs text-muted-foreground">{transaction.description}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<label className="space-y-1.5">
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">Find bill</span>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
|
|
|
<Input
|
|
|
|
|
value={query}
|
|
|
|
|
onChange={e => setQuery(e.target.value)}
|
|
|
|
|
placeholder="Search bills"
|
|
|
|
|
className="pl-8"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<div className="max-h-72 overflow-y-auto rounded-lg border border-border/60">
|
|
|
|
|
{filteredBills.length === 0 ? (
|
2026-05-29 18:06:12 -05:00
|
|
|
<div className="flex flex-col items-center gap-3 px-4 py-8 text-center">
|
|
|
|
|
<p className="text-sm text-muted-foreground">No bills found.</p>
|
|
|
|
|
{onCreateBill && (() => {
|
|
|
|
|
const af = transaction?.advisory_filter;
|
|
|
|
|
const label = query.trim()
|
|
|
|
|
? `Create "${query.trim()}" as a new bill`
|
|
|
|
|
: 'Create a new bill';
|
|
|
|
|
if (af?.confidence === 'high') {
|
|
|
|
|
return (
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
Probably not a bill ·{' '}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="underline hover:text-foreground transition-colors"
|
|
|
|
|
onClick={() => { onOpenChange(false); onCreateBill(transaction, query.trim() || undefined); }}
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
</button>
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => { onOpenChange(false); onCreateBill(transaction, query.trim() || undefined); }}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-center gap-1.5 text-xs hover:underline',
|
|
|
|
|
af?.confidence === 'medium' ? 'text-muted-foreground' : 'text-primary',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Plus className="h-3.5 w-3.5" />
|
|
|
|
|
{label}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})()}
|
|
|
|
|
</div>
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
) : (
|
|
|
|
|
<div className="divide-y divide-border/40">
|
|
|
|
|
{filteredBills.map(bill => (
|
|
|
|
|
<button
|
|
|
|
|
key={bill.id}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setSelectedBillId(String(bill.id))}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/30',
|
|
|
|
|
String(selectedBillId) === String(bill.id) && 'bg-primary/5',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="truncate text-sm font-medium">{bill.name}</p>
|
|
|
|
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
|
|
|
Due day {bill.due_day ?? '—'} · expected ${Number(bill.expected_amount || 0).toFixed(2)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
{String(selectedBillId) === String(bill.id) && (
|
|
|
|
|
<CheckCircle2 className="h-4 w-4 shrink-0 text-primary" />
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DialogFooter className="gap-2">
|
2026-05-29 18:06:12 -05:00
|
|
|
{onCreateBill && (() => {
|
|
|
|
|
const af = transaction?.advisory_filter;
|
|
|
|
|
if (af?.confidence === 'high') {
|
|
|
|
|
return (
|
|
|
|
|
<span className="mr-auto flex items-center gap-2 text-xs text-muted-foreground">
|
|
|
|
|
Probably not a bill
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="underline hover:text-foreground transition-colors"
|
|
|
|
|
onClick={() => { onOpenChange(false); onCreateBill(transaction); }}
|
|
|
|
|
>
|
|
|
|
|
create anyway
|
|
|
|
|
</button>
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className={cn(
|
|
|
|
|
'mr-auto text-xs',
|
|
|
|
|
af?.confidence === 'medium'
|
|
|
|
|
? 'text-muted-foreground/60 hover:text-muted-foreground'
|
|
|
|
|
: 'text-muted-foreground hover:text-foreground',
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => { onOpenChange(false); onCreateBill(transaction); }}
|
|
|
|
|
>
|
|
|
|
|
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
|
|
|
|
Create Bill
|
|
|
|
|
</Button>
|
|
|
|
|
);
|
|
|
|
|
})()}
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={!selectedBill || loading}
|
|
|
|
|
onClick={() => selectedBill && onConfirm(selectedBill.id)}
|
|
|
|
|
>
|
|
|
|
|
{loading ? <><Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />Matching…</> : <><Link2 className="h-3.5 w-3.5 mr-1.5" />Confirm Match</>}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-07 19:41:17 -05:00
|
|
|
function parseUtc(str) {
|
|
|
|
|
if (!str) return null;
|
|
|
|
|
const normalized = str.includes('T') ? str : str.replace(' ', 'T') + 'Z';
|
|
|
|
|
return new Date(normalized);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-28 22:06:15 -05:00
|
|
|
function timeAgo(iso) {
|
|
|
|
|
if (!iso) return null;
|
2026-06-07 19:41:17 -05:00
|
|
|
const secs = Math.floor((Date.now() - parseUtc(iso).getTime()) / 1000);
|
2026-05-28 22:06:15 -05:00
|
|
|
if (secs < 60) return 'just now';
|
|
|
|
|
if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
|
|
|
|
|
if (secs < 86400) return `${Math.floor(secs / 3600)}h ago`;
|
|
|
|
|
return `${Math.floor(secs / 86400)}d ago`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 21:52:02 -05:00
|
|
|
export default function TransactionMatchingSection({ refreshKey, simplefinConn, cardProps = {} }) {
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
const [transactions, setTransactions] = useState([]);
|
|
|
|
|
const [suggestions, setSuggestions] = useState([]);
|
|
|
|
|
const [bills, setBills] = useState([]);
|
|
|
|
|
const [filter, setFilter] = useState('open');
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [suggestionsLoading, setSuggestionsLoading] = useState(true);
|
|
|
|
|
const [billsLoading, setBillsLoading] = useState(true);
|
|
|
|
|
const [actionId, setActionId] = useState(null);
|
|
|
|
|
const [matchOpen, setMatchOpen] = useState(false);
|
|
|
|
|
const [matchTransaction, setMatchTransaction] = useState(null);
|
2026-05-29 18:06:12 -05:00
|
|
|
const [categories, setCategories] = useState([]);
|
|
|
|
|
const [createBillSourceTx, setCreateBillSourceTx] = useState(null);
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
|
|
|
|
|
const currentFilter = TRANSACTION_FILTERS.find(item => item.id === filter) || TRANSACTION_FILTERS[0];
|
|
|
|
|
|
|
|
|
|
const loadTransactions = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const data = await api.transactions({ limit: 100, ...currentFilter.params });
|
|
|
|
|
setTransactions(data || []);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err.message || 'Failed to load transactions.');
|
|
|
|
|
setTransactions([]);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadSuggestions = async () => {
|
|
|
|
|
setSuggestionsLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const data = await api.matchSuggestions({ limit: 8 });
|
|
|
|
|
setSuggestions(data || []);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err.message || 'Failed to load match suggestions.');
|
|
|
|
|
setSuggestions([]);
|
|
|
|
|
} finally {
|
|
|
|
|
setSuggestionsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const refreshTransactionWorkbench = async () => {
|
|
|
|
|
await Promise.all([loadTransactions(), loadSuggestions()]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadBills = async () => {
|
|
|
|
|
setBillsLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const data = await api.bills();
|
|
|
|
|
setBills(data || []);
|
2026-05-29 01:06:20 -05:00
|
|
|
} catch (err) {
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
setBills([]);
|
2026-05-29 01:06:20 -05:00
|
|
|
toast.error(err.message || 'Failed to load bills for matching.');
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
} finally {
|
|
|
|
|
setBillsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => { loadBills(); }, []);
|
|
|
|
|
useEffect(() => { loadTransactions(); }, [filter, refreshKey]);
|
|
|
|
|
useEffect(() => { loadSuggestions(); }, [refreshKey]);
|
2026-05-29 18:06:12 -05:00
|
|
|
useEffect(() => {
|
2026-05-31 15:06:10 -05:00
|
|
|
api.categories().then(data => setCategories(data || [])).catch(err => console.error('[TransactionMatchingSection] failed to load categories', err));
|
2026-05-29 18:06:12 -05:00
|
|
|
}, []);
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
|
|
|
|
|
const openMatchDialog = (tx) => {
|
|
|
|
|
setMatchTransaction(tx);
|
|
|
|
|
setMatchOpen(true);
|
|
|
|
|
if (!bills.length && !billsLoading) loadBills();
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-29 18:06:12 -05:00
|
|
|
const openCreateBill = (tx, nameOverride) => {
|
|
|
|
|
const amount = Math.abs(Number(tx.amount || 0)) / 100;
|
|
|
|
|
const dateStr = transactionDate(tx);
|
|
|
|
|
const day = dateStr ? parseInt(dateStr.slice(8, 10), 10) : 1;
|
|
|
|
|
setCreateBillSourceTx({
|
|
|
|
|
tx,
|
|
|
|
|
initialBill: {
|
|
|
|
|
name: nameOverride || transactionTitle(tx),
|
|
|
|
|
expected_amount: amount || 0,
|
|
|
|
|
due_day: day >= 1 && day <= 31 ? day : 1,
|
|
|
|
|
billing_cycle: 'monthly',
|
|
|
|
|
cycle_type: 'monthly',
|
|
|
|
|
cycle_day: '1',
|
|
|
|
|
active: 1,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBillCreated = async (newBill) => {
|
|
|
|
|
const tx = createBillSourceTx?.tx;
|
|
|
|
|
setCreateBillSourceTx(null);
|
|
|
|
|
if (tx && newBill?.id) {
|
|
|
|
|
try {
|
|
|
|
|
await api.matchTransaction(tx.id, newBill.id);
|
|
|
|
|
toast.success('Bill created and matched to transaction.');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err.message || 'Bill created but match failed.');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
await Promise.all([loadBills(), refreshTransactionWorkbench()]);
|
|
|
|
|
};
|
|
|
|
|
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
const runTransactionAction = async (tx, action) => {
|
|
|
|
|
setActionId(`${action}:${tx.id}`);
|
|
|
|
|
try {
|
|
|
|
|
if (action === 'unmatch') {
|
|
|
|
|
await api.unmatchTransaction(tx.id);
|
|
|
|
|
toast.success('Transaction unmatched.');
|
|
|
|
|
} else if (action === 'ignore') {
|
|
|
|
|
await api.ignoreTransaction(tx.id);
|
|
|
|
|
toast.success('Transaction ignored.');
|
|
|
|
|
} else if (action === 'unignore') {
|
|
|
|
|
await api.unignoreTransaction(tx.id);
|
|
|
|
|
toast.success('Transaction restored.');
|
|
|
|
|
}
|
|
|
|
|
await refreshTransactionWorkbench();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err.message || 'Transaction action failed.');
|
|
|
|
|
} finally {
|
|
|
|
|
setActionId(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const confirmMatch = async (billId) => {
|
|
|
|
|
if (!matchTransaction) return;
|
|
|
|
|
setActionId(`match:${matchTransaction.id}`);
|
|
|
|
|
try {
|
|
|
|
|
await api.matchTransaction(matchTransaction.id, billId);
|
|
|
|
|
toast.success('Transaction matched to bill.');
|
|
|
|
|
setMatchOpen(false);
|
|
|
|
|
setMatchTransaction(null);
|
|
|
|
|
await refreshTransactionWorkbench();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err.message || 'Transaction match failed.');
|
|
|
|
|
} finally {
|
|
|
|
|
setActionId(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const acceptSuggestion = async (suggestion) => {
|
|
|
|
|
setActionId(`suggestion-match:${suggestion.id}`);
|
|
|
|
|
try {
|
|
|
|
|
await api.matchTransaction(suggestion.transactionId, suggestion.billId);
|
|
|
|
|
toast.success('Suggested match confirmed.');
|
|
|
|
|
await refreshTransactionWorkbench();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err.message || 'Suggested match failed.');
|
|
|
|
|
} finally {
|
|
|
|
|
setActionId(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const rejectSuggestion = async (suggestion) => {
|
|
|
|
|
setActionId(`suggestion-reject:${suggestion.id}`);
|
|
|
|
|
try {
|
|
|
|
|
await api.rejectMatchSuggestion(suggestion.id);
|
|
|
|
|
toast.success('Suggestion rejected.');
|
|
|
|
|
await loadSuggestions();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err.message || 'Suggestion could not be rejected.');
|
|
|
|
|
} finally {
|
|
|
|
|
setActionId(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-28 22:06:15 -05:00
|
|
|
const [quickSyncing, setQuickSyncing] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleQuickSync = async () => {
|
|
|
|
|
if (!simplefinConn) return;
|
|
|
|
|
setQuickSyncing(true);
|
|
|
|
|
try {
|
|
|
|
|
await api.syncDataSource(simplefinConn.id);
|
|
|
|
|
await refreshTransactionWorkbench();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err.message || 'Sync failed');
|
|
|
|
|
} finally {
|
|
|
|
|
setQuickSyncing(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
return (
|
|
|
|
|
<SectionCard
|
|
|
|
|
title="Transactions"
|
|
|
|
|
subtitle="Review imported or manual transactions and confirm matches to bills."
|
2026-05-30 21:52:02 -05:00
|
|
|
{...cardProps}
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
>
|
2026-05-28 22:06:15 -05:00
|
|
|
{simplefinConn && (
|
|
|
|
|
<div className="px-6 py-2 flex items-center justify-between gap-3 border-b border-border/50 bg-muted/20 text-xs text-muted-foreground">
|
|
|
|
|
<span className="flex items-center gap-1.5">
|
|
|
|
|
<span className={cn(
|
|
|
|
|
'h-1.5 w-1.5 rounded-full shrink-0',
|
|
|
|
|
simplefinConn.last_error ? 'bg-destructive' : 'bg-emerald-500',
|
|
|
|
|
)} />
|
|
|
|
|
SimpleFIN
|
|
|
|
|
{simplefinConn.last_sync_at && (
|
|
|
|
|
<span>· synced {timeAgo(simplefinConn.last_sync_at)}</span>
|
|
|
|
|
)}
|
|
|
|
|
{simplefinConn.last_error && (
|
|
|
|
|
<span className="text-destructive">· {simplefinConn.last_error}</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={handleQuickSync}
|
|
|
|
|
disabled={quickSyncing}
|
|
|
|
|
className="flex items-center gap-1 hover:text-foreground transition-colors disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
{quickSyncing
|
|
|
|
|
? <><Loader2 className="h-3 w-3 animate-spin" />Syncing…</>
|
|
|
|
|
: <><RefreshCw className="h-3 w-3" />Sync Now</>}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
<div className="px-6 py-5 space-y-4">
|
|
|
|
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
{TRANSACTION_FILTERS.map(item => (
|
|
|
|
|
<button
|
|
|
|
|
key={item.id}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setFilter(item.id)}
|
|
|
|
|
className={cn(
|
|
|
|
|
'rounded-full border px-3 py-1.5 text-xs font-medium transition-colors',
|
|
|
|
|
filter === item.id
|
|
|
|
|
? 'border-primary/40 bg-primary/10 text-primary'
|
|
|
|
|
: 'border-border/60 bg-muted/20 text-muted-foreground hover:bg-muted hover:text-foreground',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{item.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<Button size="sm" variant="outline" type="button" onClick={refreshTransactionWorkbench} disabled={loading || suggestionsLoading}>
|
|
|
|
|
{loading ? <Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5 mr-1.5" />}
|
|
|
|
|
Refresh
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<SuggestedMatchesPanel
|
|
|
|
|
suggestions={suggestions}
|
|
|
|
|
loading={suggestionsLoading}
|
|
|
|
|
actionId={actionId}
|
|
|
|
|
onAccept={acceptSuggestion}
|
|
|
|
|
onReject={rejectSuggestion}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div className="overflow-x-auto rounded-lg border border-border/60">
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="px-4 py-8 text-center text-sm text-muted-foreground">Loading transactions…</div>
|
|
|
|
|
) : transactions.length === 0 ? (
|
|
|
|
|
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
|
|
|
|
|
No transactions found for this filter.
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-05-29 16:51:31 -05:00
|
|
|
<table className="w-full min-w-[860px] table-fixed text-sm">
|
|
|
|
|
<colgroup>
|
|
|
|
|
<col className="w-[92px]" />
|
|
|
|
|
<col />
|
|
|
|
|
<col className="w-[200px]" />
|
|
|
|
|
<col className="w-[120px]" />
|
|
|
|
|
<col className="w-[96px]" />
|
|
|
|
|
</colgroup>
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
<thead>
|
|
|
|
|
<tr className="border-b border-border/50 bg-muted/30 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
|
|
|
|
<th className="px-4 py-2 text-left">Date</th>
|
|
|
|
|
<th className="px-4 py-2 text-left">Transaction</th>
|
|
|
|
|
<th className="px-4 py-2 text-left">Match</th>
|
|
|
|
|
<th className="px-4 py-2 text-right">Amount</th>
|
|
|
|
|
<th className="px-4 py-2 text-right">Actions</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody className="divide-y divide-border/30">
|
|
|
|
|
{transactions.map(tx => {
|
|
|
|
|
const status = transactionStatus(tx);
|
|
|
|
|
const busy = actionId?.endsWith(`:${tx.id}`);
|
|
|
|
|
return (
|
|
|
|
|
<tr key={tx.id} className="hover:bg-muted/20">
|
|
|
|
|
<td className="px-4 py-3 text-xs text-muted-foreground whitespace-nowrap">
|
|
|
|
|
{transactionDate(tx)}
|
|
|
|
|
</td>
|
2026-05-29 16:51:31 -05:00
|
|
|
<td className="max-w-0 px-4 py-3">
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="truncate text-sm font-medium">{transactionTitle(tx)}</p>
|
|
|
|
|
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
|
|
|
{[tx.description, tx.account_name, tx.source_label].filter(Boolean).join(' · ') || '—'}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
2026-05-29 16:51:31 -05:00
|
|
|
<td className="px-4 py-3">
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
<div className="flex flex-col gap-1.5">
|
2026-06-07 20:07:27 -05:00
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<TransactionStatusBadge tx={tx} />
|
|
|
|
|
{tx.pending ? (
|
|
|
|
|
<span className="inline-flex items-center gap-1 rounded-full border border-sky-500/30 bg-sky-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase text-sky-600 dark:text-sky-400">
|
|
|
|
|
<Clock className="h-2.5 w-2.5" />
|
|
|
|
|
Pending
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
{tx.matched_bill_name ? (
|
2026-05-29 16:51:31 -05:00
|
|
|
<span className="truncate text-xs text-foreground">{tx.matched_bill_name}</span>
|
2026-05-29 18:06:12 -05:00
|
|
|
) : tx.advisory_filter?.confidence === 'high' ? (
|
|
|
|
|
<span className="text-xs text-muted-foreground italic">Probably not a bill</span>
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
) : (
|
|
|
|
|
<span className="text-xs text-muted-foreground">No bill linked</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td className={cn(
|
|
|
|
|
'px-4 py-3 text-right font-semibold tabular-nums whitespace-nowrap',
|
|
|
|
|
Number(tx.amount) < 0 ? 'text-destructive' : 'text-emerald-600',
|
|
|
|
|
)}>
|
|
|
|
|
{formatTransactionAmount(tx.amount, tx.currency)}
|
|
|
|
|
</td>
|
2026-05-29 16:51:31 -05:00
|
|
|
<td className="px-3 py-3">
|
|
|
|
|
<div className="flex justify-end gap-1.5 whitespace-nowrap">
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
{status === 'ignored' ? (
|
2026-05-29 16:51:31 -05:00
|
|
|
<Button
|
|
|
|
|
size="icon"
|
|
|
|
|
variant="outline"
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={busy}
|
|
|
|
|
onClick={() => runTransactionAction(tx, 'unignore')}
|
|
|
|
|
className="h-8 w-8 shrink-0"
|
|
|
|
|
aria-label="Unignore transaction"
|
|
|
|
|
title="Unignore transaction"
|
|
|
|
|
>
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Eye className="h-3.5 w-3.5" />}
|
2026-05-29 16:51:31 -05:00
|
|
|
<span className="sr-only">Unignore</span>
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{status === 'matched' ? (
|
2026-05-29 16:51:31 -05:00
|
|
|
<Button
|
|
|
|
|
size="icon"
|
|
|
|
|
variant="outline"
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={busy}
|
|
|
|
|
onClick={() => runTransactionAction(tx, 'unmatch')}
|
|
|
|
|
className="h-8 w-8 shrink-0"
|
|
|
|
|
aria-label="Unmatch transaction"
|
|
|
|
|
title="Unmatch transaction"
|
|
|
|
|
>
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Link2Off className="h-3.5 w-3.5" />}
|
2026-05-29 16:51:31 -05:00
|
|
|
<span className="sr-only">Unmatch</span>
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
</Button>
|
|
|
|
|
) : (
|
2026-05-29 16:51:31 -05:00
|
|
|
<Button
|
|
|
|
|
size="icon"
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={busy || billsLoading}
|
|
|
|
|
onClick={() => openMatchDialog(tx)}
|
|
|
|
|
className="h-8 w-8 shrink-0"
|
|
|
|
|
aria-label="Match transaction"
|
|
|
|
|
title="Match transaction"
|
|
|
|
|
>
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
<Link2 className="h-3.5 w-3.5" />
|
2026-05-29 16:51:31 -05:00
|
|
|
<span className="sr-only">Match</span>
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
</Button>
|
|
|
|
|
)}
|
2026-05-29 16:51:31 -05:00
|
|
|
<Button
|
|
|
|
|
size="icon"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={busy}
|
|
|
|
|
onClick={() => runTransactionAction(tx, 'ignore')}
|
|
|
|
|
className="h-8 w-8 shrink-0"
|
|
|
|
|
aria-label="Ignore transaction"
|
|
|
|
|
title="Ignore transaction"
|
|
|
|
|
>
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <EyeOff className="h-3.5 w-3.5" />}
|
2026-05-29 16:51:31 -05:00
|
|
|
<span className="sr-only">Ignore</span>
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<MatchBillDialog
|
|
|
|
|
open={matchOpen}
|
|
|
|
|
onOpenChange={setMatchOpen}
|
|
|
|
|
transaction={matchTransaction}
|
|
|
|
|
bills={bills}
|
|
|
|
|
loading={actionId === `match:${matchTransaction?.id}`}
|
|
|
|
|
onConfirm={confirmMatch}
|
2026-05-29 18:06:12 -05:00
|
|
|
onCreateBill={openCreateBill}
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
/>
|
2026-05-29 18:06:12 -05:00
|
|
|
|
|
|
|
|
{createBillSourceTx && (
|
|
|
|
|
<BillModal
|
|
|
|
|
key={`create-from-tx-${createBillSourceTx.tx.id}`}
|
|
|
|
|
initialBill={createBillSourceTx.initialBill}
|
|
|
|
|
categories={categories}
|
|
|
|
|
onClose={() => setCreateBillSourceTx(null)}
|
|
|
|
|
onSave={handleBillCreated}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
</SectionCard>
|
|
|
|
|
);
|
|
|
|
|
}
|