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,