Bug: click leaf 'Duyet' (/purchase-evaluations?type=2&pendingMe=1) khien
leaf 'Danh sach' (/purchase-evaluations?type=2) cung highlight cung luc.
Nguyen nhan: NavLink 'end' prop chi match pathname. 2 leaf cung pathname
/purchase-evaluations → ca 2 active.
Fix: custom isActive voi queryMatches helper — compare query string dang
key-value set (thu tu param khong quan trong). 2 leaf chi active khi
pathname + query dung khop.
Dong bo ca fe-admin + fe-user. Anh huong tat ca menu leaf co ?query=
variants: Ct_* (Danh sach /contracts?type=N vs Duyet /contracts?type=N&
pendingMe=1), Pe_* (tuong tu /purchase-evaluations), admin workflow leaf
Wf_* + PeWf_* (khong dinh vi path khong query params).
GetMyMenuTreeQuery truoc chi inherit Contracts (Ct_*) va Workflows
(Wf_*). Extend 2 root moi PurchaseEvaluations (Pe_*) + PeWorkflows
(PeWf_*) de admin co PurchaseEvaluations.Read auto thay 2 group Pe_* +
6 leaf (Danh sach/Thao tac/Duyet x 2 type) + 2 PeWf_* leaf admin designer
UI, khong can add per-subitem permission row.
Verify bug: /menus/me cho admin hien chi root 'PurchaseEvaluations'
+ 'PeWorkflows' nhung khong co Pe_DuyetNcc group / Pe_DuyetNccPhuongAn
group children du DB co 12 row (sqlcmd confirm). Root cause: hardcoded
2 inherit roots trong BuildChildren switch.
Fix: expand switch cover 4 inherit roots. Propagate nextInherit xuong
tat ca descendants.
Bài học từ VietReport VPS shared (2026-04-23): Next.js app hijack port
3000 IPv4 → Gitea bị đẩy IPv6-only → IIS ARR localhost:3000 resolve
IPv4 first → git.baocaogiaoduc.vn trả homepage VietReport.
Apply 3 rules G-084 preemptively cho SOLUTION_ERP (risk thấp vì API
in-process IIS, nhưng vẫn chuẩn hóa):
1. `scripts/deploy-iis.ps1` — HealthUrl `localhost` → `127.0.0.1`
2. `.claude/skills/iis-deploy-runbook/SKILL.md` — 7 ref localhost →
127.0.0.1 + section Hardening mới giải thích G-084 + 3 rules + note
SOLUTION_ERP relevance (risk thấp vì no standalone Kestrel/no ARR
proxy hiện tại, nhưng tương lai thêm phải tuân)
3. `docs/gotchas.md` — thêm entry #33 G-084 full writeup (triệu chứng,
root cause, 3 rules, SOLUTION_ERP relevance) + update debug
checklist
3 rules:
- Reverse-proxy luôn IP literal 127.0.0.1, không localhost
- Backend services bind loopback IPv4 explicit, không 0.0.0.0
- Service dependency cho boot order khi nhiều service cùng port family
BE:
- CreateContractFromEvaluationCommand: guard DaDuyet + SelectedSupplier
+ ContractId=null → tạo Contract draft mới với SupplierId/ProjectId/
DepartmentId kế thừa từ PE. GiaTri = sum(details.thanhTienNganSach).
DraftData = PE.PaymentTerms. Gen MaHopDong ngay + pin WorkflowDefinitionId
theo ContractType user chọn. Log Changelog cả 2 bảng (Contract +
PurchaseEvaluation), link 2 chiều PE.ContractId = contract.Id.
- ListApprovedPurchaseEvaluationsQuery: DaDuyet + ContractId=null cho
FE picker.
- 2 endpoint mới:
GET /api/purchase-evaluations/approved-pending-contract
POST /api/purchase-evaluations/{id}/create-contract
FE:
- PeDetailTabs InfoTab: nếu Phase=DaDuyet && !ContractId && SelectedSupplierId
→ banner emerald + button "Tạo HĐ từ phiếu" → CreateContractDialog
(pick ContractType dropdown 7 loại + TenHopDong + bypass CCM flag)
- Sau khi tạo → navigate /contracts/{newId}
- Mirror fe-user.
KHÔNG auto-map PE Details → Contract Details per-type (PE schema ≠ 7
ContractType details schemas — user điền lại sau). PE → Contract link
qua FK ContractId cho navigation + history.
## User-kind approver 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 behavior)
- Fallback User-kind: actorUserId.ToString() có trong UserTransitions[(from,to)]?
ContractWorkflowService.TransitionAsync dùng IsTransitionAllowed thay
inline check. Error message thêm "{N} user explicit" nếu policy có
User-kind approvers cho transition đó.
FromDefinition cũng 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.
## Warning 20% SLA
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 7 ngày → 33.6h
remaining trigger warning; In ký 1 ngày → 4.8h)
- Compute remaining = SlaDeadline - now; nếu remaining <= threshold
+ còn slot → notify Drafter via INotificationService
- Set SlaWarningSent = true để chỉ warning 1 lần per phase (reset trong
TransitionAsync khi chuyển phase mới)
- NotificationType.SlaWarning (đã có trong enum) + title icon ⚠
## Build
dotnet build BE pass (0 error)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: /system/roles trỏ tới placeholder "chưa được build" — build
trang quản lý 12 role mặc định + custom role admin tự thêm.
## BE — PermissionFeatures.cs
3 command mới:
- 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. KHÔNG đổi
Name (Identity FK trong UserRoles + WorkflowStepApprover.AssignmentValue
+ [Authorize(Roles="...")] attr — đổi = data corruption widespread)
- DeleteRoleCommand — block 2 trường hợp:
* Role thuộc AppRoles.All hardcoded (workflow guard reference)
* Còn user assigned (UserManager.GetUsersInRoleAsync count > 0)
ValidationException reference fully-qualified để tránh ambiguous với
FluentValidation.ValidationException.
## BE — RolesController
3 endpoint mới (POST/PUT/DELETE) — Authorize Admin role.
## FE — RolesPage
Table list 12 + custom roles với 5 column (Mã code / Mã viết tắt / Tên
đầy đủ / Loại badge / Ngày tạo) + actions Edit/Delete:
- Edit dialog: chỉ ShortName + Description editable, Name disabled với
hint "Không đổi được sau khi tạo"
- Delete: block với toast nếu role mặc định (HARDCODED_ROLES set check
client-side trước khi gọi BE — UX faster, BE vẫn double-check)
- Create dialog: 3 field Name (regex pattern HTML5) + ShortName + Description
- Banner amber warning về Mã code FK constraint
- Loại badge: Mặc định (slate) vs Tùy chỉnh (brand)
## FE — App.tsx
+ import RolesPage + route /system/roles → RolesPage.
## Build
- BE: dotnet build pass (0 error)
- fe-admin: tsc + vite pass (13.88s)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: thay vì placeholder dashed nhỏ "Chi tiết sẽ hiện sau khi
tạo Header", show structure thật của Chi tiết section ngay từ đầu nhưng
disabled. User thấy trước layout columns + button add → trải nghiệm
liên tục, không bất ngờ khi switch sang edit mode.
## Component mới: ContractDetailsPreview
- Section title "Chi tiết ({TypeLabel})" + amber pill "🔒 Cần tạo Header trước"
- Table opacity-60 với:
- thead column headers per type (sync với HEADERS_BY_TYPE config)
- tbody empty state: Lock icon + "Tạo Header xong sẽ thêm được hạng mục"
- Disabled "+ Thêm dòng" button (cursor-not-allowed, slate-400 text)
## HEADERS_BY_TYPE config
7 type × column headers — duplicate nhỏ với ContractDetailsTab.tsx renderers
(acceptable: chỉ là labels visual, không logic).
## Reactive theo type
User đổi dropdown "Loại HĐ" → preview headers update tương ứng (state-driven).
## Build
- fe-user: tsc + vite pass (586ms)
- fe-admin: tsc + vite pass (709ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: HĐ phải có mã ngay khi tạo (không đợi đến DangDongDau như
cũ). HĐ đã tạo trước đây nhưng chưa có mã → backfill tự động.
## Thay đổi
### CreateContractCommandHandler (App)
- Inject IContractCodeGenerator
- Load supplier + project FULL (cần Code, không chỉ check tồn tại như trước)
- Call codeGenerator.GenerateAsync TRƯỚC khi db.Contracts.Add — entity
chưa tracked nên GenerateAsync internal SaveChangesAsync chỉ save SEQ
(không kèm contract chưa tracked)
- Set entity.MaHopDong = result trước khi Add → INSERT contract đã có mã
- Changelog summary include mã: "Tạo HĐ {mã} — {tên}"
### Trade-off documented
- Mã gen sớm → HĐ TuChoi sẽ "wasted" 1 mã (gap trong sequence)
- Acceptable vì user cần mã reference vào tài liệu/giấy tờ ngay từ đầu
### ContractWorkflowService.TransitionAsync (Infra)
- Giữ logic cũ `if MaHopDong is null → gen` ở DangDongDau
- Update comment: nominal flow skip vì mã đã có; defensive cho HĐ legacy
hoặc HĐ tạo bằng path khác (seed/import)
### DbInitializer.BackfillContractCodesAsync (Infra)
- Chạy 1 lần trước WarnDefaultAdminPasswordAsync
- Idempotent: count Contracts WHERE MaHopDong IS NULL → skip nếu 0
- Loop từng HĐ: load supplier+project → GenerateAsync → SaveChangesAsync
- Skip + log warning nếu missing supplier/project (legacy data corruption)
- Try-catch per HĐ, log success/failed count cuối cùng
## Build
dotnet build BE pass (0 error, 2 pre-existing DocxRenderer warning)
## Note
Khi deploy lên prod, DbInitializer chạy startup → backfill HĐ cũ tự động.
Log line "Backfill mã HĐ: X HĐ thiếu mã, đang gen..." sẽ xuất hiện ở
Logs/log-{date}.txt để verify.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: hiển thị Edit + Xóa cho mọi row (kể cả Phase khác), nhưng
mờ và disabled khi không thao tác được — để user biết button TỒN TẠI mà
không bị bất ngờ phải hover row đúng phase mới thấy.
## Thay đổi
- Bỏ conditional render `{phase === DangSoanThao && ...}`
- Thêm canMutate = c.phase === DangSoanThao biến + className conditional:
- canMutate=true: text-slate-500 + hover brand/red + clickable
- canMutate=false: text-slate-300 + cursor-not-allowed + disabled
- Default opacity-60 (luôn visible nhẹ), group-hover:opacity-100 (rõ
khi hover)
- title tooltip thay đổi theo state — hint user lý do disable
- onClick guard early return nếu !canMutate (defense in depth)
Build: tsc + vite pass cả 2 app
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: 2 button trên row Panel 1 phải là Edit + Xóa, và CHỈ
hoạt động trong trạng thái nhập liệu/điều chỉnh (Phase = DangSoanThao).
## Thay đổi
- ✏ Pencil icon thay ExternalLink — hành động Edit (select Panel 2 form)
- 🗑 Trash2 — Xóa (giữ nguyên)
- Cả 2 button bọc trong `c.phase === DangSoanThao` conditional → Phase
khác (DangGopY, DangDamPhan, ...) → ẩn cả 2
- Lý do: BE chỉ cho update + delete khi Phase=DangSoanThao
(UpdateContractDraftCommand + DeleteContractCommand throw Conflict-
Exception nếu khác)
## UX
- Phase=DangSoanThao: hover row → 2 button fade in
- Phase khác: chỉ row click select Panel 2 (form sẽ render read-only +
banner amber "HĐ đã chuyển khỏi Đang soạn thảo")
Build: tsc + vite pass cả 2 app
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: thêm nút edit/action ở mỗi row trong list Panel 1 trang
Thao tác. Hiện absolute positioned ở góc phải-trên row, opacity-0 → 100
khi hover (group-hover). Sibling không nested để click không trigger
row select propagation.
## 2 button per row
- ⤴ ExternalLink → navigate /contracts/{id} (fullpage detail với
Workflow + History, khác Panel 2 chỉ có Edit form)
- 🗑 Trash2 → confirm() + DELETE /contracts/{id} (soft delete,
blocked sau DangInKy ở BE). Nếu xóa HĐ đang select → clear ?id=
## Implementation details
- pr-16 cho row button để chừa khoảng cho action group
- group-hover:opacity-100 transition (smooth fade in)
- Mutation invalidate ['my-contracts'] sau xóa thành công
- Toast success + getErrorMessage cho fail case (vd xóa HĐ đã qua DangInKy)
Build: tsc + vite pass cả 2 app (fe-user 515ms, fe-admin 937ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trang /contracts/new?type=X (menu "Thao tác") redesign từ single form
→ 2-panel: Panel 1 list HĐ theo type | Panel 2 Header form + Chi tiết.
## Layout
Panel 1 (320px) — flex column 3 vùng:
- Top: Search box (filter mã/tên/NCC client-side)
- Middle: List HĐ theo type (scroll, click row chọn)
- Bottom: + Thêm mới button (sticky, ring-brand khi active mode=new)
Panel 2 (flex) — 3 trạng thái theo URL:
- Empty state — chưa chọn HĐ và chưa bấm + Thêm mới
- ContractHeaderForm (mode=new) — form trống, sau Tạo HĐ draft
→ URL update ?id=newId chuyển edit mode
- ContractEditForm (id=abc) — form populated từ /contracts/{id}, +
section Chi tiết bên dưới (ContractDetailsTab reuse)
## URL state
- ?type=X → empty
- ?type=X&mode=new → form trống
- ?type=X&id=abc → edit form + Chi tiết
- ?type=X&q=keyword → search filter Panel 1
## Edit constraints
ContractEditForm respect UpdateContractDraftCommand limits:
- Editable khi Phase=DangSoanThao: Tên HĐ, Giá trị, Template, Nội dung
- Read-only luôn: Loại HĐ, NCC, Dự án, Bypass CCM (không đổi sau create
qua BE command hiện tại)
- Khi Phase != DangSoanThao: warning amber + tất cả input disabled,
nhưng Chi tiết section vẫn render để user xem (ContractDetailsTab tự
disable add/delete khi không phải draft)
## Components
ContractCreatePage.tsx (rewrite) — page entry
ContractHeaderForm — create mode (full fields editable)
ContractEditForm — edit mode (limited fields + Chi tiết section)
FormFields helper — shared form layout cho create
## Build verify
- fe-user: tsc + vite pass (374ms)
- fe-admin: tsc + vite pass (987ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: thay vì click tab để switch, hiển thị Chi tiết + Lịch sử
LUÔN ngay dưới Tổng quan content. Tỷ lệ cột 7 (Chi tiết) - 3 (Lịch sử
điều chỉnh).
## Thay đổi (apply 2 app)
ContractDetailContent.tsx:
- Bỏ TabsNav + tab state + TabButton helper
- Bỏ conditional render theo tab
- Tổng quan content (Info / Comments / Attachments) render flat đầu tiên
- Thêm grid lg:grid-cols-10 dưới cùng:
- lg:col-span-7 → ContractDetailsTab (line items)
- lg:col-span-3 → ContractChangelogsTab (timeline)
- Mobile (<lg): stack vertical 1 cột, Chi tiết trên, Lịch sử dưới
## Build verify
- fe-user: tsc + vite pass (17.23s)
- fe-admin: tsc + vite pass (8.12s)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User decision B (log cả 3): mọi thay đổi liên quan HĐ ghi vào unified
ContractChangelogs để render tab Lịch sử FE.
## IChangelogService (Application/Common/Interfaces/)
5 methods:
- LogContractChangeAsync — Header insert/update
- LogDetailChangeAsync — line item insert/update/delete
- LogWorkflowTransitionAsync — phase change (parallel với ContractApprovals)
- LogCommentAddedAsync — góp ý mới
- LogAttachmentAsync — upload/delete file
KHÔNG SaveChanges trong service — caller chịu trách nhiệm save atomic
cùng business changes (pattern giống INotificationService).
## ChangelogService impl
- Resolve actor qua ICurrentUser → UserManager.FindByIdAsync
- Denormalize UserName (FullName ?? Email) cho log readable
- null UserId = system action (vd SLA auto-approve)
- DI: AddScoped trong DependencyInjection.cs
## Wiring vào handlers hiện tại
- ContractWorkflowService.TransitionAsync — LogWorkflowTransitionAsync
sau khi insert ContractApproval
- CreateContractCommandHandler — LogContractChangeAsync(Insert)
- UpdateContractDraftCommandHandler — diff GiaTri/TenHopDong/NoiDung/
TemplateId trước update, log update với fieldChangesJson nếu có thay đổi
- AddCommentCommandHandler — LogCommentAddedAsync
- UploadContractAttachmentCommandHandler — LogAttachmentAsync(Insert)
- DeleteContractAttachmentCommandHandler — LogAttachmentAsync(Delete)
Build: dotnet build BE pass (0 error, 2 pre-existing warning trong
DocxRenderer.cs)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: Layout resolvePath map "Dashboard" key → "/inbox" cũ (coi inbox là
home), khiến menu "Tổng quan" và "Hộp thư" cùng navigate về /inbox →
user thấy interface giống nhau, không phân biệt được.
Fix:
- Tạo UserDashboardPage.tsx — overview cá nhân:
* Greeting với fullName
* 5-card "Của tôi" row (HĐ đang soạn / Chờ tôi duyệt / Sắp quá hạn /
Đã quá hạn / Tổng giá trị nháp) — dùng /api/reports/my-dashboard có sẵn
* Card click navigate vào page tương ứng (/my-contracts hoặc /inbox)
* Section HĐ gần đây — list 5 row với click → /my-contracts?id=X
- App.tsx: thêm route /dashboard + redirect "/" sang /dashboard
- Layout.tsx: Dashboard → /dashboard, logo link cũng chuyển về /dashboard
Build: tsc + vite pass (439ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: 7 group Ct_<Code> (HĐ Thầu phụ / Giao khoán / NCC / Dịch vụ
/ Mua bán / Nguyên tắc NCC / Nguyên tắc DV) trước đây expand tự do →
sidebar dài lê thê khi user mở nhiều. Mỗi group nên độc lập (accordion):
chỉ 1 group expand cùng lúc.
## Cách làm
### AccordionContext lifted to Layout
- Layout maintain `expandedCtCode: string | null` state
- React Context expose getter + setter cho MenuGroup
- MenuGroup detect key `Ct_<Code>` qua regex `/^Ct_([^_]+)$/`:
- Match → controlled mode: open = (expandedCtCode === code)
- Toggle = setExpandedCtCode(open ? null : code)
- Group khác (Hợp đồng top-level, Quy trình admin, ...) giữ behavior cũ
(independent local useState)
### Auto-expand theo URL ?type=
useEffect watch location.search:
- `/my-contracts?type=5` → INT_TO_TYPE_CODE[5] = "MuaBan" → expand HĐ Mua bán
- `/contracts/new?type=2` → expand HĐ Giao khoán
- `/inbox?type=3` → expand HĐ Nhà cung cấp
- URL không có ?type= → KHÔNG reset (giữ user-selected context)
### Visual: highlight active group
Ct_ group đang accordion-open: `bg-slate-50 text-slate-900` (subtle tint
để user biết group nào đang active trong 7 type).
## Build
fe-user: tsc -b + vite build pass (1888 modules, 1.08MB JS, 380ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- rules.md §9 mới: liệt kê 6 skill (3 domain + 3 ops) với trigger,
nguyên tắc tạo skill project-specific (không clone generic),
format SKILL.md bắt buộc, audit workflow §9.4 chi tiết 7 bước,
4 anti-patterns
- CLAUDE.md (root): block "🛠️ Skills" callout 6 skill + audit cadence
+ commit scope thêm `Skill`
- HANDOFF.md: section A1 — định kỳ audit, lần kế tiếp 2026-05-01
- migration-todos: section "Skill governance (recurring)" với checkbox
audit hàng tháng
Cron task tạo qua scheduled-tasks (ID: solution-erp-skill-audit-
monthly): chạy 9:00 AM ngày 1 mỗi tháng. Self-contained prompt cold-
start để session tự audit + log vào docs/changelog/skill-audit-
{YYYY-MM}.md. Auto-refresh stale skill nhỏ, đề xuất add/archive cho
human approve.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Redesign theo yêu cầu user: 3 panel vertical đồng thời trên cùng 1
màn hình (không modal/dialog popup).
Layout grid lg:grid-cols-[280px_1fr_300px]:
Panel 1 — Vai trò (trái, 280px):
Danh sách roles click-to-select với active highlight (brand-50 bg +
ring-brand-200 + check icon). Đếm số roles ở header.
Panel 2 — Quyền theo menu (giữa, flex):
Tìm menu inline header + sticky thead. Click vai trò → lọc menu
instant. Column toggle header (tick toàn cột) + per-cell checkbox.
Hover brand-tinted. Menu key hiện mono nhỏ dưới label.
Panel 3 — Tổng quan (phải, 300px):
Vai trò đang chọn + số quyền (progress bar brand) + chi tiết từng
CRUD (Xem/Tạo/Sửa/Xóa) với badge color-coded (slate/emerald/amber/
red) + count "X / Y menus" + tip helper cuối.
Bỏ dialog select + 3-col grid filter ở đầu (thay bằng 3 panel), giữ
logic mutation/toggle/column nguyên.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User request: 7 tab trong /system/workflows thành menu items riêng.
Domain:
- MenuKeys.WorkflowTypeLeaf(code) helper — `Wf_<TypeCode>` pattern
Infrastructure (DbInitializer):
- Seed 7 leaves dưới Workflows group (order 95..101), label matches
ContractType (HĐ Thầu phụ / Giao khoán / NCC / Dịch vụ / Mua bán /
Nguyên tắc NCC / Nguyên tắc Dịch vụ). Idempotent.
Application (GetMyMenuTreeQuery):
- Generalized inherit-perm logic: descendants of Contracts AND Workflows
inherit parent CanRead flag. Single Workflows.Read grant → all 7
Wf_* leaves visible; no per-leaf permission rows needed.
FE Layout (admin):
- resolvePath: Wf_<Code> → /system/workflows/<code>. Ct_* still hidden
on admin side.
FE App.tsx:
- New route /system/workflows/:typeCode?
FE WorkflowsPage:
- Removed horizontal tab bar; type selection now comes từ URL param.
- Landing view (no param): 3-col grid card per type với active version
badge — so admin có visual overview khi click top-level Workflows
group without selecting a type.
- TYPE_CODE_TO_INT map drives URL→int conversion.
Result: click `Quy trình HĐ > HĐ Mua bán` trong sidebar → opens
/system/workflows/MuaBan directly với designer scoped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>