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

2467 lines
90 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.

// Admin panel JavaScript
let adminMap = null;
let startMarker = null;
let storedQRCodes = {};
// 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]));
}
// A function to set viewport dimensions for admin page
function setAdminViewportDimensions() {
const doc = document.documentElement;
// Set height and width
doc.style.setProperty('--app-height', `${window.innerHeight}px`);
doc.style.setProperty('--app-width', `${window.innerWidth}px`);
// Handle safe area insets for devices with notches or home indicators
if (CSS.supports('padding: env(safe-area-inset-top)')) {
doc.style.setProperty('--safe-area-top', 'env(safe-area-inset-top)');
doc.style.setProperty('--safe-area-bottom', 'env(safe-area-inset-bottom)');
doc.style.setProperty('--safe-area-left', 'env(safe-area-inset-left)');
doc.style.setProperty('--safe-area-right', 'env(safe-area-inset-right)');
} else {
doc.style.setProperty('--safe-area-top', '0px');
doc.style.setProperty('--safe-area-bottom', '0px');
doc.style.setProperty('--safe-area-left', '0px');
doc.style.setProperty('--safe-area-right', '0px');
}
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// Set initial viewport dimensions and listen for resize events
setAdminViewportDimensions();
window.addEventListener('resize', setAdminViewportDimensions);
window.addEventListener('orientationchange', () => {
// Add a small delay for orientation change to complete
setTimeout(setAdminViewportDimensions, 100);
});
checkAdminAuth();
initializeAdminMap();
loadCurrentStartLocation();
setupEventListeners();
setupNavigation();
setupMobileMenu();
// Initialize NocoDB links with a small delay to ensure DOM is ready
setTimeout(() => {
loadWalkSheetConfig();
initializeNocodbLinks();
}, 100);
// Check if URL has a hash to show specific section
const hash = window.location.hash;
if (hash === '#walk-sheet') {
showSection('walk-sheet');
checkAndLoadWalkSheetConfig();
} else if (hash === '#convert-data') {
showSection('convert-data');
} else if (hash === '#cuts') {
showSection('cuts');
} else {
// Default to dashboard
showSection('dashboard');
// Load dashboard data on initial page load
loadDashboardData();
}
});
// Add mobile menu functionality
function setupMobileMenu() {
const menuToggle = document.getElementById('mobile-menu-toggle');
const sidebar = document.getElementById('admin-sidebar');
const closeSidebar = document.getElementById('close-sidebar');
const adminNavLinks = document.querySelectorAll('.admin-nav a');
if (menuToggle && sidebar) {
// Toggle menu
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active');
menuToggle.classList.toggle('active');
document.body.classList.toggle('sidebar-open'); // This line already exists
});
// Close sidebar button
if (closeSidebar) {
closeSidebar.addEventListener('click', () => {
sidebar.classList.remove('active');
menuToggle.classList.remove('active');
document.body.classList.remove('sidebar-open');
});
}
// Close sidebar when clicking outside
document.addEventListener('click', (e) => {
if (sidebar.classList.contains('active') &&
!sidebar.contains(e.target) &&
!menuToggle.contains(e.target)) {
sidebar.classList.remove('active');
menuToggle.classList.remove('active');
document.body.classList.remove('sidebar-open');
}
});
// Close sidebar when navigation link is clicked on mobile
adminNavLinks.forEach(link => {
link.addEventListener('click', () => {
if (window.innerWidth <= 768) {
sidebar.classList.remove('active');
menuToggle.classList.remove('active');
document.body.classList.remove('sidebar-open');
}
});
});
}
}
// Check if user is authenticated as admin
async function checkAdminAuth() {
try {
const response = await fetch('/api/auth/check');
const data = await response.json();
console.log('Admin auth check result:', data);
if (!data.authenticated || !data.user?.isAdmin) {
console.log('Redirecting to login - not authenticated or not admin');
window.location.href = '/login.html';
return;
}
console.log('User is authenticated as admin:', data.user);
// Display admin info (desktop)
document.getElementById('admin-info').innerHTML = `
<span>👤 ${escapeHtml(data.user.email)}</span>
<button id="logout-btn" class="btn btn-secondary btn-sm">Logout</button>
`;
// Display admin info (mobile)
const mobileAdminInfo = document.getElementById('mobile-admin-info');
if (mobileAdminInfo) {
mobileAdminInfo.innerHTML = `
<div>👤 ${escapeHtml(data.user.email)}</div>
<button id="mobile-logout-btn" class="btn btn-secondary btn-sm" style="margin-top: 10px; width: 100%;">Logout</button>
`;
// Add logout listener for mobile button
document.getElementById('mobile-logout-btn')?.addEventListener('click', handleLogout);
}
document.getElementById('logout-btn').addEventListener('click', handleLogout);
} catch (error) {
console.error('Auth check failed:', error);
window.location.href = '/login.html';
}
}
// Initialize the admin map
function initializeAdminMap() {
adminMap = L.map('admin-map').setView([53.5461, -113.4938], 11);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
minZoom: 2
}).addTo(adminMap);
// Add crosshair to center of map
const crosshairIcon = L.divIcon({
className: 'crosshair',
iconSize: [20, 20],
html: '<div style="width: 20px; height: 20px; position: relative;"><div style="position: absolute; top: 9px; left: 0; width: 20px; height: 2px; background: #333; box-shadow: 0 0 3px rgba(255,255,255,0.8);"></div><div style="position: absolute; top: 0; left: 9px; width: 2px; height: 20px; background: #333; box-shadow: 0 0 3px rgba(255,255,255,0.8);"></div></div>'
});
const crosshair = L.marker(adminMap.getCenter(), {
icon: crosshairIcon,
interactive: false,
zIndexOffset: 1000
}).addTo(adminMap);
// Update crosshair position when map moves
adminMap.on('move', function() {
crosshair.setLatLng(adminMap.getCenter());
});
// Add click handler to set location
adminMap.on('click', handleMapClick);
// Update coordinates when map moves
adminMap.on('moveend', updateCoordinatesFromMap);
}
// Load current start location
async function loadCurrentStartLocation() {
try {
const response = await fetch('/api/admin/start-location');
const data = await response.json();
if (data.success) {
const { latitude, longitude, zoom } = data.location;
// Update form fields
document.getElementById('start-lat').value = latitude;
document.getElementById('start-lng').value = longitude;
document.getElementById('start-zoom').value = zoom;
// Update map
adminMap.setView([latitude, longitude], zoom);
updateStartMarker(latitude, longitude);
// Show source info
if (data.source) {
const sourceText = data.source === 'database' ? 'Loaded from database' :
data.source === 'environment' ? 'Using environment defaults' :
'Using system defaults';
showStatus(sourceText, 'info');
}
}
} catch (error) {
console.error('Failed to load start location:', error);
showStatus('Failed to load current start location', 'error');
}
}
// Handle map click
function handleMapClick(e) {
const { lat, lng } = e.latlng;
document.getElementById('start-lat').value = lat.toFixed(6);
document.getElementById('start-lng').value = lng.toFixed(6);
updateStartMarker(lat, lng);
}
// Update marker position
function updateStartMarker(lat, lng) {
if (startMarker) {
startMarker.setLatLng([lat, lng]);
} else {
startMarker = L.marker([lat, lng], {
draggable: true,
title: 'Start Location'
}).addTo(adminMap);
// Update coordinates when marker is dragged
startMarker.on('dragend', (e) => {
const position = e.target.getLatLng();
document.getElementById('start-lat').value = position.lat.toFixed(6);
document.getElementById('start-lng').value = position.lng.toFixed(6);
});
}
}
// Update coordinates from current map view
function updateCoordinatesFromMap() {
const center = adminMap.getCenter();
const zoom = adminMap.getZoom();
document.getElementById('start-zoom').value = zoom;
}
// Setup event listeners
function setupEventListeners() {
// Use current view button
const useCurrentViewBtn = document.getElementById('use-current-view');
if (useCurrentViewBtn) {
useCurrentViewBtn.addEventListener('click', () => {
const center = adminMap.getCenter();
const zoom = adminMap.getZoom();
document.getElementById('start-lat').value = center.lat.toFixed(6);
document.getElementById('start-lng').value = center.lng.toFixed(6);
document.getElementById('start-zoom').value = zoom;
updateStartMarker(center.lat, center.lng);
showStatus('Captured current map view', 'success');
});
}
// Save button
const saveLocationBtn = document.getElementById('save-start-location');
if (saveLocationBtn) {
saveLocationBtn.addEventListener('click', saveStartLocation);
}
// Coordinate input changes
const startLatInput = document.getElementById('start-lat');
const startLngInput = document.getElementById('start-lng');
const startZoomInput = document.getElementById('start-zoom');
if (startLatInput) startLatInput.addEventListener('change', updateMapFromInputs);
if (startLngInput) startLngInput.addEventListener('change', updateMapFromInputs);
if (startZoomInput) startZoomInput.addEventListener('change', updateMapFromInputs);
// Walk Sheet buttons
const saveWalkSheetBtn = document.getElementById('save-walk-sheet');
const previewWalkSheetBtn = document.getElementById('preview-walk-sheet');
const printWalkSheetBtn = document.getElementById('print-walk-sheet');
const refreshPreviewBtn = document.getElementById('refresh-preview');
if (saveWalkSheetBtn) saveWalkSheetBtn.addEventListener('click', saveWalkSheetConfig);
if (previewWalkSheetBtn) previewWalkSheetBtn.addEventListener('click', generateWalkSheetPreview);
if (printWalkSheetBtn) printWalkSheetBtn.addEventListener('click', printWalkSheet);
if (refreshPreviewBtn) refreshPreviewBtn.addEventListener('click', generateWalkSheetPreview);
// Auto-update preview on input change
const walkSheetInputs = document.querySelectorAll(
'#walk-sheet-title, #walk-sheet-subtitle, #walk-sheet-footer, ' +
'[id^="qr-code-"][id$="-url"], [id^="qr-code-"][id$="-label"]'
);
walkSheetInputs.forEach(input => {
if (input) {
input.addEventListener('input', debounce(() => {
generateWalkSheetPreview();
}, 500));
}
});
// Add URL change listeners to detect when QR codes need regeneration
for (let i = 1; i <= 3; i++) {
const urlInput = document.getElementById(`qr-code-${i}-url`);
if (urlInput) {
let previousUrl = urlInput.value;
urlInput.addEventListener('change', () => {
const currentUrl = urlInput.value;
if (currentUrl !== previousUrl) {
console.log(`QR Code ${i} URL changed from "${previousUrl}" to "${currentUrl}"`);
// Remove stored QR code so it gets regenerated
delete storedQRCodes[currentUrl];
previousUrl = currentUrl;
generateWalkSheetPreview();
}
});
}
}
// 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', clearShiftForm);
}
// User form submission
const userForm = document.getElementById('create-user-form');
if (userForm) {
userForm.addEventListener('submit', createUser);
}
// Clear user form button
const clearUserBtn = document.getElementById('clear-user-form');
if (clearUserBtn) {
clearUserBtn.addEventListener('click', clearUserForm);
}
// User type change listener
const userTypeSelect = document.getElementById('user-type');
if (userTypeSelect) {
userTypeSelect.addEventListener('change', (e) => {
const expirationGroup = document.getElementById('expiration-group');
const isAdminCheckbox = document.getElementById('user-is-admin');
if (e.target.value === 'temp') {
expirationGroup.style.display = 'block';
isAdminCheckbox.checked = false;
isAdminCheckbox.disabled = true;
} else {
expirationGroup.style.display = 'none';
isAdminCheckbox.disabled = false;
if (e.target.value === 'admin') {
isAdminCheckbox.checked = true;
} else {
isAdminCheckbox.checked = false;
}
}
});
}
}
// Setup navigation between admin sections
function setupNavigation() {
const navLinks = document.querySelectorAll('.admin-nav a');
const sections = document.querySelectorAll('.admin-section');
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetId = link.getAttribute('href').substring(1);
// Update active nav
navLinks.forEach(l => l.classList.remove('active'));
link.classList.add('active');
// Show target section
sections.forEach(section => {
section.style.display = section.id === targetId ? 'block' : 'none';
});
// Update URL hash
window.location.hash = targetId;
// Load section-specific data
if (targetId === 'walk-sheet') {
checkAndLoadWalkSheetConfig();
} else if (targetId === 'dashboard') {
loadDashboardData();
} else if (targetId === 'shifts') {
loadAdminShifts();
} else if (targetId === 'users') {
loadUsers();
} else if (targetId === 'convert-data') {
// Initialize data convert event listeners when section is shown
setTimeout(() => {
if (typeof window.setupDataConvertEventListeners === 'function') {
console.log('Setting up data convert event listeners...');
window.setupDataConvertEventListeners();
} else {
console.error('setupDataConvertEventListeners not found');
}
}, 100);
}
// Close mobile menu if open
const sidebar = document.getElementById('admin-sidebar');
if (sidebar && sidebar.classList.contains('open')) {
sidebar.classList.remove('open');
}
});
});
// Set initial active state based on current hash or default
const currentHash = window.location.hash || '#dashboard';
const activeLink = document.querySelector(`.admin-nav a[href="${currentHash}"]`);
if (activeLink) {
activeLink.classList.add('active');
}
// Also check if we're already on the shifts page (via hash)
const hash = window.location.hash;
if (hash === '#shifts') {
showSection('shifts');
loadAdminShifts();
}
}
// Helper function to show a specific section
function showSection(sectionId) {
const sections = document.querySelectorAll('.admin-section');
const navLinks = document.querySelectorAll('.admin-nav a');
// Hide all sections
sections.forEach(section => {
section.style.display = section.id === sectionId ? 'block' : 'none';
});
// Update active nav
navLinks.forEach(link => {
const linkTarget = link.getAttribute('href').substring(1);
link.classList.toggle('active', linkTarget === sectionId);
});
// Special handling for convert-data section
if (sectionId === 'convert-data') {
// Initialize data convert event listeners when section is shown
setTimeout(() => {
if (typeof window.setupDataConvertEventListeners === 'function') {
console.log('Setting up data convert event listeners from showSection...');
window.setupDataConvertEventListeners();
} else {
console.error('setupDataConvertEventListeners not found in showSection');
}
}, 100);
}
// Special handling for cuts section
if (sectionId === 'cuts') {
// Initialize admin cuts manager when section is shown
setTimeout(() => {
if (typeof window.adminCutsManager === 'object' && window.adminCutsManager.initialize) {
if (!window.adminCutsManager.isInitialized) {
console.log('Initializing admin cuts manager from showSection...');
window.adminCutsManager.initialize().catch(error => {
console.error('Failed to initialize cuts manager:', error);
});
} else {
console.log('Admin cuts manager already initialized');
}
} else {
console.error('adminCutsManager not found in showSection');
}
}, 100);
}
// Special handling for shifts section
if (sectionId === 'shifts') {
console.log('Loading shifts for admin panel...');
loadAdminShifts();
}
}
// Update map from input fields
function updateMapFromInputs() {
const lat = parseFloat(document.getElementById('start-lat').value);
const lng = parseFloat(document.getElementById('start-lng').value);
const zoom = parseInt(document.getElementById('start-zoom').value);
if (!isNaN(lat) && !isNaN(lng) && !isNaN(zoom)) {
adminMap.setView([lat, lng], zoom);
updateStartMarker(lat, lng);
}
}
// Save start location
async function saveStartLocation() {
const lat = parseFloat(document.getElementById('start-lat').value);
const lng = parseFloat(document.getElementById('start-lng').value);
const zoom = parseInt(document.getElementById('start-zoom').value);
// Validate
if (isNaN(lat) || isNaN(lng) || isNaN(zoom)) {
showStatus('Please enter valid coordinates and zoom level', 'error');
return;
}
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
showStatus('Coordinates out of valid range', 'error');
return;
}
if (zoom < 2 || zoom > 19) {
showStatus('Zoom level must be between 2 and 19', 'error');
return;
}
try {
const response = await fetch('/api/admin/start-location', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
latitude: lat,
longitude: lng,
zoom: zoom
})
});
const data = await response.json();
if (data.success) {
showStatus('Start location saved successfully!', 'success');
} else {
throw new Error(data.error || 'Failed to save');
}
} catch (error) {
console.error('Save error:', error);
showStatus(error.message || 'Failed to save start location', 'error');
}
}
// Save walk sheet configuration
async function saveWalkSheetConfig() {
const config = {
walk_sheet_title: document.getElementById('walk-sheet-title')?.value || '',
walk_sheet_subtitle: document.getElementById('walk-sheet-subtitle')?.value || '',
walk_sheet_footer: document.getElementById('walk-sheet-footer')?.value || '',
qr_code_1_url: document.getElementById('qr-code-1-url')?.value || '',
qr_code_1_label: document.getElementById('qr-code-1-label')?.value || '',
qr_code_2_url: document.getElementById('qr-code-2-url')?.value || '',
qr_code_2_label: document.getElementById('qr-code-2-label')?.value || '',
qr_code_3_url: document.getElementById('qr-code-3-url')?.value || '',
qr_code_3_label: document.getElementById('qr-code-3-label')?.value || ''
};
console.log('Saving walk sheet config:', config);
// Show loading state
const saveButton = document.getElementById('save-walk-sheet');
if (!saveButton) {
showStatus('Save button not found', 'error');
return;
}
const originalText = saveButton.textContent;
saveButton.textContent = 'Saving...';
saveButton.disabled = true;
try {
const response = await fetch('/api/admin/walk-sheet-config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
});
const data = await response.json();
console.log('Save response:', data);
if (data.success) {
showStatus('Walk sheet configuration saved successfully!', 'success');
console.log('Configuration saved successfully');
// Don't reload config here - the form already has the latest values
// Just regenerate the preview
generateWalkSheetPreview();
} else {
throw new Error(data.error || 'Failed to save');
}
} catch (error) {
console.error('Save error:', error);
showStatus(error.message || 'Failed to save walk sheet configuration', 'error');
} finally {
saveButton.textContent = originalText;
saveButton.disabled = false;
}
}
// Generate walk sheet preview
function generateWalkSheetPreview() {
const title = document.getElementById('walk-sheet-title')?.value || 'Campaign Walk Sheet';
const subtitle = document.getElementById('walk-sheet-subtitle')?.value || 'Door-to-Door Canvassing Form';
const footer = document.getElementById('walk-sheet-footer')?.value || 'Thank you for your support!';
let previewHTML = `
<div class="ws-header">
<h1 class="ws-title">${escapeHtml(title)}</h1>
<p class="ws-subtitle">${escapeHtml(subtitle)}</p>
</div>
`;
// Add QR codes section
const qrCodesHTML = [];
for (let i = 1; i <= 3; i++) {
const urlInput = document.getElementById(`qr-code-${i}-url`);
const labelInput = document.getElementById(`qr-code-${i}-label`);
const url = urlInput?.value || '';
const label = labelInput?.value || '';
if (url) {
qrCodesHTML.push(`
<div class="ws-qr-item">
<div class="ws-qr-code" id="preview-qr-${i}">
<!-- QR code will be inserted here -->
</div>
<div class="ws-qr-label">${escapeHtml(label || `QR Code ${i}`)}</div>
</div>
`);
}
}
if (qrCodesHTML.length > 0) {
previewHTML += `
<div class="ws-qr-section">
${qrCodesHTML.join('')}
</div>
`;
}
// Add form fields based on the main map form
previewHTML += `
<div class="ws-form-section">
<div class="ws-form-row">
<div class="ws-form-group">
<label class="ws-form-label">First Name</label>
<div class="ws-form-field"></div>
</div>
<div class="ws-form-group">
<label class="ws-form-label">Last Name</label>
<div class="ws-form-field"></div>
</div>
</div>
<div class="ws-form-row">
<div class="ws-form-group">
<label class="ws-form-label">Email</label>
<div class="ws-form-field"></div>
</div>
<div class="ws-form-group">
<label class="ws-form-label">Phone</label>
<div class="ws-form-field"></div>
</div>
</div>
<div class="ws-form-row">
<div class="ws-form-group">
<label class="ws-form-label">Address</label>
<div class="ws-form-field"></div>
</div>
<div class="ws-form-group">
<label class="ws-form-label">Unit Number</label>
<div class="ws-form-field"></div>
</div>
</div>
<div class="ws-form-row">
<div class="ws-form-group">
<label class="ws-form-label">Support Level</label>
<div class="ws-form-field circles">
<span class="ws-circle-option"><span class="ws-circle">1</span></span>
<span class="ws-circle-option"><span class="ws-circle">2</span></span>
<span class="ws-circle-option"><span class="ws-circle">3</span></span>
<span class="ws-circle-option"><span class="ws-circle">4</span></span>
</div>
</div>
<div class="ws-form-group">
<label class="ws-form-label">Sign Request</label>
<div class="ws-form-field circles">
<span class="ws-circle-option"><span class="ws-circle">Y</span> Yes</span>
<span class="ws-circle-option"><span class="ws-circle">N</span> No</span>
</div>
</div>
</div>
<div class="ws-form-row">
<div class="ws-form-group">
<label class="ws-form-label">Sign Size</label>
<div class="ws-form-field circles">
<span class="ws-circle-option"><span class="ws-circle">R</span></span>
<span class="ws-circle-option"><span class="ws-circle">L</span></span>
<span class="ws-circle-option"><span class="ws-circle">U</span></span>
</div>
</div>
<div class="ws-form-group">
<label class="ws-form-label">Visited Date</label>
<div class="ws-form-field"></div>
</div>
</div>
</div>
<div class="ws-notes-section">
<div class="ws-notes-label">Notes & Comments</div>
<div class="ws-notes-area"></div>
</div>
`;
// Add footer
if (footer) {
previewHTML += `
<div class="ws-footer">
${escapeHtml(footer)}
</div>
`;
}
// Update preview
const previewContent = document.getElementById('walk-sheet-preview-content');
if (previewContent) {
previewContent.innerHTML = previewHTML;
// Generate QR codes after DOM is updated
setTimeout(() => {
generatePreviewQRCodes();
}, 100);
} else {
console.warn('Walk sheet preview content container not found');
}
}
// Update the generatePreviewQRCodes function to use smaller size
async function generatePreviewQRCodes() {
for (let i = 1; i <= 3; i++) {
const urlInput = document.getElementById(`qr-code-${i}-url`);
const url = urlInput?.value || '';
const qrContainer = document.getElementById(`preview-qr-${i}`);
if (url && qrContainer) {
try {
// Use our local QR code generation endpoint with size matching display
const qrImageUrl = `/api/qr?text=${encodeURIComponent(url)}&size=200`; // Generate at higher res
qrContainer.innerHTML = `<img src="${qrImageUrl}" alt="QR Code ${i}" style="width: 120px; height: 120px;">`; // Display smaller
} catch (error) {
console.error(`Failed to display QR code ${i}:`, error);
qrContainer.innerHTML = '<div style="width: 120px; height: 120px; border: 2px dashed #ccc; display: flex; align-items: center; justify-content: center; font-size: 14px; color: #999;">QR Error</div>';
}
} else if (qrContainer) {
// Clear empty QR containers
qrContainer.innerHTML = '';
}
}
}
// Print walk sheet
function printWalkSheet() {
// First generate fresh preview to ensure QR codes are generated
generateWalkSheetPreview();
// Wait for QR codes to generate, then print
setTimeout(() => {
const previewContent = document.getElementById('walk-sheet-preview-content');
const clonedContent = previewContent.cloneNode(true);
// Convert canvas elements to images for printing
const canvases = previewContent.querySelectorAll('canvas');
const clonedCanvases = clonedContent.querySelectorAll('canvas');
canvases.forEach((canvas, index) => {
if (canvas && clonedCanvases[index]) {
const img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
img.width = canvas.width;
img.height = canvas.height;
img.style.width = canvas.style.width || `${canvas.width}px`;
img.style.height = canvas.style.height || `${canvas.height}px`;
clonedCanvases[index].parentNode.replaceChild(img, clonedCanvases[index]);
}
});
// Create a print-specific window
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Walk Sheet - Print</title>
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/admin.css">
<style>
@media print {
@page {
size: letter;
margin: 0;
}
body {
margin: 0;
padding: 0;
}
.walk-sheet-page {
width: 8.5in !important;
height: 11in !important;
padding: 0.5in !important;
margin: 0 !important;
box-shadow: none !important;
page-break-after: avoid !important;
transform: none !important;
}
.ws-qr-code img {
width: 100px !important;
height: 100px !important;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
@media screen {
body {
margin: 20px;
background: #f0f0f0;
}
.walk-sheet-page {
width: 8.5in;
height: 11in;
padding: 0.5in;
margin: 0 auto;
background: white;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
}
</style>
</head>
<body>
<div class="walk-sheet-page">
${clonedContent.innerHTML}
</div>
</body>
</html>
`);
printWindow.document.close();
printWindow.onload = function() {
setTimeout(() => {
printWindow.print();
// User can close manually after printing
}, 500);
};
}, 1000); // Give QR codes time to generate
}
// Load walk sheet configuration
async function loadWalkSheetConfig() {
try {
console.log('Loading walk sheet config...');
const response = await fetch('/api/admin/walk-sheet-config');
const data = await response.json();
console.log('Loaded walk sheet config:', data);
if (data.success) {
// The config object contains the actual configuration
const config = data.config || {};
console.log('Config object:', config);
// Populate form fields - use the exact field names from the backend
const titleInput = document.getElementById('walk-sheet-title');
const subtitleInput = document.getElementById('walk-sheet-subtitle');
const footerInput = document.getElementById('walk-sheet-footer');
console.log('Found form elements:', {
title: !!titleInput,
subtitle: !!subtitleInput,
footer: !!footerInput
});
if (titleInput) {
titleInput.value = config.walk_sheet_title || 'Campaign Walk Sheet';
console.log('Set title to:', titleInput.value);
}
if (subtitleInput) {
subtitleInput.value = config.walk_sheet_subtitle || 'Door-to-Door Canvassing Form';
console.log('Set subtitle to:', subtitleInput.value);
}
if (footerInput) {
footerInput.value = config.walk_sheet_footer || 'Thank you for your support!';
console.log('Set footer to:', footerInput.value);
}
// Populate QR code fields
for (let i = 1; i <= 3; i++) {
const urlField = document.getElementById(`qr-code-${i}-url`);
const labelField = document.getElementById(`qr-code-${i}-label`);
console.log(`QR ${i} fields found:`, {
url: !!urlField,
label: !!labelField
});
if (urlField) {
urlField.value = config[`qr_code_${i}_url`] || '';
console.log(`Set QR ${i} URL to:`, urlField.value);
}
if (labelField) {
labelField.value = config[`qr_code_${i}_label`] || '';
console.log(`Set QR ${i} label to:`, labelField.value);
}
}
console.log('Walk sheet config loaded successfully');
// Show status message about data source
if (data.source) {
const sourceText = data.source === 'database' ? 'Walk sheet config loaded from database' :
data.source === 'defaults' ? 'Using walk sheet defaults' :
'Walk sheet config loaded';
showStatus(sourceText, 'info');
}
return true;
} else {
console.error('Failed to load config:', data.error);
showStatus('Failed to load walk sheet configuration', 'error');
return false;
}
} catch (error) {
console.error('Failed to load walk sheet config:', error);
showStatus('Failed to load walk sheet configuration', 'error');
return false;
}
}
// Check if walk sheet section is visible and load config if needed
function checkAndLoadWalkSheetConfig() {
const walkSheetSection = document.getElementById('walk-sheet');
if (walkSheetSection && walkSheetSection.style.display !== 'none') {
console.log('Walk sheet section is visible, loading config...');
loadWalkSheetConfig().then((success) => {
if (success) {
generateWalkSheetPreview();
}
});
}
}
// Add a function to force load config when walk sheet section is accessed
function showWalkSheetSection() {
const walkSheetSection = document.getElementById('walk-sheet');
const startLocationSection = document.getElementById('start-location');
if (startLocationSection) {
startLocationSection.style.display = 'none';
}
if (walkSheetSection) {
walkSheetSection.style.display = 'block';
// Load config after section is shown
setTimeout(() => {
loadWalkSheetConfig().then((success) => {
if (success) {
generateWalkSheetPreview();
}
});
}, 100); // Small delay to ensure DOM is ready
}
}
// Add event listener to trigger config load when walking sheet nav is clicked
document.addEventListener('DOMContentLoaded', function() {
// Add additional event listener for walk sheet nav
const walkSheetNav = document.querySelector('.admin-nav a[href="#walk-sheet"]');
if (walkSheetNav) {
walkSheetNav.addEventListener('click', function(e) {
e.preventDefault();
showWalkSheetSection();
// Update nav state
document.querySelectorAll('.admin-nav a').forEach(link => {
link.classList.remove('active');
});
this.classList.add('active');
});
}
});
// Handle logout
async function handleLogout() {
if (!confirm('Are you sure you want to logout?')) {
return;
}
try {
const response = await fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
window.location.href = '/login.html';
} else {
showStatus('Logout failed. Please try again.', 'error');
}
} catch (error) {
console.error('Logout error:', error);
showStatus('Logout failed. Please try again.', 'error');
}
}
// Show status message
function showStatus(message, type = 'info') {
let container = document.getElementById('status-container');
// Create container if it doesn't exist
if (!container) {
container = document.createElement('div');
container.id = 'status-container';
container.className = 'status-container';
// Ensure proper z-index even if CSS hasn't loaded
container.style.zIndex = '12300';
document.body.appendChild(container);
}
const messageDiv = document.createElement('div');
messageDiv.className = `status-message ${type}`;
messageDiv.textContent = message;
// Add click to dismiss functionality
messageDiv.addEventListener('click', () => {
messageDiv.remove();
});
// Add a small close button for better UX
const closeBtn = document.createElement('span');
closeBtn.innerHTML = ' ×';
closeBtn.style.float = 'right';
closeBtn.style.fontWeight = 'bold';
closeBtn.style.marginLeft = '10px';
closeBtn.style.cursor = 'pointer';
closeBtn.setAttribute('title', 'Click to dismiss');
messageDiv.appendChild(closeBtn);
container.appendChild(messageDiv);
// Auto-remove after 5 seconds
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.remove();
}
}, 5000);
}
// Escape HTML
function escapeHtml(text) {
if (text === null || text === undefined) {
return '';
}
const div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}
// Debounce function for input events
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 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>';
}
showStatus('Failed to load shifts', 'error');
}
} catch (error) {
console.error('Error loading admin shifts:', error);
if (list) {
list.innerHTML = '<p>Error loading shifts</p>';
}
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 = createLocalDate(shift.Date);
const signupCount = shift.signups ? shift.signups.length : 0;
console.log(`Shift "${shift.Title}" (ID: ${shift.ID}) has ${signupCount} volunteers:`, shift.signups?.map(s => s['User Email']) || []);
return `
<div class="shift-admin-item">
<div>
<h4>${escapeHtml(shift.Title)}</h4>
<p>📅 ${shiftDate.toLocaleDateString()} | ⏰ ${shift['Start Time']} - ${shift['End Time']}</p>
<p>📍 ${escapeHtml(shift.Location || 'TBD')}</p>
<p>👥 ${signupCount}/${shift['Max Volunteers']} volunteers</p>
<p class="status-${(shift.Status || 'open').toLowerCase()}">${shift.Status || 'Open'}</p>
</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, "&#39;")}'>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();
}
// Fix the setupShiftActionListeners function
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')) {
const shiftId = e.target.getAttribute('data-shift-id');
const shiftData = JSON.parse(e.target.getAttribute('data-shift').replace(/&#39;/g, "'"));
console.log('Manage volunteers clicked for shift:', shiftId);
showShiftUserModal(shiftId, shiftData);
}
});
}
// Update the deleteShift function (remove window. prefix)
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) {
showStatus('Shift deleted successfully', 'success');
await loadAdminShifts();
console.log('Refreshed shifts list after deleting shift');
} else {
showStatus(data.error || 'Failed to delete shift', 'error');
}
} catch (error) {
console.error('Error deleting shift:', error);
showStatus('Failed to delete shift', 'error');
}
}
// Update editShift function (remove window. prefix)
function editShift(shiftId) {
showStatus('Edit functionality coming soon', 'info');
}
// Add function to create shift
async function createShift(e) {
e.preventDefault();
const formData = {
title: document.getElementById('shift-title').value,
description: document.getElementById('shift-description').value,
date: document.getElementById('shift-date').value,
startTime: document.getElementById('shift-start').value,
endTime: document.getElementById('shift-end').value,
location: document.getElementById('shift-location').value,
maxVolunteers: document.getElementById('shift-max-volunteers').value
};
try {
const response = await fetch('/api/shifts/admin', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const data = await response.json();
if (data.success) {
showStatus('Shift created successfully', 'success');
document.getElementById('shift-form').reset();
await loadAdminShifts();
console.log('Refreshed shifts list after creating new shift');
} else {
showStatus(data.error || 'Failed to create shift', 'error');
}
} catch (error) {
console.error('Error creating shift:', error);
showStatus('Failed to create shift', 'error');
}
}
function clearShiftForm() {
const form = document.getElementById('shift-form');
if (form) {
form.reset();
showStatus('Form cleared', 'info');
}
}
// User Management Functions
async function loadUsers() {
const loadingEl = document.getElementById('users-loading');
const emptyEl = document.getElementById('users-empty');
const tableBody = document.getElementById('users-table-body');
if (loadingEl) loadingEl.style.display = 'block';
if (emptyEl) emptyEl.style.display = 'none';
if (tableBody) tableBody.innerHTML = '';
try {
const response = await fetch('/api/users');
const data = await response.json();
if (loadingEl) loadingEl.style.display = 'none';
if (data.success && data.users) {
displayUsers(data.users);
} else {
throw new Error(data.error || 'Failed to load users');
}
} catch (error) {
console.error('Error loading users:', error);
if (loadingEl) loadingEl.style.display = 'none';
if (emptyEl) {
emptyEl.textContent = 'Failed to load users';
emptyEl.style.display = 'block';
}
showStatus('Failed to load users', 'error');
}
}
function displayUsers(users) {
const container = document.querySelector('.users-list');
if (!container) return;
// Find or create the users table container, preserving the header
let usersTableContainer = container.querySelector('.users-table-container');
if (!usersTableContainer) {
// If container doesn't exist, create it after the header
const header = container.querySelector('.users-list-header');
usersTableContainer = document.createElement('div');
usersTableContainer.className = 'users-table-container';
if (header && header.nextSibling) {
container.insertBefore(usersTableContainer, header.nextSibling);
} else if (header) {
container.appendChild(usersTableContainer);
} else {
container.appendChild(usersTableContainer);
}
}
if (!users || users.length === 0) {
usersTableContainer.innerHTML = '<p class="empty-message">No users found.</p>';
return;
}
const tableHtml = `
<div class="users-table-wrapper">
<table class="users-table">
<thead>
<tr>
<th>Email</th>
<th>Name</th>
<th>Role</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="users-table-body">
${users.map(user => {
const createdDate = user.created_at || user['Created At'] || user.createdAt;
const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A';
const isAdmin = user.admin || user.Admin || false;
const userType = user.UserType || user.userType || (isAdmin ? 'admin' : 'user');
const userId = user.Id || user.id || user.ID;
// Handle expiration info
let expirationInfo = '';
if (user.ExpiresAt) {
const expirationDate = new Date(user.ExpiresAt);
const now = new Date();
const daysUntilExpiration = Math.floor((expirationDate - now) / (1000 * 60 * 60 * 24));
if (daysUntilExpiration < 0) {
expirationInfo = `<span class="expiration-info expiration-warning">Expired ${Math.abs(daysUntilExpiration)} days ago</span>`;
} else if (daysUntilExpiration <= 3) {
expirationInfo = `<span class="expiration-info expiration-warning">Expires in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? 's' : ''}</span>`;
} else {
expirationInfo = `<span class="expiration-info">Expires: ${expirationDate.toLocaleDateString()}</span>`;
}
}
return `
<tr ${user.ExpiresAt && new Date(user.ExpiresAt) < new Date() ? 'class="expired"' : (user.ExpiresAt && new Date(user.ExpiresAt) - new Date() < 3 * 24 * 60 * 60 * 1000 ? 'class="expires-soon"' : '')}>
<td data-label="Email">${escapeHtml(user.email || user.Email || 'N/A')}</td>
<td data-label="Name">${escapeHtml(user.name || user.Name || 'N/A')}</td>
<td data-label="Role">
<span class="user-role ${userType}">
${userType.charAt(0).toUpperCase() + userType.slice(1)}
</span>
${expirationInfo}
</td>
<td data-label="Created">${formattedDate}</td>
<td data-label="Actions">
<div class="user-actions">
<button class="btn btn-secondary send-login-btn" data-user-id="${userId}" data-user-email="${escapeHtml(user.email || user.Email)}">
Send Login Details
</button>
<button class="btn btn-danger delete-user-btn" data-user-id="${userId}" data-user-email="${escapeHtml(user.email || user.Email)}">
Delete
</button>
</div>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
<p id="users-loading" class="loading-message" style="display: none;">Loading...</p>
`;
usersTableContainer.innerHTML = tableHtml;
setupUserActionListeners();
}
function setupUserActionListeners() {
const container = document.querySelector('.users-list');
if (!container) return;
// Remove existing event listeners by cloning the container
const newContainer = container.cloneNode(true);
container.parentNode.replaceChild(newContainer, container);
// Get the updated reference
const updatedContainer = document.querySelector('.users-list');
updatedContainer.addEventListener('click', function(e) {
if (e.target.classList.contains('delete-user-btn')) {
const userId = e.target.getAttribute('data-user-id');
const userEmail = e.target.getAttribute('data-user-email');
console.log('Delete button clicked for user:', userId);
deleteUser(userId, userEmail);
} else if (e.target.classList.contains('send-login-btn')) {
const userId = e.target.getAttribute('data-user-id');
const userEmail = e.target.getAttribute('data-user-email');
console.log('Send login details button clicked for user:', userId);
sendLoginDetailsToUser(userId, userEmail);
} else if (e.target.id === 'email-all-users-btn') {
console.log('Email All Users button clicked');
showEmailUsersModal();
}
});
}
async function deleteUser(userId, userEmail) {
if (!confirm(`Are you sure you want to delete user "${userEmail}"? This action cannot be undone.`)) {
return;
}
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showStatus(`User "${userEmail}" deleted successfully`, 'success');
loadUsers(); // Reload the users list
} else {
throw new Error(data.error || 'Failed to delete user');
}
} catch (error) {
console.error('Error deleting user:', error);
showStatus(`Failed to delete user: ${error.message}`, 'error');
}
}
async function sendLoginDetailsToUser(userId, userEmail) {
if (!confirm(`Send login details to "${userEmail}"?`)) {
return;
}
try {
const response = await fetch(`/api/users/${userId}/send-login-details`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showStatus(`Login details sent to "${userEmail}" successfully`, 'success');
} else {
throw new Error(data.error || 'Failed to send login details');
}
} catch (error) {
console.error('Error sending login details:', error);
showStatus(`Failed to send login details: ${error.message}`, 'error');
}
}
async function createUser(e) {
e.preventDefault();
const email = document.getElementById('user-email').value.trim();
const password = document.getElementById('user-password').value;
const name = document.getElementById('user-name').value.trim();
const userType = document.getElementById('user-type').value;
const expireDays = userType === 'temp' ?
parseInt(document.getElementById('user-expire-days').value) : null;
const admin = document.getElementById('user-is-admin').checked;
if (!email || !password) {
showStatus('Email and password are required', 'error');
return;
}
if (password.length < 6) {
showStatus('Password must be at least 6 characters long', 'error');
return;
}
if (userType === 'temp' && (!expireDays || expireDays < 1 || expireDays > 365)) {
showStatus('Expiration days must be between 1 and 365 for temporary users', 'error');
return;
}
try {
const userData = {
email,
password,
name: name || '',
isAdmin: userType === 'admin' || admin,
userType,
expireDays
};
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
const data = await response.json();
if (data.success) {
showStatus('User created successfully', 'success');
clearUserForm();
loadUsers(); // Reload the users list
} else {
throw new Error(data.error || 'Failed to create user');
}
} catch (error) {
console.error('Error creating user:', error);
showStatus(`Failed to create user: ${error.message}`, 'error');
}
}
function clearUserForm() {
const form = document.getElementById('create-user-form');
if (form) {
form.reset();
// Reset user type to default
const userTypeSelect = document.getElementById('user-type');
if (userTypeSelect) {
userTypeSelect.value = 'user';
}
// Hide expiration group
const expirationGroup = document.getElementById('expiration-group');
if (expirationGroup) {
expirationGroup.style.display = 'none';
}
// Re-enable admin checkbox
const isAdminCheckbox = document.getElementById('user-is-admin');
if (isAdminCheckbox) {
isAdminCheckbox.disabled = false;
}
showStatus('User form cleared', 'info');
}
}
// Email All Users Functions
let allUsersData = [];
async function showEmailUsersModal() {
// Load current users data
try {
const response = await fetch('/api/users');
const data = await response.json();
if (data.success && data.users) {
allUsersData = data.users;
// Update recipients count
const recipientsCount = document.getElementById('recipients-count');
if (recipientsCount) {
recipientsCount.textContent = `${allUsersData.length}`;
}
}
} catch (error) {
console.error('Error loading users for email:', error);
showStatus('Failed to load user data', 'error');
return;
}
// Show modal
const modal = document.getElementById('email-users-modal');
if (modal) {
modal.style.display = 'flex';
// Clear previous content
document.getElementById('email-subject').value = '';
document.getElementById('email-content').innerHTML = '';
document.getElementById('show-preview').checked = false;
document.getElementById('email-preview').style.display = 'none';
}
}
function closeEmailUsersModal() {
const modal = document.getElementById('email-users-modal');
if (modal) {
modal.style.display = 'none';
}
}
function setupRichTextEditor() {
const toolbar = document.querySelector('.rich-text-toolbar');
const editor = document.getElementById('email-content');
if (!toolbar || !editor) return;
// Handle toolbar button clicks
toolbar.addEventListener('click', (e) => {
if (e.target.classList.contains('toolbar-btn')) {
e.preventDefault();
const command = e.target.getAttribute('data-command');
if (command === 'createLink') {
const url = prompt('Enter the URL:');
if (url) {
document.execCommand(command, false, url);
}
} else {
document.execCommand(command, false, null);
}
// Update preview if visible
updateEmailPreview();
}
});
// Update preview on content change
editor.addEventListener('input', updateEmailPreview);
// Handle preview toggle
const showPreviewCheckbox = document.getElementById('show-preview');
if (showPreviewCheckbox) {
showPreviewCheckbox.addEventListener('change', togglePreview);
}
// Update preview when subject changes
const subjectInput = document.getElementById('email-subject');
if (subjectInput) {
subjectInput.addEventListener('input', updateEmailPreview);
}
}
function togglePreview() {
const preview = document.getElementById('email-preview');
const checkbox = document.getElementById('show-preview');
if (preview && checkbox) {
if (checkbox.checked) {
preview.style.display = 'block';
updateEmailPreview();
} else {
preview.style.display = 'none';
}
}
}
function updateEmailPreview() {
const previewSubject = document.getElementById('preview-subject');
const previewBody = document.getElementById('preview-body');
const subjectInput = document.getElementById('email-subject');
const contentEditor = document.getElementById('email-content');
if (previewSubject && subjectInput) {
previewSubject.textContent = subjectInput.value || 'Your subject will appear here';
}
if (previewBody && contentEditor) {
const content = contentEditor.innerHTML || 'Your message will appear here';
previewBody.innerHTML = content;
}
}
async function sendEmailToAllUsers(e) {
e.preventDefault();
const subject = document.getElementById('email-subject').value.trim();
const content = document.getElementById('email-content').innerHTML.trim();
if (!subject) {
showStatus('Please enter an email subject', 'error');
return;
}
if (!content || content === '<br>' || content === '') {
showStatus('Please enter email content', 'error');
return;
}
if (allUsersData.length === 0) {
showStatus('No users found to email', 'error');
return;
}
const confirmMessage = `Send this email to all ${allUsersData.length} users?`;
if (!confirm(confirmMessage)) {
return;
}
// Initialize progress tracking
initializeEmailProgress(allUsersData.length);
try {
const response = await fetch('/api/users/email-all', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
subject: subject,
content: content
})
});
const data = await response.json();
if (data.success) {
// Display detailed results
updateEmailProgress(data.results);
showStatus(data.message, 'success');
console.log('Email results:', data.results);
} else {
showEmailError(data.error || 'Failed to send emails');
if (data.details) {
console.error('Failed email details:', data.details);
}
}
} catch (error) {
console.error('Error sending emails to all users:', error);
showEmailError('Failed to send emails - Network error');
}
}
// Initialize email progress display
function initializeEmailProgress(totalCount) {
const progressContainer = document.getElementById('email-progress-container');
const statusList = document.getElementById('email-status-list');
const pendingCountEl = document.getElementById('pending-count');
const successCountEl = document.getElementById('success-count');
const errorCountEl = document.getElementById('error-count');
const progressBar = document.getElementById('email-progress-bar');
const progressText = document.getElementById('progress-text');
const closeBtn = document.getElementById('close-progress-btn');
// Show progress container
progressContainer.classList.add('show');
// Reset counters
pendingCountEl.textContent = totalCount;
successCountEl.textContent = '0';
errorCountEl.textContent = '0';
// Reset progress bar
progressBar.style.width = '0%';
progressBar.classList.remove('complete', 'error');
progressText.textContent = '0%';
// Clear status list
statusList.innerHTML = '';
// Hide close button initially
closeBtn.style.display = 'none';
// Add status items for each user
allUsersData.forEach(user => {
const statusItem = document.createElement('div');
statusItem.className = 'email-status-item';
statusItem.innerHTML = `
<div class="email-status-recipient">${user.Name || user.Email}</div>
<div class="email-status-result pending">
<div class="progress-spinner"></div>
<span>Sending...</span>
</div>
`;
statusList.appendChild(statusItem);
});
}
// Update progress with results
function updateEmailProgress(results) {
const statusList = document.getElementById('email-status-list');
const pendingCountEl = document.getElementById('pending-count');
const successCountEl = document.getElementById('success-count');
const errorCountEl = document.getElementById('error-count');
const progressBar = document.getElementById('email-progress-bar');
const progressText = document.getElementById('progress-text');
const closeBtn = document.getElementById('close-progress-btn');
const successful = results.successful || [];
const failed = results.failed || [];
const total = results.total || (successful.length + failed.length);
// Update counters
successCountEl.textContent = successful.length;
errorCountEl.textContent = failed.length;
pendingCountEl.textContent = '0';
// Update progress bar
const percentage = ((successful.length + failed.length) / total * 100).toFixed(1);
progressBar.style.width = percentage + '%';
progressText.textContent = percentage + '%';
if (failed.length > 0) {
progressBar.classList.add('error');
} else {
progressBar.classList.add('complete');
}
// Update individual status items
const statusItems = statusList.children;
// Update successful emails
successful.forEach(result => {
const statusItem = Array.from(statusItems).find(item =>
item.querySelector('.email-status-recipient').textContent.includes(result.email) ||
item.querySelector('.email-status-recipient').textContent.includes(result.name)
);
if (statusItem) {
statusItem.querySelector('.email-status-result').innerHTML = `
<span class="email-status-result success"> Sent</span>
`;
}
});
// Update failed emails
failed.forEach(result => {
const statusItem = Array.from(statusItems).find(item =>
item.querySelector('.email-status-recipient').textContent.includes(result.email) ||
item.querySelector('.email-status-recipient').textContent.includes(result.name)
);
if (statusItem) {
statusItem.querySelector('.email-status-result').innerHTML = `
<span class="email-status-result error" title="${result.error || 'Unknown error'}"> Failed</span>
`;
}
});
// Show close button
closeBtn.style.display = 'block';
closeBtn.onclick = () => {
document.getElementById('email-progress-container').classList.remove('show');
closeEmailUsersModal();
};
}
// Show email error
function showEmailError(message) {
const progressContainer = document.getElementById('email-progress-container');
const progressBar = document.getElementById('email-progress-bar');
const progressText = document.getElementById('progress-text');
const closeBtn = document.getElementById('close-progress-btn');
// Show progress container if not visible
progressContainer.classList.add('show');
// Update progress bar to show error
progressBar.style.width = '100%';
progressBar.classList.add('error');
progressText.textContent = 'Error';
// Show close button
closeBtn.style.display = 'block';
closeBtn.onclick = () => {
progressContainer.classList.remove('show');
};
showStatus(message, 'error');
}
// Initialize NocoDB links in admin panel
async function initializeNocodbLinks() {
console.log('Starting NocoDB links initialization...');
try {
// Since we're in the admin panel, the user is already verified as admin
// by the requireAdmin middleware. Let's get the URLs from the server directly.
console.log('Fetching NocoDB URLs for admin panel...');
const configResponse = await fetch('/api/admin/nocodb-urls');
if (!configResponse.ok) {
throw new Error(`NocoDB URLs fetch failed: ${configResponse.status} ${configResponse.statusText}`);
}
const config = await configResponse.json();
console.log('NocoDB URLs received:', config);
if (config.success && config.nocodbUrls) {
console.log('Setting up NocoDB links with URLs:', config.nocodbUrls);
// Set up admin dashboard NocoDB links
setAdminNocodbLink('admin-nocodb-view-link', config.nocodbUrls.viewUrl);
setAdminNocodbLink('admin-nocodb-login-link', config.nocodbUrls.loginSheet);
setAdminNocodbLink('admin-nocodb-settings-link', config.nocodbUrls.settingsSheet);
setAdminNocodbLink('admin-nocodb-shifts-link', config.nocodbUrls.shiftsSheet);
setAdminNocodbLink('admin-nocodb-signups-link', config.nocodbUrls.shiftSignupsSheet);
console.log('NocoDB links initialized in admin panel');
} else {
console.warn('No NocoDB URLs found in admin config response');
// Hide the NocoDB section if no URLs are available
const nocodbSection = document.getElementById('nocodb-links');
const nocodbNav = document.querySelector('.admin-nav a[href="#nocodb-links"]');
if (nocodbSection) {
nocodbSection.style.display = 'none';
console.log('Hidden NocoDB section');
}
if (nocodbNav) {
nocodbNav.style.display = 'none';
console.log('Hidden NocoDB nav link');
}
}
} catch (error) {
console.error('Error initializing NocoDB links in admin panel:', error);
// Hide the NocoDB section on error
const nocodbSection = document.getElementById('nocodb-links');
const nocodbNav = document.querySelector('.admin-nav a[href="#nocodb-links"]');
if (nocodbSection) {
nocodbSection.style.display = 'none';
console.log('Hidden NocoDB section due to error');
}
if (nocodbNav) {
nocodbNav.style.display = 'none';
console.log('Hidden NocoDB nav link due to error');
}
}
}
// Helper function to set admin NocoDB link href
function setAdminNocodbLink(elementId, url) {
console.log(`Setting up NocoDB link: ${elementId} = ${url}`);
const element = document.getElementById(elementId);
if (element && url) {
element.href = url;
element.style.display = 'inline-flex';
// Remove any disabled state
element.classList.remove('btn-disabled');
element.removeAttribute('disabled');
console.log(` Successfully set up ${elementId}`);
} else if (element) {
element.style.display = 'none';
// Add disabled state if no URL
element.classList.add('btn-disabled');
element.setAttribute('disabled', 'disabled');
element.href = '#';
console.log(` Disabled ${elementId} - no URL provided`);
} else {
console.error(` Element not found: ${elementId}`);
}
}
// Shift User Management Functions
let currentShiftData = null;
let allUsers = [];
// Load all users for the dropdown
async function loadAllUsers() {
try {
const response = await fetch('/api/users');
const data = await response.json();
if (data.success) {
allUsers = data.users;
populateUserSelect();
} else {
console.error('Failed to load users:', data.error);
}
} catch (error) {
console.error('Error loading users:', error);
}
}
// Populate user select dropdown
function populateUserSelect() {
const select = document.getElementById('user-select');
if (!select) return;
// Clear existing options except the first one
select.innerHTML = '<option value="">Select a user...</option>';
allUsers.forEach(user => {
const option = document.createElement('option');
option.value = user.email || user.Email;
option.textContent = `${user.name || user.Name || ''} (${user.email || user.Email})`;
select.appendChild(option);
});
}
// Show the shift user management modal
async function showShiftUserModal(shiftId, shiftData) {
currentShiftData = { ...shiftData, ID: shiftId };
// Update modal title and info
document.getElementById('modal-shift-title').textContent = shiftData.Title;
const shiftDate = createLocalDate(shiftData.Date);
document.getElementById('modal-shift-details').textContent =
`${shiftDate.toLocaleDateString()} | ${shiftData['Start Time']} - ${shiftData['End Time']} | ${shiftData.Location || 'TBD'}`;
// Load users if not already loaded
if (allUsers.length === 0) {
await loadAllUsers();
}
// Display current volunteers
displayCurrentVolunteers(shiftData.signups || []);
// Show modal
document.getElementById('shift-user-modal').style.display = 'flex';
}
// Display current volunteers in the modal
function displayCurrentVolunteers(volunteers) {
const container = document.getElementById('current-volunteers-list');
if (!volunteers || volunteers.length === 0) {
container.innerHTML = '<div class="no-volunteers">No volunteers signed up yet.</div>';
return;
}
container.innerHTML = volunteers.map(volunteer => `
<div class="volunteer-item">
<div class="volunteer-info">
<div class="volunteer-name">${escapeHtml(volunteer['User Name'] || volunteer['User Email'] || 'Unknown')}</div>
<div class="volunteer-email">${escapeHtml(volunteer['User Email'])}</div>
</div>
<div class="volunteer-actions">
<button class="btn btn-danger btn-sm remove-volunteer-btn"
data-volunteer-id="${volunteer.ID || volunteer.id}"
data-volunteer-email="${volunteer['User Email']}">
Remove
</button>
</div>
</div>
`).join('');
// Add event listeners for remove buttons
setupVolunteerActionListeners();
}
// Setup event listeners for volunteer actions
function setupVolunteerActionListeners() {
const container = document.getElementById('current-volunteers-list');
container.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-volunteer-btn')) {
const volunteerId = e.target.getAttribute('data-volunteer-id');
const volunteerEmail = e.target.getAttribute('data-volunteer-email');
removeVolunteerFromShift(volunteerId, volunteerEmail);
}
});
}
// Add user to shift
async function addUserToShift() {
const userSelect = document.getElementById('user-select');
const userEmail = userSelect.value;
if (!userEmail) {
showStatus('Please select a user to add', 'error');
return;
}
if (!currentShiftData) {
showStatus('No shift selected', 'error');
return;
}
try {
const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/add-user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ userEmail })
});
const data = await response.json();
if (data.success) {
showStatus('User successfully added to shift', 'success');
userSelect.value = ''; // Clear selection
// Refresh the shift data and reload volunteers
await refreshCurrentShiftData();
console.log('Refreshed shift data after adding user');
} else {
showStatus(data.error || 'Failed to add user to shift', 'error');
}
} catch (error) {
console.error('Error adding user to shift:', error);
showStatus('Failed to add user to shift', 'error');
}
}
// Remove volunteer from shift
async function removeVolunteerFromShift(volunteerId, volunteerEmail) {
if (!confirm(`Are you sure you want to remove ${volunteerEmail} from this shift?`)) {
return;
}
if (!currentShiftData) {
showStatus('No shift selected', 'error');
return;
}
try {
const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/remove-user/${volunteerId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showStatus('Volunteer successfully removed from shift', 'success');
// Refresh the shift data and reload volunteers
await refreshCurrentShiftData();
console.log('Refreshed shift data after removing volunteer');
} else {
showStatus(data.error || 'Failed to remove volunteer from shift', 'error');
}
} catch (error) {
console.error('Error removing volunteer from shift:', error);
showStatus('Failed to remove volunteer from shift', 'error');
}
}
// Refresh current shift data
async function refreshCurrentShiftData() {
if (!currentShiftData) return;
try {
console.log('Refreshing shift data for shift ID:', currentShiftData.ID);
// Reload admin shifts to get updated data
const response = await fetch('/api/shifts/admin');
const data = await response.json();
if (data.success) {
const updatedShift = data.shifts.find(s => s.ID === currentShiftData.ID);
if (updatedShift) {
console.log('Found updated shift with', updatedShift.signups?.length || 0, 'volunteers');
currentShiftData = updatedShift;
displayCurrentVolunteers(updatedShift.signups || []);
// Immediately refresh the main shifts list to show updated counts
console.log('Refreshing main shifts list with', data.shifts.length, 'shifts');
displayAdminShifts(data.shifts);
} else {
console.warn('Could not find updated shift with ID:', currentShiftData.ID);
}
} else {
console.error('Failed to refresh shift data:', data.error);
}
} catch (error) {
console.error('Error refreshing shift data:', error);
}
}
// Close modal
function closeShiftUserModal() {
document.getElementById('shift-user-modal').style.display = 'none';
currentShiftData = null;
// Refresh the main shifts list one more time when closing the modal
// to ensure any changes are reflected
console.log('Refreshing shifts list on modal close');
loadAdminShifts();
}
// Email shift details to all volunteers
async function emailShiftDetails() {
if (!currentShiftData) {
showStatus('No shift selected', 'error');
return;
}
// Check if there are volunteers to email
const volunteers = currentShiftData.signups || [];
if (volunteers.length === 0) {
showStatus('No volunteers signed up for this shift', 'error');
return;
}
// Confirm action
const confirmMessage = `Send shift details email to ${volunteers.length} volunteer${volunteers.length !== 1 ? 's' : ''}?`;
if (!confirm(confirmMessage)) {
return;
}
// Initialize progress tracking for shift emails
initializeShiftEmailProgress(volunteers.length);
try {
const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/email-details`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
// Display detailed results
updateShiftEmailProgress(data.results);
showStatus(data.message, 'success');
console.log('Email results:', data.results);
} else {
showShiftEmailError(data.error || 'Failed to send emails');
if (data.details) {
console.error('Failed email details:', data.details);
}
}
} catch (error) {
console.error('Error sending shift details emails:', error);
showShiftEmailError('Failed to send emails - Network error');
}
}
// Initialize shift email progress display
function initializeShiftEmailProgress(totalCount) {
const progressContainer = document.getElementById('shift-email-progress-container');
const statusList = document.getElementById('shift-email-status-list');
const pendingCountEl = document.getElementById('shift-pending-count');
const successCountEl = document.getElementById('shift-success-count');
const errorCountEl = document.getElementById('shift-error-count');
const progressBar = document.getElementById('shift-email-progress-bar');
const progressText = document.getElementById('shift-progress-text');
const closeBtn = document.getElementById('close-shift-progress-btn');
// Show progress container
progressContainer.classList.add('show');
// Reset counters
pendingCountEl.textContent = totalCount;
successCountEl.textContent = '0';
errorCountEl.textContent = '0';
// Reset progress bar
progressBar.style.width = '0%';
progressBar.classList.remove('complete', 'error');
progressText.textContent = '0%';
// Clear status list
statusList.innerHTML = '';
// Hide close button initially
closeBtn.style.display = 'none';
// Add status items for each volunteer
const volunteers = currentShiftData.signups || [];
volunteers.forEach(volunteer => {
const statusItem = document.createElement('div');
statusItem.className = 'email-status-item';
statusItem.innerHTML = `
<div class="email-status-recipient">${volunteer['User Name'] || volunteer['User Email']}</div>
<div class="email-status-result pending">
<div class="progress-spinner"></div>
<span>Sending...</span>
</div>
`;
statusList.appendChild(statusItem);
});
}
// Update shift email progress with results
function updateShiftEmailProgress(results) {
const statusList = document.getElementById('shift-email-status-list');
const pendingCountEl = document.getElementById('shift-pending-count');
const successCountEl = document.getElementById('shift-success-count');
const errorCountEl = document.getElementById('shift-error-count');
const progressBar = document.getElementById('shift-email-progress-bar');
const progressText = document.getElementById('shift-progress-text');
const closeBtn = document.getElementById('close-shift-progress-btn');
const successful = results.successful || [];
const failed = results.failed || [];
const total = results.total || (successful.length + failed.length);
// Update counters
successCountEl.textContent = successful.length;
errorCountEl.textContent = failed.length;
pendingCountEl.textContent = '0';
// Update progress bar
const percentage = ((successful.length + failed.length) / total * 100).toFixed(1);
progressBar.style.width = percentage + '%';
progressText.textContent = percentage + '%';
if (failed.length > 0) {
progressBar.classList.add('error');
} else {
progressBar.classList.add('complete');
}
// Update individual status items
const statusItems = statusList.children;
// Update successful emails
successful.forEach(result => {
const statusItem = Array.from(statusItems).find(item =>
item.querySelector('.email-status-recipient').textContent.includes(result.email) ||
item.querySelector('.email-status-recipient').textContent.includes(result.name)
);
if (statusItem) {
statusItem.querySelector('.email-status-result').innerHTML = `
<span class="email-status-result success"> Sent</span>
`;
}
});
// Update failed emails
failed.forEach(result => {
const statusItem = Array.from(statusItems).find(item =>
item.querySelector('.email-status-recipient').textContent.includes(result.email) ||
item.querySelector('.email-status-recipient').textContent.includes(result.name)
);
if (statusItem) {
statusItem.querySelector('.email-status-result').innerHTML = `
<span class="email-status-result error" title="${result.error || 'Unknown error'}"> Failed</span>
`;
}
});
// Show close button
closeBtn.style.display = 'block';
closeBtn.onclick = () => {
document.getElementById('shift-email-progress-container').classList.remove('show');
};
}
// Show shift email error
function showShiftEmailError(message) {
const progressContainer = document.getElementById('shift-email-progress-container');
const progressBar = document.getElementById('shift-email-progress-bar');
const progressText = document.getElementById('shift-progress-text');
const closeBtn = document.getElementById('close-shift-progress-btn');
// Show progress container if not visible
progressContainer.classList.add('show');
// Update progress bar to show error
progressBar.style.width = '100%';
progressBar.classList.add('error');
progressText.textContent = 'Error';
// Show close button
closeBtn.style.display = 'block';
closeBtn.onclick = () => {
progressContainer.classList.remove('show');
};
showStatus(message, 'error');
}
// Setup modal event listeners when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
const closeModalBtn = document.getElementById('close-user-modal');
const addUserBtn = document.getElementById('add-user-btn');
const emailShiftDetailsBtn = document.getElementById('email-shift-details-btn');
const modal = document.getElementById('shift-user-modal');
if (closeModalBtn) {
closeModalBtn.addEventListener('click', closeShiftUserModal);
}
if (addUserBtn) {
addUserBtn.addEventListener('click', addUserToShift);
}
if (emailShiftDetailsBtn) {
emailShiftDetailsBtn.addEventListener('click', emailShiftDetails);
}
// Close modal when clicking outside
if (modal) {
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeShiftUserModal();
}
});
}
});
// Setup email users modal event listeners when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Email all users functionality
const closeEmailModalBtn = document.getElementById('close-email-modal');
const cancelEmailBtn = document.getElementById('cancel-email-btn');
const emailUsersForm = document.getElementById('email-users-form');
const emailModal = document.getElementById('email-users-modal');
if (closeEmailModalBtn) {
closeEmailModalBtn.addEventListener('click', closeEmailUsersModal);
}
if (cancelEmailBtn) {
cancelEmailBtn.addEventListener('click', closeEmailUsersModal);
}
if (emailUsersForm) {
emailUsersForm.addEventListener('submit', sendEmailToAllUsers);
}
// Close modal when clicking outside
if (emailModal) {
emailModal.addEventListener('click', function(e) {
if (e.target === emailModal) {
closeEmailUsersModal();
}
});
}
// Setup rich text editor functionality
setupRichTextEditor();
});