diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 81ca743..fe96286 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/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')} +
) } 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')} +
) }