-
- {opinion.approverFullName}
-
- Cấp {opinion.levelOrder}
-
-
+
+ Cấp {opinion.levelOrder} — {opinion.approverFullName}
+
{isAdminOverride && (
⚠ Admin {opinion.signedByFullName} duyệt thay
@@ -486,12 +495,15 @@ function StepOpinionEntry({ opinion }: { opinion: PeLevelOpinion }) {
)}
- {new Date(opinion.signedAt).toLocaleString('vi-VN')}
+ Đã duyệt
-
+
{opinion.comment}
+
+ {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 81ca743..fe96286 100644
--- a/fe-user/src/components/pe/PeDetailTabs.tsx
+++ b/fe-user/src/components/pe/PeDetailTabs.tsx
@@ -391,10 +391,13 @@ 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).
+// Session 20 Chunk C (revised): gộp opinions đồng cấp cùng Phòng → 1 wrapper box / Step,
+// BÊN TRONG render từng NV đã duyệt thành các "ô vuông" card mirror visual S19
+// (grid-cols-2 cards). User feedback turn 2: giữ visual ô vuông như trước.
+//
+// Counter fix turn 2: "Số bước duyệt" (= số Cấp / Step) KHÁC "số người duyệt trong
+// 1 bước" (= tổng NV across Cấp, OR-of-N nên chỉ 1 NV/Cấp cần ký). Counter đúng
+// hiển thị X/Y cấp đã duyệt + thông tin phụ tổng NV tham gia.
function LevelOpinionsSectionV2({ ev }: { ev: PeDetailBundle }) {
const flow = ev.approvalFlow
const opinions = ev.levelOpinions
@@ -410,18 +413,22 @@ function LevelOpinionsSectionV2({ ev }: { ev: PeDetailBundle }) {
return (
{flow.steps.map(step => {
+ const totalLevels = step.levels.length
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))
+ const signedLevels = new Set(stepOpinions.map(o => o.levelOrder)).size
return (
)
@@ -431,17 +438,19 @@ function LevelOpinionsSectionV2({ ev }: { ev: PeDetailBundle }) {
}
function StepOpinionsBox({
- stepOrder, stepName, departmentName, totalApprovers, opinions,
+ stepOrder, stepName, departmentName, totalLevels, totalApprovers, signedLevels, opinions,
}: {
stepOrder: number
stepName: string
departmentName?: string | null
- totalApprovers: number
+ totalLevels: number // số Cấp (bước duyệt nhỏ trong Step)
+ totalApprovers: number // tổng NV tham gia (FYI — OR-of-N nên không cần ký hết)
+ signedLevels: number // số Cấp đã có ít nhất 1 NV ký
opinions: PeLevelOpinion[]
}) {
return (
-
-
+
+
Bước {stepOrder} — {stepName}
@@ -450,15 +459,18 @@ function StepOpinionsBox({
{departmentName}
)}
-
- {opinions.length}/{totalApprovers} đã duyệt
+
+ {signedLevels}/{totalLevels} cấp đã duyệt · {totalApprovers} NV tham gia
{opinions.length === 0 ? (
— Chưa có ý kiến duyệt.
) : (
-
+
{opinions.map(o => )}
)}
@@ -470,15 +482,12 @@ function StepOpinionsBox({
function StepOpinionEntry({ opinion }: { opinion: PeLevelOpinion }) {
const isAdminOverride = opinion.signedByUserId !== opinion.approverUserId
return (
-
-
+
+
-
- {opinion.approverFullName}
-
- Cấp {opinion.levelOrder}
-
-
+
+ Cấp {opinion.levelOrder} — {opinion.approverFullName}
+
{isAdminOverride && (
⚠ Admin {opinion.signedByFullName} duyệt thay
@@ -486,12 +495,15 @@ function StepOpinionEntry({ opinion }: { opinion: PeLevelOpinion }) {
)}
- {new Date(opinion.signedAt).toLocaleString('vi-VN')}
+ Đã duyệt
-
+
{opinion.comment}
+
+ {new Date(opinion.signedAt).toLocaleString('vi-VN')}
+
)
}