FlotillasGPS - Sistema completo de monitoreo de flotillas GPS

Sistema completo para monitoreo y gestion de flotas de vehiculos con:
- Backend FastAPI con PostgreSQL/TimescaleDB
- Frontend React con TypeScript y TailwindCSS
- App movil React Native con Expo
- Soporte para dispositivos GPS, Meshtastic y celulares
- Video streaming en vivo con MediaMTX
- Geocercas, alertas, viajes y reportes
- Autenticacion JWT y WebSockets en tiempo real

Documentacion completa y guias de usuario incluidas.
This commit is contained in:
FlotillasGPS Developer
2026-01-21 08:18:00 +00:00
commit 51d78bacf4
248 changed files with 50171 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
/**
* Exportación centralizada de hooks
*/
export { useAuth, default as useAuthHook } from './useAuth';
export { useLocation, default as useLocationHook } from './useLocation';
export { useViaje, default as useViajeHook } from './useViaje';

View File

@@ -0,0 +1,82 @@
/**
* Hook personalizado para autenticación
* Proporciona acceso al estado de auth y acciones comunes
*/
import { useCallback, useEffect } from 'react';
import { useAuthStore } from '../store/authStore';
export const useAuth = () => {
const {
conductor,
token,
dispositivo,
isAuthenticated,
isLoading,
initialize,
requestCode,
verifyCode,
logout,
refreshToken,
updateConductor,
} = useAuthStore();
// Inicializar al montar
useEffect(() => {
initialize();
}, [initialize]);
// Login con teléfono
const login = useCallback(
async (telefono: string) => {
const result = await requestCode(telefono);
return result;
},
[requestCode]
);
// Verificar código
const verify = useCallback(
async (telefono: string, codigo: string) => {
const result = await verifyCode(telefono, codigo);
return result;
},
[verifyCode]
);
// Cerrar sesión
const signOut = useCallback(async () => {
await logout();
}, [logout]);
// Refrescar token
const refresh = useCallback(async () => {
return await refreshToken();
}, [refreshToken]);
// Actualizar perfil
const updateProfile = useCallback(
(updates: Parameters<typeof updateConductor>[0]) => {
updateConductor(updates);
},
[updateConductor]
);
return {
// Estado
conductor,
token,
dispositivo,
isAuthenticated,
isLoading,
// Acciones
login,
verify,
signOut,
refresh,
updateProfile,
};
};
export default useAuth;

View File

@@ -0,0 +1,243 @@
/**
* Hook personalizado para tracking de ubicación
* Proporciona acceso a ubicación actual y control del tracking
*/
import { useState, useCallback, useEffect, useRef } from 'react';
import {
requestLocationPermissions,
checkLocationPermissions,
getCurrentLocation,
startLocationTracking,
stopLocationTracking,
isTrackingActive,
syncOfflineLocations,
getPendingLocationsCount,
calculateDistance,
} from '../services/location';
import { useUbicacionStore } from '../store/ubicacionStore';
import type { Ubicacion, PermisoStatus } from '../types';
interface UseLocationOptions {
autoStart?: boolean;
viajeId?: string;
updateInterval?: number; // ms
}
interface UseLocationReturn {
// Estado
ubicacion: Ubicacion | null;
isTracking: boolean;
isLoading: boolean;
permissionStatus: PermisoStatus['ubicacion'];
backgroundPermission: PermisoStatus['ubicacionBackground'];
pendingCount: number;
isOnline: boolean;
error: string | null;
// Acciones
requestPermissions: () => Promise<boolean>;
startTracking: (viajeId?: string) => Promise<boolean>;
stopTracking: () => Promise<void>;
refreshLocation: () => Promise<Ubicacion | null>;
syncPending: () => Promise<number>;
getDistanceTo: (lat: number, lon: number) => number | null;
}
export const useLocation = (options: UseLocationOptions = {}): UseLocationReturn => {
const { autoStart = false, viajeId, updateInterval = 5000 } = options;
const [isLoading, setIsLoading] = useState(false);
const [isTracking, setIsTracking] = useState(false);
const [error, setError] = useState<string | null>(null);
const [permissionStatus, setPermissionStatus] =
useState<PermisoStatus['ubicacion']>('undetermined');
const [backgroundPermission, setBackgroundPermission] =
useState<PermisoStatus['ubicacionBackground']>('undetermined');
const {
ubicacionActual,
isOnline,
pendingCount,
setUbicacionActual,
syncPendingLocations,
refreshPendingCount,
} = useUbicacionStore();
const intervalRef = useRef<NodeJS.Timeout | null>(null);
// Verificar permisos al montar
useEffect(() => {
const checkPermissions = async () => {
const permissions = await checkLocationPermissions();
setPermissionStatus(permissions.foreground ? 'granted' : 'denied');
setBackgroundPermission(permissions.background ? 'granted' : 'denied');
};
checkPermissions();
refreshPendingCount();
}, [refreshPendingCount]);
// Auto-start tracking si está configurado
useEffect(() => {
if (autoStart && permissionStatus === 'granted') {
startTracking(viajeId);
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [autoStart, permissionStatus, viajeId]);
// Solicitar permisos
const requestPermissions = useCallback(async (): Promise<boolean> => {
setIsLoading(true);
setError(null);
try {
const granted = await requestLocationPermissions();
if (granted) {
const permissions = await checkLocationPermissions();
setPermissionStatus(permissions.foreground ? 'granted' : 'denied');
setBackgroundPermission(permissions.background ? 'granted' : 'denied');
return true;
}
setPermissionStatus('denied');
setError('Permisos de ubicación denegados');
return false;
} catch (err) {
setError('Error solicitando permisos');
return false;
} finally {
setIsLoading(false);
}
}, []);
// Iniciar tracking
const startTracking = useCallback(
async (tripId?: string): Promise<boolean> => {
if (isTracking) return true;
setIsLoading(true);
setError(null);
try {
const started = await startLocationTracking(tripId || viajeId);
if (started) {
setIsTracking(true);
// Configurar intervalo de actualización de UI
intervalRef.current = setInterval(async () => {
const location = await getCurrentLocation();
if (location) {
setUbicacionActual(location);
}
}, updateInterval);
// Obtener ubicación inicial
const initialLocation = await getCurrentLocation();
if (initialLocation) {
setUbicacionActual(initialLocation);
}
return true;
}
setError('No se pudo iniciar el tracking');
return false;
} catch (err) {
setError('Error iniciando tracking');
return false;
} finally {
setIsLoading(false);
}
},
[isTracking, viajeId, updateInterval, setUbicacionActual]
);
// Detener tracking
const stopTracking = useCallback(async (): Promise<void> => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
await stopLocationTracking();
setIsTracking(false);
}, []);
// Refrescar ubicación manualmente
const refreshLocation = useCallback(async (): Promise<Ubicacion | null> => {
setIsLoading(true);
try {
const location = await getCurrentLocation();
if (location) {
setUbicacionActual(location);
return location;
}
return null;
} catch (err) {
setError('Error obteniendo ubicación');
return null;
} finally {
setIsLoading(false);
}
}, [setUbicacionActual]);
// Sincronizar ubicaciones pendientes
const syncPending = useCallback(async (): Promise<number> => {
try {
const synced = await syncPendingLocations();
await refreshPendingCount();
return synced;
} catch (err) {
console.error('Error sincronizando:', err);
return 0;
}
}, [syncPendingLocations, refreshPendingCount]);
// Calcular distancia a un punto
const getDistanceTo = useCallback(
(lat: number, lon: number): number | null => {
if (!ubicacionActual) return null;
return calculateDistance(
ubicacionActual.latitud,
ubicacionActual.longitud,
lat,
lon
);
},
[ubicacionActual]
);
return {
// Estado
ubicacion: ubicacionActual,
isTracking,
isLoading,
permissionStatus,
backgroundPermission,
pendingCount,
isOnline,
error,
// Acciones
requestPermissions,
startTracking,
stopTracking,
refreshLocation,
syncPending,
getDistanceTo,
};
};
export default useLocation;

View File

@@ -0,0 +1,232 @@
/**
* Hook personalizado para gestión de viajes
* Proporciona acceso al viaje activo y acciones de control
*/
import { useCallback, useEffect, useMemo } from 'react';
import { useViajeStore } from '../store/viajeStore';
import { useLocation } from './useLocation';
import type { Viaje, Parada, TipoParada } from '../types';
interface UseViajeReturn {
// Estado del viaje
viaje: Viaje | null;
proximoViaje: Viaje | null;
paradaActual: Parada | null;
isLoading: boolean;
isTracking: boolean;
error: string | null;
// Estadísticas
viajesHoy: number;
distanciaHoy: number;
tiempoHoy: number;
progreso: number;
// Info calculada
distanciaRestante: number;
tiempoEstimadoRestante: number;
distanciaAParada: number | null;
// Acciones de viaje
cargarViajeActivo: () => Promise<void>;
cargarProximoViaje: () => Promise<void>;
iniciar: (viajeId: string) => Promise<{ success: boolean; error?: string }>;
pausar: (motivo?: string) => Promise<{ success: boolean; error?: string }>;
reanudar: () => Promise<{ success: boolean; error?: string }>;
finalizar: (notas?: string) => Promise<{ success: boolean; error?: string }>;
// Acciones de paradas
llegarAParada: (paradaId: string) => Promise<{ success: boolean; error?: string }>;
salirDeParada: (
paradaId: string,
notas?: string
) => Promise<{ success: boolean; error?: string }>;
registrarParada: (data: {
tipo: TipoParada;
notas?: string;
}) => Promise<{ success: boolean; error?: string }>;
// Utilidades
limpiarError: () => void;
}
export const useViaje = (): UseViajeReturn => {
const {
viajeActivo,
proximoViaje,
paradaActual,
isLoading,
isTracking,
error,
viajesHoy,
distanciaHoy,
tiempoConduccionHoy,
fetchViajeActivo,
fetchProximoViaje,
iniciarViaje,
pausarViaje,
reanudarViaje,
finalizarViaje,
registrarLlegadaParada,
registrarSalidaParada,
registrarParadaNoProgramada,
clearError,
} = useViajeStore();
const { ubicacion, getDistanceTo } = useLocation({
autoStart: !!viajeActivo && viajeActivo.estado === 'en_curso',
viajeId: viajeActivo?.id,
});
// Cargar viaje activo al montar
useEffect(() => {
fetchViajeActivo();
fetchProximoViaje();
}, [fetchViajeActivo, fetchProximoViaje]);
// Calcular progreso del viaje
const progreso = useMemo(() => {
if (!viajeActivo || !viajeActivo.distanciaEstimada) return 0;
const recorrido = viajeActivo.distanciaRecorrida || 0;
return Math.min(100, (recorrido / viajeActivo.distanciaEstimada) * 100);
}, [viajeActivo]);
// Calcular distancia restante
const distanciaRestante = useMemo(() => {
if (!viajeActivo) return 0;
const estimada = viajeActivo.distanciaEstimada || 0;
const recorrida = viajeActivo.distanciaRecorrida || 0;
return Math.max(0, estimada - recorrida);
}, [viajeActivo]);
// Calcular tiempo estimado restante
const tiempoEstimadoRestante = useMemo(() => {
if (!viajeActivo) return 0;
const estimado = viajeActivo.tiempoEstimado || 0;
const transcurrido = viajeActivo.tiempoTranscurrido || 0;
return Math.max(0, estimado - transcurrido);
}, [viajeActivo]);
// Calcular distancia a la parada actual
const distanciaAParada = useMemo(() => {
if (!paradaActual || !ubicacion) return null;
return getDistanceTo(
paradaActual.punto.latitud,
paradaActual.punto.longitud
);
}, [paradaActual, ubicacion, getDistanceTo]);
// Cargar viaje activo
const cargarViajeActivo = useCallback(async () => {
await fetchViajeActivo();
}, [fetchViajeActivo]);
// Cargar próximo viaje
const cargarProximoViaje = useCallback(async () => {
await fetchProximoViaje();
}, [fetchProximoViaje]);
// Iniciar viaje
const iniciar = useCallback(
async (viajeId: string) => {
return await iniciarViaje(viajeId);
},
[iniciarViaje]
);
// Pausar viaje
const pausar = useCallback(
async (motivo?: string) => {
return await pausarViaje(motivo);
},
[pausarViaje]
);
// Reanudar viaje
const reanudar = useCallback(async () => {
return await reanudarViaje();
}, [reanudarViaje]);
// Finalizar viaje
const finalizar = useCallback(
async (notas?: string) => {
return await finalizarViaje(notas);
},
[finalizarViaje]
);
// Registrar llegada a parada
const llegarAParada = useCallback(
async (paradaId: string) => {
return await registrarLlegadaParada(paradaId);
},
[registrarLlegadaParada]
);
// Registrar salida de parada
const salirDeParada = useCallback(
async (paradaId: string, notas?: string) => {
return await registrarSalidaParada(paradaId, notas);
},
[registrarSalidaParada]
);
// Registrar parada no programada
const registrarParada = useCallback(
async (data: { tipo: TipoParada; notas?: string }) => {
return await registrarParadaNoProgramada(data);
},
[registrarParadaNoProgramada]
);
// Limpiar error
const limpiarError = useCallback(() => {
clearError();
}, [clearError]);
return {
// Estado
viaje: viajeActivo,
proximoViaje,
paradaActual,
isLoading,
isTracking,
error,
// Estadísticas
viajesHoy,
distanciaHoy,
tiempoHoy: tiempoConduccionHoy,
progreso,
// Info calculada
distanciaRestante,
tiempoEstimadoRestante,
distanciaAParada,
// Acciones de viaje
cargarViajeActivo,
cargarProximoViaje,
iniciar,
pausar,
reanudar,
finalizar,
// Acciones de paradas
llegarAParada,
salirDeParada,
registrarParada,
// Utilidades
limpiarError,
};
};
export default useViaje;