From 413d1dac73732a35d1fb1d9e4d3b853022ca114e Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Sun, 10 Aug 2025 21:34:24 -0400 Subject: [PATCH] Update add new roles --- app.py | 151 ++- migrate_roles.py | 330 ++++++ static/css/users.css | 889 +++++++------- templates/create_user.html | 559 +++------ templates/edit_user.html | 770 ++++--------- templates/users.html | 2243 +++++------------------------------- 6 files changed, 1538 insertions(+), 3404 deletions(-) create mode 100644 migrate_roles.py diff --git a/app.py b/app.py index 700269d..a8983aa 100644 --- a/app.py +++ b/app.py @@ -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): """ @@ -54,6 +60,20 @@ class User(db.Model): def is_admin(self): """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): @@ -170,7 +190,84 @@ class AttendanceData(db.Model): 'location_source': self.location_source } -# Utility functions +# 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(): diff --git a/migrate_roles.py b/migrate_roles.py new file mode 100644 index 0000000..6b6ace3 --- /dev/null +++ b/migrate_roles.py @@ -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() \ No newline at end of file diff --git a/static/css/users.css b/static/css/users.css index 5cca72b..45f041f 100644 --- a/static/css/users.css +++ b/static/css/users.css @@ -1,18 +1,30 @@ /** - * Users management specific styles - * static/css/users.css + * Users Page Dedicated CSS + * static/css/users-page.css + * + * This file contains all necessary styles for the users management page + * ensuring it works independently of other CSS files. */ -/* Users Page Header */ +/* Users Page Container */ +.users-page { + max-width: 1600px; + margin: 0 auto; + padding: 2rem; + min-height: 100vh; + background: #f8fafc; +} + +/* Page Header */ .users-header { display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: var(--spacing-8); - padding: var(--spacing-6); - background: var(--white); - border-radius: var(--radius-xl); - box-shadow: var(--shadow); + margin-bottom: 2rem; + padding: 1.5rem; + background: #ffffff; + border-radius: 0.75rem; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); position: relative; overflow: hidden; } @@ -27,37 +39,49 @@ background: linear-gradient(90deg, #059669, #047857); } -.users-header h1 { - font-size: var(--font-size-3xl); +.header-content h1 { + font-size: 1.875rem; font-weight: 700; - color: var(--gray-900); - margin-bottom: var(--spacing-2); + color: #0f172a; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + gap: 0.75rem; } -.users-header p { - color: var(--gray-500); - font-size: var(--font-size-lg); +.header-content h1 i { + color: #059669; +} + +.header-content p { + color: #64748b; + font-size: 1.125rem; margin: 0; } -/* User Statistics */ +.header-actions { + display: flex; + gap: 0.75rem; +} + +/* User Statistics Grid */ .user-stats { display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: var(--spacing-6); - margin-bottom: var(--spacing-8); + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; } .stat-card { - background: var(--white); - padding: var(--spacing-6); - border-radius: var(--radius-xl); - box-shadow: var(--shadow); + background: #ffffff; + padding: 1.5rem; + border-radius: 0.75rem; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); display: flex; align-items: center; - gap: var(--spacing-4); - transition: var(--transition); - border: 1px solid var(--gray-200); + gap: 1rem; + transition: all 0.2s ease-in-out; + border: 1px solid #e2e8f0; position: relative; overflow: hidden; } @@ -76,31 +100,40 @@ } .stat-card.staff::before { - background: linear-gradient(90deg, var(--gray-500), var(--gray-600)); + background: linear-gradient(90deg, #64748b, #475569); +} + +.stat-card.payroll::before { + background: linear-gradient(90deg, #17a2b8, #138496); +} + +.stat-card.project-manager::before { + background: linear-gradient(90deg, #6f42c1, #5a2d91); } .stat-card.active::before { - background: linear-gradient(90deg, var(--success-color), #047857); + background: linear-gradient(90deg, #10b981, #047857); } .stat-card.inactive::before { - background: linear-gradient(90deg, var(--danger-color), #b91c1c); + background: linear-gradient(90deg, #ef4444, #b91c1c); } .stat-card:hover { transform: translateY(-2px); - box-shadow: var(--shadow-lg); + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); } .stat-icon { width: 50px; height: 50px; - border-radius: var(--radius-lg); + border-radius: 0.5rem; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; - color: var(--white); + color: #ffffff; flex-shrink: 0; } @@ -109,27 +142,36 @@ } .stat-icon.staff { - background: linear-gradient(135deg, var(--gray-500), var(--gray-600)); + background: linear-gradient(135deg, #64748b, #475569); +} + +.stat-icon.payroll { + background: linear-gradient(135deg, #17a2b8, #138496); +} + +.stat-icon.project-manager { + background: linear-gradient(135deg, #6f42c1, #5a2d91); } .stat-icon.active { - background: linear-gradient(135deg, var(--success-color), #047857); + background: linear-gradient(135deg, #10b981, #047857); } .stat-icon.inactive { - background: linear-gradient(135deg, var(--danger-color), #b91c1c); + background: linear-gradient(135deg, #ef4444, #b91c1c); } .stat-info h3 { font-size: 1.75rem; font-weight: 700; - color: var(--gray-900); + color: #0f172a; margin-bottom: 0.25rem; + margin-top: 0; } .stat-info p { - color: var(--gray-500); - font-size: var(--font-size-sm); + color: #64748b; + font-size: 0.875rem; margin: 0; } @@ -138,18 +180,18 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: var(--spacing-6); - padding: var(--spacing-6); - background: var(--white); - border-radius: var(--radius-xl); - box-shadow: var(--shadow); + margin-bottom: 1.5rem; + padding: 1.5rem; + background: #ffffff; + border-radius: 0.75rem; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); flex-wrap: wrap; - gap: var(--spacing-4); + gap: 1rem; } .search-filters { display: flex; - gap: var(--spacing-4); + gap: 1rem; align-items: center; flex-wrap: wrap; } @@ -162,63 +204,60 @@ .search-box i { position: absolute; - left: var(--spacing-3); - color: var(--gray-400); + left: 0.75rem; + color: #94a3b8; z-index: 1; } .search-box input { - padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-10); - border: 2px solid var(--gray-200); - border-radius: var(--radius-lg); - font-size: var(--font-size-sm); + padding: 0.75rem 0.75rem 0.75rem 2.5rem; + border: 2px solid #e2e8f0; + border-radius: 0.5rem; + font-size: 0.875rem; width: 300px; - transition: var(--transition); + transition: all 0.2s ease-in-out; + background: #ffffff; } .search-box input:focus { outline: none; - border-color: var(--primary-color); + border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); } .filter-group { display: flex; - gap: var(--spacing-3); + gap: 0.75rem; flex-wrap: wrap; } .filter-select { - padding: var(--spacing-3) var(--spacing-4); - border: 2px solid var(--gray-200); - border-radius: var(--radius-lg); - font-size: var(--font-size-sm); - background-color: var(--white); + padding: 0.75rem 1rem; + border: 2px solid #e2e8f0; + border-radius: 0.5rem; + font-size: 0.875rem; + background-color: #ffffff; cursor: pointer; - transition: var(--transition); - min-width: 120px; + transition: all 0.2s ease-in-out; + min-width: 140px; } .filter-select:focus { outline: none; - border-color: var(--primary-color); + border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); } -.results-counter { - font-size: var(--font-size-sm); - color: var(--gray-600); - padding: var(--spacing-2) var(--spacing-4); - background: var(--gray-50); - border-radius: var(--radius); - border: 1px solid var(--gray-200); +.view-options { + display: flex; + gap: 0.75rem; } -/* Users Table */ +/* Users Table Container */ .users-table-container { - background: var(--white); - border-radius: var(--radius-xl); - box-shadow: var(--shadow); + background: #ffffff; + border-radius: 0.75rem; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); overflow: hidden; } @@ -228,91 +267,104 @@ } .users-table th { - background-color: var(--gray-50); - padding: var(--spacing-4); + background-color: #f8fafc; + padding: 1rem; text-align: left; font-weight: 600; - color: var(--gray-700); - font-size: var(--font-size-sm); - border-bottom: 1px solid var(--gray-200); + color: #334155; + font-size: 0.875rem; + border-bottom: 1px solid #e2e8f0; position: sticky; top: 0; z-index: 10; } .users-table th:first-child { - width: 40px; + width: 50px; text-align: center; } .users-table td { - padding: var(--spacing-4); - border-bottom: 1px solid var(--gray-100); + padding: 1rem; + border-bottom: 1px solid #f1f5f9; vertical-align: middle; } .users-table tr.user-row { - transition: var(--transition); + transition: all 0.2s ease-in-out; cursor: pointer; } .users-table tr.user-row:hover { - background-color: var(--gray-50); + background-color: #f8fafc; } .users-table tr.user-row:last-child td { border-bottom: none; } -/* User Avatar */ +/* User Information */ +.user-info { + display: flex; + align-items: center; + gap: 0.75rem; +} + .user-avatar { width: 40px; height: 40px; - border-radius: var(--radius-full); - background: linear-gradient( - 135deg, - var(--primary-color), - var(--primary-hover) - ); - color: var(--white); + border-radius: 50%; + background: linear-gradient(135deg, #2563eb, #1d4ed8); + color: #ffffff; display: flex; align-items: center; justify-content: center; font-weight: 600; - font-size: var(--font-size-sm); - margin-right: var(--spacing-3); -} - -.user-info { - display: flex; - align-items: center; - gap: var(--spacing-3); + font-size: 0.875rem; + flex-shrink: 0; } .user-details h4 { - font-size: var(--font-size-sm); + font-size: 0.875rem; font-weight: 600; - color: var(--gray-900); - margin: 0 0 var(--spacing-1) 0; + color: #0f172a; + margin: 0 0 0.25rem 0; } .user-details p { - font-size: var(--font-size-xs); - color: var(--gray-500); + font-size: 0.75rem; + color: #64748b; margin: 0; } -/* User Role Badge */ +/* Contact Information */ +.user-contact { + font-size: 0.875rem; +} + +.contact-email { + display: flex; + align-items: center; + gap: 0.5rem; + color: #334155; +} + +.contact-email i { + color: #64748b; + font-size: 0.75rem; +} + +/* Role Badges */ .user-role { display: inline-flex; align-items: center; - gap: var(--spacing-1); - padding: var(--spacing-1) var(--spacing-3); - border-radius: var(--radius); - font-size: var(--font-size-xs); + gap: 0.25rem; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.05em; + letter-spacing: 0.025em; } .user-role.admin { @@ -323,388 +375,249 @@ .user-role.staff { background-color: rgba(100, 116, 139, 0.1); - color: var(--gray-600); + color: #64748b; border: 1px solid rgba(100, 116, 139, 0.3); } -/* User Status Badge */ +.user-role.payroll { + background-color: rgba(23, 162, 184, 0.1); + color: #138496; + border: 1px solid rgba(23, 162, 184, 0.3); +} + +.user-role.project_manager { + background-color: rgba(111, 66, 193, 0.1); + color: #5a2d91; + border: 1px solid rgba(111, 66, 193, 0.3); +} + +/* Status Badges */ .user-status { display: inline-flex; align-items: center; - gap: var(--spacing-1); - padding: var(--spacing-1) var(--spacing-3); - border-radius: var(--radius); - font-size: var(--font-size-xs); + gap: 0.25rem; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; font-weight: 600; } .user-status.active { background-color: rgba(5, 150, 105, 0.1); - color: var(--success-color); + color: #10b981; border: 1px solid rgba(5, 150, 105, 0.2); } .user-status.inactive { background-color: rgba(220, 38, 38, 0.1); - color: var(--danger-color); + color: #ef4444; border: 1px solid rgba(220, 38, 38, 0.2); } -/* User Actions Dropdown */ -.user-actions { - position: relative; -} - -.dropdown-trigger { - background: none; - border: none; - cursor: pointer; - padding: var(--spacing-2); - border-radius: var(--radius); - transition: var(--transition); - color: var(--gray-500); -} - -.dropdown-trigger:hover { - background-color: var(--gray-100); - color: var(--gray-700); -} - -.dropdown-menu { - position: absolute; - top: 100%; - right: 0; - min-width: 200px; - background: var(--white); - border: 1px solid var(--gray-200); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); - z-index: var(--z-dropdown); - opacity: 0; - visibility: hidden; - transform: translateY(-10px); - transition: all 0.2s ease-in-out; -} - -.dropdown-menu.show { - opacity: 1; - visibility: visible; - transform: translateY(0); -} - -.dropdown-item { - display: flex; - align-items: center; - gap: var(--spacing-3); - width: 100%; - padding: var(--spacing-3) var(--spacing-4); - color: var(--gray-700); - text-decoration: none; - border: none; - background: none; - text-align: left; - cursor: pointer; - transition: var(--transition); - font-size: var(--font-size-sm); -} - -.dropdown-item:hover { - background-color: var(--gray-50); - color: var(--gray-900); - text-decoration: none; -} - -.dropdown-item.danger { - color: var(--danger-color); -} - -.dropdown-item.danger:hover { - background-color: var(--danger-light); - color: var(--danger-color); -} - -.dropdown-item.success { - color: var(--success-color); -} - -.dropdown-item.success:hover { - background-color: var(--success-light); - color: var(--success-color); -} - -.dropdown-item:first-child { - border-top-left-radius: var(--radius-lg); - border-top-right-radius: var(--radius-lg); -} - -.dropdown-item:last-child { - border-bottom-left-radius: var(--radius-lg); - border-bottom-right-radius: var(--radius-lg); -} - -.dropdown-divider { - height: 1px; - background-color: var(--gray-200); - margin: var(--spacing-1) 0; -} - -/* Bulk Actions */ -.bulk-actions-bar { - position: fixed; - bottom: var(--spacing-4); - left: 50%; - transform: translateX(-50%) translateY(100px); - background: var(--white); - border: 1px solid var(--gray-200); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-xl); - padding: var(--spacing-4) var(--spacing-6); - display: flex; - align-items: center; - gap: var(--spacing-4); - z-index: var(--z-dropdown); - opacity: 0; - transition: all 0.3s ease-out; - min-width: 400px; -} - -.bulk-actions-bar.show { - transform: translateX(-50%) translateY(0); - opacity: 1; -} - -.bulk-actions-info { - font-size: var(--font-size-sm); - color: var(--gray-600); - display: flex; - align-items: center; - gap: var(--spacing-2); -} - -.selected-count { - font-weight: 600; - color: var(--primary-color); -} - -.bulk-actions-buttons { - display: flex; - gap: var(--spacing-2); -} - -/* User Details Modal */ -.user-details-modal { - max-width: 600px; -} - -.user-profile-header { - display: flex; - align-items: center; - gap: var(--spacing-4); - margin-bottom: var(--spacing-6); - padding: var(--spacing-6); - background: var(--gray-50); - border-radius: var(--radius-lg); -} - -.user-profile-avatar { - width: 80px; - height: 80px; - border-radius: var(--radius-full); - background: linear-gradient( - 135deg, - var(--primary-color), - var(--primary-hover) - ); - color: var(--white); - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 1.5rem; -} - -.user-profile-info h3 { - font-size: var(--font-size-xl); - font-weight: 700; - color: var(--gray-900); - margin-bottom: var(--spacing-1); -} - -.user-profile-info p { - color: var(--gray-500); - margin: 0; -} - -.user-details-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--spacing-4); - margin-bottom: var(--spacing-6); -} - -.detail-item { - display: flex; - flex-direction: column; - gap: var(--spacing-1); -} - -.detail-label { - font-size: var(--font-size-xs); - font-weight: 600; - color: var(--gray-500); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.detail-value { - font-size: var(--font-size-sm); - color: var(--gray-900); -} - -/* Password Reset Modal */ -.password-reset-form { - display: flex; - flex-direction: column; - gap: var(--spacing-4); -} - -.password-strength { - margin-top: var(--spacing-2); -} - -.strength-bar { - height: 4px; - background-color: var(--gray-200); - border-radius: var(--radius-full); - overflow: hidden; -} - -.strength-fill { - height: 100%; - transition: var(--transition); - border-radius: var(--radius-full); -} - -.strength-weak { - background-color: var(--danger-color); - width: 25%; -} -.strength-fair { - background-color: var(--warning-color); - width: 50%; -} -.strength-good { - background-color: #10b981; - width: 75%; -} -.strength-strong { - background-color: var(--success-color); - width: 100%; -} - -.strength-text { - font-size: var(--font-size-xs); - margin-top: var(--spacing-1); - color: var(--gray-600); -} - -/* Animation classes */ -.fade-in { - animation: fadeInUp 0.3s ease-out; -} - -.fade-out { - animation: fadeOutDown 0.3s ease-out; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes fadeOutDown { - from { - opacity: 1; - transform: translateY(0); - } - to { - opacity: 0; - transform: translateY(-20px); - } -} - -/* Loading states */ -.loading { - opacity: 0.6; - pointer-events: none; -} - -.loading::after { - content: ""; - position: absolute; - top: 50%; - left: 50%; - width: 20px; - height: 20px; - margin: -10px 0 0 -10px; - border: 2px solid var(--primary-color); - border-top: 2px solid transparent; - border-radius: var(--radius-full); - animation: spin 1s linear infinite; -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -/* Empty state */ -.users-empty-state { +/* QR Code Count */ +.user-qr-count { text-align: center; - padding: var(--spacing-16) var(--spacing-8); - background: var(--white); - border-radius: var(--radius-xl); - box-shadow: var(--shadow); } -.empty-icon { - width: 80px; - height: 80px; - margin: 0 auto var(--spacing-6); - background: var(--gray-100); - border-radius: var(--radius-full); +.qr-stats { display: flex; + flex-direction: column; + align-items: center; +} + +.qr-count { + font-size: 1.25rem; + font-weight: 700; + color: #2563eb; +} + +.qr-stats small { + font-size: 0.75rem; + color: #64748b; + margin-top: 0.25rem; +} + +/* Date Information */ +.date-info { + display: flex; + flex-direction: column; + font-size: 0.875rem; +} + +.date-main { + font-weight: 500; + color: #0f172a; +} + +.date-time { + font-size: 0.75rem; + color: #64748b; + margin-top: 0.125rem; +} + +.never-logged-in { + color: #94a3b8; + font-style: italic; + font-size: 0.875rem; +} + +/* Action Buttons */ +.user-actions { + display: flex; + justify-content: center; +} + +.action-buttons { + display: flex; + gap: 0.5rem; + align-items: center; +} + +/* Button Styles */ +.btn { + display: inline-flex; align-items: center; justify-content: center; - font-size: 2rem; - color: var(--gray-400); + gap: 0.5rem; + padding: 0.5rem 1rem; + border: 1px solid transparent; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + cursor: pointer; + transition: all 0.2s ease-in-out; + white-space: nowrap; } -.users-empty-state h3 { - font-size: var(--font-size-xl); - color: var(--gray-700); - margin-bottom: var(--spacing-3); +.btn:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1); } -.users-empty-state p { - color: var(--gray-500); - margin-bottom: var(--spacing-6); +.btn-sm { + padding: 0.375rem 0.75rem; + font-size: 0.75rem; +} + +.btn-primary { + background-color: #2563eb; + color: #ffffff; + border-color: #2563eb; +} + +.btn-primary:hover { + background-color: #1d4ed8; + border-color: #1d4ed8; + text-decoration: none; + color: #ffffff; +} + +.btn-secondary { + background-color: #64748b; + color: #ffffff; + border-color: #64748b; +} + +.btn-secondary:hover { + background-color: #475569; + border-color: #475569; + text-decoration: none; + color: #ffffff; +} + +.btn-success { + background-color: #10b981; + color: #ffffff; + border-color: #10b981; +} + +.btn-success:hover { + background-color: #047857; + border-color: #047857; + text-decoration: none; + color: #ffffff; +} + +.btn-warning { + background-color: #f59e0b; + color: #ffffff; + border-color: #f59e0b; +} + +.btn-warning:hover { + background-color: #d97706; + border-color: #d97706; + text-decoration: none; + color: #ffffff; +} + +.btn-danger { + background-color: #ef4444; + color: #ffffff; + border-color: #ef4444; +} + +.btn-danger:hover { + background-color: #dc2626; + border-color: #dc2626; + text-decoration: none; + color: #ffffff; +} + +.btn:disabled, +.btn[disabled] { + opacity: 0.5; + cursor: not-allowed; +} + +.btn:disabled:hover, +.btn[disabled]:hover { + transform: none; +} + +/* No Users Message */ +.no-users-message { + display: flex; + justify-content: center; + align-items: center; + padding: 3rem; + background: #ffffff; + border-radius: 0.75rem; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} + +.no-users-content { + text-align: center; + max-width: 400px; +} + +.no-users-content i { + font-size: 3rem; + color: #94a3b8; + margin-bottom: 1rem; +} + +.no-users-content h3 { + font-size: 1.25rem; + color: #334155; + margin-bottom: 0.75rem; +} + +.no-users-content p { + color: #64748b; + margin-bottom: 1.5rem; } /* Responsive Design */ @media (max-width: 1024px) { - .user-details-grid { - grid-template-columns: 1fr; + .users-page { + padding: 1rem; + } + + .user-stats { + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; } .search-box input { @@ -715,22 +628,24 @@ @media (max-width: 768px) { .users-header { flex-direction: column; - gap: var(--spacing-4); + gap: 1rem; text-align: center; } .user-stats { grid-template-columns: repeat(2, 1fr); - gap: var(--spacing-4); + gap: 1rem; } .users-controls { flex-direction: column; align-items: stretch; + gap: 1rem; } .search-filters { justify-content: center; + flex-wrap: wrap; } .search-box input { @@ -743,35 +658,14 @@ } /* Hide less important columns on mobile */ - .users-table th:nth-child(4), - .users-table td:nth-child(4), - .users-table th:nth-child(5), - .users-table td:nth-child(5) { + .users-table th:nth-child(6), + .users-table td:nth-child(6), + .users-table th:nth-child(7), + .users-table td:nth-child(7), + .users-table th:nth-child(8), + .users-table td:nth-child(8) { display: none; } - - .user-profile-header { - flex-direction: column; - text-align: center; - } - - .bulk-actions-bar { - left: var(--spacing-3); - right: var(--spacing-3); - transform: translateY(100px); - min-width: auto; - flex-direction: column; - gap: var(--spacing-3); - } - - .bulk-actions-bar.show { - transform: translateY(0); - } - - .bulk-actions-buttons { - width: 100%; - justify-content: center; - } } @media (max-width: 480px) { @@ -780,35 +674,36 @@ } .users-table { - font-size: var(--font-size-xs); + font-size: 0.75rem; } .users-table th, .users-table td { - padding: var(--spacing-2); + padding: 0.5rem; } .user-avatar { width: 32px; height: 32px; - font-size: var(--font-size-xs); + font-size: 0.75rem; } .user-details h4 { - font-size: var(--font-size-xs); + font-size: 0.75rem; } .user-details p { font-size: 0.625rem; } - /* Hide checkbox column on very small screens */ - .users-table th:first-child, - .users-table td:first-child { + /* Hide more columns on very small screens */ + .users-table th:nth-child(5), + .users-table td:nth-child(5) { display: none; } - .bulk-actions-bar { - padding: var(--spacing-3); + .action-buttons { + flex-direction: column; + gap: 0.25rem; } } diff --git a/templates/create_user.html b/templates/create_user.html index 6e51d90..4f5022a 100644 --- a/templates/create_user.html +++ b/templates/create_user.html @@ -1,50 +1,60 @@ -{% extends "base.html" %} {% block title %}Create User - QR Code Management{% -endblock %} {% block content %} -
-
-
-

Create New User

-

Add a new user to the QR management system

+{% extends "base_authenticated.html" %} {% block title %}Create New User - QR +Code Management{% endblock %} {% block content %} +
+ -
-
-
- - - Legal name as it should appear in the system +
+ +
+

+ + User Information +

+ +
+
+ + + First and last name +
+ +
+ + + Must be unique in the system +
-
- - - Primary email for notifications and account recovery -
-
- -
@@ -72,10 +81,12 @@ endblock %} {% block content %} Staff can manage QR codes; Admin has full system accessDetermines user permissions and system access level
@@ -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 = ` -

${permissions.title}

-
-
-
Allowed Actions
-
    - ${permissions.permissions - .map((perm) => `
  • ${perm}
  • `) - .join("")} -
-
-
-
Restrictions
-
    - ${permissions.restrictions - .map((rest) => `
  • ${rest}
  • `) - .join("")} -
-
-
- `; + let permissionsHtml = ` +

${roleData.title}

+
+
Permissions
+
    + `; + roleData.permissions.forEach((permission) => { + permissionsHtml += `
  • ${permission}
  • `; + }); + + permissionsHtml += ` +
+
Restrictions
+
    + `; + + roleData.restrictions.forEach((restriction) => { + permissionsHtml += `
  • ${restriction}
  • `; + }); + + permissionsHtml += `
`; + + 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 = - ' 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" }; + } + } - - {% endblock %} diff --git a/templates/edit_user.html b/templates/edit_user.html index 2c34dbe..12f9db8 100644 --- a/templates/edit_user.html +++ b/templates/edit_user.html @@ -1,86 +1,57 @@ -{% extends "base.html" %} - -{% block title %}Edit User - QR Code Management{% endblock %} +{% extends "base_authenticated.html" %} +{% block title %}Edit User - QR Code Management{% endblock %} {% block content %} -
-
-
-

Edit User

-

Update user information and permissions

+
+ - -