# Flutter WebSocket Integration — Loan ID Generation

## Overview

Connect your Flutter app to the Socket.IO `/admin` namespace to:
- **Peek** at the next loan ID when opening the "New Loan" screen (`loan_id:next`)
- **Consume** a loan ID when saving a new loan (`loan_id:request`)
- **Listen** for broadcasts when another terminal takes a number (`loan_id:next`)

## 1. Add Dependency

```yaml
# pubspec.yaml
dependencies:
  socket_io_client: ^3.0.0
```

## 2. WebSocket Service

Create a singleton service that manages the connection:

```dart
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:socket_io_client/socket_io_client.dart';

class LoanIdSocketService {
  static final LoanIdSocketService _instance = LoanIdSocketService._();
  factory LoanIdSocketService() => _instance;
  LoanIdSocketService._();

  IO.Socket? _socket;
  bool get isConnected => _socket?.connected ?? false;

  /// Connect to the /admin namespace
  void connect({
    required String token,
    String serverUrl = 'https://dayloan.agniplay.com',
    String path = '/ws',
  }) {
    if (_socket != null && _socket!.connected) return;

    _socket = IO.io(
      '$serverUrl/admin',
      OptionBuilder()
          .setPath(path)
          .setAuth({'token': token})
          .setTransports(['websocket'])
          .disableAutoConnect()
          .build(),
    );

    _socket!.connect();

    _socket!.onConnect((_) {
      print('[WS] Connected to /admin');
    });

    _socket!.onDisconnect((_) {
      print('[WS] Disconnected');
    });

    _socket!.onError((error) {
      print('[WS] Error: $error');
    });
  }

  void disconnect() {
    _socket?.disconnect();
    _socket?.dispose();
    _socket = null;
  }

  /// Emit loan_id:next — get the next loan ID without consuming it
  void requestNextLoanId({required String requestId}) {
    _socket?.emit('loan_id:next', {
      'requestId': requestId,
    });
  }

  /// Emit loan_id:request — consume the next loan ID
  void requestConsumeLoanId({required String requestId}) {
    _socket?.emit('loan_id:request', {
      'requestId': requestId,
    });
  }

  /// Listen for ack responses (includes loan_id data)
  void onAck(void Function(Map<String, dynamic> data) callback) {
    _socket?.on('ack', (data) {
      callback(data as Map<String, dynamic>);
    });
  }

  /// Listen for loan_id:next broadcasts from other terminals
  void onLoanIdNext(void Function(Map<String, dynamic> data) callback) {
    _socket?.on('loan_id:next', (data) {
      callback(data as Map<String, dynamic>);
    });
  }

  /// Listen for errors
  void onError(void Function(Map<String, dynamic> data) callback) {
    _socket?.on('error', (data) {
      callback(data as Map<String, dynamic>);
    });
  }

  void removeAllListeners() {
    _socket?.off('ack');
    _socket?.off('loan_id:next');
    _socket?.off('error');
  }
}
```

## 3. ViewModel / Usage Example

```dart
class NewLoanViewModel {
  final LoanIdSocketService _ws = LoanIdSocketService();

  String? _nextLoanId;
  String? _currentLoanId;
  bool _isLoading = false;
  int _requestCounter = 0;

  /// Call when the "New Loan" screen opens
  Future<void> onScreenOpen(String token) async {
    _ws.removeAllListeners();

    // Connect if not already connected
    _ws.connect(token: token);

    // Listen for acks
    _ws.onAck((data) {
      final type = data['type'];
      final requestId = data['requestId'];
      final responseData = data['data'] as Map<String, dynamic>?;

      if (requestId == 'loan_next') {
        _nextLoanId = responseData?['loan_id'];
        print('Next loan ID: $_nextLoanId');
        // Update UI
      } else if (requestId == 'loan_consume') {
        _currentLoanId = responseData?['loan_id'];
        _nextLoanId = responseData?['next_loan_id'];
        print('Assigned loan ID: $_currentLoanId, next: $_nextLoanId');
        // Now save the loan via your PHP API with _currentLoanId
      }
    });

    // Listen for broadcasts (other terminals)
    _ws.onLoanIdNext((data) {
      final loanId = data['data']?['loan_id'];
      if (loanId != null) {
        _nextLoanId = loanId;
        print('Broadcast — next loan ID updated to: $loanId');
        // Update UI
      }
    });

    _ws.onError((data) {
      print('WS Error: $data');
    });

    // Wait briefly for connection, then peek
    await Future.delayed(Duration(seconds: 1));
    _ws.requestNextLoanId(requestId: 'loan_next');
  }

  /// Call when user taps "Save" on a new loan
  void onSaveLoan() {
    _ws.requestConsumeLoanId(requestId: 'loan_consume');
  }

  /// After receiving consumed loan_id, call your PHP API
  Future<void> saveLoanToApi(String loanId) async {
    // POST to https://dayloanphp.agniplay.com/loan.php with loan_id = loanId
    // ...
  }

  void dispose() {
    // Don't disconnect on every screen close — keep alive for broadcasts
    _ws.removeAllListeners();
  }
}
```

## 4. Event Flow Diagram

```
Screen Opens              Socket.IO Server                Other Terminals
    |                         |                                |
    |--- "loan_id:next" ---->|                                |
    |<-- "ack" --------------|                                |
    |    loan_id: "DL2026024"                                 |
    |                         |                                |
User taps Save               |                                |
    |--- "loan_id:request" ->|                                |
    |<-- "ack" --------------|---- "loan_id:next" ----------->|
    |    loan_id: "DL2026024" |    loan_id: "DL2026025"        |
    |    next: "DL2026025"    |                                |
    |                         |                                |
    |--- POST /loan.php ----->|                                |
    |    (REST API)           |                                |
```

## 5. Important Notes

| | Detail |
|---|---|
| **Auth** | Pass JWT token via `socket.auth.token`. Same token used for PHP REST API. |
| **Namespace** | Always connect to `/admin` — the default namespace is rejected. |
| **Transport** | Force `websocket` transport only (`setTransports(['websocket'])`). |
| **Connection Lifecycle** | Connect once when app starts (or first screen opens). Keep alive for broadcasts. Remove listeners per-screen. |
| **Broadcasts** | `loan_id:next` is emitted to ALL admins/staff in the same company every time a loan_id is consumed. Update your local "next" display immediately. |
| **No Pre-reservation** | `loan_id:next` is read-only — nobody else can see your peek. Only `loan_id:request` consumes a number. |
| **Race Safety** | The server's single-threaded JS event loop ensures no two `loan_id:request` events get the same number. |
| **Token Expiry** | Tokens now last 500 days. Old short-lived tokens will stop working when they expire — user must re-login. |

## 6. Testing (via curl)

See `INVOICE_WEBSOCKET_CURL.md` for command-line testing with `wscat` or Node.js scripts.
