From 21adaf4caa92ed4732de0b9ec6d1cb4af4277a5a Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 29 Aug 2025 12:43:53 -0600 Subject: [PATCH] filter component ahora toma los datos raw, los procesa, los entrega y entrega el filtro usado --- client/src/components/DataSourceSelector.vue | 123 --- client/src/components/EventFilters.vue | 870 +++++++++++++++++-- client/src/views/Leaderboard.vue | 594 ++++--------- 3 files changed, 956 insertions(+), 631 deletions(-) delete mode 100644 client/src/components/DataSourceSelector.vue diff --git a/client/src/components/DataSourceSelector.vue b/client/src/components/DataSourceSelector.vue deleted file mode 100644 index 9b3933f..0000000 --- a/client/src/components/DataSourceSelector.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - - - \ No newline at end of file diff --git a/client/src/components/EventFilters.vue b/client/src/components/EventFilters.vue index 2227626..bc65df6 100644 --- a/client/src/components/EventFilters.vue +++ b/client/src/components/EventFilters.vue @@ -1,88 +1,542 @@ + \ No newline at end of file diff --git a/client/src/views/Leaderboard.vue b/client/src/views/Leaderboard.vue index 82ff646..0559237 100644 --- a/client/src/views/Leaderboard.vue +++ b/client/src/views/Leaderboard.vue @@ -22,22 +22,22 @@ {{ periodLabel }} - + - + - {{ roundLabel }} - + Ronda: {{ filterState.rounds.join(',') }} + - + - {{ gameLabel }} - + Juego: {{ filterState.games.join(',') }} + - + - Jugadores: {{ selectedUuids.length }} - + Jugadores: {{ filterState.playerUuids.length }} + @@ -47,84 +47,17 @@
- -
-
- - -
-
- - -
-
- - - - - - -
-
-
- - + -
- -
-
- - -
-
- -
-
@@ -137,7 +70,7 @@ :event-styles="EVENT_STYLES" :global-event-counts="combinedGlobalCounts" :player-event-counts="combinedPlayerCounts" - :selected-player-uuid="selectedUuids.length ? selectedUuids[0] : ''" + :selected-player-uuid="filterState.playerUuids.length ? filterState.playerUuids[0] : ''" :player-bar-gradient="playerBarGradient" view-mode="ratio" :loading="loading" @@ -165,6 +98,18 @@
{{ prettyRaw }}
+ + +
+
+
Datos filtrados (vista actual)
+
+ + +
+
+
{{ prettyFiltered }}
+
@@ -184,103 +129,36 @@ const loading = ref(false); const eventFilters = useEventFilters(); const filtersCollapsed = ref(false); -// Time mode and range handling -type TimeMode = 'active' | 'range'; -const timeMode = ref('range'); -const rangeFromStr = ref(''); -const rangeToStr = ref(''); -const liveEnd = ref(true); -let liveTimer: any = null; +// Filter state using new EventFilters approach +const filterState = ref({ + timeMode: 'range' as 'active' | 'range', + rangeFrom: '', + rangeTo: '', + liveEnd: true, + rounds: [] as number[], + games: [] as string[], + playerUuids: [] as string[], + rooms: [] as string[] +}); + const fullAggregatedEvents = ref>([]); +const filteredData = ref(null); -function fmtLocal(dt: Date) { - const pad = (n: number) => String(n).padStart(2, '0'); - const y = dt.getFullYear(); - const m = pad(dt.getMonth() + 1); - const d = pad(dt.getDate()); - const hh = pad(dt.getHours()); - const mm = pad(dt.getMinutes()); - return `${y}-${m}-${d}T${hh}:${mm}`; -} - -function initDefaultRange() { - const now = new Date(); - const from = new Date(now.getTime() - 60 * 60 * 1000); // 1h atrás - rangeFromStr.value = fmtLocal(from); - rangeToStr.value = fmtLocal(now); -} - -function setTimeMode(mode: TimeMode) { - timeMode.value = mode; - applyTimeMode(); -} - -function applyTimeMode() { - if (timeMode.value === 'active') { - eventFilters.dataSource.value = 'active-rooms'; - eventFilters.roomFilter.value = []; - selectedRoomIds.value = []; - eventFilters.applyFilters(EVENTS); - return; - } - eventFilters.dataSource.value = 'aggregated'; - // Parse range - const fromMs = Date.parse(rangeFromStr.value || ''); - const toMs = liveEnd.value ? Date.now() : Date.parse(rangeToStr.value || ''); - if (liveEnd.value) { - rangeToStr.value = fmtLocal(new Date(toMs)); - } - const valid = !Number.isNaN(fromMs) && !Number.isNaN(toMs) && toMs >= fromMs; - const ranged = valid - ? fullAggregatedEvents.value.filter(ev => typeof ev.timestamp === 'number' && (ev.timestamp as number) >= fromMs && (ev.timestamp as number) <= toMs) - : fullAggregatedEvents.value; - const counts: Record = Object.fromEntries(EVENTS.map(k => [k, 0])) as any; - ranged.forEach(ev => { if (EVENTS.includes(ev.kind)) { counts[ev.kind] = (counts[ev.kind] || 0) + 1; } }); - eventFilters.updateAggregatedData(ranged as any, counts); - // Build rooms and sync roomFilter to current range - const lastSeenIndex: Record = {}; - const ids = new Set(); - ranged.forEach((ev, i) => { - const rid = String(ev?.roomId || '').trim(); - if (!rid) return; - ids.add(rid); - lastSeenIndex[rid] = i; - }); - availableRooms.value = Array.from(ids).map(rid => ({ - roomId: rid, - name: `Sala ${rid.slice(0, 8)}`, - playerCount: ranged.filter(e => e?.roomId === rid).length, - _lastSeen: lastSeenIndex[rid] ?? -1 - })).sort((a: any, b: any) => (a._lastSeen - b._lastSeen)).map(({ _lastSeen, ...rest }: any) => rest); - selectedRoomIds.value = Array.from(ids); - eventFilters.roomFilter.value = [...selectedRoomIds.value]; - eventFilters.applyFilters(EVENTS); -} - +// Clear time filter - switch to active rooms mode function clearTimeFilter() { - // Switch to active rooms and set a 1-minute window ending now for consistency - timeMode.value = 'active'; - const now = new Date(); - rangeToStr.value = fmtLocal(now); - const from = new Date(now.getTime() - 60 * 1000); - rangeFromStr.value = fmtLocal(from); - liveEnd.value = true; - applyTimeMode(); + filterState.value.timeMode = 'active'; } -function toggleLiveEnd() { - liveEnd.value = !liveEnd.value; - if (liveEnd.value) { - if (liveTimer) { clearInterval(liveTimer); liveTimer = null; } - // Update immediately and every 15s - rangeToStr.value = fmtLocal(new Date()); - applyTimeMode(); - liveTimer = setInterval(() => { - rangeToStr.value = fmtLocal(new Date()); - applyTimeMode(); - }, 15000); - } else { - if (liveTimer) { clearInterval(liveTimer); liveTimer = null; } +// Handle filtered data from EventFilters component +function onFiltered(data: any) { + filteredData.value = data; + if (data) { + // Update global counts from filtered data + eventFilters.globalEventCounts.value = data.aggregatedCounts || {}; + // Update metrics + additionalMetrics.value = data.metrics || {}; + // Recompute selected player metrics + computeSelectedPlayersMetrics(); } } @@ -345,15 +223,15 @@ const allPlayersWithScores = ref([]); // Function to check if a score passes the current filters function scorePassesFilters(score: any, roomId: string) { // Room filter (array): empty means all - const rf = eventFilters.roomFilter.value; + const rf = filterState.value.rooms; if (rf.length && !rf.includes(String(roomId))) return false; // Round filter (array) - const rds = eventFilters.roundFilter.value; + const rds = filterState.value.rounds; if (rds.length && !rds.includes(Number(score.round))) return false; // Game variant filter (array) - const gfs = eventFilters.gameFilter.value; + const gfs = filterState.value.games; if (gfs.length && !gfs.includes(String(score.variant))) return false; return true; @@ -416,7 +294,7 @@ function computeMetricsFromScores() { // Function to compute metrics for selected players (multi-select) function computeSelectedPlayersMetrics() { - const uuids = selectedUuids.value; + const uuids = filterState.value.playerUuids; if (!uuids.length) { selectedPlayerMetrics.value = { players_seated: 0, @@ -534,122 +412,25 @@ const totalPlayersCount = computed(() => { // Active filters object const activeFilters = computed(() => ({ - dataSource: eventFilters.dataSource.value, - 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 + dataSource: filterState.value.timeMode === 'active' ? 'active-rooms' : 'aggregated', + round: filterState.value.rounds.join(',') || 'all', + game: filterState.value.games.join(',') || 'all', + hasFilters: filterState.value.rounds.length > 0 || filterState.value.games.length > 0 || filterState.value.playerUuids.length > 0, + selectedPlayer: filterState.value.playerUuids.length ? `${filterState.value.playerUuids.length} jugadores` : undefined, + selectedRoom: filterState.value.rooms.length ? `${filterState.value.rooms.length} salas` : undefined })); -// Watch for changes in filters and data source -watch([eventFilters.dataSource, eventFilters.roundFilter, eventFilters.gameFilter, eventFilters.roomFilter], () => { - eventFilters.applyFilters(EVENTS); - // Recalculate metrics when filters change - computeMetricsFromScores(); -}); +// Watch for changes in filter state to update selected player metrics +watch(filterState, () => { + computeSelectedPlayersMetrics(); +}, { deep: true }); -// selectedUuid watch will be added after selectedUuid is declared - -const players = ref<{ uuid: string; name: string; color?: string }[]>([]); const availableRooms = ref<{ roomId: string; name: string; playerCount?: number }[]>([]); -const search = ref(''); -const roomSearch = ref(''); -const playersFiltered = computed(() => { - const q = (search.value || '').toLowerCase(); - if (!q) return players.value; - return players.value.filter(p => (p.name || '').toLowerCase().includes(q) || (p.uuid || '').toLowerCase().includes(q)); -}); - -const roomsFiltered = computed(() => { - const q = (roomSearch.value || '').toLowerCase(); - if (!q) return availableRooms.value; - return availableRooms.value.filter(r => - (r.roomId || '').toLowerCase().includes(q) || - (r.name || '').toLowerCase().includes(q) - ); -}); - -// Reset page when search changes -watch(search, () => { - page.value = 1; -}); -watch(roomSearch, () => { - roomPage.value = 1; -}); -const page = ref(1); -const roomPage = ref(1); -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 = 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 - - const availableWidth = containerWidth.value - searchWidth - paginationWidth - clearBtnWidth - margin; - const maxChips = Math.max(3, Math.floor(availableWidth / chipWidth)); // Minimum 3 chips - - return Math.min(maxChips, 15); // Maximum 15 chips per page -}); - -const roomDynamicPageSize = computed(() => { - // Similar calculation for rooms - const chipWidth = 160; // Room chips might be slightly wider - const clearBtnWidth = selectedRoomIds.value.length ? 150 : 0; - const searchWidth = 240; - const paginationWidth = roomPageCount.value > 1 ? 100 : 0; - const margin = 40; - - const availableWidth = containerWidth.value - searchWidth - paginationWidth - clearBtnWidth - margin; - const maxChips = Math.max(3, Math.floor(availableWidth / chipWidth)); - - return Math.min(maxChips, 10); // Maximum 10 room chips per page -}); - -const pageCount = computed(() => Math.max(1, Math.ceil((playersFiltered.value.length || 0) / dynamicPageSize.value))); -const roomPageCount = computed(() => Math.max(1, Math.ceil((roomsFiltered.value.length || 0) / roomDynamicPageSize.value))); - -const playersPage = computed(() => { - const start = (page.value - 1) * dynamicPageSize.value; - return playersFiltered.value.slice(start, start + dynamicPageSize.value); -}); - - - -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 players changes to update their metrics -watch(selectedUuids, () => { - computeSelectedPlayersMetrics(); - updateSelectedPlayersCounts(); -}, { deep: true }); - -function initials(name: string): string { - const n = (name || '').trim(); - if (!n) return '🙂'; - const parts = n.split(/\s+/).slice(0, 2); - const chars = parts.map(p => p[0]?.toUpperCase()).join(''); - return chars || '🙂'; -} - - -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 clearPlayers() { - selectedUuids.value = []; - updateSelectedPlayersCounts(); -} @@ -657,22 +438,6 @@ function goHome() { router.push('/'); } -function prevPage() { page.value = Math.max(1, page.value - 1); } -function nextPage() { page.value = Math.min(pageCount.value, page.value + 1); } - - -// Ensure page doesn't exceed pageCount when players change -watch(pageCount, (newCount) => { - if (page.value > newCount) { - page.value = Math.max(1, newCount); - } -}); - -watch(roomPageCount, (newCount) => { - if (roomPage.value > newCount) { - roomPage.value = Math.max(1, newCount); - } -}); // Dynamic per-player overlay bar gradient and label color const playerBarGradient = computed(() => '#8b5cf6'); @@ -696,59 +461,61 @@ function copyRaw() { } catch {} } +// Filtered snapshot (what the UI is showing after filters) +const showFiltered = ref(false); +const filteredSnapshot = computed(() => { + try { + return { + metadata: { + timestamp: new Date().toISOString(), + dataSource: filterState.value.timeMode === 'active' ? 'active-rooms' : 'aggregated', + filters: { + room: filterState.value.rooms, + round: filterState.value.rounds, + game: filterState.value.games, + selectedPlayers: filterState.value.playerUuids, + selectedRooms: selectedRoomIds.value, + timeMode: filterState.value.timeMode, + from: filterState.value.rangeFrom, + to: filterState.value.rangeTo, + liveEnd: filterState.value.liveEnd + } + }, + chartTotals: { + offers: offersTotal.value, + responses: responsesTotal.value, + force: forceTotal.value, + shame: shameTotal.value, + report: reportTotal.value, + averageScore: averageScoreTotal.value, + totalPlayers: totalPlayersCount.value + }, + globalEventCounts: eventFilters.globalEventCounts.value, + playerEventCounts: playerEventCounts.value, + combinedCounts: { + global: combinedGlobalCounts.value, + players: combinedPlayerCounts.value + } + }; + } catch (e) { + return { error: String(e) }; + } +}); +const prettyFiltered = computed(() => JSON.stringify(filteredSnapshot.value, null, 2)); +function copyFiltered() { + try { navigator.clipboard.writeText(prettyFiltered.value); } catch {} +} + // Indicator labels const periodLabel = computed(() => { - if (timeMode.value === 'active') return 'Salas activas'; - const from = (rangeFromStr.value || '').replace('T', ' '); - const to = (rangeToStr.value || '').replace('T', ' '); - return `${from} → ${to}${liveEnd.value ? ' (ahora)' : ''}`; + if (filterState.value.timeMode === 'active') return 'Salas activas'; + const from = (filterState.value.rangeFrom || '').replace('T', ' '); + const to = (filterState.value.rangeTo || '').replace('T', ' '); + return `${from} → ${to}${filterState.value.liveEnd ? ' (ahora)' : ''}`; }); -const roundActive = computed(() => eventFilters.roundFilter.value.length > 0); -const gameActive = computed(() => eventFilters.gameFilter.value.length > 0); -const roundLabel = computed(() => `Ronda: ${eventFilters.roundFilter.value.join(',')}`); -const gameLabel = computed(() => `Juego: ${eventFilters.gameFilter.value.join(',')}`); - -function onRoundGameChange() { - eventFilters.applyFilters(EVENTS); - computeMetricsFromScores(); -} -function incrementFrom(n: number, unit: 'm'|'h'|'d'|'mo'|'y') { - // Ensure we are in range mode to apply manual adjustments - if (timeMode.value !== 'range') { - timeMode.value = 'range'; - } - // Determine current 'to' reference (now when liveEnd is enabled) - const to = liveEnd.value ? new Date() : new Date(Date.parse(rangeToStr.value || '')); - if (Number.isNaN(to.getTime())) { - // Initialize to now if empty/invalid - to.setTime(Date.now()); - } - // Parse current 'from' or initialize to a sensible default window - let from = new Date(Date.parse(rangeFromStr.value || '')); - if (Number.isNaN(from.getTime())) { - from = new Date(to.getTime()); - } - // Apply incremental increase to 'from' - // Move 'Desde' backwards in time by the chosen amount (expand window) - if (unit === 'm') from.setMinutes(from.getMinutes() - n); - else if (unit === 'h') from.setHours(from.getHours() - n); - else if (unit === 'd') from.setDate(from.getDate() - n); - else if (unit === 'mo') from.setMonth(from.getMonth() - n); - else if (unit === 'y') from.setFullYear(from.getFullYear() - n); - - // Clamp to keep at least 1 minute window and never exceed 'to' - const minWindowMs = 60 * 1000; - if (to.getTime() - from.getTime() < minWindowMs) { - from = new Date(to.getTime() - minWindowMs); - } - - rangeFromStr.value = fmtLocal(from); - rangeToStr.value = fmtLocal(to); - applyTimeMode(); -} function closeStreams() { try { esActions.value?.close(); } catch {} @@ -779,15 +546,6 @@ function setupStreams() { }); }); - // Lista de jugadores para chips con color (si disponible) - players.value = list - .map((p: any) => ({ - uuid: String(p?.uuid || ''), - name: String(p?.name || 'player'), - color: colorMap.get(String(p?.uuid || '')) || undefined - })) - .filter((p: any) => !!p.uuid) - .sort((a: any, b: any) => a.name.localeCompare(b.name)); // Mapear counts por jugador const byUuid: Record> = {}; @@ -801,32 +559,10 @@ function setupStreams() { }); playersActionsByUuid.value = byUuid; - // Datos agregados desde el payload + // Store aggregated events for the EventFilters component const aggEvents = Array.isArray(data?.aggregated?.detailedEvents) ? data.aggregated.detailedEvents : []; - // Store full events with timestamps for range filtering fullAggregatedEvents.value = aggEvents as any; - // Apply current time mode (range/active) - applyTimeMode(); - // Datos de salas activas → eventos activos y counts - if (data?.activeRooms?.rooms) { - const activeDetailed: Array<{ kind: string; round?: number; gameVariant?: string; roomId?: string }> = []; - (data.activeRooms.rooms || []).forEach((r: any) => { - (Array.isArray(r?.systemMessages) ? r.systemMessages : []).forEach((m: any) => { - const k = String(m?.kind || ''); - if (EVENTS.includes(k)) { - activeDetailed.push({ - kind: k, - round: m?.round, - gameVariant: m?.gameVariant || m?.variant, - roomId: m?.roomId || r?.roomId - }); - } - }); - }); - const activeCounts = data?.activeRooms?.counts || Object.fromEntries(EVENTS.map(k => [k, 0])); - eventFilters.updateActiveRoomsData(activeDetailed, activeCounts); - } // Construir listado de salas desde aggregated.rooms si está, si no derivar de eventos const roomsList = Array.isArray(data?.aggregated?.rooms) ? data.aggregated.rooms : null; @@ -857,14 +593,9 @@ function setupStreams() { })).sort((a: any, b: any) => (a._lastSeen - b._lastSeen)).map(({ _lastSeen, ...rest }: any) => rest); } - // Sin slider: la selección de salas se sincroniza con el rango de tiempo - - // Recalcular métricas desde score history + // Recalculate metrics from score history computeMetricsFromScores(); - // Aplicar filtros según dataSource actual - eventFilters.applyFilters(EVENTS); - // Actualizar conteos del/los jugador(es) seleccionado(s) updateSelectedPlayersCounts(); @@ -883,7 +614,7 @@ function updateSelectedPlayersCounts() { playerLoading.value = true; const next: Record = Object.fromEntries(EVENTS.map(k => [k, 0])) as any; const byUuid = playersActionsByUuid.value; - selectedUuids.value.forEach(uuid => { + filterState.value.playerUuids.forEach(uuid => { const counts = byUuid[uuid] || {}; EVENTS.forEach(k => { next[k] = (next[k] || 0) + Number(counts[k] || 0); }); }); @@ -891,37 +622,32 @@ function updateSelectedPlayersCounts() { playerLoading.value = false; } -// Update container width on resize -function updateContainerWidth() { - containerWidth.value = window.innerWidth; -} onMounted(() => { setupStreams(); // Initialize with aggregated data as default eventFilters.globalEventCounts.value = { ...eventFilters.globalEventCountsAggregated.value }; - // Set initial container width and add resize listener - updateContainerWidth(); - window.addEventListener('resize', updateContainerWidth); - initDefaultRange(); - // Start live-end timer by default if enabled - if (liveEnd.value) { - toggleLiveEnd(); // sets up the interval and applies - } + // Initialize default time range for filter state + const now = new Date(); + const from = new Date(now.getTime() - 60 * 60 * 1000); // 1h ago + const pad = (n: number) => String(n).padStart(2, '0'); + const formatLocal = (dt: Date) => { + const y = dt.getFullYear(); + const m = pad(dt.getMonth() + 1); + const d = pad(dt.getDate()); + const hh = pad(dt.getHours()); + const mm = pad(dt.getMinutes()); + return `${y}-${m}-${d}T${hh}:${mm}`; + }; + filterState.value.rangeFrom = formatLocal(from); + filterState.value.rangeTo = formatLocal(now); }); onUnmounted(() => { closeStreams(); - window.removeEventListener('resize', updateContainerWidth); - try { if (liveTimer) clearInterval(liveTimer); } catch {} }); -// Reset to first page when search changes or players list length shrinks below current page -watch(() => search.value, () => { page.value = 1; }); -watch(() => playersFiltered.value.length, () => { - if (page.value > pageCount.value) page.value = pageCount.value; -}); // Removed totals table and sorting; keep actions stream for per-player counts only // Deprecated: previously used to show a totals list; now unused @@ -933,15 +659,18 @@ function downloadJSON() { const currentData = { metadata: { timestamp: new Date().toISOString(), - dataSource: eventFilters.dataSource.value, + dataSource: filterState.value.timeMode === 'active' ? 'active-rooms' : 'aggregated', filters: { - room: eventFilters.roomFilter.value, - round: eventFilters.roundFilter.value, - game: eventFilters.gameFilter.value, - hasActiveFilters: eventFilters.hasActiveFilters.value, - filterSummary: eventFilters.filterSummary.value, - selectedPlayers: selectedUuids.value, - selectedRooms: selectedRoomIds.value + room: filterState.value.rooms, + round: filterState.value.rounds, + game: filterState.value.games, + hasActiveFilters: activeFilters.value.hasFilters, + selectedPlayers: filterState.value.playerUuids, + selectedRooms: selectedRoomIds.value, + timeMode: filterState.value.timeMode, + rangeFrom: filterState.value.rangeFrom, + rangeTo: filterState.value.rangeTo, + liveEnd: filterState.value.liveEnd } }, @@ -960,20 +689,20 @@ function downloadJSON() { globalEventCounts: eventFilters.globalEventCounts.value, // Selected player event counts (if applicable) - selectedPlayersEventCounts: selectedUuids.value.length ? playerEventCounts.value : null, + selectedPlayersEventCounts: filterState.value.playerUuids.length ? playerEventCounts.value : null, // Additional metrics being displayed additionalMetrics: additionalMetrics.value, // Selected player metrics (if applicable) - selectedPlayersMetrics: selectedUuids.value.length ? selectedPlayerMetrics.value : null, + selectedPlayersMetrics: filterState.value.playerUuids.length ? selectedPlayerMetrics.value : null, // Combined counts used in charts combinedGlobalCounts: combinedGlobalCounts.value, - combinedPlayerCounts: selectedUuids.value.length ? combinedPlayerCounts.value : null, + combinedPlayerCounts: filterState.value.playerUuids.length ? combinedPlayerCounts.value : null, // Players data - players: players.value, + players: allPlayersWithScores.value, availableRooms: availableRooms.value, // Current filtered data if available @@ -1004,8 +733,8 @@ function downloadJSON() { // Generate filename with current filters and timestamp 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 dataSourceLabel = filterState.value.timeMode === 'range' ? 'agregados' : 'activos'; + const filterLabel = activeFilters.value.hasFilters ? '_filtrado' : ''; const filename = `leaderboard_${dataSourceLabel}${filterLabel}_${timestamp}.json`; link.setAttribute('download', filename); @@ -1075,11 +804,6 @@ function downloadJSON() { .dual-slider .range.start { z-index: 2; } .dual-slider .range.end { z-index: 3; } -/* Quick select buttons: super compact and subtle */ -.quick-select { display: flex; justify-content: flex-end; gap: 6px; margin-top: 4px; } -.qs-btn { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(148,163,184,0.35); background: rgba(255,255,255,0.6); color: #475569; font-size: 11px; font-weight: 800; cursor: pointer; opacity: 0.85; transition: all 0.2s ease; } -.qs-btn:hover { opacity: 1; box-shadow: 0 2px 6px rgba(0,0,0,0.08); transform: translateY(-1px); } -.qs-btn:active { transform: translateY(0); } @media (max-width: 640px) { .dual-slider { height: 32px; padding: 14px 6px; } @@ -1222,17 +946,7 @@ function downloadJSON() { .raw-actions { display: flex; gap: 8px; } .raw-pre { max-height: 420px; overflow: auto; background: #0b1020; color: #e5e7eb; padding: 12px; border-radius: 10px; font-size: 12px; line-height: 1.4; border: 1px solid #1f2937; } -/* Time selector styles */ -.time-selector { align-items: center; } -.mode-buttons { display: flex; gap: 6px; margin-bottom: 10px; } -.mode-btn { padding: 6px 10px; border-radius: 8px; border: 1px solid #cbd5e1; background: #fff; color: #334155; font-weight: 800; font-size: 12px; cursor: pointer; } -.mode-btn.active { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; border-color: #667eea; } -.range-inputs { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 10px; } -.range-inputs.disabled { opacity: 0.6; filter: grayscale(0.1); } -.range-inputs label { display: flex; gap: 6px; align-items: center; font-weight: 700; color: #334155; } -.range-inputs input[type="datetime-local"] { padding: 6px 8px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 12px; } -.live-btn { padding: 6px 10px; border-radius: 8px; border: 1px solid #cbd5e1; background: #fff; color: #334155; font-weight: 800; font-size: 12px; cursor: pointer; } -.live-btn.active { background: linear-gradient(135deg, #06b6d4 0%, #8b5cf6 100%); color: #fff; border-color: #06b6d4; } +/* Time indicator key */ .key.time { background: linear-gradient(90deg, #f59e0b, #d97706); box-shadow: 0 0 8px rgba(245,158,11,0.35); }