استكمال قرار 2026-07-27 (كوميت fff76c9 أرشف الباك إند):
apps/rider → apps/rider-archive-cubit
apps/driver → apps/driver-archive-cubit
dashboards/admin-web → dashboards/admin-web-archive
dashboards/service-web → dashboards/service-web-archive
إعادة تسمية بلا تعديل محتوى. rider/driver كانا إعادة بناء Cubit مبنية على
عقد NestJS المؤرشف. admin-web و service-web نموذجان بملف HTML واحد (164 و
128 سطراً) — يحلّ محلهما siro_admin و siro_service المكتملان.
يُحمل من المؤرشف ولا يُعاد اكتشافه: عقد map-saas المثبَّت بالتجربة في
apps/rider-archive-cubit/lib/core/api/ (x-api-key ترويسة، reverse مصفوفة،
route في points) ودرس مسار FCM.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
2.5 KiB
Dart
92 lines
2.5 KiB
Dart
import 'dart:async';
|
|
import 'package:geolocator/geolocator.dart';
|
|
|
|
/// خدمة موقع السائق — تتبع الموقع في الخلفية وإرساله للسيرفر.
|
|
///
|
|
/// تتبع 3 ثوانٍ عند التنشيط، ثم إرسال كل دقيقتين (docs/23 §10).
|
|
/// تراعي حالة البطارية وتتوقف عند انخفاضها.
|
|
class DriverLocationService {
|
|
final Future<void> Function(double lat, double lng, {double? heading, double? speed})
|
|
_onLocationUpdate;
|
|
|
|
StreamSubscription<Position>? _positionSub;
|
|
Timer? _heartbeatTimer;
|
|
bool _active = false;
|
|
|
|
DriverLocationService(this._onLocationUpdate);
|
|
|
|
bool get isActive => _active;
|
|
|
|
/// بدء التتبع.
|
|
Future<void> start() async {
|
|
if (_active) return;
|
|
|
|
// تحقق من صلاحية الموقع.
|
|
var permission = await Geolocator.checkPermission();
|
|
if (permission == LocationPermission.denied) {
|
|
permission = await Geolocator.requestPermission();
|
|
if (permission == LocationPermission.denied ||
|
|
permission == LocationPermission.deniedForever) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
_active = true;
|
|
|
|
// أرسل الموقع فوراً.
|
|
_sendCurrentLocation();
|
|
|
|
// استمع لتحديثات الموقع كل 3 ثوانٍ.
|
|
_positionSub = Geolocator.getPositionStream(
|
|
locationSettings: const LocationSettings(
|
|
accuracy: LocationAccuracy.high,
|
|
distanceFilter: 15, // تحديث عند تغيير 15 متر
|
|
),
|
|
).listen((pos) {
|
|
_onLocationUpdate(
|
|
pos.latitude,
|
|
pos.longitude,
|
|
heading: pos.heading,
|
|
speed: pos.speed,
|
|
);
|
|
});
|
|
|
|
// دفعة كل دقيقتين كحد أقصى.
|
|
_heartbeatTimer = Timer.periodic(const Duration(minutes: 2), (_) {
|
|
_sendCurrentLocation();
|
|
});
|
|
}
|
|
|
|
/// إيقاف التتبع.
|
|
void stop() {
|
|
_active = false;
|
|
_positionSub?.cancel();
|
|
_positionSub = null;
|
|
_heartbeatTimer?.cancel();
|
|
_heartbeatTimer = null;
|
|
}
|
|
|
|
/// إرسال الموقع الحالي مرة واحدة.
|
|
Future<void> _sendCurrentLocation() async {
|
|
try {
|
|
final pos = await Geolocator.getCurrentPosition(
|
|
locationSettings: const LocationSettings(
|
|
accuracy: LocationAccuracy.high,
|
|
),
|
|
);
|
|
await _onLocationUpdate(
|
|
pos.latitude,
|
|
pos.longitude,
|
|
heading: pos.heading,
|
|
speed: pos.speed,
|
|
);
|
|
} catch (_) {
|
|
// خطأ صامت — سيحاول مرة أخرى مع الدقيقة التالية.
|
|
}
|
|
}
|
|
|
|
void dispose() {
|
|
stop();
|
|
}
|
|
}
|