2026-02-21 09:53:31 -05:00
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
import { useScheduleStore } from '@/stores/schedule'
|
|
|
|
|
import { useRouteStore } from '@/stores/route'
|
|
|
|
|
import { formatTo12Hour } from '@/utils/timeFormatter'
|
|
|
|
|
import { analyticsService } from '@/services/analyticsService'
|
|
|
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
|
|
|
|
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
|
|
|
|
const scheduleStore = useScheduleStore()
|
|
|
|
|
const routeStore = useRouteStore()
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'Schedules' })
|
|
|
|
|
await routeStore.loadRoutes()
|
|
|
|
|
|
|
|
|
|
const queryRouteId = route.query.routeId as string
|
|
|
|
|
if (queryRouteId) {
|
|
|
|
|
const foundRoute = routeStore.allRoutes.find(r => r.id === queryRouteId)
|
|
|
|
|
if (foundRoute) {
|
|
|
|
|
syncRouteSelection(foundRoute.id, foundRoute.name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const unwatchQuery = watch(
|
|
|
|
|
() => route.query.routeId,
|
|
|
|
|
(newRouteId) => {
|
|
|
|
|
if (newRouteId) {
|
|
|
|
|
const foundRoute = routeStore.allRoutes.find(r => r.id === newRouteId as string)
|
|
|
|
|
if (foundRoute) {
|
|
|
|
|
syncRouteSelection(foundRoute.id, foundRoute.name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
unwatchQuery()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function syncRouteSelection(routeId: string, routeName: string) {
|
|
|
|
|
routeStore.selectRoute(routeId, routeName)
|
|
|
|
|
scheduleStore.loadRouteSchedules(routeId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectRouteAndClose(routeId: string, routeName: string) {
|
|
|
|
|
analyticsService.logEvent({
|
|
|
|
|
event_name: 'schedule_viewed',
|
|
|
|
|
item_id: routeName,
|
|
|
|
|
properties: { route_id: routeId }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
routeStore.selectRoute(routeId, routeName)
|
|
|
|
|
scheduleStore.loadRouteSchedules(routeId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function goToMap() {
|
|
|
|
|
if (routeStore.selectedRouteId) {
|
|
|
|
|
router.push({
|
|
|
|
|
path: '/map',
|
|
|
|
|
query: { routeId: routeStore.selectedRouteId }
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-02-22 15:05:59 -05:00
|
|
|
<div class="schedules-view-redesign bg-slate-50 dark:bg-zinc-950 text-slate-900 dark:text-slate-100 min-h-screen">
|
|
|
|
|
<!-- Header Sticky con Blur -->
|
|
|
|
|
<header class="sticky top-0 z-50 bg-white/90 dark:bg-zinc-900/90 backdrop-blur-md px-6 pt-10 pb-4 border-b border-slate-200 dark:border-zinc-800">
|
|
|
|
|
<div class="flex items-center justify-between mb-6">
|
|
|
|
|
<button @click="router.back()" class="w-10 h-10 flex items-center justify-center -ml-2 rounded-full active:bg-slate-100 dark:active:bg-zinc-800 transition-colors">
|
|
|
|
|
<span class="material-icons text-2xl">arrow_back</span>
|
|
|
|
|
</button>
|
|
|
|
|
<h1 class="text-xl font-bold tracking-tight">{{ t('schedules.title') }}</h1>
|
|
|
|
|
<div class="w-10"></div>
|
2026-02-22 10:29:41 -05:00
|
|
|
</div>
|
2026-02-22 15:05:59 -05:00
|
|
|
|
|
|
|
|
<!-- Selector de Línea -->
|
|
|
|
|
<div class="relative group" v-if="!routeStore.isLoadingRoutes && routeStore.allRoutes.length > 0">
|
|
|
|
|
<label class="block text-[10px] font-bold text-slate-400 dark:text-zinc-500 uppercase tracking-widest mb-1.5 ml-1">{{ t('schedules.selectRoute') || 'Seleccionar Línea' }}</label>
|
|
|
|
|
<div class="relative">
|
|
|
|
|
<select
|
|
|
|
|
class="w-full pl-4 pr-12 py-4 bg-slate-50 dark:bg-zinc-800 border-2 border-transparent focus:border-primary focus:ring-0 rounded-2xl text-lg font-bold transition-all cursor-pointer appearance-none"
|
|
|
|
|
id="route-select"
|
|
|
|
|
:value="routeStore.selectedRouteId"
|
|
|
|
|
@change="e => {
|
|
|
|
|
const target = e.target as HTMLSelectElement;
|
|
|
|
|
if (target) {
|
|
|
|
|
const selectedRoute = routeStore.allRoutes.find(r => r.id === target.value);
|
|
|
|
|
if (selectedRoute) selectRouteAndClose(target.value, selectedRoute.name);
|
|
|
|
|
}
|
|
|
|
|
}"
|
2026-02-21 09:53:31 -05:00
|
|
|
>
|
2026-02-22 15:05:59 -05:00
|
|
|
<option value="" disabled>{{ t('schedules.placeholder') || 'Elige una ruta...' }}</option>
|
|
|
|
|
<option v-for="route in routeStore.allRoutes" :key="route.id" :value="route.id">
|
|
|
|
|
{{ route.name }}
|
|
|
|
|
</option>
|
|
|
|
|
</select>
|
|
|
|
|
<span class="material-icons absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none">expand_more</span>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-22 15:05:59 -05:00
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<main class="flex-1 px-6 pt-6 pb-32">
|
|
|
|
|
<div v-if="routeStore.isLoadingRoutes" class="flex flex-col items-center justify-center py-20">
|
|
|
|
|
<div class="w-12 h-12 border-4 border-primary/20 border-t-primary rounded-full animate-spin mb-4"></div>
|
|
|
|
|
<p class="text-slate-500 dark:text-zinc-400 font-medium">{{ t('schedules.loadingRoutes') }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-else-if="!routeStore.selectedRouteId" class="flex flex-col items-center justify-center py-20 text-center">
|
|
|
|
|
<span class="material-icons text-6xl text-slate-200 dark:text-zinc-800 mb-4">route</span>
|
|
|
|
|
<p class="text-slate-500 dark:text-zinc-400 font-medium italic">Selecciona una ruta para ver sus horarios</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
<!-- Estado Próximas Salidas -->
|
|
|
|
|
<div class="flex items-center justify-between mb-6">
|
|
|
|
|
<h2 class="text-sm font-semibold text-slate-500 dark:text-zinc-400">{{ t('schedules.upcoming') || 'Próximas salidas' }}</h2>
|
|
|
|
|
<div class="flex items-center gap-1.5 px-2.5 py-1 bg-[#fee715]/10 rounded-full">
|
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full bg-[#fee715] animate-pulse"></span>
|
|
|
|
|
<span class="text-[10px] font-bold text-slate-700 dark:text-[#fee715] tracking-wide">EN VIVO</span>
|
|
|
|
|
</div>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
2026-02-22 15:05:59 -05:00
|
|
|
|
|
|
|
|
<!-- Listado de Horarios -->
|
|
|
|
|
<div v-if="scheduleStore.isLoading" class="flex justify-center py-10">
|
|
|
|
|
<div class="w-8 h-8 border-2 border-primary/20 border-t-primary rounded-full animate-spin"></div>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
2026-02-22 15:05:59 -05:00
|
|
|
|
|
|
|
|
<div v-else-if="scheduleStore.schedules.length === 0" class="bg-white dark:bg-zinc-800/50 p-8 rounded-2xl border border-slate-100 dark:border-zinc-800 text-center">
|
|
|
|
|
<span class="material-icons text-slate-300 dark:text-zinc-700 text-4xl mb-2">event_busy</span>
|
|
|
|
|
<p class="text-slate-500 dark:text-zinc-400">{{ t('schedules.noSchedules') || 'No hay salidas programadas para hoy' }}</p>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
2026-02-22 15:05:59 -05:00
|
|
|
|
|
|
|
|
<div v-else class="space-y-4">
|
|
|
|
|
<div
|
|
|
|
|
v-for="schedule in scheduleStore.schedules"
|
|
|
|
|
:key="schedule.id"
|
|
|
|
|
class="p-4 bg-white dark:bg-zinc-800/50 border border-slate-100 dark:border-zinc-800 rounded-2xl flex items-center justify-between hover:border-primary/50 transition-all shadow-sm"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
<div class="text-3xl font-extrabold tracking-tighter text-slate-900 dark:text-white">
|
|
|
|
|
{{ formatTo12Hour(schedule.departure_time) }}
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
2026-02-22 15:05:59 -05:00
|
|
|
<div class="h-8 w-px bg-slate-200 dark:bg-zinc-700"></div>
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-[10px] font-medium text-slate-400 dark:text-zinc-500 uppercase tracking-wider">Línea</p>
|
|
|
|
|
<p class="font-bold text-slate-900 dark:text-white leading-tight">
|
|
|
|
|
{{ routeStore.selectedRouteName }}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span
|
|
|
|
|
class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider rounded-full"
|
|
|
|
|
:class="schedule.schedule_type === 'weekday' ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' : 'bg-slate-100 dark:bg-zinc-700 text-slate-500 dark:text-zinc-400'"
|
|
|
|
|
>
|
|
|
|
|
{{ t(`schedules.types.${schedule.schedule_type}`) || schedule.schedule_type }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
2026-02-22 15:05:59 -05:00
|
|
|
</template>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<!-- Botón Flotante para ir al Mapa -->
|
|
|
|
|
<Transition name="fade">
|
|
|
|
|
<button
|
|
|
|
|
v-if="routeStore.selectedRouteId"
|
|
|
|
|
@click="goToMap"
|
|
|
|
|
class="fixed bottom-24 right-6 w-14 h-14 bg-primary text-slate-900 rounded-full shadow-2xl flex items-center justify-center active:scale-95 transition-all z-40"
|
|
|
|
|
>
|
|
|
|
|
<span class="material-icons text-3xl">map</span>
|
|
|
|
|
</button>
|
|
|
|
|
</Transition>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-02-22 15:05:59 -05:00
|
|
|
.schedules-view-redesign {
|
|
|
|
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
2026-02-21 09:53:31 -05:00
|
|
|
overflow-x: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
select {
|
|
|
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
background-position: right 1rem center;
|
|
|
|
|
background-size: 1.25rem;
|
2026-02-21 09:53:31 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
.fade-enter-active, .fade-leave-active {
|
|
|
|
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
2026-02-21 09:53:31 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
.fade-enter-from, .fade-leave-to {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateY(20px);
|
2026-02-21 09:53:31 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
/* Transiciones para la lista */
|
|
|
|
|
.space-y-4 > * {
|
|
|
|
|
animation: slideUp 0.4s ease-out forwards;
|
2026-02-21 09:53:31 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
@keyframes slideUp {
|
|
|
|
|
from { opacity: 0; transform: translateY(10px); }
|
|
|
|
|
to { opacity: 1; transform: translateY(0); }
|
2026-02-21 09:53:31 -05:00
|
|
|
}
|
|
|
|
|
</style>
|