# SPEC DRAFT — YC-013: 8 panel "Duyệt KHKK — Nhóm 1..8" mirror trang Duyệt NCC (S182/L13)

> Tổng hợp từ 3 Fable deep-pass: `sub-invest-be-fable.md` (§1-§5+VERDICT) · `sub-invest-db-fable.md` (§1-§7) · `sub-invest-fe-fable.md` (§1-§6). Trạng thái: **DRAFT chờ 2 reviewer Fable + anh chốt D3**.

## MỤC 1 — Mục tiêu + phạm vi + tiền đề đã chứng

**Mục tiêu (YC-013):** 8 mục sidebar "Duyệt KHKK — Nhóm 1..8" ra ĐÚNG khuôn trang `approval-workflows-v2/DuyetNcc`: panel active-card + 4 nút (Sửa-tại-chỗ / Tạo-phiên-bản-mới / Ghim-Bỏ-ghim / Xoá-version) + Lịch sử versions + dialog Designer 9-checkbox per-NV + rule-box. **UI không lệch.**

**Tiền đề 3 lens hội tụ (mỗi cái có file:line trong artifact):**
- **0 MIGRATION** — 9/9 checkbox = 9 cột bit sẵn trên `ApprovalWorkflowLevels` dùng chung; versioning UNIQUE `(Code,Version)` + `IsActive`; Archived = `IsActive=false`.
- **BE generic ~85%** — 5 action controller + PUT diff-by-Id + guard chữ-ký 3-site ĐÃ KHKK-aware + `SetUserSelectable`/`Delete` by-Id.
- **Danh tính nhóm = `Code` (`KHKK-N{n}`), KHÔNG phải ApplicableType** — 8 workflow cùng type=10. Đây là gốc của MỌI việc phải sửa.
- **Hiện trạng "lệch":** 8 route `/system/approval-workflows-v2/KhkkN{n}` (Layout `:188`) đổ về **landing grid** vì `TYPE_CODE_TO_INT` không có key `KhkkN*` (`ApprovalWorkflowsV2Page.tsx:164-172`).
- **Authz:** giữ `Workflows.Create` y trang DuyetNcc (32 policy `AwV2_KhkkN*` 0-consumer BE — đo + control-dương; per-group-authz = đổi hình controller 9-type, KHÔNG làm trừ khi anh lệnh).
- Runtime 9-flag: KHKK hiện chỉ nghe **#8 Finalize**; 4 Trả-về + Sửa-S2 + Sửa-NS + Duyệt-thẳng + XÓA = **BỎ QUA/CHƯA WIRE** ⇒ điểm quyết D3 (dưới).

## MỤC 2 — WAVE-PLAN chi tiết

### W0 — prep local (bắt buộc trước mọi test)
1. `dotnet ef database update` ×2 DB (Dev + Design **đang kẹt Mig 71**, đĩa = Mig 72 — committed-but-unapplied class S53).
2. Boot API 1 lần → `SeedKhkkGroupWorkflowsAsync` đẻ 8 row local. ⚠️ Local 8 workflow **RỖNG NGƯỜI là ĐÚNG** (seeder resolve email prod) — acceptance cấu trúc chạy local, acceptance đội-hình chạy prod.

### W1 — BE-1: 4 fix, 2 file, 0-mig (`ApprovalWorkflowV2AdminFeatures.cs` + `ContractSigningPlanFeatures.cs` +1 helper)
| # | Fix | Site | Cách |
|---|---|---|---|
| a | 🔴 **Deactivate-per-Code cho type-10** (mìn M1, 3 lens hội tụ) | `:339-343` | `typeEnum == ContractSigningPlan` ⇒ scope `w.Code == request.Code`; type khác giữ per-type (P1 — KHÔNG làm P2 per-Code toàn cục: đổi hành vi PE/Contract chưa lường) |
| b | **Group-match guard 2-site** (bypass rào-v) | `:603-607` + `SubmitAsync` | extract rào (v) `:479-486` → `EnsureWorkflowGroupMatchAsync(db, awId, group)`; gọi ở Create + UpdateDraft + Submit (chốt-cuối, khuôn 2-tầng `:179-187`). Message lỗi chỉ việc cần làm cho phiếu lỡ pin lệch trước vá |
| c | **Changelog type-10 khi PUT sửa-tại-chỗ** | `:863-893` | thêm nhánh mirror block PE → `ContractSigningPlanChangelogs` (bảng sẵn Mig 69; khuôn ghi `ContractSigningPlanWorkflowService.cs:546-557`), query phiếu pin `def.Id` Phase ∉ {DaDuyet, TuChoi} |
| d | **DELETE usage-check → 409 đích danh** (lợi MỌI type) | `:1193-1205` | pre-count pin 8 module có cột `ApprovalWorkflowId` + LevelOpinions (khuôn `CountLiveOpinionsAsync` theo workflow-id) → 409 "N phiếu đang pin"; hết cảnh `SqlException 547 → 500 thô` (`GlobalExceptionMiddleware.cs:32-39` không map DbUpdateException) |

**Test W1 (test-before cho a/b, test-after c/d):** tạo v2 `KHKK-N1` ⇒ N2..N8 `IsActive` bất biến + regression DuyetNcc tạo-Code-mới-cùng-type vẫn tắt Code cũ · UpdateDraft re-pin lệch nhóm ⇒ 409 · PUT workflow nhóm có phiếu treo ⇒ 1 row `ContractSigningPlanChangelogs` · DELETE version có pin ⇒ 409 (không 500).

### W2 — FE: 1 file chính `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` (~60-90 dòng diff)
| # | Chạm | Site | Cách |
|---|---|---|---|
| a | Parse kind | `:164-172` | `match(/^KhkkN([1-8])$/)` → `khkkGroup`; `selectedTypeInt = khkkGroup ? 10 : TYPE_CODE_TO_INT[typeCode]` (query overview giữ `applicableType=10`) |
| b | Synthetic slice per-Code | `:299-301` | từ type-10: `versions = history.filter(d => d.code === khkkGroupWorkflowCode(g))` · `active = versions.find(v => v.isActive) ?? null` (🔴 KHÔNG tin ô `Active` per-type của BE — nó số-ít, 8 nhóm trộn) · title = "Duyệt KHKK — Nhóm {g}" + phụ đề nghiệp vụ `khkkGroupMenuLabel(g)` (D4 — lead chọn theo đề xuất FE, reviewer chấm) |
| c | `lockedCode` xuyên suốt | `TypePanel:357` → `Designer:620/:650/:798/:927` | prop `lockedCode?: string`; ô Mã `disabled={isEdit || !!lockedCode}`; POST gửi `lockedCode ?? code`. 🔴 Thiếu = "Tạo quy trình mới" từ panel nhóm với mã tự gõ đẻ version **rơi khỏi slice** + không được rào (v) nhận |
| d | **D3 — 7 ô chưa-wire** (chờ anh chốt, xem MỤC 3) | `:1275/:1284/:944-961/…` | thi hành theo phương án anh chọn {A giữ / B port / C mờ-ô} |
| e | (optional) mirror menuKeys | `fe-{admin,user}/src/lib/menuKeys.ts:31-32` | +8 const `AwV2_KhkkN*` — 0 hệ quả runtime, mirror-completeness |

**SHA-mirror 2 app: KHÔNG áp dụng** — Designer là fe-admin-only (đã CHỨNG: fe-user 0 route/0 page/0 nhánh `AwV2_*`); `types/khkk.ts` 2 app identical sẵn, không chạm.
**Regression:** 4 route cũ (`DuyetNcc`/`PhuongAn`/`Contract`/`ContractSigningPlan`) render như cũ · fe-user tạo phiếu nhóm auto-pin `KHKK-N{g}` không đổi API · **hệ quả nghiệp vụ THẬT của nút Ghim:** `IsUserSelectable=false` 1 nhóm ⇒ CHẶN TẠO PHIẾU nhóm đó cả 2 app (`KhkkCreatePage.tsx:104/:113-116`) — acceptance phải nêu để anh biết nút này có răng.

### W3 — CHỈ KHI anh chọn D3=B: port semantics vào `ContractSigningPlanWorkflowService`
F6 XÓA-đúng-lượt (mirror PE 3-rào `PeSoftDeleteFeatures.cs:110-114`) · F1×4 return-modes per-level (mirror switch `PurchaseEvaluationWorkflowService.cs:421-424`) · F3-analog sửa-Lines-lúc-duyệt · F2 skip-to-final (+gỡ ép-false `:387` + gỡ disable FE `:1292-1302` **CÙNG WAVE** — tiền lệ F5 @K3). Mỗi cờ = test RED→GREEN riêng. `CeoApprovalThreshold` **KHÔNG port** (OG-3 NĐUQ human-gate — owner đã chốt trước).

### W4 — test-specialist đóng suite (baseline 645 → +N per W1/W3)
### W5 — reviewer adversarial pre-commit → commit → cicd-monitor 3-chân (bundle **admin rotate**, eoffice giữ nguyên nếu chỉ W2a-d; +eoffice nếu e) → acceptance prod + 3 query SQL verify (DB §4 bàn giao)

## MỤC 3 — Điểm quyết + acceptance + rollback

**D3 (ANH CHỐT — chặn scope W2d/W3):** 6/9 checkbox tick được mà KHKK-runtime KHÔNG nghe (#78 config-lie class):
- **A** — giữ 9 ô tick được (uniform tuyệt đối với ảnh; cờ lưu DB nằm chờ; risk: người cấu hình tưởng có hiệu lực).
- **B** — port cho sống thật (W3; đổi hành vi duyệt KHKK; nặng + cần test per-cờ).
- **C** — mờ ô chưa-wire + tooltip "chưa áp dụng cho KHKK" (tiền lệ CHÍNH TRANG NÀY: SkipToFinal đã disabled cho type-10 `:1292-1302`; + ô Ngưỡng CEO cùng cách; trung thực #78; port sau gỡ dần).

**D4 (lead quyết, reviewer chấm):** title panel = menu-label "Duyệt KHKK — Nhóm {g}" + phụ đề nghiệp vụ từ `khkkGroupMenuLabel(g)`.

**Acceptance (đo được, từ FE §5):**
1. 8 leaf sidebar → 8 slice panel khác nhau (URL `/KhkkN{n}`, header đúng nhóm, card `KHKK-N{n} v01` + badge + 3 Bước + 4 nút + Lịch sử) — hết landing grid.
2. Dialog Sửa-tại-chỗ: title `KHKK-N{n} v{NN} (giữ nguyên phiên bản)`, khối cam 9 checkbox đúng khuôn (theo D3), Drafter tick sẵn, rule-box.
3. "Tạo phiên bản mới" nhóm N1 ⇒ v02 nằm ĐÚNG panel N1; **badge "Đang áp dụng" 7 nhóm khác KHÔNG mất** (W1a).
4. Regression 4 route cũ + fe-user auto-pin + 645 test cũ xanh.

**Rollback:** 0 mig ⇒ revert commit là sạch; W1a có regression-test 2 chiều giữ hành vi cũ cho type ≠ 10.

## Nợ NGOÀI scope (khai, không lặng): GET Overview 0-policy lộ roster+9-flag cho mọi user authenticated (BE §5.2 — information-disclosure mềm, owner ngó sau) · roster 8 nhóm = snapshot email lúc seed, KHÔNG tự sync theo QT-DN-V2-001 (BE §5.3) · `Khkk_WfView` panel aggregate giấu 7/8 nhóm (FE §6.1 — slice này vá cho 8 leaf mới; re-point mục cũ = việc riêng) · comment "7 bảng" stale `:1120`.
