fix notification and add billing page and contract

This commit is contained in:
root
2026-05-13 00:09:39 -04:00
committed by Administrator
parent 1a39aa8433
commit 89621a163b
52 changed files with 5631 additions and 1110 deletions
+93 -31
View File
@@ -1,9 +1,10 @@
'use client'
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { SHARED_LANGUAGE_COOKIE, SHARED_LANGUAGE_KEY, SHARED_THEME_KEY, readCurrentUserScopedPreference, readScopedPreference, writeScopedPreference } from '@/lib/preferences'
export type DashboardLanguage = 'en' | 'fr' | 'ar'
export type DashboardTheme = 'light' | 'dark'
export type DashboardTheme = 'light' | 'medium' | 'dark'
type FleetDict = {
statusLabels: Record<'AVAILABLE' | 'RENTED' | 'MAINTENANCE' | 'OUT_OF_SERVICE', string>
@@ -294,6 +295,7 @@ type DashboardDictionary = {
language: string
theme: string
light: string
medium: string
dark: string
fleet: FleetDict
vehicleDetail: VehicleDetailDict
@@ -307,26 +309,31 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
nav: {
dashboard: 'Dashboard',
fleet: 'Fleet',
reservations: 'Reservations',
reservations: 'Booking',
onlineReservations: 'Online Reservations',
customers: 'Customers',
offers: 'Offers',
team: 'Team',
reports: 'Reports',
billing: 'Billing',
subscription: 'Subscription',
billing: 'Rent Car Billing',
contracts: 'Contracts',
notifications: 'Notifications',
settings: 'Settings',
},
titles: {
'/dashboard': 'Dashboard',
'/dashboard/fleet': 'Fleet Management',
'/dashboard/reservations': 'Reservations',
'/dashboard/reservations': 'Booking',
'/dashboard/reservations/new': 'Book Car',
'/dashboard/online-reservations': 'Online Reservations',
'/dashboard/customers': 'Customers',
'/dashboard/offers': 'Offers',
'/dashboard/team': 'Team',
'/dashboard/reports': 'Reports',
'/dashboard/billing': 'Billing',
'/dashboard/subscription': 'Subscription',
'/dashboard/billing': 'Rent Car Billing',
'/dashboard/contracts': 'Contracts',
'/dashboard/notifications': 'Notifications',
'/dashboard/settings': 'Settings',
},
@@ -339,6 +346,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
language: 'Language',
theme: 'Theme',
light: 'Light',
medium: 'Medium',
dark: 'Dark',
fleet: {
statusLabels: { AVAILABLE: 'Available', RENTED: 'Rented', MAINTENANCE: 'Maintenance', OUT_OF_SERVICE: 'Out of Service' },
@@ -572,11 +580,11 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
trialActive: "You're on a free trial.",
pastDueMsg: 'Your payment is past due. Please update your billing information.',
suspendedMsg: 'Your account has been suspended. Please contact support or renew your subscription.',
manageBilling: 'Manage billing →',
manageBilling: 'Manage subscription →',
statusLabels: { CONFIRMED: 'Confirmed', PENDING: 'Pending', ACTIVE: 'Active', COMPLETED: 'Completed', CANCELLED: 'Cancelled' },
},
reservations: {
heading: 'Reservations',
heading: 'Booking',
subtitle: 'All booking sources, including dashboard, public site, and marketplace.',
colCustomer: 'Customer',
colVehicle: 'Vehicle',
@@ -650,7 +658,9 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
offers: 'Offres',
team: 'Équipe',
reports: 'Rapports',
billing: 'Facturation',
subscription: 'Abonnement',
billing: 'Facturation location',
contracts: 'Contrats',
notifications: 'Notifications',
settings: 'Paramètres',
},
@@ -658,12 +668,15 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
'/dashboard': 'Tableau de bord',
'/dashboard/fleet': 'Gestion de flotte',
'/dashboard/reservations': 'Réservations',
'/dashboard/reservations/new': 'Réserver une voiture',
'/dashboard/online-reservations': 'Réservations en ligne',
'/dashboard/customers': 'Clients',
'/dashboard/offers': 'Offres',
'/dashboard/team': 'Équipe',
'/dashboard/reports': 'Rapports',
'/dashboard/billing': 'Facturation',
'/dashboard/subscription': 'Abonnement',
'/dashboard/billing': 'Facturation location',
'/dashboard/contracts': 'Contrats',
'/dashboard/notifications': 'Notifications',
'/dashboard/settings': 'Paramètres',
},
@@ -676,6 +689,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
language: 'Langue',
theme: 'Mode',
light: 'Clair',
medium: 'Moyen',
dark: 'Sombre',
fleet: {
statusLabels: { AVAILABLE: 'Disponible', RENTED: 'Loué', MAINTENANCE: 'Maintenance', OUT_OF_SERVICE: 'Hors service' },
@@ -909,7 +923,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
trialActive: "Vous êtes en période d'essai.",
pastDueMsg: 'Votre paiement est en retard. Veuillez mettre à jour vos informations de facturation.',
suspendedMsg: 'Votre compte a été suspendu. Veuillez contacter le support ou renouveler votre abonnement.',
manageBilling: 'Gérer la facturation →',
manageBilling: 'Gérer labonnement →',
statusLabels: { CONFIRMED: 'Confirmé', PENDING: 'En attente', ACTIVE: 'Actif', COMPLETED: 'Terminé', CANCELLED: 'Annulé' },
},
reservations: {
@@ -987,7 +1001,9 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
offers: 'العروض',
team: 'الفريق',
reports: 'التقارير',
billing: 'الفوترة',
subscription: 'الاشتراك',
billing: 'فوترة تأجير السيارات',
contracts: 'العقود',
notifications: 'الإشعارات',
settings: 'الإعدادات',
},
@@ -995,12 +1011,15 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
'/dashboard': 'لوحة التحكم',
'/dashboard/fleet': 'إدارة الأسطول',
'/dashboard/reservations': 'الحجوزات',
'/dashboard/reservations/new': 'حجز سيارة',
'/dashboard/online-reservations': 'الحجوزات الإلكترونية',
'/dashboard/customers': 'العملاء',
'/dashboard/offers': 'العروض',
'/dashboard/team': 'الفريق',
'/dashboard/reports': 'التقارير',
'/dashboard/billing': 'الفوترة',
'/dashboard/subscription': 'الاشتراك',
'/dashboard/billing': 'فوترة تأجير السيارات',
'/dashboard/contracts': 'العقود',
'/dashboard/notifications': 'الإشعارات',
'/dashboard/settings': 'الإعدادات',
},
@@ -1013,6 +1032,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
language: 'اللغة',
theme: 'الوضع',
light: 'فاتح',
medium: 'متوسط',
dark: 'داكن',
fleet: {
statusLabels: { AVAILABLE: 'متاح', RENTED: 'مؤجر', MAINTENANCE: 'صيانة', OUT_OF_SERVICE: 'خارج الخدمة' },
@@ -1246,7 +1266,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
trialActive: 'أنت في فترة تجريبية مجانية.',
pastDueMsg: 'دفعتك متأخرة. يرجى تحديث معلومات الفوترة.',
suspendedMsg: 'تم تعليق حسابك. يرجى التواصل مع الدعم أو تجديد اشتراكك.',
manageBilling: 'إدارة الفوترة ←',
manageBilling: 'إدارة الاشتراك ←',
statusLabels: { CONFIRMED: 'مؤكد', PENDING: 'قيد الانتظار', ACTIVE: 'نشط', COMPLETED: 'مكتمل', CANCELLED: 'ملغى' },
},
reservations: {
@@ -1325,25 +1345,38 @@ type I18nContextValue = {
}
const I18nContext = createContext<I18nContextValue | null>(null)
const DASHBOARD_LANGUAGE_KEY = 'dashboard-language'
export function DashboardI18nProvider({ children }: { children: React.ReactNode }) {
const [language, setLanguage] = useState<DashboardLanguage>('en')
function normalizeLanguage(value: string | null | undefined): DashboardLanguage {
return value === 'fr' || value === 'ar' || value === 'en' ? value : 'en'
}
export function DashboardI18nProvider({
children,
initialLanguage = 'en',
}: {
children: React.ReactNode
initialLanguage?: DashboardLanguage
}) {
const [language, setLanguage] = useState<DashboardLanguage>(initialLanguage)
const [theme, setTheme] = useState<DashboardTheme>('light')
const themeInitialized = useRef(false)
useEffect(() => {
const stored = window.localStorage.getItem('dashboard-language')
const storedTheme = window.localStorage.getItem('dashboard-theme')
const stored = readScopedPreference(SHARED_LANGUAGE_KEY, [DASHBOARD_LANGUAGE_KEY])
if (stored === null) return
const next = normalizeLanguage(stored)
if (next !== language) setLanguage(next)
}, [])
if (stored === 'en' || stored === 'fr' || stored === 'ar') {
setLanguage(stored)
}
if (storedTheme === 'light' || storedTheme === 'dark') {
setTheme(storedTheme)
useEffect(() => {
const storedTheme = readScopedPreference(SHARED_THEME_KEY, ['dashboard-theme'])
if (storedTheme === 'light' || storedTheme === 'medium' || storedTheme === 'dark') {
if (storedTheme !== theme) setTheme(storedTheme)
return
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
if (window.matchMedia('(prefers-color-scheme: dark)').matches && theme !== 'dark') {
setTheme('dark')
}
}, [])
@@ -1351,17 +1384,46 @@ export function DashboardI18nProvider({ children }: { children: React.ReactNode
useEffect(() => {
document.documentElement.lang = language
document.documentElement.dir = language === 'ar' ? 'rtl' : 'ltr'
window.localStorage.setItem('dashboard-language', language)
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`
}, [language])
useEffect(() => {
if (!themeInitialized.current) {
// Skip first run: inline script already applied the correct class; writing 'light' here would flash and corrupt storage.
themeInitialized.current = true
return
}
document.documentElement.classList.remove('light', 'dark')
document.documentElement.classList.add(theme)
document.documentElement.style.colorScheme = theme
document.documentElement.classList.add(theme === 'light' ? 'light' : 'dark')
document.documentElement.style.colorScheme = theme === 'light' ? 'light' : 'dark'
document.body.dataset.theme = theme
window.localStorage.setItem('dashboard-theme', theme)
writeScopedPreference(SHARED_THEME_KEY, theme, ['dashboard-theme'])
}, [theme])
useEffect(() => {
function handleAuthChanged() {
const scopedLanguage = readCurrentUserScopedPreference(SHARED_LANGUAGE_KEY)
const scopedTheme = readCurrentUserScopedPreference(SHARED_THEME_KEY)
if (scopedLanguage && normalizeLanguage(scopedLanguage) !== language) {
setLanguage(normalizeLanguage(scopedLanguage))
} else {
writeScopedPreference(SHARED_LANGUAGE_KEY, language, [DASHBOARD_LANGUAGE_KEY])
}
if (scopedTheme === 'light' || scopedTheme === 'medium' || scopedTheme === 'dark') {
if (scopedTheme !== theme) setTheme(scopedTheme)
} else {
writeScopedPreference(SHARED_THEME_KEY, theme, ['dashboard-theme'])
}
}
window.addEventListener('rentaldrivego:auth-changed', handleAuthChanged as EventListener)
return () => window.removeEventListener('rentaldrivego:auth-changed', handleAuthChanged as EventListener)
}, [language, theme])
const value = useMemo(
() => ({ language, setLanguage, theme, setTheme, dict: dictionaries[language] }),
[language, theme],
@@ -1426,7 +1488,7 @@ export function DashboardThemeSwitcher() {
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">
{dict.theme}
</span>
{(['light', 'dark'] as DashboardTheme[]).map((value) => {
{(['light', 'medium', 'dark'] as DashboardTheme[]).map((value) => {
const active = value === theme
return (
<button
@@ -1439,7 +1501,7 @@ export function DashboardThemeSwitcher() {
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
}`}
>
{value === 'light' ? dict.light : dict.dark}
{value === 'light' ? dict.light : value === 'medium' ? dict.medium : dict.dark}
</button>
)
})}