Files
HackOnLinces_app/aplicacion_hack/lib/services/api_service.dart
hack_23030943_f11325 5eae8782bf Siguientes funcionalidades
2026-05-22 21:24:43 -06:00

371 lines
12 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 rutaNombre;
final String rutaStatus;
final bool gpsOk;
final String etaTexto;
final int etaMinutos;
final String mensajePreventivo;
ETAInfo({
required this.usuarioId,
required this.colonia,
required this.rutaNombre,
required this.rutaStatus,
required this.gpsOk,
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'],
rutaNombre: json['ruta_nombre'] ?? '',
rutaStatus: json['ruta_status'] ?? '',
gpsOk: json['gps_ok'] ?? true,
etaTexto: json['eta_texto'],
etaMinutos: json['eta_minutos'],
mensajePreventivo: json['mensaje_preventivo'],
);
}
}
class DireccionInfo {
final String colonia;
final String direccion;
DireccionInfo({
required this.colonia,
required this.direccion,
});
factory DireccionInfo.fromJson(Map<String, dynamic> json) {
return DireccionInfo(
colonia: json['colonia'],
direccion: json['direccion'],
);
}
}
class UsuarioInfo {
final int usuarioId;
final String nombre;
final String email;
final List<DireccionInfo> direcciones;
UsuarioInfo({
required this.usuarioId,
required this.nombre,
required this.email,
required this.direcciones,
});
factory UsuarioInfo.fromJson(Map<String, dynamic> json) {
return UsuarioInfo(
usuarioId: json['usuario_id'],
nombre: json['nombre'],
email: json['email'],
direcciones: List<Map<String, dynamic>>.from(json['direcciones'])
.map(DireccionInfo.fromJson)
.toList(),
);
}
}
class RouteInfo {
final String routeId;
final String name;
final String status;
final int lastPositionId;
final String lastTimestamp;
final bool gpsOk;
RouteInfo({
required this.routeId,
required this.name,
required this.status,
required this.lastPositionId,
required this.lastTimestamp,
required this.gpsOk,
});
factory RouteInfo.fromJson(Map<String, dynamic> json) {
return RouteInfo(
routeId: json['route_id'],
name: json['name'],
status: json['status'],
lastPositionId: json['last_position_id'],
lastTimestamp: json['last_timestamp'],
gpsOk: json['gps_ok'],
);
}
}
// ----------------------------------------------------------------
// 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://192.168.192.96:8000';
// 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: loginConCorreo
//
// Llama a POST /api/usuarios/login con email y obtiene el usuario_id
// ----------------------------------------------------------------
Future<int> loginConCorreo(String email) async {
final url = Uri.parse('$_baseUrl/api/usuarios/login');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: json.encode({'email': email.trim().toLowerCase()}),
).timeout(_timeout);
if (response.statusCode == 200) {
final Map<String, dynamic> jsonData = json.decode(response.body);
return jsonData['usuario_id'];
} else {
throw Exception('Error al iniciar sesión: ${response.body}');
}
}
// ----------------------------------------------------------------
// MÉTODO: registrarUsuario
//
// Llama a POST /api/usuarios/register y crea el usuario con su primera dirección.
// ----------------------------------------------------------------
Future<int> registrarUsuario(
String nombre,
String email,
String direccion,
String colonia,
) async {
final url = Uri.parse('$_baseUrl/api/usuarios/register');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: json.encode({
'nombre': nombre.trim(),
'email': email.trim().toLowerCase(),
'colonia': colonia,
'direccion': direccion.trim(),
}),
).timeout(_timeout);
if (response.statusCode == 200) {
final Map<String, dynamic> jsonData = json.decode(response.body);
return jsonData['usuario_id'];
} else {
throw Exception('Error al registrar usuario: ${response.body}');
}
}
// ----------------------------------------------------------------
// MÉTODO: obtenerUsuario
//
// Llama a GET /api/usuarios/{usuario_id} y retorna los datos de perfil.
// ----------------------------------------------------------------
Future<UsuarioInfo> obtenerUsuario(int usuarioId) async {
final url = Uri.parse('$_baseUrl/api/usuarios/$usuarioId');
final response = await http.get(url).timeout(_timeout);
if (response.statusCode == 200) {
final Map<String, dynamic> jsonData = json.decode(response.body);
return UsuarioInfo.fromJson(jsonData);
} else {
throw Exception('Error al obtener usuario: ${response.body}');
}
}
// ----------------------------------------------------------------
// MÉTODO: obtenerRutas
//
// Llama a GET /api/rutas para listar el estado actual de cada camión.
// ----------------------------------------------------------------
Future<List<RouteInfo>> obtenerRutas() async {
final url = Uri.parse('$_baseUrl/api/rutas');
final response = await http.get(url).timeout(_timeout);
if (response.statusCode == 200) {
final Map<String, dynamic> jsonData = json.decode(response.body);
return List<Map<String, dynamic>>.from(jsonData['rutas'])
.map(RouteInfo.fromJson)
.toList();
}
throw Exception('Error al obtener rutas: ${response.body}');
}
// ----------------------------------------------------------------
// MÉTODO: avanzarRuta
//
// Llama a POST /api/rutas/{route_id}/avanzar para simular el avance del camión.
// ----------------------------------------------------------------
Future<RouteInfo> avanzarRuta(String routeId) async {
final url = Uri.parse('$_baseUrl/api/rutas/$routeId/avanzar');
final response = await http.post(url).timeout(_timeout);
if (response.statusCode == 200) {
final Map<String, dynamic> jsonData = json.decode(response.body);
return RouteInfo.fromJson(jsonData);
}
throw Exception('Error al avanzar la ruta: ${response.body}');
}
// ----------------------------------------------------------------
// MÉTODO: agregarDireccion
//
// Llama a POST /api/usuarios/{usuario_id}/direcciones para guardar
// una nueva dirección asociada al usuario.
// ----------------------------------------------------------------
Future<void> agregarDireccion(
int usuarioId,
String colonia,
String direccion,
) async {
final url = Uri.parse('$_baseUrl/api/usuarios/$usuarioId/direcciones');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: json.encode({
'colonia': colonia,
'direccion': direccion.trim(),
}),
).timeout(_timeout);
if (response.statusCode != 200) {
throw Exception('Error al guardar la dirección: ${response.body}');
}
}
// ----------------------------------------------------------------
// 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}');
}
}
}