From 6deb196d2fe17c1b93f7f7e537806114a98d32de 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 01:33:35 +0000 Subject: [PATCH 1/6] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/integrations/supabase/types.ts | 126 ++++++++++++++++++ ...2_132f0401-f02d-443a-a586-1b050d56ebdd.sql | 69 ++++++++++ 2 files changed, 195 insertions(+) create mode 100644 supabase/migrations/20260418013332_132f0401-f02d-443a-a586-1b050d56ebdd.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 10a7930..a14d4eb 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -141,6 +141,48 @@ export type Database = { }, ] } + case_field_values: { + Row: { + case_id: string + created_at: string + field_id: string + id: string + updated_at: string + value: string | null + } + Insert: { + case_id: string + created_at?: string + field_id: string + id?: string + updated_at?: string + value?: string | null + } + Update: { + case_id?: string + created_at?: string + field_id?: string + id?: string + updated_at?: string + value?: string | null + } + Relationships: [ + { + foreignKeyName: "case_field_values_case_id_fkey" + columns: ["case_id"] + isOneToOne: false + referencedRelation: "cases" + referencedColumns: ["id"] + }, + { + foreignKeyName: "case_field_values_field_id_fkey" + columns: ["field_id"] + isOneToOne: false + referencedRelation: "custom_case_fields" + referencedColumns: ["id"] + }, + ] + } cases: { Row: { assigned_attorney_id: string | null @@ -859,6 +901,90 @@ export type Database = { } Relationships: [] } + custom_case_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 + created_at: string + created_by: string | null + description: string | null + font_family: string + font_size_pt: number + hide_title: boolean + id: string + name: string + signature_blocks: Json + title: string + updated_at: string + } + Insert: { + body_html?: string + created_at?: string + created_by?: string | null + description?: string | null + font_family?: string + font_size_pt?: number + hide_title?: boolean + id?: string + name: string + signature_blocks?: Json + title?: string + updated_at?: string + } + Update: { + body_html?: string + created_at?: string + created_by?: string | null + description?: string | null + font_family?: string + font_size_pt?: number + hide_title?: boolean + id?: string + name?: string + signature_blocks?: Json + title?: string + updated_at?: string + } + Relationships: [] + } document_folders: { Row: { case_id: string diff --git a/supabase/migrations/20260418013332_132f0401-f02d-443a-a586-1b050d56ebdd.sql b/supabase/migrations/20260418013332_132f0401-f02d-443a-a586-1b050d56ebdd.sql new file mode 100644 index 0000000..c9e0387 --- /dev/null +++ b/supabase/migrations/20260418013332_132f0401-f02d-443a-a586-1b050d56ebdd.sql @@ -0,0 +1,69 @@ +-- 1) Firm-wide custom case field definitions +CREATE TABLE public.custom_case_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_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + created_by uuid +); +ALTER TABLE public.custom_case_fields ENABLE ROW LEVEL SECURITY; +CREATE POLICY ccf_select_auth ON public.custom_case_fields FOR SELECT TO authenticated USING (true); +CREATE POLICY ccf_insert_admin ON public.custom_case_fields FOR INSERT TO authenticated WITH CHECK (public.is_admin(auth.uid())); +CREATE POLICY ccf_update_admin ON public.custom_case_fields FOR UPDATE TO authenticated USING (public.is_admin(auth.uid())); +CREATE POLICY ccf_delete_admin ON public.custom_case_fields FOR DELETE TO authenticated USING (public.is_admin(auth.uid())); +CREATE TRIGGER trg_ccf_updated_at BEFORE UPDATE ON public.custom_case_fields + FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); + +-- 2) Per-case values for those fields +CREATE TABLE public.case_field_values ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + case_id uuid NOT NULL REFERENCES public.cases(id) ON DELETE CASCADE, + field_id uuid NOT NULL REFERENCES public.custom_case_fields(id) ON DELETE CASCADE, + value text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (case_id, field_id) +); +CREATE INDEX idx_case_field_values_case ON public.case_field_values(case_id); +ALTER TABLE public.case_field_values ENABLE ROW LEVEL SECURITY; +CREATE POLICY cfv_select_case ON public.case_field_values FOR SELECT TO authenticated + USING (public.can_access_case(case_id, auth.uid())); +CREATE POLICY cfv_insert_case ON public.case_field_values FOR INSERT TO authenticated + WITH CHECK (public.can_access_case(case_id, auth.uid())); +CREATE POLICY cfv_update_case ON public.case_field_values FOR UPDATE TO authenticated + USING (public.can_access_case(case_id, auth.uid())); +CREATE POLICY cfv_delete_case ON public.case_field_values FOR DELETE TO authenticated + USING (public.can_access_case(case_id, auth.uid())); +CREATE TRIGGER trg_cfv_updated_at BEFORE UPDATE ON public.case_field_values + FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); + +-- 3) Custom-form templates (separate from document_templates so we can store rich settings) +CREATE TABLE public.custom_form_templates ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL, + description text, + title text NOT NULL DEFAULT '', + hide_title boolean NOT NULL DEFAULT false, + body_html text NOT NULL DEFAULT '', + font_family text NOT NULL DEFAULT 'Bookman Old Style', + font_size_pt integer NOT NULL DEFAULT 12, + signature_blocks jsonb NOT NULL DEFAULT '[]'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + created_by uuid +); +ALTER TABLE public.custom_form_templates ENABLE ROW LEVEL SECURITY; +CREATE POLICY cft_select_auth ON public.custom_form_templates FOR SELECT TO authenticated USING (true); +CREATE POLICY cft_insert_auth ON public.custom_form_templates FOR INSERT TO authenticated + WITH CHECK (auth.uid() IS NOT NULL); +CREATE POLICY cft_update_owner ON public.custom_form_templates FOR UPDATE TO authenticated + USING (public.is_admin(auth.uid()) OR created_by = auth.uid()); +CREATE POLICY cft_delete_owner ON public.custom_form_templates FOR DELETE TO authenticated + USING (public.is_admin(auth.uid()) OR created_by = auth.uid()); +CREATE TRIGGER trg_cft_updated_at BEFORE UPDATE ON public.custom_form_templates + FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); \ No newline at end of file From 59a889d5abed815bf5bc3c1ca5e35653f0dbeeb1 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 01:34:27 +0000 Subject: [PATCH 2/6] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/custom-fields-tab.tsx | 118 ++++++++++++ src/routes/settings.custom-fields.tsx | 208 +++++++++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 src/components/cases/custom-fields-tab.tsx create mode 100644 src/routes/settings.custom-fields.tsx diff --git a/src/components/cases/custom-fields-tab.tsx b/src/components/cases/custom-fields-tab.tsx new file mode 100644 index 0000000..cd9d491 --- /dev/null +++ b/src/components/cases/custom-fields-tab.tsx @@ -0,0 +1,118 @@ +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 CaseCustomFieldsTab({ caseId }: { caseId: 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_case_fields") + .select("id,key,label,field_type,description") + .eq("active", true) + .order("sort_order"), + supabase.from("case_field_values").select("field_id,value").eq("case_id", caseId), + ]); + const list = (d ?? []) as FieldDef[]; + setDefs(list); + const map: Record = {}; + (v ?? []).forEach((row: any) => { + map[row.field_id] = row.value ?? ""; + }); + setValues(map); + setLoading(false); + })(); + }, [caseId]); + + const save = async () => { + setSaving(true); + const rows = defs.map((d) => ({ + case_id: caseId, + field_id: d.id, + value: values[d.id] ?? "", + })); + // Upsert each row by (case_id, field_id) + for (const row of rows) { + const { error } = await supabase + .from("case_field_values") + .upsert(row, { onConflict: "case_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 fields are configured. An admin can add them in Settings → Custom case fields. + Once defined, they'll appear here and can be used as variables in Custom Forms. +

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