528 lines
19 KiB
Dart
Executable File
528 lines
19 KiB
Dart
Executable File
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<void> initNotifications() async {
|
|
// إعدادات الأندرويد
|
|
const AndroidInitializationSettings android =
|
|
AndroidInitializationSettings('@mipmap/launcher_icon');
|
|
|
|
// إعدادات أزرار الآيفون (Categories)
|
|
// هذا الجزء ضروري لظهور الأزرار في iOS
|
|
final List<DarwinNotificationCategory> 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<dynamic> 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: <AndroidNotificationAction>[
|
|
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<void> handleNotificationResponse(NotificationResponse response) async {
|
|
final payload = response.payload;
|
|
if (payload == null) return;
|
|
|
|
final payloadData = jsonDecode(payload) as Map<String, dynamic>;
|
|
final rawData = payloadData['data'];
|
|
|
|
List<dynamic> 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<void> _processAcceptOrder(List<dynamic> 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<HomeCaptainController>()) {
|
|
print("♻️ Reviving HomeCaptainController...");
|
|
Get.put(HomeCaptainController());
|
|
} else {
|
|
Get.find<HomeCaptainController>().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<void> _processRejectOrder(List<dynamic> 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<String, dynamic> _buildRideArgs(List<dynamic> 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<dynamic> 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<void> _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,
|
|
);
|
|
}
|
|
}
|