import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; // Keep your specific imports import 'package:sefer_admin1/controller/functions/crud.dart'; import 'package:sefer_admin1/print.dart'; /// -------------------------------------------------------------------------- /// 1. DATA MODELS /// -------------------------------------------------------------------------- class DriverLocation { final double latitude; final double longitude; final double speed; final double heading; final String updatedAt; DriverLocation({ required this.latitude, required this.longitude, required this.speed, required this.heading, required this.updatedAt, }); factory DriverLocation.fromJson(Map json) { return DriverLocation( latitude: double.tryParse(json['latitude'].toString()) ?? 0.0, longitude: double.tryParse(json['longitude'].toString()) ?? 0.0, speed: double.tryParse(json['speed'].toString()) ?? 0.0, heading: double.tryParse(json['heading'].toString()) ?? 0.0, updatedAt: json['updated_at'] ?? '', ); } } /// -------------------------------------------------------------------------- /// 2. GETX CONTROLLER /// -------------------------------------------------------------------------- class RideMonitorController extends GetxController { // CONFIGURATION final String apiUrl = "https://api.intaleq.xyz/intaleq/Admin/rides/monitorRide.php"; // INPUT CONTROLLERS final TextEditingController phoneInputController = TextEditingController(); // OBSERVABLES var isTracking = false.obs; var isLoading = false.obs; var hasError = false.obs; var errorMessage = ''.obs; // Driver & Ride Data var driverLocation = Rxn(); var driverName = "Unknown Driver".obs; var rideStatus = "Waiting...".obs; // Route Data var startPoint = Rxn(); var endPoint = Rxn(); var routePolyline = [].obs; // List of points for the line // Map Variables final MapController mapController = MapController(); Timer? _timer; bool _isFirstLoad = true; // To trigger auto-fit bounds only on first success @override void onClose() { _timer?.cancel(); phoneInputController.dispose(); super.onClose(); } // --- ACTIONS --- void startSearch() { if (phoneInputController.text.trim().isEmpty) { Get.snackbar("Error", "Please enter a phone number"); return; } // Reset state hasError.value = false; errorMessage.value = ''; driverLocation.value = null; startPoint.value = null; endPoint.value = null; routePolyline.clear(); driverName.value = "Loading..."; rideStatus.value = "Loading..."; _isFirstLoad = true; // Switch UI isTracking.value = true; isLoading.value = true; // Start fetching fetchRideData(); // Start Polling _timer?.cancel(); _timer = Timer.periodic(const Duration(seconds: 10), (timer) { fetchRideData(); }); } void stopTracking() { _timer?.cancel(); isTracking.value = false; isLoading.value = false; phoneInputController.clear(); } Future fetchRideData() async { final phone = phoneInputController.text.trim(); if (phone.isEmpty) return; try { final response = await CRUD().post( link: apiUrl, payload: {"phone": phone}, ); // Log.print('response: ${response}'); if (response != 'failure') { final jsonResponse = response; if ((jsonResponse['message'] != null && jsonResponse['message'] != 'failure') || jsonResponse['status'] == 'success') { final data = jsonResponse['message'] ?? jsonResponse['data'] ?? jsonResponse; // 1. Parse Driver Info if (data['driver_details'] != null) { driverName.value = data['driver_details']['fullname'] ?? "Unknown"; } // 2. Parse Ride Info & Route if (data['ride_details'] != null) { rideStatus.value = data['ride_details']['status'] ?? "Unknown"; // Parse Start/End Locations (Format: "lat,lng") String? startStr = data['ride_details']['start_location']; String? endStr = data['ride_details']['end_location']; LatLng? s = _parseLatLngString(startStr); LatLng? e = _parseLatLngString(endStr); if (s != null && e != null) { startPoint.value = s; endPoint.value = e; routePolyline.value = [s, e]; // Straight line for now } } // 3. Parse Live Location final locData = data['driver_location']; if (locData is Map) { final newLocation = DriverLocation.fromJson(locData); driverLocation.value = newLocation; // 4. Update Camera Bounds _updateMapBounds(); } else { // Even if no live driver, we might want to show the route if (startPoint.value != null && endPoint.value != null) { _updateMapBounds(); } print("No live location coordinates."); } hasError.value = false; } else { hasError.value = true; errorMessage.value = jsonResponse['message'] ?? "Phone number not found or no active ride."; } } else { hasError.value = true; errorMessage.value = "Connection Failed"; } } catch (e) { print("Polling Error: $e"); if (isLoading.value) { hasError.value = true; errorMessage.value = e.toString(); } } finally { isLoading.value = false; } } // Helper to parse "lat,lng" string LatLng? _parseLatLngString(String? str) { if (str == null || !str.contains(',')) return null; try { final parts = str.split(','); final lat = double.parse(parts[0].trim()); final lng = double.parse(parts[1].trim()); return LatLng(lat, lng); } catch (e) { print("Error parsing location string '$str': $e"); return null; } } // Logic to fit start, end, and driver on screen void _updateMapBounds() { // Only auto-fit on the first successful load to avoid fighting user pan/zoom if (!_isFirstLoad) return; List pointsToFit = []; if (startPoint.value != null) pointsToFit.add(startPoint.value!); if (endPoint.value != null) pointsToFit.add(endPoint.value!); if (driverLocation.value != null) { pointsToFit.add(LatLng( driverLocation.value!.latitude, driverLocation.value!.longitude)); } if (pointsToFit.isNotEmpty) { try { final bounds = LatLngBounds.fromPoints(pointsToFit); mapController.fitCamera( CameraFit.bounds( bounds: bounds, padding: const EdgeInsets.all(80.0), // Padding so markers aren't on edge ), ); _isFirstLoad = false; // Disable auto-fit after initial success } catch (e) { print("Map Controller not ready yet: $e"); } } } } /// -------------------------------------------------------------------------- /// 3. UI SCREEN /// -------------------------------------------------------------------------- class RideMonitorScreen extends StatelessWidget { const RideMonitorScreen({super.key}); @override Widget build(BuildContext context) { final RideMonitorController controller = Get.put(RideMonitorController()); return Scaffold( appBar: AppBar( title: const Text("Admin Ride Monitor"), backgroundColor: Colors.blueAccent, foregroundColor: Colors.white, actions: [ Obx(() => controller.isTracking.value ? IconButton( icon: const Icon(Icons.close), onPressed: controller.stopTracking, tooltip: "Stop Tracking", ) : const SizedBox.shrink()), ], ), body: Obx(() { if (!controller.isTracking.value) { return _buildSearchForm(context, controller); } return _buildMapTrackingView(controller); }), ); } Widget _buildSearchForm( BuildContext context, RideMonitorController controller) { return Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.map_outlined, size: 80, color: Colors.blueAccent), const SizedBox(height: 20), const Text( "Track Active Ride", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), const Text( "Enter Driver or Passenger Phone Number", style: TextStyle(color: Colors.grey), ), const SizedBox(height: 30), TextField( controller: controller.phoneInputController, keyboardType: TextInputType.phone, decoration: InputDecoration( labelText: "Phone Number", hintText: "e.g. 9639...", border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), prefixIcon: const Icon(Icons.phone), ), ), const SizedBox(height: 20), SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: controller.startSearch, style: ElevatedButton.styleFrom( backgroundColor: Colors.blueAccent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), child: const Text("Start Monitoring", style: TextStyle(color: Colors.white, fontSize: 16)), ), ), ], ), ), ); } Widget _buildMapTrackingView(RideMonitorController controller) { return Stack( children: [ FlutterMap( mapController: controller.mapController, options: MapOptions( initialCenter: const LatLng(30.0444, 31.2357), initialZoom: 12.0, ), children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'com.sefer.admin', ), // 1. ROUTE LINE (Polyline) if (controller.routePolyline.isNotEmpty) PolylineLayer( polylines: [ Polyline( points: controller.routePolyline.value, strokeWidth: 5.0, color: Colors.blueAccent.withOpacity(0.8), borderStrokeWidth: 2.0, borderColor: Colors.blue[900]!, ), ], ), // 2. START & END MARKERS MarkerLayer( markers: [ // Start Point (Green Flag) if (controller.startPoint.value != null) Marker( point: controller.startPoint.value!, width: 40, height: 40, child: const Icon(Icons.flag, color: Colors.green, size: 40), alignment: Alignment.topCenter, ), // End Point (Red Flag) if (controller.endPoint.value != null) Marker( point: controller.endPoint.value!, width: 40, height: 40, child: const Icon(Icons.flag, color: Colors.red, size: 40), alignment: Alignment.topCenter, ), // Driver Car Marker if (controller.driverLocation.value != null) Marker( point: LatLng( controller.driverLocation.value!.latitude, controller.driverLocation.value!.longitude, ), width: 60, height: 60, child: Transform.rotate( angle: (controller.driverLocation.value!.heading * (3.14159 / 180)), child: Column( children: [ const Icon( Icons.directions_car_filled, color: Colors .black, // Dark car for visibility on blue line size: 35, ), Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 1), decoration: BoxDecoration( color: Colors.white.withOpacity(0.8), borderRadius: BorderRadius.circular(4), ), child: Text( "${controller.driverLocation.value!.speed.toInt()} km", style: const TextStyle( fontSize: 10, fontWeight: FontWeight.bold), ), ) ], ), ), ), ], ), ], ), // LOADING OVERLAY if (controller.isLoading.value && controller.driverLocation.value == null && controller.startPoint.value == null) Container( color: Colors.black45, child: const Center( child: CircularProgressIndicator(color: Colors.white)), ), // ERROR OVERLAY if (controller.hasError.value) Center( child: Container( margin: const EdgeInsets.all(20), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: const [ BoxShadow(blurRadius: 10, color: Colors.black26) ]), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, color: Colors.red, size: 40), const SizedBox(height: 10), Text(controller.errorMessage.value, textAlign: TextAlign.center), const SizedBox(height: 10), ElevatedButton( onPressed: controller.stopTracking, child: const Text("Back")) ], ), ), ), // INFO CARD if (!controller.hasError.value && !controller.isLoading.value) Positioned( bottom: 20, left: 20, right: 20, child: Card( elevation: 8, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15)), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const CircleAvatar( backgroundColor: Colors.blueAccent, child: Icon(Icons.person, color: Colors.white), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( controller.driverName.value, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), Row( children: [ Icon(Icons.circle, size: 10, color: controller.rideStatus.value == 'Begin' ? Colors.green : Colors.grey), const SizedBox(width: 5), Text(controller.rideStatus.value, style: const TextStyle( fontWeight: FontWeight.w600)), ], ), ], ), ), ], ), const Divider(), if (controller.driverLocation.value != null) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildInfoBadge(Icons.speed, "${controller.driverLocation.value!.speed.toStringAsFixed(1)} km/h"), _buildInfoBadge( Icons.access_time, controller.driverLocation.value!.updatedAt .split(' ') .last), ], ) else const Text("Connecting to driver...", style: TextStyle( color: Colors.orange, fontStyle: FontStyle.italic)), ], ), ), ), ), ], ); } Widget _buildInfoBadge(IconData icon, String text) { return Row( children: [ Icon(icon, size: 16, color: Colors.grey[600]), const SizedBox(width: 4), Text(text, style: TextStyle( color: Colors.grey[800], fontWeight: FontWeight.bold)), ], ); } }