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
189 lines
7.7 KiB
HTML
189 lines
7.7 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Profile - Parking Manager</title>
|
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
<link rel="stylesheet" href="/css/styles.css">
|
|
</head>
|
|
<body>
|
|
<aside class="sidebar">
|
|
<div class="sidebar-header">
|
|
<h1>Parking Manager</h1>
|
|
</div>
|
|
<nav class="sidebar-nav"></nav>
|
|
<div class="sidebar-footer">
|
|
<div class="user-menu">
|
|
<button class="user-button" id="userMenuButton">
|
|
<div class="user-avatar">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
|
<circle cx="12" cy="7" r="4"></circle>
|
|
</svg>
|
|
</div>
|
|
<div class="user-info">
|
|
<div class="user-name" id="userName">Loading...</div>
|
|
<div class="user-role" id="userRole">-</div>
|
|
</div>
|
|
</button>
|
|
<div class="user-dropdown" id="userDropdown" style="display: none;">
|
|
<a href="/profile" class="dropdown-item">Profile</a>
|
|
<a href="/settings" class="dropdown-item">Settings</a>
|
|
<hr class="dropdown-divider">
|
|
<button class="dropdown-item" id="logoutButton">Logout</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="main-content">
|
|
<header class="page-header">
|
|
<h2>Profile</h2>
|
|
</header>
|
|
|
|
<div class="content-wrapper">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>Personal Information</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<form id="profileForm">
|
|
<div class="form-group">
|
|
<label for="name">Full Name</label>
|
|
<input type="text" id="name" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="email">Email</label>
|
|
<input type="email" id="email" disabled>
|
|
<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>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-dark">Save Changes</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>Change Password</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<form id="passwordForm">
|
|
<div class="form-group">
|
|
<label for="currentPassword">Current Password</label>
|
|
<input type="password" id="currentPassword" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newPassword">New Password</label>
|
|
<input type="password" id="newPassword" required minlength="8">
|
|
<small>Minimum 8 characters</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="confirmPassword">Confirm New Password</label>
|
|
<input type="password" id="confirmPassword" required>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-dark">Change Password</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script src="/js/api.js"></script>
|
|
<script src="/js/utils.js"></script>
|
|
<script src="/js/nav.js"></script>
|
|
<script>
|
|
let currentUser = null;
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
if (!api.requireAuth()) return;
|
|
|
|
currentUser = await api.getCurrentUser();
|
|
if (!currentUser) return;
|
|
|
|
await loadOffices();
|
|
populateForm();
|
|
setupEventListeners();
|
|
});
|
|
|
|
async function loadOffices() {
|
|
const response = await api.get('/api/offices');
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
|
|
function populateForm() {
|
|
document.getElementById('name').value = currentUser.name || '';
|
|
document.getElementById('email').value = currentUser.email;
|
|
document.getElementById('office').value = currentUser.office_id || '';
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Profile form
|
|
document.getElementById('profileForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const data = {
|
|
name: document.getElementById('name').value,
|
|
office_id: document.getElementById('office').value || null
|
|
};
|
|
|
|
const response = await api.put('/api/users/me/profile', data);
|
|
if (response && response.ok) {
|
|
utils.showMessage('Profile updated successfully', 'success');
|
|
currentUser = await api.getCurrentUser();
|
|
} else {
|
|
const error = await response.json();
|
|
utils.showMessage(error.detail || 'Failed to update profile', 'error');
|
|
}
|
|
});
|
|
|
|
// Password form
|
|
document.getElementById('passwordForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const newPassword = document.getElementById('newPassword').value;
|
|
const confirmPassword = document.getElementById('confirmPassword').value;
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
utils.showMessage('Passwords do not match', 'error');
|
|
return;
|
|
}
|
|
|
|
const data = {
|
|
current_password: document.getElementById('currentPassword').value,
|
|
new_password: newPassword
|
|
};
|
|
|
|
const response = await api.post('/api/users/me/change-password', data);
|
|
if (response && response.ok) {
|
|
utils.showMessage('Password changed successfully', 'success');
|
|
document.getElementById('passwordForm').reset();
|
|
} else {
|
|
const error = await response.json();
|
|
utils.showMessage(error.detail || 'Failed to change password', 'error');
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|