import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:ui'; // للألوان import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/timezone.dart' as tz; import '../../constant/box_name.dart'; import '../../constant/links.dart'; import '../../main.dart'; // للوصول لـ box import '../../print.dart'; import '../../views/home/Captin/driver_map_page.dart'; import '../../views/home/Captin/orderCaptin/order_request_page.dart'; import '../functions/crud.dart'; import '../home/captin/home_captain_controller.dart'; class NotificationController extends GetxController { final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); // ============================================================================== // 1. تهيئة الإشعارات (إعداد القنوات والأزرار للآيفون والأندرويد) // ============================================================================== Future initNotifications() async { // إعدادات الأندرويد const AndroidInitializationSettings android = AndroidInitializationSettings('@mipmap/launcher_icon'); // إعدادات أزرار الآيفون (Categories) // هذا الجزء ضروري لظهور الأزرار في iOS final List darwinNotificationCategories = [ DarwinNotificationCategory( 'ORDER_CATEGORY', // المعرف المستخدم لربط الإشعار بالأزرار actions: [ DarwinNotificationAction.plain('ACCEPT_ORDER', '✅ قبول'), DarwinNotificationAction.plain('SHOW_DETAILS', '📄 تفاصيل'), DarwinNotificationAction.plain( 'REJECT_ORDER', '❌ رفض', options: { DarwinNotificationActionOption.destructive }, // يظهر باللون الأحمر ), ], ) ]; // إعدادات الآيفون العامة final DarwinInitializationSettings ios = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, notificationCategories: darwinNotificationCategories, // تسجيل الأزرار ); InitializationSettings initializationSettings = InitializationSettings(android: android, iOS: ios); tz.initializeTimeZones(); print('✅ Notifications initialized with Action Buttons Support'); await _flutterLocalNotificationsPlugin.initialize( initializationSettings, onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); // إنشاء قناة الأندرويد ذات الأهمية القصوى const AndroidNotificationChannel channel = AndroidNotificationChannel( 'high_importance_channel', 'High Importance Notifications', description: 'This channel is used for important notifications.', importance: Importance.max, // أقصى أهمية playSound: true, ); await _flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(channel); } // ============================================================================== // 2. دالة عرض الإشعار المطور (شكل واضح + أزرار + صوت مخصص) // ============================================================================== void showOrderNotification( String title, String body, String tone, String myListString) async { // أ) تنسيق النص والبيانات بشكل جميل String formattedBigText = body; String summaryText = 'طلب جديد'; String price = ''; try { List data = jsonDecode(myListString); // استخراج البيانات (تأكد أن الاندكسات مطابقة للباك إند عندك) price = _getVal(data, 26); String distance = _getVal(data, 5); String startLoc = _getVal(data, 29); String endLoc = _getVal(data, 30); String paxName = _getVal(data, 8); // String rating = _getVal(data, 33); // تنسيق النص ليكون 4 أسطر واضحة formattedBigText = "👤 $paxName\n" "💰 $price ${'SYP'.tr} | 🛣️ $distance كم\n" "🟢 من: $startLoc\n" "🏁 إلى: $endLoc"; summaryText = 'سعر الرحلة: $price'; } catch (e) { print("Error formatting notification text: $e"); } // ب) نمط النص الكبير (BigText) للأندرويد BigTextStyleInformation bigTextStyleInformation = BigTextStyleInformation( formattedBigText, contentTitle: '🚖 $title', summaryText: summaryText, htmlFormatContent: true, htmlFormatContentTitle: true, ); // ج) معالجة اسم الصوت (أندرويد بدون امتداد، آيفون مع امتداد) String soundNameAndroid = tone.contains('.') ? tone.split('.').first : tone; String soundNameIOS = tone.contains('.') ? tone : "$tone.wav"; // د) إعدادات الأندرويد (الأزرار + Full Screen) final androidDetails = AndroidNotificationDetails( 'high_importance_channel', 'High Importance Notifications', importance: Importance.max, priority: Priority.max, fullScreenIntent: true, // يفتح الشاشة وتظهر التفاصيل category: AndroidNotificationCategory.call, // يعامل كمكالمة (رنين مستمر) visibility: NotificationVisibility.public, ongoing: true, // يمنع الحذف بالسحب sound: RawResourceAndroidNotificationSound(soundNameAndroid), audioAttributesUsage: AudioAttributesUsage.alarm, // صوت عالٍ كالمنبه styleInformation: bigTextStyleInformation, color: const Color(0xFF1A252F), // الأزرار الثلاثة actions: [ const AndroidNotificationAction( 'ACCEPT_ORDER', '✅ قبول فوري', showsUserInterface: true, titleColor: Color(0xFF4CAF50), // أخضر ), const AndroidNotificationAction( 'SHOW_DETAILS', '📄 التفاصيل', showsUserInterface: true, titleColor: Color(0xFF2196F3), // أزرق ), const AndroidNotificationAction( 'REJECT_ORDER', '❌ رفض', showsUserInterface: false, // لا يفتح التطبيق cancelNotification: true, titleColor: Color(0xFFE53935), // أحمر ), ], ); // هـ) إعدادات الآيفون final iosDetails = DarwinNotificationDetails( sound: soundNameIOS, presentAlert: true, presentBadge: true, presentSound: true, categoryIdentifier: 'ORDER_CATEGORY', // ربط الأزرار interruptionLevel: InterruptionLevel.critical, // محاولة لكسر الصامت ); final details = NotificationDetails(android: androidDetails, iOS: iosDetails); // عرض الإشعار await _flutterLocalNotificationsPlugin.show( 1001, // ID ثابت لاستبدال الإشعار القديم title, "$price - مسافة $formattedBigText", // نص مختصر يظهر في البار العلوي details, payload: jsonEncode({ 'type': 'Order', 'data': myListString, }), ); } // ============================================================================== // 3. معالجة الاستجابة (عند الضغط على الأزرار) // ============================================================================== Future handleNotificationResponse(NotificationResponse response) async { final payload = response.payload; if (payload == null) return; final payloadData = jsonDecode(payload) as Map; final rawData = payloadData['data']; List listData = []; if (rawData is String) { listData = jsonDecode(rawData); } else if (rawData is List) { listData = rawData; } print("🔔 Notification Action: ${response.actionId}"); // أ) زر القبول if (response.actionId == 'ACCEPT_ORDER') { await _flutterLocalNotificationsPlugin.cancel(1001); // حذف الإشعار _processAcceptOrder(listData); } // ب) زر التفاصيل else if (response.actionId == 'SHOW_DETAILS') { // await _flutterLocalNotificationsPlugin.cancel(1001); // اختياري: حذف الإشعار Get.to(() => OrderRequestPage(), arguments: {'myListString': rawData}); } // ج) زر الرفض else if (response.actionId == 'REJECT_ORDER') { await _flutterLocalNotificationsPlugin.cancel(1001); // حذف الإشعار _processRejectOrder(listData); } // د) الضغط على الإشعار نفسه (بدون أزرار) else { Get.to(() => OrderRequestPage(), arguments: {'myListString': rawData}); } } // ============================================================================== // 4. منطق القبول الآمن (Safe Accept Logic) // ============================================================================== Future _processAcceptOrder(List data) async { // إظهار Loading Get.dialog( WillPopScope( onWillPop: () async => false, child: const Center( child: CircularProgressIndicator(color: Colors.white), ), ), barrierDismissible: false, ); try { final driverId = box.read(BoxName.driverID); String orderId = _getVal(data, 16); String passengerToken = _getVal(data, 9); print("🚀 Sending Accept Request for Order: $orderId"); var res = await CRUD().post( link: "${AppLink.ride}/rides/acceptRide.php", payload: { 'id': orderId, 'rideTimeStart': DateTime.now().toString(), 'status': 'Apply', 'passengerToken': passengerToken, 'driver_id': driverId, }, ); print("📥 Server Response: $res"); if (Get.isDialogOpen == true) Get.back(); // إغلاق اللودينج // 🔴 فحص النتيجة بدقة (Map أو String) bool isFailure = false; if (res is Map && res['status'] == 'failure') { isFailure = true; } else if (res == 'failure') { isFailure = true; } if (isFailure) { Get.defaultDialog( title: "تنبيه", middleText: "عذراً، الطلب أخذه سائق آخر.", confirmTextColor: Colors.white, onConfirm: () => Get.back(), textConfirm: "حسناً", ); return; // توقف هنا ولا تكمل } // ✅ نجاح -> تجهيز الانتقال // حماية من الكراش: التأكد من وجود HomeCaptainController if (!Get.isRegistered()) { print("♻️ Reviving HomeCaptainController..."); Get.put(HomeCaptainController()); } else { Get.find().changeRideId(); } box.write(BoxName.statusDriverLocation, 'on'); box.write(BoxName.rideStatus, 'Apply'); var rideArgs = _buildRideArgs(data); box.write(BoxName.rideArguments, rideArgs); // استخدام offAll لمنع الرجوع لصفحة الطلب Get.offAll(() => PassengerLocationMapPage(), arguments: rideArgs); } catch (e) { if (Get.isDialogOpen == true) Get.back(); print("❌ Error in accept process: $e"); Get.snackbar("خطأ", "حدث خطأ غير متوقع"); } } // ============================================================================== // 5. منطق الرفض (يعمل في الخلفية بدون فتح صفحات) // ============================================================================== Future _processRejectOrder(List data) async { try { final driverId = box.read(BoxName.driverID); String orderId = _getVal(data, 16); if (driverId != null && orderId.isNotEmpty) { print("📤 Rejecting Order: $orderId"); await CRUD().post(link: AppLink.addDriverOrder, payload: { 'driver_id': driverId, 'order_id': orderId, 'status': 'Refused' }); print("✅ Order Rejected Successfully"); } } catch (e) { print("❌ Error rejecting order: $e"); } } // ============================================================================== // 6. دوال مساعدة (Helpers) // ============================================================================== Map _buildRideArgs(List data) { return { 'passengerLocation': '${_getVal(data, 0)},${_getVal(data, 1)}', 'passengerDestination': '${_getVal(data, 3)},${_getVal(data, 4)}', 'Duration': _getVal(data, 4), // انتبه: تأكد من الإندكس الصحيح للوقت 'totalCost': _getVal(data, 26), 'Distance': _getVal(data, 5), 'name': _getVal(data, 8), 'phone': _getVal(data, 10), 'email': _getVal(data, 28), 'WalletChecked': _getVal(data, 13), 'tokenPassenger': _getVal(data, 9), 'direction': 'https://www.google.com/maps/dir/${_getVal(data, 0)}/${_getVal(data, 1)}/', 'DurationToPassenger': _getVal(data, 15), 'rideId': _getVal(data, 16), 'passengerId': _getVal(data, 7), 'driverId': _getVal(data, 18), 'durationOfRideValue': _getVal(data, 19), 'paymentAmount': _getVal(data, 2), 'paymentMethod': _getVal(data, 13) == 'true' ? 'visa' : 'cash', 'isHaveSteps': _getVal(data, 20), 'step0': _getVal(data, 21), 'step1': _getVal(data, 22), 'step2': _getVal(data, 23), 'step3': _getVal(data, 24), 'step4': _getVal(data, 25), 'passengerWalletBurc': _getVal(data, 26), 'timeOfOrder': DateTime.now().toString(), 'totalPassenger': _getVal(data, 2), 'carType': _getVal(data, 31), 'kazan': _getVal(data, 32), 'startNameLocation': _getVal(data, 29), 'endNameLocation': _getVal(data, 30), }; } String _getVal(List data, int index) { if (data.length > index && data[index] != null) { return data[index].toString(); } return ''; } // Callbacks void onDidReceiveNotificationResponse(NotificationResponse response) { handleNotificationResponse(response); } void onDidReceiveBackgroundNotificationResponse( NotificationResponse response) { handleNotificationResponse(response); } // ============================================================================== // 7. الدوال القديمة (Old Scheduled Notifications) - لم يتم تغييرها // ============================================================================== void showNotification( String title, String message, String tone, String payLoad) async { // هذه الدالة القديمة للإشعارات البسيطة (ليس الطلبات) BigTextStyleInformation bigTextStyleInformation = BigTextStyleInformation( message, contentTitle: title.tr, htmlFormatContent: true, htmlFormatContentTitle: true, ); AndroidNotificationDetails android = AndroidNotificationDetails( 'high_importance_channel', 'High Importance Notifications', importance: Importance.max, priority: Priority.high, sound: RawResourceAndroidNotificationSound(tone.split('.').first), ); DarwinNotificationDetails ios = const DarwinNotificationDetails( sound: 'default', presentAlert: true, presentBadge: true, presentSound: true, ); NotificationDetails details = NotificationDetails(android: android, iOS: ios); await _flutterLocalNotificationsPlugin.show(0, title, message, details, payload: jsonEncode({'title': title, 'data': payLoad})); } void scheduleNotificationsForSevenDays( String title, String message, String tone) async { final AndroidNotificationDetails android = AndroidNotificationDetails( 'high_importance_channel', 'High Importance Notifications', importance: Importance.max, priority: Priority.high, sound: RawResourceAndroidNotificationSound(tone.split('.').first), ); const DarwinNotificationDetails ios = DarwinNotificationDetails( sound: 'default', presentAlert: true, presentBadge: true, presentSound: true, ); final NotificationDetails details = NotificationDetails(android: android, iOS: ios); if (Platform.isAndroid) { if (await Permission.scheduleExactAlarm.isDenied) { await Permission.scheduleExactAlarm.request(); } } for (int day = 0; day < 7; day++) { final notificationTimes = [ {'hour': 8, 'minute': 0, 'id': day * 1000 + 1}, {'hour': 15, 'minute': 0, 'id': day * 1000 + 2}, {'hour': 20, 'minute': 0, 'id': day * 1000 + 3}, ]; for (var time in notificationTimes) { final notificationId = time['id'] as int; bool isScheduled = box.read('notification_$notificationId') ?? false; if (!isScheduled) { await _scheduleNotificationForTime( day, time['hour'] as int, time['minute'] as int, title, message, details, notificationId, ); box.write('notification_$notificationId', true); } } } } Future _scheduleNotificationForTime( int dayOffset, int hour, int minute, String title, String message, NotificationDetails details, int notificationId, ) async { tz.initializeTimeZones(); var cairoLocation = tz.getLocation('Africa/Cairo'); // تأكد من المنطقة الزمنية final now = tz.TZDateTime.now(cairoLocation); tz.TZDateTime scheduledDate = tz.TZDateTime( cairoLocation, now.year, now.month, now.day + dayOffset, hour, minute, ); if (scheduledDate.isBefore(now)) { scheduledDate = scheduledDate.add(const Duration(days: 1)); } await _flutterLocalNotificationsPlugin.zonedSchedule( notificationId, title, message, scheduledDate, details, androidScheduleMode: AndroidScheduleMode.exact, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, matchDateTimeComponents: null, ); } }