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;
|
||||
|
||||
@@ -26,21 +26,42 @@ const api = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
* Check if user is authenticated (token or Authelia)
|
||||
*/
|
||||
isAuthenticated() {
|
||||
return !!this.getToken();
|
||||
return !!this.getToken() || this._autheliaAuth;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check authentication - works with both JWT and Authelia
|
||||
* Call this on page load to verify auth status
|
||||
*/
|
||||
async checkAuth() {
|
||||
// Try to get current user - works with Authelia headers or JWT
|
||||
const response = await fetch('/api/auth/me', {
|
||||
headers: this.getToken() ? { 'Authorization': `Bearer ${this.getToken()}` } : {}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this._autheliaAuth = true;
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
this._autheliaAuth = false;
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Redirect to login if not authenticated
|
||||
* Returns user object if authenticated, null otherwise
|
||||
*/
|
||||
requireAuth() {
|
||||
if (!this.isAuthenticated()) {
|
||||
async requireAuth() {
|
||||
const user = await this.checkAuth();
|
||||
if (!user) {
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
return true;
|
||||
return user;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -143,11 +164,11 @@ const api = {
|
||||
/**
|
||||
* Register
|
||||
*/
|
||||
async register(email, password, name, officeId = null) {
|
||||
async register(email, password, name) {
|
||||
const response = await fetch('/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password, name, office_id: officeId })
|
||||
body: JSON.stringify({ email, password, name })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
|
||||
@@ -43,8 +43,8 @@ const ICONS = {
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: '/presence', icon: 'calendar', label: 'My Presence' },
|
||||
{ href: '/team-calendar', icon: 'users', label: 'Team Calendar', roles: ['admin', 'manager'] },
|
||||
{ href: '/office-rules', icon: 'rules', label: 'Office Rules', roles: ['admin', 'manager'] },
|
||||
{ href: '/team-calendar', icon: 'users', label: 'Team Calendar' },
|
||||
{ href: '/team-rules', icon: 'rules', label: 'Team Rules', roles: ['admin', 'manager'] },
|
||||
{ href: '/admin/users', icon: 'user', label: 'Manage Users', roles: ['admin'] }
|
||||
];
|
||||
|
||||
@@ -77,26 +77,19 @@ async function initNav() {
|
||||
if (!navContainer) return;
|
||||
|
||||
const currentPath = window.location.pathname;
|
||||
let userRole = null;
|
||||
let currentUser = null;
|
||||
|
||||
// Get user info
|
||||
if (api.isAuthenticated()) {
|
||||
currentUser = await api.getCurrentUser();
|
||||
if (currentUser) {
|
||||
userRole = currentUser.role;
|
||||
}
|
||||
}
|
||||
// Get user info (works with both JWT and Authelia)
|
||||
const currentUser = await api.checkAuth();
|
||||
|
||||
// Render navigation
|
||||
navContainer.innerHTML = renderNav(currentPath, userRole);
|
||||
navContainer.innerHTML = renderNav(currentPath, currentUser?.role);
|
||||
|
||||
// Update user info in sidebar
|
||||
if (currentUser) {
|
||||
const userName = document.getElementById('userName');
|
||||
const userRole = document.getElementById('userRole');
|
||||
if (userName) userName.textContent = currentUser.name || 'User';
|
||||
if (userRole) userRole.textContent = currentUser.role || '-';
|
||||
const userNameEl = document.getElementById('userName');
|
||||
const userRoleEl = document.getElementById('userRole');
|
||||
if (userNameEl) userNameEl.textContent = currentUser.name || 'User';
|
||||
if (userRoleEl) userRoleEl.textContent = currentUser.role || '-';
|
||||
}
|
||||
|
||||
// Setup user menu
|
||||
|
||||
@@ -10,9 +10,7 @@ let parkingData = {};
|
||||
let currentAssignmentId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!api.requireAuth()) return;
|
||||
|
||||
currentUser = await api.getCurrentUser();
|
||||
currentUser = await api.requireAuth();
|
||||
if (!currentUser) return;
|
||||
|
||||
await Promise.all([loadPresences(), loadParkingAssignments()]);
|
||||
|
||||
@@ -16,17 +16,9 @@ let selectedDate = null;
|
||||
let currentAssignmentId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!api.requireAuth()) return;
|
||||
|
||||
currentUser = await api.getCurrentUser();
|
||||
currentUser = await api.requireAuth();
|
||||
if (!currentUser) return;
|
||||
|
||||
// Only managers and admins can view
|
||||
if (currentUser.role === 'employee') {
|
||||
window.location.href = '/presence';
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize start date based on week start preference
|
||||
const weekStartDay = currentUser.week_start_day || 0;
|
||||
currentStartDate = utils.getWeekStart(new Date(), weekStartDay);
|
||||
@@ -38,7 +30,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
});
|
||||
|
||||
async function loadManagers() {
|
||||
const response = await api.get('/api/offices/managers/list');
|
||||
const response = await api.get('/api/managers');
|
||||
if (response && response.ok) {
|
||||
managers = await response.json();
|
||||
const select = document.getElementById('managerFilter');
|
||||
@@ -48,20 +40,32 @@ async function loadManagers() {
|
||||
if (currentUser.role === 'manager') {
|
||||
// Manager only sees themselves
|
||||
filteredManagers = managers.filter(m => m.id === currentUser.id);
|
||||
} else if (currentUser.role === 'employee') {
|
||||
// Employee only sees their own manager
|
||||
if (currentUser.manager_id) {
|
||||
filteredManagers = managers.filter(m => m.id === currentUser.manager_id);
|
||||
} else {
|
||||
filteredManagers = [];
|
||||
}
|
||||
}
|
||||
|
||||
filteredManagers.forEach(manager => {
|
||||
const option = document.createElement('option');
|
||||
option.value = manager.id;
|
||||
const officeNames = manager.offices.map(o => o.name).join(', ');
|
||||
option.textContent = `${manager.name} (${officeNames})`;
|
||||
const userCount = manager.managed_user_count || 0;
|
||||
option.textContent = `${manager.name} (${userCount} users)`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
// Auto-select if only one manager (for manager role)
|
||||
// Auto-select for managers and employees (they only see their team)
|
||||
if (filteredManagers.length === 1) {
|
||||
select.value = filteredManagers[0].id;
|
||||
}
|
||||
|
||||
// Hide manager filter for employees (they can only see their team)
|
||||
if (currentUser.role === 'employee') {
|
||||
select.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +133,7 @@ function renderCalendar() {
|
||||
|
||||
// Build header row
|
||||
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
let headerHtml = '<th>Name</th><th>Office</th>';
|
||||
let headerHtml = '<th>Name</th><th>Manager</th>';
|
||||
|
||||
for (let i = 0; i < dayCount; i++) {
|
||||
const date = new Date(startDate);
|
||||
@@ -161,7 +165,7 @@ function renderCalendar() {
|
||||
teamData.forEach(member => {
|
||||
bodyHtml += `<tr>
|
||||
<td class="member-name">${member.name || 'Unknown'}</td>
|
||||
<td class="member-office">${member.office_name || '-'}</td>`;
|
||||
<td class="member-manager">${member.manager_name || '-'}</td>`;
|
||||
|
||||
for (let i = 0; i < dayCount; i++) {
|
||||
const date = new Date(startDate);
|
||||
@@ -197,16 +201,18 @@ function renderCalendar() {
|
||||
});
|
||||
body.innerHTML = bodyHtml;
|
||||
|
||||
// Add click handlers to cells
|
||||
body.querySelectorAll('.calendar-cell').forEach(cell => {
|
||||
cell.style.cursor = 'pointer';
|
||||
cell.addEventListener('click', () => {
|
||||
const userId = cell.dataset.userId;
|
||||
const date = cell.dataset.date;
|
||||
const userName = cell.dataset.userName;
|
||||
openDayModal(userId, date, userName);
|
||||
// Add click handlers to cells (only for admins and managers)
|
||||
if (currentUser.role === 'admin' || currentUser.role === 'manager') {
|
||||
body.querySelectorAll('.calendar-cell').forEach(cell => {
|
||||
cell.style.cursor = 'pointer';
|
||||
cell.addEventListener('click', () => {
|
||||
const userId = cell.dataset.userId;
|
||||
const date = cell.dataset.date;
|
||||
const userName = cell.dataset.userName;
|
||||
openDayModal(userId, date, userName);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openDayModal(userId, dateStr, userName) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Office Rules Page
|
||||
* Team Rules Page
|
||||
* Manage closing days, parking guarantees, and exclusions
|
||||
*
|
||||
* Rules are set at manager level and apply to all offices managed by that manager.
|
||||
* Rules are set at manager level for their parking pool.
|
||||
*/
|
||||
|
||||
let currentUser = null;
|
||||
@@ -10,9 +10,7 @@ let selectedManagerId = null;
|
||||
let managerUsers = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!api.requireAuth()) return;
|
||||
|
||||
currentUser = await api.getCurrentUser();
|
||||
currentUser = await api.requireAuth();
|
||||
if (!currentUser) return;
|
||||
|
||||
// Only managers and admins can access
|
||||
@@ -26,10 +24,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
});
|
||||
|
||||
async function loadManagers() {
|
||||
const response = await api.get('/api/offices/managers/list');
|
||||
const response = await api.get('/api/managers');
|
||||
if (response && response.ok) {
|
||||
const managers = await response.json();
|
||||
const select = document.getElementById('officeSelect');
|
||||
const select = document.getElementById('managerSelect');
|
||||
|
||||
// Filter to managers this user can see
|
||||
let filteredManagers = managers;
|
||||
@@ -45,14 +43,10 @@ async function loadManagers() {
|
||||
filteredManagers.forEach(manager => {
|
||||
const option = document.createElement('option');
|
||||
option.value = manager.id;
|
||||
// Show manager name and count of offices
|
||||
const officeCount = manager.offices.length;
|
||||
const officeNames = manager.offices.map(o => o.name).join(', ');
|
||||
if (officeCount > 0) {
|
||||
option.textContent = `${manager.name} (${officeNames})`;
|
||||
} else {
|
||||
option.textContent = `${manager.name} (no offices)`;
|
||||
}
|
||||
// Show manager name with user count and parking quota
|
||||
const userCount = manager.managed_user_count || 0;
|
||||
const quota = manager.parking_quota || 0;
|
||||
option.textContent = `${manager.name} (${userCount} users, ${quota} spots)`;
|
||||
select.appendChild(option);
|
||||
totalManagers++;
|
||||
if (!firstManagerId) firstManagerId = manager.id;
|
||||
@@ -71,12 +65,12 @@ async function selectManager(managerId) {
|
||||
|
||||
if (!managerId) {
|
||||
document.getElementById('rulesContent').style.display = 'none';
|
||||
document.getElementById('noOfficeMessage').style.display = 'block';
|
||||
document.getElementById('noManagerMessage').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('rulesContent').style.display = 'block';
|
||||
document.getElementById('noOfficeMessage').style.display = 'none';
|
||||
document.getElementById('noManagerMessage').style.display = 'none';
|
||||
|
||||
await Promise.all([
|
||||
loadWeeklyClosingDays(),
|
||||
@@ -88,7 +82,7 @@ async function selectManager(managerId) {
|
||||
}
|
||||
|
||||
async function loadWeeklyClosingDays() {
|
||||
const response = await api.get(`/api/offices/managers/${selectedManagerId}/weekly-closing-days`);
|
||||
const response = await api.get(`/api/managers/${selectedManagerId}/weekly-closing-days`);
|
||||
if (response && response.ok) {
|
||||
const days = await response.json();
|
||||
const weekdays = days.map(d => d.weekday);
|
||||
@@ -102,7 +96,7 @@ async function loadWeeklyClosingDays() {
|
||||
}
|
||||
|
||||
async function loadManagerUsers() {
|
||||
const response = await api.get(`/api/offices/managers/${selectedManagerId}/users`);
|
||||
const response = await api.get(`/api/managers/${selectedManagerId}/users`);
|
||||
if (response && response.ok) {
|
||||
managerUsers = await response.json();
|
||||
updateUserSelects();
|
||||
@@ -123,7 +117,7 @@ function updateUserSelects() {
|
||||
}
|
||||
|
||||
async function loadClosingDays() {
|
||||
const response = await api.get(`/api/offices/managers/${selectedManagerId}/closing-days`);
|
||||
const response = await api.get(`/api/managers/${selectedManagerId}/closing-days`);
|
||||
const container = document.getElementById('closingDaysList');
|
||||
|
||||
if (response && response.ok) {
|
||||
@@ -158,7 +152,7 @@ function formatDateRange(startDate, endDate) {
|
||||
}
|
||||
|
||||
async function loadGuarantees() {
|
||||
const response = await api.get(`/api/offices/managers/${selectedManagerId}/guarantees`);
|
||||
const response = await api.get(`/api/managers/${selectedManagerId}/guarantees`);
|
||||
const container = document.getElementById('guaranteesList');
|
||||
|
||||
if (response && response.ok) {
|
||||
@@ -188,7 +182,7 @@ async function loadGuarantees() {
|
||||
}
|
||||
|
||||
async function loadExclusions() {
|
||||
const response = await api.get(`/api/offices/managers/${selectedManagerId}/exclusions`);
|
||||
const response = await api.get(`/api/managers/${selectedManagerId}/exclusions`);
|
||||
const container = document.getElementById('exclusionsList');
|
||||
|
||||
if (response && response.ok) {
|
||||
@@ -220,7 +214,7 @@ async function loadExclusions() {
|
||||
// Delete functions
|
||||
async function deleteClosingDay(id) {
|
||||
if (!confirm('Delete this closing day?')) return;
|
||||
const response = await api.delete(`/api/offices/managers/${selectedManagerId}/closing-days/${id}`);
|
||||
const response = await api.delete(`/api/managers/${selectedManagerId}/closing-days/${id}`);
|
||||
if (response && response.ok) {
|
||||
await loadClosingDays();
|
||||
}
|
||||
@@ -228,7 +222,7 @@ async function deleteClosingDay(id) {
|
||||
|
||||
async function deleteGuarantee(id) {
|
||||
if (!confirm('Remove this parking guarantee?')) return;
|
||||
const response = await api.delete(`/api/offices/managers/${selectedManagerId}/guarantees/${id}`);
|
||||
const response = await api.delete(`/api/managers/${selectedManagerId}/guarantees/${id}`);
|
||||
if (response && response.ok) {
|
||||
await loadGuarantees();
|
||||
}
|
||||
@@ -236,7 +230,7 @@ async function deleteGuarantee(id) {
|
||||
|
||||
async function deleteExclusion(id) {
|
||||
if (!confirm('Remove this parking exclusion?')) return;
|
||||
const response = await api.delete(`/api/offices/managers/${selectedManagerId}/exclusions/${id}`);
|
||||
const response = await api.delete(`/api/managers/${selectedManagerId}/exclusions/${id}`);
|
||||
if (response && response.ok) {
|
||||
await loadExclusions();
|
||||
}
|
||||
@@ -244,7 +238,7 @@ async function deleteExclusion(id) {
|
||||
|
||||
function setupEventListeners() {
|
||||
// Manager selection
|
||||
document.getElementById('officeSelect').addEventListener('change', (e) => {
|
||||
document.getElementById('managerSelect').addEventListener('change', (e) => {
|
||||
selectManager(e.target.value);
|
||||
});
|
||||
|
||||
@@ -255,7 +249,7 @@ function setupEventListeners() {
|
||||
|
||||
if (e.target.checked) {
|
||||
// Add weekly closing day
|
||||
const response = await api.post(`/api/offices/managers/${selectedManagerId}/weekly-closing-days`, { weekday });
|
||||
const response = await api.post(`/api/managers/${selectedManagerId}/weekly-closing-days`, { weekday });
|
||||
if (!response || !response.ok) {
|
||||
e.target.checked = false;
|
||||
const error = await response.json();
|
||||
@@ -263,12 +257,12 @@ function setupEventListeners() {
|
||||
}
|
||||
} else {
|
||||
// Remove weekly closing day - need to find the ID first
|
||||
const getResponse = await api.get(`/api/offices/managers/${selectedManagerId}/weekly-closing-days`);
|
||||
const getResponse = await api.get(`/api/managers/${selectedManagerId}/weekly-closing-days`);
|
||||
if (getResponse && getResponse.ok) {
|
||||
const days = await getResponse.json();
|
||||
const day = days.find(d => d.weekday === weekday);
|
||||
if (day) {
|
||||
const deleteResponse = await api.delete(`/api/offices/managers/${selectedManagerId}/weekly-closing-days/${day.id}`);
|
||||
const deleteResponse = await api.delete(`/api/managers/${selectedManagerId}/weekly-closing-days/${day.id}`);
|
||||
if (!deleteResponse || !deleteResponse.ok) {
|
||||
e.target.checked = true;
|
||||
}
|
||||
@@ -320,7 +314,7 @@ function setupEventListeners() {
|
||||
date: document.getElementById('closingDate').value,
|
||||
reason: document.getElementById('closingReason').value || null
|
||||
};
|
||||
const response = await api.post(`/api/offices/managers/${selectedManagerId}/closing-days`, data);
|
||||
const response = await api.post(`/api/managers/${selectedManagerId}/closing-days`, data);
|
||||
if (response && response.ok) {
|
||||
document.getElementById('closingDayModal').style.display = 'none';
|
||||
await loadClosingDays();
|
||||
@@ -337,7 +331,7 @@ function setupEventListeners() {
|
||||
start_date: document.getElementById('guaranteeStartDate').value || null,
|
||||
end_date: document.getElementById('guaranteeEndDate').value || null
|
||||
};
|
||||
const response = await api.post(`/api/offices/managers/${selectedManagerId}/guarantees`, data);
|
||||
const response = await api.post(`/api/managers/${selectedManagerId}/guarantees`, data);
|
||||
if (response && response.ok) {
|
||||
document.getElementById('guaranteeModal').style.display = 'none';
|
||||
await loadGuarantees();
|
||||
@@ -354,7 +348,7 @@ function setupEventListeners() {
|
||||
start_date: document.getElementById('exclusionStartDate').value || null,
|
||||
end_date: document.getElementById('exclusionEndDate').value || null
|
||||
};
|
||||
const response = await api.post(`/api/offices/managers/${selectedManagerId}/exclusions`, data);
|
||||
const response = await api.post(`/api/managers/${selectedManagerId}/exclusions`, data);
|
||||
if (response && response.ok) {
|
||||
document.getElementById('exclusionModal').style.display = 'none';
|
||||
await loadExclusions();
|
||||
@@ -54,10 +54,7 @@
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Office</th>
|
||||
<th>Managed Offices</th>
|
||||
<th>Parking Quota</th>
|
||||
<th>Spot Prefix</th>
|
||||
<th>Manager</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -78,9 +75,16 @@
|
||||
<div class="modal-body">
|
||||
<form id="userForm">
|
||||
<input type="hidden" id="userId">
|
||||
|
||||
<!-- LDAP notice -->
|
||||
<div id="ldapNotice" class="form-notice" style="display: none;">
|
||||
<small>This user is managed by LDAP. Some fields cannot be edited.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editName">Name</label>
|
||||
<input type="text" id="editName" required>
|
||||
<small id="nameHelp" class="text-muted" style="display: none;">Managed by LDAP</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editEmail">Email</label>
|
||||
@@ -93,28 +97,32 @@
|
||||
<option value="manager">Manager</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<small id="roleHelp" class="text-muted" style="display: none;">Admin role is managed by LDAP group</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editOffice">Office</label>
|
||||
<select id="editOffice">
|
||||
<option value="">No office</option>
|
||||
<div class="form-group" id="managerGroup">
|
||||
<label for="editManager">Manager</label>
|
||||
<select id="editManager">
|
||||
<option value="">No manager</option>
|
||||
</select>
|
||||
<small class="text-muted">Who manages this user</small>
|
||||
</div>
|
||||
<div class="form-group" id="quotaGroup" style="display: none;">
|
||||
<label for="editQuota">Parking Quota</label>
|
||||
<input type="number" id="editQuota" min="0" value="0">
|
||||
<small class="text-muted">Number of parking spots this manager can assign</small>
|
||||
</div>
|
||||
<div class="form-group" id="prefixGroup" style="display: none;">
|
||||
<label for="editPrefix">Spot Prefix</label>
|
||||
<input type="text" id="editPrefix" maxlength="2" placeholder="A, B, C...">
|
||||
<small class="text-muted">Letter prefix for parking spots (e.g., A for spots A1, A2...)</small>
|
||||
</div>
|
||||
<div class="form-group" id="managedOfficesGroup" style="display: none;">
|
||||
<label>Managed Offices</label>
|
||||
<div id="managedOfficesCheckboxes" class="checkbox-group"></div>
|
||||
<small class="text-muted">Select offices this manager controls</small>
|
||||
|
||||
<!-- Manager-specific fields -->
|
||||
<div id="managerFields" style="display: none;">
|
||||
<hr>
|
||||
<h4>Manager Settings</h4>
|
||||
<div class="form-group">
|
||||
<label for="editQuota">Parking Quota</label>
|
||||
<input type="number" id="editQuota" min="0" value="0">
|
||||
<small class="text-muted">Number of parking spots this manager controls</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editPrefix">Spot Prefix</label>
|
||||
<input type="text" id="editPrefix" maxlength="2" placeholder="A, B, C...">
|
||||
<small class="text-muted">Letter prefix for parking spots (e.g., A for spots A1, A2...)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="cancelUser">Cancel</button>
|
||||
<button type="submit" class="btn btn-dark">Save</button>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1>Parking Manager</h1>
|
||||
<p>Manage office presence and parking assignments</p>
|
||||
<p>Manage team presence and parking assignments</p>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
@@ -23,10 +23,25 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Redirect if already logged in
|
||||
if (localStorage.getItem('access_token')) {
|
||||
window.location.href = '/presence';
|
||||
// Redirect if already logged in (JWT token or Authelia)
|
||||
async function checkAndRedirect() {
|
||||
// Check JWT token first
|
||||
if (localStorage.getItem('access_token')) {
|
||||
window.location.href = '/presence';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check Authelia (backend will read headers)
|
||||
try {
|
||||
const response = await fetch('/api/auth/me');
|
||||
if (response.ok) {
|
||||
window.location.href = '/presence';
|
||||
}
|
||||
} catch (e) {
|
||||
// Not authenticated, stay on landing
|
||||
}
|
||||
}
|
||||
checkAndRedirect();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -48,10 +48,16 @@
|
||||
<h3>Personal Information</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- LDAP Notice -->
|
||||
<div id="ldapNotice" class="form-notice" style="display: none; margin-bottom: 1rem;">
|
||||
<small>Your account is managed by LDAP. Some information cannot be changed here.</small>
|
||||
</div>
|
||||
|
||||
<form id="profileForm">
|
||||
<div class="form-group">
|
||||
<label for="name">Full Name</label>
|
||||
<input type="text" id="name" required>
|
||||
<small id="nameHelp" class="text-muted" style="display: none;">Managed by LDAP</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
@@ -59,19 +65,24 @@
|
||||
<small class="text-muted">Email cannot be changed</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="office">Office</label>
|
||||
<select id="office">
|
||||
<option value="">No office</option>
|
||||
</select>
|
||||
<label for="role">Role</label>
|
||||
<input type="text" id="role" disabled>
|
||||
<small class="text-muted">Role is assigned by your administrator</small>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<div class="form-group">
|
||||
<label for="manager">Manager</label>
|
||||
<input type="text" id="manager" disabled>
|
||||
<small class="text-muted">Your manager is assigned by the administrator</small>
|
||||
</div>
|
||||
<div class="form-actions" id="profileActions">
|
||||
<button type="submit" class="btn btn-dark">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<!-- Password section - hidden for LDAP users -->
|
||||
<div class="card" id="passwordCard">
|
||||
<div class="card-header">
|
||||
<h3>Change Password</h3>
|
||||
</div>
|
||||
@@ -104,36 +115,37 @@
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
let currentUser = null;
|
||||
let isLdapUser = false;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!api.requireAuth()) return;
|
||||
|
||||
currentUser = await api.getCurrentUser();
|
||||
currentUser = await api.requireAuth();
|
||||
if (!currentUser) return;
|
||||
|
||||
await loadOffices();
|
||||
populateForm();
|
||||
await loadProfile();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
async function loadOffices() {
|
||||
const response = await api.get('/api/offices');
|
||||
async function loadProfile() {
|
||||
const response = await api.get('/api/users/me/profile');
|
||||
if (response && response.ok) {
|
||||
const offices = await response.json();
|
||||
const select = document.getElementById('office');
|
||||
offices.forEach(office => {
|
||||
const option = document.createElement('option');
|
||||
option.value = office.id;
|
||||
option.textContent = office.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
}
|
||||
const profile = await response.json();
|
||||
isLdapUser = profile.is_ldap_user;
|
||||
|
||||
function populateForm() {
|
||||
document.getElementById('name').value = currentUser.name || '';
|
||||
document.getElementById('email').value = currentUser.email;
|
||||
document.getElementById('office').value = currentUser.office_id || '';
|
||||
// Populate form
|
||||
document.getElementById('name').value = profile.name || '';
|
||||
document.getElementById('email').value = profile.email;
|
||||
document.getElementById('role').value = profile.role;
|
||||
document.getElementById('manager').value = profile.manager_name || 'None';
|
||||
|
||||
// LDAP mode adjustments
|
||||
if (isLdapUser) {
|
||||
document.getElementById('ldapNotice').style.display = 'block';
|
||||
document.getElementById('name').disabled = true;
|
||||
document.getElementById('nameHelp').style.display = 'block';
|
||||
document.getElementById('profileActions').style.display = 'none';
|
||||
document.getElementById('passwordCard').style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
@@ -141,15 +153,21 @@
|
||||
document.getElementById('profileForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (isLdapUser) {
|
||||
utils.showMessage('Profile is managed by LDAP', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
name: document.getElementById('name').value,
|
||||
office_id: document.getElementById('office').value || null
|
||||
name: document.getElementById('name').value
|
||||
};
|
||||
|
||||
const response = await api.put('/api/users/me/profile', data);
|
||||
if (response && response.ok) {
|
||||
utils.showMessage('Profile updated successfully', 'success');
|
||||
currentUser = await api.getCurrentUser();
|
||||
// Update nav display
|
||||
const nameEl = document.getElementById('userName');
|
||||
if (nameEl) nameEl.textContent = data.name;
|
||||
} else {
|
||||
const error = await response.json();
|
||||
utils.showMessage(error.detail || 'Failed to update profile', 'error');
|
||||
|
||||
@@ -31,12 +31,6 @@
|
||||
<input type="password" id="password" required autocomplete="new-password" minlength="8">
|
||||
<small>Minimum 8 characters</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="office">Office (optional)</label>
|
||||
<select id="office">
|
||||
<option value="">Select an office...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-dark btn-full">Create Account</button>
|
||||
</form>
|
||||
|
||||
@@ -53,39 +47,17 @@
|
||||
window.location.href = '/presence';
|
||||
}
|
||||
|
||||
// Load offices
|
||||
async function loadOffices() {
|
||||
try {
|
||||
const response = await fetch('/api/offices');
|
||||
if (response.ok) {
|
||||
const offices = await response.json();
|
||||
const select = document.getElementById('office');
|
||||
offices.forEach(office => {
|
||||
const option = document.createElement('option');
|
||||
option.value = office.id;
|
||||
option.textContent = office.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load offices:', error);
|
||||
}
|
||||
}
|
||||
|
||||
loadOffices();
|
||||
|
||||
document.getElementById('registerForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const name = document.getElementById('name').value;
|
||||
const email = document.getElementById('email').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const officeId = document.getElementById('office').value || null;
|
||||
const errorDiv = document.getElementById('errorMessage');
|
||||
|
||||
errorDiv.innerHTML = '';
|
||||
|
||||
const result = await api.register(email, password, name, officeId);
|
||||
const result = await api.register(email, password, name);
|
||||
|
||||
if (result.success) {
|
||||
window.location.href = '/presence';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Office Rules - Parking Manager</title>
|
||||
<title>Team Rules - Parking Manager</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
@@ -39,9 +39,9 @@
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<h2>Office Rules</h2>
|
||||
<h2>Team Rules</h2>
|
||||
<div class="header-actions">
|
||||
<select id="officeSelect" class="form-select">
|
||||
<select id="managerSelect" class="form-select">
|
||||
<option value="">Select Manager</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -54,7 +54,7 @@
|
||||
<h3>Weekly Closing Days</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted" style="margin-bottom: 1rem;">Days of the week the office is regularly closed</p>
|
||||
<p class="text-muted" style="margin-bottom: 1rem;">Days of the week parking is unavailable</p>
|
||||
<div class="weekday-checkboxes" id="weeklyClosingDays">
|
||||
<label><input type="checkbox" data-weekday="0"> Sunday</label>
|
||||
<label><input type="checkbox" data-weekday="1"> Monday</label>
|
||||
@@ -74,7 +74,7 @@
|
||||
<button class="btn btn-secondary btn-sm" id="addClosingDayBtn">Add</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Specific dates when the office is closed (holidays, etc.)</p>
|
||||
<p class="text-muted">Specific dates when parking is unavailable (holidays, etc.)</p>
|
||||
<div id="closingDaysList" class="rules-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,10 +104,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-wrapper" id="noOfficeMessage">
|
||||
<div class="content-wrapper" id="noManagerMessage">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<p>Select a manager to manage their office rules</p>
|
||||
<p>Select a manager to manage their parking rules</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,6 +210,6 @@
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/utils.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script src="/js/office-rules.js"></script>
|
||||
<script src="/js/team-rules.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user