From fd68407a2f2fcd3fa05b0b3c42fdae5bbcc74ff4 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 29 Jul 2026 20:46:44 +0700 Subject: [PATCH] wal: flush 20260729T2046 --- .claude/WAL.md | 3 +- .../implementer-backend/MEMORY.md | 2 + .../implementer-frontend/MEMORY.md | 2 + .../runs/2026-07-29-S161-khkk-w2-crud/run.md | 6 +- .../sub-implementer-backend-0.md | 174 ++++++++++++++++++ .../sub-implementer-frontend-1.md | 54 ++++++ 6 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 .claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/sub-implementer-backend-0.md create mode 100644 .claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/sub-implementer-frontend-1.md diff --git a/.claude/WAL.md b/.claude/WAL.md index 121974f..d73bdd5 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -16,7 +16,8 @@ chain: - [x] commit `a8027c0` (30 file +8.987/−22, F-0 gate ĐẠT) + PUSH origin/main - [x] **W1 ĐÓNG — cicd VERDICT=PASS 6/6** (Run #427 5m40s · 566 · Mig 69 prod 96 bảng · menu 142 GIỮ · marker 3-tầng · đo-CỜ 13/13/13/2 + control-âm Khkk_G1 + log `0 added + 91 upgraded` khớp dự-đoán · label type-10 VN sống). Synthesis `runs/2026-07-29-S161-khkk-w1-schema/w1-synthesis.md`. Tồn: setup WF tay trước W5 · UAT non-Admin (nv.test KHÔNG tồn tại prod) · 51-bis GẤP (cicd _INDEX 99,2%/167B) · F-4 - [x] gate 5-anchor W2 PASS 5/5 (A4 line-drift content-found; A5 KeHoachKyKet∈All:180 THẬT; phân xử lệch: mã sinh @CREATE per spec, entity-comment W1 sai → worker sửa) -- [!] wf: hmw W2 đợt-1 `wf_6f0aad26-229` ĐANG CHẠY — 2 lane {implementer-backend: mega-file ContractSigningPlanFeatures CHỈ-CRUD (cấm copy phần duyệt, bẫy flatten :427-439) + Controller authz-2-tầng ≥4 policy · implementer-frontend: pages/khkk 3-page ×2 app + 4-place re-point 6 leaf khỏi coming-soon + SHA-pair} — run-folder `runs/2026-07-29-S161-khkk-w2-crud/` (taskList 6 stage) +- [x] wf W2 đợt-1 `wf_6f0aad26-229`: 2/2 lane chết #53 NHƯNG sub-md cứu trọn (BE 16.5KB — 15 endpoint policy 15/15 + 4 quyết D1-D4 lead duyệt cả 4 · FE 4 file SHA-pair) → lead solo đuôi FE 4-place (WfView 2-app-2-đường + whitelist matrix +10) → **build BE 0W/0E + FE ×2 PASS + coming-soon 0/0**; harvest 2 diary xong +- [!] wf: W2 đợt-2 `wf_7490179a-70b` test-specialist ĐANG CHẠY (2 test PIN + 2-4 test acceptance, sàn ≥568) → về thì: lead suite + reviewer → commit/push → cicd - [ ] đợt-2 SAU khi wf về: test-specialist + database-agent (song song) → lead acceptance §③-B (build·test·sqlcmd 96·Down sạch·đo CỜ) → reviewer → commit/push → cicd-monitor 3-chân-kiềng - [ ] setup thủ công TRƯỚC W5 (SAU W1 vì type=10 cần mig 69): dựng workflow type=3 + type=10 3-trạm qua Designer (seed chỉ 1 trạm) - [ ] housekeeping @pause/closeout kế: nén MIND-1 5.447B (trần `_mind` 83,2% — MIND-5 chắc vượt) · re-carry 2 ý D rơi (vá-GỐC-Phase-3.5-refresh {mới-nêu} CÒN SỐNG + khuôn-5-đợt→sổ bền {gần-chốt}) · STATUS:6 bổ arc dry-run S160 (mù có lý do — land 15:27 trước pipeline) · hạ nhãn DR/WF-3-trạm đã-định-đoạt · E đếm 7 sub-file (không 6) · mở lại slot-45 trần `_mind` (tiền-đề lật: file SỐNG 83,2%) · quyết bookkeeping OVERDUE:light (cặp H24 chạy @S159 bookend mà `last_audit` đứng 27) diff --git a/.claude/agent-memory/implementer-backend/MEMORY.md b/.claude/agent-memory/implementer-backend/MEMORY.md index 61a0d0a..a280c9e 100644 --- a/.claude/agent-memory/implementer-backend/MEMORY.md +++ b/.claude/agent-memory/implementer-backend/MEMORY.md @@ -1,5 +1,7 @@ # Implementer-Backend Agent — Persistent Memory +- **S161-W2 (07-29) KHKK CRUD mega-file + Controller `[hmw wf_6f0aad26-229 · chết #53 SAU §7 (trước §8 build-log) NHƯNG sub-md 16.5KB ghi-từng-mục TRỌN + build lead 0W/0E 2s incremental = lane đã build xanh; entry on-behalf]`:** `ContractSigningPlanFeatures.cs` (Create 4-rào-fail-sớm + auto-Lines snapshot + UpdateDraft null-safe 3/3 #73 + Get/List/inbox/deleted + Dossier + Attachments-mọi-phase + Delete allow-list M6 + Picker) + `ContractSigningPlansController` 15 action **policy 15/15 kể cả Read tường minh** (#82). 4 QUYẾT khai-để-lead-phân-xử (lead duyệt cả 4): D1 `PeId` giữ nguyên spec (hợp-đồng 2-lane, đổi = gãy body JSON) · D2 `DrafterUserId=currentUser` KHÔNG copy pe.Drafter (Guid?→non-null = Guid.Empty hỏng quyền) · D3 IDOR mình-soạn ∪ có-chân-duyệt (khuôn Proposal 0-rào là khuôn HỞ — bài 8b S155; nới-phòng = 1 vế 1 chỗ) · D4 ProposedAmount mồi=PeReferenceAmount (W2 0 command sửa line). Bẫy né được ghi lại: **(1) codegen gọi TRƯỚC `Add(plan)`** — generator SaveChanges nội bộ trong tx riêng, Add trước = flush phiếu dở (S88 self-committing); **(2) SUM quote so `w.Id` (row PeSupplier) KHÔNG `w.SupplierId`** (nhầm = cộng tiền NCC khác); **(3) `Remove(plan)` TRƯỚC `Add(changelog)`** — CascadeDeleteTiming.Immediate detach entity Added (bug T26 S155); **(4) subquery `Any` lồng 2 tầng trong Where vỡ dịch SQL CHỈ LÚC CHẠY** → dùng hình dạng join :185-189; **(5) tự mở lại 5 anchor + grep-cùng-lớp ra 3 site comment sai khi lead giao 1** (S122). Tag `[s161-w2, khkk-crud, 4-quyet-khai-lead-phan-xu, codegen-truoc-add, wid-vs-supplierid, remove-truoc-changelog]` + - **S161 (07-29) W1 KHKK schema Mig 69 `[hmw wf_7d6fb81b-640 + wf_f8c806d2-5a3 · CẢ 2 lượt chết #53 (×2 cùng task) NHƯNG lượt-2 sub-md 6.2KB ghi-TỪNG-MỤC-trong-lúc-làm cứu TRỌN narrative + code đĩa sống 100%; lead verify build 0W/0E + solo đuôi; entry on-behalf]`:** Module `Domain/ContractSigningPlans/` 7 entity + 3 enum (Phase 1/2/3/98/99 · DossierItemKind/Status trong DossierItem.cs, start=1) + `ApplicableType.ContractSigningPlan=10` append-only + 7 Configuration + DbSets ×2 file + **Mig 69 `20260729122015_AddContractSigningPlans`** (7 CreateTable · 0 ALTER · Down 7 DropTable · UNIQUE filtered `(PlanId,SupplierId) WHERE IsDeleted=0` #57 · MaKeHoach unique filter NOT-NULL · FK 6-con Cascade 1-hop + Plan→AWF Restrict — **0 multiple-cascade-path by design**: Attachment.DossierItemId chỉ HasIndex KHÔNG FK) · apply 2 DB **0 drift** (khác S133) · sys.tables 89→96 · MenuKeys.All+=KeHoachKyKet:179. Lead solo đuôi: seeder `SeedKeHoachKyKetAccessAsync` (khuôn S118 + MỞ RỘNG vế CanDelete Drafter/Admin, UPGRADE-if-exists F-S1, call SAU revoke) + `ContractSigningPlanCodeGenerator` (KHKK/{YYYY}/{Seq:D3}, SERIALIZABLE, `WorkflowAppCodeSequences` dùng chung ns Domain.Office) + DI + 3 site AWV2Admin (count/retained/orphan — danh sách kết ở VehicleBooking, ItTicket KHÔNG có LevelOpinions). Learned: (1) **ghi-sub-md-TỪNG-MỤC = thứ duy nhất sống khi return chết** — lượt-1 dồn cuối mất trắng narrative, lượt-2 ghi từng mục giữ đủ grep-proof; (2) audit-first §0 bảng đo trước khi làm tiếp = relaunch-cắt-gọt chuẩn; (3) Changelog/Approval = BaseEntity KHÔNG HasQueryFilter (parity PE) — schema-test phải assert theo THỰC TẾ entity không đoán. Tag `[s161, w1-khkk-mig69, sub-md-tung-muc-cuu-narrative, 53-x2-cung-task, cascade-1-hop, upgrade-if-exists-candelete]` - **S153 (07-26) arc ctx-adopt T1 + T3 (2 lượt hmw đợt-1) `[lead seed on-behalf @closeout M9; T1 return mất schema-fail → derive-from-body; T3 return CLEAN]`:** **T1 máy `mind-check`** — EXTEND `scripts/session_ctx.py` (+528 dòng, ADD-only: `_TS_KEY_RE` module-level canonical + subcommand + parser + dispatch; 2 subcommand cũ BẤT ĐỘNG). 7 phép + 2 phụ · 4 mức {đạt·trượt·cờ·bỏ-qua-có-khai} · draft/closed/`--root`/`--session` PIN · exit {0,1,3}. **9 quyết-định thiết-kế có lý-do đo được**, đắt nhất: **D1 config đọc LAZY** — chỉ đọc `memory-budget.json` khi CÓ tệp để đo, vì 2 dòng acceptance chỉ đồng-thời đúng theo thứ tự đó ("không có gì để đo thì không đọc trần; có thứ để đo thì TỪ CHỐI đoán trần") · D3 marker VẮNG ⇒ quét TRỌN + CỜ (hướng hỏng NGHIÊM: tệp thiếu marker = tệp sai khuôn, phải lộ) · D4 rào-3 in pattern-name + `file:line` **KHÔNG in snippet** (gate chống rò không được tự rò ra log) · D6 `_ascii_safe()` backslashreplace (console cp1252 gặp diacritic ⇒ `UnicodeEncodeError` = máy chết GIỮA gate). diff --git a/.claude/agent-memory/implementer-frontend/MEMORY.md b/.claude/agent-memory/implementer-frontend/MEMORY.md index 6e1e995..ef578d8 100644 --- a/.claude/agent-memory/implementer-frontend/MEMORY.md +++ b/.claude/agent-memory/implementer-frontend/MEMORY.md @@ -1,5 +1,7 @@ # Implementer-Frontend Agent — Persistent Memory +- **S161-W2 (07-29) KHKK 3 page ×2 app `[hmw wf_6f0aad26-229 · chết #53 sau MUC-1+cp (4-place dở) → lead solo đuôi {Layout ×2 re-point + fe-admin App routes + matrix whitelist +10} · build ×2 PASS + SHA-pair 4/4 + coming-soon 0/0]`:** `types/khkk.ts` + 3 page `pages/khkk/` viết fe-user trước rồi cp (SHA-pair 4/4). Types theo ENTITY (BE lane song song chưa land Application lúc recon — luật đề bài "ưu tiên shape entity" ăn); header types khai HỢP-ĐỒNG FE↔BE 12 endpoint để BE đối chiếu 1 chỗ. Chip lọc List đổi STATE không ghi URL (bug UAT S155 menu-sáng-nhầm). Create: dropdown workflow đọc `{types:[...]}` KHÔNG phải flat array (khuôn ProposalCreatePage:59-69). Detail: Lines READ-ONLY (W2 không endpoint sửa line — KHÔNG bịa UI). **WfView 2 app 2 ĐƯỜNG (lead chốt @đuôi):** fe-user → alias route `/khkk/workflow-matrix?type=10` tái dùng `WorkflowMatrixViewPage` (whitelist :56 nay `1|2|3|10`) · fe-admin → Designer deep-link `/system/approval-workflows-v2/ContractSigningPlan` (admin CỐ Ý không có matrix — chốt 07-27; "Luồng duyệt" admin = dựng quy trình). menuKeys.ts place-3 SKIP-có-khai: pages 0 usage MenuKeys/usePermission (khớp khuôn Proposal) — entry-không-consumer = drift. Root `KeHoachKyKet` staticMap fe-user /dashboard→`/khkk/list`. Tag `[s161-w2, khkk-3-page, types-theo-entity, wfview-2-app-2-duong, alias-route-khong-de-page, place3-skip-khong-consumer]` + - **S161 (07-29) W1 KHKK Designer type-10 fe-admin `[hmw wf_7d6fb81b-640 lane-2 · return CLEAN · lead verify: build tươi PASS + disk khớp từng site; diagnostics IDE giữa-agent = stale (#68) đĩa thắng]`:** 1 file `ApprovalWorkflowsV2Page.tsx` (+67/−9): `TYPE_INT_CONTRACT_SIGNING_PLAN=10` (:147) → `TYPE_CODE_TO_INT:162` (deep-link `/ContractSigningPlan`, route sẵn `App.tsx:76`) + `DEFAULT_CODE_BY_TYPE:170` `'QT-KHKK-V2-001'` (suy diễn khuôn — spec KHÔNG chốt, đổi 1 dòng) + rào 2 checkbox early-finalize **2 LỚP**: render (`disabled` + `checked` ép false + tooltip dùng chung `KHKK_EARLY_FINALIZE_NOTE` :1284-1330, className ternary chuỗi ĐẦY ĐỦ Pattern-14) **VÀ** payload (`levelFlagsPayload(e, lockEarlyFinalize)` :247 ép false :254-255 — 2/2 call-site PUT :783 + POST :815) — chỉ disable ô = containment GIẢ vì `copyFromDefinition()` vẫn tuồn `true` vào state→BE. Cân 409: PUT echo-so-BẰNG (`AWV2AdminFeatures:580-585`) KHÔNG nổ vì type-10 mọi bản đều POST-ép-false + cloneFrom cùng-type. **2 tiền-đề đề bài SAI, lane tự bẻ (mirror bài S160):** (i) FE KHÔNG có bảng nhãn ApplicableType — nhãn từ BE `applicableTypeLabel`; (ii) khuôn "entry type=9" không tồn tại (`TYPE_CODE_TO_INT` dừng ở 3) → mirror `Contract:3`. **Cờ đỏ báo lead:** type-10 (và cả type-3 xưa nay) KHÔNG có đường sidebar — allowlist `Layout.tsx:162-168` chỉ có 2 key `AwV2_` PE; admin vào bằng URL trực tiếp; BE seed leaf mới thì PHẢI nới `Layout.tsx:165` cùng lượt. Tag `[s161, w1-designer-type10, two-layer-containment-render-payload, fe-khong-label-map, tien-de-de-bai-sai-tu-be, stale-diag-68]` - **S153 (07-26) arc ctx-adopt T2 + T5 (2 lượt hmw) `[lead seed on-behalf @closeout M9; T2 return mất schema-fail (#53 +1) → derive-from-body, template VẪN LAND 8.819B; T5 return CLEAN]`:** **T2 template** `.claude/templates/session-mind-template.md` — 3 marker (RULES-START/END · MIND-TOP) · **0 dòng key `ts:`** (ts sống trong HEADING sau em-dash ⇒ không phạm rào-2) · 6 hit `rào-` (3 rào × tên+lý-do) · chiều chèn **NGƯỢC** `_context` khai TO trong khối luật · placeholder `` trong schema-comment chống parser false-match (mirror trick template `_context:32`) · kiểm `_SECRET_RES` để ví-dụ rào-3 không tự khớp mẫu (secrets-sweep rglob TRỌN session-dir, KHÔNG biết enclosure). diff --git a/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/run.md b/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/run.md index 7c8b00f..a77cab2 100644 --- a/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/run.md +++ b/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/run.md @@ -8,9 +8,9 @@ | # | Stage | Vai / cách | Trạng thái | |---|---|---|---| -| 1 | Đợt-1a BE: mega-file `ContractSigningPlanFeatures.cs` (Create+auto-Lines+MaKeHoach@CREATE · UpdateDraft · Get/List/pendingMe+inbox-precompute · Dossier UPSERT/DELETE · Attachments · DELETE allow-list · picker PE-chờ-lập-KH) + `ContractSigningPlansController` authz 2-tầng per-action | hmw `implementer-backend` | [ ] | -| 2 | Đợt-1b FE: `pages/khkk/` 3 page + types ×2 app + 4-place mirror (routes + staticMap re-point 6 leaf khỏi coming-soon + menuKeys) + build ×2 + SHA-pair | hmw `implementer-frontend` | [ ] | -| 3 | Đợt-2: test-specialist (≥2 test PIN: `ContractSigningPlansController_EveryWriteEndpoint_HasAuthorizePolicy` + `CreateContractSigningPlan_FromNonApprovedPe_Throws`) | hmw `test-specialist` | [ ] | +| 1 | Đợt-1a BE: mega-file `ContractSigningPlanFeatures.cs` (Create+auto-Lines+MaKeHoach@CREATE · UpdateDraft · Get/List/pendingMe+inbox-precompute · Dossier UPSERT/DELETE · Attachments · DELETE allow-list · picker PE-chờ-lập-KH) + `ContractSigningPlansController` authz 2-tầng per-action | hmw `implementer-backend` | [x] `wf_6f0aad26-229` lane-0 chết #53 sau §7 NHƯNG sub-md **16.5KB ghi-từng-mục TRỌN** (tự mở 5 anchor + grep-cùng-lớp 3 site comment + 15 endpoint policy 15/15 + **4 quyết D1-D4 khai-lead-phân-xử → lead DUYỆT cả 4**) · build lead 0W/0E | +| 2 | Đợt-1b FE: `pages/khkk/` 3 page + types ×2 app + 4-place mirror (routes + staticMap re-point 6 leaf khỏi coming-soon + menuKeys) + build ×2 + SHA-pair | hmw `implementer-frontend` | [x] lane-1 chết #53 sau MUC-1+cp (SHA-pair 4/4) → **lead solo đuôi**: Layout ×2 re-point 6 leaf (WfView 2 app 2 đường: user=alias matrix?type=10 whitelist+10 · admin=Designer deep-link) + fe-admin App routes + root KeHoachKyKet→/khkk/list; menuKeys place-3 SKIP-có-khai (0 consumer). Build ×2 PASS + coming-soon **0/0** | +| 3 | Đợt-2: test-specialist (≥2 test PIN: `ContractSigningPlansController_EveryWriteEndpoint_HasAuthorizePolicy` + `CreateContractSigningPlan_FromNonApprovedPe_Throws`) | hmw `test-specialist` | [!] `wf_7490179a-70b` ĐANG CHẠY nền | | 4 | Lead: build ×2 + suite + acceptance §③-B sweep | lead | [ ] | | 5 | Reviewer adversarial (CRUD endpoints POST/PUT/DELETE — bắt buộc) | `reviewer` | [ ] | | 6 | Commit + push + cicd (3-chân-kiềng + 2-bearer NẾU có tài khoản non-Admin — nv.test KHÔNG tồn tại prod, xem w1-synthesis tồn-2) | lead + `cicd-monitor` | [ ] | diff --git a/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/sub-implementer-backend-0.md b/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/sub-implementer-backend-0.md new file mode 100644 index 0000000..ab9cc6d --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/sub-implementer-backend-0.md @@ -0,0 +1,174 @@ +# sub-implementer-backend-0 — W2 KHKK BE (CRUD nháp + căn cứ b.8-9) + +> Run: `2026-07-29-S161-khkk-w2-crud` · vai: implementer-backend · lượt 0 +> Luật: ghi TỪNG MỤC ngay sau khi làm xong mục đó (W1: 5/7 lượt chết #53 — file này là thứ duy nhất sống). + +--- + +## §0 — AUDIT TRƯỚC KHI VIẾT (đọc đĩa, không đoán) + +Mọi số dưới đây đo từ đĩa lúc bắt đầu lượt (HEAD `77c723b`, W1 đã land ở `a8027c0`). + +### 0.1 — 5 anchor spec §③-A: TỰ MỞ LẠI (không tin gate của lead) + +| # | Anchor | Đo được | Ghi chú | +|---|---|---|---| +| A1 | `Application/Office/ProposalFeatures.cs:427-439` | ✔ SỐNG — `allLevels = wf.Steps.OrderBy(s=>s.Order).SelectMany(s=>s.Levels...)` rồi `ElementAtOrDefault(CurrentApprovalLevelOrder-1)` | 🔴 BẪY: flatten toàn workflow ⇒ mọi Level thành 1 chuỗi tuần tự = OR-of-N biến thành AND. ĐỌC ĐỂ NÉ. W2 KHÔNG copy (phần duyệt = W3) | +| A2 | `PurchaseEvaluations/CreateContractFromEvaluationFeatures.cs:53-62` | ✔ guard `pe.Phase != DaDuyet → ConflictException` (`:53-54`) + `winners = pe.Suppliers.Where(s => s.IsWinner)` (`:56`) + `winners.Count == 0 → Conflict` (`:57-58`) | phép tính tiền ở `:88-90` | +| A3 | `ProposalFeatures.cs:258-267` | ✔ guard workflow-type TRỌN 2 nhánh: `wfType is null → NotFoundException` (`:264-265`) + `wfType != ProposalGeneral → ConflictException` (`:266-267`) | copy CẢ nhánh NotFound | +| A4 | `MenuKeys.cs:42` const + `:180` vào `All` | ✔ `KeHoachKyKet = "KeHoachKyKet"` (`:42`), có mặt trong `All` (`:180`) | ⇒ 4 policy `KeHoachKyKet.{Read,Create,Update,Delete}` sinh runtime (Program.cs nhân `All × Actions`) | +| A5 | `ApprovalWorkflow.cs` enum | ✔ `ContractSigningPlan = 10` (append-only, đúng slot) | dùng cho guard type | + +### 0.2 — Nền W1 đang có trên đĩa (7 entity + 7 config + DbSet + codegen) + +- Entity: `Domain/ContractSigningPlans/{ContractSigningPlan, ContractSigningPlanLine, ...DossierItem, ...Attachment, ...LevelOpinion, ...Changelog, ...Approval}.cs` + `ContractSigningPlanPhase.cs`. +- Phase enum: `DangSoanThao=1 · ChoDuyet=2 · DaDuyet=3 · TraLai=98 · TuChoi=99` (`ContractSigningPlanPhase.cs:8-15`). +- DbSet ×7: `IApplicationDbContext.cs:149-155`. +- Codegen: `Application/ContractSigningPlans/Services/IContractSigningPlanCodeGenerator.cs` — `Task GenerateAsync(CancellationToken)`; DI đã đăng ký `Infrastructure/DependencyInjection.cs:42`. + +### 0.3 — MaxLength THẬT (EF = source of truth, KHÔNG trust spec — bài S35) + +Đo từ Configurations (không đọc spec): + +| Field | EF config | file:line | +|---|---|---| +| `Plan.MaKeHoach` | 50 | `ContractSigningPlanConfiguration.cs:19` | +| `Plan.HoSoLink` | 1000 | `:21` | +| `Plan.GhiChu` | 2000 | `:22` | +| `Line.Note` | 1000 | `ContractSigningPlanLineConfiguration.cs:20` | +| `DossierItem.Name` | 500 **required** | `ContractSigningPlanDossierItemConfiguration.cs:18` | +| `DossierItem.TvgsName` | 200 | `:19` | +| `DossierItem.Note` | 1000 | `:20` | +| `Attachment.FileName` | 500 | `ContractSigningPlanAttachmentConfiguration.cs:24` | +| `Attachment.StoragePath` | 1000 | `:25` | +| `Attachment.ContentType` | 200 | `:26` | +| `Attachment.Note` | 1000 | `:28` | + +### 0.4 — HasQueryFilter: KHKK CÓ (khác HRM), trừ 2 bảng audit + +`grep -c HasQueryFilter` trên 7 config KHKK: có ở `ContractSigningPlan/Line/DossierItem/Attachment` (`:42/:33/:30/:38`). +`Changelog` + `Approval` = `BaseEntity` (không `IsDeleted`) ⇒ không có filter. `LevelOpinion` = `AuditableEntity` nhưng **CỐ Ý KHÔNG filter** (`ContractSigningPlanLevelOpinion.cs:18-20` — site dọn chữ ký mồ côi phải thấy row soft-deleted). + +⇒ Hệ quả cho W2: list/detail **KHÔNG cần** `.Where(!IsDeleted)` thủ công (filter tự chạy); ngược lại màn "đã xóa" BẮT BUỘC `IgnoreQueryFilters().Where(x => x.IsDeleted)` — luật `ContractFeatures.cs:356-360` (Ignore GỠ filter chứ không ĐẢO). + +### 0.5 — LỆCH SPEC vs COMMENT W1 (lead đã phân xử: SPEC THẮNG) + 2 site cùng-lớp lead CHƯA nêu + +Lead giao sửa 1 chỗ. Grep cùng-lớp ra **3** chỗ khai cùng một điều sai ("gen lúc Submit"): + +| # | Site | Nguyên văn | Lead giao? | +|---|---|---|---| +| 1 | `Domain/ContractSigningPlans/ContractSigningPlan.cs:17` | `// "KHKK/2026/001" — gen atomic lúc Submit (W2)` | ✔ CÓ | +| 2 | `Application/ContractSigningPlans/Services/IContractSigningPlanCodeGenerator.cs:9` | `// Caller: W2 Submit (gen lúc trình duyệt, KHÔNG lúc tạo nháp — mirror quyết spec cũ)` | ✗ KHÔNG | +| 3 | `Infrastructure/.../ContractSigningPlanConfiguration.cs:24` | `// Mã phiếu UNIQUE khi đã gen (gen lúc Submit ⇒ nháp còn null)` | ✗ KHÔNG | + +Xử: sửa cả 3 (comment-only, 0 thay đổi hành vi) — theo bài `feedback_root_cause_over_symptom` "được chỉ 1 chỗ ⇒ PHẢI grep cùng-lớp". Index filtered `[MaKeHoach] IS NOT NULL` GIỮ NGUYÊN (vẫn đúng: gen-lúc-create thì cột luôn NOT NULL, filter chỉ rộng hơn cần thiết chứ không sai). + +**ĐÃ LÀM** — 3 Edit comment-only: +- `ContractSigningPlan.cs:17-22` (5 dòng comment mới + prop giữ nguyên chữ ký `public string? MaKeHoach`) +- `IContractSigningPlanCodeGenerator.cs:9-11` +- `ContractSigningPlanConfiguration.cs:24-27` + +--- + +## §1 — CREATE (`CreateContractSigningPlanCommand`) + +File: `src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` (Region 1). + +**Chữ ký:** `CreateContractSigningPlanCommand(Guid PeId, Guid ApprovalWorkflowId, string? GhiChu = null) : IRequest` +→ `CreateContractSigningPlanResult(Guid Id, string MaKeHoach, int LineCount)`. + +🔴 Tên `PeId` (KHÔNG `PurchaseEvaluationId` theo convention repo) là CỐ Ý: spec §②-2 viết đúng chữ đó, lane FE dựng body JSON từ CÙNG bản spec ⇒ đổi tên = gãy tích hợp 2 lane (`camelCase` body sẽ là `peId`). Đã ghi lý do vào `` ngay trên record. + +**4 rào theo đúng thứ tự** (fail sớm, rào rẻ trước): +1. Phase PE `!= DaDuyet` → `ConflictException` — mirror `CreateContractFromEvaluationFeatures.cs:53-54`. +2. `winners = pe.Suppliers.Where(s => s.IsWinner)`, rỗng → Conflict — mirror `:56-58`. +3. Đã có kế hoạch SỐNG (`AnyAsync(p.PurchaseEvaluationId == pe.Id && p.Phase != TuChoi)`) → Conflict 409. `TuChoi` không tính ⇒ lập lại được; phiếu xoá mềm cũng không tính (global filter tự loại) ⇒ acceptance "DELETE → tạo lại → 201" đi qua rào này. +4. Workflow type — copy TRỌN 2 nhánh Proposal `:264-267` vào helper `ContractSigningPlanScope.EnsureWorkflowTypeAsync` (dùng lại ở Update). NotFound giữ nguyên là `NotFoundException`, sai loại là `ConflictException`. + +**Codegen:** `await codeGen.GenerateAsync(ct)` gọi **TRƯỚC** `db.ContractSigningPlans.Add(plan)`. +Lý do đo được: `ContractSigningPlanCodeGenerator.cs:23-43` mở transaction SERIALIZABLE **rồi gọi `db.SaveChangesAsync` nội bộ** ⇒ nếu phiếu đã nằm trong change-tracker, lượt lưu đó flush phiếu DỞ (chưa có mã) trong transaction của codegen (bài S88 self-committing codegen). Add + SaveChanges đúng 1 lần ở cuối. + +**Lines auto-sinh:** 1 dòng / winner, `PeReferenceAmount = SUM(Quote.ThanhTien WHERE IsSelected && q.PurchaseEvaluationSupplierId == w.Id)` — copy `:88-90`. +🔴 So với `w.Id` (Id của ROW `PurchaseEvaluationSuppliers`), KHÔNG phải `w.SupplierId` (Id master) — nhầm 2 cái này là cộng nhầm tiền của NCC khác. Snapshot 1 lần, không đọc live (cùng triết lý freeze NS Mig 67). + +**Denorm:** `ProjectId`/`DepartmentId` ← PE. `DrafterUserId` ← **currentUser** (KHÔNG phải `pe.DrafterUserId`) — xem §6 DECISION-2. + +**Changelog:** 1 entry `Header/Insert/PhaseAtChange=DangSoanThao` + `ContextNote = "Nguồn: phiếu Duyệt NCC {MaPhieu} ({Id})"`. + +## §2 — UPDATE nháp (`UpdateContractSigningPlanDraftCommand`) + +`(Guid Id, string? GhiChu = null, string? HoSoLink = null, Guid? ApprovalWorkflowId = null)`. +- Guard qua `ContractSigningPlanScope.EnsureDraftEditable(plan, currentUser)`: (a) owner ∥ Admin → `ForbiddenException`; (b) phase ∈ {DangSoanThao, TraLai} → `ConflictException`. +- **Null-safe 3/3 field** (#73/S42): `null` = client không gửi ⇒ GIỮ; `""` = chủ động xoá ⇒ set null (khuôn `HoSoLink` `PurchaseEvaluationFeatures.cs:285-286`). Không field nào absolute-set. +- Re-pin workflow: **guard type LẠI** (admin có thể đổi loại quy trình giữa chừng) — cùng helper §1 rào 4. + +## §3 — GET detail / LIST / INBOX / ĐÃ XOÁ + +**`GetContractSigningPlanQuery`** — Include `Lines + DossierItems + Attachments + LevelOpinions` (khuôn Proposal `:159-162`) + tra thêm: PE (MaPhieu/TenGoiThau), Project (Name/Code), Department, Drafter, **tên NCC 1 lượt** (`Suppliers` dictionary dùng chung Lines + DossierItems), **workflow tree** (Steps→Levels + tên người duyệt) và metadata Cấp cho `LevelOpinions` (StepOrder/StepName/LevelOrder/ApproverUserId) — mirror `ProposalFeatures.cs:182-217` nhưng KHÔNG dùng `SelectMany` flatten (chỉ dựng `Dictionary`). + +**`ListContractSigningPlansQuery(Phase?, ProjectId?, PurchaseEvaluationId?, PendingMe) : PagedRequest`** +- Không cần `.Where(!IsDeleted)` thủ công — 4 bảng KHKK CÓ global filter (§0.4). +- `PendingMe=true` → giao với `ResolveV2InboxIdsAsync` (non-admin) hoặc `AdminInboxPhases` (admin). +- **IDOR scope** non-admin — xem §6 DECISION-3. + +**`GetMyContractSigningPlanInboxQuery`** — admin: phase-list TƯỜNG MINH `AdminInboxPhases = [ChoDuyet]` (mirror PE `:731-742`, nháp KHÔNG vào inbox theo S89); non-admin: `ResolveV2InboxIdsAsync` mirror `PurchaseEvaluationFeatures.cs:817-852` — in-memory join vì `CurrentWorkflowStepIndex` là INDEX 0-based vào list Steps đã sort, không dịch thẳng SQL được; so Cấp bằng GIÁ TRỊ `Level.Order` + `Any(...)` ⇒ **giữ OR-of-N**. W2 luôn trả rỗng (chưa có phiếu ChoDuyet vì transition = W3) — wire sẵn, W3 không phải sửa list/inbox. + +**`ListDeletedContractSigningPlansQuery`** — `IgnoreQueryFilters().Where(x => x.IsDeleted)` theo LUẬT `ContractFeatures.cs:356-360` (Ignore **GỠ** filter chứ không ĐẢO) + cùng vị ngữ IDOR như list sống. + +Projection dùng chung 3 query qua `ListContractSigningPlansQueryHandler.ProjectPagedAsync` (1 hình dạng DTO, không lệch). + +## §4 — Dossier items (căn cứ b.8-9) + Attachments + +- `UpsertContractSigningPlanDossierItemCommand(PlanId, Id?, Kind, SupplierId?, Name, Status, TvgsName?, TvgsResultAt?, Note?)` → `Guid`. `Id == null` ⇒ Insert, có ⇒ Update (verify item thuộc ĐÚNG phiếu). Guard = `EnsureDraftEditable` ⇒ **chỉ {DangSoanThao, TraLai}**. +- `DeleteContractSigningPlanDossierItemCommand` — `Remove()` = XOÁ MỀM (`AuditingInterceptor.cs:54-63` đổi Deleted→Modified + set IsDeleted/DeletedAt/DeletedBy) + changelog. +- **Attachments MỞ MỌI PHASE** (triết lý PE S147): người duyệt phải bổ sung được scan kết quả TVGS ngay lúc đang duyệt ⇒ 3 handler upload/download/delete KHÔNG gọi `EnsureDraftEditable`. Chỉ CĂN CỨ mới khoá theo phase. +- Upload: `IFileStorage` + path `contract-signing-plans/{planId}/{attId}_{safeName}` + sanitize (mirror PE `:115-123`). `DossierItemId` phải thuộc đúng phiếu — **handler tự kiểm** vì cột đó CỐ Ý không có FK vật lý (tránh multiple cascade path, `ContractSigningPlanAttachment.cs:14-19`) ⇒ DB không kiểm hộ. +- Validator MaxLength lấy từ EF KHKK (FileName **500** / ContentType 200 / Note 1000) — KHÔNG copy số của PE (255/100/500); PE hẹp hơn vì là bản cũ. + +## §5 — DELETE phiếu + PICKER + +**`DeleteContractSigningPlanCommand(Id, Reason?)`** — owner ∥ Admin; allow-list **{DangSoanThao, TuChoi}** (M6: thiếu đường này thì lỡ tạo nhầm 1 kế hoạch là PE khoá 409 vĩnh viễn). +🔴 Thứ tự `db.ContractSigningPlans.Remove(plan)` **TRƯỚC** `Add(changelog)` — bug T26 S155: EF chạy cascade client-side NGAY tại `Remove()` (`CascadeDeleteTiming.Immediate`), changelog đang ở state `Added` sẽ bị DETACH ⇒ SaveChanges ghi 0 row, IM LẶNG. (Ở dossier-item/attachment thì changelog trỏ PLAN chứ không trỏ thứ bị Remove nên không dính, vẫn giữ cùng thứ tự cho nhất quán.) + +**`ListApprovedPeAwaitingPlanQuery`** — mirror `CreateContractFromEvaluationFeatures.cs:162-174`, đổi vị ngữ: bỏ `e.ContractId == null` (không liên quan GĐ2), thêm `có ≥1 IsWinner` + `chưa có plan sống` ⇒ picker khớp ĐÚNG 2 rào của Create, không mời user bấm vào thứ chắc chắn 409. +⚠️ `WinnerQuoteTotal` viết lại sau lần build đầu: bỏ `Any(...)` lồng trong `Where` của subquery (sâu 2 tầng, vỡ dịch SQL chỉ nổ LÚC CHẠY chứ không nổ lúc build) → dùng hình dạng subquery-có-join đã chạy được ở `:185-189`. + +## §6 — 4 QUYẾT ĐỊNH lệch/ngoài chữ của lệnh (khai để lead phân xử) + +| # | Chỗ | Lệnh/spec nói | Tôi làm | Vì sao | +|---|---|---|---|---| +| D1 | tên tham số Create | spec + lệnh viết `PeId` | `PeId` (trái convention repo `PurchaseEvaluationId`) | spec = hợp đồng chung 2 lane; FE đọc CÙNG spec ⇒ đổi tên là gãy body JSON | +| D2 | `DrafterUserId` | lệnh gộp "DepartmentId/DrafterUserId/ProjectId denorm từ PE" | `= currentUser.UserId` (chỉ Project+Dept mới denorm) | (a) entity khai "BCH soạn kế hoạch" `ContractSigningPlan.cs:24` — người soạn PE (QS) ≠ người lập kế hoạch (BCH); (b) khuôn Proposal `:276` + PE `:147` đều lấy currentUser; (c) `pe.DrafterUserId` là `Guid?` còn cột này non-null ⇒ copy sang cho phiếu PE cũ thiếu drafter sẽ ra `Guid.Empty` = hỏng cả quyền sửa lẫn scope list | +| D3 | IDOR scope list | lệnh không nói | non-admin thấy: phiếu MÌNH soạn ∪ phiếu đã rời nháp thuộc quy trình mình có chân duyệt | bài 8b S155 "handler MỚI mặc định KHÔNG có rào nào"; khuôn Proposal (0 rào) là khuôn HỞ. Bỏ vế eligible-phase-theo-role vì module này KHÔNG có bản đồ role→phase. **Nới cho cả phòng = thêm 1 vế `DepartmentId` ở đúng 1 chỗ** (comment đã chỉ chỗ), áp cả list sống lẫn màn xoá | +| D4 | `ProposedAmount` lúc tạo | không nói | mồi `= PeReferenceAmount` | W2 KHÔNG có command sửa dòng giá (spec §②-2 không liệt kê) ⇒ để 0 là cả phiếu 0 và không ai sửa được. Mồi = giá đã duyệt ở PE = trường hợp thường gặp | + +**Cố ý KHÔNG làm (ngoài spec, để W3):** command sửa Line (ProposedAmount/Note) · mọi transition (submit/approve/reject/return) · `SlaDeadline` để null (cột là display-only, không job — `ContractSigningPlan.cs:34-37`) · xoá phiếu đang duyệt (cần rào đúng-lượt như `PeSoftDeleteFeatures`). + +## §7 — Endpoint + authz (14 action, per-action policy 14/14) + +Route gốc `api/contract-signing-plans`, class `[Authorize(Policy = "KeHoachKyKet.Read")]`. + +| Verb | Path | Policy | +|---|---|---| +| GET | `/` | `KeHoachKyKet.Read` | +| GET | `/inbox` | Read | +| GET | `/deleted` | Read | +| GET | `/approved-pe-awaiting-plan` | Read | +| GET | `/{id}` | Read | +| POST | `/` | **Create** | +| PUT | `/{id}` | **Update** | +| DELETE | `/{id}` | **Delete** | +| POST | `/{id}/dossier-items` | **Update** | +| PUT | `/{id}/dossier-items/{itemId}` | **Update** | +| DELETE | `/{id}/dossier-items/{itemId}` | **Update** | +| POST | `/{id}/attachments` | **Update** | +| GET | `/{id}/attachments/{attId}/download` | Read | +| GET | `/{id}/attachments/{attId}/view` | Read | +| DELETE | `/{id}/attachments/{attId}` | **Update** | + +(15 dòng bảng = 15 action; xem số đo grep ở §8.) +Ghi Read tường minh cả trên action chỉ-đọc: `[Authorize]` trần / để trống = lỗ hổng (gotcha #82 — ẩn menu KHÔNG đóng API). Dossier-item + attachment dùng **Update** chứ không Create: "tạo" ở module này = tạo PHIẾU. +Endpoint dossier-item verify `id == cmd.ContractSigningPlanId` → `BadRequest` (Pattern 12-ter). + + + diff --git a/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/sub-implementer-frontend-1.md b/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/sub-implementer-frontend-1.md new file mode 100644 index 0000000..9ae22a1 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/sub-implementer-frontend-1.md @@ -0,0 +1,54 @@ +# sub-implementer-frontend-1 — W2 KHKK FE 3 page x 2 app (S161) + +> Lane: FE-only. KHONG cham src/Backend (chi Read entity de suy DTO shape). KHONG commit. +> Ghi TUNG MUC ngay sau khi lam xong muc do (chong #53). + +## MUC 0 — Recon (DONE) + +Khuon nguon doc that (khong doan): +- `fe-user/src/pages/office/ProposalsListPage.tsx` 252 dong / `ProposalCreatePage.tsx` 245 / `ProposalDetailPage.tsx` 386 / `fe-user/src/types/proposal.ts` 95. + SHA256 = IDENTICAL voi fe-admin counterpart cho ca 4 file (do bang `sha256sum`): + - Create `fb852a8df3191aa8696468ee6c762c21710f662db146ab678933c57a7373464b` + - Detail `a550a5da29729d0d44981627bf8a6934255ac02c80477545f8c4eff393e83f81` + - List `780060dc1c4a9f08387e7ad35737846e38aff92191ad9fe2a7e8f2fa303f5af6` + - types `95607052ff1138f21d266d7fe66d45bab180c97270030f939be1ffcb681fdeda` +- Entity BE W1 (`src/Backend/SolutionErp.Domain/ContractSigningPlans/*.cs`, 8 file) = nguon shape cho types: + `ContractSigningPlan` (MaKeHoach, PurchaseEvaluationId, ProjectId, DepartmentId, DrafterUserId, Phase, + ApprovalWorkflowId, CurrentWorkflowStepIndex, CurrentApprovalLevelOrder, SlaDeadline, HoSoLink, GhiChu) + · `Line` (SupplierId, PeReferenceAmount, ProposedAmount, ApprovedAmount?, ContractId?, Note?) + · `DossierItem` (Kind 1/2/99, SupplierId?, Name, Status 1..4, TvgsName?, TvgsResultAt?, Note?) + · `Attachment` (FileName, StoragePath, FileSize, ContentType, Purpose 1/2/99, Note?, DossierItemId?) + · `Phase` enum 1/2/3/98/99. +- BE lane CHUA land file Application/Controller luc toi recon (`ls src/Backend/SolutionErp.Application/ContractSigningPlans/` = chi co `Services/` cua W1) + => types FE viet theo ENTITY (dung luat de bai: "uu tien shape ENTITY"). +- Layout staticMap truoc khi sua: fe-user `Layout.tsx:59-62,79-80` (6 leaf Khkk_* -> /coming-soon), + fe-admin `Layout.tsx:34-39` (6 leaf -> /coming-soon). +- `Khkk_*` KHONG ton tai trong `fe-{user,admin}/src/lib/menuKeys.ts` (grep 0 hit) => phai them (place 3). +- Pe_WfView hien tai: fe-user `Layout.tsx:159` -> `/purchase-evaluations/workflow-matrix?type=N` + (page `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx`, whitelist type `1|2|3` o `:60`). + fe-admin CO Y KHONG co Pe_WfView (`Layout.tsx:125-129` lead chot 2026-07-27 — khong co matrix page ben admin). +- fe-admin Designer da mo type-10: `ApprovalWorkflowsV2Page.tsx:147` `TYPE_INT_CONTRACT_SIGNING_PLAN = 10`, + `:162` `ContractSigningPlan: 10` (deep-link), route `fe-admin/src/App.tsx:76` `/system/approval-workflows-v2/:typeCode`. +- Upload multipart khuon: `fe-user/src/components/pe/PeDetailTabs.tsx:3323-3331` (FormData + header multipart) + + download blob `:3352-3360`. + +## MUC 1 — 4 file MOI ben fe-user (DONE, viet truoc roi cp sang fe-admin) + +- `fe-user/src/types/khkk.ts` — enum-as-const 4 bo (Phase 1/2/3/98/99 · DossierItemKind 1/2/99 · + DossierItemStatus 1..4 · AttachmentPurpose 1/2/99) + badge full-class-literal (Pattern 14) + + DTO (List/Line/DossierItem/Attachment/LevelOpinion/Detail) + input (Create/UpdateDraft/UpsertDossier) + + `KhkkPeCandidateDto` (CO Y la TAP CON cua `PurchaseEvaluationListItemDto` BE tra — field thua bo qua runtime) + + `PagedResult`. Header file KHAI HOP DONG FE<->BE 12 endpoint (de BE lane doi chieu 1 cho). +- `fe-user/src/pages/khkk/KhkkListPage.tsx` — 6 KpiCard loc phase + o tim + bang 7 cot + phan trang; + `?filter=` map ten-phase -> int (`FILTER_TO_PHASE`), `?view=deleted` -> endpoint rieng + `/contract-signing-plans/deleted` (mirror PE S155) + an nut tao + row khong click. + 🔴 Chip loc chi doi STATE (khong ghi URL) => menu khong sang nham (bug UAT S155). +- `fe-user/src/pages/khkk/KhkkCreatePage.tsx` — picker PE nguon (list nut chon, hien maPhieu + + tenGoiThau + du an/hang muc + chip winner names) + dropdown workflow `applicableType=10` loc + `isUserSelectable` (khuon ProposalCreatePage:59-69, endpoint tra `{types:[{applicableType,history}]}` + KHONG phai flat array) + GhiChu; POST -> navigate `/khkk/{id}`. FE KHONG gui lines (BE sinh tu winner). +- `fe-user/src/pages/khkk/KhkkDetailPage.tsx` — 4 section: Thong tin (sua GhiChu/HoSoLink khi + DangSoanThao|TraLai) · bang Lines per-NCC READ-ONLY (PeReferenceAmount/Proposed/Approved formatVnd — + W2 khong co endpoint sua line nen KHONG bia) · bang DossierItems chip 4 muc + upsert/delete dialog + (chi mo khi draft) · Attachments upload multi/download blob/delete (mo MOI phase). + 🔴 KHONG panel trinh/duyet + KHONG banner cap duyet = W3.