2025-08-11 14:01:25 -06:00

1149 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

let currentUser = null;
let allShifts = [];
let mySignups = [];
let currentView = 'grid'; // 'grid' or 'calendar'
let currentCalendarDate = new Date(); // For calendar navigation
// Utility function to create a local date from YYYY-MM-DD string
// This prevents timezone issues when displaying dates
function createLocalDate(dateString) {
if (!dateString) return null;
const parts = dateString.split('-');
if (parts.length !== 3) return new Date(dateString); // fallback to original behavior
// Create date using local timezone (year, month-1, day)
return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
}
// Function to set viewport dimensions for shifts page
function setShiftsViewportDimensions() {
const doc = document.documentElement;
doc.style.setProperty('--app-height', `${window.innerHeight}px`);
doc.style.setProperty('--app-width', `${window.innerWidth}px`);
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', async () => {
// Set initial viewport dimensions and listen for resize events
setShiftsViewportDimensions();
window.addEventListener('resize', setShiftsViewportDimensions);
window.addEventListener('orientationchange', () => {
setTimeout(setShiftsViewportDimensions, 100);
});
await checkAuth();
await loadShifts();
await loadMySignups();
setupEventListeners();
initializeViewToggle();
// 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 = '<div class="no-shifts">No shifts available for the selected criteria.</div>';
return;
}
grid.innerHTML = shifts.map(shift => {
const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID);
const isFull = shift['Current Volunteers'] >= shift['Max Volunteers'];
const shiftDate = createLocalDate(shift.Date);
return `
<div class="shift-card ${isSignedUp ? 'signed-up' : ''} ${isFull && !isSignedUp ? 'full' : ''}">
<h3>${escapeHtml(shift.Title)}</h3>
<div class="shift-details">
<p>📅 ${shiftDate.toLocaleDateString()}</p>
<p>⏰ ${shift['Start Time']} - ${shift['End Time']}</p>
<p>📍 ${escapeHtml(shift.Location || 'TBD')}</p>
<p>👥 ${shift['Current Volunteers']}/${shift['Max Volunteers']} volunteers</p>
</div>
${shift.Description ? `<p class="shift-description">${escapeHtml(shift.Description)}</p>` : ''}
<div class="shift-actions">
${isSignedUp
? `<button class="btn btn-danger btn-sm cancel-signup-btn" data-shift-id="${shift.ID}">Cancel Signup</button>
${generateCalendarDropdown(shift)}`
: isFull
? '<button class="btn btn-secondary btn-sm" disabled>Shift Full</button>'
: `<button class="btn btn-primary btn-sm signup-btn" data-shift-id="${shift.ID}">Sign Up</button>`
}
</div>
</div>
`;
}).join('');
// Set up event listeners using delegation
setupShiftCardListeners();
// Update calendar view if it's currently active
if (currentView === 'calendar') {
renderCalendar();
}
}
function displayMySignups() {
const list = document.getElementById('my-signups-list');
if (mySignups.length === 0) {
list.innerHTML = '<p class="no-shifts">You haven\'t signed up for any shifts yet.</p>';
return;
}
// Need to match signups with shift details for date/time info
const signupsWithDetails = mySignups.map(signup => {
const shift = allShifts.find(s => s.ID === signup.shift_id);
return { ...signup, shift };
}).filter(s => s.shift); // Only show signups where we can find the shift details
list.innerHTML = signupsWithDetails.map(signup => {
const shiftDate = createLocalDate(signup.shift.Date);
return `
<div class="signup-item">
<div>
<h4>${escapeHtml(signup.shift.Title)}</h4>
<p>📅 ${shiftDate.toLocaleDateString()}${signup.shift['Start Time']} - ${signup.shift['End Time']}</p>
<p>📍 ${escapeHtml(signup.shift.Location || 'TBD')}</p>
</div>
<div class="signup-actions">
${generateCalendarDropdown(signup.shift)}
<button class="btn btn-danger btn-sm cancel-signup-btn" data-shift-id="${signup.shift_id}">Cancel Signup</button>
</div>
</div>
`;
}).join('');
// Set up event listeners using delegation
setupMySignupsListeners();
// Update calendar view if it's currently active
if (currentView === 'calendar') {
renderCalendar();
}
}
// New function to setup listeners for shift cards
function setupShiftCardListeners() {
const grid = document.getElementById('shifts-grid');
if (!grid) return;
// Use event delegation on the grid itself, not cloning
grid.removeEventListener('click', handleShiftCardClick); // Remove if exists
grid.addEventListener('click', handleShiftCardClick);
}
// Create a separate handler function
function handleShiftCardClick(e) {
const target = e.target;
// Handle signup button
if (target.classList.contains('signup-btn')) {
e.preventDefault();
e.stopPropagation();
const shiftId = target.getAttribute('data-shift-id');
if (shiftId) signupForShift(shiftId);
return;
}
// Handle cancel button
if (target.classList.contains('cancel-signup-btn')) {
e.preventDefault();
e.stopPropagation();
const shiftId = target.getAttribute('data-shift-id');
if (shiftId) cancelSignup(shiftId);
return;
}
// Handle calendar toggle
if (target.classList.contains('calendar-toggle')) {
e.preventDefault();
e.stopPropagation();
const dropdown = target.nextElementSibling;
// Close all other dropdowns
document.querySelectorAll('.calendar-options').forEach(opt => {
if (opt !== dropdown) opt.style.display = 'none';
});
// Toggle this dropdown
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
return;
}
// Handle calendar option clicks
if (target.classList.contains('calendar-option')) {
e.stopPropagation();
// Let the link work naturally
return;
}
}
// New function to setup listeners for my signups
function setupMySignupsListeners() {
const list = document.getElementById('my-signups-list');
if (!list) return;
// Use event delegation
list.removeEventListener('click', handleMySignupsClick); // Remove if exists
list.addEventListener('click', handleMySignupsClick);
}
// Create a separate handler for my signups
function handleMySignupsClick(e) {
const target = e.target;
// Handle cancel button
if (target.classList.contains('cancel-signup-btn')) {
e.preventDefault();
e.stopPropagation();
const shiftId = target.getAttribute('data-shift-id');
if (shiftId) cancelSignup(shiftId);
return;
}
// Handle calendar toggle
if (target.classList.contains('calendar-toggle')) {
e.preventDefault();
e.stopPropagation();
const dropdown = target.nextElementSibling;
// Close all other dropdowns
document.querySelectorAll('.calendar-options').forEach(opt => {
if (opt !== dropdown) opt.style.display = 'none';
});
// Toggle this dropdown
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
return;
}
}
// New function to generate calendar URLs
function generateCalendarUrls(shift) {
const shiftDate = createLocalDate(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 `
<div class="calendar-dropdown">
<button class="btn btn-secondary btn-sm calendar-toggle" data-shift-id="${shift.ID}">
📅 Add to Calendar ▼
</button>
<div class="calendar-options" style="display: none;">
<a href="${urls.google}" target="_blank" class="calendar-option" data-calendar-type="google">
Google Calendar
</a>
<a href="${urls.outlook}" target="_blank" class="calendar-option" data-calendar-type="outlook">
Outlook
</a>
<a href="${urls.apple}" download="${urls.icsFilename}" class="calendar-option" data-calendar-type="apple">
Apple Calendar
</a>
</div>
</div>
`;
}
// Update setupShiftCardListeners to handle calendar dropdowns
function setupShiftCardListeners() {
const grid = document.getElementById('shifts-grid');
if (!grid) return;
// Use event delegation on the grid itself, not cloning
grid.removeEventListener('click', handleShiftCardClick); // Remove if exists
grid.addEventListener('click', handleShiftCardClick);
}
// Create a separate handler function
function handleShiftCardClick(e) {
const target = e.target;
// Handle signup button
if (target.classList.contains('signup-btn')) {
e.preventDefault();
e.stopPropagation();
const shiftId = target.getAttribute('data-shift-id');
if (shiftId) signupForShift(shiftId);
return;
}
// Handle cancel button
if (target.classList.contains('cancel-signup-btn')) {
e.preventDefault();
e.stopPropagation();
const shiftId = target.getAttribute('data-shift-id');
if (shiftId) cancelSignup(shiftId);
return;
}
// Handle calendar toggle
if (target.classList.contains('calendar-toggle')) {
e.preventDefault();
e.stopPropagation();
const dropdown = target.nextElementSibling;
// Close all other dropdowns
document.querySelectorAll('.calendar-options').forEach(opt => {
if (opt !== dropdown) opt.style.display = 'none';
});
// Toggle this dropdown
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
return;
}
// Handle calendar option clicks
if (target.classList.contains('calendar-option')) {
e.stopPropagation();
// Let the link work naturally
return;
}
}
// Fix the setupMySignupsListeners function similarly
function setupMySignupsListeners() {
const list = document.getElementById('my-signups-list');
if (!list) return;
// Use event delegation
list.removeEventListener('click', handleMySignupsClick); // Remove if exists
list.addEventListener('click', handleMySignupsClick);
}
// Create a separate handler for my signups
function handleMySignupsClick(e) {
const target = e.target;
// Handle cancel button
if (target.classList.contains('cancel-signup-btn')) {
e.preventDefault();
e.stopPropagation();
const shiftId = target.getAttribute('data-shift-id');
if (shiftId) cancelSignup(shiftId);
return;
}
// Handle calendar toggle
if (target.classList.contains('calendar-toggle')) {
e.preventDefault();
e.stopPropagation();
const dropdown = target.nextElementSibling;
// Close all other dropdowns
document.querySelectorAll('.calendar-options').forEach(opt => {
if (opt !== dropdown) opt.style.display = 'none';
});
// Toggle this dropdown
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
return;
}
}
// Update the displayShifts function to properly show calendar dropdowns
function displayShifts(shifts) {
const grid = document.getElementById('shifts-grid');
if (shifts.length === 0) {
grid.innerHTML = '<div class="no-shifts">No shifts available for the selected criteria.</div>';
return;
}
grid.innerHTML = shifts.map(shift => {
const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID);
const isFull = shift['Current Volunteers'] >= shift['Max Volunteers'];
const shiftDate = createLocalDate(shift.Date);
return `
<div class="shift-card ${isSignedUp ? 'signed-up' : ''} ${isFull && !isSignedUp ? 'full' : ''}">
<h3>${escapeHtml(shift.Title)}</h3>
<div class="shift-details">
<p>📅 ${shiftDate.toLocaleDateString()}</p>
<p>⏰ ${shift['Start Time']} - ${shift['End Time']}</p>
<p>📍 ${escapeHtml(shift.Location || 'TBD')}</p>
<p>👥 ${shift['Current Volunteers']}/${shift['Max Volunteers']} volunteers</p>
</div>
${shift.Description ? `<p class="shift-description">${escapeHtml(shift.Description)}</p>` : ''}
<div class="shift-actions">
${isSignedUp
? `<button class="btn btn-danger btn-sm cancel-signup-btn" data-shift-id="${shift.ID}">Cancel Signup</button>
${generateCalendarDropdown(shift)}`
: isFull
? '<button class="btn btn-secondary btn-sm" disabled>Shift Full</button>'
: `<button class="btn btn-primary btn-sm signup-btn" data-shift-id="${shift.ID}">Sign Up</button>`
}
</div>
</div>
`;
}).join('');
// Set up event listeners using delegation
setupShiftCardListeners();
// Update calendar view if it's currently active
if (currentView === 'calendar') {
renderCalendar();
}
}
// Update the displayMySignups function to always show calendar dropdowns
function displayMySignups() {
const list = document.getElementById('my-signups-list');
if (mySignups.length === 0) {
list.innerHTML = '<p class="no-shifts">You haven\'t signed up for any shifts yet.</p>';
return;
}
// Need to match signups with shift details for date/time info
const signupsWithDetails = mySignups.map(signup => {
const shift = allShifts.find(s => s.ID === signup.shift_id);
return { ...signup, shift };
}).filter(s => s.shift); // Only show signups where we can find the shift details
list.innerHTML = signupsWithDetails.map(signup => {
const shiftDate = createLocalDate(signup.shift.Date);
return `
<div class="signup-item">
<div>
<h4>${escapeHtml(signup.shift.Title)}</h4>
<p>📅 ${shiftDate.toLocaleDateString()}${signup.shift['Start Time']} - ${signup.shift['End Time']}</p>
<p>📍 ${escapeHtml(signup.shift.Location || 'TBD')}</p>
</div>
<div class="signup-actions">
${generateCalendarDropdown(signup.shift)}
<button class="btn btn-danger btn-sm cancel-signup-btn" data-shift-id="${signup.shift_id}">Cancel Signup</button>
</div>
</div>
`;
}).join('');
// Set up event listeners using delegation
setupMySignupsListeners();
// Update calendar view if it's currently active
if (currentView === 'calendar') {
renderCalendar();
}
}
// Add a global variable to track popup cleanup
let currentPopup = null;
// Update the showShiftPopup function to handle z-index and cleanup
function showShiftPopup(shift, targetElement) {
// Remove any existing popup
if (currentPopup) {
currentPopup.remove();
currentPopup = null;
}
const existingPopup = document.querySelector('.shift-popup');
if (existingPopup) {
existingPopup.remove();
}
const popup = document.createElement('div');
popup.className = 'shift-popup';
const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID);
const isFull = shift['Current Volunteers'] >= shift['Max Volunteers'];
const shiftDate = createLocalDate(shift.Date);
popup.innerHTML = `
<h4>${escapeHtml(shift.Title)}</h4>
<p>📅 ${shiftDate.toLocaleDateString()}</p>
<p>⏰ ${shift['Start Time']} - ${shift['End Time']}</p>
<p>📍 ${escapeHtml(shift.Location || 'TBD')}</p>
<p>👥 ${shift['Current Volunteers']}/${shift['Max Volunteers']} volunteers</p>
${shift.Description ? `<p>${escapeHtml(shift.Description)}</p>` : ''}
<div class="shift-actions">
${isSignedUp
? `<button class="btn btn-danger btn-sm cancel-signup-btn" data-shift-id="${shift.ID}">Cancel Signup</button>`
: isFull
? '<button class="btn btn-secondary btn-sm" disabled>Shift Full</button>'
: `<button class="btn btn-primary btn-sm signup-btn" data-shift-id="${shift.ID}">Sign Up</button>`
}
</div>
`;
// Position popup
document.body.appendChild(popup);
currentPopup = popup; // Track the current popup
const rect = targetElement.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
let left = rect.left + (rect.width / 2) - (popupRect.width / 2);
let top = rect.bottom + 10;
// Adjust if popup goes off screen
if (left < 10) left = 10;
if (left + popupRect.width > window.innerWidth - 10) {
left = window.innerWidth - popupRect.width - 10;
}
if (top + popupRect.height > window.innerHeight - 10) {
top = rect.top - popupRect.height - 10;
}
popup.style.left = `${left}px`;
popup.style.top = `${top}px`;
// Add event listeners for buttons in popup
const signupBtn = popup.querySelector('.signup-btn');
const cancelBtn = popup.querySelector('.cancel-signup-btn');
if (signupBtn) {
signupBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await signupForShift(shift.ID);
popup.remove();
currentPopup = null;
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await cancelSignup(shift.ID);
popup.remove();
currentPopup = null;
});
}
// Close popup when clicking outside
const closePopup = (e) => {
if (!popup.contains(e.target) && e.target !== targetElement) {
popup.remove();
currentPopup = null;
document.removeEventListener('click', closePopup);
}
};
setTimeout(() => {
document.addEventListener('click', closePopup);
}, 100);
}
// Close calendar dropdowns when clicking outside
document.addEventListener('click', function(e) {
// Don't close if clicking on a toggle or option
if (!e.target.classList.contains('calendar-toggle') &&
!e.target.classList.contains('calendar-option') &&
!e.target.closest('.calendar-dropdown')) {
document.querySelectorAll('.calendar-options').forEach(opt => {
opt.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');
}
}
// Add a custom confirmation modal function
function showConfirmModal(message, onConfirm, onCancel = null) {
// Remove any existing modal
const existingModal = document.querySelector('.confirm-modal');
if (existingModal) {
existingModal.remove();
}
// Create modal
const modal = document.createElement('div');
modal.className = 'confirm-modal';
modal.innerHTML = `
<div class="confirm-modal-backdrop">
<div class="confirm-modal-content">
<h3>Confirm Action</h3>
<p>${message}</p>
<div class="confirm-modal-actions">
<button class="btn btn-secondary confirm-cancel">Cancel</button>
<button class="btn btn-danger confirm-ok">Confirm</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Add event listeners
const cancelBtn = modal.querySelector('.confirm-cancel');
const confirmBtn = modal.querySelector('.confirm-ok');
const backdrop = modal.querySelector('.confirm-modal-backdrop');
const cleanup = () => {
modal.remove();
};
cancelBtn.addEventListener('click', () => {
cleanup();
if (onCancel) onCancel();
});
confirmBtn.addEventListener('click', () => {
cleanup();
onConfirm();
});
// Close on backdrop click
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) {
cleanup();
if (onCancel) onCancel();
}
});
// Close on escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
cleanup();
document.removeEventListener('keydown', handleEscape);
if (onCancel) onCancel();
}
};
document.addEventListener('keydown', handleEscape);
// Focus the confirm button for keyboard navigation
setTimeout(() => {
confirmBtn.focus();
}, 100);
}
// Update the cancelSignup function to use the custom modal
async function cancelSignup(shiftId) {
showConfirmModal(
'Are you sure you want to cancel your signup for this shift?',
async () => {
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;
}
// Calendar View Functions
function initializeViewToggle() {
const gridBtn = document.getElementById('grid-view-btn');
const calendarBtn = document.getElementById('calendar-view-btn');
const prevBtn = document.getElementById('prev-month');
const nextBtn = document.getElementById('next-month');
if (gridBtn && calendarBtn) {
gridBtn.addEventListener('click', () => switchView('grid'));
calendarBtn.addEventListener('click', () => switchView('calendar'));
// Set initial active state
gridBtn.classList.add('active');
}
if (prevBtn && nextBtn) {
prevBtn.addEventListener('click', () => navigateCalendar(-1));
nextBtn.addEventListener('click', () => navigateCalendar(1));
}
}
function switchView(view) {
const gridView = document.getElementById('shifts-grid');
const calendarView = document.getElementById('calendar-view');
const gridBtn = document.getElementById('grid-view-btn');
const calendarBtn = document.getElementById('calendar-view-btn');
currentView = view;
if (view === 'calendar') {
gridView.style.display = 'none';
calendarView.style.display = 'block';
gridBtn.classList.remove('active');
calendarBtn.classList.add('active');
renderCalendar();
} else {
gridView.style.display = 'grid';
calendarView.style.display = 'none';
gridBtn.classList.add('active');
calendarBtn.classList.remove('active');
}
}
function navigateCalendar(direction) {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() + direction);
renderCalendar();
}
function renderCalendar() {
const year = currentCalendarDate.getFullYear();
const month = currentCalendarDate.getMonth();
// Update header
const monthYearHeader = document.getElementById('calendar-month-year');
if (monthYearHeader) {
const monthNames = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
monthYearHeader.textContent = `${monthNames[month]} ${year}`;
}
// Get calendar grid
const calendarGrid = document.getElementById('calendar-grid');
if (!calendarGrid) return;
calendarGrid.innerHTML = '';
// Add day headers
const dayHeaders = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
dayHeaders.forEach(day => {
const dayHeader = document.createElement('div');
dayHeader.className = 'calendar-day-header';
dayHeader.textContent = day;
calendarGrid.appendChild(dayHeader);
});
// Get first day of month and number of days
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
// Get previous month's last days
const prevMonth = new Date(year, month, 0);
const daysInPrevMonth = prevMonth.getDate();
// Add previous month's trailing days
for (let i = startingDayOfWeek - 1; i >= 0; i--) {
const dayNumber = daysInPrevMonth - i;
const dayElement = createCalendarDay(dayNumber, true, new Date(year, month - 1, dayNumber));
calendarGrid.appendChild(dayElement);
}
// Add current month's days
for (let day = 1; day <= daysInMonth; day++) {
const currentDate = new Date(year, month, day);
const dayElement = createCalendarDay(day, false, currentDate);
calendarGrid.appendChild(dayElement);
}
// Add next month's leading days to fill the grid
const totalCells = calendarGrid.children.length;
const remainingCells = 42 - totalCells; // 6 rows × 7 days
for (let day = 1; day <= remainingCells; day++) {
const dayElement = createCalendarDay(day, true, new Date(year, month + 1, day));
calendarGrid.appendChild(dayElement);
}
}
function createCalendarDay(dayNumber, isOtherMonth, date) {
const dayElement = document.createElement('div');
dayElement.className = 'calendar-day';
if (isOtherMonth) {
dayElement.classList.add('other-month');
}
// Check if it's today
const today = new Date();
if (date.toDateString() === today.toDateString()) {
dayElement.classList.add('today');
}
// Add day number
const dayNumberElement = document.createElement('div');
dayNumberElement.className = 'calendar-day-number';
dayNumberElement.textContent = dayNumber;
dayElement.appendChild(dayNumberElement);
// Add shifts for this day
const shiftsContainer = document.createElement('div');
shiftsContainer.className = 'calendar-shifts';
// Create a consistent date string for comparison without timezone conversion
const calendarDateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
// Debug logging to verify date matching
const dayShifts = allShifts.filter(shift => {
return shift.Date === calendarDateString;
});
dayShifts.forEach(shift => {
const shiftElement = createCalendarShift(shift);
shiftsContainer.appendChild(shiftElement);
});
dayElement.appendChild(shiftsContainer);
return dayElement;
}
function createCalendarShift(shift) {
const shiftElement = document.createElement('div');
shiftElement.className = 'calendar-shift';
// Determine shift type and color
const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID);
const isFull = shift['Current Volunteers'] >= shift['Max Volunteers'];
if (isSignedUp) {
shiftElement.classList.add('my-shift');
} else if (isFull) {
shiftElement.classList.add('full-shift');
} else {
shiftElement.classList.add('available-shift');
}
// Set shift text (time and title)
const timeText = `${shift['Start Time']} ${shift.Title}`;
shiftElement.textContent = timeText;
shiftElement.title = `${shift.Title}\n${shift['Start Time']} - ${shift['End Time']}\n${shift.Location || 'TBD'}`;
// Add click handler
shiftElement.addEventListener('click', (e) => {
e.stopPropagation();
showShiftPopup(shift, e.target);
});
return shiftElement;
}
function showShiftPopup(shift, targetElement) {
// Remove any existing popup
if (currentPopup) {
currentPopup.remove();
currentPopup = null;
}
const existingPopup = document.querySelector('.shift-popup');
if (existingPopup) {
existingPopup.remove();
}
const popup = document.createElement('div');
popup.className = 'shift-popup';
const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID);
const isFull = shift['Current Volunteers'] >= shift['Max Volunteers'];
const shiftDate = createLocalDate(shift.Date);
popup.innerHTML = `
<h4>${escapeHtml(shift.Title)}</h4>
<p>📅 ${shiftDate.toLocaleDateString()}</p>
<p>⏰ ${shift['Start Time']} - ${shift['End Time']}</p>
<p>📍 ${escapeHtml(shift.Location || 'TBD')}</p>
<p>👥 ${shift['Current Volunteers']}/${shift['Max Volunteers']} volunteers</p>
${shift.Description ? `<p>${escapeHtml(shift.Description)}</p>` : ''}
<div class="shift-actions">
${isSignedUp
? `<button class="btn btn-danger btn-sm cancel-signup-btn" data-shift-id="${shift.ID}">Cancel Signup</button>`
: isFull
? '<button class="btn btn-secondary btn-sm" disabled>Shift Full</button>'
: `<button class="btn btn-primary btn-sm signup-btn" data-shift-id="${shift.ID}">Sign Up</button>`
}
</div>
`;
// Position popup
document.body.appendChild(popup);
currentPopup = popup; // Track the current popup
const rect = targetElement.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
let left = rect.left + (rect.width / 2) - (popupRect.width / 2);
let top = rect.bottom + 10;
// Adjust if popup goes off screen
if (left < 10) left = 10;
if (left + popupRect.width > window.innerWidth - 10) {
left = window.innerWidth - popupRect.width - 10;
}
if (top + popupRect.height > window.innerHeight - 10) {
top = rect.top - popupRect.height - 10;
}
popup.style.left = `${left}px`;
popup.style.top = `${top}px`;
// Add event listeners for buttons in popup
const signupBtn = popup.querySelector('.signup-btn');
const cancelBtn = popup.querySelector('.cancel-signup-btn');
if (signupBtn) {
signupBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await signupForShift(shift.ID);
popup.remove();
currentPopup = null;
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await cancelSignup(shift.ID);
popup.remove();
currentPopup = null;
});
}
// Close popup when clicking outside
const closePopup = (e) => {
if (!popup.contains(e.target) && e.target !== targetElement) {
popup.remove();
currentPopup = null;
document.removeEventListener('click', closePopup);
}
};
setTimeout(() => {
document.addEventListener('click', closePopup);
}, 100);
}
// 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';
});
});