// 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 } from 'react'
import { 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, Wallet } from 'lucide-react'
import { AttachmentPreviewDialog, isPreviewable } from './AttachmentPreviewDialog'
import { Button } from '@/components/ui/Button'
import { Dialog } from '@/components/ui/Dialog'
import { Input } from '@/components/ui/Input'
import { Label } from '@/components/ui/Label'
import { Select } from '@/components/ui/Select'
import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError'
import { cn } from '@/lib/cn'
import { useAuth } from '@/contexts/AuthContext'
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 { BudgetPhase, type BudgetListItem } from '@/types/budget'
import type { Paged, 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

// 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á).
  // Khi phase=ChoDuyet + workflow.AllowApproverEditDetails + actor match
  // CurrentLevel.ApproverUserId → cho phép edit Section 2 dù readOnly=true.
  const { user: currentUser } = useAuth()
  const isAdmin = currentUser?.roles?.includes('Admin') ?? false
  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
  // itemsReadOnly = readOnly trừ khi approver mode F3 mở
  const itemsReadOnly = readOnly && !approverEditMode

  // Mig 31 (S23 t1) — F2 Drafter-from-Nháp semantic deprecated. skipToFinal
  // moved sang Approver scope ChoDuyet (per-Level slot — xem PeWorkflowPanel).
  // Drafter SUBMIT chạy normal init pointer Step 0 Cấp 1.

  // "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.
  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)),
  })

  const forwardPhase = evaluation.workflow.nextPhases.find(p =>
    p !== PurchaseEvaluationPhase.TuChoi && p !== PurchaseEvaluationPhase.TraLai)
  const canSubmitForApproval = mode === 'workspace'
    && canEditPhase
    && !readOnly
    && forwardPhase != null

  // 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).
  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.`
        : null

  return (
    <div className="rounded-lg border border-slate-200 bg-white shadow-sm">
      <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>
            {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>
        </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>

      <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.
              Banner cảnh báo "Bạn đang chỉnh sửa khi đang duyệt" khi approverEditMode. */}
          {/* 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. Chọn NCC / TP thắng thầu">
          <ChonNccSection ev={evaluation} readOnly={readOnly} />
        </Section>
        <Section title="4. Ý 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>
        {/* S22+4 — Feature 2: Section "Điều chỉnh ngân sách" (mirror fe-admin).
            Drafter (Nháp/Trả lại) HOẶC Approver currentLevel (Đang duyệt) HOẶC
            Admin sửa Budget link / Manual amount. BE PATCH /budget-adjust. */}
        <Section title="5. Điều chỉnh ngân sách">
          <BudgetAdjustSection ev={evaluation} readOnly={readOnly} />
        </Section>
      </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 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,
        budgetId: ev.budgetId,
        budgetManualName: ev.budgetManualName,
        budgetManualAmount: ev.budgetManualAmount,
      })
    },
    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>
        <div className="md:col-span-2">
          <Label className="text-[11px]">Điều khoản thanh toán</Label>
          <Input
            value={paymentTerms}
            onChange={e => setPaymentTerms(e.target.value)}
            placeholder="JSON hoặc text"
          />
        </div>
      </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 — dropdown picker (user 2026-05-07) =====
// Workspace + canEdit phase: render Select dropdown từ ev.suppliers (Section 3
// tham gia list). Read-only: hiển thị "✓ Tên NCC" hoặc "(chưa chọn)".
// Save dùng POST /pe/:id/select-winner endpoint hiện có.
function NccSelectorRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
  const canEdit = !readOnly && isEditablePhase(ev.phase)
  const qc = useQueryClient()
  const setWinner = useMutation({
    mutationFn: async (supplierId: string) =>
      api.post(`/purchase-evaluations/${ev.id}/select-winner`, { supplierId }),
    onSuccess: () => {
      toast.success('Đã chọn NCC.')
      qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
      qc.invalidateQueries({ queryKey: ['pe-list'] })
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  if (!canEdit) {
    return (
      <FormRow
        label="a. NCC / TP được chọn"
        value={ev.selectedSupplierName
          ? <span className="font-medium text-emerald-700">✓ {ev.selectedSupplierName}</span>
          : <span className="text-slate-400">— (chưa chọn)</span>}
      />
    )
  }

  return (
    <div className="flex items-baseline gap-3 border-b border-dotted border-slate-200 pb-1.5">
      <span className="w-44 shrink-0 text-[12px] text-slate-500">a. NCC / TP được chọn</span>
      <div className="relative min-w-0 flex-1">
        <Select
          value={ev.selectedSupplierId ?? ''}
          onChange={e => setWinner.mutate(e.target.value)}
          disabled={ev.suppliers.length === 0 || setWinner.isPending}
          className="text-sm"
        >
          <option value="">— Chọn NCC từ danh sách Section 3 —</option>
          {ev.suppliers.map(s => (
            <option key={s.id} value={s.supplierId}>
              {s.supplierName}{s.displayName ? ` — ${s.displayName}` : ''}
            </option>
          ))}
        </Select>
        {/* Loading spinner inline khi save có delay (user 2026-05-07) */}
        {setWinner.isPending && (
          <div className="mt-1 flex items-center gap-1.5 text-[11px] text-brand-600">
            <div className="h-3 w-3 animate-spin rounded-full border-2 border-brand-300 border-t-brand-600" />
            <span>Đang chọn NCC + sync cột giá Section 4…</span>
          </div>
        )}
        {ev.suppliers.length === 0 && (
          <p className="mt-1 text-[11px] text-amber-600">
            Thêm NCC ở Section 3 trước rồi mới chọn winner.
          </p>
        )}
      </div>
    </div>
  )
}

// ===== b. Ngân sách inline editor (Mig 17) =====
// Hiển thị + edit budget link / manual fields ngay trong Section 2 — KHÔNG cần
// đi tới "Sửa header" page. Visible trong cả 3 view (Workspace / Danh sách /
// Duyệt). Edit chỉ enable khi !readOnly + phase editable (DangSoanThao /
// TraLai). Read-only khi pendingMe=1 hoặc phase đã gửi duyệt / đã duyệt /
// từ chối. Empty values hiển thị empty (per user 2026-05-07).
function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
  const canEdit = !readOnly && isEditablePhase(ev.phase)
  const qc = useQueryClient()

  // Detect mode khi mount/refresh: prefer manual mode nếu đã có data manual + ko link.
  // Session 20 turn 6: user yêu cầu manual mode chỉ nhập số tiền — bỏ Tên field
  // khỏi UI. State manualName drop, BE save luôn null cho field này. Data cũ với
  // tên vẫn hiển thị OK ở read-only display (ev.budgetManualName).
  const initialManual = (ev.budgetManualName !== null || ev.budgetManualAmount !== null) && !ev.budgetId
  const [manualMode, setManualMode] = useState(initialManual)
  const [budgetId, setBudgetId] = useState(ev.budgetId ?? '')
  const [manualAmount, setManualAmount] = useState(ev.budgetManualAmount ?? 0)

  // Eligible budgets — chỉ fetch khi user có khả năng edit
  const eligibleBudgets = useQuery({
    queryKey: ['eligible-budgets', ev.projectId],
    queryFn: async () => (await api.get<Paged<BudgetListItem>>('/budgets', {
      params: { pageSize: 100, projectId: ev.projectId, phase: BudgetPhase.DaDuyet },
    })).data.items,
    enabled: canEdit,
  })

  // Dirty detect — compare current state vs ev original
  const dirty = manualMode !== initialManual
    || (manualMode && manualAmount !== (ev.budgetManualAmount ?? 0))
    || (!manualMode && budgetId !== (ev.budgetId ?? ''))

  const save = useMutation({
    mutationFn: async () => {
      const payload = manualMode
        ? { budgetId: null, budgetManualName: null, budgetManualAmount: manualAmount > 0 ? manualAmount : null }
        : { budgetId: budgetId || null, budgetManualName: null, budgetManualAmount: null }
      await api.put(`/purchase-evaluations/${ev.id}`, {
        id: ev.id,
        tenGoiThau: ev.tenGoiThau,
        diaDiem: ev.diaDiem,
        moTa: ev.moTa,
        paymentTerms: ev.paymentTerms,
        ...payload,
      })
    },
    onSuccess: () => {
      toast.success('Đã cập nhật ngân sách')
      qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
      qc.invalidateQueries({ queryKey: ['pe-list'] })
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  // Read-only mode: chỉ display (không toggle, không edit)
  if (!canEdit) {
    return (
      <FormRow
        label="b. Ngân sách"
        value={ev.budget ? (
          <a href={`/budgets?id=${ev.budget.id}`} className="text-brand-600 hover:underline">
            <span className="font-mono text-[11px]">{ev.budget.maNganSach ?? '—'}</span>
            {' · '}{ev.budget.tenNganSach}
            {' · '}<span className="text-slate-500">{ev.budget.tongNganSach.toLocaleString('vi-VN')} đ</span>
          </a>
        ) : ev.budgetManualAmount != null || ev.budgetManualName ? (
          <span className="text-slate-700">
            {ev.budgetManualName && <span>{ev.budgetManualName}</span>}
            {ev.budgetManualName && ev.budgetManualAmount != null && ' · '}
            {ev.budgetManualAmount != null && (
              <span className="font-semibold text-slate-900">{ev.budgetManualAmount.toLocaleString('vi-VN')} đ</span>
            )}
            <span className="ml-2 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-500">nhập tay</span>
          </span>
        ) : <span className="text-slate-400">—</span>}
      />
    )
  }

  // Editable mode (canEdit=true)
  return (
    <div className="flex gap-3">
      <span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">b. Ngân sách</span>
      <div className="min-w-0 flex-1 space-y-2">
        <label className="inline-flex cursor-pointer items-center gap-1.5 text-[11px] text-slate-600">
          <input
            type="checkbox"
            checked={manualMode}
            onChange={e => setManualMode(e.target.checked)}
            className="h-3.5 w-3.5 rounded border-slate-300"
          />
          Nhập tay (không link)
        </label>
        {!manualMode ? (
          <Select
            value={budgetId}
            onChange={e => setBudgetId(e.target.value)}
            className="text-sm"
          >
            <option value="">—</option>
            {eligibleBudgets.data?.map(b => (
              <option key={b.id} value={b.id}>
                {b.maNganSach ?? '—'} · {b.tenNganSach} · {b.tongNganSach.toLocaleString('vi-VN')} đ
              </option>
            ))}
          </Select>
        ) : (
          <div className="relative max-w-xs">
            <Input
              type="text"
              inputMode="numeric"
              value={formatVndInput(manualAmount)}
              onChange={e => setManualAmount(parseVnd(e.target.value))}
              placeholder="0"
              className="pr-10 font-mono text-right text-sm"
            />
            <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-[12px] font-medium text-slate-500">đ</span>
          </div>
        )}
        {dirty && (
          <div className="flex items-center gap-2">
            <Button
              onClick={() => save.mutate()}
              disabled={save.isPending}
              className="h-7 px-3 text-xs"
            >
              {save.isPending ? 'Đang lưu…' : 'Lưu ngân sách'}
            </Button>
            <button
              onClick={() => {
                setManualMode(initialManual)
                setBudgetId(ev.budgetId ?? '')
                setManualAmount(ev.budgetManualAmount ?? 0)
              }}
              className="text-[11px] text-slate-500 hover:text-slate-700"
            >
              Hủy thay đổi
            </button>
          </div>
        )}
      </div>
    </div>
  )
}

// ===== Section "Điều chỉnh ngân sách" (S22+4 — Feature 2) =====
// Mirror fe-admin BudgetAdjustSection — Drafter (Nháp/Trả lại) HOẶC Approver
// currentLevel (Đang duyệt) HOẶC Admin sửa Budget link / Manual amount via
// PATCH /budget-adjust riêng. Audit changelog tự động.
function BudgetAdjustSection({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
  const { user: currentUser } = useAuth()
  const qc = useQueryClient()
  const [editing, setEditing] = useState(false)

  const isAdmin = currentUser?.roles?.includes('Admin') ?? false
  const isDrafter = currentUser?.id != null && ev.drafterUserId === currentUser.id
  const isDrafterPhase = ev.phase === PurchaseEvaluationPhase.DangSoanThao
    || ev.phase === PurchaseEvaluationPhase.TraLai
  // F4 Approver scope (Mig 30): ChoDuyet + actor in approvers + flag tick.
  const actorInCurrentLevel = ev.currentApproval?.approvers?.some(a => a.userId === currentUser?.id) ?? false
  const approverEditBudgetAllowed = ev.currentLevelOptions?.allowApproverEditBudget ?? false
  const isApproverChoDuyet = ev.phase === PurchaseEvaluationPhase.ChoDuyet
    && actorInCurrentLevel
    && approverEditBudgetAllowed

  // S23 t2 bug fix: F4 Approver scope BYPASS readOnly (mirror F3 itemsReadOnly
  // pattern line 118). Khi admin tick AllowApproverEditBudget cho slot + actor
  // match + Phase=ChoDuyet → button "Điều chỉnh" enable trong menu Duyệt (readOnly=true)
  // dù chế độ chỉ-đọc. Drafter + Admin vẫn cần !readOnly (chỉ active từ Workspace).
  const canAdjust = isAdmin
    || (!readOnly && isDrafter && isDrafterPhase)
    || isApproverChoDuyet

  const initialManual = (ev.budgetManualName !== null || ev.budgetManualAmount !== null) && !ev.budgetId
  const [manualMode, setManualMode] = useState(initialManual)
  const [budgetId, setBudgetId] = useState(ev.budgetId ?? '')
  const [manualAmount, setManualAmount] = useState(ev.budgetManualAmount ?? 0)
  const [manualName, setManualName] = useState(ev.budgetManualName ?? '')

  const eligibleBudgets = useQuery({
    queryKey: ['eligible-budgets-adjust', ev.projectId],
    queryFn: async () => (await api.get<Paged<BudgetListItem>>('/budgets', {
      params: { pageSize: 100, projectId: ev.projectId, phase: BudgetPhase.DaDuyet },
    })).data.items,
    enabled: editing && canAdjust,
  })

  const adjustMut = useMutation({
    mutationFn: async () => {
      const payload = manualMode
        ? { budgetId: null, budgetManualName: manualName || null, budgetManualAmount: manualAmount > 0 ? manualAmount : null }
        : { budgetId: budgetId || null, budgetManualName: null, budgetManualAmount: null }
      await api.patch(`/purchase-evaluations/${ev.id}/budget-adjust`, payload)
    },
    onSuccess: () => {
      toast.success('Đã điều chỉnh ngân sách')
      qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
      qc.invalidateQueries({ queryKey: ['pe-list'] })
      setEditing(false)
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  const displayLink = ev.budget ? (
    <span>
      <span className="font-mono text-[11px] text-brand-700">{ev.budget.maNganSach ?? '—'}</span>
      {' · '}{ev.budget.tenNganSach}
      {' · '}<span className="font-semibold text-slate-900">{ev.budget.tongNganSach.toLocaleString('vi-VN')} đ</span>
    </span>
  ) : (ev.budgetManualAmount != null || ev.budgetManualName) ? (
    <span>
      {ev.budgetManualName && <span>{ev.budgetManualName}</span>}
      {ev.budgetManualName && ev.budgetManualAmount != null && ' · '}
      {ev.budgetManualAmount != null && (
        <span className="font-semibold text-slate-900">{ev.budgetManualAmount.toLocaleString('vi-VN')} đ</span>
      )}
      <span className="ml-2 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-500">nhập tay</span>
    </span>
  ) : <span className="italic text-slate-400">Chưa có ngân sách</span>

  return (
    <div className="space-y-3">
      {!editing && (
        <div className="flex items-start justify-between gap-3 rounded border border-emerald-200 bg-emerald-50/40 px-3 py-2">
          <div className="flex items-start gap-2">
            <Wallet className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600" />
            <div className="text-sm text-slate-700">{displayLink}</div>
          </div>
          {canAdjust && (
            <Button
              onClick={() => {
                setManualMode(initialManual)
                setBudgetId(ev.budgetId ?? '')
                setManualAmount(ev.budgetManualAmount ?? 0)
                setManualName(ev.budgetManualName ?? '')
                setEditing(true)
              }}
              variant="ghost"
              className="h-7 shrink-0 px-2 text-xs"
            >
              <Pencil className="h-3 w-3" /> Điều chỉnh
            </Button>
          )}
        </div>
      )}

      {editing && canAdjust && (
        <div className="space-y-3 rounded border border-emerald-300 bg-emerald-50/30 p-3">
          {isApproverChoDuyet && (
            <div className="rounded border border-amber-200 bg-amber-50 px-2 py-1.5 text-[11px] text-amber-800">
              ⓘ Bạn đang điều chỉnh ngân sách lúc phiếu đang duyệt — thay đổi sẽ được ghi vào lịch sử.
            </div>
          )}
          <label className="inline-flex cursor-pointer items-center gap-1.5 text-[11px] text-slate-600">
            <input
              type="checkbox"
              checked={manualMode}
              onChange={e => setManualMode(e.target.checked)}
              className="h-3.5 w-3.5 rounded border-slate-300"
            />
            Nhập tay (không link Budget)
          </label>
          {!manualMode ? (
            <div>
              <Label className="text-[11px]">Chọn Budget từ danh sách</Label>
              <Select value={budgetId} onChange={e => setBudgetId(e.target.value)} className="text-sm">
                <option value="">— (huỷ link)</option>
                {eligibleBudgets.data?.map(b => (
                  <option key={b.id} value={b.id}>
                    {b.maNganSach ?? '—'} · {b.tenNganSach} · {b.tongNganSach.toLocaleString('vi-VN')} đ
                  </option>
                ))}
              </Select>
            </div>
          ) : (
            <div className="grid gap-2 md:grid-cols-2">
              <div>
                <Label className="text-[11px]">Tên (không bắt buộc)</Label>
                <Input
                  value={manualName}
                  onChange={e => setManualName(e.target.value)}
                  placeholder="vd Ngân sách dự phòng Q2/2026"
                  className="text-sm"
                />
              </div>
              <div>
                <Label className="text-[11px]">Số tiền (VND)</Label>
                <div className="relative">
                  <Input
                    type="text"
                    inputMode="numeric"
                    value={formatVndInput(manualAmount)}
                    onChange={e => setManualAmount(parseVnd(e.target.value))}
                    placeholder="0"
                    className="pr-10 font-mono text-right text-sm"
                  />
                  <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-[12px] font-medium text-slate-500">đ</span>
                </div>
              </div>
            </div>
          )}
          <div className="flex items-center justify-end gap-2">
            <Button
              variant="ghost"
              onClick={() => setEditing(false)}
              className="h-7 px-3 text-xs"
            >
              Hủy
            </Button>
            <Button
              onClick={() => adjustMut.mutate()}
              disabled={adjustMut.isPending}
              className="h-7 px-3 text-xs"
            >
              {adjustMut.isPending ? 'Đang lưu…' : 'Lưu điều chỉnh'}
            </Button>
          </div>
        </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.selectedSupplierId
  const [createOpen, setCreateOpen] = useState(false)

  // c. Giá chào thầu = sum quotes của NCC được chọn (winner)
  const winnerSupplierRowId = ev.selectedSupplierId
    ? ev.suppliers.find(s => s.supplierId === ev.selectedSupplierId)?.id ?? null
    : null
  const giaChaoThau = winnerSupplierRowId
    ? ev.details
        .flatMap(d => d.quotes)
        .filter(q => q.purchaseEvaluationSupplierId === winnerSupplierRowId)
        .reduce((sum, q) => sum + q.thanhTien, 0)
    : null

  // d. Bản so sánh — attachments với purpose=ComparisonTable hoặc supplier-row null
  const banSoSanhAttachments = ev.attachments.filter(
    a => a.purchaseEvaluationSupplierId === null,
  )

  return (
    <div className="space-y-3">
      <NccSelectorRow ev={ev} readOnly={readOnly} />
      <BudgetFieldRow ev={ev} readOnly={readOnly} />
      <FormRow
        label="c. Giá chào thầu"
        value={
          winnerSupplierRowId === null ? (
            <span className="text-slate-400">— (chọn NCC/TP ở (a) trước)</span>
          ) : giaChaoThau === 0 ? (
            <span className="text-slate-400">— (chưa nhập báo giá ở Section 4)</span>
          ) : (
            <span className="font-semibold text-slate-900">{giaChaoThau!.toLocaleString('vi-VN')} đ</span>
          )
        }
      />
      <div>
        <div className="flex gap-3">
          <span className="w-44 shrink-0 text-[12px] text-slate-500">d. Bản so sánh</span>
          <div className="min-w-0 flex-1">
            <GeneralAttachmentsSection
              evaluationId={ev.id}
              attachments={banSoSanhAttachments}
              readOnly={readOnly}
            />
          </div>
        </div>
      </div>

      {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>
  )
}

// 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 () =>
      api.post<{ contractId: string }>(`/purchase-evaluations/${evaluation.id}/create-contract`, form),
    onSuccess: res => {
      toast.success('Đã tạo HĐ từ phiếu.')
      navigate(`/contracts/${res.data.contractId}`)
    },
    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.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 } })).data.items,
  })
  const [form, setForm] = useState({
    supplierId: '',
    displayName: '',
    contactName: '',
    contactEmail: '',
    contactPhone: '',
    paymentTermText: '',
    note: '',
    thanhTien: 0,
  })
  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

  const mut = useMutation({
    mutationFn: async () => {
      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
      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>
          <Select
            value={form.supplierId}
            onChange={e => {
              // 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 === e.target.value)
              setForm(prev => ({
                ...prev,
                supplierId: e.target.value,
                contactName: picked?.contactPerson ?? '',
                contactPhone: picked?.phone ?? '',
                contactEmail: picked?.email ?? '',
                note: picked?.note ?? '',
              }))
            }}
          >
            <option value="">-- Chọn --</option>
            {suppliers.data?.map(s => (
              <option key={s.id} value={s.id}>{s.code} — {s.name}</option>
            ))}
          </Select>
          {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>}
        </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="relative mt-1 max-w-xs">
                <Input
                  type="text"
                  inputMode="numeric"
                  value={formatVndInput(form.thanhTien)}
                  onChange={e => setForm({ ...form, thanhTien: parseVnd(e.target.value) })}
                  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>
              <p className="mt-1 text-[11px] text-slate-500">
                Để trống / 0 → chỉ tạo NCC, chưa báo giá. 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)

  // Budget comparison — fetch full Budget bundle nếu có link để so sánh per-row.
  const budgetBundle = useQuery({
    queryKey: ['budget-detail-for-pe', ev.budgetId],
    queryFn: async () => (await api.get<{ details: { groupCode: string; itemCode: string | null; thanhTien: number }[]; tongNganSach: number }>(
      `/budgets/${ev.budgetId}`)).data,
    enabled: !!ev.budgetId,
  })
  const budgetRowMap = (() => {
    const m = new Map<string, number>()
    budgetBundle.data?.details.forEach(d => {
      m.set(`${d.groupCode}|${d.itemCode ?? ''}`, d.thanhTien)
    })
    return m
  })()
  const showBudgetCol = !!ev.budgetId

  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>
        {!readOnly && (
          <Button onClick={() => setAddOpen(true)} className="gap-1.5 text-xs">
            <Plus className="h-3.5 w-3.5" /> Thêm hạng mục
          </Button>
        )}
      </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}
              budgetRowMap={budgetRowMap}
              showBudgetCol={showBudgetCol}
              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, budgetRowMap, showBudgetCol, onEditDetail,
}: {
  detail: PeDetailRow
  ev: PeDetailBundle
  readOnly: boolean
  budgetRowMap: Map<string, number>
  showBudgetCol: 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)),
  })
  const setWinner = useMutation({
    mutationFn: async (supplierId: string) =>
      api.post(`/purchase-evaluations/${ev.id}/select-winner`, { supplierId }),
    onSuccess: () => { toast.success('Đã chọn NCC thắng.'); qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] }) },
    onError: e => toast.error(getErrorMessage(e)),
  })

  const bgValue = budgetRowMap.get(`${detail.groupCode}|${detail.itemCode ?? ''}`)
  const delta = bgValue != null ? detail.thanhTienNganSach - bgValue : null

  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>
          {showBudgetCol && bgValue != null && (
            <div className="border-l border-slate-200 pl-3">
              <div className="text-[10px] uppercase text-slate-400">NS link</div>
              <div className="font-mono text-[11px]">{fmtMoney(bgValue)}</div>
              <div className={cn(
                'font-mono text-[10px]',
                delta! > 0 && 'text-red-600',
                delta! < 0 && 'text-emerald-600',
                delta === 0 && 'text-slate-500',
              )}>
                Δ {delta! > 0 ? '+' : ''}{fmtMoney(delta!)}
              </div>
            </div>
          )}
        </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">
              <table className="min-w-full border border-slate-200 text-xs">
                <thead className="bg-slate-50 text-slate-600">
                  <tr>
                    <th className="border-r border-slate-200 px-2 py-1.5 text-left">NCC</th>
                    <th className="border-r border-slate-200 px-2 py-1.5 text-left">SĐT</th>
                    <th className="border-r border-slate-200 px-2 py-1.5 text-left">Email</th>
                    <th className="border-r border-slate-200 px-2 py-1.5 text-left">Điều khoản TT</th>
                    <th className="border-r border-slate-200 px-2 py-1.5 text-left">File báo giá</th>
                    <th className="border-r border-slate-200 px-2 py-1.5 text-right">Số tiền</th>
                    {!readOnly && <th className="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
                    const isWinner = ev.selectedSupplierId === s.supplierId
                    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',
                          isWinner
                            ? '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">
                            {isWinner && <span className="mr-1 text-base font-bold text-emerald-700">✓</span>}
                            <span className={cn(isWinner && '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="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
                                  ? isWinner
                                    ? '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', isWinner && '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={() => setWinner.mutate(s.supplierId)}
                                className={cn(
                                  'rounded px-1 py-0.5',
                                  isWinner ? 'bg-emerald-100 text-emerald-700' : 'text-slate-400 hover:bg-emerald-50 hover:text-emerald-700',
                                )}
                                title={isWinner ? 'NCC đã được chọn (winner)' : 'Chọn NCC thắng'}
                              >
                                <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,
  })

  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="relative">
            <Input
              type="text"
              inputMode="numeric"
              value={formatVndInput(form.thanhTien)}
              onChange={e => setForm({ thanhTien: parseVnd(e.target.value) })}
              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>
          <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>
    </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 deploy
  // (userName="" empty hoặc null). Build map từ data có sẵn PeDetailBundle:
  // drafter + approvals + approvalFlow + levelOpinions + departmentOpinions.
  // KHÔNG cần extra fetch /api/users (admin permission). Cover gần hết users
  // tham gia phiếu.
  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))
    }
  }

  function onPick(e: React.ChangeEvent<HTMLInputElement>) {
    const f = e.target.files?.[0]
    if (f) upload.mutate(f)
    e.target.value = ''
  }

  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"
            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))
    }
  }

  function onPick(e: React.ChangeEvent<HTMLInputElement>) {
    const f = e.target.files?.[0]
    if (f) upload.mutate(f)
    e.target.value = ''
  }

  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"
            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>
  )
}
