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