/** * Resource Location Hook * * Fetches the latest location for a resource's linked staff member. * Used for tracking staff during en route jobs. */ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useRef, useCallback } from 'react'; import apiClient from '../api/client'; export interface ResourceLocation { hasLocation: boolean; latitude?: number; longitude?: number; accuracy?: number; heading?: number; speed?: number; timestamp?: string; isTracking: boolean; activeJob?: { id: number; title: string; status: string; statusDisplay: string; } | null; message?: string; } interface BackendLocationResponse { has_location: boolean; latitude?: number; longitude?: number; accuracy?: number; heading?: number; speed?: number; timestamp?: string; is_tracking?: boolean; active_job?: { id: number; title: string; status: string; status_display: string; } | null; message?: string; } /** * Hook to fetch a resource's latest location */ export const useResourceLocation = (resourceId: string | null, options?: { enabled?: boolean }) => { return useQuery({ queryKey: ['resourceLocation', resourceId], queryFn: async () => { const { data } = await apiClient.get(`/resources/${resourceId}/location/`); return { hasLocation: data.has_location, latitude: data.latitude, longitude: data.longitude, accuracy: data.accuracy, heading: data.heading, speed: data.speed, timestamp: data.timestamp, isTracking: data.is_tracking ?? false, activeJob: data.active_job ? { id: data.active_job.id, title: data.active_job.title, status: data.active_job.status, statusDisplay: data.active_job.status_display, } : null, message: data.message, }; }, enabled: !!resourceId && (options?.enabled !== false), refetchInterval: false, // We'll use WebSocket for live updates instead staleTime: 30000, // Consider data stale after 30 seconds }); }; /** * Hook for live location updates via WebSocket * * Connects to WebSocket when enabled and updates the query cache * when new location data arrives. */ export const useLiveResourceLocation = ( resourceId: string | null, options?: { enabled?: boolean } ) => { const queryClient = useQueryClient(); const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); const connect = useCallback(() => { if (!resourceId || options?.enabled === false) return; // Get WebSocket URL from current host const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.hostname; const port = '8000'; // Backend port const wsUrl = `${protocol}//${host}:${port}/ws/resource-location/${resourceId}/`; try { const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { console.log('[useLiveResourceLocation] WebSocket connected for resource:', resourceId); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'location_update') { // Update the query cache with new location data queryClient.setQueryData(['resourceLocation', resourceId], (old) => ({ ...old, hasLocation: true, latitude: data.latitude, longitude: data.longitude, accuracy: data.accuracy, heading: data.heading, speed: data.speed, timestamp: data.timestamp, isTracking: true, activeJob: data.active_job ? { id: data.active_job.id, title: data.active_job.title, status: data.active_job.status, statusDisplay: data.active_job.status_display, } : old?.activeJob ?? null, })); } else if (data.type === 'tracking_stopped') { // Staff stopped tracking queryClient.setQueryData(['resourceLocation', resourceId], (old) => ({ ...old, hasLocation: old?.hasLocation ?? false, isTracking: false, })); } } catch (err) { console.error('[useLiveResourceLocation] Failed to parse message:', err); } }; ws.onerror = (error) => { console.error('[useLiveResourceLocation] WebSocket error:', error); }; ws.onclose = (event) => { console.log('[useLiveResourceLocation] WebSocket closed:', event.code, event.reason); wsRef.current = null; // Reconnect after 5 seconds if not a clean close if (event.code !== 1000 && options?.enabled !== false) { reconnectTimeoutRef.current = setTimeout(() => { connect(); }, 5000); } }; } catch (err) { console.error('[useLiveResourceLocation] Failed to connect:', err); } }, [resourceId, options?.enabled, queryClient]); useEffect(() => { connect(); return () => { if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); } if (wsRef.current) { wsRef.current.close(1000, 'Component unmounting'); wsRef.current = null; } }; }, [connect]); // Return a function to manually refresh return { refresh: () => queryClient.invalidateQueries({ queryKey: ['resourceLocation', resourceId] }), }; };