Initial commit for Intaleq Driver

This commit is contained in:
Hamza-Ayed
2025-09-01 19:04:50 +03:00
parent 889c67a691
commit 8c7f3e3a75
46 changed files with 4300 additions and 6192 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"cmake.sourceDirectory": "/Users/hamzaaleghwairyeen/development/App/intaleq_driver/linux"
}

View File

@@ -44,10 +44,10 @@ android {
applicationId = "com.intaleq_driver" applicationId = "com.intaleq_driver"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 23 minSdk = 29
targetSdk = flutter.targetSdkVersion targetSdk = 36
versionCode = 8 versionCode = 13
versionName = '1.0.8' versionName = '1.0.13'
multiDexEnabled =true multiDexEnabled =true
} }

View File

@@ -32,10 +32,14 @@
<uses-feature android:name="android.hardware.camera.autofocus" /> <uses-feature android:name="android.hardware.camera.autofocus" />
<application <application
android:allowBackup="true" android:name="${applicationName}"
android:icon="@mipmap/launcher_icon" android:icon="@mipmap/launcher_icon"
android:label="Intaleq Driver" android:label="Intaleq Driver"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:allowBackup="false"
android:fullBackupContent="false"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="false"
android:theme="@style/LaunchTheme"> android:theme="@style/LaunchTheme">
<activity <activity

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">intaleq.xyz</domain>
<pin-set expiration="2027-01-01">
<!-- <pin digest="SHA-256">pXmP2hTQLxDEvlTVmP5N7xpiA32sycBsxB6hBFT2uL4=</pin> -->
<pin digest="SHA-256">XJXX7XthMj5VlSHfvo1q73sY7orJ9Wle0X4avj0/Vwo=</pin>
<pin digest="SHA-256">C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHESsl=</pin>
</pin-set>
</domain-config>
</network-security-config>

BIN
assets/alert.wav Normal file

Binary file not shown.

View File

@@ -12,6 +12,7 @@ class BoxName {
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 fingerPrint = "fingerPrint"; static const String fingerPrint = "fingerPrint";
static const String updateInterval = "updateInterval";
static const String payMobApikey = "payMobApikey"; static const String payMobApikey = "payMobApikey";
static const String refreshToken = "refreshToken"; static const String refreshToken = "refreshToken";
static const String lang = "lang"; static const String lang = "lang";

File diff suppressed because one or more lines are too long

View File

@@ -14,6 +14,7 @@ class AppLink {
'https://walletintaleq.intaleq.xyz/v1/main'; 'https://walletintaleq.intaleq.xyz/v1/main';
static final String endPoint = 'https://intaleq.xyz/intaleq'; static final String endPoint = 'https://intaleq.xyz/intaleq';
static final String syria = 'https://syria.intaleq.xyz/intaleq';
// 'https://api.tripz-egypt.com/tripz'; // 'https://api.tripz-egypt.com/tripz';
static final String server = endPoint; static final String server = endPoint;
static String seferCairoServer = endPoint; static String seferCairoServer = endPoint;
@@ -97,6 +98,7 @@ class AppLink {
//-----------------ridessss------------------ //-----------------ridessss------------------
static String addRides = "$ride/rides/add.php"; static String addRides = "$ride/rides/add.php";
static String getRides = "$ride/rides/get.php"; static String getRides = "$ride/rides/get.php";
static String getPlacesSyria = "$ride/places_syria/get.php";
static String getMishwari = "$ride/mishwari/get.php"; static String getMishwari = "$ride/mishwari/get.php";
static String getMishwariDriver = "$ride/mishwari/getDriver.php"; static String getMishwariDriver = "$ride/mishwari/getDriver.php";
static String getTripCountByCaptain = "$ride/rides/getTripCountByCaptain.php"; static String getTripCountByCaptain = "$ride/rides/getTripCountByCaptain.php";
@@ -250,6 +252,7 @@ class AppLink {
static String uploadImage = "$server/uploadImage.php"; static String uploadImage = "$server/uploadImage.php";
static String uploadImage1 = "$server/uploadImage1.php"; static String uploadImage1 = "$server/uploadImage1.php";
static String uploadImagePortrate = "$server/uploadImagePortrate.php"; static String uploadImagePortrate = "$server/uploadImagePortrate.php";
static String uploadSyrianDocs = "$syria/auth/syria/uploadSyrianDocs.php";
static String uploadImageType = "$server/uploadImageType.php"; static String uploadImageType = "$server/uploadImageType.php";
//=============egypt documents ============== //=============egypt documents ==============
static String uploadEgyptidFront = static String uploadEgyptidFront =
@@ -310,6 +313,8 @@ class AppLink {
static String updatePassengerGift = "$ride/invitor/updatePassengerGift.php"; static String updatePassengerGift = "$ride/invitor/updatePassengerGift.php";
static String updateInvitationCodeFromRegister = static String updateInvitationCodeFromRegister =
"$ride/invitor/updateInvitationCodeFromRegister.php"; "$ride/invitor/updateInvitationCodeFromRegister.php";
static String register_driver_and_car =
"$auth/syria/driver/register_driver_and_car.php";
static String updateDriverInvitationDirectly = static String updateDriverInvitationDirectly =
"$ride/invitor/updateDriverInvitationDirectly.php"; "$ride/invitor/updateDriverInvitationDirectly.php";
static String updatePassengersInvitation = static String updatePassengersInvitation =

View File

@@ -5,9 +5,8 @@ import 'package:crypto/crypto.dart';
import 'dart:math'; import 'dart:math';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:secure_string_operations/secure_string_operations.dart';
import 'package:sefer_driver/controller/functions/location_background_controller.dart';
import 'package:sefer_driver/views/auth/captin/cards/sms_signup.dart'; import 'package:sefer_driver/views/auth/captin/cards/sms_signup.dart';
import 'package:sefer_driver/views/auth/syria/registration_view.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.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/error_snakbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -21,13 +20,10 @@ import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart';
import 'package:location/location.dart'; import 'package:location/location.dart';
import '../../../constant/api_key.dart'; import '../../../constant/api_key.dart';
import '../../../constant/char_map.dart';
import '../../../constant/info.dart'; import '../../../constant/info.dart';
import '../../../constant/table_names.dart';
import '../../../print.dart';
import '../../../views/auth/captin/cards/syrian_card_a_i.dart';
import '../../../views/auth/captin/otp_page.dart'; import '../../../views/auth/captin/otp_page.dart';
import '../../../views/auth/captin/otp_token_page.dart'; import '../../../views/auth/captin/otp_token_page.dart';
import '../../../views/auth/syria/pending_driver_page.dart';
import '../../firebase/firbase_messge.dart'; import '../../firebase/firbase_messge.dart';
import '../../functions/encrypt_decrypt.dart'; import '../../functions/encrypt_decrypt.dart';
import '../../functions/package_info.dart'; import '../../functions/package_info.dart';
@@ -79,11 +75,11 @@ class LoginDriverController extends GetxController {
var res = await CRUD().get( var res = await CRUD().get(
link: AppLink.getTesterApp, link: AppLink.getTesterApp,
payload: {'appPlatform': AppInformation.appName}); payload: {'appPlatform': AppInformation.appName});
Log.print('res: ${res}'); // Log.print('res: ${res}');
if (res != 'failure') { if (res != 'failure') {
var d = jsonDecode(res); var d = jsonDecode(res);
isTest = d['message'][0]['isTest']; isTest = d['message'][0]['isTest'];
Log.print('isTest: ${isTest}'); // Log.print('isTest: ${isTest}');
box.write(BoxName.isTest, isTest); box.write(BoxName.isTest, isTest);
// Log.print('isTest: ${box.read(BoxName.isTest)}'); // Log.print('isTest: ${box.read(BoxName.isTest)}');
@@ -108,7 +104,8 @@ class LoginDriverController extends GetxController {
) )
}); });
if (res != 'failure') { if (res != 'failure') {
Get.offAll(() => SyrianCardAI()); // Get.offAll(() => SyrianCardAI());
Get.offAll(() => RegistrationView());
// isloading = false; // isloading = false;
// update(); // update();
@@ -322,46 +319,53 @@ class LoginDriverController extends GetxController {
} else if (int.parse(d['year'].toString()) < 2002) { } else if (int.parse(d['year'].toString()) < 2002) {
box.write(BoxName.carTypeOfDriver, 'Awfar Car'); box.write(BoxName.carTypeOfDriver, 'Awfar Car');
} }
updateAppTester(AppInformation.appName); // updateAppTester(AppInformation.appName);
if (d['status'].toString() != 'yet') {
var token = await CRUD().get(
link: AppLink.getDriverToken,
payload: {
'captain_id': (box.read(BoxName.driverID)).toString()
});
var token = await CRUD().get( String fingerPrint = await DeviceHelper.getDeviceFingerprint();
link: AppLink.getDriverToken, await storage.write(
payload: {'captain_id': (box.read(BoxName.driverID)).toString()}); key: BoxName.fingerPrint, value: fingerPrint.toString());
// print(jsonDecode(token)['data'][0]['token'].toString());
String fingerPrint = await DeviceHelper.getDeviceFingerprint(); // print(box.read(BoxName.tokenDriver).toString());
await storage.write( if (email == '962798583052@intaleqapp.com') {
key: BoxName.fingerPrint, value: fingerPrint.toString()); } else {
// print(jsonDecode(token)['data'][0]['token'].toString()); if (token != 'failure') {
// print(box.read(BoxName.tokenDriver).toString()); if ((jsonDecode(token)['data'][0]['token'].toString()) !=
if (email == '962798583052@intaleqapp.com') { box.read(BoxName.tokenDriver).toString()) {
} else { await Get.defaultDialog(
if (token != 'failure') { title: 'Device Change Detected'.tr,
if ((jsonDecode(token)['data'][0]['token'].toString()) != middleText: 'Please verify your identity'.tr,
box.read(BoxName.tokenDriver).toString()) { textConfirm: 'Verify'.tr,
await Get.defaultDialog( confirmTextColor: Colors.white,
title: 'Device Change Detected'.tr, onConfirm: () {
middleText: 'Please verify your identity'.tr, // Get.back();
textConfirm: 'Verify'.tr, // انتقل لصفحة OTP الجديدة
confirmTextColor: Colors.white, Get.to(
onConfirm: () { () => OtpVerificationPage(
// Get.back(); phone: d['phone'].toString(),
// انتقل لصفحة OTP الجديدة deviceToken: fingerPrint.toString(),
Get.to( token: token.toString(),
() => OtpVerificationPage( ptoken:
phone: d['phone'].toString(), jsonDecode(token)['data'][0]['token'].toString(),
deviceToken: fingerPrint.toString(), ),
token: token.toString(), );
ptoken: },
jsonDecode(token)['data'][0]['token'].toString(), );
), }
);
},
);
} }
} }
Get.offAll(() => HomeCaptain());
} else {
Get.off(() => DriverVerificationScreen());
} }
Get.off(() => HomeCaptain()); // Get.off(() => HomeCaptain());
} else { } else {
Get.offAll(() => PhoneNumberScreen()); Get.offAll(() => PhoneNumberScreen());
@@ -476,7 +480,8 @@ class LoginDriverController extends GetxController {
if (res == 'failure') { if (res == 'failure') {
//Failure //Failure
if (box.read(BoxName.phoneVerified).toString() == '1') { if (box.read(BoxName.phoneVerified).toString() == '1') {
Get.offAll(() => SyrianCardAI()); // Get.offAll(() => SyrianCardAI());
Get.offAll(() => RegistrationView());
} else { } else {
Get.offAll(() => SmsSignupEgypt()); Get.offAll(() => SmsSignupEgypt());
} }
@@ -551,24 +556,7 @@ class LoginDriverController extends GetxController {
'captain_id': box.read(BoxName.driverID).toString(), 'captain_id': box.read(BoxName.driverID).toString(),
'fingerPrint': (fingerPrint).toString() 'fingerPrint': (fingerPrint).toString()
}); });
await CRUD().post(
link:
"${AppLink.seferAlexandriaServer}/ride/firebase/addDriver.php",
payload: {
'token': box.read(BoxName.tokenDriver),
'captain_id':
box.read(BoxName.driverID).toString(),
'fingerPrint': (fingerPrint).toString()
});
await CRUD().post(
link:
"${AppLink.seferGizaServer}/ride/firebase/addDriver.php",
payload: {
'token': box.read(BoxName.tokenDriver),
'captain_id':
box.read(BoxName.driverID).toString(),
'fingerPrint': (fingerPrint).toString()
});
Get.back(); Get.back();
})); }));
} }

View File

@@ -1,15 +1,14 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart'; import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:sefer_driver/controller/functions/crud.dart'; import 'package:sefer_driver/controller/functions/crud.dart';
import 'package:sefer_driver/views/auth/captin/cards/syrian_card_a_i.dart'; import 'package:sefer_driver/print.dart';
import 'package:sefer_driver/views/home/on_boarding_page.dart'; import 'package:sefer_driver/views/home/on_boarding_page.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart'; import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import '../../../constant/box_name.dart'; import '../../../constant/box_name.dart';
import '../../../constant/links.dart'; import '../../../constant/links.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../../print.dart'; import '../../../views/auth/syria/registration_view.dart';
import '../../../views/auth/captin/otp_page.dart';
// --- Helper Class for Phone Authentication --- // --- Helper Class for Phone Authentication ---
@@ -27,9 +26,9 @@ class PhoneAuthHelper {
link: _sendOtpUrl, link: _sendOtpUrl,
payload: {'receiver': phoneNumber}, payload: {'receiver': phoneNumber},
); );
Log.print('response: ${response}');
if (response != 'failure') { if (response != 'failure') {
final data = (response); final data = (response);
Log.print('data: ${data}');
// if (data['status'] == 'success') { // if (data['status'] == 'success') {
mySnackbarSuccess('An OTP has been sent to your WhatsApp number.'.tr); mySnackbarSuccess('An OTP has been sent to your WhatsApp number.'.tr);
return true; return true;
@@ -42,7 +41,6 @@ class PhoneAuthHelper {
return false; return false;
} }
} catch (e) { } catch (e) {
Log.print('e: ${e}');
// mySnackeBarError('An error occurred: $e'); // mySnackeBarError('An error occurred: $e');
return false; return false;
} }
@@ -61,17 +59,14 @@ class PhoneAuthHelper {
if (data['status'] == 'success') { if (data['status'] == 'success') {
final isRegistered = data['message']['isRegistered'] ?? false; final isRegistered = data['message']['isRegistered'] ?? false;
Log.print('isRegistered: ${isRegistered}');
box.write(BoxName.phoneVerified, true); box.write(BoxName.phoneVerified, true);
box.write(BoxName.phoneDriver, phoneNumber); box.write(BoxName.phoneDriver, phoneNumber);
box.write(BoxName.driverID, data['message']['driverID']); box.write(BoxName.driverID, data['message']['driverID']);
Log.print('BoxName.driverID: ${box.read(BoxName.driverID)}');
if (isRegistered) { if (isRegistered) {
// ✅ السائق مسجل مسبقًا - سجل دخوله واذهب إلى الصفحة الرئيسية // ✅ السائق مسجل مسبقًا - سجل دخوله واذهب إلى الصفحة الرئيسية
final driver = data['message']['driver']; final driver = data['message']['driver'];
// mySnackbarSuccess('Welcome back, ${driver['first_name']}!'); // mySnackbarSuccess('Welcome back, ${driver['first_name']}!');
Log.print('Welcome: }');
// حفظ بيانات السائق إذا أردت: // حفظ بيانات السائق إذا أردت:
box.write(BoxName.driverID, driver['id']); box.write(BoxName.driverID, driver['id']);
@@ -82,7 +77,8 @@ class PhoneAuthHelper {
} else { } else {
// ✅ رقم الهاتف تم التحقق منه لكن السائق غير مسجل // ✅ رقم الهاتف تم التحقق منه لكن السائق غير مسجل
// mySnackbarSuccess('Phone verified. Please complete registration.'); // mySnackbarSuccess('Phone verified. Please complete registration.');
Get.to(() => SyrianCardAI()); // Get.offAll(() => SyrianCardAI());
Get.offAll(() => RegistrationView());
} }
} else { } else {
mySnackeBarError(data['message'] ?? 'Verification failed.'); mySnackeBarError(data['message'] ?? 'Verification failed.');
@@ -92,7 +88,6 @@ class PhoneAuthHelper {
} }
} catch (e) { } catch (e) {
mySnackeBarError('An error occurred: $e'); mySnackeBarError('An error occurred: $e');
Log.print('e: ${e}');
} }
} }
@@ -121,7 +116,6 @@ class PhoneAuthHelper {
"User with this phone number or email already exists.".tr); "User with this phone number or email already exists.".tr);
} }
} catch (e) { } catch (e) {
Log.print('e: ${e}');
mySnackeBarError('An error occurred: $e'); mySnackeBarError('An error occurred: $e');
} }
} }

View File

@@ -2,8 +2,6 @@ import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart'; import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:sefer_driver/views/auth/captin/cards/syrian_card_a_i.dart';
import 'package:sefer_driver/views/auth/captin/register_captin.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart'; import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -17,9 +15,8 @@ import 'package:sefer_driver/views/auth/captin/verify_email_captain.dart';
import '../../../constant/colors.dart'; import '../../../constant/colors.dart';
import '../../../views/auth/captin/ai_page.dart'; import '../../../views/auth/captin/ai_page.dart';
import '../../../views/auth/captin/car_license_page.dart'; import '../../../views/auth/syria/registration_view.dart';
import '../../../views/home/Captin/home_captain/home_captin.dart'; import '../../../views/home/Captin/home_captain/home_captin.dart';
import '../../functions/encrypt_decrypt.dart';
import '../../functions/sms_egypt_controller.dart'; import '../../functions/sms_egypt_controller.dart';
class RegisterCaptainController extends GetxController { class RegisterCaptainController extends GetxController {
@@ -282,7 +279,8 @@ class RegisterCaptainController extends GetxController {
// box.read(BoxName.driverID).toString(), // box.read(BoxName.driverID).toString(),
// box.read(BoxName.emailDriver).toString(), // box.read(BoxName.emailDriver).toString(),
// ); // );
Get.to(SyrianCardAI()); // Get.offAll(() => SyrianCardAI());
Get.offAll(() => RegistrationView());
// } else { // } else {
// Get.snackbar('title', 'message'); // Get.snackbar('title', 'message');
// } // }

View File

@@ -1,5 +1,6 @@
import 'package:sefer_driver/constant/box_name.dart'; import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart'; import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:sefer_driver/controller/functions/crud.dart';
import 'package:sefer_driver/main.dart'; import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/auth/captin/cards/sms_signup.dart'; import 'package:sefer_driver/views/auth/captin/cards/sms_signup.dart';
import 'package:sefer_driver/views/home/on_boarding_page.dart'; import 'package:sefer_driver/views/home/on_boarding_page.dart';
@@ -68,9 +69,10 @@ class GoogleSignInHelper {
} }
return googleUser; return googleUser;
} catch (error) { } catch (error, stackTrace) {
mySnackeBarError('$error'); mySnackeBarError('$error');
addError(error.toString(), 'GoogleSignInAccount?> signInFromLogin()'); CRUD.addError(error.toString(), stackTrace.toString(),
'GoogleSignInAccount?> signInFromLogin()');
return null; return null;
} }
} }

View File

@@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:image_cropper/image_cropper.dart'; import 'package:image_cropper/image_cropper.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'package:sefer_driver/constant/links.dart';
import '../../../constant/box_name.dart'; import '../../../constant/box_name.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
// --- Final Submission --- // --- Final Submission ---
@@ -19,8 +20,10 @@ import 'package:path/path.dart' as p;
import '../../../constant/colors.dart'; import '../../../constant/colors.dart';
import '../../../constant/info.dart'; import '../../../constant/info.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../../print.dart';
import '../../functions/crud.dart'; import '../../functions/crud.dart';
import '../../functions/encrypt_decrypt.dart'; import '../../functions/encrypt_decrypt.dart';
import '../captin/login_captin_controller.dart';
// You can create a simple enum to manage image types // You can create a simple enum to manage image types
enum ImageType { enum ImageType {
@@ -222,6 +225,97 @@ class RegistrationController extends GetxController {
return Color(int.parse(v, radix: 16)); return Color(int.parse(v, radix: 16));
} }
//uploadSyrianDocs
// دالة مساعدة: تضيف الحقل إذا كان له قيمة
void _addField(Map<String, String> fields, String key, String? value) {
if (value != null && value.toString().isNotEmpty) {
fields[key] = value.toString();
}
}
// دالة رفع إلى السيرفر السوري: ترجع file_url (Signed URL)
Future<String> uploadToSyria({
required String docType,
required File file,
required Uri syrianUploadUri,
required String authHeader,
required String hmacHeader,
required String driverId,
Duration timeout = const Duration(seconds: 60),
http.Client? clientOverride,
}) async {
final client = clientOverride ?? http.Client();
try {
final mime = lookupMimeType(file.path) ?? 'image/jpeg';
final parts = mime.split('/');
final req = http.MultipartRequest('POST', syrianUploadUri);
req.headers.addAll({
'Authorization': authHeader,
'X-HMAC-Auth': hmacHeader,
});
req.fields['driver_id'] = driverId;
req.fields['doc_type'] = docType;
req.files.add(
await http.MultipartFile.fromPath(
'file',
file.path,
filename: p.basename(file.path),
contentType: MediaType(parts.first, parts.last),
),
);
// ====== الطباعة قبل الإرسال ======
// Log.print('--- Syrian Upload Request ---');
// Log.print('URL: $syrianUploadUri');
// // Log.print('Method: POST');
// // Log.print('Headers: ${req.headers}');
// Log.print('Fields: ${req.fields}');
// // Log.print(
// // 'File: ${file.path} (${await file.length()} bytes, mime: $mime)');
// Log.print('-----------------------------');
// الإرسال
final streamed = await client.send(req).timeout(timeout);
final resp = await http.Response.fromStream(streamed);
// ====== الطباعة بعد الاستجابة ======
// Log.print('--- Syrian Upload Response ---');
Log.print('Status: ${resp.statusCode}');
// Log.print('Headers: ${resp.headers}');
// Log.print('Body: ${resp.body}');
// Log.print('-------------------------------');
Map<String, dynamic> j = {};
try {
j = jsonDecode(resp.body) as Map<String, dynamic>;
} catch (e) {
Log.print('⚠️ Failed to parse JSON: $e');
}
// التحمّل لشكلين من الـ JSON:
final statusOk = j['status'] == 'success';
final fileUrl = (j['file_url'] ?? j['message']?['file_url'])?.toString();
final fileName =
(j['file_name'] ?? j['message']?['file_name'])?.toString();
if (resp.statusCode == 200 &&
statusOk &&
(fileUrl?.isNotEmpty ?? false)) {
// Log.print(
// '✅ Syrian upload success: $fileUrl (file: ${fileName ?? "-"})');
return fileUrl!;
}
throw Exception(
'❌ Syrian upload failed ($docType): ${j['message'] ?? resp.body}');
} finally {
if (clientOverride == null) client.close();
}
}
Future<void> submitRegistration() async { Future<void> submitRegistration() async {
// 1) تحقق من الصور // 1) تحقق من الصور
if (driverLicenseFrontImage == null || if (driverLicenseFrontImage == null ||
@@ -229,30 +323,82 @@ class RegistrationController extends GetxController {
carLicenseFrontImage == null || carLicenseFrontImage == null ||
carLicenseBackImage == null) { carLicenseBackImage == null) {
Get.snackbar( Get.snackbar(
'Missing Documents'.tr, 'Please upload all 4 required documents.'.tr, 'Missing Documents'.tr,
snackPosition: SnackPosition.BOTTOM, 'Please upload all 4 required documents.'.tr,
backgroundColor: Colors.orange, snackPosition: SnackPosition.BOTTOM,
colorText: Colors.white); backgroundColor: Colors.orange,
colorText: Colors.white,
);
return; return;
} }
isLoading.value = true; isLoading.value = true;
final uri = Uri.parse( // روابط الـ API
'https://intaleq.xyz/intaleq/auth/syria/driver/register_driver_and_car.php', final registerUri =
); Uri.parse(AppLink.register_driver_and_car); // التسجيل الرئيسي (PHP)
final syrianUploadUri =
// Uri.parse(AppLink.uploadSyrianDocs); // رفع الصور في سوريا
Uri.parse(
'https://syria.intaleq.xyz/intaleq/auth/syria/uploadSyrianDocs.php'); // رفع الصور في سوريا
final client = http.Client(); final client = http.Client();
try { try {
final req = http.MultipartRequest('POST', uri); // ترويسات مشتركة
final bearer =
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
final hmac = '${box.read(BoxName.hmac)}';
// 2) ارفع الصور أولاً على السيرفر السوري واحصل على روابطها (Signed URLs)
final driverId = (box.read(BoxName.driverID) ?? '').toString();
final driverFrontUrl = await uploadToSyria(
docType: 'driver_license_front',
file: driverLicenseFrontImage!,
syrianUploadUri: syrianUploadUri,
authHeader: bearer,
hmacHeader: hmac,
driverId: driverId,
clientOverride: client,
);
final driverBackUrl = await uploadToSyria(
docType: 'driver_license_back',
file: driverLicenseBackImage!,
syrianUploadUri: syrianUploadUri,
authHeader: bearer,
hmacHeader: hmac,
driverId: driverId,
clientOverride: client,
);
final carFrontUrl = await uploadToSyria(
docType: 'car_license_front',
file: carLicenseFrontImage!,
syrianUploadUri: syrianUploadUri,
authHeader: bearer,
hmacHeader: hmac,
driverId: driverId,
clientOverride: client,
);
final carBackUrl = await uploadToSyria(
docType: 'car_license_back',
file: carLicenseBackImage!,
syrianUploadUri: syrianUploadUri,
authHeader: bearer,
hmacHeader: hmac,
driverId: driverId,
clientOverride: client,
);
// 3) جهّز طلب التسجيل الرئيسي: نرسل الحقول + روابط الصور (لا نرفع الصور مرة ثانية)
final req = http.MultipartRequest('POST', registerUri);
req.headers.addAll({
'Authorization': bearer,
'X-HMAC-Auth': hmac,
});
// مهم: لا تضع Content-Type يدويًا، الـ MultipartRequest يتكفّل فيه ببناء boundary.
final headers = {
'Authorization':
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}',
'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
};
// 2) الحقول النصية
final fields = <String, String>{}; final fields = <String, String>{};
// --- Driver Data --- // --- Driver Data ---
@@ -266,48 +412,33 @@ class RegistrationController extends GetxController {
fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك
_addField(fields, 'status', 'yet'); _addField(fields, 'status', 'yet');
_addField(fields, 'email', _addField(fields, 'email',
'Not specified'); // سكربت السيرفر سيحوّلها null ويبني ايميل افتراضي 'Not specified'); // السيرفر سيحوّلها null ويبني ايميل افتراضي
_addField(fields, 'gender', 'Male'); _addField(fields, 'gender', 'Male');
// --- Car Data (مطابقة لما يتوقّعه السكربت) --- // --- Car Data ---
_addField(fields, 'vin', 'carVinController.text);'); _addField(fields, 'vin', 'yet'); // تم تصحيح الاقتباس
_addField(fields, 'car_plate', carPlateController.text); _addField(fields, 'car_plate', carPlateController.text);
_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', 'carRegistrationExpiryController'); _addField(fields, 'expiration_date',
driverLicenseExpiryController.text); // تم التصحيح
_addField(fields, 'color', carColorController.text); _addField(fields, 'color', carColorController.text);
_addField(fields, 'fuel', 'Gasoline'); // أو حسب اختيارك _addField(fields, 'fuel', 'Gasoline');
_addField(fields, 'color_hex', colorHex); // مهم if (colorHex != null && colorHex!.isNotEmpty) {
// لو عندك حقول إضافية مطلوبة بالسكربت (مالك المركبة / الكود اللوني / الوقود) مرّرها: _addField(fields, 'color_hex', colorHex!);
_addField(fields, 'owner',
firstNameController.text + ' ' + lastNameController.text);
// if (colorHex != null) _addField(fields, 'color_hex', colorHex);
// if (fuelType != null) _addField(fields, 'fuel', fuelType);
req.headers.addAll(headers);
req.fields.addAll(fields);
// 3) الملفات (4 صور) — مفاتيحها مطابقة للسكربت
Future<void> addFile(String field, File file) async {
final mime = lookupMimeType(file.path) ?? 'image/jpeg';
final parts = mime.split('/');
final mediaType = MediaType(parts.first, parts.last);
req.files.add(
await http.MultipartFile.fromPath(
field,
file.path,
filename: p.basename(file.path),
contentType: mediaType,
),
);
} }
_addField(fields, 'owner',
'${firstNameController.text} ${lastNameController.text}');
await addFile('driver_license_front', driverLicenseFrontImage!); // --- روابط الصور الموقّعة من سوريا ---
await addFile('driver_license_back', driverLicenseBackImage!); _addField(fields, 'driver_license_front', driverFrontUrl);
await addFile('car_license_front', carLicenseFrontImage!); _addField(fields, 'driver_license_back', driverBackUrl);
await addFile('car_license_back', carLicenseBackImage!); _addField(fields, 'car_license_front', carFrontUrl);
_addField(fields, 'car_license_back', carBackUrl);
// (اختياري) هيدر للقبول بـ JSON // أضف الحقول
req.fields.addAll(fields);
// 4) الإرسال // 4) الإرسال
final streamed = final streamed =
@@ -320,46 +451,187 @@ class RegistrationController extends GetxController {
json = jsonDecode(resp.body) as Map<String, dynamic>; json = jsonDecode(resp.body) as Map<String, dynamic>;
} catch (_) {} } catch (_) {}
if (resp.statusCode == 200 && if (resp.statusCode == 200 && json?['status'] == 'success') {
json != null &&
json['status'] == 'success') {
// ممكن يرجّع driverID, carRegID, documents
final driverID = final driverID =
(json['data']?['driverID'] ?? json['driverID'])?.toString(); (json!['data']?['driverID'] ?? json['driverID'])?.toString();
if (driverID != null && driverID.isNotEmpty) { if (driverID != null && driverID.isNotEmpty) {
box.write(BoxName.driverID, driverID); box.write(BoxName.driverID, driverID);
} }
Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr, Get.snackbar(
snackPosition: SnackPosition.BOTTOM, 'Success'.tr,
backgroundColor: Colors.green, 'Registration completed successfully!'.tr,
colorText: Colors.white); snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
// TODO: انتقل للصفحة التالية أو حدّث الحالة… // TODO: التنقّل أو تحديث الحالة…
final email = box.read<String?>(BoxName.emailDriver) ?? '';
final c = Get.isRegistered<LoginDriverController>()
? Get.find<LoginDriverController>()
: Get.put(LoginDriverController());
c.loginWithGoogleCredential(driverId, email);
} else { } else {
final msg = final msg =
(json?['message'] ?? 'Registration failed. Please try again.') (json?['message'] ?? 'Registration failed. Please try again.')
.toString(); .toString();
Get.snackbar('Error'.tr, msg, Log.print('msg: ${msg}');
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red, Get.snackbar(
colorText: Colors.white); 'Error'.tr,
} msg,
} catch (e) {
Get.snackbar('Error'.tr, '${'An unexpected error occurred:'.tr} $e',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white); colorText: Colors.white,
);
}
} catch (e) {
Get.snackbar(
'Error'.tr,
'${'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;
} }
} }
// Future<void> submitRegistration() async {
// // 1) تحقق من الصور
// if (driverLicenseFrontImage == null ||
// driverLicenseBackImage == null ||
// carLicenseFrontImage == null ||
// carLicenseBackImage == null) {
// Get.snackbar(
// 'Missing Documents'.tr, 'Please upload all 4 required documents.'.tr,
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: Colors.orange,
// colorText: Colors.white);
// return;
// }
// isLoading.value = true;
// final uri = Uri.parse(
// 'https://intaleq.xyz/intaleq/auth/syria/driver/register_driver_and_car.php',
// );
// final client = http.Client();
// try {
// final req = http.MultipartRequest('POST', uri);
// // مهم: لا تضع Content-Type يدويًا، الـ MultipartRequest يتكفّل فيه ببناء boundary.
// final headers = {
// 'Authorization':
// 'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}',
// 'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
// };
// // 2) الحقول النصية
// final fields = <String, String>{};
// // --- Driver Data ---
// _addField(fields, 'id', box.read(BoxName.driverID)?.toString());
// _addField(fields, 'first_name', firstNameController.text);
// _addField(fields, 'last_name', lastNameController.text);
// _addField(fields, 'phone', box.read(BoxName.phoneDriver) ?? '');
// _addField(fields, 'national_number', nationalIdController.text);
// _addField(fields, 'expiry_date', driverLicenseExpiryController.text);
// _addField(
// fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك
// _addField(fields, 'status', 'yet');
// _addField(fields, 'email',
// 'Not specified'); // سكربت السيرفر سيحوّلها null ويبني ايميل افتراضي
// _addField(fields, 'gender', 'Male');
// // --- Car Data (مطابقة لما يتوقّعه السكربت) ---
// _addField(fields, 'vin', 'carVinController.text);');
// _addField(fields, 'car_plate', carPlateController.text);
// _addField(fields, 'make', carMakeController.text);
// _addField(fields, 'model', carModelController.text);
// _addField(fields, 'year', carYearController.text);
// _addField(fields, 'expiration_date', 'carRegistrationExpiryController');
// _addField(fields, 'color', carColorController.text);
// _addField(fields, 'fuel', 'Gasoline'); // أو حسب اختيارك
// _addField(fields, 'color_hex', colorHex); // مهم
// // لو عندك حقول إضافية مطلوبة بالسكربت (مالك المركبة / الكود اللوني / الوقود) مرّرها:
// _addField(fields, 'owner',
// firstNameController.text + ' ' + lastNameController.text);
// // if (colorHex != null) _addField(fields, 'color_hex', colorHex);
// // if (fuelType != null) _addField(fields, 'fuel', fuelType);
// req.headers.addAll(headers);
// req.fields.addAll(fields);
// // 3) الملفات (4 صور) — مفاتيحها مطابقة للسكربت
// Future<void> addFile(String field, File file) async {
// final mime = lookupMimeType(file.path) ?? 'image/jpeg';
// final parts = mime.split('/');
// final mediaType = MediaType(parts.first, parts.last);
// req.files.add(
// await http.MultipartFile.fromPath(
// field,
// file.path,
// filename: p.basename(file.path),
// contentType: mediaType,
// ),
// );
// }
// await addFile('driver_license_front', driverLicenseFrontImage!);
// await addFile('driver_license_back', driverLicenseBackImage!);
// await addFile('car_license_front', carLicenseFrontImage!);
// await addFile('car_license_back', carLicenseBackImage!);
// // 4) الإرسال
// final streamed =
// await client.send(req).timeout(const Duration(seconds: 60));
// final resp = await http.Response.fromStream(streamed);
// // 5) فحص النتيجة
// Map<String, dynamic>? json;
// try {
// json = jsonDecode(resp.body) as Map<String, dynamic>;
// } catch (_) {}
// if (resp.statusCode == 200 &&
// json != null &&
// json['status'] == 'success') {
// // ممكن يرجّع driverID, carRegID, documents
// final driverID =
// (json['data']?['driverID'] ?? json['driverID'])?.toString();
// if (driverID != null && driverID.isNotEmpty) {
// box.write(BoxName.driverID, driverID);
// }
// Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr,
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: Colors.green,
// colorText: Colors.white);
// // TODO: انتقل للصفحة التالية أو حدّث الحالة…
// } else {
// final msg =
// (json?['message'] ?? 'Registration failed. Please try again.')
// .toString();
// Get.snackbar('Error'.tr, msg,
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: Colors.red,
// colorText: Colors.white);
// }
// } catch (e) {
// Get.snackbar('Error'.tr, '${'An unexpected error occurred:'.tr} $e',
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: Colors.red,
// colorText: Colors.white);
// } finally {
// client.close();
// isLoading.value = false;
// }
// }
// Helpers // Helpers
void _addField(Map<String, String> fields, String key, String? value) {
if (value != null && value.toString().trim().isNotEmpty) {
fields[key] = value.toString().trim();
}
}
} }

View File

@@ -344,6 +344,7 @@ class FirebaseMessagesController extends GetxController {
title: 'Ok'.tr, title: 'Ok'.tr,
onPressed: () { onPressed: () {
box.write(BoxName.rideStatus, 'Cancel'); box.write(BoxName.rideStatus, 'Cancel');
box.write(BoxName.statusDriverLocation, 'off');
Log.print( Log.print(
'rideStatus from 347 : ${box.read(BoxName.rideStatus)}'); 'rideStatus from 347 : ${box.read(BoxName.rideStatus)}');
Get.offAll(HomeCaptain()); Get.offAll(HomeCaptain());

View File

@@ -3,17 +3,30 @@ import '../../constant/links.dart';
import '../../main.dart'; import '../../main.dart';
import 'crud.dart'; import 'crud.dart';
addError(String error, where) async { addError1(String error, String details, String where) async {
CRUD().post(link: AppLink.addError, payload: { try {
'error': error.toString(), // Example error description // Get user information for the error log
'userId': box.read(BoxName.driverID) ?? final userId = box.read(BoxName.driverID) ?? box.read(BoxName.passengerID);
box.read(BoxName.passengerID), // Example user ID final userType =
'userType': box.read(BoxName.driverID) != null box.read(BoxName.driverID) != null ? 'Driver' : 'passenger';
? 'Driver' final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
: 'passenger', // Example user type
'phone': box.read(BoxName.phone) ??
box.read(BoxName.phoneDriver), // Example phone number
'device': where // Send the error data to the server
}); // Note: This is a fire-and-forget call. We don't await it or handle its response
// to prevent an infinite loop if the addError endpoint itself is failing.
CRUD().post(
link: AppLink.addError,
payload: {
'error': error.toString(),
'userId': userId.toString(),
'userType': userType,
'phone': phone.toString(),
'device': where, // The location of the error
'details': details, // The detailed stack trace or context
},
);
} catch (e) {
// If logging the error itself fails, print to the console to avoid infinite loops.
print("Failed to log error to server: $e");
}
} }

View File

@@ -17,6 +17,201 @@ import 'gemeni.dart';
import 'upload_image.dart'; import 'upload_image.dart';
class CRUD { class CRUD {
/// Stores the signature of the last logged error to prevent duplicates.
static String _lastErrorSignature = '';
/// Stores the timestamp of the last logged error.
static DateTime _lastErrorTimestamp =
DateTime(2000); // Initialize with an old date
/// The minimum time that must pass before logging the same error again.
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
/// Asynchronously logs an error to the server with debouncing to prevent log flooding.
///
/// [error]: A concise description of the error.
/// [details]: Detailed information, such as a stack trace or the server response body.
/// [where]: The location in the code where the error occurred (e.g., 'ClassName.methodName').
static Future<void> addError(
String error, String details, String where) async {
try {
// Create a unique signature for the current error
final currentErrorSignature = '$where-$error';
final now = DateTime.now();
// Check if the same error occurred recently
if (currentErrorSignature == _lastErrorSignature &&
now.difference(_lastErrorTimestamp) < _errorLogDebounceDuration) {
// If it's the same error within the debounce duration, ignore it.
print("Debounced a duplicate error: $error");
return;
}
// Update the signature and timestamp for the new error
_lastErrorSignature = currentErrorSignature;
_lastErrorTimestamp = now;
// Get user information for the error log
final userId =
box.read(BoxName.driverID) ?? box.read(BoxName.passengerID);
final userType =
box.read(BoxName.driverID) != null ? 'Driver' : 'passenger';
final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
// Send the error data to the server
// Note: This is a fire-and-forget call. We don't await it or handle its response
// to prevent an infinite loop if the addError endpoint itself is failing.
CRUD().post(
link: AppLink.addError,
payload: {
'error': error.toString(),
'userId': userId.toString(),
'userType': userType,
'phone': phone.toString(),
'device': where, // The location of the error
'details': details, // The detailed stack trace or context
},
);
} catch (e) {
// If logging the error itself fails, print to the console to avoid infinite loops.
print("Failed to log error to server: $e");
}
}
Future<dynamic> _makeRequest({
required String link,
Map<String, dynamic>? payload,
required Map<String, String> headers,
}) async {
var url = Uri.parse(link);
try {
var response = await http.post(
url,
body: payload,
headers: headers,
);
// Handle successful response (200 OK)
if (response.statusCode == 200) {
try {
var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'success') {
return jsonData; // Return the full JSON object on success
} else {
// The API reported a logical failure (e.g., validation error)
addError(
'API Logic Error: ${jsonData['status']}',
'Response: ${response.body}',
'CRUD._makeRequest - $link',
);
return jsonData['status']; // Return the specific status string
}
} catch (e, stackTrace) {
// Error decoding the JSON response from the server
addError(
'JSON Decode Error: $e',
'Response Body: ${response.body}\nStack Trace: $stackTrace',
'CRUD._makeRequest - $link',
);
return 'failure';
}
}
// Handle Unauthorized (401) - typically means token expired
else if (response.statusCode == 401) {
var jsonData = jsonDecode(response.body);
if (jsonData['error'] == 'Token expired') {
// The token refresh logic is handled before the call,
// but we log this case if it still happens.
// addError(
// 'Token Expired',
// 'A new token should have been fetched before this call.',
// 'CRUD._makeRequest - $link',
// );
return 'token_expired';
} else {
// Other 401 errors (e.g., invalid token)
addError(
'Unauthorized Error: ${jsonData['error']}',
'Status Code: 401',
'CRUD._makeRequest - $link',
);
return 'failure';
}
}
// Handle all other non-successful status codes
else {
addError(
'HTTP Error',
'Status Code: ${response.statusCode}\nResponse Body: ${response.body}',
'CRUD._makeRequest - $link',
);
return 'failure';
}
} catch (e, stackTrace) {
// Handle network exceptions (e.g., no internet, DNS error)
addError(
'HTTP Request Exception: $e',
'Stack Trace: $stackTrace',
'CRUD._makeRequest - $link',
);
return 'failure';
}
}
Future<dynamic> post({
required String link,
Map<String, dynamic>? payload,
}) async {
// 1. Check if the token is expired
bool isTokenExpired = JwtDecoder.isExpired(X
.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
.toString()
.split(AppInformation.addd)[0]);
// 2. If expired, get a new one
if (isTokenExpired) {
await LoginDriverController().getJWT();
}
// 3. Prepare the headers with the valid token
final headers = {
"Content-Type": "application/x-www-form-urlencoded",
'Authorization':
'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}'
};
// 4. Make the request using the centralized helper
return await _makeRequest(
link: link,
payload: payload,
headers: headers,
);
}
/// Performs an authenticated POST request to the wallet endpoints.
/// Uses a separate JWT and HMAC for authentication.
Future<dynamic> postWallet({
required String link,
Map<String, dynamic>? payload,
}) async {
// 1. Get the specific JWT and HMAC for the wallet
var jwt = await LoginDriverController().getJwtWallet();
final hmac = box.read(BoxName.hmac);
// 2. Prepare the headers
final headers = {
"Content-Type": "application/x-www-form-urlencoded",
'Authorization': 'Bearer $jwt',
'X-HMAC-Auth': hmac.toString(),
};
// 3. Make the request using the centralized helper
return await _makeRequest(
link: link,
payload: payload,
headers: headers,
);
}
Future<dynamic> get({ Future<dynamic> get({
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,
@@ -125,132 +320,132 @@ class CRUD {
} }
} }
Future<dynamic> postWallet( // Future<dynamic> postWallet(
{required String link, Map<String, dynamic>? payload}) async { // {required String link, Map<String, dynamic>? payload}) async {
var s = await LoginDriverController().getJwtWallet(); // var s = await LoginDriverController().getJwtWallet();
// Log.print('jwt: ${s}'); // // Log.print('jwt: ${s}');
final hmac = box.read(BoxName.hmac); // final hmac = box.read(BoxName.hmac);
// Log.print('hmac: ${hmac}'); // // Log.print('hmac: ${hmac}');
var url = Uri.parse(link); // var url = Uri.parse(link);
// Log.print('url: ${url}'); // // Log.print('url: ${url}');
try { // try {
// await LoginDriverController().getJWT(); // // await LoginDriverController().getJWT();
var response = await http.post( // var response = await http.post(
url, // url,
body: payload, // body: payload,
headers: { // headers: {
"Content-Type": "application/x-www-form-urlencoded", // "Content-Type": "application/x-www-form-urlencoded",
'Authorization': 'Bearer $s', // 'Authorization': 'Bearer $s',
'X-HMAC-Auth': hmac.toString(), // 'X-HMAC-Auth': hmac.toString(),
}, // },
); // );
// Log.print('response.request:${response.request}'); // // Log.print('response.request:${response.request}');
// Log.print('response.body: ${response.body}'); // // Log.print('response.body: ${response.body}');
// Log.print('payload:$payload'); // // Log.print('payload:$payload');
if (response.statusCode == 200) { // if (response.statusCode == 200) {
try { // try {
var jsonData = jsonDecode(response.body); // var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'success') { // if (jsonData['status'] == 'success') {
return jsonData; // return jsonData;
} else { // } else {
return jsonData['status']; // return jsonData['status'];
} // }
} catch (e) { // } catch (e) {
// addError(e.toString(), 'crud().post - JSON decoding'); // // addError(e.toString(), 'crud().post - JSON decoding');
return 'failure'; // return 'failure';
} // }
} else if (response.statusCode == 401) { // } else if (response.statusCode == 401) {
// Specifically handle 401 Unauthorized // // Specifically handle 401 Unauthorized
var jsonData = jsonDecode(response.body); // var jsonData = jsonDecode(response.body);
if (jsonData['error'] == 'Token expired') { // if (jsonData['error'] == 'Token expired') {
return 'token_expired'; // Return a specific value for token expiration // return 'token_expired'; // Return a specific value for token expiration
} else { // } else {
// Other 401 errors // // Other 401 errors
// addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); // // addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401');
return 'failure'; // return 'failure';
} // }
} else { // } else {
// addError('Non-200 response code: ${response.statusCode}', // // addError('Non-200 response code: ${response.statusCode}',
// 'crud().post - Other'); // // 'crud().post - Other');
return 'failure'; // return 'failure';
} // }
} catch (e) { // } catch (e) {
// addError('HTTP request error: $e', 'crud().post - HTTP'); // // addError('HTTP request error: $e', 'crud().post - HTTP');
return 'failure'; // return 'failure';
} // }
} // }
Future<dynamic> post( // Future<dynamic> post(
{required String link, Map<String, dynamic>? payload}) async { // {required String link, Map<String, dynamic>? payload}) async {
var url = Uri.parse(link); // var url = Uri.parse(link);
try { // try {
bool isTokenExpired = JwtDecoder.isExpired(X // bool isTokenExpired = JwtDecoder.isExpired(X
.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs) // .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
.toString() // .toString()
.split(AppInformation.addd)[0]); // .split(AppInformation.addd)[0]);
if (isTokenExpired) { // if (isTokenExpired) {
await LoginDriverController().getJWT(); // await LoginDriverController().getJWT();
} // }
var response = await http.post( // var response = await http.post(
url, // url,
body: payload, // body: payload,
headers: { // headers: {
"Content-Type": "application/x-www-form-urlencoded", // "Content-Type": "application/x-www-form-urlencoded",
'Authorization': // 'Authorization':
'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}' // 'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}'
// 'Authorization': 'Bearer ${box.read(BoxName.jwt)}' // // 'Authorization': 'Bearer ${box.read(BoxName.jwt)}'
}, // },
); // );
// print(response.request); // print(response.request);
// Log.print('response.body: ${response.body}'); // Log.print('response.body: ${response.body}');
// print(payload); // print(payload);
if (response.statusCode == 200) { // if (response.statusCode == 200) {
try { // try {
var jsonData = jsonDecode(response.body); // var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'success') { // if (jsonData['status'] == 'success') {
return jsonData; // return jsonData;
} else { // } else {
return jsonData['status']; // return jsonData['status'];
} // }
} catch (e) { // } catch (e) {
// addError(e.toString(), url); // // addError(e.toString(), url);
return 'failure'; // return 'failure';
} // }
} else if (response.statusCode == 401) { // } else if (response.statusCode == 401) {
// Specifically handle 401 Unauthorized // // Specifically handle 401 Unauthorized
var jsonData = jsonDecode(response.body); // var jsonData = jsonDecode(response.body);
if (jsonData['error'] == 'Token expired') { // if (jsonData['error'] == 'Token expired') {
// Show snackbar prompting to re-login // // Show snackbar prompting to re-login
// await Get.put(LoginDriverController()).getJWT(); // // await Get.put(LoginDriverController()).getJWT();
// MyDialog().getDialog( // // MyDialog().getDialog(
// 'Session expired. Please log in again.'.tr, // // 'Session expired. Please log in again.'.tr,
// '', // // '',
// () { // // () {
// Get.put(LoginController()).loginUsingCredentials( // // Get.put(LoginController()).loginUsingCredentials(
// box.read(BoxName.passengerID), box.read(BoxName.email)); // // box.read(BoxName.passengerID), box.read(BoxName.email));
// Get.back(); // // Get.back();
// }, // // },
// ); // // );
return 'token_expired'; // Return a specific value for token expiration // return 'token_expired'; // Return a specific value for token expiration
} else { // } else {
// Other 401 errors // // Other 401 errors
// addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); // // addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401');
return 'failure'; // return 'failure';
} // }
} else { // } else {
// addError('Non-200 response code: ${response.statusCode}', // // addError('Non-200 response code: ${response.statusCode}',
// 'crud().post - Other'); // // 'crud().post - Other');
return 'failure'; // return 'failure';
} // }
} catch (e) { // } catch (e) {
// addError('HTTP request error: $e', 'crud().post - HTTP'); // // addError('HTTP request error: $e', 'crud().post - HTTP');
return 'failure'; // return 'failure';
} // }
} // }
Future<dynamic> getAgoraToken({ Future<dynamic> getAgoraToken({
required String channelName, required String channelName,
@@ -579,7 +774,10 @@ class CRUD {
url, url,
body: payload, body: payload,
); );
Log.print('esponse.body: ${response.body}');
Log.print('esponse.body: ${response.request}');
var jsonData = jsonDecode(response.body); var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'OK') { if (jsonData['status'] == 'OK') {
return jsonData; return jsonData;
} }

View File

@@ -290,7 +290,6 @@ class AI extends GetxController {
'site': (idBackSy['address'].toString()) ?? 'Not specified', 'site': (idBackSy['address'].toString()) ?? 'Not specified',
'employmentType': 'Not specified', 'employmentType': 'Not specified',
}; };
Log.print('payload driver: ${payload}');
try { try {
var res = await CRUD().post(link: AppLink.signUpCaptin, payload: payload); var res = await CRUD().post(link: AppLink.signUpCaptin, payload: payload);
@@ -303,7 +302,6 @@ class AI extends GetxController {
isDriverSaved = true; isDriverSaved = true;
box.write(BoxName.emailDriver, box.write(BoxName.emailDriver,
'${box.read(BoxName.phoneDriver)}${Env.email}'); '${box.read(BoxName.phoneDriver)}${Env.email}');
Log.print('BoxName.emailDriver: ${box.read(BoxName.emailDriver)}');
mySnackbarSuccess('Driver data saved successfully'); mySnackbarSuccess('Driver data saved successfully');
} else { } else {
mySnackeBarError('${'Failed to save driver data'.tr}: }'); mySnackeBarError('${'Failed to save driver data'.tr}: }');
@@ -345,7 +343,6 @@ class AI extends GetxController {
'color_hex': vehicleFrontSy['colorHex'].toString(), 'color_hex': vehicleFrontSy['colorHex'].toString(),
'fuel': vehicleBackSy['fuel'].toString(), 'fuel': vehicleBackSy['fuel'].toString(),
}; };
Log.print('payload: ${payload}');
var res = var res =
await CRUD().post(link: AppLink.addRegisrationCar, payload: payload); await CRUD().post(link: AppLink.addRegisrationCar, payload: payload);
isLoading = false; isLoading = false;
@@ -500,7 +497,6 @@ class AI extends GetxController {
final response = await request.send(); final response = await request.send();
final result = await http.Response.fromStream(response); final result = await http.Response.fromStream(response);
Log.print('result: ${result.body}');
if (result.statusCode == 200) { if (result.statusCode == 200) {
final responseData = jsonDecode(result.body); final responseData = jsonDecode(result.body);
@@ -557,7 +553,6 @@ class AI extends GetxController {
isloading = false; isloading = false;
update(); update();
MyDialog().getDialog("Error".tr, e.toString(), () => Get.back()); MyDialog().getDialog("Error".tr, e.toString(), () => Get.back());
Log.print('e: ${e}');
} }
} }
@@ -620,10 +615,7 @@ class AI extends GetxController {
var extractedString = var extractedString =
await CRUD().arabicTextExtractByVisionAndAI(imagePath: imagePath); await CRUD().arabicTextExtractByVisionAndAI(imagePath: imagePath);
var json = jsonDecode(extractedString); var json = jsonDecode(extractedString);
// Log.print('extractedString: ${extractedString}');
var textValues = CRUD().extractTextFromLines(json); var textValues = CRUD().extractTextFromLines(json);
Log.print('textValues: $textValues');
// Log.print('json: ${json}');
DocumentType detectedType = checkDocumentType(textValues); DocumentType detectedType = checkDocumentType(textValues);
String expectedDocument = getExpectedDocument(imagePath); String expectedDocument = getExpectedDocument(imagePath);
@@ -930,7 +922,6 @@ class AI extends GetxController {
jsonDecode(responseData['content'][0]['text']); jsonDecode(responseData['content'][0]['text']);
} else if (idType == 'non_id_front') { } else if (idType == 'non_id_front') {
responseNonIdCardFront = jsonDecode(responseData['content'][0]['text']); responseNonIdCardFront = jsonDecode(responseData['content'][0]['text']);
Log.print('responseNonIdCardFront: $responseNonIdCardFront');
} else if (idType == 'non_id_back') { } else if (idType == 'non_id_back') {
responseNonIdCardBack = jsonDecode(responseData['content'][0]['text']); responseNonIdCardBack = jsonDecode(responseData['content'][0]['text']);
} }

View File

@@ -36,6 +36,7 @@ class HomeCaptainController extends GetxController {
speedPrice = 0, speedPrice = 0,
deliveryPrice = 0, deliveryPrice = 0,
mashwariPrice = 0, mashwariPrice = 0,
familyPrice = 0,
fuelPrice = 0; fuelPrice = 0;
double naturePrice = 0; double naturePrice = 0;
bool isCallOn = false; bool isCallOn = false;
@@ -384,6 +385,7 @@ class HomeCaptainController extends GetxController {
speedPrice = double.parse(json['message'][0]['speedPrice']); speedPrice = double.parse(json['message'][0]['speedPrice']);
deliveryPrice = double.parse(json['message'][0]['deliveryPrice']); deliveryPrice = double.parse(json['message'][0]['deliveryPrice']);
mashwariPrice = double.parse(json['message'][0]['freePrice']); mashwariPrice = double.parse(json['message'][0]['freePrice']);
familyPrice = double.parse(json['message'][0]['familyPrice']);
fuelPrice = double.parse(json['message'][0]['fuelPrice']); fuelPrice = double.parse(json['message'][0]['fuelPrice']);
} }
update(); update();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,574 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_polyline_algorithm/google_polyline_algorithm.dart';
import 'package:sefer_driver/constant/colors.dart';
// استخدام نفس مسارات الاستيراد التي قدمتها
import '../../../constant/api_key.dart';
import '../../../constant/links.dart';
import '../../functions/crud.dart';
import '../../functions/tts.dart';
class NavigationController extends GetxController {
// --- متغيرات الحالة العامة ---
bool isLoading = false;
GoogleMapController? mapController;
final TextEditingController placeDestinationController =
TextEditingController();
// --- متغيرات الخريطة والموقع ---
LatLng? myLocation;
double heading = 0.0;
final Set<Marker> markers = {};
final Set<Polyline> polylines = {};
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
BitmapDescriptor destinationIcon = BitmapDescriptor.defaultMarker;
// --- متغيرات النظام الذكي للتحديث ---
Timer? _locationUpdateTimer; // المؤقت الرئيسي للتحكم في التحديثات
Duration _currentUpdateInterval =
const Duration(seconds: 2); // القيمة الافتراضية
// --- متغيرات البحث عن الأماكن ---
List<dynamic> placesDestination = [];
Timer? _debounce;
// --- متغيرات الملاحة (Navigation) ---
LatLng? _finalDestination;
List<Map<String, dynamic>> routeSteps = [];
List<LatLng> _fullRouteCoordinates = [];
List<List<LatLng>> _stepPolylines = []; // لتخزين نقاط كل خطوة على حدة
bool _nextInstructionSpoken = false;
String currentInstruction = "";
String nextInstruction = "";
int currentStepIndex = 0;
double currentSpeed = 0.0;
String distanceToNextStep = "";
final List<LatLngBounds> _stepBounds = [];
@override
void onInit() {
super.onInit();
_initialize();
}
Future<void> _initialize() async {
await _loadCustomIcons();
await _getCurrentLocationAndStartUpdates();
if (!Get.isRegistered<TextToSpeechController>()) {
Get.put(TextToSpeechController());
}
}
@override
void onClose() {
_locationUpdateTimer?.cancel(); // إيقاف المؤقت عند إغلاق الصفحة
mapController?.dispose();
_debounce?.cancel();
placeDestinationController.dispose();
super.onClose();
}
// =======================================================================
// ١. النظام الذكي لتحديد الموقع والتحديث
// =======================================================================
Future<void> _getCurrentLocationAndStartUpdates() async {
try {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
myLocation = LatLng(position.latitude, position.longitude);
update();
animateCameraToPosition(myLocation!);
// بدء التحديثات باستخدام المؤقت بدلاً من الـ Stream
_startLocationTimer();
} catch (e) {
print("Error getting location: $e");
}
}
// --- تم استبدال الـ Stream بمؤقت للتحكم الكامل ---
void _startLocationTimer() {
_locationUpdateTimer?.cancel(); // إلغاء أي مؤقت قديم
_locationUpdateTimer = Timer.periodic(_currentUpdateInterval, (timer) {
_updateLocationAndProcess();
});
}
// --- هذه الدالة هي التي تعمل الآن بشكل دوري ---
Future<void> _updateLocationAndProcess() async {
try {
// طلب موقع واحد فقط عند كل مرة يعمل فيها المؤقت
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
myLocation = LatLng(position.latitude, position.longitude);
heading = position.heading;
currentSpeed = position.speed * 3.6;
_updateCarMarker();
if (polylines.isNotEmpty && myLocation != null) {
animateCameraToPosition(
myLocation!,
bearing: heading,
zoom: 18.5,
);
_checkNavigationStep(myLocation!);
}
update();
} catch (e) {
print("Error in _updateLocationAndProcess: $e");
}
}
// --- الدالة المسؤولة عن تغيير سرعة التحديث ديناميكياً ---
void _adjustUpdateInterval() {
if (currentStepIndex >= routeSteps.length) return;
final currentStepDistance =
routeSteps[currentStepIndex]['distance']['value'];
// إذا كانت الخطوة الحالية طويلة (شارع سريع > 1.5 كم)
if (currentStepDistance > 1500) {
_currentUpdateInterval = const Duration(seconds: 4);
}
// إذا كانت الخطوة قصيرة (منعطفات داخل المدينة < 1.5 كم)
else {
_currentUpdateInterval = const Duration(seconds: 2);
}
// إعادة تشغيل المؤقت بالسرعة الجديدة
_startLocationTimer();
}
// ... باقي دوال إعداد الخريطة ...
void onMapCreated(GoogleMapController controller) {
mapController = controller;
if (myLocation != null) {
animateCameraToPosition(myLocation!);
}
}
void _updateCarMarker() {
if (myLocation == null) return;
markers.removeWhere((m) => m.markerId.value == 'myLocation');
markers.add(
Marker(
markerId: const MarkerId('myLocation'),
position: myLocation!,
icon: carIcon,
rotation: heading,
anchor: const Offset(0.5, 0.5),
flat: true,
),
);
}
void animateCameraToPosition(LatLng position,
{double zoom = 16.0, double bearing = 0.0}) {
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: position, zoom: zoom, bearing: bearing, tilt: 45.0),
),
);
}
// =======================================================================
// ٢. الملاحة والتحقق من الخطوات
// =======================================================================
void _checkNavigationStep(LatLng currentPosition) {
if (routeSteps.isEmpty ||
currentStepIndex >= routeSteps.length ||
_finalDestination == null) return;
_updateTraveledPolyline(currentPosition);
final step = routeSteps[currentStepIndex];
final endLatLng =
LatLng(step['end_location']['lat'], step['end_location']['lng']);
final distance = Geolocator.distanceBetween(
currentPosition.latitude,
currentPosition.longitude,
endLatLng.latitude,
endLatLng.longitude,
);
distanceToNextStep = (distance > 1000)
? "${(distance / 1000).toStringAsFixed(1)} كم"
: "${distance.toStringAsFixed(0)} متر";
if (distance < 30 &&
!_nextInstructionSpoken &&
nextInstruction.isNotEmpty) {
Get.find<TextToSpeechController>().speakText(nextInstruction);
_nextInstructionSpoken = true;
}
if (distance < 20) {
_advanceStep();
}
}
void _advanceStep() {
currentStepIndex++;
if (currentStepIndex < routeSteps.length) {
currentInstruction =
_parseInstruction(routeSteps[currentStepIndex]['html_instructions']);
nextInstruction = ((currentStepIndex + 1) < routeSteps.length)
? _parseInstruction(
routeSteps[currentStepIndex + 1]['html_instructions'])
: "الوجهة النهائية";
_nextInstructionSpoken = false;
// **هنا يتم تعديل سرعة التحديث عند الانتقال لخطوة جديدة**
_adjustUpdateInterval();
if (currentStepIndex < _stepBounds.length) {
mapController?.animateCamera(
CameraUpdate.newLatLngBounds(_stepBounds[currentStepIndex], 70.0));
}
update();
} else {
currentInstruction = "لقد وصلت إلى وجهتك.";
nextInstruction = "";
distanceToNextStep = "";
_locationUpdateTimer?.cancel(); // إيقاف التحديثات عند الوصول
Get.find<TextToSpeechController>().speakText(currentInstruction);
update();
}
}
// =======================================================================
// ٣. تحسين خوارزمية البحث ورسم المسار المقطوع
// =======================================================================
void _updateTraveledPolyline(LatLng currentPosition) {
// **التحسين:** البحث فقط في الخطوة الحالية والخطوة التالية
int searchEndIndex = (currentStepIndex + 1 < _stepPolylines.length)
? currentStepIndex + 1
: currentStepIndex;
int overallClosestIndex = -1;
double minDistance = double.infinity;
// البحث في نقاط الخطوة الحالية والتالية فقط
for (int i = currentStepIndex; i <= searchEndIndex; i++) {
for (int j = 0; j < _stepPolylines[i].length; j++) {
final distance = Geolocator.distanceBetween(
currentPosition.latitude,
currentPosition.longitude,
_stepPolylines[i][j].latitude,
_stepPolylines[i][j].longitude);
if (distance < minDistance) {
minDistance = distance;
// نحتاج إلى حساب الفهرس العام في القائمة الكاملة
overallClosestIndex = _getOverallIndex(i, j);
}
}
}
if (overallClosestIndex == -1) return;
List<LatLng> traveledPoints =
_fullRouteCoordinates.sublist(0, overallClosestIndex + 1);
traveledPoints.add(currentPosition);
List<LatLng> remainingPoints =
_fullRouteCoordinates.sublist(overallClosestIndex);
remainingPoints.insert(0, currentPosition);
polylines.removeWhere((p) => p.polylineId.value == 'traveled_route');
polylines.add(Polyline(
polylineId: const PolylineId('traveled_route'),
points: traveledPoints,
color: Colors.grey.shade600,
width: 7,
));
polylines.removeWhere((p) => p.polylineId.value == 'remaining_route');
polylines.add(Polyline(
polylineId: const PolylineId('remaining_route'),
points: remainingPoints,
color: const Color(0xFF4A80F0),
width: 7,
));
}
// دالة مساعدة لحساب الفهرس العام
int _getOverallIndex(int stepIndex, int pointInStepIndex) {
int overallIndex = 0;
for (int i = 0; i < stepIndex; i++) {
overallIndex += _stepPolylines[i].length;
}
return overallIndex + pointInStepIndex;
}
// =======================================================================
// ٤. دوال مساعدة وتجهيز البيانات
// =======================================================================
void _prepareStepData() {
_stepBounds.clear();
_stepPolylines.clear();
if (routeSteps.isEmpty) return;
for (final step in routeSteps) {
final pointsString = step['polyline']['points'];
final List<List<num>> points =
decodePolyline(pointsString).cast<List<num>>();
final polylineCoordinates = points
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
.toList();
_stepPolylines.add(polylineCoordinates); // تخزين نقاط الخطوة
_stepBounds.add(_boundsFromLatLngList(polylineCoordinates));
}
}
// ... باقي دوال الكنترولر بدون تغيير ...
// (selectDestination, onMapLongPressed, startNavigationTo, getRoute, etc.)
Future<void> selectDestination(dynamic place) async {
placeDestinationController.clear();
placesDestination = [];
final double lat = double.parse(place['latitude'].toString());
final double lng = double.parse(place['longitude'].toString());
final LatLng destination = LatLng(lat, lng);
await startNavigationTo(destination,
infoWindowTitle: place['name'] ?? 'وجهة محددة');
}
Future<void> onMapLongPressed(LatLng tappedPoint) async {
Get.dialog(
AlertDialog(
title: const Text('بدء الملاحة؟'),
content: const Text('هل تريد الذهاب إلى هذا الموقع المحدد؟'),
actionsAlignment: MainAxisAlignment.spaceBetween,
actions: [
TextButton(
child: const Text('إلغاء', style: TextStyle(color: Colors.grey)),
onPressed: () => Get.back(),
),
TextButton(
child: const Text('اذهب الآن'),
onPressed: () {
Get.back();
startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد');
},
),
],
),
);
}
Future<void> startNavigationTo(LatLng destination,
{String infoWindowTitle = ''}) async {
isLoading = true;
update();
try {
_finalDestination = destination;
clearRoute(isNewRoute: true);
markers.add(
Marker(
markerId: const MarkerId('destination'),
position: destination,
icon: destinationIcon,
infoWindow: InfoWindow(title: infoWindowTitle),
),
);
await getRoute(myLocation!, destination);
} catch (e) {
Get.snackbar('خطأ', 'حدث خطأ أثناء تحديد الوجهة.');
print("Error starting navigation: $e");
} finally {
isLoading = false;
update();
}
}
Future<void> getRoute(LatLng origin, LatLng destination) async {
final url =
'${AppLink.googleMapsLink}directions/json?language=ar&destination=${destination.latitude},${destination.longitude}&origin=${origin.latitude},${origin.longitude}&key=${AK.mapAPIKEY}';
var response = await CRUD().getGoogleApi(link: url, payload: {});
if (response == null || response['routes'].isEmpty) {
Get.snackbar('خطأ', 'لم يتم العثور على مسار.');
return;
}
polylines.clear();
final pointsString = response['routes'][0]['overview_polyline']['points'];
final List<List<num>> points =
decodePolyline(pointsString).cast<List<num>>();
_fullRouteCoordinates = points
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
.toList();
polylines.add(
Polyline(
polylineId: const PolylineId('remaining_route'),
points: _fullRouteCoordinates,
color: const Color(0xFF4A80F0),
width: 7,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
),
);
polylines.add(
const Polyline(
polylineId: PolylineId('traveled_route'),
points: [],
color: Colors.grey,
width: 7,
),
);
routeSteps = List<Map<String, dynamic>>.from(
response['routes'][0]['legs'][0]['steps']);
_prepareStepData();
currentStepIndex = 0;
_nextInstructionSpoken = false;
if (routeSteps.isNotEmpty) {
currentInstruction =
_parseInstruction(routeSteps[0]['html_instructions']);
nextInstruction = (routeSteps.length > 1)
? _parseInstruction(routeSteps[1]['html_instructions'])
: "الوجهة النهائية";
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
_adjustUpdateInterval(); // تحديد سرعة التحديث لأول مرة
final boundsData = response['routes'][0]['bounds'];
mapController?.animateCamera(CameraUpdate.newLatLngBounds(
LatLngBounds(
northeast: LatLng(
boundsData['northeast']['lat'], boundsData['northeast']['lng']),
southwest: LatLng(
boundsData['southwest']['lat'], boundsData['southwest']['lng']),
),
100.0,
));
}
Future<void> recalculateRoute() async {
if (myLocation == null || _finalDestination == null || isLoading) return;
isLoading = true;
update();
Get.snackbar(
'إعادة التوجيه',
'جاري حساب مسار جديد من موقعك الحالي...',
backgroundColor: AppColor.goldenBronze,
);
await getRoute(myLocation!, _finalDestination!);
isLoading = false;
update();
}
void clearRoute({bool isNewRoute = false}) {
polylines.clear();
if (!isNewRoute) {
markers.removeWhere((m) => m.markerId.value == 'destination');
_finalDestination = null;
}
routeSteps.clear();
currentInstruction = "";
nextInstruction = "";
distanceToNextStep = "";
currentSpeed = 0.0;
_stepBounds.clear();
_fullRouteCoordinates.clear();
_stepPolylines.clear();
_nextInstructionSpoken = false;
_locationUpdateTimer?.cancel(); // إيقاف التحديثات عند إلغاء المسار
update();
}
LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
assert(list.isNotEmpty);
double? x0, x1, y0, y1;
for (LatLng latLng in list) {
if (x0 == null) {
x0 = x1 = latLng.latitude;
y0 = y1 = latLng.longitude;
} else {
if (latLng.latitude > x1!) x1 = latLng.latitude;
if (latLng.latitude < x0) x0 = latLng.latitude;
if (latLng.longitude > y1!) y1 = latLng.longitude;
if (latLng.longitude < y0!) y0 = latLng.longitude;
}
}
return LatLngBounds(
northeast: LatLng(x1!, y1!), southwest: LatLng(x0!, y0!));
}
Future<void> _loadCustomIcons() async {
carIcon = await BitmapDescriptor.fromAssetImage(
const ImageConfiguration(size: Size(40, 40)), 'assets/images/car.png');
destinationIcon = await BitmapDescriptor.fromAssetImage(
const ImageConfiguration(size: Size(25, 25)), 'assets/images/b.png');
}
String _parseInstruction(String html) =>
html.replaceAll(RegExp(r'<[^>]*>'), ' ');
Future<void> getPlaces() async {
if (placeDestinationController.text.trim().isEmpty) {
placesDestination = [];
update();
return;
}
if (myLocation == null) {
Get.snackbar('انتظر', 'جاري تحديد موقعك الحالي...');
return;
}
final query = placeDestinationController.text.trim();
final lat = myLocation!.latitude;
final lng = myLocation!.longitude;
const double range = 2.2;
final lat_min = lat - range,
lat_max = lat + range,
lng_min = lng - range,
lng_max = lng + range;
try {
final response = await CRUD().post(
link: AppLink.getPlacesSyria,
payload: {
'query': query,
'lat_min': lat_min.toString(),
'lat_max': lat_max.toString(),
'lng_min': lng_min.toString(),
'lng_max': lng_max.toString(),
},
);
if (response != 'failure') {
placesDestination = response['message'] ?? [];
} else {
placesDestination = [];
}
} catch (e) {
print('Exception in getPlaces: $e');
} finally {
update();
}
}
void onSearchChanged(String query) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 700), () => getPlaces());
}
}

View File

@@ -0,0 +1,296 @@
// lib/views/navigation_view.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'dart:ui';
import 'navigation_controller.dart'; // For BackdropFilter
// استخدام نفس مسار الاستيراد الذي قدمته
class NavigationView extends StatelessWidget {
const NavigationView({super.key});
@override
Widget build(BuildContext context) {
final NavigationController controller = Get.put(NavigationController());
return Scaffold(
body: GetBuilder<NavigationController>(
builder: (_) => Stack(
children: [
// --- الخريطة ---
GoogleMap(
onMapCreated: controller.onMapCreated,
// --- السطر المضاف والمهم هنا ---
onLongPress: controller.onMapLongPressed,
initialCameraPosition: CameraPosition(
target: controller.myLocation ??
const LatLng(33.5138, 36.2765), // Default to Damascus
zoom: 16.0,
),
markers: controller.markers,
polylines: controller.polylines,
myLocationEnabled: false,
myLocationButtonEnabled: false,
compassEnabled: false,
zoomControlsEnabled: false,
// تعديل الـ padding لإعطاء مساحة للعناصر العلوية والسفلية
padding: EdgeInsets.only(
bottom: controller.currentInstruction.isNotEmpty ? 130 : 0,
top: 140),
),
// --- واجهة البحث ونتائجه ---
_buildSearchUI(controller),
// --- إرشادات الملاحة المطورة ---
if (controller.currentInstruction.isNotEmpty)
_buildNavigationInstruction(controller),
// --- أزرار التحكم على الخريطة ---
_buildMapControls(controller),
// --- مؤشر التحميل ---
if (controller.isLoading)
Container(
color: Colors.black.withOpacity(0.5),
child: const Center(
child: CircularProgressIndicator(color: Colors.white)),
),
],
),
),
);
}
// --- ويدجت خاصة بواجهة البحث ---
Widget _buildSearchUI(NavigationController controller) {
return Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: TextField(
controller: controller.placeDestinationController,
onChanged: (val) {
controller.onSearchChanged(val);
},
decoration: InputDecoration(
hintText: 'إلى أين تريد الذهاب؟',
prefixIcon: const Icon(Icons.search, color: Colors.grey),
suffixIcon: controller
.placeDestinationController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, color: Colors.grey),
onPressed: () {
controller.placeDestinationController.clear();
controller.placesDestination = [];
controller.update();
},
)
: (controller.polylines.isNotEmpty
? IconButton(
icon:
const Icon(Icons.close, color: Colors.red),
tooltip: 'إلغاء المسار',
onPressed: () => controller.clearRoute(),
)
: null),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 15),
),
),
),
const SizedBox(height: 8),
if (controller.placesDestination.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
constraints: const BoxConstraints(maxHeight: 220),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.85),
borderRadius: BorderRadius.circular(15.0),
),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: controller.placesDestination.length,
itemBuilder: (context, index) {
final place = controller.placesDestination[index];
return ListTile(
title: Text(place['name'] ?? 'اسم غير معروف',
style: const TextStyle(
fontWeight: FontWeight.bold)),
subtitle: Text(place['address'] ?? '',
maxLines: 1, overflow: TextOverflow.ellipsis),
leading: const Icon(Icons.location_on_outlined,
color: Colors.blue),
onTap: () => controller.selectDestination(place),
);
},
),
),
),
),
],
),
),
),
);
}
// --- ويدجت خاصة بأزرار التحكم ---
Widget _buildMapControls(NavigationController controller) {
return Positioned(
bottom: controller.currentInstruction.isNotEmpty ? 150 : 20,
right: 12,
child: Column(
children: [
if (controller.polylines.isNotEmpty) ...[
FloatingActionButton(
heroTag: 'rerouteBtn',
mini: true,
backgroundColor: Colors.white,
tooltip: 'إعادة حساب المسار',
onPressed: () => controller.recalculateRoute(),
child: const Icon(Icons.sync_alt, color: Colors.blue),
),
const SizedBox(height: 10),
],
FloatingActionButton(
heroTag: 'gpsBtn',
mini: true,
backgroundColor: Colors.white,
onPressed: () {
if (controller.myLocation != null) {
controller.animateCameraToPosition(
controller.myLocation!,
bearing: controller.heading,
zoom: 18.5,
);
}
},
child: const Icon(Icons.gps_fixed, color: Colors.black54),
),
],
),
);
}
// --- ويدجت خاصة بإرشادات الطريق المطورة ---
Widget _buildNavigationInstruction(NavigationController controller) {
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade900, Colors.blue.shade600],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 15,
offset: const Offset(0, -5),
),
],
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// --- الصف العلوي: السرعة والمسافة ---
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
controller.distanceToNextStep,
style: const TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold),
),
Row(
children: [
Text(
controller.currentSpeed.toStringAsFixed(0),
style: const TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold),
),
const SizedBox(width: 4),
const Text(
"كم/س",
style: TextStyle(color: Colors.white70, fontSize: 14),
),
],
),
],
),
const Divider(color: Colors.white38, height: 20, thickness: 0.8),
// --- الصف السفلي: الإرشاد القادم ---
Row(
children: [
const Icon(Icons.navigation_rounded,
color: Colors.white, size: 32),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("الخطوة التالية",
style:
TextStyle(color: Colors.white70, fontSize: 12)),
Text(
controller.nextInstruction.isNotEmpty
? controller.nextInstruction
: controller.currentInstruction,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -9,6 +9,7 @@ class MyTranslation extends Translations {
"Cancel Trip": "إلغاء الرحلة", "Cancel Trip": "إلغاء الرحلة",
"Passenger Cancel Trip": "الراكب ألغى الرحلة", "Passenger Cancel Trip": "الراكب ألغى الرحلة",
"VIP Order": "طلب VIP", "VIP Order": "طلب VIP",
'Hi ,I Arrive your site': "مرحبًا، لقد وصلت إلى موقعك",
"The driver accepted your trip": "السائق قبل رحلتك", "The driver accepted your trip": "السائق قبل رحلتك",
"message From passenger": "رسالة من الراكب", "message From passenger": "رسالة من الراكب",
"Cancel": "إلغاء", "Cancel": "إلغاء",
@@ -34,7 +35,6 @@ class MyTranslation extends Translations {
"نأسف لإعلامك بأن سائقًا آخر قد قبل هذا الطلب.", "نأسف لإعلامك بأن سائقًا آخر قد قبل هذا الطلب.",
"Driver Applied the Ride for You": "السائق قدم الطلب لك", "Driver Applied the Ride for You": "السائق قدم الطلب لك",
"Applied": "تم التقديم", "Applied": "تم التقديم",
"Hi ,I Arrive your site": "مرحبًا، لقد وصلت إلى موقعك",
"Please go to Car Driver": "يرجى الذهاب إلى سائق السيارة", "Please go to Car Driver": "يرجى الذهاب إلى سائق السيارة",
"Ok I will go now.": "حسنًا، سأذهب الآن.", "Ok I will go now.": "حسنًا، سأذهب الآن.",
"Accepted Ride": "تم قبول الرحلة", "Accepted Ride": "تم قبول الرحلة",
@@ -146,7 +146,11 @@ Raih Gai: For same-day return trips longer than 50km.
رحّي غاي: للرحلات ذات العودة في نفس اليوم التي تزيد عن 50 كم. رحّي غاي: للرحلات ذات العودة في نفس اليوم التي تزيد عن 50 كم.
""", """,
"I will go now": "هروح دلوقتي", "I will go now": "هروح دلوقتي",
"Yes": "أيوة", "Yes": "نعم",
'Privacy Policy': "سياسة الخصوصية",
'Ride info': "معلومات الرحلة",
'you dont have accepted ride': "ليس لديك رحلة مقبولة",
'Total Points': "إجمالي النقاط",
"No,I want": "لا، أنا عاوز", "No,I want": "لا، أنا عاوز",
"Your fee is": "المبلغ بتاعك هو", "Your fee is": "المبلغ بتاعك هو",
"Do you want to pay Tips for this Driver": "Do you want to pay Tips for this Driver":
@@ -424,7 +428,25 @@ Raih Gai: For same-day return trips longer than 50km.
"color.beige": "بيج", "color.beige": "بيج",
"color.brown": "بني", "color.brown": "بني",
"color.maroon": "خمري", "color.maroon": "خمري",
'Ride History': "تاريخ الرحلات",
"color.burgundy": "برغندي", "color.burgundy": "برغندي",
'Name must be at least 2 characters':
"الاسم يجب أن يكون على الأقل 2 حرف",
'This Trip Was Cancelled': "تم إلغاء هذه الرحلة",
'Trip Details': "تفاصيل الرحلة",
'Could not load trip details.': "تعذر تحميل تفاصيل الرحلة.",
'Trip Info': "معلومات الرحلة",
'Order ID': "رقم الطلب",
'Date': "التاريخ",
'Earnings & Distance': "الأرباح والمسافة",
'Trip Timeline': "جدول الرحلة",
'Time to Passenger': "الوقت للراكب",
'Trip Started': "بدأت الرحلة",
'Trip Finished': "انتهت الرحلة",
'Passenger & Status': "الراكب والحالة",
'Status': "الحالة",
'Passenger Name': "اسم الراكب",
'National ID must be 11 digits': "الرقم الوطني يجب أن يكون 11 رقمًا",
"color.yellow": "أصفر", "color.yellow": "أصفر",
"color.orange": "برتقالي", "color.orange": "برتقالي",
"color.gold": "ذهبي", "color.gold": "ذهبي",
@@ -756,8 +778,8 @@ Raih Gai: For same-day return trips longer than 50km.
"لم نجد أي سائقين بعد. ضع في اعتبارك زيادة رسوم رحلتك لجعل عرضك أكثر جاذبية للسائقين.", "لم نجد أي سائقين بعد. ضع في اعتبارك زيادة رسوم رحلتك لجعل عرضك أكثر جاذبية للسائقين.",
"Allow Location Access": "السماح بالوصول إلى الموقع", "Allow Location Access": "السماح بالوصول إلى الموقع",
"Show My Trip Count": "عرض عدد رحلاتي", "Show My Trip Count": "عرض عدد رحلاتي",
"Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 8%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.": "Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 15%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.":
"انطلق هو التطبيق الأكثر أمانًا لمشاركة الركوب الذي يقدم العديد من الميزات لكل من السائقين والركاب. نحن نقدم أقل عمولة بنسبة 8% فقط، مما يضمن حصولك على أفضل قيمة لرحلاتك. يتضمن تطبيقنا التأمين لأفضل السائقين، الصيانة المنتظمة للسيارات مع أفضل المهندسين، والخدمات على الطريق لضمان تجربة محترمة وعالية الجودة لجميع المستخدمين.", "انطلق هو التطبيق الأكثر أمانًا لمشاركة الركوب الذي يقدم العديد من الميزات لكل من السائقين والركاب. نحن نقدم أقل عمولة بنسبة 15% فقط، مما يضمن حصولك على أفضل قيمة لرحلاتك. يتضمن تطبيقنا التأمين لأفضل السائقين، الصيانة المنتظمة للسيارات مع أفضل المهندسين، والخدمات على الطريق لضمان تجربة محترمة وعالية الجودة لجميع المستخدمين.",
"You can contact us during working hours from 12:00 - 19:00.": "You can contact us during working hours from 12:00 - 19:00.":
"يمكنك الاتصال بنا خلال ساعات العمل من 12:00 - 7:00.", "يمكنك الاتصال بنا خلال ساعات العمل من 12:00 - 7:00.",
"Show maintenance center near my location": "Show maintenance center near my location":
@@ -1550,17 +1572,40 @@ Raih Gai: For same-day return trips longer than 50km.
"otp verification failed": "رمز التحقق غير صحيح.", "otp verification failed": "رمز التحقق غير صحيح.",
"registration failed": "فشلت عملية التسجيل.", "registration failed": "فشلت عملية التسجيل.",
"welcome user": "أهلاً بك، @firstName!", "welcome user": "أهلاً بك، @firstName!",
"Driver Wallet": "محفظة السائق", 'Balance': 'الرصيد',
"Today's Promo": "عرض اليوم",
'Credit': 'رصيد', 'Debit': 'خصم',
'Transactions this week': 'المعاملات هذا الأسبوع',
'Weekly Summary': 'ملخص أسبوعي',
'Total Weekly Earnings': 'إجمالي الأرباح الأسبوعية',
'No transactions this week': 'لا توجد معاملات هذا الأسبوع',
"Driver Balance": "رصيد السائق",
"The 30000 points equal 30000 S.P for you \nSo go and gain your money": "The 30000 points equal 30000 S.P for you \nSo go and gain your money":
"الـ 30000 نقطة تساوي 30000 ل.س لك \nلذا اذهب واكسب أموالك", "الـ 30000 نقطة تساوي 30000 ل.س لك \nلذا اذهب واكسب أموالك",
"OK": "موافق", "OK": "موافق",
"Your Application is Under Review": "طلبك قيد المراجعة",
"We have received your application to join us as a driver. Our team is currently reviewing it. Thank you for your patience.":
"لقد استلمنا طلبك للانضمام إلينا كسائق. يقوم فريقنا حاليًا بمراجعته. شكرًا لك على صبرك.",
"You Will Be Notified": "سيتم إشعارك قريباً",
"We will send you a notification as soon as your account is approved. You can safely close this page, and we'll let you know when the review is complete.":
"سنرسل لك إشعاراً فور الموافقة على حسابك. يمكنك إغلاق هذه الصفحة بأمان، وسنعلمك عند اكتمال المراجعة.",
"Refresh Status": "تحديث الحالة",
"Checking for updates...": "جاري التحقق من التحديثات...",
"Total Points is": "إجمالي النقاط هو", "Total Points is": "إجمالي النقاط هو",
"Charge your Account": "اشحن حسابك", "Charge your Account": "اشحن حسابك",
'''Types of Trips in Intaleq:
- Comfort: For cars newer than 2017 with air conditioning.
- Lady: For girl drivers.
- Speed: For fixed salary and endpoints.
- Mashwari: For flexible trips where passengers choose the car and driver with prior arrangements.
- Raih Gai: For same-day return trips longer than 50km.''':
"أنواع الرحلات في انطلق:\n\n- مريح: للسيارات الأحدث من 2017 مع تكييف الهواء.\n- سيدة: للسائقات الإناث.\n- سرعة: لرحلات ذات راتب ثابت ونقاط نهاية محددة.\n- مشاوير: لرحلات مرنة حيث يختار الركاب السيارة والسائق مع ترتيبات مسبقة.\n- رايح جاي: لرحلات العودة في نفس اليوم لأكثر من 50 كم.",
'L.S': 'ل.س', 'L.S': 'ل.س',
"Total Amount:": "المبلغ الإجمالي:", "Total Amount:": "المبلغ الإجمالي:",
"Intaleq Wallet": "محفظة انطلق", "Intaleq Wallet": "رصيد انطلق",
"Current Balance": "الرصيد الحالي", "Current Balance": "الرصيد الحالي",
"S.P.": "ل.س.", "SYP": "ل.س.",
"Your total balance:": "رصيدك الإجمالي:", "Your total balance:": "رصيدك الإجمالي:",
"Payment Method:": "طريقة الدفع:", "Payment Method:": "طريقة الدفع:",
"e.g., 0912345678": "مثال: 0912345678", "e.g., 0912345678": "مثال: 0912345678",
@@ -1643,23 +1688,12 @@ Raih Gai: For same-day return trips longer than 50km.
"الشريك السائق في انتظارك في الموقع المُحدَّد .", "الشريك السائق في انتظارك في الموقع المُحدَّد .",
"Pay with Your": "ادفع باستخدام", "Pay with Your": "ادفع باستخدام",
"Pay with Credit Card": "ادفع ببطاقة الائتمان", "Pay with Credit Card": "ادفع ببطاقة الائتمان",
"Payment History": "سجل الدفعات",
"Show Promos to Charge": "إظهار العروض الترويجية للشحن", "Show Promos to Charge": "إظهار العروض الترويجية للشحن",
"Point": "نقطة", "Point": "نقطة",
"Driver Wallet": "محفظة الشريك السائق",
"Total Points is": "‏رصيد التشغيل",
"Total Budget from trips is": "الميزانية الإجمالية من الرحلات هي", "Total Budget from trips is": "الميزانية الإجمالية من الرحلات هي",
"Total Amount:": "المبلغ الإجمالي:",
"Total Budget from trips by": "الميزانية الإجمالية من الرحلات حسب", "Total Budget from trips by": "الميزانية الإجمالية من الرحلات حسب",
"Credit card is": "بطاقة الائتمان", "Credit card is": "بطاقة الائتمان",
"This amount for all trip I get from Passengers":
"هذا المبلغ لجميع الرحلات التي أحصل عليها من الركاب",
"Pay from my budget": "ادفع من ميزانيتي",
// "This amount for all trip I get from Passengers and Collected For me in":
// "هذا المبلغ لجميع الرحلات التي أحصل عليها من الركاب والتي تم جمعها من أجلي في",
// "You can buy points from your budget":
// "يمكنك شراء النقاط من ميزانيتك",
// "insert amount": "أدخل المبلغ",
"You can buy Points to let you online": "You can buy Points to let you online":
"يمكنك شراء النقاط لتمكينك من الدخول عبر الإنترنت", "يمكنك شراء النقاط لتمكينك من الدخول عبر الإنترنت",
"by this list below": "من خلال هذه القائمة أدناه", "by this list below": "من خلال هذه القائمة أدناه",
@@ -2572,7 +2606,7 @@ Raih Gai: For same-day return trips longer than 50km.
"Payment History": "Historique des paiements", "Payment History": "Historique des paiements",
"Show Promos to Charge": "Afficher les promotions d'expédition", "Show Promos to Charge": "Afficher les promotions d'expédition",
"Point": "Points", "Point": "Points",
"Driver Wallet": "Portefeuille chauffeur", "Driver Balance": "Portefeuille chauffeur",
"Total Points is": "Le score total est de", "Total Points is": "Le score total est de",
"Total Budget from trips is": "Le budget total des voyages est de", "Total Budget from trips is": "Le budget total des voyages est de",
"Total Amount:": "Montant total", "Total Amount:": "Montant total",
@@ -3439,7 +3473,7 @@ Raih Gai: For same-day return trips longer than 50km.
"Payment History": "Historique des paiements", "Payment History": "Historique des paiements",
"Show Promos to Charge": "Afficher les promotions d'expédition", "Show Promos to Charge": "Afficher les promotions d'expédition",
"Point": "Points", "Point": "Points",
"Driver Wallet": "Portefeuille chauffeur", "Driver Balance": "Portefeuille chauffeur",
"Total Points is": "Le score total est de", "Total Points is": "Le score total est de",
"Total Budget from trips is": "Le budget total des voyages est de", "Total Budget from trips is": "Le budget total des voyages est de",
"Total Amount:": "Montant total", "Total Amount:": "Montant total",
@@ -4300,7 +4334,7 @@ Raih Gai: For same-day return trips longer than 50km.
"Payment History": "Historique des paiements", "Payment History": "Historique des paiements",
"Show Promos to Charge": "Afficher les promotions d'expédition", "Show Promos to Charge": "Afficher les promotions d'expédition",
"Point": "Points", "Point": "Points",
"Driver Wallet": "Portefeuille chauffeur", "Driver Balance": "Portefeuille chauffeur",
"Total Points is": "Le score total est de", "Total Points is": "Le score total est de",
"Total Budget from trips is": "Le budget total des voyages est de", "Total Budget from trips is": "Le budget total des voyages est de",
"Total Amount:": "Montant total", "Total Amount:": "Montant total",
@@ -5165,7 +5199,7 @@ Raih Gai: For same-day return trips longer than 50km.
"Payment History": "Historique des paiements", "Payment History": "Historique des paiements",
"Show Promos to Charge": "Afficher les promotions d'expédition", "Show Promos to Charge": "Afficher les promotions d'expédition",
"Point": "Points", "Point": "Points",
"Driver Wallet": "Portefeuille chauffeur", "Driver Balance": "Portefeuille chauffeur",
"Total Points is": "Le score total est de", "Total Points is": "Le score total est de",
"Total Budget from trips is": "Le budget total des voyages est de", "Total Budget from trips is": "Le budget total des voyages est de",
"Total Amount:": "Montant total", "Total Amount:": "Montant total",
@@ -6099,7 +6133,7 @@ Raih Gai: For same-day return trips longer than 50km.
"Payment History": "Historique des paiements", "Payment History": "Historique des paiements",
"Show Promos to Charge": "Afficher les promotions d'expédition", "Show Promos to Charge": "Afficher les promotions d'expédition",
"Point": "Points", "Point": "Points",
"Driver Wallet": "Portefeuille chauffeur", "Driver Balance": "Portefeuille chauffeur",
"Total Points is": "Le score total est de", "Total Points is": "Le score total est de",
"Total Budget from trips is": "Le budget total des voyages est de", "Total Budget from trips is": "Le budget total des voyages est de",
"Total Amount:": "Montant total", "Total Amount:": "Montant total",
@@ -6948,7 +6982,7 @@ Raih Gai: For same-day return trips longer than 50km.
"Payment History": "भुगतान इतिहास", "Payment History": "भुगतान इतिहास",
"Show Promos to Charge": "शिपिंग प्रमोशन दिखाएँ", "Show Promos to Charge": "शिपिंग प्रमोशन दिखाएँ",
"Point": "pt", "Point": "pt",
"Driver Wallet": "ड्राइवर पार्टनर का वॉलेट", "Driver Balance": "ड्राइवर पार्टनर का वॉलेट",
"Total Points is": "कुल स्कोर है", "Total Points is": "कुल स्कोर है",
"Total Budget from trips is": "ट्रिप का कुल बजट है", "Total Budget from trips is": "ट्रिप का कुल बजट है",
"Total Amount:": "कुल राश", "Total Amount:": "कुल राश",
@@ -7796,7 +7830,7 @@ Raih Gai: For same-day return trips longer than 50km.
"Show Promos to Charge": "نمایش تبلیغات حمل و نقل", "Show Promos to Charge": "نمایش تبلیغات حمل و نقل",
"Point": "Point":
"نقطه , خال , لکه , نقطه دار کردن , نوک , سر , نکته , ماده , اصل , موضوع , جهت , درجه , امتياز بازي , نمره درس , پوان , هدف , مسير , مرحله , قله , پايان , تيزکردن , گوشه دارکردن , نوکدار کردن , نوک گذاشتن (به) , خاطر نشان کردن , نشان دادن , متوجه ساختن , نقطه گذاري کردن , لک , لکه يا خال ميوه , ذره , لکه دار کردن , خالدار کردن", "نقطه , خال , لکه , نقطه دار کردن , نوک , سر , نکته , ماده , اصل , موضوع , جهت , درجه , امتياز بازي , نمره درس , پوان , هدف , مسير , مرحله , قله , پايان , تيزکردن , گوشه دارکردن , نوکدار کردن , نوک گذاشتن (به) , خاطر نشان کردن , نشان دادن , متوجه ساختن , نقطه گذاري کردن , لک , لکه يا خال ميوه , ذره , لکه دار کردن , خالدار کردن",
"Driver Wallet": "کیف پول راننده", "Driver Balance": "کیف پول راننده",
"Total Points is": "مجموع امتیاز است", "Total Points is": "مجموع امتیاز است",
"Total Budget from trips is": "بودجه کل انطلقها می باشد", "Total Budget from trips is": "بودجه کل انطلقها می باشد",
"Total Amount:": "مبلغ کل:", "Total Amount:": "مبلغ کل:",
@@ -8560,7 +8594,7 @@ Raih Gai: For same-day return trips longer than 50km.
"Payment History": "付款紀錄", "Payment History": "付款紀錄",
"Show Promos to Charge": "顯示運送優惠", "Show Promos to Charge": "顯示運送優惠",
"Point": "一個點", "Point": "一個點",
"Driver Wallet": "職業駕駛錢包", "Driver Balance": "職業駕駛錢包",
"Total Points is": "總分為", "Total Points is": "總分為",
"Total Budget from trips is": "行程總預算為", "Total Budget from trips is": "行程總預算為",
"Total Amount:": "總金額:", "Total Amount:": "總金額:",

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/foundation.dart'
show kIsWeb, defaultTargetPlatform, TargetPlatform;
import 'controller/functions/device_analyzer.dart'; import 'controller/functions/device_analyzer.dart';
// --- CompatibilityDetailCard Widget (Updated to use 'max_score') --- // --- CompatibilityDetailCard Widget (كما هو) ---
class CompatibilityDetailCard extends StatelessWidget { class CompatibilityDetailCard extends StatelessWidget {
final Map<String, dynamic> detail; final Map<String, dynamic> detail;
const CompatibilityDetailCard({super.key, required this.detail}); const CompatibilityDetailCard({super.key, required this.detail});
@@ -16,17 +18,14 @@ class CompatibilityDetailCard extends StatelessWidget {
IconData _getIconForLabel(String label) { IconData _getIconForLabel(String label) {
if (label.contains('رام')) return Icons.memory; if (label.contains('رام')) return Icons.memory;
if (label.contains('معالج') || label.contains('CPU')) { if (label.contains('معالج') || label.contains('CPU'))
return Icons.developer_board; return Icons.developer_board;
} if (label.contains('تخزين') || label.contains('كتابة'))
if (label.contains('تخزين') || label.contains('كتابة')) {
return Icons.sd_storage_outlined; return Icons.sd_storage_outlined;
}
if (label.contains('أندرويد')) return Icons.android; if (label.contains('أندرويد')) return Icons.android;
if (label.contains('خدمات')) return Icons.g_mobiledata; if (label.contains('خدمات')) return Icons.g_mobiledata;
if (label.contains('حساسات') || label.contains('Gyroscope')) { if (label.contains('حساسات') || label.contains('Gyroscope'))
return Icons.sensors; return Icons.sensors;
}
return Icons.smartphone; return Icons.smartphone;
} }
@@ -35,7 +34,6 @@ class CompatibilityDetailCard extends StatelessWidget {
final bool status = detail['status'] ?? false; final bool status = detail['status'] ?? false;
final String label = detail['label'] ?? ""; final String label = detail['label'] ?? "";
final int achievedScore = detail['achieved_score'] ?? 0; final int achievedScore = detail['achieved_score'] ?? 0;
// Corrected to use 'max_score' from the analyzer
final int maxScore = detail['max_score'] ?? 1; final int maxScore = detail['max_score'] ?? 1;
final Color color = _getStatusColor(status, achievedScore, maxScore); final Color color = _getStatusColor(status, achievedScore, maxScore);
final double progress = final double progress =
@@ -49,10 +47,9 @@ class CompatibilityDetailCard extends StatelessWidget {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.grey.withOpacity(0.08), color: Colors.grey.withOpacity(0.08),
blurRadius: 15, blurRadius: 15,
offset: const Offset(0, 5), offset: const Offset(0, 5))
)
], ],
), ),
child: Column( child: Column(
@@ -64,20 +61,15 @@ class CompatibilityDetailCard extends StatelessWidget {
color: Colors.grey.shade600, size: 20), color: Colors.grey.shade600, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(label,
label, style: TextStyle(
fontSize: 15,
color: Colors.grey.shade800,
fontWeight: FontWeight.w600)),
),
Text("$achievedScore/$maxScore نقطة",
style: TextStyle( style: TextStyle(
fontSize: 15, color: color, fontWeight: FontWeight.bold, fontSize: 14)),
color: Colors.grey.shade800,
fontWeight: FontWeight.w600),
),
),
// Corrected to display points out of max_score
Text(
"$achievedScore/$maxScore نقطة",
style: TextStyle(
color: color, fontWeight: FontWeight.bold, fontSize: 14),
),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -94,7 +86,7 @@ class CompatibilityDetailCard extends StatelessWidget {
} }
} }
// --- Main Page Widget --- // --- Main Page Widget (Android-only) ---
class DeviceCompatibilityPage extends StatefulWidget { class DeviceCompatibilityPage extends StatefulWidget {
const DeviceCompatibilityPage({super.key}); const DeviceCompatibilityPage({super.key});
@override @override
@@ -107,23 +99,28 @@ class _DeviceCompatibilityPageState extends State<DeviceCompatibilityPage> {
List<Map<String, dynamic>> details = []; List<Map<String, dynamic>> details = [];
bool isLoading = true; bool isLoading = true;
bool get _isAndroid =>
!kIsWeb && defaultTargetPlatform == TargetPlatform.android;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializePage(); if (_isAndroid) {
_initializePage();
} else {
// منصّة غير أندرويد: لا تعمل أي تحليلات
setState(() => isLoading = false);
}
} }
Future<void> _initializePage() async { Future<void> _initializePage() async {
// await BatteryNotifier.checkBatteryAndNotify();
final result = await DeviceAnalyzer().analyzeDevice(); final result = await DeviceAnalyzer().analyzeDevice();
if (!mounted) return;
if (mounted) { setState(() {
setState(() { score = result['score'];
score = result['score']; details = List<Map<String, dynamic>>.from(result['details']);
details = List<Map<String, dynamic>>.from(result['details']); isLoading = false;
isLoading = false; });
});
}
} }
Color _getColorForScore(int value) { Color _getColorForScore(int value) {
@@ -141,6 +138,41 @@ class _DeviceCompatibilityPageState extends State<DeviceCompatibilityPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// حظر الصفحة على غير أندرويد
if (!_isAndroid) {
return Scaffold(
backgroundColor: const Color(0xFFF7F8FC),
appBar: AppBar(
title: const Text("توافق الجهاز",
style: TextStyle(
color: Colors.black87, fontWeight: FontWeight.bold)),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.black87),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.phone_iphone, size: 56, color: Colors.grey),
const SizedBox(height: 12),
const Text("هذه الصفحة متاحة لأجهزة أندرويد فقط",
style: TextStyle(fontSize: 16)),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => Get.back(),
child: const Text("رجوع"),
),
],
),
),
),
);
}
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF7F8FC), backgroundColor: const Color(0xFFF7F8FC),
appBar: AppBar( appBar: AppBar(
@@ -188,57 +220,48 @@ class _DeviceCompatibilityPageState extends State<DeviceCompatibilityPage> {
); );
} }
/// ## Corrected Score Header Widget /// الهيدر
/// This widget now uses a `Stack` to correctly place the text over the `PieChart`.
Widget _buildScoreHeader() { Widget _buildScoreHeader() {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
height: 220, // Give the container a fixed height height: 220,
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
// Layer 1: The Pie Chart
PieChart( PieChart(
PieChartData( PieChartData(
sectionsSpace: 4, sectionsSpace: 4,
// This creates the "hole" in the middle.
centerSpaceRadius: 80, centerSpaceRadius: 80,
startDegreeOffset: -90, startDegreeOffset: -90,
sections: [ sections: [
PieChartSectionData( PieChartSectionData(
color: _getColorForScore(score), color: _getColorForScore(score),
value: score.toDouble(), value: score.toDouble(),
title: '', title: '',
radius: 25, radius: 25),
),
PieChartSectionData( PieChartSectionData(
color: Colors.grey.shade200, color: Colors.grey.shade200,
value: (100 - score).toDouble().clamp(0, 100), value: (100 - score).toDouble().clamp(0, 100),
title: '', title: '',
radius: 25, radius: 25),
),
], ],
), ),
), ),
// Layer 2: The text and message, centered on top of the chart
Column( Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text("$score%",
"$score%", style: TextStyle(
style: TextStyle( fontSize: 52,
fontSize: 52, fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, color: _getColorForScore(score))),
color: _getColorForScore(score)),
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(_getScoreMessage(score),
_getScoreMessage(score), textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.grey.shade700, color: Colors.grey.shade700,
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500), fontWeight: FontWeight.w500)),
),
], ],
), ),
], ],

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:intl/date_symbol_data_local.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:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
@@ -17,7 +18,9 @@ import 'constant/api_key.dart';
import 'constant/info.dart'; import 'constant/info.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/battery_status.dart'; import 'controller/functions/battery_status.dart';
import 'controller/functions/crud.dart';
import 'controller/functions/encrypt_decrypt.dart'; import 'controller/functions/encrypt_decrypt.dart';
import 'controller/functions/secure_storage.dart'; import 'controller/functions/secure_storage.dart';
import 'controller/local/local_controller.dart'; import 'controller/local/local_controller.dart';
@@ -134,13 +137,22 @@ void main() async {
await WakelockPlus.enable(); await WakelockPlus.enable();
await GetStorage.init(); await GetStorage.init();
await initializeDateFormatting();
Stripe.publishableKey = AK.publishableKeyStripe; Stripe.publishableKey = AK.publishableKeyStripe;
SystemChrome.setPreferredOrientations([ SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp, DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown, DeviceOrientation.portraitDown,
]); ]);
runZonedGuarded<Future<void>>(() async {
runApp(const MyApp()); // ... الكود الحالي الموجود في دالة main ...
runApp(const MyApp());
}, (error, stack) {
// أي خطأ غير متوقع في التطبيق سيتم التقاطه هنا CRUD.
print("Caught Dart error: $error");
print(stack);
// أرسل الخطأ إلى السيرفر
CRUD.addError(error.toString(), stack.toString(), 'main');
});
} }
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {

View File

@@ -240,7 +240,7 @@ class RatePassenger extends StatelessWidget {
RatingBar.builder( RatingBar.builder(
initialRating: 0, initialRating: 0,
itemCount: 5, itemCount: 5,
itemSize: 50, itemSize: 40,
itemPadding: const EdgeInsets.symmetric(horizontal: 4), itemPadding: const EdgeInsets.symmetric(horizontal: 4),
itemBuilder: (context, index) { itemBuilder: (context, index) {
switch (index) { switch (index) {

View File

@@ -35,7 +35,7 @@ class ContactUsPage extends StatelessWidget {
IconButton( IconButton(
onPressed: () async { onPressed: () async {
Get.put(TextToSpeechController()).speakText( Get.put(TextToSpeechController()).speakText(
'Tripz is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 8%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.' 'Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 15%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.'
.tr); .tr);
}, },
icon: const Icon(Icons.headphones), icon: const Icon(Icons.headphones),
@@ -43,7 +43,7 @@ class ContactUsPage extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'Tripz is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 8%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.' 'Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 15%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.'
.tr, .tr,
style: AppStyle.title, style: AppStyle.title,
textAlign: TextAlign.center, textAlign: TextAlign.center,

View File

@@ -8,10 +8,12 @@ import 'package:flutter_font_icons/flutter_font_icons.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.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';
import 'package:url_launcher/url_launcher.dart';
import '../../../constant/box_name.dart'; import '../../../constant/box_name.dart';
import '../../../constant/colors.dart'; import '../../../constant/colors.dart';
import '../../../constant/info.dart'; import '../../../constant/info.dart';
import '../../../constant/links.dart';
import '../../../constant/style.dart'; 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';
@@ -356,7 +358,12 @@ class LoginCaptin extends StatelessWidget {
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
color: AppColor.blueColor, color: AppColor.blueColor,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
recognizer: TapGestureRecognizer()..onTap = () {}), recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(
Uri.parse('${AppLink.server}/privacy_policy.php'),
mode: LaunchMode.externalApplication);
}),
TextSpan(text: " and acknowledge our Privacy Policy.".tr), TextSpan(text: " and acknowledge our Privacy Policy.".tr),
], ],
), ),
@@ -369,7 +376,9 @@ class LoginCaptin extends StatelessWidget {
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: HtmlWidget(AppInformation.privacyPolicyArabic), child: HtmlWidget(box.read(BoxName.lang).toString() == 'ar'
? AppInformation.privacyPolicyArabic
: AppInformation.privacyPolicy),
), ),
), ),
), ),

View File

@@ -0,0 +1,167 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:math' as math;
class DriverVerificationScreen extends StatefulWidget {
const DriverVerificationScreen({super.key});
@override
State<DriverVerificationScreen> createState() =>
_DriverVerificationScreenState();
}
class _DriverVerificationScreenState extends State<DriverVerificationScreen>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Animated Icon
AnimatedBuilder(
animation: _controller,
builder: (_, child) {
return Transform.rotate(
angle: _controller.value * 2 * math.pi,
child: child,
);
},
child: Icon(
Icons.sync,
size: 80,
color: theme.primaryColor.withOpacity(0.8),
),
),
const SizedBox(height: 32),
// Title
Text(
"Your Application is Under Review".tr,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: theme.textTheme.titleLarge?.color,
),
),
const SizedBox(height: 16),
// Main Message
Text(
"We have received your application to join us as a driver. Our team is currently reviewing it. Thank you for your patience."
.tr,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
color: Colors.black54,
height: 1.5,
),
),
const SizedBox(height: 32),
// Notification Card
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.primaryColor.withOpacity(0.3),
width: 1,
),
),
child: Row(
children: [
Icon(Icons.notifications_active_outlined,
color: theme.primaryColor, size: 30),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"You Will Be Notified".tr,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: theme.primaryColor,
),
),
const SizedBox(height: 4),
Text(
"We will send you a notification as soon as your account is approved. You can safely close this page, and we'll let you know when the review is complete."
.tr,
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
height: 1.4,
),
),
],
),
),
],
),
),
const SizedBox(height: 40),
// Refresh Button
// ElevatedButton.icon(
// onPressed: () {
// // TODO: Add logic to check status from your API
// Get.snackbar(
// "Status", // This can also be a key if you want
// "Checking for updates...".tr,
// snackPosition: SnackPosition.BOTTOM,
// );
// },
// icon: const Icon(Icons.refresh, color: Colors.white),
// label: Text(
// "Refresh Status".tr,
// style: const TextStyle(fontSize: 16, color: Colors.white),
// ),
// style: ElevatedButton.styleFrom(
// backgroundColor: theme.primaryColor,
// padding: const EdgeInsets.symmetric(
// horizontal: 40, vertical: 15),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(30.0),
// ),
// elevation: 3,
// ),
// ),
],
),
),
),
),
);
}
}

View File

@@ -72,29 +72,53 @@ class RegistrationView extends StatelessWidget {
TextFormField( TextFormField(
controller: c.firstNameController, controller: c.firstNameController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'First Name'.tr, labelText: 'First Name'.tr,
border: const OutlineInputBorder()), border: const OutlineInputBorder(),
validator: (v) => ),
(v?.isEmpty ?? true) ? 'Required field'.tr : null, validator: (v) {
if (v == null || v.isEmpty) {
return 'Required field'.tr;
}
if (v.length < 2) {
return 'Name must be at least 2 characters'.tr;
}
return null;
},
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: c.lastNameController, controller: c.lastNameController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Last Name'.tr, labelText: 'Last Name'.tr,
border: const OutlineInputBorder()), border: const OutlineInputBorder(),
validator: (v) => ),
(v?.isEmpty ?? true) ? 'Required field'.tr : null, validator: (v) {
if (v == null || v.isEmpty) {
return 'Required field'.tr;
}
if (v.length < 2) {
return 'Name must be at least 2 characters'.tr;
}
return null;
},
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: c.nationalIdController, controller: c.nationalIdController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'National ID Number'.tr, labelText: 'National ID Number'.tr,
border: const OutlineInputBorder()), border: const OutlineInputBorder(),
),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
validator: (v) => validator: (v) {
(v?.isEmpty ?? true) ? 'Required field'.tr : null, if (v == null || v.isEmpty) {
return 'Required field'.tr;
}
if (v.length != 11) {
return 'National ID must be 11 digits'.tr;
}
return null;
},
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
@@ -296,6 +320,15 @@ class RegistrationView extends StatelessWidget {
); );
} }
Widget signedImageWithAuth(String fileUrl, String bearerToken) {
return Image.network(
fileUrl,
headers: {'Authorization': 'Bearer $bearerToken'},
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Text('Image expired or unauthorized'),
);
}
Widget _buildImagePickerBox(String title, File? img, VoidCallback onTap) { Widget _buildImagePickerBox(String title, File? img, VoidCallback onTap) {
return Card( return Card(
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),

View File

@@ -26,7 +26,7 @@ class AboutPage extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Text( child: Text(
'SEFER LLC\n${box.read(BoxName.countryCode).toString().tr}', 'Intaleq LLC\n${'Syria'.tr}',
style: AppStyle.headTitle2, style: AppStyle.headTitle2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -34,7 +34,7 @@ class AboutPage extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text( child: Text(
'SEFER is a ride-sharing app designed with your safety and affordability in mind. We connect you with reliable drivers in your area, ensuring a convenient and stress-free travel experience.\n\nHere are some of the key features that set us apart:' 'Intaleq is a ride-sharing app designed with your safety and affordability in mind. We connect you with reliable drivers in your area, ensuring a convenient and stress-free travel experience.\n\nHere are some of the key features that set us apart:'
.tr, .tr,
style: AppStyle.title, style: AppStyle.title,
textAlign: TextAlign.center, textAlign: TextAlign.center,

View File

@@ -10,6 +10,7 @@ import '../../Rate/rate_passenger.dart';
import '../../widgets/my_textField.dart'; import '../../widgets/my_textField.dart';
import 'mapDriverWidgets/driver_end_ride_bar.dart'; import 'mapDriverWidgets/driver_end_ride_bar.dart';
import 'mapDriverWidgets/google_driver_map_page.dart'; import 'mapDriverWidgets/google_driver_map_page.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';
@@ -44,12 +45,13 @@ class PassengerLocationMapPage extends StatelessWidget {
// 2. شريط تعليمات الطريق في الأعلى // 2. شريط تعليمات الطريق في الأعلى
const InstructionsOfRoads(), const InstructionsOfRoads(),
// 3. زر إلغاء الرحلة في الأعلى يسارًا
CancelWidget(mapDriverController: mapDriverController),
// 4. نافذة معلومات الراكب في الأسفل (تظهر قبل بدء الرحلة) // 4. نافذة معلومات الراكب في الأسفل (تظهر قبل بدء الرحلة)
const PassengerInfoWindow(),
const PassengerInfoWindow(),
// 3. زر إلغاء الرحلة في الأعلى يسارًا
CancelWidget(mapDriverController: mapDriverController),
// Changed: تم تعديل تصميم زر الإلغاء ليكون أيقونة بسيطة في الأعلى
// 5. شريط معلومات وإنهاء الرحلة (يظهر بعد بدء الرحلة) // 5. شريط معلومات وإنهاء الرحلة (يظهر بعد بدء الرحلة)
driverEndRideBar(), driverEndRideBar(),
@@ -58,7 +60,7 @@ class PassengerLocationMapPage extends StatelessWidget {
// 7. دائرة عرض السرعة // 7. دائرة عرض السرعة
speedCircle(), speedCircle(),
GoogleMapApp(),
// 8. نافذة عرض السعر النهائي (تظهر بعد انتهاء الرحلة) // 8. نافذة عرض السعر النهائي (تظهر بعد انتهاء الرحلة)
const PricesWindow(), const PricesWindow(),
], ],
@@ -131,7 +133,7 @@ class CancelWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Positioned( return Positioned(
top: 10, top: 70,
left: 10, left: 10,
child: GetBuilder<MapDriverController>( child: GetBuilder<MapDriverController>(
builder: (controller) { builder: (controller) {

View File

@@ -1,12 +1,9 @@
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:flutter_staggered_animations/flutter_staggered_animations.dart';
import '../../../../controller/auth/captin/history_captain.dart'; import '../../../../controller/auth/captin/history_captain.dart';
import 'package:flutter/cupertino.dart'; import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'package:get/get.dart';
import '../../../../controller/functions/encrypt_decrypt.dart';
class HistoryCaptain extends StatelessWidget { class HistoryCaptain extends StatelessWidget {
const HistoryCaptain({super.key}); const HistoryCaptain({super.key});
@@ -14,136 +11,221 @@ class HistoryCaptain extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Get.put(HistoryCaptainController()); Get.put(HistoryCaptainController());
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar( return Scaffold(
middle: Text('History Page'.tr), backgroundColor: Colors.grey[100], // A softer background color
leading: CupertinoNavigationBarBackButton( appBar: AppBar(
onPressed: () => Get.back(), title: Text('Ride History'.tr),
), backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 1,
), ),
child: SafeArea( body: GetBuilder<HistoryCaptainController>(
child: GetBuilder<HistoryCaptainController>( builder: (controller) {
builder: (historyCaptainController) => historyCaptainController if (controller.isloading) {
.isloading return const Center(child: CircularProgressIndicator());
? const Center(child: CupertinoActivityIndicator()) }
: historyCaptainController.historyData['message'].length < 1
? Center( if (controller.historyData['message'].isEmpty) {
child: Text( return Center(
'No ride Yet.'.tr, child: Column(
style: CupertinoTheme.of(context) mainAxisAlignment: MainAxisAlignment.center,
.textTheme children: [
.navTitleTextStyle, Icon(Icons.history_toggle_off,
size: 80, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'No Rides Yet'.tr,
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(color: Colors.grey[600]),
),
],
),
);
}
// 动画: Wrap ListView with AnimationLimiter for staggered animations
return AnimationLimiter(
child: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: controller.historyData['message'].length,
itemBuilder: (BuildContext context, int index) {
var trip = controller.historyData['message'][index];
// 动画: Apply animation to each list item
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: _AnimatedHistoryCard(
trip: trip,
onTap: () {
// Your original logic is preserved here
if (trip['status'] != 'Cancel') {
controller.getHistoryDetails(trip['order_id']);
} else {
MyDialog().getDialog(
'This Trip Was Cancelled'.tr,
'This Trip Was Cancelled'.tr,
() => Get.back(),
);
}
},
), ),
)
: ListView.builder(
itemCount: historyCaptainController
.historyData['message'].length,
itemBuilder: (BuildContext context, int index) {
var list = historyCaptainController
.historyData['message'][index];
return Padding(
padding: const EdgeInsets.all(4.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: CupertinoColors.systemGrey, width: 1),
borderRadius:
const BorderRadius.all(Radius.circular(8.0)),
),
child: CupertinoButton(
onPressed: () {
if (list['status'] != 'Cancel') {
historyCaptainController
.getHistoryDetails(list['order_id']);
} else {
MyDialog().getDialog(
'This Trip Cancelled'.tr,
'This Trip Cancelled'.tr,
() => Get.back(),
);
}
},
child: Container(
margin: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'OrderId'.tr,
style: CupertinoTheme.of(context)
.textTheme
.navTitleTextStyle,
),
Text(
EncryptionHelper.instance
.decryptData(list['order_id']),
style: CupertinoTheme.of(context)
.textTheme
.textStyle,
),
],
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'created time'.tr,
style: CupertinoTheme.of(context)
.textTheme
.navTitleTextStyle,
),
Text(
list['created_at'],
style: CupertinoTheme.of(context)
.textTheme
.textStyle,
),
],
),
Text(
list['status'],
style: EncryptionHelper.instance
.decryptData(
list['status']) ==
'Apply'
? CupertinoTheme.of(context)
.textTheme
.navTitleTextStyle
.copyWith(
color: CupertinoColors
.systemGreen)
: EncryptionHelper.instance.decryptData(
list['status']) ==
'Refused'
? CupertinoTheme.of(context)
.textTheme
.navTitleTextStyle
.copyWith(
color: CupertinoColors
.systemRed)
: CupertinoTheme.of(context)
.textTheme
.navTitleTextStyle
.copyWith(
color: CupertinoColors
.systemYellow),
),
],
),
),
),
),
);
},
), ),
),
);
},
),
);
},
),
);
}
}
// 动画: A new stateful widget to handle the tap animation
class _AnimatedHistoryCard extends StatefulWidget {
final Map<String, dynamic> trip;
final VoidCallback onTap;
const _AnimatedHistoryCard({required this.trip, required this.onTap});
@override
__AnimatedHistoryCardState createState() => __AnimatedHistoryCardState();
}
class __AnimatedHistoryCardState extends State<_AnimatedHistoryCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 150),
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails details) {
_controller.forward();
}
void _onTapUp(TapUpDetails details) {
_controller.reverse();
widget.onTap();
}
void _onTapCancel() {
_controller.reverse();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
child: ScaleTransition(
scale: _scaleAnimation,
child: Card(
elevation: 4,
shadowColor: Colors.black.withOpacity(0.1),
margin: const EdgeInsets.only(bottom: 16.0),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(Icons.receipt_long,
color: Theme.of(context).primaryColor, size: 40),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${'OrderId'.tr}: ${widget.trip['order_id']}',
style:
Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
widget.trip['created_at'],
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
const SizedBox(width: 16),
_buildStatusChip(widget.trip['status']),
],
),
),
), ),
), ),
); );
} }
} }
// 🎨 A separate function for the status chip, slightly restyled for Material
Widget _buildStatusChip(String status) {
Color chipColor;
Color textColor;
String statusText = status;
IconData iconData;
switch (status) {
case 'Apply':
chipColor = Colors.green.shade50;
textColor = Colors.green.shade800;
iconData = Icons.check_circle;
break;
case 'Refused':
chipColor = Colors.red.shade50;
textColor = Colors.red.shade800;
iconData = Icons.cancel;
break;
case 'Cancel':
chipColor = Colors.orange.shade50;
textColor = Colors.orange.shade800;
iconData = Icons.info;
statusText = 'Cancelled';
break;
default:
chipColor = Colors.grey.shade200;
textColor = Colors.grey.shade800;
iconData = Icons.hourglass_empty;
}
return Chip(
avatar: Icon(iconData, color: textColor, size: 16),
label: Text(
statusText.tr,
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
),
backgroundColor: chipColor,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
side: BorderSide.none,
);
}

View File

@@ -1,252 +1,369 @@
import 'package:sefer_driver/controller/functions/location_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:sefer_driver/controller/auth/captin/history_captain.dart'; import 'package:sefer_driver/controller/auth/captin/history_captain.dart';
import 'package:sefer_driver/controller/functions/launch.dart'; import 'package:sefer_driver/controller/functions/launch.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:flutter/cupertino.dart'; class HistoryDetailsPage extends StatefulWidget {
const HistoryDetailsPage({super.key});
import '../../../../controller/functions/encrypt_decrypt.dart'; @override
State<HistoryDetailsPage> createState() => _HistoryDetailsPageState();
}
class HistoryDetailsPage extends StatelessWidget { class _HistoryDetailsPageState extends State<HistoryDetailsPage> {
HistoryDetailsPage({super.key}); // Get the controller instance
HistoryCaptainController historyCaptainController = final HistoryCaptainController controller =
Get.put(HistoryCaptainController()); Get.find<HistoryCaptainController>();
// Helper method to safely parse LatLng from a string 'lat,lng'
LatLng? _parseLatLng(String? latLngString) {
if (latLngString == null) return null;
final parts = latLngString.split(',');
if (parts.length != 2) return null;
final lat = double.tryParse(parts[0]);
final lng = double.tryParse(parts[1]);
if (lat == null || lng == null) return null;
return LatLng(lat, lng);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CupertinoPageScaffold( return Scaffold(
navigationBar: CupertinoNavigationBar( backgroundColor: Colors.grey[50],
middle: Text('Trip Detail'.tr), appBar: AppBar(
leading: CupertinoButton( title: Text('Trip Details'.tr),
padding: EdgeInsets.zero, backgroundColor: Colors.white,
child: const Icon(CupertinoIcons.back), elevation: 1,
onPressed: () => Navigator.pop(context),
),
), ),
child: GetBuilder<HistoryCaptainController>( body: GetBuilder<HistoryCaptainController>(
builder: (historyCaptainController) { builder: (controller) {
var res = historyCaptainController.historyDetailsData['data']; if (controller.isloading) {
return historyCaptainController.isloading return const Center(child: CircularProgressIndicator());
? const Center( }
child: CupertinoActivityIndicator(),
) final res = controller.historyDetailsData['data'];
: CupertinoScrollbar( if (res == null) {
child: SingleChildScrollView( return Center(child: Text('Could not load trip details.'.tr));
child: Padding( }
padding: const EdgeInsets.all(8.0),
child: Column( final startLocation = _parseLatLng(res['start_location']);
mainAxisAlignment: MainAxisAlignment.start, final endLocation = _parseLatLng(res['end_location']);
crossAxisAlignment: CrossAxisAlignment.center,
children: [ // Create markers for the map
const SizedBox( final Set<Marker> markers = {};
height: 20, if (startLocation != null) {
), markers.add(Marker(
CupertinoButton( markerId: const MarkerId('start'),
onPressed: () { position: startLocation,
String mapUrl = infoWindow: InfoWindow(title: 'Start'.tr)));
'https://www.google.com/maps/dir/${EncryptionHelper.instance.decryptData(res['start_location'])}/${EncryptionHelper.instance.decryptData(res['end_location'])}/'; }
showInBrowser(mapUrl); if (endLocation != null) {
}, markers.add(Marker(
child: Container( markerId: const MarkerId('end'),
width: MediaQuery.of(context).size.width * 0.9, position: endLocation,
padding: const EdgeInsets.all(12.0), infoWindow: InfoWindow(title: 'End'.tr)));
decoration: BoxDecoration( }
borderRadius: BorderRadius.circular(12.0),
border: Border.all( return AnimationLimiter(
color: CupertinoColors.activeBlue, child: ListView(
width: 2), padding: const EdgeInsets.all(16.0),
), children: AnimationConfiguration.toStaggeredList(
child: Column( duration: const Duration(milliseconds: 375),
children: [ childAnimationBuilder: (widget) => SlideAnimation(
const SizedBox( verticalOffset: 50.0,
height: 20, child: FadeInAnimation(child: widget),
), ),
SizedBox( children: [
height: MediaQuery.of(context).size.height * // --- Map Card ---
0.3, _buildMapCard(context, startLocation, endLocation, markers),
child: GoogleMap( const SizedBox(height: 16),
initialCameraPosition: CameraPosition(
target: Get.find<LocationController>() // --- Trip Info Card ---
.myLocation, _DetailCard(
tilt: 80, icon: Icons.receipt_long,
zoom: 13, title: 'Trip Info'.tr,
), child: Column(
zoomControlsEnabled: true, children: [
polylines: { _InfoTile(
Polyline( label: 'Order ID'.tr,
polylineId: const PolylineId('route'), value: res['id']?.toString() ?? 'N/A'),
points: [ _InfoTile(
LatLng( label: 'Date'.tr,
double.parse(EncryptionHelper value: res['date']?.toString() ?? 'N/A'),
.instance ],
.decryptData(
res['start_location'])
.toString()
.split(',')[0]),
double.parse(EncryptionHelper
.instance
.decryptData(
res['start_location'])
.toString()
.split(',')[1]),
),
LatLng(
double.parse(EncryptionHelper
.instance
.decryptData(
res['end_location'])
.toString()
.split(',')[0]),
double.parse(EncryptionHelper
.instance
.decryptData(
res['end_location'])
.toString()
.split(',')[1]),
)
],
color: CupertinoColors.activeGreen,
width: 5,
),
},
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'${'Order ID'.tr} ${EncryptionHelper.instance.decryptData(res['id'])}',
style: CupertinoTheme.of(context)
.textTheme
.navActionTextStyle,
),
Text(
res['date'].toString(),
style: CupertinoTheme.of(context)
.textTheme
.navActionTextStyle,
),
],
),
],
),
),
),
const SizedBox(height: 20),
Container(
width: MediaQuery.of(context).size.width * 0.9,
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
border: Border.all(
color: CupertinoColors.activeGreen, width: 2),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${'Price is'.tr} ${EncryptionHelper.instance.decryptData(res['price_for_driver'])}',
style: CupertinoTheme.of(context)
.textTheme
.textStyle,
),
Text(
'${'Distance is'.tr} ${EncryptionHelper.instance.decryptData(res['distance'])} KM',
style: CupertinoTheme.of(context)
.textTheme
.textStyle,
),
],
),
),
const SizedBox(height: 20),
Text(
'Times of Trip'.tr,
style: CupertinoTheme.of(context)
.textTheme
.navTitleTextStyle,
),
const SizedBox(height: 10),
Container(
width: MediaQuery.of(context).size.width * 0.9,
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
border: Border.all(
color: CupertinoColors.destructiveRed,
width: 2),
),
child: Column(
children: [
Text(
'${'Time to Passenger is'.tr} ${res['DriverIsGoingToPassenger']}',
style: CupertinoTheme.of(context)
.textTheme
.textStyle,
),
Text(
'${'TimeStart is'.tr} ${res['rideTimeStart']}',
style: CupertinoTheme.of(context)
.textTheme
.textStyle,
),
Text(
'${'Time Finish is'.tr} ${res['rideTimeFinish']}',
style: CupertinoTheme.of(context)
.textTheme
.textStyle,
),
],
),
),
const SizedBox(height: 20),
Container(
width: MediaQuery.of(context).size.width * 0.9,
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
border: Border.all(
color: CupertinoColors.systemGreen, width: 2),
),
child: Center(
child: Text(
'${'Passenger Name is'.tr} ${EncryptionHelper.instance.decryptData(res['first_name'])} ${EncryptionHelper.instance.decryptData(res['last_name'])}',
style: CupertinoTheme.of(context)
.textTheme
.textStyle,
),
),
),
const SizedBox(height: 20),
Container(
width: MediaQuery.of(context).size.width * 0.9,
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
border: Border.all(
color: CupertinoColors.systemYellow,
width: 2),
),
child: Center(
child: Text(
'${'Status is'.tr} ${EncryptionHelper.instance.decryptData(res['status'])}',
style: CupertinoTheme.of(context)
.textTheme
.textStyle,
),
),
),
],
),
), ),
), ),
);
// --- Earnings Card ---
_DetailCard(
icon: Icons.account_balance_wallet,
title: 'Earnings & Distance'.tr,
child: Column(
children: [
_InfoTile(
label: 'Your Earnings'.tr,
value: '${res['price_for_driver']}'),
_InfoTile(
label: 'Distance'.tr,
value: '${res['distance']} KM'),
],
),
),
// --- Timeline Card ---
_DetailCard(
icon: Icons.timeline,
title: 'Trip Timeline'.tr,
child: Column(
children: [
_InfoTile(
label: 'Time to Passenger'.tr,
value: res['DriverIsGoingToPassenger'] ?? 'N/A'),
_InfoTile(
label: 'Trip Started'.tr,
value: res['rideTimeStart'] ?? 'N/A'),
_InfoTile(
label: 'Trip Finished'.tr,
value: res['rideTimeFinish'] ?? 'N/A'),
],
),
),
// --- Passenger & Status Card ---
_DetailCard(
icon: Icons.person,
title: 'Passenger & Status'.tr,
child: Column(
children: [
_InfoTile(
label: 'Passenger Name'.tr,
value:
'${res['passengerName']} ${res['last_name']}'),
_InfoTile(
label: 'Status'.tr,
value: res['status'] ?? 'N/A',
isStatus: true),
],
),
),
],
),
),
);
}, },
), ),
); );
} }
Widget _buildMapCard(BuildContext context, LatLng? startLocation,
LatLng? endLocation, Set<Marker> markers) {
// A fallback position if locations are not available
final initialCameraPosition = (startLocation != null)
? CameraPosition(target: startLocation, zoom: 14)
: const CameraPosition(
target: LatLng(31.96, 35.92), zoom: 12); // Fallback to Amman
return Card(
elevation: 4,
shadowColor: Colors.black.withOpacity(0.1),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior:
Clip.antiAlias, // Ensures the map respects the border radius
child: Stack(
children: [
SizedBox(
height: 250,
child: GoogleMap(
initialCameraPosition: initialCameraPosition,
markers: markers,
polylines: {
if (startLocation != null && endLocation != null)
Polyline(
polylineId: const PolylineId('route'),
points: [startLocation, endLocation],
color: Colors.deepPurple,
width: 5,
),
},
onMapCreated: (GoogleMapController mapController) {
// Animate camera to fit the route
if (startLocation != null && endLocation != null) {
LatLngBounds bounds = LatLngBounds(
southwest: LatLng(
startLocation.latitude < endLocation.latitude
? startLocation.latitude
: endLocation.latitude,
startLocation.longitude < endLocation.longitude
? startLocation.longitude
: endLocation.longitude,
),
northeast: LatLng(
startLocation.latitude > endLocation.latitude
? startLocation.latitude
: endLocation.latitude,
startLocation.longitude > endLocation.longitude
? startLocation.longitude
: endLocation.longitude,
),
);
mapController.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 60.0));
}
},
),
),
Positioned(
top: 10,
right: 10,
child: FloatingActionButton.small(
heroTag: 'open_maps',
onPressed: () {
if (startLocation != null && endLocation != null) {
String mapUrl =
'https://www.google.com/maps/dir/${startLocation.latitude},${startLocation.longitude}/${endLocation.latitude},${endLocation.longitude}/';
showInBrowser(mapUrl);
}
},
child: const Icon(Icons.directions),
),
),
],
),
);
}
}
// A reusable widget for the main detail cards
class _DetailCard extends StatelessWidget {
final IconData icon;
final String title;
final Widget child;
const _DetailCard({
required this.icon,
required this.title,
required this.child,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shadowColor: Colors.black.withOpacity(0.05),
margin: const EdgeInsets.only(bottom: 16.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: Theme.of(context).primaryColor),
const SizedBox(width: 8),
Text(
title,
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(fontWeight: FontWeight.bold),
),
],
),
const Divider(height: 24),
child,
],
),
),
);
}
}
// A reusable widget for a label-value pair inside a card
class _InfoTile extends StatelessWidget {
final String label;
final String value;
final bool isStatus;
const _InfoTile({
required this.label,
required this.value,
this.isStatus = false,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(color: Colors.grey[700])),
if (isStatus)
_buildStatusChip(value)
else
Flexible(
child: Text(
value,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.end,
),
),
],
),
);
}
}
// Reusing the status chip from the previous page for consistency
Widget _buildStatusChip(String status) {
Color chipColor;
Color textColor;
IconData iconData;
switch (status.toLowerCase()) {
case 'apply':
case 'completed': // Assuming 'Apply' means completed
chipColor = Colors.green.shade50;
textColor = Colors.green.shade800;
iconData = Icons.check_circle;
status = 'Completed';
break;
case 'refused':
chipColor = Colors.red.shade50;
textColor = Colors.red.shade800;
iconData = Icons.cancel;
break;
case 'cancel':
chipColor = Colors.orange.shade50;
textColor = Colors.orange.shade800;
iconData = Icons.info;
status = 'Cancelled';
break;
default:
chipColor = Colors.grey.shade200;
textColor = Colors.grey.shade800;
iconData = Icons.hourglass_empty;
}
return Chip(
avatar: Icon(iconData, color: textColor, size: 16),
label: Text(
status.tr,
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
),
backgroundColor: chipColor,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
side: BorderSide.none,
);
} }

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@@ -20,6 +22,7 @@ import 'package:sefer_driver/views/home/Captin/About Us/settings_captain.dart';
import 'package:sefer_driver/views/home/my_wallet/walet_captain.dart'; 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 '../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';
@@ -47,7 +50,7 @@ class AppDrawer extends StatelessWidget {
// 2. تعريف بيانات القائمة بشكل مركزي ومنظم // 2. تعريف بيانات القائمة بشكل مركزي ومنظم
final List<DrawerItem> drawerItems = [ final List<DrawerItem> drawerItems = [
DrawerItem( DrawerItem(
title: 'Wallet'.tr, title: 'Balance'.tr,
icon: Icons.account_balance_wallet, icon: Icons.account_balance_wallet,
color: Colors.green, color: Colors.green,
onTap: () => Get.to(() => WalletCaptainRefactored())), onTap: () => Get.to(() => WalletCaptainRefactored())),
@@ -81,16 +84,16 @@ class AppDrawer extends StatelessWidget {
icon: Icons.share, icon: Icons.share,
color: Colors.indigo, color: Colors.indigo,
onTap: () => Get.to(() => InviteScreen())), onTap: () => Get.to(() => InviteScreen())),
DrawerItem( // DrawerItem(
title: 'Maintenance Center'.tr, // title: 'Maintenance Center'.tr,
icon: Icons.build, // icon: Icons.build,
color: Colors.brown, // color: Colors.brown,
onTap: () => Get.to(() => MaintainCenterPage())), // onTap: () => Get.to(() => MaintainCenterPage())),
DrawerItem( // DrawerItem(
title: 'Health Insurance'.tr, // title: 'Health Insurance'.tr,
icon: Icons.favorite, // icon: Icons.favorite,
color: Colors.pink, // color: Colors.pink,
onTap: () => Get.to(() => AssuranceHealthPage())), // onTap: () => Get.to(() => AssuranceHealthPage())),
DrawerItem( DrawerItem(
title: 'Contact Us'.tr, title: 'Contact Us'.tr,
icon: Icons.email, icon: Icons.email,
@@ -111,6 +114,12 @@ class AppDrawer extends StatelessWidget {
icon: Icons.memory, icon: Icons.memory,
color: Colors.greenAccent, color: Colors.greenAccent,
onTap: () => Get.to(() => DeviceCompatibilityPage())), onTap: () => Get.to(() => DeviceCompatibilityPage())),
DrawerItem(
title: 'Privacy Policy'.tr,
icon: Icons.memory,
color: Colors.greenAccent,
onTap: () =>
launchUrl(Uri.parse('${AppLink.server}/privacy_policy.php'))),
DrawerItem( DrawerItem(
title: 'Settings'.tr, title: 'Settings'.tr,
icon: Icons.settings, icon: Icons.settings,

View File

@@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:ui';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:sefer_driver/constant/box_name.dart'; import 'package:sefer_driver/constant/box_name.dart';
@@ -26,18 +27,19 @@ import 'widget/connect.dart';
import 'widget/left_menu_map_captain.dart'; import 'widget/left_menu_map_captain.dart';
import '../../../../main.dart'; import '../../../../main.dart';
// الويدجت الرئيسية للصفحة بعد تنظيمها // ==================================================================
// Redesigned Main Widget (V3)
// ==================================================================
class HomeCaptain extends StatelessWidget { class HomeCaptain extends StatelessWidget {
HomeCaptain({super.key}); HomeCaptain({super.key});
// تم الإبقاء على تعريف الـ Controllers كما هو في الكود الأصلي
final LocationController locationController = Get.put(LocationController()); final LocationController locationController = Get.put(LocationController());
final HomeCaptainController homeCaptainController = final HomeCaptainController homeCaptainController =
Get.put(HomeCaptainController()); Get.put(HomeCaptainController());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// لم يتم تغيير أي شيء في هذه الأوامر // Initial calls remain the same.
Get.put(HomeCaptainController()); Get.put(HomeCaptainController());
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
closeOverlayIfFound(); closeOverlayIfFound();
@@ -46,19 +48,20 @@ class HomeCaptain extends StatelessWidget {
showDriverGiftClaim(context); showDriverGiftClaim(context);
}); });
// التصميم الجديد: أصبح الـ build الرئيسي نظيفاً وواضحاً جداً // The stack is now even simpler.
return Scaffold( return Scaffold(
appBar: const _HomeAppBar(), // 1. تم فصل الـ AppBar في ويدجت خاصة appBar: const _HomeAppBar(),
drawer: AppDrawer(), drawer: AppDrawer(),
body: Stack( body: Stack(
children: [ children: [
// كل جزء من الواجهة أصبح ويدجت منفصلة // 1. The Map View is the base layer.
const _MapView(), // 2. تم فصل الخريطة const _MapView(),
const _DriverStatsOverlay(), // 3. تم فصل كارت الإحصائيات العلوي
const _DriverDurationOverlay(), // 4. تم فصل كارت مدة العمل // 2. The new floating "Status Pod" at the bottom.
const _FloatingActionButtons(), // 5. تم فصل الأزرار الجانبية العائمة const _StatusPodOverlay(),
const _ConnectButtonOverlay(), // 6. تم فصل زر الاتصال السفلي
leftMainMenuCaptainIcons(), // هذه بقيت كما هي // This widget from the original code remains.
leftMainMenuCaptainIcons(),
], ],
), ),
); );
@@ -66,64 +69,51 @@ class HomeCaptain extends StatelessWidget {
} }
// ================================================================== // ==================================================================
// الأجزاء الصغيرة التي تم فصلها (Helper Widgets) // Redesigned Helper Widgets (V3)
// هذه الويدجتس تحتوي على نفس كود التصميم الأصلي الخاص بك تماماً، ولكنها منظمة بشكل أفضل
// ================================================================== // ==================================================================
/// 1. ويدجت الـ AppBar /// 1. The AppBar now contains the map actions in a PopupMenuButton.
class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget { class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
const _HomeAppBar(); const _HomeAppBar();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// نفس الكود الأصلي للـ AppBar
final homeCaptainController = Get.find<HomeCaptainController>(); final homeCaptainController = Get.find<HomeCaptainController>();
return AppBar( return AppBar(
elevation: 2, backgroundColor: Colors.white,
flexibleSpace: Container( elevation: 1,
decoration: BoxDecoration( shadowColor: Colors.black.withOpacity(0.1),
gradient: LinearGradient(
colors: [Colors.white, Colors.grey.shade50],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 4,
),
],
),
),
title: Row( title: Row(
children: [ children: [
Image.asset( Image.asset(
'assets/images/logo.gif', 'assets/images/logo.gif',
height: 32, height: 35,
width: 35,
), ),
const SizedBox(width: 8), const SizedBox(width: 10),
Text( Text(
AppInformation.appName.split(' ')[0].toString().tr, AppInformation.appName.split(' ')[0].toString().tr,
style: AppStyle.title.copyWith( style: AppStyle.title.copyWith(
fontSize: 22, fontSize: 24,
fontWeight: FontWeight.w600, fontWeight: FontWeight.bold,
color: AppColor.blueColor, color: AppColor.blueColor,
), ),
), ),
], ],
), ),
actions: [ actions: [
// Refuse count indicator
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.only(right: 8.0),
child: MyCircleContainer( child: Center(
child: Text( child: MyCircleContainer(
homeCaptainController.countRefuse.toString(), child: Text(
style: AppStyle.title, homeCaptainController.countRefuse.toString(),
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
),
), ),
), ),
), ),
// The new PopupMenuButton for all map and ride actions.
Container( Container(
margin: const EdgeInsets.symmetric(horizontal: 4), margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -165,28 +155,45 @@ class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
], ],
), ),
), ),
const SizedBox(width: 8),
], ],
); );
} }
PopupMenuItem<String> _buildPopupMenuItem({
required String value,
IconData? icon,
Widget? iconWidget,
required String text,
Color? iconColor,
}) {
return PopupMenuItem<String>(
value: value,
child: Row(
children: [
iconWidget ?? Icon(icon, color: iconColor ?? Colors.grey.shade600),
const SizedBox(width: 16),
Text(text),
],
),
);
}
@override @override
Size get preferredSize => const Size.fromHeight(kToolbarHeight); Size get preferredSize => const Size.fromHeight(kToolbarHeight);
} }
/// 2. ويدجت الخريطة /// 2. The Map View is unchanged functionally.
class _MapView extends StatelessWidget { class _MapView extends StatelessWidget {
const _MapView(); const _MapView();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final locationController = Get.find<LocationController>(); final locationController = Get.find<LocationController>();
// نفس الكود الأصلي للخريطة
return GetBuilder<HomeCaptainController>(builder: (controller) { return GetBuilder<HomeCaptainController>(builder: (controller) {
return controller.isLoading return controller.isLoading
? const MyCircularProgressIndicator() ? const MyCircularProgressIndicator()
: GoogleMap( : GoogleMap(
padding: EdgeInsets.only(bottom: 50, top: 300), padding: const EdgeInsets.only(bottom: 110, top: 300),
fortyFiveDegreeImageryEnabled: true, fortyFiveDegreeImageryEnabled: true,
onMapCreated: controller.onMapCreated, onMapCreated: controller.onMapCreated,
minMaxZoomPreference: const MinMaxZoomPreference(6, 18), minMaxZoomPreference: const MinMaxZoomPreference(6, 18),
@@ -217,7 +224,7 @@ class _MapView extends StatelessWidget {
myLocationEnabled: false, myLocationEnabled: false,
trafficEnabled: controller.mapTrafficON, trafficEnabled: controller.mapTrafficON,
buildingsEnabled: true, buildingsEnabled: true,
mapToolbarEnabled: true, mapToolbarEnabled: false,
compassEnabled: true, compassEnabled: true,
zoomControlsEnabled: false, zoomControlsEnabled: false,
); );
@@ -225,353 +232,213 @@ class _MapView extends StatelessWidget {
} }
} }
/// 3. ويدجت كارت الإحصائيات العلوي /// 3. The floating "Status Pod" at the bottom of the screen.
class _DriverStatsOverlay extends StatelessWidget { class _StatusPodOverlay extends StatelessWidget {
const _DriverStatsOverlay(); const _StatusPodOverlay();
@override void _showDetailsDialog(BuildContext context) {
Widget build(BuildContext context) { Get.dialog(
// نفس الكود الأصلي لكارت الإحصائيات const _DriverDetailsDialog(),
return Positioned( barrierColor: Colors.black.withOpacity(0.3),
top: 5,
right: Get.width * .05,
left: Get.width * .05,
child: GetBuilder<HomeCaptainController>(
builder: (homeCaptainController) {
return Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.white, Colors.white70],
),
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
width: Get.width * .8,
height: 120,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.greenColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
const Icon(
Entypo.wallet,
color: AppColor.greenColor,
size: 20,
),
const SizedBox(width: 8),
Text(
'${"Today".tr}: ${(homeCaptainController.totalMoneyToday)}',
style: AppStyle.title.copyWith(
color: AppColor.greenColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.yellowColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
const Icon(
Entypo.wallet,
color: AppColor.yellowColor,
size: 20,
),
const SizedBox(width: 8),
Text(
'${AppInformation.appName}: ${(homeCaptainController.totalMoneyInSEFER)}',
style: AppStyle.title.copyWith(
color: AppColor.yellowColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${'Total Points is'.tr}: ${(homeCaptainController.totalPoints)}',
style: AppStyle.title.copyWith(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: int.parse(
(homeCaptainController.countRideToday)) <
5
? AppColor.accentColor
: int.parse((homeCaptainController
.countRideToday)) >
5 &&
int.parse((homeCaptainController
.countRideToday)) <
10
? AppColor.yellowColor
: AppColor.greenColor,
),
child: Row(
children: [
const Icon(
Icons.directions_car_rounded,
color: Colors.white,
size: 18,
),
const SizedBox(width: 4),
Text(
'${"Ride Today : ".tr}: ${(homeCaptainController.countRideToday)}',
style: AppStyle.title.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
]),
);
},
),
); );
} }
}
/// 4. ويدجت كارت مدة العمل
class _DriverDurationOverlay extends StatelessWidget {
const _DriverDurationOverlay();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// نفس الكود الأصلي لكارت المدة final homeCaptainController = Get.find<HomeCaptainController>();
return Positioned( return Positioned(
bottom: 65, bottom: 16,
right: Get.width * .1, left: 16,
left: Get.width * .1, right: 16,
child: GetBuilder<HomeCaptainController>( child: GestureDetector(
builder: (homeCaptainController) => Container( onTap: () => _showDetailsDialog(context),
decoration: BoxDecoration( child: ClipRRect(
color: Colors.white, borderRadius: BorderRadius.circular(24),
borderRadius: BorderRadius.circular(12), child: BackdropFilter(
boxShadow: [ filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
BoxShadow( child: Container(
color: Colors.grey.withOpacity(0.2), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
spreadRadius: 2, decoration: BoxDecoration(
blurRadius: 8, color: Colors.white.withOpacity(0.85),
offset: const Offset(0, 2), borderRadius: BorderRadius.circular(24),
), border: Border.all(color: Colors.white.withOpacity(0.5)),
], boxShadow: [
), BoxShadow(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), color: Colors.black.withOpacity(0.1),
child: Column( blurRadius: 20,
children: [ spreadRadius: -5,
Row( )
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.timer_outlined, color: AppColor.greenColor),
const SizedBox(width: 8),
Text(
'Active Duration:'.tr,
style: AppStyle.title,
),
const SizedBox(width: 4),
Text(
(homeCaptainController.stringActiveDuration),
style: AppStyle.title.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.greenColor,
),
),
], ],
), ),
const SizedBox(height: 8), child: Row(
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.access_time, color: AppColor.accentColor), const ConnectWidget(),
const Spacer(),
_buildQuickStat(
icon: Icons.directions_car_rounded,
value: homeCaptainController.countRideToday,
label: 'Rides'.tr,
color: AppColor.blueColor,
),
const SizedBox(width: 16),
_buildQuickStat(
icon: Entypo.wallet,
value: homeCaptainController.totalMoneyToday.toString(),
label: 'Today'.tr,
color: AppColor.greenColor,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(
'Total Connection Duration:'.tr,
style: AppStyle.title,
),
const SizedBox(width: 4),
Text(
(homeCaptainController.totalDurationToday),
style: AppStyle.title.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.accentColor,
),
),
], ],
), ),
], ),
), ),
), ),
), ),
); );
} }
}
/// 5. ويدجت الأزرار الجانبية العائمة Widget _buildQuickStat(
class _FloatingActionButtons extends StatelessWidget { {required IconData icon,
const _FloatingActionButtons(); required String value,
required String label,
@override required Color color}) {
Widget build(BuildContext context) { return Column(
// نفس الكود الأصلي للأزرار mainAxisSize: MainAxisSize.min,
return Positioned( crossAxisAlignment: CrossAxisAlignment.center,
bottom: Get.height * .2, children: [
right: 6, Row(
child:
GetBuilder<HomeCaptainController>(builder: (homeCaptainController) {
return Column(
children: [ children: [
Platform.isAndroid Icon(icon, color: color, size: 20),
? AnimatedContainer( const SizedBox(width: 4),
duration: const Duration(microseconds: 200), Text(value,
width: homeCaptainController.widthMapTypeAndTraffic, style: AppStyle.title
decoration: BoxDecoration( .copyWith(fontSize: 16, fontWeight: FontWeight.bold)),
border: Border.all(color: AppColor.blueColor),
color: AppColor.secondaryColor,
borderRadius: BorderRadius.circular(15)),
child: IconButton(
onPressed: () async {
Bubble().startBubbleHead(sendAppToBackground: true);
},
icon: Image.asset(
'assets/images/logo1.png',
fit: BoxFit.cover,
width: 35,
height: 35,
),
),
)
: const SizedBox(),
const SizedBox(
height: 5,
),
AnimatedContainer(
duration: const Duration(microseconds: 200),
width: homeCaptainController.widthMapTypeAndTraffic,
decoration: BoxDecoration(
border: Border.all(color: AppColor.blueColor),
color: AppColor.secondaryColor,
borderRadius: BorderRadius.circular(15)),
child: IconButton(
onPressed: () {
Get.to(() => const AvailableRidesPage());
},
icon: const Icon(
Icons.train_sharp,
size: 29,
color: AppColor.blueColor,
),
),
),
const SizedBox(
height: 5,
),
box.read(BoxName.rideStatus) == 'Applied' ||
box.read(BoxName.rideStatus) == 'Begin'
? Positioned(
bottom: Get.height * .2,
right: 6,
child: AnimatedContainer(
duration: const Duration(microseconds: 200),
width: homeCaptainController.widthMapTypeAndTraffic,
decoration: BoxDecoration(
border: Border.all(color: AppColor.blueColor),
color: AppColor.secondaryColor,
borderRadius: BorderRadius.circular(15)),
child: GestureDetector(
onLongPress: () {
box.write(BoxName.rideStatus, 'delete');
homeCaptainController.update();
},
child: IconButton(
onPressed: () {
box.read(BoxName.rideStatus) == 'Applied'
? {
Get.to(() => PassengerLocationMapPage(),
arguments:
box.read(BoxName.rideArguments)),
Get.put(MapDriverController())
.changeRideToBeginToPassenger()
}
: {
Get.to(() => PassengerLocationMapPage(),
arguments:
box.read(BoxName.rideArguments)),
Get.put(MapDriverController())
.startRideFromStartApp()
};
},
icon: const Icon(
Icons.directions_rounded,
size: 29,
color: AppColor.blueColor,
),
),
),
),
)
: const SizedBox()
], ],
); ),
}), Text(label,
style: AppStyle.title
.copyWith(fontSize: 12, color: Colors.grey.shade700)),
],
); );
} }
} }
/// 6. ويدجت زر الاتصال السفلي /// 4. The Dialog that shows detailed driver stats.
class _ConnectButtonOverlay extends StatelessWidget { class _DriverDetailsDialog extends StatelessWidget {
const _ConnectButtonOverlay(); const _DriverDetailsDialog();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// نفس الكود الأصلي لزر الاتصال final homeCaptainController = Get.find<HomeCaptainController>();
return Positioned( return BackdropFilter(
bottom: 10, filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
right: Get.width * .1, child: AlertDialog(
left: Get.width * .1, backgroundColor: Colors.white.withOpacity(0.95),
child: const ConnectWidget()); shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
titlePadding: const EdgeInsets.only(top: 20),
title: Center(
child: Text(
'Your Activity'.tr,
style: AppStyle.title
.copyWith(fontSize: 20, fontWeight: FontWeight.bold),
),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(height: 20),
_buildStatRow(
icon: Entypo.wallet,
color: AppColor.greenColor,
label: 'Today'.tr,
value: homeCaptainController.totalMoneyToday.toString(),
),
const SizedBox(height: 12),
_buildStatRow(
icon: Entypo.wallet,
color: AppColor.yellowColor,
label: AppInformation.appName,
value: homeCaptainController.totalMoneyInSEFER.toString(),
),
const Divider(height: 24),
_buildDurationRow(
icon: Icons.timer_outlined,
label: 'Active Duration:'.tr,
value: homeCaptainController.stringActiveDuration,
color: AppColor.greenColor,
),
const SizedBox(height: 12),
_buildDurationRow(
icon: Icons.access_time,
label: 'Total Connection Duration:'.tr,
value: homeCaptainController.totalDurationToday,
color: AppColor.accentColor,
),
const Divider(height: 24),
_buildStatRow(
icon: Icons.star_border_rounded,
color: AppColor.blueColor,
label: 'Total Points'.tr,
value: homeCaptainController.totalPoints.toString(),
),
],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('Close'.tr,
style: AppStyle.title.copyWith(
color: AppColor.blueColor, fontWeight: FontWeight.bold)),
)
],
),
);
}
Widget _buildStatRow(
{required IconData icon,
required Color color,
required String label,
required String value}) {
return Row(
children: [
Icon(icon, color: color, size: 22),
const SizedBox(width: 12),
Text('$label:', style: AppStyle.title),
const Spacer(),
Text(
value,
style: AppStyle.title.copyWith(
color: color, fontWeight: FontWeight.bold, fontSize: 18),
),
],
);
}
Widget _buildDurationRow(
{required IconData icon,
required String label,
required String value,
required Color color}) {
return Row(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 12),
Text(label, style: AppStyle.title),
const Spacer(),
Text(
value,
style: AppStyle.title.copyWith(
fontWeight: FontWeight.bold, color: color, fontSize: 16),
),
],
);
} }
} }
// هذه الويدجت المساعدة بقيت كما هي
class _MapControlButton extends StatelessWidget { class _MapControlButton extends StatelessWidget {
final IconData icon; final IconData icon;
final VoidCallback onPressed; final VoidCallback onPressed;
@@ -606,7 +473,44 @@ class _MapControlButton extends StatelessWidget {
} }
} }
// الدوال المساعدة الأخرى تبقى كما هي في ملفك... /// NOTE: The _FloatingActionButtons and _MapControlButton widgets have been removed
// showFirstTimeOfferNotification(BuildContext context) async { ... } /// as their functionality is now integrated into the _HomeAppBar.
// bool _checkIfFirstTime() { ... } ///
// void _markAsNotFirstTime() { ... } /// You will still need to modify your existing `ConnectWidget`
/// to accept an `isCompact` boolean flag as mentioned in the previous design.
/*
class ConnectWidget extends StatelessWidget {
final bool isCompact;
const ConnectWidget({super.key, this.isCompact = false});
@override
Widget build(BuildContext context) {
// ... your existing controller logic
if (isCompact) {
// Return a smaller version for the pod
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: controller.isConnect ? AppColor.greenColor : AppColor.accentColor,
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(controller.isConnect ? Icons.wifi_tethering_rounded : Icons.wifi_tethering_off_rounded, color: Colors.white, size: 20),
const SizedBox(width: 8),
Text(
controller.isConnect ? 'Online'.tr : 'Offline'.tr,
style: AppStyle.title.copyWith(color: Colors.white, fontSize: 14),
),
],
),
);
}
// Return the original, larger button
return ElevatedButton.icon(...)
}
}
*/

View File

@@ -1,6 +1,7 @@
import 'package:sefer_driver/constant/box_name.dart'; import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/controller/firebase/local_notification.dart'; import 'package:sefer_driver/controller/firebase/local_notification.dart';
import 'package:sefer_driver/main.dart'; import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/auth/captin/login_captin.dart';
import 'package:sefer_driver/views/home/Captin/driver_map_page.dart'; import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
import 'package:sefer_driver/views/home/Captin/orderCaptin/vip_order_page.dart'; import 'package:sefer_driver/views/home/Captin/orderCaptin/vip_order_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -14,6 +15,7 @@ import '../../../../../constant/links.dart';
import '../../../../../controller/firebase/firbase_messge.dart'; import '../../../../../controller/firebase/firbase_messge.dart';
import '../../../../../controller/functions/crud.dart'; import '../../../../../controller/functions/crud.dart';
import '../../../../../controller/home/captin/order_request_controller.dart'; import '../../../../../controller/home/captin/order_request_controller.dart';
import '../../../../../controller/home/navigation/navigation_view.dart';
import '../../../../Rate/ride_calculate_driver.dart'; import '../../../../Rate/ride_calculate_driver.dart';
import '../../../../auth/syria/registration_view.dart'; import '../../../../auth/syria/registration_view.dart';
@@ -156,18 +158,42 @@ GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
child: Builder(builder: (context) { child: Builder(builder: (context) {
return IconButton( return IconButton(
onPressed: () async { onPressed: () async {
Get.to(() => const RegistrationView()); box.remove(BoxName.agreeTerms);
Get.to(() => const NavigationView());
// box.write(BoxName.statusDriverLocation, 'off'); // box.write(BoxName.statusDriverLocation, 'off');
}, },
icon: const Icon( icon: const Icon(
FontAwesome5.grin_tears, FontAwesome5.map,
size: 29, size: 29,
color: AppColor.blueColor, color: AppColor.blueColor,
), ),
); );
}), }),
), ),
// AnimatedContainer(
// duration: const Duration(microseconds: 200),
// width: controller.widthMapTypeAndTraffic,
// decoration: BoxDecoration(
// color: AppColor.secondaryColor,
// border: Border.all(color: AppColor.blueColor),
// borderRadius: BorderRadius.circular(15)),
// child: Builder(builder: (context) {
// return IconButton(
// onPressed: () async {
// box.remove(BoxName.agreeTerms);
// Get.to(() => const NavigationView());
// // box.write(BoxName.statusDriverLocation, 'off');
// },
// icon: const Icon(
// FontAwesome5.grin_tears,
// size: 29,
// color: AppColor.blueColor,
// ),
// );
// }),
// ),
const SizedBox( const SizedBox(
height: 5, height: 5,

View File

@@ -204,7 +204,7 @@ GetBuilder<MapDriverController> speedCircle() {
? Positioned( ? Positioned(
// New: تم وضع دائرة السرعة في الأسفل يمينًا // New: تم وضع دائرة السرعة في الأسفل يمينًا
bottom: 25, bottom: 25,
left: 16, left: 3,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,

View File

@@ -1,117 +1,3 @@
// import 'package:flutter/material.dart';
// import 'package:get/get.dart';
// import 'package:google_maps_flutter/google_maps_flutter.dart';
// import '../../../../controller/functions/location_controller.dart';
// import '../../../../controller/home/captin/map_driver_controller.dart';
// class GoogleDriverMap extends StatelessWidget {
// const GoogleDriverMap({
// super.key,
// required this.locationController,
// });
// final LocationController locationController;
// @override
// Widget build(BuildContext context) {
// Get.put(MapDriverController());
// return Padding(
// padding: const EdgeInsets.all(8.0),
// child: GetBuilder<MapDriverController>(
// builder: (controller) => Column(
// children: [
// SizedBox(
// height: Get.height * .92,
// child: GoogleMap(
// onMapCreated: controller.onMapCreated,
// zoomControlsEnabled: true,
// // initialCameraPosition: CameraPosition(
// // target: locationController.myLocation,
// // zoom: 13,
// // bearing: locationController.heading,
// // tilt: 40,
// // ),
// initialCameraPosition: CameraPosition(
// target: locationController.myLocation,
// zoom: 17,
// bearing: locationController.heading, // استخدام اتجاه السائق
// tilt: 60, // زاوية ميل
// ),
// cameraTargetBounds:
// CameraTargetBounds.unbounded, // Allow unrestricted movement
// onCameraMove: (position) {
// CameraPosition(
// target: locationController.myLocation,
// zoom: 13,
// bearing: locationController.heading,
// tilt: 40,
// );
// //todo
// // locationController.myLocation = position.target;
// //
// // controller.mapController
// // ?.animateCamera(CameraUpdate.newCameraPosition(position));
// },
// minMaxZoomPreference: const MinMaxZoomPreference(8, 15),
// myLocationEnabled: true,
// myLocationButtonEnabled: true,
// compassEnabled: true,
// mapType: MapType.terrain,
// rotateGesturesEnabled: true,
// scrollGesturesEnabled: true,
// trafficEnabled: false,
// buildingsEnabled: true,
// mapToolbarEnabled: true,
// fortyFiveDegreeImageryEnabled: true,
// zoomGesturesEnabled: true,
// polylines: {
// Polyline(
// zIndex: 2,
// geodesic: true,
// polylineId: const PolylineId('route1'),
// points: controller.polylineCoordinates,
// color: const Color.fromARGB(255, 163, 81, 246),
// width: 5,
// ),
// Polyline(
// zIndex: 2,
// geodesic: true,
// polylineId: const PolylineId('route'),
// points: controller.polylineCoordinatesDestination,
// color: const Color.fromARGB(255, 10, 29, 126),
// width: 5,
// ),
// },
// markers: {
// Marker(
// markerId: MarkerId('MyLocation'.tr),
// position: locationController.myLocation,
// draggable: true,
// icon: controller.carIcon,
// rotation: locationController.heading,
// ),
// Marker(
// markerId: MarkerId('start'.tr),
// position: controller.latLngPassengerLocation,
// draggable: true,
// icon: controller.startIcon,
// ),
// Marker(
// markerId: MarkerId('end'.tr),
// position: controller.latLngPassengerDestination,
// draggable: true,
// icon: controller.endIcon,
// ),
// },
// ),
// ),
// ],
// ),
// ),
// );
// }
// }
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
@@ -179,15 +65,30 @@ class GoogleDriverMap extends StatelessWidget {
startCap: Cap.roundCap, startCap: Cap.roundCap,
endCap: Cap.roundCap, endCap: Cap.roundCap,
), ),
Polyline( // Polyline(
zIndex: 2, // zIndex: 2,
polylineId: const PolylineId('route'), // polylineId: const PolylineId('route'),
points: controller.polylineCoordinatesDestination, // points: controller.polylineCoordinatesDestination,
color: const Color.fromARGB(255, 10, 29, 126), // color: const Color.fromARGB(255, 10, 29, 126),
width: 6, // Changed: زيادة عرض الخط // width: 6, // Changed: زيادة عرض الخط
startCap: Cap.roundCap, // startCap: Cap.roundCap,
endCap: Cap.roundCap, // endCap: Cap.roundCap,
// ),
Polyline(
polylineId: const PolylineId('upcoming_route'),
points: controller.upcomingPathPoints,
color: Colors.blue, // أو أي لون آخر تختاره للمسار
width: 8,
zIndex: 2,
),
// 2. الخط المقطوع (تحت)
Polyline(
polylineId: const PolylineId('traveled_route'),
points: controller.traveledPathPoints,
color: Colors.grey.withOpacity(0.8),
width: 7,
zIndex: 1,
), ),
}, },
markers: { markers: {

View File

@@ -14,7 +14,7 @@ class GoogleMapApp extends StatelessWidget {
return GetBuilder<MapDriverController>( return GetBuilder<MapDriverController>(
builder: (mapDriverController) => mapDriverController.isRideStarted builder: (mapDriverController) => mapDriverController.isRideStarted
? Positioned( ? Positioned(
left: 150, right: 3,
bottom: 20, bottom: 20,
child: Container( child: Container(
decoration: AppStyle.boxDecoration, decoration: AppStyle.boxDecoration,

View File

@@ -144,6 +144,11 @@ class PassengerInfoWindow extends StatelessWidget {
borderRadius: BorderRadius.circular(10)), borderRadius: BorderRadius.circular(10)),
), ),
onPressed: () async { onPressed: () async {
controller.getRoute(
origin: controller.latLngPassengerLocation,
destination: controller.latLngPassengerDestination,
routeColor: Colors.blue // أو أي لون
);
if (await controller if (await controller
.calculateDistanceBetweenDriverAndPassengerLocation() < .calculateDistanceBetweenDriverAndPassengerLocation() <
140) { 140) {

View File

@@ -274,13 +274,18 @@ class _OrderRequestPageState extends State<OrderRequestPage> {
controller.myList[8].toString(), controller.myList[8].toString(),
controller.myList[9].toString(), controller.myList[9].toString(),
]; ];
FirebaseMessagesController() final fmc =
.sendNotificationToPassengerToken( Get.isRegistered<FirebaseMessagesController>()
"Accepted Ride".tr, ? Get.find<FirebaseMessagesController>()
'your ride is Accepted'.tr, : Get.put(FirebaseMessagesController());
controller.myList[9].toString(),
bodyToPassenger, fmc.sendNotificationToDriverMAP(
'start.wav'); "Accepted Ride".tr,
'your ride is Accepted'.tr,
controller.myList[9].toString(),
bodyToPassenger,
'start.wav',
);
Get.back(); Get.back();
box.write(BoxName.rideArguments, { box.write(BoxName.rideArguments, {
'passengerLocation': 'passengerLocation':

View File

@@ -95,7 +95,7 @@ class CardSeferWalletDriver extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Text(
'محفظة انطلق', 'رصيد انطلق',
style: AppStyle.headTitle.copyWith( style: AppStyle.headTitle.copyWith(
fontFamily: 'Amiri', // خط يوحي بالفخامة fontFamily: 'Amiri', // خط يوحي بالفخامة
color: Colors.white, color: Colors.white,

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:sefer_driver/constant/colors.dart'; import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart'; import 'package:sefer_driver/constant/style.dart'; // Assuming this has your text styles
import 'package:sefer_driver/views/widgets/my_scafold.dart'; import 'package:sefer_driver/views/widgets/mycircular.dart'; // Assuming this is your loading widget
import 'package:sefer_driver/views/widgets/mycircular.dart';
import '../../../controller/payment/driver_payment_controller.dart'; import '../../../controller/payment/driver_payment_controller.dart';
@@ -12,43 +12,133 @@ class PaymentHistoryDriverPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Initialize your controller
Get.put(DriverWalletHistoryController()); Get.put(DriverWalletHistoryController());
return MyScafolld(
title: 'Payment History'.tr, return Scaffold(
body: [ appBar: AppBar(
GetBuilder<DriverWalletHistoryController>( title: Text('Payment History'.tr),
builder: (controller) => controller.isLoading backgroundColor: Colors.white,
? const MyCircularProgressIndicator() elevation: 1,
: ListView.builder( ),
itemCount: controller.archive.length, backgroundColor: Colors.grey[100],
itemBuilder: (BuildContext context, int index) { body: GetBuilder<DriverWalletHistoryController>(
var list = controller.archive[index]; builder: (controller) {
return Padding( if (controller.isLoading) {
padding: const EdgeInsets.all(4), // Using your custom loading indicator
child: Container( return const Center(child: MyCircularProgressIndicator());
decoration: BoxDecoration( }
color: double.parse(list['amount']) < 0
? AppColor.redColor.withOpacity(.4) if (controller.archive.isEmpty) {
: AppColor.greenColor.withOpacity(.4)), return Center(
child: Row( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Icon(Icons.account_balance_wallet_outlined,
list['amount'], size: 80, color: Colors.grey[400]),
style: AppStyle.title, const SizedBox(height: 16),
), Text(
Text( 'No transactions yet'.tr,
list['created_at'], style: Theme.of(context)
style: AppStyle.title, .textTheme
), .headlineSmall
], ?.copyWith(color: Colors.grey[600]),
),
),
);
},
), ),
) ],
], ),
isleading: true); );
}
return AnimationLimiter(
child: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: controller.archive.length,
itemBuilder: (BuildContext context, int index) {
var transaction = controller.archive[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: _TransactionCard(transaction: transaction),
),
),
);
},
),
);
},
),
);
}
}
// A dedicated widget for displaying a single transaction with a modern UI.
class _TransactionCard extends StatelessWidget {
final Map<String, dynamic> transaction;
const _TransactionCard({required this.transaction});
@override
Widget build(BuildContext context) {
// Safely parse the amount to avoid errors
final double amount =
double.tryParse(transaction['amount']?.toString() ?? '0') ?? 0;
final bool isCredit = amount >= 0;
final Color indicatorColor =
isCredit ? AppColor.greenColor : AppColor.redColor;
final IconData iconData =
isCredit ? Icons.arrow_upward_rounded : Icons.arrow_downward_rounded;
final String transactionType = (isCredit ? 'Credit'.tr : 'Debit'.tr).tr;
return Card(
elevation: 2,
shadowColor: Colors.black.withOpacity(0.05),
margin: const EdgeInsets.only(bottom: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
clipBehavior: Clip.antiAlias, // Ensures the color bar is clipped neatly
child: IntrinsicHeight(
// Ensures the color bar and content have the same height
child: Row(
children: [
// Left-side color indicator bar
Container(width: 6, color: indicatorColor),
Expanded(
child: ListTile(
leading: Icon(iconData, color: indicatorColor, size: 30),
title: Text(
// Use .abs() to remove the negative sign from the display
'${amount.abs().toStringAsFixed(2)} ${'SYP'.tr}',
style: AppStyle.title.copyWith(
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
subtitle: Text(
transaction['created_at'] ?? 'No date',
style: AppStyle.title.copyWith(
fontSize: 12,
color: Colors.grey[600],
),
),
trailing: Text(
transactionType,
style: AppStyle.title.copyWith(
fontSize: 14,
color: indicatorColor,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
);
} }
} }

View File

@@ -47,7 +47,7 @@ class WalletCaptainRefactored extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
captainWalletController.refreshCaptainWallet(); captainWalletController.refreshCaptainWallet();
return MyScafolld( return MyScafolld(
title: 'Driver Wallet'.tr, title: 'Driver Balance'.tr,
isleading: true, isleading: true,
action: IconButton( action: IconButton(
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
@@ -173,7 +173,7 @@ class WalletCaptainRefactored extends StatelessWidget {
'This amount for all trip I get from Passengers' 'This amount for all trip I get from Passengers'
.tr), .tr),
child: const Icon(Icons.headphones)), child: const Icon(Icons.headphones)),
'${'Total Amount:'.tr} ${controller.totalAmount} ${'S.P'.tr}', '${'Total Amount:'.tr} ${controller.totalAmount} ${'SYP'.tr}',
'This amount for all trip I get from Passengers'.tr, 'This amount for all trip I get from Passengers'.tr,
duration: const Duration(seconds: 6), duration: const Duration(seconds: 6),
backgroundColor: AppColor.yellowColor, backgroundColor: AppColor.yellowColor,
@@ -195,7 +195,7 @@ class WalletCaptainRefactored extends StatelessWidget {
' Intaleq Wallet'.tr), ' Intaleq Wallet'.tr),
child: const Icon(Icons.headphones), child: const Icon(Icons.headphones),
), ),
'${'Total Amount:'.tr} ${controller.totalAmountVisa} ${'S.P'.tr}', '${'Total Amount:'.tr} ${controller.totalAmountVisa} ${'SYP'.tr}',
'This amount for all trip I get from Passengers and Collected For me in' 'This amount for all trip I get from Passengers and Collected For me in'
.tr + .tr +
' ${AppInformation.appName} Wallet'.tr, ' ${AppInformation.appName} Wallet'.tr,

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:sefer_driver/constant/colors.dart'; import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart'; import 'package:sefer_driver/constant/style.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:intl/intl.dart';
import '../../../controller/payment/driver_payment_controller.dart'; import '../../../controller/payment/driver_payment_controller.dart';
class WeeklyPaymentPage extends StatelessWidget { class WeeklyPaymentPage extends StatelessWidget {
@@ -14,128 +14,210 @@ class WeeklyPaymentPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Get.put(DriverWalletHistoryController()); Get.put(DriverWalletHistoryController());
return MyScafolld(
title: 'Payment History'.tr, return Scaffold(
body: [ appBar: AppBar(
GetBuilder<DriverWalletHistoryController>( title: Text('Weekly Summary'.tr),
builder: (controller) => controller.isLoading backgroundColor: Colors.white,
? const MyCircularProgressIndicator() elevation: 1,
: Column( ),
children: [ backgroundColor: Colors.grey[100],
Container( body: GetBuilder<DriverWalletHistoryController>(
width: Get.width * .8, builder: (controller) {
decoration: AppStyle.boxDecoration1, if (controller.isLoading) {
child: Row( return const Center(child: MyCircularProgressIndicator());
mainAxisAlignment: MainAxisAlignment.center, }
children: [
Padding( return Column(
padding: const EdgeInsets.all(8.0), children: [
child: Container( // 1. Prominent Summary Card at the top
decoration: AppStyle.boxDecoration1, _buildSummaryCard(controller),
child: Padding(
padding: const EdgeInsets.all(8.0), // 2. A title for the transactions list
child: Text( Padding(
controller.weeklyList.isEmpty padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
? '0' child: Row(
: controller.weeklyList[0] children: [
['totalAmount'] Icon(Icons.list_alt, color: Colors.grey[600]),
.toString(), const SizedBox(width: 8),
style: AppStyle.number, Text(
), 'Transactions this week'.tr,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
],
),
),
// 3. The animated list of transactions
Expanded(
child: controller.weeklyList.isEmpty
? _buildEmptyState(context)
: AnimationLimiter(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
itemCount: controller.weeklyList.length,
itemBuilder: (BuildContext context, int index) {
var transaction = controller.weeklyList[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: _TransactionListItem(
transaction: transaction),
), ),
), ),
), );
Text( },
' Total weekly is '.tr,
style: AppStyle.title,
),
],
), ),
), ),
const SizedBox( ),
height: 10, ],
), );
Padding( },
padding: const EdgeInsets.symmetric( ),
horizontal: 10, vertical: 5), );
child: SizedBox( }
height: Get.height * .75,
child: controller.weeklyList.isNotEmpty // A widget for the top summary card.
? ListView.builder( Widget _buildSummaryCard(DriverWalletHistoryController controller) {
itemCount: controller.weeklyList.length, final String totalAmount = controller.weeklyList.isEmpty
itemBuilder: ? '0.00'
(BuildContext context, int index) { : controller.weeklyList[0]['totalAmount']?.toString() ?? '0.00';
var list = controller.weeklyList[index];
return Padding( return Card(
padding: const EdgeInsets.all(2.0), margin: const EdgeInsets.all(16.0),
child: Container( elevation: 4,
decoration: AppStyle.boxDecoration1, shadowColor: AppColor.primaryColor.withOpacity(0.2),
child: Padding( clipBehavior: Clip.antiAlias,
padding: const EdgeInsets.all(4), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
child: Column( child: Container(
children: [ padding: const EdgeInsets.all(20.0),
Card( decoration: BoxDecoration(
elevation: 2, gradient: LinearGradient(
color: list['paymentMethod'] == colors: [AppColor.primaryColor, AppColor.greenColor],
'visa' begin: Alignment.topLeft,
? AppColor.blueColor end: Alignment.bottomRight,
: AppColor.secondaryColor, ),
child: Padding( ),
padding: child: Row(
const EdgeInsets.all(8.0), children: [
child: Text( const Icon(Icons.account_balance_wallet,
list['paymentMethod'] == color: Colors.white, size: 40),
'Remainder' const SizedBox(width: 16),
? 'Remainder'.tr Expanded(
: list['paymentMethod'] == child: Column(
'fromBudget' crossAxisAlignment: CrossAxisAlignment.start,
? 'fromBudget'.tr children: [
: list[ Text(
'paymentMethod'], 'Total Weekly Earnings'.tr,
style: AppStyle.title, style: AppStyle.title
), .copyWith(color: Colors.white70, fontSize: 16),
),
),
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Card(
child: Padding(
padding:
const EdgeInsets.all(
8.0),
child: Text(
list['amount'],
style: AppStyle.number,
),
),
),
Text(
DateFormat(
'yyyy-MM-dd hh:mm a')
.format(DateTime.parse(
list[
'dateUpdated'])),
style: AppStyle.number,
),
],
),
],
),
),
),
);
},
)
: const SizedBox(),
),
),
],
), ),
) const SizedBox(height: 4),
Text(
'$totalAmount ${'SYP'.tr}',
style: AppStyle.number
.copyWith(color: Colors.white, fontSize: 32),
),
],
),
),
],
),
),
);
}
// A dedicated widget for the list item.
Widget _TransactionListItem({required Map<String, dynamic> transaction}) {
final String paymentMethod = transaction['paymentMethod'] ?? 'Unknown';
final String amount = transaction['amount']?.toString() ?? '0';
final DateTime? date = DateTime.tryParse(transaction['dateUpdated'] ?? '');
return Card(
elevation: 2,
shadowColor: Colors.black.withOpacity(0.05),
margin: const EdgeInsets.only(bottom: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: ListTile(
leading: CircleAvatar(
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
child: Icon(_getPaymentIcon(paymentMethod),
color: AppColor.primaryColor, size: 22),
),
title: Text(
'$amount ${'SYP'.tr}',
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
),
subtitle: Text(
date != null
? DateFormat('EEEE, hh:mm a', Get.locale?.toString())
.format(date) // e.g., Tuesday, 10:11 AM
: 'Invalid Date',
style: AppStyle.title.copyWith(fontSize: 12, color: Colors.grey[600]),
),
trailing: Chip(
label: Text(
_getTranslatedPaymentMethod(paymentMethod),
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
),
backgroundColor: Colors.grey.shade200,
padding: const EdgeInsets.symmetric(horizontal: 6),
side: BorderSide.none,
),
),
);
}
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.receipt_long_outlined, size: 80, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'No transactions this week'.tr,
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(color: Colors.grey[600]),
),
], ],
isleading: true); ),
);
}
// Helper to get a specific icon for each payment method.
IconData _getPaymentIcon(String paymentMethod) {
switch (paymentMethod.toLowerCase()) {
case 'visa':
return Icons.credit_card;
case 'frombudget':
return Icons.account_balance_wallet_outlined;
case 'remainder':
return Icons.receipt_long;
case 'cash':
return Icons.money;
default:
return Icons.payment;
}
}
// Helper to get translated or formatted payment method names.
String _getTranslatedPaymentMethod(String paymentMethod) {
switch (paymentMethod) {
case 'Remainder':
return 'Remainder'.tr;
case 'fromBudget':
return 'From Budget'.tr;
default:
return paymentMethod;
}
} }
} }

View File

@@ -59,6 +59,7 @@
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES"> allowLocationSimulation = "YES">
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">