Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-17 02:37:21 +00:00
co-authored by renee-png
parent 970a94c7d5
commit d59f0378f0
2 changed files with 14 additions and 46 deletions
+14 -37
View File
@@ -4,17 +4,15 @@ import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { FilePlus, Loader2 } from "lucide-react";
import { FilePlus } from "lucide-react";
import { formatCurrency, formatDate, statusBadgeClass } from "@/lib/format";
import { toast } from "sonner";
import { GenerateInvoiceDialog } from "@/components/invoices/generate-invoice-dialog";
export function CaseInvoicesTab({ caseRecord }: { caseRecord: any }) {
const { user } = useAuth();
const [invoices, setInvoices] = useState<any[]>([]);
const [unbilledTime, setUnbilledTime] = useState<any[]>([]);
const [unbilledExpenses, setUnbilledExpenses] = useState<any[]>([]);
const [generating, setGenerating] = useState(false);
const [genOpen, setGenOpen] = useState(false);
const load = async () => {
const [{ data: invs }, { data: t }, { data: ex }] = await Promise.all([
@@ -33,48 +31,19 @@ export function CaseInvoicesTab({ caseRecord }: { caseRecord: any }) {
const expensesTotal = unbilledExpenses.reduce((s, e) => s + Number(e.amount), 0);
const subtotal = timeTotal + expensesTotal;
const generate = async () => {
if (subtotal <= 0) { toast.error("Nothing to invoice"); return; }
setGenerating(true);
const yr = new Date().getFullYear();
const num = `INV-${yr}-${Math.floor(1000 + Math.random() * 9000)}`;
const due = new Date(); due.setDate(due.getDate() + 30);
const { data: inv, error } = await supabase.from("invoices").insert({
invoice_number: num,
client_id: caseRecord.client.id,
case_id: caseRecord.id,
status: "draft",
issue_date: new Date().toISOString().slice(0, 10),
due_date: due.toISOString().slice(0, 10),
subtotal,
tax: 0,
total: subtotal,
created_by: user?.id,
}).select("id").single();
if (error || !inv) { toast.error(error?.message || "Failed"); setGenerating(false); return; }
// Link entries
const timeIds = unbilledTime.map((t) => t.id);
const expIds = unbilledExpenses.map((e) => e.id);
if (timeIds.length) await supabase.from("time_entries").update({ invoice_id: inv.id }).in("id", timeIds);
if (expIds.length) await supabase.from("expenses").update({ invoice_id: inv.id }).in("id", expIds);
setGenerating(false);
toast.success(`Invoice ${num} created (draft)`);
load();
};
return (
<div className="space-y-4">
<Card className="border-border/60 bg-muted/20">
<CardContent className="p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="text-sm space-y-0.5">
<div className="font-medium">Unbilled work</div>
<div className="font-medium">Unbilled work on this case</div>
<div className="text-muted-foreground text-xs">
{unbilledTime.length} time entries · {unbilledExpenses.length} expenses
</div>
<div className="font-serif text-2xl text-foreground mt-1">{formatCurrency(subtotal)}</div>
</div>
<Button onClick={generate} disabled={subtotal <= 0 || generating}>
{generating ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <FilePlus className="h-4 w-4 mr-2" />}
<Button onClick={() => setGenOpen(true)} disabled={subtotal <= 0}>
<FilePlus className="h-4 w-4 mr-2" />
Generate invoice
</Button>
</CardContent>
@@ -111,6 +80,14 @@ export function CaseInvoicesTab({ caseRecord }: { caseRecord: any }) {
</table>
</CardContent>
</Card>
<GenerateInvoiceDialog
open={genOpen}
onOpenChange={setGenOpen}
clientId={caseRecord.client.id}
clientName={caseRecord.client.name}
presetCaseId={caseRecord.id}
/>
</div>
);
}
-9
View File
@@ -616,12 +616,3 @@ const rootRouteChildren: RootRouteChildren = {
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}