Update add new roles

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