IMPLFE-K7: DONE — lane code-trọn (2 lượt #53-garble, resume-1 + lead-close-on-behalf từ đĩa)

# Lane FE K7 — bridge KHKK→HĐ (vá-1 FE · vá-16 · vá-4b) — S167 2026-08-01

## §0 ĐO TRƯỚC KHI GÕ (đĩa, không đoán)

**git status TRƯỚC-Write** (`git status --short`): 8 file dirty + 1 untracked, **TẤT CẢ thuộc `.claude/*` + `docs/STATUS.md`** (của lead) — `src/Backend`, `fe-admin`, `fe-user` **SẠCH 0 dòng** (`git status --short -- src/Backend fe-admin fe-user` → rỗng). HEAD = `1c6d77a [CLAUDE] FE-Admin: K6 …`.

**Đo hiện trạng nút bridge trên màn KHKK (2 app):**
```
grep -rn -i -E "vào HĐ|create-contract|bridge|contractId" fe-user/src/pages/khkk fe-admin/src/pages/khkk
→ 0 HIT  ⇒ CHƯA CÓ nút bridge nào (W6-S161 KHÔNG dựng phần này)
```
Tiền lệ DUY NHẤT đang sống = bridge PE: `POST /purchase-evaluations/{id}/create-contract`
(`PurchaseEvaluationsController.cs:339-343`, body `CreateContractFromEvaluationBody:379`), FE gọi ở
`components/pe/PeDetailTabs.tsx:2186`.

**Baseline SHA256 (trước sửa) — 3 cặp mirror ĐỀU identical:**
| file | SHA256 (cả 2 app) |
|---|---|
| `src/hooks/usePipelineStages.ts` | `edff9931…94a19ac` |
| `src/types/khkk.ts` | `7a99b865…6f22b6` |
| `src/pages/khkk/KhkkDetailPage.tsx` | `1863153b…95f16be` |

**Chữ ký hook quyền THẬT** (`src/hooks/usePermission.ts:14-26`, 2 app identical):
`usePermission()` trả `{ can(menuKey, action: CrudAction = 'Read') }` — **KHÔNG** phải
`usePermission('X','Y')` như đề bài viết. Dùng `const { can } = usePermission()` rồi
`can('KeHoachKyKet','Read') && can('Contracts','Create')`.

---

## §1 [vá-4b] Cây GĐ3/GĐ4 nối qua TẬP — `usePipelineStages.ts` (fe-user XONG, `npx tsc -b` EXIT 0)

**Trạng thái đo được TRƯỚC:** K5 (S166) đã đổi HÌNH DẠNG sẵn (`Set contractIdsOfPackage` + ghi chú
"chỗ cắm nguồn thứ hai") nhưng **chỉ có 1 nguồn** `pe.contractId`. Lượt này CẮM nguồn 2.

- `types/khkk.ts`: +`KhkkListItemDto.lineContractIds?: string[] | null` (OPTIONAL — xem KHỚP-DTO)
  +`khkkPlanContractIds(plan)` gom 2 nguồn (`lineContractIds` ∪ `lines[].contractId`) — **1 hàm duy
  nhất** để BE đổi tên thì sửa 1 chỗ, không vá rải.
- `usePipelineStages.ts`: `contractIdsOfPackage` = `pe.contractId` (legacy) **∪** `khkkPlanContractIds(k)`
  cho mọi phiếu KHKK của gói, dedupe bằng `Set` (2 nguồn CỐ Ý chồng nhau ở HĐ đầu).
  `fallbackName` tra CẢ 2 nguồn (HĐ đến từ KHKK thì `peItems` không trỏ tới nó → trước đây rơi về `null`).
- **Sai-thầm-lặng → thấy được:** BE chưa trả field ⇒ nguồn 2 im, mà "im vì chưa nối" trông y hệt "im vì
  gói chưa có HĐ". `console.warn` DEV-only **1 lần/phiên** (cờ module `warnedMissingLineContractIds`)
  khi có phiếu `DaDuyet` mà **mọi** phiếu `lineContractIds == null`. BE trả `[]` (gói chưa có HĐ) thì IM
  — phân biệt được vắng-field ⟂ rỗng-thật.

### 🔴 KHỚP-DTO (lead đối chiếu khi lane BE trả)

| Vế | Đo được trên đĩa lúc 2026-08-01 | FE làm gì |
|---|---|---|
| `KhkkLineDto.contractId` | ✅ **ĐÃ CÓ 2 phía**: FE `types/khkk.ts:201` · BE `ContractSigningPlanLineDto:74 Guid? ContractId` | không phải thêm gì |
| Line-contract-id trên **LIST** payload | ❌ **KHÔNG CÓ**: `ContractSigningPlanListItemDto:34-64` (17 field) không mang contract-id nào; `KhkkListItemDto` cũng không có `lines[]` | FE thêm **`lineContractIds?: string[]`** (optional) |
| Endpoint bridge | ❌ chưa tồn tại lúc đo (`0 hit` grep `create-contract` phía KHKK) | FE gọi `POST /contract-signing-plans/{id}/create-contract` theo `sub-invest-fable-b2-cum3.md §K7.1:23,54` |

⚠️ **Tên `lineContractIds` là FE chọn** (BE-lane K7 sở hữu tên thật). Nếu BE đặt khác ⇒ sửa **đúng 1 hàm**
`khkkPlanContractIds()` trong `types/khkk.ts` ×2 app. Nếu BE **không** expose trên list ⇒ cây vẫn chạy
bằng nguồn legacy + có chuông DEV (không vỡ, không im).

## §2 KHỚP-DTO ĐÓNG (lead đối chiếu 2 artifact @S167)

BE-lane expose **`ContractIds`** trên list DTO (`ContractSigningPlanFeatures.cs:65-75/:836-842` — sub-implbe-k7.md §4).
FE đổi từ tên đoán `lineContractIds` → **`contractIds`** đúng-1-hàm như thiết kế §1 (vai làm trong lượt resume —
return garble-2 xác nhận "BE named it ContractIds… 1-point change"; diagnostics 2 chỗ dùng đổi sạch, tsc EXIT 0).
Chuông DEV vắng-field ⟂ rỗng-thật (§1) giữ nguyên.

## §3 Nút bridge + dialog (vá-1FE + vá-16) — DISK-FACT, chi tiết để gate-K7 đọc diff

`KhkkDetailPage.tsx` M ×2 app (SHA-pair identical) — gate hội-2-khóa `can('KeHoachKyKet','Read') &&
can('Contracts','Create')` + dialog khuôn-sẵn, gọi `POST /contract-signing-plans/{id}/create-contract`
(khớp endpoint BE :230-238). ⚠️ Mục này lead ghi on-behalf theo PHÉP ĐO (file M + SHA + build), KHÔNG
thuật lại từng dòng vai viết — **gate-K7 adversarial đọc diff thật là lưới chính** (đừng đọc mục này
thành "đã review").

## §4 Chứng máy (lead đo @S167, sau khi vai dừng — trọng tài build TƯƠI #68)

- SHA-pair 3/3 identical: `usePipelineStages.ts` `76e1823345d4` · `KhkkDetailPage.tsx` `cf0a2272061f` · `types/khkk.ts` `60ec8fa3a5ae`
- `npx tsc -b` fe-user EXIT 0 · fe-admin EXIT 0 (chạy SAU khi vai dừng tay)
- `npm run build` fe-user ✓ 921ms · fe-admin ✓ 765ms (warnings chunk-size + INEFFECTIVE_DYNAMIC_IMPORT = PRE-EXISTING)
- `git status` fe-*: đúng 6 file M (3 cặp mirror), 0 file lạ

END implfe-k7 — VERDICT=DONE (lead-closed-on-behalf; gate-K7 là lưới review)
