7.8 KiB
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ốisub-d2-fe-2.md) - scope:
fe-admin/src/**+fe-user/src/**ONLY. KHÔNG chạmsrc/Backend/**, KHÔNG chạmtests/. - lượt trước để lại: types ×2 app đã +
allowApproverDelete(còn trong working tree, chưa commit). Mục "váWfViewcho 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? bodyvớirecord DeleteByApproverBody(string? Reason)⇒ lý do đi trong BODY, không phải query ⇒ FE dùngapi.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ẳngPeListItem, 0 type mới.MenuKeys.csfactoryPe_{code}_Approved·Pe_{code}_DeletedĐÃ có;DbInitializerseed 2 mục +labelBackfillđổiDuyệ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/**):
PurchaseEvaluationDtos.cs:131— thêm member positional thứ 9:bool AllowApproverDelete);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-1085khi 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.)