fix: add project contents
This commit is contained in:
483
HackOnLinces_app/aplicacion_hack/lib/services/api_service.dart
Normal file
483
HackOnLinces_app/aplicacion_hack/lib/services/api_service.dart
Normal file
@@ -0,0 +1,483 @@
|
||||
// ================================================================
|
||||
// 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.55: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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user