// List + Detail phiếu Duyệt NCC — 3-panel: List | Detail tabs | Workflow + history.
// URL params: type (filter A/B), pendingMe (1=inbox), id (selected), q (search), expand (1=overlay).
// Plan AG Phase 1 (S26 2026-05-21) — Tree view 2-level Project > Gói thầu > PE
// UAT feedback bro Tra Sol: flat list "đám rừng" → Outlook folder structure.
// FE-only group view (no schema change) — Phase 2 ProjectPackage defer sau UAT confirm.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { toast } from 'sonner'
import { ArrowLeft, ClipboardCheck, Maximize2, Search, X } from 'lucide-react'
import { Input } from '@/components/ui/Input'
import { Select } from '@/components/ui/Select'
import { EmptyState } from '@/components/EmptyState'
import { api } from '@/lib/api'
import { getErrorMessage, isNotFound } from '@/lib/apiError'
import { cn } from '@/lib/cn'
import type { Paged } from '@/types/master'
import {
  PeDisplayStatus,
  PeDisplayStatusColor,
  PeDisplayStatusLabel,
  PurchaseEvaluationPhase,
  PurchaseEvaluationTypeLabel,
  getPeDisplayStatus,
  type PeDetailBundle,
  type PeListItem,
} from '@/types/purchaseEvaluation'
import { PeDetailTabs } from '@/components/pe/PeDetailTabs'
import { PeUrgentChips } from '@/components/pe/PeUrgentChips'
import { PeFinalizeChip } from '@/components/pe/PeFinalizeChip'
import { PeWorkflowPanel } from '@/components/pe/PeWorkflowPanel'
import { PipelineStageFolders } from '@/components/pipeline/PipelineStageFolders'
import { usePipelineStages } from '@/hooks/usePipelineStages'

export function PurchaseEvaluationsListPage() {
  const qc = useQueryClient()
  const [sp, setSp] = useSearchParams()
  const typeFilter = sp.get('type') ? Number(sp.get('type')) : null
  const pendingMe = sp.get('pendingMe') === '1'
  // [S155 đợt 2] Mục menu MỚI "Đã xóa" (`Pe_{code}_Deleted` → `?type=N&deleted=1`).
  // View CHỈ XEM (owner chốt (6)): KHÔNG khôi phục, KHÔNG thao tác, KHÔNG mở chi tiết —
  // `GET /purchase-evaluations/{id}` bị global filter `PurchaseEvaluationConfiguration.cs:84`
  // loại phiếu đã xóa nên bấm vào chỉ ra "Không tìm thấy phiếu". Chặn ở nguồn thay vì để
  // người dùng đâm vào ngõ cụt. Mục "Đã duyệt" KHÔNG cần cờ nào — nó chỉ là `?phase=7`.
  const deletedView = sp.get('deleted') === '1'
  const search = sp.get('q') ?? ''
  const phase = sp.get('phase') ?? ''
  const approvalWorkflowId = sp.get('awId') ?? ''       // Mig 23 — filter quy trình
  const selectedId = sp.get('id')
  const isExpand = sp.get('expand') === '1'             // S79 — overlay mở rộng

  // [S162] Nguồn 4 folder giai đoạn dưới từng gói thầu (GĐ2 KHKK / GĐ3 HĐ /
  // GĐ4 HĐ cứng). Hook tự gate quyền TRƯỚC khi fetch — không quyền thì không
  // gọi API, folder hiện dạng khoá.
  const { buildStages } = usePipelineStages()
  // Đang lọc (tìm kiếm / trạng thái / "Chờ tôi" / "Đã xóa") ⇒ chỉ hiện folder
  // CÓ nội dung khớp, tránh đổ 4 folder rỗng dưới mọi gói thầu.
  const filterActive = search.trim() !== '' || phase !== '' || pendingMe || deletedView

  // Mig 23 — list quy trình duyệt V2 cho dropdown filter (filter theo type screen)
  const approvalWorkflows = useQuery({
    queryKey: ['approval-workflows-v2-filter', typeFilter],
    queryFn: async () => {
      if (!typeFilter) return []
      const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean }[] }[] }>(
        '/approval-workflows-v2',
        { params: { applicableType: typeFilter } },
      )
      return res.data.types.find(t => t.applicableType === typeFilter)?.history ?? []
    },
    enabled: !!typeFilter,
  })

  const list = useQuery({
    queryKey: ['pe-list', { typeFilter, pendingMe, search, phase, approvalWorkflowId, deletedView }],
    queryFn: async () => {
      // [S155 đợt 2] Nhánh "Đã xóa" phải đứng TRƯỚC `pendingMe`: 2 view loại trừ nhau, và
      // cả `/inbox` lẫn `/purchase-evaluations` đều bị global filter loại phiếu đã xóa ⇒
      // chỉ endpoint riêng `/deleted` (chỗ DUY NHẤT toàn BE dùng `IgnoreQueryFilters`) thấy.
      if (deletedView) {
        const res = await api.get<Paged<PeListItem>>('/purchase-evaluations/deleted', {
          params: {
            page: 1,
            pageSize: 50,
            type: typeFilter ?? undefined,
            search: search || undefined,
          },
        })
        return res.data
      }
      if (pendingMe) {
        const res = await api.get<PeListItem[]>('/purchase-evaluations/inbox', {
          params: {
            type: typeFilter ?? undefined,
            approvalWorkflowId: approvalWorkflowId || undefined,
          },
        })
        return { items: res.data, total: res.data.length, page: 1, pageSize: res.data.length }
      }
      const res = await api.get<Paged<PeListItem>>('/purchase-evaluations', {
        params: {
          pageSize: 50,
          search: search || undefined,
          type: typeFilter ?? undefined,
          phase: phase || undefined,
          approvalWorkflowId: approvalWorkflowId || undefined,
        },
      })
      return res.data
    },
  })

  const detail = useQuery({
    queryKey: ['pe-detail', selectedId],
    queryFn: async () => (await api.get<PeDetailBundle>(`/purchase-evaluations/${selectedId}`)).data,
    enabled: !!selectedId,
  })

  const del = useMutation({
    mutationFn: async (id: string) => api.delete(`/purchase-evaluations/${id}`),
    onSuccess: () => {
      toast.success('Đã xóa phiếu.')
      // Xoá phiếu → clear cả id + expand (về danh sách).
      const next = new URLSearchParams(sp)
      next.delete('id')
      next.delete('expand')
      next.delete('page')
      setSp(next)
      qc.invalidateQueries({ queryKey: ['pe-list'] })
    },
    onError: e => toast.error(getErrorMessage(e)),
  })

  const setParam = useCallback((key: string, value: string | null) => {
    const next = new URLSearchParams(sp)
    if (value == null || value === '') next.delete(key)
    else next.set(key, value)
    if (key !== 'id') next.delete('page')
    setSp(next, { replace: key === 'q' })
  }, [sp, setSp])

  // S79 — decouple "chọn" khỏi "mở rộng" (anh chốt 2026-06-19, annotated screenshot):
  //  • Bấm dòng phiếu = chọn → detail INLINE 3-panel "như cũ" (8e68ed1). Trên màn
  //    hẹp (<lg) 3-panel không vừa → bấm dòng đi thẳng overlay full-bleed.
  //  • Nút "Xem mở rộng" mỗi dòng = id + expand=1 → overlay trượt full màn.
  //  • Overlay render CHỈ khi id && expand. Inline detail render khi id && !expand.
  const LG_BREAKPOINT = 1024
  const isWideViewport = () => typeof window !== 'undefined' && window.innerWidth >= LG_BREAKPOINT

  // Chọn dòng: set id; clear expand. <lg → set expand=1 luôn (đi thẳng overlay vì
  // 3-panel inline không vừa màn hẹp).
  const selectRow = useCallback((id: string) => {
    const next = new URLSearchParams(sp)
    next.set('id', id)
    if (isWideViewport()) next.delete('expand')
    else next.set('expand', '1')
    setSp(next)
  }, [sp, setSp])

  // "Xem mở rộng" trên 1 dòng → mở overlay cho phiếu đó (id + expand=1).
  const expandRow = useCallback((id: string) => {
    const next = new URLSearchParams(sp)
    next.set('id', id)
    next.set('expand', '1')
    setSp(next)
  }, [sp, setSp])

  // Thu gọn overlay → bỏ expand, GIỮ id (quay lại inline detail, KHÔNG về list).
  const collapseFocus = useCallback(() => setParam('expand', null), [setParam])

  // Đóng inline detail (nút ← Đóng / xoá phiếu) → clear id + expand → về danh sách.
  const closeDetail = useCallback(() => {
    const next = new URLSearchParams(sp)
    next.delete('id')
    next.delete('expand')
    next.delete('page')
    setSp(next)
  }, [sp, setSp])

  // Duyệt xong (onApproved, không phải Trả lại/Từ chối) → phiếu rời inbox →
  // đóng hẳn overlay + clear selection (về danh sách).
  const onApproved = useCallback(() => closeDetail(), [closeDetail])

  // [S155 đợt 2] Xóa mềm xong → BẮT BUỘC đóng detail/overlay: phiếu vừa xóa không đọc lại
  // được (`GET /{id}` bị global filter loại) nên giữ overlay sẽ hiện "Không tìm thấy phiếu"
  // ngay sau một thao tác THÀNH CÔNG — trông y như lỗi. Panel đã invalidate `pe-list`;
  // gọi thêm ở đây để list nền refetch kể cả khi caller khác không invalidate.
  const onDeleted = useCallback(() => {
    closeDetail()
    qc.invalidateQueries({ queryKey: ['pe-list'] })
  }, [closeDetail, qc])

  const allRows = list.data?.items ?? []
  // Duyệt (pendingMe) → filter cứng client-side chỉ "Đã gửi duyệt" (Nháp/Trả lại/
  // Đã duyệt/Từ chối loại bỏ). BE /inbox hiện loose UAT có thể trả phiếu Nháp →
  // FE filter để UX đúng kỳ vọng. Phân quyền strict V2 BE pending Session 18+.

  // S59 — anh Kiệt FDC (Zalo 2026-06-11) + anh chốt follow-up: tree 4 tầng
  // "Năm (tạo phiếu) > Dự án > Hạng mục công việc > Phiếu cần duyệt".
  // Hạng mục từ WorkItemId Mig 49, phiếu cũ chưa gắn → "(Chưa gắn hạng mục)".
  // NCC bỏ khỏi tree — vẫn hiện ở card/detail. Filter pendingMe TRƯỚC group.
  // Sort: Năm DESC + Dự án A-Z (vi) + Hạng mục A-Z (vi) + PE createdAt DESC.
  type WorkItemGroup = {
    workItemId: string | null
    workItemName: string
    items: PeListItem[]
  }
  type ProjectGroup = {
    projectId: string | null
    projectName: string
    projectCode: string
    workItems: WorkItemGroup[]
    totalCount: number
  }
  type YearGroup = {
    year: number
    projects: ProjectGroup[]
    totalCount: number
  }
  const yearGroups = useMemo<YearGroup[]>(() => {
    const filtered = pendingMe
      ? allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet)
      : allRows
    const yearMap = new Map<number, YearGroup>()
    for (const p of filtered) {
      const year = new Date(p.createdAt).getFullYear()
      if (!yearMap.has(year)) {
        yearMap.set(year, { year, projects: [], totalCount: 0 })
      }
      const yg = yearMap.get(year)!
      const projKey = p.projectId ?? '__no_project__'
      const projName = p.projectName?.trim() || '(Dự án đã xoá)'
      let pg = yg.projects.find(g => (g.projectId ?? '__no_project__') === projKey)
      if (!pg) {
        pg = { projectId: p.projectId ?? null, projectName: projName, projectCode: p.projectCode || '', workItems: [], totalCount: 0 }
        yg.projects.push(pg)
      }
      const wiKey = p.workItemId ?? '__no_workitem__'
      const wiName = p.workItemName?.trim() || '(Chưa gắn hạng mục)'
      let wg = pg.workItems.find(w => (w.workItemId ?? '__no_workitem__') === wiKey)
      if (!wg) {
        wg = { workItemId: p.workItemId ?? null, workItemName: wiName, items: [] }
        pg.workItems.push(wg)
      }
      wg.items.push(p)
      pg.totalCount++
      yg.totalCount++
    }
    const arr = Array.from(yearMap.values())
    arr.sort((a, b) => b.year - a.year)
    for (const yg of arr) {
      yg.projects.sort((a, b) => (a.projectCode || a.projectName).localeCompare(b.projectCode || b.projectName, 'vi'))
      for (const pg of yg.projects) {
        // S59 — numeric:true vì tên PMH bắt đầu bằng STT không pad ("2 Mat…" < "10 Mat…")
        pg.workItems.sort((a, b) => a.workItemName.localeCompare(b.workItemName, 'vi', { numeric: true }))
        for (const wg of pg.workItems) {
          wg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
        }
      }
    }
    return arr
  }, [allRows, pendingMe])

  // Total row count cho header badge (pendingMe đếm filtered, Danh sách đếm BE total).
  const totalRowCount = yearGroups.reduce((sum, yg) => sum + yg.totalCount, 0)

  // Plan AG2 — Expand state localStorage Set<string>. Default empty Set (all collapse).
  // S59 key v3: node scheme chốt "Năm > Dự án > Hạng mục" → key v1/v2 vô nghĩa.
  const STORAGE_KEY = 'pe_list_expanded_projects_v3'
  const [expandedSet, setExpandedSet] = useState<Set<string>>(() => {
    try {
      const raw = localStorage.getItem(STORAGE_KEY)
      return raw ? new Set(JSON.parse(raw) as string[]) : new Set<string>()
    } catch {
      return new Set<string>()
    }
  })
  const isExpanded = (key: string) => expandedSet.has(key)
  const toggleExpand = (key: string, open: boolean) => {
    setExpandedSet(prev => {
      const next = new Set(prev)
      if (open) next.add(key)
      else next.delete(key)
      try { localStorage.setItem(STORAGE_KEY, JSON.stringify([...next])) } catch {}
      return next
    })
  }

  const headerTitle = deletedView
    ? (typeFilter ? `${PurchaseEvaluationTypeLabel[typeFilter]} — Đã xóa` : 'Duyệt NCC — Đã xóa')
    : typeFilter
      ? (pendingMe ? `${PurchaseEvaluationTypeLabel[typeFilter]} — Chờ duyệt` : PurchaseEvaluationTypeLabel[typeFilter])
      : pendingMe ? 'Duyệt NCC — Chờ tôi' : 'Quy trình Duyệt NCC'

  // ─── S79 Focus-mode overlay (anh chốt 2026-06-19) ─────────────────────────
  // Overlay full-bleed trượt từ phải CHE menu trái + TopBar + list + inline
  // detail. Chỉ mở khi `expand=1`. Thu gọn → bỏ expand (về inline detail).
  // Mount-then-animate: expand bật → mount overlay → next frame → translate-x-0.
  const overlayActive = !!selectedId && isExpand
  const [focusMounted, setFocusMounted] = useState(false)
  const [focusVisible, setFocusVisible] = useState(false)
  const overlayRef = useRef<HTMLDivElement>(null)
  const closeBtnRef = useRef<HTMLButtonElement>(null)
  const prefersReducedMotion = useRef(
    typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches,
  )

  useEffect(() => {
    if (overlayActive) {
      setFocusMounted(true)
      // 2 rAF để browser commit translate-x-full TRƯỚC khi đổi → translate-x-0
      // (1 rAF đôi khi bị batch chung frame → không thấy slide). reduced-motion
      // → bật ngay (CSS transition đã off).
      if (prefersReducedMotion.current) {
        setFocusVisible(true)
      } else {
        const r1 = requestAnimationFrame(() => {
          const r2 = requestAnimationFrame(() => setFocusVisible(true))
          ;(window as unknown as { __peR2?: number }).__peR2 = r2
        })
        return () => {
          cancelAnimationFrame(r1)
          const r2 = (window as unknown as { __peR2?: number }).__peR2
          if (r2) cancelAnimationFrame(r2)
        }
      }
    } else {
      setFocusVisible(false)
      if (prefersReducedMotion.current) {
        setFocusMounted(false)
      } else {
        const t = setTimeout(() => setFocusMounted(false), 240)
        return () => clearTimeout(t)
      }
    }
  }, [overlayActive])

  // Khoá scroll body khi overlay mở (tránh double-scroll nền). Focus nút Thu gọn
  // khi mở (a11y — đưa focus vào overlay).
  useEffect(() => {
    if (!focusMounted) return
    const prevOverflow = document.body.style.overflow
    document.body.style.overflow = 'hidden'
    const tf = setTimeout(() => closeBtnRef.current?.focus(), 60)
    return () => {
      document.body.style.overflow = prevOverflow
      clearTimeout(tf)
    }
  }, [focusMounted])

  // Esc thu gọn + focus-trap (Tab vòng trong overlay) — a11y dialog floor (FD5).
  useEffect(() => {
    if (!focusMounted) return
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        e.preventDefault()
        collapseFocus()
        return
      }
      if (e.key === 'Tab' && overlayRef.current) {
        const focusables = overlayRef.current.querySelectorAll<HTMLElement>(
          'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
        )
        if (focusables.length === 0) return
        const first = focusables[0]
        const last = focusables[focusables.length - 1]
        if (e.shiftKey && document.activeElement === first) {
          e.preventDefault()
          last.focus()
        } else if (!e.shiftKey && document.activeElement === last) {
          e.preventDefault()
          first.focus()
        }
      }
    }
    document.addEventListener('keydown', onKey)
    return () => document.removeEventListener('keydown', onKey)
  }, [focusMounted, collapseFocus])

  return (
    <div className="flex h-[calc(100vh-4rem)] flex-col">
      <header className="flex shrink-0 flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-white px-6 py-3">
        <div className="flex items-center gap-2">
          <ClipboardCheck className="h-5 w-5 text-slate-500" />
          <h1 className="text-base font-semibold tracking-tight text-slate-900">{headerTitle}</h1>
          <span className="ml-2 rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-600">
            {pendingMe ? totalRowCount : (list.data?.total ?? 0)}
          </span>
        </div>
      </header>

      {/* S79 — list state: grid 3-panel bám trái lấp đầy màn hình. List <aside>
          trái + Detail tabs (giữa) + Workflow panel (phải) INLINE "như cũ"
          (8e68ed1). Bấm dòng → chọn → 2 panel phải render detail. Nút "Xem mở
          rộng" mỗi dòng → overlay full-bleed CHE hẳn. <lg → grid 1 cột (chỉ
          list) → bấm dòng đi thẳng overlay. */}
      <div className="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[400px_1fr_360px]">
        {/* Panel 1: List — bám trái, fills, không max-w/mx-auto. */}
        <aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">
          <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 => setParam('q', e.target.value)}
                placeholder="Tìm mã / tên gói thầu / dự án…"
                className="pl-8"
              />
            </div>
            {/* Mig 23 — Dropdown quy trình duyệt CHỈ ở "Duyệt" (pendingMe).
                Danh sách giữ nguyên (chỉ filter trạng thái) — user feedback
                muốn Danh sách show hết phiếu không filter quy trình. */}
            {pendingMe && !deletedView && (
              <Select value={approvalWorkflowId} onChange={e => setParam('awId', e.target.value)}>
                <option value="">Tất cả quy trình duyệt</option>
                {approvalWorkflows.data?.map(w => (
                  <option key={w.id} value={w.id}>
                    {w.code} v{String(w.version).padStart(2, '0')} — {w.name}
                  </option>
                ))}
              </Select>
            )}
            {/* Duyệt (pendingMe) → filter cứng "Đã gửi duyệt", ẩn dropdown trạng thái.
                Danh sách (pendingMe=false) → giữ dropdown cho user filter mọi trạng thái.
                [S155 đợt 2] "Đã xóa" → ẩn CẢ HAI: dropdown trạng thái vô nghĩa ở đây (phiếu
                giữ nguyên phase lúc bị xóa, `IsDeleted` mới là thứ phân loại). */}
            {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.
                Các phiếu ở đây <strong>không còn tính vào lũy kế</strong> ngân sách.
              </div>
            ) : pendingMe ? (
              <div className="rounded border border-amber-200 bg-amber-50 px-2 py-1.5 text-[11px] text-amber-700">
                Lọc cố định: <strong>Đã gửi duyệt</strong> (phiếu đang chờ duyệt)
              </div>
            ) : (
              <Select value={phase} onChange={e => setParam('phase', e.target.value)}>
                <option value="">Tất cả trạng thái</option>
                {Object.values(PeDisplayStatus).map(s => {
                  const phaseValue = s === PeDisplayStatus.Nhap
                    ? String(PurchaseEvaluationPhase.DangSoanThao)
                    : s === PeDisplayStatus.DaDuyet
                    ? String(PurchaseEvaluationPhase.DaDuyet)
                    : s === PeDisplayStatus.TraLai
                    ? String(PurchaseEvaluationPhase.TraLai)
                    : s === PeDisplayStatus.TuChoi
                    ? String(PurchaseEvaluationPhase.TuChoi)
                    : '' // DaGuiDuyet — multi-phase, không filter exact (TODO BE)
                  return phaseValue ? (
                    <option key={s} value={phaseValue}>{PeDisplayStatusLabel[s]}</option>
                  ) : null
                })}
              </Select>
            )}
          </div>

          <div className="flex-1 overflow-y-auto">
            {list.isLoading && (
              <div className="space-y-2 p-3">
                {Array.from({ length: 6 }).map((_, i) => (
                  <div key={i} className="h-16 animate-pulse rounded-md bg-slate-100" />
                ))}
              </div>
            )}
            {!list.isLoading && yearGroups.length === 0 && (
              <div className="p-6">
                <EmptyState icon={ClipboardCheck} title="Chưa có phiếu" description="Tạo phiếu mới để bắt đầu quy trình." />
              </div>
            )}
            {/* S59 — Tree view chốt: 📅 Năm (bg-slate-50) > 📁 Dự án > 🧱 Hạng mục công việc > PE card.
                3 layer <details>, named groups group/year + group/proj + group/wi cho chevron rotation. */}
            <div className="divide-y divide-slate-100">
              {yearGroups.map(yg => {
                const yearKey = `y${yg.year}`
                return (
                  <details
                    key={yearKey}
                    open={isExpanded(yearKey)}
                    onToggle={e => toggleExpand(yearKey, (e.currentTarget as HTMLDetailsElement).open)}
                    className="group/year"
                  >
                    <summary className="flex cursor-pointer items-center gap-1.5 bg-slate-50 px-3 py-2 hover:bg-slate-100 [&::-webkit-details-marker]:hidden">
                      <svg className="h-3 w-3 shrink-0 text-slate-500 transition-transform group-open/year:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
                      <span className="text-base">📅</span>
                      <span className="flex-1 truncate text-[13px] font-semibold text-slate-900">Năm {yg.year}</span>
                      <span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{yg.totalCount}</span>
                    </summary>
                    <div className="ml-3 border-l border-slate-200">
                      {yg.projects.map(pg => {
                        const projKey = `${yearKey}::p${pg.projectId ?? '_none_'}`
                        return (
                          <details
                            key={projKey}
                            open={isExpanded(projKey)}
                            onToggle={e => toggleExpand(projKey, (e.currentTarget as HTMLDetailsElement).open)}
                            className="group/proj"
                          >
                            <summary className="flex cursor-pointer items-center gap-1.5 px-3 py-1.5 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
                              <svg className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/proj:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
                              <span className="text-sm">📁</span>
                              <span className="flex-1 truncate text-[12px] font-medium text-slate-700" title={pg.projectName}>{pg.projectCode || pg.projectName}</span>
                              <span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{pg.totalCount}</span>
                            </summary>
                            <div className="ml-3 border-l border-slate-200">
                              {pg.workItems.map(wg => {
                                const wiKey = `${projKey}::w${wg.workItemId ?? '_none_'}`
                                return (
                                  <details
                                    key={wiKey}
                                    open={isExpanded(wiKey)}
                                    onToggle={e => toggleExpand(wiKey, (e.currentTarget as HTMLDetailsElement).open)}
                                    className="group/wi"
                                  >
                                    <summary className="flex cursor-pointer items-center gap-1.5 px-3 py-1.5 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
                                      <svg className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/wi:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
                                      <span className="text-sm">🧱</span>
                                      <span className={cn('flex-1 truncate text-[12px]', wg.workItemId ? 'font-medium text-slate-700' : 'italic text-slate-400')}>{wg.workItemName}</span>
                                      <span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{wg.items.length}</span>
                                    </summary>
                                  {/* [S162] 4 folder giai đoạn dưới GÓI THẦU (owner chốt AskUser 30-07).
                                      Khối <li> phiếu bên dưới GIỮ NGUYÊN từng byte — kể cả thụt lề — để
                                      diff chứng minh không đụng behavior chọn phiếu / chip GẤP / nút
                                      "Xem mở rộng"; nó chỉ được RE-PARENT vào folder GĐ1 "Duyệt NCC". */}
                                  <PipelineStageFolders className="ml-3 border-l border-slate-200" currentStage={1} hideEmpty={filterActive} stages={buildStages(wg.items, { stage1Content: (
                                    <ul className="divide-y divide-slate-100">
                                      {wg.items.map(p => (
                                        <li key={p.id}>
                                          {/* S79 — dòng phiếu: vùng bấm chính (chọn → inline) + nút
                                              "Xem mở rộng" (overlay) nằm trong cùng row, group hover. */}
                                          <div
                                            className={cn(
                                              'group/row relative flex items-stretch transition hover:bg-slate-50',
                                              selectedId === p.id && 'bg-brand-50 ring-1 ring-inset ring-brand-200',
                                            )}
                                          >
                                            <button
                                              onClick={() => selectRow(p.id)}
                                              // [S155 đợt 2] "Đã xóa" = CHỈ XEM ⇒ dòng không bấm được.
                                              disabled={deletedView}
                                              className={cn(
                                                'block min-w-0 flex-1 px-3 py-2 pr-9 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500/60',
                                                deletedView && 'cursor-default',
                                              )}
                                            >
                                              {/* Plan AG6 — compact card 3 row: title+badge / mã+time / drafter+dept+contract */}
                                              <div className="flex items-start justify-between gap-2">
                                                <div className="flex min-w-0 flex-1 items-center gap-1 truncate text-[13px] font-medium text-slate-900">
                                                  {/* S77 — cờ gấp PILL đồng bộ mọi danh sách: 🔴 GẤP (PRO) / 🟢 GẤP (CCM). */}
                                                  <PeUrgentChips isUrgentByPro={p.isUrgentByPro} isUrgentByCcm={p.isUrgentByCcm} />
                                                  <PeFinalizeChip endsBeforeCeo={p.endsBeforeCeo} finalizeStepName={p.finalizeStepName} phase={p.phase} />
                                                  <span className="truncate">{p.tenGoiThau}</span>
                                                </div>
                                                <span
                                                  className={cn(
                                                    'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
                                                    PeDisplayStatusColor[getPeDisplayStatus(p.phase)],
                                                  )}
                                                >
                                                  {PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
                                                </span>
                                              </div>
                                              <div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-slate-500">
                                                <span className="font-mono">{p.maPhieu ?? '—'}</span>
                                                <span className="text-slate-300">·</span>
                                                {/* S23 t2 UAT: BE list sort theo UpdatedAt DESC (fallback CreatedAt). */}
                                                <span title={`Tạo lúc ${new Date(p.createdAt).toLocaleString('vi-VN')}`}>
                                                  {new Date(p.createdAt).toLocaleString('vi-VN', {
                                                    day: '2-digit', month: '2-digit', year: 'numeric',
                                                    hour: '2-digit', minute: '2-digit',
                                                  })}
                                                </span>
                                                {/* [S155 đợt 2] "Xóa lúc" — 🔴 API `/deleted` trả DTO DÙNG CHUNG với
                                                    list sống ⇒ KHÔNG có `deletedAt`/`deletedBy`. Mốc dưới đây 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. */}
                                                {deletedView && p.updatedAt && (
                                                  <>
                                                    <span className="text-slate-300">·</span>
                                                    <span
                                                      className="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 ≈ {new Date(p.updatedAt).toLocaleString('vi-VN', {
                                                        day: '2-digit', month: '2-digit', year: 'numeric',
                                                        hour: '2-digit', minute: '2-digit',
                                                      })}
                                                    </span>
                                                  </>
                                                )}
                                              </div>
                                              {(p.drafterName || p.departmentName || p.contractId) && (
                                                <div className="mt-0.5 flex items-center justify-between gap-2 text-[11px]">
                                                  <span className="min-w-0 flex-1 truncate text-slate-500">
                                                    {p.drafterName && <>👤 {p.drafterName}</>}
                                                    {p.drafterName && p.departmentName && <span className="text-slate-300"> · </span>}
                                                    {p.departmentName && <span className="text-slate-400">{p.departmentName}</span>}
                                                  </span>
                                                  {p.contractId && <span className="shrink-0 text-[10px] font-medium text-brand-600">✓ HĐ</span>}
                                                </div>
                                              )}
                                            </button>
                                            {/* "Xem mở rộng" — icon-button góc phải, lộ rõ khi hover/focus
                                                hoặc dòng đang chọn; luôn bấm được (a11y aria-label + tooltip).
                                                [S155 đợt 2] ẨN HẲN ở view "Đã xóa": đó là thao tác, mà mục này
                                                CHỈ XEM; hơn nữa detail của phiếu đã xóa không tải được. */}
                                            {!deletedView && (
                                            <button
                                              type="button"
                                              onClick={() => expandRow(p.id)}
                                              title="Xem mở rộng"
                                              aria-label={`Xem mở rộng phiếu ${p.tenGoiThau}`}
                                              className={cn(
                                                'absolute right-1.5 top-1.5 inline-flex h-7 w-7 items-center justify-center rounded-md border border-slate-200 bg-white text-slate-500 opacity-0 transition hover:border-brand-300 hover:bg-brand-50 hover:text-brand-700 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/70 group-hover/row:opacity-100',
                                                selectedId === p.id && 'opacity-100',
                                              )}
                                            >
                                              <Maximize2 className="h-3.5 w-3.5" />
                                            </button>
                                            )}
                                          </div>
                                        </li>
                                      ))}
                                    </ul>
                                  ) })} />
                                  </details>
                                )
                              })}
                            </div>
                          </details>
                        )
                      })}
                    </div>
                  </details>
                )
              })}
            </div>
          </div>
        </aside>

        {/* Panel 2: Detail tabs INLINE (như 8e68ed1). Render khi chọn phiếu &&
            chưa mở rộng. Khi expand → overlay phủ lên (panel này vẫn ở dưới). */}
        <main className="hidden overflow-y-auto bg-slate-50 p-6 lg:block">
          {!selectedId && (
            <EmptyState
              icon={ClipboardCheck}
              title={deletedView ? 'Phiếu đã xóa — chỉ xem danh sách' : 'Chọn phiếu ở danh sách'}
              description={deletedView
                ? 'Mục này chỉ liệt kê phiếu đã xóa để đối chiếu. Không mở chi tiết, không khôi phục, không thao tác.'
                : 'Chi tiết NCC + báo giá + duyệt sẽ hiển thị ở đây.'}
            />
          )}
          {selectedId && detail.isLoading && <div className="text-sm text-slate-500">Đang tải…</div>}
          {/* [S189 · YC-028(5)] 404 thật ⟂ lỗi hệ thống — đừng để lỗi 500/timeout đội lốt
              "không tìm thấy" (sự cố A/059: phiếu CÒN NGUYÊN, server nghẹt). */}
          {selectedId && !detail.isLoading && !detail.data && (
            detail.isError && !isNotFound(detail.error) ? (
              <div className="text-sm text-red-600">
                Hệ thống đang chậm hoặc lỗi khi tải phiếu —{' '}
                <button type="button" className="font-medium underline" onClick={() => detail.refetch()}>thử lại</button>.
              </div>
            ) : (
              <div className="text-sm text-red-600">Không tìm thấy phiếu.</div>
            )
          )}
          {selectedId && !isExpand && detail.data && (
            <PeDetailTabs
              evaluation={detail.data}
              onBack={closeDetail}
              onDelete={() => del.mutate(detail.data!.id)}
              readOnly={true}
            />
          )}
        </main>

        {/* Panel 3: Workflow panel INLINE (như 8e68ed1). */}
        <aside className="hidden overflow-y-auto border-l border-slate-200 bg-white p-4 lg:block">
          {!selectedId && (
            <div className="rounded-lg border border-dashed border-slate-200 p-6 text-center text-sm text-slate-400">
              <X className="mx-auto mb-2 h-5 w-5" />
              {deletedView
                ? 'Phiếu đã xóa không còn quy trình duyệt.'
                : 'Quy trình duyệt sẽ hiện khi chọn phiếu.'}
            </div>
          )}
          {selectedId && !isExpand && detail.data && (
            <PeWorkflowPanel
              evaluation={detail.data}
              readOnly={!pendingMe}
              onApproved={onApproved}
              onDeleted={onDeleted}
            />
          )}
        </aside>
      </div>

      {/* ─── S79 Focus overlay (full-bleed, slide-from-right) ─────────────────
          Render CHỈ khi id && expand. Che hẳn menu trái + TopBar + list + inline
          detail. Phiếu full-width scroll RIÊNG (anh Kiệt: "slibar của phiếu phải
          có") + panel duyệt cạnh phải (desktop) / stack dưới (narrow). Duyệt
          xong → onApproved đóng hẳn (về list). Thu gọn / Esc / click backdrop →
          bỏ expand (về inline detail, GIỮ id). */}
      {focusMounted && (
        <div
          className="fixed inset-0 z-50"
          role="dialog"
          aria-modal="true"
          aria-label={`Chi tiết phiếu ${detail.data?.tenGoiThau ?? ''}`}
        >
          {/* Backdrop mờ — fade riêng, click để thu gọn (mép trái còn hở ~chút
              trên màn cực rộng vì panel max-w). */}
          <div
            className={cn(
              'absolute inset-0 bg-slate-900/40 transition-opacity duration-200 motion-reduce:transition-none',
              focusVisible ? 'opacity-100' : 'opacity-0',
            )}
            onClick={collapseFocus}
            aria-hidden="true"
          />
          {/* Panel trượt — chiếm full viewport, transform translateX. */}
          <div
            ref={overlayRef}
            className={cn(
              'absolute inset-0 flex flex-col bg-slate-50 shadow-2xl transition-transform duration-200 ease-out will-change-transform motion-reduce:transition-none',
              focusVisible ? 'translate-x-0' : 'translate-x-full',
            )}
          >
            {/* Top strip riêng của overlay: Thu gọn + tên phiếu + trạng thái. */}
            <div className="flex shrink-0 items-center gap-3 border-b border-slate-200 bg-white px-4 py-2.5 sm:px-6">
              <button
                ref={closeBtnRef}
                onClick={collapseFocus}
                aria-label="Thu gọn — quay lại chi tiết trong danh sách"
                className="inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 transition hover:border-brand-300 hover:bg-brand-50 hover:text-brand-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/70 focus-visible:ring-offset-1"
              >
                <ArrowLeft className="h-4 w-4" />
                Thu gọn
              </button>
              <div className="flex min-w-0 flex-1 items-center gap-2">
                <ClipboardCheck className="hidden h-4 w-4 shrink-0 text-brand-600 sm:block" />
                <span className="truncate text-sm font-semibold text-slate-900">
                  {detail.data?.tenGoiThau ?? (detail.isLoading ? 'Đang tải…' : 'Chi tiết phiếu')}
                </span>
                {pendingMe && (
                  <span className="hidden shrink-0 rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-medium text-amber-700 sm:inline">
                    Chế độ duyệt
                  </span>
                )}
              </div>
            </div>

            {/* Body: phiếu (scroll riêng) | panel duyệt (scroll riêng).
                Desktop = 2 cột [1fr | 360-400px]. Narrow (<lg) = 1 cột stack:
                phiếu trên, panel duyệt dưới — 1 code path phục vụ cả 2. */}
            {detail.isLoading && (
              <div className="flex flex-1 items-center justify-center text-sm text-slate-500">Đang tải phiếu…</div>
            )}
            {!detail.isLoading && !detail.data && (
              detail.isError && !isNotFound(detail.error) ? (
                <div className="flex flex-1 items-center justify-center p-6 text-sm text-red-600">
                  Hệ thống đang chậm hoặc lỗi khi tải phiếu —{' '}
                  <button type="button" className="font-medium underline" onClick={() => detail.refetch()}>thử lại</button>.
                </div>
              ) : (
                <div className="flex flex-1 items-center justify-center p-6 text-sm text-red-600">Không tìm thấy phiếu.</div>
              )
            )}
            {detail.data && (
              <div className="flex min-h-0 flex-1 flex-col overflow-y-auto lg:flex-row lg:overflow-hidden">
                {/* Cột phiếu — scrollbar ĐỘC LẬP (anh Kiệt yêu cầu). */}
                <main className="min-w-0 flex-1 p-3 sm:p-5 lg:overflow-y-auto lg:p-6">
                  <PeDetailTabs
                    evaluation={detail.data}
                    onBack={collapseFocus}
                    onDelete={() => del.mutate(detail.data!.id)}
                    readOnly={true}
                  />
                </main>
                {/* Panel duyệt — cạnh phải desktop, dưới phiếu narrow. Scroll riêng. */}
                <aside className="shrink-0 border-t border-slate-200 bg-white p-3 sm:p-5 lg:w-[24rem] lg:border-l lg:border-t-0 lg:p-5 lg:overflow-y-auto xl:w-[26rem]">
                  <PeWorkflowPanel
                    evaluation={detail.data}
                    readOnly={!pendingMe}
                    onApproved={onApproved}
                    onDeleted={onDeleted}
                  />
                </aside>
              </div>
            )}
          </div>
        </div>
      )}
    </div>
  )
}

// Fullpage detail route cho mobile (/purchase-evaluations/:id)
export function PurchaseEvaluationDetailPage() {
  const navigate = useNavigate()
  const id = location.pathname.split('/').pop()!
  const detail = useQuery({
    queryKey: ['pe-detail', id],
    queryFn: async () => (await api.get<PeDetailBundle>(`/purchase-evaluations/${id}`)).data,
  })
  const del = useMutation({
    mutationFn: async () => api.delete(`/purchase-evaluations/${id}`),
    onSuccess: () => {
      toast.success('Đã xóa.')
      navigate('/purchase-evaluations')
    },
  })
  if (detail.isLoading) return <div className="p-6 text-sm text-slate-500">Đang tải…</div>
  if (!detail.data) {
    // [S189 · YC-028(5)] mirror 2 site trên — 404 thật mới được nói "không tìm thấy".
    if (detail.isError && !isNotFound(detail.error)) return (
      <div className="p-6 text-sm text-red-600">
        Hệ thống đang chậm hoặc lỗi khi tải phiếu —{' '}
        <button type="button" className="font-medium underline" onClick={() => detail.refetch()}>thử lại</button>.
      </div>
    )
    return <div className="p-6 text-sm text-red-600">Không tìm thấy phiếu.</div>
  }
  return (
    <div className="space-y-4 p-6">
      <PeDetailTabs evaluation={detail.data} onBack={() => navigate('/purchase-evaluations')} onDelete={() => del.mutate()} />
      {/* [S155 đợt 2] Call-site THỨ BA của panel (route fullpage mobile `/purchase-evaluations/:id`).
          Ở đây `readOnly` để MẶC ĐỊNH = false ⇒ nút Xóa CÓ THỂ hiện ⇒ bắt buộc truyền `onDeleted`,
          nếu không trang sẽ đứng lại với phiếu vừa biến mất (fetch lại là 404). */}
      <PeWorkflowPanel evaluation={detail.data} onDeleted={() => navigate('/purchase-evaluations')} />
    </div>
  )
}
