Updated PM role with view permissions
This commit is contained in:
@@ -212,7 +212,7 @@ STAFF_LEVEL_ROLES = ['staff', 'payroll', 'project_manager']
|
|||||||
|
|
||||||
# Import and initialize models
|
# Import and initialize models
|
||||||
from models import set_db
|
from models import set_db
|
||||||
User, QRCode, QRCodeStyle, Project, AttendanceData, Employee, TimeAttendance = set_db(db)
|
User, QRCode, QRCodeStyle, Project, AttendanceData, Employee, TimeAttendance, UserProjectPermission, UserLocationPermission = set_db(db)
|
||||||
|
|
||||||
# Initialize the logging system
|
# Initialize the logging system
|
||||||
logger_handler = AppLogger(app, db)
|
logger_handler = AppLogger(app, db)
|
||||||
@@ -1816,28 +1816,42 @@ def users():
|
|||||||
@admin_required
|
@admin_required
|
||||||
@log_database_operations('user_creation')
|
@log_database_operations('user_creation')
|
||||||
def create_user():
|
def create_user():
|
||||||
"""Create new user (Admin only)"""
|
"""Create new user (Admin only) with Project Manager permissions support"""
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
full_name = request.form['full_name']
|
# Get basic form data
|
||||||
email = request.form['email']
|
full_name = request.form.get('full_name', '').strip()
|
||||||
username = request.form['username']
|
email = request.form.get('email', '').strip()
|
||||||
password = request.form['password']
|
username = request.form.get('username', '').strip()
|
||||||
role = request.form['role']
|
password = request.form.get('password', '')
|
||||||
|
role = request.form.get('role', '')
|
||||||
|
|
||||||
|
# Validate required fields
|
||||||
|
if not all([full_name, email, username, password, role]):
|
||||||
|
flash('All fields are required.', 'error')
|
||||||
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
locations = get_all_locations_from_qr_codes()
|
||||||
|
return render_template('create_user.html', projects=projects, locations=locations)
|
||||||
|
|
||||||
# Validate role
|
# Validate role
|
||||||
if role not in VALID_ROLES:
|
if role not in VALID_ROLES:
|
||||||
flash(f'Invalid role selected. Valid roles: {", ".join(VALID_ROLES)}', 'error')
|
flash(f'Invalid role selected. Valid roles: {", ".join(VALID_ROLES)}', 'error')
|
||||||
return render_template('create_user.html')
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
locations = get_all_locations_from_qr_codes()
|
||||||
|
return render_template('create_user.html', projects=projects, locations=locations)
|
||||||
|
|
||||||
# Check if user already exists
|
# Check if user already exists
|
||||||
if User.query.filter_by(username=username).first():
|
if User.query.filter_by(username=username).first():
|
||||||
flash('Username already exists.', 'error')
|
flash('Username already exists.', 'error')
|
||||||
return render_template('create_user.html')
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
locations = get_all_locations_from_qr_codes()
|
||||||
|
return render_template('create_user.html', projects=projects, locations=locations)
|
||||||
|
|
||||||
if User.query.filter_by(email=email).first():
|
if User.query.filter_by(email=email).first():
|
||||||
flash('Email already registered.', 'error')
|
flash('Email already registered.', 'error')
|
||||||
return render_template('create_user.html')
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
locations = get_all_locations_from_qr_codes()
|
||||||
|
return render_template('create_user.html', projects=projects, locations=locations)
|
||||||
|
|
||||||
# Create new user
|
# Create new user
|
||||||
new_user = User(
|
new_user = User(
|
||||||
@@ -1850,6 +1864,69 @@ def create_user():
|
|||||||
new_user.set_password(password)
|
new_user.set_password(password)
|
||||||
|
|
||||||
db.session.add(new_user)
|
db.session.add(new_user)
|
||||||
|
db.session.flush() # Get the user ID without committing
|
||||||
|
|
||||||
|
# Handle Project Manager permissions
|
||||||
|
if role == 'project_manager':
|
||||||
|
# Get selected projects - getlist returns empty list if field doesn't exist
|
||||||
|
selected_projects = request.form.getlist('assigned_projects')
|
||||||
|
|
||||||
|
# Validate and filter project IDs
|
||||||
|
valid_project_ids = []
|
||||||
|
if selected_projects:
|
||||||
|
for pid in selected_projects:
|
||||||
|
try:
|
||||||
|
project_id = int(pid)
|
||||||
|
# Verify project exists
|
||||||
|
if Project.query.get(project_id):
|
||||||
|
valid_project_ids.append(project_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
logger_handler.logger.warning(f"Invalid project ID received: {pid}")
|
||||||
|
|
||||||
|
# Add project permissions
|
||||||
|
if valid_project_ids:
|
||||||
|
for project_id in valid_project_ids:
|
||||||
|
try:
|
||||||
|
permission = UserProjectPermission(
|
||||||
|
user_id=new_user.id,
|
||||||
|
project_id=project_id
|
||||||
|
)
|
||||||
|
db.session.add(permission)
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error adding project permission: {e}")
|
||||||
|
|
||||||
|
logger_handler.logger.info(
|
||||||
|
f"Admin {session['username']} assigned {len(valid_project_ids)} projects to new Project Manager {username}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get selected locations
|
||||||
|
selected_locations = request.form.getlist('assigned_locations')
|
||||||
|
|
||||||
|
# Filter and clean location names
|
||||||
|
valid_locations = []
|
||||||
|
if selected_locations:
|
||||||
|
for location in selected_locations:
|
||||||
|
location_clean = location.strip()
|
||||||
|
if location_clean:
|
||||||
|
valid_locations.append(location_clean)
|
||||||
|
|
||||||
|
# Add location permissions
|
||||||
|
if valid_locations:
|
||||||
|
for location_name in valid_locations:
|
||||||
|
try:
|
||||||
|
permission = UserLocationPermission(
|
||||||
|
user_id=new_user.id,
|
||||||
|
location_name=location_name
|
||||||
|
)
|
||||||
|
db.session.add(permission)
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error adding location permission: {e}")
|
||||||
|
|
||||||
|
logger_handler.logger.info(
|
||||||
|
f"Admin {session['username']} assigned {len(valid_locations)} locations to new Project Manager {username}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Commit all changes
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# Log user creation
|
# Log user creation
|
||||||
@@ -1858,12 +1935,46 @@ def create_user():
|
|||||||
flash(f'User "{full_name}" created successfully with role "{role}".', 'success')
|
flash(f'User "{full_name}" created successfully with role "{role}".', 'success')
|
||||||
return redirect(url_for('users'))
|
return redirect(url_for('users'))
|
||||||
|
|
||||||
|
except KeyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
logger_handler.logger.error(f"Missing form field: {e}")
|
||||||
|
flash(f'Missing required field: {e}. Please fill in all fields.', 'error')
|
||||||
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
locations = get_all_locations_from_qr_codes()
|
||||||
|
return render_template('create_user.html', projects=projects, locations=locations)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
logger_handler.log_database_error('user_creation', e)
|
logger_handler.log_database_error('user_creation', e)
|
||||||
flash('Failed to create user. Please try again.', 'error')
|
logger_handler.logger.error(f"User creation error details: {str(e)}")
|
||||||
|
flash('User creation failed. Please try again.', 'error')
|
||||||
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
locations = get_all_locations_from_qr_codes()
|
||||||
|
return render_template('create_user.html', projects=projects, locations=locations)
|
||||||
|
|
||||||
return render_template('create_user.html', valid_roles=VALID_ROLES)
|
# GET request - load form with projects and locations
|
||||||
|
try:
|
||||||
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
locations = get_all_locations_from_qr_codes()
|
||||||
|
return render_template('create_user.html', projects=projects, locations=locations)
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error loading create user form: {e}")
|
||||||
|
flash('Error loading form. Please try again.', 'error')
|
||||||
|
return redirect(url_for('users'))
|
||||||
|
|
||||||
|
def get_all_locations_from_qr_codes():
|
||||||
|
"""Helper function to get all unique locations from QR codes"""
|
||||||
|
try:
|
||||||
|
result = db.session.execute(text("""
|
||||||
|
SELECT DISTINCT location
|
||||||
|
FROM qr_codes
|
||||||
|
WHERE location IS NOT NULL
|
||||||
|
AND active_status = 1
|
||||||
|
ORDER BY location
|
||||||
|
"""))
|
||||||
|
return [row[0] for row in result.fetchall()]
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error loading locations: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
@app.route('/users/<int:user_id>/delete', methods=['GET', 'POST'])
|
@app.route('/users/<int:user_id>/delete', methods=['GET', 'POST'])
|
||||||
@admin_required
|
@admin_required
|
||||||
@@ -2001,35 +2112,72 @@ def demote_user(user_id):
|
|||||||
|
|
||||||
@app.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
|
@app.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
|
||||||
@admin_required
|
@admin_required
|
||||||
@log_database_operations('user_update')
|
@log_database_operations('user_edit')
|
||||||
def edit_user(user_id):
|
def edit_user(user_id):
|
||||||
"""Edit user details (Admin only)"""
|
"""Edit existing user with Project Manager permissions support"""
|
||||||
try:
|
try:
|
||||||
user_to_edit = User.query.get_or_404(user_id)
|
user_to_edit = User.query.get_or_404(user_id)
|
||||||
|
|
||||||
|
# Track old role for permission cleanup
|
||||||
|
old_role = user_to_edit.role
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# Track changes
|
# Store old values for change tracking
|
||||||
changes = {}
|
|
||||||
old_values = {
|
old_values = {
|
||||||
'full_name': user_to_edit.full_name,
|
'full_name': user_to_edit.full_name,
|
||||||
'email': user_to_edit.email,
|
'email': user_to_edit.email,
|
||||||
'role': user_to_edit.role
|
'username': user_to_edit.username,
|
||||||
|
'role': user_to_edit.role,
|
||||||
|
'active_status': user_to_edit.active_status
|
||||||
}
|
}
|
||||||
|
changes = {}
|
||||||
|
|
||||||
# Update user details
|
# Update basic info with validation
|
||||||
user_to_edit.full_name = request.form['full_name']
|
full_name = request.form.get('full_name', '').strip()
|
||||||
user_to_edit.email = request.form['email']
|
email = request.form.get('email', '').strip()
|
||||||
new_role = request.form['role']
|
username = request.form.get('username', '').strip()
|
||||||
|
|
||||||
# Validate role
|
if not all([full_name, email, username]):
|
||||||
|
flash('Name, email, and username are required.', 'error')
|
||||||
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
locations = get_all_locations_from_qr_codes()
|
||||||
|
assigned_project_ids = []
|
||||||
|
assigned_location_names = []
|
||||||
|
if user_to_edit.role == 'project_manager':
|
||||||
|
assigned_project_ids = [p.project_id for p in UserProjectPermission.query.filter_by(user_id=user_id).all()]
|
||||||
|
assigned_location_names = [l.location_name for l in UserLocationPermission.query.filter_by(user_id=user_id).all()]
|
||||||
|
return render_template('edit_user.html', user=user_to_edit, valid_roles=VALID_ROLES,
|
||||||
|
projects=projects, locations=locations,
|
||||||
|
assigned_project_ids=assigned_project_ids,
|
||||||
|
assigned_location_names=assigned_location_names)
|
||||||
|
|
||||||
|
user_to_edit.full_name = full_name
|
||||||
|
user_to_edit.email = email
|
||||||
|
user_to_edit.username = username
|
||||||
|
|
||||||
|
# Update active status
|
||||||
|
user_to_edit.active_status = 'active_status' in request.form
|
||||||
|
|
||||||
|
# Update role with validation
|
||||||
|
new_role = request.form.get('role', '')
|
||||||
if new_role not in VALID_ROLES:
|
if new_role not in VALID_ROLES:
|
||||||
flash(f'Invalid role selected. Valid roles: {", ".join(VALID_ROLES)}', 'error')
|
flash(f'Invalid role selected. Valid roles: {", ".join(VALID_ROLES)}', 'error')
|
||||||
return render_template('edit_user.html', user=user_to_edit, valid_roles=VALID_ROLES)
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
locations = get_all_locations_from_qr_codes()
|
||||||
|
assigned_project_ids = []
|
||||||
|
assigned_location_names = []
|
||||||
|
if user_to_edit.role == 'project_manager':
|
||||||
|
assigned_project_ids = [p.project_id for p in UserProjectPermission.query.filter_by(user_id=user_id).all()]
|
||||||
|
assigned_location_names = [l.location_name for l in UserLocationPermission.query.filter_by(user_id=user_id).all()]
|
||||||
|
return render_template('edit_user.html', user=user_to_edit, valid_roles=VALID_ROLES,
|
||||||
|
projects=projects, locations=locations,
|
||||||
|
assigned_project_ids=assigned_project_ids,
|
||||||
|
assigned_location_names=assigned_location_names)
|
||||||
|
|
||||||
user_to_edit.role = new_role
|
user_to_edit.role = new_role
|
||||||
|
|
||||||
# Handle password update if provided
|
# Handle password update if provided
|
||||||
new_password = request.form.get('new_password')
|
new_password = request.form.get('new_password', '')
|
||||||
if new_password and new_password.strip():
|
if new_password and new_password.strip():
|
||||||
user_to_edit.set_password(new_password)
|
user_to_edit.set_password(new_password)
|
||||||
changes['password'] = 'Password updated'
|
changes['password'] = 'Password updated'
|
||||||
@@ -2040,25 +2188,141 @@ def edit_user(user_id):
|
|||||||
severity="MEDIUM"
|
severity="MEDIUM"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Handle Project Manager permissions
|
||||||
|
if new_role == 'project_manager':
|
||||||
|
# Update project permissions
|
||||||
|
# First, remove existing project permissions
|
||||||
|
try:
|
||||||
|
UserProjectPermission.query.filter_by(user_id=user_id).delete()
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error deleting old project permissions: {e}")
|
||||||
|
|
||||||
|
# Add new project permissions
|
||||||
|
selected_projects = request.form.getlist('assigned_projects')
|
||||||
|
|
||||||
|
# Validate project IDs
|
||||||
|
valid_project_ids = []
|
||||||
|
if selected_projects:
|
||||||
|
for pid in selected_projects:
|
||||||
|
try:
|
||||||
|
project_id = int(pid)
|
||||||
|
# Verify project exists
|
||||||
|
if Project.query.get(project_id):
|
||||||
|
valid_project_ids.append(project_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
logger_handler.logger.warning(f"Invalid project ID received: {pid}")
|
||||||
|
|
||||||
|
# Add validated project permissions
|
||||||
|
if valid_project_ids:
|
||||||
|
for project_id in valid_project_ids:
|
||||||
|
try:
|
||||||
|
permission = UserProjectPermission(
|
||||||
|
user_id=user_id,
|
||||||
|
project_id=project_id
|
||||||
|
)
|
||||||
|
db.session.add(permission)
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error adding project permission: {e}")
|
||||||
|
|
||||||
|
changes['assigned_projects'] = f'{len(valid_project_ids)} projects assigned'
|
||||||
|
logger_handler.logger.info(
|
||||||
|
f"Admin {session['username']} updated project permissions for Project Manager {user_to_edit.username}: {len(valid_project_ids)} projects"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update location permissions
|
||||||
|
# First, remove existing location permissions
|
||||||
|
try:
|
||||||
|
UserLocationPermission.query.filter_by(user_id=user_id).delete()
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error deleting old location permissions: {e}")
|
||||||
|
|
||||||
|
# Add new location permissions
|
||||||
|
selected_locations = request.form.getlist('assigned_locations')
|
||||||
|
|
||||||
|
# Validate and clean locations
|
||||||
|
valid_locations = []
|
||||||
|
if selected_locations:
|
||||||
|
for location in selected_locations:
|
||||||
|
location_clean = location.strip()
|
||||||
|
if location_clean:
|
||||||
|
valid_locations.append(location_clean)
|
||||||
|
|
||||||
|
# Add validated location permissions
|
||||||
|
if valid_locations:
|
||||||
|
for location_name in valid_locations:
|
||||||
|
try:
|
||||||
|
permission = UserLocationPermission(
|
||||||
|
user_id=user_id,
|
||||||
|
location_name=location_name
|
||||||
|
)
|
||||||
|
db.session.add(permission)
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error adding location permission: {e}")
|
||||||
|
|
||||||
|
changes['assigned_locations'] = f'{len(valid_locations)} locations assigned'
|
||||||
|
logger_handler.logger.info(
|
||||||
|
f"Admin {session['username']} updated location permissions for Project Manager {user_to_edit.username}: {len(valid_locations)} locations"
|
||||||
|
)
|
||||||
|
|
||||||
|
# If role changed from project_manager to something else, remove permissions
|
||||||
|
elif old_role == 'project_manager' and new_role != 'project_manager':
|
||||||
|
try:
|
||||||
|
UserProjectPermission.query.filter_by(user_id=user_id).delete()
|
||||||
|
UserLocationPermission.query.filter_by(user_id=user_id).delete()
|
||||||
|
logger_handler.logger.info(
|
||||||
|
f"Admin {session['username']} removed Project Manager permissions from user {user_to_edit.username} (role changed to {new_role})"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error removing permissions: {e}")
|
||||||
|
|
||||||
# Track changes
|
# Track changes
|
||||||
for field, old_value in old_values.items():
|
for field, old_value in old_values.items():
|
||||||
new_value = getattr(user_to_edit, field)
|
new_value = getattr(user_to_edit, field)
|
||||||
if old_value != new_value:
|
if old_value != new_value:
|
||||||
changes[field] = {'old': old_value, 'new': new_value}
|
changes[field] = {'old': old_value, 'new': new_value}
|
||||||
|
|
||||||
|
# Commit all changes
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# Log user update
|
# Log user update
|
||||||
if changes:
|
if changes:
|
||||||
logger_handler.logger.info(f"Admin user {session['username']} updated user {user_to_edit.username}: {json.dumps(changes)}")
|
logger_handler.logger.info(f"Admin user {session['username']} updated user {user_to_edit.username}: {json.dumps(changes, default=str)}")
|
||||||
|
|
||||||
flash(f'User "{user_to_edit.full_name}" updated successfully.', 'success')
|
flash(f'User "{user_to_edit.full_name}" updated successfully.', 'success')
|
||||||
return redirect(url_for('users'))
|
return redirect(url_for('users'))
|
||||||
|
|
||||||
return render_template('edit_user.html', user=user_to_edit, valid_roles=VALID_ROLES)
|
# GET request - load form with current assignments
|
||||||
|
try:
|
||||||
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
locations = get_all_locations_from_qr_codes()
|
||||||
|
|
||||||
|
# Get current assignments if user is a project manager
|
||||||
|
assigned_project_ids = []
|
||||||
|
assigned_location_names = []
|
||||||
|
|
||||||
|
if user_to_edit.role == 'project_manager':
|
||||||
|
try:
|
||||||
|
assigned_project_ids = [p.project_id for p in UserProjectPermission.query.filter_by(user_id=user_id).all()]
|
||||||
|
assigned_location_names = [l.location_name for l in UserLocationPermission.query.filter_by(user_id=user_id).all()]
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error loading current permissions: {e}")
|
||||||
|
|
||||||
|
return render_template('edit_user.html',
|
||||||
|
user=user_to_edit,
|
||||||
|
valid_roles=VALID_ROLES,
|
||||||
|
projects=projects,
|
||||||
|
locations=locations,
|
||||||
|
assigned_project_ids=assigned_project_ids,
|
||||||
|
assigned_location_names=assigned_location_names)
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error loading edit user form: {e}")
|
||||||
|
flash('Error loading edit form. Please try again.', 'error')
|
||||||
|
return redirect(url_for('users'))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
logger_handler.log_database_error('user_update', e)
|
logger_handler.log_database_error('user_update', e)
|
||||||
|
logger_handler.logger.error(f"User update error details: {str(e)}")
|
||||||
flash('Error updating user. Please try again.', 'error')
|
flash('Error updating user. Please try again.', 'error')
|
||||||
return redirect(url_for('users'))
|
return redirect(url_for('users'))
|
||||||
|
|
||||||
@@ -2241,6 +2505,47 @@ def user_stats_api():
|
|||||||
print(f"Error fetching user stats: {e}")
|
print(f"Error fetching user stats: {e}")
|
||||||
return jsonify({'error': 'Failed to fetch user statistics'}), 500
|
return jsonify({'error': 'Failed to fetch user statistics'}), 500
|
||||||
|
|
||||||
|
@app.route('/api/locations-by-projects', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def get_locations_by_projects():
|
||||||
|
"""Get locations that belong to selected projects"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
project_ids = data.get('project_ids', [])
|
||||||
|
|
||||||
|
if not project_ids:
|
||||||
|
# No projects selected, return empty list
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'locations': [],
|
||||||
|
'message': 'No projects selected'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get unique locations from QR codes that belong to selected projects
|
||||||
|
result = db.session.execute(text("""
|
||||||
|
SELECT DISTINCT location
|
||||||
|
FROM qr_codes
|
||||||
|
WHERE project_id IN :project_ids
|
||||||
|
AND location IS NOT NULL
|
||||||
|
AND active_status = 1
|
||||||
|
ORDER BY location
|
||||||
|
"""), {'project_ids': tuple(project_ids)})
|
||||||
|
|
||||||
|
locations = [row[0] for row in result.fetchall()]
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'locations': locations,
|
||||||
|
'count': len(locations)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error fetching locations by projects: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
@app.route('/api/roles/permissions')
|
@app.route('/api/roles/permissions')
|
||||||
@admin_required
|
@admin_required
|
||||||
def role_permissions_api():
|
def role_permissions_api():
|
||||||
|
|||||||
+2
-1
@@ -19,5 +19,6 @@ def set_db(database):
|
|||||||
from .attendance import AttendanceData
|
from .attendance import AttendanceData
|
||||||
from .employee import Employee
|
from .employee import Employee
|
||||||
from .time_attendance import TimeAttendance
|
from .time_attendance import TimeAttendance
|
||||||
|
from .permissions import UserProjectPermission, UserLocationPermission
|
||||||
|
|
||||||
return User, QRCode, QRCodeStyle, Project, AttendanceData, Employee, TimeAttendance
|
return User, QRCode, QRCodeStyle, Project, AttendanceData, Employee, TimeAttendance, UserProjectPermission, UserLocationPermission
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""
|
||||||
|
Permission Models for QR Attendance Management System
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Permission models to manage Project Manager access control.
|
||||||
|
These models define which projects and locations a Project Manager can access.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from . import base
|
||||||
|
|
||||||
|
class UserProjectPermission(base.db.Model):
|
||||||
|
"""
|
||||||
|
UserProjectPermission model to manage project access for Project Managers
|
||||||
|
Links users to specific projects they are allowed to view
|
||||||
|
"""
|
||||||
|
__tablename__ = 'user_project_permissions'
|
||||||
|
|
||||||
|
id = base.db.Column(base.db.Integer, primary_key=True)
|
||||||
|
user_id = base.db.Column(base.db.Integer, base.db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
||||||
|
project_id = base.db.Column(base.db.Integer, base.db.ForeignKey('projects.id', ondelete='CASCADE'), nullable=False)
|
||||||
|
created_date = base.db.Column(base.db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user = base.db.relationship('User', backref=base.db.backref('project_permissions', lazy='dynamic', cascade='all, delete-orphan'))
|
||||||
|
project = base.db.relationship('Project', backref=base.db.backref('user_permissions', lazy='dynamic'))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<UserProjectPermission user_id={self.user_id} project_id={self.project_id}>'
|
||||||
|
|
||||||
|
|
||||||
|
class UserLocationPermission(base.db.Model):
|
||||||
|
"""
|
||||||
|
UserLocationPermission model to manage location access for Project Managers
|
||||||
|
Links users to specific locations they are allowed to view
|
||||||
|
"""
|
||||||
|
__tablename__ = 'user_location_permissions'
|
||||||
|
|
||||||
|
id = base.db.Column(base.db.Integer, primary_key=True)
|
||||||
|
user_id = base.db.Column(base.db.Integer, base.db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
||||||
|
location_name = base.db.Column(base.db.String(200), nullable=False)
|
||||||
|
created_date = base.db.Column(base.db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user = base.db.relationship('User', backref=base.db.backref('location_permissions', lazy='dynamic', cascade='all, delete-orphan'))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<UserLocationPermission user_id={self.user_id} location={self.location_name}>'
|
||||||
@@ -649,6 +649,74 @@ a:hover {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Project Manager Permissions Section Styles */
|
||||||
|
.permissions-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-section .section-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-section .section-header h3 {
|
||||||
|
color: #495057;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-section .section-description {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
max-height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 4px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label:hover {
|
||||||
|
background-color: #f1f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-input {
|
||||||
|
margin-right: 12px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-text {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input:checked + .checkbox-text {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
.footer {
|
.footer {
|
||||||
background-color: var(--gray-800);
|
background-color: var(--gray-800);
|
||||||
@@ -1234,3 +1302,198 @@ body:not(.has-sidebar) .main-content {
|
|||||||
gap: var(--spacing-3);
|
gap: var(--spacing-3);
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Step-by-step Permission Selection Styles */
|
||||||
|
.permission-step {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-step:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-info h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-info p {
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-footer {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-summary i {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-summary strong {
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-loading,
|
||||||
|
.location-empty {
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-loading i {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-empty i {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-empty p {
|
||||||
|
margin: 10px 0 5px 0;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-empty small {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-overview {
|
||||||
|
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
|
||||||
|
border: 2px solid #3b82f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-header i {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-item strong {
|
||||||
|
color: #1f2937;
|
||||||
|
min-width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-item span {
|
||||||
|
color: #374151;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-qr-count {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: #e0f2fe;
|
||||||
|
color: #0369a1;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-qr-count i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.step-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-item {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-item strong {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
+300
-9
@@ -110,6 +110,114 @@ Code Management{% endblock %} {% block content %}
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Project Manager Permissions Section -->
|
||||||
|
<div class="permissions-section" id="pmPermissionsSection" style="display: none;">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
Project Manager Permissions
|
||||||
|
</h3>
|
||||||
|
<p class="section-description">
|
||||||
|
Follow the steps below to configure access permissions for this Project Manager.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 1: Select Projects -->
|
||||||
|
<div class="permission-step" id="projectStep">
|
||||||
|
<div class="step-header">
|
||||||
|
<span class="step-number">1</span>
|
||||||
|
<div class="step-info">
|
||||||
|
<h4>Select Projects</h4>
|
||||||
|
<p>Choose which projects this Project Manager can access</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox-group" id="projectCheckboxGroup">
|
||||||
|
{% if projects %}
|
||||||
|
{% for project in projects %}
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="assigned_projects"
|
||||||
|
value="{{ project.id }}"
|
||||||
|
class="checkbox-input project-checkbox"
|
||||||
|
data-project-name="{{ project.name }}"
|
||||||
|
>
|
||||||
|
<span class="checkbox-text">{{ project.name }}</span>
|
||||||
|
<span class="project-qr-count" title="QR codes in this project">
|
||||||
|
<i class="fas fa-qrcode"></i> {{ project.qr_count or 0 }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No active projects available</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step-footer">
|
||||||
|
<div class="selection-summary" id="projectSummary">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>No projects selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Select Locations -->
|
||||||
|
<div class="permission-step" id="locationStep" style="display: none;">
|
||||||
|
<div class="step-header">
|
||||||
|
<span class="step-number">2</span>
|
||||||
|
<div class="step-info">
|
||||||
|
<h4>Select Locations</h4>
|
||||||
|
<p>Choose specific locations within the selected projects</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="location-loading" id="locationLoading" style="display: none;">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
<span>Loading locations...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox-group" id="locationCheckboxGroup">
|
||||||
|
<!-- Locations will be populated dynamically -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="location-empty" id="locationEmpty" style="display: none;">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<p>No locations found for the selected projects.</p>
|
||||||
|
<small>QR codes in these projects may not have location data.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step-footer">
|
||||||
|
<div class="selection-summary" id="locationSummary">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>No locations selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selection Overview -->
|
||||||
|
<div class="permissions-overview" id="permissionsOverview" style="display: none;">
|
||||||
|
<div class="overview-header">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
<h4>Permission Summary</h4>
|
||||||
|
</div>
|
||||||
|
<div class="overview-content">
|
||||||
|
<div class="overview-item">
|
||||||
|
<strong>Projects:</strong>
|
||||||
|
<span id="overviewProjects">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="overview-item">
|
||||||
|
<strong>Locations:</strong>
|
||||||
|
<span id="overviewLocations">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Role Information Panel -->
|
<!-- Role Information Panel -->
|
||||||
<div class="info-panel" id="roleInfo" style="display: none">
|
<div class="info-panel" id="roleInfo" style="display: none">
|
||||||
<div class="info-header">
|
<div class="info-header">
|
||||||
@@ -170,6 +278,18 @@ Code Management{% endblock %} {% block content %}
|
|||||||
const previewBtn = document.getElementById("previewUser");
|
const previewBtn = document.getElementById("previewUser");
|
||||||
const creationSummary = document.getElementById("creationSummary");
|
const creationSummary = document.getElementById("creationSummary");
|
||||||
|
|
||||||
|
// Permission section elements
|
||||||
|
const pmSection = document.getElementById('pmPermissionsSection');
|
||||||
|
const projectStep = document.getElementById('projectStep');
|
||||||
|
const locationStep = document.getElementById('locationStep');
|
||||||
|
const permissionsOverview = document.getElementById('permissionsOverview');
|
||||||
|
const projectCheckboxes = document.querySelectorAll('.project-checkbox');
|
||||||
|
const projectSummary = document.getElementById('projectSummary');
|
||||||
|
const locationSummary = document.getElementById('locationSummary');
|
||||||
|
const locationCheckboxGroup = document.getElementById('locationCheckboxGroup');
|
||||||
|
const locationLoading = document.getElementById('locationLoading');
|
||||||
|
const locationEmpty = document.getElementById('locationEmpty');
|
||||||
|
|
||||||
// Form inputs
|
// Form inputs
|
||||||
const fullNameInput = document.getElementById("full_name");
|
const fullNameInput = document.getElementById("full_name");
|
||||||
const emailInput = document.getElementById("email");
|
const emailInput = document.getElementById("email");
|
||||||
@@ -213,17 +333,19 @@ Code Management{% endblock %} {% block content %}
|
|||||||
project_manager: {
|
project_manager: {
|
||||||
title: "Project Manager Permissions",
|
title: "Project Manager Permissions",
|
||||||
permissions: [
|
permissions: [
|
||||||
"Create and edit QR codes",
|
"View assigned projects only",
|
||||||
"View all QR codes in the system",
|
"View assigned locations only",
|
||||||
|
"Create and edit QR codes for assigned projects",
|
||||||
|
"View attendance data for assigned projects/locations",
|
||||||
"Download QR code images",
|
"Download QR code images",
|
||||||
"Update personal profile information",
|
"Update personal profile information",
|
||||||
"Access dashboard and reports",
|
"Access dashboard with filtered data",
|
||||||
"Same permissions as Staff (additional features coming soon)",
|
|
||||||
],
|
],
|
||||||
restrictions: [
|
restrictions: [
|
||||||
"Cannot delete QR codes",
|
"Cannot delete QR codes",
|
||||||
"Cannot manage other users",
|
"Cannot manage other users",
|
||||||
"Cannot access admin settings",
|
"Cannot access admin settings",
|
||||||
|
"Can only view data for assigned projects and locations",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
@@ -246,28 +368,23 @@ Code Management{% endblock %} {% block content %}
|
|||||||
|
|
||||||
if (selectedRole && rolePermissions[selectedRole]) {
|
if (selectedRole && rolePermissions[selectedRole]) {
|
||||||
const roleData = rolePermissions[selectedRole];
|
const roleData = rolePermissions[selectedRole];
|
||||||
|
|
||||||
let permissionsHtml = `
|
let permissionsHtml = `
|
||||||
<h4>${roleData.title}</h4>
|
<h4>${roleData.title}</h4>
|
||||||
<div class="permissions-section">
|
<div class="permissions-section">
|
||||||
<h5><i class="fas fa-check-circle text-success"></i> Permissions</h5>
|
<h5><i class="fas fa-check-circle text-success"></i> Permissions</h5>
|
||||||
<ul class="permissions-list">
|
<ul class="permissions-list">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
roleData.permissions.forEach((permission) => {
|
roleData.permissions.forEach((permission) => {
|
||||||
permissionsHtml += `<li><i class="fas fa-check"></i> ${permission}</li>`;
|
permissionsHtml += `<li><i class="fas fa-check"></i> ${permission}</li>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
permissionsHtml += `
|
permissionsHtml += `
|
||||||
</ul>
|
</ul>
|
||||||
<h5><i class="fas fa-times-circle text-warning"></i> Restrictions</h5>
|
<h5><i class="fas fa-times-circle text-warning"></i> Restrictions</h5>
|
||||||
<ul class="restrictions-list">
|
<ul class="restrictions-list">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
roleData.restrictions.forEach((restriction) => {
|
roleData.restrictions.forEach((restriction) => {
|
||||||
permissionsHtml += `<li><i class="fas fa-times"></i> ${restriction}</li>`;
|
permissionsHtml += `<li><i class="fas fa-times"></i> ${restriction}</li>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
permissionsHtml += `</ul></div>`;
|
permissionsHtml += `</ul></div>`;
|
||||||
|
|
||||||
roleContent.innerHTML = permissionsHtml;
|
roleContent.innerHTML = permissionsHtml;
|
||||||
@@ -277,8 +394,182 @@ Code Management{% endblock %} {% block content %}
|
|||||||
roleInfo.style.display = "none";
|
roleInfo.style.display = "none";
|
||||||
roleInfo.classList.remove("fade-in");
|
roleInfo.classList.remove("fade-in");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show/hide Project Manager permissions section
|
||||||
|
if (selectedRole === 'project_manager') {
|
||||||
|
pmSection.style.display = 'block';
|
||||||
|
setupPermissionFlow();
|
||||||
|
} else {
|
||||||
|
pmSection.style.display = 'none';
|
||||||
|
resetPermissionFlow();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Setup cascading permission flow
|
||||||
|
function setupPermissionFlow() {
|
||||||
|
// Reset state
|
||||||
|
locationStep.style.display = 'none';
|
||||||
|
permissionsOverview.style.display = 'none';
|
||||||
|
locationCheckboxGroup.innerHTML = '';
|
||||||
|
|
||||||
|
// Add event listeners to project checkboxes
|
||||||
|
projectCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', handleProjectSelection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle project selection changes
|
||||||
|
function handleProjectSelection() {
|
||||||
|
const selectedProjects = Array.from(projectCheckboxes)
|
||||||
|
.filter(cb => cb.checked)
|
||||||
|
.map(cb => ({
|
||||||
|
id: cb.value,
|
||||||
|
name: cb.dataset.projectName
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update project summary
|
||||||
|
if (selectedProjects.length > 0) {
|
||||||
|
projectSummary.innerHTML = `
|
||||||
|
<i class="fas fa-check-circle" style="color: #10b981;"></i>
|
||||||
|
<span><strong>${selectedProjects.length}</strong> project${selectedProjects.length > 1 ? 's' : ''} selected</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show location step and load locations
|
||||||
|
locationStep.style.display = 'block';
|
||||||
|
loadLocationsByProjects(selectedProjects.map(p => p.id));
|
||||||
|
} else {
|
||||||
|
projectSummary.innerHTML = `
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>No projects selected</span>
|
||||||
|
`;
|
||||||
|
locationStep.style.display = 'none';
|
||||||
|
permissionsOverview.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load locations for selected projects
|
||||||
|
async function loadLocationsByProjects(projectIds) {
|
||||||
|
locationLoading.style.display = 'flex';
|
||||||
|
locationCheckboxGroup.innerHTML = '';
|
||||||
|
locationEmpty.style.display = 'none';
|
||||||
|
locationSummary.innerHTML = `
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>Loading locations...</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/locations-by-projects', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ project_ids: projectIds })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
locationLoading.style.display = 'none';
|
||||||
|
|
||||||
|
if (data.success && data.locations.length > 0) {
|
||||||
|
// Populate location checkboxes
|
||||||
|
locationCheckboxGroup.innerHTML = data.locations.map(location => `
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="assigned_locations"
|
||||||
|
value="${location}"
|
||||||
|
class="checkbox-input location-checkbox"
|
||||||
|
>
|
||||||
|
<span class="checkbox-text">${location}</span>
|
||||||
|
</label>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Add event listeners to location checkboxes
|
||||||
|
document.querySelectorAll('.location-checkbox').forEach(cb => {
|
||||||
|
cb.addEventListener('change', handleLocationSelection);
|
||||||
|
});
|
||||||
|
|
||||||
|
locationSummary.innerHTML = `
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>${data.count} location${data.count > 1 ? 's' : ''} available</span>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
locationEmpty.style.display = 'block';
|
||||||
|
locationSummary.innerHTML = `
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
<span>No locations available</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePermissionsOverview();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading locations:', error);
|
||||||
|
locationLoading.style.display = 'none';
|
||||||
|
locationEmpty.style.display = 'block';
|
||||||
|
locationSummary.innerHTML = `
|
||||||
|
<i class="fas fa-exclamation-triangle" style="color: #ef4444;"></i>
|
||||||
|
<span>Error loading locations</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle location selection changes
|
||||||
|
function handleLocationSelection() {
|
||||||
|
const selectedLocations = Array.from(document.querySelectorAll('.location-checkbox:checked'));
|
||||||
|
|
||||||
|
if (selectedLocations.length > 0) {
|
||||||
|
locationSummary.innerHTML = `
|
||||||
|
<i class="fas fa-check-circle" style="color: #10b981;"></i>
|
||||||
|
<span><strong>${selectedLocations.length}</strong> location${selectedLocations.length > 1 ? 's' : ''} selected</span>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
const totalLocations = document.querySelectorAll('.location-checkbox').length;
|
||||||
|
locationSummary.innerHTML = `
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>${totalLocations} location${totalLocations > 1 ? 's' : ''} available</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePermissionsOverview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update permissions overview
|
||||||
|
function updatePermissionsOverview() {
|
||||||
|
const selectedProjects = Array.from(projectCheckboxes)
|
||||||
|
.filter(cb => cb.checked)
|
||||||
|
.map(cb => cb.dataset.projectName);
|
||||||
|
|
||||||
|
const selectedLocations = Array.from(document.querySelectorAll('.location-checkbox:checked'))
|
||||||
|
.map(cb => cb.value);
|
||||||
|
|
||||||
|
if (selectedProjects.length > 0 || selectedLocations.length > 0) {
|
||||||
|
permissionsOverview.style.display = 'block';
|
||||||
|
|
||||||
|
document.getElementById('overviewProjects').textContent =
|
||||||
|
selectedProjects.length > 0
|
||||||
|
? selectedProjects.join(', ')
|
||||||
|
: 'All projects';
|
||||||
|
|
||||||
|
document.getElementById('overviewLocations').textContent =
|
||||||
|
selectedLocations.length > 0
|
||||||
|
? selectedLocations.join(', ')
|
||||||
|
: 'All locations in selected projects';
|
||||||
|
} else {
|
||||||
|
permissionsOverview.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset permission flow
|
||||||
|
function resetPermissionFlow() {
|
||||||
|
projectCheckboxes.forEach(cb => {
|
||||||
|
cb.checked = false;
|
||||||
|
cb.removeEventListener('change', handleProjectSelection);
|
||||||
|
});
|
||||||
|
locationStep.style.display = 'none';
|
||||||
|
permissionsOverview.style.display = 'none';
|
||||||
|
locationCheckboxGroup.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
// Password strength indicator
|
// Password strength indicator
|
||||||
passwordInput.addEventListener("input", function () {
|
passwordInput.addEventListener("input", function () {
|
||||||
const password = this.value;
|
const password = this.value;
|
||||||
|
|||||||
+330
-1
@@ -51,7 +51,8 @@
|
|||||||
<i class="fas fa-user"></i>
|
<i class="fas fa-user"></i>
|
||||||
Username
|
Username
|
||||||
</label>
|
</label>
|
||||||
<input type="text" id="username" value="{{ user.username }}" disabled>
|
<input type="text" id="username_display" value="{{ user.username }}" disabled>
|
||||||
|
<input type="hidden" name="username" value="{{ user.username }}">
|
||||||
<small class="form-help">Username cannot be changed</small>
|
<small class="form-help">Username cannot be changed</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -81,6 +82,114 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Project Manager Permissions Section -->
|
||||||
|
<div class="permissions-section" id="pmPermissionsSection" style="display: none;">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
Project Manager Permissions
|
||||||
|
</h3>
|
||||||
|
<p class="section-description">
|
||||||
|
Follow the steps below to configure access permissions for this Project Manager.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 1: Select Projects -->
|
||||||
|
<div class="permission-step" id="projectStep">
|
||||||
|
<div class="step-header">
|
||||||
|
<span class="step-number">1</span>
|
||||||
|
<div class="step-info">
|
||||||
|
<h4>Select Projects</h4>
|
||||||
|
<p>Choose which projects this Project Manager can access</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox-group" id="projectCheckboxGroup">
|
||||||
|
{% if projects %}
|
||||||
|
{% for project in projects %}
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="assigned_projects"
|
||||||
|
value="{{ project.id }}"
|
||||||
|
class="checkbox-input project-checkbox"
|
||||||
|
data-project-name="{{ project.name }}"
|
||||||
|
>
|
||||||
|
<span class="checkbox-text">{{ project.name }}</span>
|
||||||
|
<span class="project-qr-count" title="QR codes in this project">
|
||||||
|
<i class="fas fa-qrcode"></i> {{ project.qr_count or 0 }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No active projects available</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step-footer">
|
||||||
|
<div class="selection-summary" id="projectSummary">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>No projects selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Select Locations -->
|
||||||
|
<div class="permission-step" id="locationStep" style="display: none;">
|
||||||
|
<div class="step-header">
|
||||||
|
<span class="step-number">2</span>
|
||||||
|
<div class="step-info">
|
||||||
|
<h4>Select Locations</h4>
|
||||||
|
<p>Choose specific locations within the selected projects</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="location-loading" id="locationLoading" style="display: none;">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
<span>Loading locations...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox-group" id="locationCheckboxGroup">
|
||||||
|
<!-- Locations will be populated dynamically -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="location-empty" id="locationEmpty" style="display: none;">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<p>No locations found for the selected projects.</p>
|
||||||
|
<small>QR codes in these projects may not have location data.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step-footer">
|
||||||
|
<div class="selection-summary" id="locationSummary">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>No locations selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selection Overview -->
|
||||||
|
<div class="permissions-overview" id="permissionsOverview" style="display: none;">
|
||||||
|
<div class="overview-header">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
<h4>Permission Summary</h4>
|
||||||
|
</div>
|
||||||
|
<div class="overview-content">
|
||||||
|
<div class="overview-item">
|
||||||
|
<strong>Projects:</strong>
|
||||||
|
<span id="overviewProjects">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="overview-item">
|
||||||
|
<strong>Locations:</strong>
|
||||||
|
<span id="overviewLocations">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Password Section -->
|
<!-- Password Section -->
|
||||||
<div class="password-section">
|
<div class="password-section">
|
||||||
<h3>
|
<h3>
|
||||||
@@ -186,6 +295,7 @@
|
|||||||
const passwordStrength = document.getElementById('passwordStrength');
|
const passwordStrength = document.getElementById('passwordStrength');
|
||||||
const resetPasswordBtn = document.getElementById('resetPassword');
|
const resetPasswordBtn = document.getElementById('resetPassword');
|
||||||
|
|
||||||
|
|
||||||
// Role change warning system
|
// Role change warning system
|
||||||
roleSelect.addEventListener('change', function() {
|
roleSelect.addEventListener('change', function() {
|
||||||
const newRole = this.value;
|
const newRole = this.value;
|
||||||
@@ -249,6 +359,225 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pmSection = document.getElementById('pmPermissionsSection');
|
||||||
|
const projectStep = document.getElementById('projectStep');
|
||||||
|
const locationStep = document.getElementById('locationStep');
|
||||||
|
const permissionsOverview = document.getElementById('permissionsOverview');
|
||||||
|
const projectCheckboxes = document.querySelectorAll('.project-checkbox');
|
||||||
|
const projectSummary = document.getElementById('projectSummary');
|
||||||
|
const locationSummary = document.getElementById('locationSummary');
|
||||||
|
const locationCheckboxGroup = document.getElementById('locationCheckboxGroup');
|
||||||
|
const locationLoading = document.getElementById('locationLoading');
|
||||||
|
const locationEmpty = document.getElementById('locationEmpty');
|
||||||
|
|
||||||
|
// Show/hide Project Manager permissions based on role selection
|
||||||
|
if (roleSelect) {
|
||||||
|
roleSelect.addEventListener('change', function() {
|
||||||
|
if (this.value === 'project_manager') {
|
||||||
|
pmSection.style.display = 'block';
|
||||||
|
setupPermissionFlow();
|
||||||
|
} else {
|
||||||
|
pmSection.style.display = 'none';
|
||||||
|
resetPermissionFlow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize permission flow if Project Manager role is already selected
|
||||||
|
if (roleSelect && roleSelect.value === 'project_manager') {
|
||||||
|
pmSection.style.display = 'block';
|
||||||
|
setupPermissionFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup cascading permission flow
|
||||||
|
function setupPermissionFlow() {
|
||||||
|
// Add event listeners to project checkboxes
|
||||||
|
projectCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', handleProjectSelection);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pre-select assigned projects if in edit mode
|
||||||
|
{% if assigned_project_ids %}
|
||||||
|
const assignedProjectIds = {{ assigned_project_ids | tojson }};
|
||||||
|
projectCheckboxes.forEach(cb => {
|
||||||
|
if (assignedProjectIds.includes(parseInt(cb.value))) {
|
||||||
|
cb.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger initial load if projects are assigned
|
||||||
|
if (assignedProjectIds.length > 0) {
|
||||||
|
handleProjectSelection().then(() => {
|
||||||
|
// After locations are loaded, pre-select assigned locations
|
||||||
|
{% if assigned_location_names %}
|
||||||
|
setTimeout(() => {
|
||||||
|
const assignedLocations = {{ assigned_location_names | tojson }};
|
||||||
|
document.querySelectorAll('.location-checkbox').forEach(cb => {
|
||||||
|
if (assignedLocations.includes(cb.value)) {
|
||||||
|
cb.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
handleLocationSelection();
|
||||||
|
}, 500);
|
||||||
|
{% endif %}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle project selection changes
|
||||||
|
async function handleProjectSelection() {
|
||||||
|
const selectedProjects = Array.from(projectCheckboxes)
|
||||||
|
.filter(cb => cb.checked)
|
||||||
|
.map(cb => ({
|
||||||
|
id: cb.value,
|
||||||
|
name: cb.dataset.projectName
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update project summary
|
||||||
|
if (selectedProjects.length > 0) {
|
||||||
|
projectSummary.innerHTML = `
|
||||||
|
<i class="fas fa-check-circle" style="color: #10b981;"></i>
|
||||||
|
<span><strong>${selectedProjects.length}</strong> project${selectedProjects.length > 1 ? 's' : ''} selected</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show location step and load locations
|
||||||
|
locationStep.style.display = 'block';
|
||||||
|
await loadLocationsByProjects(selectedProjects.map(p => p.id));
|
||||||
|
} else {
|
||||||
|
projectSummary.innerHTML = `
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>No projects selected</span>
|
||||||
|
`;
|
||||||
|
locationStep.style.display = 'none';
|
||||||
|
permissionsOverview.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load locations for selected projects
|
||||||
|
async function loadLocationsByProjects(projectIds) {
|
||||||
|
locationLoading.style.display = 'flex';
|
||||||
|
locationCheckboxGroup.innerHTML = '';
|
||||||
|
locationEmpty.style.display = 'none';
|
||||||
|
locationSummary.innerHTML = `
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>Loading locations...</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/locations-by-projects', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ project_ids: projectIds })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
locationLoading.style.display = 'none';
|
||||||
|
|
||||||
|
if (data.success && data.locations.length > 0) {
|
||||||
|
// Populate location checkboxes
|
||||||
|
locationCheckboxGroup.innerHTML = data.locations.map(location => `
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="assigned_locations"
|
||||||
|
value="${location}"
|
||||||
|
class="checkbox-input location-checkbox"
|
||||||
|
>
|
||||||
|
<span class="checkbox-text">${location}</span>
|
||||||
|
</label>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Add event listeners to location checkboxes
|
||||||
|
document.querySelectorAll('.location-checkbox').forEach(cb => {
|
||||||
|
cb.addEventListener('change', handleLocationSelection);
|
||||||
|
});
|
||||||
|
|
||||||
|
locationSummary.innerHTML = `
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>${data.count} location${data.count > 1 ? 's' : ''} available</span>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
locationEmpty.style.display = 'block';
|
||||||
|
locationSummary.innerHTML = `
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
<span>No locations available</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePermissionsOverview();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading locations:', error);
|
||||||
|
locationLoading.style.display = 'none';
|
||||||
|
locationEmpty.style.display = 'block';
|
||||||
|
locationSummary.innerHTML = `
|
||||||
|
<i class="fas fa-exclamation-triangle" style="color: #ef4444;"></i>
|
||||||
|
<span>Error loading locations</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle location selection changes
|
||||||
|
function handleLocationSelection() {
|
||||||
|
const selectedLocations = Array.from(document.querySelectorAll('.location-checkbox:checked'));
|
||||||
|
|
||||||
|
if (selectedLocations.length > 0) {
|
||||||
|
locationSummary.innerHTML = `
|
||||||
|
<i class="fas fa-check-circle" style="color: #10b981;"></i>
|
||||||
|
<span><strong>${selectedLocations.length}</strong> location${selectedLocations.length > 1 ? 's' : ''} selected</span>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
const totalLocations = document.querySelectorAll('.location-checkbox').length;
|
||||||
|
locationSummary.innerHTML = `
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>${totalLocations} location${totalLocations > 1 ? 's' : ''} available</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePermissionsOverview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update permissions overview
|
||||||
|
function updatePermissionsOverview() {
|
||||||
|
const selectedProjects = Array.from(projectCheckboxes)
|
||||||
|
.filter(cb => cb.checked)
|
||||||
|
.map(cb => cb.dataset.projectName);
|
||||||
|
|
||||||
|
const selectedLocations = Array.from(document.querySelectorAll('.location-checkbox:checked'))
|
||||||
|
.map(cb => cb.value);
|
||||||
|
|
||||||
|
if (selectedProjects.length > 0 || selectedLocations.length > 0) {
|
||||||
|
permissionsOverview.style.display = 'block';
|
||||||
|
|
||||||
|
document.getElementById('overviewProjects').textContent =
|
||||||
|
selectedProjects.length > 0
|
||||||
|
? selectedProjects.join(', ')
|
||||||
|
: 'All projects';
|
||||||
|
|
||||||
|
document.getElementById('overviewLocations').textContent =
|
||||||
|
selectedLocations.length > 0
|
||||||
|
? selectedLocations.join(', ')
|
||||||
|
: 'All locations in selected projects';
|
||||||
|
} else {
|
||||||
|
permissionsOverview.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset permission flow
|
||||||
|
function resetPermissionFlow() {
|
||||||
|
projectCheckboxes.forEach(cb => {
|
||||||
|
cb.checked = false;
|
||||||
|
cb.removeEventListener('change', handleProjectSelection);
|
||||||
|
});
|
||||||
|
locationStep.style.display = 'none';
|
||||||
|
permissionsOverview.style.display = 'none';
|
||||||
|
locationCheckboxGroup.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function hideRoleWarning() {
|
function hideRoleWarning() {
|
||||||
roleWarning.classList.remove('fade-in');
|
roleWarning.classList.remove('fade-in');
|
||||||
setTimeout(() => roleWarning.style.display = 'none', 300);
|
setTimeout(() => roleWarning.style.display = 'none', 300);
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""
|
||||||
|
Database Migration Script for Project Manager Permissions (MySQL Version)
|
||||||
|
==========================================================================
|
||||||
|
|
||||||
|
This script creates the necessary tables for Project Manager role permissions.
|
||||||
|
It adds support for assigning specific projects and locations to Project Managers.
|
||||||
|
|
||||||
|
Tables Created:
|
||||||
|
1. user_project_permissions: Links users to projects they can access
|
||||||
|
2. user_location_permissions: Links users to locations they can access
|
||||||
|
|
||||||
|
Run this script ONCE after backing up your database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from sqlalchemy import text
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Initialize Flask app for migration
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'mysql://user:pass@localhost/qr_management')
|
||||||
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
def run_migration():
|
||||||
|
"""Execute the migration to add project manager permission tables"""
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("PROJECT MANAGER PERMISSIONS MIGRATION (MySQL)")
|
||||||
|
print("="*70 + "\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if tables already exist
|
||||||
|
print("🔍 Checking if migration is needed...")
|
||||||
|
|
||||||
|
result = db.session.execute(text("""
|
||||||
|
SELECT TABLE_NAME
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME IN ('user_project_permissions', 'user_location_permissions')
|
||||||
|
"""))
|
||||||
|
existing_tables = [row[0] for row in result.fetchall()]
|
||||||
|
|
||||||
|
if len(existing_tables) == 2:
|
||||||
|
print("✅ Migration tables already exist. No action needed.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if 'user_project_permissions' in existing_tables:
|
||||||
|
print("⚠️ user_project_permissions table already exists, skipping...")
|
||||||
|
else:
|
||||||
|
# Create user_project_permissions table
|
||||||
|
print("\n📝 Creating user_project_permissions table...")
|
||||||
|
db.session.execute(text("""
|
||||||
|
CREATE TABLE user_project_permissions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
project_id INT NOT NULL,
|
||||||
|
created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY unique_user_project (user_id, project_id),
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_project_id (project_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
|
"""))
|
||||||
|
print("✅ user_project_permissions table created successfully")
|
||||||
|
|
||||||
|
if 'user_location_permissions' in existing_tables:
|
||||||
|
print("⚠️ user_location_permissions table already exists, skipping...")
|
||||||
|
else:
|
||||||
|
# Create user_location_permissions table
|
||||||
|
print("\n📝 Creating user_location_permissions table...")
|
||||||
|
db.session.execute(text("""
|
||||||
|
CREATE TABLE user_location_permissions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
location_name VARCHAR(200) NOT NULL,
|
||||||
|
created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY unique_user_location (user_id, location_name),
|
||||||
|
INDEX idx_user_id (user_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
|
"""))
|
||||||
|
print("✅ user_location_permissions table created successfully")
|
||||||
|
|
||||||
|
# Commit all changes
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("✅ MIGRATION COMPLETED SUCCESSFULLY")
|
||||||
|
print("="*70)
|
||||||
|
print("\nNext Steps:")
|
||||||
|
print("1. The tables are ready for use")
|
||||||
|
print("2. You can now assign projects and locations to Project Managers")
|
||||||
|
print("3. Restart your application\n")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"\n❌ Migration failed: {e}")
|
||||||
|
print("Please check your database and try again.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("\n⚠️ IMPORTANT: Backup your database before running this migration!")
|
||||||
|
response = input("Continue with migration? (yes/no): ")
|
||||||
|
|
||||||
|
if response.lower() == 'yes':
|
||||||
|
success = run_migration()
|
||||||
|
if success:
|
||||||
|
print("\n✅ Migration completed. You can now restart your application.")
|
||||||
|
else:
|
||||||
|
print("\n❌ Migration failed. Please check the errors above.")
|
||||||
|
else:
|
||||||
|
print("\n❌ Migration cancelled.")
|
||||||
Reference in New Issue
Block a user