régler problème avec le bot discord et ajouter un panneau pour gérer les one on one (accepter ,refuser, confirmer)

This commit is contained in:
cedrick2711
2026-07-29 20:24:10 -04:00
parent 962e621fee
commit 19c6740edb
6 changed files with 483 additions and 58 deletions
+1 -1
View File
@@ -358,7 +358,7 @@ def create_app():
# Start the Discord bot for notifications # Start the Discord bot for notifications
try: try:
from app.discord_bot import start_bot from app.discord_bot import start_bot
start_bot() start_bot(flask_app=app)
except Exception as e: except Exception as e:
app.logger.warning('Could not start Discord bot: %s', e) app.logger.warning('Could not start Discord bot: %s', e)
+184 -56
View File
@@ -10,6 +10,7 @@ import os
import logging import logging
import asyncio import asyncio
import threading import threading
import traceback
from datetime import datetime, timedelta from datetime import datetime, timedelta
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from queue import Queue, Empty from queue import Queue, Empty
@@ -37,15 +38,17 @@ class TeamTryoutsBot(commands.Bot):
Handles One on One requests, schedule additions, and daily reminders. Handles One on One requests, schedule additions, and daily reminders.
""" """
def __init__(self): def __init__(self, flask_app=None):
intents = Intents.default() intents = Intents.default()
intents.message_content = True intents.message_content = True
intents.dm_messages = True intents.dm_messages = True
intents.dm_reactions = True intents.dm_reactions = True
intents.reactions = True intents.reactions = True
intents.guilds = True intents.guilds = True
intents.members = True
super().__init__(command_prefix='!', intents=intents) super().__init__(command_prefix='!', intents=intents)
self.flask_app = flask_app
self.pending_requests = {} # Maps message_id to {type, id} for reaction handling self.pending_requests = {} # Maps message_id to {type, id} for reaction handling
self.message_queue = Queue() # Thread-safe queue for messages from Flask self.message_queue = Queue() # Thread-safe queue for messages from Flask
self.scheduler = AsyncIOScheduler() self.scheduler = AsyncIOScheduler()
@@ -57,7 +60,11 @@ class TeamTryoutsBot(commands.Bot):
async def on_ready(self): async def on_ready(self):
"""Log when the bot is ready and start background tasks.""" """Log when the bot is ready and start background tasks."""
logger.info(f'TeamTryoutsBot is ready! Logged in as {self.user}') try:
guilds = [g.name for g in self.guilds]
logger.info(f'TeamTryoutsBot is ready! Logged in as {self.user} | Guilds: {guilds}')
except Exception:
logger.info(f'TeamTryoutsBot is ready! Logged in as {self.user}')
# Start the queue processing task # Start the queue processing task
self.loop.create_task(self.process_queue()) self.loop.create_task(self.process_queue())
@@ -92,10 +99,16 @@ class TeamTryoutsBot(commands.Bot):
if item.get('type') == 'one_on_one_request': if item.get('type') == 'one_on_one_request':
await self._send_one_on_one_dm(**item['data']) await self._send_one_on_one_dm(**item['data'])
elif item.get('type') == 'schedule_addition': elif item.get('type') == 'schedule_addition':
await self._send_schedule_notification(**item['data']) if self.flask_app:
with self.flask_app.app_context():
await self._send_schedule_notification(**item['data'])
else:
await self._send_schedule_notification(**item['data'])
elif item.get('type') == 'one_on_one_response':
await self._send_one_on_one_response_dm(**item['data'])
except Exception as e: except Exception as e:
logger.error(f"Error processing queue: {e}") logger.error(f"Error processing queue: {e}\n{traceback.format_exc()}")
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
def is_dm_channel(self, channel) -> bool: def is_dm_channel(self, channel) -> bool:
@@ -123,14 +136,30 @@ class TeamTryoutsBot(commands.Bot):
if emoji_str == CHECK_EMOJI: if emoji_str == CHECK_EMOJI:
if handler_type == 'one_on_one': if handler_type == 'one_on_one':
await self.handle_one_on_one_approve(user, message_id, request_id, reaction.message) if self.flask_app:
with self.flask_app.app_context():
await self.handle_one_on_one_approve(user, message_id, request_id, reaction.message)
else:
await self.handle_one_on_one_approve(user, message_id, request_id, reaction.message)
elif handler_type == 'schedule_addition': elif handler_type == 'schedule_addition':
await self.handle_attendance_confirm(user, message_id, request_id, reaction.message) if self.flask_app:
with self.flask_app.app_context():
await self.handle_attendance_confirm(user, message_id, request_id, reaction.message)
else:
await self.handle_attendance_confirm(user, message_id, request_id, reaction.message)
elif emoji_str == CROSS_EMOJI: elif emoji_str == CROSS_EMOJI:
if handler_type == 'one_on_one': if handler_type == 'one_on_one':
await self.handle_one_on_one_reject(user, message_id, request_id, reaction.message) if self.flask_app:
with self.flask_app.app_context():
await self.handle_one_on_one_reject(user, message_id, request_id, reaction.message)
else:
await self.handle_one_on_one_reject(user, message_id, request_id, reaction.message)
elif handler_type == 'schedule_addition': elif handler_type == 'schedule_addition':
await self.handle_attendance_decline(user, message_id, request_id, reaction.message) if self.flask_app:
with self.flask_app.app_context():
await self.handle_attendance_decline(user, message_id, request_id, reaction.message)
else:
await self.handle_attendance_decline(user, message_id, request_id, reaction.message)
async def _send_one_on_one_dm(self, coach_name: str, coach_discord_id: str, player_name: str, async def _send_one_on_one_dm(self, coach_name: str, coach_discord_id: str, player_name: str,
team_name: str, date_str: str, start_time: str, end_time: str, team_name: str, date_str: str, start_time: str, end_time: str,
@@ -188,7 +217,7 @@ class TeamTryoutsBot(commands.Bot):
""" """
try: try:
# Look up the DB user to get their Discord user ID # Look up the DB user to get their Discord user ID
from app.models.models import User as DBUser from app.models import User as DBUser
db_user = DBUser.query.get(user_id) db_user = DBUser.query.get(user_id)
if not db_user: if not db_user:
logger.warning(f"DB user {user_id} not found for schedule notification") logger.warning(f"DB user {user_id} not found for schedule notification")
@@ -234,13 +263,10 @@ class TeamTryoutsBot(commands.Bot):
async def handle_one_on_one_approve(self, coach, message_id, request_id, original_message): async def handle_one_on_one_approve(self, coach, message_id, request_id, original_message):
"""Handle coach approving a One on One request.""" """Handle coach approving a One on One request."""
try: try:
from app.models.models import OneOnOneRequest, db from app.models import OneOnOneRequest
from sqlalchemy.orm import joinedload from app.extensions import db
request = OneOnOneRequest.query.options( request = OneOnOneRequest.query.get(request_id)
joinedload(OneOnOneRequest.player),
joinedload(OneOnOneRequest.coach)
).get(request_id)
if not request: if not request:
return return
@@ -248,11 +274,13 @@ class TeamTryoutsBot(commands.Bot):
await original_message.channel.send("⚠️ You are not the intended recipient.") await original_message.channel.send("⚠️ You are not the intended recipient.")
return return
# Capture data before commit (to avoid expired session issues) # Re-attach to current session (object may be detached across app contexts)
player = request.player request = db.session.merge(request)
# Capture data before commit
player_full_name = request.player.full_name if request.player else 'Unknown'
player_discord_id = request.player.discord_user_id if request.player else None
coach_obj = request.coach coach_obj = request.coach
player_full_name = player.full_name
player_discord_id = player.discord_user_id
request.status = 'approved' request.status = 'approved'
request.responded_at = datetime.utcnow() request.responded_at = datetime.utcnow()
@@ -262,29 +290,27 @@ class TeamTryoutsBot(commands.Bot):
f"✅ You have **approved** the One on One session with {player_full_name}." f"✅ You have **approved** the One on One session with {player_full_name}."
) )
# Pass the pre-fetched data to avoid session expiration issues # Notify player via Discord
await self.notify_player_about_one_on_one_direct( if player_discord_id:
player_discord_id=player_discord_id, await self.notify_player_about_one_on_one_direct(
player_full_name=player_full_name, player_discord_id=player_discord_id,
coach_full_name=coach_obj.full_name, player_full_name=player_full_name,
request=request, coach_full_name=coach_obj.full_name if coach_obj else 'Coach',
approved=True request=request,
) approved=True
)
del self.pending_requests[message_id] del self.pending_requests[message_id]
except Exception as e: except Exception as e:
logger.error(f"Error handling approval: {e}") logger.error(f"Error handling approval: {e}\n{traceback.format_exc()}")
async def handle_one_on_one_reject(self, coach, message_id, request_id, original_message): async def handle_one_on_one_reject(self, coach, message_id, request_id, original_message):
"""Handle coach rejecting a One on One request.""" """Handle coach rejecting a One on One request."""
try: try:
from app.models.models import OneOnOneRequest, db from app.models import OneOnOneRequest
from sqlalchemy.orm import joinedload from app.extensions import db
request = OneOnOneRequest.query.options( request = OneOnOneRequest.query.get(request_id)
joinedload(OneOnOneRequest.player),
joinedload(OneOnOneRequest.coach)
).get(request_id)
if not request: if not request:
return return
@@ -292,11 +318,12 @@ class TeamTryoutsBot(commands.Bot):
await original_message.channel.send("⚠️ You are not the intended recipient.") await original_message.channel.send("⚠️ You are not the intended recipient.")
return return
# Capture data before commit (to avoid expired session issues) # Re-attach to current session (object may be detached across app contexts)
player = request.player request = db.session.merge(request)
player_full_name = request.player.full_name if request.player else 'Unknown'
player_discord_id = request.player.discord_user_id if request.player else None
coach_obj = request.coach coach_obj = request.coach
player_full_name = player.full_name
player_discord_id = player.discord_user_id
refusal_note = None refusal_note = None
try: try:
@@ -320,23 +347,25 @@ class TeamTryoutsBot(commands.Bot):
rejection_msg += "\n\n️ The player has been notified that you are not available." rejection_msg += "\n\n️ The player has been notified that you are not available."
await original_message.channel.send(rejection_msg) await original_message.channel.send(rejection_msg)
await self.notify_player_about_one_on_one_direct( if player_discord_id:
player_discord_id=player_discord_id, await self.notify_player_about_one_on_one_direct(
player_full_name=player_full_name, player_discord_id=player_discord_id,
coach_full_name=coach_obj.full_name, player_full_name=player_full_name,
request=request, coach_full_name=coach_obj.full_name if coach_obj else 'Coach',
approved=False, request=request,
refusal_note=refusal_note approved=False,
) refusal_note=refusal_note
)
del self.pending_requests[message_id] del self.pending_requests[message_id]
except Exception as e: except Exception as e:
logger.error(f"Error handling rejection: {e}") logger.error(f"Error handling rejection: {e}\n{traceback.format_exc()}")
async def handle_attendance_confirm(self, player, message_id, reference_id, original_message): async def handle_attendance_confirm(self, player, message_id, reference_id, original_message):
"""Handle player confirming attendance for a match/tryout.""" """Handle player confirming attendance for a match/tryout."""
try: try:
from app.models.models import MatchParticipant, TryoutRegistration, Match, Tryout, db from app.models import MatchParticipant, TryoutRegistration, Match, Tryout
from app.extensions import db
request_info = self.pending_requests[message_id] request_info = self.pending_requests[message_id]
event_type = request_info.get('event_type') event_type = request_info.get('event_type')
@@ -344,10 +373,12 @@ class TeamTryoutsBot(commands.Bot):
if event_type == 'match': if event_type == 'match':
participant = MatchParticipant.query.get(reference_id) participant = MatchParticipant.query.get(reference_id)
if participant: if participant:
participant = db.session.merge(participant)
participant.attendance_confirmed = True participant.attendance_confirmed = True
elif event_type == 'tryout': elif event_type == 'tryout':
registration = TryoutRegistration.query.get(reference_id) registration = TryoutRegistration.query.get(reference_id)
if registration: if registration:
registration = db.session.merge(registration)
registration.attendance_confirmed = True registration.attendance_confirmed = True
db.session.commit() db.session.commit()
@@ -356,12 +387,13 @@ class TeamTryoutsBot(commands.Bot):
del self.pending_requests[message_id] del self.pending_requests[message_id]
except Exception as e: except Exception as e:
logger.error(f"Error handling attendance confirmation: {e}") logger.error(f"Error handling attendance confirmation: {e}\n{traceback.format_exc()}")
async def handle_attendance_decline(self, player, message_id, reference_id, original_message): async def handle_attendance_decline(self, player, message_id, reference_id, original_message):
"""Handle player declining attendance for a match/tryout.""" """Handle player declining attendance for a match/tryout."""
try: try:
from app.models.models import MatchParticipant, TryoutRegistration, Match, Tryout, db from app.models import MatchParticipant, TryoutRegistration, Match, Tryout
from app.extensions import db
request_info = self.pending_requests[message_id] request_info = self.pending_requests[message_id]
event_type = request_info.get('event_type') event_type = request_info.get('event_type')
@@ -369,10 +401,12 @@ class TeamTryoutsBot(commands.Bot):
if event_type == 'match': if event_type == 'match':
participant = MatchParticipant.query.get(reference_id) participant = MatchParticipant.query.get(reference_id)
if participant: if participant:
participant = db.session.merge(participant)
db.session.delete(participant) db.session.delete(participant)
elif event_type == 'tryout': elif event_type == 'tryout':
registration = TryoutRegistration.query.get(reference_id) registration = TryoutRegistration.query.get(reference_id)
if registration: if registration:
registration = db.session.merge(registration)
registration.status = 'no_show' registration.status = 'no_show'
db.session.commit() db.session.commit()
@@ -381,7 +415,7 @@ class TeamTryoutsBot(commands.Bot):
del self.pending_requests[message_id] del self.pending_requests[message_id]
except Exception as e: except Exception as e:
logger.error(f"Error handling attendance decline: {e}") logger.error(f"Error handling attendance decline: {e}\n{traceback.format_exc()}")
async def notify_player_about_one_on_one(self, request, approved=True, refusal_note=None): async def notify_player_about_one_on_one(self, request, approved=True, refusal_note=None):
"""Send confirmation to player about One on One response. """Send confirmation to player about One on One response.
@@ -471,7 +505,19 @@ class TeamTryoutsBot(commands.Bot):
async def send_daily_reminders(self): async def send_daily_reminders(self):
"""Send daily reminders at 18:00 EDT for events in 24-48 hours.""" """Send daily reminders at 18:00 EDT for events in 24-48 hours."""
try: try:
from app.models.models import Match, Tryout, MatchParticipant, TryoutRegistration, OneOnOneRequest, db if self.flask_app:
with self.flask_app.app_context():
await self._send_daily_reminders_impl()
else:
await self._send_daily_reminders_impl()
except Exception as e:
logger.error(f"Error sending daily reminders: {e}\n{traceback.format_exc()}")
async def _send_daily_reminders_impl(self):
"""Internal implementation of daily reminders with proper app context."""
try:
from app.models import Match, Tryout, MatchParticipant, TryoutRegistration, OneOnOneRequest
from app.extensions import db
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
now = datetime.now(self.timezone) now = datetime.now(self.timezone)
@@ -540,6 +586,56 @@ class TeamTryoutsBot(commands.Bot):
except Exception as e: except Exception as e:
logger.error(f"Error sending tryout reminder: {e}") logger.error(f"Error sending tryout reminder: {e}")
async def _send_one_on_one_response_dm(self, player_discord_id: str, player_full_name: str,
coach_full_name: str, date_str: str, start_time: str,
end_time: str, points: str, approved: bool,
refusal_note: str = None) -> bool:
"""Send a DM to a player notifying them of their One on One request response.
Called from the message queue when a coach accepts/rejects via the web app.
"""
try:
if not player_discord_id:
logger.warning("Cannot send response DM: no player_discord_id")
return False
player_user = await self.fetch_user(int(player_discord_id))
if not player_user:
logger.warning(f"Could not fetch Discord user {player_discord_id}")
return False
if approved:
message = (
"🎉 **One on One Session Confirmed!**\n\n"
f"Your coach **{coach_full_name}** has approved your request:\n"
f"**Date:** {date_str}\n"
f"**Time:** {start_time} - {end_time}\n"
f"**Discussion Points:** {points or 'No specific points provided'}\n\n"
"Please prepare for your session!"
)
else:
if refusal_note:
message = (
"😞 **One on One Session Rejected**\n\n"
f"Your coach **{coach_full_name}** has declined:\n"
f"**Reason:** {refusal_note}\n\n"
"Please try selecting a different time slot."
)
else:
message = (
"😞 **One on One Session Unavailable**\n\n"
f"Your coach **{coach_full_name}** is not available.\n\n"
"Please try selecting a different time slot."
)
await player_user.send(message)
logger.info(f"Sent One on One response DM to player {player_full_name} (approved={approved})")
return True
except Exception as e:
logger.error(f"Error sending One on One response DM: {e}")
return False
async def send_one_on_one_reminder(self, player, session): async def send_one_on_one_reminder(self, player, session):
"""Send One on One reminder to player.""" """Send One on One reminder to player."""
try: try:
@@ -562,11 +658,13 @@ bot_instance = None
bot_thread = None bot_thread = None
def get_bot(): def get_bot(flask_app=None):
"""Get or create the bot instance.""" """Get or create the bot instance."""
global bot_instance global bot_instance
if bot_instance is None: if bot_instance is None:
bot_instance = TeamTryoutsBot() bot_instance = TeamTryoutsBot(flask_app=flask_app)
elif flask_app is not None and bot_instance.flask_app is None:
bot_instance.flask_app = flask_app
return bot_instance return bot_instance
@@ -618,11 +716,41 @@ def send_schedule_notification(user_id: int, event_type: str, event_title: str,
return False return False
def start_bot(): def send_one_on_one_response(player_discord_id: str, player_full_name: str,
coach_full_name: str, date_str: str, start_time: str,
end_time: str, points: str, approved: bool,
refusal_note: str = None) -> bool:
"""Queue a One on One response DM to be sent to the player by the bot.
Called from Flask routes when a coach accepts/rejects via the web app.
"""
bot = get_bot()
try:
bot.message_queue.put({
'type': 'one_on_one_response',
'data': {
'player_discord_id': player_discord_id,
'player_full_name': player_full_name,
'coach_full_name': coach_full_name,
'date_str': date_str,
'start_time': start_time,
'end_time': end_time,
'points': points,
'approved': approved,
'refusal_note': refusal_note,
}
})
return True
except Exception as e:
logger.error(f"Error queuing One on One response DM: {e}")
return False
def start_bot(flask_app=None):
"""Start the Discord bot in the background.""" """Start the Discord bot in the background."""
global bot_thread global bot_thread
bot = get_bot() bot = get_bot(flask_app=flask_app)
if DISCORD_BOT_TOKEN and bot_thread is None: if DISCORD_BOT_TOKEN and bot_thread is None:
def run_bot(): def run_bot():
try: try:
+32
View File
@@ -10,6 +10,7 @@ from app.models import (
Admin, Manager, Coach, Player, Scout, Admin, Manager, Coach, Player, Scout,
User, Tryout, Match, MatchParticipant, Team, TeamMember, User, Tryout, Match, MatchParticipant, Team, TeamMember,
OrgTeam, TryoutRegistration, PlayerDisponibility, OrgTeam, TryoutRegistration, PlayerDisponibility,
OneOnOneRequest,
) )
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from app.discord_bot import send_schedule_notification from app.discord_bot import send_schedule_notification
@@ -101,6 +102,37 @@ def api_events():
}, },
}) })
# Add approved One on One sessions for the current user (player or coach)
if isinstance(current_user, Player):
one_on_ones = OneOnOneRequest.query.filter_by(
player_id=current_user.id,
status='approved'
).all()
elif isinstance(current_user, Coach):
one_on_ones = OneOnOneRequest.query.filter_by(
coach_id=current_user.id,
status='approved'
).all()
else:
one_on_ones = []
for ooo in one_on_ones:
events.append({
'id': f'one_on_one_{ooo.id}',
'title': f'1:1 - {ooo.player.full_name} & {ooo.coach.full_name}',
'date': ooo.date.strftime('%Y-%m-%d'),
'type': 'one_on_one',
'color': '#8b5cf6',
'extendedProps': {
'location': 'Discord / Voice Chat',
'status': 'approved',
'description': ooo.points or 'One on One session',
'start_time': ooo.start_time.strftime('%H:%M') if ooo.start_time else None,
'end_time': ooo.end_time.strftime('%H:%M') if ooo.end_time else None,
'participants': f"{ooo.player.full_name} with {ooo.coach.full_name}",
},
})
return jsonify(events) return jsonify(events)
+103 -1
View File
@@ -788,11 +788,104 @@ def one_on_one():
'display': d.strftime('%B %d, %Y (%A)'), 'display': d.strftime('%B %d, %Y (%A)'),
}) })
# Player's own One on One request history
my_requests = OneOnOneRequest.query.filter_by(
player_id=current_user.id
).order_by(OneOnOneRequest.created_at.desc()).all()
return render_template('pages/one_on_one.html', return render_template('pages/one_on_one.html',
org_team=org_team, coach=coach, org_team=org_team, coach=coach,
team_notes=team_notes, personal_notes=personal_notes, team_notes=team_notes, personal_notes=personal_notes,
coach_availability=coach_availability, coach_availability=coach_availability,
dates=dates) dates=dates,
my_requests=my_requests)
@users_bp.route('/one-on-one/<int:request_id>/accept', methods=['POST'])
@login_required
def accept_one_on_one(request_id):
"""Coach accepts a One on One request."""
if not isinstance(current_user, Coach):
flash('Only coaches can accept One on One requests.', 'danger')
return redirect(url_for('main.dashboard'))
request_obj = OneOnOneRequest.query.get_or_404(request_id)
if request_obj.coach_id != current_user.id:
flash('This request is not for you.', 'danger')
return redirect(url_for('users.notes_dashboard'))
if request_obj.status != 'pending':
flash('This request has already been processed.', 'info')
return redirect(url_for('users.notes_dashboard'))
player = request_obj.player
request_obj.status = 'approved'
request_obj.responded_at = datetime.utcnow()
db.session.commit()
# Notify player via Discord (same message as if approved through Discord reactions)
if player and player.discord_user_id:
from app.discord_bot import send_one_on_one_response
send_one_on_one_response(
player_discord_id=player.discord_user_id,
player_full_name=player.full_name,
coach_full_name=current_user.full_name,
date_str=request_obj.date.strftime('%A, %B %d, %Y'),
start_time=request_obj.start_time.strftime('%I:%M %p') if request_obj.start_time else 'TBD',
end_time=request_obj.end_time.strftime('%I:%M %p') if request_obj.end_time else 'TBD',
points=request_obj.points or 'No specific points provided',
approved=True,
)
flash(f'One on One request from {player.username if player else "Unknown"} has been approved!', 'success')
return redirect(url_for('users.notes_dashboard'))
@users_bp.route('/one-on-one/<int:request_id>/reject', methods=['POST'])
@login_required
def reject_one_on_one(request_id):
"""Coach rejects a One on One request."""
if not isinstance(current_user, Coach):
flash('Only coaches can reject One on One requests.', 'danger')
return redirect(url_for('main.dashboard'))
request_obj = OneOnOneRequest.query.get_or_404(request_id)
if request_obj.coach_id != current_user.id:
flash('This request is not for you.', 'danger')
return redirect(url_for('users.notes_dashboard'))
if request_obj.status != 'pending':
flash('This request has already been processed.', 'info')
return redirect(url_for('users.notes_dashboard'))
rejection_reason = request.form.get('rejection_reason', '').strip()
player = request_obj.player
request_obj.status = 'rejected'
request_obj.responded_at = datetime.utcnow()
if rejection_reason:
request_obj.coach_rejection_message = rejection_reason
db.session.commit()
# Notify player via Discord (same message as if rejected through Discord reactions)
if player and player.discord_user_id:
from app.discord_bot import send_one_on_one_response
send_one_on_one_response(
player_discord_id=player.discord_user_id,
player_full_name=player.full_name,
coach_full_name=current_user.full_name,
date_str=request_obj.date.strftime('%A, %B %d, %Y'),
start_time=request_obj.start_time.strftime('%I:%M %p') if request_obj.start_time else 'TBD',
end_time=request_obj.end_time.strftime('%I:%M %p') if request_obj.end_time else 'TBD',
points=request_obj.points or 'No specific points provided',
approved=False,
refusal_note=rejection_reason or None,
)
flash(f'One on One request from {player.username if player else "Unknown"} has been rejected.', 'info')
return redirect(url_for('users.notes_dashboard'))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -920,6 +1013,14 @@ def notes_dashboard():
coach_id=current_user.id, coach_id=current_user.id,
).order_by(PersonalNote.created_at.desc()).all() ).order_by(PersonalNote.created_at.desc()).all()
# One on One requests from team players
one_on_one_requests = []
if org_team and players:
player_ids_list = [p.id for p in players]
one_on_one_requests = OneOnOneRequest.query.filter(
OneOnOneRequest.player_id.in_(player_ids_list)
).order_by(OneOnOneRequest.created_at.desc()).all()
# For context selectors in the form # For context selectors in the form
from app.models import Team as MatchTeam from app.models import Team as MatchTeam
matches = Match.query.filter( matches = Match.query.filter(
@@ -936,6 +1037,7 @@ def notes_dashboard():
team_notes=team_notes, team_notes=team_notes,
latest_team_note=latest_team_note, latest_team_note=latest_team_note,
personal_notes=personal_notes, personal_notes=personal_notes,
one_on_one_requests=one_on_one_requests,
matches=matches, matches=matches,
tryouts=tryouts, tryouts=tryouts,
teams=teams) teams=teams)
+104
View File
@@ -105,6 +105,94 @@
</div> </div>
</div> </div>
<!-- One on One Requests Section -->
<div class="card mt-4">
<div class="card-header">
<h3><i class="fas fa-calendar-check"></i> One on One Requests</h3>
{% if org_team %}
<span class="badge badge-esport">{{ org_team.name }}</span>
{% endif %}
</div>
<div class="card-body">
{% if one_on_one_requests %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Player</th>
<th>Date</th>
<th>Time</th>
<th>Discussion Points</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for req in one_on_one_requests %}
<tr>
<td>{{ req.player.username if req.player else 'Unknown' }}</td>
<td>{{ req.date.strftime('%b %d, %Y') }}</td>
<td>{{ req.start_time.strftime('%I:%M %p') }} - {{ req.end_time.strftime('%I:%M %p') }}</td>
<td>{{ req.points or 'N/A' }}</td>
<td>
{% if req.status == 'pending' %}
<span class="badge badge-warning">Pending</span>
{% elif req.status == 'approved' %}
<span class="badge badge-success">Approved</span>
{% elif req.status == 'rejected' %}
<span class="badge badge-danger">Rejected</span>
{% endif %}
</td>
<td>
{% if req.status == 'pending' %}
<form method="POST" action="{{ url_for('users.accept_one_on_one', request_id=req.id) }}" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-sm btn-success" title="Accept">
<i class="fas fa-check"></i> Accept
</button>
</form>
<button type="button" class="btn btn-sm btn-danger" onclick="showRejectModal({{ req.id }})" title="Refuse">
<i class="fas fa-times"></i> Refuse
</button>
{% elif req.status == 'rejected' and req.coach_rejection_message %}
<span class="text-muted small" title="{{ req.coach_rejection_message }}">Reason: {{ req.coach_rejection_message[:50] }}{% if req.coach_rejection_message|length > 50 %}...{% endif %}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No One on One requests from your players yet.</p>
{% endif %}
</div>
</div>
<!-- Reject Modal -->
<div id="rejectModal" class="modal" style="display:none;">
<div class="modal-overlay" onclick="hideRejectModal()"></div>
<div class="modal-content">
<div class="modal-header">
<h4><i class="fas fa-times-circle"></i> Reject One on One Request</h4>
<button type="button" class="modal-close" onclick="hideRejectModal()">&times;</button>
</div>
<form id="rejectForm" method="POST" action="">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="modal-body">
<div class="form-group">
<label for="rejection_reason">Reason for rejection (optional):</label>
<textarea name="rejection_reason" id="rejection_reason" class="form-textarea" rows="3" placeholder="Let the player know why this time doesn't work..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="hideRejectModal()">Cancel</button>
<button type="submit" class="btn btn-danger">Reject Request</button>
</div>
</form>
</div>
</div>
<div class="dashboard-grid mt-4"> <div class="dashboard-grid mt-4">
<!-- Team Notes History --> <!-- Team Notes History -->
{% if org_team and team_notes %} {% if org_team and team_notes %}
@@ -169,4 +257,20 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endblock %}
{% block scripts %}
<script>
function showRejectModal(requestId) {
const modal = document.getElementById('rejectModal');
const form = document.getElementById('rejectForm');
form.action = "{{ url_for('users.reject_one_on_one', request_id=0) }}".replace('0', requestId);
modal.style.display = 'flex';
}
function hideRejectModal() {
document.getElementById('rejectModal').style.display = 'none';
document.getElementById('rejection_reason').value = '';
}
</script>
{% endblock %} {% endblock %}
+59
View File
@@ -54,7 +54,66 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
<!-- My One on One Requests Tracker -->
<div class="card mt-4">
<div class="card-header">
<h3><i class="fas fa-list-check"></i> My One on One Requests</h3>
</div>
<div class="card-body">
{% if my_requests %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Time</th>
<th>Discussion Points</th>
<th>Status</th>
<th>Coach Response</th>
</tr>
</thead>
<tbody>
{% for req in my_requests %}
<tr>
<td>{{ req.date.strftime('%b %d, %Y') }}</td>
<td>{{ req.start_time.strftime('%I:%M %p') }} - {{ req.end_time.strftime('%I:%M %p') }}</td>
<td>{{ req.points or 'N/A' }}</td>
<td>
{% if req.status == 'pending' %}
<span class="badge badge-warning"><i class="fas fa-clock"></i> Pending</span>
{% elif req.status == 'approved' %}
<span class="badge badge-success"><i class="fas fa-check-circle"></i> Approved</span>
{% elif req.status == 'rejected' %}
<span class="badge badge-danger"><i class="fas fa-times-circle"></i> Rejected</span>
{% endif %}
</td>
<td>
{% if req.status == 'approved' %}
<span class="text-success">Session confirmed!</span>
{% elif req.status == 'rejected' %}
{% if req.coach_rejection_message %}
<span class="text-danger small">{{ req.coach_rejection_message }}</span>
{% else %}
<span class="text-muted small">Coach is unavailable</span>
{% endif %}
{% else %}
<span class="text-muted small">Awaiting coach response...</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">You haven't made any One on One requests yet.</p>
{% endif %}
</div>
</div>
<div class="dashboard-grid mt-4">
<!-- One on One Request Section --> <!-- One on One Request Section -->
<div class="card" style="grid-column: 1 / -1;"> <div class="card" style="grid-column: 1 / -1;">
<div class="card-header"> <div class="card-header">