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:
494
mobile/src/store/viajeStore.ts
Normal file
494
mobile/src/store/viajeStore.ts
Normal file
@@ -0,0 +1,494 @@
|
||||
/**
|
||||
* Store de viajes usando Zustand
|
||||
* Gestiona el viaje activo, paradas y estado del tracking
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { viajesApi, paradasApi } from '../services/api';
|
||||
import { storage, STORAGE_KEYS } from '../services/storage';
|
||||
import {
|
||||
startLocationTracking,
|
||||
stopLocationTracking,
|
||||
getCurrentLocation,
|
||||
} from '../services/location';
|
||||
import type { Viaje, Parada, Ubicacion, TipoParada, EstadoViaje } from '../types';
|
||||
|
||||
interface ViajeState {
|
||||
// Estado
|
||||
viajeActivo: Viaje | null;
|
||||
proximoViaje: Viaje | null;
|
||||
paradaActual: Parada | null;
|
||||
isTracking: boolean;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Estadísticas del día
|
||||
viajesHoy: number;
|
||||
distanciaHoy: number;
|
||||
tiempoConduccionHoy: number;
|
||||
}
|
||||
|
||||
interface ViajeStore extends ViajeState {
|
||||
// Acciones de viaje
|
||||
fetchViajeActivo: () => Promise<void>;
|
||||
fetchProximoViaje: () => Promise<void>;
|
||||
iniciarViaje: (viajeId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
pausarViaje: (motivo?: string) => Promise<{ success: boolean; error?: string }>;
|
||||
reanudarViaje: () => Promise<{ success: boolean; error?: string }>;
|
||||
finalizarViaje: (notas?: string) => Promise<{ success: boolean; error?: string }>;
|
||||
|
||||
// Acciones de paradas
|
||||
registrarLlegadaParada: (paradaId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
registrarSalidaParada: (
|
||||
paradaId: string,
|
||||
notas?: string
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
registrarParadaNoProgramada: (data: {
|
||||
tipo: TipoParada;
|
||||
notas?: string;
|
||||
}) => Promise<{ success: boolean; error?: string }>;
|
||||
|
||||
// Utilidades
|
||||
setViajeActivo: (viaje: Viaje | null) => void;
|
||||
updateEstadisticasDia: (stats: Partial<ViajeState>) => void;
|
||||
clearError: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const initialState: ViajeState = {
|
||||
viajeActivo: null,
|
||||
proximoViaje: null,
|
||||
paradaActual: null,
|
||||
isTracking: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
viajesHoy: 0,
|
||||
distanciaHoy: 0,
|
||||
tiempoConduccionHoy: 0,
|
||||
};
|
||||
|
||||
export const useViajeStore = create<ViajeStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...initialState,
|
||||
|
||||
// Obtiene el viaje activo del servidor
|
||||
fetchViajeActivo: async () => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const response = await viajesApi.getViajeActivo();
|
||||
|
||||
if (response.success) {
|
||||
const viaje = response.data;
|
||||
|
||||
if (viaje) {
|
||||
// Encontrar parada actual (primera no completada)
|
||||
const paradaActual =
|
||||
viaje.paradas.find((p) => !p.completada && p.orden === 1) ||
|
||||
viaje.paradas.find((p) => !p.completada) ||
|
||||
null;
|
||||
|
||||
// Si hay viaje en curso, asegurar que el tracking está activo
|
||||
if (viaje.estado === 'en_curso') {
|
||||
await startLocationTracking(viaje.id);
|
||||
set({ isTracking: true });
|
||||
}
|
||||
|
||||
set({ viajeActivo: viaje, paradaActual });
|
||||
|
||||
// Guardar localmente para modo offline
|
||||
await storage.set(STORAGE_KEYS.VIAJE_ACTIVO, viaje);
|
||||
} else {
|
||||
set({ viajeActivo: null, paradaActual: null });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Intentar cargar desde storage local
|
||||
const viajeLocal = await storage.get<Viaje>(STORAGE_KEYS.VIAJE_ACTIVO);
|
||||
|
||||
if (viajeLocal) {
|
||||
set({ viajeActivo: viajeLocal });
|
||||
}
|
||||
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Error al obtener viaje',
|
||||
});
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Obtiene el próximo viaje asignado
|
||||
fetchProximoViaje: async () => {
|
||||
try {
|
||||
const response = await viajesApi.getProximoViaje();
|
||||
|
||||
if (response.success) {
|
||||
set({ proximoViaje: response.data });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error obteniendo próximo viaje:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Inicia un viaje
|
||||
iniciarViaje: async (viajeId: string) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const response = await viajesApi.iniciarViaje(viajeId);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const viaje = response.data;
|
||||
|
||||
// Iniciar tracking de ubicación
|
||||
await startLocationTracking(viaje.id);
|
||||
|
||||
const paradaActual =
|
||||
viaje.paradas.find((p) => !p.completada) || null;
|
||||
|
||||
set({
|
||||
viajeActivo: viaje,
|
||||
paradaActual,
|
||||
isTracking: true,
|
||||
proximoViaje: null,
|
||||
});
|
||||
|
||||
await storage.set(STORAGE_KEYS.VIAJE_ACTIVO, viaje);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: response.error || 'No se pudo iniciar el viaje',
|
||||
};
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Error al iniciar viaje';
|
||||
set({ error: message });
|
||||
return { success: false, error: message };
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Pausa el viaje activo
|
||||
pausarViaje: async (motivo?: string) => {
|
||||
const { viajeActivo } = get();
|
||||
|
||||
if (!viajeActivo) {
|
||||
return { success: false, error: 'No hay viaje activo' };
|
||||
}
|
||||
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const response = await viajesApi.pausarViaje(viajeActivo.id, motivo);
|
||||
|
||||
if (response.success && response.data) {
|
||||
// Detener tracking
|
||||
await stopLocationTracking();
|
||||
|
||||
set({
|
||||
viajeActivo: response.data,
|
||||
isTracking: false,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: response.error || 'No se pudo pausar el viaje',
|
||||
};
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Error al pausar viaje';
|
||||
set({ error: message });
|
||||
return { success: false, error: message };
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Reanuda el viaje pausado
|
||||
reanudarViaje: async () => {
|
||||
const { viajeActivo } = get();
|
||||
|
||||
if (!viajeActivo) {
|
||||
return { success: false, error: 'No hay viaje activo' };
|
||||
}
|
||||
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const response = await viajesApi.reanudarViaje(viajeActivo.id);
|
||||
|
||||
if (response.success && response.data) {
|
||||
// Reanudar tracking
|
||||
await startLocationTracking(response.data.id);
|
||||
|
||||
set({
|
||||
viajeActivo: response.data,
|
||||
isTracking: true,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: response.error || 'No se pudo reanudar el viaje',
|
||||
};
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Error al reanudar viaje';
|
||||
set({ error: message });
|
||||
return { success: false, error: message };
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Finaliza el viaje activo
|
||||
finalizarViaje: async (notas?: string) => {
|
||||
const { viajeActivo } = get();
|
||||
|
||||
if (!viajeActivo) {
|
||||
return { success: false, error: 'No hay viaje activo' };
|
||||
}
|
||||
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const response = await viajesApi.finalizarViaje(viajeActivo.id, notas);
|
||||
|
||||
if (response.success) {
|
||||
// Detener tracking
|
||||
await stopLocationTracking();
|
||||
|
||||
// Actualizar estadísticas del día
|
||||
const state = get();
|
||||
set({
|
||||
viajeActivo: null,
|
||||
paradaActual: null,
|
||||
isTracking: false,
|
||||
viajesHoy: state.viajesHoy + 1,
|
||||
distanciaHoy: state.distanciaHoy + (viajeActivo.distanciaRecorrida || 0),
|
||||
tiempoConduccionHoy:
|
||||
state.tiempoConduccionHoy + (viajeActivo.tiempoTranscurrido || 0),
|
||||
});
|
||||
|
||||
await storage.remove(STORAGE_KEYS.VIAJE_ACTIVO);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: response.error || 'No se pudo finalizar el viaje',
|
||||
};
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Error al finalizar viaje';
|
||||
set({ error: message });
|
||||
return { success: false, error: message };
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Registra llegada a una parada
|
||||
registrarLlegadaParada: async (paradaId: string) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const ubicacion = await getCurrentLocation();
|
||||
|
||||
if (!ubicacion) {
|
||||
return { success: false, error: 'No se pudo obtener la ubicación' };
|
||||
}
|
||||
|
||||
const response = await paradasApi.registrarLlegada(paradaId, ubicacion);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const { viajeActivo } = get();
|
||||
|
||||
if (viajeActivo) {
|
||||
// Actualizar parada en el viaje
|
||||
const paradasActualizadas = viajeActivo.paradas.map((p) =>
|
||||
p.id === paradaId ? response.data! : p
|
||||
);
|
||||
|
||||
const viajeActualizado = {
|
||||
...viajeActivo,
|
||||
paradas: paradasActualizadas,
|
||||
};
|
||||
|
||||
set({
|
||||
viajeActivo: viajeActualizado,
|
||||
paradaActual: response.data,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: response.error || 'No se pudo registrar la llegada',
|
||||
};
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Error al registrar llegada';
|
||||
set({ error: message });
|
||||
return { success: false, error: message };
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Registra salida de una parada
|
||||
registrarSalidaParada: async (paradaId: string, notas?: string) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const response = await paradasApi.registrarSalida(paradaId, notas);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const { viajeActivo } = get();
|
||||
|
||||
if (viajeActivo) {
|
||||
// Actualizar parada y encontrar la siguiente
|
||||
const paradasActualizadas = viajeActivo.paradas.map((p) =>
|
||||
p.id === paradaId ? { ...response.data!, completada: true } : p
|
||||
);
|
||||
|
||||
const siguienteParada =
|
||||
paradasActualizadas.find((p) => !p.completada) || null;
|
||||
|
||||
const viajeActualizado = {
|
||||
...viajeActivo,
|
||||
paradas: paradasActualizadas,
|
||||
};
|
||||
|
||||
set({
|
||||
viajeActivo: viajeActualizado,
|
||||
paradaActual: siguienteParada,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: response.error || 'No se pudo registrar la salida',
|
||||
};
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Error al registrar salida';
|
||||
set({ error: message });
|
||||
return { success: false, error: message };
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Registra una parada no programada
|
||||
registrarParadaNoProgramada: async (data: {
|
||||
tipo: TipoParada;
|
||||
notas?: string;
|
||||
}) => {
|
||||
const { viajeActivo } = get();
|
||||
|
||||
if (!viajeActivo) {
|
||||
return { success: false, error: 'No hay viaje activo' };
|
||||
}
|
||||
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const ubicacion = await getCurrentLocation();
|
||||
|
||||
if (!ubicacion) {
|
||||
return { success: false, error: 'No se pudo obtener la ubicación' };
|
||||
}
|
||||
|
||||
const response = await paradasApi.registrarParadaNoProgramada({
|
||||
viajeId: viajeActivo.id,
|
||||
tipo: data.tipo,
|
||||
ubicacion,
|
||||
notas: data.notas,
|
||||
});
|
||||
|
||||
if (response.success && response.data) {
|
||||
// Añadir parada al viaje
|
||||
const viajeActualizado = {
|
||||
...viajeActivo,
|
||||
paradas: [...viajeActivo.paradas, response.data],
|
||||
};
|
||||
|
||||
set({ viajeActivo: viajeActualizado });
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: response.error || 'No se pudo registrar la parada',
|
||||
};
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Error al registrar parada';
|
||||
set({ error: message });
|
||||
return { success: false, error: message };
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Establece viaje activo manualmente
|
||||
setViajeActivo: (viaje: Viaje | null) => {
|
||||
set({ viajeActivo: viaje });
|
||||
if (viaje) {
|
||||
storage.set(STORAGE_KEYS.VIAJE_ACTIVO, viaje);
|
||||
} else {
|
||||
storage.remove(STORAGE_KEYS.VIAJE_ACTIVO);
|
||||
}
|
||||
},
|
||||
|
||||
// Actualiza estadísticas del día
|
||||
updateEstadisticasDia: (stats: Partial<ViajeState>) => {
|
||||
set(stats);
|
||||
},
|
||||
|
||||
// Limpia error
|
||||
clearError: () => {
|
||||
set({ error: null });
|
||||
},
|
||||
|
||||
// Reinicia el store
|
||||
reset: () => {
|
||||
stopLocationTracking();
|
||||
set(initialState);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'viaje-storage',
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
partialState: (state) => ({
|
||||
viajeActivo: state.viajeActivo,
|
||||
viajesHoy: state.viajesHoy,
|
||||
distanciaHoy: state.distanciaHoy,
|
||||
tiempoConduccionHoy: state.tiempoConduccionHoy,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useViajeStore;
|
||||
Reference in New Issue
Block a user