chore: apply finalized MapView and ETA card fixes

This commit is contained in:
2026-02-27 12:22:15 -05:00
parent bddb8916ba
commit 504e61bfb2
4 changed files with 158 additions and 57 deletions

View File

@ -5,11 +5,26 @@
<!-- Bottom Sheet container --> <!-- Bottom Sheet container -->
<div <div
class="relative bg-white dark:bg-gray-900 rounded-t-3xl shadow-2xl p-5 transform transition-transform duration-300 ease-out flex flex-col gap-4 max-h-[85vh] overflow-y-auto" ref="sheetRef"
:class="isOpen ? 'translate-y-0' : 'translate-y-full'" class="relative bg-white dark:bg-gray-900 rounded-t-3xl shadow-2xl p-5 transform flex flex-col gap-4 max-h-[85vh] overflow-y-auto"
:style="{
transform: `translateY(${dragY}px)`,
transition: isDragging ? 'none' : 'transform 0.3s ease-out'
}"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
> >
<!-- Indicador de arrastre (visual) --> <!-- Pestaña de arrastre (visual + funcional) -->
<div class="absolute top-3 left-1/2 -translate-x-1/2 w-12 h-1.5 bg-gray-300 dark:bg-gray-600 rounded-full cursor-pointer" @click="closeCard"></div> <div
class="absolute top-3 left-1/2 -translate-x-1/2 w-12 h-1.5 bg-gray-300 dark:bg-gray-600 rounded-full cursor-grab active:cursor-grabbing"
@touchstart="onTouchStart"
></div>
<!-- Indicador visual de que se puede arrastrar -->
<p class="text-center text-[10px] text-gray-400 mt-1 mb-0 pointer-events-none">
Desliza hacia abajo para cerrar
</p>
<!-- Cabecera de la parada --> <!-- Cabecera de la parada -->
<div class="mt-4 flex items-start gap-4 pb-4 border-b border-gray-100 dark:border-gray-800"> <div class="mt-4 flex items-start gap-4 pb-4 border-b border-gray-100 dark:border-gray-800">
@ -115,7 +130,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'; import { ref, onMounted, onUnmounted } from 'vue';
import type { BusETA } from '@/composables/useETA'; import type { BusETA } from '@/composables/useETA';
defineProps<{ defineProps<{
@ -132,6 +147,45 @@ const emit = defineEmits<{
(e: 'refresh'): void; (e: 'refresh'): void;
}>(); }>();
// ── DRAG TO DISMISS ──────────────────────────────────
const sheetRef = ref<HTMLElement | null>(null);
const dragY = ref(0); // desplazamiento actual del drag
const isDragging = ref(false);
const startY = ref(0);
const DISMISS_THRESHOLD = 0.30; // 30% de la altura = cerrar
function onTouchStart(e: TouchEvent) {
startY.value = e.touches[0]?.clientY ?? 0;
isDragging.value = true;
dragY.value = 0;
}
function onTouchMove(e: TouchEvent) {
if (!isDragging.value) return;
const delta = (e.touches[0]?.clientY ?? 0) - startY.value;
// Solo permitir arrastrar hacia ABAJO (delta positivo)
if (delta > 0) {
dragY.value = delta;
e.preventDefault(); // evitar scroll del contenido mientras arrastra
}
}
function onTouchEnd() {
if (!isDragging.value) return;
isDragging.value = false;
const sheetHeight = sheetRef.value?.offsetHeight ?? 400;
const draggedRatio = dragY.value / sheetHeight;
if (draggedRatio >= DISMISS_THRESHOLD) {
// Arrastró suficiente → cerrar
emit('close');
}
// Siempre resetear posición (snap back o después de cerrar)
dragY.value = 0;
}
// ── AUTO REFRESH ─────────────────────────────────────
let intervalId: number; let intervalId: number;
function closeCard() { function closeCard() {

View File

@ -7,7 +7,8 @@ import { useRouteStore } from '@/stores/route'
export const useFlujoPrincipal = () => { export const useFlujoPrincipal = () => {
const { limpiarMapa, registrarMarker } = useMapState() const { limpiarMapa, registrarMarker } = useMapState()
const { encontrarParadaCercana } = useParadaCercana() const paradaCercanaInst = useParadaCercana()
const { encontrarParadaCercana, paradaCercana } = paradaCercanaInst
const { trazarRuta } = useDirectionsRoute() const { trazarRuta } = useDirectionsRoute()
const cargando = ref(false) const cargando = ref(false)
const errorMsg = ref('') const errorMsg = ref('')
@ -68,12 +69,6 @@ export const useFlujoPrincipal = () => {
// ── PASO 4: Calcular parada más cercana ─────────────── // ── PASO 4: Calcular parada más cercana ───────────────
await encontrarParadaCercana(ubicacion, paradas, map) await encontrarParadaCercana(ubicacion, paradas, map)
// Buscamos manualmente porque encontrarParadaCercana guarda en su propio ref interno que no retornamos fácil
// o podemos usar useParadaCercana().paradaCercana.value
// Actually, refactor from useParadaCercana: finding the closest
const { paradaCercana } = useParadaCercana()
await encontrarParadaCercana(ubicacion, paradas, undefined) // sin mapa para no dibujar polilínea vieja por encima
const paradaCercanaFound = paradaCercana.value const paradaCercanaFound = paradaCercana.value

View File

@ -524,6 +524,9 @@ export function useGoogleMaps() {
if ('remove' in overlay && typeof overlay.remove === 'function') { if ('remove' in overlay && typeof overlay.remove === 'function') {
overlay.remove() overlay.remove()
} }
if ('onRemove' in overlay && typeof overlay.onRemove === 'function') {
overlay.onRemove()
}
} catch (e) { } catch (e) {
// Ignore errors when removing overlays // Ignore errors when removing overlays
console.warn('Error removing overlay:', e) console.warn('Error removing overlay:', e)

View File

@ -107,23 +107,24 @@ function closeUberSearch() {
} }
async function clearAllMapData() { async function clearAllMapData() {
console.log('🤖 JARVIS: Iniciando PURGA nuclear con tolerancia a fallos...'); console.log('🤖 JARVIS: Iniciando PURGA nuclear...');
// 1. Respuesta inmediata en UI // 1. UI inmediata
showUberSearch.value = false; showUberSearch.value = false;
showRoutesToggle.value = false; showRoutesToggle.value = false;
destinationQuery.value = ""; destinationQuery.value = "";
stopSearchQuery.value = ""; stopSearchQuery.value = "";
showETACard.value = false;
// 2. Invalidar cualquier hilo de dibujo en curso // 2. Invalidar hilos en curso
mappingSequenceId.value++; mappingSequenceId.value++;
try { try {
// 3. Resetear Store // 3. Resetear stores
routeStore.clearSelection(); routeStore.clearSelection();
lastProcessedRouteId.value = null; lastProcessedRouteId.value = null;
// 4. Limpieza manual protegida de marcadores // 4. Limpiar markers locales
const sweep = (arrayRef: any) => { const sweep = (arrayRef: any) => {
if (!arrayRef.value) return; if (!arrayRef.value) return;
arrayRef.value.forEach((m: any) => { arrayRef.value.forEach((m: any) => {
@ -135,7 +136,7 @@ async function clearAllMapData() {
sweep(markers); sweep(markers);
sweep(promoMarkers); sweep(promoMarkers);
// Limpiar Unidades de transporte // Limpiar unidades de transporte
if (unitMarkers.value) { if (unitMarkers.value) {
unitMarkers.value.forEach((m: any) => { unitMarkers.value.forEach((m: any) => {
try { if (m && m.setMap) m.setMap(null); } catch (e) {} try { if (m && m.setMap) m.setMap(null); } catch (e) {}
@ -143,37 +144,75 @@ async function clearAllMapData() {
unitMarkers.value.clear(); unitMarkers.value.clear();
} }
// 5. Barrido profundo de Google // 5. Limpiar polilíneas (CORREGIDO: agregar walkingPolylineBorder)
if (polyline.value) {
polyline.value.setMap(null);
polyline.value = null;
}
if (walkingPolyline.value) {
walkingPolyline.value.setMap(null);
walkingPolyline.value = null;
}
// ✅ NUEVO: limpiar el borde blanco de la ruta caminando
if (walkingPolylineBorder.value) {
walkingPolylineBorder.value.setMap(null);
walkingPolylineBorder.value = null;
}
// 6. Limpiar pulso de parada óptima (CORREGIDO)
if (optimalStopPulse.value) {
try {
// Intentar setMap primero
if (typeof optimalStopPulse.value.setMap === 'function') {
optimalStopPulse.value.setMap(null);
}
// Si es un overlay HTML, también intentar remove()
if (typeof optimalStopPulse.value.remove === 'function') {
optimalStopPulse.value.remove();
}
// Si tiene onRemove (OverlayView pattern)
if (typeof optimalStopPulse.value.onRemove === 'function') {
optimalStopPulse.value.onRemove();
}
} catch(e) {
console.warn('SIBU | No se pudo limpiar optimalStopPulse:', e);
}
optimalStopPulse.value = null;
}
// 7. Limpiar composables
limpiarCaminata();
// 8. Barrido profundo de Google Maps overlays
if (typeof clearAllOverlays === 'function') { if (typeof clearAllOverlays === 'function') {
try { clearAllOverlays(); } catch (e) {} try { clearAllOverlays(); } catch (e) {}
} }
// 6. Limpiar polilíneas y pulsos // 9. Purgación centralizada (useMapState)
if (polyline.value) { polyline.value.setMap(null); polyline.value = null; }
if (walkingPolyline.value) { walkingPolyline.value.setMap(null); walkingPolyline.value = null; }
if (optimalStopPulse.value) {
try { if (optimalStopPulse.value.setMap) optimalStopPulse.value.setMap(null); } catch(e){}
optimalStopPulse.value = null;
}
limpiarCaminata();
showETACard.value = false;
// Nueva Purgación Centralizada:
limpiarTodoCentralizado(); limpiarTodoCentralizado();
// 7. Restaurar Solo Usuario tras un breve respiro // 10. Restaurar SOLO el marcador del usuario
await nextTick(); await nextTick();
if (userCoords.value) { if (userCoords.value) {
const { lat, lng } = userCoords.value; const { lat, lng } = userCoords.value;
if (userMarker.value && userMarker.value.setMap) { // Limpiar marcador anterior del usuario
try { userMarker.value.setMap(null); } catch(e){} if (userMarker.value) {
try {
if (userMarker.value.setMap) userMarker.value.setMap(null);
if (userMarker.value.remove) userMarker.value.remove();
} catch(e) {}
} }
userMarker.value = addHtmlMarker({ lat, lng }, sonarHtml, { x: -30, y: -30 }); // Redibujar solo el sonar del usuario
userMarker.value = addHtmlMarker(
{ lat, lng },
sonarHtml,
{ x: -30, y: -30 }
);
} }
console.log('🤖 JARVIS: Purga completada con éxito.'); console.log('🤖 JARVIS: Purga completada. Solo queda el usuario ✓');
} catch (err) { } catch (err) {
console.error('❌ JARVIS: Error crítico en purga, pero el mapa debería estar limpio:', err); console.error('❌ JARVIS: Error en purga:', err);
} }
} }
@ -344,6 +383,11 @@ async function initializeMap() {
map.value.addListener('zoom_changed', () => { map.value.addListener('zoom_changed', () => {
updateMarkersStyles(); updateMarkersStyles();
}); });
map.value.addListener('click', () => {
if (showETACard.value) {
showETACard.value = false;
}
});
} }
// If we have a selected route, show its stops // If we have a selected route, show its stops
@ -626,9 +670,18 @@ async function highlightOptimalStopForRoute() {
{ x: -30, y: -30 } { x: -30, y: -30 }
); );
// Calcular ETAs // PASO 1: Mostrar ETACard inferior primero
await calcularETA(routeStore.selectedRouteId!, stopObj); await calcularETA(routeStore.selectedRouteId!, stopObj);
showETACard.value = true; showETACard.value = true;
// PASO 2: Esperar 2 segundos antes de mostrar el banner superior
// para que no saturen la pantalla al mismo tiempo
await new Promise(resolve => setTimeout(resolve, 2000));
// PASO 3: Mostrar banner superior solo si ETACard sigue abierto
// (si el usuario ya cerró el ETACard, no mostrar el banner)
// paradaCercana ya tiene el valor, el banner aparece automáticamente
// porque usa v-if="paradaCercana && routeStore.selectedRouteId && !showETACard"
} }
} }
@ -756,26 +809,22 @@ function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop:
<!-- Banner de Parada Más Cercana Inteligente --> <!-- Banner de Parada Más Cercana Inteligente -->
<div <div
v-if="paradaCercana && routeStore.selectedRouteId" v-if="paradaCercana && routeStore.selectedRouteId && !showETACard"
class="fixed left-0 right-0 z-40 px-3 transition-transform duration-300 pointer-events-none" class="fixed left-0 right-0 z-40 px-3 transition-transform duration-300 pointer-events-none"
:style="{ top: alturaNavbar + 'px' }" :style="{ top: alturaNavbar + 'px' }"
> >
<div class="bg-white dark:bg-gray-900 rounded-b-2xl shadow-xl border-t-4 border-blue-600 border-t-blue-600 p-3 flex items-center gap-3 pointer-events-auto"> <!-- Solo mostrar cuando ETACard está CERRADO -->
<div class="bg-blue-100 dark:bg-blue-900/40 rounded-full p-2 shrink-0"> <!-- v-if agrega condición: && !showETACard -->
<span class="material-icons text-blue-600 dark:text-blue-400">directions_bus</span> <div class="bg-white/90 dark:bg-gray-900/90 backdrop-blur-sm rounded-b-2xl shadow-lg border-t-2 border-yellow-400 px-4 py-2 flex items-center gap-2 pointer-events-auto">
</div> <span class="material-icons text-yellow-500 text-sm">directions_bus</span>
<div class="flex-1 min-w-0"> <span class="text-sm font-bold text-gray-800 dark:text-white truncate flex-1">
<p class="text-[11px] text-gray-500 font-bold uppercase">Parada más cercana</p> {{ paradaCercana?.name }}
<p class="text-sm font-bold text-gray-800 dark:text-white truncate"> </span>
{{ paradaCercana?.name }} <span class="text-xs text-gray-500 whitespace-nowrap">
</p> {{ (distanciaMetros && distanciaMetros < 1000) ? Math.round(distanciaMetros) + 'm' : (distanciaMetros ? (distanciaMetros / 1000).toFixed(1) + 'km' : '') }}
<p class="text-xs text-blue-600 dark:text-blue-400 font-medium whitespace-nowrap overflow-hidden text-ellipsis"> </span>
{{ (distanciaMetros && distanciaMetros < 1000) ? Math.round(distanciaMetros) + ' m' : (distanciaMetros ? (distanciaMetros / 1000).toFixed(1) + ' km' : '') }} <button @click="paradaCercana = null" class="text-gray-400 hover:text-gray-600 shrink-0 p-0.5 ml-1">
<span v-if="duracionCaminata">· {{ Math.round(duracionCaminata / 60) }} min caminando</span> <span class="material-icons text-sm">close</span>
</p>
</div>
<button @click="paradaCercana = null" class="text-gray-400 hover:text-gray-600 shrink-0 p-1">
<span class="material-icons">close</span>
</button> </button>
</div> </div>
</div> </div>