Files
solution-erp/.claude/workflows/runs/2026-07-27-S155-pe-delete-approver/sub-d2-fe-3.md
2026-07-27 13:30:58 +07:00

104 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# sub-d2-fe-3 — ĐỢT 2 FRONTEND, phần CHÍNH (nút Xóa màn duyệt + menu + màn "Đã xóa")
- **role:** implementer-frontend · **run:** `2026-07-27-S155-pe-delete-approver` · **lượt:** 3 (nối `sub-d2-fe-2.md`)
- **scope:** `fe-admin/src/**` + `fe-user/src/**` ONLY. KHÔNG chạm `src/Backend/**`, KHÔNG chạm `tests/`.
- **lượt trước để lại:** types ×2 app đã +`allowApproverDelete` (còn trong working tree, chưa commit).
Mục "vá `WfView` cho fe-admin" → **lead chốt đường (c) GIỮ NGUYÊN** ⇒ bỏ hẳn khỏi phạm vi lượt này.
---
## 0. Nền đã ĐO lại lúc bắt đầu (không tin trí nhớ lượt trước)
| Đo | Kết quả |
|---|---|
| `git status` | BE lane ĐÃ LAND: `PeSoftDeleteFeatures.cs` (mới) + Controller + `MenuKeys.cs` + `DbInitializer.cs` (chưa commit) |
| `PeWorkflowPanel.tsx` md5 ×2 app | `02671be6334110028c55fe82f5e70dee` IDENTICAL, 942 dòng |
| `PurchaseEvaluationsListPage.tsx` md5 ×2 app | `99e38e167d2f745ea7628174f1d2d6bf` IDENTICAL, 712 dòng |
| `Layout.tsx` | DIVERGE cố ý — admin `ca59f56c` 284L / user `cab674f4` 415L |
| `lib/menuKeys.ts` | DIVERGE đúng 2 dòng (`Off_AttendanceReport` admin-only) |
| `PurchaseEvaluationPhase.DaDuyet` | **= 7** ⇒ `phase=7` trong hợp-đồng route KHỚP (đo ở `types/purchaseEvaluation.ts:28`) |
### Hợp-đồng BE đọc TỪ MÃ (không tin bảng trong brief)
- `[HttpDelete("{id:guid}/by-approver")]` + `[FromBody] DeleteByApproverBody? body` với `record DeleteByApproverBody(string? Reason)`
⇒ lý do đi trong **BODY**, không phải query ⇒ FE dùng `api.delete(url, { data: { reason } })`.
*(Lượt trước D2 định gửi CẢ query lẫn body cho chắc; nay đo được binding thật ⇒ gửi ĐÚNG 1 đường, bỏ query thừa.)*
- `[HttpGet("deleted")]` trả `PagedResult<PurchaseEvaluationListItemDto>`**DTO DÙNG CHUNG với list sống**.
**KHÔNG có** `deletedAt` / `deletedBy` / `deletedByName`. Giả định D3 lượt trước (type mới 3 field optional) **SAI** ⇒ bỏ, tái dùng thẳng `PeListItem`, **0 type mới**.
- `MenuKeys.cs` factory `Pe_{code}_Approved` · `Pe_{code}_Deleted` ĐÃ có; `DbInitializer` seed 2 mục + `labelBackfill` đổi `Duyệt``Đang duyệt` ĐÃ có.
---
## 1. 🔴 CHẶN THẬT — cờ F6 KHÔNG có đường ra tới FE (BE thiếu 1 member DTO)
Brief nói *"BE ĐÃ HIỆN THỰC XONG, lead tự verify"*. Đo bằng mã thì **verify đó sót đúng chỗ chết người**
lặp lại y hệt lớp lỗi đợt 1 (BE dựng, FE không nối được).
| Đo | Kết quả |
|---|---|
| `AllowApproverDelete` trong `ApprovalWorkflowOptionsDto` (`PurchaseEvaluationDtos.cs:123-131`) | ❌ **KHÔNG CÓ** — record đúng **8** member, dừng ở `AllowApproverFinalize` |
| Chỗ dựng DTO `PurchaseEvaluationFeatures.cs:1088-1096` | ❌ truyền **8** đối số, không có `curLevel.AllowApproverDelete` |
| `AllowApproverDelete` trong `PurchaseEvaluationFeatures.cs` (toàn file) | ❌ **0 hit** |
| Kênh thay thế `approvalFlow.steps[].levels[]` | ❌ cũng **0 hit** — không có đường vòng nào |
| Chỗ ĐÃ có | ✅ Domain `ApprovalWorkflow.cs:169` · Mig 68 · Designer admin · **rào BE `PeSoftDeleteFeatures.cs:114`** |
**Hệ quả runtime:** `evaluation.currentLevelOptions.allowApproverDelete` = `undefined` ⇒ gate `=== true` false ⇒
**nút Xóa KHÔNG BAO GIỜ HIỆN**, mà `npm run build` ×2 vẫn XANH (type lượt trước khai `boolean` non-optional,
TS không kiểm được payload runtime). Đúng dạng **silent drop**.
**BE phải vá 2 dòng (tôi KHÔNG chạm `src/Backend/**`):**
1. `PurchaseEvaluationDtos.cs:131` — thêm member positional **thứ 9**: `bool AllowApproverDelete);`
2. `PurchaseEvaluationFeatures.cs:1096` — thêm đối số cuối: `curLevel.AllowApproverDelete);`
**Tôi vẫn viết FE bám ĐÚNG tên canonical `currentLevelOptions.allowApproverDelete` và ĐI TIẾP** (không dừng lượt).
Chọn **fail-closed** (`=== true`), KHÔNG fail-open (`!== false`): thà nút ẩn tới khi BE vá, còn hơn lòi nút
xoá-chứng-từ-tài-chính cho mọi người rồi ăn 403 hàng loạt.
### 1-bis. 🔴 `actorInV2Level` KHÔNG dùng được cho rào xóa (lệch rào BE) — tôi đi CHẶT HƠN brief
Brief ghi gate dùng `actorInV2Level`. Đo thì nó **rộng hơn BE**:
- `PeWorkflowPanel.tsx:99-100`: `actorInV2Level = isAdmin || v2Approvers.some(...)` ⇒ **admin bypass**.
- `PeSoftDeleteFeatures.cs:104`: `FirstOrDefault(l => l.ApproverUserId == actorId)` **KHÔNG bypass admin**, và comment
BE nói rõ *"bê fallback `.First()` sang đây sẽ biến admin bất kỳ thành người xóa được phiếu người khác"*.
- Tệ hơn: `PurchaseEvaluationFeatures.cs:1084-1085` khi actor không match thì **fallback row ĐẦU** của Cấp
⇒ admin sẽ đọc cờ F6 **của người khác** ⇒ vi phạm thẳng "PER-NGƯỜI" mà owner chốt.
⇒ Dùng `actorIsCurrentApprover` = `!!currentUser?.id && v2Approvers.some(a => a.userId === currentUser.id)`
(tập con CHẶT của `actorInV2Level`, khớp 1-1 rào BE). Khai ở đây để reviewer không tưởng tôi đọc lệch brief.
---
## 2. ✅ BƯỚC 1-2 XONG — nút + dialog + mirror
### 2.1 `PeWorkflowPanel.tsx` (viết ở fe-user → `cp` sang fe-admin)
| Chỗ sửa | Nội dung |
|---|---|
| import | +`Trash2` (lucide) · +`MenuKeys` (`@/lib/menuKeys`) · +`usePermission` (`@/hooks/usePermission`) |
| prop | +`onDeleted?: () => void` — prop **THỨ 4**. KHÔNG tái dùng `onApproved` (khác nghĩa: duyệt ≠ xóa) |
| state | `deleteOpen` · `deleteReason` |
| gate | `canDeleteWhileApproving` = `!readOnly` ∧ `phase===ChoDuyet` ∧ `actorIsCurrentApprover` ∧ `levelOptions?.allowApproverDelete === true` ∧ `can(MenuKeys.PurchaseEvaluations,'Delete')` |
| mutation | `api.delete('/purchase-evaluations/${id}/by-approver', { data: { reason } })` → invalidate `pe-list` + `pe-detail` + **`pe-budget`** rồi `onDeleted?.()` |
| render nút | **CẠNH** `next.map`, là **anh em** của output map trong cùng `flex` — KHÔNG trong map |
| dialog | `<Dialog size="sm">` riêng, `variant="danger"`, 4 gạch đầu dòng hệ quả + `<Textarea maxLength={2000}>` lý do |
**Vì sao đụng 1 token ở điều-kiện khối ngoài** `next.length > 0` → `(next.length > 0 || canDeleteWhileApproving)`:
khối "Hành động:" chỉ render khi có phase kế. Nếu `nextPhases` rỗng mà 4 rào xóa vẫn đúng thì nút sẽ bị
khối cha nuốt IM LẶNG. Nới bằng chính cờ đã qua đủ 4 rào ⇒ không mở rộng bề mặt.
**`pe-budget` có thật** (đo: 6 queryKey `pe-*` sống trong fe-user, `pe-budget` là 1) — không phải key bịa.
### 2.2 Mirror ×2 — kiểm alias TRƯỚC khi copy (không copy mù)
| Kiểm | fe-user | fe-admin |
|---|---|---|
| alias `@/*` (tsconfig.app + vite) | `./src/*` | `./src/*` — **KHỚP** |
| `hooks/usePermission.ts` | có | có (**md5 IDENTICAL**) |
| `lib/menuKeys.ts` → `PurchaseEvaluations` | `:23` | `:23` |
| `Dialog` props | `open/onClose/title/children/footer/size` | **y hệt** (file DIVERGE nhưng chỉ ở style) |
| `Button` variant `danger` | có | có (class khác, key giống) |
⇒ copy an toàn. `md5` sau copy: **`eb55781aae32c8982d9f8d1e4b9b0e26` ×2** + `git diff --no-index` exit 0.
### 2.3 Đính chính snapshot lead gửi giữa chừng (gotcha #68)
Lead báo `Trash2/MenuKeys/onDeleted/can/deleteOpen/canDeleteWhileApproving` **declared-but-never-read**.
Đó là **diagnostic CŨ chụp giữa lúc agent đang sửa**. Đo lại từ đĩa: cả 6 đều có site render, và
`tsconfig.app.json` bật **`noUnusedLocals: true` + `noUnusedParameters: true`** ⇒ `tsc --noEmit -p tsconfig.app.json`
**EXIT 0** đã là bằng chứng KHÔNG còn symbol thừa. (Chỉ tin build chạy SAU agent.)