Files
ATLAS/mobile/src/store/viajeStore.ts
FlotillasGPS Developer 51d78bacf4 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.
2026-01-21 08:18:00 +00:00

495 lines
14 KiB
TypeScript

/**
* 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;