K2-FE: DONE — build ×2 PASS · SHA-pair 5/5

# sub-implfe-k2 — K2 FRONTEND (S164, wave 2/8, SP-002 phiếu mang hạng-mục + nhóm)

> Luật đan-xen (bài K1 + #53): làm mục nào GHI NGAY mục đó. Dòng-1 = verdict, cập nhật CUỐI.
> Nguồn: `sub-implbe-k2.md` (BE đã land) + spec-cum1 vá-4 + spec-tổng-quát QĐ-1/OG-1/OG-7 + nền `sub-invest-fable-b2-cum1.md` §K2.f.

## §0 Tiền đề — ĐO TRƯỚC MỌI WRITE (không tin sổ)

- `git status --porcelain` @start: `sub-implfe-k2.md` **KHÔNG tồn tại** trong run-folder (có k1/implbe-k2/test/reviewer/cicd) ⇒ Write mới AN TOÀN, không đè lane khác.
- **0 file `fe-*/src` nào đang M** trong git status @start ⇒ mọi thay đổi FE dưới đây là của lane này, git-diff sẽ quy trách nhiệm đúng.
- BE lane đã land (đọc `sub-implbe-k2.md`): Mig 71 applied 2-DB · rào (PeId,Group) · submit-guard · list `?group=` · Σ-Approved · build 0W/0E · suite 598/0.
- 🔴 CẤM theo task: đụng BE/.cs · chạy dotnet (lane test song song) · route/menu mới (K4a) · sửa spec.

### §0.a SHA baseline 5 cặp KHKK FE (TRƯỚC khi sửa) — cả 5 IDENTICAL

| File (`fe-*/src/…`) | admin (12 ký tự đầu SHA256) | user | Trạng thái |
|---|---|---|---|
| `types/khkk.ts` | `8b757f5b1422` | `8b757f5b1422` | IDENTICAL |
| `pages/khkk/KhkkCreatePage.tsx` | `ea51e7626eb1` | `ea51e7626eb1` | IDENTICAL |
| `pages/khkk/KhkkDetailPage.tsx` | `c8ab2eadd3fd` | `c8ab2eadd3fd` | IDENTICAL |
| `pages/khkk/KhkkListPage.tsx` | `40a650bf6964` | `40a650bf6964` | IDENTICAL |
| `pages/khkk/KhkkWorkflowPanel.tsx` | `2b64144ef6e8` | `2b64144ef6e8` | IDENTICAL |

⇒ Chiến lược: sửa **fe-admin** → `cp` sang **fe-user** → đo lại SHA (khuôn cookie-cutter, 9× proven). Không có UX riêng 2 app trong 5 file này (đối chứng: baseline identical).

### §0.b Hình dạng DTO BE ĐỌC TỪ MÃ THẬT (không đoán — task ép)

- `ContractSigningPlanListItemDto` `ContractSigningPlanFeatures.cs:58-64`: append-CUỐI **`int ApprovalGroup`** + **`decimal TotalApprovedAmount`** (vá-8 — Σ thứ 3).
- `ContractSigningPlanLineDto` `:66-79`: append-CUỐI **`Guid? CatalogEntryId`** + **`string? TenHangMuc`** (denorm).
- `ContractSigningPlanDetailDto` `:130-166`: append-CUỐI (SAU cả các List) **`int ApprovalGroup`** + **`bool BudgetFrozen`** (display-gate, KHÔNG phải cột DB).
- `ApprovedPeAwaitingPlanDto` `:169-191`: append-CUỐI **`List<int> ExistingGroups`** (nhóm của các plan `Phase != TuChoi`; rỗng = PE chưa có phiếu). Picker **KHÔNG còn loại PE-đã-có-phiếu** (`:1291-1295` gỡ vế `!Any(...)`).
- `CreateContractSigningPlanCommand` `:333-337`: `PeId, ApprovalWorkflowId, GhiChu?, **int? ApprovalGroup = null**` — **null ⇒ nhóm 1** (`:393`); validator `InclusiveBetween(1,8)` CHỈ khi có giá trị (`:349-353`).
- `ListContractSigningPlansQuery` `:700-705` `+int? ApprovalGroup`; controller expose **`?group=`** (BE lane §f).
- 409 message create (`:400-402`): *"Phiếu Duyệt NCC này đã có kế hoạch ký kết cho nhóm duyệt N{g}. Chọn nhóm duyệt khác, hoặc xoá/để bị từ chối kế hoạch cũ của nhóm này trước khi lập lại."*

> 🔴 **[lead @S165] p1 CHẾT ngay sau §0.b** — 4 dòng markup tool-call rò vào đuôi file (sub-class #53 MỚI:
> content-Write lẫn cú pháp invoke), lead đã cắt. Disk-truth p1: fe-admin 4 file M (types +69 · CreatePage +177
> mid-wire [unused: select-nhóm + nút-phụ + badge chưa vào JSX] · Detail +33 · List +1); fe-user CHƯA cp.
> p2 tiếp từ đây — append §1 trở xuống.
---

# p2 (relaunch @S165) — tiếp từ §0.b

## §1 Wire CreatePage — ĐO ĐĨA TRƯỚC, KHÔNG TIN SỔ

🔴 **Tiền đề của lead SAI MỘT PHẦN (đã đo lại bằng `git diff`, không đọc lại sổ):**
lead khai CreatePage "GIỮA CHỪNG WIRE, các khai báo chưa vào JSX". Đo thật trên đĩa
(`git diff -- fe-admin/src/pages/khkk/KhkkCreatePage.tsx`, 177 dòng +) thì **8/8 khai báo
lead liệt là unused đều ĐÃ có call-site trong JSX**:

(số dòng dưới = đo bằng `grep -n` trên file SAU khi p2 xong, không phải trí nhớ)

| Khai báo lead nói "chưa vào JSX" | Call-site THẬT trên đĩa (fe-admin) |
|---|---|
| `KHKK_APPROVAL_GROUPS` | `firstFreeGroup()` `:55` + `.map` dựng 8 `<option>` `:281` |
| `KHKK_APPROVAL_GROUP_DEFAULT` | `useState<number>(...)` `:62` |
| `KHKK_APPROVAL_GROUP_LABELS` | `title=` badge nhóm-đã-dùng `:232` + text `<option>` `:285` |
| `approvalGroup` / `setApprovalGroup` | body POST `:99` · `value=` select `:277` · `onChange` `:278` |
| `firstFreeGroup` | `selectPe()` `:131` + `const free` mỗi dòng PE `:180` |
| `groupTaken` | cảnh báo đỏ `:291-293` · `disabled` nút Lưu `:360` |
| `selectPe` | `onClick` dòng PE `:185` + `onClick` nút "Tạo thêm cho nhóm khác" `:240` |
| icon `Layers` / `Plus` | `<Layers>` label nhóm `:272` · `<Plus>` nút phụ `:241` |

⇒ **Suy đoán về nguyên nhân lệch:** p1 chết ở lượt Write CUỐI (rò markup tool-call), nên
lead nhìn thấy transcript dở-dang và kết luận theo Ý ĐỊNH đang thực thi, không theo đĩa.
Đây đúng khuôn "WRITE-lane garble ≠ mất code" (MEMORY S114) — **code đã LAND, chỉ báo cáo
chết**. p2 KHÔNG viết lại CreatePage (viết lại = phá thứ đang đúng).

**Nội dung K2 mà CreatePage đã có (đọc để xác nhận đúng spec, không phải đọc để chép):**
- vá-4: `<select id="approvalGroup">` N1–N8, default 1 (`KHKK_APPROVAL_GROUP_DEFAULT`),
  option nhóm đã dùng bị `disabled` + hậu tố "(đã có kế hoạch)"; header section đổi
  "Quy trình duyệt" → "Nhóm duyệt & quy trình", lưới 1→2 cột `md:grid-cols-2`.
- body POST `approvalGroup` gửi TƯỜNG MINH (không dựa default ngầm `CreateCommand:337`).
- OG-7 UI-hướng-1: dòng PE **không còn bị ẩn** khi đã có kế hoạch → hiện hàng chip
  `N{g}` (nhóm đã dùng) + nút phụ **"Tạo thêm cho nhóm khác"**; hết 8 nhóm → chữ
  "Đã dùng hết 8 nhóm duyệt" (không mời bấm vào thứ chắc chắn 409).
- `<button>` PE bọc trong `<div key>` phụ vì nút phụ **không được lồng trong `<button>`**
  (HTML không hợp lệ) — p1 đã xử đúng.
- Rào cứng vẫn ở BE (`Features.cs:395-402` 409); FE chỉ `disabled` nút Lưu khi `groupTaken`.

⇒ **§1 = 0 dòng sửa mới.** Việc thật của p2 dồn vào §2 (ListPage) + §3 (cp) + §4 (build/SHA).

## §2 Detail / List

**KhkkDetailPage — cũng ĐÃ LAND ĐỦ (0 dòng sửa mới, +33 của p1):**
- badge `N{approvalGroup}` ở `PageHeader actions`, ĐỨNG TRƯỚC badge phase, `title` = nhãn
  nghiệp vụ (`:298-304`), guard `!= null` cho payload cache cũ.
- cột **"Hạng mục"** trong bảng Lines (`<th>` `:460`, `<td>` `:472` — `tenHangMuc` rỗng ⇒
  `—` xám) + 1 câu chú thích "ô gán thuộc bước Lines-editor" (K4b) ở cuối bảng `:485`.
- badge `🔒 Giá đã chốt tại thời điểm duyệt` khi `budgetFrozen` `:443` — gắn qua prop `actions`
  của `SectionCard`. ✅ đã kiểm: `actions?: ReactNode` là prop **CÓ SẴN** `:63` (không
  phải p1 tự bịa), render `{actions && <span className="ml-auto">…}` `:82`.

**KhkkListPage — ĐÂY LÀ CHỖ THẬT SỰ DỞ (p1 chỉ kịp thêm dòng import).** p2 sửa:
1. `:240-246` badge `N{g}` **trong ô "Mã kế hoạch"**, không đẻ cột riêng — bảng này đã
   cuộn ngang ở 1366 khi có cây GĐ 19rem bên trái (ghi chú S162 ngay trong file); guard
   `!= null` + `title` = `KHKK_APPROVAL_GROUP_LABELS[k.approvalGroup]` `:243` ⇒ **import
   treo đã có người dùng** (đây chính là dòng làm build đỏ TS6133 nếu bỏ mặc).
2. `:190` +cột **"Tổng duyệt"** (Σ `totalApprovedAmount`) và `:271-273` ô giá trị
   (emerald, `tabular-nums`). Phiếu chưa duyệt BE trả **0** ⇒ ép hiện `—` (`k.totalApprovedAmount ? … : '—'`),
   không in "0 đ" gây đọc nhầm "đã duyệt 0 đ".
3. `colSpan={7}` → **`{8}`** hai chỗ (hàng "Đang tải..." `:198` + hàng rỗng `:205`) —
   quên chỗ này thì 2 hàng đó thụt 1 ô, lỗi câm không ai thấy tới lúc bảng rỗng.
   *(Cột này KHÔNG có trong gạch đầu dòng của lead, nhưng là **vá-8 spec-cum1** — "list
   thêm Σ Approved sau freeze để list/detail không lệch số" — và BE đã trả field
   `ListItemDto:64`; khai lệch ở phần cuối.)*
- ❌ **KHÔNG** thêm ô lọc `?group=` ở list: K2 là CHỦ tham số nhưng **chỗ lọc thuộc K4b/K5**
  (leaf nhóm + cây 8 folder) — đẻ chỗ lọc thứ hai bây giờ là giẫm chân wave sau.

## §3 cp sang fe-user (khuôn cookie-cutter)

`cp` 4 file admin → user, KHÔNG gõ tay lần hai (gõ tay = đẻ lệch không ai thấy):
`types/khkk.ts` · `pages/khkk/{KhkkCreatePage,KhkkDetailPage,KhkkListPage}.tsx`.

- **`KhkkWorkflowPanel.tsx` KHÔNG cp** vì K2 không sửa nó — và đó là **đối chứng dương**
  của phép đo §4: hash panel sau lượt này phải BẰNG ĐÚNG baseline §0.a `2b64144ef6e8`.
  Nếu nó cũng "identical" mà lệch baseline thì phép đo của tôi vô nghĩa.
- Không import mới nào ngoài `lucide-react` (`Layers`,`Plus`) + const trong chính
  `types/khkk.ts` đã cp ⇒ không có rủi ro "fe-user thiếu component" (bẫy Card/Badge quen).
- `git diff --stat` **ĐỐI XỨNG 2 app**: `269 insertions(+), 36 deletions(-)`, từng file
  khớp số dòng (177 / 33 / 26 / 69). Lệch số ở đây = cp sót, không cần đợi tới SHA.

## §4 Build ×2 + SHA-pair — TRỌNG TÀI

`npm run build` = **`tsc -b && vite build`** (`package.json:11` cả 2 app) và
`tsconfig.app.json:19 noUnusedLocals: true` ⇒ **build LÀ phép thử TS6133**: import treo
`KHKK_APPROVAL_GROUP_LABELS` ở ListPage chắc chắn làm đỏ, không phải "IDE dọa" (#68).

| App | Kết quả | Bundle | Thời gian |
|---|---|---|---|
| fe-admin | ✅ PASS 0 TS error | `dist/assets/index-DORfkFnc.js` 1,722.60 kB | 23.10s |
| fe-user | ✅ PASS 0 TS error | `dist/assets/index-qYI2yszp.js` 1,638.80 kB | 8.80s |

Warning cả 2 app = **pre-existing**, không phải do lượt này: `@import` phải đứng trước
rule khác (font Google trong CSS) · chunk > 500 kB · `realtime.ts` vừa dynamic vừa
static import từ `NotificationBell`.

**SHA256 (`Get-FileHash`, 12 ký tự đầu) — 5/5 cặp IDENTICAL:**

| File (`fe-*/src/…`) | admin | user | Cặp | So baseline §0.a |
|---|---|---|---|---|
| `types/khkk.ts` | `72A71451B98F` | `72A71451B98F` | ✅ | ĐỔI (từ `8b757f5b1422`) |
| `pages/khkk/KhkkCreatePage.tsx` | `677F4AB803B0` | `677F4AB803B0` | ✅ | ĐỔI (từ `ea51e7626eb1`) |
| `pages/khkk/KhkkDetailPage.tsx` | `1863153B4F99` | `1863153B4F99` | ✅ | ĐỔI (từ `c8ab2eadd3fd`) |
| `pages/khkk/KhkkListPage.tsx` | `E892310434F2` | `E892310434F2` | ✅ | ĐỔI (từ `40a650bf6964`) |
| `pages/khkk/KhkkWorkflowPanel.tsx` | `2B64144EF6E8` | `2B64144EF6E8` | ✅ | **BẰNG baseline** (chứng: đo đúng thứ, không đụng file ngoài phạm vi) |

## §5 Ranh giới đã giữ + khoảng hở khai thật

**Giữ đúng 5 điều cấm của task:** 0 lệnh `dotnet` · 0 file `.cs` bị ghi (13 `.cs` đang M/??
trong `git status` là **lane BE** — Mig `20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime`
+ `ContractSigningPlanFeatures.cs` + 3 test file; tôi chỉ ĐỌC DTO, không Edit) · 0 route
mới (`App.tsx` không nằm trong diff) · 0 menu key mới (`lib/menuKeys.ts`,
`components/Layout.tsx` không nằm trong diff — K4a giữ chỗ đó) · 0 dòng spec bị sửa.

**Khoảng hở / lệch — khai để lead phán, không tự nuốt:**

1. 🔴 **Tiền đề task sai một phần** (§1): CreatePage + DetailPage đã LAND ĐỦ trên đĩa từ
   p1; chỉ ListPage dở. Việc thật của p2 ≈ 1/4 mô tả. Tôi **không viết lại** phần đã đúng.
   *(Bài học tái xác nhận: báo cáo chết ≠ code chết — đo `git diff` trước khi gõ.)*
2. ⚠️ **Cột "Tổng duyệt" (Σ Approved) là việc TÔI THÊM ngoài gạch đầu dòng của lead.**
   Căn cứ: **vá-8 spec-cum1** (*"list thêm Σ Approved sau freeze để list/detail không lệch
   số"*) + BE đã trả `ListItemDto:64` + p1 đã khai field trong types kèm đúng lý do đó.
   Không thêm thì field nằm chết. **Nếu lead thấy quá tay: gỡ = xoá 1 `<th>` + 1 `<td>` +
   trả `colSpan` 8→7, không ảnh hưởng gì khác.**
3. ⚠️ **`existingGroups` chỉ đọc `Phase != TuChoi`** (BE). Phiếu **xóa mềm** có rơi khỏi tập
   này không thì tôi KHÔNG kiểm được từ FE. Nếu BE không loại phiếu deleted thì user xoá
   phiếu nhóm 2 xong vẫn thấy N2 bị khoá trong ô chọn (rào 409 BE mới là lời cuối). Câu này
   thuộc lane BE — nêu để có người soi, không tự sửa.
4. ⚠️ **Nhãn 8 nhóm còn treo [OG-6]** (owner soát sau trên UI). Đang nằm 1 chỗ
   `KHKK_APPROVAL_GROUP_LABELS` × 2 app ⇒ K4a đổi nhãn menu thì sửa đúng 1 const, đừng vá rải.
5. ℹ️ **Không tự bấm được UI** (acceptance vá-4 "tạo phiếu nhóm 2 TỪ UI ×2 app" cần người
   chạy app + login). Tôi dừng ở mức build-PASS + đọc mã; **acceptance UI vẫn ĐANG MỞ**,
   đừng đọc "build PASS" thành "đã UAT".
6. ℹ️ Chỗ lọc `?group=` **cố ý chưa làm** (thuộc K4b/K5) — nêu lại ở đây để wave sau không
   tưởng là sót.

END p2 — 4 file × 2 app · build ×2 PASS · SHA-pair 5/5.
