158 lines
5.9 KiB
Dart
158 lines
5.9 KiB
Dart
// ================================================================
|
|
// 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<String, dynamic> 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<ETAInfo> 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<String, dynamic> 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<List<String>> obtenerColonias() async {
|
|
final url = Uri.parse('$_baseUrl/api/colonias');
|
|
|
|
final response = await http.get(url).timeout(_timeout);
|
|
|
|
if (response.statusCode == 200) {
|
|
final Map<String, dynamic> jsonData = json.decode(response.body);
|
|
// El backend devuelve: { "colonias": ["Zona Centro", "Col. Hidalgo", ...] }
|
|
return List<String>.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<void> 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}');
|
|
}
|
|
}
|
|
}
|