fix: improve GPS position sync reliability and add route trails (#21)
Backend: - Increase sync overlap buffer from 5s to 30s to catch late-arriving positions - Add position history endpoint GET /gps/locations/:driverId/history - Add logging for position sync counts (returned vs inserted) Frontend: - Add useDriverLocationHistory hook for fetching position trails - Draw Polyline route trails on GPS map for each tracked driver - Historical positions shown as semi-transparent paths behind live markers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,24 @@ export function useDriverLocation(driverId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver location history (for route trail display)
|
||||
*/
|
||||
export function useDriverLocationHistory(driverId: string | null, from?: string, to?: string) {
|
||||
return useQuery<Array<{ latitude: number; longitude: number; speed: number; timestamp: Date }>>({
|
||||
queryKey: ['gps', 'locations', driverId, 'history', from, to],
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams();
|
||||
if (from) params.append('from', from);
|
||||
if (to) params.append('to', to);
|
||||
const { data } = await api.get(`/gps/locations/${driverId}/history?${params}`);
|
||||
return data;
|
||||
},
|
||||
enabled: !!driverId,
|
||||
refetchInterval: 30000, // Refresh every 30 seconds
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver stats
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
|
||||
import { MapContainer, TileLayer, Marker, Popup, Polyline, useMap } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
useTraccarSetup,
|
||||
useOpenTraccarAdmin,
|
||||
useDeviceQr,
|
||||
useDriverLocationHistory,
|
||||
} from '@/hooks/useGps';
|
||||
import { Loading } from '@/components/Loading';
|
||||
import { ErrorMessage } from '@/components/ErrorMessage';
|
||||
@@ -115,6 +116,8 @@ export function GpsTracking() {
|
||||
const [selectedDriverId, setSelectedDriverId] = useState<string>('');
|
||||
const [enrollmentResult, setEnrollmentResult] = useState<any>(null);
|
||||
const [showQrDriverId, setShowQrDriverId] = useState<string | null>(null);
|
||||
const [selectedDriverForTrail, setSelectedDriverForTrail] = useState<string | null>(null);
|
||||
const [showTrails, setShowTrails] = useState(true);
|
||||
|
||||
// Check admin access
|
||||
if (backendUser?.role !== 'ADMINISTRATOR') {
|
||||
@@ -138,6 +141,16 @@ export function GpsTracking() {
|
||||
const { data: driverStats } = useDriverStats(selectedDriverId);
|
||||
const { data: qrInfo, isLoading: qrLoading } = useDeviceQr(showQrDriverId);
|
||||
|
||||
// Fetch location history for trail display
|
||||
// If a specific driver is selected, show only their trail. Otherwise, fetch for all active drivers.
|
||||
const activeDriverIds = useMemo(() => {
|
||||
return locations?.filter(l => l.location).map(l => l.driverId) || [];
|
||||
}, [locations]);
|
||||
|
||||
// For simplicity, fetch history for the selected driver or the first active driver
|
||||
const driverIdForTrail = selectedDriverForTrail || (showTrails ? activeDriverIds[0] : null);
|
||||
const { data: locationHistory } = useDriverLocationHistory(driverIdForTrail, undefined, undefined);
|
||||
|
||||
// Mutations
|
||||
const updateSettings = useUpdateGpsSettings();
|
||||
const traccarSetup = useTraccarSetup();
|
||||
@@ -368,6 +381,20 @@ export function GpsTracking() {
|
||||
maxZoom={19}
|
||||
/>
|
||||
{locations && <MapFitBounds locations={locations} />}
|
||||
|
||||
{/* Route trail polyline */}
|
||||
{showTrails && locationHistory && locationHistory.length > 1 && (
|
||||
<Polyline
|
||||
positions={locationHistory.map(loc => [loc.latitude, loc.longitude])}
|
||||
pathOptions={{
|
||||
color: '#3b82f6',
|
||||
weight: 3,
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Current position markers */}
|
||||
{locations?.filter(l => l.location).map((driver) => (
|
||||
<Marker
|
||||
key={driver.driverId}
|
||||
@@ -397,6 +424,12 @@ export function GpsTracking() {
|
||||
<span>{formatDistanceToNow(new Date(driver.location!.timestamp), { addSuffix: true })}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedDriverForTrail(driver.driverId)}
|
||||
className="mt-2 w-full px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Show Route Trail
|
||||
</button>
|
||||
</div>
|
||||
</Popup>
|
||||
</Marker>
|
||||
@@ -404,18 +437,40 @@ export function GpsTracking() {
|
||||
</MapContainer>
|
||||
)}
|
||||
|
||||
{/* Map Legend */}
|
||||
{/* Map Legend & Controls */}
|
||||
<div className="absolute bottom-4 left-4 bg-card/90 backdrop-blur-sm border border-border rounded-lg p-3 z-[1000]">
|
||||
<h4 className="text-sm font-medium text-foreground mb-2">Legend</h4>
|
||||
<div className="space-y-1 text-sm">
|
||||
<h4 className="text-sm font-medium text-foreground mb-2">Map Controls</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-green-500"></div>
|
||||
<span className="text-muted-foreground">Active</span>
|
||||
<span className="text-xs text-muted-foreground">Active</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-gray-400"></div>
|
||||
<span className="text-muted-foreground">Inactive</span>
|
||||
<span className="text-xs text-muted-foreground">Inactive</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-12 h-0.5 bg-blue-500 opacity-60"></div>
|
||||
<span className="text-xs text-muted-foreground">Route Trail</span>
|
||||
</div>
|
||||
<hr className="border-border" />
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showTrails}
|
||||
onChange={(e) => setShowTrails(e.target.checked)}
|
||||
className="rounded border-gray-300 text-primary focus:ring-primary"
|
||||
/>
|
||||
<span className="text-xs text-foreground">Show Trails</span>
|
||||
</label>
|
||||
{selectedDriverForTrail && (
|
||||
<button
|
||||
onClick={() => setSelectedDriverForTrail(null)}
|
||||
className="text-xs text-primary hover:text-primary/80 underline w-full text-left"
|
||||
>
|
||||
Clear selected trail
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user