Update: 2026-08-08 14:26:43
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user