From ffd97646ab1b8cd9580aaae599a8b244f085a749 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Wed, 27 Aug 2025 23:05:02 -0600 Subject: [PATCH] nuevas metricas agregadas --- client/src/components/EventChart.vue | 22 +++- client/src/views/Leaderboard.vue | 159 +++++++++++++++++++++++++-- 2 files changed, 173 insertions(+), 8 deletions(-) diff --git a/client/src/components/EventChart.vue b/client/src/components/EventChart.vue index c8f8e03..0068d40 100644 --- a/client/src/components/EventChart.vue +++ b/client/src/components/EventChart.vue @@ -160,6 +160,17 @@ const ratioGroups = [ name: 'Denuncias', actions: ['p1_report', 'p1_no_report'], labels: ['Denunciar', 'No Denunciar'] + }, + { + name: 'Puntuaciones', + actions: ['score_p1', 'score_p2'], + labels: ['P1', 'P2'] + }, + { + name: 'Jugadores', + actions: ['players_with_shame', 'players_seated'], + labels: ['Con vergüenza', 'Sin vergüenza'], + isCustomRatio: true // Special handling needed } ]; @@ -170,7 +181,16 @@ const ratioData = computed(() => { ? props.playerEventCounts : props.globalEventCounts; - const values = group.actions.map(action => counts[action] || 0); + let values = group.actions.map(action => counts[action] || 0); + + // Special handling for players ratio (shame vs no shame) + if (group.isCustomRatio && group.name === 'Jugadores') { + const playersWithShame = counts['players_with_shame'] || 0; + const totalPlayersSeated = counts['players_seated'] || 0; + const playersWithoutShame = Math.max(0, totalPlayersSeated - playersWithShame); + values = [playersWithShame, playersWithoutShame]; + } + const total = values.reduce((sum, val) => sum + val, 0); return { diff --git a/client/src/views/Leaderboard.vue b/client/src/views/Leaderboard.vue index 422b809..b5e6bef 100644 --- a/client/src/views/Leaderboard.vue +++ b/client/src/views/Leaderboard.vue @@ -72,10 +72,10 @@ = { + // Event types 'p1_propose': { icon: '✨', color: '#667eea', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }, 'p1_no_offer': { icon: '❌', color: '#6b7280', gradient: 'linear-gradient(135deg, #94a3b8 0%, #64748b 100%)' }, 'p2_accept': { icon: '✓', color: '#10b981', gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)' }, @@ -144,14 +152,118 @@ const EVENT_STYLES: Record>({ + players_seated: 0, + score_p1: 0, + score_p2: 0, + players_with_shame: 0 +}); + +const selectedPlayerMetrics = ref>({ + players_seated: 0, + score_p1: 0, + score_p2: 0, + players_with_shame: 0 +}); + +// Function to compute additional metrics from room data +function computeMetrics(roomDetails: any) { + let playersSeated = 0; + let totalP1Score = 0; + let totalP2Score = 0; + let playersWithShame = 0; + + Object.values(roomDetails || {}).forEach((room: any) => { + const roomPlayers = room?.players || []; + roomPlayers.forEach((player: any) => { + // Count seated players (have a name) + if (player?.name && player.name.trim()) { + playersSeated++; + + const pavoTokens = player.pavoTokens || 0; + const eloteTokens = player.eloteTokens || 0; + const shameTokens = player.shameTokens || 0; + const role = player.role; + + // Add scores based on role + if (role === 'P1') { + totalP1Score += calculateP1Score(pavoTokens, eloteTokens); + } else if (role === 'P2') { + totalP2Score += calculateP2Score(pavoTokens, eloteTokens); + } + + // Count players with shame + if (shameTokens > 0) { + playersWithShame++; + } + } + }); + }); + + additionalMetrics.value = { + players_seated: playersSeated, + score_p1: totalP1Score, + score_p2: totalP2Score, + players_with_shame: playersWithShame + }; + + // Update selected player metrics if one is selected (using setTimeout to ensure selectedUuid is available) + setTimeout(() => { + if (selectedUuid.value) { + computeSelectedPlayerMetrics(selectedUuid.value); + } + }, 0); +} + +// Function to compute metrics for a selected player +function computeSelectedPlayerMetrics(uuid: string) { + // Individual player metrics are simpler - just show if they're seated, their score, etc. + // For now, set basic values. In a real implementation, we'd need to query current player state + selectedPlayerMetrics.value = { + players_seated: 1, // If player is selected, they're seated + score_p1: 0, // Would need current player data to calculate + score_p2: 0, // Would need current player data to calculate + players_with_shame: 0 // Would need current player data to determine + }; +} + +// Combined counts for charts (events + metrics) +const combinedGlobalCounts = computed(() => ({ + ...eventFilters.globalEventCounts.value, + ...additionalMetrics.value +})); + +const combinedPlayerCounts = computed(() => ({ + ...playerEventCounts.value, + ...selectedPlayerMetrics.value +})); + // Watch for changes in filters and data source watch([eventFilters.dataSource, eventFilters.roundFilter, eventFilters.gameFilter], () => { eventFilters.applyFilters(EVENTS); }); +// selectedUuid watch will be added after selectedUuid is declared + const rooms = ref([]); const players = ref<{ uuid: string; name: string; color?: string }[]>([]); const search = ref(''); @@ -192,6 +304,13 @@ const playerLoading = ref(false); const playerEventCounts = ref>((Object.fromEntries(EVENTS.map(k => [k, 0])) as Record)); const playersActionsByUuid = ref>>({}); +// Watch for selected player changes to update their metrics +watch(selectedUuid, (newUuid) => { + if (newUuid) { + computeSelectedPlayerMetrics(newUuid); + } +}); + function initials(name: string): string { const n = (name || '').trim(); if (!n) return '🙂'; @@ -273,6 +392,9 @@ function setupStreams() { eventFilters.updateActiveRoomsData(detailedEvents, counts); + // Compute additional metrics + computeMetrics(details); + // Apply filters and update display if (eventFilters.dataSource.value === 'active-rooms') { eventFilters.applyFilters(EVENTS); @@ -462,7 +584,7 @@ function downloadCSV() { const currentEvents = eventFilters.currentSourceEvents.value; // Create CSV headers - const headers = ['Event', 'Count', 'Round', 'GameVariant', 'PlayerUuid', 'PlayerName', 'DataSource']; + const headers = ['Type', 'Event', 'Count', 'Round', 'GameVariant', 'PlayerUuid', 'PlayerName', 'DataSource']; // Create CSV rows - one row per individual event occurrence const rows: string[][] = []; @@ -471,6 +593,7 @@ function downloadCSV() { currentEvents.forEach(event => { if (EVENTS.includes(event.kind)) { rows.push([ + 'event', event.kind, '1', event.round?.toString() || '', @@ -491,6 +614,7 @@ function downloadCSV() { // Add one row per occurrence for proper import compatibility for (let i = 0; i < count; i++) { rows.push([ + 'event', eventType, '1', '', // Round info not available in aggregated player data @@ -504,6 +628,27 @@ function downloadCSV() { }); }); + // Add metric data + const currentMetrics = eventFilters.dataSource.value === 'aggregated' + ? additionalMetrics.value + : additionalMetrics.value; // Same source for now + + METRICS.forEach(metricType => { + const count = currentMetrics[metricType] || 0; + if (count > 0) { + rows.push([ + 'metric', + metricType, + count.toString(), + '', + '', + '', + '', + eventFilters.dataSource.value + ]); + } + }); + // Convert to CSV string const csvContent = [ headers.join(','), @@ -520,7 +665,7 @@ function downloadCSV() { const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-'); const dataSourceLabel = eventFilters.dataSource.value === 'aggregated' ? 'agregados' : 'activos'; const filterLabel = eventFilters.hasActiveFilters.value ? `_${eventFilters.filterSummary.value.replace(/\s+/g, '_')}` : ''; - const filename = `eventos_${dataSourceLabel}${filterLabel}_${timestamp}.csv`; + const filename = `datos_${dataSourceLabel}${filterLabel}_${timestamp}.csv`; link.setAttribute('download', filename); link.style.visibility = 'hidden';