import { writeFileSync } from "fs"; writeFileSync("app/transactions/page.tsx", `"use client"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { AppShell } from "../../components/app-shell"; import { apiFetch, getStoredToken } from "../../lib/api"; type ApiResponse = { data: T; meta: { timestamp: string; version: "v1" }; error: null | { message: string; code?: string }; }; type TransactionRow = { id: string; name?: string; description?: string; amount: string; category?: string | null; note?: string | null; status?: string; hidden?: boolean; date: string; accountId?: string | null; }; type Account = { id: string; institutionName: string; accountType: string; mask?: string | null; }; type ImportResult = { imported: number; skipped: number; errors?: string[]; }; export default function TransactionsPage() { const [rows, setRows] = useState([]); const [status, setStatus] = useState("Loading transactions..."); const [summary, setSummary] = useState<{ total: string; count: number; income?: string; expense?: string; net?: string; } | null>(null); const [datePreset, setDatePreset] = useState("this_month"); const [showFilters, setShowFilters] = useState(false); const [accounts, setAccounts] = useState([]); const [autoSync, setAutoSync] = useState(true); const [isSyncing, setIsSyncing] = useState(false); const [showManual, setShowManual] = useState(false); const [showImport, setShowImport] = useState(false); const [importStatus, setImportStatus] = useState(""); const [importLoading, setImportLoading] = useState(false); const fileInputRef = useRef(null); const [manualForm, setManualForm] = useState({ accountId: "", date: new Date().toISOString().slice(0, 10), description: "", amount: "", category: "", note: "", }); const [editingId, setEditingId] = useState(null); const [editForm, setEditForm] = useState({ category: "", note: "", hidden: false }); const [filters, setFilters] = useState({ startDate: "", endDate: "", minAmount: "", maxAmount: "", category: "", source: "", search: "", includeHidden: false, }); const applyPreset = (preset: string) => { setDatePreset(preset); if (preset === "custom") return; const now = new Date(); const end = new Date(now.getFullYear(), now.getMonth(), now.getDate()); let start = new Date(end); if (preset === "this_month") { start = new Date(end.getFullYear(), end.getMonth(), 1); } else if (preset === "last_month") { start = new Date(end.getFullYear(), end.getMonth() - 1, 1); end.setDate(0); } else if (preset === "last_6_months") { start = new Date(end.getFullYear(), end.getMonth() - 5, 1); } else if (preset === "last_year") { start = new Date(end.getFullYear() - 1, 0, 1); end.setMonth(11, 31); } const fmt = (d: Date) => d.toISOString().slice(0, 10); setFilters((prev) => ({ ...prev, startDate: fmt(start), endDate: fmt(end) })); }; const buildQuery = () => { const params = new URLSearchParams(); if (filters.startDate) params.set("start_date", filters.startDate); if (filters.endDate) params.set("end_date", filters.endDate); if (filters.minAmount) params.set("min_amount", filters.minAmount); if (filters.maxAmount) params.set("max_amount", filters.maxAmount); if (filters.category) params.set("category", filters.category); if (filters.source) params.set("source", filters.source); if (filters.search) params.set("search", filters.search); if (filters.includeHidden) params.set("include_hidden", "true"); return params.toString() ? \`?\${params.toString()}\` : ""; }; const load = async () => { const query = buildQuery(); const res = await apiFetch(\`/api/transactions\${query}\`); if (res.error) { setStatus(res.error.message ?? "Unable to load transactions."); return; } setRows(res.data ?? []); setStatus((res.data ?? []).length ? "" : "No transactions yet."); }; const loadAccounts = async () => { const res = await apiFetch("/api/accounts"); if (!res.error) setAccounts(res.data ?? []); }; const loadSummary = async () => { const query = buildQuery(); const res = await apiFetch<{ total: string; count: number }>(\`/api/transactions/summary\${query}\`); if (!res.error) setSummary(res.data); }; useEffect(() => { applyPreset("this_month"); load(); loadSummary(); loadAccounts(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (!autoSync) return; const id = setInterval(() => { onSync(); }, 5 * 60 * 1000); return () => clearInterval(id); // eslint-disable-next-line react-hooks/exhaustive-deps }, [autoSync, filters.startDate, filters.endDate]); const onSync = async () => { if (isSyncing) return; setIsSyncing(true); setStatus("Syncing transactions..."); const res = await apiFetch("/api/transactions/sync", { method: "POST", body: JSON.stringify({ startDate: filters.startDate || undefined, endDate: filters.endDate || undefined }), }); if (res.error) { setStatus(res.error.message ?? "Sync failed."); setIsSyncing(false); return; } setStatus("Sync complete."); await load(); await loadSummary(); setIsSyncing(false); }; const onImportCsv = async (file: File) => { setImportLoading(true); setImportStatus("Uploading..."); const formData = new FormData(); formData.append("file", file); const token = getStoredToken(); try { const res = await fetch("/api/transactions/import", { method: "POST", headers: token ? { Authorization: \`Bearer \${token}\` } : {}, body: formData, }); const payload = (await res.json()) as ApiResponse; if (!res.ok || payload.error) { setImportStatus(payload.error?.message ?? "Import failed."); setImportLoading(false); return; } const r = payload.data; setImportStatus(\`Imported \${r.imported} transaction\${r.imported === 1 ? "" : "s"}, skipped \${r.skipped} duplicate\${r.skipped === 1 ? "" : "s"}.\`); await load(); await loadSummary(); } catch { setImportStatus("Import failed. Please try again."); } setImportLoading(false); }; const formatAmount = (value: string) => { const numeric = Number.parseFloat(value.replace(/[^0-9.-]/g, "")); if (Number.isNaN(numeric)) return { display: value, tone: "text-foreground" }; return { display: numeric < 0 ? \`-\$\${Math.abs(numeric).toFixed(2)}\` : \`\$\${numeric.toFixed(2)}\`, tone: numeric < 0 ? "text-foreground" : "text-primary font-bold", }; }; const onManualCreate = async (event: React.FormEvent) => { event.preventDefault(); const amount = Number.parseFloat(manualForm.amount); if (Number.isNaN(amount)) { setStatus("Invalid amount."); return; } setStatus("Saving manual transaction..."); const res = await apiFetch("/api/transactions/manual", { method: "POST", body: JSON.stringify({ accountId: manualForm.accountId || undefined, date: manualForm.date, description: manualForm.description, amount, category: manualForm.category || undefined, note: manualForm.note || undefined, }), }); if (res.error) { setStatus(res.error.message ?? "Unable to save transaction."); return; } setManualForm((prev) => ({ ...prev, description: "", amount: "", category: "", note: "" })); setShowManual(false); setStatus("Manual transaction saved."); await load(); await loadSummary(); }; const startEdit = (row: TransactionRow) => { setEditingId(row.id); setEditForm({ category: row.category ?? "", note: row.note ?? "", hidden: Boolean(row.hidden) }); }; const saveEdit = async () => { if (!editingId) return; setStatus("Saving edits..."); const res = await apiFetch(\`/api/transactions/\${editingId}/derived\`, { method: "PATCH", body: JSON.stringify({ userCategory: editForm.category || undefined, userNotes: editForm.note || undefined, isHidden: editForm.hidden, }), }); if (res.error) { setStatus(res.error.message ?? "Unable to save edits."); return; } setEditingId(null); setStatus("Transaction updated."); await load(); await loadSummary(); }; const inputCls = "mt-2 w-full rounded-md border border-border bg-background/50 px-3 py-2 text-sm text-foreground focus:border-primary focus:ring-primary focus:outline-none"; const labelCls = "text-xs font-semibold text-muted-foreground uppercase tracking-wider"; return ( {/* Action bar */}
{datePreset === "custom" ? "Custom range" : datePreset.replace(/_/g, " ")} Export
{/* CSV Import panel */} {showImport && (

Import CSV

Supports Chase, Bank of America, Wells Fargo, and generic CSV formats. Duplicate transactions are skipped automatically.

fileInputRef.current?.click()} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); const file = e.dataTransfer.files[0]; if (file && file.name.endsWith(".csv")) onImportCsv(file); }} > { const f = e.target.files?.[0]; if (f) onImportCsv(f); }} /> {importLoading ? (

Uploading...

) : (

Drop a CSV file here or click to browse

)}
{importStatus && (

{importStatus}

)}
)} {/* Manual transaction form */} {showManual && (

Add Manual Transaction

setManualForm((p) => ({ ...p, date: e.target.value }))} className={inputCls} required />
setManualForm((p) => ({ ...p, description: e.target.value }))} className={inputCls} required />
setManualForm((p) => ({ ...p, amount: e.target.value }))} className={inputCls} required placeholder="-42.50" />
setManualForm((p) => ({ ...p, category: e.target.value }))} className={inputCls} />
setManualForm((p) => ({ ...p, note: e.target.value }))} className={inputCls} />
)} {/* Filters */} {showFilters && (
setFilters((p) => ({ ...p, startDate: e.target.value }))} className={inputCls} disabled={datePreset !== "custom"} />
setFilters((p) => ({ ...p, endDate: e.target.value }))} className={inputCls} disabled={datePreset !== "custom"} />
setFilters((p) => ({ ...p, search: e.target.value }))} className={inputCls} placeholder="Description..." />
setFilters((p) => ({ ...p, category: e.target.value }))} className={inputCls} />
setFilters((p) => ({ ...p, minAmount: e.target.value }))} className={inputCls} />
setFilters((p) => ({ ...p, maxAmount: e.target.value }))} className={inputCls} />
)} {/* Summary cards */} {summary && (
{[ { label: "Total", value: \`\$\${Math.abs(Number.parseFloat(summary.total ?? "0")).toFixed(2)}\`, sub: \`\${summary.count} transactions\` }, { label: "Income", value: \`+\$\${Math.abs(Number.parseFloat(summary.income ?? "0")).toFixed(2)}\`, sub: "Credits" }, { label: "Expenses", value: \`-\$\${Math.abs(Number.parseFloat(summary.expense ?? "0")).toFixed(2)}\`, sub: "Debits" }, ].map((c) => (

{c.label}

{c.value}

{c.sub}

))}
)} {/* Transaction table */}
{status && (
{status}
)}
{rows.map((row) => editingId === row.id ? ( ) : ( ) )} {!rows.length && !status && ( )}
Date Description Category Amount Actions
setEditForm((p) => ({ ...p, category: e.target.value }))} placeholder="Category" className="w-28 rounded border border-border bg-background px-2 py-1 text-xs text-foreground focus:border-primary focus:outline-none" /> setEditForm((p) => ({ ...p, note: e.target.value }))} placeholder="Note" className="flex-1 rounded border border-border bg-background px-2 py-1 text-xs text-foreground focus:border-primary focus:outline-none" />
{new Date(row.date).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })} {row.description ?? row.name ?? "—"} {row.category ? ( {row.category} ) : ( )} {formatAmount(row.amount).display}
No transactions found. Try adjusting your filters or sync your accounts.
); } `); console.log("✅ transactions/page.tsx written with CSV import UI and apiFetch");