prevent booking when no car added
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Plus, Edit, Eye, Search, Upload, X, ChevronDown, DollarSign } from 'lucide-react'
|
import { Plus, Edit, Eye, Search, Upload, X, DollarSign } from 'lucide-react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { apiFetch } from '@/lib/api'
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import { EMPLOYEE_PROFILE_KEY, apiFetch } from '@/lib/api'
|
||||||
import { formatCurrency } from '@rentaldrivego/types'
|
import { formatCurrency } from '@rentaldrivego/types'
|
||||||
import { useDashboardI18n } from '@/components/I18nProvider'
|
import { useDashboardI18n } from '@/components/I18nProvider'
|
||||||
|
|
||||||
@@ -31,35 +32,13 @@ interface AddVehicleModalProps {
|
|||||||
onSaved: () => void
|
onSaved: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_BADGE: Record<Vehicle['status'], string> = {
|
type EmployeeRole = 'OWNER' | 'MANAGER' | 'AGENT'
|
||||||
AVAILABLE: 'badge-green',
|
|
||||||
RESERVED: 'badge-blue',
|
|
||||||
READY: 'badge-purple',
|
|
||||||
RENTED: 'badge-indigo',
|
|
||||||
RETURNED: 'badge-amber',
|
|
||||||
NEEDS_CLEANING: 'badge-amber',
|
|
||||||
MAINTENANCE: 'badge-amber',
|
|
||||||
DAMAGE_REVIEW: 'badge-red',
|
|
||||||
OUT_OF_SERVICE: 'badge-red',
|
|
||||||
}
|
|
||||||
|
|
||||||
const ALL_STATUSES: Vehicle['status'][] = [
|
const ALL_STATUSES: Vehicle['status'][] = [
|
||||||
'AVAILABLE', 'RESERVED', 'READY', 'RENTED',
|
'AVAILABLE', 'RESERVED', 'READY', 'RENTED',
|
||||||
'RETURNED', 'NEEDS_CLEANING', 'MAINTENANCE', 'DAMAGE_REVIEW', 'OUT_OF_SERVICE',
|
'RETURNED', 'NEEDS_CLEANING', 'MAINTENANCE', 'DAMAGE_REVIEW', 'OUT_OF_SERVICE',
|
||||||
]
|
]
|
||||||
|
|
||||||
const STATUS_DOT: Record<Vehicle['status'], string> = {
|
|
||||||
AVAILABLE: 'bg-green-500',
|
|
||||||
RESERVED: 'bg-blue-500',
|
|
||||||
READY: 'bg-teal-500',
|
|
||||||
RENTED: 'bg-indigo-500',
|
|
||||||
RETURNED: 'bg-amber-400',
|
|
||||||
NEEDS_CLEANING: 'bg-amber-500',
|
|
||||||
MAINTENANCE: 'bg-orange-500',
|
|
||||||
DAMAGE_REVIEW: 'bg-red-400',
|
|
||||||
OUT_OF_SERVICE: 'bg-red-600',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MaintenanceModalProps {
|
interface MaintenanceModalProps {
|
||||||
vehicleId: string
|
vehicleId: string
|
||||||
vehicleName: string
|
vehicleName: string
|
||||||
@@ -156,88 +135,48 @@ function MaintenanceModal({ vehicleId, vehicleName, open, onClose, onSaved }: Ma
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusDropdown({ vehicleId, status, onStatusChange }: {
|
function StatusDropdown({ vehicleId, status, onStatusChange, disabled, disabledReason, onError }: {
|
||||||
vehicleId: string
|
vehicleId: string
|
||||||
status: Vehicle['status']
|
status: Vehicle['status']
|
||||||
onStatusChange: (id: string, status: Vehicle['status']) => void
|
onStatusChange: (id: string, status: Vehicle['status']) => void
|
||||||
|
disabled?: boolean
|
||||||
|
disabledReason?: string
|
||||||
|
onError: (message: string) => void
|
||||||
}) {
|
}) {
|
||||||
const { dict } = useDashboardI18n()
|
const { dict } = useDashboardI18n()
|
||||||
const f = dict.fleet
|
const f = dict.fleet
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [pos, setPos] = useState({ top: 0, left: 0 })
|
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
||||||
const panelRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const openDropdown = () => {
|
|
||||||
if (triggerRef.current) {
|
|
||||||
const r = triggerRef.current.getBoundingClientRect()
|
|
||||||
setPos({ top: r.bottom + 4, left: r.left })
|
|
||||||
}
|
|
||||||
setOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return
|
|
||||||
const close = (e: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
!triggerRef.current?.contains(e.target as Node) &&
|
|
||||||
!panelRef.current?.contains(e.target as Node)
|
|
||||||
) setOpen(false)
|
|
||||||
}
|
|
||||||
const closeOnScroll = () => setOpen(false)
|
|
||||||
document.addEventListener('mousedown', close)
|
|
||||||
window.addEventListener('scroll', closeOnScroll, true)
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', close)
|
|
||||||
window.removeEventListener('scroll', closeOnScroll, true)
|
|
||||||
}
|
|
||||||
}, [open])
|
|
||||||
|
|
||||||
const select = async (next: Vehicle['status']) => {
|
const select = async (next: Vehicle['status']) => {
|
||||||
if (next === status) { setOpen(false); return }
|
if (next === status) return
|
||||||
setOpen(false)
|
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
await apiFetch(`/vehicles/${vehicleId}/status`, { method: 'PATCH', body: JSON.stringify({ status: next }) })
|
await apiFetch(`/vehicles/${vehicleId}/status`, { method: 'PATCH', body: JSON.stringify({ status: next }) })
|
||||||
onStatusChange(vehicleId, next)
|
onStatusChange(vehicleId, next)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.message)
|
onError(err.message ?? 'Failed to update vehicle status')
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<select
|
||||||
<button
|
value={status}
|
||||||
ref={triggerRef}
|
disabled={disabled || saving}
|
||||||
onClick={openDropdown}
|
title={disabled ? disabledReason : undefined}
|
||||||
disabled={saving}
|
onChange={(e) => void select(e.target.value as Vehicle['status'])}
|
||||||
className={`inline-flex items-center gap-1 ${STATUS_BADGE[status]} cursor-pointer select-none`}
|
className={[
|
||||||
>
|
'rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 shadow-sm',
|
||||||
{saving ? '…' : f.statusLabels[status]}
|
disabled || saving ? 'cursor-not-allowed opacity-60' : 'cursor-pointer hover:border-slate-300',
|
||||||
<ChevronDown className="w-3 h-3 opacity-60" />
|
].join(' ')}
|
||||||
</button>
|
>
|
||||||
{open && (
|
{ALL_STATUSES.map((value) => (
|
||||||
<div
|
<option key={value} value={value}>
|
||||||
ref={panelRef}
|
{f.statusLabels[value]}
|
||||||
style={{ top: pos.top, left: pos.left }}
|
</option>
|
||||||
className="fixed z-50 min-w-[160px] rounded-xl border border-slate-100 bg-white shadow-lg py-1"
|
))}
|
||||||
>
|
</select>
|
||||||
{ALL_STATUSES.map((s) => (
|
|
||||||
<button
|
|
||||||
key={s}
|
|
||||||
onClick={() => select(s)}
|
|
||||||
className={`w-full flex items-center gap-2 px-3 py-2 text-sm text-start hover:bg-slate-50 transition-colors ${s === status ? 'font-semibold' : ''}`}
|
|
||||||
>
|
|
||||||
<span className={`inline-block w-2 h-2 rounded-full flex-shrink-0 ${STATUS_DOT[s]}`} />
|
|
||||||
{f.statusLabels[s]}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,12 +544,23 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function FleetPage() {
|
export default function FleetPage() {
|
||||||
const { dict } = useDashboardI18n()
|
const { dict, language } = useDashboardI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
const f = dict.fleet
|
const f = dict.fleet
|
||||||
|
const managerRequiredMessage =
|
||||||
|
language === 'fr'
|
||||||
|
? 'Cette action nécessite le rôle Gestionnaire ou supérieur.'
|
||||||
|
: language === 'ar'
|
||||||
|
? 'يتطلب هذا الإجراء دور المدير أو أعلى.'
|
||||||
|
: 'This action requires the Manager role or higher.'
|
||||||
const [vehicles, setVehicles] = useState<Vehicle[]>([])
|
const [vehicles, setVehicles] = useState<Vehicle[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [actionError, setActionError] = useState<string | null>(null)
|
||||||
const [showAddModal, setShowAddModal] = useState(false)
|
const [showAddModal, setShowAddModal] = useState(false)
|
||||||
|
const [employeeRole, setEmployeeRole] = useState<EmployeeRole | null>(null)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [statusFilter, setStatusFilter] = useState('')
|
const [statusFilter, setStatusFilter] = useState('')
|
||||||
const [categoryFilter, setCategoryFilter] = useState('')
|
const [categoryFilter, setCategoryFilter] = useState('')
|
||||||
@@ -627,12 +577,51 @@ export default function FleetPage() {
|
|||||||
|
|
||||||
useEffect(() => { fetchVehicles() }, [])
|
useEffect(() => { fetchVehicles() }, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cached = window.localStorage.getItem(EMPLOYEE_PROFILE_KEY)
|
||||||
|
if (cached) {
|
||||||
|
try {
|
||||||
|
const profile = JSON.parse(cached) as { role?: EmployeeRole }
|
||||||
|
if (profile.role) setEmployeeRole(profile.role)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
apiFetch<{ employee: { role: EmployeeRole } }>('/auth/employee/me')
|
||||||
|
.then(({ employee }) => {
|
||||||
|
if (cancelled) return
|
||||||
|
setEmployeeRole(employee.role)
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchParams.get('modal') === 'add-car') {
|
||||||
|
setShowAddModal(true)
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
|
||||||
|
const canManageFleet = employeeRole === 'OWNER' || employeeRole === 'MANAGER'
|
||||||
|
|
||||||
|
function closeAddModal() {
|
||||||
|
setShowAddModal(false)
|
||||||
|
if (searchParams.get('modal') === 'add-car') {
|
||||||
|
const params = new URLSearchParams(searchParams.toString())
|
||||||
|
params.delete('modal')
|
||||||
|
router.replace(params.toString() ? `${pathname}?${params.toString()}` : pathname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const togglePublished = async (id: string, current: boolean) => {
|
const togglePublished = async (id: string, current: boolean) => {
|
||||||
|
setActionError(null)
|
||||||
try {
|
try {
|
||||||
await apiFetch(`/vehicles/${id}`, { method: 'PATCH', body: JSON.stringify({ isPublished: !current }) })
|
await apiFetch(`/vehicles/${id}`, { method: 'PATCH', body: JSON.stringify({ isPublished: !current }) })
|
||||||
setVehicles((prev) => prev.map((v) => v.id === id ? { ...v, isPublished: !current } : v))
|
setVehicles((prev) => prev.map((v) => v.id === id ? { ...v, isPublished: !current } : v))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.message)
|
setActionError(err.statusCode === 403 ? managerRequiredMessage : (err.message ?? 'Failed to update publication status'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,11 +654,13 @@ export default function FleetPage() {
|
|||||||
<h2 className="text-xl font-semibold text-slate-900">{f.heading}</h2>
|
<h2 className="text-xl font-semibold text-slate-900">{f.heading}</h2>
|
||||||
<p className="text-sm text-slate-500 mt-0.5">{f.vehicleCount(vehicles.length)}</p>
|
<p className="text-sm text-slate-500 mt-0.5">{f.vehicleCount(vehicles.length)}</p>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setShowAddModal(true)} className="btn-primary">
|
<button onClick={() => setShowAddModal(true)} className="btn-primary" disabled={!canManageFleet} title={!canManageFleet ? managerRequiredMessage : undefined}>
|
||||||
<Plus className="w-4 h-4" /> {f.addVehicle}
|
<Plus className="w-4 h-4" /> {f.addVehicle}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{actionError ? <div className="card p-4 text-sm text-red-600">{actionError}</div> : null}
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="card p-4 flex flex-wrap gap-3">
|
<div className="card p-4 flex flex-wrap gap-3">
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-48">
|
<div className="flex items-center gap-2 flex-1 min-w-48">
|
||||||
@@ -739,7 +730,14 @@ export default function FleetPage() {
|
|||||||
<span className="badge-gray">{f.categoryLabels[vehicle.category as keyof typeof f.categoryLabels] ?? vehicle.category}</span>
|
<span className="badge-gray">{f.categoryLabels[vehicle.category as keyof typeof f.categoryLabels] ?? vehicle.category}</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<StatusDropdown vehicleId={vehicle.id} status={vehicle.status} onStatusChange={changeStatus} />
|
<StatusDropdown
|
||||||
|
vehicleId={vehicle.id}
|
||||||
|
status={vehicle.status}
|
||||||
|
onStatusChange={changeStatus}
|
||||||
|
disabled={!canManageFleet}
|
||||||
|
disabledReason={managerRequiredMessage}
|
||||||
|
onError={setActionError}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 text-sm font-medium text-slate-900">
|
<td className="px-6 py-4 text-sm font-medium text-slate-900">
|
||||||
{formatCurrency(vehicle.dailyRate, 'MAD')}{f.perDay}
|
{formatCurrency(vehicle.dailyRate, 'MAD')}{f.perDay}
|
||||||
@@ -748,8 +746,11 @@ export default function FleetPage() {
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => togglePublished(vehicle.id, vehicle.isPublished)}
|
onClick={() => togglePublished(vehicle.id, vehicle.isPublished)}
|
||||||
|
disabled={!canManageFleet}
|
||||||
|
title={!canManageFleet ? managerRequiredMessage : undefined}
|
||||||
className={[
|
className={[
|
||||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none',
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none',
|
||||||
|
!canManageFleet ? 'cursor-not-allowed opacity-60' : '',
|
||||||
vehicle.isPublished ? 'bg-blue-600' : 'bg-slate-200',
|
vehicle.isPublished ? 'bg-blue-600' : 'bg-slate-200',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
@@ -790,7 +791,7 @@ export default function FleetPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddVehicleModal open={showAddModal} onClose={() => setShowAddModal(false)} onSaved={fetchVehicles} />
|
<AddVehicleModal open={showAddModal} onClose={closeAddModal} onSaved={fetchVehicles} />
|
||||||
{maintenanceModal && (
|
{maintenanceModal && (
|
||||||
<MaintenanceModal
|
<MaintenanceModal
|
||||||
vehicleId={maintenanceModal.vehicleId}
|
vehicleId={maintenanceModal.vehicleId}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
import { formatCurrency } from '@rentaldrivego/types'
|
import { formatCurrency } from '@rentaldrivego/types'
|
||||||
import { apiFetch } from '@/lib/api'
|
import { apiFetch } from '@/lib/api'
|
||||||
import { useDashboardI18n } from '@/components/I18nProvider'
|
import { useDashboardI18n } from '@/components/I18nProvider'
|
||||||
@@ -25,18 +26,26 @@ interface ReservationRow {
|
|||||||
customer: { firstName: string; lastName: string; email: string }
|
customer: { firstName: string; lastName: string; email: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VehicleRow {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function ReservationsPage() {
|
export default function ReservationsPage() {
|
||||||
const { dict, language } = useDashboardI18n()
|
const { dict, language } = useDashboardI18n()
|
||||||
|
const router = useRouter()
|
||||||
const r = dict.reservations
|
const r = dict.reservations
|
||||||
const localeCode = language === 'fr' ? 'fr-FR' : language === 'ar' ? 'ar-MA' : 'en-US'
|
const localeCode = language === 'fr' ? 'fr-FR' : language === 'ar' ? 'ar-MA' : 'en-US'
|
||||||
|
|
||||||
const [rows, setRows] = useState<ReservationRow[]>([])
|
const [rows, setRows] = useState<ReservationRow[]>([])
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [loadError, setLoadError] = useState<string | null>(null)
|
||||||
|
const [actionError, setActionError] = useState<string | null>(null)
|
||||||
|
const [checkingNewReservation, setCheckingNewReservation] = useState(false)
|
||||||
|
const [showNoVehiclesModal, setShowNoVehiclesModal] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiFetch<ReservationRow[]>('/reservations?pageSize=100')
|
apiFetch<ReservationRow[]>('/reservations?pageSize=100')
|
||||||
.then((result) => setRows(result ?? []))
|
.then((result) => setRows(result ?? []))
|
||||||
.catch((err) => setError(err.message))
|
.catch((err) => setLoadError(err.message))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const formatDate = (iso: string) =>
|
const formatDate = (iso: string) =>
|
||||||
@@ -52,6 +61,40 @@ export default function ReservationsPage() {
|
|||||||
}
|
}
|
||||||
return language === 'fr' ? 'Voir la réservation' : language === 'ar' ? 'عرض الحجز' : 'View reservation'
|
return language === 'fr' ? 'Voir la réservation' : language === 'ar' ? 'عرض الحجز' : 'View reservation'
|
||||||
}
|
}
|
||||||
|
const noVehiclesMessage =
|
||||||
|
language === 'fr'
|
||||||
|
? 'Ajoutez au moins un véhicule à votre flotte avant de créer une réservation.'
|
||||||
|
: language === 'ar'
|
||||||
|
? 'أضف مركبة واحدة على الأقل إلى الأسطول قبل إنشاء حجز.'
|
||||||
|
: 'Add at least one vehicle to your fleet before creating a reservation.'
|
||||||
|
|
||||||
|
async function handleNewReservationClick() {
|
||||||
|
setActionError(null)
|
||||||
|
setCheckingNewReservation(true)
|
||||||
|
try {
|
||||||
|
const vehicles = await apiFetch<VehicleRow[]>('/vehicles?pageSize=1')
|
||||||
|
if (!vehicles || vehicles.length === 0) {
|
||||||
|
setShowNoVehiclesModal(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push('/reservations/new')
|
||||||
|
} catch (err: any) {
|
||||||
|
setActionError(err.message ?? (
|
||||||
|
language === 'fr'
|
||||||
|
? 'Impossible de vérifier la flotte avant de créer la réservation.'
|
||||||
|
: language === 'ar'
|
||||||
|
? 'تعذر التحقق من الأسطول قبل إنشاء الحجز.'
|
||||||
|
: 'Failed to verify fleet before creating the reservation.'
|
||||||
|
))
|
||||||
|
} finally {
|
||||||
|
setCheckingNewReservation(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddCar() {
|
||||||
|
setShowNoVehiclesModal(false)
|
||||||
|
router.push('/fleet?modal=add-car')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -60,13 +103,29 @@ export default function ReservationsPage() {
|
|||||||
<h2 className="text-xl font-semibold text-slate-900">{r.heading}</h2>
|
<h2 className="text-xl font-semibold text-slate-900">{r.heading}</h2>
|
||||||
<p className="text-sm text-slate-500 mt-1">{r.subtitle}</p>
|
<p className="text-sm text-slate-500 mt-1">{r.subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/reservations/new" className="btn-primary whitespace-nowrap">
|
<button
|
||||||
{language === 'fr' ? 'Réserver une voiture' : language === 'ar' ? 'حجز سيارة' : 'Book car'}
|
type="button"
|
||||||
</Link>
|
className="btn-primary whitespace-nowrap"
|
||||||
|
onClick={handleNewReservationClick}
|
||||||
|
disabled={checkingNewReservation}
|
||||||
|
>
|
||||||
|
{checkingNewReservation
|
||||||
|
? language === 'fr'
|
||||||
|
? 'Vérification…'
|
||||||
|
: language === 'ar'
|
||||||
|
? 'جارٍ التحقق…'
|
||||||
|
: 'Checking…'
|
||||||
|
: language === 'fr'
|
||||||
|
? 'Réserver une voiture'
|
||||||
|
: language === 'ar'
|
||||||
|
? 'حجز سيارة'
|
||||||
|
: 'Book car'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{actionError ? <div className="card p-4 text-sm text-red-600">{actionError}</div> : null}
|
||||||
<div className="card overflow-hidden">
|
<div className="card overflow-hidden">
|
||||||
{error ? (
|
{loadError ? (
|
||||||
<div className="p-8 text-sm text-red-600">{error}</div>
|
<div className="p-8 text-sm text-red-600">{loadError}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
@@ -109,6 +168,29 @@ export default function ReservationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{showNoVehiclesModal ? (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-blue-950/40 p-4"
|
||||||
|
onClick={(event) => {
|
||||||
|
if (event.target === event.currentTarget) setShowNoVehiclesModal(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl">
|
||||||
|
<h3 className="text-lg font-semibold text-slate-900">
|
||||||
|
{language === 'fr' ? 'Ajouter un véhicule' : language === 'ar' ? 'أضف مركبة' : 'Add a car'}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm text-slate-600">{noVehiclesMessage}</p>
|
||||||
|
<div className="mt-6 flex gap-3">
|
||||||
|
<button type="button" className="btn-primary flex-1" onClick={handleAddCar}>
|
||||||
|
{language === 'fr' ? 'Ajouter une voiture' : language === 'ar' ? 'إضافة مركبة' : 'Add car'}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn-secondary flex-1" onClick={() => setShowNoVehiclesModal(false)}>
|
||||||
|
{language === 'fr' ? 'Annuler' : language === 'ar' ? 'إلغاء' : 'Cancel'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user