Files
org-parking/frontend/js/presence.js
Stefano Manfredi 7168fa4b72 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
2025-12-02 13:30:04 +00:00

369 lines
13 KiB
JavaScript

/**
* 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 () => {
currentUser = await api.requireAuth();
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');
}
});
}