"use client"; import { useState } from "react"; import { api } from "~/trpc/react"; import { PageHeader } from "~/components/layout/page-header"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Badge } from "~/components/ui/badge"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Checkbox } from "~/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { DatePicker } from "~/components/ui/date-picker"; import { NumberInput } from "~/components/ui/number-input"; import { toast } from "sonner"; import { Plus, Pencil, Trash2, Receipt } from "lucide-react"; import { formatCurrency, SUPPORTED_CURRENCIES } from "~/lib/currency"; import { EXPENSE_CATEGORIES } from "~/lib/expense-categories"; interface ExpenseFormData { date: Date; description: string; amount: number; currency: string; category: string; billable: boolean; reimbursable: boolean; taxDeductible: boolean; notes: string; clientId: string; } const defaultForm: ExpenseFormData = { date: new Date(), description: "", amount: 0, currency: "USD", category: "", billable: false, reimbursable: false, taxDeductible: false, notes: "", clientId: "", }; export default function ExpensesPage() { const [open, setOpen] = useState(false); const [editId, setEditId] = useState(null); const [form, setForm] = useState(defaultForm); const [deleteId, setDeleteId] = useState(null); const utils = api.useUtils(); const { data: expenses = [], isLoading } = api.expenses.getAll.useQuery(); const { data: clients = [] } = api.clients.getAll.useQuery(); const create = api.expenses.create.useMutation({ onSuccess: () => { toast.success("Expense added"); void utils.expenses.getAll.invalidate(); setOpen(false); setForm(defaultForm); }, onError: (e) => toast.error(e.message), }); const update = api.expenses.update.useMutation({ onSuccess: () => { toast.success("Expense updated"); void utils.expenses.getAll.invalidate(); setOpen(false); setEditId(null); setForm(defaultForm); }, onError: (e) => toast.error(e.message), }); const del = api.expenses.delete.useMutation({ onSuccess: () => { toast.success("Expense deleted"); void utils.expenses.getAll.invalidate(); setDeleteId(null); }, onError: (e) => toast.error(e.message), }); const handleOpen = () => { setEditId(null); setForm(defaultForm); setOpen(true); }; const handleEdit = (expense: typeof expenses[0]) => { setEditId(expense.id); setForm({ date: new Date(expense.date), description: expense.description, amount: expense.amount, currency: expense.currency, category: expense.category ?? "", billable: expense.billable, reimbursable: expense.reimbursable, taxDeductible: expense.taxDeductible ?? false, notes: expense.notes ?? "", clientId: expense.clientId ?? "", }); setOpen(true); }; const handleSubmit = () => { if (!form.description.trim()) { toast.error("Description is required"); return; } if (form.amount <= 0) { toast.error("Amount must be greater than 0"); return; } const payload = { ...form, clientId: form.clientId || undefined, category: form.category || undefined, notes: form.notes || undefined, taxDeductible: form.taxDeductible }; if (editId) update.mutate({ id: editId, ...payload }); else create.mutate(payload); }; const totalExpenses = expenses.reduce((s, e) => s + e.amount, 0); const billableTotal = expenses.filter((e) => e.billable).reduce((s, e) => s + e.amount, 0); const deductibleTotal = expenses.filter((e) => e.taxDeductible).reduce((s, e) => s + e.amount, 0); return (
{/* Summary cards */}

Total

{formatCurrency(totalExpenses)}

Billable

{formatCurrency(billableTotal)}

Deductible

{formatCurrency(deductibleTotal)}

Count

{expenses.length}

{/* Expenses list */} All Expenses {isLoading ? (
Loading…
) : expenses.length === 0 ? (

No expenses yet. Add your first expense.

) : (
{expenses.map((expense) => (

{expense.description}

{expense.billable && Billable} {expense.reimbursable && Reimbursable} {expense.taxDeductible && Tax Deductible} {expense.category && {expense.category}}

{new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric" }).format(new Date(expense.date))} {expense.client ? ` · ${expense.client.name}` : ""}

{expense.notes &&

{expense.notes}

}

{formatCurrency(expense.amount, expense.currency)}

))}
)}
{/* Add/Edit dialog */} {editId ? "Edit Expense" : "Add Expense"}
setForm((p) => ({ ...p, description: e.target.value }))} placeholder="e.g. Laptop charger" />
setForm((p) => ({ ...p, amount: v }))} min={0} step={0.01} />
setForm((p) => ({ ...p, date: d ?? new Date() }))} className="w-full" />
setForm((p) => ({ ...p, notes: e.target.value }))} placeholder="Additional details…" />
{/* Delete dialog */} !o && setDeleteId(null)}> Delete Expense This action cannot be undone.
); }