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'; const API_URL = 'http://10.137.112.65:8000'; const COLONIAS = [ 'Zona Centro', 'Las Arboledas', 'Trojes', 'San Juanico', 'Los Olivos', 'Rancho Seco', 'Las Insurgentes' ]; export default function App() { const [screen, setScreen] = useState('splash'); const [email, setEmail] = useState(''); const [telefono, setTelefono] = useState(''); const [password, setPassword] = useState(''); const [usarTelefono, setUsarTelefono] = useState(false); const [token, setToken] = useState(null); const [domicilios, setDomicilios] = useState([]); const [domicilioActivo, setDomicilioActivo] = useState(null); const [eta, setEta] = useState(null); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [coloniaSeleccionada, setColoniaSeleccionada] = useState(''); const [mostrarColonias, setMostrarColonias] = useState(false); const [direccion, setDireccion] = useState(''); const [codigoPostal, setCodigoPostal] = useState(''); const [notifConfig, setNotifConfig] = useState({ routeStart: true, proximity: true, completed: true, }); useEffect(() => { cargarSesion(); }, []); useEffect(() => { let ws = null; let interval = null; if (screen === 'eta' && domicilioActivo && token) { ws = conectarWebSocket(domicilioActivo.id, token); interval = setInterval(() => { if (!ws || ws.readyState !== WebSocket.OPEN) { consultarETA(domicilioActivo.id, token, true); } }, 120000); } 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); if (doms && doms.length > 0) { const activo = dId ? doms.find(d => d.id === parseInt(dId)) || doms[0] : doms[0]; setDomicilioActivo(activo); setScreen('eta'); consultarETA(activo.id, t); } else { setScreen('domicilio'); } } else { setScreen('login'); } } catch { setScreen('login'); } }; const cargarDomicilios = async (t) => { try { const res = await fetch(`${API_URL}/domicilios`, { headers: { 'Authorization': `Bearer ${t || token}` } }); const data = await res.json(); setDomicilios(data); return data; } catch { return []; } }; const cerrarSesion = async () => { await AsyncStorage.clear(); setToken(null); setDomicilios([]); setDomicilioActivo(null); setEta(null); setEmail(''); setPassword(''); setTelefono(''); setDireccion(''); setColoniaSeleccionada(''); setCodigoPostal(''); setNotifConfig({ routeStart: true, proximity: true, completed: true }); setScreen('login'); }; const register = async () => { const identifier = usarTelefono ? telefono.trim() : email.trim(); if (!identifier || !password.trim()) { Alert.alert('Campos requeridos', 'Por favor completa todos los campos'); return; } if (password.length < 4) { Alert.alert('Contraseña débil', 'Mínimo 4 caracteres'); return; } setLoading(true); try { const res = await fetch(`${API_URL}/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: identifier, password }), }); const data = await res.json(); if (data.access_token) { setToken(data.access_token); await AsyncStorage.setItem('token', data.access_token); setScreen('domicilio'); } else { Alert.alert('Error', data.detail || 'Error al registrar'); } } catch { Alert.alert('Error', 'No se pudo conectar al servidor'); } setLoading(false); }; const login = async () => { const identifier = usarTelefono ? telefono.trim() : email.trim(); if (!identifier || !password.trim()) { Alert.alert('Campos requeridos', 'Por favor completa todos los campos'); return; } setLoading(true); try { const res = await fetch(`${API_URL}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: identifier, password }), }); const data = await res.json(); if (data.access_token) { setToken(data.access_token); await AsyncStorage.setItem('token', data.access_token); const doms = await cargarDomicilios(data.access_token); if (doms && doms.length > 0) { const activo = doms[0]; setDomicilioActivo(activo); await AsyncStorage.setItem('domicilioId', String(activo.id)); setScreen('eta'); consultarETA(activo.id, data.access_token); } else { setScreen('domicilio'); } } else { Alert.alert('Error', 'Credenciales incorrectas'); } } catch { Alert.alert('Error', 'No se pudo conectar al servidor'); } setLoading(false); }; const guardarDomicilio = async () => { if (!direccion.trim() || !coloniaSeleccionada) { Alert.alert('Campos requeridos', 'Ingresa dirección y selecciona una colonia'); return; } setLoading(true); try { const res = await fetch(`${API_URL}/domicilios`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ direccion, colonia: coloniaSeleccionada, lat: 20.5185, lng: -100.8450 }), }); const data = await res.json(); if (data.id) { await AsyncStorage.setItem('domicilioId', String(data.id)); setDomicilioActivo(data); await cargarDomicilios(token); setDireccion(''); setColoniaSeleccionada(''); setCodigoPostal(''); setScreen('eta'); consultarETA(data.id, token); } else { Alert.alert('Error', data.detail || 'Colonia no encontrada'); } } catch { Alert.alert('Error', 'No se pudo guardar el domicilio'); } setLoading(false); }; const consultarETA = async (id, t, silencioso = false) => { if (!silencioso) setLoading(true); try { const res = await fetch(`${API_URL}/eta/${id}`, { headers: { 'Authorization': `Bearer ${t || token}` } }); const data = await res.json(); 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); } } catch { if (!silencioso) Alert.alert('Error', 'No se pudo obtener el ETA'); } setLoading(false); }; const onRefresh = useCallback(async () => { setRefreshing(true); if (domicilioActivo) await consultarETA(domicilioActivo.id, token, true); setRefreshing(false); }, [domicilioActivo, token]); 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 ( 🚛 BasuraApp ); if (screen === 'login') return ( 🚛 BasuraApp Ingresa a tu cuenta setUsarTelefono(false)}> 📧 Email setUsarTelefono(true)}> 📱 Teléfono {usarTelefono ? : } {loading ? : <> Iniciar sesión Crear cuenta nueva Alert.alert('Soporte', 'Escríbenos a soporte@basuraapp.mx\no llama al 800-BASURA-1')}> ¿Necesitas ayuda? Contactar soporte Alert.alert('Recuperar contraseña', 'Te enviaremos un enlace a tu email o un SMS a tu teléfono registrado.')}> Olvidé mi contraseña } ); if (screen === 'domicilio') return ( 📍 Agregar domicilio ¿Dónde quieres recibir alertas? setMostrarColonias(!mostrarColonias)}> {coloniaSeleccionada || 'Selecciona tu colonia ▾'} {mostrarColonias && ( {COLONIAS.map(c => ( { setColoniaSeleccionada(c); setMostrarColonias(false); }}> {c} ))} )} {domicilios.length > 0 && ( Tus domicilios registrados: {domicilios.map(d => ( { seleccionarDomicilio(d); setScreen('eta'); }}> 📍 {d.direccion} — {d.colonia} ))} )} {loading ? : Guardar domicilio } {domicilios.length > 0 && setScreen('eta')}> ← Volver al horario } Cerrar sesión ); if (screen === 'eta') return ( }> 🕐 Horario de recolección {domicilios.length > 1 && ( {domicilios.map(d => ( seleccionarDomicilio(d)}> 📍 {d.colonia} ))} )} {loading ? : eta ? <> {eta.evento === 'TRUCK_PROXIMITY' ? '🚨 ¡Camión cercano!' : eta.evento === 'ROUTE_START' ? '🟢 Ruta iniciada' : eta.evento === 'ROUTE_COMPLETED' ? '✅ Servicio finalizado' : '🚛 En camino'} {eta.mensaje} Ventana de llegada {eta.ventana_inicio} – {eta.ventana_fin} 📍 {eta.colonia} · {eta.route_id} 🔒 Solo ves la información de tu zona. No se muestra la ruta completa del camión. consultarETA(domicilioActivo.id, token)}> Actualizar setScreen('domicilio')}> ➕ Agregar otro domicilio setScreen('separacion')}> 📚 Guía de separación setScreen('reporte')}> 📋 Reportar incidencia setScreen('configuracion')}> ⚙️ Configurar alertas Cerrar sesión : Sin datos} ); if (screen === 'separacion') return ( ♻️ Guía de separación {[ { emoji: '🟢', tipo: 'Orgánicos', ejemplos: 'Cáscaras, restos de comida, café, frutas' }, { emoji: '🔵', tipo: 'Reciclables', ejemplos: 'Papel, cartón, plástico, vidrio, metal' }, { emoji: '🔴', tipo: 'Sanitarios', ejemplos: 'Pañales, papel higiénico, gasas, algodón' }, { emoji: '⚠️', tipo: 'Especiales', ejemplos: 'Pilas, medicamentos, electrónicos, pinturas' }, ].map(item => ( {item.emoji} {item.tipo} {item.ejemplos} ))} setScreen('eta')}> ← Volver al horario ); if (screen === 'reporte') return ( 📋 Reportar incidencia ¿Qué problema tuviste? {['El camión no pasó', 'Pasó fuera de horario', 'No recogió mis residuos', 'Otro'].map(tipo => ( { await fetch(`${API_URL}/reportes?domicilio_id=${domicilioActivo?.id}&tipo=${tipo}&descripcion=${tipo}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); Alert.alert('¡Gracias!', 'Tu reporte fue enviado correctamente.'); setScreen('eta'); }}> {tipo} ))} setScreen('eta')}> ← Cancelar ); 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({ splashContainer: { flex: 1, backgroundColor: '#1a7a4a', alignItems: 'center', justifyContent: 'center' }, splashEmoji: { fontSize: 72 }, splashTitle: { fontSize: 36, fontWeight: 'bold', color: '#fff', marginTop: 16 }, container: { flexGrow: 1, backgroundColor: '#f0f4f8', alignItems: 'center', justifyContent: 'center', padding: 24 }, bigEmoji: { fontSize: 64, marginBottom: 8 }, title: { fontSize: 26, fontWeight: 'bold', color: '#1a7a4a', marginBottom: 6, textAlign: 'center' }, subtitle: { fontSize: 14, color: '#555', marginBottom: 20, textAlign: 'center' }, input: { width: '100%', backgroundColor: '#fff', borderRadius: 10, padding: 14, fontSize: 15, marginBottom: 12, borderWidth: 1, borderColor: '#ddd' }, combobox: { justifyContent: 'center' }, dropdown: { width: '100%', backgroundColor: '#fff', borderRadius: 10, borderWidth: 1, borderColor: '#ddd', marginBottom: 12, overflow: 'hidden' }, dropdownItem: { padding: 14, borderBottomWidth: 0.5, borderBottomColor: '#eee' }, dropdownText: { fontSize: 15, color: '#1a1a1a' }, toggleRow: { flexDirection: 'row', width: '100%', marginBottom: 16, borderRadius: 10, overflow: 'hidden', borderWidth: 1, borderColor: '#1a7a4a' }, toggleBtn: { flex: 1, padding: 12, alignItems: 'center' }, toggleActive: { backgroundColor: '#1a7a4a' }, toggleText: { fontSize: 14, color: '#1a7a4a', fontWeight: '500' }, toggleTextActive: { color: '#fff' }, btn: { width: '100%', backgroundColor: '#1a7a4a', borderRadius: 10, padding: 16, alignItems: 'center', marginTop: 8 }, btnText: { color: '#fff', fontWeight: 'bold', fontSize: 16 }, btnSecondary: { width: '100%', borderRadius: 10, padding: 16, alignItems: 'center', marginTop: 8, borderWidth: 1, borderColor: '#1a7a4a' }, btnSecondaryText: { color: '#1a7a4a', fontWeight: 'bold', fontSize: 15 }, linkText: { color: '#1a7a4a', fontSize: 13, textAlign: 'center' }, logoutText: { color: '#e53935', fontSize: 14, textAlign: 'center' }, domiciliosExistentes: { width: '100%', marginBottom: 12 }, domiciliosTitle: { fontSize: 13, color: '#555', marginBottom: 8 }, domicilioChip: { padding: 12, borderRadius: 8, borderWidth: 1, borderColor: '#ddd', backgroundColor: '#fff', marginBottom: 6 }, domicilioChipActivo: { borderColor: '#1a7a4a', backgroundColor: '#e8f5ee' }, domicilioChipText: { fontSize: 13, color: '#333' }, domicilioTab: { paddingHorizontal: 14, paddingVertical: 8, borderRadius: 20, borderWidth: 1, borderColor: '#ddd', backgroundColor: '#fff', marginRight: 8, height: 36, justifyContent: 'center' }, domicilioTabActivo: { backgroundColor: '#1a7a4a', borderColor: '#1a7a4a', height: 36 }, domicilioTabText: { fontSize: 13, color: '#555' }, domicilioTabTextActivo: { color: '#fff', fontWeight: '500' }, etaCard: { width: '100%', backgroundColor: '#fff', borderRadius: 16, padding: 20, marginBottom: 16, borderWidth: 1, borderColor: '#d0e8d8' }, etaEvento: { fontSize: 18, fontWeight: 'bold', color: '#1a7a4a', marginBottom: 8 }, etaMensaje: { fontSize: 15, color: '#333', marginBottom: 16, lineHeight: 22 }, ventanaBox: { backgroundColor: '#e8f5ee', borderRadius: 10, padding: 14, marginBottom: 12 }, ventanaLabel: { fontSize: 12, color: '#555', marginBottom: 4 }, ventanaHora: { fontSize: 24, fontWeight: 'bold', color: '#1a7a4a' }, coloniaText: { fontSize: 12, color: '#888' }, privacyBox: { width: '100%', backgroundColor: '#fff8e1', borderRadius: 10, padding: 14, marginBottom: 16, borderWidth: 1, borderColor: '#ffe082' }, privacyText: { fontSize: 12, color: '#795548', textAlign: 'center' }, separacionCard: { width: '100%', backgroundColor: '#fff', borderRadius: 12, padding: 16, marginBottom: 12, flexDirection: 'row', alignItems: 'center', gap: 14, borderWidth: 1, borderColor: '#ddd' }, 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 }, });