Print function correction + player analysis fix and player stats fix
This commit is contained in:
@@ -638,14 +638,9 @@ def analyze_player_performance(player_id, archives_data):
|
|||||||
if score < player_stats['worst_tournament_score']:
|
if score < player_stats['worst_tournament_score']:
|
||||||
player_stats['worst_tournament_score'] = score
|
player_stats['worst_tournament_score'] = score
|
||||||
|
|
||||||
# Count shots fired
|
# Count shots fired - NOW WITH PROPER COUNTING FOR ALL FORMATS
|
||||||
targets = participant.get('targets', {})
|
tournament_type = archive.get('tournament_type', '20_targets')
|
||||||
shots_in_tournament = 0
|
shots_in_tournament = count_shots_in_tournament(participant, tournament_type)
|
||||||
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
|
|
||||||
|
|
||||||
player_stats['total_shots_fired'] += shots_in_tournament
|
player_stats['total_shots_fired'] += shots_in_tournament
|
||||||
|
|
||||||
@@ -655,7 +650,7 @@ def analyze_player_performance(player_id, archives_data):
|
|||||||
'score': score,
|
'score': score,
|
||||||
'tournament_type': archive['tournament_type'],
|
'tournament_type': archive['tournament_type'],
|
||||||
'completed': completed,
|
'completed': completed,
|
||||||
'shots_fired': shots_in_tournament
|
'shots_fired': shots_in_tournament # NOW CORRECTLY CALCULATED
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error analyzing tournament archive: {e}")
|
print(f"Error analyzing tournament archive: {e}")
|
||||||
@@ -923,47 +918,6 @@ def api_get_archive_stats():
|
|||||||
return jsonify({'status': 'success', 'stats': stats})
|
return jsonify({'status': 'success', 'stats': stats})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
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'])
|
@app.route('/api/archive/players/with-stats', methods=['GET'])
|
||||||
def api_get_players_with_stats():
|
def api_get_players_with_stats():
|
||||||
@@ -2162,7 +2116,504 @@ def get_camera_titles():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
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__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
@@ -880,12 +880,94 @@
|
|||||||
.refresh-indicator.show {
|
.refresh-indicator.show {
|
||||||
opacity: 1;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<div class="navbar-title">🏆 League Results</div>
|
<div class="navbar-title">🎖️ League Results</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-controls">
|
<div class="navbar-controls">
|
||||||
<a href="/" class="nav-btn">← Dashboard</a>
|
<a href="/" class="nav-btn">← Dashboard</a>
|
||||||
@@ -927,7 +1009,7 @@
|
|||||||
{% if participants and participants|length >= 3 %}
|
{% if participants and participants|length >= 3 %}
|
||||||
<!-- Champion Podium -->
|
<!-- Champion Podium -->
|
||||||
<div class="champion-section">
|
<div class="champion-section">
|
||||||
<div class="champion-title">🏆 League Champions</div>
|
<div class="champion-title">🎖️ League Champions</div>
|
||||||
<div class="champion-container">
|
<div class="champion-container">
|
||||||
{% for i in range(3) %}
|
{% for i in range(3) %}
|
||||||
{% set participant = participants[i] %}
|
{% set participant = participants[i] %}
|
||||||
@@ -1091,11 +1173,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Auto-refresh indicator -->
|
|
||||||
<div class="refresh-indicator" id="refreshIndicator">
|
|
||||||
🔄 Updating...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const participants = {{ participants|tojson }};
|
const participants = {{ participants|tojson }};
|
||||||
const league = {{ league|tojson }};
|
const league = {{ league|tojson }};
|
||||||
|
|||||||
@@ -653,19 +653,19 @@
|
|||||||
<div class="stat-label">Total Tournaments</div>
|
<div class="stat-label">Total Tournaments</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card target-40">
|
<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-value">{{ tournaments|selectattr('tournament_type', 'equalto', '40_targets')|list|length if tournaments else 0 }}</div>
|
||||||
<div class="stat-label">40-Target Tournaments</div>
|
<div class="stat-label">40-Target Tournaments</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card target-20">
|
<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-value">{{ tournaments|selectattr('tournament_type', 'equalto', '20_targets')|list|length if tournaments else 0 }}</div>
|
||||||
<div class="stat-label">20-Target Tournaments</div>
|
<div class="stat-label">20-Target Tournaments</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card target-4">
|
<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-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>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<span class="stat-icon">👥</span>
|
<span class="stat-icon">👥</span>
|
||||||
@@ -679,11 +679,6 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<span>🎖️ League Championships</span>
|
<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>
|
||||||
<div class="archive-grid" id="leagues-grid">
|
<div class="archive-grid" id="leagues-grid">
|
||||||
{% for league in leagues %}
|
{% for league in leagues %}
|
||||||
@@ -716,12 +711,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="archive-actions">
|
<div class="archive-actions">
|
||||||
<a href="/archive/league/{{ league.filename }}" class="action-btn view-btn">🏆 View</a>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -738,12 +727,7 @@
|
|||||||
{% if tournaments_40 %}
|
{% if tournaments_40 %}
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<span>🎯 40-Target Tournaments</span>
|
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="archive-grid" id="tournaments-40-grid">
|
<div class="archive-grid" id="tournaments-40-grid">
|
||||||
{% for tournament in tournaments_40 %}
|
{% for tournament in tournaments_40 %}
|
||||||
@@ -776,12 +760,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="archive-actions">
|
<div class="archive-actions">
|
||||||
<a href="/archive/tournament/{{ tournament.filename }}" class="action-btn view-btn">📊 View</a>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -795,12 +773,7 @@
|
|||||||
{% if tournaments_20 %}
|
{% if tournaments_20 %}
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<span>🏹 20-Target Tournaments</span>
|
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="archive-grid" id="tournaments-20-grid">
|
<div class="archive-grid" id="tournaments-20-grid">
|
||||||
{% for tournament in tournaments_20 %}
|
{% for tournament in tournaments_20 %}
|
||||||
@@ -833,12 +806,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="archive-actions">
|
<div class="archive-actions">
|
||||||
<a href="/archive/tournament/{{ tournament.filename }}" class="action-btn view-btn">📊 View</a>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -852,12 +819,7 @@
|
|||||||
{% if tournaments_4 %}
|
{% if tournaments_4 %}
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<span>🎪 4-Target Tournaments</span>
|
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="archive-grid" id="tournaments-4-grid">
|
<div class="archive-grid" id="tournaments-4-grid">
|
||||||
{% for tournament in tournaments_4 %}
|
{% for tournament in tournaments_4 %}
|
||||||
@@ -890,12 +852,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="archive-actions">
|
<div class="archive-actions">
|
||||||
<a href="/archive/tournament/{{ tournament.filename }}" class="action-btn view-btn">📊 View</a>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -910,11 +866,6 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<span>🏆 Other Tournaments</span>
|
<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>
|
||||||
<div class="archive-grid" id="tournaments-other-grid">
|
<div class="archive-grid" id="tournaments-other-grid">
|
||||||
{% for tournament in tournaments_other %}
|
{% for tournament in tournaments_other %}
|
||||||
@@ -947,12 +898,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="archive-actions">
|
<div class="archive-actions">
|
||||||
<a href="/archive/tournament/{{ tournament.filename }}" class="action-btn view-btn">📊 View</a>
|
<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>
|
</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
|
// Filtering functions
|
||||||
function filterLeagues(filter) {
|
function filterLeagues(filter) {
|
||||||
const cards = document.querySelectorAll('#leagues-grid .archive-card');
|
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
|
// Modal functions
|
||||||
function showModal(title, message, confirmCallback) {
|
function showModal(title, message, confirmCallback) {
|
||||||
document.getElementById('modalTitle').textContent = title;
|
document.getElementById('modalTitle').textContent = title;
|
||||||
@@ -1211,38 +1071,6 @@
|
|||||||
closeEditModal();
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -65,28 +65,6 @@
|
|||||||
color: #007bff;
|
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 {
|
.nav-btn.secondary {
|
||||||
background: #6c757d;
|
background: #6c757d;
|
||||||
border-color: #495057;
|
border-color: #495057;
|
||||||
@@ -146,10 +124,142 @@
|
|||||||
gap: 10px;
|
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 */
|
/* Compact Stats Overview */
|
||||||
.stats-overview {
|
.stats-overview {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
@@ -453,7 +563,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stats-overview {
|
.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>
|
</style>
|
||||||
@@ -461,7 +589,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<!-- Navigation Bar -->
|
<!-- Navigation Bar -->
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<div class="navbar-title">👤 Player Analysis</div>
|
<div class="navbar-title">🎯 Player Analysis</div>
|
||||||
<div class="navbar-controls">
|
<div class="navbar-controls">
|
||||||
<a href="/" class="nav-btn primary">← Dashboard</a>
|
<a href="/" class="nav-btn primary">← Dashboard</a>
|
||||||
<a href="/archive" class="nav-btn secondary">📚 Archive</a>
|
<a href="/archive" class="nav-btn secondary">📚 Archive</a>
|
||||||
@@ -473,22 +601,52 @@
|
|||||||
<div class="stats-overview">
|
<div class="stats-overview">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<span class="stat-icon">👥</span>
|
<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 class="stat-label">Total Players</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<span class="stat-icon">📊</span>
|
<span class="stat-icon">🎯</span>
|
||||||
<div class="stat-value" id="avgTournaments">{{ overview_stats.avg_tournaments if overview_stats else 0 }}</div>
|
<div class="stat-value" id="totalTournaments">Loading...</div>
|
||||||
<div class="stat-label">Avg Tournaments</div>
|
<div class="stat-label">Total Tournaments</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<span class="stat-icon">🏆</span>
|
<span class="stat-icon">🏆</span>
|
||||||
<div class="stat-value" id="topScore">{{ overview_stats.top_score if overview_stats else 0 }}</div>
|
<div class="stat-value" id="score20Targets">Loading...</div>
|
||||||
<div class="stat-label">Highest Score</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Navigation -->
|
||||||
<div class="section">
|
<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>
|
<div class="section-title">Select a Player to Analyze</div>
|
||||||
|
|
||||||
<!-- Controls -->
|
<!-- Controls -->
|
||||||
@@ -514,11 +672,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Players data from Flask
|
// Global data
|
||||||
let playersData = {{ players|tojson }};
|
let playersData = [];
|
||||||
let filteredPlayers = [...playersData];
|
let tournamentData = [];
|
||||||
|
let filteredPlayers = [];
|
||||||
let currentFilter = 'all';
|
let currentFilter = 'all';
|
||||||
let currentSort = 'name';
|
let currentSort = 'name';
|
||||||
let playersWithStats = [];
|
let playersWithStats = [];
|
||||||
@@ -526,9 +686,180 @@
|
|||||||
// Initialize page
|
// Initialize page
|
||||||
function initializePage() {
|
function initializePage() {
|
||||||
loadPlayerStats();
|
loadPlayerStats();
|
||||||
|
loadTournamentLeaders();
|
||||||
setupEventListeners();
|
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
|
// Load player statistics
|
||||||
async function loadPlayerStats() {
|
async function loadPlayerStats() {
|
||||||
try {
|
try {
|
||||||
@@ -539,9 +870,13 @@
|
|||||||
playersWithStats = result.players;
|
playersWithStats = result.players;
|
||||||
playersData = playersWithStats;
|
playersData = playersWithStats;
|
||||||
filteredPlayers = [...playersWithStats];
|
filteredPlayers = [...playersWithStats];
|
||||||
|
|
||||||
|
// Update total players count
|
||||||
|
document.getElementById('totalPlayers').textContent = playersData.length;
|
||||||
|
|
||||||
filterAndSortPlayers();
|
filterAndSortPlayers();
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to load player stats');
|
console.error('Failed to load player stats:', result.message);
|
||||||
renderPlayersBasic();
|
renderPlayersBasic();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -554,22 +889,12 @@
|
|||||||
function setupEventListeners() {
|
function setupEventListeners() {
|
||||||
const searchBox = document.getElementById('searchBox');
|
const searchBox = document.getElementById('searchBox');
|
||||||
const sortSelect = document.getElementById('sortSelect');
|
const sortSelect = document.getElementById('sortSelect');
|
||||||
const filterButtons = document.querySelectorAll('.filter-btn');
|
|
||||||
|
|
||||||
searchBox.addEventListener('input', filterAndSortPlayers);
|
searchBox.addEventListener('input', filterAndSortPlayers);
|
||||||
sortSelect.addEventListener('change', (e) => {
|
sortSelect.addEventListener('change', (e) => {
|
||||||
currentSort = e.target.value;
|
currentSort = e.target.value;
|
||||||
filterAndSortPlayers();
|
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
|
// Filter and sort players
|
||||||
@@ -675,15 +1000,31 @@
|
|||||||
|
|
||||||
// Render players without stats (fallback)
|
// Render players without stats (fallback)
|
||||||
function renderPlayersBasic() {
|
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');
|
const playersGrid = document.getElementById('playersGrid');
|
||||||
|
|
||||||
playersGrid.innerHTML = playersData.map(player => `
|
if (basicPlayers.length === 0) {
|
||||||
<div class="player-card ${player.current_player ? 'current-player' : 'archived-player'}"
|
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})">
|
onclick="viewPlayerStats(${player.id})">
|
||||||
<div class="player-header">
|
<div class="player-header">
|
||||||
<div class="player-name">${player.name}</div>
|
<div class="player-name">${player.name}</div>
|
||||||
<div class="player-badge ${player.current_player ? 'current' : 'archived'}">
|
<div class="player-badge ${player.enabled ? 'current' : 'archived'}">
|
||||||
${player.current_player ? 'Current' : 'Archived'}
|
${player.enabled ? 'Current' : 'Archived'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -711,6 +1052,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).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
|
// Create mini performance chart for player
|
||||||
|
|||||||
@@ -48,14 +48,14 @@
|
|||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
border: 2px solid #e9ecef;
|
border: 2px solid #e9ecef;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 8px 16px;
|
padding: 12px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
color: #333;
|
color: #333;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 0.85rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-btn:hover {
|
.nav-btn:hover {
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Top Section - Stats and Chart */
|
/* Top Section - Stats and Charts */
|
||||||
.top-section {
|
.top-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 320px 1fr;
|
grid-template-columns: 320px 1fr;
|
||||||
@@ -146,8 +146,8 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chart Panel */
|
/* Charts Panel */
|
||||||
.chart-panel {
|
.charts-panel {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
@@ -156,10 +156,151 @@
|
|||||||
flex-direction: column;
|
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 {
|
.chart-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
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 */
|
/* Bottom Section - History */
|
||||||
@@ -412,7 +553,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Top Section - Stats and Chart -->
|
<!-- Top Section - Stats and Charts -->
|
||||||
<div class="top-section">
|
<div class="top-section">
|
||||||
<!-- Stats Panel -->
|
<!-- Stats Panel -->
|
||||||
<div class="stats-panel">
|
<div class="stats-panel">
|
||||||
@@ -445,11 +586,92 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Performance Chart -->
|
<!-- Performance Charts -->
|
||||||
<div class="chart-panel">
|
<div class="charts-panel">
|
||||||
<div class="panel-title">📈 Performance Trend</div>
|
<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">
|
<div class="chart-container">
|
||||||
<canvas id="performanceChart"></canvas>
|
<canvas id="tournamentChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -532,42 +754,283 @@
|
|||||||
// Player data from Flask
|
// Player data from Flask
|
||||||
const playerStats = {{ stats|tojson }};
|
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
|
// Initialize page
|
||||||
function initializePage() {
|
function initializePage() {
|
||||||
createPerformanceChart();
|
groupTournamentsByType();
|
||||||
|
setupEventListeners();
|
||||||
|
loadShotAccuracyData();
|
||||||
|
updateChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create performance trend chart
|
// Load shot accuracy data from archive
|
||||||
function createPerformanceChart() {
|
async function loadShotAccuracyData() {
|
||||||
const ctx = document.getElementById('performanceChart').getContext('2d');
|
try {
|
||||||
const scores = playerStats.tournament_scores || [];
|
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) {
|
if (result.status === 'success') {
|
||||||
ctx.font = '14px Arial';
|
shotAccuracyData = result.data;
|
||||||
ctx.fillStyle = '#666';
|
console.log('Shot accuracy data loaded:', shotAccuracyData);
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText('No tournament data available', ctx.canvas.width / 2, ctx.canvas.height / 2);
|
// 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;
|
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',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: scores.map((_, i) => `T${i + 1}`),
|
labels: scores.map((_, i) => `${i + 1}`),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Tournament Score',
|
label: `${currentTournamentType} Score`,
|
||||||
data: scores,
|
data: scores,
|
||||||
borderColor: '#28a745',
|
borderColor: typeConfig.color,
|
||||||
backgroundColor: 'rgba(40, 167, 69, 0.15)',
|
backgroundColor: typeConfig.color + '20',
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
pointRadius: 5,
|
pointRadius: 5,
|
||||||
pointHoverRadius: 8,
|
pointHoverRadius: 8,
|
||||||
pointBackgroundColor: '#28a745',
|
pointBackgroundColor: typeConfig.color,
|
||||||
pointBorderColor: '#fff',
|
pointBorderColor: '#fff',
|
||||||
pointBorderWidth: 2,
|
pointBorderWidth: 2,
|
||||||
pointHoverBackgroundColor: '#1e7e34',
|
pointHoverBackgroundColor: typeConfig.color,
|
||||||
pointHoverBorderColor: '#fff'
|
pointHoverBorderColor: '#fff'
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
@@ -577,17 +1040,52 @@
|
|||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
backgroundColor: 'rgba(40, 167, 69, 0.9)',
|
backgroundColor: typeConfig.color + 'E6',
|
||||||
titleColor: '#fff',
|
titleColor: '#fff',
|
||||||
bodyColor: '#fff',
|
bodyColor: '#fff',
|
||||||
borderColor: '#28a745',
|
borderColor: typeConfig.color,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title: function(context) {
|
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) {
|
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: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
grid: { color: 'rgba(40, 167, 69, 0.1)' },
|
grid: {
|
||||||
|
color: typeConfig.color + '20'
|
||||||
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
color: '#28a745',
|
color: typeConfig.color,
|
||||||
font: { size: 11, weight: 'bold' }
|
font: { size: 12, weight: 'bold' }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
x: {
|
x: {
|
||||||
grid: { color: 'rgba(40, 167, 69, 0.1)' },
|
grid: {
|
||||||
|
color: typeConfig.color + '20'
|
||||||
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
color: '#28a745',
|
color: typeConfig.color,
|
||||||
font: { size: 11, weight: 'bold' }
|
font: { size: 12, weight: 'bold' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 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;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden; /* Prevent scrolling for TV display */
|
overflow: hidden; /* Prevent scrolling for TV display */
|
||||||
@@ -652,6 +652,87 @@
|
|||||||
.tv-refresh-indicator.show {
|
.tv-refresh-indicator.show {
|
||||||
opacity: 1;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -661,9 +742,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="navbar-controls">
|
<div class="navbar-controls">
|
||||||
<a href="/" class="nav-btn">← Dashboard</a>
|
<a href="/" class="nav-btn">← Dashboard</a>
|
||||||
<a href="/tournament/draft" class="nav-btn">📋 Draft</a>
|
<button class="nav-btn" onclick="printResults()">🖨️ Print</button>
|
||||||
<a href="/results/calculator" class="nav-btn primary">🎯 Calculator</a>
|
|
||||||
<button class="nav-btn" onclick="window.print()">🖨️ Print</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -867,6 +946,11 @@
|
|||||||
document.body.offsetHeight; // Trigger reflow
|
document.body.offsetHeight; // Trigger reflow
|
||||||
document.body.style.display = '';
|
document.body.style.display = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function printResults() {
|
||||||
|
// Trigger print dialog
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1082,21 +1082,21 @@
|
|||||||
|
|
||||||
<!-- Tournament Type Selection -->
|
<!-- Tournament Type Selection -->
|
||||||
<div class="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-options">
|
||||||
<div class="type-option" onclick="selectTournamentType('4_targets')">
|
<div class="type-option" onclick="selectTournamentType('4_targets')">
|
||||||
<input type="radio" name="tournament_type" value="4_targets">
|
<input type="radio" name="tournament_type" value="4_targets">
|
||||||
<div class="type-name">4 Targets</div>
|
<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-description">Quick format with 4 targets, 5 shots each <br> (20 shots total)</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="type-option selected" onclick="selectTournamentType('20_targets')">
|
<div class="type-option selected" onclick="selectTournamentType('20_targets')">
|
||||||
<input type="radio" name="tournament_type" value="20_targets" checked>
|
<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 class="type-description">Standard format with 20 targets, 2 shots each (40 shots total)</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="type-option" onclick="selectTournamentType('40_targets')">
|
<div class="type-option" onclick="selectTournamentType('40_targets')">
|
||||||
<input type="radio" name="tournament_type" value="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 class="type-description">Extended format with 40 targets, 2 shots each (80 shots total)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1108,7 +1108,7 @@
|
|||||||
|
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button class="action-btn success" id="startLeagueBtn" onclick="startLeague()">🏆 Start League (6 Tournaments)</button>
|
<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>
|
||||||
|
|
||||||
<div class="warning" id="warningMessage" style="display: none;">
|
<div class="warning" id="warningMessage" style="display: none;">
|
||||||
|
|||||||
Reference in New Issue
Block a user