2909 lines
97 KiB
HTML
2909 lines
97 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Рабочее окно матча</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0f1115;
|
||
--panel: #171a21;
|
||
--panel-2: #1d222b;
|
||
--border: #2b3240;
|
||
--text: #e8ecf3;
|
||
--muted: #9aa4b2;
|
||
--accent: #4f8cff;
|
||
--accent-hover: #3e78e6;
|
||
--success: #2ea043;
|
||
--danger: #dc3545;
|
||
--warning: #f2c94c;
|
||
--shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
||
--radius: 16px;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-family: Arial, sans-serif;
|
||
}
|
||
|
||
.page {
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.topbar {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
background: rgba(15, 17, 21, 0.96);
|
||
backdrop-filter: blur(8px);
|
||
border-bottom: 1px solid var(--border);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.topbar-inner {
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
padding: 16px 24px;
|
||
}
|
||
|
||
.match-header {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto 1fr auto;
|
||
gap: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.team-box {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
|
||
.team-box.right {
|
||
justify-content: flex-end;
|
||
text-align: right;
|
||
}
|
||
|
||
.team-logo {
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: 12px;
|
||
background: var(--panel-2);
|
||
border: 1px solid var(--border);
|
||
object-fit: contain;
|
||
padding: 6px;
|
||
}
|
||
|
||
.team-name {
|
||
font-size: 26px;
|
||
font-weight: 700;
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.match-meta {
|
||
text-align: center;
|
||
min-width: 320px;
|
||
}
|
||
|
||
.match-tour {
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
margin-bottom: 6px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.4px;
|
||
}
|
||
|
||
.match-time {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.match-place {
|
||
font-size: 14px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
align-items: stretch;
|
||
min-width: 220px;
|
||
}
|
||
|
||
.btn {
|
||
min-height: 42px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
padding: 0 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.15s ease, transform 0.05s ease;
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--accent);
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: var(--accent-hover);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: transparent;
|
||
color: var(--text);
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: rgba(255, 255, 255, 0.04);
|
||
}
|
||
|
||
.content {
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
padding: 22px 24px 32px;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 22px;
|
||
}
|
||
|
||
.tab-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
min-height: 40px;
|
||
padding: 0 14px;
|
||
border-radius: 999px;
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
color: var(--text);
|
||
text-decoration: none;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.tab-link:hover {
|
||
background: var(--panel-2);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.tab-link.active {
|
||
background: var(--accent);
|
||
border-color: var(--accent);
|
||
color: white;
|
||
}
|
||
|
||
.panel {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow);
|
||
padding: 18px;
|
||
}
|
||
|
||
.game-layout {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 440px;
|
||
gap: 20px;
|
||
align-items: start;
|
||
}
|
||
|
||
.game-main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.lineups-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 20px;
|
||
}
|
||
|
||
.match-toolbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.match-toolbar-left,
|
||
.match-toolbar-right {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.timer-box {
|
||
min-width: 122px;
|
||
height: 46px;
|
||
border-radius: 12px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--panel-2);
|
||
border: 1px solid var(--border);
|
||
font-size: 24px;
|
||
font-weight: 800;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.period-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
min-height: 36px;
|
||
padding: 0 12px;
|
||
border-radius: 999px;
|
||
background: rgba(79, 140, 255, 0.14);
|
||
color: var(--accent);
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.team-panel-title {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.section-title {
|
||
margin: 18px 0 10px;
|
||
font-size: 15px;
|
||
color: var(--muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.4px;
|
||
}
|
||
|
||
.section-title:first-child {
|
||
margin-top: 0;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
overflow: hidden;
|
||
border-radius: 12px;
|
||
background: var(--panel-2);
|
||
table-layout: auto;
|
||
}
|
||
|
||
th,
|
||
td {
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid var(--border);
|
||
text-align: left;
|
||
font-size: 14px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
th {
|
||
color: var(--muted);
|
||
font-weight: 600;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
}
|
||
|
||
tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
tr:hover td {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
}
|
||
|
||
.num-col {
|
||
width: 60px;
|
||
text-align: center;
|
||
}
|
||
|
||
.num-cell {
|
||
text-align: center;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.col-last {
|
||
width: 36%;
|
||
}
|
||
|
||
.col-first {
|
||
width: 22%;
|
||
}
|
||
|
||
.actions-col {
|
||
width: 230px;
|
||
text-align: right;
|
||
}
|
||
|
||
.player-name,
|
||
.player-first {
|
||
white-space: normal;
|
||
overflow: hidden;
|
||
text-overflow: unset;
|
||
}
|
||
|
||
.player-name {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.player-first {
|
||
color: var(--muted);
|
||
}
|
||
|
||
.gk-badge,
|
||
.captain-badge {
|
||
display: inline-block;
|
||
margin-left: 6px;
|
||
padding: 2px 6px;
|
||
font-size: 10px;
|
||
border-radius: 6px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
|
||
.gk-badge {
|
||
background: rgba(79, 140, 255, 0.2);
|
||
color: #8ab4ff;
|
||
}
|
||
|
||
.captain-badge {
|
||
background: rgba(255, 215, 0, 0.18);
|
||
color: #ffd54a;
|
||
}
|
||
|
||
.row-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
justify-content: flex-end;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.icon-btn {
|
||
width: 28px;
|
||
height: 28px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
color: var(--text);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
transition: transform 0.12s ease, background 0.12s ease, border-color 0.12s ease;
|
||
}
|
||
|
||
.icon-btn:hover {
|
||
transform: translateY(-1px);
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border-color: rgba(255, 255, 255, 0.18);
|
||
}
|
||
|
||
.icon-btn.goal {
|
||
font-size: 15px;
|
||
}
|
||
|
||
.icon-btn.yellow-card,
|
||
.icon-btn.red-card {
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.icon-btn.yellow-card::before,
|
||
.icon-btn.red-card::before {
|
||
content: "";
|
||
width: 12px;
|
||
height: 16px;
|
||
border-radius: 2px;
|
||
display: block;
|
||
}
|
||
|
||
.icon-btn.yellow-card::before {
|
||
background: var(--warning);
|
||
}
|
||
|
||
.icon-btn.red-card::before {
|
||
background: var(--danger);
|
||
}
|
||
|
||
.icon-btn.sub {
|
||
position: relative;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.icon-btn.sub::before {
|
||
content: "↗";
|
||
position: absolute;
|
||
left: 4px;
|
||
top: 2px;
|
||
color: #22c55e;
|
||
font-size: 10px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.icon-btn.sub::after {
|
||
content: "↘";
|
||
position: absolute;
|
||
right: 4px;
|
||
bottom: 2px;
|
||
color: #ef4444;
|
||
font-size: 10px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.empty-box {
|
||
padding: 18px;
|
||
border-radius: 12px;
|
||
background: var(--panel-2);
|
||
color: var(--muted);
|
||
border: 1px dashed var(--border);
|
||
}
|
||
|
||
.small-meta {
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.coaches-block {
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||
}
|
||
|
||
.coaches-block-right {
|
||
text-align: right;
|
||
}
|
||
|
||
.coach-line {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.coach-line:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.coach-left,
|
||
.coach-right {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.coach-name {
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.coach-badge {
|
||
display: inline-block;
|
||
padding: 3px 8px;
|
||
font-size: 11px;
|
||
border-radius: 8px;
|
||
font-weight: 600;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: var(--text);
|
||
}
|
||
|
||
.coach-actions {
|
||
display: inline-flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.events-panel {
|
||
position: sticky;
|
||
top: 110px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.panel-title-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
margin: 0;
|
||
}
|
||
|
||
.events-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
max-height: calc(100vh - 220px);
|
||
overflow-y: auto;
|
||
padding-right: 4px;
|
||
}
|
||
|
||
.event-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 56px 1fr;
|
||
gap: 10px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.event-side {
|
||
min-height: 64px;
|
||
}
|
||
|
||
.event-card {
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
background: var(--panel-2);
|
||
padding: 10px 12px;
|
||
min-height: 64px;
|
||
}
|
||
|
||
.event-card.home {
|
||
text-align: right;
|
||
}
|
||
|
||
.event-card.away {
|
||
text-align: left;
|
||
}
|
||
|
||
.event-time {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 12px;
|
||
background: rgba(79, 140, 255, 0.12);
|
||
color: var(--accent);
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
border: 1px solid rgba(79, 140, 255, 0.18);
|
||
}
|
||
|
||
.event-title {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.event-sub {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.event-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 26px;
|
||
height: 20px;
|
||
padding: 0 6px;
|
||
border-radius: 999px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: var(--text);
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
@media (max-width: 1320px) {
|
||
.game-layout {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.events-panel {
|
||
position: static;
|
||
}
|
||
|
||
.events-list {
|
||
max-height: none;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 980px) {
|
||
.match-header {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.team-box.right {
|
||
justify-content: flex-start;
|
||
text-align: left;
|
||
}
|
||
|
||
.lineups-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
|
||
.lineups-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 20px;
|
||
}
|
||
|
||
.team-panel-title {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.section-title {
|
||
margin: 18px 0 10px;
|
||
font-size: 15px;
|
||
color: var(--muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.4px;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
overflow: hidden;
|
||
border-radius: 12px;
|
||
background: var(--panel-2);
|
||
}
|
||
|
||
th,
|
||
td {
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid var(--border);
|
||
text-align: left;
|
||
font-size: 14px;
|
||
}
|
||
|
||
th {
|
||
color: var(--muted);
|
||
font-weight: 600;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
}
|
||
|
||
tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
tr:hover td {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
}
|
||
|
||
.num-col {
|
||
width: 70px;
|
||
}
|
||
|
||
.empty-box {
|
||
padding: 18px;
|
||
border-radius: 12px;
|
||
background: var(--panel-2);
|
||
color: var(--muted);
|
||
border: 1px dashed var(--border);
|
||
}
|
||
|
||
.small-meta {
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.num-col {
|
||
width: 60px;
|
||
text-align: center;
|
||
}
|
||
|
||
.num-cell {
|
||
text-align: center;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.player-name {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.player-first {
|
||
color: var(--muted);
|
||
}
|
||
|
||
.gk-badge {
|
||
display: inline-block;
|
||
margin-left: 6px;
|
||
padding: 2px 6px;
|
||
font-size: 10px;
|
||
border-radius: 6px;
|
||
background: rgba(79, 140, 255, 0.2);
|
||
color: #8ab4ff;
|
||
font-weight: 700;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
|
||
.table-compact th,
|
||
.table-compact td {
|
||
padding: 8px 10px;
|
||
}
|
||
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
border-radius: 8px;
|
||
letter-spacing: 0.3px;
|
||
white-space: normal;
|
||
}
|
||
|
||
.status-scheduled {
|
||
background: rgba(154, 164, 178, 0.18);
|
||
color: #c2cad5;
|
||
}
|
||
|
||
.status-live {
|
||
background: rgba(79, 140, 255, 0.18);
|
||
color: #8ab4ff;
|
||
}
|
||
|
||
.status-finished {
|
||
background: rgba(46, 160, 67, 0.18);
|
||
color: #57d17a;
|
||
}
|
||
|
||
.col-last {
|
||
width: 45%;
|
||
}
|
||
|
||
.col-first {
|
||
width: 45%;
|
||
}
|
||
|
||
.num-col {
|
||
width: 60px;
|
||
}
|
||
|
||
table {
|
||
table-layout: auto;
|
||
}
|
||
|
||
.player-name,
|
||
.player-first {
|
||
white-space: normal;
|
||
overflow: hidden;
|
||
text-overflow: unset;
|
||
}
|
||
|
||
.tour-table {
|
||
table-layout: auto;
|
||
}
|
||
|
||
.tour-date-col {
|
||
width: 1%;
|
||
white-space: normal;
|
||
}
|
||
|
||
.tour-match-col {
|
||
width: 52%;
|
||
}
|
||
|
||
.tour-meta-col {
|
||
width: 1%;
|
||
white-space: normal;
|
||
}
|
||
|
||
.tour-stadium-col {
|
||
width: auto;
|
||
}
|
||
|
||
.tour-date-cell {
|
||
white-space: normal;
|
||
color: var(--text);
|
||
}
|
||
|
||
.tour-match-cell {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.tour-meta-cell {
|
||
white-space: normal;
|
||
}
|
||
|
||
.tour-meta-wrap {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
white-space: normal;
|
||
}
|
||
|
||
.score-badge {
|
||
display: inline-block;
|
||
min-width: 48px;
|
||
text-align: center;
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
border-radius: 8px;
|
||
background: rgba(255, 255, 255, 0.06);
|
||
color: var(--text);
|
||
}
|
||
|
||
.stadium-cell {
|
||
color: var(--muted);
|
||
overflow: hidden;
|
||
text-overflow: unset;
|
||
}
|
||
|
||
.standings-table .st-pos-col {
|
||
width: 50px;
|
||
text-align: center;
|
||
}
|
||
|
||
.standings-table .st-logo-col {
|
||
width: 54px;
|
||
text-align: center;
|
||
}
|
||
|
||
.standings-table .st-team-col {
|
||
width: auto;
|
||
}
|
||
|
||
.standings-table .st-stat-col,
|
||
.standings-table .st-gfga-col,
|
||
.standings-table .st-points-col {
|
||
width: 64px;
|
||
text-align: center;
|
||
white-space: normal;
|
||
}
|
||
|
||
.standings-logo {
|
||
width: 28px;
|
||
height: 28px;
|
||
object-fit: contain;
|
||
display: inline-block;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.standings-row-home td {
|
||
background: #FF00FF !important;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.standings-row-away td {
|
||
background: #6E00A5 !important;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.standings-row-home:hover td,
|
||
.standings-row-away:hover td {
|
||
background: inherit !important;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.standings-row-home .player-name,
|
||
.standings-row-away .player-name {
|
||
color: #ffffff;
|
||
}
|
||
|
||
.standings-row-home img,
|
||
.standings-row-away img {
|
||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.35));
|
||
}
|
||
|
||
.captain-badge {
|
||
display: inline-block;
|
||
margin-left: 6px;
|
||
padding: 2px 6px;
|
||
font-size: 10px;
|
||
border-radius: 6px;
|
||
background: rgba(255, 215, 0, 0.18);
|
||
color: #ffd54a;
|
||
font-weight: 700;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
|
||
.coaches-block {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.coach-line {
|
||
font-size: 13px;
|
||
line-height: 1.3;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.coach-name {
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
|
||
.coach-role {
|
||
color: var(--muted);
|
||
}
|
||
|
||
.coaches-block-right {
|
||
text-align: right;
|
||
}
|
||
|
||
.coaches-block {
|
||
margin-bottom: 10px;
|
||
padding-bottom: 6px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||
}
|
||
|
||
.coach-badge {
|
||
display: inline-block;
|
||
margin-left: 6px;
|
||
padding: 3px 8px;
|
||
font-size: 11px;
|
||
border-radius: 8px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.2px;
|
||
white-space: normal;
|
||
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: var(--text);
|
||
}
|
||
|
||
.formation-toolbar {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
margin-bottom: 14px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.formation-select {
|
||
min-height: 40px;
|
||
padding: 0 12px;
|
||
border-radius: 10px;
|
||
border: 1px solid var(--border);
|
||
background: var(--panel-2);
|
||
color: var(--text);
|
||
}
|
||
|
||
.pitch-half {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 980px;
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
border: 2px solid rgba(255, 255, 255, 0.18);
|
||
background: #4b0b68;
|
||
}
|
||
|
||
/* Контур поля */
|
||
.pitch-half::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 14px;
|
||
border: 3px solid rgba(255, 255, 255, 0.6);
|
||
border-bottom: none;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.pitch-half::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: 14px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 180px;
|
||
height: 72px;
|
||
border: 3px solid rgba(255, 255, 255, 0.6);
|
||
border-top: none;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Центральный круг нижней части */
|
||
.pitch-center-circle {
|
||
position: absolute;
|
||
left: 50%;
|
||
bottom: 64px;
|
||
transform: translateX(-50%);
|
||
width: 180px;
|
||
height: 180px;
|
||
border: 3px solid rgba(255, 255, 255, 0.22);
|
||
border-radius: 50%;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.pitch-center-line {
|
||
position: absolute;
|
||
left: 14px;
|
||
right: 14px;
|
||
bottom: 154px;
|
||
height: 0;
|
||
border-top: 3px solid rgba(255, 255, 255, 0.35);
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Игрок */
|
||
.player-node {
|
||
position: absolute;
|
||
width: 150px;
|
||
margin-left: -95px;
|
||
transform: translateY(-50%);
|
||
z-index: 5;
|
||
user-select: none;
|
||
cursor: grab;
|
||
}
|
||
|
||
.player-node.dragging {
|
||
cursor: grabbing;
|
||
z-index: 20;
|
||
}
|
||
|
||
.player-photo {
|
||
width: 110px;
|
||
height: 138px;
|
||
margin: 0 auto;
|
||
display: block;
|
||
object-fit: contain;
|
||
filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.35));
|
||
pointer-events: none;
|
||
}
|
||
|
||
.player-photo,
|
||
.player-silhouette {
|
||
width: 95px;
|
||
height: 120px;
|
||
}
|
||
|
||
.player-silhouette {
|
||
position: relative;
|
||
width: 110px;
|
||
height: 138px;
|
||
margin: 0 auto;
|
||
border-radius: 18px 18px 8px 8px;
|
||
background: linear-gradient(180deg, #f2f2f2 0%, #d9d9d9 100%);
|
||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.35);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.player-silhouette::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: 18px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 34px;
|
||
height: 34px;
|
||
border-radius: 50%;
|
||
background: #b8b8b8;
|
||
}
|
||
|
||
.player-silhouette::after {
|
||
content: "";
|
||
position: absolute;
|
||
left: 50%;
|
||
bottom: 18px;
|
||
transform: translateX(-50%);
|
||
width: 66px;
|
||
height: 62px;
|
||
border-radius: 36px 36px 12px 12px;
|
||
background: #b8b8b8;
|
||
}
|
||
|
||
.player-label {
|
||
margin: -6px auto 0;
|
||
width: 150px;
|
||
min-height: 28px;
|
||
padding: 4px 8px;
|
||
background: #97ef1f;
|
||
color: #2a2440;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
line-height: 1.1;
|
||
text-transform: uppercase;
|
||
text-align: center;
|
||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
|
||
}
|
||
|
||
.player-label-text {
|
||
display: block;
|
||
white-space: normal;
|
||
overflow: hidden;
|
||
text-overflow: unset;
|
||
}
|
||
|
||
|
||
.match-lineup-table th {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.match-lineup-table td {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.player-fullname {
|
||
font-size: 13px;
|
||
line-height: 1.15;
|
||
}
|
||
|
||
|
||
.player-last {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.player-first {
|
||
font-size: 12px;
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.num-cell {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.actions-col {
|
||
font-size: 12px;
|
||
}
|
||
.match-lineup-table th,
|
||
.match-lineup-table td {
|
||
padding: 7px 10px; /* было 10+ */
|
||
}
|
||
|
||
.row-actions .icon-btn {
|
||
width: 24px;
|
||
height: 24px;
|
||
font-size: 12px;
|
||
}
|
||
.row-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
flex-wrap: nowrap; /* ❗ главное */
|
||
gap: 4px;
|
||
}
|
||
.actions-col {
|
||
white-space: nowrap; /* ❗ чтобы колонка не ломалась */
|
||
}
|
||
.row-actions .icon-btn {
|
||
width: 22px;
|
||
height: 22px;
|
||
font-size: 11px;
|
||
padding: 0;
|
||
}
|
||
|
||
/* === Game tab: one beautiful player column === */
|
||
.game-lineup-table {
|
||
width: 100%;
|
||
table-layout: fixed;
|
||
}
|
||
|
||
.game-lineup-table .num-col {
|
||
width: 56px;
|
||
text-align: center;
|
||
}
|
||
|
||
.game-lineup-table .player-col {
|
||
width: auto;
|
||
}
|
||
|
||
.game-lineup-table .actions-col {
|
||
width: 150px;
|
||
text-align: right;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.game-lineup-table th,
|
||
.game-lineup-table td {
|
||
padding: 8px 10px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.game-lineup-table td.player-cell {
|
||
min-width: 0;
|
||
}
|
||
|
||
.player-inline {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
.player-inline-text {
|
||
min-width: 0;
|
||
flex: 1 1 auto;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
font-size: 13px;
|
||
line-height: 1.15;
|
||
}
|
||
|
||
.player-inline-text .last-name {
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
}
|
||
|
||
.player-inline-text .first-name {
|
||
font-weight: 400;
|
||
color: var(--text);
|
||
opacity: 0.92;
|
||
}
|
||
|
||
.player-inline .badges {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex: 0 0 auto;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.player-inline .gk-badge,
|
||
.player-inline .captain-badge {
|
||
margin-left: 0;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.game-lineup-table .row-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
.game-lineup-table .icon-btn {
|
||
width: 22px;
|
||
height: 22px;
|
||
font-size: 11px;
|
||
padding: 0;
|
||
background: transparent;
|
||
border: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.game-lineup-table .icon-btn:hover {
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border: none;
|
||
}
|
||
|
||
|
||
.score-box {
|
||
min-width: 110px;
|
||
height: 46px;
|
||
border-radius: 12px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--panel-2);
|
||
border: 1px solid var(--border);
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
letter-spacing: 1px;
|
||
padding: 0 14px;
|
||
}
|
||
|
||
.extra-time-wrap {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.extra-time-input {
|
||
width: 76px;
|
||
min-height: 38px;
|
||
padding: 0 10px;
|
||
border-radius: 10px;
|
||
border: 1px solid var(--border);
|
||
background: var(--panel-2);
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.bench-pick-hint {
|
||
display: none;
|
||
margin: 6px 0 10px;
|
||
font-size: 12px;
|
||
color: var(--accent);
|
||
}
|
||
|
||
.bench-pick-active {
|
||
background: rgba(79, 140, 255, 0.12) !important;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
|
||
.lineup-row-sub-in td {
|
||
background: rgba(46, 160, 67, 0.14) !important;
|
||
}
|
||
|
||
.lineup-row-sub-out td {
|
||
background: rgba(220, 53, 69, 0.14) !important;
|
||
}
|
||
|
||
|
||
.event-card {
|
||
position: relative;
|
||
}
|
||
|
||
.event-delete-btn {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
width: 18px;
|
||
height: 18px;
|
||
border: none;
|
||
border-radius: 999px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: var(--muted);
|
||
cursor: pointer;
|
||
font-size: 11px;
|
||
line-height: 1;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.event-delete-btn:hover {
|
||
background: rgba(220, 53, 69, 0.18);
|
||
color: #ffd7dc;
|
||
}
|
||
|
||
.event-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
line-height: 1.2;
|
||
padding-right: 18px;
|
||
}
|
||
|
||
</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">
|
||
<form method="post" action="/admin/session/{{ session[3] }}/load-match-data">
|
||
<button type="submit" class="btn btn-primary" style="width:100%;">
|
||
Загрузить данные по матчу
|
||
</button>
|
||
</form>
|
||
|
||
<form method="post" action="/admin/session/{{ session[3] }}/close">
|
||
<button type="submit" class="btn btn-secondary" style="width:100%;">
|
||
Закрыть сессию
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<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" onclick="setMatchPeriod('1H')">1-й тайм</button>
|
||
<button type="button" class="btn btn-secondary" onclick="setMatchPeriod('HT')">Перерыв</button>
|
||
<button type="button" class="btn btn-primary" onclick="setMatchPeriod('2H')">2-й тайм</button>
|
||
<button type="button" class="btn btn-secondary" 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">
|
||
<input type="number" min="0" step="1" class="extra-time-input" id="extraTimeMinutes" placeholder="+мин">
|
||
<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">
|
||
{% 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="Жёлтая карточка"
|
||
onclick='addCoachEvent("home", "yellow", {{ c.coach_name | tojson }})'></button>
|
||
<button type="button" class="icon-btn red-card" title="Красная карточка"
|
||
onclick='addCoachEvent("home", "red", {{ c.coach_name | tojson }})'></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">
|
||
{% 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="Жёлтая карточка"
|
||
onclick='addCoachEvent("away", "yellow", {{ c.coach_name | tojson }})'></button>
|
||
<button type="button" class="icon-btn red-card" title="Красная карточка"
|
||
onclick='addCoachEvent("away", "red", {{ c.coach_name | tojson }})'></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 == 'referees' %}
|
||
<div class="panel">
|
||
|
||
<div class="team-panel-title">Судейская бригада</div>
|
||
|
||
{% if referees %}
|
||
<table class="table-compact">
|
||
<thead>
|
||
<tr>
|
||
<th>Роль</th>
|
||
<th>ФИО</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for r in referees %}
|
||
<tr>
|
||
<td>{{ r.role or "—" }}</td>
|
||
<td class="player-name">{{ r.referee_name }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div class="empty-box">Судьи не найдены.</div>
|
||
{% endif %}
|
||
|
||
</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="table-compact 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-gfga-col">ПМ</th>
|
||
<th class="st-points-col">О</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for row in standings %}
|
||
<tr class="
|
||
{% if row.team_name == session[14] %}
|
||
standings-row-home
|
||
{% elif row.team_name == session[17] %}
|
||
standings-row-away
|
||
{% endif %}
|
||
">
|
||
<td class="num-cell">{{ row.position }}</td>
|
||
|
||
<td class="num-cell">
|
||
{% if row.team_logo %}
|
||
<img src="{{ row.team_logo }}" alt="{{ row.team_name }}" class="standings-logo">
|
||
{% endif %}
|
||
</td>
|
||
|
||
<td class="player-name">
|
||
{{ row.team_name }}
|
||
</td>
|
||
|
||
<td class="num-cell">{{ row.played }}</td>
|
||
<td class="num-cell">{{ row.wins }}</td>
|
||
<td class="num-cell">{{ row.draws }}</td>
|
||
<td class="num-cell">{{ row.losses }}</td>
|
||
<td class="num-cell">{{ row.goals_for }}</td>
|
||
<td class="num-cell">{{ row.goals_against }}</td>
|
||
<td class="num-cell"><strong>{{ row.points }}</strong></td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div class="empty-box">Турнирная таблица пока не загружена.</div>
|
||
{% endif %}
|
||
</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">
|
||
<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>
|
||
</select>
|
||
<button class="btn btn-secondary" onclick="applyPreset('home')">Применить расстановку</button>
|
||
<button class="btn btn-primary" 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">
|
||
<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>
|
||
</select>
|
||
<button class="btn btn-secondary" onclick="applyPreset('away')">Применить расстановку</button>
|
||
<button class="btn btn-primary" 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>
|
||
{% else %}
|
||
<div class="panel">
|
||
<div class="empty-box">Эта вкладка пока в разработке.</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- prettier-ignore -->
|
||
<script>
|
||
const TEAM_NAMES = {
|
||
home: {{ session[14] | tojson }},
|
||
away: {{ session[17] | tojson }}
|
||
};
|
||
const SESSION_TOKEN = {{ session[3] | tojson }};
|
||
|
||
let currentPeriod = null;
|
||
let matchClockSeconds = 0;
|
||
let timerId = null;
|
||
let eventSeq = 1;
|
||
let matchEvents = [];
|
||
let scoreState = { home: 0, away: 0 };
|
||
let activeSubstitution = null;
|
||
let extraTimeMinutes = { first: 0, second: 0 };
|
||
let originalLineupState = null;
|
||
const STORAGE_KEY = `match_admin_state_${window.location.pathname}`;
|
||
|
||
function escapeHtml(value) {
|
||
return String(value ?? "")
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
function formatClock(totalSeconds) {
|
||
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0");
|
||
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
||
return `${minutes}:${seconds}`;
|
||
}
|
||
|
||
function getFootballMinute(totalSeconds) {
|
||
return Math.floor(totalSeconds / 60) + 1;
|
||
}
|
||
|
||
function updateTimerUI() {
|
||
const timer = document.getElementById("matchTimer");
|
||
if (timer) {
|
||
timer.textContent = formatClock(matchClockSeconds);
|
||
}
|
||
}
|
||
|
||
function updateScoreUI() {
|
||
const score = document.getElementById("matchScore");
|
||
if (score) {
|
||
score.textContent = `${scoreState.home} : ${scoreState.away}`;
|
||
}
|
||
}
|
||
|
||
function updatePeriodBadge(text) {
|
||
const badge = document.getElementById("periodBadge");
|
||
if (badge) {
|
||
badge.textContent = text;
|
||
}
|
||
}
|
||
|
||
function stopTimer() {
|
||
if (timerId) {
|
||
clearInterval(timerId);
|
||
timerId = null;
|
||
}
|
||
saveMatchState();
|
||
}
|
||
|
||
function startTimer() {
|
||
stopTimer();
|
||
timerId = setInterval(() => {
|
||
matchClockSeconds += 1;
|
||
updateTimerUI();
|
||
saveMatchState();
|
||
}, 1000);
|
||
saveMatchState();
|
||
}
|
||
|
||
|
||
function getPersistedState() {
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY);
|
||
return raw ? JSON.parse(raw) : null;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
|
||
async function saveEventToServer(eventData) {
|
||
if (!SESSION_TOKEN) return null;
|
||
try {
|
||
const res = await fetch(`/admin/session/${SESSION_TOKEN}/event`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(eventData)
|
||
});
|
||
if (!res.ok) return null;
|
||
return await res.json();
|
||
} catch (e) {
|
||
console.warn("Не удалось сохранить событие в БД", e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function pushEvent(side, type, personName, meta = "", playerId = null) {
|
||
let eventSide = side;
|
||
|
||
if (type === "own_goal") {
|
||
eventSide = getOppositeSide(side);
|
||
}
|
||
|
||
if (type === "goal" || type === "penalty" || type === "own_goal") {
|
||
scoreState[eventSide] += 1;
|
||
updateScoreUI();
|
||
}
|
||
|
||
const minuteLabel = formatMinuteLabel(matchClockSeconds);
|
||
const numericMinute =
|
||
parseInt(String(minuteLabel).split("+").pop().replace("'", ""), 10) ||
|
||
getFootballMinute(matchClockSeconds);
|
||
|
||
const item = {
|
||
id: null,
|
||
side: eventSide,
|
||
type,
|
||
icon: getEventIcon(type),
|
||
title: getEventTitle(type, personName),
|
||
teamName: TEAM_NAMES[eventSide] || "Команда",
|
||
minute: minuteLabel,
|
||
seconds: matchClockSeconds,
|
||
meta,
|
||
player_name: personName
|
||
};
|
||
|
||
matchEvents.push(item);
|
||
matchEvents.sort((a, b) => {
|
||
if (a.seconds !== b.seconds) return a.seconds - b.seconds;
|
||
return (a.id || 0) - (b.id || 0);
|
||
});
|
||
|
||
renderEvents();
|
||
saveMatchState();
|
||
|
||
const saved = await saveEventToServer({
|
||
side: eventSide,
|
||
type,
|
||
player_name: personName,
|
||
player_id: playerId,
|
||
minute: numericMinute,
|
||
seconds: matchClockSeconds,
|
||
meta
|
||
});
|
||
|
||
if (saved && saved.id) {
|
||
item.id = saved.id;
|
||
renderEvents();
|
||
saveMatchState();
|
||
}
|
||
}
|
||
|
||
|
||
|
||
async function loadEventsFromServer() {
|
||
if (!SESSION_TOKEN) return;
|
||
|
||
try {
|
||
const res = await fetch(`/admin/session/${SESSION_TOKEN}/events`);
|
||
if (!res.ok) return;
|
||
|
||
const data = await res.json();
|
||
if (!Array.isArray(data)) return;
|
||
|
||
matchEvents = data.map((event) => ({
|
||
...event,
|
||
icon: getEventIcon(event.type),
|
||
title: getEventTitle(event.type, event.player_name),
|
||
minute: typeof event.minute === "string" && event.minute.includes("'")
|
||
? event.minute
|
||
: `${event.minute || 1}'`
|
||
}));
|
||
|
||
scoreState = { home: 0, away: 0 };
|
||
matchEvents.forEach((event) => {
|
||
if (["goal", "penalty", "own_goal"].includes(event.type)) {
|
||
scoreState[event.side] = (scoreState[event.side] || 0) + 1;
|
||
}
|
||
});
|
||
|
||
updateScoreUI();
|
||
renderEvents();
|
||
} catch (e) {
|
||
console.warn("Не удалось загрузить события из БД", e);
|
||
}
|
||
}
|
||
|
||
async function deleteEvent(eventId) {
|
||
if (!eventId) return;
|
||
|
||
try {
|
||
const res = await fetch(`/admin/session/${SESSION_TOKEN}/event/${eventId}`, {
|
||
method: "DELETE"
|
||
});
|
||
if (!res.ok) return;
|
||
} catch (e) {
|
||
console.warn("Не удалось удалить событие", e);
|
||
return;
|
||
}
|
||
|
||
matchEvents = matchEvents.filter((event) => Number(event.id) !== Number(eventId));
|
||
|
||
scoreState = { home: 0, away: 0 };
|
||
matchEvents.forEach((event) => {
|
||
if (["goal", "penalty", "own_goal"].includes(event.type)) {
|
||
scoreState[event.side] = (scoreState[event.side] || 0) + 1;
|
||
}
|
||
});
|
||
|
||
updateScoreUI();
|
||
renderEvents();
|
||
saveMatchState();
|
||
}
|
||
|
||
|
||
|
||
|
||
async function clearEventsOnServer() {
|
||
if (!SESSION_TOKEN) return;
|
||
try {
|
||
await fetch(`/admin/session/${SESSION_TOKEN}/events`, {
|
||
method: "DELETE"
|
||
});
|
||
} catch (e) {
|
||
console.warn("Не удалось очистить события в БД", e);
|
||
}
|
||
}
|
||
|
||
|
||
function saveMatchState() {
|
||
try {
|
||
const payload = {
|
||
currentPeriod,
|
||
matchClockSeconds,
|
||
timerRunning: !!timerId,
|
||
savedAt: Date.now(),
|
||
eventSeq,
|
||
matchEvents,
|
||
scoreState,
|
||
extraTimeMinutes,
|
||
originalLineupState
|
||
};
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
|
||
} catch (e) {
|
||
console.warn("Не удалось сохранить состояние матча", e);
|
||
}
|
||
}
|
||
|
||
function restoreMatchState() {
|
||
const state = getPersistedState();
|
||
if (!state) return;
|
||
|
||
currentPeriod = state.currentPeriod ?? null;
|
||
matchClockSeconds = Number(state.matchClockSeconds || 0);
|
||
eventSeq = Number(state.eventSeq || 1);
|
||
matchEvents = Array.isArray(state.matchEvents) ? state.matchEvents : [];
|
||
scoreState = state.scoreState || { home: 0, away: 0 };
|
||
extraTimeMinutes = state.extraTimeMinutes || { first: 0, second: 0 };
|
||
originalLineupState = state.originalLineupState || originalLineupState;
|
||
|
||
if (state.timerRunning && state.savedAt) {
|
||
const elapsed = Math.max(0, Math.floor((Date.now() - Number(state.savedAt)) / 1000));
|
||
matchClockSeconds += elapsed;
|
||
}
|
||
|
||
if (currentPeriod === "1H") {
|
||
updatePeriodBadge(extraTimeMinutes.first ? `1-й тайм +${extraTimeMinutes.first}` : "1-й тайм");
|
||
} else if (currentPeriod === "HT") {
|
||
updatePeriodBadge(extraTimeMinutes.first ? `Перерыв · +${extraTimeMinutes.first}` : "Перерыв");
|
||
} else if (currentPeriod === "2H") {
|
||
updatePeriodBadge(extraTimeMinutes.second ? `2-й тайм +${extraTimeMinutes.second}` : "2-й тайм");
|
||
} else if (currentPeriod === "FT") {
|
||
updatePeriodBadge("Матч завершён");
|
||
saveMatchState();
|
||
}
|
||
|
||
updateTimerUI();
|
||
updateScoreUI();
|
||
renderEvents();
|
||
|
||
if (state.timerRunning && (currentPeriod === "1H" || currentPeriod === "2H")) {
|
||
startTimer();
|
||
}
|
||
|
||
if (!originalLineupState) {
|
||
originalLineupState = captureCurrentLineupState();
|
||
}
|
||
}
|
||
|
||
function setMatchPeriod(period) {
|
||
currentPeriod = period;
|
||
|
||
if (period === "1H") {
|
||
matchClockSeconds = 0;
|
||
updateTimerUI();
|
||
updatePeriodBadge(extraTimeMinutes.first ? `1-й тайм +${extraTimeMinutes.first}` : "1-й тайм");
|
||
startTimer();
|
||
saveMatchState();
|
||
return;
|
||
}
|
||
|
||
if (period === "HT") {
|
||
stopTimer();
|
||
if (matchClockSeconds < 45 * 60) {
|
||
matchClockSeconds = 45 * 60;
|
||
updateTimerUI();
|
||
}
|
||
updatePeriodBadge(extraTimeMinutes.first ? `Перерыв · +${extraTimeMinutes.first}` : "Перерыв");
|
||
saveMatchState();
|
||
return;
|
||
}
|
||
|
||
if (period === "2H") {
|
||
if (matchClockSeconds < 45 * 60) {
|
||
matchClockSeconds = 45 * 60;
|
||
}
|
||
updateTimerUI();
|
||
updatePeriodBadge(extraTimeMinutes.second ? `2-й тайм +${extraTimeMinutes.second}` : "2-й тайм");
|
||
startTimer();
|
||
saveMatchState();
|
||
return;
|
||
}
|
||
|
||
if (period === "FT") {
|
||
stopTimer();
|
||
updatePeriodBadge("Матч завершён");
|
||
saveMatchState();
|
||
}
|
||
}
|
||
|
||
function applyExtraTime() {
|
||
const input = document.getElementById("extraTimeMinutes");
|
||
const value = Math.max(0, Number(input ? input.value : 0));
|
||
|
||
if (currentPeriod === "1H" || currentPeriod === "HT") {
|
||
extraTimeMinutes.first = value;
|
||
updatePeriodBadge(value ? `1-й тайм +${value}` : "1-й тайм");
|
||
saveMatchState();
|
||
return;
|
||
}
|
||
|
||
if (currentPeriod === "2H" || currentPeriod === "FT") {
|
||
extraTimeMinutes.second = value;
|
||
updatePeriodBadge(value ? `2-й тайм +${value}` : "2-й тайм");
|
||
saveMatchState();
|
||
}
|
||
}
|
||
|
||
function getOppositeSide(side) {
|
||
return side === "home" ? "away" : "home";
|
||
}
|
||
|
||
function getEventIcon(type) {
|
||
const map = {
|
||
goal: "⚽",
|
||
own_goal: "АГ",
|
||
penalty: "П",
|
||
sub: "⇄",
|
||
yellow: "🟨",
|
||
red: "🟥",
|
||
coach_yellow: "🟨",
|
||
coach_red: "🟥"
|
||
};
|
||
return map[type] || "•";
|
||
}
|
||
|
||
function getEventTitle(type, name) {
|
||
const map = {
|
||
goal: "Гол",
|
||
own_goal: "Автогол",
|
||
penalty: "Пенальти",
|
||
sub: "Замена",
|
||
yellow: "Жёлтая карточка",
|
||
red: "Красная карточка",
|
||
coach_yellow: "Жёлтая карточка тренеру",
|
||
coach_red: "Красная карточка тренеру"
|
||
};
|
||
return `${map[type] || "Событие"}${name ? " — " + name : ""}`;
|
||
}
|
||
|
||
function formatMinuteLabel(totalSeconds) {
|
||
const minute = getFootballMinute(totalSeconds);
|
||
|
||
if ((currentPeriod === "1H" || currentPeriod === "HT") && minute > 45 && extraTimeMinutes.first > 0) {
|
||
return `45+${Math.min(minute - 45, extraTimeMinutes.first)}'`;
|
||
}
|
||
|
||
if ((currentPeriod === "2H" || currentPeriod === "FT") && minute > 90 && extraTimeMinutes.second > 0) {
|
||
return `90+${Math.min(minute - 90, extraTimeMinutes.second)}'`;
|
||
}
|
||
|
||
return `${minute}'`;
|
||
}
|
||
|
||
function setBenchHint(side, visible) {
|
||
const hint = document.getElementById(side === "home" ? "homeBenchHint" : "awayBenchHint");
|
||
if (hint) hint.style.display = visible ? "block" : "none";
|
||
}
|
||
|
||
function clearBenchSelection() {
|
||
document.querySelectorAll(".bench-pick-active").forEach(el => el.classList.remove("bench-pick-active"));
|
||
setBenchHint("home", false);
|
||
setBenchHint("away", false);
|
||
}
|
||
|
||
function beginSubstitution(side, playerId, playerName, btn) {
|
||
const row = btn ? btn.closest("tr.lineup-row") : null;
|
||
activeSubstitution = {
|
||
side,
|
||
outPlayerId: playerId,
|
||
outPlayerName: playerName,
|
||
outRow: row
|
||
};
|
||
clearBenchSelection();
|
||
setBenchHint(side, true);
|
||
}
|
||
|
||
async function selectBenchPlayer(side, playerId, playerName, btn) {
|
||
if (!activeSubstitution || activeSubstitution.side !== side) return;
|
||
|
||
clearBenchSelection();
|
||
if (btn) btn.classList.add("bench-pick-active");
|
||
|
||
const outName = activeSubstitution.outPlayerName;
|
||
const inName = playerName;
|
||
const benchRow = btn ? btn.closest("tr.lineup-row") : null;
|
||
const outRow = activeSubstitution.outRow || null;
|
||
|
||
if (outRow && benchRow) {
|
||
swapLineupPlayers(outRow, benchRow);
|
||
}
|
||
|
||
const minuteLabel = formatMinuteLabel(matchClockSeconds);
|
||
const numericMinute =
|
||
parseInt(String(minuteLabel).split("+").pop().replace("'", ""), 10) ||
|
||
getFootballMinute(matchClockSeconds);
|
||
|
||
const item = {
|
||
id: null,
|
||
side,
|
||
type: "sub",
|
||
icon: getEventIcon("sub"),
|
||
title: `${outName} → ${inName}`,
|
||
minute: minuteLabel,
|
||
seconds: matchClockSeconds,
|
||
player_name: `${outName} → ${inName}`
|
||
};
|
||
|
||
matchEvents.push(item);
|
||
renderEvents();
|
||
saveMatchState();
|
||
|
||
const saved = await saveEventToServer({
|
||
side,
|
||
type: "sub",
|
||
player_name: `${outName} → ${inName}`,
|
||
player_out_id: activeSubstitution.outPlayerId,
|
||
player_in_id: playerId,
|
||
minute: numericMinute,
|
||
seconds: matchClockSeconds,
|
||
meta: null
|
||
});
|
||
|
||
if (saved && saved.id) {
|
||
item.id = saved.id;
|
||
renderEvents();
|
||
saveMatchState();
|
||
}
|
||
|
||
activeSubstitution = null;
|
||
clearBenchSelection();
|
||
}
|
||
|
||
|
||
async function pushEvent(side, type, personName, meta = "", playerId = null) {
|
||
let eventSide = side;
|
||
|
||
if (type === "own_goal") {
|
||
eventSide = getOppositeSide(side);
|
||
}
|
||
|
||
if (type === "goal" || type === "penalty" || type === "own_goal") {
|
||
scoreState[eventSide] += 1;
|
||
updateScoreUI();
|
||
}
|
||
|
||
const minuteLabel = formatMinuteLabel(matchClockSeconds);
|
||
const numericMinute = parseInt(String(minuteLabel).split("+").pop().replace("'", ""), 10) || getFootballMinute(matchClockSeconds);
|
||
|
||
const item = {
|
||
id: null,
|
||
side: eventSide,
|
||
type,
|
||
icon: getEventIcon(type),
|
||
title: getEventTitle(type, personName),
|
||
teamName: TEAM_NAMES[eventSide] || "Команда",
|
||
minute: minuteLabel,
|
||
seconds: matchClockSeconds,
|
||
meta,
|
||
player_name: personName
|
||
};
|
||
|
||
matchEvents.push(item);
|
||
matchEvents.sort((a, b) => {
|
||
if (a.seconds !== b.seconds) return a.seconds - b.seconds;
|
||
return (a.id || 0) - (b.id || 0);
|
||
});
|
||
renderEvents();
|
||
saveMatchState();
|
||
|
||
const saved = await saveEventToServer({
|
||
side: eventSide,
|
||
type,
|
||
player_name: personName,
|
||
player_id: playerId, // 🔥 ВОТ ЭТО
|
||
minute: numericMinute,
|
||
seconds: matchClockSeconds,
|
||
meta
|
||
});
|
||
|
||
if (saved && saved.id) {
|
||
item.id = saved.id;
|
||
renderEvents();
|
||
saveMatchState();
|
||
}
|
||
}
|
||
|
||
function addPlayerEvent(side, type, playerId, playerName) {
|
||
pushEvent(side, type, playerName, `Игрок #${playerId ?? ""}`, playerId);
|
||
}
|
||
|
||
function addSubEvent(side, playerId, playerName, isBench) {
|
||
if (isBench) {
|
||
selectBenchPlayer(side, playerId, playerName, null);
|
||
} else {
|
||
beginSubstitution(side, playerId, playerName);
|
||
}
|
||
}
|
||
|
||
function addCoachEvent(side, cardType, coachName) {
|
||
const type = cardType === "yellow" ? "coach_yellow" : "coach_red";
|
||
pushEvent(side, type, coachName, "Тренер", null);
|
||
}
|
||
|
||
function renderEvents() {
|
||
const list = document.getElementById("eventsList");
|
||
if (!list) return;
|
||
|
||
if (!matchEvents.length) {
|
||
list.innerHTML = '<div class="empty-box">Событий пока нет.</div>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = matchEvents.map((event) => {
|
||
const compactTitle = String(event.title || "").includes(" — ")
|
||
? String(event.title).split(" — ").slice(1).join(" — ")
|
||
: String(event.title || "");
|
||
|
||
const deleteHtml = event.id
|
||
? `<button type="button" class="event-delete-btn" title="Удалить событие" onclick="deleteEvent(${Number(event.id)})">×</button>`
|
||
: "";
|
||
|
||
const cardHtml = `
|
||
<div class="event-card ${event.side}">
|
||
${deleteHtml}
|
||
<div class="event-badge">${escapeHtml(event.icon)}</div>
|
||
<div class="event-title">${escapeHtml(compactTitle)}</div>
|
||
</div>
|
||
`;
|
||
|
||
return `
|
||
<div class="event-row">
|
||
<div class="event-side">${event.side === "home" ? cardHtml : ""}</div>
|
||
<div class="event-time">${escapeHtml(event.minute)}</div>
|
||
<div class="event-side">${event.side === "away" ? cardHtml : ""}</div>
|
||
</div>
|
||
`;
|
||
}).join("");
|
||
}
|
||
|
||
async function clearMatchEvents() {
|
||
matchEvents = [];
|
||
scoreState = { home: 0, away: 0 };
|
||
eventSeq = 1;
|
||
activeSubstitution = null;
|
||
clearBenchSelection();
|
||
restoreOriginalLineupRows();
|
||
updateScoreUI();
|
||
renderEvents();
|
||
saveMatchState();
|
||
await clearEventsOnServer();
|
||
}
|
||
|
||
restoreMatchState();
|
||
updateTimerUI();
|
||
updateScoreUI();
|
||
renderEvents();
|
||
|
||
|
||
function escapeJsString(value) {
|
||
return String(value ?? "")
|
||
.replaceAll("\\", "\\\\")
|
||
.replaceAll("'", "\\'");
|
||
}
|
||
|
||
function isGoalkeeperPosition(position) {
|
||
const pos = String(position || "").trim().toLowerCase();
|
||
return ["вр.", "вр", "вратарь", "gk", "goalkeeper"].includes(pos);
|
||
}
|
||
|
||
function getRowPlayer(row) {
|
||
return {
|
||
id: row.dataset.playerId || "",
|
||
name: row.dataset.playerName || "",
|
||
lastName: row.dataset.playerLast || "",
|
||
firstName: row.dataset.playerFirst || "",
|
||
number: row.dataset.playerNumber || "",
|
||
position: row.dataset.playerPosition || "",
|
||
isCaptain: row.dataset.playerCaptain === "1"
|
||
};
|
||
}
|
||
|
||
function setRowPlayer(row, player) {
|
||
row.dataset.playerId = player.id ?? "";
|
||
row.dataset.playerName = player.name ?? "";
|
||
row.dataset.playerLast = player.lastName ?? "";
|
||
row.dataset.playerFirst = player.firstName ?? "";
|
||
row.dataset.playerNumber = player.number ?? "";
|
||
row.dataset.playerPosition = player.position ?? "";
|
||
row.dataset.playerCaptain = player.isCaptain ? "1" : "0";
|
||
}
|
||
|
||
function renderLineupRow(row) {
|
||
const side = row.dataset.side;
|
||
const role = row.dataset.role;
|
||
const player = getRowPlayer(row);
|
||
const fullName = (player.name || `${player.lastName || ""} ${player.firstName || ""}`).trim();
|
||
const gkBadge = isGoalkeeperPosition(player.position) ? '<span class="gk-badge">ВР</span>' : '';
|
||
const captainBadge = player.isCaptain ? '<span class="captain-badge">К</span>' : '';
|
||
const firstNameHtml = player.firstName ? `<span class="first-name"> ${escapeHtml(player.firstName)}</span>` : '';
|
||
const idJs = JSON.stringify(player.id || player.number || "");
|
||
const nameJs = JSON.stringify(fullName);
|
||
|
||
let actionsHtml = '';
|
||
if (role === "starting") {
|
||
actionsHtml = `
|
||
<div class="row-actions">
|
||
<button type="button" class="icon-btn goal" title="Гол"
|
||
onclick='addPlayerEvent("${side}", "goal", ${idJs}, ${nameJs})'>⚽</button>
|
||
<button type="button" class="icon-btn" title="Автогол"
|
||
onclick='addPlayerEvent("${side}", "own_goal", ${idJs}, ${nameJs})'>АГ</button>
|
||
<button type="button" class="icon-btn" title="Пенальти"
|
||
onclick='addPlayerEvent("${side}", "penalty", ${idJs}, ${nameJs})'>П</button>
|
||
<button type="button" class="icon-btn sub" title="Замена"
|
||
onclick='beginSubstitution("${side}", ${idJs}, ${nameJs}, this)'>⇄</button>
|
||
<button type="button" class="icon-btn yellow-card" title="Жёлтая карточка"
|
||
onclick='addPlayerEvent("${side}", "yellow", ${idJs}, ${nameJs})'></button>
|
||
<button type="button" class="icon-btn red-card" title="Красная карточка"
|
||
onclick='addPlayerEvent("${side}", "red", ${idJs}, ${nameJs})'></button>
|
||
</div>
|
||
`;
|
||
} else {
|
||
actionsHtml = `
|
||
<div class="row-actions">
|
||
<button type="button" class="icon-btn sub" title="Замена"
|
||
onclick='selectBenchPlayer("${side}", ${idJs}, ${nameJs}, this)'>⇄</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
row.innerHTML = `
|
||
<td class="num-cell">${escapeHtml(player.number)}</td>
|
||
<td class="player-cell">
|
||
<div class="player-inline">
|
||
<div class="player-inline-text">
|
||
<span class="last-name">${escapeHtml(player.lastName || player.name)}</span>${firstNameHtml}
|
||
</div>
|
||
<span class="badges">${gkBadge}${captainBadge}</span>
|
||
</div>
|
||
</td>
|
||
<td class="actions-col">${actionsHtml}</td>
|
||
`;
|
||
}
|
||
|
||
function flashSubstitutionRows(inRow, outRow) {
|
||
document.querySelectorAll(".lineup-row-sub-in, .lineup-row-sub-out").forEach(el => {
|
||
el.classList.remove("lineup-row-sub-in", "lineup-row-sub-out");
|
||
});
|
||
|
||
if (inRow) inRow.classList.add("lineup-row-sub-in");
|
||
if (outRow) outRow.classList.add("lineup-row-sub-out");
|
||
}
|
||
|
||
function swapLineupPlayers(startingRow, benchRow) {
|
||
const startingPlayer = getRowPlayer(startingRow);
|
||
const benchPlayer = getRowPlayer(benchRow);
|
||
|
||
setRowPlayer(startingRow, benchPlayer);
|
||
setRowPlayer(benchRow, startingPlayer);
|
||
|
||
renderLineupRow(startingRow);
|
||
renderLineupRow(benchRow);
|
||
sortLineupRows(startingRow.dataset.side, "starting");
|
||
sortLineupRows(startingRow.dataset.side, "bench");
|
||
flashSubstitutionRows(startingRow, benchRow);
|
||
saveMatchState();
|
||
}
|
||
|
||
|
||
|
||
function sortLineupRows(side, role) {
|
||
const rows = Array.from(document.querySelectorAll(
|
||
`tr.lineup-row[data-side="${side}"][data-role="${role}"]`
|
||
));
|
||
|
||
const tbody = rows[0]?.closest("tbody");
|
||
if (!tbody) return;
|
||
|
||
rows.sort((a, b) => {
|
||
const pa = getRowPlayer(a);
|
||
const pb = getRowPlayer(b);
|
||
|
||
const gkA = isGoalkeeperPosition(pa.position) ? 1 : 0;
|
||
const gkB = isGoalkeeperPosition(pb.position) ? 1 : 0;
|
||
|
||
if (gkA !== gkB) return gkB - gkA;
|
||
|
||
const numA = parseInt(pa.number, 10);
|
||
const numB = parseInt(pb.number, 10);
|
||
|
||
const safeA = Number.isFinite(numA) ? numA : 999;
|
||
const safeB = Number.isFinite(numB) ? numB : 999;
|
||
|
||
return safeA - safeB;
|
||
});
|
||
|
||
rows.forEach(row => tbody.appendChild(row));
|
||
}
|
||
|
||
function captureCurrentLineupState() {
|
||
const rows = Array.from(document.querySelectorAll("tr.lineup-row"));
|
||
return rows.map((row, index) => ({
|
||
index,
|
||
side: row.dataset.side || "",
|
||
role: row.dataset.role || "",
|
||
player: getRowPlayer(row)
|
||
}));
|
||
}
|
||
|
||
function restoreOriginalLineupRows() {
|
||
if (!Array.isArray(originalLineupState) || !originalLineupState.length) return;
|
||
|
||
const rows = Array.from(document.querySelectorAll("tr.lineup-row"));
|
||
originalLineupState.forEach((item) => {
|
||
const row = rows[item.index];
|
||
if (!row) return;
|
||
setRowPlayer(row, item.player);
|
||
renderLineupRow(row);
|
||
row.classList.remove("lineup-row-sub-in", "lineup-row-sub-out", "bench-pick-active");
|
||
});
|
||
sortLineupRows("home", "starting");
|
||
sortLineupRows("home", "bench");
|
||
sortLineupRows("away", "starting");
|
||
sortLineupRows("away", "bench");
|
||
}
|
||
|
||
const formationState = {
|
||
home: {{ home_formations | tojson }},
|
||
away: {{ away_formations | tojson }},
|
||
};
|
||
|
||
const fallbackLineups = {
|
||
home: {{ lineups.home_starting | tojson }},
|
||
away: {{ lineups.away_starting | tojson }},
|
||
};
|
||
|
||
function ensureInitialState(side) {
|
||
if (!formationState[side] || formationState[side].length === 0) {
|
||
formationState[side] = fallbackLineups[side].slice(0, 11).map((p, idx) => ({
|
||
player_id: p.player_id || null,
|
||
player_name: p.player_name || "",
|
||
last_name: p.last_name || "",
|
||
number: p.number || "",
|
||
position: p.position || "",
|
||
is_captain: !!p.is_captain,
|
||
photo: p.photo || "",
|
||
x: 50,
|
||
y: 12 + idx * 6,
|
||
}));
|
||
}
|
||
}
|
||
|
||
function extractLastName(player) {
|
||
return String(player.last_name || "").trim().toUpperCase();
|
||
}
|
||
|
||
function buildPlayerSuffix(player) {
|
||
const pos = String(player.position || "").trim().toLowerCase();
|
||
const isGoalkeeper = ["вр.", "вр", "вратарь", "gk", "goalkeeper"].includes(pos);
|
||
const isCaptain = !!player.is_captain;
|
||
|
||
if (isGoalkeeper && isCaptain) return " (ВР, К)";
|
||
if (isGoalkeeper) return " (ВР)";
|
||
if (isCaptain) return " (К)";
|
||
return "";
|
||
}
|
||
|
||
function buildPlayerLabel(player) {
|
||
const number = player.number || "";
|
||
const lastName = extractLastName(player);
|
||
return `${number} ${lastName}${buildPlayerSuffix(player)}`.trim();
|
||
}
|
||
|
||
function createPlayerNode(player, side) {
|
||
const el = document.createElement("div");
|
||
el.className = "player-node";
|
||
el.dataset.playerId = player.player_id ?? "";
|
||
el.dataset.side = side;
|
||
el.style.left = `${player.x}%`;
|
||
el.style.top = `${player.y}%`;
|
||
|
||
const visual = player.photo
|
||
? `<img src="${player.photo}" alt="${player.player_name || ""}" class="player-photo">`
|
||
: `<div class="player-silhouette"></div>`;
|
||
|
||
el.innerHTML = `
|
||
${visual}
|
||
<div class="player-label">
|
||
<span class="player-label-text">${buildPlayerLabel(player)}</span>
|
||
</div>
|
||
`;
|
||
|
||
makeDraggable(el);
|
||
return el;
|
||
}
|
||
|
||
function renderFormation(side) {
|
||
ensureInitialState(side);
|
||
const pitch = document.getElementById(`pitch-${side}`);
|
||
if (!pitch) return;
|
||
|
||
const overlays = pitch.querySelectorAll(".player-node");
|
||
overlays.forEach(node => node.remove());
|
||
|
||
formationState[side].forEach(player => {
|
||
pitch.appendChild(createPlayerNode(player, side));
|
||
});
|
||
}
|
||
|
||
function makeDraggable(el) {
|
||
let dragging = false;
|
||
|
||
el.addEventListener("mousedown", (e) => {
|
||
dragging = true;
|
||
el.classList.add("dragging");
|
||
e.preventDefault();
|
||
});
|
||
|
||
document.addEventListener("mouseup", () => {
|
||
if (dragging) {
|
||
dragging = false;
|
||
el.classList.remove("dragging");
|
||
}
|
||
});
|
||
|
||
document.addEventListener("mousemove", (e) => {
|
||
if (!dragging) return;
|
||
|
||
const pitch = el.parentElement;
|
||
if (!pitch) return;
|
||
|
||
const rect = pitch.getBoundingClientRect();
|
||
|
||
let x = ((e.clientX - rect.left) / rect.width) * 100;
|
||
let y = ((e.clientY - rect.top) / rect.height) * 100;
|
||
|
||
x = Math.max(8, Math.min(92, x));
|
||
y = Math.max(8, Math.min(92, y));
|
||
|
||
el.style.left = `${x}%`;
|
||
el.style.top = `${y}%`;
|
||
|
||
const side = el.dataset.side;
|
||
const playerId = el.dataset.playerId;
|
||
|
||
const item = formationState[side].find(
|
||
p => String(p.player_id ?? "") === String(playerId)
|
||
);
|
||
|
||
if (item) {
|
||
item.x = +x.toFixed(2);
|
||
item.y = +y.toFixed(2);
|
||
}
|
||
});
|
||
}
|
||
|
||
async function applyPreset(side) {
|
||
const preset = document.getElementById(`${side}-preset`).value;
|
||
|
||
const response = await fetch(`${window.location.pathname}/formations/apply`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ side, preset })
|
||
});
|
||
|
||
const data = await response.json();
|
||
formationState[side] = data.players || [];
|
||
renderFormation(side);
|
||
}
|
||
|
||
async function saveFormation(side) {
|
||
const response = await fetch(`${window.location.pathname}/formations/save`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
side,
|
||
players: formationState[side]
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
alert("Расстановка сохранена");
|
||
} else {
|
||
alert("Ошибка сохранения");
|
||
}
|
||
}
|
||
|
||
window.addEventListener("beforeunload", saveMatchState);
|
||
|
||
renderFormation("home");
|
||
renderFormation("away");
|
||
sortLineupRows("home", "starting");
|
||
sortLineupRows("home", "bench");
|
||
sortLineupRows("away", "starting");
|
||
sortLineupRows("away", "bench");
|
||
if (!originalLineupState) {
|
||
originalLineupState = captureCurrentLineupState();
|
||
saveMatchState();
|
||
}
|
||
loadEventsFromServer();
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|