1827 lines
77 KiB
JavaScript
1827 lines
77 KiB
JavaScript
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 = '<option value=""' + (!selectedValue ? ' selected' : '') + '>No Recurrence</option>';
|
|
|
|
recurringTypes.forEach(type => {
|
|
const selected = selectedValue === type.value ? ' selected' : '';
|
|
console.log(`Comparing "${selectedValue}" === "${type.value}": ${selectedValue === type.value}`);
|
|
options += '<option value="' + type.value + '"' + selected + '>' + type.label + '</option>';
|
|
});
|
|
|
|
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 = '<div class="empty-state"><div class="empty-icon success">🎉</div><p class="empty-title">No pending events!</p><p>All caught up. Great job managing your church events!</p></div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="content-header"><h2 class="content-title"><span>📋</span>Pending Events</h2><div class="content-badge pending">' + events.length + ' pending</div></div>';
|
|
|
|
events.forEach(event => {
|
|
const imageUrl = getImageUrl(event);
|
|
html += '<div class="event-item"><div class="event-content">';
|
|
html += '<div class="event-image">';
|
|
if (imageUrl) {
|
|
html += '<img src="' + escapeHtml(imageUrl) + '" alt="Event image" onerror="this.parentElement.innerHTML=\'📅\'">';
|
|
} else {
|
|
html += '📅';
|
|
}
|
|
html += '</div>';
|
|
html += '<div class="event-details">';
|
|
html += '<h3 class="event-title">' + escapeHtml(event.title) + '</h3>';
|
|
html += '<div class="event-description">' + renderHtmlContent(event.description) + '</div>';
|
|
html += '<div class="event-meta">';
|
|
html += '<div class="meta-item"><span>📅</span>' + formatDate(event.start_time) + '</div>';
|
|
html += '<div class="meta-item"><span>📍</span>' + escapeHtml(event.location) + '</div>';
|
|
html += '<div class="meta-item"><span>🏷️</span><span class="category-badge pending">' + escapeHtml(event.category) + '</span></div>';
|
|
if (event.submitter_email) {
|
|
html += '<div class="meta-item"><span>✉️</span>' + escapeHtml(event.submitter_email) + '</div>';
|
|
}
|
|
html += '</div></div>';
|
|
html += '<div class="event-actions">';
|
|
html += '<button onclick="showApproveModal(\'' + event.id + '\')" class="btn-action btn-approve"><span>✓</span>Approve</button>';
|
|
html += '<button onclick="showRejectModal(\'' + event.id + '\')" class="btn-action btn-reject"><span>✗</span>Reject</button>';
|
|
html += '<button onclick="deleteEvent(\'' + event.id + '\')" class="btn-action btn-delete"><span>🗑️</span>Delete</button>';
|
|
html += '</div></div></div>';
|
|
});
|
|
|
|
content.innerHTML = html;
|
|
}
|
|
|
|
function renderAllEvents(events) {
|
|
const content = document.getElementById('content');
|
|
|
|
if (events.length === 0) {
|
|
content.innerHTML = '<div class="empty-state"><div class="empty-icon info">📅</div><p class="empty-title">No approved events yet.</p><p>Events will appear here once you approve pending submissions.</p></div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="content-header"><h2 class="content-title"><span>📅</span>All Events</h2><div class="content-badge total">' + events.length + ' published</div></div>';
|
|
|
|
events.forEach(event => {
|
|
const imageUrl = getImageUrl(event);
|
|
html += '<div class="event-item"><div class="event-content">';
|
|
html += '<div class="event-image">';
|
|
if (imageUrl) {
|
|
html += '<img src="' + escapeHtml(imageUrl) + '" alt="Event image" onerror="this.parentElement.innerHTML=\'📅\'">';
|
|
} else {
|
|
html += '📅';
|
|
}
|
|
html += '</div>';
|
|
html += '<div class="event-details">';
|
|
html += '<h3 class="event-title">' + escapeHtml(event.title) + '</h3>';
|
|
html += '<div class="event-description">' + renderHtmlContent(event.description) + '</div>';
|
|
html += '<div class="event-meta">';
|
|
html += '<div class="meta-item"><span>📅</span>' + formatDate(event.start_time) + '</div>';
|
|
html += '<div class="meta-item"><span>📍</span>' + escapeHtml(event.location) + '</div>';
|
|
html += '<div class="meta-item"><span>🏷️</span><span class="category-badge approved">' + escapeHtml(event.category) + '</span></div>';
|
|
if (event.is_featured) {
|
|
html += '<div class="meta-item"><span>⭐</span><span class="category-badge featured">Featured</span></div>';
|
|
}
|
|
html += '</div></div>';
|
|
html += '<div class="event-actions">';
|
|
html += '<button onclick="editEvent(\'' + event.id + '\')" class="btn-action btn-edit"><span>✏️</span>Edit</button>';
|
|
html += '<button onclick="deleteApprovedEvent(\'' + event.id + '\')" class="btn-action btn-delete"><span>🗑️</span>Delete</button>';
|
|
html += '</div></div></div>';
|
|
});
|
|
|
|
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 = '<div class="form-grid"><div><h4>' + escapeHtml(event.title) + '</h4><p>' + escapeHtml(event.description) + '</p></div><div><label for="approveNotes">Admin Notes (optional)</label><textarea id="approveNotes" rows="3" placeholder="Add any notes for this approval..."></textarea></div></div>';
|
|
document.getElementById('modalActions').innerHTML = '<button onclick="approveEvent(\'' + eventId + '\')" class="btn-approve"><span>✓</span>Approve Event</button>';
|
|
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 = '<div class="form-grid"><div><h4>' + escapeHtml(event.title) + '</h4><p>' + escapeHtml(event.description) + '</p></div><div><label for="rejectNotes">Reason for rejection *</label><textarea id="rejectNotes" rows="3" placeholder="Please explain why this event is being rejected..." required></textarea></div></div>';
|
|
document.getElementById('modalActions').innerHTML = '<button onclick="rejectEvent(\'' + eventId + '\')" class="btn-reject"><span>✗</span>Reject Event</button>';
|
|
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 = '<div class="form-grid">' +
|
|
'<div class="form-grid cols-2">' +
|
|
'<div><label for="editTitle">Title</label><input id="editTitle" type="text" value="' + escapeHtml(event.title) + '"></div>' +
|
|
'<div><label for="editCategory">Category</label><select id="editCategory">' +
|
|
'<option value="Service"' + (event.category === 'Service' ? ' selected' : '') + '>Service</option>' +
|
|
'<option value="Ministry"' + (event.category === 'Ministry' ? ' selected' : '') + '>Ministry</option>' +
|
|
'<option value="Social"' + (event.category === 'Social' ? ' selected' : '') + '>Social</option>' +
|
|
'<option value="Other"' + (event.category === 'Other' ? ' selected' : '') + '>Other</option>' +
|
|
'</select></div>' +
|
|
'</div>' +
|
|
'<div><label for="editDescription">Description</label><textarea id="editDescription" rows="3">' + escapeHtml(event.description) + '</textarea></div>' +
|
|
'<div class="form-grid cols-2">' +
|
|
'<div><label for="editStartTime">Start Time</label><input id="editStartTime" type="datetime-local" value="' + startTime + '"></div>' +
|
|
'<div><label for="editEndTime">End Time</label><input id="editEndTime" type="datetime-local" value="' + endTime + '"></div>' +
|
|
'</div>' +
|
|
'<div class="form-grid cols-2">' +
|
|
'<div><label for="editLocation">Location</label><input id="editLocation" type="text" value="' + escapeHtml(event.location) + '"></div>' +
|
|
'<div><label for="editLocationUrl">Location URL (optional)</label><input id="editLocationUrl" type="url" value="' + (event.location_url || '') + '" placeholder="https://..."></div>' +
|
|
'</div>' +
|
|
'<div class="form-grid cols-2">' +
|
|
'<div><label for="editRecurringType">Recurring Type</label><select id="editRecurringType"><option value="' + escapeHtml(event.recurring_type || '') + '">' + escapeHtml(event.recurring_type || 'No Recurrence') + '</option></select></div>' +
|
|
'<div><label><input id="editFeatured" type="checkbox"' + (event.is_featured ? ' checked' : '') + '> Featured Event</label></div>' +
|
|
'</div>' +
|
|
'<div class="form-grid cols-2">' +
|
|
'<div>' +
|
|
'<label for="editEventImage">Event Image</label>' +
|
|
'<input type="file" id="editEventImage" accept="image/*" style="width: 100%;">' +
|
|
'<div id="editEventImagePreview" style="margin-top: 10px; text-align: center;">' +
|
|
(event.image ? '<div style="margin-bottom: 10px;"><strong>Current:</strong><br><img src="' + escapeHtml(event.image) + '" style="max-width: 200px; max-height: 150px; border-radius: 8px;" onerror="this.parentElement.innerHTML=\'❌ Current image unavailable\'"></div>' : '') +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="editEventThumbnail">Thumbnail Image</label>' +
|
|
'<input type="file" id="editEventThumbnail" accept="image/*" style="width: 100%;">' +
|
|
'<div id="editEventThumbnailPreview" style="margin-top: 10px; text-align: center;">' +
|
|
(event.thumbnail ? '<div style="margin-bottom: 10px;"><strong>Current:</strong><br><img src="' + escapeHtml(event.thumbnail) + '" style="max-width: 150px; max-height: 100px; border-radius: 8px;" onerror="this.parentElement.innerHTML=\'❌ Current thumbnail unavailable\'"></div>' : '') +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
document.getElementById('modalContent').innerHTML = modalHTML;
|
|
document.getElementById('modalActions').innerHTML = '<button onclick="saveEventEdit(\'' + event.id + '\')" class="btn-edit"><span>💾</span>Save Changes</button>';
|
|
|
|
// 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 ? '<div style="margin-bottom: 10px;"><strong>Current:</strong><br><img src="' + escapeHtml(event.image) + '" style="max-width: 200px; max-height: 150px; border-radius: 8px;" onerror="this.parentElement.innerHTML=\'❌ Current image unavailable\'"></div>' : '') +
|
|
'<div style="margin-top: 10px;"><strong>New:</strong><br><img src="' + e.target.result + '" style="max-width: 200px; max-height: 150px; border-radius: 8px;"></div>';
|
|
};
|
|
reader.readAsDataURL(file);
|
|
} else {
|
|
preview.innerHTML = event.image ? '<div style="margin-bottom: 10px;"><strong>Current:</strong><br><img src="' + escapeHtml(event.image) + '" style="max-width: 200px; max-height: 150px; border-radius: 8px;" onerror="this.parentElement.innerHTML=\'❌ Current image unavailable\'"></div>' : '';
|
|
}
|
|
});
|
|
|
|
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 ? '<div style="margin-bottom: 10px;"><strong>Current:</strong><br><img src="' + escapeHtml(event.thumbnail) + '" style="max-width: 150px; max-height: 100px; border-radius: 8px;" onerror="this.parentElement.innerHTML=\'❌ Current thumbnail unavailable\'"></div>' : '') +
|
|
'<div style="margin-top: 10px;"><strong>New:</strong><br><img src="' + e.target.result + '" style="max-width: 150px; max-height: 100px; border-radius: 8px;"></div>';
|
|
};
|
|
reader.readAsDataURL(file);
|
|
} else {
|
|
preview.innerHTML = event.thumbnail ? '<div style="margin-bottom: 10px;"><strong>Current:</strong><br><img src="' + escapeHtml(event.thumbnail) + '" style="max-width: 150px; max-height: 100px; border-radius: 8px;" onerror="this.parentElement.innerHTML=\'❌ Current thumbnail unavailable\'"></div>' : '';
|
|
}
|
|
});
|
|
|
|
// 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 = '<div class="content-header">' +
|
|
'<h2 class="content-title"><span>👥</span>Schedule Management</h2>' +
|
|
'<div style="display: flex; gap: 1rem;">' +
|
|
'<button onclick="showCreateScheduleModal()" class="btn-edit"><span>✨</span>Create Schedule</button>' +
|
|
'<button onclick="showImportScheduleModal()" class="btn-approve"><span>📁</span>Import JSON</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
if (schedules.length === 0) {
|
|
html += '<div class="empty-state">' +
|
|
'<div class="empty-icon purple">👥</div>' +
|
|
'<p class="empty-title">No schedules yet.</p>' +
|
|
'<p>Create individual schedules or import from your JSON file.</p>' +
|
|
'</div>';
|
|
} else {
|
|
schedules.forEach(schedule => {
|
|
html += '<div class="event-item">' +
|
|
'<div class="event-content">' +
|
|
'<div class="event-details">' +
|
|
'<h3 class="event-title">Schedule for ' + formatDate(schedule.date) + '</h3>' +
|
|
'<div class="event-meta" style="grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));">' +
|
|
(schedule.song_leader ? '<div class="meta-item"><span>🎵</span>Song Leader: ' + escapeHtml(schedule.song_leader) + '</div>' : '') +
|
|
(schedule.ss_teacher ? '<div class="meta-item"><span>📚</span>SS Teacher: ' + escapeHtml(schedule.ss_teacher) + '</div>' : '') +
|
|
(schedule.ss_leader ? '<div class="meta-item"><span>👨🏫</span>SS Leader: ' + escapeHtml(schedule.ss_leader) + '</div>' : '') +
|
|
(schedule.scripture ? '<div class="meta-item"><span>📖</span>Scripture: ' + escapeHtml(schedule.scripture) + '</div>' : '') +
|
|
(schedule.sermon_speaker ? '<div class="meta-item"><span>🎙️</span>Speaker: ' + escapeHtml(schedule.sermon_speaker) + '</div>' : '') +
|
|
(schedule.special_music ? '<div class="meta-item"><span>🎼</span>Special Music: ' + escapeHtml(schedule.special_music) + '</div>' : '') +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="event-actions">' +
|
|
'<button onclick="editSchedule(\'' + schedule.id + '\')" class="btn-action btn-edit"><span>✏️</span>Edit</button>' +
|
|
'<button onclick="deleteSchedule(\'' + schedule.id + '\')" class="btn-action btn-delete"><span>🗑️</span>Delete</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
});
|
|
}
|
|
|
|
content.innerHTML = html;
|
|
}
|
|
|
|
function showImportScheduleModal() {
|
|
document.getElementById('modalTitle').textContent = 'Import Schedule from JSON';
|
|
document.getElementById('modalContent').innerHTML =
|
|
'<div class="form-grid">' +
|
|
'<div>' +
|
|
'<label for="scheduleJsonFile">Select JSON File</label>' +
|
|
'<input id="scheduleJsonFile" type="file" accept=".json" required>' +
|
|
'<small style="color: var(--muted-color); margin-top: 0.5rem; display: block;">Upload your quarterly schedule JSON file</small>' +
|
|
'</div>' +
|
|
'<div id="importPreview" style="display: none;">' +
|
|
'<h4>Preview:</h4>' +
|
|
'<div id="importPreviewContent"></div>' +
|
|
'</div>' +
|
|
'<div id="importProgress" style="display: none;">' +
|
|
'<div class="progress-bar">' +
|
|
'<div id="progressFill" class="progress-fill" style="width: 0%;"></div>' +
|
|
'</div>' +
|
|
'<p id="progressText">Processing...</p>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
document.getElementById('modalActions').innerHTML =
|
|
'<button id="importButton" onclick="importScheduleFromJson()" class="btn-approve">' +
|
|
'<span>📁</span>Import Schedules' +
|
|
'</button>';
|
|
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 = '<p><strong>Found ' + data.scheduleData.length + ' schedule entries</strong></p>';
|
|
html += '<div style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 1rem; border-radius: 0.5rem;">';
|
|
|
|
data.scheduleData.slice(0, 5).forEach(entry => {
|
|
const date = new Date(entry.date).toLocaleDateString();
|
|
html += '<div style="margin-bottom: 0.5rem;">';
|
|
html += '<strong>' + date + '</strong>: ';
|
|
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 += '</div>';
|
|
});
|
|
|
|
if (data.scheduleData.length > 5) {
|
|
html += '<p><em>... and ' + (data.scheduleData.length - 5) + ' more entries</em></p>';
|
|
}
|
|
|
|
html += '</div>';
|
|
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 = `
|
|
<div class="form-grid">
|
|
<div>
|
|
<label for="scheduleDate">Date</label>
|
|
<input id="scheduleDate" type="date" required>
|
|
</div>
|
|
<div class="form-grid cols-2">
|
|
<div>
|
|
<label for="songLeader">Song Leader</label>
|
|
<input id="songLeader" type="text" placeholder="e.g., Lisa">
|
|
</div>
|
|
<div>
|
|
<label for="ssTeacher">SS Teacher</label>
|
|
<input id="ssTeacher" type="text" placeholder="e.g., John Smith">
|
|
</div>
|
|
</div>
|
|
<div class="form-grid cols-2">
|
|
<div>
|
|
<label for="ssLeader">SS Leader</label>
|
|
<input id="ssLeader" type="text" placeholder="e.g., Wayne Tino">
|
|
</div>
|
|
<div>
|
|
<label for="missionStory">Mission Story</label>
|
|
<input id="missionStory" type="text" placeholder="e.g., Jerry Travers">
|
|
</div>
|
|
</div>
|
|
<div class="form-grid cols-2">
|
|
<div>
|
|
<label for="scripture">Scripture Reading</label>
|
|
<input id="scripture" type="text" placeholder="e.g., Orville Castillo">
|
|
</div>
|
|
<div>
|
|
<label for="offering">Offering</label>
|
|
<input id="offering" type="text" placeholder="e.g., Audley Brown">
|
|
</div>
|
|
</div>
|
|
<div class="form-grid cols-2">
|
|
<div>
|
|
<label for="specialMusic">Special Music</label>
|
|
<input id="specialMusic" type="text" placeholder="e.g., Michelle Maitland">
|
|
</div>
|
|
<div>
|
|
<label for="sermonSpeaker">Sermon Speaker</label>
|
|
<input id="sermonSpeaker" type="text" placeholder="e.g., Pastor Joseph Piresson">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('modalActions').innerHTML = `
|
|
<button onclick="createSchedule()" class="btn-edit">
|
|
<span>✨</span>Create Schedule
|
|
</button>
|
|
`;
|
|
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 = `
|
|
<div class="form-grid">
|
|
<div>
|
|
<label for="editScheduleDate">Date</label>
|
|
<input id="editScheduleDate" type="date" value="${formattedDate}" required>
|
|
</div>
|
|
<div class="form-grid cols-2">
|
|
<div>
|
|
<label for="editSongLeader">Song Leader</label>
|
|
<input id="editSongLeader" type="text" value="${schedule.song_leader || ''}">
|
|
</div>
|
|
<div>
|
|
<label for="editSsTeacher">SS Teacher</label>
|
|
<input id="editSsTeacher" type="text" value="${schedule.ss_teacher || ''}">
|
|
</div>
|
|
</div>
|
|
<div class="form-grid cols-2">
|
|
<div>
|
|
<label for="editSsLeader">SS Leader</label>
|
|
<input id="editSsLeader" type="text" value="${schedule.ss_leader || ''}">
|
|
</div>
|
|
<div>
|
|
<label for="editMissionStory">Mission Story</label>
|
|
<input id="editMissionStory" type="text" value="${schedule.mission_story || ''}">
|
|
</div>
|
|
</div>
|
|
<div class="form-grid cols-2">
|
|
<div>
|
|
<label for="editScripture">Scripture Reading</label>
|
|
<input id="editScripture" type="text" value="${schedule.scripture || ''}">
|
|
</div>
|
|
<div>
|
|
<label for="editOffering">Offering</label>
|
|
<input id="editOffering" type="text" value="${schedule.offering || ''}">
|
|
</div>
|
|
</div>
|
|
<div class="form-grid cols-2">
|
|
<div>
|
|
<label for="editSpecialMusic">Special Music</label>
|
|
<input id="editSpecialMusic" type="text" value="${schedule.special_music || ''}">
|
|
</div>
|
|
<div>
|
|
<label for="editSermonSpeaker">Sermon Speaker</label>
|
|
<input id="editSermonSpeaker" type="text" value="${schedule.sermon_speaker || ''}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('modalActions').innerHTML = `
|
|
<button onclick="saveScheduleEdit('${schedule.id}')" class="btn-edit">
|
|
<span>💾</span>Save Changes
|
|
</button>
|
|
`;
|
|
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 =
|
|
'<div class="empty-state">' +
|
|
'<div class="empty-icon purple">🗞️</div>' +
|
|
'<p class="empty-title">No bulletins yet.</p>' +
|
|
'<p>Create your first church bulletin to get started.</p>' +
|
|
'<button onclick="showCreateBulletinModal()" class="btn-edit"><span>✨</span>Create First Bulletin</button>' +
|
|
'</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="content-header"><h2 class="content-title"><span>🗞️</span>Church Bulletins</h2><button onclick="showCreateBulletinModal()" class="btn-edit"><span>✨</span>Create New</button></div>';
|
|
|
|
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 +=
|
|
'<div class="event-item">' +
|
|
'<div class="event-content">' +
|
|
'<div class="event-image">' +
|
|
(bulletin.cover_image ?
|
|
'<img src="' + escapeHtml(bulletin.cover_image) + '" alt="Bulletin cover image" onerror="this.parentElement.innerHTML=\'🗞️\'">' :
|
|
'🗞️'
|
|
) +
|
|
'</div>' +
|
|
'<div class="event-details">' +
|
|
'<h3 class="event-title">' + escapeHtml(bulletin.title || 'Church Bulletin') + '</h3>' +
|
|
'<p class="event-description">' + escapeHtml(contentPreview) + showMore + '</p>' +
|
|
'<div class="event-meta">' +
|
|
'<div class="meta-item"><span>📅</span>' + formatDate(bulletin.date || bulletin.created_at) + '</div>' +
|
|
'<div class="meta-item"><span>📄</span>' + (bulletin.pdf_path ? 'PDF Available' : 'No PDF') + '</div>' +
|
|
'<div class="meta-item"><span>📝</span>' + (bulletin.is_active ? '<span class="category-badge featured">Active</span>' : '<span class="category-badge approved">Inactive</span>') + '</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="event-actions">' +
|
|
'<button onclick="viewBulletin(\'' + bulletin.id + '\')" class="btn-action btn-edit"><span>👁️</span>View</button>' +
|
|
'<button onclick="editBulletin(\'' + bulletin.id + '\')" class="btn-action btn-edit"><span>✏️</span>Edit</button>' +
|
|
'<button onclick="deleteBulletin(\'' + bulletin.id + '\')" class="btn-action btn-delete"><span>🗑️</span>Delete</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
});
|
|
|
|
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 = '<div class="form-grid">';
|
|
|
|
// Display cover image if available
|
|
if (bulletin.cover_image) {
|
|
modalContent += '<div style="text-align: center; margin-bottom: 20px;"><img src="' + escapeHtml(bulletin.cover_image) + '" alt="Bulletin cover image" style="max-width: 100%; max-height: 300px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"></div>';
|
|
}
|
|
|
|
// Display bulletin date
|
|
if (bulletin.date) {
|
|
modalContent += '<div><h4>📅 Date</h4><p>' + formatDate(bulletin.date) + '</p></div>';
|
|
}
|
|
|
|
// Display Sabbath School section
|
|
if (bulletin.sabbath_school) {
|
|
modalContent += '<div><h4>📚 Sabbath School</h4><div>' + renderHtmlContent(bulletin.sabbath_school) + '</div></div>';
|
|
}
|
|
|
|
// Display Divine Worship section
|
|
if (bulletin.divine_worship) {
|
|
modalContent += '<div><h4>⛪ Divine Worship</h4><div>' + renderHtmlContent(bulletin.divine_worship) + '</div></div>';
|
|
}
|
|
|
|
// Display Scripture Reading
|
|
if (bulletin.scripture_reading) {
|
|
modalContent += '<div><h4>📖 Scripture Reading</h4><div>' + renderHtmlContent(bulletin.scripture_reading) + '</div></div>';
|
|
}
|
|
|
|
// Display Sunset times
|
|
if (bulletin.sunset) {
|
|
modalContent += '<div><h4>🌅 Sunset</h4><div>' + renderHtmlContent(bulletin.sunset) + '</div></div>';
|
|
}
|
|
|
|
// 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 += '<div><h4>📄 PDF Download</h4><p><a href="' + pdfUrl + '" target="_blank" style="color: var(--church-primary);">Download PDF</a></p></div>';
|
|
}
|
|
|
|
modalContent += '</div>';
|
|
|
|
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 =
|
|
'<div class="form-grid">' +
|
|
'<div class="form-grid cols-2">' +
|
|
'<div>' +
|
|
'<label for="bulletinTitle">Title</label>' +
|
|
'<input id="bulletinTitle" type="text" placeholder="Sunday Bulletin - June 28, 2025" required>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="bulletinDate">Date</label>' +
|
|
'<input id="bulletinDate" type="date" required>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="bulletinSabbathSchool">Sabbath School</label>' +
|
|
'<textarea id="bulletinSabbathSchool" rows="4" placeholder="Enter Sabbath School program details..."></textarea>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="bulletinDivineWorship">Divine Worship</label>' +
|
|
'<textarea id="bulletinDivineWorship" rows="6" placeholder="Enter Divine Worship service details..."></textarea>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="bulletinScripture">Scripture Reading</label>' +
|
|
'<textarea id="bulletinScripture" rows="4" placeholder="Enter scripture reading..."></textarea>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="bulletinSunset">Sunset Times</label>' +
|
|
'<textarea id="bulletinSunset" rows="2" placeholder="Sunset Tonight: 8:29 pm\\nSunset Next Friday: 8:31 pm"></textarea>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="bulletinCoverImage">Cover Image</label>' +
|
|
'<input type="file" id="bulletinCoverImage" accept="image/*" style="width: 100%;">' +
|
|
'<div id="bulletinCoverImagePreview" style="margin-top: 10px; text-align: center;"></div>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label>' +
|
|
'<input id="bulletinIsActive" type="checkbox" checked>' +
|
|
' Set as active bulletin' +
|
|
'</label>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
document.getElementById('modalActions').innerHTML =
|
|
'<button onclick="createBulletin()" class="btn-edit">' +
|
|
'<span>✨</span>Create Bulletin' +
|
|
'</button>';
|
|
|
|
// 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 = '<img src="' + e.target.result + '" style="max-width: 200px; max-height: 150px; border-radius: 8px;">';
|
|
};
|
|
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 =
|
|
'<div class="form-grid">' +
|
|
'<div class="form-grid cols-2">' +
|
|
'<div>' +
|
|
'<label for="editBulletinTitle">Title</label>' +
|
|
'<input id="editBulletinTitle" type="text" value="' + escapeHtml(bulletin.title || '') + '" required>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="editBulletinDate">Date</label>' +
|
|
'<input id="editBulletinDate" type="date" value="' + (bulletin.date || '') + '" required>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="editBulletinSabbathSchool">Sabbath School</label>' +
|
|
'<textarea id="editBulletinSabbathSchool" rows="4">' + escapeHtml(stripHtmlTags(bulletin.sabbath_school || '')) + '</textarea>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="editBulletinDivineWorship">Divine Worship</label>' +
|
|
'<textarea id="editBulletinDivineWorship" rows="6">' + escapeHtml(stripHtmlTags(bulletin.divine_worship || '')) + '</textarea>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="editBulletinScripture">Scripture Reading</label>' +
|
|
'<textarea id="editBulletinScripture" rows="4">' + escapeHtml(stripHtmlTags(bulletin.scripture_reading || '')) + '</textarea>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="editBulletinSunset">Sunset Times</label>' +
|
|
'<textarea id="editBulletinSunset" rows="2">' + escapeHtml(stripHtmlTags(bulletin.sunset || '')) + '</textarea>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label for="editBulletinCoverImage">Cover Image</label>' +
|
|
'<input type="file" id="editBulletinCoverImage" accept="image/*" style="width: 100%;">' +
|
|
'<div id="editBulletinCoverImagePreview" style="margin-top: 10px; text-align: center;">' +
|
|
(bulletin.cover_image ? '<div style="margin-bottom: 10px;"><strong>Current:</strong><br><img src="' + escapeHtml(bulletin.cover_image) + '" style="max-width: 200px; max-height: 150px; border-radius: 8px;" onerror="this.parentElement.innerHTML=\'❌ Current image unavailable\'"></div>' : '') +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<label>' +
|
|
'<input id="editBulletinIsActive" type="checkbox"' + (bulletin.is_active ? ' checked' : '') + '>' +
|
|
' Set as active bulletin' +
|
|
'</label>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
document.getElementById('modalActions').innerHTML =
|
|
'<button onclick="saveBulletinEdit(\'' + bulletin.id + '\')" class="btn-edit">' +
|
|
'<span>💾</span>Save Changes' +
|
|
'</button>';
|
|
|
|
// 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 ? '<div style="margin-bottom: 10px;"><strong>Current:</strong><br><img src="' + escapeHtml(bulletin.cover_image) + '" style="max-width: 200px; max-height: 150px; border-radius: 8px;" onerror="this.parentElement.innerHTML=\'❌ Current image unavailable\'"></div>' : '') +
|
|
'<div style="margin-top: 10px;"><strong>New:</strong><br><img src="' + e.target.result + '" style="max-width: 200px; max-height: 150px; border-radius: 8px;"></div>';
|
|
};
|
|
reader.readAsDataURL(file);
|
|
} else {
|
|
// If no file selected, show only original image if it exists
|
|
preview.innerHTML = bulletin.cover_image ? '<div style="margin-bottom: 10px;"><strong>Current:</strong><br><img src="' + escapeHtml(bulletin.cover_image) + '" style="max-width: 200px; max-height: 150px; border-radius: 8px;" onerror="this.parentElement.innerHTML=\'❌ Current image unavailable\'"></div>' : '';
|
|
}
|
|
});
|
|
|
|
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 = '<div class="empty-state"><p style="color: var(--danger-color); font-size: 1.25rem;">⚠️ ' + message + '</p></div>';
|
|
}
|
|
|
|
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(/<p style="[^"]*">/g, '<p>')
|
|
.replace(/<\/p>/g, '</p>')
|
|
.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();
|
|
}
|
|
}); |