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:url_launcher/url_launcher.dart'; // ضروري للاتصال import '../../../controller/functions/crud.dart'; import '../../../constant/box_name.dart'; // لتحديد هوية المستخدم الحالي import '../../../main.dart'; // للوصول لـ box // ========================================== // 1. MODEL // ========================================== class RideDashboardModel { final String rideId; final String status; final String startLocation; final String endLocation; final String date; final String time; final String price; final String distance; final String driverId; final String driverName; final String driverPhone; final String driverCompletedCount; final String driverCanceledCount; final String passengerName; final String passengerPhone; final String passengerCompletedCount; final String cancelReason; RideDashboardModel({ required this.rideId, required this.status, required this.startLocation, required this.endLocation, required this.date, required this.time, required this.price, required this.distance, required this.driverId, required this.driverName, required this.driverPhone, required this.driverCompletedCount, required this.driverCanceledCount, required this.passengerName, required this.passengerPhone, required this.passengerCompletedCount, required this.cancelReason, }); factory RideDashboardModel.fromJson(Map json) { return RideDashboardModel( rideId: json['id'].toString(), status: json['status'] ?? '', startLocation: json['start_location'] ?? '', endLocation: json['end_location'] ?? '', date: json['date'] ?? '', time: json['time'] ?? '', price: json['price']?.toString() ?? '0', distance: json['distance']?.toString() ?? '0', driverId: json['driver_id'].toString(), driverName: json['driver_full_name'] ?? 'غير معروف', driverPhone: json['d_phone'] ?? '', driverCompletedCount: (json['d_completed'] ?? 0).toString(), driverCanceledCount: (json['d_canceled'] ?? 0).toString(), passengerName: json['passenger_full_name'] ?? 'غير معروف', passengerPhone: json['p_phone'] ?? '', passengerCompletedCount: (json['p_completed'] ?? 0).toString(), cancelReason: json['cancel_reason'] ?? '', ); } LatLng? getStartLatLng() { try { var parts = startLocation.split(','); return LatLng(double.parse(parts[0]), double.parse(parts[1])); } catch (e) { return null; } } LatLng? getEndLatLng() { try { var parts = endLocation.split(','); return LatLng(double.parse(parts[0]), double.parse(parts[1])); } catch (e) { return null; } } } // ========================================== // 2. CONTROLLER // ========================================== class RidesListController extends GetxController { var isLoading = false.obs; var allRidesList = []; var displayedRides = [].obs; TextEditingController searchController = TextEditingController(); String currentStatus = 'Begin'; // === التحقق من صلاحية الأدمن === // نقرأ رقم الهاتف الحالي المخزن في التطبيق String myPhone = box.read(BoxName.adminPhone)?.toString() ?? ''; bool get isSuperAdmin { // ضع هنا أرقام هواتف الأدمن المسموح لهم برؤية الأرقام والاتصال return myPhone == '963942542053' || myPhone == '963992952235'; } final String apiUrl = "https://api.intaleq.xyz/intaleq/Admin/rides/get_rides_by_status.php"; @override void onInit() { super.onInit(); fetchRides(); } void changeTab(String status) { currentStatus = status; searchController.clear(); fetchRides(); } void filterRides(String query) { if (query.isEmpty) { displayedRides.value = allRidesList; } else { displayedRides.value = allRidesList.where((ride) { return ride.driverPhone.contains(query) || ride.passengerPhone.contains(query) || ride.driverName.toLowerCase().contains(query.toLowerCase()) || ride.passengerName.toLowerCase().contains(query.toLowerCase()) || ride.rideId.contains(query); }).toList(); } } Future fetchRides() async { isLoading.value = true; allRidesList.clear(); displayedRides.clear(); try { var response = await CRUD().post(link: apiUrl, payload: {"status": currentStatus}); if (response != 'failure' && response['status'] == 'success') { List data = []; if (response['message'] is List) data = response['message']; else if (response['data'] is List) data = response['data']; allRidesList = data.map((e) => RideDashboardModel.fromJson(e)).toList(); displayedRides.value = allRidesList; } } catch (e) { print("Error fetching rides: $e"); } finally { isLoading.value = false; } } } // ========================================== // 3. MAIN DASHBOARD SCREEN // ========================================== class RidesDashboardScreen extends StatelessWidget { const RidesDashboardScreen({super.key}); @override Widget build(BuildContext context) { final controller = Get.put(RidesListController()); return DefaultTabController( length: 4, child: Scaffold( appBar: AppBar( title: const Text("مراقبة الرحلات"), bottom: TabBar( isScrollable: true, onTap: (index) { List statuses = ['Begin', 'New', 'Completed', 'Canceled']; controller.changeTab(statuses[index]); }, tabs: const [ Tab(text: "جارية", icon: Icon(Icons.directions_car)), Tab(text: "جديدة", icon: Icon(Icons.new_releases)), Tab(text: "مكتملة", icon: Icon(Icons.check_circle)), Tab(text: "ملغاة", icon: Icon(Icons.cancel)), ], ), ), body: Column( children: [ Padding( padding: const EdgeInsets.all(10.0), child: TextField( controller: controller.searchController, onChanged: (val) => controller.filterRides(val), decoration: InputDecoration( hintText: "بحث...", prefixIcon: const Icon(Icons.search), filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), ), ), ), Expanded( child: Obx(() { if (controller.isLoading.value) return const Center(child: CircularProgressIndicator()); if (controller.displayedRides.isEmpty) return const Center(child: Text("لا توجد رحلات")); return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), itemCount: controller.displayedRides.length, itemBuilder: (context, index) { final ride = controller.displayedRides[index]; return _buildRideCard( ride, controller.isSuperAdmin); // نمرر صلاحية الأدمن }, ); }), ), ], ), ), ); } Widget _buildRideCard(RideDashboardModel ride, bool isAdmin) { Color statusColor = _getStatusColor(ride.status); String statusText = _getStatusText(ride.status); return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () => Get.to(() => RideMapMonitorScreen( ride: ride, isAdmin: isAdmin)), // نمرر الصلاحية للخريطة أيضاً child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("رحلة #${ride.rideId}", style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16)), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: statusColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: statusColor)), child: Text(statusText, style: TextStyle( color: statusColor, fontWeight: FontWeight.bold, fontSize: 12)), ), ], ), const SizedBox(height: 10), // Stats Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade200)), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _statItem( Icons.attach_money, "السعر", "${double.tryParse(ride.price)?.toStringAsFixed(0) ?? 0}", Colors.green), Container( width: 1, height: 25, color: Colors.grey.shade300), _statItem( Icons.social_distance, "المسافة", "${double.tryParse(ride.distance)?.toStringAsFixed(1) ?? 0} كم", Colors.blue), Container( width: 1, height: 25, color: Colors.grey.shade300), _statItem( Icons.access_time, "الوقت", ride.time.length > 5 ? ride.time.substring(0, 5) : ride.time, Colors.orange), ], ), ), const SizedBox(height: 12), _locationRow(Icons.my_location, ride.startLocation, Colors.blue), const SizedBox(height: 6), _locationRow(Icons.location_on, ride.endLocation, Colors.red), const Divider(height: 20), // === معلومات السائق والراكب مع ميزة إخفاء الرقم === Row( children: [ Expanded( child: _userInfo( title: "الكابتن", name: ride.driverName, phone: ride.driverPhone, isAdmin: isAdmin, completed: ride.driverCompletedCount, canceled: ride.driverCanceledCount)), Container(width: 1, height: 40, color: Colors.grey.shade300), const SizedBox(width: 10), Expanded( child: _userInfo( title: "الراكب", name: ride.passengerName, phone: ride.passengerPhone, isAdmin: isAdmin, completed: ride.passengerCompletedCount)), ], ), if ((ride.status.contains('Cancel') || ride.status == 'TimeOut') && ride.cancelReason.isNotEmpty && ride.cancelReason != 'لا يوجد سبب') ...[ const SizedBox(height: 10), Container( width: double.infinity, padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade200)), child: Text("السبب: ${ride.cancelReason}", style: TextStyle(color: Colors.red.shade900, fontSize: 13)), ) ] ], ), ), ), ); } // === ويدجت عرض المعلومات مع منطق الإخفاء === Widget _userInfo( {required String title, required String name, required String phone, required bool isAdmin, String? completed, String? canceled}) { // 1. منطق الإخفاء (Masking) String displayPhone = phone; if (!isAdmin && phone.length > 4) { // إظهار آخر 4 أرقام فقط displayPhone = phone.substring(phone.length - 4).padLeft(phone.length, '*'); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontSize: 10, color: Colors.grey)), Text(name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), // 2. رقم الهاتف وزر الاتصال Row( children: [ Text(displayPhone, style: const TextStyle( fontSize: 11, color: Colors.grey, letterSpacing: 1)), if (isAdmin && phone.isNotEmpty) ...[ const SizedBox(width: 4), InkWell( onTap: () => _makePhoneCall(phone), child: const Icon(Icons.call, size: 14, color: Colors.green), ) ] ], ), if (completed != null) Text("تم: $completed ${canceled != null ? '| ألغى: $canceled' : ''}", style: const TextStyle(fontSize: 9, color: Colors.black54)), ], ); } Future _makePhoneCall(String phoneNumber) async { // التحقق مما إذا كانت العلامة موجودة مسبقاً لتجنب التكرار (++963) String formattedPhone = phoneNumber; if (!formattedPhone.startsWith('+')) { formattedPhone = '+$formattedPhone'; } final Uri launchUri = Uri(scheme: 'tel', path: formattedPhone); if (await canLaunchUrl(launchUri)) { await launchUrl(launchUri); } else { // يمكنك هنا إضافة تنبيه بسيط في حال فشل فتح التطبيق debugPrint("لا يمكن الاتصال بالرقم: $formattedPhone"); } } // Helpers Color _getStatusColor(String s) { if (s == 'Begin' || s == 'Arrived') return Colors.green; if (s == 'Finished') return Colors.teal; if (s.contains('Cancel') || s == 'TimeOut') return Colors.red; if (s == 'New') return Colors.blue; return Colors.grey; } String _getStatusText(String s) { if (s == 'Begin' || s == 'Arrived') return "جارية 🟢"; if (s == 'Finished') return "مكتملة ✅"; if (s == 'CancelFromDriver' || s == 'CancelFromDriverAfterApply') return "ألغاها السائق 👨‍✈️"; if (s == 'CancelFromPassenger') return "ألغاها الراكب 👤"; if (s == 'TimeOut') return "انتهى الوقت ⏱️"; if (s == 'New') return "جديدة 🆕"; return "ملغاة ❌"; } Widget _statItem(IconData icon, String label, String value, Color color) { return Column(children: [ Icon(icon, size: 18, color: color), Text(value, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)) ]); } Widget _locationRow(IconData icon, String text, Color color) { return Row(children: [ Icon(icon, size: 16, color: color), const SizedBox(width: 8), Expanded( child: Text(text, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13))) ]); } } // ========================================== // 4. MAP MONITOR SCREEN // ========================================== class RideMapMonitorScreen extends StatefulWidget { final RideDashboardModel ride; final bool isAdmin; // نستقبل الصلاحية هنا أيضاً const RideMapMonitorScreen( {super.key, required this.ride, required this.isAdmin}); @override State createState() => _RideMapMonitorScreenState(); } class _RideMapMonitorScreenState extends State { final MapController mapController = MapController(); LatLng? startPos, endPos, driverPos; Timer? _timer; bool isFirstLoad = true; @override void initState() { super.initState(); startPos = widget.ride.getStartLatLng(); endPos = widget.ride.getEndLatLng(); if (widget.ride.status == 'Begin' || widget.ride.status == 'Arrived') { fetchDriverLocation(); _timer = Timer.periodic( const Duration(seconds: 10), (_) => fetchDriverLocation()); } WidgetsBinding.instance.addPostFrameCallback((_) => _fitBounds()); } @override void dispose() { _timer?.cancel(); mapController.dispose(); super.dispose(); } void _fitBounds() { List points = []; if (startPos != null) points.add(startPos!); if (endPos != null) points.add(endPos!); if (driverPos != null) points.add(driverPos!); if (points.isNotEmpty) { try { mapController.fitCamera(CameraFit.bounds( bounds: LatLngBounds.fromPoints(points), padding: const EdgeInsets.all(50))); } catch (e) {} } } Future fetchDriverLocation() async { String trackUrl = "https://api.intaleq.xyz/intaleq/Admin/rides/get_driver_live_pos.php"; try { var response = await CRUD() .post(link: trackUrl, payload: {"driver_id": widget.ride.driverId}); if (response != 'failure') { var d = response['message']; setState(() { driverPos = LatLng(double.parse(d['latitude'].toString()), double.parse(d['longitude'].toString())); }); if (isFirstLoad) { _fitBounds(); isFirstLoad = false; } } } catch (e) {} } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("تتبع الرحلة #${widget.ride.rideId}"), backgroundColor: Colors.white, foregroundColor: Colors.black, elevation: 1), body: Stack( children: [ FlutterMap( mapController: mapController, options: MapOptions( initialCenter: startPos ?? const LatLng(33.513, 36.276), initialZoom: 13), children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'), if (startPos != null && endPos != null) PolylineLayer(polylines: [ Polyline( points: [startPos!, endPos!], strokeWidth: 4, color: Colors.blue.withOpacity(0.7)) ]), MarkerLayer(markers: [ if (startPos != null) Marker( point: startPos!, width: 40, height: 40, child: const Icon(Icons.flag, color: Colors.green, size: 40), alignment: Alignment.topCenter), if (endPos != null) Marker( point: endPos!, width: 40, height: 40, child: const Icon(Icons.location_on, color: Colors.red, size: 40), alignment: Alignment.topCenter), if (driverPos != null) Marker( point: driverPos!, width: 50, height: 50, child: Container( decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow(blurRadius: 5, color: Colors.black26) ]), child: const Icon(Icons.directions_car, color: Colors.blue, size: 30))), ]), ], ), Positioned( bottom: 20, left: 15, right: 15, child: Card( elevation: 5, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15)), child: Padding( padding: const EdgeInsets.all(15.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("السعر: ${widget.ride.price}", style: const TextStyle( fontWeight: FontWeight.bold, color: Colors.green)), Text("المسافة: ${widget.ride.distance} كم") ]), const Divider(), _mapInfo(Icons.person, "الكابتن: ${widget.ride.driverName}", widget.ride.driverPhone), const SizedBox(height: 5), _mapInfo( Icons.person_outline, "الراكب: ${widget.ride.passengerName}", widget.ride.passengerPhone), const SizedBox(height: 5), _simpleInfo( Icons.my_location, "من: ${widget.ride.startLocation}"), _simpleInfo( Icons.location_on, "إلى: ${widget.ride.endLocation}"), ], ), ), ), ) ], ), floatingActionButton: FloatingActionButton( mini: true, child: const Icon(Icons.center_focus_strong), onPressed: _fitBounds), ); } // ويدجت خاصة بالخريطة تطبق نفس منطق الإخفاء Widget _mapInfo(IconData icon, String text, String phone) { String displayPhone = phone; if (!widget.isAdmin && phone.length > 4) { displayPhone = phone.substring(phone.length - 4).padLeft(phone.length, '*'); } return Row( children: [ Icon(icon, size: 18, color: Colors.grey[700]), const SizedBox(width: 8), Expanded( child: Text("$text ($displayPhone)", maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14))), if (widget.isAdmin && phone.isNotEmpty) InkWell( onTap: () async { final Uri launchUri = Uri(scheme: 'tel', path: phone); if (await canLaunchUrl(launchUri)) await launchUrl(launchUri); }, child: const Icon(Icons.call, size: 18, color: Colors.green), ) ], ); } Widget _simpleInfo(IconData icon, String text) { return Row(children: [ Icon(icon, size: 18, color: Colors.grey[700]), const SizedBox(width: 8), Expanded( child: Text(text, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14))) ]); } }