[CLAUDE] FE-User: Chunk D — Layout filter !isVisible + render displayLabel
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m51s

Session 20 turn 7 Chunk D. fe-user (eOffice) áp dụng Ẩn/Hiện + Đổi tên menu
admin set qua MenuVisibilityPage.

fe-user/types/menu.ts: MenuItem + MenuNode +isVisible bool +displayLabel
string|null (mirror fe-admin).

fe-user/components/Layout.tsx:
  - filterForUser: filter 2 tầng — USER_HIDDEN_KEYS hardcode (Master/System/
    Forms/Reports — structural never-show) + dynamic !isVisible (Mig 27)
  - Helper effectiveLabel(n) = displayLabel?.trim() || label — admin custom
    label thắng, fallback gốc
  - Replace 3 callsite `{node.label}` → `{effectiveLabel(node)}` (Group/Leaf/
    NavLink render)
  - USER_FIXED_TOP entry "__inbox" +isVisible:true +displayLabel:null (giữ
    type check pass)

fe-admin Layout KHÔNG đụng — admin sidebar luôn dùng Label gốc + render hết
menu (kể cả isVisible=false), per user Q2=b.

Verify:
- npm run build × fe-user pass
- npm run build × fe-admin pass (no regression)

Pending Chunk E: Docs S20 turn 7 + STATUS + HANDOFF

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-11 11:39:43 +07:00
parent 059bfcbe38
commit 1ed6530fdd
2 changed files with 17 additions and 5 deletions

View File

@ -99,11 +99,19 @@ const USER_HIDDEN_KEYS = new Set([
]) ])
function filterForUser(nodes: MenuNode[]): MenuNode[] { function filterForUser(nodes: MenuNode[]): MenuNode[] {
// Filter 2 tầng: hardcode USER_HIDDEN_KEYS (system-level, structural never-show)
// + dynamic isVisible (Mig 27 admin toggle qua MenuVisibilityPage). isVisible
// mặc định true, admin set false → ẩn khỏi sidebar eOffice.
return nodes return nodes
.filter(n => !USER_HIDDEN_KEYS.has(n.key)) .filter(n => !USER_HIDDEN_KEYS.has(n.key) && n.isVisible !== false)
.map(n => ({ ...n, children: filterForUser(n.children) })) .map(n => ({ ...n, children: filterForUser(n.children) }))
} }
// Mig 27: ưu tiên displayLabel admin custom, fallback label gốc.
function effectiveLabel(n: { label: string; displayLabel?: string | null }): string {
return (n.displayLabel && n.displayLabel.trim()) || n.label
}
// Accordion state cho groups Ct_<Code> (7 HĐ) + Pe_<Code> (2 phiếu) — mỗi // Accordion state cho groups Ct_<Code> (7 HĐ) + Pe_<Code> (2 phiếu) — mỗi
// family mutex độc lập. Auto-expand theo URL `?type=X` (Ct_ dùng route // family mutex độc lập. Auto-expand theo URL `?type=X` (Ct_ dùng route
// /contracts|/my-contracts|/inbox, Pe_ dùng /purchase-evaluations). Group // /contracts|/my-contracts|/inbox, Pe_ dùng /purchase-evaluations). Group
@ -174,7 +182,7 @@ function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) {
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
{node.label} {effectiveLabel(node)}
</span> </span>
<ChevronDown className={cn('h-3.5 w-3.5 text-slate-400 transition', !open && '-rotate-90')} /> <ChevronDown className={cn('h-3.5 w-3.5 text-slate-400 transition', !open && '-rotate-90')} />
</button> </button>
@ -236,7 +244,7 @@ function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) {
)} )}
> >
<Icon className={cn(isDeep ? 'h-3.5 w-3.5' : 'h-4 w-4')} /> <Icon className={cn(isDeep ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
{node.label} {effectiveLabel(node)}
</NavLink> </NavLink>
) )
} }
@ -244,7 +252,7 @@ function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) {
// Static entries prepended to the dynamic menu tree — these are user-app // Static entries prepended to the dynamic menu tree — these are user-app
// specific (inbox + quick create) not backed by MenuItems DB rows. // specific (inbox + quick create) not backed by MenuItems DB rows.
const USER_FIXED_TOP: MenuNode[] = [ const USER_FIXED_TOP: MenuNode[] = [
{ key: '__inbox', label: 'Hộp thư', parentKey: null, order: 0, icon: 'Inbox', canRead: true, canCreate: true, canUpdate: true, canDelete: true, children: [] }, { key: '__inbox', label: 'Hộp thư', parentKey: null, order: 0, icon: 'Inbox', canRead: true, canCreate: true, canUpdate: true, canDelete: true, isVisible: true, displayLabel: null, children: [] },
] ]
function staticResolvePath(key: string): string | null { function staticResolvePath(key: string): string | null {
@ -268,7 +276,7 @@ function StaticLeaf({ node }: { node: MenuNode }) {
} }
> >
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
{node.label} {effectiveLabel(node)}
</NavLink> </NavLink>
) )
} }

View File

@ -8,6 +8,8 @@ export type MenuNode = {
canCreate: boolean canCreate: boolean
canUpdate: boolean canUpdate: boolean
canDelete: boolean canDelete: boolean
isVisible: boolean
displayLabel: string | null
children: MenuNode[] children: MenuNode[]
} }
@ -17,6 +19,8 @@ export type MenuItem = {
parentKey: string | null parentKey: string | null
order: number order: number
icon: string | null icon: string | null
isVisible: boolean
displayLabel: string | null
} }
export type Role = { export type Role = {