diff --git a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx index 1202a77..95b0113 100644 --- a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -115,44 +115,45 @@ 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+. - // S59 — anh Kiệt FDC (Zalo 2026-06-11): "Tên dự án ( 2026 ) -> Hạng mục công việc -> Phiếu cần duyệt" - // (mirror cấu trúc folder Outlook FDC, thứ tự label "Dự án - Năm"). Thay tree AG5 cũ (Project > Năm > NCC): - // mỗi cặp (Dự án, Năm-tạo-phiếu) = 1 folder label "Tên dự án (Năm)", level 2 = Hạng mục công việc - // (WorkItemId Mig 49, phiếu cũ chưa gắn → "(Chưa gắn hạng mục)"). NCC bỏ khỏi tree — vẫn hiện ở card/detail. - // Filter pendingMe TRƯỚC group. Sort: Project A-Z (vi) + Năm DESC + Hạng mục A-Z (vi) + PE createdAt DESC. + // S59 — anh Kiệt FDC (Zalo 2026-06-11) + anh chốt follow-up: tree 4 tầng + // "Năm (tạo phiếu) > Dự án > Hạng mục công việc > Phiếu cần duyệt". + // Hạng mục từ WorkItemId Mig 49, phiếu cũ chưa gắn → "(Chưa gắn hạng mục)". + // NCC bỏ khỏi tree — vẫn hiện ở card/detail. Filter pendingMe TRƯỚC group. + // Sort: Năm DESC + Dự án A-Z (vi) + Hạng mục A-Z (vi) + PE createdAt DESC. type WorkItemGroup = { workItemId: string | null workItemName: string items: PeListItem[] } - type ProjectYearGroup = { + type ProjectGroup = { projectId: string | null projectName: string - year: number - label: string workItems: WorkItemGroup[] totalCount: number } - const projectGroups = useMemo(() => { + type YearGroup = { + year: number + projects: ProjectGroup[] + totalCount: number + } + const yearGroups = useMemo(() => { const filtered = pendingMe ? allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet) : allRows - const groupMap = new Map() + const yearMap = new Map() for (const p of filtered) { const year = new Date(p.createdAt).getFullYear() - const projName = p.projectName?.trim() || '(Dự án đã xoá)' - const groupKey = `${p.projectId ?? '__no_project__'}::y${year}` - if (!groupMap.has(groupKey)) { - groupMap.set(groupKey, { - projectId: p.projectId ?? null, - projectName: projName, - year, - label: `${projName} (${year})`, - workItems: [], - totalCount: 0, - }) + if (!yearMap.has(year)) { + yearMap.set(year, { year, projects: [], totalCount: 0 }) + } + const yg = yearMap.get(year)! + const projKey = p.projectId ?? '__no_project__' + const projName = p.projectName?.trim() || '(Dự án đã xoá)' + let pg = yg.projects.find(g => (g.projectId ?? '__no_project__') === projKey) + if (!pg) { + pg = { projectId: p.projectId ?? null, projectName: projName, workItems: [], totalCount: 0 } + yg.projects.push(pg) } - const pg = groupMap.get(groupKey)! const wiKey = p.workItemId ?? '__no_workitem__' const wiName = p.workItemName?.trim() || '(Chưa gắn hạng mục)' let wg = pg.workItems.find(w => (w.workItemId ?? '__no_workitem__') === wiKey) @@ -162,24 +163,28 @@ export function PurchaseEvaluationsListPage() { } wg.items.push(p) pg.totalCount++ + yg.totalCount++ } - const arr = Array.from(groupMap.values()) - arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi') || b.year - a.year) - for (const pg of arr) { - pg.workItems.sort((a, b) => a.workItemName.localeCompare(b.workItemName, 'vi')) - for (const wg of pg.workItems) { - wg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + const arr = Array.from(yearMap.values()) + arr.sort((a, b) => b.year - a.year) + for (const yg of arr) { + yg.projects.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi')) + for (const pg of yg.projects) { + pg.workItems.sort((a, b) => a.workItemName.localeCompare(b.workItemName, 'vi')) + for (const wg of pg.workItems) { + wg.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 = yearGroups.reduce((sum, yg) => sum + yg.totalCount, 0) // Plan AG2 — Expand state localStorage Set. Default empty Set (all collapse). - // S59 key v2: node scheme đổi (Dự án+Năm gộp 1 node + Hạng mục thay NCC) → key cũ vô nghĩa. - const STORAGE_KEY = 'pe_list_expanded_projects_v2' + // S59 key v3: node scheme chốt "Năm > Dự án > Hạng mục" → key v1/v2 vô nghĩa. + const STORAGE_KEY = 'pe_list_expanded_projects_v3' const [expandedSet, setExpandedSet] = useState>(() => { try { const raw = localStorage.getItem(STORAGE_KEY) @@ -276,92 +281,112 @@ export function PurchaseEvaluationsListPage() { ))} )} - {!list.isLoading && projectGroups.length === 0 && ( + {!list.isLoading && yearGroups.length === 0 && (
)} - {/* S59 — Tree view theo anh Kiệt FDC: 📁 "Tên dự án (Năm)" (bg-slate-50) > 🧱 Hạng mục - công việc > PE card. 2 layer
, named groups group/proj + group/wi cho chevron. */} + {/* S59 — Tree view chốt: 📅 Năm (bg-slate-50) > 📁 Dự án > 🧱 Hạng mục công việc > PE card. + 3 layer
, named groups group/year + group/proj + group/wi cho chevron rotation. */}
- {projectGroups.map(pg => { - const projKey = `${pg.projectId ?? '__no_project__'}::y${pg.year}` + {yearGroups.map(yg => { + const yearKey = `y${yg.year}` return (
toggleExpand(projKey, (e.currentTarget as HTMLDetailsElement).open)} - className="group/proj" + key={yearKey} + open={isExpanded(yearKey)} + onToggle={e => toggleExpand(yearKey, (e.currentTarget as HTMLDetailsElement).open)} + className="group/year" > - - 📁 - {pg.label} - {pg.totalCount} + + 📅 + Năm {yg.year} + {yg.totalCount}
- {pg.workItems.map(wg => { - const wiKey = `${projKey}::w${wg.workItemId ?? '_none_'}` + {yg.projects.map(pg => { + const projKey = `${yearKey}::p${pg.projectId ?? '_none_'}` return (
toggleExpand(wiKey, (e.currentTarget as HTMLDetailsElement).open)} - className="group/wi" + key={projKey} + open={isExpanded(projKey)} + onToggle={e => toggleExpand(projKey, (e.currentTarget as HTMLDetailsElement).open)} + className="group/proj" > - - 🧱 - {wg.workItemName} - {wg.items.length} + + 📁 + {pg.projectName} + {pg.totalCount} -
    - {wg.items.map(p => ( -
  • - -
  • - ))} -
+ + + 🧱 + {wg.workItemName} + {wg.items.length} + +
    + {wg.items.map(p => ( +
  • + +
  • + ))} +
+
+ ) + })} +
) })} diff --git a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx index 1202a77..95b0113 100644 --- a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -115,44 +115,45 @@ 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+. - // S59 — anh Kiệt FDC (Zalo 2026-06-11): "Tên dự án ( 2026 ) -> Hạng mục công việc -> Phiếu cần duyệt" - // (mirror cấu trúc folder Outlook FDC, thứ tự label "Dự án - Năm"). Thay tree AG5 cũ (Project > Năm > NCC): - // mỗi cặp (Dự án, Năm-tạo-phiếu) = 1 folder label "Tên dự án (Năm)", level 2 = Hạng mục công việc - // (WorkItemId Mig 49, phiếu cũ chưa gắn → "(Chưa gắn hạng mục)"). NCC bỏ khỏi tree — vẫn hiện ở card/detail. - // Filter pendingMe TRƯỚC group. Sort: Project A-Z (vi) + Năm DESC + Hạng mục A-Z (vi) + PE createdAt DESC. + // S59 — anh Kiệt FDC (Zalo 2026-06-11) + anh chốt follow-up: tree 4 tầng + // "Năm (tạo phiếu) > Dự án > Hạng mục công việc > Phiếu cần duyệt". + // Hạng mục từ WorkItemId Mig 49, phiếu cũ chưa gắn → "(Chưa gắn hạng mục)". + // NCC bỏ khỏi tree — vẫn hiện ở card/detail. Filter pendingMe TRƯỚC group. + // Sort: Năm DESC + Dự án A-Z (vi) + Hạng mục A-Z (vi) + PE createdAt DESC. type WorkItemGroup = { workItemId: string | null workItemName: string items: PeListItem[] } - type ProjectYearGroup = { + type ProjectGroup = { projectId: string | null projectName: string - year: number - label: string workItems: WorkItemGroup[] totalCount: number } - const projectGroups = useMemo(() => { + type YearGroup = { + year: number + projects: ProjectGroup[] + totalCount: number + } + const yearGroups = useMemo(() => { const filtered = pendingMe ? allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet) : allRows - const groupMap = new Map() + const yearMap = new Map() for (const p of filtered) { const year = new Date(p.createdAt).getFullYear() - const projName = p.projectName?.trim() || '(Dự án đã xoá)' - const groupKey = `${p.projectId ?? '__no_project__'}::y${year}` - if (!groupMap.has(groupKey)) { - groupMap.set(groupKey, { - projectId: p.projectId ?? null, - projectName: projName, - year, - label: `${projName} (${year})`, - workItems: [], - totalCount: 0, - }) + if (!yearMap.has(year)) { + yearMap.set(year, { year, projects: [], totalCount: 0 }) + } + const yg = yearMap.get(year)! + const projKey = p.projectId ?? '__no_project__' + const projName = p.projectName?.trim() || '(Dự án đã xoá)' + let pg = yg.projects.find(g => (g.projectId ?? '__no_project__') === projKey) + if (!pg) { + pg = { projectId: p.projectId ?? null, projectName: projName, workItems: [], totalCount: 0 } + yg.projects.push(pg) } - const pg = groupMap.get(groupKey)! const wiKey = p.workItemId ?? '__no_workitem__' const wiName = p.workItemName?.trim() || '(Chưa gắn hạng mục)' let wg = pg.workItems.find(w => (w.workItemId ?? '__no_workitem__') === wiKey) @@ -162,24 +163,28 @@ export function PurchaseEvaluationsListPage() { } wg.items.push(p) pg.totalCount++ + yg.totalCount++ } - const arr = Array.from(groupMap.values()) - arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi') || b.year - a.year) - for (const pg of arr) { - pg.workItems.sort((a, b) => a.workItemName.localeCompare(b.workItemName, 'vi')) - for (const wg of pg.workItems) { - wg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + const arr = Array.from(yearMap.values()) + arr.sort((a, b) => b.year - a.year) + for (const yg of arr) { + yg.projects.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi')) + for (const pg of yg.projects) { + pg.workItems.sort((a, b) => a.workItemName.localeCompare(b.workItemName, 'vi')) + for (const wg of pg.workItems) { + wg.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 = yearGroups.reduce((sum, yg) => sum + yg.totalCount, 0) // Plan AG2 — Expand state localStorage Set. Default empty Set (all collapse). - // S59 key v2: node scheme đổi (Dự án+Năm gộp 1 node + Hạng mục thay NCC) → key cũ vô nghĩa. - const STORAGE_KEY = 'pe_list_expanded_projects_v2' + // S59 key v3: node scheme chốt "Năm > Dự án > Hạng mục" → key v1/v2 vô nghĩa. + const STORAGE_KEY = 'pe_list_expanded_projects_v3' const [expandedSet, setExpandedSet] = useState>(() => { try { const raw = localStorage.getItem(STORAGE_KEY) @@ -276,92 +281,112 @@ export function PurchaseEvaluationsListPage() { ))}
)} - {!list.isLoading && projectGroups.length === 0 && ( + {!list.isLoading && yearGroups.length === 0 && (
)} - {/* S59 — Tree view theo anh Kiệt FDC: 📁 "Tên dự án (Năm)" (bg-slate-50) > 🧱 Hạng mục - công việc > PE card. 2 layer
, named groups group/proj + group/wi cho chevron. */} + {/* S59 — Tree view chốt: 📅 Năm (bg-slate-50) > 📁 Dự án > 🧱 Hạng mục công việc > PE card. + 3 layer
, named groups group/year + group/proj + group/wi cho chevron rotation. */}
- {projectGroups.map(pg => { - const projKey = `${pg.projectId ?? '__no_project__'}::y${pg.year}` + {yearGroups.map(yg => { + const yearKey = `y${yg.year}` return (
toggleExpand(projKey, (e.currentTarget as HTMLDetailsElement).open)} - className="group/proj" + key={yearKey} + open={isExpanded(yearKey)} + onToggle={e => toggleExpand(yearKey, (e.currentTarget as HTMLDetailsElement).open)} + className="group/year" > - - 📁 - {pg.label} - {pg.totalCount} + + 📅 + Năm {yg.year} + {yg.totalCount}
- {pg.workItems.map(wg => { - const wiKey = `${projKey}::w${wg.workItemId ?? '_none_'}` + {yg.projects.map(pg => { + const projKey = `${yearKey}::p${pg.projectId ?? '_none_'}` return (
toggleExpand(wiKey, (e.currentTarget as HTMLDetailsElement).open)} - className="group/wi" + key={projKey} + open={isExpanded(projKey)} + onToggle={e => toggleExpand(projKey, (e.currentTarget as HTMLDetailsElement).open)} + className="group/proj" > - - 🧱 - {wg.workItemName} - {wg.items.length} + + 📁 + {pg.projectName} + {pg.totalCount} -
    - {wg.items.map(p => ( -
  • - -
  • - ))} -
+ + + 🧱 + {wg.workItemName} + {wg.items.length} + +
    + {wg.items.map(p => ( +
  • + +
  • + ))} +
+
+ ) + })} +
) })}