diff --git a/backend/ride/scheduled/list.php b/backend/ride/scheduled/list.php index 34807a0b..4e39b1ac 100644 --- a/backend/ride/scheduled/list.php +++ b/backend/ride/scheduled/list.php @@ -11,17 +11,23 @@ if (empty($passengerId)) { jsonError('Unauthorized', 401); } -$scope = filterRequest('scope') === 'all' ? 'all' : 'upcoming'; +$scope = filterRequest('scope') ?: 'upcoming'; try { - $sql = "SELECT * FROM scheduled_rides WHERE passenger_id = ?"; - if ($scope === 'upcoming') { - $sql .= " AND status = 'scheduled' AND scheduled_at >= NOW()"; + if ($scope === 'upcoming_driver') { + $sql = "SELECT * FROM scheduled_rides WHERE driver_id = ? AND scheduled_at >= NOW() ORDER BY scheduled_at ASC LIMIT 50"; + $stmt = $con->prepare($sql); + $stmt->execute([$passengerId]); // $passengerId here is actually the user_id (driver's ID) + } else { + $sql = "SELECT * FROM scheduled_rides WHERE passenger_id = ?"; + if ($scope === 'upcoming') { + $sql .= " AND status = 'scheduled' AND scheduled_at >= NOW()"; + } + $sql .= " ORDER BY scheduled_at ASC LIMIT 50"; + + $stmt = $con->prepare($sql); + $stmt->execute([$passengerId]); } - $sql .= " ORDER BY scheduled_at ASC LIMIT 50"; - - $stmt = $con->prepare($sql); - $stmt->execute([$passengerId]); jsonSuccess(['bookings' => $stmt->fetchAll(PDO::FETCH_ASSOC)], 'ok'); diff --git a/siro_driver/lib/constant/links.dart b/siro_driver/lib/constant/links.dart index 4484169e..31265eef 100755 --- a/siro_driver/lib/constant/links.dart +++ b/siro_driver/lib/constant/links.dart @@ -678,4 +678,7 @@ class AppLink { if (clean.startsWith('01') || clean.startsWith('1')) return 'Egypt'; return ''; } + + // ================= Scheduled Rides ================= + static String get scheduledList => "$server/ride/scheduled/list.php"; } diff --git a/siro_driver/lib/controller/scheduled/driver_scheduled_rides_controller.dart b/siro_driver/lib/controller/scheduled/driver_scheduled_rides_controller.dart new file mode 100644 index 00000000..c0926499 --- /dev/null +++ b/siro_driver/lib/controller/scheduled/driver_scheduled_rides_controller.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; +import 'package:get/get.dart'; +import '../../constant/links.dart'; +import '../../views/widgets/error_snakbar.dart'; +import '../functions/crud.dart'; + +class DriverScheduledRidesController extends GetxController { + final CRUD _crud = CRUD(); + + final bookings = >[].obs; + final isLoading = false.obs; + + @override + void onInit() { + super.onInit(); + fetch(); + } + + Future fetch() async { + isLoading.value = true; + try { + final res = await _crud.post( + link: AppLink.scheduledList, + payload: {'scope': 'upcoming_driver'}, + ); + final data = _extract(res); + final list = (data?['bookings'] as List?) ?? []; + bookings.assignAll( + list.map((e) => Map.from(e as Map)).toList()); + } catch (e) { + mySnackbarError('Failed to fetch scheduled rides'.tr); + } finally { + isLoading.value = false; + } + } + + Map? _extract(dynamic res) { + final decoded = res is String ? jsonDecode(res) : res; + if (decoded is! Map) { + return null; + } + if (decoded['status'] != 'success') { + return null; + } + final data = decoded['message'] ?? decoded['data']; + return data is Map ? Map.from(data) : {}; + } +} diff --git a/siro_driver/lib/translations_ar.json b/siro_driver/lib/translations_ar.json index 85487231..2ca0883b 100644 --- a/siro_driver/lib/translations_ar.json +++ b/siro_driver/lib/translations_ar.json @@ -942,5 +942,10 @@ "Thu": "خميس", "Fri": "جمعة", "Sat": "سبت", - "Sun": "أحد" + "Sun": "أحد", + "Upcoming Scheduled Rides": "الرحلات المجدولة القادمة", + "You have no upcoming scheduled rides": "ليس لديك رحلات مجدولة قادمة", + "Starting Point": "نقطة الانطلاق", + "Destination": "وجهة الوصول", + "Failed to fetch scheduled rides": "تعذّر جلب الرحلات المجدولة" } \ No newline at end of file diff --git a/siro_driver/lib/translations_en.json b/siro_driver/lib/translations_en.json index b660dc96..ff3865cc 100644 --- a/siro_driver/lib/translations_en.json +++ b/siro_driver/lib/translations_en.json @@ -900,5 +900,10 @@ "Thu": "Thu", "Fri": "Fri", "Sat": "Sat", - "Sun": "Sun" + "Sun": "Sun", + "Upcoming Scheduled Rides": "Upcoming Scheduled Rides", + "You have no upcoming scheduled rides": "You have no upcoming scheduled rides", + "Starting Point": "Starting Point", + "Destination": "Destination", + "Failed to fetch scheduled rides": "Failed to fetch scheduled rides" } \ No newline at end of file diff --git a/siro_driver/lib/views/home/Captin/home_captain/drawer_captain.dart b/siro_driver/lib/views/home/Captin/home_captain/drawer_captain.dart index 722af9d2..ba68d471 100755 --- a/siro_driver/lib/views/home/Captin/home_captain/drawer_captain.dart +++ b/siro_driver/lib/views/home/Captin/home_captain/drawer_captain.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -17,7 +15,6 @@ import 'package:siro_driver/views/auth/captin/invite_driver_screen.dart'; import 'package:siro_driver/views/home/statistics/statistics_dashboard.dart'; import 'package:siro_driver/views/gamification/challenges_page.dart'; import 'package:siro_driver/views/gamification/leaderboard_page.dart'; -import 'package:siro_driver/views/gamification/referral_center_page.dart'; import 'package:siro_driver/views/home/journal/schedule_page.dart'; import 'package:siro_driver/views/notification/available_rides_page.dart'; import 'package:siro_driver/views/auth/captin/logout_captain.dart'; @@ -30,10 +27,8 @@ import 'package:siro_driver/views/notification/notification_captain.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../../constant/colors.dart'; import '../About Us/video_page.dart'; -import '../assurance_health_page.dart'; -import '../maintain_center_page.dart'; -import '../../../transit/transit_driver_home_page.dart'; import '../../../food_delivery/food_delivery_home_page.dart'; +import '../../journal/driver_scheduled_rides_page.dart'; // 1. إنشاء Class لتعريف بيانات كل عنصر في القائمة class DrawerItem { @@ -87,6 +82,11 @@ class AppDrawer extends StatelessWidget { icon: Icons.calendar_today_rounded, color: Colors.teal, onTap: () => Get.to(() => SchedulePage())), + DrawerItem( + title: 'الرحلات المجدولة القادمة'.tr, // or 'Reserved Rides' + icon: Icons.edit_calendar_rounded, + color: Colors.orangeAccent, + onTap: () => Get.to(() => const DriverScheduledRidesPage())), DrawerItem( title: 'Leaderboard'.tr, icon: Icons.leaderboard_rounded, @@ -326,10 +326,9 @@ class UserHeader extends StatelessWidget { fontSize: 18, shadows: [Shadow(blurRadius: 2, color: Colors.black26)]), ), - accountEmail: - box.read(BoxName.emailDriver).toString().contains('siroapp') - ? Text('Your email not updated yet'.tr) - : Text(box.read(BoxName.emailDriver)), + accountEmail: box.read(BoxName.emailDriver).toString().contains('siroapp') + ? Text('Your email not updated yet'.tr) + : Text(box.read(BoxName.emailDriver)), currentAccountPicture: GetBuilder( builder: (controller) => Stack( clipBehavior: Clip.none, @@ -339,7 +338,8 @@ class UserHeader extends StatelessWidget { shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ - BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 5) + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), blurRadius: 5) ], ), child: controller.isloading diff --git a/siro_driver/lib/views/home/journal/driver_scheduled_rides_page.dart b/siro_driver/lib/views/home/journal/driver_scheduled_rides_page.dart new file mode 100644 index 00000000..cabbd104 --- /dev/null +++ b/siro_driver/lib/views/home/journal/driver_scheduled_rides_page.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../controller/scheduled/driver_scheduled_rides_controller.dart'; +import '../../../constant/colors.dart'; + +class DriverScheduledRidesPage extends StatelessWidget { + const DriverScheduledRidesPage({super.key}); + + @override + Widget build(BuildContext context) { + final controller = Get.put(DriverScheduledRidesController()); + + return Scaffold( + appBar: AppBar( + title: Text('Upcoming Scheduled Rides'.tr), + centerTitle: true, + ), + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (controller.bookings.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.calendar_month, + size: 80, color: Colors.grey.shade400), + const SizedBox(height: 16), + Text( + 'You have no upcoming scheduled rides'.tr, + style: const TextStyle(fontSize: 18, color: Colors.grey), + ), + ], + ), + ); + } + + return RefreshIndicator( + onRefresh: controller.fetch, + child: ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: controller.bookings.length, + itemBuilder: (context, index) { + final booking = controller.bookings[index]; + return _buildBookingCard(booking, context); + }, + ), + ); + }), + ); + } + + Widget _buildBookingCard(Map booking, BuildContext context) { + return Card( + margin: const EdgeInsets.only(bottom: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppColor.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Icon(Icons.access_time, + size: 16, color: AppColor.primaryColor), + const SizedBox(width: 6), + Text( + booking['scheduled_at'] ?? '', + style: const TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold), + ), + ], + ), + ), + Text( + '${booking['estimated_price']} ${'Currency'.tr}', + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 16), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + const Icon(Icons.my_location, color: Colors.green, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + booking['start_name'] ?? 'Starting Point'.tr, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon(Icons.location_on, color: Colors.red, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + booking['end_name'] ?? 'Destination'.tr, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart index a0176fa2..9f10acc4 100644 --- a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart +++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart @@ -808,25 +808,85 @@ class RideLifecycleController extends GetxController { /// ‏خطأ تجربة سيئة، ومنعها من الأساس أوضح. Future _pickScheduleTime(ScheduledRidesController ctrl) async { final now = DateTime.now(); + DateTime tempPickedDate = ctrl.earliest; - final date = await showDatePicker( + final picked = await showModalBottomSheet( context: Get.context!, - initialDate: ctrl.earliest, - firstDate: now, - lastDate: ctrl.latest, - helpText: 'موعد الرحلة'.tr, - ); - if (date == null) return null; + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + final isDark = Get.isDarkMode; + final bgColor = isDark ? const Color(0xFF1E1E1E) : Colors.white; + final primaryColor = AppColor.primaryColor; - final time = await showTimePicker( - context: Get.context!, - initialTime: TimeOfDay.fromDateTime(ctrl.earliest), - helpText: 'وقت الانطلاق'.tr, + return Container( + height: 320, + decoration: BoxDecoration( + color: bgColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Column( + children: [ + // Header + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: isDark ? Colors.white12 : Colors.black12, + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancel'.tr, + style: TextStyle(color: Colors.redAccent, fontSize: 16), + ), + ), + TextButton( + onPressed: () => Navigator.pop(context, tempPickedDate), + child: Text( + 'Confirm'.tr, + style: TextStyle( + color: primaryColor, + fontSize: 16, + fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + // Cupertino Picker + Expanded( + child: CupertinoTheme( + data: CupertinoThemeData( + brightness: isDark ? Brightness.dark : Brightness.light, + ), + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.dateAndTime, + initialDateTime: ctrl.earliest, + minimumDate: now, + maximumDate: ctrl.latest, + use24hFormat: false, + onDateTimeChanged: (DateTime newDateTime) { + tempPickedDate = newDateTime; + }, + ), + ), + ), + ], + ), + ); + }, ); - if (time == null) return null; - final picked = - DateTime(date.year, date.month, date.day, time.hour, time.minute); + if (picked == null) return null; // ‏منتقي التاريخ يحصر اليوم لا الساعة، فقد يختار الراكب يوماً صالحاً // ‏بساعة ماضية أو قريبة جداً. diff --git a/siro_rider/lib/controller/local/ar_eg.dart b/siro_rider/lib/controller/local/ar_eg.dart index ca876dd7..df4e811e 100644 --- a/siro_rider/lib/controller/local/ar_eg.dart +++ b/siro_rider/lib/controller/local/ar_eg.dart @@ -1801,4 +1801,7 @@ final Map ar_eg = { "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": "هذه الخدمة غير متوفرة حالياً في منطقتك", + "Confirm": "تأكيد", + "Cancel": "إلغاء", + "Schedule for Later": "احجزها لوقت لاحق" }; diff --git a/siro_rider/lib/controller/local/ar_jo.dart b/siro_rider/lib/controller/local/ar_jo.dart index c916ebba..ff825c9d 100644 --- a/siro_rider/lib/controller/local/ar_jo.dart +++ b/siro_rider/lib/controller/local/ar_jo.dart @@ -1815,4 +1815,7 @@ final Map ar_jo = { "فشل تسجيل الحضور": "فشل تسجيل الحضور", "لا يوجد ركاب مسجلين في هذه المحطة": "لا يوجد ركاب مسجلين في هذه المحطة", "حاضر": "حاضر", + "Confirm": "تأكيد", + "Cancel": "إلغاء", + "Schedule for Later": "احجزها لوقت لاحق" }; diff --git a/siro_rider/lib/controller/local/ar_sy.dart b/siro_rider/lib/controller/local/ar_sy.dart index 50368d5e..6c2486e0 100644 --- a/siro_rider/lib/controller/local/ar_sy.dart +++ b/siro_rider/lib/controller/local/ar_sy.dart @@ -1801,4 +1801,7 @@ final Map ar_sy = { "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": "هذه الخدمة غير متوفرة حالياً في منطقتك", + "Confirm": "تأكيد", + "Cancel": "إلغاء", + "Schedule for Later": "احجزها لوقت لاحق" }; diff --git a/siro_rider/lib/views/home/map_widget.dart/cash_confirm_bottom_page.dart b/siro_rider/lib/views/home/map_widget.dart/cash_confirm_bottom_page.dart index b40470d0..ab93d3fd 100644 --- a/siro_rider/lib/views/home/map_widget.dart/cash_confirm_bottom_page.dart +++ b/siro_rider/lib/views/home/map_widget.dart/cash_confirm_bottom_page.dart @@ -120,34 +120,49 @@ class CashConfirmPageShown extends StatelessWidget { } // في كل الحالات الأخرى (كاش، أو محفظة برصيد كافٍ) else { - return Column( + return Row( children: [ - MyElevatedButton( - title: 'Confirm & Find a Ride'.tr, - onPressed: () { - // --- نفس منطقك القديم بالضبط --- - controller.changeCashConfirmPageShown(); - // controller.isSearchingWindow = true; - controller.startSearchingForDriver(); - // controller.update(); - }, + Expanded( + flex: 1, + child: MyElevatedButton( + title: 'Confirm & Find a Ride'.tr, + onPressed: () { + // --- نفس منطقك القديم بالضبط --- + controller.changeCashConfirmPageShown(); + // controller.isSearchingWindow = true; + controller.startSearchingForDriver(); + // controller.update(); + }, + ), ), - const SizedBox(height: 6), + const SizedBox(width: 8), // ‏مسار موازٍ لا بديل: الحجز لا يفتح نافذة بحث ولا // ‏ينتظر سائقاً — يسجّل موعداً ويتولّاه الخادم. // ‏ثانوي بصرياً عمداً: الرحلة الفورية تبقى الافتراض. - TextButton.icon( - onPressed: () { - controller.changeCashConfirmPageShown(); - controller.scheduleRideForLater(); - }, - icon: Icon(Icons.schedule, - size: 18, color: AppColor.primaryColor), - label: Text( - 'احجزها لوقت لاحق'.tr, - style: TextStyle( - color: AppColor.primaryColor, - fontWeight: FontWeight.w600), + Expanded( + flex: 1, + child: SizedBox( + height: 52, + child: TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 4), + ), + onPressed: () { + controller.changeCashConfirmPageShown(); + controller.scheduleRideForLater(); + }, + icon: Icon(Icons.schedule, + size: 18, color: AppColor.primaryColor), + label: FittedBox( + child: Text( + 'Schedule for Later'.tr, + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: 14), + ), + ), + ), ), ), ], diff --git a/siro_rider/lib/views/home/map_widget.dart/map_menu_widget.dart b/siro_rider/lib/views/home/map_widget.dart/map_menu_widget.dart index 6972b72d..e00a595c 100644 --- a/siro_rider/lib/views/home/map_widget.dart/map_menu_widget.dart +++ b/siro_rider/lib/views/home/map_widget.dart/map_menu_widget.dart @@ -10,6 +10,7 @@ import 'package:siro_rider/constant/style.dart'; import 'package:siro_rider/views/home/my_wallet/passenger_wallet.dart'; import 'package:siro_rider/views/home/profile/complaint_page.dart'; import 'package:siro_rider/views/home/profile/order_history.dart'; +import 'package:siro_rider/views/scheduled/scheduled_rides_page.dart'; import 'package:siro_rider/views/home/profile/promos_passenger_page.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -174,6 +175,11 @@ class MapMenuWidget extends StatelessWidget { icon: Icons.history_rounded, onTap: () => Get.to(() => const OrderHistory()), ), + MenuListItem( + title: 'Scheduled Rides'.tr, + icon: Icons.schedule_rounded, + onTap: () => Get.to(() => const ScheduledRidesPage()), + ), MenuListItem( title: 'Promos'.tr, icon: Icons.local_offer_outlined,