Avance de la aplicacion

This commit is contained in:
2026-05-22 20:43:49 -06:00
parent 37e83a8226
commit 458af32fcf
13 changed files with 1918 additions and 463 deletions

View File

@@ -5,11 +5,13 @@ import '../database/db_helper.dart';
class AuthService extends ChangeNotifier {
UserModel? _user;
DomicilioModel? _domicilio;
DomicilioModel? _primaryDomicilio;
List<DomicilioModel> _allDomicilios = [];
bool _loading = true;
UserModel? get currentUser => _user;
DomicilioModel? get primaryDomicilio => _domicilio;
DomicilioModel? get primaryDomicilio => _primaryDomicilio;
List<DomicilioModel> get allDomicilios => _allDomicilios;
bool get isLoggedIn => _user != null;
bool get loading => _loading;
String get rol => _user?.rol ?? '';
@@ -21,22 +23,28 @@ class AuthService extends ChangeNotifier {
final id = p.getInt('user_id');
if (id != null) {
_user = await DbHelper.getUserById(id);
if (_user?.rol == 'CIUDADANO') {
_domicilio = await DbHelper.getPrimaryDomicilio(id);
}
if (_user?.rol == 'CIUDADANO') await reloadDomicilios();
}
_loading = false;
notifyListeners();
}
Future<void> reloadDomicilios() async {
if (_user == null) return;
_allDomicilios = await DbHelper.getDomiciliosByUser(_user!.id!);
_primaryDomicilio = _allDomicilios.isNotEmpty
? _allDomicilios.firstWhere((d) => d.isPrimary,
orElse: () => _allDomicilios.first)
: null;
notifyListeners();
}
Future<String?> login(String email, String password) async {
final user = await DbHelper.getUserByEmail(email.trim().toLowerCase());
if (user == null) return 'Correo no registrado';
if (user.password != password) return 'Contraseña incorrecta';
_user = user;
if (user.rol == 'CIUDADANO') {
_domicilio = await DbHelper.getPrimaryDomicilio(user.id!);
}
if (user.rol == 'CIUDADANO') await reloadDomicilios();
final p = await SharedPreferences.getInstance();
await p.setInt('user_id', user.id!);
notifyListeners();
@@ -51,10 +59,11 @@ class AuthService extends ChangeNotifier {
final user = UserModel(nombre:nombre.trim(),
email:email.trim().toLowerCase(), password:password, rol:'CIUDADANO');
final uid = await DbHelper.insertUser(user);
await DbHelper.insertDomicilio(DomicilioModel(userId:uid, calle:calle.trim(),
colonia:colonia, routeId:routeId, horarioEstimado:horarioEstimado));
await DbHelper.insertDomicilio(DomicilioModel(userId:uid, alias:'Casa',
calle:calle.trim(), colonia:colonia, routeId:routeId,
horarioEstimado:horarioEstimado, isPrimary:true));
_user = await DbHelper.getUserById(uid);
_domicilio = await DbHelper.getPrimaryDomicilio(uid);
await reloadDomicilios();
final p = await SharedPreferences.getInstance();
await p.setInt('user_id', uid);
notifyListeners();
@@ -62,7 +71,7 @@ class AuthService extends ChangeNotifier {
}
Future<void> logout() async {
_user = null; _domicilio = null;
_user = null; _primaryDomicilio = null; _allDomicilios = [];
final p = await SharedPreferences.getInstance();
await p.remove('user_id');
notifyListeners();

View File

@@ -5,13 +5,16 @@ import '../models/models.dart';
import '../data/routes_data.dart';
import '../database/db_helper.dart';
enum NotifEvent { routeStart, truckProximity, routeCompleted, gpsLost, truckStopped, routeCancelled, none }
enum NotifEvent {
routeStart, truckProximity, truckApproaching15min, routeCompleted,
gpsLost, truckStopped, routeCancelled, reviewPrompt, none
}
class AppNotification {
final NotifEvent event;
final String title;
final String body;
final String routeId; // Para filtrar por usuario
final String routeId;
final DateTime timestamp;
AppNotification({required this.event, required this.title,
required this.body, required this.routeId})
@@ -24,8 +27,10 @@ class SimulatorState {
bool gpsActive;
DateTime lastMoved;
bool stoppedAlertSent;
SimulatorState({required this.routeId, this.positionIndex = 0,
this.gpsActive = true, required this.lastMoved, this.stoppedAlertSent = false});
bool reviewPromptSent;
SimulatorState({required this.routeId, this.positionIndex=0,
this.gpsActive=true, required this.lastMoved,
this.stoppedAlertSent=false, this.reviewPromptSent=false});
}
class RouteSimulatorService extends ChangeNotifier {
@@ -33,40 +38,40 @@ class RouteSimulatorService extends ChangeNotifier {
Timer? _globalTimer;
Timer? _gpsMonitorTimer;
AppNotification? _lastNotification; // Admin ve todas
AppNotification? _lastNotification;
final List<AppNotification> _history = [];
// ── Getters ─────────────────────────────────────────────────────────────
// Admin: ve la última notificación global
// ── Getters ─────────────────────────────────────────────────────────────
AppNotification? get lastNotification => _lastNotification;
// Ciudadano/Conductor: solo ve notificaciones de SU ruta
// Ciudadano/Conductor: solo su ruta
AppNotification? getNotificationForRoute(String routeId) {
if (_lastNotification?.routeId == routeId) return _lastNotification;
return null;
}
List<AppNotification> get history => List.unmodifiable(_history);
// Historial filtrado por ruta
List<AppNotification> historyForRoute(String routeId) =>
_history.where((n) => n.routeId == routeId).toList();
bool needsReviewPrompt(String routeId) =>
_states[routeId]?.reviewPromptSent == true;
// ── Inicio ───────────────────────────────────────────────────────────────
void startAllRoutes() {
for (final r in routesData) {
_states[r.routeId] = SimulatorState(routeId: r.routeId, lastMoved: DateTime.now());
_states[r.routeId] = SimulatorState(routeId:r.routeId, lastMoved:DateTime.now());
}
_globalTimer?.cancel();
_globalTimer = Timer.periodic(const Duration(seconds: 30), (_) => _tick());
_globalTimer = Timer.periodic(const Duration(seconds:30), (_) => _tick());
_gpsMonitorTimer?.cancel();
_gpsMonitorTimer = Timer.periodic(const Duration(minutes: 5), (_) => _monitorGps());
_gpsMonitorTimer = Timer.periodic(const Duration(minutes:5), (_) => _monitorGps());
notifyListeners();
}
void startRoute(String routeId) {
_states[routeId] = SimulatorState(routeId: routeId, lastMoved: DateTime.now());
_globalTimer ??= Timer.periodic(const Duration(seconds: 30), (_) => _tick());
_states[routeId] = SimulatorState(routeId:routeId, lastMoved:DateTime.now());
_globalTimer ??= Timer.periodic(const Duration(seconds:30), (_) => _tick());
notifyListeners();
}
@@ -92,45 +97,62 @@ class RouteSimulatorService extends ChangeNotifier {
final diff = DateTime.now().difference(state.lastMoved);
if (diff.inMinutes >= 30 && !state.stoppedAlertSent) {
state.stoppedAlertSent = true;
_fireAndSaveAlert(
event: NotifEvent.truckStopped, routeId: state.routeId,
title: '⚠️ Camión detenido',
body: 'El camión ${state.routeId} lleva +30 min sin moverse.',
tipo: 'CAMION_DETENIDO',
);
_fireAndSave(event:NotifEvent.truckStopped, routeId:state.routeId,
title:'⚠️ Camión detenido',
body:'El camión ${state.routeId} lleva +30 min sin moverse. Verifica.',
tipo:'CAMION_DETENIDO');
}
}
}
void _checkNotification(SimulatorState state, RouteModel route) {
if (state.positionIndex == 1) {
final idx = state.positionIndex;
final total = route.positions.length;
if (idx == 1) {
// Ruta iniciada
_fireNotif(NotifEvent.routeStart, '¡Ruta Iniciada! 🚛',
'El camión ha salido del Relleno Sanitario rumbo a tu sector.', state.routeId);
} else if (state.positionIndex == 3) {
_fireNotif(NotifEvent.truckProximity, 'Camión Cercano ⚠️',
'El camión está a menos de 15 minutos. ¡Saca tus bolsas!', state.routeId);
} else if (state.positionIndex == route.positions.length - 1) {
_fireNotif(NotifEvent.routeCompleted, 'Servicio Finalizado 🏁',
'El camión de ${state.routeId} concluyó su jornada.', state.routeId);
'El camión ha salido del Relleno Sanitario rumbo a tu sector. '
'Prepara tus bolsas pero espera la señal para sacarlas.', state.routeId);
} else if (idx == 2) {
// ~30 min — aviso preventivo
_fireNotif(NotifEvent.truckApproaching15min, '🕐 El camión se acerca',
'Tu camión recolector está en camino. Tendrás otro aviso cuando esté a '
'15 minutos. ⚠️ No saques la basura todavía — espera el aviso.', state.routeId);
} else if (idx == 3) {
// ~15 min — MOMENTO de sacar la basura
_fireNotif(NotifEvent.truckProximity, '⚠️ ¡Saca tus bolsas AHORA!',
'El camión llega en aprox. 15 minutos a tu colonia. '
'Este es el momento de sacar tus bolsas a la acera. '
'🚫 No persigas ni interceptes la unidad.', state.routeId);
} else if (idx == total - 2) {
// Pasando por la zona
_fireNotif(NotifEvent.truckProximity, '✅ El camión está en tu zona',
'El camión recolector está pasando por tu colonia. '
'Si ya sacaste tus bolsas, el servicio está en curso.', state.routeId);
} else if (idx == total - 1) {
// Servicio finalizado → prompt de reseña
state.reviewPromptSent = true;
_fireNotif(NotifEvent.reviewPrompt, '🌟 ¿Cómo fue el servicio?',
'¡El camión concluyó su jornada! Ayúdanos calificando el servicio '
'de recolección de hoy. Tu opinión mejora el servicio.', state.routeId);
}
}
void _fireNotif(NotifEvent event, String title, String body, String routeId) {
final n = AppNotification(event: event, title: title, body: body, routeId: routeId);
final n = AppNotification(event:event, title:title, body:body, routeId:routeId);
_lastNotification = n;
_history.insert(0, n);
notifyListeners();
}
Future<void> _fireAndSaveAlert({required NotifEvent event, required String routeId,
Future<void> _fireAndSave({required NotifEvent event, required String routeId,
required String title, required String body, required String tipo}) async {
_fireNotif(event, title, body, routeId);
await DbHelper.insertAlerta(AlertaModel(
tipo: tipo, routeId: routeId, mensaje: body,
fecha: DateTime.now().toIso8601String()));
await DbHelper.insertAlerta(AlertaModel(tipo:tipo, routeId:routeId,
mensaje:body, fecha:DateTime.now().toIso8601String()));
}
// Notificación manual (admin cancela, retrasa ruta, etc.)
void fireCustomNotification(String title, String body, String routeId, NotifEvent event) {
_fireNotif(event, title, body, routeId);
}
@@ -140,12 +162,10 @@ class RouteSimulatorService extends ChangeNotifier {
final state = _states[routeId];
if (state == null) return;
state.gpsActive = false;
await _fireAndSaveAlert(
event: NotifEvent.gpsLost, routeId: routeId,
title: '📡 GPS Desactivado',
body: 'Se perdió la señal GPS del camión $routeId.',
tipo: 'GPS_PERDIDO',
);
await _fireAndSave(event:NotifEvent.gpsLost, routeId:routeId,
title:'📡 GPS Desactivado',
body:'Se perdió la señal GPS del camión $routeId.',
tipo:'GPS_PERDIDO');
notifyListeners();
}
@@ -161,6 +181,13 @@ class RouteSimulatorService extends ChangeNotifier {
SimulatorState? getState(String routeId) => _states[routeId];
int getPositionIndex(String routeId) => _states[routeId]?.positionIndex ?? 0;
bool isTruckClose(String routeId) => getPositionIndex(routeId) >= 3;
bool isRouteCompleted(String routeId) {
final state = _states[routeId];
if (state == null) return false;
final route = getRouteById(routeId);
if (route == null) return false;
return state.positionIndex >= route.positions.length - 1;
}
bool isGpsActive(String routeId) => _states[routeId]?.gpsActive ?? true;
String getEtaText(String routeId) {
@@ -173,27 +200,28 @@ class RouteSimulatorService extends ChangeNotifier {
if (idx >= route.positions.length) return '✅ Servicio finalizado';
switch (idx) {
case 0: return '🕐 Ruta por iniciar';
case 1: return '🚛 Camión en camino';
case 2: return '🚛 Aprox. 30 min para llegar';
case 3: return '⚠️ Menos de 15 min — ¡Saca tus bolsas!';
case 4: return '🔔 El camión está en tu zona';
case 5: return 'Pasando por tu colonia';
case 6: return '↩️ Regresando al relleno';
case 1: return '🚛 Camión en camino — mantén tus bolsas adentro';
case 2: return '🚛 Aprox. 30 min — espera el aviso de 15 min';
case 3: return '⚠️ ¡15 min! Saca tus bolsas a la acera ahora';
case 4: return '🔔 El camión está en tu colonia';
case 5: return 'Recogiendo basura en tu zona';
case 6: return '↩️ Regresando al relleno sanitario';
default: return '🏁 Servicio del día finalizado';
}
}
void dismissNotification() {
_lastNotification = null;
notifyListeners();
}
void dismissNotification() { _lastNotification = null; notifyListeners(); }
void dismissRouteNotification(String routeId) {
if (_lastNotification?.routeId == routeId) {
_lastNotification = null;
notifyListeners();
}
}
void clearReviewPrompt(String routeId) {
final state = _states[routeId];
if (state != null) state.reviewPromptSent = false;
notifyListeners();
}
@override
void dispose() {