513 lines
20 KiB
JavaScript
513 lines
20 KiB
JavaScript
/**
|
|
* Admin Shifts Management Module
|
|
* Handles shift CRUD operations, volunteer management, and email functionality
|
|
*/
|
|
|
|
// Shift state
|
|
let editingShiftId = null;
|
|
let currentShiftData = null;
|
|
let allUsers = [];
|
|
|
|
// Add shift management functions
|
|
async function loadAdminShifts() {
|
|
const list = document.getElementById('admin-shifts-list');
|
|
if (list) {
|
|
list.innerHTML = '<p>Loading shifts...</p>';
|
|
}
|
|
|
|
try {
|
|
console.log('Loading admin shifts...');
|
|
const response = await fetch('/api/shifts/admin');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
console.log('Successfully loaded', data.shifts.length, 'shifts');
|
|
displayAdminShifts(data.shifts);
|
|
} else {
|
|
console.error('Failed to load shifts:', data.error);
|
|
if (list) {
|
|
list.innerHTML = '<p>Failed to load shifts</p>';
|
|
}
|
|
window.adminCore.showStatus('Failed to load shifts', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading admin shifts:', error);
|
|
if (list) {
|
|
list.innerHTML = '<p>Error loading shifts</p>';
|
|
}
|
|
window.adminCore.showStatus('Failed to load shifts', 'error');
|
|
}
|
|
}
|
|
|
|
function displayAdminShifts(shifts) {
|
|
const list = document.getElementById('admin-shifts-list');
|
|
|
|
if (!list) {
|
|
console.error('Admin shifts list element not found');
|
|
return;
|
|
}
|
|
|
|
if (shifts.length === 0) {
|
|
list.innerHTML = '<p>No shifts created yet.</p>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = shifts.map(shift => {
|
|
const shiftDate = window.adminCore.createLocalDate(shift.Date);
|
|
const signupCount = shift.signups ? shift.signups.length : 0;
|
|
const isPublic = shift['Is Public'] !== false;
|
|
|
|
console.log(`Shift "${shift.Title}" (ID: ${shift.ID}) has ${signupCount} volunteers:`, shift.signups?.map(s => s['User Email']) || []);
|
|
|
|
// Generate list of first names for volunteers
|
|
const firstNames = shift.signups ? shift.signups.map(volunteer => {
|
|
const fullName = volunteer['User Name'] || volunteer['User Email'] || 'Unknown';
|
|
// Extract first name (everything before first space, or email username if no space)
|
|
const firstName = fullName.includes(' ') ? fullName.split(' ')[0] :
|
|
fullName.includes('@') ? fullName.split('@')[0] : fullName;
|
|
return window.adminCore.escapeHtml(firstName);
|
|
}).slice(0, 8) : []; // Limit to first 8 names to avoid overflow
|
|
|
|
const namesDisplay = firstNames.length > 0 ?
|
|
`<span class="volunteer-names">(${firstNames.join(', ')}${firstNames.length === 8 && signupCount > 8 ? '...' : ''})</span>` :
|
|
'';
|
|
|
|
return `
|
|
<div class="shift-admin-item" data-shift-id="${shift.ID}">
|
|
<div>
|
|
<h4>${window.adminCore.escapeHtml(shift.Title)}</h4>
|
|
<p>📅 ${shiftDate.toLocaleDateString()} | ⏰ ${shift['Start Time']} - ${shift['End Time']}</p>
|
|
<p>📍 ${window.adminCore.escapeHtml(shift.Location || 'TBD')}</p>
|
|
<p class="volunteer-count">👥 ${signupCount}/${shift['Max Volunteers']} volunteers ${namesDisplay}</p>
|
|
<p class="status-${(shift.Status || 'open').toLowerCase()}">${shift.Status || 'Open'}</p>
|
|
<p class="${isPublic ? 'public-shift' : 'private-shift'}">${isPublic ? '🌐 Public' : '🔒 Private'}</p>
|
|
${isPublic ? `
|
|
<div class="public-link-section">
|
|
<label>Public Link:</label>
|
|
<div class="input-group">
|
|
<input type="text" class="public-link-input" value="${generateShiftPublicLink(shift.ID)}" readonly />
|
|
<button type="button" class="btn btn-secondary btn-sm copy-shift-link-btn" data-shift-id="${shift.ID}">📋</button>
|
|
<button type="button" class="btn btn-info btn-sm open-shift-link-btn" data-shift-id="${shift.ID}">🔗</button>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
<div class="shift-actions">
|
|
<button class="btn btn-primary btn-sm manage-volunteers-btn" data-shift-id="${shift.ID}" data-shift='${JSON.stringify(shift).replace(/'/g, "'")}'>Manage Volunteers</button>
|
|
<button class="btn btn-secondary btn-sm edit-shift-btn" data-shift-id="${shift.ID}">Edit</button>
|
|
<button class="btn btn-danger btn-sm delete-shift-btn" data-shift-id="${shift.ID}">Delete</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
// Add event listeners using delegation
|
|
setupShiftActionListeners();
|
|
}
|
|
|
|
// Setup shift action listeners
|
|
function setupShiftActionListeners() {
|
|
const list = document.getElementById('admin-shifts-list');
|
|
if (!list) return;
|
|
|
|
// Remove any existing listeners to avoid duplicates
|
|
const newList = list.cloneNode(true);
|
|
list.parentNode.replaceChild(newList, list);
|
|
|
|
// Get the updated reference
|
|
const updatedList = document.getElementById('admin-shifts-list');
|
|
|
|
updatedList.addEventListener('click', function(e) {
|
|
if (e.target.classList.contains('delete-shift-btn')) {
|
|
const shiftId = e.target.getAttribute('data-shift-id');
|
|
console.log('Delete button clicked for shift:', shiftId);
|
|
deleteShift(shiftId);
|
|
} else if (e.target.classList.contains('edit-shift-btn')) {
|
|
const shiftId = e.target.getAttribute('data-shift-id');
|
|
console.log('Edit button clicked for shift:', shiftId);
|
|
editShift(shiftId);
|
|
} else if (e.target.classList.contains('manage-volunteers-btn')) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const shiftId = e.target.getAttribute('data-shift-id');
|
|
const shiftDataStr = e.target.getAttribute('data-shift');
|
|
|
|
console.log('Manage volunteers clicked for shift:', shiftId);
|
|
console.log('Checking for adminShiftVolunteers module availability...');
|
|
console.log('adminShiftVolunteers exists:', !!window.adminShiftVolunteers);
|
|
|
|
if (window.adminShiftVolunteers) {
|
|
console.log('adminShiftVolunteers functions:', Object.keys(window.adminShiftVolunteers));
|
|
console.log('showShiftUserModal type:', typeof window.adminShiftVolunteers.showShiftUserModal);
|
|
}
|
|
|
|
// Parse the shift data
|
|
let shiftData;
|
|
try {
|
|
shiftData = JSON.parse(shiftDataStr.replace(/'/g, "'"));
|
|
console.log('Parsed shift data:', shiftData);
|
|
} catch (error) {
|
|
console.error('Error parsing shift data:', error);
|
|
if (window.adminCore && window.adminCore.showStatus) {
|
|
window.adminCore.showStatus('Error parsing shift data', 'error');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Function to attempt showing the modal
|
|
const attemptShowModal = () => {
|
|
if (window.adminShiftVolunteers && typeof window.adminShiftVolunteers.showShiftUserModal === 'function') {
|
|
console.log('Module ready, showing modal for shift:', shiftId);
|
|
try {
|
|
window.adminShiftVolunteers.showShiftUserModal(shiftId, shiftData);
|
|
} catch (modalError) {
|
|
console.error('Error showing modal:', modalError);
|
|
if (window.adminCore && window.adminCore.showStatus) {
|
|
window.adminCore.showStatus('Error opening volunteer management modal', 'error');
|
|
}
|
|
}
|
|
return true;
|
|
} else {
|
|
console.log('Module not ready - adminShiftVolunteers:', !!window.adminShiftVolunteers);
|
|
if (window.adminShiftVolunteers) {
|
|
console.log('Available functions:', Object.keys(window.adminShiftVolunteers));
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Try immediately
|
|
if (!attemptShowModal()) {
|
|
// If not ready, wait for the ready event or use timeout
|
|
let attempts = 0;
|
|
const maxAttempts = 20;
|
|
const retryInterval = 250;
|
|
|
|
const retryTimer = setInterval(() => {
|
|
attempts++;
|
|
console.log(`Retry attempt ${attempts}/${maxAttempts} for volunteer modal...`);
|
|
|
|
if (attemptShowModal()) {
|
|
clearInterval(retryTimer);
|
|
} else if (attempts >= maxAttempts) {
|
|
clearInterval(retryTimer);
|
|
console.error('Failed to load volunteer management module after', maxAttempts, 'attempts');
|
|
console.log('Final state - adminShiftVolunteers:', window.adminShiftVolunteers);
|
|
if (window.adminCore && window.adminCore.showStatus) {
|
|
window.adminCore.showStatus('Volunteer management module failed to load. Please refresh the page.', 'error');
|
|
}
|
|
}
|
|
}, retryInterval);
|
|
|
|
// Also listen for the ready event as backup
|
|
const readyListener = () => {
|
|
console.log('Received adminShiftVolunteersReady event');
|
|
if (attemptShowModal()) {
|
|
clearInterval(retryTimer);
|
|
window.removeEventListener('adminShiftVolunteersReady', readyListener);
|
|
}
|
|
};
|
|
window.addEventListener('adminShiftVolunteersReady', readyListener);
|
|
}
|
|
|
|
} else if (e.target.classList.contains('copy-shift-link-btn')) {
|
|
const shiftId = e.target.getAttribute('data-shift-id');
|
|
console.log('Copy link button clicked for shift:', shiftId);
|
|
copyShiftLink(shiftId);
|
|
} else if (e.target.classList.contains('open-shift-link-btn')) {
|
|
const shiftId = e.target.getAttribute('data-shift-id');
|
|
console.log('Open link button clicked for shift:', shiftId);
|
|
openShiftLink(shiftId);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Delete shift
|
|
async function deleteShift(shiftId) {
|
|
if (!confirm('Are you sure you want to delete this shift? All signups will be cancelled.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/shifts/admin/${shiftId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
window.adminCore.showStatus('Shift deleted successfully', 'success');
|
|
await loadAdminShifts();
|
|
console.log('Refreshed shifts list after deleting shift');
|
|
} else {
|
|
window.adminCore.showStatus(data.error || 'Failed to delete shift', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting shift:', error);
|
|
window.adminCore.showStatus('Failed to delete shift', 'error');
|
|
}
|
|
}
|
|
|
|
// Edit shift
|
|
async function editShift(shiftId) {
|
|
try {
|
|
// Find the shift in the current data
|
|
const response = await fetch('/api/shifts/admin');
|
|
const data = await response.json();
|
|
|
|
if (!data.success) {
|
|
window.adminCore.showStatus('Failed to load shift data', 'error');
|
|
return;
|
|
}
|
|
|
|
const shift = data.shifts.find(s => s.ID === parseInt(shiftId));
|
|
if (!shift) {
|
|
window.adminCore.showStatus('Shift not found', 'error');
|
|
return;
|
|
}
|
|
|
|
// Set editing mode
|
|
editingShiftId = shiftId;
|
|
|
|
// Populate the form
|
|
const titleInput = document.getElementById('shift-title');
|
|
const descInput = document.getElementById('shift-description');
|
|
const dateInput = document.getElementById('shift-date');
|
|
const startInput = document.getElementById('shift-start');
|
|
const endInput = document.getElementById('shift-end');
|
|
const locationInput = document.getElementById('shift-location');
|
|
const maxVolInput = document.getElementById('shift-max-volunteers');
|
|
|
|
if (titleInput) titleInput.value = shift.Title || '';
|
|
if (descInput) descInput.value = shift.Description || '';
|
|
if (dateInput) dateInput.value = shift.Date || '';
|
|
if (startInput) startInput.value = shift['Start Time'] || '';
|
|
if (endInput) endInput.value = shift['End Time'] || '';
|
|
if (locationInput) locationInput.value = shift.Location || '';
|
|
if (maxVolInput) maxVolInput.value = shift['Max Volunteers'] || '';
|
|
|
|
// Update public checkbox if it exists
|
|
const publicCheckbox = document.getElementById('shift-is-public');
|
|
if (publicCheckbox) {
|
|
publicCheckbox.checked = shift['Is Public'] !== false;
|
|
}
|
|
|
|
// Change submit button text
|
|
const submitBtn = document.querySelector('#shift-form button[type="submit"]');
|
|
if (submitBtn) {
|
|
submitBtn.textContent = 'Update Shift';
|
|
}
|
|
|
|
// Remove editing class from any previous item
|
|
document.querySelectorAll('.shift-admin-item.editing').forEach(el => {
|
|
el.classList.remove('editing');
|
|
});
|
|
|
|
// Add editing class to current item
|
|
const shiftElement = document.querySelector(`[data-shift-id="${shiftId}"]`);
|
|
if (shiftElement) {
|
|
const shiftItem = shiftElement.closest('.shift-admin-item');
|
|
if (shiftItem) {
|
|
shiftItem.classList.add('editing');
|
|
}
|
|
}
|
|
|
|
// Scroll to form
|
|
const form = document.getElementById('shift-form');
|
|
if (form) {
|
|
form.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
|
|
window.adminCore.showStatus('Editing shift: ' + shift.Title, 'info');
|
|
|
|
} catch (error) {
|
|
console.error('Error loading shift for edit:', error);
|
|
window.adminCore.showStatus('Failed to load shift for editing', 'error');
|
|
}
|
|
}
|
|
|
|
// Create or update shift
|
|
async function createShift(e) {
|
|
e.preventDefault();
|
|
|
|
const titleInput = document.getElementById('shift-title');
|
|
const descInput = document.getElementById('shift-description');
|
|
const dateInput = document.getElementById('shift-date');
|
|
const startInput = document.getElementById('shift-start');
|
|
const endInput = document.getElementById('shift-end');
|
|
const locationInput = document.getElementById('shift-location');
|
|
const maxVolInput = document.getElementById('shift-max-volunteers');
|
|
|
|
const title = titleInput?.value;
|
|
const description = descInput?.value;
|
|
const date = dateInput?.value;
|
|
const startTime = startInput?.value;
|
|
const endTime = endInput?.value;
|
|
const location = locationInput?.value;
|
|
const maxVolunteers = maxVolInput?.value;
|
|
|
|
// Get public checkbox value
|
|
const publicCheckbox = document.getElementById('shift-is-public');
|
|
const isPublic = publicCheckbox?.checked ?? true;
|
|
|
|
const shiftData = {
|
|
title,
|
|
description,
|
|
date,
|
|
startTime,
|
|
endTime,
|
|
location,
|
|
maxVolunteers: parseInt(maxVolunteers),
|
|
isPublic
|
|
};
|
|
|
|
try {
|
|
let response;
|
|
if (editingShiftId) {
|
|
// Update existing shift
|
|
response = await fetch(`/api/shifts/admin/${editingShiftId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(shiftData)
|
|
});
|
|
} else {
|
|
// Create new shift
|
|
response = await fetch('/api/shifts/admin', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(shiftData)
|
|
});
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
window.adminCore.showStatus(editingShiftId ? 'Shift updated successfully' : 'Shift created successfully', 'success');
|
|
clearShiftForm();
|
|
await loadAdminShifts();
|
|
console.log('Refreshed shifts list after saving shift');
|
|
} else {
|
|
window.adminCore.showStatus(data.error || 'Failed to save shift', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving shift:', error);
|
|
window.adminCore.showStatus('Failed to save shift', 'error');
|
|
}
|
|
}
|
|
|
|
function clearShiftForm() {
|
|
const form = document.getElementById('shift-form');
|
|
if (form) {
|
|
form.reset();
|
|
|
|
// Reset editing state
|
|
editingShiftId = null;
|
|
|
|
// Reset submit button text
|
|
const submitBtn = document.querySelector('#shift-form button[type="submit"]');
|
|
if (submitBtn) {
|
|
submitBtn.textContent = 'Create Shift';
|
|
}
|
|
|
|
// Remove editing class from any shift items
|
|
document.querySelectorAll('.shift-admin-item.editing').forEach(el => {
|
|
el.classList.remove('editing');
|
|
});
|
|
|
|
window.adminCore.showStatus('Form cleared', 'info');
|
|
}
|
|
}
|
|
|
|
// Public Shifts Functions
|
|
function generateShiftPublicLink(shiftId) {
|
|
const baseUrl = window.location.origin;
|
|
return `${baseUrl}/public-shifts.html#shift-${shiftId}`;
|
|
}
|
|
|
|
function copyShiftLink(shiftId) {
|
|
const link = generateShiftPublicLink(shiftId);
|
|
navigator.clipboard.writeText(link).then(() => {
|
|
window.adminCore.showStatus('Public shift link copied to clipboard!', 'success');
|
|
}).catch(() => {
|
|
// Fallback for older browsers
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = link;
|
|
document.body.appendChild(textArea);
|
|
textArea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textArea);
|
|
window.adminCore.showStatus('Public shift link copied to clipboard!', 'success');
|
|
});
|
|
}
|
|
|
|
function openShiftLink(shiftId) {
|
|
const link = generateShiftPublicLink(shiftId);
|
|
window.open(link, '_blank');
|
|
}
|
|
|
|
// Update the shift form to include Is Public checkbox
|
|
function updateShiftFormWithPublicOption() {
|
|
const form = document.getElementById('shift-form');
|
|
if (!form) return;
|
|
|
|
// Check if public checkbox already exists
|
|
if (document.getElementById('shift-is-public')) return;
|
|
|
|
const maxVolunteersGroup = form.querySelector('.form-group:has(#shift-max-volunteers)');
|
|
if (maxVolunteersGroup) {
|
|
const publicGroup = document.createElement('div');
|
|
publicGroup.className = 'form-group';
|
|
publicGroup.innerHTML = `
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" id="shift-is-public" checked />
|
|
<span>Show on public signup page</span>
|
|
</label>
|
|
`;
|
|
maxVolunteersGroup.insertAdjacentElement('afterend', publicGroup);
|
|
}
|
|
}
|
|
|
|
// Setup shift event listeners
|
|
function setupShiftEventListeners() {
|
|
// Shift form submission
|
|
const shiftForm = document.getElementById('shift-form');
|
|
if (shiftForm) {
|
|
shiftForm.addEventListener('submit', createShift);
|
|
}
|
|
|
|
// Clear shift form button
|
|
const clearShiftBtn = document.getElementById('clear-shift-form');
|
|
if (clearShiftBtn) {
|
|
clearShiftBtn.addEventListener('click', function() {
|
|
const wasEditing = editingShiftId !== null;
|
|
clearShiftForm();
|
|
if (wasEditing) {
|
|
window.adminCore.showStatus('Edit cancelled', 'info');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Export shift management functions
|
|
window.adminShifts = {
|
|
loadAdminShifts,
|
|
displayAdminShifts,
|
|
deleteShift,
|
|
editShift,
|
|
createShift,
|
|
clearShiftForm,
|
|
generateShiftPublicLink,
|
|
copyShiftLink,
|
|
openShiftLink,
|
|
updateShiftFormWithPublicOption,
|
|
setupShiftEventListeners,
|
|
getEditingShiftId: () => editingShiftId,
|
|
getCurrentShiftData: () => currentShiftData,
|
|
getAllUsers: () => allUsers
|
|
};
|