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:
@@ -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