// "Tổng quan tất cả luồng duyệt" (fe-user / eOffice) — trang chủ của người dùng.
// [S159 2026-07-29 — owner chốt qua AskUser] Hero = dải pipeline 4 GIAI ĐOẠN xuyên
// suốt quy trình mua sắm → ký kết hợp đồng:
//   GĐ1  Duyệt NCC                 (bước 1 → 6, CEO chốt)   🟢 LIVE — module PE
//   GĐ2  Kế hoạch ký kết HĐ        (bước 7 → 12)            ⏳ sắp triển khai
//   GĐ3  Duyệt hợp đồng            (bước 13 → 18)           🟢 LIVE — module Contract
//   GĐ4  Hợp đồng cứng             (bước 19 → 21)           ⏳ sắp triển khai
//
// [S159 ĐỢT-4 — owner khoanh đỏ toàn bộ khối dưới hero]
//   "bỏ hết mấy cái này đi, hiện theo list chi tiết đàng hoàng luôn. Nghĩ cách bố
//    trí sao cho nó có danh sách phiếu mà User nhìn vào vẫn thấy đc tổng quan liên kết."
//   ⇒ GỠ: section "Việc của tôi" (5 thẻ KPI) + "HĐ gần đây" + "Phiếu Duyệt NCC gần đây".
//   ⇒ THAY bằng section "Danh sách phiếu toàn trình" = 2 TẦNG LIÊN KẾT:
//      · tầng 1 — thanh tab 4 giai đoạn mang COUNT (GĐ1 = tổng phiếu, GĐ3 = tổng HĐ,
//        GĐ2/GĐ4 badge "Sắp") ⇒ nhìn 1 phát biết khối lượng cả toàn trình.
//      · tầng 2 — cột "Hành trình" trên TỪNG DÒNG (mini-strip 4 chấm) ⇒ mỗi phiếu tự
//        kể nó đang đứng ở đâu trong 4 giai đoạn, không cần mở phiếu.
//      + bảng chi tiết full-width theo tab, PHÂN TRANG THẬT + Ô TÌM KIẾM (2 tính năng
//        này CÓ THẬT ở BE, xem "Nguồn số" bên dưới), bấm dòng → mở đúng phiếu/HĐ.
//   Hero GIỮ NGUYÊN (owner không khoanh) — chỉ đổi nguồn nhãn sang const STAGES.
// fe-user ONLY — KHÔNG mirror sang fe-admin (trang này là UX riêng của user).
//
// Nguồn số — ĐÃ ĐỌC CONTROLLER, KHÔNG suy đoán param:
//   · `GET /api/purchase-evaluations` (PurchaseEvaluationsController.cs:18-28) nhận
//     page · pageSize · search · sortDesc · type · phase · projectId · approvalWorkflowId
//     → PagedResult {items,total,page,pageSize,totalPages,hasNext,hasPrev}.
//     `search` lọc MaPhieu ∨ TenGoiThau ∨ Project.Name ∨ Project.Code
//     (PurchaseEvaluationFeatures.cs:624-632) — placeholder ghi đúng chừng đó.
//   · `GET /api/contracts` (ContractsController.cs:16-24) nhận page · pageSize · search
//     · sortDesc · phase · supplierId · projectId. `search` lọc MaHopDong ∨ TenHopDong
//     ∨ Supplier.Name ∨ Project.Name (ContractFeatures.cs:309-316).
//     ⇒ phân trang + tìm kiếm là THẬT ở cả 2 bảng, không phải cắt tính năng nào.
//   · ⚠️ KHÔNG có query-param `pendingMe` ở BE — đó là ENDPOINT RIÊNG trả MẢNG phẳng
//     (`/purchase-evaluations/inbox`, controller:30-35); `pendingMe=1` chỉ là URL param
//     của TRANG list. `phase` thì đúng là query-param BE.
//   · GĐ1 tổng phiếu / GĐ3 tổng HĐ = query `pageSize=1` chỉ lấy `.total` (rẻ nhất).
//   · GĐ3 KPI (`chờ tôi duyệt` / `quá hạn`) = ['my-dashboard'] — GetMyDashboardQuery.cs:51-75
//     chỉ đếm bảng Contracts ⇒ gán vào GĐ3 là đúng nghĩa.
//   · GĐ2/GĐ4 = KHÔNG bịa số — badge "Sắp triển khai" + mô tả owner đưa.
//   · Bảng chi tiết LAZY theo tab (`enabled: stage === n`) ⇒ GĐ2/GĐ4 bắn 0 request,
//     và không bao giờ bắn cả 4 nguồn cùng lúc.
//
// ⚠️ Gotcha CSS (index.css): `h1..h4`, `.stat-value`, `.icon-chip` là rule KHÔNG
// nằm trong @layer ⇒ thắng mọi utility Tailwind v4 (layer utilities). Muốn đổi màu
// chữ heading / số thống kê PHẢI dùng dấu `!` (vd `text-brand-800!`).
// ⚠️ Contrast: brand-600 trên nền trắng chỉ 4.41:1 (hụt AA) ⇒ nền tab active dùng
// brand-700 (5.77:1 với chữ trắng) / teal-700 (5.82:1), badge trên tab active là
// pill TRẮNG chữ brand-800 (7.75:1) chứ không phải white/20 (3.85:1 — trượt AA).
import { useEffect, useRef, useState } from 'react'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import {
  AlertTriangle,
  ArrowRight,
  ChevronLeft,
  ChevronRight,
  ClipboardCheck,
  FileSignature,
  FileText,
  Hourglass,
  ListChecks,
  Plus,
  RefreshCw,
  Search,
  SearchX,
  Stamp,
  X,
} from 'lucide-react'
import { PageHeader } from '@/components/PageHeader'
import { PhaseBadge } from '@/components/PhaseBadge'
import { EmptyState } from '@/components/EmptyState'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { useAuth } from '@/contexts/AuthContext'
import { api } from '@/lib/api'
import { cn } from '@/lib/cn'
import type { Paged } from '@/types/master'
import { ContractPhase, type ContractListItem } from '@/types/contracts'
import { ContractTypeLabel } from '@/types/forms'
import { PeFinalizeChip } from '@/components/pe/PeFinalizeChip'
import { PeUrgentChips } from '@/components/pe/PeUrgentChips'
import {
  getPeDisplayStatus,
  PeDisplayStatus,
  PeDisplayStatusColor,
  PeDisplayStatusLabel,
  type PeListItem,
} from '@/types/purchaseEvaluation'

const PAGE_SIZE = 10

const fmtMoney = (v: number) => {
  if (v >= 1_000_000_000) return (v / 1_000_000_000).toFixed(1) + ' tỷ'
  if (v >= 1_000_000) return (v / 1_000_000).toFixed(1) + ' tr'
  return v.toLocaleString('vi-VN')
}

const fmtDate = (iso?: string | null) => (iso ? new Date(iso).toLocaleDateString('vi-VN') : '—')

/* ─────────────────────────────────────────────────────────────────────────────
   4 GIAI ĐOẠN — SINGLE SOURCE cho CẢ hero LẪN thanh tab.
   Nhãn 4 GĐ đã bị sửa qua 3 đợt; hero và tab mà chép tay 2 nơi thì drift lần 4 là
   chắc chắn. Khai 1 chỗ ⇒ tab echo đúng hero DO CẤU TRÚC, không do cẩn thận.
   Tên chốt khớp `components/pe/PePipelineStrip.tsx` (owner vẽ tab lên ảnh phiếu).
   ───────────────────────────────────────────────────────────────────────────── */

type StageTone = 'brand' | 'teal'

type StageMeta = {
  n: number
  title: string
  stepRange: string
  icon: React.ComponentType<{ className?: string }>
  /** undefined = giai đoạn chưa triển khai (khung mờ, không có số). */
  tone?: StageTone
  /** Câu mô tả cho giai đoạn chưa triển khai (hero + panel dùng chung). */
  soonDesc?: string
}

const STAGES: StageMeta[] = [
  { n: 1, title: 'Duyệt NCC', stepRange: 'Bước 1 → 6 · CEO chốt', icon: ClipboardCheck, tone: 'brand' },
  {
    n: 2,
    title: 'Kế hoạch ký kết HĐ',
    stepRange: 'Bước 7 → 12',
    icon: FileSignature,
    soonDesc: 'Phiếu nối tiếp từ phiếu Duyệt NCC đã duyệt — chốt giá trị ký kết.',
  },
  { n: 3, title: 'Duyệt hợp đồng', stepRange: 'Bước 13 → 18', icon: FileText, tone: 'teal' },
  {
    n: 4,
    title: 'Hợp đồng cứng',
    stepRange: 'Bước 19 → 21',
    icon: Stamp,
    soonDesc: 'Tải lên bản hợp đồng đã ký cứng.',
  },
]

const stageOf = (n: number) => STAGES.find(s => s.n === n) ?? STAGES[0]

/* ─────────────────────────────────────────────────────────────────────────────
   HERO — dải pipeline 4 giai đoạn (GIỮ NGUYÊN, owner đã duyệt)
   ───────────────────────────────────────────────────────────────────────────── */

/* ─────────────────────────────────────────────────────────────────────────────
   TẦNG LIÊN KẾT 2 — "Hành trình": mini-strip 4 chấm trên TỪNG DÒNG.
   Chấm của chính giai đoạn dòng đó mang trạng thái phiếu; 3 chấm còn lại mờ +
   tooltip tên giai đoạn ⇒ user đọc 1 dòng là biết phiếu đang đứng đâu trong 21 bước.
   KHÔNG suy diễn "có HĐ ⇒ GĐ1/GĐ2 xong": cầu nối per-phiếu giữa các giai đoạn CHƯA
   có (PePipelineStrip.tsx:8-9 nói rõ) — bịa ra là nói dối user.
   ───────────────────────────────────────────────────────────────────────────── */

type DotState = 'done' | 'running' | 'returned' | 'draft' | 'stopped' | 'linked' | 'none'

const DOT_TEXT: Record<DotState, string> = {
  done: 'đã duyệt xong',
  running: 'đang chạy quy trình duyệt',
  returned: 'bị trả lại, cần chỉnh sửa',
  draft: 'còn là bản nháp',
  stopped: 'đã từ chối — dừng tại đây',
  // [FLAG-2 reviewer đợt-4] 'chưa tới' là lời KHẲNG ĐỊNH sai với chấm PHÍA TRƯỚC
  // giai đoạn hiện tại (HĐ ở GĐ3 thì GĐ1/GĐ2 là quá khứ, không phải "chưa tới").
  none: 'chưa có thông tin ở giai đoạn này',
  linked: 'đã sinh hợp đồng — theo dõi ở giai đoạn 3',
}

function dotVisual(state: DotState, tone: StageTone): { className: string; style?: React.CSSProperties } {
  const solid = tone === 'brand' ? 'var(--color-brand-600)' : 'var(--color-teal-600)'
  const mid = tone === 'brand' ? 'var(--color-brand-500)' : 'var(--color-teal-500)'
  switch (state) {
    case 'done':
      return { className: 'border-transparent', style: { background: solid } }
    case 'running':
      // Nửa chấm = đang đi giữa chừng. Gradient cứng 50/50, không phải mờ dần.
      return { className: '', style: { background: `linear-gradient(90deg, ${mid} 50%, #ffffff 50%)`, borderColor: mid } }
    case 'returned':
      return {
        className: '',
        style: {
          background: 'linear-gradient(90deg, var(--color-amberx-500) 50%, #ffffff 50%)',
          borderColor: 'var(--color-amberx-500)',
        },
      }
    case 'draft':
      return { className: 'border-slate-300 bg-white' }
    case 'stopped':
      return { className: 'border-transparent', style: { background: 'var(--color-accent-500)' } }
    case 'linked':
      // Viền teal đậm, ruột trắng — "có vật ở GĐ3" nhưng KHÔNG khẳng định trạng thái của nó.
      return { className: 'bg-white', style: { borderColor: 'var(--color-teal-600)', borderWidth: 2 } }
    default:
      return { className: 'border-transparent bg-slate-200' }
  }
}

// [FLAG-2 đợt-4] Nhận TRẠNG THÁI TỪNG CHẤM — aria chỉ kể các chấm CÓ thông tin,
// không khẳng định hộ các chấm không biết.
function JourneyDots({ dots, tone }: { dots: DotState[]; tone: StageTone }) {
  const known = dots
    .map((d, i) => (d === 'none' ? null : `giai đoạn ${i + 1} ${STAGES[i].title} — ${DOT_TEXT[d]}`))
    .filter((x): x is string => x !== null)
  return (
    <span
      role="img"
      aria-label={
        known.length > 0
          ? `Hành trình toàn trình: ${known.join('; ')}.`
          : 'Hành trình toàn trình: chưa có thông tin.'
      }
      className="inline-flex items-center"
    >
      {STAGES.map((s, i) => {
        const st: DotState = dots[i] ?? 'none'
        const v = dotVisual(st, tone)
        return (
          <span key={s.n} className="inline-flex items-center">
            <span
              aria-hidden
              title={`Giai đoạn ${s.n} · ${s.title} — ${DOT_TEXT[st]}`}
              className={cn('h-2.5 w-2.5 shrink-0 rounded-full border', v.className)}
              style={v.style}
            />
            {i < STAGES.length - 1 && <span aria-hidden className="h-px w-2 shrink-0 bg-slate-200" />}
          </span>
        )
      })}
    </span>
  )
}

const peDot = (phase: number): DotState => {
  const s = getPeDisplayStatus(phase)
  if (s === PeDisplayStatus.DaDuyet) return 'done'
  if (s === PeDisplayStatus.TuChoi) return 'stopped'
  if (s === PeDisplayStatus.TraLai) return 'returned'
  if (s === PeDisplayStatus.Nhap) return 'draft'
  return 'running'
}

const contractDot = (phase: number): DotState => {
  if (phase === ContractPhase.TuChoi) return 'stopped'
  if (phase === ContractPhase.TraLai) return 'returned'
  if (phase === ContractPhase.DangChon || phase === ContractPhase.DangSoanThao) return 'draft'
  return 'running'
}

// [FLAG-2 ca-3 đợt-4] DangDongDau(b.19-20)/DaPhatHanh(b.21) chính là phần việc GĐ4
// "Hợp đồng cứng" theo định nghĩa trang này ⇒ chấm 4 nhận trạng thái, chấm 3 = done
// (suy diễn TIẾN-TRÌNH trong CÙNG module HĐ — phase tuyến tính — hợp lệ; GĐ1/GĐ2
// KHÔNG biết ⇒ trung tính).
const contractDots = (phase: number): DotState[] => {
  if (phase === ContractPhase.DaPhatHanh) return ['none', 'none', 'done', 'done']
  if (phase === ContractPhase.DangDongDau) return ['none', 'none', 'done', 'running']
  return ['none', 'none', contractDot(phase), 'none']
}

// [FLAG-2 ca-2] Chấm 3 của dòng phiếu dùng field THẬT `contractId` trong payload
// (types/purchaseEvaluation.ts:126): chỉ nói "đã sinh HĐ", không đoán hơn.
const peDots = (phase: number, contractId: string | null): DotState[] => [
  peDot(phase),
  'none',
  contractId ? 'linked' : 'none',
  'none',
]

/* ─────────────────────────────────────────────────────────────────────────────
   TẦNG LIÊN KẾT 1 — thanh tab 4 giai đoạn (segmented), mang COUNT
   ───────────────────────────────────────────────────────────────────────────── */

const TAB_TONE: Record<'brand' | 'teal' | 'soon', {
  active: string; idle: string; chip: string; badge: string
}> = {
  brand: {
    active: 'border-brand-700 bg-brand-700 text-white shadow-sm shadow-brand-700/25',
    idle: 'border-slate-200 bg-white text-slate-700 hover:border-brand-300 hover:bg-brand-50',
    chip: 'bg-brand-50 text-brand-600',
    badge: 'bg-white text-brand-800',
  },
  teal: {
    active: 'border-teal-700 bg-teal-700 text-white shadow-sm shadow-teal-700/25',
    idle: 'border-slate-200 bg-white text-slate-700 hover:border-teal-500 hover:bg-teal-50',
    chip: 'bg-teal-50 text-teal-700',
    badge: 'bg-white text-teal-700',
  },
  soon: {
    active: 'border-slate-600 bg-slate-600 text-white shadow-sm',
    idle: 'border-dashed border-slate-300 bg-white text-slate-600 hover:border-slate-400 hover:bg-slate-100',
    chip: 'bg-slate-100 text-slate-500',
    badge: 'bg-white text-slate-600',
  },
}

function StageTab({
  stage,
  active,
  count,
  loading,
  onSelect,
  onKeyDown,
  btnRef,
}: {
  stage: StageMeta
  active: boolean
  count?: number
  loading?: boolean
  onSelect: () => void
  onKeyDown: (e: React.KeyboardEvent<HTMLButtonElement>) => void
  btnRef: (el: HTMLButtonElement | null) => void
}) {
  const { n, title, icon: Icon, tone } = stage
  const t = TAB_TONE[tone ?? 'soon']
  const soon = !tone
  return (
    <button
      ref={btnRef}
      type="button"
      role="tab"
      id={`stage-tab-${n}`}
      aria-selected={active}
      aria-controls="stage-panel"
      tabIndex={active ? 0 : -1}
      onClick={onSelect}
      onKeyDown={onKeyDown}
      className={cn(
        'flex min-w-[10.5rem] flex-1 items-center gap-2.5 rounded-xl border px-2.5 py-2 text-left transition',
        'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-1',
        active ? t.active : t.idle,
      )}
    >
      <span
        aria-hidden
        className={cn(
          'flex h-7 w-7 shrink-0 items-center justify-center rounded-lg',
          active ? 'bg-white/15 text-white' : t.chip,
        )}
      >
        <Icon className="h-4 w-4" />
      </span>
      <span className="min-w-0 flex-1">
        <span
          className={cn(
            'block text-[10px] font-bold uppercase tracking-wide',
            active ? 'text-white/90' : 'text-slate-500',
          )}
        >
          Giai đoạn {n}
        </span>
        <span className="block truncate text-[13px] font-semibold leading-tight">{title}</span>
      </span>
      {soon ? (
        <span
          className={cn(
            'shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide',
            active ? t.badge : 'bg-slate-100 text-slate-600',
          )}
        >
          Sắp
        </span>
      ) : (
        <span
          className={cn(
            'shrink-0 rounded-full px-1.5 py-0.5 text-center text-[11px] font-bold tabular-nums',
            'min-w-[1.75rem]',
            active ? t.badge : 'bg-slate-100 text-slate-700',
          )}
        >
          {loading ? '…' : (count ?? '—')}
        </span>
      )}
    </button>
  )
}

/* ─────────────────────────────────────────────────────────────────────────────
   Mảnh dùng chung của panel: toolbar tìm kiếm · skeleton · phân trang
   ───────────────────────────────────────────────────────────────────────────── */

function ListToolbar({
  value,
  onChange,
  onClear,
  placeholder,
  right,
}: {
  value: string
  onChange: (v: string) => void
  // [FLAG-7 đợt-4] X phải xoá NGAY (cả input lẫn từ khoá đã áp) — onChange('') chỉ đổi
  // input rồi chờ debounce 300ms ⇒ hai nút "xoá tìm kiếm" phản hồi khác nhau.
  onClear: () => void
  placeholder: string
  right?: React.ReactNode
}) {
  return (
    <div className="flex flex-wrap items-center gap-2 border-b border-slate-100 px-3 py-2.5 sm:px-4">
      <div className="relative min-w-0 flex-1 sm:max-w-sm">
        <Search
          aria-hidden
          className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400"
        />
        <Input
          className="pl-8 pr-8"
          value={value}
          placeholder={placeholder}
          aria-label={placeholder}
          onChange={e => onChange(e.target.value)}
        />
        {value !== '' && (
          <button
            type="button"
            onClick={onClear}
            aria-label="Xoá ô tìm kiếm"
            // 24×24 = sàn WCAG 2.5.8 cho vùng bấm.
            className="absolute right-1 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
          >
            <X className="h-3.5 w-3.5" />
          </button>
        )}
      </div>
      {right && <div className="ml-auto flex items-center gap-2">{right}</div>}
    </div>
  )
}

// Skeleton dùng 1 ô colSpan — KHÔNG dựng cell theo từng cột, tránh lệch với các
// cột bị ẩn theo breakpoint (hidden md/lg/xl:table-cell).
function TableSkeleton({ cols, rows = 6 }: { cols: number; rows?: number }) {
  return (
    <tbody>
      {Array.from({ length: rows }).map((_, i) => (
        <tr key={i} className="border-t border-slate-100">
          <td colSpan={cols} className="px-3 py-3 sm:px-4">
            <span className="flex items-center gap-3">
              <span className="h-3 w-24 shrink-0 animate-pulse rounded bg-slate-100 motion-reduce:animate-none" />
              <span className="h-3 flex-1 animate-pulse rounded bg-slate-100 motion-reduce:animate-none" />
              <span className="h-4 w-20 shrink-0 animate-pulse rounded-full bg-slate-100 motion-reduce:animate-none" />
            </span>
          </td>
        </tr>
      ))}
    </tbody>
  )
}

function Pager({
  data,
  unit,
  page,
  onPage,
}: {
  data?: Paged<unknown>
  unit: string
  // [FLAG-3 đợt-4] trang ĐANG CHỌN (state) — nút ± tính từ đây, không từ data.page
  // (data là ảnh chụp request đã về; bấm nhanh 2 lần khi request đang bay ⇒ chỉ đi 1 trang).
  page: number
  onPage: (p: number) => void
}) {
  if (!data) return null
  if (data.total === 0)
    // [FLAG-4 đợt-4] giữ vùng thông báo cho screen-reader khi kết quả về 0
    // (aria-live bị gỡ khỏi cây thì không ai được báo).
    return (
      <p role="status" className="sr-only">
        Không tìm thấy {unit} nào khớp.
      </p>
    )
  const from = (data.page - 1) * data.pageSize + 1
  const to = Math.min(data.page * data.pageSize, data.total)
  return (
    <div className="flex flex-wrap items-center justify-between gap-2 border-t border-slate-100 px-3 py-2.5 sm:px-4">
      <p aria-live="polite" className="text-[11px] text-slate-600">
        Hiển thị <span className="font-semibold tabular-nums text-slate-800">{from}–{to}</span> trong{' '}
        <span className="font-semibold tabular-nums text-slate-800">{data.total}</span> {unit}
      </p>
      <div className="flex items-center gap-1.5">
        <Button variant="outline" size="sm" disabled={!data.hasPrev} onClick={() => onPage(Math.max(1, page - 1))}>
          <ChevronLeft className="h-3.5 w-3.5" />
          Trước
        </Button>
        <span className="px-1 text-[11px] font-semibold tabular-nums text-slate-600">
          Trang {data.page}/{Math.max(data.totalPages, 1)}
        </span>
        <Button variant="outline" size="sm" disabled={!data.hasNext} onClick={() => onPage(page + 1)}>
          Sau
          <ChevronRight className="h-3.5 w-3.5" />
        </Button>
      </div>
    </div>
  )
}

const TH = 'px-3 py-2 font-bold sm:px-4'
const THEAD_ROW = 'border-b border-slate-200 bg-slate-50/80 text-[10px] uppercase tracking-wide text-slate-500'

/* ─────────────────────────────────────────────────────────────────────────────
   BẢNG GĐ1 — phiếu Duyệt NCC
   ───────────────────────────────────────────────────────────────────────────── */

const PE_COLS = 7

function PeTable({
  items,
  isLoading,
  isError,
  isFetching,
  hasSearch,
  onRetry,
  onOpen,
  onCreate,
  onClearSearch,
}: {
  items?: PeListItem[]
  isLoading: boolean
  isError: boolean
  isFetching: boolean
  hasSearch: boolean
  onRetry: () => void
  onOpen: (id: string) => void
  onCreate: () => void
  onClearSearch: () => void
}) {
  return (
    <div className="overflow-x-auto">
      <table className="w-full border-collapse text-left">
        <caption className="sr-only">
          Danh sách phiếu Duyệt NCC — giai đoạn 1 của toàn trình. Cột Hành trình cho biết mỗi phiếu đang
          đứng ở giai đoạn nào.
        </caption>
        <thead>
          <tr className={THEAD_ROW}>
            <th scope="col" className={TH}>Mã phiếu</th>
            <th scope="col" className="px-3 py-2 font-bold">Gói thầu</th>
            <th scope="col" className="hidden px-3 py-2 font-bold lg:table-cell">Dự án</th>
            <th scope="col" className="hidden px-3 py-2 font-bold xl:table-cell">Người soạn</th>
            <th scope="col" className="hidden px-3 py-2 font-bold md:table-cell">Ngày</th>
            <th scope="col" className={TH}>Trạng thái</th>
            <th scope="col" className="hidden px-3 py-2 font-bold md:table-cell">Hành trình</th>
          </tr>
        </thead>

        {isLoading ? (
          <TableSkeleton cols={PE_COLS} />
        ) : (
          <tbody
            aria-busy={isFetching}
            className={cn(
              'divide-y divide-slate-100',
              isFetching && 'opacity-60 transition-opacity motion-reduce:transition-none',
            )}
          >
            {isError && (
              <tr>
                <td colSpan={PE_COLS}>
                  <EmptyState
                    icon={AlertTriangle}
                    title="Không tải được danh sách phiếu"
                    description="Kiểm tra kết nối rồi thử lại."
                    action={
                      <Button variant="outline" size="sm" onClick={onRetry}>
                        <RefreshCw className="h-3.5 w-3.5" />
                        Thử lại
                      </Button>
                    }
                  />
                </td>
              </tr>
            )}

            {!isError && (items?.length ?? 0) === 0 && (
              <tr>
                <td colSpan={PE_COLS}>
                  <EmptyState
                    icon={hasSearch ? SearchX : ClipboardCheck}
                    title={hasSearch ? 'Không tìm thấy phiếu nào khớp' : 'Chưa có phiếu Duyệt NCC nào'}
                    description={
                      hasSearch
                        ? 'Ô tìm kiếm lọc theo mã phiếu, tên gói thầu, tên hoặc mã dự án — thử từ khoá khác.'
                        : 'Phiếu Duyệt NCC sẽ hiển thị tại đây ngay khi được tạo.'
                    }
                    action={
                      hasSearch ? (
                        <Button variant="outline" size="sm" onClick={onClearSearch}>
                          <X className="h-3.5 w-3.5" />
                          Xoá tìm kiếm
                        </Button>
                      ) : (
                        <Button size="sm" onClick={onCreate}>
                          <Plus className="h-3.5 w-3.5" />
                          Tạo phiếu Duyệt NCC
                        </Button>
                      )
                    }
                  />
                </td>
              </tr>
            )}

            {!isError &&
              items?.map(p => (
                <tr
                  key={p.id}
                  onClick={() => onOpen(p.id)}
                  className="cursor-pointer transition hover:bg-brand-50/60"
                >
                  <th scope="row" className="px-3 py-2.5 align-top font-normal sm:px-4">
                    <button
                      type="button"
                      onClick={e => {
                        e.stopPropagation()
                        onOpen(p.id)
                      }}
                      aria-label={`Mở phiếu ${p.maPhieu ?? 'chưa cấp mã'} — ${p.tenGoiThau}`}
                      className="rounded font-mono text-[12px] font-semibold text-brand-700 underline-offset-2 transition hover:text-brand-800 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
                    >
                      {p.maPhieu ?? '(chưa cấp mã)'}
                    </button>
                  </th>

                  <td className="w-full max-w-0 px-3 py-2.5 align-top">
                    <div className="flex min-w-0 items-center gap-1.5">
                      <span className="truncate text-[13px] font-medium text-slate-900">{p.tenGoiThau}</span>
                      <PeUrgentChips isUrgentByPro={p.isUrgentByPro} isUrgentByCcm={p.isUrgentByCcm} />
                    </div>
                    {p.workItemName && (
                      <div className="mt-0.5 hidden truncate text-[11px] text-slate-500 lg:block">
                        {p.workItemName}
                      </div>
                    )}
                    {/* Mobile: gom Dự án + Ngày vào đây vì 2 cột đó ẩn dưới lg/md. */}
                    <div className="mt-0.5 truncate text-[11px] text-slate-500 lg:hidden">
                      {p.projectName} · {fmtDate(p.updatedAt ?? p.createdAt)}
                    </div>
                    {/* [FLAG-5 đợt-4] cột Hành trình ẩn <md ⇒ tầng liên kết 2 (điểm chủ đạo
                        của đợt) biến mất trên điện thoại — đưa 4 chấm vào dòng phụ. */}
                    <div className="mt-1 md:hidden">
                      <JourneyDots tone="brand" dots={peDots(p.phase, p.contractId)} />
                    </div>
                  </td>

                  <td className="hidden px-3 py-2.5 align-top lg:table-cell">
                    <div className="max-w-[12rem] truncate text-[12px] text-slate-700">{p.projectName}</div>
                    {p.projectCode && (
                      <div className="max-w-[12rem] truncate font-mono text-[10px] text-slate-500">
                        {p.projectCode}
                      </div>
                    )}
                  </td>

                  <td className="hidden px-3 py-2.5 align-top xl:table-cell">
                    <div className="max-w-[10rem] truncate text-[12px] text-slate-700">
                      {p.drafterName ?? '—'}
                    </div>
                    {p.departmentName && (
                      <div className="max-w-[10rem] truncate text-[10px] text-slate-500">
                        {p.departmentName}
                      </div>
                    )}
                  </td>

                  <td className="hidden whitespace-nowrap px-3 py-2.5 align-top text-[12px] tabular-nums text-slate-600 md:table-cell">
                    {fmtDate(p.updatedAt ?? p.createdAt)}
                  </td>

                  <td className="px-3 py-2.5 align-top sm:px-4">
                    <div className="flex flex-wrap items-center gap-1">
                      <span
                        className={cn(
                          'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold ring-1 ring-inset ring-current/15',
                          PeDisplayStatusColor[getPeDisplayStatus(p.phase)],
                        )}
                      >
                        {PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
                      </span>
                      <PeFinalizeChip
                        endsBeforeCeo={p.endsBeforeCeo}
                        finalizeStepName={p.finalizeStepName}
                        phase={p.phase}
                      />
                    </div>
                  </td>

                  <td className="hidden px-3 py-2.5 align-top md:table-cell">
                    <JourneyDots tone="brand" dots={peDots(p.phase, p.contractId)} />
                  </td>
                </tr>
              ))}
          </tbody>
        )}
      </table>
    </div>
  )
}

/* ─────────────────────────────────────────────────────────────────────────────
   BẢNG GĐ3 — hợp đồng
   ───────────────────────────────────────────────────────────────────────────── */

const CT_COLS = 7

function ContractTable({
  items,
  isLoading,
  isError,
  isFetching,
  hasSearch,
  onRetry,
  onOpen,
  onCreate,
  onClearSearch,
}: {
  items?: ContractListItem[]
  isLoading: boolean
  isError: boolean
  isFetching: boolean
  hasSearch: boolean
  onRetry: () => void
  onOpen: (id: string) => void
  onCreate: () => void
  onClearSearch: () => void
}) {
  return (
    <div className="overflow-x-auto">
      <table className="w-full border-collapse text-left">
        <caption className="sr-only">
          Danh sách hợp đồng — giai đoạn 3 của toàn trình. Cột Hành trình cho biết mỗi hợp đồng đang đứng ở
          giai đoạn nào.
        </caption>
        <thead>
          <tr className={THEAD_ROW}>
            <th scope="col" className={TH}>Mã HĐ</th>
            <th scope="col" className="px-3 py-2 font-bold">Tên hợp đồng</th>
            <th scope="col" className="hidden px-3 py-2 font-bold lg:table-cell">Loại</th>
            <th scope="col" className="hidden px-3 py-2 font-bold xl:table-cell">Giá trị</th>
            <th scope="col" className="hidden px-3 py-2 font-bold md:table-cell">Ngày</th>
            <th scope="col" className={TH}>Trạng thái</th>
            <th scope="col" className="hidden px-3 py-2 font-bold md:table-cell">Hành trình</th>
          </tr>
        </thead>

        {isLoading ? (
          <TableSkeleton cols={CT_COLS} />
        ) : (
          <tbody
            aria-busy={isFetching}
            className={cn(
              'divide-y divide-slate-100',
              isFetching && 'opacity-60 transition-opacity motion-reduce:transition-none',
            )}
          >
            {isError && (
              <tr>
                <td colSpan={CT_COLS}>
                  <EmptyState
                    icon={AlertTriangle}
                    title="Không tải được danh sách hợp đồng"
                    description="Kiểm tra kết nối rồi thử lại."
                    action={
                      <Button variant="outline" size="sm" onClick={onRetry}>
                        <RefreshCw className="h-3.5 w-3.5" />
                        Thử lại
                      </Button>
                    }
                  />
                </td>
              </tr>
            )}

            {!isError && (items?.length ?? 0) === 0 && (
              <tr>
                <td colSpan={CT_COLS}>
                  <EmptyState
                    icon={hasSearch ? SearchX : FileText}
                    title={hasSearch ? 'Không tìm thấy hợp đồng nào khớp' : 'Chưa có hợp đồng nào'}
                    description={
                      hasSearch
                        ? 'Ô tìm kiếm lọc theo mã HĐ, tên HĐ, nhà cung cấp hoặc dự án — thử từ khoá khác.'
                        : 'Giai đoạn 3 sẽ có danh sách ngay khi hợp đồng đầu tiên được tạo.'
                    }
                    action={
                      hasSearch ? (
                        <Button variant="outline" size="sm" onClick={onClearSearch}>
                          <X className="h-3.5 w-3.5" />
                          Xoá tìm kiếm
                        </Button>
                      ) : (
                        <Button size="sm" onClick={onCreate}>
                          <Plus className="h-3.5 w-3.5" />
                          Tạo HĐ mới
                        </Button>
                      )
                    }
                  />
                </td>
              </tr>
            )}

            {!isError &&
              items?.map(c => (
                <tr
                  key={c.id}
                  onClick={() => onOpen(c.id)}
                  className="cursor-pointer transition hover:bg-teal-50/60"
                >
                  <th scope="row" className="px-3 py-2.5 align-top font-normal sm:px-4">
                    <button
                      type="button"
                      onClick={e => {
                        e.stopPropagation()
                        onOpen(c.id)
                      }}
                      aria-label={`Mở hợp đồng ${c.maHopDong ?? 'chưa cấp mã'} — ${c.tenHopDong ?? 'chưa đặt tên'}`}
                      className="rounded font-mono text-[12px] font-semibold text-teal-700 underline-offset-2 transition hover:text-teal-600 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-500"
                    >
                      {c.maHopDong ?? '(chưa cấp mã)'}
                    </button>
                  </th>

                  <td className="w-full max-w-0 px-3 py-2.5 align-top">
                    <div className="truncate text-[13px] font-medium text-slate-900">
                      {c.tenHopDong ?? '(chưa đặt tên)'}
                    </div>
                    <div className="mt-0.5 truncate text-[11px] text-slate-500">
                      {c.supplierName} · {c.projectName}
                    </div>
                    {/* Mobile: 3 cột dưới đây ẩn ⇒ gom giá trị + ngày vào dòng phụ. */}
                    <div className="mt-0.5 truncate text-[11px] text-slate-500 md:hidden">
                      {fmtMoney(c.giaTri)} · {fmtDate(c.createdAt)}
                    </div>
                  </td>

                  <td className="hidden whitespace-nowrap px-3 py-2.5 align-top text-[12px] text-slate-700 lg:table-cell">
                    {ContractTypeLabel[c.type] ?? '—'}
                  </td>

                  <td className="hidden whitespace-nowrap px-3 py-2.5 align-top text-[12px] font-medium tabular-nums text-slate-800 xl:table-cell">
                    {fmtMoney(c.giaTri)}
                  </td>

                  <td className="hidden whitespace-nowrap px-3 py-2.5 align-top text-[12px] tabular-nums text-slate-600 md:table-cell">
                    {fmtDate(c.createdAt)}
                  </td>

                  <td className="px-3 py-2.5 align-top sm:px-4">
                    <PhaseBadge phase={c.phase} className="text-[10px]" />
                  </td>

                  <td className="hidden px-3 py-2.5 align-top md:table-cell">
                    <JourneyDots tone="teal" dots={contractDots(c.phase)} />
                  </td>
                </tr>
              ))}
          </tbody>
        )}
      </table>
    </div>
  )
}

/* ─────────────────────────────────────────────────────────────────────────────
   Panel GĐ2 / GĐ4 — "Sắp triển khai" NGẮN (2-3 dòng), không lặp lại nguyên
   trang ComingSoon; nút dẫn sang `/coming-soon?stage=N` (route THẬT, đọc `stage`).
   ───────────────────────────────────────────────────────────────────────────── */

function SoonPanel({ stage, onOpenRoadmap }: { stage: StageMeta; onOpenRoadmap: () => void }) {
  const Icon = stage.icon
  return (
    <div className="px-4 py-9">
      <div className="mx-auto flex max-w-md flex-col items-center gap-2 text-center">
        <span
          className="icon-chip"
          style={{ ['--chip-bg' as string]: '#f1f5f9', ['--chip-fg' as string]: '#64748b' }}
        >
          <Icon className="h-4 w-4" />
        </span>
        <span className="inline-flex items-center gap-1 rounded-full bg-slate-200/80 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-slate-600">
          <Hourglass className="h-3 w-3" />
          Sắp triển khai
        </span>
        <h3 className="text-[15px] leading-tight text-slate-700!">
          Giai đoạn {stage.n} · {stage.title}
        </h3>
        <p className="text-xs leading-relaxed text-slate-600">{stage.soonDesc}</p>
        <p className="text-xs leading-relaxed text-slate-500">
          Phân hệ đang được xây dựng — danh sách phiếu của giai đoạn này sẽ hiện ngay tại đây khi chạy.
        </p>
        <Button variant="outline" size="sm" className="mt-1" onClick={onOpenRoadmap}>
          Xem lộ trình giai đoạn {stage.n}
          <ArrowRight className="h-3.5 w-3.5" />
        </Button>
      </div>
    </div>
  )
}

/* ─────────────────────────────────────────────────────────────────────────────
   TRANG
   ───────────────────────────────────────────────────────────────────────────── */

export function UserDashboardPage() {
  const navigate = useNavigate()
  const { user } = useAuth()

  /* ── Số liệu hero (5 query nền, y như trước — chỉ 2 query list rút pageSize 5 → 1
        vì 2 danh sách "gần đây" đã bị gỡ, hero chỉ cần `.total`). ───────────── */

  const peTotalQuery = useQuery({
    queryKey: ['pipeline-pe-total'],
    queryFn: async () =>
      (await api.get<Paged<PeListItem>>('/purchase-evaluations', { params: { page: 1, pageSize: 1 } })).data
        .total,
  })

  const contractTotalQuery = useQuery({
    queryKey: ['pipeline-contract-total'],
    queryFn: async () =>
      (await api.get<Paged<ContractListItem>>('/contracts', { params: { page: 1, pageSize: 1 } })).data.total,
  })

  /* ── Section "Danh sách phiếu toàn trình" ──────────────────────────────── */

  const [stage, setStage] = useState(1)
  const [page, setPage] = useState(1)
  const [searchInput, setSearchInput] = useState('')
  const [search, setSearch] = useState('')
  const tabRefs = useRef<(HTMLButtonElement | null)[]>([])

  // Gõ tới đâu bắn request tới đó = phí. Chờ 300ms im mới commit, và chỉ khi giá
  // trị THỰC SỰ đổi (gõ rồi xoá về như cũ thì không reset trang đang xem).
  useEffect(() => {
    const v = searchInput.trim()
    if (v === search) return
    const t = setTimeout(() => {
      setSearch(v)
      setPage(1)
    }, 300)
    return () => clearTimeout(t)
  }, [searchInput, search])

  const selectStage = (n: number) => {
    if (n === stage) return
    setStage(n)
    setPage(1)
    setSearchInput('')
    setSearch('')
  }

  // Bàn phím trong tablist theo WAI-ARIA: ←/→ cuộn vòng, Home/End nhảy biên.
  const onTabKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>, i: number) => {
    const last = STAGES.length - 1
    let next = -1
    if (e.key === 'ArrowRight') next = i === last ? 0 : i + 1
    else if (e.key === 'ArrowLeft') next = i === 0 ? last : i - 1
    else if (e.key === 'Home') next = 0
    else if (e.key === 'End') next = last
    if (next < 0) return
    e.preventDefault()
    selectStage(STAGES[next].n)
    tabRefs.current[next]?.focus()
  }

  const clearSearch = () => {
    setSearchInput('')
    setSearch('')
    setPage(1)
  }

  const peList = useQuery({
    queryKey: ['dash-stage-pe', { page, pageSize: PAGE_SIZE, search }],
    queryFn: async () =>
      (
        await api.get<Paged<PeListItem>>('/purchase-evaluations', {
          params: { page, pageSize: PAGE_SIZE, search: search || undefined },
        })
      ).data,
    enabled: stage === 1,
    placeholderData: keepPreviousData,
  })

  const contractList = useQuery({
    queryKey: ['dash-stage-contracts', { page, pageSize: PAGE_SIZE, search }],
    queryFn: async () =>
      (
        await api.get<Paged<ContractListItem>>('/contracts', {
          params: { page, pageSize: PAGE_SIZE, search: search || undefined },
        })
      ).data,
    enabled: stage === 3,
    placeholderData: keepPreviousData,
  })

  const activeStage = stageOf(stage)

  return (
    <div className="p-6">
      <PageHeader
        title="Tổng quan quy trình"
        description={`Xin chào ${user?.fullName ?? user?.email ?? 'bạn'} — toàn bộ hành trình từ Duyệt NCC đến Ký kết hợp đồng, 4 giai đoạn. Bấm vào số liệu để mở đúng danh sách.`}
      />

      {/* [S159-dot5 owner: "O dep day, vay bo cai tren di nhe, cai duoi OK day"]
          HERO pipeline 4 card DA GO — tang tong-quan-lien-ket nay la thanh tab
          4 GD cua Danh sach chi tiet ben duoi (count + cot Hanh trinh per-row). */}

      {/* ── DANH SÁCH PHIẾU TOÀN TRÌNH ───────────────────────────────────── */}
      <section className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
        <header className="flex flex-wrap items-center gap-2.5 border-b border-slate-200 px-4 py-3">
          <span className="icon-chip shrink-0">
            <ListChecks className="h-4 w-4" />
          </span>
          <div className="min-w-0 flex-1">
            <div className="text-[11px] font-bold uppercase tracking-[0.07em] text-brand-700">
              Danh sách chi tiết
            </div>
            <h2 className="text-sm leading-tight">Danh sách phiếu toàn trình</h2>
          </div>
          <p className="hidden text-[11px] leading-relaxed text-slate-500 md:block">
            Chọn giai đoạn để xem danh sách của giai đoạn đó.
            <br />
            Cột <span className="font-semibold text-slate-600">Hành trình</span> cho biết từng phiếu đang đứng
            ở đâu.
          </p>
        </header>

        {/* Tầng liên kết 1 — thanh tab 4 giai đoạn mang count */}
        <div className="border-b border-slate-200 bg-slate-50/70 px-3 py-2.5">
          <div
            role="tablist"
            aria-label="Giai đoạn của toàn trình"
            className="flex gap-2 overflow-x-auto pb-0.5"
          >
            {STAGES.map((st, i) => (
              <StageTab
                key={st.n}
                stage={st}
                active={stage === st.n}
                count={st.n === 1 ? peTotalQuery.data : st.n === 3 ? contractTotalQuery.data : undefined}
                loading={st.n === 1 ? peTotalQuery.isLoading : st.n === 3 ? contractTotalQuery.isLoading : false}
                onSelect={() => selectStage(st.n)}
                onKeyDown={e => onTabKeyDown(e, i)}
                btnRef={el => {
                  tabRefs.current[i] = el
                }}
              />
            ))}
          </div>
        </div>

        {/* Tầng liên kết 2 — bảng chi tiết theo tab đang chọn */}
        <div id="stage-panel" role="tabpanel" aria-labelledby={`stage-tab-${stage}`}>
          {stage === 1 && (
            <>
              <ListToolbar
                value={searchInput}
                onChange={setSearchInput}
                onClear={clearSearch}
                placeholder="Tìm mã phiếu, gói thầu, dự án…"
                right={
                  <Button variant="outline" size="sm" onClick={() => navigate('/purchase-evaluations')}>
                    Mở màn Duyệt NCC
                    <ArrowRight className="h-3.5 w-3.5" />
                  </Button>
                }
              />
              <PeTable
                items={peList.data?.items}
                isLoading={peList.isLoading}
                isError={peList.isError}
                isFetching={peList.isFetching}
                hasSearch={search !== ''}
                onRetry={() => void peList.refetch()}
                onOpen={id => navigate(`/purchase-evaluations/${id}`)}
                onCreate={() => navigate('/purchase-evaluations/new')}
                onClearSearch={clearSearch}
              />
              <Pager data={peList.data} unit="phiếu" page={page} onPage={setPage} />
            </>
          )}

          {stage === 3 && (
            <>
              <ListToolbar
                value={searchInput}
                onChange={setSearchInput}
                onClear={clearSearch}
                placeholder="Tìm mã HĐ, tên HĐ, nhà cung cấp, dự án…"
                right={
                  <Button variant="outline" size="sm" onClick={() => navigate('/my-contracts')}>
                    Mở màn Hợp đồng
                    <ArrowRight className="h-3.5 w-3.5" />
                  </Button>
                }
              />
              <ContractTable
                items={contractList.data?.items}
                isLoading={contractList.isLoading}
                isError={contractList.isError}
                isFetching={contractList.isFetching}
                hasSearch={search !== ''}
                onRetry={() => void contractList.refetch()}
                onOpen={id =>
                  // [FLAG-1 đợt-4] MyContractsPage render detail ở panel `hidden lg:block`
                  // ⇒ mobile mở ?id= là ngõ cụt. Mirror ĐÚNG nhánh trang đích (:62-67).
                  window.matchMedia('(min-width: 1024px)').matches
                    ? navigate(`/my-contracts?id=${id}`)
                    : navigate(`/contracts/${id}`)
                }
                onCreate={() => navigate('/contracts/new')}
                onClearSearch={clearSearch}
              />
              <Pager data={contractList.data} unit="hợp đồng" page={page} onPage={setPage} />
            </>
          )}

          {(stage === 2 || stage === 4) && (
            <SoonPanel
              stage={activeStage}
              onOpenRoadmap={() => navigate(`/coming-soon?stage=${activeStage.n}`)}
            />
          )}
        </div>
      </section>
    </div>
  )
}
