Author SHA1 Message Date
cedrick2711 cb59f84698 essai look lockin 2026-08-13 15:53:29 -04:00
9 changed files with 203 additions and 265 deletions
-1
View File
@@ -20,7 +20,6 @@ from app.models._constants import (
GAME_PLATFORMS,
PLATFORM_CODES,
TRN_URLS,
EVALUATION_CRITERIA,
)
# =========================================================================
-15
View File
@@ -9,21 +9,6 @@ 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',
+2 -108
View File
@@ -9,7 +9,7 @@ from app.extensions import db
from app.models import (
Admin, Coach, Manager, Player,
User, Tryout, Evaluation, TryoutRegistration,
OrgTeam, GAME_POSITIONS, EVALUATION_CRITERIA,
OrgTeam, GAME_POSITIONS,
)
from sqlalchemy import func
from sqlalchemy.orm import aliased
@@ -30,21 +30,6 @@ 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():
@@ -245,95 +230,4 @@ 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)
@evaluations_bp.route('/<int:tryout_id>/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)
return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players)
+17 -22
View File
@@ -407,18 +407,9 @@ def add_disponibility():
@users_bp.route('/disponibilities/add_bulk', methods=['POST'])
@login_required
def add_disponibilities_bulk():
"""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.
"""
"""Add multiple disponibility blocks at once."""
data = request.get_json()
slots = data.get('slots', []) if data else []
# Clear existing disponibilities for the current player.
PlayerDisponibility.query.filter_by(player_id=current_user.id).delete()
slots = data.get('slots', [])
created = []
for slot in slots:
day_of_week = slot.get('day_of_week')
@@ -431,17 +422,21 @@ def add_disponibilities_bulk():
continue
end_time = add_30_minutes(start_time)
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'),
})
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'),
})
db.session.commit()
return jsonify({'success': True, 'created': created})
+94 -49
View File
@@ -1,8 +1,8 @@
:root {
/* New Color Palette */
--primary: #00984C;
--primary-dark: #003E21;
--primary-light: #E5A939;
/* New Color Palette (cozy / e-sporty) */
--primary: #6C4CFF;
--primary-dark: #5530D9;
--primary-light: #8B6CFF;
--success: #00984C;
--success-light: #F0F2F2;
--warning: #E5A939;
@@ -13,7 +13,7 @@
--info-light: #eff6ff;
--secondary: #6b7280;
--secondary-light: #F0F2F2;
--dark: #12130F;
--dark: #0D0D12;
--gray-50: #F0F2F2;
--gray-100: #E8EAEB;
--gray-200: #D1D5DB;
@@ -22,8 +22,8 @@
--gray-500: #4B5563;
--gray-600: #374151;
--gray-700: #1F2937;
--gray-800: #12130F;
--gray-900: #0A0B0A;
--gray-800: #16161D;
--gray-900: #0D0D12;
--sidebar-width: 260px;
--sidebar-collapsed: 0px;
--radius: 12px;
@@ -34,21 +34,66 @@
--transition: all 0.2s ease;
}
/* Local-first Sora font-face (local installed fonts preferred, then /static/fonts fallback). */
/* To use local files, place these filenames in app/static/fonts: Sora-300.woff2, Sora-400.woff2, Sora-500.woff2, Sora-600.woff2, Sora-700.woff2, Sora-800.woff2 */
@font-face {
font-family: 'Sora';
src: local('Sora'), local('Sora-Regular'), url('/static/fonts/Sora-400.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Sora';
src: local('Sora Medium'), local('Sora-Medium'), url('/static/fonts/Sora-500.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Sora';
src: local('Sora SemiBold'), local('Sora-SemiBold'), url('/static/fonts/Sora-600.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Sora';
src: local('Sora Bold'), local('Sora-Bold'), url('/static/fonts/Sora-700.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Sora';
src: local('Sora ExtraBold'), local('Sora-ExtraBold'), url('/static/fonts/Sora-800.woff2') format('woff2');
font-weight: 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Sora';
src: local('Sora Light'), local('Sora-Light'), url('/static/fonts/Sora-300.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
/* Dark Mode Variables */
[data-theme="dark"] {
--bg-primary: #12130F;
--bg-secondary: #1A1D17;
--bg-tertiary: #232820;
--text-primary: #F0F2F2;
--bg-primary: #0D0D12;
--bg-secondary: #16161D;
--bg-tertiary: #23232E;
--text-primary: #FFFFFF;
--text-secondary: #D1D5DB;
--text-muted: #9CA3AF;
--border-color: #2D342A;
--card-bg: #1A1D17;
--sidebar-bg: #0A0B0A;
--border-color: #2D2A3A;
--card-bg: #16161D;
--sidebar-bg: #0D0D12;
--sidebar-text: #F0F2F2;
--sidebar-hover: rgba(240, 242, 242, 0.08);
--input-bg: #232820;
--input-border: #2D342A;
--sidebar-hover: rgba(108, 76, 255, 0.08);
--input-bg: #23232E;
--input-border: #2D2A3A;
--shadow: 0 1px 3px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2);
--shadow-md: 0 4px 6px rgba(0,0,0,0.25), 0 2px 4px rgba(0,0,0,0.2);
--shadow-lg: 0 10px 15px rgba(0,0,0,0.3), 0 4px 6px rgba(0,0,0,0.2);
@@ -57,13 +102,40 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-family: 'Sora', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--gray-50);
color: var(--gray-800);
line-height: 1.6;
min-height: 100vh;
}
/* Headings use Sora Bold for stronger, e-sporty look */
h1, h2, h3, h4, h5, h6, .page-header h1, .card-header h3 {
font-family: 'Sora', sans-serif;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--text-primary, var(--gray-900));
}
/* Tweak letter-spacing when local Sora is available vs fallback */
html.font-sora-available h1, html.font-sora-available h2, html.font-sora-available h3,
html.font-sora-available h4, html.font-sora-available h5, html.font-sora-available h6,
html.font-sora-available .page-header h1, html.font-sora-available .card-header h3 {
letter-spacing: -0.02em; /* tighter when true Sora is present */
}
html.font-sora-fallback h1, html.font-sora-fallback h2, html.font-sora-fallback h3,
html.font-sora-fallback h4, html.font-sora-fallback h5, html.font-sora-fallback h6,
html.font-sora-fallback .page-header h1, html.font-sora-fallback .card-header h3 {
letter-spacing: -0.01em; /* keep the default for fallback fonts */
}
/* Logo text use extra-bold feel */
.logo span, .auth-header h2 {
font-family: 'Sora', sans-serif;
font-weight: 800;
letter-spacing: 0.02em;
}
a { color: var(--primary); text-decoration: none; }
a:hover { color: var(--primary-dark); }
@@ -502,7 +574,7 @@ a:hover { color: var(--primary-dark); }
.form-group textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 152, 76, 0.1);
box-shadow: 0 0 0 3px rgba(108, 76, 255, 0.12);
}
.form-group textarea { resize: vertical; min-height: 80px; }
@@ -573,12 +645,12 @@ a:hover { color: var(--primary-dark); }
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #00984C 0%, #003E21 100%);
background: linear-gradient(135deg, #6C4CFF 0%, #16161D 100%);
padding: 20px;
}
[data-theme="dark"] .auth-wrapper {
background: linear-gradient(135deg, #003E21 0%, #12130F 100%);
background: linear-gradient(135deg, #5530D9 0%, #0D0D12 100%);
}
.auth-wrapper .flash-messages {
@@ -656,7 +728,7 @@ a:hover { color: var(--primary-dark); }
.auth-form .form-group input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 152, 76, 0.1);
box-shadow: 0 0 0 3px rgba(108, 76, 255, 0.12);
outline: none;
}
@@ -1955,30 +2027,3 @@ 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;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

+41 -2
View File
@@ -5,7 +5,46 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}UdeS team manager{% endblock %}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script>
// Font detection: prefer local Sora, add html.font-sora-available or html.font-sora-fallback
(function(){
var fontName = 'Sora';
function mark(available){
try { document.documentElement.classList.add(available ? 'font-sora-available' : 'font-sora-fallback'); } catch(e){}
}
if (document.fonts && document.fonts.check) {
// Quick check for regular weight first
try {
if (document.fonts.check('1em "' + fontName + '"')) return mark(true);
} catch(e){}
// Wait briefly for the font to load (up to 1500ms)
var settled = false;
var timeout = setTimeout(function(){ if (!settled) { settled = true; mark(false); } }, 1500);
document.fonts.load('1em "' + fontName + '"').then(function(loaded){
if (settled) return;
settled = true;
clearTimeout(timeout);
// document.fonts.load resolves when font is available; double-check with check()
var ok = document.fonts.check('1em "' + fontName + '"');
mark(!!ok);
}).catch(function(){ if (!settled){ settled = true; clearTimeout(timeout); mark(false); } });
} else {
// Fallback: inject hidden element and compare computed family
var span = document.createElement('span');
span.style.fontFamily = fontName + ', monospace';
span.style.position = 'absolute';
span.style.left = '-9999px';
span.style.visibility = 'hidden';
span.textContent = 'Axm4';
document.head.appendChild(span);
var computed = window.getComputedStyle(span).fontFamily || '';
document.head.removeChild(span);
mark(computed.toLowerCase().indexOf(fontName.toLowerCase()) !== -1);
}
})();
</script>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏆</text></svg>">
</head>
<body>
@@ -13,7 +52,7 @@
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="logo">
<img src="{{ url_for('static', filename='images/UdeS_logo.png') }}" alt="UdeS Logo" class="logo-img">
<img src="{{ url_for('static', filename='images/Lockin_logo.png') }}" alt="Lockin Logo" class="logo-img">
<span>UdeS team manager</span>
</div>
<div class="user-badge">
@@ -160,7 +199,7 @@
</div>
<div class="auth-container">
<div class="auth-header">
<img src="{{ url_for('static', filename='images/UdeS_logo.png') }}" alt="UdeS Logo" class="auth-logo-img">
<img src="{{ url_for('static', filename='images/Lockin_logo.png') }}" alt="Lockin Logo" class="auth-logo-img">
<h2>UdeS team manager</h2>
<p>UdeS team manager</p>
</div>
+44 -66
View File
@@ -6,76 +6,54 @@
{% block content %}
<div class="card">
<div class="card-header">
<h3><i class="fas fa-users"></i> Select players to evaluate in {{ tryout.title }}</h3>
<h3><i class="fas fa-users"></i> Players in {{ tryout.title }}</h3>
</div>
<div class="card-body">
<p class="text-muted mb-3">Choose the players you want to evaluate, then load all of them on a single page.</p>
<form method="GET" action="{{ url_for('evaluations.batch_evaluate', tryout_id=tryout.id) }}">
<div class="table-container">
<table class="table">
<thead>
<tr>
<th><input type="checkbox" id="select-all" onclick="toggleAll(this)"></th>
<th>Player</th>
<th>Contact</th>
<th>Attendance</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for entry in players %}
<tr>
<td>
<input type="checkbox" name="player_ids" value="{{ entry.player.id }}" class="player-check">
</td>
<td>
<div class="user-mini">
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>Player</th>
<th>Contact</th>
<th>Attendance</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for entry in players %}
<tr>
<td>
<div class="user-mini">
<div class="avatar-sm">{{ entry.player.username[:2] | upper }}</div>
<span>{{ entry.player.username }}</span>
</div>
</td>
<td>{{ entry.player.email }}</td>
<td>
<span class="badge badge-{{ entry.registration.status }}">{{ entry.registration.status }}</span>
</td>
<td>
{% if entry.evaluated %}
<span class="badge badge-success">Evaluated</span>
{% else %}
<span class="badge badge-warning">Not Evaluated</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('evaluations.evaluate_player', tryout_id=tryout.id, player_id=entry.player.id) }}" class="btn btn-sm btn-outline">
<i class="fas fa-clipboard"></i> {% if entry.evaluated %}View/Edit{% else %}Single{% endif %}
</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="text-center">No players registered for this tryout.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="form-actions">
<a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-clipboard-check"></i> Evaluate Selected
</button>
</div>
</form>
</div>
</td>
<td>{{ entry.player.email }}</td>
<td>
<span class="badge badge-{{ entry.registration.status }}">{{ entry.registration.status }}</span>
</td>
<td>
{% if entry.evaluated %}
<span class="badge badge-success">Evaluated</span>
{% else %}
<span class="badge badge-warning">Not Evaluated</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('evaluations.evaluate_player', tryout_id=tryout.id, player_id=entry.player.id) }}" class="btn btn-sm btn-primary">
<i class="fas fa-clipboard"></i> {% if entry.evaluated %}View/Edit{% else %}Evaluate{% endif %}
</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center">No players registered for this tryout.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
function toggleAll(master) {
var boxes = document.querySelectorAll('.player-check');
for (var i = 0; i < boxes.length; i++) {
boxes[i].checked = master.checked;
}
}
</script>
{% endblock %}
+5 -2
View File
@@ -193,6 +193,9 @@
<p class="text-muted">Loading...</p>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="saveDisponibilities()">
<i class="fas fa-save"></i> Save Disponibilities
</button>
<button type="button" class="btn btn-secondary" onclick="clearDisponibilities()">
<i class="fas fa-trash"></i> Clear All
</button>
@@ -366,7 +369,7 @@ function saveCoachAvailability() {
const msg = document.createElement('div');
msg.className = 'alert alert-success';
msg.style.marginTop = '10px';
msg.innerHTML = '<span><i class="fas fa-check"></i> Availability saved!</span>';
msg.innerHTML = '<i class="fas fa-check"></i> Availability saved!';
document.getElementById('availability-grid').appendChild(msg);
setTimeout(() => msg.remove(), 3000);
}
@@ -568,7 +571,7 @@ function saveDisponibilities() {
var msg = document.createElement('div');
msg.className = 'alert alert-success';
msg.style.marginTop = '10px';
msg.innerHTML = '<span><i class="fas fa-check"></i> Disponibilities saved successfully!</span>';
msg.innerHTML = '<i class="fas fa-check"></i> Disponibilities saved successfully!';
document.getElementById('disponibilities-grid').appendChild(msg);
setTimeout(function() { msg.remove(); }, 3000);
}