[CLAUDE] FE-User+FE-Admin: Plan AG5 — PE List tree 3-level Project > Năm > NCC > PE
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m32s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m32s
Anh feedback 2026-05-21: "Folder cấp dưới dự án là theo năm và dưới năm là theo NCC nhé". Plan AG3 chỉ 1-level Project > PE. Plan AG5 extend xuống 3 cấp: Năm + NCC. Group structure: - Level 1: 📁 Project (bg-slate-50, font-medium 13px) - Level 2: 📅 Năm {year} (border-l ml-3, 12px) - Level 3: 🏢 NCC (border-l ml-3, 12px, italic slate-400 nếu "Chưa chọn NCC") - Leaf: PE card (border-l ml-3, giữ nguyên content) Sort: - Project A-Z (vi locale) - Năm DESC (2026 trước 2025) - NCC A-Z (vi locale) - PE within NCC: createdAt DESC Fallback: - empty projectName → "(Dự án đã xoá)" - selectedSupplierName null (PE chưa DaDuyet) → "(Chưa chọn NCC)" group + italic style Drop redundant selectedSupplierName line trong PE card (đã hiện ở NCC group header). localStorage keys: - Project: projectId - Năm: `${projectId}::y${year}` - NCC: `${projectId}::y${year}::s${supplierId|'_none_'}` Verify: - npm build fe-user PASS 0 TS err 1292.68 KB (gzip 337.18 KB) 1907 modules - npm build fe-admin PASS 0 TS err 1404.02 KB (gzip 357.70 KB) 1926 modules - 2 file SHA256 IDENTICAL E5FE4979... (mirror §3.9) - KHÔNG BE change, KHÔNG Mig, KHÔNG test (UAT mode) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -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 →
|
// Đã 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+.
|
||||||
|
|
||||||
// Plan AG2 — Group 1-level Project > PE (drop tầng gói thầu per anh feedback 2026-05-21:
|
// Plan AG5 — Group 3-level Project > Năm > NCC > PE (anh feedback 2026-05-21:
|
||||||
// "gói thầu thì ko cần thiết phải treedow"). Filter (pendingMe → DaGuiDuyet) TRƯỚC group.
|
// "Folder cấp dưới dự án là theo năm và dưới năm là theo NCC"). Filter pendingMe TRƯỚC group.
|
||||||
// Fallback "(Dự án đã xoá)" empty projectName. Sort vi locale Project A-Z + PE createdAt DESC.
|
// 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 = {
|
type ProjectGroup = {
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
projectName: string
|
projectName: string
|
||||||
items: PeListItem[]
|
years: YearGroup[]
|
||||||
|
totalCount: number
|
||||||
}
|
}
|
||||||
const projectGroups = useMemo<ProjectGroup[]>(() => {
|
const projectGroups = useMemo<ProjectGroup[]>(() => {
|
||||||
const filtered = pendingMe
|
const filtered = pendingMe
|
||||||
@ -132,20 +144,42 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
const projKey = p.projectId ?? '__no_project__'
|
const projKey = p.projectId ?? '__no_project__'
|
||||||
const projName = p.projectName?.trim() || '(Dự án đã xoá)'
|
const projName = p.projectName?.trim() || '(Dự án đã xoá)'
|
||||||
if (!projectMap.has(projKey)) {
|
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())
|
const arr = Array.from(projectMap.values())
|
||||||
arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi'))
|
arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi'))
|
||||||
for (const pg of arr) {
|
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
|
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.items.length, 0)
|
const totalRowCount = projectGroups.reduce((sum, pg) => sum + pg.totalCount, 0)
|
||||||
|
|
||||||
// Plan AG2 — Expand state localStorage Set<string> (projectId only, drop ::gtKey suffix).
|
// Plan AG2 — Expand state localStorage Set<string> (projectId only, drop ::gtKey suffix).
|
||||||
// Default empty Set (all collapse). Single-PE project skip <details> wrapper (render flat).
|
// Default empty Set (all collapse). Single-PE project skip <details> wrapper (render flat).
|
||||||
@ -251,10 +285,10 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
<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>
|
||||||
)}
|
)}
|
||||||
{/* Plan AG3 — Tree view 1-level Project > PE consistent (anh feedback 2026-05-21:
|
{/* Plan AG5 — Tree view 3-level Project > Năm > NCC > PE (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").
|
"Folder cấp dưới dự án là theo năm và dưới năm là theo NCC").
|
||||||
Mọi dự án dù có 1 hay nhiều PE đều render <details> folder collapsed.
|
3 layer <details>: 📁 Project (bg-slate-50) > 📅 Năm > 🏢 NCC > PE card.
|
||||||
Tailwind v3 named group group/proj + [&::-webkit-details-marker]:hidden. */}
|
Tailwind v3 named groups group/proj + group/year + group/sup cho chevron rotation. */}
|
||||||
<div className="divide-y divide-slate-100">
|
<div className="divide-y divide-slate-100">
|
||||||
{projectGroups.map(pg => {
|
{projectGroups.map(pg => {
|
||||||
const projKey = pg.projectId ?? '__no_project__'
|
const projKey = pg.projectId ?? '__no_project__'
|
||||||
@ -269,10 +303,42 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
<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-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>
|
||||||
<span className="text-base">📁</span>
|
<span className="text-base">📁</span>
|
||||||
<span className="flex-1 truncate text-[13px] font-medium text-slate-900">{pg.projectName}</span>
|
<span className="flex-1 truncate text-[13px] font-medium text-slate-900">{pg.projectName}</span>
|
||||||
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{pg.items.length}</span>
|
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{pg.totalCount}</span>
|
||||||
|
</summary>
|
||||||
|
<div className="ml-3 border-l border-slate-200">
|
||||||
|
{pg.years.map(yg => {
|
||||||
|
const yearKey = `${projKey}::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 px-3 py-1.5 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
|
||||||
|
<svg className="h-3 w-3 shrink-0 text-slate-400 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-sm">📅</span>
|
||||||
|
<span className="flex-1 truncate text-[12px] font-medium text-slate-700">Năm {yg.year}</span>
|
||||||
|
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{yg.totalCount}</span>
|
||||||
|
</summary>
|
||||||
|
<div className="ml-3 border-l border-slate-200">
|
||||||
|
{yg.suppliers.map(sg => {
|
||||||
|
const supKey = `${yearKey}::s${sg.supplierId ?? '_none_'}`
|
||||||
|
return (
|
||||||
|
<details
|
||||||
|
key={supKey}
|
||||||
|
open={isExpanded(supKey)}
|
||||||
|
onToggle={e => toggleExpand(supKey, (e.currentTarget as HTMLDetailsElement).open)}
|
||||||
|
className="group/sup"
|
||||||
|
>
|
||||||
|
<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-400 transition-transform group-open/sup: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-sm">🏢</span>
|
||||||
|
<span className={cn('flex-1 truncate text-[12px]', sg.supplierId ? 'text-slate-700' : 'italic text-slate-400')}>{sg.supplierName}</span>
|
||||||
|
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{sg.items.length}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<ul className="ml-3 divide-y divide-slate-100 border-l border-slate-200">
|
<ul className="ml-3 divide-y divide-slate-100 border-l border-slate-200">
|
||||||
{pg.items.map(p => (
|
{sg.items.map(p => (
|
||||||
<li key={p.id}>
|
<li key={p.id}>
|
||||||
<button
|
<button
|
||||||
onClick={() => selectRow(p.id)}
|
onClick={() => selectRow(p.id)}
|
||||||
@ -293,11 +359,6 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
{p.departmentName && <span className="text-slate-400"> · {p.departmentName}</span>}
|
{p.departmentName && <span className="text-slate-400"> · {p.departmentName}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{p.selectedSupplierName && (
|
|
||||||
<div className="mt-0.5 truncate text-[11px] text-emerald-600">
|
|
||||||
✓ {p.selectedSupplierName}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -332,6 +393,14 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
@ -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 →
|
// Đã 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+.
|
||||||
|
|
||||||
// Plan AG2 — Group 1-level Project > PE (drop tầng gói thầu per anh feedback 2026-05-21:
|
// Plan AG5 — Group 3-level Project > Năm > NCC > PE (anh feedback 2026-05-21:
|
||||||
// "gói thầu thì ko cần thiết phải treedow"). Filter (pendingMe → DaGuiDuyet) TRƯỚC group.
|
// "Folder cấp dưới dự án là theo năm và dưới năm là theo NCC"). Filter pendingMe TRƯỚC group.
|
||||||
// Fallback "(Dự án đã xoá)" empty projectName. Sort vi locale Project A-Z + PE createdAt DESC.
|
// 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 = {
|
type ProjectGroup = {
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
projectName: string
|
projectName: string
|
||||||
items: PeListItem[]
|
years: YearGroup[]
|
||||||
|
totalCount: number
|
||||||
}
|
}
|
||||||
const projectGroups = useMemo<ProjectGroup[]>(() => {
|
const projectGroups = useMemo<ProjectGroup[]>(() => {
|
||||||
const filtered = pendingMe
|
const filtered = pendingMe
|
||||||
@ -132,20 +144,42 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
const projKey = p.projectId ?? '__no_project__'
|
const projKey = p.projectId ?? '__no_project__'
|
||||||
const projName = p.projectName?.trim() || '(Dự án đã xoá)'
|
const projName = p.projectName?.trim() || '(Dự án đã xoá)'
|
||||||
if (!projectMap.has(projKey)) {
|
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())
|
const arr = Array.from(projectMap.values())
|
||||||
arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi'))
|
arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi'))
|
||||||
for (const pg of arr) {
|
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
|
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.items.length, 0)
|
const totalRowCount = projectGroups.reduce((sum, pg) => sum + pg.totalCount, 0)
|
||||||
|
|
||||||
// Plan AG2 — Expand state localStorage Set<string> (projectId only, drop ::gtKey suffix).
|
// Plan AG2 — Expand state localStorage Set<string> (projectId only, drop ::gtKey suffix).
|
||||||
// Default empty Set (all collapse). Single-PE project skip <details> wrapper (render flat).
|
// Default empty Set (all collapse). Single-PE project skip <details> wrapper (render flat).
|
||||||
@ -251,10 +285,10 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
<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>
|
||||||
)}
|
)}
|
||||||
{/* Plan AG3 — Tree view 1-level Project > PE consistent (anh feedback 2026-05-21:
|
{/* Plan AG5 — Tree view 3-level Project > Năm > NCC > PE (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").
|
"Folder cấp dưới dự án là theo năm và dưới năm là theo NCC").
|
||||||
Mọi dự án dù có 1 hay nhiều PE đều render <details> folder collapsed.
|
3 layer <details>: 📁 Project (bg-slate-50) > 📅 Năm > 🏢 NCC > PE card.
|
||||||
Tailwind v3 named group group/proj + [&::-webkit-details-marker]:hidden. */}
|
Tailwind v3 named groups group/proj + group/year + group/sup cho chevron rotation. */}
|
||||||
<div className="divide-y divide-slate-100">
|
<div className="divide-y divide-slate-100">
|
||||||
{projectGroups.map(pg => {
|
{projectGroups.map(pg => {
|
||||||
const projKey = pg.projectId ?? '__no_project__'
|
const projKey = pg.projectId ?? '__no_project__'
|
||||||
@ -269,10 +303,42 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
<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-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>
|
||||||
<span className="text-base">📁</span>
|
<span className="text-base">📁</span>
|
||||||
<span className="flex-1 truncate text-[13px] font-medium text-slate-900">{pg.projectName}</span>
|
<span className="flex-1 truncate text-[13px] font-medium text-slate-900">{pg.projectName}</span>
|
||||||
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{pg.items.length}</span>
|
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{pg.totalCount}</span>
|
||||||
|
</summary>
|
||||||
|
<div className="ml-3 border-l border-slate-200">
|
||||||
|
{pg.years.map(yg => {
|
||||||
|
const yearKey = `${projKey}::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 px-3 py-1.5 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
|
||||||
|
<svg className="h-3 w-3 shrink-0 text-slate-400 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-sm">📅</span>
|
||||||
|
<span className="flex-1 truncate text-[12px] font-medium text-slate-700">Năm {yg.year}</span>
|
||||||
|
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{yg.totalCount}</span>
|
||||||
|
</summary>
|
||||||
|
<div className="ml-3 border-l border-slate-200">
|
||||||
|
{yg.suppliers.map(sg => {
|
||||||
|
const supKey = `${yearKey}::s${sg.supplierId ?? '_none_'}`
|
||||||
|
return (
|
||||||
|
<details
|
||||||
|
key={supKey}
|
||||||
|
open={isExpanded(supKey)}
|
||||||
|
onToggle={e => toggleExpand(supKey, (e.currentTarget as HTMLDetailsElement).open)}
|
||||||
|
className="group/sup"
|
||||||
|
>
|
||||||
|
<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-400 transition-transform group-open/sup: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-sm">🏢</span>
|
||||||
|
<span className={cn('flex-1 truncate text-[12px]', sg.supplierId ? 'text-slate-700' : 'italic text-slate-400')}>{sg.supplierName}</span>
|
||||||
|
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{sg.items.length}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<ul className="ml-3 divide-y divide-slate-100 border-l border-slate-200">
|
<ul className="ml-3 divide-y divide-slate-100 border-l border-slate-200">
|
||||||
{pg.items.map(p => (
|
{sg.items.map(p => (
|
||||||
<li key={p.id}>
|
<li key={p.id}>
|
||||||
<button
|
<button
|
||||||
onClick={() => selectRow(p.id)}
|
onClick={() => selectRow(p.id)}
|
||||||
@ -293,11 +359,6 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
{p.departmentName && <span className="text-slate-400"> · {p.departmentName}</span>}
|
{p.departmentName && <span className="text-slate-400"> · {p.departmentName}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{p.selectedSupplierName && (
|
|
||||||
<div className="mt-0.5 truncate text-[11px] text-emerald-600">
|
|
||||||
✓ {p.selectedSupplierName}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -332,6 +393,14 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user