// Panel 3 cho 3-panel layout — gom WorkflowSummaryCard (timeline policy) + // approval history (ai/khi/quyết định gì) + ContractChangelogsTab (mọi thay // đổi header/detail/comment/attachment/transition) vào 1 stack. Dùng cho cả // MyContracts 3-panel và ContractDetailPage fullpage. import { ArrowRight, Clock, History, Users2 } from 'lucide-react' import { useQuery } from '@tanstack/react-query' import { PhaseBadge } from '@/components/PhaseBadge' import { WorkflowSummaryCard } from '@/components/WorkflowSummaryCard' import { ContractChangelogsTab } from '@/components/contracts/ContractChangelogsTab' import { api } from '@/lib/api' import { cn } from '@/lib/cn' import { ApprovalStage, ContractPhaseColor, ContractPhaseLabel, type ContractDepartmentApproval, type ContractDetail, } from '@/types/contracts' const fmt = (s: string) => new Date(s).toLocaleString('vi-VN') const fmtTime = (iso: string) => new Date(iso).toLocaleString('vi-VN', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) export function WorkflowHistoryPanel({ contract: c }: { contract: ContractDetail }) { const { data: deptApprovals = [] } = useQuery({ queryKey: ['contract-dept-approvals', c.id], queryFn: async () => (await api.get(`/contracts/${c.id}/department-approvals`)).data, }) return (
{c.workflow && } {deptApprovals.length > 0 && ( )}

Lịch sử duyệt ({c.approvals.length})

    {c.approvals.length === 0 &&
  1. Chưa có lịch sử.
  2. } {c.approvals.map(a => (
  3. {a.approverName ?? 'Hệ thống'}
    {fmt(a.approvedAt)}
    {a.comment && (
    {a.comment}
    )}
  4. ))}

Lịch sử điều chỉnh

) } // 2-stage dept approval timeline (Migration 16). Group by phase × dept. Same // pattern PE — block phase transition khi current phase có Review nhưng chưa // Confirm → highlight amber để user biết "đang chờ TPB confirm". function DeptApprovalsSection({ rows, currentPhase, }: { rows: ContractDepartmentApproval[] currentPhase: number }) { const grouped = new Map>() for (const r of rows) { if (!grouped.has(r.phaseAtApproval)) grouped.set(r.phaseAtApproval, new Map()) const byDept = grouped.get(r.phaseAtApproval)! if (!byDept.has(r.departmentId)) byDept.set(r.departmentId, []) byDept.get(r.departmentId)!.push(r) } const phaseOrder = [...grouped.keys()].sort((a, b) => a - b) return (

Tiến trình duyệt 2-cấp phòng ban

NV Review → TPB Confirm. Phase chỉ chuyển khi có Confirm.

{phaseOrder.map(phase => { const byDept = grouped.get(phase)! return (
{ContractPhaseLabel[phase] ?? `Phase ${phase}`}
{[...byDept.entries()].map(([deptId, stages]) => { const review = stages.find(s => s.stage === ApprovalStage.Review) const confirm = stages.find(s => s.stage === ApprovalStage.Confirm) const deptName = stages[0]?.departmentName ?? '(không rõ phòng)' const isPending = phase === currentPhase && review && !confirm return (
{deptName}
Review: {review ? <>✓ {review.approverName} — {fmtTime(review.approvedAt)}{review.comment && · "{review.comment}"} : '— chưa có'} Confirm: {confirm ? <>✓ {confirm.approverName}{confirm.isBypassed && bypass} — {fmtTime(confirm.approvedAt)}{confirm.comment && · "{confirm.comment}"} : '⏳ chờ TPB confirm'}
) })}
) })}
) }