Fixes & Updates - 2026-06-01: Integrate Back-End v3 updates, fix call/connection issues across apps
This commit is contained in:
@@ -4,6 +4,7 @@ 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:sefer_driver/constant/box_name.dart';
|
||||
import 'dart:async';
|
||||
@@ -29,7 +30,7 @@ class HomeCaptainController extends GetxController {
|
||||
Timer? activeTimer;
|
||||
Map data = {};
|
||||
bool isHomeMapActive = true;
|
||||
InlqBitmap carIcon = InlqBitmap.defaultMarker;
|
||||
InlqBitmap carIcon = InlqBitmap.fromAsset('assets/images/car.png');
|
||||
bool isMapReadyForCommands = false;
|
||||
bool isLoading = true;
|
||||
late double kazan = 0;
|
||||
@@ -186,7 +187,8 @@ class HomeCaptainController extends GetxController {
|
||||
_heatmapTimer?.cancel();
|
||||
fetchAndDrawHeatmap();
|
||||
|
||||
_heatmapTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
|
||||
// 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();
|
||||
@@ -213,7 +215,8 @@ class HomeCaptainController extends GetxController {
|
||||
}
|
||||
|
||||
String stringActiveDuration = '';
|
||||
|
||||
int _fatigueSeconds = 0; // عداد ثواني الإرهاق المؤقت
|
||||
|
||||
// ==========================================
|
||||
// ====== 🛡️ Fatigue Monitoring System ======
|
||||
// ==========================================
|
||||
@@ -230,7 +233,8 @@ class HomeCaptainController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
if (totalSecondsToday >= 12 * 3600) { // 12 Hours
|
||||
if (totalSecondsToday >= 12 * 3600) {
|
||||
// 12 Hours
|
||||
_forceOfflineDueToFatigue();
|
||||
throw Exception('Fatigue Limit Exceeded');
|
||||
}
|
||||
@@ -244,12 +248,15 @@ class HomeCaptainController extends GetxController {
|
||||
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,
|
||||
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),
|
||||
titleStyle:
|
||||
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||
confirm: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
onPressed: () => Get.back(),
|
||||
@@ -263,30 +270,34 @@ class HomeCaptainController extends GetxController {
|
||||
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
|
||||
int totalSeconds = box.read('fatigue_total_seconds') ?? 0;
|
||||
totalSeconds += 1;
|
||||
box.write('fatigue_total_seconds', totalSeconds);
|
||||
|
||||
if (totalSeconds >= 12 * 3600) { // 12 hours
|
||||
_forceOfflineDueToFatigue();
|
||||
// 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();
|
||||
@@ -311,7 +322,7 @@ class HomeCaptainController extends GetxController {
|
||||
activeTimer?.cancel();
|
||||
savePeriod(activeDuration);
|
||||
activeDuration = Duration.zero;
|
||||
|
||||
|
||||
// Save offline time for Fatigue Monitoring reset
|
||||
box.write('fatigue_last_offline', DateTime.now().toIso8601String());
|
||||
update();
|
||||
@@ -486,6 +497,7 @@ class HomeCaptainController extends GetxController {
|
||||
|
||||
// late IntaleqMapController mapHomeCaptainController;
|
||||
IntaleqMapController? mapHomeCaptainController;
|
||||
LatLng? _lastCameraLoc; // لتتبع آخر موقع حرك الكاميرا
|
||||
|
||||
// --- FIX 2: Smart Map Creation ---
|
||||
void onMapCreated(IntaleqMapController controller) {
|
||||
@@ -504,7 +516,7 @@ class HomeCaptainController extends GetxController {
|
||||
print(
|
||||
"🔥 [HomeCaptain] Safely moving camera to: ${currentLoc.latitude}");
|
||||
mapHomeCaptainController!.moveCamera(
|
||||
CameraUpdate.newLatLngZoom(currentLoc, 15),
|
||||
CameraUpdate.newLatLngZoom(currentLoc, 17.5),
|
||||
);
|
||||
} else {
|
||||
print("🔥 [HomeCaptain] Safely moving to default Damascus");
|
||||
@@ -680,19 +692,30 @@ class HomeCaptainController extends GetxController {
|
||||
checkAndShowBlockDialog();
|
||||
box.write(BoxName.statusDriverLocation, 'off');
|
||||
// 2. عدل الليسنر ليصبح مشروطاً
|
||||
// 2. مؤقت التتبع التلقائي (كل 5 ثوانٍ كما في الكود السابق)
|
||||
_cameraFollowTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
|
||||
// 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 {
|
||||
// 🔥 Safety double-check before animating
|
||||
if (mapHomeCaptainController != null) {
|
||||
print("🔥 [HomeCaptain] Safely moving camera to: ${loc.latitude}");
|
||||
mapHomeCaptainController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(loc, 17.5),
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -219,14 +219,36 @@ class OrderRequestController extends GetxController
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
Future<void> _calculateFullJourney() async {
|
||||
if (mapController == null) return; // Wait for controller to draw
|
||||
// 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 {
|
||||
Position driverPos = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
LatLng driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
|
||||
// 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, driverPos.heading);
|
||||
updateDriverLocation(driverLatLng, driverHeading);
|
||||
|
||||
// Clear old polylines to avoid "ghost lines"
|
||||
polylines.clear();
|
||||
@@ -240,9 +262,9 @@ class OrderRequestController extends GetxController
|
||||
var tripFuture = _fetchRouteData(
|
||||
start: LatLng(latPassenger, lngPassenger),
|
||||
end: LatLng(latDestination, lngDestination),
|
||||
color: Colors.green,
|
||||
color: Colors.black,
|
||||
id: 'trip_route',
|
||||
isDashed: true);
|
||||
getSteps: true); // 🔥 نطلب الخطوات للمسار
|
||||
|
||||
var results = await Future.wait([pickupFuture, tripFuture]);
|
||||
|
||||
@@ -259,6 +281,11 @@ class OrderRequestController extends GetxController
|
||||
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(
|
||||
@@ -267,8 +294,10 @@ class OrderRequestController extends GetxController
|
||||
destTime: totalTripDuration,
|
||||
destDist: totalTripDistance);
|
||||
|
||||
// Now zoom to fit all polylines and markers
|
||||
zoomToFitRide();
|
||||
// Now zoom to fit all polylines and markers (if controller available)
|
||||
if (canZoom) {
|
||||
zoomToFitRide();
|
||||
}
|
||||
|
||||
update();
|
||||
} catch (e) {
|
||||
@@ -297,18 +326,19 @@ class OrderRequestController extends GetxController
|
||||
required LatLng end,
|
||||
required Color color,
|
||||
required String id,
|
||||
bool isDashed = false}) async {
|
||||
bool getSteps = false}) async {
|
||||
try {
|
||||
if (start.latitude == 0 || end.latitude == 0) return null;
|
||||
if (mapController == null) 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': 'false',
|
||||
'steps': getSteps ? 'true' : 'false',
|
||||
'alternatives': 'false',
|
||||
'locale': 'ar',
|
||||
});
|
||||
|
||||
final response = await http.get(saasUrl, headers: {
|
||||
@@ -347,7 +377,9 @@ class OrderRequestController extends GetxController
|
||||
return {
|
||||
'distance_text': distanceText,
|
||||
'duration_text': durationText,
|
||||
'polyline': polyline
|
||||
'polyline': polyline,
|
||||
'encoded_polyline': encodedPoints,
|
||||
'raw_response': response.body, // 🔥 نمرر الـ JSON كاملاً
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
212
lib/controller/home/captin/v2_review_delta.html
Normal file
212
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>
|
||||
@@ -93,13 +93,13 @@ class NavigationController extends GetxController
|
||||
String totalDistanceRemaining = "";
|
||||
String estimatedTimeRemaining = "";
|
||||
dynamic currentManeuverModifier = 0;
|
||||
String arrivalTime = "--:--";
|
||||
String arrivalTime = "--:--"; // NEW: For the active navigation HUD
|
||||
|
||||
double _routeTotalDistanceM = 0;
|
||||
double _routeTotalDurationS = 0;
|
||||
|
||||
bool isNavigating = false;
|
||||
bool isMuted = false;
|
||||
bool isMuted = false; // Sound toggle state
|
||||
String distanceWithUnit = "";
|
||||
bool _cameraLockedToUser = true;
|
||||
bool _mapReady = false;
|
||||
@@ -114,6 +114,7 @@ class NavigationController extends GetxController
|
||||
Future<void> submitNewPlace(String name, String category) async {
|
||||
if (mapController == null || name.isEmpty || category.isEmpty) return;
|
||||
|
||||
// Get current center of the map as the picked location
|
||||
final LatLng pickedPos = mapController!.cameraPosition!.target;
|
||||
|
||||
isLoading = true;
|
||||
@@ -139,15 +140,21 @@ class NavigationController extends GetxController
|
||||
isLoading = false;
|
||||
if (response != null) {
|
||||
HapticFeedback.lightImpact();
|
||||
mySnackbarSuccess('Place added successfully! Thanks for your contribution.'.tr);
|
||||
mySnackbarSuccess(box.read(BoxName.lang) == 'ar'
|
||||
? 'تمت إضافة المكان بنجاح! شكراً لمساهمتك.'
|
||||
: 'Place added successfully! Thanks for your contribution.');
|
||||
isSelectingPlaceLocation = false;
|
||||
} else {
|
||||
mySnackbarWarning('Failed to add place. Please try again later.'.tr);
|
||||
mySnackbarWarning(box.read(BoxName.lang) == 'ar'
|
||||
? 'تعذر إضافة المكان. يرجى المحاولة لاحقاً.'
|
||||
: 'Failed to add place. Please try again later.');
|
||||
}
|
||||
update();
|
||||
} catch (e) {
|
||||
isLoading = false;
|
||||
mySnackbarWarning('An error occurred while connecting to the server.'.tr);
|
||||
mySnackbarWarning(box.read(BoxName.lang) == 'ar'
|
||||
? 'حدث خطأ أثناء الاتصال بالخادم.'
|
||||
: 'An error occurred while connecting to the server.');
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -181,6 +188,7 @@ class NavigationController extends GetxController
|
||||
return 55.0;
|
||||
}
|
||||
|
||||
// Categories list for the picker
|
||||
static final List<Map<String, String>> placeCategories = [
|
||||
{
|
||||
'id': 'restaurant',
|
||||
@@ -303,6 +311,7 @@ class NavigationController extends GetxController
|
||||
_smoothedHeading = _lerpAngle(_oldHeading, _targetHeading, t);
|
||||
|
||||
if (isStyleLoaded) {
|
||||
_updateCarMarker();
|
||||
if (_cameraLockedToUser) {
|
||||
animateCameraToPosition(myLocation!,
|
||||
bearing: _smoothedHeading,
|
||||
@@ -365,7 +374,6 @@ class NavigationController extends GetxController
|
||||
void onMapCreated(IntaleqMapController controller) async {
|
||||
Log.print("DEBUG: NavigationController.onMapCreated called");
|
||||
mapController = controller;
|
||||
await onStyleLoaded();
|
||||
}
|
||||
|
||||
Future<void> onStyleLoaded() async {
|
||||
@@ -381,6 +389,7 @@ class NavigationController extends GetxController
|
||||
if (myLocation != null) {
|
||||
Log.print("DEBUG: Animating camera to initial location: $myLocation");
|
||||
animateCameraToPosition(myLocation!);
|
||||
_updateCarMarker();
|
||||
}
|
||||
if (_fullRouteCoordinates.isNotEmpty) {
|
||||
Log.print("DEBUG: Updating initial polylines");
|
||||
@@ -394,7 +403,7 @@ class NavigationController extends GetxController
|
||||
if (isNavigating || routes.isEmpty) return;
|
||||
|
||||
int? bestIndex;
|
||||
double minDistance = 100.0;
|
||||
double minDistance = 100.0; // 100 meters threshold for tap
|
||||
|
||||
for (int i = 0; i < routes.length; i++) {
|
||||
for (var coord in routes[i].coordinates) {
|
||||
@@ -422,12 +431,12 @@ class NavigationController extends GetxController
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Text('Start Navigation?'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
content: Text('Do you want to go to this location?'.tr),
|
||||
title: const Text('بدء الملاحة؟',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
content: const Text('هل تريد الذهاب إلى هذا الموقع؟'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey)),
|
||||
child: const Text('إلغاء', style: TextStyle(color: Colors.grey)),
|
||||
onPressed: () => Get.back()),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
@@ -435,10 +444,10 @@ class NavigationController extends GetxController
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12))),
|
||||
child:
|
||||
Text('Go Now'.tr, style: const TextStyle(color: Colors.white)),
|
||||
const Text('اذهب الآن', style: TextStyle(color: Colors.white)),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
startNavigationTo(tappedPoint, infoWindowTitle: 'Selected Location'.tr);
|
||||
startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد');
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -458,6 +467,7 @@ class NavigationController extends GetxController
|
||||
_smoothedHeading = position.heading;
|
||||
update();
|
||||
if (isStyleLoaded) animateCameraToPosition(myLocation!);
|
||||
// Start the Location Stream for real-time updates
|
||||
_startLocationStream();
|
||||
_startBatchTimers();
|
||||
} catch (e) {
|
||||
@@ -467,10 +477,12 @@ class NavigationController extends GetxController
|
||||
|
||||
void _startLocationStream() {
|
||||
_locationStreamSubscription?.cancel();
|
||||
// Listen to location updates with minimum distance filter of 2 meters
|
||||
// This provides real-time updates without the 3-4 second delay
|
||||
_locationStreamSubscription = Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: 2,
|
||||
distanceFilter: 2, // Update every 2 meters
|
||||
),
|
||||
).listen(
|
||||
(Position position) {
|
||||
@@ -489,8 +501,9 @@ class NavigationController extends GetxController
|
||||
|
||||
try {
|
||||
final newLoc = LatLng(position.latitude, position.longitude);
|
||||
currentSpeed = position.speed * 3.6;
|
||||
currentSpeed = position.speed * 3.6; // Convert m/s to km/h
|
||||
|
||||
// Skip if movement is too small
|
||||
if (_lastProcessedLocation != null) {
|
||||
final d = Geolocator.distanceBetween(
|
||||
newLoc.latitude,
|
||||
@@ -507,6 +520,7 @@ class NavigationController extends GetxController
|
||||
Log.print(
|
||||
"DEBUG: Location update - Speed: ${currentSpeed.toStringAsFixed(1)} km/h, Loc: $newLoc");
|
||||
|
||||
// Update total distance
|
||||
if (_lastDistanceLocation != null) {
|
||||
final d = Geolocator.distanceBetween(
|
||||
_lastDistanceLocation!.latitude,
|
||||
@@ -536,6 +550,8 @@ class NavigationController extends GetxController
|
||||
_animController?.forward(from: 0.0);
|
||||
_lastProcessedLocation = newLoc;
|
||||
|
||||
if (isStyleLoaded) _updateCarMarker();
|
||||
|
||||
if (_fullRouteCoordinates.isNotEmpty) {
|
||||
_updateTraveledPolylineSmart(newLoc);
|
||||
_checkNavigationStep(newLoc);
|
||||
@@ -556,7 +572,7 @@ class NavigationController extends GetxController
|
||||
}
|
||||
|
||||
void _checkOffRoute(LatLng pos) {
|
||||
if (_autoRecalcInProgress || isLoading) return;
|
||||
if (!isNavigating || _autoRecalcInProgress || isLoading) return;
|
||||
if (_fullRouteCoordinates.isEmpty) return;
|
||||
|
||||
const int searchWindow = 80;
|
||||
@@ -591,33 +607,11 @@ class NavigationController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
/// Recalculate immediately from the latest GPS point to the destination.
|
||||
Future<void> _smartRecalculateRoute(LatLng currentPos) async {
|
||||
try {
|
||||
if (routes.isNotEmpty && selectedRouteIndex < routes.length - 1) {
|
||||
final nextIndex = selectedRouteIndex + 1;
|
||||
final nextRoute = routes[nextIndex];
|
||||
|
||||
double minDist = double.infinity;
|
||||
for (var coord in nextRoute.coordinates) {
|
||||
final d = Geolocator.distanceBetween(
|
||||
currentPos.latitude,
|
||||
currentPos.longitude,
|
||||
coord.latitude,
|
||||
coord.longitude,
|
||||
);
|
||||
if (d < minDist) minDist = d;
|
||||
}
|
||||
|
||||
if (minDist < 100) {
|
||||
selectRoute(nextIndex);
|
||||
Log.print("DEBUG: Switched to alternative route due to deviation");
|
||||
_autoRecalcInProgress = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_finalDestination != null) {
|
||||
await recalculateRoute();
|
||||
await recalculateRoute(origin: currentPos, keepNavigationActive: true);
|
||||
}
|
||||
_autoRecalcInProgress = false;
|
||||
} catch (e) {
|
||||
@@ -669,13 +663,13 @@ class NavigationController extends GetxController
|
||||
if (_trackBuffer.isEmpty) return;
|
||||
final batch = List<Map<String, dynamic>>.from(_trackBuffer);
|
||||
_trackBuffer.clear();
|
||||
final String driverId = (box.read(BoxName.driverID) ?? '').toString();
|
||||
final String passengerId = (box.read(BoxName.passengerID) ?? '').toString();
|
||||
|
||||
try {
|
||||
await CRUD().post(
|
||||
link: '${AppLink.locationServerSide}/add_batch.php',
|
||||
payload: {
|
||||
'driver_id': driverId,
|
||||
'driver_id': passengerId,
|
||||
'batch_data': jsonEncode(batch),
|
||||
'session_dist': totalDistance.toStringAsFixed(1),
|
||||
},
|
||||
@@ -685,6 +679,10 @@ class NavigationController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateCarMarker() async {
|
||||
// Car marker is now handled natively by myLocationEnabled: true.
|
||||
}
|
||||
|
||||
void animateCameraToPosition(LatLng position,
|
||||
{double? zoom, double bearing = 0.0, double tilt = 0.0}) {
|
||||
if (!_mapReady || mapController == null) return;
|
||||
@@ -774,8 +772,11 @@ class NavigationController extends GetxController
|
||||
|
||||
Future<void> _updatePolylinesSets(
|
||||
List<LatLng> traveled, List<LatLng> remaining) async {
|
||||
Log.print(
|
||||
"DEBUG: Updating polylines. Traveled: ${traveled.length}, Remaining: ${remaining.length}");
|
||||
Set<Polyline> newPolylines = {};
|
||||
|
||||
// Render Alternative Routes first
|
||||
for (int i = 0; i < routes.length; i++) {
|
||||
if (i == selectedRouteIndex) continue;
|
||||
newPolylines.add(Polyline(
|
||||
@@ -840,7 +841,7 @@ class NavigationController extends GetxController
|
||||
if (dest != null && myLocation != null) {
|
||||
getRoute(myLocation!, dest);
|
||||
} else {
|
||||
mySnackbarWarning(box.read(BoxName.lang) == 'ar' ? 'الموقع غير متاح حالياً.' : 'Location not available.');
|
||||
mySnackbarWarning('الموقع غير متاح حالياً.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,12 +866,13 @@ class NavigationController extends GetxController
|
||||
LatLng getAirportLatLng() {
|
||||
final String country = box.read(BoxName.countryCode) ?? 'JO';
|
||||
if (country == 'SY') {
|
||||
return const LatLng(33.4111, 36.5147);
|
||||
return const LatLng(33.4111, 36.5147); // Damascus Airport
|
||||
}
|
||||
return const LatLng(31.7225, 35.9933);
|
||||
return const LatLng(31.7225, 35.9933); // Queen Alia Airport (JO)
|
||||
}
|
||||
|
||||
Future<void> getRoute(LatLng origin, LatLng destination) async {
|
||||
Future<void> getRoute(LatLng origin, LatLng destination,
|
||||
{bool keepNavigationActive = false}) async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
@@ -899,12 +901,13 @@ class NavigationController extends GetxController
|
||||
if (response.statusCode != 200) {
|
||||
isLoading = false;
|
||||
update();
|
||||
mySnackbarWarning(box.read(BoxName.lang) == 'ar' ? 'تعذر الاتصال بخدمة التوجيه.' : 'Failed to connect to routing service.');
|
||||
mySnackbarWarning('تعذر الاتصال بخدمة التوجيه.');
|
||||
return;
|
||||
}
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
// ── Parse primary route (top-level in response) ──
|
||||
routes.clear();
|
||||
final primaryPts = data['points']?.toString() ?? "";
|
||||
if (primaryPts.isNotEmpty) {
|
||||
@@ -919,8 +922,10 @@ class NavigationController extends GetxController
|
||||
));
|
||||
}
|
||||
|
||||
// ── Parse alternative routes (in data['alternatives']) ──
|
||||
// إذا كان هناك routes بديلة متاحة من API
|
||||
if (data['alternatives'] != null && data['alternatives'] is List) {
|
||||
_hasAlternativeRoutes = (data['alternatives'] as List).isNotEmpty;
|
||||
_hasAlternativeRoutes = data['alternatives'].isNotEmpty;
|
||||
for (var alt in data['alternatives']) {
|
||||
final altPts = alt['points']?.toString() ?? "";
|
||||
if (altPts.isEmpty) continue;
|
||||
@@ -934,6 +939,9 @@ class NavigationController extends GetxController
|
||||
points: altPts,
|
||||
));
|
||||
}
|
||||
if (_hasAlternativeRoutes) {
|
||||
Log.print("DEBUG: ${routes.length - 1} alternative routes available");
|
||||
}
|
||||
} else {
|
||||
_hasAlternativeRoutes = false;
|
||||
}
|
||||
@@ -941,7 +949,7 @@ class NavigationController extends GetxController
|
||||
if (routes.isEmpty) {
|
||||
isLoading = false;
|
||||
update();
|
||||
mySnackbarWarning(box.read(BoxName.lang) == 'ar' ? 'لم يتم العثور على مسار.' : 'No route found.');
|
||||
mySnackbarWarning('لم يتم العثور على مسار.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -965,8 +973,8 @@ class NavigationController extends GetxController
|
||||
currentStepIndex = 0;
|
||||
_nextInstructionSpoken = false;
|
||||
|
||||
isNavigating = false;
|
||||
_cameraLockedToUser = false;
|
||||
isNavigating = keepNavigationActive;
|
||||
_cameraLockedToUser = keepNavigationActive;
|
||||
_offRouteStartTime = null;
|
||||
isLoading = false;
|
||||
|
||||
@@ -986,7 +994,13 @@ class NavigationController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
if (_fullRouteCoordinates.length >= 2) {
|
||||
// Re-add car marker after polyline updates (ensures it stays on top)
|
||||
if (isStyleLoaded) _updateCarMarker();
|
||||
|
||||
if (keepNavigationActive && myLocation != null) {
|
||||
animateCameraToPosition(myLocation!,
|
||||
bearing: _smoothedHeading, zoom: _targetZoom, tilt: _targetTilt);
|
||||
} else if (_fullRouteCoordinates.length >= 2) {
|
||||
final bounds =
|
||||
data['bbox'] != null && (data['bbox'] as List).length == 4
|
||||
? LatLngBounds(
|
||||
@@ -1012,17 +1026,22 @@ class NavigationController extends GetxController
|
||||
final remainingM = _routeTotalDistanceM * fraction;
|
||||
final remainingS = _routeTotalDurationS * fraction;
|
||||
|
||||
// Distance
|
||||
final String langCode = box.read(BoxName.lang) ?? 'ar';
|
||||
if (remainingM > 1000) {
|
||||
totalDistanceRemaining = (remainingM / 1000).toStringAsFixed(1);
|
||||
// We will handle the unit in the view or provide a unit string here
|
||||
} else {
|
||||
totalDistanceRemaining = remainingM.toStringAsFixed(0);
|
||||
}
|
||||
// New variable to hold formatted distance with unit
|
||||
distanceWithUnit = _formatDistance(remainingM, langCode);
|
||||
|
||||
// Time Remaining
|
||||
final minutes = (remainingS / 60).round();
|
||||
estimatedTimeRemaining = minutes.toString();
|
||||
|
||||
// Arrival Time Calculation
|
||||
final arrival = DateTime.now().add(Duration(seconds: remainingS.toInt()));
|
||||
final h = arrival.hour > 12
|
||||
? arrival.hour - 12
|
||||
@@ -1040,6 +1059,7 @@ class NavigationController extends GetxController
|
||||
_finalDestination = destination;
|
||||
await clearRoute(isNewRoute: true);
|
||||
|
||||
// Preserve car marker if it exists
|
||||
markers = markers.where((m) => m.markerId.value == 'car').toSet();
|
||||
|
||||
markers.add(Marker(
|
||||
@@ -1065,12 +1085,23 @@ class NavigationController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> recalculateRoute() async {
|
||||
if (myLocation == null || _finalDestination == null || isLoading) return;
|
||||
Future<void> recalculateRoute(
|
||||
{LatLng? origin, bool keepNavigationActive = false}) async {
|
||||
final LatLng? routeOrigin = origin ?? myLocation;
|
||||
if (routeOrigin == null || _finalDestination == null || isLoading) return;
|
||||
|
||||
isLoading = true;
|
||||
update();
|
||||
mySnackbarInfo(box.read(BoxName.lang) == 'ar' ? 'جاري حساب مسار جديد...' : 'Calculating new route...');
|
||||
await getRoute(myLocation!, _finalDestination!);
|
||||
|
||||
markers = markers.where((m) => m.markerId.value != 'origin').toSet();
|
||||
markers.add(Marker(
|
||||
markerId: const MarkerId('origin'),
|
||||
position: routeOrigin,
|
||||
icon: InlqBitmap.fromStyleImage('start_icon'),
|
||||
));
|
||||
|
||||
await getRoute(routeOrigin, _finalDestination!,
|
||||
keepNavigationActive: keepNavigationActive);
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
@@ -1087,8 +1118,11 @@ class NavigationController extends GetxController
|
||||
isNavigating = true;
|
||||
_cameraLockedToUser = true;
|
||||
|
||||
// Ensure ETA and distances are up-to-date
|
||||
_lastTraveledIndexInFullRoute = _lastTraveledIndexInFullRoute;
|
||||
_recomputeETA();
|
||||
|
||||
// Initialize current instruction if available
|
||||
if (routeSteps.isNotEmpty && currentStepIndex < routeSteps.length) {
|
||||
currentInstruction = routeSteps[currentStepIndex]['text'] ?? "";
|
||||
currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0;
|
||||
@@ -1105,6 +1139,7 @@ class NavigationController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
// Center camera on user for navigation mode
|
||||
if (myLocation != null) {
|
||||
animateCameraToPosition(myLocation!,
|
||||
bearing: _smoothedHeading, zoom: _targetZoom, tilt: _targetTilt);
|
||||
@@ -1145,22 +1180,21 @@ class NavigationController extends GetxController
|
||||
_routeTotalDistanceM = 0;
|
||||
_routeTotalDurationS = 0;
|
||||
|
||||
if (!isNewRoute) {
|
||||
await _updateCarMarker();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> _loadCustomIcons() async {
|
||||
if (mapController == null) return;
|
||||
try {
|
||||
final carBytes = await rootBundle.load('assets/images/car.png');
|
||||
final startBytes = await rootBundle.load('assets/images/A.png');
|
||||
final destBytes = await rootBundle.load('assets/images/b.png');
|
||||
await mapController!.addImage('car_icon', carBytes.buffer.asUint8List());
|
||||
await mapController!
|
||||
.addImage('start_icon', startBytes.buffer.asUint8List());
|
||||
await mapController!.addImage('dest_icon', destBytes.buffer.asUint8List());
|
||||
} catch (e) {
|
||||
Log.print("Error loading custom icons: $e");
|
||||
}
|
||||
final carBytes = await rootBundle.load('assets/images/car.png');
|
||||
final startBytes = await rootBundle.load('assets/images/A.png');
|
||||
final destBytes = await rootBundle.load('assets/images/b.png');
|
||||
await mapController!.addImage('car_icon', carBytes.buffer.asUint8List());
|
||||
await mapController!
|
||||
.addImage('start_icon', startBytes.buffer.asUint8List());
|
||||
await mapController!.addImage('dest_icon', destBytes.buffer.asUint8List());
|
||||
}
|
||||
|
||||
void _checkNavigationStep(LatLng pos) {
|
||||
@@ -1233,18 +1267,21 @@ class NavigationController extends GetxController
|
||||
if (mapController == null) return;
|
||||
|
||||
try {
|
||||
// ✅ Use searchPlaces from intaleq_maps SDK
|
||||
final results = await mapController!.searchPlaces(q);
|
||||
|
||||
|
||||
if (myLocation != null) {
|
||||
for (final p in results) {
|
||||
final plat = double.tryParse(p['latitude']?.toString() ?? '0') ?? 0.0;
|
||||
final plng = double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0;
|
||||
p['distanceKm'] = _haversineKm(myLocation!.latitude, myLocation!.longitude, plat, plng);
|
||||
final plng =
|
||||
double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0;
|
||||
p['distanceKm'] = _haversineKm(
|
||||
myLocation!.latitude, myLocation!.longitude, plat, plng);
|
||||
}
|
||||
results.sort((a, b) =>
|
||||
(a['distanceKm'] as double).compareTo(b['distanceKm'] as double));
|
||||
}
|
||||
|
||||
|
||||
placesDestination = results;
|
||||
update();
|
||||
} catch (e) {
|
||||
@@ -1258,7 +1295,7 @@ class NavigationController extends GetxController
|
||||
final lat = double.parse(place['latitude'].toString());
|
||||
final lng = double.parse(place['longitude'].toString());
|
||||
await startNavigationTo(LatLng(lat, lng),
|
||||
infoWindowTitle: place['name'] ?? (box.read(BoxName.lang) == 'ar' ? 'وجهة' : 'Destination'));
|
||||
infoWindowTitle: place['name'] ?? 'وجهة');
|
||||
}
|
||||
|
||||
void onSearchChanged(String query) {
|
||||
@@ -1278,6 +1315,9 @@ class NavigationController extends GetxController
|
||||
return R * 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||
}
|
||||
|
||||
double _kmToLatDelta(double km) => km / 111.32;
|
||||
double _kmToLngDelta(double km, double lat) =>
|
||||
km / (111.32 * cos(lat * pi / 180));
|
||||
LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
|
||||
double? x0, x1, y0, y1;
|
||||
for (final ll in list) {
|
||||
@@ -1328,12 +1368,12 @@ class NavigationController extends GetxController
|
||||
'name': name,
|
||||
'lat': myLocation!.latitude.toString(),
|
||||
'lng': myLocation!.longitude.toString(),
|
||||
'driver_id': box.read(BoxName.driverID),
|
||||
'passenger_id': box.read(BoxName.passengerID),
|
||||
};
|
||||
await CRUD().post(link: AppLink.getPlacesSyria, payload: payload);
|
||||
mySnackbarInfo(box.read(BoxName.lang) == 'ar'
|
||||
? "تم استلام اقتراحك! شكراً لمساهمتك."
|
||||
: "Suggestion received! Thanks for your contribution.");
|
||||
? "تم استلام اقتراحك! مكافأتك: +٥٠ نقطة"
|
||||
: "Suggestion received! Reward: +50 points");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
|
||||
201
lib/controller/home/profile/complaint_controller.dart
Normal file
201
lib/controller/home/profile/complaint_controller.dart
Normal file
@@ -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:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/constant/links.dart';
|
||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:sefer_driver/controller/functions/encrypt_decrypt.dart';
|
||||
import 'package:sefer_driver/env/env.dart';
|
||||
import 'package:sefer_driver/print.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:sefer_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:sefer_driver/views/auth/captin/login_captin.dart';
|
||||
import 'package:sefer_driver/views/home/on_boarding_page.dart';
|
||||
import '../functions/app_update_controller.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../main.dart';
|
||||
@@ -33,6 +34,7 @@ class SplashScreenController extends GetxController
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
Get.put(AppUpdateController()); // تهيئة متحكم التحديثات الذكي
|
||||
_setupAnimations();
|
||||
_initializeAndNavigate();
|
||||
checkSecurity();
|
||||
|
||||
Reference in New Issue
Block a user