From 5113e4c771b3a14a32ce558bea368ff9b19da017 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 21 Apr 2026 12:01:11 +0700 Subject: [PATCH] [CLAUDE] Phase2: Form Engine MVP + docs (gotchas, skill, handoff) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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) --- .claude/skills/form-engine/SKILL.md | 130 +++- .claude/skills/permission-matrix/SKILL.md | 144 +++- CLAUDE.md | 2 + docs/HANDOFF.md | 155 ++++ docs/STATUS.md | 86 ++- docs/changelog/migration-todos.md | 39 +- .../2026-04-21-1200-phase2-form-engine.md | 108 +++ docs/gotchas.md | 187 +++++ fe-admin/src/App.tsx | 2 + fe-admin/src/pages/forms/FormsPage.tsx | 142 ++++ fe-admin/src/types/forms.ts | 21 + scripts/convert-doc-to-docx.ps1 | 44 ++ .../Controllers/FormsController.cs | 31 + src/Backend/SolutionErp.Api/Program.cs | 1 + .../Services/WebHostEnvironmentLocator.cs | 9 + ...-FO-002.01.v01 Bang kiem tra hop dong.docx | Bin 0 -> 113214 bytes ...v01 Dieu kien chung hop dong tron goi.docx | Bin 0 -> 192608 bytes ...CCM-FO-002.05.v01 Hop dong Giao khoan.docx | Bin 0 -> 459676 bytes .../SOL-CCM-FO-002.07.V01 Don dat hang.xlsx | Bin 0 -> 1032615 bytes .../SOL-CCM-RG-001.v02 QD ma so hop dong.docx | Bin 0 -> 46147 bytes .../Interfaces/IApplicationDbContext.cs | 3 + .../Forms/FormFeatures.cs | 96 +++ .../Forms/Services/IFormRenderer.cs | 15 + .../Forms/ContractClause.cs | 13 + .../Forms/ContractTemplate.cs | 18 + .../DependencyInjection.cs | 4 + .../Forms/DocxRenderer.cs | 77 ++ .../Forms/FormRenderer.cs | 24 + .../Forms/XlsxRenderer.cs | 47 ++ .../Persistence/ApplicationDbContext.cs | 3 + .../ContractClauseConfiguration.cs | 22 + .../ContractTemplateConfiguration.cs | 28 + .../Persistence/DbInitializer.cs | 67 +- .../20260421043848_AddForms.Designer.cs | 725 ++++++++++++++++++ .../Migrations/20260421043848_AddForms.cs | 92 +++ .../ApplicationDbContextModelSnapshot.cs | 130 ++++ .../SolutionErp.Infrastructure.csproj | 2 + 37 files changed, 2379 insertions(+), 88 deletions(-) create mode 100644 docs/HANDOFF.md create mode 100644 docs/changelog/sessions/2026-04-21-1200-phase2-form-engine.md create mode 100644 docs/gotchas.md create mode 100644 fe-admin/src/pages/forms/FormsPage.tsx create mode 100644 fe-admin/src/types/forms.ts create mode 100644 scripts/convert-doc-to-docx.ps1 create mode 100644 src/Backend/SolutionErp.Api/Controllers/FormsController.cs create mode 100644 src/Backend/SolutionErp.Api/Services/WebHostEnvironmentLocator.cs create mode 100644 src/Backend/SolutionErp.Api/wwwroot/templates/SOL-CCM-FO-002.01.v01 Bang kiem tra hop dong.docx create mode 100644 src/Backend/SolutionErp.Api/wwwroot/templates/SOL-CCM-FO-002.04.v01 Dieu kien chung hop dong tron goi.docx create mode 100644 src/Backend/SolutionErp.Api/wwwroot/templates/SOL-CCM-FO-002.05.v01 Hop dong Giao khoan.docx create mode 100644 src/Backend/SolutionErp.Api/wwwroot/templates/SOL-CCM-FO-002.07.V01 Don dat hang.xlsx create mode 100644 src/Backend/SolutionErp.Api/wwwroot/templates/SOL-CCM-RG-001.v02 QD ma so hop dong.docx create mode 100644 src/Backend/SolutionErp.Application/Forms/FormFeatures.cs create mode 100644 src/Backend/SolutionErp.Application/Forms/Services/IFormRenderer.cs create mode 100644 src/Backend/SolutionErp.Domain/Forms/ContractClause.cs create mode 100644 src/Backend/SolutionErp.Domain/Forms/ContractTemplate.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Forms/DocxRenderer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Forms/FormRenderer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Forms/XlsxRenderer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/ContractClauseConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/ContractTemplateConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260421043848_AddForms.Designer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260421043848_AddForms.cs diff --git a/.claude/skills/form-engine/SKILL.md b/.claude/skills/form-engine/SKILL.md index 84c31f1..05ce295 100644 --- a/.claude/skills/form-engine/SKILL.md +++ b/.claude/skills/form-engine/SKILL.md @@ -1,34 +1,130 @@ --- name: form-engine -description: Template engine render 8 form hợp đồng ra .docx/.xlsx giống 100% mẫu gốc. Gen mã HĐ theo RG-001. Dùng khi export HĐ, upload template mới, debug render lỗi format. +description: Template engine render 8 form hợp đồng ra .docx/.xlsx giống 100% mẫu gốc. Placeholder syntax {{field}}. Dùng khi export HĐ, upload template mới, debug render lỗi format, gen mã HĐ theo RG-001. when-to-use: - "export contract to word" - "render template docx" - "xuất đơn đặt hàng excel" - - "gen mã hợp đồng" + - "placeholder không replace" - "upload template mới" + - "gen mã hợp đồng" --- # Form Engine Skill -> **Phase 2 deliverable.** Hiện tại skill này là PLACEHOLDER. +> **Status:** Phase 2 implemented (MVP — placeholder replace cơ bản). +> **Missing:** loop `{{#loop}}...{{/loop}}` cho table lặp, field spec JSON, PDF convert, form builder FE. -## Context +## Tech stack -Xem đầy đủ ở [`docs/forms-spec.md`](../../../docs/forms-spec.md): -- 8 form (6 .docx/.doc + 1 .xlsx + 1 .docx quy định) -- Mã HĐ format theo SOL-CCM-RG-001: `{Project}/{Type}/SOL&{Partner}/{Seq}` và biến thể -- 3 file `.doc` cần convert qua Word COM / LibreOffice headless trước khi parse +| Layer | Tech | Purpose | +|---|---|---| +| `.docx` render | **DocumentFormat.OpenXml 3.x** | Free, maintained by MS | +| `.xlsx` render | **ClosedXML 0.105+** | Free LGPL, dễ dùng, support formula/style | +| PDF convert | LibreOffice headless (chưa implement) | `soffice --headless --convert-to pdf` — Phase 4 | +| `.doc` → `.docx` | Word COM (PowerShell) | Chạy offline 1 lần trên dev machine | -## Tech stack dự kiến +## Placeholder syntax -- **.docx render:** DocumentFormat.OpenXml (free, verbose) hoặc Aspose.Words (phí, dễ) -- **.xlsx render:** EPPlus (free non-commercial) hoặc ClosedXML (free) -- **PDF preview:** wkhtmltopdf hoặc LibreOffice `--convert-to pdf` +Template (.docx hoặc .xlsx) chứa `{{fieldName}}` — regex `\{\{([a-zA-Z0-9_\.]+)\}\}`: -## Code pointers (sẽ có sau Phase 2) +``` +Hợp đồng số: {{maHopDong}} +Bên A: {{benA_tenCongTy}} +Giá trị: {{giaTri}} +``` -- `src/Backend/SolutionErp.Application/Forms/Services/IFormRenderer.cs` -- `src/Backend/SolutionErp.Infrastructure/Forms/DocxRenderer.cs` -- `src/Backend/SolutionErp.Infrastructure/Forms/XlsxRenderer.cs` -- `src/Backend/SolutionErp.Infrastructure/Services/ContractCodeGenerator.cs` +**Data dictionary:** +```json +{ + "maHopDong": "FLOCK 01/HĐGK/SOL&PVL/03", + "benA_tenCongTy": "Công ty TNHH Xây dựng Solutions", + "giaTri": "150,000,000 VND" +} +``` + +Null value → replace bằng rỗng. Key không có trong data → giữ nguyên placeholder. + +## Code pointers + +- `src/Backend/SolutionErp.Application/Forms/Services/IFormRenderer.cs` — interface +- `src/Backend/SolutionErp.Infrastructure/Forms/DocxRenderer.cs` — OpenXml-based +- `src/Backend/SolutionErp.Infrastructure/Forms/XlsxRenderer.cs` — ClosedXML-based +- `src/Backend/SolutionErp.Infrastructure/Forms/FormRenderer.cs` — router theo format +- `src/Backend/SolutionErp.Application/Forms/FormFeatures.cs` — CQRS list/get/render +- `src/Backend/SolutionErp.Api/Controllers/FormsController.cs` — REST endpoints +- `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs` — `SeedContractTemplatesAsync` +- `fe-admin/src/pages/forms/FormsPage.tsx` — UI list + render test +- `src/Backend/SolutionErp.Api/wwwroot/templates/` — file templates vật lý + +## API endpoints + +| Method | Path | Purpose | +|---|---|---| +| GET | `/api/forms/templates?type=&onlyActive=` | List templates | +| GET | `/api/forms/templates/{id}` | Get single | +| POST | `/api/forms/templates/{id}/render` | Render với data dictionary → return file | + +## Key algorithm — Placeholder split fix + +Word thường split placeholder thành nhiều `` runs (vì style, typo check…). `DocxRenderer` xử lý bằng: + +```csharp +foreach (var para in root.Descendants()) { + var textElements = para.Descendants().ToList(); + var combined = string.Concat(textElements.Select(t => t.Text)); + if (!combined.Contains("{{")) continue; + + var replaced = Regex.Replace(combined, @"\{\{([a-zA-Z0-9_\.]+)\}\}", match => + data.TryGetValue(match.Groups[1].Value, out var v) ? (v ?? "") : match.Value); + + if (replaced != combined) { + textElements[0].Text = replaced; // gán vào text đầu + textElements[0].Space = ...Preserve; // giữ space + for (var i = 1; i < textElements.Count; i++) + textElements[i].Text = ""; // clear phần còn lại + } +} +``` + +## Workflow thêm template mới + +1. Upload file `.docx` / `.xlsx` → `wwwroot/templates/{formCode}.{ext}` +2. Insert row vào `ContractTemplates`: + ```sql + INSERT INTO ContractTemplates (FormCode, Name, ContractType, FileName, StoragePath, Format, IsActive, CreatedAt) + VALUES ('SOL-CCM-FO-002.XX', 'Tên', 1, 'file.docx', 'templates/file.docx', 'docx', 1, GETUTCDATE()); + ``` +3. Template tự động xuất hiện ở `/forms` FE + +## Known limitations + +| # | Limitation | Phase fix | +|---|---|---| +| 1 | Không support `{{#loop}}...{{/loop}}` cho table lặp | Phase 2 iteration 2 | +| 2 | Không có field spec JSON — form builder FE phải điền JSON thủ công | Phase 2 iteration 2 | +| 3 | 3 file `.doc` (FO-002.02/03/06) chưa convert → IsActive=false | Convert offline qua Word COM | +| 4 | Không có PDF convert → preview chỉ download .docx | Phase 4 | +| 5 | Không handle `.docm` (macro) — chỉ accept `.docx` / `.xlsx` | By design | +| 6 | Không convert format trong template (vd number → `150,000,000 VND`) — BE phải format trước khi pass data | Phase 3 khi gen mã HĐ | + +## Common pitfalls (xem gotchas.md) + +- **Placeholder bị split runs** → đã handle trong DocxRenderer +- **SpaceProcessingModeValues namespace** — xem gotcha #9 +- **Word COM stuck** — gotcha #12 +- **SaveAs type conversion** — gotcha #11 +- **Template file không tồn tại** → throw `NotFoundException` ở RenderCommandHandler + +## Gen mã HĐ (RG-001) — chưa implement (Phase 3) + +Xem [`docs/forms-spec.md §RG-001`](../../../docs/forms-spec.md). Tóm tắt format: + +| Loại HĐ | Format | +|---|---| +| HĐ Thầu phụ | `{Project}/HĐTP/SOL&{Partner}/{Seq}` | +| HĐ Giao khoán | `{Project}/HĐGK/SOL&{Partner}/{Seq}` | +| HĐ NCC | `{Project}/NCC/SOL&{Partner}/{Seq}` | +| HĐ Nguyên tắc | `{Year}/NCC/SOL&{Partner}/{Seq}` | + +Planned implementation: `IContractCodeGenerator.GenerateAsync()` với transaction SERIALIZABLE + `ContractCodeSequences` table để tránh race. diff --git a/.claude/skills/permission-matrix/SKILL.md b/.claude/skills/permission-matrix/SKILL.md index cdcb41e..0a398c6 100644 --- a/.claude/skills/permission-matrix/SKILL.md +++ b/.claude/skills/permission-matrix/SKILL.md @@ -1,41 +1,143 @@ --- name: permission-matrix -description: Hệ thống phân quyền Role × MenuKey × CRUD. Sidebar gating, permission guard, seed default, reset password. Dùng khi debug access denied, gán role, menu không hiện. +description: Hệ thống phân quyền Role × MenuKey × CRUD. Seed 12 menu + admin full. FE PermissionGuard + usePermission. BE AuthorizationHandler + 48 policy. Dùng khi debug access denied, gán role, menu không hiện. when-to-use: - "permission denied" - "access denied" - "menu không hiện" - "gán role cho user" - - "reset password" - "seed permission" + - "permission matrix edit" --- # Permission Matrix Skill -> **Phase 1 deliverable.** Hiện tại skill này là PLACEHOLDER. +> **Status:** Phase 1 đợt 2 IMPLEMENTED. -## Context +## Model -Pattern copy từ **NamGroup** skill `permission-system` nhưng đơn giản hóa: -- 1 User có N Role -- 1 Role có ma trận (MenuKey, CRUD flags) — `Permission` table -- Không có per-user override (giữ đơn giản cho Phase 1) -- Menu tree flat 2 cấp, hardcode `MenuKey` +``` +User ────< UserRoles ────< Role ────< Permissions ────< MenuItem + (RoleId, MenuKey, CRUD flags) +``` -## Tech +- 1 User có N Role (qua `AspNetUserRoles` rename → `UserRoles`) +- 1 Role có N Permission (1 row per MenuKey × 4 CRUD flag) +- Union (OR) nhiều role → user có quyền nếu **bất kỳ role nào** cho quyền đó +- Admin role → **bypass** check (luôn pass mọi policy) -- BE: `[Authorize(Policy = "Menu.Read")]` attribute -- FE: `` + `usePermission().can("Contracts", "Update")` -- Resolution: API `/api/menus/me` trả về tree + permissions đã resolved theo user's roles +## Menu tree (seed) -## Code pointers (sẽ có sau Phase 1) +12 menu trong `MenuKeys.All`: -- `src/Backend/SolutionErp.Domain/Identity/Permission.cs` -- `src/Backend/SolutionErp.Application/Permissions/Queries/GetMyMenuTreeQuery.cs` -- `fe-admin/src/components/PermissionGuard.tsx` -- `fe-admin/src/hooks/usePermission.ts` +``` +Dashboard +Master + ├── Suppliers + ├── Projects + └── Departments +Contracts +Forms +Reports +System + ├── Users + ├── Roles + └── Permissions +``` -## Common pitfalls (dự kiến) +Tree hierarchy qua `ParentKey` field. Seed trong `DbInitializer.SeedMenuTreeAsync`. -- Quên refresh token sau khi admin update permission → user phải logout/login mới thấy -- MenuKey hardcode dễ typo → tập trung vào file `src/lib/menuKeys.ts` (FE) + `MenuKeys.cs` (BE const) +## Code pointers + +**Backend:** +- `Domain/Identity/MenuKeys.cs` — const class, single source of truth +- `Domain/Identity/MenuItem.cs` — entity (Key PK, Label, ParentKey, Order, Icon) +- `Domain/Identity/Permission.cs` — entity (RoleId, MenuKey, 4 flag) +- `Application/Permissions/Queries/GetMyMenuTree/GetMyMenuTreeQuery.cs` — resolve per-user, union OR, filter tree +- `Application/Permissions/PermissionFeatures.cs` — list/upsert +- `Api/Authorization/MenuPermissionRequirement.cs` + `MenuPermissionHandler.cs` — policy check +- `Api/Program.cs` — register 48 policy `{menu}.{action}` trong AddAuthorization +- `Infrastructure/Persistence/DbInitializer.cs` — `SeedMenuTreeAsync` + `SeedAdminPermissionsAsync` +- `Api/Controllers/MenusController.cs`, `RolesController.cs`, `PermissionsController.cs` + +**Frontend (fe-admin):** +- `src/lib/menuKeys.ts` — const mirror, cần **đồng bộ tay** với BE +- `src/types/menu.ts` — MenuNode type +- `src/hooks/usePermission.ts` — `can(menuKey, action)` helper +- `src/components/PermissionGuard.tsx` — wrap button/content +- `src/components/Layout.tsx` — render sidebar động từ AuthContext.menu +- `src/pages/system/PermissionsPage.tsx` — ma trận edit UI +- `src/contexts/AuthContext.tsx` — `loadMenu()` on login + localStorage cache + +## BE policy usage + +Register trong Program.cs: + +```csharp +services.AddAuthorization(opts => +{ + foreach (var menu in MenuKeys.All) + foreach (var action in MenuKeys.Actions) + opts.AddPolicy($"{menu}.{action}", p => + p.Requirements.Add(new MenuPermissionRequirement(menu, action))); +}); +services.AddScoped(); +``` + +Apply ở controller: + +```csharp +[HttpPut("{id:guid}")] +[Authorize(Policy = "Contracts.Update")] +public async Task Update(...) { } +``` + +## FE guard usage + +```tsx +// Hook +const { can } = usePermission() +if (!can('Contracts', 'Update')) return null + +// Component wrap + + + + +// Route guard +}> + + + } +/> +``` + +## Workflow — gán quyền cho role mới + +1. Admin login → `/system/permissions` +2. Chọn role (vd "CostControl") +3. Tick checkbox trên matrix grid — mỗi lần tick tự động PUT `/api/permissions` upsert +4. User thuộc role đó logout/login lại → thấy permission mới (menu refresh từ `/api/menus/me`) + +## Guard rules đã implement + +- **Admin bypass:** role `Admin` luôn pass mọi policy (kể cả chưa seed row Permission) +- **Not user active:** `User.IsActive=false` → AuthorizationHandler return fail +- **Self-demote protection:** admin đang edit không thể giảm quyền role Admin (check trong `UpsertPermissionCommandHandler`) + +## Common pitfalls + +- **Quên refresh menu sau update permission** → user thấy menu cũ. Giải pháp: logout/login, hoặc Phase 3 thêm SignalR push. +- **MenuKey typo** — TS không check vì menu.key là string. Luôn dùng `MenuKeys.Contracts` const, không hardcode `"Contracts"`. +- **FE cache menu trong localStorage** → sau user được assign role mới, FE thấy menu cũ. Login lại fix. +- **Hai role conflict** (1 cho, 1 cấm): union OR → có ít nhất 1 role cho là được. +- **403 ở API nhưng FE không hide button** → FE guard chỉ UX, BE phải là source of truth. Phải apply `[Authorize(Policy = "X.Y")]` ở controller. + +## Phase tiếp theo + +- **Phase 3:** SignalR notify khi permission đổi → FE tự refetch `/api/menus/me` +- **Phase 4:** Per-user override (ngoài role) — thêm bảng `UserPermissionOverrides` +- **Phase 4:** Invalidate JWT khi role đổi (rare event, nhưng secure) diff --git a/CLAUDE.md b/CLAUDE.md index 534df2c..080d20b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,6 +70,8 @@ Kiến trúc: **.NET 10 Clean Architecture + 2 React FE (admin + user) + SQL Ser | [`docs/forms-spec.md`](docs/forms-spec.md) | Catalog 8 form + quy định mã HĐ | | [`docs/database/database-guide.md`](docs/database/database-guide.md) | DB conventions + schema hiện tại + planned + ERD | | [`docs/flows/README.md`](docs/flows/README.md) | Index 6 flow (auth, permission, contract create/approve, form render, SLA) | +| [`docs/gotchas.md`](docs/gotchas.md) | ⭐ 17 bẫy đã gặp — đọc trước khi debug tương tự | +| [`docs/HANDOFF.md`](docs/HANDOFF.md) | Brief 5 phút: session trước làm gì + P1 tasks tiếp | ## ⚠️ Kết thúc session diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md new file mode 100644 index 0000000..9040b4d --- /dev/null +++ b/docs/HANDOFF.md @@ -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 diff --git a/docs/STATUS.md b/docs/STATUS.md index 04f2eeb..bf8c457 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -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 diff --git a/docs/changelog/migration-todos.md b/docs/changelog/migration-todos.md index 31e6441..2de1615 100644 --- a/docs/changelog/migration-todos.md +++ b/docs/changelog/migration-todos.md @@ -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) diff --git a/docs/changelog/sessions/2026-04-21-1200-phase2-form-engine.md b/docs/changelog/sessions/2026-04-21-1200-phase2-form-engine.md new file mode 100644 index 0000000..bfc8e9d --- /dev/null +++ b/docs/changelog/sessions/2026-04-21-1200-phase2-form-engine.md @@ -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ả `` 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/` diff --git a/docs/gotchas.md b/docs/gotchas.md new file mode 100644 index 0000000..5533a86 --- /dev/null +++ b/docs/gotchas.md @@ -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 ``. Khi đó `RequestHandlerDelegate` 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'`. + +**Fix:** Tạo `IDesignTimeDbContextFactory` 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.Preserve); +``` + +### 10. Placeholder `{{field}}` bị split giữa 2 `` 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 `` khác nhau → regex replace miss. + +**Fix (đã implement trong DocxRenderer):** Iterate theo Paragraph, gom text của mọi `` trong cùng paragraph → replace → gán lại vào `` đầ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 `. + +### 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 diff --git a/fe-admin/src/App.tsx b/fe-admin/src/App.tsx index 7de0770..eae17cc 100644 --- a/fe-admin/src/App.tsx +++ b/fe-admin/src/App.tsx @@ -9,6 +9,7 @@ import { SuppliersPage } from '@/pages/master/SuppliersPage' import { ProjectsPage } from '@/pages/master/ProjectsPage' import { DepartmentsPage } from '@/pages/master/DepartmentsPage' import { PermissionsPage } from '@/pages/system/PermissionsPage' +import { FormsPage } from '@/pages/forms/FormsPage' function App() { return ( @@ -28,6 +29,7 @@ function App() { } /> } /> } /> + } /> } /> (null) + const [dataJson, setDataJson] = useState(`{ + "benA_tenCongTy": "Công ty TNHH Xây dựng Solutions", + "giaTri": "150,000,000 VND", + "ngayKy": "21/04/2026" +}`) + + const list = useQuery({ + queryKey: ['contract-templates'], + queryFn: async () => (await api.get('/forms/templates', { params: { onlyActive: false } })).data, + }) + + const render = useMutation({ + mutationFn: async ({ id, data }: { id: string; data: Record }) => { + const res = await api.post(`/forms/templates/${id}/render`, data, { responseType: 'blob' }) + return { blob: res.data as Blob, filename: res.headers['content-disposition']?.match(/filename="?([^";]+)"?/)?.[1] ?? 'render.docx' } + }, + onSuccess: ({ blob, filename }) => { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + a.click() + URL.revokeObjectURL(url) + toast.success('Đã tải file render') + setDialog(null) + }, + onError: err => toast.error(getErrorMessage(err)), + }) + + function handleRender() { + if (!dialog) return + let data: Record + try { + data = JSON.parse(dataJson) + } catch { + toast.error('JSON không hợp lệ') + return + } + render.mutate({ id: dialog.id, data }) + } + + const columns: Column[] = [ + { + key: 'format', + header: '', + width: 'w-12', + align: 'center', + render: t => + t.format === 'xlsx' ? : , + }, + { key: 'formCode', header: 'Form Code', render: t => {t.formCode}, width: 'w-40' }, + { key: 'name', header: 'Tên', render: t => t.name }, + { key: 'contractType', header: 'Loại HĐ', render: t => (t.contractType ? ContractTypeLabel[t.contractType] : '—'), width: 'w-40' }, + { + key: 'isActive', + header: 'Trạng thái', + width: 'w-28', + align: 'center', + render: t => + t.isActive ? ( + + ) : ( + + + Chưa active + + ), + }, + { + key: 'actions', + header: '', + align: 'right', + width: 'w-32', + render: t => ( + + ), + }, + ] + + return ( +
+ + + t.id} isLoading={list.isLoading} /> + + setDialog(null)} + title={dialog ? `Render: ${dialog.name}` : ''} + size="lg" + footer={ + <> + + + + } + > +
+
+ Hướng dẫn: Template chứa placeholder dạng {'{{fieldName}}'}. Điền key-value JSON + dưới đây, backend sẽ replace placeholder trong file gốc. +
+
+ + +
+
+ +