Files
solution-erp/.claude/workflows/runs/2026-07-27-S155-pe-delete-approver/sub-implementer-frontend-2.md
2026-07-27 10:50:36 +07:00

125 lines
9.1 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-implementer-frontend-2 — [F-11] nối cờ F6 `allowApproverDelete` vào Designer V2
> run `2026-07-27-S155-pe-delete-approver` · ĐỢT 1 · vai `implementer-frontend`
> Spec canonical: `spec-pe-delete-approver.md` §F.4 mục **F-11** (dòng 303) + §F.2 bảng 13 thao tác hạng **2a ✅ AN TOÀN**.
> Ghi TRONG LÚC LÀM (5/5 sub run này mất return vì #53) — file này luôn hợp lệ giữa chừng.
## 0. Phạm vi tự chốt trước khi viết dòng nào
| Làm | Không làm (đợt 2) |
|---|---|
| 5 dây Designer fe-admin + ô tick F6 | nút Xóa ở màn duyệt (`PeWorkflowPanel.tsx`) |
| 1 dây hiển thị chỉ-xem fe-user (`WorkflowMatrixViewPage:282`) | menu restructure (`Layout.tsx` regex, `menuKeys`) |
| type/DTO FE tương ứng (2 chỗ) | màn "Đã xóa" |
**Ý nghĩa cờ (viết đúng, không suy diễn):** **PER-NGƯỜI**, KHÔNG per-Cấp. 1 `Level` row = 1 người
(`ApproverUserId` là field của row); "Cấp" chỉ là tập row cùng `Order`. Owner chốt per-ROW để giữ
sát ý *"người đứng đầu phòng"* = **1 người, admin chỉ đích danh** (spec §B2 dòng 60-66).
Khối UI tick nằm trong ô `Quyền duyệt {tên NV}` (`:928-930`) ⇒ ngữ cảnh per-người đã hiện sẵn trên màn.
## 1. Đối chứng: F5 `allowApproverFinalize` đi qua ĐÚNG 5 chỗ nào (đọc trước, không tin spec suông)
Grep `allowApproverFinalize` trên `fe-admin/` → 5 hit trong `ApprovalWorkflowsV2Page.tsx` ngoài ô tick:
| Dây | Line (trước sửa) | Vai trò | Hỏng gì nếu thiếu |
|---|---|---|---|
| 1 | `:54` | `LevelDto` — mirror BE `AwAdminOverviewDto` (đọc VỀ) | TS2339 khi đọc `l.allowApproverDelete` ở dây 3 |
| 2 | `:110` | `EditLevelEntry` — state form đang soạn | TS2339 ở `updateField` + dây 4/5 |
| 3 | `:159` | `copyFromDefinition()`**Nhân bản** version cũ | tick cũ mất trắng khi admin bấm "Nhân bản" |
| 4 | `:177` | `makeDefaultLevelEntry()` — "+ Thêm NV" | entry mới thiếu key ⇒ TS2739 (object literal thiếu field) |
| 5 | `:605` | **payload POST** `/approval-workflows-v2` | 🔴 **BẪY**: tick xong KHÔNG gửi lên BE |
🔴 **Vì sao dây `:605` là bẫy thật (không phải cảnh báo lễ nghi):** payload POST là object literal
**liệt kê từng field bằng tay** (`:594-606`), KHÔNG spread `...e`. TypeScript **không** báo lỗi khi
thiếu field (thừa field mới lỗi, thiếu thì chỉ là object hẹp hơn) ⇒ build vẫn PASS, UI tick vẫn
hiện dấu tick, `onChange` vẫn chạy, state vẫn đổi — **chỉ có BE không bao giờ nhận**. Không có
compiler/test nào bắt; chỉ lộ khi UAT tick → lưu → mở lại thấy trống, hoặc tệ hơn: quyền xóa im
lặng không tới người được chỉ định.
## 2. Thay đổi đã áp — 3 file, 10 neo
### 2.1 `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` (+28 dòng) — 5 dây + ô tick
| # | Line MỚI (cũ) | Nội dung |
|---|---|---|
| Dây 1 | `:58` (54) | `LevelDto` + `allowApproverDelete: boolean` ngay dưới `allowApproverFinalize` |
| Dây 2 | `:116` (110) | `EditLevelEntry` + `allowApproverDelete: boolean` |
| Dây 3 | `:168` (159) | `copyFromDefinition()``allowApproverDelete: l.allowApproverDelete ?? false` |
| Dây 4 | `:188` (177) | `makeDefaultLevelEntry()``allowApproverDelete: false` |
| **Dây 5** | **`:620`** (605) | **payload POST** `/approval-workflows-v2``allowApproverDelete: e.allowApproverDelete` |
| Ô tick | `:1025-1034` | `<label class="col-span-2 … text-rose-700">` ngay DƯỚI ô F5 (kết ở `:1024`) |
Nhãn ô tick (tiếng Việt, per-người): **"Cho phép NV này XÓA phiếu khi đến lượt duyệt của mình"**.
Màu `text-rose-700` phân biệt với F5 `text-emerald-700` — hành vi phá huỷ, cố ý cho khác màu.
`updateField``(field: keyof EditLevelEntry, value: boolean)` (`:940`) ⇒ chuỗi `'allowApproverDelete'`
được TS kiểm, không phải magic string.
### 2.2 `fe-user/src/types/approvalWorkflowV2.ts` (+7/1)
`AwLevelDto` + `allowApproverDelete: boolean` (`:27`); header đổi **"8 Allow* flag"** → **"9 Allow* flag"** (`:5-6`).
### 2.3 `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` (+2) — dây chỉ-xem
`:284` (F5 ở `:282`) — `<FlagRow active={r.level.allowApproverDelete} … colSpan2 />` cùng nhãn với Designer.
### 2.4 🔴 Dây `:605`/`:620` — KHAI RIÊNG THEO YÊU CẦU: **ĐÃ NỐI**, và đây là bằng chứng nó thật sự là bẫy
Kiểm cuối cùng, sau khi đã build xong. Grep `allowApproverDelete` toàn repo `*.ts,*.tsx` = **10 hit / 3 file**,
trong đó hit `ApprovalWorkflowsV2Page.tsx:620` nằm **bên trong** `s.levelEntries.map(e => ({…}))` của
`await api.post('/approval-workflows-v2', …)`.
**Ba tầng cùng im lặng nếu thiếu dòng này** (đo, không đoán):
1. **TS không bắt** — payload là object literal liệt kê tay từng field (`:609-621`), **không** `...e`.
Thiếu key = object hẹp hơn, hợp lệ với `any` param của `api.post` ⇒ build vẫn `✓ built`.
2. **BE không bắt**`AwLevelInput` là positional record với **default `= false`**
(`ApprovalWorkflowV2AdminFeatures.cs:216-230`) ⇒ JSON thiếu key bind thẳng `false`, **không 400**,
không validator nào kêu.
3. **UI không bắt** — ô tick là controlled input trên state FE, tick vẫn hiện dấu, vẫn đổi màu.
⇒ Chuỗi "tick → lưu → toast xanh → DB ghi `false`" **hoàn toàn không có tín hiệu lỗi ở bất kỳ tầng nào**.
Chỉ lộ khi UAT mở lại thấy ô trống, hoặc tệ hơn: người được chỉ định bấm xóa thì **403** mà không ai hiểu vì sao.
## 3. Verify
| Phép đo | Kết quả |
|---|---|
| `npm run build` **fe-admin** | ✅ **PASS**`✓ 1950 modules transformed` · `✓ built in 21.05s` · bundle `index-_gosUXpu.js` 1,630.28 kB · **0 TS error** |
| `npm run build` **fe-user** | ✅ **PASS**`✓ 1939 modules transformed` · `✓ built in 8.32s` · bundle `index-B-nir85X.js` 1,536.32 kB · **0 TS error** |
| Warning | 3 cái, **pre-existing 100%**: `@import` phải đứng trước rule · chunk >500 kB · `realtime.ts` INEFFECTIVE_DYNAMIC_IMPORT |
| Dây đã nối | **5/5** + ô tick + 2 neo fe-user (grep 10 hit / 3 file) |
| `git status` | đúng **3 file code** + sub-MD này. (`.session-counter.json` = harness, không phải tôi; `sub-implementer-backend-1.md` = lane BE song song) |
**Mirror / SHA:** slice này **KHÔNG có cặp mirror nào** — Designer chỉ tồn tại ở `fe-admin`
(fe-user không có `ApprovalWorkflowsV2Page.tsx`), matrix chỉ-xem chỉ tồn tại ở `fe-user`
(fe-admin không có `WorkflowMatrixViewPage.tsx`, cũng không có `types/approvalWorkflowV2.ts`).
⇒ 3 file, 0 cặp SHA. Không phải bỏ sót mirror.
**Chứng minh đợt-2 chưa bị đụng:** `PeWorkflowPanel.tsx` md5 **`02671be6334110028c55fe82f5e70dee`** ×2 app
— khớp đúng con số spec §E3 ⇒ file đặt nút Xóa còn nguyên vẹn và vẫn byte-identical.
## 4. 🔴 Phụ thuộc chéo lane — cần lane BE xác nhận (chưa xong lúc tôi return)
Đo **2 lần**, lane BE land giữa chừng:
- **Lúc bắt đầu:** `AllowApproverDelete` trong `src/Backend` = **0 hit**.
- **Lúc return (sau build):** đã có **entity** `ApprovalWorkflow.cs:169 public bool AllowApproverDelete`
+ migration `20260727033522_AddPeAllowApproverDelete` (3-file). ✅ **Tên khớp chính xác** cái FE chờ.
NHƯNG `SolutionErp.Application/` vẫn **0 hit****4 neo DTO chưa nối**.
⇒ Đúng lúc này, tick trên Designer **vẫn chưa tới được DB**: cột đã có, ô tick đã có, nhưng
không có đường DTO nối 2 đầu. Cần lane BE đóng nốt 4 neo dưới đây (đây là số dòng của khuôn F5,
đo trực tiếp trên file):
- **Đọc về:** `AwLevelDto` (`…AdminFeatures.cs:41`) phải có `bool AllowApproverDelete` + projection `:188`
⇒ camelCase mặc định của System.Text.Json cho ra đúng `allowApproverDelete``LevelDto:58` chờ.
- **Ghi lên:** `AwLevelInput` (`:230`) phải có `bool AllowApproverDelete = false` + map entity `:368`.
🔴 Nếu BE đặt **tên khác** (vd `AllowDeleteWhilePending`) thì FE gửi key lạ → **bị bỏ qua im lặng**,
đúng **cùng một class lỗi** với bẫy `:620`. Đây là chỗ duy nhất 2 lane có thể lệch mà cả 2 build đều xanh.
## 5. Đã CỐ Ý không làm (ranh giới đợt 1/đợt 2)
| Bỏ qua | Lý do |
|---|---|
| Nút Xóa ở màn duyệt (`PeWorkflowPanel.tsx` ×2) | đợt 2, spec mục 14/14a — cần prop + mutation mới |
| `types/purchaseEvaluation.ts` (`fe-admin:274/427` · `fe-user:276/429`) | đó là bundle **PE detail** (`levelOptions` + `approvalFlow`), BE đợt 1 **không** trả cờ này ⇒ khai `boolean` non-optional bây giờ = **type nói dối**. Thuộc gate E4 đợt 2 |
| `Layout.tsx` regex, `menuKeys.ts`, màn "Đã xóa" | checklist mục 3-28, đợt 2 |
| Luật 4-place mirror | không kích: 0 route mới, 0 menu key mới, 0 page mới (spec §C xác nhận thêm nút/mục không kích 4-place) |
<!-- END sub-implementer-frontend-2 -->