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Đ
+ )} + + + ))} + +
+ ) + })}