feat: Refinamiento del tracking de analiticas y actualizacion del dashboard admin

This commit is contained in:
2026-03-04 20:36:31 -05:00
parent c376627d39
commit 4e17613d49
12 changed files with 189 additions and 62 deletions

View File

@ -142,17 +142,27 @@
<table class="data-table">
<thead>
<tr>
<th>Ruta</th>
<th>Conversión</th>
<th>Ratio</th>
<th>Shuttle</th>
<th>Interés (Vistas)</th>
<th>Reservas & Llamadas</th>
<th>Ratio de Conversión</th>
</tr>
</thead>
<tbody>
<tr v-for="(data, id) in stats.shuttles" :key="id">
<td class="id-cell">{{ id }}</td>
<td>{{ calculateConversion(data.views, data.contacts) }}%</td>
<tr v-for="(data, name) in stats.shuttles" :key="name">
<td class="id-cell">{{ name }}</td>
<td>{{ data.views }}</td>
<td>
<div class="mini-bar"><div class="fill" :style="{ width: calculateConversion(data.views, data.contacts) + '%' }"></div></div>
<div style="display:flex; gap: 12px; align-items:center;">
<span title="WhatsApp" style="display:flex; align-items:center; gap:2px;"><span class="material-icons" style="font-size:14px; color:#25D366;">chat</span> {{ data.whatsapp }}</span>
<span title="Llamadas" style="display:flex; align-items:center; gap:2px;"><span class="material-icons" style="font-size:14px; color:#cbd5e1;">phone</span> {{ data.calls }}</span>
</div>
</td>
<td>
<div style="display:flex; flex-direction:column; gap:4px;">
<div class="mini-bar"><div class="fill" :style="{ width: calculateConversion(data.views, data.contacts) + '%' }"></div></div>
<span style="font-size: 0.65rem; color: #64748b; font-weight:700;">{{ calculateConversion(data.views, data.contacts) }}%</span>
</div>
</td>
</tr>
</tbody>
@ -202,15 +212,29 @@
<tr>
<th>Negocio</th>
<th>Visitas</th>
<th>Interacciones (R/LL/M)</th>
<th>Cupones</th>
<th>Favoritos</th>
<th>Salud</th>
</tr>
</thead>
<tbody>
<tr v-for="(data, id) in stats.businesses" :key="id">
<td class="id-cell">{{ id }}</td>
<tr v-for="(data, name) in stats.businesses" :key="name">
<td class="id-cell">{{ name }}</td>
<td>{{ data.views }}</td>
<td>
<div style="display:flex; gap:8px; font-size:0.75rem; color:#cbd5e1;">
<span title="Click en Redes Sociales">S: {{ data.social }}</span>
<span title="Llamadas Realizadas">L: {{ data.calls }}</span>
<span title="Visitas a Maps">M: {{ data.location }}</span>
</div>
</td>
<td>{{ data.promos }}</td>
<td>
<span style="display:flex; align-items:center; gap:2px;">
<span class="material-icons" style="font-size:14px; color:#e91e63;">favorite</span> {{ data.favs }}
</span>
</td>
<td>
<span class="status-pill" :class="getHealthClass(calculateConversion(data.views, data.promos))">
{{ 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; }
});
</script>