fix sidebar of companies

This commit is contained in:
root
2026-06-02 02:03:16 -04:00
parent 29886c3397
commit b5a974ee14
2 changed files with 101 additions and 12 deletions
@@ -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