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:
2026-02-08 16:42:41 +01:00
parent 3bc9cd0bca
commit 4dbb899409
4 changed files with 160 additions and 11 deletions

View File

@@ -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>