[CLAUDE] PE Workflow V2: disable nút Duyệt nếu actor không trong cấp hiện tại
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: "Nếu không đúng bước duyệt thì nút duyệt cho Disable luôn cũng đc."
BE — DTO + Handler populate "Bước/Cấp đang chờ duyệt":
- Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs:
+PurchaseEvaluationApprovalLevelApproverDto { UserId, FullName, Email }
+PurchaseEvaluationCurrentApprovalDto { StepIndex, StepName,
StepDepartmentId/Name, LevelOrder, LevelName, Approvers[] }
PurchaseEvaluationDetailBundleDto +CurrentApproval? optional field
- Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs handler
GetById: khi pin V2 + Phase=ChoDuyet → load AW.Steps.Levels Include
3-level + group by Order = Cấp + resolve user names → populate
CurrentApproval. Null khi V1 legacy hoặc không phải ChoDuyet.
FE — types + PeWorkflowPanel (cả 2 app mirror):
- types/purchaseEvaluation.ts: +PeCurrentApproval + PeCurrentApprovalLevelApprover
+ PeDetail.currentApproval optional
- PeWorkflowPanel:
* Banner V2 hiển thị "Đang chờ Bước N (TênBước · Phòng X) — Cấp K"
+ list NV được duyệt + status emerald (đến lượt) / amber (không phải lượt)
* useAuth() để check currentUser.id ∈ approvers + Admin bypass
* Button "Duyệt forward" disabled khi V2 pin + actor không khớp.
Title tooltip "Cấp K chỉ {NV X / NV Y} mới duyệt được."
* Button "Trả lại" + "Từ chối" vẫn enabled (BE không gating 2 hành
động này theo Cấp — Approver có thể reject bất cứ lúc nào).
* Send-back logic update: target = DangSoanThao OR TraLai (V2 dùng TraLai)
- Admin role bypass mọi check.
Verify: 81 test pass · npm build × 2 OK · BE 0 error.
Test thử:
1. NV X (approver Cấp 1 V2) login → banner emerald "Đến lượt bạn duyệt"
+ nút "✓ Duyệt → ChoDuyet" enabled
2. NV Y (không phải approver) login → banner amber "Không phải lượt
bạn — chỉ NV X mới duyệt được" + nút Duyệt grey disabled, hover tooltip
3. Admin login → bypass, button enabled
This commit is contained in:
@ -12,6 +12,7 @@ import { Textarea } from '@/components/ui/Textarea'
|
|||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { getErrorMessage } from '@/lib/apiError'
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
import { cn } from '@/lib/cn'
|
import { cn } from '@/lib/cn'
|
||||||
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
import {
|
import {
|
||||||
ApprovalStage,
|
ApprovalStage,
|
||||||
PurchaseEvaluationPhase,
|
PurchaseEvaluationPhase,
|
||||||
@ -33,6 +34,20 @@ export function PeWorkflowPanel({
|
|||||||
const [target, setTarget] = useState<number | null>(null)
|
const [target, setTarget] = useState<number | null>(null)
|
||||||
const [comment, setComment] = useState('')
|
const [comment, setComment] = useState('')
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const { user: currentUser } = useAuth()
|
||||||
|
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
||||||
|
|
||||||
|
// Mig 24 — V2 schema chỉ cho phép approver trong CurrentApproval.approvers
|
||||||
|
// duyệt cấp hiện tại. Nếu actor không khớp → disable nút "Duyệt forward"
|
||||||
|
// (Trả lại / Từ chối vẫn enabled vì Service không kiểm Bước/Cấp với 2
|
||||||
|
// hành động này — Approver có thể reject bất cứ lúc nào trong phiên).
|
||||||
|
// Admin bypass.
|
||||||
|
const v2Approvers = evaluation.currentApproval?.approvers ?? []
|
||||||
|
const actorInV2Level = isAdmin
|
||||||
|
|| (currentUser?.id && v2Approvers.some(a => a.userId === currentUser.id))
|
||||||
|
// V2 active = phiếu pin V2 + Phase=ChoDuyet + có currentApproval
|
||||||
|
const isV2Pending = !!evaluation.currentApproval
|
||||||
|
const blockedByV2Level = isV2Pending && !actorInV2Level
|
||||||
|
|
||||||
// 2-stage dept approvals (Migration 16) — fetch riêng để FE Workflow Panel
|
// 2-stage dept approvals (Migration 16) — fetch riêng để FE Workflow Panel
|
||||||
// hiển thị progress per phase × dept (Stage Review NV / Confirm TPB).
|
// hiển thị progress per phase × dept (Stage Review NV / Confirm TPB).
|
||||||
@ -107,33 +122,65 @@ export function PeWorkflowPanel({
|
|||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
{/* 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. */}
|
||||||
|
{isV2Pending && evaluation.currentApproval && !readOnly && (
|
||||||
|
<div className={cn(
|
||||||
|
'rounded border px-3 py-2 text-[11px]',
|
||||||
|
actorInV2Level
|
||||||
|
? 'border-emerald-200 bg-emerald-50 text-emerald-800'
|
||||||
|
: 'border-amber-300 bg-amber-50 text-amber-800',
|
||||||
|
)}>
|
||||||
|
<div className="font-medium">
|
||||||
|
Đang chờ Bước {evaluation.currentApproval.stepIndex + 1} ({evaluation.currentApproval.stepName})
|
||||||
|
{evaluation.currentApproval.stepDepartmentName && <> · {evaluation.currentApproval.stepDepartmentName}</>}
|
||||||
|
{' — '}Cấp {evaluation.currentApproval.levelOrder}
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5">
|
||||||
|
NV duyệt: {evaluation.currentApproval.approvers.map(a => a.fullName).join(' / ') || '(chưa có)'}
|
||||||
|
</div>
|
||||||
|
{actorInV2Level
|
||||||
|
? <div className="mt-0.5 font-medium">✓ Đến lượt bạn duyệt</div>
|
||||||
|
: <div className="mt-0.5">⚠ Không phải lượt bạn — chỉ NV trên mới duyệt được cấp này</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{next.length > 0 && !readOnly && (
|
{next.length > 0 && !readOnly && (
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">Hành động:</Label>
|
<Label className="text-xs">Hành động:</Label>
|
||||||
<div className="mt-1 flex flex-wrap gap-1.5">
|
<div className="mt-1 flex flex-wrap gap-1.5">
|
||||||
{next.map(p => {
|
{next.map(p => {
|
||||||
// Phân loại button theo hành động:
|
// Phân loại button theo hành động:
|
||||||
// - Trả lại = về DangSoanThao (từ phase trung gian) — red
|
// - Trả lại = về DangSoanThao/TraLai (từ phase trung gian) — red
|
||||||
// - Hủy/Từ chối = TuChoi (chỉ ở phase DangSoanThao đầu) — red
|
// - Hủy/Từ chối = TuChoi (chỉ ở phase DangSoanThao đầu) — red
|
||||||
// - Duyệt = forward phase tiếp theo — brand
|
// - Duyệt = forward phase tiếp theo — brand
|
||||||
const isSendBack = p === PurchaseEvaluationPhase.DangSoanThao
|
const isSendBack = (p === PurchaseEvaluationPhase.DangSoanThao || p === PurchaseEvaluationPhase.TraLai)
|
||||||
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao
|
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao
|
||||||
|
&& evaluation.phase !== PurchaseEvaluationPhase.TraLai
|
||||||
const isCancel = p === PurchaseEvaluationPhase.TuChoi
|
const isCancel = p === PurchaseEvaluationPhase.TuChoi
|
||||||
const isDanger = isSendBack || isCancel
|
const isDanger = isSendBack || isCancel
|
||||||
|
const isForwardApprove = !isDanger
|
||||||
|
// Mig 24 — disable Duyệt forward nếu V2 pin + actor không trong cấp hiện tại
|
||||||
|
const isDisabled = isForwardApprove && blockedByV2Level
|
||||||
const label = isSendBack
|
const label = isSendBack
|
||||||
? '← Trả lại (về Drafter sửa)'
|
? '← Trả lại (về Drafter sửa)'
|
||||||
: isCancel
|
: isCancel
|
||||||
? '✗ Hủy / Từ chối'
|
? '✗ Hủy / Từ chối'
|
||||||
: `✓ Duyệt → ${PurchaseEvaluationPhaseLabel[p]}`
|
: `✓ Duyệt → ${PurchaseEvaluationPhaseLabel[p]}`
|
||||||
|
const title = isDisabled && evaluation.currentApproval
|
||||||
|
? `Cấp ${evaluation.currentApproval.levelOrder} chỉ ${evaluation.currentApproval.approvers.map(a => a.fullName).join(' / ')} mới duyệt được.`
|
||||||
|
: undefined
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={p}
|
key={p}
|
||||||
onClick={() => setTarget(p)}
|
onClick={() => !isDisabled && setTarget(p)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
title={title}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded border px-2 py-1 text-[11px] font-medium transition',
|
'rounded border px-2 py-1 text-[11px] font-medium transition',
|
||||||
isDanger
|
isDisabled && 'cursor-not-allowed border-slate-200 bg-slate-50 text-slate-400 hover:bg-slate-50',
|
||||||
? 'border-red-200 text-red-700 hover:bg-red-50'
|
!isDisabled && isDanger && 'border-red-200 text-red-700 hover:bg-red-50',
|
||||||
: 'border-brand-300 text-brand-700 hover:bg-brand-50',
|
!isDisabled && !isDanger && 'border-brand-300 text-brand-700 hover:bg-brand-50',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@ -206,6 +206,24 @@ export type PeWorkflowSummary = {
|
|||||||
nextPhases: number[]
|
nextPhases: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mig 22-24 V2 — info Bước/Cấp đang chờ duyệt + danh sách approver được duyệt.
|
||||||
|
// FE check userId hiện tại có trong approvers[] → enable nút Duyệt.
|
||||||
|
export type PeCurrentApprovalLevelApprover = {
|
||||||
|
userId: string
|
||||||
|
fullName: string
|
||||||
|
email: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PeCurrentApproval = {
|
||||||
|
stepIndex: number // 0-based
|
||||||
|
stepName: string
|
||||||
|
stepDepartmentId: string | null
|
||||||
|
stepDepartmentName: string | null
|
||||||
|
levelOrder: number // 1/2/3
|
||||||
|
levelName: string | null
|
||||||
|
approvers: PeCurrentApprovalLevelApprover[]
|
||||||
|
}
|
||||||
|
|
||||||
export type PeChangelog = {
|
export type PeChangelog = {
|
||||||
id: string
|
id: string
|
||||||
entityType: number
|
entityType: number
|
||||||
@ -315,6 +333,8 @@ export type PeDetailBundle = {
|
|||||||
approvalWorkflowCode: string | null
|
approvalWorkflowCode: string | null
|
||||||
approvalWorkflowName: string | null
|
approvalWorkflowName: string | null
|
||||||
approvalWorkflowVersion: number | null
|
approvalWorkflowVersion: number | null
|
||||||
|
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
||||||
|
currentApproval: PeCurrentApproval | null
|
||||||
suppliers: PeSupplier[]
|
suppliers: PeSupplier[]
|
||||||
details: PeDetailRow[]
|
details: PeDetailRow[]
|
||||||
approvals: PeApproval[]
|
approvals: PeApproval[]
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { Textarea } from '@/components/ui/Textarea'
|
|||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { getErrorMessage } from '@/lib/apiError'
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
import { cn } from '@/lib/cn'
|
import { cn } from '@/lib/cn'
|
||||||
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
import {
|
import {
|
||||||
ApprovalStage,
|
ApprovalStage,
|
||||||
PurchaseEvaluationPhase,
|
PurchaseEvaluationPhase,
|
||||||
@ -33,6 +34,16 @@ export function PeWorkflowPanel({
|
|||||||
const [target, setTarget] = useState<number | null>(null)
|
const [target, setTarget] = useState<number | null>(null)
|
||||||
const [comment, setComment] = useState('')
|
const [comment, setComment] = useState('')
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const { user: currentUser } = useAuth()
|
||||||
|
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
||||||
|
|
||||||
|
// Mig 24 — V2 schema chỉ cho phép approver trong CurrentApproval.approvers
|
||||||
|
// duyệt cấp hiện tại. Admin bypass.
|
||||||
|
const v2Approvers = evaluation.currentApproval?.approvers ?? []
|
||||||
|
const actorInV2Level = isAdmin
|
||||||
|
|| (currentUser?.id && v2Approvers.some(a => a.userId === currentUser.id))
|
||||||
|
const isV2Pending = !!evaluation.currentApproval
|
||||||
|
const blockedByV2Level = isV2Pending && !actorInV2Level
|
||||||
|
|
||||||
// 2-stage dept approvals (Migration 16) — fetch riêng để FE Workflow Panel
|
// 2-stage dept approvals (Migration 16) — fetch riêng để FE Workflow Panel
|
||||||
// hiển thị progress per phase × dept (Stage Review NV / Confirm TPB).
|
// hiển thị progress per phase × dept (Stage Review NV / Confirm TPB).
|
||||||
@ -107,33 +118,60 @@ export function PeWorkflowPanel({
|
|||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
{/* Mig 24 — V2 banner Bước/Cấp + danh sách NV duyệt */}
|
||||||
|
{isV2Pending && evaluation.currentApproval && !readOnly && (
|
||||||
|
<div className={cn(
|
||||||
|
'rounded border px-3 py-2 text-[11px]',
|
||||||
|
actorInV2Level
|
||||||
|
? 'border-emerald-200 bg-emerald-50 text-emerald-800'
|
||||||
|
: 'border-amber-300 bg-amber-50 text-amber-800',
|
||||||
|
)}>
|
||||||
|
<div className="font-medium">
|
||||||
|
Đang chờ Bước {evaluation.currentApproval.stepIndex + 1} ({evaluation.currentApproval.stepName})
|
||||||
|
{evaluation.currentApproval.stepDepartmentName && <> · {evaluation.currentApproval.stepDepartmentName}</>}
|
||||||
|
{' — '}Cấp {evaluation.currentApproval.levelOrder}
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5">
|
||||||
|
NV duyệt: {evaluation.currentApproval.approvers.map(a => a.fullName).join(' / ') || '(chưa có)'}
|
||||||
|
</div>
|
||||||
|
{actorInV2Level
|
||||||
|
? <div className="mt-0.5 font-medium">✓ Đến lượt bạn duyệt</div>
|
||||||
|
: <div className="mt-0.5">⚠ Không phải lượt bạn — chỉ NV trên mới duyệt được cấp này</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{next.length > 0 && !readOnly && (
|
{next.length > 0 && !readOnly && (
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">Hành động:</Label>
|
<Label className="text-xs">Hành động:</Label>
|
||||||
<div className="mt-1 flex flex-wrap gap-1.5">
|
<div className="mt-1 flex flex-wrap gap-1.5">
|
||||||
{next.map(p => {
|
{next.map(p => {
|
||||||
// Phân loại button theo hành động:
|
const isSendBack = (p === PurchaseEvaluationPhase.DangSoanThao || p === PurchaseEvaluationPhase.TraLai)
|
||||||
// - Trả lại = về DangSoanThao (từ phase trung gian) — red
|
|
||||||
// - Hủy/Từ chối = TuChoi (chỉ ở phase DangSoanThao đầu) — red
|
|
||||||
// - Duyệt = forward phase tiếp theo — brand
|
|
||||||
const isSendBack = p === PurchaseEvaluationPhase.DangSoanThao
|
|
||||||
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao
|
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao
|
||||||
|
&& evaluation.phase !== PurchaseEvaluationPhase.TraLai
|
||||||
const isCancel = p === PurchaseEvaluationPhase.TuChoi
|
const isCancel = p === PurchaseEvaluationPhase.TuChoi
|
||||||
const isDanger = isSendBack || isCancel
|
const isDanger = isSendBack || isCancel
|
||||||
|
const isForwardApprove = !isDanger
|
||||||
|
// Mig 24 — disable Duyệt forward nếu V2 pin + actor không trong cấp hiện tại
|
||||||
|
const isDisabled = isForwardApprove && blockedByV2Level
|
||||||
const label = isSendBack
|
const label = isSendBack
|
||||||
? '← Trả lại (về Drafter sửa)'
|
? '← Trả lại (về Drafter sửa)'
|
||||||
: isCancel
|
: isCancel
|
||||||
? '✗ Hủy / Từ chối'
|
? '✗ Hủy / Từ chối'
|
||||||
: `✓ Duyệt → ${PurchaseEvaluationPhaseLabel[p]}`
|
: `✓ Duyệt → ${PurchaseEvaluationPhaseLabel[p]}`
|
||||||
|
const title = isDisabled && evaluation.currentApproval
|
||||||
|
? `Cấp ${evaluation.currentApproval.levelOrder} chỉ ${evaluation.currentApproval.approvers.map(a => a.fullName).join(' / ')} mới duyệt được.`
|
||||||
|
: undefined
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={p}
|
key={p}
|
||||||
onClick={() => setTarget(p)}
|
onClick={() => !isDisabled && setTarget(p)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
title={title}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded border px-2 py-1 text-[11px] font-medium transition',
|
'rounded border px-2 py-1 text-[11px] font-medium transition',
|
||||||
isDanger
|
isDisabled && 'cursor-not-allowed border-slate-200 bg-slate-50 text-slate-400 hover:bg-slate-50',
|
||||||
? 'border-red-200 text-red-700 hover:bg-red-50'
|
!isDisabled && isDanger && 'border-red-200 text-red-700 hover:bg-red-50',
|
||||||
: 'border-brand-300 text-brand-700 hover:bg-brand-50',
|
!isDisabled && !isDanger && 'border-brand-300 text-brand-700 hover:bg-brand-50',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@ -205,6 +205,23 @@ export type PeWorkflowSummary = {
|
|||||||
nextPhases: number[]
|
nextPhases: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mig 22-24 V2 — info Bước/Cấp đang chờ duyệt + danh sách approver được duyệt.
|
||||||
|
export type PeCurrentApprovalLevelApprover = {
|
||||||
|
userId: string
|
||||||
|
fullName: string
|
||||||
|
email: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PeCurrentApproval = {
|
||||||
|
stepIndex: number
|
||||||
|
stepName: string
|
||||||
|
stepDepartmentId: string | null
|
||||||
|
stepDepartmentName: string | null
|
||||||
|
levelOrder: number
|
||||||
|
levelName: string | null
|
||||||
|
approvers: PeCurrentApprovalLevelApprover[]
|
||||||
|
}
|
||||||
|
|
||||||
export type PeChangelog = {
|
export type PeChangelog = {
|
||||||
id: string
|
id: string
|
||||||
entityType: number
|
entityType: number
|
||||||
@ -314,6 +331,8 @@ export type PeDetailBundle = {
|
|||||||
approvalWorkflowCode: string | null
|
approvalWorkflowCode: string | null
|
||||||
approvalWorkflowName: string | null
|
approvalWorkflowName: string | null
|
||||||
approvalWorkflowVersion: number | null
|
approvalWorkflowVersion: number | null
|
||||||
|
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
||||||
|
currentApproval: PeCurrentApproval | null
|
||||||
suppliers: PeSupplier[]
|
suppliers: PeSupplier[]
|
||||||
details: PeDetailRow[]
|
details: PeDetailRow[]
|
||||||
approvals: PeApproval[]
|
approvals: PeApproval[]
|
||||||
|
|||||||
@ -84,6 +84,23 @@ public record PurchaseEvaluationWorkflowSummaryDto(
|
|||||||
List<PurchaseEvaluationPhase> ActivePhases,
|
List<PurchaseEvaluationPhase> ActivePhases,
|
||||||
List<PurchaseEvaluationPhase> NextPhases);
|
List<PurchaseEvaluationPhase> NextPhases);
|
||||||
|
|
||||||
|
// Mig 22-24 V2 schema — info "Bước/Cấp đang chờ duyệt" để FE disable nút
|
||||||
|
// Duyệt nếu user không phải approver. Null khi pin V1 legacy hoặc Phase
|
||||||
|
// không phải ChoDuyet.
|
||||||
|
public record PurchaseEvaluationApprovalLevelApproverDto(
|
||||||
|
Guid UserId,
|
||||||
|
string FullName,
|
||||||
|
string? Email);
|
||||||
|
|
||||||
|
public record PurchaseEvaluationCurrentApprovalDto(
|
||||||
|
int StepIndex, // 0-based
|
||||||
|
string StepName,
|
||||||
|
Guid? StepDepartmentId,
|
||||||
|
string? StepDepartmentName,
|
||||||
|
int LevelOrder, // 1/2/3
|
||||||
|
string? LevelName,
|
||||||
|
List<PurchaseEvaluationApprovalLevelApproverDto> Approvers);
|
||||||
|
|
||||||
public record PurchaseEvaluationAttachmentDto(
|
public record PurchaseEvaluationAttachmentDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid? PurchaseEvaluationSupplierId,
|
Guid? PurchaseEvaluationSupplierId,
|
||||||
@ -135,6 +152,7 @@ public record PurchaseEvaluationDetailBundleDto(
|
|||||||
string? ApprovalWorkflowCode,
|
string? ApprovalWorkflowCode,
|
||||||
string? ApprovalWorkflowName,
|
string? ApprovalWorkflowName,
|
||||||
int? ApprovalWorkflowVersion,
|
int? ApprovalWorkflowVersion,
|
||||||
|
PurchaseEvaluationCurrentApprovalDto? CurrentApproval,
|
||||||
List<PurchaseEvaluationSupplierDto> Suppliers,
|
List<PurchaseEvaluationSupplierDto> Suppliers,
|
||||||
List<PurchaseEvaluationDetailDto> Details,
|
List<PurchaseEvaluationDetailDto> Details,
|
||||||
List<PurchaseEvaluationApprovalDto> Approvals,
|
List<PurchaseEvaluationApprovalDto> Approvals,
|
||||||
|
|||||||
@ -453,19 +453,66 @@ 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 } để
|
||||||
|
// FE biết user nào được duyệt cấp hiện tại → disable button đúng.
|
||||||
string? awCode = null, awName = null;
|
string? awCode = null, awName = null;
|
||||||
int? awVersion = null;
|
int? awVersion = null;
|
||||||
|
PurchaseEvaluationCurrentApprovalDto? currentApproval = 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()
|
||||||
.Where(w => w.Id == awId)
|
.Include(w => w.Steps.OrderBy(s => s.Order))
|
||||||
.Select(w => new { w.Code, w.Name, w.Version })
|
.ThenInclude(s => s.Levels.OrderBy(l => l.Order))
|
||||||
.FirstOrDefaultAsync(ct);
|
.FirstOrDefaultAsync(w => w.Id == awId, ct);
|
||||||
if (aw is not null)
|
if (aw is not null)
|
||||||
{
|
{
|
||||||
awCode = aw.Code;
|
awCode = aw.Code;
|
||||||
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();
|
||||||
|
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
|
||||||
|
? new Dictionary<Guid, (string FullName, string? Email)>()
|
||||||
|
: 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,6 +527,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,
|
||||||
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