import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:fluttertoast/fluttertoast.dart'; import '../../models/bus_stop_model.dart'; import '../../models/route_model.dart'; import '../../services/transportation_service.dart'; import '../../services/app_state_service.dart'; import '../../services/supabase_service.dart'; import '../../widgets/custom_bottom_bar.dart'; import '../../widgets/route_selection_bottom_sheet.dart'; import '../../widgets/debug_banner_widget.dart'; import './widgets/bus_arrival_bottom_sheet.dart'; class MapScreen extends StatefulWidget { const MapScreen({super.key}); @override State createState() => _MapScreenState(); } class _MapScreenState extends State { final TransportationService _transportationService = TransportationService(); final AppStateService _appStateService = AppStateService(); GoogleMapController? _mapController; // State variables List _routeStops = []; BusStopModel? _selectedStop; Set _markers = {}; bool _isLoading = false; String? _error; String? _nextBusMessage; String? _lastRoutesError; int _routeCount = 0; // New connection check state variables bool _isConnected = false; String _connectionStatus = 'Not checked'; // Panama coordinates (centered around David/Boquete area) static const LatLng _initialPosition = LatLng(8.4177, -82.4270); @override void initState() { super.initState(); _appStateService.addListener(_onGlobalStateChanged); _performSupabaseConnectionCheck(); } @override void dispose() { _appStateService.removeListener(_onGlobalStateChanged); _mapController?.dispose(); super.dispose(); } void _onGlobalStateChanged() { // React to global route selection changes if (mounted) { _loadStopsForSelectedRoute(); } } /// Perform Supabase connection check as specified in requirements Future _performSupabaseConnectionCheck() async { if (!mounted) return; setState(() { _isLoading = true; _error = null; }); // Step 1 & 2: Validate credentials and initialize Supabase client final connectionResult = await SupabaseService.performConnectionCheck(); if (connectionResult['success']) { // Connection successful - show "Connected ✓" and count if (mounted) { setState(() { _isConnected = true; _connectionStatus = 'Connected ✓'; _routeCount = connectionResult['count'] ?? 0; _lastRoutesError = null; _isLoading = false; }); } // Load initial data after successful connection if (mounted) { await _loadInitialData(); } } else { // Connection failed - show error final errorMessage = connectionResult['error'] ?? 'Unknown error'; if (mounted) { setState(() { _isConnected = false; _connectionStatus = 'Connection Failed'; _lastRoutesError = errorMessage; _error = errorMessage; _isLoading = false; }); } // Show red toast for credential issues if (errorMessage.contains('Missing') || errorMessage.contains('SUPABASE_URL') || errorMessage.contains('SUPABASE_ANON_KEY')) { Fluttertoast.showToast( msg: "Missing SUPABASE_URL or SUPABASE_ANON_KEY", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0, ); } } } Future _loadInitialData() async { setState(() { _isLoading = true; _error = null; _lastRoutesError = null; }); try { // Check if we have routes loaded globally if (_appStateService.allRoutes.isEmpty && !_appStateService.isLoadingRoutes) { await _appStateService.loadRoutes(); } final routes = _appStateService.allRoutes; if (mounted) { setState(() { _routeCount = routes.length; _lastRoutesError = null; // Clear any previous errors when successful _isConnected = true; // Update connection status on successful data load _connectionStatus = 'Connected ✓'; }); } // Show toast if no routes found as specified in requirements if (routes.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { Fluttertoast.showToast( msg: "No routes available", toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: Colors.orange, textColor: Colors.white, fontSize: 16.0, ); }); if (mounted) { setState(() { _isLoading = false; }); } return; } // Auto-pick the first route if no route is selected (Step 6 from requirements) if (mounted && _appStateService.selectedRouteId == null && routes.isNotEmpty) { final firstRoute = routes.first; await _appStateService.selectRoute(firstRoute.id); } // Load stops for currently selected route if (mounted) { await _loadStopsForSelectedRoute(); } } catch (e) { if (mounted) { setState(() { _error = 'Error loading routes: ${e.toString()}'; _lastRoutesError = e.toString(); _isLoading = false; _isConnected = false; _connectionStatus = 'Connection Failed'; }); } // Show Supabase connection error as per requirements if (e.toString().contains('connection') || e.toString().contains('credentials')) { WidgetsBinding.instance.addPostFrameCallback((_) { 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, ); }); } } } Future _loadStopsForSelectedRoute() async { final selectedRouteId = _appStateService.selectedRouteId; if (selectedRouteId == null) { setState(() { _routeStops = []; _markers = {}; _selectedStop = null; _nextBusMessage = null; _isLoading = false; }); return; } setState(() { _isLoading = true; _error = null; _selectedStop = null; _nextBusMessage = null; }); try { // Use the exact query from requirements (Step 7) final stops = await _transportationService.getRouteStopsOrderedBySeq( selectedRouteId, ); if (mounted) { setState(() { _routeStops = stops; _isLoading = false; }); } // Clear and re-render markers if (mounted) { await _updateMapMarkers(); } // Move camera to show all stops if (mounted && stops.isNotEmpty && _mapController != null) { _fitCameraToStops(stops); } } catch (e) { if (mounted) { setState(() { _error = 'Error loading stops: ${e.toString()}'; _isLoading = false; _routeStops = []; _markers = {}; }); } } } Future _updateMapMarkers() async { Set markers = {}; for (int i = 0; i < _routeStops.length; i++) { final stop = _routeStops[i]; final isSelected = _selectedStop?.id == stop.id; markers.add( Marker( markerId: MarkerId(stop.id), position: LatLng(stop.lat, stop.lng), onTap: () => _onStopTapped(stop), icon: await _createStopMarkerIcon( isSelected: isSelected, stopNumber: (i + 1).toString(), ), infoWindow: InfoWindow(title: stop.name, snippet: 'Parada ${i + 1}'), ), ); } if (mounted) { setState(() { _markers = markers; }); } } Future _createStopMarkerIcon({ required bool isSelected, required String stopNumber, }) async { if (isSelected) { return BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen); } else { return BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueYellow); } } Future _onStopTapped(BusStopModel stop) async { if (!mounted) return; setState(() { _selectedStop = stop; _nextBusMessage = null; }); if (mounted) { await _updateMapMarkers(); } // Calculate next bus time for this stop if (mounted) { final selectedRouteId = _appStateService.selectedRouteId; if (selectedRouteId != null) { await _calculateNextBusTime(stop, selectedRouteId); } } } Future _calculateNextBusTime(BusStopModel stop, String routeId) async { try { final nextBusInfo = await _transportationService.getNextBusTime( routeId, stop.id, ); if (nextBusInfo != null) { if (nextBusInfo['minutes_until_arrival'] != null) { final minutes = nextBusInfo['minutes_until_arrival'] ?? 0; final scheduleType = nextBusInfo['schedule_type'] ?? 'weekday'; String scheduleDisplay = ''; switch (scheduleType) { case 'weekday': scheduleDisplay = 'Lunes-Viernes'; break; case 'saturday': scheduleDisplay = 'Sábado'; break; case 'sunday': scheduleDisplay = 'Domingo'; break; } if (mounted) { setState(() { _nextBusMessage = 'Próximo bus en: $minutes min ($scheduleDisplay)'; }); } } else if (nextBusInfo['first_tomorrow'] != null) { if (mounted) { setState(() { _nextBusMessage = 'No hay más buses hoy. Primer bus mañana: ${nextBusInfo['first_tomorrow']}'; }); } } } else { if (mounted) { setState(() { _nextBusMessage = 'No hay más buses programados'; }); } } } catch (e) { if (mounted) { setState(() { _nextBusMessage = 'Error calculando próximo bus'; }); } } } void _showBusArrivalInfo(BusStopModel stop, RouteModel route) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => DraggableScrollableSheet( initialChildSize: 0.6, maxChildSize: 0.9, minChildSize: 0.3, builder: (context, scrollController) => BusArrivalBottomSheet(busStop: stop, route: route), ), ); } void _showRouteSelector() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) => RouteSelectionBottomSheet( title: 'Seleccionar Ruta', onRouteChanged: () { // The global state listener will handle the reload }, ), ); } void _fitCameraToStops(List stops) { if (stops.isEmpty || _mapController == null) return; double minLat = stops.first.lat; double maxLat = stops.first.lat; double minLng = stops.first.lng; double maxLng = stops.first.lng; for (final stop in stops) { minLat = math.min(minLat, stop.lat); maxLat = math.max(maxLat, stop.lat); minLng = math.min(minLng, stop.lng); maxLng = math.max(maxLng, stop.lng); } _mapController!.animateCamera( CameraUpdate.newLatLngBounds( LatLngBounds( southwest: LatLng(minLat - 0.01, minLng - 0.01), northeast: LatLng(maxLat + 0.01, maxLng + 0.01), ), 100.0, ), ); } void _onMapCreated(GoogleMapController controller) { _mapController = controller; if (_routeStops.isNotEmpty) { _fitCameraToStops(_routeStops); } } @override Widget build(BuildContext context) { final selectedRoute = _appStateService.getSelectedRoute(); final selectedRouteName = _appStateService.selectedRouteName; return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: const Color(0xFFFEE715), elevation: 0, leading: IconButton( icon: const Icon(Icons.menu, color: Color(0xFF101820)), onPressed: () { // TODO: Implement menu functionality }, ), title: const Text( 'SIBU', style: TextStyle( color: Color(0xFF101820), fontWeight: FontWeight.bold, fontSize: 20, ), ), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.feedback, color: Color(0xFF101820)), onPressed: () { // TODO: Implement feedback functionality }, ), ], ), body: RefreshIndicator( onRefresh: _performSupabaseConnectionCheck, child: Stack( children: [ // Google Map GoogleMap( onMapCreated: _onMapCreated, initialCameraPosition: const CameraPosition( target: _initialPosition, zoom: 11.0, ), markers: _markers, myLocationEnabled: true, myLocationButtonEnabled: false, zoomControlsEnabled: false, mapToolbarEnabled: false, ), // Debug banner with updated connection status DebugBannerWidget( lastError: _lastRoutesError, routeCount: _routeCount, isConnected: _isConnected, connectionStatus: _connectionStatus, ), // Route selector card (always visible when route is selected) if (selectedRoute != null && selectedRouteName != null) Positioned( top: 80, left: 16, right: 16, child: GestureDetector( onTap: _showRouteSelector, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(26), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( children: [ Icon( Icons.route, color: const Color(0xFF101820), size: 24, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Route: $selectedRouteName', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF101820), ), ), Text( '${_routeStops.length} stops', style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), ], ), ), Icon( Icons.keyboard_arrow_down, color: Colors.grey[600], size: 20, ), ], ), ), ), ), // Next bus info (when stop is selected) if (_selectedStop != null && selectedRoute != null) Positioned( bottom: 100, left: 16, right: 16, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFFEE715), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(26), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( _selectedStop!.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF101820), ), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( _nextBusMessage ?? 'Calculando próximo bus...', style: TextStyle( fontSize: 14, color: const Color(0xFF101820).withAlpha(204), ), textAlign: TextAlign.center, ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => _showBusArrivalInfo( _selectedStop!, selectedRoute, ), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF101820), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.symmetric(vertical: 12), ), child: const Text( 'CONSULTAR SIGUIENTE BUS', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ), ), ), ), ], ), ), ), // Loading indicator if (_isLoading) Container( color: Colors.black.withAlpha(77), child: const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFFFEE715)), ), ), ), // Error message (Step 4: red error card) if (_error != null) Positioned( top: 150, left: 16, right: 16, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.red[100], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.red[300]!), ), child: Row( children: [ Icon(Icons.error_outline, color: Colors.red[600], size: 24), const SizedBox(width: 12), Expanded( child: Text( _error!, style: TextStyle(color: Colors.red[600], fontSize: 14), ), ), IconButton( onPressed: () { setState(() => _error = null); _performSupabaseConnectionCheck(); }, icon: Icon( Icons.refresh, color: Colors.red[600], size: 20, ), ), ], ), ), ), // No routes found message (Step 4) if (_routeCount == 0 && !_isLoading && _error == null) Positioned( top: 150, left: 16, right: 16, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.orange[100], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.orange[300]!), ), child: Row( children: [ Icon( Icons.warning_outlined, color: Colors.orange[600], size: 24, ), const SizedBox(width: 12), const Expanded( child: Text( 'No routes available', style: TextStyle(fontSize: 14), ), ), IconButton( onPressed: _performSupabaseConnectionCheck, icon: Icon( Icons.refresh, color: Colors.orange[600], size: 20, ), ), ], ), ), ), // Route selector floating button (when no route selected) if (selectedRoute == null && !_appStateService.isLoadingRoutes && _routeCount > 0) Positioned( top: 80, right: 16, child: FloatingActionButton( mini: true, backgroundColor: const Color(0xFFFEE715), onPressed: _showRouteSelector, child: const Icon(Icons.route, color: Color(0xFF101820)), ), ), // Small refresh icon with updated refresh method if (selectedRoute != null) Positioned( top: 80, right: 16, child: FloatingActionButton( mini: true, backgroundColor: const Color(0xFFFEE715), onPressed: _performSupabaseConnectionCheck, // Updated to use connection check child: const Icon(Icons.refresh, color: Color(0xFF101820)), ), ), ], ), ), bottomNavigationBar: CustomBottomBar( currentIndex: 0, onTap: (int index) { // Handle navigation tap }, ), ); } }