diff --git a/fe-admin/src/components/DynamicForm.tsx b/fe-admin/src/components/DynamicForm.tsx new file mode 100644 index 0000000..aed9b79 --- /dev/null +++ b/fe-admin/src/components/DynamicForm.tsx @@ -0,0 +1,150 @@ +import { useMemo } from 'react' +import { AlertTriangle } from 'lucide-react' +import { Input } from '@/components/ui/Input' +import { Label } from '@/components/ui/Label' +import { Select } from '@/components/ui/Select' +import { Textarea } from '@/components/ui/Textarea' + +// FieldSpec schema — stored on ContractTemplate.FieldSpec as JSON string. +// Designed to be human-authorable by admin in JSON textarea but also machine- +// readable for the DynamicForm renderer below. +// +// { +// "fieldKey": { +// "label": "Tên Cty bên A", +// "type": "text" | "textarea" | "number" | "date" | "currency" | "select", +// "required": false, +// "placeholder": "...", +// "hint": "...", +// "options": ["opt1", "opt2"] // only for type=select +// } +// } + +export type FieldDef = { + label: string + type?: 'text' | 'textarea' | 'number' | 'date' | 'currency' | 'select' + required?: boolean + placeholder?: string + hint?: string + options?: string[] +} + +export type FieldSpec = Record + +export type DynamicFormValues = Record + +export function parseFieldSpec(json: string | null | undefined): { spec: FieldSpec | null; error: string | null } { + if (!json || !json.trim()) return { spec: null, error: null } + try { + const obj = JSON.parse(json) + if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) + return { spec: null, error: 'FieldSpec phải là object JSON' } + return { spec: obj as FieldSpec, error: null } + } catch (e) { + return { spec: null, error: `JSON lỗi: ${(e as Error).message}` } + } +} + +export function DynamicForm({ + spec, + values, + onChange, +}: { + spec: FieldSpec + values: DynamicFormValues + onChange: (key: string, value: string | null) => void +}) { + const entries = useMemo(() => Object.entries(spec), [spec]) + + if (entries.length === 0) { + return ( +
+ FieldSpec trống — thêm field vào spec JSON để hiện form. +
+ ) + } + + return ( +
+ {entries.map(([key, def]) => { + const type = def.type ?? 'text' + const val = values[key] ?? '' + const commonProps = { + id: key, + value: val, + placeholder: def.placeholder, + required: def.required, + } + const spanClass = type === 'textarea' ? 'md:col-span-2' : '' + + return ( +
+ + {type === 'textarea' && ( +