Compare commits

..

10 Commits

Author SHA1 Message Date
bl3kunja 988cb53de1 Fixes to tournament. Refactor analysis and arhive with new treeview. 2026-04-18 19:39:32 +02:00
bl3kunja d0a468b740 Bug fix for mainscreen update and higher version of app to 1.0.2 2026-04-13 11:30:42 +02:00
bl3kunja 024ab24ceb added scroolbar to player cards inside calculator. 2026-04-11 18:29:23 +02:00
bl3kunja 30d480f053 Newest Main page regreshing. Updated calculator. Removed unused stuff. 2026-04-11 17:09:00 +02:00
bl3kunja d6f8ff78e1 no tournament started 2026-02-08 19:57:30 +01:00
bl3kunja baeaad0d49 Merge APP_V2/main: Combine quality updates with league features
- Merged remote changes: Liga krog3 and league state management
- Resolved merge conflicts in locale files and templates
- Kept local v1.0.1 version and styling improvements
- Added league combine functionality from remote
- Preserved new league_state.json and tournament_results.json
- Integrated i18n improvements

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-08 19:35:27 +01:00
bl3kunja d0bbf7bdce quality update:
- calculator
- result screens
- some fixes
2026-02-08 15:19:13 +01:00
bl3kunja-FW 13c7bd3239 Liga krog3 2026-01-17 09:36:27 +01:00
bl3kunja-FW 27e8b31ae0 combining league jsons 2025-11-14 17:03:30 +01:00
bl3kunja aa01f4136d player stats updated badges 2025-11-12 19:05:44 +01:00
49 changed files with 5424 additions and 71650 deletions
+213 -12
View File
@@ -1,5 +1,5 @@
"""
TV_APP V1.0.0 - Tournament and League Management System
TV_APP V1.0.1 - Tournament and League Management System
Flask web application for managing tournaments with multi-camera streaming
"""
@@ -20,7 +20,8 @@ from app.utils import (
from app.storage import (
SettingsStorage, PlayerStorage, TournamentStorage,
ResultsStorage, LeagueStorage, ArchiveStorage,
TOURNAMENT_FILE, RESULTS_FILE, LEAGUE_FILE
TOURNAMENT_FILE, RESULTS_FILE, LEAGUE_FILE,
ARCHIVE_DIR, LEAGUE_ARCHIVE_DIR
)
from app.models import Tournament, Scoring, RoundManager
@@ -44,10 +45,6 @@ STREAMS = [
SUPPORTED_LANGUAGES = ['sl', 'en']
DEFAULT_LANGUAGE = 'sl'
# Data file paths (organized in data directory)
ARCHIVE_DIR = 'data/tournament_archives'
LEAGUE_ARCHIVE_DIR = 'data/league_archives'
# Convenience function wrappers that delegate to storage classes
def load_settings():
return SettingsStorage.load_settings()
@@ -79,6 +76,7 @@ def load_results():
def save_results(results_data):
return ResultsStorage.save_results(results_data)
def archive_tournament(tournament_data, results_data):
return ArchiveStorage.archive_tournament(tournament_data, results_data)
@@ -223,6 +221,9 @@ def analyze_player_performance(player_id, archives_data):
'best_tournament_score': 0,
'worst_tournament_score': float('inf'),
'average_tournament_score': 0,
'best_4_targets_score': 0,
'best_20_targets_score': 0,
'best_40_targets_score': 0,
'total_shots_fired': 0,
'performance_trend': [],
'tournament_history': [],
@@ -264,6 +265,17 @@ def analyze_player_performance(player_id, archives_data):
player_stats['total_shots_fired'] += shots_in_tournament
# Track format-specific best scores
if tournament_type == '4_targets':
if score > player_stats['best_4_targets_score']:
player_stats['best_4_targets_score'] = score
elif tournament_type == '20_targets':
if score > player_stats['best_20_targets_score']:
player_stats['best_20_targets_score'] = score
elif tournament_type == '40_targets':
if score > player_stats['best_40_targets_score']:
player_stats['best_40_targets_score'] = score
# Calculate and aggregate shot accuracy
if tournament_type in player_stats['shot_accuracy']:
shot_accuracy = calculate_shot_accuracy(participant)
@@ -512,6 +524,43 @@ def view_player_stats(player_id):
# Enhanced API endpoints for the modern archive system
@app.route('/api/archive/tournament/<filename>/standings')
def api_tournament_standings(filename):
filepath = os.path.join(ARCHIVE_DIR, filename)
data = load_archive_file(filepath)
if not data:
return jsonify({'error': 'not found'}), 404
results_data = data.get('results', {})
tournament_type = data.get('tournament', {}).get('tournament_type', '20_targets')
participants = []
for player_id, p in results_data.get('participants', {}).items():
targets = p.get('targets', {})
tens = calculate_tens_from_targets(targets)
participants.append({'name': p['name'], 'total_score': p['total_score'], 'tens_count': tens})
participants.sort(key=lambda x: (x['total_score'], x['tens_count']), reverse=True)
for i, p in enumerate(participants):
p['rank'] = i + 1
return jsonify({
'kind': 'tournament',
'tournament_type': tournament_type,
'created_at': results_data.get('created_at', ''),
'archived_at': data.get('archived_at', ''),
'participants': participants
})
@app.route('/api/archive/league/<filename>/standings')
def api_league_standings(filename):
filepath = os.path.join(LEAGUE_ARCHIVE_DIR, filename)
data = load_archive_file(filepath)
if not data:
return jsonify({'error': 'not found'}), 404
league_data = data.get('league', {})
tournament_type = league_data.get('tournament_type', '20_targets')
calculate_league_final_scores(league_data)
participants = get_league_final_rankings(league_data)
rows = [{'rank': p['rank'], 'name': p['name'], 'final_score': p['final_score'], 'total_score': p['total_score'], 'tournaments_participated': p['tournaments_participated']} for p in participants]
return jsonify({'kind': 'league', 'tournament_type': tournament_type, 'archived_at': data.get('archived_at', ''), 'participants': rows})
@app.route('/api/archive/stats', methods=['GET'])
def api_get_archive_stats():
"""API endpoint to get archive overview statistics"""
@@ -571,6 +620,10 @@ def api_get_players_with_stats():
for player in all_players:
player_stats = analyze_player_performance(player['id'], archives_data)
total_tens = sum(
player_stats['shot_accuracy'][t]['tens']
for t in player_stats['shot_accuracy']
)
players_with_stats.append({
'id': player['id'],
'name': player['name'],
@@ -581,7 +634,8 @@ def api_get_players_with_stats():
'best_tournament_score': player_stats['best_tournament_score'],
'average_tournament_score': player_stats['average_tournament_score'],
'total_shots_fired': player_stats['total_shots_fired'],
'performance_trend': player_stats['tournament_scores'][-8:] if len(player_stats['tournament_scores']) >= 8 else player_stats['tournament_scores'] # Last 8 tournaments for mini chart
'total_tens': total_tens,
'performance_trend': player_stats['tournament_scores'][-8:] if len(player_stats['tournament_scores']) >= 8 else player_stats['tournament_scores']
}
})
@@ -629,11 +683,21 @@ def view_archived_tournament(filename):
for i, participant in enumerate(participants):
participant['rank'] = i + 1
# Use the existing results display template but with archived data
# Sanitize tournament_data - convert to JSON-serializable format
sanitized_tournament = {
'tournament_id': str(tournament_data.get('tournament_id', '')),
'tournament_type': str(tournament_data.get('tournament_type', '20_targets')),
'total_rounds': tournament_data.get('total_rounds', 1),
'created_at': str(tournament_data.get('created_at', ''))
}
embed = request.args.get('embed') == '1'
return render_template('results_display.html',
tournament=sanitized_tournament,
results=results_data,
participants=participants,
archived=True,
embed=embed,
archive_info={
'filename': filename,
'archived_at': data.get('archived_at'),
@@ -658,11 +722,12 @@ def view_archived_league(filename):
calculate_league_final_scores(league_data)
participants = get_league_final_rankings(league_data)
# Use the existing league results display template but with archived data
embed = request.args.get('embed') == '1'
return render_template('league_scoreboard_display.html',
league=league_data,
participants=participants,
archived=True,
embed=embed,
archive_info={
'filename': filename,
'archived_at': data.get('archived_at'),
@@ -987,7 +1052,6 @@ def tournament():
players_data = load_players()
tournament_state = load_tournament_state()
league_state = load_league_state()
return render_template('tournament.html',
players=players_data['players'],
tournament_state=tournament_state,
@@ -1073,7 +1137,21 @@ def results_display():
translations=get_translations(),
current_language=get_current_language())
# Priority 1.5: Check if current results are from a finished league (even if league state was archived)
# Priority 1.5: Check if results contain league_data (from league preview/combiner)
elif results and results.get('league_data'):
league_data = results.get('league_data')
calculate_league_final_scores(league_data)
participants = get_league_final_rankings(league_data)
return render_template('league_scoreboard_display.html',
league=league_data,
participants=participants,
results=None,
preview_mode=True,
translations=get_translations(),
current_language=get_current_language())
# Priority 1.6: Check if current results are from a finished league (even if league state was archived)
elif results and results.get('league_tournament_number'):
# This is a league tournament result, but league state was archived
# Try to find the archived league data
@@ -1126,7 +1204,15 @@ def results_display():
for i, participant in enumerate(participants):
participant['rank'] = i + 1
# Create sanitized tournament data for template
sanitized_tournament = {
'tournament_id': str(results.get('tournament_id', '')),
'tournament_type': str(results.get('tournament_type', '20_targets')),
'total_rounds': results.get('total_rounds', 1)
}
return render_template('results_display.html',
tournament=sanitized_tournament,
results=results,
participants=participants,
league=None,
@@ -1455,6 +1541,25 @@ def reset_tournament():
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 400
@app.route('/api/tournament/cancel', methods=['POST'])
def cancel_league_tournament():
"""Cancel the current tournament within a league, rolling back the tournament counter"""
try:
if os.path.exists(TOURNAMENT_FILE):
os.remove(TOURNAMENT_FILE)
if os.path.exists(RESULTS_FILE):
os.remove(RESULTS_FILE)
# Roll back league's current_tournament counter
league_state = load_league_state()
if league_state and league_state.get('current_tournament', 1) > 1:
league_state['current_tournament'] -= 1
save_league_state(league_state)
return jsonify({'status': 'success'})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 400
# RESULTS API Routes (Updated)
@app.route('/api/results/participant/<int:player_id>', methods=['POST'])
def update_participant_scores(player_id):
@@ -1566,6 +1671,8 @@ def finish_tournament():
'participated': False,
'joker': True
})
# Mark joker as used for this player
participant['joker_used'] = True
# Calculate total shots correctly for any tournament type
tournament_type = results.get('tournament_type', '20_targets')
@@ -1684,6 +1791,100 @@ def get_league():
else:
return jsonify({'status': 'error', 'message': 'No league found'}), 404
@app.route('/api/dashboard/state', methods=['GET'])
def api_dashboard_state():
"""Get current dashboard state for polling updates (TV display sync)"""
tournament_state = load_tournament_state()
league_state = load_league_state()
results = load_results()
tournament_finished = results.get('tournament_finished', False) if results else False
league_finished = league_state.get('league_finished', False) if league_state else False
response_data = {
'tournament_active': tournament_state is not None,
'current_round': None,
'total_rounds': None,
'league_active': league_state is not None,
'league_tournament': None,
'league_total': None,
'player_names': [],
'tournament_finished': tournament_finished,
'league_finished': league_finished,
}
if tournament_state:
response_data['current_round'] = tournament_state.get('current_round', 1)
response_data['total_rounds'] = tournament_state.get('total_rounds', 1)
try:
current_round_data = get_current_round_data()
if current_round_data and isinstance(current_round_data, dict):
players = current_round_data.get('players', [])
if isinstance(players, list):
response_data['player_names'] = [
str(p.get('name', 'Unknown')) if isinstance(p, dict) else 'Unknown'
for p in players if p
]
except Exception as e:
# Log error but don't fail - player_names will remain empty list
print(f"Error getting player names for dashboard state: {e}")
response_data['player_names'] = []
if league_state:
response_data['league_tournament'] = league_state.get('current_tournament', 1)
response_data['league_total'] = league_state.get('total_tournaments', 6)
settings = load_settings()
response_data['tv_view'] = settings.get('tv_view', 'cameras')
response_data['tv_url'] = settings.get('tv_url')
response_data['event_active'] = False
response_data['event_player_names'] = []
# When no active tournament/results, expose latest archive filename so clients can redirect correctly
response_data['archive_filename'] = None
response_data['league_archive_filename'] = None
if not tournament_state and not results:
try:
archive_files = sorted(
glob.glob(os.path.join(ARCHIVE_DIR, 'tournament_*.json')),
key=os.path.getmtime, reverse=True
)
if archive_files:
response_data['archive_filename'] = os.path.basename(archive_files[0])
except Exception:
pass
try:
league_archive_files = sorted(
glob.glob(os.path.join(LEAGUE_ARCHIVE_DIR, 'league_*.json')),
key=os.path.getmtime, reverse=True
)
if league_archive_files:
response_data['league_archive_filename'] = os.path.basename(league_archive_files[0])
except Exception:
pass
return jsonify(response_data)
@app.route('/api/tv/view', methods=['POST'])
def set_tv_view():
"""Set the desired TV display view (cameras or draft)"""
data = request.get_json() or {}
view = data.get('view', 'cameras')
if view not in ('cameras', 'draft', 'results'):
return jsonify({'status': 'error', 'message': 'Invalid view'}), 400
settings = load_settings()
settings['tv_view'] = view
# Optional explicit URL for the TV to navigate to (e.g. a specific league archive)
tv_url = data.get('tv_url')
if tv_url:
settings['tv_url'] = tv_url
elif view == 'cameras':
settings.pop('tv_url', None)
save_settings(settings)
return jsonify({'status': 'success', 'tv_view': view})
# Add this route to your Flask app (around line 850, with the other mobile routes)
@app.route('/mobile/remote')
@@ -2340,4 +2541,4 @@ def set_language(language):
}), 400
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
app.run(host='0.0.0.0', port=4000, debug=True)
+1 -1
View File
@@ -10,4 +10,4 @@ class Config:
SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-for-sessions')
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
HOST = os.getenv('HOST', '0.0.0.0')
PORT = int(os.getenv('PORT', 5000))
PORT = int(os.getenv('PORT', 5001))
+2 -1
View File
@@ -131,7 +131,8 @@ class Tournament:
'name': player['name'],
'targets': targets,
'total_score': 0,
'completed': False
'completed': False,
'joker_selected': False # Track if joker was selected in calculator
}
return results
+12 -8
View File
@@ -8,14 +8,18 @@ import os
import glob
from datetime import datetime
# Base directory anchored to this file's location (project root), so paths work
# regardless of the current working directory when the app is launched.
_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# File paths - organized in data directory
SETTINGS_FILE = 'data/camera_settings.json'
PLAYERS_FILE = 'data/players.json'
TOURNAMENT_FILE = 'data/tournament_state.json'
RESULTS_FILE = 'data/tournament_results.json'
LEAGUE_FILE = 'data/league_state.json'
ARCHIVE_DIR = 'data/tournament_archives'
LEAGUE_ARCHIVE_DIR = 'data/league_archives'
SETTINGS_FILE = os.path.join(_BASE_DIR, 'data', 'camera_settings.json')
PLAYERS_FILE = os.path.join(_BASE_DIR, 'data', 'players.json')
TOURNAMENT_FILE = os.path.join(_BASE_DIR, 'data', 'tournament_state.json')
RESULTS_FILE = os.path.join(_BASE_DIR, 'data', 'tournament_results.json')
LEAGUE_FILE = os.path.join(_BASE_DIR, 'data', 'league_state.json')
ARCHIVE_DIR = os.path.join(_BASE_DIR, 'data', 'tournament_archives')
LEAGUE_ARCHIVE_DIR = os.path.join(_BASE_DIR, 'data', 'league_archives')
# Default settings
DEFAULT_SETTINGS = {
@@ -40,7 +44,7 @@ class FileStorage:
@staticmethod
def _ensure_directory(directory):
"""Ensure directory exists"""
if not os.path.exists(directory):
if directory and not os.path.exists(directory):
os.makedirs(directory)
@staticmethod
+4 -3
View File
@@ -9,7 +9,8 @@
},
"display_options": {
"show_titles": true,
"title_size": 1.2,
"target_number_size": 1.4
}
"title_size": 1.4,
"target_number_size": 1.7
},
"tv_view": "cameras"
}
File diff suppressed because it is too large Load Diff
@@ -1,275 +0,0 @@
{
"league": {
"league_id": "league_20251109_144611",
"created_at": "2025-11-09T14:46:11.803719",
"tournament_type": "4_targets",
"total_tournaments": 5,
"current_tournament": 0,
"participants": {
"2": {
"name": "Nik Pleterski",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"4": {
"name": "Mateja Pleterski",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"5": {
"name": "Jo\u017ee Verhnjak",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"6": {
"name": "Mateja Senica",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"10": {
"name": "Mitja \u010ceh",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"11": {
"name": "Rado Kefer",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"12": {
"name": "Matej Kvasnik",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"14": {
"name": "Karli Proje",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"16": {
"name": "Silvo Poro\u010dnik",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"18": {
"name": "Matja\u017e Pleterski",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"20": {
"name": "Jo\u017ee Preglav",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"21": {
"name": "Marko Blimen",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"23": {
"name": "Robi Krautberger",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"24": {
"name": "Jo\u017ee Verdinek",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"30": {
"name": "Maja Hirtl",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"33": {
"name": "Namir Uzunovi\u0107",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"34": {
"name": "Jo\u017ee Planin\u0161ec",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"35": {
"name": "Vanja Kolar",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"36": {
"name": "Klara Wankmuller",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"37": {
"name": "Milan Stramec",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"39": {
"name": "Tia Sudar",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"41": {
"name": "Tadej \u0160truc",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"42": {
"name": "Jure Glaser",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"43": {
"name": "Marko Pokr\u017enik",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"45": {
"name": "Lidija Blimen",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"46": {
"name": "Tijana \u0160tumpfl",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"47": {
"name": "Ljuba Mr\u0161ak",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"48": {
"name": "Janja Salcman",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
},
"49": {
"name": "Jolanda Verhnjak",
"joker_used": false,
"tournament_results": [],
"total_score": 0,
"final_score": 0,
"tournaments_participated": 0,
"excluded_tournament": null
}
},
"completed_tournaments": [],
"league_finished": false
},
"archived_at": "2025-11-09T14:46:18.477787"
}
File diff suppressed because it is too large Load Diff
+51 -41
View File
@@ -3,32 +3,32 @@
{
"id": 1,
"name": "Domen Pleterski",
"enabled": true
"enabled": false
},
{
"id": 2,
"name": "Nik Pleterski",
"enabled": true
"enabled": false
},
{
"id": 3,
"name": "Ivan Tandler",
"enabled": true
"enabled": false
},
{
"id": 4,
"name": "Mateja Pleterski",
"enabled": true
"enabled": false
},
{
"id": 5,
"name": "Jože Verhnjak",
"enabled": true
"enabled": false
},
{
"id": 6,
"name": "Mateja Senica",
"enabled": true
"enabled": false
},
{
"id": 7,
@@ -38,27 +38,27 @@
{
"id": 8,
"name": "Franc Žigart",
"enabled": true
"enabled": false
},
{
"id": 9,
"name": "Janez Božič",
"enabled": true
"enabled": false
},
{
"id": 10,
"name": "Mitja Čeh",
"enabled": true
"enabled": false
},
{
"id": 11,
"name": "Rado Kefer",
"enabled": true
"enabled": false
},
{
"id": 12,
"name": "Matej Kvasnik",
"enabled": true
"enabled": false
},
{
"id": 13,
@@ -68,57 +68,57 @@
{
"id": 14,
"name": "Karli Proje",
"enabled": true
"enabled": false
},
{
"id": 15,
"name": "Jan Pleterski",
"enabled": true
"enabled": false
},
{
"id": 16,
"name": "Silvo Poročnik",
"enabled": true
"enabled": false
},
{
"id": 17,
"name": "Dušan Onuk",
"enabled": true
"enabled": false
},
{
"id": 18,
"name": "Matjaž Pleterski",
"enabled": true
"enabled": false
},
{
"id": 19,
"name": "Franc Rizmal",
"enabled": true
"enabled": false
},
{
"id": 20,
"name": "Jože Preglav",
"enabled": true
"enabled": false
},
{
"id": 21,
"name": "Marko Blimen",
"enabled": true
"enabled": false
},
{
"id": 22,
"name": "Doris Fesel",
"enabled": true
"enabled": false
},
{
"id": 23,
"name": "Robi Krautberger",
"enabled": true
"enabled": false
},
{
"id": 24,
"name": "Jože Verdinek",
"enabled": true
"enabled": false
},
{
"id": 25,
@@ -128,12 +128,12 @@
{
"id": 26,
"name": "Jakob Herman",
"enabled": true
"enabled": false
},
{
"id": 27,
"name": "Janez Mrak",
"enabled": true
"enabled": false
},
{
"id": 28,
@@ -148,12 +148,12 @@
{
"id": 30,
"name": "Maja Hirtl",
"enabled": true
"enabled": false
},
{
"id": 31,
"name": "Dejan Kučnik",
"enabled": true
"enabled": false
},
{
"id": 32,
@@ -163,27 +163,27 @@
{
"id": 33,
"name": "Namir Uzunović",
"enabled": true
"enabled": false
},
{
"id": 34,
"name": "Jože Planinšec",
"enabled": true
"enabled": false
},
{
"id": 35,
"name": "Vanja Kolar",
"enabled": true
"enabled": false
},
{
"id": 36,
"name": "Klara Wankmuller",
"enabled": true
"enabled": false
},
{
"id": 37,
"name": "Milan Stramec",
"enabled": true
"enabled": false
},
{
"id": 38,
@@ -193,27 +193,27 @@
{
"id": 39,
"name": "Tia Sudar",
"enabled": true
"enabled": false
},
{
"id": 40,
"name": "Jaka Cvar",
"enabled": true
"enabled": false
},
{
"id": 41,
"name": "Tadej Štruc",
"enabled": true
"enabled": false
},
{
"id": 42,
"name": "Jure Glaser",
"enabled": true
"enabled": false
},
{
"id": 43,
"name": "Marko Pokržnik",
"enabled": true
"enabled": false
},
{
"id": 44,
@@ -223,27 +223,37 @@
{
"id": 45,
"name": "Lidija Blimen",
"enabled": true
"enabled": false
},
{
"id": 46,
"name": "Tijana Štumpfl",
"enabled": true
"enabled": false
},
{
"id": 47,
"name": "Ljuba Mršak",
"enabled": true
"enabled": false
},
{
"id": 48,
"name": "Janja Salcman",
"enabled": true
"enabled": false
},
{
"id": 49,
"name": "Jolanda Verhnjak",
"enabled": true
"enabled": false
},
{
"id": 50,
"name": "Vid Ravnjak",
"enabled": false
},
{
"id": 51,
"name": "Robi Ovčar",
"enabled": false
}
]
}
@@ -1,759 +0,0 @@
{
"tournament": {
"rounds": [
{
"round_number": 1,
"players": [
{
"id": 4,
"name": "Mateja Pleterski",
"enabled": true
},
{
"id": 20,
"name": "Jo\u017ee Preglav",
"enabled": true
},
{
"id": 3,
"name": "Ivan Tandler",
"enabled": true
},
{
"id": 17,
"name": "Du\u0161an Onuk",
"enabled": true
},
{
"id": 9,
"name": "Janez Bo\u017ei\u010d",
"enabled": true
},
{
"id": 44,
"name": "Anka Ka\u010dnik",
"enabled": true
}
],
"status": "pending"
},
{
"round_number": 2,
"players": [
{
"id": 48,
"name": "Janja Salcman",
"enabled": true
},
{
"id": 15,
"name": "Jan Pleterski",
"enabled": true
},
{
"id": 45,
"name": "Lidija Blimen",
"enabled": true
},
{
"id": 21,
"name": "Marko Blimen",
"enabled": true
},
{
"id": 49,
"name": "Jolanda Verhnjak",
"enabled": true
},
{
"id": 11,
"name": "Rado Kefer",
"enabled": true
}
],
"status": "waiting"
},
{
"round_number": 3,
"players": [
{
"id": 12,
"name": "Matej Kvasnik",
"enabled": true
},
{
"id": 47,
"name": "Ljuba Mr\u0161ak",
"enabled": true
},
{
"id": 46,
"name": "Tijana \u0160tumpfl",
"enabled": true
},
{
"id": 1,
"name": "Domen Pleterski",
"enabled": true
},
{
"id": 7,
"name": "Branko Poker\u017enik",
"enabled": true
},
{
"id": 5,
"name": "Jo\u017ee Verhnjak",
"enabled": true
}
],
"status": "waiting"
}
],
"created_at": "2025-08-30T13:09:30.384521",
"total_players": 18,
"total_rounds": 3,
"current_round": 1,
"tournament_type": "4_targets"
},
"results": {
"tournament_id": "2025-08-30T13:09:30.384521",
"tournament_type": "4_targets",
"participants": {
"4": {
"name": "Mateja Pleterski",
"targets": {
"1": {
"shot1": 0,
"shot2": 0,
"shot3": 0,
"shot4": 0,
"shot5": 0
},
"2": {
"shot1": 0,
"shot2": 0,
"shot3": 0,
"shot4": 0,
"shot5": 0
},
"3": {
"shot1": 0,
"shot2": 0,
"shot3": 0,
"shot4": 0,
"shot5": 0
},
"4": {
"shot1": 0,
"shot2": 0,
"shot3": 0,
"shot4": 0,
"shot5": 0
}
},
"total_score": 0,
"completed": true
},
"20": {
"name": "Jo\u017ee Preglav",
"targets": {
"1": {
"shot1": 10,
"shot2": 8,
"shot3": 8,
"shot4": 7,
"shot5": 5
},
"2": {
"shot1": 9,
"shot2": 7,
"shot3": 7,
"shot4": 5,
"shot5": 4
},
"3": {
"shot1": 9,
"shot2": 9,
"shot3": 7,
"shot4": 6,
"shot5": 6
},
"4": {
"shot1": 8,
"shot2": 8,
"shot3": 7,
"shot4": 6,
"shot5": 6
}
},
"total_score": 142,
"completed": true
},
"3": {
"name": "Ivan Tandler",
"targets": {
"1": {
"shot1": 0,
"shot2": 0,
"shot3": 0,
"shot4": 0,
"shot5": 0
},
"2": {
"shot1": 0,
"shot2": 0,
"shot3": 0,
"shot4": 0,
"shot5": 0
},
"3": {
"shot1": 0,
"shot2": 0,
"shot3": 0,
"shot4": 0,
"shot5": 0
},
"4": {
"shot1": 0,
"shot2": 0,
"shot3": 0,
"shot4": 0,
"shot5": 0
}
},
"total_score": 0,
"completed": true
},
"17": {
"name": "Du\u0161an Onuk",
"targets": {
"1": {
"shot1": 10,
"shot2": 9,
"shot3": 9,
"shot4": 9,
"shot5": 9
},
"2": {
"shot1": 10,
"shot2": 10,
"shot3": 9,
"shot4": 8,
"shot5": 7
},
"3": {
"shot1": 10,
"shot2": 9,
"shot3": 9,
"shot4": 9,
"shot5": 9
},
"4": {
"shot1": 10,
"shot2": 9,
"shot3": 9,
"shot4": 8,
"shot5": 8
}
},
"total_score": 180,
"completed": true
},
"9": {
"name": "Janez Bo\u017ei\u010d",
"targets": {
"1": {
"shot1": 10,
"shot2": 10,
"shot3": 9,
"shot4": 9,
"shot5": 9
},
"2": {
"shot1": 10,
"shot2": 9,
"shot3": 8,
"shot4": 8,
"shot5": 7
},
"3": {
"shot1": 10,
"shot2": 10,
"shot3": 10,
"shot4": 9,
"shot5": 7
},
"4": {
"shot1": 10,
"shot2": 10,
"shot3": 10,
"shot4": 8,
"shot5": 6
}
},
"total_score": 179,
"completed": true
},
"44": {
"name": "Anka Ka\u010dnik",
"targets": {
"1": {
"shot1": 10,
"shot2": 9,
"shot3": 6,
"shot4": 4,
"shot5": 3
},
"2": {
"shot1": 9,
"shot2": 7,
"shot3": 6,
"shot4": 3,
"shot5": 2
},
"3": {
"shot1": 7,
"shot2": 4,
"shot3": 4,
"shot4": 2,
"shot5": 1
},
"4": {
"shot1": 7,
"shot2": 6,
"shot3": 5,
"shot4": 4,
"shot5": 3
}
},
"total_score": 102,
"completed": true
},
"48": {
"name": "Janja Salcman",
"targets": {
"1": {
"shot1": 7,
"shot2": 3,
"shot3": 2,
"shot4": 2,
"shot5": 0
},
"2": {
"shot1": 6,
"shot2": 5,
"shot3": 2,
"shot4": 2,
"shot5": 0
},
"3": {
"shot1": 9,
"shot2": 9,
"shot3": 7,
"shot4": 1,
"shot5": 0
},
"4": {
"shot1": 8,
"shot2": 5,
"shot3": 3,
"shot4": 1,
"shot5": 0
}
},
"total_score": 72,
"completed": true
},
"15": {
"name": "Jan Pleterski",
"targets": {
"1": {
"shot1": 10,
"shot2": 10,
"shot3": 9,
"shot4": 6,
"shot5": 6
},
"2": {
"shot1": 10,
"shot2": 10,
"shot3": 9,
"shot4": 8,
"shot5": 8
},
"3": {
"shot1": 10,
"shot2": 9,
"shot3": 9,
"shot4": 8,
"shot5": 6
},
"4": {
"shot1": 10,
"shot2": 9,
"shot3": 6,
"shot4": 5,
"shot5": 3
}
},
"total_score": 161,
"completed": true
},
"45": {
"name": "Lidija Blimen",
"targets": {
"1": {
"shot1": 10,
"shot2": 9,
"shot3": 6,
"shot4": 4,
"shot5": 4
},
"2": {
"shot1": 8,
"shot2": 8,
"shot3": 6,
"shot4": 6,
"shot5": 4
},
"3": {
"shot1": 8,
"shot2": 7,
"shot3": 4,
"shot4": 1,
"shot5": 1
},
"4": {
"shot1": 9,
"shot2": 8,
"shot3": 8,
"shot4": 8,
"shot5": 5
}
},
"total_score": 124,
"completed": true
},
"21": {
"name": "Marko Blimen",
"targets": {
"1": {
"shot1": 10,
"shot2": 9,
"shot3": 9,
"shot4": 8,
"shot5": 7
},
"2": {
"shot1": 10,
"shot2": 9,
"shot3": 9,
"shot4": 7,
"shot5": 7
},
"3": {
"shot1": 9,
"shot2": 8,
"shot3": 8,
"shot4": 7,
"shot5": 6
},
"4": {
"shot1": 10,
"shot2": 10,
"shot3": 9,
"shot4": 8,
"shot5": 7
}
},
"total_score": 167,
"completed": true
},
"49": {
"name": "Jolanda Verhnjak",
"targets": {
"1": {
"shot1": 8,
"shot2": 7,
"shot3": 6,
"shot4": 4,
"shot5": 3
},
"2": {
"shot1": 8,
"shot2": 8,
"shot3": 8,
"shot4": 8,
"shot5": 3
},
"3": {
"shot1": 7,
"shot2": 6,
"shot3": 5,
"shot4": 3,
"shot5": 3
},
"4": {
"shot1": 10,
"shot2": 8,
"shot3": 8,
"shot4": 5,
"shot5": 4
}
},
"total_score": 122,
"completed": true
},
"11": {
"name": "Rado Kefer",
"targets": {
"1": {
"shot1": 10,
"shot2": 9,
"shot3": 9,
"shot4": 8,
"shot5": 7
},
"2": {
"shot1": 10,
"shot2": 9,
"shot3": 9,
"shot4": 8,
"shot5": 8
},
"3": {
"shot1": 10,
"shot2": 9,
"shot3": 9,
"shot4": 8,
"shot5": 8
},
"4": {
"shot1": 9,
"shot2": 9,
"shot3": 9,
"shot4": 8,
"shot5": 8
}
},
"total_score": 174,
"completed": true
},
"12": {
"name": "Matej Kvasnik",
"targets": {
"1": {
"shot1": 9,
"shot2": 8,
"shot3": 8,
"shot4": 8,
"shot5": 6
},
"2": {
"shot1": 9,
"shot2": 9,
"shot3": 8,
"shot4": 6,
"shot5": 4
},
"3": {
"shot1": 10,
"shot2": 8,
"shot3": 6,
"shot4": 5,
"shot5": 6
},
"4": {
"shot1": 8,
"shot2": 8,
"shot3": 8,
"shot4": 7,
"shot5": 3
}
},
"total_score": 144,
"completed": true
},
"47": {
"name": "Ljuba Mr\u0161ak",
"targets": {
"1": {
"shot1": 0,
"shot2": 0,
"shot3": 0,
"shot4": 0,
"shot5": 0
},
"2": {
"shot1": 0,
"shot2": 0,
"shot3": 0,
"shot4": 0,
"shot5": 0
},
"3": {
"shot1": 0,
"shot2": 0,
"shot3": 0,
"shot4": 0,
"shot5": 0
},
"4": {
"shot1": 0,
"shot2": 0,
"shot3": 0,
"shot4": 0,
"shot5": 0
}
},
"total_score": 0,
"completed": true
},
"46": {
"name": "Tijana \u0160tumpfl",
"targets": {
"1": {
"shot1": 10,
"shot2": 10,
"shot3": 10,
"shot4": 1,
"shot5": 1
},
"2": {
"shot1": 5,
"shot2": 4,
"shot3": 2,
"shot4": 0,
"shot5": 0
},
"3": {
"shot1": 7,
"shot2": 7,
"shot3": 2,
"shot4": 1,
"shot5": 0
},
"4": {
"shot1": 5,
"shot2": 3,
"shot3": 2,
"shot4": 1,
"shot5": 0
}
},
"total_score": 71,
"completed": true
},
"1": {
"name": "Domen Pleterski",
"targets": {
"1": {
"shot1": 10,
"shot2": 10,
"shot3": 8,
"shot4": 8,
"shot5": 8
},
"2": {
"shot1": 9,
"shot2": 9,
"shot3": 9,
"shot4": 9,
"shot5": 7
},
"3": {
"shot1": 10,
"shot2": 10,
"shot3": 9,
"shot4": 9,
"shot5": 8
},
"4": {
"shot1": 10,
"shot2": 10,
"shot3": 10,
"shot4": 9,
"shot5": 8
}
},
"total_score": 180,
"completed": true
},
"7": {
"name": "Branko Poker\u017enik",
"targets": {
"1": {
"shot1": 9,
"shot2": 9,
"shot3": 9,
"shot4": 8,
"shot5": 7
},
"2": {
"shot1": 10,
"shot2": 10,
"shot3": 9,
"shot4": 8,
"shot5": 7
},
"3": {
"shot1": 10,
"shot2": 10,
"shot3": 9,
"shot4": 8,
"shot5": 8
},
"4": {
"shot1": 9,
"shot2": 9,
"shot3": 9,
"shot4": 8,
"shot5": 6
}
},
"total_score": 172,
"completed": true
},
"5": {
"name": "Jo\u017ee Verhnjak",
"targets": {
"1": {
"shot1": 10,
"shot2": 9,
"shot3": 8,
"shot4": 8,
"shot5": 5
},
"2": {
"shot1": 9,
"shot2": 8,
"shot3": 8,
"shot4": 6,
"shot5": 4
},
"3": {
"shot1": 10,
"shot2": 9,
"shot3": 8,
"shot4": 8,
"shot5": 7
},
"4": {
"shot1": 8,
"shot2": 9,
"shot3": 7,
"shot4": 6,
"shot5": 8
}
},
"total_score": 155,
"completed": true
}
},
"tournament_finished": true,
"created_at": "2025-08-30T13:09:30.386527",
"finished_at": "2025-08-30T15:33:42.405404"
},
"archived_at": "2025-08-30T15:33:42.405434"
}
@@ -1,759 +0,0 @@
{
"tournament": {
"rounds": [
{
"round_number": 1,
"players": [
{
"id": 4,
"name": "Mateja Pleterski",
"enabled": true
},
{
"id": 21,
"name": "Marko Blimen",
"enabled": true
},
{
"id": 3,
"name": "Ivan Tandler",
"enabled": true
},
{
"id": 7,
"name": "Branko Poker\u017enik",
"enabled": true
},
{
"id": 46,
"name": "Tijana \u0160tumpfl",
"enabled": true
},
{
"id": 9,
"name": "Janez Bo\u017ei\u010d",
"enabled": true
}
],
"status": "pending"
},
{
"round_number": 2,
"players": [
{
"id": 48,
"name": "Janja Salcman",
"enabled": true
},
{
"id": 45,
"name": "Lidija Blimen",
"enabled": true
},
{
"id": 20,
"name": "Jo\u017ee Preglav",
"enabled": true
},
{
"id": 11,
"name": "Rado Kefer",
"enabled": true
},
{
"id": 17,
"name": "Du\u0161an Onuk",
"enabled": true
},
{
"id": 15,
"name": "Jan Pleterski",
"enabled": true
}
],
"status": "waiting"
},
{
"round_number": 3,
"players": [
{
"id": 5,
"name": "Jo\u017ee Verhnjak",
"enabled": true
},
{
"id": 1,
"name": "Domen Pleterski",
"enabled": true
},
{
"id": 44,
"name": "Anka Ka\u010dnik",
"enabled": true
},
{
"id": 47,
"name": "Ljuba Mr\u0161ak",
"enabled": true
},
{
"id": 12,
"name": "Matej Kvasnik",
"enabled": true
},
{
"id": 49,
"name": "Jolanda Verhnjak",
"enabled": true
}
],
"status": "waiting"
}
],
"created_at": "2025-09-08T19:19:14.422360",
"total_players": 18,
"total_rounds": 3,
"current_round": 1,
"tournament_type": "4_targets"
},
"results": {
"tournament_id": "2025-09-08T19:19:14.422360",
"tournament_type": "4_targets",
"participants": {
"4": {
"name": "Mateja Pleterski",
"targets": {
"1": {
"shot1": 10,
"shot2": 9,
"shot3": 2,
"shot4": 10,
"shot5": 4
},
"2": {
"shot1": 8,
"shot2": 9,
"shot3": 8,
"shot4": 3,
"shot5": 7
},
"3": {
"shot1": 4,
"shot2": 2,
"shot3": 6,
"shot4": 0,
"shot5": 10
},
"4": {
"shot1": 7,
"shot2": 9,
"shot3": 10,
"shot4": 5,
"shot5": 6
}
},
"total_score": 129,
"completed": true
},
"21": {
"name": "Marko Blimen",
"targets": {
"1": {
"shot1": 9,
"shot2": 1,
"shot3": 8,
"shot4": 10,
"shot5": 4
},
"2": {
"shot1": 2,
"shot2": 4,
"shot3": 7,
"shot4": 7,
"shot5": 1
},
"3": {
"shot1": 0,
"shot2": 10,
"shot3": 1,
"shot4": 8,
"shot5": 9
},
"4": {
"shot1": 1,
"shot2": 8,
"shot3": 4,
"shot4": 7,
"shot5": 10
}
},
"total_score": 111,
"completed": true
},
"3": {
"name": "Ivan Tandler",
"targets": {
"1": {
"shot1": 2,
"shot2": 0,
"shot3": 9,
"shot4": 10,
"shot5": 2
},
"2": {
"shot1": 2,
"shot2": 0,
"shot3": 6,
"shot4": 10,
"shot5": 4
},
"3": {
"shot1": 0,
"shot2": 8,
"shot3": 2,
"shot4": 7,
"shot5": 4
},
"4": {
"shot1": 10,
"shot2": 1,
"shot3": 6,
"shot4": 7,
"shot5": 7
}
},
"total_score": 97,
"completed": true
},
"7": {
"name": "Branko Poker\u017enik",
"targets": {
"1": {
"shot1": 5,
"shot2": 9,
"shot3": 1,
"shot4": 9,
"shot5": 10
},
"2": {
"shot1": 2,
"shot2": 9,
"shot3": 10,
"shot4": 10,
"shot5": 7
},
"3": {
"shot1": 10,
"shot2": 1,
"shot3": 7,
"shot4": 4,
"shot5": 8
},
"4": {
"shot1": 2,
"shot2": 0,
"shot3": 10,
"shot4": 5,
"shot5": 0
}
},
"total_score": 119,
"completed": true
},
"46": {
"name": "Tijana \u0160tumpfl",
"targets": {
"1": {
"shot1": 6,
"shot2": 9,
"shot3": 4,
"shot4": 6,
"shot5": 1
},
"2": {
"shot1": 7,
"shot2": 8,
"shot3": 6,
"shot4": 5,
"shot5": 3
},
"3": {
"shot1": 10,
"shot2": 0,
"shot3": 6,
"shot4": 6,
"shot5": 8
},
"4": {
"shot1": 10,
"shot2": 4,
"shot3": 0,
"shot4": 1,
"shot5": 10
}
},
"total_score": 110,
"completed": true
},
"9": {
"name": "Janez Bo\u017ei\u010d",
"targets": {
"1": {
"shot1": 5,
"shot2": 10,
"shot3": 8,
"shot4": 5,
"shot5": 5
},
"2": {
"shot1": 8,
"shot2": 9,
"shot3": 10,
"shot4": 7,
"shot5": 10
},
"3": {
"shot1": 7,
"shot2": 5,
"shot3": 10,
"shot4": 4,
"shot5": 9
},
"4": {
"shot1": 8,
"shot2": 1,
"shot3": 6,
"shot4": 2,
"shot5": 5
}
},
"total_score": 134,
"completed": true
},
"48": {
"name": "Janja Salcman",
"targets": {
"1": {
"shot1": 10,
"shot2": 6,
"shot3": 4,
"shot4": 3,
"shot5": 9
},
"2": {
"shot1": 10,
"shot2": 5,
"shot3": 1,
"shot4": 1,
"shot5": 10
},
"3": {
"shot1": 9,
"shot2": 4,
"shot3": 0,
"shot4": 8,
"shot5": 9
},
"4": {
"shot1": 0,
"shot2": 1,
"shot3": 2,
"shot4": 0,
"shot5": 9
}
},
"total_score": 101,
"completed": true
},
"45": {
"name": "Lidija Blimen",
"targets": {
"1": {
"shot1": 2,
"shot2": 4,
"shot3": 1,
"shot4": 2,
"shot5": 6
},
"2": {
"shot1": 7,
"shot2": 0,
"shot3": 1,
"shot4": 2,
"shot5": 4
},
"3": {
"shot1": 4,
"shot2": 10,
"shot3": 2,
"shot4": 0,
"shot5": 7
},
"4": {
"shot1": 5,
"shot2": 7,
"shot3": 4,
"shot4": 7,
"shot5": 4
}
},
"total_score": 79,
"completed": true
},
"20": {
"name": "Jo\u017ee Preglav",
"targets": {
"1": {
"shot1": 7,
"shot2": 6,
"shot3": 7,
"shot4": 2,
"shot5": 4
},
"2": {
"shot1": 9,
"shot2": 4,
"shot3": 2,
"shot4": 0,
"shot5": 8
},
"3": {
"shot1": 5,
"shot2": 3,
"shot3": 8,
"shot4": 3,
"shot5": 4
},
"4": {
"shot1": 3,
"shot2": 10,
"shot3": 2,
"shot4": 9,
"shot5": 5
}
},
"total_score": 101,
"completed": true
},
"11": {
"name": "Rado Kefer",
"targets": {
"1": {
"shot1": 8,
"shot2": 10,
"shot3": 7,
"shot4": 10,
"shot5": 7
},
"2": {
"shot1": 10,
"shot2": 6,
"shot3": 0,
"shot4": 7,
"shot5": 4
},
"3": {
"shot1": 1,
"shot2": 6,
"shot3": 8,
"shot4": 9,
"shot5": 1
},
"4": {
"shot1": 0,
"shot2": 1,
"shot3": 0,
"shot4": 6,
"shot5": 9
}
},
"total_score": 110,
"completed": true
},
"17": {
"name": "Du\u0161an Onuk",
"targets": {
"1": {
"shot1": 0,
"shot2": 7,
"shot3": 0,
"shot4": 2,
"shot5": 2
},
"2": {
"shot1": 5,
"shot2": 1,
"shot3": 6,
"shot4": 4,
"shot5": 7
},
"3": {
"shot1": 7,
"shot2": 7,
"shot3": 0,
"shot4": 6,
"shot5": 3
},
"4": {
"shot1": 6,
"shot2": 4,
"shot3": 8,
"shot4": 9,
"shot5": 5
}
},
"total_score": 89,
"completed": true
},
"15": {
"name": "Jan Pleterski",
"targets": {
"1": {
"shot1": 1,
"shot2": 10,
"shot3": 8,
"shot4": 9,
"shot5": 8
},
"2": {
"shot1": 7,
"shot2": 2,
"shot3": 5,
"shot4": 4,
"shot5": 5
},
"3": {
"shot1": 10,
"shot2": 6,
"shot3": 1,
"shot4": 9,
"shot5": 10
},
"4": {
"shot1": 6,
"shot2": 2,
"shot3": 1,
"shot4": 2,
"shot5": 1
}
},
"total_score": 107,
"completed": true
},
"5": {
"name": "Jo\u017ee Verhnjak",
"targets": {
"1": {
"shot1": 1,
"shot2": 10,
"shot3": 10,
"shot4": 9,
"shot5": 1
},
"2": {
"shot1": 0,
"shot2": 10,
"shot3": 5,
"shot4": 10,
"shot5": 3
},
"3": {
"shot1": 8,
"shot2": 7,
"shot3": 4,
"shot4": 0,
"shot5": 4
},
"4": {
"shot1": 6,
"shot2": 1,
"shot3": 1,
"shot4": 9,
"shot5": 8
}
},
"total_score": 107,
"completed": true
},
"1": {
"name": "Domen Pleterski",
"targets": {
"1": {
"shot1": 0,
"shot2": 5,
"shot3": 10,
"shot4": 9,
"shot5": 6
},
"2": {
"shot1": 4,
"shot2": 9,
"shot3": 8,
"shot4": 6,
"shot5": 2
},
"3": {
"shot1": 7,
"shot2": 3,
"shot3": 7,
"shot4": 2,
"shot5": 0
},
"4": {
"shot1": 2,
"shot2": 4,
"shot3": 4,
"shot4": 5,
"shot5": 2
}
},
"total_score": 95,
"completed": true
},
"44": {
"name": "Anka Ka\u010dnik",
"targets": {
"1": {
"shot1": 1,
"shot2": 4,
"shot3": 10,
"shot4": 5,
"shot5": 9
},
"2": {
"shot1": 4,
"shot2": 5,
"shot3": 10,
"shot4": 3,
"shot5": 2
},
"3": {
"shot1": 6,
"shot2": 3,
"shot3": 7,
"shot4": 3,
"shot5": 5
},
"4": {
"shot1": 5,
"shot2": 10,
"shot3": 8,
"shot4": 10,
"shot5": 8
}
},
"total_score": 118,
"completed": true
},
"47": {
"name": "Ljuba Mr\u0161ak",
"targets": {
"1": {
"shot1": 5,
"shot2": 9,
"shot3": 6,
"shot4": 7,
"shot5": 0
},
"2": {
"shot1": 0,
"shot2": 4,
"shot3": 0,
"shot4": 2,
"shot5": 8
},
"3": {
"shot1": 2,
"shot2": 0,
"shot3": 6,
"shot4": 5,
"shot5": 7
},
"4": {
"shot1": 9,
"shot2": 1,
"shot3": 2,
"shot4": 0,
"shot5": 9
}
},
"total_score": 82,
"completed": true
},
"12": {
"name": "Matej Kvasnik",
"targets": {
"1": {
"shot1": 2,
"shot2": 7,
"shot3": 1,
"shot4": 0,
"shot5": 5
},
"2": {
"shot1": 10,
"shot2": 0,
"shot3": 3,
"shot4": 3,
"shot5": 6
},
"3": {
"shot1": 7,
"shot2": 2,
"shot3": 6,
"shot4": 5,
"shot5": 0
},
"4": {
"shot1": 10,
"shot2": 8,
"shot3": 8,
"shot4": 8,
"shot5": 9
}
},
"total_score": 100,
"completed": true
},
"49": {
"name": "Jolanda Verhnjak",
"targets": {
"1": {
"shot1": 5,
"shot2": 2,
"shot3": 0,
"shot4": 3,
"shot5": 2
},
"2": {
"shot1": 8,
"shot2": 0,
"shot3": 10,
"shot4": 5,
"shot5": 4
},
"3": {
"shot1": 2,
"shot2": 9,
"shot3": 4,
"shot4": 9,
"shot5": 2
},
"4": {
"shot1": 2,
"shot2": 9,
"shot3": 0,
"shot4": 4,
"shot5": 10
}
},
"total_score": 90,
"completed": true
}
},
"tournament_finished": true,
"created_at": "2025-09-08T19:19:14.422540",
"finished_at": "2025-09-08T19:19:26.929175"
},
"archived_at": "2025-09-08T19:19:26.929203"
}
@@ -1,759 +0,0 @@
{
"tournament": {
"rounds": [
{
"round_number": 1,
"players": [
{
"id": 9,
"name": "Janez Bo\u017ei\u010d",
"enabled": true
},
{
"id": 48,
"name": "Janja Salcman",
"enabled": true
},
{
"id": 4,
"name": "Mateja Pleterski",
"enabled": true
},
{
"id": 21,
"name": "Marko Blimen",
"enabled": true
},
{
"id": 45,
"name": "Lidija Blimen",
"enabled": true
},
{
"id": 1,
"name": "Domen Pleterski",
"enabled": true
}
],
"status": "pending"
},
{
"round_number": 2,
"players": [
{
"id": 44,
"name": "Anka Ka\u010dnik",
"enabled": true
},
{
"id": 12,
"name": "Matej Kvasnik",
"enabled": true
},
{
"id": 5,
"name": "Jo\u017ee Verhnjak",
"enabled": true
},
{
"id": 47,
"name": "Ljuba Mr\u0161ak",
"enabled": true
},
{
"id": 46,
"name": "Tijana \u0160tumpfl",
"enabled": true
},
{
"id": 49,
"name": "Jolanda Verhnjak",
"enabled": true
}
],
"status": "waiting"
},
{
"round_number": 3,
"players": [
{
"id": 17,
"name": "Du\u0161an Onuk",
"enabled": true
},
{
"id": 15,
"name": "Jan Pleterski",
"enabled": true
},
{
"id": 7,
"name": "Branko Poker\u017enik",
"enabled": true
},
{
"id": 20,
"name": "Jo\u017ee Preglav",
"enabled": true
},
{
"id": 11,
"name": "Rado Kefer",
"enabled": true
},
{
"id": 3,
"name": "Ivan Tandler",
"enabled": true
}
],
"status": "waiting"
}
],
"created_at": "2025-09-08T19:23:24.597390",
"total_players": 18,
"total_rounds": 3,
"current_round": 1,
"tournament_type": "4_targets"
},
"results": {
"tournament_id": "2025-09-08T19:23:24.597390",
"tournament_type": "4_targets",
"participants": {
"9": {
"name": "Janez Bo\u017ei\u010d",
"targets": {
"1": {
"shot1": 9,
"shot2": 2,
"shot3": 4,
"shot4": 6,
"shot5": 1
},
"2": {
"shot1": 1,
"shot2": 4,
"shot3": 4,
"shot4": 3,
"shot5": 4
},
"3": {
"shot1": 5,
"shot2": 4,
"shot3": 3,
"shot4": 3,
"shot5": 2
},
"4": {
"shot1": 3,
"shot2": 7,
"shot3": 2,
"shot4": 0,
"shot5": 0
}
},
"total_score": 67,
"completed": true
},
"48": {
"name": "Janja Salcman",
"targets": {
"1": {
"shot1": 6,
"shot2": 10,
"shot3": 0,
"shot4": 6,
"shot5": 5
},
"2": {
"shot1": 0,
"shot2": 9,
"shot3": 5,
"shot4": 4,
"shot5": 7
},
"3": {
"shot1": 8,
"shot2": 2,
"shot3": 8,
"shot4": 0,
"shot5": 1
},
"4": {
"shot1": 1,
"shot2": 4,
"shot3": 3,
"shot4": 5,
"shot5": 1
}
},
"total_score": 85,
"completed": true
},
"4": {
"name": "Mateja Pleterski",
"targets": {
"1": {
"shot1": 6,
"shot2": 10,
"shot3": 1,
"shot4": 1,
"shot5": 3
},
"2": {
"shot1": 3,
"shot2": 4,
"shot3": 6,
"shot4": 2,
"shot5": 10
},
"3": {
"shot1": 1,
"shot2": 10,
"shot3": 8,
"shot4": 5,
"shot5": 5
},
"4": {
"shot1": 9,
"shot2": 4,
"shot3": 9,
"shot4": 6,
"shot5": 3
}
},
"total_score": 106,
"completed": true
},
"21": {
"name": "Marko Blimen",
"targets": {
"1": {
"shot1": 3,
"shot2": 2,
"shot3": 3,
"shot4": 3,
"shot5": 3
},
"2": {
"shot1": 0,
"shot2": 6,
"shot3": 7,
"shot4": 0,
"shot5": 7
},
"3": {
"shot1": 3,
"shot2": 8,
"shot3": 1,
"shot4": 7,
"shot5": 6
},
"4": {
"shot1": 3,
"shot2": 4,
"shot3": 3,
"shot4": 8,
"shot5": 1
}
},
"total_score": 78,
"completed": true
},
"45": {
"name": "Lidija Blimen",
"targets": {
"1": {
"shot1": 8,
"shot2": 3,
"shot3": 8,
"shot4": 7,
"shot5": 7
},
"2": {
"shot1": 3,
"shot2": 0,
"shot3": 5,
"shot4": 7,
"shot5": 4
},
"3": {
"shot1": 7,
"shot2": 8,
"shot3": 0,
"shot4": 7,
"shot5": 3
},
"4": {
"shot1": 2,
"shot2": 10,
"shot3": 2,
"shot4": 9,
"shot5": 0
}
},
"total_score": 100,
"completed": true
},
"1": {
"name": "Domen Pleterski",
"targets": {
"1": {
"shot1": 9,
"shot2": 1,
"shot3": 3,
"shot4": 2,
"shot5": 4
},
"2": {
"shot1": 8,
"shot2": 4,
"shot3": 10,
"shot4": 9,
"shot5": 0
},
"3": {
"shot1": 7,
"shot2": 3,
"shot3": 2,
"shot4": 5,
"shot5": 6
},
"4": {
"shot1": 6,
"shot2": 6,
"shot3": 6,
"shot4": 3,
"shot5": 10
}
},
"total_score": 104,
"completed": true
},
"44": {
"name": "Anka Ka\u010dnik",
"targets": {
"1": {
"shot1": 6,
"shot2": 8,
"shot3": 6,
"shot4": 9,
"shot5": 2
},
"2": {
"shot1": 9,
"shot2": 0,
"shot3": 9,
"shot4": 2,
"shot5": 8
},
"3": {
"shot1": 5,
"shot2": 0,
"shot3": 4,
"shot4": 10,
"shot5": 3
},
"4": {
"shot1": 5,
"shot2": 4,
"shot3": 2,
"shot4": 7,
"shot5": 0
}
},
"total_score": 99,
"completed": true
},
"12": {
"name": "Matej Kvasnik",
"targets": {
"1": {
"shot1": 9,
"shot2": 1,
"shot3": 7,
"shot4": 9,
"shot5": 6
},
"2": {
"shot1": 10,
"shot2": 3,
"shot3": 2,
"shot4": 9,
"shot5": 2
},
"3": {
"shot1": 0,
"shot2": 9,
"shot3": 9,
"shot4": 3,
"shot5": 6
},
"4": {
"shot1": 4,
"shot2": 10,
"shot3": 6,
"shot4": 5,
"shot5": 2
}
},
"total_score": 112,
"completed": true
},
"5": {
"name": "Jo\u017ee Verhnjak",
"targets": {
"1": {
"shot1": 4,
"shot2": 9,
"shot3": 2,
"shot4": 4,
"shot5": 4
},
"2": {
"shot1": 3,
"shot2": 9,
"shot3": 4,
"shot4": 3,
"shot5": 1
},
"3": {
"shot1": 10,
"shot2": 7,
"shot3": 9,
"shot4": 0,
"shot5": 3
},
"4": {
"shot1": 10,
"shot2": 0,
"shot3": 8,
"shot4": 6,
"shot5": 10
}
},
"total_score": 106,
"completed": true
},
"47": {
"name": "Ljuba Mr\u0161ak",
"targets": {
"1": {
"shot1": 7,
"shot2": 10,
"shot3": 4,
"shot4": 2,
"shot5": 9
},
"2": {
"shot1": 1,
"shot2": 9,
"shot3": 2,
"shot4": 7,
"shot5": 0
},
"3": {
"shot1": 9,
"shot2": 8,
"shot3": 6,
"shot4": 5,
"shot5": 7
},
"4": {
"shot1": 9,
"shot2": 9,
"shot3": 10,
"shot4": 0,
"shot5": 2
}
},
"total_score": 116,
"completed": true
},
"46": {
"name": "Tijana \u0160tumpfl",
"targets": {
"1": {
"shot1": 10,
"shot2": 6,
"shot3": 9,
"shot4": 9,
"shot5": 7
},
"2": {
"shot1": 0,
"shot2": 6,
"shot3": 7,
"shot4": 6,
"shot5": 10
},
"3": {
"shot1": 3,
"shot2": 8,
"shot3": 6,
"shot4": 5,
"shot5": 5
},
"4": {
"shot1": 1,
"shot2": 2,
"shot3": 9,
"shot4": 8,
"shot5": 8
}
},
"total_score": 125,
"completed": true
},
"49": {
"name": "Jolanda Verhnjak",
"targets": {
"1": {
"shot1": 8,
"shot2": 0,
"shot3": 9,
"shot4": 6,
"shot5": 7
},
"2": {
"shot1": 4,
"shot2": 4,
"shot3": 6,
"shot4": 4,
"shot5": 8
},
"3": {
"shot1": 0,
"shot2": 9,
"shot3": 3,
"shot4": 4,
"shot5": 5
},
"4": {
"shot1": 2,
"shot2": 2,
"shot3": 0,
"shot4": 0,
"shot5": 0
}
},
"total_score": 81,
"completed": true
},
"17": {
"name": "Du\u0161an Onuk",
"targets": {
"1": {
"shot1": 3,
"shot2": 6,
"shot3": 5,
"shot4": 5,
"shot5": 6
},
"2": {
"shot1": 2,
"shot2": 4,
"shot3": 2,
"shot4": 8,
"shot5": 8
},
"3": {
"shot1": 3,
"shot2": 8,
"shot3": 5,
"shot4": 8,
"shot5": 1
},
"4": {
"shot1": 6,
"shot2": 5,
"shot3": 3,
"shot4": 1,
"shot5": 6
}
},
"total_score": 95,
"completed": true
},
"15": {
"name": "Jan Pleterski",
"targets": {
"1": {
"shot1": 9,
"shot2": 1,
"shot3": 7,
"shot4": 7,
"shot5": 6
},
"2": {
"shot1": 7,
"shot2": 2,
"shot3": 1,
"shot4": 10,
"shot5": 10
},
"3": {
"shot1": 9,
"shot2": 0,
"shot3": 3,
"shot4": 9,
"shot5": 10
},
"4": {
"shot1": 2,
"shot2": 2,
"shot3": 8,
"shot4": 4,
"shot5": 3
}
},
"total_score": 110,
"completed": true
},
"7": {
"name": "Branko Poker\u017enik",
"targets": {
"1": {
"shot1": 6,
"shot2": 1,
"shot3": 8,
"shot4": 9,
"shot5": 1
},
"2": {
"shot1": 6,
"shot2": 3,
"shot3": 10,
"shot4": 6,
"shot5": 0
},
"3": {
"shot1": 8,
"shot2": 6,
"shot3": 8,
"shot4": 0,
"shot5": 6
},
"4": {
"shot1": 2,
"shot2": 0,
"shot3": 10,
"shot4": 5,
"shot5": 6
}
},
"total_score": 101,
"completed": true
},
"20": {
"name": "Jo\u017ee Preglav",
"targets": {
"1": {
"shot1": 4,
"shot2": 1,
"shot3": 9,
"shot4": 2,
"shot5": 1
},
"2": {
"shot1": 0,
"shot2": 8,
"shot3": 0,
"shot4": 6,
"shot5": 5
},
"3": {
"shot1": 5,
"shot2": 7,
"shot3": 3,
"shot4": 1,
"shot5": 9
},
"4": {
"shot1": 4,
"shot2": 7,
"shot3": 3,
"shot4": 1,
"shot5": 1
}
},
"total_score": 77,
"completed": true
},
"11": {
"name": "Rado Kefer",
"targets": {
"1": {
"shot1": 1,
"shot2": 9,
"shot3": 7,
"shot4": 3,
"shot5": 10
},
"2": {
"shot1": 1,
"shot2": 2,
"shot3": 0,
"shot4": 2,
"shot5": 4
},
"3": {
"shot1": 1,
"shot2": 8,
"shot3": 10,
"shot4": 0,
"shot5": 0
},
"4": {
"shot1": 10,
"shot2": 2,
"shot3": 9,
"shot4": 1,
"shot5": 1
}
},
"total_score": 81,
"completed": true
},
"3": {
"name": "Ivan Tandler",
"targets": {
"1": {
"shot1": 5,
"shot2": 0,
"shot3": 9,
"shot4": 5,
"shot5": 7
},
"2": {
"shot1": 6,
"shot2": 0,
"shot3": 0,
"shot4": 1,
"shot5": 0
},
"3": {
"shot1": 4,
"shot2": 9,
"shot3": 5,
"shot4": 7,
"shot5": 3
},
"4": {
"shot1": 0,
"shot2": 5,
"shot3": 8,
"shot4": 5,
"shot5": 4
}
},
"total_score": 83,
"completed": true
}
},
"tournament_finished": true,
"created_at": "2025-09-08T19:23:24.597575",
"finished_at": "2025-09-08T19:23:34.271264"
},
"archived_at": "2025-09-08T19:23:34.271291"
}
@@ -1,759 +0,0 @@
{
"tournament": {
"rounds": [
{
"round_number": 1,
"players": [
{
"id": 44,
"name": "Anka Ka\u010dnik",
"enabled": true
},
{
"id": 9,
"name": "Janez Bo\u017ei\u010d",
"enabled": true
},
{
"id": 7,
"name": "Branko Poker\u017enik",
"enabled": true
},
{
"id": 1,
"name": "Domen Pleterski",
"enabled": true
},
{
"id": 21,
"name": "Marko Blimen",
"enabled": true
},
{
"id": 12,
"name": "Matej Kvasnik",
"enabled": true
}
],
"status": "pending"
},
{
"round_number": 2,
"players": [
{
"id": 48,
"name": "Janja Salcman",
"enabled": true
},
{
"id": 5,
"name": "Jo\u017ee Verhnjak",
"enabled": true
},
{
"id": 4,
"name": "Mateja Pleterski",
"enabled": true
},
{
"id": 20,
"name": "Jo\u017ee Preglav",
"enabled": true
},
{
"id": 15,
"name": "Jan Pleterski",
"enabled": true
},
{
"id": 17,
"name": "Du\u0161an Onuk",
"enabled": true
}
],
"status": "waiting"
},
{
"round_number": 3,
"players": [
{
"id": 46,
"name": "Tijana \u0160tumpfl",
"enabled": true
},
{
"id": 49,
"name": "Jolanda Verhnjak",
"enabled": true
},
{
"id": 3,
"name": "Ivan Tandler",
"enabled": true
},
{
"id": 11,
"name": "Rado Kefer",
"enabled": true
},
{
"id": 45,
"name": "Lidija Blimen",
"enabled": true
},
{
"id": 47,
"name": "Ljuba Mr\u0161ak",
"enabled": true
}
],
"status": "waiting"
}
],
"created_at": "2025-09-08T19:26:24.708355",
"total_players": 18,
"total_rounds": 3,
"current_round": 1,
"tournament_type": "4_targets"
},
"results": {
"tournament_id": "2025-09-08T19:26:24.708355",
"tournament_type": "4_targets",
"participants": {
"44": {
"name": "Anka Ka\u010dnik",
"targets": {
"1": {
"shot1": 8,
"shot2": 4,
"shot3": 0,
"shot4": 7,
"shot5": 4
},
"2": {
"shot1": 2,
"shot2": 7,
"shot3": 5,
"shot4": 0,
"shot5": 0
},
"3": {
"shot1": 0,
"shot2": 7,
"shot3": 8,
"shot4": 9,
"shot5": 7
},
"4": {
"shot1": 9,
"shot2": 6,
"shot3": 2,
"shot4": 4,
"shot5": 9
}
},
"total_score": 98,
"completed": true
},
"9": {
"name": "Janez Bo\u017ei\u010d",
"targets": {
"1": {
"shot1": 7,
"shot2": 7,
"shot3": 10,
"shot4": 4,
"shot5": 1
},
"2": {
"shot1": 9,
"shot2": 5,
"shot3": 5,
"shot4": 0,
"shot5": 4
},
"3": {
"shot1": 5,
"shot2": 1,
"shot3": 3,
"shot4": 4,
"shot5": 0
},
"4": {
"shot1": 6,
"shot2": 3,
"shot3": 3,
"shot4": 10,
"shot5": 7
}
},
"total_score": 94,
"completed": true
},
"7": {
"name": "Branko Poker\u017enik",
"targets": {
"1": {
"shot1": 9,
"shot2": 5,
"shot3": 10,
"shot4": 8,
"shot5": 3
},
"2": {
"shot1": 4,
"shot2": 4,
"shot3": 4,
"shot4": 7,
"shot5": 4
},
"3": {
"shot1": 8,
"shot2": 2,
"shot3": 5,
"shot4": 0,
"shot5": 9
},
"4": {
"shot1": 0,
"shot2": 4,
"shot3": 2,
"shot4": 4,
"shot5": 6
}
},
"total_score": 98,
"completed": true
},
"1": {
"name": "Domen Pleterski",
"targets": {
"1": {
"shot1": 0,
"shot2": 8,
"shot3": 8,
"shot4": 5,
"shot5": 3
},
"2": {
"shot1": 1,
"shot2": 7,
"shot3": 3,
"shot4": 4,
"shot5": 4
},
"3": {
"shot1": 1,
"shot2": 2,
"shot3": 0,
"shot4": 5,
"shot5": 5
},
"4": {
"shot1": 2,
"shot2": 8,
"shot3": 8,
"shot4": 7,
"shot5": 8
}
},
"total_score": 89,
"completed": true
},
"21": {
"name": "Marko Blimen",
"targets": {
"1": {
"shot1": 8,
"shot2": 8,
"shot3": 2,
"shot4": 8,
"shot5": 0
},
"2": {
"shot1": 1,
"shot2": 9,
"shot3": 4,
"shot4": 10,
"shot5": 1
},
"3": {
"shot1": 0,
"shot2": 4,
"shot3": 6,
"shot4": 8,
"shot5": 1
},
"4": {
"shot1": 10,
"shot2": 1,
"shot3": 7,
"shot4": 10,
"shot5": 9
}
},
"total_score": 107,
"completed": true
},
"12": {
"name": "Matej Kvasnik",
"targets": {
"1": {
"shot1": 3,
"shot2": 10,
"shot3": 0,
"shot4": 0,
"shot5": 0
},
"2": {
"shot1": 4,
"shot2": 7,
"shot3": 9,
"shot4": 7,
"shot5": 2
},
"3": {
"shot1": 8,
"shot2": 8,
"shot3": 3,
"shot4": 7,
"shot5": 8
},
"4": {
"shot1": 6,
"shot2": 7,
"shot3": 9,
"shot4": 7,
"shot5": 10
}
},
"total_score": 115,
"completed": true
},
"48": {
"name": "Janja Salcman",
"targets": {
"1": {
"shot1": 0,
"shot2": 1,
"shot3": 4,
"shot4": 8,
"shot5": 2
},
"2": {
"shot1": 3,
"shot2": 0,
"shot3": 0,
"shot4": 6,
"shot5": 0
},
"3": {
"shot1": 7,
"shot2": 6,
"shot3": 4,
"shot4": 3,
"shot5": 6
},
"4": {
"shot1": 5,
"shot2": 9,
"shot3": 10,
"shot4": 6,
"shot5": 7
}
},
"total_score": 87,
"completed": true
},
"5": {
"name": "Jo\u017ee Verhnjak",
"targets": {
"1": {
"shot1": 2,
"shot2": 8,
"shot3": 4,
"shot4": 10,
"shot5": 0
},
"2": {
"shot1": 0,
"shot2": 2,
"shot3": 5,
"shot4": 5,
"shot5": 1
},
"3": {
"shot1": 3,
"shot2": 0,
"shot3": 8,
"shot4": 5,
"shot5": 5
},
"4": {
"shot1": 9,
"shot2": 10,
"shot3": 5,
"shot4": 1,
"shot5": 6
}
},
"total_score": 89,
"completed": true
},
"4": {
"name": "Mateja Pleterski",
"targets": {
"1": {
"shot1": 6,
"shot2": 10,
"shot3": 1,
"shot4": 7,
"shot5": 0
},
"2": {
"shot1": 9,
"shot2": 1,
"shot3": 9,
"shot4": 2,
"shot5": 6
},
"3": {
"shot1": 7,
"shot2": 1,
"shot3": 4,
"shot4": 1,
"shot5": 9
},
"4": {
"shot1": 2,
"shot2": 6,
"shot3": 8,
"shot4": 4,
"shot5": 4
}
},
"total_score": 97,
"completed": true
},
"20": {
"name": "Jo\u017ee Preglav",
"targets": {
"1": {
"shot1": 5,
"shot2": 10,
"shot3": 4,
"shot4": 8,
"shot5": 9
},
"2": {
"shot1": 7,
"shot2": 6,
"shot3": 0,
"shot4": 6,
"shot5": 0
},
"3": {
"shot1": 6,
"shot2": 9,
"shot3": 1,
"shot4": 6,
"shot5": 1
},
"4": {
"shot1": 1,
"shot2": 2,
"shot3": 5,
"shot4": 0,
"shot5": 6
}
},
"total_score": 92,
"completed": true
},
"15": {
"name": "Jan Pleterski",
"targets": {
"1": {
"shot1": 0,
"shot2": 6,
"shot3": 9,
"shot4": 3,
"shot5": 8
},
"2": {
"shot1": 3,
"shot2": 0,
"shot3": 6,
"shot4": 4,
"shot5": 0
},
"3": {
"shot1": 3,
"shot2": 0,
"shot3": 4,
"shot4": 4,
"shot5": 10
},
"4": {
"shot1": 6,
"shot2": 9,
"shot3": 8,
"shot4": 0,
"shot5": 1
}
},
"total_score": 84,
"completed": true
},
"17": {
"name": "Du\u0161an Onuk",
"targets": {
"1": {
"shot1": 7,
"shot2": 5,
"shot3": 2,
"shot4": 7,
"shot5": 5
},
"2": {
"shot1": 9,
"shot2": 3,
"shot3": 8,
"shot4": 0,
"shot5": 6
},
"3": {
"shot1": 1,
"shot2": 6,
"shot3": 5,
"shot4": 5,
"shot5": 7
},
"4": {
"shot1": 3,
"shot2": 7,
"shot3": 6,
"shot4": 7,
"shot5": 7
}
},
"total_score": 106,
"completed": true
},
"46": {
"name": "Tijana \u0160tumpfl",
"targets": {
"1": {
"shot1": 5,
"shot2": 4,
"shot3": 4,
"shot4": 9,
"shot5": 3
},
"2": {
"shot1": 7,
"shot2": 0,
"shot3": 5,
"shot4": 4,
"shot5": 7
},
"3": {
"shot1": 2,
"shot2": 4,
"shot3": 7,
"shot4": 5,
"shot5": 9
},
"4": {
"shot1": 1,
"shot2": 4,
"shot3": 0,
"shot4": 1,
"shot5": 6
}
},
"total_score": 87,
"completed": true
},
"49": {
"name": "Jolanda Verhnjak",
"targets": {
"1": {
"shot1": 9,
"shot2": 10,
"shot3": 5,
"shot4": 8,
"shot5": 2
},
"2": {
"shot1": 4,
"shot2": 8,
"shot3": 8,
"shot4": 5,
"shot5": 1
},
"3": {
"shot1": 3,
"shot2": 5,
"shot3": 2,
"shot4": 6,
"shot5": 7
},
"4": {
"shot1": 3,
"shot2": 2,
"shot3": 1,
"shot4": 1,
"shot5": 0
}
},
"total_score": 90,
"completed": true
},
"3": {
"name": "Ivan Tandler",
"targets": {
"1": {
"shot1": 8,
"shot2": 10,
"shot3": 0,
"shot4": 8,
"shot5": 0
},
"2": {
"shot1": 3,
"shot2": 9,
"shot3": 3,
"shot4": 6,
"shot5": 6
},
"3": {
"shot1": 10,
"shot2": 8,
"shot3": 5,
"shot4": 1,
"shot5": 10
},
"4": {
"shot1": 9,
"shot2": 7,
"shot3": 8,
"shot4": 6,
"shot5": 9
}
},
"total_score": 126,
"completed": true
},
"11": {
"name": "Rado Kefer",
"targets": {
"1": {
"shot1": 10,
"shot2": 7,
"shot3": 3,
"shot4": 8,
"shot5": 6
},
"2": {
"shot1": 9,
"shot2": 1,
"shot3": 5,
"shot4": 3,
"shot5": 8
},
"3": {
"shot1": 2,
"shot2": 3,
"shot3": 5,
"shot4": 2,
"shot5": 1
},
"4": {
"shot1": 1,
"shot2": 0,
"shot3": 3,
"shot4": 1,
"shot5": 6
}
},
"total_score": 84,
"completed": true
},
"45": {
"name": "Lidija Blimen",
"targets": {
"1": {
"shot1": 9,
"shot2": 1,
"shot3": 8,
"shot4": 5,
"shot5": 8
},
"2": {
"shot1": 5,
"shot2": 7,
"shot3": 8,
"shot4": 2,
"shot5": 9
},
"3": {
"shot1": 4,
"shot2": 10,
"shot3": 5,
"shot4": 7,
"shot5": 7
},
"4": {
"shot1": 0,
"shot2": 1,
"shot3": 0,
"shot4": 4,
"shot5": 0
}
},
"total_score": 100,
"completed": true
},
"47": {
"name": "Ljuba Mr\u0161ak",
"targets": {
"1": {
"shot1": 5,
"shot2": 0,
"shot3": 7,
"shot4": 4,
"shot5": 1
},
"2": {
"shot1": 2,
"shot2": 7,
"shot3": 3,
"shot4": 1,
"shot5": 4
},
"3": {
"shot1": 1,
"shot2": 4,
"shot3": 1,
"shot4": 3,
"shot5": 1
},
"4": {
"shot1": 0,
"shot2": 5,
"shot3": 3,
"shot4": 9,
"shot5": 7
}
},
"total_score": 68,
"completed": true
}
},
"tournament_finished": true,
"created_at": "2025-09-08T19:26:24.708574",
"finished_at": "2025-09-08T19:26:31.871139"
},
"archived_at": "2025-09-08T19:26:31.871173"
}
@@ -1,759 +0,0 @@
{
"tournament": {
"rounds": [
{
"round_number": 1,
"players": [
{
"id": 5,
"name": "Jo\u017ee Verhnjak",
"enabled": true
},
{
"id": 20,
"name": "Jo\u017ee Preglav",
"enabled": true
},
{
"id": 12,
"name": "Matej Kvasnik",
"enabled": true
},
{
"id": 9,
"name": "Janez Bo\u017ei\u010d",
"enabled": true
},
{
"id": 21,
"name": "Marko Blimen",
"enabled": true
},
{
"id": 17,
"name": "Du\u0161an Onuk",
"enabled": true
}
],
"status": "pending"
},
{
"round_number": 2,
"players": [
{
"id": 45,
"name": "Lidija Blimen",
"enabled": true
},
{
"id": 47,
"name": "Ljuba Mr\u0161ak",
"enabled": true
},
{
"id": 3,
"name": "Ivan Tandler",
"enabled": true
},
{
"id": 49,
"name": "Jolanda Verhnjak",
"enabled": true
},
{
"id": 15,
"name": "Jan Pleterski",
"enabled": true
},
{
"id": 1,
"name": "Domen Pleterski",
"enabled": true
}
],
"status": "waiting"
},
{
"round_number": 3,
"players": [
{
"id": 7,
"name": "Branko Poker\u017enik",
"enabled": true
},
{
"id": 4,
"name": "Mateja Pleterski",
"enabled": true
},
{
"id": 11,
"name": "Rado Kefer",
"enabled": true
},
{
"id": 44,
"name": "Anka Ka\u010dnik",
"enabled": true
},
{
"id": 46,
"name": "Tijana \u0160tumpfl",
"enabled": true
},
{
"id": 48,
"name": "Janja Salcman",
"enabled": true
}
],
"status": "waiting"
}
],
"created_at": "2025-09-08T19:27:18.397859",
"total_players": 18,
"total_rounds": 3,
"current_round": 1,
"tournament_type": "4_targets"
},
"results": {
"tournament_id": "2025-09-08T19:27:18.397859",
"tournament_type": "4_targets",
"participants": {
"5": {
"name": "Jo\u017ee Verhnjak",
"targets": {
"1": {
"shot1": 6,
"shot2": 0,
"shot3": 1,
"shot4": 2,
"shot5": 0
},
"2": {
"shot1": 4,
"shot2": 9,
"shot3": 1,
"shot4": 4,
"shot5": 3
},
"3": {
"shot1": 3,
"shot2": 6,
"shot3": 8,
"shot4": 6,
"shot5": 3
},
"4": {
"shot1": 8,
"shot2": 7,
"shot3": 8,
"shot4": 1,
"shot5": 5
}
},
"total_score": 85,
"completed": true
},
"20": {
"name": "Jo\u017ee Preglav",
"targets": {
"1": {
"shot1": 3,
"shot2": 0,
"shot3": 3,
"shot4": 5,
"shot5": 3
},
"2": {
"shot1": 1,
"shot2": 4,
"shot3": 2,
"shot4": 0,
"shot5": 1
},
"3": {
"shot1": 6,
"shot2": 3,
"shot3": 8,
"shot4": 2,
"shot5": 5
},
"4": {
"shot1": 4,
"shot2": 6,
"shot3": 2,
"shot4": 9,
"shot5": 2
}
},
"total_score": 69,
"completed": true
},
"12": {
"name": "Matej Kvasnik",
"targets": {
"1": {
"shot1": 1,
"shot2": 4,
"shot3": 2,
"shot4": 6,
"shot5": 6
},
"2": {
"shot1": 5,
"shot2": 1,
"shot3": 2,
"shot4": 0,
"shot5": 8
},
"3": {
"shot1": 1,
"shot2": 4,
"shot3": 7,
"shot4": 3,
"shot5": 10
},
"4": {
"shot1": 0,
"shot2": 3,
"shot3": 3,
"shot4": 9,
"shot5": 5
}
},
"total_score": 80,
"completed": true
},
"9": {
"name": "Janez Bo\u017ei\u010d",
"targets": {
"1": {
"shot1": 10,
"shot2": 0,
"shot3": 6,
"shot4": 7,
"shot5": 6
},
"2": {
"shot1": 0,
"shot2": 7,
"shot3": 10,
"shot4": 10,
"shot5": 4
},
"3": {
"shot1": 9,
"shot2": 1,
"shot3": 3,
"shot4": 9,
"shot5": 6
},
"4": {
"shot1": 1,
"shot2": 9,
"shot3": 6,
"shot4": 5,
"shot5": 9
}
},
"total_score": 118,
"completed": true
},
"21": {
"name": "Marko Blimen",
"targets": {
"1": {
"shot1": 9,
"shot2": 8,
"shot3": 3,
"shot4": 0,
"shot5": 4
},
"2": {
"shot1": 9,
"shot2": 9,
"shot3": 9,
"shot4": 10,
"shot5": 3
},
"3": {
"shot1": 9,
"shot2": 4,
"shot3": 8,
"shot4": 10,
"shot5": 9
},
"4": {
"shot1": 4,
"shot2": 8,
"shot3": 1,
"shot4": 1,
"shot5": 0
}
},
"total_score": 118,
"completed": true
},
"17": {
"name": "Du\u0161an Onuk",
"targets": {
"1": {
"shot1": 5,
"shot2": 10,
"shot3": 8,
"shot4": 5,
"shot5": 1
},
"2": {
"shot1": 6,
"shot2": 3,
"shot3": 4,
"shot4": 9,
"shot5": 6
},
"3": {
"shot1": 7,
"shot2": 8,
"shot3": 1,
"shot4": 3,
"shot5": 4
},
"4": {
"shot1": 1,
"shot2": 4,
"shot3": 6,
"shot4": 9,
"shot5": 0
}
},
"total_score": 100,
"completed": true
},
"45": {
"name": "Lidija Blimen",
"targets": {
"1": {
"shot1": 1,
"shot2": 6,
"shot3": 0,
"shot4": 0,
"shot5": 8
},
"2": {
"shot1": 7,
"shot2": 7,
"shot3": 2,
"shot4": 10,
"shot5": 2
},
"3": {
"shot1": 2,
"shot2": 10,
"shot3": 1,
"shot4": 8,
"shot5": 10
},
"4": {
"shot1": 2,
"shot2": 8,
"shot3": 0,
"shot4": 8,
"shot5": 1
}
},
"total_score": 93,
"completed": true
},
"47": {
"name": "Ljuba Mr\u0161ak",
"targets": {
"1": {
"shot1": 9,
"shot2": 7,
"shot3": 0,
"shot4": 8,
"shot5": 2
},
"2": {
"shot1": 0,
"shot2": 0,
"shot3": 5,
"shot4": 4,
"shot5": 4
},
"3": {
"shot1": 4,
"shot2": 3,
"shot3": 9,
"shot4": 0,
"shot5": 0
},
"4": {
"shot1": 0,
"shot2": 3,
"shot3": 8,
"shot4": 7,
"shot5": 8
}
},
"total_score": 81,
"completed": true
},
"3": {
"name": "Ivan Tandler",
"targets": {
"1": {
"shot1": 7,
"shot2": 3,
"shot3": 5,
"shot4": 1,
"shot5": 0
},
"2": {
"shot1": 0,
"shot2": 5,
"shot3": 8,
"shot4": 10,
"shot5": 9
},
"3": {
"shot1": 7,
"shot2": 0,
"shot3": 0,
"shot4": 3,
"shot5": 6
},
"4": {
"shot1": 1,
"shot2": 8,
"shot3": 5,
"shot4": 0,
"shot5": 8
}
},
"total_score": 86,
"completed": true
},
"49": {
"name": "Jolanda Verhnjak",
"targets": {
"1": {
"shot1": 7,
"shot2": 2,
"shot3": 0,
"shot4": 8,
"shot5": 3
},
"2": {
"shot1": 10,
"shot2": 9,
"shot3": 7,
"shot4": 8,
"shot5": 6
},
"3": {
"shot1": 0,
"shot2": 2,
"shot3": 5,
"shot4": 3,
"shot5": 9
},
"4": {
"shot1": 2,
"shot2": 5,
"shot3": 10,
"shot4": 3,
"shot5": 5
}
},
"total_score": 104,
"completed": true
},
"15": {
"name": "Jan Pleterski",
"targets": {
"1": {
"shot1": 3,
"shot2": 0,
"shot3": 3,
"shot4": 2,
"shot5": 1
},
"2": {
"shot1": 10,
"shot2": 2,
"shot3": 10,
"shot4": 0,
"shot5": 6
},
"3": {
"shot1": 2,
"shot2": 9,
"shot3": 0,
"shot4": 4,
"shot5": 4
},
"4": {
"shot1": 2,
"shot2": 3,
"shot3": 8,
"shot4": 9,
"shot5": 5
}
},
"total_score": 83,
"completed": true
},
"1": {
"name": "Domen Pleterski",
"targets": {
"1": {
"shot1": 8,
"shot2": 5,
"shot3": 10,
"shot4": 8,
"shot5": 4
},
"2": {
"shot1": 0,
"shot2": 3,
"shot3": 6,
"shot4": 5,
"shot5": 2
},
"3": {
"shot1": 8,
"shot2": 0,
"shot3": 10,
"shot4": 2,
"shot5": 0
},
"4": {
"shot1": 0,
"shot2": 6,
"shot3": 7,
"shot4": 4,
"shot5": 8
}
},
"total_score": 96,
"completed": true
},
"7": {
"name": "Branko Poker\u017enik",
"targets": {
"1": {
"shot1": 9,
"shot2": 8,
"shot3": 4,
"shot4": 1,
"shot5": 3
},
"2": {
"shot1": 4,
"shot2": 5,
"shot3": 10,
"shot4": 0,
"shot5": 0
},
"3": {
"shot1": 6,
"shot2": 2,
"shot3": 9,
"shot4": 4,
"shot5": 2
},
"4": {
"shot1": 2,
"shot2": 8,
"shot3": 1,
"shot4": 8,
"shot5": 3
}
},
"total_score": 89,
"completed": true
},
"4": {
"name": "Mateja Pleterski",
"targets": {
"1": {
"shot1": 9,
"shot2": 10,
"shot3": 1,
"shot4": 6,
"shot5": 8
},
"2": {
"shot1": 10,
"shot2": 1,
"shot3": 0,
"shot4": 9,
"shot5": 10
},
"3": {
"shot1": 0,
"shot2": 1,
"shot3": 5,
"shot4": 5,
"shot5": 0
},
"4": {
"shot1": 3,
"shot2": 0,
"shot3": 10,
"shot4": 4,
"shot5": 2
}
},
"total_score": 94,
"completed": true
},
"11": {
"name": "Rado Kefer",
"targets": {
"1": {
"shot1": 7,
"shot2": 2,
"shot3": 7,
"shot4": 7,
"shot5": 9
},
"2": {
"shot1": 7,
"shot2": 8,
"shot3": 4,
"shot4": 6,
"shot5": 0
},
"3": {
"shot1": 2,
"shot2": 8,
"shot3": 5,
"shot4": 2,
"shot5": 10
},
"4": {
"shot1": 6,
"shot2": 5,
"shot3": 6,
"shot4": 5,
"shot5": 10
}
},
"total_score": 116,
"completed": true
},
"44": {
"name": "Anka Ka\u010dnik",
"targets": {
"1": {
"shot1": 10,
"shot2": 1,
"shot3": 5,
"shot4": 1,
"shot5": 8
},
"2": {
"shot1": 3,
"shot2": 4,
"shot3": 3,
"shot4": 8,
"shot5": 6
},
"3": {
"shot1": 5,
"shot2": 7,
"shot3": 10,
"shot4": 2,
"shot5": 6
},
"4": {
"shot1": 10,
"shot2": 8,
"shot3": 2,
"shot4": 9,
"shot5": 7
}
},
"total_score": 115,
"completed": true
},
"46": {
"name": "Tijana \u0160tumpfl",
"targets": {
"1": {
"shot1": 8,
"shot2": 6,
"shot3": 5,
"shot4": 0,
"shot5": 2
},
"2": {
"shot1": 3,
"shot2": 8,
"shot3": 6,
"shot4": 4,
"shot5": 1
},
"3": {
"shot1": 10,
"shot2": 10,
"shot3": 8,
"shot4": 7,
"shot5": 4
},
"4": {
"shot1": 7,
"shot2": 2,
"shot3": 2,
"shot4": 4,
"shot5": 3
}
},
"total_score": 100,
"completed": true
},
"48": {
"name": "Janja Salcman",
"targets": {
"1": {
"shot1": 5,
"shot2": 0,
"shot3": 1,
"shot4": 1,
"shot5": 4
},
"2": {
"shot1": 7,
"shot2": 4,
"shot3": 10,
"shot4": 2,
"shot5": 6
},
"3": {
"shot1": 6,
"shot2": 3,
"shot3": 2,
"shot4": 3,
"shot5": 5
},
"4": {
"shot1": 0,
"shot2": 8,
"shot3": 9,
"shot4": 6,
"shot5": 3
}
},
"total_score": 85,
"completed": true
}
},
"tournament_finished": true,
"created_at": "2025-09-08T19:27:18.398031",
"finished_at": "2025-09-08T19:27:27.631332"
},
"archived_at": "2025-09-08T19:27:27.631357"
}
@@ -1,759 +0,0 @@
{
"tournament": {
"rounds": [
{
"round_number": 1,
"players": [
{
"id": 49,
"name": "Jolanda Verhnjak",
"enabled": true
},
{
"id": 7,
"name": "Branko Poker\u017enik",
"enabled": true
},
{
"id": 3,
"name": "Ivan Tandler",
"enabled": true
},
{
"id": 46,
"name": "Tijana \u0160tumpfl",
"enabled": true
},
{
"id": 15,
"name": "Jan Pleterski",
"enabled": true
},
{
"id": 17,
"name": "Du\u0161an Onuk",
"enabled": true
}
],
"status": "pending"
},
{
"round_number": 2,
"players": [
{
"id": 12,
"name": "Matej Kvasnik",
"enabled": true
},
{
"id": 9,
"name": "Janez Bo\u017ei\u010d",
"enabled": true
},
{
"id": 4,
"name": "Mateja Pleterski",
"enabled": true
},
{
"id": 21,
"name": "Marko Blimen",
"enabled": true
},
{
"id": 20,
"name": "Jo\u017ee Preglav",
"enabled": true
},
{
"id": 11,
"name": "Rado Kefer",
"enabled": true
}
],
"status": "waiting"
},
{
"round_number": 3,
"players": [
{
"id": 47,
"name": "Ljuba Mr\u0161ak",
"enabled": true
},
{
"id": 48,
"name": "Janja Salcman",
"enabled": true
},
{
"id": 45,
"name": "Lidija Blimen",
"enabled": true
},
{
"id": 1,
"name": "Domen Pleterski",
"enabled": true
},
{
"id": 5,
"name": "Jo\u017ee Verhnjak",
"enabled": true
},
{
"id": 44,
"name": "Anka Ka\u010dnik",
"enabled": true
}
],
"status": "waiting"
}
],
"created_at": "2025-09-08T19:39:27.077222",
"total_players": 18,
"total_rounds": 3,
"current_round": 1,
"tournament_type": "4_targets"
},
"results": {
"tournament_id": "2025-09-08T19:39:27.077222",
"tournament_type": "4_targets",
"participants": {
"49": {
"name": "Jolanda Verhnjak",
"targets": {
"1": {
"shot1": 6,
"shot2": 10,
"shot3": 6,
"shot4": 3,
"shot5": 7
},
"2": {
"shot1": 6,
"shot2": 2,
"shot3": 1,
"shot4": 10,
"shot5": 0
},
"3": {
"shot1": 5,
"shot2": 5,
"shot3": 0,
"shot4": 3,
"shot5": 9
},
"4": {
"shot1": 9,
"shot2": 6,
"shot3": 8,
"shot4": 4,
"shot5": 8
}
},
"total_score": 108,
"completed": true
},
"7": {
"name": "Branko Poker\u017enik",
"targets": {
"1": {
"shot1": 10,
"shot2": 10,
"shot3": 6,
"shot4": 0,
"shot5": 9
},
"2": {
"shot1": 1,
"shot2": 2,
"shot3": 1,
"shot4": 4,
"shot5": 6
},
"3": {
"shot1": 3,
"shot2": 10,
"shot3": 5,
"shot4": 7,
"shot5": 7
},
"4": {
"shot1": 7,
"shot2": 7,
"shot3": 3,
"shot4": 2,
"shot5": 10
}
},
"total_score": 110,
"completed": true
},
"3": {
"name": "Ivan Tandler",
"targets": {
"1": {
"shot1": 6,
"shot2": 4,
"shot3": 1,
"shot4": 2,
"shot5": 0
},
"2": {
"shot1": 7,
"shot2": 6,
"shot3": 5,
"shot4": 10,
"shot5": 8
},
"3": {
"shot1": 4,
"shot2": 1,
"shot3": 4,
"shot4": 3,
"shot5": 7
},
"4": {
"shot1": 7,
"shot2": 2,
"shot3": 1,
"shot4": 1,
"shot5": 10
}
},
"total_score": 89,
"completed": true
},
"46": {
"name": "Tijana \u0160tumpfl",
"targets": {
"1": {
"shot1": 7,
"shot2": 10,
"shot3": 3,
"shot4": 7,
"shot5": 6
},
"2": {
"shot1": 5,
"shot2": 0,
"shot3": 10,
"shot4": 5,
"shot5": 1
},
"3": {
"shot1": 7,
"shot2": 9,
"shot3": 8,
"shot4": 6,
"shot5": 1
},
"4": {
"shot1": 3,
"shot2": 9,
"shot3": 0,
"shot4": 5,
"shot5": 0
}
},
"total_score": 102,
"completed": true
},
"15": {
"name": "Jan Pleterski",
"targets": {
"1": {
"shot1": 1,
"shot2": 4,
"shot3": 3,
"shot4": 1,
"shot5": 6
},
"2": {
"shot1": 5,
"shot2": 8,
"shot3": 1,
"shot4": 9,
"shot5": 2
},
"3": {
"shot1": 6,
"shot2": 9,
"shot3": 4,
"shot4": 1,
"shot5": 7
},
"4": {
"shot1": 5,
"shot2": 0,
"shot3": 2,
"shot4": 7,
"shot5": 4
}
},
"total_score": 85,
"completed": true
},
"17": {
"name": "Du\u0161an Onuk",
"targets": {
"1": {
"shot1": 6,
"shot2": 7,
"shot3": 8,
"shot4": 2,
"shot5": 8
},
"2": {
"shot1": 10,
"shot2": 7,
"shot3": 6,
"shot4": 9,
"shot5": 0
},
"3": {
"shot1": 1,
"shot2": 9,
"shot3": 0,
"shot4": 6,
"shot5": 4
},
"4": {
"shot1": 9,
"shot2": 5,
"shot3": 6,
"shot4": 3,
"shot5": 6
}
},
"total_score": 112,
"completed": true
},
"12": {
"name": "Matej Kvasnik",
"targets": {
"1": {
"shot1": 1,
"shot2": 0,
"shot3": 9,
"shot4": 2,
"shot5": 7
},
"2": {
"shot1": 7,
"shot2": 2,
"shot3": 4,
"shot4": 5,
"shot5": 5
},
"3": {
"shot1": 1,
"shot2": 0,
"shot3": 8,
"shot4": 2,
"shot5": 2
},
"4": {
"shot1": 6,
"shot2": 6,
"shot3": 5,
"shot4": 2,
"shot5": 8
}
},
"total_score": 82,
"completed": true
},
"9": {
"name": "Janez Bo\u017ei\u010d",
"targets": {
"1": {
"shot1": 9,
"shot2": 9,
"shot3": 7,
"shot4": 7,
"shot5": 9
},
"2": {
"shot1": 6,
"shot2": 0,
"shot3": 5,
"shot4": 1,
"shot5": 9
},
"3": {
"shot1": 8,
"shot2": 5,
"shot3": 10,
"shot4": 4,
"shot5": 4
},
"4": {
"shot1": 1,
"shot2": 2,
"shot3": 8,
"shot4": 1,
"shot5": 5
}
},
"total_score": 110,
"completed": true
},
"4": {
"name": "Mateja Pleterski",
"targets": {
"1": {
"shot1": 0,
"shot2": 10,
"shot3": 2,
"shot4": 4,
"shot5": 0
},
"2": {
"shot1": 0,
"shot2": 7,
"shot3": 4,
"shot4": 7,
"shot5": 0
},
"3": {
"shot1": 1,
"shot2": 0,
"shot3": 5,
"shot4": 6,
"shot5": 1
},
"4": {
"shot1": 4,
"shot2": 10,
"shot3": 4,
"shot4": 7,
"shot5": 2
}
},
"total_score": 74,
"completed": true
},
"21": {
"name": "Marko Blimen",
"targets": {
"1": {
"shot1": 8,
"shot2": 5,
"shot3": 4,
"shot4": 7,
"shot5": 8
},
"2": {
"shot1": 3,
"shot2": 1,
"shot3": 7,
"shot4": 2,
"shot5": 9
},
"3": {
"shot1": 1,
"shot2": 0,
"shot3": 2,
"shot4": 6,
"shot5": 5
},
"4": {
"shot1": 8,
"shot2": 3,
"shot3": 0,
"shot4": 3,
"shot5": 9
}
},
"total_score": 91,
"completed": true
},
"20": {
"name": "Jo\u017ee Preglav",
"targets": {
"1": {
"shot1": 1,
"shot2": 5,
"shot3": 0,
"shot4": 5,
"shot5": 9
},
"2": {
"shot1": 6,
"shot2": 5,
"shot3": 1,
"shot4": 8,
"shot5": 6
},
"3": {
"shot1": 4,
"shot2": 6,
"shot3": 10,
"shot4": 9,
"shot5": 2
},
"4": {
"shot1": 5,
"shot2": 9,
"shot3": 6,
"shot4": 3,
"shot5": 8
}
},
"total_score": 108,
"completed": true
},
"11": {
"name": "Rado Kefer",
"targets": {
"1": {
"shot1": 3,
"shot2": 5,
"shot3": 5,
"shot4": 1,
"shot5": 9
},
"2": {
"shot1": 2,
"shot2": 10,
"shot3": 8,
"shot4": 6,
"shot5": 1
},
"3": {
"shot1": 7,
"shot2": 9,
"shot3": 7,
"shot4": 9,
"shot5": 1
},
"4": {
"shot1": 0,
"shot2": 2,
"shot3": 9,
"shot4": 1,
"shot5": 3
}
},
"total_score": 98,
"completed": true
},
"47": {
"name": "Ljuba Mr\u0161ak",
"targets": {
"1": {
"shot1": 0,
"shot2": 2,
"shot3": 9,
"shot4": 7,
"shot5": 7
},
"2": {
"shot1": 7,
"shot2": 0,
"shot3": 0,
"shot4": 2,
"shot5": 3
},
"3": {
"shot1": 9,
"shot2": 9,
"shot3": 0,
"shot4": 6,
"shot5": 4
},
"4": {
"shot1": 2,
"shot2": 10,
"shot3": 9,
"shot4": 7,
"shot5": 7
}
},
"total_score": 100,
"completed": true
},
"48": {
"name": "Janja Salcman",
"targets": {
"1": {
"shot1": 8,
"shot2": 5,
"shot3": 3,
"shot4": 2,
"shot5": 2
},
"2": {
"shot1": 10,
"shot2": 6,
"shot3": 10,
"shot4": 4,
"shot5": 7
},
"3": {
"shot1": 10,
"shot2": 0,
"shot3": 6,
"shot4": 3,
"shot5": 3
},
"4": {
"shot1": 10,
"shot2": 0,
"shot3": 9,
"shot4": 8,
"shot5": 1
}
},
"total_score": 107,
"completed": true
},
"45": {
"name": "Lidija Blimen",
"targets": {
"1": {
"shot1": 8,
"shot2": 4,
"shot3": 8,
"shot4": 2,
"shot5": 1
},
"2": {
"shot1": 2,
"shot2": 4,
"shot3": 6,
"shot4": 7,
"shot5": 0
},
"3": {
"shot1": 7,
"shot2": 3,
"shot3": 10,
"shot4": 7,
"shot5": 8
},
"4": {
"shot1": 1,
"shot2": 2,
"shot3": 10,
"shot4": 4,
"shot5": 6
}
},
"total_score": 100,
"completed": true
},
"1": {
"name": "Domen Pleterski",
"targets": {
"1": {
"shot1": 9,
"shot2": 2,
"shot3": 10,
"shot4": 2,
"shot5": 2
},
"2": {
"shot1": 5,
"shot2": 4,
"shot3": 10,
"shot4": 10,
"shot5": 7
},
"3": {
"shot1": 9,
"shot2": 5,
"shot3": 5,
"shot4": 1,
"shot5": 9
},
"4": {
"shot1": 5,
"shot2": 8,
"shot3": 2,
"shot4": 10,
"shot5": 2
}
},
"total_score": 117,
"completed": true
},
"5": {
"name": "Jo\u017ee Verhnjak",
"targets": {
"1": {
"shot1": 2,
"shot2": 5,
"shot3": 3,
"shot4": 6,
"shot5": 5
},
"2": {
"shot1": 9,
"shot2": 7,
"shot3": 0,
"shot4": 2,
"shot5": 5
},
"3": {
"shot1": 5,
"shot2": 7,
"shot3": 0,
"shot4": 4,
"shot5": 2
},
"4": {
"shot1": 0,
"shot2": 7,
"shot3": 1,
"shot4": 4,
"shot5": 2
}
},
"total_score": 76,
"completed": true
},
"44": {
"name": "Anka Ka\u010dnik",
"targets": {
"1": {
"shot1": 5,
"shot2": 3,
"shot3": 2,
"shot4": 5,
"shot5": 1
},
"2": {
"shot1": 6,
"shot2": 7,
"shot3": 10,
"shot4": 10,
"shot5": 4
},
"3": {
"shot1": 1,
"shot2": 3,
"shot3": 1,
"shot4": 9,
"shot5": 0
},
"4": {
"shot1": 9,
"shot2": 10,
"shot3": 5,
"shot4": 10,
"shot5": 1
}
},
"total_score": 102,
"completed": true
}
},
"tournament_finished": true,
"created_at": "2025-09-08T19:39:27.077445",
"finished_at": "2025-09-08T19:39:44.030962"
},
"archived_at": "2025-09-08T19:39:44.030989"
}
@@ -1,759 +0,0 @@
{
"tournament": {
"rounds": [
{
"round_number": 1,
"players": [
{
"id": 12,
"name": "Matej Kvasnik",
"enabled": true
},
{
"id": 21,
"name": "Marko Blimen",
"enabled": true
},
{
"id": 1,
"name": "Domen Pleterski",
"enabled": true
},
{
"id": 4,
"name": "Mateja Pleterski",
"enabled": true
},
{
"id": 5,
"name": "Jo\u017ee Verhnjak",
"enabled": true
},
{
"id": 11,
"name": "Rado Kefer",
"enabled": true
}
],
"status": "pending"
},
{
"round_number": 2,
"players": [
{
"id": 44,
"name": "Anka Ka\u010dnik",
"enabled": true
},
{
"id": 48,
"name": "Janja Salcman",
"enabled": true
},
{
"id": 3,
"name": "Ivan Tandler",
"enabled": true
},
{
"id": 7,
"name": "Branko Poker\u017enik",
"enabled": true
},
{
"id": 9,
"name": "Janez Bo\u017ei\u010d",
"enabled": true
},
{
"id": 45,
"name": "Lidija Blimen",
"enabled": true
}
],
"status": "waiting"
},
{
"round_number": 3,
"players": [
{
"id": 47,
"name": "Ljuba Mr\u0161ak",
"enabled": true
},
{
"id": 46,
"name": "Tijana \u0160tumpfl",
"enabled": true
},
{
"id": 20,
"name": "Jo\u017ee Preglav",
"enabled": true
},
{
"id": 17,
"name": "Du\u0161an Onuk",
"enabled": true
},
{
"id": 15,
"name": "Jan Pleterski",
"enabled": true
},
{
"id": 49,
"name": "Jolanda Verhnjak",
"enabled": true
}
],
"status": "waiting"
}
],
"created_at": "2025-09-08T20:15:56.266521",
"total_players": 18,
"total_rounds": 3,
"current_round": 1,
"tournament_type": "4_targets"
},
"results": {
"tournament_id": "2025-09-08T20:15:56.266521",
"tournament_type": "4_targets",
"participants": {
"12": {
"name": "Matej Kvasnik",
"targets": {
"1": {
"shot1": 1,
"shot2": 6,
"shot3": 5,
"shot4": 1,
"shot5": 3
},
"2": {
"shot1": 8,
"shot2": 3,
"shot3": 3,
"shot4": 10,
"shot5": 4
},
"3": {
"shot1": 10,
"shot2": 0,
"shot3": 0,
"shot4": 2,
"shot5": 6
},
"4": {
"shot1": 7,
"shot2": 5,
"shot3": 0,
"shot4": 2,
"shot5": 5
}
},
"total_score": 81,
"completed": true
},
"21": {
"name": "Marko Blimen",
"targets": {
"1": {
"shot1": 1,
"shot2": 1,
"shot3": 0,
"shot4": 0,
"shot5": 5
},
"2": {
"shot1": 9,
"shot2": 0,
"shot3": 0,
"shot4": 2,
"shot5": 3
},
"3": {
"shot1": 3,
"shot2": 8,
"shot3": 2,
"shot4": 1,
"shot5": 0
},
"4": {
"shot1": 4,
"shot2": 6,
"shot3": 3,
"shot4": 9,
"shot5": 0
}
},
"total_score": 57,
"completed": true
},
"1": {
"name": "Domen Pleterski",
"targets": {
"1": {
"shot1": 0,
"shot2": 4,
"shot3": 0,
"shot4": 9,
"shot5": 10
},
"2": {
"shot1": 0,
"shot2": 3,
"shot3": 0,
"shot4": 1,
"shot5": 10
},
"3": {
"shot1": 5,
"shot2": 3,
"shot3": 4,
"shot4": 6,
"shot5": 10
},
"4": {
"shot1": 0,
"shot2": 2,
"shot3": 1,
"shot4": 8,
"shot5": 2
}
},
"total_score": 78,
"completed": true
},
"4": {
"name": "Mateja Pleterski",
"targets": {
"1": {
"shot1": 2,
"shot2": 0,
"shot3": 2,
"shot4": 3,
"shot5": 3
},
"2": {
"shot1": 7,
"shot2": 2,
"shot3": 9,
"shot4": 8,
"shot5": 6
},
"3": {
"shot1": 3,
"shot2": 8,
"shot3": 1,
"shot4": 1,
"shot5": 3
},
"4": {
"shot1": 10,
"shot2": 5,
"shot3": 10,
"shot4": 5,
"shot5": 7
}
},
"total_score": 95,
"completed": true
},
"5": {
"name": "Jo\u017ee Verhnjak",
"targets": {
"1": {
"shot1": 8,
"shot2": 3,
"shot3": 3,
"shot4": 2,
"shot5": 6
},
"2": {
"shot1": 7,
"shot2": 6,
"shot3": 7,
"shot4": 5,
"shot5": 9
},
"3": {
"shot1": 2,
"shot2": 0,
"shot3": 7,
"shot4": 6,
"shot5": 2
},
"4": {
"shot1": 6,
"shot2": 1,
"shot3": 5,
"shot4": 7,
"shot5": 5
}
},
"total_score": 97,
"completed": true
},
"11": {
"name": "Rado Kefer",
"targets": {
"1": {
"shot1": 4,
"shot2": 2,
"shot3": 3,
"shot4": 7,
"shot5": 2
},
"2": {
"shot1": 9,
"shot2": 6,
"shot3": 10,
"shot4": 7,
"shot5": 6
},
"3": {
"shot1": 8,
"shot2": 6,
"shot3": 9,
"shot4": 9,
"shot5": 1
},
"4": {
"shot1": 3,
"shot2": 0,
"shot3": 6,
"shot4": 8,
"shot5": 2
}
},
"total_score": 108,
"completed": true
},
"44": {
"name": "Anka Ka\u010dnik",
"targets": {
"1": {
"shot1": 8,
"shot2": 8,
"shot3": 4,
"shot4": 10,
"shot5": 3
},
"2": {
"shot1": 2,
"shot2": 8,
"shot3": 10,
"shot4": 7,
"shot5": 9
},
"3": {
"shot1": 3,
"shot2": 2,
"shot3": 0,
"shot4": 6,
"shot5": 9
},
"4": {
"shot1": 9,
"shot2": 5,
"shot3": 10,
"shot4": 10,
"shot5": 6
}
},
"total_score": 129,
"completed": true
},
"48": {
"name": "Janja Salcman",
"targets": {
"1": {
"shot1": 2,
"shot2": 6,
"shot3": 9,
"shot4": 4,
"shot5": 0
},
"2": {
"shot1": 0,
"shot2": 7,
"shot3": 3,
"shot4": 2,
"shot5": 0
},
"3": {
"shot1": 4,
"shot2": 6,
"shot3": 4,
"shot4": 8,
"shot5": 8
},
"4": {
"shot1": 2,
"shot2": 7,
"shot3": 0,
"shot4": 10,
"shot5": 3
}
},
"total_score": 85,
"completed": true
},
"3": {
"name": "Ivan Tandler",
"targets": {
"1": {
"shot1": 10,
"shot2": 9,
"shot3": 8,
"shot4": 8,
"shot5": 2
},
"2": {
"shot1": 7,
"shot2": 10,
"shot3": 3,
"shot4": 9,
"shot5": 2
},
"3": {
"shot1": 9,
"shot2": 4,
"shot3": 0,
"shot4": 2,
"shot5": 6
},
"4": {
"shot1": 0,
"shot2": 4,
"shot3": 1,
"shot4": 2,
"shot5": 10
}
},
"total_score": 106,
"completed": true
},
"7": {
"name": "Branko Poker\u017enik",
"targets": {
"1": {
"shot1": 4,
"shot2": 0,
"shot3": 2,
"shot4": 3,
"shot5": 6
},
"2": {
"shot1": 4,
"shot2": 5,
"shot3": 4,
"shot4": 10,
"shot5": 10
},
"3": {
"shot1": 2,
"shot2": 1,
"shot3": 0,
"shot4": 4,
"shot5": 5
},
"4": {
"shot1": 7,
"shot2": 10,
"shot3": 8,
"shot4": 5,
"shot5": 10
}
},
"total_score": 100,
"completed": true
},
"9": {
"name": "Janez Bo\u017ei\u010d",
"targets": {
"1": {
"shot1": 3,
"shot2": 9,
"shot3": 10,
"shot4": 5,
"shot5": 3
},
"2": {
"shot1": 5,
"shot2": 9,
"shot3": 6,
"shot4": 8,
"shot5": 5
},
"3": {
"shot1": 1,
"shot2": 7,
"shot3": 8,
"shot4": 1,
"shot5": 9
},
"4": {
"shot1": 6,
"shot2": 3,
"shot3": 10,
"shot4": 9,
"shot5": 4
}
},
"total_score": 121,
"completed": true
},
"45": {
"name": "Lidija Blimen",
"targets": {
"1": {
"shot1": 2,
"shot2": 9,
"shot3": 10,
"shot4": 2,
"shot5": 10
},
"2": {
"shot1": 8,
"shot2": 3,
"shot3": 7,
"shot4": 6,
"shot5": 8
},
"3": {
"shot1": 0,
"shot2": 7,
"shot3": 4,
"shot4": 1,
"shot5": 1
},
"4": {
"shot1": 2,
"shot2": 2,
"shot3": 4,
"shot4": 10,
"shot5": 1
}
},
"total_score": 97,
"completed": true
},
"47": {
"name": "Ljuba Mr\u0161ak",
"targets": {
"1": {
"shot1": 9,
"shot2": 1,
"shot3": 1,
"shot4": 5,
"shot5": 9
},
"2": {
"shot1": 2,
"shot2": 10,
"shot3": 10,
"shot4": 0,
"shot5": 9
},
"3": {
"shot1": 1,
"shot2": 5,
"shot3": 4,
"shot4": 10,
"shot5": 0
},
"4": {
"shot1": 3,
"shot2": 3,
"shot3": 2,
"shot4": 2,
"shot5": 2
}
},
"total_score": 88,
"completed": true
},
"46": {
"name": "Tijana \u0160tumpfl",
"targets": {
"1": {
"shot1": 1,
"shot2": 3,
"shot3": 2,
"shot4": 3,
"shot5": 3
},
"2": {
"shot1": 9,
"shot2": 5,
"shot3": 7,
"shot4": 7,
"shot5": 9
},
"3": {
"shot1": 3,
"shot2": 8,
"shot3": 1,
"shot4": 5,
"shot5": 6
},
"4": {
"shot1": 3,
"shot2": 0,
"shot3": 9,
"shot4": 9,
"shot5": 7
}
},
"total_score": 100,
"completed": true
},
"20": {
"name": "Jo\u017ee Preglav",
"targets": {
"1": {
"shot1": 4,
"shot2": 4,
"shot3": 9,
"shot4": 5,
"shot5": 2
},
"2": {
"shot1": 6,
"shot2": 0,
"shot3": 10,
"shot4": 0,
"shot5": 5
},
"3": {
"shot1": 7,
"shot2": 8,
"shot3": 9,
"shot4": 6,
"shot5": 5
},
"4": {
"shot1": 8,
"shot2": 7,
"shot3": 5,
"shot4": 5,
"shot5": 10
}
},
"total_score": 115,
"completed": true
},
"17": {
"name": "Du\u0161an Onuk",
"targets": {
"1": {
"shot1": 0,
"shot2": 3,
"shot3": 3,
"shot4": 2,
"shot5": 2
},
"2": {
"shot1": 2,
"shot2": 6,
"shot3": 1,
"shot4": 10,
"shot5": 10
},
"3": {
"shot1": 1,
"shot2": 7,
"shot3": 4,
"shot4": 5,
"shot5": 5
},
"4": {
"shot1": 10,
"shot2": 2,
"shot3": 8,
"shot4": 7,
"shot5": 9
}
},
"total_score": 97,
"completed": true
},
"15": {
"name": "Jan Pleterski",
"targets": {
"1": {
"shot1": 0,
"shot2": 0,
"shot3": 2,
"shot4": 0,
"shot5": 4
},
"2": {
"shot1": 1,
"shot2": 7,
"shot3": 8,
"shot4": 7,
"shot5": 9
},
"3": {
"shot1": 2,
"shot2": 8,
"shot3": 7,
"shot4": 2,
"shot5": 5
},
"4": {
"shot1": 9,
"shot2": 5,
"shot3": 1,
"shot4": 7,
"shot5": 3
}
},
"total_score": 87,
"completed": true
},
"49": {
"name": "Jolanda Verhnjak",
"targets": {
"1": {
"shot1": 8,
"shot2": 2,
"shot3": 7,
"shot4": 5,
"shot5": 0
},
"2": {
"shot1": 1,
"shot2": 8,
"shot3": 0,
"shot4": 4,
"shot5": 10
},
"3": {
"shot1": 0,
"shot2": 4,
"shot3": 4,
"shot4": 7,
"shot5": 4
},
"4": {
"shot1": 5,
"shot2": 6,
"shot3": 4,
"shot4": 6,
"shot5": 0
}
},
"total_score": 85,
"completed": true
}
},
"tournament_finished": true,
"created_at": "2025-09-08T20:15:56.266721",
"finished_at": "2025-09-08T20:16:01.584621"
},
"archived_at": "2025-09-08T20:16:01.584647"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+43 -4
View File
@@ -18,10 +18,12 @@
"all": "All",
"status": "Status",
"actions": "Actions",
"select": "Select",
"select_all": "Select All",
"enable_selected": "Enable Selected",
"disable_selected": "Disable Selected",
"print": "Print",
"export": "Export JSON",
"visible": "Visible",
"date": "Date",
"view": "View",
@@ -75,6 +77,7 @@
"tournament_management": "Tournament Management",
"tournament_type": "Tournament Type",
"participants": "Participants",
"rounds": "Rounds",
"start_tournament": "Start Tournament",
"reset_tournament": "Reset Tournament",
"tournament_setup": "Tournament Setup",
@@ -94,7 +97,8 @@
"created": "Created",
"tournaments": "Tournaments",
"league_tournament": "League Tournament",
"finished": "Finished"
"finished": "Finished",
"view_current_results": "View Current Standings"
},
"tournament_types": {
"4_targets": "4 Targets",
@@ -127,7 +131,7 @@
"final_rankings": "Final Rankings",
"final_rankings_best_4_of_5": "Final Rankings - Best 4 of 5 Tournaments",
"best_4_of_5": "Best 4 of 5 Tournaments",
"league_champions": "League Champions",
"league_champions": "🎖️ League Champions",
"participant": "Participant",
"tournament_scores": "Tournament Scores",
"final": "Final",
@@ -140,6 +144,33 @@
"highest_score": "Highest Score",
"average_final": "Average Final",
"5_tournament_league": "5 Tournament League - Best 4 Count",
"combine_leagues": "Combine Leagues",
"combine_mode": "Combine Leagues",
"convert_mode": "Convert Tournaments to League",
"upload_league_files": "Upload League JSON Files",
"upload_tournament_files": "Upload Tournament JSON Files",
"combine_leagues_desc": "Upload multiple league JSON files to combine them into a single results table.",
"convert_tournaments_desc": "Upload 1-5 tournament JSON files to create a league. You can upload partial leagues as tournaments complete. Final scoring uses best 4 results when all 5 are uploaded.",
"click_browse": "Click to browse",
"drag_drop_files": "or drag and drop league JSON files here",
"supports_multiple": "Supports multiple file selection",
"clear_all": "Clear All",
"combine_preview": "Combine & Preview",
"convert_to_league": "Convert to League",
"valid_league": "Valid League",
"valid_tournament": "Valid Tournament",
"participants": "participants",
"tournaments": "tournaments",
"invalid_format": "Invalid file format",
"select_mode": "Select Mode",
"start_over": "Start Over",
"export_combined_results": "Export Combined Results",
"combined_league_info": "Combined League Information",
"uploaded_files": "Uploaded Files",
"remove": "Remove",
"type": "Type",
"failed_parse_json": "Failed to parse JSON",
"invalid_tournament_format": "Invalid tournament file format",
"joker_used_badge": "Joker Used",
"search_players_placeholder": "Search players by name...",
"no_players_found": "No players found matching your search criteria.",
@@ -166,7 +197,9 @@
"system": "System",
"camera": "Camera",
"tournaments": "Tournaments",
"results.most_tens": "Most 10s"
"results.most_tens": "Most 10s",
"progress": "Progress",
"combine_leagues": "Combine Leagues"
},
"results": {
"results": "Results",
@@ -182,12 +215,15 @@
"average_score": "Average Score",
"highest_score": "Highest Score",
"worst_score": "Worst Score",
"hit_rate": "Hit Rate",
"most_common": "Most Common",
"completed": "Completed",
"position": "Position",
"points": "Points",
"top_3_winners": "Top 3 Winners"
},
"players": {
"participants": "Participants",
"player": "Player",
"players": "Players",
"player_name": "Player Name",
@@ -270,7 +306,10 @@
"finish_tournament_button": "Finish Tournament & Show Results",
"enter_scores_40_targets": "Enter scores for each participant (40 targets, 2 shots each). Score 0 = miss.",
"enter_scores_20_targets": "Enter scores for each participant (20 targets, 2 shots each). Score 0 = miss.",
"enter_scores_4_targets": "Enter scores for each participant (4 targets, 5 shots each). Score 0 = miss."
"enter_scores_4_targets": "Enter scores for each participant (4 targets, 5 shots each). Score 0 = miss.",
"fill_zeros": "Fill Zeros",
"clear_all_scores": "Clear all scores",
"fill_empty_with_zero": "Fill empty entries with 0"
},
"mobile": {
"mobile_streams": "Mobile Streams",
+33 -5
View File
@@ -18,10 +18,12 @@
"all": "Vse",
"status": "Status",
"actions": "Dejanja",
"select": "Izberi",
"select_all": "Izberi Vse",
"enable_selected": "Omogoči Izbrane",
"disable_selected": "Onemogoči Izbrane",
"print": "Natisni",
"export": "Izvozi JSON",
"visible": "Vidni",
"date": "Datum",
"view": "Oglej",
@@ -74,6 +76,7 @@
"tournament_management": "Upravljanje Turnirja",
"tournament_type": "Tip Turnirja",
"participants": "Udeleženci",
"rounds": "Krogi",
"start_tournament": "Začni Turnir",
"reset_tournament": "Ponastavi Turnir",
"tournament_setup": "Nastavitev Turnirja",
@@ -95,7 +98,8 @@
"league_completed": "Liga Zaključena",
"section_title": "Sekcija",
"finished": "Zaključeno",
"targets": "Število Tarč"
"targets": "Število Tarč",
"view_current_results": "Oglej Trenutne Rezultate"
},
"tournament_types": {
"4_targets": "4 Tarče",
@@ -132,7 +136,7 @@
"league_complete_info": "Vsi turnirji načrtovani. Zaključi trenutnega za končne rezultate.",
"view_league_results": "Oglej si Rezultate Lige",
"score_tournament": "Točkuj Turnir",
"league_champions": "Ligaški Prvaki",
"league_champions": "🎖️ Ligaški Prvaki",
"participant": "Udeleženec",
"tournament_scores": "Rezultati Turnirjev",
"final": "Končni",
@@ -171,8 +175,26 @@
"system": "Sistem",
"camera": "Kamera",
"tournaments": "Turnirji",
"results.most_tens": "Največ Desetk"
"results.most_tens": "Največ Desetk",
"progress": "Stanje",
"combine_leagues": "Združi Lige",
"combine_mode": "Združi Lige",
"convert_mode": "Pretvori Turnirje v Ligo",
"upload_league_files": "Naloži Datoteke Lig",
"upload_tournament_files": "Naloži Datoteke Turnirjev",
"combine_leagues_desc": "Naloži več datotek lig JSON, da jih združiš v eno tabelo rezultatov.",
"convert_tournaments_desc": "Naloži 1-5 datotek turnirjev JSON za ustvarjanje lige. Lahko naložiš delne lige ko turnirji napredujejo. Končno točkovanje uporablja najboljše 4 rezultate, ko so vseh 5 naloženih.",
"click_browse": "Klikni za brskanje",
"drag_drop_files": "ali povleci in spusti datoteke JSON lig tukaj",
"supports_multiple": "Podpira izbiro več datotek",
"combine_preview": "Združi in Predoglej",
"convert_to_league": "Pretvori v Ligo",
"valid_league": "Veljavna Liga",
"valid_tournament": "Veljaven Turnir",
"participants": "udeleženci",
"invalid_format": "Neveljavna oblika datoteke",
"select_mode": "Izberi Način",
"start_over": "Začni Znova"
},
"results": {
"results": "Rezultati",
@@ -188,12 +210,15 @@
"average_score": "Povprečni Rezultat",
"highest_score": "Najvišji Rezultat",
"worst_score": "Najslabši Rezultat",
"hit_rate": "Stopnja Uspešnosti",
"most_common": "Najpogostejši",
"completed": "Zaključeno",
"position": "Uvrstitev",
"points": "Točke",
"top_3_winners": "Top 3 Zmagovalci"
},
"players": {
"participants": "Udeleženci",
"player": "Igralec",
"players": "Igralci",
"player_name": "Ime Igralca",
@@ -276,7 +301,10 @@
"finish_tournament_button": "Zaključi Turnir & Prikaži Rezultate",
"enter_scores_40_targets": "Vnesi rezultate za vsakega udeleženca (40 tarč, 2 strela na tarčo). Rezultat 0 = zgrešeno.",
"enter_scores_20_targets": "Vnesi rezultate za vsakega udeleženca (20 tarč, 2 strela na tarčo). Rezultat 0 = zgrešeno.",
"enter_scores_4_targets": "Vnesi rezultate za vsakega udeleženca (4 tarče, 5 strelov na tarčo). Rezultat 0 = zgrešeno."
"enter_scores_4_targets": "Vnesi rezultate za vsakega udeleženca (4 tarče, 5 strelov na tarčo). Rezultat 0 = zgrešeno.",
"fill_zeros": "Napolni Ničle",
"clear_all_scores": "Počisti vse rezultate",
"fill_empty_with_zero": "Napolni prazne vnose z 0"
},
"mobile": {
"mobile_streams": "Mobilni Prenosi",
+7
View File
@@ -275,3 +275,10 @@ document.addEventListener('DOMContentLoaded', initI18n);
window.t = t;
window.changeLanguage = changeLanguage;
window.createLanguageSelector = createLanguageSelector;
// Export i18n object with useful functions
window.i18n = {
t: t,
updatePageTranslations: translatePage,
changeLanguage: changeLanguage
};
+361 -240
View File
@@ -24,45 +24,40 @@
height: calc(100vh - 70px);
display: flex;
flex-direction: column;
padding: 15px;
gap: 15px;
padding: 0px;
gap: 0px;
}
.tournament-header {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 15px 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
padding: 10px 16px;
flex-shrink: 0;
position: relative;
}
.tournament-header .nav-btn {
position: absolute;
top: 10px;
right: 15px;
margin: 0;
margin: 8px 15px 0px 15px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.tournament-title {
font-size: 1.5rem;
font-weight: bold;
font-size: 1rem;
font-weight: 700;
color: #333;
margin-bottom: 6px;
white-space: nowrap;
}
.tournament-stats {
color: #666;
font-size: 0.9rem;
margin-bottom: 12px;
color: #888;
font-size: 0.82rem;
}
.tournament-controls {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
gap: 10px;
margin-left: auto;
}
.round-nav-btn {
@@ -93,7 +88,7 @@
.current-round-display {
font-size: 1rem;
font-weight: bold;
color: #28a745;
color: #333;
margin: 0 15px;
min-width: 140px;
text-align: center;
@@ -107,82 +102,123 @@
gap: 8px;
min-height: 0;
overflow-y: auto;
padding: 5px;
overflow-x: hidden;
padding: 10px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
margin: 8px 15px 8px 15px;
}
/* Custom scrollbar styling */
.rounds-container::-webkit-scrollbar {
width: 12px;
}
.rounds-container::-webkit-scrollbar-track {
background: #f5f5f5;
border-radius: 10px;
}
.rounds-container::-webkit-scrollbar-thumb {
background: #5a8fd1;
border-radius: 10px;
border: 2px solid #f5f5f5;
}
.rounds-container::-webkit-scrollbar-thumb:hover {
background: #4a7db8;
}
.round-row {
background: white;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
overflow: hidden;
transition: all 0.2s ease;
transition: all 0.3s ease;
display: flex;
flex-direction: row;
flex-shrink: 0;
min-height: 0;
border: 2px solid #e9ecef;
border-left: 5px solid #9ca3af;
}
.round-row.current {
border-left: 4px solid #28a745;
box-shadow: 0 4px 15px rgba(0, 123, 255, 0.25);
border-left: 5px solid #5a8fd1;
border-color: #5a8fd1;
box-shadow: 0 6px 24px rgba(90, 143, 209, 0.25);
}
.round-row.completed {
border-left: 4px solid #28a745;
opacity: 0.95;
border-left: 5px solid #28a745;
border-color: #28a745;
opacity: 0.98;
box-shadow: 0 4px 16px rgba(40, 167, 69, 0.15);
}
.round-row.waiting {
opacity: 0.8;
border-color: #d1d5db;
}
.round-header {
background: #f8f9fa;
border-right: 1px solid #e9ecef;
background: linear-gradient(135deg, #f8f9fa 0%, #f0f1f3 100%);
border-right: 3px solid #e9ecef;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 10px 15px;
min-width: 120px;
padding: 14px 16px;
flex-shrink: 0;
text-align: center;
min-width: 130px;
}
.round-row.current .round-header {
background: #e3f2fd;
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-right-color: #5a8fd1;
}
.round-row.completed .round-header {
background: #e8f5e8;
background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%);
border-right-color: #28a745;
}
.round-title {
font-size: 1rem;
font-weight: bold;
font-size: 1.05rem;
font-weight: 700;
color: #333;
margin-bottom: 4px;
margin-bottom: 6px;
letter-spacing: 0.3px;
}
.round-badge {
padding: 3px 8px;
border-radius: 10px;
font-size: 0.65rem;
font-weight: bold;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.current-badge {
background: #28a745;
background: #5a8fd1;
color: white;
box-shadow: 0 2px 8px rgba(90, 143, 209, 0.3);
}
.completed-badge {
background: #28a745;
color: white;
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
}
.waiting-badge {
background: #6c757d;
background: #9ca3af;
color: white;
box-shadow: 0 2px 6px rgba(156, 163, 175, 0.2);
}
.positions-container {
@@ -191,6 +227,7 @@
grid-template-columns: repeat(6, 1fr);
gap: 8px;
padding: 12px;
overflow: hidden;
}
.position-card {
@@ -201,49 +238,68 @@
display: flex;
flex-direction: row;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(156, 163, 175, 0.2);
min-height: 70px;
box-shadow: 0 2px 8px rgba(156, 163, 175, 0.18);
min-height: 80px;
flex-shrink: 0;
}
.position-card:hover {
transform: translateY(-2px);
box-shadow: 0 3px 12px rgba(156, 163, 175, 0.28);
}
/* Current round - blue cards */
.round-row.current .position-card {
border-color: #5a8fd1;
box-shadow: 0 2px 6px rgba(90, 143, 209, 0.2);
box-shadow: 0 2px 8px rgba(90, 143, 209, 0.25);
}
.round-row.current .position-card:hover {
box-shadow: 0 3px 12px rgba(90, 143, 209, 0.35);
}
.round-row.current .position-card .position-header {
background: #5a8fd1;
background: linear-gradient(135deg, #5a8fd1 0%, #4a7db8 100%);
}
/* Completed round - green cards */
.round-row.completed .position-card {
border-color: #28a745;
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.25);
}
.round-row.completed .position-card:hover {
box-shadow: 0 3px 12px rgba(40, 167, 69, 0.35);
}
.round-row.completed .position-card .position-header {
background: #28a745;
background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
}
/* Waiting round - gray cards */
.round-row.waiting .position-card {
border-color: #9ca3af;
box-shadow: 0 2px 6px rgba(156, 163, 175, 0.2);
box-shadow: 0 2px 8px rgba(156, 163, 175, 0.2);
}
.round-row.waiting .position-card:hover {
box-shadow: 0 3px 10px rgba(156, 163, 175, 0.3);
}
.round-row.waiting .position-card .position-header {
background: #9ca3af;
background: linear-gradient(135deg, #9ca3af 0%, #8b929f 100%);
}
.position-header {
background: #9ca3af;
padding: 0;
padding: 12px 0;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
min-width: 50px;
width: 50px;
min-height: 80px;
}
.position-card.empty .position-header {
@@ -251,14 +307,14 @@
}
.position-number {
font-size: 1.6rem;
font-size: 2rem;
font-weight: 700;
color: white;
line-height: 1;
}
.position-body {
padding: 10px 12px;
padding: 12px;
display: flex;
flex-direction: column;
justify-content: center;
@@ -266,6 +322,7 @@
flex: 1;
background: white;
border-radius: 0 8px 8px 0;
overflow: visible;
}
.position-card.empty .position-body {
@@ -276,10 +333,13 @@
font-size: 1.15rem;
font-weight: 600;
color: #2c3e50;
line-height: 1.3;
line-height: 1.4;
word-wrap: break-word;
max-width: 100%;
text-align: center;
white-space: normal;
overflow: visible;
text-overflow: clip;
}
.position-card.empty .player-name {
@@ -321,6 +381,14 @@
display: none !important;
}
.print-logo {
display: block !important;
height: 80px !important;
max-width: 300px !important;
object-fit: contain !important;
margin: 0 auto 16px auto !important;
}
html, body {
height: auto !important;
overflow: visible !important;
@@ -344,20 +412,47 @@
page-break-inside: avoid;
}
.tournament-title {
font-size: 24pt !important;
font-weight: bold !important;
color: #333 !important;
margin-bottom: 10px;
}
.tournament-title,
.tournament-stats {
font-size: 12pt !important;
color: #666 !important;
margin-bottom: 0 !important;
display: none !important;
}
.current-round-info {
.tournament-header {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: space-between !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
border-radius: 0 !important;
padding: 0 0 16px 0 !important;
margin-bottom: 16px !important;
border-bottom: 1px solid #ddd !important;
}
.print-logo {
display: block !important;
height: 80px !important;
max-width: 300px !important;
object-fit: contain !important;
margin: 0 !important;
}
.print-meta {
display: flex !important;
flex-direction: column !important;
align-items: flex-end !important;
font-size: 11pt !important;
color: #555 !important;
gap: 4px;
}
.current-round-info,
.round-badge,
.round-print-info,
.player-id,
.no-tournament {
display: none !important;
}
@@ -365,175 +460,106 @@
display: block !important;
overflow: visible !important;
padding: 0 !important;
}
.round-row {
background: white !important;
border: 1px solid #ddd !important;
background: transparent !important;
border-radius: 0 !important;
box-shadow: none !important;
margin-bottom: 15px;
page-break-inside: avoid;
display: block !important;
margin: 0 !important;
}
.round-row,
.round-row.current,
.round-row.completed,
.round-row.waiting {
border-left: 1px solid #ddd !important;
box-shadow: none !important;
page-break-inside: avoid !important;
opacity: 1 !important;
}
.round-header {
background: #f8f9fa !important;
border: none !important;
border-bottom: 1px solid #ddd !important;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
padding: 10px 15px !important;
margin-bottom: 6px !important;
min-height: 0 !important;
border-top: 1px solid #dee2e6 !important;
border-right: 1px solid #dee2e6 !important;
border-bottom: 1px solid #dee2e6 !important;
border-left: 1px solid #dee2e6 !important;
border-color: #dee2e6 !important;
}
.round-row.current .round-header,
.round-row.completed .round-header,
.round-row.waiting .round-header {
background: #f8f9fa !important;
background: linear-gradient(135deg, #f8f9fa 0%, #f0f1f3 100%) !important;
}
.round-title {
font-size: 14pt !important;
font-weight: bold !important;
flex-shrink: 0;
}
.round-badge {
display: none !important;
}
.round-print-info {
display: flex !important;
align-items: center !important;
gap: 20px !important;
}
.round-time-field {
display: flex !important;
align-items: center !important;
gap: 8px !important;
}
.round-time-label {
font-size: 11pt !important;
color: #666 !important;
font-weight: 500 !important;
}
.round-time-input {
border: 2px solid #333 !important;
border-radius: 4px !important;
padding: 6px 10px !important;
width: 100px !important;
height: 32px !important;
background: white !important;
}
.round-checkbox-field {
display: flex !important;
align-items: center !important;
gap: 10px !important;
}
.round-checkbox-label {
font-size: 11pt !important;
color: #666 !important;
font-weight: 500 !important;
}
.round-checkbox {
width: 24px !important;
height: 24px !important;
border: 2px solid #333 !important;
border-radius: 4px !important;
background: white !important;
display: inline-block !important;
}
.positions-container {
display: grid !important;
grid-template-columns: repeat(3, 1fr) !important;
gap: 10px !important;
padding: 15px !important;
}
.position-card {
border: 1px solid #5a8fd1 !important;
border-radius: 8px !important;
overflow: hidden !important;
background: white !important;
box-shadow: none !important;
min-height: 60px !important;
page-break-inside: avoid;
display: flex !important;
flex-direction: row !important;
}
/* All cards in print have same blue color */
.round-row.current .position-card,
.round-row.completed .position-card,
.round-row.waiting .position-card {
border-color: #5a8fd1 !important;
}
.position-header {
background: #5a8fd1 !important;
padding: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
flex-shrink: 0 !important;
min-width: 40px !important;
width: 40px !important;
}
.round-row.current .position-card .position-header,
.round-row.completed .position-card .position-header,
.round-row.waiting .position-card .position-header {
background: #5a8fd1 !important;
}
.position-card.empty .position-header {
background: #9ca3af !important;
.round-header {
padding: 8px 12px !important;
min-width: 90px !important;
}
.round-title {
font-size: 9pt !important;
}
.round-badge {
display: none !important;
}
.positions-container {
gap: 5px !important;
padding: 8px !important;
}
.position-card {
box-shadow: none !important;
transform: none !important;
transition: none !important;
page-break-inside: avoid !important;
min-height: 48px !important;
overflow: hidden !important;
border-radius: 10px !important;
}
.position-header {
min-width: 36px !important;
width: 36px !important;
}
.position-number {
font-size: 14pt !important;
color: white !important;
}
.position-body {
padding: 8px 10px !important;
background: white !important;
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
}
.position-card.empty .position-body {
background: #f8f9fa !important;
font-size: 1.2rem !important;
}
.player-name {
font-size: 11pt !important;
color: #2c3e50 !important;
font-size: 10pt !important;
}
.position-card.empty .player-name {
color: #6c757d !important;
/* Done checkbox */
.print-done-check {
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 14px !important;
flex-shrink: 0 !important;
border-left: 1px solid rgba(0,0,0,0.1) !important;
}
.player-id {
.print-done-box {
width: 20px !important;
height: 20px !important;
border: 1.5px solid #555 !important;
border-radius: 4px !important;
display: block !important;
}
.print-done-label {
display: none !important;
}
@@ -541,68 +567,53 @@
display: none !important;
}
/* Print header - show logo in tournament header */
.print-header {
display: none !important;
}
.tournament-header .print-logo {
display: block !important;
height: 60px !important;
max-width: 160px !important;
margin: 0 auto 10px auto !important;
}
}
/* Print-only elements - hidden on screen */
.print-header,
@media screen {
.round-print-info,
.tournament-header .print-logo {
display: none;
.print-logo,
.print-meta,
.print-done-check {
display: none !important;
}
}
</style>
<script src="/static/js/i18n.js"></script>
</head>
<body>
<div class="navbar">
<div class="navbar-title">🏆 <span data-i18n="draft.tournament_draft">Tournament Draft</span></div>
<div class="navbar-title">🏆 Razpored Turnirja</div>
<div class="navbar-controls">
<a href="/" class="nav-btn">📺 <span data-i18n="navigation.dashboard">Dashboard</span></a>
<a href="/tournament" class="nav-btn">🏆 <span data-i18n="navigation.tournament">Tournament</span></a>
<a href="/tournament/draft" class="nav-btn active">📋 <span data-i18n="tournament.view_draft">Draft</span></a>
<a href="/tournament/draft" class="nav-btn active">📋 Razpored</a>
<a href="/results/calculator" class="nav-btn">🎯 <span data-i18n="navigation.calculator">Results Calculator</span></a>
<a href="/" class="nav-btn"></a>
</div>
</div>
<!-- Print-only header with logo -->
<div class="print-header">
<img src="/static/logo.png" alt="Logo" class="print-logo" onerror="this.style.display='none'" />
</div>
<div class="main-container">
{% if tournament %}
<div class="tournament-header">
<button class="nav-btn" onclick="printDraft()" title="Print Tournament Draft">🖨️ <span data-i18n="general.print">Print</span></button>
<img src="/static/logo.png" alt="Logo" class="print-logo" onerror="this.style.display='none'" />
<img src="/static/logo.png" alt="Logo" class="print-logo" />
<div class="tournament-title">🎯 <span data-i18n="draft.shooting_tournament">Shooting Tournament</span></div>
<div class="tournament-stats">
{{ tournament.total_players }} <span data-i18n="draft.players">players</span> • {{ tournament.total_rounds }} <span data-i18n="draft.rounds">rounds</span>
{% if tournament.current_round %}
<span class="current-round-info"><span data-i18n="draft.currently_on_round">Currently on Round</span> {{ tournament.current_round }}</span>
{% endif %}
</div>
<div class="print-meta">
<div>{{ tournament.total_players }} <span data-i18n="draft.players">igralcev</span></div>
<div id="printDate"></div>
</div>
{% if tournament.current_round %}
<div class="tournament-controls">
<button class="round-nav-btn" id="prevRoundBtn" onclick="changeRound(-1)" title="Previous Round">
<span data-i18n="draft.previous">Previous</span>
</button>
<span class="current-round-display"><span data-i18n="draft.round">Round</span> {{ tournament.current_round }} <span data-i18n="draft.of">of</span> {{ tournament.total_rounds }}</span>
<button class="round-nav-btn" id="nextRoundBtn" onclick="changeRound(1)" title="Next Round">
<span data-i18n="draft.next">Next</span>
</button>
<button class="round-nav-btn" id="prevRoundBtn" onclick="changeRound(-1)"></button>
<span class="current-round-display"><span data-i18n="draft.round">Round</span> {{ tournament.current_round }} / {{ tournament.total_rounds }}</span>
<button class="round-nav-btn" id="nextRoundBtn" onclick="changeRound(1)"></button>
</div>
{% endif %}
<button class="nav-btn" onclick="printDraft()" title="Print">🖨️</button>
</div>
<div class="rounds-container" data-total-rounds="{{ tournament.total_rounds }}">
@@ -651,6 +662,10 @@
</div>
{% endfor %}
</div>
<div class="print-done-check">
<span class="print-done-box"></span>
<span class="print-done-label">Done</span>
</div>
</div>
{% endfor %}
</div>
@@ -670,6 +685,24 @@
let currentRound = {{ tournament.current_round if tournament else 1 }};
const totalRounds = {{ tournament.total_rounds if tournament else 1 }};
// TV view toggle — only active when loaded as TV display (?tv=1)
const isTvDisplay = new URLSearchParams(window.location.search).get('tv') === '1';
if (isTvDisplay) {
const btn = document.getElementById('btnSwitchView');
if (btn) btn.style.display = '';
}
async function switchToView() {
try {
await fetch('/api/tv/view', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ view: 'cameras' })
});
window.location.href = '/';
} catch (e) { console.error(e); }
}
function updateRoundNavigation() {
if (tournamentActive) {
const prevBtn = document.getElementById('prevRoundBtn');
@@ -722,8 +755,19 @@
}
}
// Set print date
const printDateEl = document.getElementById('printDate');
if (printDateEl) {
const now = new Date();
printDateEl.textContent = now.toLocaleDateString('sl-SI', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
// Print function
function printDraft() {
document.querySelectorAll('.round-row').forEach(el => {
el.classList.remove('current', 'completed', 'waiting');
el.classList.add('waiting');
});
window.print();
}
@@ -736,7 +780,7 @@
window.location.reload();
} else if (event.key === 'p' || event.key === 'P') {
event.preventDefault();
window.print();
printDraft();
} else if (tournamentActive) {
if (event.key === 'ArrowLeft') {
event.preventDefault();
@@ -758,15 +802,92 @@
if (currentRoundRow) {
currentRoundRow.scrollIntoView({
behavior: 'smooth',
block: 'center'
block: 'start'
});
}
}, 500);
}, 300);
console.log('🖥️ PC Tournament Draft (Vertical Rows) loaded');
console.log('🏆 Tournament active:', tournamentActive);
console.log(`📊 Displaying ${totalRounds} rounds in vertical layout`);
// Poll for state changes
let lastStateHash = null;
setInterval(async () => {
if (document.visibilityState !== 'visible') return;
try {
const response = await fetch('/api/dashboard/state');
if (!response.ok) return;
const data = await response.json();
// If tournament just became inactive, redirect to archive (not reload)
if (!data.tournament_active) {
if (data.archive_filename) {
window.location.href = `/archive/tournament/${data.archive_filename}`;
} else if (data.league_archive_filename) {
window.location.href = `/archive/league/${data.league_archive_filename}`;
} else {
window.location.href = '/results';
}
return;
}
// Follow TV view changes when used as TV display
if (isTvDisplay) {
if (data.tv_view === 'cameras') { window.location.href = '/'; return; }
if (data.tv_view === 'results') {
if (data.archive_filename) {
window.location.href = `/archive/tournament/${data.archive_filename}?tv=1`;
} else if (data.league_archive_filename) {
window.location.href = `/archive/league/${data.league_archive_filename}?tv=1`;
} else {
window.location.href = '/results?tv=1';
}
return;
}
}
// Reload on round change to keep draft display in sync
const hash = JSON.stringify({ round: data.current_round });
if (lastStateHash !== null && hash !== lastStateHash) {
window.location.reload();
}
lastStateHash = hash;
updateTvViewBtn(data.tv_view || 'cameras');
} catch (e) {}
}, 3000);
});
// ── TV VIEW TOGGLE ────────────────────────────────────────────────────────
let currentTvView = 'cameras';
function updateTvViewBtn(view) {
currentTvView = view;
const btn = document.getElementById('btnTvView');
if (!btn) return;
if (view === 'draft') {
btn.textContent = '📺';
btn.style.background = '#28a745';
btn.style.color = 'white';
btn.style.borderColor = '#1e7e34';
} else {
btn.textContent = '📺';
btn.style.background = '';
btn.style.color = '';
btn.style.borderColor = '';
}
}
async function toggleTvView() {
const newView = currentTvView === 'draft' ? 'cameras' : 'draft';
try {
await fetch('/api/tv/view', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ view: newView })
});
updateTvViewBtn(newView);
} catch (e) { console.error(e); }
}
// Load initial tv_view state
fetch('/api/dashboard/state').then(r => r.json()).then(d => updateTvViewBtn(d.tv_view || 'cameras')).catch(() => {});
</script>
</body>
</html>
+78 -8
View File
@@ -655,6 +655,12 @@
</div>
<div style="display: flex; align-items: center; gap: 15px;">
{% if settings.tournament_active %}
<div class="datetime" style="text-align: right;">
<div class="time" id="time"></div>
<div class="date" id="date"></div>
</div>
{% endif %}
<button class="hamburger-menu" id="menuButton" data-i18n-title="general.settings">
<div class="hamburger-line"></div>
<div class="hamburger-line"></div>
@@ -682,18 +688,13 @@
</div>
<div class="tournament-actions">
<a href="/tournament/draft" class="nav-link tournament-btn">📋 <span data-i18n="tournament.view_full_tournament_draft">Oglej si Celoten Žreb Turnirja</span></a>
<a href="/tournament/draft" class="nav-link tournament-btn">📋 Razpored Turnirja</a>
<a href="/results/calculator" class="nav-link tournament-btn">🎯 <span data-i18n="scoring.results_calculator">Calculator</span></a>
<a href="/tournament" class="nav-link tournament-btn" id="manageTournamentLink">⚙️ <span data-i18n="tournament.manage_tournament">Upravljaj Turnir</span></a>
</div>
</div>
{% endif %}
{% if settings.tournament_active %}
<div class="settings-group">
<a href="/results/calculator" class="nav-link tournament-btn">🎯 <span data-i18n="scoring.results_calculator">Calculator</span></a>
</div>
{% endif %}
<!-- Tournaments Section -->
<div class="settings-group">
<h4 data-i18n="league.tournaments">Turnirji</h4>
@@ -771,7 +772,7 @@
<!-- Version Information -->
<div class="version-info" style="margin-top: 30px; padding: 15px; text-align: center; border-top: 1px solid #e9ecef; color: #6c757d; font-size: 0.85rem;">
Version 1.0.0
Version 1.0.2
</div>
</div>
</div>
@@ -1088,6 +1089,7 @@
}
}
// TV view toggle (cameras ↔ draft)
// Debounce function
function debounce(func, wait) {
let timeout;
@@ -1466,11 +1468,79 @@
}, { once: true });
});
// TV Display Auto-Update Polling
// (Separate from remote control polling)
let lastDashboardStateHash = null;
let dashboardPollingInterval = null;
async function pollDashboardState() {
// Only poll when page is visible
if (document.visibilityState !== 'visible') {
return;
}
try {
const response = await fetch('/api/dashboard/state');
if (!response.ok) return;
const data = await response.json();
// If switched to draft view and tournament is active, follow it
if (data.tv_view === 'draft' && data.tournament_active) {
window.location.href = '/tournament/draft?tv=1';
return;
}
// If switched to results view, follow it (use explicit tv_url first, then fallback)
if (data.tv_view === 'results') {
if (data.tv_url) {
window.location.href = data.tv_url.includes('?') ? data.tv_url + '&tv=1' : data.tv_url + '?tv=1';
} else if (data.archive_filename) {
window.location.href = `/archive/tournament/${data.archive_filename}?tv=1`;
} else if (data.league_archive_filename) {
window.location.href = `/archive/league/${data.league_archive_filename}?tv=1`;
} else {
window.location.href = '/results?tv=1';
}
return;
}
// Create a hash of the state to detect changes
const stateHash = JSON.stringify(data);
// If structural state changed, reload the page
if (lastDashboardStateHash !== null && stateHash !== lastDashboardStateHash) {
console.log('🔄 Tournament state changed, reloading TV display...');
window.location.reload();
}
lastDashboardStateHash = stateHash;
} catch (error) {
console.error('Dashboard polling error:', error);
// Silent failure - keep polling
}
}
// Start dashboard polling (every 3 seconds)
dashboardPollingInterval = setInterval(pollDashboardState, 3000);
// Initial poll
pollDashboardState();
// Pause polling when page is hidden
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
pollDashboardState();
}
});
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
if (remotePollingInterval) {
clearInterval(remotePollingInterval);
}
if (dashboardPollingInterval) {
clearInterval(dashboardPollingInterval);
}
});
</script>
</body>
+771
View File
@@ -0,0 +1,771 @@
<!DOCTYPE html>
<html lang="sl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="league.combine_leagues">Combine Leagues</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/navbar.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/buttons.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}">
<style>
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
background: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.navbar {
background: white;
border-bottom: 1px solid #e1e5e9;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 15px 25px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0;
}
.navbar-title {
font-size: 1.8rem;
font-weight: bold;
color: #333;
margin: 0;
display: flex;
align-items: center;
gap: 10px;
}
.nav-btn {
background: #f8f9fa;
border: 2px solid #e9ecef;
cursor: pointer;
padding: 12px 20px;
border-radius: 8px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: #333;
text-decoration: none;
font-weight: bold;
font-size: 0.9rem;
}
.nav-btn:hover {
background: #e9ecef;
border-color: #28a745;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
color: #28a745;
}
.upload-section {
background: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.upload-area {
border: 3px dashed #28a745;
border-radius: 10px;
padding: 40px;
text-align: center;
background: #f8f9fa;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
background: #e9f7ed;
border-color: #1e7e34;
}
.upload-area.drag-over {
background: #d4edda;
border-color: #28a745;
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.2);
}
.file-input {
display: none;
}
.upload-icon {
font-size: 48px;
margin-bottom: 10px;
}
.uploaded-files {
margin-top: 20px;
}
.file-item {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
border-left: 4px solid #28a745;
}
.file-item.error {
border-left-color: #dc3545;
background: #f8d7da;
}
.file-info {
display: flex;
flex-direction: column;
gap: 5px;
}
.file-name {
font-weight: bold;
color: #2c3e50;
}
.file-details {
font-size: 0.9em;
color: #6c757d;
}
.remove-btn {
background: #dc3545;
color: white;
border: none;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
.remove-btn:hover {
background: #c82333;
}
.action-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
padding: 12px 24px;
border: 2px solid;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: all 0.2s ease;
flex: 1;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-primary {
background: #28a745;
color: white;
border-color: #1e7e34;
}
.btn-primary:hover {
background: #1e7e34;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.btn-primary:disabled {
background: #6c757d;
border-color: #5a6268;
cursor: not-allowed;
opacity: 0.6;
transform: none;
}
.btn-secondary {
background: #f8f9fa;
color: #333;
border-color: #e9ecef;
}
.btn-secondary:hover {
background: #e9ecef;
border-color: #dc3545;
color: #dc3545;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.preview-section {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
display: none;
}
.preview-section.active {
display: block;
}
.info-box {
background: #d4edda;
border-left: 4px solid #28a745;
padding: 15px;
margin-bottom: 20px;
border-radius: 8px;
}
.info-box h3 {
margin: 0 0 10px 0;
color: #155724;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
border-left: 4px solid #28a745;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #28a745;
}
.stat-label {
font-size: 14px;
color: #6c757d;
margin-top: 5px;
}
.mode-btn {
flex: 1;
padding: 12px 20px;
border: 2px solid #28a745;
background: white;
color: #28a745;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.mode-btn:hover {
background: #e9f7ed;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.mode-btn.active {
background: #28a745;
color: white;
border-color: #1e7e34;
}
</style>
</head>
<body>
<div class="navbar">
<h1 class="navbar-title">🔗 <span data-i18n="league.combine_leagues">Combine Leagues</span></h1>
<a href="/" class="nav-btn"></a>
</div>
<div class="container">
<div class="upload-section">
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 10px; font-weight: bold; color: #2c3e50;">
<span data-i18n="league.select_mode">Select Mode</span>:
</label>
<div style="display: flex; gap: 10px;">
<button class="mode-btn active" id="combineMode" onclick="switchMode('combine')">
🔗 <span data-i18n="league.combine_mode">Combine Leagues</span>
</button>
<button class="mode-btn" id="convertMode" onclick="switchMode('convert')">
🔄 <span data-i18n="league.convert_mode">Convert Tournaments to League</span>
</button>
</div>
</div>
<div id="combineSection">
<h2>📤 <span data-i18n="league.upload_league_files">Upload League JSON Files</span></h2>
<p style="color: #6c757d; margin-bottom: 20px;">
<span data-i18n="league.combine_leagues_desc">Upload multiple league JSON files to combine them into a single results table.</span>
</p>
</div>
<div id="convertSection" style="display: none;">
<h2>📤 <span data-i18n="league.upload_tournament_files">Upload Tournament JSON Files</span></h2>
<p style="color: #6c757d; margin-bottom: 20px;">
<span data-i18n="league.convert_tournaments_desc">Upload 1-5 tournament JSON files to create a league. You can upload partial leagues as tournaments complete. Final scoring uses best 4 results when all 5 are uploaded.</span>
</p>
</div>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📁</div>
<p><strong data-i18n="league.click_browse">Click to browse</strong> <span data-i18n="league.drag_drop_files">or drag and drop league JSON files here</span></p>
<p style="font-size: 0.9em; color: #6c757d;"><span data-i18n="league.supports_multiple">Supports multiple file selection</span></p>
<input type="file" id="fileInput" class="file-input" accept=".json" multiple>
</div>
<div class="uploaded-files" id="uploadedFiles"></div>
<div class="action-buttons">
<button class="btn btn-secondary" id="clearBtn" disabled>
🗑️ <span data-i18n="scoring.clear_all">Clear All</span>
</button>
<button class="btn btn-primary" id="processBtn" disabled>
<span id="processBtnText">🔗 <span data-i18n="league.combine_preview">Combine & Preview</span></span>
</button>
</div>
</div>
<div class="preview-section" id="previewSection">
<div class="info-box">
<h3><span data-i18n="league.combined_league_info">Combined League Information</span></h3>
<div id="combinedInfo"></div>
</div>
<div class="stats-grid" id="statsGrid"></div>
<div id="resultsDisplay"></div>
<div class="action-buttons" style="margin-top: 20px;">
<button class="btn btn-secondary" onclick="window.location.reload()">
🔄 <span data-i18n="league.start_over">Start Over</span>
</button>
<button class="btn btn-primary" id="exportBtn">
💾 <span data-i18n="league.export_combined_results">Export Combined Results</span>
</button>
</div>
</div>
</div>
<script src="/static/js/i18n.js"></script>
<script>
let uploadedLeagues = [];
let currentMode = 'combine'; // 'combine' or 'convert'
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const uploadedFilesDiv = document.getElementById('uploadedFiles');
const clearBtn = document.getElementById('clearBtn');
const processBtn = document.getElementById('processBtn');
const processBtnText = document.getElementById('processBtnText');
const previewSection = document.getElementById('previewSection');
// Helper function to get translated text
function t(key) {
if (window.i18n && window.i18n.t) {
return window.i18n.t(key);
}
// Fallback if i18n not loaded yet
return key.split('.').pop();
}
function switchMode(mode) {
currentMode = mode;
// Update button states
document.getElementById('combineMode').classList.toggle('active', mode === 'combine');
document.getElementById('convertMode').classList.toggle('active', mode === 'convert');
// Update sections
document.getElementById('combineSection').style.display = mode === 'combine' ? 'block' : 'none';
document.getElementById('convertSection').style.display = mode === 'convert' ? 'block' : 'none';
// Update button text with translations
processBtnText.innerHTML = mode === 'combine'
? '🔗 <span data-i18n="league.combine_preview">Combine & Preview</span>'
: '🔄 <span data-i18n="league.convert_to_league">Convert to League</span>';
// Re-apply translations after DOM change
if (window.i18n) window.i18n.updatePageTranslations();
// Clear uploaded files
uploadedLeagues = [];
updateFileList();
previewSection.classList.remove('active');
}
// Click to upload
uploadArea.addEventListener('click', () => fileInput.click());
// Drag and drop
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('drag-over');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('drag-over');
handleFiles(e.dataTransfer.files);
});
// File input change
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
function handleFiles(files) {
Array.from(files).forEach(file => {
if (file.type === 'application/json' || file.name.endsWith('.json')) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (currentMode === 'combine') {
// Validate it's a league file (handles both archived and raw formats)
let leagueData = null;
if (data.league && data.league.participants) {
// Archived format (wrapped)
leagueData = data.league;
} else if (data.participants) {
// Raw league format (unwrapped)
leagueData = data;
}
if (leagueData) {
uploadedLeagues.push({
filename: file.name,
data: data,
leagueData: leagueData, // Store the actual league data
valid: true,
type: 'league'
});
} else {
uploadedLeagues.push({
filename: file.name,
error: 'Invalid league file format',
valid: false,
type: 'league'
});
}
} else {
// Convert mode - validate it's a tournament file
if (data.tournament && data.results && data.results.participants) {
uploadedLeagues.push({
filename: file.name,
data: data,
valid: true,
type: 'tournament'
});
} else {
uploadedLeagues.push({
filename: file.name,
error: t('league.invalid_tournament_format'),
valid: false,
type: 'tournament'
});
}
}
updateFileList();
} catch (error) {
uploadedLeagues.push({
filename: file.name,
error: t('league.failed_parse_json'),
valid: false
});
updateFileList();
}
};
reader.readAsText(file);
}
});
}
function updateFileList() {
uploadedFilesDiv.innerHTML = '';
if (uploadedLeagues.length > 0) {
const heading = document.createElement('h3');
heading.textContent = `📋 ${t('league.uploaded_files')} (${uploadedLeagues.length})`;
uploadedFilesDiv.appendChild(heading);
uploadedLeagues.forEach((league, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item' + (league.valid ? '' : ' error');
const fileInfo = document.createElement('div');
fileInfo.className = 'file-info';
const fileName = document.createElement('div');
fileName.className = 'file-name';
fileName.textContent = league.filename;
const fileDetails = document.createElement('div');
fileDetails.className = 'file-details';
if (league.valid) {
if (league.type === 'league') {
const participantCount = Object.keys(league.leagueData.participants).length;
const tournamentCount = league.leagueData.total_tournaments || 0;
fileDetails.textContent = `${t('league.valid_league')} | ${participantCount} ${t('league.participants')} | ${tournamentCount} ${t('league.tournaments')}`;
} else {
const participantCount = Object.keys(league.data.results.participants).length;
const tournamentType = league.data.results.tournament_type || '20_targets';
fileDetails.textContent = `${t('league.valid_tournament')} | ${participantCount} ${t('league.participants')} | ${t('league.type')}: ${tournamentType}`;
}
} else {
fileDetails.textContent = `${league.error}`;
}
fileInfo.appendChild(fileName);
fileInfo.appendChild(fileDetails);
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.textContent = `${t('league.remove')}`;
removeBtn.onclick = () => removeFile(index);
fileItem.appendChild(fileInfo);
fileItem.appendChild(removeBtn);
uploadedFilesDiv.appendChild(fileItem);
});
clearBtn.disabled = false;
const validCount = uploadedLeagues.filter(l => l.valid).length;
if (currentMode === 'combine') {
processBtn.disabled = validCount < 1; // Allow preview with 1+ files
} else {
// Convert mode requires 1-5 tournament files
processBtn.disabled = validCount < 1 || validCount > 5;
}
} else {
clearBtn.disabled = true;
processBtn.disabled = true;
}
}
function removeFile(index) {
uploadedLeagues.splice(index, 1);
updateFileList();
}
clearBtn.addEventListener('click', () => {
uploadedLeagues = [];
updateFileList();
previewSection.classList.remove('active');
});
processBtn.addEventListener('click', async () => {
const validFiles = uploadedLeagues.filter(l => l.valid);
if (currentMode === 'combine') {
if (validFiles.length < 1) {
alert('Please upload at least 1 valid league file');
return;
}
// Send to backend for combination
const response = await fetch('/api/league/combine', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
leagues: validFiles.map(l => l.leagueData)
})
});
if (response.ok) {
const result = await response.json();
// Store in session and redirect to preview
const previewResponse = await fetch('/league/set-preview', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
league: result.combined_league
})
});
if (previewResponse.ok) {
const previewResult = await previewResponse.json();
window.location.href = previewResult.redirect_url;
} else {
alert('Failed to preview league');
}
} else {
alert('Failed to combine leagues');
}
} else {
// Convert mode
if (validFiles.length < 1 || validFiles.length > 5) {
alert('Please upload 1-5 valid tournament files');
return;
}
// Send to backend for conversion
const response = await fetch('/api/league/convert', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tournaments: validFiles.map(l => l.data)
})
});
if (response.ok) {
const result = await response.json();
// Store in session and redirect to preview
const previewResponse = await fetch('/league/set-preview', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
league: result.combined_league
})
});
if (previewResponse.ok) {
const previewResult = await previewResponse.json();
window.location.href = previewResult.redirect_url;
} else {
alert('Failed to preview league');
}
} else {
alert('Failed to convert tournaments to league');
}
}
});
function displayCombinedResults(result) {
// Display combined info
const sourceType = result.is_partial !== undefined ?
(result.is_partial ? 'tournament files (Partial League)' : 'tournament files (Complete League)') :
'league files';
document.getElementById('combinedInfo').innerHTML = `
<p><strong>Combined from:</strong> ${result.source_count} ${sourceType}</p>
<p><strong>Tournament Type:</strong> ${result.tournament_type}</p>
<p><strong>Total Participants:</strong> ${result.total_participants}</p>
${result.is_partial ? '<p style="color: #ff9800;"><strong>⚠️ Partial League:</strong> Upload more tournaments to complete the league (5 total needed)</p>' : ''}
`;
// Display stats
const statsGrid = document.getElementById('statsGrid');
statsGrid.innerHTML = `
<div class="stat-card">
<div class="stat-value">${result.total_participants}</div>
<div class="stat-label">Total Players</div>
</div>
<div class="stat-card">
<div class="stat-value">${result.total_tournaments}</div>
<div class="stat-label">Total Tournaments</div>
</div>
<div class="stat-card">
<div class="stat-value">${result.highest_score}</div>
<div class="stat-label">Highest Score</div>
</div>
<div class="stat-card">
<div class="stat-value">${result.avg_score}</div>
<div class="stat-label">Average Score</div>
</div>
`;
// Store combined data for export
window.combinedLeagueData = result.combined_league;
// Redirect to results display with combined data
displayResultsTable(result.participants);
}
function displayResultsTable(participants) {
const resultsDisplay = document.getElementById('resultsDisplay');
let html = `
<h2>🏆 Combined Results</h2>
<table style="width: 100%; border-collapse: collapse; margin-top: 20px;">
<thead>
<tr style="background: #667eea; color: white;">
<th style="padding: 12px; text-align: left;">Rank</th>
<th style="padding: 12px; text-align: left;">Player</th>
<th style="padding: 12px; text-align: center;">Final Score</th>
<th style="padding: 12px; text-align: center;">Total Score</th>
<th style="padding: 12px; text-align: center;">Tournaments</th>
<th style="padding: 12px; text-align: center;">10s</th>
</tr>
</thead>
<tbody>
`;
participants.forEach((p, i) => {
html += `
<tr style="border-bottom: 1px solid #dee2e6; ${i % 2 === 0 ? 'background: #f8f9fa;' : ''}">
<td style="padding: 12px; font-weight: bold;">${i + 1}</td>
<td style="padding: 12px;">${p.name}</td>
<td style="padding: 12px; text-align: center; font-weight: bold; color: #667eea;">${p.final_score}</td>
<td style="padding: 12px; text-align: center;">${p.total_score}</td>
<td style="padding: 12px; text-align: center;">${p.tournaments_participated}</td>
<td style="padding: 12px; text-align: center;">${p.total_tens}</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
resultsDisplay.innerHTML = html;
}
document.getElementById('exportBtn').addEventListener('click', () => {
if (window.combinedLeagueData) {
// Wrap in archive format to be compatible with results display
const archiveData = {
league: window.combinedLeagueData,
archived_at: new Date().toISOString()
};
const dataStr = JSON.stringify(archiveData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `combined_league_${new Date().toISOString().slice(0, 10)}.json`;
link.click();
URL.revokeObjectURL(url);
}
});
</script>
</body>
</html>
+229 -100
View File
@@ -96,22 +96,25 @@
.league-container {
height: calc(100vh - 90px);
display: grid;
grid-template-columns: 1fr 3fr;
grid-template-columns: 1fr 2fr;
gap: 20px;
padding: 20px;
overflow: hidden;
}
/* Left Column - Header & Champions */
.left-column {
display: flex;
flex-direction: column;
gap: 20px;
gap: 0;
min-height: 0;
overflow: hidden;
}
.league-header {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 25px;
border-radius: 12px 12px 0 0;
box-shadow: none;
padding: 20px;
text-align: center;
flex-shrink: 0;
color: white;
@@ -149,37 +152,37 @@
}
.header-logo {
height: 70px;
max-width: 180px;
height: 50px;
max-width: 150px;
object-fit: contain;
margin-bottom: 20px;
margin-bottom: 10px;
filter: brightness(1.2) contrast(1.1);
background-color: white;
padding: 10px;
border-radius: 8px;
padding: 6px;
border-radius: 6px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.league-title {
font-size: 2.2rem;
font-size: 1.8rem;
font-weight: 700;
color: rgb(255, 255, 255);
margin-bottom: 10px;
margin-bottom: 8px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.league-subtitle {
font-size: 1.1rem;
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.95);
margin-bottom: 20px;
margin-bottom: 12px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.league-meta {
display: flex;
justify-content: space-around;
gap: 15px;
gap: 10px;
}
.meta-item {
@@ -187,7 +190,7 @@
}
.meta-number {
font-size: 1.6rem;
font-size: 1.2rem;
font-weight: 700;
color: #ffffff;
display: block;
@@ -195,83 +198,112 @@
}
.meta-label {
font-size: 0.8rem;
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.9);
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.3px;
font-weight: 500;
}
/* League Champion Section */
.champion-section {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 25px;
border-radius: 0 0 12px 12px;
box-shadow: none;
padding: 12px;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
border: 1px solid #e9ecef;
}
.champion-title {
text-align: center;
font-size: 1.4rem;
font-size: 0.9rem;
font-weight: 700;
color: #2c3e50;
margin-bottom: 20px;
margin-bottom: 8px;
flex-shrink: 0;
}
.champion-container {
display: flex;
flex-direction: column;
gap: 15px;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 0.75fr 0.75fr 1.5fr;
gap: 10px;
flex: 1;
overflow: hidden;
min-height: 0;
}
.champion-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 20px;
box-shadow: none;
padding: 12px 12px 18px 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
transition: all 0.2s ease;
gap: 8px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
border-left: 5px solid;
border: 1px solid #e9ecef;
border-top-width: 6px;
justify-content: flex-end;
}
.champion-card::before {
display: none;
}
.champion-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.champion-card.rank-1 {
border-left-color: #ffd700;
background: linear-gradient(135deg, #fff9e6 0%, #ffffff 100%);
border-top-color: #ffd700;
border-color: #ffd700;
background: linear-gradient(135deg, #fffbf0 0%, #fff9e6 100%);
grid-column: 2;
grid-row: 1 / 4;
}
.champion-card.rank-2 {
border-left-color: #c0c0c0;
background: linear-gradient(135deg, #f5f5f5 0%, #ffffff 100%);
border-top-color: #c0c0c0;
border-color: #c0c0c0;
background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
grid-column: 1;
grid-row: 2 / 4;
}
.champion-card.rank-3 {
border-left-color: #cd7f32;
background: linear-gradient(135deg, #fdf6f0 0%, #ffffff 100%);
border-top-color: #cd7f32;
border-color: #cd7f32;
background: linear-gradient(135deg, #fff5f0 0%, #ffe8dc 100%);
grid-column: 3;
grid-row: 3;
}
.rank-display {
display: flex;
flex-direction: column;
align-items: center;
min-width: 60px;
gap: 2px;
position: relative;
z-index: 2;
}
.medal {
font-size: 4.5rem;
line-height: 1;
}
.rank-number {
font-size: 1.8rem;
font-weight: bold;
font-size: 1.9rem;
font-weight: 900;
color: #333;
line-height: 1;
}
@@ -280,29 +312,31 @@
.champion-card.rank-2 .rank-number { color: #696969; }
.champion-card.rank-3 .rank-number { color: #8b4513; }
.rank-suffix {
font-size: 0.7rem;
color: #666;
text-transform: uppercase;
font-weight: bold;
}
.medal {
font-size: 1.5rem;
margin-top: 3px;
}
.participant-info {
flex: 1;
min-width: 0;
text-align: center;
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.participant-name {
font-size: 1.3rem;
font-weight: bold;
color: #333;
margin-bottom: 5px;
font-size: 1rem;
font-weight: 700;
color: #2c3e50;
margin: 0;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.2;
}
.participant-details {
@@ -315,74 +349,96 @@
.participant-id {
background: #28a745;
color: white;
padding: 3px 10px;
padding: 6px 12px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
font-size: 0.75rem;
font-weight: 700;
display: inline-block;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
}
.joker-badge {
background: #ffc107;
color: #856404;
padding: 3px 8px;
border-radius: 8px;
font-size: 0.7rem;
font-weight: bold;
padding: 6px 12px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 700;
display: inline-block;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 2px 8px rgba(255, 193, 7, 0.3);
}
.score-display {
text-align: right;
min-width: 100px;
text-align: center;
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
gap: 1px;
}
.final-score {
font-size: 2rem;
font-weight: bold;
.score-number {
font-size: 1.8rem;
font-weight: 900;
color: #28a745;
line-height: 1;
}
.tens-count {
font-size: 1.2rem;
color: #ffc107;
font-weight: 700;
}
.total-score {
font-size: 0.9rem;
color: #666;
margin-top: 2px;
font-size: 0.8rem;
color: #ffc107;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.tens-count {
font-size: 0.9rem;
font-size: 0.8rem;
color: #ffc107;
font-weight: bold;
margin-top: 2px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.score-label {
font-size: 0.8rem;
color: #666;
font-size: 0.6rem;
color: #999;
text-transform: uppercase;
font-weight: bold;
font-weight: 600;
letter-spacing: 0.3px;
}
/* Right Column - League Table */
.right-column {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 12px;
box-shadow: none;
overflow: hidden;
display: flex;
flex-direction: column;
border: 1px solid #e9ecef;
}
.table-header {
background: #f8f9fa;
padding: 15px 20px;
padding: 12px 15px;
border-bottom: 1px solid #dee2e6;
flex-shrink: 0;
}
.table-title {
font-size: 1.25rem;
font-size: 0.95rem;
font-weight: 600;
color: #2c3e50;
margin: 0;
@@ -396,12 +452,12 @@
.league-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
font-size: 0.75rem;
}
.league-table th,
.league-table td {
padding: 7px 5px;
padding: 6px 4px;
text-align: center;
border-bottom: 1px solid #f1f3f4;
border-right: 1px solid #f1f3f4;
@@ -421,20 +477,20 @@
.league-table th.player-col {
text-align: left;
width: 120px;
width: 100px;
}
.league-table th.tournament-col {
width: 70px;
width: 60px;
}
.league-table th.final-col {
width: 80px;
width: 70px;
background: #e3f2fd;
}
.league-table th.tens-col {
width: 70px;
width: 60px;
background: #fff3cd;
}
@@ -786,8 +842,10 @@
<div class="navbar-title">🎖️ <span data-i18n="league.league_results">League Results</span></div>
</div>
<div class="navbar-controls">
<a href="/" class="nav-btn">📺 <span data-i18n="navigation.dashboard">Dashboard</span></a>
<button class="nav-btn" onclick="exportLeagueJSON()">💾 <span data-i18n="general.export">Export JSON</span></button>
<button class="nav-btn" onclick="window.print()">🖨️ <span data-i18n="general.print">Print</span></button>
<button class="nav-btn" id="btnTvView" onclick="toggleTvView()" title="Preklopi pogled na TV zaslonu">📺</button>
<a href="/" class="nav-btn"></a>
</div>
</div>
@@ -1043,10 +1101,6 @@
const topThree = processedParticipants.slice(0, 3);
return topThree.map(participant => {
const suffix = participant.rank === 1 ? 'st' :
participant.rank === 2 ? 'nd' :
participant.rank === 3 ? 'rd' : 'th';
const medal = participant.rank === 1 ? '🥇' :
participant.rank === 2 ? '🥈' :
participant.rank === 3 ? '🥉' : '';
@@ -1055,21 +1109,16 @@
return `
<div class="champion-card rank-${participant.rank}">
<div class="rank-display">
<div class="rank-number">${participant.rank}</div>
<div class="rank-suffix">${suffix}</div>
<div class="medal">${medal}</div>
</div>
<div class="participant-info">
<div class="participant-name">${participant.name}</div>
<div class="participant-details">
<div class="participant-id">ID: ${participant.id}</div>
</div>
</div>
<div class="score-display">
<div class="final-score">${participant.final_score}</div>
<div class="tens-count">🎯 ${participant.total_tens} × 10</div>
<div class="score-number">${participant.final_score}</div>
<div class="tens-count">🎯 ${participant.total_tens}</div>
</div>
</div>
`;
@@ -1099,6 +1148,8 @@
tournamentCells += '<td><span class="tournament-score joker">🃏</span></td>';
} else {
const score = result.score;
const tensCount = result.tens_count || 0;
// Check if this specific tournament index should be excluded
const isExcluded = best4Logic.excludedIndices.includes(participatedTournamentIndex) && best4Logic.allScores.length > 4;
const scoreClass = isExcluded ? 'excluded' : 'counted';
@@ -1199,6 +1250,31 @@
// Initialize when page loads
document.addEventListener('DOMContentLoaded', initializePage);
// Export league data as JSON
function exportLeagueJSON() {
const leagueData = {{ league | tojson | safe }};
// Wrap in archive format for compatibility
const archiveData = {
league: leagueData,
archived_at: new Date().toISOString()
};
const dataStr = JSON.stringify(archiveData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
// Generate filename with league info
const leagueId = leagueData.league_id || 'league';
const date = new Date().toISOString().slice(0, 10);
link.download = `${leagueId}_${date}.json`;
link.click();
URL.revokeObjectURL(url);
}
// Keyboard shortcuts
document.addEventListener('keydown', function(event) {
if (event.key === 'r' || event.key === 'R') {
@@ -1209,6 +1285,59 @@
window.print();
}
});
// ── TV VIEW TOGGLE ────────────────────────────────────────────────────────
const isTvDisplay = new URLSearchParams(window.location.search).get('tv') === '1';
let currentTvView = 'results';
function updateTvViewBtn(view) {
currentTvView = view;
const btn = document.getElementById('btnTvView');
if (!btn) return;
btn.style.background = (view === 'results') ? '#28a745' : '';
btn.style.color = (view === 'results') ? 'white' : '';
btn.style.borderColor = (view === 'results') ? '#1e7e34' : '';
}
async function toggleTvView() {
const newView = currentTvView === 'results' ? 'cameras' : 'results';
try {
const payload = { view: newView };
if (newView === 'results') {
// Pass the current page path so the TV follows to this league archive, not a tournament archive
payload.tv_url = window.location.pathname;
}
await fetch('/api/tv/view', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
updateTvViewBtn(newView);
if (isTvDisplay && newView === 'cameras') {
window.location.href = '/';
}
} catch (e) { console.error(e); }
}
// Poll to stay in sync and follow back-redirect when in TV mode
setInterval(async () => {
if (document.visibilityState !== 'visible') return;
try {
const r = await fetch('/api/dashboard/state');
if (!r.ok) return;
const d = await r.json();
updateTvViewBtn(d.tv_view || 'cameras');
if (isTvDisplay && d.tv_view !== 'results') {
if (d.tv_view === 'draft' && d.tournament_active) {
window.location.href = '/tournament/draft?tv=1';
} else {
window.location.href = '/';
}
}
} catch (e) {}
}, 3000);
fetch('/api/dashboard/state').then(r => r.json()).then(d => updateTvViewBtn(d.tv_view || 'cameras')).catch(() => {});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+224 -384
View File
@@ -37,7 +37,8 @@
body {
font-family: Arial, sans-serif;
background: #f5f5f5;
min-height: 100vh;
height: 100vh;
overflow: hidden;
color: #333;
}
@@ -385,6 +386,133 @@
font-weight: 500;
}
/* ── PLAYERS SPLIT LAYOUT ── */
.players-layout {
display: flex;
gap: 8px;
height: calc(100vh - 50px);
padding: 8px 20px 20px 20px;
}
.players-left-panel {
width: 260px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
overflow: hidden;
border: 1px solid #e9ecef;
}
.players-panel-header {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 12px;
background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
border-radius: 12px 12px 0 0;
flex-shrink: 0;
}
.players-panel-header-title {
font-size: 0.8rem; font-weight: 700; color: white; letter-spacing: 0.3px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.players-panel-count {
font-size: 0.7rem; font-weight: 600;
background: rgba(255,255,255,0.25); color: white;
padding: 2px 7px; border-radius: 10px;
}
.players-sort-bar {
padding: 6px 8px;
border-bottom: 1px solid #e9ecef;
flex-shrink: 0;
background: #f8f9fa;
display: flex;
flex-direction: column;
gap: 5px;
}
.players-search {
width: 100%; border: 1px solid #dee2e6; border-radius: 6px;
padding: 5px 9px; font-size: 0.78rem; outline: none; background: white;
}
.players-search:focus { border-color: #28a745; }
.players-sort-btns {
display: flex; gap: 4px;
}
.psort-btn {
flex: 1;
padding: 2px 7px;
border: 2px solid #dee2e6; background: white; border-radius: 6px;
cursor: pointer; font-size: 0.6rem; font-weight: 600; color: #666;
transition: all 0.15s; white-space: nowrap; text-align: center;
}
.psort-btn.active { background: #28a745; border-color: #1e7e34; color: white; }
.psort-btn:hover:not(.active) { border-color: #28a745; color: #28a745; }
.player-tree-list {
flex: 1; overflow-y: auto; overflow-x: hidden;
}
.player-tree-list::-webkit-scrollbar { width: 5px; }
.player-tree-list::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; }
.player-tree-item {
display: flex; align-items: center;
padding: 8px 10px 8px 14px;
cursor: pointer; border-left: 4px solid transparent;
gap: 8px; transition: all 0.15s;
}
.player-tree-item:hover { background: #f0faf3; border-left-color: #7dcf95; }
.player-tree-item.active {
background: #28a745; border-left-color: #28a745;
font-weight: 700; color: white;
}
.player-tree-avatar {
width: 28px; height: 28px; border-radius: 50%;
background: #e9ecef; display: flex; align-items: center; justify-content: center;
font-size: 0.75rem; font-weight: 700; color: #666; flex-shrink: 0;
}
.player-tree-item.active .player-tree-avatar { background: rgba(255,255,255,0.25); color: white; }
.player-tree-info { flex: 1; min-width: 0; }
.player-tree-name { font-size: 0.82rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.player-tree-meta { font-size: 0.65rem; color: #999; }
.player-tree-item.active .player-tree-meta { color: rgba(255,255,255,0.75); }
.player-tree-score {
font-size: 0.75rem; font-weight: 700; color: #28a745; flex-shrink: 0;
}
.player-tree-item.active .player-tree-score { color: rgba(255,255,255,0.9); }
.player-tree-rank {
font-size: 1.1rem; flex-shrink: 0; line-height: 1;
}
.player-tree-rank-num {
width: 24px; height: 24px; border-radius: 50%;
background: #cfe2ff; color: #084298;
display: flex; align-items: center; justify-content: center;
font-size: 0.6rem; font-weight: 700; flex-shrink: 0;
}
.player-tree-item.active .player-tree-rank-num {
background: rgba(255,255,255,0.3); color: white;
}
.players-right-panel {
flex: 1; overflow: hidden; min-width: 0;
display: flex; flex-direction: column;
}
#playerDetailEmbed { flex: 1; min-height: 0; display: flex; flex-direction: column; }
#playerDetailEmbed iframe { flex: 1; min-height: 0; height: 100%; width: 100%; border: none; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.player-placeholder {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center;
color: #ccc; gap: 10px;
background: white; border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border: 1px solid #e9ecef;
}
.player-placeholder-icon { font-size: 2.5rem; }
.player-placeholder-text { font-size: 0.95rem; }
/* Compact Controls */
.controls {
display: flex;
@@ -707,90 +835,44 @@
<body>
<!-- Navigation Bar -->
<div class="navbar">
<div class="navbar-title" data-i18n="players.player_analysis">🎯 👤 Player Analysis</div>
<div class="navbar-title">👤 Analiza Igralcev</div>
<div class="navbar-controls">
<a href="/" class="nav-btn">📺 <span data-i18n="navigation.dashboard">Dashboard</span></a>
<a href="/archive/player-analysis" class="nav-btn active">👤 <span data-i18n="players.player_analysis">Player Analysis</span></a>
<a href="/archive/player-analysis" class="nav-btn active">👤 <span data-i18n="players.player_analysis">Analiza Igralcev</span></a>
<a href="/archive" class="nav-btn">📚 <span data-i18n="navigation.archive">Archive</span></a>
<a href="/" class="nav-btn"></a>
</div>
</div>
<div class="container">
<!-- Stats Overview -->
<div class="stats-badges">
<div class="stat-badge">
<span class="stat-icon">👥</span>
<div class="stat-number" id="totalPlayers">Loading...</div>
<div class="stat-label" data-i18n="players.total_players">Total Players</div>
<div class="players-layout">
<!-- LEFT: player tree -->
<div class="players-left-panel">
<div class="players-panel-header">
<div class="players-panel-header-title">👥 Igralci</div>
<div class="players-panel-count" id="playerTreeCount">0</div>
</div>
<div class="stat-badge">
<span class="stat-icon">🎯</span>
<div class="stat-number" id="totalTournaments">Loading...</div>
<div class="stat-label" data-i18n="analysis.total_tournaments">Total Tournaments</div>
<div class="players-sort-bar">
<input type="text" class="players-search" id="searchBox" placeholder="🔍 Iskanje…" oninput="filterAndSortPlayers()">
<div class="players-sort-btns">
<button class="psort-btn active" data-sort="best_score" onclick="setSort('best_score')">🏆 Skupaj</button>
<button class="psort-btn" data-sort="total_tens" onclick="setSort('total_tens')">🎯 Desetke</button>
<button class="psort-btn" data-sort="best_single" onclick="setSort('best_single')">⭐ Enkrat</button>
</div>
<div class="stat-badge">
<span class="stat-icon">🏆</span>
<div class="stat-number" id="score20Targets">Loading...</div>
<div class="stat-label" data-i18n="tournament_types.20_targets">20 Targets</div>
</div>
<div class="stat-badge">
<span class="stat-icon">🎖️</span>
<div class="stat-number" id="score40Targets">Loading...</div>
<div class="stat-label" data-i18n="tournament_types.40_targets">40 Targets</div>
</div>
<div class="stat-badge">
<span class="stat-icon">🥇</span>
<div class="stat-number" id="score4Targets">Loading...</div>
<div class="stat-label" data-i18n="tournament_types.4_targets">4 Targets</div>
<div class="player-tree-list" id="playerTreeList">
<div class="loading"><div class="loading-spinner"></div></div>
</div>
</div>
<!-- Tab Navigation -->
<div class="section">
<div class="tab-navigation">
<button class="tab-btn active" onclick="switchTab('tournament-leaders')" data-i18n="analysis.overall_champions">🏆 Overall Champions</button>
<button class="tab-btn" onclick="switchTab('players')" data-i18n="analysis.all_players">👥 All Players</button>
<!-- RIGHT: player detail -->
<div class="players-right-panel" id="playersRightPanel">
<div class="player-placeholder" id="playerPlaceholder">
<div class="player-placeholder-icon">👤</div>
<div class="player-placeholder-text">Izberi igralca iz seznama</div>
</div>
<div id="playerDetailEmbed" style="display:none"></div>
</div>
<!-- Tournament Leaders Tab -->
<div id="tournament-leaders" class="tab-content active">
<div class="section-title" data-i18n="analysis.overview_champions">Overall Champions by Tournament Type</div>
<div class="tournament-leaders" id="tournamentLeaders">
<div class="loading">
<div class="loading-spinner"></div>
<p>Loading tournament data...</p>
</div>
</div>
</div>
<!-- Players Tab -->
<div id="players" class="tab-content">
<div class="section-title" data-i18n="analysis.select_player">Izberi igralca za analizo</div>
<!-- Controls -->
<div class="controls">
<input type="text" class="search-box" id="searchBox" data-i18n-placeholder="league.search_players_placeholder" placeholder="🔍 Search players by name...">
<select class="sort-select" id="sortSelect">
<option value="name" data-i18n="analysis.sort_by_name">Sort by Name</option>
<option value="best_score" data-i18n="analysis.sort_by_best_score">Sort by Best Score</option>
<option value="average_score" data-i18n="analysis.sort_by_average_score">Sort by Average Score</option>
<option value="total_tournaments" data-i18n="analysis.sort_by_total_tournaments">Sort by Total Tournaments</option>
<option value="total_leagues" data-i18n="analysis.sort_by_total_leagues">Sort by Total Leagues</option>
<option value="total_shots" data-i18n="analysis.sort_by_total_shots">Sort by Total Shots</option>
</select>
</div>
<!-- Players Grid -->
<div class="players-grid" id="playersGrid">
<div class="loading">
<div class="loading-spinner"></div>
<p>Loading players...</p>
</div>
</div>
</div>
</div>
</div>
<script>
@@ -799,187 +881,15 @@
let tournamentData = [];
let filteredPlayers = [];
let currentFilter = 'all';
let currentSort = 'name';
let currentSort = 'best_score';
let playersWithStats = [];
// Initialize page
function initializePage() {
loadPlayerStats();
loadTournamentLeaders();
setupEventListeners();
}
// Switch between tabs
function switchTab(tabName) {
// Hide all tab contents
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// Remove active class from all tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
// Show selected tab content
document.getElementById(tabName).classList.add('active');
// Add active class to clicked button
event.target.classList.add('active');
}
// Load tournament leaders data
async function loadTournamentLeaders() {
try {
const response = await fetch('/api/archive/tournament-leaders');
const result = await response.json();
if (result.status === 'success') {
tournamentData = result.tournament_types;
renderTournamentLeaders();
updateOverallStats();
} else {
console.error('Failed to load tournament data:', result.message);
document.getElementById('tournamentLeaders').innerHTML = `
<div class="empty-state">
<div class="empty-icon">🎯</div>
<h3 data-i18n="analysis.unable_load_data">Ne morem naložiti podatkov turnirja</h3>
<p>${result.message || 'Please try refreshing the page'}</p>
</div>
`;
}
} catch (error) {
console.error('Error loading tournament data:', error);
document.getElementById('tournamentLeaders').innerHTML = `
<div class="empty-state">
<div class="empty-icon">🎯</div>
<h3>Unable to Load Tournament Data</h3>
<p>Please try refreshing the page</p>
</div>
`;
}
}
// Render tournament leaders
function renderTournamentLeaders() {
const leadersContainer = document.getElementById('tournamentLeaders');
if (tournamentData.length === 0) {
leadersContainer.innerHTML = `
<div class="empty-state">
<div class="empty-icon">🎯</div>
<h3 data-i18n="analysis.no_tournament_data">Ni podatkov o turnirju</h3>
<p>Tournament results will appear here once available</p>
</div>
`;
return;
}
// Sort tournament types by target count: 4, 20, 40
const sortOrder = ['4_targets', '20_targets', '40_targets'];
const sortedTournamentData = [...tournamentData].sort((a, b) => {
const indexA = sortOrder.indexOf(a.id);
const indexB = sortOrder.indexOf(b.id);
// If not in sortOrder, put at end
if (indexA === -1 && indexB === -1) return 0;
if (indexA === -1) return 1;
if (indexB === -1) return -1;
return indexA - indexB;
});
leadersContainer.innerHTML = sortedTournamentData.map(tournamentType => {
// Get translated tournament name and description based on tournament type
let translatedName = tournamentType.name;
let translatedDescription = tournamentType.description;
switch(tournamentType.id) {
case '4_targets':
translatedName = t('tournament_types.4_targets');
translatedDescription = t('tournament_types.4_targets_desc');
break;
case '20_targets':
translatedName = t('tournament_types.20_targets');
translatedDescription = t('tournament_types.20_targets_desc');
break;
case '40_targets':
translatedName = t('tournament_types.40_targets');
translatedDescription = t('tournament_types.40_targets_desc');
break;
}
return `
<div class="tournament-card" data-type="${tournamentType.id}">
<div class="tournament-header">
<div class="tournament-name">${translatedName}</div>
<div class="tournament-date">${tournamentType.total_tournaments} ${tournamentType.total_tournaments === 1 ? t('tournament.tournament') : t('tournament.tournaments')}</div>
</div>
<div style="text-align: center; margin-bottom: 28px; color: rgba(255, 255, 255, 0.95); font-size: 1rem; font-weight: 500; position: relative; z-index: 3; background: rgba(0, 0, 0, 0.15); padding: 14px 24px; border-radius: 14px; backdrop-filter: blur(15px); border: 1px solid rgba(255, 255, 255, 0.25); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);">
${translatedDescription}
</div>
<div class="leaders-grid">
<div class="leader-category">
<div class="category-title">🏆 ${t('analysis.best_score')}</div>
<div class="leader-info">
<div class="leader-name">${tournamentType.best_score.player_name || t('messages.no_data')}</div>
<div class="leader-score">${tournamentType.best_score.score || 0}</div>
</div>
</div>
<div class="leader-category">
<div class="category-title">🎯 ${t('analysis.most_tens')}</div>
<div class="leader-info">
<div class="leader-name">${tournamentType.most_tens.player_name || t('messages.no_data')}</div>
<div class="leader-score tens">${tournamentType.most_tens.tens || 0}x 🎯</div>
</div>
</div>
</div>
</div>
`;
}).join('');
}
// Update overall statistics
function updateOverallStats() {
let totalTournaments = 0;
let score20Targets = 0;
let score40Targets = 0;
let score4Targets = 0;
// Calculate statistics across all tournament types
tournamentData.forEach(tournamentType => {
totalTournaments += tournamentType.total_tournaments || 0;
// Get best scores for each tournament type using the id field
if (tournamentType.id === '20_targets' && tournamentType.best_score) {
score20Targets = tournamentType.best_score.score || 0;
} else if (tournamentType.id === '40_targets' && tournamentType.best_score) {
score40Targets = tournamentType.best_score.score || 0;
} else if (tournamentType.id === '4_targets' && tournamentType.best_score) {
score4Targets = tournamentType.best_score.score || 0;
}
});
document.getElementById('totalTournaments').textContent = totalTournaments;
document.getElementById('score20Targets').textContent = score20Targets || '-';
document.getElementById('score40Targets').textContent = score40Targets || '-';
document.getElementById('score4Targets').textContent = score4Targets || '-';
}
// Get tournament type display name
function getTournamentTypeDisplay(tournamentType) {
const typeMap = {
'20_targets': t('tournament_types.20_targets_full'),
'40_targets': t('tournament_types.40_targets_full'),
'4_targets': t('tournament_types.4_targets_full')
};
return typeMap[tournamentType] || tournamentType;
}
// Format date for display
function formatDate(dateString) {
if (!dateString) return t('analysis.unknown_date');
@@ -1026,7 +936,7 @@
filteredPlayers = [...playersWithStats];
// Update total players count
document.getElementById('totalPlayers').textContent = playersData.length;
document.getElementById('playerTreeCount').textContent = playersData.length;
filterAndSortPlayers();
} else {
@@ -1041,176 +951,95 @@
// Setup event listeners
function setupEventListeners() {
const searchBox = document.getElementById('searchBox');
const sortSelect = document.getElementById('sortSelect');
// search handled inline via oninput on the input element
}
searchBox.addEventListener('input', filterAndSortPlayers);
sortSelect.addEventListener('change', (e) => {
currentSort = e.target.value;
function setSort(sort) {
currentSort = sort;
document.querySelectorAll('.psort-btn').forEach(b =>
b.classList.toggle('active', b.dataset.sort === sort));
filterAndSortPlayers();
});
}
// Filter and sort players
function filterAndSortPlayers() {
const searchTerm = document.getElementById('searchBox').value.toLowerCase();
// Filter players
filteredPlayers = playersData.filter(player => {
// Sort full list and assign permanent ranks
const sorted = [...playersData].sort((a, b) => {
switch (currentSort) {
case 'best_score': return (b.stats?.best_tournament_score || 0) - (a.stats?.best_tournament_score || 0);
case 'total_tens': return (b.stats?.total_tens || 0) - (a.stats?.total_tens || 0);
case 'best_single': return (b.stats?.best_tournament_score || 0) - (a.stats?.best_tournament_score || 0);
default: return 0;
}
});
sorted.forEach((p, i) => { p._rank = i + 1; });
// Filter preserving original rank
filteredPlayers = sorted.filter(player => {
const matchesSearch = player.name.toLowerCase().includes(searchTerm);
const matchesFilter = currentFilter === 'all' ||
(currentFilter === 'current' && player.current_player) ||
(currentFilter === 'archived' && !player.current_player);
return matchesSearch && matchesFilter;
});
// Sort players
filteredPlayers.sort((a, b) => {
switch (currentSort) {
case 'name':
return a.name.localeCompare(b.name);
case 'best_score':
return (b.stats?.best_tournament_score || 0) - (a.stats?.best_tournament_score || 0);
case 'average_score':
return (b.stats?.average_tournament_score || 0) - (a.stats?.average_tournament_score || 0);
case 'total_tournaments':
return (b.stats?.total_tournaments || 0) - (a.stats?.total_tournaments || 0);
case 'total_leagues':
return (b.stats?.total_leagues || 0) - (a.stats?.total_leagues || 0);
case 'total_shots':
return (b.stats?.total_shots_fired || 0) - (a.stats?.total_shots_fired || 0);
default:
return 0;
}
});
renderPlayers();
}
// Render players with stats
// Render players into the tree panel
let activePlayerId = null;
function renderPlayers() {
const playersGrid = document.getElementById('playersGrid');
const treeList = document.getElementById('playerTreeList');
document.getElementById('playerTreeCount').textContent = filteredPlayers.length;
if (filteredPlayers.length === 0) {
playersGrid.innerHTML = `
<div class="empty-state">
<div class="empty-icon">👤</div>
<h3>No Players Found</h3>
<p>Try adjusting your search or filter criteria</p>
</div>
`;
treeList.innerHTML = `<div style="padding:24px;text-align:center;color:#bbb;font-size:0.82rem">Ni zadetkov</div>`;
return;
}
playersGrid.innerHTML = filteredPlayers.map(player => `
<div class="player-card ${player.current_player ? 'current-player' : 'archived-player'}"
onclick="viewPlayerStats(${player.id})">
<div class="player-header">
<div class="player-name">${player.name}</div>
</div>
<div class="player-stats">
<div class="stat-item">
<span class="icon">🎯</span>
<span class="label">${t('tournament.tournaments')}:</span>
<span class="value">${player.stats?.total_tournaments || 0}</span>
</div>
<div class="stat-item">
<span class="icon">🏆</span>
<span class="label">${t('league.league')}:</span>
<span class="value">${player.stats?.total_leagues || 0}</span>
</div>
<div class="stat-item">
<span class="icon">📈</span>
<span class="label">${t('analysis.best_score_label')}</span>
<span class="value">${player.stats?.best_tournament_score || 0}</span>
</div>
<div class="stat-item">
<span class="icon">📊</span>
<span class="label">${t('results.average_score')}</span>
<span class="value">${Math.round(player.stats?.average_tournament_score || 0)}</span>
</div>
</div>
<div class="player-chart">
<canvas class="chart-canvas" id="chart-${player.id}"></canvas>
</div>
</div>
`).join('');
// Create mini charts for each player
setTimeout(() => {
filteredPlayers.forEach(player => {
if (player.stats?.performance_trend) {
createMiniChart(player);
treeList.innerHTML = filteredPlayers.map((player) => {
const initials = player.name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
const active = player.id === activePlayerId ? ' active' : '';
let scoreVal, scoreMeta;
if (currentSort === 'total_tens') {
scoreVal = player.stats?.total_tens || 0;
scoreMeta = `${scoreVal} 🎯`;
} else {
scoreVal = player.stats?.best_tournament_score || 0;
scoreMeta = `${scoreVal} pts`;
}
});
}, 100);
const rank = player._rank || 0;
const rankHtml = rank === 1 ? `<div class="player-tree-rank">🥇</div>`
: rank === 2 ? `<div class="player-tree-rank">🥈</div>`
: rank === 3 ? `<div class="player-tree-rank">🥉</div>`
: `<div class="player-tree-rank-num">${rank}</div>`;
return `<div class="player-tree-item${active}" onclick="selectPlayer(${player.id})">
${rankHtml}
<div class="player-tree-info">
<div class="player-tree-name">${player.name}</div>
<div class="player-tree-meta">${player.stats?.total_tournaments || 0}T · ${scoreMeta}</div>
</div>
</div>`;
}).join('');
}
// Render players without stats (fallback)
function renderPlayersBasic() {
// If we can't get stats, try to get basic player list
fetch('/api/players')
.then(response => response.json())
.then(data => {
const basicPlayers = data.players || [];
const playersGrid = document.getElementById('playersGrid');
if (basicPlayers.length === 0) {
playersGrid.innerHTML = `
<div class="empty-state">
<div class="empty-icon">👤</div>
<h3>No Players Found</h3>
<p>No player data available</p>
</div>
`;
return;
}
playersGrid.innerHTML = basicPlayers.map(player => `
<div class="player-card ${player.enabled ? 'current-player' : 'archived-player'}"
onclick="viewPlayerStats(${player.id})">
<div class="player-header">
<div class="player-name">${player.name}</div>
</div>
<div class="player-stats">
<div class="stat-item">
<span class="icon">🎯</span>
<span class="label">${t('tournament.tournaments')}:</span>
<span class="value">Loading...</span>
</div>
<div class="stat-item">
<span class="icon">🏆</span>
<span class="label">${t('league.league')}:</span>
<span class="value">Loading...</span>
</div>
<div class="stat-item">
<span class="icon">📈</span>
<span class="label">${t('analysis.best_score_label')}</span>
<span class="value">Loading...</span>
</div>
<div class="stat-item">
<span class="icon">📊</span>
<span class="label">${t('results.average_score')}</span>
<span class="value">Loading...</span>
</div>
</div>
</div>
`).join('');
playersData = basicPlayers.map(p => ({ id: p.id, name: p.name, current_player: p.enabled, stats: {} }));
filteredPlayers = [...playersData];
document.getElementById('playerTreeCount').textContent = playersData.length;
renderPlayers();
})
.catch(error => {
console.error('Error loading basic players:', error);
const playersGrid = document.getElementById('playersGrid');
playersGrid.innerHTML = `
<div class="empty-state">
<div class="empty-icon">❌</div>
<h3>Error Loading Players</h3>
<p>Unable to load player data</p>
</div>
`;
.catch(() => {
document.getElementById('playerTreeList').innerHTML =
`<div style="padding:24px;text-align:center;color:#bbb;font-size:0.82rem">Napaka pri nalaganju</div>`;
});
}
@@ -1265,9 +1094,20 @@
});
}
// Navigate to player stats
// Select player — load stats into right panel iframe
function selectPlayer(playerId) {
activePlayerId = playerId;
renderPlayers();
document.getElementById('playerPlaceholder').style.display = 'none';
const embed = document.getElementById('playerDetailEmbed');
embed.style.display = 'block';
embed.innerHTML = `<iframe src="/archive/player/${playerId}"></iframe>`;
}
// Navigate to player stats (kept for compatibility)
function viewPlayerStats(playerId) {
window.location.href = `/archive/player/${playerId}`;
selectPlayer(playerId);
}
// Initialize page when DOM is loaded and translations are ready
+627 -203
View File
@@ -25,6 +25,9 @@
color: #333;
}
body.in-iframe .navbar { display: none; }
body.in-iframe { background: transparent; }
/* Standardized Container */
.container {
max-width: 1400px;
@@ -73,6 +76,45 @@
font-weight: 500;
}
/* Tournament Badge Styling */
.stat-badge.tournament-badge {
display: flex;
flex-direction: column;
justify-content: center;
}
.tournament-scores {
display: flex;
gap: 10px;
margin-bottom: 8px;
justify-content: center;
}
.tournament-score-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.tournament-type {
font-size: 0.65rem;
color: #999;
font-weight: 600;
text-transform: uppercase;
}
.tournament-emoji {
font-size: 1rem;
margin-bottom: 2px;
}
.tournament-best {
font-size: 1.3rem;
font-weight: bold;
color: #28a745;
}
/* Charts Section */
.charts-section {
min-height: 450px;
@@ -147,7 +189,7 @@
gap: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
width: 100%;
width: auto;
}
@@ -625,6 +667,49 @@
max-height: 100%;
}
/* Shot Count Cards */
.shot-count-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(70px, 1fr));
gap: 12px;
margin-top: 25px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
}
.shot-count-card {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border: 1px solid #e9ecef;
border-radius: 10px;
padding: 12px;
text-align: center;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 90px;
}
.shot-count-card:hover {
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.15);
transform: translateY(-2px);
border-color: #28a745;
}
.shot-score {
font-size: 1.3rem;
font-weight: bold;
color: #28a745;
margin-bottom: 8px;
}
.shot-count {
font-size: 1.8rem;
font-weight: bold;
color: #333;
}
.history-list {
flex: 1;
overflow-y: auto;
@@ -968,7 +1053,226 @@
min-height: 300px;
padding: 10px;
}
/* Responsive Shot Count Cards */
.shot-count-cards {
grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
gap: 8px;
margin-top: 20px;
padding-top: 15px;
}
.shot-count-card {
min-height: 80px;
padding: 10px;
}
.shot-score {
font-size: 1.1rem;
margin-bottom: 5px;
}
.shot-count {
font-size: 1.5rem;
}
/* Responsive Overall Accuracy Dashboard */
.overall-top-section {
flex-direction: column;
gap: 15px;
margin-bottom: 15px;
}
.gauge-section {
flex: 0 0 100%;
min-height: 250px;
}
.stat-card-group {
flex: 0 0 100%;
grid-template-columns: 2fr 2fr;
gap: 10px;
}
.overall-bottom-section {
grid-template-columns: 1fr;
gap: 15px;
margin-top: 15px;
}
.overall-bar-chart,
.overall-radar-chart {
min-height: 280px;
padding: 10px;
}
.stat-card {
padding: 10px;
}
.stat-card-icon {
font-size: 1.4rem;
margin-bottom: 4px;
}
.stat-card-value {
font-size: 1.2rem;
margin-bottom: 2px;
}
.stat-card-label {
font-size: 0.65rem;
}
.gauge-stats {
gap: 15px;
padding: 8px 12px;
font-size: 0.8rem;
}
.gauge-stat-value {
font-size: 1.1rem;
}
.accuracy-gauge-wrapper {
height: 250px;
}
}
/* ── SINGLE CARD LAYOUT ── */
body { overflow: hidden; }
.ps-layout {
display: flex; gap: 8px;
height: calc(100vh - 50px);
padding: 8px 12px 10px;
}
body.in-iframe .ps-layout { height: 100vh; }
.ps-card {
background: white; border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border: 1px solid #e9ecef;
overflow: hidden; display: flex; flex-direction: column;
}
.ps-left {
flex: 1; min-width: 0;
}
/* Card header */
.ps-topbar {
display: flex; align-items: center;
padding: 6px 12px; flex-shrink: 0; gap: 8px;
background: #f8f9fa; border-bottom: 2px solid #e9ecef;
border-radius: 10px 10px 0 0;
}
.ps-type-btns { display: flex; gap: 4px; flex-shrink: 0; }
.ps-type-btns .type-btn {
padding: 7px 10px; font-size: 0.7rem; border-radius: 6px;
border: 2px solid #dee2e6; background: white; color: #666;
cursor: pointer; font-weight: 600; transition: all 0.15s; white-space: nowrap;
flex-shrink: 0; width: auto;
}
.ps-type-btns .type-btn.active.targets-40 { background: #8b00ff; border-color: #6a00cc; color: white; }
.ps-type-btns .type-btn.active.targets-20 { background: #fd7e14; border-color: #e06c0a; color: white; }
.ps-type-btns .type-btn.active.targets-4 { background: #28a745; border-color: #1e7e34; color: white; }
.ps-type-btns .type-btn:hover:not(.active) { border-color: #28a745; color: #28a745; }
.ps-mini-stats { display: flex; gap: 20px; margin-left: auto; }
.ps-mini-stats .chart-stat { text-align: center; }
.ps-mini-stats .chart-stat-value { font-size: 0.85rem; font-weight: 700; color: #28a745; line-height: 1.1; }
.ps-mini-stats .chart-stat-label { font-size: 0.6rem; color: #aaa; }
/* Graph sections */
.ps-sections { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; }
.ps-section {
display: flex; flex-direction: column; flex: 1; min-height: 0;
border-bottom: 1px solid #e9ecef; padding: 6px 14px 4px;
}
.ps-section:last-child { border-bottom: none; }
.ps-section-header {
display: flex; align-items: baseline; gap: 6px; flex-shrink: 0; margin-bottom: 2px;
}
.ps-section-title {
font-size: 0.7rem; font-weight: 700; color: #444; white-space: nowrap;
}
.ps-section-desc {
font-size: 0.6rem; color: #aaa;
}
.ps-section-body {
display: flex; gap: 8px; flex: 1; min-height: 0;
}
.ps-chart-wrap {
flex: 1; min-width: 0; min-height: 0; position: relative;
}
.ps-chart-wrap canvas { width: 100% !important; height: 100% !important; }
.ps-radar-wrap { flex: 0 0 30%; }
/* Shot count badges */
.ps-shot-counts {
flex: 0 0 72px; border-left: 1px solid #e9ecef;
display: flex; flex-direction: column; justify-content: space-around;
padding: 4px 8px; gap: 2px;
}
.ps-sc-row {
display: flex; align-items: center; justify-content: space-between;
gap: 4px; font-size: 0.62rem;
}
.ps-sc-badge {
width: 18px; height: 18px; border-radius: 5px;
display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 0.6rem; color: white; flex-shrink: 0;
}
.ps-sc-count {
font-size: 0.68rem; font-weight: 600; color: #555; text-align: right;
}
/* Right: history column */
.ps-right {
width: 220px; flex-shrink: 0; display: flex; flex-direction: column;
}
.ps-hist-header {
display: flex; flex-direction: column; gap: 5px;
padding: 7px 10px; flex-shrink: 0;
background: #f8f9fa; border-bottom: 2px solid #e9ecef;
border-radius: 10px 10px 0 0;
}
.ps-hist-title { font-size: 0.78rem; font-weight: 700; color: #444; }
.ps-hist-tabs { display: flex; gap: 4px; }
.ps-hist-tabs .history-tab {
flex: 1; padding: 3px 6px; font-size: 0.65rem; border-radius: 6px;
border: 2px solid #dee2e6; background: white; color: #555;
cursor: pointer; font-weight: 600; transition: all 0.15s; white-space: nowrap;
}
.ps-hist-tabs .history-tab.active { background: #28a745; border-color: #1e7e34; color: white; }
.ps-hist-tabs .history-tab:hover:not(.active) { border-color: #28a745; color: #28a745; }
.ps-history-body { flex: 1; overflow-y: auto; min-height: 0; }
.ps-history-body::-webkit-scrollbar { width: 4px; }
.ps-history-body::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 2px; }
.ps-hist-item {
display: flex; align-items: center; gap: 6px;
padding: 7px 10px 7px 12px;
cursor: pointer; border-left: 3px solid transparent;
transition: all 0.15s; border-bottom: 1px solid #f5f5f5; font-size: 0.78rem;
}
.ps-hist-item:hover { background: #f0faf3; border-left-color: #28a745; }
.ps-hist-item.active { background: #28a745; border-left-color: #1e7e34; color: white; }
.ps-hist-date { color: #888; font-size: 0.68rem; flex-shrink: 0; }
.ps-hist-item.active .ps-hist-date { color: rgba(255,255,255,0.8); }
.ps-hist-info { flex: 1; min-width: 0; }
.ps-hist-score { font-weight: 700; color: #28a745; flex-shrink: 0; font-size: 0.88rem; }
.ps-hist-item.active .ps-hist-score { color: white; }
.ps-hist-arrow { color: #ddd; flex-shrink: 0; font-size: 0.72rem; }
.ps-league-scores { display: flex; gap: 3px; flex-wrap: wrap; }
.ps-ls { font-size: 0.68rem; padding: 1px 4px; border-radius: 4px; background: #f0f0f0; }
.ps-ls-joker { opacity: 0.5; }
.ps-ls-excl { text-decoration: line-through; color: #999; background: #fee; }
.history-tab-content { display: none; }
.history-tab-content.active { display: block; }
</style>
<script src="/static/js/i18n.js"></script>
</head>
@@ -977,240 +1281,127 @@
<div class="navbar">
<div class="navbar-title">📊 {{ player.name }} - <span data-i18n="players.player_stats">Stats</span></div>
<div class="navbar-controls">
<a href="/" class="nav-btn">📺 <span data-i18n="navigation.dashboard">Dashboard</span></a>
<a href="/archive/player-analysis" class="nav-btn active">👤 <span data-i18n="players.player_analysis">Player Analysis</span></a>
<a href="/archive" class="nav-btn">📚 <span data-i18n="navigation.archive">Archive</span></a>
<a href="/" class="nav-btn"></a>
</div>
</div>
<div class="container">
<!-- Stats Overview -->
<div class="stats-badges">
<div class="stat-badge">
<span class="stat-icon">🏆</span>
<div class="stat-number">{{ stats.total_tournaments }}</div>
<div class="stat-label" data-i18n="tournament.tournaments">Tournaments</div>
<div class="ps-layout">
<!-- LEFT: stats card -->
<div class="ps-card ps-left">
<!-- Card header: filter buttons -->
<div class="ps-topbar ps-topbar-header">
<div class="tournament-type-buttons ps-type-btns">
<button class="type-btn active targets-40" data-type="40 Targets"><span>💪</span> 40T</button>
<button class="type-btn targets-20" data-type="20 Targets"><span></span> 20T</button>
<button class="type-btn targets-4" data-type="4 Targets"><span>🎯</span> 4T</button>
</div>
<div class="stat-badge">
<span class="stat-icon">🎖️</span>
<div class="stat-number">{{ stats.total_leagues }}</div>
<div class="stat-label" data-i18n="league.league">Leagues</div>
</div>
<div class="stat-badge">
<span class="stat-icon"></span>
<div class="stat-number">{{ stats.best_tournament_score }}</div>
<div class="stat-label" data-i18n="results.best_score">Best Score</div>
</div>
<div class="stat-badge">
<span class="stat-icon">📊</span>
<div class="stat-number">{{ stats.average_tournament_score|round|int if stats.average_tournament_score > 0 else 0 }}</div>
<div class="stat-label" data-i18n="results.average_score">Average</div>
</div>
<div class="stat-badge">
<span class="stat-icon">🔫</span>
<div class="stat-number">{{ stats.total_shots_fired|default(0) }}</div>
<div class="stat-label" data-i18n="results.total_shots">Total Shots</div>
</div>
<div class="stat-badge">
<span class="stat-icon">📉</span>
<div class="stat-number">{{ stats.worst_tournament_score if stats.worst_tournament_score > 0 else 0 }}</div>
<div class="stat-label" data-i18n="results.worst_score">Worst Score</div>
<div class="ps-mini-stats">
<div class="chart-stat"><div class="chart-stat-value" id="gameCount">0</div><div class="chart-stat-label">Iger</div></div>
<div class="chart-stat"><div class="chart-stat-value" id="bestScore">0</div><div class="chart-stat-label">Najboljši</div></div>
<div class="chart-stat"><div class="chart-stat-value" id="avgScore">0</div><div class="chart-stat-label">Povprečje</div></div>
</div>
</div>
<!-- Card 1: Overall Accuracy (Colorful - No Filters) -->
<div class="overall-accuracy-card">
<div class="panel-title">📊 <span data-i18n="analysis.shot_accuracy">Overall Shot Accuracy</span></div>
<div class="ps-sections">
<!-- Overall Shot Accuracy Section -->
<div class="overall-content-wrapper">
<div class="overall-chart-left">
<canvas id="overallAccuracyChart"></canvas>
</div>
<div class="overall-radar-right">
<canvas id="overallRadarChart"></canvas>
<!-- Section 1: Natančnost -->
<div class="ps-section">
<div class="ps-section-header">
<span class="ps-section-title">🎯 Natančnost strelov</span>
<span class="ps-section-desc">Kako pogosto zadeneš posamezno vrednost</span>
</div>
<div class="ps-section-body">
<div class="ps-chart-wrap"><canvas id="accuracyChart"></canvas></div>
</div>
</div>
<!-- Card 2: Filtered Accuracy (Main Outer Card - White) -->
<div style="background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); padding: 24px; margin-bottom: 30px; border: 1px solid #e9ecef;">
<div class="panel-title">📊 <span data-i18n="analysis.comparison">Filtered Analysis</span></div>
<!-- Filter Controls (Top - Outside inner cards) -->
<div class="tournament-type-buttons" style="margin-bottom: 30px;">
<button class="type-btn active targets-40" data-type="40 Targets">
<span>💪</span> <span data-i18n="tournament_types.40_targets">40 Targets</span>
</button>
<button class="type-btn targets-20" data-type="20 Targets">
<span></span> <span data-i18n="tournament_types.20_targets">20 Targets</span>
</button>
<button class="type-btn targets-4" data-type="4 Targets">
<span>🎯</span> <span data-i18n="tournament_types.4_targets">4 Targets</span>
</button>
<!-- Section 2: Napredek + Profil -->
<div class="ps-section">
<div class="ps-section-body">
<div style="display:flex; flex-direction:column; flex:1; min-width:0; min-height:0;">
<div class="ps-section-header">
<span class="ps-section-title">📈 Napredek</span>
<span class="ps-section-desc">Rezultati skozi čas po turnirjih</span>
</div>
<!-- Stats Inner Card -->
<div style="background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); border-radius: 8px; padding: 16px; margin-bottom: 20px; border: 1px solid #e9ecef;">
<div class="chart-info" id="chartInfo">
<div class="chart-stats">
<div class="chart-stat">
<div class="chart-stat-value" id="gameCount">0</div>
<div class="chart-stat-label" data-i18n="tournament.tournaments">Games</div>
<div class="ps-chart-wrap" style="flex:1; min-height:0;"><canvas id="tournamentChart"></canvas></div>
</div>
<div class="chart-stat">
<div class="chart-stat-value" id="avgScore">0</div>
<div class="chart-stat-label" data-i18n="results.average_score">Average</div>
</div>
<div class="chart-stat">
<div class="chart-stat-value" id="bestScore">0</div>
<div class="chart-stat-label" data-i18n="results.best_score">Best</div>
</div>
</div>
</div>
</div>
<!-- Performance Line Chart Inner Card -->
<div style="background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); border-radius: 8px; padding: 16px; margin-bottom: 20px; border: 1px solid #e9ecef;">
<div class="chart-container">
<canvas id="tournamentChart"></canvas>
</div>
</div>
<!-- Filtered Shot Accuracy Charts Container -->
<div class="accuracy-charts-container">
<!-- Shot Accuracy Bar Chart Card -->
<div style="background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); border-radius: 8px; padding: 16px; border: 1px solid #e9ecef;">
<div class="accuracy-section">
<div class="accuracy-chart-container">
<canvas id="accuracyChart"></canvas>
</div>
</div>
</div>
<!-- Shot Accuracy Radar Chart Card -->
<div style="background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); border-radius: 8px; padding: 16px; border: 1px solid #e9ecef;">
<div class="accuracy-section">
<div class="accuracy-chart-container">
<canvas id="accuracyRadarChart"></canvas>
<div style="display:flex; flex-direction:column; flex:0 0 32%; min-height:0; border-left:1px solid #e9ecef; padding-left:10px;">
<div class="ps-section-header">
<span class="ps-section-title">🕸 Profil</span>
<span class="ps-section-desc">Razporeditev strelov</span>
</div>
<div class="ps-chart-wrap" style="flex:1; min-height:0;"><canvas id="accuracyRadarChart"></canvas></div>
</div>
</div>
</div>
</div>
<!-- Bottom Section - History Tables in Single Card -->
<div class="history-section">
<div class="panel-title">📜 <span data-i18n="analysis.player_history">Player History</span></div>
</div><!-- ps-left card -->
<!-- Tab Navigation -->
<div class="history-tabs">
<button class="history-tab active" data-tab="tournaments">
🎯 <span data-i18n="analysis.tournament_history">Tournament History</span>
</button>
<button class="history-tab" data-tab="leagues">
🏆 <span data-i18n="league.league_championship">League History</span>
</button>
<!-- RIGHT: history card -->
<div class="ps-card ps-right">
<div class="ps-hist-header">
<div class="ps-hist-title">📜 Zgodovina</div>
<div class="history-tabs ps-hist-tabs">
<button class="history-tab active" data-tab="tournaments">🎯 T</button>
<button class="history-tab" data-tab="leagues">🏆 L</button>
</div>
</div>
<!-- Tournament History Table -->
<div class="ps-history-body">
<!-- Tournament History -->
<div id="tournaments-tab" class="history-tab-content active">
{% if stats.tournament_history %}
<div class="table-wrapper">
<table class="history-table">
<thead>
<tr>
<th data-i18n="general.date">Date</th>
<th data-i18n="general.type">Type</th>
<th data-i18n="results.score">Score</th>
<th data-i18n="results.shots">Shots</th>
<th data-i18n="general.action">Action</th>
</tr>
</thead>
<tbody>
{% for tournament in stats.tournament_history %}
<tr class="history-table-row" onclick="window.location.href='/archive/tournament/{{ tournament.filename }}'">
<td class="date-cell">{{ tournament.date[:10] if tournament.date != 'Unknown' else 'Unknown' }}</td>
<td><span class="tournament-type-badge" data-tournament-type="{{ tournament.tournament_type }}">{{ tournament.tournament_type.replace('_', ' ')|title }}</span></td>
<td class="score-cell"><strong>{{ tournament.score }}</strong></td>
<td>{{ tournament.shots_fired }}</td>
<td><span class="view-link" data-i18n="general.view">View →</span></td>
</tr>
<div class="ps-hist-item" onclick="window.location.href='/archive/tournament/{{ tournament.filename }}'">
<div class="ps-hist-date">{{ tournament.date[:10] if tournament.date != 'Unknown' else '?' }}</div>
<div class="ps-hist-info">
<span class="tournament-type-badge" data-tournament-type="{{ tournament.tournament_type }}">{{ tournament.tournament_type.replace('_targets','T') }}</span>
</div>
<div class="ps-hist-score">{{ tournament.score }}</div>
<div class="ps-hist-arrow"></div>
</div>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<div class="empty-icon">🎯</div>
<div data-i18n="messages.no_archives_found">No tournament history</div>
</div>
<div class="empty-state"><div class="empty-icon">🎯</div><div>Ni zgodovine</div></div>
{% endif %}
</div>
<!-- League History Table -->
<!-- League History -->
<div id="leagues-tab" class="history-tab-content">
{% if stats.league_history %}
<div class="table-wrapper">
<table class="history-table">
<thead>
<tr>
<th data-i18n="general.date">Date</th>
<th colspan="5" style="text-align: center;">Tournament Scores</th>
<th data-i18n="league.final">Final Score</th>
<th data-i18n="general.action">Action</th>
</tr>
<tr>
<th></th>
<th style="text-align: center;">T1</th>
<th style="text-align: center;">T2</th>
<th style="text-align: center;">T3</th>
<th style="text-align: center;">T4</th>
<th style="text-align: center;">T5</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for league in stats.league_history %}
<tr class="history-table-row league-history-row" data-league-index="{{ loop.index0 }}" onclick="window.location.href='/archive/league/{{ league.filename }}'">
<td class="date-cell">{{ league.date[:10] if league.date != 'Unknown' else 'Unknown' }}</td>
<div class="ps-hist-item" onclick="window.location.href='/archive/league/{{ league.filename }}'">
<div class="ps-hist-date">{{ league.date[:10] if league.date != 'Unknown' else '?' }}</div>
<div class="ps-hist-info ps-league-scores">
{% if league.tournament_results %}
{% for i in range(league.tournament_results|length) %}
{% set result = league.tournament_results[i] %}
{% set tournament_num = i + 1 %}
{% set is_excluded = league.excluded_tournament and league.excluded_tournament == tournament_num %}
{% set is_excluded = league.excluded_tournament and league.excluded_tournament == (i+1) %}
{% set is_joker = result.joker or not result.participated %}
<td style="text-align: center;" class="league-score-cell {% if is_joker %}joker-tournament{% elif is_excluded %}excluded-tournament{% endif %}"
title="{% if is_joker %}Joker used (skipped){% elif is_excluded %}Lowest score - excluded from final{% endif %}">
{% if is_joker %}<span style="opacity: 0.7;">🃏</span>{% elif is_excluded %}<span class="removed-score">{{ result.score if result else '-' }}</span>{% else %}{{ result.score if result else '-' }}{% endif %}
</td>
<span class="ps-ls {% if is_joker %}ps-ls-joker{% elif is_excluded %}ps-ls-excl{% endif %}">
{% if is_joker %}🃏{% elif is_excluded %}<s>{{ result.score if result else '-' }}</s>{% else %}{{ result.score if result else '-' }}{% endif %}
</span>
{% endfor %}
{% else %}
<td style="text-align: center;">-</td>
<td style="text-align: center;">-</td>
<td style="text-align: center;">-</td>
<td style="text-align: center;">-</td>
<td style="text-align: center;">-</td>
{% endif %}
<td class="score-cell"><strong>{{ league.final_score }}</strong></td>
<td><span class="view-link" data-i18n="general.view">View →</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<div class="empty-icon">🏆</div>
<div data-i18n="messages.no_archives_found">No league history</div>
</div>
{% endif %}
</div>
<div class="ps-hist-score">{{ league.final_score }}</div>
<div class="ps-hist-arrow"></div>
</div>
{% endfor %}
{% else %}
<div class="empty-state"><div class="empty-icon">🏆</div><div>Ni zgodovine</div></div>
{% endif %}
</div>
</div><!-- ps-history-body -->
</div><!-- ps-right card -->
</div>
</div><!-- ps-layout -->
<script>
// Player data from Flask
@@ -1342,11 +1533,75 @@
}
});
// Create overall accuracy bar chart
// Create overall bar chart
createOverallAccuracyChart(totalCounts);
// Create overall radar chart
// Create overall pie chart
createOverallRadarChart(totalCounts);
// Populate shot count cards
populateShotCountCards(totalCounts);
}
// Populate shot count cards below charts
function populateShotCountCards(totalCounts) {
const scoreMap = {
10: totalCounts.tens,
9: totalCounts.nines,
8: totalCounts.eights,
7: totalCounts.sevens,
6: totalCounts.sixes,
5: totalCounts.fives,
4: totalCounts.fours,
3: totalCounts.threes,
2: totalCounts.twos,
1: totalCounts.ones,
0: totalCounts.zeros
};
// Update shot count cards
for (const [score, count] of Object.entries(scoreMap)) {
const countElement = document.getElementById(`count-${score}`);
if (countElement) {
countElement.textContent = count;
}
}
// Update top stat badges
updateTopStatsBadges(totalCounts);
}
// Update top stat badges with overall metrics
function updateTopStatsBadges(totalCounts) {
// Perfect 10s count
const totalTensBadge = document.getElementById('totalTens-badge');
if (totalTensBadge) {
totalTensBadge.textContent = totalCounts.tens;
}
// Calculate most common shot value
let mostCommonShot = 0;
let maxCount = 0;
const shotScores = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
const shotCountsArray = [
totalCounts.tens, totalCounts.nines, totalCounts.eights,
totalCounts.sevens, totalCounts.sixes, totalCounts.fives,
totalCounts.fours, totalCounts.threes, totalCounts.twos,
totalCounts.ones, totalCounts.zeros
];
for (let i = 0; i < shotScores.length; i++) {
if (shotCountsArray[i] > maxCount) {
maxCount = shotCountsArray[i];
mostCommonShot = shotScores[i];
}
}
const mostCommonBadge = document.getElementById('mostCommon-badge');
if (mostCommonBadge) {
mostCommonBadge.textContent = mostCommonShot;
}
}
// Create overall accuracy bar chart
@@ -1505,6 +1760,124 @@
});
}
// Create accuracy gauge chart (doughnut showing overall accuracy percentage)
let accuracyGaugeChartInstance = null;
function createAccuracyGaugeChart(totalCounts) {
const canvas = document.getElementById('accuracyGaugeChart');
if (!canvas) {
console.warn('Accuracy gauge chart canvas not found');
return;
}
const ctx = canvas.getContext('2d');
// Destroy existing chart if it exists
if (accuracyGaugeChartInstance) {
accuracyGaugeChartInstance.destroy();
}
// Calculate accuracy percentage (perfect shots 8-10 / total shots)
const perfectShots = totalCounts.tens + totalCounts.nines + totalCounts.eights;
const totalShots = Object.values(totalCounts).reduce((a, b) => a + b, 0);
const accuracyPercentage = totalShots > 0 ? Math.round((perfectShots / totalShots) * 100) : 0;
// Calculate consistency score (lower variance = higher consistency)
const shotValues = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
const shotCounts = [totalCounts.tens, totalCounts.nines, totalCounts.eights, totalCounts.sevens, totalCounts.sixes, totalCounts.fives, totalCounts.fours, totalCounts.threes, totalCounts.twos, totalCounts.ones, totalCounts.zeros];
let mean = 0;
if (totalShots > 0) {
mean = shotValues.reduce((sum, val, i) => sum + (val * shotCounts[i]), 0) / totalShots;
}
let variance = 0;
if (totalShots > 0) {
variance = shotValues.reduce((sum, val, i) => sum + (Math.pow(val - mean, 2) * shotCounts[i]), 0) / totalShots;
}
const consistency = Math.max(0, Math.round(100 - (variance * 5))); // Scale variance to 0-100
// Update DOM with percentages
document.getElementById('accuracyPercentage').textContent = accuracyPercentage + '%';
document.getElementById('consistencyScore').textContent = consistency;
const gaugeData = [accuracyPercentage, 100 - accuracyPercentage];
accuracyGaugeChartInstance = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Accuracy', 'Missed'],
datasets: [{
data: gaugeData,
backgroundColor: ['#28a745', '#f0f0f0'],
borderColor: ['#28a745', '#e0e0e0'],
borderWidth: 3,
circumference: 180,
rotation: 270
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return context.label + ': ' + context.parsed + '%';
}
}
}
}
}
});
}
// Update stat cards with performance metrics
function updateStatCards(statsData, totalCounts) {
try {
// Count tournaments
const tournaments = statsData.tournament_history ? statsData.tournament_history.length : 0;
document.getElementById('totalTournaments').textContent = tournaments;
// Count total 10s
const totalTens = totalCounts.tens || 0;
document.getElementById('totalTens').textContent = totalTens;
// Calculate variance
const shotValues = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
const shotCounts = [totalCounts.tens, totalCounts.nines, totalCounts.eights, totalCounts.sevens, totalCounts.sixes, totalCounts.fives, totalCounts.fours, totalCounts.threes, totalCounts.twos, totalCounts.ones, totalCounts.zeros];
const totalShots = shotCounts.reduce((a, b) => a + b, 0);
let mean = 0;
if (totalShots > 0) {
mean = shotValues.reduce((sum, val, i) => sum + (val * shotCounts[i]), 0) / totalShots;
}
let variance = 0;
if (totalShots > 0) {
variance = shotValues.reduce((sum, val, i) => sum + (Math.pow(val - mean, 2) * shotCounts[i]), 0) / totalShots;
}
document.getElementById('scoreVariance').textContent = variance.toFixed(2);
// Calculate range (highest - lowest non-zero score)
let minScore = null;
let maxScore = null;
for (let i = 0; i < shotValues.length; i++) {
if (shotCounts[i] > 0) {
if (maxScore === null) maxScore = shotValues[i];
minScore = shotValues[i];
}
}
const range = maxScore !== null ? (maxScore - minScore) : 0;
document.getElementById('scoreRange').textContent = range;
} catch (error) {
console.error('Error updating stat cards:', error);
}
}
// Load shot accuracy data from template context
function loadShotAccuracyData() {
try {
@@ -1672,16 +2045,62 @@
const avgScore = gameCount > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / gameCount) : 0;
const bestScore = gameCount > 0 ? Math.max(...scores) : 0;
// Calculate most common shot value
let mostCommonShot = 0;
const shotCounts = { 10: 0, 9: 0, 8: 0, 7: 0, 6: 0, 5: 0, 4: 0, 3: 0, 2: 0, 1: 0, 0: 0 };
tournaments.forEach(tournament => {
if (tournament.shot_breakdown) {
const breakdown = tournament.shot_breakdown;
shotCounts[10] += breakdown.tens || 0;
shotCounts[9] += breakdown.nines || 0;
shotCounts[8] += breakdown.eights || 0;
shotCounts[7] += breakdown.sevens || 0;
shotCounts[6] += breakdown.sixes || 0;
shotCounts[5] += breakdown.fives || 0;
shotCounts[4] += breakdown.fours || 0;
shotCounts[3] += breakdown.threes || 0;
shotCounts[2] += breakdown.twos || 0;
shotCounts[1] += breakdown.ones || 0;
shotCounts[0] += breakdown.zeros || 0;
}
});
// Find shot value with highest count
let maxCount = 0;
for (const [score, count] of Object.entries(shotCounts)) {
if (count > maxCount) {
maxCount = count;
mostCommonShot = parseInt(score);
}
}
// Update basic stats
document.getElementById('gameCount').textContent = gameCount;
document.getElementById('avgScore').textContent = avgScore;
document.getElementById('bestScore').textContent = bestScore;
document.getElementById('avgScore').textContent = avgScore;
const mcEl = document.getElementById('mostCommonShot'); if (mcEl) mcEl.textContent = mostCommonShot;
// Update shot accuracy stats (if available in tournament data)
updateAccuracyStats(tournaments);
}
// Update shot accuracy statistics
function renderShotCountTable(counts) {
const el = document.getElementById('shotCountTable');
if (!el) return;
const labels = [10,9,8,7,6,5,4,3,2,1,0];
const colors = [
'#1a7a3c','#2d9e54','#3db86a','#5ec47c','#88d4a0',
'#b0dfc0','#c5c5c5','#e0a89a','#e07a6e','#d95050','#c0392b'
];
el.innerHTML = labels.map((lbl, i) => `
<div class="ps-sc-row">
<div class="ps-sc-badge" style="background:${colors[i]}">${lbl}</div>
<div class="ps-sc-count">${counts[i]}</div>
</div>`).join('');
}
function updateAccuracyStats(tournaments) {
let tens = 0, nines = 0, eights = 0, sevens = 0, sixes = 0;
let fives = 0, fours = 0, threes = 0, twos = 0, ones = 0, zeros = 0;
@@ -1748,6 +2167,9 @@
// Render accuracy bar chart
createAccuracyChart(tens, nines, eights, sevens, sixes, fives, fours, threes, twos, ones, zeros);
// Render shot count table
renderShotCountTable([tens, nines, eights, sevens, sixes, fives, fours, threes, twos, ones, zeros]);
console.log(`Shot accuracy for ${currentTournamentType}:`, {
tens, nines, eights, sevens, sixes, fives, fours, threes, twos, ones, zeros
});
@@ -1767,17 +2189,17 @@
function generateColorfulArray() {
// Color mapping from green (perfect 10) to red (misses 0): labels are [10,9,8,7,6,5,4,3,2,1,0]
return [
'#2E7D32', // 10 - green (perfect)
'#388E3C', // 9 - green
'#43A047', // 8 - green
'#558B2F', // 7 - light green
'#9CCC65', // 6 - lime green
'#FDD835', // 5 - yellow
'#FBC02D', // 4 - golden yellow
'#FFA726', // 3 - orange
'#FF7043', // 2 - light orange-red
'#E53935', // 1 - red
'#C62828' // 0 - dark red (miss)
'#4A9D6F', // 10 - medium green (perfect)
'#5BA97A', // 9 - medium green
'#6CB585', // 8 - medium green
'#7DC285', // 7 - medium light green
'#8FCC7F', // 6 - medium lime green
'#D4B84D', // 5 - medium yellow
'#CDA642', // 4 - medium golden yellow
'#D99A5D', // 3 - medium orange
'#D87A6C', // 2 - medium light orange-red
'#C85C5C', // 1 - medium red
'#B84A4A' // 0 - medium dark red (miss)
];
}
@@ -2071,6 +2493,8 @@
});
}
if (window.self !== window.top) document.body.classList.add('in-iframe');
// Initialize page when i18n is ready
window.addEventListener('i18nReady', initializePage);
</script>
File diff suppressed because it is too large Load Diff
+253 -120
View File
@@ -96,7 +96,7 @@
.tv-container {
height: calc(100vh - 90px);
display: grid;
grid-template-columns: 1fr 3fr;
grid-template-columns: 1fr 2fr;
gap: 20px;
padding: 20px;
}
@@ -105,18 +105,26 @@
.left-column {
display: flex;
flex-direction: column;
gap: 20px;
gap: 0;
overflow: hidden;
flex: 1;
min-height: 0;
}
.results-header {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 25px;
border-radius: 12px 12px 0 0;
box-shadow: none;
padding: 15px;
text-align: center;
flex-shrink: 0;
color: white;
position: relative;
overflow: hidden;
min-height: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* Dynamic header colors based on tournament type */
@@ -149,37 +157,37 @@
}
.header-logo {
height: 70px;
max-width: 180px;
height: 60px;
max-width: 160px;
object-fit: contain;
margin-bottom: 20px;
margin-bottom: 12px;
filter: brightness(1.2) contrast(1.1);
background-color: white;
padding: 10px;
padding: 8px;
border-radius: 8px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.results-title {
font-size: 2.2rem;
font-size: 2rem;
font-weight: 700;
color: rgb(255, 255, 255);
margin-bottom: 10px;
margin-bottom: 6px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.results-subtitle {
font-size: 1.1rem;
font-size: 1rem;
color: rgba(255, 255, 255, 0.95);
margin-bottom: 20px;
margin-bottom: 12px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.results-meta {
display: flex;
justify-content: space-around;
gap: 20px;
gap: 12px;
}
.meta-item {
@@ -187,7 +195,7 @@
}
.meta-number {
font-size: 1.8rem;
font-size: 1.2rem;
font-weight: 700;
color: #ffffff;
display: block;
@@ -195,83 +203,120 @@
}
.meta-label {
font-size: 0.8rem;
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.9);
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.3px;
font-weight: 500;
}
/* Modern Podium Design */
.podium-section {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 25px;
border-radius: 0 0 12px 12px;
padding: 12px;
flex: 1;
display: flex;
flex-direction: column;
overflow: visible;
min-height: 0;
box-shadow: none;
border: 1px solid #e9ecef;
}
.podium-title {
text-align: center;
font-size: 1.4rem;
font-size: 0.9rem;
font-weight: 700;
color: #2c3e50;
margin-bottom: 20px;
margin-bottom: 8px;
}
.podium-container {
display: flex;
flex-direction: column;
gap: 15px;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 0.75fr 0.75fr 1.5fr;
gap: 10px;
overflow: hidden;
flex: 1;
min-height: 0;
}
.podium-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 20px;
box-shadow: none;
padding: 12px 12px 18px 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
transition: all 0.2s ease;
gap: 8px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
border-left: 5px solid;
border: 1px solid #e9ecef;
border-top-width: 6px;
justify-content: flex-end;
}
.podium-card::before {
content: '';
position: absolute;
top: 6px;
left: 1px;
right: 1px;
bottom: 1px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0) 100%);
pointer-events: none;
z-index: 1;
border-radius: 0 0 11px 11px;
}
.podium-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
box-shadow: none;
}
.podium-card.rank-1 {
border-left-color: #ffd700;
background: linear-gradient(135deg, #fff9e6 0%, #ffffff 100%);
border-top-color: #ffd700;
border-color: #ffd700;
background: linear-gradient(135deg, #fffbf0 0%, #fff9e6 100%);
grid-column: 2;
grid-row: 1 / 4;
}
.podium-card.rank-2 {
border-left-color: #c0c0c0;
background: linear-gradient(135deg, #f5f5f5 0%, #ffffff 100%);
border-top-color: #c0c0c0;
border-color: #c0c0c0;
background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
grid-column: 1;
grid-row: 2 / 4;
}
.podium-card.rank-3 {
border-left-color: #cd7f32;
background: linear-gradient(135deg, #fdf6f0 0%, #ffffff 100%);
border-top-color: #cd7f32;
border-color: #cd7f32;
background: linear-gradient(135deg, #fff5f0 0%, #ffe8dc 100%);
grid-column: 3;
grid-row: 3;
}
.rank-display {
display: flex;
flex-direction: column;
align-items: center;
min-width: 60px;
gap: 2px;
position: relative;
z-index: 2;
}
.medal {
font-size: 4.5rem;
line-height: 1;
}
.rank-number {
font-size: 1.8rem;
font-weight: bold;
font-size: 1.9rem;
font-weight: 900;
color: #333;
line-height: 1;
}
@@ -280,86 +325,90 @@
.podium-card.rank-2 .rank-number { color: #696969; }
.podium-card.rank-3 .rank-number { color: #8b4513; }
.rank-suffix {
font-size: 0.7rem;
color: #666;
text-transform: uppercase;
font-weight: bold;
}
.medal {
font-size: 1.5rem;
margin-top: 3px;
}
.participant-info {
flex: 1;
min-width: 0;
text-align: center;
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.participant-name {
font-size: 1.3rem;
font-weight: bold;
color: #333;
margin-bottom: 5px;
font-size: 1rem;
font-weight: 700;
color: #2c3e50;
margin: 0;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.2;
}
.participant-id {
background: #28a745;
color: white;
padding: 3px 10px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
display: inline-block;
display: none;
}
.score-display {
text-align: right;
min-width: 100px;
text-align: center;
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
gap: 1px;
}
.score-number {
font-size: 2rem;
font-weight: bold;
font-size: 1.8rem;
font-weight: 900;
color: #28a745;
line-height: 1;
}
.tens-count {
font-size: 1rem;
font-size: 1.2rem;
color: #ffc107;
font-weight: bold;
margin-top: 2px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.score-label {
font-size: 0.8rem;
color: #666;
font-size: 0.6rem;
color: #999;
text-transform: uppercase;
font-weight: bold;
font-weight: 600;
letter-spacing: 0.3px;
}
/* Right Column - Full Results Table */
.right-column {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 12px;
box-shadow: none;
overflow: hidden;
display: flex;
flex-direction: column;
border: 1px solid #e9ecef;
}
.table-header {
background: #f8f9fa;
padding: 15px 20px;
padding: 8px 12px;
border-bottom: 1px solid #dee2e6;
flex-shrink: 0;
}
.table-title {
font-size: 1.25rem;
font-size: 0.95rem;
font-weight: 600;
color: #2c3e50;
margin: 0;
@@ -367,17 +416,19 @@
.table-container {
flex: 1;
overflow-y: auto;
overflow: auto;
min-height: 0;
}
.results-table {
width: 100%;
border-collapse: collapse;
font-size: 0.75rem;
}
.results-table th,
.results-table td {
padding: 10px 8px;
padding: 4px 6px;
text-align: center;
border-bottom: 1px solid #f1f3f4;
border-right: 1px solid #f1f3f4;
@@ -388,15 +439,15 @@
font-weight: 600;
color: #495057;
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 0.5px;
font-size: 0.65rem;
letter-spacing: 0.3px;
position: sticky;
top: 0;
z-index: 10;
}
.results-table th.rank-col {
width: 80px;
width: auto;
}
.results-table th.player-col {
@@ -405,12 +456,12 @@
}
.results-table th.score-col {
width: 100px;
width: auto;
background: #e3f2fd;
}
.results-table th.tens-col {
width: 100px;
width: auto;
background: #fff3cd;
}
@@ -452,30 +503,30 @@
.rank-2 { color: #6c757d; }
.rank-3 { color: #8b4513; }
.results-table tbody {
display: table-row-group;
}
.player-cell {
text-align: left !important;
padding-left: 12px !important;
}
.player-name {
font-size: 1rem;
font-size: 0.75rem;
font-weight: 600;
color: #2c3e50;
word-break: keep-all;
overflow-wrap: break-word;
hyphens: none;
}
.score-cell {
background: #e3f2fd !important;
font-size: 1.1rem;
font-size: 0.75rem;
font-weight: 700;
color: #1976d2;
}
.tens-cell {
background: #fff3cd !important;
font-size: 1rem;
font-size: 0.75rem;
font-weight: 700;
color: #856404;
}
@@ -484,10 +535,10 @@
.stats-footer {
background: white;
border-top: 1px solid #dee2e6;
padding: 10px 20px;
padding: 6px 15px;
display: flex;
justify-content: space-around;
font-size: 0.8rem;
font-size: 0.7rem;
color: #6c757d;
flex-shrink: 0;
}
@@ -569,37 +620,47 @@
color: #333 !important;
box-shadow: none !important;
border: 2px solid #ddd !important;
padding: 20px;
margin-bottom: 20px;
border-radius: 8px !important;
padding: 16px 20px !important;
margin-bottom: 16px;
overflow: visible !important;
position: static !important;
}
.results-header::before {
display: none !important;
}
.results-header * {
position: static !important;
z-index: auto !important;
}
.header-logo {
height: 60px !important;
max-width: 160px !important;
display: block !important;
height: 50px !important;
max-width: 140px !important;
background-color: transparent !important;
padding: 0 !important;
border: none !important;
filter: none !important;
margin-bottom: 15px;
}
.results-title {
font-size: 24pt !important;
font-weight: bold !important;
color: #333 !important;
text-shadow: none !important;
backdrop-filter: none !important;
margin-bottom: 10px;
}
.results-subtitle {
font-size: 14pt !important;
color: #666 !important;
.results-title {
font-size: 20pt !important;
font-weight: bold !important;
color: #111 !important;
text-shadow: none !important;
margin-bottom: 15px;
margin-bottom: 4px;
}
.results-subtitle {
font-size: 11pt !important;
color: #555 !important;
text-shadow: none !important;
margin-bottom: 10px;
}
.results-meta {
@@ -607,14 +668,14 @@
}
.meta-number {
color: #333 !important;
font-size: 14pt !important;
color: #111 !important;
text-shadow: none !important;
font-size: 16pt !important;
}
.meta-label {
font-size: 9pt !important;
color: #666 !important;
font-size: 10pt !important;
}
.podium-section {
@@ -656,8 +717,10 @@
<div class="navbar-title">🏆 <span data-i18n="tournament.tournament_results">Tournament Results</span></div>
</div>
<div class="navbar-controls">
<a href="/" class="nav-btn">📺 <span data-i18n="navigation.dashboard">Dashboard</span></a>
<button class="nav-btn" onclick="exportResultsJSON()">💾 <span data-i18n="general.export">Export JSON</span></button>
<button class="nav-btn" onclick="printResults()">🖨️ <span data-i18n="general.print">Print</span></button>
<button class="nav-btn" id="btnTvView" onclick="toggleTvView()" title="Preklopi pogled na TV zaslonu">📺</button>
<a href="/" class="nav-btn"></a>
</div>
</div>
@@ -833,10 +896,6 @@
const topThree = processedParticipants.slice(0, 3);
return topThree.map(participant => {
const suffix = participant.rank === 1 ? 'st' :
participant.rank === 2 ? 'nd' :
participant.rank === 3 ? 'rd' : 'th';
const medal = participant.rank === 1 ? '🥇' :
participant.rank === 2 ? '🥈' :
participant.rank === 3 ? '🥉' : '';
@@ -844,8 +903,6 @@
return `
<div class="podium-card rank-${participant.rank}">
<div class="rank-display">
<div class="rank-number">${participant.rank}</div>
<div class="rank-suffix">${suffix}</div>
<div class="medal">${medal}</div>
</div>
@@ -856,7 +913,7 @@
<div class="score-display">
<div class="score-number">${participant.total_score}</div>
<div class="tens-count">🎯 ${participant.tens_count} × 10</div>
<div class="tens-count">🎯 ${participant.tens_count}</div>
</div>
</div>
`;
@@ -962,6 +1019,33 @@
}
}
// Export results as JSON
function exportResultsJSON() {
const tournamentData = {{ tournament | tojson | safe }};
const resultsData = {{ results | tojson | safe }};
// Wrap in archive format for compatibility
const archiveData = {
tournament: tournamentData,
results: resultsData,
archived_at: new Date().toISOString()
};
const dataStr = JSON.stringify(archiveData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
// Generate filename with tournament info
const tournamentId = tournamentData.tournament_id || 'tournament';
const date = new Date().toISOString().slice(0, 10);
link.download = `${tournamentId}_${date}.json`;
link.click();
URL.revokeObjectURL(url);
}
// Initialize when page loads
document.addEventListener('DOMContentLoaded', initializePage);
@@ -975,6 +1059,55 @@
window.print();
}
});
// ── TV VIEW TOGGLE ────────────────────────────────────────────────────────
const isTvDisplay = new URLSearchParams(window.location.search).get('tv') === '1';
let currentTvView = 'results';
function updateTvViewBtn(view) {
currentTvView = view;
const btn = document.getElementById('btnTvView');
if (!btn) return;
btn.style.background = (view === 'results') ? '#28a745' : '';
btn.style.color = (view === 'results') ? 'white' : '';
btn.style.borderColor= (view === 'results') ? '#1e7e34' : '';
}
async function toggleTvView() {
const newView = currentTvView === 'results' ? 'cameras' : 'results';
try {
await fetch('/api/tv/view', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ view: newView })
});
updateTvViewBtn(newView);
if (isTvDisplay && newView === 'cameras') {
window.location.href = '/';
}
} catch (e) { console.error(e); }
}
// Poll to stay in sync and follow back-redirect when in TV mode
let lastResultsStateHash = null;
setInterval(async () => {
if (document.visibilityState !== 'visible') return;
try {
const r = await fetch('/api/dashboard/state');
if (!r.ok) return;
const d = await r.json();
updateTvViewBtn(d.tv_view || 'cameras');
if (isTvDisplay && d.tv_view !== 'results') {
if (d.tv_view === 'draft' && d.tournament_active) {
window.location.href = '/tournament/draft?tv=1';
} else {
window.location.href = '/';
}
}
} catch (e) {}
}, 3000);
fetch('/api/dashboard/state').then(r => r.json()).then(d => updateTvViewBtn(d.tv_view || 'cameras')).catch(() => {});
</script>
</body>
</html>
+562 -518
View File
File diff suppressed because it is too large Load Diff