agregado filtro de eventos por sala en la que se realizaron

This commit is contained in:
2025-08-27 23:23:17 -06:00
parent ffd97646ab
commit 5321870336
4 changed files with 162 additions and 14 deletions

View File

@@ -15,6 +15,9 @@
<span class="filter-tag player-tag" v-if="selectedPlayerUuid && activeFilters?.selectedPlayer">
👤 {{ activeFilters.selectedPlayer }}
</span>
<span class="filter-tag room-tag" v-if="activeFilters?.selectedRoom">
🏠 {{ activeFilters.selectedRoom }}
</span>
</div>
</div>
<div v-if="loading" class="placeholder">Cargando datos</div>
@@ -124,6 +127,7 @@ interface Props {
game: string;
hasFilters: boolean;
selectedPlayer?: string;
selectedRoom?: string;
};
}

View File

@@ -1,4 +1,4 @@
import { ref, computed, watch } from 'vue';
import { ref, computed } from 'vue';
export interface DetailedEvent {
kind: string;
@@ -6,17 +6,20 @@ export interface DetailedEvent {
gameVariant?: string;
playerUuid?: string;
playerName?: string;
roomId?: string;
}
export type DataSource = 'aggregated' | 'active-rooms';
export type RoundFilter = 'all' | 1 | 2 | 3;
export type GameFilter = 'all' | 'G1' | 'G2' | 'G3' | 'G4' | 'G5';
export type RoomFilter = 'all' | string;
export function useEventFilters() {
// Filter states
const dataSource = ref<DataSource>('aggregated');
const roundFilter = ref<RoundFilter>('all');
const gameFilter = ref<GameFilter>('all');
const roomFilter = ref<RoomFilter>('all');
// Event data stores
const detailedEventsAggregated = ref<DetailedEvent[]>([]);
@@ -33,7 +36,7 @@ export function useEventFilters() {
? detailedEventsAggregated.value
: detailedEventsActiveRooms.value;
// Filter events based on round and game
// Filter events based on round, game, and room
const filteredEvents = sourceEvents.filter(event => {
if (roundFilter.value !== 'all' && event.round !== roundFilter.value) {
return false;
@@ -41,6 +44,9 @@ export function useEventFilters() {
if (gameFilter.value !== 'all' && event.gameVariant !== gameFilter.value) {
return false;
}
if (roomFilter.value !== 'all' && event.roomId !== roomFilter.value) {
return false;
}
return true;
});
@@ -71,6 +77,7 @@ export function useEventFilters() {
function resetFilters() {
roundFilter.value = 'all';
gameFilter.value = 'all';
roomFilter.value = 'all';
}
// Computed properties
@@ -83,13 +90,14 @@ export function useEventFilters() {
);
const hasActiveFilters = computed(() =>
roundFilter.value !== 'all' || gameFilter.value !== 'all'
roundFilter.value !== 'all' || gameFilter.value !== 'all' || roomFilter.value !== 'all'
);
const filterSummary = computed(() => {
const parts = [];
if (roundFilter.value !== 'all') parts.push(`Round ${roundFilter.value}`);
if (gameFilter.value !== 'all') parts.push(`Game ${gameFilter.value}`);
if (roomFilter.value !== 'all') parts.push(`Room ${roomFilter.value.slice(0, 8)}`);
return parts.length > 0 ? parts.join(' + ') : 'Sin filtros';
});
@@ -98,6 +106,7 @@ export function useEventFilters() {
dataSource,
roundFilter,
gameFilter,
roomFilter,
detailedEventsAggregated,
detailedEventsActiveRooms,
globalEventCounts,

View File

@@ -42,6 +42,8 @@
<span class="key global"></span> Global
<span class="sep">·</span>
<span class="key player" v-if="selectedUuid"></span> Jugador
<span class="sep" v-if="selectedRoomId">·</span>
<span class="key room" v-if="selectedRoomId"></span> Sala
</div>
<div class="player-chips">
<div class="search-controls">
@@ -68,6 +70,33 @@
<button v-if="selectedUuid" class="chip clear" @click="clearPlayer">Quitar selección</button>
</div>
</div>
<!-- Room Filter Section -->
<div class="room-chips">
<div class="search-controls">
<input class="search" v-model="roomSearch" placeholder="Buscar sala…" />
<div class="pagination compact" v-if="roomPageCount > 1">
<button class="pg-btn compact" @click="prevRoomPage" :disabled="roomPage <= 1"></button>
<span class="pg-ind">{{ roomPage }}/{{ roomPageCount }}</span>
<button class="pg-btn compact" @click="nextRoomPage" :disabled="roomPage >= roomPageCount"></button>
</div>
</div>
<div class="chips">
<button
v-for="r in roomsPage"
:key="r.roomId"
class="chip room-chip"
:class="{ active: r.roomId === selectedRoomId }"
@click="selectRoom(r.roomId)"
:title="`Sala: ${r.roomId} (${r.playerCount || 0} jugadores)`"
>
<span class="avatar">🏠</span>
<span class="label">{{ r.name }}</span>
<span class="count" v-if="r.playerCount">{{ r.playerCount }}</span>
</button>
<button v-if="selectedRoomId" class="chip clear" @click="clearRoom">Quitar selección</button>
</div>
</div>
</div>
</Transition>
@@ -85,8 +114,9 @@
dataSource: eventFilters.dataSource.value,
round: eventFilters.roundFilter.value.toString(),
game: eventFilters.gameFilter.value,
hasFilters: eventFilters.hasActiveFilters.value,
selectedPlayer: selectedUuid ? players.find(p => p.uuid === selectedUuid)?.name || 'Jugador' : undefined
hasFilters: eventFilters.hasActiveFilters.value || !!selectedRoomId,
selectedPlayer: selectedUuid ? players.find(p => p.uuid === selectedUuid)?.name || 'Jugador' : undefined,
selectedRoom: selectedRoomId ? availableRooms.find(r => r.roomId === selectedRoomId)?.name || 'Sala' : undefined
}"
/>
</div>
@@ -235,7 +265,7 @@ function computeMetrics(roomDetails: any) {
}
// Function to compute metrics for a selected player
function computeSelectedPlayerMetrics(uuid: string) {
function computeSelectedPlayerMetrics(_uuid: string) {
// Individual player metrics are simpler - just show if they're seated, their score, etc.
// For now, set basic values. In a real implementation, we'd need to query current player state
selectedPlayerMetrics.value = {
@@ -258,7 +288,7 @@ const combinedPlayerCounts = computed(() => ({
}));
// Watch for changes in filters and data source
watch([eventFilters.dataSource, eventFilters.roundFilter, eventFilters.gameFilter], () => {
watch([eventFilters.dataSource, eventFilters.roundFilter, eventFilters.gameFilter, eventFilters.roomFilter], () => {
eventFilters.applyFilters(EVENTS);
});
@@ -266,18 +296,33 @@ watch([eventFilters.dataSource, eventFilters.roundFilter, eventFilters.gameFilte
const rooms = ref<RoomInfo[]>([]);
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
@@ -293,13 +338,35 @@ const dynamicPageSize = computed(() => {
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 = selectedRoomId.value ? 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 roomsPage = computed(() => {
const start = (roomPage.value - 1) * roomDynamicPageSize.value;
return roomsFiltered.value.slice(start, start + roomDynamicPageSize.value);
});
const selectedUuid = ref('');
const selectedRoomId = ref('');
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>>>({});
@@ -319,6 +386,10 @@ function initials(name: string): string {
return chars || '🙂';
}
function shortRoomId(roomId: string): string {
return roomId.slice(0, 8);
}
function selectPlayer(uuid: string) {
if (selectedUuid.value === uuid) return;
selectedUuid.value = uuid;
@@ -329,8 +400,23 @@ function clearPlayer() {
selectedUuid.value = '';
}
function selectRoom(roomId: string) {
if (selectedRoomId.value === roomId) return;
selectedRoomId.value = roomId;
eventFilters.roomFilter.value = roomId;
eventFilters.applyFilters(EVENTS);
}
function clearRoom() {
selectedRoomId.value = '';
eventFilters.roomFilter.value = 'all';
eventFilters.applyFilters(EVENTS);
}
function prevPage() { page.value = Math.max(1, page.value - 1); }
function nextPage() { page.value = Math.min(pageCount.value, page.value + 1); }
function prevRoomPage() { roomPage.value = Math.max(1, roomPage.value - 1); }
function nextRoomPage() { roomPage.value = Math.min(roomPageCount.value, roomPage.value + 1); }
// Ensure page doesn't exceed pageCount when players change
watch(pageCount, (newCount) => {
@@ -339,6 +425,12 @@ watch(pageCount, (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(() => {
const p = players.value.find(x => x.uuid === selectedUuid.value);
@@ -372,11 +464,13 @@ function setupStreams() {
const data = JSON.parse((e as MessageEvent).data || '{}');
const details = data?.roomDetails || {};
// Room details are for active rooms, we'll get room data from player history instead
// Collect detailed events for active rooms
const detailedEvents: Array<{ kind: string; round?: number; gameVariant?: string }> = [];
const detailedEvents: Array<{ kind: string; round?: number; gameVariant?: string; roomId?: string }> = [];
const counts: Record<string, number> = Object.fromEntries(EVENTS.map(k => [k, 0])) as any;
Object.values(details).forEach((d: any) => {
Object.entries(details).forEach(([roomId, d]: [string, any]) => {
(Array.isArray(d?.systemMessages) ? d.systemMessages : []).forEach((m: any) => {
const k = (m?.kind || '').toString();
if (EVENTS.includes(k)) {
@@ -384,7 +478,8 @@ function setupStreams() {
detailedEvents.push({
kind: k,
round: m?.round,
gameVariant: m?.gameVariant || m?.variant
gameVariant: m?.gameVariant || m?.variant,
roomId: m?.roomId || roomId // Use message roomId if available, otherwise use room key
});
}
});
@@ -395,6 +490,8 @@ function setupStreams() {
// Compute additional metrics
computeMetrics(details);
// Don't extract rooms from active data - we'll get them from aggregated player history
// Apply filters and update display
if (eventFilters.dataSource.value === 'active-rooms') {
eventFilters.applyFilters(EVENTS);
@@ -445,8 +542,9 @@ function setupStreams() {
const list = Array.isArray(data?.players) ? data.players : [];
allPlayersActions.value = list.map((p: any) => ({ uuid: String(p.uuid||''), name: String(p.name||''), total: Number(p.total||0) }));
// Collect all detailed events from all players
const allDetailedEvents: Array<{ kind: string; round?: number; gameVariant?: string }> = [];
const allDetailedEvents: Array<{ kind: string; round?: number; gameVariant?: string; roomId?: string }> = [];
const aggregatedCounts: Record<string, number> = Object.fromEntries(EVENTS.map(k => [k, 0])) as any;
// Update detailed counts map
@@ -473,6 +571,21 @@ function setupStreams() {
playersActionsByUuid.value = byUuid;
eventFilters.updateAggregatedData(allDetailedEvents, aggregatedCounts);
// Extract unique room IDs from aggregated events - this is our primary source for rooms
const roomIds = new Set<string>();
allDetailedEvents.forEach(event => {
if (event.roomId && event.roomId.trim()) {
roomIds.add(event.roomId);
}
});
// Build available rooms list from aggregated events
availableRooms.value = Array.from(roomIds).map(roomId => ({
roomId,
name: `Sala ${roomId.slice(0, 8)}`,
playerCount: allDetailedEvents.filter(e => e.roomId === roomId).length
}));
// Apply filters and update display if viewing aggregated data
if (eventFilters.dataSource.value === 'aggregated') {
eventFilters.applyFilters(EVENTS);
@@ -580,11 +693,12 @@ watch(() => playersFiltered.value.length, () => {
// Removed totals table and sorting; keep actions stream for per-player counts only
const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([]);
function downloadCSV() {
const currentEvents = eventFilters.currentSourceEvents.value;
// Create CSV headers
const headers = ['Type', 'Event', 'Count', 'Round', 'GameVariant', 'PlayerUuid', 'PlayerName', 'DataSource'];
const headers = ['Type', 'Event', 'Count', 'Round', 'GameVariant', 'PlayerUuid', 'PlayerName', 'RoomId', 'DataSource'];
// Create CSV rows - one row per individual event occurrence
const rows: string[][] = [];
@@ -600,6 +714,7 @@ function downloadCSV() {
event.gameVariant || '',
event.playerUuid || '',
event.playerName || '',
event.roomId || '',
eventFilters.dataSource.value
]);
}
@@ -621,6 +736,7 @@ function downloadCSV() {
'', // GameVariant info not available in aggregated player data
uuid,
player?.name || '',
'', // Room info not available in aggregated player data
'player-aggregated'
]);
}
@@ -644,6 +760,7 @@ function downloadCSV() {
'',
'',
'',
'',
eventFilters.dataSource.value
]);
}
@@ -695,14 +812,21 @@ function downloadCSV() {
.key { width: 12px; height: 12px; border-radius: 999px; display:inline-block; }
.key.global { background: linear-gradient(90deg, #34d399, #10b981); box-shadow: 0 0 8px rgba(16,185,129,0.35); }
.key.player { background: linear-gradient(90deg, #a78bfa, #6366f1); box-shadow: 0 0 8px rgba(99,102,241,0.35); }
.key.room { background: linear-gradient(90deg, #f59e0b, #d97706); box-shadow: 0 0 8px rgba(245,158,11,0.35); }
.sep { opacity: 0.6; }
.player-chips {
.player-chips, .room-chips {
display: flex;
flex-direction: column;
gap: 12px;
}
.room-chips {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(203, 213, 225, 0.5);
}
.search-controls {
display: flex;
align-items: center;
@@ -736,6 +860,16 @@ function downloadCSV() {
.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); }
.chip.clear { background:#fff; border-style:dashed; color:#334155; }
.chip.room-chip { --primary: #f59e0b; }
.chip.room-chip .count {
background: color-mix(in srgb, var(--primary) 20%, white);
color: color-mix(in srgb, var(--primary) 80%, #111);
font-size: 11px;
font-weight: 800;
padding: 2px 6px;
border-radius: 10px;
margin-left: 4px;
}
.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); }

View File

@@ -828,7 +828,8 @@ async function sendPlayersActionsUpdate(client?: Response) {
detailedHistory.push({
kind,
round: (entry as any)?.round,
gameVariant: (entry as any)?.gameVariant || (entry as any)?.variant
gameVariant: (entry as any)?.gameVariant || (entry as any)?.variant,
roomId: (entry as any)?.roomId
});
}
const total = ACTION_EVENTS.reduce((acc, k) => acc + (counts[k] || 0), 0);