feat: replace map promo coupons with featured activities marker logic

This commit is contained in:
2026-03-18 18:54:39 -05:00
parent e2dff3fd5e
commit fd61cb7f87
5 changed files with 52 additions and 36 deletions

View File

@ -4,7 +4,7 @@
<!-- Header --> <!-- Header -->
<div class="sheet-header"> <div class="sheet-header">
<div class="sheet-title-group"> <div class="sheet-title-group">
<span class="sheet-title">{{ t('coupons.title') }}</span> <span class="sheet-title">Actividades </span>
</div> </div>
<button class="sheet-close" @click="$emit('close')"> <button class="sheet-close" @click="$emit('close')">
<span class="material-icons">close</span> <span class="material-icons">close</span>
@ -13,8 +13,8 @@
<!-- Card area with nav arrows wrapped in AuthGuard --> <!-- Card area with nav arrows wrapped in AuthGuard -->
<AuthGuard <AuthGuard
:title="t('coupons.auth.title') || 'Ofertas Exclusivas'" title="Explorar Actividad"
:message="t('coupons.auth.message') || 'Regístrate para ver todos los detalles de esta promoción y canjear tu código.'" message="¿Quieres ver más detalles de esta actividad o contactar al proveedor?"
> >
<div class="sheet-card-area"> <div class="sheet-card-area">
<button class="sheet-nav" @click="$emit('prev')" :disabled="coupons.length < 2"> <button class="sheet-nav" @click="$emit('prev')" :disabled="coupons.length < 2">
@ -26,18 +26,18 @@
v-if="currentPromo" v-if="currentPromo"
:key="currentPromo.id" :key="currentPromo.id"
class="sheet-card" class="sheet-card"
:style="{ backgroundImage: `url(${getImageUrl(currentPromo.image_url, 'coupon')})` }" :style="{ backgroundImage: `url(${getImageUrl(currentPromo.image_url, 'business')})` }"
@mouseenter="$emit('pause')" @mouseenter="$emit('pause')"
@touchstart="$emit('pause')" @touchstart="$emit('pause')"
@mouseleave="$emit('resume')" @mouseleave="$emit('resume')"
> >
<div class="sheet-card-overlay"> <div class="sheet-card-overlay">
<div class="sheet-info"> <div class="sheet-info">
<span class="sheet-biz-name">{{ currentPromo.business?.name || 'Local' }}</span> <span class="sheet-biz-name">{{ currentPromo.category || 'Recomendado' }} {{ currentPromo.area || 'Chiriquí' }}</span>
<h3 class="sheet-promo-title">{{ currentPromo.title }}</h3> <h3 class="sheet-promo-title">{{ currentPromo.name }}</h3>
<div class="sheet-actions"> <div class="sheet-actions">
<button class="sheet-cta" @click="$emit('promo-click', currentPromo)">{{ t('coupons.viewDetails') }}</button> <button class="sheet-cta" @click="$emit('promo-click', currentPromo)">Ver Detalles</button>
<span v-if="currentPromo.discount_percentage" class="sheet-discount-tag">-{{ currentPromo.discount_percentage }}%</span> <span v-if="currentPromo.supplier_name" class="sheet-discount-tag" style="background:#0F172A;">por {{ currentPromo.supplier_name }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -66,7 +66,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { getImageUrl } from '@/utils/imageUrl' import { getImageUrl } from '@/utils/imageUrl'
import AuthGuard from '@/components/common/AuthGuard.vue' import AuthGuard from '@/components/common/AuthGuard.vue'
@ -78,8 +77,6 @@ const props = defineProps<{
defineEmits(['close', 'prev', 'next', 'pause', 'resume', 'promo-click', 'update:index']) defineEmits(['close', 'prev', 'next', 'pause', 'resume', 'promo-click', 'update:index'])
const { t } = useI18n()
const currentPromo = computed(() => { const currentPromo = computed(() => {
if (props.coupons.length === 0) return null if (props.coupons.length === 0) return null
const idx = props.currentIndex % props.coupons.length const idx = props.currentIndex % props.coupons.length

View File

@ -2,7 +2,7 @@
import { supabase } from '@/supabase' import { supabase } from '@/supabase'
import type { Business } from '@/types' import type { Business } from '@/types'
const SELECT_FIELDS = 'id, name, address, phone, image_url, social_media, category, latitude, longitude, area, description, website, supplier_name, supplier_description, schedule, whatsapp, instagram, facebook, gallery_images, updated_at' const SELECT_FIELDS = 'id, name, address, phone, image_url, social_media, category, latitude, longitude, area, description, website, supplier_name, supplier_description, schedule, whatsapp, instagram, facebook, gallery_images, updated_at, is_featured'
export const businessService = { export const businessService = {
/** Helper to upload file to supabase storage */ /** Helper to upload file to supabase storage */
@ -44,8 +44,14 @@ export const businessService = {
// Comes as JSON string from the form // Comes as JSON string from the form
try { payload[key] = JSON.parse(value) } catch { /* ignore */ } try { payload[key] = JSON.parse(value) } catch { /* ignore */ }
} else if (value !== 'null' && value !== '' && key !== 'file' && key !== 'image') { } else if (value !== 'null' && value !== '' && key !== 'file' && key !== 'image') {
if (value === 'true') {
payload[key] = true
} else if (value === 'false') {
payload[key] = false
} else {
payload[key] = value payload[key] = value
} }
}
}) })
return { payload, fileUpload } return { payload, fileUpload }

View File

@ -102,6 +102,7 @@ export interface Business {
website?: string | null website?: string | null
supplier_name?: string | null // Name of the business/operator supplier_name?: string | null // Name of the business/operator
supplier_description?: string | null // Info about the business/operator supplier_description?: string | null // Info about the business/operator
is_featured?: boolean // To show on Map
// Template/Mold fields // Template/Mold fields
schedule?: string | null // "Lun-Sáb 8am-10pm" schedule?: string | null // "Lun-Sáb 8am-10pm"
whatsapp?: string | null // WhatsApp number whatsapp?: string | null // WhatsApp number

View File

@ -36,6 +36,7 @@ const businessForm = ref<Partial<Business>>({
facebook: '', facebook: '',
supplier_name: '', supplier_name: '',
supplier_description: '', supplier_description: '',
is_featured: false,
gallery_images: [] gallery_images: []
}); });
@ -122,6 +123,8 @@ async function saveBusiness() {
formData.append('facebook', businessForm.value.facebook || ''); formData.append('facebook', businessForm.value.facebook || '');
formData.append('supplier_name', businessForm.value.supplier_name || ''); formData.append('supplier_name', businessForm.value.supplier_name || '');
formData.append('supplier_description', businessForm.value.supplier_description || ''); formData.append('supplier_description', businessForm.value.supplier_description || '');
formData.append('is_featured', String(businessForm.value.is_featured === true));
if (businessForm.value.gallery_images?.length) { if (businessForm.value.gallery_images?.length) {
formData.append('gallery_images', JSON.stringify(businessForm.value.gallery_images)); formData.append('gallery_images', JSON.stringify(businessForm.value.gallery_images));
@ -231,6 +234,16 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
</div> </div>
</div> </div>
<div class="form-group checkbox-box" style="margin-top:-8px; margin-bottom:16px;">
<label style="display:flex; align-items:center; cursor:pointer;" class="text-white">
<input type="checkbox" v-model="businessForm.is_featured" style="margin-right: 8px; width: 18px; height: 18px;">
Destacar en el Mapa principal
</label>
<p style="font-size: 0.75rem; color: #94a3b8; margin-top: 4px; margin-left: 26px;">
Aparecerá en el recuadro flotante del mapa principal como una actividad recomendada.
</p>
</div>
<div class="form-group"> <div class="form-group">
<label>Descripción de la Actividad</label> <label>Descripción de la Actividad</label>
<textarea <textarea

View File

@ -4,7 +4,7 @@ import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRouteStore } from "@/stores/route"; import { useRouteStore } from "@/stores/route";
import { useMapStore } from "@/stores/map"; import { useMapStore } from "@/stores/map";
import { useCouponStore } from "@/stores/coupon"; import { businessService } from "@/services/businessService";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useGoogleMaps } from "@/composables/useGoogleMaps"; import { useGoogleMaps } from "@/composables/useGoogleMaps";
import { analyticsService } from "@/services/analyticsService"; import { analyticsService } from "@/services/analyticsService";
@ -29,8 +29,8 @@ const { t } = useI18n();
const routeStore = useRouteStore(); const routeStore = useRouteStore();
const mapStore = useMapStore(); const mapStore = useMapStore();
const couponStore = useCouponStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const featuredActivities = ref<any[]>([]);
const { map, isLoaded, error: mapsError, initMap, addHtmlMarker, setCenter, setZoom, addMarker, addCleanMarker } = useGoogleMaps(); const { map, isLoaded, error: mapsError, initMap, addHtmlMarker, setCenter, setZoom, addMarker, addCleanMarker } = useGoogleMaps();
const { estasCargando: estasCargandoRuta, errorRuta } = useDirectionsRoute(); const { estasCargando: estasCargandoRuta, errorRuta } = useDirectionsRoute();
@ -139,9 +139,8 @@ async function animateAndReload() {
}, 500); }, 500);
} }
function handlePromoClick(promo: any) { function handlePromoClick(act: any) {
selectedPromo.value = promo; router.push('/business/' + act.id);
showPromoModal.value = true;
} }
function closePromoModal() { function closePromoModal() {
@ -150,10 +149,12 @@ function closePromoModal() {
} }
async function fetchData(isBackground = false) { async function fetchData(isBackground = false) {
// Estas son actualizaciones asíncronas no urgentes para render principal
Promise.all([ Promise.all([
routeStore.loadRoutes(undefined, false, isBackground), routeStore.loadRoutes(undefined, false, isBackground),
couponStore.loadCoupons({ active_only: true }, isBackground) businessService.getAllBusinesses().then(b => {
featuredActivities.value = b.filter(biz => biz.is_featured && biz.latitude && biz.longitude);
if (showPromos.value) updatePromoMarkers();
})
]); ]);
// Update super-urgente, dibuja los buses actuales en el mapa al instante // Update super-urgente, dibuja los buses actuales en el mapa al instante
@ -381,18 +382,16 @@ async function updatePromoMarkers() {
promoMarkers.value.forEach(m => m.setMap(null)); promoMarkers.value.forEach(m => m.setMap(null));
const newMarkers: any[] = []; const newMarkers: any[] = [];
const promosWithCoords = couponStore.coupons.filter(c => const promosWithCoords = featuredActivities.value;
c.is_active && c.business && c.business.latitude && c.business.longitude
);
promosWithCoords.forEach(promo => { promosWithCoords.forEach(promo => {
const marker = addMarker( const marker = addMarker(
{ lat: promo.business!.latitude!, lng: promo.business!.longitude! }, { lat: promo.latitude!, lng: promo.longitude! },
{ {
title: promo.title, title: promo.name,
icon: { icon: {
path: "M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.54-2.5 1.35l-.5.65-.5-.65C10.96 2.54 10.05 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm11 15H4v-2h16v2zm0-5H4V8h5.08L7 10.83 8.62 12 11 8.76l1-1.36 1 1.36L15.38 12 17 10.83 14.92 8H20v6z", path: "M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z",
fillColor: '#FF4081', fillColor: '#FFD700',
fillOpacity: 1, fillOpacity: 1,
strokeColor: '#FFFFFF', strokeColor: '#FFFFFF',
strokeWeight: 2, strokeWeight: 2,
@ -414,8 +413,8 @@ async function updatePromoMarkers() {
function startCarousel() { function startCarousel() {
if (carouselTimer.value) clearInterval(carouselTimer.value); if (carouselTimer.value) clearInterval(carouselTimer.value);
carouselTimer.value = setInterval(() => { carouselTimer.value = setInterval(() => {
if (couponStore.coupons.length > 0) { if (featuredActivities.value.length > 0) {
currentCarouselIndex.value = (currentCarouselIndex.value + 1) % couponStore.coupons.length; currentCarouselIndex.value = (currentCarouselIndex.value + 1) % featuredActivities.value.length;
} }
}, 5000); }, 5000);
} }
@ -610,9 +609,9 @@ watch([() => authStore.userProfile?.auto_location, isLoaded], ([canLocate, loade
<div id="map" class="map" :style="{ display: isLoaded ? 'block' : 'none' }"></div> <div id="map" class="map" :style="{ display: isLoaded ? 'block' : 'none' }"></div>
<div class="map-floating-controls"> <div class="map-floating-controls">
<button v-if="isLoaded && !showPromos && couponStore.coupons.length > 0" class="offers-fab pulse" @click="showPromos = true"> <button v-if="isLoaded && !showPromos && featuredActivities.length > 0" class="offers-fab pulse" @click="showPromos = true">
<span class="material-icons">local_offer</span> <span class="material-icons">star</span>
<span v-if="couponStore.coupons.length > 0" class="offers-badge">{{ couponStore.coupons.length }}</span> <span v-if="featuredActivities.length > 0" class="offers-badge">{{ featuredActivities.length }}</span>
</button> </button>
<!-- SMART LOCATION BUTTON: Hidden by default if auto-location is active, shows up with text when map moved --> <!-- SMART LOCATION BUTTON: Hidden by default if auto-location is active, shows up with text when map moved -->
@ -662,12 +661,12 @@ watch([() => authStore.userProfile?.auto_location, isLoaded], ([canLocate, loade
<!-- COMPONENTIZED PROMOS --> <!-- COMPONENTIZED PROMOS -->
<PromoCarousel <PromoCarousel
:is-open="showPromos" :is-open="showPromos"
:coupons="couponStore.coupons" :coupons="featuredActivities"
:current-index="currentCarouselIndex" :current-index="currentCarouselIndex"
@update:index="currentCarouselIndex = $event" @update:index="currentCarouselIndex = $event"
@close="showPromos = false" @close="showPromos = false"
@prev="currentCarouselIndex = (currentCarouselIndex - 1 + couponStore.coupons.length) % couponStore.coupons.length" @prev="currentCarouselIndex = (currentCarouselIndex - 1 + featuredActivities.length) % featuredActivities.length"
@next="currentCarouselIndex = (currentCarouselIndex + 1) % couponStore.coupons.length" @next="currentCarouselIndex = (currentCarouselIndex + 1) % featuredActivities.length"
@pause="stopCarousel" @pause="stopCarousel"
@resume="startCarousel" @resume="startCarousel"
@promo-click="handlePromoClick" @promo-click="handlePromoClick"