Refactor: Map UI improvements, ETA metrics, Schedule fixes, and Transport Detail styling
This commit is contained in:
@ -98,6 +98,7 @@ function selectStopFromSearch(stop: BusStop) {
|
||||
}
|
||||
|
||||
function openUberSearch() {
|
||||
showPromos.value = false; // Cerramos ofertas para evitar solapamiento
|
||||
showUberSearch.value = true;
|
||||
showRoutesToggle.value = true; // Forzar que al abrir estemos en modo rutas
|
||||
}
|
||||
@ -180,6 +181,10 @@ function closeBusStopModal() {
|
||||
selectedBusStop.value = null;
|
||||
}
|
||||
|
||||
function reloadPage() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function handlePromoClick(promo: any) {
|
||||
selectedPromo.value = promo;
|
||||
showPromoModal.value = true;
|
||||
@ -503,7 +508,9 @@ async function updateActiveUnits() {
|
||||
if (!isLoaded.value) return;
|
||||
|
||||
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) {
|
||||
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>
|
||||
</div>
|
||||
|
||||
<!-- Nuevo Banner de Parada Cercana Alineado -->
|
||||
<!-- Nuevo Banner de Parada Cercana Alineado (Redimensionado y con ETA) -->
|
||||
<div
|
||||
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="flex flex-col flex-1 truncate">
|
||||
<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 class="banner-icon-bg">
|
||||
<span class="material-icons text-white text-[16px]">directions_bus</span>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -1531,6 +1552,76 @@ html.light-theme .uber-search-trigger-compact {
|
||||
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 {
|
||||
position: fixed;
|
||||
top: 70px; /* Debajo del header superior */
|
||||
|
||||
@ -56,7 +56,7 @@ function getScheduleDay(schedule: any): 'today' | 'tomorrow' | 'other' {
|
||||
|
||||
// Comparar con el tipo del horario
|
||||
// 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 isTomorrow = type === tomorrowType || type === 'todos'
|
||||
@ -81,27 +81,35 @@ function getDayLabel(schedule: any): string {
|
||||
// ── Filtrado de horarios
|
||||
const filteredSchedules = computed(() => {
|
||||
const now = new Date()
|
||||
const tomorrow = new Date(now)
|
||||
tomorrow.setDate(now.getDate() + 1)
|
||||
|
||||
const hhmmAhora = now.getHours() * 100 + now.getMinutes()
|
||||
|
||||
return scheduleStore.schedules.filter(s => {
|
||||
const d = getScheduleDay(s)
|
||||
const [hStr, mStr] = (s.departure_time || '00:00').split(':')
|
||||
const h = parseInt(hStr || '0')
|
||||
const m = parseInt(mStr || '0')
|
||||
const hhmmSched = h * 100 + m
|
||||
const isPassed = hhmmSched < hhmmAhora - 2 // margen de 2 min
|
||||
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') {
|
||||
return d === 'today' && !isPassed
|
||||
const [hStr, mStr] = (s.departure_time || '00:00').split(':')
|
||||
const h = parseInt(hStr || '0')
|
||||
const m = parseInt(mStr || '0')
|
||||
const hhmmSched = h * 100 + m
|
||||
const isPassed = hhmmSched < hhmmAhora - 2 // margen de 2 min
|
||||
return isActuallyToday && !isPassed
|
||||
}
|
||||
|
||||
// Filtro Mañana: Es mañana
|
||||
// Filtro Mañana: Es mañana (sin importar si pasó la hora hoy)
|
||||
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
|
||||
})
|
||||
})
|
||||
@ -281,7 +289,7 @@ onUnmounted(() => {
|
||||
v-for="(schedule, i) in filteredSchedules"
|
||||
:key="schedule.id"
|
||||
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 -->
|
||||
<div class="card-accent"></div>
|
||||
@ -295,8 +303,8 @@ onUnmounted(() => {
|
||||
<!-- Info -->
|
||||
<div class="card-info">
|
||||
<div class="card-top-row">
|
||||
<span class="day-tag" :class="`day-tag--${getScheduleDay(schedule)}`">
|
||||
{{ getDayLabel(schedule) }}
|
||||
<span class="day-tag" :class="dayFilter === 'today' ? `day-tag--${getScheduleDay(schedule)}` : 'day-tag--tomorrow'">
|
||||
{{ dayFilter === 'tomorrow' ? 'Mañana' : (dayFilter === 'all' ? getDayLabel(schedule) : getDayLabel(schedule)) }}
|
||||
</span>
|
||||
<span v-if="i === 0 && getBusStatus(schedule.departure_time) === 'departing'" class="departing-pulse">SALIENDO</span>
|
||||
</div>
|
||||
@ -308,11 +316,11 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
{{ getBusStatus(schedule.departure_time) === 'departing' ? 'directions_run' :
|
||||
getBusStatus(schedule.departure_time) === 'ontime' ? 'check_circle' :
|
||||
getBusStatus(schedule.departure_time) === 'passed' ? 'history' : 'access_time' }}
|
||||
{{ (dayFilter === 'today' && getBusStatus(schedule.departure_time) === 'departing') ? 'directions_run' :
|
||||
(dayFilter === 'today' && getBusStatus(schedule.departure_time) === 'ontime') ? 'check_circle' :
|
||||
(dayFilter === 'today' && getBusStatus(schedule.departure_time) === 'passed') ? 'history' : 'access_time' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
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 reloadPage = () => {
|
||||
@ -24,7 +29,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="taxi-view">
|
||||
<header class="header-main">
|
||||
<header v-if="isMainView" class="header-main">
|
||||
<h1 class="brand-title">{{ t('taxi.title') }}</h1>
|
||||
<div class="hub-tabs">
|
||||
<div class="tabs-background">
|
||||
|
||||
@ -54,13 +54,13 @@ const volver = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="shuttle-detalle-container bg-surface pb-24 min-h-screen relative">
|
||||
<!-- Header con botón volver -->
|
||||
<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);">
|
||||
<button @click="volver" class="p-2 rounded-full hover:bg-hover flex items-center justify-center transition">
|
||||
<span class="material-icons text-text-primary">arrow_back</span>
|
||||
<div class="shuttle-detalle-container bg-[var(--bg-primary)] pb-24 min-h-screen relative">
|
||||
<!-- Header con botón volver (Solid background to avoid overlap on scroll) -->
|
||||
<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-[var(--hover-bg)] flex items-center justify-center transition">
|
||||
<span class="material-icons text-[var(--text-primary)]">arrow_back</span>
|
||||
</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' }}
|
||||
</h1>
|
||||
</div>
|
||||
@ -90,32 +90,32 @@ const volver = () => {
|
||||
class="w-full h-full object-cover"
|
||||
@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>
|
||||
{{ shuttle.vehicle_type }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 flex-col flex-1">
|
||||
<span class="text-xs text-text-tertiary font-semibold mb-1 uppercase tracking-wider">Origen</span>
|
||||
<span class="font-bold text-text-primary text-lg leading-tight break-words">
|
||||
<span class="text-xs text-[var(--text-secondary)] font-semibold mb-1 uppercase tracking-wider">Origen</span>
|
||||
<span class="font-bold text-[var(--text-primary)] text-lg leading-tight break-words">
|
||||
{{ shuttle.origin }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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 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="font-bold text-text-primary text-lg leading-tight break-words">
|
||||
<span class="text-xs text-[var(--text-secondary)] font-semibold mb-1 uppercase tracking-wider">Destino</span>
|
||||
<span class="font-bold text-[var(--text-primary)] text-lg leading-tight break-words">
|
||||
{{ shuttle.destination }}
|
||||
</span>
|
||||
</div>
|
||||
@ -123,28 +123,28 @@ const volver = () => {
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<h2 class="text-xl font-bold text-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>
|
||||
<h2 class="text-xl font-bold text-[var(--text-primary)]">{{ shuttle.company_name }}</h2>
|
||||
<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 class="grid grid-cols-2 gap-4 pt-4 border-t border-border">
|
||||
<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="font-semibold text-text-primary bg-bg-secondary p-2 rounded-lg text-sm text-center border border-border">
|
||||
<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-[var(--text-primary)] bg-[var(--bg-primary)] p-2 rounded-lg text-sm text-center border border-border">
|
||||
{{ shuttle.departure_times }}
|
||||
</span>
|
||||
</div>
|
||||
<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="font-semibold text-text-primary bg-bg-secondary p-2 rounded-lg text-sm text-center border border-border capitalize">
|
||||
<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-[var(--text-primary)] bg-[var(--bg-primary)] p-2 rounded-lg text-sm text-center border border-border capitalize">
|
||||
{{ shuttle.trip_type.replace('_', ' ') }}
|
||||
</span>
|
||||
</div>
|
||||
<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="font-semibold text-text-primary bg-bg-secondary p-2 rounded-lg text-sm text-center border border-border">
|
||||
<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-[var(--text-primary)] bg-[var(--bg-primary)] p-2 rounded-lg text-sm text-center border border-border">
|
||||
Español · English
|
||||
</span>
|
||||
</div>
|
||||
@ -167,10 +167,10 @@ const volver = () => {
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<h3 class="font-bold text-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>
|
||||
<h3 class="font-bold text-[var(--text-primary)] text-lg">Reserva e Información</h3>
|
||||
<p class="text-sm text-[var(--text-secondary)] mt-1">Contacta directamente al operador para confirmar disponibilidad.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
@ -186,7 +186,7 @@ const volver = () => {
|
||||
|
||||
<a v-if="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' } })"
|
||||
>
|
||||
<span class="material-icons">phone_in_talk</span>
|
||||
|
||||
Reference in New Issue
Block a user