Features: - Manager-centric parking spot management - Fair assignment algorithm (parking/presence ratio) - Presence tracking calendar - Closing days (specific & weekly recurring) - Guarantees and exclusions - Authelia/LLDAP integration for SSO Stack: - FastAPI backend - SQLite database - Vanilla JS frontend - Docker deployment
248 lines
8.7 KiB
JavaScript
248 lines
8.7 KiB
JavaScript
/**
|
|
* Admin Users Page
|
|
* Manage all users in the system
|
|
*/
|
|
|
|
let currentUser = null;
|
|
let users = [];
|
|
let offices = [];
|
|
let managedOfficesMap = {}; // user_id -> [office_ids]
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
if (!api.requireAuth()) return;
|
|
|
|
currentUser = await api.getCurrentUser();
|
|
if (!currentUser) return;
|
|
|
|
// Only admins can access
|
|
if (currentUser.role !== 'admin') {
|
|
window.location.href = '/presence';
|
|
return;
|
|
}
|
|
|
|
await Promise.all([loadUsers(), loadOffices()]);
|
|
await loadManagedOffices();
|
|
renderUsers();
|
|
setupEventListeners();
|
|
});
|
|
|
|
async function loadUsers() {
|
|
const response = await api.get('/api/users');
|
|
if (response && response.ok) {
|
|
users = await response.json();
|
|
}
|
|
}
|
|
|
|
async function loadOffices() {
|
|
const response = await api.get('/api/offices');
|
|
if (response && response.ok) {
|
|
offices = await response.json();
|
|
const select = document.getElementById('editOffice');
|
|
offices.forEach(office => {
|
|
const option = document.createElement('option');
|
|
option.value = office.id;
|
|
option.textContent = office.name;
|
|
select.appendChild(option);
|
|
});
|
|
}
|
|
}
|
|
|
|
async function loadManagedOffices() {
|
|
// Load managed offices for all managers
|
|
const response = await api.get('/api/offices/managers/list');
|
|
if (response && response.ok) {
|
|
const managers = await response.json();
|
|
managedOfficesMap = {};
|
|
managers.forEach(m => {
|
|
managedOfficesMap[m.id] = m.offices.map(o => o.id);
|
|
});
|
|
}
|
|
}
|
|
|
|
function getManagedOfficeNames(userId) {
|
|
const officeIds = managedOfficesMap[userId] || [];
|
|
if (officeIds.length === 0) return '-';
|
|
return officeIds.map(id => {
|
|
const office = offices.find(o => o.id === id);
|
|
return office ? office.name : id;
|
|
}).join(', ');
|
|
}
|
|
|
|
function renderUsers(filter = '') {
|
|
const tbody = document.getElementById('usersBody');
|
|
const filtered = filter
|
|
? users.filter(u =>
|
|
u.name.toLowerCase().includes(filter.toLowerCase()) ||
|
|
u.email.toLowerCase().includes(filter.toLowerCase())
|
|
)
|
|
: users;
|
|
|
|
if (filtered.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="8" class="text-center">No users found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = filtered.map(user => {
|
|
const office = offices.find(o => o.id === user.office_id);
|
|
const isManager = user.role === 'manager';
|
|
const managedOffices = isManager ? getManagedOfficeNames(user.id) : '-';
|
|
return `
|
|
<tr>
|
|
<td>${user.name}</td>
|
|
<td>${user.email}</td>
|
|
<td><span class="badge badge-${user.role}">${user.role}</span></td>
|
|
<td>${office ? office.name : '-'}</td>
|
|
<td>${managedOffices}</td>
|
|
<td>${isManager ? (user.manager_parking_quota || 0) : '-'}</td>
|
|
<td>${isManager ? (user.manager_spot_prefix || '-') : '-'}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-secondary" onclick="editUser('${user.id}')">Edit</button>
|
|
${user.id !== currentUser.id ? `
|
|
<button class="btn btn-sm btn-danger" onclick="deleteUser('${user.id}')">Delete</button>
|
|
` : ''}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function editUser(userId) {
|
|
const user = users.find(u => u.id === userId);
|
|
if (!user) return;
|
|
|
|
document.getElementById('userId').value = user.id;
|
|
document.getElementById('editName').value = user.name;
|
|
document.getElementById('editEmail').value = user.email;
|
|
document.getElementById('editRole').value = user.role;
|
|
document.getElementById('editOffice').value = user.office_id || '';
|
|
document.getElementById('editQuota').value = user.manager_parking_quota || 0;
|
|
document.getElementById('editPrefix').value = user.manager_spot_prefix || '';
|
|
|
|
const isManager = user.role === 'manager';
|
|
|
|
// Show/hide manager fields based on role
|
|
document.getElementById('quotaGroup').style.display = isManager ? 'block' : 'none';
|
|
document.getElementById('prefixGroup').style.display = isManager ? 'block' : 'none';
|
|
document.getElementById('managedOfficesGroup').style.display = isManager ? 'block' : 'none';
|
|
|
|
// Build managed offices checkboxes
|
|
const checkboxContainer = document.getElementById('managedOfficesCheckboxes');
|
|
const userManagedOffices = managedOfficesMap[userId] || [];
|
|
checkboxContainer.innerHTML = offices.map(office => `
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" name="managedOffice" value="${office.id}"
|
|
${userManagedOffices.includes(office.id) ? 'checked' : ''}>
|
|
${office.name}
|
|
</label>
|
|
`).join('');
|
|
|
|
document.getElementById('userModal').style.display = 'flex';
|
|
}
|
|
|
|
async function deleteUser(userId) {
|
|
const user = users.find(u => u.id === userId);
|
|
if (!user) return;
|
|
|
|
if (!confirm(`Delete user "${user.name}"? This cannot be undone.`)) return;
|
|
|
|
const response = await api.delete(`/api/users/${userId}`);
|
|
if (response && response.ok) {
|
|
await loadUsers();
|
|
await loadManagedOffices();
|
|
renderUsers();
|
|
utils.showMessage('User deleted', 'success');
|
|
} else {
|
|
const error = await response.json();
|
|
utils.showMessage(error.detail || 'Failed to delete user', 'error');
|
|
}
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Search
|
|
document.getElementById('searchInput').addEventListener('input', (e) => {
|
|
renderUsers(e.target.value);
|
|
});
|
|
|
|
// Modal
|
|
document.getElementById('closeUserModal').addEventListener('click', () => {
|
|
document.getElementById('userModal').style.display = 'none';
|
|
});
|
|
|
|
document.getElementById('cancelUser').addEventListener('click', () => {
|
|
document.getElementById('userModal').style.display = 'none';
|
|
});
|
|
|
|
// Role change shows/hides manager fields
|
|
document.getElementById('editRole').addEventListener('change', (e) => {
|
|
const isManager = e.target.value === 'manager';
|
|
document.getElementById('quotaGroup').style.display = isManager ? 'block' : 'none';
|
|
document.getElementById('prefixGroup').style.display = isManager ? 'block' : 'none';
|
|
document.getElementById('managedOfficesGroup').style.display = isManager ? 'block' : 'none';
|
|
});
|
|
|
|
// Form submit
|
|
document.getElementById('userForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const userId = document.getElementById('userId').value;
|
|
const role = document.getElementById('editRole').value;
|
|
|
|
const data = {
|
|
name: document.getElementById('editName').value,
|
|
role: role,
|
|
office_id: document.getElementById('editOffice').value || null
|
|
};
|
|
|
|
if (role === 'manager') {
|
|
data.manager_parking_quota = parseInt(document.getElementById('editQuota').value) || 0;
|
|
data.manager_spot_prefix = document.getElementById('editPrefix').value.toUpperCase() || null;
|
|
}
|
|
|
|
const response = await api.put(`/api/users/${userId}`, data);
|
|
if (response && response.ok) {
|
|
// Update managed offices if manager
|
|
if (role === 'manager') {
|
|
await updateManagedOffices(userId);
|
|
}
|
|
|
|
document.getElementById('userModal').style.display = 'none';
|
|
await loadUsers();
|
|
await loadManagedOffices();
|
|
renderUsers();
|
|
utils.showMessage('User updated', 'success');
|
|
} else {
|
|
const error = await response.json();
|
|
utils.showMessage(error.detail || 'Failed to update user', 'error');
|
|
}
|
|
});
|
|
|
|
utils.setupModalClose('userModal');
|
|
}
|
|
|
|
async function updateManagedOffices(userId) {
|
|
// Get currently selected offices
|
|
const checkboxes = document.querySelectorAll('input[name="managedOffice"]:checked');
|
|
const selectedOfficeIds = Array.from(checkboxes).map(cb => cb.value);
|
|
|
|
// Get current managed offices
|
|
const currentOfficeIds = managedOfficesMap[userId] || [];
|
|
|
|
// Find offices to add and remove
|
|
const toAdd = selectedOfficeIds.filter(id => !currentOfficeIds.includes(id));
|
|
const toRemove = currentOfficeIds.filter(id => !selectedOfficeIds.includes(id));
|
|
|
|
// Add new memberships
|
|
for (const officeId of toAdd) {
|
|
await api.post(`/api/offices/${officeId}/managers`, { user_id: userId });
|
|
}
|
|
|
|
// Remove old memberships
|
|
for (const officeId of toRemove) {
|
|
await api.delete(`/api/offices/${officeId}/managers/${userId}`);
|
|
}
|
|
}
|
|
|
|
// Make functions globally accessible
|
|
window.editUser = editUser;
|
|
window.deleteUser = deleteUser;
|