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 = 'No Recurrence ';
recurringTypes.forEach(type => {
const selected = selectedValue === type.value ? ' selected' : '';
console.log(`Comparing "${selectedValue}" === "${type.value}": ${selectedValue === type.value}`);
options += '' + type.label + ' ';
});
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 = '';
events.forEach(event => {
const imageUrl = getImageUrl(event);
html += '';
html += '
';
if (imageUrl) {
html += '
';
} else {
html += '📅';
}
html += '
';
html += '
';
html += '
' + escapeHtml(event.title) + ' ';
html += '
' + renderHtmlContent(event.description) + '
';
html += '
';
html += '
';
html += '✓ Approve ';
html += '✗ Reject ';
html += '🗑️ Delete ';
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 = '';
events.forEach(event => {
const imageUrl = getImageUrl(event);
html += '';
html += '
';
if (imageUrl) {
html += '
';
} else {
html += '📅';
}
html += '
';
html += '
';
html += '
' + escapeHtml(event.title) + ' ';
html += '
' + renderHtmlContent(event.description) + '
';
html += '
';
html += '
';
html += '✏️ Edit ';
html += '🗑️ Delete ';
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 = '';
document.getElementById('modalActions').innerHTML = '✓ Approve Event ';
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 = '';
document.getElementById('modalActions').innerHTML = '✗ Reject Event ';
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 = '';
document.getElementById('modalContent').innerHTML = modalHTML;
document.getElementById('modalActions').innerHTML = '💾 Save Changes ';
// 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 = '';
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) + ' ' +
'
' +
'
' +
'
' +
'✏️ Edit ' +
'🗑️ Delete ' +
'
' +
'
' +
'
';
});
}
content.innerHTML = html;
}
function showImportScheduleModal() {
document.getElementById('modalTitle').textContent = 'Import Schedule from JSON';
document.getElementById('modalContent').innerHTML =
'';
document.getElementById('modalActions').innerHTML =
'' +
'📁 Import Schedules' +
' ';
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 = `
✨ Create Schedule
`;
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,
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 = `
💾 Save Changes
`;
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,
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.
' +
'
✨ Create First Bulletin' +
'
';
return;
}
let html = '';
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 ?
'
' :
'🗞️'
) +
'
' +
'
' +
'
' + escapeHtml(bulletin.title || 'Church Bulletin') + ' ' +
'
' + escapeHtml(contentPreview) + showMore + '
' +
'
' +
'
' +
'
' +
'👁️ View ' +
'✏️ Edit ' +
'🗑️ Delete ' +
'
' +
'
' +
'
';
});
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 = '';
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 =
'' +
'✨ Create Bulletin' +
' ';
// 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 =
'';
document.getElementById('modalActions').innerHTML =
'' +
'💾 Save Changes' +
' ';
// 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 = '';
}
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();
}
});