feat: add GPS tracking with Traccar integration

- Add GPS module with Traccar client service for device management
- Add driver enrollment flow with QR code generation
- Add real-time location tracking on driver profiles
- Add GPS settings configuration in admin tools
- Add Auth0 OpenID Connect setup script for Traccar
- Add deployment configs for production server
- Update nginx configs for SSL on GPS port 5055
- Add timezone setting support
- Various UI improvements and bug fixes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 18:13:17 +01:00
parent 3814d175ff
commit 5ded039793
91 changed files with 4403 additions and 68 deletions

View File

@@ -0,0 +1,333 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import type {
DriverLocation,
GpsDevice,
DriverStats,
GpsStatus,
GpsSettings,
EnrollmentResponse,
MyGpsStatus,
} from '@/types/gps';
import toast from 'react-hot-toast';
// ============================================
// Admin GPS Hooks
// ============================================
/**
* Get GPS system status
*/
export function useGpsStatus() {
return useQuery<GpsStatus>({
queryKey: ['gps', 'status'],
queryFn: async () => {
const { data } = await api.get('/gps/status');
return data;
},
refetchInterval: 30000, // Refresh every 30 seconds
});
}
/**
* Get GPS settings
*/
export function useGpsSettings() {
return useQuery<GpsSettings>({
queryKey: ['gps', 'settings'],
queryFn: async () => {
const { data } = await api.get('/gps/settings');
return data;
},
});
}
/**
* Update GPS settings
*/
export function useUpdateGpsSettings() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (settings: Partial<GpsSettings>) => {
const { data } = await api.patch('/gps/settings', settings);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['gps', 'settings'] });
queryClient.invalidateQueries({ queryKey: ['gps', 'status'] });
toast.success('GPS settings updated');
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to update GPS settings');
},
});
}
/**
* Get all enrolled GPS devices
*/
export function useGpsDevices() {
return useQuery<GpsDevice[]>({
queryKey: ['gps', 'devices'],
queryFn: async () => {
const { data } = await api.get('/gps/devices');
return data;
},
});
}
/**
* Get all active driver locations (for map)
*/
export function useDriverLocations() {
return useQuery<DriverLocation[]>({
queryKey: ['gps', 'locations'],
queryFn: async () => {
const { data } = await api.get('/gps/locations');
return data;
},
refetchInterval: 30000, // Refresh every 30 seconds
});
}
/**
* Get a specific driver's location
*/
export function useDriverLocation(driverId: string) {
return useQuery<DriverLocation>({
queryKey: ['gps', 'locations', driverId],
queryFn: async () => {
const { data } = await api.get(`/gps/locations/${driverId}`);
return data;
},
enabled: !!driverId,
refetchInterval: 30000,
});
}
/**
* Get driver stats
*/
export function useDriverStats(driverId: string, from?: string, to?: string) {
return useQuery<DriverStats>({
queryKey: ['gps', 'stats', driverId, 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/stats/${driverId}?${params.toString()}`);
return data;
},
enabled: !!driverId,
});
}
/**
* Enroll a driver for GPS tracking
*/
export function useEnrollDriver() {
const queryClient = useQueryClient();
return useMutation<EnrollmentResponse, Error, { driverId: string; sendSignalMessage?: boolean }>({
mutationFn: async ({ driverId, sendSignalMessage = true }) => {
const { data } = await api.post(`/gps/enroll/${driverId}`, { sendSignalMessage });
return data;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['gps', 'devices'] });
queryClient.invalidateQueries({ queryKey: ['gps', 'status'] });
queryClient.invalidateQueries({ queryKey: ['drivers'] });
if (data.signalMessageSent) {
toast.success('Driver enrolled! Setup instructions sent via Signal.');
} else {
toast.success('Driver enrolled! Share the setup instructions with them.');
}
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to enroll driver');
},
});
}
/**
* Unenroll a driver from GPS tracking
*/
export function useUnenrollDriver() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (driverId: string) => {
const { data } = await api.delete(`/gps/devices/${driverId}`);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['gps', 'devices'] });
queryClient.invalidateQueries({ queryKey: ['gps', 'status'] });
queryClient.invalidateQueries({ queryKey: ['gps', 'locations'] });
queryClient.invalidateQueries({ queryKey: ['drivers'] });
toast.success('Driver unenrolled from GPS tracking');
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to unenroll driver');
},
});
}
// ============================================
// Driver Self-Service Hooks
// ============================================
/**
* Get my GPS enrollment status (for drivers)
*/
export function useMyGpsStatus() {
return useQuery<MyGpsStatus>({
queryKey: ['gps', 'me'],
queryFn: async () => {
const { data } = await api.get('/gps/me');
return data;
},
});
}
/**
* Get my GPS stats (for drivers)
*/
export function useMyGpsStats(from?: string, to?: string) {
return useQuery<DriverStats>({
queryKey: ['gps', 'me', 'stats', 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/me/stats?${params.toString()}`);
return data;
},
});
}
/**
* Get my current location (for drivers)
*/
export function useMyLocation() {
return useQuery<DriverLocation>({
queryKey: ['gps', 'me', 'location'],
queryFn: async () => {
const { data } = await api.get('/gps/me/location');
return data;
},
refetchInterval: 30000,
});
}
/**
* Confirm/revoke GPS tracking consent (for drivers)
*/
export function useUpdateGpsConsent() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (consentGiven: boolean) => {
const { data } = await api.post('/gps/me/consent', { consentGiven });
return data;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['gps', 'me'] });
toast.success(data.message);
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to update consent');
},
});
}
// ============================================
// Traccar Admin Hooks
// ============================================
/**
* Check Traccar setup status
*/
export function useTraccarSetupStatus() {
return useQuery<{ needsSetup: boolean; isAvailable: boolean }>({
queryKey: ['gps', 'traccar', 'status'],
queryFn: async () => {
const { data } = await api.get('/gps/traccar/status');
return data;
},
});
}
/**
* Perform initial Traccar setup
*/
export function useTraccarSetup() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
const { data } = await api.post('/gps/traccar/setup');
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['gps', 'traccar', 'status'] });
queryClient.invalidateQueries({ queryKey: ['gps', 'status'] });
toast.success('Traccar setup complete!');
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to setup Traccar');
},
});
}
/**
* Sync VIP admins to Traccar
*/
export function useSyncAdminsToTraccar() {
return useMutation({
mutationFn: async () => {
const { data } = await api.post('/gps/traccar/sync-admins');
return data;
},
onSuccess: (data: { synced: number; failed: number }) => {
toast.success(`Synced ${data.synced} admins to Traccar`);
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to sync admins');
},
});
}
/**
* Get Traccar admin URL for current user
*/
export function useTraccarAdminUrl() {
return useQuery<{ url: string; directAccess: boolean }>({
queryKey: ['gps', 'traccar', 'admin-url'],
queryFn: async () => {
const { data } = await api.get('/gps/traccar/admin-url');
return data;
},
enabled: false, // Only fetch when explicitly called
});
}
/**
* Open Traccar admin (fetches URL and opens in new tab)
*/
export function useOpenTraccarAdmin() {
return useMutation({
mutationFn: async () => {
const { data } = await api.get('/gps/traccar/admin-url');
return data;
},
onSuccess: (data: { url: string; directAccess: boolean }) => {
// Open Traccar in new tab
window.open(data.url, '_blank');
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to open Traccar admin');
},
});
}