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>
207 lines
10 KiB
Markdown
207 lines
10 KiB
Markdown
# 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ó).
|