Refactor: Map UI improvements, ETA metrics, Schedule fixes, and Transport Detail styling

This commit is contained in:
2026-02-28 10:39:20 -05:00
parent 8d967814f9
commit 621da9e4c3
4 changed files with 163 additions and 59 deletions

View File

@ -98,6 +98,7 @@ function selectStopFromSearch(stop: BusStop) {
} }
function openUberSearch() { function openUberSearch() {
showPromos.value = false; // Cerramos ofertas para evitar solapamiento
showUberSearch.value = true; showUberSearch.value = true;
showRoutesToggle.value = true; // Forzar que al abrir estemos en modo rutas showRoutesToggle.value = true; // Forzar que al abrir estemos en modo rutas
} }
@ -180,6 +181,10 @@ function closeBusStopModal() {
selectedBusStop.value = null; selectedBusStop.value = null;
} }
function reloadPage() {
window.location.reload();
}
function handlePromoClick(promo: any) { function handlePromoClick(promo: any) {
selectedPromo.value = promo; selectedPromo.value = promo;
showPromoModal.value = true; showPromoModal.value = true;
@ -503,7 +508,9 @@ async function updateActiveUnits() {
if (!isLoaded.value) return; if (!isLoaded.value) return;
try { try {
// No-op for now. Backend is purely Supabase now. if (routeStore.selectedRouteId && paradaCercana.value) {
await calcularETA(routeStore.selectedRouteId, paradaCercana.value as BusStop);
}
} catch (e) { } catch (e) {
console.error('Failed to update active units', e); console.error('Failed to update active units', e);
} }
@ -841,21 +848,35 @@ async function calculateWalkingPath(origin: { lat: number, lng: number }, target
<span class="trigger-label">ver rutas</span> <span class="trigger-label">ver rutas</span>
</div> </div>
<!-- Nuevo Banner de Parada Cercana Alineado --> <!-- Nuevo Banner de Parada Cercana Alineado (Redimensionado y con ETA) -->
<div <div
v-if="paradaCercana && routeStore.selectedRouteId && !showETACard" v-if="paradaCercana && routeStore.selectedRouteId && !showETACard"
class="uber-search-trigger best-stop-banner" class="best-stop-banner-compact"
> >
<span class="material-icons text-yellow-500 mr-3">directions_bus</span> <div class="banner-icon-bg">
<div class="flex flex-col flex-1 truncate"> <span class="material-icons text-white text-[16px]">directions_bus</span>
<span class="text-[10px] uppercase font-bold text-yellow-500 leading-tight">Parada cercana</span>
<span class="trigger-text truncate leading-tight">{{ paradaCercana?.name }}</span>
</div> </div>
<div class="text-[11px] font-bold bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded-lg ml-2 whitespace-nowrap">
{{ (distanciaMetros && distanciaMetros < 1000) ? Math.round(distanciaMetros) + 'm' : (distanciaMetros ? (distanciaMetros / 1000).toFixed(1) + 'km' : '') }} <div class="flex flex-col flex-1 truncate ml-2">
<span class="text-[9px] uppercase font-bold text-gray-500 dark:text-gray-400 leading-none">Tiempo de llegada</span>
<span class="trigger-text-compact truncate leading-tight">{{ paradaCercana?.name }}</span>
</div> </div>
<button @click.stop="clearAllMapData" class="ml-3 p-1 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
<span class="material-icons text-[20px] text-gray-400 hover:text-red-500">close</span> <div class="eta-badge">
<template v-if="etaCargando">
<div class="eta-loader"></div>
</template>
<template v-else-if="busesActivos.length > 0">
<span class="eta-value">{{ busesActivos[0].etaMinutos > 0 ? busesActivos[0].etaMinutos : '0' }}</span>
<span class="eta-unit">min</span>
</template>
<template v-else>
<span class="eta-unit">-- min</span>
</template>
</div>
<button @click.stop="reloadPage" class="ml-2 p-1 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
<span class="material-icons text-[18px] text-gray-400 hover:text-red-500">close</span>
</button> </button>
</div> </div>
@ -1531,6 +1552,76 @@ html.light-theme .uber-search-trigger-compact {
max-width: none; max-width: none;
} }
.best-stop-banner-compact {
flex: 1;
background: var(--header-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
height: 40px; /* Más compacto (de 44px a 40px) */
border-radius: 10px;
display: flex;
align-items: center;
padding: 0 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
border: 1px solid var(--border-color);
max-width: none;
pointer-events: auto;
}
.banner-icon-bg {
background: #EAB308; /* yellow-500 */
width: 24px;
height: 24px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.trigger-text-compact {
color: var(--text-primary);
font-size: 0.95rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.eta-badge {
background: rgba(234, 179, 8, 0.1); /* yellow-500 with opacity */
color: #EAB308;
padding: 2px 8px;
border-radius: 6px;
display: flex;
align-items: baseline;
gap: 2px;
font-weight: 800;
margin-left: 8px;
border: 1px solid rgba(234, 179, 8, 0.2);
}
.eta-value {
font-size: 1.1rem;
line-height: 1;
}
.eta-unit {
font-size: 0.7rem;
text-transform: uppercase;
}
.eta-loader {
width: 14px;
height: 14px;
border: 2px solid #EAB308;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.uber-search-panel { .uber-search-panel {
position: fixed; position: fixed;
top: 70px; /* Debajo del header superior */ top: 70px; /* Debajo del header superior */

View File

@ -56,7 +56,7 @@ function getScheduleDay(schedule: any): 'today' | 'tomorrow' | 'other' {
// Comparar con el tipo del horario // Comparar con el tipo del horario
// Nota: Si el horario es 'todos', cuenta para hoy y mañana (pero priorizamos hoy si pides hoy) // Nota: Si el horario es 'todos', cuenta para hoy y mañana (pero priorizamos hoy si pides hoy)
const type = schedule.schedule_type || 'todos' const type = (schedule.schedule_type as string) || 'todos'
const isToday = type === todayType || type === 'todos' const isToday = type === todayType || type === 'todos'
const isTomorrow = type === tomorrowType || type === 'todos' const isTomorrow = type === tomorrowType || type === 'todos'
@ -81,27 +81,35 @@ function getDayLabel(schedule: any): string {
// ── Filtrado de horarios // ── Filtrado de horarios
const filteredSchedules = computed(() => { const filteredSchedules = computed(() => {
const now = new Date() const now = new Date()
const tomorrow = new Date(now)
tomorrow.setDate(now.getDate() + 1)
const hhmmAhora = now.getHours() * 100 + now.getMinutes() const hhmmAhora = now.getHours() * 100 + now.getMinutes()
return scheduleStore.schedules.filter(s => { return scheduleStore.schedules.filter(s => {
const d = getScheduleDay(s) const type = (s.schedule_type as string) || 'todos'
const todayType = (now.getDay() === 0 || now.getDay() === 6) ? 'weekend' : 'weekday'
const tomorrowType = (tomorrow.getDay() === 0 || tomorrow.getDay() === 6) ? 'weekend' : 'weekday'
const isActuallyToday = type === todayType || type === 'todos'
const isActuallyTomorrow = type === tomorrowType || type === 'todos'
// Filtro Hoy: Es hoy Y no ha pasado (o es de los que dice salir en este rango)
if (dayFilter.value === 'today') {
const [hStr, mStr] = (s.departure_time || '00:00').split(':') const [hStr, mStr] = (s.departure_time || '00:00').split(':')
const h = parseInt(hStr || '0') const h = parseInt(hStr || '0')
const m = parseInt(mStr || '0') const m = parseInt(mStr || '0')
const hhmmSched = h * 100 + m const hhmmSched = h * 100 + m
const isPassed = hhmmSched < hhmmAhora - 2 // margen de 2 min const isPassed = hhmmSched < hhmmAhora - 2 // margen de 2 min
return isActuallyToday && !isPassed
// Filtro Hoy: Es hoy Y no ha pasado (o es de los que dice salir en este rango)
if (dayFilter.value === 'today') {
return d === 'today' && !isPassed
} }
// Filtro Mañana: Es mañana // Filtro Mañana: Es mañana (sin importar si pasó la hora hoy)
if (dayFilter.value === 'tomorrow') { if (dayFilter.value === 'tomorrow') {
return d === 'tomorrow' return isActuallyTomorrow
} }
// Filtro Todos: Mostrar todo sin importar si pasó o es otro día // Filtro Todos: Mostrar todo
return true return true
}) })
}) })
@ -281,7 +289,7 @@ onUnmounted(() => {
v-for="(schedule, i) in filteredSchedules" v-for="(schedule, i) in filteredSchedules"
:key="schedule.id" :key="schedule.id"
class="schedule-card" class="schedule-card"
:class="`schedule-card--${getBusStatus(schedule.departure_time)}`" :class="dayFilter === 'today' ? `schedule-card--${getBusStatus(schedule.departure_time)}` : 'schedule-card--upcoming'"
> >
<!-- Borde izquierdo decorativo --> <!-- Borde izquierdo decorativo -->
<div class="card-accent"></div> <div class="card-accent"></div>
@ -295,8 +303,8 @@ onUnmounted(() => {
<!-- Info --> <!-- Info -->
<div class="card-info"> <div class="card-info">
<div class="card-top-row"> <div class="card-top-row">
<span class="day-tag" :class="`day-tag--${getScheduleDay(schedule)}`"> <span class="day-tag" :class="dayFilter === 'today' ? `day-tag--${getScheduleDay(schedule)}` : 'day-tag--tomorrow'">
{{ getDayLabel(schedule) }} {{ dayFilter === 'tomorrow' ? 'Mañana' : (dayFilter === 'all' ? getDayLabel(schedule) : getDayLabel(schedule)) }}
</span> </span>
<span v-if="i === 0 && getBusStatus(schedule.departure_time) === 'departing'" class="departing-pulse">SALIENDO</span> <span v-if="i === 0 && getBusStatus(schedule.departure_time) === 'departing'" class="departing-pulse">SALIENDO</span>
</div> </div>
@ -308,11 +316,11 @@ onUnmounted(() => {
</div> </div>
<!-- Badge estado --> <!-- Badge estado -->
<div class="status-badge" :class="`status-badge--${getBusStatus(schedule.departure_time)}`"> <div class="status-badge" :class="dayFilter === 'today' ? `status-badge--${getBusStatus(schedule.departure_time)}` : 'status-badge--upcoming'">
<span class="material-icons status-icon"> <span class="material-icons status-icon">
{{ getBusStatus(schedule.departure_time) === 'departing' ? 'directions_run' : {{ (dayFilter === 'today' && getBusStatus(schedule.departure_time) === 'departing') ? 'directions_run' :
getBusStatus(schedule.departure_time) === 'ontime' ? 'check_circle' : (dayFilter === 'today' && getBusStatus(schedule.departure_time) === 'ontime') ? 'check_circle' :
getBusStatus(schedule.departure_time) === 'passed' ? 'history' : 'access_time' }} (dayFilter === 'today' && getBusStatus(schedule.departure_time) === 'passed') ? 'history' : 'access_time' }}
</span> </span>
</div> </div>
</div> </div>

View File

@ -1,10 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } 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()
// Solo mostrar el header con tabs en las vistas principales
const isMainView = computed(() => {
return route.name === 'TaxisLocales' || route.name === 'ViajesTuristicos'
})
const mountError = ref(false) const mountError = ref(false)
const reloadPage = () => { const reloadPage = () => {
@ -24,7 +29,7 @@ onMounted(async () => {
<template> <template>
<div class="taxi-view"> <div class="taxi-view">
<header class="header-main"> <header v-if="isMainView" class="header-main">
<h1 class="brand-title">{{ t('taxi.title') }}</h1> <h1 class="brand-title">{{ t('taxi.title') }}</h1>
<div class="hub-tabs"> <div class="hub-tabs">
<div class="tabs-background"> <div class="tabs-background">

View File

@ -54,13 +54,13 @@ const volver = () => {
</script> </script>
<template> <template>
<div class="shuttle-detalle-container bg-surface pb-24 min-h-screen relative"> <div class="shuttle-detalle-container bg-[var(--bg-primary)] pb-24 min-h-screen relative">
<!-- Header con botón volver --> <!-- Header con botón volver (Solid background to avoid overlap on scroll) -->
<div class="sticky top-0 z-10 bg-surface border-b border-border flex items-center gap-3 px-4 py-3 shadow-sm" style="padding-top: max(env(safe-area-inset-top), 12px);"> <div class="sticky top-0 z-50 bg-[var(--bg-primary)] border-b border-border flex items-center gap-3 px-4 py-3 shadow-md" style="padding-top: max(env(safe-area-inset-top), 12px);">
<button @click="volver" class="p-2 rounded-full hover:bg-hover flex items-center justify-center transition"> <button @click="volver" class="p-2 rounded-full hover:bg-[var(--hover-bg)] flex items-center justify-center transition">
<span class="material-icons text-text-primary">arrow_back</span> <span class="material-icons text-[var(--text-primary)]">arrow_back</span>
</button> </button>
<h1 class="font-bold text-text-primary text-lg truncate flex-1"> <h1 class="font-bold text-[var(--text-primary)] text-lg truncate flex-1">
{{ shuttle?.company_name || 'Detalle del viaje' }} {{ shuttle?.company_name || 'Detalle del viaje' }}
</h1> </h1>
</div> </div>
@ -90,32 +90,32 @@ const volver = () => {
class="w-full h-full object-cover" class="w-full h-full object-cover"
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'shuttle')" @error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'shuttle')"
/> />
<div class="absolute bottom-3 left-3 bg-surface/90 backdrop-blur-sm px-3 py-1 rounded-full text-sm font-bold shadow-sm flex items-center gap-1"> <div class="absolute bottom-3 left-3 bg-[var(--bg-primary)]/90 backdrop-blur-sm px-3 py-1 rounded-full text-sm font-bold shadow-sm flex items-center gap-1">
<span class="material-icons text-sm" style="color: var(--active-color)">directions_bus</span> <span class="material-icons text-sm" style="color: var(--active-color)">directions_bus</span>
{{ shuttle.vehicle_type }} {{ shuttle.vehicle_type }}
</div> </div>
</div> </div>
<!-- Rutas Origen - Destino prominente --> <!-- Rutas Origen - Destino prominente -->
<div class="bg-surface rounded-2xl p-5 shadow-sm space-y-3 border border-border"> <div class="bg-[var(--bg-secondary)] rounded-2xl p-5 shadow-sm space-y-3 border border-border">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<span class="text-xs text-text-tertiary font-semibold mb-1 uppercase tracking-wider">Origen</span> <span class="text-xs text-[var(--text-secondary)] font-semibold mb-1 uppercase tracking-wider">Origen</span>
<span class="font-bold text-text-primary text-lg leading-tight break-words"> <span class="font-bold text-[var(--text-primary)] text-lg leading-tight break-words">
{{ shuttle.origin }} {{ shuttle.origin }}
</span> </span>
</div> </div>
<div class="flex flex-col items-center px-4 shrink-0"> <div class="flex flex-col items-center px-4 shrink-0">
<span class="text-xs text-text-secondary font-bold mb-1">{{ shuttle.estimated_duration }}</span> <span class="text-xs text-[var(--text-secondary)] font-bold mb-1">{{ shuttle.estimated_duration }}</span>
<div class="w-16 border-t-2 border-dashed border-border relative my-1"> <div class="w-16 border-t-2 border-dashed border-border relative my-1">
<span class="material-icons absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-text-secondary bg-surface px-1 text-sm">east</span> <span class="material-icons absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[var(--text-secondary)] bg-[var(--bg-secondary)] px-1 text-sm">east</span>
</div> </div>
</div> </div>
<div class="flex flex-col flex-1 text-right"> <div class="flex flex-col flex-1 text-right">
<span class="text-xs text-text-tertiary font-semibold mb-1 uppercase tracking-wider">Destino</span> <span class="text-xs text-[var(--text-secondary)] font-semibold mb-1 uppercase tracking-wider">Destino</span>
<span class="font-bold text-text-primary text-lg leading-tight break-words"> <span class="font-bold text-[var(--text-primary)] text-lg leading-tight break-words">
{{ shuttle.destination }} {{ shuttle.destination }}
</span> </span>
</div> </div>
@ -123,28 +123,28 @@ const volver = () => {
</div> </div>
<!-- Info principal --> <!-- Info principal -->
<div class="bg-surface rounded-2xl p-5 shadow-sm space-y-4 border border-border"> <div class="bg-[var(--bg-secondary)] rounded-2xl p-5 shadow-sm space-y-4 border border-border">
<div> <div>
<h2 class="text-xl font-bold text-text-primary">{{ shuttle.company_name }}</h2> <h2 class="text-xl font-bold text-[var(--text-primary)]">{{ shuttle.company_name }}</h2>
<p class="text-text-secondary text-sm mt-1 leading-relaxed" v-if="shuttle.description" style="white-space: pre-wrap;">{{ shuttle.description }}</p> <p class="text-[var(--text-secondary)] text-sm mt-1 leading-relaxed" v-if="shuttle.description" style="white-space: pre-wrap;">{{ shuttle.description }}</p>
</div> </div>
<div class="grid grid-cols-2 gap-4 pt-4 border-t border-border"> <div class="grid grid-cols-2 gap-4 pt-4 border-t border-border">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-xs text-text-tertiary uppercase tracking-wider font-semibold flex items-center gap-1"><span class="material-icons text-sm">schedule</span> Hora de salida</span> <span class="text-xs text-[var(--text-secondary)] uppercase tracking-wider font-semibold flex items-center gap-1"><span class="material-icons text-sm">schedule</span> Hora de salida</span>
<span class="font-semibold text-text-primary bg-bg-secondary p-2 rounded-lg text-sm text-center border border-border"> <span class="font-semibold text-[var(--text-primary)] bg-[var(--bg-primary)] p-2 rounded-lg text-sm text-center border border-border">
{{ shuttle.departure_times }} {{ shuttle.departure_times }}
</span> </span>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-xs text-text-tertiary uppercase tracking-wider font-semibold flex items-center gap-1"><span class="material-icons text-sm">swap_horiz</span> Tipo de viaje</span> <span class="text-xs text-[var(--text-secondary)] uppercase tracking-wider font-semibold flex items-center gap-1"><span class="material-icons text-sm">swap_horiz</span> Tipo de viaje</span>
<span class="font-semibold text-text-primary bg-bg-secondary p-2 rounded-lg text-sm text-center border border-border capitalize"> <span class="font-semibold text-[var(--text-primary)] bg-[var(--bg-primary)] p-2 rounded-lg text-sm text-center border border-border capitalize">
{{ shuttle.trip_type.replace('_', ' ') }} {{ shuttle.trip_type.replace('_', ' ') }}
</span> </span>
</div> </div>
<div class="flex flex-col gap-1" v-if="shuttle.english_speaking"> <div class="flex flex-col gap-1" v-if="shuttle.english_speaking">
<span class="text-xs text-text-tertiary uppercase tracking-wider font-semibold flex items-center gap-1"><span class="material-icons text-sm">g_translate</span> Idiomas</span> <span class="text-xs text-[var(--text-secondary)] uppercase tracking-wider font-semibold flex items-center gap-1"><span class="material-icons text-sm">g_translate</span> Idiomas</span>
<span class="font-semibold text-text-primary bg-bg-secondary p-2 rounded-lg text-sm text-center border border-border"> <span class="font-semibold text-[var(--text-primary)] bg-[var(--bg-primary)] p-2 rounded-lg text-sm text-center border border-border">
Español · English Español · English
</span> </span>
</div> </div>
@ -167,10 +167,10 @@ const volver = () => {
</div> </div>
<!-- Contacto --> <!-- Contacto -->
<div class="bg-surface rounded-2xl p-5 shadow-sm space-y-4 border border-border mb-8"> <div class="bg-[var(--bg-secondary)] rounded-2xl p-5 shadow-sm space-y-4 border border-border mb-8">
<div> <div>
<h3 class="font-bold text-text-primary text-lg">Reserva e Información</h3> <h3 class="font-bold text-[var(--text-primary)] text-lg">Reserva e Información</h3>
<p class="text-sm text-text-secondary mt-1">Contacta directamente al operador para confirmar disponibilidad.</p> <p class="text-sm text-[var(--text-secondary)] mt-1">Contacta directamente al operador para confirmar disponibilidad.</p>
</div> </div>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
@ -186,7 +186,7 @@ const volver = () => {
<a v-if="shuttle.phone_number" <a v-if="shuttle.phone_number"
:href="`tel:${shuttle.phone_number}`" :href="`tel:${shuttle.phone_number}`"
class="flex justify-center items-center gap-2 p-3.5 bg-bg-secondary text-text-primary rounded-xl font-bold hover:bg-hover transition active:scale-95 border border-border" class="flex justify-center items-center gap-2 p-3.5 bg-[var(--bg-primary)] text-[var(--text-primary)] rounded-xl font-bold hover:bg-[var(--hover-bg)] transition active:scale-95 border border-border"
@click="analyticsService.logEvent({ event_name: 'shuttle_contact', item_id: shuttle.id, properties: { action: 'call' } })" @click="analyticsService.logEvent({ event_name: 'shuttle_contact', item_id: shuttle.id, properties: { action: 'call' } })"
> >
<span class="material-icons">phone_in_talk</span> <span class="material-icons">phone_in_talk</span>