Files
solution-erp/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/sub-reviewer-diff-dot5.md
2026-07-29 14:53:58 +07:00

155 lines
22 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-reviewer — soi đối kháng diff đợt-5 (S159 L8)
**Vật soi:** `git diff` working-tree vs HEAD (`bbc39be`), 9 file trong scope (BE 4 + FE 5).
**Chế độ:** tĩnh (không live-verify, theo lệnh lead).
**Ghi từng FLAG NGAY khi chốt (chống #53).**
---
## Bản đồ file trong scope (đo từ `git diff --stat`)
| File | +/- |
|---|---|
| `src/Backend/SolutionErp.Api/Controllers/ContractsController.cs` | +12 |
| `src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs` | +55 |
| `src/Backend/SolutionErp.Application/PurchaseEvaluations/PeSoftDeleteFeatures.cs` | +1/-1 (comment) |
| `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs` | +33/-3 |
| `fe-user/src/App.tsx` | +2 |
| `fe-user/src/components/Layout.tsx` | +15/-3 |
| `fe-user/src/pages/UserDashboardPage.tsx` | +18/-339 |
| `fe-user/src/pages/contracts/MyContractsPage.tsx` | +32/-7 |
| `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` | +4/-1 |
Ngoài scope lead khai (KHÔNG soi, chỉ ghi nhận có mặt trong working-tree): `.claude/agent-memory/cicd-monitor/MEMORY.md`, `.claude/commands/session-{start,end}.md`, `.claude/governance/.session-counter.json`, `docs/HANDOFF.md`, `docs/STATUS.md`, `scripts/governance-detectors.ps1`, `scripts/session_ctx.py`.
---
## FLAG (ghi dần)
### FLAG-1 [M] — `fe-admin` thiếu 2 route ⇒ 2 lá menu mới rơi IM LẶNG (gotcha #50)
**File:** `fe-admin/src/components/Layout.tsx:31-44` (`staticMap`)
**Đo:** staticMap fe-admin có `Khkk_List` / `Khkk_Create` / `Khkk_Pending` / `Khkk_Approved` nhưng **KHÔNG có** `Khkk_WfView`, `Khkk_Deleted` — trong khi seeder (`DbInitializer.cs:1780-1786`) đã thêm 2 key này vào cây menu dùng CHUNG cho cả 2 app.
`MenuLeaf` của fe-admin: `resolvePath` trả `null``return null` ⇒ lá biến mất, KHÔNG có cảnh báo. Đúng lớp bẫy mà chính comment `Layout.tsx:33` cảnh báo ("thiếu route = sidebar drop SILENT").
**Hệ quả:** nhóm "1. Kế hoạch ký kết HĐ (NCC-TP)" bên fe-admin hiện 4/6 lá; lệch với fe-user 6/6. Vi phạm `docs/rules.md §3.9` (mirror 2 app).
**Không phải lỗi cho `Ct_*_WfView/Approved/Deleted`:** fe-admin ẩn TOÀN BỘ `Ct_*` (`isAdminHidden` :176-178 `key.startsWith('Ct_')`) ⇒ 21 lá HĐ mới không cần route bên admin. Đã kiểm, CLEAN.
**Tiêu chí nhận:** thêm 2 dòng `Khkk_WfView` + `Khkk_Deleted` vào staticMap fe-admin (trỏ `/coming-soon?stage=2&m=...` như fe-user), rồi `grep -c "Khkk_" ` ở 2 app phải BẰNG NHAU (=6).
### FLAG-2 [M] — Màn "Đã xóa" HĐ: bấm vào dòng ⇒ 2 panel TRẮNG (không thông báo), khác hẳn khuôn PE
**File:** `fe-user/src/pages/contracts/MyContractsPage.tsx:163-190` (`onClick={() => selectContract(c.id)}`) + `:52-56` (`detail` query) + `:196-222` (Panel 2/3)
**Đo (tĩnh, 2 vế):**
- Khuôn PE (S155) CHẶN hẳn: `fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx:524-527``disabled={deletedView}` + `cursor-default`.
- Bản HĐ đợt-5 **không có nhánh `showDeleted`** ở nút dòng ⇒ vẫn `selectContract(id)` → set `?id=``detail` query `GET /contracts/{id}`.
- `Contract` CÓ global query filter `!IsDeleted` (`ContractConfiguration.cs:44`) và endpoint chi tiết KHÔNG dùng `IgnoreQueryFilters` ⇒ HĐ đã xóa **404 NotFound**.
- Render: Panel 2 chỉ có 3 nhánh `!selectedId` / `isLoading` / `detail.data` — nhánh `isError` KHÔNG có ⇒ lỗi 404 rơi vào khoảng trống ⇒ **panel trắng trơn**, người dùng không biết vì sao. Panel 3 (`:221`) cũng trắng.
- Trên mobile (< lg) còn nặng hơn: `selectContract` (`:75-81`) `navigate('/contracts/{id}')` **rời khỏi màn** sang trang chi tiết 404.
**Tiêu chí nhận:** hoặc `disabled={showDeleted}` cho nút dòng (rẻ nhất, đúng khuôn PE), hoặc thêm nhánh `detail.isError` hiển thị "Phiếu đã xóa không xem được chi tiết". Nếu chọn cách 2 phải chặn thêm nhánh mobile-navigate.
### FLAG-3 [M] — Chú thích khai "Hộp thư viết HOA" nhưng mã KHÔNG chạm "Hộp thư"
**File:** `fe-user/src/components/Layout.tsx:354-357` (chú thích + nhánh `depth === 0`) vs `:385-408` (`StaticLeaf`)
**Đo:** "Hộp thư" KHÔNG render bằng `MenuLeaf` `USER_FIXED_TOP` (`:376-378`) render qua **`StaticLeaf`** (`:385`), lớp CSS hard-code `px-3 py-1.5 text-[12px] font-medium` **không** `uppercase`. Sửa `MenuLeaf` không với tới .
**Hệ quả:** ngay đầu sidebar sẽ "Hộp thư" (thường, 12px) nằm cạnh "TỔNG QUAN QUY TRÌNH" (HOA, 11px, semibold) đúng thứ owner muốn đồng bộ thì lại lệch; chú thích trong tả sai việc làm (bẫy cho người đọc sau).
**Ghi nhận CLEAN kèm theo:** nhánh 3 ngả `depth===0 ? … : isDeep ? … : …` **không** phá sâu depth 1 vẫn ăn nhánh , depth 2 vẫn `isDeep`; khối `isActive` nằm NGOÀI ternary nên style active nguyên vẹn (trục 6 PASS phần này).
**Tiêu chí nhận:** hoặc cùng bộ lớp sang `StaticLeaf`, hoặc sửa chú thích bỏ chữ "Hộp thư" (khai đúng phạm vi).
### FLAG-4 [L] — "Đã duyệt" của HĐ lọc `phase=9` (Đã phát hành), bỏ sót HĐ đã được duyệt nhưng chưa phát hành
**File:** `fe-user/src/components/Layout.tsx:141` (`?type=N&phase=9`) + `MyContractsPage.tsx:117-121` (chip "Đã phát hành")
**Đo:** `ContractPhase` 9 = `DaPhatHanh`. phase 8 `DangDongDau` **đã qua chữ ký BOD** (`DangTrinhKy → DangDongDau` do Director/AuthorizedSigner) về nghiệp vụ đã "được duyệt" nhưng KHÔNG lọt vào màn "Đã duyệt". Param `phase` của BE so-khớp BẰNG (`ContractFeatures.cs:305`), không diễn tả được `>= 8`.
Chính FE cũng lệch tên: nhãn menu "Đã duyệt" chip trên trang "Đã phát hành".
**Tiêu chí nhận:** owner chốt định nghĩa; nếu giữ 9 thì đổi nhãn thành "Đã phát hành" cho khớp chip, nếu muốn "đã duyệt" thì cần BE nhận `phaseFrom`/danh sách phase.
### FLAG-5 [L] — Ở `?type=N&phase=9`, lá "Danh sách" SÁNG cùng lúc với "Đã duyệt"
**File:** `fe-user/src/components/Layout.tsx:304-329` (`TRANSIENT_QUERY_KEYS` + `queryMatches`)
**Đo (chạy tay luật ở :322-328):** URL hiện tại `type=1&phase=9`, đích "Danh sách" = `type=1` `pinnedByTarget={type}` `phase` transient & không được đích ghim **bị bỏ qua** `aKeys=[type]` = `bKeys=[type]` MATCH. Đích "Đã duyệt" = `type=1&phase=9` cũng MATCH. 2 cùng sáng.
Bản S155 chỉ đóng chiều ngược ( `?type=1` không cho "Đã duyệt" sáng). **Là tật kế thừa từ PE, không phải do đợt-5 gây ra** nhưng đợt-5 nhân ra 7 nhóm nên ghi để owner biết.
**Không dính lá "Đã xóa":** `deleted` không nằm trong `TRANSIENT_QUERY_KEYS` so khớp nghiêm, phân biệt đúng. Đã kiểm.
**Tiêu chí nhận:** (tùy chọn) đối xứng hóa: nếu URL key MỘT đích anh-em ghim thì đích không-ghim phải trượt.
### FLAG-6 [L] — Endpoint mới siết `Contracts.Read` trong khi list sống không siết ⇒ mở đúng hình bẫy #44
**File:** `src/Backend/SolutionErp.Api/Controllers/ContractsController.cs:26-35` vs `:16-24`
**Đo:** `MenuPermissionHandler.cs:40-49` so khớp **ĐÚNG KHÓA** `MenuKey == "Contracts"`, **không** kế thừa cha-con (kế thừa chỉ tầng hiển thị `GetMyMenuTreeQuery`, gotcha #35). vai nào được cấp lẻ `Ct_X_Deleted` **không** dòng `Contracts` CanRead=1 sẽ thấy menu nhưng ăn **403 câm** (TanStack nuốt lỗi `isError` trang hiện "Không "/trắng).
Rủi ro hiện thấp vì: (a) `Contracts` trong `MenuKeys.All` (`MenuKeys.cs:168`) policy `Contracts.Read` tồn tại thật **trục 1 câu hỏi này PASS**; (b) `SeedAllRolesReviewReadPermissionsAsync` (`DbInitializer.cs:2157-2214`) đã đưa `Contracts` vào bộ cấp-cho-mọi-vai; (c) PE S155 dùng y hệt khuôn này đã chạy thật.
**Nhưng:** nhánh cấp cho khóa thường **bỏ-qua-nếu-đã-có** (chú thích :2211-2213 tự khai), nên trên prod dòng `Contracts` CanRead=0 sẽ KHÔNG được nâng bởi seeder. Đây rủi ro triển khai, không phải lỗi .
**Tiêu chí nhận (rẻ):** 1 câu `SELECT r.Name, p.CanRead FROM Permissions p JOIN Roles r ON r.Id=p.RoleId WHERE p.MenuKey='Contracts'` trên prod mọi vai đang dùng phải CanRead=1; hoặc curl `/api/contracts/deleted` bằng `nv.test@` kỳ vọng 200.
### FLAG-7 [L] — Khóa menu mới viết chuỗi trần, không đi qua `MenuKeys` factory (lệch chuẩn PE)
**File:** `DbInitializer.cs:1871,1875,1876` + `:2163,2167,2168` + `:2179-2180` (`"Ct_" + code + "_WfView"` …)
**Đo:** PE khi thêm đúng 2 mục tương ứng (S155) đã bổ sung factory `MenuKeys.PurchaseEvaluationApproved/Deleted` + `PurchaseEvaluationWorkflowView` (`MenuKeys.cs:140-156`). Bản **không** thêm `ContractTypeWfView/Approved/Deleted`, nối chuỗi tay **2 chỗ khác nhau** (cây menu + danh sách cấp quyền). `MenuKeys.cs:3` tự nhận "nguồn duy nhất".
**Hệ quả:** sai 1 tự 1 trong 2 chỗ = lệch câm, trình biên dịch không bắt.
**Tiêu chí nhận:** thêm 3 factory vào `MenuKeys.cs` (KHÔNG đưa vào `MenuKeys.All` giữ số canonical `Menu keys`/`Policies` không trôi, đúng như ghi chú PE `MenuKeys.cs:145-148`), rồi thay 8 chỗ nối chuỗi.
### FLAG-8 [L] — Danh sách "Đã xóa" sắp theo `DeletedAt` nhưng không trả về cột nào cho người dùng thấy mốc xóa
**File:** `ContractFeatures.cs:387` (`OrderByDescending(x => x.c.DeletedAt)`) + `:391-397` (DTO)
**Đo:** `ContractListItemDto` không `deletedAt` (trường thời gian duy nhất `CreatedAt`). Người dùng thấy thứ tự "mới xóa trước" nhưng trên dòng chỉ ngày tạo trông như sắp lộn xộn. PE gặp đúng vấn đề này đã xử bằng cách sắp theo `UpdatedAt` **và hiển thị** (`PurchaseEvaluationsListPage.tsx:557-562`).
`DeletedAt` chắc chắn số liệu: `DeleteContractCommandHandler` (`ContractFeatures.cs:632`) gọi `.Remove()` `AuditingInterceptor.cs:54-63` đặt `IsDeleted/DeletedAt/DeletedBy`. Nên đây vấn đề trình bày, không phải sai dữ liệu.
**Tiêu chí nhận:** hiện `deletedAt` (thêm trường vào DTO hoặc mượn `updatedAt` như PE), hoặc đổi sang sắp theo trường đang hiển thị.
### FLAG-9 [L] — Chế độ "Đã duyệt": lọc loại HĐ làm ở phía trình duyệt trên 100 dòng đầu ⇒ có thể ra rỗng oan
**File:** `MyContractsPage.tsx:41-49` (chỉ gửi `phase`, KHÔNG gửi `type`) + `:58-60` (`items.filter(c => c.type === typeFilter)`)
**Đo:** chế độ **không**-xóa, `type` cố ý không gửi lên máy chủ (giữ nguyên nếp ), nhưng nay cộng thêm `phase=9`: máy chủ trả 100 phase 9 của **mọi loại**, rồi trình duyệt mới lọc loại. Nếu số đã phát hành > 100 thì lá "Đã duyệt" của một loại có thể hiện 0 dù thực tế có.
Chế độ "Đã xóa" thì ngược lại — có gửi `type` lên máy chủ (`:45`), nên không dính. Lệch nhau giữa 2 nhánh trong cùng một hàm.
**Tiêu chí nhận:** gửi `type` lên máy chủ cho cả 2 nhánh (BE `ListContractsQuery` hiện **chưa** có tham số `type` — cần thêm), hoặc chấp nhận và ghi rõ giới hạn 100.
---
## Ghi nhận PASS (đã kiểm, không thành FLAG)
- **Trục 1 — `Where(IsDeleted)`:** `ContractFeatures.cs:369-370``.IgnoreQueryFilters().Where(x => x.IsDeleted)` — đúng bài 8a S155 (Ignore GỠ chứ không ĐẢO). CLEAN.
- **Trục 1 — IDOR:** `ContractFeatures.cs:375-380` sao đúng khuôn list sống `:298-303` (`DrafterUserId == userId || eligiblePhases.Contains(Phase)`), gọi lại chính `ListContractsQueryHandler.GetEligiblePhases` (internal static, cùng assembly — biên dịch được). Không nới rộng hơn list sống. CLEAN.
- **Trục 1 — policy có thật:** `Contracts``MenuKeys.All` (`MenuKeys.cs:168`) ⇒ `Contracts.Read` được đăng ký (`|All| × |Actions|`). Không phải tên policy ma. CLEAN.
- **Trục 1 — Contract CÓ query filter:** `ContractConfiguration.cs:44` `HasQueryFilter(x => !x.IsDeleted)` ⇒ list sống không rò HĐ đã xóa; endpoint mới là đường DUY NHẤT thấy chúng. CLEAN.
- **Trục 2 — định dạng khóa khớp lưới FE:** seeder sinh `Ct_<Code>_{WfView,List,Create,Pending,Approved,Deleted}`; lưới FE `Layout.tsx:130` `/^Ct_([^_]+)_(List|Create|Pending|WfView|Approved|Deleted)$/`. 7 mã loại (`ThauPhu``NguyenTacDv`) **không chứa dấu gạch dưới**`[^_]+` bắt trọn. 6/6 nhánh có nhánh trả đường dẫn (`:136-142`). CLEAN.
- **Trục 2 — `order++` chèn giữa không rối nhóm khác:** biến `order` (`DbInitializer.cs:1860`) chỉ phục vụ nhóm `Ct_*`; `Order` chỉ so trong cùng cấp anh em; nhóm loại HĐ nay cách nhau 7 thay vì 4 nhưng **thứ tự tương đối giữ nguyên**; `Hdc_*` dùng biến riêng `hdcOrder`, `Wf_*` dùng `wfOrder=95` (cha khác). Upsert chỉ đặt lại `Order` (`:1933-1937`), không đụng Label/ParentKey/Icon. CLEAN.
- **Trục 2 — labelBackfill KHÔNG đè tên admin đặt:** backfill ghi `item.Label` (`:1999-2001`); tên admin đổi qua Mig 27 nằm ở cột **khác**`DisplayLabel` (`MenuItem.cs:16`), và FE ưu tiên `displayLabel` (`Layout.tsx:202-203` `effectiveLabel`). ⇒ ép `Label` mỗi lần khởi động là **enforce có chủ đích****không** giẫm lên tuỳ biến của admin. CLEAN (đúng như lead khai).
- **Trục 3 — `Khkk_WfView`/`Khkk_Deleted` bên fe-user:** đã có trong staticMap (`Layout.tsx:78-80`). Bên fe-admin thiếu → xem FLAG-1.
- **Trục 3 — trạng thái sáng của 6 lá Khkk:** phân biệt nhau nhờ tham số `m` (`m=wfview|list|create|pending|approved|deleted`), `m` không nằm trong danh sách transient ⇒ so khớp nghiêm. CLEAN. (`ComingSoonPage.tsx:27-28` chỉ đọc `stage`, bỏ qua `m` — đúng ý đồ, `m` chỉ để tách trạng-thái-sáng.)
- **Trục 4 — nút "Tạo HĐ mới" đã ẩn ở chế độ xóa:** `MyContractsPage.tsx:122-127` bọc `{!showDeleted && …}`. Kèm chip "Đã xóa — chỉ xem" (`:112-116`) và câu rỗng riêng (`:158`). CLEAN.
- **Trục 5 — cắt hero không để lại tham chiếu mồ côi (kiểm ĐỘC LẬP, 2 phép):**
1. Đếm từng ký hiệu đã gỡ trong file hiện tại: `STAGE_TONE`, `StageCard`, `StageArrow`, `StageStatRow`, `StageLink`, `StageSoon`, `MyDashboard`, `peApproved`, `hasError`, `retryFailed`, `stats`, `Workflow`, `ChevronDown`, `PurchaseEvaluationPhase` = **0/0 hết**.
2. `npx tsc -p tsconfig.app.json --noEmit`**EXIT=0, không một dòng lỗi**. `tsconfig.app.json``noUnusedLocals: true` + `noUnusedParameters: true` ⇒ nếu còn import/biến mồ côi thì trình biên dịch đã đỏ. Đây là chứng mạnh hơn grep.
⚠️ Đo đúng phạm vi: tsconfig này **không bật** `strict`/`strictNullChecks`, nên "tsc xanh" ở đây chỉ chứng "không mồ côi + không sai kiểu thô", KHÔNG chứng an toàn null.
- **Trục 5 — phần danh sách còn nguyên:** tab 4 giai đoạn (`UserDashboardPage.tsx:1030-1034` `STAGES.map` + `active={stage === st.n}`), 2 bảng (`:536`, `:735`), cột "Hành trình" + `JourneyDots` (`:635`, `:682`, `:847`), nạp lười theo tab (`:974`, `:986`). Dữ liệu `soonDesc` vẫn có người đọc (`:881`), `tone` vẫn dùng (`:311`) ⇒ không thành rác. CLEAN.
- **Trục 5 — tiêu đề mới:** `PageHeader title="Tổng quan quy trình"` khớp nhãn menu backfill `MenuKeys.Dashboard = "Tổng quan quy trình"`. CLEAN.
- **`?type=3` của trang ma trận:** `ApprovalWorkflowApplicableType.Contract = 3` có thật (`ApprovalWorkflow.cs:56`); endpoint `GET /api/approval-workflows-v2` **chỉ cần đăng nhập**, không gắn policy (`ApprovalWorkflowsV2Controller.cs:16-26`) ⇒ **không** dính bẫy #44. Trang có sẵn nhánh rỗng lịch sự nếu admin chưa ghim quy trình nào cho loại 3 (`WorkflowMatrixViewPage.tsx:99-103`). CLEAN. *(Còn 1 điều KHÔNG kiểm được bằng đọc mã: prod đã có `ApprovalWorkflow` nào `ApplicableType=3` và `IsUserSelectable=true` chưa — nếu chưa, 7 lá "Luồng duyệt" đều hiện khung rỗng "Liên hệ Admin".)*
- ~~**Sửa chú thích D3 ở PE:** … đúng 2 chỗ …~~ 🔴 **TÔI TỰ ĐÍNH CHÍNH — xem FLAG-10.** (Tôi suýt duyệt con số này bằng mắt; chỉ khi chạy `grep -rn "IgnoreQueryFilters" src/Backend` mới lòi ra. Bài học tự ghi: **con số trong chú thích cũng là một khẳng định đo được — phải chạy phép đếm, không đọc lướt.**) Phần CÒN LẠI của chú thích vẫn đúng và giữ nguyên giá trị: cả 2 handler list-deleted đều kèm `.Where(x => x.IsDeleted)`, và lệnh cấm gộp thành helper dùng chung vẫn nguyên.
### FLAG-10 [M] — Chú thích vừa sửa trong chính diff này khai SAI con số, và nó là loại sai tự lan
**File:** `PeSoftDeleteFeatures.cs:184` (dòng ĐƯỢC SỬA trong diff) + `ContractFeatures.cs:356` (dòng MỚI trong diff)
**Khẳng định trong mã:** "NAY CÓ **2** chỗ dùng `IgnoreQueryFilters`" và "Chỗ **THỨ HAI** trong `src/Backend`".
**Phép phản chứng (1 lệnh, chạy thật):** `grep -rn "IgnoreQueryFilters" src/Backend --include=*.cs` ⇒ ngoài 2 chỗ nói trên còn **`ApprovalWorkflowV2AdminFeatures.cs`** dùng ở **15 vị trí** (`:957, 964, 971, 978, 985, 992, 999` — 7 bảng `*LevelOpinions`; và `:1038, 1042, 1046, 1050, 1054, 1058, 1062` — 7 phép dò "phiếu cha còn sống không"). Tổng thực tế ≈ **3 tệp / 17 vị trí**, không phải 2.
**Vì sao không chỉ là lỗi chính tả:** câu này được viết dưới dạng **luật** ("chỗ DUY NHẤT/THỨ HAI ⇒ CẤM gộp helper"). Người đọc sau tin vào con số để kết luận "mình đang sửa chỗ duy nhất còn lại" trong khi thực tế có cả một cụm 15 chỗ ở module khác. Con số sai từ S155 ("DUY NHẤT") đã có cơ hội được sửa trong đúng lượt này, nhưng bị viết lại thành một con số sai mới.
**Tiêu chí nhận:** bỏ hẳn con số đếm ra khỏi chú thích (nêu **luật** thay vì **số**: "mọi nơi dùng `IgnoreQueryFilters` để liệt kê bản ghi đã xoá BẮT BUỘC kèm `.Where(x => x.IsDeleted)`"), hoặc giữ số nhưng nói rõ phạm vi đếm ("2 chỗ **thuộc lớp list-deleted**"; các chỗ trong `ApprovalWorkflowV2AdminFeatures` thuộc lớp khác — dò mồ côi). Kèm 1 lệnh grep trong chú thích để người sau kiểm lại được.
- **Không có dấu vết giả-lập:** diff không chứa `// Mock` / `alert(` / `TODO.*wire` / `placeholder`. Endpoint mới là truy vấn thật, FE gọi thật.
- **Chứng cứ cho FLAG-2 (đã truy tới đĩa):** truy vấn chi tiết HĐ nằm ở `ContractFeatures.cs:462` (`GetContractQuery`) → `:471` `db.Contracts.AsNoTracking()`**không** `IgnoreQueryFilters` ⇒ HĐ đã xoá chắc chắn rơi vào `NotFoundException` (404). Không phải suy đoán.
---
## Phạm vi KHÔNG kiểm (khai thật)
- **Không chạy `dotnet build` / `dotnet test`.** Phần BE chỉ soi tĩnh. Rủi ro biên dịch thấp (chỉ dùng lại `internal static` cùng assembly, DTO có sẵn) nhưng **tôi chưa có chứng**.
- **Không kiểm chứng dữ liệu prod:** (a) dòng `Permissions` khoá `Contracts` của 13 vai (FLAG-6); (b) có `ApprovalWorkflow` nào `ApplicableType=3, IsUserSelectable=1` chưa. Cả hai cần 1 câu `sqlcmd` hoặc 1 lần curl — lead chốt soi tĩnh nên tôi dừng đúng ranh.
- **Không kiểm 7 tệp ngoài phạm vi lead khai** (`docs/*`, `scripts/*`, `.claude/*`) — chúng có mặt trong cây làm việc và sẽ đi cùng lượt commit nếu dùng `commit -a`.
- **Không kiểm `git status` cho `Migrations/`** — đợt này không đụng migration nào (0 tệp trong `Migrations/`), nên bẫy "migration chưa được git theo dõi" (S155) không áp dụng.
- **Chưa chạy `npm run build` cho fe-admin** — FLAG-1 là thiếu dữ liệu trong bản đồ tuyến, không phải lỗi kiểu, nên trình biên dịch sẽ không bắt được; chạy build cũng không đổi kết luận.
---
## PHÁN QUYẾT
**PASS_WITH_FLAGS — 10 FLAG (0 High / 4 Medium / 6 Low).**
Không có lỗi nào đủ nặng để chặn commit: đường vào dữ liệu của endpoint mới **đúng** (có `.Where(IsDeleted)`, có rào IDOR sao đúng khuôn list sống, policy có thật, không rò dữ liệu). Việc cắt hero **sạch**, có chứng độc lập (`tsc` EXIT=0 với `noUnusedLocals`).
Bốn việc nên xử **trước khi bàn giao cho người dùng thử** (không nhất thiết trước khi commit):
1. **FLAG-2** — bấm dòng ở màn "Đã xóa" cho ra 2 panel trắng; một dòng `disabled={showDeleted}` là xong, đúng khuôn PE đã có.
2. **FLAG-1** — thêm 2 dòng vào `staticMap` của fe-admin, nếu không 2 lá menu biến mất im lặng bên app quản trị.
3. **FLAG-10** — con số trong chú thích sai và nó được viết dưới dạng luật, dễ dẫn người sau đi sai.
4. **FLAG-3** — hoặc làm nốt "Hộp thư", hoặc sửa chú thích cho khớp việc mã thực sự làm.
Sáu FLAG mức thấp còn lại là chuyện định nghĩa nghiệp vụ (FLAG-4), tật kế thừa từ PE (FLAG-5), rủi ro triển khai cần một câu truy vấn để yên tâm (FLAG-6), nợ quy ước (FLAG-7), trình bày (FLAG-8) và giới hạn phân trang (FLAG-9).
---
**END — TOTAL: 10 FLAG (0 High / 4 Medium / 6 Low) · VERDICT: PASS_WITH_FLAGS**