From 6eeba99c45b03c439a4c6fc0f13fc6269bc80247 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 11 Jun 2026 12:30:41 -0400 Subject: [PATCH] prevent booking when no car added --- .../src/app/(dashboard)/fleet/page.tsx | 185 +++++++++--------- .../src/app/(dashboard)/reservations/page.tsx | 96 ++++++++- 2 files changed, 182 insertions(+), 99 deletions(-) diff --git a/apps/dashboard/src/app/(dashboard)/fleet/page.tsx b/apps/dashboard/src/app/(dashboard)/fleet/page.tsx index 96f39fa..84c60b6 100644 --- a/apps/dashboard/src/app/(dashboard)/fleet/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/fleet/page.tsx @@ -1,10 +1,11 @@ 'use client' 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 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 { useDashboardI18n } from '@/components/I18nProvider' @@ -31,35 +32,13 @@ interface AddVehicleModalProps { onSaved: () => void } -const STATUS_BADGE: Record = { - 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', -} +type EmployeeRole = 'OWNER' | 'MANAGER' | 'AGENT' const ALL_STATUSES: Vehicle['status'][] = [ 'AVAILABLE', 'RESERVED', 'READY', 'RENTED', 'RETURNED', 'NEEDS_CLEANING', 'MAINTENANCE', 'DAMAGE_REVIEW', 'OUT_OF_SERVICE', ] -const STATUS_DOT: Record = { - 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 { vehicleId: 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 status: Vehicle['status'] onStatusChange: (id: string, status: Vehicle['status']) => void + disabled?: boolean + disabledReason?: string + onError: (message: string) => void }) { const { dict } = useDashboardI18n() const f = dict.fleet - const [open, setOpen] = useState(false) const [saving, setSaving] = useState(false) - const [pos, setPos] = useState({ top: 0, left: 0 }) - const triggerRef = useRef(null) - const panelRef = useRef(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']) => { - if (next === status) { setOpen(false); return } - setOpen(false) + if (next === status) return setSaving(true) try { await apiFetch(`/vehicles/${vehicleId}/status`, { method: 'PATCH', body: JSON.stringify({ status: next }) }) onStatusChange(vehicleId, next) } catch (err: any) { - alert(err.message) + onError(err.message ?? 'Failed to update vehicle status') } finally { setSaving(false) } } return ( - <> - - {open && ( -
- {ALL_STATUSES.map((s) => ( - - ))} -
- )} - + ) } @@ -605,12 +544,23 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) { } 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 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([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [actionError, setActionError] = useState(null) const [showAddModal, setShowAddModal] = useState(false) + const [employeeRole, setEmployeeRole] = useState(null) const [search, setSearch] = useState('') const [statusFilter, setStatusFilter] = useState('') const [categoryFilter, setCategoryFilter] = useState('') @@ -627,12 +577,51 @@ export default function FleetPage() { 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) => { + setActionError(null) try { await apiFetch(`/vehicles/${id}`, { method: 'PATCH', body: JSON.stringify({ isPublished: !current }) }) setVehicles((prev) => prev.map((v) => v.id === id ? { ...v, isPublished: !current } : v)) } 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() {

{f.heading}

{f.vehicleCount(vehicles.length)}

- + {actionError ?
{actionError}
: null} + {/* Filters */}
@@ -739,7 +730,14 @@ export default function FleetPage() { {f.categoryLabels[vehicle.category as keyof typeof f.categoryLabels] ?? vehicle.category} - + {formatCurrency(vehicle.dailyRate, 'MAD')}{f.perDay} @@ -748,8 +746,11 @@ export default function FleetPage() {
- setShowAddModal(false)} onSaved={fetchVehicles} /> + {maintenanceModal && ( ([]) - const [error, setError] = useState(null) + const [loadError, setLoadError] = useState(null) + const [actionError, setActionError] = useState(null) + const [checkingNewReservation, setCheckingNewReservation] = useState(false) + const [showNoVehiclesModal, setShowNoVehiclesModal] = useState(false) useEffect(() => { apiFetch('/reservations?pageSize=100') .then((result) => setRows(result ?? [])) - .catch((err) => setError(err.message)) + .catch((err) => setLoadError(err.message)) }, []) const formatDate = (iso: string) => @@ -52,6 +61,40 @@ export default function ReservationsPage() { } 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('/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 (
@@ -60,13 +103,29 @@ export default function ReservationsPage() {

{r.heading}

{r.subtitle}

- - {language === 'fr' ? 'Réserver une voiture' : language === 'ar' ? 'حجز سيارة' : 'Book car'} - +
+ {actionError ?
{actionError}
: null}
- {error ? ( -
{error}
+ {loadError ? ( +
{loadError}
) : (
@@ -109,6 +168,29 @@ export default function ReservationsPage() { )} + {showNoVehiclesModal ? ( +
{ + if (event.target === event.currentTarget) setShowNoVehiclesModal(false) + }} + > +
+

+ {language === 'fr' ? 'Ajouter un véhicule' : language === 'ar' ? 'أضف مركبة' : 'Add a car'} +

+

{noVehiclesMessage}

+
+ + +
+
+
+ ) : null} ) }