feat: auto-geolocation improves, fix route stops query and map soft-reset
This commit is contained in:
@ -35,11 +35,22 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="search-title">{{ t('map.availableRoutes') }}</div>
|
<div class="search-title">{{ t('map.availableRoutes') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Search Input -->
|
||||||
|
<div class="search-input-wrapper">
|
||||||
|
<span class="material-icons search-field-icon">search</span>
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('map.search')"
|
||||||
|
class="route-search-input"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
<div class="uber-results custom-scrollbar">
|
<div class="uber-results custom-scrollbar">
|
||||||
<div
|
<div
|
||||||
v-for="route in allRoutes"
|
v-for="route in filteredRoutes"
|
||||||
:key="route.id"
|
:key="route.id"
|
||||||
class="uber-result-item"
|
class="uber-result-item"
|
||||||
:class="{ 'selected-route': route.id === selectedRouteId && wasSelectedFromMap }"
|
:class="{ 'selected-route': route.id === selectedRouteId && wasSelectedFromMap }"
|
||||||
@ -63,6 +74,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -77,6 +89,17 @@ const props = defineProps<{
|
|||||||
defineEmits(['open', 'close', 'select-route'])
|
defineEmits(['open', 'close', 'select-route'])
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
const filteredRoutes = computed(() => {
|
||||||
|
if (!searchQuery.value.trim()) return props.allRoutes
|
||||||
|
const query = searchQuery.value.toLowerCase().trim()
|
||||||
|
return props.allRoutes.filter(r =>
|
||||||
|
r.name.toLowerCase().includes(query) ||
|
||||||
|
(r.origin_city && r.origin_city.toLowerCase().includes(query)) ||
|
||||||
|
(r.destination_city && r.destination_city.toLowerCase().includes(query))
|
||||||
|
)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -175,6 +198,39 @@ const { t } = useI18n()
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-field-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 12px 12px 40px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--active-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(254, 231, 21, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.uber-results {
|
.uber-results {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@ -7,11 +7,11 @@ export const routesService = {
|
|||||||
async getAllRoutes(filters?: { originCity?: string, destinationCity?: string }): Promise<Route[]> {
|
async getAllRoutes(filters?: { originCity?: string, destinationCity?: string }): Promise<Route[]> {
|
||||||
let query = supabase.from('routes').select('id, name, description, color, direction, origin_city, destination_city, distance_km, estimated_duration_minutes, average_speed_kmh, status, created_at, updated_at')
|
let query = supabase.from('routes').select('id, name, description, color, direction, origin_city, destination_city, distance_km, estimated_duration_minutes, average_speed_kmh, status, created_at, updated_at')
|
||||||
|
|
||||||
if (filters?.originCity) {
|
if (filters?.originCity?.trim()) {
|
||||||
query = query.eq('origin_city', filters.originCity)
|
query = query.ilike('origin_city', `%${filters.originCity.trim()}%`)
|
||||||
}
|
}
|
||||||
if (filters?.destinationCity) {
|
if (filters?.destinationCity?.trim()) {
|
||||||
query = query.eq('destination_city', filters.destinationCity)
|
query = query.ilike('destination_city', `%${filters.destinationCity.trim()}%`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await query
|
const { data, error } = await query
|
||||||
@ -37,7 +37,7 @@ export const routesService = {
|
|||||||
stop_delay_minutes,
|
stop_delay_minutes,
|
||||||
is_pickup_point,
|
is_pickup_point,
|
||||||
is_dropoff_point,
|
is_dropoff_point,
|
||||||
bus_stops (*)
|
bus_stops (id, name, latitude, longitude, city, address, stop_type, has_shelter, has_seating, is_accessible, created_at, updated_at)
|
||||||
`)
|
`)
|
||||||
.eq('route_id', routeId)
|
.eq('route_id', routeId)
|
||||||
.order('stop_order', { ascending: true })
|
.order('stop_order', { ascending: true })
|
||||||
|
|||||||
@ -59,9 +59,32 @@ const carouselTimer = ref<any>(null);
|
|||||||
const isMapMoved = ref(false);
|
const isMapMoved = ref(false);
|
||||||
|
|
||||||
// Search optimization: Simple debounce implementation
|
// Search optimization: Simple debounce implementation
|
||||||
// Helper functions
|
// REQUISITO TÉCNICO: Implementar geolocalización automática al iniciar sesión.
|
||||||
|
function calculateDistance(point1: { lat: number; lng: number }, point2: { lat: number; lng: number }) {
|
||||||
|
const R = 6371; // Radio de la Tierra en km
|
||||||
|
const dLat = (point2.lat - point1.lat) * Math.PI / 180;
|
||||||
|
const dLng = (point2.lng - point1.lng) * Math.PI / 180;
|
||||||
|
const a =
|
||||||
|
Math.sin(dLat/2) * Math.sin(dLat/2) +
|
||||||
|
Math.cos(point1.lat * Math.PI / 180) * Math.cos(point2.lat * Math.PI / 180) *
|
||||||
|
Math.sin(dLng/2) * Math.sin(dLng/2);
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||||
|
return R * c;
|
||||||
|
}
|
||||||
|
|
||||||
// performSearch removed
|
function updateIsMapMoved() {
|
||||||
|
if (!map.value || !userCoords.value) return;
|
||||||
|
const center = map.value.getCenter();
|
||||||
|
if (!center) return;
|
||||||
|
|
||||||
|
const dist = calculateDistance(
|
||||||
|
{ lat: center.lat(), lng: center.lng() },
|
||||||
|
userCoords.value
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si se movió más de 0.1 km (100 metros), mostrar botón
|
||||||
|
isMapMoved.value = dist > 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
function openUberSearch() {
|
function openUberSearch() {
|
||||||
showPromos.value = false;
|
showPromos.value = false;
|
||||||
@ -72,11 +95,24 @@ function closeUberSearch() {
|
|||||||
showUberSearch.value = false;
|
showUberSearch.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function animateAndReload() {
|
async function animateAndReload() {
|
||||||
isBannerClosing.value = true;
|
isBannerClosing.value = true;
|
||||||
routeStore.clearSelection();
|
routeStore.clearSelection();
|
||||||
// Solución anterior: Recargar para mapa limpio
|
router.replace({ query: {} });
|
||||||
window.location.href = window.location.origin + window.location.pathname;
|
|
||||||
|
// Limpiar mapa sin recargar
|
||||||
|
clearMapMarkers();
|
||||||
|
|
||||||
|
// Recentrar en el usuario si está disponible (soft-reset)
|
||||||
|
if (userCoords.value) {
|
||||||
|
setCenter(userCoords.value.lat, userCoords.value.lng);
|
||||||
|
setZoom(16);
|
||||||
|
reDrawUserMarker();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isBannerClosing.value = false;
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePromoClick(promo: any) {
|
function handlePromoClick(promo: any) {
|
||||||
@ -143,7 +179,10 @@ async function initializeMap() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Detect user interaction with the map to show/hide location button
|
// Detect user interaction with the map to show/hide location button
|
||||||
|
map.value.addListener('center_changed', updateIsMapMoved);
|
||||||
map.value.addListener('dragstart', () => {
|
map.value.addListener('dragstart', () => {
|
||||||
|
// Forzar visibilidad inmediata en drag si se desea un feedback instantáneo,
|
||||||
|
// pero el watcher de distancia es el que manda finalmente.
|
||||||
isMapMoved.value = true;
|
isMapMoved.value = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -281,18 +320,30 @@ async function updateActiveUnits() {
|
|||||||
|
|
||||||
function locateUser(): Promise<void> {
|
function locateUser(): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!navigator.geolocation) { return resolve(); }
|
if (!navigator.geolocation) {
|
||||||
|
console.warn('Geolocation no soportado');
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
(position) => {
|
(position) => {
|
||||||
const { latitude, longitude } = position.coords;
|
const { latitude, longitude } = position.coords;
|
||||||
userCoords.value = { lat: latitude, lng: longitude };
|
userCoords.value = { lat: latitude, lng: longitude };
|
||||||
|
|
||||||
|
// Centrar y mostrar
|
||||||
|
if (map.value) {
|
||||||
setCenter(latitude, longitude);
|
setCenter(latitude, longitude);
|
||||||
setZoom(16);
|
setZoom(16);
|
||||||
|
}
|
||||||
|
|
||||||
reDrawUserMarker();
|
reDrawUserMarker();
|
||||||
isMapMoved.value = false; // Reset interaction state
|
isMapMoved.value = false;
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
() => {
|
(error) => {
|
||||||
|
console.error('SIBU | Error obteniendo ubicación:', error);
|
||||||
|
// Si falló por falta de permisos o error y el usuario tenía auto_location activo,
|
||||||
|
// lo desactivamos para no re-intentar infinitamente
|
||||||
if (authStore.userProfile?.auto_location) {
|
if (authStore.userProfile?.auto_location) {
|
||||||
authStore.updateProfile({ auto_location: false });
|
authStore.updateProfile({ auto_location: false });
|
||||||
}
|
}
|
||||||
@ -374,9 +425,10 @@ function handleImageError(event: Event) {
|
|||||||
(event.target as HTMLImageElement).src = getImageUrl(null, 'coupon');
|
(event.target as HTMLImageElement).src = getImageUrl(null, 'coupon');
|
||||||
}
|
}
|
||||||
|
|
||||||
// AUTO-LOCATION: Watch for user profile to trigger location if preference is enabled
|
// Watch for user profile to trigger location if preference is enabled OR on auth changes
|
||||||
watch(() => authStore.userProfile?.auto_location, (canLocate) => {
|
watch([() => authStore.userProfile?.auto_location, isLoaded], ([canLocate, loaded]) => {
|
||||||
if (canLocate && isLoaded.value && !userCoords.value) {
|
if (canLocate && loaded && !userCoords.value) {
|
||||||
|
console.log('SIBU | Iniciando geolocalización automática...');
|
||||||
locateUser();
|
locateUser();
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user