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

7.8 KiB
Raw Blame History

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 = 7phase=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 = !readOnlyphase===ChoDuyetactorIsCurrentApproverlevelOptions?.allowApproverDelete === truecan(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ó (md5 IDENTICAL)
lib/menuKeys.tsPurchaseEvaluations :23 :23
Dialog props open/onClose/title/children/footer/size y hệt (file DIVERGE nhưng chỉ ở style)
Button variant danger 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: truetsc --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.)