Files
acmcc/src/pages/accounting/AccountingVendorsPage.tsx
T
2026-06-01 20:19:26 -04:00

171 lines
7.6 KiB
TypeScript

import { useQuery, useQueryClient } from "@tanstack/react-query";
import { 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 { Card, CardContent } from "@/components/ui/card";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import { Plus, Pencil, Trash2, Building2, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { EmptyState } from "./components/EmptyState";
const EMPTY = { name: "", email: "", phone: "", address: "" };
export default function AccountingVendorsPage() {
const { companyId, loading: companyLoading, error: companyError, associationId } = useCompanyId();
const cid = companyId ?? "";
const qc = useQueryClient();
const [open, setOpen] = useState(false);
const [editId, setEditId] = useState<string | null>(null);
const [form, setForm] = useState({ ...EMPTY });
const [saving, setSaving] = useState(false);
const { data: vendors = [] } = useQuery({
queryKey: ["vendors", cid],
enabled: !!cid,
queryFn: async () =>
(await accounting.from("vendors").select("*").eq("company_id", cid).order("name")).data ?? [],
});
const resetForm = () => { setEditId(null); setForm({ ...EMPTY }); };
const openEdit = (v: any) => {
setEditId(v.id);
setForm({ name: v.name ?? "", email: v.email ?? "", phone: v.phone ?? "", address: v.address ?? "" });
setOpen(true);
};
const save = async () => {
if (!form.name.trim()) return toast.error("Name required");
setSaving(true);
if (editId) {
const { error } = await accounting.from("vendors").update(form).eq("id", editId);
if (error) { setSaving(false); return toast.error(error.message); }
toast.success("Vendor updated");
} else {
const { error } = await accounting.from("vendors").insert({ ...form, company_id: cid });
if (error) { setSaving(false); return toast.error(error.message); }
toast.success("Vendor added");
}
setSaving(false);
setOpen(false);
resetForm();
qc.invalidateQueries({ queryKey: ["vendors", cid] });
qc.invalidateQueries({ queryKey: ["vendors-lookup", cid] });
};
const remove = async (id: string) => {
if (!confirm("Delete this vendor?")) return;
const { error } = await accounting.from("vendors").delete().eq("id", id);
if (error) return toast.error(error.message);
qc.invalidateQueries({ queryKey: ["vendors", cid] });
qc.invalidateQueries({ queryKey: ["vendors-lookup", cid] });
};
if (!associationId) return <p className="text-sm text-muted-foreground">Select an association.</p>;
if (companyLoading) return <div className="flex justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /></div>;
if (companyError || !companyId) return <p className="text-sm text-muted-foreground text-center py-12">{companyError || "Accounting setup is not ready."}</p>;
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold">Vendors</h1>
<p className="text-sm text-muted-foreground">{(vendors as any[]).length} vendor{(vendors as any[]).length !== 1 ? "s" : ""}</p>
</div>
<Button onClick={() => { resetForm(); setOpen(true); }}>
<Plus className="mr-1 h-4 w-4" /> New vendor
</Button>
</div>
<Card>
<CardContent className="p-0">
{(vendors as any[]).length === 0 ? (
<EmptyState icon={Building2} title="No vendors yet" description="Add your first vendor to start tracking bills and payments." />
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Phone</TableHead>
<TableHead>Address</TableHead>
<TableHead className="w-20"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{(vendors as any[]).map((v: any) => (
<TableRow key={v.id} className="hover:bg-muted/40">
<TableCell className="font-medium">{v.name}</TableCell>
<TableCell className="text-sm text-muted-foreground">
{v.email ? <a href={`mailto:${v.email}`} className="text-primary hover:underline">{v.email}</a> : "—"}
</TableCell>
<TableCell className="text-sm">{v.phone ?? "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground max-w-[200px] truncate">{v.address ?? "—"}</TableCell>
<TableCell>
<div className="flex justify-end gap-1">
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => openEdit(v)}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button size="icon" variant="ghost" className="h-7 w-7 text-destructive hover:text-destructive" onClick={() => remove(v.id)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
{/* New / Edit dialog */}
<Dialog open={open} onOpenChange={(v) => { setOpen(v); if (!v) resetForm(); }}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editId ? "Edit vendor" : "New vendor"}</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<div>
<Label>Name *</Label>
<Input maxLength={120} value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="Acme Services, LLC" autoFocus />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label>Email</Label>
<Input type="email" maxLength={255} value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} placeholder="billing@acme.com" />
</div>
<div>
<Label>Phone</Label>
<Input maxLength={40} value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} placeholder="(555) 000-0000" />
</div>
</div>
<div>
<Label>Address <span className="text-muted-foreground text-xs font-normal">(prints on checks)</span></Label>
<Textarea
rows={3}
value={form.address}
onChange={(e) => setForm({ ...form, address: e.target.value })}
placeholder={"123 Main Street\nSuite 100\nCity, ST 12345"}
className="resize-none"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => { setOpen(false); resetForm(); }}>Cancel</Button>
<Button onClick={save} disabled={saving || !form.name.trim()}>
{saving ? "Saving…" : editId ? "Save changes" : "Add vendor"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}