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 %} -
Add a new user to the QR management system
+{% extends "base_authenticated.html" %} {% block title %}Create New User - QR +Code Management{% endblock %} {% block content %} +Add a new user to the system with appropriate role permissions