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
|
||||
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
|
||||
logger_handler = AppLogger(app, db)
|
||||
@@ -1816,28 +1816,42 @@ def users():
|
||||
@admin_required
|
||||
@log_database_operations('user_creation')
|
||||
def create_user():
|
||||
"""Create new user (Admin only)"""
|
||||
"""Create new user (Admin only) with Project Manager permissions support"""
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
full_name = request.form['full_name']
|
||||
email = request.form['email']
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
role = request.form['role']
|
||||
# Get basic form data
|
||||
full_name = request.form.get('full_name', '').strip()
|
||||
email = request.form.get('email', '').strip()
|
||||
username = request.form.get('username', '').strip()
|
||||
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
|
||||
if role not in VALID_ROLES:
|
||||
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
|
||||
if User.query.filter_by(username=username).first():
|
||||
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():
|
||||
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
|
||||
new_user = User(
|
||||
@@ -1850,6 +1864,69 @@ def create_user():
|
||||
new_user.set_password(password)
|
||||
|
||||
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()
|
||||
|
||||
# Log user creation
|
||||
@@ -1858,12 +1935,46 @@ def create_user():
|
||||
flash(f'User "{full_name}" created successfully with role "{role}".', 'success')
|
||||
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:
|
||||
db.session.rollback()
|
||||
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'])
|
||||
@admin_required
|
||||
@@ -2001,35 +2112,72 @@ def demote_user(user_id):
|
||||
|
||||
@app.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
@log_database_operations('user_update')
|
||||
@log_database_operations('user_edit')
|
||||
def edit_user(user_id):
|
||||
"""Edit user details (Admin only)"""
|
||||
"""Edit existing user with Project Manager permissions support"""
|
||||
try:
|
||||
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':
|
||||
# Track changes
|
||||
changes = {}
|
||||
# Store old values for change tracking
|
||||
old_values = {
|
||||
'full_name': user_to_edit.full_name,
|
||||
'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
|
||||
user_to_edit.full_name = request.form['full_name']
|
||||
user_to_edit.email = request.form['email']
|
||||
new_role = request.form['role']
|
||||
# Update basic info with validation
|
||||
full_name = request.form.get('full_name', '').strip()
|
||||
email = request.form.get('email', '').strip()
|
||||
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:
|
||||
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
|
||||
|
||||
# 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():
|
||||
user_to_edit.set_password(new_password)
|
||||
changes['password'] = 'Password updated'
|
||||
@@ -2040,25 +2188,141 @@ def edit_user(user_id):
|
||||
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
|
||||
for field, old_value in old_values.items():
|
||||
new_value = getattr(user_to_edit, field)
|
||||
if old_value != new_value:
|
||||
changes[field] = {'old': old_value, 'new': new_value}
|
||||
|
||||
# Commit all changes
|
||||
db.session.commit()
|
||||
|
||||
# Log user update
|
||||
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')
|
||||
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:
|
||||
db.session.rollback()
|
||||
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')
|
||||
return redirect(url_for('users'))
|
||||
|
||||
@@ -2241,6 +2505,47 @@ def user_stats_api():
|
||||
print(f"Error fetching user stats: {e}")
|
||||
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')
|
||||
@admin_required
|
||||
def role_permissions_api():
|
||||
|
||||
+2
-1
@@ -19,5 +19,6 @@ def set_db(database):
|
||||
from .attendance import AttendanceData
|
||||
from .employee import Employee
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
background-color: var(--gray-800);
|
||||
@@ -1234,3 +1302,198 @@ body:not(.has-sidebar) .main-content {
|
||||
gap: var(--spacing-3);
|
||||
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>
|
||||
|
||||
<!-- 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 -->
|
||||
<div class="info-panel" id="roleInfo" style="display: none">
|
||||
<div class="info-header">
|
||||
@@ -170,6 +278,18 @@ Code Management{% endblock %} {% block content %}
|
||||
const previewBtn = document.getElementById("previewUser");
|
||||
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
|
||||
const fullNameInput = document.getElementById("full_name");
|
||||
const emailInput = document.getElementById("email");
|
||||
@@ -213,17 +333,19 @@ Code Management{% endblock %} {% block content %}
|
||||
project_manager: {
|
||||
title: "Project Manager Permissions",
|
||||
permissions: [
|
||||
"Create and edit QR codes",
|
||||
"View all QR codes in the system",
|
||||
"View assigned projects only",
|
||||
"View assigned locations only",
|
||||
"Create and edit QR codes for assigned projects",
|
||||
"View attendance data for assigned projects/locations",
|
||||
"Download QR code images",
|
||||
"Update personal profile information",
|
||||
"Access dashboard and reports",
|
||||
"Same permissions as Staff (additional features coming soon)",
|
||||
"Access dashboard with filtered data",
|
||||
],
|
||||
restrictions: [
|
||||
"Cannot delete QR codes",
|
||||
"Cannot manage other users",
|
||||
"Cannot access admin settings",
|
||||
"Can only view data for assigned projects and locations",
|
||||
],
|
||||
},
|
||||
admin: {
|
||||
@@ -246,28 +368,23 @@ Code Management{% endblock %} {% block content %}
|
||||
|
||||
if (selectedRole && rolePermissions[selectedRole]) {
|
||||
const roleData = rolePermissions[selectedRole];
|
||||
|
||||
let permissionsHtml = `
|
||||
<h4>${roleData.title}</h4>
|
||||
<div class="permissions-section">
|
||||
<h5><i class="fas fa-check-circle text-success"></i> Permissions</h5>
|
||||
<ul class="permissions-list">
|
||||
`;
|
||||
|
||||
roleData.permissions.forEach((permission) => {
|
||||
permissionsHtml += `<li><i class="fas fa-check"></i> ${permission}</li>`;
|
||||
});
|
||||
|
||||
permissionsHtml += `
|
||||
</ul>
|
||||
<h5><i class="fas fa-times-circle text-warning"></i> Restrictions</h5>
|
||||
<ul class="restrictions-list">
|
||||
`;
|
||||
|
||||
roleData.restrictions.forEach((restriction) => {
|
||||
permissionsHtml += `<li><i class="fas fa-times"></i> ${restriction}</li>`;
|
||||
});
|
||||
|
||||
permissionsHtml += `</ul></div>`;
|
||||
|
||||
roleContent.innerHTML = permissionsHtml;
|
||||
@@ -277,8 +394,182 @@ Code Management{% endblock %} {% block content %}
|
||||
roleInfo.style.display = "none";
|
||||
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
|
||||
passwordInput.addEventListener("input", function () {
|
||||
const password = this.value;
|
||||
|
||||
+330
-1
@@ -51,7 +51,8 @@
|
||||
<i class="fas fa-user"></i>
|
||||
Username
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -81,6 +82,114 @@
|
||||
</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 -->
|
||||
<div class="password-section">
|
||||
<h3>
|
||||
@@ -186,6 +295,7 @@
|
||||
const passwordStrength = document.getElementById('passwordStrength');
|
||||
const resetPasswordBtn = document.getElementById('resetPassword');
|
||||
|
||||
|
||||
// Role change warning system
|
||||
roleSelect.addEventListener('change', function() {
|
||||
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() {
|
||||
roleWarning.classList.remove('fade-in');
|
||||
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