# WQ1-FE — remake /my-contracts về layout KHKK-THUẦN (S190 · YC-030 Q1)

Vai: implementer-frontend · run `2026-08-12-S190-soltuon-dieu-chinh-hd`
Ranh: Write/Edit CHỈ {fe-user `MyContractsPage.tsx` · fe-user `components/contracts/ContractListPanel.tsx` (MỚI) · sub-md}. KHÔNG fe-admin · KHÔNG `ContractWorkflowPanel` · KHÔNG commit.
(File này = sub-MD của lane; alias trong footer spawn là `sub-implementer-frontend-1.md` — nội dung KHÔNG tách đôi, tất cả ở đây.)

## Việc đã làm

- **[lượt 1]** Ghi khung rỗng file này (chống #53 — append sau MỖI đơn vị việc).
- **[đọc khuôn]** `fe-user/src/pages/khkk/KhkkListPage.tsx` :1-281 trọn · `fe-user/src/components/khkk/KhkkListPanel.tsx` :1-491 trọn · hiện trạng `fe-user/src/pages/contracts/MyContractsPage.tsx` :1-369 (bản sau W2+W3).
- **[đo đĩa trước khi gõ — 4 phép, đổi cả thiết kế]**
  1. `ls fe-admin/src/pages/contracts/` = `ContractCreatePage` · `ContractDetailPage` · `ContractsListPage` · `HardCopiesPage` ⇒ **KHÔNG có `MyContractsPage`** ⇒ component mới là **fe-user-ONLY có chủ đích**, KHÔNG có cặp SHA để mirror (khai ngay ở header file mới :6-11).
  2. 🔴 `fe-user/src/components/contracts/ContractDetailContent.tsx:331-335` **ĐÃ render `<ContractWorkflowPanel contract={c} />`** trong nhánh `isV2` ⇒ nếu panel-3 render thêm thì **một HĐ có HAI bộ nút Duyệt/Trả lại** — đúng thứ chính khối W6 cấm ở `ContractDetailContent.tsx:140-142` ("để cả hai thì cùng một HĐ V2 có 2 đường bấm duyệt"). ⇒ **lệch spec có chủ đích**, xem §DEFERRED.
  3. `usePipelineStages.ts:83` + index `enabled: canPe` ⇒ cây GĐ3 cũng chết khi thiếu quyền Duyệt NCC ⇒ **bê nguyên mitigation M-1 của KHKK** (`!canPe` ép chế-độ phẳng), nếu không thì gộp 2 cột thành 1 sẽ làm người chỉ-có-quyền-HĐ **mất trắng danh sách** (mặt còn lại gotcha #82).
  4. `git show 502f4d2e:…MyContractsPage.tsx` (bản TRƯỚC-W2) ⇒ panel-3 khi đó = `WorkflowHistoryPanel` trong `aside … w 360px p-4` ⇒ **bề ngang 360px đã có tiền lệ chạy thật** (bài học #15: đo bề ngang trước khi port khuôn).
- **[thi công 1]** TẠO `fe-user/src/components/contracts/ContractListPanel.tsx` (346 dòng) — mirror-ADAPT `KhkkListPanel`:
  - filter-block trên cùng theo đúng thứ tự spec: `Select` **loại HĐ** (Q2 — giữ từ W2) → ô **Tìm** → `Select` **trạng thái** (khuôn KHKK, nguồn nhãn `ContractPhaseLabel` 12 mục).
  - `flatMode = !canPe || term.length > 0 || phase !== null || pendingMe || deletedView` (`:139`). CÂY = mặc định: `PipelineTreePanel currentStage={3} hideHeader hideSearch bodyMaxHClass="lg:max-h-none"` + `stage2Options={{ onOpenContract, stage3TypeFilter }}`.
  - Query `['my-contracts', type, phase, deletedView, term, pendingMe]` **dời nguyên từ host xuống panel** + `enabled: flatMode` (khuôn `KhkkListPanel.tsx:260` — chế-độ cây KHÔNG gọi endpoint nữa). Giữ nguyên 3 nhánh endpoint của W3/W7: `deleted` → `pendingMe` (`/contracts/inbox`, 0 tham số, lọc loại ở client, nợ `[carry:w7b-inbox-param]`) → thường.
  - Card `ContractRowCard` = **markup cũ dời chỗ, không vẽ lại** (mã · tên · NCC · chip phase · giá + `SlaTimer`).
  - **THÊM 3 thứ KHÔNG có ở bản W2** (đều là chống-câm, khai ở đây để reviewer không tưởng scope creep): (a) nhánh `list.isError` + nút "Thử lại" — trước đó lỗi mạng đọc thành "Không có HĐ"; (b) chân cột `x / total HĐ` + cảnh báo cắt `PAGE_SIZE=100` (trang chưa có UI phân trang); (c) dòng chữ hiện "cây dựng từ phiếu Duyệt NCC — HĐ ngoài gói không nằm trên cây…" chỉ hiện ở chế-độ cây.
- **[thi công 2]** VIẾT LẠI `MyContractsPage.tsx` (369 → 215 dòng): grid GIỮ `[400px_1fr_360px]`; panel-1 = `ContractListPanel`; panel-2 = `ContractDetailContent` (bỏ `WorkflowHistoryPanel` khỏi đây); panel-3 = **khuôn KHKK QĐ-2** hộp `border-dashed` + icon `X` khi chưa chọn ⟂ `WorkflowHistoryPanel` khi đã chọn. **BỎ cột danh-sách thường-trực** (nay là chế-độ phẳng của panel-1). BỎ badge `{rows.length}` ở header (không còn nguồn: chế-độ cây không có "số dòng" — số đếm xuống chân panel-1).
- **[thi công 3]** Lớp override lọc: 1 state `{nav, type, phase}` **đóng dấu leaf** (`navKey = type|phase|deleted|pendingMe`) — đổi leaf ⇒ dấu lệch ⇒ override tự rơi. **KHÔNG `useEffect`** (bài W2: ăn `react-hooks/set-state-in-effect`, 47→48) và **KHÔNG set-state-trong-render** (khuôn KHKK `KhkkListPage.tsx:83-88` dùng cách đó; ở đây dẫn-xuất thuần nên tránh luôn rủi ro lint). 3 trạng thái `undefined`/`null`/số giữ nguyên nghĩa KHKK.
- **[thi công 4]** `Select` loại HĐ nay lọc **CẢ cây LẪN danh sách** (W2: chỉ cây). Lý do khai tại `ContractListPanel.tsx` khối `queryFn`: gộp 2 cột thành 1 mà `Select` chỉ lọc cây thì ở chế-độ phẳng nó là **nút chết**. Kéo theo: `typeLabel` ở header + `?type=` của nút "Tạo HĐ mới" đọc loại **ĐÃ GIẢI** (khuôn `KhkkListPage.tsx:151-153` đọc `group` đã giải) — chọn "Tất cả loại HĐ" mà tiêu đề còn treo tên 1 loại là tiêu đề nói dối.
- **[GIỮ nguyên]** `?id=` chọn tại chỗ · deep-link `/contracts/{id}` khi `<lg` (`window.matchMedia('(min-width: 1024px)')`) · chặn mở khi `deleted=1` (2 lớp: `selectContract` return sớm + card `disabled`) · `?q=` ghi URL `{replace:true}` · nhãn chế-độ ở header (Đã xóa / Đã phát hành / Đang duyệt).

## Ô acceptance

| # | Ô | Lệnh | Số đo | Verdict |
|---|---|---|---|---|
| A1 | `ContractListPanel` trong page ≥1 | `grep -c` | **6** | PASS |
| A2 | `border-dashed` trong page ≥1 (panel-3 dashed) | `grep -c` | **1** | PASS |
| A3 | `{!selectedId && <PipelineTreePanel` = 0 | `grep -cF` | **0** | PASS |
| A3-ctrl | control-DƯƠNG cùng regex trên `081557b4` (bản từng có khối đó) | `git show \| grep -cF` | **1** | phép đo CÓ RĂNG |
| A3b | `<PipelineTreePanel` element trong page = 0 | `grep -c` | **0** (HEAD bản W3 = **1**) | PASS + control-dương |
| A4 | `flatMode\|isFlat` trong panel ≥2 | `grep -cE` | **9** | PASS |
| A5 | eslint fe-user KHÔNG TĂNG (baseline 47) | `npx eslint .` | **47 problems (41 error/6 warn)** — 2 file mới: **0** | PASS |
| A6 | `npm run build` fe-user EXIT 0 | `npm run build` | **BUILD_EXIT=0**, built in 16.65s | PASS |
| A7 | `tsc -b` EXIT 0 | `npx tsc -b` | **TSC_EXIT=0** | PASS |

**8/8 ô máy PASS** (A3-ctrl tính là chứng-cứ của A3, không tính ô riêng ⇒ 8 dòng đo, 0 FAIL).

Phụ — chống **bẫy tự-trích-dẫn** (bài S188): `grep -c 'ContractWorkflowPanel' MyContractsPage` = **1** hit, nhưng bỏ dòng `//` còn **0** ⇒ hit duy nhất là **văn xuôi trong comment FLAG**, KHÔNG phải render. Tương tự `PipelineTreePanel` còn **1** hit ở `:197` — nằm trong `{/* … */}` (khối comment JSX), câu chữ là "KHÔNG nhét `PipelineTreePanel` vào đây".

Warning build = **PRE-EXISTING** (chunk >500 kB + `INEFFECTIVE_DYNAMIC_IMPORT src/lib/realtime.ts`), đúng như 3 đợt trước — không đổ lỗi cho đợt này.

Pattern 16-bis 4-place: **N/A có bằng chứng** — không thêm route/menu-key nào. `fe-user/src/App.tsx:66` đã có `<Route path="/my-contracts" …>`; `Layout.tsx:90,166,174,179` đã trỏ 4 leaf về đúng URL này. Đợt này chỉ đổi RUỘT của trang.

Diện chạm: `git status --short fe-user fe-admin` = **đúng 2 dòng** (`M MyContractsPage.tsx`, `?? ContractListPanel.tsx`), **fe-admin 0 dòng**.

## DEFERRED/DỪNG

1. 🔴 **FLAG WQ1→WQ3b — panel-3 CỐ Ý KHÔNG có `ContractWorkflowPanel` (lệch spec BƯỚC 2(2), có bằng chứng).** Spec bảo panel-3 = `ContractWorkflowPanel + WorkflowHistoryPanel`, nhưng `ContractDetailContent.tsx:331-335` **đã** render `ContractWorkflowPanel` (nhánh `isV2`) và `ContractDetailContent` chính là panel-2 ⇒ làm đúng chữ spec = **2 bộ nút Duyệt/Trả lại cho cùng 1 HĐ**. Gỡ khối `isV2` khỏi `ContractDetailContent` là cách đúng, nhưng file đó (a) **ngoài ranh WQ1**, (b) là **cặp mirror ×2 app**, (c) còn nuôi cả `ContractDetailPage` fullpage. ⇒ Để WQ3b/lead quyết. Panel-3 hiện = dashed ⟂ `WorkflowHistoryPanel` (sơ đồ quy trình + duyệt phòng ban + lịch sử) = đúng hình KHKK QĐ-2 **trừ 2 nút bấm**.
2. **HÀNH VI = DEFERRED-TO-LOCAL-VERIFY** (lead đo trên máy): (a) cây GĐ3 mở ra đúng ở `/my-contracts` trần; (b) gõ tìm / chọn trạng thái ⇒ nhảy sang danh sách phẳng và ngược lại; (c) 4 leaf menu (List/Pending/Approved/Deleted) vào đúng chế-độ; (d) `<lg` bấm row vẫn ra fullpage; (e) panel-3 dashed → có nội dung khi chọn HĐ.
3. **Chưa đụng phân trang.** Chế-độ phẳng vẫn `pageSize: 100` trang 1 như W2/W7 (KHÔNG có UI phân trang). Nay ít nhất **nói ra** chỗ bị cắt (`x / total` + dòng amber) thay vì cắt câm. Muốn phân trang thật = việc đợt sau (KHKK có sẵn khuôn `KhkkListPanel.tsx:457-488`).
4. **Nợ cũ giữ nguyên `[carry:w7b-inbox-param]`:** `GET /contracts/inbox` là record 0 tham số + `Take(100)` cứng ⇒ chế-độ "Đang duyệt" khoá ô tìm và ẩn `Select` trạng thái, lọc loại chạy client. Đã nói bằng **chữ hiện** (không dùng `title` trên control `disabled` — bài S182).
5. **Dòng chữ "HĐ ngoài gói"** (`ContractListPanel` cuối khối filter) là **phán đoán của lane**, không có trong spec: nó là đường DUY NHẤT lộ ra cho HĐ tạo trực tiếp sau khi cột danh-sách bị gộp. Anh từng bảo bỏ mấy dòng giải-thích cơ-chế ở KHKK (S175) ⇒ **nếu anh không thích, xoá 1 khối `{!flatMode && (…)}` là xong**, không kéo theo gì.
