fix signin signout and apply the new style
This commit is contained in:
+2
-2
@@ -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>
|
||||
+2
-2
@@ -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()}>
|
||||
+1
-1
@@ -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>
|
||||
+70
@@ -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>
|
||||
+65
-4
@@ -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>
|
||||
+1
-1
@@ -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>
|
||||
+4
-4
@@ -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>
|
||||
+2
-2
@@ -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}>
|
||||
+3
-3
@@ -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>
|
||||
+3
-3
@@ -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
-1
@@ -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'
|
||||
|
||||
@@ -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 |
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export default function OnboardingPage() {
|
||||
isListedOnMarketplace: payments.isListedOnMarketplace,
|
||||
}),
|
||||
})
|
||||
router.push('/dashboard')
|
||||
router.push('/')
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
|
||||
@@ -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 l’entreprise requises pour créer l’espace.',
|
||||
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 l’espace entreprise et démarrer l’essai de 14 jours avec la formule choisie.',
|
||||
reviewBody: 'Nous allons créer immédiatement l’espace entreprise et démarrer l’essai 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">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user