# 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` là `(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` mà `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 -->
