[CLAUDE] FE-Admin+Docs: Contract workflow N-stage Designer mirror PE + Docs (Chunk F)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m3s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m3s
FE Admin WorkflowsPage Designer extend mirror PeWorkflowsPage: - Type InnerStepDto + extend StepDto +innerSteps - Type EditInnerStep + extend EditStep +innerSteps - copyFromDefinition include innerSteps map - Default new step +innerSteps:[] - departmentsList useQuery - Save mutation payload include innerSteps Order asc - UI sub-section "Cấp duyệt nhỏ trong phòng" drag-list per step card với Phòng × Cấp + required checkbox + button "+ Thêm cấp duyệt" emerald - Empty state hint fallback 2-cấp legacy KHÔNG đụng fe-user — WorkflowsPage admin-only. Reuse PositionLevel const + Label maps từ Session 12 types/users.ts. Docs: - STATUS.md Last updated + Phase summary (19→20 mig, 89→95 test, 56→57 bảng) + 1 row Recently Done Session 13 (KEEP narrative cũ) - HANDOFF.md TL;DR Session 13 prepend + 7 cảnh báo Session 14+ - migration-todos.md Phase 9 + Session 13 block 5 chunk - Session log NEW `2026-05-07-2400-n-stage-contract-mirror.md` đầy đủ rationale + per-chunk + bug log Defer cron audit 2026-06-01: schema-diagram §17 Mig 20, skill ef-core-migration row, skill contract-workflow N-stage cross-ref. 🎉 SESSION 13 COMPLETE: Mirror N-stage Contract module (Mig 20). 5 commit per-chunk + skip Chunk E auto-bind. Total 95 test pass. Backward compat 100% với 2-stage Mig 16 legacy. Pending Task 4: Wire BE TraLai PE transition + Task 2: Sample data seed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,50 @@
|
|||||||
# HANDOFF — Brief 5 phút cho session tiếp theo
|
# HANDOFF — Brief 5 phút cho session tiếp theo
|
||||||
|
|
||||||
**Last updated:** 2026-05-07 (Session 12 — **N-stage workflow approval Phòng × PositionLevel cấu hình động — Mig 18+19. 6 commit per-chunk: Domain → App CQRS → Service logic → Tests +6 → API → FE Designer + UsersPage. 89 test pass. PE-only first.**)
|
**Last updated:** 2026-05-07 (Session 13 — **Mirror N-stage Contract (Mig 20, 5 commit per-chunk + skip Chunk E auto-bind). Domain → App → Service → Tests +6 → FE Designer. 95 test pass. Budget defer cần versioned WF.**)
|
||||||
|
|
||||||
|
## TL;DR Session 13 (07/05 — Mirror N-stage workflow sang Contract)
|
||||||
|
|
||||||
|
User chỉ thị mirror N-stage từ PE (Mig 18+19) sang Contract. Budget defer (cần migration `AddBudgetVersionedWorkflow` trước — hardcoded `BudgetPolicy.Default` chưa có WorkflowDefinition entity).
|
||||||
|
|
||||||
|
**5 chunk per-commit** (Chunk E skipped — WorkflowsController auto-bind record qua [FromBody]):
|
||||||
|
|
||||||
|
- **Chunk A (`951ffa3`)** Domain `WorkflowStepInnerStep` + nav WorkflowStep.InnerSteps + ALTER ContractDeptApproval.InnerStepId + EF config + **Migration 20** GỘP 1 (CREATE TABLE + ALTER + DropIndex old + Recreate filtered legacy/N-stage)
|
||||||
|
- **Chunk B (`04cf2a0`)** Application CQRS DTO/Input/Validator/Handler mirror PE Chunk B (default null backward compat)
|
||||||
|
- **Chunk C (`e247b67`)** ContractWorkflowService refactor — load InnerSteps eager + reject clear N-stage rows + dept block split hasInnerSteps→N-stage / else→legacy 2-stage
|
||||||
|
- **Chunk D (`7c0772a`)** ContractNStageApprovalTests 6 test mirror PE + helper SeedWorkflowDefinitionAsync 2 step adjacent + FakeChangelogService + FakeContractCodeGenerator stubs. **89→95 test pass**
|
||||||
|
- **Chunk E SKIP** — auto-bind no code change
|
||||||
|
- **Chunk F (current)** FE WorkflowsPage Designer extend InnerSteps sub-section mirror PeWorkflowsPage + Docs
|
||||||
|
|
||||||
|
**Verify:** dotnet build pass + dotnet ef database update Mig 20 LocalDB applied + dotnet test 95 pass + npm build fe-admin pass.
|
||||||
|
|
||||||
|
**Cumulative sau Session 13:**
|
||||||
|
|
||||||
|
| | Trước S13 | Sau S13 |
|
||||||
|
|---|---:|---:|
|
||||||
|
| BE LOC | ~15300 | ~15700 (+400) |
|
||||||
|
| Migrations | 19 | **20** (+Mig 20) |
|
||||||
|
| DB tables | 56 | **57** (+1 WorkflowStepInnerSteps) |
|
||||||
|
| DB columns mới | — | +1 (ContractDeptApproval.InnerStepId) |
|
||||||
|
| API endpoints | ~134 | ~134 (no change — auto-bind) |
|
||||||
|
| FE pages | 32 | 32 (extend existing WorkflowsPage) |
|
||||||
|
| Tests | 89 | **95** (+6 Contract N-stage) |
|
||||||
|
| Commits | (after S12) | **+5** (A→F per-chunk, E skipped) |
|
||||||
|
|
||||||
|
## ⚠️ CẢNH BÁO Session 14+
|
||||||
|
|
||||||
|
1. **Budget N-stage defer** — Budget chưa có versioned WorkflowDefinition entity (hardcoded `BudgetPolicy.Default`). Để mirror N-stage Budget cần migration `AddBudgetVersionedWorkflow` trước (4 bảng `BudgetWorkflowDefinitions/Steps/StepApprovers/InnerSteps` + `Budget.WorkflowDefinitionId?`). Defer cho user quyết riêng — feature mở rộng module Budget lớn.
|
||||||
|
|
||||||
|
2. **PE-only deploy first** — Contract N-stage giờ đã có nhưng PE đã được test trước. UAT cả 2 module song song để verify pattern reusable.
|
||||||
|
|
||||||
|
3. **Sample data N-stage** — pending (Task 2). Khi user yêu cầu sẽ seed PositionLevel cho 30 demo user + 1 N-stage workflow definition active cho DuyetNcc + 1 cho ContractType (ví dụ HopDongThauPhu).
|
||||||
|
|
||||||
|
4. **TraLai BE wire** — pending (Task 4). Phase TraLai = 98 enum đã có ở Session S11+++++++ FE side, nhưng BE workflow service chưa wire button "Trả lại" cho approver. Khi user yêu cầu sẽ thêm decision `SendBack` (or reuse Reject với target=TraLai) + Service logic + FE button trong Workflow Panel.
|
||||||
|
|
||||||
|
5. **schema-diagram §17 Mig 20** chưa update — defer cron audit 2026-06-01.
|
||||||
|
|
||||||
|
6. **Skill ef-core-migration Mig 20 row** chưa add — defer cron audit.
|
||||||
|
|
||||||
|
7. **Skill contract-workflow N-stage cross-ref** chưa add — defer cron audit.
|
||||||
|
|
||||||
## TL;DR Session 12 (07/05 — N-stage workflow approval per phase × dept × cấp)
|
## TL;DR Session 12 (07/05 — N-stage workflow approval per phase × dept × cấp)
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
> **Update rule:** trước khi bắt đầu 1 task → ghi row vào `🔥 In Progress`. Xong → chuyển sang `✅ Recently Done`.
|
> **Update rule:** trước khi bắt đầu 1 task → ghi row vào `🔥 In Progress`. Xong → chuyển sang `✅ Recently Done`.
|
||||||
|
|
||||||
**Last updated:** 2026-05-07 (Session 12 — **N-stage workflow approval Phòng × PositionLevel cấu hình động (Mig 18+19): Domain enum + entity + EF + Migration → App CQRS DTO + UpdateUserPositionLevel → Service logic N-stage + legacy 2-stage fallback + smart reject reset → 6 test mới → API endpoint → FE Designer InnerSteps sub-section + UsersPage cột "Cấp". 6 commit per-chunk.**)
|
**Last updated:** 2026-05-07 (Session 13 — **Mirror N-stage workflow sang Contract (Mig 20): Domain entity WorkflowStepInnerStep + ALTER ContractDeptApproval.InnerStepId + EF + Migration gộp 1 → App CQRS extend → ContractWorkflowService N-stage + legacy 2-stage fallback + smart reject reset → 6 test mới → FE WorkflowsPage Designer InnerSteps. 5 commit per-chunk (E API auto-bind skipped). PE-only first → giờ áp Contract.**)
|
||||||
|
|
||||||
## 📍 Phase hiện tại: **Phase 9 active — UAT** — **56 DB tables, 19 migrations, ~134 API endpoints, 32 FE pages. 89 unit test pass** (54 Domain + 35 Infra). 41 gotcha. 30 demo user. 6 skill. **5 PE display status** (Bản nháp / Đã gửi duyệt / Trả lại / Đã duyệt / Từ chối). **9 PE phase enum** (+TraLai = 98). **N-stage workflow** Phòng × PositionLevel (NV/PP/TP) cấu hình động per WorkflowStep cha — Mig 18+19.
|
## 📍 Phase hiện tại: **Phase 9 active — UAT** — **57 DB tables, 20 migrations, ~134 API endpoints, 32 FE pages. 95 unit test pass** (54 Domain + 41 Infra). 41 gotcha. 30 demo user. 6 skill. **5 PE display status** (Bản nháp / Đã gửi duyệt / Trả lại / Đã duyệt / Từ chối). **9 PE phase enum** (+TraLai = 98). **N-stage workflow** Phòng × PositionLevel (NV/PP/TP) cấu hình động per WorkflowStep cha — **Mig 18+19 (PE) + Mig 20 (Contract)**. Budget defer (cần versioned WF migration trước).
|
||||||
|
|
||||||
### 🌐 Production URLs
|
### 🌐 Production URLs
|
||||||
|
|
||||||
@ -61,6 +61,7 @@
|
|||||||
|
|
||||||
| Ngày | Ai | Task | Commit |
|
| Ngày | Ai | Task | Commit |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
| 2026-05-07 | Claude | **🎯 SESSION 13 — Mirror N-stage workflow sang Contract (Mig 20, 5 commit per-chunk + skip Chunk E API)** — User chỉ thị mirror N-stage từ PE sang Contract. Budget defer (cần versioned WF migration trước, hardcoded BudgetPolicy hiện tại chưa có WorkflowDefinition). 5 chunk per-commit (build + ef + test pass mỗi chunk): **Chunk A (`951ffa3`)** Domain entity `WorkflowStepInnerStep` (Domain/Contracts/) + nav WorkflowStep.InnerSteps + ALTER ContractDepartmentApproval.InnerStepId Guid? + EF config FK Cascade Step / Restrict Dept+InnerStep + **Migration 20** `AddContractWorkflowInnerStepsAndAlterDeptApprovalUnique` GỘP 1 (CREATE TABLE WorkflowStepInnerSteps + ALTER InnerStepId + DropIndex old + Recreate filtered legacy `WHERE InnerStepId IS NULL` + new filtered N-stage `WHERE InnerStepId IS NOT NULL` + 3 IX + 3 FK). **Chunk B (`04cf2a0`)** Application CQRS DTO — `WorkflowStepInnerStepDto` + extend `WorkflowStepDto` + GetWorkflowAdminOverview Include InnerSteps + DeptNames map + `CreateWorkflowStepInnerStepInput` + `CreateWorkflowStepInput` extend (default null backward compat) + Validator child rules + Handler atomic batch insert. **Chunk C (`e247b67`)** ContractWorkflowService refactor mirror PE — load definition InnerSteps eager, reject branch clear N-stage rows tại fromPhase, dept approval block split hasInnerSteps→N-stage logic / else→legacy 2-stage. N-stage flow giống PE: yêu cầu actor có DeptId+PositionLevel, match firstPending Order asc + (exact level OR canBypass + level≥), exact upsert 1 row InnerStepId, bypass batch upsert NV+PP+TP cùng dept ≤ actor (audit IsBypassed cho cấp dưới), recheck stillPending → BLOCK + log "duyệt cấp X (còn Y pending)". **Chunk D (`7c0772a`)** Tests 6 N-stage Contract mirror PE pattern + helper SeedWorkflowDefinitionAsync 2 step adjacent (DangGopY + DangDamPhan) + SeedContractAsync với Project + Supplier seed + FakeChangelogService + FakeContractCodeGenerator stubs. Bug fix: legacy fallback test ban đầu fail (Standard policy DangGopY → DangDamPhan chỉ cho [Drafter, DeptManager], không Procurement) → switched phase pair sang DangKiemTraCCM → DangTrinhKy + role CostControl khớp. Total 89 → **95 test pass**. **Chunk E SKIP** — WorkflowsController auto-bind `[FromBody] CreateWorkflowDefinitionCommand` record qua JSON, no code change cần. **Chunk F (current)** FE-Admin types/users.ts đã có PositionLevel const từ Session 12 reuse. WorkflowsPage Designer extend mirror PeWorkflowsPage Chunk F: InnerStepDto + EditInnerStep types + copyFromDefinition include + departmentsList query + sub-section "Cấp duyệt nhỏ trong phòng" drag-list { Phòng × Cấp + required } + button "+ Thêm cấp duyệt" emerald + payload include Order asc. Empty state hint fallback 2-cấp legacy. KHÔNG đụng fe-user (admin-only). Docs/Skill update. **Backward compat 100%**: workflow Contract no InnerSteps → fallback legacy 2-stage Mig 16. Data legacy InnerStepId=null vẫn enforce unique cũ qua filtered index. Defer Budget mirror cho session sau (cần migration `AddBudgetVersionedWorkflow` trước). | `951ffa3` (A) · `04cf2a0` (B) · `e247b67` (C) · `7c0772a` (D) · (current F) |
|
||||||
| 2026-05-07 | Claude | **🎯 SESSION 12 — N-stage workflow approval Phòng × PositionLevel cấu hình động (PE-only first, 6 commit per-chunk + Mig 18+19)** — User yêu cầu mở rộng từ 2-stage Mig 16 (NV.Review/TPB.Confirm) sang N-stage cấu hình động: mỗi WorkflowStep cha (= 1 phase) có thể cấu hình chuỗi InnerSteps con theo Department × PositionLevel với Order sequential. Spec defaults chốt 6 câu (PositionLevel int 1=NV/2=PP/3=TP, sequential pure, bypass cùng dept TP skip NV+PP, smart reject reset N-stage rows về DangSoanThao, PE-only first, designer 1 sub-section InnerSteps). 6 chunk per-commit (build + ef + test pass mỗi chunk per `feedback_per_chunk_commit.md`): **Chunk A (`13ab533`)** Domain enum `PositionLevel` (NV/PP/TP) + entity `PurchaseEvaluationWorkflowStepInnerStep` + ALTER User.PositionLevel int? + ALTER PEDeptApprovals.InnerStepId Guid? + EF config + **Migration 18** `AddPeWorkflowInnerStepsAndPositionLevel` (1 CREATE TABLE + 2 ALTER + 3 index + FK Cascade Step / Restrict Dept/InnerStep). 3-file rule. **Chunk B (`0e56bd0`)** Application CQRS DTO — `PeWorkflowStepInnerStepDto` + extend `PeWorkflowStepDto` + `CreatePeWorkflowStepInnerStepInput` (default null backward compat existing PeWorkflowAdminTests) + Validator child rules + Handler atomic batch insert + UserDto +PositionLevel field + `SetUserPositionLevelCommand` mirror SetBypassReview. **Chunk C (`0c62e24`)** Service N-stage logic — **Migration 19** `AlterPeDeptApprovalsUniqueFilteredForInnerSteps` (filtered unique: legacy `WHERE InnerStepId IS NULL` + N-stage `WHERE InnerStepId IS NOT NULL`) cho phép multi-row cùng dept khác inner step. PurchaseEvaluationWorkflowService refactor: load definition InnerSteps eager + reject branch clear N-stage rows + dept block split hasInnerSteps→N-stage logic / else→legacy 2-stage. N-stage flow: yêu cầu actor có DeptId+PositionLevel, match firstPending (Order asc IsRequired) same dept + (exact level OR canBypass + level≥), exact match upsert 1 row InnerStepId, bypass batch upsert NV+PP+TP cùng dept ≤ actor level (audit IsBypassed cho cấp dưới skip), recheck stillPending → BLOCK + log "duyệt cấp X (còn Y pending)" / all done → fall through phase transition. Backward compat: workflow no InnerSteps fallback legacy + InnerStepId=null filter unique cũ vẫn enforce. **Chunk D (`3d76c6b`)** Tests N-stage 6 test mới (NV first blocks / 3-level sequential pass / TP bypass skips / wrong dept throws 403 / reject clears rows / legacy fallback no inner) + IdentityFixture extend `+positionLevel` arg + helper SeedWorkflowDefinitionAsync 2 step adjacent (ChoPurchasing+ChoCCM) cho FromDefinition build transition policy guard pass. Total 83→**89 test pass**. **Chunk E (`83ffabd`)** API `PATCH /users/{id}/position-level` mirror SetBypassReview pattern + body `{positionLevel:int?}` Authorize Users.Update. **Chunk F (current)** FE-Admin types/users.ts +positionLevel field + PositionLevel const + Label/Short maps. PeWorkflowsPage Designer extend: InnerStepDto type + EditInnerStep type + copyFromDefinition include + departmentsList query + sub-section "Cấp duyệt nhỏ trong phòng" per step card với drag-drop list { Phòng × Cấp + required checkbox } + button "+ Thêm cấp duyệt" (xanh emerald) + payload include innerSteps Order asc. UsersPage column "Cấp" badge NV/PP/TP emerald + action button cycle null→1→2→3→null call positionLevelMut PATCH. KHÔNG đụng fe-user (admin-only feature). Docs/Skill update. PE-only first. Backward compat 100%: workflow no InnerSteps + data legacy 2-stage rows không phá. | `13ab533` (A) · `0e56bd0` (B) · `0c62e24` (C) · `3d76c6b` (D) · `83ffabd` (E) · (current F) |
|
| 2026-05-07 | Claude | **🎯 SESSION 12 — N-stage workflow approval Phòng × PositionLevel cấu hình động (PE-only first, 6 commit per-chunk + Mig 18+19)** — User yêu cầu mở rộng từ 2-stage Mig 16 (NV.Review/TPB.Confirm) sang N-stage cấu hình động: mỗi WorkflowStep cha (= 1 phase) có thể cấu hình chuỗi InnerSteps con theo Department × PositionLevel với Order sequential. Spec defaults chốt 6 câu (PositionLevel int 1=NV/2=PP/3=TP, sequential pure, bypass cùng dept TP skip NV+PP, smart reject reset N-stage rows về DangSoanThao, PE-only first, designer 1 sub-section InnerSteps). 6 chunk per-commit (build + ef + test pass mỗi chunk per `feedback_per_chunk_commit.md`): **Chunk A (`13ab533`)** Domain enum `PositionLevel` (NV/PP/TP) + entity `PurchaseEvaluationWorkflowStepInnerStep` + ALTER User.PositionLevel int? + ALTER PEDeptApprovals.InnerStepId Guid? + EF config + **Migration 18** `AddPeWorkflowInnerStepsAndPositionLevel` (1 CREATE TABLE + 2 ALTER + 3 index + FK Cascade Step / Restrict Dept/InnerStep). 3-file rule. **Chunk B (`0e56bd0`)** Application CQRS DTO — `PeWorkflowStepInnerStepDto` + extend `PeWorkflowStepDto` + `CreatePeWorkflowStepInnerStepInput` (default null backward compat existing PeWorkflowAdminTests) + Validator child rules + Handler atomic batch insert + UserDto +PositionLevel field + `SetUserPositionLevelCommand` mirror SetBypassReview. **Chunk C (`0c62e24`)** Service N-stage logic — **Migration 19** `AlterPeDeptApprovalsUniqueFilteredForInnerSteps` (filtered unique: legacy `WHERE InnerStepId IS NULL` + N-stage `WHERE InnerStepId IS NOT NULL`) cho phép multi-row cùng dept khác inner step. PurchaseEvaluationWorkflowService refactor: load definition InnerSteps eager + reject branch clear N-stage rows + dept block split hasInnerSteps→N-stage logic / else→legacy 2-stage. N-stage flow: yêu cầu actor có DeptId+PositionLevel, match firstPending (Order asc IsRequired) same dept + (exact level OR canBypass + level≥), exact match upsert 1 row InnerStepId, bypass batch upsert NV+PP+TP cùng dept ≤ actor level (audit IsBypassed cho cấp dưới skip), recheck stillPending → BLOCK + log "duyệt cấp X (còn Y pending)" / all done → fall through phase transition. Backward compat: workflow no InnerSteps fallback legacy + InnerStepId=null filter unique cũ vẫn enforce. **Chunk D (`3d76c6b`)** Tests N-stage 6 test mới (NV first blocks / 3-level sequential pass / TP bypass skips / wrong dept throws 403 / reject clears rows / legacy fallback no inner) + IdentityFixture extend `+positionLevel` arg + helper SeedWorkflowDefinitionAsync 2 step adjacent (ChoPurchasing+ChoCCM) cho FromDefinition build transition policy guard pass. Total 83→**89 test pass**. **Chunk E (`83ffabd`)** API `PATCH /users/{id}/position-level` mirror SetBypassReview pattern + body `{positionLevel:int?}` Authorize Users.Update. **Chunk F (current)** FE-Admin types/users.ts +positionLevel field + PositionLevel const + Label/Short maps. PeWorkflowsPage Designer extend: InnerStepDto type + EditInnerStep type + copyFromDefinition include + departmentsList query + sub-section "Cấp duyệt nhỏ trong phòng" per step card với drag-drop list { Phòng × Cấp + required checkbox } + button "+ Thêm cấp duyệt" (xanh emerald) + payload include innerSteps Order asc. UsersPage column "Cấp" badge NV/PP/TP emerald + action button cycle null→1→2→3→null call positionLevelMut PATCH. KHÔNG đụng fe-user (admin-only feature). Docs/Skill update. PE-only first. Backward compat 100%: workflow no InnerSteps + data legacy 2-stage rows không phá. | `13ab533` (A) · `0e56bd0` (B) · `0c62e24` (C) · `3d76c6b` (D) · `83ffabd` (E) · (current F) |
|
||||||
| 2026-05-08 00:30 | Claude | **🎯 SESSION PHASE 2 WRAP-UP — B12-B14 PE detail polish iterate (3 commit FE-only sau wrap-up `6e7a6db`)** — User UAT iteration tiếp, áp rule strict verify khi rename/remove (lesson hotfix CI). 3 batch nhỏ: **B12 (`378c993`)** "Lưu" no-close (chỉ toast + invalidate, KHÔNG đóng workspace) + nút "Xóa phiếu" red bottom CHỈ Bản nháp (soft-delete `IsDeleted=true` qua AuditableEntity, không xóa hoàn toàn DB) + bỏ header bar workspace mode "Sửa header"/"Xóa"/"Đóng" (chuyển hết xuống bottom action bar) + Section 4 column header dùng `s.supplierName` thay `displayName` (NCC master) + Section 3 row chặn xóa NCC khi đã có quotes (`hasQuotes` computed) + tooltip "xóa báo giá trước". **B13 (`e320027`)** InfoTab `useEffect` watch `[autoEdit, canEdit, ev.id, ...]` → re-trigger edit mode khi pencil click phiếu khác (fix useState mount-time only) + sync values từ ev mới (tránh stale state) + Pencil "sáng lên" active state khi `editingRowId === p.id` (bg-brand-100 + text-brand-700 + ring-brand-300 + shadow-sm + tooltip cập nhật) + wire `editingRowId={autoEditHeader ? selectedId : null}` từ Workspace → PeListPanel. **B14 (`d2306b8`)** QuoteDialog bỏ checkbox "Chọn NCC này cho hạng mục" (consolidate winner ở Section 2.a NccSelectorRow, isSelected vẫn gửi BE giữ trạng thái cũ) + winner column Section 4 matrix highlight emerald (header `bg-emerald-50` + `✓ ` prefix + cells `bg-emerald-50 font-semibold` cho ENTIRE column, không chỉ cell có quote) + loading overlay full-screen QuoteDialog (`bg-white/70 backdrop-blur-sm` + spinner ring brand-600 + text "Đang lưu báo giá…"/"Đang xóa…") + NccSelectorRow inline spinner "Đang chọn NCC + sync cột giá Section 4…" + disable Hủy/Xóa/Lưu buttons khi `isSaving`. Verify: `npm run build` × 2 app pass mỗi commit · `dotnet test` 83 pass (KHÔNG regression). | `378c993` (B12) · `e320027` (B13) · `d2306b8` (B14) |
|
| 2026-05-08 00:30 | Claude | **🎯 SESSION PHASE 2 WRAP-UP — B12-B14 PE detail polish iterate (3 commit FE-only sau wrap-up `6e7a6db`)** — User UAT iteration tiếp, áp rule strict verify khi rename/remove (lesson hotfix CI). 3 batch nhỏ: **B12 (`378c993`)** "Lưu" no-close (chỉ toast + invalidate, KHÔNG đóng workspace) + nút "Xóa phiếu" red bottom CHỈ Bản nháp (soft-delete `IsDeleted=true` qua AuditableEntity, không xóa hoàn toàn DB) + bỏ header bar workspace mode "Sửa header"/"Xóa"/"Đóng" (chuyển hết xuống bottom action bar) + Section 4 column header dùng `s.supplierName` thay `displayName` (NCC master) + Section 3 row chặn xóa NCC khi đã có quotes (`hasQuotes` computed) + tooltip "xóa báo giá trước". **B13 (`e320027`)** InfoTab `useEffect` watch `[autoEdit, canEdit, ev.id, ...]` → re-trigger edit mode khi pencil click phiếu khác (fix useState mount-time only) + sync values từ ev mới (tránh stale state) + Pencil "sáng lên" active state khi `editingRowId === p.id` (bg-brand-100 + text-brand-700 + ring-brand-300 + shadow-sm + tooltip cập nhật) + wire `editingRowId={autoEditHeader ? selectedId : null}` từ Workspace → PeListPanel. **B14 (`d2306b8`)** QuoteDialog bỏ checkbox "Chọn NCC này cho hạng mục" (consolidate winner ở Section 2.a NccSelectorRow, isSelected vẫn gửi BE giữ trạng thái cũ) + winner column Section 4 matrix highlight emerald (header `bg-emerald-50` + `✓ ` prefix + cells `bg-emerald-50 font-semibold` cho ENTIRE column, không chỉ cell có quote) + loading overlay full-screen QuoteDialog (`bg-white/70 backdrop-blur-sm` + spinner ring brand-600 + text "Đang lưu báo giá…"/"Đang xóa…") + NccSelectorRow inline spinner "Đang chọn NCC + sync cột giá Section 4…" + disable Hủy/Xóa/Lưu buttons khi `isSaving`. Verify: `npm run build` × 2 app pass mỗi commit · `dotnet test` 83 pass (KHÔNG regression). | `378c993` (B12) · `e320027` (B13) · `d2306b8` (B14) |
|
||||||
| 2026-05-07 | Claude | **🎯 SESSION WRAP-UP S10-11+++++++ — PE Workspace UX overhaul đầy đủ (23 commit / ~3500 LOC FE + Mig 17 BE)** — User UAT live mode iterate liên tục, áp rule `feedback_uat_skip_verify` (skip dotnet test sau mỗi chunk, push ngay). 7 batch chính: **B1 (S10) PE Thao tác 2-panel workspace** — leaf `Pe_*_Create` từ page Create header riêng → workspace 2-panel `[320px_1fr]` mirror HĐ Thầu phụ; PeListPanel pure picker + sticky "+ Thêm mới"; PeDetailTabs `mode='workspace'` ẩn Workflow/Approvals/History + Section 5 disabled "nhập khi duyệt" (4 commit). **B2 (S11) Migration 17** `AddManualBudgetFieldsToPeAndContract` — 4 ALTER (PE + HĐ × `BudgetManualName` nvarchar(200) + `BudgetManualAmount` decimal(18,2)) cho fallback "user nhập tay khi không link Budget entity approved". Domain + EF config + App CQRS Create/Update + DTO + Validator + carry-forward `CreateContractFromEvaluation`. FE toggle "Nhập tay" trong PeHeaderForm + ContractCreatePage NewForm/EditForm × 2 app (5 commit). **B3 (S11+) BudgetFieldRow inline editor** — Section 2 "b. Ngân sách" thay FormRow tĩnh → editable component (toggle + Select OR 2 input + Save dirty + Hủy). canEdit cho cả 3 view (Workspace/Danh sách/Duyệt mode), readOnly chỉ display (3 commit). **B4 (S11++) InfoTab inline edit + PeListPanel pencil hover** — Section 1 "✎ Sửa" button flip display↔inputs (Tên/Địa điểm/Mô tả/Payment editable, Dự án locked). PeListPanel thêm pencil icon group-hover absolute right + URL `?editHeader=1` chain → `autoEditHeader` prop trigger mount-time edit (3 commit). **B5 (S11+++) Workspace "new" sectioned create view** — `PeWorkspaceCreateView.tsx` ~230 LOC layout 5 sections giống PeDetailTabs visual. S1 + S2.b editable, S3-5 LockedHint "Lưu phiếu trước". POST trigger create. Replace `PeHeaderForm` trong workspace mode='new' (1 commit). **B6 (S11++++) Danh sách disable toàn bộ interactions** — PurchaseEvaluationsListPage `readOnly={true}` hardcoded cho PeDetailTabs + `readOnly={!pendingMe}` cho PeWorkflowPanel (List view → ẩn Chuyển tiếp + show hint "Vào menu Duyệt"; Pending vẫn approve được) (2 commit). **B7 (S11+++++) Lock Loại quy trình + payment preset** — workspace `<Select>` Loại quy trình → `<Input disabled>` theo URL `?type=N`. `<Textarea>` JSON Điều khoản TT → `<Select>` 8 preset Việt + "Khác (nhập tay)" → text input fallback (1 commit). **B8 (S11++++++) Display status meta** — `PeDisplayStatus` enum 4-5 trạng thái UI (Bản nháp / Đã gửi duyệt / Trả lại / Đã duyệt / Từ chối) gom phase chi tiết. `getPeDisplayStatus()` helper. Workflow timeline Panel 3 vẫn giữ phase chi tiết (1 commit). **B9 (S11+++++++) Phase TraLai + pencil always visible + edit gating** — Domain `PurchaseEvaluationPhase` thêm `TraLai = 98` (giữa DaDuyet=7 + TuChoi=99). FE label/color + display status badge. `isEditablePhase()` helper: chỉ DangSoanThao + TraLai. PeListPanel pencil bỏ hover-only → LUÔN visible (bright khi editable / xám disabled khi không) + click guard. Workspace `editableOnly` filter client-side (1 commit). **B10 (hotfix CI)** — TS strict catch fail 2 commit B7+B8 do skip-verify (`forcedPhase` rename quên xóa destructuring args + unused `PurchaseEvaluationType` import). Update memory `feedback_uat_skip_verify.md` thêm exception "rename/remove → BẮT BUỘC `npm run build`" (1 commit). **B11 (last) PE detail polish** — `NccSelectorRow` Section 2.a thay FormRow tĩnh → Select dropdown từ ev.suppliers (Section 3 list) wire `/select-winner` API. Section 2.c text rõ "(chọn NCC trước)" / "(chưa nhập báo giá)". Section 3 row khi `isWinner` → ẩn ✏ + 🗑 (chỉ giữ ✓ active). Bottom action bar workspace mode: **2 nút "Lưu (đóng)" + "Lưu & Gửi Duyệt →"** confirm dialog → POST `/transitions` với `targetPhase = first nextPhase` skip TuChoi/TraLai → workflow chuyển từ Bản nháp/Trả lại → Đã gửi duyệt (ChoPurchasing) → onBack đóng workspace (1 commit). | `ee0d360` (S10 C1) → `4c0625c` (B11) — 23 commit total |
|
| 2026-05-07 | Claude | **🎯 SESSION WRAP-UP S10-11+++++++ — PE Workspace UX overhaul đầy đủ (23 commit / ~3500 LOC FE + Mig 17 BE)** — User UAT live mode iterate liên tục, áp rule `feedback_uat_skip_verify` (skip dotnet test sau mỗi chunk, push ngay). 7 batch chính: **B1 (S10) PE Thao tác 2-panel workspace** — leaf `Pe_*_Create` từ page Create header riêng → workspace 2-panel `[320px_1fr]` mirror HĐ Thầu phụ; PeListPanel pure picker + sticky "+ Thêm mới"; PeDetailTabs `mode='workspace'` ẩn Workflow/Approvals/History + Section 5 disabled "nhập khi duyệt" (4 commit). **B2 (S11) Migration 17** `AddManualBudgetFieldsToPeAndContract` — 4 ALTER (PE + HĐ × `BudgetManualName` nvarchar(200) + `BudgetManualAmount` decimal(18,2)) cho fallback "user nhập tay khi không link Budget entity approved". Domain + EF config + App CQRS Create/Update + DTO + Validator + carry-forward `CreateContractFromEvaluation`. FE toggle "Nhập tay" trong PeHeaderForm + ContractCreatePage NewForm/EditForm × 2 app (5 commit). **B3 (S11+) BudgetFieldRow inline editor** — Section 2 "b. Ngân sách" thay FormRow tĩnh → editable component (toggle + Select OR 2 input + Save dirty + Hủy). canEdit cho cả 3 view (Workspace/Danh sách/Duyệt mode), readOnly chỉ display (3 commit). **B4 (S11++) InfoTab inline edit + PeListPanel pencil hover** — Section 1 "✎ Sửa" button flip display↔inputs (Tên/Địa điểm/Mô tả/Payment editable, Dự án locked). PeListPanel thêm pencil icon group-hover absolute right + URL `?editHeader=1` chain → `autoEditHeader` prop trigger mount-time edit (3 commit). **B5 (S11+++) Workspace "new" sectioned create view** — `PeWorkspaceCreateView.tsx` ~230 LOC layout 5 sections giống PeDetailTabs visual. S1 + S2.b editable, S3-5 LockedHint "Lưu phiếu trước". POST trigger create. Replace `PeHeaderForm` trong workspace mode='new' (1 commit). **B6 (S11++++) Danh sách disable toàn bộ interactions** — PurchaseEvaluationsListPage `readOnly={true}` hardcoded cho PeDetailTabs + `readOnly={!pendingMe}` cho PeWorkflowPanel (List view → ẩn Chuyển tiếp + show hint "Vào menu Duyệt"; Pending vẫn approve được) (2 commit). **B7 (S11+++++) Lock Loại quy trình + payment preset** — workspace `<Select>` Loại quy trình → `<Input disabled>` theo URL `?type=N`. `<Textarea>` JSON Điều khoản TT → `<Select>` 8 preset Việt + "Khác (nhập tay)" → text input fallback (1 commit). **B8 (S11++++++) Display status meta** — `PeDisplayStatus` enum 4-5 trạng thái UI (Bản nháp / Đã gửi duyệt / Trả lại / Đã duyệt / Từ chối) gom phase chi tiết. `getPeDisplayStatus()` helper. Workflow timeline Panel 3 vẫn giữ phase chi tiết (1 commit). **B9 (S11+++++++) Phase TraLai + pencil always visible + edit gating** — Domain `PurchaseEvaluationPhase` thêm `TraLai = 98` (giữa DaDuyet=7 + TuChoi=99). FE label/color + display status badge. `isEditablePhase()` helper: chỉ DangSoanThao + TraLai. PeListPanel pencil bỏ hover-only → LUÔN visible (bright khi editable / xám disabled khi không) + click guard. Workspace `editableOnly` filter client-side (1 commit). **B10 (hotfix CI)** — TS strict catch fail 2 commit B7+B8 do skip-verify (`forcedPhase` rename quên xóa destructuring args + unused `PurchaseEvaluationType` import). Update memory `feedback_uat_skip_verify.md` thêm exception "rename/remove → BẮT BUỘC `npm run build`" (1 commit). **B11 (last) PE detail polish** — `NccSelectorRow` Section 2.a thay FormRow tĩnh → Select dropdown từ ev.suppliers (Section 3 list) wire `/select-winner` API. Section 2.c text rõ "(chọn NCC trước)" / "(chưa nhập báo giá)". Section 3 row khi `isWinner` → ẩn ✏ + 🗑 (chỉ giữ ✓ active). Bottom action bar workspace mode: **2 nút "Lưu (đóng)" + "Lưu & Gửi Duyệt →"** confirm dialog → POST `/transitions` với `targetPhase = first nextPhase` skip TuChoi/TraLai → workflow chuyển từ Bản nháp/Trả lại → Đã gửi duyệt (ChoPurchasing) → onBack đóng workspace (1 commit). | `ee0d360` (S10 C1) → `4c0625c` (B11) — 23 commit total |
|
||||||
|
|||||||
@ -157,6 +157,24 @@ Session log: `2026-04-28-chot-session-4-budget.md`.
|
|||||||
|
|
||||||
## 📝 Phase 9 — UAT + Ops + carry over (Session 6+ active)
|
## 📝 Phase 9 — UAT + Ops + carry over (Session 6+ active)
|
||||||
|
|
||||||
|
### ✅ Session 13 done (2026-05-07) — Mirror N-stage Contract (Mig 20, 5 commit per-chunk + skip Chunk E auto-bind)
|
||||||
|
|
||||||
|
User chỉ thị mirror N-stage từ PE sang Contract. Budget defer (cần migration `AddBudgetVersionedWorkflow` trước — hardcoded `BudgetPolicy.Default` chưa có WorkflowDefinition entity). Pattern reusable đầy đủ từ PE.
|
||||||
|
|
||||||
|
- [x] **Chunk A (`951ffa3`)** Domain entity `WorkflowStepInnerStep` (Domain/Contracts/) + nav WorkflowStep.InnerSteps + ALTER ContractDeptApproval.InnerStepId + EF config FK Cascade Step / Restrict Dept+InnerStep + **Migration 20** `AddContractWorkflowInnerStepsAndAlterDeptApprovalUnique` GỘP 1 (CREATE TABLE WorkflowStepInnerSteps + ALTER InnerStepId + DropIndex old + Recreate filtered legacy `WHERE InnerStepId IS NULL` + new filtered N-stage `WHERE InnerStepId IS NOT NULL`).
|
||||||
|
- [x] **Chunk B (`04cf2a0`)** Application CQRS DTO mirror PE Chunk B — `WorkflowStepInnerStepDto` + extend `WorkflowStepDto` + `CreateWorkflowStepInnerStepInput` + extend `CreateWorkflowStepInput` (default null backward compat) + Validator child rules + Handler atomic batch insert.
|
||||||
|
- [x] **Chunk C (`e247b67`)** ContractWorkflowService refactor mirror PE — load InnerSteps eager + reject branch clear N-stage rows + dept block split hasInnerSteps→N-stage logic / else→legacy 2-stage. N-stage flow giống PE: match firstPending Order asc + (exact level OR canBypass + level≥), exact upsert / bypass batch upsert, recheck stillPending → BLOCK.
|
||||||
|
- [x] **Chunk D (`7c0772a`)** ContractNStageApprovalTests 6 test mirror PE pattern + helper SeedWorkflowDefinitionAsync 2 step adjacent (DangGopY + DangDamPhan) + SeedContractAsync với Project + Supplier + FakeChangelogService + FakeContractCodeGenerator stubs. Bug fix legacy fallback test: switched phase pair sang DangKiemTraCCM → DangTrinhKy + role CostControl khớp Standard.Transitions. **89→95 test pass**.
|
||||||
|
- [x] **Chunk E SKIP** — WorkflowsController auto-bind `[FromBody] CreateWorkflowDefinitionCommand` record qua JSON, no code change cần.
|
||||||
|
- [x] **Chunk F (current)** FE WorkflowsPage Designer extend mirror PeWorkflowsPage Chunk F: InnerStepDto + EditInnerStep types + copyFromDefinition include + departmentsList query + sub-section "Cấp duyệt nhỏ trong phòng" drag-list + button "+ Thêm cấp duyệt" emerald + payload include Order asc. Empty state hint fallback 2-cấp legacy. KHÔNG đụng fe-user (admin-only).
|
||||||
|
|
||||||
|
**Backward compat 100%**: workflow Contract no InnerSteps configured → service fallback legacy 2-stage Mig 16. Data legacy InnerStepId=null vẫn enforce unique cũ qua filtered index.
|
||||||
|
|
||||||
|
**Defer Session 14+:**
|
||||||
|
- [ ] **Budget N-stage** — cần migration `AddBudgetVersionedWorkflow` trước (4 bảng + ALTER Budget.WorkflowDefinitionId), sau đó migration `AddBudgetWorkflowInnerSteps`. User quyết riêng (feature mở rộng module lớn).
|
||||||
|
- [ ] schema-diagram.md §17 Mig 20 update — defer cron audit 2026-06-01.
|
||||||
|
- [ ] Skill ef-core-migration row Mig 20 — defer cron audit.
|
||||||
|
|
||||||
### ✅ Session 12 done (2026-05-07) — N-stage workflow approval (Mig 18+19, 6 commit per-chunk, PE-only)
|
### ✅ Session 12 done (2026-05-07) — N-stage workflow approval (Mig 18+19, 6 commit per-chunk, PE-only)
|
||||||
|
|
||||||
User yêu cầu: workflow level cha (= phase) cấu hình được level con (Phòng × Cấp NV/PP/TP) sequential, mỗi cấp = 1 inner step duyệt riêng, có bypass cùng dept. 6 spec defaults chốt + 6 chunk per-commit.
|
User yêu cầu: workflow level cha (= phase) cấu hình được level con (Phòng × Cấp NV/PP/TP) sequential, mỗi cấp = 1 inner step duyệt riêng, có bypass cùng dept. 6 spec defaults chốt + 6 chunk per-commit.
|
||||||
|
|||||||
@ -0,0 +1,201 @@
|
|||||||
|
# Session 2026-05-07 (S13) — Mirror N-stage Contract (PE → HĐ)
|
||||||
|
|
||||||
|
**Dev:** Claude
|
||||||
|
**Duration:** ~2h
|
||||||
|
**Base commit:** `5e5042d` (sau Session 12 N-stage PE)
|
||||||
|
**Final commit:** (current Chunk F)
|
||||||
|
**Total commits:** 5 per-chunk (Chunk E skipped)
|
||||||
|
|
||||||
|
## Bối cảnh
|
||||||
|
|
||||||
|
Sau Session 12 đóng N-stage cho PE module (Mig 18+19). User confirm mirror sang Contract với caveat Budget defer (Budget chưa có versioned WorkflowDefinition entity — hardcoded `BudgetPolicy.Default`, cần migration `AddBudgetVersionedWorkflow` trước).
|
||||||
|
|
||||||
|
Pattern Contract N-stage 100% mirror PE Mig 18 logic — reusable hoàn toàn vì Contract đã có versioned WF từ Mig 8.
|
||||||
|
|
||||||
|
## 5 chunk per-commit (Chunk E skipped)
|
||||||
|
|
||||||
|
### Chunk A — Domain + Migration 20 (`951ffa3`)
|
||||||
|
|
||||||
|
**Files mới (1):**
|
||||||
|
- (Cùng file `Domain/Contracts/WorkflowDefinition.cs` — class `WorkflowStepInnerStep` thêm cuối file).
|
||||||
|
|
||||||
|
**Files edit (3):**
|
||||||
|
- `Domain/Contracts/WorkflowDefinition.cs` — `using SolutionErp.Domain.Identity` (PositionLevel reuse Session 12) + class WorkflowStep nav `+List<InnerSteps>` + class mới `WorkflowStepInnerStep` (Order, DepartmentId, PositionLevel, Name, SlaDays, IsRequired).
|
||||||
|
- `Domain/Contracts/ContractDepartmentApproval.cs` — `+public Guid? InnerStepId { get; set; }`.
|
||||||
|
|
||||||
|
**Infra edit (3):**
|
||||||
|
- `Infrastructure/Persistence/Configurations/WorkflowDefinitionConfiguration.cs` — config InnerStep table (Cascade Step, Restrict Department, IX (StepId, Order) + IX DeptId).
|
||||||
|
- `Infrastructure/Persistence/Configurations/DepartmentApprovalsConfiguration.cs` — ContractDeptApproval section: Drop UNIQUE old → 2 filtered (legacy `WHERE InnerStepId IS NULL` + N-stage `WHERE InnerStepId IS NOT NULL`) + IX InnerStepId + FK Restrict.
|
||||||
|
- `Infrastructure/Persistence/ApplicationDbContext.cs` — `DbSet<WorkflowStepInnerStep>`.
|
||||||
|
|
||||||
|
**Migration 20** `AddContractWorkflowInnerStepsAndAlterDeptApprovalUnique` GỘP 1 (thay vì tách 2 như Mig 18+19 cho PE — combined CREATE TABLE + ALTER + filtered unique alter trong cùng migration cho Contract):
|
||||||
|
- DropIndex UX_ContractDeptApprovals_Contract_Phase_Dept_Stage
|
||||||
|
- AddColumn InnerStepId nullable
|
||||||
|
- CreateTable WorkflowStepInnerSteps + 2 FK (Cascade Step / Restrict Dept)
|
||||||
|
- CreateIndex IX_InnerStepId
|
||||||
|
- CreateIndex 2 filtered unique (legacy + N-stage)
|
||||||
|
- 2 IX cho InnerSteps (StepId+Order, DeptId)
|
||||||
|
- AddForeignKey ContractDeptApproval → InnerStep Restrict
|
||||||
|
|
||||||
|
3-file rule: Migration .cs + Designer + Snapshot.
|
||||||
|
|
||||||
|
**Verify:** dotnet build pass, ef database update LocalDB applied, 89 test pass (no regression).
|
||||||
|
|
||||||
|
### Chunk B — Application CQRS DTO (`04cf2a0`)
|
||||||
|
|
||||||
|
**`Application/Contracts/WorkflowAdminFeatures.cs`:**
|
||||||
|
- record `WorkflowStepInnerStepDto` (Id, Order, DepartmentId, DepartmentName, PositionLevel, Name, SlaDays, IsRequired) — mirror PE Mig 18.
|
||||||
|
- record `WorkflowStepDto` extend `+List<WorkflowStepInnerStepDto> InnerSteps`.
|
||||||
|
- `GetWorkflowAdminOverviewQueryHandler` — Include InnerSteps OrderBy Order + resolve DeptNames cho display.
|
||||||
|
- record `CreateWorkflowStepInnerStepInput` (Order, DeptId, PositionLevel, Name, SlaDays, IsRequired).
|
||||||
|
- record `CreateWorkflowStepInput` extend `+List<...InnerSteps>? = null` (default null cho backward compat existing test code positional `new(...)`).
|
||||||
|
- Validator child rules cho InnerSteps (Order ≥1, DeptId not empty, PositionLevel 1-3, SlaDays ≥0).
|
||||||
|
- `CreateWorkflowDefinitionCommandHandler` — convert InnerSteps khi build entity (atomic batch insert qua nav collection).
|
||||||
|
|
||||||
|
**Verify:** dotnet build 0 error, 89 test pass.
|
||||||
|
|
||||||
|
### Chunk C — Service N-stage logic (`e247b67`)
|
||||||
|
|
||||||
|
**`Infrastructure/Services/ContractWorkflowService.cs` refactor TransitionAsync:**
|
||||||
|
1. **Reject branch** thêm: clear N-stage approval rows tại `fromPhase` (resume sẽ approve lại từ inner đầu).
|
||||||
|
2. **Load policy** với `.ThenInclude(s => s.InnerSteps.OrderBy(i => i.Order))` eager + assign `definition` outer scope.
|
||||||
|
3. **Department approval block split:**
|
||||||
|
```
|
||||||
|
if (decision==Approve && targetPhase != Stao && targetPhase != TuChoi
|
||||||
|
&& !isResumingAfterReject && !isAdmin && !isSystem
|
||||||
|
&& actorUserId is Guid actorUid)
|
||||||
|
{
|
||||||
|
var step = def?.Steps.FirstOrDefault(s => s.Phase == fromPhase);
|
||||||
|
var hasInnerSteps = step?.InnerSteps.Count > 0;
|
||||||
|
if (hasInnerSteps) { /* N-stage logic */ }
|
||||||
|
else if (actor.DeptId is Guid deptId) { /* legacy 2-stage Mig 16 */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. **N-stage logic** mirror PE pattern:
|
||||||
|
- Yêu cầu actor có DeptId + PositionLevel (else throw 403).
|
||||||
|
- Load existing approvals tại fromPhase với InnerStepId IN [innerIds] → compute doneInnerIds.
|
||||||
|
- firstPending = pendingRequired[0].
|
||||||
|
- Match: actorDept == firstPending.Dept AND (actorPos == firstPending.Level OR canBypass + actorPos ≥ firstPending.Level). Mismatch → 403.
|
||||||
|
- Exact match: 1 row upsert (Stage=Confirm, InnerStepId=firstPending, IsBypassed=false).
|
||||||
|
- Bypass: batch upsert tất cả inners cùng dept actor có level từ firstPending.Level đến actorPos (IsBypassed=true cho cấp dưới skip).
|
||||||
|
- Recheck stillPending → BLOCK + log Approval/Changelog "duyệt cấp X (còn Y pending)" + return early.
|
||||||
|
- All done → fall through phase transition.
|
||||||
|
5. **Legacy fallback** (else branch) — giữ nguyên logic 2-stage Mig 16 với InnerStepId=null filter.
|
||||||
|
|
||||||
|
**Verify:** dotnet build 0 error, 89 test pass (legacy backward compat OK).
|
||||||
|
|
||||||
|
### Chunk D — Tests N-stage Contract 6 test (`7c0772a`)
|
||||||
|
|
||||||
|
**`tests/.../Services/ContractNStageApprovalTests.cs` (NEW, 6 test):**
|
||||||
|
|
||||||
|
| # | Test | Cover |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | NStage_FirstInner_NV_Approve_Blocks_Phase_Transition | NV cấp 1 → 1 row InnerStepId set, phase chưa đổi |
|
||||||
|
| 2 | NStage_All_3_Levels_Sequential_Pass_Allow_Phase_Transition | NV → PP → TP → 3 rows + phase chuyển |
|
||||||
|
| 3 | NStage_TP_Bypass_Skips_Lower_Levels_Same_Dept | TP+canBypass → 3 rows (NV+PP IsBypassed, TP exact) |
|
||||||
|
| 4 | NStage_Wrong_Department_Throws_Forbidden | Actor dept khác → ForbiddenException |
|
||||||
|
| 5 | NStage_Reject_Clears_InnerStep_Rows_At_Phase | Admin reject → DangSoanThao + clear N-stage rows |
|
||||||
|
| 6 | LegacyFallback_NoInnerSteps_Uses_2Stage_Logic | Contract no pinned def → fallback 2-stage Stage=Review |
|
||||||
|
|
||||||
|
**Helper `SeedWorkflowDefinitionAsync`** tạo definition với 2 step adjacent (DangGopY có inner steps + DangDamPhan next, no inner) — đủ cho FromDefinition build transition policy guard pass actor role Procurement.
|
||||||
|
|
||||||
|
**Helper `SeedContractAsync`** tạo Contract với Project + Supplier seed cho FK constraints.
|
||||||
|
|
||||||
|
**Stubs `FakeChangelogService` + `FakeContractCodeGenerator`** — no-op cho tests không cần verify changelog/codegen path.
|
||||||
|
|
||||||
|
**Bug fix:** Legacy fallback test ban đầu fail (Standard policy DangGopY → DangDamPhan chỉ cho [Drafter, DeptManager], không Procurement) → switched phase pair sang DangKiemTraCCM → DangTrinhKy + role CostControl khớp Standard.Transitions. NV.CCM không có DeptManager role → Stage=Review block đúng pattern.
|
||||||
|
|
||||||
|
**Verify:** 89 → **95 test pass** (54 Domain + 41 Infra: 17 codegen + 6 PE WF versioning + 6 PE 2-stage + 6 PE N-stage + 6 Contract N-stage).
|
||||||
|
|
||||||
|
### Chunk E SKIP — API auto-bind
|
||||||
|
|
||||||
|
WorkflowsController.cs Create endpoint dùng `[FromBody] CreateWorkflowDefinitionCommand`. Record positional với InnerSteps (default null) → JSON body bind tự động. KHÔNG cần code change.
|
||||||
|
|
||||||
|
### Chunk F — FE Designer + Docs (current)
|
||||||
|
|
||||||
|
**`fe-admin/src/types/users.ts`:** đã có PositionLevel const + Label/Short maps từ Session 12 → reuse.
|
||||||
|
|
||||||
|
**`fe-admin/src/pages/system/WorkflowsPage.tsx`:**
|
||||||
|
- Type `InnerStepDto` + extend `StepDto` `+innerSteps: InnerStepDto[]` mirror PE.
|
||||||
|
- Type `EditInnerStep` + extend `EditStep` `+innerSteps: EditInnerStep[]`.
|
||||||
|
- `copyFromDefinition` include innerSteps map.
|
||||||
|
- Default new step `+innerSteps: []`.
|
||||||
|
- `departmentsList` useQuery fetch /departments.
|
||||||
|
- Save mutation payload: `s.innerSteps.map((ii, ix) => ({ order: ix+1, departmentId, positionLevel, name, slaDays, isRequired }))`.
|
||||||
|
- UI sub-section "Cấp duyệt nhỏ trong phòng (sequential — Order asc)" trong từng step card sau "Người duyệt" section, drag-list rows { Order badge emerald + Select Phòng + Select Cấp + checkbox required + Trash button } + button "+ Thêm cấp duyệt" emerald (disabled khi departmentsList empty).
|
||||||
|
- Empty state hint fallback 2-cấp legacy.
|
||||||
|
|
||||||
|
**KHÔNG đụng fe-user** — WorkflowsPage admin-only.
|
||||||
|
|
||||||
|
**Verify:** npm run build fe-admin pass (✓ built), 0 TS error.
|
||||||
|
|
||||||
|
## E2E verified
|
||||||
|
|
||||||
|
- ✅ `dotnet test SolutionErp.slnx` 95/95 pass (54 Domain + 41 Infra)
|
||||||
|
- ✅ `dotnet build SolutionErp.slnx` 0 error, 2 pre-existing warning
|
||||||
|
- ✅ `dotnet ef database update` Mig 20 LocalDB applied OK
|
||||||
|
- ✅ `npm run build` fe-admin pass — no TS error
|
||||||
|
- 🔄 Manual UAT — defer cho user thử workflow Designer Contract Create version mới có Inner Steps
|
||||||
|
|
||||||
|
## Bug + Fix log
|
||||||
|
|
||||||
|
| # | Issue | Fix | Commit |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | Test legacy fallback `Standard policy DangGopY → DangDamPhan không cho Procurement` | Switch phase pair sang DangKiemTraCCM → DangTrinhKy + role CostControl khớp | `7c0772a` (Chunk D inline) |
|
||||||
|
|
||||||
|
## Docs updates
|
||||||
|
|
||||||
|
- ✅ STATUS.md — Last updated + Phase summary count + 1 row Recently Done Session 13 (KEEP narrative cũ)
|
||||||
|
- ✅ HANDOFF.md — TL;DR Session 13 prepend + 7 cảnh báo Session 14+ + giữ Session 12 narrative
|
||||||
|
- ✅ migration-todos.md — Phase 9 + Session 13 block 5 chunk + 3 defer task
|
||||||
|
- ✅ Session log (file này)
|
||||||
|
- ⏸️ schema-diagram.md §17 Mig 20 — defer cron audit 2026-06-01
|
||||||
|
- ⏸️ Skill ef-core-migration row Mig 20 — defer cron audit
|
||||||
|
- ⏸️ Skill contract-workflow N-stage cross-ref — defer cron audit
|
||||||
|
|
||||||
|
## Stats cumulative (sau Session 13)
|
||||||
|
|
||||||
|
| | Trước S13 | Sau S13 | Diff |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| BE LOC | ~15300 | ~15700 | +400 |
|
||||||
|
| API endpoints | ~134 | ~134 | 0 (auto-bind) |
|
||||||
|
| Migrations | 19 | **20** | +1 (Mig 20 gộp) |
|
||||||
|
| DB tables | 56 | **57** | +1 (WorkflowStepInnerSteps) |
|
||||||
|
| DB columns mới | — | +1 | ContractDeptApproval.InnerStepId |
|
||||||
|
| FE pages | 32 | 32 | 0 (extend existing) |
|
||||||
|
| Tests | 89 | **95** | +6 (Contract N-stage) |
|
||||||
|
| Docs | ~56 | ~57 | +1 (session log này) |
|
||||||
|
| Commits S13 | — | **+5** | A→F per-chunk, E skipped |
|
||||||
|
|
||||||
|
## Plan organization sau session 13
|
||||||
|
|
||||||
|
```
|
||||||
|
Plan cha: Phase 9 active — UAT
|
||||||
|
├── Plan con A: Hard blockers (chờ user/ops) — 6 task (giữ nguyên)
|
||||||
|
├── ...
|
||||||
|
├── Plan con S12 ✅ DONE: N-stage PE (Mig 18+19, 6 chunk)
|
||||||
|
├── Plan con S13 ✅ DONE: N-stage Contract (Mig 20, 5 chunk + skip E)
|
||||||
|
│ ├── ✅ A: Domain + Mig 20
|
||||||
|
│ ├── ✅ B: App CQRS
|
||||||
|
│ ├── ✅ C: Service logic
|
||||||
|
│ ├── ✅ D: 6 test
|
||||||
|
│ ├── ⊘ E: API auto-bind skip
|
||||||
|
│ └── ✅ F: FE Designer + Docs
|
||||||
|
├── Plan con kế (Task 4): Wire BE TraLai PE — pending
|
||||||
|
├── Plan con kế (Task 2): Sample data N-stage seed — pending
|
||||||
|
└── Plan con Defer S13+:
|
||||||
|
├── Budget N-stage (cần versioned WF migration trước)
|
||||||
|
├── schema-diagram §17 Mig 20
|
||||||
|
└── Skill ef-core-migration Mig 20 row
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
UAT iteration mode. Contract N-stage giờ đã ready. Workflow N-stage Contract backward compat 100% với data legacy 2-stage Mig 16.
|
||||||
|
|
||||||
|
**Cron audit kế:** 2026-06-01 (~25 ngày).
|
||||||
|
|
||||||
|
**Tiếp theo theo user order:**
|
||||||
|
1. ✅ Task 3 (3a Contract done, 3b Budget defer)
|
||||||
|
2. ⏸ Task 4 (Wire BE TraLai PE)
|
||||||
|
3. ⏸ Task 2 (Sample data seed)
|
||||||
@ -12,12 +12,33 @@ import { Select } from '@/components/ui/Select'
|
|||||||
import { Textarea } from '@/components/ui/Textarea'
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { getErrorMessage } from '@/lib/apiError'
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
import { AVAILABLE_ROLES, RoleLabel } from '@/types/users'
|
import { AVAILABLE_ROLES, RoleLabel, PositionLevel, PositionLevelLabel } from '@/types/users'
|
||||||
|
import type { Department, Paged } from '@/types/master'
|
||||||
|
|
||||||
// ===== Types =====
|
// ===== Types =====
|
||||||
|
|
||||||
type ApproverDto = { kind: number; assignmentValue: string; displayName: string | null }
|
type ApproverDto = { kind: number; assignmentValue: string; displayName: string | null }
|
||||||
type StepDto = { id: string; order: number; phase: number; phaseLabel: string; name: string; slaDays: number | null; approvers: ApproverDto[] }
|
// Mig 20 — N-stage inner step DTO mirror PE Mig 18
|
||||||
|
type InnerStepDto = {
|
||||||
|
id: string
|
||||||
|
order: number
|
||||||
|
departmentId: string
|
||||||
|
departmentName: string | null
|
||||||
|
positionLevel: number // 1=NV, 2=PP, 3=TP
|
||||||
|
name: string | null
|
||||||
|
slaDays: number | null
|
||||||
|
isRequired: boolean
|
||||||
|
}
|
||||||
|
type StepDto = {
|
||||||
|
id: string
|
||||||
|
order: number
|
||||||
|
phase: number
|
||||||
|
phaseLabel: string
|
||||||
|
name: string
|
||||||
|
slaDays: number | null
|
||||||
|
approvers: ApproverDto[]
|
||||||
|
innerSteps: InnerStepDto[]
|
||||||
|
}
|
||||||
type DefinitionDto = {
|
type DefinitionDto = {
|
||||||
id: string
|
id: string
|
||||||
code: string
|
code: string
|
||||||
@ -51,7 +72,22 @@ const PHASE_OPTIONS: { value: number; label: string }[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
type EditStepApprover = { kind: 1 | 2; assignmentValue: string }
|
type EditStepApprover = { kind: 1 | 2; assignmentValue: string }
|
||||||
type EditStep = { phase: number; name: string; slaDays: number | null; approvers: EditStepApprover[] }
|
// Mig 20 — Inner step level con
|
||||||
|
type EditInnerStep = {
|
||||||
|
order: number
|
||||||
|
departmentId: string
|
||||||
|
positionLevel: number // 1/2/3
|
||||||
|
name: string
|
||||||
|
slaDays: number | null
|
||||||
|
isRequired: boolean
|
||||||
|
}
|
||||||
|
type EditStep = {
|
||||||
|
phase: number
|
||||||
|
name: string
|
||||||
|
slaDays: number | null
|
||||||
|
approvers: EditStepApprover[]
|
||||||
|
innerSteps: EditInnerStep[]
|
||||||
|
}
|
||||||
|
|
||||||
function copyFromDefinition(d: DefinitionDto): EditStep[] {
|
function copyFromDefinition(d: DefinitionDto): EditStep[] {
|
||||||
return d.steps.map(s => ({
|
return d.steps.map(s => ({
|
||||||
@ -59,6 +95,14 @@ function copyFromDefinition(d: DefinitionDto): EditStep[] {
|
|||||||
name: s.name,
|
name: s.name,
|
||||||
slaDays: s.slaDays,
|
slaDays: s.slaDays,
|
||||||
approvers: s.approvers.map(a => ({ kind: a.kind as 1 | 2, assignmentValue: a.assignmentValue })),
|
approvers: s.approvers.map(a => ({ kind: a.kind as 1 | 2, assignmentValue: a.assignmentValue })),
|
||||||
|
innerSteps: (s.innerSteps ?? []).map(i => ({
|
||||||
|
order: i.order,
|
||||||
|
departmentId: i.departmentId,
|
||||||
|
positionLevel: i.positionLevel,
|
||||||
|
name: i.name ?? '',
|
||||||
|
slaDays: i.slaDays,
|
||||||
|
isRequired: i.isRequired,
|
||||||
|
})),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,7 +323,7 @@ function WorkflowDesigner({
|
|||||||
() =>
|
() =>
|
||||||
cloneFrom
|
cloneFrom
|
||||||
? copyFromDefinition(cloneFrom)
|
? copyFromDefinition(cloneFrom)
|
||||||
: [{ phase: 2, name: 'Soạn thảo', slaDays: 7, approvers: [] }],
|
: [{ phase: 2, name: 'Soạn thảo', slaDays: 7, approvers: [], innerSteps: [] }],
|
||||||
[cloneFrom],
|
[cloneFrom],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -294,6 +338,12 @@ function WorkflowDesigner({
|
|||||||
(await api.get<{ items: { id: string; fullName: string; email: string }[] }>('/users', { params: { page: 1, pageSize: 200 } })).data.items,
|
(await api.get<{ items: { id: string; fullName: string; email: string }[] }>('/users', { params: { page: 1, pageSize: 200 } })).data.items,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const departmentsList = useQuery({
|
||||||
|
queryKey: ['departments-for-inner-step'],
|
||||||
|
queryFn: async () =>
|
||||||
|
(await api.get<Paged<Department>>('/departments', { params: { page: 1, pageSize: 200 } })).data.items,
|
||||||
|
})
|
||||||
|
|
||||||
const save = useMutation({
|
const save = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
await api.post('/workflows', {
|
await api.post('/workflows', {
|
||||||
@ -307,6 +357,14 @@ function WorkflowDesigner({
|
|||||||
name: s.name,
|
name: s.name,
|
||||||
slaDays: s.slaDays,
|
slaDays: s.slaDays,
|
||||||
approvers: s.approvers,
|
approvers: s.approvers,
|
||||||
|
innerSteps: s.innerSteps.map((ii, ix) => ({
|
||||||
|
order: ix + 1,
|
||||||
|
departmentId: ii.departmentId,
|
||||||
|
positionLevel: ii.positionLevel,
|
||||||
|
name: ii.name || null,
|
||||||
|
slaDays: ii.slaDays,
|
||||||
|
isRequired: ii.isRequired,
|
||||||
|
})),
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -366,7 +424,7 @@ function WorkflowDesigner({
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setSteps([...steps, { phase: 3, name: '', slaDays: 7, approvers: [] }])
|
setSteps([...steps, { phase: 3, name: '', slaDays: 7, approvers: [], innerSteps: [] }])
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5" />
|
<Plus className="h-3.5 w-3.5" />
|
||||||
@ -490,6 +548,104 @@ function WorkflowDesigner({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Inner Steps (Mig 20) — N-stage approval Phòng × Cấp chức danh — mirror PE Mig 18 */}
|
||||||
|
<div className="mt-2 border-t border-slate-200 pt-2">
|
||||||
|
<div className="mb-1 flex items-center justify-between">
|
||||||
|
<Label className="text-[11px]">
|
||||||
|
Cấp duyệt nhỏ trong phòng (sequential — Order asc)
|
||||||
|
{s.innerSteps.length > 0 && <span className="ml-1 text-slate-400">· {s.innerSteps.length} cấp</span>}
|
||||||
|
</Label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!departmentsList.data || departmentsList.data.length === 0}
|
||||||
|
onClick={() => {
|
||||||
|
const firstDeptId = departmentsList.data?.[0]?.id ?? ''
|
||||||
|
setSteps(steps.map((x, i) =>
|
||||||
|
i === idx ? {
|
||||||
|
...x,
|
||||||
|
innerSteps: [...x.innerSteps, {
|
||||||
|
order: x.innerSteps.length + 1,
|
||||||
|
departmentId: firstDeptId,
|
||||||
|
positionLevel: PositionLevel.NhanVien,
|
||||||
|
name: '',
|
||||||
|
slaDays: null,
|
||||||
|
isRequired: true,
|
||||||
|
}],
|
||||||
|
} : x,
|
||||||
|
))
|
||||||
|
}}
|
||||||
|
className="rounded bg-emerald-50 px-2 py-0.5 text-[11px] font-medium text-emerald-700 hover:bg-emerald-100 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
+ Thêm cấp duyệt
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{s.innerSteps.length === 0 && (
|
||||||
|
<div className="rounded bg-slate-100 px-2 py-1.5 text-[11px] italic text-slate-500">
|
||||||
|
Chưa cấu hình cấp con — workflow fallback logic 2-cấp NV/TPB legacy.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{s.innerSteps.length > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{s.innerSteps.map((ii, ix) => (
|
||||||
|
<div key={ix} className="flex items-center gap-1 rounded border border-slate-200 bg-white p-1">
|
||||||
|
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-emerald-600 text-[10px] font-bold text-white">
|
||||||
|
{ix + 1}
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
value={ii.departmentId}
|
||||||
|
onChange={e =>
|
||||||
|
setSteps(steps.map((x, i) =>
|
||||||
|
i === idx ? { ...x, innerSteps: x.innerSteps.map((y, j) => (j === ix ? { ...y, departmentId: e.target.value } : y)) } : x,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
className="h-7 flex-1 text-xs"
|
||||||
|
>
|
||||||
|
{departmentsList.data?.map(d => (
|
||||||
|
<option key={d.id} value={d.id}>{d.name}</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={ii.positionLevel}
|
||||||
|
onChange={e =>
|
||||||
|
setSteps(steps.map((x, i) =>
|
||||||
|
i === idx ? { ...x, innerSteps: x.innerSteps.map((y, j) => (j === ix ? { ...y, positionLevel: Number(e.target.value) } : y)) } : x,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
className="h-7 w-28 text-xs"
|
||||||
|
>
|
||||||
|
<option value={PositionLevel.NhanVien}>{PositionLevelLabel[1]}</option>
|
||||||
|
<option value={PositionLevel.PhoPhong}>{PositionLevelLabel[2]}</option>
|
||||||
|
<option value={PositionLevel.TruongPhong}>{PositionLevelLabel[3]}</option>
|
||||||
|
</Select>
|
||||||
|
<label className="flex shrink-0 items-center gap-1 text-[11px] text-slate-500">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={ii.isRequired}
|
||||||
|
onChange={e =>
|
||||||
|
setSteps(steps.map((x, i) =>
|
||||||
|
i === idx ? { ...x, innerSteps: x.innerSteps.map((y, j) => (j === ix ? { ...y, isRequired: e.target.checked } : y)) } : x,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
required
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setSteps(steps.map((x, i) =>
|
||||||
|
i === idx ? { ...x, innerSteps: x.innerSteps.filter((_, j) => j !== ix) } : x,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-red-50 hover:text-red-600"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user