Refactor to manager-centric model, add team calendar for all users
Key changes: - Removed office-centric model (deleted offices.py, office-rules) - Renamed to team-rules, managers are part of their own team - Team calendar visible to all (read-only for employees) - Admins can have a manager assigned
This commit is contained in:
@@ -1,141 +1,138 @@
|
||||
/**
|
||||
* Admin Users Page
|
||||
* Manage all users in the system
|
||||
* Manage users with LDAP-aware editing
|
||||
*/
|
||||
|
||||
let currentUser = null;
|
||||
let users = [];
|
||||
let offices = [];
|
||||
let managedOfficesMap = {}; // user_id -> [office_ids]
|
||||
let managers = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!api.requireAuth()) return;
|
||||
|
||||
currentUser = await api.getCurrentUser();
|
||||
currentUser = await api.requireAuth();
|
||||
if (!currentUser) return;
|
||||
|
||||
// Only admins can access
|
||||
if (currentUser.role !== 'admin') {
|
||||
window.location.href = '/presence';
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([loadUsers(), loadOffices()]);
|
||||
await loadManagedOffices();
|
||||
renderUsers();
|
||||
await loadManagers();
|
||||
await loadUsers();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
async function loadManagers() {
|
||||
const response = await api.get('/api/managers');
|
||||
if (response && response.ok) {
|
||||
managers = await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
const response = await api.get('/api/users');
|
||||
if (response && response.ok) {
|
||||
users = await response.json();
|
||||
renderUsers();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
const filterLower = filter.toLowerCase();
|
||||
|
||||
let filtered = users;
|
||||
if (filter) {
|
||||
filtered = users.filter(u =>
|
||||
(u.name || '').toLowerCase().includes(filterLower) ||
|
||||
(u.email || '').toLowerCase().includes(filterLower) ||
|
||||
(u.role || '').toLowerCase().includes(filterLower) ||
|
||||
(u.manager_name || '').toLowerCase().includes(filterLower)
|
||||
);
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center">No users found</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="5" 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) : '-';
|
||||
const ldapBadge = user.is_ldap_user ? '<span class="badge badge-info">LDAP</span>' : '';
|
||||
const managerInfo = user.role === 'manager'
|
||||
? `<span class="badge badge-success">${user.managed_user_count || 0} users</span>`
|
||||
: (user.manager_name || '-');
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${user.name}</td>
|
||||
<td>${user.name || '-'} ${ldapBadge}</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><span class="badge badge-${getRoleBadgeClass(user.role)}">${user.role}</span></td>
|
||||
<td>${managerInfo}</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>
|
||||
` : ''}
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteUser('${user.id}')" ${user.id === currentUser.id ? 'disabled' : ''}>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function editUser(userId) {
|
||||
function getRoleBadgeClass(role) {
|
||||
switch (role) {
|
||||
case 'admin': return 'danger';
|
||||
case 'manager': return 'warning';
|
||||
default: return 'secondary';
|
||||
}
|
||||
}
|
||||
|
||||
async function editUser(userId) {
|
||||
const user = users.find(u => u.id === userId);
|
||||
if (!user) return;
|
||||
|
||||
// Populate form
|
||||
document.getElementById('userId').value = user.id;
|
||||
document.getElementById('editName').value = user.name;
|
||||
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';
|
||||
// Populate manager dropdown
|
||||
const managerSelect = document.getElementById('editManager');
|
||||
managerSelect.innerHTML = '<option value="">No manager</option>';
|
||||
managers.forEach(m => {
|
||||
if (m.id !== userId) { // Can't be own manager
|
||||
const option = document.createElement('option');
|
||||
option.value = m.id;
|
||||
option.textContent = m.name;
|
||||
if (m.id === user.manager_id) option.selected = true;
|
||||
managerSelect.appendChild(option);
|
||||
}
|
||||
});
|
||||
|
||||
// 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';
|
||||
// Handle LDAP restrictions
|
||||
const isLdap = user.is_ldap_user;
|
||||
const isLdapAdmin = user.is_ldap_admin;
|
||||
|
||||
// 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('');
|
||||
// LDAP notice
|
||||
document.getElementById('ldapNotice').style.display = isLdap ? 'block' : 'none';
|
||||
|
||||
// Name field - disabled for LDAP users
|
||||
const nameInput = document.getElementById('editName');
|
||||
nameInput.disabled = isLdap;
|
||||
document.getElementById('nameHelp').style.display = isLdap ? 'block' : 'none';
|
||||
|
||||
// Role field - admin option disabled for LDAP admins (they can't be demoted)
|
||||
const roleSelect = document.getElementById('editRole');
|
||||
roleSelect.disabled = isLdapAdmin;
|
||||
document.getElementById('roleHelp').style.display = isLdapAdmin ? 'block' : 'none';
|
||||
|
||||
// Manager group - show for all users (admins can also be assigned to a manager)
|
||||
document.getElementById('managerGroup').style.display = 'block';
|
||||
|
||||
// Manager fields - show only for managers
|
||||
document.getElementById('managerFields').style.display = user.role === 'manager' ? 'block' : 'none';
|
||||
|
||||
document.getElementById('userModalTitle').textContent = 'Edit User';
|
||||
document.getElementById('userModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
@@ -143,14 +140,12 @@ 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;
|
||||
if (!confirm(`Delete user "${user.name || user.email}"?`)) return;
|
||||
|
||||
const response = await api.delete(`/api/users/${userId}`);
|
||||
if (response && response.ok) {
|
||||
await loadUsers();
|
||||
await loadManagedOffices();
|
||||
renderUsers();
|
||||
utils.showMessage('User deleted', 'success');
|
||||
await loadUsers();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
utils.showMessage(error.detail || 'Failed to delete user', 'error');
|
||||
@@ -163,22 +158,21 @@ function setupEventListeners() {
|
||||
renderUsers(e.target.value);
|
||||
});
|
||||
|
||||
// Modal
|
||||
// Role change - toggle manager fields (manager group always visible since any user can have a manager)
|
||||
document.getElementById('editRole').addEventListener('change', (e) => {
|
||||
const role = e.target.value;
|
||||
document.getElementById('managerFields').style.display = role === 'manager' ? 'block' : 'none';
|
||||
// Manager group stays visible - any user (including admins) can have a manager assigned
|
||||
});
|
||||
|
||||
// Modal close
|
||||
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';
|
||||
});
|
||||
utils.setupModalClose('userModal');
|
||||
|
||||
// Form submit
|
||||
document.getElementById('userForm').addEventListener('submit', async (e) => {
|
||||
@@ -188,60 +182,35 @@ function setupEventListeners() {
|
||||
const role = document.getElementById('editRole').value;
|
||||
|
||||
const data = {
|
||||
name: document.getElementById('editName').value,
|
||||
role: role,
|
||||
office_id: document.getElementById('editOffice').value || null
|
||||
manager_id: document.getElementById('editManager').value || null
|
||||
};
|
||||
|
||||
// Only include name if not disabled (LDAP users can't change name)
|
||||
const nameInput = document.getElementById('editName');
|
||||
if (!nameInput.disabled) {
|
||||
data.name = nameInput.value;
|
||||
}
|
||||
|
||||
// Manager-specific fields
|
||||
if (role === 'manager') {
|
||||
data.manager_parking_quota = parseInt(document.getElementById('editQuota').value) || 0;
|
||||
data.manager_spot_prefix = document.getElementById('editPrefix').value.toUpperCase() || null;
|
||||
data.manager_spot_prefix = document.getElementById('editPrefix').value || 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');
|
||||
await loadManagers(); // Reload in case role changed
|
||||
await loadUsers();
|
||||
} 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
|
||||
// Make functions available globally for onclick handlers
|
||||
window.editUser = editUser;
|
||||
window.deleteUser = deleteUser;
|
||||
|
||||
Reference in New Issue
Block a user