import os from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file from flask_login import login_required, current_user from extensions import db, hash_password from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility, UserGamertag, GAME_PLATFORMS, Contract, OrgTeam from werkzeug.utils import secure_filename from datetime import datetime, timedelta 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('//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, game_platforms=GAME_PLATFORMS) selected_games = request.form.getlist('games') 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.discord_username = discord_username or None user.league_os_profile = league_os_profile or None # Handle gamertags - save or delete based on form input # First, get all existing gamertags for this user existing_gamertags = {gt.game: gt for gt in user.gamertags} for game in selected_games: gamertag = request.form.get(f'gamertag_{game}', '').strip() platform = request.form.get(f'platform_{game}', '').strip() if GAME_PLATFORMS.get(game) else None existing = existing_gamertags.get(game) if gamertag: if existing: existing.gamertag = gamertag existing.platform = platform else: gt = UserGamertag(user_id=user.id, game=game, gamertag=gamertag, platform=platform) db.session.add(gt) elif existing: db.session.delete(existing) # Remove gamertags for games that are no longer selected for game in existing_gamertags: if game not in selected_games: db.session.delete(existing_gamertags[game]) 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')) user_gamertags = {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} for gt in user.gamertags} return render_template('pages/edit_user.html', user=user, roles=ROLES, esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, user_gamertags=user_gamertags) @users_bp.route('//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(): # Get contracts ordered by uploaded_at desc for the current user contracts = None if current_user.role == 'player': contracts = Contract.query.filter_by(player_id=current_user.id).order_by(Contract.uploaded_at.desc()).all() return render_template('pages/profile.html', user=current_user, contracts=contracts) @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') selected_games = request.form.getlist('games') discord_username = request.form.get('discord_username', '').strip() league_os_profile = request.form.get('league_os_profile', '').strip() 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, game_platforms=GAME_PLATFORMS) 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.discord_username = discord_username or None current_user.league_os_profile = league_os_profile or None # Handle gamertags - save or delete based on form input # First, get all existing gamertags for this user existing_gamertags = {gt.game: gt for gt in current_user.gamertags} for game in selected_games: gamertag = request.form.get(f'gamertag_{game}', '').strip() platform = request.form.get(f'platform_{game}', '').strip() if GAME_PLATFORMS.get(game) else None existing = existing_gamertags.get(game) if gamertag: if existing: existing.gamertag = gamertag existing.platform = platform else: gt = UserGamertag(user_id=current_user.id, game=game, gamertag=gamertag, platform=platform) db.session.add(gt) elif existing: db.session.delete(existing) # Remove gamertags for games that are no longer selected for game in existing_gamertags: if game not in selected_games: db.session.delete(existing_gamertags[game]) 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')) user_gamertags = {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} for gt in current_user.gamertags} return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, user_gamertags=user_gamertags) # Day names for disponibility DAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] def add_30_minutes(t): """Add 30 minutes to a time object.""" return (datetime.combine(datetime.today(), t) + timedelta(minutes=30)).time() @users_bp.route('/disponibilities') @login_required def get_disponibilities(): """API endpoint to get all player disponibilities for scheduling.""" # Only managers and above can view disponibilities for scheduling if not current_user.can_manage_teams() and not current_user.can_schedule_matches(): return jsonify({'error': 'Unauthorized'}), 403 players = User.query.filter_by(role='player', is_active_account=True).order_by(User.full_name).all() result = {} for player in players: disponibilities = list(player.disponibilities) result[player.id] = { 'full_name': player.full_name, 'disponibilities': [ { 'id': d.id, 'day_of_week': d.day_of_week, 'day_name': DAY_NAMES[d.day_of_week], 'start_time': d.start_time.strftime('%H:%M'), 'end_time': d.end_time.strftime('%H:%M') } for d in disponibilities ] } return jsonify(result) @users_bp.route('/disponibilities/my') @login_required def get_my_disponibilities(): """API endpoint for players to get their own disponibilities.""" disponibilities = PlayerDisponibility.query.filter_by(player_id=current_user.id).all() # Group by day for easier display result = {} for d in disponibilities: day = d.day_of_week if day not in result: result[day] = [] result[day].append({ 'id': d.id, 'day_of_week': d.day_of_week, 'day_name': DAY_NAMES[d.day_of_week], 'start_time': d.start_time.strftime('%H:%M'), 'end_time': d.end_time.strftime('%H:%M') }) return jsonify(result) @users_bp.route('/disponibilities/add', methods=['POST']) @login_required def add_disponibility(): """Add a disponibility block for the current player.""" day_of_week = request.form.get('day_of_week', type=int) start_time_str = request.form.get('start_time') if day_of_week is None or day_of_week < 0 or day_of_week > 6: return jsonify({'error': 'Invalid day of week'}), 400 try: start_time = datetime.strptime(start_time_str, '%H:%M').time() except (ValueError, TypeError): return jsonify({'error': 'Invalid time format'}), 400 end_time = add_30_minutes(start_time) disponibility = PlayerDisponibility( player_id=current_user.id, day_of_week=day_of_week, start_time=start_time, end_time=end_time ) db.session.add(disponibility) db.session.commit() return jsonify({ 'id': disponibility.id, 'day_of_week': disponibility.day_of_week, 'day_name': DAY_NAMES[disponibility.day_of_week], 'start_time': disponibility.start_time.strftime('%H:%M'), 'end_time': disponibility.end_time.strftime('%H:%M') }) @users_bp.route('/disponibilities/add_bulk', methods=['POST']) @login_required def add_disponibilities_bulk(): """Add multiple disponibility blocks at once (for grid selection).""" data = request.get_json() slots = data.get('slots', []) # List of {day_of_week, start_time} created = [] for slot in slots: day_of_week = slot.get('day_of_week') start_time_str = slot.get('start_time') if day_of_week is None or day_of_week < 0 or day_of_week > 6: continue try: start_time = datetime.strptime(start_time_str, '%H:%M').time() except (ValueError, TypeError): continue end_time = add_30_minutes(start_time) # Check if this slot already exists for this player existing = PlayerDisponibility.query.filter_by( player_id=current_user.id, day_of_week=day_of_week, start_time=start_time ).first() if not existing: disponibility = PlayerDisponibility( player_id=current_user.id, day_of_week=day_of_week, start_time=start_time, end_time=end_time ) db.session.add(disponibility) db.session.flush() created.append({ 'id': disponibility.id, 'day_of_week': disponibility.day_of_week, 'day_name': DAY_NAMES[disponibility.day_of_week], 'start_time': disponibility.start_time.strftime('%H:%M'), 'end_time': disponibility.start_time.strftime('%H:%M') }) db.session.commit() return jsonify({'success': True, 'created': created}) @users_bp.route('/disponibilities/clear', methods=['POST']) @login_required def clear_disponibilities(): """Clear all disponibilities for the current player (for resetting).""" PlayerDisponibility.query.filter_by(player_id=current_user.id).delete() db.session.commit() return jsonify({'success': True}) @users_bp.route('/disponibilities//delete', methods=['POST']) @login_required def delete_disponibility(disponibility_id): """Delete a disponibility block.""" disponibility = PlayerDisponibility.query.get_or_404(disponibility_id) # Only the owner can delete their disponibility if disponibility.player_id != current_user.id: return jsonify({'error': 'Unauthorized'}), 403 db.session.delete(disponibility) db.session.commit() return jsonify({'success': True}) # Contract Dropbox Functions def can_manage_player_contract(user, player_id): """Check if a user can upload contracts for a specific player.""" # President can manage all contracts if user.role == 'president': return True # Manager can upload contracts for any player if user.role == 'manager': return True # Coach can upload contracts only for players on their team if user.role == 'coach': org_team = OrgTeam.query.filter_by(coach_id=user.id).first() if org_team: player = User.query.get(player_id) if player and player.team_id == org_team.id: return True return False @users_bp.route('/contracts') @login_required def list_contracts(): """View contracts for the current user (player) or players they manage.""" contracts = None if current_user.role == 'player': # Players see their own contracts contracts = Contract.query.filter_by(player_id=current_user.id).order_by(Contract.uploaded_at.desc()).all() elif current_user.role in ['president', 'manager', 'coach']: # Superiors see contracts for players on their teams players = [] if current_user.role == 'coach': org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() if org_team: players = User.query.filter_by(role='player', team_id=org_team.id).all() else: # president or manager players = User.query.filter_by(role='player').all() if players: player_ids = [p.id for p in players] contracts = Contract.query.filter(Contract.player_id.in_(player_ids)).order_by(Contract.uploaded_at.desc()).all() return render_template('pages/contracts.html', contracts=contracts, players=players if current_user.role in ['president', 'manager', 'coach'] else None) @users_bp.route('/contracts/upload', methods=['GET', 'POST']) @login_required def upload_contract(): """Upload a contract for a player.""" if current_user.role not in ['president', 'manager', 'coach']: flash('Only presidents, managers, and coaches can upload contracts.', 'danger') return redirect(url_for('users.list_contracts')) # Get players this user can manage if current_user.role == 'coach': org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() players = User.query.filter_by(role='player', team_id=org_team.id).all() if org_team else [] else: players = User.query.filter_by(role='player').all() if request.method == 'POST': player_id = request.form.get('player_id', type=int) notes = request.form.get('notes', '').strip() if not can_manage_player_contract(current_user, player_id): flash('You do not have permission to upload a contract for this player.', 'danger') return redirect(url_for('users.upload_contract')) if 'contract_file' not in request.files: flash('No file selected.', 'danger') return redirect(url_for('users.upload_contract')) file = request.files['contract_file'] if file.filename == '': flash('No file selected.', 'danger') return redirect(url_for('users.upload_contract')) # Create upload directory upload_dir = os.path.join(os.getcwd(), 'documents', 'contrats signés') os.makedirs(upload_dir, exist_ok=True) # Get player info for folder structure player = User.query.get_or_404(player_id) team = OrgTeam.query.get(player.team_id) if player.team_id else None # Create team folder if team exists if team: team_folder = os.path.join(upload_dir, secure_filename(team.name)) os.makedirs(team_folder, exist_ok=True) final_dir = team_folder else: final_dir = upload_dir # Generate unique filename original_filename = secure_filename(file.filename) timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') stored_filename = f"{secure_filename(player.full_name)}_{timestamp}_{original_filename}" stored_filename = stored_filename.replace(' ', '_') # Save file file_path = os.path.join(final_dir, stored_filename) file.save(file_path) # Create database record contract = Contract( player_id=player_id, team_id=player.team_id, uploaded_by_id=current_user.id, original_filename=original_filename, stored_filename=stored_filename, file_path=file_path, notes=notes if notes else None ) db.session.add(contract) db.session.commit() flash(f'Contract uploaded successfully for {player.full_name}!', 'success') return redirect(url_for('users.list_contracts')) return render_template('pages/upload_contract.html', players=players) @users_bp.route('/contracts//upload_signed', methods=['POST']) @login_required def upload_signed_contract(contract_id): """Upload a signed contract.""" contract = Contract.query.get_or_404(contract_id) if not contract.can_upload_signed(current_user): flash('Only the player can upload their signed contract.', 'danger') return redirect(url_for('users.list_contracts')) if 'signed_file' not in request.files: flash('No file selected.', 'danger') return redirect(url_for('users.list_contracts')) file = request.files['signed_file'] if file.filename == '': flash('No file selected.', 'danger') return redirect(url_for('users.list_contracts')) # Generate unique filename for signed contract original_filename = secure_filename(file.filename) signed_filename = f"signed_{contract.stored_filename}" # Save file file.save(contract.file_path.replace(contract.stored_filename, signed_filename)) # Update contract record contract.signed_filename = signed_filename contract.signed_file_path = contract.file_path.replace(contract.stored_filename, signed_filename) contract.status = 'signed' contract.signed_at = datetime.utcnow() db.session.commit() flash('Signed contract uploaded successfully!', 'success') return redirect(url_for('users.list_contracts')) @users_bp.route('/contracts//download') @login_required def download_contract(contract_id): """Download a contract file.""" contract = Contract.query.get_or_404(contract_id) if not contract.can_view(current_user): flash('You do not have permission to download this contract.', 'danger') return redirect(url_for('users.list_contracts')) return send_file(contract.file_path, as_attachment=True, download_name=contract.original_filename) @users_bp.route('/contracts//download_signed') @login_required def download_signed_contract(contract_id): """Download a signed contract file.""" contract = Contract.query.get_or_404(contract_id) if not contract.can_view(current_user): flash('You do not have permission to download this contract.', 'danger') return redirect(url_for('users.list_contracts')) if not contract.signed_file_path: flash('No signed contract available.', 'danger') return redirect(url_for('users.list_contracts')) return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename)