# Mobile — Active Ride Screen (Socket + API)

Guide for the **active ride** screen (`رحلة نشطة`): which Socket.IO events and API calls supply each UI field.

**Related:**

- [SOCKET-SCOOTER-LOCATION.md](./SOCKET-SCOOTER-LOCATION.md) — full `scooter-location` payload
- [MQTT.md](./MQTT.md) — broker and MQTT topics

**Out of scope here:** trip duration timer (`مدة الرحلة`) — not documented per product decision.

---

## Screen field map

| UI (Arabic) | UI example | Source | Field / logic |
|-------------|------------|--------|----------------|
| كود السكوتر | `SCT-0042` | API (on load) | `ride.scooter.code` |
| اللوكيشن على الخريطة | map marker | **Socket** `scooter-location` | `lat`, `lng` when `db_saved: true` |
| العنوان | طريق الملك فهد، الرياض | **Client** | reverse geocode `lat`/`lng` (Google Places) |
| المسافة | `0.01 كم` | **Socket** `scooter-location` | `trip_distance_m / 1000` (ECU) or `session_distance_m / 1000` (GPS) |
| السرعة | `0.0 كم/س` | **Socket** `scooter-location` | `speed_kmh` (ECU) or `derived.gps_speed_kmh` |
| بطارية السكوتر | `87%` | **Socket** `scooter-heartbeat` / `scooter-vehicle-info` | `battery_percent` / `sysSoc` |
| مسافة متبقية تقريبية | `~35 كم` | **Socket** + formula | `remaining_distance_m / 1000` or `battery_percent * 0.4` |
| الرصيد المتبقى | `85.48 ريال` | **API** poll | `live.remaining_balance` (see balance section) |

```mermaid
flowchart TB
    subgraph socket [Socket.IO real-time]
        enter[enter-scooter]
        loc[scooter-location]
        hb[scooter-heartbeat]
        info[scooter-vehicle-info]
    end
    subgraph api [Laravel API poll]
        active["GET /api/client/rides/active"]
    end
    subgraph ui [Active Ride Screen]
        map[Map marker]
        dist[Distance km]
        speed[Speed km/h]
        batt[Battery %]
        range[Range ~km]
        wallet[Remaining balance]
    end
    enter --> loc
    enter --> hb
    enter --> info
    loc --> map
    loc --> dist
    loc --> speed
    hb --> batt
    info --> batt
    info --> range
    active --> wallet
```

---

## 1. Socket.IO connection

**URL:** `https://{NODE_HOST}:{NODE_PORT}` — production: `https://beebbeeb.sa.com:5055`

**Required handshake query** ([`socket/validation.js`](./socket/validation.js)):

| Query param | Required | Example |
|-------------|----------|---------|
| `userId` | yes | `51` |
| `userType` | yes | `admin`, `provider`, `delegate`, or `user` |
| `name` | yes | `Ahmed` |
| `lang` | yes | `ar` or `en` |
| `deviceType` | yes | `ios`, `android` |
| `deviceId` | yes | unique device id |

```javascript
import { io } from 'socket.io-client';

const socket = io('https://beebbeeb.sa.com:5055', {
  transports: ['websocket', 'polling'],
  query: {
    userId: String(clientId),
    userType: 'user',      // use type that exists in Node userTypeRepo
    name: clientName,
    lang: 'ar',
    deviceType: 'ios',
    deviceId: deviceUniqueId,
  },
});
```

On validation failure the server emits `error-message` and may disconnect.

---

## 2. Socket events to use

### Client → server

| Event | When | Payload |
|-------|------|---------|
| `enter-scooter` | Open active ride screen | `{ imei: '862499071894209' }` or `{ scooter_id: 38 }` |
| `exit-scooter` | Leave screen / end ride | same shape as `enter-scooter` |

```javascript
socket.on('connect', () => {
  socket.emit('enter-scooter', { imei: scooterImei });
});
```

### Server → client

| Event | Frequency | Feeds UI |
|-------|-----------|----------|
| `scooter-location` | ~every 30s | map, distance, speed |
| `scooter-heartbeat` | ~every 4 min | battery |
| `scooter-vehicle-info` | after heartbeat query | battery, ECU speed, trip distance, range |

All scooter events include: `imei`, `scooter_id`, `scooter_code`, `type`, `received_at`.

---

## 3. Field details per event

### `scooter-location` — map, distance, speed

Listen only when GPS is valid:

```javascript
socket.on('scooter-location', (p) => {
  if (p.imei !== expectedImei) return;
  if (!p.db_saved) return;

  // Map
  updateMarker(p.lat, p.lng);
  reverseGeocode(p.lat, p.lng); // address text — client-side

  // Speed (km/h)
  const speed = p.speed_kmh ?? p.derived?.gps_speed_kmh ?? 0;
  setSpeed(speed);

  // Distance (km) — prefer ECU trip, else GPS session
  const meters = p.trip_distance_m ?? p.session_distance_m ?? 0;
  setDistanceKm(Number((meters / 1000).toFixed(2)));
});
```

| Field | Type | Use on screen |
|-------|------|----------------|
| `lat` | number | map latitude |
| `lng` | number | map longitude |
| `db_saved` | boolean | must be `true` to trust coords |
| `speed_kmh` | number \| null | speed from last ECU report |
| `derived.gps_speed_kmh` | number \| null | GPS speed fallback |
| `trip_distance_m` | number \| null | ECU single-ride distance (m) |
| `session_distance_m` | number \| null | GPS cumulative distance (m) |

Full payload: [SOCKET-SCOOTER-LOCATION.md](./SOCKET-SCOOTER-LOCATION.md)

---

### `scooter-heartbeat` — battery

```javascript
socket.on('scooter-heartbeat', (p) => {
  if (p.imei !== expectedImei) return;

  const battery = p.battery_percent ?? p.sysSoc ?? 0;
  setBatteryPercent(battery);
  setRangeKm(Math.round(battery * 0.4)); // same as Laravel km_can_run
});
```

| Field | Type | Use on screen |
|-------|------|----------------|
| `battery_percent` | number | battery % |
| `sysSoc` | number | alias |
| `lockSta` | number | lock state (optional UI) |

---

### `scooter-vehicle-info` — battery, range, ECU metrics

```javascript
socket.on('scooter-vehicle-info', (p) => {
  if (p.imei !== expectedImei) return;

  const battery = p.battery_percent ?? p.sysSoc;
  if (battery != null) setBatteryPercent(battery);

  if (p.remaining_distance_m != null) {
    setRangeKm(Number((p.remaining_distance_m / 1000).toFixed(0)));
  }

  if (p.speed_kmh != null) setSpeed(p.speed_kmh);
  if (p.trip_distance_m != null) {
    setDistanceKm(Number((p.trip_distance_m / 1000).toFixed(2)));
  }
});
```

| Field | Type | Use on screen |
|-------|------|----------------|
| `battery_percent` | number | battery % |
| `speed_kmh` | number \| null | ECU speed |
| `trip_distance_m` | number \| null | trip distance (m) |
| `remaining_distance_m` | number \| null | estimated range (m) |

---

## 4. API — remaining balance (الرصيد المتبقى)

Wallet balance is **not** sent over Socket. Poll Laravel while the ride is `active`.

**Endpoint:** `GET /api/client/rides/active`  
**Auth:** `Authorization: Bearer {client_token}`  
**Route:** [`routes/api/client.php`](../routes/api/client.php)

### Example response

```json
{
  "key": "success",
  "data": {
    "ride": {
      "id": 1,
      "status": "active",
      "ride_number": "RIDE-000001",
      "scooter": {
        "code": "SCT-0042",
        "battery_percent": 87,
        "km_can_run": 34.8
      },
      "metrics": {
        "distance_km": 0.01,
        "cost": 0,
        "currency": "SAR"
      }
    },
    "live": {
      "elapsed_seconds": 180,
      "duration_formatted": "03:00",
      "current_cost": 1.35,
      "remaining_balance": 85.48,
      "distance_km": 0.01
    }
  }
}
```

### Balance display

| Field | Meaning |
|-------|---------|
| `live.remaining_balance` | Current wallet balance (SAR) — not deducted until ride `end()` |
| `live.current_cost` | Running trip cost so far |

**Recommended UI:**

```text
display_balance = remaining_balance - current_cost
```

Example: `85.48 - 1.35 = 84.13` ريال متبقي بعد تكلفة الرحلة الحالية.

Poll every **10–15 seconds** during active ride (or after each `scooter-location` if you want tighter sync).

```javascript
async function pollActiveRide() {
  const res = await fetch(`${API_BASE}/api/client/rides/active`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  const { data } = await res.json();
  if (!data?.ride) return;

  const { remaining_balance, current_cost } = data.live;
  setWalletDisplay(remaining_balance - current_cost);
}
```

---

## 5. Full integration sketch

```javascript
const state = {
  lat: null,
  lng: null,
  speedKmh: 0,
  distanceKm: 0,
  batteryPercent: 0,
  rangeKm: 0,
  walletSar: 0,
};

// --- Socket ---
socket.on('connect', () => {
  socket.emit('enter-scooter', { imei: scooterImei });
});

socket.on('scooter-location', (p) => {
  if (p.imei !== scooterImei || !p.db_saved) return;
  state.lat = p.lat;
  state.lng = p.lng;
  state.speedKmh = p.speed_kmh ?? p.derived?.gps_speed_kmh ?? 0;
  state.distanceKm = (p.trip_distance_m ?? p.session_distance_m ?? 0) / 1000;
  render();
});

socket.on('scooter-heartbeat', (p) => {
  if (p.imei !== scooterImei) return;
  state.batteryPercent = p.battery_percent ?? p.sysSoc ?? 0;
  state.rangeKm = Math.round(state.batteryPercent * 0.4);
  render();
});

socket.on('scooter-vehicle-info', (p) => {
  if (p.imei !== scooterImei) return;
  if (p.battery_percent != null) state.batteryPercent = p.battery_percent;
  if (p.remaining_distance_m) state.rangeKm = p.remaining_distance_m / 1000;
  if (p.speed_kmh != null) state.speedKmh = p.speed_kmh;
  if (p.trip_distance_m) state.distanceKm = p.trip_distance_m / 1000;
  render();
});

// --- API poll (balance) ---
setInterval(pollActiveRide, 15000);
pollActiveRide();

// --- Cleanup ---
onUnmount(() => {
  socket.emit('exit-scooter', { imei: scooterImei });
  clearInterval(pollInterval);
});
```

---

## 6. Address text (العنوان)

Socket events provide coordinates only. Convert to street address on the device:

1. Use `lat`/`lng` from `scooter-location`
2. Call reverse geocoding (e.g. Google Geocoding API — `google_places` in site settings)
3. Show result under the map (e.g. "طريق الملك فهد، الرياض")

---

## 7. Priority when sources disagree

| Field | Priority order |
|-------|----------------|
| Speed | `scooter-vehicle-info.speed_kmh` → `scooter-location.speed_kmh` → `derived.gps_speed_kmh` |
| Distance | `trip_distance_m` (ECU) → `session_distance_m` (GPS) → API `live.distance_km` |
| Battery | latest of `scooter-vehicle-info` / `scooter-heartbeat` → API `ride.scooter.battery_percent` on poll |
| Range | `remaining_distance_m / 1000` → `battery_percent * 0.4` |

---

## 8. Test tools

Verify `scooter-location` on server:

```bash
cd /home/beebbeeb/public_html/node
node scripts/test-socket-scooter-location.js
```

Expected: `PASS: received scooter-location with db_saved=true`

---

## 9. Checklist for mobile dev

- [ ] Connect Socket with all required handshake query params
- [ ] `enter-scooter` with ride scooter `imei` or `scooter_id`
- [ ] Listen `scooter-location`, `scooter-heartbeat`, `scooter-vehicle-info`
- [ ] Map uses `lat`/`lng` only when `db_saved === true`
- [ ] Poll `GET /api/client/rides/active` for wallet (`remaining_balance - current_cost`)
- [ ] Reverse geocode for address label
- [ ] `exit-scooter` on screen close
- [ ] Do **not** use `updateLocation` socket event — it is for delegate tracking, not scooters
