# Session 2026-04-23 ~22:00 — RolesPage + 7 demo HĐ + clear pending tasks **Focus:** Phiên 3 trong ngày — sau session toolkit/4-bảng/master/roles VN (15:00, ff5e35f). Build trang `/system/roles` đã trỏ về placeholder, sau đó xử lý hết task pending (demo HĐ + user-kind approver + warning SLA + edit detail row + deps script + mở rộng master + backfill HĐ). 5 commit (072ad6d → bcdc007), 0 migration mới, BE pass. ## Outcomes ### A. RolesPage CRUD `/system/roles` ✓ User screenshot: trang trỏ "Trang này chưa được build". Build full CRUD: **BE (`PermissionFeatures.cs` + `RolesController.cs`):** - `CreateRoleCommand` — Name regex `^[A-Za-z][A-Za-z0-9_]*$` (chỉ chữ/số/ underscore, bắt đầu chữ), throw ConflictException nếu code đã tồn tại - `UpdateRoleCommand` — CHỈ update ShortName + Description (Name là FK trong UserRoles + WorkflowStepApprover.AssignmentValue + [Authorize] attr — không đổi) - `DeleteRoleCommand` — block 2 trường hợp: * Role thuộc AppRoles.All (12 hardcoded) * Còn user assigned (UserManager.GetUsersInRoleAsync count > 0) - 3 endpoint mới (POST/PUT/DELETE Authorize Admin) **FE (`fe-admin/src/pages/system/RolesPage.tsx` ~280 dòng):** - Table list 12 mặc định + custom với 5 column (Mã code / Mã viết tắt / Tên đầy đủ / Loại badge / Ngày tạo) + Edit + Delete actions - Edit dialog: Name disabled với hint, ShortName + Description editable - Delete: HARDCODED_ROLES set client-side check → toast error nếu role mặc định, BE double-check - Create dialog: 3 field + regex pattern HTML5 - Banner amber warning Mã code FK constraint - Loại badge: Mặc định (slate) vs Tùy chỉnh (brand) Commit: `072ad6d` ### B. SeedDemoContractsAsync — 7 HĐ varied phases ✓ User feedback: "Demo User đi" → tạo demo data toàn diện cho UAT-ready. 7 HĐ covering 7 ContractTypes + varied final phases: | # | Type | Final Phase | Tên | Giá trị | Details | |---|---|---|---|---|---| | 1 | ThauPhu | DangSoanThao | Thi công móng + cột tầng 1 — FLOCK 01 | 850M | 3 hạng mục | | 2 | GiaoKhoan | DangGopY | Khoán nhân công xây/trát/sơn | 320M | 3 CV + 2 comments | | 3 | NhaCungCap | DangInKy | Cung cấp xi măng + sắt thép Q2/2026 | 1.2B | 3 SP | | 4 | DichVu | DangTrinhKy | Thuê cẩu tháp 6 tháng | 540M | 1 DV | | 5 | MuaBan | DaPhatHanh | Mua máy phát điện 250kVA | 850M | 2 SP có VAT 10% | | 6 | NguyenTacNCC | DangGopY | HĐ nguyên tắc cung cấp vật tư 2026 | 0 (framework) | 2 SP với khung giá | | 7 | NguyenTacDV | DangSoanThao | HĐ nguyên tắc bảo trì TB 2026 | 0 (framework) | 1 DV với SLA | Workflow simulation: loop transition phases tới finalPhase, insert ContractApproval row mỗi phase với ApproverUserId mapping đúng role (CCM → ccm.tran, BOD → bod.huynh, HRA → hra.dang, Drafter → qs.hoang). SkipCcm policy bỏ DangKiemTraCCM khỏi flow. Mã HĐ auto gen RG-001. Idempotent: skip nếu đã có HĐ tên `[DEMO]` (path 1 create / path 2 backfill — xem section F). Commit: `8bc9565` ### C. User-kind approver runtime guard ✓ Trước: WorkflowDefinition Designer cho admin pick User cụ thể vào step approver, nhưng runtime guard bỏ qua (User-kind treat như DeptManager fallback — per skill doc). Bây giờ: enable đầy đủ. **WorkflowPolicy + UserTransitions parallel dict:** - Default null cho hardcoded Standard/SkipCcm - Populated qua FromDefinition khi WorkflowStepApprover Kind=User **IsTransitionAllowed signature update:** `(from, to, actorRoles, actorUserId?)` - Check Role first (existing) - Fallback User-kind: actorUserId.ToString() có trong UserTransitions? **ContractWorkflowService.TransitionAsync** dùng IsTransitionAllowed helper. Error message thêm "{N} user explicit" nếu policy có User-kind. **FromDefinition** update: nếu step CHỈ có User-kind (không Role), không fallback DeptManager nữa — guard sẽ check user-level. Chỉ fallback DeptManager nếu step thiếu cả 2 (cấu hình broken). Commit (gộp với D): `4edcd58` ### D. Warning 20% SLA notification ✓ `SlaExpiryJob.ProcessWarningsAsync` mới — chạy trước ProcessAsync (auto- approve quá hạn): - Pull Contracts WHERE !SlaWarningSent && SlaDeadline > now && Phase NOT IN (DaPhatHanh, TuChoi, DangDongDau) - Per phase, threshold = 20% × default SLA (vd Soạn thảo 7d → 33.6h remaining trigger; In ký 1d → 4.8h) - Nếu remaining ≤ threshold + còn slot → notify Drafter via INotificationService với NotificationType.SlaWarning + title icon ⚠ - Set SlaWarningSent = true để chỉ warning 1 lần per phase (reset trong TransitionAsync khi chuyển phase mới) Commit: `4edcd58` ### E. Edit detail row inline ✓ User cần edit hạng mục mà không phải xóa-thêm lại. **BE — 7 typed UpdateXxxDetailCommand handler trong ContractDetailsFeatures:** - EnsureContractType guard + log ChangelogAction.Update với summary "Sửa " - 7 PUT endpoints `/contracts/{id}/details/{slug}/{detailId}` **FE — `ContractDetailsTab.tsx` refactor:** - DeleteBtn → ActionBtns (Pencil + Trash) với onEdit + onDelete callbacks - 7 XxxTable signatures + onEdit prop + pass row data - New `EditRowDialog` component: * useEffect populate form từ row data khi target thay đổi * Reuse FIELDS_BY_TYPE config + buildPayload (compute thanhTien) * Date field convert ISO → yyyy-MM-dd cho input[type=date] * PUT /contracts/{id}/details/{slug}/{detailId} - Parent state editTarget — open dialog, close khi save thành công Mirror fe-admin (file copy). Commit: `e53cd3a` ### F. Mở rộng master data + backfill demo HĐ ✓ User feedback: "thêm master data NCC + Project + cập nhật HĐ đang thiếu/trùng nhau". **SeedDemoMasterDataAsync — per-Code idempotent (thay vì per-table):** - 15 demo suppliers (5 SupplierType × 3 entities mỗi loại): - NhaCungCap: VLXD-ABC, Xi măng Hà Tiên, Thép Hòa Phát, Cadivi, Tiền Phong - NhaThauPhu: Thăng Long XD, Cô Công, MEP Hà Nội, Next Stage - ToDoi: Hoàng Nam, Bắc Ninh xây trát, Hà Trang ốp lát - DonViDichVu: Clean Pro, Hồng Phát Vận chuyển, Long An Bảo vệ - ChuDauTu: Vingroup, Sun Group, Masterise Homes - 8 demo projects (đa dạng quy mô + giai đoạn): - FLOCK01 / SKYGARDEN / INDUSTRIAL (existing) - VHOMES-OP (350B), ECOPARK-VL (180B), BWTOWER (95B), WAREHOUSE-LB (32B), RESORT-PQ (220B) **Backfill demo HĐ (BackfillDemoContractsSupplierProjectAsync):** Trước: 7 HĐ tất cả dùng cùng 1 supplier+project (FirstOrDefault) → list nhìn không thực tế. Sau: maps `DemoSupplierByType` + `DemoProjectByType` đa dạng theo loại: | ContractType | Supplier | Project | |---|---|---| | HopDongThauPhu | NTP-XD | FLOCK01 | | HopDongGiaoKhoan | TD-NEHOANG | FLOCK01 | | HopDongNhaCungCap | NCC-XIMANG | VHOMES-OP | | HopDongDichVu | DV-VANCHUYEN | RESORT-PQ | | HopDongMuaBan | NCC-DIEN | BWTOWER | | HopDongNguyenTacNCC | NCC-THEP | ECOPARK-VL | | HopDongNguyenTacDichVu | DV-CLEAN | WAREHOUSE-LB | `SeedDemoContractsAsync` 2 path: 1. Lần đầu (no [DEMO] HĐ): tạo mới với supplier+project diverse per type 2. Đã có [DEMO]: gọi BackfillDemoContractsSupplierProjectAsync — loop từng demo HĐ, update supplier_id + project_id nếu mismatch (idempotent) Commit: `bcdc007` ### G. Deps audit helper script ✓ `scripts/deps-audit.ps1` — chạy thủ công hoặc CI gate: - `dotnet list SolutionErp.slnx package --vulnerable --include-transitive` (BE) - `npm audit --audit-level=moderate` (fe-admin + fe-user) - Color-coded output (green/red), summary cuối - `-FailOnHigh` switch để CI gate Skill ref: `.claude/skills/dependency-audit-erp/SKILL.md` cho pin constraints + workflow fix. Commit (cùng E): `e53cd3a` ## Stats | | Trước session | Sau session | Δ | |---|---|---|---| | BE LOC | ~7800 | ~8800 | +1000 | | DB tables | 36 | 36 | 0 (chỉ add seed data) | | Migrations | 11 | 11 | 0 | | API endpoints | ~80 | **~93** | +3 Roles + 7 Update Detail + 3 dùng cũ vẫn | | FE pages | ~22 | **~23** | +RolesPage | | FE components | many | + EditRowDialog | refactor ActionBtns | | Scripts PS | 4 | **5** | +deps-audit.ps1 | | Demo data | 5 NCC + 3 Project + 0 HĐ | **15 NCC + 8 Project + 7 HĐ** | massive expand | | Commits session này | — | **5** | 072ad6d → bcdc007 | ## Architectural notes 1. **Role.Name English giữ nguyên** — FK + [Authorize] attr không break. ShortName + Description VN cho display layer. Edit chỉ 2 field này. 2. **DeleteRole defense in depth** — FE check HARDCODED_ROLES + BE check AppRoles.All + BE count UserRoles → 3 lớp prevent illegal delete. 3. **User-kind approver guard** = optional fallback, không thay Role- based. Step có cả Role + User → bất kỳ match nào pass. 4. **Demo data per-Code idempotent** — admin add custom supplier/project không bị clobber khi restart. Backfill chỉ update mismatched [DEMO] HĐ. 5. **Edit detail tách typed UpdateCommand** — verbose nhưng TS strict FE bắt typo + EF tracker không nhầm entity. ## Cron audit fire 2026-05-01 `solution-erp-skill-audit-monthly` (đã set ở session 2026-04-23 09:00) sẽ chạy 9:00 AM 1/5 — log vào `docs/changelog/skill-audit-2026-05.md` + commit auto. Self-contained prompt, không cần context session hiện tại. ## Next session priority (chỉ còn blockers user/ops) 1. **UAT 2-3 user thật** — giờ có 13 demo accounts + 7 demo HĐ + 15 NCC + 8 Project + 4 catalogs đầy đủ → UAT-ready 100% 2. Email outbox (chờ SMTP) 3. Rotate creds (admin + 13 demo + SA + vrapp + JWT) 4. SQL backup Task Scheduler (script `scripts/backup-sql.ps1` đã có) **Optional minor:** - PermissionsPage grant `Workflows.Read` cho non-admin role → menu Wf_* visible (1 click trong matrix) - E2E test với 13 demo users (login từng role, chuyển phase đầy đủ flow) - Run `pwsh scripts/deps-audit.ps1` mỗi đầu tuần check vuln **Done in this session (clear hết high-impact pending):** - ✅ Roles CRUD admin - ✅ User-kind approver runtime guard - ✅ Warning 20% SLA notification - ✅ Edit detail row inline (7 typed Update commands + EditRowDialog) - ✅ 7 demo contracts seed varied phases + details + comments - ✅ Master data expand 5→15 NCC + 3→8 Project - ✅ Backfill demo HĐ assign supplier+project diverse - ✅ Deps audit helper script