1084 lines
37 KiB
Dart
Executable File
1084 lines
37 KiB
Dart
Executable File
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();
|
||
}
|
||
}
|