# impl-fe K1

## Tiến độ
- Call-1: khung sub-file. OK.
- Đọc xong nguồn:
  - `sub-invest-fable-b2-cum1.md:53-57` §K1.d — kind mới `contract-catalog` vào KIND_CONFIG ×2 app, endpoint `api/catalogs/contract-catalog`, route `:kind` sẵn ⇒ 0 route mới.
  - BE THẬT: `src/Backend/SolutionErp.Api/Controllers/ContractCatalogController.cs:21` `[Route("api/catalogs/contract-catalog")]`, `[Authorize]` class + `[Authorize(Roles="Admin")]` 3 verb ghi (`:39,:47,:56`). List query `groupCode/approvalGroup/isActive/q` (`:27-30`).
  - DTO `ContractCatalogFeatures.cs:27-38` = 11 field {Id, Code, TenVi, TenEn?, GroupCode, ApprovalGroup int, SignerRole, InitialsChain?, NduqNote?, IsActive, SortOrder}. Create/Update command = 10 field (không Id) `:96-106` / `:155-166`. `ApprovalGroup` InclusiveBetween(1,8) `:118`. PUT check `id != body.Id` → 400 ⇒ FE phải gửi kèm `id`.
  - Khuôn FE: `fe-user/src/pages/master/CatalogsPage.tsx` 321 dòng — SHA **IDENTICAL** với fe-admin (`6f482614…`) ⇒ viết 1 lần rồi `cp`.
  - Menu: BE `MenuKeys.cs:28` const `ContractCatalog` (VÀO `All` :173) + `DbInitializer` seed row `(ContractCatalog, "Danh mục Hợp đồng", parent Catalogs, order 5, icon "FileSpreadsheet")`.
  - `Layout.tsx` + `menuKeys.ts` DIVERGE 2 app (SHA khác) ⇒ mirror TAY, không cp.
- Sửa `CatalogsPage.tsx` fe-user (kind thứ 5) → `cp` fe-admin → menuKeys ×2 → Layout staticMap ×2 → build ×2. XONG.

## File tạo/sửa (6 file, 0 file mới — 3 file/app, đối xứng)

| File | Δ | Nội dung |
|---|---|---|
| `fe-user/src/pages/master/CatalogsPage.tsx` | +201/−68 (321→454 dòng) | kind thứ 5 |
| `fe-admin/src/pages/master/CatalogsPage.tsx` | +201/−68 | `cp` y nguyên — SHA-pair |
| `fe-user/src/lib/menuKeys.ts` | +4 | `ContractCatalog: 'ContractCatalog'` |
| `fe-admin/src/lib/menuKeys.ts` | +4 | idem (2 file DIVERGE sẵn ⇒ edit tay, không cp) |
| `fe-user/src/components/Layout.tsx` | +4 | staticMap `ContractCatalog: '/master/catalogs/contract-catalog'` |
| `fe-admin/src/components/Layout.tsx` | +4 | idem |

### Chi tiết `CatalogsPage.tsx` (khuôn KIND_CONFIG, ADD-KIND playbook)
1. `type Kind` += `'contract-catalog'` · `KINDS` += (sub-tab thứ 5 tự hiện).
2. `FieldDef` nới 3 thứ: `type` += `'number' | 'select'`, `options?: {value:number;label:string}[]`, `numeric?: boolean`.
3. `KIND_CONFIG['contract-catalog']` — icon `FileSpreadsheet` (KHỚP icon BE seed), 10 field ĐÚNG 10 field của `CreateContractCatalogEntryCommand`: code · groupCode · tenVi · tenEn · approvalGroup(select N1–N8, numeric) · sortOrder(number, numeric) · signerRole · initialsChain · nduqNote(textarea) · isActive(checkbox). 4 field `*` = 4 `NotEmpty` của validator BE (`ContractCatalogFeatures.cs:114-120`).
4. `columns` (trước giờ là config CHẾT, không ai đọc) → thành nguồn `<thead>` cho nhánh contract-catalog: 8 cột {Mã · Tên VN · Tên EN · Nhóm · Nhóm duyệt · Vai ký chính · Ký nháy · Trạng thái}.
5. `<tbody>` nhánh riêng (4 kind cũ giữ NGUYÊN byte cell cũ) · `colSpan` hardcode 6 → `colCount` tính theo kind.
6. `CatalogRow.name` `string` → `string?` (contract-catalog dùng `tenVi`) + helper `rowLabel()` cho confirm xóa (trước là `row.name` → sẽ in "undefined").
7. `ApprovalGroupBadge` — Pattern 14: 8 chuỗi class ĐẦY ĐỦ trong `APPROVAL_GROUP_BADGE`, cycle `(value-1) % 8`, KHÔNG interpolate.
8. `buildBody` — field `numeric` ép `Number()`, rỗng → **0 chứ không null** (BE `int` non-nullable, `null → int` là System.Text.Json ném). Nhánh cũ (`'' | undefined → null`) giữ nguyên cho field còn lại.
9. `openCreate` — select mặc định option đầu (N1) để không POST rỗng dính `InclusiveBetween(1,8)`.
10. `PageHeader description` đổi theo kind (giữ signature title/description, không thêm prop).

### Pattern 16-bis — 4 chỗ
| # | Chỗ | Trạng thái |
|---|---|---|
| 1 | Page/types | ✅ `CatalogsPage.tsx` ×2 (0 file types mới — row là `Record<string, unknown>`) |
| 2 | `App.tsx` Route | ✅ **KHÔNG cần** — `/master/catalogs/:kind` có sẵn (`fe-user/src/App.tsx:63`, `fe-admin/src/App.tsx:69`), kind mới đi chung |
| 3 | `lib/menuKeys.ts` | ✅ ×2 |
| 4 | `Layout.tsx` staticMap | ✅ ×2 — chỗ DỄ MISS (gotcha #50) |

## Kết quả build
- `fe-user`: **PASS** — `tsc -b && vite build`, 1950 modules, `dist/assets/index-C5SIaqt5.js` 1,634.97 kB, ✓ built in 21.57s, **0 TS error**.
- `fe-admin`: **PASS** — `dist/assets/index-DLUxeB3d.js` 1,718.77 kB, ✓ built in 9.52s, **0 TS error**.
- Warning: `@import` phải đứng trước rule + chunk >500 kB + INEFFECTIVE_DYNAMIC_IMPORT `realtime.ts` — **pre-existing cả 3**, không phải do K1.
- SHA-pair `CatalogsPage.tsx`: `d71a9e3bb17b835b85b4f3467905af2ab414f52e3ec0c58e79d55ef57673985a` ×2 IDENTICAL (trước sửa: `6f482614…` ×2).
- Chứng Pattern 14 có răng (class KHÔNG bị purge) — grep CSS đã build: `bg-sky-50` `bg-violet-50` `bg-teal-50` `bg-orange-50` `border-indigo-200` `text-rose-700` có mặt ở CẢ `fe-user/dist/assets/index-BjUevXzu.css` LẪN `fe-admin/dist/assets/index-B62-9mVN.css`.
- `git diff --stat` đối xứng: 201/201 · 4/4 · 4/4 → 350 insert / 68 delete / 6 file.

## Ngoài phạm vi (KHÔNG đụng — đúng lệnh)
- 0 file `src/Backend/**` (chỉ Read `ContractCatalogController.cs` + `ContractCatalogFeatures.cs` để lấy shape).
- 0 file pipeline/khkk (K4/K5). 0 commit, 0 push.
- KHÔNG sửa `agent-memory/*` (spawn-record trả về qua `memoryDelta`, em main harvest).

END impl-fe-k1 — 6 file · build ×2 PASS
