'use client'; import { useState, useCallback, useEffect } from 'react'; import { type ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2, Plus } from 'lucide-react'; import { DataTable } from '@/components/shared/data-table'; import { PageHeader } from '@/components/shared/page-header'; import { ConfirmationDialog } from '@/components/shared/confirmation-dialog'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { apiFetch } from '@/lib/api/client'; import { CustomFieldForm, type CustomFieldDefinition } from './custom-field-form'; // ─── Types ──────────────────────────────────────────────────────────────────── type EntityTab = 'client' | 'interest' | 'berth'; const TAB_LABELS: Record = { client: 'Clients', interest: 'Interests', berth: 'Berths', }; const FIELD_TYPE_LABELS: Record = { text: 'Text', number: 'Number', date: 'Date', boolean: 'Yes / No', select: 'Dropdown', }; // ─── Component ──────────────────────────────────────────────────────────────── export function CustomFieldsManager() { const [activeTab, setActiveTab] = useState('client'); const [fields, setFields] = useState([]); const [loading, setLoading] = useState(true); const [formOpen, setFormOpen] = useState(false); const [editingField, setEditingField] = useState(null); const [deletingId, setDeletingId] = useState(null); const fetchFields = useCallback(async () => { setLoading(true); try { const res = await apiFetch<{ data: CustomFieldDefinition[] }>('/api/v1/admin/custom-fields'); setFields(res.data); } finally { setLoading(false); } }, []); useEffect(() => { void fetchFields(); }, [fetchFields]); const filteredFields = fields.filter((f) => f.entityType === activeTab); function handleCreate() { setEditingField(null); setFormOpen(true); } function handleEdit(field: CustomFieldDefinition) { setEditingField(field); setFormOpen(true); } async function handleDelete(id: string) { setDeletingId(id); try { await apiFetch(`/api/v1/admin/custom-fields/${id}`, { method: 'DELETE' }); await fetchFields(); } finally { setDeletingId(null); } } function getDeleteDescription(field: CustomFieldDefinition): string { return `Are you sure you want to delete "${field.fieldLabel}" (${field.fieldName})? All stored values for this field will also be permanently deleted.`; } const columns: ColumnDef[] = [ { accessorKey: 'fieldName', header: 'Name', cell: ({ row }) => {row.original.fieldName}, }, { accessorKey: 'fieldLabel', header: 'Label', cell: ({ row }) => {row.original.fieldLabel}, }, { accessorKey: 'fieldType', header: 'Type', cell: ({ row }) => ( {FIELD_TYPE_LABELS[row.original.fieldType] ?? row.original.fieldType} ), }, { accessorKey: 'isRequired', header: 'Required', cell: ({ row }) => row.original.isRequired ? ( Required ) : ( Optional ), }, { accessorKey: 'sortOrder', header: 'Order', cell: ({ row }) => ( {row.original.sortOrder} ), }, { id: 'actions', header: '', cell: ({ row }) => (
Delete } title="Delete Custom Field" description={getDeleteDescription(row.original)} confirmLabel="Delete Field" onConfirm={() => handleDelete(row.original.id)} loading={deletingId === row.original.id} />
), enableSorting: false, size: 80, }, ]; return (
New Field } />
Heads up: custom fields render in detail-page sidebars and the entity export, and merge-tokens of the form{' '} {`{{custom.fieldName}}`} now expand in EOI/contract/email templates for client/interest/berth contexts. They still don’t plug into the global search index, the berth recommender, or the entity-diff audit log — use them for rep-only annotations and template-merge values, but anything load-bearing for the deal flow still needs a first-class column.
setActiveTab(v as EntityTab)}> {(Object.keys(TAB_LABELS) as EntityTab[]).map((tab) => ( {TAB_LABELS[tab]} ))} {(Object.keys(TAB_LABELS) as EntityTab[]).map((tab) => ( row.id} emptyState={

No custom fields for {TAB_LABELS[tab]} yet.

} />
))}
); }