Fix UI overlapping, transport load error handling, and schedule filtering bugs
This commit is contained in:
@ -16,7 +16,9 @@ export function useGoogleMaps() {
|
|||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const {
|
const {
|
||||||
registrarMarker,
|
registrarMarker,
|
||||||
|
registrarRenderer,
|
||||||
registrarPolyline,
|
registrarPolyline,
|
||||||
|
registrarCallbackLimpieza,
|
||||||
limpiarMapa: limpiarTodoCentralizado
|
limpiarMapa: limpiarTodoCentralizado
|
||||||
} = useMapState()
|
} = useMapState()
|
||||||
|
|
||||||
@ -121,6 +123,13 @@ export function useGoogleMaps() {
|
|||||||
if (map.value && !globalOverlays.has(map.value)) {
|
if (map.value && !globalOverlays.has(map.value)) {
|
||||||
globalOverlays.set(map.value, new Set())
|
globalOverlays.set(map.value, new Set())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Registrar callback para limpiar globalOverlays cuando useMapState.limpiarMapa() sea llamado
|
||||||
|
registrarCallbackLimpieza(() => {
|
||||||
|
if (map.value && globalOverlays.has(map.value)) {
|
||||||
|
clearAllOverlaysForMap(map.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function addMarker(
|
function addMarker(
|
||||||
@ -420,6 +429,12 @@ export function useGoogleMaps() {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Limpiar antes de dibujar una nueva ruta para evitar acumulación
|
||||||
|
limpiarTodoCentralizado()
|
||||||
|
if (map.value && globalOverlays.has(map.value)) {
|
||||||
|
clearAllOverlaysForMap(map.value)
|
||||||
|
}
|
||||||
|
|
||||||
const directionsService = new google.maps.DirectionsService();
|
const directionsService = new google.maps.DirectionsService();
|
||||||
const renderizadoresActivos: google.maps.DirectionsRenderer[] = [];
|
const renderizadoresActivos: google.maps.DirectionsRenderer[] = [];
|
||||||
const tamañoChunk = 25;
|
const tamañoChunk = 25;
|
||||||
@ -453,14 +468,15 @@ export function useGoogleMaps() {
|
|||||||
suppressMarkers: true,
|
suppressMarkers: true,
|
||||||
preserveViewport: true, // Siempre conservar la vista ya que trazamos fragmentos
|
preserveViewport: true, // Siempre conservar la vista ya que trazamos fragmentos
|
||||||
polylineOptions: {
|
polylineOptions: {
|
||||||
strokeColor: '#0057FF', // Azul
|
strokeColor: '#FBBF24', // Amarillo consistente con paradas
|
||||||
strokeWeight: 4,
|
strokeWeight: 5,
|
||||||
strokeOpacity: 0.8
|
strokeOpacity: 0.95
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
renderer.setDirections(response);
|
renderer.setDirections(response);
|
||||||
renderizadoresActivos.push(renderer);
|
renderizadoresActivos.push(renderer);
|
||||||
|
registrarRenderer(renderer); // Registrar para limpieza centralizada
|
||||||
|
|
||||||
// Registrar en global overlays para limpiarlos después
|
// Registrar en global overlays para limpiarlos después
|
||||||
if (!globalOverlays.has(map.value)) {
|
if (!globalOverlays.has(map.value)) {
|
||||||
|
|||||||
@ -39,6 +39,13 @@ export const useMapState = () => {
|
|||||||
return infoWindow
|
return infoWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Callback para sincronización externa (ej. useGoogleMaps globalOverlays)
|
||||||
|
const onLimpiarCallback = ref<(() => void) | null>(null)
|
||||||
|
|
||||||
|
const registrarCallbackLimpieza = (fn: () => void) => {
|
||||||
|
onLimpiarCallback.value = fn
|
||||||
|
}
|
||||||
|
|
||||||
// ⚠️ FUNCIÓN CRÍTICA: limpiar ABSOLUTAMENTE TODO del mapa
|
// ⚠️ FUNCIÓN CRÍTICA: limpiar ABSOLUTAMENTE TODO del mapa
|
||||||
const limpiarMapa = () => {
|
const limpiarMapa = () => {
|
||||||
// Eliminar markers
|
// Eliminar markers
|
||||||
@ -97,6 +104,13 @@ export const useMapState = () => {
|
|||||||
})
|
})
|
||||||
circles.value = []
|
circles.value = []
|
||||||
|
|
||||||
|
// Ejecutar callback de limpieza externa si existe
|
||||||
|
if (onLimpiarCallback.value) {
|
||||||
|
try { onLimpiarCallback.value() } catch (e) {
|
||||||
|
console.warn('Error en callback de limpieza externa', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('SIBU | Mapa limpiado completamente ✓')
|
console.log('SIBU | Mapa limpiado completamente ✓')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +125,7 @@ export const useMapState = () => {
|
|||||||
registrarPolyline,
|
registrarPolyline,
|
||||||
registrarCircle,
|
registrarCircle,
|
||||||
registrarInfoWindow,
|
registrarInfoWindow,
|
||||||
|
registrarCallbackLimpieza,
|
||||||
limpiarMapa
|
limpiarMapa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -807,24 +807,22 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Banner de Parada Más Cercana Inteligente -->
|
<!-- Banner de Parada Más Cercana Inteligente (Chip Compacto) -->
|
||||||
<div
|
<div
|
||||||
v-if="paradaCercana && routeStore.selectedRouteId && !showETACard"
|
v-if="paradaCercana && routeStore.selectedRouteId && !showETACard"
|
||||||
class="fixed left-0 right-0 z-40 px-3 transition-transform duration-300 pointer-events-none"
|
class="fixed z-[1050] px-3 transition-all duration-300 pointer-events-none"
|
||||||
:style="{ top: alturaNavbar + 'px' }"
|
:style="{ top: (alturaNavbar + 8) + 'px', right: '16px' }"
|
||||||
>
|
>
|
||||||
<!-- Solo mostrar cuando ETACard está CERRADO -->
|
<div class="bg-white/95 dark:bg-gray-900/95 backdrop-blur-sm rounded-2xl shadow-lg border border-yellow-400/50 px-3 py-1.5 flex items-center gap-2 max-w-[220px] pointer-events-auto">
|
||||||
<!-- v-if agrega condición: && !showETACard -->
|
<span class="material-icons text-yellow-500" style="font-size:16px">directions_bus</span>
|
||||||
<div class="bg-white/90 dark:bg-gray-900/90 backdrop-blur-sm rounded-b-2xl shadow-lg border-t-2 border-yellow-400 px-4 py-2 flex items-center gap-2 pointer-events-auto">
|
<span class="text-xs font-bold text-gray-800 dark:text-white truncate flex-1">
|
||||||
<span class="material-icons text-yellow-500 text-sm">directions_bus</span>
|
|
||||||
<span class="text-sm font-bold text-gray-800 dark:text-white truncate flex-1">
|
|
||||||
{{ paradaCercana?.name }}
|
{{ paradaCercana?.name }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs text-gray-500 whitespace-nowrap">
|
<span class="text-[10px] text-gray-500 whitespace-nowrap">
|
||||||
{{ (distanciaMetros && distanciaMetros < 1000) ? Math.round(distanciaMetros) + 'm' : (distanciaMetros ? (distanciaMetros / 1000).toFixed(1) + 'km' : '') }}
|
{{ (distanciaMetros && distanciaMetros < 1000) ? Math.round(distanciaMetros) + 'm' : (distanciaMetros ? (distanciaMetros / 1000).toFixed(1) + 'km' : '') }}
|
||||||
</span>
|
</span>
|
||||||
<button @click="paradaCercana = null" class="text-gray-400 hover:text-gray-600 shrink-0 p-0.5 ml-1">
|
<button @click="paradaCercana = null" class="text-gray-400 hover:text-gray-600 shrink-0 p-0.5 ml-1">
|
||||||
<span class="material-icons text-sm">close</span>
|
<span class="material-icons" style="font-size:14px">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1252,7 +1250,8 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
|
|||||||
═══════════════════════════════════════ */
|
═══════════════════════════════════════ */
|
||||||
.offers-sheet {
|
.offers-sheet {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 110px; /* Separado más de la barra inferior para evitar solapamiento */
|
/* Base 72px (altura menú) + 16px espacio visual + safe area */
|
||||||
|
bottom: calc(72px + 16px + env(safe-area-inset-bottom, 0px));
|
||||||
left: 10px;
|
left: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@ -1262,6 +1261,9 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
|
|||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
box-shadow: 0 -4px 15px rgba(0,0,0,0.2);
|
box-shadow: 0 -4px 15px rgba(0,0,0,0.2);
|
||||||
color: #000;
|
color: #000;
|
||||||
|
/* Limitar altura máxima para no ocupar toda la pantalla */
|
||||||
|
max-height: calc(100vh - 200px);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@ -1272,6 +1274,15 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.offers-sheet {
|
||||||
|
/* En móvil más espacio aún por el menú nativo */
|
||||||
|
bottom: calc(80px + env(safe-area-inset-bottom, 0px));
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sheet-handle {
|
.sheet-handle {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
@ -2137,4 +2148,14 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
|
|||||||
left: 15px;
|
left: 15px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
.map-floating-controls {
|
||||||
|
position: fixed;
|
||||||
|
/* Subir los botones FAB cuando el carrusel está abierto */
|
||||||
|
bottom: 85px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -22,14 +22,19 @@ const DAY_TYPES: Record<string, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Calcular estado del bus según horario
|
// ── Calcular estado del bus según horario
|
||||||
function getBusStatus(timeStr: string): 'departing' | 'ontime' | 'upcoming' {
|
function getBusStatus(timeStr: string): 'departing' | 'ontime' | 'upcoming' | 'passed' {
|
||||||
if (!timeStr) return 'upcoming'
|
if (!timeStr) return 'upcoming'
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const [h, m] = timeStr.split(':').map(Number)
|
const [h, m] = timeStr.split(':').map(Number)
|
||||||
const schedDate = new Date()
|
const schedDate = new Date()
|
||||||
schedDate.setHours(h || 0, m || 0, 0, 0)
|
schedDate.setHours(h || 0, m || 0, 0, 0)
|
||||||
|
|
||||||
const diffMin = (schedDate.getTime() - now.getTime()) / 60000
|
const diffMin = (schedDate.getTime() - now.getTime()) / 60000
|
||||||
if (diffMin >= 0 && diffMin <= 10) return 'departing'
|
|
||||||
|
// Si el bus ya pasó (más de 2 minutos de margen de gracia)
|
||||||
|
if (diffMin < -2) return 'passed'
|
||||||
|
|
||||||
|
if (diffMin >= -2 && diffMin <= 10) return 'departing'
|
||||||
if (diffMin > 10 && diffMin <= 60) return 'ontime'
|
if (diffMin > 10 && diffMin <= 60) return 'ontime'
|
||||||
return 'upcoming'
|
return 'upcoming'
|
||||||
}
|
}
|
||||||
@ -56,10 +61,20 @@ function getDayLabel(schedule: any): string {
|
|||||||
// ── Filtrado de horarios
|
// ── Filtrado de horarios
|
||||||
const filteredSchedules = computed(() => {
|
const filteredSchedules = computed(() => {
|
||||||
return scheduleStore.schedules.filter(s => {
|
return scheduleStore.schedules.filter(s => {
|
||||||
if (dayFilter.value === 'all') return true
|
|
||||||
const d = getScheduleDay(s)
|
const d = getScheduleDay(s)
|
||||||
if (dayFilter.value === 'today') return d === 'today'
|
const status = getBusStatus(s.departure_time)
|
||||||
if (dayFilter.value === 'tomorrow') return d === 'tomorrow'
|
|
||||||
|
// Filtro Hoy: Solo buses de hoy que NO han pasado
|
||||||
|
if (dayFilter.value === 'today') {
|
||||||
|
return d === 'today' && status !== 'passed'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro Mañana: Solo buses de mañana
|
||||||
|
if (dayFilter.value === 'tomorrow') {
|
||||||
|
return d === 'tomorrow'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro Todos: Mostrar todo
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -269,7 +284,8 @@ onUnmounted(() => {
|
|||||||
<div class="status-badge" :class="`status-badge--${getBusStatus(schedule.departure_time)}`">
|
<div class="status-badge" :class="`status-badge--${getBusStatus(schedule.departure_time)}`">
|
||||||
<span class="material-icons status-icon">
|
<span class="material-icons status-icon">
|
||||||
{{ getBusStatus(schedule.departure_time) === 'departing' ? 'directions_run' :
|
{{ getBusStatus(schedule.departure_time) === 'departing' ? 'directions_run' :
|
||||||
getBusStatus(schedule.departure_time) === 'ontime' ? 'check_circle' : 'access_time' }}
|
getBusStatus(schedule.departure_time) === 'ontime' ? 'check_circle' :
|
||||||
|
getBusStatus(schedule.departure_time) === 'passed' ? 'history' : 'access_time' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -679,6 +695,28 @@ onUnmounted(() => {
|
|||||||
50% { opacity: 0.6; }
|
50% { opacity: 0.6; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Estado Pasado (Faded) */
|
||||||
|
.schedule-card--passed {
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(0.8);
|
||||||
|
border-left-color: #6b7280;
|
||||||
|
border-left-width: 3px;
|
||||||
|
transform: none !important; /* No hover effect for passed */
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-card--passed .card-accent {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-card--passed .time-big {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--passed {
|
||||||
|
background: rgba(107, 114, 128, 0.15);
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
.route-name {
|
.route-name {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
|
|||||||
@ -1,9 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const mountError = ref(false)
|
||||||
|
|
||||||
|
const reloadPage = () => {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// Aquí iría cualquier inicialización global del layout si fuera necesaria
|
||||||
|
console.log('Transporte Hub mounted')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error mounting Transporte Hub:', e)
|
||||||
|
mountError.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -25,7 +41,16 @@ const route = useRoute()
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<router-view v-slot="{ Component }">
|
<div v-if="mountError" class="error-container">
|
||||||
|
<span class="material-icons">error_outline</span>
|
||||||
|
<p>Error al cargar la sección de transporte</p>
|
||||||
|
<button @click="reloadPage" class="retry-btn">
|
||||||
|
<span class="material-icons">refresh</span>
|
||||||
|
Reintentar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<router-view v-else v-slot="{ Component }">
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
@ -118,4 +143,39 @@ const route = useRoute()
|
|||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container .material-icons {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #ef4444;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
background: var(--active-color);
|
||||||
|
color: #101820;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user