Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 17:14:41 +00:00
co-authored by renee-png
parent 01692742da
commit edb3f25412
4 changed files with 332 additions and 1 deletions
@@ -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<FieldDef[]>([]);
const [values, setValues] = useState<Record<string, string>>({});
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<string, string> = {};
(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 <p className="text-sm text-muted-foreground">Loading…</p>;
if (defs.length === 0) {
return (
<Card>
<CardContent className="pt-6">
<p className="text-sm text-muted-foreground">
No custom client fields are configured. An admin can add them in <strong>Settings → Custom client fields</strong>.
</p>
</CardContent>
</Card>
);
}
return (
<Card>
<CardContent className="pt-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{defs.map((d) => (
<div key={d.id} className={d.field_type === "textarea" ? "md:col-span-2" : ""}>
<Label className="flex items-center gap-2">
{d.label}
<code className="text-[10px] font-mono text-muted-foreground">{`{{client.custom.${d.key}}}`}</code>
</Label>
{d.field_type === "textarea" ? (
<Textarea
rows={3}
value={values[d.id] ?? ""}
onChange={(e) => setValues((m) => ({ ...m, [d.id]: e.target.value }))}
/>
) : (
<Input
type={d.field_type === "number" ? "number" : d.field_type === "date" ? "date" : "text"}
value={values[d.id] ?? ""}
onChange={(e) => setValues((m) => ({ ...m, [d.id]: e.target.value }))}
/>
)}
{d.description && <p className="text-[11px] text-muted-foreground mt-1">{d.description}</p>}
</div>
))}
</div>
<Button onClick={save} disabled={saving}>
{saving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
Save changes
</Button>
</CardContent>
</Card>
);
}