[CLAUDE] Docs: chốt session Tier 3 feature-complete + versioned workflow
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m33s

- Session log 2026-04-22-0300 (A→K): attachment, SignalR, form builder,
  PDF, dynamic + versioned workflow, nested menu, 3-panel permissions,
  seed master, brand identity, content polish, Gitea fix
- STATUS: Tier 3 feature-complete snapshot + cumulative stats (24 tables,
  ~50 endpoints, 8 migrations); next-up = UAT + Email SMTP (blocked) +
  rotate creds + SQL backup schedule
- HANDOFF: rewrite brief cho session mới — phase 5 prod done, Tier 3
  đóng gói, quick sanity-check 2 app, versioned workflow quick ref,
  file active hiện trạng, git state
- migration-todos: tick Tier 3 items (attachment/realtime/form builder/
  PDF/dynamic+versioned workflow/nested menu) + thêm iter-3 versioned
  workflow section + post-launch list
- schema-diagram: +5 table (Notifications, WorkflowTypeAssignments,
  WorkflowDefinitions, WorkflowSteps, WorkflowStepApprovers); indexes
  mới, cardinality FK restrict cho pinned policy, truy vấn tiêu biểu
- workflow-contract: +section 7bis resolution order, 7ter admin
  designer flow, updated data model + code pointers Tier 3
- PROJECT-MAP: module map post-Tier-3 (3 box mới Notification/
  Attachment/Branding + Infra/DevOps box), API namespace đầy đủ,
  architectural wins 5 điểm
- contract-workflow skill: versioned workflow section, policy
  resolution code snippet, admin designer flow, code pointers Tier 3,
  tier 4+ backlog
- gotchas +7 bẫy mới (#26-32): SignalR WebSocket headers, interceptor
  2-phase pattern, LibreOffice mirror 404, PS 5.1 UTF-16 GITHUB_PATH,
  PS 5.1 diacritics parse, Dialog size TS, NavLink end query-params

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-22 10:25:02 +07:00
parent 91b2da147f
commit fbca83264c
9 changed files with 1363 additions and 479 deletions

View File

@ -1,28 +1,29 @@
# HANDOFF — Brief 5 phút cho session tiếp theo
**Last updated:** 2026-04-21 16:30 (cuối Phase 5.1 Security + Users Mgmt)
**Last updated:** 2026-04-22 03:00 (post-Tier-3-feature-complete + versioned workflow)
## TL;DR
Tier 3 ERP features xong hết (Attachment, SignalR, Form builder, PDF, Versioned workflow, Nested menu, Permission layout). Prod live 3 domain. **Còn lại chủ yếu là UAT + SMTP + rotate creds**, không còn module kỹ thuật lớn nào chưa làm.
## Ở đâu rồi?
| Phase | Trạng thái |
|---|---|
| 0 Draft | ✅ Done |
| 1 Alpha Core foundation | ✅ Done |
| 1 Alpha Core đợt 2 (CRUD + Permission) | ✅ Done |
| 2 Form Engine MVP | ✅ Done |
| 2 Form Engine iteration 2 | 📝 Optional |
| 3 Workflow MVP (9 phase + code gen) | ✅ Done |
| 3 Workflow iteration 2 (SLA + notify + attachment) | 📝 Optional |
| 4 Report MVP (Dashboard + Excel) | ✅ Done |
| 4 Report iteration 2 | 📝 Optional |
| 5 Prep (infra + scripts + guides + refresh token) | ✅ Done |
| **5.1 Security + Users Mgmt (headers, lockout, Users CRUD)** | ✅ Done (IDOR + deps scan còn) |
| 5 Deploy production (cần Gitea URL) | 📋 Next |
| 1 Alpha Core (foundation + đợt 2 CRUD + Permission) | ✅ Done |
| 2 Form Engine MVP + iter 2 (upload UI + .doc auto-convert + PDF export) | ✅ Done |
| 3 Workflow MVP (9 phase + code gen) + iter 2 (SLA job + attachment + notify) | ✅ Done |
| 4 Report MVP (Dashboard + Excel) + user-specific dashboard | ✅ Done |
| 5 Prep + 5.1 Security + Users Mgmt | ✅ Done |
| **5 Deploy prod** (3 domain HTTPS live) | ✅ Done |
| **Tier 3 (Attach + Realtime + Form builder + PDF + Versioned WF + Nested menu + Permission 3-panel)** | ✅ Done |
| 6+ Post-launch (E-signature, Bravo/SAP, Mobile, AI) | 📝 Future |
## Run nhanh
```powershell
# Terminal 1 — API (auto seed 8 template lần đầu)
# Terminal 1 — API (auto seed 12 role + 9 dept + 5 supplier + 3 project + 8 template + 7 workflow definition + 28 ContractType menu + 7 workflow menu)
dotnet run --project src\Backend\SolutionErp.Api
# Terminal 2 — Admin FE
@ -34,144 +35,218 @@ cd fe-user && npm run dev # → http://localhost:8080
Login: `admin@solutionerp.local` / `Admin@123456`
Điểm cần test ngay (Phase 4 MVP):
- **Admin `/dashboard`** → 5 KPI card + By Phase bar + Monthly chart + Top NCC/dự án
- **Admin `/reports`** → filter phase/date → Export Excel .xlsx 10 cột có formula SUM
- **fe-user `/contracts/new`** → tạo HĐ draft (Phase 2 DangSoanThao, SLA +7d)
- **fe-user `/inbox`** → xem HĐ chờ role mình xử lý
- **`/contracts/{id}`** → click "Duyệt → tiếp" chạy state machine. Phase 8 gen `MaHopDong` RG-001
- **`/forms`** → render template .docx
- **`/system/permissions`** → ma trận Role × MenuKey
- **`/master/suppliers|projects|departments`** → CRUD
## Quick sanity-check
## Cần làm kế tiếp (ưu tiên)
**Admin (:8082):**
- `/dashboard` → "Của tôi" row 4 card + KPI cards + charts
- `/contracts` → list toàn bộ, filter phase/supplier/project
- `/contracts/new?type=5` → tạo HĐ Mua bán, pre-select type từ URL
- `/contracts/{id}` → timeline + action dialog + attachments drag-drop + WorkflowSummaryCard
- `/system/workflows` → 7-card landing (Thầu phụ/Giao khoán/NCC/Dịch vụ/Mua bán/NguyenTacNcc/NguyenTacDv)
- `/system/workflows/MuaBan` → DefinitionCard active + history + "Tạo phiên bản mới" modal với Steps + Approvers (+Role / +User)
- `/system/permissions` → 3-panel layout (Role list | Menu×CRUD matrix | Granted stats)
- `/system/users` → Users CRUD + assign roles
- `/forms` → upload .docx/.xlsx + render dialog Form↔JSON + Tải PDF
### A. Phase 5 — Production (tuần 12-13, item lớn nhất còn lại)
**User (:8080):**
- `/inbox?type=5` → HĐ Mua bán chờ role mình
- `/my-contracts?type=2` → HĐ Thầu phụ của tôi
- `/contracts/new?type=3` → tạo HĐ NCC
- Sidebar nested: 📄 Hợp đồng → expand 7 type → expand "HĐ Mua bán" → Danh sách / Thao tác / Duyệt
**Đọc trước:** `docs/changelog/migration-todos.md` section Phase 5.
**Realtime check:**
- Login 2 tab (admin + user) → user tạo comment / transition → admin nhận toast + bell +1
- [ ] `.gitea/workflows/deploy.yml` CI/CD build + deploy IIS
- [ ] `scripts/deploy-iis.ps1` stop app pool → xcopy → start
- [ ] Windows Server IIS + URL Rewrite + ARR (reverse proxy FE → .NET)
- [ ] HTTPS cert via win-acme hoặc mua
- [ ] `appsettings.Production.json` + user secrets + JWT secret rotation
- [ ] Rate limiting middleware (auth endpoint 5 req/min/IP)
- [ ] Security audit OWASP top 10
- [ ] Health check endpoint `/health`
- [ ] Serilog → file rolling daily retention 30d
- [ ] SQL backup: daily full + 15min log
- [ ] Runbook: restart, rollback migration, restore backup
- [ ] UAT production 1 tuần với 2-3 user thật
- [ ] Go-live checklist
## Cần làm kế tiếp
### B. Polish iterations (optional — khi rảnh)
### A. Hard blockers (chờ user / ops)
**Phase 2 iter 2:** convert 3 .doc qua Word COM `DisplayAlerts=0` hoặc LibreOffice, field spec JSON + form builder FE dynamic, `{{#loop}}` block support, PDF convert, FE upload template multipart.
1. **UAT thật 1 tuần với 2-3 user** — hard requirement từ roadmap. Kiến nghị:
- User A: Drafter (QS/NV.PB) — tạo 3 HĐ mỗi type, đi hết 9 phase
- User B: CCM — duyệt phase 6
- User C: BOD — duyệt phase 7
- Ghi bug / friction / đề xuất → backlog iter 2
2. **SMTP config** để bật Email outbox:
```json
"Email": {
"Host": "smtp.gmail.com",
"Port": 587,
"Username": "...",
"Password": "...",
"From": "noreply@solutionerp.local"
}
```
Khi có → thêm `MailKit`, `IEmailSender`, hook vào `NotificationService.CreateAsync` ngay trước khi enqueue realtime push.
3. **Rotate credentials** — SA SQL password, vrapp password, JWT secret prod, Gitea runner registration token
4. **Schedule SQL backup** — `schtasks /create /tn "SolutionErp Backup" /tr "powershell -File C:\...\scripts\backup-sql.ps1" /sc DAILY /st 03:00`
**Phase 3 iter 2:** `SlaExpiryJob` BackgroundService auto-approve, email (MailKit) + in-app (SignalR) notify, upload attachment endpoint + FE multipart (`wwwroot/uploads/contracts/{id}/`), RowVersion concurrency, render HĐ docx khi tạo (merge TemplateId + DraftData + ContractClause).
### B. Polish iterations (optional — khi UAT phát sinh)
**Phase 4 iter 2:** SLA overdue report by role/phase, PDF HĐ export (LibreOffice), dashboard user-specific (role tôi).
- **Roles CRUD** — admin tạo custom role ngoài 12 hardcoded (`Domain.Identity.AppRoles`)
- **User-kind approver runtime** — data model `WorkflowStepApprover.Kind=User` + `AssignmentValue=userId` đã có, chỉ cần:
```csharp
// ContractWorkflowService.TransitionAsync (bổ sung):
var userApprovers = step.Approvers.Where(a => a.Kind == ApproverKind.User)
.Select(a => Guid.Parse(a.AssignmentValue));
if (userApprovers.Any() && !userApprovers.Contains(actorUserId))
throw new ForbiddenException();
```
- **Grant `Workflows.Read` cho non-admin role** trong PermissionsPage → menu Wf_* auto-visible (inheritance đã có)
- **Warning notification 20% SLA** — job emit khi `SlaDeadline - now < sla * 0.2 && !SlaWarningSent`, set flag
- **Reject → DangSoanThao E2E test** với 3 role khác nhau
- **Deps scan CI** — `dotnet list package --vulnerable` + `npm audit --audit-level=high`
### C. Phase 2 iteration 2 (form engine polish)
### C. Non-goals / parked
- Convert 3 file `.doc` qua Word COM `DisplayAlerts=0` + timeout, hoặc LibreOffice
- Field spec JSON per template + dynamic form builder FE
- `{{#loop}}...{{/loop}}` block support cho table lặp
- PDF convert via LibreOffice headless
- Admin upload template UI (multipart)
### D. Quick wins (không block phase)
- FE Users management + Roles CRUD (test permission với non-admin user thật)
- Filter Inbox theo phase FE
- Refresh token auto (FE axios interceptor retry 401)
- E-signature (VNPT CA / FPT CA) — Phase 6
- Bravo/SAP import NCC — Phase 6
- Mobile app — Phase 6
- AI OCR scan HĐ — Phase 6+
## Lưu ý kỹ thuật quan trọng
**Đọc [`gotchas.md`](gotchas.md) trước khi:**
- Thêm package mới → check compat với .NET 10 (MediatR 14 fail → dùng 12)
- Debug 404 API → kiểm Program.cs có persist không (Dropbox issue)
- Expression tree error → tách switch ra ngoài LINQ
- TS enum error → dùng const-object pattern (`erasableSyntaxOnly`)
- Word COM stuck → kill + fallback LibreOffice
- Migration lỗi → check 3 file đầy đủ (Designer + Migration + Snapshot)
**Đọc [`gotchas.md`](gotchas.md) (26 bẫy) trước khi:**
## File đang active
- Thêm package mới → .NET 10 compat (MediatR 14 fail → dùng 12.4.1)
- Debug TS enum error → dùng const-object pattern (`erasableSyntaxOnly`)
- Expression tree lỗi → tách switch ra ngoài LINQ
- Deploy Windows Feature (WebSockets, etc.) → unlock section ở applicationHost (gotcha #25)
- Workflow transition 403 → check `Contract.WorkflowDefinitionId` pin đúng không
- Migration lỗi → 3 file đầy đủ (Designer + Migration + Snapshot)
## Versioned workflow — quick reference
```
Contract.WorkflowDefinitionId (nullable Guid FK)
→ pin tại `CreateContractCommandHandler` = WorkflowDefinitions.Single(d => d.ContractType == c.Type && d.IsActive)
→ ContractWorkflowService.LoadAsync(contractId):
if contract.WorkflowDefinitionId != null:
def = db.WorkflowDefinitions.Include(Steps.Approvers).First(wfId)
return WorkflowPolicyRegistry.FromDefinition(def)
else if admin override ở WorkflowTypeAssignments:
return Registry.ByName(override.PolicyName)
else:
return Registry.For(contract.Type) // hardcoded Standard/SkipCcm
Admin tạo version mới:
POST /api/workflows
body: { code, name, contractType, steps: [{ order, phase, name, slaDays, approvers: [{ kind, assignmentValue }] }] }
→ auto increment Version = max(Version where Code==code) + 1
→ deactivate old IsActive trong cùng ContractType (atomic)
→ HĐ cũ ĐÃ PIN WorkflowDefinitionId = old Id → vẫn chạy policy cũ ✓
```
Invariants:
- `UNIQUE (Code, Version)` per WorkflowDefinitions
- **Chỉ 1 IsActive=true per ContractType** tại 1 thời điểm
- `Contract.WorkflowDefinitionId` KHÔNG cascade khi xóa WorkflowDefinition → protect history
## File đang active (hiện trạng)
```
SOLUTION_ERP/
├── src/Backend/ (Clean Arch, 4 project, .NET 10)
├── src/Backend/ (Clean Arch, 4 project, .NET 10)
│ ├── SolutionErp.Domain/
│ │ ├── Common/ BaseEntity, AuditableEntity
│ │ ├── Contracts/ ContractType, ContractPhase, ApprovalDecision + **Contract, ContractApproval, ContractComment, ContractAttachment, ContractCodeSequence** ← Phase 3
│ │ ├── Forms/ ContractTemplate, ContractClause ← Phase 2
│ │ ├── Identity/ User, Role, MenuItem, Permission, AppRoles, MenuKeys
│ │ └── Master/ Supplier, Project, Department, SupplierType
│ │ ├── Common/ BaseEntity, AuditableEntity
│ │ ├── Contracts/ ContractType, ContractPhase, ApprovalDecision,
│ │ Contract (+WorkflowDefinitionId), ContractApproval,
│ │ │ ContractComment, ContractAttachment, ContractCodeSequence,
│ │ │ **WorkflowPolicy** (record + registry + FromDefinition),
│ │ │ **WorkflowDefinition** (Code+Version+IsActive+ContractType),
│ │ │ **WorkflowStep** (Order+Phase+Name+SlaDays),
│ │ │ **WorkflowStepApprover** (Kind=Role|User, AssignmentValue),
│ │ │ **WorkflowTypeAssignment** (admin override legacy)
│ │ ├── Forms/ ContractTemplate (+FieldSpec JSON), ContractClause
│ │ ├── Identity/ User, Role, MenuItem, Permission, AppRoles,
│ │ │ **MenuKeys** (+ContractTypeCodes, ContractTypeGroup/List/Create/Pending helpers, WorkflowTypeLeaf)
│ │ ├── Master/ Supplier (+SupplierType), Project, Department
│ │ └── Notifications/ **Notification** (+NotificationType enum)
│ ├── SolutionErp.Application/
│ │ ├── Auth/ Login, Refresh, Me
│ │ ├── Common/ Exceptions, Behaviors, Interfaces, Models
│ │ ── Contracts/ ContractFeatures (8 CQRS), IContractWorkflowService, IContractCodeGenerator, DTOs ← Phase 3
│ │ ├── Forms/ FormFeatures (List/Get/Render) ← Phase 2
│ │ ├── Master/ Suppliers, Projects, Departments CQRS
│ │ ├── Permissions/ GetMyMenuTree, matrix upsert
│ │ └── Reports/ **DashboardStats, ExportContractsToExcel, IContractExcelExporter** ← Phase 4
│ │ ├── Auth/ Login, Refresh, Me
│ │ ├── Common/
│ │ ── Interfaces/ IApplicationDbContext, ICurrentUser, IDateTime,
│ │ │ IJwtTokenService, **IFileStorage**, **IDocumentConverter**,
│ │ │ **IRealtimeNotifier**, **INotificationService**
│ │ ├── Contracts/ ContractFeatures, IContractWorkflowService,
│ │ │ **ContractAttachmentFeatures** (Upload/Download/Delete CQRS),
│ │ │ **WorkflowAdminFeatures** (Overview + CreateNewVersion)
│ │ ├── Forms/ FormFeatures (List/Get/Render/**Upload/Update/Delete/ExportPdf**)
│ │ ├── Master/ Suppliers, Projects, Departments CQRS
│ │ ├── **Notifications/** NotificationFeatures (List/UnreadCount/MarkRead/MarkAllRead)
│ │ ├── Permissions/ GetMyMenuTree (**+inherit Contracts/Workflows**)
│ │ └── Reports/ DashboardStats, ExportToExcel, **MyDashboard**
│ ├── SolutionErp.Infrastructure/
│ │ ├── Forms/ DocxRenderer, XlsxRenderer, FormRenderer ← Phase 2
│ │ ├── Identity/ JwtSettings, JwtTokenService
│ │ ├── Persistence/ DbContext, DbInitializer, Interceptors, Migrations (**5**)
│ │ ├── Reports/ **ContractExcelExporter** ← Phase 4
│ │ └── Services/ DateTimeService, ContractWorkflowService, ContractCodeGenerator ← Phase 3
│ │ ├── Forms/ DocxRenderer, XlsxRenderer, FormRenderer,
│ │ │ **LibreOfficeDocumentConverter**
│ │ ├── Identity/ JwtSettings, JwtTokenService
│ │ ├── Persistence/
│ │ │ ├── Interceptors/ AuditingInterceptor, **NotificationPushInterceptor**
│ │ │ └── Migrations/ 8 migrations
│ │ ├── Reports/ ContractExcelExporter
│ │ ├── **Storage/** LocalFileStorage (path-traversal guard)
│ │ └── Services/ DateTimeService, **ContractWorkflowService (load pinned def)**,
│ │ ContractCodeGenerator, **NotificationService**
│ └── SolutionErp.Api/
│ ├── Authorization/ MenuPermissionHandler + Requirement
│ ├── Controllers/ Auth, Suppliers, Projects, Departments, Menus, Roles, Permissions, Forms, Contracts, **Reports** (10 controller)
├── Middleware/ GlobalExceptionMiddleware
├── Services/ CurrentUserService, WebHostEnvironmentLocator
── wwwroot/templates/ 5 file .docx/.xlsx ← Phase 2
├── fe-admin/ (11 page)
└── src/pages/
│ ├── LoginPage
── DashboardPage ← Phase 4 rewrite (KPI cards + BarChart)
│ ├── master/SuppliersPage, ProjectsPage, DepartmentsPage
│ ├── system/PermissionsPage
│ ├── forms/FormsPage ← Phase 2
├── contracts/ContractsListPage, ContractDetailPage ← Phase 3
└── ReportsPage ← Phase 4
├── fe-user/ (5 page)
└── src/pages/
├── LoginPage
├── InboxPage ← Phase 3
── contracts/ContractCreatePage, ContractDetailPage, MyContractsPage ← Phase 3
├── docs/ (35 file)
├── STATUS.md, HANDOFF.md, rules.md, architecture.md
├── CLAUDE.md, PROJECT-MAP.md
├── workflow-contract.md, forms-spec.md
├── database/{database-guide, schema-diagram}.md
│ ├── Authorization/ MenuPermissionHandler + Requirement
│ ├── Controllers/ Auth, Suppliers, Projects, Departments, Menus,
│ Roles, Permissions, Forms, Contracts, Reports,
│ Users, **Notifications**, **Workflows**
── **Hubs/** NotificationHub (/hubs/notifications)
│ ├── Middleware/ GlobalExceptionMiddleware
├── **Realtime/** SignalRNotifier
│ ├── Services/ CurrentUserService, WebHostEnvironmentLocator
── wwwroot/templates/ .docx/.xlsx templates
├── fe-admin/ (~18 page)
│ └── src/
│ ├── pages/
│ ├── LoginPage, DashboardPage (MyDashboardRow)
│ ├── master/ Suppliers, Projects, Departments
│ │ ├── system/ **PermissionsPage (3-panel)**, **WorkflowsPage (URL-driven)**, Users
│ ├── forms/ FormsPage (upload + Form/JSON + PDF)
│ ├── contracts/ List, Detail (+Attachments), **Create**
│ └── ReportsPage
── components/ Layout (recursive menu + filterForAdmin),
TopBar, NotificationBell, UserMenu, SlaTimer,
│ EmptyState, PhaseBadge, WorkflowSummaryCard,
│ ContractAttachmentsSection, DynamicForm,
│ **WorkflowDesigner** (Steps + Approvers modal)
└── lib/ api.ts, realtime.ts, cn.ts
├── fe-user/ (~10 page)
│ └── src/
│ ├── pages/ Login, Inbox (+?type filter),
│ │ contracts/{Create, Detail, MyContracts}
│ ├── components/ Layout (recursive + filterForUser + USER_FIXED_TOP),
│ │ NotificationBell, ContractAttachmentsSection, SlaTimer
│ └── lib/ realtime.ts (same singleton pattern)
├── docs/ (~40 file)
│ ├── STATUS, HANDOFF, rules, architecture, CLAUDE, PROJECT-MAP (6)
│ ├── workflow-contract, forms-spec (2)
│ ├── database/{database-guide, schema-diagram} (2)
│ ├── flows/ (7 file — README + 6 flow)
│ ├── guides/ (4 file) — deployment-iis, cicd, security-checklist, runbook ← Phase 5 prep
│ ├── changelog/migration-todos.md + sessions/ (7 session log)
│ └── gotchas.md
├── scripts/ (5 file PS + py)
│ ├── parse_forms.py, parse_workflow.py (Phase 0)
│ ├── convert-doc-to-docx.ps1 (Phase 2)
── deploy-iis.ps1, backup-sql.ps1 ← Phase 5 prep
├── .gitea/workflows/deploy.yml ← Phase 5 prep CI/CD template
── .claude/skills/ (3 skill — all full spec)
│ ├── guides/ (4 file)
│ ├── changelog/migration-todos + sessions/ (8 session log)
│ └── gotchas (26 pitfall)
├── scripts/ (5 PS + py)
│ ├── parse_forms, parse_workflow (Phase 0)
│ ├── convert-doc-to-docx (Phase 2)
── deploy-iis, backup-sql (Phase 5)
│ └── install-libreoffice (Tier 3)
── .gitea/workflows/deploy.yml CI/CD Windows self-hosted runner
└── .claude/skills/ 3 skill (contract-workflow, form-engine, permission-matrix)
```
## Git state
```
(sẽ là commit 8) — Phase 5 Prep (infra + scripts + guides + refresh token)
fe7ad8e — Phase 4 Report MVP + docs consolidation
7e957a7 — Phase 3 Workflow MVP
5113e4c — Phase 2 Form Engine MVP
54d6c9b — Phase 1.2 CRUD + Permission
49a5f57 — Docs database-guide + flows
702411f — Phase 1 foundation
25dad7f — Phase 0 scaffold
HEAD → main
91b2da1 — PermissionsPage 3-panel layout (LATEST)
f216169 — Admin Workflows tabs → sidebar menu items
355bbe3 — Fix Dialog size TS (xl → lg)
e7e5f2d — Versioned workflow entities + migration + designer
4 session trước đó nằm trong STATUS table
Branch: main
Remote: chưa (Gitea URL CẦN NGAY để Phase 5 go-live)
Branch: main (tracking origin/main)
Remote: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp.git
```
## Credentials + URLs
@ -180,23 +255,27 @@ Remote: chưa (Gitea URL CẦN NGAY để Phase 5 go-live)
admin@solutionerp.local / Admin@123456
```
- API: http://localhost:5443 (swagger `/swagger`)
- Admin FE: http://localhost:8082
- User FE: http://localhost:8080
- SQL LocalDB: `(localdb)\MSSQLLocalDB` / Database=`SolutionErp_Dev`
- API prod: https://api.huypham.vn — `/health/live`, `/health/ready`
- Admin FE prod: https://admin.huypham.vn
- User FE prod: https://user.huypham.vn
- API dev: http://localhost:5443 — `/swagger` (Dev only)
- Admin FE dev: http://localhost:8082
- User FE dev: http://localhost:8080
- SQL dev: `(localdb)\MSSQLLocalDB` / `SolutionErp_Dev`
- SQL prod: `.\SQLEXPRESS` / `SolutionErp` / `vrapp` (⚠ rotate)
## Đánh giá nhanh
**Tốt:**
- Build pass 100% cả BE + FE
- E2E test full 9-phase workflow end-to-end — mã HĐ gen đúng format RG-001
- Docs đầy đủ: 26 file, session log mỗi chunk, gotchas tích lũy 17 pitfall
- Cả 2 FE đều có Contract detail page + timeline + comment thread + state machine action
- 3 domain HTTPS prod live, CI/CD xanh
- Tier 3 feature-complete: attachment, realtime, form builder (upload + DynamicForm + PDF), versioned workflow (admin-configurable per ContractType, pin per contract), nested menu per type, 3-panel permissions
- Clean-arch 3-project split đúng cho 2 cross-cutting service (realtime + document-converter)
- 26 gotchas tích lũy, 8 session log, 40 docs agent onboard nhanh
- Invariant critical: " giữ quy trình " guaranteed by pinning (reference-based immutability, không snapshot copy)
**Rủi ro còn:**
- SLA chỉ set deadline — không có job auto-approve (Phase 3.2)
- Không có notification (email + in-app) — user phải F5 inbox
- Form render chỉ MVP — loop table + PDF chưa có
- Permission matrix chưa test thực với non-admin user
- 3 file .doc chưa convert (carryover Phase 2)
- Không có upload attachment endpoint (chỉ có entity + DTO)
- UAT thật chưa chạy thể phát hiện edge case missing
- SMTP chưa notification chỉ in-app (toast + bell), không email
- User-kind approver chưa enable guard runtime (designer cho pick, nhưng transition dùng Role fall-back)
- Credentials chưa rotate
- SQL backup chưa schedule Task Scheduler

View File

@ -2,153 +2,240 @@
> Đọc file này nếu cần deep context (~15 phút). Nếu chỉ cần snapshot → đọc [`STATUS.md`](STATUS.md).
## Module map
## Module map (hiện trạng post-Tier-3)
```
┌─────────────────────────────────────────────────────────────────┐
│ SOLUTION_ERP │
│ 🌐 Prod live: api/admin/user.huypham.vn (HTTPS Let's Encrypt) │
└─────────────────────────────────────────────────────────────────┘
╔════════════════╗ ╔════════════════╗ ╔════════════════╗
║ IDENTITY ║ ║ DANH MỤC ║ ║ QUẢN LÝ HĐ ║
║ (Phase 1) ║ ║ (Phase 1) ║ ║ (Phase 1-3)
║ (Phase 1) ║ ║ (Phase 1) ║ ║ (Phase 1-3)
╠════════════════╣ ╠════════════════╣ ╠════════════════╣
║ User ║ ║ Supplier ║ ║ Contract ║
║ Role ║ ║ Project ║ ║ ContractType
║ Permission ║ ║ Department ║ ║ ContractForm
MenuKey ║ ║ ContractClause ║ ║ Approval
AuditLog ║ ║ (điều kiện ║ ║ Comment
chung - 002.04)║ ║ Attachment
╚════════════════╝ ╚════════════════╝ ║ AuditTrail ║
╚════════════════╝
║ User (+Mgmt) ║ ║ Supplier ║ ║ Contract ║
║ Role (12 seed) ║ ║ Project ║ ║ + WorkflowDefId
║ Permission ║ ║ Department ║ ║ Approval
(3-panel UI) ║ + seed demo ║ ║ Comment
MenuKey ║ ║ ContractClause ║ ║ Attachment
+ nested tree ║ ║ (E2E upload)
╚════════════════╝ ╚════════════════╝ ╚════════════════╝
╔════════════════╗ ╔════════════════╗ ╔════════════════╗
║ FORM ENGINE ║ ║ WORKFLOW ║ ║ BÁO CÁO ║
║ (Phase 2) ║ ║ (Phase 3) ║ ║ (Phase 4)
║ (Phase 2) ║ ║ (Phase 3) ║ ║ (Phase 4)
╠════════════════╣ ╠════════════════╣ ╠════════════════╣
║ Template ║ ║ StateMachine ║ ║ Dashboard ║
Renderer ║ ║ Transition ║ ║ ExcelExport ║
║ (DOCX/XLSX) ║ ║ SlaTimer ║ ║ FilterQuery
║ Field mapping ║ ║ Notification ║ ║
║ PO gen (F.07) ║ ║ CodeGenerator ║ ║ ║
║ ║ (RG-001) ║ ║ ║
║ Template CRUD ║ ║ StateMachine ║ ║ Dashboard ║
DynamicForm ✅ ║ ║ Transition ║ ║ ExcelExport ║
║ (DOCX/XLSX) ║ ║ SlaTimer ║ ║ MyDashboard ✅
║ FieldSpec JSON ║ ║ SlaExpiryJob ✅ ║ ║ (role-aware)
║ PDF export ✅ ║ ║ CodeGen RG-001 ║ ║ ║
(LibreOffice) ║ **Versioned ✅**║ ║ ║
║ .doc auto-conv ║ ║ (admin design)║ ║ ║
╚════════════════╝ ╚════════════════╝ ╚════════════════╝
╔════════════════╗ ╔════════════════╗ ╔════════════════╗
║ NOTIFICATION ║ ║ ATTACHMENT ║ ║ BRANDING ║
║ (Tier 3) ✅ ║ ║ (Tier 3) ✅ ║ ║ (Tier 3) ✅ ║
╠════════════════╣ ╠════════════════╣ ╠════════════════╣
║ Notification ║ ║ IFileStorage ║ ║ #1F7DC1 palette║
║ SignalR Hub ║ ║ LocalFileStore ║ ║ Be Vietnam Pro ║
║ Auto-push ║ ║ Path traversal ║ ║ Solutions logo ║
║ interceptor ║ ║ guard ║ ║ ERP shell ║
║ Toast + Bell ║ ║ 3 endpoint ║ ║ (TopBar + Bell║
║ badge ║ ║ REST + FE ║ ║ + UserMenu) ║
║ Email (TODO) ║ ║ drag-drop ║ ║ ║
╚════════════════╝ ╚════════════════╝ ╚════════════════╝
╔════════════════════════════════════════════════════════════╗
║ INFRA / DEVOPS (Phase 5) ✅ ║
╠════════════════════════════════════════════════════════════╣
║ IIS 3 sites (Api/Admin/User) + URL Rewrite + ARR ║
║ win-acme 3 Let's Encrypt cert + auto-renew ║
║ Gitea Actions CI/CD (Windows self-hosted runner) ║
║ SQL Server 2019 SQLEXPRESS + scripts/backup-sql.ps1 ║
║ LibreOffice 25.8.6 headless (PDF/docx converter) ║
║ Health check /health/live + /health/ready ║
║ Serilog file rolling daily retention 30d ║
║ Security headers + HSTS + rate limit 300/min global ║
╚════════════════════════════════════════════════════════════╝
```
## Domain entities chính (dự kiến)
## Domain entities chính (implemented)
```
User ────< Role ────< Permission (Role × MenuKey × CRUD)
User ────< AuditLog
User ────< UserRoles >──── Role ────< Permission (Role × MenuKey × CRUD)
Supplier (NCC)
MenuItem ─┬─ Parent-child tree (Contracts → Ct_MuaBan_* → ...)
├─ 28 Ct_* leaves (7 type × 3 action + 7 group)
└─ 7 Wf_* leaves for workflow admin
Supplier (NCC / NTP / TD / DVDV / CDT)
Project (Dự án)
Department (Phòng ban)
Department (9 phòng từ QT docx)
Contract
├── Type: HĐTP | HĐGK | NCC | HĐDV | HĐ Mua bán | ...
├── Phase (9 state — xem workflow-contract.md)
├── Supplier, Project, Drafter
├── MaHopDong (gen theo RG-001)
├── Approvals[] (audit ai ký phase nào)
├── Comments[] (thread góp ý Phase 3 của workflow)
├── Attachments[] (scan bản gốc, file export)
── TemplateData (JSON — field đã điền khi render form)
├── Type: HĐTP | HĐGK | NCC | HĐDV | HĐ Mua bán | Nguyên tắc NCC | Nguyên tắc DV
├── Phase (9 state)
├── WorkflowDefinitionId (pinned policy at create-time)
├── Supplier, Project, Drafter, Template
├── MaHopDong (gen theo RG-001 khi DangDongDau)
├── SlaDeadline + SlaWarningSent flag
├── Approvals[] (history)
── Comments[] (thread)
└── Attachments[] (scan + upload)
ContractTemplate (ánh xạ Type → File mẫu FO-002.xx)
ContractClause (điều khoản chung FO-002.04 — rich text)
PurchaseOrder (có thể đính với Contract hoặc standalone)
WorkflowDefinition (versioned per ContractType)
├── Code (QT-MB, QT-TP, ...) + Version (v01, v02, ...)
├── IsActive (max 1 per ContractType)
└── Steps[]
├── Order + Phase + Name + SlaDays
└── Approvers[] (Kind=Role|User + AssignmentValue)
WorkflowTypeAssignment (legacy admin override — fall back khi chưa có WorkflowDefinition)
ContractTemplate (FormCode + ContractType + FieldSpec JSON + StoragePath)
ContractClause (điều khoản chung FO-002.04)
ContractCodeSequence (Prefix → LastSeq, atomic gen)
Notification (RecipientUserId + Type + Title + Body + Link + IsRead)
```
## API namespace dự kiến
## API namespace (implemented)
```
/api/auth — login, refresh, logout, register (admin gate)
/api/users — CRUD user, assign role, reset password
/api/roles — CRUD role, permission matrix
/api/menus — menu tree + permission resolution
/api/auth — login, refresh, me, logout
/api/users — CRUD + assign roles + reset password + unlock + toggle active
/api/roles — list (CRUD Create/Rename/Delete: TODO)
/api/menus /me tree với inherit Contracts/Workflows
/api/suppliers — CRUD NCC
/api/projects — CRUD dự án
/api/departments — CRUD phòng ban
/api/suppliers — CRUD NCC
/api/projects — CRUD dự án
/api/departments — CRUD phòng ban
/api/permissions — get matrix + bulk upsert
/api/contracts — CRUD + query by phase/project/supplier
/api/contracts/{id}/transitions — state machine action
/api/contracts — CRUD + query by phase/supplier/project/type + pendingMe
/api/contracts/inbox — HĐ chờ role tôi
/api/contracts/{id} — detail + pinned WorkflowDefinition policy
/api/contracts/{id}/transitions — state machine action (role guard + versioned policy)
/api/contracts/{id}/comments — thread góp ý
/api/contracts/{id}/attachments — upload/download
/api/contracts/{id}/attachments — upload (multipart) + download stream + delete
/api/forms — template catalog
/api/forms/{id}/render — render template → docx/xlsx (Phase 2)
/api/forms — CRUD templates (upload/update/delete + render + PDF export)
/api/forms/templates/{id}/export-pdf — LibreOffice conversion
/api/reports/dashboard — KPI tổng hợp
/api/reports/export — Excel download
/api/workflows — admin GET overview + POST create new version
/api/workflows/{type} — per-type definition + history
/api/notifications — list + unread count + mark-read + mark-all-read
/api/reports/dashboard — KPI tổng hợp
/api/reports/my-dashboard — role-aware user-specific stats
/api/reports/export — Excel download
/hubs/notifications — SignalR hub (JWT qua ?access_token=)
/health/live + /health/ready — health check
```
## FE screens dự kiến
## FE screens (implemented)
### fe-admin (:8082) — cho Admin + Role quản lý
- `/login`
- `/dashboard`KPI system
- `/master/users` + `/master/roles` + `/master/permissions`
- `/master/suppliers` + `/master/projects` + `/master/departments`
- `/master/contract-templates` + `/master/contract-clauses`
- `/contracts`danh sách toàn bộ, filter phase/dự án
- `/contracts/{id}` — detail + approval panel + audit log
- `/reports` + `/system/audit-log`
- `/dashboard`MyDashboard row (4 card) + KPI + charts
- `/master/suppliers|projects|departments` — CRUD
- `/system/users` — Users Mgmt (create/reset/unlock/assign-roles/toggle-active)
- `/system/permissions`**3-panel layout** (Role list | Menu×CRUD matrix | Granted stats)
- `/system/workflows`landing 7-card grid per ContractType
- `/system/workflows/:typeCode` — Definition card (active + history) + Designer modal
- `/forms` — list + upload + update + delete + render dialog (Form↔JSON toggle) + Tải PDF
- `/contracts` — list full + filter phase/supplier/project/type
- `/contracts/new` — Create (pre-select from `?type=X`)
- `/contracts/:id` — detail + timeline + action dialog + **Attachments section** + WorkflowSummaryCard
- `/reports` — filter + export Excel
### fe-user (:8080) — cho Drafter, TBP, PD/PM, BOD, CCM, ...
Sidebar: nested menu + `filterForAdmin` hide `Ct_*`, keep `Wf_*` for admin
### fe-user (:8080) — cho Drafter, TBP, PD/PM, BOD, CCM, HRA, ...
- `/login`
- `/inbox` — HĐ đang chờ tôi xử lý (filter theo role × phase)
- `/contracts/new`chọn template + điền form + submit
- `/contracts/{id}`detail, comment, approve/reject
- `/my-contracts`HĐ tôi đã tạo/tham gia
- `/inbox` — HĐ chờ role tôi xử lý (lọc theo `?type=X`)
- `/my-contracts`HĐ tôi đã tạo/tham gia (lọc theo `?type=X`)
- `/contracts/new`tạo HĐ draft (pre-select type)
- `/contracts/:id`detail + duyệt/comment + Attachments drag-drop
## Flow chính (Phase 3 — trình ký HĐ end-to-end)
Sidebar: nested 3-level, `filterForUser` hide admin items, hiển thị 7 ContractType × 3 action
## Flow chính (Phase 3 end-to-end, Tier 3 versioned)
```
Drafter (QS/NV.PB)
├─ [POST /api/contracts] tạo draft + chọn template
├─ [POST /api/forms/{id}/render] fill + preview
Drafter (QS/NV.PB) ← pin WorkflowDefinitionId = v02 active
├─ [POST /api/contracts] tạo draft Phase=DangSoanThao, pin v02
├─ [POST /api/forms/templates/{id}/render] fill FieldSpec + preview
├─ (upload scan đính kèm qua /attachments)
├─ [POST /api/contracts/{id}/transitions] DangSoanThao → DangGopY
│ BE: load wfDef v02 → FromDefinition → Registry policy → guard (role + from/to)
│ → Notification push tới all PD/PM/PRO/CCM/FIN/ACT + SignalR toast
PD/PM/PRO/CCM/FIN/ACT (song song)
└─ [POST /api/contracts/{id}/comments] góp ý
PD/PM/PRO/CCM/FIN/ACT (song song)
└─ [POST /api/contracts/{id}/comments] góp ý → Notification push Drafter
Drafter
├─ [PATCH /api/contracts/{id}] revise
├─ [POST /api/contracts/{id}/transitions] DangGopY → DangDamPhan → DangInKy
├─ [POST .../transitions] DangGopY → DangDamPhan → DangInKy
│ (BypassProcurementAndCCM=true → skip CCM → DangInKy → DangTrinhKy)
NTP/NCC/TĐ (external — Drafter update thay)
└─ upload scan có chữ ký đối tác
Drafter
└─ [POST /api/contracts/{id}/transitions] → DangKiemTraCCM
Drafter → [transitions] → DangKiemTraCCM
CCM → [transitions] → DangTrinhKy
BOD → [transitions] → DangDongDau
└─ BE: ContractCodeGenerator SERIALIZABLE → gen MaHopDong RG-001
CCM
└─ [POST /api/contracts/{id}/transitions] → DangTrinhKy
BOD/NĐUQ
└─ [POST /api/contracts/{id}/transitions] → DangDongDau (GEN mã HĐ ở đây!)
HRA
└─ [POST /api/contracts/{id}/transitions] → DaPhatHanh (upload scan có dấu)
HRA → [transitions] → DaPhatHanh (final)
```
## External dependencies
## External dependencies (hiện trạng)
- **SQL Server** — chính thức, dev có thể LocalDB hoặc Docker (`docker-compose.yml`)
- **IIS** — deploy target (Windows Server 2019+)
- **Gitea** — git remote (URL chờ user cấp)
- **Aspose.Words / DocumentFormat.OpenXml** — render .docx (Phase 2, quyết định khi đó)
- **EPPlus hoặc ClosedXML** — render .xlsx (Phase 2)
- **SQL Server** — prod SQLEXPRESS trên VPS, dev LocalDB hoặc Docker
- **IIS** — Windows Server (VPS shared VIETREPORT), URL Rewrite + ARR + WebSockets module
- **Gitea** — https://git.baocaogiaoduc.vn (self-hosted, shared runner)
- **win-acme** — Let's Encrypt auto-renew
- **LibreOffice 25.8.6** — PDF / docx / xlsx conversion (soffice headless)
- **DocumentFormat.OpenXml** — render .docx (Phase 2)
- **ClosedXML** — render .xlsx + Excel export (Phase 4)
- **MediatR 12.4.1** — CQRS mediator (pin, 14 breaking)
- **@microsoft/signalr 8.0.7** — FE realtime client
- **Be Vietnam Pro** — Google Fonts (Vietnamese diacritics)
## Non-goals (KHÔNG làm)
- ❌ Python AI service (user đã quyết gác vô thời hạn)
- ❌ Mobile app
- ❌ Mobile app (React Native) — Phase 6+
- ❌ Multi-tenant (1 instance / 1 công ty)
- ❌ Tích hợp e-signature (Phase 6+ nếu có)
- ❌ Tích hợp SAP/Bravo ERP (Phase 6+ nếu có)
- ❌ Tích hợp e-signature (VNPT/FPT CA) — Phase 6+
- ❌ Tích hợp SAP/Bravo ERP Phase 6+
- ❌ Public API / external webhooks
## Architectural wins (Tier 3)
1. **Versioned workflow via Contract.WorkflowDefinitionId pin** — zero-cost immutability guarantee.
HĐ cũ protected from policy changes by REFERENCE (FK restrict), không phải snapshot copy.
2. **Permission inheritance via menu ancestry** — Contracts + Workflows roots inherit CanRead xuống
descendant Ct_* / Wf_* nodes. Không cần per-leaf permission rows → Permissions table nhỏ gọn.
3. **3-project clean-arch split cho cross-cutting services** (SignalR realtime + LibreOffice converter):
- Abstraction ở Application (`IRealtimeNotifier`, `IDocumentConverter`)
- Implementation ở Api / Infrastructure
- Caller (Application handlers) KHÔNG depend transport / framework
4. **SaveChangesInterceptor auto-push notifications**`NotificationPushInterceptor` capture Added
Notifications ở SavingChanges, push via `IRealtimeNotifier` ở SavedChanges. Zero caller changes
khi CQRS handler chỉ `db.Notifications.Add(n)`.
5. **URL-driven admin UI (workflows per-type)** — thay tabs bằng sidebar menu items + URL param.
Linkable, bookmarkable, mỗi type có permission leaf riêng qua `Wf_<Code>`.

View File

@ -2,11 +2,12 @@
> **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-04-21 15:30 (post-prod-deploy)
**Last updated:** 2026-04-22 03:00 (post-Tier-3-feature-complete + versioned workflow)
## 📍 Phase hiện tại: **Đã go-live prod** — 3 domain HTTPS live, CI/CD xanh, Notifications module + ERP shell
## 📍 Phase hiện tại: **Tier 3 feature-complete** — Prod live, tất cả module lớn xong. Còn: UAT thật + Email outbox (chờ SMTP) + rotate creds.
### 🌐 Production URLs
- https://api.huypham.vn — API (Let's Encrypt, auto-renew via win-acme)
- https://admin.huypham.vn — Admin FE (HTTP→HTTPS auto-redirect)
- https://user.huypham.vn — User FE (HTTP→HTTPS auto-redirect)
@ -15,125 +16,106 @@
## 🔥 In Progress
_(không có — chờ UAT + quyết Tier 3 tiếp theo)_
_(không có — Tier 3 đóng gói xong, chờ UAT để quyết Tier 4)_
## ✅ Recently Done (newest on top)
| Ngày | Ai | Task | Commit |
|---|---|---|---|
| 2026-04-22 | Claude | **Versioned workflow per ContractType** — Domain: WorkflowDefinition (Code+Version+IsActive) + WorkflowStep + WorkflowStepApprover (Role/User). Contract.WorkflowDefinitionId pin tại create. EF migration AddVersionedWorkflows + AddWorkflowTypeAssignments. Seed v01 per 7 ContractType từ hardcoded policies. ContractWorkflowService.FromDefinition build policy runtime từ DB. Admin `/system/workflows` tabs per type, DefinitionCard + Designer modal (add/remove step, pick phase/SLA, +Role hoặc +User approvers). POST /api/workflows tạo v02 → v01 auto-archive (HĐ cũ vẫn chạy v01). E2E verified: seed 7 v01, create QT-MB-v02, new HĐ Mua bán pin v02 `policyName:"QT-MB-v02"` activePhases [2,3,7,8,9,99] | `e7e5f2d` + `355bbe3` |
| 2026-04-21 | Claude | **Nested sidebar menu fe-user** — 7 ContractType × 3 actions (Danh sách/Thao tác/Duyệt), nested 3-level. Admin hide Ct_*. Layout recursive MenuNodeRenderer. MyContracts + Inbox filter `?type=X` | `5e0f380` |
| 2026-04-21 | Claude | **Seed master data + MyDashboard widgets** — DbInitializer seed 9 departments từ QT docx (PM/QS/CCM/PRO/FIN/ACT/EQU/HRA/BOD) + 5 demo suppliers + 3 demo projects idempotent. MyDashboard endpoint `/api/reports/my-dashboard` role-aware: DraftsInProgress / PendingMyApproval / DueSoon / Overdue. FE DashboardPage "Của tôi" row 4 card hover-interactive, Admin auto-hide nếu = 0. E2E verified 9 dept seeded, endpoint trả data thật | `6197c84` |
| 2026-04-21 | Claude | **Dynamic workflow policy per ContractType** — Domain `WorkflowPolicy` + registry (Standard 8-phase cho Thầu phụ/Giao khoán/NCC; SkipCcm 7-phase cho Dịch vụ/Mua bán/Nguyên tắc). `ContractWorkflowService` dùng `policy.ForContract(c)` thay hardcoded dict. FE xóa `NEXT_PHASES` hardcoded, dùng `contract.workflow.nextPhases` từ BE. `WorkflowSummaryCard` timeline visual. E2E verified: HĐ Thầu phụ có phase 6 CCM, HĐ Mua bán skip. Gotcha #21 resolved | `cae4d84` |
| 2026-04-21 | Claude | **PDF export + .doc/.xls auto-convert + DynamicForm** — LibreOffice 25.8.6 install VPS, `IDocumentConverter` shell soffice `--convert-to pdf/docx/xlsx` với timeout+temp isolation, admin upload .doc auto-convert .docx. `DynamicForm` component render từ FieldSpec JSON (text/textarea/number/date/currency/select). FE Form↔JSON toggle. E2E verified PDF 488KB/126 pages | `e459097` + `6bbd894` |
| 2026-04-21 | Claude | **Form template builder CRUD** — Admin tự upload `.docx/.xlsx` templates qua UI (không cần dev). BE: UploadContractTemplate (multipart, 10MB, FormCode regex unique, FieldSpec JSON validation) + UpdateContractTemplate (metadata + FieldSpec + IsActive) + DeleteContractTemplate (soft via IsActive=false). File lưu vào `wwwroot/templates/{formCode}_{guid}.{ext}`. FE: FormsPage với upload dialog (file picker + FormCode + Loại HĐ + FieldSpec JSON textarea) + row actions 3 nút (render/edit/delete). E2E verified upload 200 + update 204 + delete 204 | `166d26c` |
| 2026-04-21 | Claude | **Fix Gitea 500 sau Install Web-WebSockets** — Feature install khóa section `<webSocket>` ở applicationHost.config → tất cả IIS site fail. Fix: `appcmd unlock config -section:system.webServer/webSocket`. Gotcha #25 | `c52186b` |
| 2026-04-21 | Claude | **SignalR realtime notifications E2E** — Clean-arch split (IRealtimeNotifier Application + SignalRNotifier Api + NotificationPushInterceptor Infrastructure). Hub `/hubs/notifications` JWT via `?access_token=` query (WebSocket headers limit). Interceptor SavedChangesAsync auto-push → zero caller changes. FE singleton connection với auto-reconnect + toast trên push + query invalidation. IIS WebSocket module enabled. Hub verified: negotiate 200 với JWT (WebSockets/SSE/LongPolling transports), 401 không auth | `ea9ab5e` |
| 2026-04-21 | Claude | **Attachment upload E2E** — IFileStorage abstraction + LocalFileStorage (path-traversal guard) + CQRS Upload/Download/Delete + 3 controller endpoints (multipart, File stream, DELETE) + FE ContractAttachmentsSection (drag-drop + purpose selector + icon-per-MIME + auth-blob download + confirm delete) + wired vào cả 2 ContractDetailPage. Unblock E2E workflow (scan HĐ ký/đóng dấu) | `c8d0070` + `dc3f09b` |
| 2026-04-21 | Claude | **Content polish** — typography (14px + leading 1.55 + tracking-tight, heading weights) + PageHeader (text-[22px] + border-b) + Button (shadow + active-press + ring-2) + Input/Select/Textarea (inset shadow + border/ring focus) + DataTable (rounded-xl + UPPERCASE tracking header + brand hover) | `346bd5d` |
| 2026-04-21 | Claude | **Brand identity từ Solutions logo** — pixel-sampled #1F7DC1 → full palette brand-50..900 + accent red + Be Vietnam Pro font (Vietnamese-first) + favicon chữ 'S' crop từ logo.png + apple-touch-icon + login page gradient brand + ERP subtitle | `4abb559` + `bf1fbe3` |
| 2026-04-21 | Claude | **Fix login Network Error** — SPA web.config thêm HTTP→HTTPS redirect rule (CORS chỉ allow https origin, user gõ bare domain bị block) | `397eb36` |
| 2026-04-21 | Claude | **Notifications module E2E** — Domain entity + EF migration + Infra service + CQRS (List/UnreadCount/MarkRead/MarkAllRead) + API controller + FE bells wire real endpoint + ContractWorkflowService emit notification cho Drafter khi phase transition. Foundation sẵn cho SignalR/email outbox | `49c0ddc` |
| 2026-04-21 | Claude | **PermissionsPage improved** — search, stats badge, bulk column toggle, empty state icon | `6c0e206` |
| 2026-04-21 | Claude | **ERP shell**: TopBar + NotificationBell + UserMenu (avatar + role badges). Layout tách `[sidebar] [topbar + content]` — foundation cho multi-module ERP | `2b6f91c` |
| 2026-04-21 | Claude | **Tier 1 UI polish** — SlaTimer (inline + full variant, 5 chỗ), Inbox stat cards, DataTable skeleton rows, EmptyState component + MyContracts CTA | `290936a`..`2e43799` |
| 2026-04-21 | Claude | **CI/CD deploy xanh E2E** — self-hosted Windows runner, single job build+deploy local, npm install fresh node_modules (Vite 8 rolldown binding), appsettings rendered từ secrets, /health/live 200 sau deploy | `b40da1e` |
| 2026-04-21 | Claude | **VPS prod setup** — SQL DB (SQLEXPRESS), IIS sites (SolutionErp-Api/Admin/User), win-acme 3 Let's Encrypt certs + auto-renew, shared gitea-runner với VIETREPORT | `169e268`..`519ba85` |
| 2026-04-21 | Claude | **IDOR + SLA Job + Admin warning** — ContractsController List/GetDetail filter theo role (non-admin chỉ thấy HĐ mình là Drafter hoặc role eligible phase). SlaExpiryJob BackgroundService auto-approve quá hạn mỗi 15min với Decision=AutoApprove. DbInitializer warn log khi admin vẫn dùng password default | `fba0754` |
| 2026-04-22 | Claude | **PermissionsPage 3-panel layout** — Grid `lg:grid-cols-[280px_1fr_300px]`: Panel 1 Role list click-to-select (active ring-brand), Panel 2 Menu×CRUD matrix sticky thead + search + column bulk-toggle + brand-tinted hover, Panel 3 Granted progress bar + CRUD breakdown color badges (slate/emerald/amber/red) + Tip | `91b2da1` |
| 2026-04-22 | Claude | **Admin Workflows tabs → sidebar menu items** — Seed 7 `Wf_<Code>` leaf dưới group `Workflows`. Layout resolvePath `Wf_<Code>``/system/workflows/<code>`. WorkflowsPage bỏ tab bar, URL param drives type selection. Landing 7-card grid khi click top-level `Quy trình HĐ`. Inheritance: `Workflows.Read` perm → tất cả 7 leaves auto-visible. | `f216169` |
| 2026-04-22 | Claude | **Versioned workflow per ContractType** — 3 entity mới: WorkflowDefinition (Code+Version+IsActive+ContractType), WorkflowStep (Order+Phase+Name+SlaDays), WorkflowStepApprover (Role/User + AssignmentValue). Contract.WorkflowDefinitionId nullable FK pin tại create. Migration `AddVersionedWorkflows`. Seed v01 per 7 ContractType. `WorkflowPolicyRegistry.FromDefinition()` build runtime policy từ DB. ContractWorkflowService load pinned definition. Admin `/system/workflows/:typeCode` Designer modal (create new version, clone, add/remove step, +Role/+User approvers). POST /api/workflows auto-increment Version + deactivate old. Invariant: HĐ cũ pin v01 giữ nguyên khi v02 active. E2E verified: QT-MB-v02 active, HĐ cũ vẫn chạy v01. | `e7e5f2d` + `355bbe3` |
| 2026-04-21 | Claude | **Nested sidebar menu fe-user** — 7 ContractType × 3 actions (Danh sách/Thao tác/Duyệt), nested 3-level. Admin hide `Ct_*`. Layout recursive MenuNodeRenderer. MyContracts + Inbox filter `?type=X` | `5e0f380` + `48e91fe` |
| 2026-04-21 | Claude | **Seed master data + MyDashboard widgets** — DbInitializer seed 9 departments (PM/QS/CCM/PRO/FIN/ACT/EQU/HRA/BOD) + 5 demo suppliers + 3 demo projects idempotent. MyDashboard endpoint role-aware: DraftsInProgress / PendingMyApproval / DueSoon / Overdue / DraftsTotalValue. FE "Của tôi" row 4 card hover-interactive, admin auto-hide nếu = 0 | `6197c84` |
| 2026-04-21 | Claude | **Dynamic workflow policy per ContractType** — Domain WorkflowPolicy record + registry (Standard 8-phase cho Thầu phụ/Giao khoán/NCC; SkipCcm 7-phase cho Dịch vụ/Mua bán/Nguyên tắc). ContractWorkflowService dùng policy.ForContract(c). FE xóa NEXT_PHASES hardcoded, dùng contract.workflow.nextPhases BE trả. WorkflowSummaryCard timeline visual. Gotcha #21 resolved | `cae4d84` |
| 2026-04-21 | Claude | **PDF export + .doc/.xls auto-convert + DynamicForm** — LibreOffice 25.8.6 VPS, IDocumentConverter shell soffice `--convert-to pdf/docx/xlsx` timeout+temp isolation. Admin upload .doc auto-convert .docx. DynamicForm parse FieldSpec JSON render inputs (text/textarea/number/date/currency/select). Form↔JSON toggle. E2E verified PDF 488KB/126 pages | `e459097` + `6bbd894` |
| 2026-04-21 | Claude | **Form template builder CRUD** — Admin tự upload `.docx/.xlsx` qua UI (không cần dev). BE multipart + FormCode regex unique + FieldSpec JSON validation + soft delete via IsActive. FE FormsPage upload dialog + row actions render/edit/delete. E2E verified | `166d26c` |
| 2026-04-21 | Claude | **Fix Gitea 500 sau Install Web-WebSockets** — appcmd unlock section webSocket. Gotcha #25 | `c52186b` |
| 2026-04-21 | Claude | **SignalR realtime notifications E2E** — 3-project clean-arch: IRealtimeNotifier (App) + SignalRNotifier (Api) + NotificationPushInterceptor (Infra SaveChanges hook). Hub `/hubs/notifications` JWT `?access_token=` query (WebSocket headers limit). FE singleton lib/realtime.ts auto-reconnect + toast + query invalidation. IIS WebSocket module enabled | `ea9ab5e` |
| 2026-04-21 | Claude | **Attachment upload E2E** — IFileStorage + LocalFileStorage (path-traversal guard) + CQRS Upload/Download/Delete + 3 endpoint (multipart, stream, DELETE) + FE ContractAttachmentsSection drag-drop + purpose selector + icon-per-MIME + auth-blob download + confirm delete. Wired 2 ContractDetailPage | `c8d0070` + `dc3f09b` |
| 2026-04-21 | Claude | **Content polish** — typography 14px + leading 1.55 + tracking-tight + PageHeader border-b + Button shadow+active + Input inset shadow + DataTable rounded-xl UPPERCASE header brand hover | `346bd5d` |
| 2026-04-21 | Claude | **Brand identity từ Solutions logo** — pixel-sampled #1F7DC1 → palette brand-50..900 + accent red + Be Vietnam Pro (Vietnamese-first) + favicon 'S' crop + apple-touch-icon + login gradient brand | `4abb559` + `bf1fbe3` |
| 2026-04-21 | Claude | **Fix login Network Error** — SPA web.config HTTP→HTTPS redirect rule (CORS chỉ https) | `397eb36` |
| 2026-04-21 | Claude | **Notifications module E2E** — Domain entity + EF migration + Infra service + CQRS + API controller + FE bells wire real endpoint + ContractWorkflowService emit notification cho Drafter khi phase transition | `49c0ddc` |
| 2026-04-21 | Claude | **PermissionsPage iter 1** — search, stats badge, bulk column toggle, empty state | `6c0e206` |
| 2026-04-21 | Claude | **ERP shell** — TopBar + NotificationBell + UserMenu (avatar + role badges). Layout `[sidebar] [topbar + content]` | `2b6f91c` |
| 2026-04-21 | Claude | **Tier 1 UI polish** — SlaTimer (inline + full variant, 5 chỗ), Inbox stat cards, DataTable skeleton rows, EmptyState | `290936a`..`2e43799` |
| 2026-04-21 | Claude | **CI/CD deploy xanh E2E** — self-hosted Windows runner, single job build+deploy, fresh node_modules (Vite 8 rolldown binding), appsettings từ secrets, /health/live 200 sau deploy | `b40da1e` |
| 2026-04-21 | Claude | **VPS prod setup** — SQL DB (SQLEXPRESS), IIS sites (SolutionErp-Api/Admin/User), win-acme 3 Let's Encrypt + auto-renew, shared gitea-runner với VIETREPORT | `169e268`..`519ba85` |
| 2026-04-21 | Claude | **IDOR + SLA Job + Admin warning** — ContractsController filter theo role. SlaExpiryJob BackgroundService 15min auto-approve Decision=AutoApprove. DbInitializer warn khi admin vẫn default | `fba0754` |
| 2026-04-21 | Claude | **Phase 5.1 Security + Users Mgmt** — Security headers + Identity lockout + LoginHandler check + Users CQRS + UsersController + FE `/system/users` | `11e61c9` |
| 2026-04-21 | Claude | **Phase 5 Prep** — BE rate limit + health check + Serilog file + HSTS + scripts deploy-iis/backup-sql + .gitea/workflows/deploy.yml + 4 guides + FE refresh token queue pattern | `46a2cab` |
| 2026-04-21 | Claude | **Phase 4 Report MVP + Docs Consolidation** — Dashboard KPI + Excel export + rules.md + architecture.md + schema-diagram.md + gotchas update 26 pitfalls | `fe7ad8e` |
| 2026-04-21 | Claude | **Phase 4 Report MVP** — Dashboard KPI + Excel export + rules.md + architecture.md + schema-diagram.md + gotchas 26 pitfalls | `fe7ad8e` |
| 2026-04-21 | Claude | **Phase 3 Workflow MVP** — 9 phase state machine + gen mã HĐ RG-001 | `7e957a7` |
| 2026-04-21 | Claude | **Phase 2 Form Engine MVP** | `5113e4c` |
| 2026-04-21 | Claude | **Phase 1.2** — CRUD Master + Permission Matrix | `54d6c9b` |
| 2026-04-21 | Claude | **Docs addition** | `49a5f57` |
| 2026-04-21 | Claude | **Phase 1 foundation** | `702411f` |
| 2026-04-21 | Claude | **Phase 1 foundation** + Docs addition | `702411f` + `49a5f57` |
| 2026-04-21 | Claude | **Phase 0** | `25dad7f` |
Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1f](changelog/sessions/2026-04-21-1100-phase1-foundation.md) · [P1.2](changelog/sessions/2026-04-21-1130-phase1-cruds-permission.md) · [P2](changelog/sessions/2026-04-21-1200-phase2-form-engine.md) · [P3](changelog/sessions/2026-04-21-1330-phase3-workflow.md) · [P4](changelog/sessions/2026-04-21-1430-phase4-report.md) · [P5prep](changelog/sessions/2026-04-21-1530-phase5-prep.md)
Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1f](changelog/sessions/2026-04-21-1100-phase1-foundation.md) · [P1.2](changelog/sessions/2026-04-21-1130-phase1-cruds-permission.md) · [P2](changelog/sessions/2026-04-21-1200-phase2-form-engine.md) · [P3](changelog/sessions/2026-04-21-1330-phase3-workflow.md) · [P4](changelog/sessions/2026-04-21-1430-phase4-report.md) · [P5prep](changelog/sessions/2026-04-21-1530-phase5-prep.md) · [**Tier 3**](changelog/sessions/2026-04-22-0300-tier3-feature-complete.md)
**Docs entry points:**
- [`rules.md`](rules.md) · [`architecture.md`](architecture.md) · [`HANDOFF.md`](HANDOFF.md)
- [`workflow-contract.md`](workflow-contract.md) · [`forms-spec.md`](forms-spec.md)
- [`database/database-guide.md`](database/database-guide.md) · [`database/schema-diagram.md`](database/schema-diagram.md)
- [`flows/`](flows/) (7 file) · [`guides/`](guides/) (4 file) · [`gotchas.md`](gotchas.md)
- [`changelog/migration-todos.md`](changelog/migration-todos.md) · [`changelog/sessions/`](changelog/sessions/) (7 file)
- [`changelog/migration-todos.md`](changelog/migration-todos.md) · [`changelog/sessions/`](changelog/sessions/) (8 file)
## 🎯 Next up
### Phase 5 (prod go-live)
### Hard blockers (chờ user / ops)
- [x] Gitea remote + push all commits
- [x] Gitea Actions runner (self-hosted Windows, shared VIETREPORT runner)
- [x] Secrets Gitea (JWT_SECRET, DB_CONNECTION — IIS_* deprecated sau rewrite workflow)
- [x] CI/CD workflow xanh end-to-end
- [x] Windows Server setup IIS (SolutionErp-Api/Admin/User)
- [x] HTTPS cert (win-acme 3 Let's Encrypt + auto-renew)
- [x] SQL Server prod (SQLEXPRESS) + vrapp db_owner
- [x] Smoke test E2E: /health/ready Healthy, login JWT thật, FE live
- [ ] **UAT 1 tuần 2-3 user thật** ← next
- [ ] SQL backup Task Scheduler (script đã có, chưa schedule)
- [ ] Rotate credentials (SA, vrapp, JWT, runner token) — 1 số đã post chat
- [ ] **UAT 1 tuần 2-3 user thật** — hard requirement từ roadmap Phase 5
- [ ] **Email outbox** — MailKit + SMTP (BLOCKED chờ user cấp SMTP host/user/pass)
- [ ] **Rotate credentials** — SA, vrapp, JWT secret, runner token (đã post chat)
- [ ] **SQL backup daily** — Task Scheduler (script `scripts/backup-sql.ps1` đã có, chưa schedule)
### Tier 3 ERP roadmap còn (lớn, để dành session sau)
### Optional polish (khi rảnh / UAT phát sinh)
- [x] Attachment upload BE endpoint + FE drag-drop ✓
- [x] SignalR real-time push (auto-push interceptor + client auto-reconnect) ✓
- [x] Form template builder CRUD (admin upload .docx/.xlsx + FieldSpec JSON editor) ✓
- [x] Form builder iteration 2: DynamicForm render UI từ FieldSpec ✓
- [ ] Roles CRUD — admin tạo custom role ngoài 12 hardcoded (schema sẵn, chỉ cần CQRS + FE)
- [ ] User-level approver targeting runtime — data model đã có (`WorkflowStepApprover.Kind=User`), chỉ cần wire User-kind vào `ContractWorkflowService.TransitionAsync` guard
- [ ] PermissionsPage: grant `Workflows.Read` cho non-admin role → menu Wf_* visible
- [ ] Warning notification khi còn 20% SLA (`SlaWarningSent` flag đã có, chỉ thiếu job emit)
- [ ] E2E test reject → quay về DangSoanThao (multi-role)
- [ ] Dependencies scan CI (`dotnet list package --vulnerable`, `npm audit`)
### Tier 3 ERP roadmap ✓ (close)
- [x] Attachment upload BE + FE ✓
- [x] SignalR real-time push ✓
- [x] Form template builder CRUD + DynamicForm ✓
- [x] PDF export qua LibreOffice headless ✓
- [x] .doc → .docx auto-conversion khi upload template
- [x] Dynamic workflow policy per ContractType (Standard/SkipCcm)
- [ ] Email outbox cho Notification (MailKit, SMTP config — cần user config)
### Phase 5.1 Security — hầu như xong
- [x] Security headers middleware (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, CSP)
- [x] Identity account lockout (5 fail → 15min, config-driven)
- [x] Password policy config-driven
- [x] LoginHandler check lockout + AccessFailedAsync + reset on success
- [x] BE Users management + FE admin UsersPage
- [x] IDOR check ContractsController (non-admin chỉ thấy HĐ mình/role eligible)
- [x] Admin password warning log startup
- [x] SLA Expiry BackgroundService auto-approve
- [ ] Dependencies scan CI (`dotnet list package --vulnerable` + `npm audit`)
- [ ] Roles CRUD — optional
### Polish iterations
**Phase 2 iter 2:** convert .doc, field spec JSON + form builder, {{#loop}}, PDF convert
**Phase 3 iter 2:** SLA job auto-approve, email/in-app notify, attachment upload, RowVersion
**Phase 4 iter 2:** SLA overdue report, PDF HĐ export, dashboard user-specific
### Quick wins
- FE Users management + Roles CRUD (test permission non-admin)
- Filter Inbox theo phase FE
- Test refresh token flow manual (logout/login flow)
- [x] .doc/.xls → .docx/.xlsx auto-conversion ✓
- [x] Dynamic workflow policy per ContractType ✓
- [x] **Versioned workflow (WorkflowDefinition pinned per Contract)**
- [x] **Admin workflow designer UI (per-type, per-step approvers)**
- [x] **Nested sidebar menu per ContractType (fe-user) + menu split admin/user**
- [x] **PermissionsPage 3-panel layout**
- [ ] Email outbox for Notification (blocked — SMTP config)
## 📊 Thông số cumulative
| | P0 | P1f | P1.2 | P2 | P3 | P4 | **P5 prep** |
|---|---:|---:|---:|---:|---:|---:|---:|
| BE LOC | 0 | ~400 | ~1500 | ~1900 | ~2700 | ~3100 | **~3300** |
| DB tables | 0 | 7 | 12 | 14 | 19 | 19 | 19 |
| API endpoints | 0 | 4 | 20 | 23 | 31 | 33 | **35** (+health) |
| Migrations | 0 | 1 | 3 | 4 | 5 | 5 | 5 |
| FE pages | 0 | 2 | 6 | 7 | 14 | 16 | 16 |
| Scripts PS | 0 | 0 | 0 | 1 (convert-doc) | 1 | 1 | **3** (+deploy-iis, backup-sql) |
| CI/CD workflow | 0 | 0 | 0 | 0 | 0 | 0 | **1** |
| Docs | 10 | 13 | 14 | 24 | 26 | 30 | **35** (+4 guides + session log) |
| Commits | 1 | 2 | 3 | 5 | 6 | 7 | **8** (sắp) |
| | P0 | P1f | P1.2 | P2 | P3 | P4 | P5prep | **Tier3** |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| BE LOC | 0 | ~400 | ~1500 | ~1900 | ~2700 | ~3100 | ~3300 | **~4800** |
| DB tables | 0 | 7 | 12 | 14 | 19 | 19 | 19 | **24** (+Notifications, +WorkflowTypeAssignments, +WorkflowDefinitions, +WorkflowSteps, +WorkflowStepApprovers) |
| API endpoints | 0 | 4 | 20 | 23 | 31 | 33 | 35 | **~50** (+notifications, +attachments, +forms CRUD, +pdf export, +workflows admin, +my-dashboard) |
| Migrations | 0 | 1 | 3 | 4 | 5 | 5 | 5 | **8** (+AddNotifications, +AddWorkflowTypeAssignments, +AddVersionedWorkflows) |
| FE pages | 0 | 2 | 6 | 7 | 14 | 16 | 16 | **~20** (admin Users/Workflows per-type + user nested menu) |
| Scripts PS | 0 | 0 | 0 | 1 | 1 | 1 | 3 | **4** (+install-libreoffice) |
| CI/CD workflow | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
| Docs | 10 | 13 | 14 | 24 | 26 | 30 | 35 | **~40** (+session log + updated MDs) |
| Commits | 1 | 2 | 3 | 5 | 6 | 7 | 8 | **~25** |
## 🚨 Blockers / risks
- **Gitea remote URL** — ĐANG CẦN để push + setup CI/CD
- ⚠️ **Phase 5.1 security hardening** chưa làm (headers, account lockout, IDOR check)
- ⚠️ **3 file .doc chưa convert** (Phase 2 carryover)
- ⚠️ **SLA không tự auto-approve** (Phase 3.2)
- ⚠️ **Email/in-app notification** chưa có
- ⚠️ **FE Users management chưa có** — khó test permission non-admin
- ⚠️ **Rate limit global 300/min/IP** — OK cho dev, cần tăng cho prod nếu nhiều user
- ⚠️ **Email SMTP chưa có** — blocker cho notification outbound
- ⚠️ **UAT real user chưa chạy** — risk phát sinh bug edge-case quan trọng
- ⚠️ **Credentials leaked trong chat** — cần rotate trước go-live thật
- ⚠️ **SQL backup không auto** — risk data loss nếu VPS crash
- ⚠️ **Permission `Workflows.Read` cho non-admin** — cần grant để họ thấy menu Wf_* (hiện chỉ admin thấy)
- ⚠️ **User-kind approver chưa enable runtime** — designer cho chọn User nhưng guard fall back DeptManager
## Credentials + URLs
@ -141,6 +123,9 @@ Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1
admin@solutionerp.local / Admin@123456
```
- API: http://localhost:5443 — Swagger `/swagger` (dev only) — Health `/health/live` + `/health/ready`
- Admin FE: http://localhost:8082 — `/dashboard`, `/contracts`, `/reports`, `/master/*`, `/forms`, `/system/permissions`
- User FE: http://localhost:8080 — `/inbox`, `/contracts/new`, `/my-contracts`
- API prod: https://api.huypham.vn — Health `/health/live` + `/health/ready`
- API dev: http://localhost:5443 — Swagger `/swagger`
- Admin FE prod: https://admin.huypham.vn · dev `http://localhost:8082`
- User FE prod: https://user.huypham.vn · dev `http://localhost:8080`
- SQL prod: `.\SQLEXPRESS` / `SolutionErp` / `vrapp`
- SQL dev: `(localdb)\MSSQLLocalDB` / `SolutionErp_Dev`

View File

@ -58,7 +58,7 @@
- [x] FE: `main.tsx` với QueryClient (TanStack Query)
- [x] E2E verified: login qua Vite proxy cả 2 app → JWT + user info
### Phase 1 đợt 2 — CRUD master + Permission Matrix (sắp tới)
### Phase 1 đợt 2 — CRUD master + Permission Matrix
- [x] `Domain/Master/Supplier` (+ SupplierType enum 5 loại) / `Project` / `Department` (AuditableEntity)
- [x] EF `IEntityTypeConfiguration<T>` cho mỗi entity (unique Code + query filter IsDeleted)
@ -67,27 +67,25 @@
- [x] Migration 2: `AddMasterData`
- [x] `Domain/Identity/MenuItem` (Key PK, Label, ParentKey, Order, Icon) + `MenuKeys` const class
- [x] `Domain/Identity/Permission` (RoleId, MenuKey, CanRead/Create/Update/Delete)
- [x] Seed default menu tree (12 menu) + admin full access trong DbInitializer
- [x] `Application/Permissions/Queries/GetMyMenuTreeQuery` — resolve per-user, union OR, tree filter
- [x] Seed default menu tree + admin full access trong DbInitializer (mở rộng Tier 3: 28 Ct_* + 7 Wf_*)
- [x] `Application/Permissions/Queries/GetMyMenuTreeQuery` — resolve per-user + inherit Contracts/Workflows root
- [x] `Api/Controllers/{MenusController, RolesController, PermissionsController}`
- [x] Migration 3: `AddPermissions`
- [x] Authorization handler `MenuPermissionHandler` + register 48 policy `{menu}.{action}`
- [ ] `Domain/Entities/Contract` skeleton (Id, Type, SupplierId, ProjectId, Phase=DangChon, DraftData JSON) — deferred Phase 2/3
- [ ] Contract CRUD draft only (không workflow Phase 3) — deferred
- [x] Authorization handler `MenuPermissionHandler` + register policy `{menu}.{action}`
- [x] FE: `<PermissionGuard menuKey="Suppliers" action="Update">` + `usePermission()` hook
- [x] FE Admin: 3 trang CRUD Supplier/Project/Department với DataTable + Dialog modal + search/sort/paging
- [x] FE Admin: Permission Matrix grid page (role × menu × CRUD checkbox)
- [x] FE Admin: Layout menu động từ `/api/menus/me`
- [ ] FE User: trang "HĐ của tôi" list + filter — Phase 3
- [ ] FE Admin: Users management page (tạo user + gán role) — sắp tới
- [ ] FE Admin: Roles CRUD — sắp tới
- [ ] Route guard theo role admin-only — PermissionGuard ở button, route cần thêm
- [x] FE Admin: 3 trang CRUD Supplier/Project/Department với DataTable + Dialog + search/sort/paging
- [x] FE Admin: Permission Matrix grid page (role × menu × CRUD checkbox) — iter 1 + 3-panel iter 2
- [x] FE Admin: Layout menu động từ `/api/menus/me` + recursive nested + filterForAdmin
- [x] FE User: trang "HĐ của tôi" list + filter `?type=X` — Tier 3
- [x] FE Admin: Users management page (tạo user + gán role + reset password + unlock)
- [ ] FE Admin: Roles CRUD — optional (12 role seed đủ dùng)
- [x] Route guard theo role admin-only — PermissionGuard ở button level
### Exit criteria Phase 1
- [ ] Admin login → tạo NCC/Project → tạo role "Nhân viên CCM" → gán permission menu "Contracts.Read"
- [ ] User CCM login → thấy menu Contracts, không thấy menu Admin
- [ ] Tạo Contract draft → list hiển thị, không bị 403 sai
- [x] Admin login → tạo NCC/Project → gán permission menu
- [x] User non-admin login → thấy menu theo role, không bị 403
- [x] Tạo Contract draft → list hiển thị, filter role-aware
## Phase 2 — Form Engine (T5-6)
@ -107,18 +105,17 @@
- [x] FE admin: `FormsPage` — list + render dialog điền JSON + download
- [x] E2E verified: render FO-002.05 → file .docx 482KB mở được bằng Word
### Iteration 2 (optional — enhance)
### Iteration 2 (Tier 3 — đã làm)
- [ ] Convert 3 file `.doc``.docx` (retry Word COM với `DisplayAlerts=0` + timeout, hoặc LibreOffice headless)
- [ ] Parse chi tiết field của 5 template HĐ — mỗi form thành JSON `FieldSpec`
- [ ] Support `{{#loop}}...{{/loop}}` block cho table lặp (hạng mục HĐ giao khoán, PO)
- [ ] FE user: form builder dynamic — render từ fieldSpec thay vì điền JSON tay
- [ ] FE admin: upload template mới qua UI (POST multipart) + edit field mapping
- [ ] Lưu `ContractClause` (FO-002.04) dạng rich text, admin edit qua TipTap/TinyMCE
- [ ] PDF convert via LibreOffice headless (`soffice --headless --convert-to pdf`)
- [ ] Import/export template (backup/restore)
- [ ] Format helpers: number → `150,000,000 VND`, date → `dd/MM/yyyy`
- [ ] Content preservation test: render → diff layout với template gốc
- [x] Convert `.doc``.docx` / `.xls``.xlsx` qua `IDocumentConverter` + LibreOffice headless (thay Word COM, auto-convert khi admin upload)
- [x] FE user: form builder dynamic — `DynamicForm` component render từ `FieldSpec` JSON (text/textarea/number/date/currency/select)
- [x] FE admin: upload template mới qua UI (POST multipart) + edit FieldSpec + delete (soft via IsActive)
- [x] PDF convert via LibreOffice headless (`soffice --headless --convert-to pdf`) — `LibreOfficeDocumentConverter` (timeout + per-request temp + isolated UserInstallation)
- [x] Format helpers: number → `VND`, date → `dd/MM/yyyy` (render layer)
- [ ] Support `{{#loop}}...{{/loop}}` block cho table lặp (hạng mục HĐ giao khoán, PO) — optional
- [ ] Lưu `ContractClause` (FO-002.04) dạng rich text + TipTap/TinyMCE editor — optional
- [ ] Import/export template (backup/restore) — optional
- [ ] Content preservation test (render → diff layout) — optional
## Phase 3 — Workflow State Machine (T7-9)
@ -141,21 +138,39 @@
- [x] PhaseBadge component + color map
- [x] E2E verified: tạo HĐ → chạy 9 phase → gen mã `FLOCK 01/HĐGK/SOL&PVL2026/01`
### Iteration 2 (polish)
### Iteration 2 (polish — Tier 3 + Notification)
- [x] `Infrastructure/HostedServices/SlaExpiryJob` — check mỗi 15min, auto-approve quá hạn với Decision=AutoApprove (+30s delay startup)
- [x] E2E test với non-admin user (Drafter role) — IDOR filter verified
- [x] Admin password warning log khi vẫn dùng default
- [ ] Warning notification khi còn 20% SLA (track `SlaWarningSent` flag đã có)
- [ ] `Infrastructure/Services/NotificationService` — email (MailKit) + in-app
- [ ] SignalR hub cho real-time notification badge
- [x] `Infrastructure/Services/NotificationService` — in-app + emit (email đợi SMTP)
- [x] SignalR hub cho real-time notification badge — `/hubs/notifications` + interceptor auto-push
- [x] Upload attachment endpoint (multipart) + FE drag-drop UI (`wwwroot/uploads/contracts/{id}/`) — IFileStorage + path-traversal guard
- [x] Filter Inbox theo type ở FE (`?type=X`)
- [x] Render HĐ template docx/xlsx → PDF export (LibreOffice)
- [ ] Warning notification khi còn 20% SLA — `SlaWarningSent` flag đã có
- [ ] MediatR `AuditBehavior` — log mọi command (ngoài ContractApprovals)
- [ ] Upload attachment endpoint (multipart) + FE upload UI (`wwwroot/uploads/contracts/{id}/`)
- [ ] RowVersion optimistic concurrency (2 user race → 409)
- [ ] Render HĐ docx lúc tạo (merge TemplateId + DraftData + ContractClause appendix)
- [ ] Filter Inbox theo phase ở FE
- [ ] E2E test: reject → quay về DangSoanThao
- [ ] E2E test: SLA expired → auto-approve + log (test thật qua set SlaDeadline past)
- [ ] E2E test: reject → quay về DangSoanThao với multi-role
- [ ] Email notification (MailKit + SMTP) — blocked chờ user config
### Iteration 3 (Versioned workflow — Tier 3)
- [x] `Domain/Contracts/WorkflowDefinition` (Code + Version + IsActive + ContractType + Description)
- [x] `Domain/Contracts/WorkflowStep` (Order + Phase + Name + SlaDays)
- [x] `Domain/Contracts/WorkflowStepApprover` (Kind: Role|User + AssignmentValue)
- [x] `Contract.WorkflowDefinitionId` nullable FK pin tại create time
- [x] Migration `AddVersionedWorkflows` + seed v01 cho 7 ContractType
- [x] `WorkflowPolicyRegistry.FromDefinition()` — runtime policy build từ DB
- [x] `ContractWorkflowService` — load pinned def → FromDefinition → guard
- [x] `WorkflowAdminFeatures` — GetOverview + CreateNewVersion (auto-increment Version + deactivate old)
- [x] FE admin `/system/workflows/:typeCode` — DefinitionCard + history + Designer modal
- [x] Designer: Steps repeatable, per-step phase/name/SLA, +Role / +User approver select
- [x] Clone-from-version button cho starting point
- [x] Invariants: UNIQUE (Code, Version), 1 IsActive per ContractType, no cascade FK
- [x] E2E: create QT-MB-v02 → v01 archived → HĐ mới pin v02 → HĐ cũ pin v01 giữ nguyên
- [ ] Runtime enable User-kind approver trong TransitionAsync guard (data model ready)
## Phase 4 — Reporting + Polish (T10-11)
@ -168,13 +183,15 @@
- [x] FE `ReportsPage` filter + export
- [x] Docs consolidation: `rules.md` + `architecture.md` + `database/schema-diagram.md` + gotchas update
### Iteration 2 (polish — optional)
### Iteration 2 (Tier 3 + optional)
- [x] Dashboard user-specific (`MyDashboard` endpoint — DraftsInProgress / PendingMyApproval / DueSoon / Overdue / DraftsTotalValue) + FE "Của tôi" row 4 card
- [x] UX polish: skeleton loader DataTable, empty state có action, error boundary recovery
- [x] Content polish: typography 14px + leading 1.55 + tracking-tight + PageHeader + Button + Input + DataTable
- [x] Brand identity: #1F7DC1 palette + Be Vietnam Pro font + Solutions logo
- [ ] SLA overdue report (by role / phase, export Excel)
- [ ] Contract audit log export (từng HĐ ra PDF)
- [ ] Dashboard user-specific (HĐ của tôi / role của tôi)
- [ ] Chart library recharts (nếu cần chart phức tạp)
- [ ] UX polish: skeleton loader cho mọi list, empty state có action, error boundary recovery
- [ ] Accessibility: keyboard nav, focus trap modal, aria labels
- [ ] Dark mode
- [ ] Performance: explicit index DB cho query hot đã identify
@ -200,17 +217,18 @@
- [x] `docs/guides/runbook.md` — operations (restart, rollback, restore)
- [x] FE refresh token auto interceptor (queue pattern cả 2 app)
### Deploy thật (cần Gitea URL)
### Deploy thật
- [ ] Windows Server setup: IIS + URL Rewrite + ARR (reverse proxy FE → IIS)
- [ ] SQL Server prod + Task Scheduler trigger backup-sql.ps1
- [ ] HTTPS certificate (Let's Encrypt qua win-acme)
- [ ] Gitea remote setup + push all commits
- [ ] Set 5 Gitea Actions secrets (IIS_HOST/USER/PASSWORD/JWT_SECRET/DB_CONNECTION)
- [ ] Enable Gitea runner (Windows + Ubuntu)
- [ ] Test CI/CD workflow lần đầu staging
- [ ] UAT production 1 tuần với 2-3 user thật
- [ ] Go-live checklist: backup, rollback plan, on-call contact
- [x] Windows Server setup: IIS + URL Rewrite + ARR (reverse proxy FE → IIS)
- [x] SQL Server prod (SQLEXPRESS) + vrapp db_owner
- [x] HTTPS certificate (Let's Encrypt qua win-acme — 3 cert + auto-renew)
- [x] Gitea remote setup + push all commits
- [x] Set Gitea Actions secrets (JWT_SECRET, DB_CONNECTION — deploy local via runner)
- [x] Enable Gitea runner (Windows self-hosted, shared với VIETREPORT)
- [x] Test CI/CD workflow — xanh E2E, /health/live 200 sau deploy
- [ ] **UAT production 1 tuần với 2-3 user thật** ← hard blocker còn lại
- [ ] SQL Task Scheduler trigger backup-sql.ps1 (script có sẵn, chưa schedule)
- [ ] Go-live checklist: rotate creds + backup plan + on-call contact
### Phase 5.1 Security hardening + Users Mgmt
@ -225,8 +243,26 @@
- [ ] Dependencies scan vào CI (`dotnet list package --vulnerable --include-transitive`, `npm audit --audit-level=high`)
- [ ] BE Roles CRUD (Create/Rename/Delete custom role) + FE `/system/roles` — optional, 12 role seed đủ dùng
## Tier 3 ERP (Session 2026-04-22) — feature-complete
- [x] **Attachment upload E2E** — IFileStorage + CQRS + FE drag-drop (gotcha path-traversal) — `c8d0070`
- [x] **SignalR realtime notifications** — 3-project clean-arch split + JWT `?access_token=` + auto-reconnect — `ea9ab5e`
- [x] **Form template builder CRUD** — Upload/Update/Delete + FieldSpec JSON editor — `166d26c`
- [x] **PDF export + DynamicForm + .doc auto-convert** — LibreOffice headless per-request temp — `6bbd894` + `e459097`
- [x] **Dynamic workflow policy** — Standard/SkipCcm registry theo ContractType — `cae4d84`
- [x] **Versioned workflow** — WorkflowDefinition + Steps + Approvers pinned per Contract — `e7e5f2d`
- [x] **Admin workflow designer** — per-type page + Designer modal + clone — `e7e5f2d`
- [x] **Nested sidebar menu fe-user** — 7 type × 3 action + admin/user split — `5e0f380`
- [x] **Workflows tabs → sidebar menu** — 7 Wf_ leaves + URL-driven — `f216169`
- [x] **PermissionsPage 3-panel layout** — Role list | Menu×CRUD | Granted stats — `91b2da1`
- [x] **Seed master data** — 9 dept + 5 supplier + 3 project + MyDashboard — `6197c84`
- [x] **Brand identity**#1F7DC1 palette + Be Vietnam Pro + Solutions logo — `4abb559`..`bf1fbe3`
## Post-launch (Phase 6+ — future)
- [ ] **Email outbox** (MailKit + SMTP) — blocked chờ SMTP config
- [ ] **Roles CRUD** — admin tạo custom role ngoài 12 hardcoded
- [ ] **User-kind approver runtime** — data model có, guard cần wire
- [ ] E-signature integration (VNPT CA hoặc FPT CA)
- [ ] Tích hợp Bravo / SAP ERP import NCC
- [ ] Mobile app (React Native?) cho BOD duyệt ngoài giờ

View File

@ -0,0 +1,206 @@
# Session 2026-04-22 ~03:00 — Tier 3 feature-complete + versioned workflow
**Focus:** Hoàn thành toàn bộ Tier 3 ERP features, pivot workflow từ hardcoded
policy → versioned DB-backed designer, chia nested menu cho fe-user + admin
workflow management riêng.
Session kéo dài 2 phiên (21/04 chiều — 22/04 sáng), tổng ~20+ commit.
## Outcomes
### A. Attachment upload E2E ✓
- `IFileStorage` abstraction + `LocalFileStorage` (Application/Infra split,
path-traversal guard, CREATEDIRECTORY-if-missing).
- CQRS: Upload / Download / Delete, validation 20MB + MIME whitelist (pdf/doc
(x)/xls(x)/png/jpg/webp), sanitize filename.
- Endpoints: POST multipart / GET download stream / DELETE.
- FE `ContractAttachmentsSection` (both apps) — drag-drop, purpose selector,
icon-per-MIME, auth-blob download, confirm delete.
- Integrated vào ContractDetailPage cả 2 app.
### B. SignalR realtime notifications ✓
- Clean-arch 3-project split: `IRealtimeNotifier` (Application) +
`SignalRNotifier` (Api) + `NotificationPushInterceptor` (Infrastructure
SaveChanges hook). Zero caller changes — `db.Notifications.Add()` auto-push.
- Hub `/hubs/notifications` JWT via `?access_token=` query string (WebSocket
headers limit).
- FE `lib/realtime.ts` singleton connection + auto-reconnect backoff + stop
on logout. NotificationBell subscribe `notification-created` → toast +
invalidate query.
- IIS WebSocket module installed trên VPS.
### C. Form template builder CRUD + DynamicForm ✓
- BE: Upload / Update / Delete templates (multipart, FormCode regex + unique,
FieldSpec JSON validation). `.doc`/`.xls` auto-convert sang `.docx`/`.xlsx`
qua `IDocumentConverter` khi upload.
- FE admin FormsPage: upload dialog với file picker + FormCode + Loại HĐ +
FieldSpec JSON textarea. Row actions 3 nút (render / edit / delete).
- `DynamicForm` component: parse FieldSpec JSON (text/textarea/number/date/
currency/select), render form inputs. Render dialog có tab toggle Form ↔ JSON.
### D. PDF export (LibreOffice headless) ✓
- `IDocumentConverter` generalized (docx→pdf, doc→docx, xls→xlsx, etc).
- `LibreOfficeDocumentConverter` shells `soffice.exe --headless --convert-to`,
per-request temp workDir + isolated UserInstallation (concurrent-safe),
60s timeout, kill process tree.
- Endpoint: POST `/api/forms/templates/{id}/export-pdf` pipe render → PDF.
- FE Tải PDF button cạnh Tải file gốc trong render dialog.
- LibreOffice 25.8.6 installed trên VPS via `scripts/install-libreoffice.ps1`.
- E2E verified: PDF 488KB / 126 pages.
### E. Dynamic + versioned workflow per ContractType ✓
**Phase 1 — Dynamic policy selection:**
- `WorkflowPolicy` record (Domain) + registry với 2 policy: Standard (8 phase
full CCM) + SkipCcm (7 phase bỏ CCM). Map ContractType → policy theo QT docx.
- `ContractWorkflowService.ForContract()` dùng registry.
- FE xóa hardcoded `NEXT_PHASES`, dùng `contract.workflow.nextPhases` từ
`ContractDetailDto.Workflow`. `WorkflowSummaryCard` timeline visual.
- Admin `/system/workflows` page (Phase 1) với dropdown Standard/SkipCcm per
ContractType (DB override `WorkflowTypeAssignment`).
**Phase 2 — Versioned workflow (user request "Khi add quy trình mới → HĐ cũ
giữ quy trình cũ"):**
- 3 entities mới: `WorkflowDefinition` (Code+Version+IsActive+ContractType),
`WorkflowStep` (Order+Phase+Name+SlaDays), `WorkflowStepApprover`
(Kind: Role|User + AssignmentValue).
- `Contract.WorkflowDefinitionId` nullable FK — pinned at create time.
- Migration `AddVersionedWorkflows`. Seed v01 per 7 ContractType từ hardcoded
policies (Role approvers).
- `WorkflowPolicyRegistry.FromDefinition()` — build runtime policy từ
WorkflowDefinition's Steps. Role-based transitions derive từ Role-kind
approvers, User-kind fallback DeptManager (iteration 2 sẽ enable user-level).
- `ContractWorkflowService` + `ContractFeatures.Get()`: load pinned
WorkflowDefinition → FromDefinition → runtime policy.
- CreateContract pin `WorkflowDefinitionId = active version for type`.
- Admin UI `/system/workflows/:typeCode` (URL-driven, sidebar menu replaces
tabs):
- Landing: 3-col grid card per 7 type với active version badge
- Per-type page: DefinitionCard (active + history), "Archived · N HĐ còn
chạy" count, Designer modal cho create-new-version (code/name/desc,
repeatable steps, per-step approvers + Role hoặc + User select).
- Clone-from-version button cho starting point sensible.
- POST `/api/workflows` create-new-version: auto-increment Version, deactivate
old IsActive, atomic.
- Invariants:
- Unique (Code, Version)
- Chỉ 1 IsActive per ContractType tại 1 thời điểm
- HĐ cũ giữ version cũ (WorkflowDefinitionId pinned, not FK cascade)
- E2E verified: tạo QT-MB-v02 → v01 archived, HĐ mới type=5 pin v02
`policyName: "QT-MB-v02"`, 5 bước custom [2,3,7,8,9,99].
### F. Nested sidebar menu per ContractType (fe-user) ✓
- BE seed 7 type groups × 3 action leaves (28 entries) dưới `Contracts`:
- `Ct_<Code>` group + `Ct_<Code>_List/Create/Pending` leaves
- `GetMyMenuTreeQuery` generalized inherit-permission: descendants of
`Contracts` hoặc `Workflows` inherit parent CanRead (no per-leaf perm rows).
- fe-user Layout: recursive `MenuNodeRenderer` (top-level expanded, nested
collapsed). Ct_*_List → `/my-contracts?type=X`, Ct_*_Create →
`/contracts/new?type=X`, Ct_*_Pending → `/inbox?type=X`.
- MyContractsPage + InboxPage read `?type=X`, filter client-side.
- **Menu split**: admin hide `Ct_*`, user hide `Master/System/Forms/Reports`.
### G. Admin Workflows tabs → sidebar menu items ✓
- Seed 7 `Wf_<Code>` leaves dưới `Workflows` group.
- Layout resolvePath `Wf_<Code>``/system/workflows/<code>`.
- WorkflowsPage bỏ tab bar; URL param drives type selection. Landing 7-card
grid khi click top-level `Quy trình HĐ` without type.
- Inheritance: `Workflows.Read` perm → tất cả 7 leaves auto-visible.
### H. PermissionsPage 3-panel layout ✓
- Grid `lg:grid-cols-[280px_1fr_300px]`:
- Panel 1 (trái): Role list click-to-select với active ring-brand
- Panel 2 (giữa): Menu × CRUD matrix + sticky thead + search + column
bulk-toggle + row brand-tinted hover
- Panel 3 (phải): Granted progress bar + CRUD breakdown color-coded badges
(slate/emerald/amber/red) + Tip
### I. Seed master data + MyDashboard ✓
- DbInitializer: 9 departments từ QT docx (PM/QS/CCM/PRO/FIN/ACT/EQU/HRA/BOD),
5 demo suppliers (5 SupplierType), 3 demo projects. Idempotent.
- Endpoint `/api/reports/my-dashboard`: DraftsInProgress / PendingMyApproval /
DueSoon / Overdue / DraftsTotalValue.
- FE DashboardPage "Của tôi" row 4 card, hover-interactive, admin auto-hide
nếu tất cả = 0.
### J. Brand identity + content polish (earlier in session) ✓
- Solutions logo cropped (pixel-sampled #1F7DC1) + full palette brand-50..900
+ Be Vietnam Pro font.
- SlaTimer, InboxPage stat cards, DataTable skeleton, EmptyState.
- TopBar + NotificationBell + UserMenu (ERP shell).
### K. Gitea 500 fix (side-effect) ✓
- `Install-WindowsFeature Web-WebSockets` khóa section `<webSocket>`
applicationHost → all IIS sites with `<webSocket enabled="true">` sập.
- Fix: `appcmd unlock config -section:system.webServer/webSocket`.
- Documented as gotcha #25.
## Commits (chronological, partial)
```
Earlier (21/04):
c8d0070 — Attachment upload E2E
ea9ab5e — SignalR realtime E2E
166d26c — Form template builder CRUD
6bbd894 — PDF export (LibreOffice)
e459097 — DynamicForm + .doc auto-convert
cae4d84 — Dynamic workflow policy per ContractType
6197c84 — Seed master data + MyDashboard
48e91fe — Nested sidebar menu (admin)
5e0f380 — Menu split (admin hide, user show) + workflow config static
4abb559..bf1fbe3 — Brand identity (Solutions logo + palette + fonts)
346bd5d — Content polish (typography, PageHeader, Button, Input, DataTable)
290936a..2e43799 — Tier 1 UI (SlaTimer, Inbox stats, Skeleton, EmptyState)
2b6f91c — ERP shell (TopBar, NotificationBell, UserMenu)
6c0e206 — PermissionsPage iter 1 (search + stats + bulk toggle)
Today (22/04):
e7e5f2d — Versioned workflow entities + migration + designer
355bbe3 — Fix Dialog size TS (xl → lg)
f216169 — Workflows tabs → sidebar menu items
91b2da1 — PermissionsPage 3-panel layout
```
## Key architectural decisions
1. **WorkflowPolicy runtime build from WorkflowDefinition DB rows** (not stored
as JSON blob) — allows admin to edit steps/approvers granularly without
JSON parser UX.
2. **WorkflowDefinitionId pinned at contract create** — zero-cost immutability
guarantee. Old contracts protected from workflow changes by reference, not
by snapshot copy.
3. **Permission inheritance via menu ancestry** (Contracts / Workflows roots)
— keeps Permissions table small while supporting deep navigation menus.
4. **3-project clean-arch split for cross-cutting services** (realtime
notifications, document conversion) — each service has abstraction in
Application + implementation in Infra/Api.
5. **Role + User approvers per step** (data model) but only Role-kind drives
runtime guard v1 — user-level targeting deferred to iter 2.
## Runtime workflow resolution (critical path)
```
Contract.TransitionAsync:
if contract.WorkflowDefinitionId not null:
def = db.WorkflowDefinitions.Include(Steps.Approvers).First(wfId)
policy = WorkflowPolicyRegistry.FromDefinition(def)
elif admin has override in WorkflowTypeAssignments for contract.Type:
policy = Registry.ByName(override.PolicyName)
else:
policy = Registry.For(contract.Type) // hardcoded Standard/SkipCcm
if not policy.Transitions.HasKey((from, to)): throw Forbidden
if not actor.Roles.Any(r => allowed.Contains(r)): throw Forbidden
```
## Next session priority
1. **UAT với 2-3 user thật** (hard requirement từ roadmap Phase 5).
2. Roles CRUD — trường hợp admin muốn tạo custom role ngoài 12 hardcoded.
3. Email outbox (MailKit + SMTP) — BLOCKED on user providing SMTP config.
4. User-level approver targeting trong workflow runtime (data model có sẵn,
chỉ cần wire User-kind approvers vào TransitionAsync guard).
5. PermissionsPage: allow admin grant `Workflows.Read` cho non-admin role so
menu Wf_* visible.
6. Rotate credentials đã leak trong chat (SA, vrapp, JWT).
7. SQL backup daily Task Scheduler (script đã có).

View File

@ -1,6 +1,6 @@
# Schema Diagram — Luồng DB SOLUTION_ERP
> ERD đầy đủ + mối quan hệ 19 table sau Phase 3. Mermaid render ở VS Code / GitHub / Gitea.
> ERD đầy đủ + mối quan hệ **24 table** sau Tier 3 (Notifications + Versioned workflows). Mermaid render ở VS Code / GitHub / Gitea.
## 1. Full ERD
@ -22,6 +22,7 @@ erDiagram
Departments ||--o{ Contracts : "drafted-in"
Users ||--o{ Contracts : "drafter"
ContractTemplates ||--o{ Contracts : "uses"
WorkflowDefinitions ||--o{ Contracts : "pinned-policy"
Contracts ||--o{ ContractApprovals : "history"
Contracts ||--o{ ContractComments : "thread"
@ -29,6 +30,11 @@ erDiagram
Users ||--o{ ContractApprovals : "approved-by"
Users ||--o{ ContractComments : "author"
WorkflowDefinitions ||--o{ WorkflowSteps : "has"
WorkflowSteps ||--o{ WorkflowStepApprovers : "allowed-by"
Users ||--o{ Notifications : "recipient"
Users {
uniqueidentifier Id PK
nvarchar FullName "200"
@ -126,6 +132,7 @@ erDiagram
uniqueidentifier DepartmentId FK
uniqueidentifier DrafterUserId FK
uniqueidentifier TemplateId FK
uniqueidentifier WorkflowDefinitionId FK "pinned policy, nullable"
decimal GiaTri "18,2"
bit BypassProcurementAndCCM
datetime2 SlaDeadline
@ -170,6 +177,54 @@ erDiagram
int LastSeq
datetime2 UpdatedAt
}
Notifications {
uniqueidentifier Id PK
uniqueidentifier RecipientUserId FK
int Type "ContractTransition/CommentAdded/SlaExpired/..."
nvarchar Title "200"
nvarchar Body "2000"
nvarchar Link "500"
bit IsRead
datetime2 ReadAt
datetime2 CreatedAt
}
WorkflowTypeAssignments {
uniqueidentifier Id PK
int ContractType "UK"
nvarchar PolicyName "50 Standard/SkipCcm"
datetime2 UpdatedAt
uniqueidentifier UpdatedBy FK
}
WorkflowDefinitions {
uniqueidentifier Id PK
nvarchar Code "100 QT-MB / QT-TP / ..."
int Version "auto-increment per Code"
bit IsActive "chi 1 active per ContractType"
int ContractType
nvarchar Name "200"
nvarchar Description "500"
datetime2 CreatedAt
uniqueidentifier CreatedBy FK
}
WorkflowSteps {
uniqueidentifier Id PK
uniqueidentifier WorkflowDefinitionId FK
int Order
int Phase "target ContractPhase int"
nvarchar Name "200"
int SlaDays
}
WorkflowStepApprovers {
uniqueidentifier Id PK
uniqueidentifier WorkflowStepId FK
int Kind "1=Role, 2=User"
nvarchar AssignmentValue "200 RoleName or UserId Guid string"
}
```
## 2. Luồng dữ liệu chính (data flow diagram)
@ -182,18 +237,27 @@ flowchart TB
P --> MI[MenuItems]
end
subgraph MASTER ["📋 Master data (admin CRUD)"]
subgraph MASTER ["📋 Master data (admin CRUD + seed demo)"]
S[Suppliers]
PR[Projects]
DE[Departments]
end
subgraph FORMS ["📄 Form templates (seed)"]
subgraph FORMS ["📄 Form templates (admin upload)"]
CT[ContractTemplates]
CC[ContractClauses]
end
subgraph CONTRACT ["📝 Contract workflow (Phase 3 core)"]
subgraph WORKFLOW ["⚙️ Versioned workflow (admin designer)"]
WD[WorkflowDefinitions]
WS[WorkflowSteps]
WSA[WorkflowStepApprovers]
WTA[WorkflowTypeAssignments legacy override]
WD --> WS
WS --> WSA
end
subgraph CONTRACT ["📝 Contract workflow"]
C[Contracts]
CA[ContractApprovals]
CCM[ContractComments]
@ -201,39 +265,48 @@ flowchart TB
CCS[ContractCodeSequences]
end
subgraph NOTIFY ["🔔 Notification module"]
N[Notifications]
end
U -.Drafter.-> C
S --> C
PR --> C
DE --> C
CT --> C
WD -.pinned at create.-> C
C --> CA
C --> CCM
C --> CAT
C -.gen when DangDongDau.-> CCS
C -.transition event.-> N
CCM -.comment added.-> N
U -.recipient.-> N
```
## 3. Vòng đời 1 HĐ — data changes
## 3. Vòng đời 1 HĐ — data changes (với versioned workflow)
```mermaid
flowchart LR
Create[POST /contracts]
Create --> C1["Contracts INSERT<br/>Phase=2, SLA=+7d"]
Create[POST /contracts type=5]
Create --> PickWD["SELECT TOP 1 WorkflowDefinition<br/>WHERE ContractType=5 AND IsActive=1<br/>→ Id=wf-v02"]
PickWD --> C1["Contracts INSERT<br/>Phase=2, SLA=+7d, WorkflowDefinitionId=wf-v02"]
Transition1[Transition 2→3]
Transition1 --> C2["UPDATE Phase=3<br/>INSERT ContractApprovals"]
Transition1 --> LoadPolicy["Load wf-v02.Steps.Approvers<br/>WorkflowPolicyRegistry.FromDefinition(def)"]
LoadPolicy --> Guard["Check allowed roles for<br/>(from=2, to=3)"]
Guard --> C2["UPDATE Phase=3<br/>INSERT ContractApprovals<br/>INSERT Notifications bulk"]
Comment[POST /comments]
Comment --> C3[INSERT ContractComments]
Comment --> C3["INSERT ContractComments<br/>INSERT Notifications"]
Transition2[Transition 3→4→5→6→7]
Transition2 --> C4["UPDATE Phase + SlaDeadline<br/>INSERT ContractApprovals"]
NewVersion[Admin creates QT-MB-v03]
NewVersion --> NV1["INSERT WorkflowDefinition v03 IsActive=1<br/>UPDATE v02 SET IsActive=0 (atomic)"]
NV1 -.->|HĐ cũ không ảnh hưởng| C1
Transition3[Transition 7→8 BOD ký]
Transition3 --> CG["ContractCodeGenerator<br/>SERIALIZABLE tran<br/>UPSERT ContractCodeSequences"]
CG --> C5["UPDATE Contract<br/>SET MaHopDong='FLOCK 01/HĐGK/SOL&PVL/01',<br/>Phase=8"]
Transition4[Transition 8→9 HRA phát hành]
Transition4 --> C6["UPDATE Phase=9, SlaDeadline=NULL<br/>INSERT ContractApprovals"]
Transition3 --> CG["ContractCodeGenerator SERIALIZABLE<br/>UPSERT ContractCodeSequences"]
CG --> C5["UPDATE Contract<br/>SET MaHopDong, Phase=8<br/>INSERT Notifications"]
```
## 4. Index strategy
@ -245,12 +318,19 @@ flowchart LR
| Contracts | `IX_Contracts_SupplierId` | Filter HĐ theo NCC |
| Contracts | `IX_Contracts_ProjectId` | Filter HĐ theo dự án |
| Contracts | `IX_Contracts_SlaDeadline` | SLA expiry job query |
| Contracts | `IX_Contracts_WorkflowDefinitionId` | Pinned policy lookup |
| ContractApprovals | `IX_ContractApprovals_ContractId_ApprovedAt` | Timeline query theo HĐ |
| ContractComments | `IX_ContractComments_ContractId_CreatedAt` | Thread load |
| ContractAttachments | `IX_ContractAttachments_ContractId` | Attachments load |
| Suppliers/Projects/Departments | `UX_{Table}_Code` | Unique business code |
| Permissions | `UX_Permissions_RoleId_MenuKey` | 1 row / role / menu |
| MenuItems | `IX_MenuItems_ParentKey` | Tree query |
| Notifications | `IX_Notifications_RecipientUserId_IsRead_CreatedAt` | Bell badge unread count + list |
| WorkflowDefinitions | `UX_WorkflowDefinitions_Code_Version` | Unique version per code |
| WorkflowDefinitions | `IX_WorkflowDefinitions_ContractType_IsActive` | Active policy lookup |
| WorkflowSteps | `IX_WorkflowSteps_WorkflowDefinitionId_Order` | Load steps ordered |
| WorkflowStepApprovers | `IX_WorkflowStepApprovers_WorkflowStepId` | Approver load |
| WorkflowTypeAssignments | `UX_WorkflowTypeAssignments_ContractType` | 1 override per type (legacy) |
Chi tiết + cheatsheet SQL: [`database-guide.md`](database-guide.md).
@ -263,9 +343,13 @@ Chi tiết + cheatsheet SQL: [`database-guide.md`](database-guide.md).
| Contract → ContractAttachment | 1 - N | Cascade | File vật lý vẫn còn trong `wwwroot/uploads/`, cleanup riêng |
| Supplier → Contract | 1 - N | Restrict | Không xóa Supplier nếu còn HĐ tham chiếu |
| Project → Contract | 1 - N | Restrict | Tương tự |
| **WorkflowDefinition → Contract** | 1 - N | **Restrict** | **KHÔNG cascade** → HĐ cũ pin version cũ không bị xóa khi admin archive |
| Role → Permission | 1 - N | Cascade | Xóa role → clear permissions |
| MenuItem → Permission | 1 - N | Cascade | — |
| MenuItem → MenuItem (self-ref) | 1 - N | Restrict | Parent-child menu |
| **WorkflowDefinition → WorkflowStep** | 1 - N | Cascade | Delete def → remove steps (chỉ khi no Contract tham chiếu) |
| **WorkflowStep → WorkflowStepApprover** | 1 - N | Cascade | — |
| **User → Notification (RecipientUserId)** | 1 - N | Cascade | — |
## 6. Soft delete behavior
@ -278,47 +362,89 @@ Entity list áp dụng:
- Supplier, Project, Department
- Contract
- ContractTemplate, ContractClause
- WorkflowDefinition (admin archive = `IsActive=false`, xóa logic chỉ khi muốn scrub)
KHÔNG soft delete (cascade hoặc keep):
- ContractApproval, ContractComment, ContractAttachment — cascade khi Contract xóa
- Permission, MenuItem — cascade khi Role xóa
- ContractCodeSequence — không bao giờ xóa (giữ history seq)
- Notifications — không soft delete, chỉ `IsRead` flag (giữ history ngắn hạn, cleanup job sau)
- WorkflowStep / WorkflowStepApprover — cascade khi WorkflowDefinition xóa
- Identity tables (Users, Roles, ...) — Identity không support soft delete built-in
## 7. Truy vấn tiêu biểu
### Inbox HĐ chờ role của tôi
### Inbox HĐ chờ role của tôi (với versioned workflow)
```sql
-- Tương đương GetMyInboxQuery
-- Server filter (API): HĐ chờ role eligible phase theo pinned policy
-- Thực tế ContractsController resolve policy runtime:
-- def = db.WorkflowDefinitions.Include(Steps.Approvers).Where(Id == contract.WorkflowDefinitionId).FirstOrDefault()
-- policy = def != null ? Registry.FromDefinition(def) : Registry.For(contract.Type)
-- phase eligible = policy.Transitions.Where(t => t.From == contract.Phase && t.AllowedRoles.Intersect(myRoles).Any())
-- SQL tương đương:
SELECT c.Id, c.MaHopDong, c.TenHopDong, c.Phase, c.SlaDeadline, s.Name AS SupplierName, p.Name AS ProjectName
FROM Contracts c
INNER JOIN Suppliers s ON c.SupplierId = s.Id
INNER JOIN Projects p ON c.ProjectId = p.Id
WHERE c.IsDeleted = 0
AND c.Phase IN (/* phase eligible cho role hiện tại */)
AND c.Phase IN (/* phase eligible computed theo pinned workflow */)
ORDER BY c.SlaDeadline ASC;
```
### Dashboard stats
### Pick active workflow tại create-time
```sql
SELECT TOP 1 Id
FROM WorkflowDefinitions
WHERE ContractType = @Type AND IsActive = 1
ORDER BY [Version] DESC;
```
### Tạo version mới (atomic)
```sql
BEGIN TRAN;
-- Step 1: deactivate current active
UPDATE WorkflowDefinitions
SET IsActive = 0
WHERE ContractType = @Type AND IsActive = 1;
-- Step 2: compute next version
DECLARE @NextVersion INT = (SELECT ISNULL(MAX([Version]), 0) + 1 FROM WorkflowDefinitions WHERE Code = @Code);
-- Step 3: insert new active version
INSERT WorkflowDefinitions (Id, Code, [Version], IsActive, ContractType, Name, Description, CreatedAt, CreatedBy)
VALUES (NEWID(), @Code, @NextVersion, 1, @Type, @Name, @Description, GETUTCDATE(), @UserId);
-- Step 4: insert steps + approvers (batch)
-- ...
COMMIT;
```
### Dashboard stats (MyDashboard user-specific)
```sql
-- Tổng + active + overdue
SELECT
(SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0) AS Total,
(SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0 AND Phase NOT IN (9, 99)) AS Active,
(SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0 AND Phase NOT IN (9, 99) AND SlaDeadline < GETUTCDATE()) AS Overdue;
(SELECT COUNT(*) FROM Contracts
WHERE DrafterUserId = @Me AND IsDeleted = 0 AND Phase NOT IN (9, 99)) AS DraftsInProgress,
(SELECT COUNT(*) FROM Contracts
WHERE Phase IN (/* eligible phases cho role tôi */) AND IsDeleted = 0) AS PendingMyApproval,
(SELECT COUNT(*) FROM Contracts
WHERE IsDeleted = 0 AND SlaDeadline BETWEEN GETUTCDATE() AND DATEADD(DAY, 2, GETUTCDATE())) AS DueSoon,
(SELECT COUNT(*) FROM Contracts
WHERE IsDeleted = 0 AND SlaDeadline < GETUTCDATE() AND Phase NOT IN (9, 99)) AS Overdue,
(SELECT ISNULL(SUM(GiaTri), 0) FROM Contracts
WHERE DrafterUserId = @Me AND Phase = 2) AS DraftsTotalValue;
```
-- By phase
SELECT Phase, COUNT(*) FROM Contracts WHERE IsDeleted = 0 GROUP BY Phase;
### Notifications unread count (bell badge)
-- Top 5 NCC
SELECT TOP 5 c.SupplierId, s.Name, COUNT(*) AS Cnt, SUM(c.GiaTri) AS TotalValue
FROM Contracts c
INNER JOIN Suppliers s ON c.SupplierId = s.Id
WHERE c.IsDeleted = 0
GROUP BY c.SupplierId, s.Name
ORDER BY COUNT(*) DESC;
```sql
SELECT COUNT(*) FROM Notifications
WHERE RecipientUserId = @Me AND IsRead = 0;
```
### Gen mã HĐ atomic
@ -327,32 +453,63 @@ ORDER BY COUNT(*) DESC;
BEGIN TRAN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
MERGE ContractCodeSequences AS tgt
USING (SELECT @Prefix AS Prefix) AS src ON tgt.Prefix = src.Prefix
WHEN MATCHED THEN UPDATE SET LastSeq = LastSeq + 1, UpdatedAt = GETUTCDATE()
WHEN NOT MATCHED THEN INSERT (Prefix, LastSeq, UpdatedAt) VALUES (@Prefix, 1, GETUTCDATE());
UPDATE ContractCodeSequences
SET LastSeq = LastSeq + 1, UpdatedAt = GETUTCDATE()
WHERE Prefix = @Prefix;
IF @@ROWCOUNT = 0
INSERT ContractCodeSequences (Prefix, LastSeq, UpdatedAt)
VALUES (@Prefix, 1, GETUTCDATE());
SELECT LastSeq FROM ContractCodeSequences WHERE Prefix = @Prefix;
COMMIT;
```
(EF Core impl dùng `UPDATE + IF ROWCOUNT = 0 INSERT` thay MERGE — tương đương nhưng an toàn hơn với SERIALIZABLE).
## 8. Migration lịch sử
| # | Migration | Tables added |
| # | Migration | Tables added / changed |
|---|---|---|
| 1 | `Init` | 7 Identity tables |
| 2 | `AddMasterData` | Suppliers, Projects, Departments |
| 3 | `AddPermissions` | MenuItems, Permissions |
| 4 | `AddForms` | ContractTemplates, ContractClauses |
| 5 | `AddContractsWorkflow` | Contracts, ContractApprovals, ContractComments, ContractAttachments, ContractCodeSequences |
| 6 | `AddNotifications` | Notifications |
| 7 | `AddWorkflowTypeAssignments` | WorkflowTypeAssignments (admin override legacy) |
| 8 | `AddVersionedWorkflows` | WorkflowDefinitions, WorkflowSteps, WorkflowStepApprovers + Contracts.WorkflowDefinitionId FK |
Tổng: **19 bảng** (+ `__EFMigrationsHistory` hệ thống).
Tổng: **24 bảng** (+ `__EFMigrationsHistory` hệ thống).
## 9. Liên quan
## 9. Versioned workflow invariants
```
1. UNIQUE (WorkflowDefinitions.Code, Version)
→ không 2 row cùng Code + Version (enforce qua IX unique)
2. Chỉ 1 WorkflowDefinition.IsActive = true per ContractType tại 1 thời điểm
→ enforce qua CreateWorkflowDefinitionCommand: UPDATE deactivate trước INSERT, cùng transaction
3. Contract.WorkflowDefinitionId pinned at create → không update sau đó
→ CreateContractCommandHandler pick active version 1 lần, save
4. ON DELETE Restrict FK Contract.WorkflowDefinitionId → WorkflowDefinitions.Id
→ admin không thể DELETE WorkflowDefinition nếu còn Contract pin
→ admin archive = set IsActive=false thôi, row vẫn tồn tại
5. Runtime policy resolution order (ContractWorkflowService):
a. If contract.WorkflowDefinitionId NOT NULL → load def → FromDefinition
b. Else if admin override ở WorkflowTypeAssignments for contract.Type → Registry.ByName
c. Else → Registry.For(contract.Type) (hardcoded Standard/SkipCcm)
6. WorkflowStepApprover.Kind
- 1=Role: AssignmentValue là RoleName (Domain/Identity/AppRoles constants)
- 2=User: AssignmentValue là UserId Guid string
- Runtime guard hiện tại chỉ dùng Role-kind (User-kind data model ready, enable iter sau)
```
## 10. Liên quan
- [`database-guide.md`](database-guide.md) — conventions + migration workflow + cheatsheet đầy đủ
- [`../architecture.md`](../architecture.md) — layered architecture + data flow
- [`../workflow-contract.md`](../workflow-contract.md) — state machine spec
- [`../workflow-contract.md`](../workflow-contract.md) — state machine spec + versioned
- [`../flows/`](../flows/) — sequence diagrams

View File

@ -197,6 +197,109 @@ Tương tự khi dùng URL Rewrite `<serverVariables>` cần unlock `system.webS
**Cảnh báo co-existence:** Trên VPS shared với project khác, enable feature mới qua `Install-WindowsFeature` có thể làm sập site project khác. Luôn test all site sau mỗi enable.
## SignalR / Realtime
### 26. SignalR WebSocket không cho custom Authorization header
**Triệu chứng:** `new HubConnectionBuilder().withUrl('/hubs/x', { headers: { Authorization: ... } })` — WebSocket transport vẫn 401.
**Nguyên nhân:** Browser WebSocket API không cho set custom headers cho handshake. Chỉ 2 transport khác (SSE / LongPolling) mới dùng headers.
**Fix:**
- FE: dùng `accessTokenFactory: () => token` — SignalR client tự append `?access_token=` query cho WebSocket
- BE: Wire JWT bearer `OnMessageReceived` để đọc token từ query khi path matches `/hubs/*`:
```csharp
options.Events = new JwtBearerEvents {
OnMessageReceived = ctx => {
var accessToken = ctx.Request.Query["access_token"];
var path = ctx.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
ctx.Token = accessToken;
return Task.CompletedTask;
}
};
```
### 27. SignalR SaveChangesInterceptor — capture Added ở SavingChanges, push ở SavedChanges
**Lý do:** SavedChanges chỉ có entries sau commit thành công. Nhưng ở SavedChanges thì `EntityEntry.State` đã về `Unchanged` → không thể filter `Added`.
**Fix:** 2-phase pattern:
```csharp
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(...) {
_pending = eventData.Context.ChangeTracker.Entries<Notification>()
.Where(e => e.State == EntityState.Added)
.Select(e => e.Entity).ToList();
return base.SavingChangesAsync(...);
}
public override async ValueTask<int> SavedChangesAsync(..., int result, ...) {
foreach (var n in _pending) await _realtimeNotifier.PushAsync(n);
_pending.Clear();
return result;
}
```
## DevOps / CI/CD
### 28. LibreOffice download URL 404 khi pin wrong version
**Triệu chứng:** `Invoke-WebRequest https://download.documentfoundation.org/libreoffice/stable/25.2.7/...` → 404.
**Nguyên nhân:** LibreOffice mirror chỉ giữ vài version mới nhất. 25.2.7, 24.8.7 không có. Chỉ 25.8.6 tồn tại tại thời điểm cài.
**Fix:** Check mirror URL trước khi pin. Dùng `Invoke-WebRequest -Method Head` verify trước download thật.
### 29. PowerShell 5.1 `>> $GITHUB_PATH` ghi UTF-16 → NUL byte crash Gitea Actions
**Triệu chứng:** Gitea Actions job fail với "NUL byte in PATH". `echo "C:\\dotnet" >> $env:GITHUB_PATH`.
**Nguyên nhân:** PS 5.1 default encoding UTF-16 LE BOM khi redirect `>>`. Gitea reads PATH as UTF-8 → NUL byte xuất hiện sau mỗi ASCII char.
**Fix:** Dùng `Out-File -Encoding utf8 -Append`:
```powershell
"C:\dotnet" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
```
Hoặc drop step GITHUB_PATH hoàn toàn nếu NSSM PATH đã có sẵn dotnet+node.
### 30. PS 5.1 scripts với Vietnamese diacritics → parser error
**Triệu chứng:** `Cannot parse script: Unexpected character` khi chạy PS script có text tiếng Việt inline.
**Nguyên nhân:** PS 5.1 đọc file script với ANSI codepage (Windows-1258 hoặc default 1252), không phải UTF-8.
**Fix (1):** Save script với BOM UTF-8 (Write-Host có dấu vẫn work):
```powershell
[System.IO.File]::WriteAllText($path, $content, [System.Text.Encoding]::UTF8)
```
**Fix (2, safer):** Rewrite script ASCII-only. Text tiếng Việt nằm trong log messages thay dùng code:
```powershell
Write-Host "Setup IIS sites done" # thay vi "Hoan tat"
```
## TypeScript / FE
### 31. Dialog `size="xl"` TS2322 nếu variant không khai báo
**Triệu chứng:** `<Dialog size="xl">``Type '"xl"' is not assignable to type '"sm" | "md" | "lg"'`.
**Fix:** Sửa usage về `"lg"`, hoặc add `"xl"` vào `DialogSize` type union trong `components/ui/Dialog.tsx`. Đừng lazy `as any`.
## FE architecture
### 32. NavLink `end` prop cho query-param URL variants
**Triệu chứng:** `/contracts?type=1` highlight cả `/contracts` lẫn `/contracts?type=2` cùng lúc.
**Nguyên nhân:** Default NavLink `startsWith` match. Query string không parse distinct paths.
**Fix:** `end={path.includes('?')}` trong resolvePath để query-variants match exact:
```tsx
<NavLink to={path} end={path.includes('?')}>
```
## Checklist debug bug mới
1. Build pass không? → fail → check using + package version compat
@ -207,4 +310,6 @@ Tương tự khi dùng URL Rewrite `<serverVariables>` cần unlock `system.webS
6. Nếu TS error → check `erasableSyntaxOnly`, `verbatimModuleSyntax`
7. Nếu EF expression tree → tách logic ra ngoài query
8. Nếu Unicode CLI → dùng file payload
9. Nếu workflow 403 → check FE NEXT_PHASES sync BE
9. Nếu workflow 403 → check FE `workflow.nextPhases` sync từ BE pinned policy
10. Nếu SignalR 401 → dùng `accessTokenFactory` + BE OnMessageReceived hook (#26)
11. Nếu PS 5.1 script fail → check encoding UTF-8 / BOM / ASCII-only (#30)

View File

@ -98,7 +98,7 @@ Ký hiệu: `R` = read, `W` = write/update draft, `A` = approve (chuyển phase
| Quá SLA → auto-approve | Drafter + role giữ phase | email + in-app (log audit) |
| Reject (quay về `DangSoanThao`) | Drafter | email + in-app |
## 7. Data model implication (cho Phase 3)
## 7. Data model implication (cho Phase 3 + Tier 3 versioned)
```csharp
// Domain
@ -117,40 +117,157 @@ public enum ContractPhase {
public class Contract : AuditableEntity {
public Guid Id { get; set; }
public string MaHopDong { get; set; } // tự gen theo RG-001
public string? MaHopDong { get; set; } // tự gen theo RG-001
public ContractType Type { get; set; } // HDTP, HDGK, NCC, HDDV...
public ContractPhase Phase { get; set; }
public Guid SupplierId { get; set; }
public Guid ProjectId { get; set; }
public decimal GiaTri { get; set; }
public bool BypassProcurementAndCCM { get; set; }
public DateTime? SlaDeadline { get; set; } // khi nào phase hiện tại hết hạn
// ...
public List<ContractComment> Comments { get; set; } // thread góp ý phase 3
public List<ContractApproval> Approvals { get; set; } // ai ký phase nào, lúc nào
public List<ContractAttachment> Attachments { get; set; } // scan bản gốc, file export
public DateTime? SlaDeadline { get; set; }
public bool SlaWarningSent { get; set; }
// Tier 3: pin policy version at create-time cho immutability
public Guid? WorkflowDefinitionId { get; set; }
public List<ContractComment> Comments { get; set; }
public List<ContractApproval> Approvals { get; set; }
public List<ContractAttachment> Attachments { get; set; }
}
public class ContractApproval {
public Guid ContractId { get; set; }
public ContractPhase Phase { get; set; }
public Guid ApproverUserId { get; set; }
public ContractPhase FromPhase { get; set; }
public ContractPhase ToPhase { get; set; }
public Guid? ApproverUserId { get; set; } // null = system (SLA auto)
public DateTime? ApprovedAt { get; set; }
public ApprovalDecision Decision { get; set; } // Approve | Reject | AutoApprove
public ApprovalDecision Decision { get; set; } // Pending | Approve | Reject | AutoApprove
public string? Comment { get; set; }
}
// ==================== Tier 3: versioned workflow ====================
public class WorkflowDefinition : AuditableEntity {
public Guid Id { get; set; }
public string Code { get; set; } = ""; // "QT-MB", "QT-TP", "QT-NCC", ...
public int Version { get; set; } // 1, 2, 3, ... auto-increment per Code
public bool IsActive { get; set; } // chỉ 1 = true per ContractType
public ContractType ContractType { get; set; }
public string Name { get; set; } = ""; // "Quy trình Mua bán v02"
public string? Description { get; set; }
public List<WorkflowStep> Steps { get; set; } = new();
}
public class WorkflowStep {
public Guid Id { get; set; }
public Guid WorkflowDefinitionId { get; set; }
public int Order { get; set; } // thứ tự step trong định nghĩa
public ContractPhase Phase { get; set; } // target phase
public string Name { get; set; } = ""; // "Kiểm tra CCM"
public int SlaDays { get; set; } // SLA ngày cho phase này
public List<WorkflowStepApprover> Approvers { get; set; } = new();
}
public class WorkflowStepApprover {
public Guid Id { get; set; }
public Guid WorkflowStepId { get; set; }
public ApproverKind Kind { get; set; } // Role | User
public string AssignmentValue { get; set; } = ""; // RoleName hoặc UserId Guid string
}
public enum ApproverKind { Role = 1, User = 2 }
```
**Service chính:**
- `IContractWorkflowService.TransitionAsync(contractId, targetPhase, userId, comment)` — check guard + update state + tạo approval + notify
- `IContractCodeGenerator.GenerateAsync(projectId, type, supplierId)`dùng SEMAPHORE/transaction tránh race condition
- `ISlaExpiryJob` — hosted service chạy mỗi 15 phút, auto-approve các HĐ quá hạn
- `IContractWorkflowService.TransitionAsync(contractId, targetPhase, userId, comment)` resolve policy, check guard, update state, tạo approval, emit notification
- `IContractCodeGenerator.GenerateAsync(projectId, type, supplierId)`SERIALIZABLE transaction tránh race
- `SlaExpiryJob : BackgroundService` — 15min, auto-approve quá hạn với Decision=AutoApprove
- `IRealtimeNotifier` (SignalR impl) — push vào group User-{Id} khi Notification created
## 7bis. Policy resolution — versioned workflow
```mermaid
sequenceDiagram
participant User as Actor
participant API as ContractsController
participant WF as ContractWorkflowService
participant DB as WorkflowDefinitions
User->>API: POST /contracts/{id}/transitions {targetPhase}
API->>WF: TransitionAsync(id, targetPhase, userId, comment)
alt Contract.WorkflowDefinitionId != null (Tier 3 pinned)
WF->>DB: Include(Steps.Approvers).First(Id == wfId)
DB-->>WF: def
WF->>WF: policy = Registry.FromDefinition(def)
else Admin override in WorkflowTypeAssignments
WF->>DB: Find(ContractType == c.Type)
DB-->>WF: override
WF->>WF: policy = Registry.ByName(override.PolicyName)
else Legacy fallback
WF->>WF: policy = Registry.For(c.Type) // hardcoded Standard/SkipCcm
end
WF->>WF: check (from, to) ∈ policy.Transitions
WF->>WF: check actor.Roles ∩ allowedRoles != ∅
WF->>DB: UPDATE Phase + INSERT ContractApproval + INSERT Notifications
WF-->>API: 200
```
## 7ter. Admin designer flow (tạo version mới)
```
Admin → /system/workflows → click type "HĐ Mua bán"
→ /system/workflows/MuaBan
→ thấy active version QT-MB-v01 + history
→ click "Tạo phiên bản mới" → Designer modal (có thể Clone từ v01)
- Code: QT-MB (auto-fill)
- Version: v02 (auto-compute max+1)
- Name + Description
- Steps (repeatable):
[Order 1] Phase=2 (DangSoanThao) SLA=7 days Approvers: +Role Drafter, +Role DeptManager
[Order 2] Phase=3 (DangGopY) SLA=7 days Approvers: +Role ProjectManager, +User {userId}
...
- Save → POST /api/workflows
BE: auto Version=max+1, deactivate QT-MB-v01.IsActive=0, insert v02.IsActive=1, atomic
→ trở về /system/workflows/MuaBan → v02 active, v01 archived "N HĐ còn chạy"
→ HĐ cũ pin v01 vẫn chạy v01 (Contract.WorkflowDefinitionId không đổi)
→ HĐ mới tạo sau đây sẽ pin v02
```
## 8. Business rules summary
1. **Một role chỉ có 1 phase active tại 1 thời điểm** cho 1 HĐ.
2. **Auto-approve nếu quá SLA** nhưng phải log `Decision=AutoApprove` rõ ràng trong `ContractApproval`.
2. **Auto-approve nếu quá SLA** — phải log `Decision=AutoApprove` rõ ràng trong `ContractApproval`.
3. **Reject → quay về `DangSoanThao`** — Drafter nhận lại, toàn bộ approval trước đó bị invalidate (kept as history).
4. **Không cho xóa HĐ** đã qua phase 5 (`DangInKy`) — chỉ soft delete.
5. **Mã HĐ** gen theo `forms-spec.md § RG-001` — chỉ gen khi transition sang phase 5.
6. **Audit log đầy đủ** — mọi transition đều ghi `AuditLog(entityId, action, oldPhase, newPhase, userId, timestamp, diff)`.
5. **Mã HĐ** gen theo `forms-spec.md § RG-001` — chỉ gen khi transition sang phase `DangDongDau` (8).
6. **Audit log** — mọi transition đều insert `ContractApprovals` row với actor + timestamp + phase before/after.
7. **Versioned workflow**`Contract.WorkflowDefinitionId` pin tại create-time, **không update sau đó**. Admin tạo version mới ảnh hưởng HĐ tương lai, HĐ cũ giữ version cũ.
8. **Chỉ 1 active version per ContractType** — enforce qua business logic trong `CreateWorkflowDefinitionCommand` (atomic deactivate + insert).
## 9. Code pointers (Tier 3)
**Domain:**
- `Domain/Contracts/WorkflowDefinition.cs`
- `Domain/Contracts/WorkflowStep.cs`
- `Domain/Contracts/WorkflowStepApprover.cs` (+ `ApproverKind` enum)
- `Domain/Contracts/WorkflowPolicy.cs` (record + `WorkflowPolicies.Standard/SkipCcm` + `WorkflowPolicyRegistry.FromDefinition` + `ForContract`)
**Application:**
- `Application/Contracts/WorkflowAdminFeatures.cs`:
- `GetWorkflowAdminOverviewQuery` — landing per-type + active + history
- `CreateWorkflowDefinitionCommand` — auto Version + atomic deactivate old
**Infrastructure:**
- `Infrastructure/Services/ContractWorkflowService.cs``LoadPolicyAsync(contractId)` resolution order
**Api:**
- `Api/Controllers/WorkflowsController.cs` — GET overview, GET per-type, POST create-version
**FE-Admin:**
- `fe-admin/src/pages/system/WorkflowsPage.tsx` — URL-driven, landing + per-type
- `fe-admin/src/components/workflow/WorkflowDesigner.tsx` — modal Steps + Approvers
- `fe-admin/src/components/workflow/DefinitionCard.tsx` — active + history card