From e45909712bdf4d5a6cf7e8d788ebccd3a95cc15a Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 21 Apr 2026 21:35:05 +0700 Subject: [PATCH] [CLAUDE] App+Infra+FE-Admin: DynamicForm + .doc/.xls auto-convert on upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tier 3 iter2 — form builder UI dùng FieldSpec thay raw JSON textarea. FE: - DynamicForm component — parse FieldSpec JSON (record of FieldDef với label/type/required/placeholder/hint/options) và render inputs dynamic theo type: text/textarea/number/date/currency/select. - FormsPage render dialog thêm toggle Form ↔ JSON (segmented control). Mặc định Form mode khi template có FieldSpec, JSON mode khi không. Khi mở dialog cho row khác, reset formValues + chọn đúng default mode. - parseFieldSpec helper trả { spec, error } — UI báo lỗi nếu JSON không parse được, fallback JSON textarea. BE — generalize converter thành IDocumentConverter: - IPdfConverter → IDocumentConverter (ConvertAsync(bytes, src, tgt, ct)) — đủ gánh cả pdf, docx, xlsx targets. - LibreOfficeDocumentConverter — 1 shell-out pattern cho mọi conversion (docx→pdf, doc→docx, xls→xlsx, xlsx→pdf), target arg truyền vào --convert-to. - ExportTemplatePdfCommand update dùng "pdf" target. Auto-convert .doc/.xls trên upload: - Validator accept thêm .doc/.xls (thêm note "sẽ tự convert"). - UploadContractTemplateCommandHandler: nếu ext là doc/xls → read stream → converter.ConvertAsync → lưu file .docx/.xlsx thay vì format gốc. File rendering pipeline (DocxRenderer/XlsxRenderer) chỉ support docx/ xlsx — convert đảm bảo consistent. - Display FileName preserve original name nhưng đổi extension. Unblock 3 file .doc legacy template — admin giờ upload .doc bình thường, system tự convert. Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/components/DynamicForm.tsx | 150 ++++++++++++++++++ fe-admin/src/pages/forms/FormsPage.tsx | 94 ++++++++--- .../Common/Interfaces/IPdfConverter.cs | 15 +- .../Forms/FormFeatures.cs | 54 +++++-- .../DependencyInjection.cs | 2 +- .../Forms/LibreOfficePdfConverter.cs | 41 ++--- 6 files changed, 300 insertions(+), 56 deletions(-) create mode 100644 fe-admin/src/components/DynamicForm.tsx 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' && ( +