// [S171 KHKK 3-panel] PANEL 1 của màn "Kế hoạch ký kết HĐ" (GĐ2) — mirror bố-cục
// Duyệt NCC (GĐ1) / Hợp đồng (GĐ3): khối filter trên cùng + thân danh sách cuộn riêng.
// File MIRROR SHA256 identical với fe-admin counterpart.
//
// 🔴 KHỐI Ý-KIẾN / SƠ-ĐỒ DUYỆT là bản thứ 3 của cùng một hình (PE `PeDetailTabs.tsx:705-774`
// · HĐ `components/contracts/WorkflowHistoryPanel.tsx`). Gộp được khi và chỉ khi BE 3 module
// thống-nhất shape `status` precompute. Trước đó, gộp = đẻ `if (module === …)`.
//
// ══ OWNER CHỐT 2026-08-04 (QĐ-1): panel 1 = CÂY TOÀN TRÌNH `PipelineTreePanel currentStage={2}`.
// Chọn HÌNH ≠ chấp-nhận VỠ ⇒ 4 mitigation dưới đây là BẮT BUỘC, không phải tuỳ chọn:
//   M-1  `canPe` (`usePipelineStages.ts:83`) khoá cây bằng 3 key của Duyệt NCC. Người CHỈ có
//        quyền KHKK (vd người duyệt nhóm N5) sẽ thấy cây trống trong khi API
//        `GET /contract-signing-plans` (policy `KeHoachKyKet.Read`) VẪN cho họ đọc — đúng mặt
//        còn lại của gotcha #82. ⇒ `!canPe` FALLBACK sang DANH-SÁCH PHẲNG cùng ô này.
//        Cây là lớp LÀM GIÀU, KHÔNG phải điều-kiện để thấy phiếu.
//   M-2  Leaf/card KHKK trên cây KHÔNG `navigate('/khkk/:id')` nữa mà gọi `onSelect` của host
//        (`usePipelineStages.ts` `onOpenKhkk`) — không thì bấm 1 cái là văng khỏi màn 3-panel.
//   B1   Truyền `stage2Content` PHẢI kèm `stage2Count` (`PipelineStageFolders.tsx:95-97`).
//        Trade-off khai thẳng: trong cây của trang này, folder GĐ2 hiện CARD PHIẾU thay cho
//        8 ngăn N1-N8; lối vào nhóm giữ ở `Select` nhóm ngay bên trên.
//   R-11 Cây chỉ tải 200 phiếu PE mới nhất (`PipelineTreePanel.tsx:22`) và KHÔNG có "trang 2".
//        ⇒ ô tìm + `Select` trạng-thái đi SERVER-SIDE thẳng `GET /contract-signing-plans`
//        (bật chế-độ danh-sách phẳng), luôn có đường tới phiếu cũ; và dòng cảnh-báo
//        "Cây tải 200 phiếu mới nhất…" của cây KHÔNG bị nuốt.
import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { ChevronLeft, ChevronRight, FileCheck, Inbox, Search } from 'lucide-react'
import { Input } from '@/components/ui/Input'
import { Select } from '@/components/ui/Select'
import { PipelineTreePanel } from '@/components/pipeline/PipelineTreePanel'
import { usePipelineLinkage, usePipelineStages } from '@/hooks/usePipelineStages'
import { api } from '@/lib/api'
import { cn } from '@/lib/cn'
import {
  KHKK_PHASE_BADGE,
  KHKK_PHASE_LABELS,
  KhkkPhase,
  type KhkkListItemDto,
  type KhkkPhaseValue,
  type PagedResult,
} from '@/types/khkk'

const PAGE_SIZE = 20
// [S176] Cửa-sổ nguồn cho CÂY ở chế-độ thùng rác. Bằng đúng `INDEX_PAGE_SIZE`/
// `PE_INDEX_PAGE_SIZE` của 2 index cây (200) để cả cây kể CÙNG một câu chuyện về giới-hạn
// — lấy 20/trang như chế-độ phẳng thì cây sẽ cụt mà không có dòng nào khai.
const TREE_SOURCE_PAGE_SIZE = 200

function formatVnd(n: number | null | undefined): string {
  if (n === null || n === undefined) return '—'
  return n.toLocaleString('vi-VN') + ' đ'
}

function formatDateTime(iso: string): string {
  return new Date(iso).toLocaleString('vi-VN', {
    day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit',
  })
}

/**
 * [F-2] Chip "⚑ Kết thúc · {Bước}" — khuôn `components/pe/PeFinalizeChip.tsx` (S89/S97),
 * dựng lại ở đây thay vì import vì nguồn cờ khác module (KHKK đọc
 * `endedByLevelFinalize` + `finalizeStepName`, PE đọc `endsBeforeCeo`).
 */
export function KhkkFinalizeChip({
  endedByLevelFinalize,
  finalizeStepName,
  phase,
}: {
  endedByLevelFinalize: boolean
  finalizeStepName: string | null
  phase?: number
}) {
  if (!endedByLevelFinalize) {
    if (phase === KhkkPhase.DaDuyet) {
      return (
        <span
          className="inline-flex shrink-0 items-center rounded bg-emerald-100 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-800"
          title="Kế hoạch đi HẾT quy trình (không kết thúc sớm ở cấp dưới)"
        >
          ✅ Duyệt hết quy trình
        </span>
      )
    }
    return null
  }
  return (
    <span
      className="inline-flex shrink-0 items-center rounded bg-violet-100 px-1.5 py-0.5 text-[10px] font-semibold text-violet-800"
      title={`Quy trình kết thúc tại ${finalizeStepName ?? 'cấp này'} — không trình tiếp cấp sau`}
    >
      {finalizeStepName ? `⚑ Kết thúc · ${finalizeStepName}` : '⚑ Kết thúc sớm'}
    </span>
  )
}

/**
 * Card 3 hàng — thay cho bảng 8 cột + 6 KpiCard của bản cũ (bảng "cột nào chết, chuyển
 * đi đâu" nằm ở `sub-implementer-frontend-s171.md` §4).
 * [M-8] Σ tiền in MỘT số, suy từ `phase === DaDuyet` (đúng như BE suy `budgetFrozen`) —
 * KHÔNG thêm field vào `KhkkListItemDto`, vì `budgetFrozen` chỉ có ở DetailDto.
 */
export function KhkkPlanCard({
  plan,
  selected,
  onSelect,
  finalize,
  deleted,
}: {
  plan: KhkkListItemDto
  selected: boolean
  onSelect: (id: string) => void
  /** Chỉ có khi phiếu ĐANG được chọn (detail đã tải) — list payload không mang cờ này. */
  finalize?: { endedByLevelFinalize: boolean; finalizeStepName: string | null }
  /** [S176] Đang xem thùng rác ⇒ hiện dấu thời-điểm xóa. Mirror khuôn màn Duyệt NCC. */
  deleted?: boolean
}) {
  const frozen = plan.phase === KhkkPhase.DaDuyet
  const amount = frozen ? (plan.totalApprovedAmount ?? plan.totalProposedAmount) : plan.totalProposedAmount
  return (
    <button
      type="button"
      onClick={() => onSelect(plan.id)}
      className={cn(
        'block w-full px-3 py-2.5 text-left transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500/60',
        selected && 'bg-brand-50 ring-1 ring-inset ring-brand-200',
      )}
    >
      <div className="flex items-start justify-between gap-2">
        <div className="min-w-0 flex-1">
          <div className="flex flex-wrap items-center gap-1">
            {finalize && (
              <KhkkFinalizeChip
                endedByLevelFinalize={finalize.endedByLevelFinalize}
                finalizeStepName={finalize.finalizeStepName}
                phase={plan.phase}
              />
            )}
            <span className="min-w-0 flex-1 truncate text-[13px] font-medium text-slate-900">
              {plan.peTenGoiThau ?? '(chưa có tên gói thầu)'}
            </span>
          </div>
        </div>
        <span
          className={cn(
            'shrink-0 rounded-md border px-1.5 py-0.5 text-[10px] font-medium',
            KHKK_PHASE_BADGE[plan.phase],
          )}
        >
          {KHKK_PHASE_LABELS[plan.phase]}
        </span>
      </div>
      <div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-slate-500">
        <span className="font-mono">{plan.maKeHoach ?? '(chưa cấp mã)'}</span>
        {/* [S175 · B3] Chip nhóm: `text-[9px]` là nấc thứ TƯ của thang chữ (13/11/10/9) —
            gom về 3 nấc chuẩn 13/11/10. Và `inline` + cỡ chữ khác dòng chứa ⇒ hộp chip cao
            khác chữ bên cạnh nên tụt baseline; `inline-flex items-center leading-none` ghim
            nó thẳng hàng thay vì dựa vào baseline của 2 cỡ chữ khác nhau. */}
        {plan.approvalGroup != null && (
          <span className="inline-flex shrink-0 items-center rounded border border-violet-200 bg-violet-50 px-1 py-0.5 text-[10px] font-semibold leading-none text-violet-700">
            N{plan.approvalGroup}
          </span>
        )}
        <span>·</span>
        <span className="truncate">{formatDateTime(plan.createdAt)}</span>
        {/* [S176] "Xóa lúc" — mirror khuôn màn Duyệt NCC (`PurchaseEvaluationsListPage:573`).
            🔴 API `/contract-signing-plans/deleted` trả DTO DÙNG CHUNG với list sống ⇒ KHÔNG
            có `deletedAt`/`deletedBy`. Mốc dưới là `updatedAt`, mà `AuditingInterceptor` ghi
            ngay lượt SaveChanges của thao tác xóa mềm ⇒ XẤP XỈ thời điểm xóa. Nhãn "≈" để
            KHÔNG hứa chính xác. Tên người xóa API không trả — cố ý bỏ trống thay vì đoán. */}
        {deleted && plan.updatedAt && (
          <>
            <span>·</span>
            <span
              className="shrink-0 rounded bg-red-50 px-1 py-0.5 text-[10px] font-medium text-red-700"
              title="Thời điểm xóa (xấp xỉ — lấy từ mốc cập nhật cuối, ghi cùng lượt xóa mềm). Xem lịch sử phiếu để biết ai xóa và lý do."
            >
              🗑 xóa ≈ {formatDateTime(plan.updatedAt)}
            </span>
          </>
        )}
      </div>
      <div className="mt-0.5 flex items-center justify-between gap-2 text-[11px] text-slate-500">
        <span className="min-w-0 truncate">👤 {plan.drafterFullName ?? '—'}</span>
        <span className={cn('shrink-0 tabular-nums', frozen ? 'text-emerald-700' : 'text-brand-800')}>
          {formatVnd(amount)}
        </span>
      </div>
    </button>
  )
}

export function KhkkListPanel({
  selectedId,
  onSelect,
  search,
  onSearchChange,
  phase,
  onPhaseChange,
  group,
  deletedView,
  page,
  onPageChange,
  selectedFinalize,
  onOpenPe,
}: {
  selectedId: string | null
  onSelect: (id: string) => void
  search: string
  onSearchChange: (v: string) => void
  phase: KhkkPhaseValue | null
  onPhaseChange: (v: KhkkPhaseValue | null) => void
  // [S174 owner] CHỈ còn đọc — `Select` nhóm đã gỡ khỏi UI. Nhóm nay đến DUY NHẤT từ URL
  // (`?group=n`, tức 8 leaf nhóm ở menu trái). Máy lọc giữ NGUYÊN (query + `stage2Content`);
  // chỉ bỏ đường đổi nhóm bằng tay — vào từ leaf nhóm rồi thì ô đó là thừa.
  group: number | null
  deletedView: boolean
  page: number
  onPageChange: (p: number) => void
  selectedFinalize?: { endedByLevelFinalize: boolean; finalizeStepName: string | null }
  /** [S175 · O6] Bấm card phiếu Duyệt NCC trên cây ⇒ host xem TẠI CHỖ (không rời màn). */
  onOpenPe?: (id: string) => void
}) {
  const { canPe } = usePipelineStages()
  const linkage = usePipelineLinkage()
  const term = search.trim()

  // [S176 owner] "ở các menu đang duyệt/ đã duyệt/ xóa → cho hiển thị năm → dự án → hạng
  // mục → phiếu KHKK nhé" ⇒ 3 menu đó nay đi CÂY (chế-độ lá 4 tầng), không còn phẳng.
  //
  // 🧊 Trước S176: `flatMode` còn gồm `deletedView || phase !== null`. Bỏ 2 vế đó.
  // [R-11] GIỮ vế từ-khoá: tìm kiếm đi SERVER-SIDE có phân-trang thật (`total` từ server)
  //        nên phiếu nằm ngoài cửa-sổ 200 VẪN tới được — đó là đường thoát khi cây không
  //        với tới, và là lý do KHÔNG bỏ nốt vế này.
  // [M-1] GIỮ vế `!canPe`: cây dựng trên xương sống phiếu Duyệt NCC (`enabled: canPe`),
  //        ai không có quyền đó thì cây KHÔNG dựng được — ép cây sẽ làm họ mất trắng phiếu
  //        của chính mình. Danh-sách phẳng là lưới an-toàn, không phải tuỳ chọn.
  const flatMode = !canPe || term.length > 0

  // Xem thùng rác ở chế-độ CÂY: phiếu đã xoá KHÔNG nằm trong index của cây
  // (`usePipelineLinkage` chỉ gọi `/contract-signing-plans`), nên phải tự lấy từ endpoint
  // riêng rồi tự gắn vào PE. Lấy đúng cửa-sổ 200 như 2 index kia để cùng một câu chuyện
  // truncation, thay vì 20/trang của chế-độ phẳng.
  const treeDeleted = deletedView && !flatMode

  const list = useQuery({
    queryKey: ['khkk-list', { group, phase, search: term, page, deletedView, tree: treeDeleted }],
    queryFn: async () => {
      const url = deletedView ? '/contract-signing-plans/deleted' : '/contract-signing-plans'
      return (await api.get<PagedResult<KhkkListItemDto>>(url, {
        params: {
          phase: deletedView ? undefined : (phase ?? undefined),
          group: group ?? undefined,
          search: term || undefined,
          page: treeDeleted ? 1 : page,
          pageSize: treeDeleted ? TREE_SOURCE_PAGE_SIZE : PAGE_SIZE,
        },
      })).data
    },
    enabled: flatMode || treeDeleted,
  })

  // Phiếu đã xoá gom theo PE — chỉ dựng khi đang xem thùng rác bằng cây.
  const deletedByPeId = useMemo(() => {
    const map = new Map<string, KhkkListItemDto[]>()
    if (!treeDeleted) return map
    for (const k of list.data?.items ?? []) {
      const bucket = map.get(k.purchaseEvaluationId)
      if (bucket) bucket.push(k)
      else map.set(k.purchaseEvaluationId, [k])
    }
    return map
  }, [treeDeleted, list.data])

  // Phiếu treo dưới một gói thầu, SAU khi áp đủ bộ lọc đang bật (trạng-thái + nhóm).
  // 🔴 Lọc ở ĐÂY = lọc TRƯỚC khi cây dựng ⇒ badge đếm ở cả 3 tầng tự khớp tập đang hiện.
  // Đây đúng là luật vòng-3 ("lọc phase TRƯỚC khi dựng cây, lọc sau thì badge nói dối").
  const plansForPe = useMemo(() => {
    return (peId: string): KhkkListItemDto[] => {
      const src = treeDeleted ? (deletedByPeId.get(peId) ?? []) : (linkage.khkkByPeId.get(peId) ?? [])
      const byGroup = group === null ? src : src.filter(k => k.approvalGroup === group)
      // Thùng rác KHÔNG lọc theo trạng-thái: phiếu đã xoá giữ nguyên phase lúc bị xoá, lọc
      // thêm chỉ tổ giấu mất phiếu người dùng đang đi tìm.
      return treeDeleted || phase === null ? byGroup : byGroup.filter(k => k.phase === phase)
    }
  }, [treeDeleted, deletedByPeId, linkage.khkkByPeId, group, phase])

  const items = list.data?.items ?? []
  const total = list.data?.total ?? 0
  const totalPages = list.data?.totalPages ?? 1

  // [B1] Thân folder GĐ2 của cây = CARD phiếu của chính trang này; `Select` nhóm lọc luôn
  // trên cây (client-side, an toàn: đây chỉ là chia lại đúng tập phiếu cây đã có).
  const stage2Content = (plans: KhkkListItemDto[]) => {
    const shown = group === null ? plans : plans.filter(k => k.approvalGroup === group)
    if (shown.length === 0) {
      return (
        <p className="px-3 py-2 text-[11px] italic text-slate-500">
          {group === null ? 'Gói này chưa có kế hoạch ký kết.' : `Gói này chưa có phiếu nhóm N${group}.`}
        </p>
      )
    }
    return (
      <ul className="divide-y divide-slate-100">
        {shown.map(k => (
          // [S176] Thùng rác trên CÂY phải cư-xử y hệt thùng rác ở danh-sách phẳng:
          // làm mờ + KHÔNG cho mở. Thiếu vế này thì cùng một phiếu đã xoá lại mở được ở
          // cây mà không mở được ở phẳng — cùng dữ-liệu, hai luật.
          <li key={k.id} className={cn(deletedView && 'opacity-70')}>
            <KhkkPlanCard
              plan={k}
              selected={selectedId === k.id}
              onSelect={deletedView ? () => { /* phiếu đã xóa: chỉ liệt kê, không mở */ } : onSelect}
              finalize={selectedId === k.id ? selectedFinalize : undefined}
              deleted={deletedView}
            />
          </li>
        ))}
      </ul>
    )
  }
  const stage2Count = (plans: KhkkListItemDto[]) =>
    (group === null ? plans : plans.filter(k => k.approvalGroup === group)).length

  return (
    <aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">
      {/* Khối filter — CHỖ ĐẶT giống PE/GĐ3 (trên cùng panel 1). `Select` nhóm N1-N8 là
          chỗ KHKK khác nguồn dữ-liệu chứ không khác bố-trí (P1-4). */}
      <div className="space-y-2 border-b border-slate-200 p-3">
        <div className="relative">
          <Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-slate-400" />
          <Input
            value={search}
            onChange={e => onSearchChange(e.target.value)}
            placeholder="Tìm mã kế hoạch / mã phiếu / gói thầu…"
            className="pl-8"
          />
        </div>
        {/* [S176] Thùng rác ⇒ THAY dropdown bằng banner, mirror khuôn màn Duyệt NCC
            (`PurchaseEvaluationsListPage:427-431`).
            🔴 Ẩn dropdown là CÓ LÝ DO, không phải cho gọn: phiếu giữ NGUYÊN phase lúc bị xóa,
            `IsDeleted` mới là thứ phân loại ⇒ lọc trạng thái ở đây là nút chết. Chính `plansForPe`
            bên dưới cũng cố ý KHÔNG lọc phase khi xem thùng rác — để dropdown lại thì người dùng
            bấm mà không thấy gì đổi.
            🔸 CỐ Ý BỎ vế "không còn tính vào lũy kế ngân sách" của bản PE: `grep 'lũy kế'` trong
            toàn bộ KHKK = 0 hit ⇒ bê nguyên là khai một điều chưa chứng minh cho module này. */}
        {deletedView ? (
          <div className="rounded border border-red-200 bg-red-50 px-2 py-1.5 text-[11px] text-red-700">
            🗑 <strong>Phiếu đã xóa</strong> — chỉ xem. Không khôi phục, không thao tác.
          </div>
        ) : (
          <Select
            value={phase ?? ''}
            aria-label="Lọc trạng thái"
            onChange={e => onPhaseChange(e.target.value === '' ? null : (Number(e.target.value) as KhkkPhaseValue))}
          >
            <option value="">Tất cả trạng thái</option>
            {(Object.keys(KHKK_PHASE_LABELS) as unknown as number[]).map(p => (
              <option key={p} value={p}>{KHKK_PHASE_LABELS[Number(p) as KhkkPhaseValue]}</option>
            ))}
          </Select>
        )}
        {/* 🧊 [S175] Dòng "Đang lọc — danh sách lấy thẳng từ máy chủ…" ĐÃ GỠ (anh: "Bỏ mấy dòng
            này đi"). Đó là chú-thích về CƠ-CHẾ bên trong, người dùng không cần biết cây tải 200
            phiếu hay lấy thẳng máy chủ — họ chỉ cần thấy đúng phiếu. */}
        {!canPe && (
          <p className="text-[10px] italic text-slate-500">
            Bạn chưa có quyền xem phiếu Duyệt NCC nên cây toàn trình không dựng được — danh
            sách kế hoạch ký kết vẫn liệt kê đầy đủ ở dưới.
          </p>
        )}
      </div>

      {/* [S175 · B3] ĐẦU CỘT — trước đây chế-độ CÂY đi thẳng vào thân, không tiêu-đề cũng
          không số đếm ⇒ cột "cụt đầu": lệch cả với màn Thao tác (`KhkkWorkListPanel` có
          `Tier` header) LẪN với chính chế-độ phẳng (có dòng đếm ở chân). Mirror đúng khuôn
          `Tier` để 2 màn KHKK cùng một hình. */}
      {/* 🧊 [S175] Dải "ĐẦU CỘT" (thêm ở đợt B để chữa "cột cụt đầu") ĐÃ GỠ — anh khoanh đỏ
          cùng dòng "Đang lọc" và bảo bỏ. Tiêu-đề cột đã có ở header trang ngay trên. */}
      <div className="flex-1 overflow-y-auto">
        {!flatMode && (
          <PipelineTreePanel
            currentStage={2}
            hideHeader
            hideSearch
            bodyMaxHClass="max-h-[28rem] lg:max-h-none"
            stage2Options={{ stage2Content, stage2Count, onOpenKhkk: onSelect, onOpenPe }}
            // [S176 owner] ĐÚNG 3 menu anh gọi tên — `filter=ChoDuyet` (Đang duyệt) ·
            // `filter=DaDuyet` (Đã duyệt) · `view=deleted` (Đã xóa) ⇒ cây rút về 4 tầng
            // Năm › Dự án › Hạng mục › phiếu KHKK.
            // 🔴 CẤM thêm vế `group !== null` vào đây: CẢ BỐN leaf menu KHKK đều mang
            // `?group=1` (`Layout.tsx:41-45`), kể cả "Danh sách" không lọc — thêm vế đó là
            // kéo luôn menu TOÀN CẢNH vào chế-độ lá, tức đổi một màn owner không hề yêu cầu.
            // Nhóm là chiều CHIA NGĂN, không phải chiều CHỌN HÌNH CÂY.
            leafMode={
              phase !== null || deletedView
                ? {
                    countForPe: pe => plansForPe(pe.id).length,
                    render: peItems => stage2Content(peItems.flatMap(p => plansForPe(p.id))),
                    emptyText: deletedView
                      ? 'Không có phiếu đã xóa.'
                      : 'Chưa có kế hoạch ký kết nào khớp bộ lọc.',
                  }
                : undefined
            }
          />
        )}

        {flatMode && list.isLoading && (
          <div className="space-y-2 p-3">
            {Array.from({ length: 8 }).map((_, i) => (
              <div key={i} className="h-16 animate-pulse rounded-md bg-slate-100 motion-reduce:animate-none" />
            ))}
          </div>
        )}
        {flatMode && list.isError && (
          <div className="space-y-2 px-3 py-4">
            <p className="text-[12px] text-slate-500">Không tải được danh sách kế hoạch ký kết.</p>
            <button
              type="button"
              onClick={() => list.refetch()}
              className="rounded-md border border-slate-300 px-2 py-1 text-[11px] font-semibold text-slate-600 transition hover:border-brand-300 hover:bg-brand-50 hover:text-brand-700"
            >
              Thử lại
            </button>
          </div>
        )}
        {flatMode && !list.isLoading && !list.isError && items.length === 0 && (
          <div className="p-6 text-center text-sm text-slate-500">
            <span
              className="icon-chip mx-auto mb-2 flex"
              style={{ ['--chip-bg' as string]: '#f1f5f9', ['--chip-fg' as string]: '#94a3b8' }}
              aria-hidden
            >
              <Inbox className="h-4 w-4" />
            </span>
            {deletedView ? 'Không có phiếu đã xóa.' : 'Chưa có kế hoạch ký kết nào khớp bộ lọc.'}
          </div>
        )}
        {flatMode && items.length > 0 && (
          <ul className="divide-y divide-slate-100">
            {items.map(k => (
              <li key={k.id} className={cn(deletedView && 'opacity-70')}>
                <KhkkPlanCard
                  plan={k}
                  selected={selectedId === k.id}
                  onSelect={deletedView ? () => { /* phiếu đã xóa: chỉ liệt kê, không mở */ } : onSelect}
                  finalize={selectedId === k.id ? selectedFinalize : undefined}
                  deleted={deletedView}
                />
              </li>
            ))}
          </ul>
        )}
      </div>

      {flatMode && totalPages > 1 && (
        <div className="flex shrink-0 items-center justify-between border-t border-slate-200 px-3 py-2 text-[11px]">
          <span className="text-slate-500">
            <FileCheck className="mr-1 inline h-3 w-3" />
            {total} kế hoạch — Trang {page} / {totalPages}
          </span>
          {/* [S175 · B3] Ký-tự trần `←`/`→` → icon: mũi tên dạng chữ đổi hình theo font hệ
              điều-hành và KHÔNG có tên cho trình đọc màn hình. Icon + `aria-label` sửa cả hai. */}
          <div className="flex gap-1">
            <button
              type="button"
              onClick={() => onPageChange(Math.max(1, page - 1))}
              disabled={page <= 1}
              aria-label="Trang trước"
              title="Trang trước"
              className="rounded border border-slate-300 p-1 text-slate-600 hover:bg-slate-50 disabled:opacity-40"
            >
              <ChevronLeft className="h-3.5 w-3.5" />
            </button>
            <button
              type="button"
              onClick={() => onPageChange(page + 1)}
              disabled={page >= totalPages}
              aria-label="Trang sau"
              title="Trang sau"
              className="rounded border border-slate-300 p-1 text-slate-600 hover:bg-slate-50 disabled:opacity-40"
            >
              <ChevronRight className="h-3.5 w-3.5" />
            </button>
          </div>
        </div>
      )}
    </aside>
  )
}
