[CLAUDE] FE-User: cây tổ chức gốc "SOLUTION COMPANY" -> toả xuống phòng ban
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m17s

Anh: hiển thị cha-con với gốc SOLUTION COMPANY trên cùng, phòng ban fan-out xuống
(giống NamGroup "Nam Group" là gốc). Gộp nút "Tất cả phòng ban" + list phẳng thành
1 node gốc công ty trong EmployeesListPage org-tree panel: chevron mở/gập
(companyOpen, mở mặc định) + bấm tên = pickDept(null) tất cả NV + CountBadge tổng;
các phòng ban render TreeNode depth=1 toả xuống dưới. Build PASS fe-user (tsc -b).
fe-user only (mirror fe-admin defer cùng Phase B).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-16 11:23:58 +07:00
parent 5a0aaa4e83
commit ec517f7174

View File

@ -78,6 +78,7 @@ export function EmployeesListPage() {
const [localSearch, setLocalSearch] = useState(search)
const [treeOpenMobile, setTreeOpenMobile] = useState(false)
const [companyOpen, setCompanyOpen] = useState(true) // gốc công ty mở mặc định
// Org tree (consume /departments/tree). Class-level [Authorize] only → any
// authenticated user. Counts come pre-rolled-up from BE (TotalEmployeeCount).
@ -175,30 +176,54 @@ export function EmployeesListPage() {
</div>
</header>
<div className="min-h-0 flex-1 overflow-auto p-2">
<button
type="button"
onClick={() => pickDept(null)}
className={cn(
'mb-1 flex w-full items-center justify-between gap-2 rounded-lg px-2.5 py-1.5 text-left text-sm transition',
!deptFilter ? 'bg-brand-50 font-semibold text-brand-700' : 'text-slate-700 hover:bg-slate-50',
)}
>
<span className="flex items-center gap-2">
<Users className="h-4 w-4 shrink-0 text-slate-400" />
Tất cả phòng ban
</span>
<CountBadge value={list.data?.total ?? 0} active={!deptFilter} />
</button>
{tree.isLoading ? (
<div className="px-2 py-6 text-center text-xs text-slate-400">Đang tải cây tổ chức</div>
) : !tree.data || tree.data.length === 0 ? (
<div className="px-2 py-6 text-center text-xs text-slate-400">Chưa phòng ban.</div>
) : (
<ul className="space-y-0.5">
{tree.data.map(n => (
<TreeNode key={n.id} node={n} depth={0} selectedId={deptFilter} onPick={pickDept} />
))}
{/* Gốc công ty — bấm = tất cả NV; các phòng ban toả xuống dưới (cha→con) */}
<li>
<div
className={cn(
'group flex items-center gap-1 rounded-lg pr-1.5 transition',
!deptFilter ? 'bg-brand-50' : 'hover:bg-slate-50',
)}
>
<button
type="button"
onClick={() => setCompanyOpen(v => !v)}
className="grid h-5 w-5 shrink-0 place-items-center rounded text-slate-400 hover:text-slate-700"
aria-label={companyOpen ? 'Thu gọn' : 'Mở rộng'}
aria-expanded={companyOpen}
>
<ChevronRight className={cn('h-3.5 w-3.5 transition-transform', companyOpen && 'rotate-90')} />
</button>
<button
type="button"
onClick={() => pickDept(null)}
className={cn(
'flex min-w-0 flex-1 items-center justify-between gap-2 py-1.5 text-left text-sm transition',
!deptFilter ? 'font-semibold text-brand-700' : 'text-slate-700',
)}
title="SOLUTION COMPANY"
>
<span className="flex min-w-0 items-center gap-2">
<Building2 className="h-4 w-4 shrink-0 text-brand-500" />
<span className="truncate font-semibold tracking-tight">SOLUTION COMPANY</span>
</span>
<CountBadge value={list.data?.total ?? 0} active={!deptFilter} />
</button>
</div>
{companyOpen &&
(!tree.data || tree.data.length === 0 ? (
<div className="py-3 pl-9 text-xs text-slate-400">Chưa phòng ban.</div>
) : (
<ul className="space-y-0.5">
{tree.data.map(n => (
<TreeNode key={n.id} node={n} depth={1} selectedId={deptFilter} onPick={pickDept} />
))}
</ul>
))}
</li>
</ul>
)}
</div>