# 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_` group + `Ct__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_` leaves dưới `Workflows` group. - Layout resolvePath `Wf_` → `/system/workflows/`. - 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 `` ở applicationHost → all IIS sites with `` 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ó).