From 2b943c5c228b928e0ac7a18f74ce019f1006f44d Mon Sep 17 00:00:00 2001 From: cedrick2711 Date: Fri, 14 Aug 2026 12:03:39 -0400 Subject: [PATCH 1/2] =?UTF-8?q?r=C3=A9gler=20probl=C3=A8me=20avec=20le=20b?= =?UTF-8?q?outon=20pour=20sauvegarder=20les=20dispo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/users.py | 39 ++++++++++++++++++-------------- app/templates/pages/profile.html | 7 ++---- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/app/routes/users.py b/app/routes/users.py index 4a0a38b..0e3294d 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -407,9 +407,18 @@ def add_disponibility(): @users_bp.route('/disponibilities/add_bulk', methods=['POST']) @login_required def add_disponibilities_bulk(): - """Add multiple disponibility blocks at once.""" + """Replace the player's disponibilities with the submitted slots. + + Performs a full replace (delete existing + insert submitted) so that + deselected slots are correctly removed, mirroring the coach availability + flow. This keeps the auto-save idempotent and correct. + """ data = request.get_json() - slots = data.get('slots', []) + slots = data.get('slots', []) if data else [] + + # Clear existing disponibilities for the current player. + PlayerDisponibility.query.filter_by(player_id=current_user.id).delete() + created = [] for slot in slots: day_of_week = slot.get('day_of_week') @@ -422,21 +431,17 @@ def add_disponibilities_bulk(): continue end_time = add_30_minutes(start_time) - existing = PlayerDisponibility.query.filter_by( - player_id=current_user.id, day_of_week=day_of_week, start_time=start_time, - ).first() - if not existing: - disponibility = PlayerDisponibility( - player_id=current_user.id, day_of_week=day_of_week, - start_time=start_time, end_time=end_time, - ) - db.session.add(disponibility) - db.session.flush() - created.append({ - 'id': disponibility.id, 'day_of_week': disponibility.day_of_week, - 'day_name': DAY_NAMES[disponibility.day_of_week], - 'start_time': disponibility.start_time.strftime('%H:%M'), - }) + disponibility = PlayerDisponibility( + player_id=current_user.id, day_of_week=day_of_week, + start_time=start_time, end_time=end_time, + ) + db.session.add(disponibility) + db.session.flush() + created.append({ + 'id': disponibility.id, 'day_of_week': disponibility.day_of_week, + 'day_name': DAY_NAMES[disponibility.day_of_week], + 'start_time': disponibility.start_time.strftime('%H:%M'), + }) db.session.commit() return jsonify({'success': True, 'created': created}) diff --git a/app/templates/pages/profile.html b/app/templates/pages/profile.html index 3eb2eff..f4b8b84 100644 --- a/app/templates/pages/profile.html +++ b/app/templates/pages/profile.html @@ -193,9 +193,6 @@

Loading...

- @@ -369,7 +366,7 @@ function saveCoachAvailability() { const msg = document.createElement('div'); msg.className = 'alert alert-success'; msg.style.marginTop = '10px'; - msg.innerHTML = ' Availability saved!'; + msg.innerHTML = ' Availability saved!'; document.getElementById('availability-grid').appendChild(msg); setTimeout(() => msg.remove(), 3000); } @@ -571,7 +568,7 @@ function saveDisponibilities() { var msg = document.createElement('div'); msg.className = 'alert alert-success'; msg.style.marginTop = '10px'; - msg.innerHTML = ' Disponibilities saved successfully!'; + msg.innerHTML = ' Disponibilities saved successfully!'; document.getElementById('disponibilities-grid').appendChild(msg); setTimeout(function() { msg.remove(); }, 3000); } From a0e31d1a2fc1f60a92caa1283850fad102cee460 Mon Sep 17 00:00:00 2001 From: cedrick2711 Date: Mon, 17 Aug 2026 18:16:22 -0400 Subject: [PATCH 2/2] ajout dune page batch evaluation --- app/models/__init__.py | 1 + app/models/_constants.py | 15 +++ app/routes/evaluations.py | 110 ++++++++++++++++++- app/static/css/style.css | 27 +++++ app/templates/pages/players_to_evaluate.html | 110 +++++++++++-------- 5 files changed, 217 insertions(+), 46 deletions(-) diff --git a/app/models/__init__.py b/app/models/__init__.py index e82c7e9..41de8f4 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -20,6 +20,7 @@ from app.models._constants import ( GAME_PLATFORMS, PLATFORM_CODES, TRN_URLS, + EVALUATION_CRITERIA, ) # ========================================================================= diff --git a/app/models/_constants.py b/app/models/_constants.py index 180c14a..48480e0 100644 --- a/app/models/_constants.py +++ b/app/models/_constants.py @@ -9,6 +9,21 @@ Contains game lists, position mappings, platform codes, and TRN URL templates. USER_TYPES = ['admin', 'manager', 'coach', 'player', 'scout'] +# Ordered list of (field_name, human_label) pairs for the player evaluation +# score criteria. Kept in a single place so the evaluation forms, batch +# evaluation page, and any future reporting all stay in sync. +EVALUATION_CRITERIA = [ + ('mecanics_score', 'Mecanics'), + ('cohesion_score', 'Cohesion'), + ('communication_score', 'Communication'), + ('gamesense_score', 'Gamesense'), + ('versatility_score', 'Versatility'), + ('discipline_score', 'Discipline'), + ('analysis_score', 'Analysis'), + ('sport_ethics_score', 'Sport Ethics'), + ('mental_score', 'Mental'), +] + ESPORT_GAMES = [ 'Valorant', 'League of Legends', diff --git a/app/routes/evaluations.py b/app/routes/evaluations.py index 6efa290..128a61c 100644 --- a/app/routes/evaluations.py +++ b/app/routes/evaluations.py @@ -9,7 +9,7 @@ from app.extensions import db from app.models import ( Admin, Coach, Manager, Player, User, Tryout, Evaluation, TryoutRegistration, - OrgTeam, GAME_POSITIONS, + OrgTeam, GAME_POSITIONS, EVALUATION_CRITERIA, ) from sqlalchemy import func from sqlalchemy.orm import aliased @@ -30,6 +30,21 @@ def validate_score(score_value): return None +def compute_overall(scores): + """Average the non-None scores, or return None if there are none.""" + valid = [s for s in scores if s is not None] + return sum(valid) / len(valid) if valid else None + + +def _apply_evaluation(evaluation, scores, comments, position): + """Write validated scores/comments/position onto an Evaluation instance.""" + for field_name, _ in EVALUATION_CRITERIA: + setattr(evaluation, field_name, scores[field_name]) + evaluation.overall_score = compute_overall(list(scores.values())) + evaluation.comments = comments + evaluation.position_recommendation = position + + @evaluations_bp.route('') @login_required def list_evaluations(): @@ -230,4 +245,95 @@ def players_to_evaluate(tryout_id): players.append({'player': p, 'evaluated': existing is not None, 'registration': reg}) - return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players) \ No newline at end of file + return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players) + + +@evaluations_bp.route('//batch', methods=['GET', 'POST']) +@login_required +def batch_evaluate(tryout_id): + """Evaluate multiple players at once in a tryout. + + GET renders a single form listing every selected player with their + evaluation criteria. POST saves (creates or updates) all of them. + """ + if not current_user.can_evaluate(): + flash('You do not have permission to evaluate players.', 'danger') + return redirect(url_for('main.dashboard')) + + tryout = Tryout.query.get_or_404(tryout_id) + if not current_user.can_manage_this_tryout(tryout): + flash('You do not have permission to evaluate players in this tryout.', 'danger') + return redirect(url_for('tryouts.list_tryouts')) + + # Resolve selected player ids (query string on GET, hidden fields on POST). + player_ids = [] + for raw in request.values.getlist('player_ids'): + try: + pid = int(raw) + except (ValueError, TypeError): + continue + if pid not in player_ids: + player_ids.append(pid) + + if not player_ids: + flash('Please select at least one player to evaluate.', 'warning') + return redirect(url_for('evaluations.players_to_evaluate', tryout_id=tryout_id)) + + players = [] + for pid in player_ids: + player = User.query.get(pid) + if not player or not isinstance(player, Player): + continue + is_registered = TryoutRegistration.query.filter_by( + tryout_id=tryout_id, player_id=pid, + ).first() is not None + if not is_registered: + continue + existing = Evaluation.query.filter_by( + tryout_id=tryout_id, player_id=pid, evaluator_id=current_user.id, + ).first() + existing_scores = { + field_name: getattr(existing, field_name) if existing else None + for field_name, _ in EVALUATION_CRITERIA + } + players.append({ + 'player': player, + 'existing': existing, + 'existing_scores': existing_scores, + }) + + if not players: + flash('No valid players selected for evaluation.', 'danger') + return redirect(url_for('evaluations.players_to_evaluate', tryout_id=tryout_id)) + + if request.method == 'POST': + saved = 0 + for entry in players: + pid = entry['player'].id + scores = { + field_name: validate_score(request.form.get(f'{field_name}_{pid}')) + for field_name, _ in EVALUATION_CRITERIA + } + comments = request.form.get(f'comments_{pid}') + position = request.form.get(f'position_recommendation_{pid}') + + existing = entry['existing'] + if existing: + _apply_evaluation(existing, scores, comments, position) + else: + evaluation = Evaluation( + tryout_id=tryout_id, player_id=pid, + evaluator_id=current_user.id, + ) + _apply_evaluation(evaluation, scores, comments, position) + db.session.add(evaluation) + saved += 1 + + db.session.commit() + flash(f'Saved evaluations for {saved} player(s).', 'success') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + + return render_template('pages/batch_evaluate.html', + tryout=tryout, players=players, + evaluation_criteria=EVALUATION_CRITERIA, + game_positions=GAME_POSITIONS) diff --git a/app/static/css/style.css b/app/static/css/style.css index aab79eb..f2264ca 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -1955,3 +1955,30 @@ a:hover { color: var(--primary-dark); } [data-theme="dark"] .error-container p { color: var(--text-secondary); } + +/* Batch Evaluation - 2 cards wide layout */ +.batch-eval-form { + max-width: none; +} + +.batch-eval-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + margin-bottom: 20px; +} + +.batch-eval-grid .card { + margin-bottom: 0; +} + +/* Allow criteria rows to wrap for a 3x3 grid inside each card */ +.batch-eval-grid .form-row { + flex-wrap: wrap; +} + +@media (max-width: 1100px) { + .batch-eval-grid { + grid-template-columns: 1fr; + } +} diff --git a/app/templates/pages/players_to_evaluate.html b/app/templates/pages/players_to_evaluate.html index 6f4bf3e..06c75d2 100644 --- a/app/templates/pages/players_to_evaluate.html +++ b/app/templates/pages/players_to_evaluate.html @@ -6,54 +6,76 @@ {% block content %}
-

Players in {{ tryout.title }}

+

Select players to evaluate in {{ tryout.title }}

-
- - - - - - - - - - - - {% for entry in players %} - - + + + + + + {% else %} + + + + {% endfor %} + +
PlayerContactAttendanceStatusActions
-
+

Choose the players you want to evaluate, then load all of them on a single page.

+
+
+ + + + + + + + + + + + + {% for entry in players %} + + + - - - - - - {% else %} - - - - {% endfor %} - -
PlayerContactAttendanceStatusActions
+ + +
{{ entry.player.username[:2] | upper }}
{{ entry.player.username }} -
-
{{ entry.player.email }} - {{ entry.registration.status }} - - {% if entry.evaluated %} - Evaluated - {% else %} - Not Evaluated - {% endif %} - - - {% if entry.evaluated %}View/Edit{% else %}Evaluate{% endif %} - -
No players registered for this tryout.
-
+
+
{{ entry.player.email }} + {{ entry.registration.status }} + + {% if entry.evaluated %} + Evaluated + {% else %} + Not Evaluated + {% endif %} + + + {% if entry.evaluated %}View/Edit{% else %}Single{% endif %} + +
No players registered for this tryout.
+
+
+ Cancel + +
+
+ + {% endblock %} \ No newline at end of file