docs: restructure documentation, add digital legacy report, and secure contracts
This commit is contained in:
@@ -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<dynamic> 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 │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
@@ -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**: `SiroMaps` (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)
|
||||
@@ -0,0 +1,110 @@
|
||||
# 🎓 خطة التوتوريال الكامل داخل الموقع — «كيف تستخدم Siro»
|
||||
|
||||
**الهدف:** صفحات شرح تفاعلية داخل siromove.com (وداخل التطبيق لاحقاً) تشرح للمستخدم الجديد
|
||||
كل عملية خطوة بخطوة، مدعومة بلقطات شاشة حقيقية من التطبيق.
|
||||
|
||||
**قاعدة التسمية للقطات:** `tutorial/{القسم}/{رقم الخطوة}_{وصف}.png`
|
||||
مثال: `tutorial/booking/03_select_car_type.png`
|
||||
|
||||
**مواصفات اللقطات:** جهاز حقيقي أو محاكي بدقة 1170×2532 (iPhone) أو 1080×2400 (Android)،
|
||||
واجهة عربية، وضع فاتح (+ نسخة داكنة اختيارية)، بيانات وهمية (لا أرقام هواتف حقيقية).
|
||||
|
||||
---
|
||||
|
||||
## الوحدة 1: التسجيل وإنشاء الحساب (5 لقطات)
|
||||
|
||||
| # | الخطوة | الشاشة في الكود | اللقطة المطلوبة |
|
||||
|---|---|---|---|
|
||||
| 1 | تحميل التطبيق وفتحه | `splash_screen_page.dart` | شاشة البداية بشعار Siro |
|
||||
| 2 | شاشات التعريف | `onbording_page.dart` | شريحة الـ Onboarding الأولى |
|
||||
| 3 | إدخال رقم الهاتف | `login_page.dart` | حقل الهاتف مع اختيار الدولة (🇯🇴🇪🇬🇸🇾) |
|
||||
| 4 | رمز التحقق OTP | `otp_page.dart` | شاشة إدخال الرمز (يصل واتساب/SMS) |
|
||||
| 5 | إكمال البيانات | `register_page.dart` | الاسم + البريد (اختياري) ثم «ابدأ» |
|
||||
|
||||
**نص الشرح المرافق:** التسجيل يحتاج رقم هاتف فقط — لا بريد إلكتروني إجباري ولا بطاقة دفع.
|
||||
أقل من دقيقة من التحميل حتى أول شاشة خريطة.
|
||||
|
||||
## الوحدة 2: طلب الرحلة (6 لقطات)
|
||||
|
||||
| # | الخطوة | الشاشة في الكود | اللقطة المطلوبة |
|
||||
|---|---|---|---|
|
||||
| 1 | الخريطة الرئيسية | `map_page_passenger.dart` | الخريطة مع السيارات القريبة تتحرك |
|
||||
| 2 | تحديد الوجهة | `LocationSearchController` | شريط البحث + النتائج |
|
||||
| 3 | تأكيد نقطة الانطلاق | — | دبوس الانطلاق قابل للسحب |
|
||||
| 4 | اختيار فئة السيارة | — | قائمة الفئات التسع مع سعر كل فئة وشارة «توفّر X% عن المنافسين» |
|
||||
| 5 | كود الخصم (اختياري) | `promos_passenger_page.dart` | حقل البرومو مطبّق على السعر |
|
||||
| 6 | تأكيد الطلب | — | زر «اطلب الآن» مع السعر النهائي |
|
||||
|
||||
**نقطة مهمة للشرح:** السعر المعروض نهائي ومحسوب من الخادم — لا مفاجآت عند الدفع
|
||||
(ما لم تتغير الوجهة أو يزد الانتظار).
|
||||
|
||||
## الوحدة 3: الانتظار والمطابقة (4 لقطات)
|
||||
|
||||
| # | الخطوة | الشاشة في الكود | اللقطة المطلوبة |
|
||||
|---|---|---|---|
|
||||
| 1 | البحث عن كابتن | `searching_captain_window.dart` | نافذة البحث مع المؤقّت |
|
||||
| 2 | اقتراح المفاوض الذكي | `_showIncreaseFeeDialog()` | حوار «ارفع السعر 5% لنتائج أسرع» (يظهر بعد 90 ثانية) |
|
||||
| 3 | قبول الكابتن | `driver_card_from_passenger.dart` | بطاقة الكابتن: الاسم، الصورة، التقييم، السيارة، اللوحة |
|
||||
| 4 | الكابتن في الطريق | `driver_time_arrive_passenger.dart` | الخريطة مع مسار الكابتن الأصفر والوقت المتوقع |
|
||||
|
||||
**نص الشرح:** متوسط زمن المطابقة أقل من 90 ثانية. إذا لم يقبل أحد، النظام يقترح
|
||||
تعديلاً بسيطاً على السعر بدل إلغاء طلبك.
|
||||
|
||||
## الوحدة 4: أثناء الرحلة والسلامة (5 لقطات)
|
||||
|
||||
| # | الخطوة | الشاشة في الكود | اللقطة المطلوبة |
|
||||
|---|---|---|---|
|
||||
| 1 | وصول الكابتن | `driverArrivePassengerDialoge()` | إشعار «الكابتن وصل» + مهلة انتظار 5 دقائق |
|
||||
| 2 | بدء الرحلة | `ride_begin_passenger.dart` | المسار الأزرق + العداد الحي |
|
||||
| 3 | مشاركة الرحلة | شاشة المراقبة الحية | رابط متابعة لطرف ثالث (للأهل) |
|
||||
| 4 | زر الطوارئ SOS | — | زر SOS + شرح «هزّ الهاتف 5 مرات» |
|
||||
| 5 | الاتصال بالكابتن | WebRTC داخلي | شاشة المكالمة داخل التطبيق (بدون كشف رقمك) |
|
||||
|
||||
## الوحدة 5: الإلغاء (3 لقطات)
|
||||
|
||||
| # | الخطوة | الشاشة في الكود | اللقطة المطلوبة |
|
||||
|---|---|---|---|
|
||||
| 1 | زر الإلغاء | `timer_for_cancell_trip_from_passenger.dart` | مكان الزر أثناء البحث/الانتظار |
|
||||
| 2 | اختيار السبب | `cancelRide/add.php` | قائمة أسباب الإلغاء |
|
||||
| 3 | تأكيد الإلغاء | — | رسالة التأكيد + سياسة الرسوم |
|
||||
|
||||
**نقاط يجب شرحها بوضوح:**
|
||||
- الإلغاء قبل قبول الكابتن: مجاني دائماً.
|
||||
- الإلغاء بعد وصول الكابتن أو تجاوز المهلة: قد تُطبَّق رسوم.
|
||||
- سياسة السائق: 3 إلغاءات متتالية = إيقاف مؤقت (شفافية للطرفين).
|
||||
|
||||
## الوحدة 6: الدفع والتقييم (6 لقطات)
|
||||
|
||||
| # | الخطوة | الشاشة في الكود | اللقطة المطلوبة |
|
||||
|---|---|---|---|
|
||||
| 1 | نهاية الرحلة | `processRideFinished()` | شاشة الفاتورة النهائية |
|
||||
| 2 | اختيار طريقة الدفع | `payment_method.page.dart` | القائمة: كاش/محفظة/بطاقة/MTN/Syriatel/Cliq |
|
||||
| 3 | الدفع كاش | `cash_confirm_bottom_page.dart` | حوار تأكيد الكاش |
|
||||
| 4 | الدفع بالمحفظة | `passenger_wallet.dart` | الخصم من الرصيد + شاشة الشحن بمكافأة («ادفع 20 خذ 21») |
|
||||
| 5 | تقييم الكابتن | `rate_captain.dart` | النجوم + الإكرامية (Tip) |
|
||||
| 6 | سجل الرحلات | `order_history.dart` | قائمة الرحلات السابقة والفواتير |
|
||||
|
||||
## الوحدة 7: مواصلاتي — للطلاب (5 لقطات)
|
||||
|
||||
| # | الخطوة | الشاشة | اللقطة المطلوبة |
|
||||
|---|---|---|---|
|
||||
| 1 | تبويب مواصلاتي | `siro_rider` تبويب Transit | اختيار الجامعة |
|
||||
| 2 | تفعيل العضوية | — | إدخال الرقم الجامعي (تفعيل فوري إذا كان بالكشف) |
|
||||
| 3 | استعراض الخطوط | — | قائمة الخطوط المعتمدة وجداولها |
|
||||
| 4 | التتبع الحي | خريطة الخط | الباص يتحرك على المسار + المحطات |
|
||||
| 5 | «فاتك الباص؟» | زر التحويل | الزر البارز لطلب سيارة بديلة |
|
||||
|
||||
---
|
||||
|
||||
## خطة التنفيذ على الموقع
|
||||
|
||||
1. **الصفحة:** `siromove.com/tutorial` — صفحة واحدة بتبويبات (وحدة لكل تبويب)، RTL،
|
||||
بنفس هوية تقرير الإرث الرقمي البصرية.
|
||||
2. **العرض:** لكل خطوة سطر شرح + لقطة شاشة داخل إطار هاتف (CSS phone frame) —
|
||||
حتى تجهز اللقطات، توضع صور Placeholder رمادية بنفس اسم الملف المطلوب.
|
||||
3. **التصوير:** جلسة واحدة (~ساعتان) بحساب تجريبي على بيئة staging تنتج الـ 34 لقطة
|
||||
المذكورة أعلاه دفعة واحدة، بالترتيب أعلاه.
|
||||
4. **لاحقاً:** تحويل كل وحدة إلى فيديو قصير (30-60 ثانية) بنفس اللقطات، ودمج
|
||||
التوتوريال داخل التطبيق كـ Coach Marks عند أول استخدام.
|
||||
|
||||
**إجمالي اللقطات المطلوبة: 34 لقطة** (5+6+4+5+3+6+5)
|
||||
@@ -0,0 +1,358 @@
|
||||
# تقرير فني: دورة حياة الرحلة وتتبع نظام الموقع في تطبيق السائق (Siro Driver)
|
||||
|
||||
## 1. منطق الاتصال بالشبكة وفحوصات السلامة (Connection & Safety Logic)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
يتم التحكم في حالة اتصال السائق بالشبكة وتفعيل استقبال الطلبات في ملف التحكم
|
||||
</div>
|
||||
|
||||
[HomeCaptainController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/home_captain_controller.dart)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
من خلال الدالة
|
||||
</div>
|
||||
|
||||
[onButtonSelected](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/home_captain_controller.dart#L278)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
والتي تقوم بإجراء الفحوصات المتتالية التالية قبل السماح للسائق بالدخول في حالة النشاط (`isActive = true`):
|
||||
</div>
|
||||
|
||||
### أ. فحص عقوبة إلغاء الرحلات (Cancellation Penalty Check)
|
||||
<div dir="rtl" align="right">
|
||||
تتحقق الدالة
|
||||
</div>
|
||||
|
||||
[checkAndShowBlockDialog](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/home_captain_controller.dart#L347)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
من وجود تاريخ حظر نشط مخزن في الذاكرة المحلية تحت المفتاح `blockUntilDate`. إذا كان الوقت الحالي قبل وقت انتهاء الحظر، يتم إجبار السائق على وضع عدم الاتصال وعرض نافذة حوار مانعة تعرض عداداً تنازلياً لوقت فك الحظر. تفرض هذه العقوبة تلقائياً لمدة 4 ساعات عند إلغاء السائق لـ 3 رحلات في اليوم الواحد.
|
||||
</div>
|
||||
|
||||
### ب. فحص حد الإرهاق اليومي (Fatigue Monitoring Check)
|
||||
<div dir="rtl" align="right">
|
||||
يقوم النظام بمراقبة ساعات القيادة المتواصلة للسائق لمنع الحوادث عبر الدالة
|
||||
</div>
|
||||
|
||||
[_checkFatigueBeforeOnline](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/home_captain_controller.dart#L222)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
إذا بلغ مجموع ثواني النشاط المخزنة في `fatigue_total_seconds` ما يعادل 12 ساعة عمل، يتم استدعاء الدالة
|
||||
</div>
|
||||
|
||||
[_forceOfflineDueToFatigue](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/home_captain_controller.dart#L242)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
والتي تقطع الاتصال فوراً وتمنع السائق من العمل. لا يتم تصفير هذا العداد إلا إذا بقي السائق في وضع عدم الاتصال بشكل مستمر لمدة لا تقل عن 6 ساعات متواصلة (يتم تتبعها عبر قراءة تاريخ `fatigue_last_offline`).
|
||||
</div>
|
||||
|
||||
### ج. فحص الحد الأدنى لنقاط المحفظة (Wallet Points Threshold)
|
||||
<div dir="rtl" align="right">
|
||||
يتم استدعاء خاصية
|
||||
</div>
|
||||
|
||||
[minPointsThreshold](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/home_captain_controller.dart#L267)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
والتي تعتمد على الدولة الحالية المخزنة في إعدادات التطبيق. إذا كان الرصيد الحالي للنقاط أدنى من الحد المسموح به (والذي يساوي `-200` نقطة في سوريا ومصر، و `-3` نقاط في الأردن)، يتم منع السائق من استقبال الطلبات وإيقاف تحديثات الموقع فوراً.
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 2. نظام التتبع وتحسين استهلاك البطارية والمعالج (GPS & Performance Optimization)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
لمعالجة مشاكل استنزاف البطارية وارتفاع حرارة الأجهزة الضعيفة، تم تطبيق استراتيجيات تحسين الأداء التالية في
|
||||
</div>
|
||||
|
||||
[MapDriverController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
و
|
||||
</div>
|
||||
|
||||
[HomeCaptainController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/home_captain_controller.dart)
|
||||
|
||||
### أ. تجميع قنوات البث ومستمع الحركة الموحد (Centralized GPS Stream & 500ms Timer Polling)
|
||||
<div dir="rtl" align="right">
|
||||
بدلاً من فتح قنوات بث (Streams) متعددة ومستقلة للـ GPS، تم تركيز البث في كلاس مركزي موحد هو
|
||||
</div>
|
||||
|
||||
[LocationController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/functions/location_controller.dart)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
حيث يقوم مستمع التوجيه والملاحة في الدالة
|
||||
</div>
|
||||
|
||||
[startListeningStepNavigation](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart#L306)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
بعمل استعلام دوري خفيف (Polling) كل 500 ملي ثانية لقراءة إحداثيات `locationController.myLocation` الجاهزة مسبقاً، مما يمنع استدعاء العتاد المادي لجهاز الاستقبال عدة مرات متزامنة.
|
||||
</div>
|
||||
|
||||
### ب. فلترة ضجيج الإحداثيات (Jitter Noise Filtering)
|
||||
<div dir="rtl" align="right">
|
||||
لمنع التحديثات المتكررة وغير المفيدة التي تنتج عن عدم دقة حساس الـ GPS عند الوقوف، يقوم الكود بمقارنة إحداثيات الموقع الجديد مع آخر موقع تم تسجيله عبر الدالة
|
||||
</div>
|
||||
|
||||
`Geolocator.distanceBetween`
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
فإذا كانت المسافة المقطوعة أقل من 3 أمتار، يتم تجاهل التحديث بالكامل وعدم تعديل المسار أو إرسال بيانات للسيرفر.
|
||||
</div>
|
||||
|
||||
### ج. التحكم الذكي في حركة الكاميرا والـ UI Throttling
|
||||
<div dir="rtl" align="right">
|
||||
يتم التحكم في حركة الكاميرا لمتابعة حركة السائق على الخريطة الرئيسية عبر مؤقت دوري يعمل كل 8 ثوانٍ:
|
||||
</div>
|
||||
|
||||
```dart
|
||||
_cameraFollowTimer = Timer.periodic(const Duration(seconds: 8), (timer) { ... });
|
||||
```
|
||||
<div dir="rtl" align="right">
|
||||
وتشترط الدالة تحرك السائق لمسافة تزيد عن 15 متراً عن آخر موقع تحركت إليه الكاميرا لتنفيذ الحركة الدائرية والتقريب، مما يقلل بشكل كبير من استهلاك معالج الرسوميات (GPU) في عمليات إعادة رسم الخريطة (Re-rendering) أثناء الوقوف. كذلك، تم تفعيل وسيلة
|
||||
</div>
|
||||
|
||||
`_uiThrottleMs = 400`
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
لكبح تكرار استدعاء الدالة `update()` المسؤولة عن تحديث الواجهات.
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 3. استقبال وإدارة الطلبات ونافذة الواجهة العائمة (Order Requests & Overlay System)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
يتم استقبال إشعارات الرحلات الجديدة إما عبر سوكيت الويب (WebSockets) أو إشعارات Firebase (FCM). عند وصول إشعار والبرنامج في الخلفية، يتم تفعيل شاشة الواجهة العائمة المفتوحة عبر حزمة
|
||||
</div>
|
||||
|
||||
`FlutterOverlayWindow`
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
والتي تظهر للسائق تفاصيل الطلب بشكل مباشر.
|
||||
</div>
|
||||
|
||||
### أ. تهيئة البيانات ودعم الصيغ المتعددة (Smart Data Handling)
|
||||
<div dir="rtl" align="right">
|
||||
يقوم الكلاس
|
||||
</div>
|
||||
|
||||
[OrderRequestController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/order_request_controller.dart)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
بتحليل البيانات المستقبلة في الدالة
|
||||
</div>
|
||||
|
||||
[_initializeData](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/order_request_controller.dart#L126)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
حيث تدعم بشكل مرن استقبال البيانات سواء كانت على شكل قائمة مرتبة (List) قادمة من إشعارات Firebase، أو على شكل خريطة مفاتيح (Map) قادمة من سوكيت الويب.
|
||||
</div>
|
||||
|
||||
### ب. مؤقت قبول الطلب وصوت التنبيه
|
||||
<div dir="rtl" align="right">
|
||||
عند فتح شاشة الطلب، يتم تشغيل صوت تنبيه متكرر وتفعيل مؤقت تنازلي مدته 15 ثانية عبر الدالة
|
||||
</div>
|
||||
|
||||
[startTimer](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/order_request_controller.dart#L555)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
إذا انتهت الـ 15 ثانية دون استجابة السائق، يتم إيقاف الصوت وإغلاق الشاشة تلقائياً.
|
||||
</div>
|
||||
|
||||
### ج. فحص القبول المسبق للطلب (Socket ride_taken Listening)
|
||||
<div dir="rtl" align="right">
|
||||
لتفادي قبول طلب تم أخذه بالفعل من قبل كابتن آخر، يقوم الكنترولر في الدالة
|
||||
</div>
|
||||
|
||||
[_listenForRideTaken](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/order_request_controller.dart#L587)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
بالاستماع لحدث السوكيت `ride_taken`. عند استقبال الحدث ومطابقة معرف الرحلة، يتم إلغاء إشعار النظام فوراً، وإغلاق شاشة الطلب وعرض تنبيه للسائق بأن "الطلب تم قبوله من قبل سائق آخر".
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 4. نظام الملاحة التفاعلي ورسم المسارات (Interactive Navigation & Mapping)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
يعتمد تطبيق السائق على خرائط انطلق المبنية على محرك مابليبرا (MapLibre)، ويتم استدعاء ورسم مسارات الملاحة التفاعلية في تطبيق السائق بدقة وتفصيل عالية عبر الفئات والأساليب التالية:
|
||||
</div>
|
||||
|
||||
### أ. رسم المسارات المزدوجة ونوافذ المعلومات في شاشة طلب الرحلة (Dual-Route & Info Windows in Order Request)
|
||||
<div dir="rtl" align="right">
|
||||
في شاشة استقبال الطلب
|
||||
</div>
|
||||
|
||||
[OrderRequestController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/order_request_controller.dart)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
يقوم التطبيق بالاستعلام ورسم مسارين جغرافيين في نفس الوقت عبر الدالة
|
||||
</div>
|
||||
|
||||
[_calculateFullJourney](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/order_request_controller.dart#L226)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
حيث يستدعي:
|
||||
1. مسار الانطلاق (Pickup Route): من موقع السائق الحالي إلى موقع الراكب (يرسم باللون الأصفر/الذهبي).
|
||||
2. مسار الرحلة الرئيسي (Trip Route): من موقع الراكب إلى الوجهة النهائية (يرسم باللون الأسود/الأزرق).
|
||||
|
||||
ولعرض تفاصيل المسافة والوقت كصندوق معلومات عائم (Info Window) مباشرة فوق الخريطة، يتم استدعاء الدالة
|
||||
</div>
|
||||
|
||||
[_updateMarkers](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/order_request_controller.dart#L468)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
والتي تقوم بطلب مولد الماركرز
|
||||
</div>
|
||||
|
||||
[MarkerGenerator.createCustomMarkerBitmap](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/views/home/Captin/orderCaptin/marker_generator.dart)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
لتوليد صور ماركر مخصصة ديناميكياً تحتوي على الوقت والمسافة كصندوق معلومات يعلو الخريطة فوق نقطة الركوب (أقرب مسافة وزمن وصول للسائق) ونقطة الوصول (المسافة والزمن المقدرين للرحلة الكلية للراكب).
|
||||
</div>
|
||||
|
||||
### ب. تسلسل رسم وحذف المسارات أثناء دورة حياة الرحلة (Lifecycle Route Transitions)
|
||||
<div dir="rtl" align="right">
|
||||
تخضع مسارات الخريطة لعملية تحديث وحذف دورية أثناء الرحلة في الكنترولر
|
||||
</div>
|
||||
|
||||
[MapDriverController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
وفق التسلسل التالي:
|
||||
1. **عند قبول الطلب**: يتم مسح خط الوجهة، ورسم خط الملاحة الجاري باتجاه الراكب (باللون الأصفر) عبر استدعاء
|
||||
</div>
|
||||
|
||||
[getRoute](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart#L1885)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
2. **عند وصول السائق لموقع الراكب**: بمجرد ضغط السائق على زر "وصلت" وتأكيده، يتم استدعاء الدالة
|
||||
</div>
|
||||
|
||||
[clearPolyline](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart#L422)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
والتي تقوم بمسح وحذف المسار الجاري الأول (الخط الأصفر الموصل للراكب) بالكامل من الخريطة لتنظيف الشاشة.
|
||||
3. **عند بدء الرحلة الفعلي**: يتم الاستعلام ورسم المسار الأزرق/الأسود الجديد المؤدي للوجهة النهائية مباشرة باتجاه وجهة الراكب عبر إعادة استدعاء دالة المسار `getRoute` للوجهة.
|
||||
</div>
|
||||
|
||||
### ج. تفادي انهيار الخرائط عند المسافات الصفرية (Same-Device Crash Protection)
|
||||
<div dir="rtl" align="right">
|
||||
عند تشغيل اختبارات الرحلة وكون موقع السائق والراكب متطابقين تماماً (مسافة أقل من 10 أمتار)، ينهار محرك الملاحة المكتوب بلغة C++ بسبب إحداثيات الصندوق المحيط (Bounds) ذات العرض الصفرى مطلقةً استثناء `std::domain_error`. لمنع ذلك، يقوم الكود بفحص المسافة، وفي حال كانت متطابقة يقوم بإظهار نافذة تنبيه
|
||||
</div>
|
||||
|
||||
[_showSameDeviceWarning](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart#L2171)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
ثم الانتقال قسرياً لتطبيق زوم تقريبي آمن بدلاً من احتواء الحدود الصفرية.
|
||||
</div>
|
||||
|
||||
### د. تحديث المسار المقطوع بنظام النافذة المنزلقة (Bidirectional Sliding Window)
|
||||
<div dir="rtl" align="right">
|
||||
لمنع إعادة رسم كامل خط المسار (Polyline) عند كل إرسال للموقع، يتم استخدام نافذة بحث منزلقة ثنائية الاتجاه تتكون من 60 نقطة (30 للخلف و 30 للأمام) في الدالة
|
||||
</div>
|
||||
|
||||
[_updateTraveledPolylineSmart](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart#L2743)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
تحدد الدالة أقرب نقطة لموقع السائق الحالي على المسار المخزن، وتقوم بقطع الـ Polyline إلى جزأين: مسار مقطوع بلون رمادي ومسار متبقي بلون أزرق/أصفر، وتحديث الخريطة فقط عند تجاوز إزاحة تزيد عن 50 متراً.
|
||||
</div>
|
||||
|
||||
### هـ. رسم خطوط المشي المنقطة (Passenger Walk Dotted Line)
|
||||
<div dir="rtl" align="right">
|
||||
عندما يكون موقع الراكب الفعلي بعيداً عن أقرب طريق إسفلتي متاح للسيارات، يتم استدعاء الدالة
|
||||
</div>
|
||||
|
||||
[_updatePassengerWalkLine](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart#L2857)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
والتي تقوم برسم خط منقط بلون أزرق رمادي مميز يمتد من نهاية طريق السيارات الإسفلتي إلى موقع الراكب الحقيقي لتوجيه السائق سيراً على الأقدام إذا لزم الأمر.
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 5. محرك تسعير الرحلة الديناميكي (Dynamic Pricing Engine)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
أثناء سير الرحلة، يعمل مؤقت دوري كل ثانية لحساب السعر الفعلي بشكل لحظي وعرضه في واجهة السائق عبر الدالة
|
||||
</div>
|
||||
|
||||
[rideIsBeginPassengerTimer](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart#L1570)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
ويعتمد الحساب على القواعد البرمجية التالية:
|
||||
</div>
|
||||
|
||||
### أ. تجميد الأسعار للرحلات الثابتة (Fixed Price Protection)
|
||||
<div dir="rtl" align="right">
|
||||
إذا كان نوع الرحلة من الفئات ذات السعر الثابت مثل `Speed` أو `Fixed Price` أو `Awfar Car`، يتم إيقاف الحساب الديناميكي وتثبيت السعر المعروض على القيمة المتفق عليها مسبقاً في عرض السعر الأولي للراكب.
|
||||
</div>
|
||||
|
||||
### ب. تسعير الفئات المتغيرة (Comfort / Electric / Van / Delivery)
|
||||
<div dir="rtl" align="right">
|
||||
للفئات المتغيرة، يتم حساب السعر التراكمي عبر دمج المسافة الفعلية المقطوعة مع وقت الرحلة الفعلي طبقاً للمعادلة:
|
||||
</div>
|
||||
|
||||
```
|
||||
Price = (Distance_KM * Per_KM_Rate) + (Duration_Minutes * Per_Minute_Rate)
|
||||
```
|
||||
<div dir="rtl" align="right">
|
||||
حيث يتم تطبيق تسعيرة الدقيقة بناءً على ساعة الرحلة الحالية لمراعاة أوقات الذروة (طبيعي، متأخر، أو حركة مرورية كثيفة)، بالإضافة إلى ضرب الناتج في عمولة السيرفر (كازان) المحددة بنسبة مئوية.
|
||||
</div>
|
||||
|
||||
### ج. تخفيضات المسافات الطويلة (Long Distance Reduction Rules)
|
||||
<div dir="rtl" align="right">
|
||||
إذا تجاوزت المسافة المقطوعة 35 كم أو 40 كم، يطبق محرك التسعير قواعد خاصة:
|
||||
- يتم تجميد تسعيرة الدقيقة وتثبيتها على قيمة ثابتة للرحلات الطويلة تعادل 600 ل.س/دقيقة.
|
||||
- يتم تطبيق نسبة خصم ديناميكية (تصل إلى 35%) على تسعيرة الكيلومتر لتخفيض الأعباء على الراكب مع الحفاظ على ربحية السائق.
|
||||
- يضمن الكود دائماً عدم نزول السعر الفعلي النهائي عن السعر المتفق عليه مسبقاً (Quoted Price).
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 6. حماية الرحلة من الإنهاء المبكر الخاطئ (Anti-Fraud & Exit Validation)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
عندما يسحب السائق شريط إنهاء الرحلة، يمر الطلب بفحص أمني دقيق للتأكد من عدم وجود تلاعب أو إنهاء وهمي للرحلة.
|
||||
</div>
|
||||
|
||||
### أ. شرط الإزاحة الأمنية (Displacement Validation Check)
|
||||
<div dir="rtl" align="right">
|
||||
يتم استدعاء الدالة
|
||||
</div>
|
||||
|
||||
[_validateTripDistance](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart#L1458)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
والتي تقوم بحساب المسافة المستقيمة الفاصلة بين موقع انطلاق الرحلة (موقع الراكب الأصلي) والموقع الجغرافي الحالي للسائق. يشترط النظام أن تتجاوز هذه المسافة قيمة **خُمس المسافة الإجمالية المخططة للرحلة** (`plannedDistance / 5`).
|
||||
</div>
|
||||
|
||||
### ب. التنبيه الصوتي المانع والتراجع (TTS Rejection Alert)
|
||||
<div dir="rtl" align="right">
|
||||
إذا حاول السائق إنهاء الرحلة قبل قطع حد الخُمس المسموح، يتم رفض الطلب فوراً وإغلاق أي نوافذ تحميل، وتفعيل قارئ النصوص الصوتي لإصدار تنبيه صوتي باللغة الإنجليزية عبر الهاتف:
|
||||
</div>
|
||||
|
||||
> "You haven't moved sufficiently!"
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
مع إظهار رسالة خطأ تحذيرية في الواجهة وإعادة زر إنهاء الرحلة لوضعه النشط للسماح بإكمال الرحلة.
|
||||
</div>
|
||||
|
||||
### ج. إتمام المعاملة المالية الموازية (Parallel Transaction Completion)
|
||||
<div dir="rtl" align="right">
|
||||
عند اجتياز فحص المسافة بنجاح، يتم تفعيل دالة الإنهاء الفعلي
|
||||
</div>
|
||||
|
||||
[finishRideFromDriver1](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart#L1363)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
والتي تقوم بإرسال طلبات التحديث المالي وإغلاق الرحلة في آن واحد إلى سيرفر العمليات وسيرفر المحفظة المالي بالتوازي عبر استدعاء
|
||||
</div>
|
||||
|
||||
`Future.wait([...])`
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
مما يقلل وقت المعاملة على أجهزة السائقين ويمنع تعليق التطبيق. بعد نجاح المعاملات، يتم توجيه السائق تلقائياً إلى صفحة تقييم الراكب وإرسال تقرير السلوك الفني.
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,252 @@
|
||||
|
||||
# تقرير محاكاة دورة حياة الرحلة — Siro Rider 🚖
|
||||
|
||||
> **الهدف:** التحقق من صحة رسم الخطوط (Polylines) والـ Markers في كل مرحلة من مراحل الرحلة
|
||||
|
||||
---
|
||||
|
||||
## 🎬 تسجيل المحاكاة
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📸 لقطات كل مرحلة
|
||||
|
||||
````carousel
|
||||

|
||||
<!-- slide -->
|
||||

|
||||
<!-- slide -->
|
||||

|
||||
<!-- slide -->
|
||||

|
||||
<!-- slide -->
|
||||

|
||||
<!-- slide -->
|
||||

|
||||
<!-- slide -->
|
||||

|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## 🔍 تحليل كل مرحلة
|
||||
|
||||
### 1️⃣ `noRide` — لا رحلة
|
||||
| العنصر | السلوك المتوقع | النتيجة |
|
||||
|--------|---------------|---------|
|
||||
| Polylines | لا شيء | ✅ |
|
||||
| Markers | سيارات قريبة (nearbyCar) + موقع الراكب | ✅ |
|
||||
| Master Timer | كل 6 ثوانٍ → `getCarsLocationByPassenger()` | ✅ |
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ `searching` — البحث عن سائق
|
||||
| العنصر | السلوك المتوقع | النتيجة |
|
||||
|--------|---------------|---------|
|
||||
| Polylines | خط الرحلة الأصلي (أزرق فاتح، للمعاينة فقط) | ✅ |
|
||||
| Markers | نقطة A (بداية) + نقطة B (وجهة) | ✅ |
|
||||
| Master Timer | كل 8 ثوانٍ → `getRideStatus()` | ✅ |
|
||||
| Socket | `initConnectionWithSocket()` | ✅ |
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ `driverApplied` — السائق قبل الرحلة
|
||||
|
||||
> [!IMPORTANT]
|
||||
> هذه المرحلة تحتوي على **ثلاثة خطوط** يجب رسمها صحيحاً
|
||||
|
||||
| العنصر | السلوك المتوقع | النتيجة |
|
||||
|--------|---------------|---------|
|
||||
| `driver_route_solid` | خط أصفر صلب من موقع السائق إلى نقطة الالتقاط | ✅ |
|
||||
| `passenger_walk_line` | خط منقط رمادي من آخر نقطة طريق إلى الراكب الدقيق | ✅ |
|
||||
| `walk_end_marker` | أيقونة مشي 🚶 عند آخر نقطة قريبة من الطريق | ✅ |
|
||||
| Car Marker | سيارة عند موقع السائق بالاتجاه الصحيح | ✅ |
|
||||
| `calculateDriverToPassengerRoute()` | رسم المسار عبر API انطلق | ✅ |
|
||||
|
||||
**الكود المقابل:**
|
||||
```dart
|
||||
// في processRideAcceptance():
|
||||
await calculateDriverToPassengerRoute(driverPos, passengerLocation);
|
||||
// في calculateDriverToPassengerRoute():
|
||||
polyLines = {...polyLines, Polyline(
|
||||
polylineId: PolylineId('driver_route_solid'),
|
||||
points: decodedPoints,
|
||||
color: Colors.amber, // مسار القدوم باللون الأصفر
|
||||
width: 5,
|
||||
)};
|
||||
// في _updatePassengerWalkLine():
|
||||
final walkDashes = _buildDashedLine(lastRoadPt, passengerLocation, ...);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4️⃣ السائق يتحرك — Real-time Updates
|
||||
| العنصر | السلوك المتوقع | النتيجة |
|
||||
|--------|---------------|---------|
|
||||
| `updateRemainingRoute()` | قص نقاط المسار المكتملة | ✅ |
|
||||
| Car Marker | يتحرك تدريجياً على المسار | ✅ |
|
||||
| `passenger_walk_line` | يتحدث مع تحرك السائق | ✅ |
|
||||
| ETA Display | يتقلص مع كل تحديث | ✅ |
|
||||
| `checkAndRecalculateIfDeviated()` | إعادة حساب عند الانحراف >30م | ✅ |
|
||||
|
||||
**آلية التحديث:**
|
||||
```
|
||||
Socket → handleDriverLocationUpdate() → updateDriverMarker() + updateRemainingRoute()
|
||||
↓ (إذا Socket فاشل)
|
||||
Watchdog Timer → getDriverCarsLocationToPassengerAfterApplied() (polling fallback)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5️⃣ `driverArrived` — السائق وصل
|
||||
|
||||
> [!WARNING]
|
||||
> **أهم نقطة للتحقق:** يجب مسح الخطوط القديمة ورسم مسار الرحلة الجديد
|
||||
|
||||
| العنصر | السلوك المتوقع | النتيجة |
|
||||
|--------|---------------|---------|
|
||||
| حذف `driver_route_solid` | ✅ يُحذف | ✅ |
|
||||
| حذف `passenger_walk_line` | ✅ يُحذف | ✅ |
|
||||
| رسم `main_route` | خط أزرق من نقطة الالتقاط للوجهة | ✅ |
|
||||
| Car Marker | عند نقطة الالتقاط | ✅ |
|
||||
| Firebase Notification | "السائق وصل!" | ✅ |
|
||||
| Timer 5 دقائق | `startTimerDriverWaitPassenger5Minute()` | ✅ |
|
||||
|
||||
**الكود المقابل:**
|
||||
```dart
|
||||
// في processDriverArrival():
|
||||
await calculateDriverToPassengerRoute(
|
||||
driverCarsLocationToPassengerAfterApplied.last,
|
||||
myDestination,
|
||||
isBeginPhase: true, // ← مهم جداً
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6️⃣ `inProgress` — الرحلة بدأت
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **`isBeginPhase: true`** يجعل الخط أزرق بدلاً من أصفر
|
||||
|
||||
| العنصر | السلوك المتوقع | النتيجة |
|
||||
|--------|---------------|---------|
|
||||
| مسح الخطوط القديمة | `driver_route*`, `main_route`, `route_direct` | ✅ |
|
||||
| رسم `main_route` | خط أزرق `#2196F3` من السائق/الراكب للوجهة | ✅ |
|
||||
| Car Marker | عند نقطة الانطلاق (أزرق) | ✅ |
|
||||
| `rideIsBeginPassengerTimer()` | عداد الرحلة يعمل | ✅ |
|
||||
| `runWhenRideIsBegin()` | polling كل 4 ثوانٍ | ✅ |
|
||||
|
||||
**الكود المقابل:**
|
||||
```dart
|
||||
// في processRideBegin():
|
||||
polyLines = polyLines.where((p) =>
|
||||
p.polylineId.value != 'main_route' &&
|
||||
p.polylineId.value != 'route_direct' &&
|
||||
!p.polylineId.value.startsWith('driver_route')
|
||||
).toSet();
|
||||
|
||||
await calculateDriverToPassengerRoute(driverPos, myDestination,
|
||||
isBeginPhase: true); // ← يرسم خط أزرق للوجهة
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7️⃣ السيارة تسير — En Route
|
||||
| العنصر | السلوك المتوقع | النتيجة |
|
||||
|--------|---------------|---------|
|
||||
| `main_route` يتقلص | يُقص من الأمام مع تحرك السيارة | ✅ |
|
||||
| Car Marker | يتحرك على المسار الأزرق | ✅ |
|
||||
| ETA يتحدث | مسافة ووقت يتقلصان | ✅ |
|
||||
| Progress Bar | يمتلئ تدريجياً | ✅ |
|
||||
|
||||
---
|
||||
|
||||
### 8️⃣ `finished` — انتهت الرحلة
|
||||
| العنصر | السلوك المتوقع | النتيجة |
|
||||
|--------|---------------|---------|
|
||||
| `mapEngine.clearPolyline()` | مسح جميع الخطوط | ✅ |
|
||||
| `markers = {}` | مسح جميع الـ Markers | ✅ |
|
||||
| `disposeRideSocket()` | إغلاق WebSocket | ✅ |
|
||||
| `stopAllTimers()` | إيقاف كل التايمرات | ✅ |
|
||||
| Rating Screen | فتح `RatingDriverBottomSheet` | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ملاحظات مهمة من تحليل الكود
|
||||
|
||||
### 1. الخط المنقط `_updatePassengerWalkLine()`
|
||||
|
||||
```dart
|
||||
// يعمل فقط في حالتي Apply و Arrived
|
||||
bool shouldShowWalkPath =
|
||||
(statusRide == 'Apply' || statusRide == 'Arrived') &&
|
||||
_currentDriverRoutePoints.isNotEmpty &&
|
||||
passengerLocation.latitude != 0;
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> يُرسم الخط المنقط من **آخر نقطة على الطريق** (`_currentDriverRoutePoints.last`) وليس من موقع السائق. هذا صحيح تماماً لأنه يمثل المسافة المشي من الطريق للراكب.
|
||||
|
||||
### 2. انحراف السائق `checkAndRecalculateIfDeviated()`
|
||||
|
||||
```dart
|
||||
final bool distanceDeviation = minDistance > _deviationThresholdMeters; // 30م
|
||||
if (distanceDeviation || _routeHeadingMismatchCount >= 2) {
|
||||
await calculateDriverToPassengerRoute(...); // إعادة الرسم
|
||||
}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> الإعادة التلقائية تعمل عند انحراف أكثر من **30 متراً** أو عند اختلاف الاتجاه مرتين متتاليتين.
|
||||
|
||||
### 3. آلية Socket + Polling الهجينة
|
||||
|
||||
```
|
||||
Socket متصل + يُرسل موقع < 20 ثانية → نعتمد على Socket فقط
|
||||
Socket صامت 15-30 ثانية → نستدعي API مرة واحدة
|
||||
Socket صامت > 30 ثانية → نبدأ polling كل 6 ثوانٍ
|
||||
Socket يعود → نوقف polling
|
||||
```
|
||||
|
||||
### 4. مشكلة محتملة في `updateRemainingRoute()`
|
||||
|
||||
```dart
|
||||
// في حالة Begin، الكود يحذف أي driver_route ولا يرسم شيئاً جديداً
|
||||
if (statusRide == 'Begin' || currentRideState.value == RideState.inProgress) {
|
||||
polyLines = polyLines
|
||||
.where((p) => !p.polylineId.value.startsWith('driver_route'))
|
||||
.toSet();
|
||||
// ← لا يرسم main_route هنا!
|
||||
}
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> **ملاحظة:** `updateRemainingRoute()` في حالة `inProgress` يحذف `driver_route*` لكنه **لا يُحدِّث** `main_route`. المسار الأزرق يُرسم مرة واحدة في `processRideBegin()` ويُقص فقط عبر هذه الدالة. تأكد من أن `main_route` لا يُحذف خطأً في هذه الدالة.
|
||||
|
||||
---
|
||||
|
||||
## ✅ خلاصة نتائج المحاكاة
|
||||
|
||||
| المرحلة | رسم الخطوط | حركة الـ Marker | التزامن | التحقق |
|
||||
|---------|-----------|----------------|---------|--------|
|
||||
| noRide | — | سيارات قريبة | Master Timer | ✅ |
|
||||
| searching | Trip preview (فاتح) | A + B | Socket init | ✅ |
|
||||
| driverApplied | أصفر + منقط | سيارة + A + B | Socket / Polling | ✅ |
|
||||
| driverMoving | أصفر يتقلص + منقط يتحدث | سيارة تتحرك | Real-time | ✅ |
|
||||
| driverArrived | **أزرق جديد** (مسح القديم) | سيارة عند A | Firebase | ✅ |
|
||||
| inProgress | أزرق كامل | سيارة أزرق | Socket / Polling | ✅ |
|
||||
| enRoute | أزرق يتقلص | سيارة تتحرك | Real-time | ✅ |
|
||||
| finished | **مسح الكل** | لا شيء | — | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ الملفات المرجعية
|
||||
|
||||
- [ride_lifecycle_controller.dart](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart) — المنطق الرئيسي
|
||||
- [map_socket_controller.dart](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_rider/lib/controller/home/map/map_socket_controller.dart) — إدارة WebSocket
|
||||
- [map_screen_binding.dart](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_rider/lib/controller/home/map/map_screen_binding.dart) — تسجيل الـ Controllers
|
||||
- [siro_ride_simulation.html](file:///Users/hamzaaleghwairyeen/.gemini/antigravity-ide/brain/3690cabc-80e2-4e43-af35-66c30922ddee/siro_ride_simulation.html) — ملف المحاكاة التفاعلية
|
||||
|
||||
@@ -0,0 +1,751 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Siro Admin – محاكاة لوحة التحكم والعمليات</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #090a0f;
|
||||
--surface: #11131c;
|
||||
--surface-elevated: #1a1d2b;
|
||||
--border: #23273c;
|
||||
--primary: #4776e6;
|
||||
--primary-glow: rgba(71, 118, 230, 0.3);
|
||||
--accent: #8e2de2;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
--info: #06b6d4;
|
||||
--text-primary: #f3f4f6;
|
||||
--text-secondary: #9ca3af;
|
||||
--divider: #1f2937;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Tajawal', sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header styling with Glassmorphism */
|
||||
header {
|
||||
background: rgba(17, 19, 28, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 16px 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 900;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 0 15px var(--primary-glow);
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(to left, #ffffff, var(--text-secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.header-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border: 1px solid var(--success);
|
||||
color: var(--success);
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--success);
|
||||
border-radius: 50%;
|
||||
animation: pulse-animation 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-animation {
|
||||
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
|
||||
70% { transform: scale(1); box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
|
||||
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
||||
}
|
||||
|
||||
/* Main layout setup */
|
||||
.dashboard-container {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
flex: 1;
|
||||
height: calc(100vh - 73px);
|
||||
}
|
||||
|
||||
/* Sidebar controls */
|
||||
.sidebar {
|
||||
background-color: var(--surface);
|
||||
border-left: 1px solid var(--border);
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.menu-section-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sidebar-btn {
|
||||
width: 100%;
|
||||
background: var(--surface-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
transition: all 0.2s ease;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.sidebar-btn:hover {
|
||||
border-color: var(--primary);
|
||||
background: rgba(71, 118, 230, 0.05);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.sidebar-btn.active {
|
||||
border-color: var(--primary);
|
||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 15px var(--primary-glow);
|
||||
}
|
||||
|
||||
/* Workspace Panel */
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Top stats row */
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
background-color: rgba(9, 10, 15, 0.5);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background-color: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(to left, var(--primary), var(--accent));
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.stat-card:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.stat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-change.up { color: var(--success); }
|
||||
.stat-change.down { color: var(--danger); }
|
||||
|
||||
/* Action view area */
|
||||
.action-view {
|
||||
padding: 0 24px 24px 24px;
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Card Panels */
|
||||
.panel-card {
|
||||
background-color: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Simulation Canvas Map */
|
||||
.map-panel {
|
||||
position: relative;
|
||||
height: 320px;
|
||||
background-color: #0b0d19;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.map-controls {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.map-btn {
|
||||
background: rgba(17, 19, 28, 0.9);
|
||||
border: 1px solid var(--border);
|
||||
color: #fff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.map-btn:hover {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
/* Logs view */
|
||||
.logs-container {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
background-color: var(--surface-elevated);
|
||||
border-right: 3px solid var(--border);
|
||||
}
|
||||
|
||||
.log-item.success { border-color: var(--success); }
|
||||
.log-item.warning { border-color: var(--warning); }
|
||||
.log-item.danger { border-color: var(--danger); }
|
||||
.log-item.info { border-color: var(--info); }
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input, select {
|
||||
background-color: var(--surface-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus, select:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* List queues (Captains documents) */
|
||||
.doc-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--surface-elevated);
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.doc-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.doc-name { font-weight: 600; font-size: 14px; }
|
||||
.doc-details { font-size: 12px; color: var(--text-secondary); }
|
||||
|
||||
.doc-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.action-btn.approve { background-color: rgba(16, 185, 129, 0.15); color: var(--success); }
|
||||
.action-btn.reject { background-color: rgba(239, 68, 68, 0.15); color: var(--danger); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Header -->
|
||||
<header>
|
||||
<div class="brand">
|
||||
<div class="logo-container">S</div>
|
||||
<h1>Siro Admin — محاكاة المشرف والعمليات</h1>
|
||||
</div>
|
||||
<div class="header-status">
|
||||
<div class="status-badge">
|
||||
<span class="pulse"></span>
|
||||
اتصال WebSocket نشط
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Container -->
|
||||
<div class="dashboard-container">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar">
|
||||
<div>
|
||||
<div class="menu-section-title">إدارة لوحة التحكم</div>
|
||||
<button class="sidebar-btn active" onclick="switchTab('dashboard', this)">
|
||||
<span>📊</span> لوحة التحكم الرئيسية
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="menu-section-title">التشغيل المالي والأسعار</div>
|
||||
<button class="sidebar-btn" onclick="switchTab('kazan', this)">
|
||||
<span>💰</span> عمولة Kazan والأسعار
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="menu-section-title">التوثيق والجودة</div>
|
||||
<button class="sidebar-btn" onclick="switchTab('docs', this)">
|
||||
<span>📑</span> وثائق الكباتن الجديدة
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="menu-section-title">الأمن والخصوصية</div>
|
||||
<button class="sidebar-btn" onclick="switchTab('fraud', this)">
|
||||
<span>🛡️</span> رادار بصمة الجهاز (الاحتيال)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workspace -->
|
||||
<div class="workspace">
|
||||
|
||||
<!-- Stats Row -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span>إجمالي الركاب</span>
|
||||
<span class="stat-change up">▲ 12%</span>
|
||||
</div>
|
||||
<div class="stat-value" id="countPassengers">18,240</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span>إجمالي الكباتن</span>
|
||||
<span class="stat-change up">▲ 8%</span>
|
||||
</div>
|
||||
<div class="stat-value" id="countDrivers">4,912</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span>رحلات الشهر الحالي</span>
|
||||
<span class="stat-change up">▲ 24%</span>
|
||||
</div>
|
||||
<div class="stat-value" id="countRides">32,490</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span>محفظة النظام (عمولات)</span>
|
||||
<span class="stat-change down">▼ 2%</span>
|
||||
</div>
|
||||
<div class="stat-value" id="walletBalance">145,200 SP</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content Area -->
|
||||
<div class="action-view">
|
||||
|
||||
<!-- Tab 1: Dashboard -->
|
||||
<div id="tab-dashboard" class="panel-card" style="grid-column: 1 / 3;">
|
||||
<div class="panel-title">📡 مراقبة الرحلات المباشرة والعمليات</div>
|
||||
|
||||
<div class="map-panel">
|
||||
<canvas id="liveMapCanvas"></canvas>
|
||||
<div class="map-controls">
|
||||
<button class="map-btn" onclick="triggerMockRide()">محاكاة رحلة جديدة</button>
|
||||
<button class="map-btn" onclick="clearSimulation()">مسح الخريطة</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-title">📝 سجل الأحداث والعمليات الفورية</div>
|
||||
<div class="logs-container" id="logsContainer">
|
||||
<div class="log-item info">[النظام]: تم تشغيل محاكاة Siro Admin بنجاح.</div>
|
||||
<div class="log-item success">[العمليات]: تم الاتصال بخادم الـ Websocket (rides.intaleq.xyz).</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Kazan pricing -->
|
||||
<div id="tab-kazan" class="panel-card" style="display:none;">
|
||||
<div class="panel-title">💰 تعديل عمولة Kazan ومعدلات التعرفة</div>
|
||||
<div class="form-group">
|
||||
<label>الدولة والمنطقة</label>
|
||||
<select id="countrySelect">
|
||||
<option value="Syria">سوريا (دمشق)</option>
|
||||
<option value="Jordan">الأردن (عمان)</option>
|
||||
<option value="Egypt">مصر (القاهرة)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>نسبة عمولة Kazan (%)</label>
|
||||
<input type="number" id="commissionPct" value="15" min="5" max="30">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>تعرفة الكيلومتر الأساسية (عملة محلية)</label>
|
||||
<input type="number" id="baseKmPrice" value="1200">
|
||||
</div>
|
||||
<button class="submit-btn" onclick="updateKazanCommission()">حفظ وتحديث نظام التسعير</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: Captin Documents (Azure OCR Simulation) -->
|
||||
<div id="tab-docs" class="panel-card" style="display:none;">
|
||||
<div class="panel-title">📑 وثائق الكباتن بانتظار التدقيق والتحقق</div>
|
||||
|
||||
<div style="display:flex; flex-direction:column; gap:12px;" id="docsQueue">
|
||||
<div class="doc-item" id="doc-c1">
|
||||
<div class="doc-info">
|
||||
<span class="doc-name">الكابتن: محمد أحمد الحموي</span>
|
||||
<span class="doc-details">رقم السيارة: دمشق - 482920 • نوع المستند: رخصة القيادة</span>
|
||||
<span class="doc-details" style="color:var(--success);">[تحليل الذكاء الاصطناعي Azure OCR]: الاسم والتواريخ متطابقة بنسبة 98%</span>
|
||||
</div>
|
||||
<div class="doc-actions">
|
||||
<button class="action-btn approve" onclick="verifyDocument('c1', true)">قبول</button>
|
||||
<button class="action-btn reject" onclick="verifyDocument('c1', false)">رفض</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="doc-item" id="doc-c2">
|
||||
<div class="doc-info">
|
||||
<span class="doc-name">الكابتن: رامي طارق المصري</span>
|
||||
<span class="doc-details">رقم السيارة: ريف دمشق - 729221 • نوع المستند: تأمين المركبة</span>
|
||||
<span class="doc-details" style="color:var(--warning);">[تحليل الذكاء الاصطناعي Azure OCR]: المستند ينتهي خلال 3 أيام</span>
|
||||
</div>
|
||||
<div class="doc-actions">
|
||||
<button class="action-btn approve" onclick="verifyDocument('c2', true)">قبول</button>
|
||||
<button class="action-btn reject" onclick="verifyDocument('c2', false)">رفض</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 4: Fraud radar device fingerprints -->
|
||||
<div id="tab-fraud" class="panel-card" style="display:none;">
|
||||
<div class="panel-title">🛡️ رادار كشف الاحتيال وتكرار بصمات الأجهزة</div>
|
||||
<div class="log-item danger" style="padding:14px;">
|
||||
<strong>تنبيه أمني هام:</strong> تم اكتشاف بصمة جهاز مكررة مرتبطة بـ 3 كباتن مختلفين!
|
||||
<br>Device FP: <code>SHA256:d89ef239fbc87a1d...</code>
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:10px;">
|
||||
<div class="doc-item">
|
||||
<div class="doc-info">
|
||||
<span class="doc-name">كابتن 1: علي سليم (نشط)</span>
|
||||
<span class="doc-details">رقم الهاتف: 963992019283</span>
|
||||
</div>
|
||||
<button class="action-btn reject" style="background-color:rgba(239,68,68,0.25)" onclick="blockCaptain('علي سليم')">حظر فوري</button>
|
||||
</div>
|
||||
<div class="doc-item">
|
||||
<div class="doc-info">
|
||||
<span class="doc-name">كابتن 2: سامر وحيد (نشط)</span>
|
||||
<span class="doc-details">رقم الهاتف: 963942091922</span>
|
||||
</div>
|
||||
<button class="action-btn reject" style="background-color:rgba(239,68,68,0.25)" onclick="blockCaptain('سامر وحيد')">حظر فوري</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Tab switching logic
|
||||
function switchTab(tabId, btn) {
|
||||
document.getElementById('tab-dashboard').style.display = 'none';
|
||||
document.getElementById('tab-kazan').style.display = 'none';
|
||||
document.getElementById('tab-docs').style.display = 'none';
|
||||
document.getElementById('tab-fraud').style.display = 'none';
|
||||
|
||||
document.getElementById('tab-' + tabId).style.display = 'flex';
|
||||
|
||||
const buttons = document.querySelectorAll('.sidebar-btn');
|
||||
buttons.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
if (tabId === 'dashboard') {
|
||||
initCanvas();
|
||||
}
|
||||
}
|
||||
|
||||
// Logging helpers
|
||||
function log(message, type = 'info') {
|
||||
const container = document.getElementById('logsContainer');
|
||||
const time = new Date().toLocaleTimeString('ar-EG');
|
||||
const div = document.createElement('div');
|
||||
div.className = `log-item ${type}`;
|
||||
div.innerText = `[${time}] ${message}`;
|
||||
container.prepend(div);
|
||||
}
|
||||
|
||||
// Kazan changes
|
||||
function updateKazanCommission() {
|
||||
const pct = document.getElementById('commissionPct').value;
|
||||
const kmPrice = document.getElementById('baseKmPrice').value;
|
||||
const country = document.getElementById('countrySelect').value;
|
||||
|
||||
// Update stats
|
||||
document.getElementById('walletBalance').innerText = `${(pct * 10000).toLocaleString()} SP`;
|
||||
log(`[نظام كازان]: تم تحديث العمولة لتصبح ${pct}% لـ ${country} مع سعر كم قدره ${kmPrice}.`, 'success');
|
||||
}
|
||||
|
||||
// Document approvals
|
||||
function verifyDocument(id, approved) {
|
||||
const element = document.getElementById(`doc-${id}`);
|
||||
if (element) {
|
||||
element.remove();
|
||||
log(`[المستندات]: تم ${approved ? 'قبول' : 'رفض'} الوثائق للكابتن بنجاح.`, approved ? 'success' : 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// Block captain
|
||||
function blockCaptain(name) {
|
||||
log(`[الأمان والخصوصية]: تم حظر الكابتن (${name}) وتجميد محفظته بسبب مطابقة البصمة الرقمية المكررة.`, 'danger');
|
||||
}
|
||||
|
||||
// Canvas map rendering
|
||||
let canvas, ctx, animId;
|
||||
let particles = [];
|
||||
|
||||
function initCanvas() {
|
||||
canvas = document.getElementById('liveMapCanvas');
|
||||
if (!canvas) return;
|
||||
ctx = canvas.getContext('2d');
|
||||
|
||||
// Resize canvas relative to its container client dimensions
|
||||
canvas.width = canvas.parentElement.clientWidth;
|
||||
canvas.height = canvas.parentElement.clientHeight || 320;
|
||||
|
||||
// Seed initial dummy drivers
|
||||
particles = [
|
||||
{ x: canvas.width * 0.3, y: canvas.height * 0.4, label: '🚗 Comfort', angle: 0.5, speed: 0.3 },
|
||||
{ x: canvas.width * 0.6, y: canvas.height * 0.7, label: '🏍️ Bike', angle: 1.2, speed: 0.6 },
|
||||
{ x: canvas.width * 0.7, y: canvas.height * 0.3, label: '🚗 Speed', angle: 2.3, speed: 0.4 }
|
||||
];
|
||||
|
||||
if (animId) cancelAnimationFrame(animId);
|
||||
draw();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
ctx.fillStyle = '#0b0d19';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Grid background
|
||||
ctx.strokeStyle = '#181b2e';
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = 0; x < canvas.width; x += 40) {
|
||||
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
|
||||
}
|
||||
for (let y = 0; y < canvas.height; y += 40) {
|
||||
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw cars
|
||||
particles.forEach(p => {
|
||||
p.x += Math.cos(p.angle) * p.speed;
|
||||
p.y += Math.sin(p.angle) * p.speed;
|
||||
|
||||
// Boundary checks
|
||||
if (p.x < 0 || p.x > canvas.width) p.angle = Math.PI - p.angle;
|
||||
if (p.y < 0 || p.y > canvas.height) p.angle = -p.angle;
|
||||
|
||||
// Draw car indicator
|
||||
ctx.fillStyle = '#4776e6';
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Label
|
||||
ctx.fillStyle = '#9ca3af';
|
||||
ctx.font = '10px Tajawal';
|
||||
ctx.fillText(p.label, p.x + 12, p.y + 4);
|
||||
});
|
||||
|
||||
animId = requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
function triggerMockRide() {
|
||||
const newCar = {
|
||||
x: canvas.width * 0.1,
|
||||
y: canvas.height * 0.1,
|
||||
label: '🚗 Speed (Active Ride)',
|
||||
angle: 0.8,
|
||||
speed: 1.2
|
||||
};
|
||||
particles.push(newCar);
|
||||
log('[رحلة جديدة]: تم بدء رحلة نشطة للراكب #3829 مع الكابتن #4928.', 'info');
|
||||
}
|
||||
|
||||
function clearSimulation() {
|
||||
particles = [];
|
||||
log('[النظام]: تم إخلاء الخريطة وتصفية كافة المركبات.', 'warning');
|
||||
}
|
||||
|
||||
// Run canvas on page load
|
||||
window.onload = initCanvas;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,352 @@
|
||||
|
||||
# تقرير نظام تسجيل السائق — سوريا 🇸🇾
|
||||
|
||||
> **ملف المحاكاة التفاعلية:** [siro_driver_registration_simulation.html](file:///Users/hamzaaleghwairyeen/.gemini/antigravity-ide/brain/3690cabc-80e2-4e43-af35-66c30922ddee/siro_driver_registration_simulation.html)
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ خريطة النظام الكاملة
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[📱 السائق يفتح التطبيق] --> B[RegisterCaptainController<br/>إدخال رقم الهاتف]
|
||||
B --> C{التحقق من الدولة}
|
||||
C -->|🇸🇾 سوريا| D[Phone Formatting<br/>09xx → 963xx]
|
||||
D --> E[POST /auth/otp/request.php<br/>country: Syria]
|
||||
E --> F{OTP Router}
|
||||
F -->|Primary| G[Intaleq WhatsApp]
|
||||
F -->|Failover 1| H[Nabeh JWT]
|
||||
F -->|Failover 2| I[SMS]
|
||||
G --> J[DB: phone_verification<br/>AES-GCM encrypted × 5min]
|
||||
J --> K[OtpVerificationController<br/>countdown 120s]
|
||||
K --> L[POST /auth/otp/verify.php]
|
||||
L --> M{✅ صحيح؟}
|
||||
M -->|نعم| N[RegistrationController<br/>3-Step Wizard]
|
||||
M -->|لا| K
|
||||
N --> O[Step 1: Driver Info<br/>الاسم + HID + DOB + License]
|
||||
O --> P[Step 2: Car Info<br/>Plate + Make + Model + VIN]
|
||||
P --> Q[Step 3: Doc Upload × 8]
|
||||
Q --> R[uploadToSyria × 8<br/>MultipartRequest + JWT]
|
||||
R --> S[uploadSyrianDocs.php<br/>private_uploads Signed URL 48h]
|
||||
S --> T[submitRegistration<br/>POST /register_driver_and_car.php]
|
||||
T --> U[🤖 Gemini Flash<br/>Vision AI Analysis]
|
||||
U --> V{AI Face Match}
|
||||
V -->|✅ high| W[DB Transaction<br/>driver + CarRegistration]
|
||||
V -->|❌ mismatch| X[jsonError - رفض]
|
||||
W --> Y[FCM → topic:service<br/>Admin Notification]
|
||||
Y --> Z[status: yet ⏳]
|
||||
Z --> AA[Admin Review]
|
||||
AA --> AB[status: active ✅]
|
||||
AB --> AC[JWT + driverToken<br/>السائق جاهز للعمل]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📱 مراحل التسجيل (8 خطوات)
|
||||
|
||||
### 1️⃣ إدخال رقم الهاتف — `RegisterCaptainController`
|
||||
|
||||
**منطق تنسيق الرقم السوري:**
|
||||
|
||||
```php
|
||||
// من: register_driver_and_car.php
|
||||
if (strpos($phone, '00963') === 0) {
|
||||
$phone = substr($phone, 2); // 00963 → 963
|
||||
} elseif (strpos($phone, '09') === 0) {
|
||||
$phone = '963' . substr($phone, 1); // 09xx → 9639xx
|
||||
} elseif (strpos($phone, '9') === 0 && strlen($phone) == 9) {
|
||||
$phone = '963' . $phone; // 9xxxxxxxx → 9639xxxxxxxx
|
||||
}
|
||||
// التأكد من وجود 9 بعد 963
|
||||
if (strpos($phone, '963') === 0 && strpos($phone, '9639') !== 0) {
|
||||
$phone = '9639' . substr($phone, 3);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ نظام OTP — `OtpVerificationController`
|
||||
|
||||
| الدولة | المزود الأساسي | Failover 1 | Failover 2 |
|
||||
|--------|---------------|-----------|-----------|
|
||||
| 🇸🇾 سوريا | Intaleq WhatsApp | Nabeh JWT | SMS |
|
||||
| 🇪🇬 مصر | Kazumi SMS | Intaleq WhatsApp | Nabeh JWT |
|
||||
| 🇯🇴 الأردن | Intaleq SMS | Nabeh JWT | — |
|
||||
|
||||
**مخطط DB:**
|
||||
|
||||
```sql
|
||||
-- جدول التحقق للسائق
|
||||
INSERT INTO phone_verification (
|
||||
phone_number, -- مشفر AES-GCM
|
||||
driverId,
|
||||
email, -- مشفر AES-GCM
|
||||
token_code, -- مشفر AES-GCM (3 أرقام)
|
||||
expiration_time, -- NOW() + 5 دقائق
|
||||
is_verified -- 0 → 1 عند النجاح
|
||||
)
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Rate Limiting: 3 محاولات كل 5 دقائق لكل IP عبر Redis
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ معلومات السائق — `RegistrationController` Page 0
|
||||
|
||||
**الحقول المطلوبة:**
|
||||
- `first_name`, `last_name`, `national_number`, `birthdate`, `expiry_date`
|
||||
- التحقق عبر `driverInfoFormKey.validate()`
|
||||
|
||||
**ملاحظة تاريخ الميلاد:**
|
||||
```php
|
||||
// من register_driver_and_car.php
|
||||
$data['birthdate'] = trim($data['birthdate']) . '-01-01';
|
||||
// "1990" → "1990-01-01"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4️⃣ معلومات المركبة — `RegistrationController` Page 1
|
||||
|
||||
**الحقول الجديدة (vehicle_category_id + fuel_type_id):**
|
||||
|
||||
| ID | نوع المركبة | ID | نوع الوقود |
|
||||
|---|------------|---|-----------|
|
||||
| 1 | سيارة (Car) | 1 | بنزين (Petrol) |
|
||||
| 2 | دراجة نارية | 2 | ديزل (Diesel) |
|
||||
| 3 | فان / باص | 3 | كهربائي |
|
||||
| — | — | 4 | هايبرد |
|
||||
|
||||
```dart
|
||||
// Flutter → submitRegistration()
|
||||
_addField(fields, 'vehicle_category_id', selectedVehicleCategoryId.toString());
|
||||
_addField(fields, 'fuel_type_id', selectedFuelTypeId.toString());
|
||||
_addField(fields, 'fuel', fuelObj['name'].toString()); // للتوافق
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5️⃣ رفع الوثائق — `uploadToSyria()` × 8
|
||||
|
||||
**الوثائق المطلوبة لسوريا:**
|
||||
|
||||
| المستند | الحقل | إلزامي سوريا؟ |
|
||||
|---------|-------|--------------|
|
||||
| هوية — وجه | `id_front` | ✅ |
|
||||
| هوية — خلف | `id_back` | ✅ |
|
||||
| رخصة القيادة — وجه | `driver_license` | ✅ |
|
||||
| رخصة القيادة — خلف | `driver_license_back` | **🇸🇾 إلزامي فقط!** |
|
||||
| صورة شخصية | `profile_picture` | ✅ |
|
||||
| لا حكم عليه | `criminal_record` | ✅ |
|
||||
| ترخيص سيارة — وجه | `car_license_front` | ✅ |
|
||||
| ترخيص سيارة — خلف | `car_license_back` | ✅ |
|
||||
|
||||
**آلية الرفع:**
|
||||
|
||||
```dart
|
||||
// Flutter: uploadToSyria()
|
||||
final req = http.MultipartRequest('POST', syrianUploadUri);
|
||||
req.headers.addAll({
|
||||
'Authorization': 'Bearer JWT',
|
||||
'X-HMAC-Auth': hmacHeader,
|
||||
});
|
||||
req.fields['driver_id'] = driverId;
|
||||
req.fields['doc_type'] = docType; // e.g., 'driver_license_back'
|
||||
// timeout: 120 ثانية — 3 محاولات تلقائية
|
||||
```
|
||||
|
||||
**Backend — الرد (Signed URL):**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"file_url": "https://api-syria.siromove.com/siro/secure_image.php?driver_id=DRV...&doc_type=id_front&ext=jpg&expires=1720000000&signature=sha256...",
|
||||
"mime_type": "image/jpeg",
|
||||
"size_bytes": 98340,
|
||||
"expires_at": "2024-06-21T..."
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> الملفات تُحفظ في `private_uploads/` (خارج الويب العام) ولا يمكن الوصول إليها إلا عبر رابط موقّع صالح لمدة **48 ساعة**.
|
||||
|
||||
---
|
||||
|
||||
### 6️⃣ الذكاء الاصطناعي Gemini Flash — Vision AI
|
||||
|
||||
**البرومبت الكامل يطلب:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success|failure",
|
||||
"reason": "إذا فشل",
|
||||
"face_match_confidence": "high|low",
|
||||
"driver": {
|
||||
"full_name": "", // الاسم الكامل بالعربي
|
||||
"national_number": "", // أرقام لاتينية فقط
|
||||
"dob": "YYYY-MM-DD",
|
||||
"governorate": "",
|
||||
"license_expiry_date": "YYYY-MM-DD",
|
||||
"license_category": "B|D1|..."
|
||||
},
|
||||
"car": {
|
||||
"car_plate": "", // e.g., "155186 درعا"
|
||||
"vin": "", // أحرف وأرقام لاتينية
|
||||
"color": "",
|
||||
"color_hex": "#FFFFFF",
|
||||
"make": "", "model": "", "year": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**قواعد الذكاء الاصطناعي الحرجة:**
|
||||
1. **FACE MATCHING**: مقارنة الصورة الشخصية ↔ الهوية ↔ رخصة القيادة
|
||||
2. **OCR ذكي**: المسح من كلا وجهي كل وثيقة
|
||||
3. تحويل الأرقام العربية (٠١٢) إلى لاتينية (012)
|
||||
4. تطبيع ألوان السيارات `أبيض → White → #FFFFFF`
|
||||
5. الفشل الكلي فقط عند: وجه غير متطابق / وثائق مزورة
|
||||
|
||||
> [!WARNING]
|
||||
> **SSRF Protection**: URLs يُسمح بها فقط من `allowedHosts` — يُمنع تحميل أي صورة من مصادر خارجية.
|
||||
|
||||
---
|
||||
|
||||
### 7️⃣ إدراج قاعدة البيانات — Transaction Atomique
|
||||
|
||||
**الحقول المشفرة في جدول `driver`:**
|
||||
|
||||
```php
|
||||
$toEncryptDriver = [
|
||||
"phone", "email", "first_name", "last_name",
|
||||
"name_arabic", "gender", "national_number",
|
||||
"address", "site", "fullNameMaritial", "birthdate"
|
||||
];
|
||||
// كلها تُشفَّر بـ AES-GCM قبل الإدراج
|
||||
```
|
||||
|
||||
**كلمة المرور (HMAC + bcrypt):**
|
||||
|
||||
```php
|
||||
$baseString = implode('|', [$data['id'], $data['phone'], $data['national_number']]);
|
||||
$rawSecret = hash_hmac('sha256', $baseString, $pepper, true);
|
||||
$pwdHashed = password_hash($rawSecret, PASSWORD_DEFAULT);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8️⃣ جداول قاعدة البيانات (`schema_primary.sql`)
|
||||
|
||||
**`CarRegistration` — الجدول الرئيسي للمركبات:**
|
||||
|
||||
```sql
|
||||
CREATE TABLE `CarRegistration` (
|
||||
`id` int AUTO_INCREMENT PRIMARY KEY,
|
||||
`driverID` varchar(100) NOT NULL, -- FK → driver.id
|
||||
`vin` varchar(100) NOT NULL, -- مشفر
|
||||
`car_plate` varchar(150), -- مشفر
|
||||
`make` varchar(255) NOT NULL,
|
||||
`model` varchar(255) NOT NULL,
|
||||
`year` int NOT NULL,
|
||||
`expiration_date` varchar(30) NOT NULL,
|
||||
`color` varchar(255) NOT NULL,
|
||||
`owner` varchar(255) NOT NULL, -- مشفر
|
||||
`color_hex` varchar(20) NOT NULL,
|
||||
`fuel` varchar(100) NOT NULL,
|
||||
`vehicle_category_id` tinyint DEFAULT 1, -- 1=Car,2=Moto,3=Van
|
||||
`fuel_type_id` tinyint DEFAULT 1, -- 1=Petrol,2=Diesel...
|
||||
`status` varchar(20) DEFAULT 'yet', -- yet|active|suspended
|
||||
`isDefault` tinyint DEFAULT 0,
|
||||
KEY `idx_driverID` (`driverID`)
|
||||
)
|
||||
```
|
||||
|
||||
**`driver` — الجدول الرئيسي للسائقين:**
|
||||
|
||||
```sql
|
||||
CREATE TABLE `driver` (
|
||||
`id` varchar(100) NOT NULL, -- DRV{timestamp}{random}
|
||||
`phone` varchar(255) NOT NULL, -- مشفر AES-GCM
|
||||
`email` varchar(255) NOT NULL,
|
||||
`password` varchar(255) NOT NULL, -- bcrypt(HMAC)
|
||||
`first_name` varchar(255) NOT NULL, -- مشفر
|
||||
`last_name` varchar(255) NOT NULL, -- مشفر
|
||||
`national_number` varchar(255), -- مشفر (UNIQUE)
|
||||
`name_arabic` varchar(255), -- مشفر — من AI
|
||||
`birthdate` varchar(255), -- مشفر
|
||||
`status` varchar(20) DEFAULT 'notDeleted', -- yet|active|notDeleted
|
||||
`expiry_date` date, -- انتهاء رخصة القيادة
|
||||
UNIQUE KEY `national_number` (`national_number`)
|
||||
)
|
||||
```
|
||||
|
||||
**`driver_documents`:**
|
||||
|
||||
```sql
|
||||
CREATE TABLE `driver_documents` (
|
||||
`id` int AUTO_INCREMENT PRIMARY KEY,
|
||||
`driverID` varchar(64) NOT NULL,
|
||||
`doc_type` varchar(64) NOT NULL, -- id_front, driver_license_back, ...
|
||||
`image_name` varchar(255) NOT NULL,
|
||||
`link` varchar(512) NOT NULL, -- Signed URL
|
||||
`upload_date` datetime NOT NULL,
|
||||
KEY `driverID` (`driverID`)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 طبقات الأمان
|
||||
|
||||
| الطبقة | التقنية | التفاصيل |
|
||||
|--------|---------|---------|
|
||||
| Authentication | JWT Bearer | يُعاد التوليد عند كل دخول |
|
||||
| Transport | HMAC-SHA256 | X-HMAC-Auth header |
|
||||
| Encryption at Rest | AES-GCM | جميع البيانات الحساسة |
|
||||
| Password | HMAC + bcrypt | pepper من env |
|
||||
| Rate Limiting | Redis | 3 OTP / 5 دقائق لكل IP |
|
||||
| File Access | Signed URLs | HMAC-SHA256 صالح 48 ساعة |
|
||||
| SSRF Protection | Allowlist hosts | منع URL injection في AI |
|
||||
| SQL Injection | PDO Prepared Statements | كل الاستعلامات |
|
||||
| File Upload | MIME detection + finfo | ليس Content-Type فقط |
|
||||
|
||||
---
|
||||
|
||||
## ✅ النتيجة: هل النظام يعمل صح؟
|
||||
|
||||
| العملية | السلوك | التقييم |
|
||||
|---------|--------|---------|
|
||||
| تنسيق الهاتف السوري | `09xx` → `9639xx` تلقائياً | ✅ |
|
||||
| OTP سوريا | Intaleq WhatsApp → Nabeh Failover | ✅ |
|
||||
| خلف رخصة القيادة | إلزامي فقط لـ `countryCode == Syria` | ✅ |
|
||||
| رفع الوثائق | Retry × 3 + Timeout 120s | ✅ |
|
||||
| AI Face Match | Gemini Flash Vision — تحقق ذكي | ✅ |
|
||||
| AI يعيد كتابة البيانات | اسم، DOB، لوحة، VIN من الوثائق | ✅ |
|
||||
| Transaction | Atomic: driver + CarRegistration + documents | ✅ |
|
||||
| تشفير الحقول | phone, name, national_number → AES-GCM | ✅ |
|
||||
| `vehicle_category_id` و `fuel_type_id` | يُرسلان من Flutter ويُخزنان في CarRegistration | ✅ |
|
||||
| إشعار خدمة العملاء | FCM → topic:service بعد التسجيل | ✅ |
|
||||
| Signed URL | صالح 48 ساعة + HMAC signed | ✅ |
|
||||
|
||||
> [!CAUTION]
|
||||
> **ملاحظة:** حقل `vin` في `submitRegistration()` يُرسل كـ `'yet'` افتراضياً ولا يُرسل من حقل `carVinController`:
|
||||
> ```dart
|
||||
> _addField(fields, 'vin', 'yet'); // ← يجب ربطه بـ carVinController.text
|
||||
> ```
|
||||
> AI سيُصحح هذا من خلال استخراج VIN من صورة ترخيص السيارة، لكن إذا لم يعمل AI فسيُخزن 'yet'.
|
||||
|
||||
---
|
||||
|
||||
## 📁 الملفات المرجعية
|
||||
|
||||
| الملف | الدور |
|
||||
|-------|-------|
|
||||
| [registration_controller.dart](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/auth/syria/registration_controller.dart) | Controller رئيسي — رفع الوثائق + تقديم التسجيل |
|
||||
| [register_captin_controller.dart](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/auth/captin/register_captin_controller.dart) | تسجيل مبدئي + OTP |
|
||||
| [opt_token_controller.dart](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_driver/lib/controller/auth/captin/opt_token_controller.dart) | إدارة OTP (120 ثانية countdown) |
|
||||
| [register_driver_and_car.php](file:///Users/hamzaaleghwairyeen/development/App/Siro/backend/auth/syria/driver/register_driver_and_car.php) | Backend — Gemini AI + DB Transaction |
|
||||
| [uploadSyrianDocs.php](file:///Users/hamzaaleghwairyeen/development/App/Siro/backend/auth/syria/uploadSyrianDocs.php) | Backend — رفع الوثائق + Signed URL |
|
||||
| [request.php](file:///Users/hamzaaleghwairyeen/development/App/Siro/backend/auth/otp/request.php) | Backend — إرسال OTP حسب الدولة |
|
||||
| [verify.php](file:///Users/hamzaaleghwairyeen/development/App/Siro/backend/auth/otp/verify.php) | Backend — التحقق من OTP |
|
||||
| [schema_primary.sql](file:///Users/hamzaaleghwairyeen/development/App/Siro/backend/schema_primary.sql) | DB Schema — driver + CarRegistration + driver_documents |
|
||||
|
||||
Reference in New Issue
Block a user