first commit
This commit is contained in:
146
siro_driver/lib/controller/home/captin/behavior_controller.dart
Normal file
146
siro_driver/lib/controller/home/captin/behavior_controller.dart
Normal 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
|
||||
}
|
||||
}
|
||||
78
siro_driver/lib/controller/home/captin/contact_us_controller.dart
Executable file
78
siro_driver/lib/controller/home/captin/contact_us_controller.dart
Executable 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
167
siro_driver/lib/controller/home/captin/duration_controller .dart
Executable file
167
siro_driver/lib/controller/home/captin/duration_controller .dart
Executable 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);
|
||||
}
|
||||
58
siro_driver/lib/controller/home/captin/help/assurance_controller.dart
Executable file
58
siro_driver/lib/controller/home/captin/help/assurance_controller.dart
Executable 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
103
siro_driver/lib/controller/home/captin/help/help_controller.dart
Executable file
103
siro_driver/lib/controller/home/captin/help/help_controller.dart
Executable 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();
|
||||
}
|
||||
}
|
||||
22
siro_driver/lib/controller/home/captin/help/maintain_center_controller.dart
Executable file
22
siro_driver/lib/controller/home/captin/help/maintain_center_controller.dart
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
69
siro_driver/lib/controller/home/captin/help/video_controller.dart
Executable file
69
siro_driver/lib/controller/home/captin/help/video_controller.dart
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
850
siro_driver/lib/controller/home/captin/home_captain_controller.dart
Executable file
850
siro_driver/lib/controller/home/captin/home_captain_controller.dart
Executable 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();
|
||||
}
|
||||
}
|
||||
2797
siro_driver/lib/controller/home/captin/map_driver_controller.dart
Executable file
2797
siro_driver/lib/controller/home/captin/map_driver_controller.dart
Executable file
File diff suppressed because it is too large
Load Diff
86
siro_driver/lib/controller/home/captin/model.dart
Normal file
86
siro_driver/lib/controller/home/captin/model.dart
Normal 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(' ', ' ');
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
101
siro_driver/lib/controller/home/captin/navigation_service.dart
Normal file
101
siro_driver/lib/controller/home/captin/navigation_service.dart
Normal 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 = "";
|
||||
}
|
||||
}
|
||||
742
siro_driver/lib/controller/home/captin/order_request_controller.dart
Executable file
742
siro_driver/lib/controller/home/captin/order_request_controller.dart
Executable 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;
|
||||
}
|
||||
}
|
||||
212
siro_driver/lib/controller/home/captin/v2_review_delta.html
Normal file
212
siro_driver/lib/controller/home/captin/v2_review_delta.html
Normal 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<bool>();
|
||||
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<String></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<String> 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>
|
||||
106
siro_driver/lib/controller/home/journal/schedule_controller.dart
Normal file
106
siro_driver/lib/controller/home/journal/schedule_controller.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
|
||||
class WorkSlot {
|
||||
int dayOfWeek; // 1=Mon ... 7=Sun
|
||||
TimeOfDay startTime;
|
||||
TimeOfDay endTime;
|
||||
bool isActive;
|
||||
|
||||
WorkSlot({required this.dayOfWeek, required this.startTime, required this.endTime, this.isActive = true});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'day': dayOfWeek, 'startH': startTime.hour, 'startM': startTime.minute,
|
||||
'endH': endTime.hour, 'endM': endTime.minute, 'active': isActive,
|
||||
};
|
||||
|
||||
factory WorkSlot.fromJson(Map<String, dynamic> json) => WorkSlot(
|
||||
dayOfWeek: json['day'] ?? 1,
|
||||
startTime: TimeOfDay(hour: json['startH'] ?? 8, minute: json['startM'] ?? 0),
|
||||
endTime: TimeOfDay(hour: json['endH'] ?? 17, minute: json['endM'] ?? 0),
|
||||
isActive: json['active'] ?? true,
|
||||
);
|
||||
|
||||
String get dayName {
|
||||
const days = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return days[dayOfWeek];
|
||||
}
|
||||
|
||||
String get dayNameAr {
|
||||
const days = ['', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'];
|
||||
return days[dayOfWeek];
|
||||
}
|
||||
|
||||
String formatTime(TimeOfDay t) => '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
|
||||
String get timeRange => '${formatTime(startTime)} - ${formatTime(endTime)}';
|
||||
}
|
||||
|
||||
class ScheduleController extends GetxController {
|
||||
List<WorkSlot> schedule = [];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadSchedule();
|
||||
}
|
||||
|
||||
void _loadSchedule() {
|
||||
final saved = box.read('work_schedule');
|
||||
if (saved != null) {
|
||||
try {
|
||||
final list = jsonDecode(saved) as List;
|
||||
schedule = list.map((e) => WorkSlot.fromJson(e)).toList();
|
||||
} catch (_) {
|
||||
_initDefault();
|
||||
}
|
||||
} else {
|
||||
_initDefault();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void _initDefault() {
|
||||
schedule = List.generate(7, (i) => WorkSlot(
|
||||
dayOfWeek: i + 1,
|
||||
startTime: const TimeOfDay(hour: 8, minute: 0),
|
||||
endTime: const TimeOfDay(hour: 18, minute: 0),
|
||||
isActive: i < 6, // الجمعة عطلة
|
||||
));
|
||||
}
|
||||
|
||||
void _save() {
|
||||
box.write('work_schedule', jsonEncode(schedule.map((s) => s.toJson()).toList()));
|
||||
update();
|
||||
}
|
||||
|
||||
void toggleDay(int dayOfWeek) {
|
||||
final slot = schedule.firstWhere((s) => s.dayOfWeek == dayOfWeek);
|
||||
slot.isActive = !slot.isActive;
|
||||
_save();
|
||||
}
|
||||
|
||||
void updateStartTime(int dayOfWeek, TimeOfDay time) {
|
||||
schedule.firstWhere((s) => s.dayOfWeek == dayOfWeek).startTime = time;
|
||||
_save();
|
||||
}
|
||||
|
||||
void updateEndTime(int dayOfWeek, TimeOfDay time) {
|
||||
schedule.firstWhere((s) => s.dayOfWeek == dayOfWeek).endTime = time;
|
||||
_save();
|
||||
}
|
||||
|
||||
double get totalWeeklyHours {
|
||||
double total = 0;
|
||||
for (var s in schedule) {
|
||||
if (!s.isActive) continue;
|
||||
final startMin = s.startTime.hour * 60 + s.startTime.minute;
|
||||
final endMin = s.endTime.hour * 60 + s.endTime.minute;
|
||||
total += (endMin - startMin) / 60;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
int get activeDays => schedule.where((s) => s.isActive).length;
|
||||
}
|
||||
14
siro_driver/lib/controller/home/menu_controller.dart
Executable file
14
siro_driver/lib/controller/home/menu_controller.dart
Executable file
@@ -0,0 +1,14 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MyMenuController extends GetxController {
|
||||
bool isDrawerOpen = true;
|
||||
|
||||
void getDrawerMenu() {
|
||||
if (isDrawerOpen == true) {
|
||||
isDrawerOpen = false;
|
||||
} else {
|
||||
isDrawerOpen = true;
|
||||
}
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
List<LatLng> decodePolylineIsolate(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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
1154
siro_driver/lib/controller/home/navigation/navigation_view.dart
Normal file
1154
siro_driver/lib/controller/home/navigation/navigation_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,145 @@
|
||||
// lib/controllers/navigation/route_matcher_worker.dart
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'dart:math';
|
||||
|
||||
/// Worker entrypoint (spawnUri/spawn).
|
||||
/// Messages:
|
||||
/// - init: {'type':'init','coords': Float64List}
|
||||
/// - match: {'type':'match','id': int, 'lat': double, 'lng': double, 'lastIndex': int, 'window': int}
|
||||
/// - dispose: {'type':'dispose'}
|
||||
///
|
||||
/// Responses are sent back as Map via SendPort:
|
||||
/// - {'type':'ready'}
|
||||
/// - {'type':'matchResult','id': id, 'index': overallIndex, 'lat': lat, 'lng': lng, 'dist': meters}
|
||||
void routeMatcherIsolateEntry(SendPort sendPort) {
|
||||
final ReceivePort port = ReceivePort();
|
||||
sendPort.send({'type': 'ready', 'port': port.sendPort});
|
||||
|
||||
Float64List? flat; // [lat,lng,lat,lng,...]
|
||||
int nPoints = 0;
|
||||
|
||||
port.listen((dynamic message) {
|
||||
try {
|
||||
if (message is Map<String, dynamic>) {
|
||||
final type = message['type'] as String? ?? '';
|
||||
if (type == 'init') {
|
||||
final data = message['coords'] as Float64List?;
|
||||
if (data != null) {
|
||||
flat = data;
|
||||
nPoints = flat!.length ~/ 2;
|
||||
sendPort.send({'type': 'inited', 'points': nPoints});
|
||||
} else {
|
||||
sendPort.send({'type': 'error', 'message': 'init missing coords'});
|
||||
}
|
||||
} else if (type == 'match') {
|
||||
if (flat == null) {
|
||||
sendPort.send({'type': 'error', 'message': 'not inited'});
|
||||
return;
|
||||
}
|
||||
final int id = message['id'] as int;
|
||||
final double lat = (message['lat'] as num).toDouble();
|
||||
final double lng = (message['lng'] as num).toDouble();
|
||||
final int lastIndex = (message['lastIndex'] as int?) ?? 0;
|
||||
final int window = (message['window'] as int?) ?? 120;
|
||||
|
||||
final result =
|
||||
_findClosestWindowInternal(flat!, lat, lng, lastIndex, window);
|
||||
sendPort.send({
|
||||
'type': 'matchResult',
|
||||
'id': id,
|
||||
'index': result['index'],
|
||||
'lat': result['lat'],
|
||||
'lng': result['lng'],
|
||||
'dist': result['dist']
|
||||
});
|
||||
} else if (type == 'dispose') {
|
||||
port.close();
|
||||
sendPort.send({'type': 'disposed'});
|
||||
} else {
|
||||
sendPort.send({'type': 'error', 'message': 'unknown message type'});
|
||||
}
|
||||
}
|
||||
} catch (e, st) {
|
||||
sendPort.send(
|
||||
{'type': 'error', 'message': e.toString(), 'stack': st.toString()});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Internal helper: projection on segments, windowed search.
|
||||
/// Returns Map {index, lat, lng, dist}
|
||||
Map<String, dynamic> _findClosestWindowInternal(
|
||||
Float64List flat, double lat, double lng, int lastIndex, int window) {
|
||||
final int n = flat.length ~/ 2;
|
||||
final int start = max(0, lastIndex - window);
|
||||
final int end = min(n - 1, lastIndex + window);
|
||||
|
||||
double minDist = double.infinity;
|
||||
int bestIdx = lastIndex;
|
||||
double bestLat = flat[lastIndex * 2];
|
||||
double bestLng = flat[lastIndex * 2 + 1];
|
||||
|
||||
for (int i = start; i < end; i++) {
|
||||
final double aLat = flat[i * 2];
|
||||
final double aLng = flat[i * 2 + 1];
|
||||
final double bLat = flat[(i + 1) * 2];
|
||||
final double bLng = flat[(i + 1) * 2 + 1];
|
||||
|
||||
final proj = _closestPointOnSegmentLatLng(lat, lng, aLat, aLng, bLat, bLng);
|
||||
final double d = proj['dist'] as double;
|
||||
if (d < minDist) {
|
||||
minDist = d;
|
||||
bestLat = proj['lat'] as double;
|
||||
bestLng = proj['lng'] as double;
|
||||
// choose overall index: i or i+1 depending on t
|
||||
final double t = proj['t'] as double;
|
||||
bestIdx = i + (t > 0.5 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
return {'index': bestIdx, 'lat': bestLat, 'lng': bestLng, 'dist': minDist};
|
||||
}
|
||||
|
||||
/// Projection math on geodetic points approximated in degrees (good for short distances).
|
||||
Map<String, dynamic> _closestPointOnSegmentLatLng(
|
||||
double px, double py, double ax, double ay, double bx, double by) {
|
||||
// Here px=lat, py=lng; ax=lat, ay=lng, etc.
|
||||
final double x0 = px;
|
||||
final double y0 = py;
|
||||
final double x1 = ax;
|
||||
final double y1 = ay;
|
||||
final double x2 = bx;
|
||||
final double y2 = by;
|
||||
|
||||
final double dx = x2 - x1;
|
||||
final double dy = y2 - y1;
|
||||
double t = 0.0;
|
||||
final double len2 = dx * dx + dy * dy;
|
||||
if (len2 > 0) {
|
||||
t = ((x0 - x1) * dx + (y0 - y1) * dy) / len2;
|
||||
if (t < 0) t = 0;
|
||||
if (t > 1) t = 1;
|
||||
}
|
||||
final double projX = x1 + t * dx;
|
||||
final double projY = y1 + t * dy;
|
||||
|
||||
final double distMeters = _haversineDistanceMeters(x0, y0, projX, projY);
|
||||
return {'lat': projX, 'lng': projY, 't': t, 'dist': distMeters};
|
||||
}
|
||||
|
||||
/// Haversine distance (meters)
|
||||
double _haversineDistanceMeters(
|
||||
double lat1, double lng1, double lat2, double lng2) {
|
||||
final double R = 6371000.0;
|
||||
final double dLat = _deg2rad(lat2 - lat1);
|
||||
final double dLon = _deg2rad(lng2 - lng1);
|
||||
final double a = sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(_deg2rad(lat1)) * cos(_deg2rad(lat2)) * sin(dLon / 2) * sin(dLon / 2);
|
||||
final double c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
|
||||
double _deg2rad(double deg) => deg * pi / 180.0;
|
||||
357
siro_driver/lib/controller/home/payment/captain_wallet_controller.dart
Executable file
357
siro_driver/lib/controller/home/payment/captain_wallet_controller.dart
Executable file
@@ -0,0 +1,357 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/firebase/firbase_messge.dart';
|
||||
import 'package:siro_driver/controller/firebase/local_notification.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:flutter/material.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 '../../../views/widgets/mydialoug.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
|
||||
class CaptainWalletController extends GetxController {
|
||||
bool isLoading = false;
|
||||
final formKeyTransfer = GlobalKey<FormState>();
|
||||
final formKeyAccount = GlobalKey<FormState>();
|
||||
Map walletDate = {};
|
||||
Map walletDateVisa = {};
|
||||
Map walletDriverPointsDate = {};
|
||||
final formKey = GlobalKey<FormState>();
|
||||
String totalAmount = '0';
|
||||
double kazan = 0;
|
||||
String totalAmountVisa = '0';
|
||||
String totalPoints = '0';
|
||||
final amountFromBudgetController = TextEditingController();
|
||||
final newDriverPhoneController = TextEditingController();
|
||||
final phoneWallet = TextEditingController();
|
||||
final cardBank = TextEditingController();
|
||||
final bankCode = TextEditingController();
|
||||
|
||||
payFromBudget() async {
|
||||
if (formKey.currentState!.validate()) {
|
||||
var pointFromBudget = int.parse((amountFromBudgetController.text));
|
||||
|
||||
// await getPaymentId('fromBudgetToPoints',
|
||||
// int.parse((amountFromBudgetController.text)) * -1);
|
||||
var paymentToken3 =
|
||||
await generateToken((pointFromBudget * -1).toString());
|
||||
var paymentID = await getPaymentId(
|
||||
'fromBudgetToPoints', (pointFromBudget * -1).toString());
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'amount': (pointFromBudget * -1).toString(),
|
||||
'rideId': paymentID.toString(),
|
||||
'payment_method': 'myBudget',
|
||||
'passengerID': 'myBudgetToPoint',
|
||||
'token': paymentToken3,
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
Future.delayed(const Duration(seconds: 1));
|
||||
await addDriverWallet(
|
||||
'fromBudget', pointFromBudget.toString(), pointFromBudget.toString());
|
||||
update();
|
||||
Get.back();
|
||||
await refreshCaptainWallet();
|
||||
NotificationController().showNotification(
|
||||
'You have successfully charged your account'.tr,
|
||||
'$pointFromBudget ${'has been added to your budget'.tr}',
|
||||
'tone1',
|
||||
'',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future refreshCaptainWallet() async {
|
||||
await getCaptainWalletFromRide();
|
||||
await getCaptainWalletFromBuyPoints();
|
||||
// await checkAccountCaptainBank();
|
||||
}
|
||||
|
||||
List amountToNewDriverMap = [];
|
||||
bool isNewTransfer = false;
|
||||
Future detectNewDriverFromMyBudget() async {
|
||||
if (formKeyTransfer.currentState!.validate()) {
|
||||
if (int.parse(amountFromBudgetController.text) <
|
||||
double.parse(totalAmountVisa) &&
|
||||
int.parse(amountFromBudgetController.text) > 10) {
|
||||
//get new driver details
|
||||
isNewTransfer = true;
|
||||
update();
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getDriverDetails,
|
||||
payload: {'driver_phone': '+2${newDriverPhoneController.text}'});
|
||||
isNewTransfer = false;
|
||||
update();
|
||||
if (res != 'failure') {
|
||||
var d = jsonDecode(res);
|
||||
amountToNewDriverMap = d['data'];
|
||||
// update();
|
||||
} else {
|
||||
mySnackeBarError("This driver is not registered".tr);
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('Your Budget less than needed'.tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future getCaptainWalletFromRide() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
var res = await CRUD().getWallet(
|
||||
link: AppLink.getAllPaymentFromRide,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
// isLoading = false;
|
||||
if (res != 'failure') {
|
||||
walletDate = jsonDecode(res);
|
||||
totalAmount = walletDate['message'][0]['total_amount'] ?? '0';
|
||||
update();
|
||||
var res1 = await CRUD().getWallet(
|
||||
link: AppLink.getAllPaymentVisa,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()});
|
||||
walletDateVisa = jsonDecode(res1);
|
||||
totalAmountVisa = walletDateVisa['message'][0]['diff'].toString();
|
||||
|
||||
update();
|
||||
} else {
|
||||
totalAmount = "0";
|
||||
totalAmountVisa = "0";
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
totalPoints = '0';
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
String paymentToken = '';
|
||||
Future<String> generateToken(String amount) async {
|
||||
var res =
|
||||
await CRUD().postWallet(link: AppLink.addPaymentTokenDriver, payload: {
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
'amount': amount.toString(),
|
||||
});
|
||||
var d = (res);
|
||||
return d['message'];
|
||||
}
|
||||
|
||||
// late String paymentID;
|
||||
Future<String> getPaymentId(String paymentMethod, amount) async {
|
||||
// paymentToken = await generateToken(amount);
|
||||
var res =
|
||||
await CRUD().postWallet(link: AppLink.addDriverPaymentPoints, payload: {
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
'amount': amount.toString(),
|
||||
'payment_method': paymentMethod.toString(),
|
||||
});
|
||||
var d = (res);
|
||||
// paymentID = d['message'].toString();
|
||||
return d['message'];
|
||||
}
|
||||
|
||||
Future addDriverWallet(String paymentMethod, point, count) async {
|
||||
paymentToken = await generateToken(count);
|
||||
var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||
await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: {
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
'paymentID': paymentID.toString(),
|
||||
'amount': point,
|
||||
'token': paymentToken,
|
||||
'paymentMethod': paymentMethod.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
Future addDriverPayment(String paymentMethod, point, wayPay) async {
|
||||
paymentToken = await generateToken(point);
|
||||
var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'amount': point.toString(),
|
||||
'rideId': paymentID.toString(),
|
||||
'payment_method': paymentMethod,
|
||||
'passengerID': wayPay,
|
||||
'token': paymentToken,
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
Future addDriverWalletFromPromo(String paymentMethod, point) async {
|
||||
var resPromotion =
|
||||
await CRUD().postWallet(link: AppLink.addpromotionDriver, payload: {
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
'payment_amount': point,
|
||||
'timePromo': paymentMethod,
|
||||
});
|
||||
if (resPromotion != 'failure') {
|
||||
paymentToken = await generateToken(point);
|
||||
var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||
var res =
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'amount': point,
|
||||
'rideId': paymentID.toString(),
|
||||
'payment_method': paymentMethod.toString(),
|
||||
'passengerID': paymentMethod,
|
||||
'token': paymentToken,
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
if (res != 'failure') {
|
||||
String title = 'wallet_updated'.tr; // Notification title
|
||||
String message = '${'wallet_credited_message'.tr} $point';
|
||||
String tone = 'default_tone'.tr; // Notification tone or sound
|
||||
String payLoad =
|
||||
'wallet_updated'; // Additional data payload for the notification
|
||||
|
||||
Get.find<NotificationController>()
|
||||
.showNotification(title, message, tone, payLoad);
|
||||
}
|
||||
} else {
|
||||
Get.back();
|
||||
mySnackeBarError(
|
||||
"A promotion record for this driver already exists for today.".tr);
|
||||
}
|
||||
}
|
||||
|
||||
Future addDriverWalletToInvitor(String paymentMethod, driverID, point) async {
|
||||
paymentToken = await generateToken(point);
|
||||
var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'driverID': driverID,
|
||||
'amount': point,
|
||||
'token': paymentToken,
|
||||
'rideId': paymentID.toString(),
|
||||
'payment_method': paymentMethod.toString(),
|
||||
'passengerID': paymentMethod,
|
||||
});
|
||||
await addSeferWallet(paymentMethod,
|
||||
(double.parse(point) * -2).toString()); // deduct 2 from sefer wallet
|
||||
}
|
||||
|
||||
Future addSeferWallet(String paymentMethod, String point) async {
|
||||
var seferToken = await generateToken(point.toString());
|
||||
await CRUD().postWallet(link: AppLink.addSeferWallet, payload: {
|
||||
'amount': point.toString(),
|
||||
'paymentMethod': paymentMethod,
|
||||
'passengerId': 'driver',
|
||||
'token': seferToken,
|
||||
'driverId': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
Future addTransferDriversWallet(String paymentMethod1, paymentMethod2) async {
|
||||
var paymentID =
|
||||
await getPaymentId(paymentMethod1, amountFromBudgetController.text);
|
||||
paymentToken = await generateToken(
|
||||
(int.parse(amountFromBudgetController.text) * -1).toString());
|
||||
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'amount': (int.parse(amountFromBudgetController.text) * -1).toString(),
|
||||
'rideId': paymentID.toString(),
|
||||
'payment_method': paymentMethod1,
|
||||
'passengerID': 'To ${amountToNewDriverMap[0]['id']}',
|
||||
'token': paymentToken,
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
|
||||
paymentID = await getPaymentId(paymentMethod2,
|
||||
(int.parse(amountFromBudgetController.text) - 5).toString());
|
||||
paymentToken = await generateToken(amountFromBudgetController.text);
|
||||
var res1 =
|
||||
await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: {
|
||||
'driverID': amountToNewDriverMap[0]['id'].toString(),
|
||||
'paymentID': paymentID.toString(),
|
||||
'amount': ((int.parse(amountFromBudgetController.text) - 5))
|
||||
// kazan) // double.parse(kazan) .08 for egypt
|
||||
.toStringAsFixed(
|
||||
0), // this will convert buddget to poitns by kazan .08
|
||||
|
||||
'token': paymentToken,
|
||||
'paymentMethod': paymentMethod2.toString(),
|
||||
});
|
||||
if (res1 != 'failure') {
|
||||
// Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
|
||||
// 'Transfer',
|
||||
// '${'You have transfer to your wallet from'.tr}'
|
||||
// '${box.read(BoxName.nameDriver)}',
|
||||
// amountToNewDriverMap[0]['token'].toString(),
|
||||
// [],
|
||||
// 'order1.wav');
|
||||
NotificationService.sendNotification(
|
||||
target: amountToNewDriverMap[0]['token'].toString(),
|
||||
title: 'Transfer'.tr,
|
||||
body: '${'You have transfer to your wallet from'.tr}'
|
||||
'${box.read(BoxName.nameDriver)}',
|
||||
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'ding',
|
||||
driverList: [], category: 'Transfer',
|
||||
);
|
||||
await addSeferWallet('payout fee', '5');
|
||||
|
||||
Get.defaultDialog(
|
||||
title: 'transfer Successful'.tr,
|
||||
middleText: '',
|
||||
titleStyle: AppStyle.title,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
Get.back();
|
||||
|
||||
await refreshCaptainWallet();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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'].toString());
|
||||
// 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']);
|
||||
// fuelPrice = double.parse(json['message'][0]['fuelPrice']);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
// getKazanPercent();
|
||||
|
||||
await refreshCaptainWallet();
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
75
siro_driver/lib/controller/home/payment/credit_card_Controller.dart
Executable file
75
siro_driver/lib/controller/home/payment/credit_card_Controller.dart
Executable file
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../functions/digit_obsecur_formate.dart';
|
||||
import '../../functions/secure_storage.dart';
|
||||
|
||||
class CreditCardController extends GetxController {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
final TextEditingController cardNumberController = TextEditingController();
|
||||
final TextEditingController cardHolderNameController =
|
||||
TextEditingController();
|
||||
final TextEditingController expiryDateController = TextEditingController();
|
||||
final TextEditingController cvvCodeController = TextEditingController();
|
||||
openPayment() async {
|
||||
String? cardNumber = await SecureStorage().readData(BoxName.cardNumber);
|
||||
String? cardHolderName =
|
||||
await SecureStorage().readData(BoxName.cardHolderName);
|
||||
String? expiryDate = await SecureStorage().readData(BoxName.expiryDate);
|
||||
String? cvvCode = await SecureStorage().readData(BoxName.cvvCode);
|
||||
|
||||
if (cvvCode != null && cvvCode.isNotEmpty) {
|
||||
final maskedCardNumber = DigitObscuringFormatter()
|
||||
.formatEditUpdate(
|
||||
TextEditingValue.empty,
|
||||
TextEditingValue(text: cardNumber ?? ''),
|
||||
)
|
||||
.text;
|
||||
|
||||
cardNumberController.text = maskedCardNumber;
|
||||
cardHolderNameController.text = cardHolderName ?? '';
|
||||
expiryDateController.text = expiryDate ?? '';
|
||||
cvvCodeController.text = cvvCode;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
openPayment();
|
||||
// String? cardNumber = await SecureStorage().readData(BoxName.cardNumber);
|
||||
// String? cardHolderName =
|
||||
// await SecureStorage().readData(BoxName.cardHolderName);
|
||||
// String? expiryDate = await SecureStorage().readData(BoxName.expiryDate);
|
||||
// String? cvvCode = await SecureStorage().readData(BoxName.cvvCode);
|
||||
|
||||
// if (cvvCode != null && cvvCode.isNotEmpty) {
|
||||
// final maskedCardNumber = DigitObscuringFormatter()
|
||||
// .formatEditUpdate(
|
||||
// TextEditingValue.empty,
|
||||
// TextEditingValue(text: cardNumber ?? ''),
|
||||
// )
|
||||
// .text;
|
||||
|
||||
// cardNumberController.text = maskedCardNumber;
|
||||
// cardHolderNameController.text = cardHolderName ?? '';
|
||||
// expiryDateController.text = expiryDate ?? '';
|
||||
// cvvCodeController.text = cvvCode;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
class CreditCardModel {
|
||||
String cardNumber;
|
||||
String cardHolderName;
|
||||
String expiryDate;
|
||||
String cvvCode;
|
||||
|
||||
CreditCardModel({
|
||||
required this.cardNumber,
|
||||
required this.cardHolderName,
|
||||
required this.expiryDate,
|
||||
required this.cvvCode,
|
||||
});
|
||||
}
|
||||
235
siro_driver/lib/controller/home/payment/paymob_payout.dart
Executable file
235
siro_driver/lib/controller/home/payment/paymob_payout.dart
Executable file
@@ -0,0 +1,235 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/controller/payment/smsPaymnet/payment_services.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../views/widgets/mydialoug.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import 'captain_wallet_controller.dart';
|
||||
|
||||
class PaymobPayout extends GetxController {
|
||||
bool isLoading = false;
|
||||
String dropdownValue = 'etisalat';
|
||||
|
||||
int payOutFee = 5;
|
||||
payToDriverWallet(String amount, String issuer, String msisdn) async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
// Authenticate the user
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment',
|
||||
// options: AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
// var dec = await CRUD()
|
||||
// .postWallet(link: AppLink.paymobPayoutDriverWallet, payload: {
|
||||
// "issuer": issuer,
|
||||
// "method": "wallet",
|
||||
// "amount": amount, //9.0,
|
||||
// "full_name":
|
||||
// '${box.read(BoxName.nameDriver)} ${box.read(BoxName.lastNameDriver)}',
|
||||
// "msisdn": msisdn, //"01010101010",
|
||||
// "bank_transaction_type": "cash_transfer"
|
||||
// });
|
||||
// if (dec['disbursement_status'] == 'successful') {
|
||||
// var paymentToken = await Get.find<CaptainWalletController>()
|
||||
// .generateToken(
|
||||
// ((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
|
||||
// .toStringAsFixed(0));
|
||||
// await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
// 'rideId': DateTime.now().toIso8601String(),
|
||||
// 'amount':
|
||||
// ((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
|
||||
// .toStringAsFixed(0),
|
||||
// 'payment_method': 'payout',
|
||||
// 'passengerID': 'myself',
|
||||
// 'token': paymentToken,
|
||||
// 'driverID': box.read(BoxName.driverID).toString(),
|
||||
// });
|
||||
// await Get.find<CaptainWalletController>()
|
||||
// .addSeferWallet('payout fee myself', payOutFee.toString());
|
||||
// await updatePaymentToPaid(box.read(BoxName.driverID).toString());
|
||||
// await sendEmail(
|
||||
// box.read(BoxName.driverID).toString(),
|
||||
// amount,
|
||||
// box.read(BoxName.phoneDriver).toString(),
|
||||
// box.read(BoxName.nameDriver).toString(),
|
||||
// 'Wallet',
|
||||
// box.read(BoxName.emailDriver).toString());
|
||||
|
||||
// mySnackbarSuccess('${'Transaction successful'.tr} ${dec['amount']}');
|
||||
|
||||
// Get.find<CaptainWalletController>().refreshCaptainWallet();
|
||||
// } else if (dec['disbursement_status'] == 'failed') {
|
||||
// mySnackeBarError('Transaction failed'.tr);
|
||||
// }
|
||||
} else {
|
||||
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog('Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future updatePaymentToPaid(String driverID) async {
|
||||
await CRUD().postWallet(link: AppLink.updatePaymetToPaid, payload: {
|
||||
'driverID': driverID.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
Future sendEmail(
|
||||
String driverId, amount, phone, name, bankCardNumber, email) async {
|
||||
await CRUD().sendEmail(AppLink.sendEmailToDrivertransaction, {
|
||||
"driverID": driverId,
|
||||
"total_amount": amount,
|
||||
"phone": phone,
|
||||
"name_arabic": name,
|
||||
"accountBank": bankCardNumber,
|
||||
"email": email
|
||||
});
|
||||
}
|
||||
|
||||
getAIKey(String key) async {
|
||||
var res =
|
||||
await CRUD().get(link: AppLink.getapiKey, payload: {"keyName": key});
|
||||
if (res != 'failure') {
|
||||
var d = jsonDecode(res)['message'];
|
||||
return d[key].toString();
|
||||
} else {}
|
||||
}
|
||||
|
||||
payToDriverBankAccount(
|
||||
String amount, String bankCardNumber, String bankCode) async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
// Authenticate the user
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment',
|
||||
// options: AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
var body = {
|
||||
"issuer": "bank_card",
|
||||
"method": "bank_card",
|
||||
"amount": amount, //9.0,
|
||||
"full_name":
|
||||
'${box.read(BoxName.nameDriver)} ${box.read(BoxName.lastNameDriver)}',
|
||||
"bank_card_number": bankCardNumber, //"1111-2222-3333-4444",
|
||||
"bank_code": bankCode, //"CIB",
|
||||
"bank_transaction_type": "cash_transfer"
|
||||
};
|
||||
var dec = await CRUD().postWallet(
|
||||
link:
|
||||
'wl.tripz-egypt.com/v1/main/ride/payMob/paymob_driver/paymob_payout.php',
|
||||
payload: body,
|
||||
);
|
||||
if (dec['disbursement_status'] == 'successful') {
|
||||
var paymentToken = await Get.find<CaptainWalletController>()
|
||||
.generateToken(
|
||||
((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
|
||||
.toStringAsFixed(0));
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'rideId': DateTime.now().toIso8601String(),
|
||||
'amount':
|
||||
((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
|
||||
.toStringAsFixed(0),
|
||||
'payment_method': 'payout',
|
||||
'passengerID': 'myself',
|
||||
'token': paymentToken,
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
await Get.find<CaptainWalletController>()
|
||||
.addSeferWallet('payout fee myself', payOutFee.toString());
|
||||
await updatePaymentToPaid(box.read(BoxName.driverID).toString());
|
||||
await sendEmail(
|
||||
box.read(BoxName.driverID).toString(),
|
||||
amount,
|
||||
box.read(BoxName.phoneDriver).toString(),
|
||||
box.read(BoxName.nameDriver).toString(),
|
||||
'Wallet',
|
||||
box.read(BoxName.emailDriver).toString());
|
||||
|
||||
mySnackbarSuccess('${'Transaction successful'.tr} ${dec['amount']}');
|
||||
Get.find<CaptainWalletController>().refreshCaptainWallet();
|
||||
} else if (dec['disbursement_status'] == 'failed') {
|
||||
mySnackeBarError('Transaction failed'.tr);
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog('Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future payToWalletDriverAll(
|
||||
String amount, String issuer, String msisdn) async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
// Authenticate the user
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment',
|
||||
// options: AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
await payToDriverWallet(amount, issuer, msisdn);
|
||||
} else {
|
||||
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog('Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future payToBankDriverAll(
|
||||
String amount, String bankCardNumber, String bankCode) async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
// Authenticate the user
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment',
|
||||
// options: AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
await payToDriverBankAccount(amount, bankCardNumber, bankCode);
|
||||
} else {
|
||||
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog('Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
147
siro_driver/lib/controller/home/points_for_rider_controller.dart
Executable file
147
siro_driver/lib/controller/home/points_for_rider_controller.dart
Executable file
@@ -0,0 +1,147 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
|
||||
import '../../constant/api_key.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../functions/crud.dart';
|
||||
import '../functions/location_controller.dart';
|
||||
|
||||
class PointsForRiderController extends GetxController {
|
||||
List<String> locations = [];
|
||||
String hintTextDestinationPoint = 'Search for your destination'.tr;
|
||||
TextEditingController placeStartController = TextEditingController();
|
||||
|
||||
void addLocation(String location) {
|
||||
locations.add(location);
|
||||
update();
|
||||
}
|
||||
|
||||
void getTextFromList(String location) {
|
||||
locations.add(location);
|
||||
update();
|
||||
Get.back();
|
||||
}
|
||||
|
||||
void removeLocation(int index) {
|
||||
locations.removeAt(index);
|
||||
update();
|
||||
}
|
||||
|
||||
void onReorder(int oldIndex, int newIndex) {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
update();
|
||||
}
|
||||
|
||||
final item = locations.removeAt(oldIndex);
|
||||
locations.insert(newIndex, item);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
class LocationModel {
|
||||
String name;
|
||||
double lat, lon;
|
||||
|
||||
LocationModel({required this.name, required this.lat, required this.lon});
|
||||
}
|
||||
|
||||
class WayPointController extends GetxController {
|
||||
// A list of text editing controllers for each text field
|
||||
// final textFields = [TextEditingController()].obs;
|
||||
List<String> wayPoints = [];
|
||||
List<List<dynamic>> placeListResponse = [];
|
||||
double wayPointHeight = 400;
|
||||
String hintTextDestinationPoint = 'Search for your destination'.tr;
|
||||
TextEditingController textSearchCotroller = TextEditingController();
|
||||
// A list of places corresponding to each text field
|
||||
final places = <String>[];
|
||||
|
||||
final hintTextPointList = <String>[];
|
||||
late LatLng myLocation;
|
||||
|
||||
void addWayPoints() {
|
||||
String wayPoint = 'Add a Stop'.tr;
|
||||
|
||||
if (wayPoints.length < 5) {
|
||||
wayPoints.add(wayPoint);
|
||||
update();
|
||||
} else {
|
||||
Get.defaultDialog(
|
||||
title: 'This is most WayPoints',
|
||||
titleStyle: AppStyle.title,
|
||||
middleText: '');
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void removeTextField(int index) {
|
||||
wayPoints.removeAt(index);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
// A method to reorder the text fields and the places
|
||||
void reorderTextFields(int oldIndex, int newIndex) {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final wayPoint = wayPoints.removeAt(oldIndex);
|
||||
wayPoints.insert(newIndex, wayPoint);
|
||||
update();
|
||||
}
|
||||
|
||||
void updatePlace(int index, String input) async {
|
||||
var url =
|
||||
'${AppLink.googleMapsLink}place/nearbysearch/json?keyword=$input&location=${myLocation.latitude},${myLocation.longitude}&radius=50000&language=en&key=${AK.mapAPIKEY.toString()}';
|
||||
var response = await CRUD().getGoogleApi(link: url, payload: {});
|
||||
// final place = input;
|
||||
// if (index == 0) {
|
||||
List<dynamic> newList = [];
|
||||
placeListResponse.add(newList);
|
||||
newList = response['results'];
|
||||
placeListResponse[index].add(newList);
|
||||
update();
|
||||
// }
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
Get.put(LocationController());
|
||||
addWayPoints();
|
||||
myLocation = Get.find<LocationController>().myLocation;
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceList extends StatelessWidget {
|
||||
// Get the controller instance
|
||||
final controller = Get.put(WayPointController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Use the Obx widget to rebuild the widget when the controller changes
|
||||
return Obx(() {
|
||||
// Use the ListView widget to display the list of places
|
||||
return ListView(
|
||||
// The children of the list are the places
|
||||
children: [
|
||||
// Loop through the places in the controller
|
||||
for (final place in controller.places)
|
||||
// Create a text widget for each place
|
||||
Text(
|
||||
// Use the place as the text
|
||||
place,
|
||||
|
||||
// Add some style and padding
|
||||
style: const TextStyle(fontSize: 18.0),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
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:http_parser/http_parser.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:siro_driver/controller/functions/encrypt_decrypt.dart';
|
||||
import 'package:siro_driver/env/env.dart';
|
||||
import 'package:siro_driver/print.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
class ComplaintController extends GetxController {
|
||||
bool isLoading = false;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final complaintController = TextEditingController();
|
||||
|
||||
List<dynamic> ridesList = [];
|
||||
Map<String, dynamic>? selectedRide;
|
||||
|
||||
Map<String, dynamic>? passengerReport;
|
||||
Map<String, dynamic>? driverReport;
|
||||
|
||||
var isUploading = false.obs;
|
||||
var uploadSuccess = false.obs;
|
||||
String audioLink = ''; // سيتم تخزين رابط الصوت هنا بعد الرفع
|
||||
String attachedFileName = '';
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
getLatestRidesForDriver();
|
||||
}
|
||||
|
||||
void _showCustomSnackbar(String title, String message,
|
||||
{bool isError = false}) {
|
||||
if (title.toLowerCase() == 'success') {
|
||||
mySnackbarSuccess(message.tr);
|
||||
} else if (isError) {
|
||||
mySnackeBarError(message.tr);
|
||||
} else {
|
||||
mySnackbarWarning(message.tr);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getLatestRidesForDriver() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
try {
|
||||
var res = await CRUD().get(link: AppLink.getRides, payload: {
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
if (res != 'failure' && res != 'no_internet') {
|
||||
var decoded = jsonDecode(res);
|
||||
if (decoded['status'] == 'success') {
|
||||
ridesList = decoded['data'] ?? [];
|
||||
if (ridesList.isNotEmpty) {
|
||||
selectedRide = ridesList[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Error getting driver rides: $e");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void selectRide(Map<String, dynamic> ride) {
|
||||
selectedRide = ride;
|
||||
audioLink = '';
|
||||
attachedFileName = '';
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> uploadAudioFile(File audioFile) async {
|
||||
try {
|
||||
isUploading.value = true;
|
||||
update();
|
||||
|
||||
var uri = Uri.parse(AppLink.uploadAudio);
|
||||
var request = http.MultipartRequest('POST', uri);
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||
|
||||
var mimeType = lookupMimeType(audioFile.path);
|
||||
request.headers.addAll({
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': fingerPrint,
|
||||
});
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath(
|
||||
'audio',
|
||||
audioFile.path,
|
||||
contentType: mimeType != null ? MediaType.parse(mimeType) : null,
|
||||
),
|
||||
);
|
||||
|
||||
var response = await request.send();
|
||||
var responseBody = await http.Response.fromStream(response);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var jsonResponse = jsonDecode(responseBody.body);
|
||||
if (jsonResponse['status'] == 'Audio file uploaded successfully.') {
|
||||
uploadSuccess.value = true;
|
||||
audioLink = jsonResponse['link'];
|
||||
attachedFileName = audioFile.path.split('/').last;
|
||||
_showCustomSnackbar('Success', 'Audio uploaded successfully.');
|
||||
} else {
|
||||
uploadSuccess.value = false;
|
||||
_showCustomSnackbar('Error', 'Failed to upload audio file.',
|
||||
isError: true);
|
||||
}
|
||||
} else {
|
||||
uploadSuccess.value = false;
|
||||
_showCustomSnackbar('Error', 'Server error: ${response.statusCode}',
|
||||
isError: true);
|
||||
}
|
||||
} catch (e) {
|
||||
uploadSuccess.value = false;
|
||||
_showCustomSnackbar(
|
||||
'Error', 'An application error occurred during upload.',
|
||||
isError: true);
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> submitComplaintToServer() async {
|
||||
if (!formKey.currentState!.validate() || complaintController.text.isEmpty) {
|
||||
_showCustomSnackbar(
|
||||
'Error', 'Please describe your issue before submitting.',
|
||||
isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedRide == null) {
|
||||
_showCustomSnackbar('Error', 'Please select a ride before submitting.',
|
||||
isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
final rideId = selectedRide!['id'].toString();
|
||||
final complaint = complaintController.text;
|
||||
|
||||
final responseData = await CRUD().post(
|
||||
link: AppLink.add_solve_all,
|
||||
payload: {
|
||||
'ride_id': rideId,
|
||||
'complaint_text': complaint,
|
||||
'audio_link': audioLink,
|
||||
},
|
||||
);
|
||||
|
||||
if (responseData == 'failure' || responseData == 'no_internet' || responseData == 'token_expired') {
|
||||
_showCustomSnackbar(
|
||||
'Error', 'Failed to connect to the server. Please try again.',
|
||||
isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseData['status'] == 'success') {
|
||||
passengerReport = responseData['data']['passenger_response'];
|
||||
driverReport = responseData['data']['driver_response'];
|
||||
update();
|
||||
|
||||
MyDialogContent().getDialog(
|
||||
'Success'.tr, Text('Your complaint has been submitted.'.tr), () {
|
||||
Get.back();
|
||||
complaintController.clear();
|
||||
audioLink = '';
|
||||
attachedFileName = '';
|
||||
formKey.currentState?.reset();
|
||||
});
|
||||
} else {
|
||||
String errorMessage =
|
||||
responseData['message'] ?? 'An unknown server error occurred'.tr;
|
||||
_showCustomSnackbar('Submission Failed', errorMessage, isError: true);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Submit Complaint Error: $e");
|
||||
_showCustomSnackbar('Error', 'An application error occurred.'.tr,
|
||||
isError: true);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
44
siro_driver/lib/controller/home/profile/feed_back_controller.dart
Executable file
44
siro_driver/lib/controller/home/profile/feed_back_controller.dart
Executable file
@@ -0,0 +1,44 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
class FeedBackController extends GetxController {
|
||||
bool isLoading = false;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final feedbackController = TextEditingController();
|
||||
|
||||
void addFeedBack() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
var res = await CRUD().post(link: AppLink.addFeedBack, payload: {
|
||||
'passengerId': box.read(BoxName.passengerID).toString(),
|
||||
'feedBack': feedbackController.text
|
||||
});
|
||||
var d = jsonDecode(res);
|
||||
if (d['status'].toString() == 'success') {
|
||||
Get.defaultDialog(
|
||||
title: 'Success'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
middleText: 'Feedback data saved successfully'.tr,
|
||||
middleTextStyle: AppStyle.title,
|
||||
confirm: MyElevatedButton(
|
||||
kolor: AppColor.greenColor,
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
35
siro_driver/lib/controller/home/profile/order_history_controller.dart
Executable file
35
siro_driver/lib/controller/home/profile/order_history_controller.dart
Executable file
@@ -0,0 +1,35 @@
|
||||
import 'dart:convert';
|
||||
|
||||
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';
|
||||
|
||||
class OrderHistoryController extends GetxController {
|
||||
List<dynamic> orderHistoryListPassenger = [];
|
||||
bool isloading = true;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
getOrderHistoryByPassenger();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
Future getOrderHistoryByPassenger() async {
|
||||
var res = await CRUD().get(link: AppLink.getRides, payload: {
|
||||
'passenger_id': box.read(BoxName.passengerID).toString(),
|
||||
});
|
||||
if (res.toString() == 'failure') {
|
||||
// Get.snackbar('failure', 'message');
|
||||
isloading = false;
|
||||
update();
|
||||
} else {
|
||||
var jsonDecoded = jsonDecode(res);
|
||||
|
||||
orderHistoryListPassenger = jsonDecoded['data'];
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
42
siro_driver/lib/controller/home/profile/promos_controller.dart
Executable file
42
siro_driver/lib/controller/home/profile/promos_controller.dart
Executable file
@@ -0,0 +1,42 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
class PromosController extends GetxController {
|
||||
List<dynamic> promoList = [];
|
||||
bool isLoading = true;
|
||||
late String promos;
|
||||
@override
|
||||
void onInit() {
|
||||
getPromoByToday();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
Future getPromoByToday() async {
|
||||
var res = await CRUD().get(link: AppLink.getPromoBytody, payload: {});
|
||||
if (res.toString() == 'failure') {
|
||||
Get.defaultDialog(
|
||||
title: 'No Promo for today .'.tr,
|
||||
middleText: '',
|
||||
titleStyle: AppStyle.title,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Back'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
}));
|
||||
isLoading = false;
|
||||
update();
|
||||
} else {
|
||||
var jsonDecoded = jsonDecode(res);
|
||||
|
||||
promoList = jsonDecoded['message'];
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
164
siro_driver/lib/controller/home/splash_screen_controlle.dart
Executable file
164
siro_driver/lib/controller/home/splash_screen_controlle.dart
Executable file
@@ -0,0 +1,164 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:siro_driver/views/auth/captin/login_captin.dart';
|
||||
import 'package:siro_driver/views/home/on_boarding_page.dart';
|
||||
import '../functions/app_update_controller.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../main.dart';
|
||||
import '../../onbording_page.dart';
|
||||
import '../../print.dart';
|
||||
import '../functions/encrypt_decrypt.dart';
|
||||
import '../functions/package_info.dart';
|
||||
import '../functions/secure_storage.dart';
|
||||
import '../functions/security_checks.dart';
|
||||
|
||||
// Assuming you have a home page to navigate to after successful login.
|
||||
// If not, you might need to adjust the navigation target.
|
||||
// import 'package:siro_driver/views/home/home_page.dart';
|
||||
|
||||
class SplashScreenController extends GetxController
|
||||
with GetTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> animation;
|
||||
final progress = 0.0.obs;
|
||||
Timer? _progressTimer;
|
||||
|
||||
String packageInfo = '';
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
Get.put(AppUpdateController()); // تهيئة متحكم التحديثات الذكي
|
||||
_setupAnimations();
|
||||
_initializeAndNavigate();
|
||||
checkSecurity();
|
||||
}
|
||||
|
||||
checkSecurity() async {
|
||||
final random = Random();
|
||||
|
||||
if (random.nextBool()) {
|
||||
await SecurityHelper.performSecurityChecks();
|
||||
} else {
|
||||
await SecurityChecks.isDeviceRootedFromNative(Get.context!);
|
||||
}
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
);
|
||||
animation =
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut);
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
/// This is the core function that initializes the app.
|
||||
/// It runs two tasks simultaneously and navigates only when necessary.
|
||||
Future<void> _initializeAndNavigate() async {
|
||||
// Start getting package info, no need to wait for it.
|
||||
_getPackageInfo();
|
||||
|
||||
const minSplashDurationMs = 4000;
|
||||
_animateProgressBar(minSplashDurationMs);
|
||||
|
||||
// Define the two concurrent tasks
|
||||
final minDuration =
|
||||
Future.delayed(const Duration(milliseconds: minSplashDurationMs));
|
||||
final navigationTargetFuture = _getNavigationTarget();
|
||||
|
||||
// Wait for both tasks to complete
|
||||
await Future.wait([minDuration, navigationTargetFuture]);
|
||||
|
||||
// The future now returns a nullable Widget (Widget?)
|
||||
final Widget? targetPage = await navigationTargetFuture;
|
||||
|
||||
// *** FIX: Only navigate if the targetPage is not null. ***
|
||||
// This prevents navigating again if the login function already handled it.
|
||||
if (targetPage != null) {
|
||||
Get.off(() => targetPage,
|
||||
transition: Transition.fadeIn,
|
||||
duration: const Duration(milliseconds: 500));
|
||||
} else {
|
||||
Log.print(
|
||||
"Navigation was handled internally by the login process. Splash screen will not navigate.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Animates the progress bar over a given duration.
|
||||
void _animateProgressBar(int totalMilliseconds) {
|
||||
const interval = 50;
|
||||
int elapsed = 0;
|
||||
_progressTimer?.cancel();
|
||||
_progressTimer =
|
||||
Timer.periodic(const Duration(milliseconds: interval), (timer) {
|
||||
elapsed += interval;
|
||||
progress.value = (elapsed / totalMilliseconds).clamp(0.0, 1.0);
|
||||
if (elapsed >= totalMilliseconds) {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Determines the correct page to navigate to, or returns null if navigation
|
||||
/// is expected to be handled by an internal process (like login).
|
||||
Future<Widget?> _getNavigationTarget() async {
|
||||
try {
|
||||
// 1) Onboarding
|
||||
final doneOnboarding = box.read(BoxName.onBoarding) == 'yes';
|
||||
if (!doneOnboarding) {
|
||||
// الأفضل: رجّع الواجهة بدل Get.off داخل الدالة
|
||||
return OnBoardingPage();
|
||||
}
|
||||
|
||||
// 2) Login
|
||||
final isDriverDataAvailable = box.read(BoxName.phoneDriver) != null;
|
||||
if (!isDriverDataAvailable) {
|
||||
return LoginCaptin();
|
||||
}
|
||||
|
||||
final loginController = Get.put(LoginDriverController());
|
||||
|
||||
final AppInitializer initializer = AppInitializer();
|
||||
await initializer.initializeApp();
|
||||
await EncryptionHelper.initialize();
|
||||
|
||||
await loginController.loginWithGoogleCredential(
|
||||
box.read(BoxName.driverID).toString(),
|
||||
box.read(BoxName.emailDriver).toString(),
|
||||
);
|
||||
|
||||
return null; // لأن loginWithGoogleCredential يوجّه
|
||||
} catch (e) {
|
||||
Log.print("Error during navigation logic: $e");
|
||||
return LoginCaptin();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getPackageInfo() async {
|
||||
try {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
packageInfo = info.version;
|
||||
await box.write(BoxName.packagInfo, packageInfo);
|
||||
update(); // To update any UI element that might be listening
|
||||
} catch (e) {
|
||||
Log.print("Could not get package info: $e");
|
||||
packageInfo = '1.0.0'; // Default value
|
||||
await box.write(BoxName.packagInfo, packageInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_progressTimer?.cancel();
|
||||
_animationController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.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/models/model/driver/rides_summary_model.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
class StatisticsController extends GetxController {
|
||||
bool isLoading = false;
|
||||
|
||||
// ═══ Weekly Data ═══
|
||||
List<DayStat> weeklyStats = [];
|
||||
double weeklyEarnings = 0;
|
||||
int weeklyTrips = 0;
|
||||
double weeklyHours = 0;
|
||||
|
||||
// ═══ Monthly Data ═══
|
||||
List<MonthlyPriceDriverModel> monthlyEarnings = [];
|
||||
List<MonthlyRideModel> monthlyRides = [];
|
||||
List<MonthlyDataModel> monthlyDuration = [];
|
||||
double monthlyTotalEarnings = 0;
|
||||
int monthlyTotalTrips = 0;
|
||||
double monthlyTotalHours = 0;
|
||||
String bestDay = '--';
|
||||
double bestDayEarnings = 0;
|
||||
|
||||
// ═══ Tab State ═══
|
||||
int selectedTab = 0; // 0=weekly, 1=monthly
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
reloadData();
|
||||
}
|
||||
|
||||
void changeTab(int tab) {
|
||||
selectedTab = tab;
|
||||
update();
|
||||
}
|
||||
|
||||
// ═══════ جلب البيانات الأسبوعية ═══════
|
||||
Future<void> fetchWeeklyData() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getWeeklyAggregate,
|
||||
payload: {
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (res != null && res != 'failure') {
|
||||
var data = jsonDecode(res);
|
||||
if (data['message'] is List) {
|
||||
weeklyStats = (data['message'] as List)
|
||||
.map((e) => DayStat.fromJson(e))
|
||||
.toList();
|
||||
weeklyEarnings = weeklyStats.fold(0, (s, d) => s + d.earnings);
|
||||
weeklyTrips = weeklyStats.fold(0, (s, d) => s + d.trips);
|
||||
weeklyHours = weeklyStats.fold(0, (s, d) => s + d.hours);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Stats] Weekly fetch error: $e');
|
||||
// Fallback: generate from local data
|
||||
_generateLocalWeeklyData();
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
// ═══════ جلب البيانات الشهرية ═══════
|
||||
Future<void> fetchMonthlyData() async {
|
||||
try {
|
||||
// 1. أرباح شهرية
|
||||
var earningsRes = await CRUD().getWallet(
|
||||
link: AppLink.getAllPaymentFromRide,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (earningsRes != null && earningsRes != 'failure') {
|
||||
var data = jsonDecode(earningsRes);
|
||||
if (data['message'] is List) {
|
||||
monthlyEarnings = (data['message'] as List)
|
||||
.map((e) => MonthlyPriceDriverModel.fromJson(e))
|
||||
.toList();
|
||||
monthlyTotalEarnings =
|
||||
monthlyEarnings.fold(0, (s, d) => s + d.pricePerDay);
|
||||
|
||||
// أفضل يوم
|
||||
if (monthlyEarnings.isNotEmpty) {
|
||||
var best = monthlyEarnings
|
||||
.reduce((a, b) => a.pricePerDay > b.pricePerDay ? a : b);
|
||||
bestDay = best.day.toString();
|
||||
bestDayEarnings = best.pricePerDay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. رحلات شهرية
|
||||
var ridesRes = await CRUD().get(
|
||||
link: AppLink.getRidesDriverByDay,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (ridesRes != null && ridesRes != 'failure') {
|
||||
var data = jsonDecode(ridesRes);
|
||||
if (data['message'] is List) {
|
||||
monthlyRides = (data['message'] as List)
|
||||
.map((e) => MonthlyRideModel.fromJson(e))
|
||||
.toList();
|
||||
monthlyTotalTrips = monthlyRides.fold(0, (s, d) => s + d.countRide);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. ساعات شهرية
|
||||
var durationRes = await CRUD().get(
|
||||
link: AppLink.getTotalDriverDuration,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (durationRes != null && durationRes != 'failure') {
|
||||
var data = jsonDecode(durationRes);
|
||||
if (data['message'] is List) {
|
||||
monthlyDuration = (data['message'] as List)
|
||||
.map((e) => MonthlyDataModel.fromJson(e))
|
||||
.toList();
|
||||
monthlyTotalHours =
|
||||
monthlyDuration.fold(0, (s, d) => s + d.totalDuration.toDouble());
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Stats] Monthly fetch error: $e');
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void _generateLocalWeeklyData() {
|
||||
// Fallback بيانات محلية عند عدم توفر الـ API
|
||||
final now = DateTime.now();
|
||||
weeklyStats = List.generate(7, (i) {
|
||||
final day = now.subtract(Duration(days: 6 - i));
|
||||
return DayStat(
|
||||
date: day,
|
||||
dayName: _getDayName(day.weekday),
|
||||
earnings: 0,
|
||||
trips: 0,
|
||||
hours: 0,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String _getDayName(int weekday) {
|
||||
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return days[weekday - 1];
|
||||
}
|
||||
|
||||
bool _isFetching = false;
|
||||
|
||||
Future<void> reloadData() async {
|
||||
if (_isFetching) return;
|
||||
_isFetching = true;
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
debugPrint('📊 [Statistics] Reloading data...');
|
||||
await fetchWeeklyData();
|
||||
await fetchMonthlyData();
|
||||
debugPrint('📊 [Statistics] Data reload complete.');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Statistics] Error reloading data: $e');
|
||||
} finally {
|
||||
_isFetching = false;
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════ نموذج إحصائية اليوم ═══════
|
||||
class DayStat {
|
||||
final DateTime date;
|
||||
final String dayName;
|
||||
final double earnings;
|
||||
final int trips;
|
||||
final double hours;
|
||||
|
||||
DayStat({
|
||||
required this.date,
|
||||
required this.dayName,
|
||||
required this.earnings,
|
||||
required this.trips,
|
||||
required this.hours,
|
||||
});
|
||||
|
||||
factory DayStat.fromJson(Map<String, dynamic> json) {
|
||||
final date =
|
||||
DateTime.tryParse(json['day']?.toString() ?? '') ?? DateTime.now();
|
||||
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return DayStat(
|
||||
date: date,
|
||||
dayName: dayNames[date.weekday - 1],
|
||||
earnings: double.tryParse(json['earnings']?.toString() ?? '0') ?? 0,
|
||||
trips: int.tryParse(json['trips']?.toString() ?? '0') ?? 0,
|
||||
hours: double.tryParse(json['hours']?.toString() ?? '0') ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user