diff --git a/app.py b/app.py index 47cf523..5abe380 100644 --- a/app.py +++ b/app.py @@ -638,14 +638,9 @@ def analyze_player_performance(player_id, archives_data): if score < player_stats['worst_tournament_score']: player_stats['worst_tournament_score'] = score - # Count shots fired - targets = participant.get('targets', {}) - shots_in_tournament = 0 - for target in targets.values(): - if target.get('shot1') is not None: - shots_in_tournament += 1 - if target.get('shot2') is not None: - shots_in_tournament += 1 + # Count shots fired - NOW WITH PROPER COUNTING FOR ALL FORMATS + tournament_type = archive.get('tournament_type', '20_targets') + shots_in_tournament = count_shots_in_tournament(participant, tournament_type) player_stats['total_shots_fired'] += shots_in_tournament @@ -655,7 +650,7 @@ def analyze_player_performance(player_id, archives_data): 'score': score, 'tournament_type': archive['tournament_type'], 'completed': completed, - 'shots_fired': shots_in_tournament + 'shots_fired': shots_in_tournament # NOW CORRECTLY CALCULATED }) except Exception as e: print(f"Error analyzing tournament archive: {e}") @@ -923,48 +918,7 @@ def api_get_archive_stats(): return jsonify({'status': 'success', 'stats': stats}) except Exception as e: return jsonify({'status': 'error', 'message': str(e)}), 500 -@app.route('/api/archive/player//performance', methods=['GET']) -def api_get_player_performance(player_id): - """API endpoint to get player performance data for charts""" - try: - archives_data = { - 'tournaments': get_archived_tournaments(), - 'leagues': get_archived_leagues() - } - - player_stats = analyze_player_performance(player_id, archives_data) - - # Prepare chart data - performance_data = { - 'trend': { - 'labels': [f'T{i+1}' for i in range(len(player_stats['tournament_scores']))], - 'data': player_stats['tournament_scores'] - }, - 'distribution': { - 'labels': ['0-50', '51-60', '61-70', '71-80', '81-90', '91-100'], - 'data': [0, 0, 0, 0, 0, 0] - } - } - - # Calculate score distribution - for score in player_stats['tournament_scores']: - if score <= 50: - performance_data['distribution']['data'][0] += 1 - elif score <= 60: - performance_data['distribution']['data'][1] += 1 - elif score <= 70: - performance_data['distribution']['data'][2] += 1 - elif score <= 80: - performance_data['distribution']['data'][3] += 1 - elif score <= 90: - performance_data['distribution']['data'][4] += 1 - else: - performance_data['distribution']['data'][5] += 1 - - return jsonify({'status': 'success', 'performance': performance_data}) - except Exception as e: - return jsonify({'status': 'error', 'message': str(e)}), 500 - + @app.route('/api/archive/players/with-stats', methods=['GET']) def api_get_players_with_stats(): """API endpoint to get all players with their basic stats""" @@ -2162,7 +2116,504 @@ def get_camera_titles(): except Exception as e: return jsonify({'status': 'error', 'message': str(e)}), 400 +@app.route('/api/archive/player//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//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/') +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/') +def get_tournament_file_debug(filename): + """ + Debug endpoint to see raw tournament file data + """ + try: + tournament_archives_path = 'tournament_archives' + file_path = os.path.join(tournament_archives_path, filename) + + if not os.path.exists(file_path): + return jsonify({ + 'status': 'error', + 'message': 'File not found' + }), 404 + + with open(file_path, 'r') as f: + data = json.load(f) + + return jsonify({ + 'status': 'success', + 'file_path': file_path, + 'data': data + }) + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': str(e) + }), 500 + +@app.route('/api/archive/tournament-leaders', methods=['GET']) +def api_get_tournament_leaders(): + """API endpoint to get overall tournament leaders by tournament type""" + try: + tournaments = get_archived_tournaments() + + # Group data by tournament type + tournament_types = { + '20_targets': { + 'name': '20 Targets', + 'description': '20 Targets (2 shots each)', + 'best_score': {'player_name': None, 'score': 0}, + 'most_tens': {'player_name': None, 'tens': 0}, + 'total_tournaments': 0 + }, + '40_targets': { + 'name': '40 Targets', + 'description': '40 Targets (2 shots each)', + 'best_score': {'player_name': None, 'score': 0}, + 'most_tens': {'player_name': None, 'tens': 0}, + 'total_tournaments': 0 + }, + '4_targets': { + 'name': '4 Targets', + 'description': '4 Targets (5 shots each)', + 'best_score': {'player_name': None, 'score': 0}, + 'most_tens': {'player_name': None, 'tens': 0}, + 'total_tournaments': 0 + } + } + + for tournament in tournaments: + try: + data = load_archive_file(tournament['filepath']) + if not data: + continue + + results = data.get('results', {}) + participants = results.get('participants', {}) + tournament_type = tournament.get('tournament_type', '20_targets') + + if tournament_type not in tournament_types or not participants: + continue + + tournament_types[tournament_type]['total_tournaments'] += 1 + + for player_id, participant in participants.items(): + if not participant.get('completed'): + continue + + player_name = participant.get('name', f'Player {player_id}') + + # Check best score for this tournament type + score = participant.get('total_score', 0) + if score > tournament_types[tournament_type]['best_score']['score']: + tournament_types[tournament_type]['best_score'] = { + 'player_name': player_name, + 'score': score + } + + # Count 10s for this player in this tournament + targets = participant.get('targets', {}) + tens_count = 0 + for target in targets.values(): + if target.get('shot1') == 10: + tens_count += 1 + if target.get('shot2') == 10: + tens_count += 1 + # For 4_targets format, check additional shots + for shot_num in range(3, 6): # shot3, shot4, shot5 + if target.get(f'shot{shot_num}') == 10: + tens_count += 1 + + if tens_count > tournament_types[tournament_type]['most_tens']['tens']: + tournament_types[tournament_type]['most_tens'] = { + 'player_name': player_name, + 'tens': tens_count + } + + except Exception as e: + print(f"Error processing tournament {tournament.get('filepath', 'unknown')}: {e}") + continue + + # Convert to list format, only include types with data + tournament_leaders = [] + for type_key, type_data in tournament_types.items(): + if type_data['total_tournaments'] > 0 and type_data['best_score']['player_name']: + tournament_leaders.append({ + 'id': type_key, + 'name': type_data['name'], + 'description': type_data['description'], + 'total_tournaments': type_data['total_tournaments'], + 'best_score': type_data['best_score'], + 'most_tens': type_data['most_tens'] + }) + + return jsonify({'status': 'success', 'tournament_types': tournament_leaders}) + + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file diff --git a/templates/league_scoreboard_display.html b/templates/league_scoreboard_display.html index 997a817..54aaec9 100644 --- a/templates/league_scoreboard_display.html +++ b/templates/league_scoreboard_display.html @@ -880,12 +880,94 @@ .refresh-indicator.show { opacity: 1; } + + /* PRINT STYLES - Clean professional printout */ + @media print { + /* Hide everything except the rankings table */ + .navbar, + .left-column, + .tv-refresh-indicator { + display: none !important; + } + + /* Reset body and html for print */ + html, body { + height: auto !important; + overflow: visible !important; + background: white !important; + margin: 0; + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + } + + /* Make the container take full width and remove grid */ + .tv-container { + display: block !important; + height: auto !important; + padding: 0 !important; + gap: 0 !important; + max-width: 100%; + } + + /* Style the right column for print */ + .right-column { + background: white !important; + border-radius: 0 !important; + box-shadow: none !important; + overflow: visible !important; + display: block !important; + height: auto !important; + margin: 0; + padding: 0; + } + + /* Create a beautiful print header */ + .table-header { + background: white !important; + padding: 30px 0 30px 0 !important; + border-bottom: 3px solid #007bff !important; + text-align: center; + margin-bottom: 30px !important; + page-break-inside: avoid; + position: relative; + } + + /* Add logo using CSS */ + .table-header::before { + content: ""; + display: block; + width: 200px; + height: 80px; + margin: 0 auto 20px auto; + background: url('/static/logo.png') no-repeat center center; + background-size: contain; + /* Fallback for when logo doesn't load */ + background-color: transparent; + } + + /* Add title using CSS */ + .table-header::after { + content: "🎖️ LEAGUE RESULTS"; + display: block; + font-size: 28pt; + font-weight: bold; + color: #333; + margin: 0; + letter-spacing: 2px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + } + + /* Hide the original table title */ + .table-title { + display: none !important; + } + }