[CLAUDE] Infra+App+Api+FE: Chunk E4 — HĐ 2-stage dept approval (mirror PE)

Mở rộng 2-stage logic từ PE sang Contract workflow (Migration 16 đã có schema):

BE Service:
- ContractWorkflowService thêm UserManager<User> DI
- Mirror logic 2-stage từ PurchaseEvaluationWorkflowService.TransitionAsync
  Sau policy guard, trước gen mã HĐ:
  - User.DepartmentId != null + actor không admin/system + KHÔNG resume
    - DeptManager (TPB) → Stage=Confirm trực tiếp
    - CanBypassReview=true → Stage=Confirm + IsBypassed=true
    - Else (NV) → Stage=Review only, BLOCK transition
  - Insert ContractDepartmentApproval row (UPSERT theo UNIQUE)
  - Block transition khi chưa có Stage=Confirm:
    - Insert ContractApproval (FromPhase=ToPhase=fromPhase, [Review NV] comment)
    - Insert ContractChangelog "đã review, chờ TPB confirm"
    - Notify TPB cùng dept (UserManager filter DeptManager role)
    - Return early — phase KHÔNG đổi

App + Api:
- ContractDepartmentApprovalFeatures.cs (List query mirror PE)
- ContractsController endpoint GET /contracts/{id}/department-approvals

FE (cả fe-admin + fe-user):
- types/contracts.ts thêm ApprovalStage const + ContractDepartmentApproval type
- WorkflowHistoryPanel section "Tiến trình duyệt 2-cấp phòng ban":
  - Group by phase × dept, show Review NV + Confirm TPB
  - Highlight amber "chờ TPB confirm" khi current phase có Review chưa Confirm
  - Badge fuchsia "bypass" khi NV.CanBypassReview=true
  - Insert giữa WorkflowSummaryCard và Lịch sử duyệt
- Mirror cả 2 app (rule §3.9)

Use case mirror PE: HĐ ở phase DangGopY (P.CCM) — nv.cao (NV) duyệt thì
phase KHÔNG đổi (Review only), chờ ccm.tran (TPB) confirm mới sang DangXetDuyet.

Build: BE pass + FE pass cả 2 + 77 test pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-04 13:43:05 +07:00
parent 4380bdc075
commit b6f5a16420
7 changed files with 447 additions and 5 deletions

View File

@ -218,6 +218,12 @@ public class ContractsController(IMediator mediator) : ControllerBase
[HttpGet("{id:guid}/changelogs")]
public async Task<List<ContractChangelogDto>> GetChangelogs(Guid id, CancellationToken ct)
=> await mediator.Send(new ListContractChangelogsQuery(id), ct);
// 2-stage department approval list (Phase 9 — Migration 16).
[HttpGet("{id:guid}/department-approvals")]
public async Task<ActionResult<List<ContractDepartmentApprovalDto>>> ListDepartmentApprovals(
Guid id, CancellationToken ct)
=> Ok(await mediator.Send(new ListContractDepartmentApprovalsQuery(id), ct));
}
public record TransitionContractBody(ContractPhase TargetPhase, ApprovalDecision Decision, string? Comment);