From f2f01f476552fb01fe559ac9901c76f851e5debb Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Mon, 11 May 2026 10:07:02 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-PE:=20Chunk=20C=20=E2=80=94=20Sec?= =?UTF-8?q?tion=20=C3=9D=20ki=E1=BA=BFn=20g=E1=BB=99p=20=C4=91=E1=BB=93ng?= =?UTF-8?q?=20c=E1=BA=A5p=20c=C3=B9ng=20Ph=C3=B2ng=20(1=20box=20/=20Step)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure Section 5 (rename Section 4 sau Chunk B) "Ý kiến cấp duyệt". User Session 20 Q3=a: gộp các comment đồng cấp cùng Phòng → 1 ô / bước (dù bước có nhiều người), CHỈ hiển thị comment của NV đã duyệt. Trước (Mig 26 S19 LevelOpinionsSectionV2): forEach step → grid-cols-2 cho forEach Level × forEach Approver → 1 box / NV Hiển thị cả NV chưa duyệt với placeholder "— chưa duyệt" Sau (Chunk C): forEach step → 1 StepOpinionsBox (đại diện Phòng) Box body: filter opinions có stepOrder == step.order → sort theo levelOrder asc, signedAt asc → render StepOpinionEntry per signed opinion NV chưa duyệt KHÔNG hiển thị Header box: "Bước N — Tên · {dept badge} · X/Y đã duyệt" FE (mirror fe-admin + fe-user): - LevelOpinionsSectionV2 forEach step → StepOpinionsBox (replace grid-cols-2) - StepOpinionsBox: header phòng + body list signed opinions - StepOpinionEntry: tên NV + Cấp badge + Admin override badge nếu có + timestamp + comment - Drop LevelOpinionBox function (per-NV pattern bỏ) - KHÔNG đụng schema Mig 26 (PE Service ApproveV2Async UPSERT giữ 1 row / Level — chỉ FE re-group render) Verify: - npm run build × fe-admin pass · fe-user pass - Test pass mặc định skip (Phase 9 UAT iteration, Q4 user public luôn) Pending Chunk D: Docs S20 changelog + STATUS + HANDOFF Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 139 ++++++++++---------- fe-user/src/components/pe/PeDetailTabs.tsx | 139 ++++++++++---------- 2 files changed, 136 insertions(+), 142 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index bae82d4..81ca743 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/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')} -
- )}
) } 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')} -
- )}
) }