diff --git a/frontend/src/composables/useGoogleMaps.ts b/frontend/src/composables/useGoogleMaps.ts index 63960ce..8fcde51 100644 --- a/frontend/src/composables/useGoogleMaps.ts +++ b/frontend/src/composables/useGoogleMaps.ts @@ -16,7 +16,9 @@ export function useGoogleMaps() { const error = ref(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)) { diff --git a/frontend/src/composables/useMapState.ts b/frontend/src/composables/useMapState.ts index ac5dcfd..0c4333e 100644 --- a/frontend/src/composables/useMapState.ts +++ b/frontend/src/composables/useMapState.ts @@ -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 } } diff --git a/frontend/src/views/MapView.vue b/frontend/src/views/MapView.vue index f6d484b..7536c20 100644 --- a/frontend/src/views/MapView.vue +++ b/frontend/src/views/MapView.vue @@ -807,24 +807,22 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop: - +
- - -
- directions_bus - +
+ directions_bus + {{ paradaCercana?.name }} - + {{ (distanciaMetros && distanciaMetros < 1000) ? Math.round(distanciaMetros) + 'm' : (distanciaMetros ? (distanciaMetros / 1000).toFixed(1) + 'km' : '') }}
@@ -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; +} diff --git a/frontend/src/views/SchedulesView.vue b/frontend/src/views/SchedulesView.vue index ca68c1a..9aae3f3 100644 --- a/frontend/src/views/SchedulesView.vue +++ b/frontend/src/views/SchedulesView.vue @@ -22,14 +22,19 @@ const DAY_TYPES: Record = { } // ── 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(() => {
{{ 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' }}
@@ -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; diff --git a/frontend/src/views/TransporteLayout.vue b/frontend/src/views/TransporteLayout.vue index 3c06dc4..f15faa7 100644 --- a/frontend/src/views/TransporteLayout.vue +++ b/frontend/src/views/TransporteLayout.vue @@ -1,9 +1,25 @@