dashboard fix

This commit is contained in:
root
2026-05-19 21:25:38 -04:00
parent 7ac0098c7f
commit d4f7de5762
27 changed files with 705 additions and 336 deletions
+34 -6
View File
@@ -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>
)
}