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, truckApproaching15min, routeCompleted, gpsLost, truckStopped, routeCancelled, reviewPrompt, none } class AppNotification { final NotifEvent event; final String title; final String body; final String routeId; 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; 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 { final Map _states = {}; Timer? _globalTimer; Timer? _gpsMonitorTimer; AppNotification? _lastNotification; final List _history = []; // ── Getters ────────────────────────────────────────────────────────────── AppNotification? get lastNotification => _lastNotification; // Ciudadano/Conductor: solo su ruta AppNotification? getNotificationForRoute(String routeId) { if (_lastNotification?.routeId == routeId) return _lastNotification; return null; } List get history => List.unmodifiable(_history); List 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()); } _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; _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) { 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. ' '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); _lastNotification = n; _history.insert(0, n); notifyListeners(); } Future _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())); } void fireCustomNotification(String title, String body, String routeId, NotifEvent event) { _fireNotif(event, title, body, routeId); } // ── GPS ────────────────────────────────────────────────────────────────── Future simulateGpsLost(String routeId) async { final state = _states[routeId]; if (state == null) return; state.gpsActive = false; 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(); } 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 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) { 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 — 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 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() { _globalTimer?.cancel(); _gpsMonitorTimer?.cancel(); super.dispose(); } }