1178 lines
47 KiB
HTML
1178 lines
47 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta
|
||
http-equiv="Cache-Control"
|
||
content="no-store, no-cache, must-revalidate, max-age=0"
|
||
/>
|
||
<meta http-equiv="Pragma" content="no-cache" />
|
||
<meta http-equiv="Expires" content="0" />
|
||
<title>Рабочее окно матча</title>
|
||
<link rel="stylesheet" href="/static/styles.css">
|
||
</head>
|
||
|
||
<body>
|
||
<div class="page">
|
||
<div class="topbar">
|
||
<div class="topbar-inner">
|
||
<div class="match-header">
|
||
<div class="team-box">
|
||
{% if session[15] %}
|
||
<img
|
||
src="{{ session[15] }}"
|
||
alt="{{ session[14] }}"
|
||
class="team-logo"
|
||
/>
|
||
{% endif %}
|
||
<div>
|
||
<div class="team-name">{{ session[14] }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="match-meta">
|
||
<div class="match-tour">{{ session[10] or "Тур не указан" }}</div>
|
||
<div class="match-time">
|
||
{{ session[9] or "Время не указано" }}
|
||
</div>
|
||
<div class="match-place">
|
||
{{ session[12] or "Стадион не указан" }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="team-box right">
|
||
<div>
|
||
<div class="team-name">{{ session[17] }}</div>
|
||
</div>
|
||
{% if session[18] %}
|
||
<img
|
||
src="{{ session[18] }}"
|
||
alt="{{ session[17] }}"
|
||
class="team-logo"
|
||
/>
|
||
{% endif %}
|
||
</div>
|
||
<div class="header-actions compact">
|
||
<form
|
||
method="post"
|
||
action="/admin/session/{{ session[3] }}/load-match-data"
|
||
>
|
||
<button
|
||
type="submit"
|
||
class="icon-btn header-icon"
|
||
title="Загрузить данные"
|
||
>
|
||
🌐
|
||
</button>
|
||
</form>
|
||
|
||
<form
|
||
method="post"
|
||
action="/admin/session/{{ session[3] }}/close"
|
||
>
|
||
<button
|
||
type="submit"
|
||
class="icon-btn header-icon"
|
||
title="Закрыть сессию"
|
||
>
|
||
⏹
|
||
</button>
|
||
</form>
|
||
|
||
<button
|
||
type="button"
|
||
id="editModeToggle"
|
||
class="icon-btn header-icon"
|
||
title="Режим редактирования"
|
||
onclick="toggleEditMode()"
|
||
>
|
||
✏️
|
||
</button>
|
||
{% if current_user and current_user.get("role") == "admin" %}
|
||
<a
|
||
href="/admin/db"
|
||
class="icon-btn header-icon"
|
||
title="База данных"
|
||
style="text-decoration: none; display: inline-flex; align-items: center; justify-content: center;"
|
||
>
|
||
🗄️
|
||
</a>
|
||
{% endif %}
|
||
|
||
<form method="post" action="/logout">
|
||
<button
|
||
type="submit"
|
||
class="icon-btn header-icon danger"
|
||
title="Выйти"
|
||
>
|
||
❌
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<div
|
||
id="sessionStatusBanner"
|
||
class="empty-box"
|
||
style="
|
||
display: none;
|
||
margin-bottom: 16px;
|
||
border: 1px solid rgba(255, 193, 7, 0.35);
|
||
background: rgba(255, 193, 7, 0.08);
|
||
"
|
||
>
|
||
Сессия истекла. Выполняется переход на страницу входа…
|
||
</div>
|
||
<div class="tabs">
|
||
<a
|
||
class="tab-link {% if tab == 'game' %}active{% endif %}"
|
||
href="/admin/session/{{ session[3] }}?tab=game"
|
||
>Игра</a
|
||
>
|
||
<a
|
||
class="tab-link {% if tab == 'formations' %}active{% endif %}"
|
||
href="/admin/session/{{ session[3] }}?tab=formations"
|
||
>Расстановки команд</a
|
||
>
|
||
<a
|
||
class="tab-link {% if tab == 'referees' %}active{% endif %}"
|
||
href="/admin/session/{{ session[3] }}?tab=referees"
|
||
>Судьи</a
|
||
>
|
||
<a
|
||
class="tab-link {% if tab == 'tour_schedule' %}active{% endif %}"
|
||
href="/admin/session/{{ session[3] }}?tab=tour_schedule"
|
||
>Расписание тура</a
|
||
>
|
||
<a
|
||
class="tab-link {% if tab == 'standings' %}active{% endif %}"
|
||
href="/admin/session/{{ session[3] }}?tab=standings"
|
||
>Турнирная таблица</a
|
||
>
|
||
</div>
|
||
|
||
{% if tab == 'game' %}
|
||
<div class="game-layout">
|
||
<div class="game-main">
|
||
<div class="panel">
|
||
<div class="match-toolbar">
|
||
<div class="match-toolbar-left">
|
||
<button
|
||
type="button"
|
||
class="btn btn-primary"
|
||
data-period-btn="1H"
|
||
onclick="setMatchPeriod('1H')"
|
||
>
|
||
1-й тайм
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
data-period-btn="HT"
|
||
onclick="setMatchPeriod('HT')"
|
||
>
|
||
Перерыв
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-primary"
|
||
data-period-btn="2H"
|
||
onclick="setMatchPeriod('2H')"
|
||
>
|
||
2-й тайм
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
data-period-btn="FT"
|
||
onclick="setMatchPeriod('FT')"
|
||
>
|
||
Завершить
|
||
</button>
|
||
</div>
|
||
|
||
<div class="match-toolbar-right">
|
||
<span class="period-badge" id="periodBadge"
|
||
>Матч не начат</span
|
||
>
|
||
<div class="timer-box" id="matchTimer">00:00</div>
|
||
<div class="score-box" id="matchScore">0 : 0</div>
|
||
|
||
<div class="extra-time-wrap">
|
||
<div class="extra-time-control">
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
step="1"
|
||
class="extra-time-input"
|
||
id="extraTimeMinutes"
|
||
placeholder="+мин"
|
||
/>
|
||
<div class="extra-time-arrows">
|
||
<button type="button" onclick="stepExtraTime(1)">
|
||
▲
|
||
</button>
|
||
<button type="button" onclick="stepExtraTime(-1)">
|
||
▼
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
onclick="applyExtraTime()"
|
||
>
|
||
Доп. время
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="lineups-grid">
|
||
<div class="panel">
|
||
{% if coaches.home %}
|
||
<div class="coaches-block" id="homeCoachesBlock">
|
||
{% for c in coaches.home %}
|
||
<div class="coach-line">
|
||
<div class="coach-left">
|
||
<span class="coach-name">{{ c.coach_name }}</span>
|
||
{% if c.role %}
|
||
<span class="coach-badge">{{ c.role }}</span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="coach-actions">
|
||
<button
|
||
type="button"
|
||
class="icon-btn yellow-card"
|
||
title="Жёлтая карточка"
|
||
data-coach-side="home"
|
||
data-coach-name="{{ c.coach_name }}"
|
||
data-card-type="yellow"
|
||
onclick="addCoachEvent('home', 'yellow', this.dataset.coachName, this)"
|
||
></button>
|
||
|
||
<button
|
||
type="button"
|
||
class="icon-btn red-card"
|
||
title="Красная карточка"
|
||
data-coach-side="home"
|
||
data-coach-name="{{ c.coach_name }}"
|
||
data-card-type="red"
|
||
onclick="addCoachEvent('home', 'red', this.dataset.coachName, this)"
|
||
></button>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="section-title">Основной состав</div>
|
||
{% if lineups.home_starting %}
|
||
<table class="game-lineup-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="num-col">#</th>
|
||
<th class="player-col">Игрок</th>
|
||
<th class="actions-col">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for p in lineups.home_starting %}
|
||
<tr
|
||
class="lineup-row"
|
||
data-side="home"
|
||
data-role="starting"
|
||
data-player-id="{{ p.player_id | default(p.number) }}"
|
||
data-player-name="{{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim }}"
|
||
data-player-last="{{ p.last_name or p.player_name or '' }}"
|
||
data-player-first="{{ p.first_name or '' }}"
|
||
data-player-number="{{ p.number or '' }}"
|
||
data-player-position="{{ p.position or '' }}"
|
||
data-player-captain="{{ 1 if p.is_captain else 0 }}"
|
||
>
|
||
<td class="num-cell">{{ p.number }}</td>
|
||
<td class="player-cell">
|
||
<div class="player-inline">
|
||
<div class="player-inline-text">
|
||
<span
|
||
class="last-name"
|
||
>{{ p.last_name or p.player_name }}</span
|
||
>
|
||
{% if p.first_name %}<span class="first-name">
|
||
{{ p.first_name }}</span
|
||
>{% endif %}
|
||
</div>
|
||
<span class="badges">
|
||
{% if p.position and p.position|lower in ['вр.',
|
||
'вр', 'вратарь', 'gk', 'goalkeeper'] %}
|
||
<span class="gk-badge">ВР</span>
|
||
{% endif %} {% if p.is_captain %}
|
||
<span class="captain-badge">К</span>
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td class="actions-col">
|
||
<div class="row-actions">
|
||
<button
|
||
type="button"
|
||
class="icon-btn goal"
|
||
title="Гол"
|
||
onclick="addPlayerEvent('home', 'goal', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }})"
|
||
>
|
||
⚽
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="icon-btn"
|
||
title="Автогол"
|
||
onclick="addPlayerEvent('home', 'own_goal', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }})"
|
||
>
|
||
АГ
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="icon-btn"
|
||
title="Пенальти"
|
||
onclick="addPlayerEvent('home', 'penalty', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }})"
|
||
>
|
||
П
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="icon-btn sub"
|
||
title="Замена"
|
||
onclick="beginSubstitution('home', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }}, this)"
|
||
>
|
||
⇄
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="icon-btn yellow-card"
|
||
title="Жёлтая карточка"
|
||
onclick="addPlayerEvent('home', 'yellow', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }})"
|
||
></button>
|
||
<button
|
||
type="button"
|
||
class="icon-btn red-card"
|
||
title="Красная карточка"
|
||
onclick="addPlayerEvent('home', 'red', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }})"
|
||
></button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div class="empty-box">Основной состав пока не загружен.</div>
|
||
{% endif %}
|
||
|
||
<div class="section-title">Запасные</div>
|
||
<div class="bench-pick-hint" id="homeBenchHint">
|
||
Выбери игрока из запасных для завершения замены
|
||
</div>
|
||
{% if lineups.home_bench %}
|
||
<table class="game-lineup-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="num-col">#</th>
|
||
<th class="player-col">Игрок</th>
|
||
<th class="actions-col">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for p in lineups.home_bench %}
|
||
<tr
|
||
class="lineup-row"
|
||
data-side="home"
|
||
data-role="bench"
|
||
data-player-id="{{ p.player_id | default(p.number) }}"
|
||
data-player-name="{{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim }}"
|
||
data-player-last="{{ p.last_name or p.player_name or '' }}"
|
||
data-player-first="{{ p.first_name or '' }}"
|
||
data-player-number="{{ p.number or '' }}"
|
||
data-player-position="{{ p.position or '' }}"
|
||
data-player-captain="{{ 1 if p.is_captain else 0 }}"
|
||
>
|
||
<td class="num-cell">{{ p.number }}</td>
|
||
<td class="player-cell">
|
||
<div class="player-inline">
|
||
<div class="player-inline-text">
|
||
<span
|
||
class="last-name"
|
||
>{{ p.last_name or p.player_name }}</span
|
||
>
|
||
{% if p.first_name %}<span class="first-name">
|
||
{{ p.first_name }}</span
|
||
>{% endif %}
|
||
</div>
|
||
<span class="badges">
|
||
{% if p.position and p.position|lower in ['вр.',
|
||
'вр', 'вратарь', 'gk', 'goalkeeper'] %}
|
||
<span class="gk-badge">ВР</span>
|
||
{% endif %} {% if p.is_captain %}
|
||
<span class="captain-badge">К</span>
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td class="actions-col">
|
||
<div class="row-actions">
|
||
<button
|
||
type="button"
|
||
class="icon-btn sub"
|
||
title="Замена"
|
||
onclick="selectBenchPlayer('home', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }}, this)"
|
||
>
|
||
⇄
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div class="empty-box">Запасные пока не загружены.</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="panel">
|
||
{% if coaches.away %}
|
||
<div
|
||
class="coaches-block coaches-block-right"
|
||
id="awayCoachesBlock"
|
||
>
|
||
{% for c in coaches.away %}
|
||
<div class="coach-line">
|
||
<div class="coach-left">
|
||
<span class="coach-name">{{ c.coach_name }}</span>
|
||
{% if c.role %}
|
||
<span class="coach-badge">{{ c.role }}</span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="coach-actions">
|
||
<button
|
||
type="button"
|
||
class="icon-btn yellow-card"
|
||
title="Жёлтая карточка"
|
||
data-coach-side="away"
|
||
data-coach-name="{{ c.coach_name }}"
|
||
data-card-type="yellow"
|
||
onclick="addCoachEvent('away', 'yellow', this.dataset.coachName, this)"
|
||
></button>
|
||
|
||
<button
|
||
type="button"
|
||
class="icon-btn red-card"
|
||
title="Красная карточка"
|
||
data-coach-side="away"
|
||
data-coach-name="{{ c.coach_name }}"
|
||
data-card-type="red"
|
||
onclick="addCoachEvent('away', 'red', this.dataset.coachName, this)"
|
||
></button>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="section-title">Основной состав</div>
|
||
{% if lineups.away_starting %}
|
||
<table class="game-lineup-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="num-col">#</th>
|
||
<th class="player-col">Игрок</th>
|
||
<th class="actions-col">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for p in lineups.away_starting %}
|
||
<tr
|
||
class="lineup-row"
|
||
data-side="away"
|
||
data-role="starting"
|
||
data-player-id="{{ p.player_id | default(p.number) }}"
|
||
data-player-name="{{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim }}"
|
||
data-player-last="{{ p.last_name or p.player_name or '' }}"
|
||
data-player-first="{{ p.first_name or '' }}"
|
||
data-player-number="{{ p.number or '' }}"
|
||
data-player-position="{{ p.position or '' }}"
|
||
data-player-captain="{{ 1 if p.is_captain else 0 }}"
|
||
>
|
||
<td class="num-cell">{{ p.number }}</td>
|
||
<td class="player-cell">
|
||
<div class="player-inline">
|
||
<div class="player-inline-text">
|
||
<span
|
||
class="last-name"
|
||
>{{ p.last_name or p.player_name }}</span
|
||
>
|
||
{% if p.first_name %}<span class="first-name">
|
||
{{ p.first_name }}</span
|
||
>{% endif %}
|
||
</div>
|
||
<span class="badges">
|
||
{% if p.position and p.position|lower in ['вр.',
|
||
'вр', 'вратарь', 'gk', 'goalkeeper'] %}
|
||
<span class="gk-badge">ВР</span>
|
||
{% endif %} {% if p.is_captain %}
|
||
<span class="captain-badge">К</span>
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td class="actions-col">
|
||
<div class="row-actions">
|
||
<button
|
||
type="button"
|
||
class="icon-btn goal"
|
||
title="Гол"
|
||
onclick="addPlayerEvent('away', 'goal', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }})"
|
||
>
|
||
⚽
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="icon-btn"
|
||
title="Автогол"
|
||
onclick="addPlayerEvent('away', 'own_goal', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }})"
|
||
>
|
||
АГ
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="icon-btn"
|
||
title="Пенальти"
|
||
onclick="addPlayerEvent('away', 'penalty', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }})"
|
||
>
|
||
П
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="icon-btn sub"
|
||
title="Замена"
|
||
onclick="beginSubstitution('away', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }}, this)"
|
||
>
|
||
⇄
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="icon-btn yellow-card"
|
||
title="Жёлтая карточка"
|
||
onclick="addPlayerEvent('away', 'yellow', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }})"
|
||
></button>
|
||
<button
|
||
type="button"
|
||
class="icon-btn red-card"
|
||
title="Красная карточка"
|
||
onclick="addPlayerEvent('away', 'red', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }})"
|
||
></button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div class="empty-box">Основной состав пока не загружен.</div>
|
||
{% endif %}
|
||
|
||
<div class="section-title">Запасные</div>
|
||
<div class="bench-pick-hint" id="awayBenchHint">
|
||
Выбери игрока из запасных для завершения замены
|
||
</div>
|
||
{% if lineups.away_bench %}
|
||
<table class="game-lineup-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="num-col">#</th>
|
||
<th class="player-col">Игрок</th>
|
||
<th class="actions-col">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for p in lineups.away_bench %}
|
||
<tr
|
||
class="lineup-row"
|
||
data-side="away"
|
||
data-role="bench"
|
||
data-player-id="{{ p.player_id | default(p.number) }}"
|
||
data-player-name="{{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim }}"
|
||
data-player-last="{{ p.last_name or p.player_name or '' }}"
|
||
data-player-first="{{ p.first_name or '' }}"
|
||
data-player-number="{{ p.number or '' }}"
|
||
data-player-position="{{ p.position or '' }}"
|
||
data-player-captain="{{ 1 if p.is_captain else 0 }}"
|
||
>
|
||
<td class="num-cell">{{ p.number }}</td>
|
||
<td class="player-cell">
|
||
<div class="player-inline">
|
||
<div class="player-inline-text">
|
||
<span
|
||
class="last-name"
|
||
>{{ p.last_name or p.player_name }}</span
|
||
>
|
||
{% if p.first_name %}<span class="first-name">
|
||
{{ p.first_name }}</span
|
||
>{% endif %}
|
||
</div>
|
||
<span class="badges">
|
||
{% if p.position and p.position|lower in ['вр.',
|
||
'вр', 'вратарь', 'gk', 'goalkeeper'] %}
|
||
<span class="gk-badge">ВР</span>
|
||
{% endif %} {% if p.is_captain %}
|
||
<span class="captain-badge">К</span>
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td class="actions-col">
|
||
<div class="row-actions">
|
||
<button
|
||
type="button"
|
||
class="icon-btn sub"
|
||
title="Замена"
|
||
onclick="selectBenchPlayer('away', {{ p.player_id | default(p.number) | tojson }}, {{ ((p.last_name or p.player_name or '') ~ ' ' ~ (p.first_name or '')) | trim | tojson }}, this)"
|
||
>
|
||
⇄
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div class="empty-box">Запасные пока не загружены.</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel events-panel">
|
||
<div class="panel-title-row">
|
||
<div>
|
||
<div class="panel-title">События матча</div>
|
||
<div class="small-meta">
|
||
Слева хозяева, справа гости. Время идёт сверху вниз по общей
|
||
шкале.
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
onclick="clearMatchEvents()"
|
||
>
|
||
Очистить
|
||
</button>
|
||
</div>
|
||
|
||
<div class="events-list" id="eventsList">
|
||
<div class="empty-box">Событий пока нет.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% elif tab == 'formations' %}
|
||
<div class="lineups-grid">
|
||
<div class="panel">
|
||
<div class="team-panel-title">Расстановка — {{ session[14] }}</div>
|
||
|
||
<div class="formation-toolbar">
|
||
<select id="home-preset" class="formation-select" data-edit-lock onchange="saveSelectedPreset('home')">
|
||
<option value="4-4-2">4-4-2</option>
|
||
<option value="4-3-3">4-3-3</option>
|
||
<option value="4-2-3-1">4-2-3-1</option>
|
||
<option value="3-5-2">3-5-2</option>
|
||
<option value="5-3-2">5-3-2</option>
|
||
<option value="4-1-4-1">4-1-4-1</option>
|
||
<option value="3-4-3">3-4-3</option>
|
||
<option value="4-5-1">4-5-1</option>
|
||
<option value="5-4-1">5-4-1</option>
|
||
<option value="4-3-1-2">4-3-1-2</option>
|
||
<option value="3-6-1">3-6-1</option>
|
||
</select>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
data-edit-lock
|
||
onclick="applyPreset('home')"
|
||
>
|
||
Применить расстановку
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-primary"
|
||
data-edit-lock
|
||
onclick="saveFormation('home')"
|
||
>
|
||
Сохранить
|
||
</button>
|
||
</div>
|
||
|
||
<div class="pitch-half" id="pitch-home">
|
||
<div class="pitch-center-line"></div>
|
||
<div class="pitch-center-circle"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="team-panel-title">Расстановка — {{ session[17] }}</div>
|
||
|
||
<div class="formation-toolbar">
|
||
<select id="away-preset" class="formation-select" data-edit-lock onchange="saveSelectedPreset('away')">
|
||
<option value="4-4-2">4-4-2</option>
|
||
<option value="4-3-3">4-3-3</option>
|
||
<option value="4-2-3-1">4-2-3-1</option>
|
||
<option value="3-5-2">3-5-2</option>
|
||
<option value="5-3-2">5-3-2</option>
|
||
<option value="4-1-4-1">4-1-4-1</option>
|
||
<option value="3-4-3">3-4-3</option>
|
||
<option value="4-5-1">4-5-1</option>
|
||
<option value="5-4-1">5-4-1</option>
|
||
<option value="4-3-1-2">4-3-1-2</option>
|
||
<option value="3-6-1">3-6-1</option>
|
||
</select>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
data-edit-lock
|
||
onclick="applyPreset('away')"
|
||
>
|
||
Применить расстановку
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-primary"
|
||
data-edit-lock
|
||
onclick="saveFormation('away')"
|
||
>
|
||
Сохранить
|
||
</button>
|
||
</div>
|
||
|
||
<div class="pitch-half" id="pitch-away">
|
||
<div class="pitch-center-line"></div>
|
||
<div class="pitch-center-circle"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
{% elif tab == 'referees' %}
|
||
<div class="panel">
|
||
<div class="team-panel-title">Судейская бригада</div>
|
||
|
||
<!-- сюда JS рендерит таблицу -->
|
||
<div id="refereesEditorWrap"></div>
|
||
|
||
<!-- кнопки редактирования -->
|
||
<div class="referee-edit-controls" data-edit-lock-wrap>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
onclick="addRefereeFromSelect()"
|
||
>
|
||
Добавить судью
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
class="btn btn-primary"
|
||
onclick="saveReferees()"
|
||
>
|
||
Сохранить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{% elif tab == 'tour_schedule' %}
|
||
<div class="panel">
|
||
<div class="team-panel-title">Расписание тура</div>
|
||
|
||
{% set month_names = { 1: 'января', 2: 'февраля', 3: 'марта', 4:
|
||
'апреля', 5: 'мая', 6: 'июня', 7: 'июля', 8: 'августа', 9: 'сентября',
|
||
10: 'октября', 11: 'ноября', 12: 'декабря' } %} {% if tour_schedule %}
|
||
<table class="table-compact tour-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="tour-date-col">Дата</th>
|
||
<th class="tour-match-col">Матч</th>
|
||
<th class="tour-meta-col">Счёт / статус</th>
|
||
<th class="tour-stadium-col">Стадион</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for m in tour_schedule %}
|
||
<tr>
|
||
<td class="tour-date-cell">
|
||
{% if m.match_date %}
|
||
{{ m.match_date.day }} {{ month_names[m.match_date.month] }}
|
||
{{ m.match_date.year }}
|
||
{% if m.match_date.hour != 0 or m.match_date.minute != 0 %}
|
||
{{ "%02d:%02d"|format(m.match_date.hour, m.match_date.minute) }}
|
||
{% endif %} {% else %} — {% endif %}
|
||
</td>
|
||
|
||
<td class="tour-match-cell">
|
||
{{ m.home_team_name }} — {{ m.away_team_name }}
|
||
{% if m.match_external_id == session[8] %}
|
||
<span class="gk-badge">ТЕКУЩИЙ</span>
|
||
{% endif %}
|
||
</td>
|
||
|
||
<td class="tour-meta-cell">
|
||
<div class="tour-meta-wrap">
|
||
{% if m.status == 'finished' and m.home_score is not none
|
||
and m.away_score is not none %}
|
||
<span class="score-badge"
|
||
>{{ m.home_score }} : {{ m.away_score }}</span
|
||
>
|
||
{% endif %} {% if m.status == 'finished' %}
|
||
<span class="status-badge status-finished">Завершён</span>
|
||
{% elif m.status == 'live' %}
|
||
<span class="status-badge status-live">Идёт</span>
|
||
{% elif m.status == 'scheduled' %}
|
||
<span class="status-badge status-scheduled"
|
||
>Не начался</span
|
||
>
|
||
{% else %}
|
||
<span
|
||
class="status-badge status-scheduled"
|
||
>{{ m.status or "—" }}</span
|
||
>
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
|
||
<td class="stadium-cell">
|
||
{{ m.stadium_name or "—" }}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div class="empty-box">Расписание тура не найдено.</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% elif tab == 'standings' %}
|
||
<div class="panel">
|
||
<div class="team-panel-title">Турнирная таблица</div>
|
||
|
||
{% if standings %}
|
||
<table class="standings-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="st-pos-col">#</th>
|
||
<th class="st-logo-col"></th>
|
||
<th class="st-team-col">Команда</th>
|
||
<th class="st-stat-col">И</th>
|
||
<th class="st-stat-col">В</th>
|
||
<th class="st-stat-col">Н</th>
|
||
<th class="st-stat-col">П</th>
|
||
<th class="st-gfga-col">М</th>
|
||
<th class="st-points-col">О</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for s in standings %}
|
||
<tr
|
||
class="{% if s.team_name == session[14] %}standings-row-home{% elif s.team_name == session[17] %}standings-row-away{% endif %}"
|
||
>
|
||
<td class="num-cell">{{ s.position }}</td>
|
||
<td class="st-logo-col">
|
||
{% if s.team_logo %}
|
||
<img
|
||
src="{{ s.team_logo }}"
|
||
alt="{{ s.team_name }}"
|
||
class="standings-logo"
|
||
/>
|
||
{% endif %}
|
||
</td>
|
||
<td class="player-name">{{ s.team_name }}</td>
|
||
<td class="num-cell">{{ s.played }}</td>
|
||
<td class="num-cell">{{ s.wins }}</td>
|
||
<td class="num-cell">{{ s.draws }}</td>
|
||
<td class="num-cell">{{ s.losses }}</td>
|
||
<td class="num-cell">
|
||
{{ s.goals_for }}-{{ s.goals_against }}
|
||
</td>
|
||
<td class="num-cell">{{ s.points }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div class="empty-box">Турнирная таблица пока не загружена.</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% else %}
|
||
<div class="panel">
|
||
<div class="empty-box">Эта вкладка пока в разработке.</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
window.MATCH_DATA = {
|
||
teamNames: {
|
||
home: {{ session[14] | tojson }},
|
||
away: {{ session[17] | tojson }}
|
||
},
|
||
sessionToken: {{ session[3] | tojson }},
|
||
matchId: {{ session[1] | tojson }},
|
||
|
||
homeFormations: {{ home_formations | tojson }},
|
||
awayFormations: {{ away_formations | tojson }},
|
||
|
||
homeStarting: {{ lineups.home_starting | tojson }},
|
||
awayStarting: {{ lineups.away_starting | tojson }},
|
||
homeBench: {{ lineups.home_bench | tojson }},
|
||
awayBench: {{ lineups.away_bench | tojson }},
|
||
|
||
homeCoaches: {{ coaches.home | tojson }},
|
||
awayCoaches: {{ coaches.away | tojson }},
|
||
|
||
referees: {{ referees | tojson }},
|
||
refereePool: {{ referee_pool | tojson }},
|
||
|
||
homeSquadPool: [],
|
||
awaySquadPool: [],
|
||
homeCoachPool: [],
|
||
awayCoachPool: []
|
||
};
|
||
</script>
|
||
<div class="formation-modal" id="formationAssignModal">
|
||
<div
|
||
class="formation-modal-backdrop"
|
||
onclick="closeFormationAssignModal()"
|
||
></div>
|
||
|
||
<div class="formation-modal-content">
|
||
<div class="formation-modal-header">
|
||
<div>
|
||
<div class="formation-modal-title">Заполнение расстановки</div>
|
||
<div
|
||
class="formation-modal-subtitle"
|
||
id="formationAssignSubtitle"
|
||
></div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="modal-close-btn"
|
||
onclick="closeFormationAssignModal()"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<div class="formation-modal-body">
|
||
<div id="formationAssignTableWrap"></div>
|
||
</div>
|
||
|
||
<div class="formation-modal-footer">
|
||
<button
|
||
type="button"
|
||
class="btn btn-cancel"
|
||
onclick="closeFormationAssignModal()"
|
||
>
|
||
Отмена
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-primary"
|
||
onclick="confirmFormationAssign()"
|
||
>
|
||
Применить игроков
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<div class="formation-modal" id="goalEditorModal">
|
||
<div class="formation-modal-backdrop" onclick="closeGoalEditor()"></div>
|
||
|
||
<div class="formation-modal-content" style="width: min(560px, 96vw);">
|
||
<div class="formation-modal-header">
|
||
<div>
|
||
<div class="formation-modal-title">Редактирование гола</div>
|
||
<div class="formation-modal-subtitle" id="goalEditorTitle"></div>
|
||
</div>
|
||
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
onclick="closeGoalEditor()"
|
||
>
|
||
Закрыть
|
||
</button>
|
||
</div>
|
||
|
||
<div class="formation-modal-body">
|
||
<div id="goalEditorList" class="goal-editor-list"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="formation-modal" id="squadEditorModal">
|
||
<div class="formation-modal-backdrop" onclick="closeSquadEditor()"></div>
|
||
|
||
<div class="formation-modal-content squad-editor-modal-content">
|
||
<div class="formation-modal-header">
|
||
<div>
|
||
<div class="formation-modal-title">Редактирование состава</div>
|
||
<div class="formation-modal-subtitle">
|
||
Добавление, удаление игроков, капитаны и тренеры
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="modal-close-btn"
|
||
onclick="closeSquadEditor()"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<div class="formation-modal-body">
|
||
<div class="squad-editor-grid">
|
||
<div class="squad-editor-team">
|
||
<div class="squad-editor-team-title">{{ session[14] }}</div>
|
||
<div id="squadEditorHome"></div>
|
||
</div>
|
||
|
||
<div class="squad-editor-team">
|
||
<div class="squad-editor-team-title">{{ session[17] }}</div>
|
||
<div id="squadEditorAway"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="formation-modal-footer">
|
||
<button
|
||
type="button"
|
||
class="btn btn-cancel"
|
||
onclick="closeSquadEditor()"
|
||
>
|
||
Отмена
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-confirm"
|
||
onclick="applySquadEditorChanges()"
|
||
>
|
||
Сохранить изменения
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
const LOGIN_URL = "/login";
|
||
const LOGOUT_URL = "/logout";
|
||
const IDLE_LIMIT_MS = 2 * 60 * 60 * 1000;
|
||
const WARNING_BEFORE_MS = 60 * 1000;
|
||
const ACTIVITY_EVENTS = ["mousemove", "mousedown", "keydown", "scroll", "touchstart", "click"];
|
||
|
||
let lastActivityAt = Date.now();
|
||
let warned = false;
|
||
let redirected = false;
|
||
|
||
const banner = document.getElementById("sessionStatusBanner");
|
||
|
||
function showBanner(message) {
|
||
if (!banner) return;
|
||
banner.textContent = message;
|
||
banner.style.display = "block";
|
||
}
|
||
|
||
function markActivity() {
|
||
lastActivityAt = Date.now();
|
||
warned = false;
|
||
if (banner) banner.style.display = "none";
|
||
}
|
||
|
||
function redirectToLogin(reason) {
|
||
if (redirected) return;
|
||
redirected = true;
|
||
const url = new URL(LOGIN_URL, window.location.origin);
|
||
if (reason) url.searchParams.set("reason", reason);
|
||
window.location.replace(url.toString());
|
||
}
|
||
|
||
function isHtmlNavigationRequest(input, init) {
|
||
const method = (init && init.method ? init.method : "GET").toUpperCase();
|
||
if (method !== "GET") return false;
|
||
const headers = new Headers((init && init.headers) || {});
|
||
const accept = headers.get("Accept") || "";
|
||
return accept.includes("text/html");
|
||
}
|
||
|
||
const nativeFetch = window.fetch.bind(window);
|
||
window.fetch = async function (input, init) {
|
||
const response = await nativeFetch(input, init);
|
||
if ([401, 403, 419, 440].includes(response.status)) {
|
||
showBanner("Сессия истекла. Выполняется переход на страницу входа…");
|
||
redirectToLogin("expired");
|
||
}
|
||
if (response.redirected && response.url && response.url.includes("/login")) {
|
||
redirectToLogin("expired");
|
||
}
|
||
if (!response.ok && isHtmlNavigationRequest(input, init)) {
|
||
const clone = response.clone();
|
||
try {
|
||
const text = await clone.text();
|
||
if (text && text.toLowerCase().includes("name=\"username\"")) {
|
||
redirectToLogin("expired");
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
return response;
|
||
};
|
||
|
||
document.addEventListener("submit", function (event) {
|
||
const form = event.target;
|
||
if (!(form instanceof HTMLFormElement)) return;
|
||
if (form.action && form.action.endsWith(LOGOUT_URL)) return;
|
||
markActivity();
|
||
}, true);
|
||
|
||
ACTIVITY_EVENTS.forEach((eventName) => {
|
||
window.addEventListener(eventName, markActivity, { passive: true });
|
||
});
|
||
|
||
setInterval(() => {
|
||
const idleFor = Date.now() - lastActivityAt;
|
||
const leftMs = IDLE_LIMIT_MS - idleFor;
|
||
|
||
if (!warned && leftMs <= WARNING_BEFORE_MS && leftMs > 0) {
|
||
warned = true;
|
||
const leftMinutes = Math.ceil(leftMs / 60000);
|
||
showBanner(`До выхода из системы из-за неактивности осталось около ${leftMinutes} мин.`);
|
||
}
|
||
|
||
if (idleFor >= IDLE_LIMIT_MS) {
|
||
showBanner("Сессия завершена из-за 2 часов неактивности.");
|
||
redirectToLogin("idle");
|
||
}
|
||
}, 15000);
|
||
})();
|
||
</script>
|
||
<script src="/static/script.js"></script>
|
||
</body>
|
||
</html>
|