Funcionalidades implementadas:
This commit is contained in:
@@ -1,30 +1,22 @@
|
||||
// ================================================================
|
||||
// lib/services/api_service.dart
|
||||
// lib/services/api_service.dart (v2)
|
||||
// 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);
|
||||
// CAMBIOS v2:
|
||||
// - LoginResponse incluye 'nombre' del usuario
|
||||
// - ActualizarPassword requiere password_actual + password_nuevo
|
||||
// - Nuevos métodos: obtenerDashboard, historialPosiciones,
|
||||
// resumenRutas, estadisticasColonias
|
||||
// ================================================================
|
||||
|
||||
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)
|
||||
// MODELOS
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
class ETAInfo {
|
||||
final int usuarioId;
|
||||
final String colonia;
|
||||
@@ -46,9 +38,6 @@ class ETAInfo {
|
||||
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'],
|
||||
@@ -67,16 +56,10 @@ class DireccionInfo {
|
||||
final String colonia;
|
||||
final String direccion;
|
||||
|
||||
DireccionInfo({
|
||||
required this.colonia,
|
||||
required this.direccion,
|
||||
});
|
||||
DireccionInfo({required this.colonia, required this.direccion});
|
||||
|
||||
factory DireccionInfo.fromJson(Map<String, dynamic> json) {
|
||||
return DireccionInfo(
|
||||
colonia: json['colonia'],
|
||||
direccion: json['direccion'],
|
||||
);
|
||||
return DireccionInfo(colonia: json['colonia'], direccion: json['direccion']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,237 +117,367 @@ class RouteInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Posición GPS individual de la ruta de un camión.
|
||||
class PosicionGPS {
|
||||
final int positionId;
|
||||
final double lat;
|
||||
final double lng;
|
||||
final int speed;
|
||||
final String timestamp;
|
||||
final bool esActual; // true = aquí está el camión ahora
|
||||
|
||||
PosicionGPS({
|
||||
required this.positionId,
|
||||
required this.lat,
|
||||
required this.lng,
|
||||
required this.speed,
|
||||
required this.timestamp,
|
||||
required this.esActual,
|
||||
});
|
||||
|
||||
factory PosicionGPS.fromJson(Map<String, dynamic> json) {
|
||||
return PosicionGPS(
|
||||
positionId: json['position_id'],
|
||||
lat: (json['lat'] as num).toDouble(),
|
||||
lng: (json['lng'] as num).toDouble(),
|
||||
speed: json['speed'],
|
||||
timestamp: json['timestamp'],
|
||||
esActual: json['es_actual'] ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Detalle de una ruta para el dashboard.
|
||||
class RutaDetalle {
|
||||
final String routeId;
|
||||
final String name;
|
||||
final String status;
|
||||
final int truckId;
|
||||
final int posicionActual;
|
||||
final int totalPosiciones;
|
||||
final double porcentajeCompletado;
|
||||
final int etaMinutos;
|
||||
final bool gpsOk;
|
||||
final int usuariosEnRuta;
|
||||
|
||||
RutaDetalle({
|
||||
required this.routeId,
|
||||
required this.name,
|
||||
required this.status,
|
||||
required this.truckId,
|
||||
required this.posicionActual,
|
||||
required this.totalPosiciones,
|
||||
required this.porcentajeCompletado,
|
||||
required this.etaMinutos,
|
||||
required this.gpsOk,
|
||||
required this.usuariosEnRuta,
|
||||
});
|
||||
|
||||
factory RutaDetalle.fromJson(Map<String, dynamic> json) {
|
||||
return RutaDetalle(
|
||||
routeId: json['route_id'],
|
||||
name: json['name'],
|
||||
status: json['status'],
|
||||
truckId: json['truck_id'],
|
||||
posicionActual: json['posicion_actual'],
|
||||
totalPosiciones: json['total_posiciones'],
|
||||
porcentajeCompletado: (json['porcentaje_completado'] as num).toDouble(),
|
||||
etaMinutos: json['eta_minutos'],
|
||||
gpsOk: json['gps_ok'],
|
||||
usuariosEnRuta: json['usuarios_en_ruta'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Respuesta completa del dashboard de operador.
|
||||
class DashboardInfo {
|
||||
final int totalRutas;
|
||||
final int rutasEnProgreso;
|
||||
final int rutasCompletadas;
|
||||
final int totalUsuarios;
|
||||
final int usuariosConToken;
|
||||
final double coberturaNotificaciones;
|
||||
final List<RutaDetalle> rutas;
|
||||
|
||||
DashboardInfo({
|
||||
required this.totalRutas,
|
||||
required this.rutasEnProgreso,
|
||||
required this.rutasCompletadas,
|
||||
required this.totalUsuarios,
|
||||
required this.usuariosConToken,
|
||||
required this.coberturaNotificaciones,
|
||||
required this.rutas,
|
||||
});
|
||||
|
||||
factory DashboardInfo.fromJson(Map<String, dynamic> json) {
|
||||
return DashboardInfo(
|
||||
totalRutas: json['total_rutas'],
|
||||
rutasEnProgreso: json['rutas_en_progreso'],
|
||||
rutasCompletadas: json['rutas_completadas'],
|
||||
totalUsuarios: json['total_usuarios'],
|
||||
usuariosConToken: json['usuarios_con_token'],
|
||||
coberturaNotificaciones: (json['cobertura_notificaciones'] as num).toDouble(),
|
||||
rutas: List<Map<String, dynamic>>.from(json['rutas'])
|
||||
.map(RutaDetalle.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Estadísticas de una colonia.
|
||||
class ColoniaEstadistica {
|
||||
final String colonia;
|
||||
final String routeId;
|
||||
final String rutaNombre;
|
||||
final String horario;
|
||||
final int totalUsuarios;
|
||||
final int usuariosConNotificaciones;
|
||||
|
||||
ColoniaEstadistica({
|
||||
required this.colonia,
|
||||
required this.routeId,
|
||||
required this.rutaNombre,
|
||||
required this.horario,
|
||||
required this.totalUsuarios,
|
||||
required this.usuariosConNotificaciones,
|
||||
});
|
||||
|
||||
factory ColoniaEstadistica.fromJson(Map<String, dynamic> json) {
|
||||
return ColoniaEstadistica(
|
||||
colonia: json['colonia'],
|
||||
routeId: json['route_id'],
|
||||
rutaNombre: json['ruta_nombre'],
|
||||
horario: json['horario'],
|
||||
totalUsuarios: json['total_usuarios'],
|
||||
usuariosConNotificaciones: json['usuarios_con_notificaciones'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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.
|
||||
// BASE URL — Cambia solo esta línea para apuntar a otro entorno
|
||||
// Android emulator local: http://10.0.2.2:8000
|
||||
// Dispositivo físico (red local): http://192.168.X.X:8000
|
||||
// ============================================================
|
||||
static const String _baseUrl = 'http://192.168.192.96:8000';
|
||||
|
||||
|
||||
// Timeout razonable para demo. Si el backend es lento, sube a 15s.
|
||||
static const String _baseUrl = 'http://192.168.198.224:8000';
|
||||
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.
|
||||
// HELPER PRIVADO: maneja errores HTTP de forma consistente
|
||||
// ----------------------------------------------------------------
|
||||
Future<ETAInfo> obtenerETA(int usuarioId) async {
|
||||
final url = Uri.parse('$_baseUrl/api/eta/$usuarioId');
|
||||
|
||||
Never _throwError(http.Response response) {
|
||||
Map<String, dynamic> body = {};
|
||||
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;
|
||||
}
|
||||
body = json.decode(response.body);
|
||||
} catch (_) {}
|
||||
final detail = body['detail'] ?? response.body;
|
||||
throw Exception(detail);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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');
|
||||
// ================================================================
|
||||
// AUTENTICACIÓN
|
||||
// ================================================================
|
||||
|
||||
/// Login con email y contraseña. Retorna [usuarioId, nombre].
|
||||
Future<Map<String, dynamic>> loginConCorreo(String email, String password) async {
|
||||
final response = await http.post(
|
||||
url,
|
||||
Uri.parse('$_baseUrl/api/usuarios/login'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({'email': email.trim().toLowerCase()}),
|
||||
body: json.encode({'email': email.trim().toLowerCase(), 'password': password}),
|
||||
).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}');
|
||||
final data = json.decode(response.body);
|
||||
return {'usuario_id': data['usuario_id'], 'nombre': data['nombre']};
|
||||
}
|
||||
_throwError(response);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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 password,
|
||||
String direccion,
|
||||
String colonia,
|
||||
) async {
|
||||
final url = Uri.parse('$_baseUrl/api/usuarios/register');
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
Uri.parse('$_baseUrl/api/usuarios/register'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({
|
||||
'nombre': nombre.trim(),
|
||||
'email': email.trim().toLowerCase(),
|
||||
'password': password,
|
||||
'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}');
|
||||
return json.decode(response.body)['usuario_id'];
|
||||
}
|
||||
_throwError(response);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// USUARIOS
|
||||
// ================================================================
|
||||
|
||||
Future<ETAInfo> obtenerETA(int usuarioId) async {
|
||||
final response = await http
|
||||
.get(Uri.parse('$_baseUrl/api/eta/$usuarioId'))
|
||||
.timeout(_timeout);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return ETAInfo.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 404) {
|
||||
throw Exception('Usuario no encontrado. ¿Corriste /api/seed en el backend?');
|
||||
}
|
||||
_throwError(response);
|
||||
}
|
||||
|
||||
Future<List<String>> obtenerColonias() async {
|
||||
final response = await http
|
||||
.get(Uri.parse('$_baseUrl/api/colonias'))
|
||||
.timeout(_timeout);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return List<String>.from(json.decode(response.body)['colonias']);
|
||||
}
|
||||
_throwError(response);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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);
|
||||
final response = await http
|
||||
.get(Uri.parse('$_baseUrl/api/usuarios/$usuarioId'))
|
||||
.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();
|
||||
return UsuarioInfo.fromJson(json.decode(response.body));
|
||||
}
|
||||
|
||||
throw Exception('Error al obtener rutas: ${response.body}');
|
||||
_throwError(response);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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');
|
||||
|
||||
Future<void> agregarDireccion(int usuarioId, String colonia, String direccion) async {
|
||||
final response = await http.post(
|
||||
url,
|
||||
Uri.parse('$_baseUrl/api/usuarios/$usuarioId/direcciones'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({'colonia': colonia, 'direccion': direccion.trim()}),
|
||||
).timeout(_timeout);
|
||||
|
||||
if (response.statusCode != 200) _throwError(response);
|
||||
}
|
||||
|
||||
/// Actualiza contraseña. Requiere la contraseña actual como confirmación.
|
||||
Future<void> actualizarPassword(int usuarioId, String passwordActual, String passwordNuevo) async {
|
||||
final response = await http.put(
|
||||
Uri.parse('$_baseUrl/api/usuarios/$usuarioId/password'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({
|
||||
'colonia': colonia,
|
||||
'direccion': direccion.trim(),
|
||||
'password_actual': passwordActual,
|
||||
'password_nuevo': passwordNuevo,
|
||||
}),
|
||||
).timeout(_timeout);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Error al guardar la dirección: ${response.body}');
|
||||
}
|
||||
if (response.statusCode != 200) _throwError(response);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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,
|
||||
Uri.parse('$_baseUrl/api/usuarios/$usuarioId/fcm-token'),
|
||||
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}');
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// RUTAS
|
||||
// ================================================================
|
||||
|
||||
Future<List<RouteInfo>> obtenerRutas(int usuarioId) async {
|
||||
final response = await http
|
||||
.get(Uri.parse('$_baseUrl/api/rutas?usuario_id=$usuarioId'))
|
||||
.timeout(_timeout);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return List<Map<String, dynamic>>.from(json.decode(response.body)['rutas'])
|
||||
.map(RouteInfo.fromJson)
|
||||
.toList();
|
||||
}
|
||||
_throwError(response);
|
||||
}
|
||||
|
||||
Future<RouteInfo> avanzarRuta(String routeId, int usuarioId) async {
|
||||
final response = await http
|
||||
.post(Uri.parse('$_baseUrl/api/rutas/$routeId/avanzar?usuario_id=$usuarioId'))
|
||||
.timeout(_timeout);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return RouteInfo.fromJson(json.decode(response.body));
|
||||
}
|
||||
_throwError(response);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// VISUALIZACIÓN — NUEVOS EN v2
|
||||
// ================================================================
|
||||
|
||||
/// Dashboard global: estado de todas las rutas + métricas de usuarios.
|
||||
Future<DashboardInfo> obtenerDashboard() async {
|
||||
final response = await http
|
||||
.get(Uri.parse('$_baseUrl/api/dashboard'))
|
||||
.timeout(_timeout);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return DashboardInfo.fromJson(json.decode(response.body));
|
||||
}
|
||||
_throwError(response);
|
||||
}
|
||||
|
||||
/// Historial de posiciones GPS de una ruta, con la posición actual marcada.
|
||||
Future<List<PosicionGPS>> historialPosiciones(String routeId) async {
|
||||
final response = await http
|
||||
.get(Uri.parse('$_baseUrl/api/rutas/$routeId/posiciones'))
|
||||
.timeout(_timeout);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return List<Map<String, dynamic>>.from(json.decode(response.body))
|
||||
.map(PosicionGPS.fromJson)
|
||||
.toList();
|
||||
}
|
||||
_throwError(response);
|
||||
}
|
||||
|
||||
/// Vista rápida y ligera de todas las rutas. Ideal para polling frecuente.
|
||||
Future<List<Map<String, dynamic>>> resumenRutas() async {
|
||||
final response = await http
|
||||
.get(Uri.parse('$_baseUrl/api/rutas/resumen'))
|
||||
.timeout(_timeout);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return List<Map<String, dynamic>>.from(json.decode(response.body)['rutas']);
|
||||
}
|
||||
_throwError(response);
|
||||
}
|
||||
|
||||
/// Estadísticas por colonia: usuarios y cobertura de notificaciones.
|
||||
Future<List<ColoniaEstadistica>> estadisticasColonias() async {
|
||||
final response = await http
|
||||
.get(Uri.parse('$_baseUrl/api/estadisticas/colonias'))
|
||||
.timeout(_timeout);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return List<Map<String, dynamic>>.from(json.decode(response.body))
|
||||
.map(ColoniaEstadistica.fromJson)
|
||||
.toList();
|
||||
}
|
||||
_throwError(response);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user