// Detail content cho 1 phiếu Duyệt NCC. Flat render (no tabs): Thông tin +
// NCC + Hạng mục + Báo giá stack vertically trong 1 màn hình.
// Duyệt history + Lịch sử thay đổi → moved to Panel 3 (xem PeWorkflowPanel
// → PeApprovalsSection + PeHistorySection).
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import { useIsFetching, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner'
import { Check, ChevronDown, ChevronRight, Download, Eye, Paperclip, Pencil, Plus, Trash2, Upload } from 'lucide-react'
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 { SearchableSelect } from '@/components/ui/SearchableSelect'
import { Select } from '@/components/ui/Select'
import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError'
import { cn } from '@/lib/cn'
import { useAuth } from '@/contexts/AuthContext'
import { AttachmentPreviewDialog, isPreviewable } from './AttachmentPreviewDialog'
import { PePipelineStrip } from './PePipelineStrip'
import {
  PeAttachmentPurpose,
  PeAttachmentPurposeLabel,
  PeDepartmentKind,
  PeDepartmentKindLabel,
  PeDisplayStatusColor,
  PeDisplayStatusLabel,
  PurchaseEvaluationPhase,
  PurchaseEvaluationPhaseLabel,
  PurchaseEvaluationTypeLabel,
  getPeDisplayStatus,
  isEditablePhase,
  type PeApproval,
  type PeAttachment,
  type PeChangelog,
  type PeDepartmentOpinion,
  type PeDetailBundle,
  type PeDetailRow,
  type PeLevelOpinion,
  type PeQuote,
  type PeSupplier,
} from '@/types/purchaseEvaluation'
import { SupplierType, SupplierTypeLabel } from '@/types/master'
import type { Supplier } from '@/types/master'

const fmtMoney = (v: number) => v.toLocaleString('vi-VN')

// Session 20 turn 4 — input helpers cho NCC/Quote inline form.
// VND format dùng convention VN dấu chấm ngàn (1.000.000). Strip non-digit
// khi parse user input → number. Empty/0 → empty string để placeholder hiện.
const parseVnd = (s: string): number => Number(s.replace(/[^\d]/g, '')) || 0
const formatVndInput = (n: number): string => (n > 0 ? n.toLocaleString('vi-VN') : '')

// Validation cơ bản FE — empty OK (optional fields). BE FluentValidation
// chưa enforce, FE check để user nhập sai biết ngay.
const PHONE_RE = /^0\d{9,10}$/  // VN: bắt đầu 0, 10-11 digits sau khi strip space/dash/dot
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const isValidPhone = (s: string): boolean => !s || PHONE_RE.test(s.replace(/[\s\-.]/g, ''))
const isValidEmail = (s: string): boolean => !s || EMAIL_RE.test(s)

// Session 20 turn 8: trang trí 5 NCC khác màu (cycle theo index). Winner override
// thành emerald nổi bật. Literal Tailwind class để JIT scan compile được.
const NCC_PALETTES = [
  'border-l-blue-400 bg-blue-50/40',
  'border-l-purple-400 bg-purple-50/40',
  'border-l-sky-400 bg-sky-50/40',
  'border-l-teal-400 bg-teal-50/40',
  'border-l-pink-400 bg-pink-50/40',
] as const

// Giá chào thầu của NCC/TP được chọn (winner) = sum quotes.thanhTien của winner
// supplier-row. Single source of truth — Section 3 (ChonNccSection) + pre-check
// nút "Lưu & Gửi Duyệt" cùng gọi để KHÔNG lệch predicate. Trả null khi chưa chọn
// NCC; trả số (có thể 0) khi đã chọn nhưng chưa nhập báo giá.
function computeGiaChaoThau(ev: PeDetailBundle): number | null {
  // [Mig 58] Multi-winner: null khi CHƯA chọn đơn vị nào; else = winnerQuoteTotal (BE
  // SUM ThanhTien MỌI đơn vị isWinner — single hoặc liên-danh ≥2). 0 = winner chưa báo giá.
  if (!ev.suppliers.some(s => s.isWinner)) return null
  return ev.winnerQuoteTotal
}

// [S116 anh Kiệt] "Có giá là được": phiếu gửi-duyệt được khi có ≥1 báo giá ĐƯỢC CHỌN
// (IsSelected) với thanhTien !== 0. Giá ÂM = phát-sinh-giảm / hoàn tiền NCC = giá THẬT,
// KHÔNG phải lỗi. Thay điều-kiện computeGiaChaoThau > 0 cũ: SUM không phân biệt all-zero
// (chưa nhập) vs net-zero (+100,-100=0 nhưng có giá thật) → net-zero bị chặn oan. Mirror
// BE selectedThanhTien = quotes WHERE IsSelected. Quote sống ở details[].quotes (KHÔNG
// suppliers[].quotes); IsSelected = báo giá được tick trúng trong hàng hạng mục.
function hasRealSelectedQuote(ev: PeDetailBundle): boolean {
  return ev.details.some(d => d.quotes.some(q => q.isSelected && q.thanhTien !== 0))
}

// Main detail content — flat render 3 section không tabs.
// Tên giữ PeDetailTabs để không break callsite (rename gây churn).
//
// `mode` (2026-05-07):
//  - 'detail' (default): full UX — Section 5 Ý kiến 4PB editable theo readOnly.
//    Dùng ở leaf "Danh sách" + "Duyệt" (3-panel pages).
//  - 'workspace': dùng ở leaf "Thao tác" (2-panel workspace). Section 5 LUÔN
//    disabled (Q5 user — ý kiến nhập khi duyệt, không phải workspace nhập liệu).
//    Workflow Panel + Approvals + History KHÔNG render trong PeDetailTabs (luôn
//    ở caller PeWorkflowPanel — workspace caller skip render Panel 3 hoàn toàn).
export function PeDetailTabs({
  evaluation,
  onBack,
  onDelete,
  readOnly = false,
  mode = 'detail',
  autoEditHeader = false,
}: {
  evaluation: PeDetailBundle
  onBack: () => void
  onDelete: () => void
  /** Menu "Duyệt" (pendingMe=1) — ẩn mọi action thêm/sửa/xóa, chỉ xem + duyệt phase. */
  readOnly?: boolean
  /** 'workspace' = Section 5 LUÔN disabled (ý kiến nhập ở leaf Duyệt). */
  mode?: 'detail' | 'workspace'
  /** Auto open Section 1 InfoTab in edit mode khi mount — triggered từ pencil icon Panel 1 */
  autoEditHeader?: boolean
}) {
  const qc = useQueryClient()
  // canEditPhase: bao gồm cả TraLai (user 2026-05-07). Header bar action
  // buttons "Sửa header" + "Xóa" + "Đóng" workspace mode đã chuyển xuống bottom
  // action bar (B11+ user 2026-05-07).
  const canEditPhase = isEditablePhase(evaluation.phase)
  const opinionsReadOnly = readOnly || mode === 'workspace'

  // Mig 28 (S21 t4) — F3: Approver edit Section 2 (Hạng mục + NCC + Báo giá).
  const { user: currentUser } = useAuth()
  const isAdmin = currentUser?.roles?.includes('Admin') ?? false
  // S69 — cờ gấp: role quyết định nút nào hiện. PRO (Procurement) → cờ ĐỎ
  // (isUrgentByPro), CCM (CostControl) → cờ XANH (isUrgentByCcm), Admin → cả 2.
  // BE chặn Forbidden role khác → FE chỉ ẩn nút (UX), không phải security.
  const isPro = currentUser?.roles?.includes('Procurement') ?? false
  const isCcm = currentUser?.roles?.includes('CostControl') ?? false
  // [S77 Tra Sol/anh Kiệt — chốt] BẤT ĐỐI XỨNG: GẮN = NV chức năng (ai làm nấy gắn);
  // GỠ = chỉ Trưởng phòng (DeptManager)/Admin (tránh NV khác lỡ tay gỡ). Nút phụ thuộc
  // trạng thái hiện tại: đã gấp → cần quyền GỠ; chưa gấp → cần quyền GẮN.
  const isDeptManager = currentUser?.roles?.includes('DeptManager') ?? false
  // [S85 anh Kiệt] Người KHAI phiếu tự GỠ cờ gấp của mình (nới GỠ +drafter; GẮN giữ role-only).
  const isDrafter = currentUser?.id != null && evaluation.drafterUserId === currentUser.id
  const canToggleProUrgent = evaluation.isUrgentByPro
    ? (isAdmin || (isPro && isDeptManager) || isDrafter)
    : (isAdmin || isPro)
  const canToggleCcmUrgent = evaluation.isUrgentByCcm
    ? (isAdmin || (isCcm && isDeptManager) || isDrafter)
    : (isAdmin || isCcm)
  const v2Approvers = evaluation.currentApproval?.approvers ?? []
  const actorMatchesLevel = isAdmin
    || (currentUser?.id != null && v2Approvers.some(a => a.userId === currentUser.id))
  const approverEditMode = evaluation.phase === PurchaseEvaluationPhase.ChoDuyet
    // Mig 29 (S21 t5) — read F3 từ currentLevelOptions (per-NV slot)
    && (evaluation.currentLevelOptions?.allowApproverEditDetails ?? false)
    && actorMatchesLevel
  const itemsReadOnly = readOnly && !approverEditMode

  // "Lưu & Gửi Duyệt" workspace mode (user 2026-05-07): trigger transition
  // sang phase tiếp theo (= Đã gửi duyệt). nextPhases[0] thường là ChoPurchasing
  // (skip TuChoi). Sau success → toast + invalidate + onBack đóng workspace.
  // Mig 31 (S23 t1) — F2 Drafter-from-Nháp semantic deprecated. skipToFinal moved
  // sang Approver scope ChoDuyet (per-Level slot — xem PeWorkflowPanel).

  const submitForApproval = useMutation({
    mutationFn: async () => {
      const next = evaluation.workflow.nextPhases.find(p => p !== PurchaseEvaluationPhase.TuChoi && p !== PurchaseEvaluationPhase.TraLai)
      if (!next) throw new Error('Không có phase tiếp theo để gửi duyệt')
      return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, {
        targetPhase: next,
        decision: 1,
        comment: null,
      })
    },
    onSuccess: () => {
      toast.success('Đã gửi duyệt phiếu — chuyển sang quy trình duyệt.')
      qc.invalidateQueries({ queryKey: ['pe-detail', evaluation.id] })
      qc.invalidateQueries({ queryKey: ['pe-list'] })
      onBack()
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  // S69 — toggle cờ gấp (PUT /urgent { isUrgent }). BE role-aware: PRO flip cờ ĐỎ,
  // CCM flip cờ XANH, Admin set CẢ 2. FE optimistic + invalidate detail + list.
  const toggleUrgent = useMutation({
    mutationFn: async (isUrgent: boolean) =>
      api.put(`/purchase-evaluations/${evaluation.id}/urgent`, { isUrgent }),
    onSuccess: (_d, isUrgent) => {
      toast.success(isUrgent ? 'Đã đánh dấu phiếu GẤP.' : 'Đã bỏ đánh dấu gấp.')
      qc.invalidateQueries({ queryKey: ['pe-detail', evaluation.id] })
      qc.invalidateQueries({ queryKey: ['pe-list'] })
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  const forwardPhase = evaluation.workflow.nextPhases.find(p =>
    p !== PurchaseEvaluationPhase.TuChoi && p !== PurchaseEvaluationPhase.TraLai)

  // Pre-check data-completeness cho action "Lưu & Gửi Duyệt" (S60 — anh Kiệt chốt).
  // CHỈ áp cho action gửi duyệt — liệt kê TẤT CẢ mục thiếu của Section 3 "Đơn vị
  // NCC/TP được chọn". Predicate khớp BE guard TransitionAsync (em main song song).
  // Dùng cùng computeGiaChaoThau như Section 3 để KHÔNG lệch.
  const missingForApproval = useMemo(() => {
    const missing: string[] = []
    // 1. Chưa chọn Đơn vị NCC/TP ([Mig 58] multi-winner: ≥1 đơn vị isWinner; selectedSupplierId
    // null khi liên-danh ≥2 nên KHÔNG dùng nó — mirror BE submit-guard IsWinner).
    if (!evaluation.suppliers.some(s => s.isWinner)) {
      missing.push("Chưa chọn Đơn vị NCC/TP")
    } else {
      // 2. [S116 anh Kiệt] Đơn vị được chọn CHƯA NHẬP giá chào thầu nào (mọi báo giá
      // được chọn = 0). "Có giá là được": giá ÂM (phát-sinh-giảm / hoàn tiền NCC) = giá
      // THẬT → gửi được; net-zero có giá thật (+100,-100) cũng gửi được. Chỉ chặn khi
      // TOÀN BỘ = 0 (chưa nhập gì). Mirror BE selectedThanhTien = quotes WHERE IsSelected.
      if (!hasRealSelectedQuote(evaluation)) missing.push("Đơn vị được chọn chưa có giá chào thầu")
    }
    // 3. Chưa nhập Ngân sách kỳ này (S61 — row 3 bảng tổng hợp, drafter nhập).
    // Predicate MIRROR BE guard: BudgetPeriodAmount is null || <= 0.
    // [S85 D3 anh Kiệt] Effective = budgetPeriodAmount ?? proInitialAmount (Ban hành lần đầu) —
    // mirror BE submit-guard fallback (row3 "tự nhảy" từ ban-hành → vẫn gửi-duyệt được).
    // [Mig 58 — anh Kiệt FDC] NS = 0 là HỢP LỆ (gói cho-không / vật tư cấp sẵn). Chỉ
    // block khi CHƯA khai (null). Mirror BE submit-guard nới lỏng cùng đợt.
    const effBudget = evaluation.budgetPeriodAmount ?? evaluation.budgetSummary?.proInitialAmount ?? null
    if (effBudget == null) {
      missing.push("Chưa nhập Ngân sách kỳ này")
    }
    // 4. Chưa đính kèm Bảng so sánh (attachment supplier-row null — chuẩn Section 3).
    // S78 — loại file "đính kèm khi duyệt" (purpose=ApprovalAttachment, supplierId=null)
    // khỏi check: nó KHÔNG phải bảng so sánh, không được false-pass submit-guard khi
    // phiếu Trả-lại re-submit (lúc đó đã tồn tại file khi-duyệt từ vòng trước).
    if (!evaluation.attachments?.some(
      a => a.purchaseEvaluationSupplierId === null
        && a.purpose !== PeAttachmentPurpose.ApprovalAttachment,
    )) {
      missing.push("Chưa đính kèm Bảng so sánh")
    }
    return missing
  }, [evaluation])

  // [D4/R3 anh Kiệt — "xanh=đã khai, đỏ=chưa khai => trường bắt buộc khai"] Checklist trực-quan
  // các trường BẮT BUỘC để gửi duyệt (mirror missingForApproval predicate). Hiện phía trên nút.
  const submitChecklist = useMemo(() => {
    const supplierOk = evaluation.suppliers.some(s => s.isWinner)   // [Mig 58] multi-winner
    // [S116 anh Kiệt] "đã nhập giá" = có ≥1 báo giá được chọn !== 0 (âm = giá thật). Không
    // dùng SUM > 0 (net-zero có giá thật bị false-đỏ). Mirror BE quotes WHERE IsSelected.
    const hasRealQuote = supplierOk && hasRealSelectedQuote(evaluation)
    const effBudget = evaluation.budgetPeriodAmount ?? evaluation.budgetSummary?.proInitialAmount ?? null
    return [
      { label: 'Quy trình duyệt', ok: evaluation.approvalWorkflowId != null },
      { label: 'Đơn vị NCC/TP được chọn', ok: supplierOk },
      { label: 'Đã nhập giá chào thầu', ok: hasRealQuote },
      { label: 'Ngân sách kỳ này', ok: effBudget != null },
      { label: 'Bảng so sánh đính kèm', ok: !!evaluation.attachments?.some(a => a.purchaseEvaluationSupplierId === null && a.purpose !== PeAttachmentPurpose.ApprovalAttachment) },
    ]
  }, [evaluation])

  const canSubmitForApproval = mode === 'workspace'
    && canEditPhase
    && !readOnly
    && forwardPhase != null
    && missingForApproval.length === 0

  // Tooltip reason cho button disabled (giúp diagnose tại sao "Lưu & Gửi Duyệt"
  // không bấm được — user feedback 2026-05-07). Reason cũ (workspace/canEditPhase/
  // readOnly/forwardPhase) giữ nguyên; append data-completeness check S60 sau cùng.
  const submitDisabledReason = !canEditPhase
    ? `Phiếu đã ở phase ${PurchaseEvaluationPhaseLabel[evaluation.phase]} — chỉ Bản nháp / Trả lại mới sửa + gửi được.`
    : readOnly
      ? 'Chế độ chỉ đọc.'
      : !forwardPhase
        ? `Workflow không có phase tiếp theo từ ${PurchaseEvaluationPhaseLabel[evaluation.phase]}. Liên hệ admin kiểm tra cấu hình quy trình.`
        : missingForApproval.length > 0
          ? `Chưa đủ thông tin mục 3 'Đơn vị NCC/TP được chọn':\n${missingForApproval.map(m => `• ${m}`).join('\n')}`
          : null

  return (
    <div className="rounded-lg border border-slate-200 bg-white shadow-sm">
      {/* [S159] dải 4 giai đoạn toàn trình — owner vẽ tab lên ảnh phiếu (phiếu PE ⇒ GĐ1 active) */}
      <PePipelineStrip current={1} />
      <div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-200 px-5 py-3">
        <div>
          <div className="flex items-center gap-2">
            <h2 className="text-base font-semibold text-slate-900">{evaluation.tenGoiThau}</h2>
            {/* Display status meta (Bản nháp / Đã gửi duyệt / Đã duyệt / Từ chối)
                — phase chi tiết hiện ở Workflow timeline Panel 3. */}
            <span
              className={cn(
                'rounded px-1.5 py-0.5 text-[11px] font-medium',
                PeDisplayStatusColor[getPeDisplayStatus(evaluation.phase)],
              )}
            >
              {PeDisplayStatusLabel[getPeDisplayStatus(evaluation.phase)]}
            </span>
            <span className="text-[10px] text-slate-400" title="Phase workflow chi tiết">
              ({PurchaseEvaluationPhaseLabel[evaluation.phase]})
            </span>
            {/* S69 — badge cờ gấp: ĐỎ (PRO) / XANH-lá (CCM). Hiển thị độc lập. */}
            {evaluation.isUrgentByPro && (
              <span className="inline-flex items-center gap-1 rounded bg-red-100 px-1.5 py-0.5 text-[11px] font-semibold text-red-700" title="Phòng Cung ứng (PRO) đánh dấu gấp">
                🔴 GẤP (PRO)
              </span>
            )}
            {evaluation.isUrgentByCcm && (
              <span className="inline-flex items-center gap-1 rounded bg-green-100 px-1.5 py-0.5 text-[11px] font-semibold text-green-700" title="Phòng Kiểm soát chi phí (CCM) đánh dấu gấp">
                🟢 GẤP (CCM)
              </span>
            )}
            {readOnly && (
              <span className="rounded bg-slate-100 px-1.5 py-0.5 text-[11px] font-medium text-slate-600">
                chế độ duyệt
              </span>
            )}
          </div>
          <div className="mt-0.5 flex flex-wrap items-center gap-2 text-[12px] text-slate-500">
            <span className="font-mono">{evaluation.maPhieu ?? '—'}</span>
            <span>·</span>
            <span>{PurchaseEvaluationTypeLabel[evaluation.type]}</span>
            <span>·</span>
            <span>{evaluation.projectName}</span>
            {/* S57bis — phiếu dạng "Dự án – Hạng mục công việc" (lời sếp) */}
            {evaluation.workItemName && <><span>–</span><span>{evaluation.workItemName}</span></>}
            {evaluation.drafterName && <><span>·</span><span>Soạn: {evaluation.drafterName}</span></>}
          </div>
          {/* S69 — nút bật/tắt cờ gấp (theo role) + hint giá trị gói vs ngưỡng CEO. */}
          {(canToggleProUrgent || canToggleCcmUrgent || evaluation.ceoApprovalThreshold != null) && (
            <div className="mt-1.5 flex flex-wrap items-center gap-2">
              {canToggleProUrgent && (
                <button
                  type="button"
                  disabled={toggleUrgent.isPending}
                  onClick={() => toggleUrgent.mutate(!evaluation.isUrgentByPro)}
                  className={cn(
                    'inline-flex items-center gap-1 rounded border px-2 py-1 text-[11px] font-medium transition disabled:opacity-50',
                    evaluation.isUrgentByPro
                      ? 'border-red-300 bg-red-50 text-red-700 hover:bg-red-100'
                      : 'border-slate-300 bg-white text-slate-600 hover:border-red-300 hover:text-red-700',
                  )}
                  title="Cờ ĐỎ — Phòng Cung ứng (PRO) đánh dấu gấp"
                >
                  🔴 {evaluation.isUrgentByPro ? 'Bỏ gấp (PRO)' : 'Đánh dấu GẤP (PRO)'}
                </button>
              )}
              {canToggleCcmUrgent && (
                <button
                  type="button"
                  disabled={toggleUrgent.isPending}
                  onClick={() => toggleUrgent.mutate(!evaluation.isUrgentByCcm)}
                  className={cn(
                    'inline-flex items-center gap-1 rounded border px-2 py-1 text-[11px] font-medium transition disabled:opacity-50',
                    evaluation.isUrgentByCcm
                      ? 'border-green-300 bg-green-50 text-green-700 hover:bg-green-100'
                      : 'border-slate-300 bg-white text-slate-600 hover:border-green-300 hover:text-green-700',
                  )}
                  title="Cờ XANH — Phòng Kiểm soát chi phí (CCM) đánh dấu gấp"
                >
                  🟢 {evaluation.isUrgentByCcm ? 'Bỏ gấp (CCM)' : 'Đánh dấu GẤP (CCM)'}
                </button>
              )}
              {/* Hint giá trị gói vs ngưỡng CEO (chỉ khi workflow có set ngưỡng). */}
              {evaluation.ceoApprovalThreshold != null && (
                <span className="inline-flex items-center gap-1 rounded bg-slate-50 px-2 py-1 text-[11px] text-slate-600">
                  Giá trị gói: <strong className="text-slate-800">{fmtMoney(evaluation.winnerQuoteTotal)}đ</strong>
                  {' — '}
                  {evaluation.winnerQuoteTotal < evaluation.ceoApprovalThreshold ? (
                    <span className="font-medium text-emerald-600">CCM duyệt là xong</span>
                  ) : (
                    <span className="font-medium text-rose-600">Cần CEO duyệt</span>
                  )}
                  <span className="text-slate-400">(ngưỡng {fmtMoney(evaluation.ceoApprovalThreshold)}đ)</span>
                </span>
              )}
            </div>
          )}
        </div>
        {/* Header bar actions: User 2026-05-07 chốt bỏ "Sửa header" + "Xóa" +
            "Đóng" (workspace mode actions chuyển xuống bottom action bar). Vẫn
            giữ Đóng cho non-workspace view (Danh sách + Duyệt — readOnly). */}
        {(readOnly || mode !== 'workspace') && (
          <div className="flex gap-2">
            <Button variant="ghost" onClick={onBack} className="text-xs">← Đóng</Button>
          </div>
        )}
      </div>

      {/* S77 [Bích Phượng hỏi / anh] — phiếu Trả lại ở chế độ XEM không có nút gửi
          (readOnly → "Lưu & Gửi Duyệt" chỉ hiện khi Sửa). Banner hướng dẫn gửi lại
          để NV khỏi lạc ("bấm nộp lần 2 chỗ nào"). */}
      {evaluation.phase === PurchaseEvaluationPhase.TraLai && readOnly && (
        <div className="m-4 rounded-md border border-amber-300 bg-amber-50 px-4 py-3 text-[13px] text-amber-900">
          <div className="font-semibold">⚠️ Phiếu đã bị trả lại để chỉnh sửa</div>
          <div className="mt-1 leading-relaxed">
            Để <strong>gửi duyệt lại</strong>: ra <strong>danh sách</strong> → bấm <strong>✏️ Sửa</strong> phiếu
            này (biểu tượng bút chì) → điều chỉnh nội dung cần sửa → bấm <strong>“Lưu &amp; Gửi Duyệt →”</strong> ở
            cuối phiếu. Lý do trả lại xem ở mục <strong>“Lịch sử”</strong> bên dưới.
          </div>
        </div>
      )}

      <div className="divide-y divide-slate-200">
        {/* Section layout (Session 20 Chunk B): Hạng mục nested expand chứa NCC
            (tầng 1 = hạng mục, tầng 2 = NCC tham gia + báo giá inline). NCC
            tham gia section riêng bỏ — gộp vào Section 2 expand panel. Tên
            hạng mục + giá trị auto từ gói thầu (Chunk A BE seed). */}
        <Section title="1. Thông tin gói thầu">
          <InfoTab ev={evaluation} readOnly={readOnly} autoEdit={autoEditHeader} />
        </Section>
        <Section title={`2. Hạng mục + Báo giá NCC (${evaluation.details.length} hạng mục · ${evaluation.suppliers.length} NCC)`}>
          {/* Mig 28 (S21 t4) — F3: itemsReadOnly cho phép approver edit Section 2 */}
          {/* Plan Q S23 t7 — Drop mx-5 banner, full-width Section padding to
              align với ItemsTab header (button "+ Thêm hạng mục" right-aligned
              KHÔNG còn lệch khỏi banner inset gap). */}
          {approverEditMode && readOnly && (
            <div className="mb-3 rounded border border-violet-200 bg-violet-50 px-3 py-2 text-[11px] text-violet-800">
              ⓘ Bạn được phép chỉnh sửa Hạng mục / NCC / Báo giá (workflow bật mode Approver edit).
              Mọi thay đổi sẽ được ghi vào Lịch sử chỉnh sửa.
            </div>
          )}
          <ItemsTab ev={evaluation} readOnly={itemsReadOnly} />
        </Section>
        <Section title="3. Đơn vị NCC/TP được chọn">
          <ChonNccSection ev={evaluation} readOnly={readOnly} />
        </Section>
        {/* [anh Kiệt FDC S87+] Khối NỔI BẬT "4. Thông tin chọn thầu" — header xanh
            giống bảng "giá trị thực hiện". Gom winner + c/d/e tách khỏi Section 3. */}
        <section className="px-3 py-3 sm:px-5 sm:py-4">
          <ThongTinChonThauSection ev={evaluation} readOnly={readOnly} />
        </section>
        <Section title="5. Ý kiến cấp duyệt (sign-off theo workflow)">
          {mode === 'workspace' && (
            <div className="mb-3 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-[12px] text-amber-800">
              Ý kiến + chữ ký auto đồng bộ khi NV duyệt phiếu — vào menu &ldquo;Duyệt&rdquo; để ký.
            </div>
          )}
          {/* Mig 26 — V2 dynamic theo ApprovalWorkflowLevel. V1 phiếu cũ
              fallback render 4 box CỨNG readOnly (data legacy giữ Mig 15). */}
          {evaluation.approvalWorkflowId
            ? <LevelOpinionsSectionV2 ev={evaluation} />
            : <DepartmentOpinionsSection ev={evaluation} readOnly={opinionsReadOnly} />}
        </Section>
        {/* S61 — Section "Điều chỉnh ngân sách" cũ (BudgetAdjustSection) XÓA:
            module Budget bỏ hẳn, bảng TỔNG HỢP NGÂN SÁCH TRÌNH KÝ trong Section 3
            thay thế (PRO/CCM/drafter nhập trực tiếp theo capability flag BE). */}
      </div>

      {/* Action bar bottom — workspace mode + canEdit + !readOnly. 3 nút:
          - Xóa phiếu (CHỈ Bản nháp, soft-delete BE) — bên trái red
          - Lưu (toast confirm, KHÔNG đóng workspace) — chính giữa ghost
          - Lưu & Gửi Duyệt → (POST /transitions → next phase) — bên phải brand
          User 2026-05-07. */}
      {mode === 'workspace' && canEditPhase && !readOnly && (
        <>
          <div className="flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-slate-200 bg-amber-50/40 px-5 py-2 text-[11px]">
            <span className="font-semibold text-slate-600">Cần đủ để gửi duyệt:</span>
            {submitChecklist.map(c => (
              <span key={c.label} className={cn('inline-flex items-center gap-1', c.ok ? 'text-emerald-600' : 'font-semibold text-red-600')}>
                {c.ok ? '✓' : '✗'} {c.label}
              </span>
            ))}
          </div>
          <div className="flex flex-wrap items-center justify-between gap-2 border-t border-slate-200 bg-slate-50 px-5 py-3">
          <div className="flex items-center gap-3">
            {/* Xóa phiếu — CHỉ DangSoanThao (bản nháp). TraLai không cho xóa
                (đã có lịch sử workflow). Soft-delete qua DELETE /pe/:id endpoint
                (AuditableEntity IsDeleted=true, không xóa hoàn toàn DB). */}
            {evaluation.phase === PurchaseEvaluationPhase.DangSoanThao && (
              <Button
                variant="danger"
                onClick={() => {
                  if (confirm(`Xóa phiếu "${evaluation.tenGoiThau}"? Phiếu sẽ ẩn khỏi danh sách (soft-delete, không xóa hoàn toàn trong DB).`)) {
                    onDelete()
                  }
                }}
                className="gap-1.5 text-xs"
              >
                <Trash2 className="h-3.5 w-3.5" /> Xóa phiếu
              </Button>
            )}
            <span className="text-[11px] text-slate-500">
              ✓ Các thay đổi đã tự động lưu khi chỉnh sửa từng phần.
            </span>
          </div>
          <div className="flex gap-2">
            <Button
              variant="ghost"
              onClick={() => {
                qc.invalidateQueries({ queryKey: ['pe-detail', evaluation.id] })
                qc.invalidateQueries({ queryKey: ['pe-list'] })
                toast.success('Đã lưu — sync server.')
              }}
              className="text-xs"
            >
              Lưu
            </Button>
            <Button
              onClick={() => {
                if (!forwardPhase) return
                const confirmMsg = `Gửi phiếu vào quy trình duyệt? Sẽ chuyển sang "${PurchaseEvaluationPhaseLabel[forwardPhase]}". Sau khi gửi sẽ KHÔNG sửa được nữa (trừ khi approver Trả lại).`
                if (confirm(confirmMsg)) {
                  submitForApproval.mutate()
                }
              }}
              disabled={!canSubmitForApproval || submitForApproval.isPending}
              title={submitDisabledReason ?? `Gửi phiếu sang "${forwardPhase ? PurchaseEvaluationPhaseLabel[forwardPhase] : '?'}"`}
              className="text-xs"
            >
              {submitForApproval.isPending ? 'Đang gửi…' : 'Lưu & Gửi Duyệt →'}
            </Button>
          </div>
        </div>
        </>
      )}
    </div>
  )
}

function Section({ title, children }: { title: string; children: React.ReactNode }) {
  // Session 20 turn 11: padding responsive cho laptop màn nhỏ — px-3 trên xs
  // (tiết kiệm ~16px width), bump px-5 từ sm+ trở lên.
  return (
    <section className="px-3 py-3 sm:px-5 sm:py-4">
      <h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</h3>
      {children}
    </section>
  )
}

// ===== Section 5 — Ý kiến 4 phòng ban =====
// Render 2x2 grid 4 box (Phê duyệt / CCM / MuaHàng / SM-PM). Mỗi box hiển
// thị Opinion text + chữ ký (UserName + SignedAt) nếu đã ký, hoặc form nhập
// + 2 button "Lưu" + "Lưu & Ký" khi chưa ký / readOnly=false.
function DepartmentOpinionsSection({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
  const KINDS: { kind: number; label: string }[] = [
    { kind: PeDepartmentKind.PheDuyet, label: PeDepartmentKindLabel[PeDepartmentKind.PheDuyet] },
    { kind: PeDepartmentKind.Ccm, label: PeDepartmentKindLabel[PeDepartmentKind.Ccm] },
    { kind: PeDepartmentKind.MuaHang, label: PeDepartmentKindLabel[PeDepartmentKind.MuaHang] },
    { kind: PeDepartmentKind.SmPm, label: PeDepartmentKindLabel[PeDepartmentKind.SmPm] },
  ]
  return (
    <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
      {KINDS.map(k => {
        const existing = ev.departmentOpinions.find(o => o.kind === k.kind) ?? null
        return (
          <OpinionBox
            key={k.kind}
            evaluationId={ev.id}
            kind={k.kind}
            kindLabel={k.label}
            existing={existing}
            readOnly={readOnly}
          />
        )
      })}
    </div>
  )
}

function OpinionBox({
  evaluationId,
  kind,
  kindLabel,
  existing,
  readOnly,
}: {
  evaluationId: string
  kind: number
  kindLabel: string
  existing: PeDepartmentOpinion | null
  readOnly: boolean
}) {
  const qc = useQueryClient()
  const [text, setText] = useState(existing?.opinion ?? '')
  const isSigned = !!existing?.signedAt

  const save = useMutation({
    mutationFn: async (sign: boolean) =>
      api.post(`/purchase-evaluations/${evaluationId}/opinions`, {
        kind,
        opinion: text || null,
        sign,
      }),
    onSuccess: () => {
      toast.success('Đã lưu ý kiến.')
      qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] })
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  return (
    <div className={cn(
      'rounded-lg border bg-white p-3',
      isSigned ? 'border-emerald-200' : 'border-slate-200',
    )}>
      <div className="mb-2 flex items-center justify-between">
        <h4 className="text-[13px] font-semibold uppercase tracking-wide text-slate-700">{kindLabel}</h4>
        {isSigned && (
          <span className="inline-flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-medium text-emerald-700">
            <Check className="h-3 w-3" /> Đã ký
          </span>
        )}
      </div>

      {readOnly ? (
        <>
          <div className="min-h-[60px] whitespace-pre-wrap text-sm text-slate-800">
            {existing?.opinion ?? <span className="italic text-slate-400">— chưa có ý kiến</span>}
          </div>
          {isSigned && (
            <div className="mt-2 border-t border-slate-100 pt-1.5 text-[11px] text-slate-500">
              Ký bởi <strong>{existing?.userName ?? '—'}</strong> · {new Date(existing!.signedAt!).toLocaleString('vi-VN')}
            </div>
          )}
        </>
      ) : (
        <>
          <textarea
            rows={3}
            value={text}
            onChange={e => setText(e.target.value)}
            placeholder="Nhập ý kiến…"
            className="w-full resize-none rounded border border-slate-200 px-2 py-1.5 text-sm focus:border-brand-300 focus:outline-none focus:ring-1 focus:ring-brand-200"
          />
          <div className="mt-2 flex items-center justify-between gap-2">
            <div className="text-[11px] text-slate-500">
              {isSigned
                ? <>Ký bởi <strong>{existing?.userName ?? '—'}</strong> · {new Date(existing!.signedAt!).toLocaleString('vi-VN')}</>
                : 'Chưa ký'}
            </div>
            <div className="flex gap-1">
              <Button
                variant="ghost"
                onClick={() => save.mutate(false)}
                disabled={save.isPending}
                className="text-xs"
              >
                Lưu text
              </Button>
              <Button
                onClick={() => save.mutate(true)}
                disabled={save.isPending}
                className="text-xs"
              >
                {isSigned ? 'Cập nhật chữ ký' : 'Lưu & Ký'}
              </Button>
            </div>
          </div>
        </>
      )}
    </div>
  )
}

// ===== Section 5 V2 — Ý kiến cấp duyệt dynamic (Mig 26 — Session 19) =====
//
// Render theo workflow đã pin: forEach Step → forEach Level (Cấp) → forEach
// approver (NV). Mỗi NV = 1 OpinionBox (read-only). Service ApproveV2Async
// auto sync comment khi duyệt (Q1=1B). Empty list → fallback message.
//
// Layout 5A: header "Bước N — Phòng X" badge + grid-cols-2 cho N approvers
// (wrap nếu N>2). Admin override badge khi SignedByUserId !== ApproverUserId.

// Session 20 Chunk C (revised): gộp opinions đồng cấp cùng Phòng → 1 wrapper box / Step,
// BÊN TRONG render từng NV đã duyệt thành các "ô vuông" card mirror visual S19
// (grid-cols-2 cards). User feedback turn 2: giữ visual ô vuông như trước.
//
// Counter fix turn 2: "Số bước duyệt" (= số Cấp / Step) KHÁC "số người duyệt trong
// 1 bước" (= tổng NV across Cấp, OR-of-N nên chỉ 1 NV/Cấp cần ký). Counter đúng
// hiển thị X/Y cấp đã duyệt + thông tin phụ tổng NV tham gia.
function LevelOpinionsSectionV2({ ev }: { ev: PeDetailBundle }) {
  const flow = ev.approvalFlow
  const opinions = ev.levelOpinions

  if (!flow || flow.steps.length === 0) {
    return (
      <div className="rounded border border-slate-200 bg-slate-50 px-3 py-2 text-[12px] text-slate-500">
        Workflow chưa được cấu hình hoặc chưa có cấp duyệt nào.
      </div>
    )
  }

  return (
    <div className="space-y-3">
      {flow.steps.map(step => {
        const totalLevels = step.levels.length
        const totalApprovers = step.levels.reduce((n, l) => n + l.approvers.length, 0)
        const stepOpinions = opinions
          .filter(o => o.stepOrder === step.order)
          .slice()
          .sort((a, b) => a.levelOrder - b.levelOrder || a.signedAt.localeCompare(b.signedAt))
        const signedLevels = new Set(stepOpinions.map(o => o.levelOrder)).size
        return (
          <StepOpinionsBox
            key={step.order}
            stepOrder={step.order}
            stepName={step.name}
            departmentName={step.departmentName}
            totalLevels={totalLevels}
            totalApprovers={totalApprovers}
            signedLevels={signedLevels}
            opinions={stepOpinions}
          />
        )
      })}
    </div>
  )
}

function StepOpinionsBox({
  stepOrder, stepName, departmentName, totalLevels, totalApprovers, signedLevels, opinions,
}: {
  stepOrder: number
  stepName: string
  departmentName?: string | null
  totalLevels: number       // số Cấp (bước duyệt nhỏ trong Step)
  totalApprovers: number    // tổng NV tham gia (FYI — OR-of-N nên không cần ký hết)
  signedLevels: number      // số Cấp đã có ít nhất 1 NV ký
  opinions: PeLevelOpinion[]
}) {
  return (
    <div className="rounded-lg border border-slate-200 bg-slate-50/40">
      <div className="flex flex-wrap items-center gap-2 border-b border-slate-200 bg-slate-50/80 px-3 py-2">
        <span className="text-[12px] font-bold uppercase tracking-wide text-slate-700">
          Bước {stepOrder} — {stepName}
        </span>
        {departmentName && (
          <span className="rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
            {departmentName}
          </span>
        )}
        <span
          className="ml-auto text-[10px] text-slate-500"
          title={`Tổng ${totalApprovers} NV tham gia (mỗi cấp chỉ cần 1 NV ký — OR-of-N)`}
        >
          {signedLevels}/{totalLevels} cấp đã duyệt · {totalApprovers} NV tham gia
        </span>
      </div>
      <div className="p-3">
        {opinions.length === 0 ? (
          <div className="text-[12px] italic text-slate-400">— Chưa có ý kiến duyệt.</div>
        ) : (
          <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
            {opinions.map(o => <StepOpinionEntry key={o.id} opinion={o} />)}
          </div>
        )}
      </div>
    </div>
  )
}

function StepOpinionEntry({ opinion }: { opinion: PeLevelOpinion }) {
  const isAdminOverride = opinion.signedByUserId !== opinion.approverUserId
  return (
    <div className="rounded-lg border border-emerald-200 bg-white p-3">
      <div className="mb-2 flex items-start justify-between gap-2">
        <div className="min-w-0 flex-1">
          <h4 className="text-[13px] font-semibold text-slate-700">
            Cấp {opinion.levelOrder} — <span className="text-slate-900">{opinion.approverFullName}</span>
          </h4>
          {isAdminOverride && (
            <div className="mt-1 inline-flex items-center gap-1 rounded bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700">
              ⚠ Admin <strong>{opinion.signedByFullName}</strong> duyệt thay
            </div>
          )}
        </div>
        <span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-medium text-emerald-700">
          <Check className="h-3 w-3" /> Đã duyệt
        </span>
      </div>
      <div className="whitespace-pre-wrap text-sm text-slate-800">
        {opinion.comment}
      </div>
      <div className="mt-2 border-t border-slate-100 pt-1.5 text-[11px] text-slate-500">
        {new Date(opinion.signedAt).toLocaleString('vi-VN')}
      </div>
    </div>
  )
}

// ===== Exports cho Panel 3 — Approvals history + Changelog =====

export function PeApprovalsSection({ ev }: { ev: PeDetailBundle }) {
  return (
    <div>
      <h3 className="mb-2 text-sm font-semibold text-slate-900">Lịch sử duyệt ({ev.approvals.length})</h3>
      <ApprovalsTab ev={ev} />
    </div>
  )
}

export function PeHistorySection({ ev }: { ev: PeDetailBundle }) {
  return (
    <div>
      <h3 className="mb-2 text-sm font-semibold text-slate-900">Lịch sử thay đổi</h3>
      <HistoryTab ev={ev} />
    </div>
  )
}

// ===== Section 1 — Thông tin gói thầu (spec: a. Tên gói thầu / b. Dự án) =====
// Inline editable khi canEdit (=!readOnly && phase editable). Edit pencil button
// "Sửa" flip display ↔ form mode. Save dùng existing PUT /pe/:id endpoint với
// current entity values + new header fields. Dự án + Type LOCKED sau create —
// chỉ Tên/Địa điểm/Mô tả/Payment editable inline. autoEdit prop cho phép trigger
// edit mode từ pencil icon trong PeListPanel (URL flag ?editHeader=1).
// Phase editable = DangSoanThao + TraLai (user 2026-05-07).
function InfoTab({ ev, readOnly, autoEdit }: { ev: PeDetailBundle; readOnly: boolean; autoEdit: boolean }) {
  const canEdit = !readOnly && isEditablePhase(ev.phase)
  const qc = useQueryClient()
  const [editing, setEditing] = useState(autoEdit && canEdit)
  const [tenGoiThau, setTenGoiThau] = useState(ev.tenGoiThau)
  const [diaDiem, setDiaDiem] = useState(ev.diaDiem ?? '')
  const [moTa, setMoTa] = useState(ev.moTa ?? '')
  const [paymentTerms, setPaymentTerms] = useState(ev.paymentTerms ?? '')

  // User 2026-05-07: re-trigger editing mode khi click pencil ở Panel 1 cho
  // PHIẾU KHÁC (ev.id thay đổi) hoặc autoEdit prop change. useState init chỉ
  // chạy mount-time → cần useEffect sync khi parent re-render với props mới.
  useEffect(() => {
    if (autoEdit && canEdit) {
      setEditing(true)
      // Sync values từ ev mới (tránh stale state khi switch giữa 2 phiếu)
      setTenGoiThau(ev.tenGoiThau)
      setDiaDiem(ev.diaDiem ?? '')
      setMoTa(ev.moTa ?? '')
      setPaymentTerms(ev.paymentTerms ?? '')
    }
  }, [autoEdit, canEdit, ev.id, ev.tenGoiThau, ev.diaDiem, ev.moTa, ev.paymentTerms])

  const dirty = tenGoiThau !== ev.tenGoiThau
    || diaDiem !== (ev.diaDiem ?? '')
    || moTa !== (ev.moTa ?? '')
    || paymentTerms !== (ev.paymentTerms ?? '')

  const save = useMutation({
    mutationFn: async () => {
      await api.put(`/purchase-evaluations/${ev.id}`, {
        id: ev.id,
        tenGoiThau,
        diaDiem: diaDiem || null,
        moTa: moTa || null,
        paymentTerms: paymentTerms || null,
        // S61 — module Budget cũ XÓA HẲN; PE giữ 2 ô ngân sách mới (echo lại
        // giá trị hiện tại để PUT update không xóa nhầm — drafter sửa qua bảng
        // TỔNG HỢP NGÂN SÁCH / PATCH budget-adjust).
        budgetPeriodAmount: ev.budgetPeriodAmount,
        expectedRemainingAmount: ev.expectedRemainingAmount,
      })
    },
    onSuccess: () => {
      toast.success('Đã cập nhật thông tin')
      qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
      qc.invalidateQueries({ queryKey: ['pe-list'] })
      setEditing(false)
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  function reset() {
    setTenGoiThau(ev.tenGoiThau)
    setDiaDiem(ev.diaDiem ?? '')
    setMoTa(ev.moTa ?? '')
    setPaymentTerms(ev.paymentTerms ?? '')
  }

  if (!editing) {
    return (
      <dl className="space-y-2 text-sm">
        <div className="flex items-start justify-between">
          <FormRow label="a. Tên gói thầu" value={ev.tenGoiThau} />
          {canEdit && (
            <button
              onClick={() => setEditing(true)}
              className="inline-flex items-center gap-1 rounded px-2 py-1 text-[11px] text-slate-500 hover:bg-slate-100 hover:text-brand-600"
              title="Sửa thông tin gói thầu"
            >
              <Pencil className="h-3 w-3" /> Sửa
            </button>
          )}
        </div>
        <FormRow label="b. Dự án" value={ev.projectName} />
        {/* S57bis — Hạng mục công việc (WorkItem master). Phiếu cũ null → "—". */}
        <FormRow label="c. Hạng mục công việc" value={ev.workItemName ? `${ev.workItemCode ? `${ev.workItemCode} — ` : ''}${ev.workItemName}` : '—'} />
        {(ev.diaDiem || ev.moTa || ev.paymentTerms) && (
          <div className="mt-3 rounded bg-slate-50 px-3 py-2 text-[12px] text-slate-600">
            {ev.diaDiem && <div><span className="text-slate-400">Địa điểm:</span> {ev.diaDiem}</div>}
            {ev.moTa && <div><span className="text-slate-400">Mô tả:</span> {ev.moTa}</div>}
            {ev.paymentTerms && <div><span className="text-slate-400">Điều khoản TT:</span> <span className="whitespace-pre-wrap">{ev.paymentTerms}</span></div>}
          </div>
        )}
      </dl>
    )
  }

  // Editing mode
  return (
    <div className="space-y-3 rounded border border-brand-200 bg-brand-50/30 p-3">
      <div className="grid gap-3 md:grid-cols-2">
        <div className="md:col-span-2">
          <Label className="text-[11px]">a. Tên gói thầu *</Label>
          <Input
            value={tenGoiThau}
            onChange={e => setTenGoiThau(e.target.value)}
            placeholder="vd Cung cấp bê tông"
          />
        </div>
        <div className="md:col-span-2">
          <Label className="text-[11px]">b. Dự án (khóa)</Label>
          <Input value={ev.projectName} disabled className="bg-slate-100" />
        </div>
        <div className="md:col-span-2">
          {/* S57bis — hạng mục khóa ở inline-edit; đổi qua "Sửa header phiếu" (PeHeaderForm). */}
          <Label className="text-[11px]">c. Hạng mục công việc (khóa)</Label>
          <Input value={ev.workItemName ? `${ev.workItemCode ? `${ev.workItemCode} — ` : ''}${ev.workItemName}` : '—'} disabled className="bg-slate-100" />
        </div>
        <div>
          <Label className="text-[11px]">Địa điểm</Label>
          <Input
            value={diaDiem}
            onChange={e => setDiaDiem(e.target.value)}
            placeholder="Lô K, KCN Lộc An..."
          />
        </div>
        <div>
          <Label className="text-[11px]">Mô tả ngắn</Label>
          <Input
            value={moTa}
            onChange={e => setMoTa(e.target.value)}
            placeholder="Phương án A: ..."
          />
        </div>
        {/* S59 vòng 5: field "Điều khoản thanh toán" GỠ khỏi inline-edit (anh chốt
            "bỏ nốt ra luôn tất cả các form"). State paymentTerms giữ — save giữ nguyên
            data cũ; phiếu đã nhập vẫn hiển thị read-only ở header info. */}
      </div>
      <div className="flex items-center justify-end gap-2">
        <Button
          variant="ghost"
          onClick={() => { reset(); setEditing(false) }}
          className="h-7 px-3 text-xs"
        >
          Hủy
        </Button>
        <Button
          onClick={() => save.mutate()}
          disabled={!dirty || !tenGoiThau || save.isPending}
          className="h-7 px-3 text-xs"
        >
          {save.isPending ? 'Đang lưu…' : 'Lưu'}
        </Button>
      </div>
    </div>
  )
}

// ===== a. NCC / TP được chọn — TÓM TẮT read-only [multi-NCC per hạng mục] =====
// Winner giờ CHỌN THEO TỪNG HẠNG MỤC ở bảng NCC (Mục 2) — nguồn sự thật = quote.isSelected
// mỗi cặp (hạng mục × NCC). Picker phiếu-wide cũ MẤT NGHĨA → hạ-cấp thành ô TÓM TẮT: mỗi
// đơn vị trúng + các hạng mục nó thắng (derive ev.details[].quotes IsSelected). Tổng =
// ev.winnerQuoteTotal (BE = SUM mọi quote IsSelected).
function NccSelectorRow({ ev }: { ev: PeDetailBundle }) {
  const winners = ev.suppliers.filter(s => s.isWinner)
  // Các hạng mục mà supplier-row `rowId` thắng (có ≥1 quote IsSelected trong hạng mục đó).
  const wonItems = (rowId: string) =>
    ev.details
      .filter(d => d.quotes.some(q => q.purchaseEvaluationSupplierId === rowId && q.isSelected))
      .map(d => d.groupCode || d.noiDung)
  return (
    <FormRow
      label="a. NCC / TP được chọn"
      value={winners.length > 0 ? (
        <div className="space-y-1">
          {winners.map(s => {
            const items = wonItems(s.id)
            return (
              <div key={s.id} className="text-[12px] leading-relaxed">
                <span className="font-medium text-emerald-700">✓ {s.supplierName}</span>
                {items.length > 0 && (
                  <span className="text-slate-500"> — thắng hạng mục: {items.join(', ')}</span>
                )}
              </div>
            )
          })}
          <div className="mt-1 flex items-center justify-between rounded bg-emerald-50 px-2 py-1 text-[12px]">
            <span className="text-emerald-700">Tổng chi phí đã chọn ({winners.length} đơn vị):</span>
            <span className="font-mono font-semibold text-emerald-800">{fmtMoney(ev.winnerQuoteTotal)} đ</span>
          </div>
          <div className="text-[11px] text-slate-400">
            Chọn NCC trúng thầu theo từng hạng mục ở Mục 2 (Hạng mục + Báo giá NCC).
          </div>
        </div>
      ) : (
        <span className="text-slate-400">— (chưa chọn — chọn theo từng hạng mục ở Mục 2)</span>
      )}
    />
  )
}

// ===== b. TỔNG HỢP NGÂN SÁCH TRÌNH KÝ (S61 — Excel anh Kiệt) =====
// Module Budget cũ XÓA HẲN → ngân sách gói thầu per (Dự án × Hạng mục) compute
// BE trả `ev.budgetSummary`. 2 block:
//   A. NGÂN SÁCH (gói thầu): full / ban hành lần đầu (CCM) / hiệu chỉnh (CCM) /
//      dự trù PRO + ghi chú (PRO) — editable theo capability flag canEditCcm/canEditPro.
//   B. THỰC HIỆN: 9 dòng công thức Excel — drafter nhập row3 (NS kỳ này) + row8
//      (giá trị thực hiện dự kiến còn lại) qua PATCH /budget-adjust.
// budgetSummary=null → phiếu cũ chưa gắn Hạng mục → banner nhắc gắn.

// fmtVnd: "1.234.567 đ". fmtPct: 1 chữ số thập phân, guard chia-0 (denom<=0 → null).
const fmtVnd = (v: number) => `${Math.round(v).toLocaleString('vi-VN')} đ`
const fmtVndSigned = (v: number) =>
  v < 0 ? `(${Math.round(Math.abs(v)).toLocaleString('vi-VN')}) đ` : `${Math.round(v).toLocaleString('vi-VN')} đ`
const fmtPct = (num: number, denom: number): string | null =>
  denom > 0 ? `${((num / denom) * 100).toFixed(1)}%` : null
// [C4b anh Kiệt FDC] Dòng "So sánh" = 0 (đề xuất ĐÚNG BẰNG ngân sách) → chữ thay "0 đ"
// cho đỡ khó hiểu. base>0 (có ngân sách thật) → "Bằng ngân sách"; base<=0 (phiếu trống) → "—".
const fmtCompareValue = (v: number, base: number): React.ReactNode =>
  v === 0
    ? <span className="font-sans text-[12px] font-normal text-slate-500">{base > 0 ? 'Bằng ngân sách' : '—'}</span>
    : <span className={cn(v < 0 && 'font-semibold text-red-600')}>{fmtVndSigned(v)}</span>

// Inline-edit số tiền VND (reuse formatVndInput/parseVnd module-level). allowNegative
// cho dòng "hiệu chỉnh tăng giảm" (CCM nhập số âm). onSave nhận number|null.
function VndInlineEdit({
  initial, allowNegative = false, onSave, saving, label, onLiveChange,
}: {
  initial: number | null
  allowNegative?: boolean
  onSave: (v: number | null) => void
  saving: boolean
  label?: string
  /** [C4a anh Kiệt FDC] báo giá trị ĐANG GÕ lên cha mỗi keystroke → live-recompute. */
  onLiveChange?: (v: number | null) => void
}) {
  const [text, setText] = useState(initial != null ? Math.abs(initial).toLocaleString('vi-VN') : '')
  const [neg, setNeg] = useState((initial ?? 0) < 0)
  const valueOf = (raw: string, isNeg: boolean): number | null => {
    const n = parseVnd(raw)
    if (n === 0 && raw.trim() === '') return null
    return allowNegative && isNeg ? -n : n
  }
  const parse = (): number | null => valueOf(text, neg)
  const dirty = parse() !== initial
  return (
    <div className="flex items-center justify-end gap-1.5">
      {allowNegative && (
        <button
          type="button"
          onClick={() => { const nv = !neg; setNeg(nv); onLiveChange?.(valueOf(text, nv)) }}
          className={cn(
            'h-6 w-6 shrink-0 rounded border text-xs font-bold',
            neg ? 'border-red-300 bg-red-50 text-red-600' : 'border-slate-300 text-slate-400',
          )}
          title="Đảo dấu âm/dương"
        >
          {neg ? '−' : '+'}
        </button>
      )}
      <div className="relative w-40">
        <Input
          type="text"
          inputMode="numeric"
          value={text}
          onChange={e => {
            setText(formatVndInput(parseVnd(e.target.value)))
            onLiveChange?.(valueOf(e.target.value, neg))
          }}
          placeholder="0"
          aria-label={label}
          className="h-7 pr-6 font-mono text-right text-[13px]"
        />
        <span className="pointer-events-none absolute inset-y-0 right-2 flex items-center text-[11px] font-medium text-slate-500">đ</span>
      </div>
      <Button
        onClick={() => onSave(parse())}
        disabled={!dirty || saving}
        className="h-7 px-2 text-[11px]"
      >
        {saving ? '…' : 'Lưu'}
      </Button>
    </div>
  )
}

// [Mig 59] BudgetRow (1-cột flex) GỠ — Section B nay là <table> 3 cột (DỰ ÁN|PRO|CCM)
// mirror Section A. Dòng/ô dùng BudgetSharedNumCell / BudgetColValue / BudgetColCompareCell.

// Block tiêu đề (A / B)
function BudgetBlockHeader({ children }: { children: React.ReactNode }) {
  return (
    <div className="border-b border-slate-200 bg-slate-100 px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-600">
      {children}
    </div>
  )
}

// [S76] Ô tiền compact cho ma trận 3 cột — editable (input + nút Lưu) hoặc display.
// allowNegative cho "hiệu chỉnh tăng giảm" (nút ± đảo dấu). onSave nhận number|null.
function BudgetCell({ value, editable, allowNegative = false, saving, onSave }: {
  value: number | null
  editable: boolean
  allowNegative?: boolean
  saving: boolean
  onSave: (v: number | null) => void
}) {
  const [text, setText] = useState(value != null ? Math.abs(value).toLocaleString('vi-VN') : '')
  const [neg, setNeg] = useState((value ?? 0) < 0)
  useEffect(() => {
    setText(value != null ? Math.abs(value).toLocaleString('vi-VN') : '')
    setNeg((value ?? 0) < 0)
  }, [value])
  if (!editable) {
    return value != null
      ? <span className={cn('font-mono text-[13px] tabular-nums', value < 0 && 'text-red-600')}>{fmtVndSigned(value)}</span>
      : <span className="text-slate-300">—</span>
  }
  const parse = (): number | null => {
    const n = parseVnd(text)
    if (n === 0 && text.trim() === '') return null
    return allowNegative && neg ? -n : n
  }
  const dirty = parse() !== value
  return (
    <div className="space-y-1">
      <div className="flex items-center gap-1">
        {allowNegative && (
          <button
            type="button"
            onClick={() => setNeg(v => !v)}
            className={cn('h-7 w-7 shrink-0 rounded border text-[11px] font-bold',
              neg ? 'border-red-300 bg-red-50 text-red-600' : 'border-slate-300 text-slate-400')}
            title="Đảo dấu âm/dương"
          >
            {neg ? '−' : '+'}
          </button>
        )}
        <Input
          type="text"
          inputMode="numeric"
          value={text}
          onChange={e => setText(formatVndInput(parseVnd(e.target.value)))}
          placeholder="0"
          className="h-7 min-w-0 flex-1 px-1.5 text-right font-mono text-[12px]"
        />
      </div>
      <div className="flex justify-end">
        <Button onClick={() => onSave(parse())} disabled={!dirty || saving} className="h-6 px-2 text-[10px]">
          {saving ? '…' : 'Lưu'}
        </Button>
      </div>
    </div>
  )
}

// [S76] Ô ghi chú phòng (Ghi chú từ PRO / từ CCM) — Textarea editable hoặc text display.
// Đặt trong <td colSpan> của bảng ngân sách (không bọc row riêng).
function BudgetNoteCell({ editable, value, setValue, savedValue, saving, onSave }: {
  editable: boolean
  value: string
  setValue: (v: string) => void
  savedValue: string | null
  saving: boolean
  onSave: () => void
}) {
  if (!editable) {
    return (
      <div className="whitespace-pre-wrap px-2 py-1.5 text-[12px] text-slate-600">
        {savedValue || <span className="text-slate-400">—</span>}
      </div>
    )
  }
  return (
    <div className="space-y-1 px-2 py-1.5">
      <textarea
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="Ghi chú…"
        rows={2}
        className="w-full rounded border border-slate-300 px-2 py-1 text-[12px]"
      />
      <div className="flex justify-end">
        <Button onClick={onSave} disabled={value === (savedValue ?? '') || saving} className="h-6 px-2 text-[11px]">
          {saving ? '…' : 'Lưu ghi chú'}
        </Button>
      </div>
    </div>
  )
}

// [Mig 59] Ô số CHUNG mọi cột (Section B THỰC HIỆN) — 1 giá trị span hết 3 cột Dự án/PRO/CCM
// (dòng 1/2/6 không tách cột). value null → "Chưa chọn".
function BudgetSharedNumCell({ value, colSpan, sub }: { value: number | null; colSpan: number; sub?: ReactNode }) {
  return (
    <td className="border border-slate-300 px-2 py-1.5 text-right font-mono tabular-nums text-slate-900" colSpan={colSpan}>
      {value == null
        ? <span className="font-sans text-slate-400">Chưa chọn</span>
        : <span className={cn(value < 0 && 'font-semibold text-red-600')}>{fmtVnd(value)}</span>}
      {sub}
    </td>
  )
}

// [Mig 59] Ô giá trị 1 cột (PRO/CCM) Section B — display read-only. value null → "—".
// signed → fmtVndSigned (âm trong ngoặc). pct → dòng % phụ dưới. asCell bọc <td> sẵn
// (cho dòng 5/6/7/8 ccm); KHÔNG asCell → chỉ nội dung (nhúng vào <td> editable dòng 3).
function BudgetColValue({ value, pct, signed = false, asCell = false, sub }: {
  value: number | null; pct?: string | null; signed?: boolean; asCell?: boolean; sub?: ReactNode
}) {
  const body = value == null
    ? <span className="text-slate-300">—</span>
    : (
      <>
        <span className={cn('font-mono text-[13px] tabular-nums', value < 0 ? 'font-semibold text-red-600' : 'text-slate-900')}>
          {signed ? fmtVndSigned(value) : fmtVnd(value)}
        </span>
        {pct && <div className="text-[10px] text-slate-400">{pct}</div>}
      </>
    )
  return asCell
    ? <td className="border border-slate-300 px-2 py-1.5 text-right align-top">{body}{sub}</td>
    : <>{body}{sub}</>
}

// [Mig 59] Ô "So sánh" 1 cột (PRO/CCM) Section B — = 0 → text "Bằng ngân sách" (reuse
// fmtCompareValue); âm → đỏ. pct dòng phụ. Luôn bọc <td>.
function BudgetColCompareCell({ value, base, pct }: { value: number; base: number; pct: string | null }) {
  return (
    <td className={cn('border border-slate-300 px-2 py-1.5 text-right align-top font-mono tabular-nums', value < 0 && 'font-semibold text-red-600')}>
      {fmtCompareValue(value, base)}
      {pct && <div className="text-[10px] font-sans font-normal text-slate-400">{pct}</div>}
    </td>
  )
}

function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
  const qc = useQueryClient()
  const bs = ev.budgetSummary
  // [S76] Khoá nút Lưu trong lúc pe-detail đang refetch (sau mỗi save) — đóng cửa-sổ
  // stale-echo: tránh lưu 1 ô khi bs (server snapshot) chưa cập nhật → đè field anh-em
  // (vd lưu PRO ban hành xong, lưu PRO hiệu chỉnh ngay sẽ echo bs.proInitial CŨ).
  const peFetching = useIsFetching({ queryKey: ['pe-detail', ev.id] }) > 0

  // Drafter nhập được row3 (NS kỳ này) + row8 (giá trị thực hiện dự kiến còn lại)
  // khi phiếu DangSoanThao/TraLai + !readOnly. Mirror predicate row3/row8 spec.
  const drafterEditable = !readOnly && isEditablePhase(ev.phase)

  const invalidate = () => {
    qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
    qc.invalidateQueries({ queryKey: ['pe-list'] })
  }

  // PUT /budget/pro — chỉ khi canEditPro. [S76] proInitial + proAdjust + proNote.
  const proMut = useMutation({
    mutationFn: async (body: { proInitialAmount: number | null; proAdjustmentAmount: number | null; proNote: string | null }) =>
      api.put(`/purchase-evaluations/${ev.id}/budget/pro`, body),
    onSuccess: () => { toast.success('Đã lưu ngân sách PRO'); invalidate() },
    onError: e => toast.error(getErrorMessage(e)),
  })
  // PUT /budget/ccm — chỉ khi canEditCcm. initialAmount + adjustmentAmount.
  const ccmMut = useMutation({
    mutationFn: async (body: { initialAmount: number | null; adjustmentAmount: number | null; ccmNote: string | null }) =>
      api.put(`/purchase-evaluations/${ev.id}/budget/ccm`, body),
    onSuccess: () => { toast.success('Đã lưu ngân sách ban hành'); invalidate() },
    onError: e => toast.error(getErrorMessage(e)),
  })
  // PATCH /budget-adjust — ABSOLUTE-SET: BE set thẳng CẢ 2 field (thiếu field =
  // null = CLEAR). Mọi call-site PHẢI gửi đủ cặp {budgetPeriodAmount,
  // expectedRemainingAmount} (field không đổi → echo giá trị hiện tại từ ev).
  const adjustMut = useMutation({
    mutationFn: async (body: { budgetPeriodAmount?: number | null; expectedRemainingAmount?: number | null }) =>
      api.patch(`/purchase-evaluations/${ev.id}/budget-adjust`, body),
    onSuccess: () => { toast.success('Đã lưu'); invalidate() },
    onError: e => toast.error(getErrorMessage(e)),
  })
  // [Mig 59 anh Kiệt FDC / CCM] PATCH /budget/ccm-period — "NS kỳ này" cột CCM (Section B
  // tách 3 cột). Role-gate Admin|CostControl (BE Forbidden nếu khác). Absolute-set (null=
  // clear). Gate FE qua bs.canEditCcm (BE-computed capability, mirror ô NS Block A).
  const ccmPeriodMut = useMutation({
    mutationFn: async (body: { ccmBudgetPeriodAmount: number | null }) =>
      api.patch(`/purchase-evaluations/${ev.id}/budget/ccm-period`, body),
    onSuccess: () => { toast.success('Đã lưu NS kỳ này (CCM)'); invalidate() },
    onError: e => toast.error(getErrorMessage(e)),
  })

  // proNote inline-edit state (Textarea — không dùng VndInlineEdit)
  const [proNoteText, setProNoteText] = useState(bs?.proNote ?? '')
  useEffect(() => { setProNoteText(bs?.proNote ?? '') }, [bs?.proNote])
  // ccmNote inline-edit state (mirror proNoteText) — [Mig anh Kiệt FDC]
  const [ccmNoteText, setCcmNoteText] = useState(bs?.ccmNote ?? '')
  useEffect(() => { setCcmNoteText(bs?.ccmNote ?? '') }, [bs?.ccmNote])

  // [C4a anh Kiệt FDC] Live-recompute: giữ giá trị ĐANG GÕ của ô 3 (NS kỳ này) + ô 8 (giá
  // trị TH dự kiến còn lại) ở state cục bộ → dòng 5/6/7/9 + So sánh + % nhảy NGAY khi gõ
  // (chưa cần bấm Lưu). Sync lại từ server (ev.*) sau mỗi save/refetch.
  // [S85 D3 anh Kiệt] draftRow3 default ← "Ngân sách Ban hành lần đầu" (bs.proInitialAmount)
  // khi chưa nhập tay budgetPeriodAmount → row3 "tự nhảy". bs có thể null (guard !bs dưới) → optional.
  const [draftRow3, setDraftRow3] = useState<number | null>(ev.budgetPeriodAmount ?? bs?.proInitialAmount ?? null)
  const [draftRow8, setDraftRow8] = useState<number | null>(ev.expectedRemainingAmount)
  useEffect(() => { setDraftRow3(ev.budgetPeriodAmount ?? bs?.proInitialAmount ?? null) }, [ev.budgetPeriodAmount, bs?.proInitialAmount])
  useEffect(() => { setDraftRow8(ev.expectedRemainingAmount) }, [ev.expectedRemainingAmount])
  // [Mig 59] Live-recompute cột CCM: giữ "NS kỳ này (CCM)" đang gõ → row5/7/9 + So sánh
  // cột CCM nhảy ngay. Default ← bs.initialAmount (NS Ban hành lần đầu CCM) khi chưa nhập.
  const [draftCcmRow3, setDraftCcmRow3] = useState<number | null>(ev.ccmBudgetPeriodAmount ?? bs?.initialAmount ?? null)
  useEffect(() => { setDraftCcmRow3(ev.ccmBudgetPeriodAmount ?? bs?.initialAmount ?? null) }, [ev.ccmBudgetPeriodAmount, bs?.initialAmount])

  // Phiếu cũ chưa gắn Hạng mục công việc → budgetSummary null.
  if (!bs) {
    return (
      <div className="rounded border border-amber-200 bg-amber-50 px-3 py-2.5 text-[12px] text-amber-800">
        Phiếu chưa gắn Hạng mục công việc — gắn Hạng mục để dùng ngân sách gói thầu.
      </div>
    )
  }

  // ===== Số liệu Excel =====
  // [Mig 59] Bỏ `full` đơn-cột (bs.fullAmount cũ) — Section A+B nay tách 3 cột, mỗi cột
  // dùng full RIÊNG = ban hành + hiệu chỉnh của cột đó (proFull / ccmFull dưới).
  const proFull = (bs.proInitialAmount ?? 0) + (bs.proAdjustmentAmount ?? 0)
  const ccmFull = (bs.initialAmount ?? 0) + (bs.adjustmentAmount ?? 0)
  // Cột "có dữ liệu" = đã nhập ban hành HOẶC hiệu chỉnh → hiện full (kể cả 0/âm); else "—".
  const proHasData = bs.proInitialAmount != null || bs.proAdjustmentAmount != null
  const ccmHasData = bs.initialAmount != null || bs.adjustmentAmount != null
  // ===== Số THỰC-TẾ dùng CHUNG mọi cột (KHÔNG per-cột) =====
  const row1 = bs.previousSubmittedTotal        // Ngân sách trình duyệt trước
  const row2 = bs.previousSelectedTotal         // Kỳ trước đã chọn thầu
  const row4 = bs.currentProposalTotal          // Giá trị kỳ này (đề xuất NCC được chọn)
  const row6 = row2 + row4                       // Lũy kế thực hiện (= 2 + 4)

  // [S134 anh Kiệt FDC] Lũy kế TẠM TÍNH — phần góp từ phiếu cùng gói thầu CHƯA duyệt
  // (?? guard data cũ chưa có field). Chỉ hiện khi phiếu CHƯA freeze (số DaDuyet là
  // snapshot bất biến S133 — KHÔNG thêm tạm-tính). N8: row1/5 dùng pendingSubmitted*,
  // row2/6 dùng pendingSelected*.
  const pendingSubmittedTotal = bs.pendingSubmittedTotal ?? 0
  const pendingSubmittedCount = bs.pendingSubmittedCount ?? 0
  const pendingSelectedTotal = bs.pendingSelectedTotal ?? 0
  const pendingSelectedCount = bs.pendingSelectedCount ?? 0
  const pendingPriorPes = bs.pendingPriorPes ?? []
  const showPending = !bs.budgetFrozen && (pendingSubmittedCount > 0 || pendingSelectedCount > 0 || pendingPriorPes.length > 0)
  // Sub-dòng amber "Tạm tính" — LUÔN kèm chữ "Tạm tính", KHÔNG bao giờ thay số chính xác.
  const pendingSub = (base: number, delta: number, count: number, withDetail: boolean) => (
    <div className="mt-0.5 text-[10px] font-sans italic text-amber-700">
      Tạm tính: {fmtVnd(base + delta)}
      {withDetail && ` (+${fmtVnd(delta)} từ ${count} phiếu chưa duyệt)`}
    </div>
  )

  // [Mig 59] Mỗi cột tính ĐỘC-LẬP theo "NS kỳ này" + "full" của CỘT đó. Helper trả về
  // bộ row3/5/7/8/9 + 3 so-sánh cho 1 cột. periodAmount = NS kỳ này (live); colFull =
  // full cột; remaining = giá trị TH dự kiến còn lại (null → default row7).
  const colCalc = (periodAmount: number | null, colFull: number, remaining: number | null) => {
    const r3 = periodAmount ?? 0
    const r5 = row1 + r3                  // Lũy kế NS đã dùng (= 1 + 3 cột)
    const r7 = colFull - r5               // NS còn lại (= full cột − 5 cột)
    const r8 = remaining ?? r7            // Giá trị TH dự kiến còn lại (default = 7 cột)
    const r9 = row4 + r8                  // Giá trị tổng TH dự kiến (= 4 + 8 cột)
    return {
      r3, r5, r7, r8, r9,
      cmpPeriod: r3 - row4,              // So với NS kỳ này (= 3 cột − 4)
      cmp56: r5 - row6,                  // So với NS (= 5 cột − 6)
      cmpFull: colFull - r9,             // So với NS full (= full cột − 9 cột)
    }
  }
  // Cột PRO: kỳ này = budgetPeriodAmount (live draftRow3) · full = proFull · row8 editable
  // (draftRow8 ↔ expectedRemainingAmount, drafter). Cột CCM: kỳ này = ccmBudgetPeriodAmount
  // (live draftCcmRow3) · full = ccmFull · row8 = default (chưa có field CCM-remaining riêng).
  const pro = colCalc(draftRow3, proFull, draftRow8)
  const ccm = colCalc(draftCcmRow3, ccmFull, null)

  // Cờ tô màu cảnh báo (cảnh báo MỀM, dựa cột PRO — luồng drafter chính). Hiện banner khi
  // PRO HOẶC CCM vượt (đề xuất > NS kỳ này, hoặc tổng TH > NS full).
  const proposalOver = bs.currentProposalTotal > (draftRow3 ?? 0) && draftRow3 != null
  const remainingOver = draftRow8 != null && draftRow8 > pro.r7
  const anyOver = pro.cmpPeriod < 0 || pro.cmpFull < 0 || ccm.cmpPeriod < 0 || ccm.cmpFull < 0

  return (
    <div className="overflow-hidden rounded-lg border border-slate-300">
      <div className="flex items-center justify-between gap-2 bg-[#1F7DC1] px-3 py-2 text-[12px] font-bold uppercase tracking-wide text-white">
        <span>Tổng hợp ngân sách trình ký</span>
        {bs.budgetFrozen && (
          <span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-medium normal-case tracking-normal text-amber-800">
            🔒 Ngân sách chốt tại thời điểm duyệt
          </span>
        )}
      </div>

      {/* [S62] Cảnh báo MỀM "vượt ngân sách" (anh Kiệt FDC) — KHÔNG chặn lưu, chỉ báo.
          Hiện khi đề xuất kỳ này > NS kỳ này (cmpPeriod<0) hoặc tổng thực hiện > NS full
          (cmpFull<0) ở BẤT KỲ cột PRO/CCM. Số dư còn lại âm vẫn lưu + gửi duyệt được. */}
      {anyOver && (
        <div className="flex items-start gap-2 border-b border-amber-300 bg-amber-50 px-3 py-2 text-[12px] text-amber-800">
          <span className="shrink-0 text-sm leading-none">⚠️</span>
          <span>
            <strong>Vượt ngân sách</strong> — giá trị đề xuất NCC đang cao hơn ngân sách của gói thầu.
            Phiếu <strong>vẫn lưu &amp; gửi duyệt được</strong>, vui lòng kiểm tra lại số liệu trước khi trình.
          </span>
        </div>
      )}

      {/* ===== Block A — NGÂN SÁCH gói thầu: BẢNG LƯỚI 3 cột (DỰ ÁN | PRO | CCM) =====
          [S76 anh Kiệt FDC] <table> viền ô đầy đủ giống file Excel. Mỗi phòng nhập+điều
          chỉnh cột CHÍNH mình (role-gate): PRO→cột PRO (canEditPro) · CCM→cột CCM
          (canEditCcm) · DỰ ÁN hiển-thị-only (—, sau có người dự án nhập). Full mỗi cột
          = ban hành + hiệu chỉnh. */}
      <BudgetBlockHeader>A. Ngân sách (gói thầu / gói vật tư)</BudgetBlockHeader>

      <div className="overflow-x-auto">
        <table className="w-full table-fixed border-collapse text-[13px]">
          <thead>
            <tr className="bg-slate-100 text-[11px] font-semibold uppercase tracking-wide text-slate-600">
              <th className="border border-slate-300 px-3 py-1.5 text-left">Khoản mục</th>
              <th className="w-20 border border-slate-300 px-2 py-1.5 text-center">Dự án</th>
              <th className="w-44 border border-slate-300 px-2 py-1.5 text-center">PRO</th>
              <th className="w-44 border border-slate-300 px-2 py-1.5 text-center">CCM</th>
            </tr>
          </thead>
          <tbody>
            {/* Ngân sách full (= ban hành + hiệu chỉnh) mỗi cột */}
            <tr className="bg-[#1F7DC1]/10 font-semibold">
              <td className="border border-slate-300 px-3 py-2 text-slate-800">Ngân sách (full gói thầu / gói vật tư)</td>
              <td className="border border-slate-300 px-2 py-2 text-right text-slate-300">—</td>
              <td className="border border-slate-300 px-2 py-2 text-right font-mono font-bold tabular-nums text-slate-900">
                {proHasData ? fmtVnd(proFull) : <span className="text-slate-300">—</span>}
              </td>
              <td className="border border-slate-300 px-2 py-2 text-right font-mono font-bold tabular-nums text-slate-900">
                {ccmHasData ? fmtVnd(ccmFull) : <span className="text-slate-300">—</span>}
              </td>
            </tr>
            {/* Ban hành lần đầu */}
            <tr>
              <td className="border border-slate-300 px-3 py-1.5 pl-8 text-slate-700">– Ngân sách Ban hành lần đầu</td>
              <td className="border border-slate-300 px-2 py-1.5 text-right text-slate-300">—</td>
              <td className="border border-slate-300 px-1.5 py-1.5 text-right align-top">
                <BudgetCell
                  value={bs.proInitialAmount}
                  editable={bs.canEditPro}
                  saving={proMut.isPending || peFetching}
                  onSave={v => proMut.mutate({ proInitialAmount: v, proAdjustmentAmount: bs.proAdjustmentAmount, proNote: bs.proNote })}
                />
              </td>
              <td className="border border-slate-300 px-1.5 py-1.5 text-right align-top">
                <BudgetCell
                  value={bs.initialAmount}
                  editable={bs.canEditCcm}
                  saving={ccmMut.isPending || peFetching}
                  onSave={v => ccmMut.mutate({ initialAmount: v, adjustmentAmount: bs.adjustmentAmount, ccmNote: bs.ccmNote })}
                />
              </td>
            </tr>
            {/* V0 / hiệu chỉnh tăng giảm (cho phép ÂM) */}
            <tr>
              <td className="border border-slate-300 px-3 py-1.5 pl-8 text-slate-700">– Ngân sách V0 / hiệu chỉnh tăng giảm</td>
              <td className="border border-slate-300 px-2 py-1.5 text-right text-slate-300">—</td>
              <td className="border border-slate-300 px-1.5 py-1.5 text-right align-top">
                <BudgetCell
                  value={bs.proAdjustmentAmount}
                  editable={bs.canEditPro}
                  allowNegative
                  saving={proMut.isPending || peFetching}
                  onSave={v => proMut.mutate({ proInitialAmount: bs.proInitialAmount, proAdjustmentAmount: v, proNote: bs.proNote })}
                />
              </td>
              <td className="border border-slate-300 px-1.5 py-1.5 text-right align-top">
                <BudgetCell
                  value={bs.adjustmentAmount}
                  editable={bs.canEditCcm}
                  allowNegative
                  saving={ccmMut.isPending || peFetching}
                  onSave={v => ccmMut.mutate({ initialAmount: bs.initialAmount, adjustmentAmount: v, ccmNote: bs.ccmNote })}
                />
              </td>
            </tr>
            {/* Ghi chú từ PRO (span 3 cột giá trị) */}
            <tr>
              <td className="border border-slate-300 px-3 py-1.5 align-top text-slate-700">Ghi chú từ PRO</td>
              <td className="border border-slate-300" colSpan={3}>
                <BudgetNoteCell
                  editable={bs.canEditPro}
                  value={proNoteText}
                  setValue={setProNoteText}
                  savedValue={bs.proNote}
                  saving={proMut.isPending || peFetching}
                  onSave={() => proMut.mutate({ proInitialAmount: bs.proInitialAmount, proAdjustmentAmount: bs.proAdjustmentAmount, proNote: proNoteText || null })}
                />
              </td>
            </tr>
            {/* Ghi chú từ CCM (span 3 cột giá trị) */}
            <tr>
              <td className="border border-slate-300 px-3 py-1.5 align-top text-slate-700">Ghi chú từ CCM</td>
              <td className="border border-slate-300" colSpan={3}>
                <BudgetNoteCell
                  editable={bs.canEditCcm}
                  value={ccmNoteText}
                  setValue={setCcmNoteText}
                  savedValue={bs.ccmNote}
                  saving={ccmMut.isPending || peFetching}
                  onSave={() => ccmMut.mutate({ initialAmount: bs.initialAmount, adjustmentAmount: bs.adjustmentAmount, ccmNote: ccmNoteText || null })}
                />
              </td>
            </tr>
          </tbody>
        </table>
      </div>

      {/* ===== Block B — THỰC HIỆN: BẢNG LƯỚI 3 cột (DỰ ÁN | PRO | CCM) =====
          [Mig 59 anh Kiệt FDC / CCM] Mirror style Block A. Mỗi cột tính ĐỘC-LẬP theo "NS kỳ
          này" + "full" của cột đó (pro/ccm = colCalc). Số thực-tế (1/2/4/6) dùng CHUNG mọi
          cột. "NS kỳ này": PRO editable drafter (VndInlineEdit + /budget-adjust hiện có) ·
          CCM editable canEditCcm (VndInlineEdit + PATCH /budget/ccm-period) · DỰ ÁN "—". */}
      <BudgetBlockHeader>B. Thực hiện</BudgetBlockHeader>

      <div className="overflow-x-auto">
        <table className="w-full table-fixed border-collapse text-[13px]">
          <thead>
            <tr className="bg-slate-100 text-[11px] font-semibold uppercase tracking-wide text-slate-600">
              <th className="border border-slate-300 px-3 py-1.5 text-left">Khoản mục</th>
              <th className="w-20 border border-slate-300 px-2 py-1.5 text-center">Dự án</th>
              <th className="w-44 border border-slate-300 px-2 py-1.5 text-center">PRO</th>
              <th className="w-44 border border-slate-300 px-2 py-1.5 text-center">CCM</th>
            </tr>
          </thead>
          <tbody>
            {/* 1 — Ngân sách trình duyệt trước (CHUNG) */}
            <tr>
              <td className="border border-slate-300 px-3 py-1.5 text-slate-700">1. Ngân sách trình duyệt trước</td>
              <BudgetSharedNumCell colSpan={3} value={bs.previousSubmittedCount === 0 ? null : row1}
                sub={showPending && pendingSubmittedCount > 0 ? pendingSub(row1, pendingSubmittedTotal, pendingSubmittedCount, true) : undefined} />
            </tr>
            {/* 2 — Kỳ trước đã chọn thầu (CHUNG) */}
            <tr>
              <td className="border border-slate-300 px-3 py-1.5 text-slate-700">2. Kỳ trước đã chọn thầu</td>
              <BudgetSharedNumCell colSpan={3} value={bs.previousSelectedCount === 0 ? null : row2}
                sub={showPending && pendingSelectedCount > 0 ? pendingSub(row2, pendingSelectedTotal, pendingSelectedCount, true) : undefined} />
            </tr>
            {/* 3 — Ngân sách kỳ này (PER-cột, editable PRO/CCM) + % /full cột */}
            <tr>
              <td className="border border-slate-300 px-3 py-1.5 align-top text-slate-700">
                3. Ngân sách - kỳ này
                <div className="text-[10px] text-slate-400">% so với NS full của cột</div>
              </td>
              <td className="border border-slate-300 px-2 py-1.5 text-right align-middle text-slate-300">—</td>
              {/* PRO — drafter giữ luồng /budget-adjust hiện có */}
              <td className="border border-slate-300 px-1.5 py-1.5 text-right align-top">
                {drafterEditable ? (
                  <VndInlineEdit
                    initial={ev.budgetPeriodAmount ?? bs.proInitialAmount}
                    saving={adjustMut.isPending || peFetching}
                    label="Ngân sách kỳ này (PRO)"
                    onLiveChange={setDraftRow3}
                    onSave={v => adjustMut.mutate({ budgetPeriodAmount: v, expectedRemainingAmount: ev.expectedRemainingAmount })}
                  />
                ) : (
                  <BudgetColValue value={ev.budgetPeriodAmount != null ? pro.r3 : null} pct={fmtPct(pro.r3, proFull)} />
                )}
                {drafterEditable && <div className="mt-0.5 text-[10px] text-slate-400">{fmtPct(pro.r3, proFull) ?? ''}</div>}
              </td>
              {/* CCM — endpoint MỚI /budget/ccm-period, gate canEditCcm */}
              <td className="border border-slate-300 px-1.5 py-1.5 text-right align-top">
                {bs.canEditCcm ? (
                  <VndInlineEdit
                    initial={ev.ccmBudgetPeriodAmount ?? bs.initialAmount}
                    saving={ccmPeriodMut.isPending || peFetching}
                    label="Ngân sách kỳ này (CCM)"
                    onLiveChange={setDraftCcmRow3}
                    onSave={v => ccmPeriodMut.mutate({ ccmBudgetPeriodAmount: v })}
                  />
                ) : (
                  <BudgetColValue value={ev.ccmBudgetPeriodAmount != null ? ccm.r3 : null} pct={fmtPct(ccm.r3, ccmFull)} />
                )}
                {bs.canEditCcm && <div className="mt-0.5 text-[10px] text-slate-400">{fmtPct(ccm.r3, ccmFull) ?? ''}</div>}
              </td>
            </tr>
            {/* 4 — Đề xuất kỳ này (CHUNG): NCC + giá trị + so sánh per-cột */}
            <tr className="bg-blue-50">
              <td className="border border-slate-300 px-3 py-1.5 text-slate-700">4. Đề xuất kỳ này — Tên thầu phụ / NCC</td>
              <td className="border border-slate-300 px-2 py-1.5 text-left font-sans text-slate-700" colSpan={3}>
                {ev.suppliers.some(s => s.isWinner)
                  ? ev.suppliers.filter(s => s.isWinner).map(s => s.supplierName).join(', ')
                  : <span className="text-slate-400">— (chưa chọn)</span>}
              </td>
            </tr>
            <tr className="bg-blue-50">
              <td className="border border-slate-300 px-3 py-1.5 pl-6 text-slate-700"><span className="mr-1 text-slate-400">–</span>Giá trị kỳ này (CHUNG)</td>
              <td className="border border-slate-300 px-2 py-1.5 text-right font-mono tabular-nums" colSpan={3}>
                {proposalOver
                  ? <span className="inline-block rounded bg-[#C00000] px-2 py-0.5 font-bold text-white">{fmtVnd(row4)}</span>
                  : fmtVnd(row4)}
              </td>
            </tr>
            <tr className="bg-blue-50">
              <td className="border border-slate-300 px-3 py-1.5 pl-6 align-top text-slate-700">
                <span className="mr-1 text-slate-400">–</span>So sánh với ngân sách kỳ này
                <div className="text-[10px] text-slate-400">= 3 − 4</div>
              </td>
              <td className="border border-slate-300 px-2 py-1.5 text-right align-middle text-slate-300">—</td>
              <BudgetColCompareCell value={pro.cmpPeriod} base={pro.r3} pct={fmtPct(pro.cmpPeriod, pro.r3)} />
              <BudgetColCompareCell value={ccm.cmpPeriod} base={ccm.r3} pct={fmtPct(ccm.cmpPeriod, ccm.r3)} />
            </tr>
            {/* 5 — Lũy kế ngân sách đã sử dụng (PER-cột = 1 + 3) */}
            <tr>
              <td className="border border-slate-300 px-3 py-1.5 align-top text-slate-700">
                5. Lũy kế ngân sách đã sử dụng
                <div className="text-[10px] text-slate-400">= 1 + 3</div>
              </td>
              <td className="border border-slate-300 px-2 py-1.5 text-right align-middle text-slate-300">—</td>
              <BudgetColValue asCell value={pro.r5}
                sub={showPending && pendingSubmittedCount > 0 ? pendingSub(pro.r5, pendingSubmittedTotal, pendingSubmittedCount, false) : undefined} />
              <BudgetColValue asCell value={ccm.r5}
                sub={showPending && pendingSubmittedCount > 0 ? pendingSub(ccm.r5, pendingSubmittedTotal, pendingSubmittedCount, false) : undefined} />
            </tr>
            {/* 6 — Lũy kế thực hiện (CHUNG = 2 + 4) */}
            <tr>
              <td className="border border-slate-300 px-3 py-1.5 align-top text-slate-700">
                6. Lũy kế thực hiện
                <div className="text-[10px] text-slate-400">= 2 + 4</div>
              </td>
              <BudgetSharedNumCell colSpan={3} value={row6}
                sub={showPending && pendingSelectedCount > 0 ? pendingSub(row6, pendingSelectedTotal, pendingSelectedCount, false) : undefined} />
            </tr>
            <tr>
              <td className="border border-slate-300 px-3 py-1.5 pl-6 align-top text-slate-700">
                <span className="mr-1 text-slate-400">–</span>So với NS
                <div className="text-[10px] text-slate-400">= 5 − 6</div>
              </td>
              <td className="border border-slate-300 px-2 py-1.5 text-right align-middle text-slate-300">—</td>
              <BudgetColCompareCell value={pro.cmp56} base={pro.r5} pct={fmtPct(pro.cmp56, pro.r5)} />
              <BudgetColCompareCell value={ccm.cmp56} base={ccm.r5} pct={fmtPct(ccm.cmp56, ccm.r5)} />
            </tr>
            {/* 7 — Ngân sách còn lại (PER-cột = full − 5) + % /full cột */}
            <tr>
              <td className="border border-slate-300 px-3 py-1.5 align-top text-slate-700">
                7. Ngân sách còn lại
                <div className="text-[10px] text-slate-400">= NS full − 5</div>
              </td>
              <td className="border border-slate-300 px-2 py-1.5 text-right align-middle text-slate-300">—</td>
              <BudgetColValue asCell signed value={pro.r7} pct={fmtPct(pro.r7, proFull)} />
              <BudgetColValue asCell signed value={ccm.r7} pct={fmtPct(ccm.r7, ccmFull)} />
            </tr>
            {/* 8 — Giá trị thực hiện dự kiến còn lại — PRO editable (drafter); CCM default */}
            <tr className={cn(remainingOver && 'bg-red-50')}>
              <td className={cn('border border-slate-300 px-3 py-1.5 align-top', remainingOver ? 'text-red-700' : 'text-slate-700')}>
                8. Giá trị thực hiện dự kiến còn lại
                <div className="text-[10px] text-slate-400">mặc định = 7 nếu chưa nhập</div>
              </td>
              <td className="border border-slate-300 px-2 py-1.5 text-right align-middle text-slate-300">—</td>
              <td className="border border-slate-300 px-1.5 py-1.5 text-right align-top">
                {drafterEditable ? (
                  <VndInlineEdit
                    initial={ev.expectedRemainingAmount ?? pro.r7}
                    allowNegative
                    saving={adjustMut.isPending || peFetching}
                    label="Giá trị thực hiện dự kiến còn lại"
                    onLiveChange={setDraftRow8}
                    onSave={v => adjustMut.mutate({ budgetPeriodAmount: ev.budgetPeriodAmount, expectedRemainingAmount: v })}
                  />
                ) : (
                  <span className={cn('font-mono text-[13px] tabular-nums', pro.r8 < 0 ? 'font-semibold text-red-600' : remainingOver ? 'font-semibold text-red-700' : 'text-slate-900')}>
                    {fmtVndSigned(pro.r8)}
                  </span>
                )}
              </td>
              <BudgetColValue asCell signed value={ccm.r8} />
            </tr>
            {/* 9 — Giá trị tổng thực hiện dự kiến (PER-cột = 4 + 8) — brand đậm */}
            <tr className="bg-[#1F7DC1] font-semibold text-white">
              <td className="border border-slate-300 px-3 py-1.5 align-top">
                9. Giá trị tổng thực hiện dự kiến
                <div className="text-[10px] text-white/70">= 4 + 8</div>
              </td>
              <td className="border border-slate-300 px-2 py-1.5 text-right align-middle text-white/60">—</td>
              <td className="border border-slate-300 px-2 py-1.5 text-right font-mono font-bold tabular-nums align-middle">
                <span className={cn(pro.r9 < 0 && 'text-red-200')}>{fmtVndSigned(pro.r9)}</span>
              </td>
              <td className="border border-slate-300 px-2 py-1.5 text-right font-mono font-bold tabular-nums align-middle">
                <span className={cn(ccm.r9 < 0 && 'text-red-200')}>{fmtVndSigned(ccm.r9)}</span>
              </td>
            </tr>
            <tr className="bg-[#1F7DC1]/10">
              <td className="border border-slate-300 px-3 py-1.5 pl-6 align-top text-slate-700">
                <span className="mr-1 text-slate-400">–</span>So sánh với Ngân sách full
                <div className="text-[10px] text-slate-400">= NS full − 9</div>
              </td>
              <td className="border border-slate-300 px-2 py-1.5 text-right align-middle text-slate-300">—</td>
              <BudgetColCompareCell value={pro.cmpFull} base={proFull} pct={fmtPct(pro.cmpFull, proFull)} />
              <BudgetColCompareCell value={ccm.cmpFull} base={ccmFull} pct={fmtPct(ccm.cmpFull, ccmFull)} />
            </tr>
          </tbody>
        </table>
      </div>
      {/* [S134] Danh sách phiếu trước cùng gói thầu CHƯA duyệt (informational — N9:
          PLAIN TEXT + badge, KHÔNG link vì click phiếu Nháp/chưa-duyệt → 403 S89). */}
      {showPending && pendingPriorPes.length > 0 && (
        <div className="border-t border-amber-200 bg-amber-50 px-3 py-2 text-[11px] leading-6 text-amber-800">
          <span className="font-medium">Phiếu trước cùng gói chưa duyệt: </span>
          {pendingPriorPes.map((p, i) => (
            <span key={p.id}>
              {i > 0 && ', '}
              <span className="font-mono">{p.maPhieu ?? '(chưa có mã)'}</span>{' '}
              <span className={cn(
                'rounded-full px-1.5 py-0.5 text-[10px] font-medium',
                p.phase === 10 ? 'bg-amber-100 text-amber-800'
                  : p.phase === 98 ? 'bg-orange-100 text-orange-800'
                    : p.phase === 1 ? 'bg-slate-200 text-slate-700'
                      : 'bg-amber-100 text-amber-800',
              )}>
                {PurchaseEvaluationPhaseLabel[p.phase] ?? `Phase ${p.phase}`}
              </span>
            </span>
          ))}
        </div>
      )}
    </div>
  )
}

// [Mig 54 2026-06-18 — anh Kiệt FDC] Giá đề xuất tại khối "c. Giá chào thầu".
// PRO nhập dải Min/Max (PUT /suggested-price/pro {minPrice,maxPrice}); CCM nhập 1
// giá (PUT /suggested-price/ccm {ccmPrice}). Role-gate qua canEditPro/CcmSuggestedPrice
// (BE-computed capability — mirror budget PRO/CCM, KHÔNG ràng phase). Read-only khi
// !canEdit → hiện text giá. Khi DaDuyet + approvedPriceAmount → dòng "Giá chốt duyệt".
const APPROVED_PRICE_SOURCE_LABEL: Record<string, string> = {
  Ncc: 'Giá NCC',
  ProMin: 'PRO Min',
  ProMax: 'PRO Max',
  ProMinMax: 'PRO Min–Max',
  Ccm: 'CCM',
}
function SuggestedPriceRows({ ev }: { ev: PeDetailBundle }) {
  const qc = useQueryClient()
  const invalidate = () => {
    qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
    qc.invalidateQueries({ queryKey: ['pe-list'] })
  }
  // PRO Min/Max + note — ABSOLUTE SET cả 3 field (mirror S74 CcmNote: field không đổi
  // echo giá trị hiện tại để không bị clear). Lưu giá → echo note hiện tại; lưu note →
  // echo min/max hiện tại.
  const proPriceMut = useMutation({
    mutationFn: async (body: { minPrice: number | null; maxPrice: number | null; note: string | null }) =>
      api.put(`/purchase-evaluations/${ev.id}/suggested-price/pro`, body),
    onSuccess: () => { toast.success('Đã lưu giá đề xuất (PRO)'); invalidate() },
    onError: e => toast.error(getErrorMessage(e)),
  })
  const ccmPriceMut = useMutation({
    mutationFn: async (body: { ccmPrice: number | null; note: string | null }) =>
      api.put(`/purchase-evaluations/${ev.id}/suggested-price/ccm`, body),
    onSuccess: () => { toast.success('Đã lưu giá đề xuất (CCM)'); invalidate() },
    onError: e => toast.error(getErrorMessage(e)),
  })

  // [S89 anh Kiệt FDC] Giá đề xuất PRO/CCM editable theo ROLE-capability (canEditPro/CcmSuggestedPrice)
  // — nhập ở mọi view khi phiếu CÒN soạn/trả-lại. [S96 2026-07-01 — anh Kiệt "duyệt là fix"] THÊM
  // gắn phase: phiếu ĐÃ GỬI DUYỆT / ĐÃ DUYỆT / TỪ CHỐI → KHÓA (read-only) — không sửa giá ngầm sau
  // khi cấp duyệt đã xem. Đồng bộ BE guard (UpdatePeSuggestedPricePro/Ccm phase-gate). Admin sửa-sai
  // qua API (BE cho admin override; FE ẩn cho gọn — hiếm khi cần).
  const editablePhase = isEditablePhase(ev.phase)   // DangSoanThao || TraLai
  const canEditPro = ev.canEditProSuggestedPrice && editablePhase
  const canEditCcm = ev.canEditCcmSuggestedPrice && editablePhase

  // Ghi chú PRO/CCM inline-edit state (Textarea). Echo cùng body absolute-set khi lưu giá.
  const [proNoteText, setProNoteText] = useState(ev.proSuggestedPriceNote ?? '')
  useEffect(() => { setProNoteText(ev.proSuggestedPriceNote ?? '') }, [ev.proSuggestedPriceNote])
  const [ccmNoteText, setCcmNoteText] = useState(ev.ccmSuggestedPriceNote ?? '')
  useEffect(() => { setCcmNoteText(ev.ccmSuggestedPriceNote ?? '') }, [ev.ccmSuggestedPriceNote])
  // [gotcha #70] khoá nút Lưu tới khi pe-detail refetch land — lưu giá xong lưu
  // ghi chú ngay (hoặc ngược lại) sẽ echo ev.* CŨ từ snapshot → mất dữ liệu.
  // Mirror peFetching của bảng ngân sách (S76).
  const peFetching = useIsFetching({ queryKey: ['pe-detail', ev.id] }) > 0
  const hasAnyValue = ev.proSuggestedMinPrice != null
    || ev.proSuggestedMaxPrice != null
    || ev.ccmSuggestedPrice != null
    || !!ev.proSuggestedPriceNote
    || !!ev.ccmSuggestedPriceNote
  const approved = ev.phase === PurchaseEvaluationPhase.DaDuyet && ev.approvedPriceAmount != null

  // Ẩn hoàn toàn khi không edit được + chưa có giá nào + chưa chốt → tránh khối rỗng.
  if (!canEditPro && !canEditCcm && !hasAnyValue && !approved) return null

  return (
    <div className="space-y-2 rounded-lg border border-slate-200 bg-slate-50/60 px-3 py-2.5">
      <div className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
        2 – 3. Giá đề xuất từ PRO / CCM (ngoài giá chào thầu)
      </div>

      {/* PRO — Giá Min / Giá Max */}
      <div className="flex items-start gap-3 text-[12px]">
        <span className="w-44 shrink-0 pt-1 text-slate-500">Giá đề xuất (PRO)</span>
        <div className="min-w-0 flex-1 space-y-1.5">
          <div className="flex items-center gap-2">
            <span className="w-12 shrink-0 text-[11px] text-slate-500">Min</span>
            {canEditPro ? (
              <VndInlineEdit
                initial={ev.proSuggestedMinPrice}
                saving={proPriceMut.isPending || peFetching}
                label="Giá đề xuất PRO — Min"
                onSave={v => proPriceMut.mutate({ minPrice: v, maxPrice: ev.proSuggestedMaxPrice, note: ev.proSuggestedPriceNote })}
              />
            ) : (
              <span className="font-semibold text-slate-800">
                {ev.proSuggestedMinPrice != null ? fmtVnd(ev.proSuggestedMinPrice) : <span className="font-normal text-slate-400">—</span>}
              </span>
            )}
          </div>
          <div className="flex items-center gap-2">
            <span className="w-12 shrink-0 text-[11px] text-slate-500">Max</span>
            {canEditPro ? (
              <VndInlineEdit
                initial={ev.proSuggestedMaxPrice}
                saving={proPriceMut.isPending || peFetching}
                label="Giá đề xuất PRO — Max"
                onSave={v => proPriceMut.mutate({ minPrice: ev.proSuggestedMinPrice, maxPrice: v, note: ev.proSuggestedPriceNote })}
              />
            ) : (
              <span className="font-semibold text-slate-800">
                {ev.proSuggestedMaxPrice != null ? fmtVnd(ev.proSuggestedMaxPrice) : <span className="font-normal text-slate-400">—</span>}
              </span>
            )}
          </div>
        </div>
      </div>

      {/* Ghi chú PRO — vì sao chọn Min/Max. Editable khi canEditPro (Textarea + nút Lưu),
          read-only hiện text khi có note. Lưu qua proPriceMut (echo min/max hiện tại). */}
      {(canEditPro || ev.proSuggestedPriceNote) && (
        <div className="flex items-start gap-3 text-[12px]">
          <span className="w-44 shrink-0 pt-1 text-slate-500">Ghi chú (PRO)</span>
          <div className="min-w-0 flex-1">
            {canEditPro ? (
              <div className="space-y-1">
                <textarea
                  value={proNoteText}
                  onChange={e => setProNoteText(e.target.value)}
                  placeholder="Ghi chú: vì sao chọn Min / Max…"
                  rows={2}
                  className="w-full rounded border border-slate-300 px-2 py-1 text-[12px]"
                />
                <div className="flex justify-end">
                  <Button
                    onClick={() => proPriceMut.mutate({
                      minPrice: ev.proSuggestedMinPrice,
                      maxPrice: ev.proSuggestedMaxPrice,
                      note: proNoteText.trim() || null,
                    })}
                    disabled={proNoteText === (ev.proSuggestedPriceNote ?? '') || proPriceMut.isPending || peFetching}
                    className="h-6 px-2 text-[11px]"
                  >
                    {proPriceMut.isPending ? '…' : 'Lưu ghi chú'}
                  </Button>
                </div>
              </div>
            ) : (
              <p className="whitespace-pre-wrap text-slate-700">{ev.proSuggestedPriceNote}</p>
            )}
          </div>
        </div>
      )}

      {/* CCM — 1 giá */}
      <div className="flex items-center gap-3 text-[12px]">
        <span className="w-44 shrink-0 text-slate-500">Giá đề xuất (CCM)</span>
        <div className="min-w-0 flex-1">
          {canEditCcm ? (
            <VndInlineEdit
              initial={ev.ccmSuggestedPrice}
              saving={ccmPriceMut.isPending || peFetching}
              label="Giá đề xuất CCM"
              onSave={v => ccmPriceMut.mutate({ ccmPrice: v, note: ev.ccmSuggestedPriceNote })}
            />
          ) : (
            <span className="font-semibold text-slate-800">
              {ev.ccmSuggestedPrice != null ? fmtVnd(ev.ccmSuggestedPrice) : <span className="font-normal text-slate-400">—</span>}
            </span>
          )}
        </div>
      </div>

      {/* Ghi chú CCM — vì sao 1 giá. Editable khi canEditCcm, read-only hiện text khi có note.
          Lưu qua ccmPriceMut (echo ccmPrice hiện tại). */}
      {(canEditCcm || ev.ccmSuggestedPriceNote) && (
        <div className="flex items-start gap-3 text-[12px]">
          <span className="w-44 shrink-0 pt-1 text-slate-500">Ghi chú (CCM)</span>
          <div className="min-w-0 flex-1">
            {canEditCcm ? (
              <div className="space-y-1">
                <textarea
                  value={ccmNoteText}
                  onChange={e => setCcmNoteText(e.target.value)}
                  placeholder="Ghi chú: vì sao chọn mức giá này…"
                  rows={2}
                  className="w-full rounded border border-slate-300 px-2 py-1 text-[12px]"
                />
                <div className="flex justify-end">
                  <Button
                    onClick={() => ccmPriceMut.mutate({
                      ccmPrice: ev.ccmSuggestedPrice,
                      note: ccmNoteText.trim() || null,
                    })}
                    disabled={ccmNoteText === (ev.ccmSuggestedPriceNote ?? '') || ccmPriceMut.isPending || peFetching}
                    className="h-6 px-2 text-[11px]"
                  >
                    {ccmPriceMut.isPending ? '…' : 'Lưu ghi chú'}
                  </Button>
                </div>
              </div>
            ) : (
              <p className="whitespace-pre-wrap text-slate-700">{ev.ccmSuggestedPriceNote}</p>
            )}
          </div>
        </div>
      )}

      {/* Giá CHỐT duyệt — chỉ khi DaDuyet + approvedPriceAmount != null. */}
      {approved && (
        <div className="flex items-center gap-3 rounded border border-emerald-200 bg-emerald-50 px-2 py-1.5 text-[12px]">
          <span className="w-44 shrink-0 font-medium text-emerald-800">Giá chốt duyệt</span>
          <span className="min-w-0 flex-1 font-semibold text-emerald-900">
            {ev.approvedPriceMaxAmount != null
              ? `${fmtVnd(ev.approvedPriceAmount!)} – ${fmtVnd(ev.approvedPriceMaxAmount)}`
              : fmtVnd(ev.approvedPriceAmount!)}
            {ev.approvedPriceSource && (
              <span className="ml-2 rounded bg-emerald-100 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
                {APPROVED_PRICE_SOURCE_LABEL[ev.approvedPriceSource] ?? ev.approvedPriceSource}
              </span>
            )}
          </span>
        </div>
      )}
    </div>
  )
}

// ===== Section 2 — Chọn NCC/TP (spec: a/b/c/d) =====
function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) {
  const canCreateContract = !readOnly && ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.suppliers.some(s => s.isWinner)
  const [createOpen, setCreateOpen] = useState(false)

  // [anh Kiệt FDC S87+] c/d/e (Giá chào thầu + Đề xuất PRO/CCM + Bảng so sánh + Link hồ
  // sơ) + hiển-thị winner ĐÃ TÁCH sang khối nổi bật "4. Thông tin chọn thầu"
  // (ThongTinChonThauSection). Section 3 này GIỮ: a. ô CHỌN winner + b. bảng ngân sách + tạo HĐ.

  return (
    <div className="space-y-3">
      <NccSelectorRow ev={ev} />
      {/* b. TỔNG HỢP NGÂN SÁCH TRÌNH KÝ (S61 — Excel anh Kiệt). Thay BudgetFieldRow
          + BudgetAdjustSection cũ (module Budget bỏ hẳn). */}
      <PeBudgetSummaryTable ev={ev} readOnly={readOnly} />
      {/* [anh Kiệt FDC S87+] c/d/e + hiển-thị winner ĐÃ chuyển sang khối nổi bật
          "4. Thông tin chọn thầu" (ThongTinChonThauSection — render ở parent, sau Section 3). */}

      {ev.paymentTerms && (
        <FormRow label="Điều khoản thanh toán" value={<span className="whitespace-pre-wrap">{ev.paymentTerms}</span>} />
      )}
      {ev.contractId && (
        <FormRow
          label="HĐ kế thừa"
          value={<a href={`/contracts/${ev.contractId}`} className="text-brand-600 hover:underline">✓ Xem HĐ</a>}
        />
      )}

      {canCreateContract && (
        <div className="rounded border border-emerald-200 bg-emerald-50 p-3">
          <div className="flex items-center justify-between gap-3">
            <div className="text-sm text-emerald-800">
              ✓ Phiếu đã duyệt. Bấm để tạo HĐ mới kế thừa NCC + hạng mục.
            </div>
            <Button onClick={() => setCreateOpen(true)} className="gap-1.5 text-xs">
              <Plus className="h-3.5 w-3.5" /> Tạo HĐ từ phiếu
            </Button>
          </div>
        </div>
      )}
      {createOpen && <CreateContractDialog evaluation={ev} onClose={() => setCreateOpen(false)} />}
    </div>
  )
}

// ===== Section 4 — Thông tin chọn thầu (anh Kiệt FDC S87+) =====
// Tách c/d/e khỏi Section 3 thành khối NỔI BẬT riêng (header xanh #1F7DC1 giống bảng
// ngân sách "giá trị thực hiện"). Đánh số 1→5: 1.Tên NCC/TP+giá trị · 2-3.Đề xuất
// PRO/CCM · 4.Bảng so sánh giá · 5.Link hồ sơ. Tên đơn vị "mang từ trên xuống" — Section 3
// giữ ô CHỌN (NccSelectorRow), khối này HIỂN THỊ kết quả (multi-winner → tên cách dấu phẩy).
function ThongTinChonThauSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) {
  const winners = ev.suppliers.filter(s => s.isWinner)
  const hasWinner = winners.length > 0
  const giaChaoThau = computeGiaChaoThau(ev)
  const banSoSanhAttachments = ev.attachments.filter(
    a => a.purchaseEvaluationSupplierId === null
      && a.purpose !== PeAttachmentPurpose.ApprovalAttachment,
  )
  // [Fix UAT — phiếu bị Trả lại: không xóa/tải lại được file "Bảng so sánh"]
  // Nút xóa/tải file so sánh phải theo NGƯỜI-SOẠN (hoặc Admin) + TRẠNG-THÁI-được-sửa,
  // KHÔNG theo readOnly của MÀN HÌNH: màn "Danh sách" mở phiếu readOnly=true nên ẩn
  // nút ở MỌI phase — kể cả Trả lại, vốn nằm trong isEditablePhase. Chỉ tách RIÊNG
  // khối đính kèm này; HoSoLink + winner bên dưới vẫn theo readOnly màn hình (không
  // mở nhầm). Chỉ áp cho "Bảng so sánh" (GeneralAttachmentsSection) — file per-NCC
  // (SupplierAttachmentsCell) giữ nguyên carve-out approverEditMode (Mig 28 F3).
  const { user: currentUser } = useAuth()
  const isAdmin = currentUser?.roles?.includes('Admin') ?? false
  const isDrafter = currentUser?.id != null && ev.drafterUserId === currentUser.id
  const attachEditable = (isAdmin || isDrafter) && isEditablePhase(ev.phase)
  return (
    <div className="overflow-hidden rounded-lg border border-slate-300">
      <div className="bg-[#1F7DC1] px-3 py-2 text-[12px] font-bold uppercase tracking-wide text-white">
        Thông tin chọn thầu
      </div>
      <div className="space-y-3 p-3">
        {/* 1. Tên NCC/TP + Giá trị (chào thầu) — kéo từ trên xuống, nổi bật */}
        <div className="rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2.5">
          <div className="mb-1 flex items-center gap-2">
            <span className="flex h-5 w-5 items-center justify-center rounded-full bg-emerald-600 text-[11px] font-bold text-white">1</span>
            <span className="text-[11px] font-semibold uppercase tracking-wide text-emerald-700">Tên NCC / TP được chọn</span>
          </div>
          <div className="pl-7">
            {hasWinner
              ? <div className="text-[15px] font-semibold text-emerald-900">{winners.map(s => s.supplierName).join(', ')}</div>
              : <div className="text-[13px] text-slate-400">— (chọn đơn vị ở mục 3 trước)</div>}
            <div className="mt-1 flex items-baseline gap-2 text-[12px]">
              <span className="text-slate-500">Giá trị (chào thầu):</span>
              {!hasWinner
                ? <span className="text-slate-400">—</span>
                : giaChaoThau === 0
                  ? <span className="text-slate-400">— (chưa nhập báo giá)</span>
                  : <span className="text-[14px] font-semibold text-emerald-900">{giaChaoThau!.toLocaleString('vi-VN')} đ</span>}
            </div>
          </div>
        </div>

        {/* 2 – 3. Giá đề xuất PRO / CCM (ngoài giá chào thầu) */}
        <SuggestedPriceRows ev={ev} />

        {/* 4. Bảng so sánh giá */}
        <div>
          <div className="mb-1 text-[12px] font-medium text-slate-600">4. Bảng so sánh giá</div>
          <GeneralAttachmentsSection evaluationId={ev.id} attachments={banSoSanhAttachments} readOnly={!attachEditable} />
        </div>

        {/* 5. Link hồ sơ */}
        <HoSoLinkRow ev={ev} readOnly={readOnly} />
      </div>
    </div>
  )
}

// e. Link hồ sơ — 1 cột HoSoLink (string? nullable) trỏ thư mục hồ sơ NAS.
// Read-only: thẻ <a> bấm-mở. Editable (phiếu DangSoanThao/TraLai + !readOnly):
// Input dán URL + nút Lưu. Save = PUT /purchase-evaluations/:id echo field bắt
// buộc (tenGoiThau + 2 ô ngân sách) như InfoTab.save để không xóa nhầm data.
function HoSoLinkRow({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) {
  const canEdit = !readOnly && isEditablePhase(ev.phase)
  const qc = useQueryClient()
  const [hoSoLink, setHoSoLink] = useState(ev.hoSoLink ?? '')
  useEffect(() => { setHoSoLink(ev.hoSoLink ?? '') }, [ev.id, ev.hoSoLink])

  const dirty = hoSoLink !== (ev.hoSoLink ?? '')
  const save = useMutation({
    mutationFn: async () => {
      await api.put(`/purchase-evaluations/${ev.id}`, {
        id: ev.id,
        tenGoiThau: ev.tenGoiThau,
        diaDiem: ev.diaDiem,
        moTa: ev.moTa,
        paymentTerms: ev.paymentTerms,
        budgetPeriodAmount: ev.budgetPeriodAmount,
        expectedRemainingAmount: ev.expectedRemainingAmount,
        // BE UpdatePeDraftHandler null-safe: "" = clear, non-empty = set,
        // omit/null = preserve. Gửi raw trimmed (="" khi xóa) để CLEAR đúng,
        // KHÔNG gửi null (null = giữ nguyên → không xóa được link cũ).
        hoSoLink: hoSoLink.trim(),
      })
    },
    onSuccess: () => {
      toast.success('Đã lưu link hồ sơ')
      qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
      qc.invalidateQueries({ queryKey: ['pe-list'] })
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  return (
    <div className="flex gap-3">
      <span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">5. Link hồ sơ</span>
      <div className="min-w-0 flex-1">
        {canEdit ? (
          <div className="flex max-w-2xl items-center gap-2">
            <Input
              type="url"
              value={hoSoLink}
              onChange={e => setHoSoLink(e.target.value)}
              placeholder="Dán link thư mục hồ sơ trên NAS..."
              className="text-sm"
            />
            <Button
              onClick={() => save.mutate()}
              disabled={!dirty || save.isPending}
              className="h-9 shrink-0 px-3 text-xs"
            >
              {save.isPending ? 'Đang lưu…' : 'Lưu'}
            </Button>
          </div>
        ) : ev.hoSoLink ? (
          /^https?:\/\//i.test(ev.hoSoLink.trim()) ? (
            // Link web (http/https — vd SharePoint) → bấm mở thẳng tab mới.
            <a
              href={ev.hoSoLink}
              target="_blank"
              rel="noopener noreferrer"
              className="break-all text-sm text-brand-600 hover:underline"
            >
              {ev.hoSoLink}
            </a>
          ) : (
            // Đường dẫn ổ cứng/ổ mạng (O:\…, \\server) → render link file:// để BẤM-THỬ
            // mở File Explorer (chạy nếu máy/trình duyệt cho phép, vd máy domain map sẵn
            // ổ mạng) + nút Copy dự phòng (default Chrome chặn https→file:// thì dùng Copy).
            <PathWithCopy path={ev.hoSoLink} />
          )
        ) : (
          <span className="text-sm text-slate-400">—</span>
        )}
      </div>
    </div>
  )
}

// Đổi đường dẫn Windows → URL file:// (O:\DATA\x → file:///O:/DATA/x · \\srv\share → file://srv/share).
function toFileUrl(p: string): string {
  const slashed = p.trim().replace(/\\/g, '/')
  const url = slashed.startsWith('//') ? 'file:' + slashed : 'file:///' + slashed
  return encodeURI(url)
}

// e.bis — Đường dẫn ổ cứng/ổ mạng (không phải http). Render LINK file:// để BẤM-THỬ
// mở File Explorer + nút Copy dự phòng. Bấm-mở chỉ chạy khi máy được cấu hình:
// Edge bật policy IntranetFileLinksEnabled (GPO 1 lần/domain, KHÔNG cài per-máy) +
// host trong Intranet Zone. Default Chrome/Edge CHẶN https→file:// (bấm no-op) →
// khi đó dùng nút Copy dán vào File Explorer (máy đã map ổ mạng là mở ngay).
function PathWithCopy({ path }: { path: string }) {
  const [copied, setCopied] = useState(false)
  const copy = async () => {
    try {
      await navigator.clipboard.writeText(path)
      setCopied(true)
      setTimeout(() => setCopied(false), 1500)
    } catch {
      toast.error('Không copy được — vui lòng bôi đen đường dẫn rồi Ctrl+C.')
    }
  }
  return (
    <div className="flex max-w-2xl items-start gap-2">
      <a
        href={toFileUrl(path)}
        target="_blank"
        rel="noopener noreferrer"
        className="min-w-0 flex-1 break-all rounded bg-slate-50 px-2 py-1 text-[13px] text-brand-700 ring-1 ring-slate-200 transition hover:bg-brand-50 hover:underline"
        title="Bấm mở trong File Explorer (cần Edge + IT bật policy). Không mở được thì bấm Copy rồi dán vào File Explorer."
      >
        {path}
      </a>
      <Button
        type="button"
        variant="outline"
        onClick={copy}
        className="h-8 shrink-0 px-2.5 text-xs"
        title="Copy đường dẫn rồi dán vào File Explorer (This PC)"
      >
        {copied ? '✓ Đã copy' : 'Copy'}
      </Button>
    </div>
  )
}

// Form row: label cố định 176px (w-44) bên trái + value bên phải (giống spec).
function FormRow({ label, value }: { label: string; value: React.ReactNode }) {
  return (
    <div className="flex items-baseline gap-3 border-b border-dotted border-slate-200 pb-1.5">
      <dt className="w-44 shrink-0 text-[12px] text-slate-500">{label}</dt>
      <dd className="min-w-0 flex-1 text-slate-800">{value}</dd>
    </div>
  )
}

function CreateContractDialog({ evaluation, onClose }: { evaluation: PeDetailBundle; onClose: () => void }) {
  const navigate = useNavigate()
  const [form, setForm] = useState({
    contractType: 1,
    tenHopDong: evaluation.tenGoiThau,
    bypassProcurementAndCCM: false,
  })
  const mut = useMutation({
    mutationFn: async () =>
      // [Mig 58 — anh Kiệt FDC] Multi-winner: BE tạo 1 HĐ / mỗi đơn vị trúng (IsWinner)
      // → trả LIST contractIds. ContractType người dùng chọn áp cho mọi HĐ.
      api.post<{ contractIds: string[] }>(`/purchase-evaluations/${evaluation.id}/create-contract`, form),
    onSuccess: res => {
      toast.success(`Đã tạo ${res.data.contractIds.length} hợp đồng từ phiếu.`)
      navigate(`/contracts/${res.data.contractIds[0]}`)
    },
    onError: e => toast.error(getErrorMessage(e)),
  })
  const typeOptions = [
    [1, 'HĐ Thầu phụ'],
    [2, 'HĐ Giao khoán'],
    [3, 'HĐ Nhà cung cấp'],
    [4, 'HĐ Dịch vụ'],
    [5, 'HĐ Mua bán'],
    [6, 'HĐ Nguyên tắc NCC'],
    [7, 'HĐ Nguyên tắc DV'],
  ] as const
  return (
    <Dialog
      open
      onClose={onClose}
      title="Tạo HĐ từ phiếu Duyệt NCC"
      footer={<>
        <Button variant="ghost" onClick={onClose}>Hủy</Button>
        <Button onClick={() => mut.mutate()} disabled={mut.isPending}>Tạo</Button>
      </>}
    >
      <div className="space-y-3">
        <p className="text-sm text-slate-500">
          NCC: <strong>{evaluation.suppliers.filter(s => s.isWinner).map(s => s.supplierName).join(', ') || evaluation.selectedSupplierName}</strong> · Dự án: {evaluation.projectName}
        </p>
        <div>
          <Label>Loại HĐ</Label>
          <Select value={form.contractType} onChange={e => setForm({ ...form, contractType: Number(e.target.value) })}>
            {typeOptions.map(([v, lbl]) => <option key={v} value={v}>{lbl}</option>)}
          </Select>
        </div>
        <div>
          <Label>Tên HĐ</Label>
          <Input value={form.tenHopDong} onChange={e => setForm({ ...form, tenHopDong: e.target.value })} />
        </div>
        <label className="flex items-center gap-2 text-sm">
          <input
            type="checkbox"
            checked={form.bypassProcurementAndCCM}
            onChange={e => setForm({ ...form, bypassProcurementAndCCM: e.target.checked })}
          />
          Bypass CCM (áp dụng HĐ với Chủ đầu tư)
        </label>
      </div>
    </Dialog>
  )
}

// Session 20 Chunk B: SuppliersTab function bỏ — NCC list giờ render nested
// trong HangMucCard (expand panel mỗi hạng mục). 2 dialog Add/Edit Supplier
// vẫn giữ vì HangMucCard call lại.

// Session 20 turn 8: Dialog thêm NCC mới — khi gọi từ HangMucCard (có detailId)
// thì input "Số tiền" hiển thị + sequential POST: tạo supplier → tạo quote
// cho hạng mục đó. detailId optional cho call site khác trong tương lai.
function AddSupplierDialog({ evaluationId, detailId, onClose }: {
  evaluationId: string
  detailId?: string
  onClose: () => void
}) {
  const qc = useQueryClient()
  const suppliers = useQuery({
    queryKey: ['all-suppliers'],
    queryFn: async () => (await api.get<{ items: Supplier[] }>('/suppliers', { params: { pageSize: 1000, published: true } })).data.items, // S113 R4: chỉ NCC đã công bố (ẩn nháp)
  })
  const [form, setForm] = useState({
    supplierId: '',
    displayName: '',
    contactName: '',
    contactEmail: '',
    contactPhone: '',
    paymentTermText: '',
    note: '',
    thanhTien: 0,
  })
  // [S116 anh Kiệt] Dấu âm cho báo giá (phát-sinh-giảm / hoàn tiền NCC). neg = state RIÊNG
  // (KHÔNG nhét vào form) — nhớ ý-định-dấu khi ô còn trống/0. Toggle ghi giá-trị-CÓ-DẤU
  // vào form.thanhTien NGAY (POST commit thẳng, không có nút "Lưu" riêng).
  const [neg, setNeg] = useState(false)
  const phoneError = !isValidPhone(form.contactPhone) ? 'SĐT không hợp lệ (cần 10-11 số bắt đầu 0)' : ''
  const emailError = !isValidEmail(form.contactEmail) ? 'Email không hợp lệ' : ''
  const hasError = !!(phoneError || emailError)
  const showQuote = !!detailId

  // S59 UAT "Không tự thêm dc tên NTP mới" — anh chốt mở POST /suppliers cho mọi
  // user đăng nhập (Sửa/Xóa vẫn Admin/CatalogManager). Tạo xong auto-select vào phiếu.
  const [showNew, setShowNew] = useState(false)
  const [newSup, setNewSup] = useState({ code: '', name: '', type: SupplierType.NhaThauPhu as SupplierType, phone: '', email: '' })
  const createSup = useMutation({
    mutationFn: async () => (await api.post<{ id: string }>('/suppliers', {
      code: newSup.code.trim(),
      name: newSup.name.trim(),
      type: newSup.type,
      phone: newSup.phone.trim() || null,
      email: newSup.email.trim() || null,
    })).data,
    onSuccess: async created => {
      toast.success('Đã tạo NCC mới vào danh mục.')
      await qc.invalidateQueries({ queryKey: ['all-suppliers'] })
      setForm(prev => ({ ...prev, supplierId: created.id, contactPhone: newSup.phone.trim(), contactEmail: newSup.email.trim() }))
      setShowNew(false)
      setNewSup({ code: '', name: '', type: SupplierType.NhaThauPhu as SupplierType, phone: '', email: '' })
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  const mut = useMutation({
    mutationFn: async () => {
      // Step 1: tạo NCC tham gia (PE.Suppliers row)
      const res = await api.post<{ id: string }>(`/purchase-evaluations/${evaluationId}/suppliers`, {
        supplierId: form.supplierId,
        displayName: form.displayName,
        contactName: form.contactName,
        contactEmail: form.contactEmail,
        contactPhone: form.contactPhone,
        paymentTermText: form.paymentTermText,
        note: form.note,
      })
      const newSupplierRowId = res.data.id
      // Step 2: tạo quote cho hạng mục (chỉ khi có detailId + thanhTien !== 0).
      // [S116] !== 0 (không > 0): giá ÂM tạo quote; rỗng/0 vẫn skip = "chưa báo giá".
      if (detailId && form.thanhTien !== 0) {
        await api.post(`/purchase-evaluations/${evaluationId}/quotes`, {
          purchaseEvaluationDetailId: detailId,
          purchaseEvaluationSupplierId: newSupplierRowId,
          bgVat: 0,
          chuaVat: 0,
          thanhTien: form.thanhTien,
          note: '',
          isSelected: false,
        })
      }
    },
    onSuccess: () => {
      toast.success(showQuote && form.thanhTien !== 0 ? 'Đã thêm NCC + báo giá.' : 'Đã thêm NCC.')
      qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] })
      onClose()
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  return (
    <Dialog
      open
      onClose={onClose}
      title="Thêm NCC vào phiếu"
      footer={<>
        <Button variant="ghost" onClick={onClose}>Hủy</Button>
        <Button onClick={() => mut.mutate()} disabled={!form.supplierId || hasError || mut.isPending}>Thêm</Button>
      </>}
    >
      <div className="space-y-3">
        <div>
          <Label>NCC (master)</Label>
          {/* S59 UAT (3 ý): gõ-tìm (SearchableSelect) + sort A-Z theo mã + "+ NCC mới"
              tạo nhanh vào danh mục dùng ngay. Auto-fill liên hệ từ master giữ nguyên. */}
          <div className="flex items-start gap-2">
            <SearchableSelect
              className="min-w-0 flex-1"
              options={(suppliers.data ?? [])
                .map(s => ({ value: s.id, label: `${s.code} — ${s.name}` }))
                .sort((a, b) => a.label.localeCompare(b.label, 'vi', { numeric: true }))}
              value={form.supplierId}
              onChange={id => {
                // Session 20 turn 10: auto-fill các field NCC từ master data sẵn có
                // (contactPerson/phone/email/note). User vẫn override được sau đó.
                const picked = suppliers.data?.find(s => s.id === id)
                setForm(prev => ({
                  ...prev,
                  supplierId: id,
                  contactName: picked?.contactPerson ?? '',
                  contactPhone: picked?.phone ?? '',
                  contactEmail: picked?.email ?? '',
                  note: picked?.note ?? '',
                }))
              }}
              placeholder="-- Chọn NCC (gõ để lọc) --"
            />
            <button
              type="button"
              onClick={() => setShowNew(v => !v)}
              className="inline-flex h-8 shrink-0 items-center gap-1 whitespace-nowrap rounded-lg border border-dashed border-brand-300 px-2.5 text-[11px] font-medium text-brand-700 hover:bg-brand-50"
            >
              <Plus className="h-3 w-3" /> NCC mới
            </button>
          </div>
          {form.supplierId && <p className="mt-1 text-[10px] text-emerald-600">✓ Đã tự điền từ Master — bạn có thể sửa lại nếu cần.</p>}
          {showNew && (
            <div className="mt-2 space-y-2 rounded-lg border border-dashed border-brand-200 bg-brand-50/40 p-2.5">
              <p className="text-[11px] font-medium text-brand-700">Tạo NCC mới vào danh mục (dùng ngay cho phiếu này)</p>
              <div className="grid grid-cols-2 gap-2">
                <div>
                  <Label className="text-[11px]">Mã *</Label>
                  <Input value={newSup.code} onChange={e => setNewSup({ ...newSup, code: e.target.value.toUpperCase() })} placeholder="VD: NTP-THANGLONG" />
                </div>
                <div>
                  <Label className="text-[11px]">Loại *</Label>
                  <Select value={newSup.type} onChange={e => setNewSup({ ...newSup, type: Number(e.target.value) as SupplierType })}>
                    {Object.values(SupplierType).map(t => (
                      <option key={t} value={t}>{SupplierTypeLabel[t]}</option>
                    ))}
                  </Select>
                </div>
                <div className="col-span-2">
                  <Label className="text-[11px]">Tên *</Label>
                  <Input value={newSup.name} onChange={e => setNewSup({ ...newSup, name: e.target.value })} placeholder="CÔNG TY ..." />
                </div>
                <div>
                  <Label className="text-[11px]">SĐT</Label>
                  <Input value={newSup.phone} onChange={e => setNewSup({ ...newSup, phone: e.target.value })} placeholder="0987654321" />
                </div>
                <div>
                  <Label className="text-[11px]">Email</Label>
                  <Input value={newSup.email} onChange={e => setNewSup({ ...newSup, email: e.target.value })} />
                </div>
              </div>
              <div className="flex justify-end">
                <Button
                  onClick={() => createSup.mutate()}
                  disabled={!newSup.code.trim() || !newSup.name.trim() || createSup.isPending}
                  className="h-7 px-3 text-xs"
                >
                  {createSup.isPending ? 'Đang tạo…' : 'Tạo & chọn'}
                </Button>
              </div>
            </div>
          )}
        </div>
        <div className="grid grid-cols-2 gap-3">
          <div><Label>Hiển thị</Label><Input value={form.displayName} onChange={e => setForm({ ...form, displayName: e.target.value })} placeholder="vd TGN-30 ngày" /></div>
          <div><Label>Điều khoản TT</Label><Input value={form.paymentTermText} onChange={e => setForm({ ...form, paymentTermText: e.target.value })} placeholder="vd 30 ngày, 300tr" /></div>
          <div><Label>Người liên hệ</Label><Input value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></div>
          <div>
            <Label>Điện thoại</Label>
            <Input
              type="tel"
              inputMode="tel"
              value={form.contactPhone}
              onChange={e => setForm({ ...form, contactPhone: e.target.value })}
              placeholder="0987654321"
              className={phoneError ? 'border-red-300' : undefined}
            />
            {phoneError && <p className="mt-0.5 text-[10px] text-red-600">{phoneError}</p>}
          </div>
          <div className="col-span-2">
            <Label>Email</Label>
            <Input
              type="email"
              value={form.contactEmail}
              onChange={e => setForm({ ...form, contactEmail: e.target.value })}
              placeholder="name@example.com"
              className={emailError ? 'border-red-300' : undefined}
            />
            {emailError && <p className="mt-0.5 text-[10px] text-red-600">{emailError}</p>}
          </div>
          <div className="col-span-2"><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} placeholder="ĐÃ CHỐT SO SÁNH LẦN 1 / ĐÀM PHÁN THÊM..." /></div>
          {showQuote && (
            <div className="col-span-2 rounded-lg border border-brand-200 bg-brand-50/40 p-3">
              <Label className="text-brand-700">Số tiền báo giá cho hạng mục</Label>
              <div className="mt-1 flex max-w-xs items-center gap-1.5">
                <button
                  type="button"
                  onClick={() => { setNeg(!neg); setForm({ ...form, thanhTien: -form.thanhTien }) }}
                  className={cn(
                    'h-9 w-9 shrink-0 rounded border text-sm font-bold',
                    neg ? 'border-red-300 bg-red-50 text-red-600' : 'border-slate-300 text-slate-400',
                  )}
                  title="Đảo dấu âm/dương (số âm = phát sinh giảm / hoàn tiền NCC)"
                >
                  {neg ? '−' : '+'}
                </button>
                <div className="relative flex-1">
                  <Input
                    type="text"
                    inputMode="numeric"
                    value={formatVndInput(Math.abs(form.thanhTien))}
                    onChange={e => { const mag = parseVnd(e.target.value); setForm({ ...form, thanhTien: neg ? -mag : mag }) }}
                    placeholder="0"
                    className="pr-10 font-mono text-right"
                  />
                  <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-[12px] font-medium text-slate-500">đ</span>
                </div>
              </div>
              <p className="mt-1 text-[11px] text-slate-500">
                Để trống / 0 → chỉ tạo NCC, chưa báo giá. Bấm +/− để nhập số âm (phát sinh giảm / hoàn tiền NCC). Sửa lại sau bằng cách click số tiền trong bảng.
              </p>
            </div>
          )}
        </div>
      </div>
    </Dialog>
  )
}

function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: string; row: PeSupplier; onClose: () => void }) {
  const qc = useQueryClient()
  const [form, setForm] = useState({
    supplierId: row.supplierId,
    displayName: row.displayName ?? '',
    contactName: row.contactName ?? '',
    contactEmail: row.contactEmail ?? '',
    contactPhone: row.contactPhone ?? '',
    paymentTermText: row.paymentTermText ?? '',
    note: row.note ?? '',
  })
  const phoneError = !isValidPhone(form.contactPhone) ? 'SĐT không hợp lệ (cần 10-11 số bắt đầu 0)' : ''
  const emailError = !isValidEmail(form.contactEmail) ? 'Email không hợp lệ' : ''
  const hasError = !!(phoneError || emailError)

  const mut = useMutation({
    mutationFn: async () => api.put(`/purchase-evaluations/${evaluationId}/suppliers/${row.id}`, form),
    onSuccess: () => { toast.success('Đã cập nhật.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
    onError: e => toast.error(getErrorMessage(e)),
  })
  return (
    <Dialog
      open
      onClose={onClose}
      title={`Sửa NCC — ${row.supplierName}`}
      footer={<>
        <Button variant="ghost" onClick={onClose}>Hủy</Button>
        <Button onClick={() => mut.mutate()} disabled={hasError || mut.isPending}>Lưu</Button>
      </>}
    >
      <div className="grid grid-cols-2 gap-3">
        <div><Label>Hiển thị</Label><Input value={form.displayName} onChange={e => setForm({ ...form, displayName: e.target.value })} /></div>
        <div><Label>Điều khoản TT</Label><Input value={form.paymentTermText} onChange={e => setForm({ ...form, paymentTermText: e.target.value })} /></div>
        <div><Label>Liên hệ</Label><Input value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></div>
        <div>
          <Label>Điện thoại</Label>
          <Input
            type="tel"
            inputMode="tel"
            value={form.contactPhone}
            onChange={e => setForm({ ...form, contactPhone: e.target.value })}
            placeholder="0987654321"
            className={phoneError ? 'border-red-300' : undefined}
          />
          {phoneError && <p className="mt-0.5 text-[10px] text-red-600">{phoneError}</p>}
        </div>
        <div className="col-span-2">
          <Label>Email</Label>
          <Input
            type="email"
            value={form.contactEmail}
            onChange={e => setForm({ ...form, contactEmail: e.target.value })}
            placeholder="name@example.com"
            className={emailError ? 'border-red-300' : undefined}
          />
          {emailError && <p className="mt-0.5 text-[10px] text-red-600">{emailError}</p>}
        </div>
        <div className="col-span-2"><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></div>
      </div>
    </Dialog>
  )
}

// ===== Tab: Hạng mục + Báo giá (Session 20 — nested cards layout) =====
// Mỗi hạng mục = 1 card với expand panel chứa NCC tham gia inline grid.
// Replace bảng matrix grid (hạng mục × NCC) cũ — user demo 1 hạng mục.
function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) {
  const [addOpen, setAddOpen] = useState(false)
  const [editDetail, setEditDetail] = useState<PeDetailRow | null>(null)

  // S61 — Budget comparison per-row (cột "NS link" + Δ) XÓA: module Budget bỏ hẳn,
  // không còn link PE → Budget entity row-by-row. So sánh ngân sách giờ ở bảng
  // TỔNG HỢP NGÂN SÁCH TRÌNH KÝ (Section 2 — PeBudgetSummaryTable).

  return (
    <div>
      <div className="mb-3 flex items-center justify-between">
        <p className="text-xs text-slate-500">
          {ev.details.length} hạng mục · {ev.suppliers.length} NCC tham gia
          {!readOnly && ' — mở hạng mục để thêm NCC + nhập báo giá.'}
        </p>
        {/* S59 vòng 6 (anh chốt "bỏ luôn cái nút thêm hạng mục"): 1 phiếu = 1 hạng mục
            chọn từ header (S57bis/S58) — hạng mục đầu auto-seed khi tạo phiếu, nút thêm
            hạng mục thứ 2+ sai mô hình → nút đã bỏ (dialog thêm/sửa hạng mục là DetailDialog). */}
      </div>

      {ev.details.length === 0 ? (
        <p className="text-sm text-slate-500">Chưa có hạng mục.</p>
      ) : (
        <div className="space-y-3">
          {ev.details.map(d => (
            <HangMucCard
              key={d.id}
              detail={d}
              ev={ev}
              readOnly={readOnly}
              onEditDetail={() => setEditDetail(d)}
            />
          ))}
        </div>
      )}

      {addOpen && <DetailDialog evaluationId={ev.id} row={null} onClose={() => setAddOpen(false)} />}
      {editDetail && <DetailDialog evaluationId={ev.id} row={editDetail} onClose={() => setEditDetail(null)} />}
    </div>
  )
}

// Card 1 hạng mục — tầng 1 header + tầng 2 NCC grid inline expand.
// Mặc định mở (expanded=true) vì user demo chỉ 1 hạng mục, đỡ click.
function HangMucCard({
  detail, ev, readOnly, onEditDetail,
}: {
  detail: PeDetailRow
  ev: PeDetailBundle
  readOnly: boolean
  onEditDetail: () => void
}) {
  const qc = useQueryClient()
  const [expanded, setExpanded] = useState(true)
  const [addNccOpen, setAddNccOpen] = useState(false)
  const [editNccRow, setEditNccRow] = useState<PeSupplier | null>(null)
  const [quoteEdit, setQuoteEdit] = useState<{ supplier: PeSupplier; existing: PeQuote | null } | null>(null)

  const removeDetail = useMutation({
    mutationFn: async () => api.delete(`/purchase-evaluations/${ev.id}/details/${detail.id}`),
    onSuccess: () => { toast.success('Đã xóa hạng mục.'); qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] }) },
    onError: e => toast.error(getErrorMessage(e)),
  })
  const removeNcc = useMutation({
    mutationFn: async (rowId: string) => api.delete(`/purchase-evaluations/${ev.id}/suppliers/${rowId}`),
    onSuccess: () => { toast.success('Đã xóa NCC.'); qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] }) },
    onError: e => toast.error(getErrorMessage(e)),
  })
  // [multi-NCC per hạng mục] Winner CHỌN THEO HẠNG MỤC NÀY: nguồn sự thật = quote.isSelected
  // của detail này. BE select-winner nhận MASTER supplierId (map qua PeSupplier.supplierId) +
  // detailId → set IsSelected cho quotes hạng mục này, derive Supplier.IsWinner toàn phiếu.
  // Gửi TOÀN BỘ tập master-id đang thắng hạng mục này sau mỗi toggle (chọn ≥2 = liên danh).
  const selectedRowIds = new Set(detail.quotes.filter(q => q.isSelected).map(q => q.purchaseEvaluationSupplierId))
  const detailWinnerSupplierIds = ev.suppliers.filter(s => selectedRowIds.has(s.id)).map(s => s.supplierId)
  const setDetailWinners = useMutation({
    mutationFn: async (supplierIds: string[]) =>
      api.post(`/purchase-evaluations/${ev.id}/select-winner`, { detailId: detail.id, supplierIds }),
    onSuccess: () => {
      toast.success('Đã cập nhật NCC trúng thầu cho hạng mục.')
      qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
      qc.invalidateQueries({ queryKey: ['pe-list'] })
    },
    onError: e => toast.error(getErrorMessage(e)),
  })
  const toggleDetailWinner = (supplierId: string, on: boolean) =>
    setDetailWinners.mutate(on
      ? [...detailWinnerSupplierIds, supplierId]
      : detailWinnerSupplierIds.filter(id => id !== supplierId))

  return (
    <div className="rounded-lg border border-slate-200 bg-white shadow-sm">
      {/* Header row — hạng mục info + actions. Session 20 turn 11: flex-wrap +
          padding responsive cho laptop nhỏ. Stat (Số tiền NS) wrap xuống dòng
          riêng khi container hẹp. */}
      <div className="flex flex-wrap items-start gap-2 border-b border-slate-100 p-2 sm:gap-3 sm:p-3">
        <button
          type="button"
          onClick={() => setExpanded(!expanded)}
          className="mt-0.5 text-slate-400 hover:text-slate-700"
          title={expanded ? 'Đóng' : 'Mở'}
        >
          {expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
        </button>
        <div className="flex-1 min-w-0">
          <div className="font-medium text-slate-900">
            <span className="font-mono text-[12px] text-slate-500 mr-2">{detail.groupCode}</span>
            {detail.noiDung}
          </div>
          <div className="mt-0.5 text-[11px] text-slate-500">
            {detail.groupName}{detail.donViTinh ? ` · ĐVT: ${detail.donViTinh}` : ''}
          </div>
        </div>
        <div className="flex flex-shrink-0 items-start gap-4 text-right text-xs">
          <div>
            <div className="text-[10px] uppercase text-slate-400">Số tiền ngân sách</div>
            <div className="font-mono text-base font-semibold text-slate-900">
              {fmtMoney(detail.thanhTienNganSach)}
              <span className="ml-1 text-xs font-normal text-slate-500">đ</span>
            </div>
          </div>
          {/* [S61 Mig 50] Cột "NS link" so sánh BudgetDetails cũ ĐÃ GỠ — module
              Budget cũ xóa hẳn; so sánh ngân sách giờ ở bảng "Tổng hợp ngân sách
              trình ký" cấp phiếu (PeBudgetSummaryTable). */}
        </div>
        {!readOnly && (
          <div className="flex flex-shrink-0 gap-1">
            <button
              onClick={onEditDetail}
              className="rounded px-1.5 py-0.5 text-slate-500 hover:bg-slate-100"
              title="Sửa hạng mục"
            >
              <Pencil className="h-3.5 w-3.5" />
            </button>
            <button
              onClick={() => { if (confirm('Xóa hạng mục? Báo giá NCC đã nhập cũng sẽ mất.')) removeDetail.mutate() }}
              className="rounded px-1.5 py-0.5 text-red-500 hover:bg-red-50"
              title="Xóa hạng mục"
            >
              <Trash2 className="h-3.5 w-3.5" />
            </button>
          </div>
        )}
      </div>

      {/* Expand panel — NCC tham gia + báo giá inline */}
      {expanded && (
        <div className="p-2 sm:p-3">
          <div className="mb-2 flex items-center justify-between">
            <div className="text-[11px] uppercase tracking-wide text-slate-500">
              NCC tham gia ({ev.suppliers.length})
            </div>
            {!readOnly && (
              <Button variant="ghost" onClick={() => setAddNccOpen(true)} className="gap-1.5 text-xs">
                <Plus className="h-3 w-3" /> Thêm NCC
              </Button>
            )}
          </div>

          {ev.suppliers.length === 0 ? (
            <p className="text-xs text-slate-500">
              {readOnly ? 'Chưa có NCC tham gia.' : 'Chưa có NCC. Thêm NCC để nhập báo giá.'}
            </p>
          ) : (
            <div className="overflow-x-auto">
              {/* S59 UAT vòng 3: "thêm file giao diện bị thay đổi không cân xứng" — auto-layout
                  để cell File (chip tên dài) phình + bóp dọc cột NCC. Fix: table-fixed + width
                  từng cột (chip file/email có truncate sẵn — kích hoạt khi cell khóa width);
                  min-w để panel hẹp thì scroll ngang (wrapper overflow-x-auto) thay vì bóp nát. */}
              <table className="w-full min-w-[860px] table-fixed border border-slate-200 text-xs">
                <thead className="bg-slate-50 text-slate-600">
                  <tr>
                    <th className="w-[24%] border-r border-slate-200 px-2 py-1.5 text-left">NCC</th>
                    <th className="w-[9%] border-r border-slate-200 px-2 py-1.5 text-left">SĐT</th>
                    <th className="w-[14%] border-r border-slate-200 px-2 py-1.5 text-left">Email</th>
                    <th className="w-[14%] border-r border-slate-200 px-2 py-1.5 text-left">Điều khoản TT</th>
                    <th className="w-[22%] border-r border-slate-200 px-2 py-1.5 text-left">File báo giá</th>
                    <th className="w-[12%] border-r border-slate-200 px-2 py-1.5 text-right">Số tiền</th>
                    {!readOnly && <th className="w-10 px-2 py-1.5"></th>}
                  </tr>
                </thead>
                <tbody className="divide-y divide-slate-100">
                  {ev.suppliers.map((s, idx) => {
                    const q = detail.quotes.find(x => x.purchaseEvaluationSupplierId === s.id) ?? null
                    // Thắng HẠNG MỤC NÀY = quote của cell có IsSelected. isWinner (phiếu-wide,
                    // derived BE) chỉ dùng guard xóa/sửa NCC — 1 NCC có thể thắng hạng mục khác.
                    const cellSelected = q?.isSelected ?? false
                    const isWinner = s.isWinner
                    const hasQuotes = ev.details.some(dd => dd.quotes.some(qq => qq.purchaseEvaluationSupplierId === s.id))
                    const canDelete = !isWinner && !hasQuotes
                    const openQuote = () => setQuoteEdit({ supplier: s, existing: q })
                    const palette = NCC_PALETTES[idx % NCC_PALETTES.length]
                    return (
                      <tr
                        key={s.id}
                        className={cn(
                          'align-top border-l-4 transition',
                          cellSelected
                            ? 'border-l-emerald-500 bg-emerald-100/70 font-semibold shadow-sm ring-1 ring-inset ring-emerald-300 hover:bg-emerald-200/70'
                            : cn(palette, 'hover:bg-white/80 hover:shadow-sm'),
                        )}
                      >
                        <td className="border-r border-slate-200 px-2 py-1.5">
                          <div className="font-medium text-slate-900">
                            {cellSelected && <span className="mr-1 text-base font-bold text-emerald-700">✓</span>}
                            <span className={cn(cellSelected && 'text-emerald-900')}>{s.supplierName}</span>
                          </div>
                          {s.displayName && <div className="text-[10px] text-slate-500">{s.displayName}</div>}
                          {s.note && <div className="text-[10px] text-amber-700">{s.note}</div>}
                        </td>
                        <td className="border-r border-slate-200 px-2 py-1.5 text-[11px] text-slate-600 font-mono">
                          {s.contactPhone || <span className="text-slate-300">—</span>}
                        </td>
                        <td className="border-r border-slate-200 px-2 py-1.5 text-[11px] text-slate-600">
                          {s.contactEmail
                            ? <span className="block truncate" title={s.contactEmail}>{s.contactEmail}</span>
                            : <span className="text-slate-300">—</span>}
                        </td>
                        <td className="border-r border-slate-200 px-2 py-1.5 text-[11px]">
                          {s.paymentTermText ?? <span className="text-slate-300">—</span>}
                        </td>
                        <td className="border-r border-slate-200 px-2 py-1.5">
                          <SupplierAttachmentsCell
                            evaluationId={ev.id}
                            supplierRowId={s.id}
                            attachments={ev.attachments.filter(a => a.purchaseEvaluationSupplierId === s.id)}
                            readOnly={readOnly}
                          />
                        </td>
                        <td className="border-r border-slate-200 px-2 py-1.5">
                          {!readOnly ? (
                            <button
                              type="button"
                              onClick={openQuote}
                              className={cn(
                                'w-full rounded border px-2 py-1 text-right font-mono text-[11px] transition',
                                q
                                  ? cellSelected
                                    ? 'border-emerald-300 bg-emerald-50 font-semibold text-emerald-700 hover:bg-emerald-100'
                                    : 'border-slate-300 bg-white font-semibold hover:border-brand-300 hover:bg-brand-50'
                                  : 'border-dashed border-slate-300 bg-slate-50 text-[10px] text-slate-400 hover:border-brand-400 hover:bg-brand-50 hover:text-brand-600',
                              )}
                              title="Click để nhập / sửa số tiền"
                            >
                              {q ? `${fmtMoney(q.thanhTien)} đ` : '+ Nhập số tiền'}
                            </button>
                          ) : (
                            <div className={cn('text-right font-mono font-semibold', cellSelected && 'text-emerald-700')}>
                              {q ? `${fmtMoney(q.thanhTien)} đ` : <span className="text-slate-300">—</span>}
                            </div>
                          )}
                        </td>
                        {!readOnly && (
                          <td className="px-2 py-1.5">
                            <div className="flex justify-end gap-0.5">
                              <button
                                onClick={() => toggleDetailWinner(s.supplierId, !cellSelected)}
                                disabled={setDetailWinners.isPending}
                                className={cn(
                                  'rounded px-1 py-0.5 disabled:opacity-50',
                                  cellSelected ? 'bg-emerald-100 text-emerald-700' : 'text-slate-400 hover:bg-emerald-50 hover:text-emerald-700',
                                )}
                                title={cellSelected ? 'Bỏ chọn trúng thầu hạng mục này (chọn ≥2 NCC = liên danh)' : 'Chọn trúng thầu hạng mục này (chọn ≥2 NCC = liên danh)'}
                              >
                                <Check className="h-3 w-3" />
                              </button>
                              {!isWinner && (
                                <button
                                  onClick={() => setEditNccRow(s)}
                                  className="rounded px-1 py-0.5 text-slate-500 hover:bg-slate-100"
                                  title="Sửa thông tin NCC"
                                >
                                  <Pencil className="h-3 w-3" />
                                </button>
                              )}
                              {canDelete ? (
                                <button
                                  onClick={() => { if (confirm('Xóa NCC này khỏi phiếu?')) removeNcc.mutate(s.id) }}
                                  className="rounded px-1 py-0.5 text-red-500 hover:bg-red-50"
                                  title="Xóa NCC"
                                >
                                  <Trash2 className="h-3 w-3" />
                                </button>
                              ) : !isWinner && hasQuotes && (
                                <span
                                  className="rounded px-1 py-0.5 text-slate-300 cursor-not-allowed"
                                  title="NCC đã có báo giá — xóa báo giá trước rồi mới xóa NCC"
                                >
                                  <Trash2 className="h-3 w-3" />
                                </span>
                              )}
                            </div>
                          </td>
                        )}
                      </tr>
                    )
                  })}
                </tbody>
              </table>
            </div>
          )}
        </div>
      )}

      {addNccOpen && <AddSupplierDialog evaluationId={ev.id} detailId={detail.id} onClose={() => setAddNccOpen(false)} />}
      {editNccRow && <EditSupplierDialog evaluationId={ev.id} row={editNccRow} onClose={() => setEditNccRow(null)} />}
      {quoteEdit && (
        <QuoteDialog
          evaluationId={ev.id}
          detailId={detail.id}
          supplierRowId={quoteEdit.supplier.id}
          supplierName={quoteEdit.supplier.supplierName}
          itemName={detail.noiDung}
          existing={quoteEdit.existing}
          onClose={() => setQuoteEdit(null)}
        />
      )}
    </div>
  )
}

function DetailDialog({ evaluationId, row, onClose }: { evaluationId: string; row: PeDetailRow | null; onClose: () => void }) {
  const qc = useQueryClient()
  // Session 20 turn 5: user yêu cầu rút gọn — chỉ Tên hạng mục + Số tiền
  // ngân sách (VND format) + Ghi chú. Các field schema khác (groupCode/
  // groupName/itemCode/donViTinh/khoiLuongs/donGia) giữ default cho BE
  // schema backward compat — KHÔNG expose UI cho user.
  const [form, setForm] = useState({
    groupCode: row?.groupCode ?? '01',
    groupName: row?.groupName ?? 'Hạng mục chính',
    itemCode: row?.itemCode ?? '',
    noiDung: row?.noiDung ?? '',
    donViTinh: row?.donViTinh ?? 'gói',
    khoiLuongNganSach: row?.khoiLuongNganSach ?? 1,
    khoiLuongThiCong: row?.khoiLuongThiCong ?? 1,
    donGiaNganSach: row?.donGiaNganSach ?? 0,
    thanhTienNganSach: row?.thanhTienNganSach ?? 0,
    ghiChu: row?.ghiChu ?? '',
  })
  const mut = useMutation({
    mutationFn: async () =>
      row
        ? api.put(`/purchase-evaluations/${evaluationId}/details/${row.id}`, form)
        : api.post(`/purchase-evaluations/${evaluationId}/details`, form),
    onSuccess: () => { toast.success(row ? 'Đã sửa.' : 'Đã thêm.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
    onError: e => toast.error(getErrorMessage(e)),
  })

  // Sync ngân sách: user nhập "Số tiền ngân sách" → set cả donGia + thanhTien
  // (KL = 1 ngầm). BE giữ schema 3 field.
  const setBudgetAmount = (n: number) => {
    setForm({ ...form, donGiaNganSach: n, thanhTienNganSach: n })
  }

  return (
    <Dialog
      open
      onClose={onClose}
      title={(row ? 'Sửa' : 'Thêm') + ' hạng mục'}
      footer={<>
        <Button variant="ghost" onClick={onClose}>Hủy</Button>
        <Button onClick={() => mut.mutate()} disabled={mut.isPending}>{row ? 'Lưu' : 'Thêm'}</Button>
      </>}
    >
      <div className="space-y-3">
        <div>
          <Label>Tên hạng mục</Label>
          <Input
            value={form.noiDung}
            onChange={e => setForm({ ...form, noiDung: e.target.value })}
            placeholder="vd Cung cấp bê tông M250"
          />
        </div>
        <div>
          <Label>Số tiền ngân sách</Label>
          <div className="relative">
            <Input
              type="text"
              inputMode="numeric"
              value={formatVndInput(form.thanhTienNganSach)}
              onChange={e => setBudgetAmount(parseVnd(e.target.value))}
              placeholder="0"
              className="pr-12 font-mono text-right"
            />
            <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-[12px] font-medium text-slate-500">đ</span>
          </div>
          <p className="mt-1 text-[11px] text-slate-500">VND — nhập số, tự format dấu chấm ngàn (vd 1.000.000)</p>
        </div>
        <div>
          <Label>Ghi chú</Label>
          <Input value={form.ghiChu} onChange={e => setForm({ ...form, ghiChu: e.target.value })} />
        </div>
      </div>
    </Dialog>
  )
}

function QuoteDialog({
  evaluationId, detailId, supplierRowId, supplierName, itemName, existing, onClose,
}: {
  evaluationId: string
  detailId: string
  supplierRowId: string
  supplierName: string
  itemName: string
  existing: PeQuote | null
  onClose: () => void
}) {
  const qc = useQueryClient()
  // Session 20 turn 3: user yêu cầu "tạm thời chỉ cần nhập số tiền, không
  // cần 3 cột có VAT / không VAT / tổng". UI chỉ 1 input thanhTien; bgVat /
  // chuaVat / note vẫn gửi BE giữ schema (default 0 / empty cho row mới,
  // giữ giá trị cũ nếu existing).
  const [form, setForm] = useState({
    thanhTien: existing?.thanhTien ?? 0,
  })
  // [S116 anh Kiệt] Dấu âm cho báo giá (phát-sinh-giảm / hoàn tiền NCC). neg = state RIÊNG
  // (KHÔNG nhét vào form: setForm({thanhTien}) thay-thế cả object sẽ nuốt mất). Init theo
  // dấu quote đang mở. Toggle ghi giá-trị-CÓ-DẤU vào form.thanhTien NGAY (POST commit thẳng).
  const [neg, setNeg] = useState((existing?.thanhTien ?? 0) < 0)

  const mut = useMutation({
    mutationFn: async () =>
      api.post(`/purchase-evaluations/${evaluationId}/quotes`, {
        purchaseEvaluationDetailId: detailId,
        purchaseEvaluationSupplierId: supplierRowId,
        bgVat: existing?.bgVat ?? 0,
        chuaVat: existing?.chuaVat ?? 0,
        thanhTien: form.thanhTien,
        note: existing?.note ?? '',
        isSelected: existing?.isSelected ?? false,
      }),
    onSuccess: () => { toast.success('Đã lưu số tiền.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
    onError: e => toast.error(getErrorMessage(e)),
  })
  const del = useMutation({
    mutationFn: async () =>
      existing ? api.delete(`/purchase-evaluations/${evaluationId}/quotes/${existing.id}`) : Promise.resolve(),
    onSuccess: () => { toast.success('Đã xóa.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
    onError: e => toast.error(getErrorMessage(e)),
  })

  const isSaving = mut.isPending || del.isPending

  return (
    <Dialog
      open
      onClose={onClose}
      title={`Báo giá — ${supplierName}`}
      footer={<>
        {existing && <Button variant="danger" onClick={() => del.mutate()} disabled={isSaving}>{del.isPending ? 'Đang xóa…' : 'Xóa'}</Button>}
        <Button variant="ghost" onClick={onClose} disabled={isSaving}>Hủy</Button>
        <Button onClick={() => mut.mutate()} disabled={isSaving}>{mut.isPending ? 'Đang lưu…' : 'Lưu'}</Button>
      </>}
    >
      <div className="relative space-y-3">
        {isSaving && (
          <div className="absolute inset-0 z-10 flex items-center justify-center rounded bg-white/70 backdrop-blur-sm">
            <div className="flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-4 py-2 shadow-md">
              <div className="h-4 w-4 animate-spin rounded-full border-2 border-brand-300 border-t-brand-600" />
              <span className="text-sm font-medium text-slate-700">
                {mut.isPending ? 'Đang lưu…' : 'Đang xóa…'}
              </span>
            </div>
          </div>
        )}
        <p className="text-sm text-slate-500">Hạng mục: <strong>{itemName}</strong></p>
        <div>
          <Label>Số tiền</Label>
          <div className="flex items-center gap-1.5">
            <button
              type="button"
              onClick={() => { setNeg(!neg); setForm({ thanhTien: -form.thanhTien }) }}
              className={cn(
                'h-9 w-9 shrink-0 rounded border text-sm font-bold',
                neg ? 'border-red-300 bg-red-50 text-red-600' : 'border-slate-300 text-slate-400',
              )}
              title="Đảo dấu âm/dương (số âm = phát sinh giảm / hoàn tiền NCC)"
            >
              {neg ? '−' : '+'}
            </button>
            <div className="relative flex-1">
              <Input
                type="text"
                inputMode="numeric"
                value={formatVndInput(Math.abs(form.thanhTien))}
                onChange={e => { const mag = parseVnd(e.target.value); setForm({ thanhTien: neg ? -mag : mag }) }}
                placeholder="0"
                autoFocus
                className="pr-12 font-mono text-right"
              />
              <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-[12px] font-medium text-slate-500">đ</span>
            </div>
          </div>
          <p className="mt-1 text-[11px] text-slate-500">VND — nhập số, tự format dấu chấm ngàn (vd 1.000.000). Bấm +/− để nhập số âm (phát sinh giảm / hoàn tiền NCC).</p>
        </div>
      </div>
    </Dialog>
  )
}

// ===== Tab: Duyệt =====
// Plan AC S25 Bug 3 — Decision badge phân biệt Approve / Trả lại / Từ chối.
// Plan AD S25 — Drop fromPhase→toPhase badges (gây nhầm khi cùng ChoDuyet);
// thay bằng next-target hint parse từ comment để rõ "gửi duyệt cho ai / trả về đâu".
const PE_DECISION_REJECT = 2
function decisionBadge(decision: number, toPhase: number): { label: string; cls: string } {
  if (decision === PE_DECISION_REJECT) {
    // Reject phân biệt: TuChoi(99) = "Từ chối" / TraLai(98) hoặc ChoDuyet(10) = "Trả lại"
    if (toPhase === 99) return { label: 'Từ chối', cls: 'bg-rose-100 text-rose-700 border border-rose-200' }
    return { label: 'Trả lại', cls: 'bg-amber-100 text-amber-700 border border-amber-200' }
  }
  return { label: 'Duyệt', cls: 'bg-emerald-100 text-emerald-700 border border-emerald-200' }
}

// Plan AD S25 — Parse comment để show next-target hint rõ ràng. BE comment
// format chuẩn từ Service:
//   Approve advance Cấp: "Hoàn tất Cấp X, sang Cấp Y cùng Bước Z"
//   Approve advance Bước: "Hoàn tất Bước X/Y, sang Bước Z (Cấp 1)"
//   Approve skipToFinal: "[Duyệt vượt cấp tới Cấp cuối] ..." (Plan AC)
//   Approve terminal: toPhase=DaDuyet(20)
//   Reject OneLevel: "Trả về Cấp X (cùng Bước Y)" hoặc "không lùi được"
//   Reject OneStep: "Trả về Bước X Cấp Y" hoặc "không lùi được"
//   Reject Assignee: "Trả về Người chỉ định — Bước X (...) Cấp Y"
//   Reject Drafter: "Trả về Người soạn thảo"
//   Reject TuChoi: toPhase=TuChoi(99)
function extractNextTargetHint(decision: number, toPhase: number, comment: string | null): string {
  if (decision === PE_DECISION_REJECT) {
    if (toPhase === 99) return '→ Từ chối hoàn toàn'
    const c = comment ?? ''
    if (c.includes('không lùi được')) return '→ Không lùi được'
    if (c.includes('Người chỉ định')) {
      const m = c.match(/Bước\s*(\d+).*?Cấp\s*(\d+)/)
      return m ? `→ Trả về Người chỉ định (Bước ${m[1]} Cấp ${m[2]})` : '→ Trả về Người chỉ định'
    }
    if (c.includes('Người soạn thảo') || c.includes('Drafter')) return '→ Trả về Người soạn thảo'
    if (c.includes('Trả về 1 Cấp') || c.includes('Trả về Cấp')) {
      const m = c.match(/Cấp\s*(\d+)/)
      return m ? `→ Lùi về Cấp ${m[1]}` : '→ Lùi 1 Cấp'
    }
    if (c.includes('Trả về 1 Bước') || c.includes('Trả về Bước')) {
      const m = c.match(/Bước\s*(\d+)/)
      return m ? `→ Lùi về Bước ${m[1]}` : '→ Lùi 1 Bước'
    }
    return ''
  }
  // Approve
  if (toPhase === 20) return '→ Đã duyệt hoàn tất'
  const c = comment ?? ''
  if (c.includes('Duyệt vượt cấp') || c.includes('Approver skip thẳng tới')) {
    return '→ Vượt cấp tới Cấp cuối'
  }
  const levelMatch = c.match(/sang Cấp\s*(\d+)/)
  if (levelMatch) return `→ Cấp ${levelMatch[1]}`
  const stepMatch = c.match(/sang Bước\s*(\d+)/)
  if (stepMatch) return `→ Bước ${stepMatch[1]} (Cấp 1)`
  return ''
}

function ApprovalsTab({ ev }: { ev: PeDetailBundle }) {
  // Plan AC2 S25 — FE merge view: fetch changelogs + reconstruct synthetic
  // Reject rows từ pre-Plan AC historical data (PE cũ deploy trước 2026-05-19
  // KHÔNG có Approval row cho Reject vì BE cũ chỉ log Changelog). Merge approvals
  // + synthetic + dedupe timestamp 5s bucket cùng approverUserId.
  const changelogs = useQuery({
    queryKey: ['pe-changelog', ev.id],
    queryFn: async () => (await api.get<PeChangelog[]>(`/purchase-evaluations/${ev.id}/changelogs`)).data,
  })

  // Plan AF S25 — userMap fallback cho historical entries pre-Plan AE
  // (userName="" empty/null). Cover real approvals + synthetic reject rows.
  const userMap = useMemo(() => {
    const m = new Map<string, string>()
    if (ev.drafterUserId && ev.drafterName) m.set(ev.drafterUserId, ev.drafterName)
    ev.approvals.forEach(a => {
      if (a.approverUserId && a.approverName) m.set(a.approverUserId, a.approverName)
    })
    ev.approvalFlow?.steps?.forEach(s =>
      s.levels?.forEach(l =>
        l.approvers?.forEach(ap => {
          if (ap.userId && ap.fullName) m.set(ap.userId, ap.fullName)
        }),
      ),
    )
    ev.levelOpinions?.forEach(o => {
      if (o.signedByUserId && o.signedByFullName) m.set(o.signedByUserId, o.signedByFullName)
    })
    ev.departmentOpinions?.forEach(o => {
      if (o.userId && o.userName) m.set(o.userId, o.userName)
    })
    return m
  }, [ev])

  const resolveActorName = (a: PeApproval): string => {
    if (a.approverName && a.approverName.trim() !== '') return a.approverName
    if (a.approverUserId) {
      const name = userMap.get(a.approverUserId)
      if (name) return name
    }
    return 'Hệ thống'
  }

  const merged = useMemo(() => {
    const phaseEnumMap: Record<string, number> = {
      DangSoanThao: 1, ChoDuyet: 10, DaDuyet: 20, TraLai: 98, TuChoi: 99,
    }
    const PE_ENTITY_WORKFLOW = 5
    const syntheticRejects: PeApproval[] = (changelogs.data ?? [])
      .filter(c => {
        if (c.entityType !== PE_ENTITY_WORKFLOW) return false
        if (c.summary?.includes('→ TraLai') || c.summary?.includes('→ TuChoi')) return true
        // 3 mode (OneLevel/OneStep/Assignee) giữ ChoDuyet → distinguish qua ContextNote keywords
        const note = c.contextNote ?? ''
        return note.includes('Trả về') || note.includes('không lùi được')
      })
      .map<PeApproval>(c => {
        const m = c.summary?.match(/Chuyển phase (\w+) → (\w+)/)
        const fromPhase = m ? (phaseEnumMap[m[1]] ?? 10) : 10
        const toPhase = m ? (phaseEnumMap[m[2]] ?? 10) : 10
        return {
          id: `syn-${c.id}`,
          fromPhase,
          toPhase,
          approverUserId: c.userId ?? null,
          approverName: c.userName ?? null,
          decision: 2,
          comment: c.contextNote ?? c.summary ?? null,
          approvedAt: c.createdAt,
        }
      })

    const realRejectKeys = new Set(
      ev.approvals
        .filter(a => a.decision === 2)
        .map(a => `${a.approverUserId ?? ''}-${Math.floor(new Date(a.approvedAt).getTime() / 5000)}`),
    )
    const dedupedSynthetic = syntheticRejects.filter(s =>
      !realRejectKeys.has(`${s.approverUserId ?? ''}-${Math.floor(new Date(s.approvedAt).getTime() / 5000)}`),
    )

    return [...ev.approvals, ...dedupedSynthetic]
      .sort((a, b) => new Date(a.approvedAt).getTime() - new Date(b.approvedAt).getTime())
  }, [ev.approvals, changelogs.data])

  if (merged.length === 0) return <p className="text-sm text-slate-500">Chưa có bước duyệt nào.</p>
  return (
    <ol className="space-y-2">
      {merged.map(a => {
        const dec = decisionBadge(a.decision, a.toPhase)
        const hint = extractNextTargetHint(a.decision, a.toPhase, a.comment)
        return (
          <li key={a.id} className="rounded border border-slate-200 bg-white p-3 text-sm">
            <div className="flex items-center justify-between gap-2">
              <div className="flex items-center gap-2 flex-wrap min-w-0">
                <span className={cn('rounded px-1.5 py-0.5 text-[11px] font-medium shrink-0', dec.cls)}>
                  {dec.label}
                </span>
                {hint && <span className="text-[12px] font-medium text-slate-700">{hint}</span>}
              </div>
              <span className="text-xs text-slate-500 shrink-0">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
            </div>
            <div className="mt-1 text-xs text-slate-500">
              {resolveActorName(a)}{a.comment && ` · ${a.comment}`}
            </div>
          </li>
        )
      })}
    </ol>
  )
}

// ===== Tab: Lịch sử =====
function HistoryTab({ ev }: { ev: PeDetailBundle }) {
  const logs = useQuery({
    queryKey: ['pe-changelog', ev.id],
    queryFn: async () => (await api.get<PeChangelog[]>(`/purchase-evaluations/${ev.id}/changelogs`)).data,
  })

  // Plan AF S25 — userMap fallback cho historical entries pre-Plan AE
  const userMap = useMemo(() => {
    const m = new Map<string, string>()
    if (ev.drafterUserId && ev.drafterName) m.set(ev.drafterUserId, ev.drafterName)
    ev.approvals.forEach(a => {
      if (a.approverUserId && a.approverName) m.set(a.approverUserId, a.approverName)
    })
    ev.approvalFlow?.steps?.forEach(s =>
      s.levels?.forEach(l =>
        l.approvers?.forEach(ap => {
          if (ap.userId && ap.fullName) m.set(ap.userId, ap.fullName)
        }),
      ),
    )
    ev.levelOpinions?.forEach(o => {
      if (o.signedByUserId && o.signedByFullName) m.set(o.signedByUserId, o.signedByFullName)
    })
    ev.departmentOpinions?.forEach(o => {
      if (o.userId && o.userName) m.set(o.userId, o.userName)
    })
    return m
  }, [ev])

  const resolveUserName = (l: PeChangelog): string => {
    if (l.userName && l.userName.trim() !== '') return l.userName
    if (l.userId) {
      const name = userMap.get(l.userId)
      if (name) return name
    }
    return 'Hệ thống'
  }

  if (logs.isLoading) return <p className="text-sm text-slate-500">Đang tải…</p>
  // User UAT 2026-05-08: chỉ track events Trả lại + Gửi duyệt lại.
  // User UAT 2026-05-19: + track Budget Adjust (Bug 1) + 4 mode Trả lại (Bug 2).
  // Filter giữ:
  //   - Workflow transition về TraLai (phaseAtChange = TraLai = 98)
  //   - Workflow transition từ TraLai → khác (Drafter gửi lại — summary "TraLai →")
  //   - Workflow Trả lại 4 mode (summary chứa "Trả lại" — Plan AB S25 fix Bug 2)
  //   - Header Budget Adjust (summary chứa "ngân sách" — Plan AB S25 fix Bug 1)
  //   - Mọi thay đổi nội dung khi phaseAtChange = TraLai (Drafter sửa trước gửi lại)
  // BE giữ data đầy đủ (audit trail) — chỉ filter ở UI, reversible.
  const PE_PHASE_TRALAI = 98
  const PE_ENTITY_WORKFLOW = 5
  const PE_ENTITY_HEADER = 1
  const filtered = (logs.data ?? []).filter(l => {
    if (l.entityType === PE_ENTITY_WORKFLOW) {
      if (l.phaseAtChange === PE_PHASE_TRALAI) return true
      if (l.summary?.includes('TraLai →')) return true
      if (l.summary?.includes('Trả lại')) return true
      return false
    }
    if (l.entityType === PE_ENTITY_HEADER && l.summary?.toLowerCase().includes('ngân sách')) {
      return true
    }
    return l.phaseAtChange === PE_PHASE_TRALAI
  })
  if (filtered.length === 0) return <p className="text-sm text-slate-500">Chưa có lịch sử trả lại / điều chỉnh ngân sách / gửi duyệt lại.</p>
  return (
    <ol className="space-y-1.5 text-sm">
      {filtered.map(l => (
        <li key={l.id} className="border-l-2 border-slate-200 pl-3 py-1">
          <div className="flex items-center justify-between text-xs text-slate-500">
            <span>{resolveUserName(l)}</span>
            <span>{new Date(l.createdAt).toLocaleString('vi-VN')}</span>
          </div>
          <div className="text-slate-800">{l.summary}</div>
          {l.contextNote && <div className="text-xs text-slate-500">{l.contextNote}</div>}
        </li>
      ))}
    </ol>
  )
}

// ===== Cell upload file đính kèm per-NCC =====
// 1 row = 1 NCC. User upload file báo giá (purpose=QuoteDocument mặc định) →
// POST multipart với supplierRowId. List N file hiện có + Download/Delete inline.
// Storage path: wwwroot/uploads/purchase-evaluations/{id}/{attId}_{safeName}
function SupplierAttachmentsCell({
  evaluationId,
  supplierRowId,
  attachments,
  readOnly = false,
}: {
  evaluationId: string
  supplierRowId: string
  attachments: PeAttachment[]
  readOnly?: boolean
}) {
  const qc = useQueryClient()
  const fileInputRef = useRef<HTMLInputElement>(null)
  const [previewAtt, setPreviewAtt] = useState<PeAttachment | null>(null)

  const upload = useMutation({
    mutationFn: async (file: File) => {
      const fd = new FormData()
      fd.append('file', file)
      fd.append('supplierRowId', supplierRowId)
      fd.append('purpose', String(PeAttachmentPurpose.QuoteDocument))
      return api.post(`/purchase-evaluations/${evaluationId}/attachments`, fd, {
        headers: { 'Content-Type': 'multipart/form-data' },
      })
    },
    onSuccess: () => {
      toast.success('Đã tải lên.')
      qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] })
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  const del = useMutation({
    mutationFn: async (attId: string) =>
      api.delete(`/purchase-evaluations/${evaluationId}/attachments/${attId}`),
    onSuccess: () => {
      toast.success('Đã xóa.')
      qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] })
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  async function download(att: PeAttachment) {
    try {
      const res = await api.get(
        `/purchase-evaluations/${evaluationId}/attachments/${att.id}/download`,
        { responseType: 'blob' },
      )
      const url = window.URL.createObjectURL(res.data as Blob)
      const a = document.createElement('a')
      a.href = url
      a.download = att.fileName
      a.click()
      window.URL.revokeObjectURL(url)
    } catch (e) {
      toast.error(getErrorMessage(e))
    }
  }

  async function onPick(e: React.ChangeEvent<HTMLInputElement>) {
    // S59 UAT "mỗi lần chỉ chọn được 1 file" → input multiple, upload tuần tự từng file.
    const files = Array.from(e.target.files ?? [])
    e.target.value = ''
    for (const f of files) {
      try { await upload.mutateAsync(f) } catch { /* toast lỗi đã hiện ở onError */ }
    }
  }

  const fmtSize = (b: number) =>
    b > 1024 * 1024 ? `${(b / 1024 / 1024).toFixed(1)}MB` : `${Math.round(b / 1024)}KB`

  return (
    <div className="space-y-1">
      {attachments.length === 0 && (
        <div className="text-[11px] italic text-slate-400">Chưa có file</div>
      )}
      {attachments.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">{fmtSize(a.fileSize)}</span>
          <span className="shrink-0 rounded bg-slate-200 px-1 text-[9px] text-slate-600">
            {PeAttachmentPurposeLabel[a.purpose] ?? ''}
          </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={() => download(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>
          {!readOnly && (
            <button
              onClick={() => { if (confirm(`Xóa "${a.fileName}"?`)) del.mutate(a.id) }}
              className="shrink-0 rounded px-1 text-red-500 hover:bg-red-50"
              title="Xóa"
            >
              <Trash2 className="h-3 w-3" />
            </button>
          )}
        </div>
      ))}
      {previewAtt && (
        <AttachmentPreviewDialog
          open
          evaluationId={evaluationId}
          attachmentId={previewAtt.id}
          fileName={previewAtt.fileName}
          onClose={() => setPreviewAtt(null)}
        />
      )}
      {!readOnly && (
        <div>
          <input
            ref={fileInputRef}
            type="file"
            multiple
            accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp"
            onChange={onPick}
            className="hidden"
          />
          <button
            onClick={() => fileInputRef.current?.click()}
            disabled={upload.isPending}
            className="inline-flex items-center gap-1 rounded border border-dashed border-slate-300 px-2 py-0.5 text-[11px] text-slate-500 hover:border-brand-300 hover:text-brand-700 disabled:opacity-50"
          >
            <Upload className="h-3 w-3" />
            {upload.isPending ? 'Đang tải…' : '+ Thêm file'}
          </button>
        </div>
      )}
    </div>
  )
}

// ===== Section Bảng so sánh — general attachments (không gắn NCC cụ thể) =====
// Purpose mặc định = ComparisonTable (4). Upload file Excel/PDF tổng hợp so
// sánh giá N NCC × M hạng mục. Storage path giống SupplierAttachmentsCell
// nhưng supplierRowId KHÔNG truyền → BE lưu NULL.
function GeneralAttachmentsSection({
  evaluationId,
  attachments,
  readOnly = false,
}: {
  evaluationId: string
  attachments: PeAttachment[]
  readOnly?: boolean
}) {
  const qc = useQueryClient()
  const fileInputRef = useRef<HTMLInputElement>(null)
  const [previewAtt, setPreviewAtt] = useState<PeAttachment | null>(null)

  const upload = useMutation({
    mutationFn: async (file: File) => {
      const fd = new FormData()
      fd.append('file', file)
      // KHÔNG append supplierRowId → BE set NULL → general attachment
      fd.append('purpose', String(PeAttachmentPurpose.ComparisonTable))
      return api.post(`/purchase-evaluations/${evaluationId}/attachments`, fd, {
        headers: { 'Content-Type': 'multipart/form-data' },
      })
    },
    onSuccess: () => {
      toast.success('Đã tải lên bảng so sánh.')
      qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] })
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  const del = useMutation({
    mutationFn: async (attId: string) =>
      api.delete(`/purchase-evaluations/${evaluationId}/attachments/${attId}`),
    onSuccess: () => {
      toast.success('Đã xóa.')
      qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] })
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  async function download(att: PeAttachment) {
    try {
      const res = await api.get(
        `/purchase-evaluations/${evaluationId}/attachments/${att.id}/download`,
        { responseType: 'blob' },
      )
      const url = window.URL.createObjectURL(res.data as Blob)
      const a = document.createElement('a')
      a.href = url
      a.download = att.fileName
      a.click()
      window.URL.revokeObjectURL(url)
    } catch (e) {
      toast.error(getErrorMessage(e))
    }
  }

  async function onPick(e: React.ChangeEvent<HTMLInputElement>) {
    // S59 UAT "mỗi lần chỉ chọn được 1 file" → input multiple, upload tuần tự từng file.
    const files = Array.from(e.target.files ?? [])
    e.target.value = ''
    for (const f of files) {
      try { await upload.mutateAsync(f) } catch { /* toast lỗi đã hiện ở onError */ }
    }
  }

  const fmtSize = (b: number) =>
    b > 1024 * 1024 ? `${(b / 1024 / 1024).toFixed(1)}MB` : `${Math.round(b / 1024)}KB`

  return (
    <div>
      {!readOnly && (
        <p className="mb-2 text-[12px] text-slate-500">
          File Excel/PDF tổng hợp so sánh giá của tất cả NCC (không gắn với 1 NCC cụ thể).
        </p>
      )}
      {attachments.length === 0 && readOnly && (
        <p className="text-sm italic text-slate-400">Chưa có bảng so sánh.</p>
      )}
      {attachments.length > 0 && (
        <div className="mb-2 space-y-1.5">
          {attachments.map(a => (
            <div
              key={a.id}
              className="flex items-center gap-2 rounded border border-slate-200 bg-white px-3 py-2 text-sm shadow-sm"
            >
              <Paperclip className="h-4 w-4 shrink-0 text-brand-500" />
              <span className="min-w-0 flex-1 truncate font-medium text-slate-800" title={a.fileName}>
                {a.fileName}
              </span>
              <span className="shrink-0 text-[11px] text-slate-500">{fmtSize(a.fileSize)}</span>
              <span className="shrink-0 rounded bg-brand-50 px-1.5 py-0.5 text-[10px] text-brand-700">
                {PeAttachmentPurposeLabel[a.purpose] ?? 'Khác'}
              </span>
              <span className="shrink-0 text-[10px] text-slate-400">
                {new Date(a.createdAt).toLocaleDateString('vi-VN')}
              </span>
              {isPreviewable(a.fileName) && (
                <button
                  onClick={() => setPreviewAtt(a)}
                  className="shrink-0 rounded p-1 text-violet-600 hover:bg-violet-50"
                  title="Xem trước"
                >
                  <Eye className="h-3.5 w-3.5" />
                </button>
              )}
              <button
                onClick={() => download(a)}
                className="shrink-0 rounded p-1 text-brand-600 hover:bg-brand-50"
                title="Tải xuống"
              >
                <Download className="h-3.5 w-3.5" />
              </button>
              {!readOnly && (
                <button
                  onClick={() => { if (confirm(`Xóa "${a.fileName}"?`)) del.mutate(a.id) }}
                  className="shrink-0 rounded p-1 text-red-500 hover:bg-red-50"
                  title="Xóa"
                >
                  <Trash2 className="h-3.5 w-3.5" />
                </button>
              )}
            </div>
          ))}
        </div>
      )}
      {previewAtt && (
        <AttachmentPreviewDialog
          open
          evaluationId={evaluationId}
          attachmentId={previewAtt.id}
          fileName={previewAtt.fileName}
          onClose={() => setPreviewAtt(null)}
        />
      )}
      {!readOnly && (
        <div>
          <input
            ref={fileInputRef}
            type="file"
            multiple
            accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp"
            onChange={onPick}
            className="hidden"
          />
          <button
            onClick={() => fileInputRef.current?.click()}
            disabled={upload.isPending}
            className="inline-flex items-center gap-1.5 rounded border border-dashed border-brand-300 bg-brand-50/50 px-3 py-2 text-xs font-medium text-brand-700 hover:border-brand-500 hover:bg-brand-50 disabled:opacity-50"
          >
            <Upload className="h-3.5 w-3.5" />
            {upload.isPending ? 'Đang tải…' : '+ Tải lên bảng so sánh'}
          </button>
        </div>
      )}
    </div>
  )
}
