[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:
155
docs/HANDOFF.md
Normal file
155
docs/HANDOFF.md
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
108
docs/changelog/sessions/2026-04-21-1200-phase2-form-engine.md
Normal file
108
docs/changelog/sessions/2026-04-21-1200-phase2-form-engine.md
Normal 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
187
docs/gotchas.md
Normal 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
|
||||
Reference in New Issue
Block a user