first commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,54 @@
|
||||
import os
|
||||
from flask import Flask
|
||||
from extensions import db, login_manager, csrf, hash_password, check_password
|
||||
from sqlalchemy import text
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'team-tryouts-secret-key-change-in-production')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///team_tryouts.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['WTF_CSRF_ENABLED'] = True
|
||||
|
||||
db.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
csrf.init_app(app)
|
||||
|
||||
from routes.auth import auth_bp
|
||||
from routes.tryouts import tryouts_bp
|
||||
from routes.evaluations import evaluations_bp
|
||||
from routes.users import users_bp
|
||||
from routes.main import main_bp
|
||||
from routes.teams import teams_bp
|
||||
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(tryouts_bp)
|
||||
app.register_blueprint(evaluations_bp)
|
||||
app.register_blueprint(users_bp)
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(teams_bp)
|
||||
|
||||
with app.app_context():
|
||||
import models
|
||||
from models import User
|
||||
try:
|
||||
# Check if the database schema is up to date by testing a query
|
||||
# that uses all model columns
|
||||
db.session.execute(text('SELECT games FROM users LIMIT 1'))
|
||||
db.create_all()
|
||||
except Exception:
|
||||
# If there's a schema mismatch, drop and recreate all tables
|
||||
db.session.rollback()
|
||||
db.drop_all()
|
||||
db.create_all()
|
||||
|
||||
# Seed database if empty
|
||||
if User.query.count() == 0:
|
||||
from seed import seed_database
|
||||
seed_database()
|
||||
|
||||
return app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
@@ -0,0 +1,147 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}My Profile - TryoutPro{% endblock %}
|
||||
{% block page_title %}My Profile{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / Profile</span>{% endblock %}
|
||||
|
||||
{% block header_actions %}
|
||||
<div class="header-actions">
|
||||
<a href="{{ url_for('users.edit_profile') }}" class="btn btn-sm btn-primary">
|
||||
<i class="fas fa-edit"></i> Edit Profile
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="dashboard-grid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-user-circle"></i> Account Information</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="profile-header">
|
||||
<div class="user-avatar avatar-xl">{{ user.full_name[:2] | upper }}</div>
|
||||
<div class="profile-info">
|
||||
<h2>{{ user.full_name }}</h2>
|
||||
<span class="badge badge-{{ user.role }} badge-lg">{{ user.role | capitalize }}</span>
|
||||
<p class="text-muted">Member since {% if user.created_at %}{{ user.created_at.strftime('%B %Y') }}{% else %}Unknown{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-grid mt-4">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Username</span>
|
||||
<span class="detail-value">{{ user.username }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Email</span>
|
||||
<span class="detail-value">{{ user.email }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Phone</span>
|
||||
<span class="detail-value">{{ user.phone or 'Not provided' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Account Status</span>
|
||||
<span class="detail-value">
|
||||
{% if user.is_active_account %}
|
||||
<span class="badge badge-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger">Inactive</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- E-Sports Profile Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-gamepad"></i> E-Sports Profile</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><i class="fas fa-headset"></i> Games</span>
|
||||
<span class="detail-value">
|
||||
{% set games_list = user.get_games_list() %}
|
||||
{% if games_list %}
|
||||
{% for game in games_list %}
|
||||
<span class="badge badge-esport">{{ game }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-muted">Not specified</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><i class="fas fa-chart-line"></i> TRN (Tracker Network)</span>
|
||||
<span class="detail-value">
|
||||
{% if user.trn_username %}
|
||||
<a href="https://tracker.gg" target="_blank" rel="noopener noreferrer" class="trn-link">
|
||||
<i class="fas fa-external-link-alt"></i> {{ user.trn_username }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Not connected</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><i class="fab fa-discord"></i> Discord</span>
|
||||
<span class="detail-value">
|
||||
{% if user.discord_username %}
|
||||
{{ user.discord_username }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not connected</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><i class="fas fa-link"></i> League OS</span>
|
||||
<span class="detail-value">
|
||||
{% if user.league_os_profile %}
|
||||
<a href="{{ user.league_os_profile }}" target="_blank" rel="noopener noreferrer" class="trn-link">
|
||||
<i class="fas fa-external-link-alt"></i> {{ user.league_os_profile }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Not connected</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-chart-bar"></i> My Statistics</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if user.role == 'player' %}
|
||||
<div class="profile-stats">
|
||||
<div class="stat-item">
|
||||
<h4>{{ user.tryout_registrations.count() }}</h4>
|
||||
<p>Tryouts Registered</p>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<h4>{{ user.evaluations_received.count() }}</h4>
|
||||
<p>Evaluations Received</p>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<h4>{{ user.team_assignments.count() }}</h4>
|
||||
<p>Team Assignments</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif user.can_evaluate() %}
|
||||
<div class="profile-stats">
|
||||
<div class="stat-item">
|
||||
<h4>{{ user.evaluations_given.count() }}</h4>
|
||||
<p>Evaluations Given</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">No statistics available for this role.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,16 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
db = SQLAlchemy()
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = 'auth.login'
|
||||
login_manager.login_message_category = 'info'
|
||||
csrf = CSRFProtect()
|
||||
|
||||
def hash_password(password):
|
||||
return generate_password_hash(password)
|
||||
|
||||
def check_password(password_hash, password):
|
||||
return check_password_hash(password_hash, password)
|
||||
Binary file not shown.
@@ -0,0 +1,161 @@
|
||||
from extensions import db, login_manager
|
||||
from flask_login import UserMixin
|
||||
from datetime import datetime
|
||||
|
||||
ROLES = ['president', 'manager', 'coach', 'player', 'scout']
|
||||
|
||||
# Popular E-Sports games list
|
||||
ESPORT_GAMES = [
|
||||
'Valorant',
|
||||
'League of Legends',
|
||||
'Counter-Strike 2',
|
||||
'Apex Legends',
|
||||
'Overwatch 2',
|
||||
'Rainbow Six Siege',
|
||||
'Fortnite',
|
||||
'Rocket League',
|
||||
'Call of Duty',
|
||||
'Dota 2',
|
||||
'Super Smash Bros.',
|
||||
'Street Fighter 6',
|
||||
]
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
__tablename__ = 'users'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(128), nullable=False)
|
||||
role = db.Column(db.String(20), nullable=False, default='player')
|
||||
full_name = db.Column(db.String(100), nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
phone = db.Column(db.String(20), nullable=True)
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
|
||||
is_active_account = db.Column(db.Boolean, default=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
# E-Sports specific fields
|
||||
games = db.Column(db.Text, nullable=True) # Comma-separated list of games
|
||||
trn_username = db.Column(db.String(120), nullable=True) # Tracker Network username
|
||||
discord_username = db.Column(db.String(100), nullable=True) # Discord handle
|
||||
league_os_profile = db.Column(db.String(255), nullable=True) # League OS profile URL or ID
|
||||
|
||||
evaluations_given = db.relationship('Evaluation', foreign_keys='Evaluation.evaluator_id', backref='evaluator', lazy='dynamic')
|
||||
evaluations_received = db.relationship('Evaluation', foreign_keys='Evaluation.player_id', backref='player', lazy='dynamic')
|
||||
tryout_registrations = db.relationship('TryoutRegistration', backref='player', lazy='dynamic')
|
||||
team_assignments = db.relationship('TeamMember', foreign_keys='TeamMember.player_id', backref='player_ref', lazy='dynamic')
|
||||
|
||||
def get_games_list(self):
|
||||
"""Return games as a list."""
|
||||
if self.games:
|
||||
return [g.strip() for g in self.games.split(',') if g.strip()]
|
||||
return []
|
||||
|
||||
def has_role(self, *roles):
|
||||
return self.role in roles
|
||||
|
||||
def can_evaluate(self):
|
||||
return self.role in ['president', 'manager', 'coach']
|
||||
|
||||
def can_manage_users(self):
|
||||
return self.role == 'president'
|
||||
|
||||
def can_manage_tryouts(self):
|
||||
return self.role in ['president', 'manager']
|
||||
|
||||
def can_manage_teams(self):
|
||||
return self.role in ['president', 'manager']
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
|
||||
class OrgTeam(db.Model):
|
||||
"""Persistent organization teams (e.g., Varsity, JV) that exist across tryouts."""
|
||||
__tablename__ = 'org_teams'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False, unique=True)
|
||||
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
coach = db.relationship('User', foreign_keys=[coach_id], backref='coached_org_team', uselist=False)
|
||||
creator = db.relationship('User', foreign_keys=[created_by])
|
||||
players = db.relationship('User', foreign_keys='User.team_id', backref='org_team', lazy='dynamic')
|
||||
|
||||
|
||||
class Tryout(db.Model):
|
||||
__tablename__ = 'tryouts'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
date = db.Column(db.Date, nullable=False)
|
||||
location = db.Column(db.String(200), nullable=True)
|
||||
status = db.Column(db.String(20), default='upcoming') # upcoming, in_progress, completed
|
||||
max_players = db.Column(db.Integer, nullable=True)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
target_org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
creator = db.relationship('User', backref='created_tryouts')
|
||||
registrations = db.relationship('TryoutRegistration', backref='tryout', lazy='dynamic')
|
||||
evaluations = db.relationship('Evaluation', backref='tryout', lazy='dynamic')
|
||||
teams = db.relationship('Team', backref='tryout', lazy='dynamic')
|
||||
target_org_team = db.relationship('OrgTeam', backref='tryouts', foreign_keys=[target_org_team_id])
|
||||
|
||||
|
||||
class TryoutRegistration(db.Model):
|
||||
__tablename__ = 'tryout_registrations'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
registered_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
status = db.Column(db.String(20), default='registered') # registered, attended, no_show
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
|
||||
|
||||
class Evaluation(db.Model):
|
||||
__tablename__ = 'evaluations'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
evaluator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
speed_score = db.Column(db.Integer, nullable=True)
|
||||
agility_score = db.Column(db.Integer, nullable=True)
|
||||
technique_score = db.Column(db.Integer, nullable=True)
|
||||
teamwork_score = db.Column(db.Integer, nullable=True)
|
||||
attitude_score = db.Column(db.Integer, nullable=True)
|
||||
overall_score = db.Column(db.Float, nullable=True)
|
||||
comments = db.Column(db.Text, nullable=True)
|
||||
position_recommendation = db.Column(db.String(50), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('tryout_id', 'player_id', 'evaluator_id', name='unique_evaluation'),
|
||||
)
|
||||
|
||||
|
||||
class Team(db.Model):
|
||||
"""Tryout-specific teams (e.g., Alpha, Bravo within a single tryout)."""
|
||||
__tablename__ = 'teams'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
creator = db.relationship('User', backref='created_teams')
|
||||
members = db.relationship('TeamMember', backref='team', lazy='dynamic')
|
||||
|
||||
|
||||
class TeamMember(db.Model):
|
||||
__tablename__ = 'team_members'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
position = db.Column(db.String(50), nullable=True)
|
||||
added_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
player = db.relationship('User', overlaps="player_ref,team_assignments") # Many-to-one, no dynamic loader
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,88 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from extensions import db, hash_password, check_password
|
||||
from models import User, ESPORT_GAMES
|
||||
|
||||
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||
|
||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
if user and check_password(user.password_hash, password):
|
||||
if not user.is_active_account:
|
||||
flash('This account has been deactivated.', 'danger')
|
||||
return render_template('pages/login.html')
|
||||
login_user(user)
|
||||
next_page = request.args.get('next')
|
||||
flash(f'Welcome back, {user.full_name}!', 'success')
|
||||
return redirect(next_page) if next_page else redirect(url_for('main.dashboard'))
|
||||
else:
|
||||
flash('Login unsuccessful. Please check username and password.', 'danger')
|
||||
|
||||
return render_template('pages/login.html')
|
||||
|
||||
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
email = request.form.get('email')
|
||||
password = request.form.get('password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
full_name = request.form.get('full_name')
|
||||
phone = request.form.get('phone')
|
||||
|
||||
# E-Sports fields
|
||||
selected_games = request.form.getlist('games')
|
||||
trn_username = request.form.get('trn_username', '').strip()
|
||||
discord_username = request.form.get('discord_username', '').strip()
|
||||
league_os_profile = request.form.get('league_os_profile', '').strip()
|
||||
|
||||
if password != confirm_password:
|
||||
flash('Passwords do not match.', 'danger')
|
||||
return render_template('pages/register.html', esport_games=ESPORT_GAMES)
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash('Username already exists.', 'danger')
|
||||
return render_template('pages/register.html', esport_games=ESPORT_GAMES)
|
||||
|
||||
if User.query.filter_by(email=email).first():
|
||||
flash('Email already registered.', 'danger')
|
||||
return render_template('pages/register.html', esport_games=ESPORT_GAMES)
|
||||
|
||||
hashed_password = hash_password(password)
|
||||
user = User(
|
||||
username=username,
|
||||
password_hash=hashed_password,
|
||||
role='player',
|
||||
full_name=full_name,
|
||||
email=email,
|
||||
phone=phone,
|
||||
games=','.join(selected_games) if selected_games else None,
|
||||
trn_username=trn_username or None,
|
||||
discord_username=discord_username or None,
|
||||
league_os_profile=league_os_profile or None
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
flash('Your account has been created! You can now log in.', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
return render_template('pages/register.html', esport_games=ESPORT_GAMES)
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
flash('You have been logged out.', 'info')
|
||||
return redirect(url_for('auth.login'))
|
||||
@@ -0,0 +1,135 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
from extensions import db
|
||||
from models import User, Tryout, Evaluation, TryoutRegistration
|
||||
from sqlalchemy import func
|
||||
|
||||
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
|
||||
|
||||
@evaluations_bp.route('')
|
||||
@login_required
|
||||
def list_evaluations():
|
||||
user = current_user
|
||||
|
||||
if user.role == 'president':
|
||||
evaluations = Evaluation.query.order_by(Evaluation.created_at.desc()).all()
|
||||
avg_scores = db.session.query(
|
||||
Evaluation.player_id,
|
||||
func.count(Evaluation.id).label('eval_count'),
|
||||
func.avg(Evaluation.overall_score).label('avg_score')
|
||||
).group_by(Evaluation.player_id).all()
|
||||
player_scores = {}
|
||||
for row in avg_scores:
|
||||
p = User.query.get(row.player_id)
|
||||
if p:
|
||||
player_scores[p.id] = {'player': p, 'count': row.eval_count, 'avg': round(row.avg_score, 1) if row.avg_score else 0}
|
||||
|
||||
elif user.can_evaluate():
|
||||
evaluations = Evaluation.query.filter_by(evaluator_id=user.id).order_by(Evaluation.created_at.desc()).all()
|
||||
player_scores = {}
|
||||
else:
|
||||
evaluations = Evaluation.query.filter_by(player_id=user.id).order_by(Evaluation.created_at.desc()).all()
|
||||
player_scores = {}
|
||||
|
||||
return render_template('pages/evaluations.html', evaluations=evaluations, player_scores=player_scores)
|
||||
|
||||
@evaluations_bp.route('/<int:tryout_id>/<int:player_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def evaluate_player(tryout_id, player_id):
|
||||
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)
|
||||
player = User.query.get_or_404(player_id)
|
||||
|
||||
if player.role != 'player':
|
||||
flash('Can only evaluate players.', 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
existing_eval = Evaluation.query.filter_by(
|
||||
tryout_id=tryout_id,
|
||||
player_id=player_id,
|
||||
evaluator_id=current_user.id
|
||||
).first()
|
||||
|
||||
if request.method == 'POST':
|
||||
speed = request.form.get('speed_score')
|
||||
agility = request.form.get('agility_score')
|
||||
technique = request.form.get('technique_score')
|
||||
teamwork = request.form.get('teamwork_score')
|
||||
attitude = request.form.get('attitude_score')
|
||||
comments = request.form.get('comments')
|
||||
position = request.form.get('position_recommendation')
|
||||
|
||||
scores = []
|
||||
if speed: scores.append(int(speed))
|
||||
if agility: scores.append(int(agility))
|
||||
if technique: scores.append(int(technique))
|
||||
if teamwork: scores.append(int(teamwork))
|
||||
if attitude: scores.append(int(attitude))
|
||||
|
||||
overall = sum(scores) / len(scores) if scores else None
|
||||
|
||||
if existing_eval:
|
||||
existing_eval.speed_score = int(speed) if speed else None
|
||||
existing_eval.agility_score = int(agility) if agility else None
|
||||
existing_eval.technique_score = int(technique) if technique else None
|
||||
existing_eval.teamwork_score = int(teamwork) if teamwork else None
|
||||
existing_eval.attitude_score = int(attitude) if attitude else None
|
||||
existing_eval.overall_score = overall
|
||||
existing_eval.comments = comments
|
||||
existing_eval.position_recommendation = position
|
||||
flash('Evaluation updated!', 'success')
|
||||
else:
|
||||
evaluation = Evaluation(
|
||||
tryout_id=tryout_id,
|
||||
player_id=player_id,
|
||||
evaluator_id=current_user.id,
|
||||
speed_score=int(speed) if speed else None,
|
||||
agility_score=int(agility) if agility else None,
|
||||
technique_score=int(technique) if technique else None,
|
||||
teamwork_score=int(teamwork) if teamwork else None,
|
||||
attitude_score=int(attitude) if attitude else None,
|
||||
overall_score=overall,
|
||||
comments=comments,
|
||||
position_recommendation=position
|
||||
)
|
||||
db.session.add(evaluation)
|
||||
flash('Evaluation submitted successfully!', 'success')
|
||||
|
||||
db.session.commit()
|
||||
return redirect(url_for('evaluations.list_evaluations'))
|
||||
|
||||
evaluators = None
|
||||
if current_user.role == 'president':
|
||||
all_evaluations = Evaluation.query.filter_by(tryout_id=tryout_id, player_id=player_id).all()
|
||||
evaluators = [{'evaluator': User.query.get(e.evaluator_id), 'eval': e} for e in all_evaluations]
|
||||
|
||||
return render_template('pages/evaluate_player.html',
|
||||
tryout=tryout,
|
||||
player=player,
|
||||
existing_eval=existing_eval,
|
||||
evaluators=evaluators)
|
||||
|
||||
@evaluations_bp.route('/<int:tryout_id>/players')
|
||||
@login_required
|
||||
def players_to_evaluate(tryout_id):
|
||||
if not current_user.can_evaluate():
|
||||
flash('Permission denied.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
|
||||
players = []
|
||||
for reg in registrations:
|
||||
p = User.query.get(reg.player_id)
|
||||
if p and p.role == 'player':
|
||||
existing = Evaluation.query.filter_by(
|
||||
tryout_id=tryout_id,
|
||||
player_id=p.id,
|
||||
evaluator_id=current_user.id
|
||||
).first()
|
||||
players.append({'player': p, 'evaluated': existing is not None, 'registration': reg})
|
||||
|
||||
return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players)
|
||||
@@ -0,0 +1,64 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash
|
||||
from flask_login import login_required, current_user
|
||||
from extensions import db
|
||||
from models import User, Tryout, Evaluation, TryoutRegistration, Team, TeamMember
|
||||
from sqlalchemy import func
|
||||
|
||||
main_bp = Blueprint('main', __name__)
|
||||
|
||||
@main_bp.route('/')
|
||||
def index():
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
@main_bp.route('/dashboard')
|
||||
@login_required
|
||||
def dashboard():
|
||||
user = current_user
|
||||
stats = {}
|
||||
|
||||
if user.role == 'president':
|
||||
stats['total_users'] = User.query.count()
|
||||
stats['total_players'] = User.query.filter_by(role='player').count()
|
||||
stats['total_tryouts'] = Tryout.query.count()
|
||||
stats['total_evaluations'] = Evaluation.query.count()
|
||||
stats['active_tryouts'] = Tryout.query.filter_by(status='in_progress').count()
|
||||
stats['upcoming_tryouts'] = Tryout.query.filter_by(status='upcoming').count()
|
||||
stats['recent_users'] = User.query.order_by(User.created_at.desc()).limit(5).all()
|
||||
stats['recent_tryouts'] = Tryout.query.order_by(Tryout.created_at.desc()).limit(5).all()
|
||||
|
||||
elif user.role == 'manager':
|
||||
stats['total_tryouts'] = Tryout.query.filter_by(created_by=user.id).count()
|
||||
stats['active_tryouts'] = Tryout.query.filter_by(created_by=user.id, status='in_progress').count()
|
||||
stats['total_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).count()
|
||||
stats['my_tryouts'] = Tryout.query.filter_by(created_by=user.id).order_by(Tryout.date.desc()).limit(5).all()
|
||||
|
||||
elif user.role == 'coach':
|
||||
stats['my_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).count()
|
||||
stats['pending_evaluations'] = 0
|
||||
registrations = TryoutRegistration.query.filter(TryoutRegistration.status.in_(['registered', 'attended'])).all()
|
||||
registered_player_ids = [r.player_id for r in registrations]
|
||||
evaluated_player_ids = [e.player_id for e in Evaluation.query.filter_by(evaluator_id=user.id).all()]
|
||||
stats['pending_evaluations'] = len(set(registered_player_ids) - set(evaluated_player_ids))
|
||||
stats['my_recent_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).order_by(Evaluation.created_at.desc()).limit(5).all()
|
||||
|
||||
elif user.role == 'player':
|
||||
stats['my_tryouts'] = TryoutRegistration.query.filter_by(player_id=user.id).count()
|
||||
stats['my_evaluations'] = Evaluation.query.filter_by(player_id=user.id).count()
|
||||
stats['average_score'] = db.session.query(func.avg(Evaluation.overall_score)).filter_by(player_id=user.id).scalar() or 0
|
||||
stats['my_registrations'] = TryoutRegistration.query.filter_by(player_id=user.id).order_by(TryoutRegistration.registered_at.desc()).limit(5).all()
|
||||
stats['my_eval_list'] = Evaluation.query.filter_by(player_id=user.id).order_by(Evaluation.created_at.desc()).limit(5).all()
|
||||
|
||||
elif user.role == 'scout':
|
||||
stats['total_players'] = User.query.filter_by(role='player').count()
|
||||
stats['total_evaluations'] = Evaluation.query.count()
|
||||
stats['avg_scores'] = db.session.query(
|
||||
Evaluation.player_id,
|
||||
func.avg(Evaluation.overall_score).label('avg_score')
|
||||
).group_by(Evaluation.player_id).order_by(func.avg(Evaluation.overall_score).desc()).limit(5).all()
|
||||
stats['top_players'] = []
|
||||
for row in stats['avg_scores']:
|
||||
p = User.query.get(row.player_id)
|
||||
if p:
|
||||
stats['top_players'].append((p, round(row.avg_score, 1)))
|
||||
|
||||
return render_template('pages/dashboard.html', user=user, stats=stats)
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_required, current_user
|
||||
from extensions import db
|
||||
from models import OrgTeam, User
|
||||
|
||||
teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
|
||||
|
||||
@teams_bp.route('')
|
||||
@login_required
|
||||
def list_teams():
|
||||
can_manage = current_user.can_manage_teams()
|
||||
|
||||
if current_user.role == 'coach':
|
||||
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
||||
teams = [org_team] if org_team else []
|
||||
elif current_user.role in ['player', 'scout'] or can_manage:
|
||||
# Players and scouts can view all teams; managers/presidents can manage
|
||||
teams = OrgTeam.query.order_by(OrgTeam.name).all()
|
||||
else:
|
||||
flash('You do not have permission to view teams.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
coaches = User.query.filter_by(role='coach').order_by(User.full_name).all()
|
||||
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
|
||||
return render_template('pages/teams.html', teams=teams, coaches=coaches, all_players=all_players, can_manage=can_manage)
|
||||
|
||||
@teams_bp.route('/create', methods=['POST'])
|
||||
@login_required
|
||||
def create_team():
|
||||
if not current_user.can_manage_teams():
|
||||
flash('You do not have permission to create teams.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
name = request.form.get('name')
|
||||
coach_id = request.form.get('coach_id')
|
||||
|
||||
if not name:
|
||||
flash('Team name is required.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
existing = OrgTeam.query.filter_by(name=name).first()
|
||||
if existing:
|
||||
flash(f'Team "{name}" already exists.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
team = OrgTeam(
|
||||
name=name,
|
||||
coach_id=int(coach_id) if coach_id else None,
|
||||
created_by=current_user.id
|
||||
)
|
||||
db.session.add(team)
|
||||
db.session.commit()
|
||||
flash(f'Team "{name}" created successfully!', 'success')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
@teams_bp.route('/<int:team_id>/edit', methods=['POST'])
|
||||
@login_required
|
||||
def edit_team(team_id):
|
||||
if not current_user.can_manage_teams():
|
||||
flash('You do not have permission to edit teams.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
team = OrgTeam.query.get_or_404(team_id)
|
||||
name = request.form.get('name')
|
||||
coach_id = request.form.get('coach_id')
|
||||
|
||||
if not name:
|
||||
flash('Team name is required.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
existing = OrgTeam.query.filter(OrgTeam.name == name, OrgTeam.id != team_id).first()
|
||||
if existing:
|
||||
flash(f'Team "{name}" already exists.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
team.name = name
|
||||
team.coach_id = int(coach_id) if coach_id else None
|
||||
db.session.commit()
|
||||
flash(f'Team "{name}" updated successfully!', 'success')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
@teams_bp.route('/<int:team_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_team(team_id):
|
||||
if not current_user.can_manage_teams():
|
||||
flash('You do not have permission to delete teams.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
team = OrgTeam.query.get_or_404(team_id)
|
||||
name = team.name
|
||||
|
||||
# Check if any tryouts are targeting this team
|
||||
from models import Tryout
|
||||
tryouts = Tryout.query.filter_by(target_org_team_id=team_id).all()
|
||||
if tryouts:
|
||||
for t in tryouts:
|
||||
t.target_org_team_id = None
|
||||
db.session.commit()
|
||||
|
||||
db.session.delete(team)
|
||||
db.session.commit()
|
||||
flash(f'Team "{name}" deleted successfully.', 'success')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
@teams_bp.route('/<int:team_id>/add_player', methods=['POST'])
|
||||
@login_required
|
||||
def add_player(team_id):
|
||||
if not current_user.can_manage_teams():
|
||||
flash('Permission denied.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
team = OrgTeam.query.get_or_404(team_id)
|
||||
player_id = request.form.get('player_id')
|
||||
position = request.form.get('position', '')
|
||||
|
||||
if not player_id:
|
||||
flash('Please select a player.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
player = User.query.get_or_404(int(player_id))
|
||||
if player.role != 'player':
|
||||
flash('Can only assign players to teams.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
# If player already belongs to a different team, remove them first
|
||||
if player.team_id and player.team_id != team_id:
|
||||
old_team = OrgTeam.query.get(player.team_id)
|
||||
flash(f'{player.full_name} was moved from {old_team.name} to {team.name}.', 'info')
|
||||
elif player.team_id == team_id:
|
||||
flash(f'{player.full_name} is already on {team.name}.', 'info')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
player.team_id = team_id
|
||||
db.session.commit()
|
||||
flash(f'{player.full_name} added to {team.name}!', 'success')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
@teams_bp.route('/<int:team_id>/remove_player/<int:player_id>', methods=['POST'])
|
||||
@login_required
|
||||
def remove_player(team_id, player_id):
|
||||
if not current_user.can_manage_teams():
|
||||
flash('Permission denied.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
team = OrgTeam.query.get_or_404(team_id)
|
||||
player = User.query.get_or_404(player_id)
|
||||
|
||||
if player.team_id != team_id:
|
||||
flash(f'{player.full_name} is not on {team.name}.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
player.team_id = None
|
||||
db.session.commit()
|
||||
flash(f'{player.full_name} removed from {team.name}.', 'success')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
@@ -0,0 +1,310 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_required, current_user
|
||||
from extensions import db
|
||||
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam
|
||||
from datetime import datetime
|
||||
|
||||
tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts')
|
||||
|
||||
def can_manage():
|
||||
return current_user.role in ['president', 'manager']
|
||||
|
||||
@tryouts_bp.route('')
|
||||
@login_required
|
||||
def list_tryouts():
|
||||
if current_user.role == 'president':
|
||||
tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
|
||||
elif current_user.role == 'manager':
|
||||
tryouts = Tryout.query.filter_by(created_by=current_user.id).order_by(Tryout.date.desc()).all()
|
||||
elif current_user.role == 'coach':
|
||||
# Coaches see tryouts targeting their org team
|
||||
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
||||
if org_team:
|
||||
tryouts = Tryout.query.filter_by(target_org_team_id=org_team.id).order_by(Tryout.date.desc()).all()
|
||||
else:
|
||||
tryouts = []
|
||||
elif current_user.role == 'player':
|
||||
tryouts = Tryout.query.filter(Tryout.status.in_(['upcoming', 'in_progress'])).order_by(Tryout.date.desc()).all()
|
||||
else:
|
||||
tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
|
||||
return render_template('pages/tryouts.html', tryouts=tryouts, now=datetime.utcnow())
|
||||
|
||||
@tryouts_bp.route('/create', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create_tryout():
|
||||
if not can_manage():
|
||||
flash('You do not have permission to create tryouts.', 'danger')
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
|
||||
org_teams = OrgTeam.query.order_by(OrgTeam.name).all()
|
||||
|
||||
if request.method == 'POST':
|
||||
title = request.form.get('title')
|
||||
description = request.form.get('description')
|
||||
date_str = request.form.get('date')
|
||||
location = request.form.get('location')
|
||||
max_players = request.form.get('max_players')
|
||||
target_org_team_id = request.form.get('target_org_team_id')
|
||||
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except (ValueError, TypeError):
|
||||
flash('Invalid date format.', 'danger')
|
||||
return render_template('pages/create_tryout.html', org_teams=org_teams)
|
||||
|
||||
tryout = Tryout(
|
||||
title=title,
|
||||
description=description,
|
||||
date=date_obj,
|
||||
location=location,
|
||||
max_players=int(max_players) if max_players else None,
|
||||
created_by=current_user.id,
|
||||
status='upcoming',
|
||||
target_org_team_id=int(target_org_team_id) if target_org_team_id else None
|
||||
)
|
||||
db.session.add(tryout)
|
||||
db.session.commit()
|
||||
flash('Tryout created successfully!', 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
||||
|
||||
return render_template('pages/create_tryout.html', org_teams=org_teams)
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_tryout(tryout_id):
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
|
||||
# Permission: president can edit any; manager can edit own; coach can edit if it targets their team
|
||||
if current_user.role == 'president':
|
||||
pass # allowed
|
||||
elif current_user.role == 'manager' and tryout.created_by == current_user.id:
|
||||
pass # allowed
|
||||
elif current_user.role == 'coach':
|
||||
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
||||
if not org_team or tryout.target_org_team_id != org_team.id:
|
||||
flash('You do not have permission to edit this tryout.', 'danger')
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
else:
|
||||
flash('You do not have permission to edit this tryout.', 'danger')
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
|
||||
org_teams = OrgTeam.query.order_by(OrgTeam.name).all()
|
||||
|
||||
if request.method == 'POST':
|
||||
title = request.form.get('title')
|
||||
description = request.form.get('description')
|
||||
date_str = request.form.get('date')
|
||||
location = request.form.get('location')
|
||||
max_players = request.form.get('max_players')
|
||||
target_org_team_id = request.form.get('target_org_team_id')
|
||||
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except (ValueError, TypeError):
|
||||
flash('Invalid date format.', 'danger')
|
||||
return render_template('pages/edit_tryout.html', tryout=tryout, org_teams=org_teams)
|
||||
|
||||
tryout.title = title
|
||||
tryout.description = description
|
||||
tryout.date = date_obj
|
||||
tryout.location = location
|
||||
tryout.max_players = int(max_players) if max_players else None
|
||||
tryout.target_org_team_id = int(target_org_team_id) if target_org_team_id else None
|
||||
db.session.commit()
|
||||
flash('Tryout updated successfully!', 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
||||
|
||||
return render_template('pages/edit_tryout.html', tryout=tryout, org_teams=org_teams)
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>')
|
||||
@login_required
|
||||
def view_tryout(tryout_id):
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
|
||||
registered_players = [User.query.get(r.player_id) for r in registrations if r.player_id]
|
||||
evaluations = Evaluation.query.filter_by(tryout_id=tryout_id).all()
|
||||
|
||||
evaluator_ids = set(e.evaluator_id for e in evaluations)
|
||||
player_ids_with_eval = set(e.player_id for e in evaluations)
|
||||
|
||||
# Check if current user has evaluated each player
|
||||
player_eval_status = {}
|
||||
if current_user.can_evaluate():
|
||||
for p in registered_players:
|
||||
existing = Evaluation.query.filter_by(
|
||||
tryout_id=tryout_id,
|
||||
player_id=p.id,
|
||||
evaluator_id=current_user.id
|
||||
).first()
|
||||
player_eval_status[p.id] = existing is not None
|
||||
|
||||
is_registered = TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=current_user.id
|
||||
).first() is not None
|
||||
|
||||
teams = Team.query.filter_by(tryout_id=tryout_id).all()
|
||||
team_data = []
|
||||
for team in teams:
|
||||
members = TeamMember.query.filter_by(team_id=team.id).all()
|
||||
team_data.append({
|
||||
'team': team,
|
||||
'members': [{'player': User.query.get(m.player_id), 'position': m.position} for m in members]
|
||||
})
|
||||
|
||||
# Determine if current user can edit this tryout
|
||||
can_edit = False
|
||||
if current_user.role == 'president':
|
||||
can_edit = True
|
||||
elif current_user.role == 'manager' and tryout.created_by == current_user.id:
|
||||
can_edit = True
|
||||
elif current_user.role == 'coach':
|
||||
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
||||
if org_team and tryout.target_org_team_id == org_team.id:
|
||||
can_edit = True
|
||||
|
||||
# Get all players (for manager registration dropdown)
|
||||
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
|
||||
|
||||
return render_template('pages/view_tryout.html',
|
||||
tryout=tryout,
|
||||
registered_players=registered_players,
|
||||
evaluations=evaluations,
|
||||
player_eval_status=player_eval_status,
|
||||
is_registered=is_registered,
|
||||
registrations=registrations,
|
||||
team_data=team_data,
|
||||
can_edit=can_edit,
|
||||
all_players=all_players,
|
||||
now=datetime.utcnow())
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/register', methods=['POST'])
|
||||
@login_required
|
||||
def register_for_tryout(tryout_id):
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
if current_user.role != 'player':
|
||||
flash('Only players can register for tryouts.', 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
if tryout.status not in ['upcoming', 'in_progress']:
|
||||
flash('This tryout is not accepting registrations.', 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
existing = TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=current_user.id).first()
|
||||
if existing:
|
||||
flash('You are already registered for this tryout.', 'info')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
if tryout.max_players:
|
||||
count = TryoutRegistration.query.filter_by(tryout_id=tryout_id).count()
|
||||
if count >= tryout.max_players:
|
||||
flash('This tryout is full.', 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
registration = TryoutRegistration(tryout_id=tryout_id, player_id=current_user.id)
|
||||
db.session.add(registration)
|
||||
db.session.commit()
|
||||
flash('Successfully registered for tryout!', 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/status', methods=['POST'])
|
||||
@login_required
|
||||
def update_status(tryout_id):
|
||||
if not can_manage():
|
||||
flash('Permission denied.', 'danger')
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
new_status = request.form.get('status')
|
||||
if new_status in ['upcoming', 'in_progress', 'completed']:
|
||||
tryout.status = new_status
|
||||
db.session.commit()
|
||||
flash(f'Tryout status updated to {new_status}.', 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/registration/<int:player_id>/status', methods=['POST'])
|
||||
@login_required
|
||||
def update_registration_status(tryout_id, player_id):
|
||||
if not can_manage():
|
||||
flash('Permission denied.', 'danger')
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
|
||||
registration = TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=player_id).first_or_404()
|
||||
new_status = request.form.get('status')
|
||||
if new_status in ['registered', 'attended', 'no_show']:
|
||||
registration.status = new_status
|
||||
db.session.commit()
|
||||
flash('Registration status updated.', 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/register_player', methods=['POST'])
|
||||
@login_required
|
||||
def register_player(tryout_id):
|
||||
if not can_manage():
|
||||
flash('Permission denied.', 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
player_id = request.form.get('player_id')
|
||||
|
||||
if not player_id:
|
||||
flash('Please select a player.', 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
player = User.query.get_or_404(int(player_id))
|
||||
if player.role != 'player':
|
||||
flash('Can only register players.', 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
existing = TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=player.id).first()
|
||||
if existing:
|
||||
flash(f'{player.full_name} is already registered for this tryout.', 'info')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
if tryout.max_players:
|
||||
count = TryoutRegistration.query.filter_by(tryout_id=tryout_id).count()
|
||||
if count >= tryout.max_players:
|
||||
flash('This tryout is full.', 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
registration = TryoutRegistration(tryout_id=tryout_id, player_id=player.id)
|
||||
db.session.add(registration)
|
||||
db.session.commit()
|
||||
flash(f'{player.full_name} registered for tryout!', 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/team/create', methods=['POST'])
|
||||
@login_required
|
||||
def create_team(tryout_id):
|
||||
if not can_manage():
|
||||
flash('Permission denied.', 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
team_name = request.form.get('team_name')
|
||||
if team_name:
|
||||
team = Team(tryout_id=tryout_id, name=team_name, created_by=current_user.id)
|
||||
db.session.add(team)
|
||||
db.session.commit()
|
||||
flash(f'Team "{team_name}" created!', 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/team/<int:team_id>/add', methods=['POST'])
|
||||
@login_required
|
||||
def add_to_team(tryout_id, team_id):
|
||||
if not can_manage():
|
||||
flash('Permission denied.', 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
player_id = request.form.get('player_id')
|
||||
position = request.form.get('position', '')
|
||||
team = Team.query.get_or_404(team_id)
|
||||
|
||||
existing = TeamMember.query.filter_by(team_id=team_id, player_id=player_id).first()
|
||||
if existing:
|
||||
flash('Player is already on this team.', 'info')
|
||||
else:
|
||||
member = TeamMember(team_id=team_id, player_id=int(player_id), position=position)
|
||||
db.session.add(member)
|
||||
db.session.commit()
|
||||
flash('Player added to team!', 'success')
|
||||
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_required, current_user
|
||||
from extensions import db, hash_password
|
||||
from models import User, ROLES, ESPORT_GAMES
|
||||
|
||||
users_bp = Blueprint('users', __name__, url_prefix='/users')
|
||||
|
||||
@users_bp.route('')
|
||||
@login_required
|
||||
def list_users():
|
||||
if current_user.role != 'president':
|
||||
flash('Only the president can manage users.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
users = User.query.order_by(User.role, User.full_name).all()
|
||||
return render_template('pages/users.html', users=users, roles=ROLES)
|
||||
|
||||
@users_bp.route('/<int:user_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_user(user_id):
|
||||
if current_user.role != 'president':
|
||||
flash('Only the president can edit users.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
full_name = request.form.get('full_name')
|
||||
email = request.form.get('email')
|
||||
phone = request.form.get('phone')
|
||||
role = request.form.get('role')
|
||||
is_active = request.form.get('is_active_account') == 'on'
|
||||
|
||||
if role not in ROLES:
|
||||
flash('Invalid role selected.', 'danger')
|
||||
return render_template('pages/edit_user.html', user=user, roles=ROLES, esport_games=ESPORT_GAMES)
|
||||
|
||||
# E-Sports fields
|
||||
selected_games = request.form.getlist('games')
|
||||
trn_username = request.form.get('trn_username', '').strip()
|
||||
discord_username = request.form.get('discord_username', '').strip()
|
||||
league_os_profile = request.form.get('league_os_profile', '').strip()
|
||||
|
||||
user.full_name = full_name
|
||||
user.email = email
|
||||
user.phone = phone
|
||||
user.role = role
|
||||
user.is_active_account = is_active
|
||||
user.games = ','.join(selected_games) if selected_games else None
|
||||
user.trn_username = trn_username or None
|
||||
user.discord_username = discord_username or None
|
||||
user.league_os_profile = league_os_profile or None
|
||||
|
||||
password = request.form.get('password')
|
||||
if password:
|
||||
user.password_hash = hash_password(password)
|
||||
|
||||
db.session.commit()
|
||||
flash(f'User {user.full_name} updated successfully!', 'success')
|
||||
return redirect(url_for('users.list_users'))
|
||||
|
||||
return render_template('pages/edit_user.html', user=user, roles=ROLES, esport_games=ESPORT_GAMES)
|
||||
|
||||
@users_bp.route('/<int:user_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_user(user_id):
|
||||
if current_user.role != 'president':
|
||||
flash('Only the president can delete users.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
if current_user.id == user_id:
|
||||
flash('You cannot delete your own account.', 'danger')
|
||||
return redirect(url_for('users.list_users'))
|
||||
|
||||
user = User.query.get_or_404(user_id)
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash(f'User {user.full_name} has been removed.', 'success')
|
||||
return redirect(url_for('users.list_users'))
|
||||
|
||||
@users_bp.route('/create', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create_user():
|
||||
if current_user.role != 'president':
|
||||
flash('Only the president can create users.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
email = request.form.get('email')
|
||||
password = request.form.get('password')
|
||||
full_name = request.form.get('full_name')
|
||||
phone = request.form.get('phone')
|
||||
role = request.form.get('role')
|
||||
|
||||
if role not in ROLES:
|
||||
flash('Invalid role selected.', 'danger')
|
||||
return render_template('pages/create_user.html', roles=ROLES)
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash('Username already exists.', 'danger')
|
||||
return render_template('pages/create_user.html', roles=ROLES)
|
||||
|
||||
if User.query.filter_by(email=email).first():
|
||||
flash('Email already registered.', 'danger')
|
||||
return render_template('pages/create_user.html', roles=ROLES)
|
||||
|
||||
hashed_password = hash_password(password)
|
||||
user = User(
|
||||
username=username,
|
||||
password_hash=hashed_password,
|
||||
role=role,
|
||||
full_name=full_name,
|
||||
email=email,
|
||||
phone=phone
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash(f'User {full_name} created as {role}!', 'success')
|
||||
return redirect(url_for('users.list_users'))
|
||||
|
||||
return render_template('pages/create_user.html', roles=ROLES)
|
||||
|
||||
@users_bp.route('/profile')
|
||||
@login_required
|
||||
def profile():
|
||||
return render_template('pages/profile.html', user=current_user)
|
||||
|
||||
@users_bp.route('/profile/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_profile():
|
||||
if request.method == 'POST':
|
||||
full_name = request.form.get('full_name')
|
||||
email = request.form.get('email')
|
||||
phone = request.form.get('phone')
|
||||
|
||||
# E-Sports fields
|
||||
selected_games = request.form.getlist('games')
|
||||
trn_username = request.form.get('trn_username', '').strip()
|
||||
discord_username = request.form.get('discord_username', '').strip()
|
||||
league_os_profile = request.form.get('league_os_profile', '').strip()
|
||||
|
||||
# Check email uniqueness if changed
|
||||
if email != current_user.email and User.query.filter_by(email=email).first():
|
||||
flash('Email already in use.', 'danger')
|
||||
return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES)
|
||||
|
||||
current_user.full_name = full_name
|
||||
current_user.email = email
|
||||
current_user.phone = phone
|
||||
current_user.games = ','.join(selected_games) if selected_games else None
|
||||
current_user.trn_username = trn_username or None
|
||||
current_user.discord_username = discord_username or None
|
||||
current_user.league_os_profile = league_os_profile or None
|
||||
|
||||
password = request.form.get('password')
|
||||
if password:
|
||||
current_user.password_hash = hash_password(password)
|
||||
|
||||
db.session.commit()
|
||||
flash('Profile updated successfully!', 'success')
|
||||
return redirect(url_for('users.profile'))
|
||||
|
||||
return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES)
|
||||
@@ -0,0 +1,254 @@
|
||||
from extensions import db, hash_password
|
||||
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
from sqlalchemy import text
|
||||
|
||||
def seed_database():
|
||||
# Clear existing data
|
||||
db.session.execute(text('DELETE FROM team_members'))
|
||||
db.session.execute(text('DELETE FROM teams'))
|
||||
db.session.execute(text('DELETE FROM evaluations'))
|
||||
db.session.execute(text('DELETE FROM tryout_registrations'))
|
||||
db.session.execute(text('DELETE FROM tryouts'))
|
||||
db.session.execute(text('DELETE FROM org_teams'))
|
||||
db.session.execute(text('DELETE FROM users'))
|
||||
db.session.commit()
|
||||
|
||||
# Create users with different roles
|
||||
users_data = [
|
||||
{'username': 'president', 'password': 'password', 'role': 'president', 'full_name': 'Sarah Johnson', 'email': '[email protected]', 'phone': '555-0101'},
|
||||
{'username': 'manager1', 'password': 'password', 'role': 'manager', 'full_name': 'Mike Williams', 'email': '[email protected]', 'phone': '555-0102'},
|
||||
{'username': 'manager2', 'password': 'password', 'role': 'manager', 'full_name': 'Emily Davis', 'email': '[email protected]', 'phone': '555-0103'},
|
||||
{'username': 'coach1', 'password': 'password', 'role': 'coach', 'full_name': 'Coach Thompson', 'email': '[email protected]', 'phone': '555-0104'},
|
||||
{'username': 'coach2', 'password': 'password', 'role': 'coach', 'full_name': 'Coach Martinez', 'email': '[email protected]', 'phone': '555-0105'},
|
||||
{'username': 'coach3', 'password': 'password', 'role': 'coach', 'full_name': 'Coach Anderson', 'email': '[email protected]', 'phone': '555-0106'},
|
||||
{'username': 'scout1', 'password': 'password', 'role': 'scout', 'full_name': 'Alex Rivera', 'email': '[email protected]', 'phone': '555-0107'},
|
||||
]
|
||||
|
||||
# Create players with E-Sports profile data
|
||||
player_data = [
|
||||
{'username': 'jplayer1', 'full_name': 'James Wilson', 'email': '[email protected]', 'games': 'Valorant,Counter-Strike 2', 'trn': 'jameswilson_tr', 'discord': 'JamesW#7291', 'league_os': 'https://leagueos.gg/player/jameswilson'},
|
||||
{'username': 'jplayer2', 'full_name': 'Emma Garcia', 'email': '[email protected]', 'games': 'League of Legends,Valorant', 'trn': 'emmagarcia_gg', 'discord': 'EmmaG#4452', 'league_os': 'https://leagueos.gg/player/emmagarcia'},
|
||||
{'username': 'jplayer3', 'full_name': 'Liam Brown', 'email': '[email protected]', 'games': 'Apex Legends,Fortnite', 'trn': 'liambrown_fn', 'discord': 'LiamB#8103', 'league_os': 'https://leagueos.gg/player/liambrown'},
|
||||
{'username': 'jplayer4', 'full_name': 'Sophia Lee', 'email': '[email protected]', 'games': 'Overwatch 2,Valorant', 'trn': 'sophialee_ow', 'discord': 'SophiaL#3327', 'league_os': 'https://leagueos.gg/player/sophialee'},
|
||||
{'username': 'jplayer5', 'full_name': 'Noah Taylor', 'email': '[email protected]', 'games': 'Counter-Strike 2,Rainbow Six Siege', 'trn': 'noahtaylor_r6', 'discord': 'NoahT#6614', 'league_os': 'https://leagueos.gg/player/noahtaylor'},
|
||||
{'username': 'jplayer6', 'full_name': 'Olivia Martin', 'email': '[email protected]', 'games': 'Rocket League,Fortnite', 'trn': 'oliviamartin_rl', 'discord': 'OliviaM#2298', 'league_os': 'https://leagueos.gg/player/oliviamartin'},
|
||||
{'username': 'jplayer7', 'full_name': 'Ethan Clark', 'email': '[email protected]', 'games': 'Valorant,Apex Legends', 'trn': 'ethanclark_val', 'discord': 'EthanC#7743', 'league_os': 'https://leagueos.gg/player/ethanclark'},
|
||||
{'username': 'jplayer8', 'full_name': 'Ava White', 'email': '[email protected]', 'games': 'League of Legends,Counter-Strike 2', 'trn': 'avawhite_lol', 'discord': 'AvaW#5561', 'league_os': 'https://leagueos.gg/player/avawhite'},
|
||||
{'username': 'jplayer9', 'full_name': 'Mason Hall', 'email': '[email protected]', 'games': 'Call of Duty,Rocket League', 'trn': 'masonhall_cod', 'discord': 'MasonH#1189', 'league_os': 'https://leagueos.gg/player/masonhall'},
|
||||
{'username': 'jplayer10', 'full_name': 'Isabella Adams', 'email': '[email protected]', 'games': 'Overwatch 2,Dota 2', 'trn': 'isabellaadams_ow', 'discord': 'IsabellaA#4437', 'league_os': 'https://leagueos.gg/player/isabellaadams'},
|
||||
]
|
||||
|
||||
for i, data in enumerate(player_data, start=10):
|
||||
users_data.append({
|
||||
'username': data['username'], 'password': 'password', 'role': 'player',
|
||||
'full_name': data['full_name'], 'email': data['email'], 'phone': f'555-01{i:02d}',
|
||||
'games': data['games'], 'trn_username': data['trn'],
|
||||
'discord_username': data['discord'], 'league_os_profile': data['league_os']
|
||||
})
|
||||
|
||||
users = []
|
||||
for data in users_data:
|
||||
hashed_pw = hash_password(data['password'])
|
||||
user = User(
|
||||
username=data['username'],
|
||||
password_hash=hashed_pw,
|
||||
role=data['role'],
|
||||
full_name=data['full_name'],
|
||||
email=data['email'],
|
||||
phone=data.get('phone', ''),
|
||||
games=data.get('games'),
|
||||
trn_username=data.get('trn_username'),
|
||||
discord_username=data.get('discord_username'),
|
||||
league_os_profile=data.get('league_os_profile')
|
||||
)
|
||||
db.session.add(user)
|
||||
users.append(user)
|
||||
db.session.commit()
|
||||
|
||||
print(f"✓ Created {len(users)} users")
|
||||
|
||||
# Map users for easy access
|
||||
user_map = {u.username: u for u in users}
|
||||
president = user_map['president']
|
||||
manager1 = user_map['manager1']
|
||||
manager2 = user_map['manager2']
|
||||
coaches = [user_map['coach1'], user_map['coach2'], user_map['coach3']]
|
||||
scout = user_map['scout1']
|
||||
players = [user_map[f'jplayer{i}'] for i in range(1, 11)]
|
||||
|
||||
# Create Organization Teams (OrgTeams)
|
||||
org_teams_data = [
|
||||
{'name': 'Varsity', 'coach': coaches[0], 'creator': president},
|
||||
{'name': 'Junior Varsity', 'coach': coaches[1], 'creator': president},
|
||||
{'name': 'U14 Development', 'coach': coaches[2], 'creator': president},
|
||||
{'name': 'Select Team', 'coach': None, 'creator': president},
|
||||
]
|
||||
|
||||
org_teams = []
|
||||
for data in org_teams_data:
|
||||
ot = OrgTeam(
|
||||
name=data['name'],
|
||||
coach_id=data['coach'].id if data['coach'] else None,
|
||||
created_by=data['creator'].id
|
||||
)
|
||||
db.session.add(ot)
|
||||
org_teams.append(ot)
|
||||
db.session.commit()
|
||||
print(f"✓ Created {len(org_teams)} organization teams")
|
||||
|
||||
# Create tryouts
|
||||
tryouts_data = [
|
||||
{'title': 'Spring Season Tryouts', 'date': datetime.utcnow() - timedelta(days=5), 'location': 'Main Stadium', 'description': 'Tryouts for the spring competitive season. All positions welcome.', 'status': 'in_progress', 'creator': manager1, 'target_team': org_teams[0]},
|
||||
{'title': 'Fall Select Team Trials', 'date': datetime.utcnow() + timedelta(days=20), 'location': 'Training Center', 'description': 'Trials for the fall select team. High skill level required.', 'status': 'upcoming', 'creator': manager1, 'target_team': org_teams[3]},
|
||||
{'title': 'Youth Development Camp', 'date': datetime.utcnow() - timedelta(days=20), 'location': 'Community Field', 'description': 'Development camp for younger players to showcase their skills.', 'status': 'completed', 'creator': manager2, 'target_team': org_teams[2]},
|
||||
{'title': 'Winter Indoor Showcase', 'date': datetime.utcnow() - timedelta(days=2), 'location': 'Indoor Arena', 'description': 'Indoor showcase event for scouting and team selection.', 'status': 'in_progress', 'creator': manager2, 'target_team': org_teams[1]},
|
||||
]
|
||||
|
||||
tryouts = []
|
||||
for data in tryouts_data:
|
||||
t = Tryout(
|
||||
title=data['title'],
|
||||
date=data['date'].date(),
|
||||
location=data['location'],
|
||||
description=data['description'],
|
||||
status=data['status'],
|
||||
max_players=15,
|
||||
created_by=data['creator'].id,
|
||||
target_org_team_id=data['target_team'].id if data['target_team'] else None
|
||||
)
|
||||
db.session.add(t)
|
||||
tryouts.append(t)
|
||||
db.session.commit()
|
||||
print(f"✓ Created {len(tryouts)} tryouts")
|
||||
|
||||
# Register players for tryouts
|
||||
registrations_data = [
|
||||
(tryouts[0], players[:8]),
|
||||
(tryouts[1], players),
|
||||
(tryouts[2], players[:6]),
|
||||
(tryouts[3], players[2:9]),
|
||||
]
|
||||
|
||||
regs = []
|
||||
for tryout, player_list in registrations_data:
|
||||
for player in player_list:
|
||||
reg = TryoutRegistration(
|
||||
tryout_id=tryout.id,
|
||||
player_id=player.id,
|
||||
status=random.choice(['registered', 'attended', 'attended'])
|
||||
)
|
||||
db.session.add(reg)
|
||||
regs.append(reg)
|
||||
db.session.commit()
|
||||
print(f"✓ Created {len(regs)} registrations")
|
||||
|
||||
# Create evaluations (for in_progress and completed tryouts)
|
||||
eval_data = []
|
||||
# Spring tryout - some evaluations
|
||||
for player in players[:8]:
|
||||
for coach in coaches:
|
||||
if random.random() > 0.3:
|
||||
speed = random.randint(4, 10)
|
||||
agility = random.randint(4, 10)
|
||||
technique = random.randint(3, 10)
|
||||
teamwork = random.randint(5, 10)
|
||||
attitude = random.randint(5, 10)
|
||||
overall = round((speed + agility + technique + teamwork + attitude) / 5, 1)
|
||||
positions = ['Forward', 'Midfield', 'Defense', 'Goalie', 'Wing', 'Center']
|
||||
eval_entry = Evaluation(
|
||||
tryout_id=tryouts[0].id,
|
||||
player_id=player.id,
|
||||
evaluator_id=coach.id,
|
||||
speed_score=speed,
|
||||
agility_score=agility,
|
||||
technique_score=technique,
|
||||
teamwork_score=teamwork,
|
||||
attitude_score=attitude,
|
||||
overall_score=overall,
|
||||
comments=f"{'Great' if overall > 7 else 'Good'} performance. {'Shows promise.' if overall > 6 else 'Needs improvement in some areas.'}",
|
||||
position_recommendation=random.choice(positions)
|
||||
)
|
||||
db.session.add(eval_entry)
|
||||
eval_data.append(eval_entry)
|
||||
|
||||
# Completed tryout - full evaluations
|
||||
for player in players[:6]:
|
||||
for coach in coaches[:2]:
|
||||
speed = random.randint(3, 10)
|
||||
agility = random.randint(3, 10)
|
||||
technique = random.randint(3, 10)
|
||||
teamwork = random.randint(4, 10)
|
||||
attitude = random.randint(4, 10)
|
||||
overall = round((speed + agility + technique + teamwork + attitude) / 5, 1)
|
||||
positions = ['Forward', 'Midfield', 'Defense', 'Goalie', 'Wing', 'Center']
|
||||
eval_entry = Evaluation(
|
||||
tryout_id=tryouts[2].id,
|
||||
player_id=player.id,
|
||||
evaluator_id=coach.id,
|
||||
speed_score=speed,
|
||||
agility_score=agility,
|
||||
technique_score=technique,
|
||||
teamwork_score=teamwork,
|
||||
attitude_score=attitude,
|
||||
overall_score=overall,
|
||||
comments=f"{'Excellent' if overall > 8 else 'Solid'} display of skills during the camp.",
|
||||
position_recommendation=random.choice(positions)
|
||||
)
|
||||
db.session.add(eval_entry)
|
||||
eval_data.append(eval_entry)
|
||||
|
||||
db.session.commit()
|
||||
print(f"✓ Created {len(eval_data)} evaluations")
|
||||
|
||||
# Create teams for the completed tryout (tryout-specific teams)
|
||||
team1 = Team(tryout_id=tryouts[2].id, name='Alpha Team', created_by=president.id)
|
||||
team2 = Team(tryout_id=tryouts[2].id, name='Bravo Team', created_by=president.id)
|
||||
db.session.add(team1)
|
||||
db.session.add(team2)
|
||||
db.session.commit()
|
||||
|
||||
# Assign players to teams
|
||||
team_members_data = [
|
||||
(team1.id, players[0].id, 'Forward'),
|
||||
(team1.id, players[1].id, 'Midfield'),
|
||||
(team1.id, players[2].id, 'Defense'),
|
||||
(team1.id, players[3].id, 'Goalie'),
|
||||
(team2.id, players[4].id, 'Midfield'),
|
||||
(team2.id, players[5].id, 'Forward'),
|
||||
]
|
||||
for team_id, player_id, position in team_members_data:
|
||||
tm = TeamMember(team_id=team_id, player_id=player_id, position=position)
|
||||
db.session.add(tm)
|
||||
db.session.commit()
|
||||
print("✓ Created 2 tryout-specific teams with player assignments")
|
||||
|
||||
# Assign players to organization teams
|
||||
org_team_assignments = [
|
||||
(org_teams[0], players[0]),
|
||||
(org_teams[0], players[1]),
|
||||
(org_teams[0], players[2]),
|
||||
(org_teams[1], players[3]),
|
||||
(org_teams[1], players[4]),
|
||||
(org_teams[1], players[5]),
|
||||
(org_teams[2], players[6]),
|
||||
(org_teams[2], players[7]),
|
||||
]
|
||||
for org_team, player in org_team_assignments:
|
||||
player.team_id = org_team.id
|
||||
db.session.commit()
|
||||
print("✓ Assigned players to organization teams")
|
||||
|
||||
print("\n🎉 Database seeded successfully!")
|
||||
print("\n=== Login Credentials ===")
|
||||
print("President: username='president', password='password'")
|
||||
print("Manager: username='manager1', password='password'")
|
||||
print("Coach: username='coach1', password='password' (assigned to Varsity)")
|
||||
print("Coach: username='coach2', password='password' (assigned to Junior Varsity)")
|
||||
print("Coach: username='coach3', password='password' (assigned to U14 Development)")
|
||||
print("Player: username='jplayer1', password='password'")
|
||||
print("Scout: username='scout1', password='password'")
|
||||
|
||||
if __name__ == '__main__':
|
||||
seed_database()
|
||||
@@ -0,0 +1,913 @@
|
||||
:root {
|
||||
--primary: #4f46e5;
|
||||
--primary-dark: #4338ca;
|
||||
--primary-light: #eef2ff;
|
||||
--success: #10b981;
|
||||
--success-light: #ecfdf5;
|
||||
--warning: #f59e0b;
|
||||
--warning-light: #fffbeb;
|
||||
--danger: #ef4444;
|
||||
--danger-light: #fef2f2;
|
||||
--info: #3b82f6;
|
||||
--info-light: #eff6ff;
|
||||
--secondary: #6b7280;
|
||||
--secondary-light: #f3f4f6;
|
||||
--dark: #1f2937;
|
||||
--gray-50: #f9fafb;
|
||||
--gray-100: #f3f4f6;
|
||||
--gray-200: #e5e7eb;
|
||||
--gray-300: #d1d5db;
|
||||
--gray-400: #9ca3af;
|
||||
--gray-500: #6b7280;
|
||||
--gray-600: #4b5563;
|
||||
--gray-700: #374151;
|
||||
--gray-800: #1f2937;
|
||||
--gray-900: #111827;
|
||||
--sidebar-width: 260px;
|
||||
--sidebar-collapsed: 0px;
|
||||
--radius: 12px;
|
||||
--radius-sm: 8px;
|
||||
--shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
|
||||
--shadow-md: 0 4px 6px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.06);
|
||||
--shadow-lg: 0 10px 15px rgba(0,0,0,0.1), 0 4px 6px rgba(0,0,0,0.05);
|
||||
--transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -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;
|
||||
}
|
||||
|
||||
a { color: var(--primary); text-decoration: none; }
|
||||
a:hover { color: var(--primary-dark); }
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: var(--sidebar-width);
|
||||
height: 100vh;
|
||||
background: var(--dark);
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
transition: var(--transition);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logo i { color: var(--primary); font-size: 1.6rem; }
|
||||
|
||||
.user-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-sm { width: 32px; height: 32px; font-size: 0.75rem; }
|
||||
.avatar-lg { width: 48px; height: 48px; font-size: 1rem; }
|
||||
.avatar-xl { width: 64px; height: 64px; font-size: 1.3rem; }
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-name { font-weight: 600; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.user-role { font-size: 0.75rem; padding: 2px 8px; border-radius: 20px; display: inline-block; width: fit-content; }
|
||||
|
||||
.nav-links {
|
||||
list-style: none;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.nav-links li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
color: var(--gray-400);
|
||||
transition: var(--transition);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.nav-links li a:hover, .nav-links li a.active {
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-links li a.active {
|
||||
border-left: 3px solid var(--primary);
|
||||
padding-left: 17px;
|
||||
}
|
||||
|
||||
.nav-links li a i { width: 20px; text-align: center; font-size: 1.1rem; }
|
||||
|
||||
.nav-divider {
|
||||
height: 1px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
.logout-link { color: var(--danger) !important; }
|
||||
.logout-link:hover { background: rgba(239,68,68,0.1) !important; }
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
transition: var(--transition);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 30px;
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.3rem;
|
||||
color: var(--gray-500);
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-toggle:hover { background: var(--gray-100); }
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.breadcrumb { font-size: 0.8rem; color: var(--gray-500); }
|
||||
.breadcrumb a { color: var(--primary); }
|
||||
|
||||
.content { padding: 24px 30px; }
|
||||
|
||||
/* Flash Messages */
|
||||
.flash-messages {
|
||||
padding: 0 30px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9rem;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateY(-10px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.alert-success { background: var(--success-light); color: #065f46; border: 1px solid #a7f3d0; }
|
||||
.alert-danger { background: var(--danger-light); color: #991b1b; border: 1px solid #fecaca; }
|
||||
.alert-warning { background: var(--warning-light); color: #92400e; border: 1px solid #fde68a; }
|
||||
.alert-info { background: var(--info-light); color: #1e40af; border: 1px solid #bfdbfe; }
|
||||
|
||||
.alert-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.3rem;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
opacity: 0.6;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.alert-close:hover { opacity: 1; }
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-body { padding: 20px; }
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.stat-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
|
||||
|
||||
.stat-card-sm { padding: 14px; }
|
||||
.stat-card-sm .stat-info h3 { font-size: 0.9rem; }
|
||||
.stat-card-sm .stat-info p { font-size: 0.75rem; }
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.3rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-info h3 { font-size: 1.5rem; font-weight: 700; color: var(--gray-900); }
|
||||
.stat-info p { font-size: 0.85rem; color: var(--gray-500); margin-top: 2px; }
|
||||
|
||||
.bg-primary { background: var(--primary-light); color: var(--primary); }
|
||||
.bg-success { background: var(--success-light); color: var(--success); }
|
||||
.bg-warning { background: var(--warning-light); color: var(--warning); }
|
||||
.bg-info { background: var(--info-light); color: var(--info); }
|
||||
.bg-danger { background: var(--danger-light); color: var(--danger); }
|
||||
.bg-secondary { background: var(--secondary-light); color: var(--secondary); }
|
||||
|
||||
/* Dashboard Grid */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.dashboard-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.table-container { overflow-x: auto; }
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.table th {
|
||||
text-align: left;
|
||||
padding: 10px 12px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-500);
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 2px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table tbody tr:hover { background: var(--gray-50); }
|
||||
|
||||
.cell-title { font-weight: 600; }
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.badge-lg { padding: 5px 14px; font-size: 0.85rem; }
|
||||
|
||||
.badge-upcoming { background: var(--info-light); color: var(--info); }
|
||||
.badge-in_progress { background: var(--warning-light); color: var(--warning); }
|
||||
.badge-completed { background: var(--success-light); color: var(--success); }
|
||||
.badge-president { background: #eef2ff; color: #4338ca; }
|
||||
.badge-manager { background: #ecfdf5; color: #059669; }
|
||||
.badge-coach { background: #fffbeb; color: #d97706; }
|
||||
.badge-player { background: #f3f4f6; color: #4b5563; }
|
||||
.badge-scout { background: #fef2f2; color: #dc2626; }
|
||||
.badge-success { background: var(--success-light); color: var(--success); }
|
||||
.badge-warning { background: var(--warning-light); color: var(--warning); }
|
||||
.badge-danger { background: var(--danger-light); color: var(--danger); }
|
||||
.badge-info { background: var(--info-light); color: var(--info); }
|
||||
.badge-registered { background: var(--info-light); color: var(--info); }
|
||||
.badge-attended { background: var(--success-light); color: var(--success); }
|
||||
.badge-no_show { background: var(--danger-light); color: var(--danger); }
|
||||
|
||||
.score {
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary { background: var(--primary); color: white; }
|
||||
.btn-primary:hover { background: var(--primary-dark); color: white; }
|
||||
|
||||
.btn-secondary { background: var(--gray-100); color: var(--gray-700); }
|
||||
.btn-secondary:hover { background: var(--gray-200); }
|
||||
|
||||
.btn-success { background: var(--success); color: white; }
|
||||
.btn-success:hover { background: #059669; color: white; }
|
||||
|
||||
.btn-danger { background: var(--danger); color: white; }
|
||||
.btn-danger:hover { background: #dc2626; color: white; }
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--gray-300);
|
||||
color: var(--gray-700);
|
||||
}
|
||||
.btn-outline:hover { background: var(--gray-50); border-color: var(--gray-400); }
|
||||
|
||||
.btn-sm { padding: 5px 10px; font-size: 0.8rem; }
|
||||
.btn-lg { padding: 12px 24px; font-size: 1rem; }
|
||||
.btn-block { width: 100%; justify-content: center; }
|
||||
|
||||
/* Forms */
|
||||
.form { max-width: 800px; }
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="email"],
|
||||
.form-group input[type="password"],
|
||||
.form-group input[type="tel"],
|
||||
.form-group input[type="number"],
|
||||
.form-group input[type="date"],
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.9rem;
|
||||
transition: var(--transition);
|
||||
background: white;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(79,70,229,0.1);
|
||||
}
|
||||
|
||||
.form-group textarea { resize: vertical; min-height: 80px; }
|
||||
|
||||
.form-select { padding: 8px 12px; border: 1px solid var(--gray-300); border-radius: var(--radius-sm); font-size: 0.9rem; }
|
||||
.form-select-sm { padding: 4px 8px; font-size: 0.8rem; }
|
||||
.form-input { padding: 8px 12px; border: 1px solid var(--gray-300); border-radius: var(--radius-sm); font-size: 0.9rem; }
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.form-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.inline-form { display: inline; }
|
||||
|
||||
.col-12 { flex: 0 0 100%; }
|
||||
.col-6 { flex: 0 0 calc(50% - 8px); }
|
||||
.col-4 { flex: 0 0 calc(33.333% - 11px); }
|
||||
.col-3 { flex: 0 0 calc(25% - 12px); }
|
||||
|
||||
/* Score Input */
|
||||
.score-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.score-input input[type="range"] {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--gray-200);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.score-input input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.range-value {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Auth */
|
||||
.auth-wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.auth-wrapper .flash-messages {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
padding: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
background: white;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.auth-header i {
|
||||
font-size: 3rem;
|
||||
color: var(--primary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-header h2 {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.auth-header p {
|
||||
color: var(--gray-500);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.auth-form .form-group {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.auth-form .form-group label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.auth-form .form-group label i {
|
||||
color: var(--gray-400);
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.auth-form .form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.95rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.auth-form .form-group input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(79,70,229,0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
/* Detail Grid */
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-item { display: flex; flex-direction: column; gap: 4px; }
|
||||
.detail-item.full-width { grid-column: 1 / -1; }
|
||||
.detail-label { font-size: 0.8rem; color: var(--gray-500); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }
|
||||
.detail-value { font-size: 0.95rem; color: var(--gray-800); }
|
||||
|
||||
/* User Mini */
|
||||
.user-mini {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Profile */
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.profile-info h2 { font-size: 1.3rem; margin-bottom: 4px; }
|
||||
.profile-info p { font-size: 0.85rem; }
|
||||
|
||||
.profile-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.stat-item h4 { font-size: 1.5rem; font-weight: 700; color: var(--primary); }
|
||||
.stat-item p { font-size: 0.8rem; color: var(--gray-500); }
|
||||
|
||||
/* Team */
|
||||
.team-card {
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.team-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: var(--gray-800);
|
||||
}
|
||||
|
||||
.team-members {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.team-members li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.position-tag {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Eval Player Info */
|
||||
.eval-player-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
color: var(--gray-300);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-state h3 { font-size: 1.1rem; color: var(--gray-600); margin-bottom: 8px; }
|
||||
.empty-state p { color: var(--gray-400); font-size: 0.9rem; margin-bottom: 16px; }
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2001;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
z-index: 2002;
|
||||
position: relative;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: var(--gray-400);
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.modal-close:hover { color: var(--gray-700); }
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* E-Sports Styles */
|
||||
.checkbox-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.checkbox-label:hover {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
accent-color: var(--primary);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-200);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-800);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-title i {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.badge-esport {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin: 2px 3px;
|
||||
}
|
||||
|
||||
.trn-link {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.trn-link:hover {
|
||||
color: var(--primary-dark);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 0.8rem;
|
||||
color: var(--gray-500);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.hidden { display: none !important; }
|
||||
.text-center { text-align: center; }
|
||||
.text-muted { color: var(--gray-500); }
|
||||
.mt-2 { margin-top: 8px; }
|
||||
.mt-4 { margin-top: 16px; }
|
||||
.mb-3 { margin-bottom: 12px; }
|
||||
.mb-4 { margin-bottom: 16px; }
|
||||
.mr-2 { margin-right: 8px; }
|
||||
.mx-2 { margin-left: 8px; margin-right: 8px; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: var(--sidebar-collapsed);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.sidebar.open {
|
||||
width: var(--sidebar-width);
|
||||
transform: translateX(0);
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
.sidebar-toggle { display: block; }
|
||||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.form-row { flex-direction: column; }
|
||||
.col-6, .col-4, .col-3 { flex: 0 0 100%; }
|
||||
.profile-stats { grid-template-columns: 1fr; }
|
||||
.detail-grid { grid-template-columns: 1fr; }
|
||||
.content { padding: 16px; }
|
||||
.top-bar { padding: 12px 16px; }
|
||||
.flash-messages { padding: 0 16px; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Sidebar toggle for mobile
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
sidebar.classList.toggle('open');
|
||||
}
|
||||
|
||||
// Close sidebar when clicking outside on mobile
|
||||
document.addEventListener('click', function(event) {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const toggle = document.getElementById('sidebarToggle');
|
||||
if (window.innerWidth <= 768) {
|
||||
if (!sidebar.contains(event.target) && !toggle.contains(event.target)) {
|
||||
sidebar.classList.remove('open');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-dismiss flash messages
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const alerts = document.querySelectorAll('.alert-dismissible');
|
||||
alerts.forEach(function(alert) {
|
||||
setTimeout(function() {
|
||||
if (alert.parentElement) {
|
||||
alert.style.opacity = '0';
|
||||
alert.style.transition = 'opacity 0.3s ease';
|
||||
setTimeout(function() {
|
||||
if (alert.parentElement) {
|
||||
alert.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
|
||||
// Confirm delete actions
|
||||
document.querySelectorAll('[data-confirm]').forEach(function(el) {
|
||||
el.addEventListener('click', function(e) {
|
||||
if (!confirm(this.getAttribute('data-confirm'))) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Team Tryout Management{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
<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>
|
||||
{% if current_user.is_authenticated %}
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo">
|
||||
<i class="fas fa-trophy"></i>
|
||||
<span>TryoutPro</span>
|
||||
</div>
|
||||
<div class="user-badge">
|
||||
<div class="user-avatar">
|
||||
{{ current_user.full_name[:2] | upper }}
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">{{ current_user.full_name }}</span>
|
||||
<span class="user-role role-{{ current_user.role }}">{{ current_user.role | capitalize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li>
|
||||
<a href="{{ url_for('main.dashboard') }}" class="{% if request.endpoint and 'dashboard' in request.endpoint %}active{% endif %}">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('tryouts.list_tryouts') }}" class="{% if request.endpoint and 'tryouts' in request.endpoint and request.endpoint != 'tryouts.create_tryout' %}active{% endif %}">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<span>Tryouts</span>
|
||||
</a>
|
||||
</li>
|
||||
{% if current_user.can_evaluate() %}
|
||||
<li>
|
||||
<a href="{{ url_for('evaluations.list_evaluations') }}" class="{% if request.endpoint and 'evaluations' in request.endpoint %}active{% endif %}">
|
||||
<i class="fas fa-clipboard-check"></i>
|
||||
<span>Evaluations</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ url_for('teams.list_teams') }}" class="{% if request.endpoint and 'teams' in request.endpoint %}active{% endif %}">
|
||||
<i class="fas fa-users-cog"></i>
|
||||
<span>Teams</span>
|
||||
</a>
|
||||
</li>
|
||||
{% if current_user.can_manage_users() %}
|
||||
<li>
|
||||
<a href="{{ url_for('users.list_users') }}" class="{% if request.endpoint and 'users' in request.endpoint and request.endpoint != 'users.profile' %}active{% endif %}">
|
||||
<i class="fas fa-users-cog"></i>
|
||||
<span>Manage Users</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ url_for('users.profile') }}" class="{% if request.endpoint == 'users.profile' %}active{% endif %}">
|
||||
<i class="fas fa-user"></i>
|
||||
<span>My Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-divider"></li>
|
||||
<li>
|
||||
<a href="{{ url_for('auth.logout') }}" class="logout-link">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="main-content" id="mainContent">
|
||||
<header class="top-bar">
|
||||
<button class="sidebar-toggle" id="sidebarToggle" onclick="toggleSidebar()">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<div class="page-header">
|
||||
<h1>{% block page_title %}Dashboard{% endblock %}</h1>
|
||||
{% block breadcrumb %}{% endblock %}
|
||||
</div>
|
||||
{% block header_actions %}{% endblock %}
|
||||
</header>
|
||||
<div class="flash-messages">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible">
|
||||
<span>{{ message }}</span>
|
||||
<button type="button" class="alert-close" onclick="this.parentElement.remove()">×</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="auth-wrapper">
|
||||
<div class="flash-messages">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible">
|
||||
<span>{{ message }}</span>
|
||||
<button type="button" class="alert-close" onclick="this.parentElement.remove()">×</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="auth-container">
|
||||
<div class="auth-header">
|
||||
<i class="fas fa-trophy"></i>
|
||||
<h2>TryoutPro</h2>
|
||||
<p>Team Tryout Management System</p>
|
||||
</div>
|
||||
{% block auth_content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Create Tryout - TryoutPro{% endblock %}
|
||||
{% block page_title %}Create Tryout{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('tryouts.list_tryouts') }}">Tryouts</a> / Create</span>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('tryouts.create_tryout') }}" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-12">
|
||||
<label for="title">Tryout Title</label>
|
||||
<input type="text" id="title" name="title" placeholder="e.g., Spring Season Tryouts" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="date">Date</label>
|
||||
<input type="date" id="date" name="date" required>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="location">Location</label>
|
||||
<input type="text" id="location" name="location" placeholder="e.g., Main Field">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="max_players">Max Players</label>
|
||||
<input type="number" id="max_players" name="max_players" placeholder="Leave blank for unlimited" min="1">
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="target_org_team_id">Target Team</label>
|
||||
<select id="target_org_team_id" name="target_org_team_id" class="form-select">
|
||||
<option value="">-- No target team --</option>
|
||||
{% for team in org_teams %}
|
||||
<option value="{{ team.id }}">{{ team.name }}{% if team.coach %} (Coach: {{ team.coach.full_name }}){% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" rows="4" placeholder="Enter any details about the tryout..."></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('tryouts.list_tryouts') }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Create Tryout</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,52 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Create User - TryoutPro{% endblock %}
|
||||
{% block page_title %}Create User{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('users.list_users') }}">Users</a> / Create</span>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('users.create_user') }}" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="full_name">Full Name</label>
|
||||
<input type="text" id="full_name" name="full_name" placeholder="Enter full name" required>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" placeholder="Choose username" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" placeholder="Enter email" required>
|
||||
</div>
|
||||
<div class="form-group col-3">
|
||||
<label for="phone">Phone</label>
|
||||
<input type="tel" id="phone" name="phone" placeholder="Phone number">
|
||||
</div>
|
||||
<div class="form-group col-3">
|
||||
<label for="role">Role</label>
|
||||
<select id="role" name="role" required>
|
||||
{% for r in roles %}
|
||||
<option value="{{ r }}">{{ r | capitalize }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" placeholder="Set password" required minlength="6">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('users.list_users') }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Create User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,330 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Dashboard - TryoutPro{% endblock %}
|
||||
{% block page_title %}Dashboard{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / Dashboard</span>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="dashboard">
|
||||
{% if user.role == 'president' %}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-primary">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.total_users }}</h3>
|
||||
<p>Total Users</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-success">
|
||||
<i class="fas fa-user-friends"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.total_players }}</h3>
|
||||
<p>Players</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-warning">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.total_tryouts }}</h3>
|
||||
<p>Total Tryouts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-info">
|
||||
<i class="fas fa-clipboard-list"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.total_evaluations }}</h3>
|
||||
<p>Evaluations</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-danger">
|
||||
<i class="fas fa-play-circle"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.active_tryouts }}</h3>
|
||||
<p>Active Tryouts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-secondary">
|
||||
<i class="fas fa-clock"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.upcoming_tryouts }}</h3>
|
||||
<p>Upcoming</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-user-plus"></i> Recent Users</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Role</th><th>Joined</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in stats.recent_users %}
|
||||
<tr>
|
||||
<td>{{ u.full_name }}</td>
|
||||
<td><span class="badge badge-{{ u.role }}">{{ u.role }}</span></td>
|
||||
<td>{{ u.created_at.strftime('%m/%d/%Y') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-calendar-alt"></i> Recent Tryouts</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Title</th><th>Date</th><th>Status</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in stats.recent_tryouts %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('tryouts.view_tryout', tryout_id=t.id) }}">{{ t.title }}</a></td>
|
||||
<td>{{ t.date.strftime('%m/%d/%Y') }}</td>
|
||||
<td><span class="badge badge-{{ t.status }}">{{ t.status }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif user.role == 'manager' %}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-primary">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.total_tryouts }}</h3>
|
||||
<p>My Tryouts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-success">
|
||||
<i class="fas fa-play-circle"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.active_tryouts }}</h3>
|
||||
<p>Active</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-info">
|
||||
<i class="fas fa-clipboard-check"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.total_evaluations }}</h3>
|
||||
<p>My Evaluations</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-calendar"></i> My Tryouts</h3>
|
||||
<a href="{{ url_for('tryouts.create_tryout') }}" class="btn btn-sm btn-primary">+ New Tryout</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Title</th><th>Date</th><th>Status</th><th>Players</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in stats.my_tryouts %}
|
||||
<tr>
|
||||
<td>{{ t.title }}</td>
|
||||
<td>{{ t.date.strftime('%m/%d/%Y') }}</td>
|
||||
<td><span class="badge badge-{{ t.status }}">{{ t.status }}</span></td>
|
||||
<td>{{ t.registrations.count() }}</td>
|
||||
<td><a href="{{ url_for('tryouts.view_tryout', tryout_id=t.id) }}" class="btn btn-sm btn-outline">View</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not stats.my_tryouts %}
|
||||
<tr><td colspan="5" class="text-center">No tryouts yet. <a href="{{ url_for('tryouts.create_tryout') }}">Create one</a></td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif user.role == 'coach' %}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-primary">
|
||||
<i class="fas fa-clipboard-check"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.my_evaluations }}</h3>
|
||||
<p>Evaluations Done</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-warning">
|
||||
<i class="fas fa-hourglass-half"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.pending_evaluations }}</h3>
|
||||
<p>Pending</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-history"></i> Recent Evaluations</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Player</th><th>Tryout</th><th>Score</th><th>Date</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in stats.my_recent_evaluations %}
|
||||
<tr>
|
||||
<td>{{ e.player.full_name }}</td>
|
||||
<td>{{ e.tryout.title }}</td>
|
||||
<td><span class="score">{{ e.overall_score }}</span></td>
|
||||
<td>{{ e.created_at.strftime('%m/%d/%Y') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif user.role == 'player' %}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-primary">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.my_tryouts }}</h3>
|
||||
<p>My Tryouts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-success">
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ "%.1f"|format(stats.average_score) if stats.average_score else 'N/A' }}</h3>
|
||||
<p>Avg Score</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-info">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.my_evaluations }}</h3>
|
||||
<p>Evaluations</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-grid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-clipboard-list"></i> My Registrations</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead><tr><th>Tryout</th><th>Date</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in stats.my_registrations %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('tryouts.view_tryout', tryout_id=r.tryout.id) }}">{{ r.tryout.title }}</a></td>
|
||||
<td>{{ r.tryout.date.strftime('%m/%d/%Y') }}</td>
|
||||
<td><span class="badge badge-{{ r.status }}">{{ r.status }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-star"></i> My Evaluations</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead><tr><th>Tryout</th><th>Evaluator</th><th>Score</th><th>Date</th></tr></thead>
|
||||
<tbody>
|
||||
{% for e in stats.my_eval_list %}
|
||||
<tr>
|
||||
<td>{{ e.tryout.title }}</td>
|
||||
<td>{{ e.evaluator.full_name }}</td>
|
||||
<td><span class="score">{{ e.overall_score }}</span></td>
|
||||
<td>{{ e.created_at.strftime('%m/%d/%Y') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif user.role == 'scout' %}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-primary">
|
||||
<i class="fas fa-user-friends"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.total_players }}</h3>
|
||||
<p>Total Players</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-info">
|
||||
<i class="fas fa-clipboard-list"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ stats.total_evaluations }}</h3>
|
||||
<p>Total Evaluations</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-trophy"></i> Top Rated Players</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead><tr><th>#</th><th>Player</th><th>Avg Score</th></tr></thead>
|
||||
<tbody>
|
||||
{% for player, score in stats.top_players %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ player.full_name }}</td>
|
||||
<td><span class="score">{{ score }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not stats.top_players %}
|
||||
<tr><td colspan="3" class="text-center">No evaluations yet.</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,75 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Edit Profile - TryoutPro{% endblock %}
|
||||
{% block page_title %}Edit Profile{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('users.profile') }}">Profile</a> / Edit</span>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('users.edit_profile') }}" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="full_name">Full Name</label>
|
||||
<input type="text" id="full_name" name="full_name" value="{{ user.full_name }}" required>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" value="{{ user.email }}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="phone">Phone</label>
|
||||
<input type="tel" id="phone" name="phone" value="{{ user.phone or '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-gamepad"></i> E-Sports Profile</h4>
|
||||
<p class="text-muted small">Update your competitive gaming profile for tryouts.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label><i class="fas fa-headset"></i> Games You Play</label>
|
||||
<div class="checkbox-grid">
|
||||
{% for game in esport_games %}
|
||||
{% set games_list = user.get_games_list() %}
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="games" value="{{ game }}" {% if game in games_list %}checked{% endif %}>
|
||||
<span>{{ game }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<small class="form-text text-muted">Select all games you're signing in for.</small>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-4">
|
||||
<label for="trn_username"><i class="fas fa-chart-line"></i> TRN Username</label>
|
||||
<input type="text" id="trn_username" name="trn_username" value="{{ user.trn_username or '' }}" placeholder="Your Tracker Network username">
|
||||
<small class="form-text text-muted">Others can click your TRN to view your stats on tracker.gg.</small>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="discord_username"><i class="fab fa-discord"></i> Discord Username</label>
|
||||
<input type="text" id="discord_username" name="discord_username" value="{{ user.discord_username or '' }}" placeholder="e.g. Name#1234">
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="league_os_profile"><i class="fas fa-link"></i> League OS Connection</label>
|
||||
<input type="text" id="league_os_profile" name="league_os_profile" value="{{ user.league_os_profile or '' }}" placeholder="League OS profile link or ID">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-lock"></i> Change Password</h4>
|
||||
<p class="text-muted small">Leave blank to keep your current password.</p>
|
||||
<div class="form-group">
|
||||
<label for="password">New Password</label>
|
||||
<input type="password" id="password" name="password" placeholder="Enter new password">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('users.profile') }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Edit Tryout - TryoutPro{% endblock %}
|
||||
{% block page_title %}Edit Tryout{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('tryouts.list_tryouts') }}">Tryouts</a> / <a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}">{{ tryout.title }}</a> / Edit</span>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('tryouts.edit_tryout', tryout_id=tryout.id) }}" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-12">
|
||||
<label for="title">Tryout Title</label>
|
||||
<input type="text" id="title" name="title" value="{{ tryout.title }}" placeholder="e.g., Spring Season Tryouts" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="date">Date</label>
|
||||
<input type="date" id="date" name="date" value="{{ tryout.date.strftime('%Y-%m-%d') }}" required>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="location">Location</label>
|
||||
<input type="text" id="location" name="location" value="{{ tryout.location or '' }}" placeholder="e.g., Main Field">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="max_players">Max Players</label>
|
||||
<input type="number" id="max_players" name="max_players" value="{{ tryout.max_players or '' }}" placeholder="Leave blank for unlimited" min="1">
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="target_org_team_id">Target Team</label>
|
||||
<select id="target_org_team_id" name="target_org_team_id" class="form-select">
|
||||
<option value="">-- No target team --</option>
|
||||
{% for team in org_teams %}
|
||||
<option value="{{ team.id }}" {% if tryout.target_org_team_id == team.id %}selected{% endif %}>
|
||||
{{ team.name }}{% if team.coach %} (Coach: {{ team.coach.full_name }}){% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" rows="4" placeholder="Enter any details about the tryout...">{{ tryout.description or '' }}</textarea>
|
||||
</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">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,83 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Edit {{ user.full_name }} - TryoutPro{% endblock %}
|
||||
{% block page_title %}Edit User{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('users.list_users') }}">Users</a> / Edit</span>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('users.edit_user', user_id=user.id) }}" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="full_name">Full Name</label>
|
||||
<input type="text" id="full_name" name="full_name" value="{{ user.full_name }}" required>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" value="{{ user.email }}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-4">
|
||||
<label for="phone">Phone</label>
|
||||
<input type="tel" id="phone" name="phone" value="{{ user.phone or '' }}">
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="role">Role</label>
|
||||
<select id="role" name="role" required>
|
||||
{% for r in roles %}
|
||||
<option value="{{ r }}" {% if user.role == r %}selected{% endif %}>{{ r | capitalize }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="is_active_account">
|
||||
<input type="checkbox" id="is_active_account" name="is_active_account" {% if user.is_active_account %}checked{% endif %}>
|
||||
Active Account
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-gamepad"></i> E-Sports Profile</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Games</label>
|
||||
<div class="checkbox-grid">
|
||||
{% for game in esport_games %}
|
||||
{% set games_list = user.get_games_list() %}
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="games" value="{{ game }}" {% if game in games_list %}checked{% endif %}>
|
||||
<span>{{ game }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-4">
|
||||
<label for="trn_username">TRN (Tracker Network) Username</label>
|
||||
<input type="text" id="trn_username" name="trn_username" value="{{ user.trn_username or '' }}" placeholder="Tracker Network username">
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="discord_username">Discord Username</label>
|
||||
<input type="text" id="discord_username" name="discord_username" value="{{ user.discord_username or '' }}" placeholder="e.g. Name#1234">
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="league_os_profile">League OS Connection</label>
|
||||
<input type="text" id="league_os_profile" name="league_os_profile" value="{{ user.league_os_profile or '' }}" placeholder="League OS profile link or ID">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">New Password <small>(leave blank to keep current)</small></label>
|
||||
<input type="password" id="password" name="password" placeholder="Enter new password">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('users.list_users') }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Update User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,120 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Evaluate {{ player.full_name }} - TryoutPro{% endblock %}
|
||||
{% block page_title %}Evaluate {{ player.full_name }}{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}">{{ tryout.title }}</a> / Evaluate</span>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="dashboard-grid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-clipboard-list"></i> Player Evaluation</h3>
|
||||
<span class="badge badge-info">{{ tryout.title }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="eval-player-info mb-4">
|
||||
<div class="user-avatar avatar-lg">{{ player.full_name[:2] | upper }}</div>
|
||||
<div>
|
||||
<h3>{{ player.full_name }}</h3>
|
||||
<p class="text-muted">{{ player.email }} | {{ player.phone or 'No phone' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if existing_eval %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i> You have already evaluated this player. Your previous scores are shown below.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url_for('evaluations.evaluate_player', tryout_id=tryout.id, player_id=player.id) }}" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-4">
|
||||
<label for="speed_score">Speed (1-10)</label>
|
||||
<div class="score-input">
|
||||
<input type="range" id="speed_score" name="speed_score" min="1" max="10" value="{{ existing_eval.speed_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
|
||||
<span class="range-value">{{ existing_eval.speed_score or 5 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="agility_score">Agility (1-10)</label>
|
||||
<div class="score-input">
|
||||
<input type="range" id="agility_score" name="agility_score" min="1" max="10" value="{{ existing_eval.agility_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
|
||||
<span class="range-value">{{ existing_eval.agility_score or 5 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="technique_score">Technique (1-10)</label>
|
||||
<div class="score-input">
|
||||
<input type="range" id="technique_score" name="technique_score" min="1" max="10" value="{{ existing_eval.technique_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
|
||||
<span class="range-value">{{ existing_eval.technique_score or 5 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-4">
|
||||
<label for="teamwork_score">Teamwork (1-10)</label>
|
||||
<div class="score-input">
|
||||
<input type="range" id="teamwork_score" name="teamwork_score" min="1" max="10" value="{{ existing_eval.teamwork_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
|
||||
<span class="range-value">{{ existing_eval.teamwork_score or 5 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="attitude_score">Attitude (1-10)</label>
|
||||
<div class="score-input">
|
||||
<input type="range" id="attitude_score" name="attitude_score" min="1" max="10" value="{{ existing_eval.attitude_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
|
||||
<span class="range-value">{{ existing_eval.attitude_score or 5 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="position_recommendation">Recommended Position</label>
|
||||
<input type="text" id="position_recommendation" name="position_recommendation" value="{{ existing_eval.position_recommendation or '' }}" placeholder="e.g., Forward, Defense">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="comments">Comments</label>
|
||||
<textarea id="comments" name="comments" rows="4" placeholder="Enter your evaluation notes...">{{ existing_eval.comments or '' }}</textarea>
|
||||
</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-save"></i> {% if existing_eval %}Update Evaluation{% else %}Submit Evaluation{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if evaluators %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-users"></i> All Evaluations for {{ player.full_name }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Evaluator</th><th>Speed</th><th>Agility</th><th>Technique</th><th>Teamwork</th><th>Attitude</th><th>Overall</th><th>Position</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in evaluators %}
|
||||
<tr>
|
||||
<td>{{ entry.evaluator.full_name }}</td>
|
||||
<td>{{ entry.eval.speed_score or '-' }}</td>
|
||||
<td>{{ entry.eval.agility_score or '-' }}</td>
|
||||
<td>{{ entry.eval.technique_score or '-' }}</td>
|
||||
<td>{{ entry.eval.teamwork_score or '-' }}</td>
|
||||
<td>{{ entry.eval.attitude_score or '-' }}</td>
|
||||
<td><span class="score">{{ entry.eval.overall_score or '-' }}</span></td>
|
||||
<td>{{ entry.eval.position_recommendation or '-' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,81 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Evaluations - TryoutPro{% endblock %}
|
||||
{% block page_title %}Evaluations{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / Evaluations</span>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if current_user.role == 'president' and player_scores %}
|
||||
<div class="stats-grid mb-4">
|
||||
{% for pid, data in player_scores.items() %}
|
||||
<div class="stat-card stat-card-sm">
|
||||
<div class="stat-icon bg-info">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ data.player.full_name[:12] }}</h3>
|
||||
<p>Avg: <strong>{{ data.avg }}</strong> / {{ data.count }} evals</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-clipboard-list"></i>
|
||||
{% if current_user.role == 'player' %}
|
||||
My Evaluations
|
||||
{% else %}
|
||||
All Evaluations
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tryout</th>
|
||||
<th>Player</th>
|
||||
<th>Evaluator</th>
|
||||
<th>Speed</th>
|
||||
<th>Agility</th>
|
||||
<th>Technique</th>
|
||||
<th>Teamwork</th>
|
||||
<th>Attitude</th>
|
||||
<th>Overall</th>
|
||||
<th>Position</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for eval in evaluations %}
|
||||
<tr>
|
||||
<td>{{ eval.tryout.title }}</td>
|
||||
<td>{{ eval.player.full_name }}</td>
|
||||
<td>{{ eval.evaluator.full_name }}</td>
|
||||
<td>{{ eval.speed_score or '-' }}</td>
|
||||
<td>{{ eval.agility_score or '-' }}</td>
|
||||
<td>{{ eval.technique_score or '-' }}</td>
|
||||
<td>{{ eval.teamwork_score or '-' }}</td>
|
||||
<td>{{ eval.attitude_score or '-' }}</td>
|
||||
<td><span class="score">{{ eval.overall_score or '-' }}</span></td>
|
||||
<td><span class="badge badge-info">{{ eval.position_recommendation or 'N/A' }}</span></td>
|
||||
<td>{{ eval.created_at.strftime('%m/%d/%Y') }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="11" class="text-center">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-clipboard"></i>
|
||||
<h3>No evaluations yet</h3>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,17 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Login - TryoutPro{% endblock %}
|
||||
{% block auth_content %}
|
||||
<form method="POST" action="{{ url_for('auth.login') }}" class="auth-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="form-group">
|
||||
<label for="username"><i class="fas fa-user"></i> Username</label>
|
||||
<input type="text" id="username" name="username" placeholder="Enter your username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password"><i class="fas fa-lock"></i> Password</label>
|
||||
<input type="password" id="password" name="password" placeholder="Enter your password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">Sign In</button>
|
||||
<p class="auth-link">Don't have an account? <a href="{{ url_for('auth.register') }}">Register here</a></p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,59 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Players to Evaluate - TryoutPro{% endblock %}
|
||||
{% block page_title %}Players to Evaluate{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}">{{ tryout.title }}</a> / Evaluate</span>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-users"></i> Players in {{ tryout.title }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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.full_name[:2] | upper }}</div>
|
||||
<span>{{ entry.player.full_name }}</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-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>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,147 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}My Profile - TryoutPro{% endblock %}
|
||||
{% block page_title %}My Profile{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / Profile</span>{% endblock %}
|
||||
|
||||
{% block header_actions %}
|
||||
<div class="header-actions">
|
||||
<a href="{{ url_for('users.edit_profile') }}" class="btn btn-sm btn-primary">
|
||||
<i class="fas fa-edit"></i> Edit Profile
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="dashboard-grid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-user-circle"></i> Account Information</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="profile-header">
|
||||
<div class="user-avatar avatar-xl">{{ user.full_name[:2] | upper }}</div>
|
||||
<div class="profile-info">
|
||||
<h2>{{ user.full_name }}</h2>
|
||||
<span class="badge badge-{{ user.role }} badge-lg">{{ user.role | capitalize }}</span>
|
||||
<p class="text-muted">Member since {{ user.created_at.strftime('%B %Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-grid mt-4">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Username</span>
|
||||
<span class="detail-value">{{ user.username }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Email</span>
|
||||
<span class="detail-value">{{ user.email }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Phone</span>
|
||||
<span class="detail-value">{{ user.phone or 'Not provided' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Account Status</span>
|
||||
<span class="detail-value">
|
||||
{% if user.is_active_account %}
|
||||
<span class="badge badge-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger">Inactive</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- E-Sports Profile Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-gamepad"></i> E-Sports Profile</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><i class="fas fa-headset"></i> Games</span>
|
||||
<span class="detail-value">
|
||||
{% set games_list = user.get_games_list() %}
|
||||
{% if games_list %}
|
||||
{% for game in games_list %}
|
||||
<span class="badge badge-esport">{{ game }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-muted">Not specified</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><i class="fas fa-chart-line"></i> TRN (Tracker Network)</span>
|
||||
<span class="detail-value">
|
||||
{% if user.trn_username %}
|
||||
<a href="https://tracker.gg" target="_blank" rel="noopener noreferrer" class="trn-link">
|
||||
<i class="fas fa-external-link-alt"></i> {{ user.trn_username }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Not connected</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><i class="fab fa-discord"></i> Discord</span>
|
||||
<span class="detail-value">
|
||||
{% if user.discord_username %}
|
||||
{{ user.discord_username }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not connected</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label"><i class="fas fa-link"></i> League OS</span>
|
||||
<span class="detail-value">
|
||||
{% if user.league_os_profile %}
|
||||
<a href="{{ user.league_os_profile }}" target="_blank" rel="noopener noreferrer" class="trn-link">
|
||||
<i class="fas fa-external-link-alt"></i> {{ user.league_os_profile }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Not connected</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-chart-bar"></i> My Statistics</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if user.role == 'player' %}
|
||||
<div class="profile-stats">
|
||||
<div class="stat-item">
|
||||
<h4>{{ user.tryout_registrations.count() }}</h4>
|
||||
<p>Tryouts Registered</p>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<h4>{{ user.evaluations_received.count() }}</h4>
|
||||
<p>Evaluations Received</p>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<h4>{{ user.team_assignments.count() }}</h4>
|
||||
<p>Team Assignments</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif user.can_evaluate() %}
|
||||
<div class="profile-stats">
|
||||
<div class="stat-item">
|
||||
<h4>{{ user.evaluations_given.count() }}</h4>
|
||||
<p>Evaluations Given</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">No statistics available for this role.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Register - TryoutPro{% endblock %}
|
||||
{% block auth_content %}
|
||||
<form method="POST" action="{{ url_for('auth.register') }}" class="auth-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="form-group">
|
||||
<label for="full_name"><i class="fas fa-id-card"></i> Full Name</label>
|
||||
<input type="text" id="full_name" name="full_name" placeholder="Enter your full name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="username"><i class="fas fa-user"></i> Username</label>
|
||||
<input type="text" id="username" name="username" placeholder="Choose a username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email"><i class="fas fa-envelope"></i> Email</label>
|
||||
<input type="email" id="email" name="email" placeholder="Enter your email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="phone"><i class="fas fa-phone"></i> Phone (Optional)</label>
|
||||
<input type="tel" id="phone" name="phone" placeholder="Enter your phone number">
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-gamepad"></i> E-Sports Profile</h4>
|
||||
<p class="text-muted small">Set up your competitive gaming profile for tryouts.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label><i class="fas fa-headset"></i> Games You Play</label>
|
||||
<div class="checkbox-grid">
|
||||
{% for game in esport_games %}
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="games" value="{{ game }}">
|
||||
<span>{{ game }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<small class="form-text text-muted">Select all games you're signing in for.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="trn_username"><i class="fas fa-chart-line"></i> TRN (Tracker Network) Username</label>
|
||||
<input type="text" id="trn_username" name="trn_username" placeholder="e.g. YourTrackerGGUsername">
|
||||
<small class="form-text text-muted">Your public Tracker Network profile name. Others can click it to view your stats.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="discord_username"><i class="fab fa-discord"></i> Discord Username</label>
|
||||
<input type="text" id="discord_username" name="discord_username" placeholder="e.g. YourName#1234">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="league_os_profile"><i class="fas fa-link"></i> League OS Connection</label>
|
||||
<input type="text" id="league_os_profile" name="league_os_profile" placeholder="League OS profile link or ID">
|
||||
<small class="form-text text-muted">Connect your League OS profile for organized play.</small>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
<div class="form-group">
|
||||
<label for="password"><i class="fas fa-lock"></i> Password</label>
|
||||
<input type="password" id="password" name="password" placeholder="Create a password" required minlength="6">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm_password"><i class="fas fa-check-circle"></i> Confirm Password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" placeholder="Confirm your password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">Create Account</button>
|
||||
<p class="auth-link">Already have an account? <a href="{{ url_for('auth.login') }}">Sign in</a></p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,230 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Teams - TryoutPro{% endblock %}
|
||||
{% block page_title %}Teams{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / Teams</span>{% endblock %}
|
||||
|
||||
{% block header_actions %}
|
||||
{% if can_manage %}
|
||||
<button class="btn btn-primary" onclick="showCreateForm()">
|
||||
<i class="fas fa-plus"></i> New Team
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if can_manage %}
|
||||
<div id="createTeamForm" class="card mb-4 {% if not form_visible %}hidden{% endif %}">
|
||||
<div class="card-header">
|
||||
<h3>Create New Team</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('teams.create_team') }}" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="name">Team Name</label>
|
||||
<input type="text" id="name" name="name" placeholder="e.g., Varsity, JV, U14" required>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="coach_id">Assigned Coach</label>
|
||||
<select id="coach_id" name="coach_id" class="form-select">
|
||||
<option value="">-- No coach assigned --</option>
|
||||
{% for coach in coaches %}
|
||||
<option value="{{ coach.id }}">{{ coach.full_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="hideCreateForm()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create Team</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for team in teams %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-users-cog"></i> {{ team.name }}</h3>
|
||||
<div class="card-actions">
|
||||
{% if team.coach %}
|
||||
<span class="text-muted">Coach: {{ team.coach.full_name }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">No coach assigned</span>
|
||||
{% endif %}
|
||||
{% if can_manage %}
|
||||
<button class="btn btn-sm btn-outline edit-team-btn" data-team-id="{{ team.id }}" data-team-name="{{ team.name }}" data-coach-id="{{ team.coach_id or '' }}">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
<form method="POST" action="{{ url_for('teams.delete_team', team_id=team.id) }}" class="inline-form" onsubmit="return confirm('Delete team {{ team.name }}? This will unassign it from any linked tryouts.')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-sm btn-danger">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Player</th>
|
||||
<th>Position / Role</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
{% if can_manage %}
|
||||
<th>Actions</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for player in team.players %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-mini">
|
||||
<div class="avatar-sm">{{ player.full_name[:2] | upper }}</div>
|
||||
<span>{{ player.full_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ player.role | capitalize }}</td>
|
||||
<td>{{ player.email }}</td>
|
||||
<td>{{ player.phone or '-' }}</td>
|
||||
{% if can_manage %}
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('teams.remove_player', team_id=team.id, player_id=player.id) }}" class="inline-form" onsubmit="return confirm('Remove {{ player.full_name }} from {{ team.name }}?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-sm btn-danger">
|
||||
<i class="fas fa-user-minus"></i> Remove
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="{% if can_manage %}5{% else %}4{% endif %}" class="text-center">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-users-slash"></i>
|
||||
<h4>No players assigned</h4>
|
||||
{% if can_manage %}
|
||||
<p>Add players to this team using the form below.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if can_manage %}
|
||||
<hr class="my-3">
|
||||
<div class="add-player-section">
|
||||
<h5 class="mb-2"><i class="fas fa-user-plus"></i> Add Player to {{ team.name }}</h5>
|
||||
<form method="POST" action="{{ url_for('teams.add_player', team_id=team.id) }}" class="form-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<select name="player_id" class="form-select" required>
|
||||
<option value="">-- Select a player --</option>
|
||||
{% for p in all_players %}
|
||||
{% if p.team_id != team.id %}
|
||||
<option value="{{ p.id }}">{{ p.full_name }} {% if p.org_team %}(currently in {{ p.org_team.name }}){% endif %}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-primary ml-2">
|
||||
<i class="fas fa-plus"></i> Add to Team
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-users-cog"></i>
|
||||
<h3>No teams yet</h3>
|
||||
{% if can_manage %}
|
||||
<p>Create organization teams and assign coaches to manage tryouts.</p>
|
||||
<button class="btn btn-primary" onclick="showCreateForm()">Create Team</button>
|
||||
{% else %}
|
||||
<p>There are no teams to display.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Edit Team Modal -->
|
||||
<div id="editTeamModal" class="modal hidden">
|
||||
<div class="modal-backdrop" onclick="hideEditForm()"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Edit Team</h3>
|
||||
<button class="modal-close" onclick="hideEditForm()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editTeamForm" method="POST" action="" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="form-group">
|
||||
<label for="edit_name">Team Name</label>
|
||||
<input type="text" id="edit_name" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit_coach_id">Assigned Coach</label>
|
||||
<select id="edit_coach_id" name="coach_id" class="form-select">
|
||||
<option value="">-- No coach assigned --</option>
|
||||
{% for coach in coaches %}
|
||||
<option value="{{ coach.id }}">{{ coach.full_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="hideEditForm()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showCreateForm() {
|
||||
document.getElementById('createTeamForm').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideCreateForm() {
|
||||
document.getElementById('createTeamForm').classList.add('hidden');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.edit-team-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var teamId = this.getAttribute('data-team-id');
|
||||
var teamName = this.getAttribute('data-team-name');
|
||||
var coachId = this.getAttribute('data-coach-id');
|
||||
document.getElementById('editTeamForm').action = '/teams/' + teamId + '/edit';
|
||||
document.getElementById('edit_name').value = teamName;
|
||||
document.getElementById('edit_coach_id').value = coachId || '';
|
||||
document.getElementById('editTeamModal').classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function showEditForm(teamId, teamName, coachId) {
|
||||
document.getElementById('editTeamForm').action = '/teams/' + teamId + '/edit';
|
||||
document.getElementById('edit_name').value = teamName;
|
||||
document.getElementById('edit_coach_id').value = coachId || '';
|
||||
document.getElementById('editTeamModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideEditForm() {
|
||||
document.getElementById('editTeamModal').classList.add('hidden');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,75 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Tryouts - TryoutPro{% endblock %}
|
||||
{% block page_title %}Tryouts{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / Tryouts</span>{% endblock %}
|
||||
|
||||
{% block header_actions %}
|
||||
{% if current_user.can_manage_tryouts() %}
|
||||
<a href="{{ url_for('tryouts.create_tryout') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> New Tryout
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Target Team</th>
|
||||
<th>Date</th>
|
||||
<th>Location</th>
|
||||
<th>Status</th>
|
||||
<th>Players</th>
|
||||
<th>Evaluations</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tryout in tryouts %}
|
||||
<tr>
|
||||
<td class="cell-title">{{ tryout.title }}</td>
|
||||
<td>{{ tryout.target_org_team.name if tryout.target_org_team else '-' }}</td>
|
||||
<td>{{ tryout.date.strftime('%b %d, %Y') }}</td>
|
||||
<td>{{ tryout.location or 'TBD' }}</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ tryout.status }}">
|
||||
{{ tryout.status | replace('_', ' ') | title }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ tryout.registrations.count() }}</td>
|
||||
<td>{{ tryout.evaluations.count() }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}" class="btn btn-sm btn-outline">
|
||||
<i class="fas fa-eye"></i> View
|
||||
</a>
|
||||
{% if current_user.can_evaluate() %}
|
||||
<a href="{{ url_for('evaluations.players_to_evaluate', tryout_id=tryout.id) }}" class="btn btn-sm btn-primary">
|
||||
<i class="fas fa-clipboard-check"></i> Evaluate
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-calendar-times"></i>
|
||||
<h3>No tryouts found</h3>
|
||||
{% if current_user.can_manage_tryouts() %}
|
||||
<p>Get started by creating a new tryout.</p>
|
||||
<a href="{{ url_for('tryouts.create_tryout') }}" class="btn btn-primary">Create Tryout</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,68 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}Manage Users - TryoutPro{% endblock %}
|
||||
{% block page_title %}Manage Users{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / Users</span>{% endblock %}
|
||||
|
||||
{% block header_actions %}
|
||||
<a href="{{ url_for('users.create_user') }}" class="btn btn-primary">
|
||||
<i class="fas fa-user-plus"></i> Add User
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Joined</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-mini">
|
||||
<div class="avatar-sm">{{ u.full_name[:2] | upper }}</div>
|
||||
<span>{{ u.full_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.email }}</td>
|
||||
<td><span class="badge badge-{{ u.role }}">{{ u.role | capitalize }}</span></td>
|
||||
<td>
|
||||
{% if u.is_active_account %}
|
||||
<span class="badge badge-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ u.created_at.strftime('%m/%d/%Y') }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('users.edit_user', user_id=u.id) }}" class="btn btn-sm btn-outline">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
{% if u.id != current_user.id %}
|
||||
<form method="POST" action="{{ url_for('users.delete_user', user_id=u.id) }}" class="inline-form" onsubmit="return confirm('Delete {{ u.full_name }}? This cannot be undone.');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-sm btn-danger">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,278 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
{% block title %}{{ tryout.title }} - TryoutPro{% endblock %}
|
||||
{% block page_title %}{{ tryout.title }}{% endblock %}
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('tryouts.list_tryouts') }}">Tryouts</a> / {{ tryout.title }}</span>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="tryout-detail">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h3>Tryout Details</h3>
|
||||
<div class="card-actions">
|
||||
{% if current_user.can_manage_tryouts() %}
|
||||
<form method="POST" action="{{ url_for('tryouts.update_status', tryout_id=tryout.id) }}" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<select name="status" onchange="this.form.submit()" class="form-select">
|
||||
<option value="upcoming" {% if tryout.status == 'upcoming' %}selected{% endif %}>Upcoming</option>
|
||||
<option value="in_progress" {% if tryout.status == 'in_progress' %}selected{% endif %}>In Progress</option>
|
||||
<option value="completed" {% if tryout.status == 'completed' %}selected{% endif %}>Completed</option>
|
||||
</select>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if can_edit %}
|
||||
<a href="{{ url_for('tryouts.edit_tryout', tryout_id=tryout.id) }}" class="btn btn-sm btn-primary">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Date</span>
|
||||
<span class="detail-value">{{ tryout.date.strftime('%A, %B %d, %Y') }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Location</span>
|
||||
<span class="detail-value">{{ tryout.location or 'Not specified' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value">
|
||||
<span class="badge badge-{{ tryout.status }}">{{ tryout.status | replace('_', ' ') | title }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Target Team</span>
|
||||
<span class="detail-value">{{ tryout.target_org_team.name if tryout.target_org_team else 'Not specified' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Registered Players</span>
|
||||
<span class="detail-value">{{ registered_players | length }}</span>
|
||||
</div>
|
||||
{% if tryout.description %}
|
||||
<div class="detail-item full-width">
|
||||
<span class="detail-label">Description</span>
|
||||
<span class="detail-value">{{ tryout.description }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user.role == 'player' and not is_registered and tryout.status in ['upcoming', 'in_progress'] %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-body text-center">
|
||||
<form method="POST" action="{{ url_for('tryouts.register_for_tryout', tryout_id=tryout.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-user-plus"></i> Register for this Tryout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.can_manage_tryouts() %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-user-plus"></i> Add Player to Tryout</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('tryouts.register_player', tryout_id=tryout.id) }}" class="form-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<select name="player_id" class="form-select" required>
|
||||
<option value="">-- Select a player --</option>
|
||||
{% for p in all_players %}
|
||||
{% set is_already = registrations | selectattr('player_id', 'equalto', p.id) | list | length > 0 %}
|
||||
<option value="{{ p.id }}" {% if is_already %}disabled class="text-muted"{% endif %}>{{ p.full_name }}{% if is_already %} (already registered){% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-primary ml-2">
|
||||
<i class="fas fa-plus"></i> Register Player
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-users"></i> Registered Players ({{ registered_players | length }})</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Player</th>
|
||||
<th>Status</th>
|
||||
{% if current_user.can_evaluate() %}
|
||||
<th>Evaluation</th>
|
||||
<th>Actions</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in registered_players %}
|
||||
{% set reg = registrations | selectattr('player_id', 'equalto', p.id) | first %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-mini">
|
||||
<div class="avatar-sm">{{ p.full_name[:2] | upper }}</div>
|
||||
<span>{{ p.full_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if current_user.can_manage_tryouts() %}
|
||||
<form method="POST" action="{{ url_for('tryouts.update_registration_status', tryout_id=tryout.id, player_id=p.id) }}" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<select name="status" onchange="this.form.submit()" class="form-select form-select-sm">
|
||||
<option value="registered" {% if reg.status == 'registered' %}selected{% endif %}>Registered</option>
|
||||
<option value="attended" {% if reg.status == 'attended' %}selected{% endif %}>Attended</option>
|
||||
<option value="no_show" {% if reg.status == 'no_show' %}selected{% endif %}>No Show</option>
|
||||
</select>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="badge badge-{{ reg.status }}">{{ reg.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if current_user.can_evaluate() %}
|
||||
<td>
|
||||
{% if player_eval_status.get(p.id) %}
|
||||
<span class="badge badge-success">Evaluated</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning">Pending</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('evaluations.evaluate_player', tryout_id=tryout.id, player_id=p.id) }}" class="btn btn-sm btn-primary">
|
||||
<i class="fas fa-edit"></i> {% if player_eval_status.get(p.id) %}Edit{% else %}Evaluate{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="{% if current_user.can_evaluate() %}4{% else %}2{% endif %}" class="text-center">
|
||||
No players registered yet.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-users-cog"></i> Teams</h3>
|
||||
{% if current_user.can_manage_tryouts() %}
|
||||
<button class="btn btn-sm btn-primary" onclick="showCreateTeam()">
|
||||
<i class="fas fa-plus"></i> New Team
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if current_user.can_manage_tryouts() %}
|
||||
<div id="createTeamForm" class="hidden mb-3">
|
||||
<form method="POST" action="{{ url_for('tryouts.create_team', tryout_id=tryout.id) }}" class="form-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="text" name="team_name" placeholder="Team name" required class="form-input mr-2">
|
||||
<button type="submit" class="btn btn-sm btn-success">Create</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="hideCreateTeam()">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for team in team_data %}
|
||||
<div class="team-card mb-3">
|
||||
<h4 class="team-name">{{ team.team.name }}</h4>
|
||||
<ul class="team-members">
|
||||
{% for member in team.members %}
|
||||
<li>
|
||||
<div class="avatar-sm">{{ member.player.full_name[:2] | upper }}</div>
|
||||
<span>{{ member.player.full_name }}</span>
|
||||
{% if member.position %}
|
||||
<span class="position-tag">{{ member.position }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="text-muted">No players assigned yet.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if current_user.can_manage_tryouts() and registered_players %}
|
||||
<form method="POST" action="{{ url_for('tryouts.add_to_team', tryout_id=tryout.id, team_id=team.team.id) }}" class="form-inline mt-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<select name="player_id" class="form-select" required>
|
||||
<option value="">Select player...</option>
|
||||
{% for p in registered_players %}
|
||||
{% if p.id not in team.members | map(attribute='player') | map(attribute='id') | list %}
|
||||
<option value="{{ p.id }}">{{ p.full_name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="text" name="position" placeholder="Position" class="form-input mx-2">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Add</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted text-center">No teams created yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-star"></i> Evaluation Summary</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Player</th>
|
||||
<th>Evaluator</th>
|
||||
<th>Speed</th>
|
||||
<th>Agility</th>
|
||||
<th>Technique</th>
|
||||
<th>Teamwork</th>
|
||||
<th>Attitude</th>
|
||||
<th>Overall</th>
|
||||
<th>Recommendation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for eval in evaluations %}
|
||||
<tr>
|
||||
<td>{{ eval.player.full_name }}</td>
|
||||
<td>{{ eval.evaluator.full_name }}</td>
|
||||
<td>{{ eval.speed_score or '-' }}</td>
|
||||
<td>{{ eval.agility_score or '-' }}</td>
|
||||
<td>{{ eval.technique_score or '-' }}</td>
|
||||
<td>{{ eval.teamwork_score or '-' }}</td>
|
||||
<td>{{ eval.attitude_score or '-' }}</td>
|
||||
<td><span class="score">{{ eval.overall_score or '-' }}</span></td>
|
||||
<td>{{ eval.position_recommendation or '-' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center">No evaluations yet.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showCreateTeam() { document.getElementById('createTeamForm').classList.remove('hidden'); }
|
||||
function hideCreateTeam() { document.getElementById('createTeamForm').classList.add('hidden'); }
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user