diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index 47a7453..fdbc28f 100644 Binary files a/backend/__pycache__/main.cpython-312.pyc and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/main.py b/backend/main.py index ed7077a..024c22d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -93,4 +93,12 @@ def crear_reporte( "domicilio_id": domicilio_id, "descripcion": descripcion, "estado": "PENDIENTE" - } \ No newline at end of file + } + +@app.get("/domicilios") +def listar_domicilios( + current_user=Depends(auth.get_current_user), + db: Session = Depends(get_db) +): + domicilios = db.query(models.Domicilio).filter_by(usuario_id=current_user.id).all() + return [{"id": d.id, "direccion": d.direccion, "colonia": d.colonia, "route_id": d.route_id} for d in domicilios] \ No newline at end of file diff --git a/frontend/App.js b/frontend/App.js index 00816d1..8e16150 100644 --- a/frontend/App.js +++ b/frontend/App.js @@ -1,166 +1,187 @@ import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, Text, View, TextInput, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native'; -import { useState, useEffect } from 'react'; +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 [domicilioId, setDomicilioId] = useState(null); + const [domicilios, setDomicilios] = useState([]); + const [domicilioActivo, setDomicilioActivo] = useState(null); const [eta, setEta] = useState(null); const [loading, setLoading] = useState(false); - const [colonia, setColonia] = useState(''); + const [refreshing, setRefreshing] = useState(false); + const [coloniaSeleccionada, setColoniaSeleccionada] = useState(''); + const [mostrarColonias, setMostrarColonias] = useState(false); const [direccion, setDireccion] = useState(''); + const [codigoPostal, setCodigoPostal] = useState(''); + + useEffect(() => { cargarSesion(); }, []); useEffect(() => { - cargarSesion(); - }, []); + if (screen === 'eta' && domicilioActivo && token) { + const interval = setInterval(() => { + consultarETA(domicilioActivo.id, token, true); + }, 120000); + return () => clearInterval(interval); + } + }, [screen, domicilioActivo, token]); const cargarSesion = async () => { try { const t = await AsyncStorage.getItem('token'); - const d = await AsyncStorage.getItem('domicilioId'); - if (t && d) { + const dId = await AsyncStorage.getItem('domicilioId'); + if (t) { setToken(t); - setDomicilioId(parseInt(d)); - setScreen('eta'); - consultarETA(parseInt(d), t); - } else if (t) { - setToken(t); - setScreen('domicilio'); + 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'); - } + } catch { setScreen('login'); } }; - const guardarSesion = async (t, dId) => { - await AsyncStorage.setItem('token', t); - if (dId) await AsyncStorage.setItem('domicilioId', String(dId)); + 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); - setDomicilioId(null); - setEta(null); - setEmail(''); - setPassword(''); - setDireccion(''); - setColonia(''); + setToken(null); setDomicilios([]); setDomicilioActivo(null); + setEta(null); setEmail(''); setPassword(''); setTelefono(''); + setDireccion(''); setColoniaSeleccionada(''); setCodigoPostal(''); setScreen('login'); }; const register = async () => { - if (!email.trim() || !password.trim()) { - Alert.alert('Campos requeridos', 'Por favor ingresa tu email y contraseña'); - return; + 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', 'La contraseña debe tener al menos 4 caracteres'); - return; + 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, password }), + body: JSON.stringify({ email: identifier, password }), }); const data = await res.json(); if (data.access_token) { setToken(data.access_token); - await guardarSesion(data.access_token, null); + 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'); - } + } catch { Alert.alert('Error', 'No se pudo conectar al servidor'); } setLoading(false); }; const login = async () => { - if (!email.trim() || !password.trim()) { - Alert.alert('Campos requeridos', 'Por favor ingresa tu email y contraseña'); - return; + 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, password }), + body: JSON.stringify({ email: identifier, password }), }); const data = await res.json(); if (data.access_token) { setToken(data.access_token); - await guardarSesion(data.access_token, null); - const domGuardado = await AsyncStorage.getItem('domicilioId'); - if (domGuardado) { - setDomicilioId(parseInt(domGuardado)); + 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(parseInt(domGuardado), data.access_token); + 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'); - } + } 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, lat: 20.5185, lng: -100.8450 }), + 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) { - setDomicilioId(data.id); - await guardarSesion(token, 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'); - } + } 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) => { - setLoading(true); + const consultarETA = async (id, t, silencioso = false) => { + if (!silencioso) setLoading(true); try { - const res = await fetch(`${API_URL}/eta/${id || domicilioId}`, { - headers: { 'Authorization': `Bearer ${t || token}` }, + const res = await fetch(`${API_URL}/eta/${id}`, { + headers: { 'Authorization': `Bearer ${t || token}` } }); const data = await res.json(); if (data.mensaje) setEta(data); - else { - await cerrarSesion(); - } - } catch { - Alert.alert('Error', 'No se pudo obtener el ETA'); - } - setLoading(false); + } catch { if (!silencioso) Alert.alert('Error', 'No se pudo obtener el ETA'); } + if (!silencioso) 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); }; if (screen === 'splash') return ( @@ -177,10 +198,27 @@ export default function App() { 🚛 BasuraApp Ingresa a tu cuenta - + + + setUsarTelefono(false)}> + 📧 Email + + setUsarTelefono(true)}> + 📱 Teléfono + + + + {usarTelefono + ? + : + } + {loading ? : <> Iniciar sesión @@ -188,23 +226,69 @@ export default function App() { 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 ( - 📍 Mi domicilio - ¿En qué colonia vives? + 📍 Agregar domicilio + ¿Dónde quieres recibir alertas? + - - Colonias: Zona Centro, Las Arboledas, Trojes, San Juanico, Los Olivos, Rancho Seco, Las Insurgentes + + + 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 y ver horario + Guardar domicilio } + + {domicilios.length > 0 && + setScreen('eta')}> + ← Volver al horario + } + Cerrar sesión @@ -212,8 +296,25 @@ export default function App() { ); if (screen === 'eta') return ( - + }> 🕐 Horario de recolección + + {domicilios.length > 1 && ( + + {domicilios.map(d => ( + seleccionarDomicilio(d)}> + + 📍 {d.colonia} + + + ))} + + )} + {loading ? : eta ? <> @@ -231,9 +332,12 @@ export default function App() { 🔒 Solo ves la información de tu zona. No se muestra la ruta completa del camión. - consultarETA(domicilioId, token)}> + consultarETA(domicilioActivo.id, token)}> Actualizar + setScreen('domicilio')}> + ➕ Agregar otro domicilio + setScreen('separacion')}> 📚 Guía de separación @@ -277,9 +381,8 @@ export default function App() { {['El camión no pasó', 'Pasó fuera de horario', 'No recogió mis residuos', 'Otro'].map(tipo => ( { - await fetch(`${API_URL}/reportes?domicilio_id=${domicilioId}&tipo=${tipo}&descripcion=${tipo}`, { - method: 'POST', - headers: { 'Authorization': `Bearer ${token}` } + 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'); @@ -300,15 +403,33 @@ const styles = StyleSheet.create({ 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: 28, fontWeight: 'bold', color: '#1a7a4a', marginBottom: 6, textAlign: 'center' }, - subtitle: { fontSize: 15, color: '#555', marginBottom: 24, textAlign: 'center' }, + 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 }, - hint: { fontSize: 11, color: '#888', marginBottom: 16, textAlign: 'center' }, + 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 }, + domicilioTabActivo: { backgroundColor: '#1a7a4a', borderColor: '#1a7a4a' }, + 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 },