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

@@ -44,8 +44,8 @@ android {
applicationId = "com.intaleq_driver" applicationId = "com.intaleq_driver"
minSdkVersion = 23 minSdkVersion = 23
targetSdk = 36 targetSdk = 36
versionCode = 48 versionCode = 56
versionName = '1.0.48' // I've used the higher version name versionName = '1.0.56' // I've used the higher version name
multiDexEnabled = true multiDexEnabled = true
ndk { ndk {

View File

@@ -1,37 +1,42 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- ===== Permissions ===== --> <!-- ===== Permissions ===== -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING" />
<uses-permission android:name="android.permission.USE_BIOMETRIC"/> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS"/> <uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/> <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.QUICKBOOT_POWERON"/> <uses-permission android:name="android.permission.QUICKBOOT_POWERON" />
<uses-permission android:name="com.htc.intent.action.QUICKBOOT_POWERON"/> <uses-permission android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<uses-permission android:name="android.permission.PICTURE_IN_PICTURE"/> <uses-permission android:name="android.permission.PICTURE_IN_PICTURE" />
<uses-feature android:name="android.hardware.camera"/> <uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus"/> <uses-feature android:name="android.hardware.camera.autofocus" />
<application android:name="${applicationName}" android:icon="@mipmap/launcher_icon" android:label="@string/label" android:enableOnBackInvokedCallback="true" android:allowBackup="false" android:fullBackupContent="false" android:networkSecurityConfig="@xml/network_security_config" android:usesCleartextTraffic="false" android:theme="@style/LaunchTheme"> <application android:name="${applicationName}" android:icon="@mipmap/launcher_icon"
android:label="@string/label" android:enableOnBackInvokedCallback="true"
android:allowBackup="false" android:fullBackupContent="false"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="false" android:theme="@style/LaunchTheme">
<!-- Flutter embedding v2 --> <!-- Flutter embedding v2 -->
<meta-data android:name="flutterEmbedding" android:value="2"/> <meta-data android:name="flutterEmbedding" android:value="2" />
<!-- تحديد نقطة دخول خلفية (للـ overlay / background executor) --> <!-- تحديد نقطة دخول خلفية (للـ overlay / background executor) -->
<!-- <meta-data <!-- <meta-data
android:name="io.flutter.embedding.android.BackgroundExecutor.DART_ENTRYPOINT" android:name="io.flutter.embedding.android.BackgroundExecutor.DART_ENTRYPOINT"
@@ -40,67 +45,87 @@
android:name="io.flutter.embedding.android.BackgroundExecutor.DART_LIBRARY_URI" android:name="io.flutter.embedding.android.BackgroundExecutor.DART_LIBRARY_URI"
android:value="main.dart" /> --> android:value="main.dart" /> -->
<!-- خرائط + إشعارات فFirebase (قناة افتراضية) --> <!-- خرائط + إشعارات فFirebase (قناة افتراضية) -->
<meta-data android:name="com.google.android.geo.API_KEY" android:value="@string/api_key"/> <meta-data android:name="com.google.android.geo.API_KEY" android:value="@string/api_key" />
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="@string/default_notification_channel_id"/> <meta-data android:name="com.google.firebase.messaging.default_notification_channel_id"
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false"/> android:value="@string/default_notification_channel_id" />
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
<!-- Main Activity --> <!-- Main Activity -->
<activity android:name=".MainActivity" android:configChanges="orientation|keyboardHidden|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:exported="true" android:hardwareAccelerated="true" android:launchMode="singleTask" android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize"> <activity android:name=".MainActivity"
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme"/> android:configChanges="orientation|keyboardHidden|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true" android:hardwareAccelerated="true" android:launchMode="singleTask"
android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize">
<meta-data android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<!-- Deep Link: intaleqapp://... --> <!-- Deep Link: intaleqapp://... -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="intaleqapp"/> <data android:scheme="intaleqapp" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- أنشطة ومكوّنات إضافية --> <!-- أنشطة ومكوّنات إضافية -->
<activity android:name="com.yalantis.ucrop.UCropActivity" android:screenOrientation="portrait" android:theme="@style/Theme.AppCompat.Light.NoActionBar"/> <activity android:name="com.yalantis.ucrop.UCropActivity" android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<!-- خدماتك الخاصة --> <!-- خدماتك الخاصة -->
<service android:name=".MyFirebaseMessagingService" android:exported="false"/> <service
<service android:name=".LocationUpdatesService" android:exported="false" android:foregroundServiceType="location"/> android:name="id.flutter.flutter_background_service.BackgroundService"
android:foregroundServiceType="location"
android:enabled="true"
android:exported="true"
/>
<service android:name=".MyFirebaseMessagingService" android:exported="false" />
<service android:name=".LocationUpdatesService" android:exported="false"
android:foregroundServiceType="location" />
<!-- خدمة Firebase الرسمية لاستقبال رسائل FCM --> <!-- خدمة Firebase الرسمية لاستقبال رسائل FCM -->
<service android:name="com.google.firebase.messaging.FirebaseMessagingService" android:exported="false" tools:replace="android:exported"> <service android:name="com.google.firebase.messaging.FirebaseMessagingService"
android:exported="false" tools:replace="android:exported">
<intent-filter> <intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/> <action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter> </intent-filter>
</service> </service>
<!-- خدمة overlay للمكتبة الأولى (إن كنت تستخدمها) --> <!-- خدمة overlay للمكتبة الأولى (إن كنت تستخدمها) -->
<!-- <service <!-- <service
android:name="com.phan_tech.flutter_overlay_apps.OverlayService" android:name="com.phan_tech.flutter_overlay_apps.OverlayService"
android:exported="false" /> --> android:exported="false" /> -->
<service android:name="flutter.overlay.window.flutter_overlay_window.OverlayService" android:exported="false" android:foregroundServiceType="specialUse"/> <service android:name="flutter.overlay.window.flutter_overlay_window.OverlayService"
android:exported="false" android:foregroundServiceType="specialUse" />
<!-- خدمة overlay الخاصة بمكتبة flutter_overlay_window --> <!-- خدمة overlay الخاصة بمكتبة flutter_overlay_window -->
<!-- استقبال توكن/رسائل قديمة (توافقية) --> <!-- استقبال توكن/رسائل قديمة (توافقية) -->
<receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND"> <receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter> <intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE"/> <action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="com.intaleq_driver"/> <category android:name="com.intaleq_driver" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- خدمة الفقاعة الخاصة بك --> <!-- خدمة الفقاعة الخاصة بك -->
<service android:name="com.dsaved.bubblehead.bubble.BubbleHeadService" android:enabled="true" android:exported="false"> <service android:name="com.dsaved.bubblehead.bubble.BubbleHeadService" android:enabled="true"
android:exported="false">
<intent-filter> <intent-filter>
<action android:name="intent.bring.app.to.foreground"/> <action android:name="intent.bring.app.to.foreground" />
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</service> </service>
<!-- Notif schedulers --> <!-- Notif schedulers -->
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" android:exported="false"/> <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver" android:exported="false"> android:exported="false" />
<receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON"/> <action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/> <action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- مستقبل برودكاست خاص بك --> <!-- مستقبل برودكاست خاص بك -->
<receiver android:name=".YourBroadcastReceiver" android:exported="false"/> <receiver android:name=".YourBroadcastReceiver" android:exported="false" />
</application> </application>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET" />
</manifest> </manifest>

View File

@@ -13,4 +13,9 @@
<string name="exit_button">Exit App</string> <string name="exit_button">Exit App</string>
<string name="device_secure">Device is secure. Proceeding normally.</string> <string name="device_secure">Device is secure. Proceeding normally.</string>
<!-- <string name="default_notification_channel_id">driver_service_channel</string> -->
<string name="location_service_channel_id">location_service_channel</string>
<string name="geolocator_channel_id">geolocator_channel</string>
</resources> </resources>

24
collect_code.py Normal file
View File

@@ -0,0 +1,24 @@
import os
# اسم الملف الناتج
output_filename = "full_project_code_driver.txt"
# المجلد الذي تريد سحب الكود منه
source_folder = "./lib"
with open(output_filename, "w", encoding="utf-8") as outfile:
for root, dirs, files in os.walk(source_folder):
for file in files:
if file.endswith(".dart"):
file_path = os.path.join(root, file)
# كتابة فاصل واسم الملف ليعرف الذكاء الاصطناعي أين يبدأ الملف
outfile.write(f"\n\n{'='*50}\n")
outfile.write(f"FILE PATH: {file_path}\n")
outfile.write(f"{'='*50}\n\n")
try:
with open(file_path, "r", encoding="utf-8") as infile:
outfile.write(infile.read())
except Exception as e:
outfile.write(f"Error reading file: {e}\n")
print(f"تم تجميع الكود بنجاح في الملف: {output_filename}")

80436
full_project_code_driver.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -6,12 +6,14 @@ class BoxName {
static const String initializationVector = 'initializationVector'; static const String initializationVector = 'initializationVector';
static const String firstTimeLoadKey = 'firstTimeLoadKey'; static const String firstTimeLoadKey = 'firstTimeLoadKey';
static const String jwt = "jwt"; static const String jwt = "jwt";
static const String blockUntilDate = "blockUntilDate";
static const String rideId = "rideId"; static const String rideId = "rideId";
static const String rideArgumentsFromBackground = static const String rideArgumentsFromBackground =
"rideArgumentsFromBackground"; "rideArgumentsFromBackground";
static const String FCM_PRIVATE_KEY = "FCM_PRIVATE_KEY"; static const String FCM_PRIVATE_KEY = "FCM_PRIVATE_KEY";
static const String hmac = "hmac"; static const String hmac = "hmac";
static const String ttsEnabled = "ttsEnabled"; static const String ttsEnabled = "ttsEnabled";
static const String rideType = "rideType";
static const String walletType = "walletType"; static const String walletType = "walletType";
static const String fingerPrint = "fingerPrint"; static const String fingerPrint = "fingerPrint";
static const String updateInterval = "updateInterval"; static const String updateInterval = "updateInterval";
@@ -37,6 +39,7 @@ class BoxName {
static const String phoneVerified = "phoneVerified"; static const String phoneVerified = "phoneVerified";
static const String carPlate = "carPlate"; static const String carPlate = "carPlate";
static const String statusDriverLocation = "statusDriverLocation"; static const String statusDriverLocation = "statusDriverLocation";
static const String isAppInForeground = "isAppInForeground";
static const String rideStatus = "rideStatus"; static const String rideStatus = "rideStatus";
static const String nameArabic = "nameArabic"; static const String nameArabic = "nameArabic";
static const String carYear = "carYear"; static const String carYear = "carYear";

View File

@@ -10,11 +10,15 @@ class AppLink {
static String locationServer = static String locationServer =
'https://location.intaleq.xyz/intaleq/ride/location'; 'https://location.intaleq.xyz/intaleq/ride/location';
static const String routeApiBaseUrl =
"https://routesjo.intaleq.xyz/route/v1/driving";
static final String endPoint = 'https://api.intaleq.xyz/intaleq'; static final String endPoint = 'https://api.intaleq.xyz/intaleq';
static final String syria = 'https://syria.intaleq.xyz/intaleq'; static final String syria = 'https://syria.intaleq.xyz/intaleq';
static final String server = endPoint; static final String server = endPoint;
static String ride = '$endPoint/ride';
///=================ride==========================///
///https://api.intaleq.xyz/intaleq/ride
static String ride = '$server/ride';
static String rideServer = 'https://rides.intaleq.xyz/intaleq/ride'; static String rideServer = 'https://rides.intaleq.xyz/intaleq/ride';
static String seferCairoServer = endPoint; static String seferCairoServer = endPoint;
@@ -338,6 +342,7 @@ class AppLink {
static String login = "$auth/login.php"; static String login = "$auth/login.php";
static String signUp = "$auth/signup.php"; static String signUp = "$auth/signup.php";
static String updateDriverClaim = "$auth/captin/updateDriverClaim.php"; static String updateDriverClaim = "$auth/captin/updateDriverClaim.php";
static String updateShamCashDriver = "$auth/captin/updateShamCashDriver.php";
static String sendVerifyEmail = "$auth/sendVerifyEmail.php"; static String sendVerifyEmail = "$auth/sendVerifyEmail.php";
static String passengerRemovedAccountEmail = static String passengerRemovedAccountEmail =
"$auth/passengerRemovedAccountEmail.php"; "$auth/passengerRemovedAccountEmail.php";

View File

@@ -190,7 +190,7 @@ class LoginDriverController extends GetxController {
// ✅ بعد التأكد أن كل المفاتيح موجودة // ✅ بعد التأكد أن كل المفاتيح موجودة
await EncryptionHelper.initialize(); await EncryptionHelper.initialize();
await AppInitializer().getKey(); // await AppInitializer().getKey();
} else {} } else {}
} else { } else {
await EncryptionHelper.initialize(); await EncryptionHelper.initialize();
@@ -215,7 +215,7 @@ class LoginDriverController extends GetxController {
final jwt = decodedResponse1['jwt']; final jwt = decodedResponse1['jwt'];
await box.write(BoxName.jwt, c(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 carVinController = TextEditingController(); // Chassis number
final carRegistrationExpiryController = TextEditingController(); final carRegistrationExpiryController = TextEditingController();
DateTime? carRegistrationExpiryDate; 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 // STEP 3: Document Uploads
File? driverLicenseFrontImage; File? driverLicenseFrontImage;
File? driverLicenseBackImage; File? driverLicenseBackImage;
@@ -464,25 +483,30 @@ class RegistrationController extends GetxController {
Future<void> submitRegistration() async { Future<void> submitRegistration() async {
// 0) دوال/مساعدات محلية // 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 driverFrontUrl = docUrls['driver_license_front'];
final driverBackUrl = docUrls['driver_license_back']; final driverBackUrl = docUrls['driver_license_back'];
final carFrontUrl = docUrls['car_license_front']; final carFrontUrl = docUrls['car_license_front'];
final carBackUrl = docUrls['car_license_back']; final carBackUrl = docUrls['car_license_back'];
isLoading.value = true; isLoading.value = true;
update();
final registerUri = Uri.parse(AppLink.register_driver_and_car); final registerUri = Uri.parse(AppLink.register_driver_and_car);
final client = http.Client(); final client = http.Client();
try { try {
// ترويسات مشتركة // ترويسات مشتركة
final bearer = final bearer =
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}'; 'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
final hmac = '${box.read(BoxName.hmac)}'; final hmac = '${box.read(BoxName.hmac)}';
// 2) جهّز طلب التسجيل الرئيسي: حقول فقط + روابط الصور (لا نرفع صور إطلاقًا)
final req = http.MultipartRequest('POST', registerUri); final req = http.MultipartRequest('POST', registerUri);
req.headers.addAll({ req.headers.addAll({
'Authorization': bearer, 'Authorization': bearer,
@@ -499,11 +523,10 @@ class RegistrationController extends GetxController {
_addField(fields, 'national_number', nationalIdController.text); _addField(fields, 'national_number', nationalIdController.text);
_addField(fields, 'birthdate', bithdateController.text); _addField(fields, 'birthdate', bithdateController.text);
_addField(fields, 'expiry_date', driverLicenseExpiryController.text); _addField(fields, 'expiry_date', driverLicenseExpiryController.text);
_addField( _addField(fields, 'password', 'generated_password_or_token');
fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك
_addField(fields, 'status', 'yet'); _addField(fields, 'status', 'yet');
_addField(fields, 'email', 'Not specified'); _addField(fields, 'email', 'Not specified');
_addField(fields, 'gender', 'Male'); _addField(fields, 'gender', 'Male'); // يفضل ربطها بـ Dropdown أيضاً
// --- Car Data --- // --- Car Data ---
_addField(fields, 'vin', 'yet'); _addField(fields, 'vin', 'yet');
@@ -511,22 +534,53 @@ class RegistrationController extends GetxController {
_addField(fields, 'make', carMakeController.text); _addField(fields, 'make', carMakeController.text);
_addField(fields, 'model', carModelController.text); _addField(fields, 'model', carModelController.text);
_addField(fields, 'year', carYearController.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, 'color', carColorController.text);
_addField(fields, 'fuel', 'Gasoline');
if (colorHex != null && colorHex!.isNotEmpty) { if (colorHex != null && colorHex!.isNotEmpty) {
_addField(fields, 'color_hex', colorHex!); _addField(fields, 'color_hex', colorHex!);
} }
_addField(fields, 'owner', _addField(fields, 'owner',
'${firstNameController.text} ${lastNameController.text}'); '${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_front', driverFrontUrl!);
_addField(fields, 'driver_license_back', driverBackUrl!); _addField(fields, 'driver_license_back', driverBackUrl!);
_addField(fields, 'car_license_front', carFrontUrl!); _addField(fields, 'car_license_front', carFrontUrl!);
_addField(fields, 'car_license_back', carBackUrl!); _addField(fields, 'car_license_back', carBackUrl!);
// أضف الحقول
req.fields.addAll(fields); req.fields.addAll(fields);
// 3) الإرسال // 3) الإرسال
@@ -534,80 +588,53 @@ class RegistrationController extends GetxController {
await client.send(req).timeout(const Duration(seconds: 60)); await client.send(req).timeout(const Duration(seconds: 60));
final resp = await http.Response.fromStream(streamed); final resp = await http.Response.fromStream(streamed);
// 4) فحص النتيجة // 4) معالجة الاستجابة
Map<String, dynamic>? json; Map<String, dynamic>? json;
try { try {
json = jsonDecode(resp.body) as Map<String, dynamic>; json = jsonDecode(resp.body) as Map<String, dynamic>;
} catch (_) {} } catch (_) {}
if (resp.statusCode == 200 && json?['status'] == 'success') { if (resp.statusCode == 200 && json?['status'] == 'success') {
// final driverID = Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr,
// (json!['data']?['driverID'] ?? json['driverID'])?.toString(); backgroundColor: Colors.green, colorText: Colors.white);
// if (driverID != null && driverID.isNotEmpty) {
// box.write(BoxName.driverID, driverID);
// }
Get.snackbar( // منطق التوكن والإشعارات وتسجيل الدخول...
'Success'.tr,
'Registration completed successfully!'.tr,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
// متابعة تسجيل الدخول إن لزم
final email = box.read(BoxName.emailDriver); final email = box.read(BoxName.emailDriver);
final driverID = box.read(BoxName.driverID); 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(); String fingerPrint = await DeviceHelper.getDeviceFingerprint();
await CRUD().post(link: AppLink.addTokensDriver, payload: { await CRUD().post(link: AppLink.addTokensDriver, payload: {
'captain_id': (box.read(BoxName.driverID)).toString(), 'captain_id': (box.read(BoxName.driverID)).toString(),
'token': (box.read(BoxName.tokenDriver)).toString(), 'token': (box.read(BoxName.tokenDriver)).toString(),
'fingerPrint': fingerPrint.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( NotificationService.sendNotification(
target: 'service', // الإرسال لجميع المشتركين في "service" target: 'service',
title: 'طلب خدمة جديد', title: 'New Driver Registration',
body: 'تم استلام طلب خدمة جديد. الرجاء مراجعة التفاصيل.', body: 'Driver $driverID has submitted registration.',
isTopic: true, 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); c.loginWithGoogleCredential(driverID, email);
} else { } else {
final msg = final msg = (json?['message'] ?? 'Registration failed.').toString();
(json?['message'] ?? 'Registration failed. Please try again.') Get.snackbar('Error'.tr, msg,
.toString(); backgroundColor: Colors.red, colorText: Colors.white);
Log.print('msg: $msg');
Get.snackbar(
'Error'.tr,
msg,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} }
} catch (e) { } catch (e) {
Get.snackbar( Get.snackbar('Error'.tr, 'Error: $e',
'Error'.tr, backgroundColor: Colors.red, colorText: Colors.white);
'${'An unexpected error occurred:'.tr} $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally { } finally {
client.close(); client.close();
isLoading.value = false; isLoading.value = false;
update();
} }
} // Future<void> submitRegistration() async { }
// // 1) تحقق من الصور // // 1) تحقق من الصور
// if (driverLicenseFrontImage == null || // if (driverLicenseFrontImage == null ||
// driverLicenseBackImage == 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 '../../views/home/Captin/orderCaptin/vip_order_page.dart';
import '../auth/google_sign.dart'; import '../auth/google_sign.dart';
import '../functions/face_detect.dart'; import '../functions/face_detect.dart';
import '../home/captin/map_driver_controller.dart';
import 'local_notification.dart'; import 'local_notification.dart';
class FirebaseMessagesController extends GetxController { class FirebaseMessagesController extends GetxController {
@@ -80,7 +81,7 @@ class FirebaseMessagesController extends GetxController {
AndroidNotification? android = notification?.android; AndroidNotification? android = notification?.android;
// if (notification != null && android != null) { // if (notification != null && android != null) {
if (message.data.isNotEmpty && message.notification != null) { if (message.data.isNotEmpty) {
fireBaseTitles(message); fireBaseTitles(message);
} }
// if (message.data.isNotEmpty && message.notification != null) { // if (message.data.isNotEmpty && message.notification != null) {
@@ -90,7 +91,7 @@ class FirebaseMessagesController extends GetxController {
FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {}); FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {});
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
if (message.data.isNotEmpty && message.notification != null) { if (message.data.isNotEmpty) {
fireBaseTitles(message); fireBaseTitles(message);
} }
}); });
@@ -110,7 +111,7 @@ class FirebaseMessagesController extends GetxController {
case 'ORDER': case 'ORDER':
case 'Order': // Handle both cases for backward compatibility case 'Order': // Handle both cases for backward compatibility
if (Platform.isAndroid) { if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'tone1', ''); notificationController.showNotification(title, body, 'order', '');
} }
var myListString = message.data['DriverList']; var myListString = message.data['DriverList'];
if (myListString != null) { if (myListString != null) {
@@ -118,7 +119,7 @@ class FirebaseMessagesController extends GetxController {
driverToken = myList[14].toString(); driverToken = myList[14].toString();
Get.put(HomeCaptainController()).changeRideId(); Get.put(HomeCaptainController()).changeRideId();
update(); update();
Get.to(() => OrderRequestPage(), arguments: { Get.toNamed('/OrderRequestPage', arguments: {
'myListString': myListString, 'myListString': myListString,
'DriverList': myList, 'DriverList': myList,
'body': body 'body': body
@@ -147,7 +148,18 @@ class FirebaseMessagesController extends GetxController {
notificationController.showNotification( notificationController.showNotification(
title, 'Passenger Cancel Trip'.tr, 'cancel', ''); 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; break;
case 'VIP Order Accepted': case 'VIP Order Accepted':

View File

@@ -1,11 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; 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_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:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@@ -74,38 +71,6 @@ class NotificationController extends GetxController {
sound: RawResourceAndroidNotificationSound(tone), 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( DarwinNotificationDetails ios = const DarwinNotificationDetails(
sound: 'default', sound: 'default',
presentAlert: true, presentAlert: true,

View File

@@ -1,41 +1,44 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:get/get.dart'; // للترجمة .tr
import '../../print.dart';
class NotificationService { class NotificationService {
// تأكد من أن هذا هو الرابط الصحيح لملف الإرسال
static const String _serverUrl = 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({ static Future<void> sendNotification({
required String target, required String target,
required String title, required String title,
required String body, required String body,
required String? category, // <-- [الإضافة الأولى] required String category, // إلزامي للتصنيف
String? tone, String? tone,
List<String>? driverList, List<String>? driverList,
bool isTopic = false, bool isTopic = false,
}) async { }) async {
try { 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, 'target': target,
'title': title, 'title': title,
'body': body, 'body': body,
'isTopic': isTopic, 'isTopic': isTopic,
'data':
customData, // 🔥🔥 التغيير الجوهري: وضعنا البيانات داخل "data" 🔥🔥
}; };
if (category != null) {
payload['category'] = category; // <-- [الإضافة الثانية]
}
if (tone != null) { if (tone != null) {
payload['tone'] = tone; requestPayload['tone'] = tone;
}
if (driverList != null) {
// [مهم] تطبيق السائق يرسل passengerList
payload['passengerList'] = jsonEncode(driverList);
} }
final response = await http.post( final response = await http.post(
@@ -43,17 +46,18 @@ class NotificationService {
headers: { headers: {
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
}, },
body: jsonEncode(payload), body: jsonEncode(requestPayload),
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
Log.print('✅ Notification sent successfully.'); print('✅ Notification sent successfully.');
// print('Response: ${response.body}');
} else { } else {
Log.print( print('❌ Failed to send notification. Code: ${response.statusCode}');
'❌ Failed to send notification. Status code: ${response.statusCode}'); print('Error Body: ${response.body}');
} }
} catch (e) { } 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. /// Centralized private method to handle all API requests.
/// Includes retry logic, network checking, and standardized error handling. /// Includes retry logic, network checking, and standardized error handling.
// --- تعديل 1: دالة _makeRequest محسنة للإنترنت الضعيف ---
Future<dynamic> _makeRequest({ Future<dynamic> _makeRequest({
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,
required Map<String, String> headers, required Map<String, String> headers,
}) async { }) async {
// timeouts أقصر // 🟢 زيادة الوقت للسماح بالشبكات البطيئة (سوريا)
const connectTimeout = Duration(seconds: 6); const connectTimeout = Duration(seconds: 20); // رفعنا الوقت من 6 لـ 20
const receiveTimeout = Duration(seconds: 10); const receiveTimeout = Duration(seconds: 40); // رفعنا الوقت من 10 لـ 40
Future<http.Response> doPost() { Future<http.Response> doPost() {
final url = Uri.parse(link); final url = Uri.parse(link);
// استخدم _client بدل http.post // نستخدم _client إذا كان معرفاً، أو ننشئ واحداً جديداً مع إغلاقه لاحقاً
return _client // لضمان عدم حدوث مشاكل، سنستخدم http.post المباشر كما في النسخة المستقرة لديك
// ولكن مع timeout أطول
return http
.post(url, body: payload, headers: headers) .post(url, body: payload, headers: headers)
.timeout(connectTimeout + receiveTimeout); .timeout(connectTimeout + receiveTimeout);
} }
http.Response response; http.Response response = http.Response('', 500); // Default initialization
try {
// retry ذكي: محاولة واحدة إضافية فقط لأخطاء شبكة/5xx
try {
response = await doPost();
} on SocketException catch (_) {
// محاولة ثانية واحدة فقط
response = await doPost();
} on TimeoutException catch (_) {
response = await doPost();
}
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; final body = response.body;
// 2xx
if (sc >= 200 && sc < 300) { if (sc >= 200 && sc < 300) {
try { try {
final jsonData = jsonDecode(body); final jsonData = jsonDecode(body);
return jsonData; // لا تعيد 'success' فقط؛ أعِد الجسم كله return jsonData;
} catch (e, st) { } catch (e, st) {
// لا تسجّل كخطأ شبكي لكل حالة؛ فقط معلومات
addError('JSON Decode Error', 'Body: $body\n$st', addError('JSON Decode Error', 'Body: $body\n$st',
'CRUD._makeRequest $link'); 'CRUD._makeRequest $link');
return 'failure'; return 'failure';
} }
} }
// 401 → دع الطبقة العليا تتعامل مع التجديد
if (sc == 401) { if (sc == 401) {
// لا تستدع getJWT هنا كي لا نضاعف الرحلات
return 'token_expired'; return 'token_expired';
} }
// 5xx: لا تعِد المحاولة هنا (حاولنا مرة ثانية فوق)
if (sc >= 500) { if (sc >= 500) {
addError( addError(
'Server 5xx', 'SC: $sc\nBody: $body', 'CRUD._makeRequest $link'); 'Server 5xx', 'SC: $sc\nBody: $body', 'CRUD._makeRequest $link');
return 'failure'; 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'; return 'failure';
} on SocketException { } on SocketException {
_netGuard.notifyOnce((title, msg) => mySnackeBarError(msg)); // معالجة صامتة لانقطاع النت
return 'no_internet'; return 'no_internet';
} on TimeoutException { } catch (e) {
return 'failure'; addError('GET Exception: $e', '', link);
} catch (e, st) {
addError('HTTP Request Exception: $e', 'Stack: $st',
'CRUD._makeRequest $link');
return 'failure'; return 'failure';
} }
} }
@@ -175,51 +258,6 @@ class CRUD {
/// Performs a standard authenticated GET request (using POST method as per original code). /// Performs a standard authenticated GET request (using POST method as per original code).
/// Automatically handles token renewal. /// 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. /// Performs an authenticated POST request to wallet endpoints.
Future<dynamic> postWallet({ Future<dynamic> postWallet({

View File

@@ -8,31 +8,38 @@ void showInBrowser(String url) async {
} }
Future<void> makePhoneCall(String phoneNumber) async { Future<void> makePhoneCall(String phoneNumber) async {
// 1. تنظيف الرقم (إزالة المسافات والفواصل) // 1. Clean the number
String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), ''); String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), '');
// 2. التحقق من طول الرقم لتحديد طريقة التنسيق // 2. Format logic (Syria/International)
if (formattedNumber.length > 6) { if (formattedNumber.length > 6) {
// --- التعديل المطلوب ---
if (formattedNumber.startsWith('09')) { if (formattedNumber.startsWith('09')) {
// إذا كان يبدأ بـ 09 (رقم موبايل سوري محلي)
// نحذف أول خانة (الصفر) ونضيف +963
formattedNumber = '+963${formattedNumber.substring(1)}'; formattedNumber = '+963${formattedNumber.substring(1)}';
} else if (!formattedNumber.startsWith('+')) { } else if (!formattedNumber.startsWith('+')) {
// إذا لم يكن يبدأ بـ + (ولم يكن يبدأ بـ 09)، نضيف + في البداية
// هذا للحفاظ على منطقك القديم للأرقام الدولية الأخرى
formattedNumber = '+$formattedNumber'; formattedNumber = '+$formattedNumber';
} }
} }
// 3. التنفيذ (Launch) // 3. Create URI
final Uri launchUri = Uri( final Uri launchUri = Uri(
scheme: 'tel', scheme: 'tel',
path: formattedNumber, path: formattedNumber,
); );
if (await canLaunchUrl(launchUri)) { // 4. Execute with externalApplication mode
await launchUrl(launchUri); 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 'dart:io';
// import 'package:background_location/background_location.dart';
// import 'package:get/get.dart';
// import 'package:permission_handler/permission_handler.dart';
// class LocationBackgroundController extends GetxController { import 'package:device_info_plus/device_info_plus.dart';
// @override import 'package:permission_handler/permission_handler.dart';
// void onInit() {
// super.onInit();
// requestLocationPermission();
// configureBackgroundLocation();
// }
// Future<void> requestLocationPermission() async { import 'background_service.dart';
// var status = await Permission.locationAlways.status;
// if (!status.isGranted) {
// await Permission.locationAlways.request();
// }
// }
// Future<void> configureBackgroundLocation() async { Future<void> requestNotificationPermission() async {
// await BackgroundLocation.setAndroidNotification( if (Platform.isAndroid) {
// title: 'Location Tracking Active'.tr, final androidInfo = await DeviceInfoPlugin().androidInfo;
// message: 'Your location is being tracked in the background.'.tr, if (androidInfo.version.sdkInt >= 33) {
// icon: '@mipmap/launcher_icon', // Android 13+
// ); final status = await Permission.notification.request();
if (!status.isGranted) {
print('⚠️ إذن الإشعارات مرفوض');
return;
}
}
}
// BackgroundLocation.setAndroidConfiguration(3000); // بعد الحصول على الإذن، ابدأ الخدمة
// BackgroundLocation.startLocationService(); await BackgroundServiceHelper.startService();
// BackgroundLocation.getLocationUpdates((location) { }
// // Handle location updates here
// });
// }
// startBackLocation() async { class PermissionsHelper {
// Timer.periodic(const Duration(seconds: 3), (timer) { /// طلب إذن الإشعارات على Android 13+
// getBackgroundLocation(); static Future<bool> requestNotificationPermission() async {
// }); if (Platform.isAndroid) {
// } final androidInfo = await DeviceInfoPlugin().androidInfo;
// getBackgroundLocation() async { // Android 13+ (API 33+) يحتاج إذن POST_NOTIFICATIONS
// var status = await Permission.locationAlways.status; if (androidInfo.version.sdkInt >= 33) {
// if (status.isGranted) { final status = await Permission.notification.request();
// await BackgroundLocation.startLocationService(
// distanceFilter: 20, forceAndroidLocationManager: true);
// BackgroundLocation.setAndroidConfiguration(
// Duration.microsecondsPerSecond); // Set interval to 5 seconds
// BackgroundLocation.getLocationUpdates((location1) {}); if (status.isDenied) {
// } else { print('⚠️ إذن الإشعارات مرفوض');
// await Permission.locationAlways.request(); 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 'dart:io';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart'; import 'package:flutter_tts/flutter_tts.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/main.dart';
class TextToSpeechController extends GetxController { class TextToSpeechController extends GetxController {
final flutterTts = FlutterTts(); final FlutterTts flutterTts = FlutterTts();
bool isComplete = false; bool isSpeaking = false;
// Initialize TTS in initState
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
initTts(); initTts();
} }
// Dispose of TTS when controller is closed
@override @override
void onClose() { void onClose() {
flutterTts.stop();
super.onClose(); super.onClose();
flutterTts.completionHandler;
} }
// Function to initialize TTS engine // --- 1. تهيئة المحرك بإعدادات قوية للملاحة ---
Future<void> initTts() async { Future<void> initTts() async {
String? lang = try {
WidgetsBinding.instance.platformDispatcher.locale.countryCode; // جلب اللغة المحفوظة أو استخدام العربية كافتراضي
await flutterTts String lang = box.read(BoxName.lang) ?? 'ar-SA';
.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 if (lang == 'ar') lang = 'ar-SA';
await flutterTts.setSpeechRate(0.5); // Adjust speech rate if (lang == 'en') lang = 'en-US';
await flutterTts.setVolume(1.0); // Set volume
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 { Future<void> speakText(String text) async {
if (text.isEmpty) return;
try { try {
await flutterTts.awaitSpeakCompletion(true); // إيقاف أي كلام حالي لضمان نطق التوجيه الجديد فوراً (أهم للملاحة)
await flutterTts.stop();
var result = await flutterTts.speak(text); var result = await flutterTts.speak(text);
if (result == 1) { if (result == 1) {
// TTS operation has started isSpeaking = true;
// You can perform additional operations here, if needed
isComplete = true;
update(); update();
} }
} catch (error) { } 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/home/my_wallet/walet_captain.dart';
import '../../../views/widgets/elevated_btn.dart'; import '../../../views/widgets/elevated_btn.dart';
import '../../firebase/firbase_messge.dart'; import '../../firebase/firbase_messge.dart';
import '../../functions/background_service.dart';
import '../../functions/crud.dart'; import '../../functions/crud.dart';
import '../../functions/location_background_controller.dart'; import '../../functions/location_background_controller.dart';
import '../../functions/location_controller.dart'; import '../../functions/location_controller.dart';
@@ -33,6 +34,7 @@ class HomeCaptainController extends GetxController {
Duration activeDuration = Duration.zero; Duration activeDuration = Duration.zero;
Timer? activeTimer; Timer? activeTimer;
Map data = {}; Map data = {};
bool isHomeMapActive = true;
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker; BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
bool isLoading = true; bool isLoading = true;
late double kazan = 0; late double kazan = 0;
@@ -80,63 +82,75 @@ class HomeCaptainController extends GetxController {
void toggleHeatmap() async { void toggleHeatmap() async {
isHeatmapVisible = !isHeatmapVisible; isHeatmapVisible = !isHeatmapVisible;
if (isHeatmapVisible) { if (isHeatmapVisible) {
await _fetchAndDrawHeatmap(); await fetchAndDrawHeatmap();
} else { } else {
heatmapPolygons.clear(); heatmapPolygons.clear();
} }
update(); // تحديث الواجهة update(); // تحديث الواجهة
} }
// دالة جلب البيانات ورسمها // داخل MapDriverController
Future<void> _fetchAndDrawHeatmap() async {
isLoading = true;
update();
// استبدل هذا الرابط برابط ملف JSON الذي يولده كود PHP // متغير لتخزين المربعات
// مثال: https://your-domain.com/api/driver/heatmap_data.json // Set<Polygon> heatmapPolygons = {};
// دالة جلب البيانات ورسم الخريطة
Future<void> fetchAndDrawHeatmap() async {
// استخدم الرابط المباشر لملف JSON لسرعة قصوى
final String jsonUrl = final String jsonUrl =
"https://rides.intaleq.xyz/intaleq/ride/heatmap/heatmap_data.json"; "https://api.intaleq.xyz/intaleq/ride/rides/heatmap_live.json";
try { 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) { if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body); final List<dynamic> data = json.decode(response.body);
_generateGoogleMapPolygons(data); _generatePolygons(data);
} else {
print("Failed to load heatmap data");
} }
} catch (e) { } catch (e) {
print("Error fetching heatmap: $e"); print("Heatmap Error: $e");
} finally {
isLoading = false;
update();
} }
} // تحويل البيانات إلى مربعات على خريطة جوجل }
void _generateGoogleMapPolygons(List<dynamic> data) { void _generatePolygons(List<dynamic> data) {
Set<Polygon> tempPolygons = {}; Set<Polygon> tempPolygons = {};
// نصف قطر المربع (تقريباً 0.0005 يعادل 50-60 متر، مما يعطي مربع 100 متر)
// يجب أن يتناسب مع الـ precision المستخدم في PHP // الأوفست لرسم المربع (نصف حجم الشبكة)
// الشبكة دقتها 0.01 درجة، لذا نصفها 0.005
double offset = 0.005; double offset = 0.005;
for (var point in data) { for (var point in data) {
double lat = double.parse(point['lat'].toString()); double lat = double.parse(point['lat'].toString());
double lng = double.parse(point['lng'].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; Color color;
if (count >= 5) { Color strokeColor;
color = Colors.red.withOpacity(0.5); // عالي جداً
} else if (count >= 3) { // 🧠 منطق الألوان: ندمج الذكاء (Intensity) مع العدد (Count)
color = Colors.orange.withOpacity(0.5); // متوسط 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 { } else {
color = Colors.green.withOpacity(0.4); // منخفض // منطقة خفيفة (أصفر)
color = Colors.yellow.withOpacity(0.3);
strokeColor = Colors.yellow.withOpacity(0.6);
} }
// إنشاء المربع // رسم المربع
tempPolygons.add(Polygon( tempPolygons.add(Polygon(
polygonId: PolygonId("$lat-$lng"), polygonId: PolygonId("$lat-$lng"),
consumeTapEvents: true, // للسماح بالضغط عليه مستقبلاً
points: [ points: [
LatLng(lat - offset, lng - offset), LatLng(lat - offset, lng - offset),
LatLng(lat + offset, lng - offset), LatLng(lat + offset, lng - offset),
@@ -144,13 +158,19 @@ class HomeCaptainController extends GetxController {
LatLng(lat - offset, lng + offset), LatLng(lat - offset, lng + offset),
], ],
fillColor: color, fillColor: color,
strokeColor: color.withOpacity(0.8), strokeColor: strokeColor,
strokeWidth: 1, strokeWidth: 2,
visible: true,
)); ));
} }
heatmapPolygons = tempPolygons; heatmapPolygons = tempPolygons;
update(); // تحديث الخريطة
}
// دالة لتشغيل الخريطة الحرارية كل فترة (مثلاً عند فتح الصفحة)
void startHeatmapCycle() {
fetchAndDrawHeatmap();
// يمكن تفعيل Timer هنا لو أردت تحديثها تلقائياً كل 5 دقائق
} }
void goToWalletFromConnect() { void goToWalletFromConnect() {
@@ -179,11 +199,14 @@ class HomeCaptainController extends GetxController {
String stringActiveDuration = ''; String stringActiveDuration = '';
void onButtonSelected() { void onButtonSelected() {
// totalPoints = Get.find<CaptainWalletController>().totalPoints; // تم الإصلاح: التأكد من أن المتحكم موجود قبل استخدامه لتجنب الكراش
if (!Get.isRegistered<CaptainWalletController>()) {
Get.put(CaptainWalletController());
}
totalPoints = Get.find<CaptainWalletController>().totalPoints;
isActive = !isActive; isActive = !isActive;
if (isActive) { if (isActive) {
if (double.parse(totalPoints) > -30000) { if (double.parse(totalPoints) > -200) {
locationController.startLocationUpdates(); locationController.startLocationUpdates();
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
// locationBackController.startBackLocation(); // 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 { void getRefusedOrderByCaptain() async {
DateTime today = DateTime.now(); DateTime today = DateTime.now();
int todayDay = today.day; int todayDay = today.day;
@@ -232,7 +356,8 @@ class HomeCaptainController extends GetxController {
await sql.getCustomQuery(customQuery); await sql.getCustomQuery(customQuery);
countRefuse = results[0]['count'].toString(); countRefuse = results[0]['count'].toString();
update(); 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(); locationController.stopLocationUpdates();
activeStartTime = null; activeStartTime = null;
activeTimer?.cancel(); activeTimer?.cancel();
@@ -322,9 +447,9 @@ class HomeCaptainController extends GetxController {
isLoading = true; isLoading = true;
update(); update();
// This ensures we try to get a fix, but map doesn't crash if it fails // 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) { if (loc.latitude != 0) {
myLocation = loc; 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 @override
void onInit() async { void onInit() async {
// ✅ طلب الإذونات أولاً
bool permissionsGranted = await PermissionsHelper.requestAllPermissions();
if (permissionsGranted) {
// ✅ بدء الخدمة بعد الحصول على الإذونات
await BackgroundServiceHelper.startService();
print('✅ Background service started successfully');
} else {
print('❌ لم يتم منح الإذونات - الخدمة لن تعمل');
// اعرض رسالة للمستخدم
}
// await locationBackController.requestLocationPermission(); // await locationBackController.requestLocationPermission();
Get.put(FirebaseMessagesController()); Get.put(FirebaseMessagesController());
addToken(); addToken();
@@ -374,24 +526,34 @@ class HomeCaptainController extends GetxController {
getCaptainWalletFromBuyPoints(); getCaptainWalletFromBuyPoints();
// onMapCreated(mapHomeCaptainController!); // onMapCreated(mapHomeCaptainController!);
// totalPoints = Get.find<CaptainWalletController>().totalPoints.toString(); // totalPoints = Get.find<CaptainWalletController>().totalPoints.toString();
getRefusedOrderByCaptain(); // getRefusedOrderByCaptain();
// 🔥 الفحص عند تشغيل التطبيق
checkAndShowBlockDialog();
box.write(BoxName.statusDriverLocation, 'off'); box.write(BoxName.statusDriverLocation, 'off');
// 2. عدل الليسنر ليصبح مشروطاً
locationController.addListener(() { locationController.addListener(() {
// Only animate if active, map is ready, AND location is valid (not 0,0) // الشرط الذهبي: إذا كانت الصفحة غير نشطة أو الخريطة غير موجودة، لا تفعل شيئاً
if (isActive && mapHomeCaptainController != null) { if (!isHomeMapActive || mapHomeCaptainController == null || isClosed)
var loc = locationController.myLocation; return;
if (isActive) {
// isActive الخاصة بالزر "متصل/غير متصل"
var loc = locationController.myLocation;
if (loc.latitude != 0 && loc.longitude != 0) { if (loc.latitude != 0 && loc.longitude != 0) {
mapHomeCaptainController!.animateCamera( try {
CameraUpdate.newCameraPosition( mapHomeCaptainController!.animateCamera(
CameraPosition( CameraUpdate.newCameraPosition(
target: loc, CameraPosition(
zoom: 17.5, target: loc,
tilt: 50.0, zoom: 17.5,
bearing: locationController.heading, 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:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart'; import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:get/get.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: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 '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 { import '../../../constant/box_name.dart';
double progress = 0; import '../../../constant/links.dart';
double progressSpeed = 0; 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 duration = 15;
int durationSpeed = 20; int remainingTime = 15;
int remainingTime = 0; Timer? _timer;
int remainingTimeSpeed = 0;
String countRefuse = '0';
bool applied = false; bool applied = false;
final locationController = Get.put(LocationController()); final locationController = Get.put(LocationController());
BitmapDescriptor startIcon = BitmapDescriptor.defaultMarker;
BitmapDescriptor endIcon = BitmapDescriptor.defaultMarker; // 🔥 متغير لمنع تكرار القبول
final arguments = Get.arguments; bool _isRideTakenHandled = false;
var myList;
late int hours; // --- الأيقونات والماركرز ---
late int minutes; BitmapDescriptor? driverIcon;
GoogleMapController? mapController; // Make it nullable 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 @override
Future<void> onInit() async { Future<void> onInit() async {
print('OrderRequestController onInit called'); // 🛑 حماية من الفتح المتكرر لنفس الطلب
await initializeOrderPage(); if (Get.arguments == null) {
if (Platform.isAndroid) { print("❌ OrderController Error: No arguments received.");
bool isOverlayActive = await FlutterOverlayWindow.isActive(); Get.back(); // إغلاق الصفحة فوراً
if (isOverlayActive) { return;
await FlutterOverlayWindow.closeOverlay();
}
} }
addCustomStartIcon();
addCustomEndIcon();
startTimer(
myList[6].toString(),
myList[16].toString(),
);
update();
super.onInit(); 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; // 🔥🔥🔥 Smart Data Handling (List & Map Support) 🔥🔥🔥
late String body; // ----------------------------------------------------------------------
late double latPassengerLocation;
late double lngPassengerLocation;
late double lngPassengerDestination;
late double latPassengerDestination;
Future<void> initializeOrderPage() async { void _initializeData() {
final myListString = Get.arguments['myListString']; var args = Get.arguments;
Log.print('myListString0000: ${myListString}'); print("📦 Order Controller Received Type: ${args.runtimeType}");
print("📦 Order Controller Data: $args");
if (Get.arguments['DriverList'] == null || if (args != null) {
Get.arguments['DriverList'].isEmpty) { // الحالة 1: قائمة مباشرة (Legacy / Some Firebase formats)
myList = jsonDecode(myListString); if (args is List) {
Log.print('myList from myListString: ${myList}'); myList = args;
} else { }
myList = Get.arguments['DriverList']; // الحالة 2: خريطة (Map)
Log.print('myList from DriverList: ${myList}'); else if (args is Map) {
} // أ) هل هي قادمة من Firebase وتحتوي على DriverList؟
if (args.containsKey('DriverList')) {
body = Get.arguments['body']; var listData = args['DriverList'];
Duration durationToAdd = if (listData is List) {
Duration(seconds: (double.tryParse(myList[4]) ?? 0).toInt()); myList = listData;
hours = durationToAdd.inHours; } else if (listData is String) {
minutes = (durationToAdd.inMinutes % 60).round(); // أحياناً تصل كنص مشفر داخل الـ Map
startTimerSpeed(myList[6].toString(), body.toString()); try {
myList = jsonDecode(listData);
// --- Using the provided logic for initialization --- } catch (e) {
var cords = myList[0].toString().split(','); print("Error decoding DriverList: $e");
var cordDestination = myList[1].toString().split(','); }
}
double? parseDouble(String value) { }
try { // ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟
return double.parse(value); else {
} catch (e) { myMapData = args;
Log.print("Error parsing value: $value"); }
return null; // or handle the error appropriately
} }
} }
latPassengerLocation = parseDouble(cords[0]) ?? 0.0; // تعبئة الإحداثيات باستخدام الدالة الذكية _getValueAt
lngPassengerLocation = parseDouble(cords[1]) ?? 0.0; latPassenger = _parseCoord(_getValueAt(0));
latPassengerDestination = parseDouble(cordDestination[0]) ?? 0.0; lngPassenger = _parseCoord(_getValueAt(1));
lngPassengerDestination = parseDouble(cordDestination[1]) ?? 0.0; latDestination = _parseCoord(_getValueAt(3));
lngDestination = _parseCoord(_getValueAt(4));
pointsDirection = [ print(
LatLng(latPassengerLocation, lngPassengerLocation), "📍 Parsed Coordinates: Pax($latPassenger, $lngPassenger) -> Dest($latDestination, $lngDestination)");
LatLng(latPassengerDestination, lngPassengerDestination) }
/// 🔥 دالة ذكية تجلب القيمة سواء كانت البيانات في 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(); 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) { void onMapCreated(GoogleMapController controller) {
mapController = controller; mapController = controller;
animateCameraToBounds();
} }
void calculateBounds() { // --- قبول الطلب وإدارة التايمر ---
double minLat = math.min(latPassengerLocation, latPassengerDestination); void startTimer() {
double maxLat = math.max(latPassengerLocation, latPassengerDestination); _timer?.cancel();
double minLng = math.min(lngPassengerLocation, lngPassengerDestination); remainingTime = duration;
double maxLng = math.max(lngPassengerLocation, lngPassengerDestination); _playAudio();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
bounds = LatLngBounds( if (remainingTime <= 0) {
southwest: LatLng(minLat, minLng), timer.cancel();
northeast: LatLng(maxLat, maxLng), _stopAudio();
); if (!applied) Get.back();
Log.print('Calculated Bounds: $bounds'); } else {
} remainingTime--;
progress = remainingTime / duration;
void animateCameraToBounds() { update();
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 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() { void _stopAudio() => audioPlayer.stop();
_timerActive = false;
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 { // Lifecycle
for (int i = 0; i <= durationSpeed; i++) { @override
await Future.delayed(const Duration(seconds: 1)); void didChangeAppLifecycleState(AppLifecycleState state) {
progressSpeed = i / durationSpeed; super.didChangeAppLifecycleState(state);
remainingTimeSpeed = durationSpeed - i; if (state == AppLifecycleState.paused ||
update(); 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();
} 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( @override
orderID, void onClose() {
String startLocation, locationController.socket?.off('ride_taken');
String endLocation, audioPlayer.dispose();
String date, WidgetsBinding.instance.removeObserver(this);
String time, _timer?.cancel();
String price, mapController?.dispose();
String passengerId, super.onClose();
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,
});
} }
} }

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 '../../constant/box_name.dart';
import '../../main.dart'; import '../../main.dart';
import '../../onbording_page.dart';
import '../../print.dart'; import '../../print.dart';
import '../functions/encrypt_decrypt.dart'; import '../functions/encrypt_decrypt.dart';
import '../functions/secure_storage.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). /// is expected to be handled by an internal process (like login).
Future<Widget?> _getNavigationTarget() async { Future<Widget?> _getNavigationTarget() async {
try { 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 isDriverDataAvailable = box.read(BoxName.phoneDriver) != null;
// final isPhoneVerified = box.read(BoxName.phoneVerified).toString() == '1'; // <-- ⛔️ تم حذف هذا السطر if (!isDriverDataAvailable) {
return LoginCaptin();
}
final loginController = Get.put(LoginDriverController()); final loginController = Get.put(LoginDriverController());
// ✅ --- (الحل) --- final AppInitializer initializer = AppInitializer();
// تم حذف التحقق من "isPhoneVerified" await initializer.initializeApp();
// هذا يسمح لـ "loginWithGoogleCredential" بتحديد الحالة والتوجيه الصحيح await EncryptionHelper.initialize();
// (إلى Home أو DriverVerificationScreen أو PhoneNumberScreen)
if (isDriverDataAvailable) {
Log.print('المستخدم مسجل. جارٍ تهيئة الجلسة...');
// الخطوة 1: ضمان جلب الـ JWT أولاً await loginController.loginWithGoogleCredential(
// (هذا هو الكود الذي كان في main.dart) box.read(BoxName.driverID).toString(),
final AppInitializer initializer = AppInitializer(); box.read(BoxName.emailDriver).toString(),
await initializer.initializeApp(); );
await EncryptionHelper.initialize();
// انتظر حتى ينتهي جلب الـ JWT
Log.print('تم جلب الـ JWT. جارٍ تسجيل الدخول ببيانات جوجل...'); return null; // لأن loginWithGoogleCredential يوجّه
// الخطوة 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();
}
} catch (e) { } catch (e) {
Log.print("Error during navigation logic: $e"); Log.print("Error during navigation logic: $e");
// أي خطأ فادح (مثل خطأ في جلب الـ JWT) سيعيدك للدخول
return LoginCaptin(); return LoginCaptin();
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,208 +1,178 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'package:flutter/material.dart';
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:get/get.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 '../../constant/links.dart';
import '../../main.dart'; // للوصول لـ box
import '../functions/crud.dart'; import '../functions/crud.dart';
import '../functions/location_controller.dart'; import '../functions/location_controller.dart';
class RideAvailableController extends GetxController { class RideAvailableController extends GetxController {
bool isLoading = false; 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) { // RxList: أي تغيير هنا سينعكس فوراً على الشاشة
const double earthRadius = 6378137.0; // Earth's radius in meters RxList<Map<String, dynamic>> availableRides = <Map<String, dynamic>>[].obs;
double latDelta = (radiusInMeters / earthRadius) * (180 / pi); DateTime? _lastFetchTime;
double lngDelta = static const _cacheDuration = Duration(seconds: 5); // تقليل مدة الكاش قليلاً
(radiusInMeters / (earthRadius * cos(pi * lat / 180))) * (180 / pi);
double minLat = lat - latDelta; // مشغل الصوت
double maxLat = lat + latDelta; final AudioPlayer _audioPlayer = AudioPlayer();
double minLng = lng - lngDelta;
double maxLng = lng + lngDelta;
minLat = max(-90.0, minLat); @override
maxLat = min(90.0, maxLat); void onInit() {
super.onInit();
minLng = (minLng + 180) % 360 - 180; // 1. جلب القائمة الأولية من السيرفر (HTTP)
maxLng = (maxLng + 180) % 360 - 180; getRideAvailable(forceRefresh: true);
if (minLng > maxLng) { // 2. تفعيل الاستماع المباشر للتحديثات (Socket)
double temp = minLng; _initSocketListeners();
minLng = maxLng; }
maxLng = temp;
@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 { try {
isLoading = true; if (forceRefresh) {
update(); isLoading = true;
update();
}
LatLngBounds bounds = calculateBounds( // الحصول على موقع السائق الحالي
Get.find<LocationController>().myLocation.latitude, final location = Get.find<LocationController>().myLocation;
Get.find<LocationController>().myLocation.longitude,
4000);
// 🔥 التعديل الجوهري: نرسل Lat/Lng فقط بدلاً من Bounds
var payload = { var payload = {
'southwestLat': bounds.southwest.latitude.toString(), 'lat': location.latitude.toString(),
'southwestLon': bounds.southwest.longitude.toString(), 'lng': location.longitude.toString(),
'northeastLat': bounds.northeast.latitude.toString(), 'radius': '50', // نصف القطر بالكيلومتر (كما حددناه في السيرفر)
'northeastLon': bounds.northeast.longitude.toString(),
}; };
var res = var res =
await CRUD().get(link: AppLink.getRideWaiting, payload: payload); await CRUD().get(link: AppLink.getRideWaiting, payload: payload);
isLoading = false; // Request is complete, stop loading indicator. isLoading = false;
_lastFetchTime = DateTime.now();
if (res != 'failure') { if (res != 'failure') {
final decodedResponse = jsonDecode(res); final decodedResponse = jsonDecode(res);
// Check for valid response structure
if (decodedResponse is Map && if (decodedResponse is Map && decodedResponse['status'] == 'success') {
decodedResponse.containsKey('message') && final rides = decodedResponse['message'];
decodedResponse['message'] is List) { if (rides is List) {
rideAvailableMap = decodedResponse; // تحويل البيانات وتخزينها
// If the list of rides is empty, show the "No Rides" dialog availableRides.value = List<Map<String, dynamic>>.from(rides);
if ((rideAvailableMap['message'] as List).isEmpty) { } else {
_showDialogAfterBuild(_buildNoRidesDialog()); availableRides.clear();
} }
} else { } else {
// If response format is unexpected, treat as no rides and show dialog availableRides.clear();
rideAvailableMap = {'message': []};
_showDialogAfterBuild(_buildNoRidesDialog());
} }
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) { } catch (e) {
isLoading = false; isLoading = false;
update(); update();
// This catches other exceptions like JSON parsing errors print("Error fetching rides: $e");
_showDialogAfterBuild(
_buildErrorDialog("An unexpected error occurred.".tr));
} }
} }
// Extracted dialogs into builder methods for cleanliness. // ========================================================================
Widget _buildNoRidesDialog() { // 2. الاستماع للسوكيت (Real-time Updates) ⚡
return CupertinoAlertDialog( // ========================================================================
title: Column( void _initSocketListeners() {
mainAxisSize: MainAxisSize.min, var locationCtrl = Get.find<LocationController>();
children: [ var socket = locationCtrl.socket;
const Icon(
CupertinoIcons.car, if (socket == null) {
size: 44, print("⚠️ Socket is null in RideAvailableController");
color: CupertinoColors.systemGrey, return;
), }
const SizedBox(height: 12),
Text( // A. عند وصول رحلة جديدة للسوق (market_new_ride)
"No Rides Available".tr, socket.on('market_new_ride', (data) {
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600), print("🔔 Socket: New Ride Market: $data");
),
], if (data != null && data is Map) {
), // فلترة: هل نوع السيارة يناسبني؟
content: Padding( if (_isCarTypeMatch(data['carType'])) {
padding: const EdgeInsets.only(top: 8), // منع التكرار (إذا كانت الرحلة موجودة مسبقاً)
child: Text( bool exists = availableRides
"Please check back later for available rides.".tr, .any((r) => r['id'].toString() == data['id'].toString());
style:
const TextStyle(fontSize: 13, color: CupertinoColors.systemGrey), if (!exists) {
), // إضافة الرحلة لأعلى القائمة
), availableRides.insert(0, Map<String, dynamic>.from(data));
actions: [
CupertinoDialogAction( // تشغيل صوت تنبيه (Bling) 🎵
onPressed: () { _playNotificationSound();
Get.back(); // Close dialog }
Get.back(); // Go back from AvailableRidesPage }
}, }
child: Text('OK'.tr), });
),
], // 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. bool _isCarTypeMatch(String? rideCarType) {
// print("Error fetching rides: $error"); if (rideCarType == null) return false;
return CupertinoAlertDialog( String myDriverType = box.read(BoxName.carTypeOfDriver).toString();
title: const Icon(
CupertinoIcons.exclamationmark_triangle_fill, // منطق التوزيع الهرمي
color: CupertinoColors.systemRed, switch (myDriverType) {
size: 44, case 'Comfort':
), return ['Speed', 'Comfort', 'Fixed Price'].contains(rideCarType);
content: Text( case 'Speed':
error, // Display the specific error message passed to the function case 'Scooter':
style: const TextStyle(fontSize: 14), case 'Awfar Car':
), return rideCarType == myDriverType;
actions: [ case 'Lady':
CupertinoDialogAction( return ['Comfort', 'Speed', 'Lady'].contains(rideCarType);
onPressed: () { default:
Get.back(); // Close dialog return true; // احتياطياً
Get.back(); // Go back from AvailableRidesPage }
},
child: Text('OK'.tr),
),
],
);
} }
@override // تشغيل صوت التنبيه
void onInit() { Future<void> _playNotificationSound() async {
super.onInit(); try {
getRideAvailable(); // تأكد من وجود الملف في 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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
// تأكد من استيراد الملفات الصحيحة حسب مشروع السائق الخاص بك
import 'package:sefer_driver/constant/links.dart'; import 'package:sefer_driver/constant/links.dart';
import 'package:sefer_driver/controller/functions/crud.dart'; import 'package:sefer_driver/controller/functions/crud.dart';
import '../../../constant/box_name.dart'; import '../../../constant/box_name.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../../print.dart'; // import '../../../print.dart'; // إذا كنت تستخدمه
// ... (PaymentService class remains unchanged) ... // --- خدمة الدفع للسائق (نفس المنطق الخاص بالسائق) ---
class PaymentService { class PaymentService {
final String _baseUrl = "${AppLink.paymentServer}/ride/shamcash"; final String _baseUrl = "${AppLink.paymentServer}/ride/shamcash";
@@ -18,7 +19,7 @@ class PaymentService {
final response = await CRUD().postWallet( final response = await CRUD().postWallet(
link: url, link: url,
payload: { payload: {
'driverID': box.read(BoxName.driverID), 'driverID': box.read(BoxName.driverID), // استخدام driverID
'amount': amount.toString(), 'amount': amount.toString(),
}, },
).timeout(const Duration(seconds: 15)); ).timeout(const Duration(seconds: 15));
@@ -74,7 +75,6 @@ class PaymentScreenSmsProvider extends StatefulWidget {
this.providerName = 'شام كاش', this.providerName = 'شام كاش',
this.providerLogo = 'assets/images/shamCash.png', this.providerLogo = 'assets/images/shamCash.png',
this.qrImagePath = 'assets/images/shamcashsend.png', this.qrImagePath = 'assets/images/shamcashsend.png',
// removed paymentPhoneNumber
}); });
@override @override
@@ -82,21 +82,47 @@ class PaymentScreenSmsProvider extends StatefulWidget {
_PaymentScreenSmsProviderState(); _PaymentScreenSmsProviderState();
} }
class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> { class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider>
with SingleTickerProviderStateMixin {
final PaymentService _paymentService = PaymentService(); final PaymentService _paymentService = PaymentService();
Timer? _pollingTimer; Timer? _pollingTimer;
PaymentStatus _status = PaymentStatus.creatingInvoice; PaymentStatus _status = PaymentStatus.creatingInvoice;
String? _invoiceNumber; String? _invoiceNumber;
// العنوان الثابت للدفع (كما في تطبيق الراكب)
final String _paymentAddress = "80f23afe40499b02f49966c3340ae0fc";
// متغيرات الأنيميشن (الوميض)
late AnimationController _blinkController;
late Animation<Color?> _colorAnimation;
late Animation<double> _shadowAnimation;
@override @override
void initState() { void initState() {
super.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(); _createAndPollInvoice();
} }
@override @override
void dispose() { void dispose() {
_pollingTimer?.cancel(); _pollingTimer?.cancel();
_blinkController.dispose();
super.dispose(); super.dispose();
} }
@@ -214,10 +240,10 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// 1. المبلغ (تصميم مميز) // 1. المبلغ المطلوب
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 15), padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [Colors.blue.shade800, Colors.blue.shade600]), colors: [Colors.blue.shade800, Colors.blue.shade600]),
@@ -231,64 +257,155 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
), ),
child: Column( child: Column(
children: [ children: [
const Text("المبلغ المطلوب", const Text("المبلغ المطلوب شحنه",
style: TextStyle(color: Colors.white70, fontSize: 14)), style: TextStyle(color: Colors.white70, fontSize: 14)),
const SizedBox(height: 8), const SizedBox(height: 5),
Text( Text(
"${currencyFormat.format(widget.amount)} ل.س", "${currencyFormat.format(widget.amount)} ل.س",
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 32, fontSize: 28,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
], ],
), ),
), ),
const SizedBox(height: 30), const SizedBox(height: 25),
// 2. التعليمات والنسخ (الجزء الأهم) // 2. رقم البيان (الإطار الأحمر الوامض)
Container( AnimatedBuilder(
padding: const EdgeInsets.all(20), animation: _blinkController,
decoration: BoxDecoration( builder: (context, child) {
color: Colors.white, return Container(
borderRadius: BorderRadius.circular(16), padding: const EdgeInsets.all(20),
border: Border.all(color: Colors.grey.shade200), decoration: BoxDecoration(
boxShadow: [ color: Colors.white,
BoxShadow( borderRadius: BorderRadius.circular(16),
color: Colors.grey.shade100, border: Border.all(
blurRadius: 10, color: _colorAnimation.value ?? Colors.red,
offset: const Offset(0, 4)) width: 3.0, // إطار سميك
], ),
), boxShadow: [
child: Column( BoxShadow(
children: [ color: (_colorAnimation.value ?? Colors.red)
Row( .withOpacity(0.4),
blurRadius: _shadowAnimation.value,
spreadRadius: 2,
)
],
),
child: Column(
children: [ children: [
Container( Row(
padding: const EdgeInsets.all(8), mainAxisAlignment: MainAxisAlignment.center,
decoration: BoxDecoration( children: [
color: Colors.orange.shade50, shape: BoxShape.circle), Icon(Icons.warning_rounded,
child: Icon(Icons.priority_high_rounded, color: Colors.red.shade800, size: 28),
color: Colors.orange.shade800, size: 20), const SizedBox(width: 8),
Text(
"هام جداً: لا تنسَ!",
style: TextStyle(
color: Colors.red.shade900,
fontWeight: FontWeight.bold,
fontSize: 18),
),
],
), ),
const SizedBox(width: 12), const SizedBox(height: 10),
const Expanded( const Text(
child: Text( "يجب نسخ (رقم البيان) هذا ووضعه في تطبيق شام كاش لضمان نجاح العملية.",
"انسخ الرقم أدناه وضعه في خانة (الملاحظات) عند الدفع.", textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w600), 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( InkWell(
onTap: () { onTap: () {
Clipboard.setData(ClipboardData(text: invoiceText)); Clipboard.setData(ClipboardData(text: _paymentAddress));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: const Text("تم نسخ رقم البيان", content: const Text("تم نسخ عنوان الدفع",
textAlign: TextAlign.center), textAlign: TextAlign.center),
backgroundColor: Colors.green.shade600, backgroundColor: Colors.green.shade600,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
@@ -298,36 +415,23 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
), ),
); );
}, },
borderRadius: BorderRadius.circular(12), child: Row(
child: Container( children: [
padding: const EdgeInsets.symmetric( Expanded(
horizontal: 15, vertical: 12), child: Text(
decoration: BoxDecoration( _paymentAddress,
color: Colors.grey.shade50, style: const TextStyle(
borderRadius: BorderRadius.circular(12), fontSize: 14,
border: fontWeight: FontWeight.bold,
Border.all(color: Colors.blue.shade200, width: 1.5), fontFamily: 'Courier',
), color: Colors.black87,
child: Row( ),
mainAxisAlignment: MainAxisAlignment.spaceBetween, overflow: TextOverflow.ellipsis,
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)),
],
), ),
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), const SizedBox(height: 30),
// 3. الـ QR Code (قابل للاختيار/الضغط) // 4. الـ QR Code
const Text("امسح الرمز للدفع", const Text("امسح الرمز للدفع",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black87)), color: Colors.black87)),
const SizedBox(height: 15), const SizedBox(height: 10),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
// تأثير بصري بسيط عند الضغط (أو تكبير الصورة في Dialog)
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => Dialog( builder: (ctx) => Dialog(
@@ -358,32 +461,19 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
); );
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.grey.shade300), border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.grey.shade200,
blurRadius: 10,
spreadRadius: 2)
],
), ),
child: Column( child: Image.asset(
children: [ widget.qrImagePath,
Image.asset( width: 150,
widget.qrImagePath, height: 150,
width: 180, fit: BoxFit.contain,
height: 180, errorBuilder: (c, o, s) =>
fit: BoxFit.contain, const Icon(Icons.qr_code_2, size: 100, color: Colors.grey),
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)),
],
), ),
), ),
), ),
@@ -410,7 +500,7 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
const Text("تم الدفع بنجاح!", const Text("تم الدفع بنجاح!",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 10), const SizedBox(height: 10),
const Text("تم إضافة الرصيد والمكافأة إلى حسابك", const Text("تم إضافة الرصيد إلى محفظتك",
style: TextStyle(color: Colors.grey)), style: TextStyle(color: Colors.grey)),
const SizedBox(height: 40), const SizedBox(height: 40),
SizedBox( 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 'package:sefer_driver/views/widgets/elevated_btn.dart';
import '../firebase/notification_service.dart'; import '../firebase/notification_service.dart';
import '../home/captin/home_captain_controller.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>() var paymentToken3 = await Get.find<MapDriverController>()
.generateTokenDriver((-1 * remainingFee).toString()); .generateTokenDriver((-1 * remainingFee).toString());
await CRUD().post(link: AppLink.addDrivePayment, payload: { await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
'rideId': 'remain$rideId', 'rideId': 'remain$rideId',
'amount': (-1 * remainingFee).toString(), 'amount': (-1 * remainingFee).toString(),
'payment_method': 'Remainder', 'payment_method': 'Remainder',
@@ -68,13 +69,6 @@ class RateController extends GetxController {
'token': paymentToken3, 'token': paymentToken3,
'driverID': box.read(BoxName.driverID).toString(), '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( NotificationService.sendNotification(
target: Get.find<MapDriverController>().tokenPassenger.toString(), target: Get.find<MapDriverController>().tokenPassenger.toString(),
title: 'Wallet Added'.tr, title: 'Wallet Added'.tr,
@@ -118,24 +112,13 @@ class RateController extends GetxController {
middleText: '', middleText: '',
confirm: MyElevatedButton(title: 'Ok', onPressed: () => Get.back())); confirm: MyElevatedButton(title: 'Ok', onPressed: () => Get.back()));
} else { } else {
await CRUD().post( await CRUD().post(link: "${AppLink.server}/ride/rate/add.php", payload: {
link: "${AppLink.seferCairoServer}/ride/rate/add.php", 'passenger_id': passengerId,
payload: { 'driverID': box.read(BoxName.driverID).toString(),
'passenger_id': passengerId, 'rideId': rideId.toString(),
'driverID': box.read(BoxName.driverID).toString(), 'rating': selectedRateItemId.toString(),
'rideId': rideId.toString(), 'comment': comment.text ?? 'none',
'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',
});
}
CRUD().sendEmail(AppLink.sendEmailToPassengerForTripDetails, { CRUD().sendEmail(AppLink.sendEmailToPassengerForTripDetails, {
'startLocation': 'startLocation':
@@ -153,10 +136,15 @@ class RateController extends GetxController {
'endNameLocation': 'endNameLocation':
Get.find<MapDriverController>().endNameLocation.toString(), Get.find<MapDriverController>().endNameLocation.toString(),
}); });
// homeCaptainController.isActive = true; if (Get.isRegistered<MapDriverController>()) {
// update(); Get.find<MapDriverController>()
// homeCaptainController.getPaymentToday(); .disposeEverything(); // الدالة التي أنشأناها في الخطوة 3
Get.delete<MapDriverController>(force: true); // حذف إجباري من الذاكرة
}
Get.offAll(HomeCaptain()); Get.offAll(HomeCaptain());
if (Get.isRegistered<HomeCaptainController>()) {
Get.find<HomeCaptainController>().resumeHomeMapUpdates();
}
} }
} }
} }

View File

@@ -16,17 +16,16 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart'; import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_stripe/flutter_stripe.dart'; import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:permission_handler/permission_handler.dart'; // ✅ جديد
import 'package:device_info_plus/device_info_plus.dart'; // ✅ جديد
import 'constant/api_key.dart'; import 'constant/api_key.dart';
import 'constant/info.dart'; import 'constant/info.dart';
import 'constant/notification.dart'; import 'constant/notification.dart';
import 'controller/firebase/firbase_messge.dart'; import 'controller/firebase/firbase_messge.dart';
import 'controller/firebase/local_notification.dart'; import 'controller/firebase/local_notification.dart';
import 'controller/functions/add_error.dart'; import 'controller/functions/background_service.dart';
import 'controller/functions/battery_status.dart';
import 'controller/functions/crud.dart'; import 'controller/functions/crud.dart';
import 'controller/functions/encrypt_decrypt.dart';
import 'controller/functions/secure_storage.dart';
import 'controller/local/local_controller.dart'; import 'controller/local/local_controller.dart';
import 'controller/local/translations.dart'; import 'controller/local/translations.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
@@ -41,7 +40,12 @@ final box = GetStorage();
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
DbSql sql = DbSql.instance; DbSql sql = DbSql.instance;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
const platform = MethodChannel('com.example.intaleq_driver/app_control'); const platform = MethodChannel('com.intaleq_driver/app_control');
// ✅ قنوات الإشعارات المطلوبة
const String backgroundServiceChannelId = 'driver_service_channel';
const String locationServiceChannelId = 'location_service_channel';
const String geolocatorChannelId = 'geolocator_channel';
/// تهيئة Firebase بوعي لمنع تهيئة مكرّرة على أندرويد (isolates متعددة) /// تهيئة Firebase بوعي لمنع تهيئة مكرّرة على أندرويد (isolates متعددة)
Future<void> initFirebaseIfNeeded() async { Future<void> initFirebaseIfNeeded() async {
@@ -53,6 +57,103 @@ Future<void> initFirebaseIfNeeded() async {
} }
} }
/// ✅ طلب إذن الإشعارات على Android 13+
Future<bool> requestNotificationPermission() async {
if (Platform.isAndroid) {
try {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt >= 33) {
final status = await Permission.notification.request();
if (status.isGranted) {
print('✅ Notification permission granted');
return true;
} else if (status.isDenied) {
print('⚠️ Notification permission denied');
return false;
} else if (status.isPermanentlyDenied) {
print('⚠️ Notification permission permanently denied');
await openAppSettings();
return false;
}
} else {
print('✅ Android < 13, no runtime notification permission needed');
return true;
}
} catch (e) {
print('❌ Error requesting notification permission: $e');
return false;
}
}
return true; // iOS
}
/// ✅ إنشاء جميع قنوات الإشعارات المطلوبة
Future<void> createAllNotificationChannels() async {
if (!Platform.isAndroid) return;
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
// قناة Background Service
const AndroidNotificationChannel backgroundChannel =
AndroidNotificationChannel(
backgroundServiceChannelId,
'خدمة السائق',
description: 'استقبال الطلبات في الخلفية',
importance: Importance.low,
playSound: false,
enableVibration: false,
showBadge: false,
);
// قناة Location Service
const AndroidNotificationChannel locationChannel = AndroidNotificationChannel(
locationServiceChannelId,
'خدمة الموقع',
description: 'تتبع موقع السائق',
importance: Importance.low,
playSound: false,
enableVibration: false,
showBadge: false,
);
// قناة Geolocator
const AndroidNotificationChannel geolocatorChannel =
AndroidNotificationChannel(
geolocatorChannelId,
'تحديد الموقع',
description: 'خدمة تحديد الموقع الجغرافي',
importance: Importance.low,
playSound: false,
enableVibration: false,
showBadge: false,
);
try {
// إنشاء جميع القنوات
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(backgroundChannel);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(locationChannel);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(geolocatorChannel);
print('✅ All notification channels created successfully');
} catch (e) {
print('❌ Error creating notification channels: $e');
}
}
/// ============ Handlers: Background ============ /// ============ Handlers: Background ============
@pragma('vm:entry-point') @pragma('vm:entry-point')
@@ -129,7 +230,6 @@ void notificationTapBackground(NotificationResponse notificationResponse) {
void overlayMain() async { void overlayMain() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await GetStorage.init(); await GetStorage.init();
// await initFirebaseIfNeeded();
if (!Get.isRegistered<NotificationController>()) { if (!Get.isRegistered<NotificationController>()) {
Get.put(NotificationController()); Get.put(NotificationController());
@@ -167,31 +267,39 @@ void main() {
DeviceOrientation.portraitDown, DeviceOrientation.portraitDown,
]); ]);
// سجل الهاندلر تبع رسائل الخلفية (لازم يكون Top-Level ومع @pragma) // ✅ الترتيب الصحيح: الإذونات → القنوات → الخدمات
// 1. طلب إذن الإشعارات أولاً (Android 13+)
bool notificationPermissionGranted = await requestNotificationPermission();
if (!notificationPermissionGranted) {
print('⚠️ تحذير: لم يتم منح إذن الإشعارات - قد لا تعمل بعض الميزات');
}
// 2. إنشاء جميع قنوات الإشعارات
await createAllNotificationChannels();
// 3. تهيئة الخدمة (بدون تشغيلها)
await BackgroundServiceHelper.initialize();
// 4. سجل الهاندلر تبع رسائل الخلفية
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler); FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
runApp(const MyApp()); runApp(const MyApp());
}, (error, stack) { }, (error, stack) {
// ==== START: ERROR FILTER ====
final errorString = error.toString(); final errorString = error.toString();
// اطبع كل شيء محلياً
// (يمكنك استبدال print بـ Log.print إن رغبت)
print("Caught Dart error: $error"); print("Caught Dart error: $error");
print(stack); print(stack);
// تجاهُل بعض الأخطاء المعروفة
final isIgnoredError = errorString.contains('PERMISSION_DENIED') || final isIgnoredError = errorString.contains('PERMISSION_DENIED') ||
errorString.contains('FormatException') || errorString.contains('FormatException') ||
errorString.contains('Null check operator used on a null value'); errorString.contains('Null check operator used on a null value');
if (!isIgnoredError) { if (!isIgnoredError) {
// أرسل فقط ما ليس ضمن قائمة التجاهل
CRUD.addError(error.toString(), stack.toString(), 'main'); CRUD.addError(error.toString(), stack.toString(), 'main');
} else { } else {
print("Ignoring error and not sending to server: $errorString"); print("Ignoring error and not sending to server: $errorString");
} }
// ==== END: ERROR FILTER ====
}); });
} }
@@ -228,8 +336,8 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
await FirebaseMessaging.instance.requestPermission(); await FirebaseMessaging.instance.requestPermission();
// يمكن أيضاً تفعيل foreground presentation options هنا لو احتجت
await NotificationController().initNotifications(); await NotificationController().initNotifications();
// Generate a random index to pick a message // Generate a random index to pick a message
final random = Random(); final random = Random();
final randomMessage = final randomMessage =
@@ -259,7 +367,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
initialRoute: '/', initialRoute: '/',
getPages: [ getPages: [
GetPage(name: '/', page: () => SplashScreen()), GetPage(name: '/', page: () => SplashScreen()),
GetPage(name: '/order-page', page: () => OrderRequestPage()), GetPage(name: '/OrderRequestPage', page: () => OrderRequestPage()),
GetPage( GetPage(
name: '/passenger-location-map', name: '/passenger-location-map',
page: () => PassengerLocationMapPage(), page: () => PassengerLocationMapPage(),

View File

@@ -1,10 +1,12 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class OverlayMethodChannel { class OverlayMethodChannel {
static const _channel = MethodChannel('com.sefer_driver/app_control'); // 1. تم تصحيح اسم القناة ليتطابق مع ملف MainActivity.kt [1]
static const _channel = MethodChannel('com.intaleq_driver/app_control');
static Future<void> bringToForeground() async { static Future bringToForeground() async {
try { try {
// استدعاء الدالة المعرفة في الكوتلن [3]
await _channel.invokeMethod('bringToForeground'); await _channel.invokeMethod('bringToForeground');
} on PlatformException catch (e) { } on PlatformException catch (e) {
print('Error bringing app to foreground: $e'); print('Error bringing app to foreground: $e');

133
lib/readme.md Normal file
View File

@@ -0,0 +1,133 @@
إليك التوثيق التقني العميق (Deep Technical Documentation) لتطبيق السائق (Driver App) في منصة **انطلق (Intaleq)**. تم إعداد هذا التقرير بناءً على تحليل الكود المصدري، مع التركيز على البنية التحتية، تدفق البيانات، والخدمات الخلفية.
---
# 📘 Intaleq Driver App - Technical Documentation
**الإصدار:** 1.0
**المعمارية:** MVC using GetX
**اللغة:** Dart (Flutter)
**إدارة الحالة:** GetX (Reactive State Management)
---
## 1. 🔐 التسجيل والتحقق (Onboarding & KYC)
نظام التسجيل في تطبيق السائق معقد لأنه يتطلب التحقق من الهوية والأهلية قبل السماح للسائق بالعمل.
### أ. دورة تسجيل السائق (`Driver Registration Flow`)
- **المسار:** `lib/controller/auth/captin/register_captin_controller.dart`
- **الآلية:**
1. **التحقق من الهاتف:** يتم استخدام `SmsEgyptController` أو `PhoneAuthHelper` لإرسال OTP. يتم التحقق مما إذا كان الرقم مسجلاً مسبقاً عبر `checkPhoneNumberISVerfiedDriver`.
2. **إدخال البيانات الأساسية:** (الاسم، البريد، تاريخ الميلاد) يتم جمعها في `RegistrationView`.
3. **إنشاء الحساب:** يتم استدعاء `signUpCaptin.php` لإنشاء سجل أولي للسائق بحالة `yet` (بانتظار التوثيق).
### ب. نظام رفع الوثائق والمعالجة بالذكاء الاصطناعي (AI & OCR)
- **المسؤوليـة:** `lib/controller/functions/gemeni.dart` (الكلاس `AI`).
- **العملية التقنية:**
1. **التقاط الصورة:** يتم استخدام `ImagePicker` و `ImageCropper` لضمان جودة الصورة.
2. **الضغط:** يتم ضغط الصورة باستخدام `FlutterImageCompress` لتقليل استهلاك البيانات.
3. **التحليل (AI Extraction):**
- يتم رفع الصورة إلى `uploadImageToAi`.
- يتم تمرير `prompt` هندسي دقيق لنموذج الذكاء الاصطناعي (مثل Gemini أو نماذج OCR مخصصة) لاستخراج البيانات كـ JSON (مثل: `vin`, `make`, `model`, `plate_number`).
4. **التعبئة التلقائية:** البيانات المستخرجة تُعبأ تلقائياً في `RegisterCaptainController` ليقوم السائق بمراجعتها فقط، مما يقلل أخطاء الإدخال اليدوي.
### ج. حالة "بانتظار الموافقة" (`Pending Approval`)
- **المسؤوليـة:** `LoginDriverController` و `DriverVerificationScreen`.
- **المنطق:**
- عند تسجيل الدخول، يتحقق النظام من الحقل `status` و `is_verified` في استجابة الـ login.
- إذا كانت الحالة `yet`، يتم توجيه السائق إجبارياً إلى شاشة `DriverVerificationScreen` التي تعرض رسالة "Your Application is Under Review" وتمنع الوصول للخريطة.
---
## 2. 📡 الخدمات الخلفية والتتبع (Background Services & Tracking)
يعتمد التطبيق على نظام تتبع ذكي (Batch Tracking) لتقليل استهلاك البطارية والبيانات مع الحفاظ على الدقة.
### أ. محرك الموقع (`LocationController`)
- **المسار:** `lib/controller/functions/location_controller.dart`.
- **آلية العمل في الخلفية:**
1. **التهيئة:** يتم تفعيل `location.enableBackgroundMode(enable: true)` لضمان استمرار الخدمة عند إغلاق الشاشة.
2. **التخزين المؤقت (Buffering):** بدلاً من إرسال كل نقطة GPS للسيرفر، يتم تخزين النقاط في قائمة محلية `_trackBuffer`.
3. **التصفية الذكية (Smart Filtering):** لا يتم تسجيل النقطة إلا إذا تحرك السائق مسافة معينة (`onMoveMetersNormal = 15m`) أو مرّ وقت محدد (`30 ثانية`) لضمان تسجيل التوقفات.
### ب. إرسال البيانات (Batch Upload)
- يوجد مؤقت `_uploadTimer` يعمل كل **دقيقتين** (في الوضع العادي).
- يقوم هذا المؤقت بجمع كل النقاط في `_trackBuffer`، تحويلها لـ JSON، وإرسالها بطلب واحد `POST` إلى `add_batch.php`. هذا يقلل الحمل على السيرفر بنسبة كبيرة جداً.
### ج. وضع توفير الطاقة (Power Saving Mode)
- يراقب التطبيق حالة البطارية عبر `BatteryNotifier`. إذا انخفضت عن 20%، يتم تقليل معدل التحديث (تسجيل كل 6 ثواني ورفع كل 4 دقائق).
---
## 3. 🔔 استقبال الطلبات (Request Handling)
### أ. النافذة العائمة (`Overlay Window`)
- **التقنية:** `flutter_overlay_window`.
- **المسار:** `lib/main.dart` (Background Handler) و `lib/views/home/Captin/orderCaptin/order_over_lay.dart`.
- **كيفية العمل:**
1. عند وصول إشعار FCM من نوع `Order` والتطبيق في الخلفية، يتم استدعاء `backgroundMessageHandler`.
2. يتم التحقق من إذن الرسم فوق التطبيقات. إذا مُنح، يتم استدعاء `FlutterOverlayWindow.showOverlay` وتمرير بيانات الطلب (`DriverList`).
3. ملف `order_over_lay.dart` هو تطبيق Flutter مصغر يعمل بشكل مستقل فوق التطبيقات الأخرى. يحتوي على منطق قبول/رفض خاص به ويتصل بالسيرفر مباشرة.
### ب. منطق القبول والرفض (`OrderRequestController`)
- **المسار:** `lib/controller/home/captin/order_request_controller.dart`.
- **العداد التنازلي:** يتم تشغيل `startTimer` لمدة 15-20 ثانية. إذا انتهى الوقت، يتم رفض الطلب تلقائياً.
- **القبول:** عند الضغط على "قبول"، يتم استدعاء `updateStausFromSpeed.php` لحجز الطلب ومنع تضارب السائقين (Race Condition).
---
## 4. 🚗 تنفيذ الرحلة (Trip Execution Workflow)
يدير `MapDriverController` دورة حياة الرحلة بالكامل.
### أ. الذهاب للراكب (`Going to Passenger`)
- عند قبول الطلب، تتغير الحالة إلى `Apply`.
- يتم عرض موقع الراكب على الخريطة ورسم المسار باستخدام `getRoute` (يعتمد على OSRM أو Google Directions).
- يمكن للسائق فتح خرائط جوجل الخارجية عبر `openGoogleMapFromDriverToPassenger`.
### ب. الوصول (`Arrived`)
- **الزر:** `I Arrive`.
- **المنطق البرمجي:** يتحقق الكود أولاً من المسافة بين السائق والراكب. إذا كانت أقل من **140 متر** (`distance < 140`)، يتم إرسال إشعار `Hi ,I Arrive your site` للراكب وتفعيل عداد الانتظار.
### ج. بدء وإنهاء الرحلة
- **البدء (`Begin`):** يطلب النظام تأكيداً "Is the Passenger in your Car?". عند الموافقة، يتم إرسال `status: Begin` للسيرفر ويبدأ عداد الرحلة الفعلي `rideIsBeginPassengerTimer`.
- **الإنهاء (`Finish`):**
- يتم حساب المسافة المقطوعة والوقت.
- يتم استدعاء `finishRideFromDriver1` التي تقوم بإرسال طلبين متزامنين: تحديث حالة الرحلة وتنفيذ عملية الدفع `process_ride_payments.php`.
- يتم تحويل السائق لصفحة تقييم الراكب `RatePassenger`.
---
## 5. 💰 المحفظة والأرباح (Wallet & Earnings)
- **المسار:** `lib/controller/home/payment/captain_wallet_controller.dart`.
### أ. عرض الرصيد والديون
- يتم جلب البيانات المالية عبر `getCaptainWalletFromRide` (للأرباح من الرحلات) و `getCaptainWalletFromBuyPoints` (للنقاط المشتراة).
- **الحظر المالي:** إذا انخفض رصيد النقاط عن حد معين (مثل -300)، يتم منع السائق من استقبال الطلبات ويظهر زر `Charge your Account`.
### ب. آلية الشحن
- يتم دعم بوابات دفع متعددة مثل **Syriatel Cash** و **MTN Cash**.
- عند الشحن، يتم إنشاء `Invoice` وانتظار الـ Callback أو التحقق الدوري (`polling`) من حالة الدفع.
---
## 6. ⚙️ الإعدادات والميزات الإضافية
- **إعدادات السيارة:** تدار عبر `DriverCarController` و `CarsInsertingPage`، حيث يمكن إضافة سيارات جديدة ورفع وثائقها.
- **اللغة:** `LocaleController` يدير التبديل بين العربية والإنجليزية ويحفظ التفضيل في `GetStorage`.
- **التقييم:** يتم جلب تقييم السائق عبر `getDriverRate` وعرضه في القائمة الجانبية أو الرأسية.

View File

@@ -3,58 +3,65 @@ import 'package:sefer_driver/views/widgets/my_textField.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; // Import this for formatting
import 'package:sefer_driver/constant/colors.dart'; import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart'; import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import '../../constant/style.dart'; import '../../constant/style.dart';
import '../../controller/rate/rate_conroller.dart'; import '../../controller/rate/rate_conroller.dart';
// Changed: تم إعادة بناء الصفحة بالكامل لتحسين التصميم وتجربة المستخدم
class RatePassenger extends StatelessWidget { class RatePassenger extends StatelessWidget {
final RateController controller = Get.put(RateController()); final RateController controller = Get.put(RateController());
// Format: 1,234.5
final NumberFormat currencyFormatter = NumberFormat("#,##0.0", "en_US");
RatePassenger({super.key}); RatePassenger({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// New: استخدام Scaffold القياسي لهيكل أكثر قوة ومرونة
return Scaffold( return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar( appBar: AppBar(
title: Text('Rate Passenger'.tr), title: Text('Trip Completed'.tr,
style: const TextStyle(color: Colors.black)),
centerTitle: true, centerTitle: true,
automaticallyImplyLeading: false, // New: إزالة سهم الرجوع automaticallyImplyLeading: false,
backgroundColor: Colors.white, backgroundColor: Colors.white,
elevation: 1, elevation: 0,
), ),
// New: استخدام GetBuilder على مستوى الجسم لضمان تحديث الواجهة
body: GetBuilder<RateController>( body: GetBuilder<RateController>(
builder: (controller) { builder: (controller) {
// New: استخدام SingleChildScrollView لتجنب مشاكل الـ overflow عند ظهور لوحة المفاتيح
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// New: استدعاء ودجت منفصلة لكل قسم لزيادة التنظيم // 1. The HERO Section: Big Price Display
_buildPriceSummaryCard(context, controller), _buildHeroPriceDisplay(context),
const SizedBox(height: 16),
// New: قسم المحفظة يظهر فقط إذا لم يتم التحقق منه
if (controller.walletChecked != 'true')
_buildWalletSection(context, controller),
const SizedBox(height: 16),
_buildRatingSection(context, controller),
const SizedBox(height: 24), const SizedBox(height: 24),
MyElevatedButton( // 2. Wallet Section (Conditional)
title: 'Submit rating'.tr, if (controller.walletChecked != 'true')
onPressed: () => controller.addRateToPassenger(), _buildWalletSection(context, controller),
// New: جعل الزر يأخذ العرض الكامل لمزيد من الوضوح
// isFullWidth: true, const SizedBox(height: 24),
// 3. Rating Section
_buildRatingSection(context, controller),
const SizedBox(height: 30),
// 4. Submit Button
SizedBox(
height: 55,
child: MyElevatedButton(
title: 'Finish & Submit'.tr,
onPressed: () => controller.addRateToPassenger(),
// isFullWidth: true,
),
), ),
], ],
), ),
@@ -65,85 +72,96 @@ class RatePassenger extends StatelessWidget {
); );
} }
// New: ودجت منفصلة لعرض بطاقة ملخص السعر // --- WIDGETS ---
Widget _buildPriceSummaryCard(
BuildContext context, RateController controller) {
final MapDriverController mapController = Get.find<MapDriverController>();
final double originalPrice =
double.tryParse(controller.price.toString()) ?? 0.0;
final double priceAfterDiscount = originalPrice - (originalPrice * 0.12);
return Card( Widget _buildHeroPriceDisplay(BuildContext context) {
elevation: 4, final MapDriverController mapController = Get.find<MapDriverController>();
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding( // Parse the string to double to format it correctly
padding: const EdgeInsets.all(16.0), double amount =
child: Column( double.tryParse(mapController.paymentAmount.toString()) ?? 0.0;
children: [ String formattedAmount = currencyFormatter.format(amount);
Text(
'${'Trip Summary with'.tr} ${mapController.passengerName}', return Container(
style: AppStyle.title padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
.copyWith(fontSize: 18, fontWeight: FontWeight.bold), decoration: BoxDecoration(
textAlign: TextAlign.center, color: AppColor.primaryColor, // Use your brand color or a dark color
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppColor.primaryColor.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
Text(
'Collect Cash'.tr.toUpperCase(),
style: const TextStyle(
color: Colors.white70,
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
), ),
const Divider(height: 24, thickness: 1), ),
Row( const SizedBox(height: 10),
mainAxisAlignment: MainAxisAlignment.spaceBetween, Row(
children: [ mainAxisAlignment: MainAxisAlignment.center,
Text('Original Fare'.tr, style: AppStyle.title), crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
priceAfterDiscount.toStringAsFixed(2), // Currency Symbol (Small)
style: AppStyle.number.copyWith( Padding(
fontSize: 16, padding: const EdgeInsets.only(top: 8.0),
color: AppColor.redColor, child: Text(
decoration: TextDecoration.lineThrough, 'SYP'.tr, // Replace with your local currency symbol if needed
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 24,
fontWeight: FontWeight.bold,
), ),
), ),
], ),
), const SizedBox(width: 4),
const SizedBox(height: 8), // The Price (Huge)
Row( Text(
mainAxisAlignment: MainAxisAlignment.spaceBetween, formattedAmount,
children: [ style: const TextStyle(
Text('Your Earnings'.tr, color: Colors.white,
style: fontSize: 56, // Very Large Font
AppStyle.title.copyWith(fontWeight: FontWeight.bold)), fontWeight: FontWeight.w900,
Container( height: 1.0,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColor.greenColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.greenColor),
),
child: Text(
mapController.paymentAmount,
style: AppStyle.number
.copyWith(color: AppColor.greenColor, fontSize: 20),
),
), ),
], ),
],
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
), ),
const SizedBox(height: 12), child: Text(
Text( 'Passenger: ${mapController.passengerName}',
'Exclusive offers and discounts always with the Sefer app'.tr, style: const TextStyle(color: Colors.white, fontSize: 14),
textAlign: TextAlign.center, ),
style: AppStyle.title ),
.copyWith(color: AppColor.redColor, fontSize: 13), ],
)
],
),
), ),
); );
} }
// New: ودجت منفصلة لقسم الدفع عبر المحفظة
Widget _buildWalletSection(BuildContext context, RateController controller) { Widget _buildWalletSection(BuildContext context, RateController controller) {
return Card( return Container(
elevation: 4, decoration: BoxDecoration(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200),
),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(20.0),
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: controller.ispassengerWantWalletFromDriver child: controller.ispassengerWantWalletFromDriver
@@ -154,37 +172,61 @@ class RatePassenger extends StatelessWidget {
); );
} }
// New: واجهة سؤال استخدام المحفظة
Widget _buildWalletQuery(RateController controller) { Widget _buildWalletQuery(RateController controller) {
return Column( return Column(
key: const ValueKey('walletQuery'), key: const ValueKey('walletQuery'),
children: [ children: [
Text( Row(
"Would the passenger like to settle the remaining fare using their wallet?" children: [
.tr, Container(
style: AppStyle.title, padding: const EdgeInsets.all(10),
textAlign: TextAlign.center, decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(Icons.account_balance_wallet,
color: Colors.orange),
),
const SizedBox(width: 15),
Expanded(
child: Text(
"Pay remaining to Wallet?".tr,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
), ),
const SizedBox(height: 16), const SizedBox(height: 20),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: MyElevatedButton( child: OutlinedButton(
title: 'No'.tr, onPressed: () {}, // Optional logic
onPressed: () { style: OutlinedButton.styleFrom(
// يمكنك هنا تحديد ما يحدث عند الضغط على "لا" foregroundColor: Colors.grey,
// حاليًا، ستبقى الواجهة كما هي أو يمكنك إخفاؤها side: BorderSide(color: Colors.grey.shade300),
}, padding: const EdgeInsets.symmetric(vertical: 12),
kolor: AppColor.redColor, shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
child: Text('No'.tr),
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 12),
Expanded( Expanded(
child: MyElevatedButton( child: ElevatedButton(
title: 'Yes, Pay'.tr, onPressed: () => controller.passengerWantPay(),
onPressed: () { style: ElevatedButton.styleFrom(
controller.passengerWantPay(); backgroundColor: AppColor.primaryColor,
}, padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
child: Text('Yes, Pay'.tr,
style: const TextStyle(color: Colors.white)),
), ),
), ),
], ],
@@ -193,137 +235,94 @@ class RatePassenger extends StatelessWidget {
); );
} }
// New: واجهة إدخال المبلغ المدفوع
Widget _buildAmountInput(RateController controller) { Widget _buildAmountInput(RateController controller) {
return Column( return Column(
key: const ValueKey('amountInput'), key: const ValueKey('amountInput'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"How much Passenger pay?".tr, "Enter Amount Paid".tr,
style: AppStyle.title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Form( Form(
key: controller.formKey, key: controller.formKey,
child: MyTextForm( child: MyTextForm(
controller: controller.passengerPayAmount, controller: controller.passengerPayAmount,
label: "Passenger paid amount".tr, label: "Amount".tr,
hint: "0.00", hint: "0.00",
type: const TextInputType.numberWithOptions(decimal: true), type: const TextInputType.numberWithOptions(decimal: true),
// Suggestion: Add a suffix icon for currency if available in your widget
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
MyElevatedButton( SizedBox(
title: "Add to Passenger Wallet".tr, width: double.infinity,
// isFullWidth: true, child: ElevatedButton(
onPressed: () { onPressed: () => controller.addPassengerWallet(),
controller.addPassengerWallet(); style: ElevatedButton.styleFrom(
}, backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
child: Text("Confirm Payment".tr,
style: const TextStyle(color: Colors.white)),
),
) )
], ],
); );
} }
// New: ودجت منفصلة لقسم التقييم وكتابة الملاحظات
Widget _buildRatingSection(BuildContext context, RateController controller) { Widget _buildRatingSection(BuildContext context, RateController controller) {
return Card( return Column(
elevation: 4, children: [
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), Text(
child: Padding( 'Rate Passenger'.tr,
padding: const EdgeInsets.all(16.0), style: TextStyle(
child: Column( color: Colors.grey[600],
children: [ fontSize: 14,
Text('How was the passenger?'.tr, fontWeight: FontWeight.w500,
style: AppStyle.title
.copyWith(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
RatingBar.builder(
initialRating: 0,
itemCount: 5,
itemSize: 40,
itemPadding: const EdgeInsets.symmetric(horizontal: 4),
itemBuilder: (context, index) {
switch (index) {
case 0:
return const Icon(Icons.sentiment_very_dissatisfied,
color: Colors.red);
case 1:
return const Icon(Icons.sentiment_dissatisfied,
color: Colors.redAccent);
case 2:
return const Icon(Icons.sentiment_neutral,
color: Colors.amber);
case 3:
return const Icon(Icons.sentiment_satisfied,
color: Colors.lightGreen);
case 4:
return const Icon(Icons.sentiment_very_satisfied,
color: Colors.green);
default:
return const Icon(Icons.sentiment_neutral,
color: Colors.amber);
}
},
onRatingUpdate: (rating) {
controller.selectRateItem(rating);
},
),
const SizedBox(height: 24),
TextFormField(
maxLines: 4,
minLines: 2,
keyboardType: TextInputType.multiline,
controller: controller.comment,
decoration: InputDecoration(
labelText: 'Add a comment (optional)'.tr,
hintText: 'Type something...'.tr,
prefixIcon: const Icon(Icons.rate_review_outlined),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
),
],
),
),
);
}
}
// New: إضافة isFullWidth إلى MyElevatedButton لتسهيل التحكم في العرض
// تأكد من تحديث ملف elevated_btn.dart بهذا التغيير
/*
class MyElevatedButton extends StatelessWidget {
final String title;
final VoidCallback onPressed;
final Color? kolor;
final bool isFullWidth; // New property
const MyElevatedButton({
Key? key,
required this.title,
required this.onPressed,
this.kolor,
this.isFullWidth = false, // Default value
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
width: isFullWidth ? double.infinity : null, // Apply width
height: 50, // Standard height
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: kolor ?? AppColor.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
), ),
textStyle: AppStyle.title.copyWith(color: Colors.white),
), ),
child: Text(title, style: const TextStyle(color: Colors.white)), const SizedBox(height: 12),
), RatingBar.builder(
initialRating: 0,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: false,
itemCount: 5,
itemSize: 45, // Large stars
itemPadding: const EdgeInsets.symmetric(horizontal: 2.0),
itemBuilder: (context, _) => const Icon(
Icons.star_rounded,
color: Colors.amber,
),
onRatingUpdate: (rating) {
controller.selectRateItem(rating);
},
),
const SizedBox(height: 20),
// Simplified comment box
TextField(
controller: controller.comment,
maxLines: 2,
decoration: InputDecoration(
hintText: 'Any comments about the passenger?'.tr,
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
),
),
],
); );
} }
} }
*/

View File

@@ -1,6 +1,4 @@
import 'dart:io'; import 'dart:io';
import 'package:sefer_driver/views/auth/captin/contact_us_page.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -18,20 +16,45 @@ import '../../../constant/style.dart';
import '../../../controller/auth/apple_sigin.dart'; import '../../../controller/auth/apple_sigin.dart';
import '../../../controller/auth/captin/login_captin_controller.dart'; import '../../../controller/auth/captin/login_captin_controller.dart';
import '../../../controller/auth/google_sign.dart'; import '../../../controller/auth/google_sign.dart';
import '../../../controller/functions/encrypt_decrypt.dart';
import '../../../controller/functions/overlay_permisssion.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../../print.dart'; import '../../../print.dart';
import '../../widgets/elevated_btn.dart'; import '../../widgets/elevated_btn.dart';
import '../../widgets/mycircular.dart'; import '../../widgets/mycircular.dart';
import '../country_widget.dart'; import 'contact_us_page.dart';
import 'otp_page.dart'; import 'otp_page.dart'; // تأكد من وجود هذا الملف لديك
class LoginCaptin extends StatelessWidget { class LoginCaptin extends StatefulWidget {
const LoginCaptin({super.key});
@override
State<LoginCaptin> createState() => _LoginCaptinState();
}
class _LoginCaptinState extends State<LoginCaptin> with WidgetsBindingObserver {
final AuthController authController = Get.put(AuthController()); final AuthController authController = Get.put(AuthController());
final LoginDriverController controller = Get.put(LoginDriverController()); final LoginDriverController controller = Get.put(LoginDriverController());
LoginCaptin({super.key}); @override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// فحص الإذن عند فتح الصفحة مباشرة
controller.getLocationPermission();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// التحقق عند العودة من الإعدادات
if (state == AppLifecycleState.resumed) {
controller.getLocationPermission();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -49,289 +72,25 @@ class LoginCaptin extends StatelessWidget {
); );
} }
/// Determines which UI to show based on the driver's progress (agreement, permissions, login).
Widget _buildBodyContent( Widget _buildBodyContent(
BuildContext context, LoginDriverController controller) { BuildContext context, LoginDriverController controller) {
// 1. صفحة الموافقة على الشروط
if (box.read(BoxName.agreeTerms) != 'agreed') { if (box.read(BoxName.agreeTerms) != 'agreed') {
return _buildAgreementPage(context, controller); return _buildAgreementPage(context, controller);
} }
// if (box.read(BoxName.countryCode) == null) {
// return CountryPicker(); // Assumed to be a full-screen widget // 2. صفحة إذن الموقع
// }
if (box.read(BoxName.locationPermission) != 'true') { if (box.read(BoxName.locationPermission) != 'true') {
return _buildLocationPermissionPage(context, controller); return _buildLocationPermissionPage(context, controller);
} }
// Once all permissions are granted, show the main login UI
// 3. صفحة تسجيل الدخول (رقم الهاتف)
return PhoneNumberScreen(); return PhoneNumberScreen();
} }
/// Redesigned UI for the main login screen. // --- صفحة الشروط والأحكام ---
Widget _buildLoginUI(BuildContext context, LoginDriverController controller) {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Image.asset('assets/images/logo.gif', height: 120, width: 120),
const SizedBox(height: 20),
Text(
'Driver Portal'.tr,
textAlign: TextAlign.center,
style: AppStyle.headTitle2.copyWith(fontSize: 28),
),
const SizedBox(height: 8),
Text(
'Sign in to start your journey'.tr,
textAlign: TextAlign.center,
style: AppStyle.subtitle,
),
const SizedBox(height: 40),
// Conditional UI based on the controller state
if (controller.isGoogleDashOpen)
_buildManualLoginForm(context, controller, isRegistration: true)
else if (Platform.isIOS && controller.isTest == 0)
_buildManualLoginForm(context, controller, isRegistration: false)
else
_buildSocialLoginOptions(context, controller),
const SizedBox(height: 32),
Center(
child: GestureDetector(
onTap: () => Get.to(() => ContactUsPage()),
child: Text(
'Need help? Contact Us'.tr,
style: AppStyle.subtitle.copyWith(
color: AppColor.blueColor,
decoration: TextDecoration.underline,
),
),
),
),
],
),
);
}
/// Builds the social login buttons (Google, Apple, and manual option).
Widget _buildSocialLoginOptions(
BuildContext context, LoginDriverController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Sign in with a provider for easy access'.tr,
textAlign: TextAlign.center,
style: AppStyle.title,
),
const SizedBox(height: 24),
_buildSocialButton(
text: 'Sign In with Google'.tr,
icon: FontAwesome.google,
backgroundColor: AppColor.redColor,
onPressed: () async {
GoogleSignInHelper().signInFromLogin();
},
),
if (Platform.isIOS) ...[
const SizedBox(height: 16),
_buildSocialButton(
text: 'Sign in with Apple'.tr,
icon: Icons.apple,
backgroundColor: Colors.black,
onPressed: () async {
User? user = await authController.signInWithApple();
if (user != null) {
box.write(BoxName.emailDriver, user.email.toString());
box.write(BoxName.driverID, user.uid);
controller.loginWithGoogleCredential(
user.uid,
user.email.toString(),
);
}
},
),
],
const SizedBox(height: 24),
Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text('Or'.tr, style: AppStyle.subtitle),
),
const Expanded(child: Divider()),
],
),
const SizedBox(height: 24),
MyElevatedButton(
title: 'Create Account with Email'.tr,
onPressed: () => controller.changeGoogleDashOpen(),
kolor: AppColor.blueColor, // Using 'kolor' as in your widget
),
],
);
}
/// Builds the form for manual email/password login or registration.
Widget _buildManualLoginForm(
BuildContext context, LoginDriverController controller,
{required bool isRegistration}) {
return Card(
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Form(
key: controller.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
isRegistration ? 'Create Driver Account'.tr : 'Driver Login'.tr,
textAlign: TextAlign.center,
style: AppStyle.headTitle2,
),
const SizedBox(height: 24),
_buildTextFormField(
controller: controller.emailController,
labelText: 'Email'.tr,
hintText: 'Enter your email'.tr,
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || !GetUtils.isEmail(value)) {
return 'Please enter a valid email'.tr;
}
return null;
},
),
const SizedBox(height: 20),
GetBuilder<LoginDriverController>(
id: 'passwordVisibility', // ID to only rebuild this widget
builder: (_) => _buildTextFormField(
controller: controller.passwordController,
labelText: 'Password'.tr,
hintText: 'Enter your password'.tr,
prefixIcon: Icons.lock_outline,
obscureText: controller.isPasswordHidden,
suffixIcon: IconButton(
icon: Icon(
controller.isPasswordHidden
? Icons.visibility_off
: Icons.visibility,
color: AppColor.primaryColor,
),
onPressed: () => controller.togglePasswordVisibility(),
),
validator: (value) {
if (value == null || value.length < 6) {
return 'Password must be at least 6 characters'.tr;
}
return null;
},
),
),
const SizedBox(height: 30),
controller.isloading
? const Center(child: MyCircularProgressIndicator())
: MyElevatedButton(
title:
(isRegistration ? 'Create Account'.tr : 'Login'.tr),
onPressed: () {
if (controller.formKey.currentState!.validate()) {
if (isRegistration) {
String email = controller.emailController.text;
String uniqueId =
controller.generateUniqueIdFromEmail(email);
box.write(BoxName.driverID, uniqueId);
box.write(BoxName.emailDriver, email);
controller.loginUsingCredentialsWithoutGoogle(
controller.passwordController.text,
email,
);
} else {
// This is the flow for iOS manual login
controller.loginWithGoogleCredential(
controller.passwordController.text,
controller.emailController.text,
);
}
}
},
),
if (isRegistration) // Show back button only on the registration form
TextButton(
onPressed: () => controller.changeGoogleDashOpen(),
child: Text(
'Back to other sign-in options'.tr,
style: TextStyle(color: AppColor.primaryColor),
),
),
],
),
),
),
);
}
/// A helper method to create styled TextFormFields.
TextFormField _buildTextFormField({
required TextEditingController controller,
required String labelText,
required String hintText,
required IconData prefixIcon,
required String? Function(String?) validator,
bool obscureText = false,
Widget? suffixIcon,
TextInputType keyboardType = TextInputType.text,
}) {
return TextFormField(
controller: controller,
validator: validator,
obscureText: obscureText,
keyboardType: keyboardType,
decoration: InputDecoration(
labelText: labelText,
hintText: hintText,
prefixIcon: Icon(prefixIcon, color: AppColor.primaryColor),
suffixIcon: suffixIcon,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.0)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide:
const BorderSide(color: AppColor.primaryColor, width: 2.0),
),
),
);
}
/// A helper for creating consistent social login buttons.
Widget _buildSocialButton({
required String text,
required IconData icon,
required Color backgroundColor,
required VoidCallback onPressed,
}) {
return ElevatedButton.icon(
icon: Icon(icon, color: Colors.white),
label:
Text(text, style: const TextStyle(color: Colors.white, fontSize: 16)),
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 14),
),
);
}
/// Redesigned UI for the Terms and Conditions agreement page.
Widget _buildAgreementPage( Widget _buildAgreementPage(
BuildContext context, LoginDriverController controller) { BuildContext context, LoginDriverController controller) {
// This UI can be identical to the one in LoginPage for consistency.
// I am reusing the improved design from the previous request.
return Padding( return Padding(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(24.0),
child: Column( child: Column(
@@ -404,7 +163,7 @@ class LoginCaptin extends StatelessWidget {
); );
} }
/// Redesigned UI for the Location Permission request page. // --- صفحة إذن الموقع ---
Widget _buildLocationPermissionPage( Widget _buildLocationPermissionPage(
BuildContext context, LoginDriverController controller) { BuildContext context, LoginDriverController controller) {
return Padding( return Padding(
@@ -439,15 +198,33 @@ class LoginCaptin extends StatelessWidget {
const SizedBox(height: 40), const SizedBox(height: 40),
MyElevatedButton( MyElevatedButton(
title: "Allow Location Access".tr, title: "Allow Location Access".tr,
kolor: AppColor.greenColor,
onPressed: () async { onPressed: () async {
Get.put(LoginDriverController()); box.write(BoxName.locationPermission, 'true');
await getLocationPermission(); // Assumes this function handles the request logic controller.update();
if (await Permission.location.isGranted) { // 1. طلب إذن الموقع العادي (أثناء الاستخدام) أولاً
box.write(BoxName.locationPermission, 'true'); var status = await Permission.location.request();
controller.update(); // Re-check conditions
if (status.isGranted) {
// 2. إذا كنت تحتاج "طوال الوقت" (Background)، اطلبه الآن بشكل منفصل
// ملاحظة: في أندرويد 11+ سينقلك هذا لصفحة إعدادات خاصة
var backgroundStatus =
await Permission.locationAlways.request();
if (backgroundStatus.isGranted) {
box.write(BoxName.locationPermission, 'true');
controller.update();
} else {
// المستخدم وافق على "أثناء الاستخدام" فقط، يمكنك المشي في الحال
// أو إجباره، حسب منطق تطبيقك (تطبيق سائق يفضل Always)
box.write(BoxName.locationPermission, 'true');
controller.update();
}
} else if (status.isPermanentlyDenied) {
// إذا كانت الحالة مرفوضة نهائياً، يجب فتح الإعدادات
openAppSettings();
} }
}, },
kolor: AppColor.greenColor,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextButton( TextButton(
@@ -459,4 +236,258 @@ class LoginCaptin extends StatelessWidget {
), ),
); );
} }
// --- واجهة تسجيل الدخول اليدوي/الاجتماعي (للاستخدام المستقبلي إذا لزم الأمر) ---
Widget _buildLoginUI(BuildContext context, LoginDriverController controller) {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Image.asset('assets/images/logo.gif', height: 120, width: 120),
const SizedBox(height: 20),
Text(
'Driver Portal'.tr,
textAlign: TextAlign.center,
style: AppStyle.headTitle2.copyWith(fontSize: 28),
),
const SizedBox(height: 8),
Text(
'Sign in to start your journey'.tr,
textAlign: TextAlign.center,
style: AppStyle.subtitle,
),
const SizedBox(height: 40),
if (controller.isGoogleDashOpen)
_buildManualLoginForm(context, controller, isRegistration: true)
else if (Platform.isIOS && controller.isTest == 0)
_buildManualLoginForm(context, controller, isRegistration: false)
else
_buildSocialLoginOptions(context, controller),
const SizedBox(height: 32),
Center(
child: GestureDetector(
onTap: () => Get.to(() => ContactUsPage()),
child: Text(
'Need help? Contact Us'.tr,
style: AppStyle.subtitle.copyWith(
color: AppColor.blueColor,
decoration: TextDecoration.underline,
),
),
),
),
],
),
);
}
Widget _buildSocialLoginOptions(
BuildContext context, LoginDriverController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Sign in with a provider for easy access'.tr,
textAlign: TextAlign.center,
style: AppStyle.title,
),
const SizedBox(height: 24),
_buildSocialButton(
text: 'Sign In with Google'.tr,
icon: FontAwesome.google,
backgroundColor: AppColor.redColor,
onPressed: () async {
GoogleSignInHelper().signInFromLogin();
},
),
if (Platform.isIOS) ...[
const SizedBox(height: 16),
_buildSocialButton(
text: 'Sign in with Apple'.tr,
icon: Icons.apple,
backgroundColor: Colors.black,
onPressed: () async {
User? user = await authController.signInWithApple();
if (user != null) {
box.write(BoxName.emailDriver, user.email.toString());
box.write(BoxName.driverID, user.uid);
controller.loginWithGoogleCredential(
user.uid,
user.email.toString(),
);
}
},
),
],
const SizedBox(height: 24),
Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text('Or'.tr, style: AppStyle.subtitle),
),
const Expanded(child: Divider()),
],
),
const SizedBox(height: 24),
MyElevatedButton(
title: 'Create Account with Email'.tr,
onPressed: () => controller.changeGoogleDashOpen(),
kolor: AppColor.blueColor,
),
],
);
}
Widget _buildManualLoginForm(
BuildContext context, LoginDriverController controller,
{required bool isRegistration}) {
return Card(
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Form(
key: controller.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
isRegistration ? 'Create Driver Account'.tr : 'Driver Login'.tr,
textAlign: TextAlign.center,
style: AppStyle.headTitle2,
),
const SizedBox(height: 24),
_buildTextFormField(
controller: controller.emailController,
labelText: 'Email'.tr,
hintText: 'Enter your email'.tr,
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || !GetUtils.isEmail(value)) {
return 'Please enter a valid email'.tr;
}
return null;
},
),
const SizedBox(height: 20),
GetBuilder<LoginDriverController>(
id: 'passwordVisibility',
builder: (_) => _buildTextFormField(
controller: controller.passwordController,
labelText: 'Password'.tr,
hintText: 'Enter your password'.tr,
prefixIcon: Icons.lock_outline,
obscureText: controller.isPasswordHidden,
suffixIcon: IconButton(
icon: Icon(
controller.isPasswordHidden
? Icons.visibility_off
: Icons.visibility,
color: AppColor.primaryColor,
),
onPressed: () => controller.togglePasswordVisibility(),
),
validator: (value) {
if (value == null || value.length < 6) {
return 'Password must be at least 6 characters'.tr;
}
return null;
},
),
),
const SizedBox(height: 30),
controller.isloading
? const Center(child: MyCircularProgressIndicator())
: MyElevatedButton(
title:
(isRegistration ? 'Create Account'.tr : 'Login'.tr),
onPressed: () {
if (controller.formKey.currentState!.validate()) {
if (isRegistration) {
String email = controller.emailController.text;
String uniqueId =
controller.generateUniqueIdFromEmail(email);
box.write(BoxName.driverID, uniqueId);
box.write(BoxName.emailDriver, email);
controller.loginUsingCredentialsWithoutGoogle(
controller.passwordController.text,
email,
);
} else {
controller.loginWithGoogleCredential(
controller.passwordController.text,
controller.emailController.text,
);
}
}
},
),
if (isRegistration)
TextButton(
onPressed: () => controller.changeGoogleDashOpen(),
child: Text(
'Back to other sign-in options'.tr,
style: TextStyle(color: AppColor.primaryColor),
),
),
],
),
),
),
);
}
TextFormField _buildTextFormField({
required TextEditingController controller,
required String labelText,
required String hintText,
required IconData prefixIcon,
required String? Function(String?) validator,
bool obscureText = false,
Widget? suffixIcon,
TextInputType keyboardType = TextInputType.text,
}) {
return TextFormField(
controller: controller,
validator: validator,
obscureText: obscureText,
keyboardType: keyboardType,
decoration: InputDecoration(
labelText: labelText,
hintText: hintText,
prefixIcon: Icon(prefixIcon, color: AppColor.primaryColor),
suffixIcon: suffixIcon,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.0)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide:
const BorderSide(color: AppColor.primaryColor, width: 2.0),
),
),
);
}
Widget _buildSocialButton({
required String text,
required IconData icon,
required Color backgroundColor,
required VoidCallback onPressed,
}) {
return ElevatedButton.icon(
icon: Icon(icon, color: Colors.white),
label:
Text(text, style: const TextStyle(color: Colors.white, fontSize: 16)),
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 14),
),
);
}
} }

View File

@@ -186,6 +186,52 @@ class RegistrationView extends StatelessWidget {
style: style:
const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 20), const SizedBox(height: 20),
// ============================================================
// 1. القائمة المنسدلة لتصنيف المركبة (سيارة، دراجة، فان)
// ============================================================
DropdownButtonFormField<int>(
value: c.selectedVehicleCategoryId,
decoration: InputDecoration(
labelText: 'Vehicle Category'.tr, // ترجمة: تصنيف المركبة
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.directions_car),
),
items: c.vehicleCategoryOptions.map((item) {
return DropdownMenuItem<int>(
value: item['id'],
child: Text(item['name']),
);
}).toList(),
onChanged: (val) {
c.selectedVehicleCategoryId = val;
c.update(); // تحديث الواجهة إذا لزم الأمر
},
validator: (v) => (v == null) ? 'Required field'.tr : null,
),
const SizedBox(height: 16),
// ============================================================
// 2. القائمة المنسدلة لنوع الوقود (بنزين، كهرباء...)
// ============================================================
DropdownButtonFormField<int>(
value: c.selectedFuelTypeId,
decoration: InputDecoration(
labelText: 'Fuel Type'.tr, // ترجمة: نوع الوقود
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.local_gas_station),
),
items: c.fuelTypeOptions.map((item) {
return DropdownMenuItem<int>(
value: item['id'],
child: Text(item['name']),
);
}).toList(),
onChanged: (val) {
c.selectedFuelTypeId = val;
c.update();
},
validator: (v) => (v == null) ? 'Required field'.tr : null,
),
const SizedBox(height: 16),
TextFormField( TextFormField(
controller: c.carPlateController, controller: c.carPlateController,
decoration: InputDecoration( decoration: InputDecoration(

View File

@@ -1,10 +1,10 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/constant/box_name.dart'; import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/constant/style.dart'; import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart'; import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart'; import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
import '../../../constant/colors.dart'; import '../../../constant/colors.dart';
import '../../../controller/functions/location_controller.dart'; import '../../../controller/functions/location_controller.dart';
import '../../../main.dart'; import '../../../main.dart';
@@ -15,6 +15,7 @@ import 'mapDriverWidgets/google_driver_map_page.dart';
import 'mapDriverWidgets/google_map_app.dart'; import 'mapDriverWidgets/google_map_app.dart';
import 'mapDriverWidgets/passenger_info_window.dart'; import 'mapDriverWidgets/passenger_info_window.dart';
import 'mapDriverWidgets/sos_connect.dart'; import 'mapDriverWidgets/sos_connect.dart';
import 'mapDriverWidgets/sped_circle.dart';
class PassengerLocationMapPage extends StatelessWidget { class PassengerLocationMapPage extends StatelessWidget {
PassengerLocationMapPage({super.key}); PassengerLocationMapPage({super.key});
@@ -22,26 +23,23 @@ class PassengerLocationMapPage extends StatelessWidget {
final MapDriverController mapDriverController = final MapDriverController mapDriverController =
Get.put(MapDriverController()); Get.put(MapDriverController());
// Helper function to show exit confirmation dialog // دالة ديالوج الخروج
Future<bool> showExitDialog() async { Future<bool> showExitDialog() async {
bool? result = await Get.defaultDialog( bool? result = await Get.defaultDialog(
title: "Warning".tr, title: "Warning".tr,
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
middleText: middleText:
"You are in an active ride. Leaving this screen might stop tracking. Are you sure you want to exit?" "Active ride in progress. Leaving might stop tracking. Exit?".tr,
.tr,
middleTextStyle: AppStyle.title,
barrierDismissible: false, barrierDismissible: false,
radius: 15,
confirm: MyElevatedButton( confirm: MyElevatedButton(
title: 'Stay'.tr, title: 'Stay'.tr,
kolor: AppColor.greenColor, kolor: AppColor.greenColor,
onPressed: () => Get.back(result: false), // Return false (Don't pop) onPressed: () => Get.back(result: false)),
),
cancel: MyElevatedButton( cancel: MyElevatedButton(
title: 'Exit'.tr, title: 'Exit'.tr,
kolor: AppColor.redColor, kolor: AppColor.redColor,
onPressed: () => Get.back(result: true), // Return true (Allow pop) onPressed: () => Get.back(result: true)),
),
); );
return result ?? false; return result ?? false;
} }
@@ -52,202 +50,205 @@ class PassengerLocationMapPage extends StatelessWidget {
if (Get.arguments != null && Get.arguments is Map<String, dynamic>) { if (Get.arguments != null && Get.arguments is Map<String, dynamic>) {
mapDriverController.argumentLoading(); mapDriverController.argumentLoading();
mapDriverController.startTimerToShowPassengerInfoWindowFromDriver(); mapDriverController.startTimerToShowPassengerInfoWindowFromDriver();
// 2. فرض التحديث لكل المعرفات (IDs) لضمان ظهورها
// لأن argumentLoading قد تستدعي update() العادية التي لا تؤثر على هؤلاء
mapDriverController
.update(['PassengerInfo', 'DriverEndBar', 'SosConnect']);
} }
}); });
// ✅ Added PopScope to intercept back button
return PopScope( return PopScope(
canPop: false, // Prevents immediate popping canPop: false,
onPopInvokedWithResult: (didPop, result) async { onPopInvokedWithResult: (didPop, result) async {
if (didPop) { if (didPop) return;
return;
}
// Show dialog
final shouldExit = await showExitDialog(); final shouldExit = await showExitDialog();
if (shouldExit) { if (shouldExit) Get.back();
Get.back(); // Manually pop if confirmed
}
}, },
child: Scaffold( child: Scaffold(
body: SafeArea( resizeToAvoidBottomInset: false,
child: Stack( body: Stack(
children: [ children: [
// 1. Map // 1. الخريطة (الخلفية)
GoogleDriverMap(locationController: locationController), Positioned.fill(
child: GoogleDriverMap(locationController: locationController)),
// 2. Instructions // 2. واجهة المستخدم (فوق الخريطة)
const InstructionsOfRoads(), SafeArea(
child: Stack(
children: [
// أ) زر الإلغاء (أعلى اليسار)
CancelWidget(mapDriverController: mapDriverController),
// 3. Passenger Info // ب) شريط إنهاء الرحلة (أعلى الوسط)
Positioned( Positioned(
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
child: PassengerInfoWindow(), child: SafeArea(child: driverEndRideBar())),
// ج) شريط التعليمات الملاحية (الأسفل)
const InstructionsOfRoads(),
// د) نافذة معلومات الراكب (تعلو التعليمات ديناميكياً)
const PassengerInfoWindow(),
// SpeedCircle(),
Positioned(
right: 16,
bottom: 20, // أو أي مسافة تناسبك
child: GetBuilder<MapDriverController>(
// id: 'SosConnect', // لتحديث الزر عند بدء الرحلة
builder: (controller) {
// حساب الهوامش ديناميكياً لرفع الأزرار فوق النوافذ السفلية
double bottomPadding = 0;
if (controller.currentInstruction.isNotEmpty)
bottomPadding += 120;
if (controller.isPassengerInfoWindow)
bottomPadding += 220;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: EdgeInsets.only(bottom: bottomPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SosConnect(), // ويدجت نظيفة
const SizedBox(height: 12),
const GoogleMapApp(), // ويدجت نظيفة
],
),
);
},
),
),
],
),
), ),
// 4. Cancel Widget // 3. النوافذ المنبثقة (Overlay)
CancelWidget(mapDriverController: mapDriverController),
// 5. End Ride Bar
driverEndRideBar(),
// 6. SOS
SosConnect(),
// 7. Speed
speedCircle(),
// 8. External Map
Positioned(
bottom: 100,
right: 10,
child: GoogleMapApp(),
),
// 9. Prices Window
const PricesWindow(), const PricesWindow(),
], ],
), ),
)), ),
); );
} }
} }
// ... The rest of your widgets (InstructionsOfRoads, CancelWidget, etc.) remain unchanged ... // ---------------------------------------------------------------------------
// ... Keep the code below exactly as you had it in the previous snippet ... // 1. ويدجت شريط التعليمات (InstructionsOfRoads)
// ---------------------------------------------------------------------------
class InstructionsOfRoads extends StatelessWidget { class InstructionsOfRoads extends StatelessWidget {
const InstructionsOfRoads({super.key}); const InstructionsOfRoads({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Positioned( return Positioned(
bottom: 10, bottom: 20,
left: MediaQuery.of(context).size.width * 0.15, left: 15,
right: MediaQuery.of(context).size.width * 0.15, right: 15,
child: GetBuilder<MapDriverController>(
builder: (controller) => controller.currentInstruction.isNotEmpty
? AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.directions, color: AppColor.primaryColor),
const SizedBox(width: 10),
Expanded(
child: Text(
controller.currentInstruction,
style: AppStyle.title.copyWith(fontSize: 16),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 10),
InkWell(
onTap: () {
controller.toggleTts();
},
child: Icon(
controller.isTtsEnabled
? Icons.volume_up
: Icons.volume_off,
color: controller.isTtsEnabled
? AppColor.greenColor
: Colors.grey,
),
),
],
),
)
: const SizedBox(),
),
);
}
}
class CancelWidget extends StatelessWidget {
const CancelWidget({
super.key,
required this.mapDriverController,
});
final MapDriverController mapDriverController;
@override
Widget build(BuildContext context) {
return Positioned(
top: 70,
left: 10,
child: GetBuilder<MapDriverController>( child: GetBuilder<MapDriverController>(
builder: (controller) { builder: (controller) {
if (controller.isRideFinished) return const SizedBox.shrink(); // إخفاء الشريط إذا لم يكن هناك تعليمات
if (controller.currentInstruction.isEmpty) return const SizedBox();
return GestureDetector( return TweenAnimationBuilder<double>(
onTap: () { tween: Tween(begin: 0.0, end: 1.0),
Get.defaultDialog( duration: const Duration(milliseconds: 500),
title: "Are you sure you want to cancel this trip?".tr, builder: (context, value, child) {
titleStyle: AppStyle.title, return Transform.translate(
content: Column( offset: Offset(0, 50 * (1 - value)), // حركة انزلاق
children: [ child: Opacity(
Text("Why do you want to cancel this trip?".tr), opacity: value,
Form( child: Container(
key: mapDriverController.formKeyCancel, padding: const EdgeInsets.symmetric(
child: MyTextForm( horizontal: 16, vertical: 12),
controller: mapDriverController.cancelTripCotroller, decoration: BoxDecoration(
label: "Write the reason for canceling the trip".tr, color: const Color(0xFF1F1F1F)
hint: "Write the reason for canceling the trip".tr, .withOpacity(0.95), // خلفية داكنة
type: TextInputType.name, borderRadius: BorderRadius.circular(16),
)) boxShadow: [
], BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 15,
offset: const Offset(0, 5)),
],
border: Border.all(color: Colors.white.withOpacity(0.1)),
),
child: Row(
children: [
// أيقونة الاتجاه
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.primaryColor,
shape: BoxShape.circle,
),
child: const Icon(Icons.turn_right_rounded,
color: Colors.white, size: 24),
),
const SizedBox(width: 14),
// نص التعليمات
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"NEXT STEP".tr,
style: TextStyle(
color: Colors.grey.shade500,
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1.2),
),
const SizedBox(height: 2),
Text(
controller.currentInstruction,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.2),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
// فاصل عمودي
Container(
width: 1,
height: 30,
color: Colors.white12,
margin: const EdgeInsets.symmetric(horizontal: 10)),
// زر التحكم بالصوت
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => controller.toggleTts(),
borderRadius: BorderRadius.circular(20),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
controller.isTtsEnabled
? Icons.volume_up_rounded
: Icons.volume_off_rounded,
color: controller.isTtsEnabled
? AppColor.greenColor
: Colors.grey,
size: 24,
),
),
),
),
],
),
), ),
confirm: MyElevatedButton(
title: 'Ok'.tr,
kolor: AppColor.redColor,
onPressed: () async {
await mapDriverController
.cancelTripFromDriverAfterApplied();
Get.back();
}),
cancel: MyElevatedButton(
title: 'No'.tr,
onPressed: () {
Get.back();
}));
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 5,
),
],
),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.clear,
size: 30,
color: AppColor.redColor,
), ),
), );
), },
); );
}, },
), ),
@@ -255,56 +256,193 @@ class CancelWidget extends StatelessWidget {
} }
} }
class PricesWindow extends StatelessWidget { // ---------------------------------------------------------------------------
const PricesWindow({ // 2. ويدجت زر الإلغاء (CancelWidget) - كامل
super.key, // ---------------------------------------------------------------------------
}); class CancelWidget extends StatelessWidget {
const CancelWidget({super.key, required this.mapDriverController});
final MapDriverController mapDriverController;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetBuilder<MapDriverController>(builder: (mapDriverController) { return Positioned(
return mapDriverController.isPriceWindow top: 10,
? Container( left: 15,
color: Colors.black.withOpacity(0.5), child: GetBuilder<MapDriverController>(builder: (controller) {
child: Center( // نخفي الزر إذا انتهت الرحلة
if (controller.isRideFinished) return const SizedBox();
return ClipRRect(
borderRadius: BorderRadius.circular(30),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 8)
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(30),
onTap: () => _showCancelDialog(context, controller),
child: const Padding(
padding: EdgeInsets.all(10.0),
child: Icon(Icons.close_rounded,
color: AppColor.redColor, size: 26),
),
),
),
),
),
);
}),
);
}
void _showCancelDialog(BuildContext context, MapDriverController controller) {
Get.defaultDialog(
title: "Cancel Trip?".tr,
titleStyle: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
radius: 16,
content: Column(
children: [
const Icon(Icons.warning_amber_rounded,
size: 50, color: Colors.orangeAccent),
const SizedBox(height: 10),
Text(
"Please tell us why you want to cancel.".tr,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(height: 15),
Form(
key: controller.formKeyCancel,
child: MyTextForm(
controller: controller.cancelTripCotroller,
label: "Reason".tr,
hint: "Write your reason...".tr,
type: TextInputType.text,
),
),
],
),
confirm: SizedBox(
width: 100,
child: MyElevatedButton(
title: 'Confirm'.tr,
kolor: AppColor.redColor,
onPressed: () async {
// استدعاء دالة الإلغاء من الكنترولر
await controller.cancelTripFromDriverAfterApplied();
// Get.back(); // عادة موجودة داخل الدالة في الكنترولر
},
),
),
cancel: SizedBox(
width: 100,
child: TextButton(
onPressed: () => Get.back(),
child: Text('Back'.tr, style: const TextStyle(color: Colors.grey)),
),
),
);
}
}
// ---------------------------------------------------------------------------
// 3. ويدجت نافذة الأسعار (PricesWindow) - كامل
// ---------------------------------------------------------------------------
class PricesWindow extends StatelessWidget {
const PricesWindow({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MapDriverController>(builder: (controller) {
// إخفاء إذا لم تكن مفعلة
if (!controller.isPriceWindow) return const SizedBox();
return Container(
color: Colors.black.withOpacity(0.6), // خلفية معتمة
child: Center(
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0.8, end: 1.0),
duration: const Duration(milliseconds: 300),
curve: Curves.elasticOut,
builder: (context, scale, child) {
return Transform.scale(
scale: scale,
child: Container( child: Container(
width: Get.width * 0.8, width: Get.width * 0.85,
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(30),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 20,
spreadRadius: 5,
),
],
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: AppColor.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(Icons.check_circle_rounded,
color: AppColor.primaryColor, size: 50),
),
const SizedBox(height: 20),
Text( Text(
'Total Price is '.tr, 'Total Price'.tr,
style: AppStyle.headTitle2, style: AppStyle.headTitle2
textAlign: TextAlign.center, .copyWith(fontSize: 18, color: Colors.grey[600]),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'${mapDriverController.totalPricePassenger} ${'\$'.tr}', '${controller.totalCost} ${'\$'.tr}',
style: AppStyle.headTitle2.copyWith( style: AppStyle.headTitle2.copyWith(
color: AppColor.primaryColor, fontSize: 36), color: Colors.black87,
fontSize: 42,
fontWeight: FontWeight.w900,
),
), ),
const SizedBox( const SizedBox(height: 30),
height: 20, SizedBox(
width: double.infinity,
height: 55,
child: MyElevatedButton(
title: 'Collect Payment'.tr,
kolor: AppColor.primaryColor,
onPressed: () {
// الذهاب لصفحة التقييم
Get.to(() => RatePassenger(), arguments: {
'rideId': controller.rideId,
'passengerId': controller.passengerId,
'driverId': controller.driverId,
'price': controller.paymentAmount,
'walletChecked': controller.walletChecked
});
},
),
), ),
MyElevatedButton(
title: 'ok'.tr,
onPressed: () =>
Get.to(() => RatePassenger(), arguments: {
'rideId': mapDriverController.rideId,
'passengerId': mapDriverController.passengerId,
'driverId': mapDriverController.driverId
}))
], ],
), ),
), ),
), );
) },
: const SizedBox(); ),
),
);
}); });
} }
} }

View File

@@ -23,6 +23,7 @@ import 'package:sefer_driver/views/home/my_wallet/walet_captain.dart';
import 'package:sefer_driver/views/home/profile/profile_captain.dart'; import 'package:sefer_driver/views/home/profile/profile_captain.dart';
import 'package:sefer_driver/views/notification/notification_captain.dart'; import 'package:sefer_driver/views/notification/notification_captain.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../../constant/colors.dart';
import '../About Us/video_page.dart'; import '../About Us/video_page.dart';
import '../assurance_health_page.dart'; import '../assurance_health_page.dart';
import '../maintain_center_page.dart'; import '../maintain_center_page.dart';
@@ -227,12 +228,57 @@ class _DrawerItemTile extends StatelessWidget {
} }
// --- ويدجت محسنة للجزء العلوي من القائمة --- // --- ويدجت محسنة للجزء العلوي من القائمة ---
// ... (الاستيرادات السابقة تبقى كما هي)
// --- تم تعديل UserHeader لإضافة التحقق من الصورة ---
class UserHeader extends StatelessWidget { class UserHeader extends StatelessWidget {
UserHeader({super.key}); UserHeader({super.key});
final ImageController imageController = Get.find<ImageController>(); final ImageController imageController = Get.find<ImageController>();
final HomeCaptainController homeCaptainController = final HomeCaptainController homeCaptainController =
Get.find<HomeCaptainController>(); Get.find<HomeCaptainController>();
// دالة لإظهار التنبيه
void _showUploadPhotoDialog(
BuildContext context, ImageController controller) {
// نستخدم addPostFrameCallback لضمان عدم ظهور الخطأ أثناء بناء الواجهة
WidgetsBinding.instance.addPostFrameCallback((_) {
// نتأكد ألا يكون هناك dialog مفتوح بالفعل لتجنب التكرار
if (Get.isDialogOpen == true) return;
Get.defaultDialog(
title: "Profile Photo Required".tr,
titleStyle:
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
middleText:
"Please upload a clear photo of your face to be identified by passengers."
.tr,
barrierDismissible: false, // منع الإغلاق بالضغط خارج النافذة
radius: 15,
contentPadding: const EdgeInsets.all(20),
confirm: ElevatedButton.icon(
onPressed: () {
Get.back(); // إغلاق النافذة الحالية
// فتح الكاميرا فوراً
controller.choosImagePicture(
AppLink.uploadImagePortrate, 'portrait');
},
icon: const Icon(Icons.camera_alt, color: Colors.white),
label: Text("Take Photo Now".tr,
style: const TextStyle(color: Colors.white)),
style: ElevatedButton.styleFrom(
backgroundColor: AppColor
.primaryColor, // تأكد من وجود هذا اللون أو استبدله بـ Colors.blue
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
),
),
cancel: TextButton(
onPressed: () => Get.back(),
child: Text("Later".tr, style: const TextStyle(color: Colors.grey)),
),
);
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return UserAccountsDrawerHeader( return UserAccountsDrawerHeader(
@@ -262,8 +308,23 @@ class UserHeader extends StatelessWidget {
child: controller.isloading child: controller.isloading
? const CircularProgressIndicator(color: Colors.white) ? const CircularProgressIndicator(color: Colors.white)
: CircleAvatar( : CircleAvatar(
// محاولة تحميل الصورة
backgroundImage: NetworkImage( backgroundImage: NetworkImage(
'${AppLink.server}/portrate_captain_image/${box.read(BoxName.driverID)}.jpg'), '${AppLink.server}/portrate_captain_image/${box.read(BoxName.driverID)}.jpg'),
// [تعديل هام]: في حال فشل تحميل الصورة (غير موجودة)
onBackgroundImageError: (exception, stackTrace) {
// طباعة الخطأ في الكونسول للتوضيح
debugPrint(
"Profile image not found or error loading: $exception");
// استدعاء نافذة التنبيه
_showUploadPhotoDialog(context, controller);
},
// أيقونة بديلة تظهر في الخلفية إذا لم تكن الصورة موجودة
backgroundColor: Colors.grey.shade300,
child: const Icon(Icons.person,
size: 40, color: Colors.white),
), ),
), ),
Positioned( Positioned(

View File

@@ -17,6 +17,7 @@ import '../../../../constant/box_name.dart';
import '../../../../constant/colors.dart'; import '../../../../constant/colors.dart';
import '../../../../constant/info.dart'; import '../../../../constant/info.dart';
import '../../../../constant/style.dart'; import '../../../../constant/style.dart';
import '../../../../controller/functions/location_background_controller.dart';
import '../../../../controller/functions/location_controller.dart'; import '../../../../controller/functions/location_controller.dart';
import '../../../../controller/functions/overlay_permisssion.dart'; import '../../../../controller/functions/overlay_permisssion.dart';
import '../../../../controller/functions/package_info.dart'; import '../../../../controller/functions/package_info.dart';
@@ -44,29 +45,33 @@ class HomeCaptain extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Initial calls remain the same. // Initial calls remain the same.
// Get.put(HomeCaptainController()); // Get.put(HomeCaptainController());
WidgetsBinding.instance.addPostFrameCallback((_) async {
closeOverlayIfFound();
checkForUpdate(context);
getPermissionOverlay();
showDriverGiftClaim(context);
checkForAppliedRide(context);
});
WidgetsBinding.instance.addPostFrameCallback((_) async {
print("🔥 HomeCaptain postFrameCallback started"); // Debug
await closeOverlayIfFound();
await checkForUpdate(context);
await getPermissionOverlay();
await showDriverGiftClaim(context);
await checkForAppliedRide(context);
print("✅ postFrameCallback completed");
});
// The stack is now even simpler. // The stack is now even simpler.
return Scaffold( return Scaffold(
appBar: const _HomeAppBar(), appBar: const _HomeAppBar(),
drawer: AppDrawer(), drawer: AppDrawer(),
body: Stack( body: SafeArea(
children: [ child: Stack(
// 1. The Map View is the base layer. children: [
const _MapView(), // 1. The Map View is the base layer.
const _MapView(),
// 2. The new floating "Status Pod" at the bottom. // 2. The new floating "Status Pod" at the bottom.
const _StatusPodOverlay(), const _StatusPodOverlay(),
FloatingActionButtons(), FloatingActionButtons(),
// This widget from the original code remains. // This widget from the original code remains.
leftMainMenuCaptainIcons(), leftMainMenuCaptainIcons(),
], ],
),
), ),
); );
} }
@@ -139,12 +144,12 @@ class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
tooltip: 'Change Map Type'.tr, tooltip: 'Change Map Type'.tr,
onPressed: homeCaptainController.changeMapType, onPressed: homeCaptainController.changeMapType,
), ),
_MapControlButton( // _MapControlButton(
iconColor: Colors.blue, // iconColor: Colors.blue,
icon: Icons.streetview_sharp, // icon: Icons.streetview_sharp,
tooltip: 'Toggle Traffic'.tr, // tooltip: 'Toggle Traffic'.tr,
onPressed: homeCaptainController.changeMapTraffic, // onPressed: homeCaptainController.changeMapTraffic,
), // ),
GetBuilder<HomeCaptainController>( GetBuilder<HomeCaptainController>(
builder: (controller) { builder: (controller) {
return _MapControlButton( return _MapControlButton(
@@ -250,6 +255,7 @@ class _MapView extends StatelessWidget {
// --- تم حذف onCameraMove الخاطئ --- // --- تم حذف onCameraMove الخاطئ ---
// === إضافة الطبقة الحرارية هنا === // === إضافة الطبقة الحرارية هنا ===
polygons: controller.heatmapPolygons, polygons: controller.heatmapPolygons,
// = // =
markers: { markers: {
Marker( Marker(
@@ -346,9 +352,10 @@ class _MapView extends StatelessWidget {
class _StatusPodOverlay extends StatelessWidget { class _StatusPodOverlay extends StatelessWidget {
const _StatusPodOverlay(); const _StatusPodOverlay();
void _showDetailsDialog(BuildContext context) { void _showDetailsDialog(
BuildContext context, HomeCaptainController controller) {
Get.dialog( Get.dialog(
const _DriverDetailsDialog(), _DriverDetailsDialog(controller), // تمرير الكنترولر هنا
barrierColor: Colors.black.withOpacity(0.3), barrierColor: Colors.black.withOpacity(0.3),
); );
} }
@@ -361,7 +368,7 @@ class _StatusPodOverlay extends StatelessWidget {
left: 16, left: 16,
right: 16, right: 16,
child: GestureDetector( child: GestureDetector(
onTap: () => _showDetailsDialog(context), onTap: () => _showDetailsDialog(context, homeCaptainController),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
child: BackdropFilter( child: BackdropFilter(
@@ -435,11 +442,16 @@ class _StatusPodOverlay extends StatelessWidget {
/// 4. The Dialog that shows detailed driver stats. /// 4. The Dialog that shows detailed driver stats.
class _DriverDetailsDialog extends StatelessWidget { class _DriverDetailsDialog extends StatelessWidget {
const _DriverDetailsDialog(); // 1. إضافة متغير للكنترولر
final HomeCaptainController controller;
// 2. تحديث البناء لاستقباله
const _DriverDetailsDialog(this.controller);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final homeCaptainController = Get.find<HomeCaptainController>(); // 3. حذف السطر الذي يسبب الخطأ: final homeCaptainController = Get.find...
return BackdropFilter( return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: AlertDialog( child: AlertDialog(
@@ -463,27 +475,28 @@ class _DriverDetailsDialog extends StatelessWidget {
icon: Entypo.wallet, icon: Entypo.wallet,
color: AppColor.greenColor, color: AppColor.greenColor,
label: 'Today'.tr, label: 'Today'.tr,
value: homeCaptainController.totalMoneyToday.toString(), // استخدام المتغير controller الذي تم تمريره
value: controller.totalMoneyToday.toString(),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildStatRow( _buildStatRow(
icon: Entypo.wallet, icon: Entypo.wallet,
color: AppColor.yellowColor, color: AppColor.yellowColor,
label: AppInformation.appName, label: AppInformation.appName,
value: homeCaptainController.totalMoneyInSEFER.toString(), value: controller.totalMoneyInSEFER.toString(),
), ),
const Divider(height: 24), const Divider(height: 24),
_buildDurationRow( _buildDurationRow(
icon: Icons.timer_outlined, icon: Icons.timer_outlined,
label: 'Active Duration:'.tr, label: 'Active Duration:'.tr,
value: homeCaptainController.stringActiveDuration, value: controller.stringActiveDuration,
color: AppColor.greenColor, color: AppColor.greenColor,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildDurationRow( _buildDurationRow(
icon: Icons.access_time, icon: Icons.access_time,
label: 'Total Connection Duration:'.tr, label: 'Total Connection Duration:'.tr,
value: homeCaptainController.totalDurationToday, value: controller.totalDurationToday,
color: AppColor.accentColor, color: AppColor.accentColor,
), ),
const Divider(height: 24), const Divider(height: 24),
@@ -491,7 +504,7 @@ class _DriverDetailsDialog extends StatelessWidget {
icon: Icons.star_border_rounded, icon: Icons.star_border_rounded,
color: AppColor.blueColor, color: AppColor.blueColor,
label: 'Total Points'.tr, label: 'Total Points'.tr,
value: homeCaptainController.totalPoints.toString(), value: controller.totalPoints.toString(),
), ),
], ],
), ),
@@ -508,6 +521,7 @@ class _DriverDetailsDialog extends StatelessWidget {
); );
} }
// ... بقية الدوال المساعدة (_buildStatRow, _buildDurationRow) تبقى كما هي ...
Widget _buildStatRow( Widget _buildStatRow(
{required IconData icon, {required IconData icon,
required Color color, required Color color,

View File

@@ -20,13 +20,13 @@ class ConnectWidget extends StatelessWidget {
// Get.put(OrderRequestController()); // Get.put(OrderRequestController());
CaptainWalletController captainWalletController = CaptainWalletController captainWalletController =
Get.put(CaptainWalletController()); Get.put(CaptainWalletController());
int refusedRidesToday = 0;
captainWalletController.getCaptainWalletFromBuyPoints(); captainWalletController.getCaptainWalletFromBuyPoints();
return Center( return Center(
child: GetBuilder<HomeCaptainController>( child: GetBuilder<HomeCaptainController>(
builder: (homeCaptainController) => double.parse( builder: (homeCaptainController) => double.parse(
(captainWalletController.totalPoints)) < (captainWalletController.totalPoints)) <
-30000 -200
? CupertinoButton( ? CupertinoButton(
onPressed: () { onPressed: () {
Get.defaultDialog( Get.defaultDialog(
@@ -34,7 +34,7 @@ class ConnectWidget extends StatelessWidget {
barrierDismissible: false, barrierDismissible: false,
title: double.parse( title: double.parse(
(captainWalletController.totalPoints)) < (captainWalletController.totalPoints)) <
-30000 -200
? 'You dont have Points'.tr ? 'You dont have Points'.tr
: 'You Are Stopped For this Day !'.tr, : 'You Are Stopped For this Day !'.tr,
titleStyle: AppStyle.title, titleStyle: AppStyle.title,
@@ -44,7 +44,7 @@ class ConnectWidget extends StatelessWidget {
onPressed: () async { onPressed: () async {
double.parse((captainWalletController double.parse((captainWalletController
.totalPoints)) < .totalPoints)) <
-30000 -200
? await Get.find<TextToSpeechController>() ? await Get.find<TextToSpeechController>()
.speakText( .speakText(
'You must be recharge your Account' 'You must be recharge your Account'
@@ -59,7 +59,7 @@ class ConnectWidget extends StatelessWidget {
Text( Text(
double.parse((captainWalletController double.parse((captainWalletController
.totalPoints)) < .totalPoints)) <
-30000 -200
? 'You must be recharge your Account'.tr ? 'You must be recharge your Account'.tr
: 'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!' : 'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!'
.tr, .tr,
@@ -69,7 +69,7 @@ class ConnectWidget extends StatelessWidget {
), ),
confirm: double.parse( confirm: double.parse(
(captainWalletController.totalPoints)) < (captainWalletController.totalPoints)) <
-30000 -200
? MyElevatedButton( ? MyElevatedButton(
title: 'Recharge my Account'.tr, title: 'Recharge my Account'.tr,
onPressed: () { onPressed: () {

View File

@@ -183,8 +183,13 @@ GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
// child: Builder(builder: (context) { // child: Builder(builder: (context) {
// return IconButton( // return IconButton(
// onPressed: () async { // onPressed: () async {
// Get.to(() => const PhoneNumberScreen()); // NotificationService.sendNotification(
// // box.write(BoxName.statusDriverLocation, 'off'); // target: 'service', // الإرسال لجميع المشتركين في "service"
// title: 'طلب خدمة جديد',
// body: 'تم استلام طلب خدمة جديد. الرجاء مراجعة التفاصيل.',
// isTopic: true,
// category: 'new_service_request', // فئة توضح نوع الإشعار
// );
// }, // },
// icon: const Icon( // icon: const Icon(
// FontAwesome5.grin_tears, // FontAwesome5.grin_tears,

View File

@@ -1,265 +1,218 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:slide_to_act/slide_to_act.dart'; import 'package:slide_to_act/slide_to_act.dart';
import 'package:vibration/vibration.dart';
import 'dart:io';
import '../../../../constant/colors.dart'; import '../../../../constant/colors.dart';
import '../../../../constant/style.dart';
import '../../../../controller/home/captin/map_driver_controller.dart'; import '../../../../controller/home/captin/map_driver_controller.dart';
import '../../../widgets/elevated_btn.dart';
// Changed: إعادة تصميم كاملة للشريط ليصبح شريطًا علويًا عند بدء الرحلة
// ملف: driver_end_ride_bar.dart
Widget driverEndRideBar() { Widget driverEndRideBar() {
// 1. Positioned هي الوالد المباشر (لأنها داخل Stack في الصفحة الرئيسية) return GetBuilder<MapDriverController>(
return Positioned( builder: (controller) {
top: 0, // 🔥 فحص هل السعر ثابت للعرض
left: 0, final String carType = controller.carType;
right: 0, final bool isFixed = (carType == 'Speed' ||
// 2. GetBuilder يكون في الداخل carType == 'Awfar' ||
child: GetBuilder<MapDriverController>( carType == 'Fixed Price');
builder: (controller) => AnimatedContainer(
duration: const Duration(milliseconds: 300), return AnimatedContainer(
// 3. نستخدم التحريك (Translation) لإخفاء الشريط وإظهاره بدلاً من تغيير الـ top duration: const Duration(milliseconds: 400),
curve: Curves.easeOutBack,
transform: Matrix4.translationValues( transform: Matrix4.translationValues(
0, controller.isRideStarted ? 0 : -250, 0), 0,
child: Card( controller.isRideStarted ? 0 : -300,
margin: EdgeInsets.zero, 0,
elevation: 10, ), // Matrix4.translationValues مستخدمة للإزاحة [web:28]
shape: const RoundedRectangleBorder( margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)), padding: const EdgeInsets.all(15),
), decoration: BoxDecoration(
child: Padding( color: Colors.white,
padding: borderRadius: BorderRadius.circular(24),
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), boxShadow: [
child: Column( BoxShadow(
children: [ color: Colors.black.withOpacity(0.1),
if (controller.carType != 'Mishwar Vip') blurRadius: 20,
Row( offset: const Offset(0, 8),
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildInfoColumn(
icon: Icons.social_distance,
text: '${controller.distance} ${'KM'.tr}',
label: 'Distance'.tr,
),
_buildInfoColumn(
icon: Icons.timelapse,
text: controller.hours > 1
? '${controller.hours}h ${controller.minutes}m'
: '${controller.minutes}m',
label: 'Time'.tr,
),
_buildInfoColumn(
icon: Icons.money_sharp,
text:
'${NumberFormat('#,##0').format(double.tryParse(controller.paymentAmount.toString()) ?? 0)} ${'SYP'.tr}',
label: 'Price'.tr,
),
],
),
// ... بقية الكود كما هو (الأزرار والمؤقت)
if (controller.carType != 'Mishwar Vip')
const Divider(height: 20),
const _builtTimerAndCarType(),
const SizedBox(height: 12),
SlideAction(
height: 55,
borderRadius: 15,
elevation: 4,
text: 'Slide to End Trip'.tr,
textStyle: AppStyle.title.copyWith(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
outerColor: AppColor.redColor,
innerColor: Colors.white,
sliderButtonIcon: const Icon(
Icons.arrow_forward_ios,
color: AppColor.redColor,
size: 24,
),
sliderRotate: false,
onSubmit: () {
HapticFeedback.mediumImpact();
controller.finishRideFromDriver();
return null;
},
),
],
),
),
),
),
),
);
}
// New: ودجت لعرض معلومات الرحلة في الشريط العلوي
Widget _buildInfoColumn(
{required IconData icon, required String text, required String label}) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: AppColor.primaryColor),
const SizedBox(height: 4),
Text(text, style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
Text(label,
style:
AppStyle.title.copyWith(color: Colors.grey[600], fontSize: 12)),
],
);
}
// Changed: تم تعديل تصميم ودجت عرض المؤقت ونوع السيارة
class _builtTimerAndCarType extends StatelessWidget {
const _builtTimerAndCarType();
@override
Widget build(BuildContext context) {
// نستخدم GetBuilder هنا لضمان تحديث العداد في كل ثانية
return GetBuilder<MapDriverController>(builder: (controller) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// -- نوع السيارة --
Container(
decoration:
AppStyle.boxDecoration1.copyWith(color: Colors.grey[200]),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
controller.carType.tr,
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
),
),
// -- مؤقت الرحلة --
if (controller.carType != 'Comfort' &&
controller.carType != 'Mishwar Vip' &&
controller.carType != 'Lady') ...[
const SizedBox(width: 10),
Expanded(
child: Container(
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
colors: [
controller.remainingTimeTimerRideBegin < 60
? AppColor.redColor.withOpacity(0.8)
: AppColor.greenColor.withOpacity(0.8),
controller.remainingTimeTimerRideBegin < 60
? AppColor.redColor
: AppColor.greenColor,
],
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Stack(
alignment: Alignment.center,
children: [
LinearProgressIndicator(
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white.withOpacity(0.2)),
minHeight: 40,
// تأكد من أن هذه القيمة بين 0.0 و 1.0 في الكونترولر
value: controller.progressTimerRideBegin.toDouble(),
),
Text(
controller.stringRemainingTimeRideBegin,
style: AppStyle.title.copyWith(
color: Colors.white, fontWeight: FontWeight.bold),
),
],
),
),
),
) )
], ],
], ),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// لوحة العدادات
Container(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.grey.shade200),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 1) المسافة (تتغير دائماً)
_buildLiveMetric(
icon: Icons.alt_route_rounded,
iconColor: Colors.blueAccent,
value: controller.currentRideDistanceKm.toStringAsFixed(2),
unit: 'KM'.tr,
label: 'Traveled'.tr,
),
_buildVerticalDivider(),
// 2) السعر (ثابت أو متغير)
_buildLiveMetric(
icon: isFixed
? Icons.lock_outline_rounded
: Icons.attach_money_rounded,
iconColor: isFixed ? Colors.grey : AppColor.primaryColor,
value: NumberFormat('#,##0').format(
double.tryParse(controller.price.toString()) ?? 0,
),
unit: 'SYP'.tr,
label: isFixed ? 'Fixed Price'.tr : 'Meter Fare'.tr,
isHighlight: true,
isFixedStyle: isFixed,
),
_buildVerticalDivider(),
// 3) الوقت (تصغير الخط + إخفاء الساعات إذا 0)
_buildLiveMetric(
icon: Icons.timer_outlined,
iconColor: Colors.orange,
value: _formatDurationFromStart(controller),
unit: '',
label: 'Duration'.tr,
valueFontSize: 14, // ✅ تصغير خط “المدة”
),
],
),
),
const SizedBox(height: 20),
// زر الإنهاء
SlideAction(
key: ValueKey(controller.isRideFinished),
height: 60,
borderRadius: 18,
elevation: 0,
text: 'Swipe to End Trip'.tr,
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
outerColor: AppColor.redColor,
innerColor: Colors.white,
sliderButtonIcon: const Icon(
Icons.stop_circle_outlined,
color: AppColor.redColor,
size: 32,
),
onSubmit: () async {
await controller.finishRideFromDriver(isFromSlider: true);
return null;
},
), // SlideAction مثال الاستخدام موجود في صفحة الحزمة [web:19]
],
),
); );
}); },
} ); // GetBuilder يعيد البناء عند update() من الكنترولر [web:21]
} }
// Changed: تم تعديل مكان ومظهر دائرة السرعة /// دالة تنسيق المدة:
// غيرنا نوع الإرجاع إلى Widget بدلاً من GetBuilder /// - أقل من ساعة: mm:ss
Widget speedCircle() { /// - ساعة فأكثر: h:mm:ss (خانة واحدة للساعات بدون leading zero)
// التحقق من السرعة يمكن أن يبقى هنا أو داخل الـ builder String _formatDurationFromStart(MapDriverController controller) {
// لكن التنبيهات (Vibration/Dialog) يفضل أن تكون داخل الـ builder لتجنب تكرارها أثناء إعادة البناء الخارجية if (controller.rideStartTime == null) return "00:00";
return Positioned( final d = DateTime.now().difference(controller.rideStartTime!);
// New: Positioned الآن هي الوالد المباشر (يجب وضع هذه الدالة داخل Stack في الصفحة الرئيسية)
bottom: 25,
left: 3,
child: GetBuilder<MapDriverController>(
builder: (controller) {
// التحقق من التنبيهات هنا
if (controller.speed > 100) {
// نستخدم addPostFrameCallback لضمان عدم استدعاء الـ Dialog أثناء عملية البناء
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!Get.isDialogOpen!) {
// تجنب فتح أكثر من نافذة
if (Platform.isIOS) {
HapticFeedback.selectionClick();
} else {
Vibration.vibrate(duration: 1000);
}
Get.defaultDialog(
barrierDismissible: false,
titleStyle: AppStyle.title,
title: 'Speed Over'.tr,
middleText: 'Please slow down'.tr,
middleTextStyle: AppStyle.title,
confirm: MyElevatedButton(
title: 'I will slow down'.tr,
onPressed: () => Get.back(),
),
);
}
});
}
return controller.isRideStarted String twoDigits(int n) => n.toString().padLeft(2, "0");
? Container(
decoration: BoxDecoration( final hours = d.inHours;
shape: BoxShape.circle, final minutes = d.inMinutes.remainder(60);
color: Colors.white, final seconds = d.inSeconds.remainder(60);
boxShadow: const [
BoxShadow(blurRadius: 5, color: Colors.black26) if (hours == 0) {
], // mm:ss
border: Border.all( final totalMinutes = d.inMinutes;
width: 4, return "${twoDigits(totalMinutes)}:${twoDigits(seconds)}";
color: controller.speed > 100 }
? Colors.red
: AppColor.greenColor, // h:mm:ss
), return "$hours:${twoDigits(minutes)}:${twoDigits(seconds)}";
} // Duration وتفكيكه (inHours/inMinutes/inSeconds) من أساسيات Dart [web:11]
Widget _buildLiveMetric({
required IconData icon,
required Color iconColor,
required String value,
required String unit,
required String label,
bool isHighlight = false,
bool isFixedStyle = false,
double? valueFontSize, // ✅ جديد: حجم خط القيمة فقط
}) {
final effectiveFontSize = valueFontSize ?? (isHighlight ? 20 : 18);
return Expanded(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 14, color: iconColor),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: TextStyle(
fontSize: effectiveFontSize, // ✅ هنا صار التحكم
fontWeight: FontWeight.w900,
color: isFixedStyle
? Colors.grey[800]
: (isHighlight ? AppColor.primaryColor : Colors.black87),
fontFamily: 'monospace',
),
),
if (unit.isNotEmpty) ...[
const SizedBox(width: 2),
Text(
unit,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
), ),
height: 70, ),
width: 70, ]
child: Center( ],
child: Column( ),
mainAxisAlignment: MainAxisAlignment.center, ],
children: [
Text(
controller.speed.toStringAsFixed(0),
style: AppStyle.number.copyWith(fontSize: 24),
),
const Text("km/h", style: TextStyle(fontSize: 10)),
],
),
),
)
: const SizedBox(); // إذا لم تبدأ الرحلة نخفي العنصر وهو داخل الـ Positioned
},
), ),
); );
} }
Widget _buildVerticalDivider() {
return Container(height: 35, width: 1, color: Colors.grey.shade300);
}

View File

@@ -55,16 +55,16 @@ class GoogleDriverMap extends StatelessWidget {
trafficEnabled: true, // Changed: تفعيل عرض حركة المرور trafficEnabled: true, // Changed: تفعيل عرض حركة المرور
buildingsEnabled: true, buildingsEnabled: true,
polylines: { polylines: {
Polyline( // Polyline(
zIndex: 2, // zIndex: 2,
polylineId: const PolylineId('route1'), // polylineId: const PolylineId('route1'),
points: controller.polylineCoordinates, // points: controller.polylineCoordinates,
color: const Color.fromARGB(255, 163, 81, 246), // color: const Color.fromARGB(255, 163, 81, 246),
width: 6, // Changed: زيادة عرض الخط // width: 6, // Changed: زيادة عرض الخط
startCap: Cap.roundCap, // startCap: Cap.roundCap,
endCap: Cap.roundCap, // endCap: Cap.roundCap,
), // ),
// Polyline( // Polyline(
// zIndex: 2, // zIndex: 2,

View File

@@ -12,43 +12,51 @@ class GoogleMapApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetBuilder<MapDriverController>( return GetBuilder<MapDriverController>(
builder: (mapDriverController) => mapDriverController.isRideStarted builder: (mapDriverController) {
? Positioned( if (!mapDriverController.isRideStarted) return const SizedBox();
right: 3,
bottom: 20,
child: Container(
decoration: AppStyle.boxDecoration,
child: IconButton(
onPressed: () async {
var startLat = Get.find<MapDriverController>()
.latLngPassengerLocation
.latitude;
var startLng = Get.find<MapDriverController>()
.latLngPassengerLocation
.longitude;
var endLat = Get.find<MapDriverController>() // REMOVED: Positioned wrapper
.latLngPassengerDestination return Material(
.latitude; elevation: 8,
var endLng = Get.find<MapDriverController>() shadowColor: Colors.black26,
.latLngPassengerDestination borderRadius: BorderRadius.circular(30),
.longitude; color: Colors.white,
child: InkWell(
borderRadius: BorderRadius.circular(30),
onTap: () async {
var endLat =
mapDriverController.latLngPassengerDestination.latitude;
var endLng =
mapDriverController.latLngPassengerDestination.longitude;
String url = 'google.navigation:q=$endLat,$endLng';
String url = if (await canLaunchUrl(Uri.parse(url))) {
'https://www.google.com/maps/dir/$startLat,$startLng/$endLat,$endLng/&directionsmode=driving'; await launchUrl(Uri.parse(url));
if (await canLaunchUrl(Uri.parse(url))) { } else {
await launchUrl(Uri.parse(url)); String webUrl =
} else { 'https://www.google.com/maps/dir/?api=1&destination=$endLat,$endLng';
throw 'Could not launch google maps'; if (await canLaunchUrl(Uri.parse(webUrl))) {
} await launchUrl(Uri.parse(webUrl));
}, }
icon: const Icon( }
MaterialCommunityIcons.map_marker_radius, },
size: 45, child: Container(
color: AppColor.blueColor, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
), decoration: BoxDecoration(
)), color: Colors.blueAccent,
) borderRadius: BorderRadius.circular(30),
: const SizedBox()); border: Border.all(
color: AppColor.blueColor.withOpacity(0.2), width: 1),
),
child: const Icon(
MaterialCommunityIcons.google_maps,
size: 28,
color: AppColor.secondaryColor,
),
),
),
);
},
);
} }
} }

View File

@@ -2,135 +2,335 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sefer_driver/constant/colors.dart'; import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart'; import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import '../../../../constant/box_name.dart';
import '../../../../constant/style.dart'; import '../../../../constant/style.dart';
import '../../../../controller/firebase/notification_service.dart';
import '../../../../main.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart'; import 'package:sefer_driver/views/widgets/mydialoug.dart';
import '../../../../controller/functions/launch.dart';
import '../../../../controller/functions/location_controller.dart';
import '../../../widgets/error_snakbar.dart';
class PassengerInfoWindow extends StatelessWidget { class PassengerInfoWindow extends StatelessWidget {
PassengerInfoWindow({super.key}); const PassengerInfoWindow({super.key});
// Optimization: defining static styles here avoids rebuilding them every frame
final TextStyle _labelStyle =
AppStyle.title.copyWith(color: Colors.grey[600], fontSize: 13);
final TextStyle _valueStyle =
AppStyle.title.copyWith(fontWeight: FontWeight.bold, fontSize: 18);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Get safe area top padding (for Notches/Status bars) // 1. حساب الهوامش الآمنة (SafeArea) من الأسفل
final double topPadding = MediaQuery.of(context).padding.top; final double safeBottomPadding = MediaQuery.of(context).padding.bottom;
final double topMargin = topPadding + 10; // Safe area + 10px spacing
return GetBuilder<MapDriverController>( return GetBuilder<MapDriverController>(
builder: (controller) => AnimatedPositioned( // id: 'PassengerInfo',
duration: const Duration(milliseconds: 400), builder: (controller) {
curve: Curves.easeInOut, // --- 1. تجهيز بيانات العرض ---
// FIX: Use calculated top margin to avoid hiding behind status bar String displayName = controller.passengerName ?? "Unknown";
top: controller.isPassengerInfoWindow ? topMargin : -250.0, String avatarText = "";
left: 15.0,
right: 15.0, // التحقق من اللغة (عربي/إنجليزي) للاسم المختصر
child: Card( bool isArabic = RegExp(r'[\u0600-\u06FF]').hasMatch(displayName);
// Optimization: Lower elevation slightly for smoother animation on cheap phones
elevation: 4, if (displayName.isNotEmpty) {
shadowColor: Colors.black.withOpacity(0.2), if (isArabic) {
color: Colors.white, avatarText = displayName.split(' ').first;
surfaceTintColor: Colors.white, // Fix for Material 3 tinting if (avatarText.length > 4) {
shape: RoundedRectangleBorder( avatarText = avatarText.substring(0, 4);
borderRadius: BorderRadius.circular(16), }
), } else {
child: Padding( avatarText = displayName[0].toUpperCase();
padding: }
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), }
// --- 2. المنطق الذكي للموقع (Smart Positioning) ---
// نرفع النافذة إذا ظهر شريط التعليمات في الأسفل لتجنب التغطية
bool hasInstructions = controller.currentInstruction.isNotEmpty;
double instructionsHeight = hasInstructions ? 110.0 : 0.0;
// الموقع النهائي: إذا كانت مفعلة تظهر، وإلا تختفي للأسفل
double finalBottomPosition = controller.isPassengerInfoWindow
? (safeBottomPadding + 10 + instructionsHeight)
: -450.0;
return AnimatedPositioned(
duration: const Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn,
bottom: finalBottomPosition,
left: 12.0,
right: 12.0,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 25,
offset: const Offset(0, 8),
spreadRadius: 2,
)
],
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildTopInfoRow(controller), // --- مقبض السحب (Visual Handle) ---
const Divider(height: 16), Center(
child: Container(
margin: const EdgeInsets.only(top: 8, bottom: 4),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(10),
),
),
),
if (!controller.isRideBegin) _buildActionButtons(controller), Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
child: Column(
children: [
// --- الصف العلوي: معلومات الراكب ---
Row(
children: [
// الصورة الرمزية
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.primaryColor.withOpacity(0.1),
border: Border.all(
color:
AppColor.primaryColor.withOpacity(0.2)),
),
child: Center(
child: Text(
avatarText,
style: TextStyle(
color: AppColor.primaryColor,
fontWeight: FontWeight.bold,
fontSize: isArabic ? 14 : 20,
),
),
),
),
const SizedBox(width: 12),
// Optimization: Only render linear indicator if needed // النصوص (الاسم والمسافة)
if (controller.remainingTimeInPassengerLocatioWait < 300 && Expanded(
controller.remainingTimeInPassengerLocatioWait != 0 && child: Column(
!controller.isRideBegin) ...[ crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: 10), children: [
_buildWaitingIndicator(controller), Text(
], displayName,
style: AppStyle.title.copyWith(
fontWeight: FontWeight.w800,
fontSize: 16),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.location_on,
size: 14, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'${controller.distance} km',
style: TextStyle(
color: Colors.grey[700],
fontSize: 13,
fontWeight: FontWeight.w600),
),
const SizedBox(width: 10),
Icon(Icons.access_time_filled,
size: 14, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
controller.hours > 0
? '${controller.hours}h ${controller.minutes}m'
: '${controller.minutes} min',
style: TextStyle(
color: Colors.grey[700],
fontSize: 13,
fontWeight: FontWeight.w600),
),
],
),
],
),
),
if (controller.isdriverWaitTimeEnd && // أزرار جانبية (سرعة + اتصال)
!controller.isRideBegin) ...[ Row(
const SizedBox(height: 10), children: [
_buildCancelAfterWaitButton(controller), _buildSpeedCircle(),
] const SizedBox(width: 10),
InkWell(
onTap: () async {
controller.isSocialPressed = true;
// نفحص النتيجة: هل مسموح له يتصل؟
bool canCall =
await controller.driverCallPassenger();
if (canCall) {
makePhoneCall(
controller.passengerPhone.toString());
} else {
// هنا ممكن تظهر رسالة: تم منع الاتصال بسبب كثرة الإلغاءات
mySnackeBarError(
"You cannot call the passenger due to policy violations"
.tr);
}
},
borderRadius: BorderRadius.circular(50),
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
shape: BoxShape.circle,
border: Border.all(
color: Colors.green.withOpacity(0.2)),
),
child: const Icon(Icons.phone,
color: Colors.green, size: 22),
),
),
],
),
],
),
// خط فاصل
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Divider(height: 1, color: Colors.grey.shade100),
),
// --- مؤشر الانتظار (يظهر عند الوصول) ---
if (controller.remainingTimeInPassengerLocatioWait <
300 &&
controller.remainingTimeInPassengerLocatioWait != 0 &&
!controller.isRideBegin) ...[
_buildWaitingIndicator(controller),
const SizedBox(height: 12),
],
// --- الأزرار الرئيسية (وصلت / ابدأ) ---
if (!controller.isRideBegin)
_buildActionButtons(controller),
// --- زر الإلغاء المدفوع (بعد انتهاء وقت الانتظار) ---
if (controller.isdriverWaitTimeEnd &&
!controller.isRideBegin)
Padding(
padding: const EdgeInsets.only(top: 10),
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFFF0F0),
foregroundColor: Colors.red,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(
color: Color(0xFFFFCDCD)),
),
),
onPressed: () {
MyDialog().getDialog(
'Confirm Cancellation'.tr,
'Are you sure you want to cancel and collect the fee?'
.tr, () async {
// كود الإلغاء
Get.back();
controller
.addWaitingTimeCostFromPassengerToDriverWallet();
});
},
icon: const Icon(Icons.money_off, size: 20),
label: Text('Cancel & Collect Fee'.tr,
style: const TextStyle(
fontWeight: FontWeight.bold)),
),
),
),
],
),
),
], ],
), ),
), ),
), );
), },
); );
} }
Widget _buildTopInfoRow(MapDriverController controller) { // --- Widgets مساعدة ---
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, Widget _buildSpeedCircle() {
crossAxisAlignment: CrossAxisAlignment.start, // Align top return GetBuilder<LocationController>(builder: (locController) {
children: [ int speedKmh = (locController.speed * 3.6).round();
Expanded( Color color = speedKmh > 100 ? Colors.red : const Color(0xFF0D47A1);
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, return Container(
children: [ width: 42,
Text('Go to passenger:'.tr, style: _labelStyle), height: 42,
const SizedBox(height: 2), decoration: BoxDecoration(
Text( color: Colors.white,
controller.passengerName ?? 'loading...', shape: BoxShape.circle,
style: _valueStyle, border: Border.all(color: color.withOpacity(0.3), width: 2),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
), ),
const SizedBox(width: 10), // Spacing between name and chips child: Column(
Column( mainAxisAlignment: MainAxisAlignment.center,
// Changed to Column for better layout on small screens
crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
_buildInfoChip(Icons.map_outlined, '${controller.distance} km'), Text('$speedKmh',
const SizedBox(height: 6), // Vertical spacing style: TextStyle(
_buildInfoChip( color: color,
Icons.timer_outlined, fontWeight: FontWeight.w900,
controller.hours > 1 fontSize: 13,
? '${controller.hours}h ${controller.minutes}m' height: 1)),
: '${controller.minutes}m', Text('km/h',
), style: TextStyle(
color: color.withOpacity(0.7), fontSize: 8, height: 1)),
], ],
), ),
], );
); });
} }
Widget _buildInfoChip(IconData icon, String text) { Widget _buildWaitingIndicator(MapDriverController controller) {
bool isUrgent = controller.remainingTimeInPassengerLocatioWait < 60;
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColor.primaryColor.withOpacity(0.1), color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(8),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(icon, color: AppColor.primaryColor, size: 14), // Smaller icon Icon(Icons.timer_outlined,
const SizedBox(width: 6), size: 16, color: isUrgent ? Colors.red : Colors.green),
const SizedBox(width: 8),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value:
controller.progressInPassengerLocationFromDriver.toDouble(),
backgroundColor: Colors.grey[200],
color: isUrgent ? Colors.red : Colors.green,
minHeight: 6,
),
),
),
const SizedBox(width: 10),
Text( Text(
text, controller.stringRemainingTimeWaitingPassenger,
style: TextStyle( style: TextStyle(
color: AppColor.primaryColor, fontWeight: FontWeight.w900,
fontWeight: FontWeight.bold, color: isUrgent ? Colors.red : Colors.green,
fontSize: 12 // Slightly smaller font for chips fontFamily: 'monospace'),
),
), ),
], ],
), ),
@@ -138,155 +338,55 @@ class PassengerInfoWindow extends StatelessWidget {
} }
Widget _buildActionButtons(MapDriverController controller) { Widget _buildActionButtons(MapDriverController controller) {
return Row( if (controller.isArrivedSend) {
children: [ return SizedBox(
if (controller.isArrivedSend) width: double.infinity,
Expanded( height: 50,
flex: 1, child: ElevatedButton.icon(
child: SizedBox( style: ElevatedButton.styleFrom(
height: 45, // Fixed height for consistency backgroundColor: const Color(0xFFF1C40F),
child: ElevatedButton( foregroundColor: Colors.white,
style: ElevatedButton.styleFrom( elevation: 2,
backgroundColor: AppColor.yellowColor, shape:
foregroundColor: Colors.black, RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: EdgeInsets.zero, // Reduce padding to fit text
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
onPressed: () async {
// LOGIC FIX: Check distance FIRST
double distance = await controller
.calculateDistanceBetweenDriverAndPassengerLocation();
if (distance < 140) {
// Only draw route and send notif if close enough
controller.getRoute(
origin: controller.latLngPassengerLocation,
destination: controller.latLngPassengerDestination,
routeColor: Colors.blue);
NotificationService.sendNotification(
target: controller.tokenPassenger.toString(),
title: 'Hi ,I Arrive your site'.tr,
body: 'I Arrive at your site'.tr,
isTopic: false,
tone: 'ding',
driverList: [],
category: 'Hi ,I Arrive your site',
);
controller.startTimerToShowDriverWaitPassengerDuration();
controller.isArrivedSend = false;
} else {
MyDialog().getDialog(
'You are not near'.tr, // Shortened title
'Please go to the pickup location exactly'.tr,
() => Get.back());
}
},
// Using Row instead of .icon constructor for better control
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.location_on, size: 16),
const SizedBox(width: 4),
Flexible(
child: Text('I Arrive'.tr,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12))),
],
),
),
),
), ),
if (controller.isArrivedSend) const SizedBox(width: 8), onPressed: () async {
Expanded( await controller.markDriverAsArrived();
flex: 2, // Give "Start" button more space },
child: SizedBox( icon: const Icon(Icons.near_me_rounded),
height: 45, label: Text('I Have Arrived'.tr,
child: ElevatedButton( style:
style: ElevatedButton.styleFrom( const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
backgroundColor: AppColor.greenColor, ),
foregroundColor: Colors.white, );
shape: RoundedRectangleBorder( } else {
borderRadius: BorderRadius.circular(10)), return SizedBox(
), width: double.infinity,
onPressed: () { height: 50,
MyDialog().getDialog( child: ElevatedButton.icon(
"Is the Passenger in your Car?".tr, style: ElevatedButton.styleFrom(
"Don't start trip if passenger not in your car".tr, backgroundColor: const Color(0xFF27AE60),
() async { foregroundColor: Colors.white,
await controller.startRideFromDriver(); elevation: 2,
Get.back(); shape:
}, RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
); ),
onPressed: () {
MyDialog().getDialog(
"Start Trip?".tr,
"Ensure the passenger is in the car.".tr,
() async {
await controller.startRideFromDriver();
Get.back();
}, },
child: Row( );
mainAxisAlignment: MainAxisAlignment.center, },
children: [ icon: const Icon(Icons.play_circle_fill_rounded),
const Icon(Icons.play_arrow_rounded, size: 22), label: Text('Start Ride'.tr,
const SizedBox(width: 6), style:
Flexible( const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
child: Text('Start the Ride'.tr,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.bold))),
],
),
),
),
), ),
], );
); }
}
Widget _buildWaitingIndicator(MapDriverController controller) {
return Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: LinearProgressIndicator(
backgroundColor: AppColor.greyColor.withOpacity(0.2),
// Ternary for color is fine
color: controller.remainingTimeInPassengerLocatioWait < 60
? AppColor.redColor
: AppColor.greenColor,
minHeight: 8, // Thinner looks more modern
value: controller.progressInPassengerLocationFromDriver.toDouble(),
),
),
const SizedBox(height: 4),
Text(
"${'Waiting'.tr}: ${controller.stringRemainingTimeWaitingPassenger}",
style: AppStyle.title.copyWith(
color: Colors.grey[700],
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
);
}
Widget _buildCancelAfterWaitButton(MapDriverController controller) {
return MyElevatedButton(
title: 'Cancel Trip & Get Cost'.tr, // Shortened text
kolor: AppColor.gold,
onPressed: () {
MyDialog().getDialog('Are you sure to cancel?'.tr, '', () async {
NotificationService.sendNotification(
target: controller.tokenPassenger.toString(),
title: 'Driver Cancelled Your Trip'.tr,
body: 'You will need to pay the cost...',
isTopic: false,
tone: 'cancel',
driverList: [],
category: 'Driver Cancelled Your Trip',
);
box.write(BoxName.rideStatus, 'Cancel');
await controller.addWaitingTimeCostFromPassengerToDriverWallet();
controller.isdriverWaitTimeEnd = false;
Get.back();
});
},
);
} }
} }

View File

@@ -1,261 +1,89 @@
// import 'dart:io';
// import 'package:bubble_head/bubble.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_font_icons/flutter_font_icons.dart';
// import 'package:get/get.dart';
// import 'package:sefer_driver/constant/info.dart';
// import 'package:sefer_driver/controller/functions/location_controller.dart';
// import 'package:sefer_driver/views/widgets/elevated_btn.dart';
// import 'package:sefer_driver/views/widgets/my_textField.dart';
// import 'package:url_launcher/url_launcher.dart';
// import '../../../../constant/box_name.dart';
// import '../../../../constant/colors.dart';
// import '../../../../constant/style.dart';
// import '../../../../controller/functions/launch.dart';
// import '../../../../controller/home/captin/map_driver_controller.dart';
// import '../../../../main.dart';
// class SosConnect extends StatelessWidget {
// const SosConnect({super.key});
// @override
// Widget build(BuildContext context) {
// return GetBuilder<MapDriverController>(
// builder: (mapDriverController) => mapDriverController.isRideStarted
// ? Positioned(
// left: 16,
// bottom: 16,
// child: Card(
// elevation: 4,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12),
// ),
// child: SizedBox(
// height: 60,
// width: 180,
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceAround,
// children: [
// IconButton(
// onPressed: () {
// _handleSosCall(mapDriverController);
// },
// icon: const Icon(
// Icons.sos_sharp,
// size: 32,
// color: AppColor.redColor,
// ),
// tooltip: 'SOS - Call Emergency',
// ),
// VerticalDivider(
// color: Colors.grey[300],
// thickness: 1,
// ),
// IconButton(
// onPressed: () {
// _handleWhatsApp(mapDriverController);
// },
// icon: const Icon(
// FontAwesome.whatsapp,
// color: AppColor.greenColor,
// size: 32,
// ),
// tooltip: 'SOS - Send WhatsApp Message',
// ),
// VerticalDivider(
// color: Colors.grey[300],
// thickness: 1,
// ),
// IconButton(
// onPressed: () {
// _handleGoogleMap(mapDriverController);
// },
// icon: const Icon(
// MaterialCommunityIcons.map_marker_radius,
// color: AppColor.primaryColor,
// size: 32,
// ),
// tooltip: 'Google Maps - Navigate',
// ),
// ],
// ),
// ),
// ),
// )
// : const SizedBox(),
// );
// }
// void _handleSosCall(MapDriverController mapDriverController) {
// if (box.read(BoxName.sosPhoneDriver) == null) {
// Get.defaultDialog(
// title: 'Insert Emergency Number'.tr,
// content: Form(
// key: mapDriverController.formKey1,
// child: MyTextForm(
// controller: mapDriverController.sosEmergincyNumberCotroller,
// label: 'Emergency Number'.tr,
// hint: 'Enter phone number'.tr,
// type: TextInputType.phone,
// ),
// ),
// confirm: MyElevatedButton(
// title: 'Save'.tr,
// onPressed: () {
// if (mapDriverController.formKey1.currentState!.validate()) {
// box.write(BoxName.sosPhoneDriver,
// mapDriverController.sosEmergincyNumberCotroller.text);
// Get.back(); // Close the dialog
// launchCommunication(
// 'phone', box.read(BoxName.sosPhoneDriver), '');
// }
// },
// ),
// );
// } else {
// launchCommunication('phone', box.read(BoxName.sosPhoneDriver), '');
// }
// }
// void _handleWhatsApp(MapDriverController mapDriverController) {
// if (box.read(BoxName.sosPhoneDriver) == null) {
// Get.defaultDialog(
// title: 'Insert Emergency Number'.tr,
// content: Form(
// key: mapDriverController.formKey1,
// child: MyTextForm(
// controller: mapDriverController.sosEmergincyNumberCotroller,
// label: 'Emergency Number'.tr,
// hint: 'Enter phone number'.tr,
// type: TextInputType.phone,
// ),
// ),
// confirm: MyElevatedButton(
// title: 'Save'.tr,
// onPressed: () {
// if (mapDriverController.formKey1.currentState!.validate()) {
// box.write(BoxName.sosPhoneDriver,
// mapDriverController.sosEmergincyNumberCotroller.text);
// Get.back(); // Close the dialog
// _sendWhatsAppMessage(mapDriverController);
// }
// },
// ),
// );
// } else {
// _sendWhatsAppMessage(mapDriverController);
// }
// }
// void _handleGoogleMap(MapDriverController mapDriverController) {
// () async {
// if (Platform.isAndroid) {
// Bubble().startBubbleHead(sendAppToBackground: true);
// }
// var startLat =
// Get.find<MapDriverController>().latLngPassengerLocation.latitude;
// var startLng =
// Get.find<MapDriverController>().latLngPassengerLocation.longitude;
// var endLat =
// Get.find<MapDriverController>().latLngPassengerDestination.latitude;
// var endLng =
// Get.find<MapDriverController>().latLngPassengerDestination.longitude;
// String url =
// 'https://www.google.com/maps/dir/$startLat,$startLng/$endLat,$endLng/&directionsmode=driving';
// if (await canLaunchUrl(Uri.parse(url))) {
// await launchUrl(Uri.parse(url));
// } else {
// throw 'Could not launch google maps';
// }
// }();
// }
// void _sendWhatsAppMessage(MapDriverController mapDriverController) {
// final sosNumber = box.read(BoxName.sosPhoneDriver);
// if (sosNumber != null) {
// launchCommunication(
// 'whatsapp',
// '+2$sosNumber', // Consider international format
// "${"Hello, this is Driver".tr} ${box.read(BoxName.nameDriver)}. "
// "${"My current location is:".tr} "
// "https://www.google.com/maps/place/"
// "${Get.find<LocationController>().myLocation.latitude},"
// "${Get.find<LocationController>().myLocation.longitude} "
// "${"\nI have a trip on".tr} ${AppInformation.appName} "
// "${"app with passenger".tr} ${mapDriverController.passengerName}.",
// );
// }
// }
// }
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart'; import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:sefer_driver/views/widgets/my_textField.dart'; import 'package:sefer_driver/views/widgets/my_textField.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart'; // Checked import
import 'package:flutter_font_icons/flutter_font_icons.dart';
import '../../../../constant/box_name.dart'; import '../../../../constant/box_name.dart';
import '../../../../constant/colors.dart'; import '../../../../constant/colors.dart';
import '../../../../constant/style.dart'; import '../../../../constant/style.dart';
import '../../../../controller/firebase/firbase_messge.dart';
import '../../../../controller/firebase/notification_service.dart'; import '../../../../controller/firebase/notification_service.dart';
import '../../../../controller/functions/launch.dart'; import '../../../../controller/functions/launch.dart';
import '../../../../controller/home/captin/map_driver_controller.dart'; import '../../../../controller/home/captin/map_driver_controller.dart';
import '../../../../main.dart'; import '../../../../main.dart';
// Changed: إعادة تصميم وتغيير موضع أزرار التواصل والطوارئ
class SosConnect extends StatelessWidget { class SosConnect extends StatelessWidget {
SosConnect({super.key}); SosConnect({super.key});
final fcm = Get.isRegistered<FirebaseMessagesController>()
? Get.find<FirebaseMessagesController>()
: Get.put(FirebaseMessagesController());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetBuilder<MapDriverController>( return GetBuilder<MapDriverController>(
id: 'SosConnect', // Keep ID for updates
builder: (controller) { builder: (controller) {
// New: تجميع الأزرار في عمود واحد على الجانب الأيمن // Check visibility logic
return Positioned( bool showPassengerContact =
bottom: 110, // New: فوق عداد السرعة !controller.isRideBegin && controller.isPassengerInfoWindow;
right: 16, bool showSos = controller.isRideStarted;
if (!showPassengerContact && !showSos) return const SizedBox();
// REMOVED: Positioned widget
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.95),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
// زر الاتصال بالراكب (يظهر قبل بدء الرحلة) // === Call Button ===
if (!controller.isRideBegin && controller.isPassengerInfoWindow) if (showPassengerContact)
_buildSocialButton( _buildModernActionButton(
icon: Icons.phone, icon: Icons.phone_in_talk,
color: AppColor.blueColor, color: Colors.white,
bgColor: AppColor.blueColor,
tooltip: 'Call Passenger', tooltip: 'Call Passenger',
onPressed: () async { onTap: () async {
controller.isSocialPressed = true; controller.isSocialPressed = true;
await controller.driverCallPassenger(); bool canCall = await controller.driverCallPassenger();
makePhoneCall(controller.passengerPhone.toString()); if (canCall) {
makePhoneCall(controller.passengerPhone.toString());
} else {
mySnackeBarError("Policy restriction on calls".tr);
}
}, },
), ),
// زر الرسائل للراكب (يظهر قبل بدء الرحلة) if (showPassengerContact) const SizedBox(height: 12),
if (!controller.isRideBegin && controller.isPassengerInfoWindow)
const SizedBox(height: 12), // === Message Button ===
if (!controller.isRideBegin && controller.isPassengerInfoWindow) if (showPassengerContact)
_buildSocialButton( _buildModernActionButton(
icon: Icons.message, icon: MaterialCommunityIcons.message_text_outline,
color: AppColor.greenColor, color: AppColor.primaryColor,
tooltip: 'Send Message', bgColor: Colors.grey.shade100,
onPressed: () { tooltip: 'Message Passenger',
// الكود الخاص بنافذة الرسائل السريعة onTap: () => _showMessageOptions(context, controller),
_showMessageOptions(context, controller);
},
), ),
// زر الطوارئ (SOS) (يظهر بعد بدء الرحلة) // === SOS Button ===
if (controller.isRideStarted) if (showSos)
_buildSocialButton( _buildModernActionButton(
icon: Icons.sos_sharp, icon: MaterialIcons.warning,
color: AppColor.redColor, color: Colors.white,
tooltip: 'SOS - Call Emergency', bgColor: AppColor.redColor,
onPressed: () => _handleSosCall(controller), tooltip: 'EMERGENCY SOS',
isPulsing: true,
onTap: () => _handleSosCall(controller),
), ),
], ],
), ),
@@ -264,42 +92,62 @@ class SosConnect extends StatelessWidget {
); );
} }
// New: ودجت منفصل لبناء أزرار التواصل Widget _buildModernActionButton({
Widget _buildSocialButton( required IconData icon,
{required IconData icon, required Color color,
required Color color, required Color bgColor,
required String tooltip, required String tooltip,
required VoidCallback onPressed}) { required VoidCallback onTap,
return Container( bool isPulsing = false,
decoration: BoxDecoration( }) {
shape: BoxShape.circle, return Material(
color: Colors.white, color: Colors.transparent,
boxShadow: [BoxShadow(blurRadius: 5, color: Colors.black26)], child: InkWell(
), onTap: onTap,
child: IconButton( borderRadius: BorderRadius.circular(12),
icon: Icon(icon, color: color, size: 28), child: Container(
tooltip: tooltip, width: 48,
onPressed: onPressed, height: 48,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
boxShadow: isPulsing
? [
BoxShadow(
color: bgColor.withOpacity(0.4),
blurRadius: 12,
spreadRadius: 2,
)
]
: [],
),
child: Icon(icon, color: color, size: 24),
),
), ),
); );
} }
// الكود الخاص بنافذة إدخال رقم الطوارئ // --- Logic Functions ---
void _handleSosCall(MapDriverController mapDriverController) { void _handleSosCall(MapDriverController mapDriverController) {
if (box.read(BoxName.sosPhoneDriver) == null) { if (box.read(BoxName.sosPhoneDriver) == null) {
Get.defaultDialog( Get.defaultDialog(
title: 'Insert Emergency Number'.tr, title: 'Emergency Contact'.tr,
content: Form( content: Column(
key: mapDriverController.formKey1, children: [
child: MyTextForm( Text('Please enter the emergency number.'.tr),
controller: mapDriverController.sosEmergincyNumberCotroller, Form(
label: 'Emergency Number'.tr, key: mapDriverController.formKey1,
hint: 'Enter phone number'.tr, child: MyTextForm(
type: TextInputType.phone, controller: mapDriverController.sosEmergincyNumberCotroller,
), label: 'Phone Number'.tr,
hint: '01xxxxxxxxx',
type: TextInputType.phone,
),
),
],
), ),
confirm: MyElevatedButton( confirm: MyElevatedButton(
title: 'Save'.tr, title: 'Save & Call'.tr,
onPressed: () { onPressed: () {
if (mapDriverController.formKey1.currentState!.validate()) { if (mapDriverController.formKey1.currentState!.validate()) {
box.write(BoxName.sosPhoneDriver, box.write(BoxName.sosPhoneDriver,
@@ -316,120 +164,70 @@ class SosConnect extends StatelessWidget {
} }
} }
// New: الكود الخاص بنافذة الرسائل السريعة (مستخرج من passenger_info_window.dart)
void _showMessageOptions( void _showMessageOptions(
BuildContext context, MapDriverController controller) { BuildContext context, MapDriverController controller) {
Get.bottomSheet( Get.bottomSheet(
backgroundColor: Colors.white, Container(
shape: const RoundedRectangleBorder( padding: const EdgeInsets.all(20),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)), decoration: const BoxDecoration(
), color: Colors.white,
Padding( borderRadius: BorderRadius.vertical(top: Radius.circular(25)),
padding: const EdgeInsets.all(16.0), ),
child: _buildMessageOptions(controller), child: Column(
), mainAxisSize: MainAxisSize.min,
);
}
Widget _buildMessageOptions(MapDriverController controller) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Select a quick message'.tr, style: AppStyle.title),
const SizedBox(height: 16),
_buildMessageTile(
text: "Where are you, sir?".tr,
onTap: () {
// fcm.sendNotificationToDriverMAP(
// 'message From Driver',
// "Where are you, sir?".tr,
// controller.tokenPassenger,
// [],
// 'ding');
NotificationService.sendNotification(
target: controller.tokenPassenger.toString(),
title: 'message From Driver'.tr,
body: "Where are you, sir?".tr,
isTopic: false, // Important: this is a token
tone: 'ding',
driverList: [], category: 'message From Driver',
);
Get.back();
}),
_buildMessageTile(
text: "I've been trying to reach you but your phone is off.".tr,
onTap: () {
// fcm.sendNotificationToDriverMAP(
// 'message From Driver',
// "I've been trying to reach you but your phone is off.".tr,
// controller.tokenPassenger,
// [],
// 'ding');
NotificationService.sendNotification(
target: controller.tokenPassenger.toString(),
title: 'message From Driver'.tr,
body: "I've been trying to reach you but your phone is off.".tr,
isTopic: false, // Important: this is a token
tone: 'ding',
driverList: [], category: 'message From Driver',
);
Get.back();
}),
const SizedBox(height: 16),
Row(
children: [ children: [
Expanded( Text('Quick Messages'.tr,
child: Form( style:
key: controller.formKey2, const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
child: MyTextForm( const SizedBox(height: 15),
controller: controller.messageToPassenger, _buildQuickMessageItem("Where are you, sir?".tr, controller),
label: 'Type something'.tr, _buildQuickMessageItem("I've arrived.".tr, controller),
hint: 'Type something'.tr, const Divider(),
type: TextInputType.text, Row(
children: [
Expanded(
child: TextField(
controller: controller.messageToPassenger,
decoration:
InputDecoration(hintText: 'Type a message...'.tr),
),
), ),
), IconButton(
), icon: const Icon(Icons.send),
IconButton( onPressed: () {
onPressed: () { _sendMessage(controller, controller.messageToPassenger.text,
// fcm.sendNotificationToDriverMAP( 'cancel');
// 'message From Driver', controller.messageToPassenger.clear();
// controller.messageToPassenger.text, Get.back();
// controller.tokenPassenger, },
// [], ),
// 'ding'); ],
NotificationService.sendNotification(
target: controller.tokenPassenger.toString(),
title: 'message From Driver'.tr,
body: 'change device'.tr,
isTopic: false, // Important: this is a token
tone: 'cancel',
driverList: [], category: 'message From Driver',
);
controller.messageToPassenger.clear();
Get.back();
},
icon: const Icon(Icons.send),
), ),
], ],
), ),
],
);
}
Widget _buildMessageTile(
{required String text, required VoidCallback onTap}) {
return InkWell(
onTap: onTap,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.grey[100],
),
child: Text(text, style: AppStyle.title),
), ),
); );
} }
Widget _buildQuickMessageItem(String text, MapDriverController controller) {
return ListTile(
title: Text(text),
onTap: () {
_sendMessage(controller, text, 'ding');
Get.back();
},
);
}
void _sendMessage(MapDriverController controller, String body, String tone) {
NotificationService.sendNotification(
target: controller.tokenPassenger.toString(),
title: 'Driver Message'.tr,
body: body,
isTopic: false,
tone: tone,
driverList: [],
category: 'message From Driver',
);
}
} }

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../constant/colors.dart';
import '../../../../constant/style.dart';
import '../../../../controller/home/captin/map_driver_controller.dart';
// ويدجت للعرض فقط (بدون منطق فتح نوافذ)
class SpeedCircle extends StatelessWidget {
const SpeedCircle({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MapDriverController>(
id: 'SpeedCircle', // نحدد ID للتحديث الخفيف
builder: (controller) {
// إذا السرعة 0 أو أقل، نخفي الدائرة
if (controller.speed <= 0) return const SizedBox();
bool isSpeeding = controller.speed > 100;
return Positioned(
left: 20,
top: 100, // مكانها المناسب
child: Container(
width: 70,
height: 70,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
border: Border.all(
color: _getSpeedColor(controller.speed),
width: 4,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
controller.speed.toStringAsFixed(0),
style: TextStyle(
fontFamily: AppStyle.title.fontFamily,
fontSize: 22,
fontWeight: FontWeight.w900,
height: 1.0,
color: Colors.black87,
),
),
Text(
"km/h",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
],
),
),
);
},
);
}
Color _getSpeedColor(double speed) {
if (speed < 60) return AppColor.greenColor;
if (speed < 100) return Colors.orange;
return Colors.red;
}
}

View File

@@ -0,0 +1,142 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:flutter/services.dart';
class MarkerGenerator {
// دالة لرسم ماركر يحتوي على نص (للوقت والمسافة)
static Future<BitmapDescriptor> createCustomMarkerBitmap({
required String title,
required String subtitle,
required Color color,
required IconData iconData,
}) async {
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
final Canvas canvas = Canvas(pictureRecorder);
// إعدادات القياسات
const double width = 220.0;
const double height = 110.0;
const double circleRadius = 25.0;
// 1. رسم المربع (Info Box)
final Paint paint = Paint()..color = color;
final RRect rRect = RRect.fromRectAndRadius(
const Rect.fromLTWH(0, 0, width, 60),
const Radius.circular(15),
);
// ظل خفيف
canvas.drawShadow(Path()..addRRect(rRect), Colors.black, 5.0, true);
canvas.drawRRect(rRect, paint);
// 2. رسم مثلث صغير أسفل المربع (Arrow Tail)
final Path path = Path();
path.moveTo(width / 2 - 10, 60);
path.lineTo(width / 2, 75);
path.lineTo(width / 2 + 10, 60);
path.close();
canvas.drawPath(path, paint);
// 3. رسم الدائرة (مكان الأيقونة)
canvas.drawCircle(const Offset(width / 2, 85), circleRadius, paint);
// 4. رسم الأيقونة داخل الدائرة
TextPainter iconPainter = TextPainter(textDirection: TextDirection.ltr);
iconPainter.text = TextSpan(
text: String.fromCharCode(iconData.codePoint),
style: TextStyle(
fontSize: 30.0,
fontFamily: iconData.fontFamily,
color: Colors.white,
),
);
iconPainter.layout();
iconPainter.paint(
canvas,
Offset((width - iconPainter.width) / 2, 85 - (iconPainter.height / 2)),
);
// 5. رسم النصوص (العنوان والوصف) داخل المربع
// العنوان (مثلاً: المدة)
TextPainter titlePainter = TextPainter(
textDirection: TextDirection.rtl,
textAlign: TextAlign.center,
);
titlePainter.text = TextSpan(
text: title,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Colors.white,
),
);
titlePainter.layout(minWidth: width);
titlePainter.paint(canvas, const Offset(0, 8));
// الوصف (مثلاً: المسافة)
TextPainter subTitlePainter = TextPainter(
textDirection: TextDirection.rtl,
textAlign: TextAlign.center,
);
subTitlePainter.text = TextSpan(
text: subtitle,
style: const TextStyle(
fontSize: 16.0,
color: Colors.white70,
),
);
subTitlePainter.layout(minWidth: width);
subTitlePainter.paint(canvas, const Offset(0, 32));
// تحويل الرسم إلى صورة
final ui.Image image = await pictureRecorder.endRecording().toImage(
width.toInt(),
(height + 20).toInt(), // مساحة إضافية
);
final ByteData? data =
await image.toByteData(format: ui.ImageByteFormat.png);
return BitmapDescriptor.fromBytes(data!.buffer.asUint8List());
}
// دالة خاصة لرسم ماركر السائق (دائرة وخلفها سهم)
static Future<BitmapDescriptor> createDriverMarker() async {
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
final Canvas canvas = Canvas(pictureRecorder);
const double size = 100.0;
final Paint paint = Paint()..color = const Color(0xFF2E7D32); // أخضر غامق
final Paint borderPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 4.0;
// الدائرة
canvas.drawCircle(const Offset(size / 2, size / 2), size / 2.5, paint);
canvas.drawCircle(
const Offset(size / 2, size / 2), size / 2.5, borderPaint);
// رسم السهم (Arrow Up)
TextPainter iconPainter = TextPainter(textDirection: TextDirection.ltr);
iconPainter.text = TextSpan(
text: String.fromCharCode(Icons.navigation.codePoint), // سهم ملاحة
style: TextStyle(
fontSize: 40.0,
fontFamily: Icons.navigation.fontFamily,
color: Colors.white,
),
);
iconPainter.layout();
iconPainter.paint(
canvas,
Offset((size - iconPainter.width) / 2, (size - iconPainter.height) / 2),
);
final ui.Image image = await pictureRecorder
.endRecording()
.toImage(size.toInt(), size.toInt());
final ByteData? data =
await image.toByteData(format: ui.ImageByteFormat.png);
return BitmapDescriptor.fromBytes(data!.buffer.asUint8List());
}
}

View File

@@ -1,10 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart'; import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:sefer_driver/constant/api_key.dart'; import 'package:sefer_driver/constant/api_key.dart';
import 'package:sefer_driver/models/overlay_service.dart';
import '../../../../constant/box_name.dart'; import '../../../../constant/box_name.dart';
import '../../../../constant/links.dart'; import '../../../../constant/links.dart';
import '../../../../controller/firebase/firbase_messge.dart'; import '../../../../controller/firebase/firbase_messge.dart';
@@ -294,6 +296,14 @@ class _OrderOverlayState extends State<OrderOverlay>
'ding', 'ding',
'', '',
); );
// 3. الخطوة الأهم: فتح التطبيق وإغلاق النافذة
try {
// استدعاء الميثود التي تم تحديثها في الخطوة 1
await OverlayMethodChannel.bringToForeground();
} catch (e) {
_log("Failed to bring app to foreground: $e");
}
await _closeOverlay(); await _closeOverlay();
} else { } else {
_log("Failed to update order status on server: $res"); _log("Failed to update order status on server: $res");
@@ -309,6 +319,12 @@ class _OrderOverlayState extends State<OrderOverlay>
_log( _log(
"A critical error occurred during server update: $e\nStackTrace: $s"); "A critical error occurred during server update: $e\nStackTrace: $s");
if (mounted) setState(() => buttonsEnabled = true); if (mounted) setState(() => buttonsEnabled = true);
_log("Error in accept order: $e");
await _closeOverlay();
// حتى في حال الخطأ، نحاول فتح التطبيق ليرى السائق ما حدث
await OverlayMethodChannel.bringToForeground();
return; return;
} }
} }
@@ -342,7 +358,7 @@ class _OrderOverlayState extends State<OrderOverlay>
_log("Driver ID is null, cannot refuse order"); _log("Driver ID is null, cannot refuse order");
return; return;
} }
_crud.post(link: AppLink.addDriverOrder, payload: { CRUD().post(link: AppLink.addDriverOrder, payload: {
'driver_id': driverId, 'driver_id': driverId,
'order_id': orderID, 'order_id': orderID,
'status': 'Refused' 'status': 'Refused'
@@ -492,6 +508,11 @@ class _OrderOverlayState extends State<OrderOverlay>
Widget _buildPrimaryInfo() { Widget _buildPrimaryInfo() {
final order = orderData!; final order = orderData!;
// FIX: Parse the price to a number safely before formatting
// This handles cases where order.price is a String like "173"
final num priceValue = num.tryParse(order.price.toString()) ?? 0;
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -508,10 +529,8 @@ class _OrderOverlayState extends State<OrderOverlay>
Expanded( Expanded(
flex: 3, flex: 3,
child: _buildHighlightInfo( child: _buildHighlightInfo(
// التعديل هنا 👇 // FIX: Use the parsed priceValue here
"${NumberFormat('#,##0').format(order.price)} ل.س", "${NumberFormat('#,##0').format(priceValue)} ل.س",
// أو يمكنك استخدام "SYP" بدلاً من "ل.س"
"السعر".tr, "السعر".tr,
Icons.monetization_on_rounded, Icons.monetization_on_rounded,
AppColors.priceHighlight, AppColors.priceHighlight,
@@ -522,7 +541,8 @@ class _OrderOverlayState extends State<OrderOverlay>
Expanded( Expanded(
flex: 2, flex: 2,
child: _buildHighlightInfo( child: _buildHighlightInfo(
"${order.tripDistanceKm.toStringAsFixed(1)} كم", // Ensure tripDistanceKm is treated safely too
"${(num.tryParse(order.tripDistanceKm.toString()) ?? 0).toStringAsFixed(1)} كم",
"المسافة".tr, "المسافة".tr,
Icons.straighten_rounded, Icons.straighten_rounded,
AppColors.accent, AppColors.accent,

View File

@@ -1,450 +1,411 @@
import 'dart:convert';
import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/controller/firebase/firbase_messge.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'dart:math' as math; import 'package:sefer_driver/constant/colors.dart';
import '../../../../constant/colors.dart'; import 'package:sefer_driver/controller/home/captin/order_request_controller.dart';
import '../../../../constant/links.dart';
import '../../../../constant/style.dart';
import '../../../../controller/firebase/notification_service.dart';
import '../../../../controller/functions/crud.dart';
import '../../../../controller/functions/encrypt_decrypt.dart';
import '../../../../controller/functions/launch.dart';
import '../../../../controller/home/captin/order_request_controller.dart';
import '../../../../print.dart';
import '../../../widgets/elevated_btn.dart';
class OrderRequestPage extends StatefulWidget { class OrderRequestPage extends StatelessWidget {
const OrderRequestPage({super.key}); const OrderRequestPage({super.key});
@override
State<OrderRequestPage> createState() => _OrderRequestPageState();
}
class _OrderRequestPageState extends State<OrderRequestPage> {
final OrderRequestController orderRequestController =
Get.put(OrderRequestController());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// حقن الكنترولر
final OrderRequestController controller = Get.put(OrderRequestController());
return Scaffold( return Scaffold(
appBar: AppBar( body: Directionality(
title: Text('Order Request'.tr), textDirection: TextDirection.rtl,
centerTitle: true, child: GetBuilder<OrderRequestController>(
), builder: (controller) {
body: GetBuilder<OrderRequestController>( // 🔥 التعديل الأهم: التحقق من وجود أي بيانات (List أو Map)
builder: (controller) { if (controller.myList == null && controller.myMapData == null) {
if (controller.myList == null) { return const Center(
return const Center(child: CircularProgressIndicator()); child:
} CircularProgressIndicator()); // شاشة تحميل بدلاً من فراغ
return Column( }
children: [
SizedBox( // 🔥 استخدام دوال الكنترولر الآمنة لجلب البيانات بدلاً من الوصول المباشر
height: Get.height * 0.3, // قمت بتحويل _safeGet إلى دالة عامة safeGet في الكنترولر (تأكد من جعلها public)
child: GoogleMap( // أو سأقوم بكتابة المنطق هنا مباشرة لضمان العمل:
mapType: MapType.normal,
initialCameraPosition: CameraPosition( String getValue(int index) {
target: LatLng(controller.latPassengerLocation, if (controller.myList != null &&
controller.lngPassengerLocation), index < controller.myList!.length) {
zoom: 14.0, return controller.myList![index].toString();
}
if (controller.myMapData != null &&
controller.myMapData!.containsKey(index.toString())) {
return controller.myMapData![index.toString()].toString();
}
return "";
}
final String passengerName =
getValue(8).isEmpty ? "عميل" : getValue(8);
final String startAddr =
getValue(29).isEmpty ? "موقع الانطلاق" : getValue(29);
final String endAddr =
getValue(30).isEmpty ? "الوجهة" : getValue(30);
final bool isVisa = (getValue(13) == 'true');
// منطق Speed = سعر ثابت
final bool isSpeed =
controller.tripType.toLowerCase().contains('speed');
final String carTypeLabel =
isSpeed ? "سعر ثابت" : controller.tripType;
final Color carTypeColor =
isSpeed ? Colors.red.shade700 : Colors.blue.shade700;
final IconData carIcon =
isSpeed ? Icons.local_offer : Icons.directions_car;
return Stack(
children: [
// 1. الخارطة
Positioned.fill(
bottom: 300,
child: GoogleMap(
mapType: MapType.normal,
initialCameraPosition: CameraPosition(
target: LatLng(
controller.latPassenger, controller.lngPassenger),
zoom: 13.0,
),
markers: controller.markers,
polylines: controller.polylines,
zoomControlsEnabled: false,
myLocationButtonEnabled: false,
compassEnabled: false,
padding: const EdgeInsets.only(
top: 80, bottom: 20, left: 20, right: 20),
onMapCreated: (c) {
controller.onMapCreated(c);
controller.update();
},
), ),
myLocationButtonEnabled: true,
onMapCreated: controller.onMapCreated,
myLocationEnabled: true,
markers: {
Marker(
markerId: const MarkerId('startLocation'),
position: LatLng(controller.latPassengerLocation,
controller.lngPassengerLocation),
icon: controller.startIcon,
),
Marker(
markerId: const MarkerId('destinationLocation'),
position: LatLng(controller.latPassengerDestination,
controller.lngPassengerDestination),
icon: controller.endIcon,
),
},
polylines: {
Polyline(
polylineId: const PolylineId('route'),
color: AppColor.primaryColor,
width: 5,
points: controller.pointsDirection,
),
},
), ),
),
Expanded( // 2. كبسولة الوصول للراكب
child: ListView( Positioned(
padding: const EdgeInsets.all(16), top: 50,
children: [ left: 0,
Card( right: 0,
elevation: 4, child: Center(
child: ListTile( child: Container(
leading: Icon( padding: const EdgeInsets.symmetric(
controller.myList[13].toString() == 'true' horizontal: 15, vertical: 8),
? Icons.credit_card decoration: BoxDecoration(
: Icons.money, color: Colors.black87,
color: controller.myList[13].toString() == 'true' borderRadius: BorderRadius.circular(30),
? AppColor.deepPurpleAccent boxShadow: [
: AppColor.greenColor, BoxShadow(color: Colors.black26, blurRadius: 8)
), ],
title: Text( ),
'Payment Method'.tr, child: Row(
style: Theme.of(context).textTheme.titleMedium, mainAxisSize: MainAxisSize.min,
), children: [
trailing: Text( const Icon(Icons.near_me,
controller.myList[13].toString() == 'true' color: Colors.amber, size: 16),
? 'Visa' const SizedBox(width: 8),
: 'Cash', Text(
style: "الوصول للراكب: ${controller.timeToPassenger}",
Theme.of(context).textTheme.titleMedium?.copyWith( style: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.white,
), fontWeight: FontWeight.bold,
), fontSize: 13),
),
],
), ),
), ),
const SizedBox(height: 10), ),
Card( ),
elevation: 4,
child: ListTile( // 3. البطاقة السفلية
leading: const Icon(Icons.account_circle, Align(
color: AppColor.secondaryColor), alignment: Alignment.bottomCenter,
title: Text( child: Container(
controller.myList[8], height: 360,
style: Theme.of(context).textTheme.titleMedium, decoration: const BoxDecoration(
), color: Colors.white,
subtitle: Row( borderRadius: BorderRadius.only(
children: [ topLeft: Radius.circular(25),
const Icon(Icons.star, topRight: Radius.circular(25),
size: 16, color: Colors.amber),
Text(
controller.myList[33].toString(),
style: const TextStyle(color: Colors.amber),
),
],
),
), ),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 20,
spreadRadius: 5)
],
), ),
const SizedBox(height: 10), padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
Card( child: Column(
elevation: 4, crossAxisAlignment: CrossAxisAlignment.start,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.location_on,
color: AppColor.greenColor),
const SizedBox(width: 8),
Expanded(
// Keep Expanded here for layout
child: Text(
controller.myList[29],
style:
Theme.of(context).textTheme.titleSmall,
maxLines: 2, // Allow up to 2 lines
overflow: TextOverflow
.ellipsis, // Handle overflow
),
),
],
),
const Divider(),
Row(
children: [
const Icon(Icons.flag,
color: AppColor.redColor),
const SizedBox(width: 8),
Expanded(
// Keep Expanded here for layout
child: Text(
controller.myList[30],
style:
Theme.of(context).textTheme.titleSmall,
maxLines: 2, // Allow up to 2 lines
overflow: TextOverflow
.ellipsis, // Handle overflow
),
),
],
),
],
),
),
),
const SizedBox(height: 10),
// Card(
// elevation: 4,
// child: Padding(
// padding: const EdgeInsets.all(16.0),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceAround,
// children: [
// _InfoTile(
// icon: Icons.timer,
// label:
// '${(double.parse(controller.myList[12]) / 60).toStringAsFixed(0)} ${'min'.tr}',
// ),
// _InfoTile(
// icon: Icons.directions_car,
// label:
// '${(double.parse(controller.myList[11]) / 1000).toStringAsFixed(1)} ${'km'.tr}',
// ),
// _InfoTile(
// icon: Icons.monetization_on,
// label: '${controller.myList[2]}',
// ),
// ],
// ),
// ),
// ),
// استبدل هذا الكود
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_InfoTile(
icon: Icons.timer,
label:
// استخدم الفهرس 13 للوقت (Duration)
'${((double.tryParse(controller.myList[13].toString()) ?? 0.0) / 60).toStringAsFixed(0)} ${'min'.tr}',
),
_InfoTile(
icon: Icons.directions_car,
label:
// استخدم الفهرس 14 للمسافة (Distance)
// استخدم tryParse للأمان لأن القيمة "" (نص فارغ)
'${((double.tryParse(controller.myList[14].toString()) ?? 0.0) / 1000).toStringAsFixed(1)} ${'km'.tr}',
),
_InfoTile(
icon: Icons.monetization_on,
label:
// السعر أصبح في الفهرس 4
'${controller.myList[4]}',
),
],
),
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
MyElevatedButton( Center(
kolor: AppColor.greenColor, child: Container(
title: 'Accept Order'.tr, width: 40,
onPressed: () async { height: 4,
var res = await CRUD().post( decoration: BoxDecoration(
link: color: Colors.grey[300],
"${AppLink.ride}/rides/updateStausFromSpeed.php", borderRadius: BorderRadius.circular(2)))),
payload: { const SizedBox(height: 15),
'id': (controller.myList[16]),
'rideTimeStart': DateTime.now().toString(),
'status': 'Apply',
'driver_id': box.read(BoxName.driverID),
});
if (res == 'failure') { // الصف الأول: الراكب والسعر
MyDialog().getDialog( Row(
"This ride is already applied by another driver." mainAxisAlignment: MainAxisAlignment.spaceBetween,
.tr, children: [
'', () { Row(
Get.back();
Get.back();
});
} else {
Get.put(HomeCaptainController()).changeRideId();
box.write(BoxName.statusDriverLocation, 'on');
controller.endTimer();
controller.changeApplied();
CRUD().postFromDialogue(
link: AppLink.addDriverOrder,
payload: {
'driver_id':
(controller.myList[6].toString()),
'order_id':
(controller.myList[16].toString()),
'status': 'Apply'
});
List<String> bodyToPassenger = [
controller.myList[6].toString(),
controller.myList[8].toString(),
controller.myList[9].toString(),
];
NotificationService.sendNotification(
target: controller.myList[9].toString(),
title: "Accepted Ride".tr,
body: 'your ride is Accepted'.tr,
isTopic: false, // Important: this is a token
tone: 'start',
driverList: bodyToPassenger,
category: 'Accepted Ride',
);
Get.back();
box.write(BoxName.rideArguments, {
'passengerLocation':
controller.myList[0].toString(),
'passengerDestination':
controller.myList[1].toString(),
'Duration': controller.myList[4].toString(),
'totalCost': controller.myList[26].toString(),
'Distance': controller.myList[5].toString(),
'name': controller.myList[8].toString(),
'phone': controller.myList[10].toString(),
'email': controller.myList[28].toString(),
'WalletChecked':
controller.myList[13].toString(),
'tokenPassenger':
controller.myList[9].toString(),
'direction':
'https://www.google.com/maps/dir/${controller.myList[0]}/${controller.myList[1]}/',
'DurationToPassenger':
controller.myList[15].toString(),
'rideId': (controller.myList[16].toString()),
'passengerId':
(controller.myList[7].toString()),
'driverId': (controller.myList[18].toString()),
'durationOfRideValue':
controller.myList[19].toString(),
'paymentAmount':
controller.myList[2].toString(),
'paymentMethod':
controller.myList[13].toString() == 'true'
? 'visa'
: 'cash',
'isHaveSteps': controller.myList[20].toString(),
'step0': controller.myList[21].toString(),
'step1': controller.myList[22].toString(),
'step2': controller.myList[23].toString(),
'step3': controller.myList[24].toString(),
'step4': controller.myList[25].toString(),
'passengerWalletBurc':
controller.myList[26].toString(),
'timeOfOrder': DateTime.now().toString(),
'totalPassenger':
controller.myList[2].toString(),
'carType': controller.myList[31].toString(),
'kazan': controller.myList[32].toString(),
'startNameLocation':
controller.myList[29].toString(),
'endNameLocation':
controller.myList[30].toString(),
});
Get.to(() => PassengerLocationMapPage(),
arguments: box.read(BoxName.rideArguments));
Log.print(
'box.read(BoxName.rideArguments): ${box.read(BoxName.rideArguments)}');
}
},
),
GetBuilder<OrderRequestController>(
builder: (timerController) {
final isNearEnd = timerController.remainingTime <=
5; // Define a threshold for "near end"
return Stack(
alignment: Alignment.center,
children: [ children: [
CircularProgressIndicator( const CircleAvatar(
value: timerController.progress, radius: 24,
// Set the color based on the "isNearEnd" condition backgroundColor: Color(0xFFF5F5F5),
color: isNearEnd ? Colors.red : Colors.blue, child: Icon(Icons.person,
color: Colors.grey, size: 28),
), ),
Text( const SizedBox(width: 10),
'${timerController.remainingTime}', Column(
style: AppStyle.number, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(passengerName,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold)),
Row(
children: [
const Icon(Icons.star,
color: Colors.amber, size: 14),
Text(controller.passengerRating,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold)),
],
),
],
), ),
], ],
); ),
}, Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("${controller.tripPrice} ل.س",
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColor.primaryColor)),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: isVisa
? Colors.purple.withOpacity(0.1)
: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(4)),
child: Row(
children: [
Text(isVisa ? "فيزا" : "كاش",
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: isVisa
? Colors.purple
: Colors.green)),
const SizedBox(width: 4),
Icon(
isVisa
? Icons.credit_card
: Icons.money,
size: 14,
color: isVisa
? Colors.purple
: Colors.green),
],
),
),
],
),
],
), ),
MyElevatedButton(
title: 'Refuse Order'.tr,
onPressed: () async {
controller.endTimer();
// List<String> bodyToPassenger = [
// box.read(BoxName.driverID).toString(),
// box.read(BoxName.nameDriver).toString(),
// box.read(BoxName.tokenDriver).toString(),
// ];
// NotificationService.sendNotification( const SizedBox(height: 15),
// target: controller.myList[9].toString(),
// title: 'Order Under Review'.tr,
// body:
// '${box.read(BoxName.nameDriver)} ${'is reviewing your order. They may need more information or a higher price.'.tr}',
// isTopic: false, // Important: this is a token
// tone: 'start',
// driverList: bodyToPassenger,
// category: 'Order Under Review',
// );
// controller.refuseOrder( // الصف الثاني: شريط المعلومات
// (controller.myList[16].toString()), Container(
// ); padding: const EdgeInsets.symmetric(
controller.addRideToNotificationDriverString( vertical: 10, horizontal: 10),
controller.myList[16].toString(), decoration: BoxDecoration(
controller.myList[29].toString(), color: const Color(0xFFF8F9FA),
controller.myList[30].toString(), borderRadius: BorderRadius.circular(12),
'${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}', border: Border.all(color: Colors.grey.shade200),
'${DateTime.now().hour}:${DateTime.now().minute}', ),
controller.myList[2].toString(), child: Row(
controller.myList[7].toString(), mainAxisAlignment: MainAxisAlignment.spaceAround,
'wait', children: [
controller.myList[31].toString(), _buildInfoItem(
controller.myList[33].toString(), carIcon, carTypeLabel, carTypeColor),
controller.myList[2].toString(), Container(
controller.myList[5].toString(), height: 20,
controller.myList[4].toString()); width: 1,
}, color: Colors.grey.shade300),
kolor: AppColor.redColor, _buildInfoItem(Icons.route,
controller.totalTripDistance, Colors.black87),
Container(
height: 20,
width: 1,
color: Colors.grey.shade300),
_buildInfoItem(Icons.access_time_filled,
controller.totalTripDuration, Colors.black87),
],
),
),
const SizedBox(height: 20),
// الصف الثالث: العناوين
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
const Icon(Icons.my_location,
size: 18, color: Colors.green),
Expanded(
child: Container(
width: 2,
color: Colors.grey.shade300,
margin: const EdgeInsets.symmetric(
vertical: 2))),
const Icon(Icons.location_on,
size: 18, color: Colors.red),
],
),
const SizedBox(width: 15),
Expanded(
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text("موقع الانطلاق",
style: TextStyle(
fontSize: 11,
color: Colors.grey)),
Text(startAddr,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600),
maxLines: 1,
overflow: TextOverflow.ellipsis),
],
),
const Spacer(),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text("الوجهة",
style: TextStyle(
fontSize: 11,
color: Colors.grey)),
Text(endAddr,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600),
maxLines: 1,
overflow: TextOverflow.ellipsis),
],
),
],
),
)
],
),
),
const SizedBox(height: 15),
// الصف الرابع: الأزرار
Row(
children: [
InkWell(
onTap: () => Get.back(),
child: Container(
height: 50,
width: 50,
decoration: BoxDecoration(
color: Colors.red.shade50,
shape: BoxShape.circle,
border:
Border.all(color: Colors.red.shade100)),
child: const Icon(Icons.close,
color: Colors.red, size: 24),
),
),
const SizedBox(width: 15),
Expanded(
child: ElevatedButton(
onPressed: () => controller.acceptOrder(),
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)),
elevation: 2,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("قبول الرحلة",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold)),
const SizedBox(width: 15),
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
value: controller.progress,
color: Colors.white,
strokeWidth: 2.5,
backgroundColor: Colors.white24),
),
const SizedBox(width: 8),
Text("${controller.remainingTime}",
style: const TextStyle(
fontSize: 14, color: Colors.white)),
],
),
),
),
],
), ),
], ],
), ),
], ),
), ),
), ],
], );
); },
}, ),
), ),
); );
} }
}
class _InfoTile extends StatelessWidget { Widget _buildInfoItem(IconData icon, String text, Color color) {
final IconData icon; return Row(
final String label; mainAxisSize: MainAxisSize.min,
const _InfoTile({required this.icon, required this.label});
@override
Widget build(BuildContext context) {
return Column(
children: [ children: [
Icon(icon, color: AppColor.primaryColor), Icon(icon, size: 16, color: color),
const SizedBox(height: 4), const SizedBox(width: 6),
Text( Text(text,
label, style: TextStyle(
style: Theme.of(context).textTheme.bodyMedium, fontSize: 13, fontWeight: FontWeight.bold, color: color)),
),
], ],
); );
} }

File diff suppressed because it is too large Load Diff

View File

@@ -382,17 +382,13 @@ class WalletCaptainRefactored extends StatelessWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
children: [ children: [
PointsCaptain( PointsCaptain(
kolor: AppColor.greyColor, kolor: AppColor.greyColor, pricePoint: 100, countPoint: '100'),
pricePoint: 10000,
countPoint: '10000'),
PointsCaptain( PointsCaptain(
kolor: AppColor.bronze, pricePoint: 20000, countPoint: '21000'), kolor: AppColor.bronze, pricePoint: 200, countPoint: '210'),
PointsCaptain( PointsCaptain(
kolor: AppColor.goldenBronze, kolor: AppColor.goldenBronze, pricePoint: 400, countPoint: '450'),
pricePoint: 40000,
countPoint: '45000'),
PointsCaptain( PointsCaptain(
kolor: AppColor.gold, pricePoint: 100000, countPoint: '110000'), kolor: AppColor.gold, pricePoint: 1000, countPoint: '1100'),
], ],
), ),
); );

View File

@@ -1,10 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/controller/profile/captain_profile_controller.dart'; import 'package:sefer_driver/controller/profile/captain_profile_controller.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/auth/captin/criminal_documents_page.dart'; import 'package:sefer_driver/views/auth/captin/criminal_documents_page.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart'; import 'package:sefer_driver/views/widgets/my_scafold.dart';
import 'package:sefer_driver/views/widgets/mycircular.dart'; import 'package:sefer_driver/views/widgets/mycircular.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart'; import 'package:sefer_driver/views/widgets/mydialoug.dart';
import '../../../constant/links.dart';
import '../../../controller/functions/crud.dart';
import 'behavior_page.dart'; import 'behavior_page.dart';
import 'captains_cars.dart'; import 'captains_cars.dart';
@@ -150,11 +155,13 @@ class ActionsGrid extends StatelessWidget {
// onTap: () => Get.to(() => CriminalDocumemtPage()), // onTap: () => Get.to(() => CriminalDocumemtPage()),
// ), // ),
_ActionTile( _ActionTile(
title: 'Bank Account'.tr, title: 'ShamCash Account'.tr, // غيرت الاسم ليكون أوضح
icon: Icons.account_balance, icon: Icons.account_balance_wallet_rounded, // أيقونة محفظة
// trailing: Icon(Icons.arrow_forward_ios,
// size: 16, color: Colors.grey), // سهم صغير للجمالية
onTap: () { onTap: () {
MyDialog().getDialog('Coming Soon'.tr, // استدعاء دالة فتح النافذة
'This service will be available soon.'.tr, () => Get.back()); showShamCashInput();
}, },
), ),
_ActionTile( _ActionTile(
@@ -167,6 +174,223 @@ class ActionsGrid extends StatelessWidget {
} }
} }
void showShamCashInput() {
// 1. القراءة من الذاكرة المحلية (GetStorage) عند فتح النافذة
// إذا لم يتم العثور على قيمة، يتم تعيينها إلى نص فارغ
final String existingName = box.read('shamcash_name') ?? '';
final String existingCode = box.read('shamcash_code') ?? '';
// تعريف أدوات التحكم للحقلين مع تحميل القيمة المحفوظة
final TextEditingController nameController =
TextEditingController(text: existingName);
final TextEditingController codeController =
TextEditingController(text: existingCode);
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(25),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
boxShadow: [
BoxShadow(
color: Colors.black26, blurRadius: 10, offset: Offset(0, -2))
],
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// --- 1. المقبض العلوي ---
Center(
child: Container(
height: 5,
width: 50,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(10)),
margin: const EdgeInsets.only(bottom: 20),
),
),
// --- 2. العنوان والأيقونة ---
Image.asset(
'assets/images/shamCash.png',
height: 50,
),
// const Icon(Icons.account_balance_wallet_rounded,
// size: 45, color: Colors.blueAccent),
const SizedBox(height: 10),
Text(
"ربط حساب شام كاش 🔗",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.blueGrey[900]),
),
const SizedBox(height: 5),
const Text(
"أدخل بيانات حسابك لاستقبال الأرباح فوراً",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13, color: Colors.grey),
),
const SizedBox(height: 25),
// --- 3. الحقل الأول: اسم الحساب (أعلى الباركود) ---
const Text("1. اسم الحساب (أعلى الباركود)",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!),
),
child: TextField(
controller: nameController,
decoration: InputDecoration(
hintText: "مثال: intaleq",
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 13),
border: InputBorder.none,
prefixIcon: const Icon(Icons.person_outline_rounded,
color: Colors.blueGrey),
contentPadding:
const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
),
),
),
const SizedBox(height: 15),
// --- 4. الحقل الثاني: الكود (أسفل الباركود) ---
const Text("2. كود المحفظة (أسفل الباركود)",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!),
),
child: TextField(
controller: codeController,
style: const TextStyle(
fontSize: 13,
letterSpacing: 0.5), // خط أصغر قليلاً للكود الطويل
decoration: InputDecoration(
hintText: "مثال: 80f23afe40...",
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 13),
border: InputBorder.none,
prefixIcon: const Icon(Icons.qr_code_2_rounded,
color: Colors.blueGrey),
contentPadding:
const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
// زر لصق الكود
suffixIcon: IconButton(
icon: const Icon(Icons.paste_rounded, color: Colors.blue),
tooltip: "لصق الكود",
onPressed: () async {
ClipboardData? data =
await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
codeController.text = data.text!;
// تحريك المؤشر للنهاية بعد اللصق
codeController.selection = TextSelection.fromPosition(
TextPosition(offset: codeController.text.length),
);
}
},
),
),
),
),
const SizedBox(height: 30),
// --- 5. زر الحفظ ---
SizedBox(
height: 50,
child: ElevatedButton(
onPressed: () async {
String name = nameController.text.trim();
String code = codeController.text.trim();
// التحقق من صحة البيانات
if (name.isNotEmpty && code.length > 5) {
// 1. إرسال البيانات إلى السيرفر
var res = await CRUD()
.post(link: AppLink.updateShamCashDriver, payload: {
"id": box.read(BoxName.driverID),
"accountBank": name,
"bankCode": code,
});
if (res != 'failure') {
// 2. 🔴 الحفظ في الذاكرة المحلية (GetStorage) بعد نجاح التحديث
box.write('shamcash_name', name);
box.write('shamcash_code', code);
Get.back(); // إغلاق النافذة
Get.snackbar(
"تم الحفظ بنجاح",
"تم ربط حساب ($name) لاستلام الأرباح.",
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(20),
icon: const Icon(Icons.check_circle_outline,
color: Colors.white),
);
return;
} else {
// في حال فشل الإرسال إلى السيرفر
Get.snackbar(
"خطأ في السيرفر",
"فشل تحديث البيانات، يرجى المحاولة لاحقاً.",
backgroundColor: Colors.redAccent,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(20),
);
}
} else {
Get.snackbar(
"بيانات ناقصة",
"يرجى التأكد من إدخال الاسم والكود بشكل صحيح.",
backgroundColor: Colors.orange,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(20),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2ecc71), // الأخضر المالي
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
elevation: 2,
),
child: const Text(
"حفظ وتفعيل الحساب",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white),
),
),
),
const SizedBox(height: 10), // مسافة سفلية إضافية للأمان
],
),
),
),
isScrollControlled: true, // ضروري لرفع النافذة عند فتح الكيبورد
);
}
/// ويدجت داخلية لزر في الشبكة /// ويدجت داخلية لزر في الشبكة
class _ActionTile extends StatelessWidget { class _ActionTile extends StatelessWidget {
final String title; final String title;

View File

@@ -1,97 +1,108 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:math'; import 'package:google_maps_flutter/google_maps_flutter.dart'; // لتحديد الأنواع إذا لزم
import '../../constant/box_name.dart'; import '../../constant/box_name.dart';
import '../../constant/colors.dart'; import '../../constant/colors.dart';
import '../../constant/links.dart'; import '../../constant/links.dart';
import '../../constant/style.dart'; import '../../constant/style.dart';
import '../../controller/firebase/firbase_messge.dart';
import '../../controller/firebase/notification_service.dart';
import '../../controller/functions/crud.dart'; import '../../controller/functions/crud.dart';
import '../../controller/home/captin/home_captain_controller.dart'; import '../../controller/home/captin/home_captain_controller.dart';
import '../../controller/notification/ride_available_controller.dart'; import '../../controller/notification/ride_available_controller.dart';
import '../../main.dart'; import '../../main.dart'; // للوصول للـ box
import '../home/Captin/driver_map_page.dart'; import '../home/Captin/driver_map_page.dart';
import '../widgets/my_scafold.dart'; import '../widgets/my_scafold.dart';
import '../widgets/mycircular.dart'; import '../widgets/mycircular.dart';
import '../widgets/mydialoug.dart'; import '../widgets/mydialoug.dart';
// --- Placeholder Classes and Variables (for demonstration) ---
// These are dummy implementations to make the code runnable.
// You should use your actual project files.
// --- End of Placeholder Classes ---
class AvailableRidesPage extends StatelessWidget { class AvailableRidesPage extends StatelessWidget {
const AvailableRidesPage({super.key}); const AvailableRidesPage({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Use findOrPut to avoid re-creating the controller on rebuilds // حقن الكنترولر (تأكد أنك تستخدم الكود الجديد الذي أعطيتك إياه للكنترولر)
Get.lazyPut(() => RideAvailableController()); Get.lazyPut(() => RideAvailableController());
Get.lazyPut(() => HomeCaptainController()); Get.lazyPut(() => HomeCaptainController());
return GetBuilder<RideAvailableController>( return GetBuilder<RideAvailableController>(
builder: (rideAvailableController) { builder: (controller) {
// rideAvailableController.sortRidesByDistance(); // Original logic return MyScafolld(
return MyScafolld(
title: 'Available for rides'.tr, title: 'Available for rides'.tr,
isleading: true,
body: [ body: [
rideAvailableController.isLoading controller.isLoading
? const MyCircularProgressIndicator() ? const Center(
child: Padding(
padding: EdgeInsets.only(top: 50.0),
child: MyCircularProgressIndicator(),
))
: Builder( : Builder(
builder: (context) { builder: (context) {
// Filtering logic remains the same // 1. الفلترة حسب نوع السيارة (تم نقل المنطق للكنترولر، لكن هنا للعرض فقط)
final filteredRides = rideAvailableController // الكنترولر الجديد يفلتر عند الإضافة، لكن لا ضرر من التأكيد هنا
.rideAvailableMap['message'] final ridesList = controller.availableRides;
.where((rideInfo) {
var driverType =
box.read(BoxName.carTypeOfDriver).toString();
switch (driverType) {
case 'Comfort':
return ['Speed', 'Comfort']
.contains(rideInfo['carType']);
case 'Speed':
case 'Scooter':
case 'Awfar Car':
return rideInfo['carType'] == driverType;
case 'Lady':
return ['Comfort', 'Speed', 'Lady']
.contains(rideInfo['carType']);
default:
return false;
}
}).toList();
if (filteredRides.isEmpty) { if (ridesList.isEmpty) {
return Center( return Center(
child: Text( child: Column(
"No rides available for your vehicle type.".tr, mainAxisAlignment: MainAxisAlignment.center,
style: AppStyle.subtitle, children: [
const SizedBox(height: 100),
Icon(CupertinoIcons.car_detailed,
size: 60,
color:
AppColor.primaryColor.withOpacity(0.5)),
const SizedBox(height: 20),
Text(
"No rides available right now.".tr,
style: AppStyle.subtitle,
),
const SizedBox(height: 20),
ElevatedButton.icon(
onPressed: () => controller.getRideAvailable(
forceRefresh: true),
icon: const Icon(Icons.refresh),
label: Text("Refresh Market".tr),
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
foregroundColor: Colors.white,
),
)
],
), ),
); );
} }
return ListView.builder( // 2. عرض القائمة
padding: const EdgeInsets.symmetric( return RefreshIndicator(
horizontal: 12, vertical: 16), onRefresh: () async {
itemCount: filteredRides.length, await controller.getRideAvailable(forceRefresh: true);
itemBuilder: (context, index) {
return RideAvailableCard(
rideInfo: filteredRides[index],
);
}, },
child: ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 16),
itemCount: ridesList.length,
itemBuilder: (context, index) {
return RideAvailableCard(
rideInfo: ridesList[index],
);
},
),
); );
}, },
) )
], ],
isleading: true); );
}); },
);
} }
} }
// =============================================================================
// بطاقة الرحلة (The Card)
// =============================================================================
class RideAvailableCard extends StatelessWidget { class RideAvailableCard extends StatelessWidget {
final Map<String, dynamic> rideInfo; final Map<String, dynamic> rideInfo;
@@ -99,50 +110,45 @@ class RideAvailableCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// The main card with improved styling
return Card( return Card(
margin: const EdgeInsets.only(bottom: 16.0), margin: const EdgeInsets.only(bottom: 16.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 5, elevation: 4,
shadowColor: Colors.black.withOpacity(0.1), shadowColor: Colors.black.withOpacity(0.1),
child: InkWell( child: Padding(
borderRadius: BorderRadius.circular(16), padding: const EdgeInsets.all(16.0),
onTap: () { child: Column(
// You can add an action here, e.g., show ride details on a map crossAxisAlignment: CrossAxisAlignment.start,
}, children: [
child: Padding( _buildHeader(),
padding: const EdgeInsets.all(16.0), const SizedBox(height: 16),
child: Column( _buildRouteInfo(),
crossAxisAlignment: CrossAxisAlignment.start, const Divider(height: 24, thickness: 0.5),
children: [ _buildRideDetails(),
_buildHeader(), const SizedBox(height: 20),
const SizedBox(height: 16), _buildAcceptButton(),
_buildRouteInfo(), ],
const Divider(height: 32),
_buildRideDetails(),
const SizedBox(height: 20),
_buildAcceptButton(),
],
),
), ),
), ),
); );
} }
// Header section with Price and Car Type // ---------------------------------------------------------------------------
// تصميم البطاقة (Header, Route, Details)
// ---------------------------------------------------------------------------
Widget _buildHeader() { Widget _buildHeader() {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Fare'.tr, style: AppStyle.subtitle.copyWith(fontSize: 12)), Text('Price'.tr, style: AppStyle.subtitle.copyWith(fontSize: 12)),
const SizedBox(height: 4), Text(
Text('${rideInfo['price']} \$', '${rideInfo['price']} ${'SYP'.tr}', // العملة
style: AppStyle.title style: AppStyle.title.copyWith(
.copyWith(fontSize: 24, color: AppColor.primaryColor)), fontSize: 20, color: AppColor.primaryColor, height: 1.2),
),
], ],
), ),
Container( Container(
@@ -150,48 +156,49 @@ class RideAvailableCard extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColor.greenColor.withOpacity(0.1), color: AppColor.greenColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColor.greenColor.withOpacity(0.3)),
), ),
child: Text( child: Text(
rideInfo['carType'], rideInfo['carType'] ?? 'Fixed Price'.tr,
style: AppStyle.title style: AppStyle.title
.copyWith(color: AppColor.greenColor, fontSize: 12), .copyWith(color: AppColor.greenColor, fontSize: 13),
), ),
), ),
], ],
); );
} }
// Visual representation of the pickup and dropoff route
Widget _buildRouteInfo() { Widget _buildRouteInfo() {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Dotted line and icons column
Column( Column(
children: [ children: [
const Icon(CupertinoIcons.circle_fill, const Icon(Icons.my_location,
color: AppColor.greenColor, size: 20), color: AppColor.primaryColor, size: 18),
...List.generate( Container(
4, height: 30,
(index) => Container( width: 1,
height: 4, color: Colors.grey.shade300,
width: 2, margin: const EdgeInsets.symmetric(vertical: 4),
color: AppColor.writeColor, ),
margin: const EdgeInsets.symmetric(vertical: 2), const Icon(Icons.location_on, color: Colors.red, size: 18),
)),
const Icon(CupertinoIcons.location_solid,
color: Colors.red, size: 20),
], ],
), ),
const SizedBox(width: 16), const SizedBox(width: 12),
// Location text column
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildLocationText(rideInfo['startName'], 'Pickup'.tr), Text(rideInfo['startName'] ?? 'Unknown Location'.tr,
const SizedBox(height: 20), maxLines: 1,
_buildLocationText(rideInfo['endName'], 'Dropoff'.tr), overflow: TextOverflow.ellipsis,
style: AppStyle.title.copyWith(fontSize: 14)),
const SizedBox(height: 22),
Text(rideInfo['endName'] ?? 'Destination'.tr,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppStyle.title.copyWith(fontSize: 14)),
], ],
), ),
) )
@@ -199,172 +206,164 @@ class RideAvailableCard extends StatelessWidget {
); );
} }
// Helper for location text
Widget _buildLocationText(String location, String label) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(label, style: AppStyle.subtitle.copyWith(fontSize: 12)),
const SizedBox(height: 2),
Text(
location,
style: AppStyle.title.copyWith(fontWeight: FontWeight.normal),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
);
}
// Ride details section with Distance and Passenger Rating
Widget _buildRideDetails() { Widget _buildRideDetails() {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
_buildInfoChip( _infoItem(Icons.social_distance, '${rideInfo['distance']} KM'),
icon: CupertinoIcons.map_pin_ellipse, _infoItem(Icons.access_time, '${rideInfo['duration']} Min'),
value: '${rideInfo['distance']} ${'KM'.tr}', _infoItem(Icons.star, '${rideInfo['passengerRate'] ?? 5.0}',
label: 'Distance'.tr, iconColor: Colors.amber),
color: AppColor.primaryColor,
),
_buildInfoChip(
icon: CupertinoIcons.star_fill,
value: '${rideInfo['passengerRate']}',
label: 'Rating'.tr,
color: Colors.amber,
),
], ],
); );
} }
// A reusable chip for displaying info with an icon Widget _infoItem(IconData icon, String text,
Widget _buildInfoChip( {Color iconColor = Colors.grey}) {
{required IconData icon, return Row(
required String value,
required String label,
required Color color}) {
return Column(
children: [ children: [
Row( Icon(icon, size: 16, color: iconColor),
children: [ const SizedBox(width: 4),
Icon(icon, color: color, size: 16), Text(text, style: AppStyle.subtitle.copyWith(fontSize: 13)),
const SizedBox(width: 8),
Text(value, style: AppStyle.title),
],
),
const SizedBox(height: 4),
Text(label, style: AppStyle.subtitle.copyWith(fontSize: 12)),
], ],
); );
} }
// The accept button with improved styling // ---------------------------------------------------------------------------
// زر القبول والمنطق الكامل (Accept Logic) 🔥
// ---------------------------------------------------------------------------
Widget _buildAcceptButton() { Widget _buildAcceptButton() {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton.icon( height: 50,
icon: const Icon(Icons.check_circle_outline, color: Colors.white), child: ElevatedButton(
label: Text('Accept'.tr, onPressed: _acceptRideNewLogic,
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)),
onPressed: _acceptRide,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColor.greenColor, backgroundColor: AppColor.greenColor,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 2, elevation: 2,
), ),
child: Text(
'Accept Ride'.tr,
style: const TextStyle(
color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
),
), ),
); );
} }
// --- Ride Acceptance Logic --- // 🔥🔥🔥 الوظيفة الأهم: قبول الرحلة وتجهيز البيانات 🔥🔥🔥
// This logic is copied exactly from your original code. void _acceptRideNewLogic() async {
void _acceptRide() async { // 1. إظهار Loading
var res = await CRUD().post( Get.dialog(
link: '${AppLink.endPoint}rides/updateStausFromSpeed.php', const Center(child: MyCircularProgressIndicator()),
barrierDismissible: false,
);
try {
String driverId = box.read(BoxName.driverID).toString();
// 2. إرسال الطلب للسيرفر (acceptRide.php الجديد)
var response = await CRUD().post(
link: "${AppLink.ride}/rides/acceptRide.php",
payload: { payload: {
'id': rideInfo['id'], 'id': rideInfo['id'].toString(),
'rideTimeStart': DateTime.now().toString(), 'driver_id': driverId,
'status': 'Apply', 'status': 'Apply', // الحالة المتفق عليها
'driver_id': box.read(BoxName.driverID), 'passengerToken': rideInfo['passengerToken'].toString(),
}); },
if (res != "failure") {
List<String> bodyToPassenger = [
box.read(BoxName.driverID).toString(),
box.read(BoxName.nameDriver).toString(),
box.read(BoxName.tokenDriver).toString(),
];
box.write(BoxName.statusDriverLocation, 'on');
await CRUD().postFromDialogue(link: AppLink.addDriverOrder, payload: {
'driver_id': box.read(BoxName.driverID),
'order_id': rideInfo['id'],
'status': 'Apply'
});
// await CRUD().post(link: AppLink.updateRides, payload: {
// 'id': rideInfo['id'],
// 'DriverIsGoingToPassenger': DateTime.now().toString(),
// 'status': 'Applied'
// });
await CRUD().post(
link: AppLink.updateWaitingRide,
payload: {'id': rideInfo['id'], 'status': 'Applied'});
// if (AppLink.endPoint.toString() != AppLink.seferCairoServer) {
NotificationService.sendNotification(
target: rideInfo['passengerToken'].toString(),
title: 'Accepted Ride'.tr,
body: 'your ride is Accepted'.tr,
isTopic: false, // Important: this is a token
tone: 'start',
driverList: bodyToPassenger, category: 'Accepted Ride',
); );
Get.back();
Get.to(() => PassengerLocationMapPage(), arguments: {
'passengerLocation': rideInfo['start_location'].toString(),
'passengerDestination': rideInfo['end_location'].toString(),
'Duration': rideInfo['duration'].toString(),
'totalCost': rideInfo['price'].toString(),
'Distance': rideInfo['distance'].toString(),
'name': rideInfo['first_name'].toString(),
'phone': rideInfo['phone'].toString(),
'email': rideInfo['email'].toString(),
'WalletChecked': rideInfo['payment_method'].toString(),
'tokenPassenger': rideInfo['passengerToken'].toString(),
'direction':
'https://www.google.com/maps/dir/${rideInfo['start_location']}/${rideInfo['end_location']}/',
'DurationToPassenger': rideInfo['duration'].toString(),
'rideId': rideInfo['id'].toString(),
'passengerId': rideInfo['passenger_id'].toString(),
'driverId': box.read(BoxName.driverID).toString(),
'durationOfRideValue': rideInfo['duration'].toString(),
'paymentAmount': rideInfo['price'].toString(),
'paymentMethod': 'cash'.toString() == 'true' ? 'visa' : 'cash',
'isHaveSteps': 'startEnd'.toString(),
'step0': ''.toString(),
'step1': ''.toString(),
'step2': ''.toString(),
'step3': ''.toString(),
'step4': ''.toString(),
'passengerWalletBurc': rideInfo['bruc'].toString(),
'timeOfOrder': DateTime.now().toString(),
'totalPassenger': rideInfo['price'].toString(),
'carType': rideInfo['carType'].toString(),
'kazan': Get.find<HomeCaptainController>().kazan.toString(),
'startNameLocation': rideInfo['startName'].toString(),
'endNameLocation': rideInfo['endName'].toString(),
});
} else {
MyDialog().getDialog(
"This ride is already taken by another driver.".tr, '', () {
CRUD().post(
link: AppLink.deleteAvailableRide, payload: {'id': rideInfo['id']});
Get.back(); // إخفاء الـ Loading
}); Get.back();
// 3. تحليل الرد
var jsonResponse = jsonDecode(response);
if (jsonResponse['status'] == 'success') {
// ✅ نجاح: أنت الفائز بالرحلة
// تحديث حالة السائق محلياً
Get.find<HomeCaptainController>().changeRideId();
box.write(BoxName.statusDriverLocation, 'on');
// 🔥 تجهيز الـ Arguments كاملة (Mapping) 📦
// نأخذ البيانات من rideInfo (القادمة من getRideWaiting) ونمررها للخريطة
Map<String, dynamic> fullRideArgs = {
// معرفات الرحلة
'rideId': rideInfo['id'].toString(),
'passengerId': rideInfo['passengerId'].toString(),
'driverId': driverId,
// المواقع (يجب أن تكون Strings بصيغة "lat,lng")
'passengerLocation': rideInfo['start_location'].toString(),
'passengerDestination': rideInfo['end_location'].toString(),
// الأسماء والعناوين
'startNameLocation': rideInfo['startName'].toString(),
'endNameLocation': rideInfo['endName'].toString(),
// تفاصيل الراكب
'name': rideInfo['first_name'] ?? 'Passenger',
'phone': rideInfo['phone'].toString(),
'email': rideInfo['email'] ?? '',
'tokenPassenger': rideInfo['passengerToken'].toString(),
'passengerWalletBurc': rideInfo['bruc'].toString(), // رصيد الراكب
// التفاصيل المالية والرحلة
'totalCost': rideInfo['price'].toString(), // السعر الكلي
'paymentAmount': rideInfo['price'].toString(), // المبلغ المطلوب
'Distance': rideInfo['distance'].toString(),
'Duration': rideInfo['duration'].toString(),
'durationOfRideValue':
rideInfo['duration'].toString(), // تكرار للتأكد
'carType': rideInfo['carType'].toString(),
// الدفع والمحفظة
'paymentMethod': (rideInfo['payment_method'] == 'visa' ||
rideInfo['payment_method'] == 'wallet')
? 'visa'
: 'cash',
'WalletChecked':
rideInfo['passenger_wallet'].toString() != '0' ? 'true' : 'false',
'kazan': Get.find<HomeCaptainController>()
.kazan
.toString(), // نسبة الشركة (من الكنترولر)
// بيانات إضافية (لتجنب الـ Null Safety errors)
'direction':
'http://googleusercontent.com/maps.google.com/maps?saddr=${rideInfo['start_location']}&daddr=${rideInfo['end_location']}',
'timeOfOrder': DateTime.now().toString(),
'isHaveSteps': 'false', // لو كان عندك خطوات في الـ waitingRides ضيفها
'step0': '', 'step1': '', 'step2': '', 'step3': '', 'step4': '',
};
// حفظ البيانات في الصندوق احتياطياً (Crash Recovery)
box.write(BoxName.rideArguments, fullRideArgs);
// الانتقال لصفحة الخريطة ومسح الصفحات السابقة لضمان عدم الرجوع للسوق
Get.offAll(() => PassengerLocationMapPage(), arguments: fullRideArgs);
} else {
// ❌ فشل: الرحلة أخذها سائق آخر
// نقوم بتحديث القائمة فوراً
Get.find<RideAvailableController>()
.getRideAvailable(forceRefresh: true);
MyDialog().getDialog(
"Trip taken".tr,
"This ride was just accepted by another driver.".tr,
() => Get.back(), // زر الموافقة
);
}
} catch (e) {
Get.back(); // إخفاء اللودينج في حال الخطأ
print("Accept Ride Error: $e");
MyDialog().getDialog(
"Error".tr,
"An unexpected error occurred. Please try again.".tr,
() => Get.back(),
);
} }
} }
} }

View File

@@ -605,6 +605,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
flutter_background_service:
dependency: "direct main"
description:
name: flutter_background_service
sha256: "70a1c185b1fa1a44f8f14ecd6c86f6e50366e3562f00b2fa5a54df39b3324d3d"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
flutter_background_service_android:
dependency: transitive
description:
name: flutter_background_service_android
sha256: ca0793d4cd19f1e194a130918401a3d0b1076c81236f7273458ae96987944a87
url: "https://pub.dev"
source: hosted
version: "6.3.1"
flutter_background_service_ios:
dependency: transitive
description:
name: flutter_background_service_ios
sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
flutter_background_service_platform_interface:
dependency: transitive
description:
name: flutter_background_service_platform_interface
sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41
url: "https://pub.dev"
source: hosted
version: "5.1.2"
flutter_cache_manager: flutter_cache_manager:
dependency: transitive dependency: transitive
description: description:
@@ -2090,6 +2122,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "2.0.2"
socket_io_client:
dependency: "direct main"
description:
name: socket_io_client
sha256: "64bd271703db3682d4195dd813c555413d21a49bbaef7c3ed38932fd2a209a10"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
socket_io_common:
dependency: transitive
description:
name: socket_io_common
sha256: "469c7e6bb0c8d571a5158c1352112654f03aedc2f0a246533e1cbdb41efa4937"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:

View File

@@ -109,6 +109,8 @@ dependencies:
# مكتبة التخزين لتحميل الخرائط أوفلاين # مكتبة التخزين لتحميل الخرائط أوفلاين
flutter_map_tile_caching: ^9.0.0 # (استخدم أحدث إصدار) flutter_map_tile_caching: ^9.0.0 # (استخدم أحدث إصدار)
latlong2: ^0.9.1 latlong2: ^0.9.1
socket_io_client: ^1.0.2
flutter_background_service: ^5.1.0
# مكتبة لتحديد النقاط على الخريطة (مثل latLng) # مكتبة لتحديد النقاط على الخريطة (مثل latLng)