first commit

This commit is contained in:
Hamza-Ayed
2026-06-09 08:40:31 +03:00
commit d8901e1a87
3161 changed files with 536187 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
import 'dart:convert';
import 'dart:math';
import 'package:get/get.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import '../../../constant/table_names.dart';
import '../../../main.dart';
import '../../../print.dart';
class DriverBehaviorController extends GetxController {
Future<List<Map<String, dynamic>>> getAllData() async {
return await sql.getAllData(TableName.behavior);
}
var isLoading = false.obs;
var overallScore = 100.0.obs;
var lastTrips = [].obs;
Future<void> fetchDriverBehavior() async {
isLoading.value = true;
try {
final response = await CRUD().get(
link: AppLink.get_driver_behavior,
payload: {"driver_id": box.read(BoxName.driverID).toString()},
);
if (response != 'failure') {
final json = jsonDecode(response);
overallScore.value =
double.parse(json['message']['overall_behavior_score'].toString());
lastTrips.value = json['message']['last_10_trips'];
} else {
// Get.snackbar("Error", json['message'] ?? "Unknown error");
}
} catch (e) {
// Get.snackbar("Error", "Exception: $e");
Log.print('e: ${e}');
} finally {
isLoading.value = false;
}
}
Future<Map<String, dynamic>> analyzeData() async {
final data = await getAllData();
if (data.isEmpty) return {};
double maxSpeed = 0;
double totalSpeed = 0;
int hardBrakes = 0;
double totalDistance = 0;
// متغيرات للمقارنة مع النقطة السابقة
double? prevLat, prevLng;
DateTime? prevTime;
// ترتيب البيانات حسب الوقت لضمان دقة الحساب (اختياري لكن مفضل)
// data.sort((a, b) => a['created_at'].compareTo(b['created_at']));
for (var item in data) {
// 1. قراءة البيانات بالأسماء الصحيحة من الجدول
double lat = item['latitude'] ?? item['lat'] ?? 0.0;
double lng = item['longitude'] ?? item['lng'] ?? 0.0;
double acc = item['acceleration'] ?? 0.0;
// قراءة الوقت لحساب السرعة
DateTime currentTime =
DateTime.tryParse(item['created_at'].toString()) ?? DateTime.now();
double currentSpeed = 0;
// 2. حساب السرعة والمسافة إذا وجدت نقطة سابقة
if (prevLat != null && prevLng != null && prevTime != null) {
double distKm = _calculateDistance(prevLat, prevLng, lat, lng);
int timeDiffSeconds = currentTime.difference(prevTime).inSeconds;
if (timeDiffSeconds > 0) {
// السرعة (كم/س) = (المسافة بالكيلومتر * 3600) / الزمن بالثواني
currentSpeed = (distKm * 3600) / timeDiffSeconds;
}
totalDistance += distKm;
}
// تحديث القيم الإحصائية
if (currentSpeed > maxSpeed) maxSpeed = currentSpeed;
totalSpeed += currentSpeed;
// حساب الفرملة القوية (يعتمد على التسارع المحفوظ مسبقاً)
if (acc.abs() > 3.0) hardBrakes++;
// حفظ النقطة الحالية لتكون هي "السابقة" في الدورة التالية
prevLat = lat;
prevLng = lng;
prevTime = currentTime;
}
// تجنب القسمة على صفر
double avgSpeed = (data.length > 1) ? totalSpeed / (data.length - 1) : 0;
// حساب تقييم السلوك
double behaviorScore = 100 - (hardBrakes * 5) - ((maxSpeed > 100) ? 10 : 0);
behaviorScore = behaviorScore.clamp(0.0, 100.0);
return {
'max_speed': maxSpeed,
'avg_speed': avgSpeed,
'hard_brakes': hardBrakes,
'total_distance': totalDistance,
'behavior_score': behaviorScore,
};
}
Future<void> sendSummaryToServer(String driverId, String tripId) async {
final summary = await analyzeData();
if (summary.isEmpty) return;
final Map<String, dynamic> body = {
'driver_id': driverId,
'trip_id': tripId,
...summary, // فيه doubles
};
// اجبر كل القيم على String
final payload = body.map((k, v) => MapEntry(k, v?.toString() ?? ''));
await CRUD().post(link: AppLink.saveBehavior, payload: payload);
await clearData();
}
Future<void> clearData() async {
await sql.deleteAllData(TableName.behavior);
}
double _calculateDistance(
double lat1, double lon1, double lat2, double lon2) {
const p = 0.017453292519943295;
final a = 0.5 -
cos((lat2 - lat1) * p) / 2 +
cos(lat1 * p) * cos(lat2 * p) * (1 - cos((lon2 - lon1) * p)) / 2;
return 12742 * asin(sqrt(a)); // distance in km
}
}

View File

@@ -0,0 +1,78 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_font_icons/flutter_font_icons.dart';
import 'package:get/get.dart';
import '../../../constant/colors.dart';
import '../../functions/launch.dart';
class ContactUsController extends GetxController {
final String phone1 = '+963992952235';
final String phone2 = '+963992952235';
final TimeOfDay workStartTime = const TimeOfDay(hour: 12, minute: 0);
final TimeOfDay workEndTime = const TimeOfDay(hour: 19, minute: 0);
bool _isWithinWorkTime(TimeOfDay now) {
return (now.hour > workStartTime.hour ||
(now.hour == workStartTime.hour &&
now.minute >= workStartTime.minute)) &&
(now.hour < workEndTime.hour ||
(now.hour == workEndTime.hour && now.minute <= workEndTime.minute));
}
void showContactDialog(BuildContext context) {
TimeOfDay now = TimeOfDay.now();
showCupertinoModalPopup(
context: context,
builder: (context) => CupertinoActionSheet(
title: Text('Contact Us'.tr),
message: Text('Choose a contact option'.tr),
actions: <Widget>[
if (_isWithinWorkTime(now))
CupertinoActionSheetAction(
child: Text(phone1),
onPressed: () => makePhoneCall(
phone1,
),
),
if (_isWithinWorkTime(now))
CupertinoActionSheetAction(
child: Text(phone2),
onPressed: () => makePhoneCall(phone2),
),
if (!_isWithinWorkTime(now))
CupertinoActionSheetAction(
child: Text(
'Work time is from 10:00 - 17:00.\nYou can send a WhatsApp message or email.'
.tr),
onPressed: () => Navigator.pop(context),
),
CupertinoActionSheetAction(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const Icon(
FontAwesome.whatsapp,
color: AppColor.greenColor,
),
Text('Send WhatsApp Message'.tr),
],
),
onPressed: () =>
launchCommunication('whatsapp', phone1, 'Hello'.tr),
),
CupertinoActionSheetAction(
child: Text('Send Email'.tr),
onPressed: () =>
launchCommunication('email', 'support@sefer.live', 'Hello'.tr),
),
],
cancelButton: CupertinoActionSheetAction(
child: Text('Cancel'.tr),
onPressed: () => Navigator.pop(context),
),
),
);
}
}

View File

@@ -0,0 +1,167 @@
import 'dart:convert';
import 'package:siro_driver/views/widgets/elevated_btn.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:get/get.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/main.dart';
import 'package:siro_driver/models/model/driver/rides_summary_model.dart';
import '../../../views/widgets/error_snakbar.dart';
class DurationController extends GetxController {
final data = DurationData;
// late AnimationController animationController;
late List<MonthlyDataModel> rideData;
late List<MonthlyRideModel> rideCountData;
late List<MonthlyPriceDriverModel> ridePriceDriverData;
Map<String, dynamic> jsonData1 = {};
Map<String, dynamic> jsonData2 = {};
bool isLoading = false;
String totalDurationToday = '';
var chartData;
var chartRideCount;
var chartRidePriceDriver;
List monthlyList = [];
@override
void onInit() async {
super.onInit();
await fetchData();
await fetchRideDriver();
await getStaticDriver();
}
getStaticDriver() async {
isLoading = true;
update();
var res = await CRUD().get(
link: AppLink.driverStatistic,
payload: {'driverID': box.read(BoxName.driverID)});
if (res == 'success') {
try {
monthlyList = jsonDecode(res)['message'];
} catch (e) {
monthlyList = [];
}
} else {
monthlyList = [];
}
isLoading = false;
update();
}
Future<void> fetchData() async {
isLoading = true;
update();
var res = await CRUD().get(
link: AppLink.getTotalDriverDuration,
payload: {'driver_id': box.read(BoxName.driverID)},
);
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 {
isLoading = true;
update(); // Notify the observers about the loading state change
var res = await CRUD().get(
link: AppLink.getRidesDriverByDay,
payload: {'driver_id': box.read(BoxName.driverID)},
);
if (res != 'failure' && res != 'no_internet' && res != 'token_expired') {
jsonData2 = jsonDecode(res);
var jsonResponse = jsonData2 as Map<String, dynamic>;
isLoading = false;
final List<dynamic> jsonData = jsonResponse['message'];
rideCountData = jsonData.map<MonthlyRideModel>((item) {
return MonthlyRideModel.fromJson(item);
}).toList();
ridePriceDriverData = jsonData.map<MonthlyPriceDriverModel>((item) {
return MonthlyPriceDriverModel.fromJson(item);
}).toList();
final List<FlSpot> spots = rideCountData
.map((data) => FlSpot(
data.day.toDouble(),
data.countRide.toDouble(),
))
.toList();
chartRideCount = spots;
final List<FlSpot> spotsDriverPrices = ridePriceDriverData
.map((data) => FlSpot(
data.day.toDouble(),
data.pricePerDay.toDouble(),
))
.toList();
chartRidePriceDriver = spotsDriverPrices;
update();
} else {
isLoading = false;
jsonData2 = {};
chartRideCount = <FlSpot>[];
chartRidePriceDriver = <FlSpot>[];
update();
if (res == 'no_internet') {
mySnackeBarError('No internet connection'.tr);
}
}
}
List<DurationData> parseData(List<dynamic> json) {
return json.map((entry) {
final Map<String, dynamic> entryMap = entry;
final day = DateTime.parse(entryMap['day']);
final totalDuration = _parseDuration(entryMap['total_duration']);
return DurationData(day, totalDuration);
}).toList();
}
Duration _parseDuration(String durationString) {
final parts = durationString.split(':');
final hours = int.parse(parts[0]);
final minutes = int.parse(parts[1]);
final seconds = int.parse(parts[2]);
return Duration(hours: hours, minutes: minutes, seconds: seconds);
}
}
class DurationData {
final DateTime day;
final Duration totalDuration;
DurationData(this.day, this.totalDuration);
}

View File

@@ -0,0 +1,58 @@
import 'dart:convert';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/main.dart';
import 'package:siro_driver/views/widgets/error_snakbar.dart';
import 'package:get/get.dart';
class AssuranceHealthController extends GetxController {
bool isLoading = false;
Map tripCount = {};
Future getTripCountByCaptain() async {
var res = await CRUD().get(link: AppLink.getTripCountByCaptain, payload: {
"driver_id": box.read(BoxName.driverID).toString(),
});
if (res != 'failure') {
tripCount = jsonDecode(res)['message'];
update();
}
}
Future<void> addDriverHealthAssurance({
String? driverId,
String? assured,
required String healthInsuranceProvider,
}) async {
// Define the URL to your PHP backend
// Data to be sent to the backend
Map<String, String> data = {
"driver_id": box.read(BoxName.driverID).toString(),
"assured": '1',
"health_insurance_provider": healthInsuranceProvider,
};
try {
// Send the POST request to your backend
var response = await CRUD()
.post(link: AppLink.addHealthInsuranceProvider, payload: data);
if (response != 'failure') {
// Handle success (e.g., show a success message)
mySnackbarSuccess(
"You have successfully opted for health insurance.".tr);
} else {
// Handle failure (e.g., show an error message)
print("Failed to save health assurance data");
mySnackeBarError("Please enter a health insurance status.".tr);
}
} catch (e) {
// Handle any errors
print("Error: $e");
}
}
}

View File

@@ -0,0 +1,103 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../constant/box_name.dart';
import '../../../../constant/links.dart';
import '../../../../constant/style.dart';
import '../../../../main.dart';
import '../../../../views/widgets/elevated_btn.dart';
import '../../../functions/crud.dart';
import '../../../functions/encrypt_decrypt.dart';
class HelpController extends GetxController {
bool isLoading = false;
final formKey = GlobalKey<FormState>();
final helpQuestionController = TextEditingController();
Map helpQuestionDate = {};
Map helpQuestionRepleyDate = {};
String status = '';
String qustion = '';
late int indexQuestion = 0;
getIndex(int i, String qustion1) async {
indexQuestion = i;
qustion = qustion1;
update();
}
void addHelpQuestion() async {
isLoading = true;
update();
var res = await CRUD().post(link: AppLink.addhelpCenter, payload: {
'driverID': box.read(BoxName.driverID).toString(),
'helpQuestion': (helpQuestionController.text)
});
var d = jsonDecode(res);
isLoading = false;
update();
if (d['status'].toString() == 'success') {
getHelpQuestion();
// Get.snackbar('Feedback data saved successfully'.tr, '',
// backgroundColor: AppColor.greenColor,
// snackPosition: SnackPosition.BOTTOM);
}
}
void getHelpQuestion() async {
isLoading = true;
update();
var res = await CRUD().get(link: AppLink.gethelpCenter, payload: {
'driverID': box.read(BoxName.driverID).toString(),
});
if (res == "failure") {
isLoading = false;
update();
Get.defaultDialog(
title: 'There is no help Question here'.tr,
titleStyle: AppStyle.title,
middleText: '',
confirm: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
MyElevatedButton(
title: 'Add Question'.tr,
onPressed: () {
Get.back();
}),
MyElevatedButton(
title: 'Back'.tr,
onPressed: () {
Get.back();
Get.back();
}),
],
));
}
helpQuestionDate = jsonDecode(res);
isLoading = false;
update();
}
Future getHelpRepley(String id) async {
isLoading = true;
update();
var res = await CRUD().get(link: AppLink.getByIdhelpCenter, payload: {
'id': id,
});
if (res == "failure") {
status = 'not yet';
isLoading = false;
update();
}
helpQuestionRepleyDate = jsonDecode(res);
isLoading = false;
update();
}
@override
void onInit() {
getHelpQuestion();
super.onInit();
}
}

View File

@@ -0,0 +1,22 @@
import 'dart:convert';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/main.dart';
import 'package:get/get.dart';
class MaintainCenterController extends GetxController {
bool isLoading = false;
Map tripCount = {};
Future getTripCountByCaptain() async {
var res = await CRUD().get(link: AppLink.getTripCountByCaptain, payload: {
"driver_id": box.read(BoxName.driverID).toString(),
});
if (res != 'failure') {
tripCount = jsonDecode(res)['message'];
update();
}
}
}

View File

@@ -0,0 +1,69 @@
import 'dart:convert';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/print.dart';
import 'package:siro_driver/views/widgets/error_snakbar.dart';
import 'package:get/get.dart';
import 'package:video_player/video_player.dart';
class VideoController extends GetxController {
var videos = [];
var isLoading = true.obs;
final String apiUrl =
'${AppLink.seferCairoServer}/ride/videos_driver/get.php';
@override
void onInit() {
fetchVideos();
super.onInit();
}
late VideoPlayerController videoPlayerController;
// Initialize the video player with the provided URL
Future<void> initializeVideo(String videoUrl) async {
videoPlayerController =
VideoPlayerController.networkUrl(Uri.parse(videoUrl));
await videoPlayerController.initialize();
videoPlayerController
.setLooping(true); // Set to true if you want the video to loop
update(); // Update the UI after the video has been initialized
}
// Play the video
void play() {
videoPlayerController.play();
update();
}
// Pause the video
void pause() {
videoPlayerController.pause();
update();
}
@override
void onClose() {
videoPlayerController
.dispose(); // Dispose of the video player controller when not in use
super.onClose();
}
void fetchVideos() async {
try {
var res = await CRUD().get(link: apiUrl, payload: {});
if (res != 'failure') {
videos = jsonDecode(res)['message'];
// Log.print('videos: ${videos}');
update();
} else {
mySnackeBarError('');
}
} catch (e) {
mySnackeBarError(e.toString());
} finally {
isLoading(false);
}
}
}

View File

@@ -0,0 +1,850 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:siro_driver/constant/box_name.dart';
import 'dart:async';
import '../../../constant/links.dart';
import '../../../constant/style.dart';
import '../../../constant/table_names.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../../views/home/my_wallet/walet_captain.dart';
import '../../../views/widgets/elevated_btn.dart';
import '../../firebase/firbase_messge.dart';
import '../../functions/background_service.dart';
import '../../functions/crud.dart';
import '../../functions/location_background_controller.dart';
import '../../functions/location_controller.dart';
import '../payment/captain_wallet_controller.dart';
class HomeCaptainController extends GetxController {
bool isActive = false;
DateTime? activeStartTime;
Duration activeDuration = Duration.zero;
Timer? activeTimer;
Map data = {};
bool isHomeMapActive = true;
InlqBitmap carIcon = InlqBitmap.fromAsset('assets/images/car.png');
bool isMapReadyForCommands = false;
bool isLoading = true;
late double kazan = 0;
double latePrice = 0;
double heavyPrice = 0;
double comfortPrice = 0,
speedPrice = 0,
deliveryPrice = 0,
mashwariPrice = 0,
familyPrice = 0,
fuelPrice = 0;
double naturePrice = 0;
bool isCallOn = false;
String totalMoneyToday = '0';
double? rating = 5;
String rideId = '0';
String countRideToday = '0';
String totalMoneyInSEFER = '0';
String totalDurationToday = '0';
Timer? timer;
Timer? _cameraFollowTimer;
LatLng myLocation = const LatLng(33.5138, 36.2765);
String totalPoints = '0';
String countRefuse = '0';
bool mapType = false;
bool mapTrafficON = false;
double widthMapTypeAndTraffic = 50;
// === متغيرات الهيت ماب الجديدة ===
bool isHeatmapVisible = false;
Set<Polygon> heatmapPolygons =
{}; // سنستخدم Polygon لرسم المربعات على جوجل مابس
// Inject the LocationController class
// final locationController = Get.put(LocationController());
// الكود الصحيح
final locationController = Get.find<LocationController>();
// final locationBackController = Get.put(LocationBackgroundController());
String formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, "0");
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
return "${duration.inHours}:$twoDigitMinutes:$twoDigitSeconds";
}
// دالة لتغيير حالة الهيت ماب (عرض/إخفاء)
void toggleHeatmap() async {
isHeatmapVisible = !isHeatmapVisible;
print("🔥 [Heatmap] Visibility toggled to: $isHeatmapVisible");
if (isHeatmapVisible) {
startHeatmapCycle();
} else {
_heatmapTimer?.cancel();
heatmapPolygons.clear();
print("🧹 [Heatmap] Polygons cleared.");
}
update(); // تحديث الواجهة
}
// داخل MapDriverController
// متغير لتخزين المربعات
// Set<Polygon> heatmapPolygons = {};
// دالة جلب البيانات ورسم الخريطة
Future<void> fetchAndDrawHeatmap() async {
print("🚀 [Heatmap] Fetching live data...");
// استخدم الرابط المباشر لملف JSON لسرعة قصوى
final String jsonUrl =
"https://ride.intaleq.xyz/intaleq/ride/heatmap_data.json";
try {
// نستخدم timestamp لمنع الكاش من الموبايل نفسه
final response = await http.get(
Uri.parse("$jsonUrl?t=${DateTime.now().millisecondsSinceEpoch}"));
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
print("✅ [Heatmap] Data received. Points count: ${data.length}");
_generatePolygons(data);
} else {
print("⚠️ [Heatmap] Server error: ${response.statusCode}");
}
} catch (e) {
print("❌ [Heatmap] Error: $e");
}
}
void _generatePolygons(List<dynamic> data) {
print("🎨 [Heatmap] Processing polygons...");
Set<Polygon> tempPolygons = {};
// الأوفست لرسم المربع (نصف حجم الشبكة)
// الشبكة دقتها 0.01 درجة، لذا نصفها 0.005
double offset = 0.005;
int highCount = 0, medCount = 0, lowCount = 0;
for (var point in data) {
double lat = double.parse(point['lat'].toString());
double lng = double.parse(point['lng'].toString());
String intensity = point['intensity'] ?? 'low';
int count = int.parse(point['count'].toString()); // ✅ جلب العدد
Color color;
Color strokeColor;
// 🧠 منطق الألوان: ندمج الذكاء (Intensity) مع العدد (Count)
if (intensity == 'high' || count >= 5) {
highCount++;
// منطقة مشتعلة (أحمر)
// إما فيها طلبات ضائعة (Timeout) أو فيها عدد كبير من الطلبات
color = Colors.red.withValues(alpha: 0.35);
strokeColor = Colors.red.withValues(alpha: 0.8);
} else if (intensity == 'medium' || count >= 3) {
medCount++;
// منطقة متوسطة (برتقالي)
color = Colors.orange.withValues(alpha: 0.35);
strokeColor = Colors.orange.withValues(alpha: 0.8);
} else {
lowCount++;
// منطقة خفيفة (أصفر)
color = Colors.yellow.withValues(alpha: 0.3);
strokeColor = Colors.yellow.withValues(alpha: 0.6);
}
// رسم المربع
tempPolygons.add(Polygon(
polygonId: PolygonId("$lat-$lng"),
// consumeTapEvents: true, // للسماح بالضغط عليه مستقبلاً
points: [
LatLng(lat - offset, lng - offset),
LatLng(lat + offset, lng - offset),
LatLng(lat + offset, lng + offset),
LatLng(lat - offset, lng + offset),
],
fillColor: color,
strokeColor: strokeColor,
strokeWidth: 2,
));
}
heatmapPolygons = tempPolygons;
print(
"✨ [Heatmap] Rendering Done. (🔥 High: $highCount, 🟠 Med: $medCount, 🟡 Low: $lowCount)");
print("📍 [Heatmap] Total Polygons on Map: ${heatmapPolygons.length}");
update(); // تحديث الخريطة
}
Timer? _heatmapTimer;
// دالة لتشغيل الخريطة الحرارية كل فترة (كل 5 دقائق) لضمان نشاط البيانات
void startHeatmapCycle() {
_heatmapTimer?.cancel();
fetchAndDrawHeatmap();
// Refresh every 15 min instead of 5 to reduce data & battery usage
_heatmapTimer = Timer.periodic(const Duration(minutes: 15), (timer) {
if (isHeatmapVisible) {
print("🔄 [Heatmap] Periodic refresh started...");
fetchAndDrawHeatmap();
} else {
timer.cancel();
}
});
}
void goToWalletFromConnect() {
Get.back();
Get.back();
Get.to(() => WalletCaptainRefactored());
}
void changeRideId() {
rideId = 'rideId';
update();
}
void addCustomCarIcon() {
carIcon = InlqBitmap.fromAsset('assets/images/car.png');
update();
}
String stringActiveDuration = '';
int _fatigueSeconds = 0; // عداد ثواني الإرهاق المؤقت
// ==========================================
// ====== 🛡️ Fatigue Monitoring System ======
// ==========================================
void _checkFatigueBeforeOnline() {
int totalSecondsToday = box.read('fatigue_total_seconds') ?? 0;
String? lastOfflineStr = box.read('fatigue_last_offline');
if (lastOfflineStr != null) {
DateTime lastOffline = DateTime.parse(lastOfflineStr);
// If offline for more than 6 continuous hours, reset the fatigue counter
if (DateTime.now().difference(lastOffline).inHours >= 6) {
totalSecondsToday = 0;
box.write('fatigue_total_seconds', 0);
}
}
if (totalSecondsToday >= 12 * 3600) {
// 12 Hours
_forceOfflineDueToFatigue();
throw Exception('Fatigue Limit Exceeded');
}
}
void _forceOfflineDueToFatigue() {
if (isActive) {
isActive = false;
locationController.stopLocationUpdates();
activeStartTime = null;
activeTimer?.cancel();
update();
}
Get.defaultDialog(
title: 'Safety First 🛑'.tr,
middleText:
'You have been driving for 12 hours. For your safety and compliance, please take a 6-hour break.'
.tr,
barrierDismissible: false,
titleStyle:
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
confirm: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () => Get.back(),
child: Text('OK'.tr, style: const TextStyle(color: Colors.white)),
),
);
}
void onButtonSelected() {
if (!Get.isRegistered<CaptainWalletController>()) {
Get.put(CaptainWalletController());
}
totalPoints = Get.find<CaptainWalletController>().totalPoints;
// Toggle Active State
isActive = !isActive;
if (isActive) {
try {
_checkFatigueBeforeOnline(); // Throws exception if tired
if (double.parse(totalPoints) > -200) {
locationController.startLocationUpdates();
HapticFeedback.heavyImpact();
activeStartTime = DateTime.now();
activeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
activeDuration = DateTime.now().difference(activeStartTime!);
stringActiveDuration = formatDuration(activeDuration);
// Increment Fatigue Counter (write to box every 30s)
_fatigueSeconds++;
if (_fatigueSeconds % 30 == 0) {
int totalSeconds =
(box.read('fatigue_total_seconds') ?? 0) + _fatigueSeconds;
box.write('fatigue_total_seconds', totalSeconds);
_fatigueSeconds = 0;
if (totalSeconds >= 12 * 3600) {
// 12 hours
_forceOfflineDueToFatigue();
}
}
update();
});
} else {
locationController.stopLocationUpdates();
activeStartTime = null;
activeTimer?.cancel();
savePeriod(activeDuration);
activeDuration = Duration.zero;
box.write('fatigue_last_offline', DateTime.now().toIso8601String());
update();
}
} catch (e) {
// Driver is fatigued, revert state
isActive = false;
update();
}
} else {
locationController.stopLocationUpdates();
activeStartTime = null;
activeTimer?.cancel();
savePeriod(activeDuration);
activeDuration = Duration.zero;
// Save offline time for Fatigue Monitoring reset
box.write('fatigue_last_offline', DateTime.now().toIso8601String());
update();
}
}
// متغيرات العداد للحظر
RxString remainingBlockTimeStr = "".obs;
Timer? _blockTimer;
/// دالة الفحص والدايلوج
void checkAndShowBlockDialog() {
String? blockStr = box.read(BoxName.blockUntilDate);
if (blockStr == null || blockStr.isEmpty) return;
DateTime blockExpiry = DateTime.parse(blockStr);
DateTime now = DateTime.now();
if (now.isBefore(blockExpiry)) {
// 1. إجبار السائق على وضع الأوفلاين
box.write(BoxName.statusDriverLocation, 'blocked');
update();
// 2. بدء العداد
_startBlockCountdown(blockExpiry);
// 3. إظهار الديالوج المانع
Get.defaultDialog(
title: "Your account is temporarily restricted ⛔".tr,
titleStyle:
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
barrierDismissible: false, // 🚫 ممنوع الإغلاق بالضغط خارجاً
onWillPop: () async => false, // 🚫 ممنوع زر الرجوع في الأندرويد
content: Obx(() => Column(
children: [
const Icon(Icons.timer_off_outlined,
size: 50, color: Colors.orange),
const SizedBox(height: 15),
Text(
"You have exceeded the allowed cancellation limit (3 times).\nYou cannot work until the penalty expires."
.tr,
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Text(
remainingBlockTimeStr.value, // 🔥 الوقت يتحدث هنا
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87),
),
const SizedBox(height: 20),
],
)),
confirm: Obx(() {
// الزر يكون مفعلاً فقط عندما ينتهي الوقت
bool isFinished = remainingBlockTimeStr.value == "00:00:00" ||
remainingBlockTimeStr.value == "Done";
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: isFinished ? Colors.green : Colors.grey,
),
onPressed: isFinished
? () {
Get.back(); // إغلاق الديالوج
box.remove(BoxName.blockUntilDate); // إزالة الحظر
Get.snackbar("Welcome".tr, "You can now receive orders".tr,
backgroundColor: Colors.green);
}
: null, // زر معطل
child: Text(isFinished ? "Go Online".tr : "Wait for timer".tr),
);
}),
);
} else {
// الوقت انتهى أصلاً -> تنظيف
box.remove(BoxName.blockUntilDate);
}
}
/// دالة العداد التنازلي
void _startBlockCountdown(DateTime expiry) {
_blockTimer?.cancel();
_blockTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
DateTime now = DateTime.now();
if (now.isAfter(expiry)) {
// انتهى الوقت
remainingBlockTimeStr.value = "Done";
timer.cancel();
} else {
// حساب الفرق وتنسيقه
Duration diff = expiry.difference(now);
String twoDigits(int n) => n.toString().padLeft(2, "0");
String hours = twoDigits(diff.inHours);
String minutes = twoDigits(diff.inMinutes.remainder(60));
String seconds = twoDigits(diff.inSeconds.remainder(60));
remainingBlockTimeStr.value = "$hours:$minutes:$seconds";
}
});
}
@override
void onClose() {
print("🔥 [HomeCaptain] onClose called. Tearing down map resources...");
_blockTimer?.cancel();
activeTimer?.cancel();
_cameraFollowTimer?.cancel();
_heatmapTimer?.cancel();
stopTimer();
mapHomeCaptainController = null;
super.onClose();
}
void getRefusedOrderByCaptain() async {
DateTime today = DateTime.now();
int todayDay = today.day;
String driverId = box.read(BoxName.driverID).toString();
String customQuery = '''
SELECT COUNT(*) AS count
FROM ${TableName.driverOrdersRefuse}
WHERE driver_id = '$driverId'
AND created_at LIKE '%$todayDay%'
''';
try {
List<Map<String, dynamic>> results =
await sql.getCustomQuery(customQuery);
countRefuse = results[0]['count'].toString();
update();
if (double.parse(totalPoints) <= -200) {
// if (int.parse(countRefuse) > 3 || double.parse(totalPoints) <= -200) {
locationController.stopLocationUpdates();
activeStartTime = null;
activeTimer?.cancel();
savePeriod(activeDuration);
activeDuration = Duration.zero;
update();
Get.defaultDialog(
// backgroundColor: CupertinoColors.destructiveRed,
barrierDismissible: false,
title: 'You Are Stopped For this Day !'.tr,
content: Text(
'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!'
.tr,
style: AppStyle.title,
),
confirm: MyElevatedButton(
title: 'Ok , See you Tomorrow'.tr,
onPressed: () {
// إغلاق الديالوج والعودة قسرياً
navigatorKey.currentState?.pop();
Get.back();
}));
}
} catch (e) {}
}
void changeMapType() {
mapType = !mapType;
// heightButtomSheetShown = isButtomSheetShown == true ? 240 : 0;
update();
}
void changeMapTraffic() {
mapTrafficON = !mapTrafficON;
update();
}
// late IntaleqMapController mapHomeCaptainController;
IntaleqMapController? mapHomeCaptainController;
LatLng? _lastCameraLoc; // لتتبع آخر موقع حرك الكاميرا
// --- FIX 2: Smart Map Creation ---
void onMapCreated(IntaleqMapController controller) {
print("🔥 [HomeCaptain] onMapCreated started");
mapHomeCaptainController = controller;
// We delay the first move to ensure the native side is fully ready
Future.delayed(const Duration(milliseconds: 800), () {
if (isClosed || mapHomeCaptainController == null) return;
try {
var currentLoc = locationController.myLocation;
if (currentLoc.latitude != 0 &&
currentLoc.latitude != null &&
!currentLoc.latitude.isNaN) {
print(
"🔥 [HomeCaptain] Safely moving camera to: ${currentLoc.latitude}");
mapHomeCaptainController!.moveCamera(
CameraUpdate.newLatLngZoom(currentLoc, 17.5),
);
} else {
print("🔥 [HomeCaptain] Safely moving to default Damascus");
mapHomeCaptainController!.moveCamera(
CameraUpdate.newLatLngZoom(myLocation, 12),
);
}
// Mark as ready for regular listener updates
isMapReadyForCommands = true;
} catch (e) {
print("❌ [HomeCaptain] Map move failed: $e");
}
});
}
void savePeriod(Duration period) {
final periods = box.read<List<dynamic>>(BoxName.periods) ?? [];
periods.add(period.inSeconds);
box.write(BoxName.periods, periods);
}
Duration calculateTotalDuration() {
final periods = box.read<List<dynamic>>(BoxName.periods) ?? [];
Duration totalDuration = Duration.zero;
for (dynamic periodInSeconds in periods) {
final periodDuration = Duration(seconds: periodInSeconds);
totalDuration += periodDuration;
}
return totalDuration;
}
Timer? _localDurationTimer;
RxString totalDurationDisplay = "00:00:00".obs; // لعرض الوقت في الواجهة
Duration _currentDuration = Duration.zero; // لتخزين الوقت ككائن Duration
void startPeriodicExecution() async {
await getCaptainDurationOnToday();
String? initialDurationStr = totalDurationToday;
if (initialDurationStr != '0') {
// تحويل النص (01:20:30) إلى كائن Duration
List<String> parts = initialDurationStr.split(':');
_currentDuration = Duration(
hours: int.parse(parts[0]),
minutes: int.parse(parts[1]),
seconds: int.parse(parts[2]),
);
// بدء العداد المحلي
_startLocalClock();
}
// Timer.periodic(const Duration(seconds: 30), (timer) async {
// await getCaptainDurationOnToday();
// });
}
void _startLocalClock() {
_localDurationTimer?.cancel();
_localDurationTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
// زيادة ثانية واحدة محلياً
_currentDuration += const Duration(seconds: 1);
// تحديث النص المعروض في الواجهة (Formatting)
totalDurationDisplay.value = _formatDuration(_currentDuration);
// اختيارياً: كل 5 دقائق فقط، قم بتحديث القيمة من السيرفر للتأكد من المزامنة
if (timer.tick % 300 == 0) {
getCaptainDurationOnToday();
}
});
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, "0");
String hours = twoDigits(duration.inHours);
String minutes = twoDigits(duration.inMinutes.remainder(60));
String seconds = twoDigits(duration.inSeconds.remainder(60));
return "$hours:$minutes:$seconds";
}
void stopTimer() {
_localDurationTimer?.cancel();
}
getlocation() async {
isLoading = true;
update();
try {
// ننتظر جلب الموقع مع مهلة 10 ثوانٍ لتجنب التعليق
var locData = await locationController.getLocation().timeout(
const Duration(seconds: 10),
onTimeout: () => null,
);
if (locData != null && locData.latitude != null) {
myLocation = LatLng(locData.latitude!, locData.longitude!);
print(
"📍 [HomeCaptain] Location updated: ${myLocation.latitude}, ${myLocation.longitude}");
} else {
print(
"⚠️ [HomeCaptain] Could not get current location, using default.");
}
} catch (e) {
print("❌ Error in getlocation: $e");
} finally {
isLoading = false;
update();
}
}
Map walletDriverPointsDate = {};
Future getCaptainWalletFromBuyPoints() async {
// isLoading = true;
update();
var res = await CRUD().getWallet(
link: AppLink.getDriverPaymentPoints,
payload: {'driverID': box.read(BoxName.driverID).toString()},
);
isLoading = false;
// update();
if (res != 'failure') {
walletDriverPointsDate = jsonDecode(res);
double totalPointsDouble = double.parse(
walletDriverPointsDate['message'][0]['total_amount'].toString());
totalPoints = totalPointsDouble.toStringAsFixed(0);
update();
} else {
totalPoints = '0';
}
}
// 3. دالة نستدعيها عند قبول الطلب
void pauseHomeMapUpdates() {
isHomeMapActive = false;
update();
}
// 4. دالة نستدعيها عند العودة للصفحة الرئيسية
void resumeHomeMapUpdates() {
isHomeMapActive = true;
// تم حذف استدعاء onMapCreated المتكرر لمنع قفز الخريطة عند العودة
update();
}
@override
void onInit() async {
// ✅ تم إرجاعه كتعليق لمنع الديالوج عند التشغيل (كما كان في الكود الأصلي)
// bool permissionsGranted = await PermissionsHelper.requestAllPermissions();
// if (permissionsGranted) {
// await BackgroundServiceHelper.startService();
// }
Get.put(FirebaseMessagesController());
addToken();
await getlocation();
onButtonSelected();
getDriverRate();
addCustomCarIcon();
getKazanPercent();
getPaymentToday();
getCountRideToday();
getAllPayment();
startPeriodicExecution();
getCaptainWalletFromBuyPoints();
// onMapCreated(mapHomeCaptainController!);
// totalPoints = Get.find<CaptainWalletController>().totalPoints.toString();
// getRefusedOrderByCaptain();
// 🔥 الفحص عند تشغيل التطبيق
checkAndShowBlockDialog();
box.write(BoxName.statusDriverLocation, 'off');
// 2. عدل الليسنر ليصبح مشروطاً
// Camera follow timer — only moves when the driver has
// actually moved > 15 meters, saving GPU/battery on idle.
_cameraFollowTimer = Timer.periodic(const Duration(seconds: 8), (timer) {
if (isClosed ||
!isHomeMapActive ||
mapHomeCaptainController == null ||
!isMapReadyForCommands ||
!isActive) return;
var loc = locationController.myLocation;
if (loc.latitude != 0 && loc.latitude != null && !loc.latitude.isNaN) {
// Skip if driver hasn't moved significantly
if (_lastCameraLoc != null) {
final double dist = Geolocator.distanceBetween(
_lastCameraLoc!.latitude,
_lastCameraLoc!.longitude,
loc.latitude,
loc.longitude,
);
if (dist < 15) return;
}
_lastCameraLoc = loc;
try {
if (mapHomeCaptainController != null) {
mapHomeCaptainController?.animateCamera(
CameraUpdate.newLatLngZoom(loc, 17.5),
);
}
} catch (e) {
print("❌ [HomeCaptain] Camera movement failed: $e");
}
}
});
// LocationController().getLocation();
super.onInit();
}
addToken() async {
String? fingerPrint = await storage.read(key: BoxName.fingerPrint);
final payload = {
'token': (box.read(BoxName.tokenDriver)),
'captain_id': (box.read(BoxName.driverID)).toString(),
'fingerPrint': (fingerPrint).toString()
};
// Log.print('payload: ${payload}');
CRUD().post(link: AppLink.addTokensDriver, payload: payload);
}
getPaymentToday() async {
var res = await CRUD().getWallet(
link: AppLink.getDriverPaymentToday,
payload: {'driverID': box.read(BoxName.driverID).toString()});
if (res != 'failure') {
data = jsonDecode(res);
totalMoneyToday = data['message'][0]['todayAmount'].toString();
update();
} else {}
}
getKazanPercent() async {
var res = await CRUD().get(
link: AppLink.getKazanPercent,
payload: {'country': box.read(BoxName.countryCode).toString()},
);
if (res != 'failure') {
var json = jsonDecode(res);
kazan = double.parse(json['message'][0]['kazan']);
naturePrice = double.parse(json['message'][0]['naturePrice']);
heavyPrice = double.parse(json['message'][0]['heavyPrice']);
latePrice = double.parse(json['message'][0]['latePrice']);
comfortPrice = double.parse(json['message'][0]['comfortPrice']);
speedPrice = double.parse(json['message'][0]['speedPrice']);
deliveryPrice = double.parse(json['message'][0]['deliveryPrice']);
mashwariPrice = double.parse(json['message'][0]['freePrice']);
familyPrice = double.parse(json['message'][0]['familyPrice']);
fuelPrice = double.parse(json['message'][0]['fuelPrice']);
}
update();
}
double mpg = 0;
calculateConsumptionFuel() {
mpg = fuelPrice / 12; //todo in register car add mpg in box
}
getCountRideToday() async {
var res = await CRUD().get(
link: AppLink.getCountRide,
payload: {'driver_id': box.read(BoxName.driverID).toString()});
data = jsonDecode(res);
countRideToday = data['message'][0]['count'].toString();
update();
}
getDriverRate() async {
var res = await CRUD().get(
link: AppLink.getDriverRate,
payload: {'driver_id': box.read(BoxName.driverID).toString()});
if (res != 'failure') {
var decod = jsonDecode(res);
if (decod['message'][0]['rating'] != null) {
rating = double.parse(decod['message'][0]['rating'].toString());
} else {
rating = 5.0; // Set a default value (e.g., 5.0 for full rating)
}
} else {
rating = 5;
}
}
getAllPayment() async {
var res = await CRUD().getWallet(
link: AppLink.getAllPaymentFromRide,
payload: {'driverID': box.read(BoxName.driverID).toString()});
if (res == 'failure') {
totalMoneyInSEFER = '0';
} else {
data = jsonDecode(res);
totalMoneyInSEFER = data['message'][0]['total_amount'];
}
update();
}
void changeToAppliedRide(String status) {
box.write(BoxName.rideStatus, status);
Log.print('rideStatus from homcaptain : ${box.read(BoxName.rideStatus)}');
update();
}
Future<void> getCaptainDurationOnToday() async {
try {
var res = await CRUD().get(
link: AppLink.getTotalDriverDurationToday,
payload: {'driver_id': box.read(BoxName.driverID).toString()},
);
if (res == null || res == 'failure') {
totalDurationToday = '0';
update();
return;
}
var data = jsonDecode(res);
totalDurationToday = data['message']?[0]?['total_duration'] ?? '0';
} catch (e) {
print('Error in getCaptainDurationOnToday: $e');
totalDurationToday = '0';
}
update();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
class NavigationStep {
final String instruction;
final String maneuver;
final double distance;
final String duration;
final LatLng startLocation;
final LatLng endLocation;
final String htmlInstructions;
NavigationStep({
required this.instruction,
required this.maneuver,
required this.distance,
required this.duration,
required this.startLocation,
required this.endLocation,
required this.htmlInstructions,
});
factory NavigationStep.fromJson(Map<String, dynamic> json) {
return NavigationStep(
instruction: json['html_instructions'] ?? '',
maneuver: json['maneuver'] ?? 'straight',
distance: (json['distance']['value'] ?? 0).toDouble(),
duration: json['duration']['text'] ?? '',
startLocation: LatLng(
json['start_location']['lat'].toDouble(),
json['start_location']['lng'].toDouble(),
),
endLocation: LatLng(
json['end_location']['lat'].toDouble(),
json['end_location']['lng'].toDouble(),
),
htmlInstructions: json['html_instructions'] ?? '',
);
}
// Get clean instruction text (remove HTML tags)
String get cleanInstruction {
return instruction
.replaceAll(RegExp(r'<[^>]*>'), '')
.replaceAll('&nbsp;', ' ');
}
// Get instruction icon based on maneuver
IconData get instructionIcon {
switch (maneuver.toLowerCase()) {
case 'turn-left':
return Icons.turn_left;
case 'turn-right':
return Icons.turn_right;
case 'turn-slight-left':
return Icons.turn_slight_left;
case 'turn-slight-right':
return Icons.turn_slight_right;
case 'turn-sharp-left':
return Icons.turn_sharp_left;
case 'turn-sharp-right':
return Icons.turn_sharp_right;
case 'uturn-left':
case 'uturn-right':
return Icons.u_turn_left;
case 'straight':
return Icons.straight;
case 'ramp-left':
return Icons.ramp_left;
case 'ramp-right':
return Icons.ramp_right;
case 'merge':
return Icons.merge;
case 'fork-left':
case 'fork-right':
return Icons.call_split;
case 'ferry':
return Icons.directions_boat;
case 'roundabout-left':
case 'roundabout-right':
return Icons.roundabout_left;
default:
return Icons.navigation;
}
}
}

View File

@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:siro_driver/constant/api_key.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/controller/functions/tts.dart';
import '../../../main.dart';
/// Handles map-related logic: fetching routes, drawing polylines, and managing markers.
class NavigationService extends GetxService {
final CRUD _crud = CRUD();
final TextToSpeechController _tts = Get.put(TextToSpeechController());
final RxSet<Marker> markers = <Marker>{}.obs;
final RxSet<Polyline> polylines = <Polyline>{}.obs;
final RxString currentInstruction = "".obs;
InlqBitmap carIcon = InlqBitmap.defaultMarker;
InlqBitmap passengerIcon = InlqBitmap.defaultMarker;
InlqBitmap startIcon = InlqBitmap.defaultMarker;
InlqBitmap endIcon = InlqBitmap.defaultMarker;
@override
void onInit() {
super.onInit();
_loadCustomIcons();
}
void _loadCustomIcons() {
carIcon = InlqBitmap.fromAsset('assets/images/car.png');
passengerIcon = InlqBitmap.fromAsset('assets/images/picker.png');
startIcon = InlqBitmap.fromAsset('assets/images/A.png');
endIcon = InlqBitmap.fromAsset('assets/images/b.png');
}
Future<Map<String, dynamic>?> getRoute({
required LatLng origin,
required LatLng destination,
}) async {
final url =
'${AppLink.googleMapsLink}directions/json?language=${box.read(BoxName.lang)}&destination=${destination.latitude},${destination.longitude}&origin=${origin.latitude},${origin.longitude}&key=${AK.mapAPIKEY}';
final response = await _crud.getGoogleApi(link: url, payload: {});
if (response != null && response['routes'].isNotEmpty) {
return response['routes'][0];
}
return null;
}
void drawRoute(Map<String, dynamic> routeData, {Color color = Colors.blue}) {
final pointsString = routeData["overview_polyline"]["points"];
final points = PolylineUtils.decode(pointsString);
final polyline = Polyline(
polylineId: PolylineId(routeData["summary"] ?? DateTime.now().toString()),
points: points,
width: 8,
color: color,
);
polylines.add(polyline);
}
void updateCarMarker(LatLng position, double heading) {
markers.removeWhere((m) => m.markerId.value == 'MyLocation');
markers.add(
Marker(
markerId: MarkerId('MyLocation'.tr),
position: position,
icon: carIcon,
rotation: heading,
anchor: const Offset(0.5, 0.5),
flat: true,
),
);
}
void setInitialMarkers(
LatLng passengerLocation, LatLng passengerDestination) {
markers.clear();
markers.add(Marker(
markerId: const MarkerId('passengerLocation'),
position: passengerLocation,
icon: passengerIcon,
));
markers.add(Marker(
markerId: const MarkerId('passengerDestination'),
position: passengerDestination,
icon: endIcon,
));
}
void clearRoutes() {
polylines.clear();
currentInstruction.value = "";
}
}

View File

@@ -0,0 +1,742 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:just_audio/just_audio.dart';
import 'package:siro_driver/views/widgets/error_snakbar.dart';
import 'dart:math' as math;
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../env/env.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../../views/home/Captin/driver_map_page.dart';
import '../../../views/home/Captin/orderCaptin/marker_generator.dart';
import '../../../views/widgets/mydialoug.dart';
import '../../firebase/local_notification.dart';
import '../../functions/crud.dart';
import '../../functions/location_controller.dart';
import '../../home/captin/home_captain_controller.dart';
class OrderRequestController extends GetxController
with WidgetsBindingObserver {
// --- متغيرات التايمر ---
double progress = 1.0;
int duration = 15;
int remainingTime = 15;
Timer? _timer;
bool applied = false;
final locationController = Get.put(LocationController());
// 🔥 متغير لمنع تكرار القبول
bool _isRideTakenHandled = false;
// --- الأيقونات والماركرز ---
InlqBitmap? driverIcon;
Map<MarkerId, Marker> markersMap = {};
Set<Marker> get markers => markersMap.values.toSet();
// --- البيانات والتحكم ---
// 🔥 تم إضافة myMapData لدعم السوكيت الجديد
List<dynamic>? myList;
Map<dynamic, dynamic>? myMapData;
IntaleqMapController? mapController;
// الإحداثيات (أزلنا late لتجنب الأخطاء القاتلة)
double latPassenger = 0.0;
double lngPassenger = 0.0;
double latDestination = 0.0;
double lngDestination = 0.0;
// --- متغيرات العرض ---
String passengerRating = "5.0";
String tripType = "Standard";
String totalTripDistance = "--";
String totalTripDuration = "--";
String tripPrice = "--";
String timeToPassenger = "Calculating...".tr;
String distanceToPassenger = "--";
// --- الخريطة ---
Set<Polyline> polylines = {};
// حالة التطبيق والصوت
bool isInBackground = false;
final AudioPlayer audioPlayer = AudioPlayer();
@override
Future<void> onInit() async {
// 🛑 حماية من الفتح المتكرر لنفس الطلب
if (Get.arguments == null) {
print("❌ OrderController Error: No arguments received.");
Get.back(); // إغلاق الصفحة فوراً
return;
}
super.onInit();
WidgetsBinding.instance.addObserver(this);
_checkOverlay();
// 🔥 تهيئة البيانات هي الخطوة الأولى والأهم
_initializeData();
_parseExtraData();
// 1. تجهيز أيقونة السائق
await _prepareDriverIcon();
// 2. وضع الماركرز المبدئية
_updateMarkers(
paxTime: "...",
paxDist: "",
destTime: totalTripDuration,
destDist: totalTripDistance);
// 3. رسم مبدئي
_initialMapSetup();
// 4. الاستماع للسوكيت
_listenForRideTaken();
// 5. حساب المسارين
await _calculateFullJourney();
// 6. تشغيل التايمر
startTimer();
}
// ----------------------------------------------------------------------
// 🔥🔥🔥 Smart Data Handling (List & Map Support) 🔥🔥🔥
// ----------------------------------------------------------------------
void _initializeData() {
var args = Get.arguments;
print("📦 Order Controller Received Type: ${args.runtimeType}");
print("📦 Order Controller Data: $args");
if (args != null) {
// الحالة 1: قائمة مباشرة (Legacy / Some Firebase formats)
if (args is List) {
myList = args;
}
// الحالة 2: خريطة (Map)
else if (args is Map) {
// أ) هل هي قادمة من Firebase وتحتوي على DriverList؟
if (args.containsKey('DriverList')) {
var listData = args['DriverList'];
if (listData is List) {
myList = listData;
} else if (listData is String) {
// أحياناً تصل كنص مشفر داخل الـ Map
try {
myList = jsonDecode(listData);
} catch (e) {
print("Error decoding DriverList: $e");
}
}
}
// ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟
else {
myMapData = args;
}
}
}
// تعبئة الإحداثيات باستخدام الدالة الذكية _getValueAt
latPassenger = _parseCoord(_getValueAt(0));
lngPassenger = _parseCoord(_getValueAt(1));
latDestination = _parseCoord(_getValueAt(3));
lngDestination = _parseCoord(_getValueAt(4));
print(
"📍 Parsed Coordinates: Pax($latPassenger, $lngPassenger) -> Dest($latDestination, $lngDestination)");
}
/// 🔥 دالة ذكية تجلب القيمة سواء كانت البيانات في List أو Map
dynamic _getValueAt(int index) {
// الأولوية للقائمة
if (myList != null && index < myList!.length) {
return myList![index];
}
// ثم الخريطة (السوكيت) - المفاتيح عبارة عن String
if (myMapData != null && myMapData!.containsKey(index.toString())) {
return myMapData![index.toString()];
}
return null;
}
/// الدالة التي يستخدمها باقي الكود لجلب البيانات كنصوص
String _safeGet(int index) {
var val = _getValueAt(index);
if (val != null) {
return val.toString();
}
return "";
}
double _parseCoord(dynamic val) {
if (val == null) return 0.0;
String s = val.toString().replaceAll(',', '').trim();
if (s.contains(' ')) s = s.split(' ')[0];
return double.tryParse(s) ?? 0.0;
}
void _parseExtraData() {
passengerRating = _safeGet(33).isEmpty ? "5.0" : _safeGet(33);
tripType = _safeGet(31);
// Format numbers to avoid many decimal places
String rawDist = _safeGet(5);
if (rawDist.isNotEmpty) {
double? d = double.tryParse(rawDist);
totalTripDistance = d != null ? "${d.toStringAsFixed(1)} km" : rawDist;
}
String rawDur = _safeGet(19);
if (rawDur.isNotEmpty) {
double? d = double.tryParse(rawDur);
totalTripDuration = d != null ? "${d.toStringAsFixed(0)} min" : rawDur;
}
String rawPrice = _safeGet(2);
if (rawPrice.isNotEmpty) {
double? p = double.tryParse(rawPrice);
tripPrice = p != null ? p.toStringAsFixed(0) : rawPrice;
}
}
// ----------------------------------------------------------------------
// 🔥🔥🔥 Core Logic: Concurrent API Calls & Bounds 🔥🔥🔥
// ----------------------------------------------------------------------
Future<void> _calculateFullJourney() async {
// Don't block on mapController being null - we'll draw routes
// and markers first, then zoom when controller is ready
bool canZoom = mapController != null;
try {
// Reuse stored location from LocationController instead of
// making a duplicate GPS hardware call (already fetched in
// _initialMapSetup).
LatLng driverLatLng;
double driverHeading = 0.0;
if (Get.isRegistered<LocationController>()) {
final locCtrl = Get.find<LocationController>();
if (locCtrl.myLocation.latitude != 0 ||
locCtrl.myLocation.longitude != 0) {
driverLatLng = locCtrl.myLocation;
driverHeading = locCtrl.heading;
} else {
Position driverPos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
driverHeading = driverPos.heading;
}
} else {
Position driverPos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
driverHeading = driverPos.heading;
}
updateDriverLocation(driverLatLng, driverHeading);
// Clear old polylines to avoid "ghost lines"
polylines.clear();
var pickupFuture = _fetchRouteData(
start: driverLatLng,
end: LatLng(latPassenger, lngPassenger),
color: Colors.amber,
id: 'pickup_route');
var tripFuture = _fetchRouteData(
start: LatLng(latPassenger, lngPassenger),
end: LatLng(latDestination, lngDestination),
color: Colors.black,
id: 'trip_route',
getSteps: true); // 🔥 نطلب الخطوات للمسار
var results = await Future.wait([pickupFuture, tripFuture]);
var pickupResult = results[0];
var tripResult = results[1];
if (pickupResult != null) {
distanceToPassenger = pickupResult['distance_text'];
timeToPassenger = pickupResult['duration_text'];
polylines.add(pickupResult['polyline']);
}
if (tripResult != null) {
totalTripDistance = tripResult['distance_text'];
totalTripDuration = tripResult['duration_text'];
polylines.add(tripResult['polyline']);
// 🔥 تخزين استجابة السيرفر كاملة (بما فيها الـ points والـ instructions)
if (tripResult['raw_response'] != null) {
box.write('cached_trip_route', tripResult['raw_response']);
}
}
await _updateMarkers(
paxTime: timeToPassenger,
paxDist: distanceToPassenger,
destTime: totalTripDuration,
destDist: totalTripDistance);
// Now zoom to fit all polylines and markers (if controller available)
if (canZoom) {
zoomToFitRide();
}
update();
} catch (e) {
print("❌ Error in Journey Calculation: $e");
}
}
String _formatDistance(dynamic rawDist) {
if (rawDist == null || rawDist.toString().isEmpty) return "--";
double dist = double.tryParse(rawDist.toString()) ?? 0.0;
if (dist <= 0) return "--";
if (dist < 1000) return "${dist.toStringAsFixed(0)} m";
return "${(dist / 1000).toStringAsFixed(1)} km";
}
String _formatDuration(dynamic rawDur) {
if (rawDur == null || rawDur.toString().isEmpty) return "--";
double dur = double.tryParse(rawDur.toString()) ?? 0.0;
if (dur <= 0) return "1 min"; // Minimum 1 min for UI
if (dur < 60) return "${dur.toStringAsFixed(0)} sec";
return "${(dur / 60).toStringAsFixed(0)} min";
}
Future<Map<String, dynamic>?> _fetchRouteData(
{required LatLng start,
required LatLng end,
required Color color,
required String id,
bool getSteps = false}) async {
try {
if (start.latitude == 0 || end.latitude == 0) return null;
// Don't block on mapController — route data fetch is independent
final saasUrl = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: {
'fromLat': start.latitude.toString(),
'fromLng': start.longitude.toString(),
'toLat': end.latitude.toString(),
'toLng': end.longitude.toString(),
'steps': getSteps ? 'true' : 'false',
'alternatives': 'false',
'locale': 'ar',
});
final response = await http.get(saasUrl, headers: {
'x-api-key': Env.mapSaasKey,
'Content-Type': 'application/json',
});
if (response.statusCode != 200) {
throw Exception("Routing request failed: ${response.statusCode}");
}
final data = jsonDecode(response.body);
print("🛣️ Route API Response [$id]: ${data}");
// The map-saas API returns the route data directly at the root,
// with 'points' being an encoded polyline string.
final String? encodedPoints = data['points']?.toString();
if (encodedPoints != null && encodedPoints.isNotEmpty) {
List<LatLng> path = controllerDecodePolyline(encodedPoints);
print("📍 Path for [$id] has ${path.length} points.");
final num? rawDist = data['distance'] is num ? data['distance'] : null;
final num? rawDur = data['duration'] is num ? data['duration'] : null;
final distanceText = data['distance_text'] ?? _formatDistance(rawDist);
final durationText = data['duration_text'] ?? _formatDuration(rawDur);
Polyline polyline = Polyline(
polylineId: PolylineId(id),
color: color,
width: 5,
points: path,
);
return {
'distance_text': distanceText,
'duration_text': durationText,
'polyline': polyline,
'encoded_polyline': encodedPoints,
'raw_response': response.body, // 🔥 نمرر الـ JSON كاملاً
};
}
} catch (e) {
print("Route Fetch Error: $e");
}
return null;
}
void zoomToFitRide() {
if (mapController == null) return;
List<LatLng> allPoints = [];
// Add all polyline points to the bounds calculation
for (var polyline in polylines) {
allPoints.addAll(polyline.points);
}
// Fallback to basic markers if polylines are empty
if (allPoints.isEmpty) {
allPoints.addAll([
LatLng(latPassenger, lngPassenger),
LatLng(latDestination, lngDestination),
]);
}
if (allPoints.isEmpty) return;
double minLat = allPoints.first.latitude;
double maxLat = allPoints.first.latitude;
double minLng = allPoints.first.longitude;
double maxLng = allPoints.first.longitude;
for (var p in allPoints) {
if (p.latitude < minLat) minLat = p.latitude;
if (p.latitude > maxLat) maxLat = p.latitude;
if (p.longitude < minLng) minLng = p.longitude;
if (p.longitude > maxLng) maxLng = p.longitude;
}
// Add some padding to the bounds
double latPad = (maxLat - minLat) * 0.25;
double lngPad = (maxLng - minLng) * 0.2;
mapController!.animateCamera(CameraUpdate.newLatLngBounds(
LatLngBounds(
southwest: LatLng(minLat - latPad, minLng - lngPad),
northeast: LatLng(maxLat + latPad, maxLng + lngPad),
),
));
}
// ----------------------------------------------------------------------
// Markers & Setup
// ----------------------------------------------------------------------
Future<void> _prepareDriverIcon() async {
driverIcon = await MarkerGenerator.createDriverMarker();
}
Future<void> _updateMarkers(
{required String paxTime,
required String paxDist,
String? destTime,
String? destDist}) async {
// حماية إذا لم يتم جلب الإحداثيات
if (latPassenger == 0 || latDestination == 0) return;
final InlqBitmap pickupIcon =
await MarkerGenerator.createCustomMarkerBitmap(
title: paxTime,
subtitle: paxDist,
color: Colors.amber.shade900, // Matching the amber pickup line
iconData: Icons.person_pin_circle,
);
final InlqBitmap dropoffIcon =
await MarkerGenerator.createCustomMarkerBitmap(
title: destTime ?? totalTripDuration,
subtitle: destDist ?? totalTripDistance,
color: Colors.red.shade800,
iconData: Icons.flag,
);
markersMap[const MarkerId('pax')] = Marker(
markerId: const MarkerId('pax'),
position: LatLng(latPassenger, lngPassenger),
icon: pickupIcon,
anchor: const Offset(0.5, 0.85),
);
markersMap[const MarkerId('dest')] = Marker(
markerId: const MarkerId('dest'),
position: LatLng(latDestination, lngDestination),
icon: dropoffIcon,
anchor: const Offset(0.5, 0.85),
);
update();
}
void _initialMapSetup() async {
Position driverPos = await Geolocator.getCurrentPosition();
LatLng driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
if (driverIcon != null) {
markersMap[const MarkerId('driver')] = Marker(
markerId: const MarkerId('driver'),
position: driverLatLng,
icon: driverIcon!,
rotation: driverPos.heading,
anchor: const Offset(0.5, 0.5),
flat: true,
zIndex: 10);
}
if (latPassenger != 0 && lngPassenger != 0) {
polylines.add(Polyline(
polylineId: const PolylineId('temp_line'),
points: [driverLatLng, LatLng(latPassenger, lngPassenger)],
color: Colors.grey,
width: 2,
));
zoomToFitRide();
}
update();
}
void updateDriverLocation(LatLng newPos, double heading) {
if (driverIcon != null) {
markersMap[const MarkerId('driver')] = Marker(
markerId: const MarkerId('driver'),
position: newPos,
icon: driverIcon!,
rotation: heading,
anchor: const Offset(0.5, 0.5),
flat: true,
zIndex: 10,
);
update();
}
}
void onMapCreated(IntaleqMapController controller) {
mapController = controller;
_calculateFullJourney();
}
// --- قبول الطلب وإدارة التايمر ---
void startTimer() {
_timer?.cancel();
remainingTime = duration;
_playAudio();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (remainingTime <= 0) {
timer.cancel();
_stopAudio();
if (!applied) Get.back();
} else {
remainingTime--;
progress = remainingTime / duration;
update();
}
});
}
void endTimer() => _timer?.cancel();
void changeApplied() => applied = true;
void _playAudio() async {
try {
await audioPlayer.setAsset('assets/order.mp3', preload: true);
await audioPlayer.setLoopMode(LoopMode.one);
await audioPlayer.play();
} catch (e) {
print(e);
}
}
void _stopAudio() => audioPlayer.stop();
void _listenForRideTaken() {
if (locationController.socket != null) {
locationController.socket!.off('ride_taken');
locationController.socket!.on('ride_taken', (data) {
if (_isRideTakenHandled) return;
String takenRideId = data['ride_id'].toString();
String myCurrentRideId = _safeGet(16);
String whoTookIt = data['taken_by_driver_id'].toString();
String myDriverId = box.read(BoxName.driverID).toString();
if (takenRideId == myCurrentRideId && whoTookIt != myDriverId) {
_isRideTakenHandled = true;
endTimer();
// 1. حذف الإشعار من شريط التنبيهات فوراً
NotificationController().cancelOrderNotification();
if (Get.isSnackbarOpen) Get.closeCurrentSnackbar();
// إغلاق أي ديالوج مفتوح قسرياً
if (Get.isDialogOpen ?? false) {
navigatorKey.currentState?.pop();
}
Get.back();
mySnackbarInfo("The order has been accepted by another driver.".tr);
}
});
}
}
// Lifecycle
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
isInBackground = true;
} else if (state == AppLifecycleState.resumed) {
isInBackground = false;
FlutterOverlayWindow.closeOverlay();
}
}
void _checkOverlay() async {
if (Platform.isAndroid && await FlutterOverlayWindow.isActive()) {
await FlutterOverlayWindow.closeOverlay();
}
}
// Accept Order Logic
Future<void> acceptOrder() async {
endTimer();
_stopAudio();
// 1. إرسال الطلب
var res = await CRUD()
.post(link: "${AppLink.ride}/rides/acceptRide.php", payload: {
'id': _safeGet(16),
'rideTimeStart': DateTime.now().toString(),
'status': 'Apply',
'passengerToken': _safeGet(9),
'driver_id': box.read(BoxName.driverID),
});
Log.print('res from orderrequestpage: ${res}');
// ============================================================
// تصحيح: فحص الرد بدقة (Map أو String)
// ============================================================
bool isFailure = false;
if (res is Map && res['status'] == 'failure') {
isFailure = true;
} else if (res == 'failure') {
isFailure = true;
}
if (isFailure) {
// ⛔ حالة الفشل: الطلب مأخوذ
MyDialog().getDialog(
"Sorry, the order was taken by another driver.".tr, '', () {
// بما أن MyDialog يغلق نفسه الآن، نحتاج Get.back() واحدة فقط لإغلاق صفحة الطلب
Get.back();
});
} else {
// ✅ حالة النجاح
// حماية من الكراش: التأكد من وجود HomeCaptainController قبل استخدامه
if (!Get.isRegistered<HomeCaptainController>()) {
Get.put(HomeCaptainController());
} else {
Get.find<HomeCaptainController>().changeRideId();
}
box.write(BoxName.statusDriverLocation, 'on');
changeApplied();
var rideArgs = {
'passengerLocation': '${_safeGet(0)},${_safeGet(1)}',
'passengerDestination': '${_safeGet(3)},${_safeGet(4)}',
'Duration': totalTripDuration,
'totalCost': _safeGet(26),
'Distance': totalTripDistance,
'name': _safeGet(8),
'phone': _safeGet(10),
'email': _safeGet(28),
'WalletChecked': _safeGet(13),
'tokenPassenger': _safeGet(9),
'direction':
'https://www.google.com/maps/dir/${_safeGet(0)}/${_safeGet(1)}/',
'DurationToPassenger': timeToPassenger,
'rideId': _safeGet(16),
'passengerId': _safeGet(7),
'driverId': _safeGet(18),
'durationOfRideValue': totalTripDuration,
'paymentAmount': _safeGet(2),
'paymentMethod': _safeGet(13) == 'true' ? 'visa' : 'cash',
'isHaveSteps': _safeGet(20),
'step0': _safeGet(21),
'step1': _safeGet(22),
'step2': _safeGet(23),
'step3': _safeGet(24),
'step4': _safeGet(25),
'passengerWalletBurc': _safeGet(26),
'timeOfOrder': DateTime.now().toString(),
'totalPassenger': _safeGet(2),
'carType': _safeGet(31),
'kazan': _safeGet(32),
'startNameLocation': _safeGet(29),
'endNameLocation': _safeGet(30),
};
box.write(BoxName.rideArguments, rideArgs);
// الانتقال النهائي
Get.off(() => PassengerLocationMapPage(), arguments: rideArgs);
}
}
@override
void onClose() {
locationController.socket?.off('ride_taken');
audioPlayer.dispose();
WidgetsBinding.instance.removeObserver(this);
_timer?.cancel();
// mapController?.dispose();
super.onClose();
}
List<LatLng> controllerDecodePolyline(String encoded) {
List<LatLng> points = [];
int index = 0, len = encoded.length;
int lat = 0, lng = 0;
while (index < len) {
int b, shift = 0, result = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lat += dlat;
shift = 0;
result = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lng += dlng;
points.add(LatLng(lat / 1E5, lng / 1E5));
}
return points;
}
}

View File

@@ -0,0 +1,212 @@
<style>
* { box-sizing: border-box; }
.wrap { padding: 1.25rem 1rem; font-size: 14px; color: var(--color-text-primary); direction: rtl; }
h1 { font-size: 18px; font-weight: 500; margin: 0 0 3px; }
.sub { font-size: 13px; color: var(--color-text-secondary); margin: 0 0 1.25rem; }
.badge { display: inline-flex; align-items: center; font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: 20px; white-space: nowrap; }
.b-ok { background: var(--color-background-success); color: var(--color-text-success); }
.b-new { background: var(--color-background-danger); color: var(--color-text-danger); }
.b-med { background: var(--color-background-warning); color: var(--color-text-warning); }
.b-min { background: var(--color-background-info); color: var(--color-text-info); }
.progress-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 1.25rem; }
.pcard { background: var(--color-background-secondary); border-radius: var(--border-radius-md); padding: 12px 14px; }
.pcard .val { font-size: 28px; font-weight: 500; }
.pcard .lbl { font-size: 12px; color: var(--color-text-secondary); margin-top: 2px; }
.ok-val { color: var(--color-text-success); }
.bad-val { color: var(--color-text-danger); }
.new-val { color: var(--color-text-warning); }
.section { margin-bottom: 1.4rem; }
.section-hdr { font-size: 14px; font-weight: 500; margin: 0 0 8px; display: flex; align-items: center; gap: 8px; }
.card { background: var(--color-background-primary); border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-lg); margin-bottom: 8px; overflow: hidden; }
.card.fixed { border-right: 3px solid var(--color-border-success); }
.card.broken{ border-right: 3px solid var(--color-border-danger); }
.card.new { border-right: 3px solid var(--color-border-warning); }
.card.minor { border-right: 3px solid var(--color-border-info); }
.ch { display: flex; align-items: flex-start; gap: 8px; padding: 10px 14px; cursor: pointer; }
.ch:hover { background: var(--color-background-secondary); }
.ch-icon { font-size: 15px; flex-shrink: 0; margin-top: 1px; }
.ch-title { font-size: 13.5px; font-weight: 500; flex: 1; line-height: 1.4; }
.ch-badge { flex-shrink: 0; }
.chev { font-size: 11px; color: var(--color-text-tertiary); transition: transform .2s; margin-right: auto; margin-left: 4px; }
.chev.open { transform: rotate(90deg); }
.cb { display: none; padding: 0 14px 14px; border-top: 0.5px solid var(--color-border-tertiary); }
.cb.open { display: block; }
.cb p { font-size: 13px; color: var(--color-text-secondary); line-height: 1.7; margin: 8px 0 6px; }
pre { font-family: var(--font-mono); font-size: 11.5px; background: var(--color-background-tertiary); border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-md); padding: 9px 11px; overflow-x: auto; margin: 6px 0; line-height: 1.6; white-space: pre; }
.fix { background: var(--color-background-success); border-radius: var(--border-radius-md); padding: 8px 11px; margin-top: 8px; font-size: 13px; line-height: 1.6; }
.fix strong { color: var(--color-text-success); font-size: 11px; display: block; margin-bottom: 2px; }
.warn { background: var(--color-background-warning); border-radius: var(--border-radius-md); padding: 8px 11px; margin-top: 8px; font-size: 13px; line-height: 1.6; }
.warn strong { color: var(--color-text-warning); font-size: 11px; display: block; margin-bottom: 2px; }
code { font-family: var(--font-mono); font-size: 12px; background: var(--color-background-secondary); padding: 0 4px; border-radius: 3px; }
.score-row { display: flex; align-items: center; gap: 10px; font-size: 13px; margin-bottom: 7px; }
.score-lbl { min-width: 160px; color: var(--color-text-secondary); }
.strack { flex: 1; height: 6px; background: var(--color-border-tertiary); border-radius: 3px; position: relative; }
.sfill { height: 100%; border-radius: 3px; }
.sval { min-width: 36px; font-size: 12px; color: var(--color-text-secondary); text-align: left; }
</style>
<div class="wrap">
<h1>مراجعة النسخة المحدّثة — V2</h1>
<p class="sub">مقارنة مع المراجعة السابقة · 16 مشكلة فُحصت</p>
<div class="progress-row">
<div class="pcard"><div class="val ok-val">11</div><div class="lbl">مشكلة مُصلحة ✅</div></div>
<div class="pcard"><div class="val bad-val">2</div><div class="lbl">مشكلة جديدة أدخلتها الإصلاحات ⚠️</div></div>
<div class="pcard"><div class="val new-val">3</div><div class="lbl">مشكلة لم تُعالج بعد</div></div>
<div class="pcard"><div class="val ok-val">69%</div><div class="lbl">تحسن من المراجعة الأولى</div></div>
</div>
<div class="score-row"><span class="score-lbl">صحة المنطق البرمجي</span><div class="strack"><div class="sfill" style="width:72%;background:#3B8BD4"></div></div><span class="sval">72% ↑</span></div>
<div class="score-row"><span class="score-lbl">نظافة الكود</span><div class="strack"><div class="sfill" style="width:63%;background:#1D9E75"></div></div><span class="sval">63% ↑</span></div>
<div class="score-row" style="margin-bottom:1.4rem"><span class="score-lbl">قابلية الصيانة</span><div class="strack"><div class="sfill" style="width:58%;background:#1D9E75"></div></div><span class="sval">58% ↑</span></div>
<!-- FIXED -->
<div class="section">
<div class="section-hdr"><span class="badge b-ok">✅ مُصلح</span> ما تم إصلاحه بشكل صحيح</div>
<div class="card fixed">
<div class="ch" onclick="t(this)"><span class="ch-icon"></span><span class="ch-title">C-1 — استبدال الحلقة التكرارية والاستدعاء الذاتي بـ <code>Timer.periodic</code></span><span class="ch-badge badge b-ok">ممتاز</span><span class="chev"></span></div>
<div class="cb"><p>تم حذف <code>updateLocation()</code> كاملاً واستبدالها بـ <code>startUpdateLocationTimer()</code> و <code>stopUpdateLocationTimer()</code>. التايمر مسجّل في <code>onClose()</code> و <code>_stopAllServices()</code>. إصلاح ممتاز.</p>
<div class="warn"><strong>ملاحظة مهمة</strong>لا يظهر في الكود استدعاء لـ <code>startUpdateLocationTimer()</code> من أي مكان. يجب التأكد أنها تُستدعى من الـ View أو من <code>startRideFromDriver()</code>.</div></div>
</div>
<div class="card fixed">
<div class="ch" onclick="t(this)"><span class="ch-icon"></span><span class="ch-title">C-4 — تحديث <code>myLocation</code> في <code>_handleLocationUpdate()</code></span><span class="chev"></span></div>
<div class="cb"><pre>void _handleLocationUpdate(geo.Position pos) {
final newLoc = LatLng(pos.latitude, pos.longitude);
myLocation = newLoc; // ← [Fix C-4] ✅ صحيح
// ...</pre></div>
</div>
<div class="card fixed">
<div class="ch" onclick="t(this)"><span class="ch-icon"></span><span class="ch-title">M-4 — دمج <code>checkForNextStep()</code> مع <code>_checkNavigationStep()</code></span><span class="chev"></span></div>
<div class="cb"><p><code>checkForNextStep</code> أصبحت wrapper بسيط يستدعي <code>_checkNavigationStep</code>. منطق واحد، لا تعارض.</p></div>
</div>
<div class="card fixed">
<div class="ch" onclick="t(this)"><span class="ch-icon"></span><span class="ch-title">M-5 — <code>disposeEverything()</code> لا تستدعي <code>onClose()</code> يدوياً</span><span class="chev"></span></div>
<div class="cb"><pre>void disposeEverything() {
_stopAllServices(); // ✅ بدون onClose()
}</pre></div>
</div>
<div class="card fixed">
<div class="ch" onclick="t(this)"><span class="ch-icon"></span><span class="ch-title">C-3 جزئي — دالة مساعدة <code>_parseDistanceToMeters()</code> مشتركة</span><span class="chev"></span></div>
<div class="cb"><p>تم استخراج منطق تحليل المسافة إلى دالة واحدة تستخدمها كلا <code>finishRideFromDriver()</code> و <code>_validateTripDistance()</code>. يحل مشكلة التضارب في الوحدات.</p>
<div class="warn"><strong>لم يُحل كاملاً</strong>التحقق من المسافة لا يزال يحدث مرتين (انظر مشكلة C-3 أدناه).</div></div>
</div>
<div class="card fixed">
<div class="ch" onclick="t(this)"><span class="ch-icon"></span><span class="ch-title">M-1 + M-2 + M-6 + N-1 + N-5 — إصلاحات طفيفة متعددة</span><span class="chev"></span></div>
<div class="cb">
<p><strong>M-1:</strong> <code>jitterMeters</code><code>jitterKm = 0.01</code></p>
<p><strong>M-2:</strong> <code>distance</code> المحلية → <code>distToPassenger</code></p>
<p><strong>M-6:</strong> تعليق يوضح أن الوحدة كيلومتر ✅</p>
<p><strong>N-1:</strong> <code>&directionsmode</code><code>?directionsmode</code></p>
<p><strong>N-5:</strong> إضافة <code>update()</code> في <code>getLocationArea()</code></p>
<p><strong>M-3:</strong> حذف <code>_performanceReadings</code> والمتغيرات الميتة ✅</p>
</div>
</div>
</div>
<!-- NEW BUGS -->
<div class="section">
<div class="section-hdr"><span class="badge b-new">🚨 جديد</span> مشاكل أدخلتها الإصلاحات</div>
<div class="card new">
<div class="ch" onclick="t(this)"><span class="ch-icon">🚨</span><span class="ch-title">BUG جديد — <code>Completer</code> في C-2 يُسبب Deadlock عند إغلاق الديالوج بـ Back</span><span class="ch-badge badge b-new">حرج</span><span class="chev"></span></div>
<div class="cb">
<p>الإصلاح استخدم <code>Completer</code> بشكل صحيح لحل مشكلة الـ callback الآني، لكنه أدخل مشكلة أخرى: لو أغلق المستخدم الديالوج بزر الرجوع (Back) في Android بدون ضغط OK، فإن <code>completer.future</code> لن تكتمل أبداً، والدالة ستبقى معلّقة (deadlock) لأن <code>_validateTripDistance()</code> هي <code>async</code> وتنتظر نتيجة لن تأتي:</p>
<pre>final completer = Completer&lt;bool&gt;();
MyDialog().getDialog('Exit Ride?'.tr, '', () {
if (!completer.isCompleted) completer.complete(true);
Get.back();
});
return await completer.future; // ← ينتظر للأبد إذا أُغلق بـ Back</pre>
<div class="fix"><strong>الحل</strong>أضف <code>barrierDismissible: false</code> للديالوج، أو استخدم <code>completer.complete(false)</code> عند إغلاق الديالوج بدون تأكيد (عبر <code>WillPopScope</code> أو <code>onDismissed</code> callback في <code>MyDialog</code>).</div>
</div>
</div>
<div class="card new">
<div class="ch" onclick="t(this)"><span class="ch-icon">🚨</span><span class="ch-title">C-3 لا يزال — المستخدم يرى ديالوجَي تأكيد متتاليَين عند إنهاء الرحلة بالزر</span><span class="ch-badge badge b-new">حرج</span><span class="chev"></span></div>
<div class="cb">
<p>رغم إضافة <code>_parseDistanceToMeters()</code>، تدفق الكود لا يزال يُقدّم ديالوجَين:</p>
<pre>// finishRideFromDriver(isFromSlider: false):
MyDialog().getDialog('Are you sure to exit ride?', '', () {
Get.back();
finishRideFromDriver1(); // ← isFromSlider = false افتراضياً
});
// finishRideFromDriver1():
if (!await _validateTripDistance(false)) return; // ← يُقدّم ديالوجاً ثانياً!</pre>
<p>المستخدم يرى "هل أنت متأكد؟" → يضغط OK → يرى "Exit Ride?" مرة ثانية → ينتظر مجدداً.</p>
<div class="fix"><strong>الحل</strong>احذف الديالوج من <code>finishRideFromDriver()</code> وأبقه في <code>_validateTripDistance()</code> فقط. أو مرّر <code>isFromSlider: true</code> لما يأتي من موافقة مسبقة.</div>
</div>
</div>
</div>
<!-- REMAINING -->
<div class="section">
<div class="section-hdr"><span class="badge b-med">⚠️ لم تُعالج</span> مشاكل لا تزال قائمة</div>
<div class="card broken">
<div class="ch" onclick="t(this)"><span class="ch-icon">⚠️</span><span class="ch-title">M-7 — Null checks على <code>String</code> غير قابلة للـ null</span><span class="chev"></span></div>
<div class="cb">
<pre>if (isSocialPressed == true && passengerId != null && rideId != null) {
// ^^^^^^^^^^^ دائماً non-null</pre>
<p>لو <code>passengerId == ''</code> يمر الشرط ويُرسل بيانات فارغة للسيرفر. الفحص الصحيح: <code>passengerId.isNotEmpty && rideId.isNotEmpty</code>.</p>
</div>
</div>
<div class="card broken">
<div class="ch" onclick="t(this)"><span class="ch-icon">⚠️</span><span class="ch-title">N-2 — تأخير 1 ثانية Hardcoded في <code>argumentLoading()</code></span><span class="chev"></span></div>
<div class="cb">
<pre>await Future.delayed(const Duration(seconds: 1));
await getRoute(...);</pre>
<p>لا يزال موجوداً. Race condition يجب معالجته بـ <code>Completer</code> بدلاً من تخمين الوقت.</p>
</div>
</div>
<div class="card broken">
<div class="ch" onclick="t(this)"><span class="ch-icon">⚠️</span><span class="ch-title">N-4 — <code>step0</code> إلى <code>step4</code> بدلاً من <code>List&lt;String&gt;</code></span><span class="chev"></span></div>
<div class="cb">
<pre>String step0 = ''; String step1 = ''; // ...
step0 = Get.arguments['step0']?.toString() ?? '';
step1 = Get.arguments['step1']?.toString() ?? '';</pre>
<p>لا تزال 5 متغيرات منفصلة. <code>List&lt;String&gt; steps = List.filled(5, '')</code> أوضح وأسهل في المعالجة.</p>
</div>
</div>
</div>
<!-- STILL MINOR -->
<div class="section">
<div class="section-hdr"><span class="badge b-min"> بسيطة</span> ملاحظات إضافية على هذه النسخة</div>
<div class="card minor">
<div class="ch" onclick="t(this)"><span class="ch-icon"></span><span class="ch-title"><code>_suggestOptimization()</code> لا تزال موجودة لكن لا يستدعيها أحد</span><span class="chev"></span></div>
<div class="cb"><p>بعد حذف <code>_performanceReadings</code> و <code>_analyzePerformance()</code>، بقيت <code>_suggestOptimization()</code> معزولة. إما أن تُستدعى من مكان ما أو تُحذف.</p></div>
</div>
<div class="card minor">
<div class="ch" onclick="t(this)"><span class="ch-icon"></span><span class="ch-title">الاستيرادات المكررة لـ <code>dart:math</code> و <code>geolocator</code> لا تزال</span><span class="chev"></span></div>
<div class="cb">
<pre>import 'dart:math';
import 'dart:math' as math; // مكرر
import 'package:geolocator/geolocator.dart' as geo;
import 'package:geolocator/geolocator.dart'; // مكرر</pre>
<p>يُسبب تحذيرات من المحلل ويُشوّش قراءة الكود. احذف النسخة غير المعرّفة.</p>
</div>
</div>
</div>
</div>
<script>
function t(header) {
const b = header.nextElementSibling;
const ch = header.querySelector('.chev');
const o = b.classList.contains('open');
b.classList.toggle('open', !o);
if (ch) ch.classList.toggle('open', !o);
}
</script>