fix sidebar of companies
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useLayoutEffect, useState } from 'react'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Car,
|
||||
@@ -33,6 +33,14 @@ interface BrandSettings {
|
||||
logoUrl: string | null
|
||||
}
|
||||
|
||||
interface CompanySummary {
|
||||
name: string
|
||||
brand?: {
|
||||
displayName?: string | null
|
||||
logoUrl?: string | null
|
||||
} | null
|
||||
}
|
||||
|
||||
interface SidebarUser {
|
||||
displayName: string
|
||||
subtitle: string
|
||||
@@ -81,6 +89,8 @@ const NAV_ITEMS = [
|
||||
{ href: '/settings', key: 'settings', icon: Settings, minRole: 'OWNER' },
|
||||
] as const
|
||||
|
||||
const SIDEBAR_BRAND_KEY = 'dashboard_brand'
|
||||
|
||||
const ROLE_RANK: Record<string, number> = { OWNER: 3, MANAGER: 2, AGENT: 1 }
|
||||
|
||||
function hasMinRole(employeeRole: string, minRole: string): boolean {
|
||||
@@ -93,7 +103,7 @@ function notifyParent(message: Record<string, unknown>) {
|
||||
}
|
||||
|
||||
export default function Sidebar() {
|
||||
const { dict, language, setLanguage } = useDashboardI18n()
|
||||
const { dict, language, setLanguage, theme } = useDashboardI18n()
|
||||
const isRtl = language === 'ar'
|
||||
const pathname = usePathname()
|
||||
const appPath = toDashboardAppPath(pathname)
|
||||
@@ -110,8 +120,54 @@ export default function Sidebar() {
|
||||
|
||||
useEffect(() => { setMounted(true) }, [])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
try {
|
||||
const cached = window.localStorage.getItem(SIDEBAR_BRAND_KEY)
|
||||
if (!cached) return
|
||||
const parsed = JSON.parse(cached) as Partial<BrandSettings>
|
||||
if (typeof parsed.displayName === 'string' && parsed.displayName.trim()) {
|
||||
setBrand({
|
||||
displayName: parsed.displayName,
|
||||
logoUrl: typeof parsed.logoUrl === 'string' ? parsed.logoUrl : null,
|
||||
})
|
||||
}
|
||||
} catch {}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch<BrandSettings>('/companies/me/brand').then(setBrand).catch(() => {})
|
||||
const token = window.localStorage.getItem(EMPLOYEE_TOKEN_KEY)
|
||||
if (!token) return
|
||||
|
||||
let cancelled = false
|
||||
|
||||
async function loadBrand() {
|
||||
try {
|
||||
const brandData = await apiFetch<BrandSettings | null>('/companies/me/brand')
|
||||
if (cancelled) return
|
||||
if (brandData?.displayName?.trim()) {
|
||||
setBrand(brandData)
|
||||
window.localStorage.setItem(SIDEBAR_BRAND_KEY, JSON.stringify(brandData))
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const company = await apiFetch<CompanySummary>('/companies/me')
|
||||
if (cancelled) return
|
||||
|
||||
const resolvedBrand: BrandSettings = {
|
||||
displayName: company.brand?.displayName?.trim() || company.name,
|
||||
logoUrl: company.brand?.logoUrl ?? null,
|
||||
}
|
||||
|
||||
setBrand(resolvedBrand)
|
||||
window.localStorage.setItem(SIDEBAR_BRAND_KEY, JSON.stringify(resolvedBrand))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
loadBrand()
|
||||
|
||||
return () => { cancelled = true }
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -218,7 +274,7 @@ export default function Sidebar() {
|
||||
<Car className="h-4 w-4 text-blue-950" />
|
||||
)}
|
||||
</div>
|
||||
<span className="truncate text-sm font-bold tracking-wide text-white">
|
||||
<span className={`truncate text-sm font-bold tracking-wide ${theme === 'light' ? 'text-blue-600' : 'text-white'}`}>
|
||||
{brand?.displayName ?? 'RentalDriveGo'}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -15,6 +15,23 @@ function computeInitials(name: string): string {
|
||||
return `${parts[0].slice(0, 1)}${parts[1].slice(0, 1)}`.toUpperCase()
|
||||
}
|
||||
|
||||
function resolveSocketOrigin(): string | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
const configuredApiUrl = process.env.NEXT_PUBLIC_API_URL
|
||||
if (configuredApiUrl) {
|
||||
try {
|
||||
return new URL(configuredApiUrl, window.location.origin).origin
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
return 'http://localhost:4000'
|
||||
}
|
||||
|
||||
return window.location.origin
|
||||
}
|
||||
|
||||
export default function TopBar() {
|
||||
const { dict } = useDashboardI18n()
|
||||
const pathname = usePathname()
|
||||
@@ -75,15 +92,31 @@ export default function TopBar() {
|
||||
const token = window.localStorage.getItem(EMPLOYEE_TOKEN_KEY)
|
||||
if (!token) return
|
||||
|
||||
const socketUrl = (process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1').replace('/api/v1', '')
|
||||
const socket = io(socketUrl, { auth: { token }, transports: ['websocket'] })
|
||||
const socketOrigin = resolveSocketOrigin()
|
||||
if (!socketOrigin) return
|
||||
|
||||
socket.on('notification', () => {
|
||||
setUnreadCount((prev) => prev + 1)
|
||||
window.dispatchEvent(new Event('notifications:updated'))
|
||||
const socket = io(socketOrigin, {
|
||||
autoConnect: false,
|
||||
auth: { token },
|
||||
transports: ['polling', 'websocket'],
|
||||
})
|
||||
|
||||
return () => { socket.disconnect() }
|
||||
const handleNotification = () => {
|
||||
setUnreadCount((prev) => prev + 1)
|
||||
window.dispatchEvent(new Event('notifications:updated'))
|
||||
}
|
||||
|
||||
socket.on('notification', handleNotification)
|
||||
|
||||
const connectTimer = window.setTimeout(() => {
|
||||
socket.connect()
|
||||
}, 0)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(connectTimer)
|
||||
socket.off('notification', handleNotification)
|
||||
socket.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -177,7 +210,7 @@ export default function TopBar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="flex h-16 items-center justify-between border-b border-stone-200/80 bg-white/78 px-6 backdrop-blur-xl transition-colors dark:border-blue-900 dark:bg-blue-950/76">
|
||||
<header className="relative z-[70] flex h-16 items-center justify-between border-b border-stone-200/80 bg-white/78 px-6 backdrop-blur-xl transition-colors dark:border-blue-900 dark:bg-blue-950/76">
|
||||
<h1 className="text-lg font-semibold text-blue-950 dark:text-stone-50">{title}</h1>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -195,7 +228,7 @@ export default function TopBar() {
|
||||
</button>
|
||||
|
||||
{showNotifs ? (
|
||||
<div className="absolute right-0 top-full z-50 mt-2 w-80 rounded-[1.75rem] border border-stone-200/80 bg-white/95 p-4 shadow-[0_20px_60px_rgba(0,0,0,0.12)] backdrop-blur dark:border-blue-900 dark:bg-blue-950/95 dark:shadow-[0_20px_60px_rgba(0,0,0,0.35)]">
|
||||
<div className="absolute right-0 top-full z-[80] mt-2 w-80 rounded-[1.75rem] border border-stone-200/80 bg-white/95 p-4 shadow-[0_20px_60px_rgba(0,0,0,0.12)] backdrop-blur dark:border-blue-900 dark:bg-blue-950/95 dark:shadow-[0_20px_60px_rgba(0,0,0,0.35)]">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<p className="text-sm font-medium text-blue-950 dark:text-stone-100">{dict.notifications}</p>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user