reset UUID y shame persistente
This commit is contained in:
112
client/src/components/DashboardActions.vue
Normal file
112
client/src/components/DashboardActions.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="actions-container">
|
||||
<div class="section-header">
|
||||
<h2>📥 Export / Tools</h2>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-export" @click="downloadCsv">Descargar resultados (CSV)</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface PlayerRow {
|
||||
sessionId: string;
|
||||
uuid?: string;
|
||||
name: string;
|
||||
role: 'P1' | 'P2' | '';
|
||||
pavoTokens?: number;
|
||||
eloteTokens?: number;
|
||||
shameTokens?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface RoomDetail {
|
||||
roomId: string;
|
||||
players?: PlayerRow[];
|
||||
gameStatus?: string;
|
||||
variant?: string;
|
||||
round?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{ rooms: any[]; roomDetails: { [key: string]: RoomDetail } }>();
|
||||
|
||||
const gameRooms = computed(() => props.rooms.filter(r => r.name === 'game'));
|
||||
|
||||
function csvEscape(v: any): string {
|
||||
const s = String(v ?? '').replace(/\r?\n/g, ' ');
|
||||
if (/[",]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
|
||||
return s;
|
||||
}
|
||||
|
||||
function buildCsv(): string {
|
||||
const headers = [
|
||||
'roomId', 'variant', 'round', 'status',
|
||||
'p1_name', 'p1_pavo', 'p1_elote',
|
||||
'p2_name', 'p2_pavo', 'p2_elote'
|
||||
];
|
||||
const lines: string[] = [headers.join(',')];
|
||||
|
||||
gameRooms.value.forEach(room => {
|
||||
const det = props.roomDetails[room.roomId] || {};
|
||||
const status = (det as any).gameStatus || room?.metadata?.gameStatus || 'waiting';
|
||||
const variant = (det as any).variant || room?.metadata?.currentVariant || 'G1';
|
||||
const round = (det as any).round || room?.metadata?.currentRound || 1;
|
||||
const players = (det.players || []) as PlayerRow[];
|
||||
const p1 = players.find(p => p.role === 'P1') || players[0] || ({} as any);
|
||||
const p2 = players.find(p => p.role === 'P2') || players[1] || ({} as any);
|
||||
|
||||
const row = [
|
||||
room.roomId,
|
||||
variant,
|
||||
round,
|
||||
status,
|
||||
p1?.name || '',
|
||||
p1?.pavoTokens ?? 0,
|
||||
p1?.eloteTokens ?? 0,
|
||||
p2?.name || '',
|
||||
p2?.pavoTokens ?? 0,
|
||||
p2?.eloteTokens ?? 0
|
||||
].map(csvEscape).join(',');
|
||||
lines.push(row);
|
||||
});
|
||||
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
function downloadCsv() {
|
||||
const csv = buildCsv();
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const now = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const fname = `snatch-results-${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.csv`;
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fname;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.actions-container {
|
||||
margin-bottom: 24px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
.section-header {
|
||||
display:flex; align-items:center; justify-content:space-between; margin-bottom:10px;
|
||||
}
|
||||
.section-header h2 { margin:0; font-size: 1.3rem; }
|
||||
.buttons { display:flex; gap: 10px; flex-wrap: wrap; }
|
||||
.btn { padding: 10px 14px; border:none; border-radius: 10px; font-weight:700; cursor:pointer; }
|
||||
.btn-export { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color:white; }
|
||||
.btn-export:hover { filter: brightness(1.05); }
|
||||
</style>
|
||||
|
||||
@@ -123,6 +123,14 @@
|
||||
>
|
||||
🏠 Send All to Lobby
|
||||
</button>
|
||||
<button
|
||||
@click="resetAllUuidProfiles"
|
||||
class="btn btn-reset-profiles"
|
||||
:disabled="isLoadingGlobal"
|
||||
title="Borrar nombre, color y vergüenza de todos los UUIDs"
|
||||
>
|
||||
🧹 Reset All UUID Profiles
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -443,8 +451,8 @@ async function pauseAllGames() {
|
||||
console.error('Failed to pause all games:', error);
|
||||
alert('Failed to pause all games. Check console for details.');
|
||||
} finally {
|
||||
isLoadingGlobal.value = false;
|
||||
}
|
||||
isLoadingGlobal.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resumeAllGames() {
|
||||
@@ -561,6 +569,40 @@ async function sendAllToLobby() {
|
||||
}
|
||||
}
|
||||
|
||||
async function resetAllUuidProfiles() {
|
||||
if (!confirm('¿Seguro que deseas resetear nombre, color y vergüenza de TODOS los UUIDs? Esta acción no se puede deshacer.')) return;
|
||||
isLoadingGlobal.value = true;
|
||||
try {
|
||||
const response = await fetch(`${apiBase()}/admin/reset-uuid-profiles`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to reset UUID profiles');
|
||||
const result = await response.json();
|
||||
console.log(result?.message || 'UUID profiles reset');
|
||||
alert(result?.message || 'UUID profiles reset');
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.error('Failed to reset UUID profiles:', error);
|
||||
alert('Failed to reset UUID profiles. Check console for details.');
|
||||
} finally {
|
||||
isLoadingGlobal.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Build API base from env or current origin
|
||||
function apiBase(): string {
|
||||
try {
|
||||
const raw = (import.meta.env as any)?.VITE_API_URL as string | undefined;
|
||||
const env = (raw || '').trim();
|
||||
if (env) return env.replace(/\/$/, '');
|
||||
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000';
|
||||
return `${origin.replace(/\/$/, '')}/api`;
|
||||
} catch {
|
||||
return 'http://localhost:3000/api';
|
||||
}
|
||||
}
|
||||
|
||||
function initSSE() {
|
||||
try {
|
||||
console.log('[Dashboard] Initializing SSE connection...');
|
||||
@@ -853,6 +895,11 @@ const selectedRoom = computed(() => {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-reset-profiles {
|
||||
background: linear-gradient(135deg, #6b7280 0%, #374151 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
<template>
|
||||
<div class="game">
|
||||
<!-- End of game modal overlay -->
|
||||
<div v-if="endModal.visible" class="end-modal-overlay" @click.self="dismissEndModal">
|
||||
<div class="end-modal">
|
||||
<button class="close-btn" @click="dismissEndModal" aria-label="Cerrar">×</button>
|
||||
<div class="title">🏁 Juego finalizado</div>
|
||||
<div class="scores">
|
||||
<div v-for="s in finalScores" :key="s.sessionId" class="score-row">
|
||||
<span class="name">{{ s.name }}</span>
|
||||
<span class="tokens">🦃 {{ s.pavo }} · 🌽 {{ s.elote }}</span>
|
||||
<span class="points">Puntos: {{ s.points }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint">Se cerrará en {{ remainingSeconds }}s</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="game-container">
|
||||
<div class="game-header">
|
||||
<h1>🧪 Demo Room</h1>
|
||||
@@ -99,6 +114,58 @@ const outcomeP2 = ref(0);
|
||||
|
||||
const variants = ['G1','G2','G3','G4','G5'];
|
||||
|
||||
// End-of-game modal state and helpers
|
||||
const endModal = ref<{ visible: boolean }>({ visible: false });
|
||||
const remainingSeconds = ref(10);
|
||||
let endTimerTimeout: any = null;
|
||||
let endTimerInterval: any = null;
|
||||
|
||||
function showEndModal() {
|
||||
// Prevent multiple timers
|
||||
if (endModal.value.visible) return;
|
||||
endModal.value.visible = true;
|
||||
remainingSeconds.value = 10;
|
||||
if (endTimerInterval) clearInterval(endTimerInterval);
|
||||
if (endTimerTimeout) clearTimeout(endTimerTimeout);
|
||||
endTimerInterval = setInterval(() => {
|
||||
remainingSeconds.value = Math.max(0, remainingSeconds.value - 1);
|
||||
}, 1000);
|
||||
endTimerTimeout = setTimeout(() => {
|
||||
dismissEndModal();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
function dismissEndModal() {
|
||||
endModal.value.visible = false;
|
||||
if (endTimerInterval) { clearInterval(endTimerInterval); endTimerInterval = null; }
|
||||
if (endTimerTimeout) { clearTimeout(endTimerTimeout); endTimerTimeout = null; }
|
||||
}
|
||||
|
||||
const finalScores = computed(() => {
|
||||
return players.value.map(p => {
|
||||
const points = (p.role === 'P2')
|
||||
? (p.eloteTokens || 0) * 1 + (p.pavoTokens || 0) * 2
|
||||
: (p.pavoTokens || 0) * 1 + (p.eloteTokens || 0) * 2;
|
||||
return {
|
||||
sessionId: p.sessionId,
|
||||
name: p.name,
|
||||
pavo: p.pavoTokens || 0,
|
||||
elote: p.eloteTokens || 0,
|
||||
points
|
||||
};
|
||||
}).sort((a, b) => b.points - a.points);
|
||||
});
|
||||
|
||||
// Round transition banner state and helper
|
||||
const roundBanner = ref<{ visible: boolean; text: string; kind: 'start'|'end' }>({ visible: false, text: '', kind: 'start' });
|
||||
let roundBannerTimeout: any = null;
|
||||
|
||||
function showRoundBanner(text: string, kind: 'start'|'end', ms = 1400) {
|
||||
if (roundBannerTimeout) { clearTimeout(roundBannerTimeout); roundBannerTimeout = null; }
|
||||
roundBanner.value = { visible: true, text, kind };
|
||||
roundBannerTimeout = setTimeout(() => { roundBanner.value.visible = false; }, ms);
|
||||
}
|
||||
|
||||
const sessionId = computed(() => colyseusService.sessionId.value);
|
||||
const myRole = computed(() => {
|
||||
const me = players.value.find(p => p.sessionId === sessionId.value);
|
||||
@@ -141,7 +208,12 @@ onMounted(() => {
|
||||
gameStatus.value = state.gameStatus || 'waiting';
|
||||
});
|
||||
|
||||
$(room.state).listen("gameStatus", (value: string) => { gameStatus.value = value; });
|
||||
$(room.state).listen("gameStatus", (value: string) => {
|
||||
gameStatus.value = value;
|
||||
if ((value || '').toLowerCase() === 'finished') {
|
||||
showEndModal();
|
||||
}
|
||||
});
|
||||
$(room.state).listen("roomId", (value: string) => { roomId.value = value; });
|
||||
$(room.state).listen("currentVariant", (value: string) => { currentVariant.value = value as any; });
|
||||
$(room.state).listen("currentRound", (value: number) => { currentRound.value = value; });
|
||||
@@ -186,9 +258,7 @@ onMounted(() => {
|
||||
colyseusService.playerName.value = info.name;
|
||||
});
|
||||
|
||||
room.onMessage("gameEnd", () => {
|
||||
// no-op for local storage
|
||||
});
|
||||
room.onMessage("gameEnd", () => { showEndModal(); });
|
||||
|
||||
// Register additional message handlers to avoid warnings
|
||||
room.onMessage("gamePaused", () => {
|
||||
@@ -203,6 +273,8 @@ onMounted(() => {
|
||||
currentVariant.value = data.variant as any;
|
||||
});
|
||||
|
||||
// No round transition banners
|
||||
|
||||
// Handle room closure/disconnection
|
||||
room.onLeave((code: number) => {
|
||||
console.log('[DemoGame] Room disconnected with code:', code);
|
||||
@@ -266,6 +338,17 @@ function leaveGame() {
|
||||
|
||||
<style scoped>
|
||||
.game { min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display:flex; align-items:center; justify-content:center; padding:20px; }
|
||||
.end-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); display:flex; align-items:center; justify-content:center; z-index: 1200; }
|
||||
.end-modal { position: relative; background: white; color:#111; border-radius: 16px; padding: 24px 24px 18px; width: min(520px, 92vw); box-shadow: 0 30px 80px rgba(0,0,0,0.5); border:1px solid #e5e7eb; }
|
||||
.end-modal .close-btn { position:absolute; top:8px; right:8px; width:32px; height:32px; border-radius: 8px; border:1px solid #e5e7eb; background:#f8fafc; color:#111; font-weight:800; cursor:pointer; }
|
||||
.end-modal .close-btn:hover { background:#eef2ff; }
|
||||
.end-modal .title { font-size: 20px; font-weight: 900; margin-bottom: 8px; }
|
||||
.end-modal .scores { display:flex; flex-direction:column; gap:8px; margin: 8px 0 12px; }
|
||||
.end-modal .score-row { display:flex; align-items:center; justify-content:space-between; gap:8px; background:#f8fafc; border:1px solid #e5e7eb; border-radius: 10px; padding:8px 10px; }
|
||||
.end-modal .score-row .name { font-weight:800; color:#1f2937; }
|
||||
.end-modal .score-row .tokens { font-weight:700; color:#374151; }
|
||||
.end-modal .score-row .points { font-weight:900; color:#111827; }
|
||||
.end-modal .hint { font-size: 12px; color:#6b7280; text-align:right; }
|
||||
.game-container { background: white; border-radius: 20px; padding: 24px; max-width: 1000px; width: 100%; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }
|
||||
.game-header { display:flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||
.game-header h1 { margin: 0; font-size: 20px; }
|
||||
|
||||
Reference in New Issue
Block a user