add editable menu items for companies
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useEffect, useLayoutEffect, useState } from 'react'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
@@ -54,6 +55,19 @@ interface EmployeeProfile {
|
||||
preferredLanguage?: string
|
||||
}
|
||||
|
||||
interface GeneratedMenuItem {
|
||||
id: string
|
||||
systemKey: string | null
|
||||
label: string
|
||||
itemType: 'INTERNAL_PAGE' | 'EXTERNAL_LINK' | 'PARENT_MENU' | 'SECTION_LABEL' | 'DIVIDER'
|
||||
routeOrUrl: string | null
|
||||
icon: string | null
|
||||
parentId: string | null
|
||||
openInNewTab: boolean
|
||||
displayOrder: number
|
||||
children: GeneratedMenuItem[]
|
||||
}
|
||||
|
||||
function computeInitials(name: string): string {
|
||||
const parts = name.trim().split(/\s+/).filter(Boolean)
|
||||
if (parts.length === 0) return 'U'
|
||||
@@ -89,6 +103,23 @@ const NAV_ITEMS = [
|
||||
{ href: '/settings', key: 'settings', icon: Settings, minRole: 'OWNER' },
|
||||
] as const
|
||||
|
||||
const ICON_MAP = {
|
||||
LayoutDashboard,
|
||||
Car,
|
||||
Calendar,
|
||||
Globe,
|
||||
Users,
|
||||
Tag,
|
||||
UserPlus,
|
||||
BarChart2,
|
||||
CreditCard,
|
||||
Bell,
|
||||
Settings,
|
||||
FileText,
|
||||
Star,
|
||||
AlertTriangle,
|
||||
} as const
|
||||
|
||||
const SIDEBAR_BRAND_KEY = 'dashboard_brand'
|
||||
|
||||
const ROLE_RANK: Record<string, number> = { OWNER: 3, MANAGER: 2, AGENT: 1 }
|
||||
@@ -107,7 +138,6 @@ export default function Sidebar() {
|
||||
const isRtl = language === 'ar'
|
||||
const pathname = usePathname()
|
||||
const appPath = toDashboardAppPath(pathname)
|
||||
const router = useRouter()
|
||||
const [brand, setBrand] = useState<BrandSettings | null>(null)
|
||||
const [user, setUser] = useState<SidebarUser>({
|
||||
displayName: dict.workspaceUser,
|
||||
@@ -115,6 +145,7 @@ export default function Sidebar() {
|
||||
initials: 'W',
|
||||
})
|
||||
const [role, setRole] = useState<string>('AGENT')
|
||||
const [menuItems, setMenuItems] = useState<GeneratedMenuItem[] | null>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
@@ -192,12 +223,16 @@ export default function Sidebar() {
|
||||
|
||||
let cancelled = false
|
||||
|
||||
apiFetch<{ employee: EmployeeProfile }>('/auth/employee/me')
|
||||
.then(({ employee }) => {
|
||||
Promise.all([
|
||||
apiFetch<{ employee: EmployeeProfile }>('/auth/employee/me'),
|
||||
apiFetch<{ items: GeneratedMenuItem[] }>('/auth/employee/menu').catch(() => null),
|
||||
])
|
||||
.then(([{ employee }, menu]) => {
|
||||
if (cancelled) return
|
||||
window.localStorage.setItem(EMPLOYEE_PROFILE_KEY, JSON.stringify(employee))
|
||||
setUser(toSidebarUser(employee, dict.workspaceUser, dict.localAuth))
|
||||
if (employee.role) setRole(employee.role)
|
||||
if (menu?.items) setMenuItems(menu.items)
|
||||
if (employee.preferredLanguage === 'en' || employee.preferredLanguage === 'fr' || employee.preferredLanguage === 'ar') {
|
||||
// Only apply the server-side preference when the employee has no local preference
|
||||
// stored yet — avoids overriding a language the user manually switched to.
|
||||
@@ -224,6 +259,90 @@ export default function Sidebar() {
|
||||
return appPath.startsWith(item.href)
|
||||
}
|
||||
|
||||
const isGeneratedItemActive = (item: GeneratedMenuItem) => {
|
||||
if (!item.routeOrUrl || item.itemType !== 'INTERNAL_PAGE') return false
|
||||
if (item.routeOrUrl === '/') return appPath === '/'
|
||||
return appPath.startsWith(item.routeOrUrl)
|
||||
}
|
||||
|
||||
const fallbackMenuItems = NAV_ITEMS
|
||||
.filter((item) => !mounted || hasMinRole(role, item.minRole))
|
||||
.map((item) => ({
|
||||
id: item.href,
|
||||
systemKey: item.key,
|
||||
label: dict.nav[item.key] ?? item.key,
|
||||
itemType: 'INTERNAL_PAGE' as const,
|
||||
routeOrUrl: item.href,
|
||||
icon: item.icon.name,
|
||||
parentId: null,
|
||||
openInNewTab: false,
|
||||
displayOrder: 0,
|
||||
children: [],
|
||||
}))
|
||||
|
||||
const resolvedMenuItems = menuItems ?? fallbackMenuItems
|
||||
|
||||
function renderGeneratedMenu(items: GeneratedMenuItem[], depth = 0): ReactNode {
|
||||
return items.map((item) => {
|
||||
const label = item.systemKey ? (dict.nav[item.systemKey] ?? item.label) : item.label
|
||||
const paddingClass = depth > 0 ? 'pl-6' : ''
|
||||
const Icon = item.icon && item.icon in ICON_MAP ? ICON_MAP[item.icon as keyof typeof ICON_MAP] : null
|
||||
|
||||
if (item.itemType === 'DIVIDER') {
|
||||
return <div key={item.id} className="my-3 border-t border-blue-900/50" />
|
||||
}
|
||||
|
||||
if (item.itemType === 'SECTION_LABEL') {
|
||||
return (
|
||||
<p key={item.id} className="px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
|
||||
{label}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.itemType === 'PARENT_MENU') {
|
||||
return (
|
||||
<div key={item.id} className="space-y-1">
|
||||
<div className={`flex items-center gap-3 rounded-2xl px-3 py-2 text-sm font-medium text-slate-300 ${paddingClass}`}>
|
||||
{Icon ? <Icon className="h-4 w-4 flex-shrink-0" /> : null}
|
||||
{label}
|
||||
</div>
|
||||
<div className="space-y-0.5">{renderGeneratedMenu(item.children, depth + 1)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const active = mounted && isGeneratedItemActive(item)
|
||||
const className = [
|
||||
'flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-medium transition-colors',
|
||||
paddingClass,
|
||||
active ? 'bg-orange-500 text-white' : 'text-slate-400 hover:bg-blue-900/40 hover:text-white',
|
||||
].join(' ')
|
||||
|
||||
if (item.itemType === 'EXTERNAL_LINK' && item.routeOrUrl) {
|
||||
return (
|
||||
<a
|
||||
key={item.id}
|
||||
href={item.routeOrUrl}
|
||||
target={item.openInNewTab ? '_blank' : '_self'}
|
||||
rel={item.openInNewTab ? 'noreferrer noopener' : undefined}
|
||||
className={className}
|
||||
>
|
||||
{Icon ? <Icon className="h-4 w-4 flex-shrink-0" /> : null}
|
||||
{label}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link key={item.id} href={item.routeOrUrl || '/'} className={className}>
|
||||
{Icon ? <Icon className="h-4 w-4 flex-shrink-0" /> : null}
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function signOut() {
|
||||
localStorage.removeItem(EMPLOYEE_TOKEN_KEY)
|
||||
localStorage.removeItem(EMPLOYEE_PROFILE_KEY)
|
||||
@@ -280,7 +399,7 @@ export default function Sidebar() {
|
||||
</a>
|
||||
|
||||
<nav className="flex-1 space-y-0.5 overflow-y-auto px-3 py-4">
|
||||
{NAV_ITEMS.filter((item) => !mounted || hasMinRole(role, item.minRole)).map((item) => {
|
||||
{menuItems ? renderGeneratedMenu(resolvedMenuItems) : NAV_ITEMS.filter((item) => !mounted || hasMinRole(role, item.minRole)).map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = mounted && isActive(item)
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user