# sub-investigator-codebase-invest-s171 — KHKK UI mirror PE (fable-real LỆNH-A)

> ENGINE-ĐẮT single top-model deep-pass. PROPOSE-ONLY. Mọi khẳng-định kèm `file:line`.
> Đường-dẫn tương-đối gốc `fe-admin/src/` trừ khi ghi rõ khác. fe-user SHA256-identical (lead đã đo 4/4).

## §0. Hai đính-chính vào Ground-truth của lead (đọc TRƯỚC §1)

**Đ-1 — Panel 1 của PE KHÔNG phải `PeListPanel`.** Lead ghi "PE panel 1/2/3 = `PeListPanel` · `PeDetailTabs` · `PeWorkflowPanel`". Đo lại: `PurchaseEvaluationsListPage.tsx` **không import** `PeListPanel`; panel 1 được viết **INLINE** trong chính file đó (`:399-639`). `PeListPanel` chỉ có **một** call-site duy nhất: `pages/pe/PurchaseEvaluationWorkspacePage.tsx:84` (màn workspace 2-panel, `grid-cols-[260px_1fr]` `:80`).
⇒ Hệ quả cho §5: **không có component list dùng-lại được** để KHKK nhặt. Muốn "giống" panel 1 thì phải TÁCH nó ra trước, hoặc chép khuôn.

**Đ-2 — Đã tồn tại một bản mirror PE 3-panel KHÁC, mới hơn: GĐ3 (Hợp đồng).** `pages/contracts/ContractsListPage.tsx:125` = `grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[340px_1fr_360px]`, header `:106-123` sao y PE, Panel 2 = `ContractDetailContent`, Panel 3 = `WorkflowHistoryPanel` (`components/contracts/`), và Panel 3 khi **chưa chọn** thì render `PipelineTreePanel currentStage={3}` (`:249`).
⇒ Đây là **tiền-lệ trong nhà, cùng chuỗi giai-đoạn, làm ở S168**. KHKK là giai-đoạn NẰM GIỮA GĐ1 (PE, 3-panel) và GĐ3 (HĐ, 3-panel) mà lại là trang-giấy-cuộn ⇒ nó là **mắt xích lệch duy nhất** của cả 4 giai-đoạn. Đây là lập-luận mạnh nhất ủng-hộ đề-bài owner, và nó **không nằm trong ảnh** — owner nhìn thấy triệu-chứng, cái này là nguyên-nhân.

---

## §1. Bản-đồ 3 panel PE (mẫu)

### 1.0 Khung trang (container)

| Hạng-mục | Sự-thật | `file:line` |
|---|---|---|
| Root | `<div className="flex h-[calc(100vh-4rem)] flex-col">` — trang CHIẾM TRỌN viewport, KHÔNG scroll body | `pages/pe/PurchaseEvaluationsListPage.tsx:381` |
| Header | 1 dòng `<header>`: icon `ClipboardCheck` + `h1` tiêu-đề động + badge đếm `rounded-full bg-slate-100` | `:382-390` |
| Tiêu-đề động | `headerTitle` đổi theo `deletedView`/`typeFilter`/`pendingMe` | `:289-293` |
| Grid | `grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[400px_1fr_360px]` | `:397` |
| URL-param lái toàn màn | `type` · `pendingMe` · `deleted` · `q` · `phase` · `awId` · `id` · `expand` | `:38-50` |
| 2 query nuôi 3 panel | `list` (`['pe-list',{…}]`) + `detail` (`['pe-detail', selectedId]`, `enabled: !!selectedId`) | `:74-117` |

🔴 PE là **một trang duy nhất**; không có route detail riêng cho desktop. `PurchaseEvaluationDetailPage` (`:782-807`) chỉ là fallback fullpage **mobile** `/purchase-evaluations/:id`, và vẫn dựng đúng 2 component đó xếp dọc.

### 1.1 Panel 1 — LIST (trái, `400px`)

`<aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">` `:399`

1. Khối filter `space-y-2 border-b border-slate-200 p-3` `:400`
   - Search icon-trong-ô: `Search` absolute + `<Input className="pl-8">`, placeholder `"Tìm mã / tên gói thầu / dự án…"` `:401-409`
   - `Select` quy-trình duyệt — **chỉ khi `pendingMe`** `:413-422`
   - Banner đỏ "Phiếu đã xóa" `:427-431` / banner amber "Lọc cố định: Đã gửi duyệt" `:432-435` / `Select` trạng-thái `:437-453` (ba-ngả loại-trừ)
2. Vùng cuộn `flex-1 overflow-y-auto` `:457`
   - Skeleton 6× `h-16 animate-pulse` `:458-464` · `EmptyState icon={ClipboardCheck}` `:465-469`
   - **Cây 3 tầng `<details>`**: `📅 Năm` → `📁 Dự án` → `🧱 Hạng mục` `:472-637`; chevron `group-open/year|proj|wi:rotate-90`; badge đếm mỗi tầng; dựng ở `yearGroups` `:220-262`
   - Tầng 4 = `<PipelineStageFolders currentStage={1} hideEmpty={filterActive} stages={buildStages(wg.items,{stage1Content:<ul>…</ul>})} />` `:524-625` — 4 folder giai-đoạn; danh-sách phiếu bị RE-PARENT vào folder GĐ1
   - Card phiếu `:530-621`: hàng 1 `PeUrgentChips` + `PeFinalizeChip` + tên gói thầu + badge trạng-thái; hàng 2 mã `font-mono` + timestamp; hàng 3 `👤 drafter · dept` + `✓ HĐ`
   - Nút `Maximize2` "Xem mở rộng" absolute, `opacity-0 … group-hover/row:opacity-100` `:607-620`
3. Persist mở/đóng: `localStorage` key `pe_list_expanded_projects_v3` `:269-287`

### 1.2 Panel 2 — DETAIL (giữa, `1fr`)

`<main className="hidden overflow-y-auto bg-slate-50 p-6 lg:block">` `:643`
- Chưa chọn ⇒ `EmptyState` "Chọn phiếu ở danh sách" `:644-652` (khớp Ảnh 1)
- `detail.data && !isExpand` ⇒ `<PeDetailTabs evaluation onBack={closeDetail} onDelete readOnly={true} />` `:657-664`

**Ruột `PeDetailTabs`** (`components/pe/PeDetailTabs.tsx`, 3613 dòng):
- Đầu phiếu: `<PePipelineStrip current={1} />` `:277` — dải 4 giai-đoạn
- Banner phiếu Trả-lại (readOnly) `:386-395`
- `<div className="divide-y divide-slate-200">` `:397` chứa **5 khối đánh số**, đúng thứ-tự dọc:
  1. `<Section title="1. Thông tin gói thầu">` `:402`
  2. `2. Hạng mục + Báo giá NCC (…)` `:405`
  3. `3. Đơn vị NCC/TP được chọn` `:418`
  4. `4. Thông tin chọn thầu` — `<section>` trần, header xanh `:423-425`
  5. **`<Section title="5. Ý kiến cấp duyệt (sign-off theo workflow)">`** `:426-437` → `LevelOpinionsSectionV2` khi có `approvalWorkflowId`, ngược lại fallback V1 `DepartmentOpinionsSection` `:434-436`
- `<Section>` = `<section className="px-3 py-3 sm:px-5 sm:py-4">` + `h3 mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500` `:514-523`

**Section 5 (khối owner khoanh đỏ ở Ảnh 3)** — `:666-774`:
- `LevelOpinionsSectionV2` lặp `flow.steps`; mỗi Step tính `totalLevels` · `totalApprovers` · `signedLevels` `:680-700`
- `StepOpinionsBox` `:705-745`: header `Bước {n} — {name}` uppercase bold + chip `departmentName` nền `bg-emerald-50` + meta phải `ml-auto` `{signedLevels}/{totalLevels} cấp đã duyệt · {totalApprovers} NV tham gia` `:718-733`; rỗng ⇒ `— Chưa có ý kiến duyệt.` `:736`; có ⇒ `grid grid-cols-1 gap-3 md:grid-cols-2` `:738`
- `StepOpinionEntry` `:747-774`: card `rounded-lg border border-emerald-200 bg-white p-3`; `Cấp {n} — {tên}`; badge `⚠ Admin {X} duyệt thay` khi `signedByUserId !== approverUserId` `:748,756-760`; badge `✓ Đã duyệt` `:762-764`; body `whitespace-pre-wrap` `:766`; footer timestamp `:769-771`

### 1.3 Panel 3 — WORKFLOW (phải, `360px`)

`<aside className="hidden overflow-y-auto border-l border-slate-200 bg-white p-4 lg:block">` `:668`
- Chưa chọn ⇒ hộp `border-dashed` + icon `X` + "Quy trình duyệt sẽ hiện khi chọn phiếu." `:669-676` (khớp Ảnh 1)
- Có phiếu ⇒ `<PeWorkflowPanel evaluation readOnly={!pendingMe} onApproved onDeleted />` `:677-684`

**Ruột `PeWorkflowPanel`** (`components/pe/PeWorkflowPanel.tsx`, 1062 dòng) — thứ-tự dọc ĐÚNG như Ảnh 2:
1. `h3 Quy trình duyệt` + meta `{code} v{NN} · {name}` `:344-354`
2. **Banner kết-thúc-sớm** `evaluation.endsBeforeCeo` `:360-377` — 2 nhánh: `DaDuyet` ⇒ "✅ Phiếu đã kết thúc tại {finalizeStepName} · {finalizeLevelName} — không qua CEO."; đang chạy ⇒ "⚑ Quy trình rút gọn…"
3. **Sơ-đồ Bước → Cấp → NV** `:381-478`: `step.status` do **BE precompute**; `isStepAfterFinalize(step.order)` ⇒ `opacity-50` `:393` + chip `Kết thúc tại {…}` `:411-418`; chip **tên phòng** `step.departmentName` `:406-410`; per-Cấp badge `⚑ Duyệt thay CEO` khi `lv.allowApproverFinalize` `:441-448`; NV liệt kê `/`-phân-cách `:453-465` + badge `✎ NS PRO` / `✎ NS CCM` `:458-463`
4. Fallback V1 "Phiếu này dùng quy trình cũ" `:481-485`
5. Banner "đến lượt bạn" `:489-508` (emerald/amber theo `actorInV2Level`)
6. Banner S134 phiếu-trước-chưa-duyệt `:512-515`
7. 3 nút + nút XÓA (S155, CẠNH `next.map`) `:564` · dialog transition + 4 return-mode `:633-707` + toggle skip-to-final `:723-745` + CCM-done `:749` + ô-tích level-finalize `:769-791` + bộ chọn giá chốt `:792` + upload file-khi-duyệt `:821`
8. `DeptApprovalsSection` (legacy 2-cấp phòng ban) `:920-924`
9. **`📎 File đính kèm khi duyệt`** `:928-967` — `h3` + phụ-đề "File do người duyệt tải lên trong quá trình duyệt." + hàng file (`Paperclip` · tên · `fmtFileSize` · `Eye` xem trước · `Download`)
10. **`Lịch sử duyệt (N)`** — `<PeApprovalsSection ev>` `:969-971`, định-nghĩa ở `PeDetailTabs.tsx:778-785` (export NGƯỢC về panel 3, import `PeWorkflowPanel.tsx:29`); ruột `ApprovalsTab` `:3109-3219`: merge `ev.approvals` + synthetic-reject dựng lại từ changelog + dedupe 5s-bucket `:3152-3192`; mỗi entry = badge quyết-định `decisionBadge` + hint `extractNextTargetHint` + timestamp + `tên · ghi-chú` `:3197-3216`
11. **`Lịch sử thay đổi`** — `<PeHistorySection ev>` `:973-975` → `HistoryTab` `:3222-3300` (GET `/purchase-evaluations/{id}/changelogs`, filter TraLai/gửi-lại/ngân-sách)

### 1.4 Overlay "Xem mở rộng" (`expand=1`) — thành-phần THỨ TƯ của mẫu

`:694-776`. `fixed inset-0 z-50`, backdrop `bg-slate-900/40`, panel `translate-x-full → translate-x-0` 200ms, mount-then-animate 2×rAF `:308-336`, khoá `body.overflow` `:340-349`, Esc + focus-trap `:352-378`. Body = 2 cột `lg:flex-row`: `<main>` `PeDetailTabs` + `<aside lg:w-[24rem] xl:w-[26rem]>` `PeWorkflowPanel` `:753-772`.
⇒ **Cùng 2 component, 3 khung khác nhau** (inline · overlay · route mobile). Đây là bằng-chứng cho §5.

---

## §2. Bản-đồ KHKK hiện-trạng

### 2.0 Khung — KHÁC LOẠI, không phải khác CSS

| Hạng-mục | KHKK | `file:line` |
|---|---|---|
| Root list | `<div className="space-y-5">` — trang **cuộn theo body** | `pages/khkk/KhkkListPage.tsx:161` |
| Header | `<PageHeader eyebrow="Giai đoạn 2" title subtitle icon accent actions>` (khuôn PURO) | `:162-192` |
| Grid | `grid gap-5 lg:grid-cols-[19rem_minmax(0,1fr)] xl:grid-cols-[21rem_minmax(0,1fr)]` = **2 panel** | `:197` |
| Panel trái | `<PipelineTreePanel currentStage={2} />` — cây TOÀN TRÌNH điều-hướng, **không chứa phiếu KHKK** | `:198` |
| Panel phải | 6 `KpiCard` lọc `:202-220` + card search `:222-233` + **bảng 8 cột** `:239-339` + phân trang `:341-355` |
| Chọn phiếu | `onClick={() => navigate('/khkk/'+k.id)}` — **rời trang** | `:280` |
| Route | `/khkk/list` · `/khkk/create` · `/khkk/:id` | `App.tsx:107-110` |
| Detail | Trang riêng, `KhkkWorkflowPanel` render INLINE **ngay dưới `PageHeader`, TRÊN mọi nội-dung** | `pages/khkk/KhkkDetailPage.tsx:411` |
| Detail sections | 4 `SectionCard` rời: Thông tin `:414` · Dòng giá per-NCC `:505` · Căn cứ hồ sơ `:626` · File đính kèm `:721` | |

### 2.1 Panel 1 (KHKK) — ĐANG SAI CHỖ, không phải "đang xấu"

- Ô panel-1 hiện bị `PipelineTreePanel` chiếm. Đó là **cây điều-hướng 4 giai-đoạn dựng từ chỉ-mục PE** (`components/pipeline/PipelineTreePanel.tsx:106-120`, query `['pipeline-pe-index']`, `pageSize 200`), **không phải danh-sách phiếu KHKK**.
- Danh-sách phiếu KHKK nằm ở **bảng 8 cột bên phải** — tức nội-dung thuộc panel 1 đang bị đặt vào ô panel 2.
- ⇒ KHKK **VẮNG** panel-1-theo-nghĩa-PE, và **THỪA** một widget khác nghiệp-vụ ở đúng chỗ đó.

### 2.2 Panel 2 (KHKK) — VẮNG ở màn list

Không ô nào ở `/khkk/list` hiển thị nội-dung phiếu. Nội-dung chỉ tồn-tại sau `navigate('/khkk/:id')`.

### 2.3 Panel 3 (KHKK) — CÓ, nhưng ĐẶT SAI CHỖ và THIẾU 5 khối

`pages/khkk/KhkkWorkflowPanel.tsx` (491 dòng — 🔴 **nằm trong `pages/`, không phải `components/`**) đã mirror **một phần** `PeWorkflowPanel`:

| Khối (theo Ảnh 2) | KHKK | `file:line` |
|---|---|---|
| Header `Quy trình duyệt` + meta | ✅ (thiếu `v{NN}`) | `:194-209` |
| Banner "đã kết thúc tại … không qua CEO" | ❌ **VẮNG** | — (§4 D-1) |
| Sơ-đồ Bước → Cấp ✓/●/○ | ✅ nhưng **FE tự suy** status | `:106-120` · `:213-266` |
| Chip **tên phòng** của Bước | ❌ **VẮNG** | `types/khkk.ts:275-282` (chỉ `departmentId`) |
| Badge `⚑ Duyệt thay CEO` per-Cấp | ❌ **VẮNG** (cờ `allowApproverFinalize` ĐÃ CÓ trên DTO, chỉ dùng cho ô-tích) | `types/khkk.ts:272` · `:143` |
| Làm mờ Bước/Cấp sau finalize | ❌ **VẮNG** | — |
| Badge vai `✎ NS PRO/CCM` | ❌ VẮNG | — |
| `📎 File đính kèm khi duyệt` | ❌ **VẮNG** | — (§4 D-7) |
| `Lịch sử duyệt (N)` | ❌ **VẮNG** | — (§4 D-2) |
| `Lịch sử thay đổi` | ❌ **VẮNG** | — (§4 D-3) |
| Ý kiến cấp duyệt | ⚠️ **CÓ NHƯNG SAI PANEL** — list phẳng 1 cột trong panel duyệt | `:376-412` |
| 3 nút + Dialog | ✅ | `:299-374` · `:415-488` |
| Ô-tích "Cấp này KẾT THÚC" (opt-in) | ✅ | `:451-470` |
| 4 return-mode | ❌ cố-ý bỏ (W3 = TraLai toàn phần) | `:6-8` · `:290` |
| Bộ chọn giá chốt | ❌ cố-ý bỏ (BE chốt ở finalize choke-point) | `:6-8` · `:449-450` |

🔴 **Delta owner nhìn thấy rõ nhất:** khối "Ý kiến cấp duyệt" (Ảnh 3, khoanh đỏ) bên PE là **Section 5 của panel 2**, nhóm **BƯỚC → PHÒNG**, lưới **2 cột**; bên KHKK nó là **list phẳng nhét trong panel 3**. Đây vừa sai **panel**, vừa sai **hình**.

---

## §3. Bảng DELTA per-panel

Luật phán (hiến-pháp run): "hiện tại nó đang thế" **KHÔNG** phải lý-do ⇒ phán lại thành GIỐNG-được.

### 3.A Khung trang

| # | Hạng-mục | PE làm sao | KHKK làm sao | Phán | Lý-do |
|---|---|---|---|---|---|
| K1 | Chiều cao | `flex h-[calc(100vh-4rem)] flex-col`, không cuộn body (`:381`) | `space-y-5` giấy-cuộn (`:161`) | **GIỐNG-được** | 3-panel bắt buộc phải chiếm-trọn-viewport, nếu không mỗi panel không có thanh cuộn riêng. GĐ3 đã làm y hệt (`ContractsListPage.tsx:106`) |
| K2 | Header | `<header>` 1 dòng: icon + h1 + badge đếm (`:382-390`) | `<PageHeader>` eyebrow/subtitle/actions (`:162-192`) | **GIỐNG-được** | Cùng chức-năng; GĐ1 và GĐ3 đều dùng header 1 dòng, chỉ GĐ2 lệch. Actions (nút "Lập kế hoạch") giữ ở góc phải header |
| K3 | Grid | `[400px_1fr_360px]` (`:397`) | `[19rem_minmax(0,1fr)]` (`:197`) | **GIỐNG-được** | Đề-bài trực-tiếp |
| K4 | Chọn phiếu | URL `?id=` in-page, `<lg` mới đi overlay (`:152-158`) | `navigate('/khkk/:id')` rời trang (`:280`) | **GIỐNG-được** | GĐ3 giữ CẢ HAI: desktop `?id=`, `<lg` `navigate` (`ContractsListPage.tsx:90-96`) — khuôn sẵn, chép được |

### 3.B Panel 1

| # | Hạng-mục | PE | KHKK | Phán | Lý-do |
|---|---|---|---|---|---|
| P1-1 | Nguồn cây | cây **phiếu PE** 3 tầng + folder GĐ (`:472-637`) | `PipelineTreePanel` cây **điều-hướng** (`:198`) | **GIỐNG-được** | Cùng bộ dựng cây `Năm>Dự án>Hạng mục>4-folder`. Chỉ cần đổ **card phiếu KHKK** vào `content` của folder GĐ2 y như PE đổ vào GĐ1 (§4 D-8) |
| P1-2 | Ô search | trong panel 1, icon-trong-ô (`:401-409`) | card riêng giữa trang (`:222-233`) | **GIỐNG-được** | Cùng chức-năng, khác chỗ đặt |
| P1-3 | Lọc trạng-thái | `<Select>` 1 dòng (`:437-453`) | 6 `KpiCard` hàng ngang (`:202-220`) | **GIỐNG-được** | KpiCard đẹp nhưng ăn hết bề ngang — không sống nổi trong cột 400px. Owner đã nói "đồng nhất" ⇒ `Select` thắng |
| P1-4 | Lọc **nhóm N1-N8** | không có (PE có `awId` dropdown quy-trình `:413-422`) | đọc từ URL `?group=` (`:79-80`), 8 leaf menu | **KHÁC-buộc-phải-khác** | **Nghiệp-vụ SP-002**: 8 nhóm × 8 workflow `KHKK-N{n}` (`types/khkk.ts:163`), rào 409 ở BE (`ContractSigningPlanFeatures.cs:420`). PE không có trục này. 🔸 Nhưng **CHỖ ĐẶT phải giống**: dropdown trong khối filter panel 1, đúng ô mà PE đặt `awId` |
| P1-5 | Dòng phiếu | card 3 hàng + chip (`:546-601`) | `<tr>` 8 cột (`:277-337`) | **GIỐNG-được** | Bảng 8 cột không vừa cột 400px; card 3 hàng chứa đủ: mã · gói thầu · trạng thái · Σ tiền · người soạn · ngày |
| P1-6 | Phân trang | không (pageSize 50 `:101`) | 20/trang, nút Trước/Sau (`:341-355`) | **GIỐNG-được** | Không phải "bỏ phân trang" — GĐ3 đã có **phân trang compact NGAY TRONG panel 1** (`ContractsListPage.tsx:200-222`). Chép khuôn đó |
| P1-7 | Nút "Xem mở rộng" → overlay | có (`:607-620` + `:694-776`) | không có | **GIỐNG-được** | Ưu-tiên P2 (xem §7) — không chặn P1 |
| P1-8 | Persist mở/đóng cây | `pe_list_expanded_projects_v3` (`:269`) | `pipeline_tree_expanded_v1` (`PipelineTreePanel.tsx:21`) | **GIỐNG-được** | Đã có sẵn, chỉ cần giữ key riêng cho GĐ2 |

### 3.C Panel 2

| # | Hạng-mục | PE | KHKK | Phán | Lý-do |
|---|---|---|---|---|---|
| P2-1 | Dải 4 giai-đoạn đầu phiếu | `<PePipelineStrip current={1} />` (`PeDetailTabs.tsx:277`) | không có | **GIỐNG-được** | Component đã dùng-chung được (`ComingSoonPage.tsx:33` truyền `current` động) ⇒ `current={2}` |
| P2-2 | Khung Section | `divide-y divide-slate-200` + `<Section>` h3 `uppercase tracking-wide` **đánh số** (`:397` · `:514-523`) | 4 `SectionCard` card-accent rời, **không đánh số** (`:414 :505 :626 :721`) | **GIỐNG-được** | Đây đúng là "tương tự ⇒ giống nhau". Đổi `SectionCard` → `Section` đánh số + `divide-y` |
| P2-3 | **Nội-dung** các Section | 1 gói thầu · 2 hạng mục+báo giá NCC · 3 chọn NCC · 4 thông tin chọn thầu | Thông tin · Dòng giá per-NCC · Căn cứ hồ sơ b.8-9 (TVGS) · File đính kèm | **KHÁC-buộc-phải-khác** | Nghiệp-vụ GĐ2 ≠ GĐ1: KHKK **không** có báo-giá/so-sánh NCC (đã chốt ở GĐ1), **có** căn-cứ hồ-sơ TVGS (`ContractSigningPlanDossierItem`, `types/khkk.ts:67-99`) |
| P2-4 | **Section CUỐI = Ý kiến cấp duyệt** | Section 5, dưới cùng panel 2 (`:426-437`) | khối nằm trong **panel 3** (`KhkkWorkflowPanel.tsx:376-412`) | **GIỐNG-được** 🔴 | **Chính là ô owner khoanh đỏ Ảnh 3** — "content nhập phía trên, duyệt ghi chú phía dưới". Phải DỜI PANEL |
| P2-5 | Nhóm ý-kiến theo BƯỚC→PHÒNG + meta `x/y cấp · N NV` | `StepOpinionsBox` (`:705-745`) | list phẳng theo `opinions[]` | **GIỐNG-được** | Chỉ cần `workflowSteps` (ĐÃ CÓ) + `departmentName` (§4 D-4) |
| P2-6 | Lưới 2 cột card ý-kiến | `md:grid-cols-2` + card emerald + badge `✓ Đã duyệt` + `⚠ … duyệt thay` + placeholder `(duyệt — không ý kiến)` (`:738-772`) | `<li>` 1 cột `text-[11px]` (đã có nhãn `duyệt thay` + placeholder) | **GIỐNG-được** | Dữ-liệu đã đủ 100% (`KhkkLevelOpinionDto` `types/khkk.ts:245-258` có `approverUserId`/`signedByUserId`/`comment`/`signedAt`) |
| P2-7 | Sửa nội-dung | panel 2 `readOnly={true}` **cứng** (`:663`); sửa ở workspace riêng (`PurchaseEvaluationWorkspacePage`) | sửa **inline** ngay trong detail (nút Sửa/Lưu, `KhkkDetailPage.tsx:422-427`) | **KHÁC-buộc-phải-khác** (có điều-kiện) | KHKK **không có** trang workspace. Ép `readOnly` mà chưa dựng workspace = **cắt mất đường sửa duy nhất** ⇒ mất chức-năng. 🔸 Điều-kiện: khi nào có workspace KHKK thì dòng này lật lại thành GIỐNG-được. Phải khai trong spec, đừng để thành di-sản im-lặng |

### 3.D Panel 3

| # | Hạng-mục | PE | KHKK | Phán | Lý-do |
|---|---|---|---|---|---|
| P3-1 | Meta workflow | `{code} v{NN} · {name}` (`:348-353`) | `{code} · {name}` (`:203-208`) | **GIỐNG-được** | Thiếu `version` (§4 D-5) |
| P3-2 | Banner kết-thúc-sớm | `endsBeforeCeo` 2 nhánh (`:360-377`) | VẮNG | **GIỐNG-được** | Cơ-chế ĐÃ port ở K3 (cột `EndedByLevelFinalize` Mig 71), chỉ chưa expose (§4 D-1). Ảnh 2 có banner này ở vị-trí #2 |
| P3-3 | Sơ-đồ Bước→Cấp ✓/●/○ | BE precompute `status` | FE tự suy (`:106-120`) | **ĐÃ GIỐNG** (hình) | Khác ở tầng dữ-liệu, không khác ở tầng nhìn. Không ép BE precompute — rủi-ro > lợi |
| P3-4 | Chip **tên phòng** của Bước | có (`:406-410`) | VẮNG | **GIỐNG-được** | §4 D-4 |
| P3-5 | Badge `⚑ Duyệt thay CEO` per-Cấp | có (`:441-448`) | VẮNG | **GIỐNG-được** | **0 công BE** — `allowApproverFinalize` đã nằm trên `KhkkWorkflowLevelDto` (`types/khkk.ts:272`) |
| P3-6 | Làm mờ Bước/Cấp sau finalize + chip `Kết thúc tại …` | có (`:393` `:411-418` `:425`) | VẮNG | **GIỐNG-được** | Suy được từ P3-5 + `workflowSteps` — 0 công BE |
| P3-7 | Badge `✎ NS PRO` / `✎ NS CCM` | có (`:458-463`) | VẮNG | **KHÁC-buộc-phải-khác** | Quyền nhập **ngân sách PRO/CCM** là khái-niệm riêng của PE (`PeWorkItemBudgets`, Mig 50). KHKK không có cột ngân sách per-role ⇒ badge sẽ vô nghĩa |
| P3-8 | `📎 File đính kèm khi duyệt` | có, tự-ẩn khi rỗng (`:928-967`) | VẮNG | **GIỐNG-được** (có ngoặc) | Khối + hành-vi tự-ẩn = giống được ngay. 🔸 Nhưng **CHO PHÉP người duyệt upload** là quyết-định owner, không tự làm: cần thêm `ApprovalAttachment` vào enum purpose (§4 D-7). Khuyến-nghị: dựng khối, đọc `purpose=Other`, đợi owner chốt vế upload |
| P3-9 | `Lịch sử duyệt (N)` | có (`:969-971` → `:3109-3219`) | VẮNG | **GIỐNG-được** | 🔴 **Dữ-liệu ĐÃ ĐƯỢC GHI** — `ContractSigningPlanWorkflowService.cs:534`. Chỉ chưa expose (§4 D-2) |
| P3-10 | `Lịch sử thay đổi` (changelog) | có (`:973-975` → `:3222-3300`) | VẮNG | **GIỐNG-được** | 🔴 Changelog ĐÃ GHI (`ContractSigningPlanWorkflowService.cs:547`, và ≥8 chỗ trong `Features.cs`), chỉ thiếu endpoint đọc (§4 D-3) |
| P3-11 | 3 nút + dialog + ô ý-kiến | có | có (`:299-374` `:415-488`) | **ĐÃ GIỐNG** | Không đụng |
| P3-12 | 4 return-mode (Cấp / Bước / NV / Drafter) | có (`:633-707`) | 1 mode (về Bước 1·Cấp 1) (`:290`) | **KHÁC-buộc-phải-khác** | Chuỗi KHKK là **3 trạm ngắn** (PRO→CCM→CEO) — "lùi 1 cấp" gần trùng "về đầu". Thêm 4 mode = đổi **state machine BE**, ngoài phạm-vi UI. 🔸 Khai rõ, không giấu |
| P3-13 | Bộ chọn giá chốt khi duyệt | có (`:792`) | không | **KHÁC-buộc-phải-khác** | KHKK chốt giá **per-Line** tại choke-point BE `ApplyApprovedValuesOnFinalize` (`KhkkWorkflowPanel.tsx:449-450`) — không có "1 giá" để người duyệt chọn |
| P3-14 | Nút XÓA phiếu trong panel duyệt | có, `AllowApproverDelete` Mig 68 (`:564`) | có nhưng ở `PageHeader` (`KhkkDetailPage.tsx:388-399`) | **GIỐNG-được** | Chỉ là dời vị-trí + dialog xác-nhận riêng |
| P3-15 | `readOnly={!pendingMe}` ẩn nút thao-tác ở view Danh sách | có (`:680`) | luôn hiện | **GIỐNG-được** | Cần khi panel 3 sống trong màn list |
| P3-16 | Empty-state panel 3 khi chưa chọn | hộp dashed + `X` + câu (`:669-676`) | n/a | **GIỐNG-được** | Ảnh 1 có. 🔸 GĐ3 chọn cách khác: chưa chọn ⇒ **nhét `PipelineTreePanel` vào panel 3** (`ContractsListPage.tsx:249`) — đây là **lối thoát cho cây toàn-trình** mà KHKK đang giữ ở panel 1 |

**Tổng:** 27 GIỐNG-được · 6 KHÁC-buộc-phải-khác · 2 ĐÃ GIỐNG (không việc).

---

## §4. Data-gap — KHKK có đủ trường nuôi panel 3 + Section 5 không?

🔴 **Kết-luận đầu tiên, quan trọng nhất:** gần như **toàn bộ dữ-liệu ĐÃ TỒN TẠI Ở DB VÀ ĐANG ĐƯỢC GHI**. Khoảng trống nằm ở **tầng đọc (DTO/endpoint)**, không phải tầng ghi. Đây là tin tốt: chi-phí BE nhỏ hơn nhiều so với ước-lượng "phải làm lại history".

| # | Thiếu cái gì | Trạng-thái thật ở BE | Ai cần | Việc phải làm |
|---|---|---|---|---|
| **D-1** | Banner kết-thúc-sớm: `endedByLevelFinalize` + `finalizeStepName` + `finalizeLevelName` | **Cột `EndedByLevelFinalize` ĐÃ CÓ** (Mig 71). `ContractSigningPlanDetailDto` (`ContractSigningPlanFeatures.cs:145-181`) **KHÔNG expose** — đã tự khai ở `types/khkk.ts:299-300` ("cột Mig 71 nằm im chờ K3 tiêu thụ") | P3-2 | +3 field vào DetailDto (append cuối, đúng convention `:174-176`) + 3 field vào `KhkkDetailDto` |
| **D-2** | `Lịch sử duyệt` — danh-sách transition | 🔴 **ĐANG GHI THẬT**: `ContractSigningPlanWorkflowService.cs:534` `db.ContractSigningPlanApprovals.Add(new ContractSigningPlanApproval{FromPhase,ToPhase,Decision,Comment,ApprovalWorkflowLevelId,ApprovedByUserId,ApprovedAt})`. Entity `ContractSigningPlanApproval.cs:17-31`. Bảng `ContractSigningPlanApprovals` + index `(ContractSigningPlanId, ApprovedAt)` (Mig 69 `:247-248`). **DTO không có `Approvals`** | P3-9 | +`List<ContractSigningPlanApprovalDto> Approvals` vào DetailDto. **Cẩn thận:** entity chỉ có `ApprovedByUserId`, **không có tên** ⇒ projection phải join Users (BE đã có `ResolveActorFullNameAsync`, `WorkflowService.cs:552`) |
| **D-3** | `Lịch sử thay đổi` — changelog | 🔴 **ĐANG GHI THẬT** ≥9 chỗ: `WorkflowService.cs:547` + `Features.cs:497,562,639,1078,1116,1224,1304`. Có `UserName` **đã denorm** ⇒ không cần join. **KHÔNG có endpoint đọc** — `ContractSigningPlansController.cs` không có route `changelogs` nào | P3-10 | +`GET /contract-signing-plans/{id}/changelogs` policy `KeHoachKyKet.Read` (mirror PE `PeDetailTabs.tsx:3225`) |
| **D-4** | `departmentName` của Bước | `ContractSigningPlanWorkflowStepDto` chỉ có `DepartmentId` — tự khai ở `types/khkk.ts:275` ("KHÔNG có tên phòng") | P2-5 **và** P3-4 (2 chỗ) | +`DepartmentName` vào StepDto (join `Departments`) |
| **D-5** | `workflowVersion` | DetailDto có `WorkflowCode` + `WorkflowName`, **không có version** (`Features.cs:160-161`) | P3-1 | +1 field |
| **D-6** | Tên người trong lịch-sử duyệt | (xem D-2) `ApprovedByUserId` không kèm tên | P3-9 | Join hoặc denorm giống `Changelog.UserName` |
| **D-7** | Purpose "file khi duyệt" | Enum `ContractSigningPlanAttachmentPurpose` chỉ có `DossierScan=1 · PlanDocument=2 · Other=99` (`ContractSigningPlanAttachment.cs:7-12`) — **không có** giá-trị cho "file người duyệt tải lên" | P3-8 | 🔸 **Quyết-định owner**: (a) dựng khối chỉ-đọc lọc `purpose=Other` — 0 công BE; (b) +`ApprovalAttachment=3` + cho upload trong dialog duyệt — đổi enum + endpoint. **Đừng tự chọn (b)** |
| **D-8** | `stage2Content` / `stage2Count` | `BuildStagesOptions` (`hooks/usePipelineStages.ts:163`) **chỉ có `stage1Content`/`stage1Count`**; stage 2 dựng bằng `leaves` (link) `:265`. `PipelineStageFolders` **đã hỗ-trợ** `content` tổng-quát (`PipelineStageFolders.tsx:88` + render `:285-286`) | P1-1 | Mở rộng ĐỐI-XỨNG: `stage2Content`/`stage2Count` — sửa **1 hook**, 0 đụng component trình-bày |
| **D-9** | *chưa đo được* | Cách `usePipelineStages` nạp phiếu KHKK để dựng leaves stage-2 (`:265-315`) — tôi **chưa đọc** đoạn này. Nó quyết-định card KHKK trong folder GĐ2 lấy dữ-liệu từ đâu (query sẵn hay phải thêm) | P1-1 | **PHẢI đọc `hooks/usePipelineStages.ts:150-320` trước khi code panel 1** |

**Trả lời thẳng câu hỏi §4:** workflow steps/levels ✅ đủ · ý-kiến per-cấp ✅ đủ (kể cả cờ "duyệt thay") · lịch-sử duyệt ⚠️ có ở DB, thiếu ở API · file-khi-duyệt ❌ chưa có khái-niệm.

---

## §5. Kiến-trúc đề-xuất

### Phương-án A — tạo `components/khkk/` riêng, chép khuôn PE

- **Làm:** `mkdir components/khkk/` → `KhkkListPanel.tsx` · `KhkkDetailContent.tsx` (+ `KhkkLevelOpinionsSection.tsx`) · dời `pages/khkk/KhkkWorkflowPanel.tsx` → `components/khkk/`. `KhkkListPage` trở thành host 3-panel.
- **Được:** 0 rủi-ro cho PE (đang chạy prod, 45 phiếu) và GĐ3. Mỗi module giữ UX riêng — đúng nguyên-tắc đã ghi trong `CLAUDE.md` ("duplicate giữa 2 app là CÓ CHỦ ĐÍCH"). Diff dễ review. Mirror 2-app đơn-giản (copy nguyên file).
- **Mất:** trùng-lặp thật. Sửa "cách hiển-thị ý-kiến cấp duyệt" sau này phải sửa ≥3 nơi (PE · KHKK · HĐ).

### Phương-án B — tổng-quát-hoá component PE thành `components/approval/` dùng chung

- **Làm:** trích `StepOpinionsBox`/`StepOpinionEntry` (`PeDetailTabs.tsx:705-774`) và bộ sơ-đồ Bước→Cấp (`PeWorkflowPanel.tsx:381-478`) thành component generic nhận `steps[] · opinions[] · finalizeInfo`.
- **Được:** 1 nguồn sự-thật cho 3 module (PE · KHKK · HĐ).
- **Mất:** 🔴 **shape KHÁC NHAU THẬT.** PE nhận `approvalFlow` **có `status` do BE precompute** (`PeWorkflowPanel.tsx:384`); KHKK nhận **cây thô, FE tự suy** (`KhkkWorkflowPanel.tsx:22-29,106-120`). Generic hoá = phải hoặc (i) ép BE KHKK precompute, hoặc (ii) nhồi adapter 2 chiều. Cả hai đều **đụng vào đường duyệt của PE đang chạy prod** — rủi-ro không cân với lợi ích thẩm-mỹ. Thêm nữa PE có ≥6 nhánh riêng (NS PRO/CCM · price-picker · 4 return-mode · CCM-done · skip-to-final · DeptApprovals legacy) mà KHKK không có: generic sẽ đầy `if (module === …)`.

### 🔴 CHỌN: **Phương-án A**, kèm 2 điều-kiện

1. **Điều-kiện 1 — lấy khuôn từ GĐ3, không phải từ PE.** `ContractsListPage.tsx` (255 dòng) đã là **bản PE-3-panel đã được gọt gọn**, làm ở S168, cùng chuỗi giai-đoạn, đã qua gate. Chép từ đó rẻ hơn và ít bẫy hơn chép từ `PurchaseEvaluationsListPage.tsx` (807 dòng, mang theo `deleted`/`pendingMe`/`awId`/overlay/2 hệ `<details>`).
2. **Điều-kiện 2 — nợ kỹ-thuật khai ngay, không giấu.** Ghi vào đầu file `components/khkk/` một dòng: *"Khối ý-kiến cấp duyệt là bản thứ 3 của cùng một hình (PE `PeDetailTabs.tsx:705-774` · HĐ `WorkflowHistoryPanel.tsx`). Gộp được khi và chỉ khi BE 3 module thống-nhất shape `status` precompute."* — biến trùng-lặp từ **tai-nạn** thành **quyết-định có điều-kiện thoát**.

### Bố-cục đích (đề-xuất)

```
/khkk/list                     ← host 3-panel, khuôn ContractsListPage
├─ Panel 1  400px   KhkkListPanel        cây Năm>Dự án>Hạng mục>folder GĐ2 → card phiếu KHKK
│                                        + search + Select trạng-thái + Select nhóm N1-N8
├─ Panel 2  1fr     KhkkDetailContent    PePipelineStrip current={2}
│                                        Section 1 Thông tin kế hoạch
│                                        Section 2 Dòng giá per-NCC trúng thầu
│                                        Section 3 Căn cứ hồ sơ (b.8-9)
│                                        Section 4 File đính kèm
│                                        Section 5 Ý KIẾN CẤP DUYỆT   ← Ảnh 3 khoanh đỏ
└─ Panel 3  360px   KhkkWorkflowPanel    chưa chọn → PipelineTreePanel currentStage={2}
                                         (khuôn ContractsListPage.tsx:249)
                                         đã chọn  → 1 Quy trình duyệt + meta
                                                    2 Banner kết-thúc-sớm
                                                    3 Sơ-đồ Bước→Cấp (+chip phòng, +⚑, +mờ-sau-finalize)
                                                    4 Nút Duyệt/Trả lại/Từ chối + Dialog
                                                    5 📎 File đính kèm khi duyệt
                                                    6 Lịch sử duyệt (N)
                                                    7 Lịch sử thay đổi
```

`/khkk/:id` **GIỮ NGUYÊN route** (fallback `<lg` + deep-link + bookmark cũ) nhưng ruột đổi thành `KhkkDetailContent` + `KhkkWorkflowPanel` xếp dọc — đúng khuôn `PurchaseEvaluationDetailPage` (`:798-805`).

---

## §6. Rủi-ro / bẫy

| # | Bẫy | Bằng-chứng | Phòng |
|---|---|---|---|
| **R-1** 🔴 | **fe-admin KHÔNG hiển thị menu KHKK.** `ADMIN_HIDDEN_KEYS` chứa `'KeHoachKyKet'` (K6/QĐ7 S164) ⇒ toàn nhánh `Khkk_*` bị cắt khỏi sidebar admin. Cũng cắt `PurchaseEvaluations` và `Contracts` | `components/Layout.tsx:227-231` | **Ảnh owner gửi là fe-user, không phải fe-admin.** ⇒ UAT phải nghiệm-thu trên **fe-user:8080**; mirror 2 app là BẮT BUỘC, không phải tuỳ chọn. Deep-link `/khkk/list` bên admin vẫn vào được (`:212-216` — display-layer ≠ authz-layer, gotcha #82) |
| **R-2** | **gotcha #85** — dùng ĐÚNG key policy của endpoint. Mọi route KHKK gate `KeHoachKyKet.{Read/Create/Update/Delete}`; riêng bắc-cầu HĐ gate `Contracts.Create` | `ContractSigningPlansController.cs:29,37,87,96,118,223,244` · `hooks/usePipelineStages.ts:11-17,57` | Panel mới **không** OR key con (`Khkk_List`…). Nếu thêm endpoint changelogs (D-3) ⇒ `[Authorize(Policy="KeHoachKyKet.Read")]`, không để `[Authorize]` trần |
| **R-3** | **Pattern 16-bis 4-place mirror.** Menu-key mới/route mới phải có mặt ở: BE seed · `Layout.tsx` staticMap/regex · `App.tsx` route · resolvePath. Thiếu 1 ⇒ leaf **DROP IM LẶNG** (gotcha #50) | `Layout.tsx:37-45,181-188` · `App.tsx:107-110` | Việc này **không đẻ menu-key mới** (giữ `/khkk/list`) ⇒ rủi-ro thấp. Nhưng nếu ai thêm `?id=` vào staticMap thì phải soát lại menu-highlight |
| **R-4** 🔴 | **Chip lọc CỐ Ý không ghi URL.** `KhkkListPage.tsx:12-13,211-216` — ghi URL sẽ làm sidebar **sáng nhầm leaf** (bug UAT S155) | `:82-102` | Panel 1 mới: `q`/`phase` ghi URL kiểu PE sẽ **hồi-sinh bug S155**. 🔸 Đề-xuất: `?id=` ghi URL (cần bookmark) nhưng `phase`/`group` giữ ở lớp override (state), y như hiện tại. **Đây là chỗ dễ làm hỏng nhất khi "chép khuôn PE cho giống"** |
| **R-5** | **8 nhóm dùng CHUNG 1 route** ⇒ mọi thứ đọc từ URL phải đọc **mỗi render**, không `useState` khởi-tạo-một-lần (đi leaf→leaf không remount) | `:15-19,70-102` (vá-5 K4b) | Host 3-panel mới phải giữ nguyên cơ-chế `navKey`/`lastNavKey` (`:95-101`), đừng viết lại bằng `useEffect` |
| **R-6** | **`PipelineTreePanel` `currentStage` + `bodyMaxHClass`.** Trong host 3-panel (Panel-3 tự cuộn) phải truyền `bodyMaxHClass="max-h-[28rem] lg:max-h-none"` nếu không sẽ **2 thanh cuộn lồng nhau** (~26px vượt) | `ContractsListPage.tsx:247-249` · `PipelineTreePanel.tsx:93-97,221` | Chép nguyên chuỗi đó. Nếu đặt cây ở panel 1 thì giữ mặc-định |
| **R-7** | **Mirror SHA256 2 app.** 4 file KHKK + `PipelineTreePanel` + `usePipelineStages` đều khai "MIRROR SHA256 identical" | `KhkkListPage.tsx:3-4` · `types/khkk.ts:2` · `PipelineTreePanel.tsx:6` | Acceptance §7 A-9: `sha256` từng cặp, không "nhìn thấy giống" |
| **R-8** | **Không có phiếu V1-legacy ở KHKK.** Module sinh 2026-07-29 (Mig 69), mọi phiếu đều V2 | `types/khkk.ts:1` · `KhkkWorkflowPanel.tsx:261-266` (chỉ fallback "chưa cấu hình Bước/Cấp") | **Không** port nhánh fallback V1 của PE (`PeDetailTabs.tsx:434-436`, `PeWorkflowPanel.tsx:481-485`) — sẽ là code chết |
| **R-9** | **Route hiện có không được phá.** `/khkk/:id` đang là đích của `navigate` ở list (`:280`), của nút "Danh sách" (`KhkkDetailPage.tsx:400`), và có thể của link ngoài | `App.tsx:110` | Giữ route; chỉ đổi ruột. Thêm `?id=` cho desktop, `navigate` cho `<lg` (khuôn `ContractsListPage.tsx:90-96`) |
| **R-10** | **`budgetFrozen` display-gate.** Phiếu `DaDuyet` đọc `ApprovedAmount`, phiếu chưa duyệt đọc `ProposedAmount` | `types/khkk.ts:296-301` · `Features.cs:177-181` | Card panel 1 hiện Σ tiền phải theo đúng cờ này, nếu không sẽ nói **2 con số khác nhau** cho cùng 1 phiếu (đúng lỗi vá-8 K2 đã sửa ở bảng — `KhkkListPage.tsx:247-250`) |
| **R-11** | **Cây tải tối đa 200 phiếu.** `PE_INDEX_PAGE_SIZE = 200`, có dòng cảnh-báo khi chạm trần | `PipelineTreePanel.tsx:22,252-256` | Nếu panel 1 dựng cây từ chỉ-mục PE thì phiếu KHKK của gói cũ hơn **sẽ không xuất hiện**. Phải giữ (và thấy) dòng cảnh-báo đó |
| **R-12** | **`KhkkWorkflowPanel` đang ở `pages/`, không phải `components/`** ⇒ import chéo `pages→pages` (`KhkkDetailPage.tsx:41`) | `KhkkDetailPage.tsx:41` | Dời file = đổi import ở ≥2 nơi × 2 app. Việc nhỏ nhưng dễ sót ở app thứ hai |

---

## §7. Checklist thực-thi ĐO ĐƯỢC

Mỗi mục có lệnh/grep kiểm được. Chạy tại repo-root. `APP` ∈ {`fe-admin`,`fe-user`} — mọi mục phải PASS **cả hai**.

### Nhóm 0 — bắt buộc TRƯỚC khi viết dòng code đầu tiên

| # | Việc | Acceptance ĐO ĐƯỢC |
|---|---|---|
| A-0 | Đọc `hooks/usePipelineStages.ts:150-320` để chốt nguồn dữ-liệu phiếu KHKK cho folder GĐ2 (§4 D-9) | Ghi vào spec 1 dòng: query nào nuôi stage-2, có `KhkkListItemDto` sẵn hay phải thêm fetch. Không có dòng này ⇒ **không được bắt đầu A-3** |

### Nhóm 1 — BE (nhỏ, 1 lượt)

| # | Việc | Acceptance |
|---|---|---|
| A-1 | DetailDto +5 field: `WorkflowVersion` · `EndedByLevelFinalize` · `FinalizeStepName` · `FinalizeLevelName` · `List<…ApprovalDto> Approvals`; StepDto +`DepartmentName` | `grep -n "record ContractSigningPlanDetailDto" -A 70 src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` → thấy đủ 5 tên; `grep -c "DepartmentName" …WorkflowStepDto` ≥1 |
| A-2 | `GET /contract-signing-plans/{id}/changelogs` policy `KeHoachKyKet.Read` | `grep -n "changelogs" src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs` → 1 hit, và dòng `[Authorize(Policy = "KeHoachKyKet.Read")]` đứng ngay trên (gotcha #85 / #82) |
| A-2b | Tên người trong `Approvals` không rỗng | Gọi thật 1 phiếu đã duyệt: response `approvals[].approvedByFullName` ≠ null cho ≥1 phần tử. 🔴 Không nghiệm-thu bằng "code trông đúng" |

### Nhóm 2 — FE cấu-trúc

| # | Việc | Acceptance |
|---|---|---|
| A-3 | `$APP/src/components/khkk/` tồn-tại, chứa `KhkkListPanel.tsx` · `KhkkDetailContent.tsx` · `KhkkWorkflowPanel.tsx` (dời từ `pages/`) | `ls $APP/src/components/khkk/` → 3 file; `grep -rn "pages/khkk/KhkkWorkflowPanel" $APP/src` → **0 hit** |
| A-4 | `KhkkListPage` là host 3-panel | `grep -n "lg:grid-cols-\[" $APP/src/pages/khkk/KhkkListPage.tsx` → khớp `^.*lg:grid-cols-\[[0-9]+px_1fr_[0-9]+px\]` ; và `grep -c "h-\[calc(100vh-4rem)\]"` = 1 |
| A-5 | Chọn phiếu = `?id=` (desktop) / `navigate` (`<lg`), route `/khkk/:id` CÒN SỐNG | `grep -n "matchMedia('(min-width: 1024px)')" $APP/src/pages/khkk/KhkkListPage.tsx` ≥1 · `grep -n 'path="/khkk/:id"' $APP/src/App.tsx` = 1 |
| A-6 | Bộ lọc trạng-thái/nhóm **KHÔNG** ghi vào URL (R-4) | `grep -n "setSearchParams\|setSp(" $APP/src/pages/khkk/KhkkListPage.tsx` → chỉ xuất hiện trong hàm set `id`; **0 hit** cho `'phase'`/`'group'` bên trong lời gọi đó |

### Nhóm 3 — FE nội-dung (từng khối của ảnh)

| # | Khối | Acceptance |
|---|---|---|
| A-7a | Panel 2 có `PePipelineStrip current={2}` | `grep -n "PePipelineStrip" $APP/src/components/khkk/KhkkDetailContent.tsx` → 1 hit kèm `current={2}` |
| A-7b | Panel 2 có **5 Section đánh số**, Section CUỐI = ý-kiến cấp duyệt | `grep -oE 'title="[0-9]\. [^"]+"' $APP/src/components/khkk/KhkkDetailContent.tsx` → 5 dòng, dòng cuối khớp `^title="5\. Ý kiến cấp duyệt` |
| A-7c | Ý-kiến nhóm theo BƯỚC + lưới 2 cột + meta `x/y cấp · N NV` | `grep -n "md:grid-cols-2" …KhkkDetailContent.tsx` ≥1 **và** `grep -c "cấp đã duyệt"` ≥1 **và** `grep -c "NV tham gia"` ≥1 |
| A-7d | Ý-kiến **KHÔNG CÒN** ở panel 3 | `grep -c "Ý kiến cấp duyệt" $APP/src/components/khkk/KhkkWorkflowPanel.tsx` = **0** |
| A-8a | Panel 3 có banner kết-thúc-sớm | `grep -c "không qua CEO" …KhkkWorkflowPanel.tsx` ≥1 |
| A-8b | Panel 3 có chip tên phòng + badge `⚑ Duyệt thay CEO` + làm mờ sau finalize | 3 grep: `departmentName` ≥1 · `Duyệt thay CEO` ≥1 · `opacity-50` ≥1 |
| A-8c | Panel 3 có `📎 File đính kèm khi duyệt` | `grep -c "File đính kèm khi duyệt"` ≥1 |
| A-8d | Panel 3 có `Lịch sử duyệt (N)` + `Lịch sử thay đổi` | `grep -c "Lịch sử duyệt"` ≥1 **và** `grep -c "Lịch sử thay đổi"` ≥1 |
| A-8e | Panel 3 chưa chọn ⇒ `PipelineTreePanel currentStage={2} bodyMaxHClass=…` (R-6) | `grep -n "bodyMaxHClass" $APP/src/pages/khkk/KhkkListPage.tsx` → 1 hit chứa `lg:max-h-none` |

### Nhóm 4 — chống hồi-quy

| # | Việc | Acceptance |
|---|---|---|
| A-9 | Mirror 2 app byte-identical | Với mỗi file mới/sửa: `sha256sum fe-admin/src/<p>` = `sha256sum fe-user/src/<p>` → **PASS 100%**, không trừ file nào |
| A-10 | PE và HĐ **không bị đụng** | `git diff --name-only` → **0 đường-dẫn** khớp `components/pe/` · `pages/pe/` · `components/contracts/` · `pages/contracts/`. Nếu có ⇒ dừng, giải-trình |
| A-11 | Build + typecheck sạch | `npm --prefix fe-admin run build` **và** `npm --prefix fe-user run build` → exit 0. 🔴 Chỉ tin build chạy **SAU** khi mọi agent nền dừng (gotcha #68) |
| A-12 | Test BE không vỡ | `dotnet test SolutionErp.slnx` → 0 failed |
| A-13 | Quyền không bị nới im-lặng | `grep -rn "Authorize" src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs \| grep -c "Policy"` — số này phải **≥ số route**, và **0 dòng** `[Authorize]` trần (gotcha #82) |
| A-14 | Nghiệm-thu THẬT trên **fe-user:8080** (R-1), không phải fe-admin | Chụp 3 màn khớp Ảnh 1/2/3; kiểm bằng **cửa-sổ ẩn-danh** (bài S96 #77: cache ≠ deploy) |

### Nhóm 5 — chờ owner, KHÔNG tự làm

| # | Việc | Vì sao phải hỏi |
|---|---|---|
| A-15 | Cho phép người duyệt **upload file khi duyệt** (D-7, thêm `ApprovalAttachment=3`) | Đổi enum + endpoint + quy-tắc lưu-trữ hồ-sơ. Ảnh 2 có khối này nhưng owner chưa nói KHKK cần upload |
| A-16 | Panel-2 chuyển sang `readOnly` cứng + dựng workspace KHKK riêng (P2-7) | Làm nửa vời = **cắt mất đường sửa duy nhất** của phiếu Nháp |
| A-17 | Overlay "Xem mở rộng" cho KHKK (P1-7) | Chi-phí cao (focus-trap + 2×rAF + khoá scroll), owner chưa nêu. Đề-xuất P2 |

---

## §8. Ba câu tôi muốn lead đọc kỹ nhất

1. **Không phải "sửa CSS", mà là "đưa GĐ2 về đúng lớp kiến-trúc mà GĐ1 và GĐ3 đang đứng".** GĐ2 là mắt-xích lệch duy nhất trong 4 giai-đoạn (§0 Đ-2). Nói với owner theo cách này thì phạm-vi công-việc tự giải-thích.
2. **Dữ-liệu lịch-sử duyệt của KHKK ĐÃ ĐƯỢC GHI TỪ ĐẦU** (`ContractSigningPlanWorkflowService.cs:534` + `:547`) — chỉ chưa có cửa đọc. Đừng ước-lượng việc này như "phải xây lại history"; nó là ~5 field DTO + 1 endpoint. Đây là chỗ dễ ước-lượng sai **đắt nhất** theo cả hai chiều.
3. **Chỗ dễ làm hỏng nhất khi "chép PE cho giống" là R-4** (ghi bộ lọc vào URL làm sidebar sáng nhầm — bug S155 đã đốt một lần) và **R-1** (nghiệm-thu nhầm app). Cả hai đều **không nhìn thấy được** trong ảnh owner gửi.

---

END investigator-codebase-invest-s171 — VERDICT=27 giống-được / 6 buộc-khác / 8 data-gap
