diff --git a/app.py b/app.py index 7f12fab..47cf523 100644 --- a/app.py +++ b/app.py @@ -299,7 +299,17 @@ def create_results_structure(tournament_data): return None tournament_type = tournament_data.get('tournament_type', '20_targets') - num_targets = 40 if tournament_type == '40_targets' else 20 + + # Determine target count and shots per target + if tournament_type == '40_targets': + num_targets = 40 + shots_per_target = 2 + elif tournament_type == '4_targets': + num_targets = 4 + shots_per_target = 5 + else: # 20_targets (default) + num_targets = 20 + shots_per_target = 2 results = { 'tournament_id': tournament_data.get('created_at', datetime.now().isoformat()), @@ -320,9 +330,18 @@ def create_results_structure(tournament_data): for player in all_players: player_id = str(player['id']) + + # Create target structure based on tournament type + targets = {} + for i in range(1, num_targets + 1): + target = {} + for j in range(1, shots_per_target + 1): + target[f'shot{j}'] = None + targets[str(i)] = target + results['participants'][player_id] = { 'name': player['name'], - 'targets': {str(i): {'shot1': None, 'shot2': None} for i in range(1, num_targets + 1)}, + 'targets': targets, 'total_score': 0, 'completed': False } @@ -333,19 +352,17 @@ def calculate_total_score(targets): """Calculate total score from targets, treating None as 0""" total = 0 for target in targets.values(): - shot1 = target.get('shot1') - shot2 = target.get('shot2') - total += (shot1 if shot1 is not None else 0) - total += (shot2 if shot2 is not None else 0) + for shot_key, shot_value in target.items(): + if shot_key.startswith('shot') and shot_value is not None: + total += shot_value return total def is_participant_completed(targets): """Check if a participant has completed all targets (all shots entered, including 0s)""" for target in targets.values(): - shot1 = target.get('shot1') - shot2 = target.get('shot2') - if shot1 is None or shot2 is None: - return False + for shot_key, shot_value in target.items(): + if shot_key.startswith('shot') and shot_value is None: + return False return True def calculate_league_final_scores(league_data): @@ -749,6 +766,8 @@ def get_all_players_from_archives(): return players_list + + # Add these routes after the existing routes in app.py # Add this to your app.py file to integrate the modern archive system @@ -875,15 +894,17 @@ def api_get_archive_stats(): 'leagues': [1, 1, 2, 1, 2, 1] } - # Calculate tournament type distribution - type_distribution = {'20_targets': 0, '40_targets': 0} + # Calculate tournament type distribution - now includes 4_targets + type_distribution = {'20_targets': 0, '40_targets': 0, '4_targets': 0} for tournament in tournaments: tournament_type = tournament.get('tournament_type', '20_targets') - type_distribution[tournament_type] += 1 + if tournament_type in type_distribution: + type_distribution[tournament_type] += 1 for league in leagues: league_type = league.get('tournament_type', '20_targets') - type_distribution[league_type] += 1 + if league_type in type_distribution: + type_distribution[league_type] += 1 stats = { 'overview': { @@ -894,15 +915,14 @@ def api_get_archive_stats(): }, 'activity_data': activity_data, 'type_distribution': { - 'labels': ['20 Targets', '40 Targets'], - 'data': [type_distribution['20_targets'], type_distribution['40_targets']] + 'labels': ['20 Targets', '40 Targets', '4 Targets'], + 'data': [type_distribution['20_targets'], type_distribution['40_targets'], type_distribution['4_targets']] } } 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""" @@ -1629,10 +1649,9 @@ def start_league(): try: data = request.get_json() tournament_type = data.get('tournament_type', '20_targets') - - if tournament_type not in ['20_targets', '40_targets']: + if tournament_type not in ['20_targets', '40_targets', '4_targets']: return jsonify({'status': 'error', 'message': 'Invalid tournament type'}), 400 - + players_data = load_players() enabled_players = [p for p in players_data['players'] if p['enabled']] @@ -1741,7 +1760,7 @@ def start_tournament(): data = request.get_json() or {} tournament_type = data.get('tournament_type', '20_targets') - if tournament_type not in ['20_targets', '40_targets']: + if tournament_type not in ['20_targets', '40_targets', '4_targets']: return jsonify({'status': 'error', 'message': 'Invalid tournament type'}), 400 players_data = load_players() @@ -1869,6 +1888,13 @@ def finish_tournament(): results['tournament_finished'] = True results['finished_at'] = datetime.now().isoformat() + # Define total shots per participant for each tournament type + total_shots_per_participant = { + '20_targets': 40, # 20 targets × 2 shots + '40_targets': 80, # 40 targets × 2 shots + '4_targets': 20 # 4 targets × 5 shots + } + league_finished = False # Track if league finished # Update league state if this is a league tournament @@ -1901,13 +1927,21 @@ def finish_tournament(): 'joker': True }) + # Calculate total shots correctly for any tournament type + tournament_type = results.get('tournament_type', '20_targets') + shots_per_participant = total_shots_per_participant.get(tournament_type, 40) + total_shots_fired = len(results['participants']) * shots_per_participant + # Add to completed tournaments league_state['completed_tournaments'].append({ 'tournament_number': tournament_number, + 'tournament_type': tournament_type, 'finished_at': datetime.now().isoformat(), 'results_summary': { 'participants': len(results['participants']), - 'total_shots': len(results['participants']) * (40 if results.get('tournament_type') == '40_targets' else 20) * 2 + 'shots_per_participant': shots_per_participant, + 'total_shots': total_shots_fired, + 'format_description': get_tournament_format_description(tournament_type) } }) @@ -1920,7 +1954,7 @@ def finish_tournament(): calculate_league_final_scores(league_state) league_finished = True - print("League finished!") + print(f"League finished! Final tournament was {tournament_type} format.") save_league_state(league_state) @@ -1928,9 +1962,11 @@ def finish_tournament(): archive_success = False if not league_state: # Only archive standalone tournaments archive_success = archive_tournament(tournament_state, results) - print(f"Standalone tournament archived: {archive_success}") + tournament_type = results.get('tournament_type', '20_targets') + print(f"Standalone {tournament_type} tournament archived: {archive_success}") else: - print("League tournament - not archiving individual tournament") + tournament_type = results.get('tournament_type', '20_targets') + print(f"League {tournament_type} tournament - not archiving individual tournament") # Archive the league if it just finished league_archive_success = False @@ -1952,7 +1988,9 @@ def finish_tournament(): 'status': 'success', 'results': results, 'archived': archive_success, - 'league_archived': league_archive_success if league_finished else None + 'league_archived': league_archive_success if league_finished else None, + 'tournament_type': results.get('tournament_type', '20_targets'), + 'tournament_format': get_tournament_format_description(results.get('tournament_type', '20_targets')) } if league_state: @@ -1967,6 +2005,15 @@ def finish_tournament(): print(f"Error finishing tournament: {e}") return jsonify({'status': 'error', 'message': str(e)}), 400 + +def get_tournament_format_description(tournament_type): + """Get human-readable description of tournament format""" + format_descriptions = { + '20_targets': '20 Targets (2 shots each)', + '40_targets': '40 Targets (2 shots each)', + '4_targets': '4 Targets (5 shots each)' + } + return format_descriptions.get(tournament_type, '20 Targets (2 shots each)') @app.route('/api/results', methods=['GET']) def get_results(): """API endpoint to get current results""" @@ -2115,5 +2162,7 @@ def get_camera_titles(): except Exception as e: return jsonify({'status': 'error', 'message': str(e)}), 400 + + if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file diff --git a/templates/modern_archive_index.html b/templates/modern_archive_index.html index dd05a5e..97b138f 100644 --- a/templates/modern_archive_index.html +++ b/templates/modern_archive_index.html @@ -137,9 +137,42 @@ margin-bottom: 15px; border-bottom: 3px solid #007bff; padding-bottom: 5px; + display: flex; + justify-content: space-between; + align-items: center; } - /* Compact Stats Overview */ + .section-controls { + display: flex; + gap: 10px; + align-items: center; + } + + .filter-btn { + background: #f8f9fa; + border: 1px solid #dee2e6; + padding: 6px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 0.8rem; + transition: all 0.2s ease; + } + + .filter-btn.active { + background: #007bff; + color: white; + border-color: #007bff; + } + + .filter-btn:hover { + background: #e9ecef; + } + + .filter-btn.active:hover { + background: #0056b3; + } + + /* Enhanced Stats Overview */ .stats-overview { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); @@ -181,7 +214,20 @@ text-transform: uppercase; } - /* Compact Archive Grid */ + /* Target-specific stat cards */ + .stat-card.target-40 { + border-left: 4px solid #dc3545; + } + + .stat-card.target-20 { + border-left: 4px solid #ffc107; + } + + .stat-card.target-4 { + border-left: 4px solid #28a745; + } + + /* Enhanced Archive Grid */ .archive-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); @@ -221,6 +267,22 @@ background: #fff3cd; } + .archive-card.tournament-40 .archive-header { + background: #f8d7da; + } + + .archive-card.tournament-20 .archive-header { + background: #fff3cd; + } + + .archive-card.tournament-4 .archive-header { + background: #d1edff; + } + + .archive-card.tournament-other .archive-header { + background: #f8f9fa; + } + .archive-title { font-size: 1.2rem; font-weight: bold; @@ -251,6 +313,21 @@ color: #856404; } + .badge-40-targets { + background: #dc3545; + color: white; + } + + .badge-20-targets { + background: #ffc107; + color: #856404; + } + + .badge-4-targets { + background: #17a2b8; + color: white; + } + .archive-content { padding: 15px 20px; } @@ -283,23 +360,28 @@ display: flex; justify-content: space-between; align-items: center; - gap: 10px; + gap: 8px; padding-top: 12px; border-top: 1px solid #f0f0f0; } - .view-btn { - background: #007bff; + .action-btn { border: none; - color: white; - padding: 8px 16px; + padding: 8px 12px; border-radius: 6px; cursor: pointer; - font-size: 0.85rem; + font-size: 0.8rem; font-weight: bold; transition: all 0.2s ease; text-decoration: none; display: inline-block; + text-align: center; + } + + .view-btn { + background: #007bff; + color: white; + flex: 1; } .view-btn:hover { @@ -307,16 +389,18 @@ transform: translateY(-1px); } + .edit-btn { + background: #28a745; + color: white; + } + + .edit-btn:hover { + background: #1e7e34; + } + .delete-btn { background: #dc3545; - border: none; color: white; - padding: 6px 12px; - border-radius: 6px; - cursor: pointer; - font-size: 0.75rem; - font-weight: bold; - transition: all 0.2s ease; } .delete-btn:hover { @@ -342,7 +426,7 @@ opacity: 0.5; } - /* Confirmation Modal */ + /* Modal Styles */ .modal-overlay { position: fixed; top: 0; @@ -364,18 +448,20 @@ visibility: visible; } - .confirmation-modal { + .modal { background: white; border-radius: 8px; padding: 20px; - max-width: 400px; + max-width: 500px; width: 90%; + max-height: 80vh; + overflow-y: auto; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); transform: scale(0.9); transition: transform 0.2s ease; } - .modal-overlay.active .confirmation-modal { + .modal-overlay.active .modal { transform: scale(1); } @@ -383,7 +469,7 @@ font-size: 1.1rem; font-weight: bold; color: #333; - margin-bottom: 10px; + margin-bottom: 15px; } .modal-message { @@ -396,6 +482,7 @@ display: flex; gap: 10px; justify-content: flex-end; + margin-top: 20px; } .modal-btn { @@ -425,6 +512,64 @@ background: #c82333; } + .modal-btn.primary { + background: #007bff; + color: white; + } + + .modal-btn.primary:hover { + background: #0056b3; + } + + /* Edit Form Styles */ + .edit-form { + display: flex; + flex-direction: column; + gap: 15px; + } + + .form-group { + display: flex; + flex-direction: column; + gap: 5px; + } + + .form-label { + font-weight: bold; + color: #333; + font-size: 0.9rem; + } + + .form-input { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.9rem; + } + + .form-input:focus { + outline: none; + border-color: #007bff; + } + + .form-select { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.9rem; + background: white; + } + + .form-select:focus { + outline: none; + border-color: #007bff; + } + + /* Hidden class for filtering */ + .hidden { + display: none !important; + } + /* Mobile responsive */ @media (max-width: 768px) { .navbar { @@ -468,9 +613,19 @@ gap: 8px; } - .view-btn { + .action-btn { text-align: center; } + + .section-title { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .section-controls { + flex-wrap: wrap; + } } @@ -485,37 +640,54 @@
- +
-
- 🏆 -
{{ stats.total_tournaments }}
-
Standalone Tournaments
-
🎖️ -
{{ stats.total_leagues }}
+
{{ leagues|length if leagues else 0 }}
Completed Leagues
- 👥 -
{{ stats.total_players }}
-
Active Players
+ 🏆 +
{{ tournaments|length if tournaments else 0 }}
+
Total Tournaments
+
+
+ 🎯 +
{{ tournaments|selectattr('tournament_type', 'equalto', '40_targets')|list|length if tournaments else 0 }}
+
40-Target Tournaments
+
+
+ 🏹 +
{{ tournaments|selectattr('tournament_type', 'equalto', '20_targets')|list|length if tournaments else 0 }}
+
20-Target Tournaments
+
+
+ 🎪 +
{{ tournaments|selectattr('tournament_type', 'equalto', '4_targets')|list|length if tournaments else 0 }}
+
4-Target Tournaments
- 📊 -
{{ stats.total_matches }}
-
Total Competitions
+ 👥 +
{{ stats.total_players if stats else 0 }}
+
Active Players
{% if leagues %}
-

🎖️ League Championships

-
+
+ 🎖️ League Championships +
+ + + +
+
+
{% for league in leagues %} -
+
League Championship
@@ -543,9 +715,12 @@
- 🏆 View Results - +
@@ -555,16 +730,198 @@
{% endif %} - + {% if tournaments %} + + + {% set tournaments_40 = tournaments|selectattr('tournament_type', 'equalto', '40_targets')|list %} + {% if tournaments_40 %}
-

🏆 Standalone Tournaments

-
- {% for tournament in tournaments %} -
+
+ 🎯 40-Target Tournaments +
+ + + +
+
+
+ {% for tournament in tournaments_40 %} +
-
Single Tournament
+
40-Target Tournament
+
{{ tournament.created_at[:10] if tournament.created_at != 'Unknown' else 'Unknown Date' }}
+
+ 40 Targets +
+
+
+
+ 👥 + {{ tournament.participants_count }} players +
+
+ 🎯 + {{ tournament.tournament_type.replace('_', ' ')|title }} +
+
+ 📅 + {{ tournament.archived_at[:10] if tournament.archived_at != 'Unknown' else 'Unknown Date' }} +
+
+ 🏁 + {{ 'Finished' if tournament.tournament_finished else 'Incomplete' }} +
+
+
+ 📊 View + + +
+
+
+ {% endfor %} +
+
+ {% endif %} + + + {% set tournaments_20 = tournaments|selectattr('tournament_type', 'equalto', '20_targets')|list %} + {% if tournaments_20 %} +
+
+ 🏹 20-Target Tournaments +
+ + + +
+
+
+ {% for tournament in tournaments_20 %} +
+
+
+
20-Target Tournament
+
{{ tournament.created_at[:10] if tournament.created_at != 'Unknown' else 'Unknown Date' }}
+
+ 20 Targets +
+
+
+
+ 👥 + {{ tournament.participants_count }} players +
+
+ 🎯 + {{ tournament.tournament_type.replace('_', ' ')|title }} +
+
+ 📅 + {{ tournament.archived_at[:10] if tournament.archived_at != 'Unknown' else 'Unknown Date' }} +
+
+ 🏁 + {{ 'Finished' if tournament.tournament_finished else 'Incomplete' }} +
+
+
+ 📊 View + + +
+
+
+ {% endfor %} +
+
+ {% endif %} + + + {% set tournaments_4 = tournaments|selectattr('tournament_type', 'equalto', '4_targets')|list %} + {% if tournaments_4 %} +
+
+ 🎪 4-Target Tournaments +
+ + + +
+
+
+ {% for tournament in tournaments_4 %} +
+
+
+
4-Target Tournament
+
{{ tournament.created_at[:10] if tournament.created_at != 'Unknown' else 'Unknown Date' }}
+
+ 4 Targets +
+
+
+
+ 👥 + {{ tournament.participants_count }} players +
+
+ 🎯 + {{ tournament.tournament_type.replace('_', ' ')|title }} +
+
+ 📅 + {{ tournament.archived_at[:10] if tournament.archived_at != 'Unknown' else 'Unknown Date' }} +
+
+ 🏁 + {{ 'Finished' if tournament.tournament_finished else 'Incomplete' }} +
+
+
+ 📊 View + + +
+
+
+ {% endfor %} +
+
+ {% endif %} + + + {% set tournaments_other = tournaments|rejectattr('tournament_type', 'in', ['4_targets', '20_targets', '40_targets'])|list %} + {% if tournaments_other %} +
+
+ 🏆 Other Tournaments +
+ + + +
+
+
+ {% for tournament in tournaments_other %} +
+
+
+
Tournament ({{ tournament.tournament_type or 'Unknown' }})
{{ tournament.created_at[:10] if tournament.created_at != 'Unknown' else 'Unknown Date' }}
Tournament @@ -589,9 +946,12 @@
- 📊 View Results - +
@@ -600,6 +960,8 @@
{% endif %} + + {% endif %} {% if not leagues and not tournaments %} @@ -616,7 +978,7 @@