diff --git a/backend/transit/cron_approaching_alerts.php b/backend/transit/cron_approaching_alerts.php new file mode 100644 index 00000000..7266abe9 --- /dev/null +++ b/backend/transit/cron_approaching_alerts.php @@ -0,0 +1,87 @@ +query( + "SELECT id, route_id, current_stop_seq FROM transit_trips WHERE status='started'" +); +$trips = $stTrips->fetchAll(); + +foreach ($trips as $trip) { + $tripId = (int)$trip['id']; + $routeId = (int)$trip['route_id']; + $currentSeq = (int)$trip['current_stop_seq']; + + // 2. جلب موقع الباص من Redis + $pos = transitGetBusPosition($tripId); + if (!$pos || empty($pos['lat'])) continue; + + $busLat = (double)$pos['lat']; + $busLng = (double)$pos['lng']; + + // 3. جلب المحطة التالية + $nextSeq = $currentSeq + 1; + $stStop = $transit_con->prepare( + "SELECT id, name_ar, latitude, longitude + FROM transit_stops + WHERE route_id=? AND sequence=? LIMIT 1" + ); + $stStop->execute([$routeId, $nextSeq]); + $nextStop = $stStop->fetch(); + + if ($nextStop) { + $stopId = (int)$nextStop['id']; + $stopLat = (double)$nextStop['latitude']; + $stopLng = (double)$nextStop['longitude']; + $stopName = $nextStop['name_ar']; + + // حساب المسافة تقريبياً + $dist = transitCalculateDistance($busLat, $busLng, $stopLat, $stopLng); + + // إذا كانت المسافة أقل من 2 كم + if ($dist <= 2000) { + // نتحقق من Redis كي لا نرسل التنبيه مراراً لنفس المحطة في نفس الرحلة + $alertKey = "transit:trip:{$tripId}:alert_stop:{$stopId}"; + if (!$redis->exists($alertKey)) { + $redis->set($alertKey, '1', 86400); // 24 ساعة + + // 4. جلب الركاب المشتركين الذين محطتهم المفضلة هي هذه + $stPass = $transit_con->prepare( + "SELECT passenger_id FROM transit_enrollments + WHERE preferred_stop_id=? AND status='active'" + ); + $stPass->execute([$stopId]); + $passengers = $stPass->fetchAll(PDO::FETCH_COLUMN); + + if (!empty($passengers)) { + $title = "الباص يقترب 🚌"; + $body = "حافلتك أصبحت قريبة جداً من محطة: {$stopName}. استعد!"; + + foreach ($passengers as $pId) { + transitSendNotificationToPassenger($pId, $title, $body); + } + } + } + } + } +} + +/** + * حساب المسافة بين نقطتين بالمتر (Haversine) + */ +function transitCalculateDistance($lat1, $lon1, $lat2, $lon2) { + $earthRadius = 6371000; // Radius of the earth in meters + $dLat = deg2rad($lat2 - $lat1); + $dLon = deg2rad($lon2 - $lon1); + $a = sin($dLat/2) * sin($dLat/2) + + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * + sin($dLon/2) * sin($dLon/2); + $c = 2 * atan2(sqrt($a), sqrt(1-$a)); + return $earthRadius * $c; +} + +echo "Approaching alerts checked.\n"; diff --git a/backend/transit/trip/board_passenger.php b/backend/transit/trip/board_passenger.php new file mode 100644 index 00000000..864abf21 --- /dev/null +++ b/backend/transit/trip/board_passenger.php @@ -0,0 +1,81 @@ +prepare( + "SELECT id, org_id FROM transit_trips WHERE driver_id=? AND status='started' LIMIT 1" +); +$stTrip->execute([$transit_driver_id]); +$trip = $stTrip->fetch(); + +if (!$trip) { + jsonError('لا توجد رحلة نشطة حالياً لتسجيل الصعود', 403); +} + +$tripId = (int)$trip['id']; +$orgId = (int)$trip['org_id']; + +// 2. تأكد من أن الراكب مشترك وفعال في هذه المؤسسة +$stEnroll = $transit_con->prepare( + "SELECT id, member_name FROM transit_enrollments + WHERE org_id=? AND passenger_id=? AND status='active' LIMIT 1" +); +$stEnroll->execute([$orgId, $passengerId]); +$enroll = $stEnroll->fetch(); + +if (!$enroll) { + jsonError('هذا الراكب غير مسجل أو اشتراكه غير فعال', 403); +} + +$enrollId = (int)$enroll['id']; + +// 3. تأكد أنه لم يُسجل صعوده من قبل في هذه الرحلة +$stCheck = $transit_con->prepare( + "SELECT id FROM transit_boardings WHERE trip_id=? AND enrollment_id=? LIMIT 1" +); +$stCheck->execute([$tripId, $enrollId]); +if ($stCheck->fetch()) { + jsonError('تم تسجيل صعود هذا الراكب مسبقاً في هذه الرحلة', 400); +} + +// 4. تسجيل الصعود +$stInsert = $transit_con->prepare( + "INSERT INTO transit_boardings (trip_id, enrollment_id, stop_id, boarded_at, method) + VALUES (?, ?, ?, NOW(), 'qr')" +); +$stInsert->execute([$tripId, $enrollId, $stopId]); + +// 5. إرسال إشعار FCM لولي الأمر (إن وُجد) +$stGuard = $transit_con->prepare( + "SELECT guardian_passenger_id FROM transit_guardians + WHERE enrollment_id=? AND notify_board=1" +); +$stGuard->execute([$enrollId]); +$guardians = $stGuard->fetchAll(PDO::FETCH_COLUMN); + +// إذا كان الطالب نفسه يملك التطبيق، أرسل له أيضاً (للتأكيد في محفظته) +$guardians[] = $passengerId; + +if (!empty($guardians)) { + $title = "صعود الحافلة 🚌"; + $body = "لقد صعد " . ($enroll['member_name'] ?? 'الطالب') . " إلى الحافلة بنجاح."; + + // إرسال الإشعار باستخدام دالة الإشعارات الأساسية بالنظام (عبر passenger_id) + foreach ($guardians as $gId) { + // يمكنك هنا مناداة دالة sendFCMToPassenger الموجودة في functions.php الأساسية + // للتبسيط، نكتفي بافتراض وجود دالة transitSendNotificationToPassenger أو نستخدم FCM_Engine + transitSendNotificationToPassenger($gId, $title, $body); + } +} + +jsonSuccess(['boarded' => true]); diff --git a/backend/transit/trip/stop_passengers.php b/backend/transit/trip/stop_passengers.php new file mode 100644 index 00000000..0f5f1072 --- /dev/null +++ b/backend/transit/trip/stop_passengers.php @@ -0,0 +1,42 @@ +prepare( + "SELECT id, org_id FROM transit_trips WHERE driver_id=? AND status='started' LIMIT 1" +); +$stTrip->execute([$transit_driver_id]); +$trip = $stTrip->fetch(); + +if (!$trip) { + jsonError('لا توجد رحلة نشطة حالياً', 403); +} + +$tripId = (int)$trip['id']; +$orgId = (int)$trip['org_id']; + +// 2. جلب جميع الركاب المشتركين الذين محطتهم المفضلة هي هذه المحطة (أو جميع ركاب الخط إن لم يكن هناك تخصيص) +// سنكتفي بجلب المخصصين لهذه المحطة + عرض حالة صعودهم +$stPass = $transit_con->prepare( + "SELECT e.passenger_id, e.student_id, e.member_name AS name, + (SELECT COUNT(id) FROM transit_boardings b + WHERE b.trip_id = ? AND b.enrollment_id = e.id) AS is_boarded + FROM transit_enrollments e + WHERE e.org_id = ? AND e.status = 'active' AND e.preferred_stop_id = ?" +); +$stPass->execute([$tripId, $orgId, $stopId]); +$passengers = $stPass->fetchAll(); + +// تحويل 0/1 إلى true/false +foreach ($passengers as &$p) { + $p['is_boarded'] = (bool)$p['is_boarded']; +} +unset($p); + +jsonSuccess($passengers); diff --git a/siro_driver/lib/controller/local/ar_jo.dart b/siro_driver/lib/controller/local/ar_jo.dart index 1e8d0c38..a06d98ed 100644 --- a/siro_driver/lib/controller/local/ar_jo.dart +++ b/siro_driver/lib/controller/local/ar_jo.dart @@ -2693,4 +2693,19 @@ final Map ar_jo = { "Destination cleared!": "تم مسح الوجهة الشخصية!", "Move map to select destination": "حرك الخريطة لتحديد الوجهة الشخصية", "service_unavailable_area": "هذه الخدمة غير متوفرة حالياً في منطقتك", + "بطاقة الصعود (Boarding Pass)": "بطاقة الصعود (Boarding Pass)", + "بطاقة الصعود": "بطاقة الصعود", + "يرجى إبراز هذا الرمز للسائق عند الصعود للحافلة": "يرجى إبراز هذا الرمز للسائق عند الصعود للحافلة", + "مسافر موثق": "مسافر موثق", + "Now at": "الآن في", + "Scan": "مسح", + "Scan Passengers": "مسح الركاب", + "Manual Passenger List": "القائمة اليدوية", + "Manual List": "القائمة اليدوية", + "تم صعود الراكب بنجاح ✅": "تم صعود الراكب بنجاح ✅", + "فشل التحقق ❌": "فشل التحقق ❌", + "تم تسجيل حضور الراكب": "تم تسجيل حضور الراكب", + "فشل تسجيل الحضور": "فشل تسجيل الحضور", + "لا يوجد ركاب مسجلين في هذه المحطة": "لا يوجد ركاب مسجلين في هذه المحطة", + "حاضر": "حاضر", }; diff --git a/siro_driver/lib/controller/local/en.dart b/siro_driver/lib/controller/local/en.dart index 3de75d3e..ef3f47c1 100644 --- a/siro_driver/lib/controller/local/en.dart +++ b/siro_driver/lib/controller/local/en.dart @@ -1603,6 +1603,22 @@ final Map en = { "Submit Your Complaint": "Submit Your Complaint", "Submit Your Question": "Submit Your Question", "Submit a Complaint": "Submit a Complaint", + "your current rating": "your current rating", + "بطاقة الصعود (Boarding Pass)": "Boarding Pass", + "بطاقة الصعود": "Boarding Pass", + "يرجى إبراز هذا الرمز للسائق عند الصعود للحافلة": "Please show this code to the driver when boarding the bus", + "مسافر موثق": "Verified Passenger", + "Now at": "Now at", + "Scan": "Scan", + "Scan Passengers": "Scan Passengers", + "Manual Passenger List": "Manual Passenger List", + "Manual List": "Manual List", + "تم صعود الراكب بنجاح ✅": "Passenger boarded successfully ✅", + "فشل التحقق ❌": "Verification failed ❌", + "تم تسجيل حضور الراكب": "Passenger attendance recorded", + "فشل تسجيل الحضور": "Failed to record attendance", + "لا يوجد ركاب مسجلين في هذه المحطة": "No passengers registered at this stop", + "حاضر": "Present", "Submit rating": "Submit rating", "Success": "Success", "Suez Canal Bank": "Suez Canal Bank", diff --git a/siro_driver/lib/controller/transit/transit_driver_controller.dart b/siro_driver/lib/controller/transit/transit_driver_controller.dart index 5d2a1174..d7c5c669 100644 --- a/siro_driver/lib/controller/transit/transit_driver_controller.dart +++ b/siro_driver/lib/controller/transit/transit_driver_controller.dart @@ -4,6 +4,7 @@ import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart' show LatLng; import '../functions/location_controller.dart'; +import '../widgets/error_snakbar.dart'; import 'transit_driver_models.dart'; import 'transit_driver_service.dart'; @@ -117,7 +118,7 @@ class TransitDriverController extends GetxController { } await fetchTodayTrips(); } else { - Get.snackbar('مواصلاتي', res.message); + mySnackbarError(res.message); } isActionInProgress = false; @@ -139,7 +140,7 @@ class TransitDriverController extends GetxController { } await fetchTodayTrips(); } else { - Get.snackbar('مواصلاتي', res.message); + mySnackbarError(res.message); } isActionInProgress = false; @@ -154,10 +155,23 @@ class TransitDriverController extends GetxController { delayMinutes: minutes, reason: reason, ); - if (!res.success) Get.snackbar('مواصلاتي', res.message); + if (!res.success) mySnackbarError(res.message); return res.success; } + Future boardPassenger(int stopId, String passengerId) async { + final res = await TransitDriverService.boardPassenger(stopId, passengerId); + return res.success; + } + + Future>> getStopPassengers(int stopId) async { + final res = await TransitDriverService.getStopPassengers(stopId); + if (res.success && res.data != null) { + return res.data!; + } + return []; + } + // ── كشف الوصول للمحطات (جيوفينس) ──────────────────────────── // تُستدعى من LocationController عند كل تحديث للموقع أثناء وضع الباص void onLocationUpdate(LatLng pos) { diff --git a/siro_driver/lib/controller/transit/transit_driver_models.dart b/siro_driver/lib/controller/transit/transit_driver_models.dart index 9914d358..a6cae121 100644 --- a/siro_driver/lib/controller/transit/transit_driver_models.dart +++ b/siro_driver/lib/controller/transit/transit_driver_models.dart @@ -1,6 +1,7 @@ // transit_driver_models.dart — نماذج بيانات مواصلاتي (جهة سائق الباص) class TransitStopInfo { + final int id; final int sequence; final String nameAr; final double latitude; @@ -9,6 +10,7 @@ class TransitStopInfo { final int? etaOffsetMin; TransitStopInfo({ + required this.id, required this.sequence, required this.nameAr, required this.latitude, @@ -18,6 +20,7 @@ class TransitStopInfo { }); factory TransitStopInfo.fromJson(Map j) => TransitStopInfo( + id: int.tryParse(j['id']?.toString() ?? '0') ?? 0, sequence: int.tryParse(j['sequence'].toString()) ?? 0, nameAr: j['name_ar']?.toString() ?? '', latitude: double.tryParse(j['latitude']?.toString() ?? '0') ?? 0, diff --git a/siro_driver/lib/controller/transit/transit_driver_service.dart b/siro_driver/lib/controller/transit/transit_driver_service.dart index 754c017c..a2ebd4c9 100644 --- a/siro_driver/lib/controller/transit/transit_driver_service.dart +++ b/siro_driver/lib/controller/transit/transit_driver_service.dart @@ -108,4 +108,32 @@ class TransitDriverService { } return TransitApiResult(false, null, _errMsg(res)); } + + static Future> boardPassenger(int stopId, String passengerId) async { + final res = await CRUD().post( + link: '$_base/trip/board_passenger.php', + payload: { + 'stop_id': stopId.toString(), + 'passenger_id': passengerId, + }, + ); + if (res is Map && res['status'] == 'success') { + return TransitApiResult(true, true, 'ok'); + } + return TransitApiResult(false, false, _errMsg(res)); + } + + static Future>>> getStopPassengers(int stopId) async { + final res = await CRUD().post( + link: '$_base/trip/stop_passengers.php', + payload: { + 'stop_id': stopId.toString(), + }, + ); + if (res is Map && res['status'] == 'success' && res['message'] is List) { + final list = (res['message'] as List).map((e) => Map.from(e)).toList(); + return TransitApiResult(true, list, 'ok'); + } + return TransitApiResult(false, [], _errMsg(res)); + } } diff --git a/siro_driver/lib/views/transit/manual_boarding_page.dart b/siro_driver/lib/views/transit/manual_boarding_page.dart new file mode 100644 index 00000000..fa35be7c --- /dev/null +++ b/siro_driver/lib/views/transit/manual_boarding_page.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../constant/colors.dart'; +import '../../constant/style.dart'; +import '../../controller/transit/transit_driver_controller.dart'; +import '../widgets/elevated_btn.dart'; +import '../widgets/error_snakbar.dart'; + +class ManualBoardingPage extends StatefulWidget { + final int stopId; + final String stopName; + const ManualBoardingPage({super.key, required this.stopId, required this.stopName}); + + @override + State createState() => _ManualBoardingPageState(); +} + +class _ManualBoardingPageState extends State { + bool isLoading = true; + List> passengers = []; + + @override + void initState() { + super.initState(); + _loadPassengers(); + } + + Future _loadPassengers() async { + final c = Get.find(); + // For now, this is a placeholder. You'll need to add getStopPassengers in the controller. + final result = await c.getStopPassengers(widget.stopId); + if (mounted) { + setState(() { + passengers = result; + isLoading = false; + }); + } + } + + void _markBoarded(int index) async { + final c = Get.find(); + final pId = passengers[index]['passenger_id']?.toString() ?? ''; + if (pId.isEmpty) return; + + final success = await c.boardPassenger(widget.stopId, pId); + if (success && mounted) { + setState(() { + passengers[index]['is_boarded'] = true; + }); + mySnackbarSuccess('تم تسجيل حضور الراكب'.tr); + } else { + mySnackbarError('فشل تسجيل الحضور'.tr); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColor.secondaryColor, + appBar: AppBar( + title: Text('${'Manual List'.tr} - ${widget.stopName}', style: AppStyle.headTitle2.copyWith(color: AppColor.writeColor)), + backgroundColor: AppColor.secondaryColor, + elevation: 0, + iconTheme: IconThemeData(color: AppColor.writeColor), + ), + body: isLoading + ? const Center(child: CircularProgressIndicator()) + : passengers.isEmpty + ? Center(child: Text('لا يوجد ركاب مسجلين في هذه المحطة'.tr)) + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: passengers.length, + itemBuilder: (context, index) { + final p = passengers[index]; + final isBoarded = p['is_boarded'] == true; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: AppColor.cardColor, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 2)) + ], + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: CircleAvatar( + backgroundColor: isBoarded ? AppColor.greenColor.withOpacity(0.2) : AppColor.grayColor.withOpacity(0.2), + child: Icon(Icons.person, color: isBoarded ? AppColor.greenColor : AppColor.grayColor), + ), + title: Text(p['name'] ?? 'Unknown', style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)), + subtitle: Text('ID: ${p['student_id'] ?? p['passenger_id']}', style: AppStyle.subtitle.copyWith(color: AppColor.grayColor)), + trailing: isBoarded + ? const Icon(Icons.check_circle, color: AppColor.greenColor, size: 28) + : SizedBox( + width: 80, + height: 35, + child: MyElevatedButton( + title: 'حاضر'.tr, + kolor: AppColor.accentColor, + onPressed: () => _markBoarded(index), + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/siro_driver/lib/views/transit/qr_scanner_page.dart b/siro_driver/lib/views/transit/qr_scanner_page.dart new file mode 100644 index 00000000..2c6478f7 --- /dev/null +++ b/siro_driver/lib/views/transit/qr_scanner_page.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:audioplayers/audioplayers.dart'; + +import '../../constant/colors.dart'; +import '../../constant/style.dart'; +import '../../controller/transit/transit_driver_controller.dart'; +import '../widgets/elevated_btn.dart'; +import 'manual_boarding_page.dart'; + +class QRScannerPage extends StatefulWidget { + final int stopId; + final String stopName; + const QRScannerPage({super.key, required this.stopId, required this.stopName}); + + @override + State createState() => _QRScannerPageState(); +} + +class _QRScannerPageState extends State { + final MobileScannerController _scannerController = MobileScannerController(); + final AudioPlayer _audioPlayer = AudioPlayer(); + + bool _isProcessing = false; + String _successMessage = ''; + + @override + void dispose() { + _scannerController.dispose(); + _audioPlayer.dispose(); + super.dispose(); + } + + void _onDetect(BarcodeCapture capture) async { + if (_isProcessing) return; + + final List barcodes = capture.barcodes; + if (barcodes.isEmpty) return; + + final String? code = barcodes.first.rawValue; + if (code == null || !code.startsWith('transit_board:')) return; + + setState(() { + _isProcessing = true; + }); + + final passengerId = code.replaceAll('transit_board:', ''); + + // Play beep sound + try { + // Assuming you might add a beep sound asset later, or just visual feedback for now. + // await _audioPlayer.play(AssetSource('sounds/beep.mp3')); + } catch (_) {} + + // Send API Request + final c = Get.find(); + final success = await c.boardPassenger(widget.stopId, passengerId); + + if (success) { + setState(() { + _successMessage = 'تم صعود الراكب بنجاح ✅'.tr; + }); + // Clear message after 1.5s + Future.delayed(const Duration(milliseconds: 1500), () { + if (mounted) { + setState(() { + _successMessage = ''; + _isProcessing = false; + }); + } + }); + } else { + setState(() { + _successMessage = 'فشل التحقق ❌'.tr; + }); + Future.delayed(const Duration(milliseconds: 2000), () { + if (mounted) { + setState(() { + _successMessage = ''; + _isProcessing = false; + }); + } + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: Text('${'Scan Passengers'.tr} - ${widget.stopName}', style: AppStyle.headTitle2.copyWith(color: Colors.white)), + backgroundColor: Colors.transparent, + elevation: 0, + iconTheme: const IconThemeData(color: Colors.white), + ), + body: Stack( + children: [ + MobileScanner( + controller: _scannerController, + onDetect: _onDetect, + ), + + // Overlay for scanner target + Center( + child: Container( + width: 250, + height: 250, + decoration: BoxDecoration( + border: Border.all(color: AppColor.accentColor, width: 3), + borderRadius: BorderRadius.circular(20), + ), + ), + ), + + // Processing Overlay + if (_isProcessing && _successMessage.isEmpty) + Container( + color: Colors.black54, + child: const Center( + child: CircularProgressIndicator(color: AppColor.accentColor), + ), + ), + + // Success / Error Message Overlay + if (_successMessage.isNotEmpty) + Container( + color: _successMessage.contains('✅') ? Colors.green.withValues(alpha: 0.8) : Colors.red.withValues(alpha: 0.8), + child: Center( + child: Text( + _successMessage, + style: AppStyle.headTitle2.copyWith(color: Colors.white), + textAlign: TextAlign.center, + ), + ), + ), + + // Fallback manual list button + Positioned( + bottom: 40, + left: 20, + right: 20, + child: SizedBox( + height: 55, + child: MyElevatedButton( + onPressed: () { + Get.to(() => ManualBoardingPage(stopId: widget.stopId, stopName: widget.stopName)); + }, + kolor: AppColor.cardColor, + title: 'Manual Passenger List'.tr, + ), + ), + ), + ], + ), + ); + } +} diff --git a/siro_driver/lib/views/transit/transit_driver_home_page.dart b/siro_driver/lib/views/transit/transit_driver_home_page.dart index 8b056cd9..7278e1f3 100644 --- a/siro_driver/lib/views/transit/transit_driver_home_page.dart +++ b/siro_driver/lib/views/transit/transit_driver_home_page.dart @@ -8,6 +8,7 @@ import '../../controller/transit/transit_driver_controller.dart'; import '../../controller/transit/transit_driver_models.dart'; import '../widgets/elevated_btn.dart'; import '../widgets/my_scafold.dart'; +import 'qr_scanner_page.dart'; class TransitDriverHomePage extends StatelessWidget { const TransitDriverHomePage({super.key}); @@ -194,7 +195,7 @@ class _ActivationPage extends StatelessWidget { width: 88, height: 88, decoration: BoxDecoration( - color: AppColor.accentColor.withOpacity(0.12), + color: AppColor.accentColor.withValues(alpha: 0.12), shape: BoxShape.circle, ), child: Icon(Icons.directions_bus_rounded, @@ -236,24 +237,14 @@ class _ActivationPage extends StatelessWidget { const SizedBox(height: 20), SizedBox( width: double.infinity, + height: 55, child: controller.isActivating ? const Center(child: CircularProgressIndicator()) - : ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppColor.accentColor, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12)), - ), + : MyElevatedButton( + kolor: AppColor.accentColor, onPressed: () => controller.activateWithToken(tokenCtrl.text), - child: Text( - 'Activate'.tr, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold), - ), + title: 'Activate'.tr, ), ), const SizedBox(height: 16), @@ -284,15 +275,28 @@ class _CurrentStopBanner extends StatelessWidget { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - color: AppColor.greenColor.withOpacity(0.12), + color: AppColor.greenColor.withValues(alpha: 0.12), child: Row( children: [ Icon(Icons.location_on_rounded, color: AppColor.greenColor, size: 18), const SizedBox(width: 8), - Text( - '${'Now at'.tr}: ${stop.nameAr}', - style: AppStyle.subtitle.copyWith( - color: AppColor.greenColor, fontWeight: FontWeight.bold), + Expanded( + child: Text( + '${'Now at'.tr}: ${stop.nameAr}', + style: AppStyle.subtitle.copyWith( + color: AppColor.greenColor, fontWeight: FontWeight.bold), + ), + ), + SizedBox( + height: 35, + width: 80, + child: MyElevatedButton( + onPressed: () { + Get.to(() => QRScannerPage(stopId: stop.id, stopName: stop.nameAr)); + }, + kolor: AppColor.accentColor, + title: 'Scan'.tr, + ), ), ], ), diff --git a/siro_driver/linux/flutter/generated_plugin_registrant.cc b/siro_driver/linux/flutter/generated_plugin_registrant.cc index e1cca731..10ddc94a 100644 --- a/siro_driver/linux/flutter/generated_plugin_registrant.cc +++ b/siro_driver/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,9 @@ #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); + audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/siro_driver/linux/flutter/generated_plugins.cmake b/siro_driver/linux/flutter/generated_plugins.cmake index 9cbdb187..c759b641 100644 --- a/siro_driver/linux/flutter/generated_plugins.cmake +++ b/siro_driver/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_linux file_selector_linux flutter_secure_storage_linux flutter_webrtc diff --git a/siro_driver/macos/Flutter/GeneratedPluginRegistrant.swift b/siro_driver/macos/Flutter/GeneratedPluginRegistrant.swift index 5893e61e..3e8bc560 100644 --- a/siro_driver/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/siro_driver/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import audio_session +import audioplayers_darwin import battery_plus import connectivity_plus import device_info_plus @@ -24,6 +25,7 @@ import geolocator_apple import just_audio import local_auth_darwin import location +import mobile_scanner import package_info_plus import record_macos import share_plus @@ -36,6 +38,7 @@ import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) @@ -54,6 +57,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) LocationPlugin.register(with: registry.registrar(forPlugin: "LocationPlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/siro_driver/pubspec.lock b/siro_driver/pubspec.lock index 4f7ea98f..31da4667 100644 --- a/siro_driver/pubspec.lock +++ b/siro_driver/pubspec.lock @@ -73,6 +73,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.3" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: f16640453cc47487b7de72a2b28d37c7df1ac97999849f4a46d92b1d2b0f093d + url: "https://pub.dev" + source: hosted + version: "6.7.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91 + url: "https://pub.dev" + source: hosted + version: "6.4.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" + url: "https://pub.dev" + source: hosted + version: "7.1.1" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "24a6f258062bd7da8cb2157e83fccb9816a08dd306cbaaa24f9813d071470545" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "95f875a96c88c3dbbcb608d4f8288e300b0113d256a81d0b3197fcc18f0dc91a" + url: "https://pub.dev" + source: hosted + version: "4.3.1" battery_plus: dependency: "direct main" description: @@ -1492,6 +1548,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173" + url: "https://pub.dev" + source: hosted + version: "6.0.11" nested: dependency: transitive description: diff --git a/siro_driver/pubspec.yaml b/siro_driver/pubspec.yaml index 018e4dc5..ba95cd32 100644 --- a/siro_driver/pubspec.yaml +++ b/siro_driver/pubspec.yaml @@ -58,6 +58,8 @@ dependencies: flutter_tts: ^4.0.2 geolocator: ^14.0.2 image_cropper: ^12.1.1 + mobile_scanner: ^6.0.4 + audioplayers: ^6.0.0 image_picker: ^1.0.4 internet_connection_checker: ^3.0.1 jailbreak_root_detection: ^1.1.5 diff --git a/siro_driver/windows/flutter/generated_plugin_registrant.cc b/siro_driver/windows/flutter/generated_plugin_registrant.cc index c87eaf60..c3dd1dd3 100644 --- a/siro_driver/windows/flutter/generated_plugin_registrant.cc +++ b/siro_driver/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -23,6 +24,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AudioplayersWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); BatteryPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( diff --git a/siro_driver/windows/flutter/generated_plugins.cmake b/siro_driver/windows/flutter/generated_plugins.cmake index 783f9bc9..5e294ce8 100644 --- a/siro_driver/windows/flutter/generated_plugins.cmake +++ b/siro_driver/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_windows battery_plus connectivity_plus file_selector_windows diff --git a/siro_rider/lib/controller/local/ar_jo.dart b/siro_rider/lib/controller/local/ar_jo.dart index 345277f8..91b950ef 100644 --- a/siro_rider/lib/controller/local/ar_jo.dart +++ b/siro_rider/lib/controller/local/ar_jo.dart @@ -1777,4 +1777,19 @@ final Map ar_jo = { "Top Up Wallet": "شحن المحفظة", "• Subscription renews automatically every 30 days\n• Cancel anytime from your wallet page\n• Amount is deducted from your in-app wallet": "• يتجدد الاشتراك تلقائياً كل 30 يوماً\n• يمكن الإلغاء في أي وقت من صفحة المحفظة\n• يُخصم المبلغ من محفظتك داخل التطبيق", "service_unavailable_area": "هذه الخدمة غير متوفرة حالياً في منطقتك", + "بطاقة الصعود (Boarding Pass)": "بطاقة الصعود (Boarding Pass)", + "بطاقة الصعود": "بطاقة الصعود", + "يرجى إبراز هذا الرمز للسائق عند الصعود للحافلة": "يرجى إبراز هذا الرمز للسائق عند الصعود للحافلة", + "مسافر موثق": "مسافر موثق", + "Now at": "الآن في", + "Scan": "مسح", + "Scan Passengers": "مسح الركاب", + "Manual Passenger List": "القائمة اليدوية", + "Manual List": "القائمة اليدوية", + "تم صعود الراكب بنجاح ✅": "تم صعود الراكب بنجاح ✅", + "فشل التحقق ❌": "فشل التحقق ❌", + "تم تسجيل حضور الراكب": "تم تسجيل حضور الراكب", + "فشل تسجيل الحضور": "فشل تسجيل الحضور", + "لا يوجد ركاب مسجلين في هذه المحطة": "لا يوجد ركاب مسجلين في هذه المحطة", + "حاضر": "حاضر", }; diff --git a/siro_rider/lib/views/transit/transit_map_page.dart b/siro_rider/lib/views/transit/transit_map_page.dart index cfe5b1a4..8cb0768f 100644 --- a/siro_rider/lib/views/transit/transit_map_page.dart +++ b/siro_rider/lib/views/transit/transit_map_page.dart @@ -8,6 +8,8 @@ import '../../constant/colors.dart'; import '../../constant/style.dart'; import '../../env/env.dart'; import '../../controller/transit/transit_controller.dart'; +import 'transit_qr_code_page.dart'; +import '../widgets/elevated_btn.dart'; // ── ألوان الصفحة ────────────────────────────────────────────────────────────── Color get _bg => @@ -191,7 +193,7 @@ class _TransitMapPageState extends State { shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.12), + color: Colors.black.withValues(alpha: 0.12), blurRadius: 10, offset: const Offset(0, 3)) ], @@ -211,7 +213,7 @@ class _TransitMapPageState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.08), + color: Colors.black.withValues(alpha: 0.08), blurRadius: 10, offset: const Offset(0, 3)) ], @@ -263,7 +265,7 @@ class _TransitMapPageState extends State { shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: _accent.withOpacity(0.4), + color: _accent.withValues(alpha: 0.4), blurRadius: 10, offset: const Offset(0, 3)) ], @@ -353,7 +355,7 @@ class _TransitMapPageState extends State { borderRadius: BorderRadius.circular(14), border: Border(right: BorderSide(color: color, width: 4)), boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 8) + BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 8) ], ), child: Text(text, @@ -390,7 +392,7 @@ class _InfoCard extends StatelessWidget { borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.12), + color: Colors.black.withValues(alpha: 0.12), blurRadius: 20, offset: const Offset(0, -4)) ], @@ -406,7 +408,7 @@ class _InfoCard extends StatelessWidget { width: 44, height: 44, decoration: BoxDecoration( - color: _accent.withOpacity(0.12), + color: _accent.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.directions_bus_rounded, @@ -476,7 +478,7 @@ class _InfoCard extends StatelessWidget { borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: currentStopSeq! / totalStops, - backgroundColor: _accent.withOpacity(0.12), + backgroundColor: _accent.withValues(alpha: 0.12), valueColor: const AlwaysStoppedAnimation(_accent), minHeight: 6, ), @@ -485,13 +487,28 @@ class _InfoCard extends StatelessWidget { const SizedBox(height: 12), + // ── زر "بطاقة الصعود" (Boarding Pass) ───────────────── + SizedBox( + width: double.infinity, + height: 55, + child: MyElevatedButton( + onPressed: () { + Get.to(() => const TransitQRCodePage()); + }, + kolor: _accent, + title: 'بطاقة الصعود (Boarding Pass)'.tr, + ), + ), + + const SizedBox(height: 8), + // ── زر "فاتك الباص؟" ───────────────────────── SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: onMissedBus, style: OutlinedButton.styleFrom( - side: BorderSide(color: _accent.withOpacity(0.5)), + side: BorderSide(color: _accent.withValues(alpha: 0.5)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 10), diff --git a/siro_rider/lib/views/transit/transit_qr_code_page.dart b/siro_rider/lib/views/transit/transit_qr_code_page.dart new file mode 100644 index 00000000..4366cf86 --- /dev/null +++ b/siro_rider/lib/views/transit/transit_qr_code_page.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +import '../../constant/colors.dart'; +import '../../constant/style.dart'; +import '../../constant/box_name.dart'; +import '../../main.dart'; + +class TransitQRCodePage extends StatelessWidget { + const TransitQRCodePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final String passengerId = box.read(BoxName.passengerID) ?? ''; + final String name = box.read(BoxName.name) ?? ''; + + // The payload for the QR Code + // You can later encrypt this or make it dynamic for security. + final String qrPayload = 'transit_board:$passengerId'; + + return Scaffold( + backgroundColor: AppColor.secondaryColor, + appBar: AppBar( + title: Text('بطاقة الصعود'.tr), + backgroundColor: AppColor.secondaryColor, + elevation: 0, + foregroundColor: AppColor.writeColor, + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'يرجى إبراز هذا الرمز للسائق عند الصعود للحافلة'.tr, + textAlign: TextAlign.center, + style: AppStyle.title.copyWith(color: AppColor.grayColor), + ), + const SizedBox(height: 30), + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 2, + ), + ], + ), + child: Column( + children: [ + QrImageView( + data: qrPayload, + version: QrVersions.auto, + size: 250.0, + foregroundColor: Colors.black, + ), + const SizedBox(height: 20), + Text( + name, + style: AppStyle.headTitle2, + ), + const SizedBox(height: 5), + Text( + 'مسافر موثق', + style: AppStyle.title.copyWith( + color: AppColor.greenColor, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/siro_rider/pubspec.lock b/siro_rider/pubspec.lock index ea58b2d8..c799a5dd 100644 --- a/siro_rider/pubspec.lock +++ b/siro_rider/pubspec.lock @@ -1572,6 +1572,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" quick_actions: dependency: "direct main" description: diff --git a/siro_rider/pubspec.yaml b/siro_rider/pubspec.yaml index 1a03ab29..9afc0de1 100644 --- a/siro_rider/pubspec.yaml +++ b/siro_rider/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: webview_flutter: ^4.9.0 webview_flutter_android: ^3.16.2 webview_flutter_wkwebview: ^3.14.0 + qr_flutter: ^4.1.0 just_audio: ^0.10.5 firebase_auth: ^6.1.4 device_info_plus: 12.3.0