# sub-implementer-frontend-1 — S182 W2 (a-h) lane FE

> ⚠️ TÊN FILE: TASK text ghi `sub-implementer-frontend-0.md`, RUN-TRACE block + structured-output ghi `sub-implementer-frontend-1.md`.
> Chọn `-1` (bản RUN-TRACE = bản em main đọc qua `subMdPath`). Khai ở đây để lead biết.

## §0 — Trạng thái lượt (append liên tục, chống-#53)
- [x] Đọc SPEC FINAL MỤC 2 W2 (a-h) + MỤC 1 R-1..R-6
- [x] Đọc file đích + đo tiền-đề từ ĐĨA (line-number trong spec có thể lệch)
- [x] W2a parse kind
- [x] W2b synthetic slice
- [x] W2c lockedCode
- [x] W2d D3=C 6 ô + #4
- [x] W2e menuKeys ×2 app
- [x] W2f ngưỡng CEO
- [x] W2g auto-pin isActive ×2 app
- [x] W2h comment + subtitle
- [x] build fe-admin — exit 0
- [x] build fe-user — exit 0

**Lượt 2 (vá-nốt):** chẩn-đoán TS trong TASK là **ảnh cũ** — đo lại từ đĩa thì đã sạch ⇒ **0 dòng code sửa**, lượt này là ĐO + CHỨNG. Chi tiết §3.3.
- [x] tsc thật ×2 app (lệnh trong spec là phép đo CÂM — §3.1)
- [x] control-dương chứng tsc có răng (§3.2)
- [x] proof-harness 27/27 + anti-Goodhart (§3.4)
- [x] đối chiếu code-vs-spec W2a-h (§3.5)
- [x] mirror proof SHA + 4-place (§3.6)

## §1 — Đo tiền-đề từ đĩa (TRƯỚC khi gõ)

| # | Tiền-đề spec | Đo trên đĩa | Verdict |
|---|---|---|---|
| P1 | `:164-172` TYPE_CODE_TO_INT | `ApprovalWorkflowsV2Page.tsx:164-172` ĐÚNG (`TYPE_INT_CONTRACT_SIGNING_PLAN=10` `:149`) | HELD |
| P2 | `:299-301` currentType | `:299-301` `overview.data?.types.find(...)` ĐÚNG | HELD |
| P3 | `:357/:620/:650-651/:798/:920-933` | `TypePanel :357` · `Designer :620` · `defaultCode :650-651` · POST code `:798` · ô Mã `:920-933` — ĐÚNG cả 5 | HELD |
| P4 | 6 ô D3 `:1235-1266`, `:1268-1276`, `:1277-1285`, `:1320-1328` | #1 `:1232-1240` · #2 `:1241-1249` · #3 `:1250-1258` · #4 `:1259-1267` · #5 `:1268-1276` · #6 `:1277-1285` · #7 `:1292-1302` · #8 `:1307-1315` · #9 `:1320-1328` | HELD (số ô #1-#4 lệch ±3 dòng so spec, khớp nội dung) |
| P5 | `:944-961` ngưỡng CEO | `:943-961` ĐÚNG | HELD |
| P6 | `menuKeys.ts:31-32` = 2 const `AwV2_*` | fe-admin `:31-32` + fe-user `:31-32` ĐÚNG, 0 key KhkkN | HELD |
| P7 | BE const để mirror | `MenuKeys.cs:127-134` `AwV2_KhkkN1..8` (VÀO `All`) | HELD |
| P8 | 4-place Layout staticMap | `fe-admin/Layout.tsx:188` `/^KhkkN[1-8]$/` ĐÃ CÓ (K3 S164/S165) ⇒ **chỗ-4 KHÔNG cần chạm** | HELD |
| P9 | `khkkGroupWorkflowCode` / `khkkGroupMenuLabel` | `types/khkk.ts:159-168`, 2 app SHA identical `b26bf674…` | HELD |
| P10 | 🔴 spec W2g *"2 file KhkkCreatePage vốn khác nhau — không cần byte-equal"* | **SAI**: `sha256sum` 2 file = `332986ae6bf1904a4db434c796164caa2768beaaebdc7843dd2b1cdd729d17da` **BẰNG NHAU** + `diff` 0 dòng | **LỆCH** → sửa mirror BYTE-EQUAL, giữ SHA-pair |
| P11 | `WorkflowDto` (local) có `isActive`? | `KhkkCreatePage.tsx:57-62` = `{id, code, name, isUserSelectable}` — **THIẾU `isActive`** | phải thêm field (BE có: `DefinitionDto.isActive :79`) |
| P12 | 🔴 spec W2h *"thêm ~1 prop `subtitle`"* | `components/PageHeader.tsx:3-11` chỉ có `title/description/actions` (bài S37 lặp lần 3) | **LỆCH** → xem §4 FLAG-1 |
| P13 | build = phép thử TS thật | `package.json` build = `tsc -b && vite build`, `noUnusedLocals`+`noUnusedParameters`+`erasableSyntaxOnly` ON | HELD |

## §2 — Thay đổi từng task (đã gõ xong, chờ build)

**File chạm (4):**
1. `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` (a,b,c,d,f,h)
2. `fe-admin/src/lib/menuKeys.ts` (e)
3. `fe-user/src/lib/menuKeys.ts` (e)
4. `fe-admin/src/pages/khkk/KhkkCreatePage.tsx` + `fe-user/…` (g, `cp` → SHA-pair)

### W2a — parse kind
- `khkkGroupMatch = typeCode?.match(/^KhkkN([1-8])$/) ?? null` → `khkkGroup: number|null`
- `selectedTypeInt = khkkGroup !== null ? TYPE_INT_CONTRACT_SIGNING_PLAN : typeCode ? TYPE_CODE_TO_INT[typeCode] : null`
- queryKey GIỮ NGUYÊN (`['approval-workflow-v2-overview', selectedTypeInt]`) ⇒ 8 panel nhóm + panel type-10 **dùng chung 1 ô cache**, invalidate làm tươi cả 9. Khai trong comment.
- Regex CHUỖI (không `Number()`) — `KhkkN03` không lọt (bài học L1 #11).
- +import `{ khkkGroupMenuLabel, khkkGroupWorkflowCode } from '@/types/khkk'` (không chép chuỗi `KHKK-N`).

### W2b — synthetic slice
- Tách `typeBucket` (raw BE) ⟂ `currentType` (lát cắt). `useMemo([khkkGroup, typeBucket])`.
- `versions = typeBucket.history.filter(d => d.code === khkkGroupWorkflowCode(g))` · `active = versions.find(v=>v.isActive) ?? null` · **KHÔNG đọc `typeBucket.active`** (1-trong-8).
- Giữ shape `TypeSummaryDto` ⇒ TypePanel/Designer không biết mình đang xem nhóm hay loại.

### W2c — lockedCode xuyên suốt
- Page: `lockedCode = khkkGroup !== null ? khkkGroupWorkflowCode(khkkGroup) : null`
- `TypePanel({type, lockedCode, onSaved})` → `<Designer lockedCode={lockedCode} …>`
- `defaultCode = lockedCode ?? DEFAULT_CODE_BY_TYPE[applicableType] ?? 'QT-V2-001'` — ô hiện ĐÚNG mã sẽ gửi (không hiện `QT-KHKK-V2-001` rồi gửi `KHKK-N3`).
- Ô Mã `disabled={isEdit || lockedCode !== null}` + hint "Mã khoá theo nhóm — version tự tăng."
- POST `code: lockedCode ?? code` (bất-biến theo CẤU TRÚC, không dựa vào input disabled).
- **+`key={typeCode ?? 'all'}` trên `<TypePanel>`** — NGOÀI spec, lý do: chuyển N1→N3 khi Designer đang mở thì `cloneFrom/editTarget` (state trong TypePanel) còn trỏ bản N1 trong khi `lockedCode` đã là N3 ⇒ POST đẻ `KHKK-N3` mang ruột N1. Remount xoá state. Khai ở đây.

### W2d — D3=C danh-sách-chốt
Cờ chung `const isKhkk = applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN` (1 nguồn, 8 chỗ dùng).
| Ô | Cờ | Xử lý |
|---|---|---|
| #1 | allowReturnOneLevel | `disabled={isKhkk}` + title chung + **`checked={entry.…}` GIỮ** + mờ text |
| #2 | allowReturnOneStep | ↑ |
| #3 | allowReturnToAssignee | ↑ |
| #4 | allowReturnToDrafter | `disabled` + `checked` THẬT + **title RIÊNG** "KHKK hiện luôn trả về người soạn khi Trả lại — cờ chưa phân nhánh" + **KHÔNG mờ** (giữ `text-slate-700`) |
| #5 | allowApproverEditDetails | như #1 |
| #6 | allowApproverEditBudget | như #1 |
| #7 | allowApproverSkipToFinal | **GIỮ hành vi** (kể cả `checked` ép-false của K3 S165); chỉ thay 3 biểu thức trùng → `isKhkk`, giá trị y hệt |
| #8 | allowApproverFinalize | **SỐNG** — 0 chạm |
| #9 | allowApproverDelete | như #1 (rose→slate khi mờ) |
- `levelFlagsPayload` **0 dòng đổi** (echo state thật).
- +1 dòng giải thích trong hộp tím (chỉ khi `isKhkk`) — nói rõ ô mờ vẫn hiện đúng giá trị DB, và 2 ô còn tác dụng.

### W2e — menuKeys ×2 app
+8 const `AwV2_KhkkN1..8` mirror `MenuKeys.cs:127-134`, sau `:32` cả 2 app. Comment viết theo app (fe-admin trỏ `Layout.tsx:188`; fe-user khai "cố ý không có Designer") — **cố ý lệch chữ, không lệch const**.

### W2f — Ngưỡng CEO
`disabled={isEdit || isKhkk}` + helper-text "KHKK không dùng ngưỡng máy — NĐUQ quyết…" + POST `ceoApprovalThreshold: isKhkk ? null : (…)`.

### W2g — auto-pin lọc isActive ×2 app
- `WorkflowDto` (local, `:57-62`) **+`isActive: boolean`** — BE có (`AwAdminOverviewDto`, đối chiếu `DefinitionDto.isActive`).
- `find(w => w.isActive && code-match)`; filter `isUserSelectable` ở query GIỮ.
- Sửa 2 câu thông báo cho khớp điều-kiện MỚI (3 vế: đúng mã ∧ đang áp dụng ∧ đã ghim) — nếu không sẽ nói "chưa có quy trình" trong khi nó đang nằm đó ở bản archived (sai-thầm-lặng).
- `cp` fe-admin → fe-user, SHA-pair `89dfa607…` **BẰNG NHAU**.

### W2h — comment + tiêu đề
- Vá comment `:641-644`: câu cũ "2 ô duyệt sớm sống cho MỌI loại" **SAI** — #7 vẫn khoá cho type-10 từ K3 S165. Viết lại + trỏ tại-chỗ.
- PageHeader: title = `Quy trình duyệt (Mới): {khkkGroupMenuLabel(g)}` · description = phụ đề nghiệp vụ (loại + nhóm N{n} + mã khoá + "7 nhóm còn lại không đổi"). Xem FLAG-1.

## §3 — Verify (build ×2 + proof)

### 3.1 🔴 Lệnh acceptance trong spec/TASK là PHÉP ĐO CÂM — phải đổi
`npx tsc --noEmit` (nguyên văn acceptance R-2 + TASK lượt 2) chạy ra **EXIT=0, 0 dòng output** ở CẢ 2 app **kể cả khi code sai**: `fe-{admin,user}/tsconfig.json` là solution-style `{"files": [], "references": [...]}` ⇒ tsc **không biên dịch file nào** rồi thoát 0. Xanh vì CÂM, không phải vì đúng.
**Lệnh THẬT** (đã dùng dưới đây): `npx tsc -p tsconfig.app.json --noEmit` — hoặc `tsc -b` (chính `npm run build`).

| # | Lệnh | Exit | Ghi chú |
|---|---|---|---|
| V1 | `fe-admin$ npx tsc -p tsconfig.app.json --noEmit` | **0** | 114 file `src/` trong tập biên dịch (`--listFiles`), có `ApprovalWorkflowsV2Page.tsx` + `types/khkk.ts` |
| V2 | `fe-user$ npx tsc -p tsconfig.app.json --noEmit` | **0** | |
| V3 | `fe-admin$ npm run build` (`tsc -b && vite build`) | **0** | ✓ 1964 modules · `index-CmEoYZiH.js` 1.761,63 kB |
| V4 | `fe-user$ npm run build` | **0** | ✓ 1953 modules · `index-CT5Y_xQ4.js` 1.677,49 kB |

V3/V4 chạy **SAU** khi đã hoàn tác tiêm-lỗi ⇒ đo đúng trạng thái cuối trên đĩa.
Warning PRE-EXISTING (không phải nợ mới): chunk >500 kB · `INEFFECTIVE_DYNAMIC_IMPORT realtime.ts` · CSS `@import` order.

### 3.2 Control-DƯƠNG — chứng phép đo có RĂNG
Tiêm `const __ctrl_neg: number = khkkGroupWorkflowCode(3)` vào cuối chính file đã ship → tsc nổ **2 lỗi**:
`TS2322 Type 'string' is not assignable to type 'number'` + **`TS6133 '__ctrl_neg' is declared but its value is never read`**.
🔴 Ý nghĩa: **TS6133 (`noUnusedLocals`) CÒN SỐNG** — đúng class lỗi mà 3 chẩn-đoán "declared-never-read" (`lockedCode :417` · `isKhkk :714` · `khkkNotYetHint :717`) thuộc về. Nên "0 lỗi" ở V1 = **thật sự hết**, không phải đo hụt.
Hoàn tác: SHA trước/sau `dda4be1f9d4dabd…b69b7f71` **BẰNG NHAU** (byte-identical).

### 3.3 🔴 Chẩn-đoán trong TASK là ẢNH CŨ — 0 dòng code phải vá
6 lỗi TS mà TASK liệt (thiếu import `khkkGroupWorkflowCode`/`khkkGroupMenuLabel` `:330/:343/:355` · `lockedCode` chưa khai ở signature `:403/:517` · 3 biến never-read) **KHÔNG còn tồn tại trên đĩa**: `import { khkkGroupMenuLabel, khkkGroupWorkflowCode } from '@/types/khkk'` ở `:36`; `lockedCode` khai đủ ở CẢ 2 signature (`TypePanel :417/:421` · `Designer :690/:699`); 3 biến đều có chỗ dùng thật (`isKhkk` 20 call-site, `khkkNotYetHint` 6, `lockedCode` 6).
⇒ Đây là **IDE-diagnostic chụp giữa lúc lane trước đang gõ** (gotcha #68). Lượt này **KHÔNG sửa 1 dòng code nào** — chỉ đo + chứng. `git diff --stat` giữ đúng **5 file**, 0 file untracked.

### 3.4 Proof-harness — chạy CHÍNH text đã ship (extract-by-anchor, neo lệch thì script tự chết)
`node proof-w2.mjs` → **27 PASS / 0 FAIL**:
- **T1 (8/8)** dây xuyên suốt `AwV2_KhkkN{n}` → `Layout.tsx:188` → `/system/approval-workflows-v2/KhkkN{n}` → regex page parse nhóm `{n}` → `lockedCode = KHKK-N{n}`, đối chiếu đủ 3 nguồn BE+admin+user.
- **T2 (7/7) control-ÂM:** `KhkkN0` · `KhkkN9` · **`KhkkN03`** · `KhkkN10` · `khkkn1` · `"KhkkN1 "` · `KhkkN` — bị chặn ở CẢ 2 regex (regex CHUỖI, không `Number()`).
- **T3 (2/2)** control-âm cho chính T1: key bịa `AwV2_KhkkN9/N0` vắng ở cả 3 nguồn.
- **T4/T5/T6 (10/10)** D3=C — xem 3.5.

**Anti-Goodhart** (`proof-control.mjs`, exit 0): áp ĐÚNG vị ngữ T4 và T5 lên ô **#7** (ô cố ý khoá + ép-false từ K3 S165) ⇒ **cả hai đều `false`**. Phép đo phân biệt được ô-sống ⟂ ô-khoá và bắt được ép-false; không phải luôn-luôn-PASS.

### 3.5 Đối chiếu code-vs-spec W2a–h
| Task | Verdict | Bằng chứng |
|---|---|---|
| W2a parse kind | ✅ | `:302-306` regex chuỗi; queryKey giữ nguyên (9 panel chung 1 ô cache — khai tại `:300-301`) |
| W2b synthetic slice | ✅ **và là BẮT BUỘC** | BE `active = versions.FirstOrDefault(v => v.IsActive)` (`:210`) — với type-10 nay có **8 bản active song song** (BE lane đổi sang tắt **per-CODE** `:336-360`) ⇒ đọc `bucket.active` sẽ sai cho 7/8 panel. `history` xác nhận **KHÔNG lọc** `IsActive` (`definitions` `:118-134` chỉ lọc Type + IsUserSelectable) |
| W2c lockedCode | ✅ | signature 2 nơi · `defaultCode :733` · ô Mã `disabled :1020` · **POST `code: lockedCode ?? code` `:884`** (bất-biến theo CẤU TRÚC). Khớp khoá với vị ngữ `w.Code == request.Code` bên BE |
| W2d D3=C | ✅ **đúng danh-sách-chốt** | 6 ô `disabled={isKhkk}` + `checked={entry.…}` THẬT · #4 tooltip riêng + KHÔNG mờ · #7 giữ nguyên · **#8 `allowApproverFinalize` 0 chạm** (T4) · `levelFlagsPayload` 0 dòng đổi |
| W2e menuKeys ×2 | ✅ | 8 const khớp `MenuKeys.cs:127-134`; khối const 2 app **diff 0 dòng**, chỉ comment cố ý lệch |
| W2f ngưỡng CEO | ✅ | `disabled={isEdit \|\| isKhkk} :1052` + helper-text `:1055` + **POST `null` `:891`** |
| W2g auto-pin isActive | ✅ | `isActive` **CÓ THẬT** trong payload (`ToDto` `:176`) — không phải field bịa; 2 câu thông báo đổi sang nói ĐIỀU KIỆN (rỗng-vì-lọc ⟂ rỗng-vì-chưa-có) |
| W2h comment + tiêu đề | ✅ | subtitle gộp vào `description` (xem FLAG-1) |

### 3.6 Mirror proof (Pattern 16-bis + SHA)
- `KhkkCreatePage.tsx` admin ⟂ user: `89dfa607061cd3d560dcbbdb8fbad157d38cae1786be3cd41a81bd8e9c1d1390` **BẰNG NHAU** (cặp `cp`, W2g).
- `types/khkk.ts` admin ⟂ user: `b26bf67492821b1003c3d523585e2ce563a26b04ef728867689911263e5a33d3` **BẰNG NHAU** — file **không sửa** ⇒ **đối chứng DƯƠNG** cho phép đo hash.
- **4-place:** ① page+types ✅ · ② `App.tsx:80` route `:typeCode` đã có (0 route mới) ✅ · ③ `menuKeys.ts` ×2 ✅ · ④ `Layout.tsx:188` staticMap **đã có sẵn từ K3 S164/S165** ⇒ chỗ-4 KHÔNG cần chạm (P8 đúng), regex Layout khớp regex page.
- **Lệch CÓ CHỦ ĐÍCH:** fe-user **không có** `ApprovalWorkflowsV2Page.tsx` + **0** nhánh `AwV2` trong `Layout.tsx` (màn cấu hình là của admin) ⇒ 8 const bên fe-user là mirror hằng-số thuần, không sinh menu.

## §4 — Nợ / FLAG cho lead

- 🔴 **FLAG-A (acceptance hỏng — cần sửa spec):** acceptance R-2 ghi *"`npm run build` ×2 app + **tsc 0-err**"*. Nếu ai chạy `npx tsc --noEmit` đúng nguyên văn thì **luôn xanh kể cả code sai** (§3.1). Đề nghị sửa acceptance thành `npx tsc -p tsconfig.app.json --noEmit`. Ảnh hưởng **mọi wave FE trước/sau**, không riêng S182.
- 🔴 **FLAG-B (spec tự mâu-thuẫn — đã chọn nhánh, xin lead ratify):** ô **#4 `allowReturnToDrafter`**. MỤC 2 dòng `d` (bảng thi-hành, có neo dòng) ghi *"hiển thị **state thật (thường TRUE)**"*; MỤC 3 bảng D3 ghi bản rút gọn *"#4 **forced-true**-disabled"*. Code đang theo **state thật** (`checked={entry.allowReturnToDrafter}`), vì:
  (1) đo đĩa: mặc định `true` ở **4 lớp độc lập** — `ApprovalWorkflow.cs:129 = true` · EF `HasDefaultValue(true)` · DTO `:232/:465` · FE hydrate `?? true` (`:213`) ⇒ thực tế **vẫn hiện TICK**, thoả acceptance A2 *"ô #4 tick-disabled"*;
  (2) hard-code `checked={true}` sẽ **nói dối** đúng ca hàng row có `false` — cùng class lỗi mà chính D3=C cấm ở 6 ô kia (*"CẤM ép false"*), chỉ đảo dấu.
  ⇒ Đọc *"forced-true"* là **rút gọn hao nghĩa** của *"state thật (thường TRUE)"*. **Nếu lead thật sự muốn ép cứng `true`** thì nói 1 câu, sửa 1 dòng — nhưng nên biết là đánh đổi tính trung thực với DB.
- ⚠️ **FLAG-1 (P12, đã xử lý — không còn là nợ):** `@/components/PageHeader` **không có** prop `subtitle` (`title/description/actions`, cả 3 đều `ReactNode`). **Không thêm prop** — phụ đề nghiệp vụ gộp vào `description`. Lý do: PageHeader dùng chung toàn app, đẻ prop cho 1 trang là mở rộng API cho cả trăm call-site. (Bài S37, lặp lần 3 — mỗi lần lại tưởng có `subtitle`.)
- ⚠️ **Ngoài spec, đã khai:** `key={typeCode ?? 'all'}` trên `<TypePanel>` (`:401`) — remount khi đổi nhóm; thiếu nó thì `cloneFrom/editTarget` còn trỏ bản N1 trong khi `lockedCode` đã sang N3 ⇒ POST đẻ `KHKK-N3` mang ruột N1.
- ⚠️ **Nợ nhận-thức (không chặn):** 8 panel nhóm + panel `/ContractSigningPlan` **dùng CHUNG 1 ô cache** (queryKey không có `khkkGroup` — cố ý, vì lọc client-side trên payload đã tải trọn). Invalidate sau khi lưu làm tươi cả 9. Nếu sau này BE nhận `code=` làm tham số **request** thì BẮT BUỘC thêm `code` vào queryKey, nếu không 9 màn dùng chung cache sai CÂM.
- ⚠️ **Chưa chạy (ngoài tay lane FE):** acceptance A1/A2b/A3/A4 là **UAT động** (bấm thật trên 8 leaf, sửa-tại-chỗ ⇒ 204, tạo v02 N1 kiểm 7 nhóm không mất badge). Lane này chỉ chứng được phần **tĩnh**. A3 phụ thuộc BE lane (tắt active per-CODE) — 2 nửa phải test CÙNG nhau.
- ✅ **Không nợ build/mirror:** 0 lỗi TS ×2 app · SHA-pair 2/2 · 4-place 4/4 · 27/27 proof + 3 control.

## §5 — vá MAJOR-1 (reviewer pre-commit, lượt sau)

**Bệnh:** `title=` nằm trên chính `<input type="checkbox" disabled={isKhkk}>`. Chromium (Chrome/Edge = trình duyệt đích của owner) **nuốt sự kiện chuột của form-control disabled** ⇒ tooltip KHÔNG bao giờ bật; `<label>` bao ngoài không mang `title` nên rê vào chữ cũng câm. Quyết định D3=C của owner là *"mờ ô chưa nghe **+ tooltip**"* ⇒ mất vế tooltip thì admin chỉ còn 7 ô xám vô cớ. Nặng nhất là ô **#4 `allowReturnToDrafter`**: câu *"KHKK hiện luôn trả về người soạn khi Trả lại — cờ chưa phân nhánh"* là thông tin **DUY NHẤT** phân biệt #4 với 6 ô "chưa áp dụng" — câm ⇒ #4 trông y hệt 6 ô kia = **ngược nghĩa**.

**(a) Dời `title` từ `<input>` lên `<label>` — 8/8 site** (label không disabled nên hover chữ là hiện):

| # | cờ | title cũ ở input | title mới ở label |
|---|---|---|---|
| 1 | `allowReturnOneLevel` | :1361 | :1366 (label @:1364) |
| 2 | `allowReturnOneStep` | :1372 | :1379 (@:1377) |
| 3 | `allowReturnToAssignee` | :1383 | :1392 (@:1390) |
| 4 | `allowReturnToDrafter` | :1399 | :1410 (@:1408) |
| 5 | `allowApproverEditDetails` | :1410 | :1432 (@:1430) |
| 6 | `allowApproverEditBudget` | :1421 | :1445 (@:1443) |
| 7 | `allowApproverSkipToFinal` | :1443 ← **tiền lệ CÙNG BỆNH từ K3**, reviewer chỉ đích danh | :1469 (@:1467) |
| 9 | `allowApproverDelete` | :1471 | :1499 (@:1497) |

**(b) RIÊNG ô #4 — thêm chữ NHÌN THẤY ĐƯỢC** (`fe-admin/.../ApprovalWorkflowsV2Page.tsx:1421-1429`, ngay dưới `</label>` #4 @:1420): `{isKhkk && <div className="col-start-2 -mt-0.5 pl-4 text-[10px] leading-snug text-slate-500">{khkkDrafterHint}</div>}`. Thông tin phân-biệt **không được phụ thuộc hover**. Dùng `col-start-2` (không phải sibling thường) vì #4 nằm ô **hàng 2 cột 2** của `grid grid-cols-2`: sibling auto-place sẽ rơi xuống **cột 1** ⇒ đọc nhầm sang ô #3 và đẩy lệch 5 ô `col-span-2` phía dưới. `col-start-2` ghim dòng chữ đúng dưới ô của nó; 5 ô `col-span-2` kế tiếp vẫn auto-place xuống hàng mới, 0 reflow.

**Chuỗi #4 gom thành 1 const** `khkkDrafterHint` (`:722`, cạnh `khkkNotYetHint`) vì nay dùng ở **2 chỗ** (tooltip + chữ hiện) — 2 bản literal sẽ trôi lệch nhau. Kèm comment tại `:715-721` ghi rõ **vì sao** `title` phải ở label, để lần sau không ai dời ngược lên input rồi làm câm lại.

**Vùng CẤM — đo lại sau khi vá, xác nhận nguyên vẹn:**
- 9/9 `checked=` **byte-identical** với bản trước khi vá (`:1371 :1384 :1397 :1415 :1437 :1450 :1474 :1488 :1504`) — gồm cả `checked={isKhkk ? false : entry.allowApproverSkipToFinal}` (quyết định K3, giữ) và `checked={entry.allowReturnToDrafter}` (**FLAG-B §4 vẫn treo y nguyên, lượt này KHÔNG động**).
- Ô **#8 `allowApproverFinalize`** (`:1484-1492`): **0** `disabled`, **0** `title` — không chạm.
- `levelFlagsPayload` (`:258`, call `:880/:920`): không chạm. `isKhkk` (`:714`): không chạm.

**Proof — checker có RĂNG (control-âm):** script parse tag-owner của mọi `title=` trong file (`scratchpad/check-title-owner.js`).
- Bản đã ship: `title=` tổng **22** · tham chiếu `isKhkk` **8** · owner=`<label>` **8/8** · owner=`<input>` **0**.
- **CONTROL-ÂM:** tiêm lại 1 `title` vào `<input>` ô #1 ⇒ checker **nổ đúng 1** (`owner=<input> :1373`). ⇒ 0-hit ở bản thật là **im-vì-đúng**, không phải im-vì-câm.

**Build:** `npx tsc --noEmit -p tsconfig.app.json` → **exit 0** (🔸 dùng `-p`; `npx tsc --noEmit` TRẦN biên dịch 0 file = xanh giả — đúng FLAG-A §4). `npm run build` fe-admin → **✓ built in 30.18s, 0 lỗi TS**; warning `>500 kB` + `INEFFECTIVE_DYNAMIC_IMPORT realtime.ts` + `@import` = **PRE-EXISTING**.

**Mirror:** `ApprovalWorkflowsV2Page.tsx` **chỉ tồn tại ở fe-admin** (fe-user: file không có, `ls pages/system | grep -i workflow` = 0, `grep khkkNotYetHint|khkkDrafterHint fe-user/src` = **0 hit**) ⇒ **không có cặp để mirror**, đúng lệch-có-chủ-đích đã khai §3.6. Lượt này chạm **đúng 1 file**.

**⚠️ FLAG-C (cùng lớp, KHÔNG chạm — xin lead quyết):** grep cả file thấy **1 site nữa cùng bệnh** ngoài phạm vi MAJOR-1: nút **"+ Thêm NV"** (`:1226-1248`) có `disabled={addDisabled}` + `title=` giải thích **lý do bị khoá** (*"Cấp N-1 phải có ≥1 NV trước"* / *"Hết NV khả dụng…"*) đặt **trên chính `<button disabled>`** ⇒ cùng Chromium suppress, admin không biết vì sao nút xám. Đây là bug **có sẵn từ trước S182**, không thuộc D3=C, và TASK ghi rõ *"1 việc DUY NHẤT — KHÔNG làm gì khác"* ⇒ **để nguyên, chỉ khai**. (Hai nút `title="Lên"/"Xuống"` `:1155/:1164` cũng disabled ở rìa nhưng title chỉ là **nhãn**, không mang thông tin lý-do ⇒ không cùng lớp thiệt hại.)
