Initial commit: SIBU 2.0 MISSION

This commit is contained in:
2026-02-21 09:53:31 -05:00
commit 0c7aa53c8b
400 changed files with 67708 additions and 0 deletions

View File

@ -0,0 +1,446 @@
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<Map<String, dynamic>> 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': <RouteModel>[], 'count': 0, 'error': e.toString()};
}
}
// Get all available routes (legacy method updated to use new pattern)
Future<List<RouteModel>> getRoutes() async {
final result = await getRoutesWithCount();
return result['data'] as List<RouteModel>;
}
// HEAD count request for just getting route count as specified in requirements
Future<int> 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<String?> 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<Map<String, dynamic>?> 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<List<Map<String, dynamic>>> 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<Map<String, dynamic>>.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<List<Map<String, dynamic>>> 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<Map<String, dynamic>>.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<List<BusStopModel>> 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<BusStopModel> stops = [];
for (var item in response as List) {
if (item['stops'] != null) {
final stopData = Map<String, dynamic>.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<RouteModel?> 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<BusStopModel?> 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);
}
}