import { createFileRoute } from "@tanstack/react-router"; import { useEffect, useState } from "react"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Trash2, Plus, Loader2 } from "lucide-react"; import { toast } from "sonner"; export const Route = createFileRoute("/settings/client-fields")({ component: ClientFieldsPage, }); interface CustomField { id: string; key: string; label: string; field_type: string; description: string | null; sort_order: number; active: boolean; } const TYPES = [ { value: "text", label: "Text" }, { value: "number", label: "Number" }, { value: "date", label: "Date" }, { value: "textarea", label: "Long text" }, ]; function slugify(s: string) { return s .toLowerCase() .replace(/[^a-z0-9]+/g, "_") .replace(/^_|_$/g, "") .slice(0, 40); } function ClientFieldsPage() { const { isAdmin } = useAuth(); const [fields, setFields] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [form, setForm] = useState({ label: "", key: "", field_type: "text", description: "" }); const load = async () => { setLoading(true); const { data } = await supabase .from("custom_client_fields") .select("*") .order("sort_order", { ascending: true }) .order("created_at", { ascending: true }); setFields((data ?? []) as CustomField[]); setLoading(false); }; useEffect(() => { load(); }, []); const add = async () => { const label = form.label.trim(); if (!label) { toast.error("Label is required"); return; } const key = (form.key.trim() || slugify(label)).toLowerCase(); if (!key) { toast.error("Could not derive a key from label"); return; } setSaving(true); const { error } = await supabase.from("custom_client_fields").insert({ label, key, field_type: form.field_type, description: form.description.trim() || null, sort_order: fields.length, }); setSaving(false); if (error) { toast.error("Could not add field", { description: error.message }); return; } setForm({ label: "", key: "", field_type: "text", description: "" }); toast.success("Field added"); load(); }; const update = async (id: string, patch: Partial) => { const { error } = await supabase.from("custom_client_fields").update(patch).eq("id", id); if (error) toast.error(error.message); else load(); }; const remove = async (id: string) => { if (!confirm("Delete this custom field? Existing client values will be removed.")) return; const { error } = await supabase.from("custom_client_fields").delete().eq("id", id); if (error) toast.error(error.message); else { toast.success("Field removed"); load(); } }; if (!isAdmin) { return

Only admins can manage custom client fields.

; } return (
Add a custom client field
setForm({ ...form, label: e.target.value, key: form.key || slugify(e.target.value) })} />
setForm({ ...form, key: slugify(e.target.value) })} />

Used in templates as {`{{client.custom.${form.key || "your_key"}}}`}

setForm({ ...form, description: e.target.value })} placeholder="Short helper text" />
Existing fields ({fields.length}) {loading ? (

Loading…

) : fields.length === 0 ? (

No custom client fields yet. Add one above to get started.

) : (
{fields.map((f) => (
setFields((xs) => xs.map((x) => x.id === f.id ? { ...x, label: e.target.value } : x))} onBlur={(e) => update(f.id, { label: e.target.value })} />
{`{{client.custom.${f.key}}}`}
update(f.id, { active: v })} /> Active
))}
)}
); }