'use client'; import { useCallback, useRef, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { ChevronDown, ChevronRight } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Button } from '@/components/ui/button'; import { apiFetch } from '@/lib/api/client'; // ─── Types ──────────────────────────────────────────────────────────────────── interface CustomFieldDefinition { id: string; fieldName: string; fieldLabel: string; fieldType: 'text' | 'number' | 'date' | 'boolean' | 'select'; selectOptions: string[] | null; isRequired: boolean; sortOrder: number; entityType: string; } interface CustomFieldValue { id: string; fieldId: string; entityId: string; value: unknown; } interface FieldEntry { definition: CustomFieldDefinition; value: CustomFieldValue | null; } interface CustomFieldsSectionProps { entityType: 'client' | 'interest' | 'berth'; entityId: string; } // ─── Component ──────────────────────────────────────────────────────────────── export function CustomFieldsSection({ entityType, entityId }: CustomFieldsSectionProps) { const [collapsed, setCollapsed] = useState(false); const queryClient = useQueryClient(); // ── Data fetching ────────────────────────────────────────────────────────── const { data: entries, isLoading } = useQuery({ queryKey: ['custom-field-values', entityId], queryFn: async () => { const res = await apiFetch<{ data: FieldEntry[] }>( `/api/v1/custom-fields/${entityId}`, ); return res.data; }, enabled: !!entityId, }); // Only show fields for this entity type const filteredEntries = entries?.filter((e) => e.definition.entityType === entityType) ?? []; // ── Mutation ─────────────────────────────────────────────────────────────── const mutation = useMutation({ mutationFn: async (values: Array<{ fieldId: string; value: unknown }>) => { await apiFetch(`/api/v1/custom-fields/${entityId}`, { method: 'PUT', body: { values }, }); }, onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ['custom-field-values', entityId] }); }, }); if (isLoading) { return ( Custom Fields
{[1, 2].map((i) => (
))}
); } return ( setCollapsed((c) => !c)} >
Custom Fields {collapsed ? ( ) : ( )}
{!collapsed && ( {filteredEntries.length === 0 ? (

No custom fields configured.

) : (
{filteredEntries.map((entry) => ( mutation.mutate([{ fieldId, value }]) } /> ))}
)}
)}
); } // ─── FieldControl ───────────────────────────────────────────────────────────── interface FieldControlProps { entry: FieldEntry; onSave: (fieldId: string, value: unknown) => void; } function FieldControl({ entry, onSave }: FieldControlProps) { const { definition, value: savedValue } = entry; const initialValue = savedValue?.value ?? null; // Debounce timer ref const timer = useRef | null>(null); function scheduleBlurSave(fieldId: string, val: unknown) { // Immediate debounce cancel then save after 500ms idle if (timer.current) clearTimeout(timer.current); timer.current = setTimeout(() => { onSave(fieldId, val); }, 500); } const label = ( ); if (definition.fieldType === 'boolean') { return ( ); } if (definition.fieldType === 'select') { return ( ); } // text / number / date return ( ); } // ─── Sub-controls ────────────────────────────────────────────────────────────── function TextLikeField({ definition, initialValue, label, onScheduleSave, }: { definition: CustomFieldDefinition; initialValue: unknown; label: React.ReactNode; onScheduleSave: (fieldId: string, val: unknown) => void; }) { const [localValue, setLocalValue] = useState( initialValue !== null && initialValue !== undefined ? String(initialValue) : '', ); const inputType = definition.fieldType === 'number' ? 'number' : definition.fieldType === 'date' ? 'date' : 'text'; function handleChange(e: React.ChangeEvent) { const raw = e.target.value; setLocalValue(raw); let parsed: unknown = raw; if (definition.fieldType === 'number') { parsed = raw === '' ? null : parseFloat(raw); } else if (raw === '') { parsed = null; } onScheduleSave(definition.id, parsed); } return (
{label}
); } function BooleanField({ definition, initialValue, label, onSave, }: { definition: CustomFieldDefinition; initialValue: boolean | null; label: React.ReactNode; onSave: (fieldId: string, val: unknown) => void; }) { const [checked, setChecked] = useState(initialValue ?? false); function handleChange(val: boolean) { setChecked(val); onSave(definition.id, val); } return (
{label}
); } function SelectField({ definition, initialValue, label, onSave, }: { definition: CustomFieldDefinition; initialValue: string | null; label: React.ReactNode; onSave: (fieldId: string, val: unknown) => void; }) { const options = definition.selectOptions ?? []; const [selected, setSelected] = useState(initialValue ?? ''); function handleChange(val: string) { setSelected(val); onSave(definition.id, val === '__none__' ? null : val); } return (
{label}
); }