diff --git a/README.md b/README.md index 9fe7fc2..9c67d75 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ PostgreSQL — Usuario → Domicilio → Zona → Ruta - [x] 3 eventos de notificación: ROUTE_START, TRUCK_PROXIMITY, ROUTE_COMPLETED - [x] Guía de separación de residuos (funciona offline) - [x] RBAC — cada usuario solo consulta su ruta -- [ ] Buzón de reportes (en desarrollo) +- [X] Buzón de reportes (en desarrollo) - [ ] Notificaciones push FCM (en desarrollo) ## 🛠️ Stack Tecnológico diff --git a/frontend/App.js b/frontend/App.js index 6f16309..00816d1 100644 --- a/frontend/App.js +++ b/frontend/App.js @@ -1,11 +1,12 @@ import { StatusBar } from 'expo-status-bar'; import { StyleSheet, Text, View, TextInput, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; const API_URL = 'http://10.137.112.65:8000'; export default function App() { - const [screen, setScreen] = useState('login'); + const [screen, setScreen] = useState('splash'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [token, setToken] = useState(null); @@ -15,7 +16,56 @@ export default function App() { const [colonia, setColonia] = useState(''); const [direccion, setDireccion] = useState(''); + useEffect(() => { + cargarSesion(); + }, []); + + const cargarSesion = async () => { + try { + const t = await AsyncStorage.getItem('token'); + const d = await AsyncStorage.getItem('domicilioId'); + if (t && d) { + setToken(t); + setDomicilioId(parseInt(d)); + setScreen('eta'); + consultarETA(parseInt(d), t); + } else if (t) { + setToken(t); + setScreen('domicilio'); + } else { + setScreen('login'); + } + } catch { + setScreen('login'); + } + }; + + const guardarSesion = async (t, dId) => { + await AsyncStorage.setItem('token', t); + if (dId) await AsyncStorage.setItem('domicilioId', String(dId)); + }; + + const cerrarSesion = async () => { + await AsyncStorage.clear(); + setToken(null); + setDomicilioId(null); + setEta(null); + setEmail(''); + setPassword(''); + setDireccion(''); + setColonia(''); + setScreen('login'); + }; + const register = async () => { + if (!email.trim() || !password.trim()) { + Alert.alert('Campos requeridos', 'Por favor ingresa tu email y contraseña'); + return; + } + if (password.length < 4) { + Alert.alert('Contraseña débil', 'La contraseña debe tener al menos 4 caracteres'); + return; + } setLoading(true); try { const res = await fetch(`${API_URL}/auth/register`, { @@ -26,17 +76,22 @@ export default function App() { const data = await res.json(); if (data.access_token) { setToken(data.access_token); + await guardarSesion(data.access_token, null); setScreen('domicilio'); } else { Alert.alert('Error', data.detail || 'Error al registrar'); } - } catch (e) { + } 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; + } setLoading(true); try { const res = await fetch(`${API_URL}/auth/login`, { @@ -47,11 +102,19 @@ export default function App() { const data = await res.json(); if (data.access_token) { setToken(data.access_token); - setScreen('domicilio'); + await guardarSesion(data.access_token, null); + const domGuardado = await AsyncStorage.getItem('domicilioId'); + if (domGuardado) { + setDomicilioId(parseInt(domGuardado)); + setScreen('eta'); + consultarETA(parseInt(domGuardado), data.access_token); + } else { + setScreen('domicilio'); + } } else { Alert.alert('Error', 'Credenciales incorrectas'); } - } catch (e) { + } catch { Alert.alert('Error', 'No se pudo conectar al servidor'); } setLoading(false); @@ -66,56 +129,64 @@ export default function App() { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, - body: JSON.stringify({ - direccion, - colonia, - lat: 20.5185, - lng: -100.8450, - }), + body: JSON.stringify({ direccion, colonia, lat: 20.5185, lng: -100.8450 }), }); const data = await res.json(); if (data.id) { setDomicilioId(data.id); + await guardarSesion(token, data.id); setScreen('eta'); - consultarETA(data.id); + consultarETA(data.id, token); } else { Alert.alert('Error', data.detail || 'Colonia no encontrada'); } - } catch (e) { + } catch { Alert.alert('Error', 'No se pudo guardar el domicilio'); } setLoading(false); }; - const consultarETA = async (id) => { + const consultarETA = async (id, t) => { setLoading(true); try { - const res = await fetch(`${API_URL}/eta/${id}`, { - headers: { 'Authorization': `Bearer ${token}` }, + const res = await fetch(`${API_URL}/eta/${id || domicilioId}`, { + headers: { 'Authorization': `Bearer ${t || token}` }, }); const data = await res.json(); - setEta(data); - } catch (e) { + if (data.mensaje) setEta(data); + else { + await cerrarSesion(); + } + } catch { Alert.alert('Error', 'No se pudo obtener el ETA'); } setLoading(false); }; + if (screen === 'splash') return ( + + 🚛 + BasuraApp + + + ); + if (screen === 'login') return ( - 🚛 BasuraApp + 🚛 + BasuraApp Ingresa a tu cuenta - {loading ? : <> + {loading ? : <> Iniciar sesión - Registrarme + Crear cuenta nueva } @@ -129,11 +200,14 @@ export default function App() { value={direccion} onChangeText={setDireccion} /> - Colonias disponibles: Zona Centro, Las Arboledas, Trojes, San Juanico, Los Olivos, Rancho Seco, Las Insurgentes - {loading ? : + Colonias: Zona Centro, Las Arboledas, Trojes, San Juanico, Los Olivos, Rancho Seco, Las Insurgentes + {loading ? : Guardar y ver horario } + + Cerrar sesión + ); @@ -142,9 +216,11 @@ export default function App() { 🕐 Horario de recolección {loading ? : eta ? <> - {eta.evento === 'TRUCK_PROXIMITY' ? '🚨 ¡Camión cercano!' : - eta.evento === 'ROUTE_START' ? '🟢 Ruta iniciada' : - eta.evento === 'ROUTE_COMPLETED' ? '✅ Servicio finalizado' : '🚛 En camino'} + + {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 @@ -155,7 +231,7 @@ export default function App() { 🔒 Solo ves la información de tu zona. No se muestra la ruta completa del camión. - consultarETA(domicilioId)}> + consultarETA(domicilioId, token)}> Actualizar setScreen('separacion')}> @@ -164,6 +240,9 @@ export default function App() { setScreen('reporte')}> 📋 Reportar incidencia + + Cerrar sesión + : Sin datos} ); @@ -216,33 +295,30 @@ export default function App() { } const styles = StyleSheet.create({ - container: { flexGrow: 1, backgroundColor: '#f0f4f8', alignItems: 'center', - justifyContent: 'center', padding: 24 }, + 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: 28, fontWeight: 'bold', color: '#1a7a4a', marginBottom: 6, textAlign: 'center' }, subtitle: { fontSize: 15, color: '#555', marginBottom: 24, textAlign: 'center' }, - input: { width: '100%', backgroundColor: '#fff', borderRadius: 10, padding: 14, - fontSize: 15, marginBottom: 12, borderWidth: 1, borderColor: '#ddd' }, - btn: { width: '100%', backgroundColor: '#1a7a4a', borderRadius: 10, - padding: 16, alignItems: 'center', marginTop: 8 }, + input: { width: '100%', backgroundColor: '#fff', borderRadius: 10, padding: 14, fontSize: 15, marginBottom: 12, borderWidth: 1, borderColor: '#ddd' }, + 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' }, + 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' }, - etaCard: { width: '100%', backgroundColor: '#fff', borderRadius: 16, - padding: 20, marginBottom: 16, borderWidth: 1, borderColor: '#d0e8d8' }, + logoutText: { color: '#e53935', fontSize: 14, textAlign: 'center' }, + 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' }, + 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' }, + 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 },