+
+
+ Bước {stepOrder} — {stepName}
+
+ {departmentName && (
+
+ {departmentName}
+
+ )}
+
+ {opinions.length}/{totalApprovers} đã duyệt
+
+
+
+ {opinions.length === 0 ? (
+
— Chưa có ý kiến duyệt.
+ ) : (
+
+ {opinions.map(o => )}
+
+ )}
+
+
+ )
+}
+
+function StepOpinionEntry({ opinion }: { opinion: PeLevelOpinion }) {
+ const isAdminOverride = opinion.signedByUserId !== opinion.approverUserId
+ return (
+
+
-
- Cấp {levelOrder} — {approverName}
-
+
+ {opinion.approverFullName}
+
+ Cấp {opinion.levelOrder}
+
+
{isAdminOverride && (
- ⚠ Admin {opinion!.signedByFullName} duyệt thay
+ ⚠ Admin {opinion.signedByFullName} duyệt thay
)}
- {isSigned && (
-
- Đã duyệt
-
- )}
+
+ {new Date(opinion.signedAt).toLocaleString('vi-VN')}
+
-
- {opinion?.comment ??
— chưa duyệt}
+
+ {opinion.comment}
- {isSigned && (
-
- {new Date(opinion!.signedAt).toLocaleString('vi-VN')}
-
- )}
)
}
diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx
index bae82d4..81ca743 100644
--- a/fe-user/src/components/pe/PeDetailTabs.tsx
+++ b/fe-user/src/components/pe/PeDetailTabs.tsx
@@ -391,6 +391,10 @@ function OpinionBox({
// 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.
+// Session 20 Chunk C: gộp opinions đồng cấp cùng Phòng → 1 box / Step.
+// Trước: 1 box / NV (mỗi Level × mỗi approver = 1 OpinionBox).
+// Bây giờ: 1 box / Step (Phòng), chỉ hiển thị các NV ĐÃ duyệt — opinion entries
+// sort theo Cấp tăng dần. NV chưa duyệt KHÔNG hiển thị (user yêu cầu Q3=a).
function LevelOpinionsSectionV2({ ev }: { ev: PeDetailBundle }) {
const flow = ev.approvalFlow
const opinions = ev.levelOpinions
@@ -404,97 +408,90 @@ function LevelOpinionsSectionV2({ ev }: { ev: PeDetailBundle }) {
}
return (
-
+
{flow.steps.map(step => {
const totalApprovers = step.levels.reduce((n, l) => n + l.approvers.length, 0)
+ const stepOpinions = opinions
+ .filter(o => o.stepOrder === step.order)
+ .slice()
+ .sort((a, b) => a.levelOrder - b.levelOrder || a.signedAt.localeCompare(b.signedAt))
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,
+function StepOpinionsBox({
+ stepOrder, stepName, departmentName, totalApprovers, opinions,
}: {
- levelOrder: number
- approverUserId: string
- approverName: string
- opinion: PeLevelOpinion | null
+ stepOrder: number
+ stepName: string
+ departmentName?: string | null
+ totalApprovers: number
+ opinions: PeLevelOpinion[]
}) {
- const isSigned = !!opinion
- const isAdminOverride = isSigned && opinion!.signedByUserId !== approverUserId
-
return (
-
-
+
+
+
+ Bước {stepOrder} — {stepName}
+
+ {departmentName && (
+
+ {departmentName}
+
+ )}
+
+ {opinions.length}/{totalApprovers} đã duyệt
+
+
+
+ {opinions.length === 0 ? (
+
— Chưa có ý kiến duyệt.
+ ) : (
+
+ {opinions.map(o => )}
+
+ )}
+
+
+ )
+}
+
+function StepOpinionEntry({ opinion }: { opinion: PeLevelOpinion }) {
+ const isAdminOverride = opinion.signedByUserId !== opinion.approverUserId
+ return (
+
+
-
- Cấp {levelOrder} — {approverName}
-
+
+ {opinion.approverFullName}
+
+ Cấp {opinion.levelOrder}
+
+
{isAdminOverride && (
- ⚠ Admin {opinion!.signedByFullName} duyệt thay
+ ⚠ Admin {opinion.signedByFullName} duyệt thay
)}
- {isSigned && (
-
- Đã duyệt
-
- )}
+
+ {new Date(opinion.signedAt).toLocaleString('vi-VN')}
+
-
- {opinion?.comment ??
— chưa duyệt}
+
+ {opinion.comment}
- {isSigned && (
-
- {new Date(opinion!.signedAt).toLocaleString('vi-VN')}
-
- )}
)
}