leaderboard implementado v3

This commit is contained in:
2025-08-28 02:47:07 -06:00
parent 220248b588
commit 318a7847b8
4 changed files with 150 additions and 106 deletions

View File

@@ -248,11 +248,22 @@ async function handleFileUpload(event: Event) {
}
const result = await response.json();
alert(`Estado cargado exitosamente. ${result.message || 'NameManager actualizado.'}`);
const message = result.importedUuids
? `Estado cargado exitosamente. ${result.importedUuids} UUIDs importados. ${result.message || ''}`
: `Estado cargado exitosamente. ${result.message || 'NameManager actualizado.'}`;
alert(message);
} catch (error) {
console.error('Error uploading nameManager state:', error);
alert('Error al cargar el estado del nameManager. Verifica que el archivo sea válido.');
// Handle different error types
if (error.message && error.message.includes('413')) {
alert('Error: El archivo es demasiado grande (máximo 50MB). Considera crear un archivo de guardado más pequeño.');
} else if (error.message && error.message.includes('400')) {
alert('Error: Archivo inválido. Asegúrate de subir un archivo .snatchSave válido.');
} else {
alert('Error al cargar el estado del nameManager. Verifica que el archivo sea válido y la conexión sea estable.');
}
} finally {
// Reset file input
target.value = '';

View File

@@ -13,8 +13,8 @@
<span class="collapse-text">{{ filtersCollapsed ? 'Mostrar filtros' : 'Ocultar filtros' }}</span>
</button>
<button class="btn" @click="refreshAll" :disabled="loading">{{ loading ? 'Actualizando…' : 'Actualizar' }}</button>
<button class="btn" @click="downloadCSV" :disabled="loading" title="Descargar datos actuales como CSV">
📊 CSV
<button class="btn" @click="downloadJSON" :disabled="loading" title="Descargar datos actuales como JSON">
📊 JSON
</button>
</div>
</div>
@@ -236,6 +236,11 @@ function computeMetricsFromScores() {
if (player.name) {
totalPlayersWithNames++;
// Count players with shame tokens
if (player.shameTokens && player.shameTokens > 0) {
playersWithShame++;
}
// Extract scores from roomScoreHistory if available
if (player.roomScoreHistory) {
player.roomScoreHistory.forEach((roomScore: any) => {
@@ -273,13 +278,13 @@ function computeMetricsFromScores() {
// Function to compute metrics for a selected player
function computeSelectedPlayerMetrics(uuid: string) {
const playerData = allPlayersWithScores.value.find(p => p.uuid === uuid);
if (!playerData?.roomScoreHistory) {
if (!playerData) {
selectedPlayerMetrics.value = {
players_seated: 1,
score_p1: 0,
score_p2: 0,
players_with_shame: 0,
players_without_shame: 0
players_without_shame: 1
};
return;
}
@@ -288,30 +293,34 @@ function computeSelectedPlayerMetrics(uuid: string) {
let totalP2Scores = 0;
let p1Count = 0;
let p2Count = 0;
const hasShame = playerData.shameTokens && playerData.shameTokens > 0;
playerData.roomScoreHistory.forEach((roomScore: any) => {
roomScore.scores.forEach((score: any) => {
// Apply filters to scores
if (!scorePassesFilters(score, roomScore.roomId)) {
return;
}
if (score.role === 'P1') {
totalP1Scores += score.score;
p1Count++;
} else if (score.role === 'P2') {
totalP2Scores += score.score;
p2Count++;
}
if (playerData.roomScoreHistory) {
playerData.roomScoreHistory.forEach((roomScore: any) => {
roomScore.scores.forEach((score: any) => {
// Apply filters to scores
if (!scorePassesFilters(score, roomScore.roomId)) {
return;
}
if (score.role === 'P1') {
totalP1Scores += score.score;
p1Count++;
} else if (score.role === 'P2') {
totalP2Scores += score.score;
p2Count++;
}
});
});
});
}
selectedPlayerMetrics.value = {
players_seated: 1,
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: 0, // Would need shame data
players_without_shame: 0
players_with_shame: hasShame ? 1 : 0,
players_without_shame: hasShame ? 0 : 1
};
}
@@ -815,86 +824,82 @@ watch(() => playersFiltered.value.length, () => {
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', 'RoomId', 'DataSource'];
// Create CSV rows - one row per individual event occurrence
const rows: string[][] = [];
// Add detailed individual event rows if we have detailed data
currentEvents.forEach(event => {
if (EVENTS.includes(event.kind)) {
rows.push([
'event',
event.kind,
'1',
event.round?.toString() || '',
event.gameVariant || '',
event.playerUuid || '',
event.playerName || '',
event.roomId || '',
eventFilters.dataSource.value
]);
}
});
// Also add per-player aggregated data from playersActionsByUuid
Object.entries(playersActionsByUuid.value).forEach(([uuid, counts]) => {
const player = players.value.find(p => p.uuid === uuid);
EVENTS.forEach(eventType => {
const count = counts[eventType] || 0;
if (count > 0) {
// Add one row per occurrence for proper import compatibility
for (let i = 0; i < count; i++) {
rows.push([
'event',
eventType,
'1',
'', // Round info not available in aggregated player data
'', // GameVariant info not available in aggregated player data
uuid,
player?.name || '',
'', // Room info not available in aggregated player data
'player-aggregated'
]);
}
function downloadJSON() {
// Create a comprehensive snapshot of current interface data
const currentData = {
metadata: {
timestamp: new Date().toISOString(),
dataSource: eventFilters.dataSource.value,
filters: {
room: eventFilters.roomFilter.value,
round: eventFilters.roundFilter.value,
game: eventFilters.gameFilter.value,
hasActiveFilters: eventFilters.hasActiveFilters.value,
filterSummary: eventFilters.filterSummary.value,
selectedPlayer: selectedUuid.value ? {
uuid: selectedUuid.value,
name: players.value.find(p => p.uuid === selectedUuid.value)?.name || 'Jugador'
} : null,
selectedRoom: selectedRoomId.value ? {
roomId: selectedRoomId.value,
name: availableRooms.value.find(r => r.roomId === selectedRoomId.value)?.name || 'Sala'
} : null
}
});
});
},
// Current chart totals being displayed
chartTotals: {
offers: offersTotal.value,
responses: responsesTotal.value,
force: forceTotal.value,
shame: shameTotal.value,
report: reportTotal.value,
averageScore: averageScoreTotal.value,
totalPlayers: totalPlayersCount.value
},
// Global event counts (filtered)
globalEventCounts: eventFilters.globalEventCounts.value,
// Selected player event counts (if applicable)
selectedPlayerEventCounts: selectedUuid.value ? playerEventCounts.value : null,
// Additional metrics being displayed
additionalMetrics: additionalMetrics.value,
// Selected player metrics (if applicable)
selectedPlayerMetrics: selectedUuid.value ? selectedPlayerMetrics.value : null,
// Combined counts used in charts
combinedGlobalCounts: combinedGlobalCounts.value,
combinedPlayerCounts: selectedUuid.value ? combinedPlayerCounts.value : null,
// Players data
players: players.value,
availableRooms: availableRooms.value,
// Current filtered data if available
currentSourceEvents: eventFilters.currentSourceEvents.value,
// Per-player action counts
playersActionsByUuid: playersActionsByUuid.value,
// All players with scores data
allPlayersWithScores: allPlayersWithScores.value.map(player => ({
uuid: player.uuid,
name: player.name,
total: player.total,
shameTokens: player.shameTokens,
counts: player.counts,
roomScoreHistory: player.roomScoreHistory
}))
};
// Add metric data
const currentMetrics = eventFilters.dataSource.value === 'aggregated'
? additionalMetrics.value
: additionalMetrics.value; // Same source for now
METRICS.forEach(metricType => {
const count = currentMetrics[metricType] || 0;
if (count > 0) {
rows.push([
'metric',
metricType,
count.toString(),
'',
'',
'',
'',
'',
eventFilters.dataSource.value
]);
}
});
// Convert to CSV string
const csvContent = [
headers.join(','),
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
].join('\n');
// Convert to formatted JSON string
const jsonString = JSON.stringify(currentData, null, 2);
// Create and download file
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
@@ -903,7 +908,7 @@ function downloadCSV() {
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 filename = `datos_${dataSourceLabel}${filterLabel}_${timestamp}.csv`;
const filename = `leaderboard_${dataSourceLabel}${filterLabel}_${timestamp}.json`;
link.setAttribute('download', filename);
link.style.visibility = 'hidden';