# sub-implementer-frontend-w5 — W5 card "Nguồn gốc KHKK" (F1) + vế (88) ruột pre-fill

> Run `2026-08-10-S187-duyet-hd-tu-khkk` · vai `implementer-frontend` (WRITE, FE 2 app) · 2026-08-10.
> Đề bài: (1) card Nguồn gốc KHKK trong màn detail HĐ ×2 app; (2) ĐO vế (88) ruột pre-fill — làm TỐI THIỂU, 0 BE/0 types/0 schema; (3) mirror SHA-pair + `npm run build` ×2.
> Ghi TỪNG MỤC trong lúc làm (chống #53). File này là chân lý, return chỉ là tóm.

## §0 — ĐO ĐĨA TRƯỚC KHI GÕ (ground-truth, không chép đề bài)

**0a. Shape `ContractSource` — ĐỌC TYPES LÀM CHUẨN (đề bài dặn, xác nhận đúng):**
`fe-admin/src/types/contracts.ts:196-208` = `maKeHoach: string|null` · `planId: string` · `approvalGroup?: number|null` ·
`tenHangMucs: string[]` · `approvedAmountTotal?: number|null` · `purchaseEvaluationId?: string|null`.
Cắm vào `ContractDetail` ở `:182` dạng `source?: ContractSource | null` — comment `:174-181` khai rõ **2 nghĩa
KHÁC NHAU**: `undefined` = payload/cache CHƯA có W2; `null` = BE nói "HĐ này không có nguồn KHKK".
⇒ Điều kiện render phải là `c.source != null` (bắt CẢ 2), KHÔNG dùng `c.source !== null`.

**0b. Vỏ trang + nơi cắm card:**
- `components/contracts/ContractDetailContent.tsx` (257 dòng) = ruột dùng chung, **5 host**: admin
  `ContractDetailPage:28` + `ContractsListPage:254`; user `ContractDetailPage:28` + `MyContractsPage:231` + `InboxPage:347`.
  Sửa 1 file ⇒ card hiện ở cả 5 chỗ, 0 nhân bản.
- Khối cùng trang KHÔNG dùng shadcn `Card` (2 app **đều không có** `components/ui/Card.tsx` — đo `ls ui/`):
  khuôn thật = `<section className="rounded-lg border border-slate-200 bg-white p-5">` + `<h2 className="mb-3 …">`.
  ⇒ card mới bám ĐÚNG khuôn đó, không đẻ primitive mới.

**0c. Route đích (grep `App.tsx` ×2 app) — CÓ SẴN cả 2:**
- `/khkk/:id` (admin `:110` · user `:84`) → `KhkkDetailPage`.
- `/purchase-evaluations/:id` (admin `:91` · user `:76`) → `PurchaseEvaluationDetailPage`.
- ✅ `?group=` KHÔNG phải param chết: `KhkkDetailPage.tsx:50-52` ×2 app đọc `searchParams.get('group')`,
  validate **REGEX CHUỖI** `/^[1-8]$/` (không `Number()`) rồi lấy làm `backToList` = `/khkk/list?group=n`
  ⇒ giữ chỗ đứng + sidebar sáng đúng leaf nhóm. Truyền `?group=` là CÓ NGƯỜI TIÊU THỤ.

**0d. Nhãn nhóm — KHÔNG đẻ map thứ 2 (bài học S166-K4a):** `KHKK_APPROVAL_GROUP_LABELS`
(`types/khkk.ts:140-149`) đã tồn tại và **SHA-pair identical ×2 app** (`29aa5633…`) ⇒ import lại, không chép chuỗi.

**0e. Palette — đo trước khi gõ class:** `index.css` SHA-pair identical (`62c26bf6…`); `@theme` chỉ định nghĩa
`violet` ở stop **50/100/500/600/700** (`:50-55`), KHÔNG có 300/800. `PipelineStageFolders.tsx:41-46` chốt
tông theo GĐ: **GĐ1 PE = brand · GĐ2 KHKK = violet · GĐ3 HĐ = amberx · GĐ4 = teal**.
⇒ card nguồn-gốc dùng tông **violet** (đúng GĐ2), link PE dùng **brand** (đúng GĐ1), và **chỉ xài stop có thật**.
🔸 Ghi nhận đi ngang (KHÔNG sửa, ngoài scope): `KhkkDetailContent.tsx:477` dùng `violet-300`/`violet-800` —
2 stop này rơi về violet MẶC ĐỊNH của Tailwind (khác hệ màu `@theme`), lệch tông nhẹ; pre-existing.

**0f. Baseline hash 2 file sẽ chạm (trước khi sửa) — cả 2 vốn đã IDENTICAL ×2 app:**
`ContractDetailContent.tsx` = `cd5ce88f211d1a801cce0626e5f6a339233cadc4` (admin ≡ user)
`pages/contracts/ContractCreatePage.tsx` = `5ec906195be1d605b16f61ada8b3113bae8ab799` (admin ≡ user)
🔸 `types/contracts.ts` KHÁC nhau ×2 app (`66b2bd54` ⟂ `3004a6e7`) — diff = **bảng màu badge phase**
(fe-user brand-tinted `-800`, fe-admin `-700`, chú thích 2026-06-16 của anh). Lệch CÓ CHỦ ĐÍCH, **không phải
việc của W5** và đề bài CẤM đụng types ⇒ không chạm.

## §1 — Card "Nguồn gốc Kế hoạch ký kết" (F1)

**Landed:** `components/contracts/ContractDetailContent.tsx` ×2 app — 1 file, 5 host (admin
`ContractDetailPage`+`ContractsListPage`; user `ContractDetailPage`+`MyContractsPage`+`InboxPage`).
Vị trí: NGAY SAU `<section>` "Thông tin HĐ", TRƯỚC "Góp ý" (người mở HĐ cần biết nó đẻ từ đâu
trước khi đọc góp ý/chi tiết). +3 import: `Link` (react-router-dom) · `GitBranch` (lucide, đã
dùng sẵn ở `WorkflowSummaryCard.tsx:1` nên chắc chắn có trong version đang cài) ·
`KHKK_APPROVAL_GROUP_LABELS` (`@/types/khkk`).

**Ruột card (5 mảnh, đúng đề bài):**
1. Mã `src.maKeHoach` → `<Link to={`/khkk/${planId}${group ? `?group=${group}` : ''}`}>`.
2. Badge nhóm `Nhóm N{g} · {LABELS[g]}` khi `approvalGroup != null` — **import map, 0 chuỗi chép**.
3. `tenHangMucs` → chip list + **đếm ngay trên nhãn** `Hạng mục đưa vào HĐ (n)`.
4. `Σ approvedAmountTotal` qua **chính `fmtMoney` của file** (`v.toLocaleString('vi-VN') + ' VND'`)
   — không đẻ formatter thứ 2.
5. Link "Mở phiếu Duyệt NCC →" `/purchase-evaluations/{peId}` khi có; không có ⇒ `—` (không giấu ô).

**3 quyết định có bằng chứng (không phải khẩu vị):**
- **Điều kiện render `src != null`** (loose) — bắt CẢ `undefined`; `types/contracts.ts:174-182`
  khai 2 nghĩa khác nhau, cả 2 đều không có gì để vẽ.
- 🔴 **KHÔNG link khi `maKeHoach === null`** (lệch 1 chữ so với đề bài — xem FLAG-1 §5).
  Đo: BE lấy mã qua query CÓ global filter (`ContractFeatures.cs:738-740`) ⇒ `maKeHoach === null`
  ⇔ phiếu cha **đã xoá mềm**; mà màn `/khkk/:id` đọc bằng `GetContractSigningPlanQuery`
  (`ContractSigningPlanFeatures.cs:957-962`) **KHÔNG** `IgnoreQueryFilters` (grep cả folder: hit
  `IgnoreQueryFilters` DUY NHẤT ở `:1383` = màn "Đã xoá") ⇒ link đó **chắc chắn 404**. Thay bằng
  chữ NHÌN THẤY ĐƯỢC "(phiếu đã xoá) — không mở được phiếu gốc", **không** giấu sau tooltip
  (bài S182: thông tin PHÂN BIỆT không được nằm sau hover).
- **Tông màu:** violet = GĐ2 (KHKK), brand = GĐ1 (PE) theo `PipelineStageFolders.tsx:41-46`;
  chỉ dùng stop violet CÓ THẬT trong `@theme` (50/100/500/600/700). `violet-300/800` ở
  `KhkkDetailContent.tsx:477` = pre-existing, KHÔNG sửa (ngoài scope).

**Đối chứng shape (không tin types một mình):** đọc thẳng BE `ContractSourceDto`
(`Contracts/Dtos/ContractDtos.cs`) = `PlanId · MaKeHoach · ApprovalGroup · PurchaseEvaluationId ·
TenHangMucs · ApprovedAmountTotal` ⇒ camelCase khớp TỪNG TÊN với `ContractSource` FE. BE **có
điền thật** (`ContractFeatures.cs:720-748` + `Source: sourceDto` `:839`), không phải field chết.
🔸 Lệch nhỏ khai luôn: BE `ApprovedAmountTotal` là `decimal` **non-null**, FE khai `?: number|null`
(siêu-tập phòng thủ) ⇒ card vẫn null-check, hiện `—` nếu vắng.

## §2 — Vế (88): ĐO bridge-map ⟂ màn edit Nháp

(đang ghi…)

## §3 — Mirror ×2 app + SHA-pair

(đang ghi…)

## §4 — Build ×2 + control-âm

(đang ghi…)

## §5 — FLAG / nợ treo

(đang ghi…)
