diff --git a/Untitled.png b/Untitled.png new file mode 100644 index 0000000..9ccb722 Binary files /dev/null and b/Untitled.png differ diff --git a/client/src/components/EventChart.vue b/client/src/components/EventChart.vue index b181eb2..d37df64 100644 --- a/client/src/components/EventChart.vue +++ b/client/src/components/EventChart.vue @@ -4,13 +4,13 @@

Eventos y comparación

- {{ activeFilters.dataSource === 'active-rooms' ? '🔴 Tiempo Real' : '📁 Agregado' }} + {{ activeFilters?.dataSource === 'active-rooms' ? '🔴 Tiempo Real' : '📁 Agregado' }} - Round {{ activeFilters.round }} + Round {{ activeFilters?.round }} - {{ activeFilters.game }} + {{ activeFilters?.game }} 👤 {{ activeFilters.selectedPlayer }} @@ -68,13 +68,20 @@
+ +
+

{{ group.name }}

+ {{ group.total }} +
+ +
(), { @@ -139,48 +155,55 @@ const props = withDefaults(defineProps(), { const highlighted = ref(''); // Define ratio groups for superposed view -const ratioGroups = [ +const ratioGroups = computed(() => [ { name: 'Ofertas', actions: ['p1_propose', 'p1_no_offer'], - labels: ['Ofrecer', 'No Ofrecer'] + labels: ['Ofrecer', 'No Ofrecer'], + total: props.groupTotals?.offers || 0 }, { name: 'Respuestas', actions: ['p2_accept', 'p2_reject', 'p2_snatch'], - labels: ['Aceptar', 'Rechazar', 'Robar'] + labels: ['Aceptar', 'Rechazar', 'Robar'], + total: props.groupTotals?.responses || 0 }, { - name: 'Fuerzas', + name: 'Forzar', actions: ['p2_force', 'p2_no_force'], - labels: ['Forzar', 'No Forzar'] + labels: ['Forzar', 'No Forzar'], + total: props.groupTotals?.force || 0 }, { - name: 'Vergüenzas', + name: 'Avergonzar', actions: ['p1_shame', 'p1_no_shame'], - labels: ['Asignar', 'No Asignar'] + labels: ['Asignar', 'No Asignar'], + total: props.groupTotals?.shame || 0 }, { - name: 'Denuncias', + name: 'Denunciar', actions: ['p1_report', 'p1_no_report'], - labels: ['Denunciar', 'No Denunciar'] + labels: ['Denunciar', 'No Denunciar'], + total: props.groupTotals?.report || 0 }, { - name: 'Puntuaciones', + name: 'Puntaje Promedio', actions: ['score_p1', 'score_p2'], - labels: ['P1', 'P2'] + labels: ['P1', 'P2'], + total: props.groupTotals?.averageScore ? props.groupTotals.averageScore.toFixed(1) : '0.0' }, { - name: 'Jugadores', - actions: ['players_with_shame', 'players_seated'], + name: 'Total Jugadores', + actions: ['players_with_shame', 'players_without_shame'], labels: ['Con vergüenza', 'Sin vergüenza'], + total: props.groupTotals?.totalPlayers || 0, isCustomRatio: true // Special handling needed } -]; +]); // Compute ratio data for each group const ratioData = computed(() => { - return ratioGroups.map(group => { + return ratioGroups.value.map(group => { const counts = props.selectedPlayerUuid ? props.playerEventCounts : props.globalEventCounts; @@ -188,10 +211,9 @@ const ratioData = computed(() => { let values = group.actions.map(action => counts[action] || 0); // Special handling for players ratio (shame vs no shame) - if (group.isCustomRatio && group.name === 'Jugadores') { + if (group.isCustomRatio && group.name === 'Total Jugadores') { const playersWithShame = counts['players_with_shame'] || 0; - const totalPlayersSeated = counts['players_seated'] || 0; - const playersWithoutShame = Math.max(0, totalPlayersSeated - playersWithShame); + const playersWithoutShame = counts['players_without_shame'] || 0; values = [playersWithShame, playersWithoutShame]; } @@ -484,15 +506,14 @@ function friendlyEventName(eventType: string): string { .ratio-bars { display: flex; flex-direction: column; - gap: 16px; - flex: 1 1 auto; - min-height: 0; + gap: 8px; padding: 8px 0; } .ratio-group { - flex: 1 1 0; - min-height: 60px; + flex: 0 0 auto; + min-height: 120px; + margin-bottom: 8px; transition: transform .18s ease; } @@ -500,9 +521,45 @@ function friendlyEventName(eventType: string): string { transform: translateX(4px); } +/* Group header styles */ +.ratio-group-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 12px; + margin-bottom: 12px; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.6); + border: 1px solid rgba(229, 231, 235, 0.4); + border-radius: 10px; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} + +.group-title { + margin: 0; + font-size: 18px; + font-weight: 800; + color: #1e293b; + letter-spacing: -0.025em; +} + +.group-total { + font-size: 16px; + font-weight: 900; + color: #667eea; + background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(102, 126, 234, 0.08) 100%); + padding: 6px 12px; + border-radius: 999px; + border: 1px solid rgba(102, 126, 234, 0.25); + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15); + min-width: 48px; + text-align: center; +} + .ratio-bar { position: relative; - height: 100%; + height: 60px; background: linear-gradient(135deg, rgba(238,242,255,0.4) 0%, rgba(199,210,254,0.2) 100%); border-radius: 12px; overflow: hidden; diff --git a/client/src/views/Leaderboard.vue b/client/src/views/Leaderboard.vue index 2f5f372..8d8a149 100644 --- a/client/src/views/Leaderboard.vue +++ b/client/src/views/Leaderboard.vue @@ -11,9 +11,6 @@ -
@@ -107,16 +104,18 @@ :player-event-counts="combinedPlayerCounts" :selected-player-uuid="selectedUuid" :player-bar-gradient="playerBarGradient" - :view-mode="viewMode" + view-mode="ratio" :loading="loading" :filters-collapsed="filtersCollapsed" - :active-filters="{ - dataSource: eventFilters.dataSource.value, - round: eventFilters.roundFilter.value.toString(), - game: eventFilters.gameFilter.value, - hasFilters: eventFilters.hasActiveFilters.value || !!selectedRoomId, - selectedPlayer: selectedUuid ? players.find(p => p.uuid === selectedUuid)?.name || 'Jugador' : undefined, - selectedRoom: selectedRoomId ? availableRooms.find(r => r.roomId === selectedRoomId)?.name || 'Sala' : undefined + :active-filters="activeFilters" + :group-totals="{ + offers: offersTotal, + responses: responsesTotal, + force: forceTotal, + shame: shameTotal, + report: reportTotal, + averageScore: averageScoreTotal, + totalPlayers: totalPlayersCount }" />
@@ -136,25 +135,6 @@ const loading = ref(false); const eventFilters = useEventFilters(); const filtersCollapsed = ref(false); -// View mode cycling -type ViewMode = 'count' | 'percent' | 'ratio'; -const viewMode = ref('count'); - -const viewModeLabel = computed(() => { - switch (viewMode.value) { - case 'count': return 'Ver conteos'; - case 'percent': return 'Ver %'; - case 'ratio': return 'Ver superpuesto'; - default: return 'Ver conteos'; - } -}); - -function cycleViewMode() { - const modes: ViewMode[] = ['count', 'percent', 'ratio']; - const currentIndex = modes.indexOf(viewMode.value); - const nextIndex = (currentIndex + 1) % modes.length; - viewMode.value = modes[nextIndex]; -} const EVENTS = [ 'p1_propose', 'p1_no_offer', @@ -164,7 +144,7 @@ const EVENTS = [ // New metric types for additional charts const METRICS = [ - 'players_seated', 'score_p1', 'score_p2', 'players_with_shame' + 'players_seated', 'score_p1', 'score_p2', 'players_with_shame', 'players_without_shame' ]; const ALL_CHART_TYPES = [...EVENTS, ...METRICS]; @@ -188,91 +168,113 @@ const EVENT_STYLES: Record>({ players_seated: 0, score_p1: 0, score_p2: 0, - players_with_shame: 0 + players_with_shame: 0, + players_without_shame: 0 }); const selectedPlayerMetrics = ref>({ players_seated: 0, score_p1: 0, score_p2: 0, - players_with_shame: 0 + players_with_shame: 0, + players_without_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; +// Store room score history from players +const allPlayersWithScores = ref([]); + +// Function to compute additional metrics from players' score history +function computeMetricsFromScores() { + if (!allPlayersWithScores.value.length) return; - 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++; - } + let totalP1Scores = 0; + let totalP2Scores = 0; + let p1Count = 0; + let p2Count = 0; + let playersWithShame = 0; + let totalPlayersWithNames = 0; + + // Get score data from players with room score history + allPlayersWithScores.value.forEach((player: any) => { + if (player.name) { + totalPlayersWithNames++; + + // Extract scores from roomScoreHistory if available + if (player.roomScoreHistory) { + player.roomScoreHistory.forEach((roomScore: any) => { + roomScore.scores.forEach((score: any) => { + if (score.role === 'P1') { + totalP1Scores += score.score; + p1Count++; + } else if (score.role === 'P2') { + totalP2Scores += score.score; + p2Count++; + } + }); + }); } - }); + } }); + + const avgP1Score = p1Count > 0 ? totalP1Scores / p1Count : 0; + const avgP2Score = p2Count > 0 ? totalP2Scores / p2Count : 0; additionalMetrics.value = { - players_seated: playersSeated, - score_p1: totalP1Score, - score_p2: totalP2Score, - players_with_shame: playersWithShame + players_seated: totalPlayersWithNames, + score_p1: Math.round(avgP1Score * 10) / 10, // Round to 1 decimal + score_p2: Math.round(avgP2Score * 10) / 10, + players_with_shame: playersWithShame, + players_without_shame: totalPlayersWithNames - 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 +function computeSelectedPlayerMetrics(uuid: string) { + const playerData = allPlayersWithScores.value.find(p => p.uuid === uuid); + if (!playerData?.roomScoreHistory) { + selectedPlayerMetrics.value = { + players_seated: 1, + score_p1: 0, + score_p2: 0, + players_with_shame: 0, + players_without_shame: 0 + }; + return; + } + + let totalP1Scores = 0; + let totalP2Scores = 0; + let p1Count = 0; + let p2Count = 0; + + playerData.roomScoreHistory.forEach((roomScore: any) => { + roomScore.scores.forEach((score: any) => { + if (score.role === 'P1') { + totalP1Scores += score.score; + p1Count++; + } else if (score.role === 'P2') { + totalP2Scores += score.score; + p2Count++; + } + }); + }); + 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 + players_seated: 1, + score_p1: p1Count > 0 ? Math.round((totalP1Scores / p1Count) * 10) / 10 : 0, + score_p2: p2Count > 0 ? Math.round((totalP2Scores / p2Count) * 10) / 10 : 0, + players_with_shame: 0, // Would need shame data + players_without_shame: 0 }; } @@ -287,6 +289,74 @@ const combinedPlayerCounts = computed(() => ({ ...selectedPlayerMetrics.value })); + +// Chart group totals +const offersTotal = computed(() => { + const propose = eventFilters.globalEventCounts.value.p1_propose || 0; + const noOffer = eventFilters.globalEventCounts.value.p1_no_offer || 0; + return propose + noOffer; +}); + +const responsesTotal = computed(() => { + const accept = eventFilters.globalEventCounts.value.p2_accept || 0; + const reject = eventFilters.globalEventCounts.value.p2_reject || 0; + const snatch = eventFilters.globalEventCounts.value.p2_snatch || 0; + return accept + reject + snatch; +}); + +const forceTotal = computed(() => { + const force = eventFilters.globalEventCounts.value.p2_force || 0; + const noForce = eventFilters.globalEventCounts.value.p2_no_force || 0; + return force + noForce; +}); + +const shameTotal = computed(() => { + const shame = eventFilters.globalEventCounts.value.p1_shame || 0; + const noShame = eventFilters.globalEventCounts.value.p1_no_shame || 0; + return shame + noShame; +}); + +const reportTotal = computed(() => { + const report = eventFilters.globalEventCounts.value.p1_report || 0; + const noReport = eventFilters.globalEventCounts.value.p1_no_report || 0; + return report + noReport; +}); + +const averageScoreTotal = computed(() => { + if (!allPlayersWithScores.value.length) return 0; + + let totalScores = 0; + let totalScoreCount = 0; + + // Sum all individual scores from all players regardless of role + allPlayersWithScores.value.forEach((player: any) => { + if (player.roomScoreHistory) { + player.roomScoreHistory.forEach((roomScore: any) => { + roomScore.scores.forEach((score: any) => { + totalScores += score.score; + totalScoreCount++; + }); + }); + } + }); + + return totalScoreCount > 0 ? Math.round((totalScores / totalScoreCount) * 10) / 10 : 0; +}); + +const totalPlayersCount = computed(() => { + return additionalMetrics.value.players_seated || 0; +}); + +// Active filters object +const activeFilters = computed(() => ({ + dataSource: eventFilters.dataSource.value, + round: eventFilters.roundFilter.value.toString(), + game: eventFilters.gameFilter.value, + hasFilters: eventFilters.hasActiveFilters.value || !!selectedRoomId.value, + selectedPlayer: selectedUuid.value ? players.value.find(p => p.uuid === selectedUuid.value)?.name || 'Jugador' : undefined, + selectedRoom: selectedRoomId.value ? availableRooms.value.find(r => r.roomId === selectedRoomId.value)?.name || 'Sala' : undefined +})); + // Watch for changes in filters and data source watch([eventFilters.dataSource, eventFilters.roundFilter, eventFilters.gameFilter, eventFilters.roomFilter], () => { eventFilters.applyFilters(EVENTS); @@ -386,9 +456,6 @@ function initials(name: string): string { return chars || '🙂'; } -function shortRoomId(roomId: string): string { - return roomId.slice(0, 8); -} function selectPlayer(uuid: string) { if (selectedUuid.value === uuid) return; @@ -487,8 +554,8 @@ function setupStreams() { eventFilters.updateActiveRoomsData(detailedEvents, counts); - // Compute additional metrics - computeMetrics(details); + // Compute additional metrics from active rooms (for current UI compatibility) + // computeMetrics(details); // Removed since we now use score history // Don't extract rooms from active data - we'll get them from aggregated player history @@ -542,6 +609,9 @@ function setupStreams() { const list = Array.isArray(data?.players) ? data.players : []; allPlayersActions.value = list.map((p: any) => ({ uuid: String(p.uuid||''), name: String(p.name||''), total: Number(p.total||0) })); + // Store complete player data with room score history + allPlayersWithScores.value = list; + // Collect all detailed events from all players const allDetailedEvents: Array<{ kind: string; round?: number; gameVariant?: string; roomId?: string }> = []; @@ -586,6 +656,9 @@ function setupStreams() { playerCount: allDetailedEvents.filter(e => e.roomId === roomId).length })); + // Compute metrics from score history + computeMetricsFromScores(); + // Apply filters and update display if viewing aggregated data if (eventFilters.dataSource.value === 'aggregated') { eventFilters.applyFilters(EVENTS); @@ -1062,4 +1135,5 @@ function downloadCSV() { min-width: 30px; } } +