reservation implementation
This commit is contained in:
@@ -22,6 +22,12 @@ interface OnlineReservation {
|
||||
createdAt: string
|
||||
vehicle: { make: string; model: string; year: number; licensePlate: string }
|
||||
customer: { firstName: string; lastName: string; email: string; phone: string | null }
|
||||
bookingRequest: {
|
||||
stage: string
|
||||
documentsRequired: boolean
|
||||
paymentRequired: boolean
|
||||
documentsMissing: string[]
|
||||
}
|
||||
}
|
||||
|
||||
const STATUS_STYLE: Record<string, string> = {
|
||||
@@ -42,6 +48,33 @@ const STATUS_LABEL: Record<string, string> = {
|
||||
NO_SHOW: 'No show',
|
||||
}
|
||||
|
||||
const REQUEST_STAGE_STYLE: Record<string, string> = {
|
||||
REQUEST_SENT: 'bg-orange-100 text-orange-800',
|
||||
COMPANY_CONFIRMED: 'bg-blue-100 text-blue-800',
|
||||
DOCUMENTS_REQUIRED: 'bg-amber-100 text-amber-800',
|
||||
PAYMENT_REQUIRED: 'bg-purple-100 text-purple-800',
|
||||
BOOKING_CONFIRMED: 'bg-emerald-100 text-emerald-700',
|
||||
CANCELLED: 'bg-rose-100 text-rose-700',
|
||||
}
|
||||
|
||||
const REQUEST_STAGE_LABEL: Record<string, string> = {
|
||||
REQUEST_SENT: 'Waiting for company review',
|
||||
COMPANY_CONFIRMED: 'Availability confirmed',
|
||||
DOCUMENTS_REQUIRED: 'Driver documents missing',
|
||||
PAYMENT_REQUIRED: 'Payment or deposit pending',
|
||||
BOOKING_CONFIRMED: 'Ready to proceed',
|
||||
CANCELLED: 'Request closed',
|
||||
}
|
||||
|
||||
const MISSING_ITEM_LABEL: Record<string, string> = {
|
||||
DATE_OF_BIRTH: 'Date of birth',
|
||||
IDENTITY_DOCUMENT: 'Identity document',
|
||||
FULL_ADDRESS: 'Full address',
|
||||
DRIVER_LICENSE: 'Driver license number',
|
||||
LICENSE_IMAGE: 'License image',
|
||||
LICENSE_APPROVAL: 'License approval',
|
||||
}
|
||||
|
||||
function DeclineModal({ onConfirm, onCancel, loading }: { onConfirm: (reason: string) => void; onCancel: () => void; loading: boolean }) {
|
||||
const [reason, setReason] = useState('')
|
||||
return (
|
||||
@@ -237,6 +270,8 @@ function ReservationCard({ r, acting, onConfirm, onDecline }: {
|
||||
onDecline: () => void
|
||||
}) {
|
||||
const days = r.totalDays
|
||||
const missingItems = r.bookingRequest.documentsMissing.map((item) => MISSING_ITEM_LABEL[item] ?? item)
|
||||
|
||||
return (
|
||||
<div className="card overflow-hidden border-l-4 border-l-orange-400">
|
||||
<div className="p-5">
|
||||
@@ -267,6 +302,27 @@ function ReservationCard({ r, acting, onConfirm, onDecline }: {
|
||||
<p className="text-sm text-slate-600 italic">"{r.notes}"</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className={`inline-flex rounded-full px-2.5 py-1 text-xs font-semibold ${REQUEST_STAGE_STYLE[r.bookingRequest.stage] ?? 'bg-slate-100 text-slate-700'}`}>
|
||||
{REQUEST_STAGE_LABEL[r.bookingRequest.stage] ?? r.bookingRequest.stage}
|
||||
</span>
|
||||
{r.bookingRequest.paymentRequired ? (
|
||||
<span className="inline-flex rounded-full bg-purple-100 px-2.5 py-1 text-xs font-semibold text-purple-800">
|
||||
Payment pending
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{missingItems.length > 0 ? (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{missingItems.map((item) => (
|
||||
<span key={item} className="inline-flex rounded-full bg-amber-100 px-2.5 py-1 text-xs font-medium text-amber-800">
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: total + actions */}
|
||||
|
||||
@@ -66,6 +66,13 @@ interface ReservationDetail {
|
||||
checkInInspectionEditable: boolean
|
||||
checkOutInspectionEditable: boolean
|
||||
}
|
||||
bookingRequest: {
|
||||
stage: string
|
||||
companyConfirmed: boolean
|
||||
documentsRequired: boolean
|
||||
paymentRequired: boolean
|
||||
documentsMissing: string[]
|
||||
}
|
||||
}
|
||||
|
||||
type EditMode = 'booking' | 'return' | null
|
||||
@@ -118,6 +125,24 @@ const detailCopy = {
|
||||
checkOutReadOnly: 'Check-out inspection becomes editable once the vehicle is returned.',
|
||||
inspectionClosed: 'This reservation is closed. Inspection edits are disabled.',
|
||||
licenseImageLabel: 'License image',
|
||||
bookingProgress: 'Booking progress',
|
||||
paymentPending: 'Payment or deposit still pending',
|
||||
stageLabels: {
|
||||
REQUEST_SENT: 'Waiting for company review',
|
||||
COMPANY_CONFIRMED: 'Availability confirmed',
|
||||
DOCUMENTS_REQUIRED: 'Driver documents missing',
|
||||
PAYMENT_REQUIRED: 'Payment or deposit pending',
|
||||
BOOKING_CONFIRMED: 'Ready to proceed',
|
||||
CANCELLED: 'Request closed',
|
||||
},
|
||||
missingItems: {
|
||||
DATE_OF_BIRTH: 'Date of birth',
|
||||
IDENTITY_DOCUMENT: 'Identity document',
|
||||
FULL_ADDRESS: 'Full address',
|
||||
DRIVER_LICENSE: 'Driver license number',
|
||||
LICENSE_IMAGE: 'License image',
|
||||
LICENSE_APPROVAL: 'License approval',
|
||||
},
|
||||
noLicenseImage: 'No license image uploaded.',
|
||||
pickupPhotosTitle: 'Pickup photos',
|
||||
dropoffPhotosTitle: 'Return photos',
|
||||
@@ -167,6 +192,24 @@ const detailCopy = {
|
||||
checkOutReadOnly: 'L’inspection de retour devient modifiable une fois le véhicule retourné.',
|
||||
inspectionClosed: 'Cette réservation est clôturée. Les modifications d’inspection sont désactivées.',
|
||||
licenseImageLabel: 'Image du permis',
|
||||
bookingProgress: 'Progression de la réservation',
|
||||
paymentPending: 'Paiement ou dépôt encore en attente',
|
||||
stageLabels: {
|
||||
REQUEST_SENT: 'En attente de validation',
|
||||
COMPANY_CONFIRMED: 'Disponibilité confirmée',
|
||||
DOCUMENTS_REQUIRED: 'Documents conducteur manquants',
|
||||
PAYMENT_REQUIRED: 'Paiement ou dépôt en attente',
|
||||
BOOKING_CONFIRMED: 'Prêt à avancer',
|
||||
CANCELLED: 'Demande clôturée',
|
||||
},
|
||||
missingItems: {
|
||||
DATE_OF_BIRTH: 'Date de naissance',
|
||||
IDENTITY_DOCUMENT: 'Pièce d’identité',
|
||||
FULL_ADDRESS: 'Adresse complète',
|
||||
DRIVER_LICENSE: 'Numéro du permis',
|
||||
LICENSE_IMAGE: 'Image du permis',
|
||||
LICENSE_APPROVAL: 'Validation du permis',
|
||||
},
|
||||
noLicenseImage: 'Aucune image de permis téléversée.',
|
||||
pickupPhotosTitle: 'Photos de départ',
|
||||
dropoffPhotosTitle: 'Photos de retour',
|
||||
@@ -216,6 +259,24 @@ const detailCopy = {
|
||||
checkOutReadOnly: 'يصبح فحص الإرجاع قابلاً للتعديل بعد عودة السيارة.',
|
||||
inspectionClosed: 'هذا الحجز مغلق. تم تعطيل تعديل الفحص.',
|
||||
licenseImageLabel: 'صورة الرخصة',
|
||||
bookingProgress: 'تقدم الحجز',
|
||||
paymentPending: 'الدفع أو العربون ما زال معلقاً',
|
||||
stageLabels: {
|
||||
REQUEST_SENT: 'بانتظار مراجعة الشركة',
|
||||
COMPANY_CONFIRMED: 'تم تأكيد التوفر',
|
||||
DOCUMENTS_REQUIRED: 'وثائق السائق ناقصة',
|
||||
PAYMENT_REQUIRED: 'الدفع أو العربون معلق',
|
||||
BOOKING_CONFIRMED: 'جاهز للمتابعة',
|
||||
CANCELLED: 'تم إغلاق الطلب',
|
||||
},
|
||||
missingItems: {
|
||||
DATE_OF_BIRTH: 'تاريخ الميلاد',
|
||||
IDENTITY_DOCUMENT: 'وثيقة الهوية',
|
||||
FULL_ADDRESS: 'العنوان الكامل',
|
||||
DRIVER_LICENSE: 'رقم الرخصة',
|
||||
LICENSE_IMAGE: 'صورة الرخصة',
|
||||
LICENSE_APPROVAL: 'اعتماد الرخصة',
|
||||
},
|
||||
noLicenseImage: 'لم يتم رفع صورة الرخصة.',
|
||||
pickupPhotosTitle: 'صور الاستلام',
|
||||
dropoffPhotosTitle: 'صور الإرجاع',
|
||||
@@ -534,6 +595,31 @@ export default function ReservationDetailPage() {
|
||||
|
||||
{actionError && <div className="card p-4 text-sm text-red-600">{actionError}</div>}
|
||||
|
||||
{reservation.source === 'MARKETPLACE' ? (
|
||||
<div className="card p-6">
|
||||
<h3 className="mb-4 text-base font-semibold text-slate-900">{copy.bookingProgress}</h3>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="badge-gray">
|
||||
{copy.stageLabels[reservation.bookingRequest.stage as keyof typeof copy.stageLabels] ?? reservation.bookingRequest.stage}
|
||||
</span>
|
||||
{reservation.bookingRequest.paymentRequired ? (
|
||||
<span className="rounded-full bg-purple-100 px-2.5 py-1 text-xs font-semibold text-purple-800">
|
||||
{copy.paymentPending}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{reservation.bookingRequest.documentsMissing.length > 0 ? (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{reservation.bookingRequest.documentsMissing.map((item) => (
|
||||
<span key={item} className="rounded-full bg-amber-100 px-2.5 py-1 text-xs font-semibold text-amber-800">
|
||||
{copy.missingItems[item as keyof typeof copy.missingItems] ?? item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<div className="card p-6">
|
||||
<h3 className="mb-4 text-base font-semibold text-slate-900">{r.sectionCustomer}</h3>
|
||||
|
||||
Reference in New Issue
Block a user