diff --git a/client/src/components/EventChart.vue b/client/src/components/EventChart.vue index 4d7e9c0..fd1bd43 100644 --- a/client/src/components/EventChart.vue +++ b/client/src/components/EventChart.vue @@ -566,6 +566,15 @@ function friendlyEventName(eventType: string): string { text-align: center; } +/* Place the group total just next to the title */ +.ratio-card .card-header { + justify-content: flex-start; + gap: 8px; +} +.ratio-card .group-total { + margin-left: 6px; +} + .ratio-bar { position: relative; height: 60px; diff --git a/client/src/components/EventFilters.vue b/client/src/components/EventFilters.vue index a32eed5..8e35772 100644 --- a/client/src/components/EventFilters.vue +++ b/client/src/components/EventFilters.vue @@ -5,8 +5,8 @@
- +
@@ -87,15 +87,15 @@ v-for="r in roomsPage" :key="r.roomId" class="chip room-chip" - :class="{ active: r.roomId === selectedRoomId }" - @click="selectRoom(r.roomId)" + :class="{ active: selectedRoomIds.includes(r.roomId) }" + @click="toggleRoom(r.roomId)" :title="`Sala: ${r.roomId} (${r.playerCount || 0} jugadores)`" > 🏠 {{ r.name }} {{ r.playerCount }} - + @@ -106,7 +106,7 @@ :event-styles="EVENT_STYLES" :global-event-counts="combinedGlobalCounts" :player-event-counts="combinedPlayerCounts" - :selected-player-uuid="selectedUuid" + :selected-player-uuid="selectedUuids.length ? selectedUuids[0] : ''" :player-bar-gradient="playerBarGradient" view-mode="ratio" :loading="loading" @@ -201,21 +201,18 @@ const allPlayersWithScores = ref([]); // Function to check if a score passes the current filters function scorePassesFilters(score: any, roomId: string) { - // Room filter - if (eventFilters.roomFilter.value !== 'all' && roomId !== eventFilters.roomFilter.value) { - return false; - } - - // Round filter - if (eventFilters.roundFilter.value !== 'all' && score.round !== eventFilters.roundFilter.value) { - return false; - } - - // Game variant filter - if (eventFilters.gameFilter.value !== 'all' && score.variant !== eventFilters.gameFilter.value) { - return false; - } - + // Room filter (array): empty means all + const rf = eventFilters.roomFilter.value; + if (rf.length && !rf.includes(String(roomId))) return false; + + // Round filter (array) + const rds = eventFilters.roundFilter.value; + if (rds.length && !rds.includes(Number(score.round))) return false; + + // Game variant filter (array) + const gfs = eventFilters.gameFilter.value; + if (gfs.length && !gfs.includes(String(score.variant))) return false; + return true; } @@ -274,16 +271,16 @@ function computeMetricsFromScores() { }; } -// Function to compute metrics for a selected player -function computeSelectedPlayerMetrics(uuid: string) { - const playerData = allPlayersWithScores.value.find(p => p.uuid === uuid); - if (!playerData) { +// Function to compute metrics for selected players (multi-select) +function computeSelectedPlayersMetrics() { + const uuids = selectedUuids.value; + if (!uuids.length) { selectedPlayerMetrics.value = { - players_seated: 1, + players_seated: 0, score_p1: 0, score_p2: 0, players_with_shame: 0, - players_without_shame: 1 + players_without_shame: 0 }; return; } @@ -292,34 +289,29 @@ function computeSelectedPlayerMetrics(uuid: string) { let totalP2Scores = 0; let p1Count = 0; let p2Count = 0; - - const hasShame = playerData.shameTokens && playerData.shameTokens > 0; + let playersWithShame = 0; - if (playerData.roomScoreHistory) { - playerData.roomScoreHistory.forEach((roomScore: any) => { - roomScore.scores.forEach((score: any) => { - // Apply filters to scores - if (!scorePassesFilters(score, roomScore.roomId)) { - return; - } - - if (score.role === 'P1') { - totalP1Scores += score.score; - p1Count++; - } else if (score.role === 'P2') { - totalP2Scores += score.score; - p2Count++; - } + uuids.forEach(uuid => { + const playerData = allPlayersWithScores.value.find(p => p.uuid === uuid); + if (!playerData) return; + if (playerData.shameTokens && playerData.shameTokens > 0) playersWithShame++; + if (playerData.roomScoreHistory) { + playerData.roomScoreHistory.forEach((roomScore: any) => { + roomScore.scores.forEach((score: any) => { + if (!scorePassesFilters(score, roomScore.roomId)) return; + if (score.role === 'P1') { totalP1Scores += score.score; p1Count++; } + else if (score.role === 'P2') { totalP2Scores += score.score; p2Count++; } + }); }); - }); - } + } + }); selectedPlayerMetrics.value = { - players_seated: 1, + players_seated: uuids.length, 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: hasShame ? 1 : 0, - players_without_shame: hasShame ? 0 : 1 + players_with_shame: playersWithShame, + players_without_shame: uuids.length - playersWithShame }; } @@ -400,11 +392,11 @@ const totalPlayersCount = computed(() => { // 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 + round: eventFilters.roundFilter.value.join(',') || 'all', + game: eventFilters.gameFilter.value.join(',') || 'all', + hasFilters: eventFilters.hasActiveFilters.value || selectedRoomIds.value.length > 0, + selectedPlayer: selectedUuids.value.length ? `${selectedUuids.value.length} jugadores` : undefined, + selectedRoom: selectedRoomIds.value.length ? `${selectedRoomIds.value.length} salas` : undefined })); // Watch for changes in filters and data source @@ -449,7 +441,7 @@ const containerWidth = ref(1200); // Default width, will be updated dynamically const dynamicPageSize = computed(() => { // Estimate space needed per chip and controls const chipWidth = 140; // Average chip width including gap - const clearBtnWidth = selectedUuid.value ? 150 : 0; // "Quitar selección" button + const clearBtnWidth = selectedUuids.value.length ? 150 : 0; // "Quitar selección" button const searchWidth = 240; // Search input const paginationWidth = pageCount.value > 1 ? 100 : 0; // Compact pagination const margin = 40; // Container margins @@ -463,7 +455,7 @@ const dynamicPageSize = computed(() => { const roomDynamicPageSize = computed(() => { // Similar calculation for rooms const chipWidth = 160; // Room chips might be slightly wider - const clearBtnWidth = selectedRoomId.value ? 150 : 0; + const clearBtnWidth = selectedRoomIds.value.length ? 150 : 0; const searchWidth = 240; const paginationWidth = roomPageCount.value > 1 ? 100 : 0; const margin = 40; @@ -487,18 +479,17 @@ const roomsPage = computed(() => { return roomsFiltered.value.slice(start, start + roomDynamicPageSize.value); }); -const selectedUuid = ref(''); -const selectedRoomId = ref(''); +const selectedUuids = ref([]); +const selectedRoomIds = ref([]); 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); - } -}); +// Watch for selected players changes to update their metrics +watch(selectedUuids, () => { + computeSelectedPlayersMetrics(); + updateSelectedPlayersCounts(); +}, { deep: true }); function initials(name: string): string { const n = (name || '').trim(); @@ -509,26 +500,29 @@ function initials(name: string): string { } -function selectPlayer(uuid: string) { - if (selectedUuid.value === uuid) return; - selectedUuid.value = uuid; - loadPlayerHistory(); +function togglePlayer(uuid: string) { + const idx = selectedUuids.value.indexOf(uuid); + if (idx >= 0) selectedUuids.value.splice(idx, 1); + else selectedUuids.value.push(uuid); + updateSelectedPlayersCounts(); } -function clearPlayer() { - selectedUuid.value = ''; +function clearPlayers() { + selectedUuids.value = []; + updateSelectedPlayersCounts(); } -function selectRoom(roomId: string) { - if (selectedRoomId.value === roomId) return; - selectedRoomId.value = roomId; - eventFilters.roomFilter.value = roomId; +function toggleRoom(roomId: string) { + const idx = selectedRoomIds.value.indexOf(roomId); + if (idx >= 0) selectedRoomIds.value.splice(idx, 1); + else selectedRoomIds.value.push(roomId); + eventFilters.roomFilter.value = [...selectedRoomIds.value]; eventFilters.applyFilters(EVENTS); } -function clearRoom() { - selectedRoomId.value = ''; - eventFilters.roomFilter.value = 'all'; +function clearRooms() { + selectedRoomIds.value = []; + eventFilters.roomFilter.value = []; eventFilters.applyFilters(EVENTS); } @@ -555,11 +549,7 @@ watch(roomPageCount, (newCount) => { }); // Dynamic per-player overlay bar gradient and label color -const playerBarGradient = computed(() => { - const p = players.value.find(x => x.uuid === selectedUuid.value); - const c = p?.color || '#667eea'; - return `linear-gradient(90deg, ${c}, ${c})`; -}); +const playerBarGradient = computed(() => '#8b5cf6'); @@ -720,10 +710,8 @@ function setupStreams() { eventFilters.applyFilters(EVENTS); } // If a player is selected, update playerEventCounts live - if (selectedUuid.value) { - const counts = byUuid[selectedUuid.value]; - if (counts) playerEventCounts.value = counts as any; - } + // Update selected players combined counts + updateSelectedPlayersCounts(); // Merge names into players list; preserve colors from previous streams const existing = new Map(); players.value.forEach(p => existing.set(p.uuid, { name: p.name, color: p.color })); @@ -782,13 +770,14 @@ async function refreshAll() { } } -async function loadPlayerHistory() { +function updateSelectedPlayersCounts() { playerLoading.value = true; const next: Record = Object.fromEntries(EVENTS.map(k => [k, 0])) as any; - const counts = playersActionsByUuid.value[selectedUuid.value || '']; - if (counts) { - EVENTS.forEach(k => { next[k] = Number(counts[k] || 0); }); - } + const byUuid = playersActionsByUuid.value; + selectedUuids.value.forEach(uuid => { + const counts = byUuid[uuid] || {}; + EVENTS.forEach(k => { next[k] = (next[k] || 0) + Number(counts[k] || 0); }); + }); playerEventCounts.value = next as any; playerLoading.value = false; } @@ -835,14 +824,8 @@ function downloadJSON() { game: eventFilters.gameFilter.value, hasActiveFilters: eventFilters.hasActiveFilters.value, filterSummary: eventFilters.filterSummary.value, - selectedPlayer: selectedUuid.value ? { - uuid: selectedUuid.value, - name: players.value.find(p => p.uuid === selectedUuid.value)?.name || 'Jugador' - } : null, - selectedRoom: selectedRoomId.value ? { - roomId: selectedRoomId.value, - name: availableRooms.value.find(r => r.roomId === selectedRoomId.value)?.name || 'Sala' - } : null + selectedPlayers: selectedUuids.value, + selectedRooms: selectedRoomIds.value } }, @@ -861,17 +844,17 @@ function downloadJSON() { globalEventCounts: eventFilters.globalEventCounts.value, // Selected player event counts (if applicable) - selectedPlayerEventCounts: selectedUuid.value ? playerEventCounts.value : null, + selectedPlayersEventCounts: selectedUuids.value.length ? playerEventCounts.value : null, // Additional metrics being displayed additionalMetrics: additionalMetrics.value, // Selected player metrics (if applicable) - selectedPlayerMetrics: selectedUuid.value ? selectedPlayerMetrics.value : null, + selectedPlayersMetrics: selectedUuids.value.length ? selectedPlayerMetrics.value : null, // Combined counts used in charts combinedGlobalCounts: combinedGlobalCounts.value, - combinedPlayerCounts: selectedUuid.value ? combinedPlayerCounts.value : null, + combinedPlayerCounts: selectedUuids.value.length ? combinedPlayerCounts.value : null, // Players data players: players.value, diff --git a/client/src/views/games/OfferControls.vue b/client/src/views/games/OfferControls.vue index 542ed83..a094284 100644 --- a/client/src/views/games/OfferControls.vue +++ b/client/src/views/games/OfferControls.vue @@ -2,8 +2,14 @@
+ +
+ Controles de oferta +
+ -
+ +
- -
+ +
Ofrecer @@ -112,7 +119,25 @@ const offerPavo = ref(0); const offerElote = ref(0); const requestPavo = ref(0); const requestElote = ref(0); -const advancedMode = ref(false); // Start in basic mode +const advancedMode = ref(false); // Start in basic mode; 'Avanzado' unlocks after 5 taps + +// Hidden unlock: 5 rapid taps on header to reveal "Avanzado" +const advancedUnlocked = ref(false); +const clickCount = ref(0); +let clickResetTimer: any = null; + +function onHeaderClick() { + if (advancedUnlocked.value) return; + if (clickResetTimer) { clearTimeout(clickResetTimer); clickResetTimer = null; } + clickCount.value += 1; + if (clickCount.value >= 5) { + advancedUnlocked.value = true; + } else { + clickResetTimer = setTimeout(() => { clickCount.value = 0; }, 1200); + } +} + +const unlockTitle = computed(() => advancedUnlocked.value ? 'Modo Avanzado disponible' : `Clicks: ${clickCount.value}/5 para desbloquear Avanzado`); const room = computed(() => colyseusService.gameRoom.value as any); const isFinished = ref(false); @@ -263,6 +288,9 @@ function noOffer() { .offer-card.disabled { opacity: 0.6; filter: grayscale(0.15); pointer-events: none; } .banner { margin-bottom:8px; padding:8px 10px; border-radius:10px; font-weight:700; font-size:13px; display:flex; align-items:center; gap:8px; } .banner.finished { background:#f8fafc; border:1px solid #e5e9f0; color:#334155; } +.offer-header { font-weight: 800; font-size: 14px; color:#334155; margin: 4px 2px 8px; } +.offer-header.clickable { cursor: pointer; user-select: none; opacity: 0.85; } +.offer-header.clickable:hover { filter: brightness(0.95); } .offer-grid { display:grid; grid-template-columns: 1fr; gap:12px; } @media (min-width: 500px) { .offer-grid { grid-template-columns: 1fr 1fr; } }