diff --git a/client/src/components/DashboardActions.vue b/client/src/components/DashboardActions.vue new file mode 100644 index 0000000..4bf37b8 --- /dev/null +++ b/client/src/components/DashboardActions.vue @@ -0,0 +1,112 @@ + + + + 📥 Export / Tools + + + Descargar resultados (CSV) + + + + + + + + diff --git a/client/src/views/Dashboard.vue b/client/src/views/Dashboard.vue index e033606..69fb18e 100644 --- a/client/src/views/Dashboard.vue +++ b/client/src/views/Dashboard.vue @@ -123,6 +123,14 @@ > 🏠 Send All to Lobby + + 🧹 Reset All UUID Profiles + @@ -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; diff --git a/client/src/views/DemoGame.vue b/client/src/views/DemoGame.vue index bb96842..9e713f7 100644 --- a/client/src/views/DemoGame.vue +++ b/client/src/views/DemoGame.vue @@ -1,5 +1,20 @@ + + + + × + 🏁 Juego finalizado + + + {{ s.name }} + 🦃 {{ s.pavo }} · 🌽 {{ s.elote }} + Puntos: {{ s.points }} + + + Se cerrará en {{ remainingSeconds }}s + + 🧪 Demo Room @@ -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() {