From d814429cee6b4e0df492595dfca728a0000e02ef Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Fri, 8 May 2026 15:03:29 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20PE=20Workflow=20V2:=20disable=20n?= =?UTF-8?q?=C3=BAt=20Duy=E1=BB=87t=20n=E1=BA=BFu=20actor=20kh=C3=B4ng=20tr?= =?UTF-8?q?ong=20c=E1=BA=A5p=20hi=E1=BB=87n=20t=E1=BA=A1i?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/components/pe/PeWorkflowPanel.tsx | 59 +++++++++++++++++-- fe-admin/src/types/purchaseEvaluation.ts | 20 +++++++ fe-user/src/components/pe/PeWorkflowPanel.tsx | 56 +++++++++++++++--- fe-user/src/types/purchaseEvaluation.ts | 19 ++++++ .../Dtos/PurchaseEvaluationDtos.cs | 18 ++++++ .../PurchaseEvaluationFeatures.cs | 54 ++++++++++++++++- 6 files changed, 208 insertions(+), 18 deletions(-) diff --git a/fe-admin/src/components/pe/PeWorkflowPanel.tsx b/fe-admin/src/components/pe/PeWorkflowPanel.tsx index 9aa63de..ab4571d 100644 --- a/fe-admin/src/components/pe/PeWorkflowPanel.tsx +++ b/fe-admin/src/components/pe/PeWorkflowPanel.tsx @@ -12,6 +12,7 @@ import { Textarea } from '@/components/ui/Textarea' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { cn } from '@/lib/cn' +import { useAuth } from '@/contexts/AuthContext' import { ApprovalStage, PurchaseEvaluationPhase, @@ -33,6 +34,20 @@ export function PeWorkflowPanel({ const [target, setTarget] = useState(null) const [comment, setComment] = useState('') 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 // hiển thị progress per phase × dept (Stage Review NV / Confirm TPB). @@ -107,33 +122,65 @@ export function PeWorkflowPanel({ })} + {/* 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 && ( +
+
+ Đang chờ Bước {evaluation.currentApproval.stepIndex + 1} ({evaluation.currentApproval.stepName}) + {evaluation.currentApproval.stepDepartmentName && <> · {evaluation.currentApproval.stepDepartmentName}} + {' — '}Cấp {evaluation.currentApproval.levelOrder} +
+
+ NV duyệt: {evaluation.currentApproval.approvers.map(a => a.fullName).join(' / ') || '(chưa có)'} +
+ {actorInV2Level + ?
✓ Đến lượt bạn duyệt
+ :
⚠ Không phải lượt bạn — chỉ NV trên mới duyệt được cấp này
} +
+ )} + {next.length > 0 && !readOnly && (
{next.map(p => { // 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 // - 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.TraLai const isCancel = p === PurchaseEvaluationPhase.TuChoi 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 ? '← Trả lại (về Drafter sửa)' : isCancel ? '✗ Hủy / Từ chối' : `✓ 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 (