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.
495 lines
14 KiB
TypeScript
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;
|