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:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user