[CLAUDE] Phase2: Form Engine MVP + docs (gotchas, skill, handoff)

Backend Forms:
- Domain/Forms: ContractTemplate (FormCode, Name, ContractType, FileName, StoragePath, Format, FieldSpec JSON, IsActive) + ContractClause
- EF config voi unique FormCode + query filter IsDeleted
- DbSets + IApplicationDbContext update
- Migration AddForms (bang 14 total)
- Packages: DocumentFormat.OpenXml 3.x + ClosedXML 0.105+
- Application/Forms:
  - IFormRenderer interface + RenderResult record
  - FormFeatures.cs: List/Get/Render CQRS
  - IWebHostEnvironmentLocator (abstract IWebHostEnvironment)
- Infrastructure/Forms:
  - DocxRenderer: OpenXml-based placeholder {{field}} replace, handle split runs (gom text tat ca <w:t> trong paragraph, replace, gan lai text dau + clear rest)
  - XlsxRenderer: ClosedXML cell value replace
  - FormRenderer router theo format docx/xlsx
- Api:
  - FormsController: GET /templates (filter type, onlyActive), GET /templates/{id}, POST /templates/{id}/render (return file)
  - WebHostEnvironmentLocator impl
- DbInitializer SeedContractTemplatesAsync: seed 8 template metadata, IsActive=true chi khi file ton tai

Templates vat ly:
- Copy 5 .docx/.xlsx tu FORM/ sang wwwroot/templates/
- 3 .doc (FO-002.02/03/06) chua convert: IsActive=false (Word COM bi stuck luc test, can retry voi DisplayAlerts=0 hoac LibreOffice)
- scripts/convert-doc-to-docx.ps1 (Word COM automation)

Frontend fe-admin:
- types/forms.ts: ContractTemplate + ContractTypeLabel
- pages/forms/FormsPage.tsx: list templates + Render dialog (paste JSON data → download .docx/.xlsx)
- Route /forms them vao App.tsx

Bug fix:
- SpaceProcessingModeValues namespace: wrap EnumValue<> full path
- SaveAs2($path, 16) thay vi SaveAs([ref], [ref]) — PowerShell type issue
- Word COM stuck: kill process, skip .doc cho MVP

Docs (theo yeu cau user):
- docs/gotchas.md MOI: 17 pitfalls nhom theo tech stack / EF Core / OpenXml / JSON / dev workflow
- .claude/skills/form-engine/SKILL.md: placeholder → full spec (algorithm + code pointers + API + limitations)
- .claude/skills/permission-matrix/SKILL.md: placeholder → full spec (BE policy + FE guard + seed + pitfalls)
- docs/HANDOFF.md MOI: brief 5 phut cho session sau (run quickstart + where we are + next steps + file tree + gotchas ref)
- docs/STATUS.md: update cumulative stats + next up Phase 3
- docs/changelog/migration-todos.md: tick Phase 2 iteration 1 items + add iteration 2 list
- docs/changelog/sessions/2026-04-21-1200-phase2-form-engine.md: session log
- CLAUDE.md root: them reference den gotchas + HANDOFF

E2E verified:
- GET /api/forms/templates (onlyActive=false) → 8 templates
- POST /api/forms/templates/{FO-002.05}/render voi data dict → HTTP 200 + file .docx 482KB (Microsoft Word 2007+ OK)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-21 12:01:11 +07:00
parent 54d6c9ba52
commit 5113e4c771
37 changed files with 2379 additions and 88 deletions

155
docs/HANDOFF.md Normal file
View File

@ -0,0 +1,155 @@
# HANDOFF — Brief 5 phút cho session tiếp theo
**Last updated:** 2026-04-21 12:00 (cuối Phase 2 MVP)
## Ở đâ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 (9 phase state machine) | 📋 Next |
| 4 Report + Polish | 📋 Queue |
| 5 Production (CI/CD IIS) | 📋 Queue |
## Run nhanh
```powershell
# Terminal 1 — API (auto seed 8 template lần đầu)
dotnet run --project src\Backend\SolutionErp.Api
# Terminal 2 — Admin FE
cd fe-admin && npm run dev # → http://localhost:8082
# Terminal 3 — User FE
cd fe-user && npm run dev # → http://localhost:8080
```
Login: `admin@solutionerp.local` / `Admin@123456`
Điểm cần test ngay:
- `/forms` → render FO-002.05 → download .docx (Phase 2 MVP)
- `/system/permissions` → chọn role → tick matrix
- `/master/suppliers|projects|departments` → CRUD
## Cần làm kế tiếp (ưu tiên)
### A. Phase 3 — Workflow (item lớn, ~3 tuần work)
**Đọc trước:**
1. [`workflow-contract.md`](workflow-contract.md) — spec 9 phase + role matrix
2. [`flows/contract-approval-flow.md`](flows/contract-approval-flow.md) — sequence diagram
3. [`flows/sla-expiry-flow.md`](flows/sla-expiry-flow.md) — BackgroundService auto-approve
4. [`forms-spec.md#RG-001`](forms-spec.md) — format mã HĐ
**Deliverable chính:**
- Entity: `Contract` (Phase, SlaDeadline, BypassProcurementAndCCM, DraftData) + `ContractApproval` + `ContractComment` + `ContractAttachment`
- `IContractWorkflowService.TransitionAsync()` — state guard (9 phase adjacency) + role guard
- `IContractCodeGenerator` (implement RG-001) với transaction SERIALIZABLE + `ContractCodeSequences` table
- `SlaExpiryJob` BackgroundService — auto-approve HĐ quá hạn mỗi 15 phút
- `INotificationService` — email (MailKit) + in-app
- API `POST /api/contracts/{id}/transitions`
- FE `/inbox` (list HĐ chờ tôi xử lý theo role × phase)
- FE `/contracts/{id}` detail — timeline 9 phase, approval panel, comment thread, attachment upload
### B. Phase 2 iteration 2 (nếu user muốn polish Form Engine)
- Convert 3 file `.doc` (retry Word COM với timeout OR LibreOffice)
- Field spec JSON → dynamic form builder
- `{{#loop}}...{{/loop}}` support
- PDF convert
- FE upload template UI
### C. Quick wins (không block phase)
- FE Users management + Roles CRUD (test permission với non-admin role)
- fe-user sync menu động (đang hardcode)
## 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)
## File đang active
```
SOLUTION_ERP/
├── src/Backend/ (Clean Arch, 4 project, .NET 10)
│ ├── SolutionErp.Domain/
│ │ ├── Common/ BaseEntity, AuditableEntity
│ │ ├── Contracts/ ContractType, ContractPhase, ApprovalDecision
│ │ ├── Forms/ ContractTemplate, ContractClause ← Phase 2
│ │ ├── Identity/ User, Role, MenuItem, Permission, AppRoles, MenuKeys
│ │ └── Master/ Supplier, Project, Department, SupplierType
│ ├── SolutionErp.Application/
│ │ ├── Auth/ Login, Refresh, Me
│ │ ├── Common/ Exceptions, Behaviors, Interfaces, Models
│ │ ├── Forms/ FormFeatures (List/Get/Render) ← Phase 2
│ │ ├── Master/ Suppliers, Projects, Departments CQRS
│ │ └── Permissions/ GetMyMenuTree, matrix upsert
│ ├── SolutionErp.Infrastructure/
│ │ ├── Forms/ DocxRenderer, XlsxRenderer, FormRenderer ← Phase 2
│ │ ├── Identity/ JwtSettings, JwtTokenService
│ │ ├── Persistence/ DbContext, DbInitializer, Interceptors, Migrations (4)
│ │ └── Services/ DateTimeService
│ └── SolutionErp.Api/
│ ├── Authorization/ MenuPermissionHandler + Requirement
│ ├── Controllers/ Auth, Suppliers, Projects, Departments, Menus, Roles, Permissions, Forms
│ ├── Middleware/ GlobalExceptionMiddleware
│ ├── Services/ CurrentUserService, WebHostEnvironmentLocator
│ └── wwwroot/templates/ 5 file .docx/.xlsx ← Phase 2
├── fe-admin/ (7 page)
│ └── src/pages/
│ ├── LoginPage
│ ├── DashboardPage
│ ├── master/SuppliersPage, ProjectsPage, DepartmentsPage
│ ├── system/PermissionsPage
│ └── forms/FormsPage ← Phase 2
├── fe-user/ (2 page — Login + Inbox placeholder)
├── docs/ (24 file — STATUS, PROJECT-MAP, workflow, forms-spec, database-guide, 6 flow, gotchas, 4 session log, 1 handoff)
└── .claude/skills/ (3 skill — contract-workflow placeholder, form-engine + permission-matrix full spec)
```
## Git state
```
49a5f57..(sẽ là commit 5) — Phase 2 MVP
54d6c9b — Phase 1.2 CRUD + Permission
49a5f57 — Docs database-guide + flows
702411f — Phase 1 foundation
25dad7f — Phase 0 scaffold
Branch: main
Remote: chưa (Gitea URL chờ user)
```
## Credentials + URLs
```
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`
## Đánh giá nhanh
**Tốt:**
- Build pass 100% cả BE + FE
- E2E test: login + CRUD + render template đều pass
- Docs đầy đủ: 24 file, có session log mỗi chunk, gotchas library tích lũy
**Rủi ro:**
- fe-user còn thô — Phase 3 phải build inbox
- Form render chỉ MVP — loop table + PDF chưa có
- Permission matrix chưa test thực với non-admin user

View File

@ -2,73 +2,85 @@
> **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 11:30
**Last updated:** 2026-04-21 12:00
## 📍 Phase hiện tại: **Phase 1 — Alpha Core (đợt 2 xong)** — sẵn sàng Phase 2 Form Engine
## 📍 Phase hiện tại: **Phase 2 Form Engine (MVP xong)** — sẵn sàng Phase 3 Workflow
## 🔥 In Progress
_(không có — Phase 1 đợt 2 hoàn tất)_
_(không có — tạm nghỉ chờ user approve move on)_
## ✅ Recently Done (newest on top)
| Ngày | Ai | Task | Commit |
|---|---|---|---|
| 2026-04-21 | Claude | **Phase 1 đợt 2 HOÀN TẤT** — BE: Supplier/Project/Department CRUD + Permission Matrix (MenuItem/Permission + Authorization handler) + 2 migration. FE: DataTable/Dialog generic, usePermission, PermissionGuard, 3 trang CRUD admin, Permission Matrix page, Layout menu động | (sắp commit) |
| 2026-04-21 | Claude | **Phase 2 Form Engine MVP** — BE: ContractTemplate/ContractClause entities + OpenXml + ClosedXML renderer (placeholder `{{field}}`) + FormsController + seed 8 template. FE: FormsPage list + render dialog. `/api/forms/templates/{id}/render` trả file .docx/.xlsx 482KB OK | (sắp commit) |
| 2026-04-21 | Claude | **Docs:** `gotchas.md` (17 pitfalls) + update 2 skill (form-engine, permission-matrix) từ placeholder → full spec | (sắp commit) |
| 2026-04-21 | Claude | **Phase 1 đợt 2** — BE CRUD Supplier/Project/Department + Permission Matrix + FE 4 page | `54d6c9b` |
| 2026-04-21 | Claude | **Docs addition**`database-guide.md` + `flows/` 6 doc | `49a5f57` |
| 2026-04-21 | Claude | **Phase 1 foundation HOÀN TẤT** — BE Clean Arch + Identity + JWT + FE 2 app + Tailwind 4 + login E2E | `702411f` |
| 2026-04-21 | Claude | **Phase 0 HOÀN TẤT** — scaffold + parse FORM/QUY_TRINH + docs + skills + git init | `25dad7f` |
| 2026-04-21 | Claude | **Phase 1 foundation** — BE Clean Arch + Identity + JWT + FE 2 app + login E2E | `702411f` |
| 2026-04-21 | Claude | **Phase 0** — scaffold + parse FORM/QUY_TRINH + docs + git init | `25dad7f` |
Session logs:
- [`changelog/sessions/2026-04-21-1045-phase0-scaffold.md`](changelog/sessions/2026-04-21-1045-phase0-scaffold.md)
- [`changelog/sessions/2026-04-21-1100-phase1-foundation.md`](changelog/sessions/2026-04-21-1100-phase1-foundation.md)
- [`changelog/sessions/2026-04-21-1130-phase1-cruds-permission.md`](changelog/sessions/2026-04-21-1130-phase1-cruds-permission.md)
- [`changelog/sessions/2026-04-21-1200-phase2-form-engine.md`](changelog/sessions/2026-04-21-1200-phase2-form-engine.md)
## 🎯 Next up — Phase 2 Form Engine (có thể bắt ngay)
Gotchas library: [`gotchas.md`](gotchas.md) — 17 pitfalls đã gặp + cách xử lý.
- [ ] Convert 3 file `.doc` (FO-002.02/03/06) → `.docx` qua PowerShell COM hoặc LibreOffice headless
- [ ] Parse chi tiết field specs cho 5 template HĐ → JSON spec
- [ ] Add NuGet: DocumentFormat.OpenXml, ClosedXML
- [ ] `Domain/Entities/ContractTemplate`, `ContractClause` + EF config
- [ ] `Application/Forms/Services/IFormRenderer` + `DocxRenderer` + `XlsxRenderer`
- [ ] `Api/Controllers/FormsController` (list template, get spec, render preview, render final)
- [ ] FE: form builder dynamic render từ fieldSpec
- [ ] FE admin: upload template + manage ContractClause (rich text editor)
- [ ] Test: FO-002.05 Giao khoán render → docx khớp mẫu 100%
## 🎯 Next up
Chi tiết: [`docs/flows/form-render-flow.md`](flows/form-render-flow.md) + [`docs/changelog/migration-todos.md`](changelog/migration-todos.md) section Phase 2.
### Phase 2 iteration 2 (optional — enhance)
## 🔄 Còn có thể làm parallel (optional, không block Phase 2)
- [ ] Convert 3 file `.doc` (retry Word COM với timeout, hoặc LibreOffice headless)
- [ ] Field spec JSON mỗi template + dynamic form builder FE
- [ ] Support `{{#loop}}...{{/loop}}` cho table lặp
- [ ] PDF convert via LibreOffice
- [ ] Admin upload template UI (POST multipart)
- [ ] FE Users management (tạo user + gán role) — cần để test permission với role khác Admin
- [ ] FE Roles CRUD (tạo custom role mới)
- [ ] Contract entity skeleton (không state machine, chỉ CRUD draft)
- [ ] E2E test permission: tạo user role Drafter-only → verify không thấy menu System/Admin
### Phase 3 — Workflow (sắp tới, item lớn)
## 📊 Thông số sau Phase 1 đợt 2
Xem [`docs/flows/contract-approval-flow.md`](flows/contract-approval-flow.md) + [`docs/workflow-contract.md`](workflow-contract.md).
- **Backend LOC:** ~1500 (Domain 150 + Application 800 + Infrastructure 350 + Api 200)
- **Migrations:** Init + AddMasterData + AddPermissions
- **DB tables:** 7 Identity + 3 Master (Suppliers/Projects/Departments) + 2 Permission (MenuItems/Permissions)
- **API endpoints:** 20+ (Auth 4 + Suppliers 5 + Projects 5 + Departments 5 + Menus 2 + Roles 1 + Permissions 2)
- **Frontend routes:** 5 (Dashboard + 3 CRUD + Permission Matrix)
- **FE LOC:** ~1700 (fe-admin; fe-user vẫn minimal)
Summary:
- [ ] Entity: Contract + ContractApproval + ContractComment + ContractAttachment
- [ ] `IContractWorkflowService` với state guard + role guard
- [ ] `IContractCodeGenerator` RG-001 + transaction SERIALIZABLE
- [ ] `SlaExpiryJob` BackgroundService
- [ ] Email + in-app notification service
- [ ] API `POST /api/contracts/{id}/transitions`
- [ ] FE Inbox + Contract detail page + timeline UI
### Optional (không block Phase 3)
- [ ] FE Users management + Roles CRUD (cho test permission với non-admin role)
- [ ] fe-user sync menu động (đang hardcode)
## 📊 Thông số cumulative
| | Phase 0 | +Phase 1f | +Phase 1.2 | +Docs | +Phase 2 MVP |
|---|---:|---:|---:|---:|---:|
| BE LOC | 0 | ~400 | ~1500 | — | ~1900 |
| DB tables | 0 | 7 | 12 | — | 14 |
| API endpoints | 0 | 4 | ~20 | — | ~23 |
| Migrations | 0 | 1 | 3 | — | 4 |
| FE pages | 0 | 2 | 6 | — | 7 |
| Docs files | 10 | 13 | 14 | 21 | 24 |
| Commits | 1 | 2 | 3 | — | 5 (sắp) |
## 🚨 Blockers / risks
-**Gitea remote** URL chờ user cấp
- ⚠️ **fe-user** chưa được update với menu động — Phase 2 sẽ sync
- ⚠️ **Users CRUD** chưa có UI → khó test permission với non-admin role thật
- ⚠️ **3 file `.doc`** Phase 2 cần convert COM
-**Gitea remote URL** — user sẽ cấp sau
- ⚠️ **3 file .doc** chưa convert (IsActive=false) — retry Word COM với timeout/`DisplayAlerts=0` hoặc LibreOffice
- ⚠️ **fe-user** chưa đồng bộ menu động (chỉ fe-admin đã chuyển) — quick fix lúc Phase 3
## Credentials mặc định
## Credentials + URLs
```
Email: admin@solutionerp.local
Password: Admin@123456
admin@solutionerp.local / Admin@123456
```
URLs dev:
- API: http://localhost:5443 — Swagger `/swagger`
- Admin FE: http://localhost:8082
- User FE: http://localhost:8080

View File

@ -91,19 +91,34 @@
## Phase 2 — Form Engine (T5-6)
- [ ] Khảo sát: OpenXml vs Aspose.Words — chọn 1 (Aspose có license phí; OpenXml free nhưng verbose)
- [ ] Convert 3 file `.doc``.docx` (COM automation PowerShell hoặc LibreOffice headless)
- [ ] Parse chi tiết field của 5 template HĐ — mỗi form thành JSON spec
- [ ] `Domain/Entities/ContractTemplate` (Id, FormCode, Name, TemplateFile path, FieldSpec JSON)
- [ ] `Application/Forms/Services/IFormRenderer` — input: template + data dict → output: byte[] (.docx)
- [ ] Implement `DocxRenderer` (OpenXml-based replace placeholder)
- [ ] Implement `XlsxRenderer` cho FO-002.07 (dùng EPPlus/ClosedXML)
- [ ] `Api/Controllers/FormsController` — GET /templates, POST /render
- [ ] FE user: form builder — chọn template → dynamic form → preview → export
- [ ] FE admin: upload template mới, edit field mapping
### MVP xong (Phase 2 iteration 1)
- [x] Khảo sát: chọn **OpenXml + ClosedXML** (free, không cần license)
- [x] `Domain/Forms/ContractTemplate` (Id, FormCode, Name, ContractType, FileName, StoragePath, Format, FieldSpec JSON, IsActive)
- [x] `Domain/Forms/ContractClause` skeleton
- [x] EF config + Migration `AddForms`
- [x] `Application/Forms/Services/IFormRenderer` interface
- [x] `Infrastructure/Forms/DocxRenderer` (OpenXml, handle placeholder split runs)
- [x] `Infrastructure/Forms/XlsxRenderer` (ClosedXML)
- [x] `Application/Forms/FormFeatures.cs` — List/Get/Render CQRS
- [x] `Api/Controllers/FormsController` — GET templates, GET single, POST render
- [x] Copy 5 .docx/.xlsx template → `wwwroot/templates/`
- [x] Seed 8 ContractTemplate rows (5 IsActive=true, 3 chờ convert)
- [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)
- [ ] 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
- [ ] Import/export template (để backup)
- [ ] Test: 1 HĐ Giao khoán filled → export .docx mở bằng Word y hệt mẫu
- [ ] 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
## Phase 3 — Workflow State Machine (T7-9)

View File

@ -0,0 +1,108 @@
# Session 2026-04-21 12:00 — Phase 2 Form Engine MVP
**Dev:** Claude (Opus 4.7)
**Duration:** ~1h
**Base commit:** `54d6c9b`
## Làm được
### Chunk D — Forms domain + packages
- NuGet: `DocumentFormat.OpenXml 3.x` + `ClosedXML 0.105+` (Infrastructure)
- Domain: `Forms/ContractTemplate` (FormCode, Name, ContractType, FileName, StoragePath, Format, FieldSpec JSON, IsActive), `Forms/ContractClause` (Code, Name, Content rich text, Version)
- EF configs: unique index FormCode, query filter IsDeleted
- DbSets + `IApplicationDbContext` update
- Migration `AddForms`
### Chunk E — Renderer + Application + Controller
- `Application/Forms/Services/IFormRenderer` + `RenderResult` record
- `Infrastructure/Forms/DocxRenderer` — OpenXml-based, xử lý placeholder bị split runs (gom text tất cả `<w:t>` trong paragraph → replace → gán lại vào text đầu)
- `Infrastructure/Forms/XlsxRenderer` — ClosedXML-based, replace cell value nếu là text chứa placeholder
- `Infrastructure/Forms/FormRenderer` — router theo format docx/xlsx
- Register `IFormRenderer` as Singleton trong Infrastructure DI
- `Application/Forms/FormFeatures.cs`:
- `ListContractTemplatesQuery` (filter type + onlyActive)
- `GetContractTemplateQuery`
- `RenderTemplateCommand` + Validator + Handler (resolve absolute path qua `IWebHostEnvironmentLocator`)
- `IWebHostEnvironmentLocator` interface trong Application — abstract `IWebHostEnvironment`, impl ở Api layer (`WebHostEnvironmentLocator`)
- `Api/Controllers/FormsController`: GET templates, GET single, POST render (return file)
### Chunk F — Convert + Seed + FE
- `scripts/convert-doc-to-docx.ps1` — Word COM automation. Chạy thử bị stuck (hidden dialog) → killed process. **3 file `.doc` chưa convert** — đánh dấu IsActive=false trong seed.
- Copy 5 file `.docx`/`.xlsx` từ `FORM/``wwwroot/templates/`
- `DbInitializer.SeedContractTemplatesAsync` — seed 8 template, check file exists để set IsActive
- FE: `types/forms.ts` (ContractTemplate + ContractTypeLabel), `pages/forms/FormsPage.tsx` — list + render button + Dialog điền JSON data → download file
- Route `/forms` add vào App.tsx (menu Layout đã có path sẵn)
## E2E verified
```
GET /api/forms/templates?onlyActive=false → 8 templates (5 active, 3 inactive)
POST /api/forms/templates/{fo-002.05-id}/render
body: {"benA_tenCongTy": "Solutions Construction", "giaTri": "150,000,000 VND", "ngayKy": "21/04/2026"}
→ HTTP 200, file .docx 482KB (Microsoft Word 2007+ format) — OK mở được bằng Word
```
TS check fe-admin pass.
## Bug gặp + fix
| Bug | Fix |
|---|---|
| `SpaceProcessingModeValues` namespace không tìm thấy | Dùng full path + wrap `EnumValue<>` |
| Word COM `SaveAs([ref])` type conversion error | Đổi sang `SaveAs2($path, 16)` |
| Word COM stuck (2 process, 164s CPU) | Kill process, fallback: skip .doc convert, đánh dấu IsActive=false cho 3 template tương ứng |
| Edit tool "File has not been read yet" sau system-reminder interrupt | Read lại rồi Write full file |
## Docs updates trong session này
- **`docs/gotchas.md`** (MỚI) — 17 bẫy đã gặp từ Phase 0 → 2, nhóm theo: tech stack constraints, EF Core, OpenXml/ClosedXML, System.Text.Json, file ops, dev workflow
- **`.claude/skills/form-engine/SKILL.md`** — update từ placeholder → full spec với code pointers, algorithm, API, limitations
- **`.claude/skills/permission-matrix/SKILL.md`** — update từ placeholder → full spec với BE policy + FE guard usage + pitfalls
- **`docs/STATUS.md`** — mark Phase 2 MVP done
- **`docs/changelog/migration-todos.md`** — tick Phase 2 items đã xong
## Handoff cho session tiếp theo
### Phase 2 còn lại (iteration 2)
- [ ] Convert 3 file `.doc` (retry Word COM với `DisplayAlerts=0` + set timeout) HOẶC dùng LibreOffice headless
- [ ] Field spec JSON mỗi template — cho phép FE render dynamic form thay vì điền JSON tay
- [ ] Form builder FE: dynamic render từ fieldSpec → validation → preview → submit
- [ ] Support `{{#loop}}...{{/loop}}` block (cho table hạng mục lặp ở FO-002.05, FO-002.07)
- [ ] PDF convert via LibreOffice headless (hoặc Aspose nếu mua license)
- [ ] Admin upload template mới qua UI (POST multipart)
- [ ] ContractClause rich text editor (TipTap) cho admin edit FO-002.04
### Phase 3 — Workflow (sắp tới)
Xem [`docs/flows/contract-approval-flow.md`](../../flows/contract-approval-flow.md).
Các việc lớn:
- Entity `Contract` + `ContractApproval` + `ContractComment` + `ContractAttachment`
- `IContractWorkflowService.TransitionAsync()` với state guard + role guard
- `IContractCodeGenerator` theo RG-001 với transaction SERIALIZABLE
- `SlaExpiryJob` hosted service auto-approve
- Email + in-app notification service
- FE Inbox + Contract detail + timeline UI
### Còn optional (không block Phase 3)
- Users management FE (tạo user + gán role)
- Roles CRUD
- fe-user menu động (hiện tại chưa sync với AuthContext menu pattern từ fe-admin)
### Blocker
-**Gitea remote** URL
## Thông số sau Phase 2 MVP
- **Git commits:** 4 (từ scaffold) + sắp thêm 1
- **Backend LOC:** ~1900 (thêm ~400 cho Forms)
- **DB tables:** 14 (thêm ContractTemplates + ContractClauses)
- **API endpoints:** ~23 (thêm Forms 3)
- **FE pages:** 6 (thêm Forms)
- **Templates vật lý:** 5 .docx/.xlsx trong `wwwroot/templates/`

187
docs/gotchas.md Normal file
View File

@ -0,0 +1,187 @@
# Gotchas — SOLUTION_ERP
> Các bẫy, pitfall đã gặp + cách xử lý. Đọc trước khi debug tương tự để không mất thời gian.
## Tech stack constraints (.NET 10 + TS 6 + Vite 8)
### 1. MediatR 14.x không tương thích → pin 12.4.1
**Triệu chứng:** `Unable to resolve service for type 'MediatR.IMediator'``AddMediatR` vẫn chạy nhưng không register IMediator.
**Nguyên nhân:** MediatR v14 (late 2025) refactored, extension methods khác.
**Fix:** Downgrade `<PackageReference Include="MediatR" Version="12.4.1" />`. Khi đó `RequestHandlerDelegate<TResponse>` là delegate không tham số (v14 có thêm CancellationToken param).
### 2. Swashbuckle 10.x + Microsoft.OpenApi 2.x breaking change
**Triệu chứng:** Build fail `The type or namespace 'Models' does not exist in 'Microsoft.OpenApi'`. Swagger endpoint 404.
**Nguyên nhân:** `.NET 10` template auto-cài `Microsoft.AspNetCore.OpenApi 10` → pull `Microsoft.OpenApi 2.0` → namespace `Microsoft.OpenApi.Models` đã bị remove.
**Fix:**
- Remove `Microsoft.AspNetCore.OpenApi` khỏi Api
- Downgrade Swashbuckle về `6.9.0` (compatible với OpenApi 1.x)
### 3. TypeScript 6 `erasableSyntaxOnly` cấm `enum`
**Triệu chứng:** `TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.` khi dùng `enum`.
**Nguyên nhân:** Vite 8 scaffold bật `erasableSyntaxOnly: true` — enum sinh runtime code nên bị cấm.
**Fix:** Dùng `const + as const + typeof[keyof]` pattern:
```ts
// ❌ Không được
export enum SupplierType { NhaCungCap = 1 }
// ✅ OK
export const SupplierType = { NhaCungCap: 1, NhaThauPhu: 2 } as const
export type SupplierType = typeof SupplierType[keyof typeof SupplierType]
```
### 4. TypeScript 6 deprecate `baseUrl`
**Triệu chứng:** `TS5101: Option 'baseUrl' is deprecated`.
**Fix:** Bỏ `baseUrl` trong `tsconfig.app.json`, chỉ giữ `paths`. Paths resolve relative to tsconfig location.
### 5. Node 22 local nhưng CI phải pin 20
**Bài học NamGroup:** CI build fail trên Node latest, phải downgrade. Dev local dùng Node 22 thoải mái.
**Fix:**
- `package.json` engines: `">=20"` (min only, không upper bound)
- `.nvmrc` = `20` (CI dùng)
- GitHub/Gitea Actions: `actions/setup-node@v4` với `node-version-file: '.nvmrc'` hoặc hardcode `'20.x'`
## EF Core 10
### 6. Expression tree không support switch expression
**Triệu chứng:** `CS8514: An expression tree may not contain a switch expression` khi viết `.AnyAsync(p => action switch { ... })`.
**Fix:** Tách switch ra ngoài, mỗi case gọi query riêng:
```csharp
// ❌
var hasPermission = await query.AnyAsync(p => action switch { "Read" => p.CanRead, ... });
// ✅
var hasPermission = action switch
{
"Read" => await query.AnyAsync(p => p.CanRead),
"Create" => await query.AnyAsync(p => p.CanCreate),
...
};
```
### 7. Design-time DbContext resolve fail
**Triệu chứng:** `dotnet ef migrations add``Unable to resolve service for type 'DbContextOptions<ApplicationDbContext>'`.
**Fix:** Tạo `IDesignTimeDbContextFactory<ApplicationDbContext>` trong Infrastructure — EF CLI sẽ dùng factory này thay vì chạy full Host.
### 8. `AddDefaultTokenProviders()` không tồn tại trong `AddIdentityCore`
**Triệu chứng:** Build fail `IdentityBuilder does not contain AddDefaultTokenProviders`.
**Nguyên nhân:** `AddIdentityCore` là minimal variant, không include token providers (password reset, email confirmation).
**Fix:** Bỏ call `AddDefaultTokenProviders()` nếu chưa cần. Khi cần password reset (Phase 4), chuyển sang `AddIdentity` hoặc add package `Microsoft.AspNetCore.Identity.UI`.
## OpenXml / ClosedXML (Form Engine Phase 2)
### 9. `SpaceProcessingModeValues.Preserve` namespace không tìm thấy
**Triệu chứng:** `CS0103: The name 'SpaceProcessingModeValues' does not exist`.
**Fix:** Dùng full path `DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve` và wrap trong `EnumValue<>`:
```csharp
textElement.Space = new DocumentFormat.OpenXml.EnumValue<DocumentFormat.OpenXml.SpaceProcessingModeValues>(
DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve);
```
### 10. Placeholder `{{field}}` bị split giữa 2 `<w:t>` elements
**Vấn đề:** Word hay split text thành nhiều run (style, typo check), placeholder `{{giaTri}}` có thể bị chia thành `{{gia` + `Tri}}` nằm trong 2 `<w:t>` khác nhau → regex replace miss.
**Fix (đã implement trong DocxRenderer):** Iterate theo Paragraph, gom text của mọi `<w:t>` trong cùng paragraph → replace → gán lại vào `<w:t>` đầu + clear rest. Giữ run style của text đầu.
### 11. Word COM SaveAs PowerShell type conversion error
**Triệu chứng:** `Cannot convert "..." value of type "psobject" to type "Object"` khi gọi `$doc.SaveAs([ref]$outPath, [ref]16)`.
**Fix:** Dùng `SaveAs2` (không đòi ref parameters):
```powershell
$doc.SaveAs2($outPath, 16) # 16 = wdFormatDocumentDefault (.docx)
```
### 12. Word COM stuck/hang
**Triệu chứng:** Script chạy không xong, process `WINWORD.EXE` còn nhưng CPU idle hoặc high.
**Nguyên nhân:** Hidden dialog (activation, recovery, template warning) block COM.
**Fix:**
- Set `$word.DisplayAlerts = 0` trước khi mở file
- Nếu stuck → `Get-Process WINWORD | Stop-Process -Force`
- Fallback: dùng LibreOffice headless `soffice --headless --convert-to docx file.doc`
## System.Text.Json (ASP.NET Core 10)
### 13. Record constructor deserialization fail với Unicode
**Triệu chứng:** POST JSON chứa ký tự tiếng Việt từ Windows bash/curl CLI → 400 "JSON value could not be converted to ... CreateSupplierCommand. Path: $.name".
**Nguyên nhân:** Encoding CLI không đúng UTF-8 khi pass vào `curl -d '{...}'`.
**Fix:**
- Test qua file: `curl --data-binary @payload.json` (file lưu UTF-8 thật)
- Không phải bug backend — API handle UTF-8 đúng qua axios/Swagger
## File operations
### 14. Dropbox sync có thể "revert" file đang edit
**Triệu chứng:** Write file thành công, build pass, nhưng file thực tế vẫn là nội dung cũ.
**Case cụ thể (Phase 1):** Program.cs Write thành công nhưng runtime chạy với default scaffold code.
**Fix:** Sau Write file quan trọng → Read lại hoặc `head -5` để xác nhận nội dung. Nếu phát hiện revert → Write lại ngay.
### 15. `.gitignore` wwwroot/uploads/ vs wwwroot/templates/
**Quy ước:**
- `wwwroot/uploads/`**ignore** (user-uploaded files, không commit)
- `wwwroot/templates/`**commit** (template files là source of truth, phải version control)
- `wwwroot/exports/` → ignore (rendered output, tạm)
## Dev workflow
### 16. Port conflict khi restart dev server
**Triệu chứng:** `npm run dev` fail với `Port 8082 is in use`.
**Nguyên nhân:** Background task trước chưa kill hẳn.
**Fix:** `TaskStop` task cũ, hoặc kill process listening port: `netstat -ano | findstr :8082``taskkill /F /PID <pid>`.
### 17. EF migration tạo 3 file, COMMIT ĐỦ
**Quy tắc:** Mỗi migration tạo:
- `{timestamp}_{Name}.cs` — up/down
- `{timestamp}_{Name}.Designer.cs` — model snapshot lúc đó
- `ApplicationDbContextModelSnapshot.cs` — current snapshot (update mỗi lần)
Commit đủ 3 file. Nếu thiếu, team khác `dotnet ef database update` sẽ fail.
## Checklist khi gặp bug mới
1. Build có pass không? Nếu fail → check using + package version
2. Log API startup có error ẩn không? → `tail` output file
3. File đã persist đúng chưa? → `head -5` verify
4. Nếu là package compat → thử downgrade về stable (không dùng preview/latest)
5. Nếu là TS error exotic → check tsconfig flags (`erasableSyntaxOnly`, `verbatimModuleSyntax`)
6. Nếu là EF expression tree error → tách logic ra ngoài query