import { useState } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Calendar } from "@/components/ui/calendar"; import { Calendar as CalendarIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { toDateString } from "@/lib/format"; import { UNCATEGORIZED } from "@/lib/categories"; import { toast } from "sonner"; import type { Account, Transaction } from "@/types/finance"; import { CategoryPicker } from "@/components/elements/CategoryPicker"; import { accountDisplayName } from "@/lib/accounts"; interface Props { open: boolean; onOpenChange: (open: boolean) => void; accounts: Account[]; onAdd: (t: Transaction) => Promise; } /** * Record a transaction by hand. * * The app had no way to add one at all, every transaction had to arrive through * Plaid, which makes cash spending and any non-linked account invisible. */ export function AddTransactionDialog({ open, onOpenChange, accounts, onAdd }: Props) { const [date, setDate] = useState(new Date()); const [description, setDescription] = useState(""); const [amount, setAmount] = useState(""); const [type, setType] = useState<"expense" | "income">("expense"); const [category, setCategory] = useState(UNCATEGORIZED); const [accountId, setAccountId] = useState(accounts[0]?.id ?? ""); const [saving, setSaving] = useState(false); const reset = () => { setDate(new Date()); setDescription(""); setAmount(""); setType("expense"); setCategory(UNCATEGORIZED); setAccountId(accounts[0]?.id ?? ""); }; const submit = async () => { const desc = description.trim(); const magnitude = Math.abs(parseFloat(amount)); if (!desc) { toast.error("Add a description"); return; } if (!magnitude || Number.isNaN(magnitude)) { toast.error("Enter an amount greater than zero"); return; } if (!accountId) { toast.error("Choose an account"); return; } const account = accounts.find((a) => a.id === accountId); setSaving(true); try { await onAdd({ // `manual-` keeps it clear of the `plaid-` prefix the sync deletes and // rebuilds on every run, otherwise a hand-entered row would vanish. id: `manual-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, date: toDateString(date), description: desc, category, // Expenses are stored negative, income positive, matching the Plaid sync. amount: type === "expense" ? -magnitude : magnitude, accountId, accountName: account ? accountDisplayName(account) : "Account", type, }); toast.success("Transaction added"); reset(); onOpenChange(false); } catch { toast.error("Couldn't save the transaction"); } finally { setSaving(false); } }; return ( { if (!o) reset(); onOpenChange(o); }}> Add transaction
setDescription(e.target.value)} placeholder="e.g. Farmers market" autoFocus />
setAmount(e.target.value)} onKeyDown={(e) => e.key === "Enter" && void submit()} />
d && setDate(d)} disabled={(d) => d > new Date()} initialFocus />
); }