Fix UI overlapping, transport load error handling, and schedule filtering bugs

This commit is contained in:
2026-02-27 20:22:29 -05:00
parent 7c800a0551
commit a2d317d1bc
5 changed files with 171 additions and 21 deletions

View File

@ -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)) {

View File

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

View File

@ -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>

View File

@ -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;

View File

@ -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>