# sub-invest-fe-fable — YC-023 "Duyệt Hợp đồng" (GĐ3) — lăng kính FE 2-app

> Run `2026-08-10-S187-duyet-hd-tu-khkk` · engine `/fable-real` (H21 lệnh-A, single deep-pass, propose-only) · vai `investigator-codebase` · 2026-08-10.
> Đề bài YC-023: đề xuất CÁCH LÀM (không code) FE module "Duyệt Hợp đồng" — cấu trúc + menu mirror KHKK/PE, màn duyệt theo 7 LOẠI HĐ, phiếu kế thừa từ KHKK đã duyệt.
> Trạng thái file: ĐANG GHI TỪNG MỤC — mục nào xong ghi ngay. Nếu return bị #53, file này là chân lý.

## §0 — Ground truth đầu vào (đọc từ STATUS.md trước khi mở code)

- Mốc sống S185 (STATUS.md:6-7): KHKK 8-panel YC-013 SHIP + guard tay-cầm-phiếu YC-016; test 668.
- STATUS.md:23 (⑦ S159): bộ tên 4-GĐ chốt lời anh — ❶ Duyệt NCC · ❷ Kế hoạch ký kết HĐ · ❸ **Duyệt hợp đồng** (*"Trình ký Hợp đồng đổi thành Duyệt hợp đồng"*) · ❹ Hợp đồng cứng. Menu 6-leaf ×7 loại HĐ đã sinh @`ea962b1` (21 key mới `Ct_*_{WfView,Approved,Deleted}`) + endpoint `GET /api/contracts/deleted`. 🔴 ❹-risk CÓ Ý THỨC: menu HĐ public 13/13 role trong khi authz HĐ còn `[Authorize]` trần (lỗ hổng S156, anh chốt *"chỗ hợp đồng cứ từ từ"*).
- STATUS.md:11 (② S162): cây 4-folder GĐ `bfc7b79` — 3 file mới ×2 app (`PipelineStageFolders` · `PipelineTreePanel` · `usePipelineStages`), SHA-pair 6/6.
- STATUS.md:519 (S168): GĐ3-D `081557b` — cây toàn trình vào Panel-3 cả 3 trang 3-panel; `PipelineTreePanel` +prop `bodyMaxHClass`.
- STATUS.md:27 (③ S155-S158): 🔴🔴 lỗ hổng prod — `ContractWorkflowService.cs:48-66` nhánh Reject chạy TRƯỚC MỌI GUARD + `ContractsController.cs:13` `[Authorize]` trần duy nhất phủ 22 endpoint ghi ⇒ mọi user đăng nhập Từ-chối/Trả-lại được bất kỳ HĐ nào kể cả DaPhatHanh. Treo chủ-đích.

## §1 — Q1 (phần a): Menu key `Ct_*` ĐÃ ROUTE VÀO TRANG HIỆN CÓ, không có trang "Duyệt HĐ" riêng

**fe-user `Layout.tsx:143-154`** — regex `^Ct_([^_]+)_(List|Create|Pending|WfView|Approved|Deleted)$` map 6 leaf ×7 loại vào trang CŨ qua query param (0 trang mới):
- `List` → `/my-contracts?type=N` · `Create` → `/contracts/new?type=N` · `Pending` → `/inbox?type=N`
- `WfView` → `/contracts/workflow-matrix?type=3&ct=N` (comment :141-142: cả 7 loại đều `type=3` — HĐ chung 1 bộ workflow V2, `&ct=N` chỉ để active-state)
- `Approved` → `/my-contracts?type=N&phase=9` · `Deleted` → `/my-contracts?type=N&deleted=1`

**fe-admin `Layout.tsx:117`** — regex Ct_ CHỈ match `(List|Create|Pending)` (KHÔNG có WfView/Approved/Deleted như fe-user). NHƯNG vô nghĩa hiện tại vì:

🔴 **fe-admin ẨN TRỌN 4 root pipeline khỏi sidebar** — `ADMIN_HIDDEN_KEYS` (`fe-admin/src/components/Layout.tsx:227-240`): `PurchaseEvaluations` (GĐ1) · `KeHoachKyKet` (GĐ2) · `Contracts` (GĐ3) · `HopDongCung` (GĐ4) + Forms/Reports/Master/Hrm/Workflows/PeWorkflows. Lọc ĐỆ QUY (`filterForAdmin:251-255`) ⇒ ẩn root là cả cây con chết theo. Comment :224-226 cảnh báo: nếu ngày nào gỡ `KeHoachKyKet` khỏi tập ẩn thì PHẢI nới regex `Khkk_G*` trong resolvePath TRƯỚC. ⇒ **Nghiệp vụ pipeline 4-GĐ hiện là lãnh địa fe-user (eoffice); fe-admin là chỗ Designer quy trình** (`Khkk_WfView` admin trỏ `/system/approval-workflows-v2/ContractSigningPlan` — `fe-admin/Layout.tsx:40`; họ `AwV2_KhkkN[1-8]` → Designer `:188`). Trang khkk/contracts bên admin vẫn TỒN TẠI (route sống, vào bằng URL tay) nhưng không có menu dẫn.

**Khuôn `Hdc_*` GĐ4 (fe-user `Layout.tsx:70-76`):** 7 leaf tĩnh `Hdc_<Code>` → `/hard-copies?type=N` — 1 trang chung + query param, KHÔNG per-type page. ComingSoonPage: GĐ2/GĐ4 đã HẾT skeleton (comment :57 "[W2 KHKK S161] GĐ2 hết skeleton", :67 "[W7] GĐ4 hết skeleton — re-point trang thật"); root `HopDongCung` fallback `/dashboard` (:84).

## §2 — Q1 (phần b): 3 trang Panel-3 + màn duyệt HĐ hiện tại là gì

**3 trang GĐ3 hiện có (S168 `081557b` đưa cây vào Panel-3 cả 3):**
1. `fe-user/src/pages/contracts/MyContractsPage.tsx` (232 dòng) — 3-panel `[320px_1fr_360px]`: P1 list compact (search + PhaseBadge + SlaTimer) · P2 `ContractDetailContent` · P3 `WorkflowHistoryPanel` hoặc `PipelineTreePanel currentStage={3}` khi chưa chọn (`:225`). Đọc `?type=`/`?phase=9`/`?deleted=1` (`:30-36`) — filter type làm CLIENT-SIDE (`:61` filter sau fetch, BE chỉ nhận phase/deleted `:42-48`). Màn Đã-xóa = chỉ-xem, chặn select tại gốc (`:74-77`).
2. `fe-user/src/pages/InboxPage.tsx` (359 dòng) — trang "Đang duyệt" (Pending) — đọc sau.
3. `fe-admin/src/pages/contracts/ContractsListPage.tsx` (255 dòng) — bản admin của list 3-panel.

**Màn duyệt HĐ hiện tại = `ContractDetailContent` (257 dòng, mirror y hệt 2 app 257/257):**
- 🔴 **Model duyệt là 9-PHASE CŨ, không phải V2-trạm:** 2 nút "Yêu cầu sửa"/"Duyệt → tiếp" hiện khi `c.workflow.nextPhases` khác rỗng (`ContractDetailContent.tsx:105-116`), bấm mở Dialog **chọn targetPhase từ Select** (`:242-246`) rồi `POST /contracts/{id}/transitions` (`:47`). KHÔNG có bộ 3-nút PE/KHKK (Duyệt · Trả lại · Từ chối); "Từ chối" (TuChoi=99) không có nút riêng — Reject default về DangSoanThao (`:76-78`).
- KHÔNG có banner "Đến lượt bạn"/gating V2 theo Cấp (PE/KHKK có); nút hiện cho MỌI user thấy phiếu — khớp lỗ hổng S156 (BE `[Authorize]` trần + Reject trước guard).
- Section "Ý kiến cấp duyệt (Quy trình V2)" ĐÃ CÓ nhưng **READ-ONLY** (`:185-224`, Plan B S29 Chunk E3 mirror PE `LevelOpinionsSectionV2`): render `c.levelOpinions` khi `c.approvalWorkflowId` pin, kèm badge "⚡ Admin duyệt thay". Nghĩa là data-model V2 (pin + opinions) đã lên FE type, thiếu ĐƯỜNG BẤM ApproveV2.
- KHÔNG hiển thị bất kỳ vết KHKK nguồn nào (0 hit `signingPlan|khkk` trong component — xác nhận ở §4).

**Thiếu cho "Duyệt Hợp đồng" đúng nghĩa (đối chiếu chuẩn PE/KHKK):** (i) màn duyệt V2-trạm: banner lượt-ai + 3 nút Duyệt/Trả-lại/Từ-chối gated theo `currentApproval` + flow-panel Bước→Cấp; (ii) list "chờ tôi duyệt" per-loại thật sự (InboxPage đọc ở §3); (iii) hiển thị nguồn gốc KHKK; (iv) BE per-action authz (ngoài scope FE nhưng là tiền đề — S156 treo chủ-đích).

## §3 — Q1 (phần c): InboxPage + nguồn dữ liệu "chờ tôi duyệt"

`fe-user/src/pages/InboxPage.tsx` (359 dòng): 3-panel + stats 4 pill; 2 SECTION trong P1 — "Hợp đồng" từ `GET /contracts/inbox` (`:70`) + "Phiếu Duyệt NCC" từ `GET /purchase-evaluations/inbox` (`:78`, PE row click → navigate `/purchase-evaluations/:id` thay vì inline — comment `:73-75` "đỡ duplicate ContractDetailContent cho PE entity khác shape"). Filter `?type=N` CLIENT-SIDE (`:91`). Panel-3: **S174 owner đã GỠ `PipelineTreePanel` khỏi Hộp thư** (comment `:341-348` — *"chỗ hộp thư vứt panel này vào làm j"*; 2 trang GĐ3/GĐ4 còn lại GIỮ — đừng suy rộng).

🔴 **Tiền đề cross-lens phải kiểm với lane BE (từ diary S156, cần re-verify):** BE `/contracts/inbox` + `GetEligiblePhases` từng đo là **100% V1-LEGACY** ⇒ HĐ pin V2 đang ChoDuyet KHÔNG hiện trong inbox của approver V2. Nếu còn đúng thì mọi thiết kế FE "Đang duyệt per-loại" đứng trên inbox này sẽ RỖNG với phiếu V2 — phải có wave BE inbox-V2 trước wave FE Pending. (FE lens: `InboxPage` chỉ là cái loa của endpoint đó.)

## §4 — Q4: Kế thừa hiển thị KHKK→HĐ — chiều xuôi ĐÃ CÓ, chiều ngược 0 dữ liệu

**Chiều xuôi (KHKK → HĐ) — K7 đã render đủ trong `KhkkDetailPage.tsx` (377 dòng, header khai MIRROR SHA256 identical 2 app `:3`):**
- Nút "Đưa vào HĐ" chỉ hiện khi `phase === DaDuyet ∧ can('KeHoachKyKet','Read')` (`:110-112`), disabled kèm title NÓI RA vế thiếu (`:113-122` — khuôn nút-khoá-kèm-title PE); hội 2 khóa quyền `KeHoachKyKet.Read ∧ Contracts.Create` (`:54-61`, đúng gotcha #85).
- Dialog `CreateContractFromKhkkDialog` (`:196-377`): radio chọn 1 DÒNG (máy BE nhận `lineIds[]` gộp N — UI cố ý 1-1 theo luật "máy rộng UI hẹp" OG-1 `:184-186`) · Select **Loại HĐ 7 loại** (`:337-341`) · picker workflow V2 `applicableType=3` lọc ĐỦ 2 cờ `isUserSelectable ∧ isActive` (`:224-227`) · tên HĐ default `{peTenGoiThau} — {tenHangMuc}` (`:209-211`) · POST `/contract-signing-plans/{id}/create-contract` → toast mã HĐ + navigate `/contracts/{contractId}` (`:242-252`) + invalidate 2 index cây (`pipeline-khkk-index`/`pipeline-contract-index` `:246-249`, staleTime 60s).
- Dòng đã bắc cầu render link **"Đã có HĐ" → `/contracts/{l.contractId}`** (`:321-325`); guard dòng-bắc-cầu-được = `contractId == null ∧ approvedAmount != null` (`:111`, `:200`).

**Chiều ngược (HĐ → KHKK nguồn) — HIỆN = 0:**
- FE `fe-user/src/types/contracts.ts`: 0 hit `signingPlan|khkk|maKeHoach` (grep). `ContractDetailContent` không render gì về nguồn.
- Domain: link nằm Ở PHÍA LINE — `ContractSigningPlanLine.ContractId` (`src/Backend/SolutionErp.Domain/ContractSigningPlans/ContractSigningPlanLine.cs:46`, comment `:7` "giữ đường quay ngược Kế-hoạch → HĐ (`ContractId`, twin `PurchaseEvaluation.ContractId`)"); `Contract.cs` 0 hit `SigningPlan` ⇒ **HĐ không mang con trỏ ngược**. Muốn màn duyệt HĐ show "nguồn: KHKK/2026/xxx · NCC · giá chốt · hạng mục" thì BE phải JOIN ngược qua Lines (extend `ContractDetailDto` — reverse-lookup `Line WHERE ContractId = id` → plan.maKeHoach + line.tenHangMuc + supplierName + approvedAmount + approvalGroup) hoặc endpoint phụ. **FE không tự chế được từ data hiện có** (trừ phi bắn thêm query list KHKK rồi lọc client — anti-pattern N+1, không đề xuất).
- Đề xuất hiển thị (FE, sau khi BE trả field): card "Nguồn gốc" trong `ContractDetailContent` ngay dưới "Thông tin HĐ" — mã KHKK (link `/khkk/:planId?group=n`), badge N{group}, hạng mục, NCC dòng, giá duyệt chốt (đối chiếu `giaTri` HĐ), + chuỗi sâu hơn "PE gốc" nếu plan có `purchaseEvaluationId` (cây pipeline đã nối GĐ1→2 qua field này). Đối xứng đủ 2 chiều: KHKK line có "Đã có HĐ" (đã ship) ⟷ HĐ có "Từ KHKK" (cần làm).

## §2-bis — Khuôn màn duyệt KHKK (bổ sung cho Q2, đo trước khi return lượt 1 bị #53)

`fe-user/src/components/khkk/KhkkWorkflowPanel.tsx` (762 dòng; bộ 4 component khkk/ = 2.687 dòng/app) — đây là khuôn màn-duyệt V2 MỚI NHẤT đã deploy:
- Cấu trúc tự khai (`:10-14`): ① stepper flow Bước→Cấp (Done/Current/Pending `:136-148`) · ② banner kết-thúc-sớm · ④ nút hành động + Dialog + Xóa phiếu.
- Gating: `actorIsCurrentApprover = user.id ∈ currentLevels.approverUserId` (`:161`), `actorInLevel = isAdmin || ...` (`:162`); banner "✓ Đến lượt bạn duyệt" (`:422`, comment `:409` khai khuôn gốc `PeWorkflowPanel:489-508`); per-level `allowApproverFinalize` (`:168-170`, `applyLevelFinalize` gửi kèm Approve `:198-199`).
- 🔴 **Owner đã THU HẸP bộ nút @S174/S175:** nút "TỪ CHỐI" GỠ khỏi UI (`:482-485` — *"vậy thì bỏ luôn từ chối"*; chỉ gỡ ĐƯỜNG BẤM, máy BE `Reject` còn) + panel chỉ còn **Duyệt / Trả lại** (`:439-441` — *"Bỏ hết đi… chỗ menu đang duyệt → Duyệt/Trả lại vậy thôi"*). KHKK giữ **1 return-mode** (về Bước 1 · Cấp 1; 4-mode PE là món riêng còn treo — `:29-30`).
- Badge lịch sử 3-NGẢ emerald/amber/rose (`:550-574` — bài học 2-ngả vẽ "Trả lại" xanh như đã-duyệt).
⇒ Bài Q2: khuôn màn duyệt cho HĐ nên mirror **KhkkWorkflowPanel** (mới nhất, đã qua 8 đợt UAT S179 + owner-trim), KHÔNG mirror PeWorkflowPanel nguyên bản (PE có món riêng: NS PRO/CCM, 4 return-mode, giá chốt).

## §5 — Q3: per-LOẠI-HĐ trên UI — khuôn ĐÃ DEPLOY là "1 trang + query-param DANH TÍNH", không phải KIND_CONFIG `:kind`

**Chọn khuôn: 1 trang + `?type=N`.** Căn cứ "ĐÃ DEPLOY = CHUẨN":
- Cả 3 họ pipeline per-type/per-group hiện hành đều đi đường query-param: `Ct_*` → `?type=N` (fe-user `Layout.tsx:143-154`) · `Hdc_*` → `/hard-copies?type=N` (`:70-76`) · `Khkk_G{n}_*` → `?group=n` (`:213-222`). KIND_CONFIG `:kind` URL-segment chỉ dùng cho CRUD catalog (HRM configs `:114-116` "cùng page :kind-driven") — khác thể loại (catalog tĩnh vs phiếu workflow).
- Luật danh-tính đã đúc 2 lần, PHẢI theo: (i) `view=approved` = KHOÁ ĐỊNH DANH không phải bộ lọc (`Layout.tsx:180-184`, bug menu-sáng-nhầm UAT 2026-07-27); (ii) `?group=n` KHÔNG nằm `TRANSIENT_QUERY_KEYS` (`:201-203`). ⇒ nếu thêm param mới cho trang duyệt HĐ (vd `?view=...`), phải kiểm nó có vào TRANSIENT hay không, sai một chữ = leaf trỏ trang mở được nhưng lọc SAI, hỏng câm (`:204-207`).
- 🟡 Lỗ nhỏ đáng vá cùng wave: `MyContractsPage.tsx:42-48` chỉ gửi `type` lên BE khi `showDeleted`; list thường filter type CLIENT-SIDE sau fetch `pageSize:100` — sai lệch khi tổng HĐ >100 (BE `/contracts` CÓ param type — skill contract-workflow bảng endpoint). InboxPage cũng client-side (`:91`).

**Key/policy: TÁI DÙNG TRỌN, KHÔNG sinh bộ mới.**
- Menu identity: 42 leaf `Ct_<Code>_{List,Create,Pending,WfView,Approved,Deleted}` ×7 loại ĐÃ seed đủ (S159 `ea962b1` +21 key mới; menu 6-leaf "y-chang-Duyệt-NCC"). Màn duyệt HĐ không cần key mới — khác KHKK: `Khkk_G*` phải sinh vì 8 NHÓM DUYỆT là chiều mới không có sẵn; 7 LOẠI HĐ đã là chiều có key từ đầu.
- 🔴 Gotcha #85 áp cụ thể: gate PermissionGuard/`can()` trong trang duyệt HĐ phải dùng **key policy của ENDPOINT = `Contracts.{Read,Create,Update,Delete}`** (root, inherit-family `Ct_*` per gotcha #35 — nhưng `MenuPermissionHandler.cs:40` so CHÍNH XÁC 1 key), KHÔNG OR các `Ct_*` leaf. Mẫu đúng đã ship: hội-2-khóa K7 (`KhkkDetailPage.tsx:54-61` — `KeHoachKyKet.Read` ∧ `Contracts.Create`).
- Tiền đề BE treo: `ContractsController` hiện `[Authorize]` trần (S156) ⇒ FE `can('Contracts', ...)` hôm nay là gate MỘT TẦNG (display-only). Khi BE mở per-action policy thì FE KHÔNG đổi key — đó là lý do chọn `Contracts.*` ngay từ giờ.
- Câu nghiệp vụ (CHỜ-ANH): có cần phân quyền per-LOẠI-HĐ ở tầng API không (vd người chỉ duyệt Thầu phụ)? Nếu có thì mới cần nâng `Ct_*` leaf thành policy tiêu thụ được ở controller — hiện 0 endpoint tiêu thụ key `Ct_*`.

## §6 — Q5: SHA-mirror 2 app — mirror được GẦN HẾT, nhưng phải trả lời "mirror sang admin để làm gì"

**Hiện trạng đo được:** bộ contracts/ 4 component = same-size 2 app (156/257/683/151 — mirror); `KhkkDetailPage` tự khai "MIRROR SHA256 identical" (`:3`); S162 SHA-pair 6/6 pipeline; S157 4-file SHA-mirror. Chuẩn đội = **component/page ruột mirror byte-identical; Layout/App.tsx/menuKeys CỐ Ý LỆCH có khai chỗ** (fe-admin `Layout.tsx:37-39` Khkk_WfView lệch chủ-đích; `:209-212` fe-user "fe-admin CỐ Ý KHÔNG nới regex").

**Phân loại cho module Duyệt HĐ:**
- Mirror identical được: card "Nguồn gốc KHKK" (component mới) · panel duyệt V2 (thay/nâng `ContractDetailContent` + `WorkflowHistoryPanel`) · types/contracts.ts delta · ContractDetailPage wrapper. Đây là vùng "sửa 1 chỗ 2 màn đổi theo" (triết lý S171 `:5-9` KhkkDetailPage).
- Buộc khác: `InboxPage` (fe-user, nguồn `/contracts/inbox`) ⟂ `ContractsListPage` (fe-admin, nguồn `/contracts`) — 2 trang khác vai đã tồn tại, không ép mirror; `Layout.tsx` (admin thiếu regex WfView/Approved/Deleted `:117` + ADMIN_HIDDEN_KEYS); `App.tsx` route set.
- 🔴 **Nghĩa của mirror-sang-admin phải chốt trước wave:** fe-admin đang ẨN cả root `Contracts` (`ADMIN_HIDDEN_KEYS:230`) ⇒ trang duyệt HĐ mirror sang admin sẽ SỐNG-KHÔNG-MENU (như khkk admin hiện tại — vào bằng URL tay). Tiền lệ chấp nhận được (KHKK ship y vậy), chi phí mirror ~0 vì component chung; nhưng nếu owner muốn admin THẤY menu GĐ3 → phải gỡ `Contracts` khỏi ADMIN_HIDDEN_KEYS + nới regex admin + quyết `Ct_*_WfView` admin trỏ đâu (Designer `/system/workflows/<type>` là ứng viên — mirror mẫu Khkk_WfView admin `:40`). CHỜ-ANH.

**Acceptance đo được (khuôn đã dùng S162/S168):** (1) SHA-pair: `git hash-object fe-admin/src/<f> fe-user/src/<f>` từng cặp bằng nhau — liệt danh sách cặp trong PR, đếm N/N; (2) build ×2: `npm run build` fe-admin + fe-user EXIT 0 (trọng tài chạy SAU khi agent dừng tay — bài #68); (3) key-count: menu-key/policy đối chiếu `docs/STATUS.md` canonical + test T2 khoá drift (nếu wave không thêm key ⇒ số GIỮ NGUYÊN là acceptance); (4) sau deploy: curl bundle prod grep marker mới ×2 app (#77 byte-verify).

## §7 — Q6: Wave FE đề xuất (khớp nhịp BE — phiếu duyệt cần endpoint trước)

Tiền đề đo được lái thứ tự: FE `types/contracts.ts` có `approvalWorkflowId`/`currentApprovalLevelOrder`/`levelOpinions`/`nextPhases` (`:138,:172-176`) nhưng **KHÔNG có `currentApproval` + `approvalFlow`** — PE có 2 DTO đó (`PurchaseEvaluationCurrentApprovalDto`/`ApprovalFlowDto`) để render banner + stepper. ⇒ màn duyệt V2-trạm cho HĐ **CHẶN bởi BE DTO**, không phải thiếu endpoint duyệt (ApproveV2Async HĐ đã wire từ S29).

| Wave | Deliverable | Chặn bởi BE? | Acceptance đo được | Rủi ro |
|---|---|---|---|---|
| **F1 Nguồn-gốc** | Card "Từ KHKK" trong `ContractDetailContent` (mã KHKK link `/khkk/:planId?group=n` + badge N{n} + hạng mục + NCC + giá chốt) — đối xứng chiều ngược của "Đã có HĐ" | 🔴 CÓ — BE extend `ContractDetailDto` reverse-join `Line WHERE ContractId=id` (§4) | HĐ dry-run K8 (`ZZTEST-K8/...`) hiện card đúng mã `KHKK/2026/00x`; HĐ không-nguồn KHÔNG hiện; SHA-pair; build ×2 | DTO null-path (HĐ cũ 0 nguồn); N-dòng-1-HĐ tương lai (máy `lineIds[]` rộng hơn UI) |
| **F2 Màn duyệt V2-trạm** | Nâng `ContractDetailContent`/panel: banner "Đến lượt bạn" + gating nút theo `currentApproval` + stepper Bước→Cấp — mirror `KhkkWorkflowPanel` (khuôn mới nhất §2-bis), GIỮ nhánh V1 cũ cho HĐ không pin (dual-render đã có sẵn `c.approvalWorkflowId &&`) | 🔴 CÓ — BE thêm `currentApproval`+`approvalFlow` vào ContractDetailDto (cookie-cutter DTO PE S17) | Phiếu V2 ChoDuyet: user ngoài Cấp thấy nút khoá + tooltip; user trong Cấp duyệt được → levelOpinion mới render; HĐ V1 giữ 2-nút cũ (regression); build ×2 + SHA-pair | Bộ nút = câu nghiệp vụ (2-nút KHKK vs 3-nút PE — CHỜ-ANH); đừng đụng tập Order workflow (F17 S155) |
| **F3 Pending-per-loại thật** | InboxPage: HĐ V2 ChoDuyet hiện đúng trong "Đang duyệt" per-loại | 🔴 CÓ — BE inbox V2-aware (mirror `ResolveV2InboxIdsAsync` PE; tiền đề S156 "inbox 100% LEGACY" cần lane BE re-verify) | Tạo HĐ V2 từ KHKK → xuất hiện ở `/inbox?type=N` của approver Cấp 1; user khác 0 thấy | Nếu BE gộp vào `/contracts/inbox` sẵn có thì FE 0-đổi (chỉ verify) — đo trước khi code |
| **F4 Per-type server-filter** | `MyContractsPage`/`InboxPage` gửi `type` lên BE thay client-filter | KHÔNG (param có sẵn) | Network tab thấy `?type=N`; list đúng khi seed >100 HĐ | Đổi queryKey → invalidate cache cũ |
| **F5 Menu/label polish + admin-visibility** | Nếu anh chốt admin thấy GĐ3: gỡ `Contracts` khỏi ADMIN_HIDDEN_KEYS + nới regex `:117` + WfView→Designer | KHÔNG | Sidebar admin hiện 7 nhóm × 6 leaf; 0 leaf drop câm (đếm leaf render vs seed) | gotcha #50 route-thiếu = drop IM LẶNG; #84 code-first seeder không SQL tay |

Thứ tự đề xuất: **F1 → F2 → F3** (mỗi wave FE đi SAU wave BE tương ứng 1 nhịp, khuôn {đợt nhỏ → ship → anh nhìn prod → vẽ tiếp} S159 ⑦); F4/F5 chèn xen kẽ lúc chờ BE. Mọi wave: KHÔNG mig, KHÔNG đổi menu-key ⇒ STATUS counters giữ nguyên = acceptance rẻ.

## CHỜ-ANH (câu nghiệp vụ — không đoán)

1. **Bộ nút màn duyệt HĐ:** theo KHKK sau S174/S175 (chỉ **Duyệt / Trả lại**, bỏ Từ chối) hay theo PE (3 nút)? Owner đã trim KHKK bằng lời trực tiếp — áp luôn cho HĐ hay HĐ là chuyện khác?
2. **Mở wave BE per-action authz `ContractsController` bây giờ?** S156 anh chốt "chỗ hợp đồng cứ từ từ" — nhưng YC-023 chính là module hợp đồng; làm màn duyệt mà giữ `[Authorize]` trần thì gating FE chỉ là trang trí (mọi user vẫn Reject được qua API).
3. **fe-admin có cần THẤY menu GĐ3 không** hay giữ posture ẩn 4 root (mirror sống-không-menu như KHKK admin)?
4. **Phân quyền per-LOẠI-HĐ tầng API** có cần không (người chỉ duyệt Thầu phụ)? Nếu không, giữ `Contracts.*` root policy là đủ và rẻ.
5. **Menu public 13/13 role** (❹-risk S159 anh tái khẳng định "hiển thị hết để góp ý") — giữ nguyên cho GĐ3 trong lúc build?

== HẾT ==
