- 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>
10 KiB
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 ✓
IFileStorageabstraction +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/notificationsJWT via?access_token=query string (WebSocket headers limit). - FE
lib/realtime.tssingleton connection + auto-reconnect backoff + stop on logout. NotificationBell subscribenotification-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/.xlsauto-convert sang.docx/.xlsxquaIDocumentConverterkhi 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).
DynamicFormcomponent: parse FieldSpec JSON (text/textarea/number/date/ currency/select), render form inputs. Render dialog có tab toggle Form ↔ JSON.
D. PDF export (LibreOffice headless) ✓
IDocumentConvertergeneralized (docx→pdf, doc→docx, xls→xlsx, etc).LibreOfficeDocumentConvertershellssoffice.exe --headless --convert-to, per-request temp workDir + isolated UserInstallation (concurrent-safe), 60s timeout, kill process tree.- Endpoint: POST
/api/forms/templates/{id}/export-pdfpipe 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:
WorkflowPolicyrecord (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ùngcontract.workflow.nextPhasestừContractDetailDto.Workflow.WorkflowSummaryCardtimeline visual. - Admin
/system/workflowspage (Phase 1) với dropdown Standard/SkipCcm per ContractType (DB overrideWorkflowTypeAssignment).
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.WorkflowDefinitionIdnullable 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/workflowscreate-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/Pendingleaves
GetMyMenuTreeQuerygeneralized inherit-permission: descendants ofContractshoặcWorkflowsinherit parent CanRead (no per-leaf perm rows).- fe-user Layout: recursive
MenuNodeRenderer(top-level expanded, nested collapsed). Ct_List →/my-contracts?type=X, CtCreate →/contracts/new?type=X, Ct*_Pending →/inbox?type=X. - MyContractsPage + InboxPage read
?type=X, filter client-side. - Menu split: admin hide
Ct_*, user hideMaster/System/Forms/Reports.
G. Admin Workflows tabs → sidebar menu items ✓
- Seed 7
Wf_<Code>leaves dướiWorkflowsgroup. - 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.Readperm → 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-WebSocketskhó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
- WorkflowPolicy runtime build from WorkflowDefinition DB rows (not stored as JSON blob) — allows admin to edit steps/approvers granularly without JSON parser UX.
- WorkflowDefinitionId pinned at contract create — zero-cost immutability guarantee. Old contracts protected from workflow changes by reference, not by snapshot copy.
- Permission inheritance via menu ancestry (Contracts / Workflows roots) — keeps Permissions table small while supporting deep navigation menus.
- 3-project clean-arch split for cross-cutting services (realtime notifications, document conversion) — each service has abstraction in Application + implementation in Infra/Api.
- 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
- UAT với 2-3 user thật (hard requirement từ roadmap Phase 5).
- Roles CRUD — trường hợp admin muốn tạo custom role ngoài 12 hardcoded.
- Email outbox (MailKit + SMTP) — BLOCKED on user providing SMTP config.
- User-level approver targeting trong workflow runtime (data model có sẵn, chỉ cần wire User-kind approvers vào TransitionAsync guard).
- PermissionsPage: allow admin grant
Workflows.Readcho non-admin role so menu Wf_* visible. - Rotate credentials đã leak trong chat (SA, vrapp, JWT).
- SQL backup daily Task Scheduler (script đã có).