From f478e98755e55dbb9d784f1335d17c59f6d3bc2b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:50:31 +0000 Subject: [PATCH 1/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/integrations/supabase/types.ts | 48 ++++++++++++++++++ ...7_614d39b8-c491-4ed9-92ba-5d1eb13c549f.sql | 50 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 supabase/migrations/20260422175027_614d39b8-c491-4ed9-92ba-5d1eb13c549f.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 15aaa8e..0d23437 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -486,6 +486,54 @@ export type Database = { }, ] } + client_fee_overrides: { + Row: { + amount: number + client_id: string + created_at: string + created_by: string | null + enabled: boolean + fee_item_id: string + id: string + updated_at: string + } + Insert: { + amount?: number + client_id: string + created_at?: string + created_by?: string | null + enabled?: boolean + fee_item_id: string + id?: string + updated_at?: string + } + Update: { + amount?: number + client_id?: string + created_at?: string + created_by?: string | null + enabled?: boolean + fee_item_id?: string + id?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "client_fee_overrides_client_id_fkey" + columns: ["client_id"] + isOneToOne: false + referencedRelation: "clients" + referencedColumns: ["id"] + }, + { + foreignKeyName: "client_fee_overrides_fee_item_id_fkey" + columns: ["fee_item_id"] + isOneToOne: false + referencedRelation: "fee_schedule_items" + referencedColumns: ["id"] + }, + ] + } client_field_values: { Row: { client_id: string diff --git a/supabase/migrations/20260422175027_614d39b8-c491-4ed9-92ba-5d1eb13c549f.sql b/supabase/migrations/20260422175027_614d39b8-c491-4ed9-92ba-5d1eb13c549f.sql new file mode 100644 index 0000000..68eec19 --- /dev/null +++ b/supabase/migrations/20260422175027_614d39b8-c491-4ed9-92ba-5d1eb13c549f.sql @@ -0,0 +1,50 @@ +-- Per-client overrides for fee schedule expense items +CREATE TABLE public.client_fee_overrides ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + client_id UUID NOT NULL REFERENCES public.clients(id) ON DELETE CASCADE, + fee_item_id UUID NOT NULL REFERENCES public.fee_schedule_items(id) ON DELETE CASCADE, + amount NUMERIC NOT NULL DEFAULT 0, + enabled BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + created_by UUID, + UNIQUE (client_id, fee_item_id) +); + +CREATE INDEX idx_client_fee_overrides_client ON public.client_fee_overrides(client_id); + +ALTER TABLE public.client_fee_overrides ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "cfo_select_auth" ON public.client_fee_overrides + FOR SELECT TO authenticated USING (true); + +CREATE POLICY "cfo_insert_auth" ON public.client_fee_overrides + FOR INSERT TO authenticated + WITH CHECK ( + is_admin(auth.uid()) OR EXISTS ( + SELECT 1 FROM public.clients c + WHERE c.id = client_fee_overrides.client_id AND c.created_by = auth.uid() + ) + ); + +CREATE POLICY "cfo_update_auth" ON public.client_fee_overrides + FOR UPDATE TO authenticated + USING ( + is_admin(auth.uid()) OR EXISTS ( + SELECT 1 FROM public.clients c + WHERE c.id = client_fee_overrides.client_id AND c.created_by = auth.uid() + ) + ); + +CREATE POLICY "cfo_delete_auth" ON public.client_fee_overrides + FOR DELETE TO authenticated + USING ( + is_admin(auth.uid()) OR EXISTS ( + SELECT 1 FROM public.clients c + WHERE c.id = client_fee_overrides.client_id AND c.created_by = auth.uid() + ) + ); + +CREATE TRIGGER trg_cfo_updated_at + BEFORE UPDATE ON public.client_fee_overrides + FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); \ No newline at end of file From 914e6b8581aa445fc5f124ab6ce098fc049a993e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:52:00 +0000 Subject: [PATCH 2/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/clients..fees.tsx | 784 +++++++++++++++++++++++++++++++++++ 1 file changed, 784 insertions(+) create mode 100644 src/routes/clients..fees.tsx diff --git a/src/routes/clients..fees.tsx b/src/routes/clients..fees.tsx new file mode 100644 index 0000000..51e3e2b --- /dev/null +++ b/src/routes/clients..fees.tsx @@ -0,0 +1,784 @@ +import { createFileRoute, Link } from "@tanstack/react-router"; +import { useEffect, useMemo, useState } from "react"; +import { ProtectedLayout } from "@/components/protected-layout"; +import { PageContainer, PageHeader } from "@/components/app-shell"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + ArrowLeft, + Briefcase, + Clock, + DollarSign, + Loader2, + Receipt, + Save, + Wand2, +} from "lucide-react"; +import { toast } from "sonner"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/lib/auth"; +import { formatCurrency } from "@/lib/format"; + +export const Route = createFileRoute("/clients/fees")({ + component: () => ( + + + + ), +}); + +type BillingMethod = "hourly" | "flat" | "contingency" | "pro_bono"; +const BILLING_OPTIONS: { value: BillingMethod; label: string }[] = [ + { value: "hourly", label: "Hourly" }, + { value: "flat", label: "Flat fee" }, + { value: "contingency", label: "Contingency" }, + { value: "pro_bono", label: "Pro bono" }, +]; + +interface CaseFeeRow { + id: string; + case_number: string; + title: string; + status: string; + billing_method: BillingMethod | null; + default_hourly_rate: number | null; + flat_fee_amount: number | null; + dirty?: boolean; +} + +interface FeeItem { + id: string; + name: string; + description: string | null; + category: "time" | "expense"; + pricing_type: "hourly" | "flat"; + amount: number; + billable: boolean; + active: boolean; +} + +interface OverrideRow { + fee_item_id: string; + enabled: boolean; + amount: number; + dirty?: boolean; + hasRecord?: boolean; +} + +function toNumOrNull(v: string): number | null { + if (v === "" || v == null) return null; + const n = Number(v); + return Number.isFinite(n) ? n : null; +} +function asInputStr(n: number | null | undefined): string { + if (n == null) return ""; + return String(n); +} + +function ClientFeesPage() { + const { clientId } = Route.useParams(); + const { user, isAdmin } = useAuth(); + + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [client, setClient] = useState(null); + + const [defaultsHourlyStr, setDefaultsHourlyStr] = useState(""); + const [defaultsFlatStr, setDefaultsFlatStr] = useState(""); + const [defaultsDirty, setDefaultsDirty] = useState(false); + + const [rows, setRows] = useState([]); + const [selected, setSelected] = useState>(new Set()); + + const [feeItems, setFeeItems] = useState([]); + const [overrides, setOverrides] = useState>({}); + + const canEdit = isAdmin || client?.created_by === user?.id; + + const load = async () => { + setLoading(true); + const [ + { data: c, error: ce }, + { data: cs, error: cse }, + { data: fi }, + { data: ov }, + ] = await Promise.all([ + supabase.from("clients").select("*").eq("id", clientId).maybeSingle(), + supabase + .from("cases") + .select( + "id, case_number, title, status, billing_method, default_hourly_rate, flat_fee_amount", + ) + .eq("client_id", clientId) + .order("title", { ascending: true }), + supabase + .from("fee_schedule_items") + .select("*") + .eq("active", true) + .order("category") + .order("sort_order") + .order("name"), + supabase + .from("client_fee_overrides") + .select("fee_item_id, enabled, amount") + .eq("client_id", clientId), + ]); + if (ce) toast.error("Failed to load client", { description: ce.message }); + if (cse) toast.error("Failed to load cases", { description: cse.message }); + + setClient(c); + setDefaultsHourlyStr(asInputStr(c?.default_hourly_rate ?? null)); + setDefaultsFlatStr(asInputStr(c?.default_flat_fee_amount ?? null)); + setDefaultsDirty(false); + + setRows( + ((cs ?? []) as any[]).map((x) => ({ + id: x.id, + case_number: x.case_number, + title: x.title, + status: x.status, + billing_method: (x.billing_method as BillingMethod | null) ?? null, + default_hourly_rate: x.default_hourly_rate ?? null, + flat_fee_amount: x.flat_fee_amount ?? null, + })), + ); + setSelected(new Set()); + + const items = (fi ?? []) as FeeItem[]; + setFeeItems(items); + + const ovMap: Record = {}; + ((ov ?? []) as any[]).forEach((o) => { + ovMap[o.fee_item_id] = { + fee_item_id: o.fee_item_id, + enabled: !!o.enabled, + amount: Number(o.amount) || 0, + hasRecord: true, + }; + }); + items.forEach((it) => { + if (!ovMap[it.id]) { + ovMap[it.id] = { + fee_item_id: it.id, + enabled: false, + amount: Number(it.amount) || 0, + hasRecord: false, + }; + } + }); + setOverrides(ovMap); + + setLoading(false); + }; + + useEffect(() => { + load(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [clientId]); + + const dirtyRows = useMemo(() => rows.filter((r) => r.dirty), [rows]); + const dirtyOverrides = useMemo( + () => Object.values(overrides).filter((o) => o.dirty), + [overrides], + ); + + const updateRow = (id: string, patch: Partial) => { + setRows((prev) => + prev.map((r) => (r.id === id ? { ...r, ...patch, dirty: true } : r)), + ); + }; + + const toggleAll = () => { + if (selected.size === rows.length) setSelected(new Set()); + else setSelected(new Set(rows.map((r) => r.id))); + }; + const toggleOne = (id: string) => { + setSelected((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + const applyDefaultsToSelected = (field: "hourly" | "flat") => { + if (selected.size === 0) { + toast.error("Pick at least one case to apply to"); + return; + } + const value = + field === "hourly" + ? toNumOrNull(defaultsHourlyStr) + : toNumOrNull(defaultsFlatStr); + if (value == null) { + toast.error("Enter a default value first"); + return; + } + setRows((prev) => + prev.map((r) => + selected.has(r.id) + ? { + ...r, + ...(field === "hourly" + ? { default_hourly_rate: value } + : { + flat_fee_amount: value, + billing_method: r.billing_method ?? "flat", + }), + dirty: true, + } + : r, + ), + ); + toast.success( + `Applied ${formatCurrency(value)} to ${selected.size} case${selected.size === 1 ? "" : "s"} — remember to save`, + ); + }; + + const saveDefaults = async () => { + setSaving(true); + const payload = { + default_hourly_rate: toNumOrNull(defaultsHourlyStr), + default_flat_fee_amount: toNumOrNull(defaultsFlatStr), + }; + const { error } = await supabase + .from("clients") + .update(payload) + .eq("id", clientId); + setSaving(false); + if (error) { + toast.error("Could not save defaults", { description: error.message }); + return; + } + setDefaultsDirty(false); + toast.success("Client default fees saved"); + }; + + const saveCases = async () => { + if (dirtyRows.length === 0) { + toast.error("Nothing to save"); + return; + } + setSaving(true); + let failed = 0; + for (const r of dirtyRows) { + const { error } = await supabase + .from("cases") + .update({ + billing_method: r.billing_method, + default_hourly_rate: r.default_hourly_rate, + flat_fee_amount: r.flat_fee_amount, + }) + .eq("id", r.id); + if (error) failed++; + } + setSaving(false); + if (failed > 0) { + toast.error(`${failed} case${failed === 1 ? "" : "s"} failed to save`); + } else { + toast.success(`Saved ${dirtyRows.length} case${dirtyRows.length === 1 ? "" : "s"}`); + } + load(); + }; + + const updateOverride = (feeItemId: string, patch: Partial) => { + setOverrides((prev) => ({ + ...prev, + [feeItemId]: { ...prev[feeItemId], ...patch, dirty: true }, + })); + }; + + const saveOverrides = async () => { + if (dirtyOverrides.length === 0) { + toast.error("Nothing to save"); + return; + } + setSaving(true); + let failed = 0; + for (const o of dirtyOverrides) { + if (!o.enabled && o.hasRecord) { + const { error } = await supabase + .from("client_fee_overrides") + .delete() + .eq("client_id", clientId) + .eq("fee_item_id", o.fee_item_id); + if (error) failed++; + } else if (o.enabled) { + const { error } = await supabase + .from("client_fee_overrides") + .upsert( + { + client_id: clientId, + fee_item_id: o.fee_item_id, + enabled: true, + amount: o.amount, + created_by: user?.id, + }, + { onConflict: "client_id,fee_item_id" }, + ); + if (error) failed++; + } + } + setSaving(false); + if (failed > 0) { + toast.error(`${failed} override${failed === 1 ? "" : "s"} failed to save`); + } else { + toast.success( + `Saved ${dirtyOverrides.length} override${dirtyOverrides.length === 1 ? "" : "s"}`, + ); + } + load(); + }; + + const enabledCount = useMemo( + () => Object.values(overrides).filter((o) => o.enabled).length, + [overrides], + ); + + if (loading) { + return ( + +

Loading…

+
+ ); + } + if (!client) { + return ( + +

Client not found.

+
+ ); + } + + const timeItems = feeItems.filter((i) => i.category === "time"); + const expenseItems = feeItems.filter((i) => i.category === "expense"); + + return ( + + + + + + + + + Defaults + + + Cases ({rows.length}) + + + Expense Overrides ({enabledCount}) + + + Time Overrides + + + + + + +
+
+

Client default fees

+

+ Reusable defaults you can apply to any case below. +

+
+ {defaultsDirty && ( + + )} +
+
+
+ + { + setDefaultsHourlyStr(e.target.value); + setDefaultsDirty(true); + }} + /> +
+
+ + { + setDefaultsFlatStr(e.target.value); + setDefaultsDirty(true); + }} + /> +
+
+

+ Switch to the Cases tab to bulk-apply these defaults to selected cases. +

+
+
+
+ + +
+
+

Case-level fees

+

+ Edit billing method and amounts per case. Tick rows to apply client defaults in bulk. +

+
+
+ + + +
+
+ + + + {rows.length === 0 ? ( +
+ No cases for this client yet. +
+ ) : ( + + + + + 0 + ? "indeterminate" + : false + } + onCheckedChange={toggleAll} + disabled={!canEdit} + aria-label="Select all" + /> + + Case + Billing method + Hourly rate + Flat fee + + + + {rows.map((r) => ( + + + toggleOne(r.id)} + disabled={!canEdit} + aria-label={`Select ${r.title}`} + /> + + +
+ {r.title} + {r.dirty && ( + + unsaved + + )} +
+
+ {r.case_number} · {r.status.replace("_", " ")} +
+
+ + + + + + updateRow(r.id, { + default_hourly_rate: toNumOrNull(e.target.value), + }) + } + /> + + + + updateRow(r.id, { + flat_fee_amount: toNumOrNull(e.target.value), + }) + } + /> + +
+ ))} +
+
+ )} +
+
+
+ + + + + + + + +
+
+ ); +} + +function FeeOverrideSection({ + title, + description, + items, + overrides, + updateOverride, + saveOverrides, + saving, + canEdit, + dirtyCount, +}: { + title: string; + description: string; + items: FeeItem[]; + overrides: Record; + updateOverride: (id: string, patch: Partial) => void; + saveOverrides: () => void; + saving: boolean; + canEdit: boolean; + dirtyCount: number; +}) { + return ( +
+
+
+

{title}

+

{description}

+
+ +
+ + + + {items.length === 0 ? ( +
+ No active fee items in this category. Add some in Settings → Fees. +
+ ) : ( + + + + + Fee item + Firm default + Client amount + + + + {items.map((it) => { + const ov = overrides[it.id]; + if (!ov) return null; + return ( + + + + updateOverride(it.id, { enabled: !!v }) + } + aria-label={`Override ${it.name}`} + /> + + +
+ {it.name} + {ov.dirty && ( + + unsaved + + )} +
+ {it.description && ( +
+ {it.description} +
+ )} +
+ + {formatCurrency(Number(it.amount))} + {it.category === "time" && ( + + {it.pricing_type === "flat" ? "flat" : "/hr"} + + )} + + + + updateOverride(it.id, { + amount: Number(e.target.value) || 0, + }) + } + /> + +
+ ); + })} +
+
+ )} +
+
+
+ ); +} From 5d44dd66b568132cc93b20673d4583f212735e22 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:52:04 +0000 Subject: [PATCH 3/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routeTree.gen.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 94db69b..9699f6b 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -46,6 +46,7 @@ import { Route as InvoicesInvoiceIdRouteImport } from './routes/invoices.$invoic import { Route as HooksPollImapRouteImport } from './routes/hooks/poll-imap' import { Route as ContactsContactIdRouteImport } from './routes/contacts.$contactId' import { Route as CollectionsCollectionIdRouteImport } from './routes/collections.$collectionId' +import { Route as ClientsFeesRouteImport } from './routes/clients..fees' import { Route as ClientsClientIdRouteImport } from './routes/clients.$clientId' import { Route as CasesNewRouteImport } from './routes/cases.new' import { Route as CasesCaseIdRouteImport } from './routes/cases.$caseId' @@ -242,6 +243,11 @@ const CollectionsCollectionIdRoute = CollectionsCollectionIdRouteImport.update({ path: '/collections/$collectionId', getParentRoute: () => rootRouteImport, } as any) +const ClientsFeesRoute = ClientsFeesRouteImport.update({ + id: '/clients/fees', + path: '/clients/fees', + getParentRoute: () => rootRouteImport, +} as any) const ClientsClientIdRoute = ClientsClientIdRouteImport.update({ id: '/clients/$clientId', path: '/clients/$clientId', @@ -304,6 +310,7 @@ export interface FileRoutesByFullPath { '/cases/$caseId': typeof CasesCaseIdRoute '/cases/new': typeof CasesNewRoute '/clients/$clientId': typeof ClientsClientIdRoute + '/clients/fees': typeof ClientsFeesRoute '/collections/$collectionId': typeof CollectionsCollectionIdRoute '/contacts/$contactId': typeof ContactsContactIdRoute '/hooks/poll-imap': typeof HooksPollImapRoute @@ -352,6 +359,7 @@ export interface FileRoutesByTo { '/cases/$caseId': typeof CasesCaseIdRoute '/cases/new': typeof CasesNewRoute '/clients/$clientId': typeof ClientsClientIdRoute + '/clients/fees': typeof ClientsFeesRoute '/collections/$collectionId': typeof CollectionsCollectionIdRoute '/contacts/$contactId': typeof ContactsContactIdRoute '/hooks/poll-imap': typeof HooksPollImapRoute @@ -402,6 +410,7 @@ export interface FileRoutesById { '/cases/$caseId': typeof CasesCaseIdRoute '/cases/new': typeof CasesNewRoute '/clients/$clientId': typeof ClientsClientIdRoute + '/clients/fees': typeof ClientsFeesRoute '/collections/$collectionId': typeof CollectionsCollectionIdRoute '/contacts/$contactId': typeof ContactsContactIdRoute '/hooks/poll-imap': typeof HooksPollImapRoute @@ -453,6 +462,7 @@ export interface FileRouteTypes { | '/cases/$caseId' | '/cases/new' | '/clients/$clientId' + | '/clients/fees' | '/collections/$collectionId' | '/contacts/$contactId' | '/hooks/poll-imap' @@ -501,6 +511,7 @@ export interface FileRouteTypes { | '/cases/$caseId' | '/cases/new' | '/clients/$clientId' + | '/clients/fees' | '/collections/$collectionId' | '/contacts/$contactId' | '/hooks/poll-imap' @@ -550,6 +561,7 @@ export interface FileRouteTypes { | '/cases/$caseId' | '/cases/new' | '/clients/$clientId' + | '/clients/fees' | '/collections/$collectionId' | '/contacts/$contactId' | '/hooks/poll-imap' @@ -600,6 +612,7 @@ export interface RootRouteChildren { CasesCaseIdRoute: typeof CasesCaseIdRoute CasesNewRoute: typeof CasesNewRoute ClientsClientIdRoute: typeof ClientsClientIdRoute + ClientsFeesRoute: typeof ClientsFeesRoute CollectionsCollectionIdRoute: typeof CollectionsCollectionIdRoute ContactsContactIdRoute: typeof ContactsContactIdRoute HooksPollImapRoute: typeof HooksPollImapRoute @@ -889,6 +902,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof CollectionsCollectionIdRouteImport parentRoute: typeof rootRouteImport } + '/clients/fees': { + id: '/clients/fees' + path: '/clients/fees' + fullPath: '/clients/fees' + preLoaderRoute: typeof ClientsFeesRouteImport + parentRoute: typeof rootRouteImport + } '/clients/$clientId': { id: '/clients/$clientId' path: '/clients/$clientId' @@ -1016,6 +1036,7 @@ const rootRouteChildren: RootRouteChildren = { CasesCaseIdRoute: CasesCaseIdRoute, CasesNewRoute: CasesNewRoute, ClientsClientIdRoute: ClientsClientIdRoute, + ClientsFeesRoute: ClientsFeesRoute, CollectionsCollectionIdRoute: CollectionsCollectionIdRoute, ContactsContactIdRoute: ContactsContactIdRoute, HooksPollImapRoute: HooksPollImapRoute, @@ -1046,3 +1067,12 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} From 411e2528190928b77739815697a5c8f0dfd66438 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:52:09 +0000 Subject: [PATCH 4/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/{clients..fees.tsx => clients.$clientId.fees.tsx} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/routes/{clients..fees.tsx => clients.$clientId.fees.tsx} (99%) diff --git a/src/routes/clients..fees.tsx b/src/routes/clients.$clientId.fees.tsx similarity index 99% rename from src/routes/clients..fees.tsx rename to src/routes/clients.$clientId.fees.tsx index 51e3e2b..1e53021 100644 --- a/src/routes/clients..fees.tsx +++ b/src/routes/clients.$clientId.fees.tsx @@ -38,7 +38,7 @@ import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; import { formatCurrency } from "@/lib/format"; -export const Route = createFileRoute("/clients/fees")({ +export const Route = createFileRoute("/clients/$clientId/fees")({ component: () => ( From 5b094ff5da0a02abf9ccb05487dea92779cac712 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:52:23 +0000 Subject: [PATCH 5/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routeTree.gen.ts | 71 ++++++++++++++++---------------- src/routes/clients.$clientId.tsx | 4 +- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 9699f6b..95b590e 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -46,7 +46,6 @@ import { Route as InvoicesInvoiceIdRouteImport } from './routes/invoices.$invoic import { Route as HooksPollImapRouteImport } from './routes/hooks/poll-imap' import { Route as ContactsContactIdRouteImport } from './routes/contacts.$contactId' import { Route as CollectionsCollectionIdRouteImport } from './routes/collections.$collectionId' -import { Route as ClientsFeesRouteImport } from './routes/clients..fees' import { Route as ClientsClientIdRouteImport } from './routes/clients.$clientId' import { Route as CasesNewRouteImport } from './routes/cases.new' import { Route as CasesCaseIdRouteImport } from './routes/cases.$caseId' @@ -57,6 +56,7 @@ import { Route as InvoicesNewClientIdRouteImport } from './routes/invoices.new.$ import { Route as DocumentsTemplatesNewRouteImport } from './routes/documents.templates.new' import { Route as DocumentsTemplatesTemplateIdRouteImport } from './routes/documents.templates.$templateId' import { Route as DocumentsPleadingNewRouteImport } from './routes/documents.pleading.new' +import { Route as ClientsClientIdFeesRouteImport } from './routes/clients.$clientId.fees' const SetupRoute = SetupRouteImport.update({ id: '/setup', @@ -243,11 +243,6 @@ const CollectionsCollectionIdRoute = CollectionsCollectionIdRouteImport.update({ path: '/collections/$collectionId', getParentRoute: () => rootRouteImport, } as any) -const ClientsFeesRoute = ClientsFeesRouteImport.update({ - id: '/clients/fees', - path: '/clients/fees', - getParentRoute: () => rootRouteImport, -} as any) const ClientsClientIdRoute = ClientsClientIdRouteImport.update({ id: '/clients/$clientId', path: '/clients/$clientId', @@ -299,6 +294,11 @@ const DocumentsPleadingNewRoute = DocumentsPleadingNewRouteImport.update({ path: '/documents/pleading/new', getParentRoute: () => rootRouteImport, } as any) +const ClientsClientIdFeesRoute = ClientsClientIdFeesRouteImport.update({ + id: '/fees', + path: '/fees', + getParentRoute: () => ClientsClientIdRoute, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -309,8 +309,7 @@ export interface FileRoutesByFullPath { '/api/stripe-webhook': typeof ApiStripeWebhookRoute '/cases/$caseId': typeof CasesCaseIdRoute '/cases/new': typeof CasesNewRoute - '/clients/$clientId': typeof ClientsClientIdRoute - '/clients/fees': typeof ClientsFeesRoute + '/clients/$clientId': typeof ClientsClientIdRouteWithChildren '/collections/$collectionId': typeof CollectionsCollectionIdRoute '/contacts/$contactId': typeof ContactsContactIdRoute '/hooks/poll-imap': typeof HooksPollImapRoute @@ -344,6 +343,7 @@ export interface FileRoutesByFullPath { '/settings/': typeof SettingsIndexRoute '/status/': typeof StatusIndexRoute '/tasks/': typeof TasksIndexRoute + '/clients/$clientId/fees': typeof ClientsClientIdFeesRoute '/documents/pleading/new': typeof DocumentsPleadingNewRoute '/documents/templates/$templateId': typeof DocumentsTemplatesTemplateIdRoute '/documents/templates/new': typeof DocumentsTemplatesNewRoute @@ -358,8 +358,7 @@ export interface FileRoutesByTo { '/api/stripe-webhook': typeof ApiStripeWebhookRoute '/cases/$caseId': typeof CasesCaseIdRoute '/cases/new': typeof CasesNewRoute - '/clients/$clientId': typeof ClientsClientIdRoute - '/clients/fees': typeof ClientsFeesRoute + '/clients/$clientId': typeof ClientsClientIdRouteWithChildren '/collections/$collectionId': typeof CollectionsCollectionIdRoute '/contacts/$contactId': typeof ContactsContactIdRoute '/hooks/poll-imap': typeof HooksPollImapRoute @@ -393,6 +392,7 @@ export interface FileRoutesByTo { '/settings': typeof SettingsIndexRoute '/status': typeof StatusIndexRoute '/tasks': typeof TasksIndexRoute + '/clients/$clientId/fees': typeof ClientsClientIdFeesRoute '/documents/pleading/new': typeof DocumentsPleadingNewRoute '/documents/templates/$templateId': typeof DocumentsTemplatesTemplateIdRoute '/documents/templates/new': typeof DocumentsTemplatesNewRoute @@ -409,8 +409,7 @@ export interface FileRoutesById { '/api/stripe-webhook': typeof ApiStripeWebhookRoute '/cases/$caseId': typeof CasesCaseIdRoute '/cases/new': typeof CasesNewRoute - '/clients/$clientId': typeof ClientsClientIdRoute - '/clients/fees': typeof ClientsFeesRoute + '/clients/$clientId': typeof ClientsClientIdRouteWithChildren '/collections/$collectionId': typeof CollectionsCollectionIdRoute '/contacts/$contactId': typeof ContactsContactIdRoute '/hooks/poll-imap': typeof HooksPollImapRoute @@ -444,6 +443,7 @@ export interface FileRoutesById { '/settings/': typeof SettingsIndexRoute '/status/': typeof StatusIndexRoute '/tasks/': typeof TasksIndexRoute + '/clients/$clientId/fees': typeof ClientsClientIdFeesRoute '/documents/pleading/new': typeof DocumentsPleadingNewRoute '/documents/templates/$templateId': typeof DocumentsTemplatesTemplateIdRoute '/documents/templates/new': typeof DocumentsTemplatesNewRoute @@ -462,7 +462,6 @@ export interface FileRouteTypes { | '/cases/$caseId' | '/cases/new' | '/clients/$clientId' - | '/clients/fees' | '/collections/$collectionId' | '/contacts/$contactId' | '/hooks/poll-imap' @@ -496,6 +495,7 @@ export interface FileRouteTypes { | '/settings/' | '/status/' | '/tasks/' + | '/clients/$clientId/fees' | '/documents/pleading/new' | '/documents/templates/$templateId' | '/documents/templates/new' @@ -511,7 +511,6 @@ export interface FileRouteTypes { | '/cases/$caseId' | '/cases/new' | '/clients/$clientId' - | '/clients/fees' | '/collections/$collectionId' | '/contacts/$contactId' | '/hooks/poll-imap' @@ -545,6 +544,7 @@ export interface FileRouteTypes { | '/settings' | '/status' | '/tasks' + | '/clients/$clientId/fees' | '/documents/pleading/new' | '/documents/templates/$templateId' | '/documents/templates/new' @@ -561,7 +561,6 @@ export interface FileRouteTypes { | '/cases/$caseId' | '/cases/new' | '/clients/$clientId' - | '/clients/fees' | '/collections/$collectionId' | '/contacts/$contactId' | '/hooks/poll-imap' @@ -595,6 +594,7 @@ export interface FileRouteTypes { | '/settings/' | '/status/' | '/tasks/' + | '/clients/$clientId/fees' | '/documents/pleading/new' | '/documents/templates/$templateId' | '/documents/templates/new' @@ -611,8 +611,7 @@ export interface RootRouteChildren { ApiStripeWebhookRoute: typeof ApiStripeWebhookRoute CasesCaseIdRoute: typeof CasesCaseIdRoute CasesNewRoute: typeof CasesNewRoute - ClientsClientIdRoute: typeof ClientsClientIdRoute - ClientsFeesRoute: typeof ClientsFeesRoute + ClientsClientIdRoute: typeof ClientsClientIdRouteWithChildren CollectionsCollectionIdRoute: typeof CollectionsCollectionIdRoute ContactsContactIdRoute: typeof ContactsContactIdRoute HooksPollImapRoute: typeof HooksPollImapRoute @@ -902,13 +901,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof CollectionsCollectionIdRouteImport parentRoute: typeof rootRouteImport } - '/clients/fees': { - id: '/clients/fees' - path: '/clients/fees' - fullPath: '/clients/fees' - preLoaderRoute: typeof ClientsFeesRouteImport - parentRoute: typeof rootRouteImport - } '/clients/$clientId': { id: '/clients/$clientId' path: '/clients/$clientId' @@ -979,6 +971,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocumentsPleadingNewRouteImport parentRoute: typeof rootRouteImport } + '/clients/$clientId/fees': { + id: '/clients/$clientId/fees' + path: '/fees' + fullPath: '/clients/$clientId/fees' + preLoaderRoute: typeof ClientsClientIdFeesRouteImport + parentRoute: typeof ClientsClientIdRoute + } } } @@ -1014,6 +1013,18 @@ const SettingsRouteWithChildren = SettingsRoute._addFileChildren( SettingsRouteChildren, ) +interface ClientsClientIdRouteChildren { + ClientsClientIdFeesRoute: typeof ClientsClientIdFeesRoute +} + +const ClientsClientIdRouteChildren: ClientsClientIdRouteChildren = { + ClientsClientIdFeesRoute: ClientsClientIdFeesRoute, +} + +const ClientsClientIdRouteWithChildren = ClientsClientIdRoute._addFileChildren( + ClientsClientIdRouteChildren, +) + interface InvoicesNewRouteChildren { InvoicesNewClientIdRoute: typeof InvoicesNewClientIdRoute } @@ -1035,8 +1046,7 @@ const rootRouteChildren: RootRouteChildren = { ApiStripeWebhookRoute: ApiStripeWebhookRoute, CasesCaseIdRoute: CasesCaseIdRoute, CasesNewRoute: CasesNewRoute, - ClientsClientIdRoute: ClientsClientIdRoute, - ClientsFeesRoute: ClientsFeesRoute, + ClientsClientIdRoute: ClientsClientIdRouteWithChildren, CollectionsCollectionIdRoute: CollectionsCollectionIdRoute, ContactsContactIdRoute: ContactsContactIdRoute, HooksPollImapRoute: HooksPollImapRoute, @@ -1067,12 +1077,3 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() - -import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' -declare module '@tanstack/react-start' { - interface Register { - ssr: true - router: Awaited> - } -} diff --git a/src/routes/clients.$clientId.tsx b/src/routes/clients.$clientId.tsx index 298f92d..227cd1a 100644 --- a/src/routes/clients.$clientId.tsx +++ b/src/routes/clients.$clientId.tsx @@ -12,7 +12,6 @@ import { useAuth } from "@/lib/auth"; import { ArrowLeft, Edit, Plus, Building2, User, MapPin, Mail, Phone, Users, Activity, Briefcase, FileDown, Contact, Receipt, Archive, ArchiveRestore, ListPlus, Save, DollarSign } from "lucide-react"; import { ContactsLinkTab } from "@/components/contacts/contacts-link-tab"; import { ClientCustomFieldsTab } from "@/components/clients/client-custom-fields-tab"; -import { AdjustFeesTab } from "@/components/clients/adjust-fees-tab"; import { formatCurrency, formatDate, formatDateTime, statusBadgeClass } from "@/lib/format"; import { toast } from "sonner"; import { downloadStatusReport, saveStatusReportToDb } from "@/lib/status-pdf"; @@ -152,6 +151,9 @@ function ClientDetail() { + From f7f1a9e4eaa9aa748d8805434b46867105f2cded Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:52:33 +0000 Subject: [PATCH 6/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/clients.$clientId.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/clients.$clientId.tsx b/src/routes/clients.$clientId.tsx index 227cd1a..c910ff7 100644 --- a/src/routes/clients.$clientId.tsx +++ b/src/routes/clients.$clientId.tsx @@ -238,7 +238,6 @@ function ClientDetail() { Cases Status Contacts - Adjust Fees Fields From 9029cb560c4edb0317755b710317bd197863fa6a Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:52:44 +0000 Subject: [PATCH 7/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/clients.$clientId.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/routes/clients.$clientId.tsx b/src/routes/clients.$clientId.tsx index c910ff7..8ac375b 100644 --- a/src/routes/clients.$clientId.tsx +++ b/src/routes/clients.$clientId.tsx @@ -345,10 +345,6 @@ function ClientDetail() { - - - -