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 },