// Panel 3: workflow timeline + transition buttons + approval history + changelog. // Pulls nextPhases từ BE bundle (single source of truth) → render per-phase // action button. Approvals + History moved here from PeDetailTabs (2 section // dưới cùng) để Panel 2 tập trung hiển thị nội dung phiếu (Info + NCC + Items). import { useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { Dialog } from '@/components/ui/Dialog' import { Button } from '@/components/ui/Button' import { Label } from '@/components/ui/Label' import { Textarea } from '@/components/ui/Textarea' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { cn } from '@/lib/cn' import { ApprovalStage, PurchaseEvaluationPhase, PurchaseEvaluationPhaseColor, PurchaseEvaluationPhaseLabel, type PeDepartmentApproval, type PeDetailBundle, } from '@/types/purchaseEvaluation' import { PeApprovalsSection, PeHistorySection } from './PeDetailTabs' export function PeWorkflowPanel({ evaluation, readOnly = false, }: { evaluation: PeDetailBundle /** true = ẩn Chuyển tiếp + Dialog transition (dùng cho Danh sách, không dùng Duyệt). */ readOnly?: boolean }) { const [target, setTarget] = useState(null) const [comment, setComment] = useState('') const qc = useQueryClient() // 2-stage dept approvals (Migration 16) — fetch riêng để FE Workflow Panel // hiển thị progress per phase × dept (Stage Review NV / Confirm TPB). const { data: deptApprovals = [] } = useQuery({ queryKey: ['pe-dept-approvals', evaluation.id], queryFn: async () => { const r = await api.get(`/purchase-evaluations/${evaluation.id}/department-approvals`) return r.data }, }) const transition = useMutation({ mutationFn: async () => { // Decision = Reject (2) khi: // - target = TuChoi (huỷ phiếu) // - target = DangSoanThao từ phase trung gian (= Trả lại — smart reject Mig 16 // set RejectedFromPhase + clear N-stage rows + Drafter resume jump-back) const isReject = target === PurchaseEvaluationPhase.TuChoi || (target === PurchaseEvaluationPhase.DangSoanThao && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao) return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, { targetPhase: target, decision: isReject ? 2 : 1, comment: comment || null, }) }, onSuccess: () => { toast.success('Đã chuyển phase.') qc.invalidateQueries({ queryKey: ['pe-detail', evaluation.id] }) qc.invalidateQueries({ queryKey: ['pe-list'] }) qc.invalidateQueries({ queryKey: ['pe-dept-approvals', evaluation.id] }) setTarget(null) setComment('') }, onError: e => toast.error(getErrorMessage(e)), }) const next = evaluation.workflow.nextPhases return (

Quy trình

{evaluation.workflow.policyDescription}

    {evaluation.workflow.activePhases .filter(p => p !== PurchaseEvaluationPhase.TuChoi) .map(p => { const isCurrent = evaluation.phase === p const isPast = isPastPhase(evaluation.phase, p, evaluation.workflow.activePhases) return (
  1. {p} {PurchaseEvaluationPhaseLabel[p]} {isCurrent && ● hiện tại} {isPast && }
  2. ) })}
{next.length > 0 && !readOnly && (
{next.map(p => { // Phân loại button theo hành động: // - Trả lại = về DangSoanThao (từ phase trung gian) — red // - Hủy/Từ chối = TuChoi (chỉ ở phase DangSoanThao đầu) — red // - Duyệt = forward phase tiếp theo — brand const isSendBack = p === PurchaseEvaluationPhase.DangSoanThao && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao const isCancel = p === PurchaseEvaluationPhase.TuChoi const isDanger = isSendBack || isCancel const label = isSendBack ? '← Trả lại (về Drafter sửa)' : isCancel ? '✗ Hủy / Từ chối' : `✓ Duyệt → ${PurchaseEvaluationPhaseLabel[p]}` return ( ) })}
)} {readOnly && next.length > 0 && (
Vào menu “Duyệt” để chuyển phase.
)} {target !== null && (() => { const isCancel = target === PurchaseEvaluationPhase.TuChoi const isSendBack = target === PurchaseEvaluationPhase.DangSoanThao && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao const dialogTitle = isCancel ? '✗ Từ chối phiếu (khoá hoàn toàn)' : isSendBack ? '← Trả lại Drafter sửa' : `✓ Duyệt → ${PurchaseEvaluationPhaseLabel[target]}` return ( setTarget(null)} title={dialogTitle} footer={<> } > {isCancel && (
⚠ Phiếu sẽ bị khoá hoàn toàn (không edit/transition được nữa). Drafter cần tạo phiếu mới nếu muốn làm lại.
)} {isSendBack && (
Phiếu sẽ về “Đang soạn thảo”. Drafter có thể sửa rồi trình lại — workflow tự jump tới phase này.
)}