[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
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:
@ -86,41 +86,95 @@ export function PeWorkflowPanel({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const next = evaluation.workflow.nextPhases
|
const next = evaluation.workflow.nextPhases
|
||||||
|
const flow = evaluation.approvalFlow
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-slate-900">Quy trình</h3>
|
<h3 className="text-sm font-semibold text-slate-900">Quy trình duyệt</h3>
|
||||||
<p className="mt-0.5 text-[11px] text-slate-500">{evaluation.workflow.policyDescription}</p>
|
{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>
|
</div>
|
||||||
|
|
||||||
<ol className="space-y-1.5">
|
{/* Mig 24 V2 — Flow render Bước → Cấp → NV thay phase cards.
|
||||||
{evaluation.workflow.activePhases
|
Status: Done (✓ emerald) / Current (● brand) / Pending (○ slate) */}
|
||||||
.filter(p => p !== PurchaseEvaluationPhase.TuChoi)
|
{flow && flow.steps.length > 0 && (
|
||||||
.map(p => {
|
<ol className="space-y-2">
|
||||||
const isCurrent = evaluation.phase === p
|
{flow.steps.map(step => {
|
||||||
const isPast = isPastPhase(evaluation.phase, p, evaluation.workflow.activePhases)
|
const stepIcon = step.status === 'Done' ? '✓' : step.status === 'Current' ? '●' : '○'
|
||||||
return (
|
return (
|
||||||
<li key={p}>
|
<li
|
||||||
<div
|
key={step.order}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 rounded border px-2 py-1.5 text-xs',
|
'rounded-md border p-2',
|
||||||
isCurrent && 'border-brand-300 bg-brand-50 font-medium',
|
step.status === 'Current' && 'border-brand-300 bg-brand-50/50',
|
||||||
isPast && 'border-emerald-200 bg-emerald-50 text-emerald-700',
|
step.status === 'Done' && 'border-emerald-200 bg-emerald-50/40',
|
||||||
!isCurrent && !isPast && 'border-slate-200 text-slate-500',
|
step.status === 'Pending' && 'border-slate-200 bg-white',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className={cn('rounded px-1.5 py-0.5 text-[10px]', PurchaseEvaluationPhaseColor[p])}>
|
<div className="flex items-center gap-2 text-xs font-medium">
|
||||||
{p}
|
<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>
|
||||||
<span className="truncate">{PurchaseEvaluationPhaseLabel[p]}</span>
|
<span className="text-slate-800">Bước {step.order} — {step.name}</span>
|
||||||
{isCurrent && <span className="ml-auto text-[10px] text-brand-700">● hiện tại</span>}
|
{step.departmentName && (
|
||||||
{isPast && <span className="ml-auto text-[10px] text-emerald-600">✓</span>}
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</ol>
|
</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 cũ — 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.
|
{/* 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. */}
|
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' })
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -224,6 +224,30 @@ export type PeCurrentApproval = {
|
|||||||
approvers: PeCurrentApprovalLevelApprover[]
|
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 = {
|
export type PeChangelog = {
|
||||||
id: string
|
id: string
|
||||||
entityType: number
|
entityType: number
|
||||||
@ -335,6 +359,8 @@ export type PeDetailBundle = {
|
|||||||
approvalWorkflowVersion: number | null
|
approvalWorkflowVersion: number | null
|
||||||
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
||||||
currentApproval: PeCurrentApproval | null
|
currentApproval: PeCurrentApproval | null
|
||||||
|
// Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level
|
||||||
|
approvalFlow: PeApprovalFlow | null
|
||||||
suppliers: PeSupplier[]
|
suppliers: PeSupplier[]
|
||||||
details: PeDetailRow[]
|
details: PeDetailRow[]
|
||||||
approvals: PeApproval[]
|
approvals: PeApproval[]
|
||||||
|
|||||||
@ -82,41 +82,94 @@ export function PeWorkflowPanel({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const next = evaluation.workflow.nextPhases
|
const next = evaluation.workflow.nextPhases
|
||||||
|
const flow = evaluation.approvalFlow
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-slate-900">Quy trình</h3>
|
<h3 className="text-sm font-semibold text-slate-900">Quy trình duyệt</h3>
|
||||||
<p className="mt-0.5 text-[11px] text-slate-500">{evaluation.workflow.policyDescription}</p>
|
{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>
|
</div>
|
||||||
|
|
||||||
<ol className="space-y-1.5">
|
{/* Mig 24 V2 — Flow render Bước → Cấp → NV thay phase cards.
|
||||||
{evaluation.workflow.activePhases
|
Status: Done (✓ emerald) / Current (● brand) / Pending (○ slate) */}
|
||||||
.filter(p => p !== PurchaseEvaluationPhase.TuChoi)
|
{flow && flow.steps.length > 0 && (
|
||||||
.map(p => {
|
<ol className="space-y-2">
|
||||||
const isCurrent = evaluation.phase === p
|
{flow.steps.map(step => {
|
||||||
const isPast = isPastPhase(evaluation.phase, p, evaluation.workflow.activePhases)
|
const stepIcon = step.status === 'Done' ? '✓' : step.status === 'Current' ? '●' : '○'
|
||||||
return (
|
return (
|
||||||
<li key={p}>
|
<li
|
||||||
<div
|
key={step.order}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 rounded border px-2 py-1.5 text-xs',
|
'rounded-md border p-2',
|
||||||
isCurrent && 'border-brand-300 bg-brand-50 font-medium',
|
step.status === 'Current' && 'border-brand-300 bg-brand-50/50',
|
||||||
isPast && 'border-emerald-200 bg-emerald-50 text-emerald-700',
|
step.status === 'Done' && 'border-emerald-200 bg-emerald-50/40',
|
||||||
!isCurrent && !isPast && 'border-slate-200 text-slate-500',
|
step.status === 'Pending' && 'border-slate-200 bg-white',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className={cn('rounded px-1.5 py-0.5 text-[10px]', PurchaseEvaluationPhaseColor[p])}>
|
<div className="flex items-center gap-2 text-xs font-medium">
|
||||||
{p}
|
<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>
|
||||||
<span className="truncate">{PurchaseEvaluationPhaseLabel[p]}</span>
|
<span className="text-slate-800">Bước {step.order} — {step.name}</span>
|
||||||
{isCurrent && <span className="ml-auto text-[10px] text-brand-700">● hiện tại</span>}
|
{step.departmentName && (
|
||||||
{isPast && <span className="ml-auto text-[10px] text-emerald-600">✓</span>}
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</ol>
|
</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 cũ — workflow chi tiết không khả dụng.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Mig 24 — V2 banner Bước/Cấp + danh sách NV duyệt */}
|
{/* Mig 24 — V2 banner Bước/Cấp + danh sách NV duyệt */}
|
||||||
{isV2Pending && evaluation.currentApproval && !readOnly && (
|
{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' })
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -222,6 +222,29 @@ export type PeCurrentApproval = {
|
|||||||
approvers: PeCurrentApprovalLevelApprover[]
|
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 = {
|
export type PeChangelog = {
|
||||||
id: string
|
id: string
|
||||||
entityType: number
|
entityType: number
|
||||||
@ -333,6 +356,8 @@ export type PeDetailBundle = {
|
|||||||
approvalWorkflowVersion: number | null
|
approvalWorkflowVersion: number | null
|
||||||
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
||||||
currentApproval: PeCurrentApproval | null
|
currentApproval: PeCurrentApproval | null
|
||||||
|
// Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level
|
||||||
|
approvalFlow: PeApprovalFlow | null
|
||||||
suppliers: PeSupplier[]
|
suppliers: PeSupplier[]
|
||||||
details: PeDetailRow[]
|
details: PeDetailRow[]
|
||||||
approvals: PeApproval[]
|
approvals: PeApproval[]
|
||||||
|
|||||||
@ -101,6 +101,27 @@ public record PurchaseEvaluationCurrentApprovalDto(
|
|||||||
string? LevelName,
|
string? LevelName,
|
||||||
List<PurchaseEvaluationApprovalLevelApproverDto> Approvers);
|
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(
|
public record PurchaseEvaluationAttachmentDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid? PurchaseEvaluationSupplierId,
|
Guid? PurchaseEvaluationSupplierId,
|
||||||
@ -153,6 +174,7 @@ public record PurchaseEvaluationDetailBundleDto(
|
|||||||
string? ApprovalWorkflowName,
|
string? ApprovalWorkflowName,
|
||||||
int? ApprovalWorkflowVersion,
|
int? ApprovalWorkflowVersion,
|
||||||
PurchaseEvaluationCurrentApprovalDto? CurrentApproval,
|
PurchaseEvaluationCurrentApprovalDto? CurrentApproval,
|
||||||
|
PurchaseEvaluationApprovalFlowDto? ApprovalFlow,
|
||||||
List<PurchaseEvaluationSupplierDto> Suppliers,
|
List<PurchaseEvaluationSupplierDto> Suppliers,
|
||||||
List<PurchaseEvaluationDetailDto> Details,
|
List<PurchaseEvaluationDetailDto> Details,
|
||||||
List<PurchaseEvaluationApprovalDto> Approvals,
|
List<PurchaseEvaluationApprovalDto> Approvals,
|
||||||
|
|||||||
@ -512,11 +512,12 @@ public class GetPurchaseEvaluationQueryHandler(
|
|||||||
|
|
||||||
// Mig 23 — load ApprovalWorkflow V2 info nếu pin (Code/Name/Version
|
// 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)").
|
// hiển thị FE detail card "QT-DN-V2-001 - Tên (v01)").
|
||||||
// Mig 24 — populate CurrentApproval { Bước/Cấp + N approvers } để
|
// Mig 24 — populate CurrentApproval (cấp hiện tại) + ApprovalFlow (full
|
||||||
// FE biết user nào được duyệt cấp hiện tại → disable button đúng.
|
// Bước/Cấp tree với Status) cho FE render flow vertical thay phase cards.
|
||||||
string? awCode = null, awName = null;
|
string? awCode = null, awName = null;
|
||||||
int? awVersion = null;
|
int? awVersion = null;
|
||||||
PurchaseEvaluationCurrentApprovalDto? currentApproval = null;
|
PurchaseEvaluationCurrentApprovalDto? currentApproval = null;
|
||||||
|
PurchaseEvaluationApprovalFlowDto? approvalFlow = null;
|
||||||
if (e.ApprovalWorkflowId is Guid awId)
|
if (e.ApprovalWorkflowId is Guid awId)
|
||||||
{
|
{
|
||||||
var aw = await db.ApprovalWorkflows.AsNoTracking()
|
var aw = await db.ApprovalWorkflows.AsNoTracking()
|
||||||
@ -529,48 +530,110 @@ public class GetPurchaseEvaluationQueryHandler(
|
|||||||
awName = aw.Name;
|
awName = aw.Name;
|
||||||
awVersion = aw.Version;
|
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();
|
var steps = aw.Steps.OrderBy(s => s.Order).ToList();
|
||||||
if (idx >= 0 && idx < steps.Count)
|
// Resolve dept names cho Steps
|
||||||
{
|
var stepDeptIds = steps.Where(s => s.DepartmentId != null)
|
||||||
var step = steps[idx];
|
.Select(s => s.DepartmentId!.Value).Distinct().ToList();
|
||||||
string? stepDeptName = null;
|
var stepDeptNames = stepDeptIds.Count == 0
|
||||||
if (step.DepartmentId is Guid stepDeptId)
|
? new Dictionary<Guid, string>()
|
||||||
{
|
: await db.Departments.AsNoTracking()
|
||||||
stepDeptName = await db.Departments.AsNoTracking()
|
.Where(d => stepDeptIds.Contains(d.Id))
|
||||||
.Where(d => d.Id == stepDeptId)
|
.ToDictionaryAsync(d => d.Id, d => d.Name, ct);
|
||||||
.Select(d => d.Name)
|
// Resolve approver user info (all levels)
|
||||||
.FirstOrDefaultAsync(ct);
|
var allApproverIds = steps.SelectMany(s => s.Levels)
|
||||||
}
|
.Select(l => l.ApproverUserId).Distinct().ToList();
|
||||||
var levelGroup = step.Levels.Where(l => l.Order == levelOrder).ToList();
|
var approverInfos = allApproverIds.Count == 0
|
||||||
var approverIds = levelGroup.Select(l => l.ApproverUserId).Distinct().ToList();
|
|
||||||
var approverInfos = approverIds.Count == 0
|
|
||||||
? new Dictionary<Guid, (string FullName, string? Email)>()
|
? new Dictionary<Guid, (string FullName, string? Email)>()
|
||||||
: await userManager.Users.AsNoTracking()
|
: 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 })
|
.Select(u => new { u.Id, u.FullName, u.Email })
|
||||||
.ToDictionaryAsync(u => u.Id, u => (u.FullName, u.Email), ct);
|
.ToDictionaryAsync(u => u.Id, u => (u.FullName, u.Email), ct);
|
||||||
|
|
||||||
var approvers = levelGroup
|
// Compute Status mỗi level theo Phase + currentStepIdx + currentLevelOrder
|
||||||
.Select(l =>
|
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);
|
approverInfos.TryGetValue(l.ApproverUserId, out var info);
|
||||||
return new PurchaseEvaluationApprovalLevelApproverDto(
|
return new PurchaseEvaluationApprovalLevelApproverDto(
|
||||||
l.ApproverUserId,
|
l.ApproverUserId,
|
||||||
info.FullName ?? l.ApproverUserId.ToString(),
|
info.FullName ?? l.ApproverUserId.ToString(),
|
||||||
info.Email);
|
info.Email);
|
||||||
})
|
}).ToList();
|
||||||
.ToList();
|
var levelName = g.FirstOrDefault()?.Name;
|
||||||
var levelName = levelGroup.FirstOrDefault()?.Name;
|
return new PurchaseEvaluationApprovalFlowLevelDto(
|
||||||
|
g.Key, levelName, approvers,
|
||||||
|
ComputeLevelStatus(i, g.Key));
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
currentApproval = new PurchaseEvaluationCurrentApprovalDto(
|
flowSteps.Add(new PurchaseEvaluationApprovalFlowStepDto(
|
||||||
idx, step.Name, step.DepartmentId, stepDeptName,
|
step.Order, step.Name,
|
||||||
levelOrder, levelName, approvers);
|
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.BudgetId, budgetSummary,
|
||||||
e.BudgetManualName, e.BudgetManualAmount,
|
e.BudgetManualName, e.BudgetManualAmount,
|
||||||
e.ApprovalWorkflowId, awCode, awName, awVersion,
|
e.ApprovalWorkflowId, awCode, awName, awVersion,
|
||||||
currentApproval,
|
currentApproval, approvalFlow,
|
||||||
e.Suppliers
|
e.Suppliers
|
||||||
.OrderBy(s => s.Order)
|
.OrderBy(s => s.Order)
|
||||||
.Select(s => new PurchaseEvaluationSupplierDto(
|
.Select(s => new PurchaseEvaluationSupplierDto(
|
||||||
|
|||||||
Reference in New Issue
Block a user