let currentUser = null; let allShifts = []; let mySignups = []; // Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', async () => { await checkAuth(); await loadShifts(); await loadMySignups(); setupEventListeners(); // Add clear filters button handler const clearBtn = document.getElementById('clear-filters-btn'); if (clearBtn) { clearBtn.addEventListener('click', clearFilters); } }); async function checkAuth() { try { const response = await fetch('/api/auth/check'); const data = await response.json(); if (!data.authenticated) { window.location.href = '/login.html'; return; } currentUser = data.user; document.getElementById('user-email').textContent = currentUser.email; // Add admin link if user is admin if (currentUser.isAdmin) { const headerActions = document.querySelector('.header-actions'); const adminLink = document.createElement('a'); adminLink.href = '/admin.html#shifts'; adminLink.className = 'btn btn-secondary'; adminLink.textContent = '⚙️ Manage Shifts'; headerActions.insertBefore(adminLink, headerActions.firstChild); } } catch (error) { console.error('Auth check failed:', error); window.location.href = '/login.html'; } } async function loadShifts() { try { const response = await fetch('/api/shifts'); const data = await response.json(); if (data.success) { allShifts = data.shifts; displayShifts(allShifts); } } catch (error) { showStatus('Failed to load shifts', 'error'); } } async function loadMySignups() { try { const response = await fetch('/api/shifts/my-signups'); const data = await response.json(); if (data.success) { mySignups = data.signups; displayMySignups(); } else { // Still display empty signups if the endpoint fails mySignups = []; displayMySignups(); } } catch (error) { console.error('Failed to load signups:', error); // Don't show error to user, just display empty signups mySignups = []; displayMySignups(); } } function displayShifts(shifts) { const grid = document.getElementById('shifts-grid'); if (shifts.length === 0) { grid.innerHTML = '

No shifts available at this time.

'; return; } grid.innerHTML = shifts.map(shift => { const shiftDate = new Date(shift.Date); const isSignedUp = mySignups.some(s => s.shift_id === shift.ID); const isFull = shift['Current Volunteers'] >= shift['Max Volunteers']; return `

${escapeHtml(shift.Title)}

📅 ${shiftDate.toLocaleDateString()}

⏰ ${shift['Start Time']} - ${shift['End Time']}

📍 ${escapeHtml(shift.Location || 'TBD')}

👥 ${shift['Current Volunteers']}/${shift['Max Volunteers']} volunteers

${shift.Description ? `
${escapeHtml(shift.Description)}
` : ''}
${isSignedUp ? ` ${generateCalendarDropdown(shift)}` : isFull ? '' : `` }
`; }).join(''); // Add event listeners after rendering setupShiftCardListeners(); } function displayMySignups() { const list = document.getElementById('my-signups-list'); if (mySignups.length === 0) { list.innerHTML = '

You haven\'t signed up for any shifts yet.

'; return; } // Need to match signups with shift details const signupsWithDetails = mySignups.map(signup => { const shift = allShifts.find(s => s.ID === signup.shift_id); return { ...signup, shift }; }).filter(s => s.shift); list.innerHTML = signupsWithDetails.map(signup => { const shiftDate = new Date(signup.shift.Date); return `

${escapeHtml(signup.shift.Title)}

📅 ${shiftDate.toLocaleDateString()} ⏰ ${signup.shift['Start Time']} - ${signup.shift['End Time']}

${generateCalendarDropdown(signup.shift)}
`; }).join(''); // Add event listeners after rendering setupMySignupsListeners(); } // New function to setup listeners for shift cards function setupShiftCardListeners() { const grid = document.getElementById('shifts-grid'); if (!grid) return; // Remove any existing listeners by cloning const newGrid = grid.cloneNode(true); grid.parentNode.replaceChild(newGrid, grid); // Add click listener for all buttons newGrid.addEventListener('click', async (e) => { // Handle signup buttons if (e.target.classList.contains('signup-btn')) { const shiftId = e.target.getAttribute('data-shift-id'); await signupForShift(shiftId); } // Handle cancel buttons else if (e.target.classList.contains('cancel-signup-btn')) { const shiftId = e.target.getAttribute('data-shift-id'); await cancelSignup(shiftId); } // Handle calendar toggle buttons else if (e.target.classList.contains('calendar-toggle')) { e.stopPropagation(); const dropdown = e.target.closest('.calendar-dropdown'); const options = dropdown.querySelector('.calendar-options'); const isOpen = options.style.display !== 'none'; // Close all other dropdowns document.querySelectorAll('.calendar-options').forEach(opt => { opt.style.display = 'none'; }); // Toggle this dropdown options.style.display = isOpen ? 'none' : 'block'; } // Handle calendar option clicks else if (e.target.closest('.calendar-option')) { e.stopPropagation(); const dropdown = e.target.closest('.calendar-dropdown'); const options = dropdown.querySelector('.calendar-options'); options.style.display = 'none'; } }); } // New function to setup listeners for my signups function setupMySignupsListeners() { const list = document.getElementById('my-signups-list'); if (!list) return; // Remove any existing listeners by cloning const newList = list.cloneNode(true); list.parentNode.replaceChild(newList, list); // Add click listener for all interactions newList.addEventListener('click', async (e) => { // Handle cancel buttons if (e.target.classList.contains('cancel-signup-btn')) { const shiftId = e.target.getAttribute('data-shift-id'); await cancelSignup(shiftId); } // Handle calendar toggle buttons else if (e.target.classList.contains('calendar-toggle')) { e.stopPropagation(); const dropdown = e.target.closest('.calendar-dropdown'); const options = dropdown.querySelector('.calendar-options'); const isOpen = options.style.display !== 'none'; // Close all other dropdowns document.querySelectorAll('.calendar-options').forEach(opt => { opt.style.display = 'none'; }); // Toggle this dropdown options.style.display = isOpen ? 'none' : 'block'; } // Handle calendar option clicks else if (e.target.closest('.calendar-option')) { e.stopPropagation(); const dropdown = e.target.closest('.calendar-dropdown'); const options = dropdown.querySelector('.calendar-options'); options.style.display = 'none'; } }); } async function signupForShift(shiftId) { try { const response = await fetch(`/api/shifts/${shiftId}/signup`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { showStatus('Successfully signed up for shift!', 'success'); await loadShifts(); await loadMySignups(); } else { showStatus(data.error || 'Failed to sign up', 'error'); } } catch (error) { console.error('Error signing up:', error); showStatus('Failed to sign up for shift', 'error'); } } async function cancelSignup(shiftId) { if (!confirm('Are you sure you want to cancel your signup for this shift?')) { return; } try { const response = await fetch(`/api/shifts/${shiftId}/cancel`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { showStatus('Signup cancelled', 'success'); await loadShifts(); await loadMySignups(); } else { showStatus(data.error || 'Failed to cancel signup', 'error'); } } catch (error) { console.error('Error cancelling signup:', error); showStatus('Failed to cancel signup', 'error'); } } function setupEventListeners() { const dateFilter = document.getElementById('date-filter'); if (dateFilter) { dateFilter.addEventListener('change', filterShifts); } } function filterShifts() { const dateFilter = document.getElementById('date-filter').value; if (!dateFilter) { displayShifts(allShifts); return; } const filtered = allShifts.filter(shift => { return shift.Date === dateFilter; // Changed from shift.date to shift.Date }); displayShifts(filtered); } function clearFilters() { document.getElementById('date-filter').value = ''; loadShifts(); // Reload shifts without filters } function showStatus(message, type = 'info') { const container = document.getElementById('status-container'); if (!container) return; const messageDiv = document.createElement('div'); messageDiv.className = `status-message ${type}`; messageDiv.textContent = message; container.appendChild(messageDiv); setTimeout(() => { messageDiv.remove(); }, 5000); } function escapeHtml(text) { if (text === null || text === undefined) { return ''; } const div = document.createElement('div'); div.textContent = String(text); return div.innerHTML; } // Add these calendar URL generation functions after the existing functions function generateCalendarUrls(shift) { const shiftDate = new Date(shift.Date); // Parse start and end times const [startHour, startMinute] = shift['Start Time'].split(':').map(n => parseInt(n)); const [endHour, endMinute] = shift['End Time'].split(':').map(n => parseInt(n)); // Create start and end datetime objects const startDate = new Date(shiftDate); startDate.setHours(startHour, startMinute, 0, 0); const endDate = new Date(shiftDate); endDate.setHours(endHour, endMinute, 0, 0); // Format dates for different calendar formats const formatGoogleDate = (date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}${month}${day}T${hours}${minutes}00`; }; const formatISODate = (date) => { return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); }; // Event details const title = shift.Title; const description = shift.Description || 'Volunteer shift'; const location = shift.Location || ''; // Google Calendar URL const googleStartStr = formatGoogleDate(startDate); const googleEndStr = formatGoogleDate(endDate); const googleParams = new URLSearchParams({ action: 'TEMPLATE', text: title, dates: `${googleStartStr}/${googleEndStr}`, details: description, location: location }); const googleUrl = `https://calendar.google.com/calendar/render?${googleParams.toString()}`; // Outlook Web Calendar URL const outlookStartStr = startDate.toISOString(); const outlookEndStr = endDate.toISOString(); const outlookParams = new URLSearchParams({ path: '/calendar/action/compose', rru: 'addevent', subject: title, startdt: outlookStartStr, enddt: outlookEndStr, body: description, location: location }); const outlookUrl = `https://outlook.live.com/calendar/0/deeplink/compose?${outlookParams.toString()}`; // Apple Calendar (.ics file) - we'll generate this dynamically const icsContent = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//BNKops//Volunteer Shifts//EN', 'BEGIN:VEVENT', `UID:${shift.ID}-${Date.now()}@bnkops.com`, `DTSTART:${formatISODate(startDate)}`, `DTEND:${formatISODate(endDate)}`, `SUMMARY:${title}`, `DESCRIPTION:${description.replace(/\n/g, '\\n')}`, `LOCATION:${location}`, 'STATUS:CONFIRMED', 'END:VEVENT', 'END:VCALENDAR' ].join('\r\n'); // Create a data URL for the .ics file const icsDataUrl = 'data:text/calendar;charset=utf-8,' + encodeURIComponent(icsContent); return { google: googleUrl, outlook: outlookUrl, apple: icsDataUrl, icsFilename: `${title.replace(/[^a-z0-9]/gi, '_')}_${shift.ID}.ics` }; } // Update calendar dropdown HTML generator (remove onclick handlers) function generateCalendarDropdown(shift) { const urls = generateCalendarUrls(shift); return `
`; } // Update setupShiftCardListeners to handle calendar dropdowns function setupShiftCardListeners() { const grid = document.getElementById('shifts-grid'); if (!grid) return; // Remove any existing listeners by cloning const newGrid = grid.cloneNode(true); grid.parentNode.replaceChild(newGrid, grid); // Add click listener for all buttons newGrid.addEventListener('click', async (e) => { // Handle signup buttons if (e.target.classList.contains('signup-btn')) { const shiftId = e.target.getAttribute('data-shift-id'); await signupForShift(shiftId); } // Handle cancel buttons else if (e.target.classList.contains('cancel-signup-btn')) { const shiftId = e.target.getAttribute('data-shift-id'); await cancelSignup(shiftId); } // Handle calendar toggle buttons else if (e.target.classList.contains('calendar-toggle')) { e.stopPropagation(); const dropdown = e.target.closest('.calendar-dropdown'); const options = dropdown.querySelector('.calendar-options'); const isOpen = options.style.display !== 'none'; // Close all other dropdowns document.querySelectorAll('.calendar-options').forEach(opt => { opt.style.display = 'none'; }); // Toggle this dropdown options.style.display = isOpen ? 'none' : 'block'; } // Handle calendar option clicks else if (e.target.closest('.calendar-option')) { e.stopPropagation(); const dropdown = e.target.closest('.calendar-dropdown'); const options = dropdown.querySelector('.calendar-options'); options.style.display = 'none'; } }); } // Update setupMySignupsListeners similarly function setupMySignupsListeners() { const list = document.getElementById('my-signups-list'); if (!list) return; // Remove any existing listeners by cloning const newList = list.cloneNode(true); list.parentNode.replaceChild(newList, list); // Add click listener for all interactions newList.addEventListener('click', async (e) => { // Handle cancel buttons if (e.target.classList.contains('cancel-signup-btn')) { const shiftId = e.target.getAttribute('data-shift-id'); await cancelSignup(shiftId); } // Handle calendar toggle buttons else if (e.target.classList.contains('calendar-toggle')) { e.stopPropagation(); const dropdown = e.target.closest('.calendar-dropdown'); const options = dropdown.querySelector('.calendar-options'); const isOpen = options.style.display !== 'none'; // Close all other dropdowns document.querySelectorAll('.calendar-options').forEach(opt => { opt.style.display = 'none'; }); // Toggle this dropdown options.style.display = isOpen ? 'none' : 'block'; } // Handle calendar option clicks else if (e.target.closest('.calendar-option')) { e.stopPropagation(); const dropdown = e.target.closest('.calendar-dropdown'); const options = dropdown.querySelector('.calendar-options'); options.style.display = 'none'; } }); } // Keep the document click handler to close dropdowns when clicking outside document.addEventListener('click', function() { document.querySelectorAll('.calendar-options').forEach(opt => { opt.style.display = 'none'; }); });