259 lines
10 KiB
TypeScript
259 lines
10 KiB
TypeScript
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { api } from '../api';
|
|
import { useAuth } from '../context/AuthContext';
|
|
import MapView, { MapMarker } from '../components/MapView';
|
|
import DriverAvailabilityToggle from '../components/features/DriverAvailabilityToggle';
|
|
import DriverRequestCard from '../components/features/DriverRequestCard';
|
|
import DriverTripNavigation from '../components/features/DriverTripNavigation';
|
|
import { Ride } from '../types/ride';
|
|
import { Car, Navigation, Radio, RefreshCw, ShieldAlert } from 'lucide-react';
|
|
import { Link } from 'react-router-dom';
|
|
|
|
export default function DriverWorkstation() {
|
|
const { user } = useAuth();
|
|
const queryClient = useQueryClient();
|
|
|
|
// Fetch driver profile to check setup & availability
|
|
const { data: profileRes, isLoading: isProfileLoading, refetch: refetchProfile } = useQuery({
|
|
queryKey: ['driver-profile'],
|
|
queryFn: async () => {
|
|
const res = await api.get('/driver/profile');
|
|
return res.data;
|
|
},
|
|
});
|
|
|
|
const driverProfile = profileRes?.profile;
|
|
const isAvailable = Boolean(driverProfile?.is_available);
|
|
|
|
// Fetch driver's current rides to locate an active trip
|
|
const { data: myRidesRes, refetch: refetchMyRides } = useQuery({
|
|
queryKey: ['rides-my'],
|
|
queryFn: async () => {
|
|
const res = await api.get('/rides/my');
|
|
return res.data;
|
|
},
|
|
refetchInterval: 5000,
|
|
});
|
|
|
|
const myRides: Ride[] = Array.isArray(myRidesRes?.rides) ? myRidesRes.rides : [];
|
|
const activeRide = myRides.find(
|
|
(r) => r.status === 'accepted' || r.status === 'picked_up'
|
|
);
|
|
|
|
// Fetch nearby ride requests when available & not in an active trip
|
|
const { refetch: refetchNearby } = useQuery({
|
|
queryKey: ['rides-nearby'],
|
|
queryFn: async () => {
|
|
const res = await api.get('/rides/nearby');
|
|
return res.data;
|
|
},
|
|
enabled: isAvailable && !activeRide,
|
|
refetchInterval: isAvailable && !activeRide ? 4000 : false,
|
|
});
|
|
|
|
const handleRefresh = () => {
|
|
refetchProfile();
|
|
refetchMyRides();
|
|
if (isAvailable && !activeRide) refetchNearby();
|
|
};
|
|
|
|
const handleAcceptRide = () => {
|
|
queryClient.invalidateQueries({ queryKey: ['rides-my'] });
|
|
queryClient.invalidateQueries({ queryKey: ['rides-nearby'] });
|
|
};
|
|
|
|
const handleRideUpdated = () => {
|
|
queryClient.invalidateQueries({ queryKey: ['rides-my'] });
|
|
queryClient.invalidateQueries({ queryKey: ['rides-nearby'] });
|
|
};
|
|
|
|
// Determine map markers strictly matching MapMarker contract
|
|
const mapMarkers: MapMarker[] = [];
|
|
if (activeRide) {
|
|
if (typeof activeRide.pickup_lat === 'number' && typeof activeRide.pickup_lng === 'number') {
|
|
mapMarkers.push({
|
|
id: 'pickup',
|
|
lat: activeRide.pickup_lat,
|
|
lng: activeRide.pickup_lng,
|
|
label: `Pickup: ${activeRide.pickup_location}`,
|
|
type: 'pickup',
|
|
});
|
|
}
|
|
if (typeof activeRide.destination_lat === 'number' && typeof activeRide.destination_lng === 'number') {
|
|
mapMarkers.push({
|
|
id: 'destination',
|
|
lat: activeRide.destination_lat,
|
|
lng: activeRide.destination_lng,
|
|
label: `Destination: ${activeRide.destination_location}`,
|
|
type: 'destination',
|
|
});
|
|
}
|
|
if (typeof activeRide.driver_lat === 'number' && typeof activeRide.driver_lng === 'number') {
|
|
mapMarkers.push({
|
|
id: 'driver',
|
|
lat: activeRide.driver_lat,
|
|
lng: activeRide.driver_lng,
|
|
label: 'Your Vehicle Position',
|
|
type: 'driver',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Calculate polyline for route
|
|
const polyline: [number, number][] | undefined =
|
|
activeRide &&
|
|
typeof activeRide.pickup_lat === 'number' &&
|
|
typeof activeRide.pickup_lng === 'number' &&
|
|
typeof activeRide.destination_lat === 'number' &&
|
|
typeof activeRide.destination_lng === 'number'
|
|
? [
|
|
[activeRide.pickup_lat, activeRide.pickup_lng],
|
|
[activeRide.destination_lat, activeRide.destination_lng],
|
|
]
|
|
: undefined;
|
|
|
|
// Determine map center
|
|
const mapCenter: [number, number] =
|
|
activeRide && typeof activeRide.driver_lat === 'number' && typeof activeRide.driver_lng === 'number'
|
|
? [activeRide.driver_lat, activeRide.driver_lng]
|
|
: activeRide && typeof activeRide.pickup_lat === 'number' && typeof activeRide.pickup_lng === 'number'
|
|
? [activeRide.pickup_lat, activeRide.pickup_lng]
|
|
: [37.7749, -122.4194];
|
|
|
|
if (!driverProfile && !isProfileLoading) {
|
|
return (
|
|
<div className="container max-w-3xl py-12 px-4">
|
|
<div className="bg-card border border-border rounded-xl p-8 text-center space-y-4 shadow-xs">
|
|
<div className="inline-flex p-3 rounded-full bg-amber-500/10 text-amber-600">
|
|
<Car className="w-8 h-8" />
|
|
</div>
|
|
<h2 className="text-2xl font-bold">Driver Profile Required</h2>
|
|
<p className="text-muted-foreground max-w-md mx-auto text-sm">
|
|
To receive ride dispatches and go online as a driver, please register your vehicle details first.
|
|
</p>
|
|
<div className="pt-2">
|
|
<Link
|
|
to="/app/onboarding"
|
|
className="inline-flex items-center gap-2 bg-primary text-primary-foreground text-sm font-semibold px-6 py-2.5 rounded-lg hover:opacity-90 transition"
|
|
>
|
|
Complete Driver Setup
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container max-w-7xl py-6 px-4 space-y-6">
|
|
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-card border border-border p-4 rounded-xl shadow-xs">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2.5 rounded-lg bg-slate-900 text-white dark:bg-slate-800">
|
|
<Navigation className="w-6 h-6 text-sky-400" />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<h1 className="text-xl font-bold tracking-tight">Driver Workstation</h1>
|
|
{driverProfile && (
|
|
<span className="text-xs px-2 py-0.5 rounded-full bg-slate-100 text-slate-700 font-mono dark:bg-slate-800 dark:text-slate-300">
|
|
{driverProfile.vehicle_make} {driverProfile.vehicle_model} • {driverProfile.license_plate}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{activeRide
|
|
? 'Active navigation in progress'
|
|
: isAvailable
|
|
? 'Online — Scanning for nearby dispatch requests'
|
|
: 'Offline — Switch online to receive requests'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 self-end md:self-auto">
|
|
<button
|
|
onClick={handleRefresh}
|
|
className="p-2 rounded-lg border border-border hover:bg-muted text-muted-foreground transition"
|
|
title="Refresh dispatch feeds"
|
|
>
|
|
<RefreshCw className="w-4 h-4" />
|
|
</button>
|
|
|
|
<DriverAvailabilityToggle
|
|
onStatusChange={() => {
|
|
queryClient.invalidateQueries({ queryKey: ['driver-profile'] });
|
|
queryClient.invalidateQueries({ queryKey: ['rides-nearby'] });
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Workstation View Split */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
|
|
|
|
<div className="lg:col-span-5 space-y-6">
|
|
{activeRide ? (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-2 text-xs font-semibold text-sky-600 bg-sky-50 dark:bg-sky-950/40 dark:text-sky-400 px-3 py-1.5 rounded-lg border border-sky-200 dark:border-sky-800">
|
|
<Radio className="w-4 h-4 animate-pulse text-sky-500" />
|
|
<span>Active Trip Engagement</span>
|
|
</div>
|
|
<DriverTripNavigation ride={activeRide} onRideUpdated={handleRideUpdated} />
|
|
</div>
|
|
) : (
|
|
<div className="bg-card border border-border rounded-xl p-4 space-y-4 shadow-xs">
|
|
<div className="flex items-center justify-between border-b border-border pb-3">
|
|
<h2 className="font-semibold flex items-center gap-2 text-sm">
|
|
<Radio className={`w-4 h-4 ${isAvailable ? 'text-emerald-500 animate-pulse' : 'text-slate-400'}`} />
|
|
Nearby Dispatch Feed
|
|
</h2>
|
|
<span className="text-xs text-muted-foreground font-mono">
|
|
{isAvailable ? 'Live' : 'Paused'}
|
|
</span>
|
|
</div>
|
|
|
|
{!isAvailable ? (
|
|
<div className="py-8 text-center space-y-3">
|
|
<div className="inline-flex p-3 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500">
|
|
<ShieldAlert className="w-6 h-6" />
|
|
</div>
|
|
<h3 className="text-sm font-semibold">You are currently Offline</h3>
|
|
<p className="text-xs text-muted-foreground max-w-xs mx-auto">
|
|
Toggle your availability to "Online" in the header bar above to receive nearby passenger ride dispatches.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<DriverRequestCard onAcceptRide={handleAcceptRide} />
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
|
|
<div className="lg:col-span-7 bg-card border border-border rounded-xl p-3 shadow-xs space-y-3">
|
|
<div className="flex items-center justify-between px-1">
|
|
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
|
|
<Navigation className="w-3.5 h-3.5 text-sky-600" />
|
|
Tactical Navigation Stage
|
|
</span>
|
|
{activeRide && (
|
|
<span className="text-xs bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300 px-2 py-0.5 rounded font-mono font-medium">
|
|
Live Route Tracking
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<MapView
|
|
center={mapCenter}
|
|
zoom={13}
|
|
markers={mapMarkers}
|
|
polyline={polyline}
|
|
className="h-[520px] w-full rounded-lg border border-border"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|