[CLAUDE] PurchaseEvaluation: tree Panel 1 chot 4 tang "Nam > Du an > Hang muc cong viec > Phieu" (anh chot follow-up, SHA256 mirror x2 app)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m51s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m51s
- Doi tu "Du an (Nam)" gop 1 node -> Nam la tang ngoai cung (DESC, bg-slate-50), trong Nam chua Du an (A-Z vi), trong Du an chua Hang muc cong viec, roi phieu. - yearGroups useMemo thay projectGroups + expand-state localStorage key v3. - Build x2 PASS, SHA256 mirror identical 95D524EE.
This commit is contained in:
@ -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 →
|
// Đã 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+.
|
// 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"
|
// S59 — anh Kiệt FDC (Zalo 2026-06-11) + anh chốt follow-up: tree 4 tầng
|
||||||
// (mirror cấu trúc folder Outlook FDC, thứ tự label "Dự án - Năm"). Thay tree AG5 cũ (Project > Năm > NCC):
|
// "Năm (tạo phiếu) > Dự án > Hạng mục công việc > Phiếu cần duyệt".
|
||||||
// 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
|
// Hạng mục từ WorkItemId Mig 49, phiếu cũ chưa gắn → "(Chưa gắn hạng mụ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.
|
// NCC bỏ khỏi tree — vẫn hiện ở card/detail. Filter pendingMe TRƯỚC group.
|
||||||
// Filter pendingMe TRƯỚC group. Sort: Project A-Z (vi) + Năm DESC + Hạng mục A-Z (vi) + PE createdAt DESC.
|
// Sort: Năm DESC + Dự án A-Z (vi) + Hạng mục A-Z (vi) + PE createdAt DESC.
|
||||||
type WorkItemGroup = {
|
type WorkItemGroup = {
|
||||||
workItemId: string | null
|
workItemId: string | null
|
||||||
workItemName: string
|
workItemName: string
|
||||||
items: PeListItem[]
|
items: PeListItem[]
|
||||||
}
|
}
|
||||||
type ProjectYearGroup = {
|
type ProjectGroup = {
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
projectName: string
|
projectName: string
|
||||||
year: number
|
|
||||||
label: string
|
|
||||||
workItems: WorkItemGroup[]
|
workItems: WorkItemGroup[]
|
||||||
totalCount: number
|
totalCount: number
|
||||||
}
|
}
|
||||||
const projectGroups = useMemo<ProjectYearGroup[]>(() => {
|
type YearGroup = {
|
||||||
|
year: number
|
||||||
|
projects: ProjectGroup[]
|
||||||
|
totalCount: number
|
||||||
|
}
|
||||||
|
const yearGroups = useMemo<YearGroup[]>(() => {
|
||||||
const filtered = pendingMe
|
const filtered = pendingMe
|
||||||
? allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet)
|
? allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet)
|
||||||
: allRows
|
: allRows
|
||||||
const groupMap = new Map<string, ProjectYearGroup>()
|
const yearMap = new Map<number, YearGroup>()
|
||||||
for (const p of filtered) {
|
for (const p of filtered) {
|
||||||
const year = new Date(p.createdAt).getFullYear()
|
const year = new Date(p.createdAt).getFullYear()
|
||||||
const projName = p.projectName?.trim() || '(Dự án đã xoá)'
|
if (!yearMap.has(year)) {
|
||||||
const groupKey = `${p.projectId ?? '__no_project__'}::y${year}`
|
yearMap.set(year, { year, projects: [], totalCount: 0 })
|
||||||
if (!groupMap.has(groupKey)) {
|
}
|
||||||
groupMap.set(groupKey, {
|
const yg = yearMap.get(year)!
|
||||||
projectId: p.projectId ?? null,
|
const projKey = p.projectId ?? '__no_project__'
|
||||||
projectName: projName,
|
const projName = p.projectName?.trim() || '(Dự án đã xoá)'
|
||||||
year,
|
let pg = yg.projects.find(g => (g.projectId ?? '__no_project__') === projKey)
|
||||||
label: `${projName} (${year})`,
|
if (!pg) {
|
||||||
workItems: [],
|
pg = { projectId: p.projectId ?? null, projectName: projName, workItems: [], totalCount: 0 }
|
||||||
totalCount: 0,
|
yg.projects.push(pg)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const pg = groupMap.get(groupKey)!
|
|
||||||
const wiKey = p.workItemId ?? '__no_workitem__'
|
const wiKey = p.workItemId ?? '__no_workitem__'
|
||||||
const wiName = p.workItemName?.trim() || '(Chưa gắn hạng mục)'
|
const wiName = p.workItemName?.trim() || '(Chưa gắn hạng mục)'
|
||||||
let wg = pg.workItems.find(w => (w.workItemId ?? '__no_workitem__') === wiKey)
|
let wg = pg.workItems.find(w => (w.workItemId ?? '__no_workitem__') === wiKey)
|
||||||
@ -162,24 +163,28 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
}
|
}
|
||||||
wg.items.push(p)
|
wg.items.push(p)
|
||||||
pg.totalCount++
|
pg.totalCount++
|
||||||
|
yg.totalCount++
|
||||||
}
|
}
|
||||||
const arr = Array.from(groupMap.values())
|
const arr = Array.from(yearMap.values())
|
||||||
arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi') || b.year - a.year)
|
arr.sort((a, b) => b.year - a.year)
|
||||||
for (const pg of arr) {
|
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'))
|
pg.workItems.sort((a, b) => a.workItemName.localeCompare(b.workItemName, 'vi'))
|
||||||
for (const wg of pg.workItems) {
|
for (const wg of pg.workItems) {
|
||||||
wg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
wg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return arr
|
return arr
|
||||||
}, [allRows, pendingMe])
|
}, [allRows, pendingMe])
|
||||||
|
|
||||||
// Total row count cho header badge (pendingMe đếm filtered, Danh sách đếm BE total).
|
// 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<string>. Default empty Set (all collapse).
|
// Plan AG2 — Expand state localStorage Set<string>. 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.
|
// 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_v2'
|
const STORAGE_KEY = 'pe_list_expanded_projects_v3'
|
||||||
const [expandedSet, setExpandedSet] = useState<Set<string>>(() => {
|
const [expandedSet, setExpandedSet] = useState<Set<string>>(() => {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
@ -276,16 +281,32 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!list.isLoading && projectGroups.length === 0 && (
|
{!list.isLoading && yearGroups.length === 0 && (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<EmptyState icon={ClipboardCheck} title="Chưa có phiếu" description="Tạo phiếu mới để bắt đầu quy trình." />
|
<EmptyState icon={ClipboardCheck} title="Chưa có phiếu" description="Tạo phiếu mới để bắt đầu quy trình." />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* S59 — Tree view theo anh Kiệt FDC: 📁 "Tên dự án (Năm)" (bg-slate-50) > 🧱 Hạng mục
|
{/* S59 — Tree view chốt: 📅 Năm (bg-slate-50) > 📁 Dự án > 🧱 Hạng mục công việc > PE card.
|
||||||
công việc > PE card. 2 layer <details>, named groups group/proj + group/wi cho chevron. */}
|
3 layer <details>, named groups group/year + group/proj + group/wi cho chevron rotation. */}
|
||||||
<div className="divide-y divide-slate-100">
|
<div className="divide-y divide-slate-100">
|
||||||
{projectGroups.map(pg => {
|
{yearGroups.map(yg => {
|
||||||
const projKey = `${pg.projectId ?? '__no_project__'}::y${pg.year}`
|
const yearKey = `y${yg.year}`
|
||||||
|
return (
|
||||||
|
<details
|
||||||
|
key={yearKey}
|
||||||
|
open={isExpanded(yearKey)}
|
||||||
|
onToggle={e => toggleExpand(yearKey, (e.currentTarget as HTMLDetailsElement).open)}
|
||||||
|
className="group/year"
|
||||||
|
>
|
||||||
|
<summary className="flex cursor-pointer items-center gap-1.5 bg-slate-50 px-3 py-2 hover:bg-slate-100 [&::-webkit-details-marker]:hidden">
|
||||||
|
<svg className="h-3 w-3 shrink-0 text-slate-500 transition-transform group-open/year:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||||
|
<span className="text-base">📅</span>
|
||||||
|
<span className="flex-1 truncate text-[13px] font-semibold text-slate-900">Năm {yg.year}</span>
|
||||||
|
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{yg.totalCount}</span>
|
||||||
|
</summary>
|
||||||
|
<div className="ml-3 border-l border-slate-200">
|
||||||
|
{yg.projects.map(pg => {
|
||||||
|
const projKey = `${yearKey}::p${pg.projectId ?? '_none_'}`
|
||||||
return (
|
return (
|
||||||
<details
|
<details
|
||||||
key={projKey}
|
key={projKey}
|
||||||
@ -293,11 +314,11 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
onToggle={e => toggleExpand(projKey, (e.currentTarget as HTMLDetailsElement).open)}
|
onToggle={e => toggleExpand(projKey, (e.currentTarget as HTMLDetailsElement).open)}
|
||||||
className="group/proj"
|
className="group/proj"
|
||||||
>
|
>
|
||||||
<summary className="flex cursor-pointer items-center gap-1.5 bg-slate-50 px-3 py-2 hover:bg-slate-100 [&::-webkit-details-marker]:hidden">
|
<summary className="flex cursor-pointer items-center gap-1.5 px-3 py-1.5 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
|
||||||
<svg className="h-3 w-3 shrink-0 text-slate-500 transition-transform group-open/proj:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
<svg className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/proj:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||||
<span className="text-base">📁</span>
|
<span className="text-sm">📁</span>
|
||||||
<span className="flex-1 truncate text-[13px] font-medium text-slate-900">{pg.label}</span>
|
<span className="flex-1 truncate text-[12px] font-medium text-slate-700">{pg.projectName}</span>
|
||||||
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{pg.totalCount}</span>
|
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{pg.totalCount}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="ml-3 border-l border-slate-200">
|
<div className="ml-3 border-l border-slate-200">
|
||||||
{pg.workItems.map(wg => {
|
{pg.workItems.map(wg => {
|
||||||
@ -370,6 +391,10 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
@ -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 →
|
// Đã 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+.
|
// 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"
|
// S59 — anh Kiệt FDC (Zalo 2026-06-11) + anh chốt follow-up: tree 4 tầng
|
||||||
// (mirror cấu trúc folder Outlook FDC, thứ tự label "Dự án - Năm"). Thay tree AG5 cũ (Project > Năm > NCC):
|
// "Năm (tạo phiếu) > Dự án > Hạng mục công việc > Phiếu cần duyệt".
|
||||||
// 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
|
// Hạng mục từ WorkItemId Mig 49, phiếu cũ chưa gắn → "(Chưa gắn hạng mụ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.
|
// NCC bỏ khỏi tree — vẫn hiện ở card/detail. Filter pendingMe TRƯỚC group.
|
||||||
// Filter pendingMe TRƯỚC group. Sort: Project A-Z (vi) + Năm DESC + Hạng mục A-Z (vi) + PE createdAt DESC.
|
// Sort: Năm DESC + Dự án A-Z (vi) + Hạng mục A-Z (vi) + PE createdAt DESC.
|
||||||
type WorkItemGroup = {
|
type WorkItemGroup = {
|
||||||
workItemId: string | null
|
workItemId: string | null
|
||||||
workItemName: string
|
workItemName: string
|
||||||
items: PeListItem[]
|
items: PeListItem[]
|
||||||
}
|
}
|
||||||
type ProjectYearGroup = {
|
type ProjectGroup = {
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
projectName: string
|
projectName: string
|
||||||
year: number
|
|
||||||
label: string
|
|
||||||
workItems: WorkItemGroup[]
|
workItems: WorkItemGroup[]
|
||||||
totalCount: number
|
totalCount: number
|
||||||
}
|
}
|
||||||
const projectGroups = useMemo<ProjectYearGroup[]>(() => {
|
type YearGroup = {
|
||||||
|
year: number
|
||||||
|
projects: ProjectGroup[]
|
||||||
|
totalCount: number
|
||||||
|
}
|
||||||
|
const yearGroups = useMemo<YearGroup[]>(() => {
|
||||||
const filtered = pendingMe
|
const filtered = pendingMe
|
||||||
? allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet)
|
? allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet)
|
||||||
: allRows
|
: allRows
|
||||||
const groupMap = new Map<string, ProjectYearGroup>()
|
const yearMap = new Map<number, YearGroup>()
|
||||||
for (const p of filtered) {
|
for (const p of filtered) {
|
||||||
const year = new Date(p.createdAt).getFullYear()
|
const year = new Date(p.createdAt).getFullYear()
|
||||||
const projName = p.projectName?.trim() || '(Dự án đã xoá)'
|
if (!yearMap.has(year)) {
|
||||||
const groupKey = `${p.projectId ?? '__no_project__'}::y${year}`
|
yearMap.set(year, { year, projects: [], totalCount: 0 })
|
||||||
if (!groupMap.has(groupKey)) {
|
}
|
||||||
groupMap.set(groupKey, {
|
const yg = yearMap.get(year)!
|
||||||
projectId: p.projectId ?? null,
|
const projKey = p.projectId ?? '__no_project__'
|
||||||
projectName: projName,
|
const projName = p.projectName?.trim() || '(Dự án đã xoá)'
|
||||||
year,
|
let pg = yg.projects.find(g => (g.projectId ?? '__no_project__') === projKey)
|
||||||
label: `${projName} (${year})`,
|
if (!pg) {
|
||||||
workItems: [],
|
pg = { projectId: p.projectId ?? null, projectName: projName, workItems: [], totalCount: 0 }
|
||||||
totalCount: 0,
|
yg.projects.push(pg)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const pg = groupMap.get(groupKey)!
|
|
||||||
const wiKey = p.workItemId ?? '__no_workitem__'
|
const wiKey = p.workItemId ?? '__no_workitem__'
|
||||||
const wiName = p.workItemName?.trim() || '(Chưa gắn hạng mục)'
|
const wiName = p.workItemName?.trim() || '(Chưa gắn hạng mục)'
|
||||||
let wg = pg.workItems.find(w => (w.workItemId ?? '__no_workitem__') === wiKey)
|
let wg = pg.workItems.find(w => (w.workItemId ?? '__no_workitem__') === wiKey)
|
||||||
@ -162,24 +163,28 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
}
|
}
|
||||||
wg.items.push(p)
|
wg.items.push(p)
|
||||||
pg.totalCount++
|
pg.totalCount++
|
||||||
|
yg.totalCount++
|
||||||
}
|
}
|
||||||
const arr = Array.from(groupMap.values())
|
const arr = Array.from(yearMap.values())
|
||||||
arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi') || b.year - a.year)
|
arr.sort((a, b) => b.year - a.year)
|
||||||
for (const pg of arr) {
|
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'))
|
pg.workItems.sort((a, b) => a.workItemName.localeCompare(b.workItemName, 'vi'))
|
||||||
for (const wg of pg.workItems) {
|
for (const wg of pg.workItems) {
|
||||||
wg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
wg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return arr
|
return arr
|
||||||
}, [allRows, pendingMe])
|
}, [allRows, pendingMe])
|
||||||
|
|
||||||
// Total row count cho header badge (pendingMe đếm filtered, Danh sách đếm BE total).
|
// 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<string>. Default empty Set (all collapse).
|
// Plan AG2 — Expand state localStorage Set<string>. 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.
|
// 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_v2'
|
const STORAGE_KEY = 'pe_list_expanded_projects_v3'
|
||||||
const [expandedSet, setExpandedSet] = useState<Set<string>>(() => {
|
const [expandedSet, setExpandedSet] = useState<Set<string>>(() => {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
@ -276,16 +281,32 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!list.isLoading && projectGroups.length === 0 && (
|
{!list.isLoading && yearGroups.length === 0 && (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<EmptyState icon={ClipboardCheck} title="Chưa có phiếu" description="Tạo phiếu mới để bắt đầu quy trình." />
|
<EmptyState icon={ClipboardCheck} title="Chưa có phiếu" description="Tạo phiếu mới để bắt đầu quy trình." />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* S59 — Tree view theo anh Kiệt FDC: 📁 "Tên dự án (Năm)" (bg-slate-50) > 🧱 Hạng mục
|
{/* S59 — Tree view chốt: 📅 Năm (bg-slate-50) > 📁 Dự án > 🧱 Hạng mục công việc > PE card.
|
||||||
công việc > PE card. 2 layer <details>, named groups group/proj + group/wi cho chevron. */}
|
3 layer <details>, named groups group/year + group/proj + group/wi cho chevron rotation. */}
|
||||||
<div className="divide-y divide-slate-100">
|
<div className="divide-y divide-slate-100">
|
||||||
{projectGroups.map(pg => {
|
{yearGroups.map(yg => {
|
||||||
const projKey = `${pg.projectId ?? '__no_project__'}::y${pg.year}`
|
const yearKey = `y${yg.year}`
|
||||||
|
return (
|
||||||
|
<details
|
||||||
|
key={yearKey}
|
||||||
|
open={isExpanded(yearKey)}
|
||||||
|
onToggle={e => toggleExpand(yearKey, (e.currentTarget as HTMLDetailsElement).open)}
|
||||||
|
className="group/year"
|
||||||
|
>
|
||||||
|
<summary className="flex cursor-pointer items-center gap-1.5 bg-slate-50 px-3 py-2 hover:bg-slate-100 [&::-webkit-details-marker]:hidden">
|
||||||
|
<svg className="h-3 w-3 shrink-0 text-slate-500 transition-transform group-open/year:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||||
|
<span className="text-base">📅</span>
|
||||||
|
<span className="flex-1 truncate text-[13px] font-semibold text-slate-900">Năm {yg.year}</span>
|
||||||
|
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{yg.totalCount}</span>
|
||||||
|
</summary>
|
||||||
|
<div className="ml-3 border-l border-slate-200">
|
||||||
|
{yg.projects.map(pg => {
|
||||||
|
const projKey = `${yearKey}::p${pg.projectId ?? '_none_'}`
|
||||||
return (
|
return (
|
||||||
<details
|
<details
|
||||||
key={projKey}
|
key={projKey}
|
||||||
@ -293,11 +314,11 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
onToggle={e => toggleExpand(projKey, (e.currentTarget as HTMLDetailsElement).open)}
|
onToggle={e => toggleExpand(projKey, (e.currentTarget as HTMLDetailsElement).open)}
|
||||||
className="group/proj"
|
className="group/proj"
|
||||||
>
|
>
|
||||||
<summary className="flex cursor-pointer items-center gap-1.5 bg-slate-50 px-3 py-2 hover:bg-slate-100 [&::-webkit-details-marker]:hidden">
|
<summary className="flex cursor-pointer items-center gap-1.5 px-3 py-1.5 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
|
||||||
<svg className="h-3 w-3 shrink-0 text-slate-500 transition-transform group-open/proj:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
<svg className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/proj:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||||
<span className="text-base">📁</span>
|
<span className="text-sm">📁</span>
|
||||||
<span className="flex-1 truncate text-[13px] font-medium text-slate-900">{pg.label}</span>
|
<span className="flex-1 truncate text-[12px] font-medium text-slate-700">{pg.projectName}</span>
|
||||||
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{pg.totalCount}</span>
|
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{pg.totalCount}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="ml-3 border-l border-slate-200">
|
<div className="ml-3 border-l border-slate-200">
|
||||||
{pg.workItems.map(wg => {
|
{pg.workItems.map(wg => {
|
||||||
@ -370,6 +391,10 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user