29154bd651
Co-authored-by: Copilot <copilot@github.com>
536 lines
19 KiB
HTML
536 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>User Manager</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.header {
|
|
background: white;
|
|
padding: 30px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
margin-bottom: 30px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.header h1 {
|
|
color: #333;
|
|
}
|
|
|
|
.card {
|
|
background: white;
|
|
padding: 30px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.card h2 {
|
|
color: #333;
|
|
margin-bottom: 20px;
|
|
border-bottom: 2px solid #667eea;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
color: #333;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.form-group input, .form-group select {
|
|
width: 100%;
|
|
padding: 12px;
|
|
border: 2px solid #e0e0e0;
|
|
border-radius: 5px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.form-group input:focus, .form-group select:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 30px;
|
|
border: none;
|
|
border-radius: 5px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #5568d3;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: #f56565;
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: #e53e3e;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #cbd5e0;
|
|
color: #333;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #a0aec0;
|
|
}
|
|
|
|
.user-item {
|
|
background: #f7fafc;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
margin-bottom: 10px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-left: 4px solid #667eea;
|
|
}
|
|
|
|
.user-item.inactive {
|
|
opacity: 0.6;
|
|
border-left-color: #cbd5e0;
|
|
}
|
|
|
|
.user-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.user-info strong {
|
|
color: #333;
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.user-info span {
|
|
color: #666;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.user-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.user-actions button {
|
|
padding: 8px 16px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.alert {
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
margin-bottom: 20px;
|
|
display: none;
|
|
}
|
|
|
|
.alert.show {
|
|
display: block;
|
|
}
|
|
|
|
.alert-success {
|
|
background: #c6f6d5;
|
|
color: #22543d;
|
|
border-left: 4px solid #48bb78;
|
|
}
|
|
|
|
.alert-error {
|
|
background: #fed7d7;
|
|
color: #742a2a;
|
|
border-left: 4px solid #f56565;
|
|
}
|
|
|
|
.active-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.active-toggle input {
|
|
width: 20px;
|
|
height: 20px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.location-checkboxes {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 10px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.location-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.location-item input {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>👥 User Manager</h1>
|
|
<a href="{{ url_for('home') }}" style="color: #333; text-decoration: none;">← Back to Home</a>
|
|
</div>
|
|
|
|
<div class="alert alert-success" id="successAlert"></div>
|
|
<div class="alert alert-error" id="errorAlert"></div>
|
|
|
|
<!-- Add User Form -->
|
|
<div class="card">
|
|
<h2>Add New User</h2>
|
|
<form id="addUserForm">
|
|
<div class="form-group">
|
|
<label for="newUsername">Username</label>
|
|
<input type="text" id="newUsername" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="newPassword">Password</label>
|
|
<input type="password" id="newPassword" required minlength="6">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="defaultLocation">Default Location</label>
|
|
<select id="defaultLocation" required>
|
|
<option value="">Select Location</option>
|
|
<option value="LINDS">LINDS</option>
|
|
<option value="IOLA">IOLA</option>
|
|
<option value="KC">KC</option>
|
|
<option value="BMD">BMD</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Accessible Locations</label>
|
|
<div class="location-checkboxes" id="accessibleLocations">
|
|
<div class="location-item">
|
|
<input type="checkbox" id="loc_LINDS" value="LINDS">
|
|
<label for="loc_LINDS">LINDS</label>
|
|
</div>
|
|
<div class="location-item">
|
|
<input type="checkbox" id="loc_IOLA" value="IOLA">
|
|
<label for="loc_IOLA">IOLA</label>
|
|
</div>
|
|
<div class="location-item">
|
|
<input type="checkbox" id="loc_KC" value="KC">
|
|
<label for="loc_KC">KC</label>
|
|
</div>
|
|
<div class="location-item">
|
|
<input type="checkbox" id="loc_BMD" value="BMD">
|
|
<label for="loc_BMD">BMD</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Permissions</label>
|
|
<div class="location-checkboxes">
|
|
<div class="location-item">
|
|
<input type="checkbox" id="perm_manage_users" value="manage_users">
|
|
<label for="perm_manage_users">Manage Users</label>
|
|
</div>
|
|
<div class="location-item">
|
|
<input type="checkbox" id="perm_manage_products" value="manage_products">
|
|
<label for="perm_manage_products">Manage Products</label>
|
|
</div>
|
|
<div class="location-item">
|
|
<input type="checkbox" id="perm_create_quotes" value="create_quotes">
|
|
<label for="perm_create_quotes">Create Quotes</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary">Add User</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- User List -->
|
|
<div class="card">
|
|
<h2>Existing Users</h2>
|
|
<div id="userList"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Current logged-in user ID
|
|
const CURRENT_USER_ID = {{ current_user_id }};
|
|
|
|
let users = [];
|
|
|
|
// Load users on page load
|
|
loadUsers();
|
|
|
|
async function loadUsers() {
|
|
try {
|
|
const response = await fetch('/users/api');
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
users = data.users;
|
|
renderUsers();
|
|
} else {
|
|
showError(data.message);
|
|
}
|
|
} catch (error) {
|
|
showError('Error loading users: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function renderUsers() {
|
|
const userList = document.getElementById('userList');
|
|
|
|
if (users.length === 0) {
|
|
userList.innerHTML = '<p style="color: #666;">No users found. Add a user above.</p>';
|
|
return;
|
|
}
|
|
|
|
const isCurrentUser = (index) => index === CURRENT_USER_ID;
|
|
const isSuperAdmin = (user) => user.superAdmin === true;
|
|
const canModify = (user, index) => {
|
|
// Can't modify yourself (except password)
|
|
if (isCurrentUser(index)) return false;
|
|
// Can only modify super admins if you ARE that super admin
|
|
if (isSuperAdmin(user) && !isCurrentUser(index)) return false;
|
|
return true;
|
|
};
|
|
|
|
userList.innerHTML = users.map((user, index) => {
|
|
const isCurrent = isCurrentUser(index);
|
|
const isSuper = isSuperAdmin(user);
|
|
const canModifyUser = canModify(user, index);
|
|
const disabledStyle = (!canModifyUser ? 'opacity: 0.5; cursor: not-allowed;' : '');
|
|
const superAdminBadge = isSuper ? '<span style="background: #ed8936; color: white; padding: 2px 8px; border-radius: 3px; font-size: 11px; margin-left: 8px;">SUPER ADMIN</span>' : '';
|
|
|
|
return `
|
|
<div class="user-item ${user.active ? '' : 'inactive'}">
|
|
<div class="user-info">
|
|
<strong>${user.username}${isCurrent ? ' (You)' : ''}${superAdminBadge}</strong>
|
|
<span>Location: ${user.defaultLocation || 'Not set'} |
|
|
Status: ${user.active ? '<span style="color: #48bb78;">Active</span>' : '<span style="color: #f56565;">Inactive</span>'}</span>
|
|
</div>
|
|
<div class="user-actions">
|
|
<div class="active-toggle" style="${!canModifyUser && !isCurrent ? disabledStyle : (isCurrent ? 'opacity: 0.5; cursor: not-allowed;' : '')}">
|
|
<input type="checkbox" ${user.active ? 'checked' : ''}
|
|
onchange="toggleActive(${index}, this.checked)"
|
|
${!canModifyUser || isCurrent ? 'disabled title="' + (isCurrent ? 'You cannot deactivate your own account' : 'Super admin accounts can only be modified by themselves') + '"' : ''}>
|
|
<span>Active</span>
|
|
</div>
|
|
<button class="btn btn-secondary"
|
|
onclick="changePassword(${index})"
|
|
${!canModifyUser && !isCurrent ? 'disabled title="Super admin accounts can only be modified by themselves" style="' + disabledStyle + '"' : ''}>
|
|
Change Password
|
|
</button>
|
|
<button class="btn btn-danger"
|
|
onclick="deleteUser(${index})"
|
|
${!canModifyUser || isCurrent ? 'disabled title="' + (isCurrent ? 'You cannot delete your own account' : 'Super admin accounts can only be deleted by themselves') + '" style="' + disabledStyle + '"' : ''}>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// Add user form submission
|
|
document.getElementById('addUserForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const username = document.getElementById('newUsername').value;
|
|
const password = document.getElementById('newPassword').value;
|
|
const defaultLocation = document.getElementById('defaultLocation').value;
|
|
|
|
// Get accessible locations
|
|
const locationSettings = {};
|
|
['LINDS', 'IOLA', 'KC', 'BMD'].forEach(loc => {
|
|
const checkbox = document.getElementById(`loc_${loc}`);
|
|
if (checkbox.checked) {
|
|
locationSettings[loc] = { accessible: true, permissions: {} };
|
|
}
|
|
});
|
|
|
|
// Get permissions
|
|
const permissions = {};
|
|
if (document.getElementById('perm_manage_users').checked) {
|
|
permissions.manage_users = true;
|
|
}
|
|
if (document.getElementById('perm_manage_products').checked) {
|
|
permissions.manage_products = true;
|
|
}
|
|
if (document.getElementById('perm_create_quotes').checked) {
|
|
permissions.create_quotes = true;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/users/api', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
username,
|
|
password,
|
|
defaultLocation,
|
|
locationSettings,
|
|
permissions,
|
|
active: true
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
showSuccess('User added successfully!');
|
|
users = data.users;
|
|
renderUsers();
|
|
e.target.reset();
|
|
} else {
|
|
showError(data.message);
|
|
}
|
|
} catch (error) {
|
|
showError('Error adding user: ' + error.message);
|
|
}
|
|
});
|
|
|
|
async function toggleActive(index, active) {
|
|
try {
|
|
const response = await fetch(`/users/api/${index}/active`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ active })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
showSuccess('User status updated!');
|
|
users = data.users;
|
|
renderUsers();
|
|
} else {
|
|
showError(data.message);
|
|
renderUsers(); // Revert checkbox
|
|
}
|
|
} catch (error) {
|
|
showError('Error updating user: ' + error.message);
|
|
renderUsers(); // Revert checkbox
|
|
}
|
|
}
|
|
|
|
async function changePassword(index) {
|
|
const newPassword = prompt('Enter new password (min 6 characters):');
|
|
if (!newPassword || newPassword.length < 6) {
|
|
alert('Password must be at least 6 characters long');
|
|
return;
|
|
}
|
|
|
|
const currentPassword = prompt('Enter YOUR password to confirm this change:');
|
|
if (!currentPassword) return;
|
|
|
|
try {
|
|
const response = await fetch(`/users/api/${index}/change-password`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
newPassword: newPassword,
|
|
currentUserPassword: currentPassword
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
showSuccess(data.message);
|
|
} else {
|
|
showError(data.message);
|
|
}
|
|
} catch (error) {
|
|
showError('Error changing password: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function deleteUser(index) {
|
|
if (!confirm(`Are you sure you want to delete ${users[index].username}?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/users/api/${index}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
showSuccess('User deleted successfully!');
|
|
users = data.users;
|
|
renderUsers();
|
|
} else {
|
|
showError(data.message);
|
|
}
|
|
} catch (error) {
|
|
showError('Error deleting user: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
const alert = document.getElementById('successAlert');
|
|
alert.textContent = message;
|
|
alert.classList.add('show');
|
|
setTimeout(() => alert.classList.remove('show'), 5000);
|
|
}
|
|
|
|
function showError(message) {
|
|
const alert = document.getElementById('errorAlert');
|
|
alert.textContent = message;
|
|
alert.classList.add('show');
|
|
setTimeout(() => alert.classList.remove('show'), 5000);
|
|
}
|
|
|
|
// Auto-check location when selected as default
|
|
document.getElementById('defaultLocation').addEventListener('change', (e) => {
|
|
const loc = e.target.value;
|
|
if (loc) {
|
|
document.getElementById(`loc_${loc}`).checked = true;
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|