diff --git a/backend/ride/scheduled/list.php b/backend/ride/scheduled/list.php
index 4e39b1ac..65d6238e 100644
--- a/backend/ride/scheduled/list.php
+++ b/backend/ride/scheduled/list.php
@@ -15,7 +15,23 @@ $scope = filterRequest('scope') ?: 'upcoming';
try {
if ($scope === 'upcoming_driver') {
- $sql = "SELECT * FROM scheduled_rides WHERE driver_id = ? AND scheduled_at >= NOW() ORDER BY scheduled_at ASC LIMIT 50";
+ // ═══════════════════════════════════════════════════════════════
+ // لا عمود `driver_id` في scheduled_rides — ولا يصحّ أن يوجد:
+ // الحجز يُنشئه الراكب، والسائق لا يُسند إلا لحظة توليد الرحلة
+ // الفعلية عبر add_ride.php. الاستعلام السابق كان يسأل عن عمود
+ // غير موجود فيسقط بـ 500 على كل فتح للشاشة.
+ //
+ // الربط الصحيح عبر الرحلة المولّدة: حجوزات أُسندت لهذا السائق
+ // ولم يحن موعدها بعد.
+ // ═══════════════════════════════════════════════════════════════
+ $sql = "SELECT sr.*
+ FROM scheduled_rides sr
+ JOIN ride r ON r.id = sr.ride_id
+ WHERE r.driver_id = ?
+ AND sr.scheduled_at >= NOW()
+ AND sr.status NOT IN ('cancelled', 'expired')
+ ORDER BY sr.scheduled_at ASC
+ LIMIT 50";
$stmt = $con->prepare($sql);
$stmt->execute([$passengerId]); // $passengerId here is actually the user_id (driver's ID)
} else {
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 65bd4cca..858deaad 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -41,6 +41,7 @@ services:
- ../payment_server/v2:/var/www/v2
- ../loction_server:/var/www/loction_server
- ./php/opcache.ini:/usr/local/etc/php/conf.d/zz-opcache.ini:ro
+ - ./php/php-prod.ini:/usr/local/etc/php/conf.d/zz-prod.ini:ro
- ./php/www-pool.conf:/usr/local/etc/php-fpm.d/zz-pool.conf:ro
- ./keys:/keys:ro
env_file: .env
@@ -68,6 +69,7 @@ services:
- ../backend/vendor:/var/www/backend/vendor:ro
- ../backend/transit:/var/www/backend/transit
- ./php/opcache.ini:/usr/local/etc/php/conf.d/zz-opcache.ini:ro
+ - ./php/php-prod.ini:/usr/local/etc/php/conf.d/zz-prod.ini:ro
- ./php/www-pool.transit.conf:/usr/local/etc/php-fpm.d/zz-pool.conf:ro
- ./keys:/keys:ro
env_file: .env
@@ -95,6 +97,7 @@ services:
- ../backend/vendor:/var/www/backend/vendor:ro
- ../backend/food:/var/www/backend/food
- ./php/opcache.ini:/usr/local/etc/php/conf.d/zz-opcache.ini:ro
+ - ./php/php-prod.ini:/usr/local/etc/php/conf.d/zz-prod.ini:ro
- ./php/food-pool.conf:/usr/local/etc/php-fpm.d/zz-pool.conf:ro
- ./keys:/keys:ro
env_file: .env
diff --git a/docker/php/php-prod.ini b/docker/php/php-prod.ini
new file mode 100644
index 00000000..e6fb0980
--- /dev/null
+++ b/docker/php/php-prod.ini
@@ -0,0 +1,37 @@
+; ============================================================
+; إعدادات PHP للإنتاج — تُركَّب على كل حاويات fpm
+; ============================================================
+;
+; صورة php الرسمية لا تشحن php.ini إطلاقاً (تترك php.ini-production
+; و php.ini-development جانباً بلا تفعيل)، فتسري قيم PHP المدمجة:
+; display_errors مُفعّل. أي أن كل تحذير أو إشعار يُطبع **داخل جسم
+; الردّ** قبل الـ JSON.
+;
+; وهذا ليس تنظيراً: شاشة رصيد الكابتن كانت تسقط بـ
+; "FormatException: Unexpected character (at character 1)
"
+; لأن getAllPayment.php أخرج تحذيراً قبل الـ JSON فصار الردّ غير قابل
+; للتحليل. العطل يبدو في التطبيق بينما مصدره سطر تحذير في PHP.
+;
+; والأسوأ أمنياً: نص التحذير يحمل المسار المطلق للملف وأحياناً جزءاً من
+; الاستعلام — يُسلَّم للعميل مباشرة.
+;
+; القاعدة: تُسجَّل الأخطاء ولا تُعرض. www-pool.conf يوجّه error_log إلى
+; مخرج الحاوية، فتُقرأ بـ `docker compose logs php`.
+
+display_errors = Off
+display_startup_errors = Off
+log_errors = On
+error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
+
+; لا تكشف نسخة PHP في ترويسة X-Powered-By.
+expose_php = Off
+
+; رفع صور المستندات (رخصة، هوية) — متوافق مع client_max_body_size 25m
+; في nginx. تركهما على الافتراضي (2m) كان يقطع رفع صور الكاميرا بصمت.
+upload_max_filesize = 25M
+post_max_size = 25M
+
+max_execution_time = 60
+memory_limit = 256M
+
+date.timezone = UTC
diff --git a/siro_driver/lib/controller/auth/captin/invit_controller.dart b/siro_driver/lib/controller/auth/captin/invit_controller.dart
index de117053..f6f3dc74 100755
--- a/siro_driver/lib/controller/auth/captin/invit_controller.dart
+++ b/siro_driver/lib/controller/auth/captin/invit_controller.dart
@@ -66,62 +66,53 @@ https://siromove.com/invite.php?code=$couponCode&app=rider
@override
void onInit() {
super.onInit();
- // **MODIFIED**: Sync contacts automatically on controller initialization.
- syncContactsToServerOnce();
+ // لا نقرأ دفتر الهاتف هنا. القراءة تحدث فقط حين يطلب الكابتن
+ // اختيار جهة اتصال — انظر loadDeviceContacts أدناه.
// fetchDriverStats();
}
- // --- NEW LOGIC: ONE-TIME CONTACTS SYNC ---
+ // --- قراءة جهات الاتصال محلياً، بطلب المستخدم وحده ---
- /// **NEW**: Syncs all phone contacts to the server, but only runs once per user.
- Future syncContactsToServerOnce() async {
- final String syncFlagKey = 'contactsSynced_${box.read(BoxName.driverID)}';
-
- // 1. Check if contacts have already been synced for this user.
- if (box.read(syncFlagKey) == true) {
- print("Contacts have already been synced for this user.");
- return;
- }
+ /// كان هنا `syncContactsToServerOnce`: يقرأ دفتر هاتف الكابتن كاملاً
+ /// تلقائياً عند فتح شاشة الدعوة، ويرفع **كل** اسم ورقم إلى
+ /// `ride/egyptPhones/syrianAdd.php` — طلب لكل جهة اتصال.
+ ///
+ /// أُزيل الرفع بالكامل، ولثلاثة أسباب لا واحد:
+ ///
+ /// ١) سياسة Google Play: رفع دفتر العناوين جملةً يتطلب إفصاحاً بارزاً
+ /// وموافقة صريحة، وإذن النظام وحده لا يكفي. والتطبيق موقوف أصلاً.
+ ///
+ /// ٢) البيانات ليست بيانات الكابتن ليتبرّع بها: هي أسماء وأرقام أطراف
+ /// ثالثة لم تُستأذن ولا تعلم بوجود سيرو.
+ ///
+ /// ٣) لا ميزة تعتمد عليها. الدعوة تعمل بالمشاركة أو باختيار جهة واحدة
+ /// عمداً، وكلاهما لا يحتاج نسخة من الدفتر على السيرفر.
+ ///
+ /// ما بقي هنا قراءة محلية بحتة لعرض قائمة يختار منها الكابتن — لا
+ /// شيء يغادر الجهاز.
+ Future loadDeviceContacts() async {
+ if (contactMaps.isNotEmpty) return; // مُحمّلة سلفاً في هذه الجلسة
try {
- // 2. Request permission and fetch all contacts.
if (await FlutterContacts.requestPermission(readonly: true)) {
- // mySnackbarSuccess('Starting contacts sync in background...'.tr);
final List allContacts =
await FlutterContacts.getContacts(withProperties: true);
- // **FIX**: Assign fetched contacts to the class variable.
contacts = allContacts;
- contactMaps.value = contacts.map((contact) {
- return {
- 'name': contact.displayName,
- 'phones':
- contact.phones.map((phone) => phone.normalizedNumber).toList(),
- 'emails': contact.emails.map((email) => email.address).toList(),
- };
- }).toList();
+ contactMaps.value = allContacts
+ .where((c) => c.phones.isNotEmpty)
+ .map((contact) => {
+ 'name': contact.displayName,
+ 'phones': contact.phones
+ .map((phone) => phone.normalizedNumber)
+ .toList(),
+ 'emails':
+ contact.emails.map((email) => email.address).toList(),
+ })
+ .toList();
update();
-
- // 3. Loop through contacts and save them to the server.
- for (var contact in allContacts) {
- if (contact.phones.isNotEmpty) {
- // Use the normalized phone number for consistency.
- var phone = contact.phones.first.normalizedNumber;
- if (phone.isNotEmpty) {
- CRUD().post(link: AppLink.savePhonesSyria, payload: {
- "driverId": box.read(BoxName.driverID), // Associate with driver
- "name": contact.displayName ?? 'No Name',
- "phone": phone,
- });
- }
- }
- }
-
- // 4. After a successful sync, set the flag to prevent future syncs.
- await box.write(syncFlagKey, true);
- // mySnackbarSuccess('Contacts sync completed successfully!'.tr);
}
} catch (e) {
- // mySnackbarError('An error occurred during contact sync: $e'.tr);
+ // قراءة دفتر الهاتف ليست حرجة — الدعوة تعمل بالمشاركة أيضاً.
}
}
diff --git a/siro_driver/lib/controller/gamification/leaderboard_controller.dart b/siro_driver/lib/controller/gamification/leaderboard_controller.dart
index 468bdf2f..cdd1c69c 100644
--- a/siro_driver/lib/controller/gamification/leaderboard_controller.dart
+++ b/siro_driver/lib/controller/gamification/leaderboard_controller.dart
@@ -56,7 +56,7 @@ class LeaderboardController extends GetxController {
payload: {'type': 'trips'},
);
if (resTrips != null && resTrips != 'failure') {
- var data = jsonDecode(resTrips);
+ var data = _decode(resTrips);
if (data['message'] is List) {
tripLeaderboard = (data['message'] as List).map((e) => LeaderboardEntry(
driverId: e['driver_id'].toString(),
@@ -75,7 +75,7 @@ class LeaderboardController extends GetxController {
payload: {'type': 'earnings'},
);
if (resEarnings != null && resEarnings != 'failure') {
- var data = jsonDecode(resEarnings);
+ var data = _decode(resEarnings);
if (data['message'] is List) {
earningsLeaderboard = (data['message'] as List).map((e) => LeaderboardEntry(
driverId: e['driver_id'].toString(),
@@ -99,4 +99,17 @@ class LeaderboardController extends GetxController {
isLoading = false;
update();
}
+
+ /// CRUD().post تُرجع الردّ **مفكوكاً** أصلاً (Map)، بينما CRUD().get
+ /// تُرجع نصاً. تمرير الـ Map إلى jsonDecode كان يرمي
+ /// "type '_Map' is not a subtype of type 'String'"،
+ /// فتُبتلع لوحة الصدارة في catch وتظهر فارغة دائماً مهما صحّ الردّ.
+ Map _decode(dynamic res) {
+ if (res is Map) return Map.from(res);
+ if (res is String && res.isNotEmpty) {
+ final decoded = jsonDecode(res);
+ if (decoded is Map) return Map.from(decoded);
+ }
+ return {};
+ }
}
diff --git a/siro_driver/lib/views/auth/captin/invite_driver_screen.dart b/siro_driver/lib/views/auth/captin/invite_driver_screen.dart
index 209f62ca..79ec74b7 100755
--- a/siro_driver/lib/views/auth/captin/invite_driver_screen.dart
+++ b/siro_driver/lib/views/auth/captin/invite_driver_screen.dart
@@ -347,11 +347,7 @@ class InviteScreen extends StatelessWidget {
onPressed: () async {
await controller.pickContactFromNativeApp();
if (controller.contacts.isNotEmpty) {
- if (box.read(BoxName.isSavedPhones) == null) {
- // controller.savePhoneToServer();
- box.write(BoxName.isSavedPhones, true);
- }
- _showContactsDialog(Get.context!);
+ await _openContactsDialog(Get.context!);
}
},
),
@@ -731,6 +727,14 @@ class InviteScreen extends StatelessWidget {
}
*/
+ /// تُحمَّل جهات الاتصال هنا عند الطلب — كانت تُقرأ وتُرفع تلقائياً عند
+ /// فتح الشاشة. لا شيء يغادر الجهاز الآن.
+ Future _openContactsDialog(BuildContext context) async {
+ await controller.loadDeviceContacts();
+ if (!context.mounted) return;
+ _showContactsDialog(context);
+ }
+
void _showContactsDialog(BuildContext context) {
showCupertinoModalPopup(
context: context,
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 6cdc3016..eaa6874d 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
@@ -26,6 +26,7 @@ import 'package:siro_driver/views/home/profile/profile_captain.dart';
import 'package:siro_driver/views/notification/notification_captain.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../constant/colors.dart';
+import '../../../transit/transit_driver_home_page.dart';
import '../About Us/video_page.dart';
import '../../../food_delivery/food_delivery_home_page.dart';
import '../../journal/driver_scheduled_rides_page.dart';
@@ -53,11 +54,11 @@ class AppDrawer extends StatelessWidget {
// 2. تعريف بيانات القائمة بشكل مركزي ومنظم
final List drawerItems = [
- // DrawerItem(
- // title: 'Mawasalati'.tr,
- // icon: Icons.directions_bus_filled_rounded,
- // color: Colors.teal,
- // onTap: () => Get.to(() => const TransitDriverHomePage())),
+ DrawerItem(
+ title: 'Mawasalati'.tr,
+ icon: Icons.directions_bus_filled_rounded,
+ color: Colors.teal,
+ onTap: () => Get.to(() => const TransitDriverHomePage())),
DrawerItem(
title: 'Delivery'.tr,
icon: Icons.delivery_dining_rounded,
@@ -353,9 +354,8 @@ class UserHeader extends StatelessWidget {
: Builder(builder: (context) {
final photoUrl =
box.read(BoxName.driverPhotoUrl)?.toString() ?? '';
- final avatarImage = photoUrl.isNotEmpty
- ? NetworkImage(photoUrl)
- : null;
+ final avatarImage =
+ photoUrl.isNotEmpty ? NetworkImage(photoUrl) : null;
return CircleAvatar(
backgroundImage: avatarImage,