From 6e913b37a191ce937973a125ae809b5e6d1b4ddd Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Sat, 9 May 2026 11:05:03 +0700 Subject: [PATCH] [CLAUDE] FE-PE: Chunk C Section 5 V2 dynamic theo ApprovalWorkflowLevel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section 5 PeDetailTabs render dynamic theo workflow đã pin (V2). Thay 4 box CỨNG (PheDuyet/CCM/MuaHàng/SmPm Mig 15) cho phiếu V2. Type: `PeLevelOpinion` (15 field) + `PeDetailBundle.levelOpinions[]`. Section 5 conditional: - evaluation.approvalWorkflowId set → - V1 legacy (no awId) → readOnly fallback (giữ data Mig 15) LevelOpinionsSectionV2: - Layout 5A — group theo Step (header "Bước N — " + dept badge emerald) - grid-cols-2 cho approvers trong tất cả Levels của Step - Hint "(N người duyệt)" khi totalApprovers > 1 - Empty state khi flow null / 0 steps LevelOpinionBox (read-only — Q1=1B sync auto từ Workflow Panel): - Title "Cấp N — " - Badge amber "⚠ Admin duyệt thay" khi SignedByUserId !== ApproverUserId - Badge emerald "✓ Đã duyệt" khi opinion tồn tại - Empty: "— chưa duyệt" italic gray - Footer: timestamp signedAt format vi-VN Workspace mode hint giữ amber "Ý kiến + chữ ký auto đồng bộ khi NV duyệt". Mirror fe-admin + fe-user (rule §3.9). Verify: npm run build × 2 pass · 0 TS error. Chunk D kế tiếp: Docs (STATUS/HANDOFF/schema-diagram/session log). --- fe-admin/src/components/pe/PeDetailTabs.tsx | 128 +++++++++++++++++++- fe-admin/src/types/purchaseEvaluation.ts | 23 ++++ fe-user/src/components/pe/PeDetailTabs.tsx | 128 +++++++++++++++++++- fe-user/src/types/purchaseEvaluation.ts | 23 ++++ 4 files changed, 296 insertions(+), 6 deletions(-) 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 }