[CLAUDE] Move nested-type menu → fe-user; Admin workflow config page
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m41s

User clarified: menu loại HĐ 3-level (Danh sách/Thao tác/Duyệt) thuộc
fe-user. Admin có page riêng để config quy trình per loại HĐ.

fe-admin Layout:
- filterForAdmin() drops Ct_* entries (hide nested type menu).
- Admin sidebar giờ về lại đơn giản: Dashboard / Master / Hợp đồng
  (leaf) / Forms / Reports / System.

fe-user Layout:
- Dynamic menu tree từ /menus/me (thay fixed USER_MENU hardcoded).
- Recursive MenuNodeRenderer (top-level expanded, nested collapsed).
- resolvePath user-specific: Ct_*_List → /my-contracts?type=X,
  Ct_*_Create → /contracts/new?type=X, Ct_*_Pending → /inbox?type=X.
- filterForUser drops admin-only entries (Master/System/Forms/Reports).
- Static USER_FIXED_TOP prepends "Hộp thư" leaf → /inbox.
- MyContractsPage + InboxPage đọc ?type=X param, filter client-side.

Workflow config (Admin side):
- Domain: WorkflowTypeAssignment entity (ContractType → PolicyName
  override). Registry.ForContractWithOverrides() prefer DB override
  else default.
- Infrastructure: EF config + migration AddWorkflowTypeAssignments,
  unique index trên ContractType. ContractWorkflowService load
  overrides dict mỗi transition. ContractFeatures load overrides khi
  build WorkflowSummaryDto.
- Application: GetWorkflowAdminOverviewQuery returns 7 types × current
  policy + available policies. SetWorkflowAssignmentCommand validate
  policy name tồn tại; nếu = default thì delete override (no stale row).
- Api: GET /api/workflows + PUT /api/workflows/{contractType}
  với policy "Workflows.Read" + "Workflows.Update".
- Menu: new key `Workflows` dưới System, label "Quy trình HĐ".
- FE /system/workflows: 7 card per type, dropdown Standard/SkipCcm +
  'Đã override' badge khi khác default, phase sequence timeline,
  explanation banner ở top. Iteration 2 note: admin-authored custom
  policies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-21 22:41:05 +07:00
parent 48e91fe7ca
commit 5e0f3801a1
20 changed files with 1737 additions and 48 deletions

View File

@ -117,23 +117,40 @@ public static class WorkflowPolicies
public static class WorkflowPolicyRegistry
{
// Mapping contract type → policy. Tuned to the real business from
// QT-TP-NCC.docx: formal NTP/NCC/Giao khoán need full CCM review; service /
// purchase / framework contracts skip CCM.
public static WorkflowPolicy For(ContractType type) => type switch
// Available policy names — extend when admin-authored custom policies
// are added in iteration 2.
public static readonly string[] AvailablePolicyNames = ["Standard", "SkipCcm"];
public static WorkflowPolicy ByName(string name) => name switch
{
ContractType.HopDongThauPhu => WorkflowPolicies.Standard,
ContractType.HopDongGiaoKhoan => WorkflowPolicies.Standard,
ContractType.HopDongNhaCungCap => WorkflowPolicies.Standard,
ContractType.HopDongDichVu => WorkflowPolicies.SkipCcm,
ContractType.HopDongMuaBan => WorkflowPolicies.SkipCcm,
ContractType.HopDongNguyenTacNCC => WorkflowPolicies.SkipCcm,
ContractType.HopDongNguyenTacDichVu => WorkflowPolicies.SkipCcm,
"SkipCcm" => WorkflowPolicies.SkipCcm,
_ => WorkflowPolicies.Standard,
};
// Instance-level bypass flag overrides the default: if a contract has
// Default mapping per contract type — used when DB override missing.
// Matches business from QT-TP-NCC.docx.
public static string DefaultPolicyNameFor(ContractType type) => type switch
{
ContractType.HopDongThauPhu or ContractType.HopDongGiaoKhoan or ContractType.HopDongNhaCungCap => "Standard",
_ => "SkipCcm",
};
public static WorkflowPolicy For(ContractType type) => ByName(DefaultPolicyNameFor(type));
// Instance-level bypass flag overrides the policy — if a contract has
// BypassProcurementAndCCM=true, always use SkipCcm regardless of type.
public static WorkflowPolicy ForContract(Contract contract) =>
contract.BypassProcurementAndCCM ? WorkflowPolicies.SkipCcm : For(contract.Type);
// DB-aware resolver: prefer assignment, else default. Pass in the
// assignments dict once per request (cached from DB).
public static WorkflowPolicy ForContractWithOverrides(
Contract contract,
IReadOnlyDictionary<ContractType, string>? assignments)
{
if (contract.BypassProcurementAndCCM) return WorkflowPolicies.SkipCcm;
if (assignments is not null && assignments.TryGetValue(contract.Type, out var policyName))
return ByName(policyName);
return For(contract.Type);
}
}

View File

@ -0,0 +1,13 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Contracts;
// Admin-configurable: which WorkflowPolicy applies to each ContractType.
// Seed with hardcoded defaults (see DbInitializer); admin can switch via
// /system/workflows UI without code changes. Later iteration will let admin
// define custom policies with bespoke phase/role/SLA.
public class WorkflowTypeAssignment : BaseEntity
{
public ContractType ContractType { get; set; }
public string PolicyName { get; set; } = string.Empty; // "Standard" | "SkipCcm" (extensible)
}

View File

@ -16,6 +16,7 @@ public static class MenuKeys
public const string Users = "Users";
public const string Roles = "Roles";
public const string Permissions = "Permissions";
public const string Workflows = "Workflows";
// Per-contract-type menu groups + 3 action leaves each.
// Key format: Ct_<TypeCode>[_<Action>] — prefix `Ct_` distinguishes from
@ -35,7 +36,7 @@ public static class MenuKeys
Dashboard,
Master, Suppliers, Projects, Departments,
Contracts, Forms, Reports,
System, Users, Roles, Permissions,
System, Users, Roles, Permissions, Workflows,
];
public static readonly string[] Actions = ["Read", "Create", "Update", "Delete"];