Print function correction + player analysis fix and player stats fix

This commit is contained in:
bl3kunja-FW
2025-08-10 18:22:22 +02:00
parent 054c81e78e
commit 33758e7340
7 changed files with 1678 additions and 383 deletions
+501 -50
View File
@@ -638,14 +638,9 @@ def analyze_player_performance(player_id, archives_data):
if score < player_stats['worst_tournament_score']:
player_stats['worst_tournament_score'] = score
# Count shots fired
targets = participant.get('targets', {})
shots_in_tournament = 0
for target in targets.values():
if target.get('shot1') is not None:
shots_in_tournament += 1
if target.get('shot2') is not None:
shots_in_tournament += 1
# Count shots fired - NOW WITH PROPER COUNTING FOR ALL FORMATS
tournament_type = archive.get('tournament_type', '20_targets')
shots_in_tournament = count_shots_in_tournament(participant, tournament_type)
player_stats['total_shots_fired'] += shots_in_tournament
@@ -655,7 +650,7 @@ def analyze_player_performance(player_id, archives_data):
'score': score,
'tournament_type': archive['tournament_type'],
'completed': completed,
'shots_fired': shots_in_tournament
'shots_fired': shots_in_tournament # NOW CORRECTLY CALCULATED
})
except Exception as e:
print(f"Error analyzing tournament archive: {e}")
@@ -923,47 +918,6 @@ def api_get_archive_stats():
return jsonify({'status': 'success', 'stats': stats})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/archive/player/<int:player_id>/performance', methods=['GET'])
def api_get_player_performance(player_id):
"""API endpoint to get player performance data for charts"""
try:
archives_data = {
'tournaments': get_archived_tournaments(),
'leagues': get_archived_leagues()
}
player_stats = analyze_player_performance(player_id, archives_data)
# Prepare chart data
performance_data = {
'trend': {
'labels': [f'T{i+1}' for i in range(len(player_stats['tournament_scores']))],
'data': player_stats['tournament_scores']
},
'distribution': {
'labels': ['0-50', '51-60', '61-70', '71-80', '81-90', '91-100'],
'data': [0, 0, 0, 0, 0, 0]
}
}
# Calculate score distribution
for score in player_stats['tournament_scores']:
if score <= 50:
performance_data['distribution']['data'][0] += 1
elif score <= 60:
performance_data['distribution']['data'][1] += 1
elif score <= 70:
performance_data['distribution']['data'][2] += 1
elif score <= 80:
performance_data['distribution']['data'][3] += 1
elif score <= 90:
performance_data['distribution']['data'][4] += 1
else:
performance_data['distribution']['data'][5] += 1
return jsonify({'status': 'success', 'performance': performance_data})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/archive/players/with-stats', methods=['GET'])
def api_get_players_with_stats():
@@ -2162,7 +2116,504 @@ def get_camera_titles():
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 400
@app.route('/api/archive/player/<int:player_id>/shot-accuracy')
def get_player_shot_accuracy(player_id):
"""
Get aggregated shot accuracy data for a player from tournament archive files
"""
try:
# Get player name from JSON file instead of database
players_data = load_players()
player = None
for p in players_data['players']:
if p['id'] == player_id:
player = p
break
if not player:
return jsonify({
'status': 'error',
'message': f'Player with ID {player_id} not found'
}), 404
player_name = player['name']
# Initialize shot counts by tournament type
shot_accuracy = {
'40 Targets': defaultdict(int),
'20 Targets': defaultdict(int),
'4 Targets': defaultdict(int)
}
# Path to tournament archives
tournament_archives_path = 'tournament_archives'
# Check if directory exists
if not os.path.exists(tournament_archives_path):
return jsonify({
'status': 'error',
'message': f'Tournament archives directory not found: {tournament_archives_path}'
}), 404
# Find all tournament archive files
archive_files = glob.glob(f"{tournament_archives_path}/tournament_*.json")
if not archive_files:
return jsonify({
'status': 'success',
'data': {},
'player_name': player_name,
'message': 'No tournament archive files found'
})
# Process each tournament archive file
for file_path in archive_files:
try:
with open(file_path, 'r') as f:
tournament_data = json.load(f)
# Check if this tournament has our player
participants = tournament_data.get('results', {}).get('participants', {})
# Find player by ID or name
player_data = None
for participant_id, participant_info in participants.items():
if (str(participant_id) == str(player_id) or
participant_info.get('name') == player_name):
player_data = participant_info
break
if not player_data or not player_data.get('completed'):
continue
# Determine tournament type
tournament_type = determine_tournament_type_from_archive(tournament_data)
# Extract individual shots - NOW WITH PROPER SHOT COUNTING FOR ALL FORMATS
shots = extract_shots_from_player_data(player_data, tournament_type)
# Debug print to verify shot counts
print(f"Player {player_name}, Tournament type: {tournament_type}, Total shots extracted: {len(shots)}")
# Count shots by value
for shot_value in shots:
if shot_value == 10:
shot_accuracy[tournament_type]['tens'] += 1
elif shot_value == 9:
shot_accuracy[tournament_type]['nines'] += 1
elif shot_value == 8:
shot_accuracy[tournament_type]['eights'] += 1
elif shot_value == 7:
shot_accuracy[tournament_type]['sevens'] += 1
elif shot_value == 6:
shot_accuracy[tournament_type]['sixes'] += 1
elif shot_value == 5:
shot_accuracy[tournament_type]['fives'] += 1
elif shot_value == 4:
shot_accuracy[tournament_type]['fours'] += 1
elif shot_value == 3:
shot_accuracy[tournament_type]['threes'] += 1
elif shot_value == 2:
shot_accuracy[tournament_type]['twos'] += 1
elif shot_value == 1:
shot_accuracy[tournament_type]['ones'] += 1
elif shot_value == 0:
shot_accuracy[tournament_type]['zeros'] += 1
except Exception as e:
print(f"Error processing tournament file {file_path}: {e}")
continue
# Convert defaultdict to regular dict for JSON serialization
result = {}
for tournament_type, counts in shot_accuracy.items():
if any(counts.values()): # Only include types with data
result[tournament_type] = dict(counts)
return jsonify({
'status': 'success',
'data': result,
'player_name': player_name,
'files_processed': len(archive_files)
})
except Exception as e:
import traceback
return jsonify({
'status': 'error',
'message': str(e),
'traceback': traceback.format_exc()
}), 500
@app.route('/api/archive/tournament/<tournament_id>/shots')
def get_tournament_shots(tournament_id):
"""
Get individual shot data for a specific tournament from archive
"""
try:
# Tournament ID might be a database ID or the tournament filename/timestamp
tournament_archives_path = 'tournament_archives'
# Try to find tournament file by various methods
tournament_file = None
# Method 1: Direct filename match
potential_files = [
f"{tournament_archives_path}/tournament_{tournament_id}.json",
f"{tournament_archives_path}/{tournament_id}.json"
]
for file_path in potential_files:
if os.path.exists(file_path):
tournament_file = file_path
break
# Method 2: Search through all tournament files for matching ID
if not tournament_file:
archive_files = glob.glob(f"{tournament_archives_path}/tournament_*.json")
for file_path in archive_files:
try:
with open(file_path, 'r') as f:
data = json.load(f)
# Check if tournament ID matches
tournament_data = data.get('tournament', {})
results_data = data.get('results', {})
if (tournament_data.get('created_at') == tournament_id or
results_data.get('tournament_id') == tournament_id or
str(tournament_id) in file_path):
tournament_file = file_path
break
except:
continue
if not tournament_file:
return jsonify({
'status': 'error',
'message': 'Tournament archive file not found'
}), 404
# Load tournament data
with open(tournament_file, 'r') as f:
tournament_data = json.load(f)
# Extract all players' shots
participants = tournament_data.get('results', {}).get('participants', {})
all_shots_data = {}
for participant_id, participant_info in participants.items():
if participant_info.get('completed'):
shots = extract_shots_from_player_data(participant_info)
all_shots_data[participant_info.get('name')] = shots
return jsonify({
'status': 'success',
'tournament_data': {
'tournament_type': determine_tournament_type_from_archive(tournament_data),
'participants': len(participants),
'file_path': tournament_file
},
'shots_by_player': all_shots_data
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
def extract_shots_from_player_data(player_data, tournament_type=None):
"""
Extract individual shot values from player data in your archive format
Now properly handles all tournament formats:
- 4 targets: 5 shots each (shot1-shot5) = 20 total shots
- 20 targets: 2 shots each (shot1-shot2) = 40 total shots
- 40 targets: 2 shots each (shot1-shot2) = 80 total shots
"""
shots = []
targets = player_data.get('targets', {})
if not targets:
return shots
# Sort targets by number to maintain order
target_numbers = sorted([int(k) for k in targets.keys() if k.isdigit()])
# Determine shots per target based on tournament format
if tournament_type and '4' in str(tournament_type):
shots_per_target = 5 # 4 targets format: 5 shots each
else:
# Auto-detect if tournament_type not provided
num_targets = len(target_numbers)
if num_targets <= 6: # Likely 4 targets format (4 targets + maybe some extras)
shots_per_target = 5
else: # 20 or 40 targets format
shots_per_target = 2
for target_num in target_numbers:
target_data = targets[str(target_num)]
# Extract shots for this target
for shot_num in range(1, shots_per_target + 1):
shot_key = f'shot{shot_num}'
shot_value = target_data.get(shot_key)
if shot_value is not None:
shots.append(int(shot_value))
return shots
def count_shots_in_tournament(participant_data, tournament_type=None):
"""
Count total shots fired by a participant in a tournament
Now properly handles all tournament formats:
- 4 targets: 5 shots each = 20 total shots
- 20 targets: 2 shots each = 40 total shots
- 40 targets: 2 shots each = 80 total shots
"""
targets = participant_data.get('targets', {})
shots_count = 0
if not targets:
return shots_count
# Determine shots per target based on tournament format
if tournament_type and '4' in str(tournament_type):
max_shots_per_target = 5 # 4 targets format: 5 shots each
else:
# Auto-detect if tournament_type not provided
target_count = len([k for k in targets.keys() if k.isdigit()])
if target_count <= 6: # Likely 4 targets format
max_shots_per_target = 5
else: # 20 or 40 targets format
max_shots_per_target = 2
for target in targets.values():
for shot_num in range(1, max_shots_per_target + 1):
shot_key = f'shot{shot_num}'
if target.get(shot_key) is not None:
shots_count += 1
return shots_count
def determine_tournament_type_from_archive(tournament_data):
"""
Determine tournament type from your archive data structure
"""
# First check the tournament_type field
tournament_info = tournament_data.get('tournament', {})
results_info = tournament_data.get('results', {})
tournament_type = (tournament_info.get('tournament_type') or
results_info.get('tournament_type'))
if tournament_type:
if '40' in tournament_type:
return '40 Targets'
elif '20' in tournament_type:
return '20 Targets'
elif '4' in tournament_type:
return '4 Targets'
# Fallback: count targets from first completed player
participants = results_info.get('participants', {})
for participant_info in participants.values():
if participant_info.get('completed'):
targets = participant_info.get('targets', {})
target_count = len([k for k in targets.keys() if k.isdigit()])
if target_count >= 30: # 40 targets
return '40 Targets'
elif target_count >= 10: # 20 targets
return '20 Targets'
elif target_count <= 6: # 4 targets (changed from <= 8 to <= 6)
return '4 Targets'
break
# Default fallback
return '20 Targets'
@app.route('/api/debug/player/<int:player_id>')
def debug_player_info(player_id):
"""
Debug endpoint to check player info and archive structure
"""
try:
# Check player exists
players_data = load_players()
player = None
for p in players_data['players']:
if p['id'] == player_id:
player = p
break
# Check archive directory
tournament_archives_path = 'tournament_archives'
archive_exists = os.path.exists(tournament_archives_path)
archive_files = []
if archive_exists:
archive_files = glob.glob(f"{tournament_archives_path}/tournament_*.json")
# Check current working directory
cwd = os.getcwd()
# List contents of current directory
current_dir_contents = os.listdir('.')
return jsonify({
'status': 'success',
'player_found': player is not None,
'player_info': player,
'current_working_directory': cwd,
'current_dir_contents': current_dir_contents,
'archive_directory_exists': archive_exists,
'archive_directory_path': tournament_archives_path,
'archive_files_found': len(archive_files),
'archive_files': [os.path.basename(f) for f in archive_files[:5]] # First 5 files
})
except Exception as e:
import traceback
return jsonify({
'status': 'error',
'message': str(e),
'traceback': traceback.format_exc()
}), 500
# Debug endpoint to see raw tournament data
@app.route('/api/archive/tournament-file/<path:filename>')
def get_tournament_file_debug(filename):
"""
Debug endpoint to see raw tournament file data
"""
try:
tournament_archives_path = 'tournament_archives'
file_path = os.path.join(tournament_archives_path, filename)
if not os.path.exists(file_path):
return jsonify({
'status': 'error',
'message': 'File not found'
}), 404
with open(file_path, 'r') as f:
data = json.load(f)
return jsonify({
'status': 'success',
'file_path': file_path,
'data': data
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@app.route('/api/archive/tournament-leaders', methods=['GET'])
def api_get_tournament_leaders():
"""API endpoint to get overall tournament leaders by tournament type"""
try:
tournaments = get_archived_tournaments()
# Group data by tournament type
tournament_types = {
'20_targets': {
'name': '20 Targets',
'description': '20 Targets (2 shots each)',
'best_score': {'player_name': None, 'score': 0},
'most_tens': {'player_name': None, 'tens': 0},
'total_tournaments': 0
},
'40_targets': {
'name': '40 Targets',
'description': '40 Targets (2 shots each)',
'best_score': {'player_name': None, 'score': 0},
'most_tens': {'player_name': None, 'tens': 0},
'total_tournaments': 0
},
'4_targets': {
'name': '4 Targets',
'description': '4 Targets (5 shots each)',
'best_score': {'player_name': None, 'score': 0},
'most_tens': {'player_name': None, 'tens': 0},
'total_tournaments': 0
}
}
for tournament in tournaments:
try:
data = load_archive_file(tournament['filepath'])
if not data:
continue
results = data.get('results', {})
participants = results.get('participants', {})
tournament_type = tournament.get('tournament_type', '20_targets')
if tournament_type not in tournament_types or not participants:
continue
tournament_types[tournament_type]['total_tournaments'] += 1
for player_id, participant in participants.items():
if not participant.get('completed'):
continue
player_name = participant.get('name', f'Player {player_id}')
# Check best score for this tournament type
score = participant.get('total_score', 0)
if score > tournament_types[tournament_type]['best_score']['score']:
tournament_types[tournament_type]['best_score'] = {
'player_name': player_name,
'score': score
}
# Count 10s for this player in this tournament
targets = participant.get('targets', {})
tens_count = 0
for target in targets.values():
if target.get('shot1') == 10:
tens_count += 1
if target.get('shot2') == 10:
tens_count += 1
# For 4_targets format, check additional shots
for shot_num in range(3, 6): # shot3, shot4, shot5
if target.get(f'shot{shot_num}') == 10:
tens_count += 1
if tens_count > tournament_types[tournament_type]['most_tens']['tens']:
tournament_types[tournament_type]['most_tens'] = {
'player_name': player_name,
'tens': tens_count
}
except Exception as e:
print(f"Error processing tournament {tournament.get('filepath', 'unknown')}: {e}")
continue
# Convert to list format, only include types with data
tournament_leaders = []
for type_key, type_data in tournament_types.items():
if type_data['total_tournaments'] > 0 and type_data['best_score']['player_name']:
tournament_leaders.append({
'id': type_key,
'name': type_data['name'],
'description': type_data['description'],
'total_tournaments': type_data['total_tournaments'],
'best_score': type_data['best_score'],
'most_tens': type_data['most_tens']
})
return jsonify({'status': 'success', 'tournament_types': tournament_leaders})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
+84 -7
View File
@@ -880,12 +880,94 @@
.refresh-indicator.show {
opacity: 1;
}
/* PRINT STYLES - Clean professional printout */
@media print {
/* Hide everything except the rankings table */
.navbar,
.left-column,
.tv-refresh-indicator {
display: none !important;
}
/* Reset body and html for print */
html, body {
height: auto !important;
overflow: visible !important;
background: white !important;
margin: 0;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* Make the container take full width and remove grid */
.tv-container {
display: block !important;
height: auto !important;
padding: 0 !important;
gap: 0 !important;
max-width: 100%;
}
/* Style the right column for print */
.right-column {
background: white !important;
border-radius: 0 !important;
box-shadow: none !important;
overflow: visible !important;
display: block !important;
height: auto !important;
margin: 0;
padding: 0;
}
/* Create a beautiful print header */
.table-header {
background: white !important;
padding: 30px 0 30px 0 !important;
border-bottom: 3px solid #007bff !important;
text-align: center;
margin-bottom: 30px !important;
page-break-inside: avoid;
position: relative;
}
/* Add logo using CSS */
.table-header::before {
content: "";
display: block;
width: 200px;
height: 80px;
margin: 0 auto 20px auto;
background: url('/static/logo.png') no-repeat center center;
background-size: contain;
/* Fallback for when logo doesn't load */
background-color: transparent;
}
/* Add title using CSS */
.table-header::after {
content: "🎖️ LEAGUE RESULTS";
display: block;
font-size: 28pt;
font-weight: bold;
color: #333;
margin: 0;
letter-spacing: 2px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* Hide the original table title */
.table-title {
display: none !important;
}
}
</style>
</head>
<body>
<div class="navbar">
<div class="navbar-brand">
<div class="navbar-title">🏆 League Results</div>
<div class="navbar-title">🎖️ League Results</div>
</div>
<div class="navbar-controls">
<a href="/" class="nav-btn">← Dashboard</a>
@@ -927,7 +1009,7 @@
{% if participants and participants|length >= 3 %}
<!-- Champion Podium -->
<div class="champion-section">
<div class="champion-title">🏆 League Champions</div>
<div class="champion-title">🎖️ League Champions</div>
<div class="champion-container">
{% for i in range(3) %}
{% set participant = participants[i] %}
@@ -1091,11 +1173,6 @@
</div>
</div>
<!-- Auto-refresh indicator -->
<div class="refresh-indicator" id="refreshIndicator">
🔄 Updating...
</div>
<script>
const participants = {{ participants|tojson }};
const league = {{ league|tojson }};
+7 -179
View File
@@ -653,19 +653,19 @@
<div class="stat-label">Total Tournaments</div>
</div>
<div class="stat-card target-40">
<span class="stat-icon">🎯</span>
<span class="stat-icon">💪</span>
<div class="stat-value">{{ tournaments|selectattr('tournament_type', 'equalto', '40_targets')|list|length if tournaments else 0 }}</div>
<div class="stat-label">40-Target Tournaments</div>
</div>
<div class="stat-card target-20">
<span class="stat-icon">🏹</span>
<span class="stat-icon"></span>
<div class="stat-value">{{ tournaments|selectattr('tournament_type', 'equalto', '20_targets')|list|length if tournaments else 0 }}</div>
<div class="stat-label">20-Target Tournaments</div>
</div>
<div class="stat-card target-4">
<span class="stat-icon">🎪</span>
<span class="stat-icon">🎯</span>
<div class="stat-value">{{ tournaments|selectattr('tournament_type', 'equalto', '4_targets')|list|length if tournaments else 0 }}</div>
<div class="stat-label">4-Target Tournaments</div>
<div class="stat-label">4-Target <br> Tournaments</div>
</div>
<div class="stat-card">
<span class="stat-icon">👥</span>
@@ -679,11 +679,6 @@
<div class="section">
<div class="section-title">
<span>🎖️ League Championships</span>
<div class="section-controls">
<button class="filter-btn active" onclick="filterLeagues('all')">All</button>
<button class="filter-btn" onclick="filterLeagues('completed')">Completed</button>
<button class="filter-btn" onclick="filterLeagues('recent')">Recent</button>
</div>
</div>
<div class="archive-grid" id="leagues-grid">
{% for league in leagues %}
@@ -716,12 +711,6 @@
</div>
<div class="archive-actions">
<a href="/archive/league/{{ league.filename }}" class="action-btn view-btn">🏆 View</a>
<button class="action-btn edit-btn" onclick="event.stopPropagation(); editArchive('league', '{{ league.filename }}', {{ league|tojson }})">
✏️ Edit
</button>
<button class="action-btn delete-btn" onclick="event.stopPropagation(); deleteArchive('league', '{{ league.filename }}')">
🗑️
</button>
</div>
</div>
</div>
@@ -738,12 +727,7 @@
{% if tournaments_40 %}
<div class="section">
<div class="section-title">
<span>🎯 40-Target Tournaments</span>
<div class="section-controls">
<button class="filter-btn active" onclick="filterTournaments('40', 'all')">All</button>
<button class="filter-btn" onclick="filterTournaments('40', 'finished')">Finished</button>
<button class="filter-btn" onclick="filterTournaments('40', 'recent')">Recent</button>
</div>
<span>💪 40-Target Tournaments</span>
</div>
<div class="archive-grid" id="tournaments-40-grid">
{% for tournament in tournaments_40 %}
@@ -776,12 +760,6 @@
</div>
<div class="archive-actions">
<a href="/archive/tournament/{{ tournament.filename }}" class="action-btn view-btn">📊 View</a>
<button class="action-btn edit-btn" onclick="event.stopPropagation(); editArchive('tournament', '{{ tournament.filename }}', {{ tournament|tojson }})">
✏️ Edit
</button>
<button class="action-btn delete-btn" onclick="event.stopPropagation(); deleteArchive('tournament', '{{ tournament.filename }}')">
🗑️
</button>
</div>
</div>
</div>
@@ -795,12 +773,7 @@
{% if tournaments_20 %}
<div class="section">
<div class="section-title">
<span>🏹 20-Target Tournaments</span>
<div class="section-controls">
<button class="filter-btn active" onclick="filterTournaments('20', 'all')">All</button>
<button class="filter-btn" onclick="filterTournaments('20', 'finished')">Finished</button>
<button class="filter-btn" onclick="filterTournaments('20', 'recent')">Recent</button>
</div>
<span> 20-Target Tournaments</span>
</div>
<div class="archive-grid" id="tournaments-20-grid">
{% for tournament in tournaments_20 %}
@@ -833,12 +806,6 @@
</div>
<div class="archive-actions">
<a href="/archive/tournament/{{ tournament.filename }}" class="action-btn view-btn">📊 View</a>
<button class="action-btn edit-btn" onclick="event.stopPropagation(); editArchive('tournament', '{{ tournament.filename }}', {{ tournament|tojson }})">
✏️ Edit
</button>
<button class="action-btn delete-btn" onclick="event.stopPropagation(); deleteArchive('tournament', '{{ tournament.filename }}')">
🗑️
</button>
</div>
</div>
</div>
@@ -852,12 +819,7 @@
{% if tournaments_4 %}
<div class="section">
<div class="section-title">
<span>🎪 4-Target Tournaments</span>
<div class="section-controls">
<button class="filter-btn active" onclick="filterTournaments('4', 'all')">All</button>
<button class="filter-btn" onclick="filterTournaments('4', 'finished')">Finished</button>
<button class="filter-btn" onclick="filterTournaments('4', 'recent')">Recent</button>
</div>
<span>🎯 4-Target Tournaments</span>
</div>
<div class="archive-grid" id="tournaments-4-grid">
{% for tournament in tournaments_4 %}
@@ -890,12 +852,6 @@
</div>
<div class="archive-actions">
<a href="/archive/tournament/{{ tournament.filename }}" class="action-btn view-btn">📊 View</a>
<button class="action-btn edit-btn" onclick="event.stopPropagation(); editArchive('tournament', '{{ tournament.filename }}', {{ tournament|tojson }})">
✏️ Edit
</button>
<button class="action-btn delete-btn" onclick="event.stopPropagation(); deleteArchive('tournament', '{{ tournament.filename }}')">
🗑️
</button>
</div>
</div>
</div>
@@ -910,11 +866,6 @@
<div class="section">
<div class="section-title">
<span>🏆 Other Tournaments</span>
<div class="section-controls">
<button class="filter-btn active" onclick="filterTournaments('other', 'all')">All</button>
<button class="filter-btn" onclick="filterTournaments('other', 'finished')">Finished</button>
<button class="filter-btn" onclick="filterTournaments('other', 'recent')">Recent</button>
</div>
</div>
<div class="archive-grid" id="tournaments-other-grid">
{% for tournament in tournaments_other %}
@@ -947,12 +898,6 @@
</div>
<div class="archive-actions">
<a href="/archive/tournament/{{ tournament.filename }}" class="action-btn view-btn">📊 View</a>
<button class="action-btn edit-btn" onclick="event.stopPropagation(); editArchive('tournament', '{{ tournament.filename }}', {{ tournament|tojson }})">
✏️ Edit
</button>
<button class="action-btn delete-btn" onclick="event.stopPropagation(); deleteArchive('tournament', '{{ tournament.filename }}')">
🗑️
</button>
</div>
</div>
</div>
@@ -1067,67 +1012,6 @@
);
}
// Edit function
function editArchive(type, filename, data) {
editingItem = { type, filename, data };
document.getElementById('editModalTitle').textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`;
document.getElementById('editName').value = data.name || `${type} - ${data.created_at?.substring(0, 10) || 'Unknown'}`;
// For format type, use a default since this isn't typically stored
document.getElementById('editType').value = type === 'league' ? 'league' : 'single_elimination';
document.getElementById('editNotes').value = data.notes || '';
// Show/hide target count field - both tournaments and leagues have tournament_type
const targetCountGroup = document.getElementById('targetCountGroup');
if (type === 'tournament' || type === 'league') {
targetCountGroup.style.display = 'flex';
document.getElementById('editTargetCount').value = data.tournament_type || '20_targets';
} else {
targetCountGroup.style.display = 'none';
}
document.getElementById('editModal').classList.add('active');
}
// Save edit function
async function saveEdit() {
if (!editingItem) return;
const updatedData = {
name: document.getElementById('editName').value,
notes: document.getElementById('editNotes').value
};
// For both tournaments and leagues, save the tournament_type (target format)
if (editingItem.type === 'tournament' || editingItem.type === 'league') {
updatedData.tournament_type = document.getElementById('editTargetCount').value;
}
try {
const response = await fetch(`/api/archive/update/${editingItem.type}/${editingItem.filename}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedData)
});
const result = await response.json();
if (result.status === 'success') {
alert('Archive updated successfully');
location.reload();
} else {
alert('Error updating archive: ' + result.message);
}
} catch (error) {
alert('Error updating archive: ' + error.message);
}
closeEditModal();
}
// Filtering functions
function filterLeagues(filter) {
const cards = document.querySelectorAll('#leagues-grid .archive-card');
@@ -1151,30 +1035,6 @@
});
}
function filterTournaments(targetCount, filter) {
const gridId = targetCount === 'other' ? 'tournaments-other-grid' : `tournaments-${targetCount}-grid`;
const cards = document.querySelectorAll(`#${gridId} .archive-card`);
const section = document.querySelector(`#${gridId}`).closest('.section');
const buttons = section.querySelectorAll('.filter-btn');
// Update active button
buttons.forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
// Filter cards
cards.forEach(card => {
const isFinished = card.dataset.status === 'finished';
const date = new Date(card.dataset.date);
const isRecent = (Date.now() - date.getTime()) < (30 * 24 * 60 * 60 * 1000); // Last 30 days
let show = true;
if (filter === 'finished' && !isFinished) show = false;
if (filter === 'recent' && !isRecent) show = false;
card.classList.toggle('hidden', !show);
});
}
// Modal functions
function showModal(title, message, confirmCallback) {
document.getElementById('modalTitle').textContent = title;
@@ -1211,38 +1071,6 @@
closeEditModal();
}
});
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
console.log('📚 Enhanced Archive loaded');
console.log('🏆 Total Tournaments:', {{ tournaments|length if tournaments else 0 }});
console.log('🎖️ Leagues:', {{ leagues|length if leagues else 0 }});
{% if tournaments %}
const tournaments = {{ tournaments|tojson }};
console.log('Tournament data sample:', tournaments[0] || 'No tournaments');
// Count tournaments by type
const tournaments40 = tournaments.filter(t => t.tournament_type === '40_targets');
const tournaments20 = tournaments.filter(t => t.tournament_type === '20_targets');
const tournaments4 = tournaments.filter(t => t.tournament_type === '4_targets');
const tournamentsOther = tournaments.filter(t => !['4_targets', '20_targets', '40_targets'].includes(t.tournament_type));
console.log('🎯 40-Target Tournaments:', tournaments40.length);
console.log('🏹 20-Target Tournaments:', tournaments20.length);
console.log('🎪 4-Target Tournaments:', tournaments4.length);
console.log('🏆 Other Tournaments:', tournamentsOther.length);
// Log tournament types for debugging
const tournamentTypes = [...new Set(tournaments.map(t => t.tournament_type))];
console.log('Available tournament types:', tournamentTypes);
{% else %}
console.log('🎯 40-Target Tournaments: 0');
console.log('🏹 20-Target Tournaments: 0');
console.log('🎪 4-Target Tournaments: 0');
console.log('🏆 Other Tournaments: 0');
{% endif %}
});
</script>
</body>
</html>
+402 -49
View File
@@ -65,28 +65,6 @@
color: #007bff;
}
.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: #007bff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
color: #007bff;
}
.nav-btn.secondary {
background: #6c757d;
border-color: #495057;
@@ -146,10 +124,142 @@
gap: 10px;
}
/* Tab Navigation */
.tab-navigation {
display: flex;
gap: 5px;
margin-bottom: 25px;
border-bottom: 2px solid #f1f3f4;
}
.tab-btn {
background: none;
border: none;
padding: 12px 24px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
color: #666;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
}
.tab-btn.active {
color: #007bff;
border-bottom-color: #007bff;
}
.tab-btn:hover {
color: #007bff;
background: rgba(0, 123, 255, 0.05);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Tournament Leaders Section */
.tournament-leaders {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.tournament-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border: 1px solid #e9ecef;
position: relative;
overflow: hidden;
}
.tournament-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #ffc107, #fd7e14);
}
.tournament-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.tournament-name {
font-size: 1.2rem;
font-weight: bold;
color: #333;
}
.tournament-date {
background: #f8f9fa;
color: #666;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.leaders-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.leader-category {
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
text-align: center;
}
.category-title {
font-size: 0.85rem;
font-weight: 600;
color: #666;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.leader-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.leader-name {
font-size: 1.1rem;
font-weight: bold;
color: #333;
}
.leader-score {
font-size: 1.3rem;
font-weight: bold;
color: #007bff;
}
.leader-score.tens {
color: #28a745;
}
/* Compact Stats Overview */
.stats-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
@@ -453,7 +563,25 @@
}
.stats-overview {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
}
.tournament-leaders {
grid-template-columns: 1fr;
}
.leaders-grid {
grid-template-columns: 1fr;
}
.tab-navigation {
flex-wrap: wrap;
gap: 2px;
}
.tab-btn {
padding: 8px 16px;
font-size: 0.9rem;
}
}
</style>
@@ -461,7 +589,7 @@
<body>
<!-- Navigation Bar -->
<div class="navbar">
<div class="navbar-title">👤 Player Analysis</div>
<div class="navbar-title">🎯 Player Analysis</div>
<div class="navbar-controls">
<a href="/" class="nav-btn primary">← Dashboard</a>
<a href="/archive" class="nav-btn secondary">📚 Archive</a>
@@ -473,22 +601,52 @@
<div class="stats-overview">
<div class="stat-card">
<span class="stat-icon">👥</span>
<div class="stat-value" id="totalPlayers">{{ players|length }}</div>
<div class="stat-value" id="totalPlayers">Loading...</div>
<div class="stat-label">Total Players</div>
</div>
<div class="stat-card">
<span class="stat-icon">📊</span>
<div class="stat-value" id="avgTournaments">{{ overview_stats.avg_tournaments if overview_stats else 0 }}</div>
<div class="stat-label">Avg Tournaments</div>
<span class="stat-icon">🎯</span>
<div class="stat-value" id="totalTournaments">Loading...</div>
<div class="stat-label">Total Tournaments</div>
</div>
<div class="stat-card">
<span class="stat-icon">🏆</span>
<div class="stat-value" id="topScore">{{ overview_stats.top_score if overview_stats else 0 }}</div>
<div class="stat-label">Highest Score</div>
<div class="stat-value" id="score20Targets">Loading...</div>
<div class="stat-label">Best 20 Targets</div>
</div>
<div class="stat-card">
<span class="stat-icon">🎖️</span>
<div class="stat-value" id="score40Targets">Loading...</div>
<div class="stat-label">Best 40 Targets</div>
</div>
<div class="stat-card">
<span class="stat-icon">🥇</span>
<div class="stat-value" id="score4Targets">Loading...</div>
<div class="stat-label">Best 4 Targets</div>
</div>
</div>
<!-- Tab Navigation -->
<div class="section">
<div class="tab-navigation">
<button class="tab-btn active" onclick="switchTab('tournament-leaders')">🏆 Overall Champions</button>
<button class="tab-btn" onclick="switchTab('players')">👥 All Players</button>
</div>
<!-- Tournament Leaders Tab -->
<div id="tournament-leaders" class="tab-content active">
<div class="section-title">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">Select a Player to Analyze</div>
<!-- Controls -->
@@ -514,11 +672,13 @@
</div>
</div>
</div>
</div>
<script>
// Players data from Flask
let playersData = {{ players|tojson }};
let filteredPlayers = [...playersData];
// Global data
let playersData = [];
let tournamentData = [];
let filteredPlayers = [];
let currentFilter = 'all';
let currentSort = 'name';
let playersWithStats = [];
@@ -526,9 +686,180 @@
// 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>Unable to Load Tournament Data</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>No Tournament Data Available</h3>
<p>Tournament results will appear here once available</p>
</div>
`;
return;
}
leadersContainer.innerHTML = tournamentData.map(tournamentType => `
<div class="tournament-card">
<div class="tournament-header">
<div class="tournament-name">${tournamentType.name}</div>
<div class="tournament-date">${tournamentType.total_tournaments} tournament${tournamentType.total_tournaments !== 1 ? 's' : ''}</div>
</div>
<div style="text-align: center; margin-bottom: 15px; color: #666; font-size: 0.9rem; font-weight: 500;">
${tournamentType.description}
</div>
<div class="leaders-grid">
<div class="leader-category">
<div class="category-title">🏆 Best Score</div>
<div class="leader-info">
<div class="leader-name">${tournamentType.best_score.player_name || 'No data'}</div>
<div class="leader-score">${tournamentType.best_score.score || 0}</div>
</div>
</div>
<div class="leader-category">
<div class="category-title">🎪 Most 10s</div>
<div class="leader-info">
<div class="leader-name">${tournamentType.most_tens.player_name || 'No data'}</div>
<div class="leader-score tens">${tournamentType.most_tens.tens || 0} 10s</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
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': '20 Targets (2 shots each)',
'40_targets': '40 Targets (2 shots each)',
'4_targets': '4 Targets (5 shots each)'
};
return typeMap[tournamentType] || tournamentType;
}
// Format date for display
function formatDate(dateString) {
if (!dateString) return 'Unknown Date';
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
// Try to parse filename format YYYYMMDD_HHMMSS
if (dateString.length >= 8) {
const year = dateString.substring(0, 4);
const month = dateString.substring(4, 6);
const day = dateString.substring(6, 8);
const parsedDate = new Date(`${year}-${month}-${day}`);
if (!isNaN(parsedDate.getTime())) {
return parsedDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
}
return 'Unknown Date';
}
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
} catch (error) {
return 'Unknown Date';
}
}
// Load player statistics
async function loadPlayerStats() {
try {
@@ -539,9 +870,13 @@
playersWithStats = result.players;
playersData = playersWithStats;
filteredPlayers = [...playersWithStats];
// Update total players count
document.getElementById('totalPlayers').textContent = playersData.length;
filterAndSortPlayers();
} else {
console.error('Failed to load player stats');
console.error('Failed to load player stats:', result.message);
renderPlayersBasic();
}
} catch (error) {
@@ -554,22 +889,12 @@
function setupEventListeners() {
const searchBox = document.getElementById('searchBox');
const sortSelect = document.getElementById('sortSelect');
const filterButtons = document.querySelectorAll('.filter-btn');
searchBox.addEventListener('input', filterAndSortPlayers);
sortSelect.addEventListener('change', (e) => {
currentSort = e.target.value;
filterAndSortPlayers();
});
filterButtons.forEach(btn => {
btn.addEventListener('click', () => {
filterButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilter = btn.dataset.filter;
filterAndSortPlayers();
});
});
}
// Filter and sort players
@@ -675,15 +1000,31 @@
// 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');
playersGrid.innerHTML = playersData.map(player => `
<div class="player-card ${player.current_player ? 'current-player' : 'archived-player'}"
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 class="player-badge ${player.current_player ? 'current' : 'archived'}">
${player.current_player ? 'Current' : 'Archived'}
<div class="player-badge ${player.enabled ? 'current' : 'archived'}">
${player.enabled ? 'Current' : 'Archived'}
</div>
</div>
@@ -711,6 +1052,18 @@
</div>
</div>
`).join('');
})
.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>
`;
});
}
// Create mini performance chart for player
+540 -38
View File
@@ -48,14 +48,14 @@
background: #f8f9fa;
border: 2px solid #e9ecef;
cursor: pointer;
padding: 8px 16px;
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.85rem;
font-size: 0.9rem;
}
.nav-btn:hover {
@@ -87,7 +87,7 @@
overflow: hidden;
}
/* Top Section - Stats and Chart */
/* Top Section - Stats and Charts */
.top-section {
display: grid;
grid-template-columns: 320px 1fr;
@@ -146,8 +146,8 @@
font-weight: 500;
}
/* Chart Panel */
.chart-panel {
/* Charts Panel */
.charts-panel {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
@@ -156,10 +156,151 @@
flex-direction: column;
}
/* Tournament Type Buttons */
.tournament-type-buttons {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.type-btn {
background: #f8f9fa;
border: 2px solid #e9ecef;
padding: 10px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
gap: 6px;
}
.type-btn:hover {
border-color: #007bff;
transform: translateY(-1px);
}
.type-btn.active {
color: white;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.type-btn.active.targets-40 {
background: #dc3545;
border-color: #dc3545;
}
.type-btn.active.targets-20 {
background: #28a745;
border-color: #28a745;
}
.type-btn.active.targets-4 {
background: #007bff;
border-color: #007bff;
}
/* Chart Info Header */
.chart-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.chart-stats {
display: flex;
gap: 20px;
font-size: 0.85rem;
}
.chart-stat {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.chart-stat-value {
font-size: 1.1rem;
font-weight: bold;
color: #333;
}
.chart-stat-label {
color: #666;
font-size: 0.75rem;
text-transform: uppercase;
}
/* Shot Accuracy Stats */
.accuracy-stats {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 0.8rem;
justify-content: center;
}
.accuracy-stat {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px 8px;
background: white;
border-radius: 6px;
border: 1px solid #e9ecef;
min-width: 35px;
transition: transform 0.2s ease;
}
.accuracy-stat:hover {
transform: scale(1.05);
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.accuracy-value {
font-weight: bold;
color: #333;
font-size: 0.9rem;
}
.accuracy-label {
color: #666;
font-size: 0.65rem;
font-weight: 500;
}
.chart-container {
position: relative;
flex: 1;
min-height: 180px;
min-height: 200px;
background: white;
border-radius: 6px;
border: 1px solid #e9ecef;
padding: 10px;
}
.no-data {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: #999;
font-size: 0.9rem;
font-style: italic;
background: #f8f9fa;
border-radius: 6px;
border: 1px dashed #dee2e6;
}
/* Bottom Section - History */
@@ -412,7 +553,7 @@
</div>
<div class="container">
<!-- Top Section - Stats and Chart -->
<!-- Top Section - Stats and Charts -->
<div class="top-section">
<!-- Stats Panel -->
<div class="stats-panel">
@@ -445,11 +586,92 @@
</div>
</div>
<!-- Performance Chart -->
<div class="chart-panel">
<div class="panel-title">📈 Performance Trend</div>
<!-- Performance Charts -->
<div class="charts-panel">
<div class="panel-title">📈 Performance by Tournament Type</div>
<!-- Tournament Type Buttons -->
<div class="tournament-type-buttons">
<button class="type-btn active targets-40" data-type="40 Targets">
<span>💪</span> 40 Targets
</button>
<button class="type-btn targets-20" data-type="20 Targets">
<span></span> 20 Targets
</button>
<button class="type-btn targets-4" data-type="4 Targets">
<span>🎯</span> 4 Targets
</button>
</div>
<!-- Chart Info and Stats -->
<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">Games</div>
</div>
<div class="chart-stat">
<div class="chart-stat-value" id="avgScore">0</div>
<div class="chart-stat-label">Average</div>
</div>
<div class="chart-stat">
<div class="chart-stat-value" id="bestScore">0</div>
<div class="chart-stat-label">Best</div>
</div>
</div>
<!-- Shot Accuracy Stats -->
<div class="accuracy-stats" id="accuracyStats">
<div class="accuracy-stat">
<div class="accuracy-value" id="tens">0</div>
<div class="accuracy-label">10s</div>
</div>
<div class="accuracy-stat">
<div class="accuracy-value" id="nines">0</div>
<div class="accuracy-label">9s</div>
</div>
<div class="accuracy-stat">
<div class="accuracy-value" id="eights">0</div>
<div class="accuracy-label">8s</div>
</div>
<div class="accuracy-stat">
<div class="accuracy-value" id="sevens">0</div>
<div class="accuracy-label">7s</div>
</div>
<div class="accuracy-stat">
<div class="accuracy-value" id="sixes">0</div>
<div class="accuracy-label">6s</div>
</div>
<div class="accuracy-stat">
<div class="accuracy-value" id="fives">0</div>
<div class="accuracy-label">5s</div>
</div>
<div class="accuracy-stat">
<div class="accuracy-value" id="fours">0</div>
<div class="accuracy-label">4s</div>
</div>
<div class="accuracy-stat">
<div class="accuracy-value" id="threes">0</div>
<div class="accuracy-label">3s</div>
</div>
<div class="accuracy-stat">
<div class="accuracy-value" id="twos">0</div>
<div class="accuracy-label">2s</div>
</div>
<div class="accuracy-stat">
<div class="accuracy-value" id="ones">0</div>
<div class="accuracy-label">1s</div>
</div>
<div class="accuracy-stat">
<div class="accuracy-value" id="zeros">0</div>
<div class="accuracy-label">0s</div>
</div>
</div>
</div>
<!-- Single Chart Container -->
<div class="chart-container">
<canvas id="performanceChart"></canvas>
<canvas id="tournamentChart"></canvas>
</div>
</div>
</div>
@@ -532,42 +754,283 @@
// Player data from Flask
const playerStats = {{ stats|tojson }};
// Tournament type configuration
const tournamentTypes = {
'40 Targets': { color: '#dc3545', icon: '💪', class: 'targets-40' },
'20 Targets': { color: '#28a745', icon: '⚡', class: 'targets-20' },
'4 Targets': { color: '#007bff', icon: '🎯', class: 'targets-4' }
};
let currentChart = null;
let currentTournamentType = '40 Targets'; // Will be updated based on available data
let tournamentsByType = {};
let shotAccuracyData = {};
// Initialize page
function initializePage() {
createPerformanceChart();
groupTournamentsByType();
setupEventListeners();
loadShotAccuracyData();
updateChart();
}
// Create performance trend chart
function createPerformanceChart() {
const ctx = document.getElementById('performanceChart').getContext('2d');
const scores = playerStats.tournament_scores || [];
// Load shot accuracy data from archive
async function loadShotAccuracyData() {
try {
const playerId = '{{ player.id }}'; // Get player ID from template
const response = await fetch(`/api/archive/player/${playerId}/shot-accuracy`);
const result = await response.json();
if (scores.length === 0) {
ctx.font = '14px Arial';
ctx.fillStyle = '#666';
ctx.textAlign = 'center';
ctx.fillText('No tournament data available', ctx.canvas.width / 2, ctx.canvas.height / 2);
if (result.status === 'success') {
shotAccuracyData = result.data;
console.log('Shot accuracy data loaded:', shotAccuracyData);
// Auto-select the first tournament type with data
const availableTypes = Object.keys(shotAccuracyData);
if (availableTypes.length > 0) {
// Update current type to first available type
const firstAvailableType = availableTypes[0];
currentTournamentType = firstAvailableType;
// Update button states
updateActiveButton();
}
updateChart(); // Refresh chart with accuracy data
} else {
console.log('No shot accuracy data available:', result.message);
// Still try to show basic tournament data without shot accuracy
updateChart();
}
} catch (error) {
console.error('Error loading shot accuracy data:', error);
console.log('API endpoints may not be set up yet. Showing basic tournament data only.');
updateChart();
}
}
// Update active button based on current tournament type
function updateActiveButton() {
const typeButtons = document.querySelectorAll('.type-btn');
typeButtons.forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.type === currentTournamentType) {
btn.classList.add('active');
}
});
}
// Setup event listeners
function setupEventListeners() {
const typeButtons = document.querySelectorAll('.type-btn');
typeButtons.forEach(btn => {
btn.addEventListener('click', () => {
// Update active button
typeButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Update current type and chart
currentTournamentType = btn.dataset.type;
updateChart();
});
});
}
// Group tournaments by type based on tournament_type field or shots fired
function groupTournamentsByType() {
const tournamentHistory = playerStats.tournament_history || [];
tournamentsByType = {};
tournamentHistory.forEach(tournament => {
let type;
// First try to use the tournament_type field if it exists
if (tournament.tournament_type) {
const typeField = tournament.tournament_type.toLowerCase();
if (typeField.includes('40') || typeField.includes('forty')) {
type = '40 Targets';
} else if (typeField.includes('20') || typeField.includes('twenty')) {
type = '20 Targets';
} else if (typeField.includes('4') || typeField.includes('four')) {
type = '4 Targets';
}
}
// If we couldn't determine from tournament_type, fall back to shots_fired
if (!type) {
const shots = tournament.shots_fired;
if (shots >= 30) {
type = '40 Targets';
} else if (shots >= 10 && shots <= 29) {
type = '20 Targets';
} else if (shots <= 9) {
type = '4 Targets';
}
}
// Skip tournaments that don't match our types
if (!type) {
console.log('Could not categorize tournament:', tournament);
return;
}
new Chart(ctx, {
if (!tournamentsByType[type]) {
tournamentsByType[type] = [];
}
tournamentsByType[type].push(tournament);
});
// Sort each type by date
Object.keys(tournamentsByType).forEach(type => {
tournamentsByType[type].sort((a, b) => new Date(a.date) - new Date(b.date));
});
console.log('Tournaments grouped by type:', tournamentsByType);
console.log('Available tournament types from database:', Object.keys(tournamentsByType));
// If no 40 Targets tournaments, try to default to an available type
if (!tournamentsByType['40 Targets'] && Object.keys(tournamentsByType).length > 0) {
const availableTypes = Object.keys(tournamentsByType);
currentTournamentType = availableTypes[0];
console.log(`No 40 Targets tournaments found. Defaulting to: ${currentTournamentType}`);
updateActiveButton();
}
}
// Update chart and statistics for current tournament type
function updateChart() {
const tournaments = tournamentsByType[currentTournamentType] || [];
updateChartInfo(tournaments);
createChart(tournaments);
}
// Update chart information and statistics
function updateChartInfo(tournaments) {
const gameCount = tournaments.length;
const scores = tournaments.map(t => t.score);
const avgScore = gameCount > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / gameCount) : 0;
const bestScore = gameCount > 0 ? Math.max(...scores) : 0;
// Update basic stats
document.getElementById('gameCount').textContent = gameCount;
document.getElementById('avgScore').textContent = avgScore;
document.getElementById('bestScore').textContent = bestScore;
// Update shot accuracy stats (if available in tournament data)
updateAccuracyStats(tournaments);
}
// Update shot accuracy statistics
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;
let hasData = false;
tournaments.forEach(tournament => {
// Check if we have shot breakdown data for this tournament
if (tournament.shot_breakdown) {
hasData = true;
const breakdown = tournament.shot_breakdown;
tens += breakdown.tens || 0;
nines += breakdown.nines || 0;
eights += breakdown.eights || 0;
sevens += breakdown.sevens || 0;
sixes += breakdown.sixes || 0;
fives += breakdown.fives || 0;
fours += breakdown.fours || 0;
threes += breakdown.threes || 0;
twos += breakdown.twos || 0;
ones += breakdown.ones || 0;
zeros += breakdown.zeros || 0;
}
});
// Also check if we have aggregated shot accuracy data for this tournament type
if (shotAccuracyData && shotAccuracyData[currentTournamentType]) {
hasData = true;
const typeData = shotAccuracyData[currentTournamentType];
tens = typeData.tens || 0;
nines = typeData.nines || 0;
eights = typeData.eights || 0;
sevens = typeData.sevens || 0;
sixes = typeData.sixes || 0;
fives = typeData.fives || 0;
fours = typeData.fours || 0;
threes = typeData.threes || 0;
twos = typeData.twos || 0;
ones = typeData.ones || 0;
zeros = typeData.zeros || 0;
}
const accuracyStatsDiv = document.getElementById('accuracyStats');
if (!hasData) {
// Hide the accuracy stats section if no data is available
accuracyStatsDiv.style.display = 'none';
console.log(`No shot accuracy data available for ${currentTournamentType}`);
return;
}
// Show the section if data is available
accuracyStatsDiv.style.display = 'flex';
document.getElementById('tens').textContent = tens;
document.getElementById('nines').textContent = nines;
document.getElementById('eights').textContent = eights;
document.getElementById('sevens').textContent = sevens;
document.getElementById('sixes').textContent = sixes;
document.getElementById('fives').textContent = fives;
document.getElementById('fours').textContent = fours;
document.getElementById('threes').textContent = threes;
document.getElementById('twos').textContent = twos;
document.getElementById('ones').textContent = ones;
document.getElementById('zeros').textContent = zeros;
console.log(`Shot accuracy for ${currentTournamentType}:`, {
tens, nines, eights, sevens, sixes, fives, fours, threes, twos, ones, zeros
});
}
// Create chart for current tournament type
function createChart(tournaments) {
const canvas = document.getElementById('tournamentChart');
const ctx = canvas.getContext('2d');
// Destroy existing chart if it exists
if (currentChart) {
currentChart.destroy();
}
if (tournaments.length === 0) {
// Show no data message
ctx.font = '16px Arial';
ctx.fillStyle = '#666';
ctx.textAlign = 'center';
ctx.fillText(`No ${currentTournamentType} tournaments found`, canvas.width / 2, canvas.height / 2);
return;
}
const typeConfig = tournamentTypes[currentTournamentType];
const scores = tournaments.map(t => t.score);
currentChart = new Chart(ctx, {
type: 'line',
data: {
labels: scores.map((_, i) => `T${i + 1}`),
labels: scores.map((_, i) => `${i + 1}`),
datasets: [{
label: 'Tournament Score',
label: `${currentTournamentType} Score`,
data: scores,
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.15)',
borderColor: typeConfig.color,
backgroundColor: typeConfig.color + '20',
borderWidth: 3,
fill: true,
tension: 0.4,
pointRadius: 5,
pointHoverRadius: 8,
pointBackgroundColor: '#28a745',
pointBackgroundColor: typeConfig.color,
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointHoverBackgroundColor: '#1e7e34',
pointHoverBackgroundColor: typeConfig.color,
pointHoverBorderColor: '#fff'
}]
},
@@ -577,17 +1040,52 @@
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(40, 167, 69, 0.9)',
backgroundColor: typeConfig.color + 'E6',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: '#28a745',
borderColor: typeConfig.color,
borderWidth: 2,
callbacks: {
title: function(context) {
return `Tournament ${context[0].dataIndex + 1}`;
const tournament = tournaments[context[0].dataIndex];
return `Game ${context[0].dataIndex + 1} - ${tournament.date.split(' ')[0]}`;
},
label: function(context) {
return `Score: ${context.parsed.y}`;
const tournament = tournaments[context.dataIndex];
const labels = [`Score: ${context.parsed.y}`, `Shots: ${tournament.shots_fired}`];
// Add tournament type if available
if (tournament.tournament_type) {
labels.push(`Type: ${tournament.tournament_type.replace('_', ' ')}`);
}
// Add shot breakdown if available
if (tournament.shot_breakdown) {
const breakdown = tournament.shot_breakdown;
const shots = [];
if (breakdown.tens) shots.push(`10s: ${breakdown.tens}`);
if (breakdown.nines) shots.push(`9s: ${breakdown.nines}`);
if (breakdown.eights) shots.push(`8s: ${breakdown.eights}`);
if (breakdown.sevens) shots.push(`7s: ${breakdown.sevens}`);
if (breakdown.sixes) shots.push(`6s: ${breakdown.sixes}`);
if (breakdown.fives) shots.push(`5s: ${breakdown.fives}`);
if (breakdown.fours) shots.push(`4s: ${breakdown.fours}`);
if (breakdown.threes) shots.push(`3s: ${breakdown.threes}`);
if (breakdown.twos) shots.push(`2s: ${breakdown.twos}`);
if (breakdown.ones) shots.push(`1s: ${breakdown.ones}`);
if (breakdown.zeros) shots.push(`0s: ${breakdown.zeros}`);
if (shots.length > 0) {
// Show only top scoring shots to keep tooltip readable
const topShots = shots.slice(0, 5);
labels.push(topShots.join(', '));
if (shots.length > 5) {
labels.push('+ more...');
}
}
}
return labels;
}
}
}
@@ -595,17 +1093,21 @@
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(40, 167, 69, 0.1)' },
grid: {
color: typeConfig.color + '20'
},
ticks: {
color: '#28a745',
font: { size: 11, weight: 'bold' }
color: typeConfig.color,
font: { size: 12, weight: 'bold' }
}
},
x: {
grid: { color: 'rgba(40, 167, 69, 0.1)' },
grid: {
color: typeConfig.color + '20'
},
ticks: {
color: '#28a745',
font: { size: 11, weight: 'bold' }
color: typeConfig.color,
font: { size: 12, weight: 'bold' }
}
}
},
+88 -4
View File
@@ -12,7 +12,7 @@
html, body {
margin: 0;
padding: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
height: 100vh;
overflow: hidden; /* Prevent scrolling for TV display */
@@ -652,6 +652,87 @@
.tv-refresh-indicator.show {
opacity: 1;
}
/* PRINT STYLES - Clean professional printout */
@media print {
/* Hide everything except the rankings table */
.navbar,
.left-column,
.tv-refresh-indicator {
display: none !important;
}
/* Reset body and html for print */
html, body {
height: auto !important;
overflow: visible !important;
background: white !important;
margin: 0;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* Make the container take full width and remove grid */
.tv-container {
display: block !important;
height: auto !important;
padding: 0 !important;
gap: 0 !important;
max-width: 100%;
}
/* Style the right column for print */
.right-column {
background: white !important;
border-radius: 0 !important;
box-shadow: none !important;
overflow: visible !important;
display: block !important;
height: auto !important;
margin: 0;
padding: 0;
}
/* Create a beautiful print header */
.table-header {
background: white !important;
padding: 30px 0 30px 0 !important;
border-bottom: 3px solid #007bff !important;
text-align: center;
margin-bottom: 30px !important;
page-break-inside: avoid;
position: relative;
}
/* Add logo using CSS */
.table-header::before {
content: "";
display: block;
width: 200px;
height: 80px;
margin: 0 auto 20px auto;
background: url('/static/logo.png') no-repeat center center;
background-size: contain;
/* Fallback for when logo doesn't load */
background-color: transparent;
}
/* Add title using CSS */
.table-header::after {
content: "🏆 TOURNAMENT RESULTS";
display: block;
font-size: 28pt;
font-weight: bold;
color: #333;
margin: 0;
letter-spacing: 2px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* Hide the original table title */
.table-title {
display: none !important;
}
}
</style>
</head>
<body>
@@ -661,9 +742,7 @@
</div>
<div class="navbar-controls">
<a href="/" class="nav-btn">← Dashboard</a>
<a href="/tournament/draft" class="nav-btn">📋 Draft</a>
<a href="/results/calculator" class="nav-btn primary">🎯 Calculator</a>
<button class="nav-btn" onclick="window.print()">🖨️ Print</button>
<button class="nav-btn" onclick="printResults()">🖨️ Print</button>
</div>
</div>
@@ -867,6 +946,11 @@
document.body.offsetHeight; // Trigger reflow
document.body.style.display = '';
});
function printResults() {
// Trigger print dialog
window.print();
}
</script>
</body>
</html>
+6 -6
View File
@@ -1082,21 +1082,21 @@
<!-- Tournament Type Selection -->
<div class="tournament-type-selection">
<div class="type-title">🎯 Select Tournament Type</div>
<div class="type-title">Select Tournament Type</div>
<div class="type-options">
<div class="type-option" onclick="selectTournamentType('4_targets')">
<input type="radio" name="tournament_type" value="4_targets">
<div class="type-name">4 Targets</div>
<div class="type-description">Quick format with 4 targets, 5 shots each (20 shots total)</div>
<div class="type-name">🎯 4 Targets</div>
<div class="type-description">Quick format with 4 targets, 5 shots each <br> (20 shots total)</div>
</div>
<div class="type-option selected" onclick="selectTournamentType('20_targets')">
<input type="radio" name="tournament_type" value="20_targets" checked>
<div class="type-name">20 Targets</div>
<div class="type-name">20 Targets</div>
<div class="type-description">Standard format with 20 targets, 2 shots each (40 shots total)</div>
</div>
<div class="type-option" onclick="selectTournamentType('40_targets')">
<input type="radio" name="tournament_type" value="40_targets">
<div class="type-name">40 Targets</div>
<div class="type-name">💪 40 Targets</div>
<div class="type-description">Extended format with 40 targets, 2 shots each (80 shots total)</div>
</div>
</div>
@@ -1108,7 +1108,7 @@
<div class="action-buttons">
<button class="action-btn success" id="startLeagueBtn" onclick="startLeague()">🏆 Start League (6 Tournaments)</button>
<button class="action-btn" id="startSingleBtn" onclick="startSingleTournament()">🎯 Start Single Tournament</button>
<button class="action-btn" id="startSingleBtn" onclick="startSingleTournament()">🏅 Start Single Tournament</button>
</div>
<div class="warning" id="warningMessage" style="display: none;">