diff --git a/frontend/src/components/auth/LoginForm.vue b/frontend/src/components/auth/LoginForm.vue index 2832af4..b1da660 100644 --- a/frontend/src/components/auth/LoginForm.vue +++ b/frontend/src/components/auth/LoginForm.vue @@ -22,7 +22,7 @@ const handleLogin = async () => { errorMessage.value = '' try { - await authStore.login(email.value.trim().toLowerCase(), password.value) + await authStore.login(email.value.trim().toLowerCase(), password.value, keepSession.value) // El rol ya está disponible en el store (del JWT), navegar directo navigateByUserRole(authStore.role || 'PASSENGER') diff --git a/frontend/src/composables/useETA.ts b/frontend/src/composables/useETA.ts index 2aaa7d1..f8b003b 100644 --- a/frontend/src/composables/useETA.ts +++ b/frontend/src/composables/useETA.ts @@ -14,38 +14,96 @@ export function useETA() { const busesActivos = ref([]); const cargando = ref(false); + // Configuración para el cálculo del ETA mejorado + const VELOCIDAD_PROMEDIO_KMH = 35; // km/h (promedio ciudad/carretera) + const TIEMPO_PARADA_SEGUNDOS = 45; // segundos detenido por parada + + // Fórmula Haversine para distancia en línea recta (km) + function getDistanceKm(lat1: number, lon1: number, lat2: number, lon2: number): number { + const R = 6371; + const dLat = (lat2 - lat1) * (Math.PI / 180); + const dLon = (lon2 - lon1) * (Math.PI / 180); + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + } + const calcularETA = async (ruta_id: string, parada_cercana: BusStop) => { cargando.value = true; busesActivos.value = []; try { - // PASO 1: Obtener travel_time_minutes de la parada del usuario - // desde route_stops usando el stop_id de la parada cercana - const { data: routeStopData, error: rsError } = await supabase - .from('route_stops') - .select('travel_time_minutes, stop_order, stop_delay_minutes') - .eq('route_id', ruta_id) - .eq('stop_id', parada_cercana.id) - .single(); + // PASO 1: Obtener detalles de la ruta y todas sus paradas para calcular distancia real + const [routeRes, stopsRes] = await Promise.all([ + supabase + .from('routes') + .select('distance_km, average_speed_kmh') + .eq('id', ruta_id) + .single(), + supabase + .from('route_stops') + .select('stop_id, stop_order, bus_stops(latitude, longitude)') + .eq('route_id', ruta_id) + .order('stop_order', { ascending: true }) + ]); - if (rsError || !routeStopData) { - console.warn('SIBU | No se encontró travel_time para la parada:', - parada_cercana.name); - return; + if (routeRes.error || !stopsRes.data) throw new Error('Error al cargar datos de ruta'); + + const routeData = routeRes.data; + const routeStops = stopsRes.data; + + // Encontrar el orden de la parada donde está el usuario + const targetStopIndex = routeStops.findIndex(s => s.stop_id === parada_cercana.id); + if (targetStopIndex === -1) return; + + // CALCULAR DISTANCIA ACUMULADA (Terminal hasta Parada Destino) + // Usamos Haversine entre cada parada consecutiva para mayor precisión que una línea recta total + let distanciaAcumuladaKm = 0; + for (let i = 0; i < targetStopIndex; i++) { + const stopA = routeStops[i]; + const stopB = routeStops[i + 1]; + + const start = stopA ? (stopA.bus_stops as any) : null; + const end = stopB ? (stopB.bus_stops as any) : null; + + if (start?.latitude != null && start?.longitude != null && + end?.latitude != null && end?.longitude != null) { + distanciaAcumuladaKm += getDistanceKm( + start.latitude, start.longitude, + end.latitude, end.longitude + ); + } } - // Tiempo total hasta la parada del usuario en minutos - const minutosHastaParada = - (routeStopData.travel_time_minutes ?? 0) + - (routeStopData.stop_delay_minutes ?? 0); + // Aplicar factor de corrección de ruta (las calles no son rectas, aprox +20%) + distanciaAcumuladaKm *= 1.2; - // PASO 2: Obtener horarios activos para hoy + // PASO 2: Aplicar Fórmula Mejorada (Requerida por el usuario) + // ETA = (Distancia / Velocidad) + (Nº Paradas * Tiempo Parada) + + const velocidad = routeData.average_speed_kmh || VELOCIDAD_PROMEDIO_KMH; + + // Tiempo de viaje (en minutos) + const tiempoViajeMinutos = (distanciaAcumuladaKm / velocidad) * 60; + + // Tiempo total en paradas (N paradas previas × tiempo promedio) + // numeroParadas = stops antes de llegar a la parada destino + const numeroParadas = targetStopIndex; + const tiempoParadasMinutos = (numeroParadas * TIEMPO_PARADA_SEGUNDOS) / 60; + + // Tiempo total hasta la parada del usuario + const minutosHastaParada = tiempoViajeMinutos + tiempoParadasMinutos; + + console.log(`SIBU ETA | Ruta: ${ruta_id} | Dist: ${distanciaAcumuladaKm.toFixed(2)}km | Paradas: ${numeroParadas} | Total: ${minutosHastaParada.toFixed(1)}min`); + + // PASO 3: Obtener horarios activos para hoy const diaActual = new Date().getDay(); - const dias = ['domingo', 'lunes', 'martes', 'miercoles', - 'jueves', 'viernes', 'sabado']; + const dias = ['domingo', 'lunes', 'martes', 'miercoles', 'jueves', 'viernes', 'sabado']; const diaString = dias[diaActual]; - const tipoDia = (diaActual === 0 || diaActual === 6) - ? 'weekend' : 'weekday'; + const tipoDia = (diaActual === 0 || diaActual === 6) ? 'weekend' : 'weekday'; const { data: horarios, error: hError } = await supabase .from('bus_schedules') @@ -56,60 +114,44 @@ export function useETA() { if (hError) throw hError; - // Filtrar horarios que operan hoy const horariosHoy = (horarios ?? []).filter(h => { if (h.dias_operacion) { - return h.dias_operacion.includes('todos') || - h.dias_operacion.includes(diaString); + return h.dias_operacion.includes('todos') || h.dias_operacion.includes(diaString); } - return h.schedule_type === tipoDia || - h.schedule_type === 'todos' || - !h.schedule_type; + return h.schedule_type === tipoDia || h.schedule_type === 'todos' || !h.schedule_type; }); - // PASO 3: Calcular ETA para cada horario + // PASO 4: Calcular ETA para cada salida desde la terminal const ahora = new Date(); const minutosAhora = ahora.getHours() * 60 + ahora.getMinutes(); - const resultados: BusETA[] = []; for (const h of horariosHoy) { - const salida = h.departure_time; // "HH:mm:ss" + const salida = h.departure_time; if (!salida) continue; - // Parsear hora de salida a minutos desde medianoche const [hStr, mStr] = salida.split(':'); const minutosSalida = parseInt(hStr) * 60 + parseInt(mStr); - // ── FÓRMULA PRINCIPAL ──────────────────────────── - // Hora de llegada del bus a la parada del usuario: + // Hora estimada de llegada a la parada del usuario: const minutosLlegadaParada = minutosSalida + minutosHastaParada; // ETA = cuántos minutos faltan desde ahora: const etaMinutos = minutosLlegadaParada - minutosAhora; - // ───────────────────────────────────────────────── - // Formatear hora de llegada para mostrar en UI const horaLlegada = minutosAHora(minutosLlegadaParada); const horaSalidaFormato = `${hStr.padStart(2, '0')}:${mStr.padStart(2, '0')}`; - // Determinar estado let estado: BusETA['estado']; - if (etaMinutos > 5) { - // Bus aún no llega a la parada - // Verificar si ya salió del terminal o no const yaPartio = minutosAhora >= minutosSalida; estado = yaPartio ? 'en_camino' : 'próximo'; } else if (etaMinutos >= -2) { - // Llegando ahora (ventana de ±2 minutos) estado = 'en_camino'; } else { - // Ya pasó la parada estado = 'pasó'; } - // No incluir buses que pasaron hace más de 60 minutos if (etaMinutos < -60) continue; resultados.push({ @@ -121,9 +163,6 @@ export function useETA() { }); } - // PASO 4: Ordenar resultados - // Prioridad: en_camino → próximo → pasó - // Dentro de cada grupo: menor ETA primero resultados.sort((a, b) => { const prioridad = { 'en_camino': 0, 'próximo': 1, 'pasó': 2 }; if (prioridad[a.estado] !== prioridad[b.estado]) { @@ -132,7 +171,6 @@ export function useETA() { return a.etaMinutos - b.etaMinutos; }); - // Máximo 3 buses busesActivos.value = resultados.slice(0, 3); } catch (e) { diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts index 1e2e7b5..3165f6b 100644 --- a/frontend/src/stores/auth.ts +++ b/frontend/src/stores/auth.ts @@ -35,12 +35,22 @@ export const useAuthStore = defineStore('auth', () => { } }) - async function login(email: string, pass: string) { + async function login(email: string, pass: string, keepSession: boolean = false) { const { data, error } = await supabase.auth.signInWithPassword({ email, password: pass }) if (error) throw new Error(error.message) if (data.user) { - // Rol disponible al instante desde el JWT — sin consultas BD bloqueantes + // Manejo de persistencia: Si el usuario NO quiere mantener sesión iniciada, + // movemos el token de localStorage a sessionStorage. + if (!keepSession) { + const storageKey = `sb-bjgixlugjzsccazdfmph-auth-token` + const sessionData = localStorage.getItem(storageKey) + if (sessionData) { + sessionStorage.setItem(storageKey, sessionData) + localStorage.removeItem(storageKey) + } + } + userSession.value = data.session userProfile.value = { id: data.user.id, diff --git a/frontend/src/supabase.ts b/frontend/src/supabase.ts index 964ca43..9c81344 100644 --- a/frontend/src/supabase.ts +++ b/frontend/src/supabase.ts @@ -3,4 +3,28 @@ import { createClient } from '@supabase/supabase-js' export const SUPABASE_URL = 'https://bjgixlugjzsccazdfmph.supabase.co' export const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJqZ2l4bHVnanpzY2NhemRmbXBoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIwNjQyMTAsImV4cCI6MjA4NzY0MDIxMH0.untLQoPi4yUr3cPnxo23wYSlg6xnNK0daKu9UHmFTp8' -export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY) +// SIBU | Hybrid Storage: Maneja persistencia según la voluntad del usuario +const authStorage = { + getItem: (key: string) => localStorage.getItem(key) || sessionStorage.getItem(key), + setItem: (key: string, val: string) => { + // Si ya existe en sessionStorage, seguimos guardando allí (no persistente) + if (sessionStorage.getItem(key)) { + sessionStorage.setItem(key, val) + } else { + // Por defecto guardamos en localStorage (persistente) + localStorage.setItem(key, val) + } + }, + removeItem: (key: string) => { + localStorage.removeItem(key) + sessionStorage.removeItem(key) + } +} + +export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { + auth: { + storage: authStorage as any, + autoRefreshToken: true, + persistSession: true + } +})