import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useMemo, useState } from "react"; import { accounting } from "@/lib/accountingClient"; 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 } 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 { Plus, Search, Trash2, Receipt, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { money, fmtDate } from "./lib/format"; import { EmptyState } from "./components/EmptyState"; import { ensureUndepositedFunds } from "./lib/undeposited"; const generateNumber = () => `SR-${Date.now().toString().slice(-6)}`; const today = () => new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" }); export default function AccountingSalesReceiptsPage() { const { companyId, loading: companyLoading, error: companyError, associationId } = useCompanyId(); const cid = companyId ?? ""; const cur = "USD"; const qc = useQueryClient(); const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const [saving, setSaving] = useState(false); // Form state const [number, setNumber] = useState(generateNumber()); const [date, setDate] = useState(today()); const [customerName, setCustomerName] = useState(""); const [customerAddress, setCustomerAddress] = useState(""); const [incomeAccountId, setIncomeAccountId] = useState(""); const [depositAccountId, setDepositAccountId] = useState(""); const [quantity, setQuantity] = useState(1); const [rate, setRate] = useState(0); const [memo, setMemo] = useState(""); const total = useMemo(() => +(Number(quantity) * Number(rate)).toFixed(2), [quantity, rate]); const { data: receipts = [], isLoading } = useQuery({ queryKey: ["sales-receipts", cid], enabled: !!cid, queryFn: async () => { const { data } = await accounting .from("sales_receipts") .select("*, income_account:accounts!sales_receipts_income_account_id_fkey(name,code), deposit_account:accounts!sales_receipts_deposit_account_id_fkey(name,code)") .eq("company_id", cid) .order("receipt_date", { ascending: false }) .order("created_at", { ascending: false }); return data ?? []; }, }); const { data: incomeAccounts = [] } = useQuery({ queryKey: ["income-accounts", cid], enabled: !!cid, queryFn: async () => (await accounting.from("accounts").select("id,name,code").eq("company_id", cid).eq("type", "income").order("code")).data ?? [], }); const { data: depositAccounts = [] } = useQuery({ queryKey: ["deposit-accounts", cid], enabled: !!cid, queryFn: async () => { const { data } = await accounting .from("accounts") .select("id,name,code,is_system") .eq("company_id", cid) .or("is_bank.eq.true,name.eq.Undeposited Funds") .order("code"); return data ?? []; }, }); const reset = () => { setNumber(generateNumber()); setDate(today()); setCustomerName(""); setCustomerAddress(""); setIncomeAccountId(""); setDepositAccountId(""); setQuantity(1); setRate(0); setMemo(""); }; const openDialog = async () => { reset(); // Make sure there's somewhere to deposit to. await ensureUndepositedFunds(cid); qc.invalidateQueries({ queryKey: ["deposit-accounts", cid] }); setOpen(true); }; const save = async () => { if (!number.trim()) return toast.error("Receipt number is required"); if (!incomeAccountId) return toast.error("Select an income account"); if (!depositAccountId) return toast.error("Select a deposit account"); if (total <= 0) return toast.error("Amount must be greater than 0"); setSaving(true); try { const incomeName = (incomeAccounts as any[]).find((a) => a.id === incomeAccountId)?.name ?? "Sale"; const desc = `Sales Receipt ${number}${customerName ? " · " + customerName : ""} · ${incomeName}`; // 1. Record the receipt document const { data: sr, error: srErr } = await accounting .from("sales_receipts") .insert({ company_id: cid, number, receipt_date: date, customer_name: customerName || null, customer_address: customerAddress || null, income_account_id: incomeAccountId, deposit_account_id: depositAccountId, quantity, rate, total, memo: memo || null, }) .select("id") .single(); if (srErr || !sr) throw new Error(srErr?.message ?? "Failed to save sales receipt"); // 2. Post the money in: debit deposit account, credit income account. // The transaction triggers handle GL posting + account balances. const { data: txn, error: txnErr } = await accounting .from("transactions") .insert({ company_id: cid, account_id: depositAccountId, coa_account_id: incomeAccountId, date, type: "credit", amount: total, description: desc, category: "Sales Receipt", reference: number, }) .select("id") .single(); if (txnErr || !txn) { // Roll back the orphaned document so we don't leave a receipt with no GL impact. await accounting.from("sales_receipts").delete().eq("id", sr.id); throw new Error(txnErr?.message ?? "Failed to post sales receipt"); } await accounting.from("sales_receipts").update({ transaction_id: txn.id }).eq("id", sr.id); toast.success("Sales receipt recorded"); setOpen(false); reset(); qc.invalidateQueries({ queryKey: ["sales-receipts", cid] }); qc.invalidateQueries({ queryKey: ["accounts", cid] }); qc.invalidateQueries({ queryKey: ["transactions", cid] }); } catch (e: any) { toast.error(e?.message ?? "Failed"); } finally { setSaving(false); } }; const remove = async (r: any) => { if (!confirm(`Delete sales receipt ${r.number}? This also reverses its accounting entry.`)) return; // Delete the transaction first so its GL + balances are reversed by triggers. if (r.transaction_id) { const { error } = await accounting.from("transactions").delete().eq("id", r.transaction_id); if (error) return toast.error(error.message); } const { error } = await accounting.from("sales_receipts").delete().eq("id", r.id); if (error) return toast.error(error.message); toast.success("Sales receipt deleted"); qc.invalidateQueries({ queryKey: ["sales-receipts", cid] }); qc.invalidateQueries({ queryKey: ["accounts", cid] }); qc.invalidateQueries({ queryKey: ["transactions", cid] }); }; const filtered = useMemo(() => { const q = search.trim().toLowerCase(); if (!q) return receipts as any[]; return (receipts as any[]).filter((r) => `${r.number} ${r.customer_name ?? ""} ${r.income_account?.name ?? ""}`.toLowerCase().includes(q) ); }, [receipts, search]); if (!associationId) return

Select an association.

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

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

; return (

Sales Receipts

{filtered.length} of {(receipts as any[]).length}

{ if (!v) { setOpen(false); reset(); } }}> New Sales Receipt
setCustomerName(e.target.value)} />
setNumber(e.target.value)} />