From c4ece8071faa247c118e0437b85413b809233c40 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Mon, 11 May 2026 10:24:07 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-PE:=20Section=20=C3=9D=20ki?= =?UTF-8?q?=E1=BA=BFn=20revise=20=E2=80=94=20=C3=B4=20vu=C3=B4ng=20cards?= =?UTF-8?q?=20grid-cols-2=20+=20counter=20C=E1=BA=A5p=20=C4=91=C3=BAng=20s?= =?UTF-8?q?emantic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback Session 20 turn 2: 1. "Chỗ ý kiến vẫn hiển thị ô vuông như trước nhé" — revert visual về cards grid-cols-2 mirror S19 (Chunk C cũ dùng vertical list inline không phải ô vuông như trước). 2. "Số bước duyệt khác số người duyệt trong 1 bước, check lại" — counter cũ `{opinions.length}/{totalApprovers}` sai semantic vì OR-of-N (mỗi Cấp chỉ cần 1 NV ký, không cần ký tất cả NV). totalApprovers đếm tổng NV gây hiểu lầm. Fix (FE-only mirror fe-admin + fe-user): - StepOpinionsBox body chuyển từ `space-y-2` (vertical list) sang `grid grid-cols-1 md:grid-cols-2 gap-3` — mỗi opinion = 1 card đầy đủ border-emerald-200 + bg-white + p-3 (mirror visual S19 LevelOpinionBox). - StepOpinionEntry restore styling đầy đủ: - Header: "Cấp N — Tên NV" font-semibold + admin override badge amber + "✓ Đã duyệt" emerald rounded-full badge - Body: comment text-sm - Footer: signedAt border-t separator (như S19) - Counter mới: `{signedLevels}/{totalLevels} cấp đã duyệt · {totalApprovers} NV tham gia` — đếm Cấp distinct (Set unique levelOrder) thay vì count NV. Tooltip giải thích "OR-of-N" cho user hiểu. - KHÔNG đụng schema Mig 26 (vẫn UPSERT 1 row / Level qua Service). Verify: - npm run build × fe-admin pass - npm run build × fe-user pass - Test pass mặc định skip (Q4 UAT iteration) Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 54 +++++++++++++-------- fe-user/src/components/pe/PeDetailTabs.tsx | 54 +++++++++++++-------- 2 files changed, 66 insertions(+), 42 deletions(-) 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')} +
) }