36a40938c7
El contenedor corría en UTC causando que la ventana horaria no coincidiera con la hora local. Se agrega TZ en docker-compose y se muestra la hora actual del servidor en el panel de votación. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
172 lines
7.3 KiB
HTML
172 lines
7.3 KiB
HTML
{% 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>
|
||
|
||
<!-- 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>
|
||
|
||
<style>
|
||
.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: #333; 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 %}
|