dashboard fix
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Calendar, Car, Users, DollarSign, AlertTriangle, Clock, XCircle } from 'lucide-react'
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'
|
||||
@@ -93,9 +94,9 @@ function SubscriptionBanner({
|
||||
<div className={`flex items-center gap-3 px-4 py-3 rounded-xl border mb-6 ${config.bg}`}>
|
||||
{config.icon}
|
||||
<p className="text-sm font-medium">{config.message}</p>
|
||||
<a href="/dashboard/subscription" className="ml-auto text-sm font-semibold underline hover:no-underline">
|
||||
<Link href="/dashboard/subscription" className="ml-auto text-sm font-semibold underline hover:no-underline">
|
||||
{manageBillingLabel}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -243,9 +244,9 @@ export default function DashboardPage() {
|
||||
<div className="card">
|
||||
<div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
|
||||
<h2 className="text-base font-semibold text-slate-900">{d.recentReservations}</h2>
|
||||
<a href="/dashboard/reservations" className="text-sm text-blue-600 hover:text-blue-700 font-medium">
|
||||
<Link href="/dashboard/reservations" className="text-sm text-blue-600 hover:text-blue-700 font-medium">
|
||||
{d.viewAll}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
import { useDashboardI18n } from '@/components/I18nProvider'
|
||||
import { BilingualField, BilingualInput, bilingualPrimary, emptyBilingual } from '@/components/ui/BilingualInput'
|
||||
|
||||
type Customer = {
|
||||
id: string
|
||||
@@ -19,15 +20,15 @@ type Customer = {
|
||||
type Vehicle = { id: string; make: string; model: string; licensePlate: string; status: string }
|
||||
type CreatedCustomer = { id: string; firstName: string; lastName: string; email: string }
|
||||
type AdditionalDriverForm = {
|
||||
firstName: string
|
||||
lastName: string
|
||||
firstName: BilingualField
|
||||
lastName: BilingualField
|
||||
email: string
|
||||
phone: string
|
||||
driverLicense: string
|
||||
licenseExpiry: string
|
||||
licenseIssuedAt: string
|
||||
dateOfBirth: string
|
||||
nationality: string
|
||||
nationality: BilingualField
|
||||
}
|
||||
|
||||
type CreatedReservation = { id: string }
|
||||
@@ -55,8 +56,8 @@ export default function NewReservationPage() {
|
||||
const [showAddCustomer, setShowAddCustomer] = useState(false)
|
||||
const [savingCustomer, setSavingCustomer] = useState(false)
|
||||
const [includeAdditionalDriver, setIncludeAdditionalDriver] = useState(false)
|
||||
const [newCustomerFirstName, setNewCustomerFirstName] = useState('')
|
||||
const [newCustomerLastName, setNewCustomerLastName] = useState('')
|
||||
const [newCustomerFirstName, setNewCustomerFirstName] = useState<BilingualField>(emptyBilingual())
|
||||
const [newCustomerLastName, setNewCustomerLastName] = useState<BilingualField>(emptyBilingual())
|
||||
const [newCustomerEmail, setNewCustomerEmail] = useState('')
|
||||
const [newCustomerPhone, setNewCustomerPhone] = useState('')
|
||||
const [driverLicense, setDriverLicense] = useState('')
|
||||
@@ -65,15 +66,15 @@ export default function NewReservationPage() {
|
||||
const [licenseCountry, setLicenseCountry] = useState('')
|
||||
const [licenseCategory, setLicenseCategory] = useState('')
|
||||
const [additionalDriver, setAdditionalDriver] = useState<AdditionalDriverForm>({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
firstName: emptyBilingual(),
|
||||
lastName: emptyBilingual(),
|
||||
email: '',
|
||||
phone: '',
|
||||
driverLicense: '',
|
||||
licenseExpiry: '',
|
||||
licenseIssuedAt: '',
|
||||
dateOfBirth: '',
|
||||
nationality: '',
|
||||
nationality: emptyBilingual(),
|
||||
})
|
||||
|
||||
const copy = {
|
||||
@@ -113,7 +114,7 @@ export default function NewReservationPage() {
|
||||
firstName: 'First name',
|
||||
lastName: 'Last name',
|
||||
email: 'Email',
|
||||
phone: 'Phone (optional)',
|
||||
phone: 'Phone',
|
||||
saveCustomer: 'Save customer',
|
||||
savingCustomer: 'Saving customer…',
|
||||
selectVehicle: 'Select vehicle…',
|
||||
@@ -162,7 +163,7 @@ export default function NewReservationPage() {
|
||||
firstName: 'Prénom',
|
||||
lastName: 'Nom',
|
||||
email: 'Email',
|
||||
phone: 'Téléphone (optionnel)',
|
||||
phone: 'Téléphone',
|
||||
saveCustomer: 'Enregistrer le client',
|
||||
savingCustomer: 'Enregistrement du client…',
|
||||
selectVehicle: 'Sélectionner un véhicule…',
|
||||
@@ -211,7 +212,7 @@ export default function NewReservationPage() {
|
||||
firstName: 'الاسم الأول',
|
||||
lastName: 'اسم العائلة',
|
||||
email: 'البريد الإلكتروني',
|
||||
phone: 'الهاتف (اختياري)',
|
||||
phone: 'الهاتف',
|
||||
saveCustomer: 'حفظ العميل',
|
||||
savingCustomer: 'جارٍ حفظ العميل…',
|
||||
selectVehicle: 'اختر مركبة…',
|
||||
@@ -264,7 +265,7 @@ export default function NewReservationPage() {
|
||||
|
||||
async function addCustomer() {
|
||||
setError(null)
|
||||
if (!newCustomerFirstName.trim() || !newCustomerLastName.trim() || !newCustomerEmail.trim()) {
|
||||
if (!bilingualPrimary(newCustomerFirstName).trim() || !bilingualPrimary(newCustomerLastName).trim() || !newCustomerEmail.trim() || !newCustomerPhone.trim()) {
|
||||
setError(copy.required)
|
||||
return
|
||||
}
|
||||
@@ -273,10 +274,12 @@ export default function NewReservationPage() {
|
||||
const created = await apiFetch<CreatedCustomer>('/customers', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
firstName: newCustomerFirstName.trim(),
|
||||
lastName: newCustomerLastName.trim(),
|
||||
firstName: bilingualPrimary(newCustomerFirstName).trim(),
|
||||
firstNameAr: newCustomerFirstName.ar.trim() || undefined,
|
||||
lastName: bilingualPrimary(newCustomerLastName).trim(),
|
||||
lastNameAr: newCustomerLastName.ar.trim() || undefined,
|
||||
email: newCustomerEmail.trim(),
|
||||
phone: newCustomerPhone.trim() || undefined,
|
||||
phone: newCustomerPhone.trim(),
|
||||
driverLicense: driverLicense.trim() || undefined,
|
||||
licenseExpiry: licenseExpiry ? new Date(licenseExpiry).toISOString() : undefined,
|
||||
licenseIssuedAt: licenseIssuedAt ? new Date(licenseIssuedAt).toISOString() : undefined,
|
||||
@@ -286,10 +289,10 @@ export default function NewReservationPage() {
|
||||
})
|
||||
setCustomers((prev) => [created, ...prev])
|
||||
setCustomerId(created.id)
|
||||
setCustomerSearch(`${created.firstName} ${created.lastName}`)
|
||||
setCustomerSearch(`${bilingualPrimary(newCustomerFirstName)} ${bilingualPrimary(newCustomerLastName)}`)
|
||||
setShowAddCustomer(false)
|
||||
setNewCustomerFirstName('')
|
||||
setNewCustomerLastName('')
|
||||
setNewCustomerFirstName(emptyBilingual())
|
||||
setNewCustomerLastName(emptyBilingual())
|
||||
setNewCustomerEmail('')
|
||||
setNewCustomerPhone('')
|
||||
} catch (err: any) {
|
||||
@@ -311,7 +314,7 @@ export default function NewReservationPage() {
|
||||
setError(copy.invalidDates)
|
||||
return
|
||||
}
|
||||
if (includeAdditionalDriver && (!additionalDriver.firstName.trim() || !additionalDriver.lastName.trim() || !additionalDriver.driverLicense.trim())) {
|
||||
if (includeAdditionalDriver && (!bilingualPrimary(additionalDriver.firstName).trim() || !bilingualPrimary(additionalDriver.lastName).trim() || !additionalDriver.driverLicense.trim())) {
|
||||
setError(copy.required)
|
||||
return
|
||||
}
|
||||
@@ -341,15 +344,18 @@ export default function NewReservationPage() {
|
||||
depositAmount: Number.isFinite(Number(depositAmount)) ? Math.max(0, Math.round(Number(depositAmount))) : 0,
|
||||
paymentMode,
|
||||
additionalDrivers: includeAdditionalDriver ? [{
|
||||
firstName: additionalDriver.firstName.trim(),
|
||||
lastName: additionalDriver.lastName.trim(),
|
||||
firstName: bilingualPrimary(additionalDriver.firstName).trim(),
|
||||
firstNameAr: additionalDriver.firstName.ar.trim() || undefined,
|
||||
lastName: bilingualPrimary(additionalDriver.lastName).trim(),
|
||||
lastNameAr: additionalDriver.lastName.ar.trim() || undefined,
|
||||
email: additionalDriver.email.trim() || undefined,
|
||||
phone: additionalDriver.phone.trim() || undefined,
|
||||
driverLicense: additionalDriver.driverLicense.trim(),
|
||||
licenseExpiry: additionalDriver.licenseExpiry ? new Date(additionalDriver.licenseExpiry).toISOString() : undefined,
|
||||
licenseIssuedAt: additionalDriver.licenseIssuedAt ? new Date(additionalDriver.licenseIssuedAt).toISOString() : undefined,
|
||||
dateOfBirth: additionalDriver.dateOfBirth ? new Date(additionalDriver.dateOfBirth).toISOString() : undefined,
|
||||
nationality: additionalDriver.nationality.trim() || undefined,
|
||||
nationality: bilingualPrimary(additionalDriver.nationality).trim() || undefined,
|
||||
nationalityAr: additionalDriver.nationality.ar.trim() || undefined,
|
||||
}] : [],
|
||||
notes: notes || undefined,
|
||||
}),
|
||||
@@ -380,7 +386,7 @@ export default function NewReservationPage() {
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label className="space-y-1">
|
||||
<span className="text-sm font-medium text-slate-700">{copy.customer}</span>
|
||||
<span className="text-sm font-medium text-slate-700">{copy.customer} <span className="text-red-600">*</span></span>
|
||||
<input
|
||||
value={customerSearch}
|
||||
onChange={(e) => setCustomerSearch(e.target.value)}
|
||||
@@ -404,10 +410,16 @@ export default function NewReservationPage() {
|
||||
{showAddCustomer ? (
|
||||
<div className="mt-2 rounded-lg border border-slate-200 p-3 space-y-2 bg-slate-50">
|
||||
<p className="text-xs font-semibold text-slate-700">{copy.addCustomerTitle}</p>
|
||||
<input value={newCustomerFirstName} onChange={(e) => setNewCustomerFirstName(e.target.value)} placeholder={copy.firstName} className="input-field" />
|
||||
<input value={newCustomerLastName} onChange={(e) => setNewCustomerLastName(e.target.value)} placeholder={copy.lastName} className="input-field" />
|
||||
<input value={newCustomerEmail} onChange={(e) => setNewCustomerEmail(e.target.value)} placeholder={copy.email} className="input-field" />
|
||||
<input value={newCustomerPhone} onChange={(e) => setNewCustomerPhone(e.target.value)} placeholder={copy.phone} className="input-field" />
|
||||
<BilingualInput label={copy.firstName} required value={newCustomerFirstName} onChange={setNewCustomerFirstName} />
|
||||
<BilingualInput label={copy.lastName} required value={newCustomerLastName} onChange={setNewCustomerLastName} />
|
||||
<div className="space-y-1">
|
||||
<span className="text-sm font-medium text-slate-700">{copy.email} <span className="text-red-600">*</span></span>
|
||||
<input value={newCustomerEmail} onChange={(e) => setNewCustomerEmail(e.target.value)} className="input-field" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-sm font-medium text-slate-700">{copy.phone} <span className="text-red-600">*</span></span>
|
||||
<input required value={newCustomerPhone} onChange={(e) => setNewCustomerPhone(e.target.value)} className="input-field" />
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button type="button" className="btn-secondary" onClick={addCustomer} disabled={savingCustomer}>
|
||||
{savingCustomer ? copy.savingCustomer : copy.saveCustomer}
|
||||
@@ -418,7 +430,7 @@ export default function NewReservationPage() {
|
||||
</label>
|
||||
|
||||
<label className="space-y-1">
|
||||
<span className="text-sm font-medium text-slate-700">{copy.vehicle}</span>
|
||||
<span className="text-sm font-medium text-slate-700">{copy.vehicle} <span className="text-red-600">*</span></span>
|
||||
<select value={vehicleId} onChange={(e) => setVehicleId(e.target.value)} className="input-field">
|
||||
<option value="">{copy.selectVehicle}</option>
|
||||
{availableVehicles.map((v) => (
|
||||
@@ -469,11 +481,11 @@ export default function NewReservationPage() {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label className="space-y-1">
|
||||
<span className="text-sm font-medium text-slate-700">{copy.startDate}</span>
|
||||
<span className="text-sm font-medium text-slate-700">{copy.startDate} <span className="text-red-600">*</span></span>
|
||||
<input type="datetime-local" value={startDate} onChange={(e) => setStartDate(e.target.value)} className="input-field" />
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-sm font-medium text-slate-700">{copy.endDate}</span>
|
||||
<span className="text-sm font-medium text-slate-700">{copy.endDate} <span className="text-red-600">*</span></span>
|
||||
<input type="datetime-local" value={endDate} onChange={(e) => setEndDate(e.target.value)} className="input-field" />
|
||||
</label>
|
||||
</div>
|
||||
@@ -517,24 +529,8 @@ export default function NewReservationPage() {
|
||||
{includeAdditionalDriver ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm font-semibold text-slate-900">{copy.additionalDriverInfo}</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label className="space-y-1">
|
||||
<span className="text-sm font-medium text-slate-700">{copy.firstName}</span>
|
||||
<input
|
||||
value={additionalDriver.firstName}
|
||||
onChange={(e) => setAdditionalDriver((current) => ({ ...current, firstName: e.target.value }))}
|
||||
className="input-field"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-sm font-medium text-slate-700">{copy.lastName}</span>
|
||||
<input
|
||||
value={additionalDriver.lastName}
|
||||
onChange={(e) => setAdditionalDriver((current) => ({ ...current, lastName: e.target.value }))}
|
||||
className="input-field"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<BilingualInput label={copy.firstName} required value={additionalDriver.firstName} onChange={(v) => setAdditionalDriver((cur) => ({ ...cur, firstName: v }))} />
|
||||
<BilingualInput label={copy.lastName} required value={additionalDriver.lastName} onChange={(v) => setAdditionalDriver((cur) => ({ ...cur, lastName: v }))} />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label className="space-y-1">
|
||||
@@ -557,21 +553,16 @@ export default function NewReservationPage() {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label className="space-y-1">
|
||||
<span className="text-sm font-medium text-slate-700">{copy.driverLicense}</span>
|
||||
<span className="text-sm font-medium text-slate-700">{copy.driverLicense} <span className="text-red-600">*</span></span>
|
||||
<input
|
||||
value={additionalDriver.driverLicense}
|
||||
onChange={(e) => setAdditionalDriver((current) => ({ ...current, driverLicense: e.target.value }))}
|
||||
className="input-field"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-sm font-medium text-slate-700">{copy.nationality}</span>
|
||||
<input
|
||||
value={additionalDriver.nationality}
|
||||
onChange={(e) => setAdditionalDriver((current) => ({ ...current, nationality: e.target.value }))}
|
||||
className="input-field"
|
||||
/>
|
||||
</label>
|
||||
<div className="space-y-1">
|
||||
<BilingualInput label={copy.nationality} value={additionalDriver.nationality} onChange={(v) => setAdditionalDriver((cur) => ({ ...cur, nationality: v }))} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
|
||||
@@ -225,7 +225,9 @@ export default function SubscriptionPage() {
|
||||
try {
|
||||
if (provider === 'AMANPAY' && !providerAvailability.amanpay) throw new Error(copy.providerUnavailable)
|
||||
if (provider === 'PAYPAL' && !providerAvailability.paypal) throw new Error(copy.providerUnavailable)
|
||||
const base = window.location.origin
|
||||
const currentUrl = new URL(window.location.href)
|
||||
currentUrl.search = ''
|
||||
currentUrl.hash = ''
|
||||
const result = await apiFetch<{ checkoutUrl: string }>('/subscriptions/checkout', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
@@ -233,8 +235,8 @@ export default function SubscriptionPage() {
|
||||
billingPeriod,
|
||||
currency,
|
||||
provider,
|
||||
successUrl: `${base}/dashboard/subscription?payment=success`,
|
||||
failureUrl: `${base}/dashboard/subscription?payment=failed`,
|
||||
successUrl: `${currentUrl.toString()}?payment=success`,
|
||||
failureUrl: `${currentUrl.toString()}?payment=failed`,
|
||||
}),
|
||||
})
|
||||
window.location.href = result.checkoutUrl
|
||||
|
||||
@@ -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 d’authentification',
|
||||
enterCode: 'Entrez le code à 6 chiffres de votre application d’authentification.',
|
||||
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>
|
||||
) : (
|
||||
|
||||
@@ -7,19 +7,21 @@ import { useDashboardI18n } from '@/components/I18nProvider'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
import PublicShell from '@/components/layout/PublicShell'
|
||||
import { marketplaceUrl } from '@/lib/urls'
|
||||
import { BilingualField, BilingualInput, bilingualPrimary, emptyBilingual } from '@/components/ui/BilingualInput'
|
||||
|
||||
type SignupForm = {
|
||||
firstName: string
|
||||
lastName: string
|
||||
firstName: BilingualField
|
||||
lastName: BilingualField
|
||||
email: string
|
||||
password: string
|
||||
confirmPassword: string
|
||||
companyName: string
|
||||
preferredLanguage: 'en' | 'fr' | 'ar' | ''
|
||||
companyName: BilingualField
|
||||
legalForm: string
|
||||
registrationNumber: string
|
||||
streetAddress: string
|
||||
city: string
|
||||
country: string
|
||||
streetAddress: BilingualField
|
||||
city: BilingualField
|
||||
country: BilingualField
|
||||
zipCode: string
|
||||
companyPhone: string
|
||||
fax: string
|
||||
@@ -53,7 +55,7 @@ const LEGAL_FORM_OPTIONS = ['SARL', 'SARL AU', 'SA', 'SAS', 'AUTO_ENTREPRENEUR',
|
||||
const YEARS_ACTIVE_OPTIONS = ['LESS_THAN_1', 'ONE_TO_FIVE', 'MORE_THAN_5'] as const
|
||||
|
||||
export default function SignUpPage() {
|
||||
const { language } = useDashboardI18n()
|
||||
const { language, setLanguage } = useDashboardI18n()
|
||||
const searchParams = useSearchParams()
|
||||
const initialPlan = normalizePlan(searchParams.get('plan'))
|
||||
const initialBillingPeriod = normalizeBillingPeriod(searchParams.get('billing'))
|
||||
@@ -63,17 +65,18 @@ export default function SignUpPage() {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [completedSignup, setCompletedSignup] = useState<CompletedSignup | null>(null)
|
||||
const [form, setForm] = useState<SignupForm>({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
firstName: emptyBilingual(),
|
||||
lastName: emptyBilingual(),
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
companyName: '',
|
||||
preferredLanguage: '',
|
||||
companyName: emptyBilingual(),
|
||||
legalForm: '',
|
||||
registrationNumber: '',
|
||||
streetAddress: '',
|
||||
city: '',
|
||||
country: '',
|
||||
streetAddress: emptyBilingual(),
|
||||
city: emptyBilingual(),
|
||||
country: emptyBilingual(),
|
||||
zipCode: '',
|
||||
companyPhone: '',
|
||||
fax: '',
|
||||
@@ -130,6 +133,9 @@ export default function SignUpPage() {
|
||||
invalidPassword: 'Choose a password with at least 8 characters.',
|
||||
passwordMismatch: 'Passwords do not match.',
|
||||
missingRequired: 'Fill in all required fields.',
|
||||
missingLanguage: 'Please select your preferred language.',
|
||||
preferredLanguageLabel: 'Preferred language',
|
||||
languageOptions: { en: 'English', fr: 'Français', ar: 'العربية' } as Record<string, string>,
|
||||
companyFallback: 'Your company',
|
||||
couldNotCreate: 'Could not create workspace',
|
||||
legalFormOptions: {
|
||||
@@ -206,6 +212,9 @@ export default function SignUpPage() {
|
||||
invalidPassword: 'Choisissez un mot de passe d’au moins 8 caractères.',
|
||||
passwordMismatch: 'Les mots de passe ne correspondent pas.',
|
||||
missingRequired: 'Renseignez tous les champs obligatoires.',
|
||||
missingLanguage: 'Veuillez sélectionner votre langue préférée.',
|
||||
preferredLanguageLabel: 'Langue préférée',
|
||||
languageOptions: { en: 'English', fr: 'Français', ar: 'العربية' } as Record<string, string>,
|
||||
companyFallback: 'Votre entreprise',
|
||||
couldNotCreate: 'Impossible de créer l’espace',
|
||||
legalFormOptions: {
|
||||
@@ -282,6 +291,9 @@ export default function SignUpPage() {
|
||||
invalidPassword: 'اختر كلمة مرور من 8 أحرف على الأقل.',
|
||||
passwordMismatch: 'كلمتا المرور غير متطابقتين.',
|
||||
missingRequired: 'أكمل جميع الحقول الإلزامية.',
|
||||
missingLanguage: 'يرجى اختيار اللغة المفضلة.',
|
||||
preferredLanguageLabel: 'اللغة المفضلة',
|
||||
languageOptions: { en: 'English', fr: 'Français', ar: 'العربية' } as Record<string, string>,
|
||||
companyFallback: 'شركتك',
|
||||
couldNotCreate: 'تعذر إنشاء المساحة',
|
||||
legalFormOptions: {
|
||||
@@ -316,6 +328,15 @@ export default function SignUpPage() {
|
||||
},
|
||||
}[language]
|
||||
|
||||
function handleStep1Continue() {
|
||||
if (!form.preferredLanguage) {
|
||||
setError(dict.missingLanguage)
|
||||
return
|
||||
}
|
||||
setError(null)
|
||||
setStep(2)
|
||||
}
|
||||
|
||||
async function submitSignup() {
|
||||
if (form.password.length < 8) {
|
||||
setError(dict.invalidPassword)
|
||||
@@ -339,16 +360,23 @@ export default function SignUpPage() {
|
||||
const response = await apiFetch<SignupResponse>('/auth/company/signup', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
firstName: form.firstName,
|
||||
lastName: form.lastName,
|
||||
firstName: form.firstName.fr || form.firstName.ar,
|
||||
firstNameAr: form.firstName.ar || undefined,
|
||||
lastName: form.lastName.fr || form.lastName.ar,
|
||||
lastNameAr: form.lastName.ar || undefined,
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
companyName: form.companyName,
|
||||
preferredLanguage: form.preferredLanguage || 'en',
|
||||
companyName: form.companyName.fr || form.companyName.ar,
|
||||
companyNameAr: form.companyName.ar || undefined,
|
||||
legalForm: form.legalForm,
|
||||
registrationNumber: form.registrationNumber,
|
||||
streetAddress: form.streetAddress,
|
||||
city: form.city,
|
||||
country: form.country,
|
||||
streetAddress: form.streetAddress.fr || form.streetAddress.ar,
|
||||
streetAddressAr: form.streetAddress.ar || undefined,
|
||||
city: form.city.fr || form.city.ar,
|
||||
cityAr: form.city.ar || undefined,
|
||||
country: form.country.fr || form.country.ar,
|
||||
countryAr: form.country.ar || undefined,
|
||||
zipCode: form.zipCode,
|
||||
companyPhone: form.companyPhone,
|
||||
fax: form.fax || undefined,
|
||||
@@ -361,7 +389,7 @@ export default function SignUpPage() {
|
||||
})
|
||||
|
||||
setCompletedSignup({
|
||||
companyName: form.companyName,
|
||||
companyName: form.companyName.fr || form.companyName.ar,
|
||||
email: form.email,
|
||||
emailWarning: getEmailWarning(response.emailDelivery, language),
|
||||
})
|
||||
@@ -426,18 +454,40 @@ export default function SignUpPage() {
|
||||
{step === 1 ? (
|
||||
<div className="space-y-5">
|
||||
<SectionIntro title={dict.ownerTitle} body={dict.ownerBody} />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<LabeledInput label={dict.firstName} required maxLength={80} value={form.firstName} onChange={(value) => setForm((current) => ({ ...current, firstName: normalizeTitleCase(value) }))} />
|
||||
<LabeledInput label={dict.lastName} required maxLength={80} value={form.lastName} onChange={(value) => setForm((current) => ({ ...current, lastName: normalizeTitleCase(value) }))} />
|
||||
</div>
|
||||
<BilingualInput label={dict.firstName} required maxLength={80} value={form.firstName} onChange={(value) => setForm((current) => ({ ...current, firstName: value }))} />
|
||||
<BilingualInput label={dict.lastName} required maxLength={80} value={form.lastName} onChange={(current) => setForm((f) => ({ ...f, lastName: current }))} />
|
||||
<LabeledInput label={dict.ownerEmail} required type="email" maxLength={254} value={form.email} onChange={(value) => setForm((current) => ({ ...current, email: normalizeEmail(value) }))} />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<LabeledInput label={dict.password} required type="password" maxLength={128} value={form.password} onChange={(value) => setForm((current) => ({ ...current, password: value }))} />
|
||||
<LabeledInput label={dict.confirmPassword} required type="password" maxLength={128} value={form.confirmPassword} onChange={(value) => setForm((current) => ({ ...current, confirmPassword: value }))} />
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">{dict.passwordHint}</p>
|
||||
<div>
|
||||
<span className="mb-1.5 block text-sm font-medium text-slate-700">
|
||||
{dict.preferredLanguageLabel} <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{(['en', 'fr', 'ar'] as const).map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setForm((current) => ({ ...current, preferredLanguage: lang }))
|
||||
setLanguage(lang)
|
||||
}}
|
||||
className={`rounded-2xl border py-3 text-sm font-semibold transition ${
|
||||
form.preferredLanguage === lang
|
||||
? 'border-slate-900 bg-slate-900 text-white'
|
||||
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
{dict.languageOptions[lang]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button type="button" onClick={() => setStep(2)} className="btn-primary">{dict.continue}</button>
|
||||
<button type="button" onClick={handleStep1Continue} className="btn-primary">{dict.continue}</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -446,21 +496,19 @@ export default function SignUpPage() {
|
||||
<div className="space-y-5">
|
||||
<SectionIntro title={dict.partnerTitle} body={dict.partnerBody} />
|
||||
<div className="space-y-4">
|
||||
<LabeledInput label={dict.companyName} required maxLength={120} value={form.companyName} onChange={(value) => setForm((current) => ({ ...current, companyName: normalizeTitleCase(value) }))} />
|
||||
<BilingualInput label={dict.companyName} required maxLength={120} value={form.companyName} onChange={(value) => setForm((current) => ({ ...current, companyName: value }))} />
|
||||
<LabeledSelect label={dict.legalForm} required value={form.legalForm} options={['', ...LEGAL_FORM_OPTIONS]} labels={dict.legalFormOptions} onChange={(value) => setForm((current) => ({ ...current, legalForm: value }))} />
|
||||
<LabeledInput label={dict.registrationNumber} required maxLength={120} value={form.registrationNumber} onChange={(value) => setForm((current) => ({ ...current, registrationNumber: normalizeTitleCase(value) }))} />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<LabeledInput label={dict.companyPhone} required maxLength={80} value={form.companyPhone} onChange={(value) => setForm((current) => ({ ...current, companyPhone: value }))} />
|
||||
<LabeledInput label={dict.fax} maxLength={80} value={form.fax} onChange={(value) => setForm((current) => ({ ...current, fax: value }))} />
|
||||
</div>
|
||||
<BilingualInput label={dict.streetAddress} required maxLength={200} value={form.streetAddress} onChange={(value) => setForm((current) => ({ ...current, streetAddress: value }))} />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<LabeledInput label={dict.streetAddress} required maxLength={200} value={form.streetAddress} onChange={(value) => setForm((current) => ({ ...current, streetAddress: normalizeUpper(value) }))} />
|
||||
<LabeledInput label={dict.city} required maxLength={120} value={form.city} onChange={(value) => setForm((current) => ({ ...current, city: normalizeUpper(value) }))} />
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<LabeledInput label={dict.country} required maxLength={120} value={form.country} onChange={(value) => setForm((current) => ({ ...current, country: normalizeUpper(value) }))} />
|
||||
<LabeledInput label={dict.zipCode} required maxLength={40} value={form.zipCode} onChange={(value) => setForm((current) => ({ ...current, zipCode: value.toUpperCase() }))} />
|
||||
<BilingualInput label={dict.city} required maxLength={120} value={form.city} onChange={(value) => setForm((current) => ({ ...current, city: value }))} />
|
||||
<BilingualInput label={dict.country} required maxLength={120} value={form.country} onChange={(value) => setForm((current) => ({ ...current, country: value }))} />
|
||||
</div>
|
||||
<LabeledInput label={dict.zipCode} required maxLength={40} value={form.zipCode} onChange={(value) => setForm((current) => ({ ...current, zipCode: value.toUpperCase() }))} />
|
||||
<LabeledSelect label={dict.yearsActive} required value={form.yearsActive} options={['', ...YEARS_ACTIVE_OPTIONS]} labels={dict.yearsActiveOptions} onChange={(value) => setForm((current) => ({ ...current, yearsActive: value }))} />
|
||||
</div>
|
||||
<NavActions onBack={() => setStep(1)} onNext={() => setStep(3)} nextLabel={dict.continue} backLabel={dict.back} loadingLabel={dict.working} />
|
||||
@@ -496,7 +544,7 @@ export default function SignUpPage() {
|
||||
<div className="space-y-5">
|
||||
<SectionIntro title={dict.reviewTitle} body={dict.reviewBody} />
|
||||
<div className="rounded-3xl border border-slate-200 bg-slate-50 p-5 text-sm text-slate-600">
|
||||
<p><span className="font-semibold text-slate-900">{dict.reviewWorkspace}:</span> {form.companyName || dict.companyFallback}</p>
|
||||
<p><span className="font-semibold text-slate-900">{dict.reviewWorkspace}:</span> {form.companyName.fr || form.companyName.ar || dict.companyFallback}{form.companyName.fr && form.companyName.ar ? <span className="ml-2 text-slate-400">/ {form.companyName.ar}</span> : null}</p>
|
||||
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewPlan}:</span> {form.plan} · {dict.billingPeriodOptions[form.billingPeriod]} · {form.currency}</p>
|
||||
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewOwnerEmail}:</span> {form.email || '—'}</p>
|
||||
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewPhone}:</span> {form.companyPhone || '—'}</p>
|
||||
@@ -519,17 +567,15 @@ export default function SignUpPage() {
|
||||
}
|
||||
|
||||
function hasRequiredPartnerFields(form: SignupForm) {
|
||||
return [
|
||||
form.companyName,
|
||||
form.legalForm,
|
||||
form.registrationNumber,
|
||||
form.streetAddress,
|
||||
form.city,
|
||||
form.country,
|
||||
form.zipCode,
|
||||
form.companyPhone,
|
||||
form.yearsActive,
|
||||
].every((value) => value.trim().length > 0)
|
||||
const bilingualFilled = (f: BilingualField) => f.fr.trim().length > 0 || f.ar.trim().length > 0
|
||||
return (
|
||||
bilingualFilled(form.companyName) &&
|
||||
bilingualFilled(form.streetAddress) &&
|
||||
bilingualFilled(form.city) &&
|
||||
bilingualFilled(form.country) &&
|
||||
[form.legalForm, form.registrationNumber, form.zipCode, form.companyPhone, form.yearsActive]
|
||||
.every((v) => v.trim().length > 0)
|
||||
)
|
||||
}
|
||||
|
||||
function SectionIntro({ title, body }: { title: string; body: string }) {
|
||||
@@ -600,6 +646,7 @@ function LabeledInput({
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function LabeledSelect({
|
||||
label,
|
||||
value,
|
||||
|
||||
Reference in New Issue
Block a user