Fix favorites system, add support for bus stops and tourist trips, and improve UI consistency
This commit is contained in:
@ -2,7 +2,7 @@
|
|||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import type { BusStop } from '@/types'
|
import type { BusStop } from '@/types'
|
||||||
import { busStopsService } from '@/services/busStopsService'
|
import { busStopsService } from '@/services/busStopsService'
|
||||||
import { favoritesService } from '@/services/favoritesService'
|
import FavoriteButton from '@/components/FavoriteButton.vue'
|
||||||
import { formatTo12Hour } from '@/utils/timeFormatter'
|
import { formatTo12Hour } from '@/utils/timeFormatter'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -15,8 +15,6 @@ const emit = defineEmits(['close', 'navigate'])
|
|||||||
|
|
||||||
const upcomingArrivals = ref<{ routeName: string; arrivalTime: string }[]>([])
|
const upcomingArrivals = ref<{ routeName: string; arrivalTime: string }[]>([])
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const isFavorited = ref(false)
|
|
||||||
const favoriteId = ref<string | null>(null)
|
|
||||||
|
|
||||||
// Function to fetch arrivals
|
// Function to fetch arrivals
|
||||||
async function loadArrivals() {
|
async function loadArrivals() {
|
||||||
@ -33,48 +31,6 @@ async function loadArrivals() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkFavoriteStatus() {
|
|
||||||
const token = localStorage.getItem('auth_token')
|
|
||||||
if (!token || !props.busStop) {
|
|
||||||
isFavorited.value = false
|
|
||||||
favoriteId.value = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const favorites = await favoritesService.getMyFavorites()
|
|
||||||
const found = favorites.find(f => f.item_type === 'stop' && f.item_id === props.busStop?.id)
|
|
||||||
isFavorited.value = !!found
|
|
||||||
favoriteId.value = found ? found.id : null
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error checking favorite status", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleFavorite() {
|
|
||||||
const token = localStorage.getItem('auth_token')
|
|
||||||
if (!token) {
|
|
||||||
alert("Debes iniciar sesión para guardar favoritos")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!props.busStop) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isFavorited.value && props.busStop) {
|
|
||||||
await favoritesService.removeFavorite('stop', props.busStop.id)
|
|
||||||
isFavorited.value = false
|
|
||||||
favoriteId.value = null
|
|
||||||
} else {
|
|
||||||
const fav = await favoritesService.addFavorite('stop', props.busStop.id)
|
|
||||||
isFavorited.value = true
|
|
||||||
favoriteId.value = fav.id
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
alert("Error al actualizar favorito")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startInternalNavigation() {
|
function startInternalNavigation() {
|
||||||
if (props.busStop) {
|
if (props.busStop) {
|
||||||
emit('navigate', props.busStop)
|
emit('navigate', props.busStop)
|
||||||
@ -85,14 +41,12 @@ function startInternalNavigation() {
|
|||||||
watch(() => props.busStop, async (newStop) => {
|
watch(() => props.busStop, async (newStop) => {
|
||||||
if (newStop && props.isOpen) {
|
if (newStop && props.isOpen) {
|
||||||
await loadArrivals()
|
await loadArrivals()
|
||||||
await checkFavoriteStatus()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => props.isOpen, async (isOpen) => {
|
watch(() => props.isOpen, async (isOpen) => {
|
||||||
if (isOpen && props.busStop) {
|
if (isOpen && props.busStop) {
|
||||||
await loadArrivals()
|
await loadArrivals()
|
||||||
await checkFavoriteStatus()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -107,11 +61,11 @@ watch(() => props.isOpen, async (isOpen) => {
|
|||||||
<div v-if="busStop" class="header-info">
|
<div v-if="busStop" class="header-info">
|
||||||
<div class="title-row">
|
<div class="title-row">
|
||||||
<h3 class="stop-name">{{ busStop.name }}</h3>
|
<h3 class="stop-name">{{ busStop.name }}</h3>
|
||||||
<button class="fav-btn" @click="toggleFavorite">
|
<FavoriteButton
|
||||||
<span class="material-icons" :class="{ 'favorited': isFavorited }">
|
item-type="stop"
|
||||||
{{ isFavorited ? 'favorite' : 'favorite_border' }}
|
:item-id="busStop.id"
|
||||||
</span>
|
:item-name="busStop.name"
|
||||||
</button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="busStop.address" class="stop-address">
|
<p v-if="busStop.address" class="stop-address">
|
||||||
<span class="material-icons text-sm">location_on</span>
|
<span class="material-icons text-sm">location_on</span>
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { useFavoritesStore } from '@/stores/favorites'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
itemType: 'coupon' | 'business' | 'taxi' | 'route'
|
itemType: 'coupon' | 'business' | 'taxi' | 'route' | 'stop'
|
||||||
itemId: string
|
itemId: string
|
||||||
itemName?: string
|
itemName?: string
|
||||||
itemImage?: string
|
itemImage?: string
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { apiClient } from '@/services/apiClient'
|
|||||||
export interface Favorite {
|
export interface Favorite {
|
||||||
id: string
|
id: string
|
||||||
user_id: string
|
user_id: string
|
||||||
item_type: 'coupon' | 'business' | 'taxi' | 'route'
|
item_type: 'coupon' | 'business' | 'taxi' | 'route' | 'stop'
|
||||||
item_id: string
|
item_id: string
|
||||||
item_name?: string
|
item_name?: string
|
||||||
item_image?: string
|
item_image?: string
|
||||||
@ -21,6 +21,7 @@ export const useFavoritesStore = defineStore('favorites', () => {
|
|||||||
const businesses = computed(() => favorites.value.filter(f => f.item_type === 'business'))
|
const businesses = computed(() => favorites.value.filter(f => f.item_type === 'business'))
|
||||||
const taxis = computed(() => favorites.value.filter(f => f.item_type === 'taxi'))
|
const taxis = computed(() => favorites.value.filter(f => f.item_type === 'taxi'))
|
||||||
const routes = computed(() => favorites.value.filter(f => f.item_type === 'route'))
|
const routes = computed(() => favorites.value.filter(f => f.item_type === 'route'))
|
||||||
|
const stops = computed(() => favorites.value.filter(f => f.item_type === 'stop'))
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
async function loadFavorites() {
|
async function loadFavorites() {
|
||||||
@ -36,7 +37,7 @@ export const useFavoritesStore = defineStore('favorites', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function addFavorite(
|
async function addFavorite(
|
||||||
itemType: 'coupon' | 'business' | 'taxi' | 'route',
|
itemType: 'coupon' | 'business' | 'taxi' | 'route' | 'stop',
|
||||||
itemId: string,
|
itemId: string,
|
||||||
itemName?: string,
|
itemName?: string,
|
||||||
itemImage?: string
|
itemImage?: string
|
||||||
@ -74,7 +75,7 @@ export const useFavoritesStore = defineStore('favorites', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toggleFavorite(
|
async function toggleFavorite(
|
||||||
itemType: 'coupon' | 'business' | 'taxi' | 'route',
|
itemType: 'coupon' | 'business' | 'taxi' | 'route' | 'stop',
|
||||||
itemId: string,
|
itemId: string,
|
||||||
itemName?: string,
|
itemName?: string,
|
||||||
itemImage?: string
|
itemImage?: string
|
||||||
@ -105,6 +106,7 @@ export const useFavoritesStore = defineStore('favorites', () => {
|
|||||||
businesses,
|
businesses,
|
||||||
taxis,
|
taxis,
|
||||||
routes,
|
routes,
|
||||||
|
stops,
|
||||||
loadFavorites,
|
loadFavorites,
|
||||||
addFavorite,
|
addFavorite,
|
||||||
removeFavorite,
|
removeFavorite,
|
||||||
|
|||||||
@ -12,10 +12,11 @@ const selectedFilter = ref('all')
|
|||||||
|
|
||||||
const filters = [
|
const filters = [
|
||||||
{ key: 'all', label: 'Todos', icon: 'star' },
|
{ key: 'all', label: 'Todos', icon: 'star' },
|
||||||
{ key: 'routes', label: 'Rutas', icon: 'directions_bus' },
|
{ key: 'routes', label: 'Buses', icon: 'directions_bus' },
|
||||||
{ key: 'taxis', label: 'Taxis', icon: 'local_taxi' },
|
{ key: 'taxis', label: 'Taxis', icon: 'local_taxi' },
|
||||||
{ key: 'businesses',label: 'Negocios', icon: 'store' },
|
{ key: 'businesses',label: 'Comercios', icon: 'store' },
|
||||||
{ key: 'coupons', label: 'Eventos', icon: 'confirmation_number' },
|
{ key: 'coupons', label: 'Ofertas', icon: 'confirmation_number' },
|
||||||
|
{ key: 'stops', label: 'Paradas', icon: 'location_on' },
|
||||||
]
|
]
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@ -34,9 +35,11 @@ async function removeFavorite(event: Event, itemType: string, itemId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function navigateToItem(item: any) {
|
function navigateToItem(item: any) {
|
||||||
if (item.item_type === 'route') router.push('/schedules')
|
if (item.item_type === 'route') router.push({ path: '/schedules', query: { routeId: item.item_id } })
|
||||||
else if (item.item_type === 'taxi') router.push('/taxi')
|
else if (item.item_type === 'taxi') router.push('/taxi')
|
||||||
else if (item.item_type === 'business') router.push('/business/' + item.item_id)
|
else if (item.item_type === 'business') router.push('/business/' + item.item_id)
|
||||||
|
else if (item.item_type === 'coupon') router.push('/coupons')
|
||||||
|
else if (item.item_type === 'stop') router.push({ path: '/map', query: { stopId: item.item_id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleRoutes = computed(() =>
|
const visibleRoutes = computed(() =>
|
||||||
@ -51,11 +54,15 @@ const visibleBusinesses = computed(() =>
|
|||||||
const visibleCoupons = computed(() =>
|
const visibleCoupons = computed(() =>
|
||||||
(selectedFilter.value === 'all' || selectedFilter.value === 'coupons') ? favoritesStore.coupons : []
|
(selectedFilter.value === 'all' || selectedFilter.value === 'coupons') ? favoritesStore.coupons : []
|
||||||
)
|
)
|
||||||
|
const visibleStops = computed(() =>
|
||||||
|
(selectedFilter.value === 'all' || selectedFilter.value === 'stops') ? favoritesStore.stops : []
|
||||||
|
)
|
||||||
|
|
||||||
const totalFavorites = computed(() => favoritesStore.favorites.length)
|
const totalFavorites = computed(() => favoritesStore.favorites.length)
|
||||||
const hasVisibleItems = computed(() =>
|
const hasVisibleItems = computed(() =>
|
||||||
visibleRoutes.value.length + visibleTaxis.value.length +
|
visibleRoutes.value.length + visibleTaxis.value.length +
|
||||||
visibleBusinesses.value.length + visibleCoupons.value.length > 0
|
visibleBusinesses.value.length + visibleCoupons.value.length +
|
||||||
|
visibleStops.value.length > 0
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -206,20 +213,22 @@ const hasVisibleItems = computed(() =>
|
|||||||
<section v-if="visibleCoupons.length > 0" class="fav-section">
|
<section v-if="visibleCoupons.length > 0" class="fav-section">
|
||||||
<div class="section-label">
|
<div class="section-label">
|
||||||
<span class="material-icons">confirmation_number</span>
|
<span class="material-icons">confirmation_number</span>
|
||||||
<span>Eventos</span>
|
<span>Ofertas y Viajes</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-list">
|
<div class="card-list">
|
||||||
<div
|
<div
|
||||||
v-for="item in visibleCoupons"
|
v-for="item in visibleCoupons"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="card card--row"
|
class="card card--row"
|
||||||
|
@click="navigateToItem(item)"
|
||||||
>
|
>
|
||||||
<div class="card-thumb card-thumb--event">
|
<div class="card-thumb card-thumb--event">
|
||||||
<span class="material-icons">local_activity</span>
|
<img v-if="item.item_image" :src="getImageUrl(item.item_image)" class="thumb-img" />
|
||||||
|
<span v-else class="material-icons">local_activity</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-info">
|
<div class="card-info">
|
||||||
<p class="card-name">{{ item.item_name }}</p>
|
<p class="card-name">{{ item.item_name }}</p>
|
||||||
<span class="badge-avail">Disponible</span>
|
<span class="badge-avail">Cupón</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="heart-btn heart-btn--active" @click.stop="removeFavorite($event, item.item_type, item.item_id)" title="Quitar de favoritos">
|
<button class="heart-btn heart-btn--active" @click.stop="removeFavorite($event, item.item_type, item.item_id)" title="Quitar de favoritos">
|
||||||
<span class="material-icons">favorite</span>
|
<span class="material-icons">favorite</span>
|
||||||
@ -228,6 +237,33 @@ const hasVisibleItems = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ── PARADAS ── -->
|
||||||
|
<section v-if="visibleStops.length > 0" class="fav-section">
|
||||||
|
<div class="section-label">
|
||||||
|
<span class="material-icons">location_on</span>
|
||||||
|
<span>Paradas</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-list">
|
||||||
|
<div
|
||||||
|
v-for="item in visibleStops"
|
||||||
|
:key="item.id"
|
||||||
|
class="card card--row"
|
||||||
|
@click="navigateToItem(item)"
|
||||||
|
>
|
||||||
|
<div class="card-thumb card-thumb--blue">
|
||||||
|
<span class="material-icons">location_on</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<p class="card-name">{{ item.item_name }}</p>
|
||||||
|
<span class="badge-avail">Favorito</span>
|
||||||
|
</div>
|
||||||
|
<button class="heart-btn heart-btn--active" @click.stop="removeFavorite($event, item.item_type, item.item_id)">
|
||||||
|
<span class="material-icons">favorite</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { telemetryService } from "@/services/telemetryService";
|
|||||||
import { analyticsService } from "@/services/analyticsService";
|
import { analyticsService } from "@/services/analyticsService";
|
||||||
|
|
||||||
import BusStopInfoModal from "@/components/BusStopInfoModal.vue";
|
import BusStopInfoModal from "@/components/BusStopInfoModal.vue";
|
||||||
|
import FavoriteButton from "@/components/FavoriteButton.vue";
|
||||||
import type { BusStop } from '@/types'
|
import type { BusStop } from '@/types'
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -231,6 +232,19 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle stopId if present
|
||||||
|
const queryStopId = router.currentRoute.value.query.stopId as string;
|
||||||
|
if (queryStopId) {
|
||||||
|
await busStopStore.loadBusStops();
|
||||||
|
const foundStop = busStopStore.busStops.find(s => s.id === queryStopId);
|
||||||
|
if (foundStop) {
|
||||||
|
selectedBusStop.value = foundStop;
|
||||||
|
showBusStopModal.value = true;
|
||||||
|
setCenter(foundStop.latitude, foundStop.longitude);
|
||||||
|
setZoom(17);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for Google Maps to load
|
// Wait for Google Maps to load
|
||||||
if (isLoaded.value) {
|
if (isLoaded.value) {
|
||||||
await initializeMap();
|
await initializeMap();
|
||||||
@ -1141,6 +1155,14 @@ function clearNavigation() {
|
|||||||
<span v-if="currentPromo.discount_percentage" class="sheet-discount">
|
<span v-if="currentPromo.discount_percentage" class="sheet-discount">
|
||||||
-{{ currentPromo.discount_percentage }}%
|
-{{ currentPromo.discount_percentage }}%
|
||||||
</span>
|
</span>
|
||||||
|
<div class="sheet-fav-pos" @click.stop>
|
||||||
|
<FavoriteButton
|
||||||
|
item-type="coupon"
|
||||||
|
:item-id="currentPromo.id"
|
||||||
|
:item-name="currentPromo.title"
|
||||||
|
:item-image="currentPromo.image_url || undefined"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
@ -1192,6 +1214,14 @@ function clearNavigation() {
|
|||||||
<div class="promo-header-modal">
|
<div class="promo-header-modal">
|
||||||
<img :src="getImageUrl(selectedPromo.image_url)" class="promo-img-modal" />
|
<img :src="getImageUrl(selectedPromo.image_url)" class="promo-img-modal" />
|
||||||
<div class="promo-badge-modal">PROMO</div>
|
<div class="promo-badge-modal">PROMO</div>
|
||||||
|
<div class="promo-modal-fav">
|
||||||
|
<FavoriteButton
|
||||||
|
item-type="coupon"
|
||||||
|
:item-id="selectedPromo.id"
|
||||||
|
:item-name="selectedPromo.title"
|
||||||
|
:item-image="selectedPromo.image_url || undefined"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="promo-body-modal">
|
<div class="promo-body-modal">
|
||||||
<h2 class="promo-title-modal">{{ selectedPromo.title }}</h2>
|
<h2 class="promo-title-modal">{{ selectedPromo.title }}</h2>
|
||||||
@ -2155,4 +2185,17 @@ function clearNavigation() {
|
|||||||
.nav-destination { color: #e8eaed; }
|
.nav-destination { color: #e8eaed; }
|
||||||
.nav-btn-close { background: #3c4043; color: #bdc1c6; }
|
.nav-btn-close { background: #3c4043; color: #bdc1c6; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sheet-fav-pos {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.promo-modal-fav {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: 15px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -237,8 +237,14 @@ const getStatusClass = (status: string) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right flex flex-col items-end gap-2">
|
||||||
<p class="font-black text-xl text-primary">{{ taxi.shift }}</p>
|
<p class="font-black text-xl text-primary">{{ taxi.shift }}</p>
|
||||||
|
<FavoriteButton
|
||||||
|
item-type="taxi"
|
||||||
|
:item-id="taxi.id"
|
||||||
|
:item-name="taxi.owner_name"
|
||||||
|
:item-image="taxi.image_url || undefined"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|||||||
@ -308,6 +308,14 @@ function getShiftLabel(shift: string) {
|
|||||||
<span class="amount">{{ shuttle.price_per_person }}</span>
|
<span class="amount">{{ shuttle.price_per_person }}</span>
|
||||||
<span class="price-pill-label">/p</span>
|
<span class="price-pill-label">/p</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="shuttle-fav-wrap" @click.stop>
|
||||||
|
<FavoriteButton
|
||||||
|
item-type="route"
|
||||||
|
:item-id="shuttle.id"
|
||||||
|
:item-name="shuttle.route_name"
|
||||||
|
:item-image="shuttle.image_url || undefined"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shuttle-route-compact" v-if="shuttle.origin && shuttle.destination">
|
<div class="shuttle-route-compact" v-if="shuttle.origin && shuttle.destination">
|
||||||
|
|||||||
Reference in New Issue
Block a user