Files

2719 lines
77 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title data-i18n="tournament.tournament_management">Tournament Management</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/css/base.css">
<link rel="stylesheet" href="/static/css/navbar.css">
<link rel="stylesheet" href="/static/css/buttons.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/responsive.css">
<style>
/* Tournament-specific styles */
/* Make page fit viewport */
html, body {
height: 100%;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
}
.container {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 8px !important;
}
/* Specific tournament styles */
.section {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 18px 20px;
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
/* Default layout needs hidden overflow so inner panels scroll, not the section */
.section:has(.default-layout) {
overflow: hidden;
}
.section-title {
font-size: 1.2rem;
font-weight: bold;
color: #333;
margin-bottom: 12px;
border-bottom: 3px solid #28a745;
padding-bottom: 6px;
}
/* League Status */
.league-status {
text-align: center;
padding: 30px;
}
.league-active {
color: #28a745;
font-size: 1.4rem;
font-weight: bold;
margin-bottom: 20px;
}
.league-inactive {
color: #6c757d;
font-size: 0.9rem;
margin-bottom: 8px;
}
.league-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 15px;
}
/* Unified League & Tournament Info Layout */
.league-tournament-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 25px;
margin: 20px 0;
flex: 1;
min-height: 0;
}
.league-tournament-container.single-card {
grid-template-columns: 1fr;
}
.league-tournament-container.no-stretch {
flex: 0 0 auto;
}
.league-tournament-card {
background: white;
border: 2px solid #dee2e6;
border-radius: 12px;
padding: 20px;
display: flex;
flex-direction: column;
}
.league-tournament-card h3 {
font-size: 1.1rem;
font-weight: bold;
color: #333;
margin: 0 0 15px 0;
padding-bottom: 10px;
border-bottom: 3px solid #28a745;
}
.league-tournament-card.league h3 {
border-bottom-color: #28a745;
}
.league-tournament-card.tournament h3 {
border-bottom-color: #28a745;
}
.compact-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.compact-info-item {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
border: 1px solid #e9ecef;
}
.compact-info-label {
font-size: 0.85rem;
color: #666;
margin-bottom: 4px;
}
.compact-info-value {
font-size: 1.2rem;
font-weight: bold;
color: #333;
}
.unified-status {
margin-bottom: 15px;
padding: 10px;
border-radius: 6px;
text-align: center;
font-weight: bold;
font-size: 0.95rem;
}
.unified-status.league {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.unified-status.tournament {
background: #cfe2ff;
color: #084298;
border: 1px solid #b6d4fe;
}
/* League Progress Bar */
.league-progress-bar {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border: 2px solid #dee2e6;
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
width: 100%;
box-sizing: border-box;
}
.progress-title {
font-size: 1.1rem;
font-weight: bold;
color: #333;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.tournaments-progress-horizontal {
display: flex;
gap: 16px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
position: relative;
}
.tournament-progress-dot {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border-radius: 12px;
border: 2.5px solid #dee2e6;
background: white;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.tournament-progress-dot:hover {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.tournament-progress-dot::after {
content: '→';
position: absolute;
right: -24px;
top: 50%;
transform: translateY(-50%);
font-size: 1.5rem;
color: #dee2e6;
font-weight: bold;
}
.tournament-progress-dot:last-child::after {
display: none;
}
.dot-number {
font-size: 1.6rem;
font-weight: bold;
color: #495057;
margin-bottom: 2px;
}
.dot-status {
font-size: 1.3rem;
line-height: 1;
}
/* Pending - Gray */
.tournament-progress-dot.pending {
background: #f8f9fa;
border-color: #dee2e6;
}
.tournament-progress-dot.pending .dot-number {
color: #adb5bd;
}
/* Active - Blue with animation */
.tournament-progress-dot.active {
background: linear-gradient(135deg, #cfe2ff 0%, #e7f1ff 100%);
border-color: #1e7e34;
border-width: 3px;
box-shadow: 0 4px 16px rgba(0, 123, 255, 0.3);
}
.tournament-progress-dot.active .dot-number {
color: #1e7e34;
animation: pulse-dot 1.5s infinite;
}
.tournament-progress-dot.active::after {
color: #28a745;
animation: pulse-arrow 1.5s infinite;
}
@keyframes pulse-dot {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.08); }
}
@keyframes pulse-arrow {
0%, 100% { transform: translateY(-50%) translateX(0); opacity: 0.6; }
50% { transform: translateY(-50%) translateX(4px); opacity: 1; }
}
/* Completed - Green */
.tournament-progress-dot.completed {
background: linear-gradient(135deg, #d4edda 0%, #e8f5e8 100%);
border-color: #1e7e34;
}
.tournament-progress-dot.completed .dot-number {
color: #1e7e34;
}
.tournament-progress-dot.completed::after {
color: #28a745;
}
/* Round Progress Timeline */
.rounds-progress-bar {
margin-top: 20px;
padding-top: 20px;
border-top: 2px solid #dee2e6;
display: flex;
flex-direction: column;
justify-content: center;
}
.rounds-progress-title {
font-size: 0.95rem;
font-weight: 600;
color: #495057;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.rounds-progress-horizontal {
display: flex;
gap: 12px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
position: relative;
}
.round-progress-dot {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 10px;
border: 2px solid #dee2e6;
background: white;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
font-size: 0.85rem;
font-weight: bold;
}
.round-progress-dot:hover {
cursor: default;
}
.round-progress-dot::after {
content: '→';
position: absolute;
right: -18px;
top: 50%;
transform: translateY(-50%);
font-size: 1.2rem;
color: #dee2e6;
font-weight: bold;
}
.round-progress-dot:last-child::after {
display: none;
}
/* Round Pending - Gray */
.round-progress-dot.pending {
background: #f8f9fa;
border-color: #dee2e6;
color: #adb5bd;
}
/* Round Active - Blue */
.round-progress-dot.active {
background: linear-gradient(135deg, #cfe2ff 0%, #e7f1ff 100%);
border-color: #1e7e34;
border-width: 2px;
color: #1e7e34;
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
.round-progress-dot.active::after {
color: #28a745;
}
/* Round Completed - Green */
.round-progress-dot.completed {
background: linear-gradient(135deg, #d4edda 0%, #e8f5e8 100%);
border-color: #1e7e34;
color: #1e7e34;
}
.round-progress-dot.completed::after {
color: #28a745;
}
/* Tournament Cards Layout */
.tournaments-progress-container {
margin-top: 30px;
}
.tournaments-grid {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
margin-top: 20px;
max-width: 600px;
}
.tournament-card {
background: white;
border: 2px solid #dee2e6;
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.tournament-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: #dee2e6;
transition: all 0.3s ease;
}
.tournament-card.pending {
opacity: 0.7;
background: #f8f9fa;
}
.tournament-card.pending::before {
background: linear-gradient(90deg, #6c757d 0%, #95a3b3 100%);
}
.tournament-card.active {
border-color: #28a745;
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
}
.tournament-card.active::before {
background: linear-gradient(90deg, #28a745 0%, #1e7e34 100%);
}
.tournament-card.completed {
opacity: 0.8;
}
.tournament-card.completed::before {
background: linear-gradient(90deg, #28a745 0%, #20c997 100%);
}
.tournament-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
}
.tournament-card-number {
font-size: 1.4rem;
font-weight: bold;
color: #28a745;
background: #e7f1ff;
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.tournament-card.pending .tournament-card-number {
background: #e9ecef;
color: #6c757d;
}
.tournament-card.completed .tournament-card-number {
background: #d4edda;
color: #28a745;
}
.tournament-card-status {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: bold;
text-align: center;
}
.tournament-card.pending .tournament-card-status {
background: #e9ecef;
color: #6c757d;
}
.tournament-card.active .tournament-card-status {
background: #cfe2ff;
color: #084298;
animation: pulse 2s infinite;
}
.tournament-card.completed .tournament-card-status {
background: #d4edda;
color: #155724;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.tournament-card-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 15px;
}
.tournament-card-info-item {
background: #f8f9fa;
padding: 10px;
border-radius: 6px;
text-align: center;
}
.tournament-card-info-label {
font-size: 0.8rem;
color: #666;
margin-bottom: 4px;
}
.tournament-card-info-value {
font-size: 1.1rem;
font-weight: bold;
color: #333;
}
.tournament-card-actions {
display: flex;
gap: 10px;
flex-direction: column;
}
.tournament-card-btn {
padding: 10px 15px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 0.9rem;
transition: all 0.2s ease;
text-decoration: none;
text-align: center;
}
.tournament-card-btn.start {
background: #28a745;
color: white;
}
.tournament-card-btn.start:hover {
background: #218838;
transform: translateY(-2px);
}
.tournament-card-btn.disabled {
background: #e9ecef;
color: #6c757d;
cursor: not-allowed;
}
.tournament-card-btn.disabled:hover {
transform: none;
}
.tournament-card-check {
font-size: 2rem;
color: #28a745;
text-align: center;
margin: 10px 0;
}
.info-item {
background: white;
padding: 15px;
border-radius: 6px;
border: 1px solid #e9ecef;
text-align: center;
}
.info-label {
font-size: 0.9rem;
color: #666;
margin-bottom: 5px;
}
.info-value {
font-size: 1.3rem;
font-weight: bold;
color: #333;
}
/* Default page two-column layout */
.default-layout {
display: grid;
grid-template-columns: 340px 1fr;
gap: 0;
flex: 1;
overflow: hidden;
min-height: 0;
}
.default-left {
border-right: 2px solid #e9ecef;
padding-right: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.default-right {
padding-left: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
/* Stack vertically on smaller screens */
@media (max-width: 900px) {
.default-layout {
grid-template-columns: 1fr;
overflow-y: auto;
}
.default-left {
border-right: none;
border-bottom: 2px solid #e9ecef;
padding-right: 0;
padding-bottom: 16px;
margin-bottom: 16px;
overflow: visible;
}
.default-right {
padding-left: 0;
}
}
/* Vertical type options for the narrow left column */
.type-options-vertical {
grid-template-columns: 1fr !important;
}
/* Tournament Type Selection */
.tournament-type-selection {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 12px;
margin: 10px 0;
}
.type-title {
font-size: 0.95rem;
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
.type-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 8px;
}
.type-option {
background: white;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 10px 14px;
cursor: pointer;
transition: all 0.3s ease;
text-align: left;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
gap: 10px;
}
.type-option::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
border-radius: 12px 12px 0 0;
background: #dee2e6;
transition: all 0.3s ease;
}
/* 4 Targets - Green theme */
.type-option[data-type="4_targets"] {
border-color: #11998e;
}
.type-option[data-type="4_targets"]:hover {
border-color: #0f766e;
box-shadow: 0 4px 15px rgba(17, 153, 142, 0.25);
transform: translateY(-2px);
}
.type-option[data-type="4_targets"].selected {
border-color: #0f766e;
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
}
.type-option[data-type="4_targets"]::before {
background: linear-gradient(90deg, #11998e 0%, #38ef7d 100%);
}
/* 20 Targets - Red theme */
.type-option[data-type="20_targets"] {
border-color: #ff6b6b;
}
.type-option[data-type="20_targets"]:hover {
border-color: #e53e3e;
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.25);
transform: translateY(-2px);
}
.type-option[data-type="20_targets"].selected {
border-color: #e53e3e;
background: linear-gradient(135deg, #fff5f5 0%, #ffe8e8 100%);
}
.type-option[data-type="20_targets"]::before {
background: linear-gradient(90deg, #ff6b6b 0%, #ff8e53 100%);
}
/* 40 Targets - Blue theme */
.type-option[data-type="40_targets"] {
border-color: #667eea;
}
.type-option[data-type="40_targets"]:hover {
border-color: #5a67d8;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.25);
transform: translateY(-2px);
}
.type-option[data-type="40_targets"].selected {
border-color: #5a67d8;
background: linear-gradient(135deg, #f8f9ff 0%, #e8ebff 100%);
}
.type-option[data-type="40_targets"]::before {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
}
.type-option input[type="radio"] {
margin-right: 10px;
width: 18px;
height: 18px;
}
.type-option-text {
flex: 1;
min-width: 0;
}
.type-name {
font-size: 1rem;
font-weight: bold;
color: #333;
margin-bottom: 2px;
}
.type-description {
font-size: 0.78rem;
color: #666;
line-height: 1.3;
}
/* Joker Management */
.joker-section {
background: #fff3cd;
border: 2px solid #ffeaa7;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
display: flex;
flex-direction: column;
}
.league-tournament-card.joker {
background: #fff3cd;
border-color: #ffeaa7;
display: flex;
flex-direction: column;
}
.joker-players-card {
background: white;
border: 1px solid #ffd54f;
border-radius: 8px;
padding: 8px;
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.joker-title {
font-size: 1.1rem;
font-weight: bold;
color: #856404;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.joker-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.joker-list {
display: flex;
flex-direction: column;
gap: 6px;
overflow-y: auto;
max-height: 510px;
flex: 1;
}
.joker-player {
background: white;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 10px 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
.joker-player.used {
opacity: 0.6;
background: #f8f9fa;
}
.joker-checkbox {
width: 18px;
height: 18px;
}
.joker-checkbox:disabled {
cursor: not-allowed;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
margin: 10px 0;
}
.action-btn {
background: #28a745;
border: none;
color: white;
padding: 10px 18px;
border-radius: 8px;
cursor: pointer;
font-size: 0.85rem;
font-weight: bold;
transition: all 0.2s ease;
min-width: 0;
text-decoration: none !important;
font-family: Arial, sans-serif;
display: inline-block;
}
.action-btn:hover {
background: #1e7e34;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
text-decoration: none !important;
}
.action-btn:focus,
.action-btn:active,
.action-btn:visited {
text-decoration: none !important;
}
.action-btn:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.action-btn.success {
background: #28a745;
}
.action-btn.success:hover {
background: #1e7e34;
}
.action-btn.danger {
background: #dc3545;
}
.action-btn.danger:hover {
background: #c82333;
}
.action-btn.info {
background: #007bff;
border-color: #0056b3;
}
.action-btn.info:hover {
background: #0056b3;
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.4);
}
.player-count {
margin: 10px 0;
font-size: 0.95rem;
color: #333;
text-align: center;
}
.enabled-count {
color: #28a745;
font-weight: bold;
font-size: 1.1rem;
}
.warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 12px 16px;
border-radius: 6px;
margin: 15px 0;
font-weight: 500;
}
/* Tournament Status */
.tournament-status {
text-align: center;
padding: 30px;
}
.tournament-active {
color: #28a745;
font-size: 1.4rem;
font-weight: bold;
margin-bottom: 20px;
}
.tournament-inactive {
color: #6c757d;
font-size: 1.2rem;
margin-bottom: 20px;
}
.tournament-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
/* Player Management Toolbar */
.player-toolbar {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
}
.search-container {
flex: 1;
min-width: 140px;
position: relative;
}
.search-input {
width: 100%;
padding: 7px 32px 7px 10px;
border: 1.5px solid #e9ecef;
border-radius: 6px;
font-size: 0.88rem;
transition: border-color 0.2s ease;
box-sizing: border-box;
}
.search-input:focus {
outline: none;
border-color: #28a745;
}
.search-icon {
position: absolute;
right: 9px;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
font-size: 0.9rem;
}
.filter-btn {
padding: 6px 10px;
border: 1.5px solid #e9ecef;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 500;
transition: all 0.2s ease;
white-space: nowrap;
}
.filter-btn:hover { border-color: #28a745; background: #f8f9fa; }
.filter-btn.active { background: #28a745; color: white; border-color: #28a745; }
/* Add player row */
.add-player-row {
display: flex;
gap: 6px;
margin-bottom: 8px;
}
.add-player-row input {
flex: 1;
padding: 7px 10px;
border: 1.5px solid #ced4da;
border-radius: 6px;
font-size: 0.88rem;
}
.add-player-row input:focus {
outline: none;
border-color: #28a745;
}
.add-btn {
background: #28a745;
border: none;
color: white;
padding: 7px 14px;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 0.85rem;
white-space: nowrap;
}
.add-btn:hover { background: #218838; }
.add-btn:disabled { background: #6c757d; cursor: not-allowed; }
/* Stats bar */
.stats-bar {
display: flex;
gap: 14px;
align-items: center;
margin-bottom: 8px;
font-size: 0.82rem;
color: #555;
}
.stats-bar strong { font-size: 0.95rem; }
.stat-enabled { color: #28a745; }
.stat-disabled { color: #dc3545; }
/* ── PLAYER LIST ── */
.player-list {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.player-row {
display: flex;
align-items: center;
gap: 0;
padding: 0 4px;
border-bottom: 1px solid #f0f0f0;
min-height: 36px;
transition: background 0.1s;
cursor: pointer;
user-select: none;
}
.player-row:last-child { border-bottom: none; }
.player-row:hover { background: #f8f9fa; }
.player-row.selected { background: #eaf4ff; }
.player-row.disabled-player { opacity: 0.5; }
/* status dot — clicking toggles enabled */
.pr-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: #dee2e6;
flex-shrink: 0;
margin-right: 10px;
margin-left: 4px;
transition: background 0.15s;
cursor: pointer;
}
.player-row:not(.disabled-player) .pr-dot { background: #28a745; }
.pr-check {
flex-shrink: 0;
margin-right: 8px;
cursor: pointer;
}
.pr-name {
flex: 1;
font-size: 0.88rem;
font-weight: 500;
color: #222;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pr-name input {
width: 100%;
padding: 2px 6px;
border: 1.5px solid #28a745;
border-radius: 4px;
font-size: 0.88rem;
font-weight: 500;
}
/* action icons — always visible but subtle */
.pr-actions {
display: flex;
gap: 2px;
flex-shrink: 0;
opacity: 0.35;
transition: opacity 0.15s;
}
.player-row:hover .pr-actions { opacity: 1; }
.pr-btn {
padding: 3px 6px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.72rem;
background: transparent;
color: #555;
transition: background 0.12s;
}
.pr-btn:hover { background: #e9ecef; }
.pr-btn.del:hover { background: #f8d7da; color: #721c24; }
/* Bulk action strip */
.bulk-strip {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
flex-wrap: wrap;
}
.bulk-btn {
padding: 4px 10px;
border: 1.5px solid #e9ecef;
background: white;
border-radius: 5px;
cursor: pointer;
font-size: 0.78rem;
font-weight: 500;
transition: all 0.15s;
white-space: nowrap;
}
.bulk-btn:hover { border-color: #28a745; background: #f8fff9; }
.bulk-btn.danger:hover { border-color: #dc3545; background: #fff5f5; }
/* legacy stat-item kept for other pages */
.stat-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
}
.stat-number {
font-weight: bold;
font-size: 1.1rem;
}
.stat-enabled {
color: #28a745;
}
.stat-disabled {
color: #dc3545;
}
.stat-total {
color: #28a745;
}
.no-results {
text-align: center;
padding: 40px 20px;
color: #6c757d;
font-style: italic;
}
/* Confirmation Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.confirmation-modal {
background: white;
border-radius: 8px;
padding: 25px;
max-width: 500px;
width: 90%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transform: scale(0.9);
transition: transform 0.2s ease;
}
.modal-overlay.active .confirmation-modal {
transform: scale(1);
}
.modal-title {
font-size: 1.2rem;
font-weight: bold;
color: #333;
margin-bottom: 12px;
}
.modal-message {
color: #666;
margin-bottom: 20px;
line-height: 1.4;
}
.modal-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.modal-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s ease;
}
.modal-btn.cancel {
background: #6c757d;
color: white;
}
.modal-btn.cancel:hover {
background: #5a6268;
}
.modal-btn.confirm {
background: #dc3545;
color: white;
}
.modal-btn.confirm:hover {
background: #c82333;
}
/* Responsive unified layout */
@media (max-width: 1024px) {
.league-tournament-container {
grid-template-columns: 1fr;
gap: 20px;
}
}
/* Mobile responsive */
@media (max-width: 768px) {
.navbar {
padding: 12px 20px;
flex-direction: column;
gap: 15px;
}
.navbar-controls {
flex-wrap: wrap;
justify-content: center;
}
.container {
padding: 20px 15px;
}
.player-toolbar {
flex-wrap: wrap;
}
.add-player-row {
flex-wrap: wrap;
}
.tournament-info, .league-info {
grid-template-columns: 1fr;
}
.type-options {
grid-template-columns: 1fr;
}
.action-buttons {
flex-direction: column;
align-items: center;
}
.action-btn {
min-width: 0;
}
}
/* Responsive Progress Bar */
@media (max-width: 1200px) {
.tournament-progress-dot {
width: 70px;
height: 70px;
}
.dot-number {
font-size: 1.3rem;
}
.dot-status {
font-size: 1.1rem;
}
.tournament-progress-dot::after {
font-size: 1.3rem;
right: -20px;
}
.tournaments-progress-horizontal {
gap: 14px;
}
}
@media (max-width: 768px) {
.league-progress-bar {
padding: 16px;
margin-bottom: 20px;
}
.progress-title {
font-size: 1rem;
margin-bottom: 12px;
}
.tournament-progress-dot {
width: 65px;
height: 65px;
}
.dot-number {
font-size: 1.2rem;
}
.dot-status {
font-size: 1rem;
}
.tournament-progress-dot::after {
font-size: 1.2rem;
right: -18px;
}
.tournaments-progress-horizontal {
gap: 12px;
}
}
@media (max-width: 600px) {
.league-progress-bar {
padding: 12px;
margin-bottom: 16px;
}
.progress-title {
font-size: 0.95rem;
margin-bottom: 10px;
}
.tournament-progress-dot {
width: 60px;
height: 60px;
}
.dot-number {
font-size: 1.1rem;
margin-bottom: 1px;
}
.dot-status {
font-size: 0.9rem;
}
.tournament-progress-dot::after {
font-size: 1rem;
right: -16px;
}
.tournaments-progress-horizontal {
gap: 10px;
}
}
/* Config modal */
.config-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1100;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
}
.config-modal-overlay.active {
opacity: 1;
visibility: visible;
}
.config-modal {
background: white;
border-radius: 12px;
padding: 28px;
width: 340px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
transform: scale(0.9);
transition: transform 0.2s ease;
}
.config-modal-overlay.active .config-modal {
transform: scale(1);
}
.config-modal h3 {
margin: 0 0 18px 0;
font-size: 1.1rem;
color: #333;
border-bottom: 3px solid #fd7e14;
padding-bottom: 8px;
}
.config-row {
margin-bottom: 16px;
}
.config-row label {
display: block;
font-size: 0.88rem;
font-weight: 600;
color: #555;
margin-bottom: 6px;
}
.config-row input[type="number"] {
width: 100%;
padding: 9px 12px;
border: 1.5px solid #dee2e6;
border-radius: 6px;
font-size: 1rem;
box-sizing: border-box;
}
.config-row input[type="number"]:focus {
outline: none;
border-color: #fd7e14;
}
.config-preview {
background: #fff8f3;
border: 1.5px solid #ffd5b0;
border-radius: 6px;
padding: 10px 14px;
font-size: 0.85rem;
color: #7a3a00;
margin-bottom: 18px;
}
.config-modal-btns {
display: flex;
gap: 10px;
justify-content: flex-end;
}
</style>
<script src="/static/js/i18n.js"></script>
</head>
<body>
<div class="navbar">
<div class="navbar-title">🏆 <span data-i18n="tournament.tournament_management">Tournament Management</span></div>
<div class="navbar-controls">
<a href="/tournament" class="nav-btn active">🏆 <span data-i18n="navigation.tournament">Tournament</span></a>
{% if tournament_state %}
<a href="/tournament/draft" class="nav-btn">📋 Razpored</a>
<a href="/results/calculator" class="nav-btn">🎯 <span data-i18n="navigation.calculator">Results Calculator</span></a>
{% endif %}
<a href="/" class="nav-btn"></a>
</div>
</div>
<div class="container">
<!-- League/Tournament Management Section -->
<div class="section">
{% if league_state and not league_state.league_finished %}
<h2 class="section-title" data-i18n="league.league_management">🏆 Upravljanje Lige</h2>
<!-- League Info Card -->
<div class="league-tournament-container {% if not tournament_state and league_state.current_tournament >= league_state.total_tournaments %}single-card no-stretch{% endif %}">
<!-- League Card -->
<div class="league-tournament-card league">
<h3>🏆 <span data-i18n="league.league_active">Liga (Aktivna)</span></h3>
<div class="unified-status league">
<span data-i18n="league.league_active">Liga je Aktivna</span>
</div>
<div class="compact-info-grid">
<div class="compact-info-item">
<div class="compact-info-label" data-i18n="tournament.tournament_type">Tip</div>
<div class="compact-info-value">
{% if league_state.tournament_type == '40_targets' %}
💪 40
{% elif league_state.tournament_type == '4_targets' %}
🎯 4
{% else %}
⚡ 20
{% endif %}
</div>
</div>
<div class="compact-info-item">
<div class="compact-info-label" data-i18n="tournament.participants">Igralci</div>
<div class="compact-info-value">{{ league_state.participants|length }}</div>
</div>
<div class="compact-info-item">
<div class="compact-info-label" data-i18n="tournament.created">Začeto</div>
<div class="compact-info-value">{{ league_state.created_at[:10] }}</div>
</div>
</div>
<!-- Tournament Progress Timeline -->
<div class="rounds-progress-bar">
<div class="rounds-progress-title">
📊 <span data-i18n="league.tournaments">Turnirji</span>
</div>
<div class="rounds-progress-horizontal">
{% for i in range(1, league_state.total_tournaments + 1) %}
{% set is_completed = i <= league_state.completed_tournaments|length %}
{% set is_active = (i == league_state.current_tournament and tournament_state) or (i == league_state.current_tournament and not tournament_state and not is_completed) %}
<div class="round-progress-dot {% if is_completed %}completed{% elif is_active %}active{% else %}pending{% endif %}" title="Tournament {{ i }}">
{{ i }}
</div>
{% endfor %}
</div>
</div>
{% if not tournament_state and league_state.current_tournament < league_state.total_tournaments %}
<div class="action-buttons" style="margin-top: auto; padding-top: 16px;">
{% if league_state.completed_tournaments|length > 0 %}
<a href="/results" class="action-btn info">
📊 <span data-i18n="tournament.view_current_results">View Current Standings</span>
</a>
{% endif %}
<button class="action-btn success" onclick="startNextTournament()">
🚀 <span data-i18n="league.start_tournament_number">Začni Turnir</span> {{ league_state.current_tournament + 1 }}
</button>
<button class="action-btn danger" onclick="stopAndDeleteLeague()">
🛑 <span data-i18n="league.stop_league">Ustavi Ligo</span>
</button>
</div>
{% endif %}
</div>
<!-- Joker Card (shown when no tournament is active) -->
{% if not tournament_state and league_state.current_tournament < league_state.total_tournaments %}
<div class="league-tournament-card joker" id="jokerSection">
<div class="joker-title">🃏 <span data-i18n="league.joker_selection_for_tournament">Izbira Jokerja za Turnir</span> {{ league_state.current_tournament + 1 }}</div>
<p style="margin-bottom: 12px; color: #856404; font-size: 0.85rem;" data-i18n="league.joker_instructions">Izberite igralce, ki bodo uporabili svojega Jokerja (preskočili ta turnir). Vsak igralec lahko uporabi svojega Žolna samo enkrat na ligo.</p>
<div class="joker-players-card">
<div class="joker-list">
{% for player_id, participant in league_state.participants.items() %}
<div class="joker-player {% if participant.joker_used %}used{% endif %}">
<span>{{ participant.name }}</span>
<input type="checkbox"
class="joker-checkbox"
id="joker_{{ player_id }}"
{% if participant.joker_used %}disabled checked{% endif %}
data-player-id="{{ player_id }}">
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- Tournament Card (only shown when tournament is active) -->
{% if tournament_state %}
<div class="league-tournament-card tournament">
<h3>🎯 <span data-i18n="tournament.active_tournament">Trenutni Turnir</span></h3>
<div class="unified-status tournament">
<span data-i18n="tournament.active_tournament">Turnir je Aktiven</span>
</div>
<div class="compact-info-grid">
<div class="compact-info-item">
<div class="compact-info-label" data-i18n="players.total_players">Igralci</div>
<div class="compact-info-value">{{ tournament_state.total_players }}</div>
</div>
<div class="compact-info-item">
<div class="compact-info-label" data-i18n="tournament.total_rounds">Skupaj Krogov</div>
<div class="compact-info-value">{{ tournament_state.total_rounds }}</div>
</div>
<div class="compact-info-item">
<div class="compact-info-label" data-i18n="tournament.current_round">Trenutni Krog</div>
<div class="compact-info-value">{{ tournament_state.current_round }}</div>
</div>
</div>
<!-- Round Progress Timeline -->
<div class="rounds-progress-bar">
<div class="rounds-progress-title">
📊 <span data-i18n="tournament.round">Krogovi</span> - {{ tournament_state.current_round }}/{{ tournament_state.total_rounds }}
</div>
<div class="rounds-progress-horizontal">
{% for i in range(1, tournament_state.total_rounds + 1) %}
{% set is_completed = i < tournament_state.current_round %}
{% set is_active = i == tournament_state.current_round %}
<div class="round-progress-dot {% if is_completed %}completed{% elif is_active %}active{% else %}pending{% endif %}" title="Round {{ i }}">
{{ i }}
</div>
{% endfor %}
</div>
</div>
<div class="action-buttons" style="margin-top: auto; padding-top: 12px;">
<button class="action-btn danger" onclick="cancelLeagueTournament()">🗑️ Ponastavi tekmo</button>
</div>
</div>
{% endif %}
</div>
{% if not tournament_state and league_state.current_tournament >= league_state.total_tournaments %}
<div class="warning">
<strong data-i18n="league.league_complete">League Complete!</strong> <span data-i18n="league.league_complete_info">All tournaments planned. Finish current for final results.</span>
</div>
{% endif %}
{% elif league_state and league_state.league_finished %}
<h2 class="section-title">🏁 League Completed</h2>
<div class="league-status">
<div class="league-active">🏆 League Completed!</div>
<div class="league-info">
<div class="info-item">
<div class="info-label" data-i18n="tournament.tournament_type">Tip Turnirja</div>
<div class="info-value">
{% if league_state.tournament_type == '40_targets' %}
40 Targets
{% elif league_state.tournament_type == '4_targets' %}
4 Targets
{% else %}
20 Targets
{% endif %}
</div>
</div>
<div class="info-item">
<div class="info-label" data-i18n="tournament.participants">Udeleženci</div>
<div class="info-value">{{ league_state.participants|length }}</div>
</div>
<div class="info-item">
<div class="info-label" data-i18n="tournament.tournaments">Turnirji</div>
<div class="info-value">{{ league_state.total_tournaments }}</div>
</div>
<div class="info-item">
<div class="info-label" data-i18n="tournament.finished">Zaključeno</div>
<div class="info-value">{{ league_state.finished_at[:10] if league_state.finished_at else 'Today' }}</div>
</div>
</div>
<div class="action-buttons">
<a href="/results" class="action-btn success">🏆 <span data-i18n="league.view_league_results">View League Results</span></a>
<button class="action-btn danger" onclick="resetLeague()">🗑️ <span data-i18n="league.reset_league">Reset League</span></button>
</div>
</div>
{% elif not league_state and tournament_state %}
<h2 class="section-title" data-i18n="tournament.tournament_management">🎯 Upravljanje Turnirja</h2>
<div class="league-tournament-container single-card">
<!-- Tournament Card -->
<div class="league-tournament-card tournament">
<h3>🎯 <span data-i18n="tournament.active_tournament">Trenutni Turnir</span></h3>
<div class="unified-status tournament">
<span data-i18n="tournament.active_tournament">Turnir je Aktiven</span>
</div>
<div class="compact-info-grid">
<div class="compact-info-item">
<div class="compact-info-label" data-i18n="tournament.tournament_type">Tip</div>
<div class="compact-info-value">
{% if tournament_state.tournament_type == '40_targets' %}
💪 40
{% elif tournament_state.tournament_type == '4_targets' %}
🎯 4
{% else %}
⚡ 20
{% endif %}
</div>
</div>
<div class="compact-info-item">
<div class="compact-info-label" data-i18n="players.total_players">Igralci</div>
<div class="compact-info-value">{{ tournament_state.total_players }}</div>
</div>
<div class="compact-info-item">
<div class="compact-info-label" data-i18n="tournament.total_rounds">Skupaj Krogov</div>
<div class="compact-info-value">{{ tournament_state.total_rounds }}</div>
</div>
<div class="compact-info-item">
<div class="compact-info-label" data-i18n="tournament.current_round">Trenutni Krog</div>
<div class="compact-info-value">{{ tournament_state.current_round }}</div>
</div>
</div>
<!-- Round Progress Timeline -->
<div class="rounds-progress-bar">
<div class="rounds-progress-title">
📊 <span data-i18n="tournament.round">Kroogi</span> - {{ tournament_state.current_round }}/{{ tournament_state.total_rounds }}
</div>
<div class="rounds-progress-horizontal">
{% for i in range(1, tournament_state.total_rounds + 1) %}
{% set is_completed = i < tournament_state.current_round %}
{% set is_active = i == tournament_state.current_round %}
<div class="round-progress-dot {% if is_completed %}completed{% elif is_active %}active{% else %}pending{% endif %}" title="Round {{ i }}">
{{ i }}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="action-buttons" style="margin-top: 20px;">
<button class="action-btn danger" onclick="resetTournament()">🗑️ <span data-i18n="tournament.reset_tournament">Reset Tournament</span></button>
</div>
{% else %}
<!-- Two-column layout for default state -->
<div class="default-layout">
<!-- LEFT: Tournament Setup -->
<div class="default-left">
<h2 class="section-title" data-i18n="league.setup">🏁 Nastavitev</h2>
<div class="league-inactive" data-i18n="league.no_active_league_tournament">Ni Aktivne Lige ali Turnirja</div>
<!-- Tournament Type Selection -->
<div class="tournament-type-selection">
<div class="type-title" data-i18n="league.select_tournament_type">Izberi Tip Turnirja</div>
<div class="type-options type-options-vertical">
<div class="type-option" data-type="4_targets" onclick="selectTournamentType('4_targets')">
<input type="radio" name="tournament_type" value="4_targets">
<div class="type-option-text">
<div class="type-name">🎯 4 Targets</div>
<div class="type-description" data-i18n="tournament_types.4_targets_desc">Hitri format s 4 tarčami, 5 strelov na tarčo (20 strelov skupaj)</div>
</div>
</div>
<div class="type-option selected" data-type="20_targets" onclick="selectTournamentType('20_targets')">
<input type="radio" name="tournament_type" value="20_targets" checked>
<div class="type-option-text">
<div class="type-name">⚡ 20 Targets</div>
<div class="type-description" data-i18n="tournament_types.20_targets_desc">Standardni format z 20 tarčami, 2 strela na tarčo (40 strelov skupaj)</div>
</div>
</div>
<div class="type-option" data-type="40_targets" onclick="selectTournamentType('40_targets')">
<input type="radio" name="tournament_type" value="40_targets">
<div class="type-option-text">
<div class="type-name">💪 40 Targets</div>
<div class="type-description" data-i18n="tournament_types.40_targets_desc">Razširjeni format s 40 tarčami, 2 strela na tarčo (80 strelov skupaj)</div>
</div>
</div>
</div>
</div>
<div class="player-count">
<span class="enabled-count" id="enabledCount">0</span> players enabled
</div>
<div class="action-buttons">
<button class="action-btn success" id="startLeagueBtn" onclick="startLeague()">🏆 <span data-i18n="league.start_league_5_tournaments">Začni Ligo (5 Turnirjev)</span></button>
<button class="action-btn" id="startSingleBtn" onclick="startSingleTournament()">🏅 <span data-i18n="league.start_single_tournament">Začni Posamezen Turnir</span></button>
</div>
<div class="warning" id="warningMessage" style="display: none;">
<strong>Note:</strong> You need at least 1 enabled player to start.
</div>
</div>
<!-- RIGHT: Player Management -->
<div class="default-right">
<h2 class="section-title" data-i18n="players.player_management">👥 Upravljanje Igralcev</h2>
<!-- Add Player -->
<div class="add-player-row">
<input type="text"
id="newPlayerName"
data-i18n="[placeholder]league.enter_player_name"
placeholder="Vnesite ime igralca..."
maxlength="50"
onkeypress="handleAddPlayerKeypress(event)">
<button class="add-btn" id="addPlayerBtn" onclick="addPlayer()">
<span data-i18n="players.add_player">Dodaj</span>
</button>
</div>
<!-- Toolbar: search + filters -->
<div class="player-toolbar">
<div class="search-container">
<input type="text"
class="search-input"
id="playerSearch"
data-i18n="[placeholder]league.search_players_placeholder"
placeholder="Išči..."
oninput="filterPlayers()">
<span class="search-icon">🔍</span>
</div>
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')" data-i18n="general.all">Vse</button>
<button class="filter-btn" data-filter="enabled" onclick="setFilter('enabled')" data-i18n="players.enabled"></button>
<button class="filter-btn" data-filter="disabled" onclick="setFilter('disabled')" data-i18n="players.disabled"></button>
</div>
<!-- Bulk actions + stats -->
<div class="bulk-strip">
<input type="checkbox" id="selectAllCheckbox" onchange="toggleSelectAll()" title="Select all">
<button class="bulk-btn" onclick="enableSelected()" data-i18n="general.enable_selected">Omogoči</button>
<button class="bulk-btn danger" onclick="disableSelected()" data-i18n="general.disable_selected">Onemogoči</button>
<div class="stats-bar" style="margin:0; flex:1; justify-content:flex-end;">
<span><strong class="stat-enabled" id="enabledPlayersCount">0</strong></span>
<span><strong class="stat-disabled" id="disabledPlayersCount">0</strong></span>
<span><strong id="totalPlayersCount">0</strong> skupaj</span>
</div>
</div>
<!-- Player pill list -->
<div class="player-list" id="playerList">
<!-- Pills rendered by JS -->
</div>
<div class="no-results" id="noResults" style="display: none;">
<span data-i18n="league.no_players_found">Ni najdenih igralcev.</span>
</div>
</div><!-- end default-right -->
</div><!-- end default-layout -->
{% endif %}
</div><!-- end section -->
</div>
<!-- Confirmation Modal -->
<div class="modal-overlay" id="confirmationModal">
<div class="confirmation-modal">
<div class="modal-title" id="modalTitle">Confirm Action</div>
<div class="modal-message" id="modalMessage">Are you sure you want to proceed?</div>
<div class="modal-buttons">
<button class="modal-btn cancel" onclick="closeModal()">Cancel</button>
<button class="modal-btn confirm" id="confirmBtn" onclick="confirmAction()">Confirm</button>
</div>
</div>
</div>
<script>
let players = {{ players|tojson }};
const leagueActive = {{ 'true' if league_state and not league_state.league_finished else 'false' }};
const tournamentActive = {{ 'true' if tournament_state else 'false' }};
let pendingAction = null;
let editingPlayer = null;
let selectedTournamentType = '20_targets';
let currentFilter = 'all';
let searchTerm = '';
let selectedPlayers = new Set();
// Tournament type selection
function selectTournamentType(type) {
selectedTournamentType = type;
document.querySelectorAll('.type-option').forEach(option => {
option.classList.remove('selected');
});
event.currentTarget.classList.add('selected');
const radio = event.currentTarget.querySelector('input[type="radio"]');
if (radio) radio.checked = true;
}
// Filter and search functions
function setFilter(filter) {
currentFilter = filter;
// Update filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-filter="${filter}"]`).classList.add('active');
renderPlayerTable();
}
function filterPlayers() {
searchTerm = document.getElementById('playerSearch').value.toLowerCase();
renderPlayerTable();
}
function getFilteredPlayers() {
let filtered = players.filter(player => {
// Search filter
if (searchTerm && !player.name.toLowerCase().includes(searchTerm)) {
return false;
}
// Status filter
if (currentFilter === 'enabled' && !player.enabled) {
return false;
}
if (currentFilter === 'disabled' && player.enabled) {
return false;
}
return true;
});
// Sort by enabled status first, then by name
filtered.sort((a, b) => {
if (a.enabled && !b.enabled) return -1;
if (!a.enabled && b.enabled) return 1;
return a.name.localeCompare(b.name);
});
return filtered;
}
// Selection functions
function togglePlayerSelection(playerId, checked) {
if (checked) {
selectedPlayers.add(playerId);
} else {
selectedPlayers.delete(playerId);
}
updateSelectAllCheckbox();
}
function toggleSelectAll() {
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const visiblePlayers = getFilteredPlayers();
if (selectAllCheckbox.checked) {
visiblePlayers.forEach(player => selectedPlayers.add(player.id));
} else {
visiblePlayers.forEach(player => selectedPlayers.delete(player.id));
}
renderPlayerTable();
}
function selectAllVisible() {
const visiblePlayers = getFilteredPlayers();
visiblePlayers.forEach(player => selectedPlayers.add(player.id));
renderPlayerTable();
}
function updateSelectAllCheckbox() {
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const visiblePlayers = getFilteredPlayers();
const selectedVisible = visiblePlayers.filter(p => selectedPlayers.has(p.id));
if (selectedVisible.length === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (selectedVisible.length === visiblePlayers.length) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
}
}
// Bulk actions
function enableSelected() {
if (selectedPlayers.size === 0) {
alert(t('league.no_players_selected'));
return;
}
if (!confirm(`Enable ${selectedPlayers.size} selected players?`)) {
return;
}
bulkUpdatePlayers(Array.from(selectedPlayers), true);
}
function disableSelected() {
if (selectedPlayers.size === 0) {
alert(t('league.no_players_selected'));
return;
}
if (!confirm(`Disable ${selectedPlayers.size} selected players?`)) {
return;
}
bulkUpdatePlayers(Array.from(selectedPlayers), false);
}
async function bulkUpdatePlayers(playerIds, enabled) {
try {
for (const playerId of playerIds) {
const response = await fetch(`/api/players/${playerId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ enabled: enabled })
});
if (response.ok) {
const player = players.find(p => p.id === playerId);
if (player) {
player.enabled = enabled;
}
}
}
selectedPlayers.clear();
renderPlayerTable();
updateStats();
} catch (error) {
console.error('Error bulk updating players:', error);
alert('Error updating players. Please try again.');
}
}
// Update stats display
function updateStats() {
const totalCount = players.length;
const enabledCount = players.filter(p => p.enabled).length;
const disabledCount = totalCount - enabledCount;
const visibleCount = getFilteredPlayers().length;
const setEl = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
setEl('totalPlayersCount', totalCount);
setEl('enabledPlayersCount', enabledCount);
setEl('disabledPlayersCount', disabledCount);
setEl('visiblePlayersCount', visibleCount);
setEl('enabledCount', enabledCount);
// Update button states
const startLeagueBtn = document.getElementById('startLeagueBtn');
const startSingleBtn = document.getElementById('startSingleBtn');
const warningMsg = document.getElementById('warningMessage');
if (startLeagueBtn && startSingleBtn && warningMsg) {
if (enabledCount < 1) {
startLeagueBtn.disabled = true;
startSingleBtn.disabled = true;
warningMsg.style.display = 'block';
} else {
startLeagueBtn.disabled = false;
startSingleBtn.disabled = false;
warningMsg.style.display = 'none';
}
}
}
// Render the player list as pills
function renderPlayerTable() {
const list = document.getElementById('playerList');
const noResults = document.getElementById('noResults');
// Check if playerList exists before proceeding (it may not exist on all pages)
if (!list) {
return;
}
const filteredPlayers = getFilteredPlayers();
list.innerHTML = '';
if (filteredPlayers.length === 0) {
if (noResults) noResults.style.display = 'block';
updateSelectAllCheckbox();
updateStats();
return;
}
if (noResults) noResults.style.display = 'none';
filteredPlayers.forEach(player => {
const row = document.createElement('div');
row.className = `player-row${player.enabled ? '' : ' disabled-player'}${selectedPlayers.has(player.id) ? ' selected' : ''}`;
row.setAttribute('data-player-id', player.id);
if (editingPlayer === player.id) {
row.innerHTML = `
<span class="pr-dot"></span>
<span class="pr-name" id="playerName${player.id}">
<input type="text" value="${player.name}" maxlength="50"
onblur="savePlayerName(${player.id})"
onkeydown="handleEditKeypress(event, ${player.id})"
autofocus>
</span>
<div class="pr-actions" style="opacity:1;">
<button class="pr-btn" onclick="editPlayer(${player.id})" title="Shrani">💾</button>
</div>
`;
} else {
row.innerHTML = `
<input type="checkbox" class="pr-check"
${selectedPlayers.has(player.id) ? 'checked' : ''}
onclick="event.stopPropagation()"
onchange="togglePlayerSelection(${player.id}, this.checked)">
<span class="pr-dot" onclick="event.stopPropagation(); togglePlayer(${player.id})" title="${player.enabled ? 'Onemogoči' : 'Omogoči'}"></span>
<span class="pr-name" id="playerName${player.id}">${player.name}</span>
<div class="pr-actions">
<button class="pr-btn" onclick="event.stopPropagation(); editPlayer(${player.id})" title="Uredi">✏️</button>
<button class="pr-btn del" onclick="event.stopPropagation(); confirmDeletePlayer(${player.id})" title="Izbriši">🗑</button>
</div>
`;
// Row click toggles selection
row.addEventListener('click', () => {
const cb = row.querySelector('.pr-check');
cb.checked = !cb.checked;
togglePlayerSelection(player.id, cb.checked);
});
}
list.appendChild(row);
});
updateSelectAllCheckbox();
updateStats();
}
// Add new player
async function addPlayer() {
const nameInput = document.getElementById('newPlayerName');
const name = nameInput.value.trim();
if (!name) {
alert('Please enter a player name');
return;
}
const addBtn = document.getElementById('addPlayerBtn');
addBtn.disabled = true;
addBtn.textContent = 'Adding...';
try {
const response = await fetch('/api/players/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: name })
});
if (response.ok) {
const result = await response.json();
players.push(result.player);
nameInput.value = '';
renderPlayerTable();
} else {
const error = await response.json();
alert('Failed to add player: ' + error.message);
}
} catch (error) {
console.error('Error adding player:', error);
alert('Error adding player. Please try again.');
} finally {
addBtn.disabled = false;
addBtn.innerHTML = '<span data-i18n="players.add_player"> Dodaj Igralca</span>';
translatePage();
}
}
// Handle Enter key in add player input
function handleAddPlayerKeypress(event) {
if (event.key === 'Enter') {
event.preventDefault();
addPlayer();
}
}
// Edit player name
function editPlayer(playerId) {
const player = players.find(p => p.id === playerId);
if (!player) return;
if (editingPlayer === playerId) {
// Save changes
const nameElement = document.getElementById(`playerName${playerId}`);
const input = nameElement ? nameElement.querySelector('input') : null;
if (input) {
const newName = input.value.trim();
if (newName && newName !== player.name) {
updatePlayerName(playerId, newName);
} else {
editingPlayer = null;
renderPlayerTable();
}
}
} else {
// Start editing — re-render the whole list so the pill shows input
editingPlayer = playerId;
renderPlayerTable();
// Focus the input after render
setTimeout(() => {
const nameElement = document.getElementById(`playerName${playerId}`);
const input = nameElement ? nameElement.querySelector('input') : null;
if (input) input.focus();
}, 20);
}
}
function savePlayerName(playerId) {
editPlayer(playerId);
}
function handleEditKeypress(event, playerId) {
if (event.key === 'Enter') {
event.preventDefault();
editPlayer(playerId);
} else if (event.key === 'Escape') {
editingPlayer = null;
renderPlayerTable();
}
}
// Update player name
async function updatePlayerName(playerId, newName) {
try {
const response = await fetch(`/api/players/${playerId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: newName.trim() })
});
if (response.ok) {
// Update local player data
const player = players.find(p => p.id === playerId);
if (player) {
player.name = newName.trim();
}
editingPlayer = null;
renderPlayerTable();
} else {
alert('Failed to update player name');
editingPlayer = null;
renderPlayerTable();
}
} catch (error) {
console.error('Error updating player:', error);
editingPlayer = null;
renderPlayerTable();
}
}
// Update a single player row without re-rendering the entire list
function updatePlayerRow(playerId) {
const player = players.find(p => p.id === playerId);
if (!player) return;
const row = document.querySelector(`.player-row[data-player-id="${playerId}"]`);
if (!row) return;
if (player.enabled) {
row.classList.remove('disabled-player');
} else {
row.classList.add('disabled-player');
}
updateStats();
}
async function togglePlayer(playerId) {
const player = players.find(p => p.id === playerId);
if (!player) return;
const newEnabled = !player.enabled;
try {
const response = await fetch(`/api/players/${playerId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ enabled: newEnabled })
});
if (response.ok) {
player.enabled = newEnabled;
updatePlayerRow(playerId);
} else {
alert('Failed to update player status');
}
} catch (error) {
console.error('Error toggling player:', error);
alert('Error updating player. Please try again.');
}
}
// Confirm delete player
function confirmDeletePlayer(playerId) {
const player = players.find(p => p.id === playerId);
if (!player) return;
showModal(
'Delete Player',
`Are you sure you want to permanently delete "${player.name}"? This action cannot be undone.`,
() => deletePlayer(playerId)
);
}
// Delete player
async function deletePlayer(playerId) {
try {
const response = await fetch(`/api/players/${playerId}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
players = players.filter(p => p.id !== playerId);
selectedPlayers.delete(playerId);
renderPlayerTable();
closeModal();
} else {
alert('Failed to delete player');
}
} catch (error) {
console.error('Error deleting player:', error);
alert('Error deleting player. Please try again.');
}
}
// Start league
async function startLeague() {
const enabledPlayers = players.filter(p => p.enabled);
if (enabledPlayers.length < 1) {
alert('Need at least 1 enabled player to start league');
return;
}
const formatText = selectedTournamentType === '40_targets' ? t('tournament_types.40_targets_full') :
selectedTournamentType === '4_targets' ? t('tournament_types.4_targets_full') :
t('tournament_types.20_targets_full');
if (!confirm(t('messages.confirm_start_league', { players: enabledPlayers.length, format: formatText }))) {
return;
}
const startBtn = document.getElementById('startLeagueBtn');
startBtn.disabled = true;
startBtn.textContent = 'Starting League...';
try {
const response = await fetch('/api/league/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ tournament_type: selectedTournamentType })
});
if (response.ok) {
const result = await response.json();
alert('League started successfully!');
window.location.reload();
} else {
const error = await response.json();
alert('Failed to start league: ' + error.message);
}
} catch (error) {
console.error('Error starting league:', error);
alert('Error starting league. Please try again.');
} finally {
startBtn.disabled = false;
startBtn.setAttribute('data-i18n', 'league.start_league_5_tournaments');
startBtn.textContent = '🎖️ Začni Ligo (5 Turnirjev)';
translatePage();
}
}
// Start single tournament
async function startSingleTournament() {
const enabledPlayers = players.filter(p => p.enabled);
if (enabledPlayers.length < 1) {
alert('Need at least 1 enabled player to start tournament');
return;
}
const formatText = selectedTournamentType === '40_targets' ? t('tournament_types.40_targets_full') :
selectedTournamentType === '4_targets' ? t('tournament_types.4_targets_full') :
t('tournament_types.20_targets_full');
if (!confirm(t('messages.confirm_start_tournament', { players: enabledPlayers.length, format: formatText }))) {
return;
}
const startBtn = document.getElementById('startSingleBtn');
startBtn.disabled = true;
startBtn.textContent = 'Starting...';
try {
const response = await fetch('/api/tournament/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ tournament_type: selectedTournamentType })
});
if (response.ok) {
const result = await response.json();
alert('Tournament started successfully!');
window.location.reload();
} else {
const error = await response.json();
alert('Failed to start tournament: ' + error.message);
}
} catch (error) {
console.error('Error starting tournament:', error);
alert('Error starting tournament. Please try again.');
} finally {
startBtn.disabled = false;
startBtn.textContent = '🎯 Start Single Tournament';
}
}
// Start next tournament in league
async function startNextTournament() {
// Get selected joker players
const jokerCheckboxes = document.querySelectorAll('.joker-checkbox:checked:not(:disabled)');
const jokerPlayers = Array.from(jokerCheckboxes).map(cb => parseInt(cb.dataset.playerId));
const confirmMessage = jokerPlayers.length === 1 ?
t('league.start_tournament_confirm_single') :
t('league.start_tournament_confirm_multiple').replace('{count}', jokerPlayers.length);
if (!confirm(confirmMessage)) {
return;
}
try {
const response = await fetch('/api/league/tournament/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ joker_players: jokerPlayers })
});
if (response.ok) {
const result = await response.json();
alert('Tournament started successfully!');
window.location.reload();
} else {
const error = await response.json();
alert('Failed to start tournament: ' + error.message);
}
} catch (error) {
console.error('Error starting tournament:', error);
alert('Error starting tournament. Please try again.');
}
}
// Reset league
async function resetLeague() {
if (!confirm('Are you sure you want to reset the league? This will delete all league and tournament data.')) {
return;
}
try {
const response = await fetch('/api/league/reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
alert('League reset successfully!');
window.location.reload();
} else {
alert('Failed to reset league');
}
} catch (error) {
console.error('Error resetting league:', error);
alert('Error resetting league. Please try again.');
}
}
// Stop league (pause current tournament and keep league data)
async function stopLeague() {
if (!confirm('Are you sure you want to stop the league? The current tournament will be stopped but league data will be kept.')) {
return;
}
try {
const response = await fetch('/api/tournament/reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
alert('Tournament stopped successfully! League is paused.');
window.location.reload();
} else {
alert('Failed to stop tournament');
}
} catch (error) {
console.error('Error stopping tournament:', error);
alert('Error stopping tournament. Please try again.');
}
}
// Stop and delete league
async function stopAndDeleteLeague() {
if (!confirm('Are you sure you want to stop and delete this league? This action cannot be undone. All league data will be deleted.')) {
return;
}
try {
const response = await fetch('/api/league/reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
alert('League has been stopped and deleted successfully!');
window.location.reload();
} else {
const error = await response.json();
alert('Failed to delete league: ' + (error.message || 'Unknown error'));
}
} catch (error) {
console.error('Error deleting league:', error);
alert('Error deleting league. Please try again.');
}
}
// Reset tournament
async function resetTournament() {
if (!confirm('Are you sure you want to reset the tournament? This will delete tournament data.')) {
return;
}
try {
const response = await fetch('/api/tournament/reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
alert('Tournament reset successfully!');
window.location.reload();
} else {
alert('Failed to reset tournament');
}
} catch (error) {
console.error('Error resetting tournament:', error);
alert('Error resetting tournament. Please try again.');
}
}
// Cancel tournament within a league (rolls back tournament counter)
async function cancelLeagueTournament() {
if (!confirm('Ponastavi tekmo? Liga ostane na istem turnirju.')) {
return;
}
try {
const response = await fetch('/api/tournament/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
window.location.reload();
} else {
alert('Napaka pri ponastavitvi tekme.');
}
} catch (error) {
console.error('Error cancelling tournament:', error);
alert('Napaka. Poskusite znova.');
}
}
// Modal functions
function showModal(title, message, confirmCallback) {
document.getElementById('modalTitle').textContent = title;
document.getElementById('modalMessage').textContent = message;
pendingAction = confirmCallback;
document.getElementById('confirmationModal').classList.add('active');
}
function closeModal() {
document.getElementById('confirmationModal').classList.remove('active');
pendingAction = null;
}
function confirmAction() {
if (pendingAction) {
pendingAction();
}
}
// Click outside modal to close
document.getElementById('confirmationModal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
// Wait for translations to be ready before rendering the player table
window.addEventListener('i18nReady', function() {
renderPlayerTable();
// Focus on add player input if no active league/tournament
const nameInput = document.getElementById('newPlayerName');
if (nameInput && !leagueActive && !tournamentActive) {
nameInput.focus();
}
console.log('🏆 Tournament Management loaded');
console.log('League active:', leagueActive);
console.log('Tournament active:', tournamentActive);
{% if league_state %}
console.log('📊 League State Debug:');
console.log(' - Completed tournaments:', {{ league_state.completed_tournaments|length }});
console.log(' - Total tournaments:', {{ league_state.total_tournaments }});
console.log(' - Current tournament:', {{ league_state.current_tournament }});
console.log(' - Completed list:', {{ league_state.completed_tournaments|tojson }});
{% endif %}
}, { once: true });
});
</script>
</body>
</html>