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'; import 'package:siro_admin/constant/links.dart'; // Keep your specific imports import 'package:siro_admin/controller/functions/crud.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 /// -------------------------------------------------------------------------- /// تطبيع رقم الهاتف تلقائياً حسب الدولة /// مثال: 0992952235 ← 963992952235 (سوريا) /// مثال: 079XXXXXXX ← 96279XXXXXXX (أردن) /// مثال: 010XXXXXXXX ← 2010XXXXXXXX (مصر) String normalizePhone(String input) { final clean = input.replaceAll(RegExp(r'\D+'), ''); // Syria: 099XXXXXXX or 9639XXXXXXX if (clean.length == 10 && clean.startsWith('09')) return '963${clean.substring(1)}'; if (clean.length == 12 && clean.startsWith('963')) return clean; if (clean.length == 9 && clean.startsWith('9')) return '963$clean'; // Jordan: 079XXXXXXX or 9627XXXXXXX if (clean.length == 10 && clean.startsWith('07')) return '962${clean.substring(1)}'; if (clean.length == 12 && clean.startsWith('962')) return clean; if (clean.length == 9 && clean.startsWith('7')) return '962$clean'; // Egypt: 010XXXXXXXX or 2010XXXXXXXX if (clean.length == 11 && clean.startsWith('01')) return '20${clean.substring(1)}'; if (clean.length == 13 && clean.startsWith('20')) return clean; return clean; } class RideMonitorController extends GetxController { // CONFIGURATION final String apiUrl = "${AppLink.server}/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( "تنبيه", "يرجى إدخال رقم الهاتف أولاً", backgroundColor: Colors.redAccent.withOpacity(0.9), colorText: Colors.white, snackPosition: SnackPosition.TOP, margin: const EdgeInsets.all(15), borderRadius: 15, ); return; } // Reset state hasError.value = false; errorMessage.value = ''; driverLocation.value = null; startPoint.value = null; endPoint.value = null; routePolyline.clear(); driverName.value = "جاري التحميل..."; rideStatus.value = "جاري التحميل..."; _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 { // تطبيع رقم الهاتف تلقائياً حسب الدولة String normalizedPhone = normalizePhone(phone); final response = await CRUD().post( link: apiUrl, payload: {"phone": normalizedPhone}, ); 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'] ?? "سائق غير معروف"; } // 2. Parse Ride Info & Route if (data['ride_details'] != null) { rideStatus.value = data['ride_details']['status'] ?? "غير معروف"; // 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(); } } hasError.value = false; } else { hasError.value = true; errorMessage.value = jsonResponse['message'] ?? "لم يتم العثور على رقم الهاتف أو لا توجد رحلة نشطة."; } } else { hasError.value = true; errorMessage.value = "فشل الاتصال بالخادم"; } } catch (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) { return null; } } // Logic to fit start, end, and driver on screen void _updateMapBounds() { 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), ), ); _isFirstLoad = false; } catch (e) { // Map Controller not ready yet } } } } /// -------------------------------------------------------------------------- /// 3. UI SCREEN (Modern Light Theme) /// -------------------------------------------------------------------------- class RideMonitorScreen extends StatelessWidget { const RideMonitorScreen({super.key}); // 🎨 الألوان العصرية (Modern Palette) final Color backgroundColor = const Color(0xFFF4F7FE); final Color primaryColor = const Color(0xFF4318FF); final Color textPrimary = const Color(0xFF2B3674); final Color textSecondary = const Color(0xFFA3AED0); @override Widget build(BuildContext context) { final RideMonitorController controller = Get.put(RideMonitorController()); return Scaffold( backgroundColor: backgroundColor, // الإبقاء على AppBar فقط في شاشة البحث appBar: PreferredSize( preferredSize: const Size.fromHeight(kToolbarHeight), child: Obx(() { if (controller.isTracking.value) return const SizedBox .shrink(); // إخفاء الـ AppBar في وضع التتبع للخريطة الكاملة return AppBar( backgroundColor: Colors.transparent, elevation: 0, centerTitle: true, iconTheme: IconThemeData(color: textPrimary), title: Text( "مراقبة الرحلات", style: TextStyle( color: textPrimary, fontWeight: FontWeight.bold, fontSize: 18, ), ), ); }), ), body: Obx(() { if (!controller.isTracking.value) { return _buildSearchForm(context, controller); } return _buildMapTrackingView(context, controller); }), ); } // --------------------------------------------------------------------------- // واجهة البحث (Search View) // --------------------------------------------------------------------------- Widget _buildSearchForm( BuildContext context, RideMonitorController controller) { return Center( child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Container( padding: const EdgeInsets.all(32.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(30), boxShadow: [ BoxShadow( color: primaryColor.withOpacity(0.08), blurRadius: 24, offset: const Offset(0, 10), ) ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: primaryColor.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon(Icons.radar_rounded, size: 60, color: primaryColor), ), const SizedBox(height: 24), Text( "تتبع رحلة نشطة", style: TextStyle( color: textPrimary, fontSize: 22, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text( "أدخل رقم هاتف السائق أو الراكب للبدء", style: TextStyle( color: textSecondary, fontSize: 14, ), textAlign: TextAlign.center, ), const SizedBox(height: 32), Container( decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.white, width: 2), ), child: TextField( controller: controller.phoneInputController, keyboardType: TextInputType.phone, textDirection: TextDirection.ltr, style: TextStyle( color: textPrimary, fontSize: 16, fontWeight: FontWeight.w600, ), decoration: InputDecoration( hintText: "مثال: 0992952235...", hintStyle: TextStyle(color: textSecondary), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( vertical: 18, horizontal: 20), prefixIcon: Icon(Icons.phone_rounded, color: primaryColor), ), ), ), const SizedBox(height: 32), SizedBox( width: double.infinity, height: 56, child: ElevatedButton( onPressed: controller.startSearch, style: ElevatedButton.styleFrom( backgroundColor: primaryColor, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), ), child: const Text( "بدء المراقبة", style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), ], ), ), ), ), ); } // --------------------------------------------------------------------------- // واجهة الخريطة (Map View) // --------------------------------------------------------------------------- Widget _buildMapTrackingView( BuildContext context, 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: 6.0, color: primaryColor.withOpacity(0.9), borderStrokeWidth: 2.0, borderColor: primaryColor.withOpacity(0.3), strokeCap: StrokeCap.round, strokeJoin: StrokeJoin.round, ), ], ), // 2. START & END MARKERS MarkerLayer( markers: [ // Start Point (Green Dot) if (controller.startPoint.value != null) Marker( point: controller.startPoint.value!, width: 30, height: 30, child: _buildPointMarker(const Color(0xFF10B981)), ), // End Point (Red Dot) if (controller.endPoint.value != null) Marker( point: controller.endPoint.value!, width: 30, height: 30, child: _buildPointMarker(const Color(0xFFEF4444)), ), // Driver Car Marker if (controller.driverLocation.value != null) Marker( point: LatLng( controller.driverLocation.value!.latitude, controller.driverLocation.value!.longitude, ), width: 80, height: 80, child: Transform.rotate( angle: (controller.driverLocation.value!.heading * (3.14159 / 180)), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 10, spreadRadius: 2, ) ], ), child: Icon( Icons.directions_car_rounded, color: primaryColor, size: 28, ), ), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: textPrimary, borderRadius: BorderRadius.circular(6), ), child: Text( "${controller.driverLocation.value!.speed.toInt()} كم", style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), textDirection: TextDirection.rtl, ), ) ], ), ), ), ], ), ], ), // زر التراجع (إيقاف التتبع) أعلى الشاشة Positioned( top: MediaQuery.of(context).padding.top + 10, right: 20, // أو left حسب لغة التطبيق child: Container( decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 4), ) ], ), child: IconButton( icon: Icon(Icons.close_rounded, color: textPrimary, size: 24), onPressed: controller.stopTracking, tooltip: "إيقاف المراقبة", ), ), ), // LOADING OVERLAY (Smooth Frosted Glass like) if (controller.isLoading.value && controller.driverLocation.value == null && controller.startPoint.value == null) Container( color: Colors.white.withOpacity(0.8), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator( color: primaryColor, strokeWidth: 3), const SizedBox(height: 16), Text( "جاري تحديد الموقع...", style: TextStyle( color: textPrimary, fontWeight: FontWeight.bold, fontSize: 16, ), ) ], ), ), ), // ERROR OVERLAY if (controller.hasError.value) Center( child: Container( margin: const EdgeInsets.all(24), padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10), ) ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.red.withOpacity(0.1), shape: BoxShape.circle, ), child: const Icon(Icons.error_outline_rounded, color: Colors.red, size: 40), ), const SizedBox(height: 16), Text( "حدث خطأ", style: TextStyle( color: textPrimary, fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text( controller.errorMessage.value, textAlign: TextAlign.center, style: TextStyle(color: textSecondary, height: 1.5), ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: controller.stopTracking, style: ElevatedButton.styleFrom( backgroundColor: backgroundColor, foregroundColor: textPrimary, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric(vertical: 14), ), child: const Text("رجوع للبحث", style: TextStyle(fontWeight: FontWeight.bold)), ), ) ], ), ), ), // INFO CARD (Bottom Floating Card) if (!controller.hasError.value && !controller.isLoading.value) Positioned( bottom: 30, left: 20, right: 20, child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 24, offset: const Offset(0, 10), ) ], ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Container( width: 50, height: 50, decoration: BoxDecoration( color: primaryColor.withOpacity(0.1), borderRadius: BorderRadius.circular(15), ), child: Icon(Icons.person_rounded, color: primaryColor, size: 28), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( controller.driverName.value, style: TextStyle( color: textPrimary, fontSize: 18, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration( shape: BoxShape.circle, color: controller.rideStatus.value .toLowerCase() == 'begin' ? const Color(0xFF10B981) : const Color(0xFFF59E0B), ), ), const SizedBox(width: 6), Text( controller.rideStatus.value, style: TextStyle( color: textSecondary, fontSize: 13, fontWeight: FontWeight.w600, ), ), ], ), ], ), ), ], ), const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Divider(height: 1, thickness: 1), ), if (controller.driverLocation.value != null) Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildModernInfoBadge( Icons.speed_rounded, "${controller.driverLocation.value!.speed.toStringAsFixed(1)} كم/س", const Color(0xFF3B82F6), ), Container( width: 1, height: 30, color: Colors.grey.withOpacity(0.2)), _buildModernInfoBadge( Icons.access_time_rounded, controller.driverLocation.value!.updatedAt .split(' ') .last, const Color(0xFF8B5CF6), ), ], ) else Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 16, height: 16, child: CircularProgressIndicator( color: primaryColor, strokeWidth: 2), ), const SizedBox(width: 10), Text( "جاري الاتصال بالسائق...", style: TextStyle( color: primaryColor, fontWeight: FontWeight.w600, fontSize: 13, ), ), ], ), ], ), ), ), ), ], ); } // --- Helper Widgets --- Widget _buildPointMarker(Color color) { return Container( decoration: BoxDecoration( color: color.withOpacity(0.3), shape: BoxShape.circle, ), child: Center( child: Container( width: 12, height: 12, decoration: BoxDecoration( color: color, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ BoxShadow( color: color.withOpacity(0.5), blurRadius: 6, spreadRadius: 1, ) ], ), ), ), ); } Widget _buildModernInfoBadge(IconData icon, String text, Color iconColor) { return Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: iconColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, size: 16, color: iconColor), ), const SizedBox(width: 8), Text( text, style: TextStyle( color: textPrimary, fontSize: 14, fontWeight: FontWeight.bold, ), textDirection: TextDirection.ltr, // للحفاظ على اتجاه الأرقام ), ], ); } }