diff --git a/src/routes/invoices.index.tsx b/src/routes/invoices.index.tsx index a7b030c..37503ea 100644 --- a/src/routes/invoices.index.tsx +++ b/src/routes/invoices.index.tsx @@ -10,7 +10,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { supabase } from "@/integrations/supabase/client"; -import { Receipt, Search, FilePlus, Clock, DollarSign, CheckCircle2, Undo2 } from "lucide-react"; +import { Receipt, Search, FilePlus, Clock, DollarSign, CheckCircle2, Undo2, Trash2, Send, Ban } from "lucide-react"; import { formatCurrency, formatDate, statusBadgeClass } from "@/lib/format"; import { ensureMarkedInvoicedPlaceholder } from "@/lib/invoice-generation"; import { useAuth } from "@/lib/auth"; @@ -41,6 +41,7 @@ function InvoicesIndex() { const [expFilter, setExpFilter] = useState("unbilled"); const [selectedTime, setSelectedTime] = useState>(new Set()); const [selectedExp, setSelectedExp] = useState>(new Set()); + const [selectedInvoices, setSelectedInvoices] = useState>(new Set()); const [working, setWorking] = useState(false); // Map of placeholder invoice id -> true (placeholder = MARKED-INVOICED-* or IMPORT-PREBILLED-*) @@ -205,6 +206,47 @@ function InvoicesIndex() { } }; + const refreshInvoices = async () => { + const { data } = await supabase + .from("invoices") + .select("*, client:clients(id, name), case:cases(id, case_number, title)") + .order("created_at", { ascending: false }); + setInvoices(data ?? []); + setPlaceholderIds(new Set( + (data ?? []) + .filter((i: any) => + typeof i.invoice_number === "string" && + (i.invoice_number.startsWith("MARKED-INVOICED-") || i.invoice_number.startsWith("IMPORT-PREBILLED-")) + ) + .map((i: any) => i.id), + )); + }; + + const bulkInvoiceAction = async (action: "delete" | "sent" | "void") => { + if (selectedInvoices.size === 0) { toast.error("Select at least one invoice"); return; } + const ids = Array.from(selectedInvoices); + const verb = action === "delete" ? "delete" : `mark as ${action}`; + if (!confirm(`${verb.charAt(0).toUpperCase() + verb.slice(1)} ${ids.length} invoice(s)?`)) return; + setWorking(true); + try { + if (action === "delete") { + const { error } = await supabase.from("invoices").delete().in("id", ids); + if (error) throw error; + toast.success(`Deleted ${ids.length} invoice(s)`); + } else { + const { error } = await supabase.from("invoices").update({ status: action }).in("id", ids); + if (error) throw error; + toast.success(`Marked ${ids.length} invoice(s) as ${action}`); + } + setSelectedInvoices(new Set()); + await refreshInvoices(); + } catch (err: any) { + toast.error(err?.message ?? "Bulk action failed"); + } finally { + setWorking(false); + } + }; + return ( + {selectedInvoices.size > 0 && ( +
+ {selectedInvoices.size} selected +
+ + + + +
+
+ )} + @@ -270,9 +335,9 @@ function InvoicesIndex() { - {loading && } + {loading && } {!loading && filtered.length === 0 && ( - @@ -281,6 +346,12 @@ function InvoicesIndex() { const balance = Number(i.total) - Number(i.amount_paid); return ( +
+ 0 && selectedInvoices.size === filtered.length} + onCheckedChange={(c) => setSelectedInvoices(c ? new Set(filtered.map((i) => i.id)) : new Set())} + /> + Invoice # Client Issued
Loading…
Loading…
+
No invoices yet. Click New invoice to generate one.
e.stopPropagation()}> + setSelectedInvoices((s) => toggleSel(s, i.id))} + /> + {i.invoice_number}