diff --git a/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-be-fable.md b/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-be-fable.md index 969c80c..d3d9a77 100644 --- a/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-be-fable.md +++ b/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-be-fable.md @@ -3,13 +3,80 @@ > Trạng thái: ĐANG ĐO — đổ ruột liên tục. Mỗi claim kèm file:line. ## §1 Endpoint map trang approval-workflows-v2 (admin) -(CHƯA ĐO) + +**Controller duy nhất:** `src/Backend/SolutionErp.Api/Controllers/ApprovalWorkflowsV2Controller.cs` — route `api/approval-workflows-v2` (:17), class-level `[Authorize]` trần (:18). + +| Action | Verb+Route | Authz | file:line | +|---|---|---|---| +| Overview (list all types + versions + history) | `GET ?applicableType=&isUserSelectable=` | **authenticated-only, 0 policy** (CỐ Ý — Drafter cần list để pick lúc create, comment :10-15) | :21-26 | +| Tạo version mới | `POST` | `Workflows.Create` | :28-34 | +| Sửa-tại-chỗ (giữ Id/Version/IsActive) | `PUT {id}` | `Workflows.Create` | :45-51 | +| Ghim/Bỏ ghim (`IsUserSelectable`) | `PATCH {id}/user-selectable` | `Workflows.Create` | :56-62 | +| Xoá version | `DELETE {id}` | `Workflows.Create` | :64-70 | + +**Tham số phân loại = `ApplicableType` (int enum), KHÔNG code-prefix.** Enum `ApprovalWorkflowApplicableType` @ `Domain/ApprovalWorkflowsV2/ApprovalWorkflow.cs:53-71`: DuyetNcc=1 · DuyetNccPhuongAn=2 · Contract=3 · Proposal/Leave/Ot/Vehicle/ItTicket/Travel=4-9 · **ContractSigningPlan=10** (:70). Trang DuyetNcc = `?applicableType=1`; "NCC và Giải pháp" = 2; KHKK = 10. + +**Handler Overview:** `Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs` — filter generic `d.ApplicableType == typeEnum` (:123-127) + `IsUserSelectable` (:129-132); khi `applicableType=null` group theo `Enum.GetValues<>()` toàn bộ (:199-213) ⇒ **GENERIC, 0 hardcode chặn type-10**. Label map `AwLabels.Type` :90-98 CÓ entry type-10 "Kế hoạch ký kết HĐ" (:97) — nhưng label này là 1-per-type, KHÔNG phân biệt 8 nhóm N1-N8 (8 nhóm cùng type=10 sẽ gộp chung 1 `AwTypeSummaryDto` với 1 `Active` duy nhất — xem §2 hệ quả). + +**9 checkbox = 9 cột `bool` per-Level row** trên `ApprovalWorkflowLevel` (`ApprovalWorkflow.cs:99-176`) — schema DÙNG CHUNG mọi ApplicableType: +| # | Checkbox UI | Flag | line | +|---|---|---|---| +| 1 | Trả về 1 Cấp trước | `AllowReturnOneLevel` (F1m1) | :120 | +| 2 | Trả về 1 Bước trước | `AllowReturnOneStep` (F1m2) | :123 | +| 3 | Trả về Người chỉ định | `AllowReturnToAssignee` (F1m3) | :126 | +| 4 | Trả về Drafter (default) | `AllowReturnToDrafter` (F1m4, default TRUE) | :129 | +| 5 | Sửa Section 2 lúc duyệt | `AllowApproverEditDetails` (F3) | :133 | +| 6 | Sửa Section ngân sách | `AllowApproverEditBudget` (F4) | :139 | +| 7 | Duyệt thẳng Cấp cuối | `AllowApproverSkipToFinal` (F2) | :147 | +| 8 | Duyệt là KẾT THÚC (không CEO) | `AllowApproverFinalize` (F5) | :158 | +| 9 | NV XÓA phiếu khi đến lượt | `AllowApproverDelete` (F6, Mig 68) | :173 | + +**🔴 Create handler ĐÃ CÓ nhánh type-10 đích danh** (`ApprovalWorkflowV2AdminFeatures.cs:387`): `AllowApproverSkipToFinal = typeEnum != ContractSigningPlan && l.AllowApproverSkipToFinal` — **ép-false F2 CHO RIÊNG type-10** (K3-test O-1: KHKK service 0 lần đọc cờ này, echo true = config-lie #78). `AllowApproverFinalize` từng ép-false @S161 nhưng ĐÃ GỠ @S165 (service KHKK port level-finalize) — comment :376-386. + +**PUT sửa-tại-chỗ (S155 F-1):** T1 khung-không-đổi (tập Step.Order + Level.Order bất biến, :552-559 + :572-574 + :593-595 + :636-648) + T2 luật-chơi-không-đổi (Code/ApplicableType/CeoApprovalThreshold/F5/F2 = ECHO-only, lệch ⇒ 409, :539-550 + :598-603). Guard chữ-ký 2 tầng (:665-724): `CountLiveOpinionsAsync` + `ListRetainedOpinionsAsync` — cần kiểm 2 helper này có đọc `ContractSigningPlanLevelOpinions` không (xem §2/§4). Transaction rào @ :744-745. + +**Deactivate-per-type khi POST:** `:339-343` — deactivate MỌI workflow `ApplicableType == typeEnum && IsActive` ⇒ **only-ONE-active-per-TYPE. Với 8 nhóm KHKK cùng type=10: tạo version mới cho N2 sẽ TẮT IsActive của N1..N8 còn lại** ⇒ đây là site chặn #1 (xem §2). + +### §1-bis Phần sau của AdminFeatures (line 800-1205) + +- **Guard chữ-ký 3-site ĐÃ phủ KHKK 3/3:** `CountLiveOpinionsAsync` :961-965 + `ListRetainedOpinionsAsync` :1033-1038 + `PurgeOrphanOpinionsAsync` :1099-1102 đều có nhánh `ContractSigningPlanLevelOpinions` (site liệt-kê-thủ-công, comment :957-960 tự khai luật "module mới PHẢI vào CẢ 3"). `DetachTrackedOpinionsOn` :1122-1136 quét theo TÊN CỘT metadata ⇒ tự phủ (comment :1120 nói "7 bảng" — stale nhẹ, thực tế 8). +- **🔴 GAP changelog PUT sửa-tại-chỗ:** `:863-893` chỉ ghi vết vào `PurchaseEvaluationChangelogs` khi `ApplicableType ∈ {DuyetNcc, DuyetNccPhuongAn}` — **KHKK type-10 bị SKIP** (comment :857-862 tự khai: `ContractSigningPlanChangelogs` là changelog PHIẾU, "wire nếu cần ở W2/W3" — CHƯA wire). Sửa workflow nhóm KHKK đang có phiếu treo ⇒ phiếu 0 vết, chỉ có `def.UpdatedAt` (:851). +- **🔴 GAP DELETE version 0 usage-check:** `DeleteAwDefinitionCommandHandler` :1193-1205 = load + `Remove` + Save, **KHÔNG kiểm phiếu đang pin** (comment :1187-1189 tự khai "Sau UAT ... cần check usage trước khi delete" — nợ từ Mig 22 era). Hard-delete; hành vi thực tế phụ thuộc FK của cột pin từng module (cần đo FK `ContractSigningPlans.ApprovalWorkflowId` — xem §2). +- **SetUserSelectable** :1173-1185 generic by-Id ⇒ type-10 dùng được 0-đổi. ## §2 Seed 8 workflow nhóm KHKK + phân loại -(CHƯA ĐO) + +**Seed = `DbInitializer.SeedKhkkGroupWorkflowsAsync`** (`Infrastructure/Persistence/DbInitializer.cs:549-687`, K3 nhịp-1 S164/S165) — KHÔNG phải Mig 71 (Mig 71 chỉ AddColumn `ApprovalGroup` lên phiếu + backfill=1; workflow ROWS do DbInitializer đẻ lúc boot, INFRASTRUCTURE seed not-gated DemoSeed, comment :539). + +- **Phân loại 8 nhóm = `Code` string `KHKK-N{n}`** (:609), n=1..8 (:607). TẤT CẢ 8 cùng `ApplicableType=ContractSigningPlan(10)` (:616). Name `"Duyệt KHKK — Nhóm {n}"` (:617). Version=1, `IsActive=true` + `IsUserSelectable=true` cả 8 (:615,:622-623). +- **Idempotent PER-CODE** `AnyAsync(w.Code == code)` (:610) — comment :540-542 tự khai vì sao KHÔNG dùng khuôn per-type của 6 seeder mẫu: 8 workflow dùng chung 1 type, check type sẽ chặn 7 nhóm còn lại. +- **Đội hình 3 trạm PRO→CCM→BGĐ** sao từ QT-DN-V2-001 prod (roster :554-585): PRO C1 4-người OR-of-N + C2 tra.bui · CCM C1 4-người + C2 chuong.phan **`AllowApproverFinalize=true`** (:576-579) · BGĐ C1 truong.nguyen. Resolve theo EMAIL, thiếu user ⇒ bỏ slot + warn không throw (:593-600), cấp rỗng ⇒ nén Order (:645), workflow 0-bước vẫn tạo giữ mã (:673-676). +- **8 nhóm hiện SỐNG CHUNG IsActive=true cùng type=10** ⇒ bất biến "only-ONE-active-per-type" của POST create (`ApprovalWorkflowV2AdminFeatures.cs:339-343`) ĐÃ VỠ từ lúc seed (8 active cùng type). Hệ quả: **POST tạo version mới cho BẤT KỲ nhóm nào sẽ deactivate CẢ 8** — site chặn đích danh #1. +- Sidebar 8 menu-row `AwV2_KhkkN1..N8` seed @ `DbInitializer.cs:1988-1995`, parent `ApprovalWorkflowsV2`; MenuKeys VÀO `All` (`MenuKeys.cs:246-249`) ⇒ 32 policy `AwV2_KhkkN{n}.{R,C,U,D}` tồn tại. ## §3 Runtime duyệt KHKK — bảng 9-flag × {PE-runtime, KHKK-runtime} -(CHƯA ĐO) + +KHKK runtime = `Infrastructure/Services/ContractSigningPlanWorkflowService.cs` (W3 S161 + K3 S165). 4 action cố định `submit/approve/return/reject` (:74-75), KHÔNG có `targetPhase` param. OR-of-N `ResolveActingLevel` (:455-469), UPSERT `ContractSigningPlanLevelOpinions` (:477-513), con-trỏ ĐÔI reset-Cấp-1-khi-sang-Bước (:309-310), submit reset về Bước1/Cấp1 cả 2 lối vào (:198-202). + +| # | Flag | PE-runtime | KHKK-runtime | Evidence KHKK | +|---|---|---|---|---| +| 1 | `AllowReturnOneLevel` | TÔN TRỌNG — `PurchaseEvaluationWorkflowService.cs:421` (switch `WorkflowReturnMode`) | **BỎ QUA** — return luôn = `TraLai` toàn phần | `ContractSigningPlanWorkflowService.cs:48-50` comment (c) "4 return-mode per-level CỐ Ý chưa đọc"; `ReturnOrRejectAsync:354-400` 0 đọc flag | +| 2 | `AllowReturnOneStep` | TÔN TRỌNG — `:422` | **BỎ QUA** — nt | nt | +| 3 | `AllowReturnToAssignee` | TÔN TRỌNG — `:423` | **BỎ QUA** — nt | nt | +| 4 | `AllowReturnToDrafter` | TÔN TRỌNG — `:424` (mode Drafter phải bật cờ) | **BỎ QUA (hành vi = luôn-cho)** — return về Drafter cho MỌI NV đúng lượt, kể cả cờ tắt | `ReturnOrRejectAsync` chỉ guard đúng-lượt (:379-380), không đọc cờ | +| 5 | `AllowApproverEditDetails` (F3) | TÔN TRỌNG — `PurchaseEvaluationDetailFeatures.cs:84` + `PurchaseEvaluationSupplierFeatures.cs:45` | **BỎ QUA / CHƯA WIRE** — KHKK Lines/Dossier edit = drafter-only Nháp/TraLai | `EnsureDraftEditable` `ContractSigningPlanFeatures.cs:295-303` (mọi đường ghi W2); 0 hit `AllowApproverEditDetails` trong ContractSigningPlan* | +| 6 | `AllowApproverEditBudget` (F4) | TÔN TRỌNG — `PurchaseEvaluationFeatures.cs:411` | **BỎ QUA / PE-semantics** (KHKK không có section NS; giá dòng = `ApprovedAmount ??= ProposedAmount` chốt tại finalize `:345-349`) | 0 hit trong ContractSigningPlan* | +| 7 | `AllowApproverSkipToFinal` (F2) | TÔN TRỌNG — `PurchaseEvaluationWorkflowService.cs:810` (+ Contract `:335`) | **CHƯA WIRE + BỊ ÉP-FALSE tại POST create cho type-10** | service 0 đọc (comment `:47` "giữ nguyên không port"); ép-false `ApprovalWorkflowV2AdminFeatures.cs:387` | +| 8 | `AllowApproverFinalize` (F5) | TÔN TRỌNG — `:870` (opt-out `applyLevelFinalize`) | **TÔN TRỌNG** — port @K3 S165, opt-out param mirror PE S96, set `EndedByLevelFinalize` runtime-flag (Mig 71) | `ContractSigningPlanWorkflowService.cs:276-289`; seed bật cho TP.CCM (`DbInitializer.cs:576-579,:655`) | +| 9 | `AllowApproverDelete` (F6) | TÔN TRỌNG — `PeSoftDeleteFeatures.cs:110-114` (3 rào: policy ∧ đúng-lượt ∧ F6) | **BỎ QUA / CHƯA WIRE** — delete KHKK không có nhánh approver-đúng-lượt (xem §3-bis delete handler) | 0 hit `AllowApproverDelete` trong ContractSigningPlan* | + +- `CeoApprovalThreshold` (workflow-level, NGOÀI 9 checkbox): PE đọc; KHKK **CỐ Ý KHÔNG port** (NĐUQ human-gate vĩnh viễn, OG-3 — comment `:46-47`). +- DTO detail KHKK ĐÃ expose `AllowApproverFinalize` per-level (`ContractSigningPlanFeatures.cs:140,:1013,:1030`) — các flag khác không expose (PE expose đủ 9 @ `PurchaseEvaluationDtos.cs:124-138`). + +### §3-bis Surprises quanh pin/đổi workflow (evidence create/update path) +- **Rào (v) group-match CHỈ chạy lúc CREATE:** `ContractSigningPlanFeatures.cs:479-486` regex `^KHKK-N([1-8])$` so `approvalGroup`; workflow code NGOÀI họ (type-10 code khác) → CHO QUA (comment :477-478). +- **🔴 UpdateDraft re-pin (:603-607) chỉ gọi `EnsureWorkflowTypeAsync` (type=10, :307-318), KHÔNG re-check group-match** ⇒ phiếu nhóm 3 tạo đúng KHKK-N3 rồi PUT draft re-pin KHKK-N5 = 200 OK; SubmitAsync cũng không re-check workflow-code↔group (`ContractSigningPlanWorkflowService.cs:169-187` chỉ check Lines↔group) ⇒ **bypass rào (v) 2-bước** — phiếu nhóm 3 chạy đội hình nhóm 5. +- FK `ContractSigningPlans.ApprovalWorkflowId` → ApprovalWorkflows = **Restrict vật lý** (`ContractSigningPlanConfiguration.cs:48-53`). ## §4 Gap-analysis BE → wave đề xuất (CHƯA ĐO) diff --git a/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-db-fable.md b/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-db-fable.md index a5813b1..d234513 100644 --- a/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-db-fable.md +++ b/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-db-fable.md @@ -4,21 +4,89 @@ > Câu hỏi trung tâm: **schema hiện tại ĐỦ chưa hay cần mig mới?** Kỳ vọng "máy rộng UI hẹp" ⇒ 0 mig — PHẢI CHỨNG. > Trạng thái file: đổ ruột liên tục (chống-#53). Khu chưa đo ghi `CHƯA ĐO`. -## §1 Schema V2 — cột thật 3 bảng (từ EF Configuration + ModelSnapshot) +## §1 Schema V2 — cột thật 3 bảng (từ ModelSnapshot + entity) -CHƯA ĐO +Nguồn: `ApplicationDbContextModelSnapshot.cs:128-313` + `src/Backend/SolutionErp.Domain/ApprovalWorkflowsV2/ApprovalWorkflow.cs` (1 file chứa cả 3 entity). -### §1b Bảng ánh xạ 9 checkbox ↔ cột +### `ApprovalWorkflows` (snapshot :128-187) +| Cột | Kiểu | Ghi chú | +|---|---|---| +| Id | uniqueidentifier PK | BaseEntity | +| Code | nvarchar(100) NOT NULL | "QT-DN-V2-001" | +| Version | int | monotonic per Code | +| ApplicableType | int | enum 1..10 — **ContractSigningPlan=10** (`ApprovalWorkflow.cs:70`) | +| Name | nvarchar(200) NOT NULL | | +| Description | nvarchar(1000) NULL | | +| IsActive | bit | | +| ActivatedAt | datetime2 NULL | | +| IsUserSelectable | bit | Mig 25 — Ghim/Bỏ ghim | +| CeoApprovalThreshold | decimal(18,2) NULL | Mig 53 — ngưỡng gói CEO (PE semantic) | +| CreatedAt/CreatedBy/UpdatedAt/UpdatedBy | | BaseEntity audit | -CHƯA ĐO +Index: `(ApplicableType, IsActive)` + **UNIQUE `(Code, Version)`** (:181-184). 🔴 **KHÔNG có IsDeleted** — `ApprovalWorkflow : BaseEntity` (entity :21), KHÔNG AuditableEntity ⇒ 0 soft-delete, 0 query-filter trên cả 3 bảng V2. -## §2 Versioning — Code/Version/Archived/unique/sửa-tại-chỗ/tạo-version/xoá +### `ApprovalWorkflowSteps` (snapshot :274-313) +Id · ApprovalWorkflowId (FK) · Order int · Name nvarchar(200) NOT NULL · DepartmentId uniqueidentifier NULL (hint) · audit 4 cột. Index: `DepartmentId` + `(ApprovalWorkflowId, Order)`. -CHƯA ĐO +### `ApprovalWorkflowLevels` (snapshot :189-272) +Id · ApprovalWorkflowStepId (FK) · Order int (Cấp, OR-of-N rows cùng Order) · Name nvarchar(200) NULL · ApproverUserId NOT NULL (1 row = 1 NV slot) · audit 4 cột · **9 cờ bit Allow\***. Index: `ApproverUserId` + `(ApprovalWorkflowStepId, Order)`. -## §3 KHKK wiring — Mig 71 catalog + ContractSigningPlans pin/pointer/finalize-flag +### §1b Bảng ánh xạ 9 checkbox ảnh ↔ cột — **9/9 CÓ ĐỦ, 0 cột thiếu** -CHƯA ĐO +Per-NV = per-row `ApprovalWorkflowLevels` (mỗi row = 1 ApproverUserId ⇒ cờ trên row chính là cờ per-NV). + +| # | Checkbox (ảnh YC-013) | Cột thật | Default (snapshot) | Mig gốc | +|---|---|---|---|---| +| 1 | Trả về 1 Cấp trước | `AllowReturnOneLevel` | false (:220-223) | 29 | +| 2 | Trả về 1 Bước trước | `AllowReturnOneStep` | false (:225-228) | 29 | +| 3 | Trả về Người chỉ định | `AllowReturnToAssignee` | false (:230-233) | 29 | +| 4 | Trả về Drafter (mặc định) | `AllowReturnToDrafter` | **true** (:235-238) | 29 | +| 5 | Sửa Section 2 | `AllowApproverEditDetails` | false (:205-208) | 29 | +| 6 | Sửa Section ngân sách | `AllowApproverEditBudget` | false (:200-203) | 30 | +| 7 | Duyệt thẳng Cấp cuối | `AllowApproverSkipToFinal` | false (:215-218) | 31 | +| 8 | Duyệt là KẾT THÚC | `AllowApproverFinalize` | false (:210-213) | 58 | +| 9 | XÓA phiếu | `AllowApproverDelete` | false (:195-198) | 68 | + +Cột nằm trên bảng V2 DÙNG CHUNG mọi ApplicableType ⇒ workflow KHKK (ApplicableType=10) **lưu được cả 9 cờ ngay hôm nay, 0 mig**. ⚠️ Ranh giới lens: cột TỒN TẠI ≠ service KHKK ĐỌC cờ (semantic #3 đọc `PeLevelOpinions`, #5/#6 đọc section PE — entity comment `ApprovalWorkflow.cs:119-173`; KHKK service có honor không = lens BE). + +## §2 Versioning — đọc từ `ApprovalWorkflowV2AdminFeatures.cs` (KHÔNG đoán từ UI) + +- **Lưu version:** cùng `Code` nhiều row, `Version int` monotonic; **UNIQUE (Code, Version)** (snapshot :183-184). "Archived" KHÔNG có cột riêng — chỉ là `IsActive=false` (nhãn UI suy từ bit). +- **Tạo phiên bản mới** = `CreateAwDefinitionCommandHandler` (:326-): INSERT row mới `Version = max(Code)+1` (:334-337), `IsActive=true, IsUserSelectable=true` (:352-353). 🔴 **LANDMINE cho KHKK:** deactivate cũ chạy theo **`ApplicableType`** chứ KHÔNG theo Code — `Where(w.ApplicableType == typeEnum && w.IsActive)` set `IsActive=false` TẤT CẢ (:339-343). Với DuyetNcc (1 code/type) đúng; với KHKK **8 code chung type=10** ⇒ tạo version mới cho `KHKK-N1` sẽ **tắt IsActive của cả KHKK-N2..N8**. Chính seeder đã né bẫy same-class ở check idempotent (`DbInitializer.cs:540-542` "check theo type sẽ chặn 7 nhóm còn lại") nhưng Create handler CHƯA vá — vá-tệp-A-quên-tệp-B. **Fix = code BE (đổi scope deactivate theo Code), 0 mig.** +- **Sửa tại chỗ** = `UpdateAwDefinitionCommandHandler` (:515-900): UPDATE in-place ĐÚNG row + **diff Levels theo `Level.Id` — GIỮ NGUYÊN Id** (:561-634, "F-3 CẤM delete-then-insert") ⇒ không đẻ Id mới, không mồ-côi con trỏ (khác nỗi lo F17/S155 — máy Update nay ĐÃ TỒN TẠI, memory cũ "WF-V2 0 lệnh Update" STALE). Cấm in-place: đổi Code/ApplicableType/CeoThreshold (:539-550), thêm/bớt/đảo Bước (:553-574), đổi Order dòng (:593), toggle 2 cờ đổi-đường-đi F5/F2 (:598-603), bỏ người giữ F5/F2 (:627-632), làm rỗng Cấp (:639-647) — tất cả 409 + gợi ý tạo version mới. 7 cờ còn lại + đổi người + thêm/bỏ người trong Cấp = hợp lệ in-place (:789-797). +- **Guard chữ ký khi bỏ/đổi người:** 2 tầng quét **CẢ 8 bảng LevelOpinions** gồm `ContractSigningPlanLevelOpinions` — count-sống (:961-965) + retained (:1033-1038); transaction rào (:744-745, :895-899). ⇒ máy sửa-tại-chỗ **ĐÃ KHKK-aware ở tầng guard**. +- ⚠️ Vết changelog khi sửa-tại-chỗ chỉ ghi cho **PE** (`ApplicableType ∈ {DuyetNcc, DuyetNccPhuongAn}` :863-892); KHKK khai-giới-hạn defer F-6b (:857-862) — bảng `ContractSigningPlanChangelogs` ĐÃ TỒN TẠI (Mig 69) ⇒ wire = code BE thuần, **0 mig**. +- **Ghim/Bỏ ghim** = `SetAwUserSelectableCommandHandler` (:1173-1185): flip 1 bit, generic mọi type — KHKK dùng ngay. +- **Xoá version** = `DeleteAwDefinitionCommandHandler` (:1193-1205): **HARD delete** (`Remove` + BaseEntity không soft-delete), **0 guard usage** — comment tự khai ":1187-1189 Sau UAT … cần check usage trước khi delete". Phiếu pin (PE/HĐ/KHKK Restrict) hoặc LevelOpinion tồn tại ⇒ SqlException 547 → 500. Version không ai đụng ⇒ xoá sạch (cascade Steps→Levels). + +## §3 KHKK wiring — Mig 70/71 + ContractSigningPlans pin/pointer/finalize-flag + +### FK map V2 (snapshot :6210-6323) — quyết định risk xoá +| Quan hệ | Behavior | +|---|---| +| Steps → ApprovalWorkflows | **Cascade** (:6229-6233) | +| Levels → Steps | **Cascade** (:6212-6216) | +| Levels.ApproverUserId → Users | Restrict (:6218-6222) | +| Steps.DepartmentId → Departments | Restrict (:6235-6238) | +| `ContractSigningPlans.ApprovalWorkflowId` → ApprovalWorkflows | **Restrict** (:6245-6248) | +| `Contracts.ApprovalWorkflowId` → | Restrict (:6327-6330) | +| `PurchaseEvaluations.ApprovalWorkflowId` → | Restrict (:6755-6758) | +| `ContractSigningPlanLevelOpinions.ApprovalWorkflowLevelId` → Levels | **Restrict** (:6297-6301) | +| `ContractSigningPlanApprovals.ApprovalWorkflowLevelId` | ⚠️ **LOOSE Guid — 0 FK** (cột + index :416/:455 nhưng relationship section :6251-6260 chỉ có 1 HasOne về Plan Cascade) | + +⇒ Hard-delete 1 workflow version bị DB CHẶN (SqlException 547) nếu: (a) bất kỳ phiếu PE/HĐ/KHKK pin nó, HOẶC (b) bất kỳ LevelOpinion (PE/HĐ/KHKK) trỏ Level của nó — cascade Steps→Levels đụng Restrict của LevelOpinions. + +### Mig 71 `AddKhkkApprovalGroupCatalogAndFinalizeRuntime` (`20260731085624_*.cs`) +- **KHÔNG có bảng catalog-nhóm-duyệt riêng** — "catalog" trong tên mig = gắn `ContractSigningPlanLines.CatalogEntryId` (loose-Guid, KHÔNG FK, :70-74) + `TenHangMuc` denorm (:76-81) trỏ về `ContractCatalogEntries` (Mig 70, bảng danh mục 86 dòng). +- `ContractSigningPlans.ApprovalGroup int NOT NULL default 0` + backfill vô-điều-kiện `SET = 1` (:47-61) — nhóm duyệt 1..8 lưu TRÊN PHIẾU dạng int trần (0 bảng, 0 FK, 0 CHECK constraint 1..8). +- `EndedByLevelFinalize bit` runtime-flag (:63-68) — ghost-window khai trước, K3 tiêu thụ. +- Index-swap: UNIQUE `(PlanId, SupplierId)` → UNIQUE `(PlanId, SupplierId, CatalogEntryId)` filtered `[IsDeleted]=0` (:43-45, :93-98). Down có bẫy F-8 (:132-141). + +### `ContractSigningPlans` pin + pointer (snapshot :315-408) — mirror ĐỦ khuôn PE +`ApprovalWorkflowId Guid?` (pin, Restrict) · `CurrentWorkflowStepIndex int?` (:336) · `CurrentApprovalLevelOrder int?` (:333) · `EndedByLevelFinalize bit` (:351) · `ApprovalGroup int` (:321) · `Phase int` · AuditableEntity (IsDeleted/DeletedAt/DeletedBy — phiếu SOFT-delete, ≠ workflow) · UNIQUE filtered `MaKeHoach` (:395-397) · index `(ApprovalGroup, Phase)` (:403). + +### 8 workflow nhóm = 8 row `ApprovalWorkflows` phân loại thế nào? +CHƯA ĐO — cần: seed DbInitializer + cơ chế resolve nhóm→workflow (Code? riêng gì?) ## §4 Dữ liệu thật LocalDB Dev (sqlcmd -E) diff --git a/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-fe-fable.md b/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-fe-fable.md index 76568b9..e15eff4 100644 --- a/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-fe-fable.md +++ b/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-fe-fable.md @@ -3,7 +3,31 @@ > Fable deep-pass đơn lẻ, /fable-real investigator-codebase. Đề: 8 mục sidebar "Duyệt KHKK — Nhóm 1..8" phải ra ĐÚNG trang `system/approval-workflows-v2/DuyetNcc` khuôn ảnh anh gửi. 0 sửa code. ## §1 Trang DuyetNcc render từ đâu (route + page + KIND_CONFIG + dialog) -CHƯA ĐO + +**Route:** `fe-admin/src/App.tsx:79-80` — `/system/approval-workflows-v2` + `/system/approval-workflows-v2/:typeCode` → cùng 1 component `ApprovalWorkflowsV2Page` (named export, `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` 1355 dòng). + +**KIND map (KHÔNG phải KIND_CONFIG object đầy đủ kiểu S29/S35 — chỉ 2 Record thưa):** +- `TYPE_CODE_TO_INT` `:164-172` = `{ DuyetNcc:1, DuyetNccPhuongAn:2, Contract:3, ContractSigningPlan:10 }` — **0 entry `KhkkN*`**. +- `DEFAULT_CODE_BY_TYPE` `:173-180` = `{1:'QT-DN-V2-001', 2:'QT-DN-PA-V2-001', 3:'QT-HD-V2-001', 10:'QT-KHKK-V2-001'}` (chỉ gợi-ý ô Mã). +- Nhãn hiển thị KHÔNG có bảng FE — lấy từ BE `applicableTypeLabel` (`AwLabels.Type`, `ApprovalWorkflowV2AdminFeatures.cs:97` có entry type-10 "Kế hoạch ký kết HĐ"). + +**Cơ chế render `:286-353`:** `selectedTypeInt = TYPE_CODE_TO_INT[typeCode]`; unknown typeCode ⇒ `undefined` ⇒ `currentType=null` ⇒ render **LANDING GRID** mọi type (`:323-343`) thay vì panel. Panel thật = `TypePanel` `:357-459` (active card + "Lịch sử versions" + nút "Tạo quy trình mới") — mỗi panel khoá vào ĐÚNG 1 `applicableType`, active = `type.active` (BE tính), history render **filter `!d.isActive`** (`:419,:426`). + +**Cột nút phải (khuôn ảnh):** `DefinitionCard:577-612` — Sửa tại chỗ `:587` / Tạo phiên bản mới `:596` / Ghim-Bỏ ghim `:606` / Xoá version `:610`. Badge: Đang áp dụng `:490` · Cho user chọn `:502` · Ngưỡng CEO `:506-513`. Badge NS PRO/CCM per NV `:560-565` (display-only suy từ role). + +**Dialog Sửa tại chỗ / Tạo mới:** cùng component `Designer:620-1354`, `isEdit = editTarget !== null` `:638`; title "Sửa tại chỗ — {code} v{NN} (giữ nguyên phiên bản)" `:868`. Khối cam "QUYỀN DUYỆT {TÊN}" `:1228-1329` = **9 checkbox** map field TS (`EditLevelEntry:100-123` / API `LevelDto:37-62`): +| # | Label UI | Field | +|---|---|---| +|1| Trả về 1 Cấp trước | `allowReturnOneLevel` | +|2| Trả về 1 Bước trước | `allowReturnOneStep` | +|3| Trả về Người chỉ định | `allowReturnToAssignee` | +|4| Trả về Drafter (mặc định) — tick sẵn | `allowReturnToDrafter` (default true `:231`) | +|5| Chỉnh sửa **Section 2 (Hạng mục/NCC/Báo giá)** | `allowApproverEditDetails` `:1275` | +|6| Chỉnh sửa Section ngân sách | `allowApproverEditBudget` `:1284` | +|7| Duyệt thẳng Cấp cuối | `allowApproverSkipToFinal` — 🔴 **DISABLED khi type=10** `:1292-1302` (K3: service KHKK chưa port SkipToFinal, BE ép-false) | +|8| Duyệt là KẾT THÚC quy trình (xanh emerald) | `allowApproverFinalize` `:1307-1315` | +|9| Cho phép NV này XÓA phiếu (đỏ rose) | `allowApproverDelete` `:1320-1328` | +9 cờ gửi qua 1 hàm chung `levelFlagsPayload:255-267` (2 call-site POST `:820` + PUT `:788` — chống silent-drop F6/F-7 S155). Rule-box xanh cuối dialog `:1343-1350`. Ô "Ngưỡng CEO" `:944-961` render cho MỌI type (kể cả 10). ## §2 8 mục KHKK sidebar hiện map đi đâu (4-chỗ pattern 16-bis) CHƯA ĐO