Update: 2026-08-08 14:26:43

This commit is contained in:
Hamza-Ayed
2026-08-08 14:26:44 +03:00
parent b97a1fd587
commit ef37752ceb
7 changed files with 123 additions and 59 deletions
+17 -1
View File
@@ -15,7 +15,23 @@ $scope = filterRequest('scope') ?: 'upcoming';
try { try {
if ($scope === 'upcoming_driver') { 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 = $con->prepare($sql);
$stmt->execute([$passengerId]); // $passengerId here is actually the user_id (driver's ID) $stmt->execute([$passengerId]); // $passengerId here is actually the user_id (driver's ID)
} else { } else {
+3
View File
@@ -41,6 +41,7 @@ services:
- ../payment_server/v2:/var/www/v2 - ../payment_server/v2:/var/www/v2
- ../loction_server:/var/www/loction_server - ../loction_server:/var/www/loction_server
- ./php/opcache.ini:/usr/local/etc/php/conf.d/zz-opcache.ini:ro - ./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 - ./php/www-pool.conf:/usr/local/etc/php-fpm.d/zz-pool.conf:ro
- ./keys:/keys:ro - ./keys:/keys:ro
env_file: .env env_file: .env
@@ -68,6 +69,7 @@ services:
- ../backend/vendor:/var/www/backend/vendor:ro - ../backend/vendor:/var/www/backend/vendor:ro
- ../backend/transit:/var/www/backend/transit - ../backend/transit:/var/www/backend/transit
- ./php/opcache.ini:/usr/local/etc/php/conf.d/zz-opcache.ini:ro - ./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 - ./php/www-pool.transit.conf:/usr/local/etc/php-fpm.d/zz-pool.conf:ro
- ./keys:/keys:ro - ./keys:/keys:ro
env_file: .env env_file: .env
@@ -95,6 +97,7 @@ services:
- ../backend/vendor:/var/www/backend/vendor:ro - ../backend/vendor:/var/www/backend/vendor:ro
- ../backend/food:/var/www/backend/food - ../backend/food:/var/www/backend/food
- ./php/opcache.ini:/usr/local/etc/php/conf.d/zz-opcache.ini:ro - ./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 - ./php/food-pool.conf:/usr/local/etc/php-fpm.d/zz-pool.conf:ro
- ./keys:/keys:ro - ./keys:/keys:ro
env_file: .env env_file: .env
+37
View File
@@ -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) <br />"
; ‏لأن 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
@@ -66,62 +66,53 @@ https://siromove.com/invite.php?code=$couponCode&app=rider
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// **MODIFIED**: Sync contacts automatically on controller initialization. // ‏لا نقرأ دفتر الهاتف هنا. القراءة تحدث فقط حين يطلب الكابتن
syncContactsToServerOnce(); // ‏اختيار جهة اتصال — انظر loadDeviceContacts أدناه.
// fetchDriverStats(); // fetchDriverStats();
} }
// --- NEW LOGIC: ONE-TIME CONTACTS SYNC --- // --- قراءة جهات الاتصال محلياً، بطلب المستخدم وحده ---
/// **NEW**: Syncs all phone contacts to the server, but only runs once per user. /// ‏كان هنا `syncContactsToServerOnce`: يقرأ دفتر هاتف الكابتن كاملاً
Future<void> syncContactsToServerOnce() async { /// ‏تلقائياً عند فتح شاشة الدعوة، ويرفع **كل** اسم ورقم إلى
final String syncFlagKey = 'contactsSynced_${box.read(BoxName.driverID)}'; /// ‏`ride/egyptPhones/syrianAdd.php` — طلب لكل جهة اتصال.
///
// 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."); /// ‏١) سياسة Google Play: رفع دفتر العناوين جملةً يتطلب إفصاحاً بارزاً
return; /// ‏وموافقة صريحة، وإذن النظام وحده لا يكفي. والتطبيق موقوف أصلاً.
} ///
/// ‏٢) البيانات ليست بيانات الكابتن ليتبرّع بها: هي أسماء وأرقام أطراف
/// ‏ثالثة لم تُستأذن ولا تعلم بوجود سيرو.
///
/// ‏٣) لا ميزة تعتمد عليها. الدعوة تعمل بالمشاركة أو باختيار جهة واحدة
/// ‏عمداً، وكلاهما لا يحتاج نسخة من الدفتر على السيرفر.
///
/// ‏ما بقي هنا قراءة محلية بحتة لعرض قائمة يختار منها الكابتن — لا
/// ‏شيء يغادر الجهاز.
Future<void> loadDeviceContacts() async {
if (contactMaps.isNotEmpty) return; // مُحمّلة سلفاً في هذه الجلسة
try { try {
// 2. Request permission and fetch all contacts.
if (await FlutterContacts.requestPermission(readonly: true)) { if (await FlutterContacts.requestPermission(readonly: true)) {
// mySnackbarSuccess('Starting contacts sync in background...'.tr);
final List<Contact> allContacts = final List<Contact> allContacts =
await FlutterContacts.getContacts(withProperties: true); await FlutterContacts.getContacts(withProperties: true);
// **FIX**: Assign fetched contacts to the class variable.
contacts = allContacts; contacts = allContacts;
contactMaps.value = contacts.map((contact) { contactMaps.value = allContacts
return { .where((c) => c.phones.isNotEmpty)
'name': contact.displayName, .map((contact) => {
'phones': 'name': contact.displayName,
contact.phones.map((phone) => phone.normalizedNumber).toList(), 'phones': contact.phones
'emails': contact.emails.map((email) => email.address).toList(), .map((phone) => phone.normalizedNumber)
}; .toList(),
}).toList(); 'emails':
contact.emails.map((email) => email.address).toList(),
})
.toList();
update(); 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) { } catch (e) {
// mySnackbarError('An error occurred during contact sync: $e'.tr); // قراءة دفتر الهاتف ليست حرجة — الدعوة تعمل بالمشاركة أيضاً.
} }
} }
@@ -56,7 +56,7 @@ class LeaderboardController extends GetxController {
payload: {'type': 'trips'}, payload: {'type': 'trips'},
); );
if (resTrips != null && resTrips != 'failure') { if (resTrips != null && resTrips != 'failure') {
var data = jsonDecode(resTrips); var data = _decode(resTrips);
if (data['message'] is List) { if (data['message'] is List) {
tripLeaderboard = (data['message'] as List).map((e) => LeaderboardEntry( tripLeaderboard = (data['message'] as List).map((e) => LeaderboardEntry(
driverId: e['driver_id'].toString(), driverId: e['driver_id'].toString(),
@@ -75,7 +75,7 @@ class LeaderboardController extends GetxController {
payload: {'type': 'earnings'}, payload: {'type': 'earnings'},
); );
if (resEarnings != null && resEarnings != 'failure') { if (resEarnings != null && resEarnings != 'failure') {
var data = jsonDecode(resEarnings); var data = _decode(resEarnings);
if (data['message'] is List) { if (data['message'] is List) {
earningsLeaderboard = (data['message'] as List).map((e) => LeaderboardEntry( earningsLeaderboard = (data['message'] as List).map((e) => LeaderboardEntry(
driverId: e['driver_id'].toString(), driverId: e['driver_id'].toString(),
@@ -99,4 +99,17 @@ class LeaderboardController extends GetxController {
isLoading = false; isLoading = false;
update(); update();
} }
/// ‏CRUD().post تُرجع الردّ **مفكوكاً** أصلاً (Map)، بينما CRUD().get
/// ‏تُرجع نصاً. تمرير الـ Map إلى jsonDecode كان يرمي
/// ‏"type '_Map<String, dynamic>' is not a subtype of type 'String'"،
/// ‏فتُبتلع لوحة الصدارة في catch وتظهر فارغة دائماً مهما صحّ الردّ.
Map<String, dynamic> _decode(dynamic res) {
if (res is Map) return Map<String, dynamic>.from(res);
if (res is String && res.isNotEmpty) {
final decoded = jsonDecode(res);
if (decoded is Map) return Map<String, dynamic>.from(decoded);
}
return <String, dynamic>{};
}
} }
@@ -347,11 +347,7 @@ class InviteScreen extends StatelessWidget {
onPressed: () async { onPressed: () async {
await controller.pickContactFromNativeApp(); await controller.pickContactFromNativeApp();
if (controller.contacts.isNotEmpty) { if (controller.contacts.isNotEmpty) {
if (box.read(BoxName.isSavedPhones) == null) { await _openContactsDialog(Get.context!);
// controller.savePhoneToServer();
box.write(BoxName.isSavedPhones, true);
}
_showContactsDialog(Get.context!);
} }
}, },
), ),
@@ -731,6 +727,14 @@ class InviteScreen extends StatelessWidget {
} }
*/ */
/// ‏تُحمَّل جهات الاتصال هنا عند الطلب — كانت تُقرأ وتُرفع تلقائياً عند
/// ‏فتح الشاشة. لا شيء يغادر الجهاز الآن.
Future<void> _openContactsDialog(BuildContext context) async {
await controller.loadDeviceContacts();
if (!context.mounted) return;
_showContactsDialog(context);
}
void _showContactsDialog(BuildContext context) { void _showContactsDialog(BuildContext context) {
showCupertinoModalPopup( showCupertinoModalPopup(
context: context, context: context,
@@ -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: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 '../../../transit/transit_driver_home_page.dart';
import '../About Us/video_page.dart'; import '../About Us/video_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'; import '../../journal/driver_scheduled_rides_page.dart';
@@ -53,11 +54,11 @@ class AppDrawer extends StatelessWidget {
// 2. تعريف بيانات القائمة بشكل مركزي ومنظم // 2. تعريف بيانات القائمة بشكل مركزي ومنظم
final List<DrawerItem> drawerItems = [ final List<DrawerItem> drawerItems = [
// DrawerItem( DrawerItem(
// title: 'Mawasalati'.tr, title: 'Mawasalati'.tr,
// icon: Icons.directions_bus_filled_rounded, icon: Icons.directions_bus_filled_rounded,
// color: Colors.teal, color: Colors.teal,
// onTap: () => Get.to(() => const TransitDriverHomePage())), onTap: () => Get.to(() => const TransitDriverHomePage())),
DrawerItem( DrawerItem(
title: 'Delivery'.tr, title: 'Delivery'.tr,
icon: Icons.delivery_dining_rounded, icon: Icons.delivery_dining_rounded,
@@ -353,9 +354,8 @@ class UserHeader extends StatelessWidget {
: Builder(builder: (context) { : Builder(builder: (context) {
final photoUrl = final photoUrl =
box.read(BoxName.driverPhotoUrl)?.toString() ?? ''; box.read(BoxName.driverPhotoUrl)?.toString() ?? '';
final avatarImage = photoUrl.isNotEmpty final avatarImage =
? NetworkImage(photoUrl) photoUrl.isNotEmpty ? NetworkImage(photoUrl) : null;
: null;
return CircleAvatar( return CircleAvatar(
backgroundImage: avatarImage, backgroundImage: avatarImage,