From 255d39a2d7152064d45ddd1bd0d12f223076e592 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 00:51:11 +0000 Subject: [PATCH 01/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/integrations/supabase/types.ts | 105 ++++++++++++++++++ ...8_8d3570de-850d-4daf-a166-562c441db8d9.sql | 100 +++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 supabase/migrations/20260417005108_8d3570de-850d-4daf-a166-562c441db8d9.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index d57bd90..2d612e2 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -405,6 +405,111 @@ export type Database = { }, ] } + fee_schedule_items: { + Row: { + active: boolean + amount: number + billable: boolean + category: string + created_at: string + created_by: string | null + description: string | null + id: string + name: string + sort_order: number + updated_at: string + } + Insert: { + active?: boolean + amount?: number + billable?: boolean + category: string + created_at?: string + created_by?: string | null + description?: string | null + id?: string + name: string + sort_order?: number + updated_at?: string + } + Update: { + active?: boolean + amount?: number + billable?: boolean + category?: string + created_at?: string + created_by?: string | null + description?: string | null + id?: string + name?: string + sort_order?: number + updated_at?: string + } + Relationships: [] + } + firm_settings: { + Row: { + address_line1: string | null + address_line2: string | null + city: string | null + company_name: string | null + contact_email: string | null + contact_phone: string | null + country: string | null + created_at: string + default_tax_rate: number | null + footer_note: string | null + id: string + invoice_prefix: string | null + logo_storage_path: string | null + postal_code: string | null + state: string | null + updated_at: string + updated_by: string | null + website: string | null + } + Insert: { + address_line1?: string | null + address_line2?: string | null + city?: string | null + company_name?: string | null + contact_email?: string | null + contact_phone?: string | null + country?: string | null + created_at?: string + default_tax_rate?: number | null + footer_note?: string | null + id?: string + invoice_prefix?: string | null + logo_storage_path?: string | null + postal_code?: string | null + state?: string | null + updated_at?: string + updated_by?: string | null + website?: string | null + } + Update: { + address_line1?: string | null + address_line2?: string | null + city?: string | null + company_name?: string | null + contact_email?: string | null + contact_phone?: string | null + country?: string | null + created_at?: string + default_tax_rate?: number | null + footer_note?: string | null + id?: string + invoice_prefix?: string | null + logo_storage_path?: string | null + postal_code?: string | null + state?: string | null + updated_at?: string + updated_by?: string | null + website?: string | null + } + Relationships: [] + } homeowners: { Row: { address: string | null diff --git a/supabase/migrations/20260417005108_8d3570de-850d-4daf-a166-562c441db8d9.sql b/supabase/migrations/20260417005108_8d3570de-850d-4daf-a166-562c441db8d9.sql new file mode 100644 index 0000000..617bd4e --- /dev/null +++ b/supabase/migrations/20260417005108_8d3570de-850d-4daf-a166-562c441db8d9.sql @@ -0,0 +1,100 @@ +-- 1. Firm settings (single-row table keyed by a fixed id) +CREATE TABLE IF NOT EXISTS public.firm_settings ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + company_name text, + contact_email text, + contact_phone text, + website text, + address_line1 text, + address_line2 text, + city text, + state text, + postal_code text, + country text, + logo_storage_path text, + invoice_prefix text, + default_tax_rate numeric, + footer_note text, + updated_by uuid, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +ALTER TABLE public.firm_settings ENABLE ROW LEVEL SECURITY; + +CREATE POLICY firm_settings_select_auth ON public.firm_settings + FOR SELECT TO authenticated USING (true); + +CREATE POLICY firm_settings_insert_admin ON public.firm_settings + FOR INSERT TO authenticated + WITH CHECK (public.is_admin(auth.uid())); + +CREATE POLICY firm_settings_update_admin ON public.firm_settings + FOR UPDATE TO authenticated + USING (public.is_admin(auth.uid())); + +CREATE POLICY firm_settings_delete_admin ON public.firm_settings + FOR DELETE TO authenticated + USING (public.is_admin(auth.uid())); + +CREATE TRIGGER firm_settings_set_updated_at + BEFORE UPDATE ON public.firm_settings + FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); + +-- 2. Fee schedule items +CREATE TABLE IF NOT EXISTS public.fee_schedule_items ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL, + description text, + category text NOT NULL CHECK (category IN ('time','expense')), + amount numeric NOT NULL DEFAULT 0, + billable boolean NOT NULL DEFAULT true, + active boolean NOT NULL DEFAULT true, + sort_order int NOT NULL DEFAULT 0, + created_by uuid, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_fee_schedule_items_category ON public.fee_schedule_items(category); + +ALTER TABLE public.fee_schedule_items ENABLE ROW LEVEL SECURITY; + +CREATE POLICY fee_schedule_items_select_auth ON public.fee_schedule_items + FOR SELECT TO authenticated USING (true); + +CREATE POLICY fee_schedule_items_insert_admin ON public.fee_schedule_items + FOR INSERT TO authenticated + WITH CHECK (public.is_admin(auth.uid())); + +CREATE POLICY fee_schedule_items_update_admin ON public.fee_schedule_items + FOR UPDATE TO authenticated + USING (public.is_admin(auth.uid())); + +CREATE POLICY fee_schedule_items_delete_admin ON public.fee_schedule_items + FOR DELETE TO authenticated + USING (public.is_admin(auth.uid())); + +CREATE TRIGGER fee_schedule_items_set_updated_at + BEFORE UPDATE ON public.fee_schedule_items + FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); + +-- 3. Public logos bucket +INSERT INTO storage.buckets (id, name, public) +VALUES ('firm-logos', 'firm-logos', true) +ON CONFLICT (id) DO NOTHING; + +CREATE POLICY firm_logos_select_public ON storage.objects + FOR SELECT TO public + USING (bucket_id = 'firm-logos'); + +CREATE POLICY firm_logos_insert_auth ON storage.objects + FOR INSERT TO authenticated + WITH CHECK (bucket_id = 'firm-logos'); + +CREATE POLICY firm_logos_update_admin ON storage.objects + FOR UPDATE TO authenticated + USING (bucket_id = 'firm-logos' AND public.is_admin(auth.uid())); + +CREATE POLICY firm_logos_delete_admin ON storage.objects + FOR DELETE TO authenticated + USING (bucket_id = 'firm-logos' AND public.is_admin(auth.uid())); \ No newline at end of file From 4715dcc4f7114684669b28be4663c82c675b7256 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 00:51:25 +0000 Subject: [PATCH 02/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routeTree.gen.ts | 30 +++++++++++++++++++++++++ src/routes/settings.tsx | 50 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/routes/settings.tsx diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index b1c7c1a..c676208 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -10,6 +10,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as SetupRouteImport } from './routes/setup' +import { Route as SettingsRouteImport } from './routes/settings' import { Route as LoginRouteImport } from './routes/login' import { Route as IndexRouteImport } from './routes/index' import { Route as ClientsIndexRouteImport } from './routes/clients.index' @@ -24,6 +25,11 @@ const SetupRoute = SetupRouteImport.update({ path: '/setup', getParentRoute: () => rootRouteImport, } as any) +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) const LoginRoute = LoginRouteImport.update({ id: '/login', path: '/login', @@ -68,6 +74,7 @@ const AdminUsersRoute = AdminUsersRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/login': typeof LoginRoute + '/settings': typeof SettingsRoute '/setup': typeof SetupRoute '/admin/users': typeof AdminUsersRoute '/cases/$caseId': typeof CasesCaseIdRoute @@ -79,6 +86,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof LoginRoute + '/settings': typeof SettingsRoute '/setup': typeof SetupRoute '/admin/users': typeof AdminUsersRoute '/cases/$caseId': typeof CasesCaseIdRoute @@ -91,6 +99,7 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/login': typeof LoginRoute + '/settings': typeof SettingsRoute '/setup': typeof SetupRoute '/admin/users': typeof AdminUsersRoute '/cases/$caseId': typeof CasesCaseIdRoute @@ -104,6 +113,7 @@ export interface FileRouteTypes { fullPaths: | '/' | '/login' + | '/settings' | '/setup' | '/admin/users' | '/cases/$caseId' @@ -115,6 +125,7 @@ export interface FileRouteTypes { to: | '/' | '/login' + | '/settings' | '/setup' | '/admin/users' | '/cases/$caseId' @@ -126,6 +137,7 @@ export interface FileRouteTypes { | '__root__' | '/' | '/login' + | '/settings' | '/setup' | '/admin/users' | '/cases/$caseId' @@ -138,6 +150,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute LoginRoute: typeof LoginRoute + SettingsRoute: typeof SettingsRoute SetupRoute: typeof SetupRoute AdminUsersRoute: typeof AdminUsersRoute CasesCaseIdRoute: typeof CasesCaseIdRoute @@ -156,6 +169,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SetupRouteImport parentRoute: typeof rootRouteImport } + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } '/login': { id: '/login' path: '/login' @@ -218,6 +238,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, LoginRoute: LoginRoute, + SettingsRoute: SettingsRoute, SetupRoute: SetupRoute, AdminUsersRoute: AdminUsersRoute, CasesCaseIdRoute: CasesCaseIdRoute, @@ -229,3 +250,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> + } +} diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx new file mode 100644 index 0000000..5f3e848 --- /dev/null +++ b/src/routes/settings.tsx @@ -0,0 +1,50 @@ +import { createFileRoute, Link, useLocation } from "@tanstack/react-router"; +import { ProtectedLayout } from "@/components/protected-layout"; +import { PageContainer, PageHeader } from "@/components/app-shell"; +import { Outlet } from "@tanstack/react-router"; +import { cn } from "@/lib/utils"; + +export const Route = createFileRoute("/settings")({ + component: SettingsLayout, +}); + +const TABS = [ + { to: "/settings", label: "Company", exact: true }, + { to: "/settings/fees", label: "Fee schedule" }, +]; + +function SettingsLayout() { + const location = useLocation(); + return ( + + + +
+ {TABS.map((t) => { + const active = t.exact + ? location.pathname === t.to + : location.pathname.startsWith(t.to); + return ( + + {t.label} + + ); + })} +
+ +
+
+ ); +} From f20ca5bd67bfed0167984edd3384e6955391dd37 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 00:52:38 +0000 Subject: [PATCH 03/11] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routeTree.gen.ts | 62 +++++- src/routes/settings.fees.tsx | 400 ++++++++++++++++++++++++++++++++++ src/routes/settings.index.tsx | 347 +++++++++++++++++++++++++++++ 3 files changed, 803 insertions(+), 6 deletions(-) create mode 100644 src/routes/settings.fees.tsx create mode 100644 src/routes/settings.index.tsx diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index c676208..047b648 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -13,8 +13,10 @@ import { Route as SetupRouteImport } from './routes/setup' import { Route as SettingsRouteImport } from './routes/settings' import { Route as LoginRouteImport } from './routes/login' import { Route as IndexRouteImport } from './routes/index' +import { Route as SettingsIndexRouteImport } from './routes/settings.index' import { Route as ClientsIndexRouteImport } from './routes/clients.index' import { Route as CasesIndexRouteImport } from './routes/cases.index' +import { Route as SettingsFeesRouteImport } from './routes/settings.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' @@ -40,6 +42,11 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const SettingsIndexRoute = SettingsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SettingsRoute, +} as any) const ClientsIndexRoute = ClientsIndexRouteImport.update({ id: '/clients/', path: '/clients/', @@ -50,6 +57,11 @@ const CasesIndexRoute = CasesIndexRouteImport.update({ path: '/cases/', getParentRoute: () => rootRouteImport, } as any) +const SettingsFeesRoute = SettingsFeesRouteImport.update({ + id: '/fees', + path: '/fees', + getParentRoute: () => SettingsRoute, +} as any) const ClientsClientIdRoute = ClientsClientIdRouteImport.update({ id: '/clients/$clientId', path: '/clients/$clientId', @@ -74,39 +86,44 @@ const AdminUsersRoute = AdminUsersRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/login': typeof LoginRoute - '/settings': typeof SettingsRoute + '/settings': typeof SettingsRouteWithChildren '/setup': typeof SetupRoute '/admin/users': typeof AdminUsersRoute '/cases/$caseId': typeof CasesCaseIdRoute '/cases/new': typeof CasesNewRoute '/clients/$clientId': typeof ClientsClientIdRoute + '/settings/fees': typeof SettingsFeesRoute '/cases/': typeof CasesIndexRoute '/clients/': typeof ClientsIndexRoute + '/settings/': typeof SettingsIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof LoginRoute - '/settings': typeof SettingsRoute '/setup': typeof SetupRoute '/admin/users': typeof AdminUsersRoute '/cases/$caseId': typeof CasesCaseIdRoute '/cases/new': typeof CasesNewRoute '/clients/$clientId': typeof ClientsClientIdRoute + '/settings/fees': typeof SettingsFeesRoute '/cases': typeof CasesIndexRoute '/clients': typeof ClientsIndexRoute + '/settings': typeof SettingsIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/login': typeof LoginRoute - '/settings': typeof SettingsRoute + '/settings': typeof SettingsRouteWithChildren '/setup': typeof SetupRoute '/admin/users': typeof AdminUsersRoute '/cases/$caseId': typeof CasesCaseIdRoute '/cases/new': typeof CasesNewRoute '/clients/$clientId': typeof ClientsClientIdRoute + '/settings/fees': typeof SettingsFeesRoute '/cases/': typeof CasesIndexRoute '/clients/': typeof ClientsIndexRoute + '/settings/': typeof SettingsIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -119,20 +136,23 @@ export interface FileRouteTypes { | '/cases/$caseId' | '/cases/new' | '/clients/$clientId' + | '/settings/fees' | '/cases/' | '/clients/' + | '/settings/' fileRoutesByTo: FileRoutesByTo to: | '/' | '/login' - | '/settings' | '/setup' | '/admin/users' | '/cases/$caseId' | '/cases/new' | '/clients/$clientId' + | '/settings/fees' | '/cases' | '/clients' + | '/settings' id: | '__root__' | '/' @@ -143,14 +163,16 @@ export interface FileRouteTypes { | '/cases/$caseId' | '/cases/new' | '/clients/$clientId' + | '/settings/fees' | '/cases/' | '/clients/' + | '/settings/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute LoginRoute: typeof LoginRoute - SettingsRoute: typeof SettingsRoute + SettingsRoute: typeof SettingsRouteWithChildren SetupRoute: typeof SetupRoute AdminUsersRoute: typeof AdminUsersRoute CasesCaseIdRoute: typeof CasesCaseIdRoute @@ -190,6 +212,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/settings/': { + id: '/settings/' + path: '/' + fullPath: '/settings/' + preLoaderRoute: typeof SettingsIndexRouteImport + parentRoute: typeof SettingsRoute + } '/clients/': { id: '/clients/' path: '/clients' @@ -204,6 +233,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof CasesIndexRouteImport parentRoute: typeof rootRouteImport } + '/settings/fees': { + id: '/settings/fees' + path: '/fees' + fullPath: '/settings/fees' + preLoaderRoute: typeof SettingsFeesRouteImport + parentRoute: typeof SettingsRoute + } '/clients/$clientId': { id: '/clients/$clientId' path: '/clients/$clientId' @@ -235,10 +271,24 @@ declare module '@tanstack/react-router' { } } +interface SettingsRouteChildren { + SettingsFeesRoute: typeof SettingsFeesRoute + SettingsIndexRoute: typeof SettingsIndexRoute +} + +const SettingsRouteChildren: SettingsRouteChildren = { + SettingsFeesRoute: SettingsFeesRoute, + SettingsIndexRoute: SettingsIndexRoute, +} + +const SettingsRouteWithChildren = SettingsRoute._addFileChildren( + SettingsRouteChildren, +) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, LoginRoute: LoginRoute, - SettingsRoute: SettingsRoute, + SettingsRoute: SettingsRouteWithChildren, SetupRoute: SetupRoute, AdminUsersRoute: AdminUsersRoute, CasesCaseIdRoute: CasesCaseIdRoute, diff --git a/src/routes/settings.fees.tsx b/src/routes/settings.fees.tsx new file mode 100644 index 0000000..9d922e3 --- /dev/null +++ b/src/routes/settings.fees.tsx @@ -0,0 +1,400 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/lib/auth"; +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 { Textarea } from "@/components/ui/textarea"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Clock, Loader2, Pencil, Plus, Receipt, Trash2 } from "lucide-react"; +import { toast } from "sonner"; +import { formatCurrency } from "@/lib/format"; + +export const Route = createFileRoute("/settings/fees")({ + component: FeeSchedulePage, +}); + +interface FeeItem { + id: string; + name: string; + description: string | null; + category: "time" | "expense"; + amount: number; + billable: boolean; + active: boolean; + sort_order: number; +} + +function FeeSchedulePage() { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [editing, setEditing] = useState | null>(null); + + const load = async () => { + setLoading(true); + const { data } = await supabase + .from("fee_schedule_items") + .select("*") + .order("category") + .order("sort_order") + .order("name"); + setItems((data ?? []) as FeeItem[]); + setLoading(false); + }; + + useEffect(() => { + load(); + }, []); + + const remove = async (id: string) => { + if (!confirm("Delete this fee item?")) return; + const { error } = await supabase + .from("fee_schedule_items") + .delete() + .eq("id", id); + if (error) toast.error("Could not delete", { description: error.message }); + else { + toast.success("Fee item deleted"); + load(); + } + }; + + const timeItems = items.filter((i) => i.category === "time"); + const expenseItems = items.filter((i) => i.category === "expense"); + + return ( +
+
+

+ Define reusable billable items. Time fees set the hourly rate when + logging time; expense fees auto-fill the amount when adding an + expense. The billable flag becomes the default for new entries. +

+ +
+ + } + unit="/ hr" + items={timeItems} + loading={loading} + onEdit={setEditing} + onDelete={remove} + /> + + } + unit="" + items={expenseItems} + loading={loading} + onEdit={setEditing} + onDelete={remove} + /> + + setEditing(null)} + onSaved={load} + /> +
+ ); +} + +function FeeSection({ + title, + icon, + unit, + items, + loading, + onEdit, + onDelete, +}: { + title: string; + icon: React.ReactNode; + unit: string; + items: FeeItem[]; + loading: boolean; + onEdit: (i: FeeItem) => void; + onDelete: (id: string) => void; +}) { + return ( +
+

+ {icon} + {title} +

+ + + {loading ? ( +
+ Loading… +
+ ) : items.length === 0 ? ( +
+ No {title.toLowerCase()} configured yet. +
+ ) : ( + + + + Name + Description + Amount + Billable + Active + + + + + {items.map((i) => ( + + {i.name} + + {i.description || "—"} + + + {formatCurrency(Number(i.amount))} + {unit && ( + + {unit} + + )} + + + {i.billable ? ( + + Billable + + ) : ( + No + )} + + + {i.active ? ( + + Active + + ) : ( + + Inactive + + )} + + + + + + + ))} + +
+ )} +
+
+
+ ); +} + +function FeeEditDialog({ + item, + onClose, + onSaved, +}: { + item: Partial | null; + onClose: () => void; + onSaved: () => void; +}) { + const { user } = useAuth(); + const [form, setForm] = useState>({}); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (item) setForm(item); + }, [item]); + + const submit = async () => { + if (!form.name?.trim()) return toast.error("Name required"); + if (!form.category) return toast.error("Category required"); + const payload: any = { + name: form.name.trim(), + description: form.description?.trim() || null, + category: form.category, + amount: Number(form.amount) || 0, + billable: form.billable ?? true, + active: form.active ?? true, + sort_order: form.sort_order ?? 0, + }; + setSaving(true); + const { error } = form.id + ? await supabase + .from("fee_schedule_items") + .update(payload) + .eq("id", form.id) + : await supabase + .from("fee_schedule_items") + .insert({ ...payload, created_by: user?.id }); + setSaving(false); + if (error) { + toast.error("Could not save", { description: error.message }); + return; + } + toast.success(form.id ? "Fee updated" : "Fee added"); + onClose(); + onSaved(); + }; + + return ( + !v && onClose()}> + + + + {form.id ? "Edit fee item" : "Add fee item"} + + + Time fees feed the hourly rate dropdown; expense fees feed the + expense amount picker. + + +
+
+
+ + +
+
+ + + setForm({ ...form, amount: parseFloat(e.target.value) || 0 }) + } + /> +
+
+
+ + setForm({ ...form, name: e.target.value })} + placeholder={ + form.category === "expense" + ? "e.g. Filing fee" + : "e.g. Senior partner rate" + } + /> +
+
+ +