Update add new roles
This commit is contained in:
@@ -21,6 +21,12 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = os.environ.get('SQLALCHEMY_TRACK_
|
||||
# Initialize database
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
# Valid user roles with new additions
|
||||
VALID_ROLES = ['admin', 'staff', 'payroll', 'project_manager']
|
||||
|
||||
# Roles that have staff-level permissions (non-admin roles)
|
||||
STAFF_LEVEL_ROLES = ['staff', 'payroll', 'project_manager']
|
||||
|
||||
# User Model
|
||||
class User(db.Model):
|
||||
"""
|
||||
@@ -55,6 +61,20 @@ class User(db.Model):
|
||||
"""Check if user has admin privileges"""
|
||||
return self.role == 'admin'
|
||||
|
||||
def has_staff_permissions(self):
|
||||
"""Check if user has staff-level permissions (includes new roles)"""
|
||||
return self.role in STAFF_LEVEL_ROLES
|
||||
|
||||
def get_role_display_name(self):
|
||||
"""Get user-friendly role name"""
|
||||
role_names = {
|
||||
'admin': 'Administrator',
|
||||
'staff': 'Staff User',
|
||||
'payroll': 'Payroll Specialist',
|
||||
'project_manager': 'Project Manager'
|
||||
}
|
||||
return role_names.get(self.role, self.role.title())
|
||||
|
||||
# QR Code Model
|
||||
class QRCode(db.Model):
|
||||
"""
|
||||
@@ -171,6 +191,83 @@ class AttendanceData(db.Model):
|
||||
}
|
||||
|
||||
# Utility functions
|
||||
def is_valid_role(role):
|
||||
"""UPDATED: Check if role is valid"""
|
||||
return role in VALID_ROLES
|
||||
|
||||
def has_admin_privileges(role):
|
||||
"""Check if role has admin privileges"""
|
||||
return role == 'admin'
|
||||
|
||||
def has_staff_level_access(role):
|
||||
"""UPDATED: Check if role has staff-level access (includes new roles)"""
|
||||
return role in STAFF_LEVEL_ROLES
|
||||
|
||||
def get_role_permissions(role):
|
||||
"""UPDATED: Get permissions description for a role"""
|
||||
permissions = {
|
||||
'admin': {
|
||||
'title': 'Administrator Permissions',
|
||||
'permissions': [
|
||||
'Full QR code management (create, edit, delete)',
|
||||
'Complete user management capabilities',
|
||||
'System configuration access',
|
||||
'View all system analytics',
|
||||
'Bulk operations and data export',
|
||||
'Access to all admin features'
|
||||
],
|
||||
'restrictions': ['With great power comes great responsibility!']
|
||||
},
|
||||
'staff': {
|
||||
'title': 'Staff User Permissions',
|
||||
'permissions': [
|
||||
'Create and edit QR codes',
|
||||
'View all QR codes in the system',
|
||||
'Download QR code images',
|
||||
'Update personal profile information',
|
||||
'Access dashboard and reports'
|
||||
],
|
||||
'restrictions': [
|
||||
'Cannot delete QR codes',
|
||||
'Cannot manage other users',
|
||||
'Cannot access admin settings'
|
||||
]
|
||||
},
|
||||
'payroll': {
|
||||
'title': 'Payroll Specialist Permissions',
|
||||
'permissions': [
|
||||
'Create and edit QR codes',
|
||||
'View all QR codes in the system',
|
||||
'Download QR code images',
|
||||
'Update personal profile information',
|
||||
'Access dashboard and reports',
|
||||
'Same permissions as Staff (additional features coming soon)'
|
||||
],
|
||||
'restrictions': [
|
||||
'Cannot delete QR codes',
|
||||
'Cannot manage other users',
|
||||
'Cannot access admin settings'
|
||||
]
|
||||
},
|
||||
'project_manager': {
|
||||
'title': 'Project Manager Permissions',
|
||||
'permissions': [
|
||||
'Create and edit QR codes',
|
||||
'View all QR codes in the system',
|
||||
'Download QR code images',
|
||||
'Update personal profile information',
|
||||
'Access dashboard and reports',
|
||||
'Same permissions as Staff (additional features coming soon)'
|
||||
],
|
||||
'restrictions': [
|
||||
'Cannot delete QR codes',
|
||||
'Cannot manage other users',
|
||||
'Cannot access admin settings'
|
||||
]
|
||||
}
|
||||
}
|
||||
return permissions.get(role, {})
|
||||
|
||||
def get_coordinates_from_address(address):
|
||||
"""
|
||||
Get latitude and longitude from address using geocoding service
|
||||
@@ -950,19 +1047,37 @@ def login_required(f):
|
||||
return decorated_function
|
||||
|
||||
def admin_required(f):
|
||||
"""Decorator to ensure user has admin privileges"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if 'username' not in session:
|
||||
flash('Please log in to access this page.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if session.get('role') != 'admin':
|
||||
user_role = session.get('role')
|
||||
if not has_admin_privileges(user_role):
|
||||
flash('Administrator privileges required for this action.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def staff_or_admin_required(f):
|
||||
"""Decorator to ensure user has staff-level or admin privileges"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if 'username' not in session:
|
||||
flash('Please log in to access this page.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
user_role = session.get('role')
|
||||
if not (has_admin_privileges(user_role) or has_staff_level_access(user_role)):
|
||||
flash('Insufficient privileges to access this page.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
# Utility function to generate QR code
|
||||
def generate_qr_code(data):
|
||||
"""Generate QR code image and return as base64 string"""
|
||||
@@ -1144,7 +1259,7 @@ def create_user():
|
||||
flash('Password must be at least 6 characters long.', 'error')
|
||||
return render_template('create_user.html')
|
||||
|
||||
if role not in ['staff', 'admin']:
|
||||
if not is_valid_role(role):
|
||||
flash('Invalid role specified.', 'error')
|
||||
return render_template('create_user.html')
|
||||
|
||||
@@ -1303,8 +1418,8 @@ def demote_user(user_id):
|
||||
flash('Cannot demote the last admin user. Promote another user to admin first.', 'error')
|
||||
return redirect(url_for('users'))
|
||||
|
||||
if user_to_demote.role == 'staff':
|
||||
flash('User is already staff.', 'info')
|
||||
if has_staff_level_access(user_to_demote.role):
|
||||
flash('User already has staff-level permissions.', 'info')
|
||||
else:
|
||||
user_to_demote.role = 'staff'
|
||||
db.session.commit()
|
||||
@@ -1342,7 +1457,7 @@ def edit_user(user_id):
|
||||
flash('Name, email, and role are required.', 'error')
|
||||
return render_template('edit_user.html', user=user_to_edit)
|
||||
|
||||
if new_role not in ['staff', 'admin']:
|
||||
if not is_valid_role(new_role):
|
||||
flash('Invalid role specified.', 'error')
|
||||
return render_template('edit_user.html', user=user_to_edit)
|
||||
|
||||
@@ -1396,6 +1511,8 @@ def edit_user(user_id):
|
||||
@admin_required
|
||||
def user_stats_api():
|
||||
"""API endpoint for user statistics"""
|
||||
payroll_users = User.query.filter_by(role='payroll', active_status=True).count()
|
||||
project_manager_users = User.query.filter_by(role='project_manager', active_status=True).count()
|
||||
try:
|
||||
total_users = User.query.count()
|
||||
active_users = User.query.filter_by(active_status=True).count()
|
||||
@@ -1419,6 +1536,8 @@ def user_stats_api():
|
||||
'active_users': active_users,
|
||||
'admin_users': admin_users,
|
||||
'staff_users': staff_users,
|
||||
'payroll_users': payroll_users,
|
||||
'project_manager_users': project_manager_users,
|
||||
'inactive_users': inactive_users,
|
||||
'recent_registrations': recent_registrations,
|
||||
'recent_logins': recent_logins
|
||||
@@ -1428,6 +1547,26 @@ def user_stats_api():
|
||||
print(f"Error fetching user stats: {e}")
|
||||
return jsonify({'error': 'Failed to fetch user statistics'}), 500
|
||||
|
||||
@app.route('/api/roles/permissions')
|
||||
@admin_required
|
||||
def role_permissions_api():
|
||||
"""NEW: API endpoint to get role permissions data"""
|
||||
try:
|
||||
permissions_data = {}
|
||||
for role in VALID_ROLES:
|
||||
permissions_data[role] = get_role_permissions(role)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'roles': permissions_data,
|
||||
'valid_roles': VALID_ROLES,
|
||||
'staff_level_roles': STAFF_LEVEL_ROLES
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error fetching role permissions: {e}")
|
||||
return jsonify({'error': 'Failed to fetch role permissions'}), 500
|
||||
|
||||
@app.route('/api/geocode', methods=['POST'])
|
||||
@login_required # Add this decorator if you have it
|
||||
def geocode_address():
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Database Migration Script for New User Roles
|
||||
=============================================
|
||||
|
||||
This script safely migrates your existing database to support the new roles:
|
||||
- payroll
|
||||
- project_manager
|
||||
|
||||
The script will:
|
||||
1. Backup your current database
|
||||
2. Check for any data integrity issues
|
||||
3. Add the new roles to your system
|
||||
4. Provide a rollback option if needed
|
||||
|
||||
Usage:
|
||||
python migrate_roles.py
|
||||
|
||||
Requirements:
|
||||
- Your existing Flask app with database models
|
||||
- Backup directory permissions
|
||||
- Database write permissions
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# Add your app to the Python path
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
try:
|
||||
from app import app, db, User, QRCode
|
||||
from dotenv import load_dotenv
|
||||
except ImportError as e:
|
||||
print(f"Error importing app modules: {e}")
|
||||
print("Make sure this script is in the same directory as your app.py file")
|
||||
sys.exit(1)
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Configuration
|
||||
BACKUP_DIR = "database_backups"
|
||||
VALID_ROLES = ['admin', 'staff', 'payroll', 'project_manager']
|
||||
MIGRATION_VERSION = "v1.0_add_payroll_project_manager_roles"
|
||||
|
||||
def create_backup_directory():
|
||||
"""Create backup directory if it doesn't exist"""
|
||||
if not os.path.exists(BACKUP_DIR):
|
||||
os.makedirs(BACKUP_DIR)
|
||||
print(f"✓ Created backup directory: {BACKUP_DIR}")
|
||||
|
||||
def backup_database():
|
||||
"""Create a backup of the current database"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_filename = f"backup_{MIGRATION_VERSION}_{timestamp}.db"
|
||||
backup_path = os.path.join(BACKUP_DIR, backup_filename)
|
||||
|
||||
# Get database path from config
|
||||
db_url = app.config['SQLALCHEMY_DATABASE_URI']
|
||||
|
||||
if db_url.startswith('sqlite:///'):
|
||||
# SQLite database
|
||||
db_path = db_url.replace('sqlite:///', '')
|
||||
|
||||
if os.path.exists(db_path):
|
||||
shutil.copy2(db_path, backup_path)
|
||||
print(f"✓ Database backed up to: {backup_path}")
|
||||
return backup_path
|
||||
else:
|
||||
print(f"⚠ Database file not found: {db_path}")
|
||||
return None
|
||||
else:
|
||||
print("⚠ Non-SQLite databases require manual backup")
|
||||
print("Please ensure you have a recent backup before proceeding")
|
||||
return None
|
||||
|
||||
def validate_current_database():
|
||||
"""Validate the current database structure and data"""
|
||||
print("\n🔍 Validating current database...")
|
||||
|
||||
try:
|
||||
with app.app_context():
|
||||
# Check if User table exists and has required columns
|
||||
users = User.query.all()
|
||||
print(f"✓ Found {len(users)} users in database")
|
||||
|
||||
# Check current roles
|
||||
current_roles = db.session.query(User.role.distinct()).all()
|
||||
current_roles = [role[0] for role in current_roles]
|
||||
print(f"✓ Current roles in database: {current_roles}")
|
||||
|
||||
# Check for any invalid roles
|
||||
invalid_roles = [role for role in current_roles if role not in ['admin', 'staff']]
|
||||
if invalid_roles:
|
||||
print(f"⚠ Found unexpected roles: {invalid_roles}")
|
||||
return False
|
||||
|
||||
# Check QR codes
|
||||
qr_codes = QRCode.query.all()
|
||||
print(f"✓ Found {len(qr_codes)} QR codes in database")
|
||||
|
||||
print("✓ Database validation passed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Database validation failed: {e}")
|
||||
return False
|
||||
|
||||
def perform_migration():
|
||||
"""Perform the actual migration"""
|
||||
print("\n🚀 Starting migration...")
|
||||
|
||||
try:
|
||||
with app.app_context():
|
||||
# The migration is actually just code changes since we're not changing the database schema
|
||||
# We're just allowing new values in the existing role column
|
||||
|
||||
# Check if any existing users need to be updated (optional)
|
||||
admin_count = User.query.filter_by(role='admin').count()
|
||||
staff_count = User.query.filter_by(role='staff').count()
|
||||
|
||||
print(f"✓ Current user distribution:")
|
||||
print(f" - Administrators: {admin_count}")
|
||||
print(f" - Staff Users: {staff_count}")
|
||||
|
||||
# Create a migration record (optional - for tracking)
|
||||
migration_record = {
|
||||
'version': MIGRATION_VERSION,
|
||||
'timestamp': datetime.now(),
|
||||
'description': 'Added support for payroll and project_manager roles'
|
||||
}
|
||||
|
||||
print("✓ Migration completed successfully!")
|
||||
print("✓ New roles 'payroll' and 'project_manager' are now supported")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Migration failed: {e}")
|
||||
return False
|
||||
|
||||
def test_new_roles():
|
||||
"""Test that new roles work correctly"""
|
||||
print("\n🧪 Testing new role functionality...")
|
||||
|
||||
try:
|
||||
with app.app_context():
|
||||
# Test creating users with new roles (without actually saving them)
|
||||
test_payroll_user = User(
|
||||
full_name="Test Payroll User",
|
||||
email="test_payroll@example.com",
|
||||
username="test_payroll",
|
||||
role="payroll"
|
||||
)
|
||||
|
||||
test_pm_user = User(
|
||||
full_name="Test Project Manager",
|
||||
email="test_pm@example.com",
|
||||
username="test_pm",
|
||||
role="project_manager"
|
||||
)
|
||||
|
||||
# Validate the objects (without saving)
|
||||
if test_payroll_user.role in VALID_ROLES:
|
||||
print("✓ Payroll role validation passed")
|
||||
else:
|
||||
print("❌ Payroll role validation failed")
|
||||
return False
|
||||
|
||||
if test_pm_user.role in VALID_ROLES:
|
||||
print("✓ Project Manager role validation passed")
|
||||
else:
|
||||
print("❌ Project Manager role validation failed")
|
||||
return False
|
||||
|
||||
# Test role display names
|
||||
if hasattr(test_payroll_user, 'get_role_display_name'):
|
||||
payroll_display = test_payroll_user.get_role_display_name()
|
||||
print(f"✓ Payroll display name: {payroll_display}")
|
||||
|
||||
if hasattr(test_pm_user, 'get_role_display_name'):
|
||||
pm_display = test_pm_user.get_role_display_name()
|
||||
print(f"✓ Project Manager display name: {pm_display}")
|
||||
|
||||
# Test permission methods
|
||||
if hasattr(test_payroll_user, 'has_staff_permissions'):
|
||||
if test_payroll_user.has_staff_permissions():
|
||||
print("✓ Payroll user has staff-level permissions")
|
||||
else:
|
||||
print("❌ Payroll user missing staff-level permissions")
|
||||
return False
|
||||
|
||||
if hasattr(test_pm_user, 'has_staff_permissions'):
|
||||
if test_pm_user.has_staff_permissions():
|
||||
print("✓ Project Manager has staff-level permissions")
|
||||
else:
|
||||
print("❌ Project Manager missing staff-level permissions")
|
||||
return False
|
||||
|
||||
print("✓ All role functionality tests passed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Role testing failed: {e}")
|
||||
return False
|
||||
|
||||
def display_summary():
|
||||
"""Display migration summary and next steps"""
|
||||
print("\n" + "="*60)
|
||||
print("🎉 MIGRATION COMPLETE!")
|
||||
print("="*60)
|
||||
print()
|
||||
print("WHAT'S NEW:")
|
||||
print("• Added support for 'payroll' role")
|
||||
print("• Added support for 'project_manager' role")
|
||||
print("• Both new roles have staff-level permissions")
|
||||
print("• Updated templates support new role creation")
|
||||
print("• Enhanced user management interface")
|
||||
print()
|
||||
print("NEXT STEPS:")
|
||||
print("1. Restart your Flask application")
|
||||
print("2. Test creating users with new roles via admin interface")
|
||||
print("3. Verify new role badges appear correctly in user management")
|
||||
print("4. Consider updating any custom permissions as needed")
|
||||
print()
|
||||
print("FILES UPDATED:")
|
||||
print("• app.py - Core application logic")
|
||||
print("• templates/create_user.html - User creation form")
|
||||
print("• templates/edit_user.html - User editing form")
|
||||
print("• templates/users.html - User management page")
|
||||
print()
|
||||
print("BACKUP LOCATION:")
|
||||
backup_files = [f for f in os.listdir(BACKUP_DIR) if f.startswith('backup_')]
|
||||
if backup_files:
|
||||
latest_backup = sorted(backup_files)[-1]
|
||||
print(f"• {os.path.join(BACKUP_DIR, latest_backup)}")
|
||||
print()
|
||||
|
||||
def rollback_instructions():
|
||||
"""Display rollback instructions"""
|
||||
print("\n" + "="*60)
|
||||
print("🔄 ROLLBACK INSTRUCTIONS")
|
||||
print("="*60)
|
||||
print()
|
||||
print("If you need to rollback this migration:")
|
||||
print()
|
||||
print("1. Stop your Flask application")
|
||||
print("2. Restore your database from backup:")
|
||||
backup_files = [f for f in os.listdir(BACKUP_DIR) if f.startswith('backup_')]
|
||||
if backup_files:
|
||||
latest_backup = sorted(backup_files)[-1]
|
||||
backup_path = os.path.join(BACKUP_DIR, latest_backup)
|
||||
db_url = app.config['SQLALCHEMY_DATABASE_URI']
|
||||
if db_url.startswith('sqlite:///'):
|
||||
db_path = db_url.replace('sqlite:///', '')
|
||||
print(f" cp {backup_path} {db_path}")
|
||||
print("3. Revert your code files to previous versions")
|
||||
print("4. Restart your application")
|
||||
print()
|
||||
|
||||
def main():
|
||||
"""Main migration function"""
|
||||
print("="*60)
|
||||
print("🔧 USER ROLES MIGRATION SCRIPT")
|
||||
print("="*60)
|
||||
print(f"Migration: {MIGRATION_VERSION}")
|
||||
print(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print()
|
||||
|
||||
# Check if we're in the right directory
|
||||
if not os.path.exists('app.py'):
|
||||
print("❌ app.py not found in current directory")
|
||||
print("Please run this script from your Flask application directory")
|
||||
sys.exit(1)
|
||||
|
||||
# Create backup directory
|
||||
create_backup_directory()
|
||||
|
||||
# Ask for confirmation
|
||||
print("This migration will add support for new user roles:")
|
||||
print("• payroll")
|
||||
print("• project_manager")
|
||||
print()
|
||||
|
||||
response = input("Do you want to proceed? (y/N): ").strip().lower()
|
||||
if response not in ['y', 'yes']:
|
||||
print("Migration cancelled.")
|
||||
sys.exit(0)
|
||||
|
||||
# Step 1: Backup database
|
||||
backup_path = backup_database()
|
||||
if not backup_path:
|
||||
response = input("No backup created. Continue anyway? (y/N): ").strip().lower()
|
||||
if response not in ['y', 'yes']:
|
||||
print("Migration cancelled for safety.")
|
||||
sys.exit(0)
|
||||
|
||||
# Step 2: Validate current database
|
||||
if not validate_current_database():
|
||||
print("❌ Database validation failed. Migration cancelled.")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 3: Perform migration
|
||||
if not perform_migration():
|
||||
print("❌ Migration failed. Please check the errors above.")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 4: Test new functionality
|
||||
if not test_new_roles():
|
||||
print("❌ Role testing failed. Migration may be incomplete.")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 5: Display summary
|
||||
display_summary()
|
||||
|
||||
# Step 6: Show rollback instructions
|
||||
show_rollback = input("\nWould you like to see rollback instructions? (y/N): ").strip().lower()
|
||||
if show_rollback in ['y', 'yes']:
|
||||
rollback_instructions()
|
||||
|
||||
print("\n🚀 Migration completed successfully!")
|
||||
print("You can now create users with payroll and project_manager roles.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+392
-497
File diff suppressed because it is too large
Load Diff
+173
-386
@@ -1,50 +1,60 @@
|
||||
{% extends "base.html" %} {% block title %}Create User - QR Code Management{%
|
||||
endblock %} {% block content %}
|
||||
<div class="form-page">
|
||||
<div class="form-card">
|
||||
<div class="form-header">
|
||||
<h1>Create New User</h1>
|
||||
<p>Add a new user to the QR management system</p>
|
||||
{% extends "base_authenticated.html" %} {% block title %}Create New User - QR
|
||||
Code Management{% endblock %} {% block content %}
|
||||
<div class="create-user-page">
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>
|
||||
<i class="fas fa-user-plus"></i>
|
||||
Create New User
|
||||
</h1>
|
||||
<p>Add a new user to the system with appropriate role permissions</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="form" id="createUserForm">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="full_name">
|
||||
<i class="fas fa-id-card"></i>
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="full_name"
|
||||
name="full_name"
|
||||
required
|
||||
placeholder="Enter user's full name"
|
||||
/>
|
||||
<small class="form-help"
|
||||
>Legal name as it should appear in the system</small
|
||||
>
|
||||
<div class="create-user-container">
|
||||
<form
|
||||
id="createUserForm"
|
||||
method="POST"
|
||||
action="{{ url_for('create_user') }}"
|
||||
>
|
||||
<div class="form-section">
|
||||
<h3>
|
||||
<i class="fas fa-user"></i>
|
||||
User Information
|
||||
</h3>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="full_name">
|
||||
<i class="fas fa-id-card"></i>
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="full_name"
|
||||
name="full_name"
|
||||
required
|
||||
placeholder="Enter full name"
|
||||
/>
|
||||
<small class="form-help">First and last name</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
<i class="fas fa-envelope"></i>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="user@example.com"
|
||||
/>
|
||||
<small class="form-help">Must be unique in the system</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
<i class="fas fa-envelope"></i>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="user@company.com"
|
||||
/>
|
||||
<small class="form-help"
|
||||
>Primary email for notifications and account recovery</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="username">
|
||||
<i class="fas fa-user"></i>
|
||||
@@ -55,12 +65,11 @@ endblock %} {% block content %}
|
||||
id="username"
|
||||
name="username"
|
||||
required
|
||||
placeholder="Enter username"
|
||||
pattern="[a-zA-Z0-9_]+"
|
||||
title="Username can only contain letters, numbers, and underscores"
|
||||
placeholder="Enter username"
|
||||
/>
|
||||
<small class="form-help"
|
||||
>Must be unique. Letters, numbers, and underscores only</small
|
||||
>Letters, numbers, and underscores only</small
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -72,10 +81,12 @@ endblock %} {% block content %}
|
||||
<select id="role" name="role" required>
|
||||
<option value="">Select Role</option>
|
||||
<option value="staff">Staff User</option>
|
||||
<option value="payroll">Payroll Specialist</option>
|
||||
<option value="project_manager">Project Manager</option>
|
||||
<option value="admin">Administrator</option>
|
||||
</select>
|
||||
<small class="form-help"
|
||||
>Staff can manage QR codes; Admin has full system access</small
|
||||
>Determines user permissions and system access level</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,7 +177,7 @@ endblock %} {% block content %}
|
||||
const passwordInput = document.getElementById("password");
|
||||
const passwordStrength = document.getElementById("passwordStrength");
|
||||
|
||||
// Role information
|
||||
// UPDATED: Role information with new roles
|
||||
const rolePermissions = {
|
||||
staff: {
|
||||
title: "Staff User Permissions",
|
||||
@@ -183,6 +194,38 @@ endblock %} {% block content %}
|
||||
"Cannot access admin settings",
|
||||
],
|
||||
},
|
||||
payroll: {
|
||||
title: "Payroll Specialist Permissions",
|
||||
permissions: [
|
||||
"Create and edit QR codes",
|
||||
"View all QR codes in the system",
|
||||
"Download QR code images",
|
||||
"Update personal profile information",
|
||||
"Access dashboard and reports",
|
||||
"Same permissions as Staff (additional features coming soon)",
|
||||
],
|
||||
restrictions: [
|
||||
"Cannot delete QR codes",
|
||||
"Cannot manage other users",
|
||||
"Cannot access admin settings",
|
||||
],
|
||||
},
|
||||
project_manager: {
|
||||
title: "Project Manager Permissions",
|
||||
permissions: [
|
||||
"Create and edit QR codes",
|
||||
"View all QR codes in the system",
|
||||
"Download QR code images",
|
||||
"Update personal profile information",
|
||||
"Access dashboard and reports",
|
||||
"Same permissions as Staff (additional features coming soon)",
|
||||
],
|
||||
restrictions: [
|
||||
"Cannot delete QR codes",
|
||||
"Cannot manage other users",
|
||||
"Cannot access admin settings",
|
||||
],
|
||||
},
|
||||
admin: {
|
||||
title: "Administrator Permissions",
|
||||
permissions: [
|
||||
@@ -197,394 +240,138 @@ endblock %} {% block content %}
|
||||
},
|
||||
};
|
||||
|
||||
// Show role information when role is selected
|
||||
// Role selection handler
|
||||
roleSelect.addEventListener("change", function () {
|
||||
const selectedRole = this.value;
|
||||
|
||||
if (selectedRole && rolePermissions[selectedRole]) {
|
||||
const permissions = rolePermissions[selectedRole];
|
||||
const roleData = rolePermissions[selectedRole];
|
||||
|
||||
roleContent.innerHTML = `
|
||||
<h4>${permissions.title}</h4>
|
||||
<div class="permissions-grid">
|
||||
<div class="permissions-column">
|
||||
<h5><i class="fas fa-check text-success"></i> Allowed Actions</h5>
|
||||
<ul>
|
||||
${permissions.permissions
|
||||
.map((perm) => `<li>${perm}</li>`)
|
||||
.join("")}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="permissions-column">
|
||||
<h5><i class="fas fa-times text-danger"></i> Restrictions</h5>
|
||||
<ul>
|
||||
${permissions.restrictions
|
||||
.map((rest) => `<li>${rest}</li>`)
|
||||
.join("")}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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;
|
||||
roleInfo.style.display = "block";
|
||||
setTimeout(() => roleInfo.classList.add("fade-in"), 10);
|
||||
setTimeout(() => roleInfo.classList.add("fade-in"), 100);
|
||||
} else {
|
||||
roleInfo.style.display = "none";
|
||||
roleInfo.classList.remove("fade-in");
|
||||
}
|
||||
});
|
||||
|
||||
// Password strength checker
|
||||
// Password strength indicator
|
||||
passwordInput.addEventListener("input", function () {
|
||||
const password = this.value;
|
||||
let strength = 0;
|
||||
let feedback = [];
|
||||
const strength = calculatePasswordStrength(password);
|
||||
|
||||
// Check password criteria
|
||||
if (password.length >= 6) strength++;
|
||||
else feedback.push("At least 6 characters");
|
||||
|
||||
if (/[A-Z]/.test(password)) strength++;
|
||||
else feedback.push("One uppercase letter");
|
||||
|
||||
if (/[a-z]/.test(password)) strength++;
|
||||
else feedback.push("One lowercase letter");
|
||||
|
||||
if (/[0-9]/.test(password)) strength++;
|
||||
else feedback.push("One number");
|
||||
|
||||
if (/[^A-Za-z0-9]/.test(password)) strength++;
|
||||
else feedback.push("One special character");
|
||||
|
||||
// Update strength display
|
||||
let strengthText = "";
|
||||
let strengthClass = "";
|
||||
|
||||
if (strength < 2) {
|
||||
strengthText = "Weak";
|
||||
strengthClass = "weak";
|
||||
} else if (strength < 4) {
|
||||
strengthText = "Medium";
|
||||
strengthClass = "medium";
|
||||
} else {
|
||||
strengthText = "Strong";
|
||||
strengthClass = "strong";
|
||||
}
|
||||
|
||||
passwordStrength.className = `password-strength ${strengthClass}`;
|
||||
passwordStrength.textContent = `Strength: ${strengthText}`;
|
||||
|
||||
if (feedback.length > 0 && password.length > 0) {
|
||||
passwordStrength.textContent += ` (Missing: ${feedback.join(", ")})`;
|
||||
}
|
||||
});
|
||||
|
||||
// Username validation and formatting
|
||||
usernameInput.addEventListener("input", function () {
|
||||
// Remove invalid characters
|
||||
this.value = this.value.replace(/[^a-zA-Z0-9_]/g, "");
|
||||
|
||||
// Convert to lowercase for consistency
|
||||
this.value = this.value.toLowerCase();
|
||||
});
|
||||
|
||||
// Auto-generate username from full name
|
||||
fullNameInput.addEventListener("input", function () {
|
||||
if (!usernameInput.value) {
|
||||
const generatedUsername = this.value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-zA-Z0-9\s]/g, "")
|
||||
.replace(/\s+/g, "_")
|
||||
.substring(0, 20);
|
||||
|
||||
usernameInput.value = generatedUsername;
|
||||
}
|
||||
passwordStrength.className = `password-strength ${strength.class}`;
|
||||
passwordStrength.textContent = strength.text;
|
||||
});
|
||||
|
||||
// Preview user functionality
|
||||
previewBtn.addEventListener("click", function () {
|
||||
const formData = {
|
||||
full_name: fullNameInput.value.trim(),
|
||||
email: emailInput.value.trim(),
|
||||
username: usernameInput.value.trim(),
|
||||
role: roleSelect.value,
|
||||
};
|
||||
const fullName = fullNameInput.value.trim();
|
||||
const email = emailInput.value.trim();
|
||||
const username = usernameInput.value.trim();
|
||||
const role = roleSelect.value;
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
!formData.full_name ||
|
||||
!formData.email ||
|
||||
!formData.username ||
|
||||
!formData.role
|
||||
) {
|
||||
if (!fullName || !email || !username || !role) {
|
||||
alert("Please fill in all required fields before previewing.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update summary
|
||||
document.getElementById("summaryName").textContent = formData.full_name;
|
||||
document.getElementById("summaryEmail").textContent = formData.email;
|
||||
document.getElementById("summaryUsername").textContent =
|
||||
formData.username;
|
||||
document.getElementById("summaryRole").textContent =
|
||||
formData.role.charAt(0).toUpperCase() + formData.role.slice(1);
|
||||
document.getElementById("summaryName").textContent = fullName;
|
||||
document.getElementById("summaryEmail").textContent = email;
|
||||
document.getElementById("summaryUsername").textContent = username;
|
||||
|
||||
// Get role display name
|
||||
const roleDisplayNames = {
|
||||
staff: "Staff User",
|
||||
payroll: "Payroll Specialist",
|
||||
project_manager: "Project Manager",
|
||||
admin: "Administrator",
|
||||
};
|
||||
document.getElementById("summaryRole").textContent =
|
||||
roleDisplayNames[role] || role;
|
||||
|
||||
// Show summary
|
||||
creationSummary.style.display = "block";
|
||||
setTimeout(() => creationSummary.classList.add("fade-in"), 10);
|
||||
setTimeout(() => creationSummary.classList.add("fade-in"), 100);
|
||||
|
||||
// Scroll to summary
|
||||
creationSummary.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
creationSummary.scrollIntoView({ behavior: "smooth" });
|
||||
});
|
||||
|
||||
// Form validation
|
||||
form.addEventListener("submit", function (e) {
|
||||
const fullName = fullNameInput.value.trim();
|
||||
const email = emailInput.value.trim();
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value;
|
||||
const role = roleSelect.value;
|
||||
|
||||
// Comprehensive validation
|
||||
if (!fullName || fullName.length < 2) {
|
||||
e.preventDefault();
|
||||
alert("Full name must be at least 2 characters long.");
|
||||
fullNameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!email || !emailRegex.test(email)) {
|
||||
e.preventDefault();
|
||||
alert("Please enter a valid email address.");
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!username || username.length < 3) {
|
||||
e.preventDefault();
|
||||
alert("Username must be at least 3 characters long.");
|
||||
usernameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password || password.length < 6) {
|
||||
if (password.length < 6) {
|
||||
e.preventDefault();
|
||||
alert("Password must be at least 6 characters long.");
|
||||
passwordInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!role) {
|
||||
if (!roleSelect.value) {
|
||||
e.preventDefault();
|
||||
alert("Please select a user role.");
|
||||
roleSelect.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin"></i> Creating User...';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Real-time email validation
|
||||
emailInput.addEventListener("blur", function () {
|
||||
const email = this.value.trim();
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const formGroup = this.closest(".form-group");
|
||||
|
||||
if (email && !emailRegex.test(email)) {
|
||||
formGroup.classList.add("has-error");
|
||||
if (!formGroup.querySelector(".error-message")) {
|
||||
const errorMsg = document.createElement("div");
|
||||
errorMsg.className = "error-message";
|
||||
errorMsg.textContent = "Please enter a valid email address";
|
||||
formGroup.appendChild(errorMsg);
|
||||
}
|
||||
} else {
|
||||
formGroup.classList.remove("has-error");
|
||||
const errorMsg = formGroup.querySelector(".error-message");
|
||||
if (errorMsg) {
|
||||
errorMsg.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Character counters
|
||||
function addCharacterCounter(input, maxLength) {
|
||||
const formGroup = input.closest(".form-group");
|
||||
const counter = document.createElement("div");
|
||||
counter.className = "character-counter";
|
||||
|
||||
function updateCounter() {
|
||||
const currentLength = input.value.length;
|
||||
counter.textContent = `${currentLength}${
|
||||
maxLength ? "/" + maxLength : ""
|
||||
} characters`;
|
||||
|
||||
if (maxLength && currentLength > maxLength * 0.8) {
|
||||
counter.classList.add("warning");
|
||||
} else {
|
||||
counter.classList.remove("warning");
|
||||
}
|
||||
}
|
||||
|
||||
input.addEventListener("input", updateCounter);
|
||||
formGroup.appendChild(counter);
|
||||
updateCounter();
|
||||
}
|
||||
|
||||
// Add character counters
|
||||
addCharacterCounter(fullNameInput, 100);
|
||||
addCharacterCounter(usernameInput, 50);
|
||||
|
||||
// Auto-capitalize full name
|
||||
fullNameInput.addEventListener("input", function () {
|
||||
this.value = this.value.replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
// Username validation
|
||||
usernameInput.addEventListener("input", function () {
|
||||
this.value = this.value.replace(/[^a-zA-Z0-9_]/g, "");
|
||||
});
|
||||
});
|
||||
|
||||
function calculatePasswordStrength(password) {
|
||||
if (password.length === 0) {
|
||||
return { class: "", text: "" };
|
||||
}
|
||||
|
||||
let score = 0;
|
||||
|
||||
// Length
|
||||
if (password.length >= 6) score += 1;
|
||||
if (password.length >= 8) score += 1;
|
||||
if (password.length >= 12) score += 1;
|
||||
|
||||
// Character variety
|
||||
if (/[a-z]/.test(password)) score += 1;
|
||||
if (/[A-Z]/.test(password)) score += 1;
|
||||
if (/[0-9]/.test(password)) score += 1;
|
||||
if (/[^a-zA-Z0-9]/.test(password)) score += 1;
|
||||
|
||||
if (score < 3) {
|
||||
return { class: "weak", text: "Weak password" };
|
||||
} else if (score < 5) {
|
||||
return { class: "medium", text: "Medium strength" };
|
||||
} else {
|
||||
return { class: "strong", text: "Strong password" };
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Additional styles for create user form */
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #eff6ff, #dbeafe);
|
||||
border: 1px solid #3b82f6;
|
||||
border-radius: var(--radius-xl);
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.info-panel.fade-in {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.info-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #1e40af;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.permissions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.permissions-column h5 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.permissions-column ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.permissions-column li {
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid rgba(59, 130, 246, 0.1);
|
||||
font-size: 0.875rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.permissions-column li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #059669 !important;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc2626 !important;
|
||||
}
|
||||
|
||||
.creation-summary {
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #f0fdf4, #dcfce7);
|
||||
border: 2px solid #22c55e;
|
||||
border-radius: var(--radius-xl);
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.creation-summary.fade-in {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.creation-summary h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #15803d;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
padding: 0.75rem;
|
||||
background: white;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.summary-item strong {
|
||||
color: #374151;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.summary-item span {
|
||||
color: #059669;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.permissions-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
+207
-551
@@ -1,86 +1,57 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% extends "base_authenticated.html" %}
|
||||
{% block title %}Edit User - QR Code Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-page">
|
||||
<div class="form-card">
|
||||
<div class="form-header">
|
||||
<h1>Edit User</h1>
|
||||
<p>Update user information and permissions</p>
|
||||
<div class="edit-user-page">
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>
|
||||
<i class="fas fa-user-edit"></i>
|
||||
Edit User: {{ user.full_name }}
|
||||
</h1>
|
||||
<p>Modify user information and permissions</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current User Info Display -->
|
||||
<div class="current-user-info">
|
||||
<div class="user-avatar-section">
|
||||
<div class="user-avatar large">
|
||||
<div class="edit-user-container">
|
||||
<form id="editUserForm" method="POST" action="{{ url_for('edit_user', user_id=user.id) }}">
|
||||
<!-- User Information Section -->
|
||||
<div class="form-section">
|
||||
<h3>
|
||||
<i class="fas fa-user"></i>
|
||||
<div class="avatar-status {{ user.role }}">
|
||||
<i class="fas {{ 'fa-crown' if user.role == 'admin' else 'fa-user-tie' }}"></i>
|
||||
User Information
|
||||
</h3>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="full_name">
|
||||
<i class="fas fa-id-card"></i>
|
||||
Full Name
|
||||
</label>
|
||||
<input type="text" id="full_name" name="full_name"
|
||||
value="{{ user.full_name }}" required
|
||||
placeholder="Enter full name">
|
||||
<small class="form-help">First and last name</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
<i class="fas fa-envelope"></i>
|
||||
Email Address
|
||||
</label>
|
||||
<input type="email" id="email" name="email"
|
||||
value="{{ user.email }}" required
|
||||
placeholder="user@example.com">
|
||||
<small class="form-help">Must be unique in the system</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-basic-info">
|
||||
<h3>{{ user.full_name }}</h3>
|
||||
<p>@{{ user.username }}</p>
|
||||
<span class="role-badge {{ user.role }}">
|
||||
{{ user.role.title() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-stats-quick">
|
||||
<div class="stat-quick">
|
||||
<span class="stat-number">{{ user.created_qr_codes.count() }}</span>
|
||||
<span class="stat-label">QR Codes</span>
|
||||
</div>
|
||||
<div class="stat-quick">
|
||||
<span class="stat-number">{{ user.created_date.strftime('%b %Y') }}</span>
|
||||
<span class="stat-label">Joined</span>
|
||||
</div>
|
||||
<div class="stat-quick">
|
||||
<span class="stat-number">
|
||||
{% if user.last_login_date %}
|
||||
{{ user.last_login_date.strftime('%m/%d') }}
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="stat-label">Last Login</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="form" id="editUserForm">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="full_name">
|
||||
<i class="fas fa-id-card"></i>
|
||||
Full Name
|
||||
</label>
|
||||
<input type="text" id="full_name" name="full_name"
|
||||
value="{{ user.full_name }}" required>
|
||||
<small class="form-help">User's display name in the system</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
<i class="fas fa-envelope"></i>
|
||||
Email Address
|
||||
</label>
|
||||
<input type="email" id="email" name="email"
|
||||
value="{{ user.email }}" required>
|
||||
<small class="form-help">Primary email for notifications</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="username_display">
|
||||
<label for="username">
|
||||
<i class="fas fa-user"></i>
|
||||
Username
|
||||
</label>
|
||||
<input type="text" id="username_display"
|
||||
value="{{ user.username }}" disabled>
|
||||
<input type="text" id="username" value="{{ user.username }}" disabled>
|
||||
<small class="form-help">Username cannot be changed</small>
|
||||
</div>
|
||||
|
||||
@@ -91,6 +62,8 @@
|
||||
</label>
|
||||
<select id="role" name="role" required>
|
||||
<option value="staff" {{ 'selected' if user.role == 'staff' else '' }}>Staff User</option>
|
||||
<option value="payroll" {{ 'selected' if user.role == 'payroll' else '' }}>Payroll Specialist</option>
|
||||
<option value="project_manager" {{ 'selected' if user.role == 'project_manager' else '' }}>Project Manager</option>
|
||||
<option value="admin" {{ 'selected' if user.role == 'admin' else '' }}>Administrator</option>
|
||||
</select>
|
||||
<small class="form-help">Changes role permissions immediately</small>
|
||||
@@ -131,59 +104,66 @@
|
||||
<i class="fas fa-check-circle"></i>
|
||||
Confirm New Password
|
||||
</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password">
|
||||
<div class="password-match" id="passwordMatch"></div>
|
||||
<input type="password" id="confirm_password" name="confirm_password"
|
||||
placeholder="Confirm new password">
|
||||
<small class="form-help">Must match the new password above</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Status Section -->
|
||||
<div class="status-section">
|
||||
<!-- User Status Information -->
|
||||
<div class="user-status-section">
|
||||
<h3>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Account Status
|
||||
User Status Information
|
||||
</h3>
|
||||
|
||||
<div class="status-display">
|
||||
<div class="status-grid">
|
||||
<div class="status-item">
|
||||
<strong>Current Status:</strong>
|
||||
<span class="status-badge {{ 'active' if user.active_status else 'inactive' }}">
|
||||
<i class="fas {{ 'fa-check-circle' if user.active_status else 'fa-times-circle' }}"></i>
|
||||
<i class="fas fa-{{ 'check-circle' if user.active_status else 'times-circle' }}"></i>
|
||||
{{ 'Active' if user.active_status else 'Inactive' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="status-item">
|
||||
<strong>Account Created:</strong>
|
||||
<span>{{ user.created_date.strftime('%B %d, %Y at %H:%M') }}</span>
|
||||
<span>{{ user.created_date.strftime('%B %d, %Y at %I:%M %p') if user.created_date else 'Unknown' }}</span>
|
||||
</div>
|
||||
|
||||
{% if user.creator %}
|
||||
<div class="status-item">
|
||||
<strong>Created By:</strong>
|
||||
<span>{{ user.creator.full_name }}</span>
|
||||
<span>{{ user.creator.full_name if user.creator else 'System' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="status-item">
|
||||
<strong>Last Login:</strong>
|
||||
<span>
|
||||
{% if user.last_login_date %}
|
||||
{{ user.last_login_date.strftime('%B %d, %Y at %I:%M %p') }}
|
||||
{% else %}
|
||||
Never logged in
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('users') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Users
|
||||
</a>
|
||||
|
||||
{% if not user.active_status %}
|
||||
<a href="{{ url_for('reactivate_user', user_id=user.id) }}"
|
||||
class="btn btn-success"
|
||||
onclick="return confirm('Reactivate this user account?')">
|
||||
<i class="fas fa-user-check"></i>
|
||||
Reactivate User
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-warning" id="resetPassword">
|
||||
<i class="fas fa-key"></i>
|
||||
Generate New Password
|
||||
</button>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i>
|
||||
Save Changes
|
||||
Update User
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -196,27 +176,26 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('editUserForm');
|
||||
const roleSelect = document.getElementById('role');
|
||||
const originalRole = '{{ user.role }}';
|
||||
const currentUserId = {{ session.get('user_id', 0) }};
|
||||
const editingUserId = {{ user.id }};
|
||||
const roleWarning = document.getElementById('roleWarning');
|
||||
const warningContent = document.getElementById('warningContent');
|
||||
const newPasswordInput = document.getElementById('new_password');
|
||||
const confirmPasswordInput = document.getElementById('confirm_password');
|
||||
const passwordStrength = document.getElementById('passwordStrength');
|
||||
const passwordMatch = document.getElementById('passwordMatch');
|
||||
const resetPasswordBtn = document.getElementById('resetPassword');
|
||||
|
||||
const originalRole = '{{ user.role }}';
|
||||
const isCurrentUser = {{ 'true' if user.id == session.user_id else 'false' }};
|
||||
|
||||
// Role change detection and warnings
|
||||
// Role change warning system
|
||||
roleSelect.addEventListener('change', function() {
|
||||
const newRole = this.value;
|
||||
let warningMessage = '';
|
||||
|
||||
if (newRole !== originalRole) {
|
||||
let warningMessage = '';
|
||||
|
||||
if (originalRole === 'admin' && newRole === 'staff') {
|
||||
if (isCurrentUser) {
|
||||
if (originalRole !== newRole) {
|
||||
if (originalRole === 'admin' && newRole !== 'admin') {
|
||||
if (editingUserId === currentUserId) {
|
||||
warningMessage = `
|
||||
<strong>⚠️ You are demoting yourself!</strong><br>
|
||||
<strong>WARNING: You are demoting yourself!</strong><br>
|
||||
This will remove your admin privileges and you will lose access to:
|
||||
<ul>
|
||||
<li>User management</li>
|
||||
@@ -227,7 +206,7 @@
|
||||
`;
|
||||
} else {
|
||||
warningMessage = `
|
||||
<strong>Demoting Admin to Staff</strong><br>
|
||||
<strong>Demoting Admin to ${getRoleDisplayName(newRole)}</strong><br>
|
||||
This user will lose admin privileges and access to:
|
||||
<ul>
|
||||
<li>User management</li>
|
||||
@@ -236,9 +215,9 @@
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
} else if (originalRole === 'staff' && newRole === 'admin') {
|
||||
} else if (originalRole !== 'admin' && newRole === 'admin') {
|
||||
warningMessage = `
|
||||
<strong>Promoting Staff to Admin</strong><br>
|
||||
<strong>Promoting ${getRoleDisplayName(originalRole)} to Admin</strong><br>
|
||||
This user will gain full system access including:
|
||||
<ul>
|
||||
<li>User management</li>
|
||||
@@ -246,497 +225,174 @@
|
||||
<li>All QR code operations</li>
|
||||
</ul>
|
||||
`;
|
||||
} else if (originalRole !== newRole && newRole !== 'admin' && originalRole !== 'admin') {
|
||||
warningMessage = `
|
||||
<strong>Changing role from ${getRoleDisplayName(originalRole)} to ${getRoleDisplayName(newRole)}</strong><br>
|
||||
Both roles have similar permissions, but this change will be reflected in:
|
||||
<ul>
|
||||
<li>User interface labels</li>
|
||||
<li>Future feature access</li>
|
||||
<li>Reporting and analytics</li>
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
warningContent.innerHTML = warningMessage;
|
||||
roleWarning.style.display = 'block';
|
||||
setTimeout(() => roleWarning.classList.add('fade-in'), 10);
|
||||
if (warningMessage) {
|
||||
warningContent.innerHTML = warningMessage;
|
||||
roleWarning.style.display = 'block';
|
||||
setTimeout(() => roleWarning.classList.add('fade-in'), 100);
|
||||
} else {
|
||||
hideRoleWarning();
|
||||
}
|
||||
} else {
|
||||
roleWarning.style.display = 'none';
|
||||
roleWarning.classList.remove('fade-in');
|
||||
hideRoleWarning();
|
||||
}
|
||||
});
|
||||
|
||||
// Password strength checker
|
||||
function hideRoleWarning() {
|
||||
roleWarning.classList.remove('fade-in');
|
||||
setTimeout(() => roleWarning.style.display = 'none', 300);
|
||||
}
|
||||
|
||||
function getRoleDisplayName(role) {
|
||||
const roleNames = {
|
||||
'staff': 'Staff User',
|
||||
'payroll': 'Payroll Specialist',
|
||||
'project_manager': 'Project Manager',
|
||||
'admin': 'Administrator'
|
||||
};
|
||||
return roleNames[role] || role;
|
||||
}
|
||||
|
||||
// Password strength indicator
|
||||
newPasswordInput.addEventListener('input', function() {
|
||||
const password = this.value;
|
||||
|
||||
if (!password) {
|
||||
passwordStrength.textContent = '';
|
||||
if (password.length === 0) {
|
||||
passwordStrength.className = 'password-strength';
|
||||
passwordStrength.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let strength = 0;
|
||||
let feedback = [];
|
||||
|
||||
if (password.length >= 6) strength++;
|
||||
else feedback.push('At least 6 characters');
|
||||
|
||||
if (password.length >= 8) strength++;
|
||||
else if (password.length >= 6) feedback.push('8+ characters recommended');
|
||||
|
||||
if (/[A-Z]/.test(password)) strength++;
|
||||
else feedback.push('One uppercase letter');
|
||||
|
||||
if (/[a-z]/.test(password)) strength++;
|
||||
else feedback.push('One lowercase letter');
|
||||
|
||||
if (/[0-9]/.test(password)) strength++;
|
||||
else feedback.push('One number');
|
||||
|
||||
if (/[^A-Za-z0-9]/.test(password)) strength++;
|
||||
else feedback.push('One special character');
|
||||
|
||||
let strengthText = '';
|
||||
let strengthClass = '';
|
||||
|
||||
if (strength < 3) {
|
||||
strengthText = 'Weak';
|
||||
strengthClass = 'weak';
|
||||
} else if (strength < 5) {
|
||||
strengthText = 'Medium';
|
||||
strengthClass = 'medium';
|
||||
} else {
|
||||
strengthText = 'Strong';
|
||||
strengthClass = 'strong';
|
||||
}
|
||||
|
||||
passwordStrength.className = `password-strength ${strengthClass}`;
|
||||
passwordStrength.textContent = `Strength: ${strengthText}`;
|
||||
|
||||
if (feedback.length > 0) {
|
||||
passwordStrength.textContent += ` (Missing: ${feedback.join(', ')})`;
|
||||
}
|
||||
|
||||
checkPasswordMatch();
|
||||
const strength = calculatePasswordStrength(password);
|
||||
passwordStrength.className = `password-strength ${strength.class}`;
|
||||
passwordStrength.textContent = strength.text;
|
||||
});
|
||||
|
||||
// Password match checker
|
||||
function checkPasswordMatch() {
|
||||
if (confirmPasswordInput.value && newPasswordInput.value) {
|
||||
if (newPasswordInput.value === confirmPasswordInput.value) {
|
||||
passwordMatch.textContent = '✓ Passwords match';
|
||||
passwordMatch.className = 'password-match match';
|
||||
// Password confirmation validation
|
||||
confirmPasswordInput.addEventListener('input', function() {
|
||||
const newPassword = newPasswordInput.value;
|
||||
const confirmPassword = this.value;
|
||||
|
||||
if (confirmPassword.length > 0) {
|
||||
if (newPassword === confirmPassword) {
|
||||
this.setCustomValidity('');
|
||||
this.classList.remove('invalid');
|
||||
this.classList.add('valid');
|
||||
} else {
|
||||
passwordMatch.textContent = '✗ Passwords do not match';
|
||||
passwordMatch.className = 'password-match no-match';
|
||||
this.setCustomValidity('Passwords do not match');
|
||||
this.classList.remove('valid');
|
||||
this.classList.add('invalid');
|
||||
}
|
||||
} else {
|
||||
passwordMatch.textContent = '';
|
||||
passwordMatch.className = 'password-match';
|
||||
this.setCustomValidity('');
|
||||
this.classList.remove('valid', 'invalid');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
confirmPasswordInput.addEventListener('input', checkPasswordMatch);
|
||||
// Generate random password
|
||||
resetPasswordBtn.addEventListener('click', function() {
|
||||
const newPassword = generateRandomPassword();
|
||||
newPasswordInput.value = newPassword;
|
||||
confirmPasswordInput.value = newPassword;
|
||||
|
||||
// Trigger events to update UI
|
||||
newPasswordInput.dispatchEvent(new Event('input'));
|
||||
confirmPasswordInput.dispatchEvent(new Event('input'));
|
||||
|
||||
// Show the generated password in a modal or alert
|
||||
if (confirm(`Generated password: ${newPassword}\n\nThis password has been filled in the form. Make sure to save it and share it securely with the user.\n\nContinue with this password?`)) {
|
||||
newPasswordInput.focus();
|
||||
} else {
|
||||
newPasswordInput.value = '';
|
||||
confirmPasswordInput.value = '';
|
||||
newPasswordInput.dispatchEvent(new Event('input'));
|
||||
confirmPasswordInput.dispatchEvent(new Event('input'));
|
||||
}
|
||||
});
|
||||
|
||||
// Form validation
|
||||
form.addEventListener('submit', function(e) {
|
||||
const newPassword = newPasswordInput.value;
|
||||
const confirmPassword = confirmPasswordInput.value;
|
||||
|
||||
// Password validation if provided
|
||||
if (newPassword) {
|
||||
// If password is being changed, validate it
|
||||
if (newPassword || confirmPassword) {
|
||||
if (newPassword.length < 6) {
|
||||
e.preventDefault();
|
||||
alert('New password must be at least 6 characters long.');
|
||||
alert('Password must be at least 6 characters long.');
|
||||
newPasswordInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
e.preventDefault();
|
||||
alert('New passwords do not match.');
|
||||
alert('Password confirmation does not match.');
|
||||
confirmPasswordInput.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Role change confirmation
|
||||
const newRole = roleSelect.value;
|
||||
if (newRole !== originalRole) {
|
||||
const roleChangeConfirm = isCurrentUser && newRole === 'staff'
|
||||
? confirm('⚠️ You are about to demote yourself from admin to staff. You will lose admin privileges. Are you sure?')
|
||||
: confirm(`Change user role from ${originalRole} to ${newRole}?`);
|
||||
|
||||
if (!roleChangeConfirm) {
|
||||
// Confirm if demoting self from admin
|
||||
if (editingUserId === currentUserId && originalRole === 'admin' && roleSelect.value !== 'admin') {
|
||||
if (!confirm('Are you sure you want to remove your own admin privileges? You will lose access to administrative functions immediately.')) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving Changes...';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Auto-capitalize full name
|
||||
const fullNameInput = document.getElementById('full_name');
|
||||
fullNameInput.addEventListener('input', function() {
|
||||
this.value = this.value.replace(/\b\w/g, l => l.toUpperCase());
|
||||
});
|
||||
});
|
||||
|
||||
function calculatePasswordStrength(password) {
|
||||
let score = 0;
|
||||
|
||||
// Length scoring
|
||||
if (password.length >= 6) score += 1;
|
||||
if (password.length >= 8) score += 1;
|
||||
if (password.length >= 12) score += 1;
|
||||
|
||||
// Character variety scoring
|
||||
if (/[a-z]/.test(password)) score += 1;
|
||||
if (/[A-Z]/.test(password)) score += 1;
|
||||
if (/[0-9]/.test(password)) score += 1;
|
||||
if (/[^a-zA-Z0-9]/.test(password)) score += 1;
|
||||
|
||||
if (score < 3) {
|
||||
return { class: 'weak', text: 'Weak password' };
|
||||
} else if (score < 5) {
|
||||
return { class: 'medium', text: 'Medium strength' };
|
||||
} else {
|
||||
return { class: 'strong', text: 'Strong password' };
|
||||
}
|
||||
}
|
||||
|
||||
function generateRandomPassword() {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
|
||||
let password = '';
|
||||
|
||||
// Ensure at least one of each type
|
||||
password += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[Math.floor(Math.random() * 26)];
|
||||
password += 'abcdefghijklmnopqrstuvwxyz'[Math.floor(Math.random() * 26)];
|
||||
password += '0123456789'[Math.floor(Math.random() * 10)];
|
||||
password += '!@#$%^&*'[Math.floor(Math.random() * 8)];
|
||||
|
||||
// Fill remaining length
|
||||
for (let i = 4; i < 12; i++) {
|
||||
password += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
|
||||
// Shuffle the password
|
||||
return password.split('').sort(() => 0.5 - Math.random()).join('');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Additional styles for edit user form */
|
||||
.current-user-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, var(--gray-50), var(--white));
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.user-avatar-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.user-avatar.large {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: var(--gray-200);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
color: var(--gray-600);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar-status {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: -2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.875rem;
|
||||
border: 2px solid var(--white);
|
||||
}
|
||||
|
||||
.avatar-status.admin {
|
||||
background: #fbbf24;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.avatar-status.staff {
|
||||
background: var(--gray-400);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.user-basic-info h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.user-basic-info p {
|
||||
color: var(--gray-500);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.user-stats-quick {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.stat-quick {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--gray-500);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.role-warning {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem;
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border: 1px solid #f59e0b;
|
||||
border-radius: var(--radius-lg);
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.role-warning.fade-in {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.warning-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #d97706;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.warning-content {
|
||||
color: #92400e;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.warning-content ul {
|
||||
margin: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.warning-content li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.password-section {
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.password-section h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.status-section {
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.status-section h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.status-display {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: var(--white);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.status-item strong {
|
||||
color: var(--gray-700);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-item span {
|
||||
color: var(--gray-900);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: rgba(5, 150, 105, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.role-badge.admin {
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.role-badge.staff {
|
||||
background: rgba(100, 116, 139, 0.1);
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.password-strength {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.password-strength.weak {
|
||||
background-color: rgba(220, 38, 38, 0.1);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.password-strength.medium {
|
||||
background-color: rgba(217, 119, 6, 0.1);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.password-strength.strong {
|
||||
background-color: rgba(5, 150, 105, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.password-match {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.password-match.match {
|
||||
background-color: rgba(5, 150, 105, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.password-match.no-match {
|
||||
background-color: rgba(220, 38, 38, 0.1);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid var(--gray-200);
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: var(--font-size-base);
|
||||
transition: var(--transition);
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.form-group input:disabled {
|
||||
background-color: var(--gray-100);
|
||||
color: var(--gray-500);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.current-user-info {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-stats-quick {
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
+285
-1958
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user