# 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) |