From d9515f7723c6a863ea623a5f2b24c50d7f228e68 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Thu, 31 Jul 2025 19:11:20 -0400 Subject: [PATCH] Make hourly rate optional for clients and invoices --- src/components/data/data-table.tsx | 10 ++- src/components/forms/client-form.tsx | 39 ++++++++---- src/components/forms/invoice-form.tsx | 89 ++++++++++++++++++++------- src/components/ui/card.tsx | 2 +- src/server/api/routers/clients.ts | 2 +- src/server/db/schema.ts | 2 +- 6 files changed, 104 insertions(+), 40 deletions(-) diff --git a/src/components/data/data-table.tsx b/src/components/data/data-table.tsx index b333e94..0abfd77 100644 --- a/src/components/data/data-table.tsx +++ b/src/components/data/data-table.tsx @@ -251,9 +251,15 @@ export function DataTable({ - All {column.title} + + All {column.title} + {column.options.map((option) => ( - + {option.label} ))} diff --git a/src/components/forms/client-form.tsx b/src/components/forms/client-form.tsx index c723e33..2c816b8 100644 --- a/src/components/forms/client-form.tsx +++ b/src/components/forms/client-form.tsx @@ -44,7 +44,7 @@ interface FormData { state: string; postalCode: string; country: string; - defaultHourlyRate: number; + defaultHourlyRate: number | null; } interface FormErrors { @@ -69,7 +69,7 @@ const initialFormData: FormData = { state: "", postalCode: "", country: "United States", - defaultHourlyRate: 100, + defaultHourlyRate: null, }; export function ClientForm({ clientId, mode }: ClientFormProps) { @@ -119,12 +119,12 @@ export function ClientForm({ clientId, mode }: ClientFormProps) { state: client.state ?? "", postalCode: client.postalCode ?? "", country: client.country ?? "United States", - defaultHourlyRate: client.defaultHourlyRate ?? 100, + defaultHourlyRate: client.defaultHourlyRate ?? null, }); } }, [client, mode]); - const handleInputChange = (field: string, value: string | number) => { + const handleInputChange = (field: string, value: string | number | null) => { setFormData((prev) => ({ ...prev, [field]: value })); setIsDirty(true); @@ -195,12 +195,17 @@ export function ClientForm({ clientId, mode }: ClientFormProps) { setIsSubmitting(true); try { + const apiData = { + ...formData, + defaultHourlyRate: formData.defaultHourlyRate ?? undefined, + }; + if (mode === "create") { - await createClient.mutateAsync(formData); + await createClient.mutateAsync(apiData); } else { await updateClient.mutateAsync({ id: clientId!, - ...formData, + ...apiData, }); } } finally { @@ -290,7 +295,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
-
+
@@ -371,7 +376,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
-
+
-
+
@@ -436,18 +441,26 @@ export function ClientForm({ clientId, mode }: ClientFormProps) { htmlFor="defaultHourlyRate" className="text-sm font-medium" > - Default Hourly Rate + Default Hourly Rate (Optional) +

+ This rate will be used as the default when creating new + invoice items for this client. +

- handleInputChange("defaultHourlyRate", value) + handleInputChange( + "defaultHourlyRate", + value === 0 ? null : value, + ) } min={0} step={1} prefix="$" width="full" disabled={isSubmitting} + placeholder="0.00" /> {errors.defaultHourlyRate && (

@@ -464,7 +477,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) { -

+
diff --git a/src/components/forms/invoice-form.tsx b/src/components/forms/invoice-form.tsx index 1d2dd1c..fa6bd35 100644 --- a/src/components/forms/invoice-form.tsx +++ b/src/components/forms/invoice-form.tsx @@ -25,6 +25,7 @@ import { InvoiceLineItems } from "./invoice-line-items"; import { api } from "~/trpc/react"; import { toast } from "sonner"; import { FileText, DollarSign, Check, Save, Clock, Trash2 } from "lucide-react"; +import { cn } from "~/lib/utils"; import { Dialog, DialogContent, @@ -62,7 +63,7 @@ interface FormData { status: "draft" | "sent" | "paid"; notes: string; taxRate: number; - defaultHourlyRate: number; + defaultHourlyRate: number | null; items: InvoiceItem[]; } @@ -100,15 +101,15 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { status: "draft", notes: "", taxRate: 0, - defaultHourlyRate: 25, + defaultHourlyRate: null, items: [ { id: crypto.randomUUID(), date: new Date(), description: "", hours: 1, - rate: 25, - amount: 25, + rate: 0, + amount: 0, }, ], }); @@ -160,7 +161,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { status: existingInvoice.status as "draft" | "sent" | "paid", notes: existingInvoice.notes ?? "", taxRate: existingInvoice.taxRate, - defaultHourlyRate: 25, + defaultHourlyRate: null, items: existingInvoice.items?.map((item) => ({ id: crypto.randomUUID(), @@ -194,18 +195,18 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { initialized, ]); - // Update default hourly rate when client changes (only during initialization) + // Update default hourly rate when client changes useEffect(() => { - if (!initialized || !formData.clientId || !clients) return; + if (!formData.clientId || !clients) return; const selectedClient = clients.find((c) => c.id === formData.clientId); - if (selectedClient?.defaultHourlyRate) { + if (selectedClient?.defaultHourlyRate != null) { setFormData((prev) => ({ ...prev, defaultHourlyRate: selectedClient.defaultHourlyRate, })); } - }, [formData.clientId, clients, initialized]); + }, [formData.clientId, clients]); // Calculate totals const totals = React.useMemo(() => { @@ -229,8 +230,8 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) { date: new Date(), description: "", hours: 1, - rate: prev.defaultHourlyRate, - amount: prev.defaultHourlyRate, + rate: prev.defaultHourlyRate ?? 0, + amount: prev.defaultHourlyRate ?? 0, }, ], })); @@ -623,18 +624,62 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
- - updateField("defaultHourlyRate", value) - } - min={0} - step={1} - prefix="$" - width="full" - /> +

c.id === formData.clientId) + ?.defaultHourlyRate + ? "text-green-600" + : "text-muted-foreground", + )} + > + {formData.clientId && + clients?.find((c) => c.id === formData.clientId) + ?.defaultHourlyRate + ? `✓ Inherited from ${clients.find((c) => c.id === formData.clientId)?.name}: $${clients.find((c) => c.id === formData.clientId)?.defaultHourlyRate}/hour` + : formData.clientId + ? "Client has no default rate set - enter rate manually" + : "Select a client first, or enter rate manually"} +

+
+ + updateField("defaultHourlyRate", value) + } + min={0} + step={1} + prefix="$" + width="full" + className={cn( + formData.clientId && + clients?.find( + (c) => c.id === formData.clientId, + )?.defaultHourlyRate + ? "border-green-200 bg-green-50/50" + : "", + )} + placeholder={ + formData.clientId && + clients?.find((c) => c.id === formData.clientId) + ?.defaultHourlyRate + ? "Inherited from client" + : "Enter hourly rate" + } + /> + {formData.clientId && + clients?.find((c) => c.id === formData.clientId) + ?.defaultHourlyRate && ( +
+ + ✓ + +
+ )} +
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 9d3267c..ffccc2b 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {