diff --git a/backend/ride/rides/acceptRide.php b/backend/ride/rides/acceptRide.php index 85c7832..487e2bc 100755 --- a/backend/ride/rides/acceptRide.php +++ b/backend/ride/rides/acceptRide.php @@ -121,6 +121,8 @@ try { c.color, c.color_hex, (SELECT ROUND(AVG(rating), 2) FROM ratingDriver WHERE driver_id = d.id) AS ratingDriver, + (SELECT COUNT(*) FROM ratingDriver WHERE driver_id = d.id) AS ratingCount, + (SELECT COUNT(*) FROM ride WHERE driver_id = d.id AND status IN ('Finished', 'finished')) AS completedRides, dt.token FROM driver d LEFT JOIN CarRegistration c ON c.driverID = d.id @@ -140,6 +142,16 @@ try { } $driverInfo['driverName'] = trim(($driverInfo['first_name'] ?? '') . ' ' . ($driverInfo['last_name'] ?? '')); $driverInfo['ratingDriver'] = $driverInfo['ratingDriver'] ?: "5.0"; + $ratingValue = (float) $driverInfo['ratingDriver']; + $ratingCount = (int) ($driverInfo['ratingCount'] ?? 0); + $completedRides = (int) ($driverInfo['completedRides'] ?? 0); + if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) { + $driverInfo['driverTier'] = 'Professional driver'; + } elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) { + $driverInfo['driverTier'] = 'Trusted driver'; + } else { + $driverInfo['driverTier'] = 'Verified driver'; + } } // ═══════════════════════════════════════════════════════════ @@ -195,4 +207,4 @@ try { } catch (PDOException $e) { error_log("[accept_ride] CRITICAL: " . $e->getMessage()); printFailure("Server error"); -} \ No newline at end of file +} diff --git a/backend/ride/rides/getRideOrderID.php b/backend/ride/rides/getRideOrderID.php index f8532ff..7bc828e 100755 --- a/backend/ride/rides/getRideOrderID.php +++ b/backend/ride/rides/getRideOrderID.php @@ -96,6 +96,17 @@ try { FROM ratingDriver WHERE ratingDriver.driver_id = :driverID_Sub ) AS ratingDriver, + ( + SELECT COUNT(*) + FROM ratingDriver + WHERE ratingDriver.driver_id = :driverID_Sub + ) AS ratingCount, + ( + SELECT COUNT(*) + FROM ride + WHERE ride.driver_id = :driverID_Sub + AND ride.status IN ('Finished', 'finished') + ) AS completedRides, driverToken.token AS token @@ -143,6 +154,16 @@ try { $finalData[$field] = $encryptionHelper->decryptData($finalData[$field]); } } + $ratingValue = (float) ($finalData['ratingDriver'] ?: 5.0); + $ratingCount = (int) ($finalData['ratingCount'] ?? 0); + $completedRides = (int) ($finalData['completedRides'] ?? 0); + if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) { + $finalData['driverTier'] = 'Professional driver'; + } elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) { + $finalData['driverTier'] = 'Trusted driver'; + } else { + $finalData['driverTier'] = 'Verified driver'; + } } echo json_encode([ @@ -155,4 +176,4 @@ try { http_response_code(500); echo json_encode(["status" => "failure", "message" => "Server Error: " . $e->getMessage()]); } -?> \ No newline at end of file +?> diff --git a/knowledge/AI_CONTEXT.md b/knowledge/AI_CONTEXT.md new file mode 100644 index 0000000..2b7f009 --- /dev/null +++ b/knowledge/AI_CONTEXT.md @@ -0,0 +1,204 @@ +# AI_CONTEXT.md — Siro (Intaleq) Ride-Hailing Platform + +## Core Architecture +- **4 Flutter apps** (rider, driver, admin, service) + **PHP backend** + **MySQL** + **WebSocket** +- **State Management**: GetX (permanent controllers in AppBindings) +- **Maps**: Intaleq Maps (custom Flutter plugin) + Google Maps + Map SaaS + OSRM +- **Domain**: siromove.com | **API**: api.intaleq.xyz/intaleq_v3 | **Ride**: rides.intaleq.xyz | **Location**: location.intaleq.xyz | **Payment**: walletintaleq.intaleq.xyz + +## Passenger Flow (siro_rider) +``` +Splash → Auth check → MapScreen (permanent map controllers) +→ Enter destination → Price calc → Ride request (waitingRides) +→ Searching (WebSocket polling + timer) → Driver accepts (RideState.driverApplied) +→ Driver arrives (RideState.driverArrived) → Ride begins (RideState.inProgress) +→ Ride finishes → Rating + Payment → Back to map +``` +- **RideState enum**: noRide, cancelled, preCheckReview, searching, driverApplied, driverArrived, inProgress, finished +- **Key Controller**: `RideLifecycleController` (4600 lines) — state machine, deviation detection, ETA calculation +- **Deviation Guard**: 50m threshold, re-routes if off path +- **Local ETA**: Route trimming by closest point, percentage-based time calculation + +## Driver Flow (siro_driver) +``` +Splash → Auth → MapScreen → Go Online (location streaming) +→ Ride offer via FCM + Android native overlay (TripOverlayPlugin) +→ Accept → Navigate to pickup → Arrived → Begin ride → Active ride → End ride +→ Rate passenger + Payment → Go Offline +``` +- **Background**: Android foreground service for GPS + FCM background handler +- **Overlay**: Custom native plugin shows trip data with accept/reject (auto-close 15s) +- **Data package**: 33-index array (passengerLat/Lng, destination, fare, distance, name, phone, etc.) + +## API Endpoints (59+ discovered) +### Auth +| Endpoint | Caller | +|----------|--------| +| POST `$server/auth/login.php` | LoginController | +| POST `$authCaptin/login.php` | Driver login | +| POST `$server/auth/signup.php` | RegisterController | +| POST `$server/loginJwtRider.php` | JWT refresh (401 handler) | + +### Ride +| Endpoint | Caller | +|----------|--------| +| POST `$rideServerSide/ride/rides/add.php` | Ride request | +| POST `$rideServerSide/rides/acceptRide.php` | Driver accept | +| POST `$rideServerSide/ride/rides/updateStausFromSpeed.php` | Status updates (Arrived/Begin/Finished) | +| POST `$rideServerSide/ride/rides/getRideStatus.php` | Polling fallback | +| POST `$server/ride/rides/getRideStatusFromStartApp.php` | App restore check | + +### Location +| Endpoint | Caller | +|----------|--------| +| POST `$location/{getSpeed,getComfort,getBalash,...}.php` | Nearby drivers by car type | +| POST `$location/getDriverCarsLocationToPassengerAfterApplied.php` | Driver GPS after accept | + +### Payment +| Endpoint | Caller | +|----------|--------| +| POST `$paymentServer/ride/payment/add.php` | Ride payment | +| POST `$paymentServer/ride/payMob/{wallet,card}/payWithPayMob.php` | Visa/Mastercard | +| POST `$paymentServer/ride/mtn/passenger/{start,confirm}_payment.php` | MTN mobile money | +| POST `$paymentServer/ride/syriatel/passenger/{start,confirm}_payment.php` | Syriatel mobile money | +| POST `$paymentServer/ecash/payWithEcash.php` | E-Cash | + +### Other +| Endpoint | Caller | +|----------|--------| +| POST `$server/ride/rate/addRateToDriver.php` | Passenger rating | +| POST `$server/ride/rate/addRateToPassenger.php` | Driver rating | +| POST `$wallet/getWalletByPassenger.php` | Wallet balance | +| POST `$walletDriver/getWalletByDriver.php` | Driver wallet | +| POST `$promo/getPromoBytody.php` | Promo code check | +| POST `$server/ride/invitor/get_unified_code.php` | Referral code | + +## Database (60+ tables in intaleqDB1 + intaleq-ridesDB) +### Core Tables +| Table | PK | Key Relationships | +|-------|----|-------------------| +| `passengers` | id (varchar) | → ride, waitingRides, payments, passengerWallet, tokens, ratingDriver, notifications | +| `driver` | idn (auto) + id (varchar) | → ride, car_locations, payments, driverWallet, driverToken, driver_orders | +| `ride` | id (auto) | passenger_id → passengers, driver_id → driver | +| `waitingRides` | id (varchar) | passenger_id → passengers, SPATIAL indexes on lat/lng | +| `car_locations` | driver_id (varchar) | SPATIAL idx_location_point (POINT), BTREE idx_loc_status_time | +| `payments` | id (varchar) | passengerID, driverID, rideId | +| `ratingDriver` | id (auto) | driver_id, passenger_id, UNIQUE ride_id | + +### Key Indexes +- `car_locations.location_point` — SPATIAL index for GIS queries +- `waitingRides` — idx_location_status (lat,lng,status,created_at) +- `palces11` — FULLTEXT on name/name_ar/name_en/address/category + +## Real-time Systems +- **WebSocket**: PHP Socket.IO server (socket_intaleq/{driver,passenger}_socket.php) +- **Events**: `driver_location_update`, `ride_accepted`, `ride_cancelled`, `ride_finished` +- **Polling fallback**: HTTP polling every N seconds when WebSocket disconnects +- **3 reliable updates**: Stops polling after 3 consecutive WebSocket location updates + +## Notifications +- **Push**: Firebase Cloud Messaging (FCM) — `FirebaseMessagesController` +- **Local**: `NotificationController` with custom channels +- **iOS Live Activity**: `IosLiveActivityService` + SwiftUI RideWidget +- **Background**: Android overlay (TripOverlayPlugin) for driver ride offers +- **Types**: Order (ride offer), OrderSpeed, ride status updates + +## GIS Logic +- **Routing**: Map SaaS (`map-saas.intaleqapp.com/api/maps/route`) or OSRM (`routesy.intaleq.xyz`) +- **Geocoding**: Map SaaS reverse geocoding + search +- **ETA**: Local algorithm in `RideLifecycleController.updateRemainingRoute()` — finds closest route point, trims polyline, recalculates percentage +- **Deviation**: `checkAndRecalculateIfDeviated()` — 50m threshold, consecutive heading check +- **Driver Tracking**: `handleDriverLocationUpdate()` — camera follows driver, zoom by speed +- **Marker Rotation**: Updates driver car icon rotation based on heading + +## Wallet Logic +- **Passenger Wallet**: `passengerWallet` table, deduct on ride, top-up via PayMob/MTN/Syriatel +- **Driver Wallet**: `driverWallet` table, credit after ride, withdraw +- **Payment Methods**: Cash (default), Visa (PayMob), Wallet, MTN, Syriatel, E-Cash +- **Kazan**: Percentage-based commission per country (kazan table: comfortPrice, speedPrice, familyPrice, etc.) +- **Tips**: `tips` table linked to ride/driver/passenger + +## State Management Map +### Rider App Controllers (permanent in AppBindings) +| Controller | States | Dependencies | +|-----------|--------|-------------| +| RideLifecycleController | RideState enum (8 states) | CRUD, MapEngine, MapSocket, UiInteractions, NearbyDrivers | +| MapSocketController | connected/disconnected | WebSocket, RideLifecycle | +| MapEngineController | map ready/loading | IntaleqMaps, markers, polylines | +| LocationSearchController | idle/searching/result | Map SaaS API | +| NearbyDriversController | empty/populated | Location API | +| UiInteractionsController | sheet states | All ride widgets | +| LoginController | logged out/authenticating/logged in | CRUD, JWT | +| SplashScreenController | animating/checking/navigating | GetStorage, CRUD | + +### Driver App +| Controller | States | Dependencies | +|-----------|--------|-------------| +| HomeCaptainController | offline/online/in-ride | LocationService, WebSocket, CRUD | +| NavigationController | idle/navigating/recalculating | Map, TTS | +| BackgroundServiceHelper | running/stopped | Android service | + +## Security +- **JWT** with device fingerprint (SHA-256) in payload +- **X-Device-FP** header validated against JWT fingerprint claim +- **HMAC** for payment server (X-HMAC-Auth header) +- **401 auto-refresh**: `LoginController.getJWT()` retries once +- **Rate limiting**: login_attempts table per IP + +## External Services +| Service | Endpoint | Key Type | +|---------|----------|----------| +| Google Maps | maps.googleapis.com | API Key | +| Map SaaS | map-saas.intaleqapp.com | x-api-key | +| Here Maps | autosuggest.search.hereapi.com | API Key | +| OSRM | routec.intaleq.xyz / routesy.intaleq.xyz | None | +| PayMob | paymob.com | HMAC | +| Twilio | verify.twilio.com | Account SID + Token | +| Azure OCR | ocrhamza.cognitiveservices.azure.com | Subscription Key | +| OpenAI | api.openai.com | Bearer Token | +| Llama | Together API | Bearer Token | +| SMS Kazumi | sms.kazumi.me | API Key | + +## File Map (Key Files) +``` +siro_rider/ + lib/main.dart, app_bindings.dart, splash_screen_page.dart + lib/controller/home/map/ride_lifecycle_controller.dart (4600 lines) + lib/controller/home/map/ride_state.dart (8-state enum) + lib/controller/home/map/map_socket_controller.dart + lib/controller/home/map/map_engine_controller.dart + lib/controller/functions/crud.dart (720 lines, unified HTTP client) + lib/constant/links.dart (all API endpoints) + lib/views/home/map_widget.dart/*.dart (20+ map UI widgets) + +siro_driver/ + lib/main.dart (550 lines, accept/reject logic) + lib/controller/functions/background_service.dart + lib/controller/home/captin/home_captain_controller.dart + lib/views/home/Captin/driver_map_page.dart + lib/views/home/Captin/orderCaptin/order_request_page.dart + +backend/ + schema_primary.sql (1826 lines, 60+ tables) + schema_ride.sql (1787 lines) + auth/*.php, ride/*.php, Admin/*.php + +socket_intaleq/ + driver_socket.php, passenger_socket.php + +siro_admin/ (Flutter Web admin panel) + 20+ controllers, 30+ views + +siro_service/ (Driver registration agent app) +``` + +## Key Business Rules +- **Ride timeout**: `_totalSearchTimeoutSeconds` → increase fee dialog +- **Wait time**: 5-minute passenger wait after driver arrival +- **Deviation**: 50m threshold before re-routing +- **Socket reliability**: 3 updates → stop polling +- **Cash payment**: Driver marks as received, confirmation dialog +- **Wallet payment**: Deduct from passengerWallet, verify balance +- **Multi-point trips**: Up to 5 waypoints (step0-step4 in ride args) +- **Car types**: Speed, Comfort, Family, Delivery, Blash (free), Late, Heavy, Nature, Electric, PinkBike, Van, FemalDriver +- **Regions**: Syria (routesy), Jordan (routesjo), Egypt (routec) \ No newline at end of file diff --git a/knowledge/API_DEPENDENCY_MATRIX.md b/knowledge/API_DEPENDENCY_MATRIX.md new file mode 100644 index 0000000..ddb8760 --- /dev/null +++ b/knowledge/API_DEPENDENCY_MATRIX.md @@ -0,0 +1,169 @@ +# API_DEPENDENCY_MATRIX.md — Complete API Endpoint Reference + +## Auth Endpoints + +| Endpoint | Method | Caller Files | Request Model | Response Model | Auth | DB Tables | +|----------|--------|-------------|---------------|----------------|------|-----------| +| `$server/auth/login.php` | POST | `login_controller.dart` | phone, password, fingerprint | JWT, passenger_id, profile | No | `passengers` | +| `$authCaptin/login.php` | POST | driver login | phone, password, fingerprint | JWT, driver_id, profile | No | `driver` | +| `$server/auth/signup.php` | POST | `register_controller.dart` | phone, email, password, name, gender, birthdate, site | user_id | No | `passengers` | +| `$authCaptin/register.php` | POST | service/register | driver data + documents | driver_id | No | `driver` | +| `$server/auth/loginFromGooglePassenger.php` | POST | `google_sign.dart` | google_token | JWT, passenger_id | No | `passengers` | +| `$authCaptin/loginFromGoogle.php` | POST | driver Google auth | google_token | JWT, driver_id | No | `driver` | +| `$server/loginJwtRider.php` | POST | `login_controller.dart` | refresh_token | new JWT | Yes | `tokens` | +| `$server/loginJwtDriver.php` | POST | driver token refresh | refresh_token | new JWT | Yes | `driverToken` | +| `$server/loginWallet.php` | POST | `login_controller.dart` | refresh_token | wallet JWT | Yes | `tokens` | +| `$server/loginJwtWalletDriver.php` | POST | driver wallet refresh | refresh_token | wallet JWT | Yes | `driverToken` | +| `$server/loginFirstTime.php` | POST | `login_controller.dart` | phone, device_info | first_time status | No | `login_attempts` | +| `$server/auth/otpmessage.php` | POST | `otp_controller.dart` | phone | OTP sent | No | `phone_verification` | +| `$server/auth/verifyOtpMessage.php` | POST | `otp_controller.dart` | phone, otp | verified | No | `phone_verification` | +| `$server/auth/sendVerifyEmail.php` | POST | passenger | email | email sent | No | `email_verifications` | +| `$server/auth/verifyEmail.php` | POST | passenger | email, token | verified | No | `email_verifications` | +| `$server/auth/cnMap.php` | POST | various | country_code | phone mapping | No | None | +| `$server/auth/packageInfo.php` | POST | `splash_screen_controlle.dart` | platform, appName | version, app_name | No | `packageInfo` | +| `$server/auth/checkPhoneNumberISVerfiedPassenger.php` | POST | auth check | phone | verified status | No | `phone_verification_passenger` | + +## Ride Endpoints + +| Endpoint | Method | Caller Files | Request Model | Response Model | Auth | DB Tables | +|----------|--------|-------------|---------------|----------------|------|-----------| +| `$rideServerSide/ride/rides/add.php` | POST | `ride_lifecycle_controller.dart` | passenger_id, start/end lat/lng, carType, price, distance, duration, payment_method | ride_id | Yes | `waitingRides` | +| `$rideServerSide/rides/acceptRide.php` | POST | driver main.dart | id, rideTimeStart, status='Apply', passengerToken, driver_id | accepted_ride | Yes | `waitingRides`, `ride` | +| `$rideServerSide/ride/rides/get.php` | POST | history, status check | passenger_id or ride_id | ride list | Yes | `ride` | +| `$rideServerSide/ride/rides/getRideOrderID.php` | POST | status polling | ride_id | order_id | Yes | `waitingRides` | +| `$rideServerSide/ride/rides/getRideStatus.php` | POST | polling | ride_id | status | Yes | `ride` | +| `$rideServerSide/ride/rides/getRideStatusBegin.php` | POST | polling | ride_id | begin status | Yes | `ride` | +| `$server/ride/rides/getRideStatusFromStartApp.php` | POST | `ride_lifecycle_controller.dart` | passenger_id | active ride + status | Yes | `ride`, `waitingRides` | +| `$rideServerSide/ride/rides/update.php` | POST | various | ride_id, fields | success | Yes | `ride` | +| `$rideServerSide/ride/rides/updateStausFromSpeed.php` | POST | driver | ride_id, status (Arrived/Begin/Finished) | success | Yes | `ride` | +| `$rideServerSide/ride/rides/delete.php` | POST | admin | ride_id | success | Yes | `ride` | +| `$rideServerSide/cancelRide/add.php` | POST | `cancel_raide_page.dart` | ride_id, passenger_id, driverID, note | cancelled | Yes | `canecl` | +| `$rideServerSide/cancelRide/get.php` | POST | status | ride_id | cancel info | Yes | `canecl` | + +## Location Endpoints + +| Endpoint | Method | Caller Files | Request Model | Response Model | Auth | DB Tables | +|----------|--------|-------------|---------------|----------------|------|-----------| +| `$location/get.php` | POST | `nearby_drivers_controller.dart` | passenger_lat, passenger_lng, carType | nearby drivers | Yes | `car_locations` | +| `$location/getSpeed.php` | POST | nearby drivers | lat, lng | Speed drivers | Yes | `car_locations` | +| `$location/getComfort.php` | POST | nearby drivers | lat, lng | Comfort drivers | Yes | `car_locations` | +| `$location/getBalash.php` | POST | nearby drivers | lat, lng | Blash drivers | Yes | `car_locations` | +| `$location/getElectric.php` | POST | nearby drivers | lat, lng | Electric drivers | Yes | `car_locations` | +| `$location/getPinkBike.php` | POST | nearby drivers | lat, lng | Bike drivers | Yes | `car_locations` | +| `$location/getDelivery.php` | POST | nearby drivers | lat, lng | Delivery drivers | Yes | `car_locations` | +| `$location/getFemalDriver.php` | POST | nearby drivers | lat, lng | Female drivers | Yes | `car_locations` | +| `$location/getDriverCarsLocationToPassengerAfterApplied.php` | POST | `ride_lifecycle_controller.dart` | driver_id | driver GPS | Yes | `car_locations` | +| `$locationServerSide/addpassengerLocation.php` | POST | passenger | passengerId, lat, lng, rideId | success | Yes | `passengerlocation` | +| `$location/getLocationParents.php` | POST | driver | driver_id | nearby passengers | Yes | `car_locations` | + +## Payment Endpoints + +| Endpoint | Method | Caller Files | Request Model | Response Model | Auth | DB Tables | +|----------|--------|-------------|---------------|----------------|------|-----------| +| `$paymentServer/ride/payment/add.php` | POST | `payment_controller.dart` | amount, payment_method, passengerID, rideId, driverID | payment_id | Yes | `payments` | +| `$paymentServer/ride/passengerWallet/addPaymentTokenPassenger.php` | POST | wallet | passengerId, amount, token | success | Yes | `payment_tokens_passenger` | +| `$paymentServer/ride/driverWallet/addPaymentToken.php` | POST | driver wallet | driverID, amount, token | success | Yes | `payment_tokens` | +| `$paymentServer/ride/payMob/wallet/payWithPayMob.php` | POST | `paymob_wallet.dart` | passengerId, amount | paymob_url | Yes | `payments` | +| `$paymentServer/ride/payMob/payWithPayMob.php` | POST | `paymob.dart` | passengerId, amount, card_data | payment_response | Yes | `payments` | +| `$paymentServer/ecash/payWithEcash.php` | POST | `e_cash_screen.dart` | passengerId, amount, ecash_data | success | Yes | `payments` | +| `$paymentServer/ride/mtn/passenger/mtn_start.php` | POST | MTN payment | passengerId, amount, phone | mtn_ref | Yes | `payments` | +| `$paymentServer/ride/mtn/passenger/mtn_confirm.php` | POST | MTN payment | mtn_ref, otp | success | Yes | `payments` | +| `$paymentServer/ride/syriatel/passenger/start_payment.php` | POST | Syriatel | passengerId, amount, phone | syriatel_ref | Yes | `payments` | +| `$paymentServer/ride/syriatel/passenger/confirm_payment.php` | POST | Syriatel | ref, otp | success | Yes | `payments` | +| `$paymentServer/ride/payment/get.php` | POST | `driver_payment_controller.dart` | driver_id | today's payments | Yes | `payments` | +| `$paymentServer/ride/payment/getCountRide.php` | POST | driver earnings | driver_id | ride_count | Yes | `payments` | +| `$paymentServer/ride/payment/getAllPayment.php` | POST | admin | date | all payments | Yes | `payments` | + +## Wallet Endpoints + +| Endpoint | Method | Caller Files | Request Model | Response Model | Auth | DB Tables | +|----------|--------|-------------|---------------|----------------|------|-----------| +| `$wallet/getWalletByPassenger.php` | POST | `passenger_wallet_history_controller.dart` | passenger_id | balance, history | Yes | `passengerWallet` | +| `$walletDriver/getWalletByDriver.php` | POST | driver wallet | driver_id | balance, history | Yes | `driverWallet` | +| `$wallet/add.php` | POST | admin/passenger | passenger_id, amount | success | Yes | `passengerWallet` | +| `$walletDriver/add.php` | POST | admin/driver | driver_id, amount | success | Yes | `driverWallet` | +| `$wallet/get.php` | POST | admin | - | all wallets | Yes | `passengerWallet` | +| `$walletDriver/get.php` | POST | admin | - | all driver wallets | Yes | `driverWallet` | +| `$wallet/getAllPassengerTransaction.php` | POST | passenger | passenger_id | transactions | Yes | `passengerWallet` | +| `$wallet/getPassengerWalletArchive.php` | POST | passenger | passenger_id | archive | Yes | `passengerWallet` | + +## Rating Endpoints + +| Endpoint | Method | Caller Files | Request Model | Response Model | Auth | DB Tables | +|----------|--------|-------------|---------------|----------------|------|-----------| +| `$server/ride/rate/addRateToDriver.php` | POST | `rate_conroller.dart` | passenger_id, driver_id, ride_id, rating, comment | success | Yes | `ratingDriver` | +| `$server/ride/rate/addRateToPassenger.php` | POST | driver rate | driverID, passenger_id, rideId, rating, comment | success | Yes | `ratingPassenger` | +| `$server/ride/rate/getDriverRate.php` | POST | profile | driver_id | avg_rating, count | Yes | `ratingDriver` | +| `$server/ride/rate/getPassengerRate.php` | POST | profile | passenger_id | avg_rating, count | Yes | `ratingPassenger` | + +## Notification Endpoints + +| Endpoint | Method | Caller Files | Request Model | Response Model | Auth | DB Tables | +|----------|--------|-------------|---------------|----------------|------|-----------| +| `$server/ride/notificationPassenger/add.php` | POST | admin | passenger_id, title, body | success | Yes | `notifications` | +| `$server/ride/notificationPassenger/get.php` | POST | `passenger_notification_controller.dart` | passenger_id | notifications | Yes | `notifications` | +| `$server/ride/notificationPassenger/update.php` | POST | passenger | id, isShown | success | Yes | `notifications` | +| `$server/ride/notificationCaptain/add.php` | POST | admin | driverID, title, body | success | Yes | `notificationCaptain` | +| `$server/ride/notificationCaptain/get.php` | POST | `notification_captain_controller.dart` | driverID | notifications | Yes | `notificationCaptain` | +| `$server/ride/notificationCaptain/update.php` | POST | driver | id, isShown | success | Yes | `notificationCaptain` | +| `$server/ride/notificationCaptain/addWaitingRide.php` | POST | driver | driver_id | success | Yes | `notificationCaptain` | +| `$server/ride/notificationCaptain/getRideWaiting.php` | POST | driver | driver_id | pending rides | Yes | `notificationCaptain` | +| `$server/ride/firebase/add.php` | POST | `firbase_messge.dart` | passenger_id, token, fingerPrint | success | Yes | `tokens` | +| `$server/ride/firebase/addDriver.php` | POST | driver | captain_id, token, fingerPrint | success | Yes | `driverToken` | +| `$server/ride/firebase/getTokensPassenger.php` | POST | admin | passenger_id | tokens | Yes | `tokens` | + +## Profile Endpoints + +| Endpoint | Method | Caller Files | Request Model | Response Model | Auth | DB Tables | +|----------|--------|-------------|---------------|----------------|------|-----------| +| `$profile/get.php` | POST | `profile_controller.dart` | passenger_id | profile | Yes | `passengers` | +| `$profile/getCaptainProfile.php` | POST | `captain_profile_controller.dart` | driver_id | driver profile | Yes | `driver` | +| `$profile/update.php` | POST | profile | id, fields | success | Yes | `passengers` | + +## Promo Endpoints + +| Endpoint | Method | Caller Files | Request Model | Response Model | Auth | DB Tables | +|----------|--------|-------------|---------------|----------------|------|-----------| +| `$promo/get.php` | POST | `promos_controller.dart` | passenger_id | promos | Yes | `promos` | +| `$promo/getPromoBytody.php` | POST | ride request | passenger_id | valid promo | Yes | `promos` | +| `$promo/getPromoFirst.php` | POST | promo check | passenger_id | first promo | Yes | `promos` | +| `$promo/add.php` | POST | admin | promo_code, amount, passengerID, dates | success | Yes | `promos` | +| `$promo/delete.php` | POST | admin | promo_id | success | Yes | `promos` | +| `$promo/update.php` | POST | admin | promo_id, fields | success | Yes | `promos` | + +## Invite/Referral Endpoints + +| Endpoint | Method | Caller Files | Request Model | Response Model | Auth | DB Tables | +|----------|--------|-------------|---------------|----------------|------|-----------| +| `$server/ride/invitor/get_unified_code.php` | POST | `invites_rewards_controller.dart` | user_id, user_type | referral_code | Yes | `user_referral_codes` | +| `$server/ride/invitor/add_unified_invite.php` | POST | invite | inviter_code, invited_user_id, invited_user_type | success | Yes | `unified_referrals` | +| `$server/ride/invitor/get_passenger_referrals.php` | POST | passenger | passenger_id | referrals | Yes | `unified_referrals` | +| `$server/ride/invitor/add.php` | POST | driver invite | driverId, inviterDriverPhone, inviteCode | success | Yes | `invites` | +| `$server/ride/invitor/get.php` | POST | driver | driverId | invites | Yes | `invites` | + +## Admin Endpoints + +| Endpoint | Method | Caller Files | Request Model | Response Model | Auth | DB Tables | +|----------|--------|-------------|---------------|----------------|------|-----------| +| `$server/Admin/errorApp.php` | POST | `crud.dart` | error, userId, userType, phone, device, details | success | No | `error` | +| `$server/Admin/getPassengerDetails.php` | POST | admin | phone/email/id | passenger details | Yes | `passengers` | +| `$server/Admin/getPassengerDetailsByPassengerID.php` | POST | admin | passenger_id | details | Yes | `passengers` | +| `$server/Admin/AdminCaptain/get.php` | POST | admin | - | all captains | Yes | `driver` | +| `$server/Admin/AdminCaptain/getCaptainDetailsByEmailOrIDOrPhone.php` | POST | admin | identifier | captain details | Yes | `driver` | +| `$server/Admin/AdminRide/get.php` | POST | admin | - | all rides | Yes | `ride` | +| `$server/Admin/AdminRide/getRidesPerMonth.php` | POST | admin | month, year | rides stats | Yes | `ride` | +| `$server/ride/kazan/get.php` | POST | admin/calculator | country | kazan rates | Yes | `kazan` | +| `$server/ride/kazan/add.php` | POST | admin | country, kazan, prices | success | Yes | `kazan` | + +## Misc Endpoints + +| Endpoint | Method | Caller Files | Request Model | Response Model | Auth | DB Tables | +|----------|--------|-------------|---------------|----------------|------|-----------| +| `$server/ride/tips/add.php` | POST | rating | driverID, passengerID, rideID, tipAmount | success | Yes | `tips` | +| `$server/ride/driver_order/add.php` | POST | driver | driver_id, order_id, status | success | Yes | `driver_orders` | +| `$server/ride/driver_order/get.php` | POST | driver | driver_id | orders | Yes | `driver_orders` | +| `$server/ride/driver_order/getOrderCancelStatus.php` | POST | driver | order_id | cancel status | Yes | `driver_orders` | +| `$server/ride/driver_order/update.php` | POST | driver | order_id, status | success | Yes | `driver_orders` | +| `$server/ride/feedBack/add.php` | POST | feedback | passengerId, feedBack | success | Yes | `feedBack` | +| `$server/ride/chat/send_message.php` | POST | chat | sender_id, receiver_id, message, ride_id | success | Yes | None (external) | +| `$server/ride/location/get_location_area_links.php` | POST | map | lat, lng | area server links | No | `server_locations` | \ No newline at end of file diff --git a/knowledge/DATABASE_DEPENDENCY_MATRIX.md b/knowledge/DATABASE_DEPENDENCY_MATRIX.md new file mode 100644 index 0000000..e41b97b --- /dev/null +++ b/knowledge/DATABASE_DEPENDENCY_MATRIX.md @@ -0,0 +1,141 @@ +# DATABASE_DEPENDENCY_MATRIX.md — Complete Database Reference + +## Database: intaleqDB1 (Primary Database) + +| Table | PK | FKs | Used By | Related APIs | +|-------|----|-----|---------|-------------| +| `passengers` | `id` (varchar) | - | Auth, Profile, Ride, Wallet | login, signup, profile/get, getPassengerDetails | +| `driver` | `idn` (auto) + `id` (varchar) | - | Auth, Profile, Admin | loginCaptin, register, getCaptainProfile | +| `ride` | `id` (auto) | `passenger_id` → passengers(id), `driver_id` → driver(id) | Ride lifecycle, Admin, History | getRides, updateRides, addRides | +| `waitingRides` | `id` (varchar) | `passenger_id` → passengers(id) | Ride dispatching, Matching | addRides, getRideOrderID | +| `car_locations` | `driver_id` (varchar) | `driver_id` → driver(id) | Location tracking, Nearby drivers | getCarsLocationByPassenger*, addLocation | +| `car_tracks` | `id` (auto) | `driver_id` → driver(id) | Location history | - | +| `payments` | `id` (varchar) | `passengerID` → passengers(id), `driverID` → driver(id), `rideId` → ride(id) | Payment processing | addPayment, getPayment | +| `passengerWallet` | `id` (auto) | `passenger_id` → passengers(id) | Wallet management | getWalletByPassenger, addWallet | +| `driverWallet` | `id` (auto) | `driverID` → driver(id) | Driver wallet | getWalletByDriver, addDriversWallet | +| `tokens` | `id` (auto) | `passengerID` → passengers(id) | Auth, FCM | addTokens, getTokensPassenger | +| `driverToken` | `id` (auto) | `captain_id` → driver(id) | Auth, FCM | addTokensDriver | +| `ratingDriver` | `id` (auto) | `driver_id` → driver(id), `passenger_id` → passengers(id), `ride_id` → ride(id) | Rating | addRateToDriver, getDriverRate | +| `ratingPassenger` | `id` (auto) | `passenger_id` → passengers(id), `driverID` → driver(id), `rideId` → ride(id) | Rating | addRateToPassenger | +| `promos` | `id` (auto) | `passengerID` → passengers(id) | Promotions | getPromos, addPromo | +| `notifications` | `id` (auto) | `passenger_id` → passengers(id) | Notifications | addNotificationPassenger, get | +| `notificationCaptain` | `id` (auto) | `driverID` → driver(id) | Notifications | addNotificationCaptain, get | +| `canecl` | `id` (auto) | `driverID` → driver(id), `passengerID` → passengers(id), `rideID` → ride(id) | Cancellations | addCancelRide | +| `complaint` | `id` (auto) | `ride_id` → ride(id), `passenger_id` → passengers(id), `driver_id` → driver(id) | Complaints | addComplaint, getComplaint | +| `tips` | `id` (auto) | `driverID` → driver(id), `passengerID` → passengers(id), `rideID` → ride(id) | Tips | addTips | +| `error` | `id` (auto) | - | Error logging | addError | +| `CarRegistration` | `id` (auto) | `driverID` → driver(id) | Car documents | addRegisrationCar, get | +| `captains_car` | `id` (auto) | `driverID` → driver(id) | Car registration | - | +| `driver_documents` | `id` (auto) | `driverID` → driver(id) | Document upload | uploadImageType | +| `card_images` | `id` (auto) | `driverID` → driver(id) | ID card images | uploadImagePortrate | +| `imageProfileCaptain` | `id` (auto) | `driverID` → driver(id) | Profile pictures | - | +| `criminalDocuments` | `id` (auto) | `driverId` → driver(id) | Criminal records | - | +| `driver_behavior` | `id` | `driver_id` → driver(id), `trip_id` → ride(id) | Driver scoring | - | +| `driver_gifts` | `id` (auto) | `driver_id` → driver(id) | Driver gifts | driver_gift_check_page | +| `driver_health_assurance` | `id` (auto) | `driver_id` → driver(id) | Health insurance | - | +| `driver_orders` | `id` (auto) | `driver_id` → driver(id) | Order management | addDriverOrder, get | +| `driver_ride_scam` | `id` (auto) | `driverID` → driver(id), `passengerID` → passengers(id), `rideID` → ride(id) | Scam detection | adddriverScam | +| `email_verifications` | `id` (auto) | - | Email verification | sendVerifyEmail, verifyEmail | +| `employee` | `id` (varchar) | - | Employee management | employee_page | +| `helpCenter` | `id` (auto) | `driverID` → driver(id) | Help center | addhelpCenter | +| `invites` | `id` (auto) | `driverId` → driver(id) | Driver referrals | addInviteDriver | +| `invitesToPassengers` | `id` (auto) | `driverId` → driver(id) | Passenger referrals | addInvitationPassenger | +| `kazan` | `id` (auto) | `adminId` → adminUser(id) | Pricing | getKazanPercent | +| `login_attempts` | `id` (auto) | - | Rate limiting | loginFirstTime | +| `login_attempts_drivers` | `id` (auto) | - | Rate limiting | - | +| `mishwaritrips` | `id` (auto) | `driverId` → driver(id), `passengerId` → passengers(id) | Mishwari trips | addMishwari | +| `notesForDriverService` | `id` (auto) | `phone` → driver(phone) | Admin notes | - | +| `notesForPassengerService` | `id` (auto) | `phone` → passengers(phone) | Admin notes | - | +| `otp_verification_fingerPrint` | `id` | - | OTP fingerprint | - | +| `packageInfo` | `id` (auto) | - | App version | packageInfo | +| `palces11` | `id` (auto) | - | Saved places | getPlacesSyria | +| `passenger_blacklist` | `id` (auto) | - | Passenger blacklist | blacklist_page | +| `passengerlocation` | `id` (auto) | `passengerId` → passengers(id) | Passenger GPS | addpassengerLocation | +| `payment_tokens` | `id` (auto) | `driverID` → driver(id) | Payment tokens | addPaymentTokenDriver | +| `payment_tokens_passenger` | `id` (auto) | `passengerId` → passengers(id) | Payment tokens | addPaymentTokenPassenger | +| `paymentsDriverPoints` | `id` (auto) | `driverID` → driver(id) | Points payments | addDriverPaymentPoints | +| `phone_verification` | `id` (auto) | `driverId` → driver(id) | Phone verification | otpmessage | +| `phone_verification_passenger` | `id` (auto) | - | Phone verification | verifyOtpPassenger | +| `places` / `placesEgypt` | `id` (auto) | - | Places data | savePlacesServer | +| `promptDriverIDEgypt` | `id` (auto) | - | AI prompts | - | +| `ratingApp` | `id` (auto) | - | App rating | - | +| `server_locations` | `id` (auto) | - | Server area links | get_location_area_links | +| `smsSender` | `id` (auto) | - | SMS sender ID | - | +| `token_verification*` | `id` (auto) | - | Token verification | - | +| `user_referral_codes` | `id` (auto) | `user_id` (polymorphic) | Referral codes | get_unified_code | +| `unified_referrals` | `id` (auto) | - | Unified referrals | add_unified_invite | +| `driver_cash_claims` | `id` (auto) | `driver_id` → driver(id), `referral_id` → unified_referrals(id) | Cash claims | - | +| `vehicles` | `id` (auto) | `driverID` → driver(id) | Vehicle info | - | +| `CarRegistration` (ride DB) | `id` (auto) | `driverID` → driver(id) | Car registration | addRegisrationCar | +| `adminUser` | `id` (auto) | - | Admin users | addAdminUser | +| `api_keys` | `id` (auto) | - | API keys | getApiKey | +| `blacklist_driver` | `id` (auto) | `driver_id` → driver(id) | Driver blacklist | blacklist_page | +| `carPlateEdit` | `id` (auto) | `driverId` → driver(id) | Plate edit requests | - | +| `carsToWork` | `id` (auto) | - | Work car registration | - | +| `contactEgypt` / `contactSyria` | `id` (auto) | `driverId` → driver(id) | Contact sync | savePhones | +| `driversWantWork` | `id` (auto) | - | Driver applications | - | +| `feedBack` | `id` (auto) | `passengerId` → passengers(id) | Feedback | addFeedBack | +| `hotels` | `id` | - | Hotel data | - | +| `invoicesAdmin` / `invoice_records` | `id` (auto) | - | Invoicing | - | +| `lisenceDetails` | `id` (varchar) | `driverID` → driver(id) | License details | - | +| `seferWallet` | `id` (auto) | `driverId` → driver(id), `passengerId` → passengers(id) | Sefer wallet | addSeferWallet | +| `test` / `testApp` | `id` (auto) | - | Testing | - | +| `videos` | `id` (auto) | - | Tutorial videos | - | +| `welcomeDriverCall` | `id` (auto) | `driverId` → driver(id) | Welcome calls | - | +| `write_argument_after_applied_from_background` | `id` (auto) | - | Background argument storage | - | + +--- + +## Database: intaleq-ridesDB (Ride-specific Database) + +This database mirrors many tables from intaleqDB1 for ride-specific operations. Tables present include: +- `ride`, `waitingRides`, `car_locations`, `car_tracks`, `driver`, `driverToken`, `payments`, `notifications`, `ratingDriver`, etc. +- Purpose: Isolated ride processing without affecting main DB performance. + +--- + +## Key Indexes + +| Table | Index | Type | Columns | +|-------|-------|------|---------| +| `car_locations` | `idx_location_point` | SPATIAL | `location_point` | +| `car_locations` | `idx_loc_status_time` | BTREE | `status`, `updated_at`, `latitude`, `longitude` | +| `waitingRides` | `idx_location_status` | BTREE | `start_lat`, `start_lng`, `status`, `created_at` | +| `waitingRides` | `idx_status_created` | BTREE | `status`, `created_at` | +| `waitingRides` | `idx_passenger` | BTREE | `passenger_id` | +| `palces11` | `idx_fulltext_search` | FULLTEXT | `name`, `name_ar`, `name_en`, `address`, `category` | +| `driver` | `national_number` | UNIQUE | `national_number` | +| `passengers` | `phone` | UNIQUE | `phone`, `email` | +| `ride` | `passengerfk` | BTREE | `passenger_id` | +| `ride` | `driverfk` | BTREE | `driver_id` | +| `error` | `idx_error_created_at` | BTREE | `created_at` | +| `error` | `idx_error_phone` | BTREE | `phone` | + +--- + +## Referential Integrity + +Most tables use InnoDB engine with foreign key relationships implied by business logic. Explicit FK constraints are minimal. Key relationships: + +``` +passengers (id) ──┬── ride (passenger_id) + ├── waitingRides (passenger_id) + ├── payments (passengerID) + ├── passengerWallet (passenger_id) + ├── tokens (passengerID) + ├── ratingDriver (passenger_id) + ├── ratingPassenger (passenger_id) + ├── notifications (passenger_id) + └── feedBack (passengerId) + +driver (id) ──┬── ride (driver_id) + ├── car_locations (driver_id) + ├── car_tracks (driver_id) + ├── payments (driverID) + ├── driverWallet (driverID) + ├── driverToken (captain_id) + ├── ratingDriver (driver_id) + ├── ratingPassenger (driverID) + ├── notificationCaptain (driverID) + ├── driver_orders (driver_id) + └── driver_documents (driverID) \ No newline at end of file diff --git a/knowledge/DRIVER_JOURNEY.md b/knowledge/DRIVER_JOURNEY.md new file mode 100644 index 0000000..2dc28fe --- /dev/null +++ b/knowledge/DRIVER_JOURNEY.md @@ -0,0 +1,318 @@ +# DRIVER_JOURNEY.md — Complete Driver (Captain) Lifecycle + +## Stage 1: App Launch & Authentication + +### Screen +- **Route**: `/` → `SplashScreen` +- **File**: `siro_driver/lib/splash_screen_page.dart` +- **Controllers**: `SplashScreenController`, `LocaleController`, `BackgroundServiceHelper` + +### State Flow +- GetX-based auth check → JWT validation → Splash → Map or Login + +### Variables +| Variable | Storage | Key | +|----------|---------|-----| +| jwt (driver) | GetStorage | `BoxName.jwt` | +| driverID | GetStorage | `BoxName.driverID` | +| isAppInForeground | GetStorage | `BoxName.isAppInForeground` | +| statusDriverLocation | GetStorage | `BoxName.statusDriverLocation` | +| rideStatus | GetStorage | `BoxName.rideStatus` | + +### APIs (Driver Auth) +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$authCaptin/login.php` | POST | phone, password, fingerprint | JWT + driver data | +| `$authCaptin/register.php` | POST | driver data | account created | +| `$server/loginJwtDriver.php` | POST | refresh token | new JWT | +| `$server/loginJwtWalletDriver.php` | POST | refresh token | wallet JWT | +| `$authCaptin/loginFromGoogle.php` | POST | google token | JWT + driver data | + +### Database Tables +- `driver` — driver records +- `driverToken` — FCM tokens + +## Stage 2: Go Online + +### Screen +- **File**: `siro_driver/lib/views/home/Captin/driver_map_page.dart` → `PassengerLocationMapPage` +- **Controller**: `HomeCaptainController`, `MapSocketController` + +### State Flow +1. Driver presses "Go Online" button +2. `HomeCaptainController.startOnlineStatus()` +3. Location service begins continuous GPS updates +4. WebSocket connects to receive ride offers + +### Background Service +- **File**: `siro_driver/lib/controller/functions/background_service.dart` +- `BackgroundServiceHelper.initialize()` — starts Android foreground service +- Location updates sent every few seconds to location server + +### APIs +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$location/add.php` | POST | driver_id, lat, lng, heading, speed, status, carType | success | +| `$server/ride/notificationCaptain/addWaitingRide.php` | POST | driver_id | waiting status | +| `$endPoint/ride/notificationCaptain/getRideWaiting.php` | POST | driver_id | pending rides | + +### Database Tables +- `car_locations` — real-time driver GPS (SPATIAL index) +- `car_tracks` — historical location tracks +- `notificationCaptain` — pending ride notifications + +### Real-time Operations +- **WebSocket Connect**: `socket_intaleq/driver_socket.php` +- **Publish**: `driver_online`, `driver_location` +- **Subscribe**: `ride_offer`, `ride_accepted`, `ride_cancelled` + +## Stage 3: Ride Offer Reception + +### Screen +- **Widget**: `order_request_page.dart` +- **Overlay**: `TripOverlayPlugin` (native Android overlay showing incoming trip) + +### State Flow +1. FCM push received with `category=Order` or `category=OrderSpeed` +2. `backgroundMessageHandler()` processes push data +3. Extracts `DriverList` array from message data +4. Shows overlay via `TripOverlayPlugin.showOverlay(tripData, autoCloseSeconds: 15)` +5. Stores pending trip in secure storage: `pending_driver_list` + +### Data Package (DriverList array indices) +| Index | Field | +|-------|-------| +| 0 | passengerLat | +| 1 | passengerLng | +| 2 | paymentAmount | +| 3 | destLat | +| 4 | destLng | +| 5 | distance | +| 7 | passengerId | +| 8 | passengerName | +| 9 | passengerToken | +| 10 | phone | +| 11 | distance (dup) | +| 13 | walletChecked | +| 15 | durationToPassenger | +| 16 | orderId | +| 18 | driverId | +| 19 | durationOfRide | +| 20-25 | steps (waypoints) | +| 26 | fare/totalCost | +| 28 | email | +| 29 | startNameLocation | +| 30 | endNameLocation | +| 31 | carType | +| 32 | kazan | + +### Notifications +- **Local**: Custom notification with "ding.wav" sound, accept/reject buttons +- **Overlay**: Android system overlay with trip info + accept/reject + +## Stage 4: Accept Ride + +### Screen +- **Function**: `_processAcceptOrder(List data)` in `siro_driver/lib/main.dart` + +### State Flow +1. Overlay accept button → `TripOverlayPlugin.onTripAccepted` fires +2. Or in-app accept button → `HomeCaptainController.acceptOrder()` +3. Shows loading dialog +4. Calls API to accept: +``` +POST {$rideServerSide}/rides/acceptRide.php + payload: { id: orderId, rideTimeStart, status: 'Apply', passengerToken, driver_id } +``` +5. On success → navigate to `PassengerLocationMapPage` with ride args +6. On failure (already taken) → show "طلب أخذه سائق آخر" dialog + +### Variables Written +| Variable | Value | +|----------|-------| +| `BoxName.statusDriverLocation` | `'on'` | +| `BoxName.rideStatus` | `'Apply'` | +| `BoxName.rideArguments` | ride args map | + +### APIs +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$rideServerSide/rides/acceptRide.php` | POST | orderId, rideTimeStart, status, passengerToken, driver_id | ride accepted | +| `$server/ride/driver_order/add.php` | POST | driver_id, order_id, status='applied' | log | + +### GIS Operations +- Route from driver → passenger pickup +- Google Maps directions URL generated + +## Stage 5: Navigate to Pickup + +### Screen +- **File**: `siro_driver/lib/views/home/Captin/driver_map_page.dart` +- **Navigation**: `siro_driver/lib/controller/home/navigation/navigation_controller.dart` + +### State Flow +- `RideStatus: 'Apply'` → navigate to passenger +- **Timer**: `startTimerFromDriverToPassengerAfterApplied()` — ETA countdown + +### GIS Operations +- **Route Drawing**: Polyline from driver → passenger pickup +- **Voice Navigation**: TTS navigation instructions +- **Deviation Detection**: Re-route if off path + +### Real-time +- **WebSocket Publish**: `driver_location` with ride context +- **WebSocket Subscribe**: `passenger_location`, `ride_cancelled` + +## Stage 6: Arrived at Pickup + +### Action +- Driver presses "I've Arrived" button +- API call updates ride status to `Arrived` +- 5-minute passenger waiting timer starts + +### APIs +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$rideServerSide/ride/rides/updateStausFromSpeed.php` | POST | ride_id, status='Arrived' | success | + +### Notifications +- **Push**: FCM sent to passenger "Driver has arrived" +- **In-App**: Navigation state changes to "Waiting for passenger" + +## Stage 7: Start Ride + +### Action +- Driver presses "Start Ride" button +- API call updates ride status to `Begin` +- Trip officially begins + +### APIs +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$rideServerSide/ride/rides/updateStausFromSpeed.php` | POST | ride_id, status='Begin' | success | + +### GIS Operations +- Route updates from current position → passenger destination +- Live ETA recalculated + +## Stage 8: Active Ride + +### Screen +- **File**: `driver_map_page.dart` (same screen, different state) + +### State Flow +- `RideStatus: 'Begin'` → navigating to destination +- Live trip timer and fare counter displayed + +### GIS Operations +- **Route Drawing**: Blue polyline to destination +- **ETA Updates**: Continuous recalculation +- **Driver Behavior**: Speed, hard brakes, distance monitored + +### Database Tables +- `driver_behavior` — speed, brakes, score per trip + +## Stage 9: End Ride + +### Action +- Driver presses "End Ride" button +- API call updates ride status to `Finished` +- Payment screen shown + +### APIs +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$rideServerSide/ride/rides/updateStausFromSpeed.php` | POST | ride_id, status='Finished' | success | +| `$paymentServer/ride/payment/add.php` | POST | amount, payment_method, passengerID, rideId, driverID | payment record | + +### GIS Operations +- **Stop Location Tracking**: End ride location published +- **Route Cleanup**: Clear map route + +### Real-time +- **WebSocket Publish**: `ride_finished` event +- **WebSocket Disconnect**: Ride room cleanup + +## Stage 10: Rating & Payment + +### Screen +- **Rate**: Rate passenger bottom sheet +- **Payment**: Cash confirmation or digital payment + +### APIs +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$server/ride/rate/addRateToPassenger.php` | POST | passenger_id, driverID, rideId, rating, comment | success | +| `$server/ride/payment/get.php` | POST | driver_id | today's earnings | + +## Stage 11: Go Offline & Earnings + +### Screen +- **Wallet**: `siro_driver/lib/views/home/Captin/wallet_page.dart` +- **Earnings**: `earnings_page.dart` + +### APIs +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$walletDriver/getWalletByDriver.php` | POST | driver_id | wallet balance | +| `$paymentServer/ride/driverPayment/get.php` | POST | driver_id | payment history | +| `$server/ride/payment/getCountRide.php` | POST | driver_id | ride count today | + +### Go Offline Flow +1. Driver presses "Go Offline" +2. `HomeCaptainController.stopOnlineStatus()` +3. Location stops updating +4. WebSocket disconnects +5. `car_locations.status` set to `'off'` + +--- + +## Navigation Route Map (Driver) + +``` +SplashScreen (/) + → [JWT exists?] + → Yes → PassengerLocationMapPage (/passenger-location-map) + → No → LoginPage → OTPPage → PassengerLocationMapPage +PassengerLocationMapPage + → [Online] → WebSocket connects, location streaming starts + → [Ride Offer via Overlay/FCM] → Accept → PassengerLocationMapPage (with ride) + → Navigate to pickup → Arrived → Start Ride → Active Ride → End Ride + → [After Ride] → Rate page → Earnings update → Back to map + → [Offline] → WebSocket disconnects, location stops +``` + +## Driver App Background Service Architecture + +``` +┌──────────────────────────────────────────────────────┐ +│ Driver App (siro_driver) │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Main Isolate (flutter) │ │ +│ │ - AppBindings, GetX Controllers │ │ +│ │ - TripOverlayPlugin.listen() │ │ +│ │ - _processAcceptOrder / Reject │ │ +│ └──────────────────┬───────────────────────────┘ │ +│ │ │ +│ ┌──────────────────┴───────────────────────────┐ │ +│ │ Background Isolate (FCM Handler) │ │ +│ │ - backgroundMessageHandler() │ │ +│ │ - Shows TripOverlay (autoClose: 15s) │ │ +│ │ - Writes pending_driver_list to SecureStore │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────┴───────────────────────────┐ │ +│ │ Android Foreground Service │ │ +│ │ - BackgroundServiceHelper.initialize() │ │ +│ │ - LocationService: continuous GPS updates │ │ +│ │ - Channels: driver_service, location_service │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────┴───────────────────────────┐ │ +│ │ Native Overlay (Android, Kotlin/Swift) │ │ +│ │ - TripOverlayPlugin (custom native plugin) │ │ +│ │ - Shows incoming trip data overlay │ │ +│ │ - Accept/Reject buttons → MethodChannel │ │ +│ └──────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────┘ \ No newline at end of file diff --git a/knowledge/PASSENGER_JOURNEY.md b/knowledge/PASSENGER_JOURNEY.md new file mode 100644 index 0000000..5da65be --- /dev/null +++ b/knowledge/PASSENGER_JOURNEY.md @@ -0,0 +1,288 @@ +# PASSENGER_JOURNEY.md — Complete Passenger Lifecycle + +## Stage 1: App Launch + +### Screen +- **Route**: `/` → SplashScreen +- **File**: `siro_rider/lib/splash_screen_page.dart` +- **Controller**: `SplashScreenController` (in `siro_rider/lib/controller/home/splash_screen_controlle.dart`) + +### State Flow +- **Type**: GetX Controller (Custom Animation) +- **Events**: `controller.init()` → animations play → `controller.checkInitialStatus()` +- **States**: Splash animation → Progress bar → Navigate based on auth status + +### Variables +| Variable | Storage | Key | +|----------|---------|-----| +| jwt | GetStorage | `box.read(BoxName.jwt)` | +| passengerID | GetStorage | `box.read(BoxName.passengerID)` | +| driverID | GetStorage | `box.read(BoxName.driverID)` | +| language | GetStorage | `box.read(BoxName.lang)` | +| themeMode | GetStorage | theme preference | +| packageInfo | GetStorage | `BoxName.packagInfo` | + +### APIs +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$server/auth/packageInfo.php` | POST | platform, appName | version info | + +### Database Tables +- `packageInfo` — app version tracking + +## Stage 2: Authentication Check + +### State Flow: SplashScreenController +1. Check `box.read(BoxName.jwt)` existence +2. If JWT exists → check ride status (`_checkInitialRideStatus()`) → navigate to MapScreen or Login +3. If no JWT → navigate to Onboarding/Login + +### Navigation Decision +``` +SplashScreen + → JWT exists? + → Yes → MapPagePassenger + → No → OnboardingPage (first time) or LoginPage +``` + +### Screens +| Screen | Route | File | +|--------|-------|------| +| Onboarding | `/onboarding` | `siro_rider/lib/onbording_page.dart` | +| Login | `/login` | `siro_rider/lib/views/auth/login_page.dart` | +| Register | `/register` | `siro_rider/lib/views/auth/register_page.dart` | +| OTP | `/otp` | `siro_rider/lib/views/auth/otp_page.dart` | + +### APIs +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$server/auth/login.php` | POST | phone, password, fingerprint | JWT, passenger data | +| `$server/auth/signup.php` | POST | phone, email, password, name, ... | user created | +| `$server/auth/loginFromGooglePassenger.php` | POST | google token | JWT, passenger data | +| `$server/auth/checkPhoneNumberISVerfiedPassenger.php` | POST | phone | verification status | +| `$auth/otpmessage.php` | POST | phone | OTP sent | +| `$auth/verifyOtpMessage.php` | POST | phone, otp | verified status | + +### Models +- **UserModel** → `passengers` table +- **TokenModel** → `tokens` table + +## Stage 3: Map Screen — Ride Request + +### Screen +- **File**: `siro_rider/lib/views/home/map_page_passenger.dart` +- **Controllers** (all permanent in AppBindings): + - `MapEngineController` — map rendering + - `MapSocketController` — WebSocket management + - `LocationSearchController` — place search + - `NearbyDriversController` — nearby driver list + - `RideLifecycleController` — ride state machine + - `UiInteractionsController` — UI bottom sheets + +### State Flow: RideLifecycleController +- **RideState enum**: `noRide → searching → driverApplied → driverArrived → inProgress → finished → preCheckReview → cancelled` + +### Search Flow +1. Passenger enters destination → `LocationSearchController.searchPlaces()` +2. Price estimate fetched via fare calculation +3. Passenger selects car type → confirms ride +4. Ride request sent → status = `waiting` → transitions to `searching` + +### APIs (Ride Request) +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$rideServerSide/ride/rides/add.php` | POST | passenger_id, start_lat, start_lng, end_lat, end_lng, carType, price, ... | ride_id | +| `$rideServerSide/cancelRide/add.php` | POST | ride_id, passenger_id, note | cancellation | +| `$server/ride/promo/getPromoBytody.php` | POST | passenger_id | promo code | + +### Database Tables +- `waitingRides` — active ride requests +- `ride` — completed rides +- `promos` — promo codes + +### GIS Operations +- **Reverse Geocoding**: Map SaaS (`/api/geocoding/reverse`) +- **Search Geocoding**: Map SaaS (`/api/geocoding/search`) +- **Routing**: Map SaaS (`/api/maps/route`) or OSRM (`routesy.intaleq.xyz`) +- **ETA Calculation**: Local algorithm in `RideLifecycleController.updateRemainingRoute()` +- **Map Rendering**: `IntaleqMaps` (custom Flutter map plugin) + +## Stage 4: Searching for Driver + +### Screen +- **Widget**: `searching_captain_window.dart` +- **Timer**: `timer_for_cancell_trip_from_passenger.dart` + +### State Flow +- `RideState.searching` +- Polling loop checks `_totalSearchTimeoutSeconds` +- On timeout → `_showIncreaseFeeDialog()` + +### Real-time Operations +| Channel | Event | Direction | +|---------|-------|-----------| +| WebSocket | `driver_location_update` | Server → Passenger | +| WebSocket | `ride_accepted` | Server → Passenger | +| Polling | `getRideStatus` | Passenger → Server (fallback) | + +### Notifications +- **Local**: Timer tick notifications +- **Push**: When driver accepts via FCM + +### Failure Scenarios +| Scenario | Handling | +|----------|----------| +| No drivers found | Show increase fee dialog | +| Network failure | Fallback to polling, show error snackbar | +| Timeout | Auto-cancel, prompt retry | + +## Stage 5: Driver Accepted + +### Screen +- **Widget**: `driver_card_from_passenger.dart`, `driver_time_arrive_passenger.dart` +- **Function**: `processRideAcceptance()` + +### State Flow +- `RideState.driverApplied` +- **Events**: `processRideAcceptance(driverData)` +- **Transitions**: `applied → arrived` (when driver reaches pickup) + +### Variables Stored +| Variable | Description | +|----------|-------------| +| `dInfo` | Driver info (name, car, rating, phone) | +| `currentRideId` | Active ride ID | +| `rideData` | Full ride details (price, locations, timestamps) | +| `datadriverCarsLocationToPassengerAfterApplied` | Driver GPS route | + +### APIs +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$rideServerSide/ride/rides/getRideStatus.php` | POST | ride_id | current status | +| `$location/getDriverCarsLocationToPassengerAfterApplied.php` | POST | driver_id | GPS location | + +### GIS Operations +- **Route Drawing**: Yellow polyline from driver → passenger +- **Driver Marker**: Real-time car icon following WebSocket updates +- **Deviation Detection**: `checkAndRecalculateIfDeviated()` with 50m threshold + +### Real-time Operations +- **WebSocket Connect**: Join ride room +- **Subscribe**: `driver_location_update` events +- **Driver Tracking**: `handleDriverLocationUpdate()` → stop polling after 3 reliable updates + +### Notifications +- **Push**: FCM when driver accepts +- **In-App**: `RideLiveNotification.showDriverOnWay()` +- **iOS Live Activity**: `IosLiveActivityService.startRideActivity()` + +## Stage 6: Driver Arrived + +### Screen +- **Widget**: `driver_time_arrive_passenger.dart`, `ride_begin_passenger.dart` +- **Function**: `processDriverArrival()` + +### State Flow +- `RideState.driverArrived` +- **Events**: `processDriverArrival("polling")` or via socket +- **Timer**: 5-minute waiting timer starts +- **Pre-drawing**: Blue route from pickup → destination pre-calculated + +### Notifications +- **In-App Dialog**: `uiInteractions.driverArrivePassengerDialoge()` +- **Push**: Driver arrived notification + +## Stage 7: Ride In Progress + +### Screen +- **Widget**: `ride_begin_passenger.dart`, `passengerRideLoctionWidget.dart` + +### State Flow +- `RideState.inProgress` +- **Events**: `processRideBegin()` +- **Timer**: `rideIsBeginPassengerTimer()` — live ride counter + +### GIS Operations +- **Blue Route**: Final path from driver → destination +- **Live ETA**: Updated via `updateRemainingRoute()` (local calculation) +- **Camera Tracking**: Follows driver, zoom adjusts by speed +- **Deviation Guard**: Continuous deviation checking, re-route if >50m off path + +### Real-time Operations +- **WebSocket**: Continuous `driver_location_update` streaming +- **Polling Fallback**: If socket disconnected, poll `getRideStatus` + +### iOS Live Activity +- `IosLiveActivityService` — Dynamic Island / Lock Screen widget +- `RideWidget` in ios/RideWidget — SwiftUI widget + +## Stage 8: Ride Finished — Payment & Rating + +### Screen +- **Rating**: `siro_rider/lib/views/Rate/rate_captain.dart`, `rating_driver_bottom.dart` +- **Payment**: `payment_method.page.dart`, `cash_confirm_bottom_page.dart` + +### State Flow +- `RideState.finished` → `processRideFinished()` +- Disposes ride socket, stops all timers +- Navigates to RateDriverFromPassenger with driver_id, ride_id, bill + +### APIs +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$server/ride/rate/addRateToDriver.php` | POST | passenger_id, driver_id, ride_id, rating, comment | success | +| `$paymentServer/ride/payment/add.php` | POST | amount, payment_method, passengerID, rideId, driverID | payment record | +| `$paymentServer/ride/passengerWallet/addPaymentTokenPassenger.php` | POST | passengerId, amount, token | wallet deduction | +| `$wallet/getAllPassengerTransaction.php` | POST | passenger_id | transaction history | +| `$server/ride/tips/add.php` | POST | driverID, passengerID, rideID, tipAmount | tip saved | + +### Payment Methods Flow +1. **Cash**: Show confirmation dialog, driver marks received +2. **Wallet**: Deduct from `passengerWallet` balance +3. **Visa (PayMob)**: `payWithPayMobCardPassenger` → `paymetVerifyPassenger` +4. **MTN**: `payWithMTNStart` → `payWithMTNConfirm` +5. **Syriatel**: `payWithSyriatelStart` → `payWithSyriatelConfirm` + +### Database Tables +- `payments` — ride payment records +- `ratingDriver` — driver ratings +- `ratingPassenger` — passenger ratings +- `tips` — tips given +- `passengerWallet` — wallet balance + +### Notifications +- **Push**: Receipt notification +- **In-App**: Rating prompt + +## Stage 9: Post-Ride + +### Screen +- **Profile**: `passenger_profile_page.dart` +- **Wallet**: `passenger_wallet.dart` +- **History**: `order_history.dart` +- **Promos**: `promos_passenger_page.dart` + +### APIs +| Endpoint | Method | Input | Output | +|----------|--------|-------|--------| +| `$wallet/getWalletByPassenger.php` | GET | passenger_id | wallet balance | +| `$profile/get.php` | POST | passenger_id | profile data | +| `$rideServerSide/ride/rides/get.php` | POST | passenger_id | ride history | +| `$promo/get.php` | POST | passenger_id | available promos | +| `$server/ride/invitor/get_passenger_referrals.php` | POST | passenger_id | referral data | + +--- + +## Navigation Route Map + +``` +SplashScreen (/) + → [JWT exists?] + → Yes → MapPagePassenger (/home/map_page_passenger) + → No → OnboardingPage → LoginPage → OTPPage → MapPagePassenger +MapPagePassenger + → [Ride states trigger widgets] + → [Menu] → Profile (/profile), Wallet (/wallet), Settings, Promos + → [Rating] → RateCaptain page + → [Contact] → ContactUsPage (/contactSupport) + → [Share] → ShareAppPage (/shareApp) \ No newline at end of file diff --git a/knowledge/PROJECT_OVERVIEW.md b/knowledge/PROJECT_OVERVIEW.md new file mode 100644 index 0000000..9a3c58a --- /dev/null +++ b/knowledge/PROJECT_OVERVIEW.md @@ -0,0 +1,135 @@ +# PROJECT_OVERVIEW — Siro (Intaleq) Ride-Hailing Platform + +## Business Purpose + +Siro (Intaleq) is a multi-region ride-hailing platform connecting passengers with drivers (captains). It operates across Syria, Jordan, and Egypt. The platform supports multiple car types (Speed, Comfort, Family, Delivery, Electric, Van, Bike) with dynamic pricing via a "Kazan" percentage-based commission system. + +### Core Ride-Hailing Workflow +1. Passenger requests ride → System searches nearby drivers → Driver accepts → Navigate to pickup → Ride begins → Ride completes → Payment processed → Rating submitted + +### User Types +- **Passenger** (Rider) — Requests rides via siro_rider app +- **Driver** (Captain) — Accepts rides via siro_driver app +- **Admin** — Manages system via siro_admin app (Flutter Web/PWA) +- **Service Agent** — Manages driver registration via siro_service app +- **Employee** — Staff managing drivers, passengers, complaints + +### Driver Types +| Type | Code | Description | +|------|------|-------------| +| Speed | Speed | Standard rides | +| Comfort | Comfort | Premium rides | +| Family | Family | Larger vehicle | +| Delivery | Delivery | Package delivery | +| Free/Blash | Blash | Economy | +| Late | Late | Off-peak | +| Heavy | Heavy | Cargo | +| Nature | Nature | Scenic routes | +| Electric | Electric | EV | +| Pink Bike | PinkBike | Motorcycle | +| Van | Van | Minibus | +| Female Driver | FemalDriver | Women-only | + +### Payment Methods +- Cash +- Visa/Credit Card (PayMob) +- Wallet (internal balance) +- MTN Mobile Money +- Syriatel Mobile Money +- E-Cash +- Stripe + +### External Integrations +| Service | Purpose | +|---------|---------| +| Google Maps | Map rendering, geocoding, routing | +| Here Maps | Place autocomplete | +| Map SaaS (intaleqapp.com) | Custom routing, reverse geocoding, places | +| OpenStreetMap (routec/routesy) | OSRM routing | +| Firebase | Push notifications, analytics, crashlytics | +| PayMob | Payment gateway | +| Twilio | SMS verification | +| WhatsApp Cloud API | OTP delivery | +| Azure OCR | Document scanning | +| OpenAI GPT | Document data extraction | +| Llama AI | Document data extraction | +| Agora | Voice/video calls | +| WebRTC | Signaling service | +| SMS Kazumi | SMS provider (Egypt) | + +--- + +## System Modules + +| Module | Purpose | Dependencies | Main Files | +|--------|---------|--------------|------------| +| **Authentication** | Login, signup, OTP, JWT management, Google/Apple auth | Firebase, Twilio, WhatsApp | `siro_rider/lib/controller/auth/*.dart`, `backend/auth/*.php` | +| **Dispatching** | Ride request → driver matching → offer → accept | WebSocket, MySQL GIS | `socket_intaleq/*.php`, `backend/ride/*.php` | +| **Matching** | Nearby driver search via spatial queries | MySQL SPATIAL indexes | `backend/ride/location/*.php` | +| **Maps & GIS** | Map rendering, routing, geocoding, driver tracking | Google Maps, Map SaaS, OSRM | `siro_rider/lib/controller/home/map/*.dart` | +| **Payments** | Ride payment, wallet, PayMob, MTN, Syriatel, E-Cash | PayMob, Stripe | `siro_rider/lib/controller/payment/*.dart`, `backend/ride/payment/*.php` | +| **Wallet** | Passenger & driver balance management | Payment server (walletintaleq.xyz) | `siro_rider/lib/controller/payment/passenger_wallet_history_controller.dart` | +| **Notifications** | Push (FCM), local, in-app notifications | Firebase | `siro_rider/lib/controller/firebase/*.dart` | +| **Chat** | In-app messaging between driver & passenger | PHP API | `backend/ride/chat/send_message.php` | +| **Rating** | Post-ride driver & passenger ratings | MySQL | `siro_rider/lib/controller/rate/*.dart`, `backend/ride/rate/*.php` | +| **Promotions** | Promo codes, referral rewards | MySQL | `siro_rider/lib/controller/home/profile/promos_controller.dart` | +| **AI Services** | OCR document scanning, data extraction | Azure OCR, OpenAI, Llama | `siro_rider/lib/controller/functions/crud.dart` (getLlama, getChatGPT, arabicTextExtractByVisionAndAI) | +| **Admin Functions** | Dashboard, driver/passenger management, analytics | All backend APIs | `siro_admin/lib/controller/admin/*.dart` | +| **Realtime Tracking** | WebSocket driver location streaming, passenger tracking | Socket.IO (PHP) | `socket_intaleq/*.php`, `siro_rider/lib/controller/home/map/map_socket_controller.dart` | +| **Referral System** | Unified referral codes for drivers & passengers | MySQL | `backend/migration_referral_system.sql`, `backend/ride/invitor/*.php` | +| **Complaints** | Post-ride issue resolution | MySQL | `siro_rider/lib/controller/home/profile/complaint_controller.dart`, `backend/Admin/AdminRide/` | +| **Emergency** | SOS signals, safety features | Agora, WebRTC | `siro_rider/lib/services/emergency_signal_service.dart` | + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────┐ +│ Mobile Apps (Flutter) │ +│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │ +│ │ siro_rider │ │ siro_driver │ │ siro_admin │ │ +│ │ (Passenger) │ │ (Captain) │ │ (Admin) │ │ +│ └──────┬──────┘ └──────┬───────┘ └─────┬──────┘ │ +└─────────┼─────────────────┼─────────────────┼────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────┐ +│ API Gateway (api.intaleq.xyz) │ +│ ┌───────────┐ ┌──────────┐ ┌───────────────────┐ │ +│ │ Auth API │ │ Ride API │ │ Payment API │ │ +│ └───────────┘ └──────────┘ └───────────────────┘ │ +└──────────────────────┬──────────────────────────────┘ + │ +┌──────────────────────┴──────────────────────────────┐ +│ Backend Servers (PHP) │ +│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │ +│ │ Main API │ │ Ride API │ │ Payment Server │ │ +│ │ intaleq_v3│ │ rides. │ │ walletintaleq.xyz│ │ +│ └──────────┘ └──────────┘ └───────────────────┘ │ +│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │ +│ │ Location │ │ Socket │ │ Map SaaS │ │ +│ │ location.│ │ rides. │ │ map-saas. │ │ +│ └──────────┘ └──────────┘ └───────────────────┘ │ +└──────────────────────┬──────────────────────────────┘ + │ +┌──────────────────────┴──────────────────────────────┐ +│ MySQL Databases │ +│ ┌────────────────┐ ┌─────────────────────┐ │ +│ │ intaleqDB1 │ │ intaleq-ridesDB │ │ +│ │ (Primary Main) │ │ (Ride-specific) │ │ +│ └────────────────┘ └─────────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Mobile | Flutter (Dart) with GetX state management | +| Backend | PHP (native, no framework) | +| Database | MySQL 8.0 with GIS (SPATIAL indexes, POINT columns) | +| Realtime | PHP WebSockets (Socket.IO compatible) | +| Maps | Google Maps (primary), Intaleq Maps (custom), OSRM routing | +| Payments | PayMob, custom wallet server | +| Auth | JWT, Firebase Auth, Google Sign-In, Apple Sign-In | +| Storage | GetStorage (local), FlutterSecureStorage | +| Push | Firebase Cloud Messaging (FCM) | \ No newline at end of file diff --git a/knowledge/SYSTEM_ARCHITECTURE.md b/knowledge/SYSTEM_ARCHITECTURE.md new file mode 100644 index 0000000..a3621f9 --- /dev/null +++ b/knowledge/SYSTEM_ARCHITECTURE.md @@ -0,0 +1,253 @@ +# SYSTEM_ARCHITECTURE.md — Full System Architecture + +## Layer Architecture + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ UI LAYER (Flutter Widgets) │ +│ │ +│ siro_rider: │ +│ Routes: /, /shareApp, /wallet, /profile, /contactSupport │ +│ Screens: SplashScreen, LoginPage, MapPagePassenger, Wallet, etc. │ +│ │ +│ siro_driver: │ +│ Routes: /, /OrderRequestPage, /passenger-location-map │ +│ Screens: SplashScreen, LoginPage, PassengerLocationMapPage, etc. │ +│ │ +│ siro_admin: │ +│ Routes: /login, /admin/dashboard, /admin/captain, /admin/rides │ +│ │ +│ siro_service: │ +│ Service agent app for driver registration & management │ +└──────────────────────┬─────────────────────────────────────────────┘ + │ GetX (Get.find, Get.put, Obx) +┌──────────────────────┴─────────────────────────────────────────────┐ +│ STATE LAYER (GetX Controllers) │ +│ │ +│ Controllers are registered in: AppBindings (permanent) │ +│ │ +│ Rider: │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ MapEngineController → Map rendering, camera, markers │ │ +│ │ MapSocketController → WebSocket lifecycle management │ │ +│ │ RideLifecycleController → Ride state machine (RideState enum)│ │ +│ │ LocationSearchController → Place search & autocomplete │ │ +│ │ NearbyDriversController → Nearby driver list management │ │ +│ │ UiInteractionsController → Bottom sheets, dialogs │ │ +│ │ LoginController → Auth & JWT management │ │ +│ │ PaymentController → Payment processing │ │ +│ │ SplashScreenController → App init & auth check │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ Driver: │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ HomeCaptainController → Online/offline, ride management │ │ +│ │ MapSocketController → WebSocket ride offers │ │ +│ │ NavigationController → Turn-by-turn navigation │ │ +│ │ BackgroundServiceHelper → Android foreground service │ │ +│ │ LocationController → GPS tracking │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└──────────────────────┬─────────────────────────────────────────────┘ + │ Dependency Injection via GetX +┌──────────────────────┴─────────────────────────────────────────────┐ +│ REPOSITORY LAYER (CRUD + Services) │ +│ │ +│ CRUD class (siro_rider/lib/controller/functions/crud.dart) │ +│ - post() → Main API POST with JWT + Device-FP headers │ +│ - get() → API GET (POST method) with retry logic │ +│ - postWallet() → Payment server POST with JWT + HMAC + FP │ +│ - getWallet() → Payment server GET with JWT + HMAC + FP │ +│ - sendWhatsAppAuth() → WhatsApp OTP delivery │ +│ - getAgoraToken() → Agora voice/video call tokens │ +│ - getLlama() → Llama AI data extraction │ +│ - getChatGPT() → OpenAI GPT data extraction │ +│ - arabicTextExtractByVisionAndAI() → Azure OCR │ +│ - getGoogleApi() → Google Maps API wrapper │ +│ - getHereMap() → Here Maps API wrapper │ +│ - getMapSaas()/postMapSaas() → Custom map service │ +│ - sendVerificationRequest() → Twilio verification │ +│ - postPayMob() → PayMob payment gateway │ +│ │ +│ NetGuard (network/connection_check.dart) → Internet monitoring │ +└──────────────────────┬─────────────────────────────────────────────┘ + │ HTTP Calls +┌──────────────────────┴─────────────────────────────────────────────┐ +│ SERVICE LAYER (PHP Backend) │ +│ │ +│ Main API: api.intaleq.xyz/intaleq_v3 │ +│ Ride API: rides.intaleq.xyz/intaleq │ +│ Location API: location.intaleq.xyz/intaleq/ride/location │ +│ Payment API: walletintaleq.intaleq.xyz/v2/main │ +│ Map SaaS: map-saas.intaleqapp.com/api │ +│ OSRM Route: routec.intaleq.xyz / routesy.intaleq.xyz │ +└──────────────────────┬─────────────────────────────────────────────┘ + │ MySQL + WebSocket +┌──────────────────────┴─────────────────────────────────────────────┐ +│ DATA LAYER (MySQL Databases) │ +│ │ +│ Database: intaleqDB1 (primary) │ +│ Database: intaleq-ridesDB (ride-specific) │ +│ Tables: 60+ tables covering users, rides, payments, etc. │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Dependency Graph + +``` +Rider App (siro_rider) ───→ Main API ─────→ intaleqDB1 + ├──→ Map SaaS ───────→ Custom Map Service + ├──→ OSRM ───────────→ OpenStreetMap + ├──→ Payment API ────→ Wallet DB + ├──→ Firebase ───────→ FCM + ├──→ WebSocket ──────→ Ride Socket Server + └──→ Google Maps ────→ Google APIs + +Driver App (siro_driver) ───→ Main API ─────→ intaleqDB1 + ├──→ Location API ───→ car_locations + ├──→ Payment API ────→ Wallet DB + ├──→ WebSocket ──────→ Driver Socket Server + ├──→ Firebase ───────→ FCM + └──→ Google Maps ────→ Google APIs + +Admin App (siro_admin) ────→ Main API ─────→ intaleqDB1 + ├──→ Payment API + ├──→ Firebase + └──→ Internal Services + +Socket Server (socket_intaleq) ──→ MySQL + ├──→ driver_socket.php ──→ Driver Socket.IO + └──→ passenger_socket.php ──→ Passenger Socket.IO + +Ride Server (rides.intaleq.xyz) ──→ MySQL (intaleq-ridesDB) +Location Server (location.intaleq.xyz) ──→ MySQL (car_locations) +Payment Server (walletintaleq.intaleq.xyz) ──→ Wallet MySQL +``` + +--- + +## External Services + +| Service | Integration Point | Auth Method | Purpose | +|---------|-----------------|-------------|---------| +| **Google Maps** | `https://maps.googleapis.com/maps/api/` | API Key | Map rendering, geocoding | +| **Here Maps** | `https://autosuggest.search.hereapi.com/v1/autosuggest` | API Key | Place autocomplete | +| **Map SaaS** | `https://map-saas.intaleqapp.com/api/` | x-api-key | Custom routing, geocoding | +| **OSRM Server** | `https://routec.intaleq.xyz/route` | None | OpenStreetMap routing | +| **Firebase** | Firebase SDK | google-services.json | FCM, Analytics, Crashlytics | +| **PayMob** | PayMob SDK | API Key + HMAC | Payment gateway (Visa/Mastercard) | +| **Twilio** | Twilio Verify API | Account SID + Auth Token | SMS OTP verification | +| **WhatsApp Cloud** | Graph API | OAuth Token | WhatsApp OTP delivery | +| **Azure OCR** | `https://ocrhamza.cognitiveservices.azure.com/` | Subscription Key | Document text extraction | +| **OpenAI** | OpenAI API | API Key | Document data extraction (GPT-3.5) | +| **Llama AI** | Llama API | Bearer Token | Document data extraction | +| **Agora** | Agora SDK | App Certificate | Voice/video calls | +| **SMS Kazumi** | `https://sms.kazumi.me/api/` | API Key | SMS provider (Egypt) | + +--- + +## Security Architecture + +### Authentication +| Mechanism | Implementation | +|-----------|---------------| +| **JWT** | Custom JWT tokens issued at login, validated on each request | +| **Fingerprint** | Device fingerprint hashed with SHA-256, stored in JWT payload | +| **HMAC** | HMAC authentication for payment server requests | +| **Bearer Tokens** | Standard Bearer token in Authorization header | +| **Social Auth** | Google Sign-In, Apple Sign-In | + +### Authorization +- **Role-based**: Passenger, Driver, Admin, Service Agent +- **Device Binding**: X-Device-FP header verified against JWT fingerprint claim +- **Wallet Auth**: Separate JWT + HMAC for payment operations + +### Token Handling +| Token | Storage | Expiry | Refresh | +|-------|---------|--------|---------| +| JWT (main) | GetStorage | 1 hour (default) | `getJWT()` auto-refresh on 401 | +| JWT (wallet) | GetStorage | 1 hour | `getJwtWallet()` auto-refresh | +| FCM Token | GetStorage + DB | Firebase-managed | On app start | +| Refresh Token | FlutterSecureStorage | Long-lived | On login | + +### Encryption +- **AES-256-CBC**: Custom encrypt/decrypt for sensitive data +- **SHA-256**: Device fingerprint hashing +- **Base64**: Basic auth credentials encoding +- **SSL/TLS**: All API calls over HTTPS + +### Secure Storage +| Data | Storage Method | +|------|---------------| +| JWT tokens | GetStorage (encrypted box) | +| Refresh tokens | FlutterSecureStorage (Keychain/Keystore) | +| Fingerprint | GetStorage (encrypted) | +| HMAC keys | GetStorage (encrypted) | + +### Session Management +- Auto-logout on token expiry (401 response) +- Fingerprint migration tools in admin panel +- Login attempt rate limiting (`login_attempts` table) + +--- + +## Caching Strategy + +| Cache Type | Implementation | Data Cached | +|-----------|---------------|-------------| +| **GetStorage** (Local) | Key-value store in app sandbox | JWT, user ID, preferences, ride state | +| **FlutterSecureStorage** | OS-level encrypted storage | Refresh tokens, sensitive data | +| **SQLite (DbSql)** | Local SQLite database | Offline maps, ride history | +| **Memory Cache** | GetX controller state | Active ride data, driver list, map markers | +| **Server Cache** | MySQL + Query Cache | Driver locations (car_locations), places | +| **In-Memory (NetGuard)** | Singleton notification state | Network error debouncing | + +--- + +## WebSocket Architecture + +``` +┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ Rider App │◄───────►│ Passenger Socket │◄───────►│ MySQL DB │ +│ │ │ (PHP) │ │ │ +└──────────────┘ └──────────────────┘ └──────────────┘ + │ + │ (Internal IPC) + │ +┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ Driver App │◄───────►│ Driver Socket │◄───────►│ MySQL DB │ +│ │ │ (PHP) │ │ │ +└──────────────┘ └──────────────────┘ └──────────────┘ +``` + +**Socket Events:** +- `driver_location_update` — Real-time driver GPS to passenger +- `ride_accepted` — Driver accepted notification +- `ride_cancelled` — Ride cancellation event +- `ride_finished` — Ride completion event +- `passenger_location` — Passenger location (for driver) +- `driver_online` / `driver_offline` — Online status changes + +--- + +## Admin Module Structure + +| Module | Controller | Views | +|--------|-----------|-------| +| Dashboard | `DashboardController`, `DashboardV2Controller` | `admin_home_page.dart` | +| Captain Management | `CaptainAdminController` | `captain.dart`, `captain_details.dart` | +| Passenger Management | `PassengerAdminController` | `passenger.dart`, `passenger_details_page.dart` | +| Ride Management | `RideAdminController`, `RideLookupController` | `rides.dart`, `ride_lookup_page.dart` | +| Financial | `FinancialV2Controller` | `financial_v2_page.dart` | +| Analytics | `AnalyticsV2Controller`, `StaticController` | `advanced_analytics_page.dart` | +| Complaints | `ComplaintController` | `complaint_list_page.dart` | +| Pricing/Kazan | `KazanController` | `kazan_editor_page.dart` | +| Promotions | `PromoController` | `promo_management_page.dart` | +| Wallet | `WalletAdminController` | `wallet.dart` | +| Driver Docs | `DriverDocsController` | `driver_documents_review_page.dart` | +| Security | `SecurityV2Controller` | `audit_logs_page.dart` | +| Quality | `QualityController` | `blacklist_page.dart`, `driver_scorecard_page.dart` | +| Staff | `StaffController` | `add_staff_page.dart`, `pending_admins_page.dart` | +| Server Monitor | `ServerMonitorController` | `monitor_server_page.dart` | +| Invoices | `GetAllInvoiceController` | `invoice_list_page.dart` | \ No newline at end of file diff --git a/review_diff.patch b/review_diff.patch new file mode 100644 index 0000000..58e8ddf --- /dev/null +++ b/review_diff.patch @@ -0,0 +1,973 @@ +diff --git a/backend/ride/rides/acceptRide.php b/backend/ride/rides/acceptRide.php +index 85c7832..487e2bc 100755 +--- a/backend/ride/rides/acceptRide.php ++++ b/backend/ride/rides/acceptRide.php +@@ -121,6 +121,8 @@ try { + c.color, + c.color_hex, + (SELECT ROUND(AVG(rating), 2) FROM ratingDriver WHERE driver_id = d.id) AS ratingDriver, ++ (SELECT COUNT(*) FROM ratingDriver WHERE driver_id = d.id) AS ratingCount, ++ (SELECT COUNT(*) FROM ride WHERE driver_id = d.id AND status IN ('Finished', 'finished')) AS completedRides, + dt.token + FROM driver d + LEFT JOIN CarRegistration c ON c.driverID = d.id +@@ -140,6 +142,16 @@ try { + } + $driverInfo['driverName'] = trim(($driverInfo['first_name'] ?? '') . ' ' . ($driverInfo['last_name'] ?? '')); + $driverInfo['ratingDriver'] = $driverInfo['ratingDriver'] ?: "5.0"; ++ $ratingValue = (float) $driverInfo['ratingDriver']; ++ $ratingCount = (int) ($driverInfo['ratingCount'] ?? 0); ++ $completedRides = (int) ($driverInfo['completedRides'] ?? 0); ++ if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) { ++ $driverInfo['driverTier'] = 'Professional driver'; ++ } elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) { ++ $driverInfo['driverTier'] = 'Trusted driver'; ++ } else { ++ $driverInfo['driverTier'] = 'Verified driver'; ++ } + } + + // ═══════════════════════════════════════════════════════════ +@@ -195,4 +207,4 @@ try { + } catch (PDOException $e) { + error_log("[accept_ride] CRITICAL: " . $e->getMessage()); + printFailure("Server error"); +-} +\ No newline at end of file ++} +diff --git a/backend/ride/rides/getRideOrderID.php b/backend/ride/rides/getRideOrderID.php +index f8532ff..7bc828e 100755 +--- a/backend/ride/rides/getRideOrderID.php ++++ b/backend/ride/rides/getRideOrderID.php +@@ -96,6 +96,17 @@ try { + FROM ratingDriver + WHERE ratingDriver.driver_id = :driverID_Sub + ) AS ratingDriver, ++ ( ++ SELECT COUNT(*) ++ FROM ratingDriver ++ WHERE ratingDriver.driver_id = :driverID_Sub ++ ) AS ratingCount, ++ ( ++ SELECT COUNT(*) ++ FROM ride ++ WHERE ride.driver_id = :driverID_Sub ++ AND ride.status IN ('Finished', 'finished') ++ ) AS completedRides, + + driverToken.token AS token + +@@ -143,6 +154,16 @@ try { + $finalData[$field] = $encryptionHelper->decryptData($finalData[$field]); + } + } ++ $ratingValue = (float) ($finalData['ratingDriver'] ?: 5.0); ++ $ratingCount = (int) ($finalData['ratingCount'] ?? 0); ++ $completedRides = (int) ($finalData['completedRides'] ?? 0); ++ if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) { ++ $finalData['driverTier'] = 'Professional driver'; ++ } elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) { ++ $finalData['driverTier'] = 'Trusted driver'; ++ } else { ++ $finalData['driverTier'] = 'Verified driver'; ++ } + } + + echo json_encode([ +@@ -155,4 +176,4 @@ try { + http_response_code(500); + echo json_encode(["status" => "failure", "message" => "Server Error: " . $e->getMessage()]); + } +-?> +\ No newline at end of file ++?> +diff --git a/siro_driver/lib/controller/firebase/firbase_messge.dart b/siro_driver/lib/controller/firebase/firbase_messge.dart +index 9de8eae..d183550 100755 +--- a/siro_driver/lib/controller/firebase/firbase_messge.dart ++++ b/siro_driver/lib/controller/firebase/firbase_messge.dart +@@ -76,15 +76,22 @@ class FirebaseMessagesController extends GetxController { + await fcmToken.subscribeToTopic("drivers"); // أو "users" حسب نوع المستخدم + print("Subscribed to 'drivers' topic ✅"); + +- FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? message) async { ++ FirebaseMessaging.instance ++ .getInitialMessage() ++ .then((RemoteMessage? message) async { + if (message != null && message.data.isNotEmpty) { + Log.print("🔔 FCM getInitialMessage payload: ${message.data}"); + String? category = message.data['category'] ?? message.data['type']; +- if (category == 'ORDER' || category == 'Order' || category == 'OrderVIP' || message.data.containsKey('DriverList')) { ++ if (category == 'ORDER' || ++ category == 'Order' || ++ category == 'OrderVIP' || ++ message.data.containsKey('DriverList')) { + String? myListString = message.data['DriverList']; + if (myListString != null && myListString.isNotEmpty) { +- await storage.write(key: 'pending_driver_list', value: myListString); +- Log.print("💾 Saved pending driver list to secure storage from getInitialMessage"); ++ await storage.write( ++ key: 'pending_driver_list', value: myListString); ++ Log.print( ++ "💾 Saved pending driver list to secure storage from getInitialMessage"); + } + } else { + Future.delayed(const Duration(milliseconds: 1500), () { +@@ -107,7 +114,6 @@ class FirebaseMessagesController extends GetxController { + // fireBaseTitles(message); + // } + }); +- FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {}); + + FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { + if (message.data.isNotEmpty) { +diff --git a/siro_driver/lib/controller/functions/background_service.dart b/siro_driver/lib/controller/functions/background_service.dart +index e112b36..5d9bec9 100644 +--- a/siro_driver/lib/controller/functions/background_service.dart ++++ b/siro_driver/lib/controller/functions/background_service.dart +@@ -9,7 +9,6 @@ import 'package:flutter_overlay_window/flutter_overlay_window.dart'; + import 'package:socket_io_client/socket_io_client.dart' as IO; + import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay; + import 'package:get_storage/get_storage.dart'; +-import 'package:geolocator/geolocator.dart' as geo; + import '../../constant/box_name.dart'; + import '../firebase/local_notification.dart'; + +@@ -129,40 +128,21 @@ Future onStart(ServiceInstance service) async { + service.stopSelf(); + }); + +- // 🔥 Location management in background isolate (Using Geolocator) +- geo.Position? latestPos; +- +- // Listen to location changes continuously in the background +- geo.Geolocator.getPositionStream( +- locationSettings: geo.AndroidSettings( +- accuracy: geo.LocationAccuracy.high, +- distanceFilter: 10, +- intervalDuration: const Duration(seconds: 10), +- ), +- ).listen((pos) { +- latestPos = pos; +- }); +- +- // 🔥 MERCY HEARTBEAT: Send location every 2 minutes to keep driver active in 'raids' +- Timer.periodic(const Duration(minutes: 2), (timer) async { +- if (socket != null && socket.connected && latestPos != null) { +- try { +- socket.emit('update_location', { +- 'driver_id': driverId, +- 'lat': latestPos!.latitude, +- 'lng': latestPos!.longitude, +- 'heading': latestPos!.heading, +- 'speed': latestPos!.speed * 3.6, +- 'status': box.read(BoxName.statusDriverLocation) ?? 'on', +- 'source': 'background_heartbeat' +- }); +- print( +- "💓 Background Mercy Heartbeat Sent: ${latestPos!.latitude}, ${latestPos!.longitude}"); +- } catch (e) { +- print("❌ Background Heartbeat Error: $e"); +- } +- } +- }); ++ // 🚫 [Architecture Rule] NO redundant GPS stream in background service! ++ // LocationController is the SINGLE SOURCE OF TRUTH for all location GPS updates. ++ // It already uses location.enableBackgroundMode(enable: true) to keep the GPS ++ // stream alive even when the app is in the background. The main socket in ++ // LocationController handles all emitLocationToSocket() calls including heartbeat. ++ // ++ // The background service is ONLY responsible for: ++ // 1. Keeping the socket connection alive for receiving 'new_ride_request' ++ // and 'cancel_ride' events while the main isolate is paused on Android. ++ // 2. Showing the Android Overlay UI for incoming ride requests. ++ // 3. Notifications for iOS background state. ++ // ++ // Location data is not sent from the background isolate — it would conflict ++ // with LocationController's stream and cause duplicate GPS listeners, ++ // battery drain, and device freeze (as documented in driver_lifecycle.md). + + Timer.periodic(const Duration(seconds: 30), (timer) async { + if (service is AndroidServiceInstance) { +diff --git a/siro_driver/lib/controller/functions/location_controller.dart b/siro_driver/lib/controller/functions/location_controller.dart +index 3367bd8..fc15ef0 100755 +--- a/siro_driver/lib/controller/functions/location_controller.dart ++++ b/siro_driver/lib/controller/functions/location_controller.dart +@@ -19,6 +19,7 @@ import '../firebase/local_notification.dart'; + import '../home/captin/home_captain_controller.dart'; + import '../home/captin/map_driver_controller.dart'; + import '../home/payment/captain_wallet_controller.dart'; ++import '../home/navigation/navigation_controller.dart'; + import 'background_service.dart'; + import 'crud.dart'; + +@@ -539,6 +540,16 @@ class LocationController extends GetxController with WidgetsBindingObserver { + } + } + ++ if (Get.isRegistered()) { ++ final mapCtrl = Get.find(); ++ mapCtrl.handleLocationUpdateFromCentral(pos, speed, heading); ++ } ++ ++ if (Get.isRegistered()) { ++ final navCtrl = Get.find(); ++ navCtrl.handleLocationUpdateFromCentral(pos, speed, heading); ++ } ++ + await _saveBehaviorIfMoved(pos, now, currentSpeed: speed); + }, onError: (e) => Log.print('❌ Location Stream Error: $e')); + } +diff --git a/siro_driver/lib/controller/home/captin/map_driver_controller.dart b/siro_driver/lib/controller/home/captin/map_driver_controller.dart +index 8aa56ae..3baad9e 100755 +--- a/siro_driver/lib/controller/home/captin/map_driver_controller.dart ++++ b/siro_driver/lib/controller/home/captin/map_driver_controller.dart +@@ -2570,27 +2570,19 @@ class MapDriverController extends GetxController + } + + void _startLocationListening() { +- _locationSubscription?.cancel(); +- _locationSubscription = geo.Geolocator.getPositionStream( +- locationSettings: const geo.LocationSettings( +- accuracy: geo.LocationAccuracy.bestForNavigation, +- distanceFilter: 2, +- ), +- ).listen((geo.Position pos) { +- _handleLocationUpdate(pos); +- }); ++ // Location stream is now centralized in LocationController to prevent device hanging. ++ // LocationController will call handleLocationUpdateFromCentral directly. + } + + /// [Fix C-4] تحديث myLocation في المستمع الأساسي +- void _handleLocationUpdate(geo.Position pos) { +- final newLoc = LatLng(pos.latitude, pos.longitude); ++ void handleLocationUpdateFromCentral(LatLng newLoc, double posSpeed, double posHeading) { + myLocation = newLoc; // ← [Fix C-4] تحديث الموقع الفوري + _oldLoc = smoothedLocation ?? newLoc; + _targetLoc = newLoc; + + _oldHeading = smoothedHeading; +- if (pos.speed > 0.5) { +- _targetHeading = pos.heading; ++ if (posSpeed > 0.5) { ++ _targetHeading = posHeading; + } else { + _targetHeading = _oldHeading; + } +diff --git a/siro_driver/lib/controller/home/captin/order_request_controller.dart b/siro_driver/lib/controller/home/captin/order_request_controller.dart +index f58d4f4..169ca95 100755 +--- a/siro_driver/lib/controller/home/captin/order_request_controller.dart ++++ b/siro_driver/lib/controller/home/captin/order_request_controller.dart +@@ -69,6 +69,7 @@ class OrderRequestController extends GetxController + + // --- الخريطة --- + Set polylines = {}; ++ bool _hasCalculatedFullJourney = false; + + // حالة التطبيق والصوت + bool isInBackground = false; +@@ -219,6 +220,11 @@ class OrderRequestController extends GetxController + // ---------------------------------------------------------------------- + + Future _calculateFullJourney() async { ++ if (_hasCalculatedFullJourney) { ++ if (mapController != null) zoomToFitRide(); ++ return; ++ } ++ _hasCalculatedFullJourney = true; + // Don't block on mapController being null - we'll draw routes + // and markers first, then zoom when controller is ready + bool canZoom = mapController != null; +@@ -281,7 +287,7 @@ class OrderRequestController extends GetxController + totalTripDistance = tripResult['distance_text']; + totalTripDuration = tripResult['duration_text']; + polylines.add(tripResult['polyline']); +- ++ + // 🔥 تخزين استجابة السيرفر كاملة (بما فيها الـ points والـ instructions) + if (tripResult['raw_response'] != null) { + box.write('cached_trip_route', tripResult['raw_response']); +diff --git a/siro_driver/lib/controller/home/navigation/navigation_controller.dart b/siro_driver/lib/controller/home/navigation/navigation_controller.dart +index e607f6d..e2a4f08 100644 +--- a/siro_driver/lib/controller/home/navigation/navigation_controller.dart ++++ b/siro_driver/lib/controller/home/navigation/navigation_controller.dart +@@ -476,32 +476,18 @@ class NavigationController extends GetxController + } + + void _startLocationStream() { +- _locationStreamSubscription?.cancel(); +- // Listen to location updates with minimum distance filter of 2 meters +- // This provides real-time updates without the 3-4 second delay +- _locationStreamSubscription = Geolocator.getPositionStream( +- locationSettings: const LocationSettings( +- accuracy: LocationAccuracy.high, +- distanceFilter: 2, // Update every 2 meters +- ), +- ).listen( +- (Position position) { +- _handleLocationUpdate(position); +- }, +- onError: (error) { +- Log.print("DEBUG: Location stream error: $error"); +- }, +- ); ++ // Location stream is now centralized in LocationController to prevent device hanging. ++ // LocationController will call handleLocationUpdateFromCentral directly. + } + + bool _isProcessing = false; +- Future _handleLocationUpdate(Position position) async { ++ Future handleLocationUpdateFromCentral(LatLng newLoc, double locSpeed, double locHeading) async { + if (_isProcessing) return; + _isProcessing = true; + + try { +- final newLoc = LatLng(position.latitude, position.longitude); +- currentSpeed = position.speed * 3.6; // Convert m/s to km/h ++ currentSpeed = locSpeed; // Convert m/s to km/h already done by location controller if needed, wait location_controller sends raw speed or km/h? It sends raw speed. So we should * 3.6 ++ currentSpeed = locSpeed * 3.6; + + // Skip if movement is too small + if (_lastProcessedLocation != null) { +@@ -544,7 +530,7 @@ class NavigationController extends GetxController + _targetLoc!.longitude, + ); + } else { +- _targetHeading = position.heading; ++ _targetHeading = locHeading; + } + + _animController?.forward(from: 0.0); +diff --git a/siro_rider/lib/controller/firebase/firbase_messge.dart b/siro_rider/lib/controller/firebase/firbase_messge.dart +index e24c3c8..5b54235 100644 +--- a/siro_rider/lib/controller/firebase/firbase_messge.dart ++++ b/siro_rider/lib/controller/firebase/firbase_messge.dart +@@ -87,12 +87,6 @@ class FirebaseMessagesController extends GetxController { + fireBaseTitles(message); + } + }); +- FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async { +- // Handle background message +- if (message.data.isNotEmpty) { +- fireBaseTitles(message); +- } +- }); + + FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { + if (message.data.isNotEmpty && message.notification != null) { +diff --git a/siro_rider/lib/controller/home/map/map_socket_controller.dart b/siro_rider/lib/controller/home/map/map_socket_controller.dart +index 3d73bae..95fbf21 100644 +--- a/siro_rider/lib/controller/home/map/map_socket_controller.dart ++++ b/siro_rider/lib/controller/home/map/map_socket_controller.dart +@@ -283,7 +283,7 @@ class MapSocketController extends GetxController { + } + + final dynamic distanceValue = +- data['distance_m'] ?? data['distance_meters'] ?? data['distance']; ++ data['distance_m'] ?? data['distance_meters']; + final double? distanceMeters = + double.tryParse(distanceValue?.toString() ?? ''); + final int? etaSeconds = data['eta_seconds'] == null +diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart +index c229ad2..c264a61 100644 +--- a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart ++++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart +@@ -112,6 +112,7 @@ class RideLifecycleController extends GetxController { + late String driverId = ''; + late String make = ''; + late String model = ''; ++ late String gender = ''; + late String carColor = ''; + late String licensePlate = ''; + late String driverName = ''; +@@ -120,6 +121,9 @@ class RideLifecycleController extends GetxController { + late String colorHex = ''; + late String carYear = ''; + late String driverRate = '5.0'; ++ late String driverRatingCount = '0'; ++ late String driverCompletedRides = '0'; ++ late String driverTier = 'Verified driver'; + late String driverToken = ''; + + double kazan = 8; +@@ -1481,7 +1485,8 @@ class RideLifecycleController extends GetxController { + + // إيقاف جلب السيارات المجاورة ومسحها، باستثناء السائق الذي قبل الطلب + mapEngine.reloadStartApp = false; +- mapEngine.markers.removeWhere((marker) => marker.markerId.value != driverId.toString()); ++ mapEngine.markers ++ .removeWhere((marker) => marker.markerId.value != driverId.toString()); + mapEngine.update(); + + await getDriverCarsLocationToPassengerAfterApplied(); +@@ -1490,8 +1495,7 @@ class RideLifecycleController extends GetxController { + LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last; + Log.print( + '[rideAppliedFromDriver] 📍 Driver at: $driverPos, Passenger at: $passengerLocation'); +- await getInitialDriverDistanceAndDuration(driverPos, passengerLocation); +- await drawDriverPathOnly(driverPos, passengerLocation); ++ await calculateDriverToPassengerRoute(driverPos, passengerLocation); + mapEngine.fitCameraToPoints(driverPos, passengerLocation); + } + +@@ -1656,6 +1660,9 @@ class RideLifecycleController extends GetxController { + driverToken = data['token']?.toString() ?? ''; + carYear = data['year']?.toString() ?? ''; + driverRate = data['ratingDriver']?.toString() ?? '5.0'; ++ driverRatingCount = data['ratingCount']?.toString() ?? '0'; ++ driverCompletedRides = data['completedRides']?.toString() ?? '0'; ++ driverTier = data['driverTier']?.toString() ?? 'Verified driver'; + + update(); + } +@@ -2221,6 +2228,15 @@ class RideLifecycleController extends GetxController { + polyLines = polyLines + .where((p) => !p.polylineId.value.startsWith('driver_route')) + .toSet(); ++ polyLines = { ++ ...polyLines, ++ Polyline( ++ polylineId: const PolylineId('main_route'), ++ points: decodedPoints, ++ color: const Color(0xFF2196F3), ++ width: 6, ++ ) ++ }; + } else { + // مسح السلمات القديمة أولاً + polyLines = polyLines +@@ -2290,7 +2306,9 @@ class RideLifecycleController extends GetxController { + _routeHeadingMismatchCount = 0; + _isRecalculatingRoute = true; + if (statusRide == 'Begin' || +- currentRideState.value == RideState.inProgress) { ++ statusRide == 'Arrived' || ++ currentRideState.value == RideState.inProgress || ++ currentRideState.value == RideState.driverArrived) { + await calculateDriverToPassengerRoute(driverPos, myDestination, + isBeginPhase: true); + } else { +@@ -2504,6 +2522,8 @@ class RideLifecycleController extends GetxController { + String icon; + if (model.contains('دراجة') || make.contains('دراجة')) { + icon = mapEngine.motoIcon; ++ } else if (gender == 'Female') { ++ icon = mapEngine.ladyIcon; + } else { + icon = mapEngine.carIcon; + } +@@ -3026,6 +3046,17 @@ class RideLifecycleController extends GetxController { + mapEngine.playRouteAnimation( + mapEngine.polylineCoordinates, mapEngine.lastComputedBounds); + } ++ ++ if (driverCarsLocationToPassengerAfterApplied.isNotEmpty && ++ myDestination.latitude != 0 && ++ myDestination.longitude != 0) { ++ await calculateDriverToPassengerRoute( ++ driverCarsLocationToPassengerAfterApplied.last, ++ myDestination, ++ isBeginPhase: true, ++ ); ++ } ++ + update(); + } + +@@ -3903,12 +3934,37 @@ class RideLifecycleController extends GetxController { + + make = data['make']?.toString() ?? ''; + model = data['model']?.toString() ?? ''; ++ gender = data['gender']?.toString() ?? ''; + carColor = data['color']?.toString() ?? ''; + colorHex = data['color_hex']?.toString() ?? ''; + licensePlate = data['car_plate']?.toString() ?? ''; + carYear = data['year']?.toString() ?? ''; + ++ // المحاولة الفورية لرسم السائق إذا توفرت الإحداثيات في البيانات ++ double lat = double.tryParse( ++ data['latitude']?.toString() ?? data['lat']?.toString() ?? '0') ?? ++ 0; ++ double lng = double.tryParse(data['longitude']?.toString() ?? ++ data['lng']?.toString() ?? ++ '0') ?? ++ 0; ++ double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0; ++ ++ if (lat != 0 && lng != 0) { ++ LatLng initialPos = LatLng(lat, lng); ++ if (driverCarsLocationToPassengerAfterApplied.isEmpty) { ++ driverCarsLocationToPassengerAfterApplied.add(initialPos); ++ } else { ++ driverCarsLocationToPassengerAfterApplied[0] = initialPos; ++ } ++ // تحديث الماركر فوراً لضمان ظهوره بشكل موثوق ++ updateDriverMarker(initialPos, heading); ++ } ++ + driverRate = data['ratingDriver']?.toString() ?? '5.0'; ++ driverRatingCount = data['ratingCount']?.toString() ?? '0'; ++ driverCompletedRides = data['completedRides']?.toString() ?? '0'; ++ driverTier = data['driverTier']?.toString() ?? 'Verified driver'; + driverToken = data['token']?.toString() ?? ''; + + update(); +@@ -4185,55 +4241,6 @@ class RideLifecycleController extends GetxController { + ); + } + +- Future getDistanceFromDriverAfterAcceptedRide( +- String origin, String destination) async { +- String apiKey = Env.mapKeyOsm; +- if (origin.isEmpty) { +- origin = '${passengerLocation.latitude},${passengerLocation.longitude}'; +- } +- var uri = Uri.parse( +- '$dynamicApiUrl?origin=$origin&destination=$destination&steps=false&overview=false'); +- Log.print('uri: $uri'); +- +- http.Response response; +- Map responseData; +- +- try { +- response = await http.get( +- uri, +- headers: { +- 'X-API-KEY': apiKey, +- }, +- ).timeout(const Duration(seconds: 20)); +- +- if (response.statusCode != 200) { +- Log.print('Error from API: ${response.statusCode}'); +- isLoading = false; +- update(); +- return; +- } +- if (Get.isBottomSheetOpen ?? false) { +- Get.back(); +- } +- isDrawingRoute = false; +- +- responseData = json.decode(response.body); +- Log.print('responseData: $responseData'); +- +- if (responseData['status'] != 'ok') { +- Log.print('API returned an error: ${responseData['message']}'); +- isLoading = false; +- update(); +- return; +- } +- } catch (e) { +- Log.print('Failed to get directions: $e'); +- isLoading = false; +- update(); +- return; +- } +- } +- + Future _stageNiceToHave() async { + Log.print('🚀 MapPassengerController: Starting _stageNiceToHave'); + +diff --git a/siro_rider/lib/controller/home/map/ui_interactions_controller.dart b/siro_rider/lib/controller/home/map/ui_interactions_controller.dart +index 388c28e..afa97d9 100644 +--- a/siro_rider/lib/controller/home/map/ui_interactions_controller.dart ++++ b/siro_rider/lib/controller/home/map/ui_interactions_controller.dart +@@ -4,7 +4,6 @@ import 'dart:ui'; + import 'package:flutter/material.dart'; + import 'package:flutter/cupertino.dart'; + import 'package:get/get.dart'; +-import 'package:intaleq_maps/intaleq_maps.dart'; + + import '../../../constant/box_name.dart'; + import '../../../constant/colors.dart'; +@@ -15,19 +14,13 @@ import '../../../main.dart'; // contains global 'box' + import '../../../print.dart'; + import '../../../services/emergency_signal_service.dart'; + import '../../../views/widgets/elevated_btn.dart'; +-import '../../../views/widgets/mydialoug.dart'; + import '../../../views/widgets/my_textField.dart'; +-import '../../../views/home/map_page_passenger.dart'; +-import '../../../views/widgets/error_snakbar.dart'; +-import '../../../models/model/painter_copoun.dart'; + import '../../functions/launch.dart'; +-import '../../firebase/local_notification.dart'; + import '../../firebase/notification_service.dart'; + import '../../functions/crud.dart'; + import '../../functions/tts.dart'; + import 'ride_lifecycle_controller.dart'; + import 'location_search_controller.dart'; +-import 'map_engine_controller.dart'; + + class UiInteractionsController extends GetxController { + TextEditingController sosPhonePassengerProfile = TextEditingController(); +@@ -56,54 +49,54 @@ class UiInteractionsController extends GetxController { + + sosPhonePassengerProfile.clear(); + Get.defaultDialog( +- title: 'Add SOS Phone'.tr, +- titleStyle: AppStyle.title, +- content: Form( +- key: sosFormKey, +- child: Column( +- children: [ +- MyTextForm( +- controller: sosPhonePassengerProfile, +- label: 'insert sos phone'.tr, +- hint: 'e.g. 0912345678 (Default +963)'.tr, +- type: TextInputType.phone, +- ), +- const SizedBox(height: 10), +- Text( +- "Note: If no country code is entered, it will be saved as Syrian (+963).".tr, +- style: TextStyle(fontSize: 12, color: Colors.grey), +- textAlign: TextAlign.center, +- ), +- ], ++ title: 'Add SOS Phone'.tr, ++ titleStyle: AppStyle.title, ++ content: Form( ++ key: sosFormKey, ++ child: Column( ++ children: [ ++ MyTextForm( ++ controller: sosPhonePassengerProfile, ++ label: 'insert sos phone'.tr, ++ hint: 'e.g. 0912345678 (Default +963)'.tr, ++ type: TextInputType.phone, ++ ), ++ const SizedBox(height: 10), ++ Text( ++ "Note: If no country code is entered, it will be saved as Syrian (+963)." ++ .tr, ++ style: TextStyle(fontSize: 12, color: Colors.grey), ++ textAlign: TextAlign.center, ++ ), ++ ], ++ ), + ), +- ), +- confirm: MyElevatedButton( +- title: 'Save'.tr, +- onPressed: () async { +- if (sosFormKey.currentState!.validate()) { +- Get.back(); +- var numberPhone = +- formatSyrianPhoneNumber(sosPhonePassengerProfile.text); +- +- await CRUD().post( +- link: AppLink.updateprofile, +- payload: { +- 'id': box.read(BoxName.passengerID), +- 'sosPhone': numberPhone, +- }, +- ); +- +- box.write(BoxName.sosPhonePassenger, numberPhone); +- onSuccess(); +- } +- }, +- ), +- cancel: MyElevatedButton( +- title: 'Cancel'.tr, +- onPressed: () => Get.back(), +- kolor: AppColor.redColor, +- ) +- ); ++ confirm: MyElevatedButton( ++ title: 'Save'.tr, ++ onPressed: () async { ++ if (sosFormKey.currentState!.validate()) { ++ Get.back(); ++ var numberPhone = ++ formatSyrianPhoneNumber(sosPhonePassengerProfile.text); ++ ++ await CRUD().post( ++ link: AppLink.updateprofile, ++ payload: { ++ 'id': box.read(BoxName.passengerID), ++ 'sosPhone': numberPhone, ++ }, ++ ); ++ ++ box.write(BoxName.sosPhonePassenger, numberPhone); ++ onSuccess(); ++ } ++ }, ++ ), ++ cancel: MyElevatedButton( ++ title: 'Cancel'.tr, ++ onPressed: () => Get.back(), ++ kolor: AppColor.redColor, ++ )); + } + + void sosPassenger() { +@@ -114,10 +107,12 @@ class UiInteractionsController extends GetxController { + titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), + content: Column( + children: [ +- Icon(Icons.warning_amber_rounded, size: 50, color: AppColor.redColor), ++ Icon(Icons.warning_amber_rounded, ++ size: 50, color: AppColor.redColor), + const SizedBox(height: 10), + Text( +- "Do you want to send an emergency message to your SOS contact?".tr, ++ "Do you want to send an emergency message to your SOS contact?" ++ .tr, + textAlign: TextAlign.center, + style: AppStyle.title, + ), +diff --git a/siro_rider/lib/controller/local/translations.dart b/siro_rider/lib/controller/local/translations.dart +index 5244054..d42c370 100644 +--- a/siro_rider/lib/controller/local/translations.dart ++++ b/siro_rider/lib/controller/local/translations.dart +@@ -42,6 +42,7 @@ class MyTranslation extends Translations { + "Arrived": "وصلنا", + "Audio Recording": "تسجيل صوتي", + "Call": "اتصال", ++ "Call Options": "خيارات الاتصال", + "Call Connected": "تم فتح الاتصال", + "Call Support": "اتصل بالدعم", + "Call left": "مكالمات متبقية", +@@ -49,6 +50,8 @@ class MyTranslation extends Translations { + "Change Photo": "تغيير الصورة", + "Captain": "الكابتن", + "Choose from Gallery": "اختر من المعرض", ++ "Choose how you want to call the driver": ++ "اختر طريقة الاتصال بالكابتن", + "Choose from contact": "اختر من جهات الاتصال", + "Click to track the trip": "اضغط لتتبع المشوار", + "Close panel": "إغلاق اللوحة", +@@ -92,6 +95,9 @@ class MyTranslation extends Translations { + "Finished": "انتهى", + "Fixed Price": "سعر ثابت", + "Free Call": "مكالمة مجانية", ++ "Professional driver": "كابتن محترف", ++ "Trusted driver": "كابتن موثوق", ++ "Verified driver": "كابتن موثق", + "General": "عام", + "Grant": "منح الإذن", + "Have a Promo Code?": "معك كود خصم؟", +@@ -178,6 +184,7 @@ class MyTranslation extends Translations { + "Preferences": "التفضيلات", + "Profile photo updated": "تم تحديث صورة الغلاف", + "Quick Message": "رسالة سريعة", ++ "reviews": "تقييم", + "Rating is": "التقييم هو", + "Received empty route data.": "تم استلام بيانات طريق فارغة.", + "Record": "تسجيل", +@@ -211,6 +218,7 @@ class MyTranslation extends Translations { + "Set as Work": "تحديد كالشغل", + "Share": "مشاركة", + "Share Trip": "مشاركة المشوار", ++ "Standard Call": "اتصال عادي", + "Share your experience to help us improve...": + "شاركنا تجربتك لنحسن خدمتنا...", + "Something went wrong. Please try again.": "صار غلط. جرب مرة تانية.", +@@ -271,6 +279,8 @@ class MyTranslation extends Translations { + "to arrive you.": "ليوصلك.", + "unknown": "غير معروف", + "wait 1 minute to recive message": "استنى دقيقة لتستلم الرسالة", ++ "Uses cellular network": "يستخدم شبكة الهاتف", ++ "Voice call over internet": "مكالمة صوتية عبر الإنترنت", + "with license plate": "برقم اللوحة", + "witout zero": "بدون صفر", + "you must insert token code": "لازم تدخل الكود", +@@ -16885,7 +16895,8 @@ class MyTranslation extends Translations { + "Support is Away": "سپورٹ اب دستیاب نہیں ہے", + "Support is currently Online": "سپورٹ اب آن لائن ہے", + "Voice Call": "صوتی کال", +- "We're here to help you 24/7": "ہم چوبیس گھنٹے آپ کی مدد کے لیے حاضر ہیں", ++ "We're here to help you 24/7": ++ "ہم چوبیس گھنٹے آپ کی مدد کے لیے حاضر ہیں", + "Working Hours:": "کام کے اوقات:", + "1 Passenger": "1 Passenger", + "2 Passengers": "2 Passengers", +@@ -18446,7 +18457,8 @@ class MyTranslation extends Translations { + "Support is Away": "सहायता अभी उपलब्ध नहीं है", + "Support is currently Online": "सहायता अभी ऑनलाइन है", + "Voice Call": "वॉइस कॉल", +- "We're here to help you 24/7": "हम आपकी सहायता के लिए 24/7 उपलब्ध हैं", ++ "We're here to help you 24/7": ++ "हम आपकी सहायता के लिए 24/7 उपलब्ध हैं", + "Working Hours:": "कार्य समय:", + "1 Passenger": "1 Passenger", + "2 Passengers": "2 Passengers", +diff --git a/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart b/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart +index 8168f4f..a0689dc 100644 +--- a/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart ++++ b/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart +@@ -250,19 +250,23 @@ class ApplyOrderWidget extends StatelessWidget { + Row( + children: [ + // صورة السائق (أصغر) +- Container( +- decoration: BoxDecoration( +- shape: BoxShape.circle, +- border: Border.all( +- color: AppColor.primaryColor.withOpacity(0.2), width: 2), +- ), +- child: CircleAvatar( +- radius: 22, // تصغير من 28 إلى 22 +- backgroundColor: Colors.grey[200], +- backgroundImage: NetworkImage( +- '${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'), +- onBackgroundImageError: (_, __) => +- const Icon(Icons.person, color: Colors.grey, size: 20), ++ GestureDetector( ++ onTap: () => _showDriverAvatarDialog(context, controller), ++ child: Container( ++ decoration: BoxDecoration( ++ shape: BoxShape.circle, ++ border: Border.all( ++ color: AppColor.primaryColor.withOpacity(0.2), ++ width: 2), ++ ), ++ child: CircleAvatar( ++ radius: 22, // تصغير من 28 إلى 22 ++ backgroundColor: Colors.grey[200], ++ backgroundImage: NetworkImage( ++ '${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'), ++ onBackgroundImageError: (_, __) => ++ const Icon(Icons.person, color: Colors.grey, size: 20), ++ ), + ), + ), + +@@ -299,6 +303,32 @@ class ApplyOrderWidget extends StatelessWidget { + ), + ], + ), ++ const SizedBox(height: 5), ++ Wrap( ++ spacing: 6, ++ runSpacing: 4, ++ children: [ ++ _buildDriverBadge( ++ icon: Icons.verified_rounded, ++ text: controller.driverTier.tr, ++ color: AppColor.primaryColor, ++ ), ++ if (controller.driverCompletedRides != '0') ++ _buildDriverBadge( ++ icon: Icons.route_rounded, ++ text: ++ '${controller.driverCompletedRides} ${'rides'.tr}', ++ color: Colors.teal, ++ ), ++ if (controller.driverRatingCount != '0') ++ _buildDriverBadge( ++ icon: Icons.reviews_rounded, ++ text: ++ '${controller.driverRatingCount} ${'reviews'.tr}', ++ color: Colors.amber.shade800, ++ ), ++ ], ++ ), + ], + ), + ), +@@ -320,6 +350,11 @@ class ApplyOrderWidget extends StatelessWidget { + Widget _buildMicroCarIcon( + RideLifecycleController controller, Color Function(String) parseColor) { + Color carColor = parseColor(controller.colorHex); ++ final String vehicleText = ++ '${controller.model} ${controller.make}'.toLowerCase(); ++ final bool isBike = vehicleText.contains('scooter') || ++ vehicleText.contains('bike') || ++ vehicleText.contains('دراجة'); + return Container( + height: 40, // تصغير من 50 + width: 40, +@@ -331,7 +366,8 @@ class ApplyOrderWidget extends StatelessWidget { + child: ColorFiltered( + colorFilter: ColorFilter.mode(carColor, BlendMode.srcIn), + child: Image.asset( +- box.read(BoxName.carType) == 'Scooter' || ++ isBike || ++ box.read(BoxName.carType) == 'Scooter' || + box.read(BoxName.carType) == 'Pink Bike' + ? 'assets/images/moto.png' + : 'assets/images/car3.png', +@@ -341,6 +377,81 @@ class ApplyOrderWidget extends StatelessWidget { + ); + } + ++ Widget _buildDriverBadge({ ++ required IconData icon, ++ required String text, ++ required Color color, ++ }) { ++ return Container( ++ padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), ++ decoration: BoxDecoration( ++ color: color.withOpacity(0.1), ++ borderRadius: BorderRadius.circular(10), ++ ), ++ child: Row( ++ mainAxisSize: MainAxisSize.min, ++ children: [ ++ Icon(icon, size: 11, color: color), ++ const SizedBox(width: 4), ++ Text( ++ text, ++ style: TextStyle( ++ color: color, ++ fontSize: 10.5, ++ fontWeight: FontWeight.w800, ++ ), ++ ), ++ ], ++ ), ++ ); ++ } ++ ++ void _showDriverAvatarDialog( ++ BuildContext context, RideLifecycleController controller) { ++ final imageUrl = ++ '${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'; ++ Get.dialog( ++ Dialog( ++ insetPadding: const EdgeInsets.symmetric(horizontal: 38), ++ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), ++ child: Padding( ++ padding: const EdgeInsets.fromLTRB(18, 20, 18, 18), ++ child: Column( ++ mainAxisSize: MainAxisSize.min, ++ children: [ ++ CircleAvatar( ++ radius: 58, ++ backgroundColor: Colors.grey[200], ++ backgroundImage: NetworkImage(imageUrl), ++ onBackgroundImageError: (_, __) {}, ++ ), ++ const SizedBox(height: 14), ++ Text( ++ controller.driverName, ++ textAlign: TextAlign.center, ++ style: AppStyle.title.copyWith( ++ fontSize: 18, ++ fontWeight: FontWeight.w900, ++ ), ++ ), ++ const SizedBox(height: 6), ++ Text( ++ '${controller.driverTier.tr} • ${controller.driverRate}', ++ textAlign: TextAlign.center, ++ style: TextStyle( ++ color: Colors.grey[700], ++ fontSize: 13, ++ fontWeight: FontWeight.w600, ++ ), ++ ), ++ ], ++ ), ++ ), ++ ), ++ barrierDismissible: true, ++ ); ++ } ++ + Widget _buildSlimLicensePlate(String plateNumber) { + return Container( + width: double.infinity, diff --git a/scratch.txt b/scratch.txt new file mode 100644 index 0000000..e69de29 diff --git a/siro_driver/knowledge/driver_lifecycle.md b/siro_driver/knowledge/driver_lifecycle.md new file mode 100644 index 0000000..71d0a38 --- /dev/null +++ b/siro_driver/knowledge/driver_lifecycle.md @@ -0,0 +1,256 @@ +# 🚗 دورة حياة الرحلة - تطبيق السائق (Driver Ride Lifecycle) + +
+هذا الملف هو مرجع هندسي شامل ودقيق (Source of Truth) يوضح كافة تفاصيل دورة حياة الرحلة (Ride Lifecycle) في تطبيق السائق (Siro Driver). تمت كتابته وتوثيقه ليكون دليلاً استرشادياً كاملاً لأي مهندس برمجي أو نموذج ذكاء اصطناعي (AI Agent) يرغب في فهم أو تعديل منطق الرحلات، تتبع المواقع، أو معالجة الحالات الفعالة. +
+ +--- + +## 1. بنية مزود الموقع المركزي (LocationController) + +
+يعد الكنترولر +[LocationController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/functions/location_controller.dart) +هو المصدر الوحيد والمركزى (Single Source of Truth) لكافة تحديثات نظام التموضع العالمي (GPS). يُمنع منعاً باتاً إنشاء اشتراكات أو فتح قنوات استماع موازية مع Geolocator أو Location في أي مكان آخر. +
+ +### 1.1 التهيئة والتحكم في دورة الحياة (Initialization & Lifecycle) + +
+* دالة التهيئة: +
+ +```dart +Future onInit() async +``` + +
+تقوم بالخطوات التالية: +1. تسجيل الكنترولر كمراقب لدورة حياة التطبيق العامة عبر `WidgetsBinding.instance.addObserver(this)`. +2. الاستماع للمفتاح `BoxName.statusDriverLocation` للتحقق من حالة السائق (مثال: محظور `blocked`). إذا تم حظره، يتم إيقاف تتبع الموقع فوراً وتحديث السيرفر وفصل الاتصال. +3. انتظار تحميل الكنترولرات المعتمد عليها (`HomeCaptainController` و `CaptainWalletController`) عبر الدالة المساعدة `_awaitDependencies()`. +4. تهيئة الاتصال بالسوكيت عبر `initSocket()` وتهيئة إعدادات تحديد الموقع عبر `_initLocationSettings()` والبدء الفوري ببث الموقع في حال لم يكن السائق محظوراً. +
+ +
+* مراقبة تغيير حالة التطبيق: +
+ +```dart +void didChangeAppLifecycleState(AppLifecycleState state) +``` + +
+1. في حالة **Resumed** (التطبيق في الواجهة): يتم إيقاف تشغيل خدمة الخلفية للأندرويد عبر `BackgroundServiceHelper.stopService()`، وإعادة تهيئة السوكيت إذا تم قطعه. +2. في حالة **Paused** أو **Detached** (التطبيق في الخلفية): يتم حفظ الحالة محلياً وتشغيل خدمة الخلفية `BackgroundServiceHelper.startService()` لضمان استمرارية البث. +
+ +### 1.2 الاتصال بالويب سوكيت (WebSocket & Socket.io) + +
+* تهيئة السوكيت: +
+ +```dart +void initSocket() +``` + +
+تقوم بفتح قناة اتصال مع خادم المواقع (`https://location.intaleq.xyz`) وتجهيز معلمات الاتصال (`driver_id`, `token`). تحتوي الدالة على حماية ضد الاستدعاءات المتداخلة (Debounce Flag: `_isInitializingSocket`). +
+ +
+* إعداد المستمعين: +
+ +```dart +void _setupSocketListeners() +``` + +
+تقوم بربط الأحداث القادمة من السيرفر كالتالي: +1. `connect`: تفعيل مؤشر الاتصال وتدشين الـ Heartbeat عبر الـ Timer. +2. `new_ride_request`: عند وصول طلب رحلة جديد، يتم معالجته فوراً وتوجيهه للواجهة عبر دالة `handleIncomingOrder()`. +3. `cancel_ride`: عند إلغاء الراكب للطلب، يتم إبلاغ `MapDriverController` فوراً لإيقاف وضع الملاحة والعودة للرئيسية عبر `processRideCancelledByPassenger()`. +
+ +
+* إرسال الموقع الفوري للشبكة: +
+ +```dart +void emitLocationToSocket(LatLng pos, double head, double spd) +``` + +
+تقوم بإرسال حزمة بيانات البث الفعلي (Latitude, Longitude, Heading, Speed, Distance, Status). في حال وجود رحلة نشطة بالحالة `Apply`, `Arrived`, أو `Begin` وبوجود معرف الراكب في الذاكرة، يتم حقن `passenger_id` و `ride_id` تلقائياً لتمكين الراكب من التتبع الحي. +
+ +### 1.3 معالجة الطلبات الواردة (Incoming Order Handler) + +
+* معالجة الطلب المستلم: +
+ +```dart +Future handleIncomingOrder(Map rideData, String source) +``` + +
+تتم معالجة الطلب بطريقتين بناءً على حالة التطبيق: +1. **خلفية التطبيق (Background)**: + * على **iOS**: يتم إرسال إشعار محلي فوري (Ding Tone) ليدعو السائق لفتح التطبيق. + * على **Android**: يتم إظهار واجهة التراكب السريعة (Overlay View). +2. **واجهة التطبيق (Foreground)**: + * يتم التحقق من عدم وجود رحلة جارية حالياً (Guard Clause: لمنع تداخل الأحداث أثناء تنفيذ رحلة نشطة). + * إخفاء أي تراكبات (Overlay Views) مفعلة. + * التوجيه الفوري للسائق نحو صفحة الطلب المخصصة `/OrderRequestPage` وتمرير معطيات الرحلة المستلمة. +
+ +### 1.4 الاستماع لتدفق الموقع المباشر (GPS Stream Listener) + +
+* الاستماع المباشر: +
+ +```dart +Future _subscribeLocationStream() +``` + +
+يقوم الكنترولر بالاستماع لتغيرات الموقع من الـ GPS، وبثها محلياً للجهات المعنية كالتالي: +1. تحديث إحداثيات السائق الحالية (`myLocation`) والسرعة والاتجاه وتخزينها محلياً في الـ Storage. +2. حساب تراكم المسافة الكلية المقطوعة (`totalDistance`). +3. تحديث خريطة السائق الرئيسية إذا كانت واجهة `HomeCaptain` نشطة. +4. إرسال الإحداثيات وتمريرها مباشرة إلى دالة `handleLocationUpdateFromCentral()` المتواجدة داخل `MapDriverController` (أثناء الذهاب للراكب أو الوجهة). +5. إرسال الإحداثيات وتمريرها مباشرة إلى دالة `handleLocationUpdateFromCentral()` المتواجدة داخل `NavigationController` (أثناء التوجيه خطوة بخطوة). +
+ +--- + +## 2. إدارة الرحلة النشطة (MapDriverController) + +
+يتحكم +[MapDriverController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart) +في مراحل تنفيذ الطلب بعد القبول، ورسم المسارات، وعمليات الوصول والإنهاء. +
+ +### 2.1 تحميل المعطيات واستعادة الحالة (Startup Argument Loading) + +
+* تحميل المعطيات: +
+ +```dart +Future argumentLoading() +``` + +
+تعمل هذه الدالة عند فتح الخريطة النشطة، وتقوم بالآتي: +1. قراءة بيانات الرحلة من المتغيرات المستلمة أو استعادتها من التخزين المحلي في حالات انقطاع التطبيق (`BoxName.rideArguments`). +2. تحليل ومعالجة إحداثيات الراكب والوجهة عبر دالة `latlng()`. +3. التحقق من حالة الرحلة المخزنة في الـ Box: + * إذا كانت الحالة **Begin**: تعني أن الرحلة قد بدأت بالفعل، فيتم تفعيل الأعلام (`isRideStarted = true` و `isRideBegin = true`)، ورسم خط المسار باللون **الأسود/الأزرق** باتجاه **الوجهة النهائية (Passenger Destination)**، وتشغيل عدادات الرحلة وتتبع الخطوات خطوة بخطوة. + * إذا كانت الحالة غير ذلك: يتم رسم خط المسار باللون **الأصفر** باتجاه **موقع الراكب (Passenger Location)**، وتشغيل محرك التوجيه لتتبع حركة السائق نحو نقطة الالتقاء. +
+ +### 2.2 تحديثات الموقع الواردة من المركز (Location Processing) + +
+* استقبال الموقع المركزي: +
+ +```dart +void handleLocationUpdateFromCentral(LatLng newLoc, double posSpeed, double posHeading) +``` + +
+يتم استدعاء هذه الدالة دورياً من قبل `LocationController` عند كل إحداثي جديد: +1. استخدام آلية التنعيم الفوري لحركة الكاميرا والسيارة على الخريطة عبر الـ `AnimationController` لعمل interpolation سلس ومنع القفزات المفاجئة. +2. استدعاء فحص الملاحة والمسارات التفصيلية عبر `_checkNavigationStep()`. +3. التحقق من اقتراب السائق من وجهته عبر `checkDestinationProximity()`. +
+ +### 2.3 الانتقال بين حالات الرحلة (State Transitions) + +
+الحالات كالتالي: +
+ +``` +OrderRequestPage: Accept -> Apply -> PassengerLocationMapPage (Yellow Polyline) -> Arrived (Distance < 100m) -> Begin (Distance < 150m) -> Trip to Destination (Blue/Black Polyline) -> Finished -> RatePassenger Page +``` + +
+* مرحلة وصول السائق للراكب (Arrive): +
+ +```dart +Future markDriverAsArrived() +``` + +
+1. التحقق من المسافة الفاصلة بين السائق والراكب؛ يجب أن تكون أقل من 100 متر. +2. إرسال طلب تفعيل حالة الوصول لخادم الرحلات عبر `arrive_ride.php`. +3. بدء تشغيل مؤشر احتساب زمن انتظار السائق للراكب عبر `startTimerToShowDriverWaitPassengerDuration()`. +4. إعادة رسم المسار الجديد للوجهة مباشرة باللون الأزرق استعداداً للمرحلة القادمة. +
+ +
+* مرحلة بدء الرحلة الفعلية (Begin): +
+ +```dart +Future startRideFromDriver() +``` + +
+1. التحقق من قرب السائق من الراكب (يجب أن يكون أقل من 150 متر للسماح بالبدء). +2. إيقاف وإعادة ضبط مؤقتات انتظار الراكب. +3. كتابة حالة الرحلة محلياً في الـ Storage وتعيينها إلى `Begin`. +4. مسح الخطوط الصفراء السابقة ورسم مسار الرحلة الفعلي باللون **الأسود/الأزرق** للوجهة. +5. تشغيل عداد الرحلة النشط (`rideIsBeginPassengerTimer`) لتحديث عداد السعر التفاعلي. +6. تشغيل بث التوجيه والملاحة عبر `startListeningStepNavigation()`. +7. بدء تسجيل الصوت لتوثيق الرحلة لأسباب أمنية عبر `AudioRecorderController`. +8. إخطار السيرفر عبر API مخصص `start_ride.php` مع وجود منطق تراجع تلقائي (Rollback) في حال فشل الطلب لإعادة الحالة إلى `Apply`. +
+ +
+* مرحلة إنهاء الرحلة بنجاح (Finish): +
+ +```dart +Future finishRideFromDriver({bool isFromSlider = false}) +Future finishRideFromDriver1({bool isFromSlider = false}) +``` + +
+1. **فحص التحقق المادي للرحلة**: حساب المسافة الإجمالية المخططة للرحلة، والتحقق من أن المسافة التي قطعتها السيارة بالفعل تتجاوز خمس (1/5) المسافة الكلية المطلوبة لمنع الإغلاقات الوهمية أو الخاطئة. +2. في حال مطابقة الشروط، يتم إيقاف تسجيل الصوت وحفظ الملف. +3. تحديث الحالات محلياً في الـ Storage إلى `Finished` وإيقاف تحديثات الموقع وإزالة معرفات الراكب المؤقتة. +4. إرسال حزمة بيانات موحدة ومتكاملة تشتمل على المسافة والوقت والتحقق المالي وخيارات المحفظة لخادم الرحلات عبر API المعاملات الموحد `finish_ride_updates.php`. +5. معالجة وتحديث محفظة السائق بالكامل وإرسال تقرير السلوك الفردي للسائق عبر `DriverBehaviorController`. +6. التوجيه النهائي لصفحة تقييم الراكب `RatePassenger` وتمرير الفاتورة المالية المحسوبة بدقة من السيرفر. +
+ +--- + +## 3. التوجيه الصوتي والملاحة التفصيلية (Navigation Engine) + +
+* الاستماع لتدفق خط الملاحة: +
+ +```dart +void checkForNextStep(LatLng currentPosition) +void _checkNavigationStep(LatLng pos) +``` + +
+تقوم بمطابقة الموقع الحالي للسائق مع قائمة النقاط والخطوات التوجيهية للرحلة: +1. تحديد الخطوة والمناورة التوجيهية الحالية بناءً على النقطة الأقرب. +2. إذا اقترب السائق من نقطة الانعطاف القادمة (أقل من 30-50 متر)، يتم تفعيل الخطوة التالية وتحديث النصوص الإرشادية. +3. استدعاء محرك التوجيه الصوتي `TextToSpeechController` لنطق التوجيه الجديد باللغة العربية تلقائياً لمنع تشتت السائق أثناء القيادة. +4. قص الأجزاء التي تم قطعها وتجاوزها من المسار المرسوم عبر `_updateTraveledPolylineSmart()` للحفاظ على وضوح ونظافة خط الرحلة على الخريطة. +
diff --git a/siro_driver/lib/controller/firebase/firbase_messge.dart b/siro_driver/lib/controller/firebase/firbase_messge.dart index 9de8eae..d183550 100755 --- a/siro_driver/lib/controller/firebase/firbase_messge.dart +++ b/siro_driver/lib/controller/firebase/firbase_messge.dart @@ -76,15 +76,22 @@ class FirebaseMessagesController extends GetxController { await fcmToken.subscribeToTopic("drivers"); // أو "users" حسب نوع المستخدم print("Subscribed to 'drivers' topic ✅"); - FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? message) async { + FirebaseMessaging.instance + .getInitialMessage() + .then((RemoteMessage? message) async { if (message != null && message.data.isNotEmpty) { Log.print("🔔 FCM getInitialMessage payload: ${message.data}"); String? category = message.data['category'] ?? message.data['type']; - if (category == 'ORDER' || category == 'Order' || category == 'OrderVIP' || message.data.containsKey('DriverList')) { + if (category == 'ORDER' || + category == 'Order' || + category == 'OrderVIP' || + message.data.containsKey('DriverList')) { String? myListString = message.data['DriverList']; if (myListString != null && myListString.isNotEmpty) { - await storage.write(key: 'pending_driver_list', value: myListString); - Log.print("💾 Saved pending driver list to secure storage from getInitialMessage"); + await storage.write( + key: 'pending_driver_list', value: myListString); + Log.print( + "💾 Saved pending driver list to secure storage from getInitialMessage"); } } else { Future.delayed(const Duration(milliseconds: 1500), () { @@ -107,7 +114,6 @@ class FirebaseMessagesController extends GetxController { // fireBaseTitles(message); // } }); - FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {}); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { if (message.data.isNotEmpty) { diff --git a/siro_driver/lib/controller/functions/background_service.dart b/siro_driver/lib/controller/functions/background_service.dart index e112b36..5d9bec9 100644 --- a/siro_driver/lib/controller/functions/background_service.dart +++ b/siro_driver/lib/controller/functions/background_service.dart @@ -9,7 +9,6 @@ import 'package:flutter_overlay_window/flutter_overlay_window.dart'; import 'package:socket_io_client/socket_io_client.dart' as IO; import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay; import 'package:get_storage/get_storage.dart'; -import 'package:geolocator/geolocator.dart' as geo; import '../../constant/box_name.dart'; import '../firebase/local_notification.dart'; @@ -129,40 +128,21 @@ Future onStart(ServiceInstance service) async { service.stopSelf(); }); - // 🔥 Location management in background isolate (Using Geolocator) - geo.Position? latestPos; - - // Listen to location changes continuously in the background - geo.Geolocator.getPositionStream( - locationSettings: geo.AndroidSettings( - accuracy: geo.LocationAccuracy.high, - distanceFilter: 10, - intervalDuration: const Duration(seconds: 10), - ), - ).listen((pos) { - latestPos = pos; - }); - - // 🔥 MERCY HEARTBEAT: Send location every 2 minutes to keep driver active in 'raids' - Timer.periodic(const Duration(minutes: 2), (timer) async { - if (socket != null && socket.connected && latestPos != null) { - try { - socket.emit('update_location', { - 'driver_id': driverId, - 'lat': latestPos!.latitude, - 'lng': latestPos!.longitude, - 'heading': latestPos!.heading, - 'speed': latestPos!.speed * 3.6, - 'status': box.read(BoxName.statusDriverLocation) ?? 'on', - 'source': 'background_heartbeat' - }); - print( - "💓 Background Mercy Heartbeat Sent: ${latestPos!.latitude}, ${latestPos!.longitude}"); - } catch (e) { - print("❌ Background Heartbeat Error: $e"); - } - } - }); + // 🚫 [Architecture Rule] NO redundant GPS stream in background service! + // LocationController is the SINGLE SOURCE OF TRUTH for all location GPS updates. + // It already uses location.enableBackgroundMode(enable: true) to keep the GPS + // stream alive even when the app is in the background. The main socket in + // LocationController handles all emitLocationToSocket() calls including heartbeat. + // + // The background service is ONLY responsible for: + // 1. Keeping the socket connection alive for receiving 'new_ride_request' + // and 'cancel_ride' events while the main isolate is paused on Android. + // 2. Showing the Android Overlay UI for incoming ride requests. + // 3. Notifications for iOS background state. + // + // Location data is not sent from the background isolate — it would conflict + // with LocationController's stream and cause duplicate GPS listeners, + // battery drain, and device freeze (as documented in driver_lifecycle.md). Timer.periodic(const Duration(seconds: 30), (timer) async { if (service is AndroidServiceInstance) { diff --git a/siro_driver/lib/controller/functions/location_controller.dart b/siro_driver/lib/controller/functions/location_controller.dart index 3367bd8..fc15ef0 100755 --- a/siro_driver/lib/controller/functions/location_controller.dart +++ b/siro_driver/lib/controller/functions/location_controller.dart @@ -19,6 +19,7 @@ import '../firebase/local_notification.dart'; import '../home/captin/home_captain_controller.dart'; import '../home/captin/map_driver_controller.dart'; import '../home/payment/captain_wallet_controller.dart'; +import '../home/navigation/navigation_controller.dart'; import 'background_service.dart'; import 'crud.dart'; @@ -539,6 +540,16 @@ class LocationController extends GetxController with WidgetsBindingObserver { } } + if (Get.isRegistered()) { + final mapCtrl = Get.find(); + mapCtrl.handleLocationUpdateFromCentral(pos, speed, heading); + } + + if (Get.isRegistered()) { + final navCtrl = Get.find(); + navCtrl.handleLocationUpdateFromCentral(pos, speed, heading); + } + await _saveBehaviorIfMoved(pos, now, currentSpeed: speed); }, onError: (e) => Log.print('❌ Location Stream Error: $e')); } diff --git a/siro_driver/lib/controller/home/captin/map_driver_controller.dart b/siro_driver/lib/controller/home/captin/map_driver_controller.dart index 8aa56ae..3baad9e 100755 --- a/siro_driver/lib/controller/home/captin/map_driver_controller.dart +++ b/siro_driver/lib/controller/home/captin/map_driver_controller.dart @@ -2570,27 +2570,19 @@ class MapDriverController extends GetxController } void _startLocationListening() { - _locationSubscription?.cancel(); - _locationSubscription = geo.Geolocator.getPositionStream( - locationSettings: const geo.LocationSettings( - accuracy: geo.LocationAccuracy.bestForNavigation, - distanceFilter: 2, - ), - ).listen((geo.Position pos) { - _handleLocationUpdate(pos); - }); + // Location stream is now centralized in LocationController to prevent device hanging. + // LocationController will call handleLocationUpdateFromCentral directly. } /// [Fix C-4] تحديث myLocation في المستمع الأساسي - void _handleLocationUpdate(geo.Position pos) { - final newLoc = LatLng(pos.latitude, pos.longitude); + void handleLocationUpdateFromCentral(LatLng newLoc, double posSpeed, double posHeading) { myLocation = newLoc; // ← [Fix C-4] تحديث الموقع الفوري _oldLoc = smoothedLocation ?? newLoc; _targetLoc = newLoc; _oldHeading = smoothedHeading; - if (pos.speed > 0.5) { - _targetHeading = pos.heading; + if (posSpeed > 0.5) { + _targetHeading = posHeading; } else { _targetHeading = _oldHeading; } diff --git a/siro_driver/lib/controller/home/captin/order_request_controller.dart b/siro_driver/lib/controller/home/captin/order_request_controller.dart index f58d4f4..169ca95 100755 --- a/siro_driver/lib/controller/home/captin/order_request_controller.dart +++ b/siro_driver/lib/controller/home/captin/order_request_controller.dart @@ -69,6 +69,7 @@ class OrderRequestController extends GetxController // --- الخريطة --- Set polylines = {}; + bool _hasCalculatedFullJourney = false; // حالة التطبيق والصوت bool isInBackground = false; @@ -219,6 +220,11 @@ class OrderRequestController extends GetxController // ---------------------------------------------------------------------- Future _calculateFullJourney() async { + if (_hasCalculatedFullJourney) { + if (mapController != null) zoomToFitRide(); + return; + } + _hasCalculatedFullJourney = true; // Don't block on mapController being null - we'll draw routes // and markers first, then zoom when controller is ready bool canZoom = mapController != null; @@ -281,7 +287,7 @@ class OrderRequestController extends GetxController totalTripDistance = tripResult['distance_text']; totalTripDuration = tripResult['duration_text']; polylines.add(tripResult['polyline']); - + // 🔥 تخزين استجابة السيرفر كاملة (بما فيها الـ points والـ instructions) if (tripResult['raw_response'] != null) { box.write('cached_trip_route', tripResult['raw_response']); diff --git a/siro_driver/lib/controller/home/navigation/navigation_controller.dart b/siro_driver/lib/controller/home/navigation/navigation_controller.dart index e607f6d..e2a4f08 100644 --- a/siro_driver/lib/controller/home/navigation/navigation_controller.dart +++ b/siro_driver/lib/controller/home/navigation/navigation_controller.dart @@ -476,32 +476,18 @@ class NavigationController extends GetxController } void _startLocationStream() { - _locationStreamSubscription?.cancel(); - // Listen to location updates with minimum distance filter of 2 meters - // This provides real-time updates without the 3-4 second delay - _locationStreamSubscription = Geolocator.getPositionStream( - locationSettings: const LocationSettings( - accuracy: LocationAccuracy.high, - distanceFilter: 2, // Update every 2 meters - ), - ).listen( - (Position position) { - _handleLocationUpdate(position); - }, - onError: (error) { - Log.print("DEBUG: Location stream error: $error"); - }, - ); + // Location stream is now centralized in LocationController to prevent device hanging. + // LocationController will call handleLocationUpdateFromCentral directly. } bool _isProcessing = false; - Future _handleLocationUpdate(Position position) async { + Future handleLocationUpdateFromCentral(LatLng newLoc, double locSpeed, double locHeading) async { if (_isProcessing) return; _isProcessing = true; try { - final newLoc = LatLng(position.latitude, position.longitude); - currentSpeed = position.speed * 3.6; // Convert m/s to km/h + currentSpeed = locSpeed; // Convert m/s to km/h already done by location controller if needed, wait location_controller sends raw speed or km/h? It sends raw speed. So we should * 3.6 + currentSpeed = locSpeed * 3.6; // Skip if movement is too small if (_lastProcessedLocation != null) { @@ -544,7 +530,7 @@ class NavigationController extends GetxController _targetLoc!.longitude, ); } else { - _targetHeading = position.heading; + _targetHeading = locHeading; } _animController?.forward(from: 0.0); diff --git a/siro_rider/knowledge/rider_lifecycle.md b/siro_rider/knowledge/rider_lifecycle.md new file mode 100644 index 0000000..54a7cdd --- /dev/null +++ b/siro_rider/knowledge/rider_lifecycle.md @@ -0,0 +1,182 @@ +# 🚶 دورة حياة الرحلة - تطبيق الراكب (Rider Ride Lifecycle) + +
+هذا الملف هو مرجع هندسي شامل ودقيق (Source of Truth) يوضح كافة تفاصيل دورة حياة الرحلة (Ride Lifecycle) في تطبيق الراكب (Siro Rider). تمت كتابته وتوثيقه ليكون دليلاً استرشادياً كاملاً لأي مهندس برمجي أو نموذج ذكاء اصطناعي (AI Agent) يرغب في فهم أو تعديل منطق تتبع السائق، استقبال أحداث الرحلة، أو معالجة الحالات المباشرة. +
+ +--- + +## 1. الكنترولر المسؤول عن الرحلة (RideLifecycleController) + +
+يعد +[RideLifecycleController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart) +هو عقل تطبيق الراكب الذي يدير الحالات والعدادات ومطابقة حركة السائق. +
+ +### 1.1 التهيئة وفحص البداية (Startup & Restoration) + +
+* فحص حالة الرحلة الأولية عند بدء التشغيل: +
+ +```dart +Future _checkInitialRideStatus() async +``` + +
+تقوم بالخطوات التالية عند تشغيل التطبيق: +1. استدعاء API جلب الحالة الحالية من الخادم عبر `getRideStatusFromStartApp()`. +2. إذا وجدت رحلة معلقة أو فعالة، تقوم بمطابقة حالة الرحلة النصية وتحويلها إلى قيم الـ Enum الخاص بـ `RideState` كالتالي: + * `"waiting"` -> `RideState.searching` (البحث عن سائق). + * `"apply"`, `"applied"`, `"accepted"` -> `RideState.driverApplied` (السائق قبل وهو في الطريق للراكب). + * `"arrived"` -> `RideState.driverArrived` (السائق وصل لنقطة الالتقاء). + * `"begin"` -> `RideState.inProgress` (الرحلة بدأت بالفعل نحو الوجهة النهائية). + * `"cancel"` -> `RideState.cancelled`. + * `"finished"` -> إذا كانت الرحلة بحاجة لتقييم (`needsReview == 1`) تحال لـ `RideState.preCheckReview` وإلا `RideState.noRide`. +3. استدعاء الموجه المركزي `_handleRideState()` لتوجيه واجهة المستخدم وتنشيط المهام التبعية. +
+ +### 1.2 موجه ومراقب حالة الرحلة (State Machine Coordinator) + +
+* مراقب الحالات الرئيسي: +
+ +```dart +Future _handleRideState(RideState state) async +``` + +
+يعمل كمركز مراقبة مستمر (Polling loop fallback) يقوم بالتحقق الدوري وتحديث شاشات الراكب ومؤقتات المراقبة عند كل حالة: +1. **`RideState.searching`**: يتحقق من فوات زمن البحث الأقصى (`_totalSearchTimeoutSeconds`). في حال انقضاء الوقت بدون موافقة، يوقف المؤشرات ويظهر نافذة زيادة السعر للراكب عبر `_showIncreaseFeeDialog()`. +2. **`RideState.driverApplied`**: إذا كان الويب سوكيت غير متصل، يعتمد على التحديثات الدورية عبر API `getRideStatus` للتحقق مما إذا كان السائق قد وصل (`Arrived`) أو بدأ الرحلة (`Begin`). +3. **`RideState.driverArrived`**: يقوم بتفعيل نافذة إشعار وصول السائق للراكب وبدء مؤقت الخمس دقائق المخصصة لانتظار الراكب. +4. **`RideState.inProgress`**: في حال غياب اتصال السوكيت، يقوم بعمل استعلام دوري للتحقق مما إذا أنهى السائق الرحلة (`Finished`) لتوجيه الراكب فوراً لصفحة الدفع والتقييم. +
+ +### 1.3 معالجة أحداث السائق التفاعلية (Driver Events Handler) + +
+* قبول الطلب من السائق (Acceptance): +
+ +```dart +Future processRideAcceptance({Map? driverData, required String source}) +``` + +
+عند قبول الطلب (سواء عبر الـ Socket أو Polling): +1. نقل حالة التطبيق فوراً لـ `RideState.driverApplied` وتحديث الحالة لـ `'Apply'`. +2. استخراج بيانات السائق الفردية وتخزينها محلياً عبر `_fillDriverDataLocally()`. +3. إيقاظ خدمة الأنشطة الحية على iOS عبر `IosLiveActivityService.startRideActivity()`. +4. إرسال إشعار فوري للراكب عبر `RideLiveNotification.showDriverOnWay()`. +5. جلب موقع السائق الفعلي من الخريطة ورسم المسار بين السائق والراكب عبر `calculateDriverToPassengerRoute()`. +6. تشغيل مؤقت حساب المسافة والزمن التقديري (`startTimerFromDriverToPassengerAfterApplied()`). +
+ +
+* وصول السائق للراكب (Driver Arrival): +
+ +```dart +Future processDriverArrival(String source) +``` + +
+1. تعيين حالة الرحلة محلياً لـ `RideState.driverArrived` وحفظ الحالة النصية لـ `'Arrived'`. +2. عرض ديالوج وصول السائق للراكب عبر `uiInteractions.driverArrivePassengerDialoge()`. +3. تفعيل إشعار وصول السائق العام عبر النظام. +4. تفعيل مؤقت انتظار السائق للراكب (5 دقائق). +5. الاستعداد المسبق ورسم الخط التقديري لمسار الرحلة الفعلي من موقع السائق إلى الوجهة النهائية للراكب. +
+ +
+* بدء الرحلة الفعلية (Begin Ride): +
+ +```dart +Future processRideBegin({String source = "Unknown"}) +``` + +
+1. نقل حالة التطبيق لـ `RideState.inProgress` وتخزين الحالة لـ `'Begin'`. +2. إيقاف ومسح مؤقتات الانتظار السابقة. +3. تنظيف مسار الخريطة الأصفر (مسار بيك اب الراكب) بالكامل لمنع التداخل والتشوه البصري. +4. رسم المسار الفعلي النهائي باللون **الأزرق** باتجاه وجهة الراكب النهائية (`myDestination`). +5. تفعيل عداد الرحلة التفاعلي `rideIsBeginPassengerTimer()`. +
+ +
+* إنهاء الرحلة (Finish Ride): +
+ +```dart +Future processRideFinished(List driverList, {String source = "Unknown"}) +``` + +
+1. نقل الحالة لـ `RideState.finished` وإيقاف وتصفير كافة عدادات ومؤقتات المراقبة. +2. تدمير وإغلاق سوكيت الرحلة النشط عبر `mapSocket.disposeRideSocket()`. +3. إيقاف وإغلاق خدمة الملاحة الحية على iOS والأنشطة الحية. +4. توجيه الراكب فوراً لصفحة التقييم والدفع المخصصة `RateDriverFromPassenger` وتمرير معرف السائق والرحلة والفاتورة. +
+ +--- + +## 2. بنية تحديث الويب سوكيت للمواقع (WebSocket Location Handler) + +
+يتولى الكنترولر +[MapSocketController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_rider/lib/controller/home/map/map_socket_controller.dart) +استقبال تحديثات السائق لحظياً عبر الحدث +`driver_location_update` +وتمرير البيانات المعالجة لمحركات الحساب والتحكم كالتالي: +
+ +```dart +void handleDriverLocationUpdate(dynamic data) +``` + +
+الخطوات التي تنفذها الدالة: +1. استلام وتمرير إحداثيات السائق الحالية وسرعته واتجاهه. +2. إذا تم استلام أكثر من 3 تحديثات ناجحة ومتتالية وموثوقة عبر السوكيت، يقوم بإيقاف استعلامات الـ Polling الدورية (`stopDriverLocationPolling()`) لتوفير استهلاك البيانات والبطارية. +3. استدعاء فحص الانحراف والمطابقة الجغرافية عبر `checkAndRecalculateIfDeviated()`. +4. توجيه كاميرا خريطة الراكب لاتباع إحداثيات السائق مع تعديل نسبة التقريب (Zoom) ديناميكياً بناءً على سرعة السيارة (تقريب الكاميرا عند البطء وتوسيع الرؤية عند السرعة العالية). +5. قراءة الحسابات الجاهزة للوقت المتبقي والمسافة من السيرفر إن وجدت وتمريرها، وإلا يتم استدعاء محرك الحساب المحلي للراكب عبر `updateRemainingRoute()`. +6. تحديث موقع ماركر سيارة السائق على الخريطة بسلاسة وتمرير زاوية الدوران الصحيحة (`updateDriverMarker()`). +
+ +--- + +## 3. محرك الحساب المحلي وحماية الانحراف (Local Routing & Deviation Engine) + +
+لتلافي المشاكل الجغرافية وعرض أرقام مضللة للراكب، تم بناء محركات ذكية ومحلية لمعالجة المواقع: +
+ +### 3.1 مطابقة المسار وإعادة الحساب (Deviation Guard) + +```dart +Future checkAndRecalculateIfDeviated(LatLng driverPos, {double? heading, double? speed}) +``` + +
+1. تقوم الدالة بمسح كافة نقاط المسار النشط ومقارنتها بموقع السائق الحالي لحساب أقرب نقطة جغرافية. +2. إذا تجاوزت المسافة الفاصلة بين السائق والمسار المخطط قيمة حد الانحراف (`_deviationThresholdMeters` - عادة 50 متر)، أو عند تطابق انحراف الاتجاه لمرتين متتاليتين، يتم إصدار أمر فوري بإعادة جلب ورسم المسار من إحداثيات السائق الحالية: + * إذا كانت الرحلة جارية نحو الوجهة: يتم رسم مسار جديد باتجاه وجهة الراكب النهائية. + * إذا كان السائق في طريقه للراكب: يتم رسم مسار جديد باتجاه موقع الراكب. +
+ +### 3.2 قص المسار واحتساب الـ ETA محلياً (Dynamic Route Trimming) + +```dart +void updateRemainingRoute(LatLng driverPos, {bool updateEta = true}) +``` + +
+1. عند تقدم السائق، تقوم الدالة بالبحث عن أقرب نقطة على خط المسار المتجه نحو الوجهة وقص (Trim) الأجزاء التي قطعتها السيارة بالفعل (`remainingPoints = route.sublist(closestIdx)`). +2. عند تفعيل `updateEta` (في حال عدم توفر معلومات مباشرة من السيرفر)، تقوم الدالة بحساب النسبة المئوية للمسافة المتبقية مقارنة بالمسافة الكلية الأصلية، وضربها في الزمن الكلي التقديري الأصلي لاستخراج قيمة زمن الوصول التقديري (ETA) المتبقي **محلياً وبشكل لحظي ودقيق جداً** دون الحاجة لإجراء اتصالات إضافية بالسيرفر. +3. تحديث خطوط الـ Polylines النشطة على الخريطة بالمسار المقصوص الجديد لضمان بقائه دقيقاً. +
diff --git a/siro_rider/lib/controller/firebase/firbase_messge.dart b/siro_rider/lib/controller/firebase/firbase_messge.dart index e24c3c8..5b54235 100644 --- a/siro_rider/lib/controller/firebase/firbase_messge.dart +++ b/siro_rider/lib/controller/firebase/firbase_messge.dart @@ -87,12 +87,6 @@ class FirebaseMessagesController extends GetxController { fireBaseTitles(message); } }); - FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async { - // Handle background message - if (message.data.isNotEmpty) { - fireBaseTitles(message); - } - }); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { if (message.data.isNotEmpty && message.notification != null) { diff --git a/siro_rider/lib/controller/home/map/map_socket_controller.dart b/siro_rider/lib/controller/home/map/map_socket_controller.dart index 3d73bae..95fbf21 100644 --- a/siro_rider/lib/controller/home/map/map_socket_controller.dart +++ b/siro_rider/lib/controller/home/map/map_socket_controller.dart @@ -283,7 +283,7 @@ class MapSocketController extends GetxController { } final dynamic distanceValue = - data['distance_m'] ?? data['distance_meters'] ?? data['distance']; + data['distance_m'] ?? data['distance_meters']; final double? distanceMeters = double.tryParse(distanceValue?.toString() ?? ''); final int? etaSeconds = data['eta_seconds'] == null diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart index c229ad2..c264a61 100644 --- a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart +++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart @@ -112,6 +112,7 @@ class RideLifecycleController extends GetxController { late String driverId = ''; late String make = ''; late String model = ''; + late String gender = ''; late String carColor = ''; late String licensePlate = ''; late String driverName = ''; @@ -120,6 +121,9 @@ class RideLifecycleController extends GetxController { late String colorHex = ''; late String carYear = ''; late String driverRate = '5.0'; + late String driverRatingCount = '0'; + late String driverCompletedRides = '0'; + late String driverTier = 'Verified driver'; late String driverToken = ''; double kazan = 8; @@ -1481,7 +1485,8 @@ class RideLifecycleController extends GetxController { // إيقاف جلب السيارات المجاورة ومسحها، باستثناء السائق الذي قبل الطلب mapEngine.reloadStartApp = false; - mapEngine.markers.removeWhere((marker) => marker.markerId.value != driverId.toString()); + mapEngine.markers + .removeWhere((marker) => marker.markerId.value != driverId.toString()); mapEngine.update(); await getDriverCarsLocationToPassengerAfterApplied(); @@ -1490,8 +1495,7 @@ class RideLifecycleController extends GetxController { LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last; Log.print( '[rideAppliedFromDriver] 📍 Driver at: $driverPos, Passenger at: $passengerLocation'); - await getInitialDriverDistanceAndDuration(driverPos, passengerLocation); - await drawDriverPathOnly(driverPos, passengerLocation); + await calculateDriverToPassengerRoute(driverPos, passengerLocation); mapEngine.fitCameraToPoints(driverPos, passengerLocation); } @@ -1656,6 +1660,9 @@ class RideLifecycleController extends GetxController { driverToken = data['token']?.toString() ?? ''; carYear = data['year']?.toString() ?? ''; driverRate = data['ratingDriver']?.toString() ?? '5.0'; + driverRatingCount = data['ratingCount']?.toString() ?? '0'; + driverCompletedRides = data['completedRides']?.toString() ?? '0'; + driverTier = data['driverTier']?.toString() ?? 'Verified driver'; update(); } @@ -2221,6 +2228,15 @@ class RideLifecycleController extends GetxController { polyLines = polyLines .where((p) => !p.polylineId.value.startsWith('driver_route')) .toSet(); + polyLines = { + ...polyLines, + Polyline( + polylineId: const PolylineId('main_route'), + points: decodedPoints, + color: const Color(0xFF2196F3), + width: 6, + ) + }; } else { // مسح السلمات القديمة أولاً polyLines = polyLines @@ -2290,7 +2306,9 @@ class RideLifecycleController extends GetxController { _routeHeadingMismatchCount = 0; _isRecalculatingRoute = true; if (statusRide == 'Begin' || - currentRideState.value == RideState.inProgress) { + statusRide == 'Arrived' || + currentRideState.value == RideState.inProgress || + currentRideState.value == RideState.driverArrived) { await calculateDriverToPassengerRoute(driverPos, myDestination, isBeginPhase: true); } else { @@ -2504,6 +2522,8 @@ class RideLifecycleController extends GetxController { String icon; if (model.contains('دراجة') || make.contains('دراجة')) { icon = mapEngine.motoIcon; + } else if (gender == 'Female') { + icon = mapEngine.ladyIcon; } else { icon = mapEngine.carIcon; } @@ -3026,6 +3046,17 @@ class RideLifecycleController extends GetxController { mapEngine.playRouteAnimation( mapEngine.polylineCoordinates, mapEngine.lastComputedBounds); } + + if (driverCarsLocationToPassengerAfterApplied.isNotEmpty && + myDestination.latitude != 0 && + myDestination.longitude != 0) { + await calculateDriverToPassengerRoute( + driverCarsLocationToPassengerAfterApplied.last, + myDestination, + isBeginPhase: true, + ); + } + update(); } @@ -3903,12 +3934,37 @@ class RideLifecycleController extends GetxController { make = data['make']?.toString() ?? ''; model = data['model']?.toString() ?? ''; + gender = data['gender']?.toString() ?? ''; carColor = data['color']?.toString() ?? ''; colorHex = data['color_hex']?.toString() ?? ''; licensePlate = data['car_plate']?.toString() ?? ''; carYear = data['year']?.toString() ?? ''; + // المحاولة الفورية لرسم السائق إذا توفرت الإحداثيات في البيانات + double lat = double.tryParse( + data['latitude']?.toString() ?? data['lat']?.toString() ?? '0') ?? + 0; + double lng = double.tryParse(data['longitude']?.toString() ?? + data['lng']?.toString() ?? + '0') ?? + 0; + double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0; + + if (lat != 0 && lng != 0) { + LatLng initialPos = LatLng(lat, lng); + if (driverCarsLocationToPassengerAfterApplied.isEmpty) { + driverCarsLocationToPassengerAfterApplied.add(initialPos); + } else { + driverCarsLocationToPassengerAfterApplied[0] = initialPos; + } + // تحديث الماركر فوراً لضمان ظهوره بشكل موثوق + updateDriverMarker(initialPos, heading); + } + driverRate = data['ratingDriver']?.toString() ?? '5.0'; + driverRatingCount = data['ratingCount']?.toString() ?? '0'; + driverCompletedRides = data['completedRides']?.toString() ?? '0'; + driverTier = data['driverTier']?.toString() ?? 'Verified driver'; driverToken = data['token']?.toString() ?? ''; update(); @@ -4185,55 +4241,6 @@ class RideLifecycleController extends GetxController { ); } - Future getDistanceFromDriverAfterAcceptedRide( - String origin, String destination) async { - String apiKey = Env.mapKeyOsm; - if (origin.isEmpty) { - origin = '${passengerLocation.latitude},${passengerLocation.longitude}'; - } - var uri = Uri.parse( - '$dynamicApiUrl?origin=$origin&destination=$destination&steps=false&overview=false'); - Log.print('uri: $uri'); - - http.Response response; - Map responseData; - - try { - response = await http.get( - uri, - headers: { - 'X-API-KEY': apiKey, - }, - ).timeout(const Duration(seconds: 20)); - - if (response.statusCode != 200) { - Log.print('Error from API: ${response.statusCode}'); - isLoading = false; - update(); - return; - } - if (Get.isBottomSheetOpen ?? false) { - Get.back(); - } - isDrawingRoute = false; - - responseData = json.decode(response.body); - Log.print('responseData: $responseData'); - - if (responseData['status'] != 'ok') { - Log.print('API returned an error: ${responseData['message']}'); - isLoading = false; - update(); - return; - } - } catch (e) { - Log.print('Failed to get directions: $e'); - isLoading = false; - update(); - return; - } - } - Future _stageNiceToHave() async { Log.print('🚀 MapPassengerController: Starting _stageNiceToHave'); diff --git a/siro_rider/lib/controller/home/map/ui_interactions_controller.dart b/siro_rider/lib/controller/home/map/ui_interactions_controller.dart index 388c28e..afa97d9 100644 --- a/siro_rider/lib/controller/home/map/ui_interactions_controller.dart +++ b/siro_rider/lib/controller/home/map/ui_interactions_controller.dart @@ -4,7 +4,6 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; -import 'package:intaleq_maps/intaleq_maps.dart'; import '../../../constant/box_name.dart'; import '../../../constant/colors.dart'; @@ -15,19 +14,13 @@ import '../../../main.dart'; // contains global 'box' import '../../../print.dart'; import '../../../services/emergency_signal_service.dart'; import '../../../views/widgets/elevated_btn.dart'; -import '../../../views/widgets/mydialoug.dart'; import '../../../views/widgets/my_textField.dart'; -import '../../../views/home/map_page_passenger.dart'; -import '../../../views/widgets/error_snakbar.dart'; -import '../../../models/model/painter_copoun.dart'; import '../../functions/launch.dart'; -import '../../firebase/local_notification.dart'; import '../../firebase/notification_service.dart'; import '../../functions/crud.dart'; import '../../functions/tts.dart'; import 'ride_lifecycle_controller.dart'; import 'location_search_controller.dart'; -import 'map_engine_controller.dart'; class UiInteractionsController extends GetxController { TextEditingController sosPhonePassengerProfile = TextEditingController(); @@ -56,54 +49,54 @@ class UiInteractionsController extends GetxController { sosPhonePassengerProfile.clear(); Get.defaultDialog( - title: 'Add SOS Phone'.tr, - titleStyle: AppStyle.title, - content: Form( - key: sosFormKey, - child: Column( - children: [ - MyTextForm( - controller: sosPhonePassengerProfile, - label: 'insert sos phone'.tr, - hint: 'e.g. 0912345678 (Default +963)'.tr, - type: TextInputType.phone, - ), - const SizedBox(height: 10), - Text( - "Note: If no country code is entered, it will be saved as Syrian (+963).".tr, - style: TextStyle(fontSize: 12, color: Colors.grey), - textAlign: TextAlign.center, - ), - ], + title: 'Add SOS Phone'.tr, + titleStyle: AppStyle.title, + content: Form( + key: sosFormKey, + child: Column( + children: [ + MyTextForm( + controller: sosPhonePassengerProfile, + label: 'insert sos phone'.tr, + hint: 'e.g. 0912345678 (Default +963)'.tr, + type: TextInputType.phone, + ), + const SizedBox(height: 10), + Text( + "Note: If no country code is entered, it will be saved as Syrian (+963)." + .tr, + style: TextStyle(fontSize: 12, color: Colors.grey), + textAlign: TextAlign.center, + ), + ], + ), ), - ), - confirm: MyElevatedButton( - title: 'Save'.tr, - onPressed: () async { - if (sosFormKey.currentState!.validate()) { - Get.back(); - var numberPhone = - formatSyrianPhoneNumber(sosPhonePassengerProfile.text); + confirm: MyElevatedButton( + title: 'Save'.tr, + onPressed: () async { + if (sosFormKey.currentState!.validate()) { + Get.back(); + var numberPhone = + formatSyrianPhoneNumber(sosPhonePassengerProfile.text); - await CRUD().post( - link: AppLink.updateprofile, - payload: { - 'id': box.read(BoxName.passengerID), - 'sosPhone': numberPhone, - }, - ); + await CRUD().post( + link: AppLink.updateprofile, + payload: { + 'id': box.read(BoxName.passengerID), + 'sosPhone': numberPhone, + }, + ); - box.write(BoxName.sosPhonePassenger, numberPhone); - onSuccess(); - } - }, - ), - cancel: MyElevatedButton( - title: 'Cancel'.tr, - onPressed: () => Get.back(), - kolor: AppColor.redColor, - ) - ); + box.write(BoxName.sosPhonePassenger, numberPhone); + onSuccess(); + } + }, + ), + cancel: MyElevatedButton( + title: 'Cancel'.tr, + onPressed: () => Get.back(), + kolor: AppColor.redColor, + )); } void sosPassenger() { @@ -114,10 +107,12 @@ class UiInteractionsController extends GetxController { titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), content: Column( children: [ - Icon(Icons.warning_amber_rounded, size: 50, color: AppColor.redColor), + Icon(Icons.warning_amber_rounded, + size: 50, color: AppColor.redColor), const SizedBox(height: 10), Text( - "Do you want to send an emergency message to your SOS contact?".tr, + "Do you want to send an emergency message to your SOS contact?" + .tr, textAlign: TextAlign.center, style: AppStyle.title, ), diff --git a/siro_rider/lib/controller/local/translations.dart b/siro_rider/lib/controller/local/translations.dart index 5244054..d42c370 100644 --- a/siro_rider/lib/controller/local/translations.dart +++ b/siro_rider/lib/controller/local/translations.dart @@ -42,6 +42,7 @@ class MyTranslation extends Translations { "Arrived": "وصلنا", "Audio Recording": "تسجيل صوتي", "Call": "اتصال", + "Call Options": "خيارات الاتصال", "Call Connected": "تم فتح الاتصال", "Call Support": "اتصل بالدعم", "Call left": "مكالمات متبقية", @@ -49,6 +50,8 @@ class MyTranslation extends Translations { "Change Photo": "تغيير الصورة", "Captain": "الكابتن", "Choose from Gallery": "اختر من المعرض", + "Choose how you want to call the driver": + "اختر طريقة الاتصال بالكابتن", "Choose from contact": "اختر من جهات الاتصال", "Click to track the trip": "اضغط لتتبع المشوار", "Close panel": "إغلاق اللوحة", @@ -92,6 +95,9 @@ class MyTranslation extends Translations { "Finished": "انتهى", "Fixed Price": "سعر ثابت", "Free Call": "مكالمة مجانية", + "Professional driver": "كابتن محترف", + "Trusted driver": "كابتن موثوق", + "Verified driver": "كابتن موثق", "General": "عام", "Grant": "منح الإذن", "Have a Promo Code?": "معك كود خصم؟", @@ -178,6 +184,7 @@ class MyTranslation extends Translations { "Preferences": "التفضيلات", "Profile photo updated": "تم تحديث صورة الغلاف", "Quick Message": "رسالة سريعة", + "reviews": "تقييم", "Rating is": "التقييم هو", "Received empty route data.": "تم استلام بيانات طريق فارغة.", "Record": "تسجيل", @@ -211,6 +218,7 @@ class MyTranslation extends Translations { "Set as Work": "تحديد كالشغل", "Share": "مشاركة", "Share Trip": "مشاركة المشوار", + "Standard Call": "اتصال عادي", "Share your experience to help us improve...": "شاركنا تجربتك لنحسن خدمتنا...", "Something went wrong. Please try again.": "صار غلط. جرب مرة تانية.", @@ -271,6 +279,8 @@ class MyTranslation extends Translations { "to arrive you.": "ليوصلك.", "unknown": "غير معروف", "wait 1 minute to recive message": "استنى دقيقة لتستلم الرسالة", + "Uses cellular network": "يستخدم شبكة الهاتف", + "Voice call over internet": "مكالمة صوتية عبر الإنترنت", "with license plate": "برقم اللوحة", "witout zero": "بدون صفر", "you must insert token code": "لازم تدخل الكود", @@ -16885,7 +16895,8 @@ class MyTranslation extends Translations { "Support is Away": "سپورٹ اب دستیاب نہیں ہے", "Support is currently Online": "سپورٹ اب آن لائن ہے", "Voice Call": "صوتی کال", - "We're here to help you 24/7": "ہم چوبیس گھنٹے آپ کی مدد کے لیے حاضر ہیں", + "We're here to help you 24/7": + "ہم چوبیس گھنٹے آپ کی مدد کے لیے حاضر ہیں", "Working Hours:": "کام کے اوقات:", "1 Passenger": "1 Passenger", "2 Passengers": "2 Passengers", @@ -18446,7 +18457,8 @@ class MyTranslation extends Translations { "Support is Away": "सहायता अभी उपलब्ध नहीं है", "Support is currently Online": "सहायता अभी ऑनलाइन है", "Voice Call": "वॉइस कॉल", - "We're here to help you 24/7": "हम आपकी सहायता के लिए 24/7 उपलब्ध हैं", + "We're here to help you 24/7": + "हम आपकी सहायता के लिए 24/7 उपलब्ध हैं", "Working Hours:": "कार्य समय:", "1 Passenger": "1 Passenger", "2 Passengers": "2 Passengers", diff --git a/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart b/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart index 8168f4f..a0689dc 100644 --- a/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart +++ b/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart @@ -250,19 +250,23 @@ class ApplyOrderWidget extends StatelessWidget { Row( children: [ // صورة السائق (أصغر) - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: AppColor.primaryColor.withOpacity(0.2), width: 2), - ), - child: CircleAvatar( - radius: 22, // تصغير من 28 إلى 22 - backgroundColor: Colors.grey[200], - backgroundImage: NetworkImage( - '${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'), - onBackgroundImageError: (_, __) => - const Icon(Icons.person, color: Colors.grey, size: 20), + GestureDetector( + onTap: () => _showDriverAvatarDialog(context, controller), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: AppColor.primaryColor.withOpacity(0.2), + width: 2), + ), + child: CircleAvatar( + radius: 22, // تصغير من 28 إلى 22 + backgroundColor: Colors.grey[200], + backgroundImage: NetworkImage( + '${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'), + onBackgroundImageError: (_, __) => + const Icon(Icons.person, color: Colors.grey, size: 20), + ), ), ), @@ -299,6 +303,32 @@ class ApplyOrderWidget extends StatelessWidget { ), ], ), + const SizedBox(height: 5), + Wrap( + spacing: 6, + runSpacing: 4, + children: [ + _buildDriverBadge( + icon: Icons.verified_rounded, + text: controller.driverTier.tr, + color: AppColor.primaryColor, + ), + if (controller.driverCompletedRides != '0') + _buildDriverBadge( + icon: Icons.route_rounded, + text: + '${controller.driverCompletedRides} ${'rides'.tr}', + color: Colors.teal, + ), + if (controller.driverRatingCount != '0') + _buildDriverBadge( + icon: Icons.reviews_rounded, + text: + '${controller.driverRatingCount} ${'reviews'.tr}', + color: Colors.amber.shade800, + ), + ], + ), ], ), ), @@ -320,6 +350,11 @@ class ApplyOrderWidget extends StatelessWidget { Widget _buildMicroCarIcon( RideLifecycleController controller, Color Function(String) parseColor) { Color carColor = parseColor(controller.colorHex); + final String vehicleText = + '${controller.model} ${controller.make}'.toLowerCase(); + final bool isBike = vehicleText.contains('scooter') || + vehicleText.contains('bike') || + vehicleText.contains('دراجة'); return Container( height: 40, // تصغير من 50 width: 40, @@ -331,7 +366,8 @@ class ApplyOrderWidget extends StatelessWidget { child: ColorFiltered( colorFilter: ColorFilter.mode(carColor, BlendMode.srcIn), child: Image.asset( - box.read(BoxName.carType) == 'Scooter' || + isBike || + box.read(BoxName.carType) == 'Scooter' || box.read(BoxName.carType) == 'Pink Bike' ? 'assets/images/moto.png' : 'assets/images/car3.png', @@ -341,6 +377,81 @@ class ApplyOrderWidget extends StatelessWidget { ); } + Widget _buildDriverBadge({ + required IconData icon, + required String text, + required Color color, + }) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 11, color: color), + const SizedBox(width: 4), + Text( + text, + style: TextStyle( + color: color, + fontSize: 10.5, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ); + } + + void _showDriverAvatarDialog( + BuildContext context, RideLifecycleController controller) { + final imageUrl = + '${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'; + Get.dialog( + Dialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 38), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), + child: Padding( + padding: const EdgeInsets.fromLTRB(18, 20, 18, 18), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + radius: 58, + backgroundColor: Colors.grey[200], + backgroundImage: NetworkImage(imageUrl), + onBackgroundImageError: (_, __) {}, + ), + const SizedBox(height: 14), + Text( + controller.driverName, + textAlign: TextAlign.center, + style: AppStyle.title.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 6), + Text( + '${controller.driverTier.tr} • ${controller.driverRate}', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey[700], + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + barrierDismissible: true, + ); + } + Widget _buildSlimLicensePlate(String plateNumber) { return Container( width: double.infinity, diff --git a/socket_intaleq/driver_socket.php b/socket_intaleq/driver_socket.php new file mode 100644 index 0000000..4b5ebfc --- /dev/null +++ b/socket_intaleq/driver_socket.php @@ -0,0 +1,547 @@ +ping(); + return $redis; + } catch (\Exception $e) { + logMsg('⚠️ Redis ping failed, reconnecting...'); + $redis = null; + } + } + + try { + $client = new RedisClient([ + 'scheme' => 'tcp', + 'host' => '127.0.0.1', + 'port' => 6379, + 'password' => $redisPass, + 'read_write_timeout' => 0, + ]); + $client->connect(); + $redis = $client; + return $redis; + } catch (\Exception $e) { + logMsg('❌ Redis Error: ' . $e->getMessage()); + return null; + } +} + +// ============================================================ +// 📐 Haversine Distance (متر) +// ============================================================ +function haversineDistance(float $lat1, float $lng1, float $lat2, float $lng2): float { + $R = 6371000; + $dLat = deg2rad($lat2 - $lat1); + $dLng = deg2rad($lng2 - $lng1); + $a = sin($dLat / 2) ** 2 + + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2; + return $R * 2 * atan2(sqrt($a), sqrt(1 - $a)); +} + +// ============================================================ +// 📡 Forward موقع السائق → سيرفر الراكب (ASYNC) +// ============================================================ +function forwardLocationToPassengerSocket( + string $driverId, + string $passengerId, + array $payload, + string $internalKey, + array &$fwdThrottle +): void { + if (empty($passengerId)) return; + + $now = time(); + $last = $fwdThrottle[$driverId] ?? null; + + if ($last !== null) { + $timeDiff = $now - $last['ts']; + $dist = haversineDistance( + $last['lat'], $last['lng'], + (float)$payload['lat'], (float)$payload['lng'] + ); + if ($dist < FORWARD_MIN_METERS && $timeDiff < FORWARD_MAX_SECONDS) return; + } + + $fwdThrottle[$driverId] = [ + 'ts' => $now, + 'lat' => (float)$payload['lat'], + 'lng' => (float)$payload['lng'], + ]; + + $http = new AsyncHttp(); + $http->request( + 'http://127.0.0.1:3031', + [ + 'method' => 'POST', + 'data' => http_build_query([ + 'action' => 'update_driver_location', + 'passenger_id' => $passengerId, + 'payload' => json_encode($payload), + ]), + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + 'x-internal-key' => $internalKey, + 'Connection' => 'close', + ], + 'timeout' => 3, + ], + null, + fn(\Exception $e) => logMsg('⚠️ Forward failed: ' . $e->getMessage()) + ); +} + +// ============================================================ +// 📲 FCM (ASYNC) +// ============================================================ +function sendFCM_Async(string $token, string $title, string $body, array $rideData): void { + if (empty($token)) return; + + $http = new AsyncHttp(); + $http->request( + 'https://api.intaleq.xyz/intaleq/ride/firebase/send_fcm.php', + [ + 'method' => 'POST', + 'data' => json_encode([ + 'target' => $token, + 'title' => $title, + 'body' => $body, + 'isTopic' => false, + 'category' => 'Order', + 'tone' => 'start', + 'passengerList' => json_encode($rideData), + ]), + 'headers' => ['Content-Type' => 'application/json; charset=UTF-8'], + 'timeout' => 5, + ], + null, + fn(\Exception $e) => logMsg('⚠️ FCM failed: ' . $e->getMessage()) + ); +} + +// ============================================================ +// 🧠 Memory State & Event Buffer +// ============================================================ +$connectedDrivers = []; +$active_orders_drivers = []; +$driverState = []; +$fwdThrottle = []; +$eventBuffer = []; // 🚀 Level 2: مصفوفة تجميع الأحداث لـ Redis + +// ============================================================ +// 🚀 Socket.IO — بورت 2020 +// ============================================================ +$io = new SocketIO(2020); + +// ============================================================ +// A. Internal HTTP Server & Redis Batch Processor (Worker Start) +// ============================================================ +$io->on('workerStart', function () use ($io, $INTERNAL_KEY) { + + // 🚀 1. Redis Pipeline Batch Processor (Level 2) + // يعمل كل نصف ثانية، يجمع كل الأوامر ويرسلها لـ Redis دفعة واحدة + Timer::add(REDIS_BATCH_INTERVAL, function() { + global $eventBuffer; + if (empty($eventBuffer)) return; + + $redis = getRedis(); + if (!$redis) return; + + try { + $pipe = $redis->pipeline(); + $processedCount = 0; + + foreach ($eventBuffer as $driverId => $ops) { + $profileKey = "driver:profile:$driverId"; + $processedCount++; + + if (isset($ops['hmset'])) { + $pipe->hmset($profileKey, $ops['hmset']); + } + if (isset($ops['expire'])) { + $pipe->expire($profileKey, $ops['expire']); + } + if (isset($ops['status_change'])) { + $oldStatus = $ops['status_change']['old']; + $newStatus = $ops['status_change']['new']; + + if ($oldStatus === 'on') $pipe->zrem('geo:drivers:busy', $driverId); + if ($oldStatus === 'off') $pipe->zrem('geo:drivers:available', $driverId); + + if ($newStatus === 'close' || $newStatus === 'blocked') { + $pipe->zrem('geo:drivers:available', $driverId); + $pipe->zrem('geo:drivers:busy', $driverId); + } + } + if (isset($ops['geoadd'])) { + $st = $ops['geoadd']['status']; + $lng = $ops['geoadd']['lng']; + $lat = $ops['geoadd']['lat']; + + if ($st === 'off') { + $pipe->geoadd('geo:drivers:available', $lng, $lat, $driverId); + } elseif ($st === 'on') { + $pipe->geoadd('geo:drivers:busy', $lng, $lat, $driverId); + } + } + } + + $pipe->execute(); + $eventBuffer = []; // إفراغ المصفوفة بعد التنفيذ الناجح + // logMsg("⚡ Processed Redis Batch: $processedCount drivers updated in 1 network call."); + + } catch (\Exception $e) { + logMsg("⚠️ Redis Pipeline Error: " . $e->getMessage()); + } + }); + + // 🌐 2. Internal HTTP Server — بورت 2021 + $innerHttp = new Worker('http://0.0.0.0:2021'); + + $innerHttp->onMessage = function ($connection, $request) use ($io, $INTERNAL_KEY) { + global $active_orders_drivers, $connectedDrivers; + + $headers = $request->header(); + if (($headers['x-internal-key'] ?? '') !== $INTERNAL_KEY) { + $connection->send('Unauthorized'); + return; + } + + $post = $request->post(); + $action = trim($post['action'] ?? ''); + $redis = getRedis(); + + // ── 1. Dispatch Order ──────────────────────────────── + if ($action === 'dispatch_order') { + $rideId = $post['ride_id'] ?? null; + $drivers = json_decode($post['drivers_ids'] ?? '[]', true); + $payload = $post['payload'] ?? []; + if (is_array($payload)) $payload = array_values($payload); + + if ($rideId && !empty($drivers)) { + $active_orders_drivers[$rideId] = $drivers; + logMsg("🚀 Dispatch Ride #$rideId → " . count($drivers) . ' drivers.'); + } + + foreach ($drivers as $driverId) { + if (!isset($connectedDrivers[$driverId])) continue; + $io->to('driver_' . $driverId)->emit('new_ride_request', $payload); + + $platform = $connectedDrivers[$driverId]['platform'] ?? 'android'; + $token = $connectedDrivers[$driverId]['token'] ?? ''; + if ($platform === 'ios' && !empty($token)) { + sendFCM_Async($token, 'طلب جديد', 'لديك رحلة جديدة قريبة منك', $payload); + } + } + $connection->send('Dispatched'); + + // ── 2. Market New Ride ──────────────────────────────── + } elseif ($action === 'market_new_ride') { + $payload = $post['payload'] ?? []; + $rideId = $payload['id'] ?? null; + $lat = (float)($payload['start_lat'] ?? 0); + $lng = (float)($payload['start_lng'] ?? 0); + + if (!$redis || !$rideId || $lat == 0 || $lng == 0) { + $connection->send('Error: Redis unavailable or invalid coords'); + return; + } + + $redis->geoadd('geo:rides:waiting', $lng, $lat, $rideId); + $nearbyDrivers = $redis->georadius('geo:drivers:available', $lng, $lat, 50, 'km'); + + $count = 0; + foreach ($nearbyDrivers as $driverId) { + if (isset($connectedDrivers[$driverId])) { + $io->to('driver_' . $driverId)->emit('market_new_ride', $payload); + $count++; + } + } + logMsg("📢 Market Ride #$rideId → $count drivers."); + $connection->send("Broadcasted to $count drivers"); + + // ── 3. Get Nearby Ride IDs ──────────────────────────── + } elseif ($action === 'get_nearby_ride_ids') { + $lat = (float)($post['lat'] ?? 0); + $lng = (float)($post['lng'] ?? 0); + $radius = (float)($post['radius'] ?? 9); + + if (!$redis) { $connection->send(json_encode([])); return; } + + $results = $redis->georadius( + 'geo:rides:waiting', $lng, $lat, $radius, 'km', + ['WITHDIST' => true, 'SORT' => 'ASC', 'COUNT' => 40] + ); + $connection->send(json_encode($results)); + + // ── 4. Ride Taken ───────────────────────────────────── + } elseif ($action === 'ride_taken_event') { + $rideId = $post['ride_id'] ?? null; + $winnerDriverId = $post['taken_by_driver_id'] ?? null; + + if (!$rideId) { $connection->send('Error: Missing ride_id'); return; } + + if ($redis) $redis->zrem('geo:rides:waiting', $rideId); + + $io->emit('ride_taken', [ + 'ride_id' => $rideId, + 'taken_by_driver_id' => $winnerDriverId, + ]); + + unset($active_orders_drivers[$rideId]); + logMsg("✅ Ride #$rideId taken by #$winnerDriverId."); + $connection->send('OK'); + + // ── 5. Force Disconnect ─────────────────────────────── + } elseif ($action === 'force_disconnect') { + $driverId = $post['driver_id'] ?? null; + + if ($driverId && isset($connectedDrivers[$driverId])) { + $connectedDrivers[$driverId]['conn']->disconnect(); + unset($connectedDrivers[$driverId]); + + if ($redis) { + $redis->zrem('geo:drivers:available', $driverId); + $redis->zrem('geo:drivers:busy', $driverId); + } + logMsg("🚫 Driver #$driverId force-disconnected."); + $connection->send('Disconnected'); + } else { + $connection->send('Driver not connected'); + } + + } else { + $connection->send('Unknown action'); + } + }; + + $innerHttp->listen(); +}); + +// ============================================================ +// B. WebSocket Events للسائقين +// ============================================================ +$io->on('connection', function ($socket) use ($INTERNAL_KEY) { + global $connectedDrivers, $driverState, $fwdThrottle, $eventBuffer; + + $query = $socket->handshake['query'] ?? []; + $driverId = $query['driver_id'] ?? null; + $platform = $query['platform'] ?? 'android'; + $token = $query['token'] ?? ''; + + if (!$driverId) { + $socket->disconnect(); + return; + } + + $socket->join('driver_' . $driverId); + $connectedDrivers[$driverId] = [ + 'conn' => $socket, + 'platform' => $platform, + 'token' => $token, + ]; + + if (!isset($driverState[$driverId])) { + $driverState[$driverId] = [ + 'lat' => 0.0, + 'lng' => 0.0, + 'speed' => -999.0, + 'heading' => -999.0, + 'status' => '', + 'expire_ts' => 0, + ]; + } + + logMsg("✅ Driver Connected: #$driverId ($platform)"); + + $socket->on('ping_alive', function () { + // Socket.IO handles pong automatically + }); + + $socket->on('update_location', function ($data) + use ($driverId, $INTERNAL_KEY, &$driverState, &$fwdThrottle, &$eventBuffer) + { + global $connectedDrivers; + + $data = (array) $data; + + $lat = isset($data['lat']) ? (float)$data['lat'] : null; + $lng = isset($data['lng']) ? (float)$data['lng'] : null; + $heading = (float)($data['heading'] ?? 0); + $speed = (float)($data['speed'] ?? 0); + $status = (string)($data['status'] ?? 'off'); + $distance = (float)($data['distance'] ?? 0); + $passengerId = (string)($data['passenger_id'] ?? ''); + $rideId = $data['ride_id'] ?? null; + + if ($lat === null || $lng === null) return; + + $state = &$driverState[$driverId]; + $now = time(); + + // 1. Forward للراكب (ASYNC + throttle) + if (!empty($passengerId)) { + forwardLocationToPassengerSocket( + $driverId, $passengerId, + [ + 'latitude' => $lat, + 'longitude' => $lng, + 'heading' => $heading, + 'speed' => $speed, + 'ride_id' => $rideId, + 'driver_id' => $driverId, + ], + $INTERNAL_KEY, $fwdThrottle + ); + } + + // 2. حساب ماذا تغيّر لتجنب ضغط Redis + $movedMeters = ($state['lat'] == 0.0 && $state['lng'] == 0.0) + ? 999.0 + : haversineDistance($state['lat'], $state['lng'], $lat, $lng); + + $didMove = $movedMeters >= MIN_MOVE_METERS; + $speedMs = $speed / 3.6; + $speedChanged = abs($speedMs - $state['speed']) >= HMSET_SPEED_DELTA; + $headingChanged = abs($heading - $state['heading']) >= HMSET_HEADING_DELTA; + $statusChanged = ($status !== $state['status']); + + $needHmset = $speedChanged || $headingChanged || $statusChanged; + $needGeoadd = $didMove; + $needExpireRefresh = ($now - $state['expire_ts']) >= EXPIRE_REFRESH_SECONDS; + + if (!$needHmset && (!$needGeoadd && !$statusChanged) && !$needExpireRefresh) { + return; // لم يتغير شيء مهم، تجاهل تماماً (0 عمليات Redis) + } + + // 🚀 3. Buffering Event بدل الإرسال المباشر لـ Redis (Level 2 Magic) + if (!isset($eventBuffer[$driverId])) { + $eventBuffer[$driverId] = []; + } + + if ($needHmset) { + $eventBuffer[$driverId]['hmset'] = [ + 'id' => $driverId, 'heading' => $heading, 'speed' => $speed, 'status' => $status, 'updated_at' => $now + ]; + $state['speed'] = $speedMs; + $state['heading'] = $heading; + } + + if ($needExpireRefresh || $needHmset) { + $eventBuffer[$driverId]['expire'] = 900; + $state['expire_ts'] = $now; + } + + if ($statusChanged) { + $eventBuffer[$driverId]['status_change'] = [ + 'old' => $state['status'], + 'new' => $status + ]; + $state['status'] = $status; + + // Auto disconnect if blocked + if ($status === 'blocked') { + if (isset($connectedDrivers[$driverId])) { + $connectedDrivers[$driverId]['conn']->disconnect(); + unset($connectedDrivers[$driverId]); + } + } + } + + if ($needGeoadd || $statusChanged) { + $eventBuffer[$driverId]['geoadd'] = [ + 'status' => $status, + 'lng' => $lng, + 'lat' => $lat + ]; + if ($needGeoadd) { + $state['lat'] = $lat; + $state['lng'] = $lng; + } + } + }); + + $socket->on('disconnect', function () use ($driverId) { + global $connectedDrivers, $driverState, $fwdThrottle; + + unset($connectedDrivers[$driverId]); + unset($driverState[$driverId]); + unset($fwdThrottle[$driverId]); + + logMsg("❌ Driver Disconnected: #$driverId"); + }); +}); + +Worker::runAll(); \ No newline at end of file diff --git a/socket_intaleq/passenger_socket.php b/socket_intaleq/passenger_socket.php new file mode 100644 index 0000000..fb80b2a --- /dev/null +++ b/socket_intaleq/passenger_socket.php @@ -0,0 +1,141 @@ +on('workerStart', function () use ($io, $INTERNAL_KEY, $INTERNAL_PORT) { + + $innerHttp = new Worker("http://0.0.0.0:$INTERNAL_PORT"); + + $innerHttp->onMessage = function ($connection, $request) use ($io, $INTERNAL_KEY) { + + $headers = $request->header(); + $clientIp = $connection->getRemoteIp(); + + if (($headers['x-internal-key'] ?? '') !== $INTERNAL_KEY) { + socket_log("[HTTP_ERROR] Unauthorized internal request from IP: $clientIp"); + $connection->send('Unauthorized'); + return; + } + + $post = $request->post(); + $action = trim($post['action'] ?? ''); + + if ($action === 'update_ride_status') { + + $passengerId = $post['passenger_id'] ?? null; + $rawPayload = $post['payload'] ?? null; + + if (!$passengerId || !$rawPayload) { + socket_log("[HTTP_ERROR] Missing passenger_id or payload for action: update_ride_status", $post); + $connection->send('Error: Missing passenger_id or payload'); + return; + } + + $payload = is_string($rawPayload) + ? (json_decode($rawPayload, true) ?? $rawPayload) + : $rawPayload; + + socket_log("[HTTP_SUCCESS] Emitting 'ride_status_change' to Passenger #$passengerId", $payload); + $io->to('passenger_' . $passengerId)->emit('ride_status_change', $payload); + + $connection->send('OK'); + + } elseif ($action === 'update_driver_location') { + + $passengerId = $post['passenger_id'] ?? null; + $rawPayload = $post['payload'] ?? null; + + if (!$passengerId || !$rawPayload) { + socket_log("[HTTP_ERROR] Missing passenger_id or payload for action: update_driver_location", $post); + $connection->send('Error: Missing passenger_id or payload'); + return; + } + + $payload = is_string($rawPayload) + ? (json_decode($rawPayload, true) ?? $rawPayload) + : $rawPayload; + + socket_log("[HTTP_SUCCESS] Emitting 'driver_location_update' to Passenger #$passengerId", $payload); + $io->to('passenger_' . $passengerId)->emit('driver_location_update', $payload); + + $connection->send('OK'); + + } else { + socket_log("[HTTP_WARNING] Unknown action received: $action", $post); + $connection->send('Unknown action: ' . $action); + } + }; + + $innerHttp->listen(); + socket_log("[INFO] Internal HTTP started on port $INTERNAL_PORT"); +}); + +$io->on('connection', function ($socket) { + + $query = $socket->handshake['query'] ?? []; + $passengerId = $query['id'] ?? null; + $clientIp = $socket->conn->remoteAddress ?? 'Unknown'; + + if (!$passengerId) { + socket_log("[SOCKET_REJECTED] Connection rejected (No passenger ID) from IP: $clientIp"); + $socket->disconnect(); + return; + } + + $socket->join('passenger_' . $passengerId); + socket_log("[SOCKET_CONNECTED] Passenger Connected: #$passengerId (IP: $clientIp)"); + + $socket->on('heartbeat', function ($data) { + // يمكن تفعيل السطر التالي للتأكد من النبضات إذا أردت دقة شديدة، لكنه قد يملأ ملف الـ log + // socket_log("[SOCKET_HEARTBEAT] Received from Passenger #$passengerId"); + }); + + $socket->on('disconnect', function () use ($passengerId, $clientIp) { + socket_log("[SOCKET_DISCONNECTED] Passenger Disconnected: #$passengerId (IP: $clientIp)"); + }); +}); + +Worker::runAll(); \ No newline at end of file