import { useRef, useState, type FormEvent } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Download, FileSpreadsheet, FileText, CheckCircle2, XCircle, Upload, Pencil, Trash2, Plus, } from 'lucide-react' import { toast } from 'sonner' import { PageHeader } from '@/components/PageHeader' import { DataTable, type Column } from '@/components/DataTable' import { Button } from '@/components/ui/Button' import { Dialog } from '@/components/ui/Dialog' import { Input } from '@/components/ui/Input' import { Label } from '@/components/ui/Label' import { Select } from '@/components/ui/Select' import { Textarea } from '@/components/ui/Textarea' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { type ContractTemplate, ContractTypeLabel } from '@/types/forms' type EditState = ContractTemplate & { __new?: boolean } const EMPTY_EDIT: EditState = { id: '', formCode: '', name: '', contractType: null, fileName: '', format: 'docx', fieldSpec: null, description: null, isActive: true, __new: true, } export function FormsPage() { const qc = useQueryClient() const [renderDialog, setRenderDialog] = useState(null) const [edit, setEdit] = useState(null) const [uploadFile, setUploadFile] = useState(null) const fileInputRef = useRef(null) const [dataJson, setDataJson] = useState(`{ "benA_tenCongTy": "Công ty TNHH Xây dựng Solutions", "giaTri": "150,000,000 VND", "ngayKy": "21/04/2026" }`) const list = useQuery({ queryKey: ['contract-templates'], queryFn: async () => (await api.get('/forms/templates', { params: { onlyActive: false } })).data, }) const render = useMutation({ mutationFn: async ({ id, data }: { id: string; data: Record }) => { const res = await api.post(`/forms/templates/${id}/render`, data, { responseType: 'blob' }) return { blob: res.data as Blob, filename: res.headers['content-disposition']?.match(/filename="?([^";]+)"?/)?.[1] ?? 'render.docx', } }, onSuccess: ({ blob, filename }) => { const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename a.click() URL.revokeObjectURL(url) toast.success('Đã tải file render') setRenderDialog(null) }, onError: err => toast.error(getErrorMessage(err)), }) const upload = useMutation({ mutationFn: async (params: { file: File; meta: EditState }) => { const form = new FormData() form.append('file', params.file) form.append('formCode', params.meta.formCode) form.append('name', params.meta.name) if (params.meta.contractType !== null) form.append('contractType', String(params.meta.contractType)) if (params.meta.description) form.append('description', params.meta.description) if (params.meta.fieldSpec) form.append('fieldSpec', params.meta.fieldSpec) await api.post('/forms/templates', form, { headers: { 'Content-Type': 'multipart/form-data' } }) }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['contract-templates'] }) toast.success('Đã upload template mới') setEdit(null) setUploadFile(null) }, onError: err => toast.error(getErrorMessage(err)), }) const update = useMutation({ mutationFn: async (meta: EditState) => api.put(`/forms/templates/${meta.id}`, { name: meta.name, contractType: meta.contractType, description: meta.description, fieldSpec: meta.fieldSpec, isActive: meta.isActive, }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['contract-templates'] }) toast.success('Đã cập nhật template') setEdit(null) }, onError: err => toast.error(getErrorMessage(err)), }) const del = useMutation({ mutationFn: async (id: string) => api.delete(`/forms/templates/${id}`), onSuccess: () => { qc.invalidateQueries({ queryKey: ['contract-templates'] }) toast.success('Đã vô hiệu hóa template') }, onError: err => toast.error(getErrorMessage(err)), }) function handleRender() { if (!renderDialog) return let data: Record try { data = JSON.parse(dataJson) } catch { toast.error('JSON không hợp lệ') return } render.mutate({ id: renderDialog.id, data }) } function handleSaveEdit(e: FormEvent) { e.preventDefault() if (!edit) return if (edit.__new) { if (!uploadFile) { toast.error('Chưa chọn file template .docx/.xlsx') return } upload.mutate({ file: uploadFile, meta: edit }) } else { update.mutate(edit) } } const columns: Column[] = [ { key: 'format', header: '', width: 'w-12', align: 'center', render: t => t.format === 'xlsx' ? ( ) : ( ), }, { key: 'formCode', header: 'Form Code', render: t => {t.formCode}, width: 'w-40' }, { key: 'name', header: 'Tên', render: t => t.name }, { key: 'contractType', header: 'Loại HĐ', render: t => (t.contractType ? ContractTypeLabel[t.contractType] : '—'), width: 'w-40', }, { key: 'isActive', header: 'Trạng thái', width: 'w-28', align: 'center', render: t => t.isActive ? ( ) : ( Tắt ), }, { key: 'actions', header: '', align: 'right', width: 'w-56', render: t => (
{t.isActive && ( )}
), }, ] return (
setEdit(EMPTY_EDIT)}> Upload template } /> t.id} isLoading={list.isLoading} /> {/* Render dialog */} setRenderDialog(null)} title={renderDialog ? `Render: ${renderDialog.name}` : ''} size="lg" footer={ <> } >
Hướng dẫn: Template chứa placeholder dạng{' '} {'{{fieldName}}'}. Điền key-value JSON dưới đây, backend sẽ replace placeholder trong file gốc.