diff --git a/frontend/src/pages/GpsTracking.tsx b/frontend/src/pages/GpsTracking.tsx
new file mode 100644
index 0000000..89699b2
--- /dev/null
+++ b/frontend/src/pages/GpsTracking.tsx
@@ -0,0 +1,853 @@
+import { useState, useEffect, useMemo } from 'react';
+import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
+import L from 'leaflet';
+import 'leaflet/dist/leaflet.css';
+import { QRCodeSVG } from 'qrcode.react';
+import {
+ MapPin,
+ Settings,
+ Users,
+ RefreshCw,
+ Navigation,
+ Battery,
+ Clock,
+ ExternalLink,
+ X,
+ Search,
+ AlertCircle,
+ CheckCircle,
+ XCircle,
+ UserPlus,
+ UserMinus,
+ Gauge,
+ Activity,
+ Smartphone,
+ Route,
+ Car,
+ Copy,
+ QrCode,
+} from 'lucide-react';
+import {
+ useGpsStatus,
+ useGpsSettings,
+ useUpdateGpsSettings,
+ useDriverLocations,
+ useGpsDevices,
+ useEnrollDriver,
+ useUnenrollDriver,
+ useDriverStats,
+ useTraccarSetupStatus,
+ useTraccarSetup,
+ useOpenTraccarAdmin,
+} from '@/hooks/useGps';
+import { Loading } from '@/components/Loading';
+import { ErrorMessage } from '@/components/ErrorMessage';
+import { useQuery } from '@tanstack/react-query';
+import { api } from '@/lib/api';
+import { useAuth } from '@/contexts/AuthContext';
+import type { Driver } from '@/types';
+import type { DriverLocation } from '@/types/gps';
+import toast from 'react-hot-toast';
+import { formatDistanceToNow } from 'date-fns';
+
+// Fix Leaflet default marker icons
+delete (L.Icon.Default.prototype as any)._getIconUrl;
+L.Icon.Default.mergeOptions({
+ iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png',
+ iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png',
+ shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png',
+});
+
+// Custom driver marker icon
+const createDriverIcon = (isActive: boolean) => {
+ return L.divIcon({
+ className: 'custom-driver-marker',
+ html: `
+
+ `,
+ iconSize: [32, 32],
+ iconAnchor: [16, 32],
+ popupAnchor: [0, -32],
+ });
+};
+
+// Map auto-fit component
+function MapFitBounds({ locations }: { locations: DriverLocation[] }) {
+ const map = useMap();
+
+ useEffect(() => {
+ const validLocations = locations.filter(d => d.location);
+ if (validLocations.length > 0) {
+ const bounds = L.latLngBounds(
+ validLocations.map(loc => [loc.location!.latitude, loc.location!.longitude])
+ );
+ map.fitBounds(bounds, { padding: [50, 50], maxZoom: 14 });
+ }
+ }, [locations, map]);
+
+ return null;
+}
+
+export function GpsTracking() {
+ const { backendUser } = useAuth();
+ const [activeTab, setActiveTab] = useState<'map' | 'devices' | 'settings' | 'stats'>('map');
+ const [showEnrollModal, setShowEnrollModal] = useState(false);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [selectedDriverId, setSelectedDriverId] = useState('');
+ const [enrollmentResult, setEnrollmentResult] = useState(null);
+
+ // Check admin access
+ if (backendUser?.role !== 'ADMINISTRATOR') {
+ return (
+
+
+
Access Denied
+
+ Only Administrators can access GPS tracking.
+
+
+ );
+ }
+
+ // Data hooks
+ const { data: status, isLoading: statusLoading } = useGpsStatus();
+ const { data: settings, isLoading: settingsLoading } = useGpsSettings();
+ const { data: locations, isLoading: locationsLoading, refetch: refetchLocations } = useDriverLocations();
+ const { data: devices, isLoading: devicesLoading } = useGpsDevices();
+ const { data: traccarStatus } = useTraccarSetupStatus();
+ const { data: driverStats } = useDriverStats(selectedDriverId);
+
+ // Mutations
+ const updateSettings = useUpdateGpsSettings();
+ const traccarSetup = useTraccarSetup();
+ const openTraccar = useOpenTraccarAdmin();
+ const enrollDriver = useEnrollDriver();
+ const unenrollDriver = useUnenrollDriver();
+
+ // Get all drivers for enrollment
+ const { data: allDrivers } = useQuery({
+ queryKey: ['drivers'],
+ queryFn: async () => {
+ const { data } = await api.get('/drivers');
+ return data;
+ },
+ });
+
+ // Filter unenrolled drivers
+ const enrolledDriverIds = new Set(devices?.map((d: any) => d.driverId) || []);
+ const unenrolledDrivers = allDrivers?.filter(d => !enrolledDriverIds.has(d.id)) || [];
+ const filteredUnenrolled = unenrolledDrivers.filter(d =>
+ d.name.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+
+ // Calculate center for map
+ const mapCenter: [number, number] = useMemo(() => {
+ const validLocations = locations?.filter(l => l.location) || [];
+ if (validLocations.length > 0) {
+ return [
+ validLocations.reduce((sum, loc) => sum + loc.location!.latitude, 0) / validLocations.length,
+ validLocations.reduce((sum, loc) => sum + loc.location!.longitude, 0) / validLocations.length,
+ ];
+ }
+ return [36.0, -79.0]; // Default to North Carolina area
+ }, [locations]);
+
+ const copyToClipboard = (text: string) => {
+ navigator.clipboard.writeText(text);
+ toast.success('Copied to clipboard');
+ };
+
+ const handleEnroll = async (driverId: string) => {
+ try {
+ const result = await enrollDriver.mutateAsync({ driverId, sendSignalMessage: true });
+ setEnrollmentResult(result);
+ } catch (error) {
+ // Error handled by hook
+ }
+ };
+
+ if (statusLoading) {
+ return ;
+ }
+
+ // Check if Traccar needs setup
+ if (traccarStatus?.needsSetup && traccarStatus?.isAvailable) {
+ return (
+
+
+
+
+
+
GPS Tracking Setup
+
+ The GPS tracking system needs to be configured before you can start tracking drivers.
+ This will set up the Traccar server connection.
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
GPS Tracking
+
Monitor driver locations in real-time
+
+
+
+ {status?.traccarAvailable && (
+
+ )}
+
+
+
+ {/* Traccar Status */}
+ {!status?.traccarAvailable && (
+
+
+
+
+
Traccar Server Offline
+
GPS tracking is unavailable until the server is online.
+
+
+
+ )}
+
+ {/* Status Cards */}
+
+
+
+
+
Total Enrolled
+
{status?.enrolledDrivers || 0}
+
+
+
+
+
+
+
+
+
+
+
Active Now
+
{status?.activeDrivers || 0}
+
+
+
+
+
+
+
+
+
Update Interval
+
{status?.settings?.updateIntervalSeconds || 60}s
+
+
+
+
+
+
+
+
+
+
+
Shift Hours
+
{status?.settings?.shiftStartTime || '4:00'} - {status?.settings?.shiftEndTime || '1:00'}
+
+
+
+
+
+
+
+
+ {/* Tabs */}
+
+
+ {[
+ { id: 'map', label: 'Live Map', icon: MapPin },
+ { id: 'devices', label: 'Devices', icon: Smartphone },
+ { id: 'stats', label: 'Stats', icon: Gauge },
+ { id: 'settings', label: 'Settings', icon: Settings },
+ ].map(tab => (
+
+ ))}
+
+
+
+ {/* Tab Content */}
+
+ {/* Map Tab */}
+ {activeTab === 'map' && (
+
+
+ {locationsLoading ? (
+
+
+
+ ) : (
+
+
+ {locations && }
+ {locations?.filter(l => l.location).map((driver) => (
+
+
+
+
{driver.driverName}
+ {driver.driverPhone && (
+
{driver.driverPhone}
+ )}
+
+
+
+
+ {driver.location?.speed?.toFixed(1) || 0} mph
+
+ {driver.location?.battery && (
+
+
+ {Math.round(driver.location.battery)}%
+
+ )}
+
+
+ {formatDistanceToNow(new Date(driver.location!.timestamp), { addSuffix: true })}
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* Map Legend */}
+
+
+
+ {/* Active Drivers List */}
+
+
Active Drivers ({locations?.filter(l => l.location).length || 0})
+ {!locations || locations.filter(l => l.location).length === 0 ? (
+
No active drivers reporting location
+ ) : (
+
+ {locations.filter(l => l.location).map((driver) => (
+
+
+
{driver.driverName}
+
+ {driver.location?.speed?.toFixed(0) || 0} mph
+
+
+
+ Online
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {/* Devices Tab */}
+ {activeTab === 'devices' && (
+
+
+
Enrolled Devices
+
+
+
+ {devicesLoading ? (
+
+ ) : devices && devices.length > 0 ? (
+
+
+
+
+ | Driver |
+ Device ID |
+ Status |
+ Consent |
+ Last Active |
+ Actions |
+
+
+
+ {devices.map((device: any) => (
+
+ |
+ {device.driver?.name || 'Unknown'}
+ {device.driver?.phone && {device.driver.phone} }
+ |
+ {device.deviceIdentifier} |
+
+
+ {device.isActive ? 'Active' : 'Inactive'}
+
+ |
+
+ {device.consentGiven ? (
+
+ ) : (
+
+ )}
+ |
+
+ {device.lastActive ? formatDistanceToNow(new Date(device.lastActive), { addSuffix: true }) : 'Never'}
+ |
+
+
+ |
+
+ ))}
+
+
+
+ ) : (
+
+
+
No devices enrolled
+
Start by enrolling drivers for GPS tracking
+
+ )}
+
+ )}
+
+ {/* Stats Tab */}
+ {activeTab === 'stats' && (
+
+
Driver Statistics
+
+
+
+
+
+
+ {selectedDriverId && driverStats ? (
+
+
+
+
+
+
{driverStats.stats?.totalMiles || 0}
+
Miles Driven
+
+
+
+
+
+
+
+
{driverStats.stats?.topSpeedMph || 0} mph
+
Top Speed
+
+
+
+
+
+
+
+
{driverStats.stats?.averageSpeedMph || 0} mph
+
Avg Speed
+
+
+
+
+
+
+
+
{driverStats.stats?.totalTrips || 0}
+
Total Trips
+
+
+
+
+ ) : selectedDriverId ? (
+
+ ) : (
+
Select a driver to view their statistics
+ )}
+
+ )}
+
+ {/* Settings Tab */}
+ {activeTab === 'settings' && (
+
+
GPS Settings
+
+ {settingsLoading ? (
+
+ ) : settings ? (
+
+
+
+
{
+ const value = parseInt(e.target.value);
+ if (value >= 30 && value <= 300 && value !== settings.updateIntervalSeconds) {
+ updateSettings.mutate({ updateIntervalSeconds: value });
+ }
+ }}
+ className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground focus:ring-primary focus:border-primary"
+ />
+
How often drivers report their location (30-300 seconds)
+
+
+
+
+
{
+ const value = parseInt(e.target.value);
+ if (value >= 7 && value <= 90 && value !== settings.retentionDays) {
+ updateSettings.mutate({ retentionDays: value });
+ }
+ }}
+ className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground focus:ring-primary focus:border-primary"
+ />
+
How long to keep location history (7-90 days)
+
+
+
+
+
+
+
+ {
+ const [hours, minutes] = e.target.value.split(':').map(Number);
+ if (!isNaN(hours) && !isNaN(minutes)) {
+ updateSettings.mutate({
+ shiftStartHour: hours,
+ shiftStartMinute: minutes,
+ });
+ }
+ }}
+ className="mt-1 px-3 py-2 border border-input rounded-md bg-background text-foreground focus:ring-primary focus:border-primary"
+ />
+
+
to
+
+
+ {
+ const [hours, minutes] = e.target.value.split(':').map(Number);
+ if (!isNaN(hours) && !isNaN(minutes)) {
+ updateSettings.mutate({
+ shiftEndHour: hours,
+ shiftEndMinute: minutes,
+ });
+ }
+ }}
+ className="mt-1 px-3 py-2 border border-input rounded-md bg-background text-foreground focus:ring-primary focus:border-primary"
+ />
+
+
+
+ Drivers are only tracked during these hours.
+
+
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ {/* Enroll Driver Modal */}
+ {showEnrollModal && (
+
+
+
+
+ {enrollmentResult ? 'Driver Enrolled!' : 'Enroll Driver for GPS'}
+
+
+
+
+ {!enrollmentResult ? (
+ <>
+
+
+
+ setSearchTerm(e.target.value)}
+ className="w-full pl-10 pr-4 py-2 border border-input rounded-md bg-background text-foreground focus:ring-primary focus:border-primary"
+ />
+
+
+ {filteredUnenrolled.length === 0 ? (
+
+
+
+ {searchTerm ? 'No matching drivers found' : 'All drivers are already enrolled'}
+
+
+ ) : (
+
+ {filteredUnenrolled.map((driver) => (
+
+
+
{driver.name}
+
{driver.phone || 'No phone'}
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ Enrolling a driver will send them setup instructions via Signal message.
+
+
+ >
+ ) : (
+
+
+
+ Driver Enrolled Successfully!
+
+
+ {enrollmentResult.signalMessageSent && (
+
+
+ Setup instructions sent via Signal
+
+ )}
+
+ {/* QR Code for Traccar Client app */}
+
+
+
+ Scan with Traccar Client
+
+
+ {/* Traccar Client expects: serverUrl?id=xxx&interval=xxx&accuracy=high */}
+
+
+
+ Open Traccar Client → Settings → Scan QR Code
+
+
+
+
+
+
+
+
+ {enrollmentResult.deviceIdentifier}
+
+
+
+
+
+
+
+
+ {enrollmentResult.serverUrl}
+
+
+
+
+
+
+
+
Driver Instructions:
+
+ - Download "Traccar Client" from App Store or Play Store
+ - Open app and enter the Device ID and Server URL
+ - Set frequency to {settings?.updateIntervalSeconds || 60} seconds
+ - Tap "Service Status" to start tracking
+
+
+
+
+
+ )}
+
+
+ )}
+
+ );
+}