diff --git a/fe-admin/src/components/pe/PeWorkflowPanel.tsx b/fe-admin/src/components/pe/PeWorkflowPanel.tsx index ab4571d..fc401eb 100644 --- a/fe-admin/src/components/pe/PeWorkflowPanel.tsx +++ b/fe-admin/src/components/pe/PeWorkflowPanel.tsx @@ -86,41 +86,95 @@ export function PeWorkflowPanel({ }) const next = evaluation.workflow.nextPhases + const flow = evaluation.approvalFlow return (
-

Quy trình

-

{evaluation.workflow.policyDescription}

+

Quy trình duyệt

+ {evaluation.approvalWorkflowCode && ( +

+ {evaluation.approvalWorkflowCode} v{String(evaluation.approvalWorkflowVersion ?? 0).padStart(2, '0')} + {evaluation.approvalWorkflowName && <> · {evaluation.approvalWorkflowName}} +

+ )}
-
    - {evaluation.workflow.activePhases - .filter(p => p !== PurchaseEvaluationPhase.TuChoi) - .map(p => { - const isCurrent = evaluation.phase === p - const isPast = isPastPhase(evaluation.phase, p, evaluation.workflow.activePhases) + {/* Mig 24 V2 — Flow render Bước → Cấp → NV thay phase cards. + Status: Done (✓ emerald) / Current (● brand) / Pending (○ slate) */} + {flow && flow.steps.length > 0 && ( +
      + {flow.steps.map(step => { + const stepIcon = step.status === 'Done' ? '✓' : step.status === 'Current' ? '●' : '○' return ( -
    1. -
      - - {p} +
    2. +
      + + {stepIcon} - {PurchaseEvaluationPhaseLabel[p]} - {isCurrent && ● hiện tại} - {isPast && } + Bước {step.order} — {step.name} + {step.departmentName && ( + + {step.departmentName} + + )}
      + {step.levels.length > 0 && ( +
        + {step.levels.map(lv => { + const lvIcon = lv.status === 'Done' ? '✓' : lv.status === 'Current' ? '●' : '○' + return ( +
      • +
        + + {lvIcon} + +
        +
        + {lv.name || `Cấp ${lv.order}`} + {lv.status === 'Current' && đang chờ} + {lv.status === 'Done' && đã duyệt} +
        +
        + {lv.approvers.map(a => a.fullName).join(' / ') || '(chưa cấu hình)'} +
        +
        +
        +
      • + ) + })} +
      + )}
    3. ) })} -
    +
+ )} + + {/* Phiếu V1 legacy không có flow → fallback hiển thị phase summary đơn giản */} + {!flow && ( +
+ Phiếu này dùng quy trình cũ — workflow chi tiết không khả dụng. +
+ )} {/* Mig 24 — V2 banner: hiển thị Bước/Cấp hiện tại + danh sách NV được duyệt. Nếu actor không có trong list → banner amber + nút Duyệt sẽ disable. */} @@ -327,9 +381,3 @@ function fmtTime(iso: string): string { return d.toLocaleString('vi-VN', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) } -function isPastPhase(current: number, p: number, active: number[]): boolean { - const orderedIdx = active.indexOf(p) - const currentIdx = active.indexOf(current) - if (orderedIdx < 0 || currentIdx < 0) return false - return orderedIdx < currentIdx && p !== PurchaseEvaluationPhase.TuChoi -} diff --git a/fe-admin/src/types/purchaseEvaluation.ts b/fe-admin/src/types/purchaseEvaluation.ts index 1216ebe..4c0ec47 100644 --- a/fe-admin/src/types/purchaseEvaluation.ts +++ b/fe-admin/src/types/purchaseEvaluation.ts @@ -224,6 +224,30 @@ export type PeCurrentApproval = { approvers: PeCurrentApprovalLevelApprover[] } +// Mig 22-24 V2 — full workflow flow snapshot (Bước → Cấp → NV) cho FE render +// thay 4-phase cards cũ. Status per Level/Step: Done/Current/Pending. +export type PeApprovalFlowLevel = { + order: number // 1/2/3 trong Step + name: string | null + approvers: PeCurrentApprovalLevelApprover[] + status: 'Done' | 'Current' | 'Pending' +} + +export type PeApprovalFlowStep = { + order: number // 1-based + name: string + departmentId: string | null + departmentName: string | null + status: 'Done' | 'Current' | 'Pending' + levels: PeApprovalFlowLevel[] +} + +export type PeApprovalFlow = { + currentStepIndex: number | null // 0-based + currentLevelOrder: number | null + steps: PeApprovalFlowStep[] +} + export type PeChangelog = { id: string entityType: number @@ -335,6 +359,8 @@ export type PeDetailBundle = { approvalWorkflowVersion: number | null // Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet) currentApproval: PeCurrentApproval | null + // Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level + approvalFlow: PeApprovalFlow | null suppliers: PeSupplier[] details: PeDetailRow[] approvals: PeApproval[] diff --git a/fe-user/src/components/pe/PeWorkflowPanel.tsx b/fe-user/src/components/pe/PeWorkflowPanel.tsx index 6e7e75a..53aa270 100644 --- a/fe-user/src/components/pe/PeWorkflowPanel.tsx +++ b/fe-user/src/components/pe/PeWorkflowPanel.tsx @@ -82,41 +82,94 @@ export function PeWorkflowPanel({ }) const next = evaluation.workflow.nextPhases + const flow = evaluation.approvalFlow return (
-

Quy trình

-

{evaluation.workflow.policyDescription}

+

Quy trình duyệt

+ {evaluation.approvalWorkflowCode && ( +

+ {evaluation.approvalWorkflowCode} v{String(evaluation.approvalWorkflowVersion ?? 0).padStart(2, '0')} + {evaluation.approvalWorkflowName && <> · {evaluation.approvalWorkflowName}} +

+ )}
-
    - {evaluation.workflow.activePhases - .filter(p => p !== PurchaseEvaluationPhase.TuChoi) - .map(p => { - const isCurrent = evaluation.phase === p - const isPast = isPastPhase(evaluation.phase, p, evaluation.workflow.activePhases) + {/* Mig 24 V2 — Flow render Bước → Cấp → NV thay phase cards. + Status: Done (✓ emerald) / Current (● brand) / Pending (○ slate) */} + {flow && flow.steps.length > 0 && ( +
      + {flow.steps.map(step => { + const stepIcon = step.status === 'Done' ? '✓' : step.status === 'Current' ? '●' : '○' return ( -
    1. -
      - - {p} +
    2. +
      + + {stepIcon} - {PurchaseEvaluationPhaseLabel[p]} - {isCurrent && ● hiện tại} - {isPast && } + Bước {step.order} — {step.name} + {step.departmentName && ( + + {step.departmentName} + + )}
      + {step.levels.length > 0 && ( +
        + {step.levels.map(lv => { + const lvIcon = lv.status === 'Done' ? '✓' : lv.status === 'Current' ? '●' : '○' + return ( +
      • +
        + + {lvIcon} + +
        +
        + {lv.name || `Cấp ${lv.order}`} + {lv.status === 'Current' && đang chờ} + {lv.status === 'Done' && đã duyệt} +
        +
        + {lv.approvers.map(a => a.fullName).join(' / ') || '(chưa cấu hình)'} +
        +
        +
        +
      • + ) + })} +
      + )}
    3. ) })} -
    +
+ )} + + {!flow && ( +
+ Phiếu này dùng quy trình cũ — workflow chi tiết không khả dụng. +
+ )} {/* Mig 24 — V2 banner Bước/Cấp + danh sách NV duyệt */} {isV2Pending && evaluation.currentApproval && !readOnly && ( @@ -318,9 +371,3 @@ function fmtTime(iso: string): string { return d.toLocaleString('vi-VN', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) } -function isPastPhase(current: number, p: number, active: number[]): boolean { - const orderedIdx = active.indexOf(p) - const currentIdx = active.indexOf(current) - if (orderedIdx < 0 || currentIdx < 0) return false - return orderedIdx < currentIdx && p !== PurchaseEvaluationPhase.TuChoi -} diff --git a/fe-user/src/types/purchaseEvaluation.ts b/fe-user/src/types/purchaseEvaluation.ts index a63cac8..0ad58a3 100644 --- a/fe-user/src/types/purchaseEvaluation.ts +++ b/fe-user/src/types/purchaseEvaluation.ts @@ -222,6 +222,29 @@ export type PeCurrentApproval = { approvers: PeCurrentApprovalLevelApprover[] } +// Mig 22-24 V2 — full workflow flow snapshot (Bước → Cấp → NV). +export type PeApprovalFlowLevel = { + order: number + name: string | null + approvers: PeCurrentApprovalLevelApprover[] + status: 'Done' | 'Current' | 'Pending' +} + +export type PeApprovalFlowStep = { + order: number + name: string + departmentId: string | null + departmentName: string | null + status: 'Done' | 'Current' | 'Pending' + levels: PeApprovalFlowLevel[] +} + +export type PeApprovalFlow = { + currentStepIndex: number | null + currentLevelOrder: number | null + steps: PeApprovalFlowStep[] +} + export type PeChangelog = { id: string entityType: number @@ -333,6 +356,8 @@ export type PeDetailBundle = { approvalWorkflowVersion: number | null // Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet) currentApproval: PeCurrentApproval | null + // Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level + approvalFlow: PeApprovalFlow | null suppliers: PeSupplier[] details: PeDetailRow[] approvals: PeApproval[] diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs index 59bc2a5..1445025 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs @@ -101,6 +101,27 @@ public record PurchaseEvaluationCurrentApprovalDto( string? LevelName, List Approvers); +// Mig 22-24 V2 — full workflow flow snapshot (Bước → Cấp → NV) cho FE render +// thay 4-phase cards cũ. Mỗi Level có Status: Done/Current/Pending. +public record PurchaseEvaluationApprovalFlowLevelDto( + int Order, // 1/2/3 trong Step + string? Name, + List Approvers, + string Status); // "Done" | "Current" | "Pending" + +public record PurchaseEvaluationApprovalFlowStepDto( + int Order, // 1-based + string Name, + Guid? DepartmentId, + string? DepartmentName, + string Status, // "Done" | "Current" | "Pending" + List Levels); + +public record PurchaseEvaluationApprovalFlowDto( + int? CurrentStepIndex, // 0-based, null khi terminal + int? CurrentLevelOrder, + List Steps); + public record PurchaseEvaluationAttachmentDto( Guid Id, Guid? PurchaseEvaluationSupplierId, @@ -153,6 +174,7 @@ public record PurchaseEvaluationDetailBundleDto( string? ApprovalWorkflowName, int? ApprovalWorkflowVersion, PurchaseEvaluationCurrentApprovalDto? CurrentApproval, + PurchaseEvaluationApprovalFlowDto? ApprovalFlow, List Suppliers, List Details, List Approvals, diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs index e0f909c..ababf43 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs @@ -512,11 +512,12 @@ public class GetPurchaseEvaluationQueryHandler( // Mig 23 — load ApprovalWorkflow V2 info nếu pin (Code/Name/Version // hiển thị FE detail card "QT-DN-V2-001 - Tên (v01)"). - // Mig 24 — populate CurrentApproval { Bước/Cấp + N approvers } để - // FE biết user nào được duyệt cấp hiện tại → disable button đúng. + // Mig 24 — populate CurrentApproval (cấp hiện tại) + ApprovalFlow (full + // Bước/Cấp tree với Status) cho FE render flow vertical thay phase cards. string? awCode = null, awName = null; int? awVersion = null; PurchaseEvaluationCurrentApprovalDto? currentApproval = null; + PurchaseEvaluationApprovalFlowDto? approvalFlow = null; if (e.ApprovalWorkflowId is Guid awId) { var aw = await db.ApprovalWorkflows.AsNoTracking() @@ -529,48 +530,110 @@ public class GetPurchaseEvaluationQueryHandler( awName = aw.Name; awVersion = aw.Version; - // Compute current approval level info nếu Phase=ChoDuyet - if (e.Phase == PurchaseEvaluationPhase.ChoDuyet - && e.CurrentWorkflowStepIndex is int idx - && e.CurrentApprovalLevelOrder is int levelOrder) + var steps = aw.Steps.OrderBy(s => s.Order).ToList(); + // Resolve dept names cho Steps + var stepDeptIds = steps.Where(s => s.DepartmentId != null) + .Select(s => s.DepartmentId!.Value).Distinct().ToList(); + var stepDeptNames = stepDeptIds.Count == 0 + ? new Dictionary() + : await db.Departments.AsNoTracking() + .Where(d => stepDeptIds.Contains(d.Id)) + .ToDictionaryAsync(d => d.Id, d => d.Name, ct); + // Resolve approver user info (all levels) + var allApproverIds = steps.SelectMany(s => s.Levels) + .Select(l => l.ApproverUserId).Distinct().ToList(); + var approverInfos = allApproverIds.Count == 0 + ? new Dictionary() + : await userManager.Users.AsNoTracking() + .Where(u => allApproverIds.Contains(u.Id)) + .Select(u => new { u.Id, u.FullName, u.Email }) + .ToDictionaryAsync(u => u.Id, u => (u.FullName, u.Email), ct); + + // Compute Status mỗi level theo Phase + currentStepIdx + currentLevelOrder + var currentIdx = e.CurrentWorkflowStepIndex; + var currentLevel = e.CurrentApprovalLevelOrder; + var phase = e.Phase; + bool isTerminalDone = phase == PurchaseEvaluationPhase.DaDuyet; + bool isPending = phase == PurchaseEvaluationPhase.DangSoanThao + || phase == PurchaseEvaluationPhase.TraLai; + bool isLocked = phase == PurchaseEvaluationPhase.TuChoi; + + string ComputeLevelStatus(int stepIdx0, int levelOrder) { - var steps = aw.Steps.OrderBy(s => s.Order).ToList(); - if (idx >= 0 && idx < steps.Count) + if (isTerminalDone) return "Done"; + if (isPending || isLocked) return "Pending"; + if (currentIdx is null || currentLevel is null) return "Pending"; + if (stepIdx0 < currentIdx.Value) return "Done"; + if (stepIdx0 == currentIdx.Value) { - var step = steps[idx]; - string? stepDeptName = null; - if (step.DepartmentId is Guid stepDeptId) - { - stepDeptName = await db.Departments.AsNoTracking() - .Where(d => d.Id == stepDeptId) - .Select(d => d.Name) - .FirstOrDefaultAsync(ct); - } - var levelGroup = step.Levels.Where(l => l.Order == levelOrder).ToList(); - var approverIds = levelGroup.Select(l => l.ApproverUserId).Distinct().ToList(); - var approverInfos = approverIds.Count == 0 - ? new Dictionary() - : await userManager.Users.AsNoTracking() - .Where(u => approverIds.Contains(u.Id)) - .Select(u => new { u.Id, u.FullName, u.Email }) - .ToDictionaryAsync(u => u.Id, u => (u.FullName, u.Email), ct); - - var approvers = levelGroup - .Select(l => - { - approverInfos.TryGetValue(l.ApproverUserId, out var info); - return new PurchaseEvaluationApprovalLevelApproverDto( - l.ApproverUserId, - info.FullName ?? l.ApproverUserId.ToString(), - info.Email); - }) - .ToList(); - var levelName = levelGroup.FirstOrDefault()?.Name; - - currentApproval = new PurchaseEvaluationCurrentApprovalDto( - idx, step.Name, step.DepartmentId, stepDeptName, - levelOrder, levelName, approvers); + if (levelOrder < currentLevel.Value) return "Done"; + if (levelOrder == currentLevel.Value) return "Current"; + return "Pending"; } + return "Pending"; + } + + string ComputeStepStatus(int stepIdx0, int stepLevelCount) + { + if (isTerminalDone) return "Done"; + if (isPending || isLocked) return "Pending"; + if (currentIdx is null) return "Pending"; + if (stepIdx0 < currentIdx.Value) return "Done"; + if (stepIdx0 == currentIdx.Value) return "Current"; + return "Pending"; + } + + var flowSteps = new List(); + for (int i = 0; i < steps.Count; i++) + { + var step = steps[i]; + var levelGroups = step.Levels.OrderBy(l => l.Order).GroupBy(l => l.Order).ToList(); + var flowLevels = levelGroups.Select(g => + { + var approvers = g.Select(l => + { + approverInfos.TryGetValue(l.ApproverUserId, out var info); + return new PurchaseEvaluationApprovalLevelApproverDto( + l.ApproverUserId, + info.FullName ?? l.ApproverUserId.ToString(), + info.Email); + }).ToList(); + var levelName = g.FirstOrDefault()?.Name; + return new PurchaseEvaluationApprovalFlowLevelDto( + g.Key, levelName, approvers, + ComputeLevelStatus(i, g.Key)); + }).ToList(); + + flowSteps.Add(new PurchaseEvaluationApprovalFlowStepDto( + step.Order, step.Name, + step.DepartmentId, + step.DepartmentId is Guid sd && stepDeptNames.TryGetValue(sd, out var sdn) ? sdn : null, + ComputeStepStatus(i, levelGroups.Count), + flowLevels)); + } + + approvalFlow = new PurchaseEvaluationApprovalFlowDto(currentIdx, currentLevel, flowSteps); + + // CurrentApproval (legacy banner) — chỉ populate nếu Phase=ChoDuyet + if (phase == PurchaseEvaluationPhase.ChoDuyet + && currentIdx is int idxCur && currentLevel is int lvlCur + && idxCur >= 0 && idxCur < steps.Count) + { + var step = steps[idxCur]; + var levelGroup = step.Levels.Where(l => l.Order == lvlCur).ToList(); + var approvers = levelGroup.Select(l => + { + approverInfos.TryGetValue(l.ApproverUserId, out var info); + return new PurchaseEvaluationApprovalLevelApproverDto( + l.ApproverUserId, + info.FullName ?? l.ApproverUserId.ToString(), + info.Email); + }).ToList(); + var levelName = levelGroup.FirstOrDefault()?.Name; + currentApproval = new PurchaseEvaluationCurrentApprovalDto( + idxCur, step.Name, step.DepartmentId, + step.DepartmentId is Guid sd2 && stepDeptNames.TryGetValue(sd2, out var sdn2) ? sdn2 : null, + lvlCur, levelName, approvers); } } } @@ -586,7 +649,7 @@ public class GetPurchaseEvaluationQueryHandler( e.BudgetId, budgetSummary, e.BudgetManualName, e.BudgetManualAmount, e.ApprovalWorkflowId, awCode, awName, awVersion, - currentApproval, + currentApproval, approvalFlow, e.Suppliers .OrderBy(s => s.Order) .Select(s => new PurchaseEvaluationSupplierDto(