diff --git a/frontend/src/components/BottomNav.vue b/frontend/src/components/BottomNav.vue
index 1f1e0ec..e042319 100644
--- a/frontend/src/components/BottomNav.vue
+++ b/frontend/src/components/BottomNav.vue
@@ -14,16 +14,24 @@ const navItems = [
{ name: 'taxi', path: '/taxi', icon: 'directions_bus' }
]
-let isNavigating = false
+const isNavigating = ref(false)
-const navigateTo = (path: string) => {
+const navigateTo = async (path: string) => {
// Prevent rapid multiple navigations (debounce guard)
- if (isNavigating) return
+ if (isNavigating.value) return
if (route.path === path) return
- isNavigating = true
- router.push(path).finally(() => {
- setTimeout(() => { isNavigating = false }, 300)
- })
+
+ try {
+ isNavigating.value = true
+ await router.push(path)
+ } catch (e: any) {
+ if (e?.name !== 'NavigationDuplicated') {
+ console.error('SIBU | Error de navegación en el menú inferior:', e)
+ }
+ } finally {
+ // Add a small delay to prevent rapid double-taps
+ setTimeout(() => { isNavigating.value = false }, 300)
+ }
}
const isActive = (path: string) => {
@@ -59,8 +67,9 @@ onUnmounted(() => {
v-for="item in navItems"
:key="item.name"
class="nav-item"
- :class="{ active: isActive(item.path) }"
- @click="navigateTo(item.path)"
+ :class="{ active: isActive(item.path), 'opacity-50 pointer-events-none': isNavigating }"
+ @click.prevent="navigateTo(item.path)"
+ @touchend.prevent="navigateTo(item.path)"
>
{{ item.icon }}
{{ t('navigation.' + item.name) }}
diff --git a/frontend/src/composables/useDirectionsRoute.ts b/frontend/src/composables/useDirectionsRoute.ts
index 336d45b..0f53f88 100644
--- a/frontend/src/composables/useDirectionsRoute.ts
+++ b/frontend/src/composables/useDirectionsRoute.ts
@@ -27,7 +27,7 @@ export function useDirectionsRoute() {
// Función utilitaria para pausar ejecución
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
- const trazarRuta = async (paradas: Parada[], map: google.maps.Map) => {
+ const trazarRuta = async (paradas: Parada[], map: google.maps.Map, isPast: boolean = false) => {
if (!paradas || paradas.length < 2) {
errorRuta.value = 'Se requieren al menos 2 paradas para trazar una ruta.';
return;
@@ -75,10 +75,14 @@ export function useDirectionsRoute() {
map: map,
suppressMarkers: true, // SIBU maneja los suyos propios
preserveViewport: true, // No auto centrar en cada tramo para evitar parpadeos visuales
- polylineOptions: {
- strokeColor: '#1E40AF', // Azul (Tailwind blue-800)
+ polylineOptions: isPast ? {
+ strokeColor: '#9CA3AF', // Gris Tailwind 400
+ strokeWeight: 3,
+ strokeOpacity: 0.4
+ } : {
+ strokeColor: '#1D4ED8', // Azul Tailwind 700
strokeWeight: 5,
- strokeOpacity: 0.8
+ strokeOpacity: 0.95
}
});
diff --git a/frontend/src/composables/useGoogleMaps.ts b/frontend/src/composables/useGoogleMaps.ts
index 5b533ee..ee3dfee 100644
--- a/frontend/src/composables/useGoogleMaps.ts
+++ b/frontend/src/composables/useGoogleMaps.ts
@@ -208,6 +208,160 @@ export function useGoogleMaps() {
return marker
}
+ function addCleanMarker(
+ position: { lat: number; lng: number },
+ title: string,
+ type: 'normal' | 'cercana' | 'origen' | 'destino',
+ onClick?: () => void
+ ): google.maps.Marker | null {
+ if (!map.value) {
+ console.error('Map not initialized');
+ return null;
+ }
+
+ const iconoParadaNormal = {
+ path: google.maps.SymbolPath.CIRCLE,
+ fillColor: '#3B82F6', // azul
+ fillOpacity: 1,
+ strokeColor: '#FFFFFF', // borde blanco limpio
+ strokeWeight: 2,
+ scale: 7
+ };
+
+ const iconoParadaCercana = {
+ path: google.maps.SymbolPath.CIRCLE,
+ fillColor: '#F59E0B', // amarillo/naranja
+ fillOpacity: 1,
+ strokeColor: '#FFFFFF',
+ strokeWeight: 3,
+ scale: 10
+ };
+
+ const iconoOrigen = {
+ path: google.maps.SymbolPath.CIRCLE,
+ fillColor: '#10B981', // verde
+ fillOpacity: 1,
+ strokeColor: '#FFFFFF',
+ strokeWeight: 3,
+ scale: 10
+ };
+
+ const iconoDestino = {
+ path: google.maps.SymbolPath.CIRCLE,
+ fillColor: '#EF4444', // rojo
+ fillOpacity: 1,
+ strokeColor: '#FFFFFF',
+ strokeWeight: 3,
+ scale: 10
+ };
+
+ const iconos = {
+ normal: iconoParadaNormal,
+ cercana: iconoParadaCercana,
+ origen: iconoOrigen,
+ destino: iconoDestino
+ };
+
+ const marker = new google.maps.Marker({
+ position,
+ map: map.value,
+ title,
+ icon: iconos[type],
+ });
+
+ if (onClick) {
+ const infoWindow = new google.maps.InfoWindow({
+ content: `
+
+ 🚌 ${title}
+
+ `
+ });
+ marker.addListener('click', () => {
+ infoWindow.open(map.value, marker);
+ onClick();
+ });
+ }
+
+ if (map.value) {
+ if (!globalOverlays.has(map.value)) {
+ globalOverlays.set(map.value, new Set());
+ }
+ globalOverlays.get(map.value)!.add(marker);
+ }
+
+ return marker;
+ }
+
+ function addHtmlMarker(
+ position: { lat: number; lng: number },
+ htmlContent: string,
+ offset: { x: number; y: number } = { x: 0, y: 0 }
+ ) {
+ if (!map.value) return null;
+
+ class CustomOverlay extends google.maps.OverlayView {
+ private div: HTMLElement | null = null;
+ private pos: google.maps.LatLng;
+
+ constructor(pos: google.maps.LatLng) {
+ super();
+ this.pos = pos;
+ }
+
+ onAdd() {
+ const div = document.createElement('div');
+ div.style.position = 'absolute';
+ div.style.cursor = 'pointer';
+ div.innerHTML = htmlContent;
+ this.div = div;
+ const panes = this.getPanes();
+ panes?.overlayMouseTarget.appendChild(div);
+ }
+
+ draw() {
+ const overlayProjection = this.getProjection();
+ const point = overlayProjection.fromLatLngToDivPixel(this.pos);
+ if (point && this.div) {
+ this.div.style.left = (point.x + offset.x) + 'px';
+ this.div.style.top = (point.y + offset.y) + 'px';
+ }
+ }
+
+ onRemove() {
+ if (this.div) {
+ try {
+ // Safer element removal
+ if (this.div.parentNode) {
+ this.div.parentNode.removeChild(this.div);
+ } else {
+ this.div.remove();
+ }
+ } catch (e) {
+ console.warn('CustomOverlay: element already removed or parent mismatch', e);
+ }
+ this.div = null;
+ }
+ }
+
+ setPosition(newPos: { lat: number; lng: number }) {
+ this.pos = new google.maps.LatLng(newPos.lat, newPos.lng);
+ this.draw();
+ }
+ }
+
+ const overlay = new CustomOverlay(new google.maps.LatLng(position.lat, position.lng));
+ overlay.setMap(map.value);
+
+ // Track for cleanup
+ if (!globalOverlays.has(map.value)) {
+ globalOverlays.set(map.value, new Set());
+ }
+ globalOverlays.get(map.value)!.add(overlay as any);
+
+ return overlay;
+ }
+
function addPolyline(path: Array<{ lat: number; lng: number }>): google.maps.Polyline | null {
if (!map.value) {
console.error('Map not initialized')
@@ -362,75 +516,6 @@ export function useGoogleMaps() {
// with Google Maps' native OverlayView management.
}
- function addHtmlMarker(
- position: { lat: number; lng: number },
- htmlContent: string,
- offset: { x: number; y: number } = { x: 0, y: 0 }
- ) {
- if (!map.value) return null;
-
- class CustomOverlay extends google.maps.OverlayView {
- private div: HTMLElement | null = null;
- private pos: google.maps.LatLng;
-
- constructor(pos: google.maps.LatLng) {
- super();
- this.pos = pos;
- }
-
- onAdd() {
- const div = document.createElement('div');
- div.style.position = 'absolute';
- div.style.cursor = 'pointer';
- div.innerHTML = htmlContent;
- this.div = div;
- const panes = this.getPanes();
- panes?.overlayMouseTarget.appendChild(div);
- }
-
- draw() {
- const overlayProjection = this.getProjection();
- const point = overlayProjection.fromLatLngToDivPixel(this.pos);
- if (point && this.div) {
- this.div.style.left = (point.x + offset.x) + 'px';
- this.div.style.top = (point.y + offset.y) + 'px';
- }
- }
-
- onRemove() {
- if (this.div) {
- try {
- // Safer element removal
- if (this.div.parentNode) {
- this.div.parentNode.removeChild(this.div);
- } else {
- this.div.remove();
- }
- } catch (e) {
- console.warn('CustomOverlay: element already removed or parent mismatch', e);
- }
- this.div = null;
- }
- }
-
- setPosition(newPos: { lat: number; lng: number }) {
- this.pos = new google.maps.LatLng(newPos.lat, newPos.lng);
- this.draw();
- }
- }
-
- const overlay = new CustomOverlay(new google.maps.LatLng(position.lat, position.lng));
- overlay.setMap(map.value);
-
- // Track for cleanup
- if (!globalOverlays.has(map.value)) {
- globalOverlays.set(map.value, new Set());
- }
- globalOverlays.get(map.value)!.add(overlay as any);
-
- return overlay;
- }
-
onMounted(() => {
loadMaps()
})
@@ -444,6 +529,7 @@ export function useGoogleMaps() {
addMarker,
addHtmlMarker,
addNumberedMarker,
+ addCleanMarker,
addPolyline,
addRoutePolyline,
fitBounds,
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index 3becef4..6a21a2f 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -44,6 +44,11 @@ const router = createRouter({
name: 'taxi',
component: () => import('@/views/TaxiView.vue'),
},
+ {
+ path: '/shuttle/:id',
+ name: 'shuttle-details',
+ component: () => import('@/views/ShuttleDetalleView.vue'),
+ },
// ─── Vistas de Descubrir ─────────────────────────────────────────────
{
diff --git a/frontend/src/views/MapView.vue b/frontend/src/views/MapView.vue
index e405fab..58854e4 100644
--- a/frontend/src/views/MapView.vue
+++ b/frontend/src/views/MapView.vue
@@ -25,7 +25,7 @@ const mapStore = useMapStore();
const busStopStore = useBusStopStore();
const couponStore = useCouponStore();
-const { map, isLoaded, error: mapsError, initMap, addNumberedMarker, addHtmlMarker, fitBounds, setCenter, setZoom, addMarker, clearAllOverlays } = useGoogleMaps();
+const { map, isLoaded, error: mapsError, initMap, addCleanMarker, addHtmlMarker, setCenter, setZoom, addMarker, clearAllOverlays } = useGoogleMaps();
const { trazarRuta, limpiarRuta, estasCargando: estasCargandoRuta, errorRuta } = useDirectionsRoute();
const { encontrarParadaCercana, limpiarCaminata, paradaCercana, distanciaMetros, duracionCaminata } = useParadaCercana();
const { calcularETA, busesActivos, cargando: etaCargando } = useETA();
@@ -39,7 +39,6 @@ const polyline = ref(null);
const walkingPolyline = ref(null);
const walkingPolylineBorder = ref(null); // Borde blanco estilo Google Maps
const optimalStopPulse = ref(null); // Radar para la parada óptima
-const navigationInfo = ref<{ distance: string, duration: string, targetName: string } | null>(null);
const showRouteDropdown = ref(false);
const routeCardRef = ref(null);
const isUpdatingMarkers = ref(false);
@@ -49,7 +48,7 @@ const userCoords = ref<{ lat: number; lng: number } | null>(null); // Store last
const currentMarkerMode = ref<'dot' | 'pin' | null>(null);
const mappingSequenceId = ref(0); // Atomic ID to prevent race conditions
-
+const alturaNavbar = ref(64);
// Search state
const stopSearchQuery = ref("");
const destinationQuery = ref("");
@@ -109,7 +108,6 @@ async function clearAllMapData() {
showRoutesToggle.value = false;
destinationQuery.value = "";
stopSearchQuery.value = "";
- navigationInfo.value = null;
// 2. Invalidar cualquier hilo de dibujo en curso
mappingSequenceId.value++;
@@ -218,8 +216,14 @@ async function claimPromo() {
}
}
-
onMounted(async () => {
+ const navbar = document.querySelector('#navbar-admin') ?? document.querySelector('nav') ?? document.querySelector('header');
+ if (navbar) {
+ alturaNavbar.value = navbar.getBoundingClientRect().height;
+ } else {
+ alturaNavbar.value = 64;
+ }
+
analyticsService.logEvent({ event_name: 'screen_view', properties: { screen_name: 'Map' } })
// Add click outside listener
document.addEventListener('click', handleClickOutside);
@@ -343,7 +347,7 @@ async function initializeMap() {
updatePromoMarkers();
// Apply initial styles based on current zoom
- updateMarkersStyles(true);
+ updateMarkersStyles();
}
// Watch for route selection changes
@@ -392,12 +396,13 @@ watch(
if (!oldStops || newStops.length !== oldStops.length ||
newStops.some((stop, idx) => stop.id !== oldStops[idx]?.id)) {
console.log('Route stops changed - updating markers')
- await updateMapMarkers();
- // FLOW REFINEMENT: After markers are loaded, find the optimal entrance stop
+ // FLOW REFINEMENT: Find the optimal entrance stop first
if (newStops.length > 0) {
- highlightOptimalStopForRoute();
+ await highlightOptimalStopForRoute();
}
+
+ await updateMapMarkers();
}
}
}
@@ -445,9 +450,6 @@ function clearMapMarkers() {
walkingPolylineBorder.value = null;
}
- // Clear navigation info
- navigationInfo.value = null;
-
// Clear optimal pulse
if (optimalStopPulse.value) {
if (typeof optimalStopPulse.value.setMap === 'function') {
@@ -474,6 +476,7 @@ async function updateMapMarkers() {
if (!currentRequestRouteId || stops.length === 0) {
clearMapMarkers();
+ limpiarRuta();
return;
}
@@ -489,121 +492,83 @@ async function updateMapMarkers() {
return;
}
- const newMarkers: any[] = [];
- const path: Array<{ lat: number; lng: number }> = [];
-
- for (let i = 0; i < stops.length; i++) {
- const stop = stops[i];
- if (!stop) continue;
-
- // Verificación atómica en cada paso
- if (mappingSequenceId.value !== thisSeq || !routeStore.selectedRouteId) {
- newMarkers.forEach(m => { if (m.setMap) m.setMap(null); });
- return;
- }
-
- const marker = addNumberedMarker(
- { lat: stop.latitude, lng: stop.longitude },
- i + 1,
- stop.name,
- () => handleBusStopClick(stop)
- );
-
- if (marker) newMarkers.push(marker);
- path.push({ lat: stop.latitude, lng: stop.longitude });
- }
-
- // Final check before committing to the map
- if (mappingSequenceId.value !== thisSeq || !routeStore.selectedRouteId) {
- newMarkers.forEach(m => { if (m.setMap) m.setMap(null); });
- return;
- }
-
clearMapMarkers();
- markers.value = newMarkers;
+ limpiarRuta();
- if (path.length > 0) fitBounds(path);
-
- } catch (err) {
- console.error('❌ JARVIS: Error en updateMapMarkers:', err);
- } finally {
- if (mappingSequenceId.value === thisSeq) {
- isUpdatingMarkers.value = false;
- if (routeStore.selectedRouteId) updateMarkersStyles(true);
+ let pastStops: any[] = [];
+ let relevantStops: any[] = [...stops];
+
+ if (paradaCercana.value) {
+ const idx = stops.findIndex(s => s.id === paradaCercana.value?.id);
+ if (idx > 0) {
+ pastStops = stops.slice(0, idx + 1); // overlap that 1 point for continuous mapping
+ relevantStops = stops.slice(idx);
}
}
+
+ const newMarkers: any[] = [];
+
+ // Paradas del tramo relevante: mostrar con clean markers
+ relevantStops.forEach((stop, index) => {
+ let tipo: 'normal' | 'cercana' | 'origen' | 'destino' = 'normal';
+
+ if (paradaCercana.value && stop.id === paradaCercana.value.id) tipo = 'cercana';
+ else if (index === relevantStops.length - 1) tipo = 'destino';
+ else if (!paradaCercana.value && index === 0) tipo = 'origen';
+
+ const marker = addCleanMarker(
+ { lat: stop.latitude, lng: stop.longitude },
+ stop.name,
+ tipo,
+ () => handleBusStopClick(stop)
+ );
+ if (marker) newMarkers.push(marker);
+ });
+
+ markers.value = newMarkers;
+
+ // Dibujar en paralelo ambos tramos
+ const renderPromises = [];
+ if (pastStops.length > 1 && map.value) {
+ renderPromises.push(trazarRuta(pastStops.map((p, i) => ({
+ id: i, nombre: p.name, latitud: p.latitude, longitud: p.longitude, orden: i
+ })), map.value, true));
+ }
+ if (relevantStops.length > 1 && map.value) {
+ renderPromises.push(trazarRuta(relevantStops.map((p, i) => ({
+ id: i, nombre: p.name, latitud: p.latitude, longitud: p.longitude, orden: i
+ })), map.value, false));
+ }
+ await Promise.all(renderPromises);
+
+ // Zoom automático al tramo que le importa al usuario
+ if (map.value) {
+ const bounds = new google.maps.LatLngBounds();
+ if (userCoords.value) {
+ bounds.extend(userCoords.value);
+ }
+ relevantStops.forEach(p => bounds.extend({ lat: p.latitude, lng: p.longitude }));
+
+ // Timeout para que los directions renderers también ajusten bounds si preserveViewport estaba false (actualmente es true)
+ setTimeout(() => {
+ if (map.value && bounds.getNorthEast() && bounds.getSouthWest()) {
+ map.value.fitBounds(bounds, { top: 80, bottom: 120, left: 20, right: 20 });
+ }
+ }, 150);
+ }
+
+ } catch (err) {
+ console.error('❌ JARVIS: Error en updateMapMarkers:', err);
+ } finally {
+ if (mappingSequenceId.value === thisSeq) {
+ isUpdatingMarkers.value = false;
+ // updateMarkersStyles NO hace falta para "clean markers". Lo mantenemos en caso sea forzado.
+ }
+ }
}
-/**
- * Optimización de rendimiento: Solo actualiza los iconos si cambiamos de modo (punto vs pin)
- * o si se fuerza la actualización (ej: al cargar nueva ruta)
- */
-function updateMarkersStyles(force = false) {
- if (!map.value || markers.value.length === 0 || !routeStore.selectedRouteId) return;
-
- const currentZoom = map.value.getZoom() || 12;
- const newMode = currentZoom >= 15 ? 'pin' : 'dot';
-
- if (!force && currentMarkerMode.value === newMode) return;
-
- currentMarkerMode.value = newMode;
- const showNumbers = newMode === 'pin';
-
- console.log(`🤖 JARVIS: Actualizando estilos de marcadores a modo: ${newMode}`);
-
- markers.value.forEach((marker: any, index: number) => {
- if (!marker) return;
-
- // Si la secuencia cambió o la ruta desapareció mientras hacíamos esto, abortamos
- if (!routeStore.selectedRouteId) {
- if (marker.setMap) marker.setMap(null);
- return;
- }
-
- if (showNumbers) {
- // MODO PREMIUM: Círculo Amarillo con Borde Negro y Numero Negro
- marker.setIcon({
- path: google.maps.SymbolPath.CIRCLE,
- fillColor: '#FEE715',
- fillOpacity: 1,
- strokeColor: '#101820',
- strokeWeight: 2.5,
- scale: 14, // Tamaño ideal para leer el número dentro
- });
- marker.setLabel({
- text: (index + 1).toString(),
- color: '#101820',
- fontSize: '13px',
- fontWeight: '900',
- });
- } else {
- // MODO COMPACTO: Punto Amarillo brillante con anillo de profundidad
- marker.setIcon({
- path: google.maps.SymbolPath.CIRCLE,
- fillColor: '#FEE715',
- fillOpacity: 1,
- strokeColor: '#101820',
- strokeWeight: 1.5,
- scale: 7,
- });
- marker.setLabel(null);
- }
- });
-
- // Dibujar la ruta usando Directions API cuando se actualicen los marcadores
- if (routeStore.selectedRouteId && map.value) {
- const stopsForDirections = markers.value.map((m, i) => {
- const pos = m.getPosition();
- return {
- id: i, // ID temporal para trazar logic
- nombre: `Stop ${i+1}`,
- latitud: pos.lat(),
- longitud: pos.lng(),
- orden: i
- };
- });
- trazarRuta(stopsForDirections, map.value);
- }
+function updateMarkersStyles() {
+ // Empty space: Clean markers are static and distinct per requirement.
}
// La función drawRouteOnRoad ha sido eliminada por petición del usuario (línea azul removida)
@@ -652,11 +617,6 @@ function selectRouteAndClose(routeId: string, routeName: string) {
routeStore.selectRoute(routeId, routeName);
showRouteDropdown.value = false;
showUberSearch.value = false; // Close the expanded search panel
-
- // Si no tenemos ubicación, la pedimos para poder calcular la parada óptima automáticamente
- if (!userCoords.value) {
- locateUser();
- }
}
async function updateActiveUnits() {
if (!isLoaded.value) return;
@@ -690,8 +650,13 @@ const optimalSonarHtml = `
`;
-function locateUser() {
- if (navigator.geolocation) {
+function locateUser(): Promise {
+ return new Promise((resolve) => {
+ if (!navigator.geolocation) {
+ resolve();
+ return;
+ }
+
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
@@ -715,13 +680,19 @@ function locateUser() {
sonarHtml,
{ x: -30, y: -30 }
);
+ resolve();
},
(error) => {
- console.error("Error getting location", error);
- alert("No se pudo obtener tu ubicación. Por favor, verifica tus permisos de GPS.");
+ console.warn("SIBU | Geolocalización denegada:", error.message);
+ resolve();
+ },
+ {
+ enableHighAccuracy: true,
+ timeout: 8000,
+ maximumAge: 30000
}
);
- }
+ });
}
/**
@@ -730,6 +701,10 @@ function locateUser() {
* y la resalta para el usuario.
*/
async function highlightOptimalStopForRoute() {
+ if (!userCoords.value) {
+ await locateUser();
+ }
+
if (!userCoords.value || routeStore.selectedRouteStops.length === 0) {
console.warn('🤖 JARVIS: Sin ubicación o paradas para calcular parada óptima.');
return;
@@ -744,9 +719,7 @@ async function highlightOptimalStopForRoute() {
const stopObj = paradaCercana.value as BusStop;
console.log(`🤖 JARVIS: Parada óptima detectada: ${stopObj.name}`);
- // Centrar mapa
- setCenter(stopObj.latitude, stopObj.longitude);
- setZoom(17);
+ // Ya no centramos o hacemos zoom aquí manual porque la nueva gráfica de updateMapMarkers ajusta bounds y engloba location.
// Añadir el PULSO NARANJA
if (optimalStopPulse.value && typeof optimalStopPulse.value.setMap === 'function') {
@@ -759,13 +732,6 @@ async function highlightOptimalStopForRoute() {
{ x: -30, y: -30 }
);
- // Mini-notificación (Opcional, se cubre ahora también con ETA card)
- navigationInfo.value = {
- distance: distanciaMetros.value < 1000 ? `${distanciaMetros.value.toFixed(0)} m` : `${(distanciaMetros.value/1000).toFixed(1)} km`,
- duration: "Calculada",
- targetName: stopObj.name
- };
-
// Calcular ETAs
await calcularETA(routeStore.selectedRouteId!, stopObj);
showETACard.value = true;
@@ -837,13 +803,9 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
const route = dirResult.routes[0];
const leg = route.legs?.[0];
- // Guardar info de navegación (ETA y Distancia)
+ // Guardar info de navegación (ETA y Distancia) (Retirado a favor de ETA Card / Parada Cercana Banner)
if (leg) {
- navigationInfo.value = {
- distance: leg.distance?.text || '---',
- duration: leg.duration?.text || '---',
- targetName: targetStop.name
- };
+ // console.log('Distancia', leg.distance?.text);
}
if (walkingPolyline.value) walkingPolyline.value.setMap(null);
@@ -881,11 +843,6 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
});
}
-function clearNavigation() {
- clearMapMarkers();
- navigationInfo.value = null;
-}
-
@@ -893,7 +850,7 @@ function clearNavigation() {
-
+
Calculando ruta real...
@@ -903,6 +860,32 @@ function clearNavigation() {
+
+
+
@@ -975,27 +958,6 @@ function clearNavigation() {
-
-
-
-
-
-
-
- {{ navigationInfo.duration }}
- {{ navigationInfo.distance }}
-
-
Parada: {{ navigationInfo.targetName }}
-
-
-
-
-
-
-
-
diff --git a/frontend/src/views/ShuttleDetalleView.vue b/frontend/src/views/ShuttleDetalleView.vue
new file mode 100644
index 0000000..600653f
--- /dev/null
+++ b/frontend/src/views/ShuttleDetalleView.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+ {{ shuttle?.company_name || 'Detalle del viaje' }}
+
+
+
+
+
+
refresh
+
Cargando...
+
+
+
+
+
error_outline
+
{{ error }}
+
+
+
+
+
+
+
+
![]()
(e.target as HTMLImageElement).src = getImageUrl(null, 'shuttle')"
+ />
+
+ directions_bus
+ {{ shuttle.vehicle_type }}
+
+
+
+
+
+
+
+ Origen
+
+ {{ shuttle.origin }}
+
+
+
+
+
{{ shuttle.estimated_duration }}
+
+ east
+
+
+
+
+ Destino
+
+ {{ shuttle.destination }}
+
+
+
+
+
+
+
+
+
{{ shuttle.company_name }}
+
{{ shuttle.description }}
+
+
+
+
+ schedule Hora de salida
+
+ {{ shuttle.departure_times }}
+
+
+
+ swap_horiz Tipo de viaje
+
+ {{ shuttle.trip_type.replace('_', ' ') }}
+
+
+
+ g_translate Idiomas
+
+ Español · English
+
+
+
+
+
+
+
+
+
Precio por pasajero
+
+ $
+ {{ parsePrice(shuttle.price_per_person) }}
+
+
+
+ Privado
+ ${{ parsePrice(shuttle.price_private_trip) }}
+
+
+
+
+
+
+
Reserva e Información
+
Contacta directamente al operador para confirmar disponibilidad.
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/TaxiView.vue b/frontend/src/views/TaxiView.vue
index 360c406..5d43ea5 100644
--- a/frontend/src/views/TaxiView.vue
+++ b/frontend/src/views/TaxiView.vue
@@ -1,5 +1,6 @@