Update: 2026-08-07 14:20:27

This commit is contained in:
Hamza-Ayed
2026-08-07 14:20:27 +03:00
parent 9e186f9ff3
commit b2a95c53aa
13 changed files with 343 additions and 58 deletions
+7 -1
View File
@@ -11,9 +11,14 @@ if (empty($passengerId)) {
jsonError('Unauthorized', 401); jsonError('Unauthorized', 401);
} }
$scope = filterRequest('scope') === 'all' ? 'all' : 'upcoming'; $scope = filterRequest('scope') ?: 'upcoming';
try { try {
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 = ?"; $sql = "SELECT * FROM scheduled_rides WHERE passenger_id = ?";
if ($scope === 'upcoming') { if ($scope === 'upcoming') {
$sql .= " AND status = 'scheduled' AND scheduled_at >= NOW()"; $sql .= " AND status = 'scheduled' AND scheduled_at >= NOW()";
@@ -22,6 +27,7 @@ try {
$stmt = $con->prepare($sql); $stmt = $con->prepare($sql);
$stmt->execute([$passengerId]); $stmt->execute([$passengerId]);
}
jsonSuccess(['bookings' => $stmt->fetchAll(PDO::FETCH_ASSOC)], 'ok'); jsonSuccess(['bookings' => $stmt->fetchAll(PDO::FETCH_ASSOC)], 'ok');
+3
View File
@@ -678,4 +678,7 @@ class AppLink {
if (clean.startsWith('01') || clean.startsWith('1')) return 'Egypt'; if (clean.startsWith('01') || clean.startsWith('1')) return 'Egypt';
return ''; return '';
} }
// ================= Scheduled Rides =================
static String get scheduledList => "$server/ride/scheduled/list.php";
} }
@@ -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 = <Map<String, dynamic>>[].obs;
final isLoading = false.obs;
@override
void onInit() {
super.onInit();
fetch();
}
Future<void> 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<String, dynamic>.from(e as Map)).toList());
} catch (e) {
mySnackbarError('Failed to fetch scheduled rides'.tr);
} finally {
isLoading.value = false;
}
}
Map<String, dynamic>? _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<String, dynamic>.from(data) : <String, dynamic>{};
}
}
+6 -1
View File
@@ -942,5 +942,10 @@
"Thu": "خميس", "Thu": "خميس",
"Fri": "جمعة", "Fri": "جمعة",
"Sat": "سبت", "Sat": "سبت",
"Sun": "أحد" "Sun": "أحد",
"Upcoming Scheduled Rides": "الرحلات المجدولة القادمة",
"You have no upcoming scheduled rides": "ليس لديك رحلات مجدولة قادمة",
"Starting Point": "نقطة الانطلاق",
"Destination": "وجهة الوصول",
"Failed to fetch scheduled rides": "تعذّر جلب الرحلات المجدولة"
} }
+6 -1
View File
@@ -900,5 +900,10 @@
"Thu": "Thu", "Thu": "Thu",
"Fri": "Fri", "Fri": "Fri",
"Sat": "Sat", "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"
} }
@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.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/home/statistics/statistics_dashboard.dart';
import 'package:siro_driver/views/gamification/challenges_page.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/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/home/journal/schedule_page.dart';
import 'package:siro_driver/views/notification/available_rides_page.dart'; import 'package:siro_driver/views/notification/available_rides_page.dart';
import 'package:siro_driver/views/auth/captin/logout_captain.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 'package:url_launcher/url_launcher.dart';
import '../../../../constant/colors.dart'; import '../../../../constant/colors.dart';
import '../About Us/video_page.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 '../../../food_delivery/food_delivery_home_page.dart';
import '../../journal/driver_scheduled_rides_page.dart';
// 1. إنشاء Class لتعريف بيانات كل عنصر في القائمة // 1. إنشاء Class لتعريف بيانات كل عنصر في القائمة
class DrawerItem { class DrawerItem {
@@ -87,6 +82,11 @@ class AppDrawer extends StatelessWidget {
icon: Icons.calendar_today_rounded, icon: Icons.calendar_today_rounded,
color: Colors.teal, color: Colors.teal,
onTap: () => Get.to(() => SchedulePage())), onTap: () => Get.to(() => SchedulePage())),
DrawerItem(
title: 'الرحلات المجدولة القادمة'.tr, // or 'Reserved Rides'
icon: Icons.edit_calendar_rounded,
color: Colors.orangeAccent,
onTap: () => Get.to(() => const DriverScheduledRidesPage())),
DrawerItem( DrawerItem(
title: 'Leaderboard'.tr, title: 'Leaderboard'.tr,
icon: Icons.leaderboard_rounded, icon: Icons.leaderboard_rounded,
@@ -326,8 +326,7 @@ class UserHeader extends StatelessWidget {
fontSize: 18, fontSize: 18,
shadows: [Shadow(blurRadius: 2, color: Colors.black26)]), shadows: [Shadow(blurRadius: 2, color: Colors.black26)]),
), ),
accountEmail: accountEmail: box.read(BoxName.emailDriver).toString().contains('siroapp')
box.read(BoxName.emailDriver).toString().contains('siroapp')
? Text('Your email not updated yet'.tr) ? Text('Your email not updated yet'.tr)
: Text(box.read(BoxName.emailDriver)), : Text(box.read(BoxName.emailDriver)),
currentAccountPicture: GetBuilder<ImageController>( currentAccountPicture: GetBuilder<ImageController>(
@@ -339,7 +338,8 @@ class UserHeader extends StatelessWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2), border: Border.all(color: Colors.white, width: 2),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 5) BoxShadow(
color: Colors.black.withValues(alpha: 0.3), blurRadius: 5)
], ],
), ),
child: controller.isloading child: controller.isloading
@@ -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<String, dynamic> 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,
),
),
],
),
],
),
),
);
}
}
@@ -808,25 +808,85 @@ class RideLifecycleController extends GetxController {
/// ‏خطأ تجربة سيئة، ومنعها من الأساس أوضح. /// ‏خطأ تجربة سيئة، ومنعها من الأساس أوضح.
Future<DateTime?> _pickScheduleTime(ScheduledRidesController ctrl) async { Future<DateTime?> _pickScheduleTime(ScheduledRidesController ctrl) async {
final now = DateTime.now(); final now = DateTime.now();
DateTime tempPickedDate = ctrl.earliest;
final date = await showDatePicker( final picked = await showModalBottomSheet<DateTime>(
context: Get.context!, context: Get.context!,
initialDate: ctrl.earliest, backgroundColor: Colors.transparent,
firstDate: now, builder: (BuildContext context) {
lastDate: ctrl.latest, final isDark = Get.isDarkMode;
helpText: 'موعد الرحلة'.tr, final bgColor = isDark ? const Color(0xFF1E1E1E) : Colors.white;
); final primaryColor = AppColor.primaryColor;
if (date == null) return null;
final time = await showTimePicker( return Container(
context: Get.context!, height: 320,
initialTime: TimeOfDay.fromDateTime(ctrl.earliest), decoration: BoxDecoration(
helpText: 'وقت الانطلاق'.tr, 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 = if (picked == null) return null;
DateTime(date.year, date.month, date.day, time.hour, time.minute);
// ‏منتقي التاريخ يحصر اليوم لا الساعة، فقد يختار الراكب يوماً صالحاً // ‏منتقي التاريخ يحصر اليوم لا الساعة، فقد يختار الراكب يوماً صالحاً
// ‏بساعة ماضية أو قريبة جداً. // ‏بساعة ماضية أو قريبة جداً.
@@ -1801,4 +1801,7 @@ final Map<String, String> ar_eg = {
"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": "هذه الخدمة غير متوفرة حالياً في منطقتك",
"Confirm": "تأكيد",
"Cancel": "إلغاء",
"Schedule for Later": "احجزها لوقت لاحق"
}; };
@@ -1815,4 +1815,7 @@ final Map<String, String> ar_jo = {
"فشل تسجيل الحضور": "فشل تسجيل الحضور", "فشل تسجيل الحضور": "فشل تسجيل الحضور",
"لا يوجد ركاب مسجلين في هذه المحطة": "لا يوجد ركاب مسجلين في هذه المحطة", "لا يوجد ركاب مسجلين في هذه المحطة": "لا يوجد ركاب مسجلين في هذه المحطة",
"حاضر": "حاضر", "حاضر": "حاضر",
"Confirm": "تأكيد",
"Cancel": "إلغاء",
"Schedule for Later": "احجزها لوقت لاحق"
}; };
@@ -1801,4 +1801,7 @@ final Map<String, String> ar_sy = {
"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": "هذه الخدمة غير متوفرة حالياً في منطقتك",
"Confirm": "تأكيد",
"Cancel": "إلغاء",
"Schedule for Later": "احجزها لوقت لاحق"
}; };
@@ -120,9 +120,11 @@ class CashConfirmPageShown extends StatelessWidget {
} }
// في كل الحالات الأخرى (كاش، أو محفظة برصيد كافٍ) // في كل الحالات الأخرى (كاش، أو محفظة برصيد كافٍ)
else { else {
return Column( return Row(
children: [ children: [
MyElevatedButton( Expanded(
flex: 1,
child: MyElevatedButton(
title: 'Confirm & Find a Ride'.tr, title: 'Confirm & Find a Ride'.tr,
onPressed: () { onPressed: () {
// --- نفس منطقك القديم بالضبط --- // --- نفس منطقك القديم بالضبط ---
@@ -132,22 +134,35 @@ class CashConfirmPageShown extends StatelessWidget {
// controller.update(); // controller.update();
}, },
), ),
const SizedBox(height: 6), ),
const SizedBox(width: 8),
// ‏مسار موازٍ لا بديل: الحجز لا يفتح نافذة بحث ولا // ‏مسار موازٍ لا بديل: الحجز لا يفتح نافذة بحث ولا
// ‏ينتظر سائقاً — يسجّل موعداً ويتولّاه الخادم. // ‏ينتظر سائقاً — يسجّل موعداً ويتولّاه الخادم.
// ‏ثانوي بصرياً عمداً: الرحلة الفورية تبقى الافتراض. // ‏ثانوي بصرياً عمداً: الرحلة الفورية تبقى الافتراض.
TextButton.icon( Expanded(
flex: 1,
child: SizedBox(
height: 52,
child: TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 4),
),
onPressed: () { onPressed: () {
controller.changeCashConfirmPageShown(); controller.changeCashConfirmPageShown();
controller.scheduleRideForLater(); controller.scheduleRideForLater();
}, },
icon: Icon(Icons.schedule, icon: Icon(Icons.schedule,
size: 18, color: AppColor.primaryColor), size: 18, color: AppColor.primaryColor),
label: Text( label: FittedBox(
'احجزها لوقت لاحق'.tr, child: Text(
'Schedule for Later'.tr,
style: TextStyle( style: TextStyle(
color: AppColor.primaryColor, color: AppColor.primaryColor,
fontWeight: FontWeight.w600), fontWeight: FontWeight.bold,
fontSize: 14),
),
),
),
), ),
), ),
], ],
@@ -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/my_wallet/passenger_wallet.dart';
import 'package:siro_rider/views/home/profile/complaint_page.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/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:siro_rider/views/home/profile/promos_passenger_page.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -174,6 +175,11 @@ class MapMenuWidget extends StatelessWidget {
icon: Icons.history_rounded, icon: Icons.history_rounded,
onTap: () => Get.to(() => const OrderHistory()), onTap: () => Get.to(() => const OrderHistory()),
), ),
MenuListItem(
title: 'Scheduled Rides'.tr,
icon: Icons.schedule_rounded,
onTap: () => Get.to(() => const ScheduledRidesPage()),
),
MenuListItem( MenuListItem(
title: 'Promos'.tr, title: 'Promos'.tr,
icon: Icons.local_offer_outlined, icon: Icons.local_offer_outlined,