**HEADLINE:** Giữ "save = advance" (1 nút) — sửa đúng 1 chỗ BE: gỡ/gate ApplyDrafterBypassOnSubmitAsync để creator ĐÁP xuống ô Cấp-1 của mình tự comment+ký thay vì bị auto-bypass ký hộ; FE gần như 0 đổi. Self-approval = bước ĐỀ-XUẤT hợp lệ, CHỈ rủi ro nếu slot creator bật AllowApproverFinalize/SkipToFinal.

## Kết luận cốt lõi
Yêu cầu "creator comment vào ô duyệt của chính họ → save → advance" **KHÔNG cần đổi save-semantics và KHÔNG cần tách 2 nút**. Cơ chế "comment-in-own-box → save → advance" đã tồn tại nguyên vẹn trong dialog Duyệt (`Textarea comment` → "Xác nhận" → `ApproveV2Async` UPSERT LevelOpinion + advance). Khiếm khuyết THẬT không nằm ở save-semantics mà ở **auto-bypass im lặng**: khi drafter (=PRO=Cấp-1) gửi duyệt, `ApplyDrafterBypassOnSubmitAsync` (service:571-681, gọi tại :260-261) **tự ký hộ** ô Cấp-1 với comment đóng hộp `"(duyệt tự động — người soạn phiếu là người duyệt cấp này)"` (:619) rồi advance qua luôn → creator KHÔNG bao giờ đáp xuống ô mình để gõ ghi-chú ngân-sách thật (đúng cái screenshot Cấp1 Bình muốn).

## Sơ đồ luồng — AS-IS vs TO-BE
```
AS-IS (auto-bypass nuốt Cấp-1 của creator):
  Nháp --gửi duyệt--> ChoDuyet, pointer=(Step0,Cấp1)
     └─ ApplyDrafterBypassOnSubmit: creator là Cấp1 → UPSERT opinion CANNED
        "(duyệt tự động…)" + Approval row AutoApprove + advance → Cấp2 (Trà)
     => creator MẤT ô comment của mình. Screenshot "Bình comment ngân-sách" KHÔNG đạt.

TO-BE (creator tự ký ô mình):
  Nháp --gửi duyệt--> ChoDuyet, pointer ĐỨNG tại (Step0, Cấp thấp nhất của creator)
     => currentApproval populated → banner "✓ Đến lượt bạn duyệt" + nút "✓ Duyệt"
     => creator mở dialog, gõ ghi-chú NS thật, "Xác nhận"
        → ApproveV2Async UPSERT opinion (comment THẬT, SignedByUserId=creator) + advance → Trà
     => Trà ký → Bước2 Bình lại ký → Chương "ok" … (đúng screenshot)
```

## Khuyến nghị save-semantics: GIỮ 1 NÚT ("save = advance")
| Tiêu chí | 1 nút (save=advance) — **KHUYẾN NGHỊ** | 2 nút (lưu nháp ý-kiến + submit) |
|---|---|---|
| Khớp yêu-cầu | ✅ dialog gõ comment → Xác nhận = "comment→save→advance" y hệt | Thừa — user không đòi giai-đoạn nháp |
| Chi phí | 1 method BE (gate bypass), FE ~0 | Schema mới (opinion cần state Draft / `SignedAt` nullable), endpoint mới, nút+handler FE mới, DTO field mới |
| Audit | Chữ-ký atomic (Approval row + opinion cùng lúc) | Nửa-trạng-thái "opinion chưa advance" làm rối reject/return + terminal |
| Sửa-lại-comment | Đã có: return→UPSERT overwrite (latest-write-wins, entity doc:17-19) | Không lợi thêm |

"Lưu nháp rồi mới đẩy" đã có sẵn xuyên **chu-kỳ Trả-lại**: opinion tìm theo `(PEId, ApprovalWorkflowLevelId)` và bị OVERWRITE mỗi lần ký lại (service:766-790) → không cần draft-state riêng.

## Bảng thay đổi BE
| # | File:line | Method/field | Đổi gì |
|---|---|---|---|
| B1 (chính) | `PurchaseEvaluationWorkflowService.cs:571-681` | `ApplyDrafterBypassOnSubmitAsync` | **NGỪNG tự ký ô CHÍNH CHỦ của creator.** Phương án **A1 (khuyến nghị, giữ S60 skip NV cấp dưới):** đổi `k=drafterSlots.Max` (:592) + `bypassedOrders = order<=k` (:595) → chỉ bypass `order < drafterSlots.Min` (thuần NV dưới creator); set pointer ĐỨNG tại `minOwnOrder` (thay `:647-680` advance qua k). Nếu creator là Cấp-1 (phổ biến/screenshot) → subordinate rỗng → pointer giữ (0,1), 0 auto-sign. **Phương án A2 (đơn giản nhất):** xóa call `:260-261` → creator luôn đáp (0,1) tự ký mọi cấp, NHƯNG mất skip-NV-cấp-dưới của S60. |
| B2 (governance guard — nên thêm) | `ApproveV2Async` ~:805 (skipToFinal) + ~:867 (`AllowApproverFinalize`) | thêm điều kiện `actorUserId == evaluation.DrafterUserId` | Chặn **creator TỰ kết-thúc/vượt-cấp phiếu mình**: nếu actor là người soạn mà slot bật finalize/skip → `throw Forbidden("Người soạn không được tự kết thúc/duyệt vượt cấp phiếu của mình — phải để cấp trên duyệt.")`. Đây là kiểm-soát self-approval THẬT (xem đánh giá dưới). |
| B3 (test — BẮT BUỘC, spec-change) | `tests/…/Services/PeSubmitGuardAndBypassTests.cs:28` | test bypass cũ | Cập-nhật kỳ-vọng: creator=Cấp1 sau submit → pointer ĐỨNG tại Cấp1 (không advance, không opinion canned); creator=Cấp2+NV-dưới → NV-dưới bypass, pointer=Cấp2 (rule §7 spec-change = update test cùng commit). |

**Match-approver KHÔNG chặn creator:** `ApproveV2Async:728-739` yêu-cầu `actorUserId ∈ pendingLevelGroup.ApproverUserId`. Creator LÀ ApproverUserId của Cấp-1 mình → pass. Không cần đụng.

## Bảng thay đổi FE (2 app byte-identical — diff exit 0, 903 dòng — sửa gì PHẢI mirror cả 2)
| # | File:line | Component/handler | Đổi gì |
|---|---|---|---|
| F0 | — | — | **Happy-path: 0 đổi chức-năng.** Banner `isV2Pending`+`actorInV2Level` (:98-103,:407-426) + nút Duyệt (:441,:449-472) + dialog comment→Xác nhận→`transition.mutate` (:153-207) đã chạy đúng khi pointer đứng ở ô creator. Nút Trả-lại/Từ-chối vốn ĐÃ ẩn cho creator (`:446` `drafterUserId===currentUser.id`); Duyệt KHÔNG ẩn. |
| F1 (polish, tùy chọn) | `PeWorkflowPanel.tsx:414-424` + dialog title `:492-496` | banner/label | Khi `drafterUserId===currentUser.id && actorInV2Level`: đổi chữ thành "Ý kiến của bạn (người soạn — Cấp {order})" để creator hiểu đang ký ô đề-xuất của mình. Thuần cosmetic. |

## SignedByUserId vs ApproverUserId — ảnh hưởng
- `ApprovalWorkflowLevel.ApproverUserId` (design-time, entity:88) = NV được cấu hình. `LevelOpinion.SignedByUserId` (runtime, entity:31) = người ký thật.
- Creator ký ô mình → `SignedByUserId == ApproverUserId` → **KHÔNG** hiện banner "Admin duyệt thay" (chỉ hiện khi lệch, service:762). Sạch, không side-effect.
- TO-BE cho opinion THẬT: `SignedByUserId=creator, Comment=ghi-chú-NS-thật` — chữ-ký audit GENUINE, **tốt hơn** canned comment của auto-bypass. Đây là cải-thiện governance, không phải nới lỏng.

## Đánh giá SELF-APPROVAL
**Là bước ĐỀ-XUẤT hợp lệ, KHÔNG phải lỗ-hổng kiểm-soát — với 2 điều kiện:**
1. Cấp-1 của creator = chữ-ký ĐỀ-XUẤT/chuẩn-bị (PRO trình phương-án + ngân-sách); thẩm-quyền DUYỆT thật nằm ở cấp trên (CCM Cấp-2, BGĐ/CEO Bước cuối). Creator **không thể** tự đẩy tới `DaDuyet` (cấp mình không phải terminal) → phân-tách nhiệm-vụ giữ nguyên.
2. Có vết audit đầy đủ: `PurchaseEvaluationApproval` row + `LevelOpinion.SignedByUserId=creator` phân-biệt rõ chữ-ký.

**THÀNH lỗ-hổng CHỈ KHI** slot Cấp-1 của creator bật `AllowApproverFinalize` (F5, entity:142) hoặc `AllowApproverSkipToFinal` (F2, entity:131) → creator một mình duyệt phiếu mình lên `DaDuyet`, bỏ CEO. **Hiện KHÔNG có guard** chặn việc này (finalize/skip chỉ gate theo admin-config slot, không xét actor==drafter). → Đề-xuất guard B2.

**Ghi nhận tích-cực:** chuyển từ auto-bypass-im-lặng (approve hộ không ai xác-nhận) sang ký-tay-tường-minh (bắt creator chủ-động nêu lý-do + tạo chữ-ký) là **nâng-cấp** minh-bạch, không giảm.

## Yêu cầu (3) Reject/Trả-lại về đúng người → re-comment → forward lại: ĐÃ ĐỦ, 0 code mới
Multi-mode Trả-lại (service `ApplyReturnModeAsync:348-548`) `OneLevel/OneStep/Assignee` giữ `Phase=ChoDuyet` + lùi pointer về đúng Cấp/người; `Assignee` (:485-510) pick từ NV đã ký (gồm creator). Người nhận lại → banner "Đến lượt bạn" → Duyệt → UPSERT overwrite comment + advance. Chỉ cần **admin bật** `AllowReturnToAssignee` (entity:110) trên slot cấp trên — config, không phải code.

## Edge cases
- Creator xuất-hiện nhiều cấp (screenshot: Bình ở Bước1-Cấp1 VÀ Bước2-Cấp1): A1/A2 xử đúng — creator tự ký từng cấp khi pointer chạm; không auto-sign chéo.
- Notify: creator không nhận chuông "cần bạn duyệt" phiếu mình (service:1164-1169 S86) — giữ nguyên, thấy ở "Phiếu của tôi". Không double-notify.
- Submit-guard Section-3 (:175-229) chạy TRƯỚC bypass → không bị ảnh hưởng.
- A2 đảo ngược ý S60 "TP tạo phiếu vẫn bắt NV cấp dưới duyệt lại" → dùng A1 nếu muốn giữ skip-NV.

Token cost estimate: ~62K

## Open Questions
1. **A1 hay A2?** A1 = giữ skip-NV-cấp-dưới của S60 (creator là TP → NV dưới vẫn auto-qua, creator tự ký ô mình) — phức tạp hơn ~15 dòng. A2 = xóa hẳn bypass, creator tự ký MỌI cấp mình chiếm (kể cả có NV dưới) — đơn giản nhất nhưng bỏ tính-năng S60. Owner chốt còn muốn skip-NV-cấp-dưới không.
2. **Guard B2 self-finalize:** có chặn creator-slot bật AllowApproverFinalize/SkipToFinal không? Nếu Designer cho phép cố-ý (vd phiếu giá-trị nhỏ creator được tự-chốt) thì B2 phải thành cảnh-báo mềm thay vì Forbidden cứng.
3. **Xác nhận KHÔNG cần 2-nút (lưu-nháp-ý-kiến riêng):** nếu owner thực-sự muốn creator "lưu comment, để đó, lát mới đẩy" (không advance ngay) thì mới cần Option B (schema+endpoint+DTO). Cần owner xác-nhận yêu-cầu chỉ là "tự ký ô mình" (1 nút đủ) hay có nhu-cầu draft-rời-advance thật.
