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:
@@ -17,11 +17,16 @@ import {
|
||||
Activity,
|
||||
Calendar,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useEffect, useState, useRef, useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { DriverChatBubble } from '@/components/DriverChatBubble';
|
||||
import { DriverChatModal } from '@/components/DriverChatModal';
|
||||
import { GpsIndicatorDot } from '@/components/GpsIndicatorDot';
|
||||
import { DriverLocationModal } from '@/components/DriverLocationModal';
|
||||
import { useUnreadCounts, useDriverResponseCheck } from '@/hooks/useSignalMessages';
|
||||
import { useDriverLocations } from '@/hooks/useGps';
|
||||
import type { DriverLocation } from '@/types/gps';
|
||||
|
||||
interface Event {
|
||||
id: string;
|
||||
@@ -89,6 +94,8 @@ export function CommandCenter() {
|
||||
const [lastRefresh, setLastRefresh] = useState(new Date());
|
||||
const [chatDriver, setChatDriver] = useState<Driver | null>(null);
|
||||
const [isChatOpen, setIsChatOpen] = useState(false);
|
||||
const [locationDriver, setLocationDriver] = useState<DriverLocation | null>(null);
|
||||
const [isLocationOpen, setIsLocationOpen] = useState(false);
|
||||
|
||||
// Refs for smooth auto-scrolling
|
||||
const activeScrollRef = useRef<HTMLDivElement>(null);
|
||||
@@ -121,6 +128,24 @@ export function CommandCenter() {
|
||||
setChatDriver(null);
|
||||
};
|
||||
|
||||
const openLocationModal = (loc: DriverLocation) => {
|
||||
setLocationDriver(loc);
|
||||
setIsLocationOpen(true);
|
||||
};
|
||||
|
||||
const closeLocationModal = () => {
|
||||
setIsLocationOpen(false);
|
||||
setLocationDriver(null);
|
||||
};
|
||||
|
||||
// GPS driver locations
|
||||
const { data: driverLocations } = useDriverLocations();
|
||||
const driverLocationMap = useMemo(() => {
|
||||
const map = new Map<string, DriverLocation>();
|
||||
driverLocations?.forEach(loc => map.set(loc.driverId, loc));
|
||||
return map;
|
||||
}, [driverLocations]);
|
||||
|
||||
const { data: events, refetch: refetchEvents } = useQuery<Event[]>({
|
||||
queryKey: ['events'],
|
||||
queryFn: async () => {
|
||||
@@ -643,6 +668,11 @@ export function CommandCenter() {
|
||||
<span className="flex items-center gap-1">
|
||||
<Users className="h-3 w-3" />
|
||||
{trip.driver.name}
|
||||
<GpsIndicatorDot
|
||||
driverLocation={driverLocationMap.get(trip.driver.id)}
|
||||
onClick={openLocationModal}
|
||||
className="ml-0.5"
|
||||
/>
|
||||
<DriverChatBubble
|
||||
unreadCount={unreadCounts?.[trip.driver.id] || 0}
|
||||
awaitingResponse={showAwaitingGlow}
|
||||
@@ -735,6 +765,11 @@ export function CommandCenter() {
|
||||
<span className="flex items-center gap-1 text-muted-foreground">
|
||||
<Users className="h-3 w-3" />
|
||||
{trip.driver.name}
|
||||
<GpsIndicatorDot
|
||||
driverLocation={driverLocationMap.get(trip.driver.id)}
|
||||
onClick={openLocationModal}
|
||||
className="ml-0.5"
|
||||
/>
|
||||
<DriverChatBubble
|
||||
unreadCount={unreadCounts?.[trip.driver.id] || 0}
|
||||
onClick={() => openChat(trip.driver!)}
|
||||
@@ -877,8 +912,14 @@ export function CommandCenter() {
|
||||
<p className="text-xs font-medium text-foreground">{vehicle.name}</p>
|
||||
<div className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" />
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground truncate">
|
||||
<p className="text-[10px] text-muted-foreground truncate flex items-center gap-1">
|
||||
{activeEvent?.driver?.name || 'Driver'}
|
||||
{activeEvent?.driver && (
|
||||
<GpsIndicatorDot
|
||||
driverLocation={driverLocationMap.get(activeEvent.driver.id)}
|
||||
onClick={openLocationModal}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -888,42 +929,66 @@ export function CommandCenter() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div className="bg-card border border-border rounded-lg shadow-soft overflow-hidden">
|
||||
<div className="bg-gray-600 dark:bg-gray-700 px-3 py-2">
|
||||
<h2 className="font-semibold text-white text-sm">Quick Actions</h2>
|
||||
</div>
|
||||
<div className="p-2 grid grid-cols-2 gap-2">
|
||||
<Link
|
||||
to="/events"
|
||||
className="flex items-center justify-center gap-1 px-2 py-2 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded text-xs font-medium hover:bg-blue-200 dark:hover:bg-blue-900/50"
|
||||
>
|
||||
<Calendar className="h-3 w-3" />
|
||||
Events
|
||||
</Link>
|
||||
<Link
|
||||
to="/fleet?tab=drivers"
|
||||
className="flex items-center justify-center gap-1 px-2 py-2 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded text-xs font-medium hover:bg-purple-200 dark:hover:bg-purple-900/50"
|
||||
>
|
||||
<Users className="h-3 w-3" />
|
||||
Drivers
|
||||
</Link>
|
||||
<Link
|
||||
to="/vips"
|
||||
className="flex items-center justify-center gap-1 px-2 py-2 bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 rounded text-xs font-medium hover:bg-emerald-200 dark:hover:bg-emerald-900/50"
|
||||
>
|
||||
<Users className="h-3 w-3" />
|
||||
VIPs
|
||||
</Link>
|
||||
<Link
|
||||
to="/fleet?tab=vehicles"
|
||||
className="flex items-center justify-center gap-1 px-2 py-2 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded text-xs font-medium hover:bg-orange-200 dark:hover:bg-orange-900/50"
|
||||
>
|
||||
<Car className="h-3 w-3" />
|
||||
Vehicles
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* Active Drivers (GPS) */}
|
||||
{(() => {
|
||||
const TEN_MINUTES = 10 * 60 * 1000;
|
||||
const activeDrivers = (driverLocations || []).filter(loc =>
|
||||
loc.lastActive && (Date.now() - new Date(loc.lastActive).getTime()) < TEN_MINUTES && loc.location
|
||||
);
|
||||
return (
|
||||
<div className="bg-card border border-border rounded-lg shadow-soft overflow-hidden">
|
||||
<div className="bg-teal-600 px-3 py-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Radio className="h-4 w-4 text-white" />
|
||||
<h2 className="font-semibold text-white text-sm">Active Drivers</h2>
|
||||
</div>
|
||||
<span className="text-white/80 text-xs">{activeDrivers.length}</span>
|
||||
</div>
|
||||
<div className="max-h-[108px] overflow-y-auto scrollbar-hide">
|
||||
{activeDrivers.length === 0 ? (
|
||||
<div className="p-4 text-center text-muted-foreground">
|
||||
<p className="text-xs">No active GPS drivers</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
{activeDrivers.map((loc) => (
|
||||
<button
|
||||
key={loc.driverId}
|
||||
onClick={() => openLocationModal(loc)}
|
||||
className="w-full px-3 py-2 text-left hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-medium text-foreground truncate">{loc.driverName}</p>
|
||||
<span className="text-[10px] text-green-600 dark:text-green-400 font-medium flex-shrink-0">
|
||||
{loc.location?.speed?.toFixed(0) || 0} mph
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{loc.lastActive && formatDistanceToNow(new Date(loc.lastActive), { addSuffix: true })}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Quick Action Links - compact icon row */}
|
||||
<div className="border-t border-border px-2 py-1.5 flex justify-around">
|
||||
<Link to="/events" className="p-1.5 rounded hover:bg-accent transition-colors" title="Events">
|
||||
<Calendar className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400" />
|
||||
</Link>
|
||||
<Link to="/fleet?tab=drivers" className="p-1.5 rounded hover:bg-accent transition-colors" title="Drivers">
|
||||
<Users className="h-3.5 w-3.5 text-purple-600 dark:text-purple-400" />
|
||||
</Link>
|
||||
<Link to="/vips" className="p-1.5 rounded hover:bg-accent transition-colors" title="VIPs">
|
||||
<Users className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400" />
|
||||
</Link>
|
||||
<Link to="/fleet?tab=vehicles" className="p-1.5 rounded hover:bg-accent transition-colors" title="Vehicles">
|
||||
<Car className="h-3.5 w-3.5 text-orange-600 dark:text-orange-400" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Driver Chat Modal */}
|
||||
@@ -932,6 +997,13 @@ export function CommandCenter() {
|
||||
isOpen={isChatOpen}
|
||||
onClose={closeChat}
|
||||
/>
|
||||
|
||||
{/* Driver Location Modal */}
|
||||
<DriverLocationModal
|
||||
driverLocation={locationDriver}
|
||||
isOpen={isLocationOpen}
|
||||
onClose={closeLocationModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user