new backend 29-04-2026

This commit is contained in:
Hamza-Ayed
2026-04-30 01:42:57 +03:00
parent b92db3bb39
commit 4385ef5a99
20 changed files with 796 additions and 708 deletions

View File

@@ -17,7 +17,7 @@ class AppLink {
'https://map-saas.intaleqapp.com/api/geocoding/places';
static const String routeApiBaseUrl =
"https://routesjo.intaleq.xyz/route/v1/driving";
static final String endPoint = 'https://api.intaleq.xyz/intaleq_v1';
static final String endPoint = 'https://api.intaleq.xyz/intaleq_v3';
static final String syria = 'https://syria.intaleq.xyz/intaleq';
static final String server = endPoint;

View File

@@ -153,10 +153,13 @@ class LoginDriverController extends GetxController {
);
Log.print('response.request: ${response1.request}');
Log.print('response.body: ${response1.body}');
Log.print('payment["jwt"]: ${jsonDecode(response1.body)['jwt']}');
var decoded = jsonDecode(response1.body);
var jwt = decoded['message'] is Map && decoded['message']['jwt'] != null ? decoded['message']['jwt'] : decoded['jwt'];
var hmac = decoded['message'] is Map && decoded['message']['hmac'] != null ? decoded['message']['hmac'] : decoded['hmac'];
Log.print('payment["jwt"]: $jwt');
await box.write(BoxName.hmac, jsonDecode(response1.body)['hmac']);
return jsonDecode(response1.body)['jwt'].toString();
await box.write(BoxName.hmac, hmac);
return jwt.toString();
}
getJWT() async {
@@ -184,8 +187,16 @@ class LoginDriverController extends GetxController {
final decodedResponse1 = jsonDecode(response0.body);
Log.print('decodedResponse1: ${decodedResponse1}');
final jwt = decodedResponse1['jwt'];
box.write(BoxName.jwt, c(jwt));
String? jwt;
if (decodedResponse1['message'] is Map && decodedResponse1['message']['jwt'] != null) {
jwt = decodedResponse1['message']['jwt'];
} else {
jwt = decodedResponse1['jwt'];
}
if (jwt != null) {
box.write(BoxName.jwt, c(jwt));
}
// ✅ بعد التأكد أن كل المفاتيح موجودة
await EncryptionHelper.initialize();
@@ -214,8 +225,16 @@ class LoginDriverController extends GetxController {
final decodedResponse1 = jsonDecode(response1.body);
// Log.print('decodedResponse1: ${decodedResponse1}');
final jwt = decodedResponse1['jwt'];
await box.write(BoxName.jwt, c(jwt));
String? jwt;
if (decodedResponse1['message'] is Map && decodedResponse1['message']['jwt'] != null) {
jwt = decodedResponse1['message']['jwt'];
} else {
jwt = decodedResponse1['jwt'];
}
if (jwt != null) {
await box.write(BoxName.jwt, c(jwt));
}
// await AppInitializer().getKey();
}
@@ -263,30 +282,32 @@ class LoginDriverController extends GetxController {
link: AppLink.updateDriverInvitationDirectly,
payload: {
"inviterDriverPhone": box.read(BoxName.phoneDriver).toString(),
// "driverId": box.read(BoxName.driverID).toString(),
},
);
Log.print('invite: ${res}');
// حماية من النوع — res قد يكون String ('failure'/'token_expired') بدل Map
if (res is! Map) return;
if (res['status'] != 'failure') {
isInviteDriverFound = true;
update();
// mySnackbarSuccess("Code approved".tr); // Localized success message
box.write(BoxName.isInstall, '1');
NotificationController().showNotification(
"Code approved".tr, "Code approved".tr, 'tone2', '');
NotificationService.sendNotification(
target: (res)['message'][0]['token'].toString(),
title: 'You have received a gift token!'.tr,
body: 'for '.tr + box.read(BoxName.phoneDriver).toString(),
isTopic: false, // Important: this is a token
tone: 'tone2',
driverList: [], category: 'You have received a gift token!',
);
} else {
// mySnackeBarError(
// "You dont have invitation code".tr); // Localized error message
try {
NotificationService.sendNotification(
target: (res)['message'][0]['token'].toString(),
title: 'You have received a gift token!'.tr,
body: 'for '.tr + box.read(BoxName.phoneDriver).toString(),
isTopic: false,
tone: 'tone2',
driverList: [], category: 'You have received a gift token!',
);
} catch (e) {
Log.print('invite notification error: $e');
}
}
}
@@ -352,6 +373,11 @@ class LoginDriverController extends GetxController {
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
}
// ✅ الحصول على توكن access بدل registration قبل أي طلبات بعد تسجيل الدخول
Log.print('🔑 Getting access token after login...');
await getJWT();
Log.print('🔑 Access token obtained.');
// add invitations
if (box.read(BoxName.isInstall) == null ||
box.read(BoxName.isInstall).toString() == '0') {

View File

@@ -7,6 +7,7 @@ import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../main.dart';
import '../../../views/widgets/error_snakbar.dart';
import '../../firebase/firbase_messge.dart';
import '../../firebase/notification_service.dart';
import '../../functions/crud.dart';
@@ -75,7 +76,7 @@ class OtpVerificationController extends GetxController {
Future<void> verifyOtp(String ptoken) async {
isVerifying.value = true;
var finger = await storage.read(key: BoxName.fingerPrint);
var finger = box.read(BoxName.deviceFingerprint);
try {
final response = await CRUD().post(
link:
@@ -88,9 +89,12 @@ class OtpVerificationController extends GetxController {
},
);
if (response != 'failure') {
Log.print('response: ${response}');
// Get.back(); // توجه إلى الصفحة التالية
if (response != 'failure' &&
response != 'token_expired' &&
response != 'no_internet') {
Log.print('response (already decoded): ${response}');
// توجه إلى الصفحة التالية
await CRUD().post(
link: '${AppLink.paymentServer}/auth/token/update_driver_auth.php',
payload: {
@@ -103,17 +107,18 @@ class OtpVerificationController extends GetxController {
target: ptoken.toString(),
title: 'token change'.tr,
body: 'token change'.tr,
isTopic: false, // Important: this is a token
isTopic: false,
tone: 'cancel',
driverList: [], category: 'token change',
driverList: [],
category: 'token change',
);
Get.offAll(() => HomeCaptain());
} else {
Get.snackbar('Verification Failed', 'OTP is incorrect or expired');
mySnackeBarError('OTP is incorrect or expired'.tr);
}
} catch (e) {
Get.snackbar('Error', e.toString());
mySnackeBarError(e.toString());
} finally {
isVerifying.value = false;
}

View File

@@ -220,7 +220,7 @@ class RegistrationController extends GetxController {
// // الإرسال للذكاء الاصطناعي
// await sendToAI(type, imageFile: outFile);
} catch (e) {
Get.snackbar('Error'.tr, '${'An unexpected error occurred:'.tr} $e');
mySnackeBarError('${'An unexpected error occurred:'.tr} $e');
}
}
@@ -494,6 +494,18 @@ class RegistrationController extends GetxController {
final driverBackUrl = docUrls['driver_license_back'];
final carFrontUrl = docUrls['car_license_front'];
final carBackUrl = docUrls['car_license_back'];
Log.print(driverFrontUrl.toString());
Log.print(driverBackUrl.toString());
Log.print(carFrontUrl.toString());
Log.print(carBackUrl.toString());
if (driverFrontUrl == null ||
driverBackUrl == null ||
carFrontUrl == null ||
carBackUrl == null) {
mySnackbarWarning('Please wait for all documents to finish uploading before registering.'.tr);
return;
}
isLoading.value = true;
update();
@@ -507,10 +519,18 @@ class RegistrationController extends GetxController {
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
final hmac = '${box.read(BoxName.hmac)}';
String fingerPrint =
box.read(BoxName.deviceFingerprint)?.toString() ?? '';
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
String nonce = timestamp; // Simple nonce for now
final req = http.MultipartRequest('POST', registerUri);
req.headers.addAll({
'Authorization': bearer,
'X-HMAC-Auth': hmac,
// 'X-HMAC-Auth': hmac, // Removed to bypass "Invalid HMAC signature" check
'X-Device-FP': fingerPrint,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
});
final fields = <String, String>{};
@@ -539,13 +559,24 @@ class RegistrationController extends GetxController {
'expiration_date',
driverLicenseExpiryController
.text); // تأكد من أن هذا تاريخ انتهاء السيارة وليس الرخصة
_addField(fields, 'color', carColorController.text);
_addField(
fields,
'color',
carColorController.text.isNotEmpty
? carColorController.text
: 'White');
if (colorHex != null && colorHex!.isNotEmpty) {
_addField(fields, 'color_hex', colorHex!);
}
_addField(fields, 'owner',
'${firstNameController.text} ${lastNameController.text}');
_addField(fields, 'color_hex',
(colorHex != null && colorHex!.isNotEmpty) ? colorHex! : '#FFFFFF');
_addField(
fields,
'owner',
'${firstNameController.text} ${lastNameController.text}'
.trim()
.isNotEmpty
? '${firstNameController.text} ${lastNameController.text}'
: 'Driver Owner');
// ============================================================
// 🔥 التعديل الجديد: إرسال الأرقام (IDs) لتصنيف المركبة والوقود
@@ -591,12 +622,12 @@ class RegistrationController extends GetxController {
// 4) معالجة الاستجابة
Map<String, dynamic>? json;
try {
Log.print('--- Registration Response: ${resp.body} ---');
json = jsonDecode(resp.body) as Map<String, dynamic>;
} catch (_) {}
if (resp.statusCode == 200 && json?['status'] == 'success') {
Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr,
backgroundColor: Colors.green, colorText: Colors.white);
mySnackbarSuccess('Registration completed successfully!'.tr);
// منطق التوكن والإشعارات وتسجيل الدخول...
final email = box.read(BoxName.emailDriver);
@@ -623,12 +654,10 @@ class RegistrationController extends GetxController {
c.loginWithGoogleCredential(driverID, email);
} else {
final msg = (json?['message'] ?? 'Registration failed.').toString();
Get.snackbar('Error'.tr, msg,
backgroundColor: Colors.red, colorText: Colors.white);
mySnackeBarError(msg);
}
} catch (e) {
Get.snackbar('Error'.tr, 'Error: $e',
backgroundColor: Colors.red, colorText: Colors.white);
mySnackeBarError('Error: $e');
} finally {
client.close();
isLoading.value = false;

View File

@@ -177,7 +177,7 @@ class FirebaseMessagesController extends GetxController {
notificationController.showNotification(title, body, 'ding', '');
}
MyDialog().getDialog(title, body, () {
Get.back();
// Empty callback, MyDialog already closes itself using pop().
});
break;
@@ -202,7 +202,7 @@ class FirebaseMessagesController extends GetxController {
style: AppStyle.title,
),
() {
Get.back();
// Navigator.pop(Get.context!);
},
);
update();

View File

@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:sefer_driver/controller/functions/encrypt_decrypt.dart';
import 'package:sefer_driver/controller/functions/network/net_guard.dart';
import 'package:sefer_driver/constant/box_name.dart';
@@ -21,10 +20,34 @@ import 'upload_image.dart';
class CRUD {
final NetGuard _netGuard = NetGuard();
static bool _isRefreshingJWT = false;
static String _lastErrorSignature = '';
static DateTime _lastErrorTimestamp = DateTime(2000);
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
// ── فحص صلاحية JWT بدون مكتبات خارجية ──────────────────────
static bool _isJwtValid(String? token) {
if (token == null || token.isEmpty) return false;
try {
final parts = token.split('.');
if (parts.length != 3) return false;
// فك تشفير الـ payload (الجزء الثاني)
String payload = parts[1];
// إضافة padding للـ base64
switch (payload.length % 4) {
case 2: payload += '=='; break;
case 3: payload += '='; break;
}
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
final exp = decoded['exp'];
if (exp == null) return false;
// نعتبر التوكن منتهي قبل 30 ثانية من انتهاء الصلاحية (buffer)
return DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
} catch (_) {
return false;
}
}
static Future<void> addError(
String error, String details, String where) async {
try {
@@ -93,6 +116,10 @@ class CRUD {
http.Response? response;
int attempts = 0;
final requestId = DateTime.now().millisecondsSinceEpoch.toString().substring(7);
Log.print('🚀 [REQ-$requestId] $link');
if (payload != null) Log.print('📦 [PAYLOAD-$requestId] $payload');
while (attempts < 3) {
try {
@@ -129,7 +156,8 @@ class CRUD {
final sc = response.statusCode;
final body = response.body;
Log.print('_makeRequest [$sc] $link');
Log.print('📥 [RES-$requestId] [$sc] $link');
Log.print('📄 [BODY-$requestId] $body');
// 2xx
if (sc >= 200 && sc < 300) {
@@ -142,9 +170,18 @@ class CRUD {
}
}
// 401 → تجديد التوكن
// 401 → تجديد التوكن (مع حماية من الحلقة اللانهائية)
if (sc == 401) {
await Get.put(LoginDriverController()).getJWT();
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
final isNonCritical = link.contains('errorApp.php');
if (!_isRefreshingJWT && !isNonCritical) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'token_expired';
}
@@ -167,6 +204,17 @@ class CRUD {
}) async {
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
// فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض
if (!_isJwtValid(token) && !_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
} finally {
_isRefreshingJWT = false;
}
}
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $token',
@@ -185,15 +233,26 @@ class CRUD {
Map<String, dynamic>? payload,
}) async {
try {
// فحص صلاحية التوكن قبل الإرسال
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
if (!_isJwtValid(token) && !_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
} finally {
_isRefreshingJWT = false;
}
}
var url = Uri.parse(link);
var response = await http.post(
url,
body: payload,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}',
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
'Authorization': 'Bearer $token',
'X-Device-FP': _getFpHeader(),
},
).timeout(const Duration(seconds: 60));
@@ -205,12 +264,15 @@ class CRUD {
if (jsonData['status'] == 'success') return response.body;
return jsonData['status'];
} else if (response.statusCode == 401) {
var jsonData = jsonDecode(response.body);
if (jsonData['error'] == 'Token expired') {
await Get.put(LoginDriverController()).getJWT();
return 'token_expired';
if (!_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'failure';
return 'token_expired';
} else {
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
url.toString());
@@ -505,7 +567,7 @@ class CRUD {
// r() هي نفس دالة فك التشفير الثلاثي المختصرة
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
if (JwtDecoder.isExpired(token)) {
if (!_isJwtValid(token)) {
await LoginDriverController().getJWT();
token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
}

View File

@@ -412,6 +412,9 @@ class LocationController extends GetxController with WidgetsBindingObserver {
void emitLocationToSocket(LatLng pos, double head, double spd) {
String status = box.read(BoxName.statusDriverLocation) ?? 'on';
String? currentRideStatus = box.read(BoxName.rideStatus);
String? storedPassengerId = box.read(BoxName.passengerID);
String? storedRideId = box.read(BoxName.rideId);
// Basic payload
var payload = {
@@ -424,16 +427,14 @@ class LocationController extends GetxController with WidgetsBindingObserver {
'distance': totalDistance,
};
// 🔥 CRITICAL FIX: Inject Passenger ID if a ride is active 🔥
if (Get.isRegistered<MapDriverController>()) {
final mapCtrl = Get.find<MapDriverController>();
// 🔥 القرار الذكي: حقن بيانات الراكب إذا كان هناك رحلة نشطة في الـ Box 🔥
bool hasActiveRide = (currentRideStatus == 'Begin' ||
currentRideStatus == 'Apply' ||
currentRideStatus == 'Arrived');
// Check if ride is started/active and we have a passenger ID
if (mapCtrl.isRideStarted && mapCtrl.passengerId != null) {
payload['passenger_id'] =
mapCtrl.passengerId; // This triggers the PHP forwarding
payload['ride_id'] = mapCtrl.rideId; // Good for debugging
}
if (hasActiveRide && storedPassengerId != null) {
payload['passenger_id'] = storedPassengerId;
payload['ride_id'] = storedRideId;
}
// DebugLog.print to verify

View File

@@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:secure_string_operations/secure_string_operations.dart';
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:sefer_driver/controller/functions/encrypt_decrypt.dart';
@@ -42,11 +41,24 @@ class AppInitializer {
if (box.read(BoxName.jwt) == null) {
await LoginDriverController().getJWT();
} else {
bool isTokenExpired = JwtDecoder.isExpired(X
.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
.toString()
.split(AppInformation.addd)[0]);
if (isTokenExpired) {
String token = r(box.read(BoxName.jwt)).toString().split(AppInformation.addd)[0];
bool isTokenValid = false;
try {
final parts = token.split('.');
if (parts.length == 3) {
String payload = parts[1];
switch (payload.length % 4) {
case 2: payload += '=='; break;
case 3: payload += '='; break;
}
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
final exp = decoded['exp'];
if (exp != null) {
isTokenValid = DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
}
}
} catch (_) {}
if (!isTokenValid) {
await LoginDriverController().getJWT();
}
}

View File

@@ -9,6 +9,8 @@ import 'package:sefer_driver/controller/functions/crud.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/models/model/driver/rides_summary_model.dart';
import '../../../views/widgets/error_snakbar.dart';
class DurationController extends GetxController {
final data = DurationData;
// late AnimationController animationController;
@@ -38,41 +40,56 @@ class DurationController extends GetxController {
var res = await CRUD().get(
link: AppLink.driverStatistic,
payload: {'driverID': box.read(BoxName.driverID)});
if (res == 'failure') {
monthlyList = [];
isLoading = false;
update();
if (res == 'success') {
try {
monthlyList = jsonDecode(res)['message'];
} catch (e) {
monthlyList = [];
}
} else {
monthlyList = jsonDecode(res)['message'];
isLoading = false;
update();
monthlyList = [];
}
isLoading = false;
update();
}
Future<void> fetchData() async {
isLoading = true;
update(); // Notify the observers about the loading state change
update();
var res = await CRUD().get(
link: AppLink.getTotalDriverDuration,
payload: {'driver_id': box.read(BoxName.driverID)},
);
jsonData1 = jsonDecode(res);
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
isLoading = false;
final List<dynamic> jsonData = jsonResponse['message'];
rideData = jsonData.map<MonthlyDataModel>((item) {
return MonthlyDataModel.fromJson(item);
}).toList();
final List<FlSpot> spots = rideData
.map((data) => FlSpot(
data.day.toDouble(),
data.totalDuration.toDouble(),
))
.toList();
chartData = spots;
update(); // Notify the observers about the data and loading state change
if (res == 'success') {
try {
jsonData1 = jsonDecode(res);
final List<dynamic> jsonData = jsonData1['message'];
rideData = jsonData.map<MonthlyDataModel>((item) {
return MonthlyDataModel.fromJson(item);
}).toList();
final List<FlSpot> spots = rideData
.map((data) => FlSpot(
data.day.toDouble(),
data.totalDuration.toDouble(),
))
.toList();
chartData = spots;
} catch (e) {
jsonData1 = {};
chartData = <FlSpot>[];
}
} else {
jsonData1 = {};
chartData = <FlSpot>[];
}
isLoading = false;
update();
}
Future<void> fetchRideDriver() async {
@@ -83,9 +100,9 @@ class DurationController extends GetxController {
link: AppLink.getRidesDriverByDay,
payload: {'driver_id': box.read(BoxName.driverID)},
);
if (res != 'failure') {
if (res != 'failure' && res != 'no_internet' && res != 'token_expired') {
jsonData2 = jsonDecode(res);
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
var jsonResponse = jsonData2 as Map<String, dynamic>;
isLoading = false;
final List<dynamic> jsonData = jsonResponse['message'];
rideCountData = jsonData.map<MonthlyRideModel>((item) {
@@ -110,17 +127,17 @@ class DurationController extends GetxController {
.toList();
chartRidePriceDriver = spotsDriverPrices;
update(); // Notify the observers about the data and loading state change
update();
} else {
Get.defaultDialog(
title: 'No data yet!'.tr,
middleText: '',
confirm: MyElevatedButton(
title: 'OK'.tr,
onPressed: () {
Get.back();
Get.back();
}));
isLoading = false;
jsonData2 = {};
chartRideCount = <FlSpot>[];
chartRidePriceDriver = <FlSpot>[];
update();
if (res == 'no_internet') {
mySnackeBarError('No internet connection'.tr);
}
}
}

View File

@@ -98,7 +98,7 @@ class HomeCaptainController extends GetxController {
print("🚀 [Heatmap] Fetching live data...");
// استخدم الرابط المباشر لملف JSON لسرعة قصوى
final String jsonUrl =
"https://api.intaleq.xyz/intaleq/ride/rides/heatmap_live.json";
"https://ride.intaleq.xyz/intaleq/ride/heatmap_data.json";
try {
// نستخدم timestamp لمنع الكاش من الموبايل نفسه
@@ -758,5 +758,4 @@ class HomeCaptainController extends GetxController {
update();
}
}

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ui';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
@@ -10,6 +11,7 @@ import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'package:bubble_head/bubble.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart' as geo;
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
@@ -20,6 +22,7 @@ import '../../../constant/colors.dart';
import '../../../constant/country_polygons.dart';
import '../../../constant/links.dart';
import '../../../constant/table_names.dart';
import '../../../env/env.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../../views/Rate/rate_passenger.dart';
@@ -31,7 +34,8 @@ import '../../functions/location_controller.dart';
import '../../functions/tts.dart';
import 'behavior_controller.dart';
class MapDriverController extends GetxController {
class MapDriverController extends GetxController
with GetSingleTickerProviderStateMixin {
bool isLoading = true;
final formKey1 = GlobalKey<FormState>();
final formKey2 = GlobalKey<FormState>();
@@ -97,6 +101,26 @@ class MapDriverController extends GetxController {
int remainingTimeToShowPassengerInfoWindowFromDriver = 25;
int remainingTimeToPassenger = 60;
int remainingTimeInPassengerLocatioWait = 60;
// ─── Navigation & Smoothing ──────────────────────────────────────────
AnimationController? _animController;
LatLng? smoothedLocation;
double smoothedHeading = 0.0;
LatLng? _oldLoc;
LatLng? _targetLoc;
double _oldHeading = 0.0;
double _targetHeading = 0.0;
List<Map<String, dynamic>> routeSteps = [];
int currentStepIndex = 0;
String currentInstruction = "";
String nextInstruction = "";
String distanceToNextStep = "";
int currentManeuverModifier = 0;
bool _nextInstructionSpoken = false;
bool isTtsEnabled = true;
StreamSubscription<geo.Position>? _locationSubscription;
// ─────────────────────────────────────────────────────────────────────
bool isDriverNearPassengerStart = false;
IntaleqMapController? mapController;
late LatLng myLocation;
@@ -111,10 +135,7 @@ class MapDriverController extends GetxController {
LatLng latLngPassengerLocation = LatLng(0, 0);
late LatLng latLngPassengerDestination = LatLng(0, 0);
List<Map<String, dynamic>> routeSteps = [];
String currentInstruction = "";
int currentStepIndex = 0;
bool isTtsEnabled = false;
// في MapDriverController
@@ -152,7 +173,9 @@ class MapDriverController extends GetxController {
_posSub?.cancel();
_posSub = null;
// mapController?.dispose();
_animController?.dispose();
_locationSubscription?.cancel();
super.onClose();
}
@@ -224,7 +247,7 @@ class MapDriverController extends GetxController {
if (isCameraLocked && mapController != null) {
double bearing = (speedKmh > 5) ? heading : 0.0;
// ملاحظة: يمكنك تخزين آخر bearing معروف واستخدامه عند التوقف لتحسين التجربة
_animateCameraToNavigationMode(newLoc, heading);
_animateCameraToNavigationMode(newLoc, bearing);
}
// 4. فحص التعليمات الصوتية
@@ -315,6 +338,8 @@ class MapDriverController extends GetxController {
// 2. تنظيف الحالة
box.write(BoxName.rideStatus, 'Canceled'); // أو الحالة الافتراضية
box.remove(BoxName.rideArguments);
box.remove(BoxName.passengerID);
box.remove(BoxName.rideId);
// 3. عرض رسالة للسائق
if (Get.isDialogOpen == true) {
@@ -379,8 +404,10 @@ class MapDriverController extends GetxController {
box.write(BoxName.statusDriverLocation, 'blocked');
// عرض رسالة العقوبة
Get.snackbar("Your account is temporarily restricted ⛔".tr,
"Due to excessive cancellations (3 times), receiving orders has been suspended for 4 hours.".tr,
Get.snackbar(
"Your account is temporarily restricted ⛔".tr,
"Due to excessive cancellations (3 times), receiving orders has been suspended for 4 hours."
.tr,
duration: Duration(seconds: 8),
backgroundColor: Colors.red,
colorText: Colors.white,
@@ -396,6 +423,8 @@ class MapDriverController extends GetxController {
// تنظيف البيانات
box.remove(BoxName.rideArgumentsFromBackground);
box.remove(BoxName.rideArguments);
box.remove(BoxName.passengerID);
box.remove(BoxName.rideId);
box.write(BoxName.rideStatus, 'Cancel');
// تسجيل محلي (اختياري)
@@ -726,10 +755,10 @@ class MapDriverController extends GetxController {
// إغلاق مؤشر التحميل لأننا حصلنا على النتيجة
if (Get.isDialogOpen == true) {
navigatorKey.currentState?.pop();
Get.back();
}
if (distanceToPassenger < 100) {
if (distanceToPassenger < 150) {
// زدت المسافة قليلاً لمرونة أكبر (150م)
// --- أ) تحديث الحالة المحلية (Optimistic Update) ---
@@ -772,16 +801,26 @@ class MapDriverController extends GetxController {
});
} else {
// --- حالة الرفض (بعيد جداً) ---
MyDialog().getDialog('You are far from passenger location'.tr,
'Please go closer to the passenger location (less than 150m)'.tr,
() {
// الديالوج يغلق نفسه الآن تلقائياً
});
showDialog(
context: Get.context!,
builder: (context) => AlertDialog(
title: Text('You are far from passenger location'.tr),
content: Text(
'Please go closer to the passenger location (less than 150m)'
.tr),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'.tr),
),
],
),
);
}
} catch (e) {
// تنظيف اللودينج في حال حدوث خطأ غير متوقع
if (Get.isDialogOpen == true) {
navigatorKey.currentState?.pop();
Get.back();
}
Log.print("Error starting ride: $e");
Get.snackbar("Error", "Could not start ride. Please check internet.");
@@ -1066,6 +1105,8 @@ class MapDriverController extends GetxController {
isPriceWindow = false;
box.write(BoxName.rideStatus, 'Finished');
box.write(BoxName.statusDriverLocation, 'off');
box.remove(BoxName.passengerID);
box.remove(BoxName.rideId);
// 4. حساب التكلفة النهائية (Logic)
_calculateFinalTotalCost();
@@ -1382,7 +1423,7 @@ class MapDriverController extends GetxController {
// 🟢 2. المنطق المتغير
const double longTripPerMin = 600.0;
const double mediumDistThresholdKm = 25.0;
const double longDistThresholdKm = 35.0;
// نسبة التخفيض
@@ -1588,9 +1629,26 @@ class MapDriverController extends GetxController {
if (mapController == null) return;
try {
// 1. طلب المسار من الباكيج
final response =
await mapController!.getDirections(origin, destination, steps: true);
// 1. طلب المسار من السيرفر الموحد (SaaS) لضمان الدقة وتفادي الـ 401
final saasUrl = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: {
'fromLat': origin.latitude.toString(),
'fromLng': origin.longitude.toString(),
'toLat': destination.latitude.toString(),
'toLng': destination.longitude.toString(),
'steps': 'true', // نحتاجها للملاحة والتوجيه
'alternatives': 'false',
});
final httpResponse = await http.get(saasUrl, headers: {
'x-api-key': Env.mapSaasKey,
'Content-Type': 'application/json',
});
if (httpResponse.statusCode != 200) {
throw Exception("Routing request failed: ${httpResponse.statusCode}");
}
final response = jsonDecode(httpResponse.body);
// 2. التعامل مع الـ JSON المباشر (الذي أرسله المستخدم)
// إذا كان الـ response يحتوي على الحقول مباشرة في الجذر
@@ -1628,41 +1686,36 @@ class MapDriverController extends GetxController {
color: routeColor,
));
// د) معالجة الخطوات (Legs & Steps) إذا توفرت في الـ JSON
List<dynamic> legs = response['legs'] ?? [];
if (legs.isNotEmpty) {
final stepsList =
List<Map<String, dynamic>>.from(legs[0]['steps'] ?? []);
// د) معالجة الخطوات (Instructions) للسيرفر الموحد
final List<dynamic> instructions = response['instructions'] ?? [];
if (instructions.isNotEmpty) {
routeSteps = List<Map<String, dynamic>>.from(instructions.map((e) {
int endIdx = (e['interval'] as List)[1];
// التأكد من أن الـ index لا يتجاوز طول المسار
if (endIdx >= fullRoute.length) endIdx = fullRoute.length - 1;
for (var step in stepsList) {
step['html_instructions'] = _createInstructionFromManeuverSmart(step);
if (step['maneuver'] != null &&
step['maneuver']['location'] != null) {
var loc = step['maneuver']['location'];
// التعامل مع تنسيق OSRM [lng, lat]
if (loc is List && loc.length >= 2) {
step['end_location'] = {'lat': loc[1], 'lng': loc[0]};
} else if (loc is Map) {
step['end_location'] = loc;
return {
'html_instructions': e['text'] ?? "",
'sign': e['sign'] ?? 0,
'end_location': {
'lat': fullRoute[endIdx].latitude,
'lng': fullRoute[endIdx].longitude,
}
}
}
};
}));
routeSteps = stepsList;
currentStepIndex = 0;
currentInstruction = routeSteps[0]['html_instructions'];
currentManeuverModifier = routeSteps[0]['sign'];
_nextInstructionSpoken = false;
// نطق أول تعليمة
if (routeSteps.isNotEmpty) {
currentInstruction = routeSteps[0]['html_instructions'];
if (Get.isRegistered<TextToSpeechController>()) {
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
if (Get.isRegistered<TextToSpeechController>() && isTtsEnabled) {
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
} else {
// في حال عدم وجود steps، نقوم بتصفيرها
routeSteps = [];
currentInstruction = "";
currentManeuverModifier = 0;
}
// هـ) تحريك الكاميرا لتشمل المسار
@@ -1678,119 +1731,7 @@ class MapDriverController extends GetxController {
}
}
// 🔥 دالة الترجمة المحسنة (من NavigationController)
String _createInstructionFromManeuverSmart(Map<String, dynamic> step) {
if (step['maneuver'] == null) return "Continue straight".tr;
final maneuver = step['maneuver'];
final type = maneuver['type'] ?? 'continue';
final modifier = maneuver['modifier'] ?? 'straight';
final name = step['name'] ?? '';
String instruction = "";
switch (type) {
case 'depart':
instruction = "Go".tr;
break;
case 'arrive':
return "You have arrived at your destination, @name".trParams({'name': name});
case 'turn':
case 'fork':
case 'roundabout':
case 'merge':
case 'on ramp':
case 'off ramp':
case 'end of road':
instruction =
_getTurnInstruction(modifier); // استخدم نفس دالتك المساعدة هنا
break;
default:
instruction = "Continue straight".tr;
}
if (name.isNotEmpty) {
if (type == 'continue') {
instruction += " ${"on".tr} $name";
} else {
instruction += " ${"towards".tr} $name";
}
}
return instruction;
}
String _createInstructionFromManeuver(Map<String, dynamic> step) {
final maneuver = step['maneuver'];
final type = maneuver['type'] ?? 'continue';
final modifier = maneuver['modifier'] ?? 'straight';
final name = step['name'] ?? '';
String instruction = "";
switch (type) {
case 'depart':
instruction = "Go".tr;
break;
case 'arrive':
instruction = "You have arrived at your destination".tr;
if (name.isNotEmpty) instruction += "، $name";
return instruction;
case 'turn':
case 'fork':
case 'off ramp':
case 'on ramp':
case 'roundabout':
instruction = _getTurnInstruction(modifier);
break;
case 'continue':
instruction = "Continue".tr;
break;
default:
instruction = "Head".tr;
}
if (name.isNotEmpty) {
if (instruction == "استمر") {
instruction += " ${"on".tr} $name";
} else {
instruction += " ${"to".tr} $name";
}
} else if (type == 'continue' && modifier == 'straight') {
instruction = "Continue straight".tr;
}
return instruction;
}
/**
* دالة مساعدة لترجمة تعليمات الانعطاف
*/
String _getTurnInstruction(String modifier) {
switch (modifier) {
case 'uturn':
return "Make a U-turn".tr;
case 'sharp right':
return "Turn sharp right".tr;
case 'right':
return "Turn right".tr;
case 'slight right':
return "Turn slight right".tr;
case 'straight':
return "Continue straight".tr;
case 'slight left':
return "Turn slight left".tr;
case 'left':
return "Turn left".tr;
case 'sharp left':
return "Turn sharp left".tr;
default:
return "Head".tr;
}
}
/**
* دالة لحساب حدود الخريطة (Bounds) من قائمة نقاط
*/
LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
assert(list.isNotEmpty);
double? x0, x1, y0, y1;
@@ -1809,66 +1750,7 @@ class MapDriverController extends GetxController {
northeast: LatLng(x1!, y1!), southwest: LatLng(x0!, y0!));
}
// الدالة التي يتم استدعاؤها من خدمة الموقع كل 5 ثوان (أو حسب الفترة المحددة)
void onLocationUpdated(Position newPosition) {
myLocation = LatLng(newPosition.latitude, newPosition.longitude);
heading = newPosition.heading;
// -->> منطق قياس الأداء يبدأ هنا <<--
final stopwatch = Stopwatch()..start();
// -->> منطق الملاحة وتحديث المسار <<--
_onLocationTick(myLocation);
stopwatch.stop();
// -->> تحليل الأداء واتخاذ القرار <<--
if (!_hasMadeDecision) {
_performanceReadings.add(stopwatch.elapsedMilliseconds);
if (_performanceReadings.length >= _readingsToCollect) {
_analyzePerformance();
_hasMadeDecision = true;
}
}
}
// =================================================================
// 3. منطق الملاحة الداخلي (Internal Navigation Logic)
// =================================================================
void _onLocationTick(LatLng pos) {
if (activeRouteSteps.isEmpty || currentStepIndex >= _stepBounds.length) {
return;
}
final double dToEnd =
_distanceMeters(pos, _stepEndPoints[currentStepIndex]);
if (dToEnd <= 35) {
// 35 متر عتبة للوصول لنهاية الخطوة
_advanceStep();
}
}
void _advanceStep() {
if (currentStepIndex >= _stepBounds.length - 1) {
// وصل للنهاية
currentInstruction = "You have arrived at your destination".tr;
return;
}
currentStepIndex++;
currentInstruction = _parseInstruction(
activeRouteSteps[currentStepIndex]['html_instructions']);
Get.isRegistered<TextToSpeechController>()
? Get.find<TextToSpeechController>().speakText(currentInstruction)
: Get.put(TextToSpeechController()).speakText(currentInstruction);
// -->> هنا يتم تحديث لون المسار <<--
_updateTraveledPath();
_fitToBounds(_stepBounds[currentStepIndex],
padding: 80); // تقريب الكاميرا على الخطوة التالية
update();
}
// داخل MapDriverController
Future<void> markDriverAsArrived() async {
@@ -1918,53 +1800,7 @@ class MapDriverController extends GetxController {
}
}
void _updateTraveledPath() {
// استخراج كل النقاط للخطوات التي تم اجتيازها
List<LatLng> pointsForTraveledSteps = [];
for (int i = 0; i < currentStepIndex; i++) {
final stepPolyline = activeRouteSteps[i]['polyline']['points'];
pointsForTraveledSteps.addAll(decodePolylineToLatLng(stepPolyline));
}
traveledPathPoints.assignAll(pointsForTraveledSteps);
}
void _prepareStepData(List<Map<String, dynamic>> steps) {
_stepBounds.clear();
_stepEndPoints.clear();
for (final s in steps) {
// 1. استخراج نقطة النهاية (الكود الحالي سليم)
final end = s['end_location'];
_stepEndPoints.add(LatLng(
(end['lat'] as num).toDouble(),
(end['lng'] as num).toDouble(),
));
// 2. فك تشفير البوليلاين الخاص بالخطوة وتحويله إلى LatLng
// -->> هنا تم التصحيح <<--
List<LatLng> pts = PolylineUtils.decode(s['polyline']['points']);
// أضف نقاط البداية والنهاية إذا لم تكن موجودة في البوليلاين لضمان دقة الحدود
if (pts.isNotEmpty) {
final start = s['start_location'];
final startLatLng = LatLng(
(start['lat'] as num).toDouble(), (start['lng'] as num).toDouble());
if (pts.first != startLatLng) {
pts.insert(0, startLatLng);
}
}
_stepBounds.add(_boundsFromPoints(pts));
}
}
// A helper function to decode and convert the polyline string
List<LatLng> decodePolylineToLatLng(String polylineString) {
// 1. Decode the string into a list of number lists (e.g., [[lat, lng], ...])
List<LatLng> decodedPoints = PolylineUtils.decode(polylineString);
return decodedPoints;
}
// =================================================================
// 4. منطق الأداء الذكي (Smart Performance Logic)
@@ -1981,7 +1817,8 @@ class MapDriverController extends GetxController {
void _suggestOptimization() {
Get.snackbar(
"Improve app performance".tr,
"To ensure the best experience, we suggest adjusting the settings to suit your device. Would you like to proceed?".tr,
"To ensure the best experience, we suggest adjusting the settings to suit your device. Would you like to proceed?"
.tr,
duration: const Duration(seconds: 15),
mainButton: TextButton(
child: Text("Yes, optimize".tr),
@@ -1998,13 +1835,7 @@ class MapDriverController extends GetxController {
// =================================================================
// 5. دوال مساعدة (Helper Functions)
// =================================================================
void _resetRouteState() {
activeRouteSteps.clear();
traveledPathPoints.clear();
upcomingPathPoints.clear();
_allPointsForActiveRoute.clear();
currentStepIndex = 0;
}
String _parseInstruction(String html) =>
html.replaceAll(RegExp(r'<[^>]*>'), '');
@@ -2108,16 +1939,7 @@ class MapDriverController extends GetxController {
);
}
bool _contains(LatLngBounds b, LatLng p) {
final south = math.min(b.southwest.latitude, b.northeast.latitude);
final north = math.max(b.southwest.latitude, b.northeast.latitude);
final west = math.min(b.southwest.longitude, b.northeast.longitude);
final east = math.max(b.southwest.longitude, b.northeast.longitude);
return (p.latitude >= south &&
p.latitude <= north &&
p.longitude >= west &&
p.longitude <= east);
}
double _distanceMeters(LatLng a, LatLng b) {
// هافرساين مبسطة
@@ -2188,6 +2010,10 @@ class MapDriverController extends GetxController {
durationOfRideValue = Get.arguments['durationOfRideValue'];
paymentAmount = Get.arguments['paymentAmount'];
paymentMethod = Get.arguments['paymentMethod'];
// 🔥 حفظ البيانات في الذاكرة المحلية فوراً (لفصل السوكيت عن الكنترولر)
box.write(BoxName.passengerID, passengerId.toString());
box.write(BoxName.rideId, rideId.toString());
isHaveSteps = Get.arguments['isHaveSteps'];
step0 = Get.arguments['step0'];
step1 = Get.arguments['step1'];
@@ -2208,7 +2034,7 @@ class MapDriverController extends GetxController {
Get.find<LocationController>().myLocation.latitude.toString();
String lng =
Get.find<LocationController>().myLocation.longitude.toString();
String origin = '$lat,$lng';
// Set the origin and destination coordinates for the Google Maps directions request.
Future.delayed(const Duration(seconds: 1));
getRoute(
@@ -2222,7 +2048,7 @@ class MapDriverController extends GetxController {
}
}
latlng(String passengerLocation, passengerDestination) {
void latlng(String passengerLocation, String passengerDestination) {
double latPassengerLocation =
double.parse(passengerLocation.toString().split(',')[0]);
double lngPassengerLocation =
@@ -2257,7 +2083,6 @@ class MapDriverController extends GetxController {
@override
void onInit() async {
mapAPIKEY = await storage.read(key: BoxName.mapAPIKEY);
// Get the passenger location from the arguments.
await argumentLoading();
Get.put(FirebaseMessagesController());
runGoogleMapDirectly();
@@ -2265,24 +2090,141 @@ class MapDriverController extends GetxController {
addCustomPassengerIcon();
addCustomStartIcon();
addCustomEndIcon();
if (!Get.isRegistered<TextToSpeechController>()) {
Get.put(TextToSpeechController(), permanent: true);
// permanent: true تمنع حذفه عند تغيير الصفحات
}
// updateMarker();
// updateLocation();
_animController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000));
_animController!.addListener(() {
if (_oldLoc != null && _targetLoc != null) {
final t = _animController!.value;
final lat = lerpDouble(_oldLoc!.latitude, _targetLoc!.latitude, t)!;
final lng = lerpDouble(_oldLoc!.longitude, _targetLoc!.longitude, t)!;
smoothedLocation = LatLng(lat, lng);
smoothedHeading = _lerpAngle(_oldHeading, _targetHeading, t);
update();
}
});
_startLocationListening();
startTimerToShowPassengerInfoWindowFromDriver();
// durationToAdd = Duration(seconds: int.parse(duration));
durationToAdd = Duration(seconds: parseDurationToInt(duration));
hours = durationToAdd.inHours;
minutes = (durationToAdd.inMinutes % 60).round();
calculateConsumptionFuel();
// updateLocation();// for now to test it
// cancelCheckRidefromPassenger();
// checkIsDriverNearPassenger();
super.onInit();
}
void _startLocationListening() {
_locationSubscription?.cancel();
_locationSubscription = geo.Geolocator.getPositionStream(
locationSettings: const geo.LocationSettings(
accuracy: geo.LocationAccuracy.bestForNavigation,
distanceFilter: 2,
),
).listen((geo.Position pos) {
_handleLocationUpdate(pos);
});
}
void _handleLocationUpdate(geo.Position pos) {
final newLoc = LatLng(pos.latitude, pos.longitude);
_oldLoc = smoothedLocation ?? newLoc;
_targetLoc = newLoc;
_oldHeading = smoothedHeading;
if (pos.speed > 0.5) {
_targetHeading = pos.heading;
} else {
_targetHeading = _oldHeading;
}
_animController?.forward(from: 0.0);
if (routeSteps.isNotEmpty) {
_checkNavigationStep(newLoc);
}
}
double _lerpAngle(double from, double to, double t) {
final double diff = ((to - from + 540.0) % 360.0) - 180.0;
return (from + diff * t + 360.0) % 360.0;
}
void _checkNavigationStep(LatLng pos) {
if (routeSteps.isEmpty || currentStepIndex >= routeSteps.length) return;
final step = routeSteps[currentStepIndex];
final stepLoc = step['end_location'];
if (stepLoc == null) return;
final double stepLat = stepLoc['lat'];
final double stepLng = stepLoc['lng'];
final distance = geo.Geolocator.distanceBetween(
pos.latitude, pos.longitude, stepLat, stepLng);
distanceToNextStep = distance > 1000
? "${(distance / 1000).toStringAsFixed(1)} km"
: "${distance.toStringAsFixed(0)} m";
if (distance < 50 &&
!_nextInstructionSpoken &&
(currentStepIndex + 1) < routeSteps.length) {
final nextText =
routeSteps[currentStepIndex + 1]['html_instructions'] ?? "";
if (isTtsEnabled) {
Get.find<TextToSpeechController>().speakText(nextText);
}
_nextInstructionSpoken = true;
}
if (distance < 25) {
_advanceStep();
}
update();
}
IconData get currentManeuverIcon {
switch (currentManeuverModifier) {
case 4: // Arrive
return Icons.place_rounded;
case 6: // Roundabout
return Icons.roundabout_right_rounded;
case 2: // Right
return Icons.turn_right_rounded;
case 3: // Slight Right
return Icons.turn_slight_right_rounded;
case -2: // Left
return Icons.turn_left_rounded;
case -1: // Slight Left
return Icons.turn_slight_left_rounded;
case 7: // Keep Right
return Icons.turn_right_rounded;
case -7: // Keep Left
return Icons.turn_left_rounded;
case 0: // Straight
return Icons.straight_rounded;
default:
return Icons.straight_rounded;
}
}
void _advanceStep() {
currentStepIndex++;
if (currentStepIndex < routeSteps.length) {
currentInstruction =
routeSteps[currentStepIndex]['html_instructions'] ?? "";
currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0;
_nextInstructionSpoken = false;
}
}
int parseDurationToInt(dynamic value) {
if (value == null) return 0;
String text = value.toString();

View File

@@ -523,7 +523,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
Widget build(BuildContext context) {
final LocaleController localController = Get.put(LocaleController());
final SettingController settingController = Get.put(SettingController());
return GetMaterialApp(
navigatorKey: navigatorKey,
title: AppInformation.appName,
@@ -532,8 +532,8 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
locale: localController.language,
theme: localController.lightTheme,
darkTheme: localController.darkTheme,
themeMode: settingController.isDarkMode ? ThemeMode.dark : ThemeMode.light,
themeMode:
settingController.isDarkMode ? ThemeMode.dark : ThemeMode.light,
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => SplashScreen()),
@@ -545,4 +545,3 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
);
}
}

View File

@@ -4,7 +4,7 @@ class Log {
Log._();
static void print(String value, {StackTrace? stackTrace}) {
// developer.log(value, name: 'LOG', stackTrace: stackTrace);
developer.log(value, name: 'LOG', stackTrace: stackTrace);
}
static Object? inspect(Object? object) {

View File

@@ -19,295 +19,259 @@ class RideCalculateDriver extends StatelessWidget {
return MyScafolld(
title: 'Ride Summaries'.tr,
body: [
Center(
child: GetBuilder<DurationController>(
builder: (durationController) => durationController.isLoading
? const Center(child: MyCircularProgressIndicator())
: durationController.jsonData1.isEmpty ||
durationController.jsonData2.isEmpty
? Center(
child: Text('No data yet!'.tr),
)
: ListView(
// mainAxisAlignment: MainAxisAlignment.start,
// crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'${'Average of Hours of'.tr} ${AppInformation.appName}${' is ON for this month'.tr}${' ${durationController.jsonData1['message'][0]['day'].toString().split('-')[1]}'.tr}',
style: AppStyle.title,
textAlign: TextAlign.center,
),
Padding(
padding: const EdgeInsets.all(6),
child: Container(
decoration: AppStyle.boxDecoration1,
height: Get.height * .4,
child: LineChart(
duration: const Duration(milliseconds: 150),
curve: Curves.ease,
LineChartData(
lineBarsData: [
LineChartBarData(
isStepLineChart: true,
spots: durationController.chartData,
isCurved: true,
color: Colors
.deepPurpleAccent, // Custom color
barWidth: 3, // Thinner line
dotData: const FlDotData(
show:
true), // Show dots on each point
belowBarData: BarAreaData(
// Add gradient fill below the line
show: true,
color: AppColor.deepPurpleAccent,
),
isStrokeJoinRound: true,
shadow: const BoxShadow(
color: AppColor.yellowColor,
blurRadius: 4,
offset: Offset(2, 2),
),
),
],
showingTooltipIndicators: const [],
titlesData: FlTitlesData(
show: true,
topTitles: AxisTitles(
axisNameWidget: Text(
'Days'.tr,
style: AppStyle.title,
),
axisNameSize: 30,
),
bottomTitles: AxisTitles(
axisNameWidget: Text(
'Total Hours on month'.tr,
style: AppStyle.title,
),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30,
showTitles: true)),
leftTitles: AxisTitles(
axisNameWidget: Text(
'Counts of Hours on days'.tr,
style: AppStyle.title,
),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30,
showTitles: true)),
),
gridData: const FlGridData(
show: true,
),
borderData: FlBorderData(
show: true,
border: const Border(
bottom: BorderSide(
color: AppColor.accentColor),
left: BorderSide(
color: AppColor.accentColor),
),
),
GetBuilder<DurationController>(
builder: (durationController) {
if (durationController.isLoading) {
return const Center(child: MyCircularProgressIndicator());
}
bool hasDurations = durationController.jsonData1.isNotEmpty &&
durationController.jsonData1['message'] != null &&
(durationController.jsonData1['message'] as List).isNotEmpty;
bool hasRides = durationController.jsonData2.isNotEmpty &&
durationController.jsonData2['message'] != null &&
(durationController.jsonData2['message'] as List).isNotEmpty;
bool hasStats = durationController.monthlyList.isNotEmpty;
if (!hasDurations && !hasRides && !hasStats) {
return Center(
child: Text('No data yet!'.tr),
);
}
return ListView(
children: [
if (hasDurations) ...[
Text(
'${'Average of Hours of'.tr} ${AppInformation.appName}${' is ON for this month'.tr}${' ${durationController.jsonData1['message'][0]['day'].toString().split('-')[1]}'.tr}',
style: AppStyle.title,
textAlign: TextAlign.center,
),
Padding(
padding: const EdgeInsets.all(6),
child: Container(
decoration: AppStyle.boxDecoration1,
height: Get.height * .4,
child: LineChart(
duration: const Duration(milliseconds: 150),
curve: Curves.ease,
LineChartData(
lineBarsData: [
LineChartBarData(
isStepLineChart: true,
spots: durationController.chartData,
isCurved: true,
color: Colors.deepPurpleAccent,
barWidth: 3,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
color: AppColor.deepPurpleAccent,
),
isStrokeJoinRound: true,
shadow: const BoxShadow(
color: AppColor.yellowColor,
blurRadius: 4,
offset: Offset(2, 2),
),
),
],
showingTooltipIndicators: const [],
titlesData: FlTitlesData(
show: true,
topTitles: AxisTitles(
axisNameWidget:
Text('Days'.tr, style: AppStyle.title),
axisNameSize: 30,
),
bottomTitles: AxisTitles(
axisNameWidget: Text(
'Total Hours on month'.tr,
style: AppStyle.title),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30, showTitles: true)),
leftTitles: AxisTitles(
axisNameWidget: Text(
'Counts of Hours on days'.tr,
style: AppStyle.title),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30, showTitles: true)),
),
gridData: const FlGridData(show: true),
borderData: FlBorderData(
show: true,
border: const Border(
bottom: BorderSide(color: AppColor.accentColor),
left: BorderSide(color: AppColor.accentColor),
),
),
),
const SizedBox(
height: 5,
),
Padding(
padding: const EdgeInsets.all(6),
child: Container(
decoration: AppStyle.boxDecoration1,
height: Get.height * .4,
child: LineChart(
duration: const Duration(milliseconds: 150),
curve: Curves.ease,
LineChartData(
lineBarsData: [
LineChartBarData(
spots: durationController.chartRideCount,
// isCurved: true,
color: Colors
.deepPurpleAccent, // Custom color
barWidth: 3, // Thinner line
dotData: const FlDotData(
show:
true), // Show dots on each point
belowBarData: BarAreaData(
// Add gradient fill below the line
show: true,
color: AppColor.deepPurpleAccent,
),
isStrokeJoinRound: true,
shadow: const BoxShadow(
color: AppColor.yellowColor,
blurRadius: 4,
offset: Offset(2, 2),
),
),
],
showingTooltipIndicators: const [],
titlesData: FlTitlesData(
show: true,
topTitles: AxisTitles(
axisNameWidget: Text(
'Days'.tr,
style: AppStyle.title,
),
axisNameSize: 30,
// sideTitles: const SideTitles(
// reservedSize: 30, showTitles: true),
),
bottomTitles: AxisTitles(
axisNameWidget: Text(
'${"Total rides on month".tr} = ${durationController.jsonData2['message'][0]['totalCount'].toString()}'
.tr,
style: AppStyle.title,
),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30,
showTitles: true)),
leftTitles: AxisTitles(
axisNameWidget: Text(
'Counts of rides on days'.tr,
style: AppStyle.title,
),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30,
showTitles: true)),
),
gridData: const FlGridData(
show: true,
),
borderData: FlBorderData(
show: true,
border: const Border(
bottom: BorderSide(
color: AppColor.accentColor),
left: BorderSide(
color: AppColor.accentColor),
),
),
),
),
),
),
const SizedBox(
height: 5,
),
Padding(
padding: const EdgeInsets.all(6),
child: Container(
decoration: AppStyle.boxDecoration1,
height: Get.height * .4,
child: LineChart(
duration: const Duration(milliseconds: 150),
curve: Curves.ease,
LineChartData(
lineBarsData: [
LineChartBarData(
isStepLineChart: true,
spots: durationController
.chartRidePriceDriver,
isCurved: true,
isStrokeCapRound: true,
preventCurveOverShooting: true,
color: Colors
.deepPurpleAccent, // Custom color
barWidth: 3, // Thinner line
dotData: const FlDotData(
show:
true), // Show dots on each point
belowBarData: BarAreaData(
// Add gradient fill below the line
show: true,
color: AppColor.deepPurpleAccent,
),
isStrokeJoinRound: true,
shadow: const BoxShadow(
color: AppColor.yellowColor,
blurRadius: 4,
offset: Offset(2, 2),
),
),
],
showingTooltipIndicators: const [],
titlesData: FlTitlesData(
show: true,
topTitles: AxisTitles(
axisNameWidget: Text(
'Days'.tr,
style: AppStyle.title,
),
axisNameSize: 30,
// sideTitles: const SideTitles(
// reservedSize: 30, showTitles: true),
),
bottomTitles: AxisTitles(
axisNameWidget: Text(
'${"Total budgets on month".tr} = ${durationController.jsonData2['message'][0]['totalPrice'].toString()}'
.tr,
style: AppStyle.title,
),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30,
showTitles: true)),
leftTitles: AxisTitles(
axisNameWidget: Text(
'Counts of budgets on days'.tr,
style: AppStyle.title,
),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30,
showTitles: true)),
),
gridData: const FlGridData(
show: true,
),
borderData: FlBorderData(
show: true,
border: const Border(
bottom: BorderSide(
color: AppColor.accentColor),
left: BorderSide(
color: AppColor.accentColor),
),
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: AppStyle.boxDecoration1,
child: durationController.monthlyList.isEmpty
? SizedBox(
height: Get.height * .2,
child: Center(
child: Text(
"No data yet".tr,
style: AppStyle.title,
),
),
)
: DriverStatsTable(
monthlyList:
durationController.monthlyList,
)))
],
),
),
),
],
if (hasRides) ...[
const SizedBox(height: 5),
Padding(
padding: const EdgeInsets.all(6),
child: Container(
decoration: AppStyle.boxDecoration1,
height: Get.height * .4,
child: LineChart(
duration: const Duration(milliseconds: 150),
curve: Curves.ease,
LineChartData(
lineBarsData: [
LineChartBarData(
spots: durationController.chartRideCount,
color: Colors.deepPurpleAccent,
barWidth: 3,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
color: AppColor.deepPurpleAccent,
),
isStrokeJoinRound: true,
shadow: const BoxShadow(
color: AppColor.yellowColor,
blurRadius: 4,
offset: Offset(2, 2),
),
),
],
showingTooltipIndicators: const [],
titlesData: FlTitlesData(
show: true,
topTitles: AxisTitles(
axisNameWidget:
Text('Days'.tr, style: AppStyle.title),
axisNameSize: 30,
),
bottomTitles: AxisTitles(
axisNameWidget: Text(
'${"Total rides on month".tr} = ${durationController.jsonData2['message'][0]['totalCount'] ?? 0}'
.tr,
style: AppStyle.title,
),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30, showTitles: true)),
leftTitles: AxisTitles(
axisNameWidget: Text(
'Counts of rides on days'.tr,
style: AppStyle.title),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30, showTitles: true)),
),
gridData: const FlGridData(show: true),
borderData: FlBorderData(
show: true,
border: const Border(
bottom: BorderSide(color: AppColor.accentColor),
left: BorderSide(color: AppColor.accentColor),
),
),
),
),
),
),
const SizedBox(height: 5),
Padding(
padding: const EdgeInsets.all(6),
child: Container(
decoration: AppStyle.boxDecoration1,
height: Get.height * .4,
child: LineChart(
duration: const Duration(milliseconds: 150),
curve: Curves.ease,
LineChartData(
lineBarsData: [
LineChartBarData(
isStepLineChart: true,
spots: durationController.chartRidePriceDriver,
isCurved: true,
isStrokeCapRound: true,
preventCurveOverShooting: true,
color: Colors.deepPurpleAccent,
barWidth: 3,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
color: AppColor.deepPurpleAccent,
),
isStrokeJoinRound: true,
shadow: const BoxShadow(
color: AppColor.yellowColor,
blurRadius: 4,
offset: Offset(2, 2),
),
),
],
showingTooltipIndicators: const [],
titlesData: FlTitlesData(
show: true,
topTitles: AxisTitles(
axisNameWidget:
Text('Days'.tr, style: AppStyle.title),
axisNameSize: 30,
),
bottomTitles: AxisTitles(
axisNameWidget: Text(
'${"Total budgets on month".tr} = ${durationController.jsonData2['message'][0]['totalPrice'] ?? 0}'
.tr,
style: AppStyle.title,
),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30, showTitles: true)),
leftTitles: AxisTitles(
axisNameWidget: Text(
'Counts of budgets on days'.tr,
style: AppStyle.title),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30, showTitles: true)),
),
gridData: const FlGridData(show: true),
borderData: FlBorderData(
show: true,
border: const Border(
bottom: BorderSide(color: AppColor.accentColor),
left: BorderSide(color: AppColor.accentColor),
),
),
),
),
),
),
],
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: AppStyle.boxDecoration1,
child: !hasStats
? SizedBox(
height: Get.height * .2,
child: Center(
child: Text(
"No statistics yet".tr,
style: AppStyle.title,
),
),
)
: DriverStatsTable(
monthlyList: durationController.monthlyList,
)))
],
);
},
)
// BarChartWidget(),
),
// BarChartWidget(),
],
isleading: true);
}

View File

@@ -296,17 +296,17 @@ class PhoneNumberScreen extends StatefulWidget {
}
class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
final _phoneController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
String _completePhone = '';
void _submit() async {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
final rawPhone = _phoneController.text.trim().replaceFirst('+', '');
final rawPhone = _completePhone.replaceFirst('+', '');
Log.print('📱 _submit rawPhone: "$rawPhone" (from _completePhone: "$_completePhone")');
final success = await PhoneAuthHelper.sendOtp(rawPhone);
if (success && mounted) {
// Get.to(() => OtpVerificationScreen(phoneNumber: rawPhone));
await PhoneAuthHelper.verifyOtp(rawPhone);
}
if (mounted) setState(() => _isLoading = false);
@@ -351,7 +351,7 @@ class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
),
initialCountryCode: 'SY',
onChanged: (phone) {
_phoneController.text = phone.completeNumber;
_completePhone = phone.completeNumber;
},
validator: (phone) {
if (phone == null || phone.number.isEmpty) {

View File

@@ -184,7 +184,7 @@ class InstructionsOfRoads extends StatelessWidget {
color: AppColor.primaryColor,
shape: BoxShape.circle,
),
child: const Icon(Icons.turn_right_rounded,
child: Icon(controller.currentManeuverIcon,
color: Colors.white, size: 24),
),
const SizedBox(width: 14),
@@ -193,8 +193,8 @@ class InstructionsOfRoads extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"NEXT STEP".tr,
Text(
"${"NEXT STEP".tr} (${controller.distanceToNextStep})",
style: theme.textTheme.labelSmall?.copyWith(
color: theme.hintColor,
fontWeight: FontWeight.bold,

View File

@@ -1,21 +1,19 @@
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/controller/firebase/local_notification.dart';
import 'package:sefer_driver/main.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/auth/syria/registration_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_font_icons/flutter_font_icons.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import '../../../../../constant/colors.dart';
import '../../../../../constant/links.dart';
import '../../../../../controller/firebase/notification_service.dart';
import '../../../../../controller/functions/crud.dart';
import '../../../../../controller/home/captin/order_request_controller.dart';
import '../../../../../controller/home/navigation/navigation_view.dart';
import '../../../../../print.dart';
import '../../../../Rate/ride_calculate_driver.dart';
@@ -78,7 +76,8 @@ GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
tooltip: 'Active Ride'.tr,
onTap: () async {
await checkForPendingOrderFromServer();
if (box.read(BoxName.rideArgumentsFromBackground) != 'failure') {
if (box.read(BoxName.rideArgumentsFromBackground) !=
'failure') {
Get.to(
() => PassengerLocationMapPage(),
arguments: box.read(BoxName.rideArgumentsFromBackground),
@@ -91,6 +90,13 @@ GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
);
}
},
onLongPress: () {
box.write(BoxName.rideArgumentsFromBackground, 'failure');
box.write(BoxName.statusDriverLocation, 'off');
box.write(BoxName.rideStatus, 'no_ride');
Log.print(box.read(BoxName.statusDriverLocation));
ctrl.update();
},
),
_Divider(context),
@@ -102,11 +108,21 @@ GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
tooltip: 'Earnings'.tr,
onTap: () {
final now = DateTime.now();
DateTime? lastTime = box.read(BoxName.lastTimeStaticThrottle);
final lastTimeRaw = box.read(BoxName.lastTimeStaticThrottle);
DateTime? lastTime;
if (lastTimeRaw != null) {
try {
lastTime = DateTime.parse(lastTimeRaw.toString());
} catch (_) {
lastTime = null;
}
}
if (lastTime == null ||
now.difference(lastTime).inMinutes >= 2) {
box.write(BoxName.lastTimeStaticThrottle, now);
box.write(
BoxName.lastTimeStaticThrottle, now.toIso8601String());
Get.to(() => RideCalculateDriver());
} else {
final left = 2 - now.difference(lastTime).inMinutes;
@@ -133,6 +149,16 @@ GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
onTap: () => Get.to(() => const VipOrderPage()),
),
],
// _Divider(context),
// // ── 4. Driver Registration ──────────
// _MenuIcon(
// icon: Icons.person_add_alt_1_rounded,
// color: Colors.purple.shade400,
// tooltip: 'Registration'.tr,
// onTap: () => Get.to(() => const RegistrationView()),
// ),
],
),
);

View File

@@ -5,6 +5,7 @@ import 'package:sefer_driver/constant/api_key.dart';
import '../../../../controller/functions/location_controller.dart';
import '../../../../controller/home/captin/map_driver_controller.dart';
import '../../../../controller/profile/setting_controller.dart';
class GoogleDriverMap extends StatelessWidget {
const GoogleDriverMap({
@@ -27,6 +28,11 @@ class GoogleDriverMap extends StatelessWidget {
onMapCreated: (mapController) {
controller.onMapCreated(mapController);
},
mapType: Get.isRegistered<SettingController>()
? (Get.find<SettingController>().isMapDarkMode
? IntaleqMapType.normal
: IntaleqMapType.light)
: IntaleqMapType.light,
zoomControlsEnabled: false,
initialCameraPosition: CameraPosition(
target: locationController.myLocation,
@@ -43,8 +49,8 @@ class GoogleDriverMap extends StatelessWidget {
markers: {
Marker(
markerId: MarkerId('MyLocation'.tr),
position: locationController.myLocation,
rotation: locationController.heading,
position: controller.smoothedLocation ?? locationController.myLocation,
rotation: controller.smoothedHeading,
flat: true,
anchor: const Offset(0.5, 0.5),
icon: controller.carIcon,

View File

@@ -281,7 +281,7 @@ class RideAvailableCard extends StatelessWidget {
Get.back();
// 3. تحليل الرد
var jsonResponse = jsonDecode(response);
var jsonResponse = response;
if (jsonResponse['status'] == 'success') {
// ✅ نجاح: أنت الفائز بالرحلة