[CLAUDE] Phase3: Workflow MVP — 9-phase state machine + code gen + FE Inbox/Detail

Backend Contracts domain (5 entities):
- Contract aggregate: Phase (9 enum), SlaDeadline, MaHopDong, BypassProcurementAndCCM, DraftData, SlaWarningSent
- ContractApproval: FromPhase → ToPhase, ApproverUserId (null = system auto-approve), Decision, Comment
- ContractComment: thread theo Phase current
- ContractAttachment: FileName + StoragePath + Purpose (DraftExport/ScannedSigned/SealedCopy)
- ContractCodeSequence: Prefix PK + LastSeq — atomic gen

EF configs:
- Unique MaHopDong filtered [MaHopDong] IS NOT NULL
- Indexes: Phase+IsDeleted, SupplierId, ProjectId, SlaDeadline, ContractId+ApprovedAt, ContractId+CreatedAt
- Cascade delete Approvals/Comments/Attachments khi Contract xoa
- Query filter IsDeleted
- Migration AddContractsWorkflow (DB 19 tables)

Workflow service:
- IContractWorkflowService.TransitionAsync:
  - Adjacency check qua Transitions Dict<(from,to), roles[]> (12 transitions)
  - Role guard: user phai co role ∈ allowed
  - Admin bypass (role Admin pass moi check)
  - System bypass (userId=null + Decision=AutoApprove → cho SLA job sau nay)
  - Bypass CCM: BypassProcurementAndCCM=true cho phep DangInKy → DangTrinhKy skip phase 6
  - Gen ma HD khi chuyen DangDongDau (idempotent — khong gen lai neu da co)
  - Reset SlaDeadline = UtcNow + PhaseSla
  - Insert ContractApproval row

Code generator (RG-001):
- 7 format theo ContractType: HDTP / HDGK / NCC / HDDV / MB + 2 framework (year prefix)
- BeginTransactionAsync(Serializable) + ContractCodeSequences UPSERT → atomic
- Idempotent: neu MaHopDong da co thi skip

CQRS (8 feature, ContractFeatures.cs):
- CreateContractCommand + Validator + Handler (set SlaDeadline = +7d)
- UpdateContractDraftCommand (chi khi Phase=DangSoanThao)
- TransitionContractCommand (delegate → WorkflowService)
- AddCommentCommand (phase = hien tai)
- ListContractsQuery (PagedResult + filter phase/supplier/project/search)
- GetMyInboxQuery (map Phase → actor roles, filter theo role user)
- GetContractQuery (detail + approvals + comments + attachments + resolve user names)
- DeleteContractCommand (soft, block > DangInKy)

Controller:
- ContractsController 8 endpoint: GET list/inbox/detail, POST create/transition/comment, PUT update, DELETE

Frontend fe-admin (2 page moi):
- types/contracts.ts: ContractPhase const + Label + Color maps + types
- components/PhaseBadge.tsx
- pages/contracts/ContractsListPage.tsx: filter phase + search + click → detail
- pages/contracts/ContractDetailPage.tsx: 2-col layout (info+comments | timeline), action dialog select target phase + comment

Frontend fe-user (4 page moi + 14 file shared):
- cp 14 file shared tu fe-admin (menuKeys, types/*, DataTable, PhaseBadge, Dialog, Textarea, Select, apiError, usePermission, PermissionGuard)
- AuthContext update: load menu tu /menus/me + cache
- Layout: menu fixed 3 muc + user info + roles display
- InboxPage: list HD cho role user xu ly (sort theo SLA)
- ContractCreatePage: form chon loai + template + NCC + du an + gia tri + bypass CDT
- ContractDetailPage: duplicate fe-admin pattern (convention)
- MyContractsPage: list HD cua toi
- App.tsx: 4 route moi

E2E verified:
- Setup Supplier + Project
- POST /contracts → 201 + phase=2
- POST /contracts/{id}/transitions x7 → di het 9 phase
- Final: MaHopDong = "FLOCK 01/HĐGK/SOL&PVL2026/01" dung format RG-001
- Approvals: 7 rows audit day du

Docs:
- .claude/skills/contract-workflow/SKILL.md: placeholder → full spec voi state machine, SLA table, role matrix, 7 code format, code pointers, API, E2E workflow, pitfalls
- docs/changelog/sessions/2026-04-21-1330-phase3-workflow.md: session log
- docs/STATUS.md: Phase 3 MVP done, next Phase 4
- docs/HANDOFF.md: update phase status + file tree + commit log + testing points
- docs/changelog/migration-todos.md: tick Phase 3 MVP items + add iteration 2 list

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-21 12:26:09 +07:00
parent 5113e4c771
commit 7e957a7654
49 changed files with 4490 additions and 156 deletions

View File

@ -0,0 +1,148 @@
# Session 2026-04-21 13:30 — Phase 3 Workflow MVP
**Dev:** Claude (Opus 4.7)
**Duration:** ~1h15m
**Base commit:** `5113e4c`
## Làm được
### Chunk G — Domain + EF + Migration
- Domain/Contracts: **Contract** (aggregate, 15 fields) + **ContractApproval** (history) + **ContractComment** (thread) + **ContractAttachment** (files với `AttachmentPurpose` enum) + **ContractCodeSequence** (Prefix PK + LastSeq)
- EF configs với:
- Unique MaHopDong filtered `WHERE [MaHopDong] IS NOT NULL`
- Indexes: (Phase, IsDeleted), SupplierId, ProjectId, SlaDeadline
- Cascade delete cho Approvals/Comments/Attachments khi Contract xóa
- Query filter IsDeleted
- DbSets + `IApplicationDbContext` update (5 DbSet mới)
- Migration `AddContractsWorkflow`
### Chunk H — Workflow service + CodeGenerator + CQRS + Controller
**Services:**
- `IContractWorkflowService.TransitionAsync(contract, targetPhase, userId, roles, decision, comment)`:
- Adjacency check qua `Transitions` Dictionary<(from,to), roles[]>
- Role check (Admin bypass)
- System actor bypass (cho SLA job Phase 3.2)
- Bypass rule Chủ đầu tư (DangInKy → DangTrinhKy skip CCM)
- Gen mã HĐ khi DangDongDau (nếu chưa có)
- Reset SlaDeadline theo target phase SLA
- Insert ContractApproval row
- `IContractCodeGenerator.GenerateAsync()`:
- 7 format theo ContractType (HĐTP/HĐGK/NCC/HĐDV/MB + 2 framework)
- `BeginTransactionAsync(IsolationLevel.Serializable)` + `ContractCodeSequences` UPSERT → atomic
- Register scoped trong Infrastructure DI
**CQRS (ContractFeatures.cs):**
- `CreateContractCommand` + Validator + Handler (tạo draft, SlaDeadline = UtcNow + 7d)
- `UpdateContractDraftCommand` (chỉ khi Phase = DangSoanThao)
- `TransitionContractCommand` (delegate → WorkflowService)
- `AddCommentCommand` (phase = hiện tại)
- `ListContractsQuery` (PagedResult với filter phase/supplier/project + search)
- `GetMyInboxQuery` (map Phase → roles, return HĐ phù hợp role user)
- `GetContractQuery` (detail + approvals + comments + attachments, resolve user names)
- `DeleteContractCommand` (soft, block > DangInKy)
**Controller:**
- `ContractsController` với 8 endpoint: GET list/inbox/detail, POST create/transition/comment, PUT update, DELETE
- Body DTOs inline: `TransitionContractBody`, `AddCommentBody`
### Chunk I — Frontend 2 app
**fe-admin (2 page mới):**
- `types/contracts.ts` — ContractPhase/Decision const-object + Label + Color maps + types
- `components/PhaseBadge.tsx` — badge màu theo phase
- `pages/contracts/ContractsListPage.tsx` — DataTable full filter + click row → detail
- `pages/contracts/ContractDetailPage.tsx` — 2-col layout: info + comments + timeline + action dialog (approve/reject với select target phase + comment)
**fe-user (4 page mới + port shared):**
- Copy 14 file shared từ fe-admin (menuKeys, types/*, DataTable, PhaseBadge, Dialog, Textarea, Select, apiError, usePermission, PermissionGuard)
- `contexts/AuthContext.tsx` update — load menu from `/menus/me` + localStorage cache
- `components/Layout.tsx` — menu fixed 3 mục (Inbox / Tạo mới / HĐ của tôi) + user info + role display
- `pages/InboxPage.tsx` — list `/api/contracts/inbox` (HĐ chờ role của tôi xử lý, sort theo SLA)
- `pages/contracts/ContractCreatePage.tsx` — form chọn loại HĐ + template + NCC + dự án + giá trị + bypass CĐT
- `pages/contracts/ContractDetailPage.tsx` — duplicate fe-admin pattern (convention)
- `pages/contracts/MyContractsPage.tsx` — HĐ của tôi
- `App.tsx` — 4 route mới
## E2E verified
```bash
# Setup
POST /api/suppliers { code: "PVL2026", type: 1 }201
POST /api/projects { code: "FLOCK 01" }201
# Tạo HĐ
POST /api/contracts { type: 2, supplierId, projectId, giaTri: 150000000 }
201 contractId e20083d5
phase=2 (DangSoanThao), slaDeadline=+7d
# Inbox
GET /api/contracts/inbox → 1 contract
# Full workflow 9 phase (admin bypass)
POST /transitions {targetPhase: 3}204
POST /transitions {targetPhase: 4}204
POST /transitions {targetPhase: 5}204
POST /transitions {targetPhase: 6}204
POST /transitions {targetPhase: 7}204
POST /transitions {targetPhase: 8}204 ← GEN MÃ HĐ
POST /transitions {targetPhase: 9}204
# Final
GET /api/contracts/{id}
→ phase: 9 (DaPhatHanh)
→ maHopDong: "FLOCK 01/HĐGK/SOL&PVL2026/01" ✅ ĐÚNG FORMAT RG-001
7 approvals với đủ from/to/decision/comment
```
TS check fe-admin + fe-user pass.
## Bug gặp + fix
| Bug | Fix |
|---|---|
| Edit tool "File not read" sau system-reminder | Re-read file rồi Write full |
| `Python f-string backslash` trong curl test script | Bỏ escape trong f-string |
## Docs updates
- `.claude/skills/contract-workflow/SKILL.md` — full spec từ placeholder: state machine, role matrix, SLA, RG-001 format 7 loại, code pointers, API, workflow E2E, pitfalls
- Session log này (session #4 trong changelog/sessions)
## Handoff cho session tiếp theo
### Phase 3 iteration 2 (optional — polish)
- [ ] `SlaExpiryJob` BackgroundService (hosted service, 15min interval, auto-approve quá hạn với `Decision=AutoApprove`) — spec ở `docs/flows/sla-expiry-flow.md`
- [ ] Email notification (MailKit) khi chuyển phase
- [ ] In-app notification (SignalR + badge counter)
- [ ] Upload attachment endpoint + FE (multipart, store vào `wwwroot/uploads/contracts/{id}/`)
- [ ] RowVersion optimistic concurrency (2 user cùng duyệt → 409)
- [ ] Render HĐ ra .docx lúc tạo (link với TemplateId + DraftData + merge ContractClause appendix)
- [ ] E2E test permission matrix: tạo user role Drafter thật → verify chỉ thấy nút approve khi ở đúng phase
### Phase 4 — Report + Polish
Xem `docs/changelog/migration-todos.md` section Phase 4.
### Quick wins
- FE Users management (tạo user, gán role) — để test workflow với multi-user
- Filter Inbox theo phase (dropdown FE)
- Export HĐ list ra Excel
### Blocker
-**Gitea remote URL** (vẫn chờ)
## Thông số cumulative
| | Sau Phase 2 | Sau Phase 3 MVP |
|---|---:|---:|
| BE LOC | ~1900 | ~2700 |
| DB tables | 14 | 19 (+Contracts, ContractApprovals, Comments, Attachments, CodeSequences) |
| API endpoints | ~23 | ~31 (+8 contract) |
| Migrations | 4 | 5 |
| FE pages | 7 (fe-admin) + 2 (fe-user) | 9 + 5 |
| Commits | 5 | 6 (sắp) |