729 lines
32 KiB
HTML
729 lines
32 KiB
HTML
{% extends "base_authenticated.html" %}
|
|
{% block title %}Edit User - QR Code Management{% endblock %}
|
|
|
|
{% block content %}
|
|
<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>
|
|
|
|
<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>
|
|
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="form-group">
|
|
<label for="username">
|
|
<i class="fas fa-user"></i>
|
|
Username
|
|
</label>
|
|
<input type="text" id="username_display" value="{{ user.username }}" disabled>
|
|
<input type="hidden" name="username" value="{{ user.username }}">
|
|
<small class="form-help">Username cannot be changed</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="role">
|
|
<i class="fas fa-user-shield"></i>
|
|
User Role
|
|
</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="accounting" {{ 'selected' if user.role == 'accounting' else '' }}>Accounting 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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Role Change Warning -->
|
|
<div class="role-warning" id="roleWarning" style="display: none;">
|
|
<div class="warning-header">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<span>Role Change Warning</span>
|
|
</div>
|
|
<div class="warning-content" id="warningContent">
|
|
<!-- Dynamic warning content -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Project Manager Permissions Section -->
|
|
<div class="permissions-section" id="pmPermissionsSection" style="display: none;">
|
|
<div class="section-header">
|
|
<h3>
|
|
<i class="fas fa-shield-alt"></i>
|
|
Project Manager Permissions
|
|
</h3>
|
|
<p class="section-description">
|
|
Follow the steps below to configure access permissions for this Project Manager.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Step 1: Select Projects -->
|
|
<div class="permission-step" id="projectStep">
|
|
<div class="step-header">
|
|
<span class="step-number">1</span>
|
|
<div class="step-info">
|
|
<h4>Select Projects</h4>
|
|
<p>Choose which projects this Project Manager can access</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="checkbox-group" id="projectCheckboxGroup">
|
|
{% if projects %}
|
|
{% for project in projects %}
|
|
<label class="checkbox-label">
|
|
<input
|
|
type="checkbox"
|
|
name="assigned_projects"
|
|
value="{{ project.id }}"
|
|
class="checkbox-input project-checkbox"
|
|
data-project-name="{{ project.name }}"
|
|
>
|
|
<span class="checkbox-text">{{ project.name }}</span>
|
|
<span class="project-qr-count" title="QR codes in this project">
|
|
<i class="fas fa-qrcode"></i> {{ project.qr_count or 0 }}
|
|
</span>
|
|
</label>
|
|
{% endfor %}
|
|
{% else %}
|
|
<p class="text-muted">No active projects available</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="step-footer">
|
|
<div class="selection-summary" id="projectSummary">
|
|
<i class="fas fa-info-circle"></i>
|
|
<span>No projects selected</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Select Locations -->
|
|
<div class="permission-step" id="locationStep" style="display: none;">
|
|
<div class="step-header">
|
|
<span class="step-number">2</span>
|
|
<div class="step-info">
|
|
<h4>Select Locations</h4>
|
|
<p>Choose specific locations within the selected projects</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="location-loading" id="locationLoading" style="display: none;">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
<span>Loading locations...</span>
|
|
</div>
|
|
|
|
<div class="checkbox-group" id="locationCheckboxGroup">
|
|
<!-- Locations will be populated dynamically -->
|
|
</div>
|
|
|
|
<div class="location-empty" id="locationEmpty" style="display: none;">
|
|
<i class="fas fa-info-circle"></i>
|
|
<p>No locations found for the selected projects.</p>
|
|
<small>QR codes in these projects may not have location data.</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="step-footer">
|
|
<div class="selection-summary" id="locationSummary">
|
|
<i class="fas fa-info-circle"></i>
|
|
<span>No locations selected</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Selection Overview -->
|
|
<div class="permissions-overview" id="permissionsOverview" style="display: none;">
|
|
<div class="overview-header">
|
|
<i class="fas fa-check-circle"></i>
|
|
<h4>Permission Summary</h4>
|
|
</div>
|
|
<div class="overview-content">
|
|
<div class="overview-item">
|
|
<strong>Projects:</strong>
|
|
<span id="overviewProjects">-</span>
|
|
</div>
|
|
<div class="overview-item">
|
|
<strong>Locations:</strong>
|
|
<span id="overviewLocations">-</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Password Section -->
|
|
<div class="password-section">
|
|
<h3>
|
|
<i class="fas fa-key"></i>
|
|
Password Management
|
|
</h3>
|
|
|
|
<div class="form-group">
|
|
<label for="new_password">
|
|
<i class="fas fa-lock"></i>
|
|
New Password (Optional)
|
|
</label>
|
|
<input type="password" id="new_password" name="new_password"
|
|
minlength="6" placeholder="Leave blank to keep current password">
|
|
<div class="password-strength" id="passwordStrength"></div>
|
|
<small class="form-help">Only enter a password if you want to change it</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="confirm_password">
|
|
<i class="fas fa-check-circle"></i>
|
|
Confirm New Password
|
|
</label>
|
|
<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>
|
|
|
|
<!-- User Status Information -->
|
|
<div class="user-status-section">
|
|
<h3>
|
|
<i class="fas fa-info-circle"></i>
|
|
User Status Information
|
|
</h3>
|
|
|
|
<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 '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 %I:%M %p') if user.created_date else 'Unknown' }}</span>
|
|
</div>
|
|
|
|
<div class="status-item">
|
|
<strong>Created By:</strong>
|
|
<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>
|
|
</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>
|
|
|
|
<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>
|
|
Update User
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
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 resetPasswordBtn = document.getElementById('resetPassword');
|
|
|
|
|
|
// Role change warning system
|
|
roleSelect.addEventListener('change', function() {
|
|
const newRole = this.value;
|
|
let warningMessage = '';
|
|
|
|
if (originalRole !== newRole) {
|
|
if (originalRole === 'admin' && newRole !== 'admin') {
|
|
if (editingUserId === currentUserId) {
|
|
warningMessage = `
|
|
<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>
|
|
<li>System administration</li>
|
|
<li>Advanced settings</li>
|
|
</ul>
|
|
<strong>You will need another admin to restore your privileges.</strong>
|
|
`;
|
|
} else {
|
|
warningMessage = `
|
|
<strong>Demoting Admin to ${getRoleDisplayName(newRole)}</strong><br>
|
|
This user will lose admin privileges and access to:
|
|
<ul>
|
|
<li>User management</li>
|
|
<li>System administration</li>
|
|
<li>Advanced settings</li>
|
|
</ul>
|
|
`;
|
|
}
|
|
} else if (originalRole !== 'admin' && newRole === 'admin') {
|
|
warningMessage = `
|
|
<strong>Promoting ${getRoleDisplayName(originalRole)} to Admin</strong><br>
|
|
This user will gain full system access including:
|
|
<ul>
|
|
<li>User management</li>
|
|
<li>System administration</li>
|
|
<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>
|
|
`;
|
|
}
|
|
|
|
if (warningMessage) {
|
|
warningContent.innerHTML = warningMessage;
|
|
roleWarning.style.display = 'block';
|
|
setTimeout(() => roleWarning.classList.add('fade-in'), 100);
|
|
} else {
|
|
hideRoleWarning();
|
|
}
|
|
} else {
|
|
hideRoleWarning();
|
|
}
|
|
});
|
|
|
|
const pmSection = document.getElementById('pmPermissionsSection');
|
|
const projectStep = document.getElementById('projectStep');
|
|
const locationStep = document.getElementById('locationStep');
|
|
const permissionsOverview = document.getElementById('permissionsOverview');
|
|
const projectCheckboxes = document.querySelectorAll('.project-checkbox');
|
|
const projectSummary = document.getElementById('projectSummary');
|
|
const locationSummary = document.getElementById('locationSummary');
|
|
const locationCheckboxGroup = document.getElementById('locationCheckboxGroup');
|
|
const locationLoading = document.getElementById('locationLoading');
|
|
const locationEmpty = document.getElementById('locationEmpty');
|
|
|
|
// Show/hide Project Manager permissions based on role selection
|
|
if (roleSelect) {
|
|
roleSelect.addEventListener('change', function() {
|
|
if (this.value === 'project_manager') {
|
|
pmSection.style.display = 'block';
|
|
setupPermissionFlow();
|
|
} else {
|
|
pmSection.style.display = 'none';
|
|
resetPermissionFlow();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize permission flow if Project Manager role is already selected
|
|
if (roleSelect && roleSelect.value === 'project_manager') {
|
|
pmSection.style.display = 'block';
|
|
setupPermissionFlow();
|
|
}
|
|
|
|
// Setup cascading permission flow
|
|
function setupPermissionFlow() {
|
|
// Add event listeners to project checkboxes
|
|
projectCheckboxes.forEach(checkbox => {
|
|
checkbox.addEventListener('change', handleProjectSelection);
|
|
});
|
|
|
|
// Pre-select assigned projects if in edit mode
|
|
{% if assigned_project_ids %}
|
|
const assignedProjectIds = {{ assigned_project_ids | tojson }};
|
|
projectCheckboxes.forEach(cb => {
|
|
if (assignedProjectIds.includes(parseInt(cb.value))) {
|
|
cb.checked = true;
|
|
}
|
|
});
|
|
|
|
// Trigger initial load if projects are assigned
|
|
if (assignedProjectIds.length > 0) {
|
|
handleProjectSelection().then(() => {
|
|
// After locations are loaded, pre-select assigned locations
|
|
{% if assigned_location_names %}
|
|
setTimeout(() => {
|
|
const assignedLocations = {{ assigned_location_names | tojson }};
|
|
document.querySelectorAll('.location-checkbox').forEach(cb => {
|
|
if (assignedLocations.includes(cb.value)) {
|
|
cb.checked = true;
|
|
}
|
|
});
|
|
handleLocationSelection();
|
|
}, 500);
|
|
{% endif %}
|
|
});
|
|
}
|
|
{% endif %}
|
|
}
|
|
|
|
// Handle project selection changes
|
|
async function handleProjectSelection() {
|
|
const selectedProjects = Array.from(projectCheckboxes)
|
|
.filter(cb => cb.checked)
|
|
.map(cb => ({
|
|
id: cb.value,
|
|
name: cb.dataset.projectName
|
|
}));
|
|
|
|
// Update project summary
|
|
if (selectedProjects.length > 0) {
|
|
projectSummary.innerHTML = `
|
|
<i class="fas fa-check-circle" style="color: #10b981;"></i>
|
|
<span><strong>${selectedProjects.length}</strong> project${selectedProjects.length > 1 ? 's' : ''} selected</span>
|
|
`;
|
|
|
|
// Show location step and load locations
|
|
locationStep.style.display = 'block';
|
|
await loadLocationsByProjects(selectedProjects.map(p => p.id));
|
|
} else {
|
|
projectSummary.innerHTML = `
|
|
<i class="fas fa-info-circle"></i>
|
|
<span>No projects selected</span>
|
|
`;
|
|
locationStep.style.display = 'none';
|
|
permissionsOverview.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Load locations for selected projects
|
|
async function loadLocationsByProjects(projectIds) {
|
|
locationLoading.style.display = 'flex';
|
|
locationCheckboxGroup.innerHTML = '';
|
|
locationEmpty.style.display = 'none';
|
|
locationSummary.innerHTML = `
|
|
<i class="fas fa-info-circle"></i>
|
|
<span>Loading locations...</span>
|
|
`;
|
|
|
|
try {
|
|
const response = await fetch('/api/locations-by-projects', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ project_ids: projectIds })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
locationLoading.style.display = 'none';
|
|
|
|
if (data.success && data.locations.length > 0) {
|
|
// Populate location checkboxes
|
|
locationCheckboxGroup.innerHTML = data.locations.map(location => `
|
|
<label class="checkbox-label">
|
|
<input
|
|
type="checkbox"
|
|
name="assigned_locations"
|
|
value="${location}"
|
|
class="checkbox-input location-checkbox"
|
|
>
|
|
<span class="checkbox-text">${location}</span>
|
|
</label>
|
|
`).join('');
|
|
|
|
// Add event listeners to location checkboxes
|
|
document.querySelectorAll('.location-checkbox').forEach(cb => {
|
|
cb.addEventListener('change', handleLocationSelection);
|
|
});
|
|
|
|
locationSummary.innerHTML = `
|
|
<i class="fas fa-info-circle"></i>
|
|
<span>${data.count} location${data.count > 1 ? 's' : ''} available</span>
|
|
`;
|
|
} else {
|
|
locationEmpty.style.display = 'block';
|
|
locationSummary.innerHTML = `
|
|
<i class="fas fa-exclamation-circle"></i>
|
|
<span>No locations available</span>
|
|
`;
|
|
}
|
|
|
|
updatePermissionsOverview();
|
|
} catch (error) {
|
|
console.error('Error loading locations:', error);
|
|
locationLoading.style.display = 'none';
|
|
locationEmpty.style.display = 'block';
|
|
locationSummary.innerHTML = `
|
|
<i class="fas fa-exclamation-triangle" style="color: #ef4444;"></i>
|
|
<span>Error loading locations</span>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Handle location selection changes
|
|
function handleLocationSelection() {
|
|
const selectedLocations = Array.from(document.querySelectorAll('.location-checkbox:checked'));
|
|
|
|
if (selectedLocations.length > 0) {
|
|
locationSummary.innerHTML = `
|
|
<i class="fas fa-check-circle" style="color: #10b981;"></i>
|
|
<span><strong>${selectedLocations.length}</strong> location${selectedLocations.length > 1 ? 's' : ''} selected</span>
|
|
`;
|
|
} else {
|
|
const totalLocations = document.querySelectorAll('.location-checkbox').length;
|
|
locationSummary.innerHTML = `
|
|
<i class="fas fa-info-circle"></i>
|
|
<span>${totalLocations} location${totalLocations > 1 ? 's' : ''} available</span>
|
|
`;
|
|
}
|
|
|
|
updatePermissionsOverview();
|
|
}
|
|
|
|
// Update permissions overview
|
|
function updatePermissionsOverview() {
|
|
const selectedProjects = Array.from(projectCheckboxes)
|
|
.filter(cb => cb.checked)
|
|
.map(cb => cb.dataset.projectName);
|
|
|
|
const selectedLocations = Array.from(document.querySelectorAll('.location-checkbox:checked'))
|
|
.map(cb => cb.value);
|
|
|
|
if (selectedProjects.length > 0 || selectedLocations.length > 0) {
|
|
permissionsOverview.style.display = 'block';
|
|
|
|
document.getElementById('overviewProjects').textContent =
|
|
selectedProjects.length > 0
|
|
? selectedProjects.join(', ')
|
|
: 'All projects';
|
|
|
|
document.getElementById('overviewLocations').textContent =
|
|
selectedLocations.length > 0
|
|
? selectedLocations.join(', ')
|
|
: 'All locations in selected projects';
|
|
} else {
|
|
permissionsOverview.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Reset permission flow
|
|
function resetPermissionFlow() {
|
|
projectCheckboxes.forEach(cb => {
|
|
cb.checked = false;
|
|
cb.removeEventListener('change', handleProjectSelection);
|
|
});
|
|
locationStep.style.display = 'none';
|
|
permissionsOverview.style.display = 'none';
|
|
locationCheckboxGroup.innerHTML = '';
|
|
}
|
|
|
|
|
|
function hideRoleWarning() {
|
|
roleWarning.classList.remove('fade-in');
|
|
setTimeout(() => roleWarning.style.display = 'none', 300);
|
|
}
|
|
|
|
function getRoleDisplayName(role) {
|
|
const roleNames = {
|
|
'staff': 'Staff User',
|
|
'payroll': 'Payroll Specialist',
|
|
'accounting': 'Accounting Specialist',
|
|
'project_manager': 'Project Manager',
|
|
'admin': 'Administrator'
|
|
};
|
|
return roleNames[role] || role;
|
|
}
|
|
|
|
// Password strength indicator
|
|
newPasswordInput.addEventListener('input', function() {
|
|
const password = this.value;
|
|
|
|
if (password.length === 0) {
|
|
passwordStrength.className = 'password-strength';
|
|
passwordStrength.textContent = '';
|
|
return;
|
|
}
|
|
|
|
const strength = calculatePasswordStrength(password);
|
|
passwordStrength.className = `password-strength ${strength.class}`;
|
|
passwordStrength.textContent = strength.text;
|
|
});
|
|
|
|
// 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 {
|
|
this.setCustomValidity('Passwords do not match');
|
|
this.classList.remove('valid');
|
|
this.classList.add('invalid');
|
|
}
|
|
} else {
|
|
this.setCustomValidity('');
|
|
this.classList.remove('valid', 'invalid');
|
|
}
|
|
});
|
|
|
|
// 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;
|
|
|
|
// If password is being changed, validate it
|
|
if (newPassword || confirmPassword) {
|
|
if (newPassword.length < 6) {
|
|
e.preventDefault();
|
|
alert('Password must be at least 6 characters long.');
|
|
newPasswordInput.focus();
|
|
return;
|
|
}
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
e.preventDefault();
|
|
alert('Password confirmation does not match.');
|
|
confirmPasswordInput.focus();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
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>
|
|
{% endblock %} |