fix signin signout and apply the new style

This commit is contained in:
root
2026-05-24 23:58:54 -04:00
parent bf97a072dd
commit 9bd0938951
68 changed files with 851 additions and 200 deletions
@@ -549,10 +549,10 @@ export default function BillingPage() {
) : (
<span className="text-xs text-slate-400">{copy.paymentActionDisabled}</span>
)}
<Link href={`/dashboard/contracts/${row.id}`} className="font-semibold text-blue-700 hover:underline">
<Link href={`/contracts/${row.id}`} className="font-semibold text-blue-700 hover:underline">
{copy.openContract}
</Link>
<Link href={`/dashboard/reservations/${row.id}`} className="text-slate-600 hover:text-slate-900 hover:underline">
<Link href={`/reservations/${row.id}`} className="text-slate-600 hover:text-slate-900 hover:underline">
{copy.openBooking}
</Link>
</div>
@@ -843,7 +843,7 @@ export default function ContractDetailPage() {
<div className="space-y-6 print:space-y-0">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between print:hidden">
<div className="space-y-2">
<Link href="/dashboard/contracts" className="text-sm font-semibold text-blue-700 hover:underline">{copy.back}</Link>
<Link href="/contracts" className="text-sm font-semibold text-blue-700 hover:underline">{copy.back}</Link>
<p className="text-sm font-semibold uppercase tracking-wide text-slate-500">{contract.company.name}</p>
<div className="flex flex-wrap items-center gap-3">
<h2 className="text-xl font-semibold text-slate-900">{contract.contractNumber ?? copy.contractNo}</h2>
@@ -855,7 +855,7 @@ export default function ContractDetailPage() {
</p>
</div>
<div className="flex flex-wrap gap-3 print:hidden">
<Link href={`/dashboard/reservations/${contract.reservationId}`} className="btn-secondary">
<Link href={`/reservations/${contract.reservationId}`} className="btn-secondary">
{copy.actionOpenBooking}
</Link>
<button type="button" className="btn-primary" onClick={() => window.print()}>
@@ -165,7 +165,7 @@ export default function ContractsPage() {
</td>
<td className="px-6 py-4 text-right text-sm font-semibold text-slate-900">{formatCurrency(row.totalAmount, 'MAD')}</td>
<td className="px-6 py-4 text-right">
<Link href={`/dashboard/contracts/${row.id}`} className="text-sm font-semibold text-blue-700 hover:underline">
<Link href={`/contracts/${row.id}`} className="text-sm font-semibold text-blue-700 hover:underline">
{row.contractNumber ? copy.open : copy.generate}
</Link>
</td>
@@ -26,6 +26,9 @@ interface VehicleDetail {
photos: string[]
notes: string | null
isPublished: boolean
pickupLocations: string[]
allowDifferentDropoff: boolean
dropoffLocations: string[]
}
const PRESET_COLORS = ['White', 'Black', 'Silver', 'Gray', 'Red', 'Blue', 'Green', 'Yellow', 'Orange', 'Brown', 'Beige', 'Gold']
@@ -55,6 +58,17 @@ const MODELS_BY_MAKE: Record<string, string[]> = {
'Volvo': ['S60', 'S90', 'V40', 'V60', 'V90', 'XC40', 'XC60', 'XC90'],
}
function parseLocationInput(value: string) {
return value
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
}
function formatLocationInput(values: string[]) {
return Array.isArray(values) ? values.join(', ') : ''
}
function initMakeSelect(make: string) {
return PRESET_MAKES.includes(make) ? make : (make ? 'custom' : '')
}
@@ -283,6 +297,7 @@ export default function FleetDetailPage() {
dailyRate: string; licensePlate: string; color: string
seats: number; transmission: string; fuelType: string
status: string; notes: string; mileage: string
pickupLocations: string; allowDifferentDropoff: boolean; dropoffLocations: string
} | null>(null)
const [makeSelect, setMakeSelect] = useState('')
const [modelSelect, setModelSelect] = useState('')
@@ -321,6 +336,9 @@ export default function FleetDetailPage() {
transmission: vehicle.transmission, fuelType: vehicle.fuelType,
status: vehicle.status, notes: vehicle.notes ?? '',
mileage: vehicle.mileage != null ? String(vehicle.mileage) : '',
pickupLocations: formatLocationInput(vehicle.pickupLocations),
allowDifferentDropoff: vehicle.allowDifferentDropoff,
dropoffLocations: formatLocationInput(vehicle.dropoffLocations),
})
setMakeSelect(initMakeSelect(vehicle.make))
setModelSelect(initModelSelect(vehicle.make, vehicle.model))
@@ -345,6 +363,10 @@ export default function FleetDetailPage() {
if (!form.licensePlate) { setSaveError(vd.plateRequired); return }
const rate = parseFloat(form.dailyRate)
if (isNaN(rate) || rate < 0) { setSaveError(vd.rateInvalid); return }
if (form.allowDifferentDropoff && parseLocationInput(form.dropoffLocations).length === 0) {
setSaveError(fl.dropoffLocationsRequired)
return
}
setSaving(true)
setSaveError(null)
@@ -359,6 +381,9 @@ export default function FleetDetailPage() {
fuelType: form.fuelType, status: form.status,
notes: form.notes || undefined,
mileage: form.mileage ? Number(form.mileage) : undefined,
pickupLocations: parseLocationInput(form.pickupLocations),
allowDifferentDropoff: form.allowDifferentDropoff,
dropoffLocations: form.allowDifferentDropoff ? parseLocationInput(form.dropoffLocations) : [],
}),
})
@@ -558,6 +583,11 @@ export default function FleetDetailPage() {
<div><dt className="text-slate-500">{vd.labelFuelType}</dt><dd className="text-slate-900">{fuelLabel}</dd></div>
<div><dt className="text-slate-500">{vd.labelColor}</dt><dd className="text-slate-900">{vehicle.color || '—'}</dd></div>
<div><dt className="text-slate-500">{vd.labelOdometer}</dt><dd className="text-slate-900">{vehicle.mileage != null ? `${vehicle.mileage.toLocaleString()} km` : '—'}</dd></div>
<div><dt className="text-slate-500">{fl.pickupLocations}</dt><dd className="text-slate-900">{vehicle.pickupLocations.length > 0 ? vehicle.pickupLocations.join(', ') : '—'}</dd></div>
<div><dt className="text-slate-500">{fl.allowDifferentDropoff}</dt><dd className="text-slate-900">{vehicle.allowDifferentDropoff ? fl.differentDropoffAvailable : fl.sameDropoffOnly}</dd></div>
{vehicle.allowDifferentDropoff && (
<div><dt className="text-slate-500">{fl.dropoffLocations}</dt><dd className="text-slate-900">{vehicle.dropoffLocations.length > 0 ? vehicle.dropoffLocations.join(', ') : '—'}</dd></div>
)}
<div><dt className="text-slate-500">{vd.labelNotes}</dt><dd className="text-slate-900">{vehicle.notes ?? '—'}</dd></div>
</dl>
) : (
@@ -701,6 +731,46 @@ export default function FleetDetailPage() {
<input type="number" className="input-field" placeholder="e.g. 45000" min="0" value={form.mileage} onChange={(e) => setForm({ ...form, mileage: e.target.value })} />
</div>
<div className="space-y-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{fl.pickupLocations}</label>
<textarea
className="input-field resize-none"
rows={2}
placeholder={fl.pickupLocationsPlaceholder}
value={form.pickupLocations}
onChange={(e) => setForm({ ...form, pickupLocations: e.target.value })}
/>
<p className="mt-1 text-xs text-slate-400">{fl.locationHint}</p>
</div>
<label className="flex items-center gap-3 rounded-lg border border-slate-200 bg-white px-3 py-3 text-sm font-medium text-slate-700">
<input
type="checkbox"
checked={form.allowDifferentDropoff}
onChange={(e) => setForm({
...form,
allowDifferentDropoff: e.target.checked,
dropoffLocations: e.target.checked ? form.dropoffLocations : '',
})}
className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
/>
<span>{fl.allowDifferentDropoff}</span>
</label>
{form.allowDifferentDropoff && (
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{fl.dropoffLocations}</label>
<textarea
className="input-field resize-none"
rows={2}
placeholder={fl.dropoffLocationsPlaceholder}
value={form.dropoffLocations}
onChange={(e) => setForm({ ...form, dropoffLocations: e.target.value })}
/>
<p className="mt-1 text-xs text-slate-400">{fl.locationHint}</p>
</div>
)}
</div>
{/* Notes */}
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{vd.notesLabel}</label>
@@ -19,6 +19,9 @@ interface Vehicle {
isPublished: boolean
primaryPhoto: string | null
licensePlate: string
pickupLocations?: string[]
allowDifferentDropoff?: boolean
dropoffLocations?: string[]
}
interface AddVehicleModalProps {
@@ -248,13 +251,20 @@ const MODELS_BY_MAKE: Record<string, string[]> = {
'Volvo': ['S60', 'S90', 'V40', 'V60', 'V90', 'XC40', 'XC60', 'XC90'],
}
function parseLocationInput(value: string) {
return value
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
}
function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
const { dict } = useDashboardI18n()
const f = dict.fleet
const [form, setForm] = useState({
make: '', model: '', year: new Date().getFullYear(), category: 'ECONOMY',
dailyRate: '', licensePlate: '', color: '', seats: 5, transmission: 'MANUAL',
fuelType: 'GASOLINE', mileage: '',
fuelType: 'GASOLINE', mileage: '', pickupLocations: '', allowDifferentDropoff: false, dropoffLocations: '',
})
const [colorSelect, setColorSelect] = useState('')
const [makeSelect, setMakeSelect] = useState('')
@@ -283,7 +293,11 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
}
const reset = () => {
setForm({ make: '', model: '', year: new Date().getFullYear(), category: 'ECONOMY', dailyRate: '', licensePlate: '', color: '', seats: 5, transmission: 'MANUAL', fuelType: 'GASOLINE', mileage: '' })
setForm({
make: '', model: '', year: new Date().getFullYear(), category: 'ECONOMY',
dailyRate: '', licensePlate: '', color: '', seats: 5, transmission: 'MANUAL',
fuelType: 'GASOLINE', mileage: '', pickupLocations: '', allowDifferentDropoff: false, dropoffLocations: '',
})
setColorSelect('')
setMakeSelect('')
setModelSelect('')
@@ -301,6 +315,10 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
if (!form.model) { setError(f.modelMissing); return }
if (!form.licensePlate) { setError(f.plateMissing); return }
if (!form.dailyRate || isNaN(parseFloat(form.dailyRate))) { setError(f.rateMissing); return }
if (form.allowDifferentDropoff && parseLocationInput(form.dropoffLocations).length === 0) {
setError(f.dropoffLocationsRequired)
return
}
setLoading(true)
setError(null)
try {
@@ -312,6 +330,9 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
year: Number(form.year),
seats: Number(form.seats),
mileage: form.mileage ? Number(form.mileage) : undefined,
pickupLocations: parseLocationInput(form.pickupLocations),
allowDifferentDropoff: form.allowDifferentDropoff,
dropoffLocations: form.allowDifferentDropoff ? parseLocationInput(form.dropoffLocations) : [],
}),
})
if (photoFiles.length > 0) {
@@ -474,6 +495,46 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
</div>
</div>
<div className="space-y-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{f.pickupLocations}</label>
<textarea
className="input-field resize-none"
rows={2}
placeholder={f.pickupLocationsPlaceholder}
value={form.pickupLocations}
onChange={(e) => setForm({ ...form, pickupLocations: e.target.value })}
/>
<p className="mt-1 text-xs text-slate-400">{f.locationHint}</p>
</div>
<label className="flex items-center gap-3 rounded-lg border border-slate-200 bg-white px-3 py-3 text-sm font-medium text-slate-700">
<input
type="checkbox"
checked={form.allowDifferentDropoff}
onChange={(e) => setForm({
...form,
allowDifferentDropoff: e.target.checked,
dropoffLocations: e.target.checked ? form.dropoffLocations : '',
})}
className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
/>
<span>{f.allowDifferentDropoff}</span>
</label>
{form.allowDifferentDropoff && (
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{f.dropoffLocations}</label>
<textarea
className="input-field resize-none"
rows={2}
placeholder={f.dropoffLocationsPlaceholder}
value={form.dropoffLocations}
onChange={(e) => setForm({ ...form, dropoffLocations: e.target.value })}
/>
<p className="mt-1 text-xs text-slate-400">{f.locationHint}</p>
</div>
)}
</div>
{/* Photos */}
<div>
<label className="block text-xs font-medium text-slate-500 mb-1.5">{f.photosLabel}</label>
@@ -682,10 +743,10 @@ export default function FleetPage() {
</td>
<td className="px-6 py-4">
<div className="flex items-center justify-end gap-2 rtl:justify-start">
<Link href={`/dashboard/fleet/${vehicle.id}`} className="p-1.5 text-slate-400 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors">
<Link href={`/fleet/${vehicle.id}`} className="p-1.5 text-slate-400 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors">
<Eye className="w-4 h-4" />
</Link>
<Link href={`/dashboard/fleet/${vehicle.id}`} className="p-1.5 text-slate-400 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors">
<Link href={`/fleet/${vehicle.id}`} className="p-1.5 text-slate-400 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors">
<Edit className="w-4 h-4" />
</Link>
</div>
@@ -301,7 +301,7 @@ function ReservationCard({ r, acting, onConfirm, onDecline }: {
<div className="border-t border-slate-100 bg-slate-50 px-5 py-2.5 flex items-center justify-between">
<p className="text-xs text-slate-400">Ref: {r.id.slice(-10).toUpperCase()}</p>
<Link href={`/dashboard/reservations/${r.id}`} className="flex items-center gap-1 text-xs text-slate-500 hover:text-blue-700">
<Link href={`/reservations/${r.id}`} className="flex items-center gap-1 text-xs text-slate-500 hover:text-blue-700">
View full detail <ChevronRight className="h-3 w-3" />
</Link>
</div>
@@ -98,7 +98,7 @@ 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>
<Link href="/dashboard/subscription" className="ml-auto text-sm font-semibold underline hover:no-underline">
<Link href="/subscription" className="ml-auto text-sm font-semibold underline hover:no-underline">
{manageBillingLabel}
</Link>
</div>
@@ -261,7 +261,7 @@ 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>
<Link href="/dashboard/reservations" className="text-sm text-blue-600 hover:text-blue-700 font-medium">
<Link href="/reservations" className="text-sm text-blue-600 hover:text-blue-700 font-medium">
{d.viewAll}
</Link>
</div>
@@ -281,9 +281,9 @@ export default function DashboardPage() {
{(data?.recentReservations ?? []).map((res) => (
<tr key={res.id} className="hover:bg-slate-50 transition-colors">
<td className="px-6 py-3">
<a href={`/dashboard/reservations/${res.id}`} className="text-sm font-medium text-blue-600 hover:text-blue-700">
<Link href={`/reservations/${res.id}`} className="text-sm font-medium text-blue-600 hover:text-blue-700">
#{res.bookingRef}
</a>
</Link>
</td>
<td className="px-6 py-3 text-sm text-slate-700">{res.customerName}</td>
<td className="px-6 py-3 text-sm text-slate-700">{res.vehicleName}</td>
@@ -418,7 +418,7 @@ export default function NewReservationPage() {
notes: notes || undefined,
}),
})
router.push(`/dashboard/reservations/${created.id}`)
router.push(`/reservations/${created.id}`)
} catch (err: any) {
setError(err.message)
} finally {
@@ -688,7 +688,7 @@ export default function NewReservationPage() {
</label>
<div className="pt-2 flex items-center justify-end gap-3">
<button type="button" className="btn-secondary" onClick={() => router.push('/dashboard/reservations')}>
<button type="button" className="btn-secondary" onClick={() => router.push('/reservations')}>
{copy.cancel}
</button>
<button type="button" className="btn-primary" onClick={submit} disabled={saving || !canSubmit}>
@@ -60,7 +60,7 @@ export default function ReservationsPage() {
<h2 className="text-xl font-semibold text-slate-900">{r.heading}</h2>
<p className="text-sm text-slate-500 mt-1">{r.subtitle}</p>
</div>
<Link href="/dashboard/reservations/new" className="btn-primary whitespace-nowrap">
<Link href="/reservations/new" className="btn-primary whitespace-nowrap">
{language === 'fr' ? 'Réserver une voiture' : language === 'ar' ? 'حجز سيارة' : 'Book car'}
</Link>
</div>
@@ -84,11 +84,11 @@ export default function ReservationsPage() {
{rows.map((row) => (
<tr key={row.id}>
<td className="px-6 py-4">
<Link href={`/dashboard/reservations/${row.id}`} className="text-sm font-semibold text-slate-900 hover:text-blue-700">
<Link href={`/reservations/${row.id}`} className="text-sm font-semibold text-slate-900 hover:text-blue-700">
{row.customer.firstName} {row.customer.lastName}
</Link>
<p className="text-xs text-slate-500">{row.customer.email}</p>
<Link href={`/dashboard/reservations/${row.id}`} className="mt-1 inline-block text-xs font-semibold text-blue-700 hover:underline">
<Link href={`/reservations/${row.id}`} className="mt-1 inline-block text-xs font-semibold text-blue-700 hover:underline">
{reservationActionLabel(row)}
</Link>
</td>
@@ -211,7 +211,7 @@ export default function SubscriptionPage() {
const profile = JSON.parse(cached) as EmployeeProfile
const allowed = profile.role === 'OWNER'
setCanViewPage(allowed)
if (!allowed) router.replace('/dashboard')
if (!allowed) router.replace('/')
return
} catch {}
}
@@ -221,11 +221,11 @@ export default function SubscriptionPage() {
window.localStorage.setItem(EMPLOYEE_PROFILE_KEY, JSON.stringify(employee))
const allowed = employee.role === 'OWNER'
setCanViewPage(allowed)
if (!allowed) router.replace('/dashboard')
if (!allowed) router.replace('/')
})
.catch(() => {
setCanViewPage(false)
router.replace('/dashboard')
router.replace('/')
})
}, [router])
@@ -0,0 +1,3 @@
export default function PublicLayout({ children }: { children: React.ReactNode }) {
return children
}
@@ -1,7 +1,7 @@
import { Suspense } from 'react'
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import SignInPageClient from './SignInPageClient'
import SignInPageClient from '@/app/sign-in/[[...sign-in]]/SignInPageClient'
const MARKETPLACE_URL = process.env.NEXT_PUBLIC_MARKETPLACE_URL ?? 'http://localhost:3000'
+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="8" fill="#0f172a" />
<text
x="16"
y="21"
text-anchor="middle"
font-family="Inter, Arial, sans-serif"
font-size="18"
font-weight="700"
fill="#ffffff"
>
R
</text>
</svg>

After

Width:  |  Height:  |  Size: 302 B

-32
View File
@@ -1,32 +0,0 @@
import { ImageResponse } from 'next/og'
export const size = {
width: 32,
height: 32,
}
export const contentType = 'image/png'
export default function Icon() {
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#0f172a',
color: '#ffffff',
fontSize: 18,
fontWeight: 700,
borderRadius: 8,
}}
>
R
</div>
),
size,
)
}
+1 -1
View File
@@ -83,7 +83,7 @@ export default function OnboardingPage() {
isListedOnMarketplace: payments.isListedOnMarketplace,
}),
})
router.push('/dashboard')
router.push('/')
} catch (err: any) {
setError(err.message)
} finally {
-5
View File
@@ -1,5 +0,0 @@
import { redirect } from 'next/navigation'
export default function DashboardRootPage() {
redirect('/sign-in')
}
@@ -8,6 +8,7 @@ import { useDashboardI18n } from '@/components/I18nProvider'
import PublicShell from '@/components/layout/PublicShell'
import { adminUrl, marketplaceUrl } from '@/lib/urls'
import { API_BASE, EMPLOYEE_PROFILE_KEY, EMPLOYEE_TOKEN_KEY } from '@/lib/api'
import { toDashboardAppPath, toPublicDashboardPath } from '@/lib/dashboardPaths'
const DASHBOARD_LOGO_SRC = '/dashboard/rentalcardrive.png'
@@ -220,6 +221,7 @@ function LocalSignInForm({
const [error, setError] = useState<string | null>(null)
const adminNext = searchParams.get('next') || '/dashboard'
const employeeRedirect = searchParams.get('redirect') || '/dashboard'
const employeeAppRedirect = toDashboardAppPath(employeeRedirect)
function redirectAdmin(token: string) {
const hash = new URLSearchParams({ token, next: adminNext }).toString()
@@ -252,8 +254,8 @@ function LocalSignInForm({
}
document.cookie = `employee_token=${token}; path=/; max-age=28800; samesite=lax`
window.dispatchEvent(new CustomEvent('rentaldrivego:auth-changed'))
notifyParent({ type: 'rentaldrivego:employee-login', path: '/dashboard' + employeeRedirect })
router.push(employeeRedirect)
notifyParent({ type: 'rentaldrivego:employee-login', path: toPublicDashboardPath(employeeRedirect) })
router.push(employeeAppRedirect)
return
}
@@ -101,9 +101,9 @@ export default function SignUpPage() {
partnerTitle: 'Company info',
partnerBody: 'Complete the company information required to create the workspace.',
planTitle: 'Choose your plan',
planBody: 'Your workspace starts immediately on a 14-day free trial in your preferred billing currency.',
planBody: 'Your workspace starts immediately on a 90-day free trial in your preferred billing currency.',
reviewTitle: 'Review and launch',
reviewBody: 'We will create the company workspace immediately and start the 14-day trial with the plan you selected.',
reviewBody: 'We will create the company workspace immediately and start the 90-day trial with the plan you selected.',
firstName: 'Manager/Owner first name',
lastName: 'Manager/Owner last name',
ownerEmail: 'Manager/Owner email',
@@ -180,9 +180,9 @@ export default function SignUpPage() {
partnerTitle: 'Infos entreprise',
partnerBody: 'Complétez les informations de lentreprise requises pour créer lespace.',
planTitle: 'Choisissez votre formule',
planBody: 'Votre espace démarre immédiatement avec un essai gratuit de 14 jours dans la devise choisie.',
planBody: 'Votre espace démarre immédiatement avec un essai gratuit de 90 jours dans la devise choisie.',
reviewTitle: 'Vérification et lancement',
reviewBody: 'Nous allons créer immédiatement lespace entreprise et démarrer lessai de 14 jours avec la formule choisie.',
reviewBody: 'Nous allons créer immédiatement lespace entreprise et démarrer lessai de 90 jours avec la formule choisie.',
firstName: 'Prénom du gérant/propriétaire',
lastName: 'Nom du gérant/propriétaire',
ownerEmail: 'E-mail du gérant/propriétaire',
@@ -431,7 +431,7 @@ export default function SignUpPage() {
</div>
) : null}
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Link href="/dashboard" className="btn-primary justify-center">
<Link href="/" className="btn-primary justify-center">
{dict.enterDashboard}
</Link>
<a href="/sign-in" className="btn-secondary justify-center">
+78 -42
View File
@@ -49,6 +49,15 @@ type FleetDict = {
manual: string
automatic: string
fuelType: string
pickupLocations: string
pickupLocationsPlaceholder: string
dropoffLocations: string
dropoffLocationsPlaceholder: string
locationHint: string
allowDifferentDropoff: string
dropoffLocationsRequired: string
sameDropoffOnly: string
differentDropoffAvailable: string
photosLabel: string
clickToAddPhotos: string
addMorePhotos: string
@@ -323,20 +332,20 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
settings: 'Settings',
},
titles: {
'/dashboard': 'Dashboard',
'/dashboard/fleet': 'Fleet Management',
'/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/subscription': 'Subscription',
'/dashboard/billing': 'Customer Billing',
'/dashboard/contracts': 'Contracts',
'/dashboard/notifications': 'Notifications',
'/dashboard/settings': 'Settings',
'/': 'Dashboard',
'/fleet': 'Fleet Management',
'/reservations': 'Booking',
'/reservations/new': 'Book Car',
'/online-reservations': 'Online Reservations',
'/customers': 'Customers',
'/offers': 'Offers',
'/team': 'Team',
'/reports': 'Reports',
'/subscription': 'Subscription',
'/billing': 'Customer Billing',
'/contracts': 'Contracts',
'/notifications': 'Notifications',
'/settings': 'Settings',
},
notifications: 'Notifications',
noNewNotifications: 'No new notifications',
@@ -390,6 +399,15 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
manual: 'Manual',
automatic: 'Automatic',
fuelType: 'Fuel Type',
pickupLocations: 'Pick-up locations',
pickupLocationsPlaceholder: 'e.g. Casablanca Airport, Casablanca Downtown',
dropoffLocations: 'Drop-off locations',
dropoffLocationsPlaceholder: 'e.g. Rabat Downtown, Marrakech Center',
locationHint: 'Separate multiple locations with commas.',
allowDifferentDropoff: 'Allow different drop-off location',
dropoffLocationsRequired: 'Please add at least one drop-off location when different drop-off is enabled.',
sameDropoffOnly: 'Same drop-off only',
differentDropoffAvailable: 'Different drop-off available',
photosLabel: 'Photos (up to 10)',
clickToAddPhotos: 'Click to add photos',
addMorePhotos: 'Add more photos',
@@ -666,20 +684,20 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
settings: 'Paramètres',
},
titles: {
'/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/subscription': 'Abonnement',
'/dashboard/billing': 'Facturation clients',
'/dashboard/contracts': 'Contrats',
'/dashboard/notifications': 'Notifications',
'/dashboard/settings': 'Paramètres',
'/': 'Tableau de bord',
'/fleet': 'Gestion de flotte',
'/reservations': 'Réservations',
'/reservations/new': 'Réserver une voiture',
'/online-reservations': 'Réservations en ligne',
'/customers': 'Clients',
'/offers': 'Offres',
'/team': 'Équipe',
'/reports': 'Rapports',
'/subscription': 'Abonnement',
'/billing': 'Facturation clients',
'/contracts': 'Contrats',
'/notifications': 'Notifications',
'/settings': 'Paramètres',
},
notifications: 'Notifications',
noNewNotifications: 'Aucune nouvelle notification',
@@ -733,6 +751,15 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
manual: 'Manuelle',
automatic: 'Automatique',
fuelType: 'Carburant',
pickupLocations: 'Lieux de départ',
pickupLocationsPlaceholder: 'ex. Aéroport de Casablanca, Centre-ville de Casablanca',
dropoffLocations: 'Lieux de retour',
dropoffLocationsPlaceholder: 'ex. Centre-ville de Rabat, Centre de Marrakech',
locationHint: 'Séparez plusieurs lieux par des virgules.',
allowDifferentDropoff: 'Autoriser un lieu de retour différent',
dropoffLocationsRequired: 'Ajoutez au moins un lieu de retour lorsque le retour différent est activé.',
sameDropoffOnly: 'Retour au même lieu uniquement',
differentDropoffAvailable: 'Retour différent disponible',
photosLabel: "Photos (jusqu'à 10)",
clickToAddPhotos: 'Cliquer pour ajouter des photos',
addMorePhotos: "Ajouter d'autres photos",
@@ -1009,20 +1036,20 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
settings: 'الإعدادات',
},
titles: {
'/dashboard': 'لوحة التحكم',
'/dashboard/fleet': 'إدارة الأسطول',
'/dashboard/reservations': 'الحجوزات',
'/dashboard/reservations/new': 'حجز سيارة',
'/dashboard/online-reservations': 'الحجوزات الإلكترونية',
'/dashboard/customers': 'العملاء',
'/dashboard/offers': 'العروض',
'/dashboard/team': 'الفريق',
'/dashboard/reports': 'التقارير',
'/dashboard/subscription': 'الاشتراك',
'/dashboard/billing': 'فوترة العملاء',
'/dashboard/contracts': 'العقود',
'/dashboard/notifications': 'الإشعارات',
'/dashboard/settings': 'الإعدادات',
'/': 'لوحة التحكم',
'/fleet': 'إدارة الأسطول',
'/reservations': 'الحجوزات',
'/reservations/new': 'حجز سيارة',
'/online-reservations': 'الحجوزات الإلكترونية',
'/customers': 'العملاء',
'/offers': 'العروض',
'/team': 'الفريق',
'/reports': 'التقارير',
'/subscription': 'الاشتراك',
'/billing': 'فوترة العملاء',
'/contracts': 'العقود',
'/notifications': 'الإشعارات',
'/settings': 'الإعدادات',
},
notifications: 'الإشعارات',
noNewNotifications: 'لا توجد إشعارات جديدة',
@@ -1076,6 +1103,15 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
manual: 'يدوي',
automatic: 'أوتوماتيكي',
fuelType: 'نوع الوقود',
pickupLocations: 'مواقع الاستلام',
pickupLocationsPlaceholder: 'مثال: مطار الدار البيضاء، وسط الدار البيضاء',
dropoffLocations: 'مواقع الإرجاع',
dropoffLocationsPlaceholder: 'مثال: وسط الرباط، مركز مراكش',
locationHint: 'افصل بين المواقع المتعددة بفواصل.',
allowDifferentDropoff: 'السماح بموقع إرجاع مختلف',
dropoffLocationsRequired: 'أضف موقع إرجاع واحداً على الأقل عند تفعيل الإرجاع المختلف.',
sameDropoffOnly: 'الإرجاع في نفس الموقع فقط',
differentDropoffAvailable: 'الإرجاع المختلف متاح',
photosLabel: 'الصور (حتى 10)',
clickToAddPhotos: 'انقر لإضافة صور',
addMorePhotos: 'إضافة المزيد من الصور',
@@ -23,6 +23,7 @@ import {
import { useDashboardI18n } from '@/components/I18nProvider'
import { marketplaceUrl } from '@/lib/urls'
import { EMPLOYEE_PROFILE_KEY, EMPLOYEE_TOKEN_KEY, apiFetch } from '@/lib/api'
import { toDashboardAppPath } from '@/lib/dashboardPaths'
import { SHARED_LANGUAGE_KEY, readCurrentUserScopedPreference } from '@/lib/preferences'
interface BrandSettings {
@@ -61,19 +62,19 @@ function toSidebarUser(profile: Partial<EmployeeProfile>, fallbackName: string,
}
const NAV_ITEMS = [
{ 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' },
{ href: '/', key: 'dashboard', icon: LayoutDashboard, exact: true, minRole: 'AGENT' },
{ href: '/fleet', key: 'fleet', icon: Car, minRole: 'AGENT' },
{ href: '/reservations', key: 'reservations', icon: Calendar, minRole: 'AGENT' },
{ href: '/online-reservations', key: 'onlineReservations', icon: Globe, minRole: 'AGENT' },
{ href: '/customers', key: 'customers', icon: Users, minRole: 'AGENT' },
{ href: '/offers', key: 'offers', icon: Tag, minRole: 'MANAGER' },
{ href: '/team', key: 'team', icon: UserPlus, minRole: 'AGENT' },
{ href: '/reports', key: 'reports', icon: BarChart2, minRole: 'MANAGER' },
{ href: '/subscription', key: 'subscription', icon: CreditCard, minRole: 'OWNER' },
{ href: '/billing', key: 'billing', icon: CreditCard, minRole: 'MANAGER' },
{ href: '/contracts', key: 'contracts', icon: FileText, minRole: 'AGENT' },
{ href: '/notifications', key: 'notifications', icon: Bell, minRole: 'AGENT' },
{ href: '/settings', key: 'settings', icon: Settings, minRole: 'OWNER' },
] as const
const ROLE_RANK: Record<string, number> = { OWNER: 3, MANAGER: 2, AGENT: 1 }
@@ -82,10 +83,16 @@ function hasMinRole(employeeRole: string, minRole: string): boolean {
return (ROLE_RANK[employeeRole] ?? 0) >= (ROLE_RANK[minRole] ?? 0)
}
function notifyParent(message: Record<string, unknown>) {
if (typeof window === 'undefined' || window.parent === window) return
window.parent.postMessage(message, '*')
}
export default function Sidebar() {
const { dict, language, setLanguage } = useDashboardI18n()
const isRtl = language === 'ar'
const pathname = usePathname()
const appPath = toDashboardAppPath(pathname)
const router = useRouter()
const [brand, setBrand] = useState<BrandSettings | null>(null)
const [user, setUser] = useState<SidebarUser>({
@@ -153,15 +160,17 @@ export default function Sidebar() {
useEffect(() => { setOpen(false) }, [pathname])
const isActive = (item: typeof NAV_ITEMS[number]) => {
if ('exact' in item && item.exact) return pathname === item.href
return pathname.startsWith(item.href)
if ('exact' in item && item.exact) return appPath === item.href
return appPath.startsWith(item.href)
}
function signOut() {
localStorage.removeItem(EMPLOYEE_TOKEN_KEY)
localStorage.removeItem(EMPLOYEE_PROFILE_KEY)
document.cookie = 'employee_token=; path=/; max-age=0; samesite=lax'
router.push('/sign-in')
window.dispatchEvent(new CustomEvent('rentaldrivego:auth-changed'))
notifyParent({ type: 'rentaldrivego:employee-logout' })
window.location.href = marketplaceUrl
}
return (
@@ -5,6 +5,7 @@ import { usePathname, useRouter } from 'next/navigation'
import { useState, useEffect } from 'react'
import { EMPLOYEE_PROFILE_KEY, EMPLOYEE_TOKEN_KEY, apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
import { toDashboardAppPath } from '@/lib/dashboardPaths'
function computeInitials(name: string): string {
const parts = name.trim().split(/\s+/).filter(Boolean)
@@ -16,6 +17,7 @@ function computeInitials(name: string): string {
export default function TopBar() {
const { dict } = useDashboardI18n()
const pathname = usePathname()
const appPath = toDashboardAppPath(pathname)
const router = useRouter()
const [unreadCount, setUnreadCount] = useState(0)
const [showNotifs, setShowNotifs] = useState(false)
@@ -33,11 +35,11 @@ export default function TopBar() {
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']
return dict.titles['/dashboard']
if (!mounted) return dict.titles['/']
if (dict.titles[appPath]) return dict.titles[appPath]
if (appPath.startsWith('/contracts/')) return dict.titles['/contracts'] ?? dict.titles['/']
if (appPath.startsWith('/reservations/')) return dict.titles['/reservations'] ?? dict.titles['/']
return dict.titles['/']
})()
async function refreshUnreadCount() {
try {
@@ -139,7 +141,7 @@ export default function TopBar() {
} catch {}
}
setShowNotifs(false)
router.push('/dashboard/notifications')
router.push('/notifications')
}
return (
+19
View File
@@ -0,0 +1,19 @@
const DASHBOARD_BASE_PATH = '/dashboard'
export function toDashboardAppPath(path?: string | null): string {
const value = (path ?? '').trim()
if (!value || value === '/') return '/'
let normalized = value.startsWith('/') ? value : `/${value}`
while (normalized === DASHBOARD_BASE_PATH || normalized.startsWith(`${DASHBOARD_BASE_PATH}/`)) {
normalized = normalized.slice(DASHBOARD_BASE_PATH.length) || '/'
}
return normalized
}
export function toPublicDashboardPath(path?: string | null): string {
const appPath = toDashboardAppPath(path)
return appPath === '/' ? DASHBOARD_BASE_PATH : `${DASHBOARD_BASE_PATH}${appPath}`
}
+20 -2
View File
@@ -4,6 +4,10 @@ import type { NextRequest } from 'next/server'
const PUBLIC_DASHBOARD_PREFIXES = ['/dashboard/sign-in', '/dashboard/forgot-password']
const MARKETPLACE_URL = process.env.NEXT_PUBLIC_MARKETPLACE_URL ?? 'http://localhost:3000'
function dedupeDashboardPath(pathname: string): string {
return pathname.replace(/^\/dashboard\/dashboard(?=\/|$)/, '/dashboard')
}
function isInternalHost(host: string | null): boolean {
if (!host) return false
const hostname = host.split(':')[0]?.toLowerCase()
@@ -17,9 +21,16 @@ function isProtectedRoute(req: NextRequest) {
}
function localJwtMiddleware(req: NextRequest): NextResponse {
const token = req.cookies.get('employee_token')?.value
if (token && req.nextUrl.pathname === '/dashboard/sign-in') {
const dashboardUrl = req.nextUrl.clone()
dashboardUrl.pathname = '/dashboard'
dashboardUrl.search = ''
return NextResponse.redirect(dashboardUrl)
}
if (!isProtectedRoute(req)) return NextResponse.next()
const token = req.cookies.get('employee_token')?.value
if (!token) {
const forwardedHost = req.headers.get('x-forwarded-host')
const forwardedProto = req.headers.get('x-forwarded-proto')
@@ -35,7 +46,7 @@ function localJwtMiddleware(req: NextRequest): NextResponse {
}
const isMarketplaceHost = signInUrl.host === marketplaceOrigin.host
signInUrl.pathname = isMarketplaceHost ? '/sign-in' : '/dashboard/sign-in'
signInUrl.pathname = isMarketplaceHost ? '/' : '/dashboard/sign-in'
signInUrl.searchParams.set('redirect', req.nextUrl.pathname)
return NextResponse.redirect(signInUrl)
}
@@ -43,6 +54,13 @@ function localJwtMiddleware(req: NextRequest): NextResponse {
}
export default function middleware(req: NextRequest) {
const dedupedPath = dedupeDashboardPath(req.nextUrl.pathname)
if (dedupedPath !== req.nextUrl.pathname) {
const redirectUrl = req.nextUrl.clone()
redirectUrl.pathname = dedupedPath
return NextResponse.redirect(redirectUrl)
}
return localJwtMiddleware(req)
}