[CLAUDE] PE Panel 3: bỏ phase cards + render flow workflow V2 thực tế
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m14s

User feedback: "bỏ luôn cái quy trình phía trên đi nhé, vì nó là trạng
thái rồi (đã có badge), update cái flow quy trình mới vào bên panel 3
đang đến ai".

BE — ApprovalFlow DTO mới (full snapshot Bước → Cấp → NV với Status):
- PurchaseEvaluationApprovalFlowDto { CurrentStepIndex, CurrentLevelOrder,
  Steps[] }
- PurchaseEvaluationApprovalFlowStepDto { Order, Name, DepartmentId/Name,
  Status, Levels[] }
- PurchaseEvaluationApprovalFlowLevelDto { Order, Name, Approvers[], Status }
- Status: "Done" | "Current" | "Pending"

Handler GetById compute Status logic:
  - Phase=DaDuyet  → tất cả Steps/Levels "Done"
  - Phase=Nháp/Trả lại/Từ chối → tất cả "Pending"
  - Phase=ChoDuyet:
    * Step.Index < currentIdx          → all Levels "Done"
    * Step.Index == currentIdx:
        Level.Order < currentLevelOrder → "Done"
        Level.Order == currentLevelOrder → "Current"
        Level.Order > currentLevelOrder → "Pending"
    * Step.Index > currentIdx           → all "Pending"
- Load Approvers info (FullName + Email) qua UserManager batch query

FE (cả 2 app mirror):
- types/purchaseEvaluation.ts: +PeApprovalFlow + Step + Level + Status union
  PeDetail.approvalFlow optional
- PeWorkflowPanel:
  * BỎ phase cards section (4 ô Nháp/TraLai/ChoDuyet/DaDuyet) — đã
    duplicate với status badge ở header
  * Header mới: "Quy trình duyệt" + Code + Version + Name workflow pin
  * Render Flow vertical: Bước (icon ✓/●/○) → border + bg theo status
    + dept badge → list Cấp (icon nhỏ) với label "đang chờ" / "đã
    duyệt" + tên NV duyệt
  * Phiếu V1 legacy (no flow): show note "dùng quy trình cũ — không
    khả dụng chi tiết"
  * Bỏ helper isPastPhase() (orphan sau khi xóa cards)

Verify: BE build 0 error · 2 FE builds OK.

Test eoffice:
1. Mở phiếu V2 đang ChoDuyet → thấy flow Bước 1 (Phòng A):
   ✓ Cấp 1 NV X (đã duyệt)
   ● Cấp 2 NV Y (đang chờ)  ← highlight
   ○ Cấp 3 NV Z (chưa)
2. Phase=DaDuyet → all Steps/Levels green ✓
3. Phase=Nháp/TraLai → all greyed ○
4. V1 legacy → fallback note
This commit is contained in:
pqhuy1987
2026-05-08 16:16:40 +07:00
parent 74745a77a7
commit de0f38dd25
6 changed files with 331 additions and 100 deletions

View File

@ -86,41 +86,95 @@ export function PeWorkflowPanel({
})
const next = evaluation.workflow.nextPhases
const flow = evaluation.approvalFlow
return (
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold text-slate-900">Quy trình</h3>
<p className="mt-0.5 text-[11px] text-slate-500">{evaluation.workflow.policyDescription}</p>
<h3 className="text-sm font-semibold text-slate-900">Quy trình duyệt</h3>
{evaluation.approvalWorkflowCode && (
<p className="mt-0.5 font-mono text-[11px] text-slate-500">
{evaluation.approvalWorkflowCode} v{String(evaluation.approvalWorkflowVersion ?? 0).padStart(2, '0')}
{evaluation.approvalWorkflowName && <> · <span className="font-sans">{evaluation.approvalWorkflowName}</span></>}
</p>
)}
</div>
<ol className="space-y-1.5">
{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 && (
<ol className="space-y-2">
{flow.steps.map(step => {
const stepIcon = step.status === 'Done' ? '✓' : step.status === 'Current' ? '●' : '○'
return (
<li key={p}>
<div
<li
key={step.order}
className={cn(
'flex items-center gap-2 rounded border px-2 py-1.5 text-xs',
isCurrent && 'border-brand-300 bg-brand-50 font-medium',
isPast && 'border-emerald-200 bg-emerald-50 text-emerald-700',
!isCurrent && !isPast && 'border-slate-200 text-slate-500',
'rounded-md border p-2',
step.status === 'Current' && 'border-brand-300 bg-brand-50/50',
step.status === 'Done' && 'border-emerald-200 bg-emerald-50/40',
step.status === 'Pending' && 'border-slate-200 bg-white',
)}
>
<span className={cn('rounded px-1.5 py-0.5 text-[10px]', PurchaseEvaluationPhaseColor[p])}>
{p}
<div className="flex items-center gap-2 text-xs font-medium">
<span className={cn(
'flex h-5 w-5 shrink-0 items-center justify-center rounded-full font-bold',
step.status === 'Done' && 'bg-emerald-500 text-white',
step.status === 'Current' && 'bg-brand-600 text-white',
step.status === 'Pending' && 'bg-slate-200 text-slate-500',
)}>
{stepIcon}
</span>
<span className="truncate">{PurchaseEvaluationPhaseLabel[p]}</span>
{isCurrent && <span className="ml-auto text-[10px] text-brand-700"> hiện tại</span>}
{isPast && <span className="ml-auto text-[10px] text-emerald-600"></span>}
<span className="text-slate-800">Bước {step.order} {step.name}</span>
{step.departmentName && (
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
{step.departmentName}
</span>
)}
</div>
{step.levels.length > 0 && (
<ul className="mt-1.5 ml-7 space-y-1 border-l-2 border-violet-200 pl-2">
{step.levels.map(lv => {
const lvIcon = lv.status === 'Done' ? '✓' : lv.status === 'Current' ? '●' : '○'
return (
<li key={lv.order} className="text-[11px]">
<div className="flex items-start gap-1.5">
<span className={cn(
'mt-0.5 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[9px] font-bold',
lv.status === 'Done' && 'bg-emerald-500 text-white',
lv.status === 'Current' && 'bg-brand-600 text-white',
lv.status === 'Pending' && 'bg-slate-200 text-slate-500',
)}>
{lvIcon}
</span>
<div className="min-w-0 flex-1">
<div className="font-medium text-slate-700">
{lv.name || `Cấp ${lv.order}`}
{lv.status === 'Current' && <span className="ml-1.5 text-[10px] font-normal text-brand-700">đang chờ</span>}
{lv.status === 'Done' && <span className="ml-1.5 text-[10px] font-normal text-emerald-600">đã duyệt</span>}
</div>
<div className="text-slate-500">
{lv.approvers.map(a => a.fullName).join(' / ') || '(chưa cấu hình)'}
</div>
</div>
</div>
</li>
)
})}
</ul>
)}
</li>
)
})}
</ol>
)}
{/* Phiếu V1 legacy không có flow → fallback hiển thị phase summary đơn giản */}
{!flow && (
<div className="rounded border border-slate-200 bg-slate-50 px-3 py-2 text-[11px] text-slate-600">
Phiếu này dùng quy trình workflow chi tiết không khả dụng.
</div>
)}
{/* 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
}

View File

@ -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[]

View File

@ -82,41 +82,94 @@ export function PeWorkflowPanel({
})
const next = evaluation.workflow.nextPhases
const flow = evaluation.approvalFlow
return (
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold text-slate-900">Quy trình</h3>
<p className="mt-0.5 text-[11px] text-slate-500">{evaluation.workflow.policyDescription}</p>
<h3 className="text-sm font-semibold text-slate-900">Quy trình duyệt</h3>
{evaluation.approvalWorkflowCode && (
<p className="mt-0.5 font-mono text-[11px] text-slate-500">
{evaluation.approvalWorkflowCode} v{String(evaluation.approvalWorkflowVersion ?? 0).padStart(2, '0')}
{evaluation.approvalWorkflowName && <> · <span className="font-sans">{evaluation.approvalWorkflowName}</span></>}
</p>
)}
</div>
<ol className="space-y-1.5">
{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 && (
<ol className="space-y-2">
{flow.steps.map(step => {
const stepIcon = step.status === 'Done' ? '✓' : step.status === 'Current' ? '●' : '○'
return (
<li key={p}>
<div
<li
key={step.order}
className={cn(
'flex items-center gap-2 rounded border px-2 py-1.5 text-xs',
isCurrent && 'border-brand-300 bg-brand-50 font-medium',
isPast && 'border-emerald-200 bg-emerald-50 text-emerald-700',
!isCurrent && !isPast && 'border-slate-200 text-slate-500',
'rounded-md border p-2',
step.status === 'Current' && 'border-brand-300 bg-brand-50/50',
step.status === 'Done' && 'border-emerald-200 bg-emerald-50/40',
step.status === 'Pending' && 'border-slate-200 bg-white',
)}
>
<span className={cn('rounded px-1.5 py-0.5 text-[10px]', PurchaseEvaluationPhaseColor[p])}>
{p}
<div className="flex items-center gap-2 text-xs font-medium">
<span className={cn(
'flex h-5 w-5 shrink-0 items-center justify-center rounded-full font-bold',
step.status === 'Done' && 'bg-emerald-500 text-white',
step.status === 'Current' && 'bg-brand-600 text-white',
step.status === 'Pending' && 'bg-slate-200 text-slate-500',
)}>
{stepIcon}
</span>
<span className="truncate">{PurchaseEvaluationPhaseLabel[p]}</span>
{isCurrent && <span className="ml-auto text-[10px] text-brand-700"> hiện tại</span>}
{isPast && <span className="ml-auto text-[10px] text-emerald-600"></span>}
<span className="text-slate-800">Bước {step.order} {step.name}</span>
{step.departmentName && (
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
{step.departmentName}
</span>
)}
</div>
{step.levels.length > 0 && (
<ul className="mt-1.5 ml-7 space-y-1 border-l-2 border-violet-200 pl-2">
{step.levels.map(lv => {
const lvIcon = lv.status === 'Done' ? '✓' : lv.status === 'Current' ? '●' : '○'
return (
<li key={lv.order} className="text-[11px]">
<div className="flex items-start gap-1.5">
<span className={cn(
'mt-0.5 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[9px] font-bold',
lv.status === 'Done' && 'bg-emerald-500 text-white',
lv.status === 'Current' && 'bg-brand-600 text-white',
lv.status === 'Pending' && 'bg-slate-200 text-slate-500',
)}>
{lvIcon}
</span>
<div className="min-w-0 flex-1">
<div className="font-medium text-slate-700">
{lv.name || `Cấp ${lv.order}`}
{lv.status === 'Current' && <span className="ml-1.5 text-[10px] font-normal text-brand-700">đang chờ</span>}
{lv.status === 'Done' && <span className="ml-1.5 text-[10px] font-normal text-emerald-600">đã duyệt</span>}
</div>
<div className="text-slate-500">
{lv.approvers.map(a => a.fullName).join(' / ') || '(chưa cấu hình)'}
</div>
</div>
</div>
</li>
)
})}
</ul>
)}
</li>
)
})}
</ol>
)}
{!flow && (
<div className="rounded border border-slate-200 bg-slate-50 px-3 py-2 text-[11px] text-slate-600">
Phiếu này dùng quy trình workflow chi tiết không khả dụng.
</div>
)}
{/* 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
}

View File

@ -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[]

View File

@ -101,6 +101,27 @@ public record PurchaseEvaluationCurrentApprovalDto(
string? LevelName,
List<PurchaseEvaluationApprovalLevelApproverDto> 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<PurchaseEvaluationApprovalLevelApproverDto> 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<PurchaseEvaluationApprovalFlowLevelDto> Levels);
public record PurchaseEvaluationApprovalFlowDto(
int? CurrentStepIndex, // 0-based, null khi terminal
int? CurrentLevelOrder,
List<PurchaseEvaluationApprovalFlowStepDto> Steps);
public record PurchaseEvaluationAttachmentDto(
Guid Id,
Guid? PurchaseEvaluationSupplierId,
@ -153,6 +174,7 @@ public record PurchaseEvaluationDetailBundleDto(
string? ApprovalWorkflowName,
int? ApprovalWorkflowVersion,
PurchaseEvaluationCurrentApprovalDto? CurrentApproval,
PurchaseEvaluationApprovalFlowDto? ApprovalFlow,
List<PurchaseEvaluationSupplierDto> Suppliers,
List<PurchaseEvaluationDetailDto> Details,
List<PurchaseEvaluationApprovalDto> Approvals,

View File

@ -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();
if (idx >= 0 && idx < steps.Count)
{
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
// 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<Guid, string>()
: 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<Guid, (string FullName, string? Email)>()
: await userManager.Users.AsNoTracking()
.Where(u => approverIds.Contains(u.Id))
.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);
var approvers = levelGroup
.Select(l =>
// 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)
{
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)
{
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<PurchaseEvaluationApprovalFlowStepDto>();
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 = levelGroup.FirstOrDefault()?.Name;
}).ToList();
var levelName = g.FirstOrDefault()?.Name;
return new PurchaseEvaluationApprovalFlowLevelDto(
g.Key, levelName, approvers,
ComputeLevelStatus(i, g.Key));
}).ToList();
currentApproval = new PurchaseEvaluationCurrentApprovalDto(
idx, step.Name, step.DepartmentId, stepDeptName,
levelOrder, levelName, approvers);
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(