import { useRef, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { FileDown, Pencil, Plus, Power, Trash2 } from 'lucide-react'; import { useAuth, Card, CardContent, CardHeader, CardTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Button, buttonVariants, Input, Label, Textarea, Badge, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, cn, formatIsoToDdMmYyyyHhMm, detailFromApiError, listTemplates, createTemplate, updateTemplate, deleteTemplate, downloadTemplateFile, saveArrayBufferAs, TEMPLATE_DOCX_MIME, type DocumentTemplate, } from '@ump/shared'; import { toast } from 'sonner'; export function TemplatesPage() { const { hasRole } = useAuth(); const qc = useQueryClient(); const isAdmin = hasRole('admin'); const templatesQuery = useQuery({ queryKey: ['templates'], queryFn: listTemplates, enabled: isAdmin, }); const templates = templatesQuery.data ?? []; const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const invalidate = () => qc.invalidateQueries({ queryKey: ['templates'] }); const createMut = useMutation({ mutationFn: createTemplate, onSuccess: (t) => { toast.success(`Đã tạo mẫu "${t.name}" — ${t.fields.length} trường.`); setCreateOpen(false); void invalidate(); }, onError: (e) => toast.error(detailFromApiError(e, 'Không tạo được mẫu.')), }); const updateMut = useMutation({ mutationFn: (v: { id: string; patch: { name?: string; description?: string; isActive?: boolean } }) => updateTemplate(v.id, v.patch), onSuccess: () => { setEditing(null); void invalidate(); }, onError: (e) => toast.error(detailFromApiError(e, 'Không cập nhật được mẫu.')), }); const deleteMut = useMutation({ mutationFn: (id: string) => deleteTemplate(id, { hard: true }), onSuccess: () => { toast.success('Đã xóa mẫu.'); setDeleteTarget(null); void invalidate(); }, onError: (e) => toast.error(detailFromApiError(e, 'Không xóa được mẫu.')), }); const handleDownload = async (t: DocumentTemplate) => { try { const buf = await downloadTemplateFile(t.id); saveArrayBufferAs(buf, t.originalFilename || `${t.name}.docx`, TEMPLATE_DOCX_MIME); } catch (e) { toast.error(detailFromApiError(e, 'Không tải được tệp mẫu.')); } }; if (!isAdmin) { return (

Chỉ tài khoản quản trị mới quản lý được mẫu tài liệu.

); } return (

Mẫu tài liệu

Tải lên mẫu .docx (Word) với các trường {'{{ ten_truong }}'}. Người nộp đơn sẽ điền theo từng mẫu.

Danh sách mẫu
STT Tên mẫu Mô tả Số trường Trạng thái Cập nhật Thao tác {templatesQuery.isLoading ? ( Đang tải… ) : templatesQuery.isError ? ( Không tải được danh sách mẫu. ) : templates.length === 0 ? ( Chưa có mẫu nào. Bấm « Tạo mẫu mới » để tải lên. ) : ( templates.map((t, i) => ( {i + 1} {t.name} {t.description || '—'} {t.fields.length} {t.isActive ? ( Đang dùng ) : ( Ẩn )} {t.updatedAt ? formatIsoToDdMmYyyyHhMm(t.updatedAt) : '—'}
)) )}
createMut.mutate(v)} /> !o && setEditing(null)} onSubmit={(patch) => editing && updateMut.mutate({ id: editing.id, patch })} /> !o && setDeleteTarget(null)}> Xóa mẫu vĩnh viễn? {deleteTarget ? ( <> Xóa hẳn mẫu "{deleteTarget.name}" và tệp .docx khỏi kho. Thao tác này không thể hoàn tác. Để tạm ẩn thay vì xóa, hãy dùng nút vô hiệu hóa. ) : null} Hủy deleteTarget && deleteMut.mutate(deleteTarget.id)} > Xóa vĩnh viễn
); } function CreateTemplateDialog(props: { open: boolean; submitting: boolean; onOpenChange: (open: boolean) => void; onSubmit: (v: { name: string; description?: string; file: File }) => void; }) { const { open, submitting, onOpenChange, onSubmit } = props; const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [file, setFile] = useState(null); const fileRef = useRef(null); const reset = () => { setName(''); setDescription(''); setFile(null); if (fileRef.current) fileRef.current.value = ''; }; const submit = () => { if (!name.trim()) return toast.error('Nhập tên mẫu.'); if (!file) return toast.error('Chọn tệp .docx.'); if (!file.name.toLowerCase().endsWith('.docx')) return toast.error('Chỉ chấp nhận tệp .docx (Word).'); onSubmit({ name: name.trim(), description: description.trim() || undefined, file }); }; return ( { if (!o) reset(); onOpenChange(o); }} > Tạo mẫu tài liệu mới
setName(e.target.value)} placeholder="VD: Đơn xin nghỉ phép" disabled={submitting} />