diff --git a/src/components/forms/invoice-form.tsx b/src/components/forms/invoice-form.tsx index 2f68a3b..af3a3b8 100644 --- a/src/components/forms/invoice-form.tsx +++ b/src/components/forms/invoice-form.tsx @@ -35,7 +35,6 @@ import type { InvoiceFormData, InvoiceItem } from "./invoice/types"; import { CountUp } from "~/components/ui/count-up"; - interface InvoiceFormProps { invoiceId?: string; } @@ -48,10 +47,11 @@ function InvoiceFormSkeleton() { description="Loading invoice form" variant="gradient" /> -
{/* Tabs Skeleton */} -
-
-
+
{" "} + {/* Tabs Skeleton */} +
+
+
); @@ -59,6 +59,7 @@ function InvoiceFormSkeleton() { export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { const router = useRouter(); + const utils = api.useUtils(); // State const [formData, setFormData] = useState({ @@ -72,7 +73,14 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { taxRate: 0, defaultHourlyRate: null, items: [ - { id: crypto.randomUUID(), date: new Date(), description: "", hours: 1, rate: 0, amount: 0 }, + { + id: crypto.randomUUID(), + date: new Date(), + description: "", + hours: 1, + rate: 0, + amount: 0, + }, ], }); @@ -82,30 +90,44 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { const [activeTab, setActiveTab] = useState("details"); // Queries (Same as before) - const { data: clients, isLoading: loadingClients } = api.clients.getAll.useQuery(); - const { data: businesses, isLoading: loadingBusinesses } = api.businesses.getAll.useQuery(); - const { data: existingInvoice, isLoading: loadingInvoice } = api.invoices.getById.useQuery( - { id: invoiceId! }, { enabled: !!invoiceId && invoiceId !== "new" }, - ); + const { data: clients, isLoading: loadingClients } = + api.clients.getAll.useQuery(); + const { data: businesses, isLoading: loadingBusinesses } = + api.businesses.getAll.useQuery(); + const { data: existingInvoice, isLoading: loadingInvoice } = + api.invoices.getById.useQuery( + { id: invoiceId! }, + { enabled: !!invoiceId && invoiceId !== "new" }, + ); const deleteInvoice = api.invoices.delete.useMutation({ - onSuccess: () => { toast.success("Invoice deleted"); router.push("/dashboard/invoices"); }, + onSuccess: () => { + toast.success("Invoice deleted"); + router.push("/dashboard/invoices"); + }, onError: (e) => toast.error(e.message ?? "Failed to delete"), }); // Init Effects (Same as before) - useEffect(() => { setInitialized(false); }, [invoiceId]); + useEffect(() => { + setInitialized(false); + }, [invoiceId]); useEffect(() => { if (invoiceId && invoiceId !== "new" && existingInvoice && !initialized) { // ... (Mapping logic same as before) - const mappedItems: InvoiceItem[] = existingInvoice.items?.map((item) => ({ - id: crypto.randomUUID(), - date: new Date(item.date), - description: item.description, - hours: item.hours, - rate: item.rate, - amount: item.amount, - })).sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) || []; + const mappedItems: InvoiceItem[] = + existingInvoice.items + ?.map((item) => ({ + id: crypto.randomUUID(), + date: new Date(item.date), + description: item.description, + hours: item.hours, + rate: item.rate, + amount: item.amount, + })) + .sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + ) || []; setFormData({ invoiceNumber: existingInvoice.invoiceNumber, businessId: existingInvoice.businessId ?? "", @@ -116,18 +138,39 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { notes: existingInvoice.notes ?? "", taxRate: existingInvoice.taxRate, defaultHourlyRate: existingInvoice.client?.defaultHourlyRate ?? null, - items: mappedItems.length > 0 ? mappedItems : [{ id: crypto.randomUUID(), date: new Date(), description: "", hours: 1, rate: 0, amount: 0 }], + items: + mappedItems.length > 0 + ? mappedItems + : [ + { + id: crypto.randomUUID(), + date: new Date(), + description: "", + hours: 1, + rate: 0, + amount: 0, + }, + ], }); setInitialized(true); - } else if ((!invoiceId || invoiceId === "new") && businesses && !initialized) { - const defaultBusiness = businesses.find((b) => b.isDefault) ?? businesses[0]; - if (defaultBusiness) setFormData((prev) => ({ ...prev, businessId: defaultBusiness.id })); + } else if ( + (!invoiceId || invoiceId === "new") && + businesses && + !initialized + ) { + const defaultBusiness = + businesses.find((b) => b.isDefault) ?? businesses[0]; + if (defaultBusiness) + setFormData((prev) => ({ ...prev, businessId: defaultBusiness.id })); setInitialized(true); } }, [invoiceId, existingInvoice, businesses, initialized]); const totals = React.useMemo(() => { - const subtotal = formData.items.reduce((sum, item) => sum + item.hours * item.rate, 0); + const subtotal = formData.items.reduce( + (sum, item) => sum + item.hours * item.rate, + 0, + ); const taxAmount = (subtotal * formData.taxRate) / 100; const total = subtotal + taxAmount; return { subtotal, taxAmount, total }; @@ -138,11 +181,31 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { const validDate = date instanceof Date ? date : new Date(); setFormData((prev) => ({ ...prev, - items: [...prev.items, { id: crypto.randomUUID(), date: validDate, description: "", hours: 1, rate: prev.defaultHourlyRate ?? 0, amount: prev.defaultHourlyRate ?? 0 }], + items: [ + ...prev.items, + { + id: crypto.randomUUID(), + date: validDate, + description: "", + hours: 1, + rate: prev.defaultHourlyRate ?? 0, + amount: prev.defaultHourlyRate ?? 0, + }, + ], })); }; - const removeItem = (idx: number) => { if (formData.items.length > 1) setFormData((prev) => ({ ...prev, items: prev.items.filter((_, i) => i !== idx) })); }; - const updateItem = (idx: number, field: string, value: string | number | Date) => { + const removeItem = (idx: number) => { + if (formData.items.length > 1) + setFormData((prev) => ({ + ...prev, + items: prev.items.filter((_, i) => i !== idx), + })); + }; + const updateItem = ( + idx: number, + field: string, + value: string | number | Date, + ) => { setFormData((prev) => ({ ...prev, items: prev.items.map((item, i) => { @@ -154,7 +217,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { return updated; } return item; - }) + }), })); }; const moveItemUp = (idx: number) => { @@ -181,25 +244,48 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { return { ...prev, items: newItems }; }); }; - const reorderItems = (newItems: InvoiceItem[]) => setFormData(prev => ({ ...prev, items: newItems })); + const reorderItems = (newItems: InvoiceItem[]) => + setFormData((prev) => ({ ...prev, items: newItems })); const createInvoice = api.invoices.create.useMutation({ - onSuccess: (inv) => { toast.success("Created"); router.push(`/dashboard/invoices/${inv.id}`); }, + onSuccess: (inv) => { + toast.success("Created"); + void utils.invoices.getAll.invalidate(); + router.push(`/dashboard/invoices/${inv.id}`); + }, onError: (e) => toast.error(e.message), }); const updateInvoice = api.invoices.update.useMutation({ - onSuccess: () => { toast.success("Updated"); router.push(invoiceId === "new" ? "/dashboard/invoices" : `/dashboard/invoices/${invoiceId}`); }, + onSuccess: () => { + toast.success("Updated"); + if (invoiceId && invoiceId !== "new") { + void utils.invoices.getById.invalidate({ id: invoiceId }); + } + void utils.invoices.getAll.invalidate(); + router.push( + invoiceId === "new" + ? "/dashboard/invoices" + : `/dashboard/invoices/${invoiceId}`, + ); + }, onError: (e) => toast.error(e.message), }); const handleSubmit = async () => { setLoading(true); - if (!formData.clientId) { toast.error("Select Client"); setLoading(false); return; } + if (!formData.clientId) { + toast.error("Select Client"); + setLoading(false); + return; + } // Validate Items - Check for empty description let invalidItemIndex = -1; for (let i = 0; i < formData.items.length; i++) { - if (!formData.items[i]?.description || formData.items[i]?.description.trim() === "") { + if ( + !formData.items[i]?.description || + formData.items[i]?.description.trim() === "" + ) { invalidItemIndex = i; break; } @@ -212,12 +298,22 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { // Timeout to allow tab switch rendering setTimeout(() => { - const element = document.getElementById(`invoice-item-${invalidItemIndex}`); + const element = document.getElementById( + `invoice-item-${invalidItemIndex}`, + ); if (element) { element.scrollIntoView({ behavior: "smooth", block: "center" }); // Optional: Highlight effect element.classList.add("ring-2", "ring-destructive", "ring-offset-2"); - setTimeout(() => element.classList.remove("ring-2", "ring-destructive", "ring-offset-2"), 2000); + setTimeout( + () => + element.classList.remove( + "ring-2", + "ring-destructive", + "ring-offset-2", + ), + 2000, + ); } }, 100); return; @@ -234,27 +330,61 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { notes: formData.notes, taxRate: formData.taxRate, items: formData.items - .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) - .map(i => ({ date: i.date, description: i.description, hours: i.hours, rate: i.rate, amount: i.hours * i.rate })), + .sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + ) + .map((i) => ({ + date: i.date, + description: i.description, + hours: i.hours, + rate: i.rate, + amount: i.hours * i.rate, + })), }; - if (invoiceId && invoiceId !== "new" && invoiceId !== undefined) await updateInvoice.mutateAsync({ id: invoiceId, ...payload }); + if (invoiceId && invoiceId !== "new" && invoiceId !== undefined) + await updateInvoice.mutateAsync({ id: invoiceId, ...payload }); else await createInvoice.mutateAsync(payload); } catch (e) { console.error(e); - } finally { setLoading(false); } + } finally { + setLoading(false); + } }; - const updateField = (field: K, value: InvoiceFormData[K]) => setFormData(p => ({ ...p, [field]: value })); + const updateField = ( + field: K, + value: InvoiceFormData[K], + ) => setFormData((p) => ({ ...p, [field]: value })); const handleDelete = () => setDeleteDialogOpen(true); - const confirmDelete = () => { if (invoiceId) deleteInvoice.mutate({ id: invoiceId }); }; + const confirmDelete = () => { + if (invoiceId) deleteInvoice.mutate({ id: invoiceId }); + }; - if (!initialized || loadingClients || loadingBusinesses || (invoiceId && invoiceId !== "new" && loadingInvoice)) return ; + if ( + !initialized || + loadingClients || + loadingBusinesses || + (invoiceId && invoiceId !== "new" && loadingInvoice) + ) + return ; return ( <>
- - {invoiceId !== "new" && } + + {invoiceId !== "new" && ( + + )} + + + Delete? + Cannot be undone. + + + + + + );