Files
intaleq/intaleq_driver/lib/controller/home/captin/home_captain_controller.dart
T

1084 lines
37 KiB
Dart
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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:intaleq_driver/constant/box_name.dart';
import 'dart:async';
import '../../../constant/links.dart';
import '../../../constant/style.dart';
import '../../../constant/table_names.dart';
import '../../../constant/country_polygons.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 '../../functions/package_info.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.fromStyleImage('car_icon', size: 2.3);
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 = {};
// === متغيرات التنبؤ الذكي بالطلب ===
bool isPredictiveVisible = false;
Set<Polygon> predictivePolygons = {};
List<Map<String, dynamic>> predictiveZones = [];
Timer? _predictiveTimer;
// 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...");
// استخدم الرابط المباشر للملف الديناميكي الجديد
final String jsonUrl = "${AppLink.ride}/heatmap/heatmap_live.php";
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 togglePredictiveDemand() {
isPredictiveVisible = !isPredictiveVisible;
if (isPredictiveVisible) {
_startPredictiveCycle();
} else {
_predictiveTimer?.cancel();
predictivePolygons.clear();
predictiveZones.clear();
}
update();
}
void _startPredictiveCycle() {
_predictiveTimer?.cancel();
fetchAndDrawPredictiveDemand();
// تحديث كل 30 دقيقة (يتوافق مع دورة الـ cron)
_predictiveTimer = Timer.periodic(const Duration(minutes: 30), (_) {
if (isPredictiveVisible) fetchAndDrawPredictiveDemand();
});
}
Future<void> fetchAndDrawPredictiveDemand() async {
print("🔮 [Predictive] Fetching demand zones...");
try {
final myLat = locationController.myLocation.latitude;
final myLng = locationController.myLocation.longitude;
final response = await http.get(
Uri.parse(
"${AppLink.getPredictiveDemand}?lat=$myLat&lng=$myLng&radius=10"
"&t=${DateTime.now().millisecondsSinceEpoch}",
),
);
if (response.statusCode == 200) {
final body = json.decode(response.body);
if (body['success'] == true) {
final zones = List<Map<String, dynamic>>.from(body['zones'] ?? []);
predictiveZones = zones;
_drawPredictivePolygons(zones);
print("✅ [Predictive] ${zones.length} zones loaded.");
}
}
} catch (e) {
print("❌ [Predictive] Error: $e");
}
}
void _drawPredictivePolygons(List<Map<String, dynamic>> zones) {
const double offset = 0.008; // أكبر قليلاً من الهيتماب (0.005) ليكون مميزاً
final Set<Polygon> temp = {};
for (final zone in zones) {
final double lat = (zone['lat'] as num).toDouble();
final double lng = (zone['lng'] as num).toDouble();
final int score = (zone['demand_score'] as num).toInt();
// ألوان التنبؤ: أزرق/سماوي (مختلف تماماً عن الهيتماب)
final Color fill = score >= 8
? const Color(0xFF0EA5E9).withOpacity(0.35) // أزرق قوي
: score >= 4
? const Color(0xFF38BDF8).withOpacity(0.30) // أزرق فاتح
: const Color(0xFF7DD3FC).withOpacity(0.25); // سماوي خفيف
final Color stroke = score >= 8
? const Color(0xFF0369A1).withOpacity(0.9)
: const Color(0xFF0EA5E9).withOpacity(0.8);
temp.add(Polygon(
polygonId: PolygonId("pred_${lat}_$lng"),
points: [
LatLng(lat - offset, lng - offset),
LatLng(lat + offset, lng - offset),
LatLng(lat + offset, lng + offset),
LatLng(lat - offset, lng + offset),
],
fillColor: fill,
strokeColor: stroke,
strokeWidth: 2,
));
}
predictivePolygons = temp;
update();
}
void goToWalletFromConnect() {
Get.back();
Get.back();
Get.to(() => WalletCaptainRefactored());
}
void changeRideId() {
rideId = 'rideId';
update();
}
void addCustomCarIcon() {
carIcon = InlqBitmap.fromAsset('assets/images/car.png', size: 2.3);
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)),
),
);
}
double get minPointsThreshold {
String country = box.read(BoxName.countryCode) ?? 'Syria';
if (country == 'Jordan') {
return -3.0;
} else if (country == 'Egypt') {
return -200.0;
} else {
return -200.0; // Default (Syria / other)
}
}
/// زرّ «متاح / غير متاح» في الواجهة — يقلب الحالة الحالية.
void onButtonSelected() => setActive(!isActive);
/// تفعيل/إيقاف الكابتن بشكل صريح.
/// مُعامِلة صريحة بدل القلب (toggle) لأن الاستدعاء التلقائي في onInit كان
/// يعتمد على أن isActive تساوي false — فأي إعادة بناء للكونترولر أو
/// استدعاء ثانٍ كانت تُطفئ الكابتن بدل تشغيله. الاستدعاء بنفس القيمة
/// الحالية آمن: يعيد التأكد من أن التتبّع شغّال فقط.
void setActive(bool active) {
if (!Get.isRegistered<CaptainWalletController>()) {
Get.put(CaptainWalletController());
}
totalPoints = Get.find<CaptainWalletController>().totalPoints;
if (!active) {
_goOffline();
return;
}
try {
_checkFatigueBeforeOnline(); // يرمي استثناءً إذا تجاوز 12 ساعة
} catch (_) {
isActive = false;
update();
return;
}
// الرصيد غير كافٍ: نُطفئ فعلياً بدل إبقاء isActive = true بلا تتبّع،
// فقد كانت الواجهة تعرض «متاح» بينما لا يُبَث أي موقع.
final double? points = double.tryParse(totalPoints);
if (points == null || points <= minPointsThreshold) {
_goOffline();
return;
}
final bool wasActive = isActive;
isActive = true;
// آمنة للاستدعاء المتكرر: تتجاهل الاشتراك إن كان قائماً بالفعل.
locationController.startLocationUpdates();
if (!wasActive) {
HapticFeedback.heavyImpact();
activeStartTime = DateTime.now();
activeTimer?.cancel();
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();
});
}
update();
}
void _goOffline() {
isActive = false;
locationController.stopLocationUpdates();
activeStartTime = null;
activeTimer?.cancel();
if (activeDuration.inSeconds > 0) savePeriod(activeDuration);
activeDuration = Duration.zero;
// Save offline time for Fatigue Monitoring reset
box.write('fatigue_last_offline', DateTime.now().toIso8601String());
update();
}
/// يضبط حالة الكابتن المخزَّنة عند إقلاع التطبيق.
/// تذكير بمصطلحات التطبيق: 'off' = متاح، 'on' = مشغول برحلة،
/// 'blocked' = محظور، وهي القيمة التي يقرأها emitLocationToSocket ويبني
/// عليها السيرفر عضوية geo:drivers:available.
///
/// كانت 'off' تُكتب دون شرط في آخر onInit، فتُلغي حظراً ساري المفعول
/// كتبه checkAndShowBlockDialog قبلها بسطر، وتُعيد كابتناً في رحلة نشطة
/// إلى حوض المتاحين بعد إعادة فتح التطبيق.
void _applyInitialDriverStatus() {
final String? blockStr = box.read(BoxName.blockUntilDate);
if (blockStr != null && blockStr.isNotEmpty) {
final DateTime? blockExpiry = DateTime.tryParse(blockStr);
if (blockExpiry != null && DateTime.now().isBefore(blockExpiry)) {
box.write(BoxName.statusDriverLocation, 'blocked');
return;
}
}
// رحلة نجت من إغلاق التطبيق: نُبقيه مشغولاً حتى لا يُوزَّع عليه طلب ثانٍ.
final String? rideStatus = box.read(BoxName.rideStatus);
if (rideStatus == 'Apply' || rideStatus == 'Arrived' ||
rideStatus == 'Begin') {
box.write(BoxName.statusDriverLocation, 'on');
return;
}
box.write(BoxName.statusDriverLocation, 'off'); // متاح
}
// متغيرات العداد للحظر
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) <= minPointsThreshold) {
// if (int.parse(countRefuse) > 3 || double.parse(totalPoints) <= minPointsThreshold) {
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;
CameraPosition? currentCameraPosition;
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");
}
});
}
Future<void> onStyleLoaded() async {
print("🎨 [HomeCaptain] onStyleLoaded");
try {
final ByteData bytes = await rootBundle.load('assets/images/car.png');
await mapHomeCaptainController?.addImage(
'car_icon', bytes.buffer.asUint8List());
carIcon = InlqBitmap.fromStyleImage('car_icon', size: 2.3);
update();
} catch (e) {
print("❌ [HomeCaptain] Map icon load error: $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) {
final rawPos = LatLng(locData.latitude!, locData.longitude!);
if (!CountryPolygons.isServiceSupported(rawPos)) {
myLocation = const LatLng(31.9539, 35.9106);
if (!Get.isSnackbarOpen) {
Get.snackbar(
'Alert'.tr,
'service_unavailable_area'.tr,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.redAccent,
colorText: Colors.white,
icon: const Icon(Icons.location_off, color: Colors.white),
margin: const EdgeInsets.all(15),
duration: const Duration(seconds: 5),
isDismissible: true,
);
}
} else {
myLocation = rawPos;
}
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();
// ⚠️ الترتيب مقصود: الحالة تُكتب قبل تشغيل التتبّع.
// ‏أول update_location يُرسَل بعد اتصال السوكيت مباشرة، وهو الذي يُدخل
// ‏الكابتن إلى geo:drivers:available على السيرفر. وكتابة الحالة بعد
// ‏تشغيل التتبّع (كما كانت في آخر السطور) تعني أن ذلك الـ emit قد يحمل
// ‏حالة قديمة ('on' من رحلة سابقة لم تُغلق مثلاً).
_applyInitialDriverStatus();
await getlocation();
setActive(true);
getDriverRate();
addCustomCarIcon();
getKazanPercent();
getPaymentToday();
getCountRideToday();
getAllPayment();
startPeriodicExecution();
getCaptainWalletFromBuyPoints();
// onMapCreated(mapHomeCaptainController!);
// totalPoints = Get.find<CaptainWalletController>().totalPoints.toString();
// getRefusedOrderByCaptain();
// 🔥 الفحص عند تشغيل التطبيق
// ‏(يكتب 'blocked' عند وجود حظر ساري — ولم تعد أي كتابة بعده تدهسه)
checkAndShowBlockDialog();
// 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 DeviceHelper.getDeviceFingerprint();
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') {
try {
var json = jsonDecode(res);
if (json['message'] is List && json['message'].isNotEmpty) {
final item = json['message'][0];
double parseDouble(dynamic val) {
if (val == null) return 0.0;
return double.tryParse(val.toString()) ?? 0.0;
}
kazan = parseDouble(item['kazanPercent'] ?? item['kazan']);
naturePrice =
parseDouble(item['normalMinPrice'] ?? item['naturePrice']);
heavyPrice = parseDouble(item['peakMinPrice'] ?? item['heavyPrice']);
latePrice = parseDouble(item['lateMinPrice'] ?? item['latePrice']);
comfortPrice = parseDouble(item['comfortPrice']);
speedPrice = parseDouble(item['speedPrice']);
deliveryPrice = parseDouble(item['deliveryPrice']);
mashwariPrice =
parseDouble(item['freePrice'] ?? item['mishwarVipPrice']);
familyPrice = parseDouble(item['familyPrice'] ?? item['ladyPrice']);
fuelPrice = parseDouble(item['fuelPrice']);
}
} catch (e) {
Log.print("Error parsing kazan percent: $e");
}
}
update();
}
double mpg = 0;
calculateConsumptionFuel() {
mpg = fuelPrice / 12; //todo in register car add mpg in box
}
// 🆕 Gamification Streak State
int streakCount = 0;
bool isZeroCommissionActive = false;
getCountRideToday() async {
var res = await CRUD().getWallet(
link: AppLink.getCountRide,
payload: {'driver_id': box.read(BoxName.driverID).toString()});
data = jsonDecode(res);
countRideToday = data['message'][0]['count'].toString();
update();
// Fetch Gamification Streak as well
getDriverStreak();
}
// 🆕 Fetch Driver Streak
getDriverStreak() async {
var res = await CRUD().get(
link: AppLink.getDriverStreak,
payload: {'driver_id': box.read(BoxName.driverID).toString()});
if (res != 'failure') {
var decode = jsonDecode(res);
if (decode['status'] == 'success') {
streakCount = decode['message']['streak_count'] ?? 0;
isZeroCommissionActive =
decode['message']['is_zero_commission_active'] ?? false;
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();
}
}