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';
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
const API_URL = 'http://10.137.112.65:8000';
const COLONIAS = [
'Zona Centro', 'Las Arboledas', 'Trojes', 'San Juanico',
'Los Olivos', 'Rancho Seco', 'Las Insurgentes'
];
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
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('');
useEffect(() => {
cargarSesion();
registrarNotificaciones();
}, []);
useEffect(() => {
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 dId = await AsyncStorage.getItem('domicilioId');
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('');
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') {
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') {
enviarNotificacionLocal(
'🟢 Ruta iniciada',
`El camión de ${data.colonia} ha salido. Prepara tus residuos.`
);
}
if (data.evento === 'ROUTE_COMPLETED' && eta?.evento !== 'ROUTE_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 registrarNotificaciones = async () => {
if (!Device.isDevice) return;
const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') return;
};
const enviarNotificacionLocal = async (titulo, cuerpo) => {
await Notifications.scheduleNotificationAsync({
content: { title: titulo, body: cuerpo, sound: true },
trigger: null,
});
};
const seleccionarDomicilio = async (dom) => {
setDomicilioActivo(dom);
await AsyncStorage.setItem('domicilioId', String(dom.id));
consultarETA(dom.id, token, true);
};
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
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
);
}
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 },
});