filter component ahora toma los datos raw, los procesa, los entrega y entrega el filtro usado
This commit is contained in:
@@ -1,123 +0,0 @@
|
||||
<template>
|
||||
<div class="data-source-selector glass light">
|
||||
<div class="source-label">Fuente de datos:</div>
|
||||
<div class="source-buttons">
|
||||
<button
|
||||
class="source-btn"
|
||||
:class="{ active: modelValue === 'aggregated' }"
|
||||
@click="$emit('update:modelValue', 'aggregated')"
|
||||
title="Muestra el total histórico de todas las acciones registradas de los jugadores"
|
||||
>
|
||||
<span class="source-icon">📁</span>
|
||||
Datos Agregados (Histórico)
|
||||
</button>
|
||||
<button
|
||||
class="source-btn"
|
||||
:class="{ active: modelValue === 'active-rooms' }"
|
||||
@click="$emit('update:modelValue', 'active-rooms')"
|
||||
title="Muestra solo los datos de las salas actualmente activas"
|
||||
>
|
||||
<span class="source-icon">🔴</span>
|
||||
Salas Activas (Tiempo Real)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DataSource } from '../composables/useEventFilters';
|
||||
|
||||
interface Props {
|
||||
modelValue: DataSource;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: DataSource];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.glass.light {
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1px solid rgba(229, 231, 235, 0.9);
|
||||
box-shadow: 0 18px 50px rgba(0,0,0,0.12), inset 0 1px 0 rgba(255,255,255,0.6);
|
||||
backdrop-filter: blur(18px) saturate(120%);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(120%);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.data-source-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.source-label {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.source-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.source-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: rgba(255,255,255,0.6);
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.source-btn:hover {
|
||||
background: rgba(255,255,255,0.9);
|
||||
border-color: #cbd5e1;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.source-btn.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 6px 20px rgba(102,126,234,0.3);
|
||||
}
|
||||
|
||||
.source-btn.active:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(102,126,234,0.4);
|
||||
}
|
||||
|
||||
.source-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.data-source-selector {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.source-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.source-btn {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,74 @@
|
||||
<template>
|
||||
<div class="filters-wrapper">
|
||||
<!-- Time selector -->
|
||||
<div class="time-selector">
|
||||
<div class="mode-buttons">
|
||||
<button
|
||||
class="mode-btn"
|
||||
:class="{ active: filters.timeMode === 'active' }"
|
||||
@click="updateTimeMode('active')"
|
||||
title="Mostrar solo salas activas (tiempo real)"
|
||||
>
|
||||
🔴 Salas activas
|
||||
</button>
|
||||
<button
|
||||
class="mode-btn"
|
||||
:class="{ active: filters.timeMode === 'range' }"
|
||||
@click="updateTimeMode('range')"
|
||||
title="Filtrar por rango de fecha y hora"
|
||||
>
|
||||
📅 Rango de tiempo
|
||||
</button>
|
||||
</div>
|
||||
<div class="range-inputs" :class="{ disabled: filters.timeMode !== 'range' }">
|
||||
<label>
|
||||
Desde
|
||||
<input
|
||||
type="datetime-local"
|
||||
:value="filters.rangeFrom"
|
||||
:disabled="filters.timeMode !== 'range'"
|
||||
@change="updateRangeFrom(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Hasta
|
||||
<input
|
||||
type="datetime-local"
|
||||
:value="filters.rangeTo"
|
||||
:disabled="filters.timeMode !== 'range' || filters.liveEnd"
|
||||
@change="updateRangeTo(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="quick">
|
||||
<button
|
||||
class="qs-btn live-btn"
|
||||
:class="{ active: filters.liveEnd }"
|
||||
:disabled="filters.timeMode !== 'range'"
|
||||
@click="toggleLiveEnd"
|
||||
title="Hasta ahora"
|
||||
>
|
||||
⏱
|
||||
</button>
|
||||
<button class="qs-btn" :disabled="filters.timeMode !== 'range'" @click="incrementFrom(10, 'm')">10m</button>
|
||||
<button class="qs-btn" :disabled="filters.timeMode !== 'range'" @click="incrementFrom(1, 'h')">1h</button>
|
||||
<button class="qs-btn" :disabled="filters.timeMode !== 'range'" @click="incrementFrom(1, 'd')">1D</button>
|
||||
<button class="qs-btn" :disabled="filters.timeMode !== 'range'" @click="incrementFrom(1, 'mo')">1M</button>
|
||||
<button class="qs-btn" :disabled="filters.timeMode !== 'range'" @click="incrementFrom(1, 'y')">1Y</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="divider" />
|
||||
|
||||
<!-- Round and Game filters -->
|
||||
<div class="filters-container" :class="{ compact }">
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">Ronda:</label>
|
||||
<div class="filter-buttons">
|
||||
<button
|
||||
class="filter-btn"
|
||||
:class="{ active: (roundFilter?.length||0) === 0 }"
|
||||
@click="$emit('update:roundFilter', [])"
|
||||
:class="{ active: filters.rounds.length === 0 }"
|
||||
@click="updateRounds([])"
|
||||
title="Mostrar todas las rondas"
|
||||
>
|
||||
Todas
|
||||
@@ -15,9 +77,9 @@
|
||||
v-for="r in [1, 2, 3]"
|
||||
:key="r"
|
||||
class="filter-btn"
|
||||
:class="{ active: roundFilter?.includes(r) }"
|
||||
@click="$emit('update:roundFilter', roundFilter?.includes(r) ? roundFilter.filter(x=>x!==r) : [...(roundFilter||[]), r])"
|
||||
:title="`Mostrar solo Ronda ${r}`"
|
||||
:class="{ active: filters.rounds.includes(r) }"
|
||||
@click="toggleRound(r)"
|
||||
:title="`Mostrar ronda ${r}`"
|
||||
>
|
||||
R{{ r }}
|
||||
</button>
|
||||
@@ -29,8 +91,8 @@
|
||||
<div class="filter-buttons">
|
||||
<button
|
||||
class="filter-btn"
|
||||
:class="{ active: (gameFilter?.length||0) === 0 }"
|
||||
@click="$emit('update:gameFilter', [])"
|
||||
:class="{ active: filters.games.length === 0 }"
|
||||
@click="updateGames([])"
|
||||
title="Mostrar todas las variantes"
|
||||
>
|
||||
Todas
|
||||
@@ -39,50 +101,442 @@
|
||||
v-for="g in ['G1', 'G2', 'G3', 'G4', 'G5']"
|
||||
:key="g"
|
||||
class="filter-btn"
|
||||
:class="{ active: gameFilter?.includes(g) }"
|
||||
@click="$emit('update:gameFilter', gameFilter?.includes(g) ? gameFilter.filter(x=>x!==g) : [...(gameFilter||[]), g])"
|
||||
:title="`Mostrar solo variante ${g}`"
|
||||
:class="{ active: filters.games.includes(g) }"
|
||||
@click="toggleGame(g)"
|
||||
:title="`Mostrar variante ${g}`"
|
||||
>
|
||||
{{ g }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasActiveFilters" class="filter-summary">
|
||||
<span class="summary-icon">🔍</span>
|
||||
<span class="summary-text">{{ filterSummary }}</span>
|
||||
<hr class="divider" />
|
||||
|
||||
<!-- Player selection -->
|
||||
<div class="player-chips compact">
|
||||
<div class="search-controls">
|
||||
<input
|
||||
v-model="playerSearch"
|
||||
class="search"
|
||||
placeholder="Buscar jugador…"
|
||||
/>
|
||||
<div class="pagination compact" v-if="playerPageCount > 1">
|
||||
<button class="pg-btn compact" @click="playerPage--" :disabled="playerPage <= 1">‹</button>
|
||||
<span class="pg-ind">{{ playerPage }}/{{ playerPageCount }}</span>
|
||||
<button class="pg-btn compact" @click="playerPage++" :disabled="playerPage >= playerPageCount">›</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chips">
|
||||
<button
|
||||
class="reset-btn"
|
||||
@click="$emit('resetFilters')"
|
||||
title="Quitar todos los filtros"
|
||||
v-for="p in playersPage"
|
||||
:key="p.uuid"
|
||||
class="chip"
|
||||
:class="{ active: filters.playerUuids.includes(p.uuid) }"
|
||||
@click="togglePlayer(p.uuid)"
|
||||
:title="p.uuid"
|
||||
:style="({ '--primary': p.color || '#667eea' } as any)"
|
||||
>
|
||||
✕
|
||||
<span class="avatar">{{ initials(p.name) }}</span>
|
||||
<span class="label">{{ p.name || 'Jugador' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RoundFilterMulti, GameFilterMulti } from '../composables/useEventFilters';
|
||||
import { ref, computed, watch, watchEffect } from 'vue';
|
||||
|
||||
type TimeMode = 'active' | 'range';
|
||||
|
||||
interface FilterState {
|
||||
timeMode: TimeMode;
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
liveEnd: boolean;
|
||||
rounds: number[];
|
||||
games: string[];
|
||||
playerUuids: string[];
|
||||
rooms: string[];
|
||||
}
|
||||
|
||||
interface PlayerData {
|
||||
uuid: string;
|
||||
name: string;
|
||||
color?: string;
|
||||
total?: number;
|
||||
shameTokens?: number;
|
||||
counts?: Record<string, number>;
|
||||
roomScoreHistory?: Array<{
|
||||
roomId: string;
|
||||
scores: Array<{
|
||||
round: number;
|
||||
variant: string;
|
||||
role: string;
|
||||
score: number;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
roundFilter: RoundFilterMulti;
|
||||
gameFilter: GameFilterMulti;
|
||||
hasActiveFilters: boolean;
|
||||
filterSummary: string;
|
||||
modelValue: FilterState;
|
||||
rawData?: {
|
||||
players?: PlayerData[];
|
||||
activeRooms?: any;
|
||||
aggregatedEvents?: any[];
|
||||
};
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), { compact: false });
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
compact: false
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
'update:roundFilter': [value: RoundFilterMulti];
|
||||
'update:gameFilter': [value: GameFilterMulti];
|
||||
'resetFilters': [];
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: FilterState];
|
||||
'filtered': [data: any];
|
||||
}>();
|
||||
|
||||
// Local reactive copy of filters
|
||||
const filters = ref<FilterState>({
|
||||
timeMode: 'range',
|
||||
rangeFrom: '',
|
||||
rangeTo: '',
|
||||
liveEnd: true,
|
||||
rounds: [],
|
||||
games: [],
|
||||
playerUuids: [],
|
||||
rooms: []
|
||||
});
|
||||
|
||||
// Sync with v-model
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
filters.value = { ...newVal };
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// Player search and pagination
|
||||
const playerSearch = ref('');
|
||||
const playerPage = ref(1);
|
||||
const pageSize = 15;
|
||||
|
||||
// Initialize default time range
|
||||
function initDefaultRange() {
|
||||
const now = new Date();
|
||||
const from = new Date(now.getTime() - 60 * 60 * 1000); // 1h ago
|
||||
filters.value.rangeFrom = formatLocal(from);
|
||||
filters.value.rangeTo = formatLocal(now);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function formatLocal(dt: Date): string {
|
||||
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}`;
|
||||
}
|
||||
|
||||
// Players list from raw data
|
||||
const players = computed(() => {
|
||||
if (!props.rawData?.players) return [];
|
||||
return props.rawData.players.filter(p => p.name);
|
||||
});
|
||||
|
||||
const playersFiltered = computed(() => {
|
||||
const q = playerSearch.value.toLowerCase();
|
||||
if (!q) return players.value;
|
||||
return players.value.filter(p =>
|
||||
(p.name || '').toLowerCase().includes(q) ||
|
||||
(p.uuid || '').toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
|
||||
const playerPageCount = computed(() =>
|
||||
Math.ceil(playersFiltered.value.length / pageSize)
|
||||
);
|
||||
|
||||
const playersPage = computed(() => {
|
||||
const start = (playerPage.value - 1) * pageSize;
|
||||
return playersFiltered.value.slice(start, start + pageSize);
|
||||
});
|
||||
|
||||
// Reset page when search changes
|
||||
watch(playerSearch, () => {
|
||||
playerPage.value = 1;
|
||||
});
|
||||
|
||||
// Helper to get initials
|
||||
function initials(name: string): string {
|
||||
if (!name) return '?';
|
||||
const parts = name.split(/\s+/);
|
||||
if (parts.length === 1) return name.slice(0, 2).toUpperCase();
|
||||
return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
}
|
||||
|
||||
// Update functions that emit changes
|
||||
function emitUpdate() {
|
||||
emit('update:modelValue', { ...filters.value });
|
||||
}
|
||||
|
||||
function updateTimeMode(mode: TimeMode) {
|
||||
filters.value.timeMode = mode;
|
||||
emitUpdate();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function updateRangeFrom(value: string) {
|
||||
filters.value.rangeFrom = value;
|
||||
emitUpdate();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function updateRangeTo(value: string) {
|
||||
filters.value.rangeTo = value;
|
||||
emitUpdate();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function toggleLiveEnd() {
|
||||
filters.value.liveEnd = !filters.value.liveEnd;
|
||||
if (filters.value.liveEnd) {
|
||||
filters.value.rangeTo = formatLocal(new Date());
|
||||
}
|
||||
emitUpdate();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function incrementFrom(n: number, unit: 'm' | 'h' | 'd' | 'mo' | 'y') {
|
||||
const to = filters.value.liveEnd ? new Date() : new Date(Date.parse(filters.value.rangeTo || ''));
|
||||
if (Number.isNaN(to.getTime())) {
|
||||
to.setTime(Date.now());
|
||||
}
|
||||
|
||||
let from = new Date(Date.parse(filters.value.rangeFrom || ''));
|
||||
if (Number.isNaN(from.getTime())) {
|
||||
from = new Date(to.getTime());
|
||||
}
|
||||
|
||||
// Move 'from' backwards in time
|
||||
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);
|
||||
|
||||
// Ensure minimum window
|
||||
const minWindowMs = 60 * 1000;
|
||||
if (to.getTime() - from.getTime() < minWindowMs) {
|
||||
from = new Date(to.getTime() - minWindowMs);
|
||||
}
|
||||
|
||||
filters.value.rangeFrom = formatLocal(from);
|
||||
filters.value.rangeTo = formatLocal(to);
|
||||
emitUpdate();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function updateRounds(rounds: number[]) {
|
||||
filters.value.rounds = rounds;
|
||||
emitUpdate();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function toggleRound(round: number) {
|
||||
const idx = filters.value.rounds.indexOf(round);
|
||||
if (idx >= 0) {
|
||||
filters.value.rounds.splice(idx, 1);
|
||||
} else {
|
||||
filters.value.rounds.push(round);
|
||||
}
|
||||
emitUpdate();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function updateGames(games: string[]) {
|
||||
filters.value.games = games;
|
||||
emitUpdate();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function toggleGame(game: string) {
|
||||
const idx = filters.value.games.indexOf(game);
|
||||
if (idx >= 0) {
|
||||
filters.value.games.splice(idx, 1);
|
||||
} else {
|
||||
filters.value.games.push(game);
|
||||
}
|
||||
emitUpdate();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function togglePlayer(uuid: string) {
|
||||
const idx = filters.value.playerUuids.indexOf(uuid);
|
||||
if (idx >= 0) {
|
||||
filters.value.playerUuids.splice(idx, 1);
|
||||
} else {
|
||||
filters.value.playerUuids.push(uuid);
|
||||
}
|
||||
emitUpdate();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
// Live update timer
|
||||
let liveTimer: any = null;
|
||||
|
||||
watchEffect(() => {
|
||||
if (filters.value.liveEnd && filters.value.timeMode === 'range') {
|
||||
if (liveTimer) clearInterval(liveTimer);
|
||||
liveTimer = setInterval(() => {
|
||||
filters.value.rangeTo = formatLocal(new Date());
|
||||
emitUpdate();
|
||||
applyFilters();
|
||||
}, 15000); // Update every 15 seconds
|
||||
} else {
|
||||
if (liveTimer) {
|
||||
clearInterval(liveTimer);
|
||||
liveTimer = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Apply filters to raw data and emit filtered result
|
||||
function applyFilters() {
|
||||
if (!props.rawData) {
|
||||
emit('filtered', null);
|
||||
return;
|
||||
}
|
||||
|
||||
const result: any = {
|
||||
players: [],
|
||||
events: [],
|
||||
metrics: {},
|
||||
aggregatedCounts: {}
|
||||
};
|
||||
|
||||
// Filter based on time mode
|
||||
if (filters.value.timeMode === 'active') {
|
||||
// Use active rooms data
|
||||
result.activeRooms = props.rawData.activeRooms;
|
||||
// TODO: Extract events from active rooms
|
||||
} else {
|
||||
// Filter by time range
|
||||
const fromMs = Date.parse(filters.value.rangeFrom || '');
|
||||
const toMs = Date.parse(filters.value.rangeTo || '');
|
||||
|
||||
if (props.rawData.aggregatedEvents) {
|
||||
result.events = props.rawData.aggregatedEvents.filter((ev: any) => {
|
||||
// Time filter
|
||||
if (!Number.isNaN(fromMs) && !Number.isNaN(toMs)) {
|
||||
const t = ev.timestamp;
|
||||
if (typeof t === 'number' && (t < fromMs || t > toMs)) return false;
|
||||
}
|
||||
|
||||
// Round filter
|
||||
if (filters.value.rounds.length > 0) {
|
||||
if (!filters.value.rounds.includes(ev.round)) return false;
|
||||
}
|
||||
|
||||
// Game filter
|
||||
if (filters.value.games.length > 0) {
|
||||
if (!filters.value.games.includes(ev.gameVariant)) return false;
|
||||
}
|
||||
|
||||
// Room filter
|
||||
if (filters.value.rooms.length > 0) {
|
||||
if (!filters.value.rooms.includes(ev.roomId)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Filter players
|
||||
if (props.rawData.players) {
|
||||
result.players = props.rawData.players.filter((p: PlayerData) => {
|
||||
if (filters.value.playerUuids.length > 0) {
|
||||
return filters.value.playerUuids.includes(p.uuid);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Calculate metrics from filtered players
|
||||
let totalP1Scores = 0;
|
||||
let totalP2Scores = 0;
|
||||
let p1Count = 0;
|
||||
let p2Count = 0;
|
||||
let playersWithShame = 0;
|
||||
|
||||
result.players.forEach((player: PlayerData) => {
|
||||
if (player.shameTokens && player.shameTokens > 0) {
|
||||
playersWithShame++;
|
||||
}
|
||||
|
||||
if (player.roomScoreHistory) {
|
||||
player.roomScoreHistory.forEach(roomScore => {
|
||||
roomScore.scores.forEach(score => {
|
||||
// Apply filters to scores
|
||||
if (filters.value.rounds.length > 0 && !filters.value.rounds.includes(score.round)) return;
|
||||
if (filters.value.games.length > 0 && !filters.value.games.includes(score.variant)) return;
|
||||
if (filters.value.rooms.length > 0 && !filters.value.rooms.includes(roomScore.roomId)) return;
|
||||
|
||||
if (score.role === 'P1') {
|
||||
totalP1Scores += score.score;
|
||||
p1Count++;
|
||||
} else if (score.role === 'P2') {
|
||||
totalP2Scores += score.score;
|
||||
p2Count++;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
result.metrics = {
|
||||
players_seated: result.players.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: playersWithShame,
|
||||
players_without_shame: result.players.length - playersWithShame
|
||||
};
|
||||
}
|
||||
|
||||
// Count events by type
|
||||
const eventTypes = [
|
||||
'p1_propose', 'p1_no_offer',
|
||||
'p2_snatch', 'p2_accept', 'p2_force', 'p2_no_force', 'p2_reject',
|
||||
'p1_shame', 'p1_no_shame', 'p1_report', 'p1_no_report'
|
||||
];
|
||||
|
||||
result.aggregatedCounts = {};
|
||||
eventTypes.forEach(type => {
|
||||
result.aggregatedCounts[type] = result.events.filter((e: any) => e.kind === type).length;
|
||||
});
|
||||
|
||||
emit('filtered', result);
|
||||
}
|
||||
|
||||
// Initialize on mount
|
||||
if (!filters.value.rangeFrom) {
|
||||
initDefaultRange();
|
||||
}
|
||||
|
||||
// Apply filters whenever raw data changes
|
||||
watch(() => props.rawData, () => {
|
||||
applyFilters();
|
||||
}, { deep: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filters-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filters-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
@@ -102,7 +556,12 @@ defineEmits<{
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filter-label { font-size: 13px; font-weight: 700; color: #334155; min-width: 52px; }
|
||||
.filter-label {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
@@ -142,17 +601,273 @@ defineEmits<{
|
||||
box-shadow: 0 6px 20px rgba(102,126,234,0.4);
|
||||
}
|
||||
|
||||
.filter-summary { display: none; }
|
||||
/* Time selector styles */
|
||||
.time-selector {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
.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;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mode-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.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;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.range-inputs input[type="datetime-local"] {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
/* Quick select buttons */
|
||||
.quick {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.qs-btn {
|
||||
padding: 4px 8px;
|
||||
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:not(:disabled) {
|
||||
opacity: 1;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.qs-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.qs-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.live-btn.active {
|
||||
background: linear-gradient(135deg, #06b6d4 0%, #8b5cf6 100%);
|
||||
color: #fff;
|
||||
border-color: #06b6d4;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border: 0;
|
||||
border-top: 1px solid rgba(203,213,225,0.6);
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
/* Player chips styles */
|
||||
.player-chips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.player-chips.compact .search {
|
||||
padding: 6px 8px;
|
||||
min-width: 180px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.player-chips.compact .chip {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.player-chips.compact .avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.search-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
justify-content: start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
border-radius: 10px;
|
||||
min-width: 240px;
|
||||
outline: none;
|
||||
flex: 1;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.search::placeholder {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: color-mix(in srgb, var(--primary) 6%, white);
|
||||
border: 1px solid color-mix(in srgb, var(--primary) 24%, #e5e7eb);
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
color: #111827;
|
||||
cursor: pointer;
|
||||
transition: transform .18s ease, background .18s ease, box-shadow .18s ease;
|
||||
}
|
||||
|
||||
.chip:hover {
|
||||
transform: translateY(-1px);
|
||||
background: color-mix(in srgb, var(--primary) 10%, white);
|
||||
box-shadow: 0 6px 18px rgba(102,126,234,0.18);
|
||||
}
|
||||
|
||||
.chip.active {
|
||||
background: color-mix(in srgb, var(--primary) 18%, white);
|
||||
border-color: color-mix(in srgb, var(--primary) 45%, #c7d2fe);
|
||||
box-shadow: 0 6px 22px rgba(99,102,241,0.22);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--primary) 25%, #eef2ff);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 900;
|
||||
color: color-mix(in srgb, var(--primary) 80%, #111);
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
background: rgba(255,255,255,0.82);
|
||||
border: 1px solid rgba(229,231,235,0.9);
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.6);
|
||||
backdrop-filter: blur(10px) saturate(120%);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(120%);
|
||||
}
|
||||
|
||||
.pagination.compact {
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.pg-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #cbd5e1;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 12px rgba(102,126,234,0.25);
|
||||
}
|
||||
|
||||
.pg-btn.compact {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 2px 8px rgba(102,126,234,0.2);
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
white-space: nowrap;
|
||||
.pg-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(102,126,234,0.35);
|
||||
}
|
||||
|
||||
.reset-btn { display: none; }
|
||||
.pg-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: #e5e7eb;
|
||||
color: #94a3b8;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pg-ind {
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
min-width: 35px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filters-container {
|
||||
@@ -169,8 +884,13 @@ defineEmits<{
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.filter-summary {
|
||||
justify-content: center;
|
||||
.time-selector {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.range-inputs {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,5 +909,19 @@ defineEmits<{
|
||||
.filter-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mode-buttons {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.quick {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -22,22 +22,22 @@
|
||||
<span class="marker-chip">
|
||||
<span class="key time"></span>
|
||||
{{ periodLabel }}
|
||||
<button class="chip-x" v-if="timeMode == 'range'" @click="clearTimeFilter" title="Borrar filtro de tiempo">×</button>
|
||||
<button class="chip-x" v-if="filterState.timeMode == 'range'" @click="clearTimeFilter" title="Borrar filtro de tiempo">×</button>
|
||||
</span>
|
||||
<span class="marker-chip" v-if="roundActive">
|
||||
<span class="marker-chip" v-if="filterState.rounds.length">
|
||||
<span class="key round"></span>
|
||||
{{ roundLabel }}
|
||||
<button class="chip-x" @click="eventFilters.roundFilter.value = []; onRoundGameChange()">×</button>
|
||||
Ronda: {{ filterState.rounds.join(',') }}
|
||||
<button class="chip-x" @click="filterState.rounds = []">×</button>
|
||||
</span>
|
||||
<span class="marker-chip" v-if="gameActive">
|
||||
<span class="marker-chip" v-if="filterState.games.length">
|
||||
<span class="key game"></span>
|
||||
{{ gameLabel }}
|
||||
<button class="chip-x" @click="eventFilters.gameFilter.value = []; onRoundGameChange()">×</button>
|
||||
Juego: {{ filterState.games.join(',') }}
|
||||
<button class="chip-x" @click="filterState.games = []">×</button>
|
||||
</span>
|
||||
<span class="marker-chip" v-if="selectedUuids.length" >
|
||||
<span class="marker-chip" v-if="filterState.playerUuids.length" >
|
||||
<span class="key player"></span>
|
||||
Jugadores: {{ selectedUuids.length }}
|
||||
<button class="chip-x" @click="clearPlayers(); onRoundGameChange()" title="Quitar selección de jugadores">×</button>
|
||||
Jugadores: {{ filterState.playerUuids.length }}
|
||||
<button class="chip-x" @click="filterState.playerUuids = []" title="Quitar selección de jugadores">×</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,84 +47,17 @@
|
||||
|
||||
<Transition name="filters-slide">
|
||||
<div v-if="!filtersCollapsed" class="controls glass light">
|
||||
<!-- Selector de periodo: Salas activas vs Rango de tiempo -->
|
||||
<div class="time-selector">
|
||||
<div class="mode-buttons">
|
||||
<button
|
||||
class="mode-btn"
|
||||
:class="{ active: timeMode === 'active' }"
|
||||
@click="setTimeMode('active')"
|
||||
title="Mostrar solo salas activas (tiempo real)"
|
||||
>
|
||||
🔴 Salas activas
|
||||
</button>
|
||||
<button
|
||||
class="mode-btn"
|
||||
:class="{ active: timeMode === 'range' }"
|
||||
@click="setTimeMode('range')"
|
||||
title="Filtrar por rango de fecha y hora"
|
||||
>
|
||||
📅 Rango de tiempo
|
||||
</button>
|
||||
</div>
|
||||
<div class="range-inputs" :class="{ disabled: timeMode !== 'range' }">
|
||||
<label>
|
||||
Desde
|
||||
<input type="datetime-local" v-model="rangeFromStr" :disabled="timeMode !== 'range'" @change="applyTimeMode" />
|
||||
</label>
|
||||
<label>
|
||||
Hasta
|
||||
<input type="datetime-local" v-model="rangeToStr" :disabled="timeMode !== 'range' || liveEnd" @change="applyTimeMode" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="quick">
|
||||
<button class="qs-btn live-btn" :class="{ active: liveEnd }" :disabled="timeMode !== 'range'" @click="toggleLiveEnd" title="Hasta ahora">⏱</button>
|
||||
<button class="qs-btn" :disabled="timeMode !== 'range'" @click="incrementFrom(10,'m')">10m</button>
|
||||
<button class="qs-btn" :disabled="timeMode !== 'range'" @click="incrementFrom(1,'h')">1h</button>
|
||||
<button class="qs-btn" :disabled="timeMode !== 'range'" @click="incrementFrom(1,'d')">1D</button>
|
||||
<button class="qs-btn" :disabled="timeMode !== 'range'" @click="incrementFrom(1,'mo')">1M</button>
|
||||
<button class="qs-btn" :disabled="timeMode !== 'range'" @click="incrementFrom(1,'y')">1Y</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="divider" />
|
||||
|
||||
<!-- Ronda / Juego -->
|
||||
<!-- EventFilters component with v-model -->
|
||||
<EventFilters
|
||||
:round-filter="eventFilters.roundFilter.value"
|
||||
:game-filter="eventFilters.gameFilter.value"
|
||||
:has-active-filters="false"
|
||||
:filter-summary="''"
|
||||
v-model="filterState"
|
||||
:raw-data="{
|
||||
players: allPlayersWithScores,
|
||||
aggregatedEvents: fullAggregatedEvents,
|
||||
activeRooms: rawActionsPayload?.activeRooms
|
||||
}"
|
||||
:compact="true"
|
||||
@update:round-filter="eventFilters.roundFilter.value = $event; onRoundGameChange()"
|
||||
@update:game-filter="eventFilters.gameFilter.value = $event; onRoundGameChange()"
|
||||
@reset-filters="eventFilters.resetFilters"
|
||||
@filtered="onFiltered"
|
||||
/>
|
||||
<hr class="divider" />
|
||||
|
||||
<div class="player-chips compact">
|
||||
<div class="search-controls">
|
||||
<input class="search" v-model="search" placeholder="Buscar jugador…" />
|
||||
<div class="pagination compact" v-if="pageCount > 1">
|
||||
<button class="pg-btn compact" @click="prevPage" :disabled="page <= 1">‹</button>
|
||||
<span class="pg-ind">{{ page }}/{{ pageCount }}</span>
|
||||
<button class="pg-btn compact" @click="nextPage" :disabled="page >= pageCount">›</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chips">
|
||||
<button
|
||||
v-for="p in playersPage"
|
||||
:key="p.uuid"
|
||||
class="chip"
|
||||
:class="{ active: selectedUuids.includes(p.uuid) }"
|
||||
@click="togglePlayer(p.uuid)"
|
||||
:title="p.uuid"
|
||||
:style="({ '--primary': p.color || '#667eea' } as any)"
|
||||
>
|
||||
<span class="avatar">{{ initials(p.name) }}</span>
|
||||
<span class="label">{{ p.name || 'Jugador' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Room slice UI removida: reemplazada por rango de tiempo/activas -->
|
||||
</div>
|
||||
@@ -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 @@
|
||||
</div>
|
||||
<pre v-show="showRaw" class="raw-pre">{{ prettyRaw }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Datos filtrados (vista actual) -->
|
||||
<div class="raw-viewer glass light">
|
||||
<div class="raw-header">
|
||||
<div class="raw-title">Datos filtrados (vista actual)</div>
|
||||
<div class="raw-actions">
|
||||
<button class="btn" @click="showFiltered = !showFiltered">{{ showFiltered ? 'Ocultar' : 'Mostrar' }}</button>
|
||||
<button class="btn" @click="copyFiltered" title="Copiar JSON">Copiar</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre v-show="showFiltered" class="raw-pre">{{ prettyFiltered }}</pre>
|
||||
</div>
|
||||
<AppCredits position="bottom-right" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -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<TimeMode>('range');
|
||||
const rangeFromStr = ref<string>('');
|
||||
const rangeToStr = ref<string>('');
|
||||
const liveEnd = ref<boolean>(true);
|
||||
let liveTimer: any = null;
|
||||
const fullAggregatedEvents = ref<Array<{ kind: string; round?: number; gameVariant?: string; roomId?: string; playerUuid?: string; timestamp?: number }>>([]);
|
||||
|
||||
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<string, number> = 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<string, number> = {};
|
||||
const ids = new Set<string>();
|
||||
ranged.forEach((ev, i) => {
|
||||
const rid = String(ev?.roomId || '').trim();
|
||||
if (!rid) return;
|
||||
ids.add(rid);
|
||||
lastSeenIndex[rid] = i;
|
||||
// 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[]
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
const fullAggregatedEvents = ref<Array<{ kind: string; round?: number; gameVariant?: string; roomId?: string; playerUuid?: string; timestamp?: number }>>([]);
|
||||
const filteredData = ref<any>(null);
|
||||
|
||||
// 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<any[]>([]);
|
||||
// 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<string[]>([]);
|
||||
const selectedRoomIds = ref<string[]>([]);
|
||||
const playerLoading = ref(false);
|
||||
const playerEventCounts = ref<Record<string, number>>((Object.fromEntries(EVENTS.map(k => [k, 0])) as Record<string, number>));
|
||||
const playersActionsByUuid = ref<Record<string, Record<string, number>>>({});
|
||||
|
||||
// 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<string, Record<string, number>> = {};
|
||||
@@ -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<string, number> = 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); }
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user