const API_BASE = 'https://api.rockvilletollandsda.church/api'; let token = localStorage.getItem('adminToken') || ''; let currentEvents = []; let recurringTypes = []; // Centralized API wrapper with token validation async function apiCall(url, options = {}) { // Add authorization header if token exists const headers = { ...options.headers, ...(token ? { 'Authorization': 'Bearer ' + token } : {}) }; const response = await fetch(url, { ...options, headers }); // Handle token expiration if (response.status === 401) { console.warn('Token expired or invalid, redirecting to login'); handleSessionExpiry(); throw new Error('Session expired'); } return response; } function handleSessionExpiry() { // Clear invalid token token = ''; localStorage.removeItem('adminToken'); // Show login screen document.getElementById('dashboard').classList.add('hidden'); document.getElementById('login').classList.remove('hidden'); // Show user-friendly message showError('loginError', 'Your session has expired. Please log in again.'); // Close any open modals closeModal(); } // Validate token on page load async function validateToken() { if (!token) { return false; } try { // Try a simple API call to validate the token const response = await apiCall(API_BASE + '/admin/events/pending'); return response.ok; } catch (error) { if (error.message === 'Session expired') { return false; } // Network errors or other issues - assume token is still valid console.warn('Could not validate token due to network error:', error); return true; } } function getImageUrl(event) { // Backend stores complete URLs, frontend just uses them directly return event.image || null; } async function loadRecurringTypes() { try { const response = await fetch(API_BASE + '/config/recurring-types'); const data = await response.json(); if (Array.isArray(data)) { // Convert API array to value/label objects, skip 'none' recurringTypes = data.filter(type => type !== 'none').map(type => ({ value: type, // Keep original lowercase value label: formatRecurringTypeLabel(type) })); } else { // Fallback to default types if API fails recurringTypes = [ { value: 'daily', label: 'Daily' }, { value: 'weekly', label: 'Weekly' }, { value: 'biweekly', label: 'Biweekly' }, { value: 'first_tuesday', label: 'First Tuesday' } ]; } } catch (error) { console.error('Failed to load recurring types:', error); // Fallback to default types recurringTypes = [ { value: 'daily', label: 'Daily' }, { value: 'weekly', label: 'Weekly' }, { value: 'biweekly', label: 'Biweekly' }, { value: 'first_tuesday', label: 'First Tuesday' } ]; } } function formatRecurringTypeLabel(type) { switch (type) { case 'daily': return 'Daily'; case 'weekly': return 'Weekly'; case 'biweekly': return 'Every Two Weeks'; case 'monthly': return 'Monthly'; case 'first_tuesday': return 'First Tuesday of Month'; case '2nd_3rd_saturday_monthly': return '2nd/3rd Saturday Monthly'; default: // Capitalize first letter of each word return type.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); } } function generateRecurringTypeOptions(selectedValue) { console.log('generateRecurringTypeOptions called with selectedValue:', selectedValue); console.log('Available recurringTypes:', recurringTypes); let options = ''; recurringTypes.forEach(type => { const selected = selectedValue === type.value ? ' selected' : ''; console.log(`Comparing "${selectedValue}" === "${type.value}": ${selectedValue === type.value}`); options += ''; }); console.log('Generated options HTML:', options); return options; } function handleLogin() { const username = document.getElementById('username').value; const password = document.getElementById('password').value; fetch(API_BASE + '/auth/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ username: username, password: password }) }) .then(response => response.json()) .then(data => { if (data.success && data.data.token) { token = data.data.token; localStorage.setItem('adminToken', token); showDashboard(); loadStats(); } else { showError('loginError', 'Invalid credentials'); } }) .catch(error => { showError('loginError', 'Login failed: ' + error.message); }); } function showDashboard() { document.getElementById('login').classList.add('hidden'); document.getElementById('dashboard').classList.remove('hidden'); loadRecurringTypes(); } function handleLogout() { token = ''; localStorage.removeItem('adminToken'); document.getElementById('dashboard').classList.add('hidden'); document.getElementById('login').classList.remove('hidden'); document.getElementById('password').value = ''; } function showError(elementId, message) { const errorDiv = document.getElementById(elementId); errorDiv.textContent = message; errorDiv.classList.remove('hidden'); setTimeout(() => errorDiv.classList.add('hidden'), 5000); } async function loadStats() { try { // Load pending events count const pendingResponse = await apiCall(API_BASE + '/admin/events/pending'); const pendingData = await pendingResponse.json(); if (pendingData.success) { document.getElementById('pendingCount').textContent = pendingData.data ? pendingData.data.length : 0; } } catch (error) { if (error.message !== 'Session expired') { console.error('Failed to load pending stats:', error); } } try { // Load total events count (this endpoint might not need auth) const totalResponse = await fetch(API_BASE + '/events'); const totalData = await totalResponse.json(); if (totalData.success) { document.getElementById('totalCount').textContent = totalData.data.total || 0; } } catch (error) { console.error('Failed to load total stats:', error); } } async function loadPending() { try { const response = await apiCall(API_BASE + '/admin/events/pending'); const data = await response.json(); if (data.success) { currentEvents = Array.isArray(data.data) ? data.data : []; renderPendingEvents(currentEvents); document.getElementById('pendingCount').textContent = data.data ? data.data.length : 0; } else { showContentError('Failed to load pending events'); } } catch (error) { if (error.message !== 'Session expired') { showContentError('Error loading pending events: ' + error.message); } } } function loadAllEvents() { fetch(API_BASE + '/events') .then(response => response.json()) .then(data => { if (data.success) { const events = data.data.items || []; renderAllEvents(events); } else { showContentError('Failed to load events'); } }) .catch(error => showContentError('Error loading events: ' + error.message)); } function renderPendingEvents(events) { const content = document.getElementById('content'); if (events.length === 0) { content.innerHTML = '
🎉

No pending events!

All caught up. Great job managing your church events!

'; return; } let html = '

📋Pending Events

' + events.length + ' pending
'; events.forEach(event => { const imageUrl = getImageUrl(event); html += '
'; html += '
'; if (imageUrl) { html += 'Event image'; } else { html += '📅'; } html += '
'; html += '
'; html += '

' + escapeHtml(event.title) + '

'; html += '
' + renderHtmlContent(event.description) + '
'; html += '
'; html += '
📅' + formatDate(event.start_time) + '
'; html += '
📍' + escapeHtml(event.location) + '
'; html += '
🏷️' + escapeHtml(event.category) + '
'; if (event.submitter_email) { html += '
✉️' + escapeHtml(event.submitter_email) + '
'; } html += '
'; html += '
'; html += ''; html += ''; html += ''; html += '
'; }); content.innerHTML = html; } function renderAllEvents(events) { const content = document.getElementById('content'); if (events.length === 0) { content.innerHTML = '
📅

No approved events yet.

Events will appear here once you approve pending submissions.

'; return; } let html = '

📅All Events

' + events.length + ' published
'; events.forEach(event => { const imageUrl = getImageUrl(event); html += '
'; html += '
'; if (imageUrl) { html += 'Event image'; } else { html += '📅'; } html += '
'; html += '
'; html += '

' + escapeHtml(event.title) + '

'; html += '
' + renderHtmlContent(event.description) + '
'; html += '
'; html += '
📅' + formatDate(event.start_time) + '
'; html += '
📍' + escapeHtml(event.location) + '
'; html += '
🏷️' + escapeHtml(event.category) + '
'; if (event.is_featured) { html += '
Featured
'; } html += '
'; html += '
'; html += ''; html += ''; html += '
'; }); content.innerHTML = html; } function showApproveModal(eventId) { const event = currentEvents.find(e => e.id === eventId); if (!event) return; document.getElementById('modalTitle').textContent = 'Approve Event'; document.getElementById('modalContent').innerHTML = '

' + escapeHtml(event.title) + '

' + escapeHtml(event.description) + '

'; document.getElementById('modalActions').innerHTML = ''; document.getElementById('modal').classList.remove('hidden'); } function showRejectModal(eventId) { const event = currentEvents.find(e => e.id === eventId); if (!event) return; document.getElementById('modalTitle').textContent = 'Reject Event'; document.getElementById('modalContent').innerHTML = '

' + escapeHtml(event.title) + '

' + escapeHtml(event.description) + '

'; document.getElementById('modalActions').innerHTML = ''; document.getElementById('modal').classList.remove('hidden'); } function closeModal() { document.getElementById('modal').classList.add('hidden'); } function approveEvent(eventId) { const notes = document.getElementById('approveNotes').value; fetch(API_BASE + '/admin/events/pending/' + eventId + '/approve', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ admin_notes: notes || null }) }) .then(response => response.json()) .then(data => { if (data.success) { closeModal(); loadPending(); loadStats(); alert('Event approved successfully!'); } else { alert('Failed to approve event: ' + (data.message || 'Unknown error')); } }) .catch(error => alert('Error approving event: ' + error.message)); } function rejectEvent(eventId) { const notes = document.getElementById('rejectNotes').value; if (!notes.trim()) { alert('Please provide a reason for rejection'); return; } fetch(API_BASE + '/admin/events/pending/' + eventId + '/reject', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ admin_notes: notes }) }) .then(response => response.json()) .then(data => { if (data.success) { closeModal(); loadPending(); loadStats(); alert('Event rejected successfully!'); } else { alert('Failed to reject event: ' + (data.message || 'Unknown error')); } }) .catch(error => alert('Error rejecting event: ' + error.message)); } function deleteEvent(eventId) { if (!confirm('Are you sure you want to delete this pending event?')) return; fetch(API_BASE + '/admin/events/pending/' + eventId, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }) .then(response => response.json()) .then(data => { if (data.success) { loadPending(); loadStats(); alert('Event deleted successfully!'); } else { alert('Failed to delete event: ' + (data.message || 'Unknown error')); } }) .catch(error => alert('Error deleting event: ' + error.message)); } function deleteApprovedEvent(eventId) { if (!confirm('Are you sure you want to delete this approved event?')) return; fetch(API_BASE + '/admin/events/' + eventId, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }) .then(response => response.json()) .then(data => { if (data.success) { loadAllEvents(); loadStats(); alert('Event deleted successfully!'); } else { alert('Failed to delete event: ' + (data.message || 'Unknown error')); } }) .catch(error => alert('Error deleting event: ' + error.message)); } function editEvent(eventId) { fetch(API_BASE + '/events/' + eventId) .then(response => response.json()) .then(data => { if (data.success && data.data) { showEditModal(data.data); } else { alert('Failed to load event details'); } }) .catch(error => alert('Error loading event: ' + error.message)); } function showEditModal(event) { document.getElementById('modalTitle').textContent = 'Edit Event'; const startTime = event.start_time ? new Date(event.start_time).toISOString().slice(0, 16) : ''; const endTime = event.end_time ? new Date(event.end_time).toISOString().slice(0, 16) : ''; const modalHTML = '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '' + '' + '
' + (event.image ? '
Current:
' : '') + '
' + '
' + '
' + '' + '' + '
' + (event.thumbnail ? '
Current:
' : '') + '
' + '
' + '
' + '
'; document.getElementById('modalContent').innerHTML = modalHTML; document.getElementById('modalActions').innerHTML = ''; // Add image preview functionality for both file inputs document.getElementById('editEventImage').addEventListener('change', function(e) { const file = e.target.files[0]; const preview = document.getElementById('editEventImagePreview'); if (file) { const reader = new FileReader(); reader.onload = function(e) { preview.innerHTML = (event.image ? '
Current:
' : '') + '
New:
'; }; reader.readAsDataURL(file); } else { preview.innerHTML = event.image ? '
Current:
' : ''; } }); document.getElementById('editEventThumbnail').addEventListener('change', function(e) { const file = e.target.files[0]; const preview = document.getElementById('editEventThumbnailPreview'); if (file) { const reader = new FileReader(); reader.onload = function(e) { preview.innerHTML = (event.thumbnail ? '
Current:
' : '') + '
New:
'; }; reader.readAsDataURL(file); } else { preview.innerHTML = event.thumbnail ? '
Current:
' : ''; } }); // Populate recurring type dropdown with API options populateRecurringTypeDropdown(event.recurring_type); document.getElementById('modal').classList.remove('hidden'); } function populateRecurringTypeDropdown(currentValue) { const select = document.getElementById('editRecurringType'); if (!select) return; // Clear existing options except the first one (current value) while (select.options.length > 1) { select.removeChild(select.lastChild); } // Add "No Recurrence" option if not already the current value if (currentValue && currentValue !== '') { const noRecurrenceOption = document.createElement('option'); noRecurrenceOption.value = ''; noRecurrenceOption.textContent = 'No Recurrence'; select.appendChild(noRecurrenceOption); } // Add all available recurring types from API recurringTypes.forEach(type => { // Skip if this is already the current value (already shown as first option) if (type.value !== currentValue) { const option = document.createElement('option'); option.value = type.value; option.textContent = type.label; select.appendChild(option); } }); } function saveEventEdit(eventId) { const title = document.getElementById('editTitle').value; const description = document.getElementById('editDescription').value; const location = document.getElementById('editLocation').value; const category = document.getElementById('editCategory').value; const startTime = document.getElementById('editStartTime').value; const endTime = document.getElementById('editEndTime').value; const locationUrl = document.getElementById('editLocationUrl').value; const recurringType = document.getElementById('editRecurringType').value; const imageFile = document.getElementById('editEventImage').files[0]; const thumbnailFile = document.getElementById('editEventThumbnail').files[0]; // Validate required fields if (!title || !description || !location || !category || !startTime || !endTime) { alert('Please fill in all required fields (title, description, location, category, start time, end time)'); return; } const eventData = { title: title.trim(), description: description.trim(), location: location.trim(), category: category, start_time: new Date(startTime).toISOString(), end_time: new Date(endTime).toISOString(), is_featured: document.getElementById('editFeatured').checked }; if (locationUrl && locationUrl.trim()) eventData.location_url = locationUrl.trim(); if (recurringType) { eventData.recurring_type = recurringType; } console.log('Sending event data:', JSON.stringify(eventData, null, 2)); // First update the event text data fetch(API_BASE + '/admin/events/' + eventId, { method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(eventData) }) .then(response => { console.log('Response status:', response.status); if (!response.ok) { return response.text().then(text => { console.error('Error response:', text); throw new Error(`HTTP ${response.status}: ${text}`); }); } return response.json(); }) .then(data => { console.log('Success response:', data); if (data.success) { // Upload images if selected const uploadPromises = []; if (imageFile) { const formData = new FormData(); formData.append('file', imageFile); const imageUpload = fetch(API_BASE + '/upload/events/' + eventId + '/image', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData }); uploadPromises.push(imageUpload); } if (thumbnailFile) { const formData = new FormData(); formData.append('file', thumbnailFile); const thumbnailUpload = fetch(API_BASE + '/upload/events/' + eventId + '/thumbnail', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData }); uploadPromises.push(thumbnailUpload); } if (uploadPromises.length > 0) { Promise.all(uploadPromises) .then(responses => { // Check if any uploads failed const failedUploads = responses.filter(r => !r.ok); if (failedUploads.length > 0) { console.warn('Some image uploads failed'); } closeModal(); loadAllEvents(); if (failedUploads.length === 0) { alert('Event updated with images successfully!'); } else { alert('Event updated, but some image uploads failed. Please try uploading the images again.'); } }) .catch(error => { console.error('Image upload error:', error); closeModal(); loadAllEvents(); alert('Event updated, but image upload failed: ' + error.message); }); } else { closeModal(); loadAllEvents(); alert('Event updated successfully!'); } } else { alert('Failed to update event: ' + (data.message || 'Unknown error')); } }) .catch(error => alert('Error updating event: ' + error.message)); } // SCHEDULE MANAGEMENT FUNCTIONALITY async function showSchedules() { try { const response = await fetch(`${API_BASE}/admin/schedule`, { headers: { 'Authorization': 'Bearer ' + token } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.success && data.data) { renderSchedules(data.data); } else { renderSchedules([]); } } catch (error) { showContentError('Error loading schedules: ' + error.message); } } function renderSchedules(schedules) { const content = document.getElementById('content'); let html = '
' + '

👥Schedule Management

' + '
' + '' + '' + '
' + '
'; if (schedules.length === 0) { html += '
' + '
👥
' + '

No schedules yet.

' + '

Create individual schedules or import from your JSON file.

' + '
'; } else { schedules.forEach(schedule => { html += '
' + '
' + '
' + '

Schedule for ' + formatDate(schedule.date) + '

' + '
' + (schedule.song_leader ? '
🎵Song Leader: ' + escapeHtml(schedule.song_leader) + '
' : '') + (schedule.ss_teacher ? '
📚SS Teacher: ' + escapeHtml(schedule.ss_teacher) + '
' : '') + (schedule.ss_leader ? '
👨‍🏫SS Leader: ' + escapeHtml(schedule.ss_leader) + '
' : '') + (schedule.scripture ? '
📖Scripture: ' + escapeHtml(schedule.scripture) + '
' : '') + (schedule.childrens_story ? '
👶Children\'s Story: ' + escapeHtml(schedule.childrens_story) + '
' : '') + (schedule.sermon_speaker ? '
🎙️Speaker: ' + escapeHtml(schedule.sermon_speaker) + '
' : '') + (schedule.special_music ? '
🎼Special Music: ' + escapeHtml(schedule.special_music) + '
' : '') + '
' + '
' + '
' + '' + '' + '
' + '
' + '
'; }); } content.innerHTML = html; } function showImportScheduleModal() { document.getElementById('modalTitle').textContent = 'Import Schedule from JSON'; document.getElementById('modalContent').innerHTML = '
' + '
' + '' + '' + 'Upload your quarterly schedule JSON file' + '
' + '' + '' + '
'; document.getElementById('modalActions').innerHTML = ''; document.getElementById('modal').classList.remove('hidden'); // Add file change listener for preview document.getElementById('scheduleJsonFile').addEventListener('change', function(e) { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function(e) { try { const data = JSON.parse(e.target.result); showImportPreview(data); } catch (error) { alert('Invalid JSON file: ' + error.message); } }; reader.readAsText(file); } }); } function showImportPreview(data) { const preview = document.getElementById('importPreview'); const content = document.getElementById('importPreviewContent'); if (data.scheduleData && data.scheduleData.length > 0) { let html = '

Found ' + data.scheduleData.length + ' schedule entries

'; html += '
'; data.scheduleData.slice(0, 5).forEach(entry => { const date = new Date(entry.date).toLocaleDateString(); html += '
'; html += '' + date + ': '; const roles = []; if (entry.songLeader) roles.push('Song Leader: ' + entry.songLeader); if (entry.scripture) roles.push('Scripture: ' + entry.scripture); if (entry.sermonSpeaker) roles.push('Speaker: ' + entry.sermonSpeaker); html += roles.length > 0 ? roles.join(', ') : 'No assignments'; html += '
'; }); if (data.scheduleData.length > 5) { html += '

... and ' + (data.scheduleData.length - 5) + ' more entries

'; } html += '
'; content.innerHTML = html; preview.style.display = 'block'; } else { alert('No schedule data found in the JSON file'); } } async function importScheduleFromJson() { const fileInput = document.getElementById('scheduleJsonFile'); const file = fileInput.files[0]; if (!file) { alert('Please select a JSON file first'); return; } const importButton = document.getElementById('importButton'); const progressDiv = document.getElementById('importProgress'); const progressFill = document.getElementById('progressFill'); const progressText = document.getElementById('progressText'); importButton.disabled = true; progressDiv.style.display = 'block'; try { const reader = new FileReader(); reader.onload = async function(e) { try { const data = JSON.parse(e.target.result); if (!data.scheduleData || !Array.isArray(data.scheduleData)) { throw new Error('Invalid JSON format - missing scheduleData array'); } const entries = data.scheduleData; let imported = 0; let errors = 0; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; const progress = ((i + 1) / entries.length) * 100; progressFill.style.width = progress + '%'; progressText.textContent = `Processing ${i + 1} of ${entries.length}...`; try { const scheduleData = { date: new Date(entry.date).toISOString().split('T')[0], song_leader: entry.songLeader || null, ss_teacher: entry.ssTeacher || null, ss_leader: entry.ssLeader || null, mission_story: entry.missionStory || null, special_program: entry.specialProgram || null, sermon_speaker: entry.sermonSpeaker || null, scripture: entry.scripture || null, offering: entry.offering || null, deacons: entry.deacons || null, special_music: entry.specialMusic || null, childrens_story: entry.childrensStory || null, afternoon_program: entry.afternoonProgram || null }; const response = await fetch(API_BASE + '/admin/schedule', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(scheduleData) }); if (response.ok) { imported++; } else { errors++; console.error('Failed to import entry for', scheduleData.date); } } catch (error) { errors++; console.error('Error processing entry:', error); } // Small delay to show progress await new Promise(resolve => setTimeout(resolve, 100)); } progressText.textContent = `Import complete! ${imported} imported, ${errors} errors`; setTimeout(() => { closeModal(); showSchedules(); alert(`Import complete!\n${imported} schedules imported successfully\n${errors} errors occurred`); }, 2000); } catch (error) { progressText.textContent = 'Import failed: ' + error.message; importButton.disabled = false; alert('Import failed: ' + error.message); } }; reader.readAsText(file); } catch (error) { progressText.textContent = 'Error reading file: ' + error.message; importButton.disabled = false; alert('Error reading file: ' + error.message); } } function showCreateScheduleModal() { document.getElementById('modalTitle').textContent = 'Create New Schedule'; document.getElementById('modalContent').innerHTML = `
`; document.getElementById('modalActions').innerHTML = ` `; document.getElementById('modal').classList.remove('hidden'); } function createSchedule() { const date = document.getElementById('scheduleDate').value; if (!date) { alert('Please select a date for the schedule'); return; } const scheduleData = { date: date, song_leader: document.getElementById('songLeader').value || null, ss_teacher: document.getElementById('ssTeacher').value || null, ss_leader: document.getElementById('ssLeader').value || null, mission_story: document.getElementById('missionStory').value || null, scripture: document.getElementById('scripture').value || null, offering: document.getElementById('offering').value || null, special_music: document.getElementById('specialMusic').value || null, childrens_story: document.getElementById('childrensStory').value || null, sermon_speaker: document.getElementById('sermonSpeaker').value || null }; fetch(API_BASE + '/admin/schedule', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(scheduleData) }) .then(response => response.json()) .then(data => { if (data.success) { closeModal(); showSchedules(); alert('Schedule created successfully!'); } else { alert('Failed to create schedule: ' + (data.message || 'Unknown error')); } }) .catch(error => alert('Error creating schedule: ' + error.message)); } function editSchedule(scheduleId) { fetch(API_BASE + '/admin/schedule', { headers: { 'Authorization': 'Bearer ' + token } }) .then(response => response.json()) .then(data => { if (data.success && data.data) { const schedule = data.data.find(s => s.id === scheduleId); if (schedule) { showEditScheduleModal(schedule); } else { alert('Schedule not found'); } } else { alert('Failed to load schedule details'); } }) .catch(error => alert('Error loading schedule: ' + error.message)); } function showEditScheduleModal(schedule) { document.getElementById('modalTitle').textContent = 'Edit Schedule'; // Format the date properly for HTML date input (YYYY-MM-DD) let formattedDate = ''; if (schedule.date) { if (schedule.date.includes('T')) { formattedDate = schedule.date.split('T')[0]; } else { formattedDate = schedule.date; } } document.getElementById('modalContent').innerHTML = `
`; document.getElementById('modalActions').innerHTML = ` `; document.getElementById('modal').classList.remove('hidden'); } function saveScheduleEdit(scheduleId) { const date = document.getElementById('editScheduleDate').value; if (!date) { alert('Please select a date for the schedule'); return; } const scheduleData = { date: date, song_leader: document.getElementById('editSongLeader').value || null, ss_teacher: document.getElementById('editSsTeacher').value || null, ss_leader: document.getElementById('editSsLeader').value || null, mission_story: document.getElementById('editMissionStory').value || null, scripture: document.getElementById('editScripture').value || null, offering: document.getElementById('editOffering').value || null, special_music: document.getElementById('editSpecialMusic').value || null, childrens_story: document.getElementById('editChildrensStory').value || null, sermon_speaker: document.getElementById('editSermonSpeaker').value || null }; fetch(API_BASE + '/admin/schedule/' + date, { method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(scheduleData) }) .then(response => response.json()) .then(data => { if (data.success) { closeModal(); showSchedules(); alert('Schedule updated successfully!'); } else { alert('Failed to update schedule: ' + (data.message || 'Unknown error')); } }) .catch(error => alert('Error updating schedule: ' + error.message)); } function deleteSchedule(scheduleId) { if (!confirm('Are you sure you want to delete this schedule? This action cannot be undone.')) return; // We need to find the date for this schedule ID fetch(API_BASE + '/admin/schedule', { headers: { 'Authorization': 'Bearer ' + token } }) .then(response => response.json()) .then(data => { if (data.success && data.data) { const schedule = data.data.find(s => s.id === scheduleId); if (schedule) { return fetch(API_BASE + '/admin/schedule/' + schedule.date, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }); } else { throw new Error('Schedule not found'); } } else { throw new Error('Failed to load schedules'); } }) .then(response => response.json()) .then(data => { if (data.success) { showSchedules(); alert('Schedule deleted successfully!'); } else { alert('Failed to delete schedule: ' + (data.message || 'Unknown error')); } }) .catch(error => alert('Error deleting schedule: ' + error.message)); } // BULLETINS FUNCTIONALITY async function showBulletins() { try { // Fetch all bulletins with pagination let allBulletins = []; let page = 1; let hasMore = true; while (hasMore) { const response = await fetch(`${API_BASE}/bulletins?page=${page}&per_page=50`, { headers: token ? { 'Authorization': 'Bearer ' + token } : {} }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.success && data.data && data.data.items) { allBulletins = allBulletins.concat(data.data.items); hasMore = data.data.has_more; page++; } else { hasMore = false; if (!data.success) { throw new Error(data.message || 'API returned success: false'); } } } renderBulletins(allBulletins); } catch (error) { showContentError('Error loading bulletins: ' + error.message); } } function renderBulletins(bulletins) { const content = document.getElementById('content'); if (bulletins.length === 0) { content.innerHTML = '
' + '
🗞️
' + '

No bulletins yet.

' + '

Create your first church bulletin to get started.

' + '' + '
'; return; } let html = '

🗞️Church Bulletins

'; bulletins.forEach(bulletin => { // Create a content preview from available sections let contentPreview = ''; const sections = [bulletin.sabbath_school, bulletin.divine_worship, bulletin.scripture_reading]; const availableContent = sections.find(section => section && section.trim()); if (availableContent) { // Strip HTML tags and get first 150 characters contentPreview = stripHtmlTags(availableContent).substring(0, 150); } else { contentPreview = 'Church bulletin for ' + (bulletin.date || 'Unknown date'); } const showMore = availableContent && availableContent.length > 150 ? '...' : ''; html += '
' + '
' + '
' + (bulletin.cover_image ? 'Bulletin cover image' : '🗞️' ) + '
' + '
' + '

' + escapeHtml(bulletin.title || 'Church Bulletin') + '

' + '

' + escapeHtml(contentPreview) + showMore + '

' + '
' + '
📅' + formatDate(bulletin.date || bulletin.created_at) + '
' + '
📄' + (bulletin.pdf_path ? 'PDF Available' : 'No PDF') + '
' + '
📝' + (bulletin.is_active ? 'Active' : 'Inactive') + '
' + '
' + '
' + '
' + '' + '' + '' + '
' + '
' + '
'; }); content.innerHTML = html; } function viewBulletin(bulletinId) { fetch(API_BASE + '/bulletins/' + bulletinId) .then(response => response.json()) .then(data => { if (data.success && data.data) { showViewBulletinModal(data.data); } else { alert('Failed to load bulletin details'); } }) .catch(error => alert('Error loading bulletin: ' + error.message)); } function showViewBulletinModal(bulletin) { document.getElementById('modalTitle').textContent = bulletin.title || 'Church Bulletin'; let modalContent = '
'; // Display cover image if available if (bulletin.cover_image) { modalContent += '
Bulletin cover image
'; } // Display bulletin date if (bulletin.date) { modalContent += '

📅 Date

' + formatDate(bulletin.date) + '

'; } // Display Sabbath School section if (bulletin.sabbath_school) { modalContent += '

📚 Sabbath School

' + renderHtmlContent(bulletin.sabbath_school) + '
'; } // Display Divine Worship section if (bulletin.divine_worship) { modalContent += '

⛪ Divine Worship

' + renderHtmlContent(bulletin.divine_worship) + '
'; } // Display Scripture Reading if (bulletin.scripture_reading) { modalContent += '

📖 Scripture Reading

' + renderHtmlContent(bulletin.scripture_reading) + '
'; } // Display Sunset times if (bulletin.sunset) { modalContent += '

🌅 Sunset

' + renderHtmlContent(bulletin.sunset) + '
'; } // PDF download link if (bulletin.pdf_file || bulletin.pdf_path) { let pdfUrl; if (bulletin.pdf_path) { // If pdf_path already contains the full URL, use it as-is pdfUrl = bulletin.pdf_path.startsWith('http') ? bulletin.pdf_path : 'https://api.rockvilletollandsda.church/' + bulletin.pdf_path.replace(/^\/+/, ''); } else { // For pdf_file, construct the URL pdfUrl = 'https://api.rockvilletollandsda.church/uploads/bulletins/' + bulletin.pdf_file; } modalContent += '

📄 PDF Download

Download PDF

'; } modalContent += '
'; document.getElementById('modalContent').innerHTML = modalContent; document.getElementById('modalActions').innerHTML = ''; document.getElementById('modal').classList.remove('hidden'); } function showCreateBulletinModal() { document.getElementById('modalTitle').textContent = 'Create New Bulletin'; document.getElementById('modalContent').innerHTML = '
' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '
' + '' + '
' + '
'; document.getElementById('modalActions').innerHTML = ''; // Add image preview functionality for file input document.getElementById('bulletinCoverImage').addEventListener('change', function(e) { const file = e.target.files[0]; const preview = document.getElementById('bulletinCoverImagePreview'); if (file) { const reader = new FileReader(); reader.onload = function(e) { preview.innerHTML = ''; }; reader.readAsDataURL(file); } else { preview.innerHTML = ''; } }); document.getElementById('modal').classList.remove('hidden'); } function createBulletin() { const title = document.getElementById('bulletinTitle').value; const date = document.getElementById('bulletinDate').value; const sabbathSchool = document.getElementById('bulletinSabbathSchool').value; const divineWorship = document.getElementById('bulletinDivineWorship').value; const scripture = document.getElementById('bulletinScripture').value; const sunset = document.getElementById('bulletinSunset').value; const isActive = document.getElementById('bulletinIsActive').checked; const coverImageFile = document.getElementById('bulletinCoverImage').files[0]; if (!title.trim()) { alert('Please enter a title for the bulletin'); return; } if (!date) { alert('Please select a date for the bulletin'); return; } const bulletinData = { title: title, date: date, is_active: isActive }; // Add optional sections if they have content if (sabbathSchool.trim()) bulletinData.sabbath_school = sabbathSchool; if (divineWorship.trim()) bulletinData.divine_worship = divineWorship; if (scripture.trim()) bulletinData.scripture_reading = scripture; if (sunset.trim()) bulletinData.sunset = sunset; // First create the bulletin fetch(API_BASE + '/admin/bulletins', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(bulletinData) }) .then(response => response.json()) .then(data => { if (data.success) { const bulletinId = data.data.id; // If cover image is selected, upload it if (coverImageFile) { const formData = new FormData(); formData.append('file', coverImageFile); console.log('Uploading cover image:', coverImageFile.name, 'to bulletin:', bulletinId); return fetch(API_BASE + '/upload/bulletins/' + bulletinId + '/cover', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData }) .then(response => { console.log('Upload response status:', response.status); if (!response.ok) { return response.text().then(text => { console.error('Upload error response:', text); throw new Error(`HTTP ${response.status}: ${text}`); }); } return response.json(); }) .then(uploadData => { console.log('Upload success:', uploadData); if (uploadData.success) { closeModal(); showBulletins(); alert('Bulletin created with cover image successfully!'); } else { alert('Bulletin created but cover image upload failed: ' + (uploadData.message || 'Unknown error')); closeModal(); showBulletins(); } }) .catch(error => { console.error('Upload error:', error); alert('Bulletin created but cover image upload failed: ' + error.message); closeModal(); showBulletins(); }); } else { closeModal(); showBulletins(); alert('Bulletin created successfully!'); } } else { alert('Failed to create bulletin: ' + (data.message || 'Unknown error')); } }) .catch(error => alert('Error creating bulletin: ' + error.message)); } function editBulletin(bulletinId) { fetch(API_BASE + '/bulletins/' + bulletinId) .then(response => response.json()) .then(data => { if (data.success && data.data) { showEditBulletinModal(data.data); } else { alert('Failed to load bulletin details'); } }) .catch(error => alert('Error loading bulletin: ' + error.message)); } function showEditBulletinModal(bulletin) { document.getElementById('modalTitle').textContent = 'Edit Bulletin'; document.getElementById('modalContent').innerHTML = '
' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + (bulletin.cover_image ? '
Current:
' : '') + '
' + '
' + '
' + '' + '
' + '
'; document.getElementById('modalActions').innerHTML = ''; // Add image preview functionality for file input document.getElementById('editBulletinCoverImage').addEventListener('change', function(e) { const file = e.target.files[0]; const preview = document.getElementById('editBulletinCoverImagePreview'); if (file) { const reader = new FileReader(); reader.onload = function(e) { preview.innerHTML = (bulletin.cover_image ? '
Current:
' : '') + '
New:
'; }; reader.readAsDataURL(file); } else { // If no file selected, show only original image if it exists preview.innerHTML = bulletin.cover_image ? '
Current:
' : ''; } }); document.getElementById('modal').classList.remove('hidden'); } function saveBulletinEdit(bulletinId) { const title = document.getElementById('editBulletinTitle').value; const date = document.getElementById('editBulletinDate').value; const sabbathSchool = document.getElementById('editBulletinSabbathSchool').value; const divineWorship = document.getElementById('editBulletinDivineWorship').value; const scripture = document.getElementById('editBulletinScripture').value; const sunset = document.getElementById('editBulletinSunset').value; const isActive = document.getElementById('editBulletinIsActive').checked; const coverImageFile = document.getElementById('editBulletinCoverImage').files[0]; if (!title.trim()) { alert('Please enter a title for the bulletin'); return; } if (!date) { alert('Please select a date for the bulletin'); return; } const bulletinData = { title: title, date: date, is_active: isActive, sabbath_school: sabbathSchool || '', divine_worship: divineWorship || '', scripture_reading: scripture || '', sunset: sunset || '' }; // First update the bulletin text content fetch(API_BASE + '/admin/bulletins/' + bulletinId, { method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(bulletinData) }) .then(response => response.json()) .then(data => { if (data.success) { // If cover image is selected, upload it if (coverImageFile) { const formData = new FormData(); formData.append('file', coverImageFile); console.log('Uploading cover image:', coverImageFile.name, 'to bulletin:', bulletinId); return fetch(API_BASE + '/upload/bulletins/' + bulletinId + '/cover', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData }) .then(response => { console.log('Upload response status:', response.status); if (!response.ok) { return response.text().then(text => { console.error('Upload error response:', text); throw new Error(`HTTP ${response.status}: ${text}`); }); } return response.json(); }) .then(uploadData => { console.log('Upload success:', uploadData); if (uploadData.success) { closeModal(); showBulletins(); alert('Bulletin updated with new cover image successfully!'); } else { alert('Bulletin updated but cover image upload failed: ' + (uploadData.message || 'Unknown error')); closeModal(); showBulletins(); } }) .catch(error => { console.error('Upload error:', error); alert('Bulletin updated but cover image upload failed: ' + error.message); closeModal(); showBulletins(); }); } else { closeModal(); showBulletins(); alert('Bulletin updated successfully!'); } } else { alert('Failed to update bulletin: ' + (data.message || 'Unknown error')); } }) .catch(error => alert('Error updating bulletin: ' + error.message)); } function deleteBulletin(bulletinId) { if (!confirm('Are you sure you want to delete this bulletin? This action cannot be undone.')) return; fetch(API_BASE + '/admin/bulletins/' + bulletinId, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }) .then(response => response.json()) .then(data => { if (data.success) { showBulletins(); alert('Bulletin deleted successfully!'); } else { alert('Failed to delete bulletin: ' + (data.message || 'Unknown error')); } }) .catch(error => alert('Error deleting bulletin: ' + error.message)); } function showContentError(message) { document.getElementById('content').innerHTML = '

⚠️ ' + message + '

'; } function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function renderHtmlContent(html) { if (!html) return ''; return html .replace(/

/g, '

') .replace(/<\/p>/g, '

') .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&'); } function stripHtmlTags(html) { if (!html) return ''; // Create a temporary div to parse HTML and extract text content const div = document.createElement('div'); div.innerHTML = html; return div.textContent || div.innerText || ''; } function formatDate(dateString) { if (!dateString) return 'N/A'; try { // Just display the date/time as-is from the database without timezone conversion if (dateString.includes('T')) { // For datetime strings like "2025-06-14T19:00:00.000Z" // Just remove the T and Z and show readable format const [datePart, timePart] = dateString.split('T'); const cleanTime = timePart.replace(/\.\d{3}Z?$/, ''); // Remove milliseconds and Z return `${datePart} ${cleanTime}`; } // If it's already just a date, return as-is return dateString; } catch { return dateString; } } // Initialize app with token validation async function initializeApp() { if (token) { console.log('Found stored token, validating...'); const isValid = await validateToken(); if (isValid) { console.log('Token is valid, showing dashboard'); showDashboard(); loadStats(); } else { console.log('Token is invalid, showing login'); // Token is invalid, clear it and show login token = ''; localStorage.removeItem('adminToken'); } } if (!token) { // Load recurring types even when not logged in, so they're ready when needed loadRecurringTypes(); } } // Start the app initializeApp(); // Event listeners document.addEventListener('keydown', function(event) { if (event.key === 'Escape') { closeModal(); } }); document.getElementById('modal').addEventListener('click', function(event) { if (event.target === this) { closeModal(); } });