import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { accounting } from "@/lib/accountingClient"; import { supabase } from "@/integrations/supabase/client"; import { useCompanyId } from "./lib/useCompanyId"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Plus, Trash2, Upload, FileText, Wallet, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { money, fmtDate } from "./lib/format"; import { EmptyState } from "./components/EmptyState"; import { TableSkeleton } from "./components/TableSkeleton"; import { handlePayment, applyPaymentToBill } from "./lib/autoBill"; import { MatchBillDialog } from "./components/MatchBillDialog"; const CATEGORIES = [ "Advertising", "Travel", "Meals & Entertainment", "Office Supplies", "Software", "Utilities", "Rent", "Payroll", "Professional Services", "Equipment", "Insurance", "Bank Fees", "Other", ]; const CURRENCIES = ["USD", "EUR", "GBP", "CAD", "AUD", "INR", "JPY"]; export default function AccountingExpensesPage() { const { companyId, loading: companyLoading, error: companyError, associationId } = useCompanyId(); const cid = companyId ?? ""; const cur = "USD"; const qc = useQueryClient(); const [open, setOpen] = useState(false); const [uploading, setUploading] = useState(false); const [date, setDate] = useState(new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" })); const [category, setCategory] = useState(""); const [vendorId, setVendorId] = useState(""); const [vendorName, setVendorName] = useState(""); const [amount, setAmount] = useState(0); const [currency, setCurrency] = useState(cur); const [paidThrough, setPaidThrough] = useState(""); const [reimbursable, setReimbursable] = useState(false); const [receiptUrl, setReceiptUrl] = useState(""); const [notes, setNotes] = useState(""); const [matchOpen, setMatchOpen] = useState(false); const [matchCandidates, setMatchCandidates] = useState([]); const [pendingPayment, setPendingPayment] = useState<{ amount: number; vendorId: string; date: string; category: string; sourceId: string } | null>(null); const { data: expenses = [], isLoading } = useQuery({ queryKey: ["expenses", cid], enabled: !!cid, queryFn: async () => (await accounting .from("expenses") .select("*, vendors(name), accounts(name)") .eq("company_id", cid) .order("date", { ascending: false })).data ?? [], }); const { data: vendors = [] } = useQuery({ queryKey: ["vendors-lookup", cid], enabled: !!cid, queryFn: async () => (await accounting.from("vendors").select("id,name").eq("company_id", cid).order("name")).data ?? [], }); const { data: accounts = [] } = useQuery({ queryKey: ["bank-accounts", cid, "expenses-payment"], enabled: !!cid, // Paid-through accounts: banks plus equity accounts; archived hidden. queryFn: async () => (await accounting.from("accounts").select("id,name").eq("company_id", cid).or("is_bank.eq.true,type.eq.equity").eq("is_archived", false).order("name")).data ?? [], }); const reset = () => { setDate(new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" })); setCategory(""); setVendorId(""); setVendorName(""); setAmount(0); setCurrency(cur); setPaidThrough(""); setReimbursable(false); setReceiptUrl(""); setNotes(""); }; const uploadReceipt = async (file: File) => { if (file.size > 10 * 1024 * 1024) return toast.error("File must be under 10MB"); setUploading(true); const path = `${cid}/${Date.now()}-${file.name}`; const { error } = await supabase.storage.from("receipts").upload(path, file); setUploading(false); if (error) return toast.error(error.message); const { data } = supabase.storage.from("receipts").getPublicUrl(path); setReceiptUrl(data.publicUrl); toast.success("Receipt uploaded"); }; const save = async () => { if (!category) return toast.error("Category is required"); if (!vendorId) return toast.error("Vendor is required — select one from the list"); if (!amount || amount <= 0) return toast.error("Amount must be greater than 0"); const selectedVendor = (vendors as any[]).find((v: any) => v.id === vendorId); const { data: inserted, error } = await accounting.from("expenses").insert({ company_id: cid, date, category, vendor_id: vendorId || null, vendor_name: selectedVendor?.name ?? vendorName ?? null, amount, currency, paid_through_account_id: paidThrough || null, reimbursable, receipt_url: receiptUrl || null, notes: notes || null, }).select("id").single(); if (error || !inserted) return toast.error(error?.message ?? "Failed"); toast.success("Expense recorded"); setOpen(false); reset(); qc.invalidateQueries({ queryKey: ["expenses", cid] }); // Auto-bill creation if (vendorId) { const result = await handlePayment({ companyId: cid, vendorId, amount, date, category, description: notes, sourceKind: "expense", sourceId: inserted.id, }); if (result.kind === "created") { toast.warning(`No existing bill found — a bill was automatically created and marked as paid for ${selectedVendor?.name ?? "vendor"}.`, { action: { label: "View Bill", onClick: () => window.location.assign("/dashboard/accounting/bills") }, duration: 8000, }); qc.invalidateQueries({ queryKey: ["bills", cid] }); } else if (result.kind === "matched") { setMatchCandidates(result.candidates); setPendingPayment({ amount, vendorId, date, category, sourceId: inserted.id }); setMatchOpen(true); } } }; const remove = async (id: string) => { await accounting.from("expenses").delete().eq("id", id); qc.invalidateQueries({ queryKey: ["expenses", cid] }); }; const total = (expenses as any[]).reduce((s: number, e: any) => s + Number(e.amount), 0); if (!associationId) return

Select an association.

; if (companyLoading) return
; if (companyError || !companyId) return

{companyError || "Accounting setup is not ready."}

; return (

Expenses

{(expenses as any[]).length} expenses · {money(total, cur)} total

{ setOpen(v); if (!v) reset(); }}> Record expense
setDate(e.target.value)} />
{(vendors as any[]).length === 0 && (

No vendors yet — add one on the Vendors page first.

)}
setAmount(Number(e.target.value))} />
{receiptUrl && ( View uploaded receipt )}