From f14c61b3fb8d9f1f8b3aa1c3d41d4a48a66e6833 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:13:10 +0000 Subject: [PATCH 1/5] Work in progress --- bun.lockb | Bin 356158 -> 356254 bytes src/routeTree.gen.ts | 9 +++++++++ 2 files changed, 9 insertions(+) diff --git a/bun.lockb b/bun.lockb index dab31462ab9a18393a0e54b194906f36db5170fd..050aece84acde1263d7bdeebc4eebe41aeebaa06 100755 GIT binary patch delta 3104 zcmZ9NdvHxh9LLYi{hf@6#Pg<}tso>J+*C95h*Ee=r#^7;UwxRcXC|_UbI>k~H>j$<9tM7(i8AB!#j`pDxxKb61fUZ^vXN^)Y60lam zG{vI8W`Om??`XxMq1UVI(iQ6ub_QBz^qOKZ&{LshLK%t;fSv{|uh(tnYn-2CllHT% zwLSb>YHtr3C8p<4pfr|Hv^bVSy=qcUUh8^6gS8&8juN2*%MibG9lfe~r+i9=#=NnjAfML@PmJ&_;H_Z) zv;vx<{eKrwtkzT4(=g~Cjv?l?^;{d)g9~Yd=H-PH4b3&^vVn#`^IWSoP)}&C--8X* zN9&Ord2M#h%E(tt&ZW+}v(+ZvDFi6c$gjxR$T>y49*%jZ$XVy;%@n8oRhxM|?CHLR zg28#N`CI6+_AK7Yb>Lin-%0~CpRmmtTf5DPAGV!RwWnk|?-61~i_l^xmvzNl6P~Nh z4jQffD|a}#Ki=W&-ISdaqdhqO@=?I!BDcXgo5dyiY)%Bn8K0|0SarK7=lRk4YPYlO zd%G!2nMBGSXA6q<=;Ik7%u;7bQ%d=GaJjCRa>`tm^_r?Jl4nL|8DKZXnXg*0z2Ps5{ z4mm_up}_}@uFMXMSeAWkf*(I;%4okhIJI>EaqGZJ@CaA}|uL}4nD7+S>@IEI~BXBiE{W`{c{jq%~kQLmQmcWLCgY z)8IG;HF7mL)Zs=NszVJv%ZHnJ<5`rv`Ye}_LF_rFbMHAlXrKsgqQfeH74$6<(9!bG zlYox6KwoQwQZ^UnMQ0_?E}@hQFFL6{`+w*BOZ+aBOR(UMmrn$@U$0T78T22Ik63M*FHZ4~Y05t*9mPj{=+n)uP&BP&(DgQwo&@I4}f9ej%4KU?vR{{$|cFL!hZ?26sQ#{&=o7N(1 MoY_)*Jj<;A7nlG!OaK4? delta 3012 zcmZ9Nc~DnX6vy9>-+AOu6EX!AMI#jj6a{g^B^3|_G@7XynN)^~%1pAHO2-<|q*4}f z<0`T!DIzJNh|d)lToAW#8MRT=%(SM(#+tP8kG}W3dwuB}?(lt|_r2$ybMAfbS9;yA zzQwO#l2>)c{N(IpS9Y>jU9x+}6j!okPIY&2xnf1oGw%J86 z{24wN_f4B!0&qe4VmwgnvSQDH{ifIz#oB@Wq1g9e_%oizcmo61G~=o=w#QutGsC#1 zSO?r^smU#h`GLKq*mcGH!KN#AL$QuvokY7-)L8F^#`S}G)EPQVv0GsHGXgNmq`-As z`MN;=gJRp@|4)i_g|=h^aNSWX5IPs^S^U4NSU2c=n`Ih51IqQfW9&q8Ws`pelZ(E9 zVJYSf6R;74F<)&$D=@?{Uc^WTlMVGK)&u%O4B5NZU@~4$j1{UEJ=vFO7`*^jD%eX+ z4hH)QOg1@KP3{fd04=Z4%GU>af?{LvUnUrWaZn0eG0GPTeN3@f#lparXCi&sjyMJT zLeG*V;Toq{Kj?Z@alB&TU=50mSFAtS3^3UZnUoAO0Ar0RE#*g)uaq2)DEv8*&3?8>mJ(%f&i5uIkbr&yoObQilkcQfo~$yU$@?s=`edXWfUP6E~ZfJxw)7IX&$zT0!4Zbb+RWViW4~$B#l)RCA#NQZ(GNS z+*}IK{Ol6)6ZdjyJHY%ZN)eCp^uvK7A)g|_dDZHC8emU~7r{$-5goX#$XcQ&MF~Xg zuY*l4V9o*77tkj95pM>Gg61B4UP2?G zd9C{;6b#M%ODv_nT5l@l-0Ys!QX6qNpStRYv&wj<5THn-nQNVmY+lRxaLkx>PM%xV z(J1W?Ea!aK^F=xJ0q3<^%IT8!-1&z4z_q-(o`z_?f4wue#|9^U-Udq09!~}D5n@J( z#T8C14=T7Pyw*n>X`=T3xzVZp`tdROVQnF@|GK3mPO zg1@Dl$4BedW+&|lTd1EhiOMaHx1g|EpUw!8UhQPmP|c@$=*XgvS-L> zq-fsDEoVJtA9=*YeY&!d;?O=$g}V~9pF^{;V!z`FsON4mFQ_N^>0`fn!146ki}Stk z03R?mK0KgboEbWiUk>uw<;SP?P-`*kkPa|P+&-ia*gU(6vEuq+-aT27h-={1K*xwj z2`FN311HPiMgox-a)jSn2@3a+x2QQ{f0*Tr;YTqe{HQad;wYB^bG(Q+rYA16R~|3I zk8{O%949s(x4(RHLjxLlBY4sLMlP0h=x*{AoldZkLE#A+W(SKAp-m_?u*p96GMmj! z$fm5x$)r^|S^5O9+@}1{@&O7VQ+`mj`*qC&P4n@`b-8Kw`k%)m{xZ!cmzd^l%AY13 z#Um~}1+0W;%pf0aDfKhuFA3XIJDRP8pT8-8KOL~Z-;_V;4*1>QlpkFQEKI4CFE8Ij Wr() + +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 f0687633b6978c72522f1e85a7f13feff269d3ed Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:13:23 +0000 Subject: [PATCH 2/5] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routeTree.gen.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 89d5bbb..8faeb78 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -868,12 +868,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> - } -} From 01692742dafcb8ccdf012c20d866e1f40507d08b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:13:48 +0000 Subject: [PATCH 3/5] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/integrations/supabase/types.ts | 81 +++++++++++++++++++ ...4_bd4ad166-596b-4c5c-8260-9f9ead6f6b73.sql | 63 +++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 supabase/migrations/20260418171344_bd4ad166-596b-4c5c-8260-9f9ead6f6b73.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 2cbfb13..4ce7b79 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -412,6 +412,48 @@ export type Database = { }, ] } + client_field_values: { + Row: { + client_id: string + created_at: string + field_id: string + id: string + updated_at: string + value: string | null + } + Insert: { + client_id: string + created_at?: string + field_id: string + id?: string + updated_at?: string + value?: string | null + } + Update: { + client_id?: string + created_at?: string + field_id?: string + id?: string + updated_at?: string + value?: string | null + } + Relationships: [ + { + foreignKeyName: "client_field_values_client_id_fkey" + columns: ["client_id"] + isOneToOne: false + referencedRelation: "clients" + referencedColumns: ["id"] + }, + { + foreignKeyName: "client_field_values_field_id_fkey" + columns: ["field_id"] + isOneToOne: false + referencedRelation: "custom_client_fields" + referencedColumns: ["id"] + }, + ] + } clients: { Row: { address_line1: string | null @@ -1007,6 +1049,45 @@ export type Database = { } Relationships: [] } + custom_client_fields: { + Row: { + active: boolean + created_at: string + created_by: string | null + description: string | null + field_type: string + id: string + key: string + label: string + sort_order: number + updated_at: string + } + Insert: { + active?: boolean + created_at?: string + created_by?: string | null + description?: string | null + field_type?: string + id?: string + key: string + label: string + sort_order?: number + updated_at?: string + } + Update: { + active?: boolean + created_at?: string + created_by?: string | null + description?: string | null + field_type?: string + id?: string + key?: string + label?: string + sort_order?: number + updated_at?: string + } + Relationships: [] + } custom_form_templates: { Row: { body_html: string diff --git a/supabase/migrations/20260418171344_bd4ad166-596b-4c5c-8260-9f9ead6f6b73.sql b/supabase/migrations/20260418171344_bd4ad166-596b-4c5c-8260-9f9ead6f6b73.sql new file mode 100644 index 0000000..ddfa02d --- /dev/null +++ b/supabase/migrations/20260418171344_bd4ad166-596b-4c5c-8260-9f9ead6f6b73.sql @@ -0,0 +1,63 @@ +-- Custom client fields (admin-defined) +CREATE TABLE public.custom_client_fields ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + key text NOT NULL UNIQUE, + label text NOT NULL, + field_type text NOT NULL DEFAULT 'text', + description text, + sort_order integer NOT NULL DEFAULT 0, + active boolean NOT NULL DEFAULT true, + created_by uuid, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +ALTER TABLE public.custom_client_fields ENABLE ROW LEVEL SECURITY; + +CREATE POLICY ccf_client_select_auth ON public.custom_client_fields + FOR SELECT TO authenticated USING (true); +CREATE POLICY ccf_client_insert_admin ON public.custom_client_fields + FOR INSERT TO authenticated WITH CHECK (is_admin(auth.uid())); +CREATE POLICY ccf_client_update_admin ON public.custom_client_fields + FOR UPDATE TO authenticated USING (is_admin(auth.uid())); +CREATE POLICY ccf_client_delete_admin ON public.custom_client_fields + FOR DELETE TO authenticated USING (is_admin(auth.uid())); + +CREATE TRIGGER trg_ccf_client_updated_at + BEFORE UPDATE ON public.custom_client_fields + FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); + +-- Per-client values +CREATE TABLE public.client_field_values ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + client_id uuid NOT NULL REFERENCES public.clients(id) ON DELETE CASCADE, + field_id uuid NOT NULL REFERENCES public.custom_client_fields(id) ON DELETE CASCADE, + value text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (client_id, field_id) +); + +ALTER TABLE public.client_field_values ENABLE ROW LEVEL SECURITY; + +-- Anyone authenticated can view; admins or client creator can manage +CREATE POLICY clfv_select_auth ON public.client_field_values + FOR SELECT TO authenticated USING (true); +CREATE POLICY clfv_insert_auth ON public.client_field_values + FOR INSERT TO authenticated WITH CHECK (auth.uid() IS NOT NULL); +CREATE POLICY clfv_update_auth ON public.client_field_values + FOR UPDATE TO authenticated USING ( + is_admin(auth.uid()) + OR EXISTS (SELECT 1 FROM public.clients c WHERE c.id = client_id AND c.created_by = auth.uid()) + ); +CREATE POLICY clfv_delete_auth ON public.client_field_values + FOR DELETE TO authenticated USING ( + is_admin(auth.uid()) + OR EXISTS (SELECT 1 FROM public.clients c WHERE c.id = client_id AND c.created_by = auth.uid()) + ); + +CREATE TRIGGER trg_clfv_updated_at + BEFORE UPDATE ON public.client_field_values + FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); + +CREATE INDEX idx_client_field_values_client ON public.client_field_values(client_id); \ No newline at end of file From edb3f2541207fc566b1061544a523b0d2e3b0b63 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:14:41 +0000 Subject: [PATCH 4/5] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- .../clients/client-custom-fields-tab.tsx | 116 ++++++++++ src/routes/clients.$clientId.tsx | 8 +- src/routes/settings.client-fields.tsx | 208 ++++++++++++++++++ src/routes/settings.tsx | 1 + 4 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 src/components/clients/client-custom-fields-tab.tsx create mode 100644 src/routes/settings.client-fields.tsx diff --git a/src/components/clients/client-custom-fields-tab.tsx b/src/components/clients/client-custom-fields-tab.tsx new file mode 100644 index 0000000..82001af --- /dev/null +++ b/src/components/clients/client-custom-fields-tab.tsx @@ -0,0 +1,116 @@ +import { useEffect, useState } from "react"; +import { supabase } from "@/integrations/supabase/client"; +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 { Button } from "@/components/ui/button"; +import { Loader2, Save } from "lucide-react"; +import { toast } from "sonner"; + +interface FieldDef { + id: string; + key: string; + label: string; + field_type: string; + description: string | null; +} + +export function ClientCustomFieldsTab({ clientId }: { clientId: string }) { + const [defs, setDefs] = useState([]); + const [values, setValues] = useState>({}); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + (async () => { + setLoading(true); + const [{ data: d }, { data: v }] = await Promise.all([ + supabase + .from("custom_client_fields") + .select("id,key,label,field_type,description") + .eq("active", true) + .order("sort_order"), + supabase.from("client_field_values").select("field_id,value").eq("client_id", clientId), + ]); + const list = (d ?? []) as FieldDef[]; + setDefs(list); + const map: Record = {}; + (v ?? []).forEach((row: any) => { + map[row.field_id] = row.value ?? ""; + }); + setValues(map); + setLoading(false); + })(); + }, [clientId]); + + const save = async () => { + setSaving(true); + const rows = defs.map((d) => ({ + client_id: clientId, + field_id: d.id, + value: values[d.id] ?? "", + })); + for (const row of rows) { + const { error } = await supabase + .from("client_field_values") + .upsert(row, { onConflict: "client_id,field_id" }); + if (error) { + toast.error("Save failed", { description: error.message }); + setSaving(false); + return; + } + } + setSaving(false); + toast.success("Custom fields saved"); + }; + + if (loading) return

Loading…

; + + if (defs.length === 0) { + return ( + + +

+ No custom client fields are configured. An admin can add them in Settings → Custom client fields. +

+
+
+ ); + } + + return ( + + +
+ {defs.map((d) => ( +
+ + {d.field_type === "textarea" ? ( +