[CLAUDE] Phase4: Report MVP + Docs Consolidation (rules, architecture, schema-diagram)
Backend Report: - Application/Reports/Dtos/DashboardStatsDto: 5 KPI + PhaseCount + SupplierCount + ProjectCount + MonthlyValue - Application/Reports/Queries/GetDashboardStats handler: total/active/overdue/published this month/totalValueActive + byPhase + top 5 NCC/du an + 12 thang monthly (fill zero khi thang empty) - Application/Reports/Services/IContractExcelExporter interface - Infrastructure/Reports/ContractExcelExporter: ClosedXML workbook 10 cot, header style bold+blue, number format #,##0, formula SUM, auto-fit, freeze header - Application/Reports/Commands/ExportContractsToExcelCommand: filter phase/supplier/project/date range - Api/Controllers/ReportsController: GET /reports/dashboard, GET /reports/contracts/export - DI register IContractExcelExporter (Scoped) Frontend fe-admin: - types/reports.ts: DashboardStats type - components/BarChart.tsx: generic horizontal bar chart — chi Tailwind, khong thu vien ngoai - pages/DashboardPage.tsx REWRITE: 5 KPI card (FileText/TrendingUp/AlertTriangle/CheckCircle2/Coins) + by-phase bar + monthly 12-month chart + top 5 NCC + top 5 du an + skeleton loader - pages/ReportsPage.tsx MOI: filter phase/fromDate/toDate → export Excel button - Route /reports vao App.tsx E2E verified: - GET /api/reports/dashboard → 200 voi day du KPI + monthly fill 12 thang - GET /api/reports/contracts/export → 200 xlsx 7229 bytes (Microsoft Excel 2007+) Docs consolidation (theo yeu cau user): - docs/rules.md MOI: 9 section coding conventions (ngon ngu UI/code/DB/docs, BE Clean Arch, CQRS+MediatR, Validation FluentValidation, Error handling, Async, Entity rules, DI, Package pinning, FE React/TS erasableSyntaxOnly, path alias, TanStack Query, Permission guard, Toast+error, DB convention, Git commit format, Docs structure, Testing, Security) - docs/architecture.md MOI: layered overview ASCII art, request lifecycle (1 POST/api/contracts qua 10 step), workflow state machine 9 phase, permission model, data flow sequence diagram 4 actor (Drafter/Manager/CCM/BOD/HRA), deployment architecture Phase 5, skill library, non-functional table - docs/database/schema-diagram.md MOI: full ERD 19 table mermaid + data flow diagram + vong doi 1 HD (create → 7 transition → gen ma → publish) + index strategy table + relationship cardinality + soft delete behavior + SQL queries (inbox/dashboard/gen ma) + migration history - docs/gotchas.md UPDATE: 17 → 26 pitfalls, them section "Claude Code harness quirks" (Edit File not read, DI build pass nhung runtime fail) + "Contract workflow" (ma HD gen 2 lan, BE-FE NEXT_PHASES sync, race condition) + "Permission matrix" (cache real-time, MenuKey typo) - docs/STATUS.md: Phase 4 MVP done, docs entry points section liet ke het, next Phase 5 Production - docs/HANDOFF.md: phase table them Phase 4 row, file tree update voi Reports, test points day du, git state commit 7 - docs/changelog/migration-todos.md: tick Phase 4 MVP items + them iteration 2 list - docs/changelog/sessions/2026-04-21-1430-phase4-report.md: session log voi thong so cumulative (BE 3100 LOC, 30 docs) - CLAUDE.md root: update Tai lieu quan trong section them rules.md, architecture.md, schema-diagram.md, .claude/skills (13 links now) Bug fix: - TS unused import ContractPhaseLabel trong DashboardPage - DI thieu register IContractExcelExporter — build pass but runtime would fail (added) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
198
docs/gotchas.md
198
docs/gotchas.md
@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
> Bẫy/pitfall đã gặp + cách xử lý. Đọc trước khi debug tương tự để không mất thời gian. Cập nhật liên tục khi gặp bug mới.
|
||||
|
||||
## Tech stack constraints (.NET 10 + TS 6 + Vite 8)
|
||||
|
||||
@ -8,180 +8,188 @@
|
||||
|
||||
**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).
|
||||
**Fix:** Pin `MediatR 12.4.1`. Khi đó `RequestHandlerDelegate<TResponse>` là delegate không tham số (v14 có thêm CancellationToken).
|
||||
|
||||
### 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.
|
||||
**Triệu chứng:** Build fail `The type or namespace 'Models' does not exist in 'Microsoft.OpenApi'`. Swagger 404.
|
||||
|
||||
**Fix:**
|
||||
- Remove `Microsoft.AspNetCore.OpenApi` khỏi Api
|
||||
- Downgrade Swashbuckle về `6.9.0` (compatible với OpenApi 1.x)
|
||||
- Downgrade Swashbuckle về `6.9.0`
|
||||
|
||||
### 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 const SupplierType = { NhaCungCap: 1 } 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, chỉ giữ `paths`. Paths resolve relative tsconfig location.
|
||||
|
||||
**Fix:** Bỏ `baseUrl` trong `tsconfig.app.json`, chỉ giữ `paths`. Paths resolve relative to tsconfig location.
|
||||
### 5. Node 22 local vs CI pin 20
|
||||
|
||||
### 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.
|
||||
**Bài học NamGroup:** CI build fail trên Node latest.
|
||||
|
||||
**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'`
|
||||
- `package.json` engines: `">=20"` (min, không upper)
|
||||
- `.nvmrc` = `20` cho CI
|
||||
- GitHub/Gitea Actions: `actions/setup-node@v4` với `node-version: '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 { ... })`.
|
||||
**Triệu chứng:** `CS8514: An expression tree may not contain a switch expression`.
|
||||
|
||||
**Fix:** Tách switch ra ngoài, mỗi case gọi query riêng:
|
||||
**Fix:** Tách switch ra ngoài LINQ:
|
||||
|
||||
```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),
|
||||
...
|
||||
_ => false,
|
||||
};
|
||||
```
|
||||
|
||||
### 7. Design-time DbContext resolve fail
|
||||
|
||||
**Triệu chứng:** `dotnet ef migrations add` → `Unable to resolve service for type 'DbContextOptions<ApplicationDbContext>'`.
|
||||
**Triệu chứng:** `dotnet ef migrations add` → `Unable to resolve service for type 'DbContextOptions<T>'`.
|
||||
|
||||
**Fix:** Tạo `IDesignTimeDbContextFactory<ApplicationDbContext>` trong Infrastructure — EF CLI sẽ dùng factory này thay vì chạy full Host.
|
||||
**Fix:** Tạo `IDesignTimeDbContextFactory<ApplicationDbContext>` trong Infrastructure.
|
||||
|
||||
### 8. `AddDefaultTokenProviders()` không tồn tại trong `AddIdentityCore`
|
||||
### 8. `AddDefaultTokenProviders()` không có trong `AddIdentityCore`
|
||||
|
||||
**Triệu chứng:** Build fail `IdentityBuilder does not contain AddDefaultTokenProviders`.
|
||||
**Fix:** Bỏ call nếu chưa cần password reset. Khi cần, chuyển `AddIdentity` hoặc add package `Microsoft.AspNetCore.Identity.UI`.
|
||||
|
||||
**Nguyên nhân:** `AddIdentityCore` là minimal variant, không include token providers (password reset, email confirmation).
|
||||
## OpenXml / ClosedXML
|
||||
|
||||
**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`.
|
||||
### 9. `SpaceProcessingModeValues` namespace
|
||||
|
||||
## 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<>`:
|
||||
**Fix:** Full path + wrap `EnumValue<>`:
|
||||
|
||||
```csharp
|
||||
textElement.Space = new DocumentFormat.OpenXml.EnumValue<DocumentFormat.OpenXml.SpaceProcessingModeValues>(
|
||||
textElement.Space = new DocumentFormat.OpenXml.EnumValue<
|
||||
DocumentFormat.OpenXml.SpaceProcessingModeValues>(
|
||||
DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve);
|
||||
```
|
||||
|
||||
### 10. Placeholder `{{field}}` bị split giữa 2 `<w:t>` elements
|
||||
### 10. Placeholder `{{field}}` bị split runs
|
||||
|
||||
**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.
|
||||
**Vấn đề:** Word hay split text thành nhiều `<w:t>` — placeholder miss khi regex replace.
|
||||
|
||||
**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.
|
||||
**Fix:** Iterate Paragraph, gom text tất cả `<w:t>` → replace → gán lại text đầu + clear rest. Đã implement trong `DocxRenderer`.
|
||||
|
||||
### 11. Word COM SaveAs PowerShell type conversion error
|
||||
### 11. Word COM `SaveAs` PowerShell type conversion
|
||||
|
||||
**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):
|
||||
**Fix:** Dùng `SaveAs2`:
|
||||
|
||||
```powershell
|
||||
$doc.SaveAs2($outPath, 16) # 16 = wdFormatDocumentDefault (.docx)
|
||||
$doc.SaveAs2($outPath, 16) # 16 = wdFormatDocumentDefault
|
||||
```
|
||||
|
||||
### 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.
|
||||
### 12. Word COM stuck
|
||||
|
||||
**Fix:**
|
||||
- Set `$word.DisplayAlerts = 0` trước khi mở file
|
||||
- `$word.DisplayAlerts = 0`
|
||||
- Nếu stuck → `Get-Process WINWORD | Stop-Process -Force`
|
||||
- Fallback: dùng LibreOffice headless `soffice --headless --convert-to docx file.doc`
|
||||
- Fallback: LibreOffice headless `soffice --headless --convert-to docx`
|
||||
|
||||
## System.Text.Json (ASP.NET Core 10)
|
||||
## System.Text.Json
|
||||
|
||||
### 13. Record constructor deserialization fail với Unicode
|
||||
### 13. Record deserialization fail với Unicode qua CLI
|
||||
|
||||
**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".
|
||||
**Triệu chứng:** POST JSON tiếng Việt từ Windows bash/curl → 400 "JSON value could not be converted".
|
||||
|
||||
**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
|
||||
**Fix:** Dùng `curl --data-binary @file.json` (file UTF-8). API handle đúng qua axios/Swagger.
|
||||
|
||||
## File operations
|
||||
|
||||
### 14. Dropbox sync có thể "revert" file đang edit
|
||||
### 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ũ.
|
||||
**Triệu chứng:** Write thành công, build pass, runtime chạy code 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 quan trọng → Read lại verify. Nếu revert → Write lại.
|
||||
|
||||
**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 rules
|
||||
|
||||
### 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)
|
||||
- `wwwroot/uploads/` → **ignore** (user files)
|
||||
- `wwwroot/templates/` → **commit** (source of truth)
|
||||
- `wwwroot/exports/` → ignore (temp)
|
||||
|
||||
## Dev workflow
|
||||
|
||||
### 16. Port conflict khi restart dev server
|
||||
|
||||
**Triệu chứng:** `npm run dev` fail với `Port 8082 is in use`.
|
||||
**Fix:** `TaskStop` task cũ, hoặc `netstat -ano | findstr :8082` → `taskkill /F /PID <pid>`.
|
||||
|
||||
**Nguyên nhân:** Background task trước chưa kill hẳn.
|
||||
### 17. EF migration 3-file rule
|
||||
|
||||
**Fix:** `TaskStop` task cũ, hoặc kill process listening port: `netstat -ano | findstr :8082` → `taskkill /F /PID <pid>`.
|
||||
Mỗi migration tạo: `{name}.cs` + `{name}.Designer.cs` + `ApplicationDbContextModelSnapshot.cs`. Commit đủ 3.
|
||||
|
||||
### 17. EF migration tạo 3 file, COMMIT ĐỦ
|
||||
## Claude Code harness quirks
|
||||
|
||||
**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)
|
||||
### 18. Edit tool "File not read" sau system-reminder
|
||||
|
||||
Commit đủ 3 file. Nếu thiếu, team khác `dotnet ef database update` sẽ fail.
|
||||
**Triệu chứng:** Edit file vừa Read, lỗi "File has not been read yet".
|
||||
|
||||
## Checklist khi gặp bug mới
|
||||
**Nguyên nhân:** System reminder interrupt reset read-cache.
|
||||
|
||||
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
|
||||
**Fix:** Read lại file rồi Write/Edit. Hoặc dùng Write (ghi đè full) thay Edit.
|
||||
|
||||
### 19. Build pass nhưng DI thiếu registration
|
||||
|
||||
**Triệu chứng:** `dotnet build` → 0 errors nhưng runtime throw `Unable to resolve service`.
|
||||
|
||||
**Nguyên nhân:** C# compiler chỉ check type, không check DI graph.
|
||||
|
||||
**Fix:** Sau thêm interface mới + impl → luôn add `services.AddScoped<IX, X>()` trong `DependencyInjection.cs`. Test API start up là OK check.
|
||||
|
||||
## Contract workflow
|
||||
|
||||
### 20. Mã HĐ gen 2 lần sau reject → approve lại
|
||||
|
||||
**Fix:** Check `if (contract.MaHopDong is null)` trước khi gen. Đã implement trong `ContractWorkflowService.TransitionAsync`.
|
||||
|
||||
### 21. BE adjacency vs FE NEXT_PHASES sync
|
||||
|
||||
**Triệu chứng:** FE hiển thị nút chuyển phase, click → BE 403.
|
||||
|
||||
**Nguyên nhân:** FE `NEXT_PHASES` map phải khớp BE `Transitions` dict.
|
||||
|
||||
**Fix:** Khi đổi adjacency BE → sync FE `src/pages/contracts/ContractDetailPage.tsx` ngay lập tức (cả 2 app).
|
||||
|
||||
### 22. Race condition gen mã HĐ khi 2 user cùng transition tới DangDongDau
|
||||
|
||||
**Fix:** `IsolationLevel.Serializable` transaction trong `ContractCodeGenerator`. Không skip.
|
||||
|
||||
## Permission matrix
|
||||
|
||||
### 23. Permission update không real-time
|
||||
|
||||
**Triệu chứng:** Admin tick permission cho role X → user X vẫn thấy menu cũ.
|
||||
|
||||
**Nguyên nhân:** FE cache menu trong `localStorage`, không auto refetch.
|
||||
|
||||
**Fix:** User phải logout/login. Phase 3 iteration 2 có thể thêm SignalR push "permission-changed" → FE tự refetch `/menus/me`.
|
||||
|
||||
### 24. MenuKey typo — không check type
|
||||
|
||||
**Fix:** Luôn dùng `MenuKeys.Contracts` const (BE) + `MenuKeys.Contracts` (FE `menuKeys.ts`). Không hardcode string.
|
||||
|
||||
## Checklist debug bug mới
|
||||
|
||||
1. Build pass không? → fail → check using + package version compat
|
||||
2. DI register đủ? → runtime error "Unable to resolve" → add `AddScoped/Singleton`
|
||||
3. API log startup có error ẩn? → `tail` output file
|
||||
4. File đã persist đúng chưa? → `head -5` verify sau Write
|
||||
5. Nếu package exotic → thử downgrade về stable trước
|
||||
6. Nếu TS error → check `erasableSyntaxOnly`, `verbatimModuleSyntax`
|
||||
7. Nếu EF expression tree → tách logic ra ngoài query
|
||||
8. Nếu Unicode CLI → dùng file payload
|
||||
9. Nếu workflow 403 → check FE NEXT_PHASES sync BE
|
||||
|
||||
Reference in New Issue
Block a user