From 4e17613d492aaccf7f07343ec2de2727978b9a3c Mon Sep 17 00:00:00 2001 From: Hanzo_dev <2002samudiojohan@gmail.com> Date: Wed, 4 Mar 2026 20:36:31 -0500 Subject: [PATCH] feat: Refinamiento del tracking de analiticas y actualizacion del dashboard admin --- frontend/src/components/FavoriteButton.vue | 10 ++ frontend/src/services/analyticsService.ts | 55 ++++++-- frontend/src/views/BusStopDetailsView.vue | 5 +- frontend/src/views/BusinessDetailsView.vue | 15 ++- frontend/src/views/CouponsView.vue | 16 ++- frontend/src/views/DiscoverView.vue | 7 +- frontend/src/views/RoutesView.vue | 4 +- frontend/src/views/SchedulesView.vue | 4 +- frontend/src/views/StrategicAnalytics.vue | 117 +++++++++++++----- .../src/views/transporte/ShuttleDetalle.vue | 4 +- .../src/views/transporte/TaxisLocales.vue | 4 +- .../src/views/transporte/ViajesTuristicos.vue | 10 +- 12 files changed, 189 insertions(+), 62 deletions(-) diff --git a/frontend/src/components/FavoriteButton.vue b/frontend/src/components/FavoriteButton.vue index 6e14803..31d1369 100644 --- a/frontend/src/components/FavoriteButton.vue +++ b/frontend/src/components/FavoriteButton.vue @@ -15,6 +15,7 @@ import { ref, computed, watch } from 'vue' import { useFavoritesStore } from '@/stores/favorites' import { useAuthStore } from '@/stores/auth' +import { analyticsService } from '@/services/analyticsService' const props = defineProps<{ itemType: 'coupon' | 'business' | 'taxi' | 'route' | 'stop' | 'shuttle' @@ -59,6 +60,15 @@ async function handleToggle() { props.itemName, props.itemImage ) + + const isNowFavorited = favoritesStore.isFavorite(props.itemType, props.itemId) + const entityTypeMap: Record = { business: 'business', shuttle: 'shuttle', coupon: 'coupon', stop: 'stop', route: 'route' } + analyticsService.logEvent({ + event_name: isNowFavorited ? 'favorite_add' : 'favorite_remove', + entity_type: entityTypeMap[props.itemType] || 'other', + entity_id: props.itemId, + entity_name: props.itemName || 'item' + }) } catch (error) { console.error('Error toggling favorite:', error) } finally { diff --git a/frontend/src/services/analyticsService.ts b/frontend/src/services/analyticsService.ts index eb8a388..9e1b4f0 100644 --- a/frontend/src/services/analyticsService.ts +++ b/frontend/src/services/analyticsService.ts @@ -1,9 +1,48 @@ -/** analyticsService — stub. Analytics via Supabase can be implemented in v3 */ -export const analyticsService = { - logEvent(_event: any) { - // no-op - }, - async getDashboardStats() { - return null - } +import { supabase } from '@/supabase'; + +export interface AnalyticsEvent { + event_name: string; + entity_type?: 'business' | 'shuttle' | 'coupon' | 'stop' | 'route' | 'taxi' | 'system' | 'other'; + entity_id?: string; // The ID of the specific entity + entity_name?: string; // Optional name for easier querying + screen_name?: string; // Optional screen name + properties?: Record; } + +export const analyticsService = { + /** + * Logs an action or event to the analytics_events table. + */ + async logEvent(event: AnalyticsEvent) { + try { + const { data: userData } = await supabase.auth.getUser(); + + const payload = { + event_name: event.event_name, + entity_type: event.entity_type, + entity_id: event.entity_id, + entity_name: event.entity_name, + properties: event.properties || {}, + user_id: userData?.user ? userData.user.id : null + }; + + const { error } = await supabase + .from('analytics_events') + .insert(payload); + + if (error) { + console.warn('Analytics logging failed:', error); + } + } catch (e) { + console.warn('Failed to dispatch analytics event:', e); + } + }, + + /** + * Gets aggregated statistics for the Dashboard. + * Can be fleshed out with RPCs later if calculations are too heavy. + */ + async getDashboardStats() { + return null; + } +}; diff --git a/frontend/src/views/BusStopDetailsView.vue b/frontend/src/views/BusStopDetailsView.vue index a3779d0..9791b86 100644 --- a/frontend/src/views/BusStopDetailsView.vue +++ b/frontend/src/views/BusStopDetailsView.vue @@ -17,8 +17,9 @@ onMounted(async () => { if (busStopStore.selectedStop) { analyticsService.logEvent({ event_name: 'stop_selected', - item_id: busStopStore.selectedStop.name, - properties: { stop_id: stopId } + entity_type: 'stop', + entity_id: stopId, + entity_name: busStopStore.selectedStop.name, }) } } diff --git a/frontend/src/views/BusinessDetailsView.vue b/frontend/src/views/BusinessDetailsView.vue index 1107e37..2901619 100644 --- a/frontend/src/views/BusinessDetailsView.vue +++ b/frontend/src/views/BusinessDetailsView.vue @@ -48,9 +48,10 @@ async function fetchData() { business.value = bizData coupons.value = allCoupons.filter(c => c.business_id === id) analyticsService.logEvent({ - event_name: 'screen_view', - screen_name: 'BusinessDetails', - item_id: bizData.name, + event_name: 'view_details', + entity_type: 'business', + entity_id: bizData.id, + entity_name: bizData.name, properties: { business_id: id } }) } catch (e) { @@ -71,6 +72,7 @@ const goBack = () => router.back() function openMaps() { if (!business.value) return + analyticsService.logEvent({ event_name: 'location_click', entity_type: 'business', entity_id: business.value.id, entity_name: business.value.name }) if (business.value.latitude && business.value.longitude) { window.open(`https://www.google.com/maps/dir/?api=1&destination=${business.value.latitude},${business.value.longitude}`, '_blank') } else if (business.value.address) { @@ -80,6 +82,7 @@ function openMaps() { function callPhone() { if (business.value?.phone) { + analyticsService.logEvent({ event_name: 'contact_click', entity_type: 'business', entity_id: business.value.id, entity_name: business.value.name, properties: { channel: 'phone' } }) window.location.href = `tel:${business.value.phone}` } } @@ -87,20 +90,20 @@ function callPhone() { function openWhatsApp() { if (!business.value?.whatsapp) return const num = business.value.whatsapp.replace(/\D/g, '') - analyticsService.logEvent({ event_name: 'business_whatsapp_click', item_id: business.value.name }) + analyticsService.logEvent({ event_name: 'social_click', entity_type: 'business', entity_id: business.value.id, entity_name: business.value.name, properties: { platform: 'whatsapp' } }) window.open(`https://wa.me/${num}`, '_blank') } function openInstagram() { if (!business.value?.instagram) return const handle = business.value.instagram.replace('@', '') - analyticsService.logEvent({ event_name: 'business_instagram_click', item_id: business.value.name }) + analyticsService.logEvent({ event_name: 'social_click', entity_type: 'business', entity_id: business.value.id, entity_name: business.value.name, properties: { platform: 'instagram' } }) window.open(`https://instagram.com/${handle}`, '_blank') } function openFacebook() { if (!business.value?.facebook) return - analyticsService.logEvent({ event_name: 'business_facebook_click', item_id: business.value.name }) + analyticsService.logEvent({ event_name: 'social_click', entity_type: 'business', entity_id: business.value.id, entity_name: business.value.name, properties: { platform: 'facebook' } }) const fb = business.value.facebook window.open(fb.startsWith('http') ? fb : `https://facebook.com/${fb}`, '_blank') } diff --git a/frontend/src/views/CouponsView.vue b/frontend/src/views/CouponsView.vue index 2795d68..e53011d 100644 --- a/frontend/src/views/CouponsView.vue +++ b/frontend/src/views/CouponsView.vue @@ -50,19 +50,23 @@ function openCoupon(coupon: Coupon) { showRedeemModal.value = true analyticsService.logEvent({ event_name: 'promo_view', - item_id: coupon.title, - properties: { coupon_id: coupon.id, business: coupon.business?.name } + entity_type: 'coupon', + entity_id: coupon.id, + entity_name: coupon.title, + properties: { business: coupon.business?.name } }) } function handleDirections() { if (!selectedCoupon.value) return analyticsService.logEvent({ - event_name: 'promo_click', - item_id: 'directions_' + selectedCoupon.value.business?.name, + event_name: 'location_click', + entity_type: 'coupon', + entity_id: selectedCoupon.value.id, + entity_name: selectedCoupon.value.title, properties: { - coupon_id: selectedCoupon.value.id, - action: 'get_directions' + action: 'get_directions', + business: selectedCoupon.value.business?.name } }) window.open(`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(selectedCoupon.value.business?.address || selectedCoupon.value.business?.name || '')}`, '_blank') diff --git a/frontend/src/views/DiscoverView.vue b/frontend/src/views/DiscoverView.vue index b451ebf..ebee682 100644 --- a/frontend/src/views/DiscoverView.vue +++ b/frontend/src/views/DiscoverView.vue @@ -118,7 +118,12 @@ const isFiltering = computed(() => ) function handleExplore(biz: Business) { - analyticsService.logEvent({ event_name: 'promo_click', item_id: biz.name, properties: { business_id: biz.id } }) + analyticsService.logEvent({ + event_name: 'view_details', + entity_type: 'business', + entity_id: biz.id, + entity_name: biz.name + }) router.push('/business/' + biz.id) } diff --git a/frontend/src/views/RoutesView.vue b/frontend/src/views/RoutesView.vue index 85ecf66..ae1f01c 100644 --- a/frontend/src/views/RoutesView.vue +++ b/frontend/src/views/RoutesView.vue @@ -57,7 +57,9 @@ const handleTaxiFilter = async () => { const goToSchedules = (route: any) => { analyticsService.logEvent({ event_name: 'route_selected', - item_id: route.name, + entity_type: 'route', + entity_id: route.id, + entity_name: route.name, properties: { route_id: route.id } }) routeStore.selectRoute(route.id, route.name) diff --git a/frontend/src/views/SchedulesView.vue b/frontend/src/views/SchedulesView.vue index 7029e2e..b8c1ac6 100644 --- a/frontend/src/views/SchedulesView.vue +++ b/frontend/src/views/SchedulesView.vue @@ -126,7 +126,9 @@ function pickRoute(id: string, name: string) { dropdownOpen.value = false analyticsService.logEvent({ event_name: 'schedule_viewed', - item_id: name, + entity_type: 'route', + entity_id: id, + entity_name: name, properties: { route_id: id } }) diff --git a/frontend/src/views/StrategicAnalytics.vue b/frontend/src/views/StrategicAnalytics.vue index 98d2513..95d0a83 100644 --- a/frontend/src/views/StrategicAnalytics.vue +++ b/frontend/src/views/StrategicAnalytics.vue @@ -142,17 +142,27 @@ - - - + + + + - - - + + + + @@ -202,15 +212,29 @@ + + - - + + + +
RutaConversiónRatioShuttleInterés (Vistas)Reservas & LlamadasRatio de Conversión
{{ id }}{{ calculateConversion(data.views, data.contacts) }}%
{{ name }}{{ data.views }} -
+
+ chat {{ data.whatsapp }} + phone {{ data.calls }} +
+
+
+
+ {{ calculateConversion(data.views, data.contacts) }}% +
Negocio VisitasInteracciones (R/LL/M) CuponesFavoritos Salud
{{ id }}
{{ name }} {{ data.views }} +
+ S: {{ data.social }} + L: {{ data.calls }} + M: {{ data.location }} +
+
{{ data.promos }} + + favorite {{ data.favs }} + + {{ getHealthLabel(calculateConversion(data.views, data.promos)) }} @@ -395,49 +419,78 @@ const getHealthLabel = (rate: any) => (parseFloat(rate) > 20 ? 'Alta' : parseFlo onMounted(async () => { try { - // Load all data in parallel const [ { count: userCount }, - { data: shuttles }, - { data: routes }, - { data: businesses } + { data: events } ] = await Promise.all([ supabase.from('users').select('*', { count: 'exact', head: true }).eq('is_active', true), - supabase.from('shuttles').select('id, route_name'), - supabase.from('routes').select('id, name'), - supabase.from('businesses').select('id, name') + // In a production app with >1M rows we might use a group-by RPC + supabase.from('analytics_events').select('*') ]) const shuttleStats: any = {} - for (const s of (shuttles || [])) { - shuttleStats[s.route_name || s.id] = { views: Math.floor(Math.random() * 100), contacts: Math.floor(Math.random() * 20) } - } - - const routeStats: any = {} - for (const r of (routes || [])) { - routeStats[r.name || r.id] = { views: Math.floor(Math.random() * 80), contacts: Math.floor(Math.random() * 15) } - } - const bizStats: any = {} - for (const b of (businesses || [])) { - bizStats[b.name || b.id] = { views: Math.floor(Math.random() * 60), promos: Math.floor(Math.random() * 10) } + let total_promo_clicks = 0 + let total_shuttle_contacts = 0 + let total_biz_views = 0 + + const safeRows = events || [] + + for (const ev of safeRows) { + const nameKey = ev.entity_name || ev.entity_id + + if (ev.entity_type === 'shuttle') { + if (!shuttleStats[nameKey]) { + shuttleStats[nameKey] = { views: 0, contacts: 0, calls: 0, whatsapp: 0 } + } + + if (ev.event_name === 'view_details') { + shuttleStats[nameKey].views++ + } else if (ev.event_name === 'shuttle_contact') { + shuttleStats[nameKey].contacts++ + total_shuttle_contacts++ + if (ev.properties?.action === 'whatsapp') shuttleStats[nameKey].whatsapp++ + if (ev.properties?.action === 'call') shuttleStats[nameKey].calls++ + } + + } else if (ev.entity_type === 'business') { + if (!bizStats[nameKey]) { + bizStats[nameKey] = { views: 0, promos: 0, favs: 0, social: 0, location: 0, calls: 0 } + } + + if (ev.event_name === 'view_details') { + bizStats[nameKey].views++ + total_biz_views++ + } else if (ev.event_name === 'favorite_add') { + bizStats[nameKey].favs++ + } else if (ev.event_name === 'social_click') { + bizStats[nameKey].social++ + } else if (ev.event_name === 'location_click') { + bizStats[nameKey].location++ + } else if (ev.event_name === 'contact_click') { + bizStats[nameKey].calls++ + } else if (ev.event_name === 'promo_click') { + bizStats[nameKey].promos++ + total_promo_clicks++ + } + } } stats.value = { shuttles: shuttleStats, businesses: bizStats, - top_stops: [], + top_stops: [], // Legacy logic can still go here if stops tracking is added users: { registered_active: userCount || 0, patterns: { registered: {}, guests: {} } }, summary: { - total_shuttle_contacts: Object.values(shuttleStats).reduce((a: any, v: any) => a + v.contacts, 0), - total_promo_clicks: Object.values(bizStats).reduce((a: any, v: any) => a + v.promos, 0), - total_biz_views: Object.values(bizStats).reduce((a: any, v: any) => a + v.views, 0) + total_shuttle_contacts, + total_promo_clicks, + total_biz_views } } - } catch (error) { console.error(error); } finally { loading.value = false; } + } catch (error) { console.error('Error fetching analytics:', error); } finally { loading.value = false; } }); diff --git a/frontend/src/views/transporte/ShuttleDetalle.vue b/frontend/src/views/transporte/ShuttleDetalle.vue index 47b3d13..4cea9dc 100644 --- a/frontend/src/views/transporte/ShuttleDetalle.vue +++ b/frontend/src/views/transporte/ShuttleDetalle.vue @@ -195,7 +195,7 @@ const getTripTypeLabel = (type: string) => { :href="`https://wa.me/${shuttle.contact_whatsapp.replace(/\+/g, '')}?text=Hola,%20me%20gustaría%20información%20sobre%20el%20shuttle%20de%20${shuttle.origin}%20a%20${shuttle.destination}`" target="_blank" class="flex justify-center items-center gap-2 p-3.5 bg-[#25D366] text-white rounded-xl font-bold hover:opacity-90 transition active:scale-95" - @click="analyticsService.logEvent({ event_name: 'shuttle_contact', item_id: shuttle.id, properties: { action: 'whatsapp' } })" + @click="analyticsService.logEvent({ event_name: 'shuttle_contact', entity_type: 'shuttle', entity_id: shuttle.id, entity_name: shuttle.company_name || 'shuttle', properties: { action: 'whatsapp' } })" > chat {{ t('shuttle.bookWhatsapp') }} @@ -204,7 +204,7 @@ const getTripTypeLabel = (type: string) => { phone_in_talk {{ t('shuttle.callOperator') }} diff --git a/frontend/src/views/transporte/TaxisLocales.vue b/frontend/src/views/transporte/TaxisLocales.vue index 3982fa0..f102fcd 100644 --- a/frontend/src/views/transporte/TaxisLocales.vue +++ b/frontend/src/views/transporte/TaxisLocales.vue @@ -63,7 +63,9 @@ const getShiftsDisplay = (taxi: Taxi) => { const handleCall = (taxi: Taxi) => { analyticsService.logEvent({ event_name: 'taxi_click', - item_id: taxi.owner_name, + entity_type: 'taxi', + entity_id: taxi.id, + entity_name: taxi.owner_name, properties: { action: 'call', taxi_id: taxi.id, diff --git a/frontend/src/views/transporte/ViajesTuristicos.vue b/frontend/src/views/transporte/ViajesTuristicos.vue index fda9d6a..a742651 100644 --- a/frontend/src/views/transporte/ViajesTuristicos.vue +++ b/frontend/src/views/transporte/ViajesTuristicos.vue @@ -23,7 +23,13 @@ const filteredShuttles = computed(() => { }) }) -const verDetalle = (shuttleId: string) => { +const verDetalle = (shuttleId: string, shuttleName: string) => { + analyticsService.logEvent({ + event_name: 'view_details', + entity_type: 'shuttle', + entity_id: shuttleId, + entity_name: shuttleName + }) router.push({ name: 'ShuttleDetalle', params: { id: shuttleId } @@ -118,7 +124,7 @@ onUnmounted(() => { :key="shuttle.id" v-memo="[shuttle.id]" class="shuttle-card-premium glass-effect" - @click="verDetalle(shuttle.id)" + @click="verDetalle(shuttle.id, shuttle.company_name || `${shuttle.origin}-${shuttle.destination}`)" >