Files
WFL/templates/match_workspace.html
2026-04-23 15:43:45 +03:00

1559 lines
57 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.

<!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>ЖФЛ Управление vMix</title>
<link rel="stylesheet" href="/static/styles.css?v=20260421_4">
<style>
.player-link-btn,
.coach-link-btn {
display: block;
width: 100%;
padding: 0;
margin: 0;
border: none;
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
font: inherit;
}
.player-link-btn .last-name,
.player-link-btn .first-name,
.coach-link-btn .coach-name {
color: #8ab4ff;
text-decoration: underline;
text-underline-offset: 2px;
}
.player-link-btn:hover .last-name,
.player-link-btn:hover .first-name,
.coach-link-btn:hover .coach-name {
color: #a9c7ff;
}
.player-link-btn:focus-visible,
.coach-link-btn:focus-visible {
outline: 2px solid rgba(138, 180, 255, 0.45);
outline-offset: 2px;
border-radius: 6px;
}
.vmix-download-btn {
text-decoration: none;
}
.vmix-icon {
width: 10px;
height: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
background: linear-gradient(135deg, #ff5f6d 0%, #7b61ff 100%);
color: #ffffff;
font-size: 11px;
font-weight: 800;
font-family: Arial, sans-serif;
letter-spacing: 0.02em;
box-shadow: 0 0 10px rgba(123, 97, 255, 0.28);
}
.vmix-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 10px;
background: #151515;
color: #fff;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.12);
transition: 0.2s ease;
}
.vmix-btn:hover {
background: #222;
}
.vmix-icon {
width: 1em; /* подгоняешь под стиль кнопок */
height: 1em;
object-fit: contain; /* чтобы не растягивалась */
display: block;
}
</style>
</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[18] }}</div>
</div>
{% if session[19] %}
<img
src="{{ session[19] }}"
alt="{{ session[18] }}"
class="team-logo"
/>
{% endif %}
</div>
<div class="header-actions compact">
<form
method="post"
action="/admin/session/{{ session[3] }}/load-match-data"
id="loadMatchDataForm"
>
<button
type="submit"
class="icon-btn header-icon"
title="Загрузить данные"
>
🌐
</button>
</form>
<a href="/admin/session/{{ session[3] }}/download-vmix-page" class="vmix-btn">
<img src="/static/images/vmix_icon.png" class="vmix-icon" alt="vMix">
</a>
<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"
data-vmix-title="СУДЕЙСКАЯ БРИГАДА"
>Судьи</a
>
<a
class="tab-link {% if tab == 'tour_schedule' %}active{% endif %}"
href="/admin/session/{{ session[3] }}?tab=tour_schedule"
data-vmix-title="РАСПИСАНИЕ ТУРА"
>
Расписание тура
</a>
<a
class="tab-link {% if tab == 'standings' %}active{% endif %}"
href="/admin/session/{{ session[3] }}?tab=standings"
data-vmix-title="ТУРНИРНАЯ ТАБЛИЦА"
>
Турнирная таблица
</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)">
&#9650;
</button>
<button type="button" onclick="stepExtraTime(-1)">
&#9660;
</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">
<button
type="button"
class="coach-left vmix-person-trigger player-link-btn coach-link-btn"
data-vmix-person-type="coach"
data-side="home"
data-name="{{ c.coach_name }}"
data-role="{{ c.role or '' }}"
title="Показать титр тренера"
>
<span class="coach-name">{{ c.coach_name }}</span>
{% if c.role %}
<span class="coach-badge">{{ c.role }}</span>
{% endif %}
</button>
<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.player_name or '' }}"
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" title="Показать титр игрока">
<div
class="player-inline vmix-person-trigger player-link"
data-vmix-person-type="player"
data-side="home"
data-first-name="{{ p.first_name or '' }}"
data-last-name="{{ p.last_name or '' }}"
data-role="{{ p.position or '' }}"
>
<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.player_name or '' }}"
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" title="Показать титр игрока">
<div
class="player-inline vmix-person-trigger player-link"
data-vmix-person-type="player"
data-side="home"
data-first-name="{{ p.first_name or '' }}"
data-last-name="{{ p.last_name or '' }}"
data-role="{{ p.position or '' }}"
>
<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">
<button
type="button"
class="coach-left vmix-person-trigger player-link-btn coach-link-btn"
data-vmix-person-type="coach"
data-side="away"
data-name="{{ c.coach_name }}"
data-role="{{ c.role or '' }}"
title="Показать титр тренера"
>
<span class="coach-name">{{ c.coach_name }}</span>
{% if c.role %}
<span class="coach-badge">{{ c.role }}</span>
{% endif %}
</button>
<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.player_name or '' }}"
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" title="Показать титр игрока">
<div
class="player-inline vmix-person-trigger player-link"
data-vmix-person-type="player"
data-side="away"
data-first-name="{{ p.first_name or '' }}"
data-last-name="{{ p.last_name or '' }}"
data-role="{{ p.position or '' }}"
>
<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.player_name or '' }}"
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" title="Показать титр игрока">
<div
class="player-inline vmix-person-trigger player-link"
data-vmix-person-type="player"
data-side="away"
data-first-name="{{ p.first_name or '' }}"
data-last-name="{{ p.last_name or '' }}"
data-role="{{ p.position or '' }}"
>
<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[18] }}</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[18] %}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[18] | tojson }}
},
teamLogos: {
home: {{ session[15] | tojson }},
away: {{ session[19] | tojson }}
},
vmixTeamLogos: {
home: {{ session[16] | tojson }},
away: {{ session[20] | 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 }},
currentUser: {{ current_user | 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[18] }}</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>
(function () {
const matchId = window.MATCH_DATA?.matchId;
const sessionToken = window.MATCH_DATA?.sessionToken;
const teamLogos = window.MATCH_DATA?.teamLogos || {};
const vmixTeamLogos = window.MATCH_DATA?.vmixTeamLogos || {};
const currentUserLogin = window.MATCH_DATA?.currentUser?.username || null;
if (!matchId || !sessionToken) {
return;
}
function formatCoachName(rawName) {
const parts = String(rawName || "").trim().split(/\s+/).filter(Boolean);
if (parts.length >= 2) {
return `${parts[1]} ${parts[0]}`;
}
return String(rawName || "").trim();
}
function formatPlayerName(firstName, lastName, fallbackName) {
const result = `${String(firstName || "").trim()} ${String(lastName || "").trim()}`.trim();
return result || String(fallbackName || "").trim();
}
async function sendNeutralPersonTitle(name, role, side) {
const inputName = "ИГРОК - НЕЙТРАЛЬНЫЙ";
const teamName = TEAM_NAMES?.[side] || "";
const logoPath = getVmixTeamLogo(teamName);
const commands = [
`/api/?Function=SetText&Input=${encodeURIComponent(inputName)}&SelectedName=${encodeURIComponent("Тренер.Text")}&Value=${encodeURIComponent(name || "")}`,
`/api/?Function=SetText&Input=${encodeURIComponent(inputName)}&SelectedName=${encodeURIComponent("Должность.Text")}&Value=${encodeURIComponent(role || "")}`,
`/api/?Function=SetImage&Input=${encodeURIComponent(inputName)}&SelectedName=${encodeURIComponent("Лого.Source")}&Value=${encodeURIComponent(logoPath || "")}`,
`/api/?Function=PreviewInput&Input=${encodeURIComponent(inputName)}`
];
try {
const response = await fetch("/api/vmix/publish-command", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Requested-With": "XMLHttpRequest"
},
credentials: "same-origin",
body: JSON.stringify({
match_id: matchId,
commands,
target_login: currentUserLogin,
meta: {
source: "person_click",
title_input: inputName,
person_name: name,
person_role: role,
side,
team_name: teamName,
logo_path: logoPath
}
})
});
if (!response.ok) {
console.error("Ошибка отправки титра в vMix:", response.status);
}
} catch (error) {
console.error("Не удалось отправить титр в vMix:", error);
}
}
document.addEventListener("click", function (event) {
const trigger = event.target.closest(".vmix-person-trigger");
if (!trigger) return;
const personType = String(trigger.dataset.vmixPersonType || "").trim();
const side = String(trigger.dataset.side || "").trim();
const rawName = String(trigger.dataset.name || "").trim();
const firstName = String(trigger.dataset.firstName || "").trim();
const lastName = String(trigger.dataset.lastName || "").trim();
const role = String(trigger.dataset.role || "").trim();
let formattedName = "";
if (personType === "coach") {
formattedName = formatCoachName(rawName);
} else {
formattedName = formatPlayerName(firstName, lastName, rawName);
}
if (!formattedName) return;
sendNeutralPersonTitle(formattedName, role, side);
});
})();
</script>
<script src="/static/script.js?v=20260421_4"></script>
<div id="matrixLoader" class="matrix-loader" aria-hidden="true">
<canvas id="matrixCanvas" class="matrix-canvas"></canvas>
<div class="matrix-content">
<div class="matrix-title" id="matrixTitle">Загрузка данных матча</div>
<div class="matrix-status" id="matrixStatusText">
Подключение к сайту...
</div>
</div>
</div>
<script>
(function () {
const loadForm = document.getElementById("loadMatchDataForm");
const loader = document.getElementById("matrixLoader");
const titleEl = document.getElementById("matrixTitle");
const statusText = document.getElementById("matrixStatusText");
const canvas = document.getElementById("matrixCanvas");
const ctx = canvas ? canvas.getContext("2d") : null;
let matrixAnimationId = null;
let drops = [];
const fontSize = 18;
const chars = "アイウエオカキクケコサシスセソ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZЖФЛ<>+*#";
let columns = 0;
let lastTime = 0;
const fps = 18;
const interval = 1000 / fps;
function resizeCanvas() {
if (!canvas || !ctx) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
columns = Math.floor(canvas.width / fontSize);
drops = Array(columns).fill(1);
}
function drawMatrix(time = 0) {
if (!canvas || !ctx) return;
if (time - lastTime < interval) {
matrixAnimationId = requestAnimationFrame(drawMatrix);
return;
}
lastTime = time;
ctx.fillStyle = "rgba(0, 0, 0, 0.12)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#00ff66";
ctx.font = fontSize + "px monospace";
for (let i = 0; i < drops.length; i++) {
const text = chars[Math.floor(Math.random() * chars.length)];
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
if (drops[i] * fontSize > canvas.height && Math.random() > 0.985) {
drops[i] = 0;
}
// drops[i] += 0.65;
drops[i] ++;
}
matrixAnimationId = requestAnimationFrame(drawMatrix);
}
function startLoader(title, message) {
if (!loader) return;
loader.classList.add("active");
loader.classList.remove("is-error", "is-success");
if (titleEl) titleEl.textContent = title || "Загрузка данных матча";
if (statusText) statusText.textContent = message || "Подключение к сайту...";
document.body.style.overflow = "hidden";
resizeCanvas();
if (!matrixAnimationId) {
drawMatrix();
}
}
function setLoaderState(state, title, message) {
if (!loader) return;
loader.classList.remove("is-error", "is-success");
if (state) {
loader.classList.add(state);
}
if (titleEl) titleEl.textContent = title || "";
if (statusText) statusText.textContent = message || "";
}
window.addEventListener("resize", resizeCanvas);
if (!loadForm) return;
loadForm.addEventListener("submit", async function (event) {
event.preventDefault();
const submitButton = loadForm.querySelector('button[type="submit"]');
if (submitButton) {
submitButton.disabled = true;
}
startLoader("Загрузка данных матча", "Подключение к сайту...");
try {
const response = await fetch(loadForm.action, {
method: "POST",
headers: {
"X-Requested-With": "XMLHttpRequest",
"Accept": "application/json"
},
credentials: "same-origin"
});
let data = {};
try {
data = await response.json();
} catch (_) {}
if (!response.ok || !data.success) {
throw new Error(data.error || "Не удалось загрузить данные с сайта");
}
setLoaderState(
"is-success",
"Загрузка завершена",
(data.message || "Данные успешно загружены") + "\nПереход во вкладку «Игра»..."
);
setTimeout(() => {
window.location.href = data.redirect_url || "{{ '/admin/session/' ~ session[3] ~ '?tab=game' }}";
}, 1200);
} catch (error) {
setLoaderState(
"is-error",
"Ошибка загрузки",
(error.message || "Не удалось загрузить данные с сайта") + "\nВозврат во вкладку «Игра» через 3 секунды..."
);
setTimeout(() => {
window.location.href = "{{ '/admin/session/' ~ session[3] ~ '?tab=game' }}";
}, 3000);
} finally {
if (submitButton) {
submitButton.disabled = false;
}
}
});
})();
</script>
</body>
</html>