# sub-implementer-frontend-w6 — W6-FE màn duyệt V2-trạm HĐ + Designer type-3 family-slice

> Lane: implementer-frontend (WRITE, FE-only). Run S187 YC-023 GĐ3.
> Đề bài: (1) `ContractWorkflowPanel` ×2 app mirror `KhkkWorkflowPanel` owner-trim 2 nút ·
> (2) wire vào `ContractDetailContent.tsx` ×2 app dual-render V1/V2 · (3) Designer type-3
> family-slice 7 mã `HD-*` khuôn KHKK nhóm · (4) SHA-pair + `npm run build` ×2.
> Đáp án owner (84) = **2 nút Duyệt / Trả lại**, KHÔNG render Từ chối (máy BE giữ).

## §0 — ĐO ĐĨA TRƯỚC KHI GÕ (tiền-đề đề bài vs thực tế)

Baseline SHA trước khi chạm (đo lúc mở lane):

| file | SHA256 (12 đầu) | ghi chú |
|---|---|---|
| `fe-{admin,user}/src/components/contracts/ContractDetailContent.tsx` | `112ccbcae77d` | **ĐANG PAIR** (W5 vừa land, cả 2 `M` trong git status) |
| `fe-{admin,user}/src/components/contracts/WorkflowHistoryPanel.tsx` | `efd01a3936b3` | pair, KHÔNG chạm wave này |
| `fe-{admin,user}/src/components/khkk/KhkkWorkflowPanel.tsx` | `e7b294c9dbad` | khuôn nguồn, pair ⇒ primitives 2 app parity |
| `fe-admin/src/types/contracts.ts` | `3d2da3bd2993` | 🔴 **KHÔNG pair** với fe-user (`1d4d649f87ae`) |
| `fe-user/src/types/contracts.ts` | `1d4d649f87ae` | lệch PRE-EXISTING: bảng `ContractPhaseColor` khác tông/app (admin -700, user -800 + brand). Đề bài CẤM chạm file này ⇒ giữ nguyên, KHÔNG "vá" thành pair. |

### Đ0.1 — Wire THẬT của "endpoint duyệt V2" (đề bài nói "grep approve/return endpoints")

🔴 **KHÔNG có endpoint V2 riêng.** `ContractsController.cs` chỉ có **một** cửa transition:
`POST /api/contracts/{id}/transitions`, body `TransitionContractBody(TargetPhase, Decision, Comment)`
(`ContractsController.cs:64-69` + `:244`). `ApproveV2Async` là **private** trong
`ContractWorkflowService.cs:230`, tới được bằng DISPATCH: `TransitionAsync` `:105` bắt
`fromPhase == ChoDuyet && decision == Approve`, rồi `:111` `if (contract.ApprovalWorkflowId is Guid awId)`
→ V2; else → V1 legacy. ⇒ FE **không** gọi đường mới, chỉ gọi đúng cửa cũ với đúng cặp
(targetPhase, decision):

- **Duyệt** → `{ targetPhase: 10 (ChoDuyet), decision: 1 (Approve), comment }`.
  Nhánh approve `:105` KHÔNG đọc `targetPhase` (BE tự tính phase kế: lên Cấp / lên Bước /
  `DaPhatHanh` + gen mã). Vẫn phải gửi 1 giá trị hợp lệ vì validator `IsInEnum()`
  (`ContractFeatures.cs:210`) ⇒ gửi `ChoDuyet` = trung tính, không hứa phase đích.
- **Trả lại** → `{ targetPhase: 98 (TraLai), decision: 2 (Reject), comment }`.
  Nhánh reject `:49-66`: `targetPhase == TuChoi` ⇒ TuChoi, **else ⇒ TraLai**. Gửi 98 tường minh
  thay vì "số nào cũng được miễn khác 99".

### Đ0.2 — 3 mảnh của khuôn KHKK KHÔNG port được (đo ra, không phải chọn cho gọn)

1. ❌ **Ô-tích "Cấp này KẾT THÚC"** — `TransitionContractBody` có ĐÚNG 3 field; `skipToFinal`
   là tham số C# default (`TransitionAsync(..., bool skipToFinal = false)`) **không nằm trên
   API surface** ⇒ render ô-tích = control chết, bấm xong không đổi gì.
2. ❌ **Badge "⚑ Duyệt thay CEO"** trên Cấp có `allowApproverFinalize` — 🔴 `ApproveV2Async`
   của HĐ **KHÔNG ĐỌC** `AllowApproverFinalize` một lần nào (`ContractWorkflowService.cs:230-407`;
   cờ duy nhất nó đọc là `AllowApproverSkipToFinal` `:335`, cho đường skip mà FE không gọi được).
   DTO có trả field (`ContractDtos.cs:138`) và chú thích của chính W2 nói "dùng hay không là
   quyết của W6". ⇒ Vẽ badge nói "duyệt ở đây là KẾT THÚC" là **nói dối**: duyệt xong vẫn chạy
   tiếp Cấp sau. Không render.
3. ❌ **Banner kết-thúc-sớm + meta `{code} v{NN} · {name}`** — `ContractDetail` **0 field**
   `endedByLevelFinalize` / `workflowCode` / `workflowVersion` / `workflowName` (đo cả type
   `types/contracts.ts:141-191`). Lấy `c.workflow.policyName` thế vào là ghép nhãn của
   **workflow policy V1** (`WorkflowSummary`) vào quy trình V2 — 2 vật khác nhau. Không render.
   (Cây thô đã inline sẵn trong detail ⇒ cũng KHÔNG gọi thêm `/approval-workflows-v2/{id}`,
   đúng lời W2: "KHÔNG đẻ đường thứ 3".)

### Đ0.3 — 2 khối KHKK cố ý bỏ vì TRÙNG chỗ đã có trên cùng trang

`ContractDetailPage` render `ContractDetailContent` **và** `WorkflowHistoryPanel`
(`WorkflowHistoryPanel.tsx:38-65` đã có "Lịch sử duyệt (N)" + `:67-73` "Lịch sử điều chỉnh").
⇒ panel mới KHÔNG đẻ bản thứ 2 của 2 khối đó. Khối "📎 File đính kèm khi duyệt" cũng bỏ:
HĐ không có đường `approval-attachments` riêng (đính kèm đi qua
`ContractAttachmentsSection` đã render sẵn trong `ContractDetailContent`).
⇒ Panel W6 = khối **1 (header) + 3 (stepper) + banner lượt-ai + 4 (2 nút + dialog)**.

### Đ0.4 — FLAG-BE (không chạm, báo lead)

🔴 **Nhánh Reject `ContractWorkflowService.cs:48-66` KHÔNG có guard đúng-trạm.** Approve V2 có
(`:268-279` match `ApproverUserId` ∈ Cấp đang chờ, Admin bypass); Reject thì **bất kỳ user đã
đăng nhập nào** POST `{decision:2}` là HĐ về `TraLai`. Panel W6 có `disabled` nút Trả lại cho
người ngoài Cấp, nhưng **đó là trang trí** (gotcha #82 — 2 tầng độc lập). Đúng vế "Đ2" mà
`sub-plan-review §7` đã treo cho W6 (phần BE `ContractWorkflowService.cs:48-66`). **Ngoài lane
FE ⇒ KHÔNG sửa, báo lead.**

---

## §1 — Việc đã làm

### 1.1 — Nối lượt chết: ĐO LẠI ĐĨA trước khi gõ (3/4 tiền-đề của lệnh nối SAI)

Lệnh nối nói *"import & cờ `isV2` đã khai ×2 app (unused — wire dở)"*. Đo tươi lúc mở lượt:

| đo | lệnh | kết quả |
|---|---|---|
| `ContractWorkflowPanel.tsx` ×2 | `git hash-object` | `48b72391` **=** `48b72391` — pair, đúng như brief |
| `ContractDetailContent.tsx` ×2 | `git hash-object` | admin `3148173d` ⟂ user `42009bd1` — **KHÔNG pair** |
| nội dung lệch | `git diff --no-index` | **+33/−1, đúng 3 khối W6** |
| `grep -n "ContractWorkflowPanel\|isV2"` ×2 app | ripgrep | **fe-user: 6 hit** (import + `isV2` + `!isV2 &&` header + `{isV2 && <ContractWorkflowPanel/>}`) · **fe-admin: 0 hit** |

⇒ Sự thật khác brief: **fe-user KHÔNG "dở"** — wire đã TRỌN và không có biến unused nào (`isV2` được đọc 2 chỗ,
`ContractWorkflowPanel` được render). Cái chết ở lượt trước là **fe-admin chưa hề chạm**, tức lệch **MIRROR**,
không phải lệch *unused-vars*. Vá đúng bệnh: `cp` fe-user → fe-admin (giữ trọn 33 dòng W6 + mọi thứ khác
byte-y-hệt), KHÔNG gõ lại tay — gõ lại là mời sai lệch vào đúng cặp file phải identical.

**Kết quả:** `git hash-object` ×2 = `42009bd1` **=** `42009bd1` ✅ · `git diff --no-index` **rc=0** (0 dòng lệch).

3 khối wire (nay có ở CẢ 2 app): (a) import + ghi chú V1-không-tải-nhánh-này; (b) `const isV2 =
c.approvalWorkflowId != null` + khối ghi chú "một cờ ba nơi đọc"; (c) `!isV2 &&` chặn 2 nút header V1 +
`{isV2 && <section><ContractWorkflowPanel/></section>}` đặt NGAY SAU card "Nguồn gốc KHKK".
🔴 Nhánh V1 legacy **KHÔNG đổi một byte** — chỉ bọc thêm điều kiện `!isV2` ở JSX bao ngoài; `openAction`,
`transition`, `Dialog` chọn-phase, `availableTargets` giữ nguyên hoàn toàn.

