Files
HackOnLinces_app/aplicacion_hack/lib/services/api_service.dart
2026-05-23 01:29:27 -06:00

484 lines
15 KiB
Dart

// ================================================================
// lib/services/api_service.dart (v2)
// Servicio de comunicación con el backend FastAPI
// ================================================================
//
// 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;
// ----------------------------------------------------------------
// MODELOS
// ----------------------------------------------------------------
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 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'],
);
}
}
/// 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 — 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.198.224:8000';
static const Duration _timeout = Duration(seconds: 10);
// ----------------------------------------------------------------
// HELPER PRIVADO: maneja errores HTTP de forma consistente
// ----------------------------------------------------------------
Never _throwError(http.Response response) {
Map<String, dynamic> body = {};
try {
body = json.decode(response.body);
} catch (_) {}
final detail = body['detail'] ?? response.body;
throw Exception(detail);
}
// ================================================================
// 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(
Uri.parse('$_baseUrl/api/usuarios/login'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'email': email.trim().toLowerCase(), 'password': password}),
).timeout(_timeout);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return {'usuario_id': data['usuario_id'], 'nombre': data['nombre']};
}
_throwError(response);
}
Future<int> registrarUsuario(
String nombre,
String email,
String password,
String direccion,
String colonia,
) async {
final response = await http.post(
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) {
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);
}
Future<UsuarioInfo> obtenerUsuario(int usuarioId) async {
final response = await http
.get(Uri.parse('$_baseUrl/api/usuarios/$usuarioId'))
.timeout(_timeout);
if (response.statusCode == 200) {
return UsuarioInfo.fromJson(json.decode(response.body));
}
_throwError(response);
}
Future<void> agregarDireccion(int usuarioId, String colonia, String direccion) async {
final response = await http.post(
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({
'password_actual': passwordActual,
'password_nuevo': passwordNuevo,
}),
).timeout(_timeout);
if (response.statusCode != 200) _throwError(response);
}
Future<void> registrarFcmToken(int usuarioId, String fcmToken) async {
final response = await http.put(
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) {
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);
}
}