/** Service for bus stop-related API calls */ import { supabase } from '@/supabase' import type { BusStop, Route } from '@/types' // Map JS getDay() (0=Sun,6=Sat) to Spanish day names used in dias_operacion const DAY_MAP: Record = { 0: 'domingo', 1: 'lunes', 2: 'martes', 3: 'miercoles', 4: 'jueves', 5: 'viernes', 6: 'sabado' } /** Convert "HH:MM:SS" time string to total minutes since midnight */ function timeToMinutes(t: string): number { const parts = t.split(':') return parseInt(parts[0] || '0') * 60 + parseInt(parts[1] || '0') } /** Convert total minutes since midnight back to "HH:MM" string */ function minutesToTime(m: number): string { const safeM = ((m % 1440) + 1440) % 1440 // wrap around midnight const h = Math.floor(safeM / 60) const min = safeM % 60 return `${h.toString().padStart(2, '0')}:${min.toString().padStart(2, '0')}` } export interface BusArrival { routeName: string routeId: string arrivalTime: string // "HH:MM" when bus arrives at this stop waitMinutes: number | null // null = no GPS, raw schedule shown hasGps: boolean departureTime: string // "HH:MM" when bus departs origin alreadyPassed: boolean // true if bus has already passed this stop } export const busStopsService = { /** Get all bus stops */ async getAllBusStops(): Promise { const { data, error } = await supabase.from('bus_stops').select('id, name, latitude, longitude, city, address, stop_type, has_shelter, has_seating, is_accessible, created_at, updated_at') if (error) throw new Error(error.message) return data as BusStop[] }, /** Get a single bus stop by ID */ async getBusStopById(id: string): Promise { const { data, error } = await supabase.from('bus_stops').select('id, name, latitude, longitude, city, address, stop_type, has_shelter, has_seating, is_accessible, created_at, updated_at').eq('id', id).single() if (error) throw new Error(error.message) return data as BusStop }, /** Get all routes passing through a bus stop */ async getBusStopRoutes(stopId: string): Promise { const { data, error } = await supabase .from('route_stops') .select('routes(*)') .eq('stop_id', stopId) if (error) throw new Error(error.message) // Extract the nested strictly typed route object automatically connected by Supabase relationships return (data || []).map((row: any) => row.routes) as Route[] }, /** * Real-time bus arrival calculator for a specific stop. * * Logic: * 1. Get all routes through this stop + their travel_time_minutes * 2. Get route's origin_stop_id → get its travel_time_minutes as the "base" * 3. Effective travel time = stop.travel_time - origin.travel_time (always >= 0) * 4. arrivalAtStop = departure_time + effective_travel_time * 5. With GPS: filter to future buses, return wait minutes * Without GPS (option A): show raw schedule times for today */ async getNextBusesForStop(stopId: string, userLat?: number, userLng?: number): Promise { const hasGps = userLat !== undefined && userLng !== undefined // ── STEP 1: Get route_stops entry for THIS stop on every route it belongs to ── const { data: routeStopsData, error: rsError } = await supabase .from('route_stops') .select('route_id, travel_time_minutes, stop_delay_minutes') .eq('stop_id', stopId) if (rsError) throw new Error(rsError.message) if (!routeStopsData || routeStopsData.length === 0) return [] const routeIds = routeStopsData.map((rs: any) => rs.route_id) // Build a lookup: routeId → raw travel_time_minutes of THIS stop const rawTravelTimeMap: Record = {} for (const rs of routeStopsData as any[]) { rawTravelTimeMap[rs.route_id] = (rs.travel_time_minutes || 0) + (rs.stop_delay_minutes || 0) } // ── STEP 2: Get route metadata including origin_stop_id ── const { data: routesData, error: routesError } = await supabase .from('routes') .select('id, name, origin_stop_id') .in('id', routeIds) if (routesError) throw new Error(routesError.message) const routeNameMap: Record = {} const originStopIdMap: Record = {} // routeId → origin_stop_id for (const r of (routesData || []) as any[]) { routeNameMap[r.id] = r.name originStopIdMap[r.id] = r.origin_stop_id || null } // ── STEP 3: Get travel_time_minutes of the origin stop for each route ── // This gives us the "base offset" so we can normalize travel times correctly. // If origin_stop has travel_time=5 and user's stop has travel_time=30, effective = 25 min. const originStopIds = [...new Set(Object.values(originStopIdMap).filter(Boolean))] as string[] const originTravelTimeMap: Record = {} // routeId → origin stop travel_time if (originStopIds.length > 0) { // Fetch travel_time_minutes for origin stops across all relevant routes const { data: originRsData } = await supabase .from('route_stops') .select('route_id, stop_id, travel_time_minutes') .in('route_id', routeIds) .in('stop_id', originStopIds) for (const ors of (originRsData || []) as any[]) { // Match route to its origin stop if (originStopIdMap[ors.route_id] === ors.stop_id) { originTravelTimeMap[ors.route_id] = ors.travel_time_minutes || 0 } } } // Compute EFFECTIVE travel time = user's stop travel_time - origin stop travel_time // This is always >= 0 if the data is correct (origin is before the user's stop) const effectiveTravelTimeMap: Record = {} for (const routeId of routeIds) { const rawTime = rawTravelTimeMap[routeId] ?? 0 const originTime = originTravelTimeMap[routeId] ?? 0 effectiveTravelTimeMap[routeId] = Math.max(0, rawTime - originTime) } // ── STEP 3: Get today's schedules ── const now = new Date() const todayDay = DAY_MAP[now.getDay()] const nowMinutes = now.getHours() * 60 + now.getMinutes() const { data: schedulesData, error: schError } = await supabase .from('bus_schedules') .select('route_id, departure_time, dias_operacion') .in('route_id', routeIds) .eq('is_published', true) .order('departure_time', { ascending: true }) if (schError) throw new Error(schError.message) if (!schedulesData || schedulesData.length === 0) return [] // ── STEP 4: Calculate arrival time for each schedule at THIS stop ── const arrivals: BusArrival[] = [] for (const sched of schedulesData as any[]) { // Check if this schedule runs today const diasOp: string[] = sched.dias_operacion || [] if (!todayDay || !diasOp.includes(todayDay)) continue const depMinutes = timeToMinutes(sched.departure_time) const travelMins = effectiveTravelTimeMap[sched.route_id] ?? 0 const arrivalMinutes = depMinutes + travelMins // Handle midnight crossover (e.g. departure 23:50 + 20min travel = 00:10 next day) const arrivalMinutesNormalized = arrivalMinutes % 1440 const alreadyPassed = arrivalMinutesNormalized < nowMinutes const waitMinutes = hasGps ? (arrivalMinutesNormalized - nowMinutes) : null // Without GPS (option A): show all future arrivals as raw times // With GPS: only show buses that haven't passed yet if (hasGps && alreadyPassed) continue // Skip buses more than 3 hours in the future to avoid showing the whole day if (hasGps && waitMinutes !== null && waitMinutes > 180) continue // Without GPS: skip past arrivals but show up to next 3 hours anyway if (!hasGps && alreadyPassed && Math.abs(arrivalMinutesNormalized - nowMinutes) > 10) continue arrivals.push({ routeName: routeNameMap[sched.route_id] || 'Ruta desconocida', routeId: sched.route_id, arrivalTime: minutesToTime(arrivalMinutesNormalized), departureTime: minutesToTime(depMinutes), waitMinutes, hasGps, alreadyPassed }) } // Sort by arrival time ascending, return first 8 arrivals.sort((a, b) => { const aMin = timeToMinutes(a.arrivalTime) const bMin = timeToMinutes(b.arrivalTime) return aMin - bMin }) return arrivals.slice(0, 8) }, /** * @deprecated Use getNextBusesForStop instead. * Kept for compatibility — calls the real engine without GPS context. */ async getNextBusArrivals(stopId: string): Promise<{ routeName: string; arrivalTime: string }[]> { const real = await this.getNextBusesForStop(stopId) return real.map(a => ({ routeName: a.routeName, arrivalTime: a.arrivalTime })) }, /** Create a new bus stop (Admin) */ async createBusStop(currentData: import('@/types').BusStopCreate): Promise { const { data, error } = await supabase .from('bus_stops') .insert([currentData]) .select() .single() if (error) throw new Error(error.message) return data as BusStop }, /** Update a bus stop (Admin) */ async updateBusStop(id: string, currentData: import('@/types').BusStopUpdate): Promise { const { data, error } = await supabase .from('bus_stops') .update(currentData) .eq('id', id) .select() .single() if (error) throw new Error(error.message) return data as BusStop }, /** Delete a bus stop (Admin) */ async deleteBusStop(id: string): Promise { const { error } = await supabase.from('bus_stops').delete().eq('id', id) if (error) throw new Error(error.message) } }