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
+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 %}
+213 -557
View File
@@ -1,86 +1,57 @@
{% extends "base.html" %}
{% block title %}Edit User - QR Code Management{% endblock %}
{% extends "base_authenticated.html" %}
{% block title %}Edit User - QR Code Management{% endblock %}
{% block content %}
<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 originalRole = '{{ user.role }}';
const isCurrentUser = {{ 'true' if user.id == session.user_id else 'false' }};
// Role change detection and warnings
const resetPasswordBtn = document.getElementById('resetPassword');
// 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