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 {
|
||||
registrarMarker,
|
||||
registrarRenderer,
|
||||
registrarPolyline,
|
||||
registrarCallbackLimpieza,
|
||||
limpiarMapa: limpiarTodoCentralizado
|
||||
} = useMapState()
|
||||
|
||||
@ -121,6 +123,13 @@ export function useGoogleMaps() {
|
||||
if (map.value && !globalOverlays.has(map.value)) {
|
||||
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(
|
||||
@ -420,6 +429,12 @@ export function useGoogleMaps() {
|
||||
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 renderizadoresActivos: google.maps.DirectionsRenderer[] = [];
|
||||
const tamañoChunk = 25;
|
||||
@ -453,14 +468,15 @@ export function useGoogleMaps() {
|
||||
suppressMarkers: true,
|
||||
preserveViewport: true, // Siempre conservar la vista ya que trazamos fragmentos
|
||||
polylineOptions: {
|
||||
strokeColor: '#0057FF', // Azul
|
||||
strokeWeight: 4,
|
||||
strokeOpacity: 0.8
|
||||
strokeColor: '#FBBF24', // Amarillo consistente con paradas
|
||||
strokeWeight: 5,
|
||||
strokeOpacity: 0.95
|
||||
}
|
||||
});
|
||||
|
||||
renderer.setDirections(response);
|
||||
renderizadoresActivos.push(renderer);
|
||||
registrarRenderer(renderer); // Registrar para limpieza centralizada
|
||||
|
||||
// Registrar en global overlays para limpiarlos después
|
||||
if (!globalOverlays.has(map.value)) {
|
||||
|
||||
@ -39,6 +39,13 @@ export const useMapState = () => {
|
||||
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
|
||||
const limpiarMapa = () => {
|
||||
// Eliminar markers
|
||||
@ -97,6 +104,13 @@ export const useMapState = () => {
|
||||
})
|
||||
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 ✓')
|
||||
}
|
||||
|
||||
@ -111,6 +125,7 @@ export const useMapState = () => {
|
||||
registrarPolyline,
|
||||
registrarCircle,
|
||||
registrarInfoWindow,
|
||||
registrarCallbackLimpieza,
|
||||
limpiarMapa
|
||||
}
|
||||
}
|
||||
|
||||
@ -807,24 +807,22 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Banner de Parada Más Cercana Inteligente -->
|
||||
<!-- Banner de Parada Más Cercana Inteligente (Chip Compacto) -->
|
||||
<div
|
||||
v-if="paradaCercana && routeStore.selectedRouteId && !showETACard"
|
||||
class="fixed left-0 right-0 z-40 px-3 transition-transform duration-300 pointer-events-none"
|
||||
:style="{ top: alturaNavbar + 'px' }"
|
||||
class="fixed z-[1050] px-3 transition-all duration-300 pointer-events-none"
|
||||
:style="{ top: (alturaNavbar + 8) + 'px', right: '16px' }"
|
||||
>
|
||||
<!-- Solo mostrar cuando ETACard está CERRADO -->
|
||||
<!-- v-if agrega condición: && !showETACard -->
|
||||
<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="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">
|
||||
<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">
|
||||
<span class="material-icons text-yellow-500" style="font-size:16px">directions_bus</span>
|
||||
<span class="text-xs font-bold text-gray-800 dark:text-white truncate flex-1">
|
||||
{{ paradaCercana?.name }}
|
||||
</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' : '') }}
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -1252,7 +1250,8 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
|
||||
═══════════════════════════════════════ */
|
||||
.offers-sheet {
|
||||
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;
|
||||
right: 10px;
|
||||
background: #fff;
|
||||
@ -1262,6 +1261,9 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
|
||||
padding-bottom: 10px;
|
||||
box-shadow: 0 -4px 15px rgba(0,0,0,0.2);
|
||||
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) {
|
||||
@ -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 {
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
@ -2137,4 +2148,14 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
|
||||
left: 15px;
|
||||
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>
|
||||
|
||||
@ -22,14 +22,19 @@ const DAY_TYPES: Record<string, string> = {
|
||||
}
|
||||
|
||||
// ── 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'
|
||||
const now = new Date()
|
||||
const [h, m] = timeStr.split(':').map(Number)
|
||||
const schedDate = new Date()
|
||||
schedDate.setHours(h || 0, m || 0, 0, 0)
|
||||
|
||||
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'
|
||||
return 'upcoming'
|
||||
}
|
||||
@ -56,10 +61,20 @@ function getDayLabel(schedule: any): string {
|
||||
// ── Filtrado de horarios
|
||||
const filteredSchedules = computed(() => {
|
||||
return scheduleStore.schedules.filter(s => {
|
||||
if (dayFilter.value === 'all') return true
|
||||
const d = getScheduleDay(s)
|
||||
if (dayFilter.value === 'today') return d === 'today'
|
||||
if (dayFilter.value === 'tomorrow') return d === 'tomorrow'
|
||||
const status = getBusStatus(s.departure_time)
|
||||
|
||||
// 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
|
||||
})
|
||||
})
|
||||
@ -269,7 +284,8 @@ onUnmounted(() => {
|
||||
<div class="status-badge" :class="`status-badge--${getBusStatus(schedule.departure_time)}`">
|
||||
<span class="material-icons status-icon">
|
||||
{{ 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>
|
||||
</div>
|
||||
</div>
|
||||
@ -679,6 +695,28 @@ onUnmounted(() => {
|
||||
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 {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
|
||||
@ -1,9 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -24,8 +40,17 @@ const route = useRoute()
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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-slot="{ Component }">
|
||||
<router-view v-else v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<keep-alive>
|
||||
<component :is="Component" />
|
||||
@ -118,4 +143,39 @@ const route = useRoute()
|
||||
.fade-leave-to {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user