Initial commit: Parking Manager
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
This commit is contained in:
370
frontend/js/presence.js
Normal file
370
frontend/js/presence.js
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* My Presence Page
|
||||
* Personal calendar for marking daily presence
|
||||
*/
|
||||
|
||||
let currentUser = null;
|
||||
let currentDate = new Date();
|
||||
let presenceData = {};
|
||||
let parkingData = {};
|
||||
let currentAssignmentId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!api.requireAuth()) return;
|
||||
|
||||
currentUser = await api.getCurrentUser();
|
||||
if (!currentUser) return;
|
||||
|
||||
await Promise.all([loadPresences(), loadParkingAssignments()]);
|
||||
renderCalendar();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
async function loadPresences() {
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
|
||||
const startDate = utils.formatDate(firstDay);
|
||||
const endDate = utils.formatDate(lastDay);
|
||||
|
||||
const response = await api.get(`/api/presence/my-presences?start_date=${startDate}&end_date=${endDate}`);
|
||||
if (response && response.ok) {
|
||||
const presences = await response.json();
|
||||
presenceData = {};
|
||||
presences.forEach(p => {
|
||||
presenceData[p.date] = p;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function loadParkingAssignments() {
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
|
||||
const startDate = utils.formatDate(firstDay);
|
||||
const endDate = utils.formatDate(lastDay);
|
||||
|
||||
const response = await api.get(`/api/parking/my-assignments?start_date=${startDate}&end_date=${endDate}`);
|
||||
if (response && response.ok) {
|
||||
const assignments = await response.json();
|
||||
parkingData = {};
|
||||
assignments.forEach(a => {
|
||||
parkingData[a.date] = a;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderCalendar() {
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
const weekStartDay = currentUser.week_start_day || 0; // 0=Sunday, 1=Monday
|
||||
|
||||
// Update month header
|
||||
document.getElementById('currentMonth').textContent = `${utils.getMonthName(month)} ${year}`;
|
||||
|
||||
// Get calendar info
|
||||
const daysInMonth = utils.getDaysInMonth(year, month);
|
||||
const firstDayOfMonth = new Date(year, month, 1).getDay(); // 0=Sunday
|
||||
const today = new Date();
|
||||
|
||||
// Calculate offset based on week start preference
|
||||
let firstDayOffset = firstDayOfMonth - weekStartDay;
|
||||
if (firstDayOffset < 0) firstDayOffset += 7;
|
||||
|
||||
// Build calendar grid
|
||||
const grid = document.getElementById('calendarGrid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
// Day headers - reorder based on week start day
|
||||
const allDayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const dayNames = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
dayNames.push(allDayNames[(weekStartDay + i) % 7]);
|
||||
}
|
||||
dayNames.forEach(name => {
|
||||
const header = document.createElement('div');
|
||||
header.className = 'calendar-day';
|
||||
header.style.cursor = 'default';
|
||||
header.style.fontWeight = '600';
|
||||
header.style.fontSize = '0.75rem';
|
||||
header.textContent = name;
|
||||
grid.appendChild(header);
|
||||
});
|
||||
|
||||
// Empty cells before first day
|
||||
for (let i = 0; i < firstDayOffset; i++) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'calendar-day';
|
||||
empty.style.visibility = 'hidden';
|
||||
grid.appendChild(empty);
|
||||
}
|
||||
|
||||
// Day cells
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const date = new Date(year, month, day);
|
||||
const dateStr = utils.formatDate(date);
|
||||
const dayOfWeek = date.getDay();
|
||||
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
||||
const isHoliday = utils.isItalianHoliday(date);
|
||||
const isToday = date.toDateString() === today.toDateString();
|
||||
const presence = presenceData[dateStr];
|
||||
const parking = parkingData[dateStr];
|
||||
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'calendar-day';
|
||||
cell.dataset.date = dateStr;
|
||||
|
||||
if (isWeekend) cell.classList.add('weekend');
|
||||
if (isHoliday) cell.classList.add('holiday');
|
||||
if (isToday) cell.classList.add('today');
|
||||
|
||||
if (presence) {
|
||||
cell.classList.add(`status-${presence.status}`);
|
||||
}
|
||||
|
||||
// Show parking badge if assigned
|
||||
const parkingBadge = parking
|
||||
? `<span class="parking-badge">${parking.spot_display_name || parking.spot_id}</span>`
|
||||
: '';
|
||||
|
||||
cell.innerHTML = `
|
||||
<div class="day-number">${day}</div>
|
||||
${parkingBadge}
|
||||
`;
|
||||
|
||||
cell.addEventListener('click', () => openDayModal(dateStr, presence, parking));
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
function openDayModal(dateStr, presence, parking) {
|
||||
const modal = document.getElementById('dayModal');
|
||||
const title = document.getElementById('dayModalTitle');
|
||||
|
||||
title.textContent = utils.formatDateDisplay(dateStr);
|
||||
|
||||
// Highlight current status
|
||||
document.querySelectorAll('.status-btn').forEach(btn => {
|
||||
const status = btn.dataset.status;
|
||||
if (presence && presence.status === status) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update parking section
|
||||
const parkingSection = document.getElementById('parkingSection');
|
||||
const parkingInfo = document.getElementById('parkingInfo');
|
||||
const releaseBtn = document.getElementById('releaseParkingBtn');
|
||||
|
||||
if (parking) {
|
||||
parkingSection.style.display = 'block';
|
||||
const spotName = parking.spot_display_name || parking.spot_id;
|
||||
parkingInfo.innerHTML = `<strong>Parking:</strong> Spot ${spotName}`;
|
||||
releaseBtn.dataset.assignmentId = parking.id;
|
||||
document.getElementById('reassignParkingBtn').dataset.assignmentId = parking.id;
|
||||
currentAssignmentId = parking.id;
|
||||
} else {
|
||||
parkingSection.style.display = 'none';
|
||||
}
|
||||
|
||||
modal.dataset.date = dateStr;
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
async function markPresence(status) {
|
||||
const modal = document.getElementById('dayModal');
|
||||
const date = modal.dataset.date;
|
||||
|
||||
const response = await api.post('/api/presence/mark', { date, status });
|
||||
if (response && response.ok) {
|
||||
await Promise.all([loadPresences(), loadParkingAssignments()]);
|
||||
renderCalendar();
|
||||
modal.style.display = 'none';
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.detail || 'Failed to mark presence');
|
||||
}
|
||||
}
|
||||
|
||||
async function clearPresence() {
|
||||
const modal = document.getElementById('dayModal');
|
||||
const date = modal.dataset.date;
|
||||
|
||||
if (!confirm('Clear presence for this date?')) return;
|
||||
|
||||
const response = await api.delete(`/api/presence/${date}`);
|
||||
if (response && response.ok) {
|
||||
await Promise.all([loadPresences(), loadParkingAssignments()]);
|
||||
renderCalendar();
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function releaseParking() {
|
||||
const modal = document.getElementById('dayModal');
|
||||
const releaseBtn = document.getElementById('releaseParkingBtn');
|
||||
const assignmentId = releaseBtn.dataset.assignmentId;
|
||||
|
||||
if (!assignmentId) return;
|
||||
if (!confirm('Release your parking spot for this date?')) return;
|
||||
|
||||
const response = await api.post(`/api/parking/release-my-spot/${assignmentId}`);
|
||||
|
||||
if (response && response.ok) {
|
||||
await loadParkingAssignments();
|
||||
renderCalendar();
|
||||
modal.style.display = 'none';
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.detail || 'Failed to release parking spot');
|
||||
}
|
||||
}
|
||||
|
||||
async function openReassignModal() {
|
||||
const assignmentId = currentAssignmentId;
|
||||
if (!assignmentId) return;
|
||||
|
||||
// Load eligible users
|
||||
const response = await api.get(`/api/parking/eligible-users/${assignmentId}`);
|
||||
if (!response || !response.ok) {
|
||||
const error = await response.json();
|
||||
alert(error.detail || 'Failed to load eligible users');
|
||||
return;
|
||||
}
|
||||
|
||||
const users = await response.json();
|
||||
const select = document.getElementById('reassignUser');
|
||||
select.innerHTML = '<option value="">Select user...</option>';
|
||||
|
||||
if (users.length === 0) {
|
||||
select.innerHTML = '<option value="">No eligible users available</option>';
|
||||
} else {
|
||||
users.forEach(user => {
|
||||
const option = document.createElement('option');
|
||||
option.value = user.id;
|
||||
option.textContent = user.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Get spot info from parking data
|
||||
const parking = Object.values(parkingData).find(p => p.id === assignmentId);
|
||||
if (parking) {
|
||||
const spotName = parking.spot_display_name || parking.spot_id;
|
||||
document.getElementById('reassignSpotInfo').textContent = `Spot ${spotName}`;
|
||||
}
|
||||
|
||||
document.getElementById('dayModal').style.display = 'none';
|
||||
document.getElementById('reassignModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function confirmReassign() {
|
||||
const assignmentId = currentAssignmentId;
|
||||
const newUserId = document.getElementById('reassignUser').value;
|
||||
|
||||
if (!assignmentId || !newUserId) {
|
||||
alert('Please select a user');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await api.post('/api/parking/reassign-spot', {
|
||||
assignment_id: assignmentId,
|
||||
new_user_id: newUserId
|
||||
});
|
||||
|
||||
if (response && response.ok) {
|
||||
await loadParkingAssignments();
|
||||
renderCalendar();
|
||||
document.getElementById('reassignModal').style.display = 'none';
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.detail || 'Failed to reassign parking spot');
|
||||
}
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
// Month navigation
|
||||
document.getElementById('prevMonth').addEventListener('click', async () => {
|
||||
currentDate.setMonth(currentDate.getMonth() - 1);
|
||||
await Promise.all([loadPresences(), loadParkingAssignments()]);
|
||||
renderCalendar();
|
||||
});
|
||||
|
||||
document.getElementById('nextMonth').addEventListener('click', async () => {
|
||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||
await Promise.all([loadPresences(), loadParkingAssignments()]);
|
||||
renderCalendar();
|
||||
});
|
||||
|
||||
// Day modal
|
||||
document.getElementById('closeDayModal').addEventListener('click', () => {
|
||||
document.getElementById('dayModal').style.display = 'none';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.status-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => markPresence(btn.dataset.status));
|
||||
});
|
||||
|
||||
document.getElementById('clearDayBtn').addEventListener('click', clearPresence);
|
||||
document.getElementById('releaseParkingBtn').addEventListener('click', releaseParking);
|
||||
document.getElementById('reassignParkingBtn').addEventListener('click', openReassignModal);
|
||||
|
||||
utils.setupModalClose('dayModal');
|
||||
|
||||
// Reassign modal
|
||||
document.getElementById('closeReassignModal').addEventListener('click', () => {
|
||||
document.getElementById('reassignModal').style.display = 'none';
|
||||
});
|
||||
document.getElementById('cancelReassign').addEventListener('click', () => {
|
||||
document.getElementById('reassignModal').style.display = 'none';
|
||||
});
|
||||
document.getElementById('confirmReassign').addEventListener('click', confirmReassign);
|
||||
utils.setupModalClose('reassignModal');
|
||||
|
||||
// Bulk mark
|
||||
document.getElementById('bulkMarkBtn').addEventListener('click', () => {
|
||||
document.getElementById('bulkMarkModal').style.display = 'flex';
|
||||
});
|
||||
|
||||
document.getElementById('closeBulkModal').addEventListener('click', () => {
|
||||
document.getElementById('bulkMarkModal').style.display = 'none';
|
||||
});
|
||||
|
||||
document.getElementById('cancelBulk').addEventListener('click', () => {
|
||||
document.getElementById('bulkMarkModal').style.display = 'none';
|
||||
});
|
||||
|
||||
utils.setupModalClose('bulkMarkModal');
|
||||
|
||||
document.getElementById('bulkMarkForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const startDate = document.getElementById('startDate').value;
|
||||
const endDate = document.getElementById('endDate').value;
|
||||
const status = document.getElementById('bulkStatus').value;
|
||||
const weekdaysOnly = document.getElementById('weekdaysOnly').checked;
|
||||
|
||||
const data = { start_date: startDate, end_date: endDate, status };
|
||||
if (weekdaysOnly) {
|
||||
data.days = [1, 2, 3, 4, 5]; // Mon-Fri (JS weekday)
|
||||
}
|
||||
|
||||
const response = await api.post('/api/presence/mark-bulk', data);
|
||||
if (response && response.ok) {
|
||||
const results = await response.json();
|
||||
alert(`Marked ${results.length} dates`);
|
||||
document.getElementById('bulkMarkModal').style.display = 'none';
|
||||
await Promise.all([loadPresences(), loadParkingAssignments()]);
|
||||
renderCalendar();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.detail || 'Failed to bulk mark');
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user