# sub-b6-w3-worker — implementer-frontend (W3, YC-029 · plan SOLTUON-12-08-2026)

> Wave W3 = "leaf **Đang duyệt** ở lại `/my-contracts`" (FE-only, fe-user, 2 file).
> Ranh: chỉ `fe-user/src/components/Layout.tsx` + `fe-user/src/pages/contracts/MyContractsPage.tsx` + file này.

## Việc đã làm

### 0. Baseline đo TRƯỚC khi gõ (đĩa, không chép artifact)

| Phép đo | Số | Ghi chú |
|---|---|---|
| `npx eslint .` fe-user | **47** (41E/6W) | khớp kỳ vọng plan Đ-1 |
| nhánh `action === 'Pending'` họ `Ct_*` | `Layout.tsx:164` = `` `/inbox?type=${typeInt}` `` | W1 đã đẩy số dòng từ ~150 → 164 |
| `grep -c pendingMe MyContractsPage.tsx` | **0** | khớp kỳ vọng |
| `git log --oneline -3` | `7b291014` W2 · `09ebd20e` W1 · `1cc16a2d` W4 | đọc trạng thái SAU 3 wave, không dựa mô tả cũ |

Đọc thêm (chỉ-đọc, không sửa): `ContractsController.cs:41-43` — `[HttpGet("inbox")] Inbox(CancellationToken ct) => GetMyInboxQuery()` ⇒ **0 tham số**, trả `List<ContractListItemDto>` (KHÔNG phân trang). Khuôn PE: `PurchaseEvaluationsListPage.tsx:77-99` (deleted TRƯỚC pendingMe → đổi endpoint → ép shape `:98`).

### 1. `fe-user/src/components/Layout.tsx` (2 chỗ)

- **`:174` nhánh `Pending` họ `Ct_*`** — `` `/inbox?type=${typeInt}` `` → `` `/my-contracts?type=${typeInt}&pendingMe=1` ``, kèm comment 6 dòng khai: `pendingMe` là **danh-tính leaf** (không vào `TRANSIENT_QUERY_KEYS`), trang tự đổi endpoint, và **route `/inbox` + `InboxPage` GIỮ NGUYÊN** làm hộp thư tổng.
- **`:61-66` vá comment stale** — câu cũ *"Duyệt to `/inbox` (pending THEIR approval)"* hết đúng cho họ `Ct_*` sau đổi; viết lại + trỏ mục cố định `USER_FIXED_TOP` / `staticResolvePath('__inbox')` là đường vào hộp thư tổng.
- **KHÔNG chạm:** `TRANSIENT_QUERY_KEYS:382` (W1 đã viết sẵn câu cấm cho `ct`/`view`/`pendingMe` — C-7 giữ 0) · `CT_TYPE_PARAM_PATHS:37` (đã có `/my-contracts` ⇒ accordion `?type=` vẫn mở đúng nhóm ở URL mới; `/inbox` giữ trong allowlist vì URL đó còn sống) · nhánh `Pe_*:203` · họ `Khkk_*`.

### 2. `fe-user/src/pages/contracts/MyContractsPage.tsx` (6 chỗ)

1. **`:63` đọc danh-tính** — `const pendingMe = searchParams.get('pendingMe') === '1'` (so chuỗi, không `Number()`).
2. **`:87` queryKey** — `['my-contracts', typeFilter, phaseFilter, showDeleted, search, pendingMe]` (+trục `pendingMe`: 2 chế độ đọc **2 nguồn khác nhau**, dùng chung ô cache = trả nhầm danh sách).
3. **`:93` nhánh mới trong queryFn** — `if (pendingMe && !showDeleted) {` ⇒ `:102` `api.get<ContractListItem[]>('/contracts/inbox')` (KHÔNG param) → lọc `type` **client** (`c.type === typeFilter`, bỏ qua khi `typeFilter == null`) → ép shape `{ items, total: items.length, page: 1, pageSize: items.length }` đúng khuôn PE `:98`.
   - Vế `&& !showDeleted` = hiện thực hoá "**deleted đứng TRƯỚC pendingMe**" (2 view loại trừ). URL lai `?deleted=1&pendingMe=1` ⇒ vẫn vào `/contracts/deleted`, không rơi vào inbox (inbox không bao giờ trả HĐ đã xóa ⇒ sẽ rỗng THẦM LẶNG).
   - Comment tại chỗ khai **vì sao không gửi param**: `GET /contracts` không có `pendingMe` (`ContractsController.cs:17-26`), ASP.NET nuốt im lặng ⇒ ra y hệt list thường (hỏng câm).
   - Comment `[carry:w7b-inbox-param]`: **lệch-W7 CHẤP NHẬN** — W7/S187 đã đẩy lọc `type` lên server cho 2 nhánh kia; nhánh này lọc client vì BE chưa có param (`GetMyInboxQuery` = record 0 tham số, còn `Take(100)` cứng). Nâng BE = đợt khác.
4. **`:215` nhãn chế độ** — chip amber "Đang duyệt — đến lượt tôi" (khuôn 2 chip sẵn có `Đã xóa — chỉ xem` / `Đã phát hành`). Cùng URL gốc + cùng tiêu đề mà đổi nguồn dữ liệu ⇒ không có nhãn thì user tưởng HĐ biến mất.
5. **`:300` khoá ô tìm** — `disabled={pendingMe}` + `value={pendingMe ? '' : search}` + **chữ HIỆN** (`<p>` ngay dưới ô) *"Chế độ 'Đang duyệt' đọc hộp thư đến — chưa hỗ trợ tìm kiếm phía máy chủ."*. 🔴 Không dùng `title` (tooltip trên control `disabled` bị Chromium nuốt — bài S182). **Phân trang: trang này KHÔNG có UI phân trang** (`pageSize: 100` cố định, 0 nút trang) ⇒ không có gì để ẩn; đã khai trong comment thay vì im lặng bỏ ô.
6. **`:177` + `:327` rỗng-vì-lọc ⟂ rỗng-vì-chưa-có-gì** — `emptyTitle`/`emptyDescription` dẫn xuất: pendingMe ⇒ *"Không có HĐ chờ bạn duyệt"* / *"Không HĐ «{loại}» nào đang đến lượt bạn duyệt."*; giữ nguyên 2 câu cũ cho deleted / list thường.
7. **`:15-18` khai 2 nguồn ở đầu file** (panel-3 có 2 nguồn, chọn bằng `?pendingMe=1`).

## Ô acceptance

> Lệnh chạy tại repo root, nối `;`, `|| true`. `grep -F` cho pattern chứa `?`.

| # | Ô | Lệnh | Số | Verdict |
|---|---|---|---|---|
| 1 | `pendingMe=1` trong Layout ≥1 | `grep -cF 'pendingMe=1' …/Layout.tsx` | **6** | ✅ PASS |
| 2a | spec-literal `'/inbox?type=` = 0 | `grep -cF "'/inbox?type=" …/Layout.tsx` | **0** | ✅ PASS *(ô này ĐÃ = 0 cả ở baseline — mã dùng backtick, không nháy đơn ⇒ **ô không có răng**, xem 2b/2d)* |
| 2b | dạng THẬT `/inbox?type=` | `grep -cF '/inbox?type=' …/Layout.tsx` | **1** | ⚠️ 1 hit CÒN LẠI = **comment** `:497` mô tả allowlist accordion (`/inbox?type=N` vẫn auto-expand vì `/inbox` còn trong `CT_TYPE_PARAM_PATHS` ⇒ **câu này vẫn ĐÚNG**, không phải dư-lượng) |
| 2c | CONTROL-DƯƠNG (HEAD) | `git show HEAD:… \| grep -cF '/inbox?type='` | **2** (`:164` mã + `:487` comment) | ✅ thước có răng |
| 2d | **discriminator CODE-ONLY** (bỏ dòng `//` — bài S188 bẫy tự-trích-dẫn) | `grep -nF … \| grep -vE '^[0-9]+: *//' \| wc -l` | **0** | ✅ PASS (nhánh `Pending` cũ hết) |
| 2e | CONTROL-DƯƠNG code-only (HEAD) | như trên trên HEAD | **1** | ✅ thước phân biệt được |
| 3 | `get('pendingMe')` trong Page ≥1 | `grep -cE "get\(['\"]pendingMe['\"]\)"` | **1** | ✅ PASS |
| 4 | `contracts/inbox` trong Page ≥1 | `grep -cF 'contracts/inbox'` | **4** | ✅ PASS |
| 5 | **C-7** transient leak = 0 | `grep -A6 'TRANSIENT_QUERY_KEYS = ' \| grep -cE "'(ct\|view\|pendingMe)'"` | **0** | ✅ PASS |
| 6 | queryKey chứa `pendingMe` | `grep -nF "queryKey: ['my-contracts'"` | `:87 … , search, pendingMe]` | ✅ PASS |
| 7 | **C-1a** eslint fe-user ≤47 | `npx eslint .` | **47** (41E/6W) — KHÔNG TĂNG | ✅ PASS |
| 7b | eslint file-wave | `-f json` per-file | `Layout.tsx` **4E/0W** (`static-components`, `set-state-in-effect` — **0 ruleId mới**) · `MyContractsPage.tsx` **0E/0W** | ✅ PASS (Đ-1 ≤4E) |
| 8 | **C-2** build fe-user | `npm run build` | `✓ built in 537ms`, **EXIT 0** | ✅ PASS *(2 warning chunk-size + `INEFFECTIVE_DYNAMIC_IMPORT realtime.ts` = **PRE-EXISTING**)* |
| 9 | route `/inbox` nguyên trạng | `grep -nF inbox fe-user/src/App.tsx` | `:58 <Route path="/inbox" element={<InboxPage />} />` | ✅ PASS (0 chạm `InboxPage`) |
| 10 | scope | `git status --porcelain` | đúng **2 file** wave `M` (các file `.claude/**`, `docs/**` dirty là của lead, **worker không chạm**) | ✅ PASS |

**Ô máy: 10/10 PASS** (ô 2a khai thẳng là ô **không có răng** — thay bằng 2d + control 2c/2e).

## DEFERRED/DỪNG

- **DEFERRED-TO-LOCAL-VERIFY — ô hành-vi W3(i):** so-số 2 chiều (user có ≥1 HĐ đến-lượt + ≥1 không-đến-lượt ⇒ bật `pendingMe=1` list ÍT HƠN + HĐ không-đến-lượt VẮNG; control-dương: user ngoài luồng ⇒ **0 dòng**, không phải cả list). Cần API chạy + dữ liệu duyệt thật ⇒ chưa dựng được trong lượt worker.
- **DEFERRED-TO-LOCAL-VERIFY — ô hành-vi W3(ii):** click leaf "Đang duyệt" ⇒ URL ở lại `/my-contracts?type=N&pendingMe=1` + đúng 1 leaf sáng (menu active-state).
- **`[carry:w7b-inbox-param]`** (nợ khai công khai, KHÔNG tự làm — ngoài wave): `GET /contracts/inbox` 0-param + `Take(100)` ⇒ (a) lọc `type` phải làm ở client, (b) user >100 HĐ chờ duyệt có thể mất dòng IM LẶNG. Vá đúng = nới `GetMyInboxQuery(type)` + bỏ/nới `Take` — việc BE, đợt khác.
- **KHÔNG DỪNG theo Đ-1..Đ-10:** eslint không tăng · không đụng file ngoài wave · neo `file:line` của plan lệch **có kiểm soát** (plan ghi `Layout.tsx:150`, thực tế `:164` do W1 chèn comment — Đ-5 ngưỡng ±15 dòng, lệch 14 ⇒ trong ngưỡng, không leo thang).
- Ô SHA-mirror: **KHÔNG áp dụng** cho W3 — cả 2 file là fe-user-only (fe-admin không có `MyContractsPage`; `Layout.tsx` 2 app cố ý khác UX). 0 cặp mirror trong wave này.
