From a9857b1b75e4dea12ac2bbf9d64f3db2ce088a3c Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 14:47:55 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- .../clients/client-custom-fields-inline.tsx | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/components/clients/client-custom-fields-inline.tsx diff --git a/src/components/clients/client-custom-fields-inline.tsx b/src/components/clients/client-custom-fields-inline.tsx new file mode 100644 index 0000000..4a5055e --- /dev/null +++ b/src/components/clients/client-custom-fields-inline.tsx @@ -0,0 +1,182 @@ +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, Pencil, X, ListPlus } from "lucide-react"; +import { toast } from "sonner"; +import { formatDate } from "@/lib/format"; + +interface FieldDef { + id: string; + key: string; + label: string; + field_type: string; + description: string | null; + sort_order: number; +} + +/** + * Inline custom fields panel for a client. Shows only filled fields when + * viewing; switches to a full editable grid (all active fields) on Edit. + * Renders nothing when there are no field definitions and nothing is filled. + */ +export function ClientCustomFieldsInline({ clientId }: { clientId: string }) { + const [defs, setDefs] = useState([]); + const [values, setValues] = useState>({}); + const [loading, setLoading] = useState(true); + const [editing, setEditing] = useState(false); + const [saving, setSaving] = useState(false); + + const load = async () => { + setLoading(true); + const [defsRes, valsRes] = await Promise.all([ + supabase + .from("custom_client_fields") + .select("id, key, label, field_type, description, sort_order") + .eq("active", true) + .order("sort_order"), + supabase + .from("client_field_values") + .select("field_id, value") + .eq("client_id", clientId), + ]); + setDefs((defsRes.data ?? []) as FieldDef[]); + const map: Record = {}; + (valsRes.data ?? []).forEach((r: any) => { + map[r.field_id] = r.value ?? ""; + }); + setValues(map); + setLoading(false); + }; + + useEffect(() => { + load(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [clientId]); + + const filled = defs + .map((d) => ({ ...d, value: (values[d.id] ?? "").toString().trim() })) + .filter((d) => d.value.length > 0); + + if (loading) return null; + // Hide entirely if there are no defs at all + if (defs.length === 0) return null; + // When viewing and nothing is filled, hide unless user clicks the Add fields entry + // We'll show a small "Add custom field info" affordance instead. + if (!editing && filled.length === 0) { + return ( + + +
+ + Custom client fields available +
+ +
+
+ ); + } + + const save = async () => { + setSaving(true); + for (const d of defs) { + const value = (values[d.id] ?? "").toString(); + const { error } = await supabase + .from("client_field_values") + .upsert( + { client_id: clientId, field_id: d.id, value }, + { onConflict: "client_id,field_id" }, + ); + if (error) { + setSaving(false); + toast.error("Save failed", { description: error.message }); + return; + } + } + setSaving(false); + setEditing(false); + toast.success("Custom fields saved"); + }; + + return ( + + +
+
+ +

Custom fields

+
+ {editing ? ( +
+ + +
+ ) : ( + + )} +
+ + {editing ? ( +
+ {defs.map((d) => ( +
+ + {d.field_type === "textarea" ? ( +