26-1-20/1

This commit is contained in:
Hamza-Ayed
2026-01-20 10:11:10 +03:00
parent 374f9e9bf3
commit 3c0ae4cf2f
53 changed files with 89652 additions and 6861 deletions

View File

@@ -190,7 +190,7 @@ class LoginDriverController extends GetxController {
// ✅ بعد التأكد أن كل المفاتيح موجودة
await EncryptionHelper.initialize();
await AppInitializer().getKey();
// await AppInitializer().getKey();
} else {}
} else {
await EncryptionHelper.initialize();
@@ -215,7 +215,7 @@ class LoginDriverController extends GetxController {
final jwt = decodedResponse1['jwt'];
await box.write(BoxName.jwt, c(jwt));
await AppInitializer().getKey();
// await AppInitializer().getKey();
}
}
}

View File

@@ -72,7 +72,26 @@ class RegistrationController extends GetxController {
final carVinController = TextEditingController(); // Chassis number
final carRegistrationExpiryController = TextEditingController();
DateTime? carRegistrationExpiryDate;
// داخل RegistrationController
// المتغيرات لتخزين القيم المختارة (لإرسالها للـ API لاحقاً)
int? selectedVehicleCategoryId; // سيخزن 1 أو 2 أو 3
int? selectedFuelTypeId; // سيخزن 1 أو 2 أو 3 أو 4
// قائمة أنواع المركبات (مطابقة لقاعدة البيانات)
final List<Map<String, dynamic>> vehicleCategoryOptions = [
{'id': 1, 'name': 'Car'.tr}, // ترجمة: سيارة
{'id': 2, 'name': 'Motorcycle'.tr}, // ترجمة: دراجة نارية
{'id': 3, 'name': 'Van / Bus'.tr}, // ترجمة: فان / باص
];
// قائمة أنواع الوقود
final List<Map<String, dynamic>> fuelTypeOptions = [
{'id': 1, 'name': 'Petrol'.tr}, // ترجمة: بنزين
{'id': 2, 'name': 'Diesel'.tr}, // ترجمة: ديزل
{'id': 3, 'name': 'Electric'.tr}, // ترجمة: كهربائي
{'id': 4, 'name': 'Hybrid'.tr}, // ترجمة: هايبرد
];
// STEP 3: Document Uploads
File? driverLicenseFrontImage;
File? driverLicenseBackImage;
@@ -464,25 +483,30 @@ class RegistrationController extends GetxController {
Future<void> submitRegistration() async {
// 0) دوال/مساعدات محلية
void _addField(Map<String, String> fields, String key, String? value) {
if (value != null && value.isNotEmpty) {
fields[key] = value;
}
}
// 1) تحقق من وجود الروابط بدل الملفات
// 1) تحقق من وجود الروابط
final driverFrontUrl = docUrls['driver_license_front'];
final driverBackUrl = docUrls['driver_license_back'];
final carFrontUrl = docUrls['car_license_front'];
final carBackUrl = docUrls['car_license_back'];
isLoading.value = true;
update();
final registerUri = Uri.parse(AppLink.register_driver_and_car);
final client = http.Client();
try {
// ترويسات مشتركة
final bearer =
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
final hmac = '${box.read(BoxName.hmac)}';
// 2) جهّز طلب التسجيل الرئيسي: حقول فقط + روابط الصور (لا نرفع صور إطلاقًا)
final req = http.MultipartRequest('POST', registerUri);
req.headers.addAll({
'Authorization': bearer,
@@ -499,11 +523,10 @@ class RegistrationController extends GetxController {
_addField(fields, 'national_number', nationalIdController.text);
_addField(fields, 'birthdate', bithdateController.text);
_addField(fields, 'expiry_date', driverLicenseExpiryController.text);
_addField(
fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك
_addField(fields, 'password', 'generated_password_or_token');
_addField(fields, 'status', 'yet');
_addField(fields, 'email', 'Not specified');
_addField(fields, 'gender', 'Male');
_addField(fields, 'gender', 'Male'); // يفضل ربطها بـ Dropdown أيضاً
// --- Car Data ---
_addField(fields, 'vin', 'yet');
@@ -511,22 +534,53 @@ class RegistrationController extends GetxController {
_addField(fields, 'make', carMakeController.text);
_addField(fields, 'model', carModelController.text);
_addField(fields, 'year', carYearController.text);
_addField(fields, 'expiration_date', driverLicenseExpiryController.text);
_addField(
fields,
'expiration_date',
driverLicenseExpiryController
.text); // تأكد من أن هذا تاريخ انتهاء السيارة وليس الرخصة
_addField(fields, 'color', carColorController.text);
_addField(fields, 'fuel', 'Gasoline');
if (colorHex != null && colorHex!.isNotEmpty) {
_addField(fields, 'color_hex', colorHex!);
}
_addField(fields, 'owner',
'${firstNameController.text} ${lastNameController.text}');
// --- روابط الصور المخزنة مسبقًا ---
// ============================================================
// 🔥 التعديل الجديد: إرسال الأرقام (IDs) لتصنيف المركبة والوقود
// ============================================================
// 1. إرسال رقم تصنيف المركبة (1=سيارة, 2=دراجة...)
if (selectedVehicleCategoryId != null) {
_addField(fields, 'vehicle_category_id',
selectedVehicleCategoryId.toString());
} else {
_addField(fields, 'vehicle_category_id', '1'); // قيمة افتراضية (سيارة)
}
// 2. إرسال رقم ونوع الوقود
if (selectedFuelTypeId != null) {
// إرسال الرقم (للبحث السريع)
_addField(fields, 'fuel_type_id', selectedFuelTypeId.toString());
// إرسال الاسم نصاً (للتوافق مع العمود القديم 'fuel' إذا لزم الأمر)
// نبحث عن الاسم داخل القائمة بناءً على الرقم المختار
final fuelObj = fuelTypeOptions.firstWhere(
(e) => e['id'] == selectedFuelTypeId,
orElse: () => {'name': 'Petrol'});
_addField(fields, 'fuel', fuelObj['name'].toString());
} else {
_addField(fields, 'fuel_type_id', '1');
_addField(fields, 'fuel', 'Petrol');
}
// --- روابط الصور ---
_addField(fields, 'driver_license_front', driverFrontUrl!);
_addField(fields, 'driver_license_back', driverBackUrl!);
_addField(fields, 'car_license_front', carFrontUrl!);
_addField(fields, 'car_license_back', carBackUrl!);
// أضف الحقول
req.fields.addAll(fields);
// 3) الإرسال
@@ -534,80 +588,53 @@ class RegistrationController extends GetxController {
await client.send(req).timeout(const Duration(seconds: 60));
final resp = await http.Response.fromStream(streamed);
// 4) فحص النتيجة
// 4) معالجة الاستجابة
Map<String, dynamic>? json;
try {
json = jsonDecode(resp.body) as Map<String, dynamic>;
} catch (_) {}
if (resp.statusCode == 200 && json?['status'] == 'success') {
// final driverID =
// (json!['data']?['driverID'] ?? json['driverID'])?.toString();
// if (driverID != null && driverID.isNotEmpty) {
// box.write(BoxName.driverID, driverID);
// }
Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr,
backgroundColor: Colors.green, colorText: Colors.white);
Get.snackbar(
'Success'.tr,
'Registration completed successfully!'.tr,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
// متابعة تسجيل الدخول إن لزم
// منطق التوكن والإشعارات وتسجيل الدخول...
final email = box.read(BoxName.emailDriver);
final driverID = box.read(BoxName.driverID);
final c = Get.isRegistered<LoginDriverController>()
? Get.find<LoginDriverController>()
: Get.put(LoginDriverController());
//token to server
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
await CRUD().post(link: AppLink.addTokensDriver, payload: {
'captain_id': (box.read(BoxName.driverID)).toString(),
'token': (box.read(BoxName.tokenDriver)).toString(),
'fingerPrint': fingerPrint.toString(),
});
// CRUD().post(link: AppLink.addTokensDriverWallet, payload: {
// 'token': box.read(BoxName.tokenDriver).toString(),
// 'fingerPrint': fingerPrint.toString(),
// 'captain_id': box.read(BoxName.driverID).toString(),
// });
NotificationService.sendNotification(
target: 'service', // الإرسال لجميع المشتركين في "service"
title: 'طلب خدمة جديد',
body: 'تم استلام طلب خدمة جديد. الرجاء مراجعة التفاصيل.',
target: 'service',
title: 'New Driver Registration',
body: 'Driver $driverID has submitted registration.',
isTopic: true,
category: 'new_service_request', // فئة توضح نوع الإشعار
category: 'new_service_request',
);
final c = Get.isRegistered<LoginDriverController>()
? Get.find<LoginDriverController>()
: Get.put(LoginDriverController());
c.loginWithGoogleCredential(driverID, email);
} else {
final msg =
(json?['message'] ?? 'Registration failed. Please try again.')
.toString();
Log.print('msg: $msg');
Get.snackbar(
'Error'.tr,
msg,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
final msg = (json?['message'] ?? 'Registration failed.').toString();
Get.snackbar('Error'.tr, msg,
backgroundColor: Colors.red, colorText: Colors.white);
}
} catch (e) {
Get.snackbar(
'Error'.tr,
'${'An unexpected error occurred:'.tr} $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
Get.snackbar('Error'.tr, 'Error: $e',
backgroundColor: Colors.red, colorText: Colors.white);
} finally {
client.close();
isLoading.value = false;
update();
}
} // Future<void> submitRegistration() async {
}
// // 1) تحقق من الصور
// if (driverLicenseFrontImage == null ||
// driverLicenseBackImage == null ||

View File

@@ -20,6 +20,7 @@ import '../../views/home/Captin/orderCaptin/order_request_page.dart';
import '../../views/home/Captin/orderCaptin/vip_order_page.dart';
import '../auth/google_sign.dart';
import '../functions/face_detect.dart';
import '../home/captin/map_driver_controller.dart';
import 'local_notification.dart';
class FirebaseMessagesController extends GetxController {
@@ -80,7 +81,7 @@ class FirebaseMessagesController extends GetxController {
AndroidNotification? android = notification?.android;
// if (notification != null && android != null) {
if (message.data.isNotEmpty && message.notification != null) {
if (message.data.isNotEmpty) {
fireBaseTitles(message);
}
// if (message.data.isNotEmpty && message.notification != null) {
@@ -90,7 +91,7 @@ class FirebaseMessagesController extends GetxController {
FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {});
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
if (message.data.isNotEmpty && message.notification != null) {
if (message.data.isNotEmpty) {
fireBaseTitles(message);
}
});
@@ -110,7 +111,7 @@ class FirebaseMessagesController extends GetxController {
case 'ORDER':
case 'Order': // Handle both cases for backward compatibility
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'tone1', '');
notificationController.showNotification(title, body, 'order', '');
}
var myListString = message.data['DriverList'];
if (myListString != null) {
@@ -118,7 +119,7 @@ class FirebaseMessagesController extends GetxController {
driverToken = myList[14].toString();
Get.put(HomeCaptainController()).changeRideId();
update();
Get.to(() => OrderRequestPage(), arguments: {
Get.toNamed('/OrderRequestPage', arguments: {
'myListString': myListString,
'DriverList': myList,
'body': body
@@ -147,7 +148,18 @@ class FirebaseMessagesController extends GetxController {
notificationController.showNotification(
title, 'Passenger Cancel Trip'.tr, 'cancel', '');
}
cancelTripDialog();
Log.print("🔔 FCM: Ride Cancelled by Passenger received.");
// 1. استخراج السبب (أرسلناه من PHP باسم 'reason')
String reason = message.data['reason'] ?? 'No reason provided';
// 2. توجيه الأمر للكنترولر
if (Get.isRegistered<MapDriverController>()) {
// استدعاء الحارس (سيتجاهل الأمر إذا كان السوكيت قد سبقه)
Get.find<MapDriverController>()
.processRideCancelledByPassenger(reason, source: "FCM");
}
break;
case 'VIP Order Accepted':

View File

@@ -1,11 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/views/home/Captin/orderCaptin/order_request_page.dart';
import 'package:sefer_driver/views/home/Captin/orderCaptin/order_speed_request.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
@@ -74,38 +71,6 @@ class NotificationController extends GetxController {
sound: RawResourceAndroidNotificationSound(tone),
);
// AndroidNotificationDetails android = AndroidNotificationDetails(
// 'high_importance_channel', // Use the same ID as before
// 'High Importance Notifications',
// importance: Importance.high,
// priority: Priority.high,
// styleInformation: bigTextStyleInformation,
// playSound: true,
// sound: RawResourceAndroidNotificationSound(tone),
// // audioAttributesUsage: AudioAttributesUsage.alarm,
// visibility: NotificationVisibility.public,
// autoCancel: false,
// color: AppColor.primaryColor,
// showProgress: true,
// showWhen: true,
// ongoing: true,
// enableVibration: true,
// vibrationPattern: Int64List.fromList([0, 1000, 500, 1000]),
// timeoutAfter: 14500,
// setAsGroupSummary: true,
// subText: message, fullScreenIntent: true,
// actions: [
// AndroidNotificationAction(
// allowGeneratedReplies: true,
// 'id',
// title.tr,
// titleColor: AppColor.blueColor,
// showsUserInterface: true,
// )
// ],
// category: AndroidNotificationCategory.message,
// );
DarwinNotificationDetails ios = const DarwinNotificationDetails(
sound: 'default',
presentAlert: true,

View File

@@ -1,41 +1,44 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../print.dart';
import 'package:get/get.dart'; // للترجمة .tr
class NotificationService {
// تأكد من أن هذا هو الرابط الصحيح لملف الإرسال
static const String _serverUrl =
'https://syria.intaleq.xyz/intaleq/fcm/send_fcm.php';
'https://api.intaleq.xyz/intaleq/ride/firebase/send_fcm.php';
static Future<void> sendNotification({
required String target,
required String title,
required String body,
required String? category, // <-- [الإضافة الأولى]
required String category, // إلزامي للتصنيف
String? tone,
List<String>? driverList,
bool isTopic = false,
}) async {
try {
final Map<String, dynamic> payload = {
// 1. تجهيز البيانات المخصصة (Data Payload)
Map<String, dynamic> customData = {};
customData['category'] = category;
// إذا كان هناك قائمة سائقين/ركاب، نضعها هنا
if (driverList != null && driverList.isNotEmpty) {
// نرسلها كـ JSON String لأن FCM v1 يدعم String Values فقط في الـ data
customData['driverList'] = jsonEncode(driverList);
}
// 2. تجهيز الطلب الرئيسي للسيرفر
final Map<String, dynamic> requestPayload = {
'target': target,
'title': title,
'body': body,
'isTopic': isTopic,
'data':
customData, // 🔥🔥 التغيير الجوهري: وضعنا البيانات داخل "data" 🔥🔥
};
if (category != null) {
payload['category'] = category; // <-- [الإضافة الثانية]
}
if (tone != null) {
payload['tone'] = tone;
}
if (driverList != null) {
// [مهم] تطبيق السائق يرسل passengerList
payload['passengerList'] = jsonEncode(driverList);
requestPayload['tone'] = tone;
}
final response = await http.post(
@@ -43,17 +46,18 @@ class NotificationService {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(payload),
body: jsonEncode(requestPayload),
);
if (response.statusCode == 200) {
Log.print('✅ Notification sent successfully.');
print('✅ Notification sent successfully.');
// print('Response: ${response.body}');
} else {
Log.print(
'❌ Failed to send notification. Status code: ${response.statusCode}');
print('❌ Failed to send notification. Code: ${response.statusCode}');
print('Error Body: ${response.body}');
}
} catch (e) {
Log.print('An error occurred while sending notification: $e');
print('Error sending notification: $e');
}
}
}

View File

@@ -0,0 +1,164 @@
import 'dart:async';
import 'dart:ui';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay;
import 'package:get_storage/get_storage.dart';
import '../../constant/box_name.dart';
const String notificationChannelId = 'driver_service_channel';
const int notificationId = 888;
const String notificationIcon = '@mipmap/launcher_icon';
@pragma('vm:entry-point')
Future<bool> onStart(ServiceInstance service) async {
DartPluginRegistrant.ensureInitialized();
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
await GetStorage.init();
final box = GetStorage();
IO.Socket? socket;
String driverId = box.read(BoxName.driverID) ?? '';
String token = box.read(BoxName.tokenDriver) ?? '';
if (driverId.isNotEmpty) {
socket = IO.io(
'https://location.intaleq.xyz',
IO.OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.setQuery({'driver_id': driverId, 'token': token})
.setReconnectionAttempts(double.infinity)
.build());
socket.connect();
socket.onConnect((_) {
print("✅ Background Service: Socket Connected!");
if (service is AndroidServiceInstance) {
flutterLocalNotificationsPlugin.show(
notificationId,
'أنت متصل الآن',
'بانتظار الطلبات...',
const NotificationDetails(
android: AndroidNotificationDetails(
notificationChannelId,
'خدمة السائق',
icon: notificationIcon,
ongoing: true,
importance: Importance.low,
priority: Priority.low,
),
),
);
}
});
socket.on('new_ride_request', (data) async {
print("🔔 Background Service: Received new_ride_request");
// 🔥 قراءة حالة التطبيق مباشرة قبل العرض
await GetStorage.init(); // تأكد من تحديث البيانات
final box = GetStorage();
bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false;
// 🔥 Check إضافي: هل الـ Overlay مفتوح بالفعل؟
bool overlayActive = await Overlay.FlutterOverlayWindow.isActive();
if (isAppInForeground || overlayActive) {
print("🛑 App is FOREGROUND or Overlay already shown. Skipping.");
return;
}
// عرض الـ Overlay
print("🚀 App is BACKGROUND. Showing Overlay...");
try {
await Overlay.FlutterOverlayWindow.showOverlay(
enableDrag: true,
overlayTitle: "طلب جديد",
overlayContent: "لديك طلب جديد وصل للتو!",
flag: OverlayFlag.focusPointer,
positionGravity: PositionGravity.auto,
height: WindowSize.matchParent,
width: WindowSize.matchParent,
startPosition: const OverlayPosition(0, -30),
);
await Overlay.FlutterOverlayWindow.shareData(data);
} catch (e) {
print("Overlay Error: $e");
}
});
}
service.on('stopService').listen((event) {
socket?.disconnect();
service.stopSelf();
});
Timer.periodic(const Duration(seconds: 30), (timer) async {
if (service is AndroidServiceInstance) {
if (await service.isForegroundService()) {
flutterLocalNotificationsPlugin.show(
notificationId,
'خدمة السائق نشطة',
'بانتظار الطلبات...',
const NotificationDetails(
android: AndroidNotificationDetails(
notificationChannelId,
'خدمة السائق',
icon: notificationIcon,
ongoing: true,
importance: Importance.low,
priority: Priority.low,
),
),
);
}
}
});
return true;
}
class BackgroundServiceHelper {
static Future<void> initialize() async {
final service = FlutterBackgroundService();
await service.configure(
androidConfiguration: AndroidConfiguration(
onStart: onStart,
autoStart: false,
isForegroundMode: true,
notificationChannelId: notificationChannelId,
initialNotificationTitle: 'تطبيق السائق',
initialNotificationContent: 'تجهيز الخدمة...',
foregroundServiceNotificationId: notificationId,
),
iosConfiguration: IosConfiguration(
autoStart: false,
onForeground: onStart,
onBackground: onStart,
),
);
}
static Future<void> startService() async {
final service = FlutterBackgroundService();
if (!await service.isRunning()) {
await service.startService();
}
}
static Future<void> stopService() async {
final service = FlutterBackgroundService();
service.invoke("stopService");
}
}

View File

@@ -77,74 +77,157 @@ class CRUD {
/// Centralized private method to handle all API requests.
/// Includes retry logic, network checking, and standardized error handling.
// --- تعديل 1: دالة _makeRequest محسنة للإنترنت الضعيف ---
Future<dynamic> _makeRequest({
required String link,
Map<String, dynamic>? payload,
required Map<String, String> headers,
}) async {
// timeouts أقصر
const connectTimeout = Duration(seconds: 6);
const receiveTimeout = Duration(seconds: 10);
// 🟢 زيادة الوقت للسماح بالشبكات البطيئة (سوريا)
const connectTimeout = Duration(seconds: 20); // رفعنا الوقت من 6 لـ 20
const receiveTimeout = Duration(seconds: 40); // رفعنا الوقت من 10 لـ 40
Future<http.Response> doPost() {
final url = Uri.parse(link);
// استخدم _client بدل http.post
return _client
// نستخدم _client إذا كان معرفاً، أو ننشئ واحداً جديداً مع إغلاقه لاحقاً
// لضمان عدم حدوث مشاكل، سنستخدم http.post المباشر كما في النسخة المستقرة لديك
// ولكن مع timeout أطول
return http
.post(url, body: payload, headers: headers)
.timeout(connectTimeout + receiveTimeout);
}
http.Response response;
try {
// retry ذكي: محاولة واحدة إضافية فقط لأخطاء شبكة/5xx
try {
response = await doPost();
} on SocketException catch (_) {
// محاولة ثانية واحدة فقط
response = await doPost();
} on TimeoutException catch (_) {
response = await doPost();
}
http.Response response = http.Response('', 500); // Default initialization
final sc = response.statusCode;
// 🟢 محاولة إعادة الاتصال (Retry) حتى 3 مرات
int attempts = 0;
while (attempts < 3) {
try {
attempts++;
response = await doPost();
// إذا نجح الاتصال، نخرج من الحلقة ونعالج الرد
break;
} on SocketException catch (_) {
if (attempts >= 3) {
_netGuard.notifyOnce((title, msg) => mySnackeBarError(msg));
return 'no_internet';
}
// انتظار بسيط قبل المحاولة التالية (مهم جداً للشبكات المتقطعة)
await Future.delayed(const Duration(seconds: 1));
} on TimeoutException catch (_) {
if (attempts >= 3) return 'failure';
// لا ننتظر هنا، نعيد المحاولة فوراً
} catch (e) {
// إذا كان الخطأ هو errno = 9 (Bad file descriptor) نعيد المحاولة
if (e.toString().contains('errno = 9') && attempts < 3) {
await Future.delayed(const Duration(milliseconds: 500));
continue;
}
// أخطاء أخرى لا يمكن تجاوزها
addError(
'HTTP Exception: $e', 'Try: $attempts', 'CRUD._makeRequest $link');
return 'failure';
}
}
// --- معالجة الرد (كما هي في كودك) ---
// ملاحظة: المتغير response هنا قد يكون غير معرف (null) إذا فشلت كل المحاولات
// لكن بسبب الـ return داخل الـ catch، لن نصل هنا إلا بوجود response
// الحل الآمن لضمان وجود response قبل استخدامه:
try {
// إعادة تعريف response لضمان عدم حدوث خطأ null safety في المحرر
// (في المنطق الفعلي لن نصل هنا إلا ومعنا response)
if (attempts > 3) return 'failure';
final sc = response.statusCode; // استخدمنا ! لأننا متأكدين
final body = response.body;
// 2xx
if (sc >= 200 && sc < 300) {
try {
final jsonData = jsonDecode(body);
return jsonData; // لا تعيد 'success' فقط؛ أعِد الجسم كله
return jsonData;
} catch (e, st) {
// لا تسجّل كخطأ شبكي لكل حالة؛ فقط معلومات
addError('JSON Decode Error', 'Body: $body\n$st',
'CRUD._makeRequest $link');
return 'failure';
}
}
// 401 → دع الطبقة العليا تتعامل مع التجديد
if (sc == 401) {
// لا تستدع getJWT هنا كي لا نضاعف الرحلات
return 'token_expired';
}
// 5xx: لا تعِد المحاولة هنا (حاولنا مرة ثانية فوق)
if (sc >= 500) {
addError(
'Server 5xx', 'SC: $sc\nBody: $body', 'CRUD._makeRequest $link');
return 'failure';
}
// 4xx أخرى: أعد الخطأ بدون تسجيل مكرر
return 'failure';
} catch (e) {
return 'failure';
}
}
// --- تعديل 2: دالة get (كما طلبت: بوست + إرجاع النص الخام) ---
// أبقيتها كما هي في كودك الأصلي تماماً، فقط حسنت الـ Timeout
Future<dynamic> get({
required String link,
Map<String, dynamic>? payload,
}) async {
try {
var url = Uri.parse(link);
// 🟢 إضافة timeout هنا أيضاً
var response = await http.post(
url,
body: payload,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
'Authorization':
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}'
},
).timeout(const Duration(seconds: 40)); // وقت كافٍ للشبكات الضعيفة
Log.print('response: ${response.body}');
Log.print('response: ${response.request}');
if (response.statusCode == 200) {
// المنطق الخاص بك: إرجاع الـ body كاملاً كنص (String)
// لأنك تريد عمل jsonDecode لاحقاً في المكان الذي استدعى الدالة
// أو التحقق من status: success داخلياً
// ملاحظة: في كودك الأصلي كنت تفحص jsonDecode هنا وتعود بـ response.body
// سأبقيها كما هي:
var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'success') {
return response.body; // إرجاع النص الخام
}
return jsonData['status'];
} else if (response.statusCode == 401) {
var jsonData = jsonDecode(response.body);
if (jsonData['error'] == 'Token expired') {
await Get.put(LoginDriverController()).getJWT();
return 'token_expired';
} else {
// addError('Unauthorized: ${jsonData['error']}', 'crud().get - 401',
// url.toString());
return 'failure';
}
} else {
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
url.toString());
return 'failure';
}
} on TimeoutException {
// معالجة صامتة للتايم أوت في الـ GET
return 'failure';
} on SocketException {
_netGuard.notifyOnce((title, msg) => mySnackeBarError(msg));
// معالجة صامتة لانقطاع النت
return 'no_internet';
} on TimeoutException {
return 'failure';
} catch (e, st) {
addError('HTTP Request Exception: $e', 'Stack: $st',
'CRUD._makeRequest $link');
} catch (e) {
addError('GET Exception: $e', '', link);
return 'failure';
}
}
@@ -175,51 +258,6 @@ class CRUD {
/// Performs a standard authenticated GET request (using POST method as per original code).
/// Automatically handles token renewal.
Future<dynamic> get({
required String link,
Map<String, dynamic>? payload,
}) async {
var url = Uri.parse(
link,
);
var response = await http.post(
url,
body: payload,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
'Authorization':
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}'
},
);
if (response.statusCode == 200) {
var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'success') {
return response.body;
}
return jsonData['status'];
} else if (response.statusCode == 401) {
// Specifically handle 401 Unauthorized
var jsonData = jsonDecode(response.body);
if (jsonData['error'] == 'Token expired') {
// Show snackbar prompting to re-login
await Get.put(LoginDriverController()).getJWT();
// mySnackbarSuccess('please order now'.tr);
return 'token_expired'; // Return a specific value for token expiration
} else {
// Other 401 errors
addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401',
url.toString());
return 'failure';
}
} else {
addError('Non-200 response code: ${response.statusCode}',
'crud().post - Other', url.toString());
return 'failure';
}
}
/// Performs an authenticated POST request to wallet endpoints.
Future<dynamic> postWallet({

View File

@@ -8,31 +8,38 @@ void showInBrowser(String url) async {
}
Future<void> makePhoneCall(String phoneNumber) async {
// 1. تنظيف الرقم (إزالة المسافات والفواصل)
// 1. Clean the number
String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), '');
// 2. التحقق من طول الرقم لتحديد طريقة التنسيق
// 2. Format logic (Syria/International)
if (formattedNumber.length > 6) {
// --- التعديل المطلوب ---
if (formattedNumber.startsWith('09')) {
// إذا كان يبدأ بـ 09 (رقم موبايل سوري محلي)
// نحذف أول خانة (الصفر) ونضيف +963
formattedNumber = '+963${formattedNumber.substring(1)}';
} else if (!formattedNumber.startsWith('+')) {
// إذا لم يكن يبدأ بـ + (ولم يكن يبدأ بـ 09)، نضيف + في البداية
// هذا للحفاظ على منطقك القديم للأرقام الدولية الأخرى
formattedNumber = '+$formattedNumber';
}
}
// 3. التنفيذ (Launch)
// 3. Create URI
final Uri launchUri = Uri(
scheme: 'tel',
path: formattedNumber,
);
if (await canLaunchUrl(launchUri)) {
await launchUrl(launchUri);
// 4. Execute with externalApplication mode
try {
// Attempt to launch directly without checking canLaunchUrl first
// (Sometimes canLaunchUrl returns false on some devices even if it works)
if (!await launchUrl(launchUri, mode: LaunchMode.externalApplication)) {
throw 'Could not launch $launchUri';
}
} catch (e) {
// Fallback: Try checking canLaunchUrl if the direct launch fails
if (await canLaunchUrl(launchUri)) {
await launchUrl(launchUri);
} else {
print("Cannot launch url: $launchUri");
}
}
}

View File

@@ -1,54 +1,74 @@
// import 'dart:async';
// import 'package:background_location/background_location.dart';
// import 'package:get/get.dart';
// import 'package:permission_handler/permission_handler.dart';
import 'dart:io';
// class LocationBackgroundController extends GetxController {
// @override
// void onInit() {
// super.onInit();
// requestLocationPermission();
// configureBackgroundLocation();
// }
import 'package:device_info_plus/device_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
// Future<void> requestLocationPermission() async {
// var status = await Permission.locationAlways.status;
// if (!status.isGranted) {
// await Permission.locationAlways.request();
// }
// }
import 'background_service.dart';
// Future<void> configureBackgroundLocation() async {
// await BackgroundLocation.setAndroidNotification(
// title: 'Location Tracking Active'.tr,
// message: 'Your location is being tracked in the background.'.tr,
// icon: '@mipmap/launcher_icon',
// );
Future<void> requestNotificationPermission() async {
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt >= 33) {
// Android 13+
final status = await Permission.notification.request();
if (!status.isGranted) {
print('⚠️ إذن الإشعارات مرفوض');
return;
}
}
}
// BackgroundLocation.setAndroidConfiguration(3000);
// BackgroundLocation.startLocationService();
// BackgroundLocation.getLocationUpdates((location) {
// // Handle location updates here
// });
// }
// بعد الحصول على الإذن، ابدأ الخدمة
await BackgroundServiceHelper.startService();
}
// startBackLocation() async {
// Timer.periodic(const Duration(seconds: 3), (timer) {
// getBackgroundLocation();
// });
// }
class PermissionsHelper {
/// طلب إذن الإشعارات على Android 13+
static Future<bool> requestNotificationPermission() async {
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
// getBackgroundLocation() async {
// var status = await Permission.locationAlways.status;
// if (status.isGranted) {
// await BackgroundLocation.startLocationService(
// distanceFilter: 20, forceAndroidLocationManager: true);
// BackgroundLocation.setAndroidConfiguration(
// Duration.microsecondsPerSecond); // Set interval to 5 seconds
// Android 13+ (API 33+) يحتاج إذن POST_NOTIFICATIONS
if (androidInfo.version.sdkInt >= 33) {
final status = await Permission.notification.request();
// BackgroundLocation.getLocationUpdates((location1) {});
// } else {
// await Permission.locationAlways.request();
// }
// }
// }
if (status.isDenied) {
print('⚠️ إذن الإشعارات مرفوض');
return false;
}
if (status.isPermanentlyDenied) {
print('⚠️ إذن الإشعارات مرفوض بشكل دائم - افتح الإعدادات');
await openAppSettings();
return false;
}
}
}
return true;
}
/// طلب جميع الإذونات المطلوبة
static Future<bool> requestAllPermissions() async {
// إذن الإشعارات أولاً
bool notificationGranted = await requestNotificationPermission();
if (!notificationGranted) return false;
// إذن الموقع
final locationStatus = await Permission.location.request();
if (!locationStatus.isGranted) {
print('⚠️ إذن الموقع مرفوض');
return false;
}
// إذن الموقع في الخلفية
if (Platform.isAndroid) {
final bgLocationStatus = await Permission.locationAlways.request();
if (!bgLocationStatus.isGranted) {
print('⚠️ إذن الموقع في الخلفية مرفوض');
}
}
return true;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +1,93 @@
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/main.dart';
class TextToSpeechController extends GetxController {
final flutterTts = FlutterTts();
bool isComplete = false;
// Initialize TTS in initState
final FlutterTts flutterTts = FlutterTts();
bool isSpeaking = false;
@override
void onInit() {
super.onInit();
initTts();
}
// Dispose of TTS when controller is closed
@override
void onClose() {
flutterTts.stop();
super.onClose();
flutterTts.completionHandler;
}
// Function to initialize TTS engine
// --- 1. تهيئة المحرك بإعدادات قوية للملاحة ---
Future<void> initTts() async {
String? lang =
WidgetsBinding.instance.platformDispatcher.locale.countryCode;
await flutterTts
.setLanguage(box.read(BoxName.lang).toString()); //'en-US' Set language
// await flutterTts.setLanguage('ar-SA'); //'en-US' Set language
// await flutterTts.setLanguage(lang!); //'en-US' Set language
await flutterTts.setSpeechRate(0.5); // Adjust speech rate
await flutterTts.setVolume(1.0); // Set volume
try {
// جلب اللغة المحفوظة أو استخدام العربية كافتراضي
String lang = box.read(BoxName.lang) ?? 'ar-SA';
// تصحيح صيغة اللغة إذا لزم الأمر
if (lang == 'ar') lang = 'ar-SA';
if (lang == 'en') lang = 'en-US';
await flutterTts.setLanguage(lang);
await flutterTts.setSpeechRate(0.5); // سرعة متوسطة وواضحة
await flutterTts.setVolume(1.0);
await flutterTts.setPitch(1.0);
// إعدادات خاصة لضمان عمل الصوت مع الملاحة (خاصة للآيفون)
if (Platform.isIOS) {
await flutterTts
.setIosAudioCategory(IosTextToSpeechAudioCategory.playback, [
IosTextToSpeechAudioCategoryOptions.mixWithOthers,
IosTextToSpeechAudioCategoryOptions.duckOthers
]);
}
// الاستماع لحالة الانتهاء
flutterTts.setCompletionHandler(() {
isSpeaking = false;
update();
});
flutterTts.setStartHandler(() {
isSpeaking = true;
update();
});
} catch (e) {
print("TTS Init Error: $e");
}
}
// Function to speak the given text
// --- 2. دالة التحدث (تقاطع الكلام القديم) ---
Future<void> speakText(String text) async {
if (text.isEmpty) return;
try {
await flutterTts.awaitSpeakCompletion(true);
// إيقاف أي كلام حالي لضمان نطق التوجيه الجديد فوراً (أهم للملاحة)
await flutterTts.stop();
var result = await flutterTts.speak(text);
if (result == 1) {
// TTS operation has started
// You can perform additional operations here, if needed
isComplete = true;
isSpeaking = true;
update();
}
} catch (error) {
mySnackeBarError('Failed to speak text: $error');
// لا تعرض سناك بار هنا لتجنب إزعاج السائق أثناء القيادة
print('Failed to speak text: $error');
}
}
// --- 3. دالة الإيقاف (ضرورية لزر الكتم) ---
Future<void> stop() async {
try {
var result = await flutterTts.stop();
if (result == 1) {
isSpeaking = false;
update();
}
} catch (e) {
print("Error stopping TTS: $e");
}
}
}

View File

@@ -22,6 +22,7 @@ import '../../../print.dart';
import '../../../views/home/my_wallet/walet_captain.dart';
import '../../../views/widgets/elevated_btn.dart';
import '../../firebase/firbase_messge.dart';
import '../../functions/background_service.dart';
import '../../functions/crud.dart';
import '../../functions/location_background_controller.dart';
import '../../functions/location_controller.dart';
@@ -33,6 +34,7 @@ class HomeCaptainController extends GetxController {
Duration activeDuration = Duration.zero;
Timer? activeTimer;
Map data = {};
bool isHomeMapActive = true;
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
bool isLoading = true;
late double kazan = 0;
@@ -80,63 +82,75 @@ class HomeCaptainController extends GetxController {
void toggleHeatmap() async {
isHeatmapVisible = !isHeatmapVisible;
if (isHeatmapVisible) {
await _fetchAndDrawHeatmap();
await fetchAndDrawHeatmap();
} else {
heatmapPolygons.clear();
}
update(); // تحديث الواجهة
}
// دالة جلب البيانات ورسمها
Future<void> _fetchAndDrawHeatmap() async {
isLoading = true;
update();
// داخل MapDriverController
// استبدل هذا الرابط برابط ملف JSON الذي يولده كود PHP
// مثال: https://your-domain.com/api/driver/heatmap_data.json
// متغير لتخزين المربعات
// Set<Polygon> heatmapPolygons = {};
// دالة جلب البيانات ورسم الخريطة
Future<void> fetchAndDrawHeatmap() async {
// استخدم الرابط المباشر لملف JSON لسرعة قصوى
final String jsonUrl =
"https://rides.intaleq.xyz/intaleq/ride/heatmap/heatmap_data.json";
"https://api.intaleq.xyz/intaleq/ride/rides/heatmap_live.json";
try {
final response = await http.get(Uri.parse(jsonUrl));
// نستخدم timestamp لمنع الكاش من الموبايل نفسه
final response = await http.get(
Uri.parse("$jsonUrl?t=${DateTime.now().millisecondsSinceEpoch}"));
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
_generateGoogleMapPolygons(data);
} else {
print("Failed to load heatmap data");
_generatePolygons(data);
}
} catch (e) {
print("Error fetching heatmap: $e");
} finally {
isLoading = false;
update();
print("Heatmap Error: $e");
}
} // تحويل البيانات إلى مربعات على خريطة جوجل
}
void _generateGoogleMapPolygons(List<dynamic> data) {
void _generatePolygons(List<dynamic> data) {
Set<Polygon> tempPolygons = {};
// نصف قطر المربع (تقريباً 0.0005 يعادل 50-60 متر، مما يعطي مربع 100 متر)
// يجب أن يتناسب مع الـ precision المستخدم في PHP
// الأوفست لرسم المربع (نصف حجم الشبكة)
// الشبكة دقتها 0.01 درجة، لذا نصفها 0.005
double offset = 0.005;
for (var point in data) {
double lat = double.parse(point['lat'].toString());
double lng = double.parse(point['lng'].toString());
int count = int.parse(point['count'].toString());
// تحديد اللون بناءً على الكثافة
String intensity = point['intensity'] ?? 'low';
int count = int.parse(point['count'].toString()); // ✅ جلب العدد
Color color;
if (count >= 5) {
color = Colors.red.withOpacity(0.5); // عالي جداً
} else if (count >= 3) {
color = Colors.orange.withOpacity(0.5); // متوسط
Color strokeColor;
// 🧠 منطق الألوان: ندمج الذكاء (Intensity) مع العدد (Count)
if (intensity == 'high' || count >= 5) {
// منطقة مشتعلة (أحمر)
// إما فيها طلبات ضائعة (Timeout) أو فيها عدد كبير من الطلبات
color = Colors.red.withOpacity(0.35);
strokeColor = Colors.red.withOpacity(0.8);
} else if (intensity == 'medium' || count >= 3) {
// منطقة متوسطة (برتقالي)
color = Colors.orange.withOpacity(0.35);
strokeColor = Colors.orange.withOpacity(0.8);
} else {
color = Colors.green.withOpacity(0.4); // منخفض
// منطقة خفيفة (أصفر)
color = Colors.yellow.withOpacity(0.3);
strokeColor = Colors.yellow.withOpacity(0.6);
}
// إنشاء المربع
// رسم المربع
tempPolygons.add(Polygon(
polygonId: PolygonId("$lat-$lng"),
consumeTapEvents: true, // للسماح بالضغط عليه مستقبلاً
points: [
LatLng(lat - offset, lng - offset),
LatLng(lat + offset, lng - offset),
@@ -144,13 +158,19 @@ class HomeCaptainController extends GetxController {
LatLng(lat - offset, lng + offset),
],
fillColor: color,
strokeColor: color.withOpacity(0.8),
strokeWidth: 1,
visible: true,
strokeColor: strokeColor,
strokeWidth: 2,
));
}
heatmapPolygons = tempPolygons;
update(); // تحديث الخريطة
}
// دالة لتشغيل الخريطة الحرارية كل فترة (مثلاً عند فتح الصفحة)
void startHeatmapCycle() {
fetchAndDrawHeatmap();
// يمكن تفعيل Timer هنا لو أردت تحديثها تلقائياً كل 5 دقائق
}
void goToWalletFromConnect() {
@@ -179,11 +199,14 @@ class HomeCaptainController extends GetxController {
String stringActiveDuration = '';
void onButtonSelected() {
// totalPoints = Get.find<CaptainWalletController>().totalPoints;
// تم الإصلاح: التأكد من أن المتحكم موجود قبل استخدامه لتجنب الكراش
if (!Get.isRegistered<CaptainWalletController>()) {
Get.put(CaptainWalletController());
}
totalPoints = Get.find<CaptainWalletController>().totalPoints;
isActive = !isActive;
if (isActive) {
if (double.parse(totalPoints) > -30000) {
if (double.parse(totalPoints) > -200) {
locationController.startLocationUpdates();
HapticFeedback.heavyImpact();
// locationBackController.startBackLocation();
@@ -214,6 +237,107 @@ class HomeCaptainController extends GetxController {
// }
}
// متغيرات العداد للحظر
RxString remainingBlockTimeStr = "".obs;
Timer? _blockTimer;
/// دالة الفحص والدايلوج
void checkAndShowBlockDialog() {
String? blockStr = box.read(BoxName.blockUntilDate);
if (blockStr == null || blockStr.isEmpty) return;
DateTime blockExpiry = DateTime.parse(blockStr);
DateTime now = DateTime.now();
if (now.isBefore(blockExpiry)) {
// 1. إجبار السائق على وضع الأوفلاين
box.write(BoxName.statusDriverLocation, 'blocked');
update();
// 2. بدء العداد
_startBlockCountdown(blockExpiry);
// 3. إظهار الديالوج المانع
Get.defaultDialog(
title: "حسابك مقيد مؤقتاً ⛔",
titleStyle:
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
barrierDismissible: false, // 🚫 ممنوع الإغلاق بالضغط خارجاً
onWillPop: () async => false, // 🚫 ممنوع زر الرجوع في الأندرويد
content: Obx(() => Column(
children: [
const Icon(Icons.timer_off_outlined,
size: 50, color: Colors.orange),
const SizedBox(height: 15),
const Text(
"لقد تجاوزت حد الإلغاء المسموح به (3 مرات).\nلا يمكنك العمل حتى انتهاء العقوبة.",
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Text(
remainingBlockTimeStr.value, // 🔥 الوقت يتحدث هنا
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87),
),
const SizedBox(height: 20),
],
)),
confirm: Obx(() {
// الزر يكون مفعلاً فقط عندما ينتهي الوقت
bool isFinished = remainingBlockTimeStr.value == "00:00:00" ||
remainingBlockTimeStr.value == "Done";
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: isFinished ? Colors.green : Colors.grey,
),
onPressed: isFinished
? () {
Get.back(); // إغلاق الديالوج
box.remove(BoxName.blockUntilDate); // إزالة الحظر
Get.snackbar("أهلاً بك", "يمكنك الآن استقبال الطلبات",
backgroundColor: Colors.green);
}
: null, // زر معطل
child: Text(isFinished ? "Go Online" : "انتظر انتهاء الوقت"),
);
}),
);
} else {
// الوقت انتهى أصلاً -> تنظيف
box.remove(BoxName.blockUntilDate);
}
}
/// دالة العداد التنازلي
void _startBlockCountdown(DateTime expiry) {
_blockTimer?.cancel();
_blockTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
DateTime now = DateTime.now();
if (now.isAfter(expiry)) {
// انتهى الوقت
remainingBlockTimeStr.value = "Done";
timer.cancel();
} else {
// حساب الفرق وتنسيقه
Duration diff = expiry.difference(now);
String twoDigits(int n) => n.toString().padLeft(2, "0");
String hours = twoDigits(diff.inHours);
String minutes = twoDigits(diff.inMinutes.remainder(60));
String seconds = twoDigits(diff.inSeconds.remainder(60));
remainingBlockTimeStr.value = "$hours:$minutes:$seconds";
}
});
}
@override
void onClose() {
_blockTimer?.cancel();
super.onClose();
}
void getRefusedOrderByCaptain() async {
DateTime today = DateTime.now();
int todayDay = today.day;
@@ -232,7 +356,8 @@ class HomeCaptainController extends GetxController {
await sql.getCustomQuery(customQuery);
countRefuse = results[0]['count'].toString();
update();
if (int.parse(countRefuse) > 3 || double.parse(totalPoints) <= -3000) {
if (double.parse(totalPoints) <= -200) {
// if (int.parse(countRefuse) > 3 || double.parse(totalPoints) <= -200) {
locationController.stopLocationUpdates();
activeStartTime = null;
activeTimer?.cancel();
@@ -322,9 +447,9 @@ class HomeCaptainController extends GetxController {
isLoading = true;
update();
// This ensures we try to get a fix, but map doesn't crash if it fails
await Get.find<LocationController>().getLocation();
await locationController.getLocation();
var loc = Get.find<LocationController>().myLocation;
var loc = locationController.myLocation;
if (loc.latitude != 0) {
myLocation = loc;
}
@@ -357,8 +482,35 @@ class HomeCaptainController extends GetxController {
}
}
// 3. دالة نستدعيها عند قبول الطلب
void pauseHomeMapUpdates() {
isHomeMapActive = false;
update();
}
// 4. دالة نستدعيها عند العودة للصفحة الرئيسية
void resumeHomeMapUpdates() {
isHomeMapActive = true;
// إنعاش الخريطة عند العودة
if (mapHomeCaptainController != null) {
onMapCreated(mapHomeCaptainController!);
}
update();
}
@override
void onInit() async {
// ✅ طلب الإذونات أولاً
bool permissionsGranted = await PermissionsHelper.requestAllPermissions();
if (permissionsGranted) {
// ✅ بدء الخدمة بعد الحصول على الإذونات
await BackgroundServiceHelper.startService();
print('✅ Background service started successfully');
} else {
print('❌ لم يتم منح الإذونات - الخدمة لن تعمل');
// اعرض رسالة للمستخدم
}
// await locationBackController.requestLocationPermission();
Get.put(FirebaseMessagesController());
addToken();
@@ -374,24 +526,34 @@ class HomeCaptainController extends GetxController {
getCaptainWalletFromBuyPoints();
// onMapCreated(mapHomeCaptainController!);
// totalPoints = Get.find<CaptainWalletController>().totalPoints.toString();
getRefusedOrderByCaptain();
// getRefusedOrderByCaptain();
// 🔥 الفحص عند تشغيل التطبيق
checkAndShowBlockDialog();
box.write(BoxName.statusDriverLocation, 'off');
// 2. عدل الليسنر ليصبح مشروطاً
locationController.addListener(() {
// Only animate if active, map is ready, AND location is valid (not 0,0)
if (isActive && mapHomeCaptainController != null) {
var loc = locationController.myLocation;
// الشرط الذهبي: إذا كانت الصفحة غير نشطة أو الخريطة غير موجودة، لا تفعل شيئاً
if (!isHomeMapActive || mapHomeCaptainController == null || isClosed)
return;
if (isActive) {
// isActive الخاصة بالزر "متصل/غير متصل"
var loc = locationController.myLocation;
if (loc.latitude != 0 && loc.longitude != 0) {
mapHomeCaptainController!.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: loc,
zoom: 17.5,
tilt: 50.0,
bearing: locationController.heading,
try {
mapHomeCaptainController!.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: loc,
zoom: 17.5,
tilt: 50.0,
bearing: locationController.heading,
),
),
),
);
);
} catch (e) {
// التقاط الخطأ بصمت إذا حدث أثناء الانتقال
}
}
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,245 +1,589 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/constant/links.dart';
import 'package:sefer_driver/main.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:just_audio/just_audio.dart';
import 'dart:math' as math;
import '../../../constant/box_name.dart';
import '../../../print.dart';
import '../../functions/audio_controller.dart';
import '../../functions/crud.dart';
import '../../functions/encrypt_decrypt.dart';
import '../../functions/location_controller.dart';
import 'home_captain_controller.dart';
class OrderRequestController extends GetxController {
double progress = 0;
double progressSpeed = 0;
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../../views/home/Captin/driver_map_page.dart';
import '../../../views/home/Captin/orderCaptin/marker_generator.dart';
import '../../../views/widgets/mydialoug.dart';
import '../../functions/crud.dart';
import '../../functions/location_controller.dart';
import '../../home/captin/home_captain_controller.dart';
import '../../firebase/notification_service.dart';
import '../navigation/decode_polyline_isolate.dart';
class OrderRequestController extends GetxController
with WidgetsBindingObserver {
// --- متغيرات التايمر ---
double progress = 1.0;
int duration = 15;
int durationSpeed = 20;
int remainingTime = 0;
int remainingTimeSpeed = 0;
String countRefuse = '0';
int remainingTime = 15;
Timer? _timer;
bool applied = false;
final locationController = Get.put(LocationController());
BitmapDescriptor startIcon = BitmapDescriptor.defaultMarker;
BitmapDescriptor endIcon = BitmapDescriptor.defaultMarker;
final arguments = Get.arguments;
var myList;
late int hours;
late int minutes;
GoogleMapController? mapController; // Make it nullable
// 🔥 متغير لمنع تكرار القبول
bool _isRideTakenHandled = false;
// --- الأيقونات والماركرز ---
BitmapDescriptor? driverIcon;
Map<MarkerId, Marker> markersMap = {};
Set<Marker> get markers => markersMap.values.toSet();
// --- البيانات والتحكم ---
// 🔥 تم إضافة myMapData لدعم السوكيت الجديد
List<dynamic>? myList;
Map<dynamic, dynamic>? myMapData;
GoogleMapController? mapController;
// الإحداثيات (أزلنا late لتجنب الأخطاء القاتلة)
double latPassenger = 0.0;
double lngPassenger = 0.0;
double latDestination = 0.0;
double lngDestination = 0.0;
// --- متغيرات العرض ---
String passengerRating = "5.0";
String tripType = "Standard";
String totalTripDistance = "--";
String totalTripDuration = "--";
String tripPrice = "--";
String timeToPassenger = "جاري الحساب...";
String distanceToPassenger = "--";
// --- الخريطة ---
Set<Polyline> polylines = {};
// حالة التطبيق والصوت
bool isInBackground = false;
final AudioPlayer audioPlayer = AudioPlayer();
@override
Future<void> onInit() async {
print('OrderRequestController onInit called');
await initializeOrderPage();
if (Platform.isAndroid) {
bool isOverlayActive = await FlutterOverlayWindow.isActive();
if (isOverlayActive) {
await FlutterOverlayWindow.closeOverlay();
}
// 🛑 حماية من الفتح المتكرر لنفس الطلب
if (Get.arguments == null) {
print("❌ OrderController Error: No arguments received.");
Get.back(); // إغلاق الصفحة فوراً
return;
}
addCustomStartIcon();
addCustomEndIcon();
startTimer(
myList[6].toString(),
myList[16].toString(),
);
update();
super.onInit();
WidgetsBinding.instance.addObserver(this);
_checkOverlay();
// 🔥 تهيئة البيانات هي الخطوة الأولى والأهم
_initializeData();
_parseExtraData();
// 1. تجهيز أيقونة السائق
await _prepareDriverIcon();
// 2. وضع الماركرز المبدئية
_updateMarkers(
paxTime: "...",
paxDist: "",
destTime: totalTripDuration,
destDist: totalTripDistance);
// 3. رسم مبدئي
_initialMapSetup();
// 4. الاستماع للسوكيت
_listenForRideTaken();
// 5. حساب المسارين
await _calculateFullJourney();
// 6. تشغيل التايمر
startTimer();
}
late LatLngBounds bounds;
late List<LatLng> pointsDirection;
late String body;
late double latPassengerLocation;
late double lngPassengerLocation;
late double lngPassengerDestination;
late double latPassengerDestination;
// ----------------------------------------------------------------------
// 🔥🔥🔥 Smart Data Handling (List & Map Support) 🔥🔥🔥
// ----------------------------------------------------------------------
Future<void> initializeOrderPage() async {
final myListString = Get.arguments['myListString'];
Log.print('myListString0000: ${myListString}');
void _initializeData() {
var args = Get.arguments;
print("📦 Order Controller Received Type: ${args.runtimeType}");
print("📦 Order Controller Data: $args");
if (Get.arguments['DriverList'] == null ||
Get.arguments['DriverList'].isEmpty) {
myList = jsonDecode(myListString);
Log.print('myList from myListString: ${myList}');
} else {
myList = Get.arguments['DriverList'];
Log.print('myList from DriverList: ${myList}');
}
body = Get.arguments['body'];
Duration durationToAdd =
Duration(seconds: (double.tryParse(myList[4]) ?? 0).toInt());
hours = durationToAdd.inHours;
minutes = (durationToAdd.inMinutes % 60).round();
startTimerSpeed(myList[6].toString(), body.toString());
// --- Using the provided logic for initialization ---
var cords = myList[0].toString().split(',');
var cordDestination = myList[1].toString().split(',');
double? parseDouble(String value) {
try {
return double.parse(value);
} catch (e) {
Log.print("Error parsing value: $value");
return null; // or handle the error appropriately
if (args != null) {
// الحالة 1: قائمة مباشرة (Legacy / Some Firebase formats)
if (args is List) {
myList = args;
}
// الحالة 2: خريطة (Map)
else if (args is Map) {
// أ) هل هي قادمة من Firebase وتحتوي على DriverList؟
if (args.containsKey('DriverList')) {
var listData = args['DriverList'];
if (listData is List) {
myList = listData;
} else if (listData is String) {
// أحياناً تصل كنص مشفر داخل الـ Map
try {
myList = jsonDecode(listData);
} catch (e) {
print("Error decoding DriverList: $e");
}
}
}
// ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟
else {
myMapData = args;
}
}
}
latPassengerLocation = parseDouble(cords[0]) ?? 0.0;
lngPassengerLocation = parseDouble(cords[1]) ?? 0.0;
latPassengerDestination = parseDouble(cordDestination[0]) ?? 0.0;
lngPassengerDestination = parseDouble(cordDestination[1]) ?? 0.0;
// تعبئة الإحداثيات باستخدام الدالة الذكية _getValueAt
latPassenger = _parseCoord(_getValueAt(0));
lngPassenger = _parseCoord(_getValueAt(1));
latDestination = _parseCoord(_getValueAt(3));
lngDestination = _parseCoord(_getValueAt(4));
pointsDirection = [
LatLng(latPassengerLocation, lngPassengerLocation),
LatLng(latPassengerDestination, lngPassengerDestination)
print(
"📍 Parsed Coordinates: Pax($latPassenger, $lngPassenger) -> Dest($latDestination, $lngDestination)");
}
/// 🔥 دالة ذكية تجلب القيمة سواء كانت البيانات في List أو Map
dynamic _getValueAt(int index) {
// الأولوية للقائمة
if (myList != null && index < myList!.length) {
return myList![index];
}
// ثم الخريطة (السوكيت) - المفاتيح عبارة عن String
if (myMapData != null && myMapData!.containsKey(index.toString())) {
return myMapData![index.toString()];
}
return null;
}
/// الدالة التي يستخدمها باقي الكود لجلب البيانات كنصوص
String _safeGet(int index) {
var val = _getValueAt(index);
if (val != null) {
return val.toString();
}
return "";
}
double _parseCoord(dynamic val) {
if (val == null) return 0.0;
String s = val.toString().replaceAll(',', '').trim();
if (s.contains(' ')) s = s.split(' ')[0];
return double.tryParse(s) ?? 0.0;
}
void _parseExtraData() {
passengerRating = _safeGet(33).isEmpty ? "5.0" : _safeGet(33);
tripType = _safeGet(31);
totalTripDistance = _safeGet(5);
totalTripDuration = _safeGet(19);
tripPrice = _safeGet(2);
}
// ----------------------------------------------------------------------
// 🔥🔥🔥 Core Logic: Concurrent API Calls & Bounds 🔥🔥🔥
// ----------------------------------------------------------------------
Future<void> _calculateFullJourney() async {
try {
Position driverPos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
LatLng driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
updateDriverLocation(driverLatLng, driverPos.heading);
var pickupFuture = _fetchRouteData(
start: driverLatLng,
end: LatLng(latPassenger, lngPassenger),
color: Colors.amber,
id: 'pickup_route');
var tripFuture = _fetchRouteData(
start: LatLng(latPassenger, lngPassenger),
end: LatLng(latDestination, lngDestination),
color: Colors.green,
id: 'trip_route',
isDashed: true);
var results = await Future.wait([pickupFuture, tripFuture]);
var pickupResult = results[0];
var tripResult = results[1];
if (pickupResult != null) {
distanceToPassenger = pickupResult['distance_text'];
timeToPassenger = pickupResult['duration_text'];
polylines.add(pickupResult['polyline']);
}
if (tripResult != null) {
totalTripDistance = tripResult['distance_text'];
totalTripDuration = tripResult['duration_text'];
polylines.add(tripResult['polyline']);
}
await _updateMarkers(
paxTime: timeToPassenger,
paxDist: distanceToPassenger,
destTime: totalTripDuration,
destDist: totalTripDistance);
zoomToFitRide(driverLatLng);
update();
} catch (e) {
print("❌ Error in Journey Calculation: $e");
}
}
Future<Map<String, dynamic>?> _fetchRouteData(
{required LatLng start,
required LatLng end,
required Color color,
required String id,
bool isDashed = false}) async {
try {
// حماية من الإحداثيات الصفرية
if (start.latitude == 0 || end.latitude == 0) return null;
String apiUrl = "https://routesjo.intaleq.xyz/route/v1/driving";
String coords =
"${start.longitude},${start.latitude};${end.longitude},${end.latitude}";
String url = "$apiUrl/$coords?steps=false&overview=full";
var response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
var json = jsonDecode(response.body);
if (json['code'] == 'Ok' && json['routes'].isNotEmpty) {
var route = json['routes'][0];
double distM = double.parse(route['distance'].toString());
double durS = double.parse(route['duration'].toString());
String distText = "${(distM / 1000).toStringAsFixed(1)} كم";
String durText = "${(durS / 60).toStringAsFixed(0)} دقيقة";
String geometry = route['geometry'];
List<LatLng> points = await compute(decodePolylineIsolate, geometry);
Polyline polyline = Polyline(
polylineId: PolylineId(id),
color: color,
width: 5,
points: points,
patterns:
isDashed ? [PatternItem.dash(10), PatternItem.gap(5)] : [],
startCap: Cap.roundCap,
endCap: Cap.roundCap,
);
return {
'distance_text': distText,
'duration_text': durText,
'polyline': polyline
};
}
}
} catch (e) {
print("Route Fetch Error: $e");
}
return null;
}
void zoomToFitRide(LatLng driverPos) {
if (mapController == null) return;
// حماية من النقاط الصفرية
if (latPassenger == 0 || latDestination == 0) return;
List<LatLng> points = [
driverPos,
LatLng(latPassenger, lngPassenger),
LatLng(latDestination, lngDestination),
];
Log.print('pointsDirection: $pointsDirection');
calculateBounds();
double minLat = points.first.latitude;
double maxLat = points.first.latitude;
double minLng = points.first.longitude;
double maxLng = points.first.longitude;
for (var p in points) {
if (p.latitude < minLat) minLat = p.latitude;
if (p.latitude > maxLat) maxLat = p.latitude;
if (p.longitude < minLng) minLng = p.longitude;
if (p.longitude > maxLng) maxLng = p.longitude;
}
mapController!.animateCamera(CameraUpdate.newLatLngBounds(
LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
),
100.0,
));
}
// ----------------------------------------------------------------------
// Markers & Setup
// ----------------------------------------------------------------------
Future<void> _prepareDriverIcon() async {
driverIcon = await MarkerGenerator.createDriverMarker();
}
Future<void> _updateMarkers(
{required String paxTime,
required String paxDist,
String? destTime,
String? destDist}) async {
// حماية إذا لم يتم جلب الإحداثيات
if (latPassenger == 0 || latDestination == 0) return;
final BitmapDescriptor pickupIcon =
await MarkerGenerator.createCustomMarkerBitmap(
title: paxTime,
subtitle: paxDist,
color: Colors.green.shade700,
iconData: Icons.person_pin_circle,
);
final BitmapDescriptor dropoffIcon =
await MarkerGenerator.createCustomMarkerBitmap(
title: destTime ?? totalTripDuration,
subtitle: destDist ?? totalTripDistance,
color: Colors.red.shade800,
iconData: Icons.flag,
);
markersMap[const MarkerId('pax')] = Marker(
markerId: const MarkerId('pax'),
position: LatLng(latPassenger, lngPassenger),
icon: pickupIcon,
anchor: const Offset(0.5, 0.85),
);
markersMap[const MarkerId('dest')] = Marker(
markerId: const MarkerId('dest'),
position: LatLng(latDestination, lngDestination),
icon: dropoffIcon,
anchor: const Offset(0.5, 0.85),
);
update();
}
void _initialMapSetup() async {
Position driverPos = await Geolocator.getCurrentPosition();
LatLng driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
if (driverIcon != null) {
markersMap[const MarkerId('driver')] = Marker(
markerId: const MarkerId('driver'),
position: driverLatLng,
icon: driverIcon!,
rotation: driverPos.heading,
anchor: const Offset(0.5, 0.5),
flat: true,
zIndex: 10);
}
if (latPassenger != 0 && lngPassenger != 0) {
polylines.add(Polyline(
polylineId: const PolylineId('temp_line'),
points: [driverLatLng, LatLng(latPassenger, lngPassenger)],
color: Colors.grey,
width: 2,
patterns: [PatternItem.dash(10), PatternItem.gap(10)],
));
zoomToFitRide(driverLatLng);
}
update();
}
void updateDriverLocation(LatLng newPos, double heading) {
if (driverIcon != null) {
markersMap[const MarkerId('driver')] = Marker(
markerId: const MarkerId('driver'),
position: newPos,
icon: driverIcon!,
rotation: heading,
anchor: const Offset(0.5, 0.5),
flat: true,
zIndex: 10,
);
update();
}
}
void onMapCreated(GoogleMapController controller) {
mapController = controller;
animateCameraToBounds();
}
void calculateBounds() {
double minLat = math.min(latPassengerLocation, latPassengerDestination);
double maxLat = math.max(latPassengerLocation, latPassengerDestination);
double minLng = math.min(lngPassengerLocation, lngPassengerDestination);
double maxLng = math.max(lngPassengerLocation, lngPassengerDestination);
bounds = LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
);
Log.print('Calculated Bounds: $bounds');
}
void animateCameraToBounds() {
if (mapController != null) {
mapController!.animateCamera(CameraUpdate.newLatLngBounds(bounds, 80.0));
} else {
Log.print('mapController is null, cannot animate camera.');
}
}
getRideDEtailsForBackgroundOrder(String rideId) async {
await CRUD().get(link: AppLink.getRidesDetails, payload: {
'id': rideId,
});
}
void addCustomStartIcon() async {
ImageConfiguration config = const ImageConfiguration(size: Size(30, 30));
BitmapDescriptor.asset(
config,
'assets/images/A.png',
).then((value) {
startIcon = value;
update();
});
}
void addCustomEndIcon() {
ImageConfiguration config = const ImageConfiguration(size: Size(30, 30));
BitmapDescriptor.asset(
config,
'assets/images/b.png',
).then((value) {
endIcon = value;
update();
});
}
void changeApplied() {
applied = true;
update();
}
double mpg = 0;
calculateConsumptionFuel() {
mpg = Get.find<HomeCaptainController>().fuelPrice / 12;
}
bool _timerActive = false;
Future<void> startTimer(String driverID, String orderID) async {
_timerActive = true;
for (int i = 0; i <= duration && _timerActive; i++) {
await Future.delayed(const Duration(seconds: 1));
progress = i / duration;
remainingTime = duration - i;
update();
}
if (remainingTime == 0 && _timerActive) {
if (applied == false) {
endTimer();
//refuseOrder(orderID);
// --- قبول الطلب وإدارة التايمر ---
void startTimer() {
_timer?.cancel();
remainingTime = duration;
_playAudio();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (remainingTime <= 0) {
timer.cancel();
_stopAudio();
if (!applied) Get.back();
} else {
remainingTime--;
progress = remainingTime / duration;
update();
}
});
}
void endTimer() => _timer?.cancel();
void changeApplied() => applied = true;
void _playAudio() async {
try {
await audioPlayer.setAsset('assets/order.mp3', preload: true);
await audioPlayer.setLoopMode(LoopMode.one);
await audioPlayer.play();
} catch (e) {
print(e);
}
}
void endTimer() {
_timerActive = false;
void _stopAudio() => audioPlayer.stop();
void _listenForRideTaken() {
if (locationController.socket != null) {
locationController.socket!.off('ride_taken');
locationController.socket!.on('ride_taken', (data) {
if (_isRideTakenHandled) return;
String takenRideId = data['ride_id'].toString();
String myCurrentRideId = _safeGet(16);
String whoTookIt = data['taken_by_driver_id'].toString();
String myDriverId = box.read(BoxName.driverID).toString();
if (takenRideId == myCurrentRideId && whoTookIt != myDriverId) {
_isRideTakenHandled = true;
endTimer();
if (Get.isSnackbarOpen) Get.closeCurrentSnackbar();
if (Get.isDialogOpen ?? false) Get.back();
Get.back();
Get.snackbar("تنبيه", "تم قبول الطلب من قبل سائق آخر",
backgroundColor: Colors.orange, colorText: Colors.white);
}
});
}
}
void startTimerSpeed(String driverID, orderID) async {
for (int i = 0; i <= durationSpeed; i++) {
await Future.delayed(const Duration(seconds: 1));
progressSpeed = i / durationSpeed;
remainingTimeSpeed = durationSpeed - i;
update();
// Lifecycle
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
isInBackground = true;
} else if (state == AppLifecycleState.resumed) {
isInBackground = false;
FlutterOverlayWindow.closeOverlay();
}
if (remainingTimeSpeed == 0) {
if (applied == false) {
}
void _checkOverlay() async {
if (Platform.isAndroid && await FlutterOverlayWindow.isActive()) {
await FlutterOverlayWindow.closeOverlay();
}
}
// Accept Order Logic
Future<void> acceptOrder() async {
endTimer();
_stopAudio();
var res = await CRUD()
.post(link: "${AppLink.ride}/rides/acceptRide.php", payload: {
'id': _safeGet(16),
'rideTimeStart': DateTime.now().toString(),
'status': 'Apply',
'passengerToken': _safeGet(9),
'driver_id': box.read(BoxName.driverID),
});
if (res == 'failure') {
MyDialog().getDialog("عذراً، الطلب أخذه سائق آخر.", '', () {
Get.back();
}
Get.back();
});
} else {
Get.put(HomeCaptainController()).changeRideId();
box.write(BoxName.statusDriverLocation, 'on');
changeApplied();
var rideArgs = {
'passengerLocation': '${_safeGet(0)},${_safeGet(1)}',
'passengerDestination': '${_safeGet(3)},${_safeGet(4)}',
'Duration': totalTripDuration,
'totalCost': _safeGet(26),
'Distance': totalTripDistance,
'name': _safeGet(8),
'phone': _safeGet(10),
'email': _safeGet(28),
'WalletChecked': _safeGet(13),
'tokenPassenger': _safeGet(9),
'direction':
'https://www.google.com/maps/dir/${_safeGet(0)}/${_safeGet(1)}/',
'DurationToPassenger': timeToPassenger,
'rideId': _safeGet(16),
'passengerId': _safeGet(7),
'driverId': _safeGet(18),
'durationOfRideValue': totalTripDuration,
'paymentAmount': _safeGet(2),
'paymentMethod': _safeGet(13) == 'true' ? 'visa' : 'cash',
'isHaveSteps': _safeGet(20),
'step0': _safeGet(21),
'step1': _safeGet(22),
'step2': _safeGet(23),
'step3': _safeGet(24),
'step4': _safeGet(25),
'passengerWalletBurc': _safeGet(26),
'timeOfOrder': DateTime.now().toString(),
'totalPassenger': _safeGet(2),
'carType': _safeGet(31),
'kazan': _safeGet(32),
'startNameLocation': _safeGet(29),
'endNameLocation': _safeGet(30),
};
box.write(BoxName.rideArguments, rideArgs);
Get.off(() => PassengerLocationMapPage(), arguments: rideArgs);
}
}
addRideToNotificationDriverString(
orderID,
String startLocation,
String endLocation,
String date,
String time,
String price,
String passengerId,
String status,
String carType,
String passengerRate,
String priceForPassenger,
String distance,
String duration,
) async {
await CRUD().post(link: AppLink.addWaitingRide, payload: {
'id': (orderID),
'start_location': startLocation,
'end_location': endLocation,
'date': date,
'time': time,
'price': price,
'passenger_id': (passengerId),
'status': status,
'carType': carType,
'passengerRate': passengerRate,
'price_for_passenger': priceForPassenger,
'distance': distance,
'duration': duration,
});
@override
void onClose() {
locationController.socket?.off('ride_taken');
audioPlayer.dispose();
WidgetsBinding.instance.removeObserver(this);
_timer?.cancel();
mapController?.dispose();
super.onClose();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ import 'package:sefer_driver/views/home/on_boarding_page.dart';
import '../../constant/box_name.dart';
import '../../main.dart';
import '../../onbording_page.dart';
import '../../print.dart';
import '../functions/encrypt_decrypt.dart';
import '../functions/secure_storage.dart';
@@ -94,46 +95,33 @@ class SplashScreenController extends GetxController
/// is expected to be handled by an internal process (like login).
Future<Widget?> _getNavigationTarget() async {
try {
// ... (التحقق من OnBoarding)
// 1) Onboarding
final doneOnboarding = box.read(BoxName.onBoarding) == 'yes';
if (!doneOnboarding) {
// الأفضل: رجّع الواجهة بدل Get.off داخل الدالة
return OnBoardingPage();
}
// 2) Login
final isDriverDataAvailable = box.read(BoxName.phoneDriver) != null;
// final isPhoneVerified = box.read(BoxName.phoneVerified).toString() == '1'; // <-- ⛔️ تم حذف هذا السطر
if (!isDriverDataAvailable) {
return LoginCaptin();
}
final loginController = Get.put(LoginDriverController());
// ✅ --- (الحل) ---
// تم حذف التحقق من "isPhoneVerified"
// هذا يسمح لـ "loginWithGoogleCredential" بتحديد الحالة والتوجيه الصحيح
// (إلى Home أو DriverVerificationScreen أو PhoneNumberScreen)
if (isDriverDataAvailable) {
Log.print('المستخدم مسجل. جارٍ تهيئة الجلسة...');
final AppInitializer initializer = AppInitializer();
await initializer.initializeApp();
await EncryptionHelper.initialize();
// الخطوة 1: ضمان جلب الـ JWT أولاً
// (هذا هو الكود الذي كان في main.dart)
final AppInitializer initializer = AppInitializer();
await initializer.initializeApp();
await EncryptionHelper.initialize();
// انتظر حتى ينتهي جلب الـ JWT
await loginController.loginWithGoogleCredential(
box.read(BoxName.driverID).toString(),
box.read(BoxName.emailDriver).toString(),
);
Log.print('تم جلب الـ JWT. جارٍ تسجيل الدخول ببيانات جوجل...');
// الخطوة 2: الآن قم بتسجيل الدخول وأنت متأكد أن الـ JWT موجود
// يجب تعديل "loginWithGoogleCredential" لتعيد "bool" (نجاح/فشل)
await loginController.loginWithGoogleCredential(
box.read(BoxName.driverID).toString(),
box.read(BoxName.emailDriver).toString(),
);
// إذا نجح تسجيل الدخول (سواء لـ Home أو لـ DriverVerification)
// فإن "loginWithGoogleCredential" تقوم بالتوجيه بنفسها
// ونحن نُرجع "null" هنا لمنع "SplashScreen" من التوجيه مرة أخرى.
} else {
Log.print('مستخدم غير مسجل. اذهب لصفحة الدخول.');
return LoginCaptin();
}
return null; // لأن loginWithGoogleCredential يوجّه
} catch (e) {
Log.print("Error during navigation logic: $e");
// أي خطأ فادح (مثل خطأ في جلب الـ JWT) سيعيدك للدخول
return LoginCaptin();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,208 +1,178 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; // Import for WidgetsBinding
import 'package:geolocator/geolocator.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:just_audio/just_audio.dart'; // لتشغيل صوت عند وصول رحلة
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart'; // للوصول لـ box
import '../functions/crud.dart';
import '../functions/location_controller.dart';
class RideAvailableController extends GetxController {
bool isLoading = false;
// FIX 1: Initialize the map with a default structure.
// This prevents `rideAvailableMap['message']` from ever being null in the UI.
Map rideAvailableMap = {'message': []};
late LatLng southwest;
late LatLng northeast;
LatLngBounds calculateBounds(double lat, double lng, double radiusInMeters) {
const double earthRadius = 6378137.0; // Earth's radius in meters
// RxList: أي تغيير هنا سينعكس فوراً على الشاشة
RxList<Map<String, dynamic>> availableRides = <Map<String, dynamic>>[].obs;
double latDelta = (radiusInMeters / earthRadius) * (180 / pi);
double lngDelta =
(radiusInMeters / (earthRadius * cos(pi * lat / 180))) * (180 / pi);
DateTime? _lastFetchTime;
static const _cacheDuration = Duration(seconds: 5); // تقليل مدة الكاش قليلاً
double minLat = lat - latDelta;
double maxLat = lat + latDelta;
double minLng = lng - lngDelta;
double maxLng = lng + lngDelta;
// مشغل الصوت
final AudioPlayer _audioPlayer = AudioPlayer();
minLat = max(-90.0, minLat);
maxLat = min(90.0, maxLat);
@override
void onInit() {
super.onInit();
minLng = (minLng + 180) % 360 - 180;
maxLng = (maxLng + 180) % 360 - 180;
// 1. جلب القائمة الأولية من السيرفر (HTTP)
getRideAvailable(forceRefresh: true);
if (minLng > maxLng) {
double temp = minLng;
minLng = maxLng;
maxLng = temp;
// 2. تفعيل الاستماع المباشر للتحديثات (Socket)
_initSocketListeners();
}
@override
void onClose() {
// تنظيف الموارد عند الخروج
var socket = Get.find<LocationController>().socket;
socket?.off('market_new_ride');
socket?.off('ride_taken'); // تم توحيد الحدث لـ ride_taken
_audioPlayer.dispose();
super.onClose();
}
// ========================================================================
// 1. جلب الرحلات (HTTP Request) - الطريقة الجديدة (Lat/Lng)
// ========================================================================
Future<void> getRideAvailable({bool forceRefresh = false}) async {
// منع الطلبات المتكررة السريعة
if (!forceRefresh &&
_lastFetchTime != null &&
DateTime.now().difference(_lastFetchTime!) < _cacheDuration) {
return;
}
return LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
);
}
double calculateDistance(String startLocation) {
List<String> startLocationParts = startLocation.split(',');
double startLatitude = double.parse(startLocationParts[0]);
double startLongitude = double.parse(startLocationParts[1]);
double currentLatitude = Get.find<LocationController>().myLocation.latitude;
double currentLongitude =
Get.find<LocationController>().myLocation.longitude;
return Geolocator.distanceBetween(
currentLatitude,
currentLongitude,
startLatitude,
startLongitude,
);
}
// A helper function to safely show dialogs after the build cycle is complete.
void _showDialogAfterBuild(Widget dialog) {
// FIX 2: Use addPostFrameCallback to ensure dialogs are shown after the build process.
// This resolves the "visitChildElements() called during build" error.
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.dialog(
dialog,
barrierDismissible: true,
transitionCurve: Curves.easeOutBack,
transitionDuration: const Duration(milliseconds: 200),
);
});
}
Future<void> getRideAvailable() async {
try {
isLoading = true;
update();
if (forceRefresh) {
isLoading = true;
update();
}
LatLngBounds bounds = calculateBounds(
Get.find<LocationController>().myLocation.latitude,
Get.find<LocationController>().myLocation.longitude,
4000);
// الحصول على موقع السائق الحالي
final location = Get.find<LocationController>().myLocation;
// 🔥 التعديل الجوهري: نرسل Lat/Lng فقط بدلاً من Bounds
var payload = {
'southwestLat': bounds.southwest.latitude.toString(),
'southwestLon': bounds.southwest.longitude.toString(),
'northeastLat': bounds.northeast.latitude.toString(),
'northeastLon': bounds.northeast.longitude.toString(),
'lat': location.latitude.toString(),
'lng': location.longitude.toString(),
'radius': '50', // نصف القطر بالكيلومتر (كما حددناه في السيرفر)
};
var res =
await CRUD().get(link: AppLink.getRideWaiting, payload: payload);
isLoading = false; // Request is complete, stop loading indicator.
isLoading = false;
_lastFetchTime = DateTime.now();
if (res != 'failure') {
final decodedResponse = jsonDecode(res);
// Check for valid response structure
if (decodedResponse is Map &&
decodedResponse.containsKey('message') &&
decodedResponse['message'] is List) {
rideAvailableMap = decodedResponse;
// If the list of rides is empty, show the "No Rides" dialog
if ((rideAvailableMap['message'] as List).isEmpty) {
_showDialogAfterBuild(_buildNoRidesDialog());
if (decodedResponse is Map && decodedResponse['status'] == 'success') {
final rides = decodedResponse['message'];
if (rides is List) {
// تحويل البيانات وتخزينها
availableRides.value = List<Map<String, dynamic>>.from(rides);
} else {
availableRides.clear();
}
} else {
// If response format is unexpected, treat as no rides and show dialog
rideAvailableMap = {'message': []};
_showDialogAfterBuild(_buildNoRidesDialog());
availableRides.clear();
}
update(); // Update the UI with new data (or empty list)
} else {
// This block now handles network/server errors correctly
HapticFeedback.lightImpact();
update(); // Update UI to turn off loader
// Show a proper error dialog instead of "No Rides"
_showDialogAfterBuild(
_buildErrorDialog("Failed to fetch rides. Please try again.".tr));
}
update(); // تحديث الواجهة
} catch (e) {
isLoading = false;
update();
// This catches other exceptions like JSON parsing errors
_showDialogAfterBuild(
_buildErrorDialog("An unexpected error occurred.".tr));
print("Error fetching rides: $e");
}
}
// Extracted dialogs into builder methods for cleanliness.
Widget _buildNoRidesDialog() {
return CupertinoAlertDialog(
title: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
CupertinoIcons.car,
size: 44,
color: CupertinoColors.systemGrey,
),
const SizedBox(height: 12),
Text(
"No Rides Available".tr,
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
),
],
),
content: Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
"Please check back later for available rides.".tr,
style:
const TextStyle(fontSize: 13, color: CupertinoColors.systemGrey),
),
),
actions: [
CupertinoDialogAction(
onPressed: () {
Get.back(); // Close dialog
Get.back(); // Go back from AvailableRidesPage
},
child: Text('OK'.tr),
),
],
);
// ========================================================================
// 2. الاستماع للسوكيت (Real-time Updates) ⚡
// ========================================================================
void _initSocketListeners() {
var locationCtrl = Get.find<LocationController>();
var socket = locationCtrl.socket;
if (socket == null) {
print("⚠️ Socket is null in RideAvailableController");
return;
}
// A. عند وصول رحلة جديدة للسوق (market_new_ride)
socket.on('market_new_ride', (data) {
print("🔔 Socket: New Ride Market: $data");
if (data != null && data is Map) {
// فلترة: هل نوع السيارة يناسبني؟
if (_isCarTypeMatch(data['carType'])) {
// منع التكرار (إذا كانت الرحلة موجودة مسبقاً)
bool exists = availableRides
.any((r) => r['id'].toString() == data['id'].toString());
if (!exists) {
// إضافة الرحلة لأعلى القائمة
availableRides.insert(0, Map<String, dynamic>.from(data));
// تشغيل صوت تنبيه (Bling) 🎵
_playNotificationSound();
}
}
}
});
// B. عند أخذ رحلة من قبل سائق آخر (ride_taken)
// هذا الحدث يصل من acceptRide.php عبر السوكيت
socket.on('ride_taken', (data) {
print("🗑️ Socket: Ride Taken: $data");
if (data != null && data['ride_id'] != null) {
// حذف الرحلة من القائمة فوراً
availableRides.removeWhere(
(r) => r['id'].toString() == data['ride_id'].toString());
}
});
}
Widget _buildErrorDialog(String error) {
// You can log the error here for debugging.
// print("Error fetching rides: $error");
return CupertinoAlertDialog(
title: const Icon(
CupertinoIcons.exclamationmark_triangle_fill,
color: CupertinoColors.systemRed,
size: 44,
),
content: Text(
error, // Display the specific error message passed to the function
style: const TextStyle(fontSize: 14),
),
actions: [
CupertinoDialogAction(
onPressed: () {
Get.back(); // Close dialog
Get.back(); // Go back from AvailableRidesPage
},
child: Text('OK'.tr),
),
],
);
// دالة مساعدة للتحقق من نوع السيارة
bool _isCarTypeMatch(String? rideCarType) {
if (rideCarType == null) return false;
String myDriverType = box.read(BoxName.carTypeOfDriver).toString();
// منطق التوزيع الهرمي
switch (myDriverType) {
case 'Comfort':
return ['Speed', 'Comfort', 'Fixed Price'].contains(rideCarType);
case 'Speed':
case 'Scooter':
case 'Awfar Car':
return rideCarType == myDriverType;
case 'Lady':
return ['Comfort', 'Speed', 'Lady'].contains(rideCarType);
default:
return true; // احتياطياً
}
}
@override
void onInit() {
super.onInit();
getRideAvailable();
// تشغيل صوت التنبيه
Future<void> _playNotificationSound() async {
try {
// تأكد من وجود الملف في assets وإضافته في pubspec.yaml
await _audioPlayer.setAsset('assets/audio/notification.mp3');
_audioPlayer.play();
} catch (e) {
// تجاهل الخطأ إذا لم يوجد ملف صوت
}
}
}

View File

@@ -2,13 +2,14 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
// تأكد من استيراد الملفات الصحيحة حسب مشروع السائق الخاص بك
import 'package:sefer_driver/constant/links.dart';
import 'package:sefer_driver/controller/functions/crud.dart';
import '../../../constant/box_name.dart';
import '../../../main.dart';
import '../../../print.dart';
// import '../../../print.dart'; // إذا كنت تستخدمه
// ... (PaymentService class remains unchanged) ...
// --- خدمة الدفع للسائق (نفس المنطق الخاص بالسائق) ---
class PaymentService {
final String _baseUrl = "${AppLink.paymentServer}/ride/shamcash";
@@ -18,7 +19,7 @@ class PaymentService {
final response = await CRUD().postWallet(
link: url,
payload: {
'driverID': box.read(BoxName.driverID),
'driverID': box.read(BoxName.driverID), // استخدام driverID
'amount': amount.toString(),
},
).timeout(const Duration(seconds: 15));
@@ -74,7 +75,6 @@ class PaymentScreenSmsProvider extends StatefulWidget {
this.providerName = 'شام كاش',
this.providerLogo = 'assets/images/shamCash.png',
this.qrImagePath = 'assets/images/shamcashsend.png',
// removed paymentPhoneNumber
});
@override
@@ -82,21 +82,47 @@ class PaymentScreenSmsProvider extends StatefulWidget {
_PaymentScreenSmsProviderState();
}
class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider>
with SingleTickerProviderStateMixin {
final PaymentService _paymentService = PaymentService();
Timer? _pollingTimer;
PaymentStatus _status = PaymentStatus.creatingInvoice;
String? _invoiceNumber;
// العنوان الثابت للدفع (كما في تطبيق الراكب)
final String _paymentAddress = "80f23afe40499b02f49966c3340ae0fc";
// متغيرات الأنيميشن (الوميض)
late AnimationController _blinkController;
late Animation<Color?> _colorAnimation;
late Animation<double> _shadowAnimation;
@override
void initState() {
super.initState();
// إعداد الأنيميشن (وميض أحمر)
_blinkController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
)..repeat(reverse: true);
_colorAnimation = ColorTween(
begin: Colors.red.shade700,
end: Colors.red.shade100,
).animate(_blinkController);
_shadowAnimation = Tween<double>(begin: 2.0, end: 15.0).animate(
CurvedAnimation(parent: _blinkController, curve: Curves.easeInOut),
);
_createAndPollInvoice();
}
@override
void dispose() {
_pollingTimer?.cancel();
_blinkController.dispose();
super.dispose();
}
@@ -214,10 +240,10 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 1. المبلغ (تصميم مميز)
// 1. المبلغ المطلوب
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 15),
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade800, Colors.blue.shade600]),
@@ -231,64 +257,155 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
),
child: Column(
children: [
const Text("المبلغ المطلوب",
const Text("المبلغ المطلوب شحنه",
style: TextStyle(color: Colors.white70, fontSize: 14)),
const SizedBox(height: 8),
const SizedBox(height: 5),
Text(
"${currencyFormat.format(widget.amount)} ل.س",
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontSize: 28,
fontWeight: FontWeight.bold),
),
],
),
),
const SizedBox(height: 30),
const SizedBox(height: 25),
// 2. التعليمات والنسخ (الجزء الأهم)
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200),
boxShadow: [
BoxShadow(
color: Colors.grey.shade100,
blurRadius: 10,
offset: const Offset(0, 4))
],
),
child: Column(
children: [
Row(
// 2. رقم البيان (الإطار الأحمر الوامض)
AnimatedBuilder(
animation: _blinkController,
builder: (context, child) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _colorAnimation.value ?? Colors.red,
width: 3.0, // إطار سميك
),
boxShadow: [
BoxShadow(
color: (_colorAnimation.value ?? Colors.red)
.withOpacity(0.4),
blurRadius: _shadowAnimation.value,
spreadRadius: 2,
)
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.orange.shade50, shape: BoxShape.circle),
child: Icon(Icons.priority_high_rounded,
color: Colors.orange.shade800, size: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.warning_rounded,
color: Colors.red.shade800, size: 28),
const SizedBox(width: 8),
Text(
"هام جداً: لا تنسَ!",
style: TextStyle(
color: Colors.red.shade900,
fontWeight: FontWeight.bold,
fontSize: 18),
),
],
),
const SizedBox(width: 12),
const Expanded(
child: Text(
"انسخ الرقم أدناه وضعه في خانة (الملاحظات) عند الدفع.",
style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w600),
const SizedBox(height: 10),
const Text(
"يجب نسخ (رقم البيان) هذا ووضعه في تطبيق شام كاش لضمان نجاح العملية.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color: Colors.black87,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 15),
InkWell(
onTap: () {
Clipboard.setData(ClipboardData(text: invoiceText));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text("تم نسخ رقم البيان ✅",
textAlign: TextAlign.center),
backgroundColor: Colors.red.shade700,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
margin: const EdgeInsets.all(20),
),
);
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: Colors.red.shade200, width: 1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("رقم البيان (Invoice No)",
style: TextStyle(
fontSize: 12, color: Colors.grey)),
Text(invoiceText,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
letterSpacing: 2.0,
color: Colors.red.shade900)),
],
),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.copy_rounded,
color: Colors.red.shade900, size: 24),
),
],
),
),
),
],
),
const SizedBox(height: 20),
);
},
),
const SizedBox(height: 25),
// 3. عنوان الدفع (للتسهيل على السائق)
Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("عنوان الدفع (Payment Address)",
style: TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 8),
InkWell(
onTap: () {
Clipboard.setData(ClipboardData(text: invoiceText));
Clipboard.setData(ClipboardData(text: _paymentAddress));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text("تم نسخ رقم البيان",
content: const Text("تم نسخ عنوان الدفع",
textAlign: TextAlign.center),
backgroundColor: Colors.green.shade600,
behavior: SnackBarBehavior.floating,
@@ -298,36 +415,23 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
),
);
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: Colors.blue.shade200, width: 1.5),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("رقم البيان (Invoice ID)",
style: TextStyle(
fontSize: 12, color: Colors.grey)),
Text(invoiceText,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
letterSpacing: 1.5)),
],
child: Row(
children: [
Expanded(
child: Text(
_paymentAddress,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
fontFamily: 'Courier',
color: Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
const Icon(Icons.copy_rounded,
color: Colors.blue, size: 24),
],
),
),
const SizedBox(width: 8),
const Icon(Icons.copy, size: 18, color: Colors.grey),
],
),
),
],
@@ -336,17 +440,16 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
const SizedBox(height: 30),
// 3. الـ QR Code (قابل للاختيار/الضغط)
// 4. الـ QR Code
const Text("امسح الرمز للدفع",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87)),
const SizedBox(height: 15),
const SizedBox(height: 10),
GestureDetector(
onTap: () {
// تأثير بصري بسيط عند الضغط (أو تكبير الصورة في Dialog)
showDialog(
context: context,
builder: (ctx) => Dialog(
@@ -358,32 +461,19 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
);
},
child: Container(
padding: const EdgeInsets.all(15),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.grey.shade200,
blurRadius: 10,
spreadRadius: 2)
],
),
child: Column(
children: [
Image.asset(
widget.qrImagePath,
width: 180,
height: 180,
fit: BoxFit.contain,
errorBuilder: (c, o, s) => const Icon(Icons.qr_code_2,
size: 100, color: Colors.grey),
),
const SizedBox(height: 8),
const Text("اضغط للتكبير",
style: TextStyle(fontSize: 10, color: Colors.grey)),
],
child: Image.asset(
widget.qrImagePath,
width: 150,
height: 150,
fit: BoxFit.contain,
errorBuilder: (c, o, s) =>
const Icon(Icons.qr_code_2, size: 100, color: Colors.grey),
),
),
),
@@ -410,7 +500,7 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
const Text("تم الدفع بنجاح!",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
const Text("تم إضافة الرصيد والمكافأة إلى حسابك",
const Text("تم إضافة الرصيد إلى محفظتك",
style: TextStyle(color: Colors.grey)),
const SizedBox(height: 40),
SizedBox(

View File

@@ -12,6 +12,7 @@ import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import '../firebase/notification_service.dart';
import '../home/captin/home_captain_controller.dart';
// import '../home/captin/home_captain_controller.dart';
@@ -60,7 +61,7 @@ class RateController extends GetxController {
var paymentToken3 = await Get.find<MapDriverController>()
.generateTokenDriver((-1 * remainingFee).toString());
await CRUD().post(link: AppLink.addDrivePayment, payload: {
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
'rideId': 'remain$rideId',
'amount': (-1 * remainingFee).toString(),
'payment_method': 'Remainder',
@@ -68,13 +69,6 @@ class RateController extends GetxController {
'token': paymentToken3,
'driverID': box.read(BoxName.driverID).toString(),
});
// Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
// 'Wallet Added'.tr,
// 'Wallet Added${(remainingFee).toStringAsFixed(0)}'.tr,
// Get.find<MapDriverController>().tokenPassenger,
// [],
// 'tone2.wav');
NotificationService.sendNotification(
target: Get.find<MapDriverController>().tokenPassenger.toString(),
title: 'Wallet Added'.tr,
@@ -118,24 +112,13 @@ class RateController extends GetxController {
middleText: '',
confirm: MyElevatedButton(title: 'Ok', onPressed: () => Get.back()));
} else {
await CRUD().post(
link: "${AppLink.seferCairoServer}/ride/rate/add.php",
payload: {
'passenger_id': passengerId,
'driverID': box.read(BoxName.driverID).toString(),
'rideId': rideId.toString(),
'rating': selectedRateItemId.toString(),
'comment': comment.text ?? 'none',
});
if (AppLink.endPoint != AppLink.seferCairoServer) {
CRUD().post(link: "${AppLink.endPoint}/ride/rate/add.php", payload: {
'passenger_id': passengerId,
'driverID': box.read(BoxName.driverID).toString(),
'rideId': rideId.toString(),
'rating': selectedRateItemId.toString(),
'comment': comment.text ?? 'none',
});
}
await CRUD().post(link: "${AppLink.server}/ride/rate/add.php", payload: {
'passenger_id': passengerId,
'driverID': box.read(BoxName.driverID).toString(),
'rideId': rideId.toString(),
'rating': selectedRateItemId.toString(),
'comment': comment.text ?? 'none',
});
CRUD().sendEmail(AppLink.sendEmailToPassengerForTripDetails, {
'startLocation':
@@ -153,10 +136,15 @@ class RateController extends GetxController {
'endNameLocation':
Get.find<MapDriverController>().endNameLocation.toString(),
});
// homeCaptainController.isActive = true;
// update();
// homeCaptainController.getPaymentToday();
if (Get.isRegistered<MapDriverController>()) {
Get.find<MapDriverController>()
.disposeEverything(); // الدالة التي أنشأناها في الخطوة 3
Get.delete<MapDriverController>(force: true); // حذف إجباري من الذاكرة
}
Get.offAll(HomeCaptain());
if (Get.isRegistered<HomeCaptainController>()) {
Get.find<HomeCaptainController>().resumeHomeMapUpdates();
}
}
}
}