diff --git a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx index d2003d5..b3a98be 100644 --- a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -115,13 +115,25 @@ 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 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. + // Plan AG5 — Group 3-level Project > Năm > NCC > PE (anh feedback 2026-05-21: + // "Folder cấp dưới dự án là theo năm và dưới năm là theo NCC"). Filter pendingMe TRƯỚC group. + // Year extract từ createdAt.getFullYear(). NCC = selectedSupplierName fallback "(Chưa chọn NCC)" + // khi PE chưa DaDuyet. Sort: Project A-Z (vi) + Year DESC + NCC A-Z (vi) + PE createdAt DESC. + type SupplierGroup = { + supplierId: string | null + supplierName: string + items: PeListItem[] + } + type YearGroup = { + year: number + suppliers: SupplierGroup[] + totalCount: number + } type ProjectGroup = { projectId: string | null projectName: string - items: PeListItem[] + years: YearGroup[] + totalCount: number } const projectGroups = useMemo(() => { const filtered = pendingMe @@ -132,20 +144,42 @@ 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, items: [] }) + projectMap.set(projKey, { projectId: p.projectId ?? null, projectName: projName, years: [], totalCount: 0 }) } - projectMap.get(projKey)!.items.push(p) + const pg = projectMap.get(projKey)! + const year = new Date(p.createdAt).getFullYear() + let yg = pg.years.find(y => y.year === year) + if (!yg) { + yg = { year, suppliers: [], totalCount: 0 } + pg.years.push(yg) + } + const supKey = p.selectedSupplierId ?? '__no_supplier__' + const supName = p.selectedSupplierName?.trim() || '(Chưa chọn NCC)' + let sg = yg.suppliers.find(s => (s.supplierId ?? '__no_supplier__') === supKey) + if (!sg) { + sg = { supplierId: p.selectedSupplierId ?? null, supplierName: supName, items: [] } + yg.suppliers.push(sg) + } + sg.items.push(p) + yg.totalCount++ + pg.totalCount++ } const arr = Array.from(projectMap.values()) arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi')) for (const pg of arr) { - pg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + pg.years.sort((a, b) => b.year - a.year) + for (const yg of pg.years) { + yg.suppliers.sort((a, b) => a.supplierName.localeCompare(b.supplierName, 'vi')) + for (const sg of yg.suppliers) { + sg.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.items.length, 0) + const totalRowCount = projectGroups.reduce((sum, pg) => sum + pg.totalCount, 0) // Plan AG2 — Expand state localStorage Set (projectId only, drop ::gtKey suffix). // Default empty Set (all collapse). Single-PE project skip
wrapper (render flat). @@ -251,10 +285,10 @@ export function PurchaseEvaluationsListPage() { )} - {/* Plan AG3 — Tree view 1-level Project > PE consistent (anh feedback 2026-05-21: - "nếu có 1 thì cũng để tương tự luôn nhé, đừng để khác các thằng kia"). - Mọi dự án dù có 1 hay nhiều PE đều render
folder collapsed. - Tailwind v3 named group group/proj + [&::-webkit-details-marker]:hidden. */} + {/* Plan AG5 — Tree view 3-level Project > Năm > NCC > PE (anh feedback 2026-05-21: + "Folder cấp dưới dự án là theo năm và dưới năm là theo NCC"). + 3 layer
: 📁 Project (bg-slate-50) > 📅 Năm > 🏢 NCC > PE card. + Tailwind v3 named groups group/proj + group/year + group/sup cho chevron rotation. */}
{projectGroups.map(pg => { const projKey = pg.projectId ?? '__no_project__' @@ -269,65 +303,100 @@ export function PurchaseEvaluationsListPage() { 📁 {pg.projectName} - {pg.items.length} + {pg.totalCount} -
    - {pg.items.map(p => ( -
  • - +
  • + ))} +
+
+ ) + })} -
- - {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 d2003d5..b3a98be 100644 --- a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -115,13 +115,25 @@ 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 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. + // Plan AG5 — Group 3-level Project > Năm > NCC > PE (anh feedback 2026-05-21: + // "Folder cấp dưới dự án là theo năm và dưới năm là theo NCC"). Filter pendingMe TRƯỚC group. + // Year extract từ createdAt.getFullYear(). NCC = selectedSupplierName fallback "(Chưa chọn NCC)" + // khi PE chưa DaDuyet. Sort: Project A-Z (vi) + Year DESC + NCC A-Z (vi) + PE createdAt DESC. + type SupplierGroup = { + supplierId: string | null + supplierName: string + items: PeListItem[] + } + type YearGroup = { + year: number + suppliers: SupplierGroup[] + totalCount: number + } type ProjectGroup = { projectId: string | null projectName: string - items: PeListItem[] + years: YearGroup[] + totalCount: number } const projectGroups = useMemo(() => { const filtered = pendingMe @@ -132,20 +144,42 @@ 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, items: [] }) + projectMap.set(projKey, { projectId: p.projectId ?? null, projectName: projName, years: [], totalCount: 0 }) } - projectMap.get(projKey)!.items.push(p) + const pg = projectMap.get(projKey)! + const year = new Date(p.createdAt).getFullYear() + let yg = pg.years.find(y => y.year === year) + if (!yg) { + yg = { year, suppliers: [], totalCount: 0 } + pg.years.push(yg) + } + const supKey = p.selectedSupplierId ?? '__no_supplier__' + const supName = p.selectedSupplierName?.trim() || '(Chưa chọn NCC)' + let sg = yg.suppliers.find(s => (s.supplierId ?? '__no_supplier__') === supKey) + if (!sg) { + sg = { supplierId: p.selectedSupplierId ?? null, supplierName: supName, items: [] } + yg.suppliers.push(sg) + } + sg.items.push(p) + yg.totalCount++ + pg.totalCount++ } const arr = Array.from(projectMap.values()) arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi')) for (const pg of arr) { - pg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + pg.years.sort((a, b) => b.year - a.year) + for (const yg of pg.years) { + yg.suppliers.sort((a, b) => a.supplierName.localeCompare(b.supplierName, 'vi')) + for (const sg of yg.suppliers) { + sg.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.items.length, 0) + const totalRowCount = projectGroups.reduce((sum, pg) => sum + pg.totalCount, 0) // Plan AG2 — Expand state localStorage Set (projectId only, drop ::gtKey suffix). // Default empty Set (all collapse). Single-PE project skip
wrapper (render flat). @@ -251,10 +285,10 @@ export function PurchaseEvaluationsListPage() { )} - {/* Plan AG3 — Tree view 1-level Project > PE consistent (anh feedback 2026-05-21: - "nếu có 1 thì cũng để tương tự luôn nhé, đừng để khác các thằng kia"). - Mọi dự án dù có 1 hay nhiều PE đều render
folder collapsed. - Tailwind v3 named group group/proj + [&::-webkit-details-marker]:hidden. */} + {/* Plan AG5 — Tree view 3-level Project > Năm > NCC > PE (anh feedback 2026-05-21: + "Folder cấp dưới dự án là theo năm và dưới năm là theo NCC"). + 3 layer
: 📁 Project (bg-slate-50) > 📅 Năm > 🏢 NCC > PE card. + Tailwind v3 named groups group/proj + group/year + group/sup cho chevron rotation. */}
{projectGroups.map(pg => { const projKey = pg.projectId ?? '__no_project__' @@ -269,65 +303,100 @@ export function PurchaseEvaluationsListPage() { 📁 {pg.projectName} - {pg.items.length} + {pg.totalCount} -
    - {pg.items.map(p => ( -
  • - +
  • + ))} +
+
+ ) + })} -
- - {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Đ
- )} - - - ))} - +
+ ) + })} +
) })}