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