// 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 { useEffect, useRef, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { Download, Eye, Paperclip, Trash2, Upload, X } from 'lucide-react'
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 { useAuth } from '@/contexts/AuthContext'
import {
  ApprovalStage,
  PeAttachmentPurpose,
  PurchaseEvaluationPhase,
  PurchaseEvaluationPhaseColor,
  PurchaseEvaluationPhaseLabel,
  WorkflowReturnMode,
  type PeAttachment,
  type PeDepartmentApproval,
  type PeDetailBundle,
} from '@/types/purchaseEvaluation'
import { AttachmentPreviewDialog } from './AttachmentPreviewDialog'
import { PeApprovalsSection, PeHistorySection } from './PeDetailTabs'

export function PeWorkflowPanel({
  evaluation,
  readOnly = false,
  onApproved,
  onDeleted,
}: {
  evaluation: PeDetailBundle
  /** true = ẩn Chuyển tiếp + Dialog transition (dùng cho Danh sách, không dùng Duyệt). */
  readOnly?: boolean
  /** S78 — gọi sau khi DUYỆT (forward approve) thành công, để caller đóng focus
   *  overlay về danh sách (anh: "chọn duyệt thì nó đóng lại như ban đầu").
   *  KHÔNG gọi khi Trả lại / Từ chối (caller giữ overlay để xem kết quả). */
  onApproved?: () => void
  /** [S155 đợt 2] gọi sau khi XÓA MỀM phiếu đang duyệt thành công — caller đóng
   *  detail/overlay về danh sách (phiếu vừa xóa không còn đọc được: global filter
   *  `PurchaseEvaluationConfiguration.cs:84` loại nó khỏi `GET /{id}` ⇒ giữ overlay
   *  sẽ hiện "Không tìm thấy phiếu"). KHÔNG dùng lại `onApproved` vì 2 việc khác nghĩa. */
  onDeleted?: () => void
}) {
  const [target, setTarget] = useState<number | null>(null)
  const [comment, setComment] = useState('')
  // Mig 28 (S21 t4) — F1 mode Trả lại. Default Drafter (backward compat S17).
  const [returnMode, setReturnMode] = useState<WorkflowReturnMode>(WorkflowReturnMode.Drafter)
  const [returnTargetUserId, setReturnTargetUserId] = useState<string | null>(null)
  // Mig 31 (S23 t1) — F2 Approver duyệt thẳng Cấp cuối. Default false (admin opt-in
  // per slot tick → checkbox visible trong dialog Approve, default unchecked).
  const [skipToFinalApprover, setSkipToFinalApprover] = useState(false)
  // [Mig 54 2026-06-18 — anh Kiệt FDC] ③ CCM tích "Duyệt done miễn CEO" + ① người duyệt
  // cuối chọn 1 giá chốt (Ncc / ProMin / ProMax / Ccm).
  const [finalizeByCcm, setFinalizeByCcm] = useState(false)
  // [S97 2026-07-01 — anh Kiệt FDC] Cấp cấu hình KẾT THÚC (AllowApproverFinalize) = ô-tick OPT-IN:
  // default UNCHECKED → trình tiếp CEO/cấp sau; TÍCH mới KẾT THÚC tại cấp này. Đảo opt-out S96.
  const [applyLevelFinalize, setApplyLevelFinalize] = useState(false)
  const [approvedPriceSource, setApprovedPriceSource] = useState<string | null>(null)
  const qc = useQueryClient()
  const { user: currentUser } = useAuth()
  const isAdmin = currentUser?.roles?.includes('Admin') ?? false
  // [S155 đợt 2 — UAT Tra Sol 2026-07-27] XÓA MỀM phiếu khi ĐANG DUYỆT: "lỡ bấm sai
  // cái gói thầu / quay lại không được / phải xóa thì nó mới ko có lũy kế lên".
  // Dialog xác nhận RIÊNG (không trộn vào dialog transition) + ô lý do → changelog BE.
  const [deleteOpen, setDeleteOpen] = useState(false)
  const [deleteReason, setDeleteReason] = useState('')
  // S78 — người duyệt đính kèm file khi DUYỆT. File chọn (chưa upload) staged ở state,
  // upload trong transition.mutationFn TRƯỚC khi chuyển phase (file lỗi = không duyệt).
  const [approveFiles, setApproveFiles] = useState<File[]>([])
  const approveFileInputRef = useRef<HTMLInputElement>(null)
  const [previewAtt, setPreviewAtt] = useState<PeAttachment | null>(null)

  // Mig 29 (S21 t5) — F1 options per-Level (Cấp Approver hiện tại).
  const levelOptions = evaluation.currentLevelOptions

  // S23 t2 fix bro UAT: khi admin tick AllowReturnToAssignee/OneLevel/OneStep
  // (mode đang gửi duyệt) + UNTICK AllowReturnToDrafter, dialog default mode
  // phải là first F1 available — KHÔNG để Drafter (radio hidden) làm initial
  // → user click Xác nhận sai mode → BE throw "không bật mode Drafter".
  // Drafter chỉ làm fallback khi không có F1 nào enabled.
  // Per bro intent: "draft chỉ khi trả lại cho người soạn thôi" — 3 F1 modes
  // mới là "trả lại trong mode đang gửi duyệt".
  useEffect(() => {
    if (target === PurchaseEvaluationPhase.TraLai) {
      const firstAvailable = levelOptions?.allowReturnOneLevel ? WorkflowReturnMode.OneLevel
        : levelOptions?.allowReturnOneStep ? WorkflowReturnMode.OneStep
          : levelOptions?.allowReturnToAssignee ? WorkflowReturnMode.Assignee
            : WorkflowReturnMode.Drafter
      setReturnMode(firstAvailable)
    }
  }, [target, levelOptions])
  // List approvers đã ký (cho mode Assignee dropdown pick)
  const signedApprovers = (evaluation.levelOpinions ?? [])
    .map(o => ({ userId: o.approverUserId, fullName: o.approverFullName ?? 'NV' }))
    .filter((v, i, arr) => arr.findIndex(x => x.userId === v.userId) === i)

  // Mig 24 — V2 schema chỉ cho phép approver trong CurrentApproval.approvers
  // thao tác cấp hiện tại. UAT S22+1 feedback: "không quyền thao tác = ko quyền
  // mọi hành động" — disable cả 3 button (Duyệt + Trả lại + Từ chối) khi actor
  // không match currentLevel.ApproverUserId. BE mirror guard trong
  // EnsureCanRejectV2Async (defense-in-depth — UI disable + BE reject).
  // Admin bypass.
  const v2Approvers = evaluation.currentApproval?.approvers ?? []
  const actorInV2Level = isAdmin
    || (currentUser?.id && v2Approvers.some(a => a.userId === currentUser.id))
  // V2 active = phiếu pin V2 + Phase=ChoDuyet + có currentApproval
  const isV2Pending = !!evaluation.currentApproval
  const blockedByV2Level = isV2Pending && !actorInV2Level

  // [S155 đợt 2] Rào hiển thị nút XÓA — dựng để khớp 1-1 với 3 rào BE
  // (`PeSoftDeleteFeatures.cs:65 / :104 / :114`) + tầng authz độc lập (gotcha #82).
  //
  // 🔴 KHÔNG dùng `actorInV2Level` ở đây dù nó sẵn có: nó có `isAdmin ||` bypass, còn
  // đường xóa BE CỐ Ý KHÔNG bypass admin (`:100-104` — bê fallback `.First()` sang sẽ
  // biến "admin bất kỳ" thành người xóa được phiếu người khác). Tệ hơn, khi actor không
  // match thì `currentLevelOptions` BE fallback về ROW ĐẦU của Cấp
  // (`PurchaseEvaluationFeatures.cs:1084-1085`) ⇒ admin sẽ đọc cờ F6 CỦA NGƯỜI KHÁC,
  // phá đúng ràng buộc PER-NGƯỜI owner chốt. Vậy dùng tập con CHẶT dưới đây.
  const actorIsCurrentApprover = !!currentUser?.id && v2Approvers.some(a => a.userId === currentUser.id)
  // 🔴 Cờ F6 đọc từ `currentLevelOptions` — kênh DUY NHẤT per-NGƯỜI (BE resolve slot theo
  // `ApproverUserId == currentUser` rồi mới đọc cờ). So `=== true` (fail-closed) CÓ CHỦ Ý:
  // field vắng ⇒ ẩn nút, KHÔNG lòi nút xoá-chứng-từ-tài-chính rồi ăn 403 hàng loạt.
  const canDeleteWhileApproving =
    !readOnly
    && evaluation.phase === PurchaseEvaluationPhase.ChoDuyet
    && actorIsCurrentApprover
    && levelOptions?.allowApproverDelete === true
  // 🔴 CỐ Ý KHÔNG gate bằng `can(MenuKeys.PurchaseEvaluations, 'Delete')` — owner chốt
  // 2026-07-27. Reviewer ĐO trên DB: mọi vai đang thật sự làm người duyệt (`Procurement`,
  // `CostControl`, …) đều `CanDelete = 0`, và không seeder nào nâng cờ đó ⇒ gate bằng quyền
  // theo vai sẽ **ẩn nút với đúng người có quyền**, im lặng, không lỗi (gotcha #44).
  // Nguồn quyền = 3 rào ở trên, khớp 1-1 với 3 rào BE trong `PeSoftDeleteFeatures.cs`.
  // Rào `allowApproverDelete` là cờ per-NGƯỜI admin tick đích danh — chặt hơn quyền theo vai.
  // ⚠️ Endpoint BE cũng đã gỡ `[Authorize(Policy=...)]` cho khớp; đừng khôi phục MỘT bên.

  // [Mig 54] ③ CCM duyệt-done miễn CEO: chỉ đủ điều kiện khi user là CCM (CostControl) +
  // workflow có ngưỡng CEO + giá gói < ngưỡng (khớp guard fail-closed BE). ① bộ chọn giá
  // chốt hiện khi đây là duyệt CUỐI (Cấp cuối Bước cuối đang "Current") HOẶC CCM tích done.
  const isCcm = currentUser?.roles?.includes('CostControl') ?? false
  const ccmDelegationEligible = isCcm
    && evaluation.ceoApprovalThreshold != null
    && evaluation.winnerQuoteTotal < evaluation.ceoApprovalThreshold
  const flowStepsForFinal = evaluation.approvalFlow?.steps ?? []
  const lastFlowStep = flowStepsForFinal[flowStepsForFinal.length - 1]
  const lastFlowLevel = lastFlowStep?.levels[lastFlowStep.levels.length - 1]
  const currentIsFinalApprover = lastFlowLevel?.status === 'Current'
  // [Mig 58 item 4] Cấp Approver hiện tại được cấu hình KẾT THÚC trong Quy trình duyệt
  // (admin bật per-slot ở Designer). Duyệt tới cấp này = TỰ kết thúc (BE auto theo cờ cấp,
  // KHÔNG cần ô-tích). FE dùng cờ này để: (1) hiện banner cảnh báo + (2) bắt chọn giá chốt.
  const approverFinalizeEligible = levelOptions?.allowApproverFinalize === true
  // [S89 anh Kiệt FDC] Cấp "Duyệt thay CEO" (finalize) ĐẦU-TIÊN theo thứ tự duyệt trong flow →
  // badge cấp đó + làm mờ Bước/Cấp SAU để người xem phiếu biết quy trình DỪNG ở cấp này.
  // [S97 anh Kiệt FDC] GATE theo endsBeforeCeo (runtime): phiếu đã đi HẾT lên CEO (approver opt-out
  // finalize) → BE trả endsBeforeCeo=false → KHÔNG làm mờ, hiện đúng CEO đã duyệt (bug "CEO duyệt mà mờ").
  const finalizeFlowStep = evaluation.endsBeforeCeo
    ? (evaluation.approvalFlow?.steps.find(s => s.levels.some(l => l.allowApproverFinalize)) ?? null)
    : null
  const finalizeFlowLevelOrder = finalizeFlowStep?.levels.find(l => l.allowApproverFinalize)?.order ?? null
  const isStepAfterFinalize = (stepOrder: number) => finalizeFlowStep != null && stepOrder > finalizeFlowStep.order
  const isLevelAfterFinalize = (stepOrder: number, levelOrder: number) =>
    isStepAfterFinalize(stepOrder)
    || (finalizeFlowStep != null && stepOrder === finalizeFlowStep.order
        && finalizeFlowLevelOrder != null && levelOrder > finalizeFlowLevelOrder)
  // [S134 anh Kiệt FDC] Phiếu trước cùng gói thầu CHƯA duyệt (informational nudge — KHÔNG
  // hard-block). M2: banner "cần duyệt phiếu trước" chỉ có nghĩa khi ĐANG chờ duyệt (phase
  // ChoDuyet); phiếu Nháp/TraLai/TuChoi cũng có pending fill nhưng banner vô nghĩa ở đó.
  const pendingPriors = evaluation.budgetSummary?.pendingPriorPes ?? []
  const showPendingBanner = pendingPriors.length > 0
    && !evaluation.budgetSummary?.budgetFrozen
    && !readOnly
    && evaluation.phase === PurchaseEvaluationPhase.ChoDuyet
  const pendingPriorsText = pendingPriors
    .map(p => `${p.maPhieu ?? '(chưa có mã)'} (${PurchaseEvaluationPhaseLabel[p.phase] ?? `Phase ${p.phase}`})`)
    .join(', ')
  // Ứng viên giá chốt — chỉ giá nào đã có giá trị (≠ null).
  // [S117 anh Kiệt FDC] Khi PRO có CẢ Min & Max → thêm ứng viên "duyệt theo khoảng"
  // (source=ProMinMax, amount=Min, amountMax=Max). Chọn nó = chốt dải [Min, Max].
  const hasBothProPrices = evaluation.proSuggestedMinPrice != null && evaluation.proSuggestedMaxPrice != null
  const priceCandidates = ([
    { source: 'Ncc', label: 'Giá NCC (giá chào thầu)', amount: evaluation.winnerQuoteTotal },
    { source: 'ProMin', label: 'PRO — Giá Min', amount: evaluation.proSuggestedMinPrice },
    { source: 'ProMax', label: 'PRO — Giá Max', amount: evaluation.proSuggestedMaxPrice },
    ...(hasBothProPrices
      ? [{ source: 'ProMinMax', label: 'PRO — cả Min và Max (duyệt theo khoảng)', amount: evaluation.proSuggestedMinPrice, amountMax: evaluation.proSuggestedMaxPrice }]
      : []),
    { source: 'Ccm', label: 'CCM đề xuất', amount: evaluation.ccmSuggestedPrice },
  ] as { source: string; label: string; amount: number | null | undefined; amountMax?: number | null }[])
    .filter((c): c is { source: string; label: string; amount: number; amountMax?: number } => c.amount != null)
  const selectedCandidate = priceCandidates.find(c => c.source === approvedPriceSource)
  const selectedPriceAmount = selectedCandidate?.amount ?? null
  const selectedPriceAmountMax = selectedCandidate?.amountMax ?? null

  // 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<PeDepartmentApproval[]>({
    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 legacy Mig 16
      //    set RejectedFromPhase + clear N-stage rows + Drafter resume jump-back)
      //  - target = TraLai (98) từ phase trung gian — Session 17 spec mới: Trả
      //    lại là Phase RIÊNG (gotcha #45 — thiếu nhánh này gây "Trả về nhưng
      //    hệ thống vẫn duyệt" do BE nhận decision=Approve → ApproveV2Async).
      // BE có guard mirror trong PurchaseEvaluationWorkflowService.TransitionAsync
      // throw ConflictException nếu payload mismatch — phải sync 2 phía.
      const isReject = target === PurchaseEvaluationPhase.TuChoi
        || (target === PurchaseEvaluationPhase.DangSoanThao
            && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao)
        || (target === PurchaseEvaluationPhase.TraLai
            && evaluation.phase !== PurchaseEvaluationPhase.TraLai)
      // Mig 28 (S21 t4) — F1: chỉ gửi returnMode khi target=TraLai + mode != null
      const isTraLaiAction = target === PurchaseEvaluationPhase.TraLai
        && evaluation.phase !== PurchaseEvaluationPhase.TraLai
      // [Mig 54] ① gửi giá chốt khi đây là duyệt cuối (CEO/NV cuối) HOẶC CCM tích done
      // HOẶC [Mig 58] cấp được cấu hình KẾT THÚC (đều là điểm CHỐT giá).
      const sendPrice = !isReject && (currentIsFinalApprover || finalizeByCcm || (approverFinalizeEligible && applyLevelFinalize))
      // [S78] Người duyệt đính kèm file khi DUYỆT (forward approve). Upload TRƯỚC khi
      // chuyển phase: file lỗi (sai định dạng / >20MB) → throw, KHÔNG duyệt (toast lỗi BE).
      // File hợp lệ giữ lại (gắn phiếu, purpose=ApprovalAttachment, uploader=actor server-side).
      if (!isReject && approveFiles.length > 0) {
        for (const f of approveFiles) {
          const fd = new FormData()
          fd.append('file', f)
          fd.append('purpose', String(PeAttachmentPurpose.ApprovalAttachment))
          fd.append('note', 'Đính kèm khi duyệt')
          await api.post(`/purchase-evaluations/${evaluation.id}/attachments`, fd, {
            headers: { 'Content-Type': 'multipart/form-data' },
          })
        }
      }
      return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, {
        targetPhase: target,
        decision: isReject ? 2 : 1,
        comment: comment || null,
        returnMode: isTraLaiAction ? returnMode : null,
        returnTargetUserId: isTraLaiAction && returnMode === WorkflowReturnMode.Assignee
          ? returnTargetUserId : null,
        // Mig 31 (S23 t1) — F2 Approver scope ChoDuyet duyệt thẳng Cấp cuối.
        // BE check matchingLevel.AllowApproverSkipToFinal (admin opt-in per slot).
        skipToFinal: !isReject && skipToFinalApprover,
        // [Mig 54 2026-06-18] ③ CCM duyệt done miễn CEO + ① giá chốt người duyệt chọn.
        finalizeByCcmDelegation: !isReject && finalizeByCcm,
        // [S96 — anh Kiệt FDC] cờ finalize opt-out: chỉ có ý nghĩa khi cấp cấu hình KẾT THÚC.
        // Bỏ tick (false) → BE không finalize, trình tiếp CEO; cấp thường gửi true (no-op).
        applyLevelFinalize: !isReject && (approverFinalizeEligible ? applyLevelFinalize : true),
        approvedPriceAmount: sendPrice ? selectedPriceAmount : null,
        approvedPriceSource: sendPrice ? approvedPriceSource : null,
        // [S117] Duyệt theo khoảng: chỉ gửi Max khi nguồn = ProMinMax (selectedPriceAmountMax != null).
        // Nguồn đơn → undefined → axios bỏ key (BE reject nếu source đơn mà kèm Max).
        approvedPriceMaxAmount: sendPrice ? (selectedPriceAmountMax ?? undefined) : undefined,
      })
    },
    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] })
      // S78 — chỉ DUYỆT (forward approve) mới auto-đóng focus overlay. Trả lại /
      // Từ chối giữ overlay (mirror isReject/isSendBack trong mutationFn).
      const wasReject = target === PurchaseEvaluationPhase.TuChoi
        || (target === PurchaseEvaluationPhase.DangSoanThao && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao)
        || (target === PurchaseEvaluationPhase.TraLai && evaluation.phase !== PurchaseEvaluationPhase.TraLai)
      setTarget(null)
      setComment('')
      setReturnMode(WorkflowReturnMode.Drafter)
      setReturnTargetUserId(null)
      setSkipToFinalApprover(false)
      setFinalizeByCcm(false)
      setApplyLevelFinalize(false)
      setApprovedPriceSource(null)
      setApproveFiles([])
      if (!wasReject) onApproved?.()
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  // [S155 đợt 2] XÓA MỀM phiếu đang duyệt. Route CANONICAL `.../{id}/by-approver` —
  // KHÁC hẳn đường xóa nháp `DELETE /purchase-evaluations/{id}` (PeDetailTabs, giữ nguyên).
  // Lý do đi trong BODY: controller bind `[FromBody] DeleteByApproverBody? body`
  // (`PurchaseEvaluationsController.cs`), record `(string? Reason)` ⇒ axios `data:`.
  const deleteByApprover = useMutation({
    mutationFn: async () =>
      api.delete(`/purchase-evaluations/${evaluation.id}/by-approver`, {
        data: { reason: deleteReason.trim() || null },
      }),
    onSuccess: () => {
      toast.success('Đã xóa phiếu — chuyển sang mục "Đã xóa", thôi tính vào lũy kế.')
      setDeleteOpen(false)
      setDeleteReason('')
      // Phiếu rời MỌI danh sách sống (global filter) + số lũy kế đổi ⇒ invalidate cả
      // pe-budget (ma trận ngân sách S133/S134 đọc lũy kế) ngoài pe-list/pe-detail.
      qc.invalidateQueries({ queryKey: ['pe-list'] })
      qc.invalidateQueries({ queryKey: ['pe-detail', evaluation.id] })
      qc.invalidateQueries({ queryKey: ['pe-budget'] })
      onDeleted?.()
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  // UAT S60 (anh Kiệt 14:14): "bỏ luôn nút Từ chối — Duyệt hoặc Trả về thôi".
  // BE policy đã gỡ TuChoi khỏi nextPhases; filter này = defense-in-depth FE
  // (BE cũ cache / deploy lệch vẫn không lòi nút). Dialog/isCancel giữ dead-safe.
  const next = evaluation.workflow.nextPhases.filter(p => p !== PurchaseEvaluationPhase.TuChoi)
  const flow = evaluation.approvalFlow

  // [S78] File do người duyệt đính kèm trong quá trình duyệt (purpose=ApprovalAttachment).
  const approvalAttachments = (evaluation.attachments ?? []).filter(
    a => a.purpose === PeAttachmentPurpose.ApprovalAttachment,
  )
  const fmtFileSize = (b: number) =>
    b > 1024 * 1024 ? `${(b / 1024 / 1024).toFixed(1)}MB` : `${Math.round(b / 1024)}KB`
  const isPreviewable = (name: string) => /\.(pdf|png|jpe?g|webp)$/i.test(name)
  async function downloadAttachment(a: PeAttachment) {
    try {
      const r = await api.get(
        `/purchase-evaluations/${evaluation.id}/attachments/${a.id}/download`,
        { responseType: 'blob' },
      )
      const url = window.URL.createObjectURL(r.data as Blob)
      const link = document.createElement('a')
      link.href = url
      link.download = a.fileName
      link.click()
      window.URL.revokeObjectURL(url)
    } catch (e) {
      toast.error(getErrorMessage(e))
    }
  }

  return (
    <div className="space-y-4">
      <div>
        <h3 className="text-sm font-semibold text-slate-900">Quy trình duyệt</h3>
        {evaluation.approvalWorkflowCode && (
          <p className="mt-0.5 font-mono text-[11px] text-slate-500">
            {evaluation.approvalWorkflowCode} v{String(evaluation.approvalWorkflowVersion ?? 0).padStart(2, '0')}
            {evaluation.approvalWorkflowName && <> · <span className="font-sans">{evaluation.approvalWorkflowName}</span></>}
          </p>
        )}
      </div>

      {/* [Mig 58 AllowApproverFinalize — anh Kiệt FDC, S89] NOTE cho MỌI người NẮM:
          quy trình của phiếu này có cấp được cấu hình "Duyệt là KẾT THÚC" (không
          trình CEO). Branch theo phase: đang chạy = callout xanh-lá báo trước /
          Đã duyệt = xác nhận đã kết thúc tại đâu. */}
      {evaluation.endsBeforeCeo && (
        <div className="rounded-md border border-emerald-300 bg-emerald-50 px-3 py-2 text-[12px] text-emerald-800">
          {evaluation.phase === PurchaseEvaluationPhase.DaDuyet ? (
            <span>
              ✅ Phiếu đã kết thúc tại{' '}
              <span className="font-semibold">{evaluation.finalizeStepName ?? 'cấp duyệt'}</span>
              {evaluation.finalizeLevelName ? <> · {evaluation.finalizeLevelName}</> : null} — không qua CEO.
            </span>
          ) : (
            <span>
              ⚑ Quy trình rút gọn: duyệt đến{' '}
              <span className="font-semibold">{evaluation.finalizeStepName ?? 'cấp này'}</span>
              {evaluation.finalizeLevelName ? <> · {evaluation.finalizeLevelName}</> : null} là KẾT THÚC,
              không trình CEO. Các cấp trước vẫn duyệt như thường.
            </span>
          )}
        </div>
      )}

      {/* Mig 24 V2 — Flow render Bước → Cấp → NV thay phase cards.
          Status: Done (✓ emerald) / Current (● brand) / Pending (○ slate) */}
      {flow && flow.steps.length > 0 && (
        <ol className="space-y-2">
          {flow.steps.map(step => {
            const stepIcon = step.status === 'Done' ? '✓' : step.status === 'Current' ? '●' : '○'
            return (
              <li
                key={step.order}
                className={cn(
                  'rounded-md border p-2',
                  step.status === 'Current' && 'border-brand-300 bg-brand-50/50',
                  step.status === 'Done' && 'border-emerald-200 bg-emerald-50/40',
                  step.status === 'Pending' && 'border-slate-200 bg-white',
                  isStepAfterFinalize(step.order) && 'opacity-50',
                )}
              >
                <div className="flex items-center gap-2 text-xs font-medium">
                  <span className={cn(
                    'flex h-5 w-5 shrink-0 items-center justify-center rounded-full font-bold',
                    step.status === 'Done' && 'bg-emerald-500 text-white',
                    step.status === 'Current' && 'bg-brand-600 text-white',
                    step.status === 'Pending' && 'bg-slate-200 text-slate-500',
                  )}>
                    {stepIcon}
                  </span>
                  <span className="text-slate-800">Bước {step.order} — {step.name}</span>
                  {step.departmentName && (
                    <span className="rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
                      {step.departmentName}
                    </span>
                  )}
                  {isStepAfterFinalize(step.order) && (
                    <span
                      className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-400"
                      title={`Quy trình kết thúc tại ${evaluation.finalizeLevelName ?? evaluation.finalizeStepName ?? 'cấp duyệt thay CEO'} phía trên — bước này không cần trình tới`}
                    >
                      Kết thúc tại {evaluation.finalizeLevelName ?? evaluation.finalizeStepName ?? 'cấp trên'}
                    </span>
                  )}
                </div>
                {step.levels.length > 0 && (
                  <ul className="mt-1.5 ml-7 space-y-1 border-l-2 border-violet-200 pl-2">
                    {step.levels.map(lv => {
                      const lvIcon = lv.status === 'Done' ? '✓' : lv.status === 'Current' ? '●' : '○'
                      return (
                        <li key={lv.order} className={cn('text-[11px]', isLevelAfterFinalize(step.order, lv.order) && 'opacity-50')}>
                          <div className="flex items-start gap-1.5">
                            <span className={cn(
                              'mt-0.5 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[9px] font-bold',
                              lv.status === 'Done' && 'bg-emerald-500 text-white',
                              lv.status === 'Current' && 'bg-brand-600 text-white',
                              lv.status === 'Pending' && 'bg-slate-200 text-slate-500',
                            )}>
                              {lvIcon}
                            </span>
                            <div className="min-w-0 flex-1">
                              <div className="font-medium text-slate-700">
                                {lv.name || `Cấp ${lv.order}`}
                                {lv.status === 'Current' && <span className="ml-1.5 text-[10px] font-normal text-brand-700">đang chờ</span>}
                                {lv.status === 'Done' && <span className="ml-1.5 text-[10px] font-normal text-emerald-600">đã duyệt</span>}
                                {/* [S89 anh Kiệt FDC] Cấp này duyệt = KẾT THÚC quy trình, duyệt THAY CEO. */}
                                {lv.allowApproverFinalize && (
                                  <span
                                    className="ml-1.5 inline-flex items-center rounded bg-violet-100 px-1.5 py-0.5 text-[9px] font-semibold text-violet-800"
                                    title="Cấp này duyệt là KẾT THÚC quy trình — duyệt THAY CEO, không trình lên Ban Giám đốc"
                                  >
                                    ⚑ Duyệt thay CEO
                                  </span>
                                )}
                              </div>
                              <div className="flex flex-wrap items-center gap-x-1 gap-y-0.5 text-slate-500">
                                {lv.approvers.length === 0
                                  ? '(chưa cấu hình)'
                                  : lv.approvers.map((a, i) => (
                                    <span key={a.userId} className="inline-flex items-center gap-1">
                                      {i > 0 && <span className="text-slate-300">/</span>}
                                      <span>{a.fullName}</span>
                                      {/* [S76] Badge quyền nhập/điều chỉnh ngân sách (hiển thị-only, suy từ role) */}
                                      {a.canEditProBudget && (
                                        <span className="rounded bg-amber-100 px-1 py-0.5 text-[8px] font-semibold text-amber-700" title="Được nhập/điều chỉnh ngân sách cột PRO">✎ NS PRO</span>
                                      )}
                                      {a.canEditCcmBudget && (
                                        <span className="rounded bg-sky-100 px-1 py-0.5 text-[8px] font-semibold text-sky-700" title="Được nhập/điều chỉnh ngân sách cột CCM">✎ NS CCM</span>
                                      )}
                                    </span>
                                  ))}
                              </div>
                            </div>
                          </div>
                        </li>
                      )
                    })}
                  </ul>
                )}
              </li>
            )
          })}
        </ol>
      )}

      {/* Phiếu V1 legacy không có flow → fallback hiển thị phase summary đơn giản */}
      {!flow && (
        <div className="rounded border border-slate-200 bg-slate-50 px-3 py-2 text-[11px] text-slate-600">
          Phiếu này dùng quy trình cũ — workflow chi tiết không khả dụng.
        </div>
      )}

      {/* Mig 24 — V2 banner: hiển thị Bước/Cấp hiện tại + danh sách NV được duyệt.
          Nếu actor không có trong list → banner amber + nút Duyệt sẽ disable. */}
      {isV2Pending && evaluation.currentApproval && !readOnly && (
        <div className={cn(
          'rounded border px-3 py-2 text-[11px]',
          actorInV2Level
            ? 'border-emerald-200 bg-emerald-50 text-emerald-800'
            : 'border-amber-300 bg-amber-50 text-amber-800',
        )}>
          <div className="font-medium">
            Đang chờ Bước {evaluation.currentApproval.stepIndex + 1} ({evaluation.currentApproval.stepName})
            {evaluation.currentApproval.stepDepartmentName && <> · {evaluation.currentApproval.stepDepartmentName}</>}
            {' — '}Cấp {evaluation.currentApproval.levelOrder}
          </div>
          <div className="mt-0.5">
            NV duyệt: {evaluation.currentApproval.approvers.map(a => a.fullName).join(' / ') || '(chưa có)'}
          </div>
          {actorInV2Level
            ? <div className="mt-0.5 font-medium">✓ Đến lượt bạn duyệt</div>
            : <div className="mt-0.5">⚠ Không phải lượt bạn — chỉ NV trên mới duyệt được cấp này</div>}
        </div>
      )}

      {/* [S134] Banner cảnh báo phiếu trước cùng gói thầu CHƯA duyệt (M2: chỉ khi phase
          ChoDuyet). Informational — KHÔNG disable nút duyệt. */}
      {showPendingBanner && (
        <div className="rounded border border-amber-300 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
          ⚠ {pendingPriors.length} phiếu trước cùng gói thầu CHƯA duyệt: {pendingPriorsText} — cần duyệt phiếu trước trước phiếu này.
        </div>
      )}

      {(next.length > 0 || canDeleteWhileApproving) && !readOnly && (
        <div>
          <Label className="text-xs">Hành động:</Label>
          <div className="mt-1 flex flex-wrap gap-1.5">
            {next.map(p => {
              // Phân loại button theo hành động (3 màu khác nhau, in đậm):
              // - Duyệt   = forward phase tiếp theo — emerald (xanh lá positive)
              // - Trả lại = về DangSoanThao/TraLai (từ phase trung gian) — amber (request changes)
              // - Từ chối = TuChoi — red (terminal negative)
              const isSendBack = (p === PurchaseEvaluationPhase.DangSoanThao || p === PurchaseEvaluationPhase.TraLai)
                && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao
                && evaluation.phase !== PurchaseEvaluationPhase.TraLai
              const isCancel = p === PurchaseEvaluationPhase.TuChoi
              const isForwardApprove = !isSendBack && !isCancel
              // S59 anh chốt (UAT: "nhân viên tạo phiếu thì trả lại và từ chối cho ai?"):
              // người duyệt CHÍNH LÀ người soạn phiếu → ẩn cả Trả lại + Từ chối
              // (trả cho chính mình vô nghĩa — đang sửa inline được; hủy phiếu sai
              // = nhờ cấp khác Từ chối, phiếu Nháp có nút Xóa riêng).
              if ((isSendBack || isCancel) && evaluation.drafterUserId === currentUser?.id) return null
              // Mig 24 + UAT S22+1 — disable cả 3 button khi actor không match
              // currentLevel.ApproverUserId. "Không quyền = ko quyền mọi hành động."
              const isDisabled = blockedByV2Level
              const label = isSendBack ? '← Trả lại' : isCancel ? '✗ Từ chối' : '✓ Duyệt'
              const title = isDisabled && evaluation.currentApproval
                ? `Cấp ${evaluation.currentApproval.levelOrder} chỉ ${evaluation.currentApproval.approvers.map(a => a.fullName).join(' / ')} mới thao tác được (Duyệt / Trả lại / Từ chối).`
                : isForwardApprove
                  ? `Duyệt → ${PurchaseEvaluationPhaseLabel[p]}`
                  : undefined
              return (
                <button
                  key={p}
                  onClick={() => !isDisabled && setTarget(p)}
                  disabled={isDisabled}
                  title={title}
                  className={cn(
                    'rounded border px-2 py-1 text-[11px] font-bold transition',
                    isDisabled && 'cursor-not-allowed border-slate-200 bg-slate-50 text-slate-400 hover:bg-slate-50',
                    !isDisabled && isForwardApprove && 'border-emerald-300 text-emerald-700 hover:bg-emerald-50',
                    !isDisabled && isSendBack && 'border-amber-300 text-amber-700 hover:bg-amber-50',
                    !isDisabled && isCancel && 'border-red-300 text-red-700 hover:bg-red-50',
                  )}
                >
                  {label}
                </button>
              )
            })}
            {/* [S155 đợt 2] Nút XÓA — CẠNH vòng `next.map`, CỐ Ý KHÔNG nằm TRONG map:
                map sinh nút theo PHASE (`workflow.nextPhases`), còn Xóa KHÔNG phải một
                phase — nhét vào map sẽ phải bịa 1 phase giả. Điều kiện hiện nút ở
                `canDeleteWhileApproving` (4 rào). KHÔNG đụng nút xóa NHÁP ở
                `PeDetailTabs.tsx:457-472` — đó là đường khác, endpoint khác, quyền khác. */}
            {canDeleteWhileApproving && (
              <button
                type="button"
                onClick={() => setDeleteOpen(true)}
                title="Xóa phiếu này (phiếu chuyển sang mục &ldquo;Đã xóa&rdquo; và thôi tính vào lũy kế)"
                className="inline-flex items-center gap-1 rounded border border-red-300 bg-white px-2 py-1 text-[11px] font-bold text-red-700 transition hover:bg-red-50"
              >
                <Trash2 className="h-3 w-3" /> Xóa phiếu
              </button>
            )}
          </div>
        </div>
      )}
      {readOnly && next.length > 0 && (
        <div className="rounded border border-dashed border-slate-200 px-3 py-2 text-[11px] text-slate-500">
          Vào menu &ldquo;Duyệt&rdquo; để chuyển phase.
        </div>
      )}

      {target !== null && (() => {
        const isCancel = target === PurchaseEvaluationPhase.TuChoi
        // isSendBack sync với button label + payload isReject (gotcha #45).
        // Include cả DangSoanThao (legacy Mig 16) lẫn TraLai (Session 17 spec)
        // — cả 2 là Trả lại Drafter sửa.
        const isSendBack = (target === PurchaseEvaluationPhase.DangSoanThao
                            || target === PurchaseEvaluationPhase.TraLai)
          && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao
          && evaluation.phase !== PurchaseEvaluationPhase.TraLai
        const dialogTitle = isCancel
          ? '✗ Từ chối phiếu (khoá hoàn toàn)'
          : isSendBack
            ? '← Trả lại Drafter sửa'
            : `✓ Duyệt → ${PurchaseEvaluationPhaseLabel[target]}`
        // [Mig 54] ① bộ chọn giá chốt khi duyệt CUỐI hoặc CCM tích done — bắt buộc chọn.
        const isApproveAction = !isCancel && !isSendBack
        const shouldPickPrice = isApproveAction && (currentIsFinalApprover || finalizeByCcm || (approverFinalizeEligible && applyLevelFinalize))
        // [Mig 54 fix] Khoá "Xác nhận" khi PHẢI chọn giá mà: chưa có giá nào (candidates
        // rỗng — buộc nhập PRO/CCM hoặc chọn NCC thắng thầu trước) HOẶC chưa chọn giá. Tránh
        // người duyệt bấm rồi nhận lỗi BE "Chọn 1 giá chốt" khó hiểu (empty-candidates edge).
        const priceMissing = shouldPickPrice && (priceCandidates.length === 0 || !approvedPriceSource)
        return (
          <Dialog
            open
            onClose={() => { setTarget(null); setApproveFiles([]) }}
            title={dialogTitle}
            footer={<>
              <Button variant="ghost" onClick={() => { setTarget(null); setApproveFiles([]) }}>Hủy</Button>
              <Button onClick={() => transition.mutate()} disabled={transition.isPending || priceMissing}>Xác nhận</Button>
            </>}
          >
            {/* [S134] NHẮC LẠI phiếu trước chưa duyệt TRONG dialog duyệt (trên bộ chọn giá)
                — người duyệt cuối chắc chắn thấy. KHÔNG đụng disable nút Xác nhận (B3). */}
            {isApproveAction && showPendingBanner && (
              <div className="mb-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
                ⚠ {pendingPriors.length} phiếu trước cùng gói thầu CHƯA duyệt: {pendingPriorsText} — cần duyệt phiếu trước trước phiếu này.
              </div>
            )}
            {isCancel && (
              <div className="mb-3 rounded border border-red-200 bg-red-50 px-3 py-2 text-[11px] text-red-800">
                ⚠ 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.
              </div>
            )}
            {isSendBack && (
              <>
                {/* Mig 28 (S21 t4) — F1 mode picker khi Trả lại. Show modes
                    enabled per workflow.options. Default Drafter (S17 fallback). */}
                {(levelOptions?.allowReturnOneLevel
                  || levelOptions?.allowReturnOneStep
                  || levelOptions?.allowReturnToAssignee
                  || levelOptions?.allowReturnToDrafter
                  || !levelOptions) && (
                  <div className="mb-3 space-y-1.5">
                    <Label className="text-[12px]">Chọn cách Trả lại</Label>
                    <div className="space-y-1">
                      {(levelOptions?.allowReturnOneLevel) && (
                        <label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
                          <input
                            type="radio"
                            className="mt-0.5"
                            checked={returnMode === WorkflowReturnMode.OneLevel}
                            onChange={() => setReturnMode(WorkflowReturnMode.OneLevel)}
                          />
                          <span>
                            <span className="font-medium">Trả về 1 Cấp trước</span>
                            <span className="block text-[10px] text-slate-500">Lùi 1 Cấp trong cùng Bước. NV cấp trước nhận lại.</span>
                          </span>
                        </label>
                      )}
                      {(levelOptions?.allowReturnOneStep) && (
                        <label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
                          <input
                            type="radio"
                            className="mt-0.5"
                            checked={returnMode === WorkflowReturnMode.OneStep}
                            onChange={() => setReturnMode(WorkflowReturnMode.OneStep)}
                          />
                          <span>
                            <span className="font-medium">Trả về 1 Bước trước</span>
                            <span className="block text-[10px] text-slate-500">Lùi sang Bước trước, NV Cấp cuối Bước đó nhận lại.</span>
                          </span>
                        </label>
                      )}
                      {(levelOptions?.allowReturnToAssignee) && (
                        <label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
                          <input
                            type="radio"
                            className="mt-0.5"
                            checked={returnMode === WorkflowReturnMode.Assignee}
                            onChange={() => setReturnMode(WorkflowReturnMode.Assignee)}
                          />
                          <span className="flex-1">
                            <span className="font-medium">Trả về Người chỉ định</span>
                            <span className="block text-[10px] text-slate-500">Pick từ list NV đã duyệt trước đó. Workflow set Cấp/Bước của NV.</span>
                            {returnMode === WorkflowReturnMode.Assignee && (
                              <select
                                className="mt-1.5 w-full rounded border border-slate-300 px-2 py-1 text-[12px]"
                                value={returnTargetUserId ?? ''}
                                onChange={e => setReturnTargetUserId(e.target.value || null)}
                              >
                                <option value="">— Chọn NV —</option>
                                {signedApprovers.map(a => (
                                  <option key={a.userId} value={a.userId}>{a.fullName}</option>
                                ))}
                              </select>
                            )}
                          </span>
                        </label>
                      )}
                      {(levelOptions?.allowReturnToDrafter !== false) && (
                        <label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
                          <input
                            type="radio"
                            className="mt-0.5"
                            checked={returnMode === WorkflowReturnMode.Drafter}
                            onChange={() => setReturnMode(WorkflowReturnMode.Drafter)}
                          />
                          <span>
                            <span className="font-medium">Trả về Người soạn thảo (mặc định)</span>
                            <span className="block text-[10px] text-slate-500">Phase → "Cần chỉnh sửa lại". Drafter sửa rồi gửi lại chạy từ Cấp 1 Bước 1.</span>
                          </span>
                        </label>
                      )}
                    </div>
                  </div>
                )}
                <div className="mb-3 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
                  {returnMode === WorkflowReturnMode.Drafter
                    ? 'Phiếu sẽ về "Cần chỉnh sửa lại". Drafter có thể sửa rồi trình lại từ Cấp 1 Bước 1.'
                    : returnMode === WorkflowReturnMode.Assignee
                      ? 'Phiếu sẽ về Cấp/Bước của NV đã chọn (vẫn "Đã gửi duyệt"). NV nhận lại để duyệt tiếp.'
                      : 'Phiếu sẽ lùi pointer (vẫn "Đã gửi duyệt"). NV trước nhận lại để duyệt tiếp.'}
                </div>
              </>
            )}
            {/* Mig 31 (S23 t1) — F2 Approver toggle: chỉ visible khi Approve forward
                + admin tick AllowApproverSkipToFinal cho slot Cấp hiện tại. */}
            {!isCancel && !isSendBack && levelOptions?.allowApproverSkipToFinal && (
              <div className="mb-3">
                <label className="flex cursor-pointer items-start gap-2 rounded border border-violet-200 bg-violet-50 px-3 py-2 text-[12px] text-violet-800 hover:bg-violet-100/60">
                  <input
                    type="checkbox"
                    className="mt-0.5"
                    checked={skipToFinalApprover}
                    onChange={e => setSkipToFinalApprover(e.target.checked)}
                  />
                  <span>
                    <span className="font-medium">Duyệt thẳng Cấp cuối (skip Bước/Cấp trung gian)</span>
                    <span className="mt-0.5 block text-[11px] text-violet-700/80">
                      Phiếu sẽ skip tới NV cuối (CEO/cấp ký cuối) — NV cuối vẫn cần duyệt thật để hoàn tất.
                    </span>
                  </span>
                </label>
              </div>
            )}
            {!isCancel && !isSendBack && skipToFinalApprover && (
              <div className="mb-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
                ⚠ Bỏ qua mọi Cấp/Bước trung gian, phiếu chuyển thẳng tới NV cuối. NV cuối
                vẫn phải ký duyệt thật để phiếu thành "Đã duyệt".
              </div>
            )}
            {/* [Mig 54 2026-06-18 — anh Kiệt FDC] ③ CCM tích "Duyệt done, miễn CEO" —
                chỉ hiện khi user là CCM + gói < ngưỡng CEO uỷ quyền. */}
            {isApproveAction && ccmDelegationEligible && (
              <div className="mb-3">
                <label className="flex cursor-pointer items-start gap-2 rounded border border-emerald-300 bg-emerald-50 px-3 py-2 text-[12px] text-emerald-800 hover:bg-emerald-100/60">
                  <input
                    type="checkbox"
                    className="mt-0.5"
                    checked={finalizeByCcm}
                    onChange={e => setFinalizeByCcm(e.target.checked)}
                  />
                  <span>
                    <span className="font-medium">Duyệt done, miễn CEO (CEO uỷ quyền cho TP CCM)</span>
                    <span className="mt-0.5 block text-[11px] text-emerald-700/80">
                      Gói {fmtMoney(evaluation.winnerQuoteTotal)}đ &lt; ngưỡng CEO {fmtMoney(evaluation.ceoApprovalThreshold ?? 0)}đ — tích để hoàn tất ngay, không cần trình CEO.
                    </span>
                  </span>
                </label>
              </div>
            )}
            {/* [S96 2026-07-01 — anh Kiệt FDC] Cấp cấu hình KẾT THÚC trong Quy trình duyệt NAY là
                ô-tích BỎ được (opt-out): default tick = duyệt xong KẾT THÚC (không lên CEO); bỏ tick
                = vẫn trình tiếp CEO. Đảo S89 (auto-finalize im lặng → "cái này option thôi"). */}
            {isApproveAction && approverFinalizeEligible && (
              <div className="mb-3 rounded border border-emerald-300 bg-emerald-50 px-3 py-2 text-[12px] text-emerald-800">
                <label className="flex cursor-pointer items-start gap-2">
                  <input
                    type="checkbox"
                    className="mt-0.5"
                    checked={applyLevelFinalize}
                    onChange={e => setApplyLevelFinalize(e.target.checked)}
                  />
                  <span>
                    <span className="font-medium">Cấp này KẾT THÚC quy trình</span>
                    <span className="mt-0.5 block text-[11px] text-emerald-700/80">
                      {applyLevelFinalize
                        ? 'Đã tích — duyệt xong là phiếu "Đã duyệt", KẾT THÚC tại cấp này, KHÔNG trình tiếp CEO. Nhớ chọn 1 giá chốt bên dưới.'
                        : 'Mặc định: duyệt xong phiếu TRÌNH TIẾP lên CEO/cấp sau. TÍCH vào nếu muốn KẾT THÚC ngay tại cấp này (không lên CEO — không cần chọn giá chốt).'}
                    </span>
                  </span>
                </label>
              </div>
            )}
            {/* [Mig 54] ① Bộ chọn giá chốt — hiện khi duyệt CUỐI hoặc CCM tích done.
                "Duyệt theo giá đề xuất": chọn 1 trong NCC / PRO Min / PRO Max / CCM. */}
            {shouldPickPrice && (
              <div className="mb-3 space-y-1.5">
                <Label className="text-[12px]">
                  Chọn giá chốt (duyệt theo giá đề xuất) <span className="text-red-500">*</span>
                </Label>
                <div className="space-y-1">
                  {priceCandidates.map(c => (
                    <label key={c.source} className="flex items-center gap-2 rounded border border-brand-200 bg-white px-2 py-1.5 text-[12px] hover:bg-brand-50/40">
                      <input
                        type="radio"
                        checked={approvedPriceSource === c.source}
                        onChange={() => setApprovedPriceSource(c.source)}
                      />
                      <span className="flex-1 text-slate-700">{c.label}</span>
                      <span className="font-mono font-semibold text-slate-900">
                        {c.amountMax != null ? `${fmtMoney(c.amount)}đ – ${fmtMoney(c.amountMax)}đ` : `${fmtMoney(c.amount)}đ`}
                      </span>
                    </label>
                  ))}
                  {priceCandidates.length === 0 && (
                    <div className="rounded border border-amber-200 bg-amber-50 px-2 py-1.5 text-[11px] text-amber-700">
                      Chưa có giá nào để chọn — nhập giá đề xuất (PRO/CCM) hoặc chọn NCC thắng thầu trước khi duyệt cuối.
                    </div>
                  )}
                </div>
              </div>
            )}
            {/* [S78 — UAT Tra Sol] Người duyệt đính kèm file khi DUYỆT — dùng khi có thay
                đổi nhỏ, không muốn Trả lại phiếu. File upload TRƯỚC khi chuyển phase. */}
            {isApproveAction && (
              <div className="mb-3">
                <Label className="text-[12px]">Đính kèm file (tùy chọn)</Label>
                <p className="mb-1.5 mt-0.5 text-[11px] text-slate-500">
                  Tải file của bạn lên kèm khi duyệt — dùng khi có thay đổi nhỏ, không cần Trả lại phiếu.
                </p>
                <input
                  ref={approveFileInputRef}
                  type="file"
                  multiple
                  accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp"
                  onChange={e => {
                    const picked = Array.from(e.target.files ?? [])
                    e.target.value = ''
                    if (picked.length) setApproveFiles(prev => [...prev, ...picked])
                  }}
                  className="hidden"
                />
                <button
                  type="button"
                  onClick={() => approveFileInputRef.current?.click()}
                  className="inline-flex items-center gap-1.5 rounded border border-dashed border-brand-300 bg-brand-50/50 px-3 py-1.5 text-[11px] font-medium text-brand-700 hover:border-brand-500 hover:bg-brand-50"
                >
                  <Upload className="h-3.5 w-3.5" /> + Chọn file đính kèm
                </button>
                {approveFiles.length > 0 && (
                  <ul className="mt-1.5 space-y-1">
                    {approveFiles.map((f, i) => (
                      <li key={i} className="flex items-center gap-1.5 rounded bg-slate-50 px-2 py-1 text-[11px]">
                        <Paperclip className="h-3 w-3 shrink-0 text-slate-400" />
                        <span className="min-w-0 flex-1 truncate text-slate-700" title={f.name}>{f.name}</span>
                        <span className="shrink-0 text-[10px] text-slate-400">{fmtFileSize(f.size)}</span>
                        <button
                          type="button"
                          onClick={() => setApproveFiles(prev => prev.filter((_, j) => j !== i))}
                          className="shrink-0 rounded px-1 text-red-500 hover:bg-red-50"
                          title="Bỏ file"
                        >
                          <X className="h-3 w-3" />
                        </button>
                      </li>
                    ))}
                  </ul>
                )}
              </div>
            )}
            <Label>Ghi chú (tùy chọn)</Label>
            <Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
          </Dialog>
        )
      })()}

      {/* [S155 đợt 2] Dialog xác nhận XÓA — RIÊNG, không trộn vào dialog transition ở trên
          (transition đi `POST /transitions` với targetPhase; xóa đi `DELETE /by-approver`).
          Nội dung nói thẳng HỆ QUẢ vì màn "Đã xóa" hiện là CHỈ XEM, chưa có khôi phục
          (owner chốt (6)) ⇒ với người dùng đây là thao tác một chiều. */}
      {deleteOpen && (
        <Dialog
          open
          size="sm"
          onClose={() => { setDeleteOpen(false); setDeleteReason('') }}
          title="🗑 Xóa phiếu đang duyệt"
          footer={<>
            <Button variant="ghost" onClick={() => { setDeleteOpen(false); setDeleteReason('') }}>Hủy</Button>
            <Button
              variant="danger"
              onClick={() => deleteByApprover.mutate()}
              disabled={deleteByApprover.isPending}
            >
              {deleteByApprover.isPending ? 'Đang xóa…' : 'Xác nhận xóa'}
            </Button>
          </>}
        >
          <div className="mb-3 rounded border border-red-200 bg-red-50 px-3 py-2 text-[12px] text-red-800">
            <div className="font-semibold">Xóa phiếu &ldquo;{evaluation.tenGoiThau}&rdquo;?</div>
            <ul className="mt-1.5 list-disc space-y-1 pl-4 text-[11px]">
              <li>Phiếu <strong>rời khỏi quy trình duyệt</strong> và chuyển sang mục <strong>&ldquo;Đã xóa&rdquo;</strong>.</li>
              <li>Phiếu <strong>thôi tính vào lũy kế</strong> ngân sách của hạng mục — đây là lý do chính của nút này.</li>
              <li>Mục &ldquo;Đã xóa&rdquo; hiện <strong>chỉ để xem</strong>: chưa có khôi phục. Cần lại thì phải tạo phiếu mới.</li>
              <li>Thao tác được <strong>ghi vết</strong> (ai xóa, lúc nào, lý do) vào lịch sử phiếu.</li>
            </ul>
          </div>
          <Label>Lý do xóa (nên ghi rõ)</Label>
          <Textarea
            value={deleteReason}
            onChange={e => setDeleteReason(e.target.value)}
            rows={3}
            maxLength={2000}
            placeholder="VD: chọn nhầm gói thầu — phiếu này sai hạng mục, sẽ tạo lại phiếu đúng."
          />
          <p className="mt-1 text-[11px] text-slate-500">
            Lý do lưu vào lịch sử phiếu (tối đa 2000 ký tự). Bỏ trống vẫn xóa được, nhưng người
            sau sẽ không biết vì sao phiếu biến mất.
          </p>
        </Dialog>
      )}

      {deptApprovals.length > 0 && (
        <div className="border-t border-slate-200 pt-4">
          <DeptApprovalsSection rows={deptApprovals} currentPhase={evaluation.phase} />
        </div>
      )}

      {/* [S78] File do người duyệt đính kèm trong quá trình duyệt — hiển thị cho mọi
          người xem phiếu (KHÔNG lẫn vào "Bảng so sánh" — xem filter PeDetailTabs). */}
      {approvalAttachments.length > 0 && (
        <div className="border-t border-slate-200 pt-4">
          <h3 className="text-sm font-semibold text-slate-900">📎 File đính kèm khi duyệt</h3>
          <p className="mt-0.5 text-[11px] text-slate-500">File do người duyệt tải lên trong quá trình duyệt.</p>
          <div className="mt-2 space-y-1">
            {approvalAttachments.map(a => (
              <div key={a.id} className="flex items-center gap-1.5 rounded bg-slate-50 px-1.5 py-1 text-[11px]">
                <Paperclip className="h-3 w-3 shrink-0 text-slate-400" />
                <span className="min-w-0 flex-1 truncate text-slate-700" title={a.fileName}>{a.fileName}</span>
                <span className="shrink-0 text-[10px] text-slate-400">{fmtFileSize(a.fileSize)}</span>
                {isPreviewable(a.fileName) && (
                  <button
                    onClick={() => setPreviewAtt(a)}
                    className="shrink-0 rounded px-1 text-violet-600 hover:bg-violet-50"
                    title="Xem trước"
                  >
                    <Eye className="h-3 w-3" />
                  </button>
                )}
                <button
                  onClick={() => downloadAttachment(a)}
                  className="shrink-0 rounded px-1 text-brand-600 hover:bg-brand-50"
                  title="Tải xuống"
                >
                  <Download className="h-3 w-3" />
                </button>
              </div>
            ))}
          </div>
          {previewAtt && (
            <AttachmentPreviewDialog
              open
              evaluationId={evaluation.id}
              attachmentId={previewAtt.id}
              fileName={previewAtt.fileName}
              onClose={() => setPreviewAtt(null)}
            />
          )}
        </div>
      )}

      <div className="border-t border-slate-200 pt-4">
        <PeApprovalsSection ev={evaluation} />
      </div>

      <div className="border-t border-slate-200 pt-4">
        <PeHistorySection ev={evaluation} />
      </div>
    </div>
  )
}

// 2-stage dept approval timeline (Migration 16). Group by phase × dept,
// show Review NV row + Confirm TPB row. Highlight pending khi current phase
// có Review nhưng chưa Confirm → user biết "đang chờ TPB confirm".
function DeptApprovalsSection({
  rows,
  currentPhase,
}: {
  rows: PeDepartmentApproval[]
  currentPhase: number
}) {
  // Group: phase → dept → stages
  const grouped = new Map<number, Map<string, PeDepartmentApproval[]>>()
  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)
  }
  // Order: phase asc
  const phaseOrder = [...grouped.keys()].sort((a, b) => a - b)

  return (
    <div>
      <h3 className="text-sm font-semibold text-slate-900">Tiến trình duyệt 2-cấp phòng ban</h3>
      <p className="mt-0.5 text-[11px] text-slate-500">NV Review → TPB Confirm. Phase chỉ chuyển khi có Confirm.</p>
      <div className="mt-2 space-y-3">
        {phaseOrder.map(phase => {
          const byDept = grouped.get(phase)!
          return (
            <div key={phase}>
              <div className={cn(
                'inline-block rounded px-1.5 py-0.5 text-[10px] font-medium',
                PurchaseEvaluationPhaseColor[phase],
              )}>
                {PurchaseEvaluationPhaseLabel[phase]}
              </div>
              <div className="mt-1 space-y-1.5">
                {[...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 (
                    <div key={deptId} className={cn(
                      'rounded border px-2 py-1.5 text-[11px]',
                      isPending ? 'border-amber-300 bg-amber-50' : 'border-slate-200 bg-slate-50',
                    )}>
                      <div className="font-medium text-slate-700">{deptName}</div>
                      <div className="mt-1 grid grid-cols-[60px_1fr] gap-x-2 gap-y-0.5">
                        <span className="text-slate-500">Review:</span>
                        <span className={review ? 'text-slate-700' : 'text-slate-400'}>
                          {review
                            ? <>✓ {review.approverName} <span className="text-slate-500">— {fmtTime(review.approvedAt)}</span>{review.comment && <span className="text-slate-500"> · "{review.comment}"</span>}</>
                            : '— chưa có'}
                        </span>
                        <span className="text-slate-500">Confirm:</span>
                        <span className={confirm ? 'text-emerald-700' : 'text-amber-700'}>
                          {confirm
                            ? <>✓ {confirm.approverName}{confirm.isBypassed && <span className="ml-1 rounded bg-fuchsia-100 px-1 text-[9px] text-fuchsia-700">bypass</span>} <span className="text-slate-500">— {fmtTime(confirm.approvedAt)}</span>{confirm.comment && <span className="text-slate-500"> · "{confirm.comment}"</span>}</>
                            : '⏳ chờ TPB confirm'}
                        </span>
                      </div>
                    </div>
                  )
                })}
              </div>
            </div>
          )
        })}
      </div>
    </div>
  )
}

function fmtTime(iso: string): string {
  const d = new Date(iso)
  return d.toLocaleString('vi-VN', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
}

function fmtMoney(n: number): string {
  return n.toLocaleString('vi-VN')
}

