mejora filtro por salas
Some checks failed
build-and-deploy / build (push) Failing after 23s
build-and-deploy / deploy (push) Has been skipped

This commit is contained in:
2025-08-28 04:05:59 -06:00
parent 7de7263c41
commit 4f4dd2d2f3
2 changed files with 136 additions and 35 deletions

View File

@@ -97,7 +97,7 @@ export function useEventFilters() {
const parts = [];
if (roundFilter.value.length) parts.push(`Round ${roundFilter.value.join(',')}`);
if (gameFilter.value.length) parts.push(`Game ${gameFilter.value.join(',')}`);
if (roomFilter.value.length) parts.push(`Rooms ${roomFilter.value.map(r => r.slice(0,8)).join(',')}`);
if (roomFilter.value.length) parts.push(`Rooms ${roomFilter.value.length}`);
return parts.length > 0 ? parts.join(' + ') : 'Sin filtros';
});

View File

@@ -72,31 +72,30 @@
</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>
<!-- Room Filter Section: slice controls -->
<div class="room-slice">
<div class="slice-header">
<div class="slice-info">
<span class="total">Salas totales: {{ availableRooms.length }}</span>
<span v-if="availableRooms.length" class="slice-summary">
Analizando {{ roomSliceIds.length }} ({{ sliceStartLabel }}{{ sliceEndLabel }})
</span>
</div>
</div>
<div class="chips">
<button
v-for="r in roomsPage"
:key="r.roomId"
class="chip room-chip"
:class="{ active: selectedRoomIds.includes(r.roomId) }"
@click="toggleRoom(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="selectedRoomIds.length" class="chip clear" @click="clearRooms">Quitar selección</button>
</div>
<div class="dual-slider">
<div class="track"></div>
<div class="highlight" :style="{ left: startPct + '%', width: (endPct - startPct) + '%' }"></div>
<input class="range start" type="range" min="0" :max="maxIndex" v-model.number="roomSliceStart" @input="onSliceStart" />
<input class="range end" type="range" min="0" :max="maxIndex" v-model.number="roomSliceEnd" @input="onSliceEnd" />
</div>
<div class="quick-select">
<button class="qs-btn" @click="selectRecent(1)" title="Más reciente"></button>
<button class="qs-btn" @click="selectRecent(5)" title="5 recientes">5</button>
<button class="qs-btn" @click="selectRecent(10)" title="10 recientes">10</button>
<button class="qs-btn" @click="selectRecent(25)" title="25 recientes">25</button>
<button class="qs-btn" @click="addRecent(10)" title="Agregar 10 más">+10</button>
</div>
</div>
</div>
</Transition>
@@ -481,6 +480,46 @@ const roomsPage = computed(() => {
const selectedUuids = ref<string[]>([]);
const selectedRoomIds = ref<string[]>([]);
const roomSliceStart = ref(0);
const roomSliceEnd = ref(0);
const maxIndex = computed(() => Math.max(0, availableRooms.value.length - 1));
const sliceInitialized = ref(false);
const roomSliceIds = computed(() => {
const a = Math.max(0, Math.min(roomSliceStart.value | 0, maxIndex.value));
const b = Math.max(0, Math.min(roomSliceEnd.value | 0, maxIndex.value));
const start = Math.min(a, b);
const end = Math.max(a, b);
return availableRooms.value.slice(start, end + 1).map(r => r.roomId);
});
const sliceStartLabel = computed(() => roomSliceIds.value.length ? Math.min(roomSliceStart.value, roomSliceEnd.value) + 1 : 0);
const sliceEndLabel = computed(() => roomSliceIds.value.length ? Math.max(roomSliceStart.value, roomSliceEnd.value) + 1 : 0);
const startPct = computed(() => maxIndex.value > 0 ? (Math.min(roomSliceStart.value, roomSliceEnd.value) / maxIndex.value) * 100 : 0);
const endPct = computed(() => maxIndex.value > 0 ? (Math.max(roomSliceStart.value, roomSliceEnd.value) / maxIndex.value) * 100 : 0);
function onSliceStart() {
if (roomSliceStart.value > roomSliceEnd.value) roomSliceStart.value = roomSliceEnd.value;
}
function onSliceEnd() {
if (roomSliceEnd.value < roomSliceStart.value) roomSliceEnd.value = roomSliceStart.value;
}
function selectRecent(count: number) {
const m = maxIndex.value;
if (m < 0) return;
const start = Math.max(0, m - (count - 1));
roomSliceStart.value = start;
roomSliceEnd.value = m;
}
function addRecent(count: number) {
const m = maxIndex.value;
if (m < 0) return;
// Current selection length anchored at end
const currentLen = roomSliceIds.value.length;
const newLen = Math.min(m + 1, Math.max(0, currentLen) + Math.max(1, count));
const start = Math.max(0, m - (newLen - 1));
roomSliceStart.value = start;
roomSliceEnd.value = m;
}
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>>>({});
@@ -521,11 +560,33 @@ function toggleRoom(roomId: string) {
}
function clearRooms() {
suppressSliceSync = true;
selectedRoomIds.value = [];
eventFilters.roomFilter.value = [];
eventFilters.applyFilters(EVENTS);
// allow user to re-enable by moving sliders
requestAnimationFrame(() => { suppressSliceSync = false; });
}
// Keep event filters in sync with slice selection
let suppressSliceSync = false;
// Initialize full-range selection as soon as rooms are available (first time)
watch(maxIndex, (m) => {
if (m >= 0 && !sliceInitialized.value) {
roomSliceStart.value = 0;
roomSliceEnd.value = m;
selectedRoomIds.value = [...roomSliceIds.value];
eventFilters.roomFilter.value = [...selectedRoomIds.value];
sliceInitialized.value = true;
}
});
watch([roomSliceStart, roomSliceEnd, () => availableRooms.value.length], () => {
if (suppressSliceSync) return;
selectedRoomIds.value = [...roomSliceIds.value];
eventFilters.roomFilter.value = [...selectedRoomIds.value];
eventFilters.applyFilters(EVENTS);
});
function goHome() {
router.push('/');
}
@@ -687,20 +748,31 @@ function setupStreams() {
playersActionsByUuid.value = byUuid;
eventFilters.updateAggregatedData(allDetailedEvents, aggregatedCounts);
// Extract unique room IDs from aggregated events - this is our primary source for rooms
// Extract unique room IDs from aggregated events and track "newness" by last seen index
const roomIds = new Set<string>();
allDetailedEvents.forEach(event => {
if (event.roomId && event.roomId.trim()) {
roomIds.add(event.roomId);
}
const lastSeenIndex: Record<string, number> = {};
allDetailedEvents.forEach((event, idx) => {
const rid = (event.roomId || '').trim();
if (!rid) return;
roomIds.add(rid);
lastSeenIndex[rid] = idx; // increasing idx means newer
});
// 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
}));
// Build available rooms list sorted by lastSeenIndex ASC (older first, newest get the highest index)
availableRooms.value = Array.from(roomIds)
.map(roomId => ({
roomId,
name: `Sala ${roomId.slice(0, 8)}`,
playerCount: allDetailedEvents.filter(e => e.roomId === roomId).length,
_lastSeen: lastSeenIndex[roomId] ?? -1
}))
.sort((a, b) => (a._lastSeen - b._lastSeen))
.map(({ _lastSeen, ...rest }) => rest);
// Initialize slice window: full range by default, preserve user selection if present
if (availableRooms.value.length > 0 && selectedRoomIds.value.length === 0) {
roomSliceStart.value = 0;
roomSliceEnd.value = availableRooms.value.length - 1;
}
// Compute metrics from score history
computeMetricsFromScores();
@@ -938,6 +1010,35 @@ function downloadJSON() {
border-top: 1px solid rgba(203, 213, 225, 0.5);
}
/* Room slice controls */
.room-slice { margin-top: 10px; padding-top: 12px; border-top: 1px solid rgba(203,213,225,0.5); display: flex; flex-direction: column; gap: 10px; }
.slice-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.slice-info { display: flex; align-items: center; gap: 10px; color: #334155; font-weight: 600; }
.slice-summary { color: #475569; font-weight: 700; }
/* Dual range slider */
.dual-slider { position: relative; height: 36px; padding: 16px 8px; }
.dual-slider .track { position: absolute; left: 8px; right: 8px; top: 50%; height: 8px; transform: translateY(-50%); border-radius: 999px; background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%); box-shadow: inset 0 1px 2px rgba(0,0,0,0.06); }
.dual-slider .highlight { position: absolute; top: 50%; height: 8px; transform: translateY(-50%); border-radius: 999px; background: linear-gradient(90deg, #06b6d4, #8b5cf6); box-shadow: 0 2px 8px rgba(139,92,246,0.25); }
.dual-slider .range { -webkit-appearance: none; appearance: none; position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: transparent; pointer-events: none; }
.dual-slider .range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 22px; height: 22px; background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); border: 2px solid #8b5cf6; border-radius: 50%; box-shadow: 0 4px 12px rgba(139,92,246,0.3); pointer-events: auto; cursor: pointer; }
.dual-slider .range::-moz-range-thumb { width: 22px; height: 22px; background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); border: 2px solid #8b5cf6; border-radius: 50%; box-shadow: 0 4px 12px rgba(139,92,246,0.3); pointer-events: auto; cursor: pointer; }
.dual-slider .range::-ms-thumb { width: 22px; height: 22px; background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); border: 2px solid #8b5cf6; border-radius: 50%; box-shadow: 0 4px 12px rgba(139,92,246,0.3); pointer-events: auto; cursor: pointer; }
.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; }
.dual-slider .track, .dual-slider .highlight { height: 6px; }
.dual-slider .range::-webkit-slider-thumb, .dual-slider .range::-moz-range-thumb { width: 18px; height: 18px; }
}
.search-controls {
display: flex;
align-items: center;