User request: mỗi loại HĐ có menu riêng với 3 action Danh sách /
Thao tác / Duyệt.
Sidebar giờ 3-level under "Hợp đồng":
Hợp đồng (group, expandable)
├── HĐ Thầu phụ (sub-group)
│ ├── Danh sách → /contracts?type=1
│ ├── Thao tác → /contracts/new?type=1
│ └── Duyệt → /contracts?type=1&pendingMe=1
├── HĐ Giao khoán (sub-group)
├── HĐ NCC / Dịch vụ / Mua bán / Nguyên tắc NCC / Nguyên tắc DV
└── ... (7 types × 4 = 28 new menu items)
BE:
- MenuKeys.cs: ContractTypeCodes array + helpers ContractTypeGroup/
List/Create/Pending → key format Ct_<TypeCode>[_<Action>]
- DbInitializer.SeedMenuTreeAsync: loop seeds 28 entries under Contracts
- GetMyMenuTreeQuery.BuildChildren: descendants of `Contracts` inherit
parent permission (avoid adding 28 rows to Permissions table per role)
FE:
- Layout.tsx recursive: MenuNodeRenderer dispatches group vs leaf by
depth; nested groups collapsed by default (top-level expanded).
Deeper levels get smaller padding/text + left border guide.
- Pattern-based resolvePath: Ct_<Type>_<Action> → URL with query.
- Contract type code → int map (matches Domain ContractType enum).
- ContractsListPage reads ?type + ?pendingMe, filters client-side.
Header title + description reflect active filter. "← Tất cả loại"
quick-reset button.
- ContractCreatePage new cho admin (copy từ fe-user), pre-select type
từ ?type URL param.
- App.tsx route /contracts/new → ContractCreatePage.
Pure navigation UX; no new permissions needed. Admin + any role with
Contracts.Read see full menu; leaves click-through to filtered views.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Đọc QT-TP-NCC.docx: quy trình 9 bước chỉ áp dụng cho Thầu phụ/NCC/Tổ đội.
Dịch vụ/Mua bán/Nguyên tắc bypass CCM. Thay hardcoded dict bằng policy
registry.
Domain — WorkflowPolicy.cs:
- Record WorkflowPolicy { Name, Description, Transitions, PhaseSla,
ActivePhases } — pure data, testable.
- WorkflowPolicies.Standard: 9-phase full (Thầu phụ/Giao khoán/NCC)
- WorkflowPolicies.SkipCcm: 7-phase (Dịch vụ/Mua bán/Nguyên tắc)
- WorkflowPolicyRegistry.For(type) map ContractType → policy
- WorkflowPolicyRegistry.ForContract(c) override nếu BypassProcurement
AndCCM=true (instance-level escape hatch)
Infrastructure — ContractWorkflowService:
- Xóa hardcoded Transitions/PhaseSla dicts → load từ policy.ForContract
- TransitionAsync: validate qua policy.Transitions thay vì dict local
- Error message include policy.Name để debug dễ hơn
- GetPhaseSla trả SLA từ Standard policy (fallback — SLA hiện tại giống
nhau giữa 2 policy)
Application — ContractDetailDto:
- Field mới `Workflow: WorkflowSummaryDto { PolicyName, Description,
ActivePhases, NextPhases }` — FE dùng để render nút chuyển phase
dynamic + timeline card.
- BuildWorkflowSummary helper trong ContractFeatures.
FE (both apps):
- Type WorkflowSummary + ContractDetail.workflow
- ContractDetailPage xóa hardcoded NEXT_PHASES — dùng
c.workflow.nextPhases từ BE (single source of truth)
- WorkflowSummaryCard: timeline của ActivePhases với check/current/
future states + policy name/description ở header
- Card hiển thị trong sidebar, phía trên "Lịch sử duyệt"
Docs:
- gotchas.md #21 marked RESOLVED (NEXT_PHASES sync không còn cần)
Foundation: sau này admin có thể edit policy qua UI khi chuyển sang DB-
backed policy — nhưng API contract (WorkflowSummaryDto) đã stable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CICD: check app pool state before Stop-WebAppPool (idempotent).
FE: new SlaTimer component with color-coded countdown (emerald/amber/red)
and progress bar. Two variants:
- inline: used in list tables (Inbox, Contracts list x2, MyContracts)
- full: used in ContractDetail card with progress bar + deadline timestamp
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>