From c5429c0d10f7375d8c218bd82a7622ef626bbc4e Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 21 May 2026 18:24:59 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-User+FE-Admin:=20Plan=20AG2=20?= =?UTF-8?q?=E2=80=94=20Simplify=20PE=20List=20tree=201-level=20+=20Panel?= =?UTF-8?q?=201=20widen=20400px?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anh feedback Plan AG (2-level Project > Gói thầu > PE) cầu kỳ quá. Simplify xuống 1-level + widen panel cho dễ đọc. 3 changes: 1. Panel 1 widen 340px → 400px (lg:grid-cols-[400px_1fr_360px]) 2. Drop GoiThauGroup nested type + inner
tree, useMemo group 1-level Project > PE[]; PE sort by createdAt DESC trong group (mirror BE sort) 3. Smart render: single-PE project → flat card (no
wrapper, project name UPPERCASE label inline) / multi-PE project →
tree expand 4. localStorage key rename 'pe_list_expanded_projects' (drop ::gtKey composite suffix) UAT visual: dự án solo PE hiện flat (không cần click expand), dự án có nhiều phiếu render tree compact. Drop redundant projectName ở PE card (đã có ở group header / UPPERCASE label). Verify: - npm build fe-user PASS 0 TS err 1291.76 KB (gzip 336.90 KB) 1907 modules - npm build fe-admin PASS 0 TS err 1403.10 KB (gzip 357.41 KB) 1926 modules - 2 file SHA256 IDENTICAL 37520D01... (mirror §3.9) - KHÔNG BE change, KHÔNG Mig, KHÔNG test (UAT mode per feedback_uat_skip_verify) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pages/pe/PurchaseEvaluationsListPage.tsx | 260 ++++++++++-------- .../pages/pe/PurchaseEvaluationsListPage.tsx | 260 ++++++++++-------- 2 files changed, 276 insertions(+), 244 deletions(-) diff --git a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx index c086933..000b5e5 100644 --- a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -115,21 +115,13 @@ export function PurchaseEvaluationsListPage() { // Đã duyệt/Từ chối loại bỏ). BE /inbox hiện loose UAT có thể trả phiếu Nháp → // FE filter để UX đúng kỳ vọng. Phân quyền strict V2 BE pending Session 18+. - // Plan AG Chunk A — Data transform useMemo group nested 2-level Project > Gói thầu - // Filter (pendingMe → DaGuiDuyet) áp dụng TRƯỚC group để empty state đúng. - // Normalize TenGoiThau: trim + toLowerCase làm group key, display raw đầu tiên. - // Fallback: "(Dự án đã xoá)" empty projectName + "(Chưa phân loại)" empty TenGoiThau. - // Sort vi locale A-Z cả 2 cấp. - type GoiThauGroup = { - displayName: string - normalizedKey: string - items: PeListItem[] - } + // Plan AG2 — Group 1-level Project > PE (drop tầng gói thầu per anh feedback 2026-05-21: + // "gói thầu thì ko cần thiết phải treedow"). Filter (pendingMe → DaGuiDuyet) TRƯỚC group. + // Fallback "(Dự án đã xoá)" empty projectName. Sort vi locale Project A-Z + PE createdAt DESC. type ProjectGroup = { projectId: string | null projectName: string - goiThauList: GoiThauGroup[] - totalCount: number + items: PeListItem[] } const projectGroups = useMemo(() => { const filtered = pendingMe @@ -140,35 +132,24 @@ export function PurchaseEvaluationsListPage() { const projKey = p.projectId ?? '__no_project__' const projName = p.projectName?.trim() || '(Dự án đã xoá)' if (!projectMap.has(projKey)) { - projectMap.set(projKey, { projectId: p.projectId ?? null, projectName: projName, goiThauList: [], totalCount: 0 }) + projectMap.set(projKey, { projectId: p.projectId ?? null, projectName: projName, items: [] }) } - const pg = projectMap.get(projKey)! - const rawGT = (p.tenGoiThau ?? '').trim() - const normKey = rawGT.toLowerCase() || '__no_goithau__' - const displayGT = rawGT || '(Chưa phân loại)' - let gt = pg.goiThauList.find(g => g.normalizedKey === normKey) - if (!gt) { - gt = { displayName: displayGT, normalizedKey: normKey, items: [] } - pg.goiThauList.push(gt) - } - gt.items.push(p) - pg.totalCount++ + projectMap.get(projKey)!.items.push(p) } const arr = Array.from(projectMap.values()) arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi')) for (const pg of arr) { - pg.goiThauList.sort((a, b) => a.displayName.localeCompare(b.displayName, 'vi')) + pg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) } return arr }, [allRows, pendingMe]) // Total row count cho header badge (pendingMe đếm filtered, Danh sách đếm BE total). - const totalRowCount = projectGroups.reduce((sum, pg) => sum + pg.totalCount, 0) + const totalRowCount = projectGroups.reduce((sum, pg) => sum + pg.items.length, 0) - // Plan AG Chunk C — Expand state localStorage persist Set - // Default empty Set (all collapse) — bro Tra Sol expect Outlook-style closed default. - // Project key: projectId or '__no_project__'; Gói thầu key: `${projectId}::${normalizedGoiThau}`. - const STORAGE_KEY = 'pe_list_expanded_groups' + // Plan AG2 — Expand state localStorage Set (projectId only, drop ::gtKey suffix). + // Default empty Set (all collapse). Single-PE project skip
wrapper (render flat). + const STORAGE_KEY = 'pe_list_expanded_projects' const [expandedSet, setExpandedSet] = useState>(() => { try { const raw = localStorage.getItem(STORAGE_KEY) @@ -204,7 +185,7 @@ export function PurchaseEvaluationsListPage() { -
+
{/* Panel 1: List */}
+
+ + {PurchaseEvaluationTypeLabel[p.type]} + + {/* S23 t2 UAT: BE list sort theo UpdatedAt DESC (fallback CreatedAt) — + phiếu vừa update (Tạo / Gửi duyệt / Trả lại) đưa lên đầu list. */} + + {new Date(p.createdAt).toLocaleString('vi-VN', { + day: '2-digit', month: '2-digit', year: 'numeric', + hour: '2-digit', minute: '2-digit', + })} + +
+ {p.contractId && ( +
✓ Đã tạo HĐ
+ )} + + + ))} + +
+ ) + })} diff --git a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx index c086933..000b5e5 100644 --- a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -115,21 +115,13 @@ export function PurchaseEvaluationsListPage() { // Đã duyệt/Từ chối loại bỏ). BE /inbox hiện loose UAT có thể trả phiếu Nháp → // FE filter để UX đúng kỳ vọng. Phân quyền strict V2 BE pending Session 18+. - // Plan AG Chunk A — Data transform useMemo group nested 2-level Project > Gói thầu - // Filter (pendingMe → DaGuiDuyet) áp dụng TRƯỚC group để empty state đúng. - // Normalize TenGoiThau: trim + toLowerCase làm group key, display raw đầu tiên. - // Fallback: "(Dự án đã xoá)" empty projectName + "(Chưa phân loại)" empty TenGoiThau. - // Sort vi locale A-Z cả 2 cấp. - type GoiThauGroup = { - displayName: string - normalizedKey: string - items: PeListItem[] - } + // Plan AG2 — Group 1-level Project > PE (drop tầng gói thầu per anh feedback 2026-05-21: + // "gói thầu thì ko cần thiết phải treedow"). Filter (pendingMe → DaGuiDuyet) TRƯỚC group. + // Fallback "(Dự án đã xoá)" empty projectName. Sort vi locale Project A-Z + PE createdAt DESC. type ProjectGroup = { projectId: string | null projectName: string - goiThauList: GoiThauGroup[] - totalCount: number + items: PeListItem[] } const projectGroups = useMemo(() => { const filtered = pendingMe @@ -140,35 +132,24 @@ export function PurchaseEvaluationsListPage() { const projKey = p.projectId ?? '__no_project__' const projName = p.projectName?.trim() || '(Dự án đã xoá)' if (!projectMap.has(projKey)) { - projectMap.set(projKey, { projectId: p.projectId ?? null, projectName: projName, goiThauList: [], totalCount: 0 }) + projectMap.set(projKey, { projectId: p.projectId ?? null, projectName: projName, items: [] }) } - const pg = projectMap.get(projKey)! - const rawGT = (p.tenGoiThau ?? '').trim() - const normKey = rawGT.toLowerCase() || '__no_goithau__' - const displayGT = rawGT || '(Chưa phân loại)' - let gt = pg.goiThauList.find(g => g.normalizedKey === normKey) - if (!gt) { - gt = { displayName: displayGT, normalizedKey: normKey, items: [] } - pg.goiThauList.push(gt) - } - gt.items.push(p) - pg.totalCount++ + projectMap.get(projKey)!.items.push(p) } const arr = Array.from(projectMap.values()) arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi')) for (const pg of arr) { - pg.goiThauList.sort((a, b) => a.displayName.localeCompare(b.displayName, 'vi')) + pg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) } return arr }, [allRows, pendingMe]) // Total row count cho header badge (pendingMe đếm filtered, Danh sách đếm BE total). - const totalRowCount = projectGroups.reduce((sum, pg) => sum + pg.totalCount, 0) + const totalRowCount = projectGroups.reduce((sum, pg) => sum + pg.items.length, 0) - // Plan AG Chunk C — Expand state localStorage persist Set - // Default empty Set (all collapse) — bro Tra Sol expect Outlook-style closed default. - // Project key: projectId or '__no_project__'; Gói thầu key: `${projectId}::${normalizedGoiThau}`. - const STORAGE_KEY = 'pe_list_expanded_groups' + // Plan AG2 — Expand state localStorage Set (projectId only, drop ::gtKey suffix). + // Default empty Set (all collapse). Single-PE project skip
wrapper (render flat). + const STORAGE_KEY = 'pe_list_expanded_projects' const [expandedSet, setExpandedSet] = useState>(() => { try { const raw = localStorage.getItem(STORAGE_KEY) @@ -204,7 +185,7 @@ export function PurchaseEvaluationsListPage() { -
+
{/* Panel 1: List */}
+
+ + {PurchaseEvaluationTypeLabel[p.type]} + + {/* S23 t2 UAT: BE list sort theo UpdatedAt DESC (fallback CreatedAt) — + phiếu vừa update (Tạo / Gửi duyệt / Trả lại) đưa lên đầu list. */} + + {new Date(p.createdAt).toLocaleString('vi-VN', { + day: '2-digit', month: '2-digit', year: 'numeric', + hour: '2-digit', minute: '2-digit', + })} + +
+ {p.contractId && ( +
✓ Đã tạo HĐ
+ )} + + + ))} + +
+ ) + })}