205 lines
7.6 KiB
Dart
205 lines
7.6 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import '../models/route_model.dart';
|
|
import '../models/models.dart';
|
|
import '../data/routes_data.dart';
|
|
import '../database/db_helper.dart';
|
|
|
|
enum NotifEvent { routeStart, truckProximity, routeCompleted, gpsLost, truckStopped, routeCancelled, none }
|
|
|
|
class AppNotification {
|
|
final NotifEvent event;
|
|
final String title;
|
|
final String body;
|
|
final String routeId; // Para filtrar por usuario
|
|
final DateTime timestamp;
|
|
AppNotification({required this.event, required this.title,
|
|
required this.body, required this.routeId})
|
|
: timestamp = DateTime.now();
|
|
}
|
|
|
|
class SimulatorState {
|
|
final String routeId;
|
|
int positionIndex;
|
|
bool gpsActive;
|
|
DateTime lastMoved;
|
|
bool stoppedAlertSent;
|
|
SimulatorState({required this.routeId, this.positionIndex = 0,
|
|
this.gpsActive = true, required this.lastMoved, this.stoppedAlertSent = false});
|
|
}
|
|
|
|
class RouteSimulatorService extends ChangeNotifier {
|
|
final Map<String, SimulatorState> _states = {};
|
|
Timer? _globalTimer;
|
|
Timer? _gpsMonitorTimer;
|
|
|
|
AppNotification? _lastNotification; // Admin ve todas
|
|
final List<AppNotification> _history = [];
|
|
|
|
// ── Getters ─────────────────────────────────────────────────────────────
|
|
// Admin: ve la última notificación global
|
|
AppNotification? get lastNotification => _lastNotification;
|
|
|
|
// Ciudadano/Conductor: solo ve notificaciones de 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();
|
|
|
|
// ── Inicio ───────────────────────────────────────────────────────────────
|
|
void startAllRoutes() {
|
|
for (final r in routesData) {
|
|
_states[r.routeId] = SimulatorState(routeId: r.routeId, lastMoved: DateTime.now());
|
|
}
|
|
_globalTimer?.cancel();
|
|
_globalTimer = Timer.periodic(const Duration(seconds: 30), (_) => _tick());
|
|
_gpsMonitorTimer?.cancel();
|
|
_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());
|
|
notifyListeners();
|
|
}
|
|
|
|
void _tick() {
|
|
bool changed = false;
|
|
for (final entry in _states.entries) {
|
|
final state = entry.value;
|
|
final route = getRouteById(state.routeId);
|
|
if (route == null || !state.gpsActive) continue;
|
|
if (state.positionIndex < route.positions.length - 1) {
|
|
state.positionIndex++;
|
|
state.lastMoved = DateTime.now();
|
|
_checkNotification(state, route);
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed) notifyListeners();
|
|
}
|
|
|
|
void _monitorGps() {
|
|
for (final state in _states.values) {
|
|
if (!state.gpsActive) continue;
|
|
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',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _checkNotification(SimulatorState state, RouteModel route) {
|
|
if (state.positionIndex == 1) {
|
|
_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);
|
|
}
|
|
}
|
|
|
|
void _fireNotif(NotifEvent event, String title, String body, String 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,
|
|
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()));
|
|
}
|
|
|
|
// Notificación manual (admin cancela, retrasa ruta, etc.)
|
|
void fireCustomNotification(String title, String body, String routeId, NotifEvent event) {
|
|
_fireNotif(event, title, body, routeId);
|
|
}
|
|
|
|
// ── GPS ──────────────────────────────────────────────────────────────────
|
|
Future<void> simulateGpsLost(String routeId) async {
|
|
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',
|
|
);
|
|
notifyListeners();
|
|
}
|
|
|
|
void restoreGps(String routeId) {
|
|
final state = _states[routeId];
|
|
if (state == null) return;
|
|
state.gpsActive = true;
|
|
state.stoppedAlertSent = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
// ── Getters de estado ────────────────────────────────────────────────────
|
|
SimulatorState? getState(String routeId) => _states[routeId];
|
|
int getPositionIndex(String routeId) => _states[routeId]?.positionIndex ?? 0;
|
|
bool isTruckClose(String routeId) => getPositionIndex(routeId) >= 3;
|
|
bool isGpsActive(String routeId) => _states[routeId]?.gpsActive ?? true;
|
|
|
|
String getEtaText(String routeId) {
|
|
final state = _states[routeId];
|
|
if (state == null) return 'Sin información';
|
|
if (!state.gpsActive) return '📡 Señal GPS perdida';
|
|
final route = getRouteById(routeId);
|
|
if (route == null) return 'Ruta no encontrada';
|
|
final idx = state.positionIndex;
|
|
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';
|
|
default: return '🏁 Servicio del día finalizado';
|
|
}
|
|
}
|
|
|
|
void dismissNotification() {
|
|
_lastNotification = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
void dismissRouteNotification(String routeId) {
|
|
if (_lastNotification?.routeId == routeId) {
|
|
_lastNotification = null;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_globalTimer?.cancel();
|
|
_gpsMonitorTimer?.cancel();
|
|
super.dispose();
|
|
}
|
|
}
|