import { useMemo, useState, type FormEvent } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { GitBranch, Plus, Trash2, CheckCircle2, Info, History } from 'lucide-react' import { toast } from 'sonner' import { PageHeader } from '@/components/PageHeader' 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 { AVAILABLE_ROLES, RoleLabel } from '@/types/users' // ===== Types ===== type ApproverDto = { kind: number; assignmentValue: string; displayName: string | null } type StepDto = { id: string; order: number; phase: number; phaseLabel: string; name: string; slaDays: number | null; approvers: ApproverDto[] } type DefinitionDto = { id: string code: string version: number contractType: number contractTypeLabel: string name: string description: string | null isActive: boolean activatedAt: string | null createdAt: string contractsUsingCount: number steps: StepDto[] } type TypeSummaryDto = { contractType: number contractTypeLabel: string active: DefinitionDto | null history: DefinitionDto[] } const PHASE_OPTIONS: { value: number; label: string }[] = [ { value: 2, label: 'Đang soạn thảo' }, { value: 3, label: 'Đang góp ý' }, { value: 4, label: 'Đang đàm phán' }, { value: 5, label: 'Đang in ký' }, { value: 6, label: 'CCM kiểm tra' }, { value: 7, label: 'Đang trình ký' }, { value: 8, label: 'Đang đóng dấu' }, { value: 9, label: 'Đã phát hành' }, ] type EditStepApprover = { kind: 1 | 2; assignmentValue: string } type EditStep = { phase: number; name: string; slaDays: number | null; approvers: EditStepApprover[] } function copyFromDefinition(d: DefinitionDto): EditStep[] { return d.steps.map(s => ({ phase: s.phase, name: s.name, slaDays: s.slaDays, approvers: s.approvers.map(a => ({ kind: a.kind as 1 | 2, assignmentValue: a.assignmentValue })), })) } // ===== Page ===== export function WorkflowsPage() { const qc = useQueryClient() const overview = useQuery({ queryKey: ['workflow-overview'], queryFn: async () => (await api.get<{ types: TypeSummaryDto[] }>('/workflows')).data, }) const [activeType, setActiveType] = useState(null) const tab = activeType ?? overview.data?.types[0]?.contractType ?? 1 const currentType = overview.data?.types.find(t => t.contractType === tab) return (
Quy trình duyệt hợp đồng } description="Mỗi loại HĐ có quy trình riêng, hỗ trợ versioning. Tạo version mới → HĐ tương lai chạy theo. HĐ cũ vẫn giữ quy trình cũ." /> {/* Tabs */} {overview.data && (
{overview.data.types.map(t => ( ))}
)} {overview.isLoading &&
Đang tải…
} {currentType && qc.invalidateQueries({ queryKey: ['workflow-overview'] })} />}
) } // ===== Per-type panel ===== function TypePanel({ type, onSaved }: { type: TypeSummaryDto; onSaved: () => void }) { const [designerOpen, setDesignerOpen] = useState(false) const [cloneFrom, setCloneFrom] = useState(null) return (
{type.active ? ( { setCloneFrom(d); setDesignerOpen(true) }} /> ) : (
Chưa có quy trình cho loại này. Tạo version đầu tiên bên dưới.
)}

Lịch sử versions

{type.history.filter(d => !d.isActive).length === 0 && (
Chưa có version cũ. Khi tạo version mới, version hiện tại tự động archive.
)}
{type.history .filter(d => !d.isActive) .map(d => ( { setCloneFrom(dd); setDesignerOpen(true) }} /> ))}
{designerOpen && ( { setDesignerOpen(false); setCloneFrom(null) }} onSaved={() => { setDesignerOpen(false); setCloneFrom(null); onSaved() }} /> )}
) } // ===== Definition card (read-only view) ===== function DefinitionCard({ def, isActive, onClone }: { def: DefinitionDto; isActive: boolean; onClone: (d: DefinitionDto) => void }) { return (

{def.name}

{def.code} v{String(def.version).padStart(2, '0')} {isActive ? ( Đang áp dụng ) : ( Archived · {def.contractsUsingCount} HĐ còn chạy )}
{def.description &&

{def.description}

}
    {def.steps.map(s => (
  1. {s.order}
    {s.name} ({s.phaseLabel}) {s.slaDays != null && ( SLA {s.slaDays}d )}
    {s.approvers.length === 0 && ( Chưa có người duyệt )} {s.approvers.map((a, i) => ( {a.kind === 1 ? 'Role' : 'User'}: {a.displayName ?? a.assignmentValue} ))}
  2. ))}
) } // ===== Designer dialog ===== function WorkflowDesigner({ contractType, contractTypeLabel, cloneFrom, onClose, onSaved, }: { contractType: number contractTypeLabel: string cloneFrom: DefinitionDto | null onClose: () => void onSaved: () => void }) { const initialSteps: EditStep[] = useMemo( () => cloneFrom ? copyFromDefinition(cloneFrom) : [{ phase: 2, name: 'Soạn thảo', slaDays: 7, approvers: [] }], [cloneFrom], ) const [code, setCode] = useState(cloneFrom?.code ?? 'QT-NEW') const [name, setName] = useState(cloneFrom ? `${cloneFrom.name} (clone)` : `Quy trình ${contractTypeLabel}`) const [description, setDescription] = useState(cloneFrom?.description ?? '') const [steps, setSteps] = useState(initialSteps) const usersList = useQuery({ queryKey: ['users-for-approver'], queryFn: async () => (await api.get<{ items: { id: string; fullName: string; email: string }[] }>('/users', { params: { page: 1, pageSize: 200 } })).data.items, }) const save = useMutation({ mutationFn: async () => { await api.post('/workflows', { contractType, code, name, description: description || null, steps: steps.map((s, i) => ({ order: i + 1, phase: s.phase, name: s.name, slaDays: s.slaDays, approvers: s.approvers, })), }) }, onSuccess: () => { toast.success('Đã lưu quy trình mới. Version cũ đã archive.') onSaved() }, onError: err => toast.error(getErrorMessage(err)), }) function submit(e: FormEvent) { e.preventDefault() if (steps.length === 0) { toast.error('Phải có ít nhất 1 bước') return } save.mutate() } return ( } >
setCode(e.target.value)} required className="font-mono" />
Ví dụ QT-TP, QT-MB. Version auto-tăng mỗi lần lưu.
setName(e.target.value)} required />