diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 84a11f4..dd753d5 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -33,6 +33,7 @@ import { type PeDepartmentOpinion, type PeDetailBundle, type PeDetailRow, + type PeLevelOpinion, type PeQuote, type PeSupplier, } from '@/types/purchaseEvaluation' @@ -173,13 +174,17 @@ export function PeDetailTabs({
-
+
{mode === 'workspace' && (
- Ý kiến + chữ ký nhập khi duyệt phiếu — vào menu “Duyệt” để ký. + Ý kiến + chữ ký auto đồng bộ khi NV duyệt phiếu — vào menu “Duyệt” để ký.
)} - + {/* Mig 26 — V2 dynamic theo ApprovalWorkflowLevel. V1 phiếu cũ + fallback render 4 box CỨNG readOnly (data legacy giữ Mig 15). */} + {evaluation.approvalWorkflowId + ? + : }
@@ -377,6 +382,123 @@ function OpinionBox({ ) } +// ===== Section 5 V2 — Ý kiến cấp duyệt dynamic (Mig 26 — Session 19) ===== +// +// Render theo workflow đã pin: forEach Step → forEach Level (Cấp) → forEach +// approver (NV). Mỗi NV = 1 OpinionBox (read-only). Service ApproveV2Async +// auto sync comment khi duyệt (Q1=1B). Empty list → fallback message. +// +// Layout 5A: header "Bước N — Phòng X" badge + grid-cols-2 cho N approvers +// (wrap nếu N>2). Admin override badge khi SignedByUserId !== ApproverUserId. + +function LevelOpinionsSectionV2({ ev }: { ev: PeDetailBundle }) { + const flow = ev.approvalFlow + const opinions = ev.levelOpinions + + if (!flow || flow.steps.length === 0) { + return ( +
+ Workflow chưa được cấu hình hoặc chưa có cấp duyệt nào. +
+ ) + } + + return ( +
+ {flow.steps.map(step => { + const totalApprovers = step.levels.reduce((n, l) => n + l.approvers.length, 0) + return ( +
+
+ + Bước {step.order} — {step.name} + + {step.departmentName && ( + + {step.departmentName} + + )} + {totalApprovers > 1 && ( + + ({totalApprovers} người duyệt) + + )} +
+
+ {step.levels.flatMap(level => + level.approvers.map(approver => { + const opinion = opinions.find(o => + o.stepOrder === step.order + && o.levelOrder === level.order + && o.approverUserId === approver.userId, + ) ?? null + return ( + + ) + }), + )} +
+
+ ) + })} +
+ ) +} + +function LevelOpinionBox({ + levelOrder, + approverUserId, + approverName, + opinion, +}: { + levelOrder: number + approverUserId: string + approverName: string + opinion: PeLevelOpinion | null +}) { + const isSigned = !!opinion + const isAdminOverride = isSigned && opinion!.signedByUserId !== approverUserId + + return ( +
+
+
+

+ Cấp {levelOrder} — {approverName} +

+ {isAdminOverride && ( +
+ ⚠ Admin {opinion!.signedByFullName} duyệt thay +
+ )} +
+ {isSigned && ( + + Đã duyệt + + )} +
+
+ {opinion?.comment ?? — chưa duyệt} +
+ {isSigned && ( +
+ {new Date(opinion!.signedAt).toLocaleString('vi-VN')} +
+ )} +
+ ) +} + // ===== Exports cho Panel 3 — Approvals history + Changelog ===== export function PeApprovalsSection({ ev }: { ev: PeDetailBundle }) { diff --git a/fe-admin/src/types/purchaseEvaluation.ts b/fe-admin/src/types/purchaseEvaluation.ts index 4c0ec47..f6d1027 100644 --- a/fe-admin/src/types/purchaseEvaluation.ts +++ b/fe-admin/src/types/purchaseEvaluation.ts @@ -298,6 +298,27 @@ export type PeDepartmentOpinion = { userName: string | null } +// Mig 26 (Session 19) — Section 5 V2 dynamic theo ApprovalWorkflowLevel. +// Service ApproveV2Async UPSERT auto khi NV duyệt (Q1=1B). Empty list cho +// phiếu V1 / V2 chưa có cấp nào duyệt → FE fallback message. +// `signedByUserId !== approverUserId` → FE banner "Admin duyệt thay". +export type PeLevelOpinion = { + id: string + approvalWorkflowLevelId: string + stepOrder: number + stepName: string + stepDepartmentId: string | null + stepDepartmentName: string | null + levelOrder: number + levelName: string | null + approverUserId: string + approverFullName: string | null + comment: string + signedAt: string + signedByUserId: string + signedByFullName: string +} + // 2-stage department approval (Migration 16) — Stage 1=Review NV, 2=Confirm TPB. // BLOCK transition khi NV review chưa có TPB confirm cùng (PE, Phase, Dept). // CanBypassReview=true → NV được Stage=Confirm + IsBypassed=true (skip Review). @@ -366,5 +387,7 @@ export type PeDetailBundle = { approvals: PeApproval[] attachments: PeAttachment[] departmentOpinions: PeDepartmentOpinion[] + // Mig 26 — Section 5 V2 dynamic. Empty cho V1 / V2 chưa có cấp duyệt. + levelOpinions: PeLevelOpinion[] workflow: PeWorkflowSummary } diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index 84a11f4..dd753d5 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -33,6 +33,7 @@ import { type PeDepartmentOpinion, type PeDetailBundle, type PeDetailRow, + type PeLevelOpinion, type PeQuote, type PeSupplier, } from '@/types/purchaseEvaluation' @@ -173,13 +174,17 @@ export function PeDetailTabs({
-
+
{mode === 'workspace' && (
- Ý kiến + chữ ký nhập khi duyệt phiếu — vào menu “Duyệt” để ký. + Ý kiến + chữ ký auto đồng bộ khi NV duyệt phiếu — vào menu “Duyệt” để ký.
)} - + {/* Mig 26 — V2 dynamic theo ApprovalWorkflowLevel. V1 phiếu cũ + fallback render 4 box CỨNG readOnly (data legacy giữ Mig 15). */} + {evaluation.approvalWorkflowId + ? + : }
@@ -377,6 +382,123 @@ function OpinionBox({ ) } +// ===== Section 5 V2 — Ý kiến cấp duyệt dynamic (Mig 26 — Session 19) ===== +// +// Render theo workflow đã pin: forEach Step → forEach Level (Cấp) → forEach +// approver (NV). Mỗi NV = 1 OpinionBox (read-only). Service ApproveV2Async +// auto sync comment khi duyệt (Q1=1B). Empty list → fallback message. +// +// Layout 5A: header "Bước N — Phòng X" badge + grid-cols-2 cho N approvers +// (wrap nếu N>2). Admin override badge khi SignedByUserId !== ApproverUserId. + +function LevelOpinionsSectionV2({ ev }: { ev: PeDetailBundle }) { + const flow = ev.approvalFlow + const opinions = ev.levelOpinions + + if (!flow || flow.steps.length === 0) { + return ( +
+ Workflow chưa được cấu hình hoặc chưa có cấp duyệt nào. +
+ ) + } + + return ( +
+ {flow.steps.map(step => { + const totalApprovers = step.levels.reduce((n, l) => n + l.approvers.length, 0) + return ( +
+
+ + Bước {step.order} — {step.name} + + {step.departmentName && ( + + {step.departmentName} + + )} + {totalApprovers > 1 && ( + + ({totalApprovers} người duyệt) + + )} +
+
+ {step.levels.flatMap(level => + level.approvers.map(approver => { + const opinion = opinions.find(o => + o.stepOrder === step.order + && o.levelOrder === level.order + && o.approverUserId === approver.userId, + ) ?? null + return ( + + ) + }), + )} +
+
+ ) + })} +
+ ) +} + +function LevelOpinionBox({ + levelOrder, + approverUserId, + approverName, + opinion, +}: { + levelOrder: number + approverUserId: string + approverName: string + opinion: PeLevelOpinion | null +}) { + const isSigned = !!opinion + const isAdminOverride = isSigned && opinion!.signedByUserId !== approverUserId + + return ( +
+
+
+

+ Cấp {levelOrder} — {approverName} +

+ {isAdminOverride && ( +
+ ⚠ Admin {opinion!.signedByFullName} duyệt thay +
+ )} +
+ {isSigned && ( + + Đã duyệt + + )} +
+
+ {opinion?.comment ?? — chưa duyệt} +
+ {isSigned && ( +
+ {new Date(opinion!.signedAt).toLocaleString('vi-VN')} +
+ )} +
+ ) +} + // ===== Exports cho Panel 3 — Approvals history + Changelog ===== export function PeApprovalsSection({ ev }: { ev: PeDetailBundle }) { diff --git a/fe-user/src/types/purchaseEvaluation.ts b/fe-user/src/types/purchaseEvaluation.ts index 0ad58a3..ebe3c7f 100644 --- a/fe-user/src/types/purchaseEvaluation.ts +++ b/fe-user/src/types/purchaseEvaluation.ts @@ -295,6 +295,27 @@ export type PeDepartmentOpinion = { userName: string | null } +// Mig 26 (Session 19) — Section 5 V2 dynamic theo ApprovalWorkflowLevel. +// Service ApproveV2Async UPSERT auto khi NV duyệt (Q1=1B). Empty list cho +// phiếu V1 / V2 chưa có cấp nào duyệt → FE fallback message. +// `signedByUserId !== approverUserId` → FE banner "Admin duyệt thay". +export type PeLevelOpinion = { + id: string + approvalWorkflowLevelId: string + stepOrder: number + stepName: string + stepDepartmentId: string | null + stepDepartmentName: string | null + levelOrder: number + levelName: string | null + approverUserId: string + approverFullName: string | null + comment: string + signedAt: string + signedByUserId: string + signedByFullName: string +} + // 2-stage department approval (Migration 16) — Stage 1=Review NV, 2=Confirm TPB. // BLOCK transition khi NV review chưa có TPB confirm cùng (PE, Phase, Dept). // CanBypassReview=true → NV được Stage=Confirm + IsBypassed=true (skip Review). @@ -363,5 +384,7 @@ export type PeDetailBundle = { approvals: PeApproval[] attachments: PeAttachment[] departmentOpinions: PeDepartmentOpinion[] + // Mig 26 — Section 5 V2 dynamic. Empty cho V1 / V2 chưa có cấp duyệt. + levelOpinions: PeLevelOpinion[] workflow: PeWorkflowSummary }