Files
intaleq_driver/lib/controller/firebase/local_notification.dart
Hamza-Ayed 12c5ce31a2 26-1-22/1
2026-01-22 19:30:31 +03:00

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,
);
}
}