feat: multiples domicilios, telefono en login, combobox colonias, pull to refresh

This commit is contained in:
2026-05-22 17:34:33 -06:00
parent ed43393775
commit 6407efad74
3 changed files with 222 additions and 93 deletions

View File

@@ -93,4 +93,12 @@ def crear_reporte(
"domicilio_id": domicilio_id, "domicilio_id": domicilio_id,
"descripcion": descripcion, "descripcion": descripcion,
"estado": "PENDIENTE" "estado": "PENDIENTE"
} }
@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]

View File

@@ -1,166 +1,187 @@
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View, TextInput, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native'; import { StyleSheet, Text, View, TextInput, TouchableOpacity, ActivityIndicator, ScrollView, Alert, RefreshControl } from 'react-native';
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
const API_URL = 'http://10.137.112.65:8000'; 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() { export default function App() {
const [screen, setScreen] = useState('splash'); const [screen, setScreen] = useState('splash');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [telefono, setTelefono] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [usarTelefono, setUsarTelefono] = useState(false);
const [token, setToken] = useState(null); 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 [eta, setEta] = useState(null);
const [loading, setLoading] = useState(false); 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 [direccion, setDireccion] = useState('');
const [codigoPostal, setCodigoPostal] = useState('');
useEffect(() => { cargarSesion(); }, []);
useEffect(() => { 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 () => { const cargarSesion = async () => {
try { try {
const t = await AsyncStorage.getItem('token'); const t = await AsyncStorage.getItem('token');
const d = await AsyncStorage.getItem('domicilioId'); const dId = await AsyncStorage.getItem('domicilioId');
if (t && d) { if (t) {
setToken(t); setToken(t);
setDomicilioId(parseInt(d)); const doms = await cargarDomicilios(t);
setScreen('eta'); if (doms && doms.length > 0) {
consultarETA(parseInt(d), t); const activo = dId ? doms.find(d => d.id === parseInt(dId)) || doms[0] : doms[0];
} else if (t) { setDomicilioActivo(activo);
setToken(t); setScreen('eta');
setScreen('domicilio'); consultarETA(activo.id, t);
} else {
setScreen('domicilio');
}
} else { } else {
setScreen('login'); setScreen('login');
} }
} catch { } catch { setScreen('login'); }
setScreen('login');
}
}; };
const guardarSesion = async (t, dId) => { const cargarDomicilios = async (t) => {
await AsyncStorage.setItem('token', t); try {
if (dId) await AsyncStorage.setItem('domicilioId', String(dId)); 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 () => { const cerrarSesion = async () => {
await AsyncStorage.clear(); await AsyncStorage.clear();
setToken(null); setToken(null); setDomicilios([]); setDomicilioActivo(null);
setDomicilioId(null); setEta(null); setEmail(''); setPassword(''); setTelefono('');
setEta(null); setDireccion(''); setColoniaSeleccionada(''); setCodigoPostal('');
setEmail('');
setPassword('');
setDireccion('');
setColonia('');
setScreen('login'); setScreen('login');
}; };
const register = async () => { const register = async () => {
if (!email.trim() || !password.trim()) { const identifier = usarTelefono ? telefono.trim() : email.trim();
Alert.alert('Campos requeridos', 'Por favor ingresa tu email y contraseña'); if (!identifier || !password.trim()) {
return; Alert.alert('Campos requeridos', 'Por favor completa todos los campos'); return;
} }
if (password.length < 4) { if (password.length < 4) {
Alert.alert('Contraseña débil', 'La contraseña debe tener al menos 4 caracteres'); Alert.alert('Contraseña débil', 'Mínimo 4 caracteres'); return;
return;
} }
setLoading(true); setLoading(true);
try { try {
const res = await fetch(`${API_URL}/auth/register`, { const res = await fetch(`${API_URL}/auth/register`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }), body: JSON.stringify({ email: identifier, password }),
}); });
const data = await res.json(); const data = await res.json();
if (data.access_token) { if (data.access_token) {
setToken(data.access_token); setToken(data.access_token);
await guardarSesion(data.access_token, null); await AsyncStorage.setItem('token', data.access_token);
setScreen('domicilio'); setScreen('domicilio');
} else { } else {
Alert.alert('Error', data.detail || 'Error al registrar'); Alert.alert('Error', data.detail || 'Error al registrar');
} }
} catch { } catch { Alert.alert('Error', 'No se pudo conectar al servidor'); }
Alert.alert('Error', 'No se pudo conectar al servidor');
}
setLoading(false); setLoading(false);
}; };
const login = async () => { const login = async () => {
if (!email.trim() || !password.trim()) { const identifier = usarTelefono ? telefono.trim() : email.trim();
Alert.alert('Campos requeridos', 'Por favor ingresa tu email y contraseña'); if (!identifier || !password.trim()) {
return; Alert.alert('Campos requeridos', 'Por favor completa todos los campos'); return;
} }
setLoading(true); setLoading(true);
try { try {
const res = await fetch(`${API_URL}/auth/login`, { const res = await fetch(`${API_URL}/auth/login`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }), body: JSON.stringify({ email: identifier, password }),
}); });
const data = await res.json(); const data = await res.json();
if (data.access_token) { if (data.access_token) {
setToken(data.access_token); setToken(data.access_token);
await guardarSesion(data.access_token, null); await AsyncStorage.setItem('token', data.access_token);
const domGuardado = await AsyncStorage.getItem('domicilioId'); const doms = await cargarDomicilios(data.access_token);
if (domGuardado) { if (doms && doms.length > 0) {
setDomicilioId(parseInt(domGuardado)); const activo = doms[0];
setDomicilioActivo(activo);
await AsyncStorage.setItem('domicilioId', String(activo.id));
setScreen('eta'); setScreen('eta');
consultarETA(parseInt(domGuardado), data.access_token); consultarETA(activo.id, data.access_token);
} else { } else {
setScreen('domicilio'); setScreen('domicilio');
} }
} else { } else { Alert.alert('Error', 'Credenciales incorrectas'); }
Alert.alert('Error', 'Credenciales incorrectas'); } catch { Alert.alert('Error', 'No se pudo conectar al servidor'); }
}
} catch {
Alert.alert('Error', 'No se pudo conectar al servidor');
}
setLoading(false); setLoading(false);
}; };
const guardarDomicilio = async () => { const guardarDomicilio = async () => {
if (!direccion.trim() || !coloniaSeleccionada) {
Alert.alert('Campos requeridos', 'Ingresa dirección y selecciona una colonia'); return;
}
setLoading(true); setLoading(true);
try { try {
const res = await fetch(`${API_URL}/domicilios`, { const res = await fetch(`${API_URL}/domicilios`, {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
'Content-Type': 'application/json', body: JSON.stringify({ direccion, colonia: coloniaSeleccionada, lat: 20.5185, lng: -100.8450 }),
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ direccion, colonia, lat: 20.5185, lng: -100.8450 }),
}); });
const data = await res.json(); const data = await res.json();
if (data.id) { if (data.id) {
setDomicilioId(data.id); await AsyncStorage.setItem('domicilioId', String(data.id));
await guardarSesion(token, data.id); setDomicilioActivo(data);
await cargarDomicilios(token);
setDireccion(''); setColoniaSeleccionada(''); setCodigoPostal('');
setScreen('eta'); setScreen('eta');
consultarETA(data.id, token); consultarETA(data.id, token);
} else { } else { Alert.alert('Error', data.detail || 'Colonia no encontrada'); }
Alert.alert('Error', data.detail || 'Colonia no encontrada'); } catch { Alert.alert('Error', 'No se pudo guardar el domicilio'); }
}
} catch {
Alert.alert('Error', 'No se pudo guardar el domicilio');
}
setLoading(false); setLoading(false);
}; };
const consultarETA = async (id, t) => { const consultarETA = async (id, t, silencioso = false) => {
setLoading(true); if (!silencioso) setLoading(true);
try { try {
const res = await fetch(`${API_URL}/eta/${id || domicilioId}`, { const res = await fetch(`${API_URL}/eta/${id}`, {
headers: { 'Authorization': `Bearer ${t || token}` }, headers: { 'Authorization': `Bearer ${t || token}` }
}); });
const data = await res.json(); const data = await res.json();
if (data.mensaje) setEta(data); if (data.mensaje) setEta(data);
else { } catch { if (!silencioso) Alert.alert('Error', 'No se pudo obtener el ETA'); }
await cerrarSesion(); if (!silencioso) setLoading(false);
} };
} catch {
Alert.alert('Error', 'No se pudo obtener el ETA'); const onRefresh = useCallback(async () => {
} setRefreshing(true);
setLoading(false); 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 ( if (screen === 'splash') return (
@@ -177,10 +198,27 @@ export default function App() {
<Text style={styles.bigEmoji}>🚛</Text> <Text style={styles.bigEmoji}>🚛</Text>
<Text style={styles.title}>BasuraApp</Text> <Text style={styles.title}>BasuraApp</Text>
<Text style={styles.subtitle}>Ingresa a tu cuenta</Text> <Text style={styles.subtitle}>Ingresa a tu cuenta</Text>
<TextInput style={styles.input} placeholder="Email" value={email}
onChangeText={setEmail} keyboardType="email-address" autoCapitalize="none" /> <View style={styles.toggleRow}>
<TouchableOpacity style={[styles.toggleBtn, !usarTelefono && styles.toggleActive]}
onPress={() => setUsarTelefono(false)}>
<Text style={[styles.toggleText, !usarTelefono && styles.toggleTextActive]}>📧 Email</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.toggleBtn, usarTelefono && styles.toggleActive]}
onPress={() => setUsarTelefono(true)}>
<Text style={[styles.toggleText, usarTelefono && styles.toggleTextActive]}>📱 Teléfono</Text>
</TouchableOpacity>
</View>
{usarTelefono
? <TextInput style={styles.input} placeholder="Número de teléfono" value={telefono}
onChangeText={setTelefono} keyboardType="phone-pad" />
: <TextInput style={styles.input} placeholder="Email" value={email}
onChangeText={setEmail} keyboardType="email-address" autoCapitalize="none" />
}
<TextInput style={styles.input} placeholder="Contraseña" value={password} <TextInput style={styles.input} placeholder="Contraseña" value={password}
onChangeText={setPassword} secureTextEntry /> onChangeText={setPassword} secureTextEntry />
{loading ? <ActivityIndicator color="#1a7a4a" size="large" style={{ marginTop: 16 }} /> : <> {loading ? <ActivityIndicator color="#1a7a4a" size="large" style={{ marginTop: 16 }} /> : <>
<TouchableOpacity style={styles.btn} onPress={login}> <TouchableOpacity style={styles.btn} onPress={login}>
<Text style={styles.btnText}>Iniciar sesión</Text> <Text style={styles.btnText}>Iniciar sesión</Text>
@@ -188,23 +226,69 @@ export default function App() {
<TouchableOpacity style={styles.btnSecondary} onPress={register}> <TouchableOpacity style={styles.btnSecondary} onPress={register}>
<Text style={styles.btnSecondaryText}>Crear cuenta nueva</Text> <Text style={styles.btnSecondaryText}>Crear cuenta nueva</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={{ marginTop: 16 }}
onPress={() => Alert.alert('Soporte', 'Escríbenos a soporte@basuraapp.mx\no llama al 800-BASURA-1')}>
<Text style={styles.linkText}>¿Necesitas ayuda? Contactar soporte</Text>
</TouchableOpacity>
<TouchableOpacity style={{ marginTop: 8 }}
onPress={() => Alert.alert('Recuperar contraseña', 'Te enviaremos un enlace a tu email o un SMS a tu teléfono registrado.')}>
<Text style={styles.linkText}>Olvidé mi contraseña</Text>
</TouchableOpacity>
</>} </>}
</ScrollView> </ScrollView>
); );
if (screen === 'domicilio') return ( if (screen === 'domicilio') return (
<ScrollView contentContainerStyle={styles.container}> <ScrollView contentContainerStyle={styles.container}>
<Text style={styles.title}>📍 Mi domicilio</Text> <Text style={styles.title}>📍 Agregar domicilio</Text>
<Text style={styles.subtitle}>¿En qué colonia vives?</Text> <Text style={styles.subtitle}>¿Dónde quieres recibir alertas?</Text>
<TextInput style={styles.input} placeholder="Dirección (ej: Calle Morelos 123)" <TextInput style={styles.input} placeholder="Dirección (ej: Calle Morelos 123)"
value={direccion} onChangeText={setDireccion} /> value={direccion} onChangeText={setDireccion} />
<TextInput style={styles.input} placeholder="Colonia (ej: Zona Centro)" <TextInput style={styles.input} placeholder="Código postal (ej: 38000)"
value={colonia} onChangeText={setColonia} /> value={codigoPostal} onChangeText={setCodigoPostal} keyboardType="numeric" />
<Text style={styles.hint}>Colonias: Zona Centro, Las Arboledas, Trojes, San Juanico, Los Olivos, Rancho Seco, Las Insurgentes</Text>
<TouchableOpacity style={[styles.input, styles.combobox]}
onPress={() => setMostrarColonias(!mostrarColonias)}>
<Text style={{ color: coloniaSeleccionada ? '#1a1a1a' : '#999', fontSize: 15 }}>
{coloniaSeleccionada || 'Selecciona tu colonia ▾'}
</Text>
</TouchableOpacity>
{mostrarColonias && (
<View style={styles.dropdown}>
{COLONIAS.map(c => (
<TouchableOpacity key={c} style={styles.dropdownItem}
onPress={() => { setColoniaSeleccionada(c); setMostrarColonias(false); }}>
<Text style={styles.dropdownText}>{c}</Text>
</TouchableOpacity>
))}
</View>
)}
{domicilios.length > 0 && (
<View style={styles.domiciliosExistentes}>
<Text style={styles.domiciliosTitle}>Tus domicilios registrados:</Text>
{domicilios.map(d => (
<TouchableOpacity key={d.id} style={[styles.domicilioChip,
domicilioActivo?.id === d.id && styles.domicilioChipActivo]}
onPress={() => { seleccionarDomicilio(d); setScreen('eta'); }}>
<Text style={styles.domicilioChipText}>📍 {d.direccion} {d.colonia}</Text>
</TouchableOpacity>
))}
</View>
)}
{loading ? <ActivityIndicator color="#1a7a4a" size="large" /> : {loading ? <ActivityIndicator color="#1a7a4a" size="large" /> :
<TouchableOpacity style={styles.btn} onPress={guardarDomicilio}> <TouchableOpacity style={styles.btn} onPress={guardarDomicilio}>
<Text style={styles.btnText}>Guardar y ver horario</Text> <Text style={styles.btnText}>Guardar domicilio</Text>
</TouchableOpacity>} </TouchableOpacity>}
{domicilios.length > 0 &&
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('eta')}>
<Text style={styles.btnSecondaryText}> Volver al horario</Text>
</TouchableOpacity>}
<TouchableOpacity onPress={cerrarSesion} style={{ marginTop: 24 }}> <TouchableOpacity onPress={cerrarSesion} style={{ marginTop: 24 }}>
<Text style={styles.logoutText}>Cerrar sesión</Text> <Text style={styles.logoutText}>Cerrar sesión</Text>
</TouchableOpacity> </TouchableOpacity>
@@ -212,8 +296,25 @@ export default function App() {
); );
if (screen === 'eta') return ( if (screen === 'eta') return (
<ScrollView contentContainerStyle={styles.container}> <ScrollView contentContainerStyle={styles.container}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#1a7a4a']} />}>
<Text style={styles.title}>🕐 Horario de recolección</Text> <Text style={styles.title}>🕐 Horario de recolección</Text>
{domicilios.length > 1 && (
<ScrollView horizontal showsHorizontalScrollIndicator={false}
style={{ width: '100%', marginBottom: 12 }}>
{domicilios.map(d => (
<TouchableOpacity key={d.id}
style={[styles.domicilioTab, domicilioActivo?.id === d.id && styles.domicilioTabActivo]}
onPress={() => seleccionarDomicilio(d)}>
<Text style={[styles.domicilioTabText, domicilioActivo?.id === d.id && styles.domicilioTabTextActivo]}>
📍 {d.colonia}
</Text>
</TouchableOpacity>
))}
</ScrollView>
)}
{loading ? <ActivityIndicator size="large" color="#1a7a4a" /> : eta ? <> {loading ? <ActivityIndicator size="large" color="#1a7a4a" /> : eta ? <>
<View style={styles.etaCard}> <View style={styles.etaCard}>
<Text style={styles.etaEvento}> <Text style={styles.etaEvento}>
@@ -231,9 +332,12 @@ export default function App() {
<View style={styles.privacyBox}> <View style={styles.privacyBox}>
<Text style={styles.privacyText}>🔒 Solo ves la información de tu zona. No se muestra la ruta completa del camión.</Text> <Text style={styles.privacyText}>🔒 Solo ves la información de tu zona. No se muestra la ruta completa del camión.</Text>
</View> </View>
<TouchableOpacity style={styles.btn} onPress={() => consultarETA(domicilioId, token)}> <TouchableOpacity style={styles.btn} onPress={() => consultarETA(domicilioActivo.id, token)}>
<Text style={styles.btnText}>Actualizar</Text> <Text style={styles.btnText}>Actualizar</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('domicilio')}>
<Text style={styles.btnSecondaryText}> Agregar otro domicilio</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('separacion')}> <TouchableOpacity style={styles.btnSecondary} onPress={() => setScreen('separacion')}>
<Text style={styles.btnSecondaryText}>📚 Guía de separación</Text> <Text style={styles.btnSecondaryText}>📚 Guía de separación</Text>
</TouchableOpacity> </TouchableOpacity>
@@ -277,9 +381,8 @@ export default function App() {
{['El camión no pasó', 'Pasó fuera de horario', 'No recogió mis residuos', 'Otro'].map(tipo => ( {['El camión no pasó', 'Pasó fuera de horario', 'No recogió mis residuos', 'Otro'].map(tipo => (
<TouchableOpacity key={tipo} style={styles.btnSecondary} <TouchableOpacity key={tipo} style={styles.btnSecondary}
onPress={async () => { onPress={async () => {
await fetch(`${API_URL}/reportes?domicilio_id=${domicilioId}&tipo=${tipo}&descripcion=${tipo}`, { await fetch(`${API_URL}/reportes?domicilio_id=${domicilioActivo?.id}&tipo=${tipo}&descripcion=${tipo}`, {
method: 'POST', method: 'POST', headers: { 'Authorization': `Bearer ${token}` }
headers: { 'Authorization': `Bearer ${token}` }
}); });
Alert.alert('¡Gracias!', 'Tu reporte fue enviado correctamente.'); Alert.alert('¡Gracias!', 'Tu reporte fue enviado correctamente.');
setScreen('eta'); setScreen('eta');
@@ -300,15 +403,33 @@ const styles = StyleSheet.create({
splashTitle: { fontSize: 36, fontWeight: 'bold', color: '#fff', marginTop: 16 }, splashTitle: { fontSize: 36, fontWeight: 'bold', color: '#fff', marginTop: 16 },
container: { flexGrow: 1, backgroundColor: '#f0f4f8', alignItems: 'center', justifyContent: 'center', padding: 24 }, container: { flexGrow: 1, backgroundColor: '#f0f4f8', alignItems: 'center', justifyContent: 'center', padding: 24 },
bigEmoji: { fontSize: 64, marginBottom: 8 }, bigEmoji: { fontSize: 64, marginBottom: 8 },
title: { fontSize: 28, fontWeight: 'bold', color: '#1a7a4a', marginBottom: 6, textAlign: 'center' }, title: { fontSize: 26, fontWeight: 'bold', color: '#1a7a4a', marginBottom: 6, textAlign: 'center' },
subtitle: { fontSize: 15, color: '#555', marginBottom: 24, 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' }, 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 }, btn: { width: '100%', backgroundColor: '#1a7a4a', borderRadius: 10, padding: 16, alignItems: 'center', marginTop: 8 },
btnText: { color: '#fff', fontWeight: 'bold', fontSize: 16 }, 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 }, 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' }, 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' }, etaCard: { width: '100%', backgroundColor: '#fff', borderRadius: 16, padding: 20, marginBottom: 16, borderWidth: 1, borderColor: '#d0e8d8' },
etaEvento: { fontSize: 18, fontWeight: 'bold', color: '#1a7a4a', marginBottom: 8 }, etaEvento: { fontSize: 18, fontWeight: 'bold', color: '#1a7a4a', marginBottom: 8 },
etaMensaje: { fontSize: 15, color: '#333', marginBottom: 16, lineHeight: 22 }, etaMensaje: { fontSize: 15, color: '#333', marginBottom: 16, lineHeight: 22 },