Files
spotify-cantina/app/templates/admin/voting.html
T
David Inostroza 22ee2b58ad mueve selector de dispositivo al panel de admin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 18:51:41 -04:00

225 lines
9.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Votación — Admin{% endblock %}
{% block content %}
<div class="admin-container">
<div class="admin-header">
<h1>Control de Votación</h1>
<div style="display:flex;gap:.5rem">
<a href="/admin/playlists" class="btn-sm">Playlists</a>
<a href="/auth/logout" class="btn-sm btn-danger"
onclick="return confirm('¿Desconectar la cuenta de Spotify?')">⏏ Cuenta Spotify</a>
<a href="/admin/logout" class="btn-sm btn-danger">Cerrar sesión</a>
</div>
</div>
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
<!-- Configuración de rango horario -->
<div class="card">
<h2>Configurar votación</h2>
<form method="post" action="/admin/voting" class="voting-form">
<div class="time-row">
<div class="time-field">
<label>Hora de inicio</label>
<input type="time" name="start_time" class="input"
value="{{ config.start_time }}" required>
</div>
<div class="time-sep"></div>
<div class="time-field">
<label>Hora de fin</label>
<input type="time" name="end_time" class="input"
value="{{ config.end_time }}" required>
</div>
</div>
<label class="toggle-label">
<input type="checkbox" name="is_active"
{% if config.is_active %}checked{% endif %}>
<span class="toggle-text">Votación activa</span>
<span class="toggle-hint">
{% if config.is_active %}
Los usuarios pueden votar dentro del rango horario
{% else %}
La votación está desactivada
{% endif %}
</span>
</label>
<button type="submit" class="btn-primary">Guardar configuración</button>
</form>
<div class="status-row">
<div class="status-badge {% if config.is_active %}status-active{% else %}status-inactive{% endif %}">
{% if config.is_active %}
🟢 Activa · Ventana: {{ config.start_time }} {{ config.end_time }}
{% else %}
🔴 Inactiva
{% endif %}
</div>
<div class="server-time">🕐 Hora del servidor: <strong>{{ server_time }}</strong></div>
</div>
</div>
<!-- Selector de dispositivo -->
<div class="card">
<h2>Dispositivo de reproducción</h2>
<div class="device-row">
<select id="device-select" class="input" onchange="setDevice(this.value)" style="flex:1">
<option value="">Cargando dispositivos...</option>
</select>
<button class="btn-sm" onclick="loadDevices()">↻ Actualizar</button>
</div>
<div id="device-msg" style="font-size:.8rem;color:var(--text-muted);margin-top:.4rem"></div>
</div>
<!-- Resultados -->
<div class="card">
<div class="results-header">
<h2>Resultados ({{ total_votes }} votos)</h2>
<div style="display:flex;gap:.5rem;align-items:center">
{% if results and results[0].votes > 0 %}
<form method="post" action="/admin/voting/play-winner">
<button type="submit" class="btn-primary btn-sm">
▶ Reproducir ganador
</button>
</form>
{% endif %}
<form method="post" action="/admin/voting/reset"
onsubmit="return confirm('¿Reiniciar todos los votos?')">
<button type="submit" class="btn-sm btn-danger">Reiniciar votos</button>
</form>
</div>
</div>
{% if results %}
<div class="results-list">
{% for item in results %}
{% set pct = (item.votes / total_votes * 100) | round(1) if total_votes > 0 else 0 %}
<div class="result-row {% if loop.first and item.votes > 0 %}result-winner{% endif %}">
<div class="result-rank">{{ loop.index }}</div>
{% if item.playlist.image_url %}
<img src="{{ item.playlist.image_url }}" alt="" class="table-thumb">
{% else %}
<div class="table-thumb-placeholder">🎵</div>
{% endif %}
<div class="result-info">
<div class="result-name">
{{ item.playlist.name }}
{% if loop.first and item.votes > 0 %}
<span class="winner-badge">🏆 Ganador</span>
{% endif %}
</div>
<div class="result-bar-wrap">
<div class="result-bar" style="width: {{ pct }}%"></div>
</div>
</div>
<div class="result-count">
<strong>{{ item.votes }}</strong>
<span class="pct-label">{{ pct }}%</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty-msg">No hay playlists configuradas.</p>
{% endif %}
</div>
</div>
<script>
async function loadDevices() {
const sel = document.getElementById('device-select');
const msg = document.getElementById('device-msg');
sel.innerHTML = '<option value="">Cargando...</option>';
msg.textContent = '';
try {
const res = await fetch('/player/devices');
const devices = await res.json();
if (!devices.length) {
sel.innerHTML = '<option value="">Sin dispositivos activos</option>';
return;
}
sel.innerHTML = devices.map(d =>
`<option value="${d.id}"${d.is_active ? ' selected' : ''}>${d.name} (${d.type})</option>`
).join('');
} catch (_) {
sel.innerHTML = '<option value="">Error al cargar dispositivos</option>';
}
}
async function setDevice(deviceId) {
if (!deviceId) return;
const msg = document.getElementById('device-msg');
try {
await fetch('/player/device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_id: deviceId }),
});
msg.textContent = 'Dispositivo actualizado.';
setTimeout(() => msg.textContent = '', 2500);
} catch (_) {
msg.textContent = 'Error al cambiar dispositivo.';
}
}
loadDevices();
</script>
<style>
.device-row { display: flex; align-items: center; gap: .5rem; }
.voting-form { display: flex; flex-direction: column; gap: 1rem; }
.time-row { display: flex; align-items: flex-end; gap: 1rem; }
.time-field { display: flex; flex-direction: column; gap: .3rem; flex: 1; }
.time-field label { font-size: .8rem; color: var(--text-muted); }
.time-sep { padding-bottom: .6rem; color: var(--text-muted); }
.toggle-label { display: flex; align-items: center; gap: .6rem; cursor: pointer; }
.toggle-label input[type=checkbox] { width: 16px; height: 16px; accent-color: var(--green); }
.toggle-text { font-weight: 600; font-size: .9rem; }
.toggle-hint { font-size: .8rem; color: var(--text-muted); }
.status-badge {
display: inline-flex;
padding: .4rem .8rem;
border-radius: 20px;
font-size: .82rem;
font-weight: 500;
align-self: flex-start;
}
.status-active { background: rgba(29,185,84,.15); color: var(--green); border: 1px solid rgba(29,185,84,.3); }
.status-inactive { background: rgba(136,136,136,.1); color: var(--text-muted); border: 1px solid #333; }
.status-row { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.server-time { font-size: .8rem; color: var(--text-muted); }
.results-header { display: flex; justify-content: space-between; align-items: center; }
.results-list { display: flex; flex-direction: column; gap: .5rem; margin-top: .25rem; }
.result-row {
display: flex;
align-items: center;
gap: .75rem;
padding: .6rem;
border-radius: 8px;
background: var(--surface2);
}
.result-winner { border: 1px solid rgba(29,185,84,.4); background: rgba(29,185,84,.06); }
.result-rank { width: 20px; text-align: center; font-size: .82rem; color: var(--text-muted); font-weight: 600; }
.result-info { flex: 1; display: flex; flex-direction: column; gap: .3rem; }
.result-name { font-size: .9rem; font-weight: 500; display: flex; align-items: center; gap: .5rem; }
.winner-badge { background: rgba(255,215,0,.15); color: gold; font-size: .75rem; padding: 1px 6px; border-radius: 10px; border: 1px solid rgba(255,215,0,.3); }
.result-bar-wrap { height: 6px; background: var(--surface3); border-radius: 3px; overflow: hidden; }
.result-bar { height: 100%; background: var(--green); border-radius: 3px; transition: width .4s; }
.result-count { display: flex; flex-direction: column; align-items: flex-end; min-width: 50px; }
.result-count strong { font-size: 1.1rem; }
.pct-label { font-size: .75rem; color: var(--text-muted); }
</style>
{% endblock %}