app-s01qyw/frontend/src/pages/DriverWorkstation.tsx
2026-09-29 20:17:53 +05:30

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>
);
}