Update: 2026-07-13 18:23:52

This commit is contained in:
Hamza-Ayed
2026-07-13 18:23:52 +03:00
parent 7bf6dc1be7
commit fd3f4a365a
23 changed files with 802 additions and 31 deletions
@@ -0,0 +1,87 @@
<?php
// backend/transit/cron_approaching_alerts.php
// يُنفذ كل دقيقة عبر Cron Job لإرسال تنبيهات اقتراب الباص من المحطات (أقل من 2 كم)
require_once __DIR__ . '/../core/bootstrap.php';
require_once __DIR__ . '/connect_transit.php';
// 1. جلب الرحلات النشطة حالياً
$stTrips = $transit_con->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";
+81
View File
@@ -0,0 +1,81 @@
<?php
// transit/trip/board_passenger.php — سائق الباص يسجل صعود راكب (عبر QR أو يدوي)
// POST: trip_id (from active trip), stop_id, passenger_id
require_once __DIR__ . '/../../transit/connect_app.php';
$stopId = (int)filterRequest('stop_id');
$passengerId = filterRequest('passenger_id');
if (!$stopId || !$passengerId) {
jsonError('Missing parameters', 400);
}
// 1. تحقق من السائق ورحلته الحالية
$stTrip = $transit_con->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]);
+42
View File
@@ -0,0 +1,42 @@
<?php
// transit/trip/stop_passengers.php — جلب قائمة ركاب المحطة (للسائق)
// POST: stop_id
require_once __DIR__ . '/../../transit/connect_app.php';
$stopId = (int)filterRequest('stop_id');
if (!$stopId) jsonError('Missing stop_id', 400);
// 1. تحقق من السائق ورحلته
$stTrip = $transit_con->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);
@@ -2693,4 +2693,19 @@ final Map<String, String> ar_jo = {
"Destination cleared!": "تم مسح الوجهة الشخصية!", "Destination cleared!": "تم مسح الوجهة الشخصية!",
"Move map to select destination": "حرك الخريطة لتحديد الوجهة الشخصية", "Move map to select destination": "حرك الخريطة لتحديد الوجهة الشخصية",
"service_unavailable_area": "هذه الخدمة غير متوفرة حالياً في منطقتك", "service_unavailable_area": "هذه الخدمة غير متوفرة حالياً في منطقتك",
"بطاقة الصعود (Boarding Pass)": "بطاقة الصعود (Boarding Pass)",
"بطاقة الصعود": "بطاقة الصعود",
"يرجى إبراز هذا الرمز للسائق عند الصعود للحافلة": "يرجى إبراز هذا الرمز للسائق عند الصعود للحافلة",
"مسافر موثق": "مسافر موثق",
"Now at": "الآن في",
"Scan": "مسح",
"Scan Passengers": "مسح الركاب",
"Manual Passenger List": "القائمة اليدوية",
"Manual List": "القائمة اليدوية",
"تم صعود الراكب بنجاح ✅": "تم صعود الراكب بنجاح ✅",
"فشل التحقق ❌": "فشل التحقق ❌",
"تم تسجيل حضور الراكب": "تم تسجيل حضور الراكب",
"فشل تسجيل الحضور": "فشل تسجيل الحضور",
"لا يوجد ركاب مسجلين في هذه المحطة": "لا يوجد ركاب مسجلين في هذه المحطة",
"حاضر": "حاضر",
}; };
+16
View File
@@ -1603,6 +1603,22 @@ final Map<String, String> en = {
"Submit Your Complaint": "Submit Your Complaint", "Submit Your Complaint": "Submit Your Complaint",
"Submit Your Question": "Submit Your Question", "Submit Your Question": "Submit Your Question",
"Submit a Complaint": "Submit a Complaint", "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", "Submit rating": "Submit rating",
"Success": "Success", "Success": "Success",
"Suez Canal Bank": "Suez Canal Bank", "Suez Canal Bank": "Suez Canal Bank",
@@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart' show LatLng; import 'package:intaleq_maps/intaleq_maps.dart' show LatLng;
import '../functions/location_controller.dart'; import '../functions/location_controller.dart';
import '../widgets/error_snakbar.dart';
import 'transit_driver_models.dart'; import 'transit_driver_models.dart';
import 'transit_driver_service.dart'; import 'transit_driver_service.dart';
@@ -117,7 +118,7 @@ class TransitDriverController extends GetxController {
} }
await fetchTodayTrips(); await fetchTodayTrips();
} else { } else {
Get.snackbar('مواصلاتي', res.message); mySnackbarError(res.message);
} }
isActionInProgress = false; isActionInProgress = false;
@@ -139,7 +140,7 @@ class TransitDriverController extends GetxController {
} }
await fetchTodayTrips(); await fetchTodayTrips();
} else { } else {
Get.snackbar('مواصلاتي', res.message); mySnackbarError(res.message);
} }
isActionInProgress = false; isActionInProgress = false;
@@ -154,10 +155,23 @@ class TransitDriverController extends GetxController {
delayMinutes: minutes, delayMinutes: minutes,
reason: reason, reason: reason,
); );
if (!res.success) Get.snackbar('مواصلاتي', res.message); if (!res.success) mySnackbarError(res.message);
return res.success; return res.success;
} }
Future<bool> boardPassenger(int stopId, String passengerId) async {
final res = await TransitDriverService.boardPassenger(stopId, passengerId);
return res.success;
}
Future<List<Map<String, dynamic>>> getStopPassengers(int stopId) async {
final res = await TransitDriverService.getStopPassengers(stopId);
if (res.success && res.data != null) {
return res.data!;
}
return [];
}
// ── كشف الوصول للمحطات (جيوفينس) ──────────────────────────── // ── كشف الوصول للمحطات (جيوفينس) ────────────────────────────
// تُستدعى من LocationController عند كل تحديث للموقع أثناء وضع الباص // تُستدعى من LocationController عند كل تحديث للموقع أثناء وضع الباص
void onLocationUpdate(LatLng pos) { void onLocationUpdate(LatLng pos) {
@@ -1,6 +1,7 @@
// transit_driver_models.dart — نماذج بيانات مواصلاتي (جهة سائق الباص) // transit_driver_models.dart — نماذج بيانات مواصلاتي (جهة سائق الباص)
class TransitStopInfo { class TransitStopInfo {
final int id;
final int sequence; final int sequence;
final String nameAr; final String nameAr;
final double latitude; final double latitude;
@@ -9,6 +10,7 @@ class TransitStopInfo {
final int? etaOffsetMin; final int? etaOffsetMin;
TransitStopInfo({ TransitStopInfo({
required this.id,
required this.sequence, required this.sequence,
required this.nameAr, required this.nameAr,
required this.latitude, required this.latitude,
@@ -18,6 +20,7 @@ class TransitStopInfo {
}); });
factory TransitStopInfo.fromJson(Map<String, dynamic> j) => TransitStopInfo( factory TransitStopInfo.fromJson(Map<String, dynamic> j) => TransitStopInfo(
id: int.tryParse(j['id']?.toString() ?? '0') ?? 0,
sequence: int.tryParse(j['sequence'].toString()) ?? 0, sequence: int.tryParse(j['sequence'].toString()) ?? 0,
nameAr: j['name_ar']?.toString() ?? '', nameAr: j['name_ar']?.toString() ?? '',
latitude: double.tryParse(j['latitude']?.toString() ?? '0') ?? 0, latitude: double.tryParse(j['latitude']?.toString() ?? '0') ?? 0,
@@ -108,4 +108,32 @@ class TransitDriverService {
} }
return TransitApiResult(false, null, _errMsg(res)); return TransitApiResult(false, null, _errMsg(res));
} }
static Future<TransitApiResult<bool>> 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<TransitApiResult<List<Map<String, dynamic>>>> 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<String, dynamic>.from(e)).toList();
return TransitApiResult(true, list, 'ok');
}
return TransitApiResult(false, [], _errMsg(res));
}
} }
@@ -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<ManualBoardingPage> createState() => _ManualBoardingPageState();
}
class _ManualBoardingPageState extends State<ManualBoardingPage> {
bool isLoading = true;
List<Map<String, dynamic>> passengers = [];
@override
void initState() {
super.initState();
_loadPassengers();
}
Future<void> _loadPassengers() async {
final c = Get.find<TransitDriverController>();
// 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<TransitDriverController>();
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),
),
),
),
);
},
),
);
}
}
@@ -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<QRScannerPage> createState() => _QRScannerPageState();
}
class _QRScannerPageState extends State<QRScannerPage> {
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<Barcode> 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<TransitDriverController>();
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,
),
),
),
],
),
);
}
}
@@ -8,6 +8,7 @@ import '../../controller/transit/transit_driver_controller.dart';
import '../../controller/transit/transit_driver_models.dart'; import '../../controller/transit/transit_driver_models.dart';
import '../widgets/elevated_btn.dart'; import '../widgets/elevated_btn.dart';
import '../widgets/my_scafold.dart'; import '../widgets/my_scafold.dart';
import 'qr_scanner_page.dart';
class TransitDriverHomePage extends StatelessWidget { class TransitDriverHomePage extends StatelessWidget {
const TransitDriverHomePage({super.key}); const TransitDriverHomePage({super.key});
@@ -194,7 +195,7 @@ class _ActivationPage extends StatelessWidget {
width: 88, width: 88,
height: 88, height: 88,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColor.accentColor.withOpacity(0.12), color: AppColor.accentColor.withValues(alpha: 0.12),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon(Icons.directions_bus_rounded, child: Icon(Icons.directions_bus_rounded,
@@ -236,24 +237,14 @@ class _ActivationPage extends StatelessWidget {
const SizedBox(height: 20), const SizedBox(height: 20),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
height: 55,
child: controller.isActivating child: controller.isActivating
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: ElevatedButton( : MyElevatedButton(
style: ElevatedButton.styleFrom( kolor: AppColor.accentColor,
backgroundColor: AppColor.accentColor,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
onPressed: () => onPressed: () =>
controller.activateWithToken(tokenCtrl.text), controller.activateWithToken(tokenCtrl.text),
child: Text( title: 'Activate'.tr,
'Activate'.tr,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold),
),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -284,15 +275,28 @@ class _CurrentStopBanner extends StatelessWidget {
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
color: AppColor.greenColor.withOpacity(0.12), color: AppColor.greenColor.withValues(alpha: 0.12),
child: Row( child: Row(
children: [ children: [
Icon(Icons.location_on_rounded, color: AppColor.greenColor, size: 18), Icon(Icons.location_on_rounded, color: AppColor.greenColor, size: 18),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Expanded(
'${'Now at'.tr}: ${stop.nameAr}', child: Text(
style: AppStyle.subtitle.copyWith( '${'Now at'.tr}: ${stop.nameAr}',
color: AppColor.greenColor, fontWeight: FontWeight.bold), 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,
),
), ),
], ],
), ),
@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <file_selector_linux/file_selector_plugin.h> #include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h> #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h> #include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
@@ -13,6 +14,9 @@
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { 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 = g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar); file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
@@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
file_selector_linux file_selector_linux
flutter_secure_storage_linux flutter_secure_storage_linux
flutter_webrtc flutter_webrtc
@@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation import Foundation
import audio_session import audio_session
import audioplayers_darwin
import battery_plus import battery_plus
import connectivity_plus import connectivity_plus
import device_info_plus import device_info_plus
@@ -24,6 +25,7 @@ import geolocator_apple
import just_audio import just_audio
import local_auth_darwin import local_auth_darwin
import location import location
import mobile_scanner
import package_info_plus import package_info_plus
import record_macos import record_macos
import share_plus import share_plus
@@ -36,6 +38,7 @@ import webview_flutter_wkwebview
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
@@ -54,6 +57,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
LocationPlugin.register(with: registry.registrar(forPlugin: "LocationPlugin")) LocationPlugin.register(with: registry.registrar(forPlugin: "LocationPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
+64
View File
@@ -73,6 +73,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.3" 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: battery_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1492,6 +1548,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" 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: nested:
dependency: transitive dependency: transitive
description: description:
+2
View File
@@ -58,6 +58,8 @@ dependencies:
flutter_tts: ^4.0.2 flutter_tts: ^4.0.2
geolocator: ^14.0.2 geolocator: ^14.0.2
image_cropper: ^12.1.1 image_cropper: ^12.1.1
mobile_scanner: ^6.0.4
audioplayers: ^6.0.0
image_picker: ^1.0.4 image_picker: ^1.0.4
internet_connection_checker: ^3.0.1 internet_connection_checker: ^3.0.1
jailbreak_root_detection: ^1.1.5 jailbreak_root_detection: ^1.1.5
@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <battery_plus/battery_plus_windows_plugin.h> #include <battery_plus/battery_plus_windows_plugin.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h> #include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
@@ -23,6 +24,8 @@
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
AudioplayersWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
BatteryPlusWindowsPluginRegisterWithRegistrar( BatteryPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar( ConnectivityPlusWindowsPluginRegisterWithRegistrar(
@@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
battery_plus battery_plus
connectivity_plus connectivity_plus
file_selector_windows file_selector_windows
@@ -1777,4 +1777,19 @@ final Map<String, String> ar_jo = {
"Top Up Wallet": "شحن المحفظة", "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• يُخصم المبلغ من محفظتك داخل التطبيق", "• 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": "هذه الخدمة غير متوفرة حالياً في منطقتك", "service_unavailable_area": "هذه الخدمة غير متوفرة حالياً في منطقتك",
"بطاقة الصعود (Boarding Pass)": "بطاقة الصعود (Boarding Pass)",
"بطاقة الصعود": "بطاقة الصعود",
"يرجى إبراز هذا الرمز للسائق عند الصعود للحافلة": "يرجى إبراز هذا الرمز للسائق عند الصعود للحافلة",
"مسافر موثق": "مسافر موثق",
"Now at": "الآن في",
"Scan": "مسح",
"Scan Passengers": "مسح الركاب",
"Manual Passenger List": "القائمة اليدوية",
"Manual List": "القائمة اليدوية",
"تم صعود الراكب بنجاح ✅": "تم صعود الراكب بنجاح ✅",
"فشل التحقق ❌": "فشل التحقق ❌",
"تم تسجيل حضور الراكب": "تم تسجيل حضور الراكب",
"فشل تسجيل الحضور": "فشل تسجيل الحضور",
"لا يوجد ركاب مسجلين في هذه المحطة": "لا يوجد ركاب مسجلين في هذه المحطة",
"حاضر": "حاضر",
}; };
@@ -8,6 +8,8 @@ import '../../constant/colors.dart';
import '../../constant/style.dart'; import '../../constant/style.dart';
import '../../env/env.dart'; import '../../env/env.dart';
import '../../controller/transit/transit_controller.dart'; import '../../controller/transit/transit_controller.dart';
import 'transit_qr_code_page.dart';
import '../widgets/elevated_btn.dart';
// ── ألوان الصفحة ────────────────────────────────────────────────────────────── // ── ألوان الصفحة ──────────────────────────────────────────────────────────────
Color get _bg => Color get _bg =>
@@ -191,7 +193,7 @@ class _TransitMapPageState extends State<TransitMapPage> {
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.12), color: Colors.black.withValues(alpha: 0.12),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 3)) offset: const Offset(0, 3))
], ],
@@ -211,7 +213,7 @@ class _TransitMapPageState extends State<TransitMapPage> {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.08), color: Colors.black.withValues(alpha: 0.08),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 3)) offset: const Offset(0, 3))
], ],
@@ -263,7 +265,7 @@ class _TransitMapPageState extends State<TransitMapPage> {
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: _accent.withOpacity(0.4), color: _accent.withValues(alpha: 0.4),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 3)) offset: const Offset(0, 3))
], ],
@@ -353,7 +355,7 @@ class _TransitMapPageState extends State<TransitMapPage> {
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
border: Border(right: BorderSide(color: color, width: 4)), border: Border(right: BorderSide(color: color, width: 4)),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 8) BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 8)
], ],
), ),
child: Text(text, child: Text(text,
@@ -390,7 +392,7 @@ class _InfoCard extends StatelessWidget {
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.12), color: Colors.black.withValues(alpha: 0.12),
blurRadius: 20, blurRadius: 20,
offset: const Offset(0, -4)) offset: const Offset(0, -4))
], ],
@@ -406,7 +408,7 @@ class _InfoCard extends StatelessWidget {
width: 44, width: 44,
height: 44, height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
color: _accent.withOpacity(0.12), color: _accent.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: const Icon(Icons.directions_bus_rounded, child: const Icon(Icons.directions_bus_rounded,
@@ -476,7 +478,7 @@ class _InfoCard extends StatelessWidget {
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: currentStopSeq! / totalStops, value: currentStopSeq! / totalStops,
backgroundColor: _accent.withOpacity(0.12), backgroundColor: _accent.withValues(alpha: 0.12),
valueColor: const AlwaysStoppedAnimation<Color>(_accent), valueColor: const AlwaysStoppedAnimation<Color>(_accent),
minHeight: 6, minHeight: 6,
), ),
@@ -485,13 +487,28 @@ class _InfoCard extends StatelessWidget {
const SizedBox(height: 12), 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( SizedBox(
width: double.infinity, width: double.infinity,
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: onMissedBus, onPressed: onMissedBus,
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: BorderSide(color: _accent.withOpacity(0.5)), side: BorderSide(color: _accent.withValues(alpha: 0.5)),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)), borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10),
@@ -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,
),
),
],
),
),
],
),
),
);
}
}
+16
View File
@@ -1572,6 +1572,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" 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: quick_actions:
dependency: "direct main" dependency: "direct main"
description: description:
+1
View File
@@ -58,6 +58,7 @@ dependencies:
webview_flutter: ^4.9.0 webview_flutter: ^4.9.0
webview_flutter_android: ^3.16.2 webview_flutter_android: ^3.16.2
webview_flutter_wkwebview: ^3.14.0 webview_flutter_wkwebview: ^3.14.0
qr_flutter: ^4.1.0
just_audio: ^0.10.5 just_audio: ^0.10.5
firebase_auth: ^6.1.4 firebase_auth: ^6.1.4
device_info_plus: 12.3.0 device_info_plus: 12.3.0