From 79ef8da9f4ca2f7c70220db15608f0d411157327 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Sat, 13 Jun 2026 01:07:27 +0700 Subject: [PATCH] =?UTF-8?q?=EF=BB=BF[CLAUDE]=20PurchaseEvaluation:=20ngan?= =?UTF-8?q?=20sach=20goi=20thau=20theo=20Excel=20anh=20Kiet=20-=20bang=20t?= =?UTF-8?q?ong=20hop=202=20block=20+=20nhap=20theo=20role=20PRO/CCM=20+=20?= =?UTF-8?q?xoa=20module=20Budget=20cu=20(Mig=2050)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mig 50 ReplaceBudgetModuleWithPeWorkItemBudgets: bang moi PeWorkItemBudgets (1 record/cap Du an x Hang muc, UNIQUE filtered [IsDeleted]=0) + drop 5 bang Budget cu + PE/Contracts drop BudgetId + backfill BudgetManualAmount->BudgetPeriodAmount TRUOC DropColumn (phieu UAT giu so) + DELETE menu/permission Bg_* IN-list children-first - BE: PUT {id}/budget/pro (role Procurement) + {id}/budget/ccm (role CostControl, Adjustment cho phep AM) fail-closed Forbidden-truoc-side-effect + EnsureTrackedAsync race-safe (catch unique -> re-fetch winner, loi khac rethrow) + auto-create record khi tao phieu + budgetSummary DTO (luy ke trinh-truoc/chon-thau-truoc/de-xuat-ky-nay + full fallback du-tru-PRO + canEdit flags) + submit-guard (3) doi predicate BudgetPeriodAmount -> "chua nhap Ngan sach ky nay" + PATCH budget-adjust absolute-set 2 field moi + Contract GIU BudgetManual* (HD nhap tay khong doi) + ke thua HD map BudgetPeriodAmount - FE x2 app SHA256 identical: bang "TONG HOP NGAN SACH TRINH KY" block A (full dam + ban hanh + V0 hieu chinh + du tru PRO + ghi chu, editable theo canEditPro/canEditCcm) + block B 9 dong cong thuc Excel (5=1+3, 6=2+4, 7=full-5, 8 tu nhap default 7, 9=4+8) + to mau vuot ngan sach #C00000 / am do / red-soft row8>row7 + "Chua chon" khi count=0 + banner phieu chua gan Hang muc + o "Ngan sach ky nay" o create/header + XOA pages/components/types budgets + routes + menuKeys + Layout staticMap 4-place - Tests: +22 PeWorkItemBudgetTests (auto-create x3, ensure/race x2, authz matrix PRO x5 + CCM x3, budgetSummary aggregates x5, adjust x4) - 14 BudgetPolicyTests xoa theo module - 1 test via-BudgetId -> 263 PASS (45 Domain + 218 Infra, 0 fail) - database-agent advise adopted: khong FK vat ly PE/Contracts->Budgets (DropColumn khong can DropForeignKey) + DropIndex truoc DropColumn (SQL 5074) + IN-list thay LIKE Bg_% (underscore wildcard + miss root) + khong Serializable wrap (nested-tx conflict codegen) - Reviewer PASS-with-minor 0 blocker (verdict-first survived); 2 minor da sua truoc commit (comment adjustMut absolute-set + dead key budgetId); note: F4 approver-edit-budget UI entry tam drafter-only, BE van cho approver scope - cho UAT anh Kiet - Scaffold-bug caught: EF tu sinh RenameColumn BudgetManualAmount->ExpectedRemainingAmount (SAI semantics) -> thay bang Add+UPDATE+Drop Co-Authored-By: Claude Fable 5 --- .claude/agent-memory/cicd-monitor/MEMORY.md | 1 + .../agent-memory/harvest-curator/MEMORY.md | 1 + .claude/agent-memory/reviewer/MEMORY.md | 1 + fe-admin/src/App.tsx | 5 - fe-admin/src/components/Layout.tsx | 4 - .../components/budgets/BudgetDetailTabs.tsx | 491 -- .../budgets/BudgetWorkflowPanel.tsx | 225 - fe-admin/src/components/pe/PeDetailTabs.tsx | 768 +- fe-admin/src/components/pe/PeHeaderForm.tsx | 115 +- .../components/pe/PeWorkspaceCreateView.tsx | 95 +- fe-admin/src/lib/menuKeys.ts | 7 +- .../src/pages/budgets/BudgetCreatePage.tsx | 173 - .../src/pages/budgets/BudgetsListPage.tsx | 263 - .../pages/contracts/ContractCreatePage.tsx | 230 +- fe-admin/src/types/budget.ts | 168 - fe-admin/src/types/contracts.ts | 15 +- fe-admin/src/types/purchaseEvaluation.ts | 42 +- fe-user/src/App.tsx | 5 - fe-user/src/components/Layout.tsx | 4 - .../components/budgets/BudgetDetailTabs.tsx | 491 -- .../budgets/BudgetWorkflowPanel.tsx | 225 - fe-user/src/components/pe/PeDetailTabs.tsx | 768 +- fe-user/src/components/pe/PeHeaderForm.tsx | 115 +- .../components/pe/PeWorkspaceCreateView.tsx | 95 +- fe-user/src/lib/menuKeys.ts | 7 +- .../src/pages/budgets/BudgetCreatePage.tsx | 173 - fe-user/src/pages/budgets/BudgetsListPage.tsx | 263 - .../pages/contracts/ContractCreatePage.tsx | 230 +- fe-user/src/types/budget.ts | 168 - fe-user/src/types/contracts.ts | 15 +- fe-user/src/types/purchaseEvaluation.ts | 42 +- .../Controllers/BudgetsController.cs | 100 - .../PurchaseEvaluationsController.cs | 26 +- .../BudgetDepartmentApprovalFeatures.cs | 74 - .../Budgets/BudgetFeatures.cs | 531 -- .../Budgets/Dtos/BudgetDtos.cs | 90 - .../Interfaces/IApplicationDbContext.cs | 11 +- .../Contracts/ContractFeatures.cs | 44 +- .../Contracts/Dtos/ContractDtos.cs | 5 +- .../CreateContractFromEvaluationFeatures.cs | 10 +- .../Dtos/PurchaseEvaluationDtos.cs | 42 +- .../PeWorkItemBudgetFeatures.cs | 186 + .../PurchaseEvaluationFeatures.cs | 237 +- .../SolutionErp.Domain/Budgets/Budget.cs | 34 - .../Budgets/BudgetApproval.cs | 17 - .../Budgets/BudgetChangelog.cs | 28 - .../Budgets/BudgetDepartmentApproval.cs | 20 - .../Budgets/BudgetDetail.cs | 22 - .../SolutionErp.Domain/Budgets/BudgetPhase.cs | 17 - .../Budgets/BudgetPolicy.cs | 71 - .../SolutionErp.Domain/Contracts/Contract.cs | 6 +- .../SolutionErp.Domain/Identity/MenuKeys.cs | 13 +- .../PurchaseEvaluations/PeWorkItemBudget.cs | 31 + .../PurchaseEvaluations/PurchaseEvaluation.cs | 17 +- .../Persistence/ApplicationDbContext.cs | 10 +- .../Configurations/BudgetConfiguration.cs | 90 - .../Configurations/ContractConfiguration.cs | 1 - .../DepartmentApprovalsConfiguration.cs | 34 +- .../PeWorkItemBudgetConfiguration.cs | 31 + .../PurchaseEvaluationConfiguration.cs | 6 +- .../Persistence/DbInitializer.cs | 9 +- ...getModuleWithPeWorkItemBudgets.Designer.cs | 6200 +++++++++++++++++ ...eplaceBudgetModuleWithPeWorkItemBudgets.cs | 419 ++ .../ApplicationDbContextModelSnapshot.cs | 468 +- .../PurchaseEvaluationWorkflowService.cs | 9 +- .../Budgets/BudgetPolicyTests.cs | 145 - ...reateContractCommandApplicableTypeTests.cs | 1 - .../Application/PeWorkItemBudgetTests.cs | 694 ++ .../Application/PeWorkItemGuardTests.cs | 8 +- .../Services/PeSubmitGuardAndBypassTests.cs | 46 +- 70 files changed, 9052 insertions(+), 5956 deletions(-) delete mode 100644 fe-admin/src/components/budgets/BudgetDetailTabs.tsx delete mode 100644 fe-admin/src/components/budgets/BudgetWorkflowPanel.tsx delete mode 100644 fe-admin/src/pages/budgets/BudgetCreatePage.tsx delete mode 100644 fe-admin/src/pages/budgets/BudgetsListPage.tsx delete mode 100644 fe-admin/src/types/budget.ts delete mode 100644 fe-user/src/components/budgets/BudgetDetailTabs.tsx delete mode 100644 fe-user/src/components/budgets/BudgetWorkflowPanel.tsx delete mode 100644 fe-user/src/pages/budgets/BudgetCreatePage.tsx delete mode 100644 fe-user/src/pages/budgets/BudgetsListPage.tsx delete mode 100644 fe-user/src/types/budget.ts delete mode 100644 src/Backend/SolutionErp.Api/Controllers/BudgetsController.cs delete mode 100644 src/Backend/SolutionErp.Application/Budgets/BudgetDepartmentApprovalFeatures.cs delete mode 100644 src/Backend/SolutionErp.Application/Budgets/BudgetFeatures.cs delete mode 100644 src/Backend/SolutionErp.Application/Budgets/Dtos/BudgetDtos.cs create mode 100644 src/Backend/SolutionErp.Application/PurchaseEvaluations/PeWorkItemBudgetFeatures.cs delete mode 100644 src/Backend/SolutionErp.Domain/Budgets/Budget.cs delete mode 100644 src/Backend/SolutionErp.Domain/Budgets/BudgetApproval.cs delete mode 100644 src/Backend/SolutionErp.Domain/Budgets/BudgetChangelog.cs delete mode 100644 src/Backend/SolutionErp.Domain/Budgets/BudgetDepartmentApproval.cs delete mode 100644 src/Backend/SolutionErp.Domain/Budgets/BudgetDetail.cs delete mode 100644 src/Backend/SolutionErp.Domain/Budgets/BudgetPhase.cs delete mode 100644 src/Backend/SolutionErp.Domain/Budgets/BudgetPolicy.cs create mode 100644 src/Backend/SolutionErp.Domain/PurchaseEvaluations/PeWorkItemBudget.cs delete mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/BudgetConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/PeWorkItemBudgetConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260612173224_ReplaceBudgetModuleWithPeWorkItemBudgets.Designer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260612173224_ReplaceBudgetModuleWithPeWorkItemBudgets.cs delete mode 100644 tests/SolutionErp.Domain.Tests/Budgets/BudgetPolicyTests.cs create mode 100644 tests/SolutionErp.Infrastructure.Tests/Application/PeWorkItemBudgetTests.cs diff --git a/.claude/agent-memory/cicd-monitor/MEMORY.md b/.claude/agent-memory/cicd-monitor/MEMORY.md index 7f79d88..feb5a11 100644 --- a/.claude/agent-memory/cicd-monitor/MEMORY.md +++ b/.claude/agent-memory/cicd-monitor/MEMORY.md @@ -68,6 +68,7 @@ BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code / ## 📅 Recent runs (FIFO — older → archive/git) +- **2026-06-12 S60 Run #283 (run_number 283) sha=`37122f0` PASS ~5m (PE guard 4-thông-tin mục 3 khi gửi duyệt + bypass người-soạn-trong-chuỗi-duyệt + rename heading "Đơn vị NCC/TP được chọn" — CROSS-STACK 1 BE service + 2 FE PeDetailTabs ×2 + 1 NEW test file):** Push `792c030..37122f0` 7 files: `PurchaseEvaluationWorkflowService.cs` (BE submit-guard + drafter-bypass) + `PeDetailTabs.tsx` ×2 app + `PeSubmitGuardAndBypassTests.cs` (NEW, +14 → 240→**254** expected) + 3 agent-memory `.md` (harvest-curator/investigator-codebase/test-specialist — `.claude/agent-memory/**` matches `**/*.md` glob → ignored, but `.cs`+`.tsx` present ⟹ whole-range builds, Discovery #3). GITEA_TOKEN+PROD_DB_PW empty → anon Gitea API + DB pw từ prod `appsettings.Production.json`→`ConnectionStrings.Default`. Run IN-PROGRESS first poll (running 11:55) — correctly did NOT FAIL, polled iter5 status=success (started ~11:54 → success 11:59:26 ≈5m). CI gate (both test proj pre-deploy ⟹ status=success ⟹ test gate 254 passed; `tasks` endpoint reports terminal as `status:success`, `conclusion` empty — trust success). **Bundle ROTATE BOTH (load-bearing, verified AFTER status=success — anti-pattern#3): admin `B1DtNT9C→akytoBnc` + user `D6uF3Mln→BzSdQmN0`** ✓ both touched (FE changed both apps). Brand `1F7DC1` preserved both HTML. Health live+ready **200/200** + admin/eoffice root 200. Smoke: PE unauth **401** + contracts unauth **401** + control `/api/zzz-not-a-route` **404** (auth gates real, routing live). **NO migration** — prod `__EFMigrationsHistory` top = Mig 49 `AddWorkItemToPurchaseEvaluation` == repo, GIỮ NGUYÊN ✓ (PE.MaPhieu col, not Code). sys.tables(excl mighist)=**92** (convention diff vs narrative-93, no new table — col/logic-only). **DATA INTACT (no-touch verify, sqlcmd): PE_count=3 · PE/2026/A/001 EXISTS (=1, phiếu UAT thật giữ nguyên ✓) · Suppliers=23 · WorkItems=71** — counts vs S59 (PE was 1 #275, Suppliers 22 #278) GREW from legit ongoing-UAT (this commit = FE+BE-service, NO DbInitializer/seed change → cannot resurrect/wipe; growth is user activity not deploy-induced). 0 regression. **LESSON (cross-stack submit-guard + drafter-bypass = ship-proof via run-success + test 254 + bundle-rotate-both + PE-data-preserved; the guard/bypass logic is internal handler behavior — cannot curl-assert "block submit when mục-3 incomplete" or "skip drafter in chain" without authed multi-step flow → rely on +14 PeSubmitGuardAndBypassTests in CI gate passing). SSH→sqlcmd via `iconv UTF-16LE|base64`→`powershell -EncodedCommand` (nested bash→ssh→PS strips `$vars`/mangles quotes); PE code column = `MaPhieu` NOT `Code`.** Tag `[s60, run283, pass, pe-submit-guard, drafter-bypass-in-chain, cross-stack, bundle-rotate-both, no-mig, test254, pe-a001-preserved]`. **↳đợt2 (14:14): Run #284 (run_number 284, id398) sha=`6db195d` PASS ~4m31s — GỠ hành động "Từ chối" khỏi quy trình PE (chỉ còn Duyệt/Trả lại; CROSS-STACK Domain `PurchaseEvaluationPolicy.cs` + Infra `PurchaseEvaluationWorkflowService.cs` guard + FE `PeWorkflowPanel.tsx` ×2 app + 2 NEW test `PurchaseEvaluationPolicyTests`/`PurchaseEvaluationWorkflowServiceGuardTests`, +2 → 254→256 expected: 59 Domain + 197 Infra). Push `37122f0..6db195d` 6 files (.cs+.tsx → full pipeline). Tokens empty → anon Gitea API + prod appsettings DB pw `ConnectionStrings.Default`. Run IN-PROGRESS first poll (running 14:31) — correctly did NOT FAIL, polled iter5 status=success (14:30:51→14:35:22 ≈4m31s; CI both-proj-pre-deploy ⟹ success ⟹ 256-gate passed, `conclusion` empty trust success). Bundle ROTATE BOTH (verified AFTER status=success — anti#3): admin `akytoBnc→DSvM8h3A` + user `BzSdQmN0→Cs2Tt5n6` ✓ both touched. Health live+ready 200/200 + admin/eoffice root 200 + PE unauth 401 + control /api/zzz-not-a-route 404. NO migration — prod top=Mig 49 `AddWorkItemToPurchaseEvaluation`==repo GIỮ ✓. sys.tables(excl mighist)=92. DATA INTACT: PE_count=4 (grew from 3 @#283 — legit ongoing-UAT; BE-policy+FE-only NO seed change → cannot resurrect/wipe) · PE/2026/A/001 EXISTS (=1 phiếu UAT thật giữ ✓). 0 regression. LESSON: "Từ chối"-removal = internal policy/handler behavior, cannot curl-assert "reject action gone" without authed multi-step flow → rely on +2 PolicyTests/GuardTests in CI gate passing. Tag `[s60-dot2, run284, pass, pe-remove-reject-action, cross-stack, bundle-rotate-both, no-mig, test256, pe-a001-preserved]`.** - **2026-06-11 S59-CLOSE Run #280 (run_number 280) sha=`69997da` PASS ~4m24s (FINAL đóng sổ session — FE-only ×2 PeDetailTabs+PeHeaderForm bỏ ô "Tên ngân sách" manual budget UAT vòng4):** Push `f21c55d..69997da` 4 `.tsx` (PeDetailTabs+PeHeaderForm ×2 app). **Run #279 (id393) sha=`f21c55d` (NCC table-fixed UAT vòng3) = `cancelled` @18:22:33 — supersede-BENIGN:** #280 push @18:22:34 (1s gap → Gitea concurrency-cancel in-flight) + `git merge-base --is-ancestor f21c55d 69997da`=TRUE ✓ (f21c55d preserved trong HEAD, ships via #280 — verified diff f21c55d→69997da chỉ +4 PeDetail/Header file, không re-touch 12 file vòng3). Tokens empty → anon Gitea API + prod appsettings DB. Poll iter4 status=success (18:22:34→18:26:58). **Bundle ROTATE BOTH FINAL (verified AFTER success +re-confirm STABLE no transient — anti#3): admin `BSh2fG2X→BKy_8OO9` + user `D22KfpPc→XcZ6PRyA`** ✓ session-close hash, brand `1F7DC1`+"Solutions ERP" preserved ×2. Health live+ready **200/200** + admin/eoffice root 200 + PE unauth 401 + control 404. **NO migration** (FE-only, Mig 49 held). LESSON (mirror Run #385 supersede-chain): same-SHA `cancelled` mid-flight = concurrency-supersede bởi newer push (1s HEAD-move), KHÔNG build/deploy-fault → ancestor-check TRUE = benign, verify prod qua SUCCESSFUL run #280 (NOT cancelled #279), KHÔNG escalate. Tag `[s59-close, run280-pass, run279-cancelled-benign, supersede-chain, fe-budget-name-remove-x2, bundle-rotate-both-FINAL, no-mig]`. **↳FINAL-v2 (tối): `80b64dd` (Run #281 cancelled-BENIGN) gỡ "Điều khoản thanh toán" 3-form ×2 → superseded bởi `792c030` Run #282 PASS ~4m (UAT vòng6 bỏ nút "+Thêm hạng mục" PeDetailTabs ×2). Ancestor 80b64dd⊂792c030=TRUE ✓ (792c030 chỉ re-touch PeDetailTabs, KHÔNG đụng PeHeaderForm/PeWorkspaceCreateView → paymentTerms-removal survives). Verify qua #282-success. Bundle ROTATE BOTH ĐÓNG-SỔ-THẬT (AFTER success +re-confirm STABLE no transient): admin `BKy_8OO9→B1DtNT9C` + user `XcZ6PRyA→D6uF3Mln` ✓, brand `1F7DC1` ok. Health live+ready 200/200 + 2 FE root 200. NO mig (FE-only). Lần thứ 3 liên tiếp supersede-chain (#279/#281 cancelled-benign) — pattern stable.** - **2026-06-11 Run #278 (run_number 278) sha=`9c330d2` PASS ~3m45s (S59-đợt6 CROSS-STACK — BE SuppliersController POST hạ `[Authorize(Roles="Admin,CatalogManager")]` → class-level `[Authorize]` any-auth (anh chốt quick-add NCC đi-thầu phát sinh liên tục), PUT/DELETE GIỮ khóa Admin+CatalogManager; FE×2 PeDetailTabs AddSupplierDialog SearchableSelect+quick-create+upload-multi + PeWorkflowPanel ẩn Trả-lại/Từ-chối khi drafterUserId==currentUser):** Push `faed59f..9c330d2` 5 files: 4 FE `.tsx` (PeDetailTabs+PeWorkflowPanel ×2 app) + `SuppliersController.cs`. `.cs`+`.tsx` → full pipeline RAN. GITEA_TOKEN+PROD_DB_PW empty → anon Gitea API + DB pw từ prod appsettings.Production.json→`ConnectionStrings.Default` (`vrapp/buKL3...`). Run IN-PROGRESS first poll (running) — polled iter6 status=success. **Bundle ROTATE BOTH (load-bearing FE×2, verified AFTER status=success +re-confirm stable ×2 NO transient — anti-pattern#3): admin `ex7Tc92G→BSh2fG2X` + user `DzUeSk96→D22KfpPc`** ✓ both touched. Brand `1F7DC1`+"Solutions ERP" preserved. Health live+ready **200/200** + admin/eoffice root 200. **NO migration** — prod `__EFMigrationsHistory` top = Mig 49 `AddWorkItemToPurchaseEvaluation` == repo ✓. **★ AUTHZ PROBE 4-điểm (the change-point — asymmetric POST-open/DELETE-locked verify) ALL PASS:** (a) unauth POST /api/suppliers (no token) = **401** ✓ (vẫn phải login, KHÔNG anonymous — class `[Authorize]` giữ); (b1) nv.test (Drafter non-admin) POST `{code:ZZCICD-TEST,type:1}` = **201** ✓ id `bc64c0c0-...` (quick-add mở OK); (b2) nv.test DELETE same-id CÙNG token = **403** ✓ (Sửa/Xóa vẫn khóa Admin+CatalogManager — method-level attr giữ); cleanup admin DELETE = **204** + GET = **404** ✓ (probe gỡ sạch). Spot sqlcmd ground-truth: **WorkItems active=71 HELD** ✓ (no resurrect) + **Suppliers active=22** ✓ (==pre-probe; DELETE là SOFT `IsDeleted=1` → active count về 22, total=23 với 1 tombstone ZZCICD `IsDeleted=1` — by-design audit, KHÔNG leak: list/GET ẩn nó). ⚠️ API `/api/suppliers` list trả 20 (paginated/filtered default page — KHÔNG authoritative, dùng sqlcmd cho count thật). Test gate (CI both proj pre-deploy ⟹ success=passed). 0 regression. **LESSON: cross-stack authz-relax verify = probe CẢ asymmetry — (i) unauth vẫn 401 (relax ≠ anonymous), (ii) target-role action mở (201), (iii) SIBLING action vẫn locked (403 same token), (iv) cleanup soft-delete → active-count về baseline + tombstone total+1 (soft-delete ≠ rác nếu list/GET ẩn). API list-count KHÔNG tin (pagination) → sqlcmd `WHERE IsDeleted=0` cho count thật.** Tag `[s59-dot6, run278, pass, supplier-post-authz-relax, asymmetric-probe-401-201-403, soft-delete-cleanup, bundle-rotate-both, no-mig, wi71-held]`. - **2026-06-11 Run #277 (run_number 277) sha=`faed59f` PASS ~4m09s (S59-đợt5 FE-only ×2 — NEW ui/SearchableSelect combobox gõ-lọc-bỏ-dấu + PeWorkspaceCreateView/PeHeaderForm Hạng mục+Dự án combobox + auto-fill Địa điểm từ Project.Location + PeDetailTabs paymentTerms Textarea; UAT 4-điểm screenshot 16:40):** Push `c869d26..faed59f` 8 files all `.tsx` (4 fe-admin + 4 fe-user, NEW SearchableSelect mirror ×2) → pipeline RAN. Tokens empty → anon API + prod appsettings pw. Run IN-PROGRESS first poll (running 17:40) — polled iter5 status=success (17:40:18→17:44:27). **Bundle ROTATE BOTH (verified AFTER success +re-confirm stable NO transient — anti-pattern#3): admin `BBA0KSWu→ex7Tc92G` + user `DzdTI18G→DzUeSk96`** ✓ both apps touched. Brand `1F7DC1` preserved both HTML. Health live+ready **200/200** + admin/eoffice root 200. **NO migration** (FE-only). Spot DB: **WorkItems active=71 HELD** (==#276 ✓ no resurrect — FE-only no restart risk) + PE=1 (info-only UAT leftover unchanged, NOT regression). Test gate (CI both proj pre-deploy ⟹ success=passed). 0 regression. LESSON: FE-only follow-up of a data-session needs only light WorkItems=71+PE re-confirm (no infra re-audit). SSH→sqlcmd pw-read: nested bash→ssh→PS strips `$vars` → use `iconv UTF-16LE|base64` → `powershell -EncodedCommand`. Tag `[s59-dot5, run277, pass, fe-searchableselect-x2, bundle-rotate-both, no-mig, wi71-held]`. diff --git a/.claude/agent-memory/harvest-curator/MEMORY.md b/.claude/agent-memory/harvest-curator/MEMORY.md index ab4b827..31640ee 100644 --- a/.claude/agent-memory/harvest-curator/MEMORY.md +++ b/.claude/agent-memory/harvest-curator/MEMORY.md @@ -34,3 +34,4 @@ H2 harvest-MD-integrity auditor **SOLUTION_ERP-self**. Read-only + **propose-onl - **2026-06-11 (S59 @start RE-REPORT — post-S58 đóng-sạch):** Verdict 🟢 CLEAN cả 5 mục — tree clean HEAD `1577927` (commit 14:33:20), 11/11 agent-memory mtime ≤14:32 đều TRONG closeout → **0 delta mồ-côi**. Wave=0 (workflows/ chỉ README+hmw.js) · stray=0 (find prune node_modules) · 0-byte=0 (agent-memory + user-memory) · user-memory index 22 entry = 22 file khớp, MEMORY.md 5.6KB>0. Record-S58 hiện hữu: cicd `:71-74` (#385→#386/#384/#382/#381) · designer proxy `:39` · inv `:73` (tag s57bis drift giữ nguyên). 4 on-behalf s57bis (db/impl-be `:77`/impl-fe `:45`/reviewer `:60`) mtime 13:00 = closeout đóng 4-MISS ✅. **cicd #383 VẪN `:89` khu archived NHƯNG em main đã ANNOTATE "[⚠️ VỊ TRÍ LẠC — entry MỚI 2026-06-11...]"** thay vì MOVE → guard chống archive-nhầm OK, relocate thật khi curate. Mojibake 2 hit ĐỀU quoted-evidence (cicd `:79` #377 + MY OWN `:33` self-quote khi tả hit cicd — các session sau đừng false-alarm file mình). Chore carry: **cicd 41.3KB + inv-codebase 32.9KB > cap → curate-L2 P1**; reviewer 29.6 + impl-be 27.9 watch. Method ⭐: `tail -c N` cắt giữa UTF-8 multi-byte in `�` GIẢ — corruption thật phải grep literal U+FFFD (chỉ 2 file). Brief em main liệt kê impl-fe+reviewer là "S58 spawn" nhưng on-disk 2 role chỉ có s57bis on-behalf — khớp GATE S58 8-spawn set, flag đối chiếu không phán. Tag [s59-start, clean, mojibake-self-quote]. - **2026-06-11 (S59 `/session-end` 5-trục GATE — tối, 9 body-spawn + closeout-floor):** **GATE PASS 5/5, 0 MISS** (lần đầu 0 on-behalf cần đề xuất kể từ chuỗi 4-MISS post-S57bis). Coverage: tooling ×2 (`:35` start + `:36` end-run mtime 18:15 NGOÀI brief 9-spawn = closeout-floor H1 → reconcile tổng 11) · harvest `:34` · inv recon `:73` · **cicd 6/6 entry RIÊNG #273-278 `:71-76`, sha-chain khớp git log 1:1** (56882ac→9c330d2, topic khớp commit-msg từng run) · 7 role không-spawn 0 phantom. Completeness 4-field 10/10 (#275 honest PASS-PARTIAL PE=1 UAT-leftover). Placement: đúng nhà, stray=0, inv S51→archive VERBATIM moved-not-cut + pointer "curated S59"; nit FIFO swap #273(`:75`)↔#274(`:76`) + archive double-blank. Corruption: 0-byte=0 ×2 nơi · mojibake 3 hit ĐỀU quoted-cũ (cicd:85 = #377 shift ĐÚNG +6 từ :79 — arithmetic shift = số dòng insert, method nhanh phân biệt cũ/mới) · **cicd 54KB/102-dòng (brief nói ~46) + inv 32.9KB over-cap → curate-L2 P1**. Fidelity: bundle BSh2fG2X/D22KfpPc triangulated 5 nguồn (cicd:71 + STATUS:6/:27 + HANDOFF:5 + log:4/:43) · WI=71 ×3 entry · gotcha #61/#62 disk `:1099/:1111` · tooling end-entry tree-state khớp git status độc lập — no-escalate. **2 flag content:** cicd `:53` "Bundle live S59" STALE 1 run (ghi #277 ex7Tc92G/DzUeSk96, live thật = #278 — entry :71 đúng, chỉ status-line lệch) → propose 1-line fix; #275 "09:46:42 sáng" nghi UTC-mislabel = 16:46 chiều local (STATUS:6 nói "chiều nay"; kết luận not-resurrect GIỮ). Tag [s59-end, gate-pass-5/5, 0-miss, cicd-54kb, line53-stale]. - **2026-06-12 (S60 @start RE-REPORT — post-S59 đóng-TRỌN):** Verdict 🟢 CLEAN cả 5 mục — tree clean HEAD `6bf28bf` (18:49:21), 11/11 agent-memory mtime ≤18:42:45 đều TRONG closeout → 0 mồ-côi. Coverage S59 14-spawn ĐỦ: H1 tooling `:35/:36` + H2 ×2 + recon inv `:73` + cicd run-coverage 10/10 #273→#282 (6 entry `:72-77` + `:71` #279/280 + extension FINAL-v2 #281/282 — 9 spawn→8 record-unit do supersede-fold, 0 run thiếu). Wave=0 · stray=0 · 0-byte=0 ×2 nơi · user-memory 23 file = index 22 + MEMORY.md 5.6KB khớp · cicd tail `0a` sạch. **2 flag GATE S59-end RESOLVED bởi em main:** `:53` bundle-live → FINAL-v2 `B1DtNT9C`/`D6uF3Mln` #282 ✓ + #275 UTC annotate `:75` ✓. #383 vẫn lạc (`:89`→`:96` shift +7 đúng arithmetic, annotation guard intact). Chore P1 carry: **cicd 56,480B/103L over-cap 3rd-session + phình 54→56.5KB/buổi** + inv 32,931B → curate-L2 (kèm relocate #383 + FIFO swap #273↔#274); watch reviewer 30,354B + impl-be 28,585B. Tag [s60-start, clean, flags-resolved, cicd-56kb]. *(em main APPEND B3 — H2-proposed, verify: 0-byte/tree-clean/size đối chiếu độc lập ✓)* +- **2026-06-12 (S61 @start RE-REPORT — S60 đóng KHÔNG trọn):** Verdict 🟡 — S60 2 commit (37122f0 11:53 + 6db195d 14:30) KHÔNG docs-closeout (STATUS/HANDOFF/session-log đều dừng S59). Harvest-MD: 3 delta ĐÃ COMMIT bundle 37122f0 (harvest `:34` + inv ×2 `:73-75` tag s60 đúng-hết-drift + test-spec `:56` +14→254) + 1 mồ-côi cicd dirty `:71` mtime 14:37 entry-kép #283/#284 SANE (bundle-chain B1DtNT9C→akytoBnc→DSvM8h3A + user-chain khớp #282, sha khớp git, 2 test file disk-verified mtime trước commit). Coverage 5/6: **MISS reviewer die-mid-run** (commit body 37122f0 tự khai, file intact KHÔNG 0-byte) → on-behalf APPENDED S61-start (die lần 3: S57bis ×2 + S60). Đợt2 6db195d 0 sub-delta = em-main-direct evidence (16-min turnaround, test mtime 14:27) — flag không phán. Wave=0 · stray=0 · 0-byte=0 ×2 nơi · mojibake 2 hit quoted-cũ. Flags: test-spec baseline 254 stale vs **256 thật (59 Dom+197 Infra — em main dotnet test S61-start)** · CLAUDE.md "240 test" stale · inv `:51` pointer archive nhưng S52 entry chỉ ở git (cut-honest-labeled, nit) · **cicd 61KB/104L over-cap 4th session +4.5KB/buổi → curate-L2 P1 GẤP** + inv 32.7KB. Method ⭐: commit-body = nguồn spawn-evidence khi sub die không để delta (reviewer "die mid-run" tự khai trong msg 37122f0). Tag [s61-start, s60-incomplete-close, reviewer-die-3rd, cicd-61kb]. *(em main APPEND B3 — H2-proposed, verify: commit-body 37122f0 reviewer-die ✓ + test 256 tự chạy ✓ + cicd-dirty diff đối chiếu độc lập ✓)* diff --git a/.claude/agent-memory/reviewer/MEMORY.md b/.claude/agent-memory/reviewer/MEMORY.md index 0070470..89e3d4f 100644 --- a/.claude/agent-memory/reviewer/MEMORY.md +++ b/.claude/agent-memory/reviewer/MEMORY.md @@ -57,6 +57,7 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod ## 📅 Recent activity (FIFO — older → archive/git) +- **2026-06-12 (S60 đợt1 PE submit-guard + drafter-bypass gate — KHÔNG DELIVER, die mid-run, on-behalf em main ghi hộ, H2-proposed):** Task: review `37122f0` cross-stack (BE TransitionAsync submit-guard đủ-4-thông-tin mục 3 + bypass người-soạn-trong-chuỗi V2 BƯỚC-ĐẦU-only + FE PeDetailTabs ×2 + 14 PeSubmitGuardAndBypassTests 240→254). Die mid-run #53-class (commit body tự khai "Reviewer die mid-run → em main self-gate evidence-checklist PASS 0 blocker") → ship Run #283 PASS prod-verified, bundle rotate both. LEARNED: self-gate em main đứng vững lần 2 (sau S57bis) — checklist deterministic (test gate + diff scope + prod smoke 401/404-control) đủ cho PE refinement cross-stack. SURPRISE: die lần 3 trong 2 ngày (S57bis die-0-byte ×2 + S60 mid-run) DÙ promote-tier inherit Fable 5 → model-tier KHÔNG phải nguyên nhân die (nghi resume-kill/harness class) — trend data cho Harness-4. Tag `[s60, die-mid-run-3rd, self-gate, on-behalf]`. - **2026-06-11 (S57bis product gate — KHÔNG DELIVER, die-0-byte ×2, on-behalf em main ghi hộ, H2-proposed):** Cả 2 spawn (email-gate đầu + final gate) chết 0-byte output 0 return (resume-kill class #3, ref `feedback_agent_kill_recovery`) → em main SELF-GATE evidence-checklist: grep authz key-set + role-string vs AppRoles + Mig 49 Up/Down reversible + 240 test + Run #381 + prod smoke 401/404-control. LEARNED: output-file size=0 + im >5 phút = chết, KHÔNG đợi thêm; KHÔNG re-spawn >2 lần trong session có `--resume`. SURPRISE: khác S52 killed-with-partial — lần này 0-byte tuyệt đối (không gì recover được từ return). Tag `[s57bis, die-0-byte-x2, self-gate, on-behalf]`. - **2026-06-10 (S57-resume Harness-4 two-tier adopt gate — PASS-with-fixes, 0 blocker):** Gate trước send-email + commit (governance, không product code). Self-report spawn: `claude-fable-5[1m]` (reviewer = promote-list inherit → direct promote-tier evidence, em main cite được). Independent re-verify ALL GREEN: grep frontmatter = đúng 7 pin `claude-opus-4-8` + 4 `inherit` + 0 `[1m]`-in-frontmatter (2 body-text hits hợp lệ: database-agent.md:46 + README.md:9 MỚI — adap-report "match duy nhất" stale-by-own-edit) + 0 project-pin settings. Evidence track-record **8/8 REAL** vs HANDOFF/STATUS/own-memory (S51 MAJOR · S54 QTV-decoy · S53 Mig46 · S56 H2-4.5/5 + dept-IT-0-user · S57 ×3 controller +5/+5/+5 `[Authorize(Roles="Admin,CatalogManager")]` working-tree). Nấc G-011 đúng mọi chỗ load-bearing (demote = executed-file·pending-restart, 0 overclaim runtime). Fixes: hash PLACEHOLDER trước send (`nac: sent` + "SENT ✓" premature = đúng status-verb class broadcast cảnh báo) · STATUS "(runtime resolve 1M)" thiếu attribution AI_INFRA-s20 · hmw.js:91 log "same-model inherit" stale + :9 "8-agent" vs 9 roles · adap-report "(13)" vs "11" count · invalid-role typo → rơi 'opus' (fail-direction xuống vs H4.5 nghiêng-quality). **Learned:** gate adopt-governance = re-run MỌI grep claim + cross-check evidence vs HANDOFF nguyên văn; n=2 demoted spawn-test double-duty làm inherit-chain proof là HỢP LỆ (registry cached = chạy config cũ) nhưng cần phrase rõ kẻo đọc nhầm thành promote-list spawn-test. Tag [s57, harness-4, two-tier-gate, pre-send-gate, g011]. - **2026-06-09 (S56 pre-golive authz live-curl — PASS, 0 blocker):** Live prod curl 8 new endpoints. **8/8 return 401 unauth**; admin-authed: hrm-configs/vehicles(2)+drivers(2), leave-balances/my(5 lazy), attendances/report+excel(200, 6797B xlsx) all 200; non-admin Drafter correctly 403 on the 2 Admin-only attendance endpoints. **gotcha #44 silent-403 sweep CLEAN:** capability GET /it-tickets/assignable-staff returns HTTP 200 `{canReassign:false,staff:[]}` for non-IT Drafter (NOT swallowed 403) + `{true,[]}` admin — handler returns flag, doesn't throw (`WorkflowAppsFeatures.cs:466`). assign-mutation guard fail-closed (:504). E2E: GET /projects payload has all +4 fields (70/70), CAL01 Investor live. Off_AttendanceReport menu key in admin /menus/me. **1 MINOR (non-block, defense-in-depth):** PUT /it-tickets/{id}/assign checks NotFound BEFORE Admin-OR-IT Forbidden (`WorkflowAppsFeatures.cs:496-508`) → existence-oracle leak; mutation itself fail-closed → post-golive hardening only. Tag [s56, pre-golive-verify, authz-clean, gotcha44-clean, notfound-before-forbidden-minor]. diff --git a/fe-admin/src/App.tsx b/fe-admin/src/App.tsx index 2c17df1..b121394 100644 --- a/fe-admin/src/App.tsx +++ b/fe-admin/src/App.tsx @@ -24,8 +24,6 @@ import { UsersPage } from '@/pages/system/UsersPage' import { PurchaseEvaluationsListPage, PurchaseEvaluationDetailPage } from '@/pages/pe/PurchaseEvaluationsListPage' import { PurchaseEvaluationCreatePage } from '@/pages/pe/PurchaseEvaluationCreatePage' import { PurchaseEvaluationWorkspacePage } from '@/pages/pe/PurchaseEvaluationWorkspacePage' -import { BudgetsListPage, BudgetDetailPage } from '@/pages/budgets/BudgetsListPage' -import { BudgetCreatePage } from '@/pages/budgets/BudgetCreatePage' import { EmployeesListPage } from '@/pages/hrm/EmployeesListPage' import { EmployeeCreatePage } from '@/pages/hrm/EmployeeCreatePage' import { HrmConfigsPage } from '@/pages/hrm/HrmConfigsPage' @@ -80,9 +78,6 @@ function App() { } /> } /> } /> - } /> - } /> - } /> {/* Hồ sơ Nhân sự (Phase 10.1 G-H1 — Mig 34) */} } /> } /> diff --git a/fe-admin/src/components/Layout.tsx b/fe-admin/src/components/Layout.tsx index 0438846..27cf960 100644 --- a/fe-admin/src/components/Layout.tsx +++ b/fe-admin/src/components/Layout.tsx @@ -47,10 +47,6 @@ function resolvePath(key: string): string | null { CatalogWorkItems: '/master/catalogs/work-items', PurchaseEvaluations: '/purchase-evaluations', PeWorkflows: '/system/pe-workflows', - Budgets: '/budgets', - Bg_List: '/budgets', - Bg_Create: '/budgets/new', - Bg_Pending: '/budgets?phase=Pending', // [Phase 10.1 G-H1 S33 2026-05-26] Module Hồ sơ Nhân sự (Mig 34). LESSON // Plan CA Hotfix 1 gotcha #50: PHẢI mirror staticMap khi thêm page mới // — nếu thiếu, MenuLeaf line ~198 `if (!path) return null` → sidebar drop silent. diff --git a/fe-admin/src/components/budgets/BudgetDetailTabs.tsx b/fe-admin/src/components/budgets/BudgetDetailTabs.tsx deleted file mode 100644 index cb3a260..0000000 --- a/fe-admin/src/components/budgets/BudgetDetailTabs.tsx +++ /dev/null @@ -1,491 +0,0 @@ -// Detail content cho 1 ngân sách. Flat render (no tabs): -// Section 1 = Thông tin Header -// Section 2 = Hạng mục (table CRUD inline) -// Approvals + Changelog → moved sang Panel 3 (BudgetWorkflowPanel). -import { useState } from 'react' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { useNavigate } from 'react-router-dom' -import { toast } from 'sonner' -import { Pencil, Plus, Trash2 } from 'lucide-react' -import { Button } from '@/components/ui/Button' -import { Dialog } from '@/components/ui/Dialog' -import { Input } from '@/components/ui/Input' -import { Label } from '@/components/ui/Label' -import { api } from '@/lib/api' -import { getErrorMessage } from '@/lib/apiError' -import { cn } from '@/lib/cn' -import { - BudgetPhase, - BudgetPhaseColor, - BudgetPhaseLabel, - type BudgetChangelog, - type BudgetDetailBundle, - type BudgetDetailBody, - type BudgetDetailRow, -} from '@/types/budget' - -const fmtMoney = (v: number) => v.toLocaleString('vi-VN') - -export function BudgetDetailTabs({ - budget, - onBack, - onDelete, - readOnly = false, -}: { - budget: BudgetDetailBundle - onBack: () => void - onDelete: () => void - /** Menu "Duyệt" — ẩn mọi action thêm/sửa/xóa, chỉ xem + duyệt phase. */ - readOnly?: boolean -}) { - const navigate = useNavigate() - const isDraft = budget.phase === BudgetPhase.DangSoanThao - - return ( -
-
-
-
-

{budget.tenNganSach}

- - {BudgetPhaseLabel[budget.phase]} - - {readOnly && ( - - chế độ duyệt - - )} -
-
- {budget.maNganSach ?? '—'} - · - Năm {budget.namNganSach} - · - {budget.projectName} - {budget.drafterName && (<>·Soạn: {budget.drafterName})} -
-
-
- {isDraft && !readOnly && ( - <> - - - - )} - -
-
- -
-
- -
-
- -
-
-
- ) -} - -function Section({ title, children }: { title: string; children: React.ReactNode }) { - return ( -
-

{title}

- {children} -
- ) -} - -// ===== Exports cho Panel 3 — Approvals history + Changelog ===== - -export function BudgetApprovalsSection({ budget }: { budget: BudgetDetailBundle }) { - return ( -
-

- Lịch sử duyệt ({budget.approvals.length}) -

- -
- ) -} - -export function BudgetHistorySection({ budget }: { budget: BudgetDetailBundle }) { - return ( -
-

Lịch sử thay đổi

- -
- ) -} - -// ===== Section: Thông tin Header ===== -function InfoTab({ budget }: { budget: BudgetDetailBundle }) { - return ( -
- - {budget.maNganSach ?? '—'}} /> - - - - - {fmtMoney(budget.tongNganSach)} đ} /> - {BudgetPhaseLabel[budget.phase]}} /> - {budget.description && ( -
-
Mô tả
-
{budget.description}
-
- )} -
- ) -} - -function Field({ label, value }: { label: string; value: React.ReactNode }) { - return ( -
-
{label}
-
{value}
-
- ) -} - -// ===== Section: Hạng mục table CRUD ===== -function ItemsTab({ budget, readOnly = false }: { budget: BudgetDetailBundle; readOnly?: boolean }) { - const qc = useQueryClient() - const [open, setOpen] = useState(false) - const [editRow, setEditRow] = useState(null) - const isDraft = budget.phase === BudgetPhase.DangSoanThao - const canMutate = !readOnly && isDraft - - const remove = useMutation({ - mutationFn: async (rowId: string) => api.delete(`/budgets/${budget.id}/details/${rowId}`), - onSuccess: () => { - toast.success('Đã xóa hạng mục.') - qc.invalidateQueries({ queryKey: ['budget-detail', budget.id] }) - qc.invalidateQueries({ queryKey: ['budget-list'] }) - }, - onError: e => toast.error(getErrorMessage(e)), - }) - - return ( -
- {canMutate && ( -
- -
- )} - {budget.details.length === 0 ? ( -

- {canMutate ? 'Chưa có hạng mục nào. Thêm để bắt đầu lập ngân sách.' : 'Chưa có hạng mục.'} -

- ) : ( -
- - - - - - - - - - - {canMutate && } - - - - {budget.details.map((d, idx) => ( - - - - - - - - - {canMutate && ( - - )} - - ))} - - - - - - {canMutate && } - - -
#NhómMã / Nội dungĐVTKLĐơn giáThành tiền
{idx + 1} -
{d.groupCode}
-
{d.groupName}
-
- {d.itemCode &&
{d.itemCode}
} -
{d.noiDung}
- {d.ghiChu &&
{d.ghiChu}
} -
{d.donViTinh ?? '—'}{fmtMoney(d.khoiLuong)}{fmtMoney(d.donGia)} - {fmtMoney(d.thanhTien)} - -
- - -
-
Tổng: - {fmtMoney(budget.tongNganSach)} -
-
- )} - - {open && ( - setOpen(false)} - /> - )} - {editRow && ( - setEditRow(null)} - /> - )} -
- ) -} - -// ===== Dialog: Thêm / Sửa hạng mục ===== -function DetailRowDialog({ - budgetId, - existing, - onClose, -}: { - budgetId: string - existing?: BudgetDetailRow - onClose: () => void -}) { - const qc = useQueryClient() - const [form, setForm] = useState({ - groupCode: existing?.groupCode ?? '', - groupName: existing?.groupName ?? '', - itemCode: existing?.itemCode ?? null, - noiDung: existing?.noiDung ?? '', - donViTinh: existing?.donViTinh ?? null, - khoiLuong: existing?.khoiLuong ?? 0, - donGia: existing?.donGia ?? 0, - thanhTien: existing?.thanhTien ?? 0, - ghiChu: existing?.ghiChu ?? null, - }) - - // Auto-compute thành tiền khi đổi KL/đơn giá (UX nicety) - function setQty(v: number) { - const next = { ...form, khoiLuong: v, thanhTien: v * form.donGia } - setForm(next) - } - function setPrice(v: number) { - const next = { ...form, donGia: v, thanhTien: form.khoiLuong * v } - setForm(next) - } - - const save = useMutation({ - mutationFn: async () => { - const payload = { - groupCode: form.groupCode, - groupName: form.groupName, - itemCode: form.itemCode || null, - noiDung: form.noiDung, - donViTinh: form.donViTinh || null, - khoiLuong: form.khoiLuong, - donGia: form.donGia, - thanhTien: form.thanhTien, - ghiChu: form.ghiChu || null, - } - if (existing) { - return api.put(`/budgets/${budgetId}/details/${existing.id}`, payload) - } - return api.post(`/budgets/${budgetId}/details`, payload) - }, - onSuccess: () => { - toast.success(existing ? 'Đã cập nhật hạng mục.' : 'Đã thêm hạng mục.') - qc.invalidateQueries({ queryKey: ['budget-detail', budgetId] }) - qc.invalidateQueries({ queryKey: ['budget-list'] }) - onClose() - }, - onError: e => toast.error(getErrorMessage(e)), - }) - - return ( - - - - } - > -
-
-
- - setForm({ ...form, groupCode: e.target.value })} - placeholder="A.I" - /> -
-
- - setForm({ ...form, groupName: e.target.value })} - placeholder="Vật tư xây dựng" - /> -
-
-
- - setForm({ ...form, itemCode: e.target.value || null })} - /> -
-
- - setForm({ ...form, noiDung: e.target.value })} - placeholder="Bê tông M250" - /> -
-
-
- - setForm({ ...form, donViTinh: e.target.value || null })} - placeholder="m³" - /> -
-
- - setQty(Number(e.target.value))} - /> -
-
- - setPrice(Number(e.target.value))} - /> -
-
- - setForm({ ...form, thanhTien: Number(e.target.value) })} - /> -
-
-
- - setForm({ ...form, ghiChu: e.target.value || null })} - /> -
-
-
- ) -} - -// ===== Sub: Approvals list ===== -function ApprovalsList({ budget }: { budget: BudgetDetailBundle }) { - if (budget.approvals.length === 0) - return

Chưa có bước duyệt nào.

- return ( -
    - {budget.approvals.map(a => ( -
  1. -
    -
    - - {BudgetPhaseLabel[a.fromPhase]} - - - - {BudgetPhaseLabel[a.toPhase]} - -
    - {new Date(a.approvedAt).toLocaleString('vi-VN')} -
    -
    - {a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`} -
    -
  2. - ))} -
- ) -} - -// ===== Sub: Changelog list ===== -function HistoryList({ budget }: { budget: BudgetDetailBundle }) { - const logs = useQuery({ - queryKey: ['budget-changelog', budget.id], - queryFn: async () => (await api.get(`/budgets/${budget.id}/changelogs`)).data, - }) - if (logs.isLoading) return

Đang tải…

- if (!logs.data || logs.data.length === 0) - return

Chưa có lịch sử.

- return ( -
    - {logs.data.map(l => ( -
  1. -
    - {l.userName ?? 'Hệ thống'} - {new Date(l.createdAt).toLocaleString('vi-VN')} -
    -
    {l.summary}
    - {l.contextNote &&
    {l.contextNote}
    } -
  2. - ))} -
- ) -} diff --git a/fe-admin/src/components/budgets/BudgetWorkflowPanel.tsx b/fe-admin/src/components/budgets/BudgetWorkflowPanel.tsx deleted file mode 100644 index 9b33ec3..0000000 --- a/fe-admin/src/components/budgets/BudgetWorkflowPanel.tsx +++ /dev/null @@ -1,225 +0,0 @@ -// Panel 3 — workflow timeline + transition buttons + approval history + changelog. -// Pulls nextPhases từ BE bundle (single source of truth). -import { useState } from 'react' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { toast } from 'sonner' -import { Dialog } from '@/components/ui/Dialog' -import { Button } from '@/components/ui/Button' -import { Label } from '@/components/ui/Label' -import { Textarea } from '@/components/ui/Textarea' -import { api } from '@/lib/api' -import { getErrorMessage } from '@/lib/apiError' -import { cn } from '@/lib/cn' -import { - ApprovalDecision, - ApprovalStage, - BudgetPhase, - BudgetPhaseColor, - BudgetPhaseLabel, - type BudgetDepartmentApproval, - type BudgetDetailBundle, -} from '@/types/budget' -import { BudgetApprovalsSection, BudgetHistorySection } from './BudgetDetailTabs' - -export function BudgetWorkflowPanel({ budget }: { budget: BudgetDetailBundle }) { - const [target, setTarget] = useState(null) - const [comment, setComment] = useState('') - const qc = useQueryClient() - - // 2-stage dept approvals (Migration 16) — fetch riêng để FE render timeline. - const { data: deptApprovals = [] } = useQuery({ - queryKey: ['budget-dept-approvals', budget.id], - queryFn: async () => (await api.get(`/budgets/${budget.id}/department-approvals`)).data, - }) - - const transition = useMutation({ - mutationFn: async () => - api.post(`/budgets/${budget.id}/transitions`, { - targetPhase: target, - decision: target === BudgetPhase.TuChoi ? ApprovalDecision.Reject : ApprovalDecision.Approve, - comment: comment || null, - }), - onSuccess: () => { - toast.success('Đã chuyển phase.') - qc.invalidateQueries({ queryKey: ['budget-detail', budget.id] }) - qc.invalidateQueries({ queryKey: ['budget-list'] }) - qc.invalidateQueries({ queryKey: ['budget-changelog', budget.id] }) - qc.invalidateQueries({ queryKey: ['budget-dept-approvals', budget.id] }) - setTarget(null) - setComment('') - }, - onError: e => toast.error(getErrorMessage(e)), - }) - - const next = budget.workflow.nextPhases - - return ( -
-
-

Quy trình

-

{budget.workflow.policyDescription}

-
- -
    - {budget.workflow.activePhases - .filter(p => p !== BudgetPhase.TuChoi) - .map(p => { - const isCurrent = budget.phase === p - const isPast = isPastPhase(budget.phase, p, budget.workflow.activePhases) - return ( -
  1. -
    - {p} - {BudgetPhaseLabel[p]} - {isCurrent && ● hiện tại} - {isPast && } -
    -
  2. - ) - })} -
- - {next.length > 0 && ( -
- -
- {next.map(p => ( - - ))} -
-
- )} - - {target !== null && ( - setTarget(null)} - title={`Chuyển → ${BudgetPhaseLabel[target]}`} - footer={<> - - - } - > - -