// ================================================================ // lib/services/api_service.dart // Servicio de comunicación con el backend FastAPI // ================================================================ // // PATRÓN: Service class singleton. // Una sola instancia maneja todas las llamadas HTTP de la app. // // ATAJO DE HACKATHON: // Usamos la librería 'http' sin abstracciones complejas. // En producción: usar Dio con interceptors para auth headers, // retry automático y mejor manejo de errores. // // CÓMO CONECTAR: // En cada Screen que necesite datos: // final service = ApiService(); // final eta = await service.obtenerETA(usuarioId); // ================================================================ import 'dart:convert'; import 'package:http/http.dart' as http; // ---------------------------------------------------------------- // MODELO: ETAInfo // Representa la respuesta del endpoint GET /api/eta/{usuario_id} // Mapea exactamente los campos que devuelve el backend (main.py) // ---------------------------------------------------------------- class ETAInfo { final int usuarioId; final String colonia; final String etaTexto; final int etaMinutos; final String mensajePreventivo; ETAInfo({ required this.usuarioId, required this.colonia, required this.etaTexto, required this.etaMinutos, required this.mensajePreventivo, }); // Factory constructor: convierte el JSON del backend a objeto Dart. // Los keys del JSON deben coincidir con los fields del ETAResponse // de Pydantic en main.py (usa snake_case, igual que FastAPI). factory ETAInfo.fromJson(Map json) { return ETAInfo( usuarioId: json['usuario_id'], colonia: json['colonia'], etaTexto: json['eta_texto'], etaMinutos: json['eta_minutos'], mensajePreventivo: json['mensaje_preventivo'], ); } } // ---------------------------------------------------------------- // CLASE PRINCIPAL: ApiService // ---------------------------------------------------------------- class ApiService { // ============================================================ // BASE URL DEL BACKEND // // DESARROLLO LOCAL: // - Android Emulator: usa 10.0.2.2 (mapea al localhost del PC) // - iOS Simulator: usa 127.0.0.1 // - Dispositivo físico: IP real de tu máquina en la red local // (ej: http://192.168.1.100:8000) // // ATAJO: Cambia solo esta constante para apuntar a staging/prod. // ============================================================ static const String _baseUrl = 'http://10.0.2.2:8000'; // static const String _baseUrl = 'http://127.0.0.1:8000'; // iOS Simulator // static const String _baseUrl = 'http://192.168.1.XX:8000'; // Dispositivo físico // Timeout razonable para demo. Si el backend es lento, sube a 15s. static const Duration _timeout = Duration(seconds: 10); // ---------------------------------------------------------------- // MÉTODO: obtenerETA // // Llama a GET /api/eta/{usuario_id} y retorna un ETAInfo. // Lanza una Exception si hay error de red o el servidor responde // con error (4xx, 5xx). La UI debe manejar el try/catch. // ---------------------------------------------------------------- Future obtenerETA(int usuarioId) async { final url = Uri.parse('$_baseUrl/api/eta/$usuarioId'); try { // Llamada HTTP GET con timeout para no bloquear la UI para siempre final response = await http.get(url).timeout(_timeout); if (response.statusCode == 200) { // Decodifica el body JSON (viene como String, lo convertimos a Map) final Map jsonData = json.decode(response.body); return ETAInfo.fromJson(jsonData); } else if (response.statusCode == 404) { // El usuario no existe en la DB — pide que corran el seed throw Exception('Usuario no encontrado. ¿Corriste /api/seed en el backend?'); } else { // Error genérico del servidor throw Exception('Error del servidor: ${response.statusCode} - ${response.body}'); } } on Exception { // Re-lanzamos para que la UI lo maneje con un mensaje amigable rethrow; } } // ---------------------------------------------------------------- // MÉTODO: obtenerColonias // // Llama a GET /api/colonias para poblar el Dropdown del LoginScreen. // Retorna una lista de strings con los nombres de las colonias. // ---------------------------------------------------------------- Future> obtenerColonias() async { final url = Uri.parse('$_baseUrl/api/colonias'); final response = await http.get(url).timeout(_timeout); if (response.statusCode == 200) { final Map jsonData = json.decode(response.body); // El backend devuelve: { "colonias": ["Zona Centro", "Col. Hidalgo", ...] } return List.from(jsonData['colonias']); } else { throw Exception('No se pudieron cargar las colonias.'); } } // ---------------------------------------------------------------- // MÉTODO: registrarFcmToken // // Envía el FCM token del dispositivo al backend para que pueda // enviar notificaciones push personalizadas. // // CUÁNDO LLAMARLO: // - En HomeScreen al iniciar, después de obtener el token de // FirebaseMessaging.instance.getToken() // ---------------------------------------------------------------- Future registrarFcmToken(int usuarioId, String fcmToken) async { final url = Uri.parse('$_baseUrl/api/usuarios/$usuarioId/fcm-token'); final response = await http.put( url, headers: {'Content-Type': 'application/json'}, body: json.encode({'fcm_token': fcmToken}), ).timeout(_timeout); if (response.statusCode != 200) { // No es crítico que falle en el hackathon, solo logueamos throw Exception('Error registrando FCM token: ${response.statusCode}'); } } }