import 'dart:math' as math; import 'package:fluttertoast/fluttertoast.dart'; import 'package:flutter/material.dart'; import '../models/bus_stop_model.dart'; import '../models/route_model.dart'; import './supabase_service.dart'; class TransportationService { final SupabaseService _supabaseService = SupabaseService.instance; /// Determines the current schedule type based on Panama timezone String _getCurrentScheduleType() { // Use Panama timezone for schedule type detection final panamaNow = DateTime.now().toUtc().add( Duration(hours: -5), ); // Panama is UTC-5 final dayOfWeek = panamaNow.weekday; // Monday = 1, Sunday = 7 if (dayOfWeek >= 1 && dayOfWeek <= 5) { return 'weekday'; } else if (dayOfWeek == 6) { return 'saturday'; } else { return 'sunday'; } } // Get all available routes using exact query pattern from requirements Future> getRoutesWithCount() async { try { // Use exact query: supabase.from('routes').select('id,name', { count: 'exact' }).order('name', {ascending: true}) final response = await SupabaseService.client .from('routes') .select('id,name') .order('name', ascending: true); final routes = (response as List) .map((route) => RouteModel.fromJson(route)) .toList(); return {'data': routes, 'count': routes.length, 'error': null}; } catch (e) { print('Error fetching routes: $e'); // Show toast on exception as specified in requirements Fluttertoast.showToast( msg: "Error loading routes: ${e.toString()}", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); return {'data': [], 'count': 0, 'error': e.toString()}; } } // Get all available routes (legacy method updated to use new pattern) Future> getRoutes() async { final result = await getRoutesWithCount(); return result['data'] as List; } // HEAD count request for just getting route count as specified in requirements Future getRoutesCount() async { try { // Use HEAD count request: supabase.from('routes').select('*', { count: 'exact', head: true }) final response = await SupabaseService.client.from('routes').select('*'); return (response as List).length; } catch (e) { print('Error getting routes count: $e'); Fluttertoast.showToast( msg: "Error getting routes count: ${e.toString()}", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); return 0; } } // Get first available route that has stops (for default selection) Future getFirstAvailableRouteId() async { try { final response = await SupabaseService.client .from('routes') .select('id') .order('name') .limit(1); if ((response as List).isNotEmpty) { // Verify the route has stops final routeId = response[0]['id']; final stopsResponse = await SupabaseService.client .from('route_stops') .select('route_id') .eq('route_id', routeId) .limit(1); if ((stopsResponse as List).isNotEmpty) { return routeId; } } return null; } catch (e) { print('Error getting first available route: $e'); Fluttertoast.showToast( msg: "Error getting default route: ${e.toString()}", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); return null; } } // Get next bus time using Panama timezone and exact query from requirements Future?> getNextBusTime( String routeId, String stopId, ) async { try { // Get current schedule type automatically using Panama time final scheduleType = _getCurrentScheduleType(); // Use exact "Next bus" query from requirements: // SELECT departure_time FROM timetable // WHERE route_id=:selectedRouteId AND schedule_type=:todayType // AND departure_time > (now() AT TIME ZONE 'America/Panama')::time // ORDER BY departure_time LIMIT 1 final response = await SupabaseService.client .from('timetable') .select('departure_time') .eq('route_id', routeId) .eq('schedule_type', scheduleType) .filter( 'departure_time', 'gt', 'extract(time from (now() AT TIME ZONE \'America/Panama\'))', ) .order('departure_time', ascending: true) .limit(1); if ((response as List).isNotEmpty) { final nextDeparture = response[0]['departure_time']; // Parse the departure time final parts = nextDeparture.split(':'); final departureHour = int.parse(parts[0]); final departureMinute = int.parse(parts[1]); // Calculate minutes until departure using Panama time final panamaNow = DateTime.now().toUtc().add(Duration(hours: -5)); final departureDateTime = DateTime( panamaNow.year, panamaNow.month, panamaNow.day, departureHour, departureMinute, ); int minutesUntil = departureDateTime.difference(panamaNow).inMinutes; // If negative, it means it's for tomorrow if (minutesUntil < 0) { minutesUntil = departureDateTime .add(const Duration(days: 1)) .difference(panamaNow) .inMinutes; } return { 'next_departure': nextDeparture, 'minutes_until_arrival': minutesUntil, 'estimated_arrival_time': departureDateTime.toIso8601String(), 'schedule_type': scheduleType, }; } // No more buses today, check for tomorrow's first departure final nextDayScheduleType = _getNextDayScheduleType(); final tomorrowResponse = await SupabaseService.client .from('timetable') .select('departure_time') .eq('route_id', routeId) .eq('schedule_type', nextDayScheduleType) .order('departure_time', ascending: true) .limit(1); if ((tomorrowResponse as List).isNotEmpty) { final firstTomorrowDeparture = tomorrowResponse[0]['departure_time']; return { 'next_departure': null, 'first_tomorrow': firstTomorrowDeparture, 'minutes_until_arrival': null, 'message': 'No more buses today', 'schedule_type': scheduleType, }; } return null; } catch (e) { print('Error getting next bus time: $e'); Fluttertoast.showToast( msg: "Error calculating next bus: ${e.toString()}", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); return null; } } String _getNextDayScheduleType() { // Use Panama timezone for next day calculation final tomorrow = DateTime.now().toUtc().add(Duration(hours: -5, days: 1)); final dayOfWeek = tomorrow.weekday; if (dayOfWeek >= 1 && dayOfWeek <= 5) { return 'weekday'; } else if (dayOfWeek == 6) { return 'saturday'; } else { return 'sunday'; } } // Get all timetables for a route with current day's schedule_type Future>> getRouteTimetables(String routeId) async { try { final scheduleType = _getCurrentScheduleType(); final response = await SupabaseService.client .from('timetable') .select('*') .eq('route_id', routeId) .eq('schedule_type', scheduleType) .order('departure_time'); return List>.from(response as List); } catch (e) { print('Error fetching route timetables: $e'); Fluttertoast.showToast( msg: "Error loading timetables: ${e.toString()}", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); return []; } } // Get all timetables for a specific schedule type using exact query from requirements Future>> getRouteTimetablesByScheduleType( String routeId, String scheduleType, ) async { try { // Use EXACT query as specified in requirements: // SELECT departure_time FROM timetable // WHERE route_id = :selectedRouteId AND schedule_type = :todayType // ORDER BY departure_time final response = await SupabaseService.client .from('timetable') .select('departure_time') .eq('route_id', routeId) .eq('schedule_type', scheduleType) .order('departure_time'); return List>.from(response as List); } catch (e) { print('Error fetching route timetables by schedule type: $e'); Fluttertoast.showToast( msg: "Error loading schedule: ${e.toString()}", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); return []; } } // Get stops for a specific route - using EXACT SQL query from requirements Future> getRouteStopsOrderedBySeq(String routeId) async { try { // Use EXACT SQL query as specified in requirements: // SELECT s.id, s.name, s.lat, s.lng, rs.seq // FROM route_stops rs JOIN stops s ON s.id = rs.stop_id // WHERE rs.route_id = :selectedRouteId ORDER BY rs.seq // REMOVED RPC CALL - using direct SQL table queries instead final response = await SupabaseService.client .from('route_stops') .select(''' seq, stops:stop_id ( id, name, lat, lng ) ''') .eq('route_id', routeId) .order('seq', ascending: true); List stops = []; for (var item in response as List) { if (item['stops'] != null) { final stopData = Map.from(item['stops']); stopData['seq'] = item['seq']; stops.add(BusStopModel.fromJson(stopData)); } } // Show "No stops found for this route" message if empty as per requirements if (stops.isEmpty) { Fluttertoast.showToast( msg: "No stops found for this route", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.orange, textColor: Colors.white, fontSize: 16.0, ); } return stops; } catch (e) { print('Error fetching route stops: $e'); // Show Supabase connection error message as per requirements if (e.toString().contains('connection') || e.toString().contains('network')) { Fluttertoast.showToast( msg: "Could not connect to Supabase. Please check credentials.", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); } else { Fluttertoast.showToast( msg: "Error loading route stops: ${e.toString()}", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); } rethrow; // Re-throw to let calling code handle it } } // Get route by ID Future getRouteById(String routeId) async { try { final response = await SupabaseService.client .from('routes') .select('*') .eq('id', routeId) .single(); return RouteModel.fromJson(response); } catch (e) { print('Error fetching route: $e'); Fluttertoast.showToast( msg: "Error loading route details: ${e.toString()}", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); return null; } } // Get stop by ID Future getStopById(String stopId) async { try { final response = await SupabaseService.client .from('stops') .select('*') .eq('id', stopId) .single(); return BusStopModel.fromJson(response); } catch (e) { print('Error fetching stop: $e'); Fluttertoast.showToast( msg: "Error loading stop details: ${e.toString()}", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); return null; } } // Simple distance calculation (Haversine formula) double _calculateDistance( double lat1, double lng1, double lat2, double lng2, ) { const double earthRadius = 6371; // km double dLat = _toRadians(lat2 - lat1); double dLng = _toRadians(lng2 - lng1); double a = math.sin(dLat / 2) * math.sin(dLat / 2) + math.cos(_toRadians(lat1)) * math.cos(_toRadians(lat2)) * math.sin(dLng / 2) * math.sin(dLng / 2); double c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); return earthRadius * c; } double _toRadians(double degrees) { return degrees * (math.pi / 180); } }