feat: N1d/N2/N3 + قانون فلاتر وإعادة ضبط تطبيق السائق
الباك إند: - تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً، وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً. - OTP_DEV_MODE كان يفشل مفتوحاً (`!== 'false'`): غياب المتغيّر أو خطأ مطبعي يترك الإنتاج برمز ثابت يفتح كل حساب، ويحرس السحب المالي كذلك. صار `=== 'true'` في الإعداد وفي قارئَيه، مع تحذير عند الإقلاع. - N2: التحقّق من اللوغو عند الرفع (PNG/JPEG · 512+ · مربّع · سقف 5MB) عبر قراءة الترويسة بلا اعتمادية — بدل اكتشاف أيقونة ممطوطة بعد النشر. N3: السكربت يولّد build_config.dart بأعلام const (طبقات docs/22 §1.5)، ويضبط bundle IDs واسم التطبيق والأيقونات/splash، و--build يشغّل Shorebird. فلاتر: - docs/23: قانون مُلزِم للتطبيقين (البنية · Cubit · طبقة الشبكة · الأعلام). - السائق: أُزيل Dart القديم (GetX) مع الإبقاء على الأصيل والإضافات وشهادات التوقيع وShorebird وFirebase الخاص به، وأُعيد هيكلته مطابقاً للراكب. - طبقة الشبكة ترسل x-device-id وتجدّد التوكن عند 401 مرة واحدة فقط. - الحزم موحّدة بين التطبيقين، والـAPI https حصراً في الاثنين. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3ab74a13a8
commit
de4bbd00ac
@@ -1,146 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/constant/links.dart';
|
||||
import 'package:sefer_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
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/constant/links.dart';
|
||||
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';
|
||||
|
||||
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 == 'failure') {
|
||||
monthlyList = [];
|
||||
isLoading = false;
|
||||
update();
|
||||
} else {
|
||||
monthlyList = jsonDecode(res)['message'];
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchData() async {
|
||||
isLoading = true;
|
||||
update(); // Notify the observers about the loading state change
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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') {
|
||||
jsonData2 = jsonDecode(res);
|
||||
var jsonResponse = jsonDecode(res) 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(); // Notify the observers about the data and loading state change
|
||||
} else {
|
||||
Get.defaultDialog(
|
||||
title: 'No data yet!'.tr,
|
||||
middleText: '',
|
||||
confirm: MyElevatedButton(
|
||||
title: 'OK'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/constant/links.dart';
|
||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
import 'package:sefer_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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/constant/links.dart';
|
||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||
import 'package:sefer_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:sefer_driver/constant/links.dart';
|
||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||
import 'package:sefer_driver/print.dart';
|
||||
import 'package:sefer_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.server}/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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,740 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
// للألوان
|
||||
// import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
// import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
// import 'package:latlong2/latlong.dart'
|
||||
// as latlng; // هذا مهم جداً للتعامل مع إحداثيات OSM
|
||||
import 'package:sefer_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;
|
||||
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
|
||||
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;
|
||||
late 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;
|
||||
if (isHeatmapVisible) {
|
||||
await fetchAndDrawHeatmap();
|
||||
} else {
|
||||
heatmapPolygons.clear();
|
||||
}
|
||||
update(); // تحديث الواجهة
|
||||
}
|
||||
|
||||
// داخل MapDriverController
|
||||
|
||||
// متغير لتخزين المربعات
|
||||
// Set<Polygon> heatmapPolygons = {};
|
||||
|
||||
// دالة جلب البيانات ورسم الخريطة
|
||||
Future<void> fetchAndDrawHeatmap() async {
|
||||
// استخدم الرابط المباشر لملف JSON لسرعة قصوى
|
||||
final String jsonUrl = "${AppLink.server}/ride/rides/heatmap_live.json";
|
||||
Log.print('jsonUrl: ${jsonUrl}');
|
||||
|
||||
try {
|
||||
// نستخدم timestamp لمنع الكاش من الموبايل نفسه
|
||||
final response = await http.get(
|
||||
Uri.parse("$jsonUrl?t=${DateTime.now().millisecondsSinceEpoch}"));
|
||||
Log.print('heatmap_live: ${response.body}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = json.decode(response.body);
|
||||
_generatePolygons(data);
|
||||
}
|
||||
} catch (e) {
|
||||
print("Heatmap Error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _generatePolygons(List<dynamic> data) {
|
||||
Set<Polygon> tempPolygons = {};
|
||||
|
||||
// الأوفست لرسم المربع (نصف حجم الشبكة)
|
||||
// الشبكة دقتها 0.01 درجة، لذا نصفها 0.005
|
||||
double offset = 0.005;
|
||||
|
||||
for (var point in data) {
|
||||
double lat = double.parse(point['lat'].toString());
|
||||
double lng = double.parse(point['lng'].toString());
|
||||
|
||||
String intensity = point['intensity'] ?? 'low';
|
||||
int count = int.parse(point['count'].toString()); // ✅ جلب العدد
|
||||
|
||||
Color color;
|
||||
Color strokeColor;
|
||||
|
||||
// 🧠 منطق الألوان: ندمج الذكاء (Intensity) مع العدد (Count)
|
||||
if (intensity == 'high' || count >= 5) {
|
||||
// منطقة مشتعلة (أحمر)
|
||||
// إما فيها طلبات ضائعة (Timeout) أو فيها عدد كبير من الطلبات
|
||||
color = Colors.red.withOpacity(0.35);
|
||||
strokeColor = Colors.red.withOpacity(0.8);
|
||||
} else if (intensity == 'medium' || count >= 3) {
|
||||
// منطقة متوسطة (برتقالي)
|
||||
color = Colors.orange.withOpacity(0.35);
|
||||
strokeColor = Colors.orange.withOpacity(0.8);
|
||||
} else {
|
||||
// منطقة خفيفة (أصفر)
|
||||
color = Colors.yellow.withOpacity(0.3);
|
||||
strokeColor = Colors.yellow.withOpacity(0.6);
|
||||
}
|
||||
|
||||
// رسم المربع
|
||||
tempPolygons.add(Polygon(
|
||||
polygonId: PolygonId("$lat-$lng"),
|
||||
consumeTapEvents: true, // للسماح بالضغط عليه مستقبلاً
|
||||
points: [
|
||||
LatLng(lat - offset, lng - offset),
|
||||
LatLng(lat + offset, lng - offset),
|
||||
LatLng(lat + offset, lng + offset),
|
||||
LatLng(lat - offset, lng + offset),
|
||||
],
|
||||
fillColor: color,
|
||||
strokeColor: strokeColor,
|
||||
strokeWidth: 2,
|
||||
));
|
||||
}
|
||||
|
||||
heatmapPolygons = tempPolygons;
|
||||
update(); // تحديث الخريطة
|
||||
}
|
||||
|
||||
// دالة لتشغيل الخريطة الحرارية كل فترة (مثلاً عند فتح الصفحة)
|
||||
void startHeatmapCycle() {
|
||||
fetchAndDrawHeatmap();
|
||||
// يمكن تفعيل Timer هنا لو أردت تحديثها تلقائياً كل 5 دقائق
|
||||
}
|
||||
|
||||
void goToWalletFromConnect() {
|
||||
Get.back();
|
||||
Get.back();
|
||||
Get.to(() => WalletCaptainRefactored());
|
||||
}
|
||||
|
||||
void changeRideId() {
|
||||
rideId = 'rideId';
|
||||
update();
|
||||
}
|
||||
|
||||
void addCustomCarIcon() {
|
||||
ImageConfiguration config = ImageConfiguration(
|
||||
size: const Size(30, 35), devicePixelRatio: Get.pixelRatio);
|
||||
BitmapDescriptor.asset(
|
||||
config,
|
||||
'assets/images/car.png',
|
||||
// mipmaps: false,
|
||||
).then((value) {
|
||||
carIcon = value;
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
String stringActiveDuration = '';
|
||||
void onButtonSelected() {
|
||||
// تم الإصلاح: التأكد من أن المتحكم موجود قبل استخدامه لتجنب الكراش
|
||||
if (!Get.isRegistered<CaptainWalletController>()) {
|
||||
Get.put(CaptainWalletController());
|
||||
}
|
||||
totalPoints = Get.find<CaptainWalletController>().totalPoints;
|
||||
isActive = !isActive;
|
||||
if (isActive) {
|
||||
if (double.parse(totalPoints) > -200) {
|
||||
locationController.startLocationUpdates();
|
||||
HapticFeedback.heavyImpact();
|
||||
// locationBackController.startBackLocation();
|
||||
activeStartTime = DateTime.now();
|
||||
activeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
activeDuration = DateTime.now().difference(activeStartTime!);
|
||||
stringActiveDuration = formatDuration(activeDuration);
|
||||
update();
|
||||
});
|
||||
} else {
|
||||
locationController.stopLocationUpdates();
|
||||
|
||||
activeStartTime = null;
|
||||
activeTimer?.cancel();
|
||||
savePeriod(activeDuration);
|
||||
activeDuration = Duration.zero;
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
locationController.stopLocationUpdates();
|
||||
|
||||
activeStartTime = null;
|
||||
activeTimer?.cancel();
|
||||
savePeriod(activeDuration);
|
||||
activeDuration = Duration.zero;
|
||||
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: "حسابك مقيد مؤقتاً ⛔",
|
||||
titleStyle:
|
||||
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||
barrierDismissible: false, // 🚫 ممنوع الإغلاق بالضغط خارجاً
|
||||
onWillPop: () async => false, // 🚫 ممنوع زر الرجوع في الأندرويد
|
||||
content: Obx(() => Column(
|
||||
children: [
|
||||
const Icon(Icons.timer_off_outlined,
|
||||
size: 50, color: Colors.orange),
|
||||
const SizedBox(height: 15),
|
||||
const Text(
|
||||
"لقد تجاوزت حد الإلغاء المسموح به (3 مرات).\nلا يمكنك العمل حتى انتهاء العقوبة.",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
remainingBlockTimeStr.value, // 🔥 الوقت يتحدث هنا
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
)),
|
||||
confirm: Obx(() {
|
||||
// الزر يكون مفعلاً فقط عندما ينتهي الوقت
|
||||
bool isFinished = remainingBlockTimeStr.value == "00:00:00" ||
|
||||
remainingBlockTimeStr.value == "Done";
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isFinished ? Colors.green : Colors.grey,
|
||||
),
|
||||
onPressed: isFinished
|
||||
? () {
|
||||
Get.back(); // إغلاق الديالوج
|
||||
box.remove(BoxName.blockUntilDate); // إزالة الحظر
|
||||
Get.snackbar("أهلاً بك", "يمكنك الآن استقبال الطلبات",
|
||||
backgroundColor: Colors.green);
|
||||
}
|
||||
: null, // زر معطل
|
||||
child: Text(isFinished ? "Go Online" : "انتظر انتهاء الوقت"),
|
||||
);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// الوقت انتهى أصلاً -> تنظيف
|
||||
box.remove(BoxName.blockUntilDate);
|
||||
}
|
||||
}
|
||||
|
||||
/// دالة العداد التنازلي
|
||||
void _startBlockCountdown(DateTime expiry) {
|
||||
_blockTimer?.cancel();
|
||||
_blockTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
DateTime now = DateTime.now();
|
||||
if (now.isAfter(expiry)) {
|
||||
// انتهى الوقت
|
||||
remainingBlockTimeStr.value = "Done";
|
||||
timer.cancel();
|
||||
} else {
|
||||
// حساب الفرق وتنسيقه
|
||||
Duration diff = expiry.difference(now);
|
||||
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||
String hours = twoDigits(diff.inHours);
|
||||
String minutes = twoDigits(diff.inMinutes.remainder(60));
|
||||
String seconds = twoDigits(diff.inSeconds.remainder(60));
|
||||
|
||||
remainingBlockTimeStr.value = "$hours:$minutes:$seconds";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_blockTimer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void getRefusedOrderByCaptain() async {
|
||||
DateTime today = DateTime.now();
|
||||
int todayDay = today.day;
|
||||
|
||||
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: () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
void changeMapType() {
|
||||
mapType = !mapType;
|
||||
// heightButtomSheetShown = isButtomSheetShown == true ? 240 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeMapTraffic() {
|
||||
mapTrafficON = !mapTrafficON;
|
||||
update();
|
||||
}
|
||||
|
||||
// late GoogleMapController mapHomeCaptainController;
|
||||
GoogleMapController? mapHomeCaptainController;
|
||||
// final locationController = Get.find<LocationController>();
|
||||
|
||||
// --- FIX 2: Smart Map Creation ---
|
||||
void onMapCreated(GoogleMapController controller) {
|
||||
mapHomeCaptainController = controller;
|
||||
|
||||
// Check actual location before moving camera
|
||||
var currentLoc = locationController.myLocation;
|
||||
if (currentLoc.latitude != 0 && currentLoc.longitude != 0) {
|
||||
controller.animateCamera(
|
||||
CameraUpdate.newLatLng(currentLoc),
|
||||
);
|
||||
} else {
|
||||
// Optional: Move to default city view instead of ocean
|
||||
controller.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(myLocation, 10),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void startPeriodicExecution() {
|
||||
Timer.periodic(const Duration(minutes: 2), (timer) async {
|
||||
await getCaptainDurationOnToday();
|
||||
});
|
||||
}
|
||||
|
||||
void stopTimer() {
|
||||
timer?.cancel();
|
||||
}
|
||||
|
||||
getlocation() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
// This ensures we try to get a fix, but map doesn't crash if it fails
|
||||
await locationController.getLocation();
|
||||
|
||||
var loc = locationController.myLocation;
|
||||
if (loc.latitude != 0) {
|
||||
myLocation = loc;
|
||||
}
|
||||
|
||||
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;
|
||||
// إنعاش الخريطة عند العودة
|
||||
if (mapHomeCaptainController != null) {
|
||||
onMapCreated(mapHomeCaptainController!);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
// ✅ طلب الإذونات أولاً
|
||||
bool permissionsGranted = await PermissionsHelper.requestAllPermissions();
|
||||
|
||||
if (permissionsGranted) {
|
||||
// ✅ بدء الخدمة بعد الحصول على الإذونات
|
||||
await BackgroundServiceHelper.startService();
|
||||
print('✅ Background service started successfully');
|
||||
} else {
|
||||
print('❌ لم يتم منح الإذونات - الخدمة لن تعمل');
|
||||
// اعرض رسالة للمستخدم
|
||||
}
|
||||
// await locationBackController.requestLocationPermission();
|
||||
Get.put(FirebaseMessagesController());
|
||||
addToken();
|
||||
await getlocation();
|
||||
onButtonSelected();
|
||||
addCustomCarIcon();
|
||||
fetchHomeData();
|
||||
// refreshFinancialData();
|
||||
// getAllPayment();
|
||||
startPeriodicExecution();
|
||||
// getCaptainWalletFromBuyPoints();
|
||||
// 🔥 الفحص عند تشغيل التطبيق
|
||||
checkAndShowBlockDialog();
|
||||
box.write(BoxName.statusDriverLocation, 'off');
|
||||
// 2. عدل الليسنر ليصبح مشروطاً
|
||||
locationController.addListener(() {
|
||||
// الشرط الذهبي: إذا كانت الصفحة غير نشطة أو الخريطة غير موجودة، لا تفعل شيئاً
|
||||
if (!isHomeMapActive || mapHomeCaptainController == null || isClosed)
|
||||
return;
|
||||
|
||||
if (isActive) {
|
||||
// isActive الخاصة بالزر "متصل/غير متصل"
|
||||
var loc = locationController.myLocation;
|
||||
if (loc.latitude != 0 && loc.longitude != 0) {
|
||||
try {
|
||||
mapHomeCaptainController!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: loc,
|
||||
zoom: 17.5,
|
||||
tilt: 50.0,
|
||||
bearing: locationController.heading,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (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);
|
||||
}
|
||||
|
||||
int morningRideCount = 0;
|
||||
int afternoonRideCount = 0;
|
||||
String weeklyIncome = '0';
|
||||
|
||||
// الدالة الموحدة لجلب كل البيانات المالية دفعة واحدة
|
||||
Future<void> refreshFinancialData() async {
|
||||
isLoading = true; // اختياري
|
||||
update();
|
||||
|
||||
try {
|
||||
var res = await CRUD().post(
|
||||
link: "${AppLink.server}/ride/driverWallet/get_financial_summary.php",
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
|
||||
if (res != 'failure') {
|
||||
// معالجة الرد بذكاء سواء كان String أو Map
|
||||
Map<String, dynamic> body = (res is String) ? jsonDecode(res) : res;
|
||||
|
||||
if (body['status'] == 'success') {
|
||||
// البيانات قد تكون في data أو message
|
||||
var data = body['data'] ?? body['message'];
|
||||
|
||||
// 1. البيانات المالية (من السيرفر المحلي)
|
||||
totalMoneyInSEFER = data['cash_balance']?.toString() ?? '0';
|
||||
totalPoints = double.parse(data['points_balance']?.toString() ?? '0')
|
||||
.toStringAsFixed(0);
|
||||
totalMoneyToday = data['today_income']?.toString() ?? '0';
|
||||
|
||||
// 2. بيانات الرحلات (من السيرفر البعيد - إن وجدت)
|
||||
morningRideCount =
|
||||
int.tryParse(data['morning_count']?.toString() ?? '0') ?? 0;
|
||||
afternoonRideCount =
|
||||
int.tryParse(data['afternoon_count']?.toString() ?? '0') ?? 0;
|
||||
weeklyIncome = data['total_amount_last_week']?.toString() ?? '0';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error fetching unified summary: $e");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// دالة موحدة لجلب (الرحلات + التقييم + الإعدادات) في طلب واحد
|
||||
Future<void> fetchHomeData() async {
|
||||
// isLoading = true; // اختياري: إذا كنت تريد إظهار لودر
|
||||
// update();
|
||||
|
||||
try {
|
||||
var res = await CRUD().post(
|
||||
link:
|
||||
"${AppLink.server}/ride/kazan/get_home_init.php", // تأكد من المسار الجديد
|
||||
payload: {
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
'country': box.read(BoxName.countryCode).toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (res != 'failure') {
|
||||
var body = (res);
|
||||
if (body['status'] == 'success') {
|
||||
// التعامل مع البيانات القادمة سواء كانت في 'data' أو 'message'
|
||||
var data = body['data'] ?? body['message'];
|
||||
|
||||
// 1. تحديث عدد الرحلات
|
||||
if (data['ride_count'] != null) {
|
||||
countRideToday = data['ride_count'].toString();
|
||||
}
|
||||
|
||||
// 2. تحديث التقييم
|
||||
if (data['rating'] != null) {
|
||||
rating = double.tryParse(data['rating'].toString()) ?? 5.0;
|
||||
}
|
||||
|
||||
// 3. تحديث الأسعار (Kazan)
|
||||
if (data['config'] != null) {
|
||||
var config = data['config'];
|
||||
kazan = double.tryParse(config['kazan']?.toString() ?? '0') ?? 0.0;
|
||||
naturePrice =
|
||||
double.tryParse(config['naturePrice']?.toString() ?? '0') ??
|
||||
0.0;
|
||||
heavyPrice =
|
||||
double.tryParse(config['heavyPrice']?.toString() ?? '0') ?? 0.0;
|
||||
latePrice =
|
||||
double.tryParse(config['latePrice']?.toString() ?? '0') ?? 0.0;
|
||||
comfortPrice =
|
||||
double.tryParse(config['comfortPrice']?.toString() ?? '0') ??
|
||||
0.0;
|
||||
speedPrice =
|
||||
double.tryParse(config['speedPrice']?.toString() ?? '0') ?? 0.0;
|
||||
deliveryPrice =
|
||||
double.tryParse(config['deliveryPrice']?.toString() ?? '0') ??
|
||||
0.0;
|
||||
mashwariPrice =
|
||||
double.tryParse(config['freePrice']?.toString() ?? '0') ?? 0.0;
|
||||
familyPrice =
|
||||
double.tryParse(config['familyPrice']?.toString() ?? '0') ??
|
||||
0.0;
|
||||
fuelPrice =
|
||||
double.tryParse(config['fuelPrice']?.toString() ?? '0') ?? 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error fetching home data: $e");
|
||||
}
|
||||
|
||||
// تحديث الواجهة مرة واحدة فقط بعد جلب كل البيانات
|
||||
// isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
double mpg = 0;
|
||||
calculateConsumptionFuel() {
|
||||
mpg = fuelPrice / 12; //todo in register car add mpg in box
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
activeTimer?.cancel();
|
||||
stopTimer();
|
||||
mapHomeCaptainController?.dispose(); // Dispose controller
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,86 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.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(' ', ' ');
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:google_polyline_algorithm/google_polyline_algorithm.dart';
|
||||
import 'package:sefer_driver/constant/api_key.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/constant/links.dart';
|
||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||
import 'package:sefer_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;
|
||||
|
||||
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
|
||||
BitmapDescriptor passengerIcon = BitmapDescriptor.defaultMarker;
|
||||
BitmapDescriptor startIcon = BitmapDescriptor.defaultMarker;
|
||||
BitmapDescriptor endIcon = BitmapDescriptor.defaultMarker;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadCustomIcons();
|
||||
}
|
||||
|
||||
void _loadCustomIcons() async {
|
||||
carIcon = await _createBitmapDescriptor('assets/images/car.png');
|
||||
passengerIcon = await _createBitmapDescriptor('assets/images/picker.png');
|
||||
startIcon = await _createBitmapDescriptor('assets/images/A.png');
|
||||
endIcon = await _createBitmapDescriptor('assets/images/b.png');
|
||||
}
|
||||
|
||||
Future<BitmapDescriptor> _createBitmapDescriptor(String assetName) {
|
||||
return BitmapDescriptor.fromAssetImage(
|
||||
ImageConfiguration(
|
||||
size: const Size(30, 35), devicePixelRatio: Get.pixelRatio),
|
||||
assetName,
|
||||
);
|
||||
}
|
||||
|
||||
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 = decodePolyline(pointsString)
|
||||
.map((p) => LatLng(p[0].toDouble(), p[1].toDouble()))
|
||||
.toList();
|
||||
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
@@ -1,617 +0,0 @@
|
||||
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:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../../views/home/Captin/driver_map_page.dart';
|
||||
import '../../../views/home/Captin/orderCaptin/marker_generator.dart';
|
||||
import '../../../views/widgets/mydialoug.dart';
|
||||
import '../../firebase/local_notification.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/location_controller.dart';
|
||||
import '../../home/captin/home_captain_controller.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
import '../navigation/decode_polyline_isolate.dart';
|
||||
|
||||
class OrderRequestController extends GetxController
|
||||
with WidgetsBindingObserver {
|
||||
// --- متغيرات التايمر ---
|
||||
double progress = 1.0;
|
||||
int duration = 15;
|
||||
int remainingTime = 15;
|
||||
Timer? _timer;
|
||||
|
||||
bool applied = false;
|
||||
final locationController = Get.put(LocationController());
|
||||
|
||||
// 🔥 متغير لمنع تكرار القبول
|
||||
bool _isRideTakenHandled = false;
|
||||
|
||||
// --- الأيقونات والماركرز ---
|
||||
BitmapDescriptor? driverIcon;
|
||||
Map<MarkerId, Marker> markersMap = {};
|
||||
Set<Marker> get markers => markersMap.values.toSet();
|
||||
|
||||
// --- البيانات والتحكم ---
|
||||
// 🔥 تم إضافة myMapData لدعم السوكيت الجديد
|
||||
List<dynamic>? myList;
|
||||
Map<dynamic, dynamic>? myMapData;
|
||||
|
||||
GoogleMapController? mapController;
|
||||
|
||||
// الإحداثيات (أزلنا late لتجنب الأخطاء القاتلة)
|
||||
double latPassenger = 0.0;
|
||||
double lngPassenger = 0.0;
|
||||
double latDestination = 0.0;
|
||||
double lngDestination = 0.0;
|
||||
|
||||
// --- متغيرات العرض ---
|
||||
String passengerRating = "5.0";
|
||||
String tripType = "Standard";
|
||||
String totalTripDistance = "--";
|
||||
String totalTripDuration = "--";
|
||||
String tripPrice = "--";
|
||||
|
||||
String timeToPassenger = "جاري الحساب...";
|
||||
String distanceToPassenger = "--";
|
||||
|
||||
// --- الخريطة ---
|
||||
Set<Polyline> polylines = {};
|
||||
|
||||
// حالة التطبيق والصوت
|
||||
bool isInBackground = false;
|
||||
final AudioPlayer audioPlayer = AudioPlayer();
|
||||
|
||||
@override
|
||||
Future<void> onInit() async {
|
||||
// 🛑 حماية من الفتح المتكرر لنفس الطلب
|
||||
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);
|
||||
totalTripDistance = _safeGet(5);
|
||||
totalTripDuration = _safeGet(19);
|
||||
tripPrice = _safeGet(2);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 🔥🔥🔥 Core Logic: Concurrent API Calls & Bounds 🔥🔥🔥
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
Future<void> _calculateFullJourney() async {
|
||||
try {
|
||||
Position driverPos = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
LatLng driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
|
||||
|
||||
updateDriverLocation(driverLatLng, driverPos.heading);
|
||||
|
||||
var pickupFuture = _fetchRouteData(
|
||||
start: driverLatLng,
|
||||
end: LatLng(latPassenger, lngPassenger),
|
||||
color: Colors.amber,
|
||||
id: 'pickup_route');
|
||||
|
||||
var tripFuture = _fetchRouteData(
|
||||
start: LatLng(latPassenger, lngPassenger),
|
||||
end: LatLng(latDestination, lngDestination),
|
||||
color: Colors.green,
|
||||
id: 'trip_route',
|
||||
isDashed: true);
|
||||
|
||||
var results = await Future.wait([pickupFuture, tripFuture]);
|
||||
|
||||
var pickupResult = results[0];
|
||||
var tripResult = results[1];
|
||||
|
||||
if (pickupResult != null) {
|
||||
distanceToPassenger = pickupResult['distance_text'];
|
||||
timeToPassenger = pickupResult['duration_text'];
|
||||
polylines.add(pickupResult['polyline']);
|
||||
}
|
||||
|
||||
if (tripResult != null) {
|
||||
totalTripDistance = tripResult['distance_text'];
|
||||
totalTripDuration = tripResult['duration_text'];
|
||||
polylines.add(tripResult['polyline']);
|
||||
}
|
||||
|
||||
await _updateMarkers(
|
||||
paxTime: timeToPassenger,
|
||||
paxDist: distanceToPassenger,
|
||||
destTime: totalTripDuration,
|
||||
destDist: totalTripDistance);
|
||||
|
||||
zoomToFitRide(driverLatLng);
|
||||
|
||||
update();
|
||||
} catch (e) {
|
||||
print("❌ Error in Journey Calculation: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> _fetchRouteData(
|
||||
{required LatLng start,
|
||||
required LatLng end,
|
||||
required Color color,
|
||||
required String id,
|
||||
bool isDashed = false}) async {
|
||||
try {
|
||||
// حماية من الإحداثيات الصفرية
|
||||
if (start.latitude == 0 || end.latitude == 0) return null;
|
||||
|
||||
String apiUrl = "https://routesy.intaleq.xyz/route/v1/driving";
|
||||
String coords =
|
||||
"${start.longitude},${start.latitude};${end.longitude},${end.latitude}";
|
||||
String url = "$apiUrl/$coords?steps=false&overview=full";
|
||||
|
||||
var response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode == 200) {
|
||||
var json = jsonDecode(response.body);
|
||||
if (json['code'] == 'Ok' && json['routes'].isNotEmpty) {
|
||||
var route = json['routes'][0];
|
||||
|
||||
double distM = double.parse(route['distance'].toString());
|
||||
double durS = double.parse(route['duration'].toString());
|
||||
|
||||
String distText = "${(distM / 1000).toStringAsFixed(1)} كم";
|
||||
String durText = "${(durS / 60).toStringAsFixed(0)} دقيقة";
|
||||
|
||||
String geometry = route['geometry'];
|
||||
List<LatLng> points = await compute(decodePolylineIsolate, geometry);
|
||||
|
||||
Polyline polyline = Polyline(
|
||||
polylineId: PolylineId(id),
|
||||
color: color,
|
||||
width: 5,
|
||||
points: points,
|
||||
patterns:
|
||||
isDashed ? [PatternItem.dash(10), PatternItem.gap(5)] : [],
|
||||
startCap: Cap.roundCap,
|
||||
endCap: Cap.roundCap,
|
||||
);
|
||||
|
||||
return {
|
||||
'distance_text': distText,
|
||||
'duration_text': durText,
|
||||
'polyline': polyline
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("Route Fetch Error: $e");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void zoomToFitRide(LatLng driverPos) {
|
||||
if (mapController == null) return;
|
||||
|
||||
// حماية من النقاط الصفرية
|
||||
if (latPassenger == 0 || latDestination == 0) return;
|
||||
|
||||
List<LatLng> points = [
|
||||
driverPos,
|
||||
LatLng(latPassenger, lngPassenger),
|
||||
LatLng(latDestination, lngDestination),
|
||||
];
|
||||
|
||||
double minLat = points.first.latitude;
|
||||
double maxLat = points.first.latitude;
|
||||
double minLng = points.first.longitude;
|
||||
double maxLng = points.first.longitude;
|
||||
|
||||
for (var p in points) {
|
||||
if (p.latitude < minLat) minLat = p.latitude;
|
||||
if (p.latitude > maxLat) maxLat = p.latitude;
|
||||
if (p.longitude < minLng) minLng = p.longitude;
|
||||
if (p.longitude > maxLng) maxLng = p.longitude;
|
||||
}
|
||||
|
||||
mapController!.animateCamera(CameraUpdate.newLatLngBounds(
|
||||
LatLngBounds(
|
||||
southwest: LatLng(minLat, minLng),
|
||||
northeast: LatLng(maxLat, maxLng),
|
||||
),
|
||||
100.0,
|
||||
));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Markers & Setup
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
Future<void> _prepareDriverIcon() async {
|
||||
driverIcon = await MarkerGenerator.createDriverMarker();
|
||||
}
|
||||
|
||||
Future<void> _updateMarkers(
|
||||
{required String paxTime,
|
||||
required String paxDist,
|
||||
String? destTime,
|
||||
String? destDist}) async {
|
||||
// حماية إذا لم يتم جلب الإحداثيات
|
||||
if (latPassenger == 0 || latDestination == 0) return;
|
||||
|
||||
final BitmapDescriptor pickupIcon =
|
||||
await MarkerGenerator.createCustomMarkerBitmap(
|
||||
title: paxTime,
|
||||
subtitle: paxDist,
|
||||
color: Colors.green.shade700,
|
||||
iconData: Icons.person_pin_circle,
|
||||
);
|
||||
|
||||
final BitmapDescriptor dropoffIcon =
|
||||
await MarkerGenerator.createCustomMarkerBitmap(
|
||||
title: destTime ?? totalTripDuration,
|
||||
subtitle: destDist ?? totalTripDistance,
|
||||
color: Colors.red.shade800,
|
||||
iconData: Icons.flag,
|
||||
);
|
||||
|
||||
markersMap[const MarkerId('pax')] = Marker(
|
||||
markerId: const MarkerId('pax'),
|
||||
position: LatLng(latPassenger, lngPassenger),
|
||||
icon: pickupIcon,
|
||||
anchor: const Offset(0.5, 0.85),
|
||||
);
|
||||
|
||||
markersMap[const MarkerId('dest')] = Marker(
|
||||
markerId: const MarkerId('dest'),
|
||||
position: LatLng(latDestination, lngDestination),
|
||||
icon: dropoffIcon,
|
||||
anchor: const Offset(0.5, 0.85),
|
||||
);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void _initialMapSetup() async {
|
||||
Position driverPos = await Geolocator.getCurrentPosition();
|
||||
LatLng driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
|
||||
|
||||
if (driverIcon != null) {
|
||||
markersMap[const MarkerId('driver')] = Marker(
|
||||
markerId: const MarkerId('driver'),
|
||||
position: driverLatLng,
|
||||
icon: driverIcon!,
|
||||
rotation: driverPos.heading,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
flat: true,
|
||||
zIndex: 10);
|
||||
}
|
||||
|
||||
if (latPassenger != 0 && lngPassenger != 0) {
|
||||
polylines.add(Polyline(
|
||||
polylineId: const PolylineId('temp_line'),
|
||||
points: [driverLatLng, LatLng(latPassenger, lngPassenger)],
|
||||
color: Colors.grey,
|
||||
width: 2,
|
||||
patterns: [PatternItem.dash(10), PatternItem.gap(10)],
|
||||
));
|
||||
|
||||
zoomToFitRide(driverLatLng);
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void updateDriverLocation(LatLng newPos, double heading) {
|
||||
if (driverIcon != null) {
|
||||
markersMap[const MarkerId('driver')] = Marker(
|
||||
markerId: const MarkerId('driver'),
|
||||
position: newPos,
|
||||
icon: driverIcon!,
|
||||
rotation: heading,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
flat: true,
|
||||
zIndex: 10,
|
||||
);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void onMapCreated(GoogleMapController controller) {
|
||||
mapController = controller;
|
||||
}
|
||||
|
||||
// --- قبول الطلب وإدارة التايمر ---
|
||||
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) Get.back();
|
||||
Get.back();
|
||||
Get.snackbar("تنبيه", "تم قبول الطلب من قبل سائق آخر",
|
||||
backgroundColor: Colors.orange, colorText: Colors.white);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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("عذراً، الطلب أخذه سائق آخر.", '', () {
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user