670 lines
23 KiB
HTML
670 lines
23 KiB
HTML
{% 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>
|
|
|
|
<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="username">
|
|
<i class="fas fa-user"></i>
|
|
Username
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="username"
|
|
name="username"
|
|
required
|
|
pattern="[a-zA-Z0-9_]+"
|
|
placeholder="Enter username"
|
|
/>
|
|
<small class="form-help"
|
|
>Letters, numbers, and underscores only</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="">Select Role</option>
|
|
<option value="staff">Staff User</option>
|
|
<option value="payroll">Payroll Specialist</option>
|
|
<option value="accounting">Accounting Specialist</option>
|
|
<option value="project_manager">Project Manager</option>
|
|
<option value="admin">Administrator</option>
|
|
</select>
|
|
<small class="form-help"
|
|
>Determines user permissions and system access level</small
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">
|
|
<i class="fas fa-lock"></i>
|
|
Initial Password
|
|
</label>
|
|
<input
|
|
type="password"
|
|
id="password"
|
|
name="password"
|
|
required
|
|
minlength="6"
|
|
placeholder="Enter initial password"
|
|
/>
|
|
<div class="password-strength" id="passwordStrength"></div>
|
|
<small class="form-help"
|
|
>Minimum 6 characters. User should change on first login</small
|
|
>
|
|
</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>
|
|
|
|
<!-- Role Information Panel -->
|
|
<div class="info-panel" id="roleInfo" style="display: none">
|
|
<div class="info-header">
|
|
<i class="fas fa-info-circle"></i>
|
|
<span>Role Permissions</span>
|
|
</div>
|
|
<div class="info-content" id="roleContent">
|
|
<!-- Dynamic content based on selected role -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User Creation Summary -->
|
|
<div class="creation-summary" id="creationSummary" style="display: none">
|
|
<h3>
|
|
<i class="fas fa-check-circle"></i>
|
|
User Creation Summary
|
|
</h3>
|
|
<div class="summary-grid">
|
|
<div class="summary-item">
|
|
<strong>Name:</strong> <span id="summaryName">-</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<strong>Email:</strong> <span id="summaryEmail">-</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<strong>Username:</strong> <span id="summaryUsername">-</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<strong>Role:</strong> <span id="summaryRole">-</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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-info" id="previewUser">
|
|
<i class="fas fa-eye"></i>
|
|
Preview User
|
|
</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-user-plus"></i>
|
|
Create User
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endblock %} {% block extra_scripts %}
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const form = document.getElementById("createUserForm");
|
|
const roleSelect = document.getElementById("role");
|
|
const roleInfo = document.getElementById("roleInfo");
|
|
const roleContent = document.getElementById("roleContent");
|
|
const previewBtn = document.getElementById("previewUser");
|
|
const creationSummary = document.getElementById("creationSummary");
|
|
|
|
// Permission section elements
|
|
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');
|
|
|
|
// Form inputs
|
|
const fullNameInput = document.getElementById("full_name");
|
|
const emailInput = document.getElementById("email");
|
|
const usernameInput = document.getElementById("username");
|
|
const passwordInput = document.getElementById("password");
|
|
const passwordStrength = document.getElementById("passwordStrength");
|
|
|
|
// UPDATED: Role information with new roles
|
|
const rolePermissions = {
|
|
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: [
|
|
"View assigned projects only",
|
|
"View assigned locations only",
|
|
"Create and edit QR codes for assigned projects",
|
|
"View attendance data for assigned projects/locations",
|
|
"Download QR code images",
|
|
"Update personal profile information",
|
|
"Access dashboard with filtered data",
|
|
],
|
|
restrictions: [
|
|
"Cannot delete QR codes",
|
|
"Cannot manage other users",
|
|
"Cannot access admin settings",
|
|
"Can only view data for assigned projects and locations",
|
|
],
|
|
},
|
|
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!"],
|
|
},
|
|
};
|
|
|
|
// Role selection handler
|
|
roleSelect.addEventListener("change", function () {
|
|
const selectedRole = this.value;
|
|
|
|
if (selectedRole && rolePermissions[selectedRole]) {
|
|
const roleData = rolePermissions[selectedRole];
|
|
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"), 100);
|
|
} else {
|
|
roleInfo.style.display = "none";
|
|
roleInfo.classList.remove("fade-in");
|
|
}
|
|
|
|
// Show/hide Project Manager permissions section
|
|
if (selectedRole === 'project_manager') {
|
|
pmSection.style.display = 'block';
|
|
setupPermissionFlow();
|
|
} else {
|
|
pmSection.style.display = 'none';
|
|
resetPermissionFlow();
|
|
}
|
|
});
|
|
|
|
// Setup cascading permission flow
|
|
function setupPermissionFlow() {
|
|
// Reset state
|
|
locationStep.style.display = 'none';
|
|
permissionsOverview.style.display = 'none';
|
|
locationCheckboxGroup.innerHTML = '';
|
|
|
|
// Add event listeners to project checkboxes
|
|
projectCheckboxes.forEach(checkbox => {
|
|
checkbox.addEventListener('change', handleProjectSelection);
|
|
});
|
|
}
|
|
|
|
// Handle project selection changes
|
|
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';
|
|
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 = '';
|
|
}
|
|
|
|
// Password strength indicator
|
|
passwordInput.addEventListener("input", function () {
|
|
const password = this.value;
|
|
const strength = calculatePasswordStrength(password);
|
|
|
|
passwordStrength.className = `password-strength ${strength.class}`;
|
|
passwordStrength.textContent = strength.text;
|
|
});
|
|
|
|
// Preview user functionality
|
|
previewBtn.addEventListener("click", function () {
|
|
const fullName = fullNameInput.value.trim();
|
|
const email = emailInput.value.trim();
|
|
const username = usernameInput.value.trim();
|
|
const role = roleSelect.value;
|
|
|
|
if (!fullName || !email || !username || !role) {
|
|
alert("Please fill in all required fields before previewing.");
|
|
return;
|
|
}
|
|
|
|
// Update summary
|
|
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;
|
|
|
|
creationSummary.style.display = "block";
|
|
setTimeout(() => creationSummary.classList.add("fade-in"), 100);
|
|
|
|
// Scroll to summary
|
|
creationSummary.scrollIntoView({ behavior: "smooth" });
|
|
});
|
|
|
|
// Form validation
|
|
form.addEventListener("submit", function (e) {
|
|
const password = passwordInput.value;
|
|
|
|
if (password.length < 6) {
|
|
e.preventDefault();
|
|
alert("Password must be at least 6 characters long.");
|
|
passwordInput.focus();
|
|
return;
|
|
}
|
|
|
|
if (!roleSelect.value) {
|
|
e.preventDefault();
|
|
alert("Please select a user role.");
|
|
roleSelect.focus();
|
|
return;
|
|
}
|
|
});
|
|
|
|
// 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>
|
|
{% endblock %}
|