Add Team Roles feature.

This commit is contained in:
Jens Luedicke
2025-06-28 22:39:28 +02:00
parent e7593dc840
commit d8ec7d636e
11 changed files with 672 additions and 37 deletions

257
app.py
View File

@@ -1,5 +1,5 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g
from models import db, TimeEntry, WorkConfig, User, SystemSettings from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role
import logging import logging
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
import os import os
@@ -78,6 +78,38 @@ def admin_required(f):
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function
# Add this decorator function after your existing decorators
def role_required(min_role):
"""
Decorator to restrict access based on user role.
min_role should be a Role enum value (e.g., Role.TEAM_LEADER)
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user is None:
return redirect(url_for('login', next=request.url))
# Admin always has access
if g.user.is_admin:
return f(*args, **kwargs)
# Check role hierarchy
role_hierarchy = {
Role.TEAM_MEMBER: 1,
Role.TEAM_LEADER: 2,
Role.SUPERVISOR: 3,
Role.ADMIN: 4
}
if role_hierarchy.get(g.user.role, 0) < role_hierarchy.get(min_role, 0):
flash('You do not have sufficient permissions to access this page.', 'error')
return redirect(url_for('home'))
return f(*args, **kwargs)
return decorator
return decorator
@app.before_request @app.before_request
def load_logged_in_user(): def load_logged_in_user():
user_id = session.get('user_id') user_id = session.get('user_id')
@@ -114,7 +146,30 @@ def login():
user = User.query.filter_by(username=username).first() user = User.query.filter_by(username=username).first()
if user and user.check_password(password): # Use the check_password method if user:
# Fix role if it's a string or None
if isinstance(user.role, str) or user.role is None:
# Map string role values to enum values
role_mapping = {
'Team Member': Role.TEAM_MEMBER,
'TEAM_MEMBER': Role.TEAM_MEMBER,
'Team Leader': Role.TEAM_LEADER,
'TEAM_LEADER': Role.TEAM_LEADER,
'Supervisor': Role.SUPERVISOR,
'SUPERVISOR': Role.SUPERVISOR,
'Administrator': Role.ADMIN,
'ADMIN': Role.ADMIN
}
if isinstance(user.role, str):
user.role = role_mapping.get(user.role, Role.TEAM_MEMBER)
else:
user.role = Role.ADMIN if user.is_admin else Role.TEAM_MEMBER
db.session.commit()
# Now proceed with password check
if user.check_password(password):
# Check if user is blocked # Check if user is blocked
if user.is_blocked: if user.is_blocked:
flash('Your account has been disabled. Please contact an administrator.', 'error') flash('Your account has been disabled. Please contact an administrator.', 'error')
@@ -127,7 +182,7 @@ def login():
flash('Login successful!', 'success') flash('Login successful!', 'success')
return redirect(url_for('home')) return redirect(url_for('home'))
else:
flash('Invalid username or password', 'error') flash('Invalid username or password', 'error')
return render_template('login.html', title='Login') return render_template('login.html', title='Login')
@@ -245,7 +300,11 @@ def create_user():
email = request.form.get('email') email = request.form.get('email')
password = request.form.get('password') password = request.form.get('password')
is_admin = 'is_admin' in request.form is_admin = 'is_admin' in request.form
auto_verify = 'auto_verify' in request.form # New checkbox for auto verification auto_verify = 'auto_verify' in request.form
# Get role and team
role_name = request.form.get('role')
team_id = request.form.get('team_id')
# Validate input # Validate input
error = None error = None
@@ -261,7 +320,21 @@ def create_user():
error = 'Email already registered' error = 'Email already registered'
if error is None: if error is None:
new_user = User(username=username, email=email, is_admin=is_admin, is_verified=auto_verify) # Convert role string to enum
try:
role = Role[role_name] if role_name else Role.TEAM_MEMBER
except KeyError:
role = Role.TEAM_MEMBER
# Create new user with role and team
new_user = User(
username=username,
email=email,
is_admin=is_admin,
is_verified=auto_verify,
role=role,
team_id=team_id if team_id else None
)
new_user.set_password(password) new_user.set_password(password)
if not auto_verify: if not auto_verify:
@@ -293,7 +366,11 @@ The TimeTrack Team
flash(error, 'error') flash(error, 'error')
return render_template('create_user.html', title='Create User') # Get all teams for the form
teams = Team.query.all()
roles = [role for role in Role]
return render_template('create_user.html', title='Create User', teams=teams, roles=roles)
@app.route('/admin/users/edit/<int:user_id>', methods=['GET', 'POST']) @app.route('/admin/users/edit/<int:user_id>', methods=['GET', 'POST'])
@admin_required @admin_required
@@ -306,6 +383,10 @@ def edit_user(user_id):
password = request.form.get('password') password = request.form.get('password')
is_admin = 'is_admin' in request.form is_admin = 'is_admin' in request.form
# Get role and team
role_name = request.form.get('role')
team_id = request.form.get('team_id')
# Validate input # Validate input
error = None error = None
if not username: if not username:
@@ -322,6 +403,14 @@ def edit_user(user_id):
user.email = email user.email = email
user.is_admin = is_admin user.is_admin = is_admin
# Convert role string to enum
try:
user.role = Role[role_name] if role_name else Role.TEAM_MEMBER
except KeyError:
user.role = Role.TEAM_MEMBER
user.team_id = team_id if team_id else None
if password: if password:
user.set_password(password) user.set_password(password)
@@ -332,7 +421,11 @@ def edit_user(user_id):
flash(error, 'error') flash(error, 'error')
return render_template('edit_user.html', title='Edit User', user=user) # Get all teams for the form
teams = Team.query.all()
roles = [role for role in Role]
return render_template('edit_user.html', title='Edit User', user=user, teams=teams, roles=roles)
@app.route('/admin/users/delete/<int:user_id>', methods=['POST']) @app.route('/admin/users/delete/<int:user_id>', methods=['POST'])
@admin_required @admin_required
@@ -728,5 +821,155 @@ def admin_settings():
return render_template('admin_settings.html', title='System Settings', settings=settings) return render_template('admin_settings.html', title='System Settings', settings=settings)
# Add these routes for team management
@app.route('/admin/teams')
@admin_required
def admin_teams():
teams = Team.query.all()
return render_template('admin_teams.html', title='Team Management', teams=teams)
@app.route('/admin/teams/create', methods=['GET', 'POST'])
@admin_required
def create_team():
if request.method == 'POST':
name = request.form.get('name')
description = request.form.get('description')
# Validate input
error = None
if not name:
error = 'Team name is required'
elif Team.query.filter_by(name=name).first():
error = 'Team name already exists'
if error is None:
new_team = Team(name=name, description=description)
db.session.add(new_team)
db.session.commit()
flash(f'Team "{name}" created successfully!', 'success')
return redirect(url_for('admin_teams'))
flash(error, 'error')
return render_template('create_team.html', title='Create Team')
@app.route('/admin/teams/edit/<int:team_id>', methods=['GET', 'POST'])
@admin_required
def edit_team(team_id):
team = Team.query.get_or_404(team_id)
if request.method == 'POST':
name = request.form.get('name')
description = request.form.get('description')
# Validate input
error = None
if not name:
error = 'Team name is required'
elif name != team.name and Team.query.filter_by(name=name).first():
error = 'Team name already exists'
if error is None:
team.name = name
team.description = description
db.session.commit()
flash(f'Team "{name}" updated successfully!', 'success')
return redirect(url_for('admin_teams'))
flash(error, 'error')
return render_template('edit_team.html', title='Edit Team', team=team)
@app.route('/admin/teams/delete/<int:team_id>', methods=['POST'])
@admin_required
def delete_team(team_id):
team = Team.query.get_or_404(team_id)
# Check if team has members
if team.users:
flash('Cannot delete team with members. Remove all members first.', 'error')
return redirect(url_for('admin_teams'))
team_name = team.name
db.session.delete(team)
db.session.commit()
flash(f'Team "{team_name}" deleted successfully!', 'success')
return redirect(url_for('admin_teams'))
@app.route('/admin/teams/<int:team_id>', methods=['GET', 'POST'])
@admin_required
def manage_team(team_id):
team = Team.query.get_or_404(team_id)
if request.method == 'POST':
action = request.form.get('action')
if action == 'update_team':
# Update team details
name = request.form.get('name')
description = request.form.get('description')
# Validate input
error = None
if not name:
error = 'Team name is required'
elif name != team.name and Team.query.filter_by(name=name).first():
error = 'Team name already exists'
if error is None:
team.name = name
team.description = description
db.session.commit()
flash(f'Team "{name}" updated successfully!', 'success')
else:
flash(error, 'error')
elif action == 'add_member':
# Add user to team
user_id = request.form.get('user_id')
if user_id:
user = User.query.get(user_id)
if user:
user.team_id = team.id
db.session.commit()
flash(f'User {user.username} added to team!', 'success')
else:
flash('User not found', 'error')
else:
flash('No user selected', 'error')
elif action == 'remove_member':
# Remove user from team
user_id = request.form.get('user_id')
if user_id:
user = User.query.get(user_id)
if user and user.team_id == team.id:
user.team_id = None
db.session.commit()
flash(f'User {user.username} removed from team!', 'success')
else:
flash('User not found or not in this team', 'error')
else:
flash('No user selected', 'error')
# Get team members
team_members = User.query.filter_by(team_id=team.id).all()
# Get users not in this team for the add member form
available_users = User.query.filter(
(User.team_id != team.id) | (User.team_id == None)
).all()
return render_template(
'manage_team.html',
title=f'Manage Team: {team.name}',
team=team,
team_members=team_members,
available_users=available_users
)
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True)

89
migrate_roles_teams.py Normal file
View File

@@ -0,0 +1,89 @@
from app import app, db
from models import User, Team, Role, SystemSettings
from sqlalchemy import text
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def migrate_roles_teams():
with app.app_context():
logger.info("Starting migration for roles and teams...")
# Check if the team table exists
try:
# Create the team table if it doesn't exist
db.engine.execute(text("""
CREATE TABLE IF NOT EXISTS team (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL UNIQUE,
description VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""))
logger.info("Team table created or already exists")
except Exception as e:
logger.error(f"Error creating team table: {e}")
return
# Check if the user table has the role and team_id columns
try:
# Check if role column exists
result = db.engine.execute(text("PRAGMA table_info(user)"))
columns = [row[1] for row in result]
if 'role' not in columns:
# Use the enum name instead of the value
db.engine.execute(text("ALTER TABLE user ADD COLUMN role VARCHAR(20) DEFAULT 'TEAM_MEMBER'"))
logger.info("Added role column to user table")
if 'team_id' not in columns:
db.engine.execute(text("ALTER TABLE user ADD COLUMN team_id INTEGER REFERENCES team(id)"))
logger.info("Added team_id column to user table")
# Create a default team for existing users
default_team = Team.query.filter_by(name="Default Team").first()
if not default_team:
default_team = Team(name="Default Team", description="Default team for existing users")
db.session.add(default_team)
db.session.commit()
logger.info("Created default team")
# Map string role values to enum values
role_mapping = {
'Team Member': Role.TEAM_MEMBER,
'TEAM_MEMBER': Role.TEAM_MEMBER,
'Team Leader': Role.TEAM_LEADER,
'TEAM_LEADER': Role.TEAM_LEADER,
'Supervisor': Role.SUPERVISOR,
'SUPERVISOR': Role.SUPERVISOR,
'Administrator': Role.ADMIN,
'admin': Role.ADMIN,
'ADMIN': Role.ADMIN
}
# Assign all existing users to the default team and set role based on admin status
users = User.query.all()
for user in users:
if user.team_id is None:
user.team_id = default_team.id
# Handle role conversion properly
if isinstance(user.role, str):
# Try to map the string to an enum value
user.role = role_mapping.get(user.role, Role.TEAM_MEMBER)
elif user.role is None:
# Set default role based on admin status
user.role = Role.ADMIN if user.is_admin else Role.TEAM_MEMBER
db.session.commit()
logger.info(f"Assigned {len(users)} existing users to default team and updated roles")
except Exception as e:
logger.error(f"Error updating user table: {e}")
return
logger.info("Migration completed successfully")
if __name__ == "__main__":
migrate_roles_teams()

View File

@@ -2,9 +2,31 @@ from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime, timedelta from datetime import datetime, timedelta
import secrets import secrets
import enum
db = SQLAlchemy() db = SQLAlchemy()
# Define Role as an Enum for better type safety
class Role(enum.Enum):
TEAM_MEMBER = "Team Member"
TEAM_LEADER = "Team Leader"
SUPERVISOR = "Supervisor"
ADMIN = "Administrator" # Keep existing admin role
# Create Team model
class Team(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
description = db.Column(db.String(255))
created_at = db.Column(db.DateTime, default=datetime.now)
# Relationship with users (one team has many users)
users = db.relationship('User', backref='team', lazy=True)
def __repr__(self):
return f'<Team {self.name}>'
# Update User model to include role and team relationship
class User(db.Model): class User(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False) username = db.Column(db.String(80), unique=True, nullable=False)
@@ -18,13 +40,17 @@ class User(db.Model):
verification_token = db.Column(db.String(100), unique=True, nullable=True) verification_token = db.Column(db.String(100), unique=True, nullable=True)
token_expiry = db.Column(db.DateTime, nullable=True) token_expiry = db.Column(db.DateTime, nullable=True)
# New field for blocking users
is_blocked = db.Column(db.Boolean, default=False)
# New fields for role and team
role = db.Column(db.Enum(Role), default=Role.TEAM_MEMBER)
team_id = db.Column(db.Integer, db.ForeignKey('team.id'), nullable=True)
# Relationships # Relationships
time_entries = db.relationship('TimeEntry', backref='user', lazy=True) time_entries = db.relationship('TimeEntry', backref='user', lazy=True)
work_config = db.relationship('WorkConfig', backref='user', lazy=True, uselist=False) work_config = db.relationship('WorkConfig', backref='user', lazy=True, uselist=False)
# New field for blocking users
is_blocked = db.Column(db.Boolean, default=False)
def set_password(self, password): def set_password(self, password):
self.password_hash = generate_password_hash(password) self.password_hash = generate_password_hash(password)

47
repair_roles.py Normal file
View File

@@ -0,0 +1,47 @@
from app import app, db
from models import User, Role
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def repair_user_roles():
with app.app_context():
logger.info("Starting user role repair...")
# Map string role values to enum values
role_mapping = {
'Team Member': Role.TEAM_MEMBER,
'TEAM_MEMBER': Role.TEAM_MEMBER,
'Team Leader': Role.TEAM_LEADER,
'TEAM_LEADER': Role.TEAM_LEADER,
'Supervisor': Role.SUPERVISOR,
'SUPERVISOR': Role.SUPERVISOR,
'Administrator': Role.ADMIN,
'ADMIN': Role.ADMIN
}
users = User.query.all()
fixed_count = 0
for user in users:
original_role = user.role
# Fix role if it's a string or None
if isinstance(user.role, str):
user.role = role_mapping.get(user.role, Role.TEAM_MEMBER)
fixed_count += 1
elif user.role is None:
user.role = Role.ADMIN if user.is_admin else Role.TEAM_MEMBER
fixed_count += 1
if fixed_count > 0:
db.session.commit()
logger.info(f"Fixed roles for {fixed_count} users")
else:
logger.info("No role fixes needed")
logger.info("Role repair completed")
if __name__ == "__main__":
repair_user_roles()

View File

@@ -154,7 +154,7 @@ button {
} }
.btn { .btn {
padding: 8px 16px; padding: 5px 10px;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
@@ -162,6 +162,27 @@ button {
color: white; color: white;
} }
.btn-primary {
background-color: #45a049;
color: white;
margin-bottom: 1rem;
margin-right: 1rem;
margin-left: 1rem;
display: inline-block;
font-size: medium;
}
.btn-sm {
padding: 5px 10px;
border-radius: 4px;
font-size: small;
}
.btn-secondary {
background-color: #f44336;
color: white;
}
.btn:hover { .btn:hover {
background-color: #45a049; background-color: #45a049;
} }
@@ -352,20 +373,6 @@ footer {
font-style: italic; font-style: italic;
} }
.btn-primary {
background-color: #007bff;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.btn-primary:hover {
background-color: #0069d9;
}
.alert { .alert {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -474,6 +481,14 @@ input[type="time"]::-webkit-datetime-edit {
} }
/* Admin Dashboard Styles */ /* Admin Dashboard Styles */
.admin-container {
padding: 1.5rem;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 2rem;
}
.admin-panel { .admin-panel {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -597,3 +612,25 @@ input[type="time"]::-webkit-datetime-edit {
.form-actions { .form-actions {
margin-top: 20px; margin-top: 20px;
} }
/* General table styling */
.data-table {
width: 100%;
border-collapse: collapse;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.data-table th, .data-table td {
padding: 0.8rem;
text-align: left;
border-bottom: 1px solid #ddd;
}
.data-table th {
background-color: #f2f2f2;
font-weight: bold;
}
.data-table tr:hover {
background-color: #f5f5f5;
}

View File

@@ -11,6 +11,12 @@
<a href="{{ url_for('admin_users') }}" class="btn btn-primary">Manage Users</a> <a href="{{ url_for('admin_users') }}" class="btn btn-primary">Manage Users</a>
</div> </div>
<div class="admin-card">
<h2>Team Management</h2>
<p>Configure teams and their members.</p>
<a href="{{ url_for('admin_teams') }}" class="btn btn-primary">Configure</a>
</div>
<div class="admin-card"> <div class="admin-card">
<h2>System Settings</h2> <h2>System Settings</h2>
<p>Configure application-wide settings like registration and more.</p> <p>Configure application-wide settings like registration and more.</p>

View File

@@ -0,0 +1,44 @@
{% extends 'layout.html' %}
{% block content %}
<div class="container">
<h1>Team Management</h1>
<div class="mb-3">
<a href="{{ url_for('create_team') }}" class="btn btn-primary">Create New Team</a>
</div>
{% if teams %}
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Members</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td>{{ team.name }}</td>
<td>{{ team.description }}</td>
<td>{{ team.users|length }}</td>
<td>{{ team.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<a href="{{ url_for('manage_team', team_id=team.id) }}" class="button btn btn-sm btn-info">Manage</a>
<form method="POST" action="{{ url_for('delete_team', team_id=team.id) }}" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this team?');">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No teams found. Create a team to get started.</p>
{% endif %}
</div>
{% endblock %}

View File

@@ -9,7 +9,7 @@
</div> </div>
<div class="user-list"> <div class="user-list">
<table class="table"> <table class="data-table">
<thead> <thead>
<tr> <tr>
<th>Username</th> <th>Username</th>

View File

@@ -0,0 +1,24 @@
{% extends "layout.html" %}
{% block content %}
<div class="admin-container">
<h1>Create New Team</h1>
<form method="POST" action="{{ url_for('create_team') }}" class="team-form">
<div class="form-group">
<label for="name">Team Name</label>
<input type="text" id="name" name="name" class="form-control" required>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" class="form-control" rows="3"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Create Team</button>
<a href="{{ url_for('admin_teams') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
{% endblock %}

View File

@@ -20,6 +20,29 @@
<input type="password" id="password" name="password" class="form-control"> <input type="password" id="password" name="password" class="form-control">
</div> </div>
<div class="form-group">
<label for="role">Role</label>
<select id="role" name="role" class="form-control">
{% for role in roles %}
<option value="{{ role.name }}" {% if user.role == role %}selected{% endif %}>
{{ role.name.replace('_', ' ').title() }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="team_id">Team</label>
<select id="team_id" name="team_id" class="form-control">
<option value="">-- No Team --</option>
{% for team in teams %}
<option value="{{ team.id }}" {% if user.team_id == team.id %}selected{% endif %}>
{{ team.name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group"> <div class="form-group">
<label class="checkbox-container"> <label class="checkbox-container">
<input type="checkbox" name="is_admin" {% if user.is_admin %}checked{% endif %}> Administrator privileges <input type="checkbox" name="is_admin" {% if user.is_admin %}checked{% endif %}> Administrator privileges

View File

@@ -0,0 +1,96 @@
{% extends 'layout.html' %}
{% block content %}
<div class="container">
<h1>Manage Team: {{ team.name }}</h1>
<div class="card mb-4">
<div class="card-header">
<h2>Team Details</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('manage_team', team_id=team.id) }}">
<input type="hidden" name="action" value="update_team">
<div class="mb-3">
<label for="name" class="form-label">Team Name</label>
<input type="text" class="form-control" id="name" name="name" value="{{ team.name }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="3">{{ team.description }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Update Team</button>
</form>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>Team Members</h2>
</div>
<div class="card-body">
{% if team_members %}
<table class="table table-striped">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for member in team_members %}
<tr>
<td>{{ member.username }}</td>
<td>{{ member.email }}</td>
<td>{{ member.role.value }}</td>
<td>
<form method="POST" action="{{ url_for('manage_team', team_id=team.id) }}" class="d-inline">
<input type="hidden" name="action" value="remove_member">
<input type="hidden" name="user_id" value="{{ member.id }}">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this user from the team?')">
Remove
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No members in this team yet.</p>
{% endif %}
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Add Team Member</h2>
</div>
<div class="card-body">
{% if available_users %}
<form method="POST" action="{{ url_for('manage_team', team_id=team.id) }}">
<input type="hidden" name="action" value="add_member">
<div class="mb-3">
<label for="user_id" class="form-label">Select User</label>
<select class="form-select" id="user_id" name="user_id" required>
<option value="">-- Select User --</option>
{% for user in available_users %}
<option value="{{ user.id }}">{{ user.username }} ({{ user.email }})</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-success">Add to Team</button>
</form>
{% else %}
<p>No available users to add to this team.</p>
{% endif %}
</div>
</div>
<div class="mt-3">
<a href="{{ url_for('admin_teams') }}" class="btn btn-secondary">Back to Teams</a>
</div>
</div>
{% endblock %}