From dcf3f83f02a28cec839546ec5c9c6cd5ec43537c Mon Sep 17 00:00:00 2001 From: hack_21031301_c761d3 <21031301@itcelaya.edu.mx> Date: Sat, 23 May 2026 01:17:49 -0600 Subject: [PATCH] feat: configuracion de alertas, WebSocket con fallback polling --- frontend/App.js | 154 ++++++++++++++++++++++++++++++------------------ 1 file changed, 97 insertions(+), 57 deletions(-) diff --git a/frontend/App.js b/frontend/App.js index 8c80dbf..e41a3cc 100644 --- a/frontend/App.js +++ b/frontend/App.js @@ -2,8 +2,6 @@ import { StatusBar } from 'expo-status-bar'; import { StyleSheet, Text, View, TextInput, TouchableOpacity, ActivityIndicator, ScrollView, Alert, RefreshControl } from 'react-native'; import { useState, useEffect, useCallback } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; -import * as Notifications from 'expo-notifications'; -import * as Device from 'expo-device'; const API_URL = 'http://10.137.112.65:8000'; @@ -11,13 +9,6 @@ const COLONIAS = [ 'Zona Centro', 'Las Arboledas', 'Trojes', 'San Juanico', 'Los Olivos', 'Rancho Seco', 'Las Insurgentes' ]; -Notifications.setNotificationHandler({ - handleNotification: async () => ({ - shouldShowAlert: true, - shouldPlaySound: true, - shouldSetBadge: false, - }), -}); export default function App() { const [screen, setScreen] = useState('splash'); @@ -35,25 +26,43 @@ export default function App() { const [mostrarColonias, setMostrarColonias] = useState(false); const [direccion, setDireccion] = useState(''); const [codigoPostal, setCodigoPostal] = useState(''); - -useEffect(() => { - cargarSesion(); - registrarNotificaciones(); -}, []); + const [notifConfig, setNotifConfig] = useState({ + routeStart: true, + proximity: true, + completed: true, + }); useEffect(() => { + cargarSesion(); + }, []); + + useEffect(() => { + let ws = null; + let interval = null; if (screen === 'eta' && domicilioActivo && token) { - const interval = setInterval(() => { - consultarETA(domicilioActivo.id, token, true); + ws = conectarWebSocket(domicilioActivo.id, token); + interval = setInterval(() => { + if (!ws || ws.readyState !== WebSocket.OPEN) { + consultarETA(domicilioActivo.id, token, true); + } }, 120000); - return () => clearInterval(interval); } + return () => { + if (ws) ws.close(); + if (interval) clearInterval(interval); + }; }, [screen, domicilioActivo, token]); + const enviarNotificacionLocal = async (titulo, cuerpo) => { + Alert.alert(titulo, cuerpo); + }; + const cargarSesion = async () => { try { const t = await AsyncStorage.getItem('token'); const dId = await AsyncStorage.getItem('domicilioId'); + const config = await AsyncStorage.getItem('notifConfig'); + if (config) setNotifConfig(JSON.parse(config)); if (t) { setToken(t); const doms = await cargarDomicilios(t); @@ -87,6 +96,7 @@ useEffect(() => { setToken(null); setDomicilios([]); setDomicilioActivo(null); setEta(null); setEmail(''); setPassword(''); setTelefono(''); setDireccion(''); setColoniaSeleccionada(''); setCodigoPostal(''); + setNotifConfig({ routeStart: true, proximity: true, completed: true }); setScreen('login'); }; @@ -180,23 +190,17 @@ useEffect(() => { }); const data = await res.json(); if (data.mensaje) { - if (data.evento === 'TRUCK_PROXIMITY' && eta?.evento !== 'TRUCK_PROXIMITY') { - enviarNotificacionLocal( - '🚨 ¡Camión cercano!', - `El camión está a menos de 15 minutos de ${data.colonia}. Saca tus bolsas a la acera.` - ); + if (data.evento === 'TRUCK_PROXIMITY' && eta?.evento !== 'TRUCK_PROXIMITY' && notifConfig.proximity) { + enviarNotificacionLocal('🚨 ¡Camión cercano!', + `El camión está a menos de 15 minutos de ${data.colonia}. Saca tus bolsas a la acera.`); } - if (data.evento === 'ROUTE_START' && eta?.evento !== 'ROUTE_START') { - enviarNotificacionLocal( - '🟢 Ruta iniciada', - `El camión de ${data.colonia} ha salido. Prepara tus residuos.` - ); + if (data.evento === 'ROUTE_START' && eta?.evento !== 'ROUTE_START' && notifConfig.routeStart) { + enviarNotificacionLocal('🟢 Ruta iniciada', + `El camión de ${data.colonia} ha salido. Prepara tus residuos.`); } - if (data.evento === 'ROUTE_COMPLETED' && eta?.evento !== 'ROUTE_COMPLETED') { - enviarNotificacionLocal( - '✅ Servicio finalizado', - `El camión de ${data.colonia} ha concluido su jornada.` - ); + if (data.evento === 'ROUTE_COMPLETED' && eta?.evento !== 'ROUTE_COMPLETED' && notifConfig.completed) { + enviarNotificacionLocal('✅ Servicio finalizado', + `El camión de ${data.colonia} ha concluido su jornada.`); } setEta(data); } @@ -210,25 +214,37 @@ useEffect(() => { setRefreshing(false); }, [domicilioActivo, token]); - const registrarNotificaciones = async () => { - if (!Device.isDevice) return; - const { status } = await Notifications.requestPermissionsAsync(); - if (status !== 'granted') return; - }; - - const enviarNotificacionLocal = async (titulo, cuerpo) => { - await Notifications.scheduleNotificationAsync({ - content: { title: titulo, body: cuerpo, sound: true }, - trigger: null, - }); - }; - const seleccionarDomicilio = async (dom) => { setDomicilioActivo(dom); await AsyncStorage.setItem('domicilioId', String(dom.id)); consultarETA(dom.id, token, true); }; + const conectarWebSocket = (id, t) => { + const ws = new WebSocket(`ws://10.137.112.65:8000/ws/eta/${id}?token=${t}`); + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.mensaje) { + if (data.evento === 'TRUCK_PROXIMITY' && eta?.evento !== 'TRUCK_PROXIMITY' && notifConfig.proximity) { + enviarNotificacionLocal('🚨 ¡Camión cercano!', + `El camión está a menos de 15 minutos de ${data.colonia}. Saca tus bolsas a la acera.`); + } + if (data.evento === 'ROUTE_START' && eta?.evento !== 'ROUTE_START' && notifConfig.routeStart) { + enviarNotificacionLocal('🟢 Ruta iniciada', + `El camión de ${data.colonia} ha salido. Prepara tus residuos.`); + } + if (data.evento === 'ROUTE_COMPLETED' && eta?.evento !== 'ROUTE_COMPLETED' && notifConfig.completed) { + enviarNotificacionLocal('✅ Servicio finalizado', + `El camión de ${data.colonia} ha concluido su jornada.`); + } + setEta(data); + } + }; + ws.onerror = () => console.log('WS error, usando polling'); + ws.onclose = () => console.log('WS cerrado'); + return ws; + }; + if (screen === 'splash') return ( 🚛 @@ -243,7 +259,6 @@ useEffect(() => { 🚛 BasuraApp Ingresa a tu cuenta - setUsarTelefono(false)}> @@ -254,7 +269,6 @@ useEffect(() => { 📱 Teléfono - {usarTelefono ? @@ -263,7 +277,6 @@ useEffect(() => { } - {loading ? : <> Iniciar sesión @@ -287,19 +300,16 @@ useEffect(() => { 📍 Agregar domicilio ¿Dónde quieres recibir alertas? - - setMostrarColonias(!mostrarColonias)}> {coloniaSeleccionada || 'Selecciona tu colonia ▾'} - {mostrarColonias && ( {COLONIAS.map(c => ( @@ -310,7 +320,6 @@ useEffect(() => { ))} )} - {domicilios.length > 0 && ( Tus domicilios registrados: @@ -323,17 +332,14 @@ useEffect(() => { ))} )} - {loading ? : Guardar domicilio } - {domicilios.length > 0 && setScreen('eta')}> ← Volver al horario } - Cerrar sesión @@ -344,7 +350,6 @@ useEffect(() => { }> 🕐 Horario de recolección - {domicilios.length > 1 && ( { ))} )} - {loading ? : eta ? <> @@ -390,6 +394,9 @@ useEffect(() => { setScreen('reporte')}> 📋 Reportar incidencia + setScreen('configuracion')}> + ⚙️ Configurar alertas + Cerrar sesión @@ -441,6 +448,35 @@ useEffect(() => { ); + + if (screen === 'configuracion') return ( + + ⚙️ Configurar alertas + Elige qué notificaciones recibir + {[ + { key: 'routeStart', label: '🟢 Ruta iniciada', desc: 'Cuando el camión sale del depósito' }, + { key: 'proximity', label: '🚨 Camión cercano', desc: 'Cuando el camión está a menos de 15 min' }, + { key: 'completed', label: '✅ Servicio finalizado', desc: 'Cuando el camión termina su recorrido' }, + ].map(item => ( + { + const nueva = { ...notifConfig, [item.key]: !notifConfig[item.key] }; + setNotifConfig(nueva); + await AsyncStorage.setItem('notifConfig', JSON.stringify(nueva)); + }}> + + {item.label} + {item.desc} + + {notifConfig[item.key] ? '🔔' : '🔕'} + + ))} + setScreen('eta')}> + ← Volver al horario + + + ); } const styles = StyleSheet.create({ @@ -489,4 +525,8 @@ const styles = StyleSheet.create({ separacionEmoji: { fontSize: 32 }, separacionTipo: { fontSize: 16, fontWeight: 'bold', color: '#333' }, separacionEjemplos: { fontSize: 12, color: '#666', marginTop: 2 }, + notifItem: { width: '100%', backgroundColor: '#fff', borderRadius: 12, padding: 16, marginBottom: 10, flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderColor: '#ddd' }, + notifItemActivo: { borderColor: '#1a7a4a', backgroundColor: '#e8f5ee' }, + notifLabel: { fontSize: 15, fontWeight: 'bold', color: '#333' }, + notifDesc: { fontSize: 12, color: '#666', marginTop: 2 }, }); \ No newline at end of file