Initial commit
This commit is contained in:
37
templates/admin_login.html
Normal file
37
templates/admin_login.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Voley — вход в админку{% endblock %}
|
||||
{% block header %}Админка{% endblock %}
|
||||
|
||||
{% block nav %}
|
||||
<a class="text-slate-300 hover:text-white" href="/">На главную</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="glass rounded-2xl p-5 max-w-md">
|
||||
<h2 class="text-lg font-semibold">Вход</h2>
|
||||
<p class="text-sm text-slate-400 mt-1">Введите пароль администратора.</p>
|
||||
|
||||
{% if request.query_params.get('error') %}
|
||||
<div class="mt-4 rounded-xl border border-rose-900 bg-rose-950/40 px-3 py-2 text-sm text-rose-200">
|
||||
Неверный пароль.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form class="mt-4 grid gap-3" method="post" action="/admin/login">
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
class="w-full rounded-xl bg-slate-950/60 border border-slate-800 px-3 py-2 text-slate-100 placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="Пароль"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full px-4 py-2 rounded-xl text-sm font-semibold bg-indigo-600 border border-indigo-500"
|
||||
>
|
||||
Войти
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
79
templates/admin_players.html
Normal file
79
templates/admin_players.html
Normal file
@@ -0,0 +1,79 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Voley — админка{% endblock %}
|
||||
{% block header %}Админка{% endblock %}
|
||||
|
||||
{% block nav %}
|
||||
<div class="flex items-center gap-4">
|
||||
<a class="text-slate-300 hover:text-white" href="/">На главную</a>
|
||||
<a class="text-slate-300 hover:text-white" href="/admin/logout">Выйти</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="glass rounded-2xl p-4 sm:p-5">
|
||||
<h2 class="text-lg font-semibold">Добавить игрока</h2>
|
||||
<form class="mt-3 flex flex-col sm:flex-row gap-3" method="post" action="/admin/players/add">
|
||||
<input
|
||||
name="name"
|
||||
class="flex-1 rounded-xl bg-slate-950/60 border border-slate-800 px-3 py-2 text-slate-100 placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="Имя игрока"
|
||||
required
|
||||
/>
|
||||
<button class="px-4 py-2 rounded-xl text-sm font-semibold bg-indigo-600 border border-indigo-500" type="submit">
|
||||
Добавить
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="mt-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">Игроки</h2>
|
||||
<div class="text-xs text-slate-400">{{ players|length }} всего</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid gap-3">
|
||||
{% for p in players %}
|
||||
<div class="glass rounded-2xl p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="text-base font-semibold truncate">
|
||||
{{ p.name }}
|
||||
{% if not p.active %}
|
||||
<span class="ml-2 text-xs font-semibold px-2 py-1 rounded-lg bg-slate-800 text-slate-200">неактивен</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-xs text-slate-400">ID: {{ p.id }}</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/admin/players/{{ p.id }}/toggle-active">
|
||||
<button
|
||||
class="px-3 py-2 rounded-xl text-sm font-semibold border
|
||||
{% if p.active %}
|
||||
bg-slate-950/40 border-slate-800 text-slate-200 hover:border-slate-600
|
||||
{% else %}
|
||||
bg-emerald-600 border-emerald-500 text-white
|
||||
{% endif %}"
|
||||
type="submit"
|
||||
>
|
||||
{% if p.active %}Деактивировать{% else %}Активировать{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form class="mt-3 flex flex-col sm:flex-row gap-3" method="post" action="/admin/players/{{ p.id }}/rename">
|
||||
<input
|
||||
name="name"
|
||||
value="{{ p.name }}"
|
||||
class="flex-1 rounded-xl bg-slate-950/60 border border-slate-800 px-3 py-2 text-slate-100 placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
required
|
||||
/>
|
||||
<button class="px-3 py-2 rounded-xl text-sm font-semibold bg-indigo-600 border border-indigo-500" type="submit">
|
||||
Переименовать
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
69
templates/base.html
Normal file
69
templates/base.html
Normal file
@@ -0,0 +1,69 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{% block title %}Voley{% endblock %}</title>
|
||||
|
||||
<!-- Tailwind (CDN) -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Flatpickr -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ru.js"></script>
|
||||
|
||||
<style>
|
||||
.container-max { max-width: 980px; }
|
||||
.fp-theme .flatpickr-calendar { border-radius: 14px; }
|
||||
|
||||
/* Glassmorphism */
|
||||
.glass {
|
||||
background: rgba(15, 23, 42, 0.38);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
box-shadow:
|
||||
0 10px 30px rgba(0,0,0,0.35),
|
||||
inset 0 1px 0 rgba(255,255,255,0.06);
|
||||
backdrop-filter: blur(14px) saturate(140%);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(140%);
|
||||
}
|
||||
.glass-hover:hover { border-color: rgba(148, 163, 184, 0.30); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen text-slate-100 relative overflow-x-hidden">
|
||||
<!-- Фон-картинка + затемнение/размытие (картинку положите в templates/bg.jpg) -->
|
||||
<div class="fixed inset-0 -z-10">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-slate-950 via-slate-950 to-slate-900"></div>
|
||||
|
||||
<!-- именно <img>, чтобы браузер точно загрузил файл -->
|
||||
<img
|
||||
src="/assets/bg.jpg?v={{ bg_version }}"
|
||||
alt=""
|
||||
class="absolute inset-0 w-full h-full object-cover opacity-55"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
/>
|
||||
|
||||
<!-- подложка и “дымка”, чтобы контент читался -->
|
||||
<div class="absolute inset-0 bg-slate-950/45 backdrop-blur-sm"></div>
|
||||
<div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(99,102,241,0.18),transparent_55%)]"></div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto container-max px-4 pb-16">
|
||||
<header class="pt-6 pb-4 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-widest text-slate-400">Волейбол</div>
|
||||
<h1 class="text-2xl font-semibold">{% block header %}Учет посещаемости{% endblock %}</h1>
|
||||
</div>
|
||||
<nav class="text-sm text-slate-300">
|
||||
{% block nav %}{% endblock %}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
BIN
templates/bg.jpg
Normal file
BIN
templates/bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
206
templates/index.html
Normal file
206
templates/index.html
Normal file
@@ -0,0 +1,206 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Voley — отметка посещения{% endblock %}
|
||||
{% block header %}Отметка посещения{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="glass rounded-2xl p-4 sm:p-5">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-sm text-slate-300">Выбранное воскресенье</div>
|
||||
<div class="text-xl font-semibold">{{ selected_date }}</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:w-[320px]">
|
||||
<label class="text-sm text-slate-300">Календарь (только воскресенья)</label>
|
||||
<input
|
||||
id="datePicker"
|
||||
class="mt-2 w-full rounded-xl bg-slate-950/60 border border-slate-800 px-3 py-2 text-slate-100 placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="Выберите воскресенье"
|
||||
value="{{ selected_date }}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">Игроки</h2>
|
||||
<div class="text-xs text-slate-400">{{ players|length }} активн.</div>
|
||||
</div>
|
||||
|
||||
<div id="playersList" class="mt-3 grid gap-3">
|
||||
{% for player in players %}
|
||||
{% set present = present_map.get(player.id, False) %}
|
||||
{% include "partials/player_row.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-8" id="teamsSection">
|
||||
<div id="teamsBox">
|
||||
{# По умолчанию рендерим команды детерминированно (по алфавиту): #}
|
||||
{% set present_names = [] %}
|
||||
{% for player in players %}
|
||||
{% if present_map.get(player.id, False) %}
|
||||
{% set _ = present_names.append(player.name) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set count = present_names|length %}
|
||||
{% set mid = (count + 1) // 2 %}
|
||||
{% set team_a = present_names[:mid] %}
|
||||
{% set team_b = present_names[mid:] %}
|
||||
{% include "partials/teams.html" %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-8 glass rounded-2xl p-4 sm:p-5">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">График</h2>
|
||||
<div class="text-xs text-slate-400">{{ chart_title }}</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a
|
||||
href="/?date={{ selected_date }}&view=month"
|
||||
class="px-3 py-2 rounded-xl border text-sm {{ 'bg-indigo-600 border-indigo-500' if chart_view=='month' else 'bg-slate-950/40 border-slate-800 text-slate-200' }}"
|
||||
>Месяц</a>
|
||||
<a
|
||||
href="/?date={{ selected_date }}&view=year"
|
||||
class="px-3 py-2 rounded-xl border text-sm {{ 'bg-indigo-600 border-indigo-500' if chart_view=='year' else 'bg-slate-950/40 border-slate-800 text-slate-200' }}"
|
||||
>Год</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 h-56 sm:h-64">
|
||||
{# SVG-график без JS/CDN — всегда отображается #}
|
||||
{% set max_v = (chart_values|max) if chart_values else 0 %}
|
||||
{% if max_v < 1 %}{% set max_v = 1 %}{% endif %}
|
||||
{% set n = chart_labels|length %}
|
||||
<svg viewBox="0 0 200 100" class="w-full h-full block" style="width:100%;height:100%;display:block" role="img" aria-label="{{ chart_title }}">
|
||||
<defs>
|
||||
<linearGradient id="barGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="rgba(129,140,248,0.95)"></stop>
|
||||
<stop offset="55%" stop-color="rgba(99,102,241,0.86)"></stop>
|
||||
<stop offset="100%" stop-color="rgba(59,130,246,0.62)"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="bgGlow" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="rgba(99,102,241,0.20)"></stop>
|
||||
<stop offset="60%" stop-color="rgba(15,23,42,0.00)"></stop>
|
||||
</linearGradient>
|
||||
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="1.2" stdDeviation="1.2" flood-color="rgba(0,0,0,0.45)"></feDropShadow>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- карточка/фон -->
|
||||
<rect x="0" y="0" width="200" height="100" rx="5" fill="rgba(2,6,23,0.22)"></rect>
|
||||
<rect x="0" y="0" width="200" height="100" rx="5" fill="url(#bgGlow)"></rect>
|
||||
<rect x="0.8" y="0.8" width="198.4" height="98.4" rx="5" fill="transparent" stroke="rgba(148,163,184,0.16)" stroke-width="0.7"></rect>
|
||||
|
||||
{# параметры области построения #}
|
||||
{% set padL = 16 %}
|
||||
{% set padR = 3 %}
|
||||
{% set padT = 8 %}
|
||||
{% set padB = 18 %}
|
||||
{% set plotW = 200 - padL - padR %}
|
||||
{% set plotH = 100 - padT - padB %}
|
||||
|
||||
<!-- оси -->
|
||||
<line x1="{{ padL }}" y1="{{ padT + plotH }}" x2="{{ 200 - padR }}" y2="{{ padT + plotH }}" stroke="rgba(148,163,184,0.18)" stroke-width="0.7"></line>
|
||||
<line x1="{{ padL }}" y1="{{ padT }}" x2="{{ padL }}" y2="{{ padT + plotH }}" stroke="rgba(148,163,184,0.14)" stroke-width="0.7"></line>
|
||||
|
||||
<!-- сетка + подписи Y -->
|
||||
{% for p in [0,25,50,75,100] %}
|
||||
{% set y = padT + plotH - (plotH * p / 100) %}
|
||||
<line x1="{{ padL }}" y1="{{ y }}" x2="{{ 200 - padR }}" y2="{{ y }}" stroke="rgba(148,163,184,0.12)" stroke-width="0.6"></line>
|
||||
<text x="5" y="{{ y + 1.7 }}" font-size="3.6" fill="rgba(226,232,240,0.70)">{{ (max_v * p / 100)|round|int }}</text>
|
||||
{% endfor %}
|
||||
|
||||
{% if n == 0 %}
|
||||
<text x="{{ padL }}" y="50" font-size="5" fill="rgba(226,232,240,0.85)">Нет данных</text>
|
||||
{% else %}
|
||||
{% set barGap = 1.4 %}
|
||||
{% set slot = plotW / n %}
|
||||
{% set barW = slot - barGap %}
|
||||
{% if barW < 0.9 %}{% set barW = 0.9 %}{% endif %}
|
||||
|
||||
{% for lab in chart_labels %}
|
||||
{% set i = loop.index0 %}
|
||||
{% set v = chart_values[i] %}
|
||||
{% set h = (v / max_v) * plotH %}
|
||||
{% set x = padL + (i * slot) + (barGap/2) %}
|
||||
{% set y = padT + plotH - h %}
|
||||
{% set cx = x + (barW/2) %}
|
||||
<rect
|
||||
x="{{ x }}"
|
||||
y="{{ y }}"
|
||||
width="{{ barW }}"
|
||||
height="{{ h }}"
|
||||
fill="url(#barGrad)"
|
||||
stroke="rgba(165,180,252,0.55)"
|
||||
stroke-width="0.25"
|
||||
rx="1.1"
|
||||
filter="url(#softShadow)"
|
||||
>
|
||||
<title>{{ lab }}: {{ v }}</title>
|
||||
</rect>
|
||||
|
||||
{# значение над столбцом (если место позволяет) #}
|
||||
{% if v > 0 and barW >= 2.2 %}
|
||||
<text
|
||||
x="{{ cx }}"
|
||||
y="{{ y - 1.4 }}"
|
||||
text-anchor="middle"
|
||||
font-size="3.6"
|
||||
fill="rgba(226,232,240,0.88)"
|
||||
>{{ v }}</text>
|
||||
{% endif %}
|
||||
|
||||
{% if (i % chart_label_step) == 0 %}
|
||||
{% if chart_view == 'month' and lab|length == 10 %}
|
||||
{% set show = lab[8:10] ~ '.' ~ lab[5:7] %}
|
||||
{% elif chart_view == 'year' and lab|length == 7 %}
|
||||
{% set show = lab[5:7] %}
|
||||
{% else %}
|
||||
{% set show = lab %}
|
||||
{% endif %}
|
||||
<text
|
||||
x="{{ cx }}"
|
||||
y="96.7"
|
||||
text-anchor="middle"
|
||||
font-size="3.6"
|
||||
fill="rgba(226,232,240,0.70)"
|
||||
>{{ show }}</text>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-8 glass rounded-2xl p-4 sm:p-5">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Выгрузка данных</h2>
|
||||
<div class="text-xs text-slate-400">История посещений игроков по датам</div>
|
||||
</div>
|
||||
<a
|
||||
href="/api/export/attendance.xlsx"
|
||||
class="px-4 py-2 rounded-xl bg-green-600 hover:bg-green-500 border border-green-500 text-sm font-medium transition-colors"
|
||||
download
|
||||
>Скачать Excel</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
window.__VOLEY__ = {
|
||||
selectedDate: {{ selected_date|tojson }},
|
||||
chartLabels: {{ chart_labels_json|safe }},
|
||||
chartValues: {{ chart_values_json|safe }},
|
||||
chartView: {{ chart_view|tojson }},
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
27
templates/partials/player_row.html
Normal file
27
templates/partials/player_row.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<div
|
||||
class="player-row glass glass-hover rounded-2xl p-4 flex items-center justify-between gap-3 transition"
|
||||
data-player-id="{{ player.id }}"
|
||||
data-player-name="{{ player.name|lower }}"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="text-base font-medium truncate">{{ player.name }}</div>
|
||||
<div class="text-xs text-slate-400">Нажмите, чтобы {% if present %}снять отметку{% else %}отметиться{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<form class="attendance-form flex items-center gap-3" method="post" action="/api/attendance/toggle">
|
||||
<input type="hidden" name="player_id" value="{{ player.id }}" />
|
||||
<input type="hidden" name="session_date" value="{{ session_date if session_date is defined else selected_date }}" />
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="min-w-[120px] sm:min-w-[140px] px-4 py-2 rounded-xl text-sm font-semibold border transition
|
||||
{% if present %}
|
||||
bg-emerald-600 border-emerald-500 text-white
|
||||
{% else %}
|
||||
bg-slate-950/50 border-slate-800 text-slate-200 hover:border-slate-600
|
||||
{% endif %}"
|
||||
>
|
||||
{% if present %}Пришёл{% else %}Не пришёл{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
47
templates/partials/teams.html
Normal file
47
templates/partials/teams.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<div class="glass rounded-2xl p-4 sm:p-5">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Команды</h2>
|
||||
<div class="text-xs text-slate-400">Отметились: {{ count }} · Дата: {{ session_date }}</div>
|
||||
</div>
|
||||
<form class="teams-form" method="post" action="/api/teams/random">
|
||||
<input type="hidden" name="session_date" value="{{ session_date if session_date is defined else selected_date }}" />
|
||||
<button
|
||||
type="submit"
|
||||
class="px-3 py-2 rounded-xl text-sm font-semibold bg-indigo-600 border border-indigo-500"
|
||||
>
|
||||
Перемешать
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if count < 2 %}
|
||||
<div class="mt-4 text-sm text-slate-300">
|
||||
Нужно минимум 2 отметившихся, чтобы разделить на команды.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mt-4 grid sm:grid-cols-2 gap-3">
|
||||
<div class="glass rounded-2xl p-4">
|
||||
<div class="text-sm font-semibold text-slate-200">Команда A</div>
|
||||
<ol class="mt-2 space-y-1 text-slate-100">
|
||||
{% for n in team_a %}
|
||||
<li class="flex items-center justify-between">
|
||||
<span class="truncate">{{ loop.index }}. {{ n }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
<div class="glass rounded-2xl p-4">
|
||||
<div class="text-sm font-semibold text-slate-200">Команда B</div>
|
||||
<ol class="mt-2 space-y-1 text-slate-100">
|
||||
{% for n in team_b %}
|
||||
<li class="flex items-center justify-between">
|
||||
<span class="truncate">{{ loop.index }}. {{ n }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user