1499 lines
56 KiB
HTML
1499 lines
56 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">
|
||
|
||
<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;
|
||
}
|
||
</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[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"
|
||
id="loadMatchDataForm"
|
||
>
|
||
<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"
|
||
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)">
|
||
▲
|
||
</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">
|
||
<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_full or 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_full or 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_full or 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_full or 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[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 }}
|
||
},
|
||
teamLogos: {
|
||
home: {{ session[15] | tojson }},
|
||
away: {{ session[18] | 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>
|
||
(function () {
|
||
const matchId = window.MATCH_DATA?.matchId;
|
||
const sessionToken = window.MATCH_DATA?.sessionToken;
|
||
const teamLogos = window.MATCH_DATA?.teamLogos || {};
|
||
|
||
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, logoUrl) {
|
||
const inputName = "ИГРОК - НЕЙТРАЛЬНЫЙ";
|
||
|
||
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(logoUrl || "")}`,
|
||
`/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,
|
||
session_token: sessionToken,
|
||
commands,
|
||
meta: {
|
||
source: "person_click",
|
||
title_input: inputName,
|
||
person_name: name,
|
||
person_role: role,
|
||
logo_url: logoUrl
|
||
}
|
||
})
|
||
});
|
||
|
||
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();
|
||
const logoUrl = side === "away" ? teamLogos.away : teamLogos.home;
|
||
|
||
let formattedName = "";
|
||
|
||
if (personType === "coach") {
|
||
formattedName = formatCoachName(rawName);
|
||
} else {
|
||
formattedName = formatPlayerName(firstName, lastName, rawName);
|
||
}
|
||
|
||
if (!formattedName) return;
|
||
|
||
sendNeutralPersonTitle(formattedName, role, logoUrl);
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
|
||
<script src="/static/script.js"></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>
|