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
415 lines
15 KiB
JavaScript
415 lines
15 KiB
JavaScript
/**
|
|
* Team Calendar Page
|
|
* Shows presence and parking for all team members
|
|
* Filtered by manager (manager-centric model)
|
|
*/
|
|
|
|
let currentUser = null;
|
|
let currentStartDate = null;
|
|
let viewMode = 'week'; // 'week' or 'month'
|
|
let managers = [];
|
|
let teamData = [];
|
|
let parkingDataLookup = {};
|
|
let parkingAssignmentLookup = {};
|
|
let selectedUserId = null;
|
|
let selectedDate = null;
|
|
let currentAssignmentId = null;
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
currentUser = await api.requireAuth();
|
|
if (!currentUser) return;
|
|
|
|
// Initialize start date based on week start preference
|
|
const weekStartDay = currentUser.week_start_day || 0;
|
|
currentStartDate = utils.getWeekStart(new Date(), weekStartDay);
|
|
|
|
await loadManagers();
|
|
await loadTeamData();
|
|
renderCalendar();
|
|
setupEventListeners();
|
|
});
|
|
|
|
async function loadManagers() {
|
|
const response = await api.get('/api/managers');
|
|
if (response && response.ok) {
|
|
managers = await response.json();
|
|
const select = document.getElementById('managerFilter');
|
|
|
|
// Filter managers based on user role
|
|
let filteredManagers = managers;
|
|
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 userCount = manager.managed_user_count || 0;
|
|
option.textContent = `${manager.name} (${userCount} users)`;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
// 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';
|
|
}
|
|
}
|
|
}
|
|
|
|
function getDateRange() {
|
|
let startDate = new Date(currentStartDate);
|
|
let endDate = new Date(currentStartDate);
|
|
|
|
if (viewMode === 'week') {
|
|
endDate.setDate(endDate.getDate() + 6);
|
|
} else {
|
|
// Month view - start from first day of month
|
|
startDate = new Date(currentStartDate.getFullYear(), currentStartDate.getMonth(), 1);
|
|
endDate = new Date(currentStartDate.getFullYear(), currentStartDate.getMonth() + 1, 0);
|
|
}
|
|
|
|
return { startDate, endDate };
|
|
}
|
|
|
|
async function loadTeamData() {
|
|
const { startDate, endDate } = getDateRange();
|
|
const startStr = utils.formatDate(startDate);
|
|
const endStr = utils.formatDate(endDate);
|
|
|
|
let url = `/api/presence/team?start_date=${startStr}&end_date=${endStr}`;
|
|
|
|
const managerFilter = document.getElementById('managerFilter').value;
|
|
if (managerFilter) {
|
|
url += `&manager_id=${managerFilter}`;
|
|
}
|
|
|
|
const response = await api.get(url);
|
|
if (response && response.ok) {
|
|
teamData = await response.json();
|
|
// Build parking lookup with spot names and assignment IDs
|
|
parkingDataLookup = {};
|
|
parkingAssignmentLookup = {};
|
|
teamData.forEach(member => {
|
|
if (member.parking_info) {
|
|
member.parking_info.forEach(p => {
|
|
const key = `${member.id}_${p.date}`;
|
|
parkingDataLookup[key] = p.spot_display_name || p.spot_id;
|
|
parkingAssignmentLookup[key] = p.id;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function renderCalendar() {
|
|
const header = document.getElementById('calendarHeader');
|
|
const body = document.getElementById('calendarBody');
|
|
const { startDate, endDate } = getDateRange();
|
|
|
|
// Update header text
|
|
if (viewMode === 'week') {
|
|
document.getElementById('currentWeek').textContent =
|
|
`${utils.formatDateShort(startDate)} - ${utils.formatDateShort(endDate)}`;
|
|
} else {
|
|
document.getElementById('currentWeek').textContent =
|
|
`${utils.getMonthName(startDate.getMonth())} ${startDate.getFullYear()}`;
|
|
}
|
|
|
|
// Calculate number of days
|
|
const dayCount = Math.round((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1;
|
|
|
|
// Build header row
|
|
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
let headerHtml = '<th>Name</th><th>Manager</th>';
|
|
|
|
for (let i = 0; i < dayCount; i++) {
|
|
const date = new Date(startDate);
|
|
date.setDate(date.getDate() + i);
|
|
const dayOfWeek = date.getDay();
|
|
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
const isHoliday = utils.isItalianHoliday(date);
|
|
const isToday = date.toDateString() === new Date().toDateString();
|
|
|
|
let classes = [];
|
|
if (isWeekend) classes.push('weekend');
|
|
if (isHoliday) classes.push('holiday');
|
|
if (isToday) classes.push('today');
|
|
|
|
headerHtml += `<th class="${classes.join(' ')}">
|
|
<div>${dayNames[dayOfWeek].charAt(0)}</div>
|
|
<div class="day-number">${date.getDate()}</div>
|
|
</th>`;
|
|
}
|
|
header.innerHTML = headerHtml;
|
|
|
|
// Build body rows
|
|
if (teamData.length === 0) {
|
|
body.innerHTML = `<tr><td colspan="${dayCount + 2}" class="text-center">No team members found</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
let bodyHtml = '';
|
|
teamData.forEach(member => {
|
|
bodyHtml += `<tr>
|
|
<td class="member-name">${member.name || 'Unknown'}</td>
|
|
<td class="member-manager">${member.manager_name || '-'}</td>`;
|
|
|
|
for (let i = 0; i < dayCount; i++) {
|
|
const date = new Date(startDate);
|
|
date.setDate(date.getDate() + i);
|
|
const dateStr = utils.formatDate(date);
|
|
const dayOfWeek = date.getDay();
|
|
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
const isHoliday = utils.isItalianHoliday(date);
|
|
|
|
const presence = member.presences.find(p => p.date === dateStr);
|
|
const parkingKey = `${member.id}_${dateStr}`;
|
|
const parkingSpot = parkingDataLookup[parkingKey];
|
|
const hasParking = member.parking_dates && member.parking_dates.includes(dateStr);
|
|
const isToday = date.toDateString() === new Date().toDateString();
|
|
|
|
let cellClasses = ['calendar-cell'];
|
|
if (isWeekend) cellClasses.push('weekend');
|
|
if (isHoliday) cellClasses.push('holiday');
|
|
if (isToday) cellClasses.push('today');
|
|
if (presence) cellClasses.push(`status-${presence.status}`);
|
|
|
|
// Show parking badge instead of just 'P'
|
|
let parkingBadge = '';
|
|
if (hasParking) {
|
|
const spotName = parkingSpot || 'P';
|
|
parkingBadge = `<span class="parking-badge-sm">${spotName}</span>`;
|
|
}
|
|
|
|
bodyHtml += `<td class="${cellClasses.join(' ')}" data-user-id="${member.id}" data-date="${dateStr}" data-user-name="${member.name || ''}">${parkingBadge}</td>`;
|
|
}
|
|
|
|
bodyHtml += '</tr>';
|
|
});
|
|
body.innerHTML = bodyHtml;
|
|
|
|
// 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) {
|
|
selectedUserId = userId;
|
|
selectedDate = dateStr;
|
|
|
|
const modal = document.getElementById('dayModal');
|
|
document.getElementById('dayModalTitle').textContent = utils.formatDateDisplay(dateStr);
|
|
document.getElementById('dayModalUser').textContent = userName;
|
|
|
|
// Find current status and parking
|
|
const member = teamData.find(m => m.id === userId);
|
|
const presence = member?.presences.find(p => p.date === dateStr);
|
|
const parkingKey = `${userId}_${dateStr}`;
|
|
const parkingSpot = parkingDataLookup[parkingKey];
|
|
const assignmentId = parkingAssignmentLookup[parkingKey];
|
|
|
|
// Highlight current status
|
|
document.querySelectorAll('#dayModal .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');
|
|
|
|
if (parkingSpot) {
|
|
parkingSection.style.display = 'block';
|
|
parkingInfo.innerHTML = `<strong>Parking:</strong> Spot ${parkingSpot}`;
|
|
currentAssignmentId = assignmentId;
|
|
} else {
|
|
parkingSection.style.display = 'none';
|
|
currentAssignmentId = null;
|
|
}
|
|
|
|
modal.style.display = 'flex';
|
|
}
|
|
|
|
async function markPresence(status) {
|
|
if (!selectedUserId || !selectedDate) return;
|
|
|
|
const response = await api.post('/api/presence/admin/mark', {
|
|
user_id: selectedUserId,
|
|
date: selectedDate,
|
|
status: status
|
|
});
|
|
|
|
if (response && response.ok) {
|
|
document.getElementById('dayModal').style.display = 'none';
|
|
await loadTeamData();
|
|
renderCalendar();
|
|
} else {
|
|
const error = await response.json();
|
|
alert(error.detail || 'Failed to mark presence');
|
|
}
|
|
}
|
|
|
|
async function clearPresence() {
|
|
if (!selectedUserId || !selectedDate) return;
|
|
if (!confirm('Clear presence for this date?')) return;
|
|
|
|
const response = await api.delete(`/api/presence/admin/${selectedUserId}/${selectedDate}`);
|
|
if (response && response.ok) {
|
|
document.getElementById('dayModal').style.display = 'none';
|
|
await loadTeamData();
|
|
renderCalendar();
|
|
}
|
|
}
|
|
|
|
async function openReassignModal() {
|
|
if (!currentAssignmentId) return;
|
|
|
|
// Load eligible users
|
|
const response = await api.get(`/api/parking/eligible-users/${currentAssignmentId}`);
|
|
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
|
|
const parkingKey = `${selectedUserId}_${selectedDate}`;
|
|
const spotName = parkingDataLookup[parkingKey] || 'Unknown';
|
|
document.getElementById('reassignSpotInfo').textContent = `Spot ${spotName}`;
|
|
|
|
document.getElementById('dayModal').style.display = 'none';
|
|
document.getElementById('reassignModal').style.display = 'flex';
|
|
}
|
|
|
|
async function confirmReassign() {
|
|
const newUserId = document.getElementById('reassignUser').value;
|
|
|
|
if (!currentAssignmentId || !newUserId) {
|
|
alert('Please select a user');
|
|
return;
|
|
}
|
|
|
|
const response = await api.post('/api/parking/reassign-spot', {
|
|
assignment_id: currentAssignmentId,
|
|
new_user_id: newUserId
|
|
});
|
|
|
|
if (response && response.ok) {
|
|
await loadTeamData();
|
|
renderCalendar();
|
|
document.getElementById('reassignModal').style.display = 'none';
|
|
} else {
|
|
const error = await response.json();
|
|
alert(error.detail || 'Failed to reassign parking spot');
|
|
}
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Navigation (prev/next)
|
|
document.getElementById('prevWeek').addEventListener('click', async () => {
|
|
if (viewMode === 'week') {
|
|
currentStartDate.setDate(currentStartDate.getDate() - 7);
|
|
} else {
|
|
currentStartDate.setMonth(currentStartDate.getMonth() - 1);
|
|
}
|
|
await loadTeamData();
|
|
renderCalendar();
|
|
});
|
|
|
|
document.getElementById('nextWeek').addEventListener('click', async () => {
|
|
if (viewMode === 'week') {
|
|
currentStartDate.setDate(currentStartDate.getDate() + 7);
|
|
} else {
|
|
currentStartDate.setMonth(currentStartDate.getMonth() + 1);
|
|
}
|
|
await loadTeamData();
|
|
renderCalendar();
|
|
});
|
|
|
|
// View toggle (week/month)
|
|
document.getElementById('viewToggle').addEventListener('change', async (e) => {
|
|
viewMode = e.target.value;
|
|
if (viewMode === 'month') {
|
|
// Set to first day of current month
|
|
currentStartDate = new Date(currentStartDate.getFullYear(), currentStartDate.getMonth(), 1);
|
|
} else {
|
|
// Set to current week start
|
|
const weekStartDay = currentUser.week_start_day || 0;
|
|
currentStartDate = utils.getWeekStart(new Date(), weekStartDay);
|
|
}
|
|
await loadTeamData();
|
|
renderCalendar();
|
|
});
|
|
|
|
// Manager filter
|
|
document.getElementById('managerFilter').addEventListener('change', async () => {
|
|
await loadTeamData();
|
|
renderCalendar();
|
|
});
|
|
|
|
// Day modal
|
|
document.getElementById('closeDayModal').addEventListener('click', () => {
|
|
document.getElementById('dayModal').style.display = 'none';
|
|
});
|
|
|
|
document.querySelectorAll('#dayModal .status-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => markPresence(btn.dataset.status));
|
|
});
|
|
|
|
document.getElementById('clearDayBtn').addEventListener('click', clearPresence);
|
|
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');
|
|
}
|