# SPEC v2 — SOLTUON-12-08-2026 (YC-029: 5 điều chỉnh GĐ3 đồng-khuôn KHKK/Duyệt NCC)

> H21 spec-file 3-mục (+④ nấc đo local). Engine propose = `sub-invest-b1-fable.md` (Fable-5, 3 chặng).
> **v2 @S190 sau B2 ensemble 3-lane reviewer** (lens-1 FAIL-11 · lens-2 PASS_WITH_FIXES-12 · lens-3
> FAIL-11 — artifact `sub-review-b2-lens{1,2,3}.md`): 2 BLOCKER tiền-đề v1 SAI đã gỡ (❶ "bỏ type=3,
> trang tự suy theo path" — trang CHỈ đọc `?type`, bỏ là rơi về Duyệt-NCC/trang-trắng; ❷ "BE param
> pendingMe sẵn" — 0-hit Contract, param lạ bị NUỐT im lặng) + W5 viết lại theo khuôn ĐÚNG (hành-vi =
> KHKK theo lời anh; bằng-chứng "KHKK có nút Xóa ở panel duyệt" v1 trỏ vào CODE CHẾT S175 — nút thật
> ở thanh đáy detail, hướng người-soạn) + Q1-Q4 tách rõ. Root-cause bug A+B: 3/3 lane xác nhận HELD.

## ① Tính-năng / đề-bài

Đồng-khuôn GĐ3 "Duyệt Hợp đồng" với KHKK + Duyệt NCC prod (owner S176: đã-deploy = CHUẨN), theo 5
điều chỉnh YC-029 (ảnh kết tinh `anh-mo-ta.md`):
- **Đ1** cây toàn trình về cột TRÁI trang `my-contracts` (chi tiết = W2, nhánh-đã-chọn Q1: HOÁN ĐỔI).
- **Đ2+Đ3** menu 7 loại HĐ đúng danh-tính ContractType — diệt bug A (accordion nhảy nhóm) + bug B
  (2 leaf cùng sáng) + leaf "Luồng duyệt" slice đúng họ `HD-*`. Máy per-loại ĐÃ XONG @S188 — đây là UI.
- **Đ4** bộ thao-tác theo hành-vi KHKK: Đang-duyệt ở lại trang list · Đã-duyệt danh-tính `view=approved`
  · đường XOÁ theo đúng khuôn KHKK (Nháp/Trả-lại/Từ-chối, người-soạn/Admin — KHÔNG mở approver-delete
  nếu anh chưa duyệt Q4).
- **Đ5** admin Designer hiện 7 leaf per-loại đã seed sẵn (thiếu 1 nhánh resolvePath — ~5 dòng).

Tổng: **W4/W1/W2/W3 FE-only · W5 BE nhỏ + FE (0 migration)**. DEPLOY CHỜ (CI runner P0 — cửa ngoài-giờ);
đo hành-vi bằng **④ nấc đo local**, ô không đo được = DEFERRED-TO-DEPLOY, cấm tick.

## ② Cách implement (per wave — đã vá theo B2)

- **W4 — Admin Designer 7 leaf (fe-admin, ≈5 dòng — wave LÀNH NHẤT, chạy ĐẦU):**
  `fe-admin/src/components/Layout.tsx` — import `findContractWfByTypeCode` từ `@/lib/contractWorkflowFamily`
  + thêm **TRONG khối `if (awV2Match)`**, sau nhánh `KhkkN` `:188`:
  `if (findContractWfByTypeCode(code)) return '/system/approval-workflows-v2/' + code`.
  (H-2/H-3 lens-3 HELD: `code` = phần sau `AwV2_` = `HdTP`… khớp `contractWorkflowFamily.ts:69,77-80`;
  route `App.tsx:80` + Designer slice sẵn. Nhánh `code === 'Contract'` `:178` = nhánh chết, 0 seed —
  KHÔNG đụng.) KHÔNG sửa DbInitializer.
- **W1 — Danh-tính menu + slice per-loại (fe-user):** 🔴 **GIỮ `?type=3&ct=N`** (khuôn KHKK
  `Layout.tsx:216` giữ type= trên URL — v1 đòi bỏ là TỰ CHẾ, gây trang trắng).
  (a) Accordion `fe-user/Layout.tsx:470-485`: **ưu tiên đọc `ct` khi có** (map `INT_TO_TYPE_CODE[ct]`)
  + **allowlist pathname** (chỉ auto-expand nhóm Ct_ từ `?type=` khi pathname ∈ {`/my-contracts`,
  `/contracts/new`, `/inbox`} — diệt luôn cross-talk `Hdc_*` `/hard-copies?type=1..7` mà blacklist v1 sót);
  (b) `WorkflowMatrixViewPage.tsx`: thêm nhánh đọc `ct` (clamp `/^[1-7]$/` — khuôn `group` `:82-86`,
  `ct` rác ⇒ KHÔNG lọc, giữ hành vi cũ) → lọc `code === 'HD-<token>'` y khuôn nhánh `group` `:84-98`;
  map ContractType→mã = **const cục-bộ 7-entry trong fe-user** (⚠️ bản-sao-thứ-3 của họ — F-7 lens-3:
  chép ĐÚNG từ `ContractApprovalWorkflowFamily.cs:33-42`, cẩn thận token gạch-nối `HD-NT-NCC`/`HD-NT-DV`;
  acceptance vòng 7 mã khoá drift);
  (c) `:152` Approved thêm `&view=approved` (khuôn PE `:180-185` — H-4 HELD; trang bỏ qua param lạ);
  (d) sửa comment stale `:141-142` ("chung 1 bộ" → 7 bộ per-loại @S188).
- **W2 — Cây toàn trình sang trái = HOÁN ĐỔI 2 khung (fe-user; cặp hook/panel mirror ×2 app):**
  Nhánh-đã-chọn **Q1 = HOÁN ĐỔI** (khai bậc-2, anh bác thì đổi): panel-1 TRÁI 400px = CÂY
  (`PipelineTreePanel currentStage={3} hideHeader hideSearch`) · panel-2 GIỮA = detail/placeholder
  (như cũ) · panel-3 PHẢI 360px = **DANH-SÁCH HĐ** (search + empty-state — GIỮ nguyên chức năng, chỉ
  đổi chỗ; KHÔNG xoá khối UI anh không bảo xoá; HĐ tạo-trực-tiếp-ngoài-gói vẫn luôn thấy được ở đây —
  vá F-3 lens-2: cây CHỈ chứa HĐ sinh từ gói).
  Nhánh-đã-chọn **Q2 = Select loại trên đầu panel cây** (khuôn KHKK Select nhóm `KhkkListPanel.tsx:19-20`):
  default = `?type=N` của trang; lọc leaf GĐ3 trên cây theo loại đã chọn.
  Kỹ thuật: `usePipelineStages.ts:377` thêm opt-in `opts.onOpenContract` (FLAT trong opts — khuôn
  `opts.onOpenKhkk :244`/`opts.onOpenPe :228`; KHÔNG bịa tầng `stage3Options` — F-4 lens-1) + panel
  nhận qua `Pick<>` `PipelineTreePanel.tsx:97,111`; fe-admin KHÔNG truyền ⇒ **mặc định TẮT = navigate
  như cũ** (control-âm bắt buộc). Search server-side: nối ô tìm → param `search` của `/contracts`
  (sẵn `ContractsController.cs:19` — F-10 lens-2, nói thẳng: ĐỔI từ client-side).
- **W3 — Đang-duyệt ở lại trang (fe-user, FE-only — khuôn PE THẬT là đổi-endpoint):**
  `Ct_*_Pending` → `/my-contracts?type=N&pendingMe=1`; `MyContractsPage` khi `pendingMe=1` ⇒ **gọi
  `/contracts/inbox`** (0-param, khuôn `PurchaseEvaluationsListPage.tsx:91-97` đổi-endpoint) + lọc
  `type` client-side (khai lệch-W7 CHẤP NHẬN: inbox không phân trang, payload nhỏ — đúng hiện trạng
  `InboxPage.tsx:103`; nâng BE = `[carry:w7b-inbox-param]`, KHÔNG gộp đợt này). GIỮ `/inbox` hộp thư tổng.
- **W5 — Đường XOÁ đồng-khuôn KHKK (BE nhỏ + FE, 0-mig, TEST-BEFORE):** *(viết lại toàn bộ theo
  F-7/F-12 lens-2 + F-3/F-4/F-5 lens-3)*
  **Khuôn HÀNH-VI = KHKK** (`ContractSigningPlanFeatures.cs:1763-1783`, lời anh "tương tự như KHKK"):
  xoá được ở **{DangSoanThao(+phase nháp <DangInKy như cũ), TraLai(98), TuChoi(99)}**, bởi **Drafter
  hoặc Admin**; **ChoDuyet(10) VẪN CẤM** (approver-delete = Q4 chờ anh — KHÔNG làm đợt này).
  Việc BE tại `Application/Contracts/ContractFeatures.cs:889-901` (DeleteContractCommand):
  (a) thêm check **Drafter/Admin** (hiện KHÔNG kiểm chủ sở hữu — lỗ sẵn có, sửa luôn và KHAI là sửa-lỗ-cũ);
  (b) guard phase: allow `< DangInKy` ∪ `{TraLai, TuChoi}`; **chặn tường minh `DaPhatHanh(9)`** và
  dải còn lại (F-5 lens-3 — HĐ đã gen mã KHÔNG BAO GIỜ xoá được);
  (c) ghi **`ContractChangelog`** (Action=Delete + PhaseAtChange + Reason) — 🔴 thứ tự **`Remove()`
  TRƯỚC, `Add(changelog)` SAU** (bẫy T26: đảo là cascade DETACH ⇒ 0 row im lặng);
  (d) 🔄 **v2.1 theo BS-3 (be-style B3):** KHÔNG gắn `[Authorize(Policy="Contracts.Delete")]` đợt
  này — policy-403 chạy TRƯỚC owner-check, tiền lệ PE S155 đo 11/13 role `CanDelete=0` ⇒ gắn =
  khoá oan Drafter mất nút xoá nháp đang có, và đi NGƯỢC chốt (82) của anh ("phân quyền tất cả
  trước, phân quyền sau" — W8 HOÃN). Chốt an toàn = **owner-check trong handler** (mục (a) — đủ
  diệt lỗ "user thường xoá HĐ người khác" F-4 lens-3, trả 403 qua `ForbiddenException`); policy
  per-action DELETE ghi vào danh mục W8 chờ anh "ổn rồi". Nấc-0 null-guard `DrafterUserId` là
  `Guid?` — khuôn `PeSoftDeleteFeatures.cs:56-58` (BS-6);
  (e) xác nhận sẵn: `Remove` = SOFT-delete qua `AuditingInterceptor.cs:54-62` (H-6 — wave chỉ assert,
  đừng đo lại).
  **STYLE-GATE cho B6 (🔄 v2.2 sau B4):** nguồn = **`style-gate-v2.md`** (delta THẮNG nền; nền =
  `sub-style-{fe,be}-b3.md`). Checklist ô đo CHỈ dùng bảng trong style-gate-v2 (bảng B3 gốc bị B4
  chứng có false-green/dương-giả bằng fault-inject). Ca test W5 = **T-matrix T-1..T-13 + phản-ca
  T-14** trong style-gate-v2 §C (thay danh sách ca ở ✅W5(i) cũ — ca "V1 nháp 2" đã bị chứng TRÙNG).
  FE: nút "Xóa phiếu" ở **thanh nút đáy `ContractDetailContent`** (port khuôn
  `KhkkDetailContent.tsx:1077-1096` — KHÔNG lấy `KhkkWorkflowPanel` làm khuôn, phần xoá trong đó là
  CODE CHẾT S175), hiện khi phase ∈ allow-list + user là Drafter/Admin; mirror ×2 app SHA-pair.

## ③ WAVE + CHECKLIST acceptance (đã siết theo B2)

Thứ tự: **W4 → W1 → W2 → W3 → W5** (W4 lành nhất đi đầu; W3 sau W2 vì "Đang duyệt" hiển thị ở panel
nào phụ thuộc layout mới; W5 cuối — BE + test-before).

- **W4:** ✅(i) sidebar admin "Quy trình duyệt (Mới)" đủ **17 leaf** (2 NCC + 8 KHKK + 7 "QT Duyệt
  HĐ — …" Order 11..17 — H-5); ✅(ii) click "QT Duyệt HĐ — Thầu phụ" → panel khoá mã `HD-TP`, history
  CHỈ HD-TP; ✅(iii) control-âm: leaf `KhkkN1` + `DuyetNcc` vẫn mở đúng trang cũ; ✅(iv) build fe-admin PASS.
- **W1:** ✅(i) tái-hiện A (đo local ④): mở nhóm 01 → click "Luồng duyệt" → accordion GIỮ nhóm 01;
  control-âm `ct=5` → mở nhóm 05; ✅(ii) tái-hiện B: `?type=1&phase=9&view=approved` → CHỈ leaf "Đã
  duyệt" sáng; ✅(iii) **vòng `ct ∈ 1..7`**: trang WfView hiện **≥1** quy trình đúng họ (Dev seed đủ
  7 `HD-*` idempotent) + **0** dòng khác họ + riêng `ct=1` phải VẮNG `QT-HD-V2-001` (tồn tại trên Dev
  ⇒ ô "vắng-vì-lọc" có nghĩa, không phải "vắng-vì-không-có"); control `ct=99` ⇒ KHÔNG lọc (giữ hành
  vi cũ); ✅(iv) `grep -rnF 'workflow-matrix?type=3&ct=' fe-user/src` **≥1 GIỮ NGUYÊN** (v2 giữ type=3;
  ô 0-hit của v1 đã bỏ) + accordion mới có nhánh `ct`: `grep -nF "get('ct')" fe-user/src/components/Layout.tsx`
  ≥1; ✅(v) build fe-user PASS.
- **W2:** ✅(i) `grep -F "grid-cols-[400px" fe-user/src/pages/contracts/MyContractsPage.tsx` ≥1;
  ✅(ii) click leaf HĐ trên cây → panel-2 mở detail, URL Ở LẠI `/my-contracts` (đo local ④);
  ✅(iii) panel-3 = danh-sách sống: search gõ → gọi `/contracts?search=` (Network tab, server-side);
  ✅(iv) cặp `usePipelineStages.ts`/`PipelineTreePanel.tsx` 2 app SHA256 khớp từng cặp (baseline
  `1b9b5c61…`/`4ffc96be…` — H-1); ✅(v) **control-âm fe-admin**: không truyền `onOpenContract` ⇒ click
  leaf GĐ3 vẫn `navigate('/contracts/{id}')` như cũ; leaf GĐ1/GĐ2/GĐ4 hành-vi không đổi ở CẢ 2 app;
  ✅(vi) **deep-link cũ** `/contracts/{id}` dán thẳng vẫn mở detail (không 404); ✅(vii) **HĐ tạo
  trực-tiếp (ngoài gói)** hiện ở panel-3 danh-sách (ca F-3 lens-2); ✅(viii) build 2 app PASS.
- **W3:** ✅(i) hành-vi so-số 2 chiều (đo local ④, user có ≥1 HĐ đến-lượt + ≥1 không-đến-lượt):
  bật `pendingMe=1` ⇒ list **ÍT HƠN** list thường + HĐ không-đến-lượt **VẮNG**; control-dương: user
  ngoài luồng ⇒ **0 dòng** (không phải cả list); ✅(ii) leaf "Đang duyệt" ở lại `/my-contracts`;
  ✅(iii) build fe-user PASS.
- **W5:** ✅(i) 🔄 v2.3 (B5 X-1/X-2): `dotnet test SolutionErp.slnx` PASS **680 → 680+N** — ca test =
  **T-matrix T-1..T-13 + phản-ca T-14 trong `style-gate-v2.md §C` (NGUỒN DUY NHẤT — danh-sách ca
  cũ của v2 đã RETIRED tại đây, đừng đọc bản git-history)**; STATUS lật **2 ô**: dòng session
  DELTA `680→680+N` tại commit + row canonical `:476` = DEFERRED-TO-DEPLOY (chỉ chép từ
  cicd-verify — deploy đang CHỜ); ✅(ii) changelog: xoá xong đọc-ngược DB ra **đúng 1
  dòng** Action=Delete (0 dòng = dẫm T26); ✅(iii) 🔄 v2.1: owner-check trong handler — test
  stranger-403 (khuôn `ContractSigningPlanCrudTests` stranger-case) + KHÔNG thêm
  `[Authorize(Policy` mới vào DELETE (`grep` DELETE-action giữ nguyên shape — policy = W8);
  ✅(iv) nút Xóa: render đúng điều kiện (phase+vai), `ContractDetailContent` mirror
  SHA ×2 app **+ thêm dòng tự-khai MIRROR ở header cùng bytes 2 bên (SF-17 — file identical
  nhưng chưa khai)**; ✅(v) build 2 app PASS.

## ④ Nấc đo local (BẮT BUỘC — deploy CHỜ, ô hành-vi KHÔNG tick bằng suy luận)

1. BE: `dotnet run --project src/Backend/SolutionErp.Api` (LocalDB `SolutionErp_Dev`; seeder idempotent
   tự có 7 `HD-*` + `QT-HD-V2-001` sample — tiền-đề ô W1(iii)).
2. FE: `npm run dev` ở `fe-user` (proxy `/api → :5443`) cho W1/W2/W3 · ở `fe-admin` cho W4.
3. Dữ liệu chuẩn bị: ≥1 HĐ trên cây GĐ3 (tạo từ KHKK) · ≥1 HĐ tạo trực-tiếp ngoài gói (W2-vii) ·
   user đúng-lượt + HĐ không-đến-lượt (W3-i) · 1 HĐ TraLai + 1 ChoDuyet + 1 DaPhatHanh (W5).
4. Ô nào không dựng được dữ liệu local ⇒ đánh **DEFERRED-TO-DEPLOY** công khai trong run.md, CẤM tick.

## Câu hỏi ĐÁNH SỐ trình anh (Q1/Q2 = nhánh-đã-chọn, đảo được; Q3/Q4/Q5 = chờ anh)

- **Q1 (Thứ-1):** em chọn **HOÁN ĐỔI** 2 khung (cây → trái 400px làm trục; danh-sách → phải 360px,
  GIỮ nguyên chức năng) thay vì bỏ hẳn danh-sách kiểu KHKK-thuần — vì cây KHÔNG chứa HĐ tạo-trực-tiếp
  (khác KHKK: mọi phiếu KHKK đều từ PE) + không xoá khối UI anh chưa bảo xoá. Anh muốn KHKK-thuần
  (bỏ list, flat-fallback trong panel cây) thì nói — đổi 1 wave.
- **Q2 (Thứ-1×2):** cây bên trái em gắn **Select loại HĐ** (khuôn Select nhóm của KHKK), default theo
  menu đang đứng — leaf GĐ3 trên cây lọc theo loại. Anh muốn cây hiện đủ mọi loại không lọc thì bỏ Select.
- **Q3 (Thứ-4):** màn duyệt HĐ có port 3 món KHKK đang có không: ô "Cấp này KẾT THÚC quy trình" ·
  nhãn "⚑ Duyệt thay CEO" · banner "kết thúc sớm ở Bước X"? **2 cờ đầu anh ĐANG THẤY trong dialog
  Sửa-tại-chỗ (ảnh-3) — hiện bật ở Admin mà máy duyệt HĐ KHÔNG đọc = cấu hình câm.** Port = nở
  `ContractDetail` DTO (BE nhỏ). Chưa port thì em ghi chú "chưa áp dụng cho HĐ" vào Designer (khuôn
  mờ-ô D3/S182) để hết câm.
- **Q4 (Thứ-4):** có mở **approver-delete** cho HĐ (người duyệt đúng-lượt xoá phiếu ChoDuyet theo cờ
  `AllowApproverDelete` per-Cấp — tính năng PE đang có, KHKK KHÔNG có)? Mặc định đợt này **KHÔNG** —
  đúng lời anh "tương tự KHKK". Anh gật thì thành wave riêng (cột DB sẵn, 0-mig).
- **Q5 (dữ liệu):** quy trình tay `QT-HD-V2-001` (bản type-3 duy nhất trên Dev, van-xả cố-ý cho mã
  ngoài họ) — GIỮ làm van xả admin, hay gỡ-ghim/thay bằng 7 bản `HD-*` để "mỗi loại 1 quy trình"
  tuyệt-đối cả trên dữ liệu? (0 code — việc Designer/DATA.)
