refactor: use Traccar trip API instead of custom detection (#23)

Replace custom trip detection (overlapping/micro-trip prone) with
Traccar's built-in trip report API. Remove merge/backfill UI and
endpoints. Add geocoded address display to trip cards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 19:38:08 +01:00
parent b80ffd3ca1
commit 53eb82c4d2
6 changed files with 193 additions and 681 deletions

View File

@@ -12,7 +12,6 @@ import type {
LocationHistoryResponse,
GpsTrip,
GpsTripDetail,
TripStatus,
} from '@/types/gps';
import toast from 'react-hot-toast';
import { queryKeys } from '@/lib/query-keys';
@@ -170,16 +169,15 @@ export function useDriverStats(driverId: string, from?: string, to?: string) {
// ============================================
/**
* Get trips for a driver
* Get trips for a driver (powered by Traccar)
*/
export function useDriverTrips(driverId: string | null, from?: string, to?: string, status?: TripStatus) {
export function useDriverTrips(driverId: string | null, from?: string, to?: string) {
return useQuery<GpsTrip[]>({
queryKey: ['gps', 'trips', driverId, from, to, status],
queryKey: ['gps', 'trips', driverId, from, to],
queryFn: async () => {
const params = new URLSearchParams();
if (from) params.append('from', from);
if (to) params.append('to', to);
if (status) params.append('status', status);
const { data } = await api.get(`/gps/trips/${driverId}?${params}`);
return data;
},
@@ -216,48 +214,6 @@ export function useActiveTrip(driverId: string | null) {
});
}
/**
* Merge two trips
*/
export function useMergeTrips() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ tripIdA, tripIdB }: { tripIdA: string; tripIdB: string }) => {
const { data } = await api.post('/gps/trips/merge', { tripIdA, tripIdB });
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['gps', 'trips'] });
toast.success('Trips merged successfully');
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to merge trips');
},
});
}
/**
* Backfill trips from historical data
*/
export function useBackfillTrips() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ driverId, from, to }: { driverId: string; from?: string; to?: string }) => {
const { data } = await api.post(`/gps/trips/backfill/${driverId}`, { from, to });
return data;
},
onSuccess: (data: { tripsCreated: number }) => {
queryClient.invalidateQueries({ queryKey: ['gps', 'trips'] });
toast.success(`Backfill complete: ${data.tripsCreated} trips created`);
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to backfill trips');
},
});
}
/**
* Enroll a driver for GPS tracking
*/

View File

@@ -28,7 +28,6 @@ import {
QrCode,
Play,
Calendar,
Merge,
ToggleLeft,
ToggleRight,
Timer,
@@ -51,8 +50,6 @@ import {
useDriverLocationHistory,
useDriverTrips,
useActiveTrip,
useMergeTrips,
useBackfillTrips,
} from '@/hooks/useGps';
import { Loading } from '@/components/Loading';
import { ErrorMessage } from '@/components/ErrorMessage';
@@ -223,8 +220,6 @@ interface StatsTabProps {
setDateTo: (date: string) => void;
toggledTripIds: Set<string>;
setToggledTripIds: (ids: Set<string>) => void;
selectedTripIds: Set<string>;
setSelectedTripIds: (ids: Set<string>) => void;
expandedDates: Set<string>;
setExpandedDates: (dates: Set<string>) => void;
}
@@ -239,14 +234,9 @@ function StatsTab({
setDateTo,
toggledTripIds,
setToggledTripIds,
selectedTripIds,
setSelectedTripIds,
expandedDates,
setExpandedDates,
}: StatsTabProps) {
const backfillTrips = useBackfillTrips();
const mergeTrips = useMergeTrips();
// Fetch trips for selected driver
const { data: trips, isLoading: tripsLoading } = useDriverTrips(
selectedDriverId || null,
@@ -316,16 +306,6 @@ function StatsTab({
setToggledTripIds(newSet);
};
const handleSelectTrip = (tripId: string) => {
const newSet = new Set(selectedTripIds);
if (newSet.has(tripId)) {
newSet.delete(tripId);
} else {
newSet.add(tripId);
}
setSelectedTripIds(newSet);
};
const handleToggleDate = (dateKey: string) => {
const newSet = new Set(expandedDates);
if (newSet.has(dateKey)) {
@@ -336,23 +316,6 @@ function StatsTab({
setExpandedDates(newSet);
};
const handleMergeSelected = () => {
const selected = Array.from(selectedTripIds);
if (selected.length !== 2) {
toast.error('Please select exactly 2 adjacent trips to merge');
return;
}
mergeTrips.mutate(
{ tripIdA: selected[0], tripIdB: selected[1] },
{
onSuccess: () => {
setSelectedTripIds(new Set());
},
}
);
};
// Playback state
const [playingTripId, setPlayingTripId] = useState<string | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
@@ -486,19 +449,6 @@ function StatsTab({
setPlaybackProgress(0);
};
const handleBackfill = () => {
if (!selectedDriverId) {
toast.error('Please select a driver first');
return;
}
backfillTrips.mutate({
driverId: selectedDriverId,
from: dateFrom,
to: dateTo,
});
};
const getStatusColor = (status: TripStatus) => {
switch (status) {
case 'ACTIVE':
@@ -540,7 +490,6 @@ function StatsTab({
onChange={(e) => {
setSelectedDriverId(e.target.value);
setToggledTripIds(new Set());
setSelectedTripIds(new Set());
}}
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground focus:ring-primary focus:border-primary"
>
@@ -573,14 +522,6 @@ function StatsTab({
className="w-full px-2 py-1 text-sm border border-input rounded-md bg-background text-foreground"
/>
</div>
<button
onClick={handleBackfill}
disabled={!selectedDriverId || backfillTrips.isPending}
className="px-3 py-1 text-sm bg-primary text-white rounded-md hover:bg-primary/90 disabled:opacity-50 transition-colors"
title="Backfill trips from historical data"
>
<RefreshCw className={`h-4 w-4 ${backfillTrips.isPending ? 'animate-spin' : ''}`} />
</button>
</div>
{/* Summary Stats Bar */}
@@ -603,18 +544,6 @@ function StatsTab({
</div>
)}
{/* Merge Button */}
{selectedTripIds.size > 0 && (
<button
onClick={handleMergeSelected}
disabled={selectedTripIds.size !== 2 || mergeTrips.isPending}
className="mb-4 w-full px-3 py-2 text-sm bg-orange-500 text-white rounded-md hover:bg-orange-600 disabled:opacity-50 transition-colors flex items-center justify-center gap-2"
>
<Merge className="h-4 w-4" />
Merge Selected ({selectedTripIds.size})
</button>
)}
{/* Trip List */}
<div className="flex-1 overflow-y-auto space-y-3">
{!selectedDriverId ? (
@@ -649,15 +578,7 @@ function StatsTab({
: 'border-border bg-card hover:bg-accent'
}`}
>
<div className="flex items-start gap-2">
{/* Checkbox for selection */}
<input
type="checkbox"
checked={selectedTripIds.has(trip.id)}
onChange={() => handleSelectTrip(trip.id)}
className="mt-1 rounded border-gray-300 text-primary focus:ring-primary"
/>
<div>
<div className="flex-1 min-w-0">
{/* Time Range */}
<div className="flex items-center justify-between mb-1">
@@ -670,6 +591,24 @@ function StatsTab({
</span>
</div>
{/* Addresses */}
{(trip.startAddress || trip.endAddress) && (
<div className="text-xs text-muted-foreground mb-2 space-y-0.5">
{trip.startAddress && (
<div className="truncate" title={trip.startAddress}>
<MapPin className="h-3 w-3 inline mr-1 text-green-500" />
{trip.startAddress}
</div>
)}
{trip.endAddress && (
<div className="truncate" title={trip.endAddress}>
<MapPin className="h-3 w-3 inline mr-1 text-red-500" />
{trip.endAddress}
</div>
)}
</div>
)}
{/* Stats */}
<div className="grid grid-cols-3 gap-2 text-xs text-muted-foreground mb-2">
<div>
@@ -958,7 +897,6 @@ export function GpsTracking() {
const [dateFrom, setDateFrom] = useState(format(subDays(new Date(), 7), 'yyyy-MM-dd'));
const [dateTo, setDateTo] = useState(format(new Date(), 'yyyy-MM-dd'));
const [toggledTripIds, setToggledTripIds] = useState<Set<string>>(new Set());
const [selectedTripIds, setSelectedTripIds] = useState<Set<string>>(new Set());
const [expandedDates, setExpandedDates] = useState<Set<string>>(new Set());
// Check admin access
@@ -1550,8 +1488,6 @@ export function GpsTracking() {
setDateTo={setDateTo}
toggledTripIds={toggledTripIds}
setToggledTripIds={setToggledTripIds}
selectedTripIds={selectedTripIds}
setSelectedTripIds={setSelectedTripIds}
expandedDates={expandedDates}
setExpandedDates={setExpandedDates}
/>

View File

@@ -124,6 +124,8 @@ export interface GpsTrip {
topSpeedMph: number | null;
averageSpeedMph: number | null;
pointCount: number;
startAddress?: string | null;
endAddress?: string | null;
}
export interface GpsTripDetail extends GpsTrip {