mejora filtro por salas
This commit is contained in:
@@ -97,7 +97,7 @@ export function useEventFilters() {
|
|||||||
const parts = [];
|
const parts = [];
|
||||||
if (roundFilter.value.length) parts.push(`Round ${roundFilter.value.join(',')}`);
|
if (roundFilter.value.length) parts.push(`Round ${roundFilter.value.join(',')}`);
|
||||||
if (gameFilter.value.length) parts.push(`Game ${gameFilter.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';
|
return parts.length > 0 ? parts.join(' + ') : 'Sin filtros';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -72,31 +72,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Room Filter Section -->
|
<!-- Room Filter Section: slice controls -->
|
||||||
<div class="room-chips">
|
<div class="room-slice">
|
||||||
<div class="search-controls">
|
<div class="slice-header">
|
||||||
<input class="search" v-model="roomSearch" placeholder="Buscar sala…" />
|
<div class="slice-info">
|
||||||
<div class="pagination compact" v-if="roomPageCount > 1">
|
<span class="total">Salas totales: {{ availableRooms.length }}</span>
|
||||||
<button class="pg-btn compact" @click="prevRoomPage" :disabled="roomPage <= 1">‹</button>
|
<span v-if="availableRooms.length" class="slice-summary">
|
||||||
<span class="pg-ind">{{ roomPage }}/{{ roomPageCount }}</span>
|
Analizando {{ roomSliceIds.length }} ({{ sliceStartLabel }}–{{ sliceEndLabel }})
|
||||||
<button class="pg-btn compact" @click="nextRoomPage" :disabled="roomPage >= roomPageCount">›</button>
|
</span>
|
||||||
</div>
|
</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>
|
<button v-if="selectedRoomIds.length" class="chip clear" @click="clearRooms">Quitar selección</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
@@ -481,6 +480,46 @@ const roomsPage = computed(() => {
|
|||||||
|
|
||||||
const selectedUuids = ref<string[]>([]);
|
const selectedUuids = ref<string[]>([]);
|
||||||
const selectedRoomIds = 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 playerLoading = ref(false);
|
||||||
const playerEventCounts = ref<Record<string, number>>((Object.fromEntries(EVENTS.map(k => [k, 0])) as Record<string, number>));
|
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>>>({});
|
const playersActionsByUuid = ref<Record<string, Record<string, number>>>({});
|
||||||
@@ -521,11 +560,33 @@ function toggleRoom(roomId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearRooms() {
|
function clearRooms() {
|
||||||
|
suppressSliceSync = true;
|
||||||
selectedRoomIds.value = [];
|
selectedRoomIds.value = [];
|
||||||
eventFilters.roomFilter.value = [];
|
eventFilters.roomFilter.value = [];
|
||||||
eventFilters.applyFilters(EVENTS);
|
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() {
|
function goHome() {
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}
|
}
|
||||||
@@ -687,20 +748,31 @@ function setupStreams() {
|
|||||||
playersActionsByUuid.value = byUuid;
|
playersActionsByUuid.value = byUuid;
|
||||||
eventFilters.updateAggregatedData(allDetailedEvents, aggregatedCounts);
|
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>();
|
const roomIds = new Set<string>();
|
||||||
allDetailedEvents.forEach(event => {
|
const lastSeenIndex: Record<string, number> = {};
|
||||||
if (event.roomId && event.roomId.trim()) {
|
allDetailedEvents.forEach((event, idx) => {
|
||||||
roomIds.add(event.roomId);
|
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
|
// Build available rooms list sorted by lastSeenIndex ASC (older first, newest get the highest index)
|
||||||
availableRooms.value = Array.from(roomIds).map(roomId => ({
|
availableRooms.value = Array.from(roomIds)
|
||||||
roomId,
|
.map(roomId => ({
|
||||||
name: `Sala ${roomId.slice(0, 8)}`,
|
roomId,
|
||||||
playerCount: allDetailedEvents.filter(e => e.roomId === roomId).length
|
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
|
// Compute metrics from score history
|
||||||
computeMetricsFromScores();
|
computeMetricsFromScores();
|
||||||
@@ -938,6 +1010,35 @@ function downloadJSON() {
|
|||||||
border-top: 1px solid rgba(203, 213, 225, 0.5);
|
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 {
|
.search-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user