197 lines
8.4 KiB
TypeScript
197 lines
8.4 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import Link from 'next/link'
|
|
import { useRouter } from 'next/navigation'
|
|
import { formatCurrency } from '@rentaldrivego/types'
|
|
import { apiFetch } from '@/lib/api'
|
|
import { useDashboardI18n } from '@/components/I18nProvider'
|
|
|
|
interface ReservationRow {
|
|
id: string
|
|
status: string
|
|
source: string
|
|
startDate: string
|
|
endDate: string
|
|
totalAmount: number
|
|
totalDays: number
|
|
contractNumber?: string | null
|
|
workflow?: {
|
|
contractGenerated: boolean
|
|
closed: boolean
|
|
coreEditable: boolean
|
|
returnEditable: boolean
|
|
}
|
|
vehicle: { make: string; model: string }
|
|
customer: { firstName: string; lastName: string; email: string }
|
|
}
|
|
|
|
interface VehicleRow {
|
|
id: string
|
|
}
|
|
|
|
export default function ReservationsPage() {
|
|
const { dict, language } = useDashboardI18n()
|
|
const router = useRouter()
|
|
const r = dict.reservations
|
|
const localeCode = language === 'fr' ? 'fr-FR' : language === 'ar' ? 'ar-MA' : 'en-US'
|
|
|
|
const [rows, setRows] = useState<ReservationRow[]>([])
|
|
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(() => {
|
|
apiFetch<ReservationRow[]>('/reservations?pageSize=100')
|
|
.then((result) => setRows(result ?? []))
|
|
.catch((err) => setLoadError(err.message))
|
|
}, [])
|
|
|
|
const formatDate = (iso: string) =>
|
|
new Date(iso).toLocaleDateString(localeCode, { month: 'short', day: 'numeric' })
|
|
const formatDateYear = (iso: string) =>
|
|
new Date(iso).toLocaleDateString(localeCode, { month: 'short', day: 'numeric', year: 'numeric' })
|
|
const reservationActionLabel = (row: ReservationRow) => {
|
|
if (row.workflow?.returnEditable) {
|
|
return language === 'fr' ? 'Retour / clôture' : language === 'ar' ? 'الإرجاع / الإغلاق' : 'Return / close'
|
|
}
|
|
if (row.workflow?.coreEditable) {
|
|
return language === 'fr' ? 'Modifier la réservation' : language === 'ar' ? 'تعديل الحجز' : 'Edit 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 (
|
|
<div className="space-y-6">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<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>
|
|
<button
|
|
type="button"
|
|
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>
|
|
{actionError ? <div className="card p-4 text-sm text-red-600">{actionError}</div> : null}
|
|
<div className="card overflow-hidden">
|
|
{loadError ? (
|
|
<div className="p-8 text-sm text-red-600">{loadError}</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="bg-slate-50 border-b border-slate-200">
|
|
<th className="text-left px-6 py-3 text-xs font-medium text-slate-500 uppercase tracking-wide">{r.colCustomer}</th>
|
|
<th className="text-left px-6 py-3 text-xs font-medium text-slate-500 uppercase tracking-wide">{r.colVehicle}</th>
|
|
<th className="text-left px-6 py-3 text-xs font-medium text-slate-500 uppercase tracking-wide">{r.colDates}</th>
|
|
<th className="text-left px-6 py-3 text-xs font-medium text-slate-500 uppercase tracking-wide">{r.colSource}</th>
|
|
<th className="text-left px-6 py-3 text-xs font-medium text-slate-500 uppercase tracking-wide">{r.colStatus}</th>
|
|
<th className="text-right px-6 py-3 text-xs font-medium text-slate-500 uppercase tracking-wide">{r.colTotal}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{rows.map((row) => (
|
|
<tr key={row.id}>
|
|
<td className="px-6 py-4">
|
|
<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={`/reservations/${row.id}`} className="mt-1 inline-block text-xs font-semibold text-blue-700 hover:underline">
|
|
{reservationActionLabel(row)}
|
|
</Link>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-slate-700">{row.vehicle.make} {row.vehicle.model}</td>
|
|
<td className="px-6 py-4 text-sm text-slate-500">{formatDate(row.startDate)} - {formatDateYear(row.endDate)}</td>
|
|
<td className="px-6 py-4 text-sm text-slate-700">{row.source}</td>
|
|
<td className="px-6 py-4"><span className="badge-blue">{row.status}</span></td>
|
|
<td className="px-6 py-4 text-right text-sm font-semibold text-slate-900">{formatCurrency(row.totalAmount, 'MAD')}</td>
|
|
</tr>
|
|
))}
|
|
{rows.length === 0 && (
|
|
<tr>
|
|
<td colSpan={6} className="px-6 py-10 text-center text-sm text-slate-400">{r.noReservations}</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</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>
|
|
)
|
|
}
|