import React, { useState, useEffect } from 'react' import api from '../api.js' import { Plus, Trash2, Edit2, X, Check, GripVertical, Type, Hash, Phone, Mail, AlertTriangle, Columns } from 'lucide-react' const TYPE_OPTIONS = [ { value: 'text', label: 'Text', icon: Type }, { value: 'number', label: 'Number', icon: Hash }, { value: 'phone', label: 'Phone', icon: Phone }, { value: 'email', label: 'Email', icon: Mail }, ] const TypeIcon = ({ type, size = 14 }) => { const opt = TYPE_OPTIONS.find(t => t.value === type) const Icon = opt?.icon || Type return } function FieldModal({ field, onClose, onSave }) { const [form, setForm] = useState({ label: field?.label || '', field_type: field?.field_type || 'text', required: field?.required || false, }) const [error, setError] = useState('') const [loading, setLoading] = useState(false) const handleSubmit = async (e) => { e.preventDefault() setError('') if (!form.label.trim()) return setError('Label is required') setLoading(true) try { if (field) { const { data } = await api.put(`/fields/${field.id}`, form) onSave(data, 'edit') } else { const { data } = await api.post('/fields', form) onSave(data, 'add') } onClose() } catch (err) { setError(err.response?.data?.error || 'Failed to save field') } finally { setLoading(false) } } return (

{field ? 'Edit Field' : 'New Field'}

{error &&
{error}
}
setForm({ ...form, label: e.target.value })} autoFocus className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition" placeholder="e.g. First Name, Contract ID..." /> {!field && form.label && (

Key: {form.label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '')}

)}
{TYPE_OPTIONS.map(({ value, label, icon: Icon }) => ( ))}
) } export default function Fields() { const [fields, setFields] = useState([]) const [loading, setLoading] = useState(true) const [modal, setModal] = useState(null) // null | 'add' | field object const [deleteConfirm, setDeleteConfirm] = useState(null) const [toast, setToast] = useState('') const [dragIdx, setDragIdx] = useState(null) const [dragOverIdx, setDragOverIdx] = useState(null) useEffect(() => { api.get('/fields').then(({ data }) => { setFields(data); setLoading(false) }).catch(() => setLoading(false)) }, []) const showToast = (msg) => { setToast(msg); setTimeout(() => setToast(''), 3000) } const handleSave = (data, type) => { if (type === 'add') setFields(prev => [...prev, data]) else setFields(prev => prev.map(f => f.id === data.id ? data : f)) showToast(type === 'add' ? 'Field added' : 'Field updated') } const handleDelete = async (field) => { try { await api.delete(`/fields/${field.id}`) setFields(prev => prev.filter(f => f.id !== field.id)) setDeleteConfirm(null) showToast('Field deleted') } catch (err) { showToast(err.response?.data?.error || 'Failed to delete') } } // Drag-to-reorder const handleDragStart = (idx) => setDragIdx(idx) const handleDragOver = (e, idx) => { e.preventDefault(); setDragOverIdx(idx) } const handleDrop = async (idx) => { if (dragIdx === null || dragIdx === idx) { setDragIdx(null); setDragOverIdx(null); return } const reordered = [...fields] const [moved] = reordered.splice(dragIdx, 1) reordered.splice(idx, 0, moved) const withOrder = reordered.map((f, i) => ({ ...f, sort_order: i + 1 })) setFields(withOrder) setDragIdx(null) setDragOverIdx(null) try { await api.put('/fields/reorder', { order: withOrder.map(f => ({ id: f.id, sort_order: f.sort_order })) }) } catch { showToast('Failed to save order') } } return (
{toast && (
{toast}
)}

Fields

Manage the columns shown in the Dashboard

{/* Info banner */}

Fields define the columns in the Dashboard. You can drag rows to reorder them. Deleting a field removes its data from all existing contracts.

{loading ? (
Loading...
) : fields.length === 0 ? (

No fields defined yet

) : ( {fields.map((f, idx) => ( handleDragStart(idx)} onDragOver={(e) => handleDragOver(e, idx)} onDrop={() => handleDrop(idx)} onDragEnd={() => { setDragIdx(null); setDragOverIdx(null) }} className={`border-b border-slate-800/50 transition ${ dragOverIdx === idx && dragIdx !== idx ? 'bg-indigo-500/10' : 'hover:bg-slate-800/30' } ${dragIdx === idx ? 'opacity-40' : ''}`} > ))}
Label Key Type Required Actions
{f.label} {f.field_key}
{f.field_type}
{f.required ? Required : Optional }
)}
{modal && ( setModal(null)} onSave={handleSave} /> )} {deleteConfirm && (

Delete Field

Delete "{deleteConfirm.label}"? This will permanently remove this column and all its data from every contract.

)}
) }