feat: add GPS location indicators and driver map modal to War Room

Add real-time GPS status dots on driver names throughout the Command Center:
- Green pulsing dot for drivers seen within 10 minutes, gray for inactive
- Clickable dots open a satellite map modal centered on the driver's position
- GPS dots appear in Active NOW cards, Upcoming cards, and In Use vehicles
- Replace Quick Actions panel with Active Drivers panel showing GPS-active
  drivers with speed and last seen time, with compact quick-link icons below
- New DriverLocationModal shows Leaflet satellite map at zoom 16 with
  speed, heading, battery, and last seen info grid

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 22:50:24 +01:00
parent ca2b341f01
commit 714cac5d10
3 changed files with 294 additions and 38 deletions

View File

@@ -0,0 +1,147 @@
import { X, Navigation, Battery, Compass, Clock } from 'lucide-react';
import { MapContainer, TileLayer, Marker } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { formatDistanceToNow } from 'date-fns';
import type { DriverLocation } from '@/types/gps';
interface DriverLocationModalProps {
driverLocation: DriverLocation | null;
isOpen: boolean;
onClose: () => void;
}
// Custom driver marker icon (same as GpsTracking page)
const createDriverIcon = () => {
return L.divIcon({
className: 'custom-driver-marker',
html: `
<div style="
background-color: #22c55e;
width: 32px;
height: 32px;
border-radius: 50%;
border: 3px solid white;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
">
<svg width="16" height="16" viewBox="0 0 24 24" fill="white">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>
</div>
`,
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
});
};
function getCourseDirection(course: number | null): string {
if (course === null || course === undefined) return 'N/A';
const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
const index = Math.round(course / 45) % 8;
return `${directions[index]} (${Math.round(course)}°)`;
}
export function DriverLocationModal({ driverLocation, isOpen, onClose }: DriverLocationModalProps) {
if (!isOpen || !driverLocation) return null;
const hasLocation = driverLocation.location !== null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-card border border-border rounded-lg shadow-lg w-full max-w-lg overflow-hidden">
{/* Header */}
<div className="flex justify-between items-center p-4 border-b border-border">
<div>
<h3 className="text-lg font-semibold text-foreground">{driverLocation.driverName}</h3>
{driverLocation.driverPhone && (
<p className="text-sm text-muted-foreground">{driverLocation.driverPhone}</p>
)}
</div>
<button
onClick={onClose}
className="text-muted-foreground hover:text-foreground transition-colors p-1 rounded hover:bg-accent"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Map */}
{hasLocation ? (
<>
<div className="h-[300px]">
<MapContainer
key={driverLocation.driverId}
center={[driverLocation.location!.latitude, driverLocation.location!.longitude]}
zoom={16}
style={{ height: '100%', width: '100%' }}
zoomControl={true}
>
<TileLayer
attribution='Tiles &copy; Esri'
url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
maxZoom={19}
/>
<Marker
position={[driverLocation.location!.latitude, driverLocation.location!.longitude]}
icon={createDriverIcon()}
/>
</MapContainer>
</div>
{/* Info Grid */}
<div className="p-4 grid grid-cols-2 gap-3">
<div className="flex items-center gap-2">
<Navigation className="h-4 w-4 text-blue-500 flex-shrink-0" />
<div>
<p className="text-xs text-muted-foreground">Speed</p>
<p className="text-sm font-medium text-foreground">
{driverLocation.location!.speed?.toFixed(1) || '0'} mph
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Compass className="h-4 w-4 text-indigo-500 flex-shrink-0" />
<div>
<p className="text-xs text-muted-foreground">Heading</p>
<p className="text-sm font-medium text-foreground">
{getCourseDirection(driverLocation.location!.course)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Battery className="h-4 w-4 text-green-500 flex-shrink-0" />
<div>
<p className="text-xs text-muted-foreground">Battery</p>
<p className="text-sm font-medium text-foreground">
{driverLocation.location!.battery !== null
? `${Math.round(driverLocation.location!.battery)}%`
: 'N/A'}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-gray-500 flex-shrink-0" />
<div>
<p className="text-xs text-muted-foreground">Last Seen</p>
<p className="text-sm font-medium text-foreground">
{formatDistanceToNow(new Date(driverLocation.location!.timestamp), { addSuffix: true })}
</p>
</div>
</div>
</div>
</>
) : (
<div className="p-8 text-center text-muted-foreground">
<Navigation className="h-10 w-10 mx-auto mb-3 opacity-30" />
<p className="text-sm">No location data available for this driver.</p>
<p className="text-xs mt-1">The driver may not have reported their position yet.</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,37 @@
import type { DriverLocation } from '@/types/gps';
interface GpsIndicatorDotProps {
driverLocation: DriverLocation | undefined;
onClick: (loc: DriverLocation) => void;
className?: string;
}
const TEN_MINUTES = 10 * 60 * 1000;
export function GpsIndicatorDot({ driverLocation, onClick, className = '' }: GpsIndicatorDotProps) {
// Don't render if driver has no GPS enrollment
if (!driverLocation) return null;
const isRecentlyActive = driverLocation.lastActive &&
(Date.now() - new Date(driverLocation.lastActive).getTime()) < TEN_MINUTES;
return (
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onClick(driverLocation);
}}
className={`inline-flex items-center justify-center p-0.5 rounded-full transition-all hover:scale-125 ${className}`}
title={isRecentlyActive ? 'GPS active - click to view location' : 'GPS inactive - click for last known location'}
>
<span
className={`block h-2.5 w-2.5 rounded-full ${
isRecentlyActive
? 'bg-green-500 animate-pulse shadow-[0_0_6px_rgba(34,197,94,0.6)]'
: 'bg-gray-400'
}`}
/>
</button>
);
}