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
@@ -1,6 +1,7 @@
'use client'
import Image from 'next/image'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useDashboardI18n } from '@/components/I18nProvider'
@@ -16,19 +17,14 @@ function notifyParent(message: Record<string, unknown>) {
export default function SignInPage() {
const { language, theme, setLanguage, setTheme } = useDashboardI18n()
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const requestedPortal = searchParams.get('portal') === 'admin' ? 'admin' : 'workspace'
const [portal, setPortal] = useState<'admin' | 'workspace'>(requestedPortal)
const requestedLanguage = searchParams.get('lang')
const requestedTheme = searchParams.get('theme')
const dict = {
en: {
title: 'Sign in',
workspaceTab: 'Company Workspace',
platformTab: 'Platform',
subtitle: 'Choose where you want to sign in, then enter your credentials.',
subtitle: 'Enter your credentials to access your account.',
email: 'Email',
password: 'Password',
signIn: 'Sign in',
@@ -39,38 +35,30 @@ export default function SignInPage() {
enterCode: 'Enter the 6-digit code from your authenticator app.',
back: 'Back to credentials',
forgotPassword: 'Forgot your password?',
newAccount: 'New company account?',
createWorkspace: 'Create your workspace',
invalidCredentials: 'Invalid email or password.',
passwordNotSet: 'No password set yet. Check your invitation email or use "Forgot your password?"',
unexpectedError: 'Something went wrong. Please try again.',
},
fr: {
title: 'Connexion',
workspaceTab: 'Espace',
platformTab: 'Plateforme',
subtitle: 'Choisissez votre accès puis saisissez vos identifiants.',
subtitle: 'Saisissez vos identifiants pour accéder à votre compte.',
email: 'Email',
password: 'Mot de passe',
signIn: 'Connexion',
signingIn: 'Connexion…',
verify: 'Vérifier le code',
verifying: 'Vérification…',
authCode: 'Code dauthentification',
enterCode: 'Entrez le code à 6 chiffres de votre application dauthentification.',
authCode: 'Code d\'authentification',
enterCode: 'Entrez le code à 6 chiffres de votre application d\'authentification.',
back: 'Retour aux identifiants',
forgotPassword: 'Mot de passe oublié ?',
newAccount: 'Nouveau compte entreprise ?',
createWorkspace: 'Créer votre espace',
invalidCredentials: 'Email ou mot de passe invalide.',
passwordNotSet: 'Aucun mot de passe défini. Vérifiez votre e-mail d\'invitation ou utilisez « Mot de passe oublié ? »',
unexpectedError: 'Une erreur est survenue. Veuillez réessayer.',
},
ar: {
title: 'تسجيل الدخول',
workspaceTab: 'المساحة',
platformTab: 'المنصة',
subtitle: 'اختر جهة الدخول ثم أدخل بياناتك.',
subtitle: 'أدخل بياناتك للوصول إلى حسابك.',
email: 'البريد الإلكتروني',
password: 'كلمة المرور',
signIn: 'تسجيل الدخول',
@@ -81,8 +69,6 @@ export default function SignInPage() {
enterCode: 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة.',
back: 'العودة إلى بيانات الدخول',
forgotPassword: 'نسيت كلمة المرور؟',
newAccount: 'حساب شركة جديد؟',
createWorkspace: 'أنشئ مساحتك',
invalidCredentials: 'البريد الإلكتروني أو كلمة المرور غير صحيحة.',
passwordNotSet: 'لم يتم تعيين كلمة مرور بعد. تحقق من بريد الدعوة أو استخدم "نسيت كلمة المرور؟"',
unexpectedError: 'حدث خطأ ما. يرجى المحاولة مرة أخرى.',
@@ -96,9 +82,6 @@ export default function SignInPage() {
title: 'text-slate-900',
subtitle: 'text-slate-500',
card: 'border-slate-200 bg-white shadow-sm',
toggleWrap: 'border-slate-200 bg-slate-50',
activeToggle: 'bg-slate-900 text-white shadow-sm',
inactiveToggle: 'text-slate-600 hover:bg-white hover:text-slate-900',
},
medium: {
main: 'bg-[radial-gradient(circle_at_top,rgba(148,163,184,0.32),transparent_35%),linear-gradient(180deg,#d7dee8,#f3f4f6)]',
@@ -107,9 +90,6 @@ export default function SignInPage() {
title: 'text-slate-900',
subtitle: 'text-slate-600',
card: 'border-slate-300/80 bg-white/88 shadow-[0_18px_50px_rgba(51,65,85,0.14)] backdrop-blur',
toggleWrap: 'border-slate-300 bg-slate-200/80',
activeToggle: 'bg-amber-400 text-slate-950 shadow-sm',
inactiveToggle: 'text-slate-700 hover:bg-white/85 hover:text-slate-950',
},
dark: {
main: 'bg-[radial-gradient(circle_at_top,rgba(37,99,235,0.2),transparent_35%),linear-gradient(180deg,#020617,#0f172a)]',
@@ -118,16 +98,9 @@ export default function SignInPage() {
title: 'text-slate-100',
subtitle: 'text-slate-400',
card: 'border-slate-800 bg-slate-900/92 shadow-[0_18px_50px_rgba(2,6,23,0.45)] backdrop-blur',
toggleWrap: 'border-slate-700 bg-slate-950/80',
activeToggle: 'bg-amber-400 text-slate-950 shadow-sm',
inactiveToggle: 'text-slate-300 hover:bg-slate-800 hover:text-white',
},
}[theme]
useEffect(() => {
setPortal(requestedPortal)
}, [requestedPortal])
useEffect(() => {
if (
(requestedLanguage === 'en' || requestedLanguage === 'fr' || requestedLanguage === 'ar') &&
@@ -151,20 +124,6 @@ export default function SignInPage() {
notifyParent({ type: 'rentaldrivego:embedded-path', path: currentPath })
}, [pathname, searchParams])
function handlePortalChange(nextPortal: 'admin' | 'workspace') {
setPortal(nextPortal)
const nextParams = new URLSearchParams(searchParams.toString())
if (nextPortal === 'admin') {
nextParams.set('portal', 'admin')
} else {
nextParams.delete('portal')
}
const nextSearch = nextParams.toString()
router.replace(nextSearch ? `${pathname}?${nextSearch}` : pathname, { scroll: false })
}
return (
<main className={`flex min-h-screen items-center justify-center px-4 py-12 transition-colors ${themeStyles.main}`}>
<div className="w-full max-w-md">
@@ -190,37 +149,14 @@ export default function SignInPage() {
</div>
<section className={`rounded-[2rem] border p-8 transition-colors ${themeStyles.card}`}>
<div className={`mb-6 grid grid-cols-2 gap-2 rounded-full border p-1 transition-colors ${themeStyles.toggleWrap}`}>
{([
{ value: 'workspace', label: dict.workspaceTab },
{ value: 'admin', label: dict.platformTab },
] as const).map((option) => {
const active = option.value === portal
return (
<button
key={option.value}
type="button"
onClick={() => handlePortalChange(option.value)}
className={`rounded-full px-4 py-2 text-sm font-semibold transition ${
active
? themeStyles.activeToggle
: themeStyles.inactiveToggle
}`}
>
{option.label}
</button>
)
})}
</div>
<LocalSignInForm dict={dict} portal={portal} />
<LocalSignInForm dict={dict} />
</section>
</div>
</main>
)
}
function LocalSignInForm({ dict, portal }: {
function LocalSignInForm({ dict }: {
dict: {
email: string
password: string
@@ -236,10 +172,10 @@ function LocalSignInForm({ dict, portal }: {
passwordNotSet: string
unexpectedError: string
}
portal: 'admin' | 'workspace'
}) {
const router = useRouter()
const searchParams = useSearchParams()
const { setLanguage } = useDashboardI18n()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [totpCode, setTotpCode] = useState('')
@@ -249,13 +185,6 @@ function LocalSignInForm({ dict, portal }: {
const [error, setError] = useState<string | null>(null)
const adminNext = searchParams.get('next') || '/dashboard'
const employeeRedirect = searchParams.get('redirect') || '/dashboard'
const forgotPasswordHref = portal === 'admin' ? `${adminUrl}/forgot-password` : '/forgot-password'
useEffect(() => {
setStep('credentials')
setTotpCode('')
setError(null)
}, [portal])
function redirectAdmin(token: string) {
const hash = new URLSearchParams({ token, next: adminNext }).toString()
@@ -268,28 +197,7 @@ function LocalSignInForm({ dict, portal }: {
setError(null)
try {
if (portal === 'admin') {
const adminRes = await fetch(`${API_BASE}/admin/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const adminJson = await adminRes.json()
if (adminRes.ok && adminJson?.data?.token) {
redirectAdmin(adminJson.data.token)
return
}
if (adminRes.status === 401 && adminJson?.error === 'totp_required') {
setStep('totp')
return
}
setError(dict.invalidCredentials)
return
}
// Try employee login first
const empRes = await fetch(`${API_BASE}/auth/employee/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -302,6 +210,13 @@ function LocalSignInForm({ dict, portal }: {
localStorage.setItem(EMPLOYEE_TOKEN_KEY, token)
if (empJson?.data?.employee) {
localStorage.setItem(EMPLOYEE_PROFILE_KEY, JSON.stringify(empJson.data.employee))
// Apply the employee's stored language preference immediately so the
// dashboard renders in the correct language before any read-effect runs.
const prefLang = empJson.data.employee?.preferredLanguage
if (prefLang === 'en' || prefLang === 'fr' || prefLang === 'ar') {
setLanguage(prefLang)
document.cookie = `rentaldrivego-language=${prefLang}; path=/; max-age=31536000; samesite=lax`
}
}
document.cookie = `employee_token=${token}; path=/; max-age=28800; samesite=lax`
window.dispatchEvent(new CustomEvent('rentaldrivego:auth-changed'))
@@ -315,6 +230,24 @@ function LocalSignInForm({ dict, portal }: {
return
}
// Fall back to admin login
const adminRes = await fetch(`${API_BASE}/admin/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const adminJson = await adminRes.json()
if (adminRes.ok && adminJson?.data?.token) {
redirectAdmin(adminJson.data.token)
return
}
if (adminRes.status === 401 && adminJson?.error === 'totp_required') {
setStep('totp')
return
}
setError(dict.invalidCredentials)
} catch {
setError(dict.unexpectedError)
@@ -411,9 +344,9 @@ function LocalSignInForm({ dict, portal }: {
</button>
<div className="text-center">
<a href={forgotPasswordHref} className="text-sm text-slate-500 hover:text-slate-700 underline decoration-slate-300 underline-offset-4">
<Link href="/forgot-password" className="text-sm text-slate-500 hover:text-slate-700 underline decoration-slate-300 underline-offset-4">
{dict.forgotPassword}
</a>
</Link>
</div>
</form>
) : (