dashboard fix
This commit is contained in:
@@ -1351,6 +1351,16 @@ function normalizeLanguage(value: string | null | undefined): DashboardLanguage
|
||||
return value === 'fr' || value === 'ar' || value === 'en' ? value : 'en'
|
||||
}
|
||||
|
||||
function detectBrowserLanguage(): DashboardLanguage | null {
|
||||
if (typeof navigator === 'undefined') return null
|
||||
const langs = Array.from(navigator.languages ?? [navigator.language])
|
||||
for (const lang of langs) {
|
||||
const code = lang.split('-')[0].toLowerCase()
|
||||
if (code === 'fr' || code === 'ar' || code === 'en') return code as DashboardLanguage
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function DashboardI18nProvider({
|
||||
children,
|
||||
initialLanguage = 'en',
|
||||
@@ -1361,12 +1371,19 @@ export function DashboardI18nProvider({
|
||||
const [language, setLanguage] = useState<DashboardLanguage>(initialLanguage)
|
||||
const [theme, setTheme] = useState<DashboardTheme>('light')
|
||||
const themeInitialized = useRef(false)
|
||||
// Skip the very first write so we don't overwrite a stored preference with the
|
||||
// server-resolved initialLanguage before the read effect has a chance to apply it.
|
||||
const skipFirstLangWrite = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
const stored = readScopedPreference(SHARED_LANGUAGE_KEY, [DASHBOARD_LANGUAGE_KEY])
|
||||
if (stored === null) return
|
||||
const next = normalizeLanguage(stored)
|
||||
if (next !== language) setLanguage(next)
|
||||
if (stored !== null) {
|
||||
const next = normalizeLanguage(stored)
|
||||
if (next !== language) setLanguage(next)
|
||||
} else {
|
||||
const detected = detectBrowserLanguage()
|
||||
if (detected && detected !== language) setLanguage(detected)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1384,6 +1401,10 @@ export function DashboardI18nProvider({
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = language
|
||||
document.documentElement.dir = language === 'ar' ? 'rtl' : 'ltr'
|
||||
if (skipFirstLangWrite.current) {
|
||||
skipFirstLangWrite.current = false
|
||||
return
|
||||
}
|
||||
writeScopedPreference(SHARED_LANGUAGE_KEY, language, [DASHBOARD_LANGUAGE_KEY])
|
||||
document.cookie = `${SHARED_LANGUAGE_COOKIE}=${language}; path=/; max-age=31536000; samesite=lax`
|
||||
document.cookie = `${DASHBOARD_LANGUAGE_KEY}=${language}; path=/; max-age=31536000; samesite=lax`
|
||||
@@ -1407,10 +1428,17 @@ export function DashboardI18nProvider({
|
||||
const scopedLanguage = readCurrentUserScopedPreference(SHARED_LANGUAGE_KEY)
|
||||
const scopedTheme = readCurrentUserScopedPreference(SHARED_THEME_KEY)
|
||||
|
||||
if (scopedLanguage && normalizeLanguage(scopedLanguage) !== language) {
|
||||
setLanguage(normalizeLanguage(scopedLanguage))
|
||||
if (scopedLanguage) {
|
||||
// Employee has a stored preference — apply it.
|
||||
const next = normalizeLanguage(scopedLanguage)
|
||||
if (next !== language) setLanguage(next)
|
||||
} else {
|
||||
writeScopedPreference(SHARED_LANGUAGE_KEY, language, [DASHBOARD_LANGUAGE_KEY])
|
||||
// No scoped pref yet (new login). Promote any base preference that was set
|
||||
// before login (e.g., language selected on the sign-up page) to the scoped key.
|
||||
const baseLang = readScopedPreference(SHARED_LANGUAGE_KEY, [DASHBOARD_LANGUAGE_KEY])
|
||||
const langToWrite = baseLang ? normalizeLanguage(baseLang) : language
|
||||
if (langToWrite !== language) setLanguage(langToWrite)
|
||||
writeScopedPreference(SHARED_LANGUAGE_KEY, langToWrite, [DASHBOARD_LANGUAGE_KEY])
|
||||
}
|
||||
|
||||
if (scopedTheme === 'light' || scopedTheme === 'medium' || scopedTheme === 'dark') {
|
||||
|
||||
@@ -39,6 +39,7 @@ interface EmployeeProfile {
|
||||
firstName: string
|
||||
lastName: string
|
||||
role: string
|
||||
preferredLanguage?: string
|
||||
}
|
||||
|
||||
function computeInitials(name: string): string {
|
||||
@@ -59,23 +60,29 @@ function toSidebarUser(profile: Partial<EmployeeProfile>, fallbackName: string,
|
||||
}
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: '/dashboard', key: 'dashboard', icon: LayoutDashboard, exact: true },
|
||||
{ href: '/dashboard/fleet', key: 'fleet', icon: Car },
|
||||
{ href: '/dashboard/reservations', key: 'reservations', icon: Calendar },
|
||||
{ href: '/dashboard/online-reservations', key: 'onlineReservations', icon: Globe },
|
||||
{ href: '/dashboard/customers', key: 'customers', icon: Users },
|
||||
{ href: '/dashboard/offers', key: 'offers', icon: Tag },
|
||||
{ href: '/dashboard/team', key: 'team', icon: UserPlus },
|
||||
{ href: '/dashboard/reports', key: 'reports', icon: BarChart2 },
|
||||
{ href: '/dashboard/subscription', key: 'subscription', icon: CreditCard },
|
||||
{ href: '/dashboard/billing', key: 'billing', icon: CreditCard },
|
||||
{ href: '/dashboard/contracts', key: 'contracts', icon: FileText },
|
||||
{ href: '/dashboard/notifications', key: 'notifications', icon: Bell },
|
||||
{ href: '/dashboard/settings', key: 'settings', icon: Settings },
|
||||
{ href: '/dashboard', key: 'dashboard', icon: LayoutDashboard, exact: true, minRole: 'AGENT' },
|
||||
{ href: '/dashboard/fleet', key: 'fleet', icon: Car, minRole: 'AGENT' },
|
||||
{ href: '/dashboard/reservations', key: 'reservations', icon: Calendar, minRole: 'AGENT' },
|
||||
{ href: '/dashboard/online-reservations', key: 'onlineReservations', icon: Globe, minRole: 'AGENT' },
|
||||
{ href: '/dashboard/customers', key: 'customers', icon: Users, minRole: 'AGENT' },
|
||||
{ href: '/dashboard/offers', key: 'offers', icon: Tag, minRole: 'MANAGER' },
|
||||
{ href: '/dashboard/team', key: 'team', icon: UserPlus, minRole: 'AGENT' },
|
||||
{ href: '/dashboard/reports', key: 'reports', icon: BarChart2, minRole: 'MANAGER' },
|
||||
{ href: '/dashboard/subscription', key: 'subscription', icon: CreditCard, minRole: 'OWNER' },
|
||||
{ href: '/dashboard/billing', key: 'billing', icon: CreditCard, minRole: 'MANAGER' },
|
||||
{ href: '/dashboard/contracts', key: 'contracts', icon: FileText, minRole: 'AGENT' },
|
||||
{ href: '/dashboard/notifications', key: 'notifications', icon: Bell, minRole: 'AGENT' },
|
||||
{ href: '/dashboard/settings', key: 'settings', icon: Settings, minRole: 'OWNER' },
|
||||
] as const
|
||||
|
||||
const ROLE_RANK: Record<string, number> = { OWNER: 3, MANAGER: 2, AGENT: 1 }
|
||||
|
||||
function hasMinRole(employeeRole: string, minRole: string): boolean {
|
||||
return (ROLE_RANK[employeeRole] ?? 0) >= (ROLE_RANK[minRole] ?? 0)
|
||||
}
|
||||
|
||||
export default function Sidebar() {
|
||||
const { dict, language } = useDashboardI18n()
|
||||
const { dict, language, setLanguage } = useDashboardI18n()
|
||||
const isRtl = language === 'ar'
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
@@ -85,7 +92,11 @@ export default function Sidebar() {
|
||||
subtitle: dict.localAuth,
|
||||
initials: 'W',
|
||||
})
|
||||
const [role, setRole] = useState<string>('AGENT')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => { setMounted(true) }, [])
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch<BrandSettings>('/companies/me/brand').then(setBrand).catch(() => {})
|
||||
@@ -107,6 +118,7 @@ export default function Sidebar() {
|
||||
try {
|
||||
const profile = JSON.parse(cached) as Partial<EmployeeProfile>
|
||||
setUser(toSidebarUser(profile, dict.workspaceUser, dict.localAuth))
|
||||
if (profile.role) setRole(profile.role)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -117,6 +129,11 @@ export default function Sidebar() {
|
||||
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 (employee.preferredLanguage === 'en' || employee.preferredLanguage === 'fr' || employee.preferredLanguage === 'ar') {
|
||||
setLanguage(employee.preferredLanguage)
|
||||
document.cookie = `rentaldrivego-language=${employee.preferredLanguage}; path=/; max-age=31536000; samesite=lax`
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return
|
||||
@@ -188,9 +205,9 @@ export default function Sidebar() {
|
||||
</a>
|
||||
|
||||
<nav className="flex-1 space-y-0.5 overflow-y-auto px-3 py-4">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
{NAV_ITEMS.filter((item) => !mounted || hasMinRole(role, item.minRole)).map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = isActive(item)
|
||||
const active = mounted && isActive(item)
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
|
||||
@@ -29,7 +29,11 @@ export default function TopBar() {
|
||||
}>>([])
|
||||
const [loadingNotifs, setLoadingNotifs] = useState(false)
|
||||
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => { setMounted(true) }, [])
|
||||
|
||||
const title = (() => {
|
||||
if (!mounted) return dict.titles['/dashboard']
|
||||
if (dict.titles[pathname]) return dict.titles[pathname]
|
||||
if (pathname.startsWith('/dashboard/contracts/')) return dict.titles['/dashboard/contracts'] ?? dict.titles['/dashboard']
|
||||
if (pathname.startsWith('/dashboard/reservations/')) return dict.titles['/dashboard/reservations'] ?? dict.titles['/dashboard']
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
'use client'
|
||||
|
||||
export type BilingualField = { fr: string; ar: string }
|
||||
|
||||
export function emptyBilingual(): BilingualField {
|
||||
return { fr: '', ar: '' }
|
||||
}
|
||||
|
||||
export function bilingualPrimary(field: BilingualField): string {
|
||||
return field.fr || field.ar
|
||||
}
|
||||
|
||||
export function BilingualInput({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
required = false,
|
||||
maxLength,
|
||||
}: {
|
||||
label: string
|
||||
value: BilingualField
|
||||
onChange: (value: BilingualField) => void
|
||||
required?: boolean
|
||||
maxLength?: number
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<span className="mb-1.5 block text-sm font-medium text-slate-700">
|
||||
{label}
|
||||
{required ? <span className="ml-1 text-red-600">*</span> : null}
|
||||
</span>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<div className="relative">
|
||||
<span className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 rounded bg-blue-50 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-blue-500">
|
||||
FR
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
dir="ltr"
|
||||
lang="fr"
|
||||
maxLength={maxLength}
|
||||
placeholder="Français…"
|
||||
value={value.fr}
|
||||
onChange={(e) => onChange({ ...value, fr: e.target.value })}
|
||||
className="input-field pl-11"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<span className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-emerald-600">
|
||||
AR
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
dir="rtl"
|
||||
lang="ar"
|
||||
maxLength={maxLength}
|
||||
placeholder="بالعربية…"
|
||||
value={value.ar}
|
||||
onChange={(e) => onChange({ ...value, ar: e.target.value })}
|
||||
className="input-field pr-11 text-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user