# sub-b6-w5c-fe — W5c port nút Xóa vào ContractDetailContent (×2 app)

Vai: implementer-frontend · S190 · YC-029 · plan `sub-b5-plan-final.md` block `### W5` việc 4+5.
Ranh đã giữ: Write/Edit CHỈ `fe-{user,admin}/src/components/contracts/ContractDetailContent.tsx` + file này. KHÔNG commit. KHÔNG chạm BE / host / agent-memory.

## Việc đã làm

### BƯỚC 0 — baseline đo TRƯỚC khi gõ (đĩa, không tin trí nhớ)

| Ô | fe-user | fe-admin |
|---|---|---|
| `sha256sum ContractDetailContent.tsx` | `cd81508f97cb…` | `cd81508f97cb…` (**identical sẵn**) |
| `grep -c 'window.confirm'` | 0 | — |
| `grep -c '<Dialog'` | **1** | 1 |
| `grep -c 'File MIRROR SHA256 identical'` | 0 | 0 |
| `npx eslint .` | 47 problems | 42 problems |

### BƯỚC 1 — khuôn đã đọc (neo tươi 2026-08-12, khớp plan, KHÔNG kích Đ-5)

- Thanh nút đáy KHKK `fe-user/src/components/khkk/KhkkDetailContent.tsx:1076-1097` — `sticky bottom-0 z-10 … border-t border-slate-200 bg-white/95 … backdrop-blur` + `<button>` `bg-red-600` + `Trash2 h-3.5 w-3.5` + `isPending ? 'Đang xóa…'` + `window.confirm` CÓ MÃ + CÓ ĐÍCH (`:1090`).
- Mutation `deletePlan` `:316-320` — 3 mảnh (`mutationFn` `api.delete` · `onSuccess` toast + `onDeleted?.()` · `onError` `getErrorMessage`).
- Prop khai `onDeleted?: () => void` + JSDoc `:164/:171`.
- `isDrafter` null-safe `fe-user/src/components/pe/PeDetailTabs.tsx:141` = `currentUser?.id != null && X.drafterUserId === currentUser.id`; auth = `const { user: currentUser } = useAuth()` (`@/contexts/AuthContext`, `:129`), `isAdmin = currentUser?.roles?.includes('Admin') ?? false` (`:130`). `UserInfo{id,email,fullName,roles}` **giống hệt 2 app** (`fe-{user,admin}/src/types/auth.ts:1-6`) ⇒ copy-byte an toàn.
- SF-17 khuôn khai mirror: `KhkkDetailContent.tsx:2` — fe-admin ghi **NGUYÊN VĂN** cùng câu ("…với fe-admin counterpart") ⇒ đúng là CÙNG BYTES, không đổi chữ theo app. Đã theo.
- Phase const: `ContractPhase.DangInKy=5 · TraLai=98 · TuChoi=99` (`types/contracts.ts:3-13`); `drafterUserId: string | null` (`:154`) — đúng plan.

### BƯỚC 2 — thi công fe-user rồi copy-byte fe-admin (+82/−1 mỗi bên, đối xứng)

1. **Header MIRROR (SF-17)** — thêm `// File MIRROR SHA256 identical với fe-admin counterpart.`
   ⚠️ **Lệch có chủ đích so lệnh "dòng 2":** đặt ở **dòng 5**, ngay SAU đoạn mở đầu 4 dòng, vì dòng 1-4 là MỘT câu liền mạch ("Reusable detail body — used by … WorkflowHistoryPanel (Panel 3)."); chèn vào giữa là cắt câu làm đôi. Ô đo C-MIRROR-KHAI không neo số dòng (`grep -c` = 1) ⇒ 0 ảnh hưởng.
2. **Import** — `Trash2` vào cụm lucide (`:8`) + `useAuth` từ `@/contexts/AuthContext` đặt giữa cụm `@/components/ui/*` và `@/lib/api` (đúng thứ tự nhóm đang có).
3. **Prop mới** `onDeleted?: () => void` + JSDoc khai thẳng "KHÔNG truyền ⇒ chỉ 3 `invalidateQueries` chạy…" (không hứa hành vi không có).
4. **Mutation `deleteContract`** — 3 mảnh đúng khuôn: `api.delete('/contracts/${c.id}', { params: reason?.trim() ? { reason: reason.trim() } : undefined })` · `onSuccess` invalidate ĐÚNG bộ key của chính file (`['contract', c.id]` + `['my-contracts']` + `['inbox']`, khuôn `transition` `:57-61`) + `toast.success('Đã xóa hợp đồng.')` + `onDeleted?.()` · `onError` `toast.error(getErrorMessage(err))`.
5. **Rào hiển thị `canDelete`** = `(phase < DangInKy || phase === TraLai || phase === TuChoi) && (isDrafter || isAdmin)`.
6. **Thanh nút đáy** `sticky bottom-0` + `window.confirm` mang MÃ + ĐÍCH + `Trash2` + `Đang xóa…` + dòng chữ phụ "Xoá mềm — dữ liệu không mất khỏi hệ thống."
7. **Comment khối `[W5c S190 · YC-029]`** khai: đây là BẢN SAO HIỂN THỊ của guard BE (BE mới là chỗ chặn thật) · vì sao `confirm` chứ không hộp thoại thứ hai · vì sao `currentUser?.id != null` phải đứng trước.
8. `cp` fe-user → fe-admin (copy-byte, không gõ tay lần 2).

### Đối chiếu FE ⟂ BE (không tin plan, đọc handler đã land W5b)

`awk '/class DeleteContractCommandHandler/,/^}/' src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs`:

```
var phaseAllowed = entity.Phase < ContractPhase.DangInKy
    || entity.Phase == ContractPhase.TraLai
    || entity.Phase == ContractPhase.TuChoi;
```
+ `isOwner = entity.DrafterUserId == currentUser.UserId` · `isAdmin = currentUser.Roles.Contains(AppRoles.Admin)` · `!isOwner && !isAdmin → Forbidden`.
⇒ FE `canDelete` **cùng hình dạng cả 2 vế**. Controller giữ `[Authorize]` trần + `[FromQuery] string? reason` (đã đọc `ContractsController.cs` khối DELETE) ⇒ endpoint nhận `?reason=` thật, không phải tôi bịa hình.

### 🔴 Bẫy tự-trích-dẫn (S188 tái phát, đã bắt TRONG lượt)

Bản nháp comment viết nguyên văn token mở hộp thoại 2 lần ⇒ `grep -c '<Dialog'` **1 → 2**, tức bình luận của tôi tự phá ô đo C-12 vế 3 (gate đếm theo DÒNG, không phân biệt code ⟂ chữ). Đã sửa: comment nói "hộp thoại `Dialog`" (bỏ dấu `<`), đo lại = 1. Token `window.confirm` thì **cố ý giữ 2 dòng lineage** (nhắc khuôn KHKK) ⇒ raw = 3, **code-only = 1**; báo cả 2 số ở dưới thay vì báo mỗi số đẹp.

## Ô acceptance

| Ô | Lệnh đo | Kết quả | Verdict |
|---|---|---|---|
| **C-12 (a)** | `grep -c 'window.confirm'` fe-user | **3** raw (1 code + 2 dòng comment lineage); lọc bỏ dòng chú-thích ⇒ **1 code** | ✅ ≥1 |
| **C-12 (b)** | `grep -A2 'window.confirm' … \| grep -c 'Đã xóa'` | **1** | ✅ ≥1 |
| **C-12 (c)** | `grep -c '<Dialog'` fe-user / fe-admin | **1 / 1** (baseline 1 — GIỮ NGUYÊN sau khi gỡ bẫy tự-trích-dẫn) | ✅ |
| **C-MIRROR-KHAI** | `grep -c 'File MIRROR SHA256 identical'` | fe-user **1** · fe-admin **1** (baseline 0/0) | ✅ |
| **SHA cặp** | `sha256sum` 2 bên | `7ad3aed5efe99bb4c8e640838f8a9721064e42a42ab09a08d58ba24eb6c76b97` **× 2 khớp** (baseline chung `cd81508f97cb…` ⇒ đổi thật, vẫn khớp cặp) | ✅ Đ-2 không kích |
| **C-2 build ×2** | `npm run build` | fe-user **EXIT 0** (`✓ built in 485ms`) · fe-admin **EXIT 0** (`✓ built in 469ms`) | ✅ |
| **C-1a eslint ×2** | `npx eslint .` | fe-user **47** (baseline 47) · fe-admin **42** (baseline 42) — **0 TĂNG**, `grep -c ContractDetailContent` trong CẢ 2 log = **0** ⇒ file tôi sửa không đẻ hit nào | ✅ Đ-1 không kích |
| **Scope** | `git diff --stat` | đúng 2 file, **+82/−1 mỗi bên** (đối xứng ⇒ chứng copy-byte, không gõ tay lệch) | ✅ |

**Warning build = PRE-EXISTING** (chunk-size >500KB + `INEFFECTIVE_DYNAMIC_IMPORT src/lib/realtime.ts`) — có ở CẢ 2 app từ trước, không phải sản phẩm wave này.

## DEFERRED/DỪNG

- **Ô hành-vi (bấm nút thật, xem HĐ rụng khỏi danh sách / toast / 403-409 từ BE) = `DEFERRED-TO-LOCAL-VERIFY`** — đúng đường thoát Đ-7, KHÔNG tick.
- **Host chưa truyền `onDeleted` (5 host, KHÔNG sửa — ngoài scope Đ-4):** `fe-user` `pages/contracts/ContractDetailPage.tsx:28` · `pages/contracts/MyContractsPage.tsx:282` · `pages/InboxPage.tsx:347`; `fe-admin` `pages/contracts/ContractDetailPage.tsx:28` · `pages/contracts/ContractsListPage.tsx:254`. Hành vi hiện tại sau khi xoá = 3 `invalidateQueries` chạy ⇒ danh sách rụng dòng; query `['contract', id]` refetch ra **404** ⇒ host rơi về nhánh sẵn có ("Không tìm thấy HĐ." ở `ContractDetailPage`, hoặc khối detail biến mất ở host 3-panel vì điều kiện `selectedId && detail.data`). Đã kiểm: **KHÔNG có `QueryCache`/`MutationCache` onError toàn cục** ở `fe-user/src/main.tsx|App.tsx` ⇒ 404 này KHÔNG bắn toast đỏ đè lên toast thành công. Muốn "rời màn detail" mượt hơn thì host truyền `onDeleted` — 1 dòng/host, đề xuất cho lead, tôi KHÔNG tự làm.
- **`reason` là DÂY CẮM CHƯA AI KÉO (khai thẳng, để reviewer cân):** `mutationFn` nhận `reason?: string` cho đúng hình endpoint `[FromQuery] string? reason`, nhưng UI **không thu lý do** (giữ khuôn KHKK `:1090` — lệnh ghi rõ "KHÔNG `window.prompt`, đừng sáng tạo") ⇒ mọi call là `mutate(undefined)`, axios bỏ hẳn key, request y hệt `DELETE /contracts/{id}`. Đây là tham số optional **chưa có caller truyền giá trị** — cùng họ (nhẹ hơn) với "gán mà không ai đọc". Giữ vì lệnh W5 yêu cầu `params { reason }`; nếu reviewer coi là dây chết thì gỡ đúng 1 dòng.
- **KHÔNG chạm:** BE (`ContractFeatures.cs` / `ContractsController.cs` / `ContractChangelog.cs` — đang `M` do W5b, không phải tôi), test `ContractDeleteGuardTests.cs` (`??` của W5a), `docs/`, host page, `agent-memory/`, STATUS.
- **Ghi chú artifact:** khối RUN-TRACE hệ thống đặt tên `sub-implementer-frontend-0.md`, lệnh trực tiếp của lead đặt tên `sub-b6-w5c-fe.md` (đã Write khung ở lượt 1 theo lệnh). Giữ **một** file này để tránh 2 bản sự thật; `subMdPath` trả về trỏ đúng đây.
