Fix icon hover colors, text alignment, and UI improvements
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# Admin directory — disable directory listing
|
||||
Options -Indexes
|
||||
|
||||
# Block direct access to any PHP files OTHER than index.php (auth is handled by JS + API)
|
||||
# All legitimate admin PHP is in /api/ — nothing in /admin/ should be a PHP script
|
||||
<FilesMatch "\.php$">
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
+38
-13
@@ -451,15 +451,15 @@
|
||||
<div class="quick-slots">
|
||||
<label>Quick Add Times</label>
|
||||
<div class="quick-slots-grid">
|
||||
<button type="button" class="quick-slot-btn" onclick="addQuickTime('09:00')">9:00 AM</button>
|
||||
<button type="button" class="quick-slot-btn" onclick="addQuickTime('10:00')">10:00 AM</button>
|
||||
<button type="button" class="quick-slot-btn" onclick="addQuickTime('11:00')">11:00 AM</button>
|
||||
<button type="button" class="quick-slot-btn" onclick="addQuickTime('13:00')">1:00 PM</button>
|
||||
<button type="button" class="quick-slot-btn" onclick="addQuickTime('14:00')">2:00 PM</button>
|
||||
<button type="button" class="quick-slot-btn" onclick="addQuickTime('15:00')">3:00 PM</button>
|
||||
<button type="button" class="quick-slot-btn" onclick="addQuickTime('16:00')">4:00 PM</button>
|
||||
<button type="button" class="quick-slot-btn" onclick="addQuickTime('17:00')">5:00 PM</button>
|
||||
<button type="button" class="quick-slot-btn" onclick="addQuickTime('18:00')">6:00 PM</button>
|
||||
<button type="button" class="quick-slot-btn" data-time="09:00">9:00 AM</button>
|
||||
<button type="button" class="quick-slot-btn" data-time="10:00">10:00 AM</button>
|
||||
<button type="button" class="quick-slot-btn" data-time="11:00">11:00 AM</button>
|
||||
<button type="button" class="quick-slot-btn" data-time="13:00">1:00 PM</button>
|
||||
<button type="button" class="quick-slot-btn" data-time="14:00">2:00 PM</button>
|
||||
<button type="button" class="quick-slot-btn" data-time="15:00">3:00 PM</button>
|
||||
<button type="button" class="quick-slot-btn" data-time="16:00">4:00 PM</button>
|
||||
<button type="button" class="quick-slot-btn" data-time="17:00">5:00 PM</button>
|
||||
<button type="button" class="quick-slot-btn" data-time="18:00">6:00 PM</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -467,7 +467,7 @@
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Add Slot
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="generateWeekSlots()">
|
||||
<button type="button" class="btn btn-secondary" id="generate-week-btn">
|
||||
<i class="fas fa-calendar-week"></i> Generate Week
|
||||
</button>
|
||||
</div>
|
||||
@@ -577,6 +577,31 @@
|
||||
document.getElementById('block-time-form').addEventListener('submit', handleBlockTime);
|
||||
document.getElementById('filter-date-from').addEventListener('change', filterSlots);
|
||||
document.getElementById('filter-date-to').addEventListener('change', filterSlots);
|
||||
document.getElementById('generate-week-btn').addEventListener('click', generateWeekSlots);
|
||||
|
||||
document.querySelector('.quick-slots-grid')?.addEventListener('click', function(event) {
|
||||
const btn = event.target.closest('.quick-slot-btn[data-time]');
|
||||
if (!btn) return;
|
||||
addQuickTime(btn.dataset.time);
|
||||
});
|
||||
|
||||
document.getElementById('blocked-times-list').addEventListener('click', function(event) {
|
||||
const btn = event.target.closest('[data-action="unblock-time"]');
|
||||
if (!btn) return;
|
||||
unblockTime(btn.dataset.id);
|
||||
});
|
||||
|
||||
document.getElementById('slots-table-body').addEventListener('click', function(event) {
|
||||
const btn = event.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
|
||||
const { action, id } = btn.dataset;
|
||||
if (action === 'toggle-slot' && id) {
|
||||
toggleSlot(id);
|
||||
} else if (action === 'delete-slot' && id) {
|
||||
deleteSlot(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setDefaultDate() {
|
||||
@@ -663,7 +688,7 @@
|
||||
<span style="font-weight: 600;">${formattedTime}</span>
|
||||
${blocked.reason ? `<span style="color: #64748b; font-size: 0.875rem; margin-left: 0.5rem;">(${blocked.reason})</span>` : ''}
|
||||
</div>
|
||||
<button onclick="unblockTime('${blocked.id}')" style="background: none; border: none; color: #dc2626; cursor: pointer; padding: 0.25rem;">
|
||||
<button data-action="unblock-time" data-id="${blocked.id}" style="background: none; border: none; color: #dc2626; cursor: pointer; padding: 0.25rem;">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -837,10 +862,10 @@
|
||||
<span style="color: var(--gray-500);">${slot.notes || '-'}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="action-btn toggle" onclick="toggleSlot('${slot.id}')">
|
||||
<button class="action-btn toggle" data-action="toggle-slot" data-id="${slot.id}">
|
||||
<i class="fas fa-${isAvailable ? 'eye-slash' : 'eye'}"></i>
|
||||
</button>
|
||||
<button class="action-btn delete" onclick="deleteSlot('${slot.id}')">
|
||||
<button class="action-btn delete" data-action="delete-slot" data-id="${slot.id}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
+39
-6
@@ -484,6 +484,39 @@
|
||||
document.getElementById('filter-date-from').addEventListener('change', loadBookings);
|
||||
document.getElementById('filter-date-to').addEventListener('change', loadBookings);
|
||||
document.getElementById('bookings-search').addEventListener('input', filterBookings);
|
||||
|
||||
document.getElementById('bookings-table-body').addEventListener('click', function(event) {
|
||||
const actionBtn = event.target.closest('[data-action]');
|
||||
if (actionBtn) {
|
||||
event.stopPropagation();
|
||||
const { action, id } = actionBtn.dataset;
|
||||
if (action === 'select-booking' && id) {
|
||||
selectBooking(id);
|
||||
} else if (action === 'confirm-booking' && id) {
|
||||
confirmBooking(id);
|
||||
} else if (action === 'cancel-booking' && id) {
|
||||
cancelBooking(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const row = event.target.closest('[data-row-action="select-booking"]');
|
||||
if (row && row.dataset.id) {
|
||||
selectBooking(row.dataset.id);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('booking-details').addEventListener('click', function(event) {
|
||||
const actionBtn = event.target.closest('[data-action]');
|
||||
if (!actionBtn) return;
|
||||
|
||||
const { action, id } = actionBtn.dataset;
|
||||
if (action === 'confirm-booking' && id) {
|
||||
confirmBooking(id);
|
||||
} else if (action === 'cancel-booking' && id) {
|
||||
cancelBooking(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadBookings() {
|
||||
@@ -536,7 +569,7 @@
|
||||
const statusClass = `status-${booking.status}`;
|
||||
|
||||
return `
|
||||
<tr class="booking-row" data-id="${booking.id}" onclick="selectBooking('${booking.id}')">
|
||||
<tr class="booking-row" data-id="${booking.id}" data-row-action="select-booking">
|
||||
<td>
|
||||
<div style="font-weight: 600;">${formattedDate}</div>
|
||||
<div style="font-size: 0.75rem; color: var(--gray-500);">${date.toLocaleDateString('en-US', { weekday: 'long' })}</div>
|
||||
@@ -550,14 +583,14 @@
|
||||
<td>${formatService(booking.service_interest)}</td>
|
||||
<td><span class="status-badge ${statusClass}">${booking.status}</span></td>
|
||||
<td>
|
||||
<button class="action-btn view" onclick="event.stopPropagation(); selectBooking('${booking.id}')">
|
||||
<button class="action-btn view" data-action="select-booking" data-id="${booking.id}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
${booking.status === 'pending' ? `
|
||||
<button class="action-btn confirm" onclick="event.stopPropagation(); confirmBooking('${booking.id}')">
|
||||
<button class="action-btn confirm" data-action="confirm-booking" data-id="${booking.id}">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
<button class="action-btn cancel" onclick="event.stopPropagation(); cancelBooking('${booking.id}')">
|
||||
<button class="action-btn cancel" data-action="cancel-booking" data-id="${booking.id}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
@@ -644,10 +677,10 @@
|
||||
|
||||
${selectedBooking.status === 'pending' ? `
|
||||
<div class="booking-actions">
|
||||
<button class="btn btn-success" onclick="confirmBooking('${selectedBooking.id}')">
|
||||
<button class="btn btn-success" data-action="confirm-booking" data-id="${selectedBooking.id}">
|
||||
<i class="fas fa-check"></i> Confirm Booking
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="cancelBooking('${selectedBooking.id}')">
|
||||
<button class="btn btn-danger" data-action="cancel-booking" data-id="${selectedBooking.id}">
|
||||
<i class="fas fa-times"></i> Cancel Booking
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+30
-13
@@ -112,6 +112,12 @@
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -162,7 +168,7 @@
|
||||
<div class="header-notifications">
|
||||
<button class="notification-btn">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span class="notification-badge">3</span>
|
||||
<span class="notification-badge" id="notif-badge">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -325,8 +331,14 @@
|
||||
const stats = response.data;
|
||||
|
||||
// Update Messages Stats
|
||||
document.getElementById('stat-messages-count').textContent = stats.messages?.unread || 0;
|
||||
const unreadCount = stats.messages?.unread || 0;
|
||||
document.getElementById('stat-messages-count').textContent = unreadCount;
|
||||
document.getElementById('stat-messages-total').textContent = `${stats.messages?.total || 0} total`;
|
||||
const badge = document.getElementById('notif-badge');
|
||||
if (badge) {
|
||||
badge.textContent = unreadCount;
|
||||
badge.style.display = unreadCount > 0 ? '' : 'none';
|
||||
}
|
||||
|
||||
// Update Bookings Stats
|
||||
document.getElementById('stat-bookings-count').textContent = stats.bookings?.pending || 0;
|
||||
@@ -355,14 +367,14 @@
|
||||
container.innerHTML = response.data.map(item => `
|
||||
<div class="content-item">
|
||||
<div class="item-image">
|
||||
<img src="${item.featured_image || '/images/logo/logo.png'}" alt="${item.title}" style="object-fit: cover;">
|
||||
<img src="${item.featured_image || '/images/logo/logo.png'}" alt="${MSPE.escapeHtml(item.title)}" style="object-fit: cover;">
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4>${item.title}</h4>
|
||||
<span class="item-meta">${MSPE.formatDate(item.created_at)} • ${item.category || 'News'}</span>
|
||||
<h4>${MSPE.escapeHtml(item.title)}</h4>
|
||||
<span class="item-meta">${MSPE.formatDate(item.created_at)} • ${MSPE.escapeHtml(item.category || 'News')}</span>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<a href="/admin/news.html?edit=${item.id}" class="action-btn edit"><i class="fas fa-edit"></i></a>
|
||||
<a href="/admin/news.html?edit=${encodeURIComponent(item.id)}" class="action-btn edit"><i class="fas fa-edit"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
@@ -392,19 +404,24 @@
|
||||
const container = document.getElementById('recent-messages');
|
||||
|
||||
if (response && response.success && response.data && response.data.length > 0) {
|
||||
container.innerHTML = response.data.map(msg => `
|
||||
<div class="message-item" style="cursor: pointer;" onclick="window.location.href='/admin/messages.html?id=${msg.id}'">
|
||||
container.innerHTML = response.data.map(msg => {
|
||||
const safeName = MSPE.escapeHtml((msg.first_name || 'Unknown') + ' ' + (msg.last_name || ''));
|
||||
const safePreview = MSPE.escapeHtml((msg.message || '').substring(0, 50)) + (msg.message && msg.message.length > 50 ? '...' : '');
|
||||
const safeStatus = ['unread', 'read', 'replied'].includes(msg.status) ? msg.status : 'unread';
|
||||
const safeId = encodeURIComponent(msg.id);
|
||||
return `
|
||||
<a class="message-item" style="cursor: pointer; text-decoration: none; color: inherit;" href="/admin/messages.html?id=${safeId}">
|
||||
<div class="message-avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=${encodeURIComponent((msg.first_name || 'U') + ' ' + (msg.last_name || ''))}&background=random" alt="Avatar">
|
||||
</div>
|
||||
<div class="message-info">
|
||||
<h4>${msg.first_name || 'Unknown'} ${msg.last_name || ''}</h4>
|
||||
<p>${(msg.message || '').substring(0, 50)}${msg.message && msg.message.length > 50 ? '...' : ''}</p>
|
||||
<h4>${safeName}</h4>
|
||||
<p>${safePreview}</p>
|
||||
<span class="message-time">${MSPE.formatDate(msg.created_at)}</span>
|
||||
</div>
|
||||
<span class="message-status ${msg.status || 'unread'}"></span>
|
||||
</div>
|
||||
`).join('');
|
||||
<span class="message-status ${safeStatus}"></span>
|
||||
</a>`;
|
||||
}).join('');
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state" style="padding: 2rem; text-align: center;">
|
||||
|
||||
+23
-1
@@ -39,6 +39,7 @@ document.head.appendChild(authStyle);
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (isAuthExemptPage()) {
|
||||
unlockAdminUi();
|
||||
enforceSafeBlankLinks();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,10 +47,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
initSidebar();
|
||||
initDropdowns();
|
||||
initLogout();
|
||||
enforceSafeBlankLinks();
|
||||
|
||||
checkAuth();
|
||||
});
|
||||
|
||||
function enforceSafeBlankLinks(scope = document) {
|
||||
scope.querySelectorAll('a[target="_blank"]').forEach(link => {
|
||||
const currentRel = (link.getAttribute('rel') || '').toLowerCase();
|
||||
const relParts = new Set(currentRel.split(/\s+/).filter(Boolean));
|
||||
relParts.add('noopener');
|
||||
relParts.add('noreferrer');
|
||||
link.setAttribute('rel', Array.from(relParts).join(' '));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sidebar Toggle
|
||||
*/
|
||||
@@ -430,11 +442,21 @@ function debounce(func, wait) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML Escape Helper — prevents XSS when inserting dynamic values into innerHTML
|
||||
*/
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(String(str ?? '')));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
window.MSPE = {
|
||||
API,
|
||||
showNotification,
|
||||
formatDate,
|
||||
confirmDialog,
|
||||
debounce
|
||||
debounce,
|
||||
escapeHtml
|
||||
};
|
||||
|
||||
+82
-40
@@ -112,6 +112,12 @@
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -154,7 +160,7 @@
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<button class="btn btn-primary" onclick="openUploadModal()">
|
||||
<button class="btn btn-primary" id="open-upload-btn">
|
||||
<i class="fas fa-upload"></i> Upload Files
|
||||
</button>
|
||||
|
||||
@@ -234,7 +240,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<button class="btn btn-outline btn-sm" onclick="createFolder()">
|
||||
<button class="btn btn-outline btn-sm" id="create-folder-btn">
|
||||
<i class="fas fa-folder-plus"></i> New Folder
|
||||
</button>
|
||||
<div class="view-toggle">
|
||||
@@ -279,13 +285,13 @@
|
||||
<!-- Bulk Actions -->
|
||||
<div class="bulk-actions" id="bulk-actions" style="display: none;">
|
||||
<span class="selected-count">0 selected</span>
|
||||
<button class="btn btn-outline btn-sm" onclick="moveSelected()">
|
||||
<button class="btn btn-outline btn-sm" id="move-selected-btn">
|
||||
<i class="fas fa-folder"></i> Move
|
||||
</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="downloadSelected()">
|
||||
<button class="btn btn-outline btn-sm" id="download-selected-btn">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteSelected()">
|
||||
<button class="btn btn-danger btn-sm" id="delete-selected-btn">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
@@ -299,7 +305,7 @@
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2>Upload Files</h2>
|
||||
<button class="modal-close" onclick="closeUploadModal()">×</button>
|
||||
<button class="modal-close" id="upload-modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="upload-dropzone" id="dropzone">
|
||||
@@ -327,7 +333,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeUploadModal()">Cancel</button>
|
||||
<button class="btn btn-outline" id="upload-cancel-btn">Cancel</button>
|
||||
<button class="btn btn-primary" id="start-upload" disabled>
|
||||
<i class="fas fa-upload"></i> Start Upload
|
||||
</button>
|
||||
@@ -341,7 +347,7 @@
|
||||
<div class="modal-container modal-lg">
|
||||
<div class="modal-header">
|
||||
<h2 id="view-title">File Details</h2>
|
||||
<button class="modal-close" onclick="closeViewModal()">×</button>
|
||||
<button class="modal-close" id="view-modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="media-view-content">
|
||||
@@ -373,7 +379,7 @@
|
||||
<label>URL</label>
|
||||
<div class="url-copy">
|
||||
<input type="text" id="detail-url" readonly>
|
||||
<button class="btn btn-sm btn-outline" onclick="copyDetailUrl()">
|
||||
<button class="btn btn-sm btn-outline" id="copy-detail-url-btn">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -386,13 +392,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-danger" onclick="deleteFromView()">
|
||||
<button class="btn btn-danger" id="delete-from-view-btn">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
<button class="btn btn-outline" onclick="downloadFile()">
|
||||
<button class="btn btn-outline" id="download-view-file-btn">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="saveDetails()">
|
||||
<button class="btn btn-primary" id="save-details-btn">
|
||||
<i class="fas fa-save"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
@@ -404,17 +410,72 @@
|
||||
let allMedia = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupEventListeners();
|
||||
loadMedia();
|
||||
|
||||
// Search & Filter
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('media-search').addEventListener('input', MSPE.debounce(filterMedia, 300));
|
||||
document.getElementById('type-filter').addEventListener('change', filterMedia);
|
||||
document.getElementById('folder-filter').addEventListener('change', filterMedia);
|
||||
|
||||
// File Input for Upload
|
||||
|
||||
document.getElementById('file-input').addEventListener('change', handleFileSelect);
|
||||
document.getElementById('start-upload').addEventListener('click', uploadFiles);
|
||||
});
|
||||
|
||||
document.getElementById('open-upload-btn')?.addEventListener('click', openUploadModal);
|
||||
document.getElementById('create-folder-btn')?.addEventListener('click', createFolder);
|
||||
document.getElementById('move-selected-btn')?.addEventListener('click', moveSelected);
|
||||
document.getElementById('download-selected-btn')?.addEventListener('click', downloadSelected);
|
||||
document.getElementById('delete-selected-btn')?.addEventListener('click', deleteSelected);
|
||||
|
||||
document.getElementById('upload-modal-close')?.addEventListener('click', closeUploadModal);
|
||||
document.getElementById('upload-cancel-btn')?.addEventListener('click', closeUploadModal);
|
||||
document.getElementById('view-modal-close')?.addEventListener('click', closeViewModal);
|
||||
|
||||
document.getElementById('copy-detail-url-btn')?.addEventListener('click', copyDetailUrl);
|
||||
document.getElementById('delete-from-view-btn')?.addEventListener('click', deleteFromView);
|
||||
document.getElementById('download-view-file-btn')?.addEventListener('click', () => downloadFile());
|
||||
document.getElementById('save-details-btn')?.addEventListener('click', saveDetails);
|
||||
|
||||
document.querySelector('#upload-modal .modal-backdrop')?.addEventListener('click', closeUploadModal);
|
||||
document.querySelector('#view-modal .modal-backdrop')?.addEventListener('click', closeViewModal);
|
||||
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
dropzone.addEventListener('click', () => fileInput.click());
|
||||
dropzone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.add('dragover');
|
||||
});
|
||||
dropzone.addEventListener('dragleave', () => {
|
||||
dropzone.classList.remove('dragover');
|
||||
});
|
||||
dropzone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.remove('dragover');
|
||||
fileInput.files = e.dataTransfer.files;
|
||||
handleFileSelect({ target: fileInput });
|
||||
});
|
||||
|
||||
document.getElementById('media-grid').addEventListener('click', function(event) {
|
||||
const actionBtn = event.target.closest('[data-action]');
|
||||
if (!actionBtn) return;
|
||||
const { action, id, path } = actionBtn.dataset;
|
||||
if (action === 'view' && id) {
|
||||
viewMedia(id);
|
||||
} else if (action === 'copy' && path) {
|
||||
copyUrl(path);
|
||||
} else if (action === 'delete' && id) {
|
||||
deleteMedia(id);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('media-grid').addEventListener('change', function(event) {
|
||||
if (event.target.matches('.media-checkbox input')) {
|
||||
updateBulkActions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadMedia() {
|
||||
try {
|
||||
@@ -454,9 +515,9 @@
|
||||
: `<i class="fas fa-file-${getFileIcon(item.type)}"></i>`
|
||||
}
|
||||
<div class="media-overlay">
|
||||
<button class="btn btn-sm" onclick="viewMedia('${item.id}')"><i class="fas fa-eye"></i></button>
|
||||
<button class="btn btn-sm" onclick="copyUrl('/${item.path}')"><i class="fas fa-link"></i></button>
|
||||
<button class="btn btn-sm" onclick="deleteMedia('${item.id}')"><i class="fas fa-trash"></i></button>
|
||||
<button class="btn btn-sm" data-action="view" data-id="${item.id}"><i class="fas fa-eye"></i></button>
|
||||
<button class="btn btn-sm" data-action="copy" data-path="/${item.path}"><i class="fas fa-link"></i></button>
|
||||
<button class="btn btn-sm" data-action="delete" data-id="${item.id}"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-info">
|
||||
@@ -464,7 +525,7 @@
|
||||
<span class="media-size">${formatBytes(item.size)}</span>
|
||||
</div>
|
||||
<label class="media-checkbox">
|
||||
<input type="checkbox" name="selected[]" value="${item.id}" onchange="updateBulkActions()">
|
||||
<input type="checkbox" name="selected[]" value="${item.id}">
|
||||
<span class="checkmark"></span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -618,25 +679,6 @@
|
||||
document.getElementById('view-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
// Drag & Drop
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
|
||||
dropzone.addEventListener('click', () => fileInput.click());
|
||||
dropzone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.add('dragover');
|
||||
});
|
||||
dropzone.addEventListener('dragleave', () => {
|
||||
dropzone.classList.remove('dragover');
|
||||
});
|
||||
dropzone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.remove('dragover');
|
||||
fileInput.files = e.dataTransfer.files;
|
||||
handleFileSelect({ target: fileInput });
|
||||
});
|
||||
|
||||
function updateBulkActions() {
|
||||
const checked = document.querySelectorAll('.media-checkbox input:checked');
|
||||
const bulkActions = document.getElementById('bulk-actions');
|
||||
|
||||
+63
-20
@@ -357,6 +357,12 @@
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -531,27 +537,38 @@
|
||||
container.innerHTML = messages.map(message => {
|
||||
const isUnread = (message.status === 'unread');
|
||||
const isActive = (message.id === selectedMessageId);
|
||||
const name = `${message.first_name} ${message.last_name}`;
|
||||
const name = `${message.first_name || ''} ${message.last_name || ''}`.trim();
|
||||
const timeAgo = getTimeAgo(new Date(message.created_at));
|
||||
const preview = message.message.length > 60
|
||||
? message.message.substring(0, 60) + '...'
|
||||
: message.message;
|
||||
const rawMessage = String(message.message || '');
|
||||
const preview = rawMessage.length > 60
|
||||
? rawMessage.substring(0, 60) + '...'
|
||||
: rawMessage;
|
||||
const safeId = MSPE.escapeHtml(message.id || '');
|
||||
const safeName = MSPE.escapeHtml(name || 'Unknown Sender');
|
||||
const safeTimeAgo = MSPE.escapeHtml(timeAgo || '');
|
||||
const safePreview = MSPE.escapeHtml(preview || '');
|
||||
|
||||
return `
|
||||
<div class="message-item ${isUnread ? 'unread' : ''} ${isActive ? 'active' : ''}"
|
||||
onclick="selectMessage('${message.id}')"
|
||||
data-id="${message.id}">
|
||||
data-id="${safeId}">
|
||||
<div class="message-header">
|
||||
<span class="message-sender">
|
||||
${isUnread ? '<span class="unread-dot"></span>' : ''}
|
||||
${name}
|
||||
${safeName}
|
||||
</span>
|
||||
<span class="message-meta">${timeAgo}</span>
|
||||
<span class="message-meta">${safeTimeAgo}</span>
|
||||
</div>
|
||||
<div class="message-preview">${preview}</div>
|
||||
<div class="message-preview">${safePreview}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.querySelectorAll('.message-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const id = item.dataset.id;
|
||||
if (id) selectMessage(id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function selectMessage(id) {
|
||||
@@ -590,7 +607,7 @@
|
||||
|
||||
function renderMessageDetails(message) {
|
||||
const container = document.getElementById('message-details');
|
||||
const name = `${message.first_name} ${message.last_name}`;
|
||||
const name = `${message.first_name || ''} ${message.last_name || ''}`.trim();
|
||||
const date = new Date(message.created_at);
|
||||
const formattedDate = date.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
@@ -608,18 +625,28 @@
|
||||
'consulting': 'Business Consulting',
|
||||
'general': 'General Inquiry'
|
||||
};
|
||||
|
||||
const safeName = MSPE.escapeHtml(name || 'Unknown Sender');
|
||||
const safeDate = MSPE.escapeHtml(formattedDate || '');
|
||||
const safeEmail = MSPE.escapeHtml(message.email || '');
|
||||
const safePhone = MSPE.escapeHtml(message.phone || 'Not provided');
|
||||
const safeCompany = MSPE.escapeHtml(message.company || 'Not provided');
|
||||
const safeService = MSPE.escapeHtml(serviceLabels[message.service] || message.service || 'Not specified');
|
||||
const safeBudget = message.budget ? MSPE.escapeHtml(message.budget) : '';
|
||||
const safeMessageBody = MSPE.escapeHtml(message.message || 'Not provided').replace(/\n/g, '<br>');
|
||||
const safeId = MSPE.escapeHtml(message.id || '');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="message-details-header">
|
||||
<div>
|
||||
<h3>${name}</h3>
|
||||
<div class="meta">${formattedDate}</div>
|
||||
<h3>${safeName}</h3>
|
||||
<div class="meta">${safeDate}</div>
|
||||
</div>
|
||||
<div class="message-actions-header">
|
||||
<button onclick="replyToMessage('${message.email}')">
|
||||
<button class="reply-btn" data-email="${safeEmail}">
|
||||
<i class="fas fa-reply"></i> Reply
|
||||
</button>
|
||||
<button class="delete" onclick="deleteMessage('${message.id}')">
|
||||
<button class="delete delete-btn" data-id="${safeId}">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
@@ -629,34 +656,50 @@
|
||||
<div class="contact-info">
|
||||
<div class="contact-info-item">
|
||||
<label>Email</label>
|
||||
<div class="value"><a href="mailto:${message.email}">${message.email}</a></div>
|
||||
<div class="value"><a href="mailto:${safeEmail}">${safeEmail}</a></div>
|
||||
</div>
|
||||
<div class="contact-info-item">
|
||||
<label>Phone</label>
|
||||
<div class="value">${message.phone || 'Not provided'}</div>
|
||||
<div class="value">${safePhone}</div>
|
||||
</div>
|
||||
<div class="contact-info-item">
|
||||
<label>Company</label>
|
||||
<div class="value">${message.company || 'Not provided'}</div>
|
||||
<div class="value">${safeCompany}</div>
|
||||
</div>
|
||||
<div class="contact-info-item">
|
||||
<label>Service Interest</label>
|
||||
<div class="value">${serviceLabels[message.service] || message.service || 'Not specified'}</div>
|
||||
<div class="value">${safeService}</div>
|
||||
</div>
|
||||
${message.budget ? `
|
||||
<div class="contact-info-item">
|
||||
<label>Budget</label>
|
||||
<div class="value">${message.budget}</div>
|
||||
<div class="value">${safeBudget}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="message-body">
|
||||
<h4>Message</h4>
|
||||
<p>${message.message.replace(/\n/g, '<br>')}</p>
|
||||
<p>${safeMessageBody}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const replyBtn = container.querySelector('.reply-btn');
|
||||
if (replyBtn) {
|
||||
replyBtn.addEventListener('click', () => {
|
||||
const email = replyBtn.dataset.email || '';
|
||||
if (email) replyToMessage(email);
|
||||
});
|
||||
}
|
||||
|
||||
const deleteBtn = container.querySelector('.delete-btn');
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', () => {
|
||||
const id = deleteBtn.dataset.id || '';
|
||||
if (id) deleteMessage(id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function replyToMessage(email) {
|
||||
|
||||
+19
-2
@@ -112,6 +112,12 @@
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -419,6 +425,17 @@
|
||||
document.getElementById('add-new-btn').addEventListener('click', showForm);
|
||||
document.getElementById('back-to-list').addEventListener('click', showList);
|
||||
document.getElementById('news-form').addEventListener('submit', saveArticle);
|
||||
document.getElementById('news-table-body').addEventListener('click', function(event) {
|
||||
const actionBtn = event.target.closest('[data-action]');
|
||||
if (!actionBtn) return;
|
||||
|
||||
const { action, id } = actionBtn.dataset;
|
||||
if (action === 'edit' && id) {
|
||||
editArticle(id);
|
||||
} else if (action === 'delete' && id) {
|
||||
deleteArticle(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Save draft button
|
||||
document.getElementById('save-draft').addEventListener('click', function() {
|
||||
@@ -541,8 +558,8 @@
|
||||
<td>${MSPE.formatDate(article.created_at)}</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<button class="action-btn edit" onclick="editArticle('${article.id}')" title="Edit"><i class="fas fa-edit"></i></button>
|
||||
<button class="action-btn delete" onclick="deleteArticle('${article.id}')" title="Delete"><i class="fas fa-trash"></i></button>
|
||||
<button class="action-btn edit" data-action="edit" data-id="${article.id}" title="Edit"><i class="fas fa-edit"></i></button>
|
||||
<button class="action-btn delete" data-action="delete" data-id="${article.id}" title="Delete"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
+51
-11
@@ -112,6 +112,12 @@
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -210,11 +216,11 @@
|
||||
|
||||
<!-- Page Editor Modal -->
|
||||
<div class="modal" id="page-modal">
|
||||
<div class="modal-overlay" onclick="closePageModal()"></div>
|
||||
<div class="modal-overlay" id="page-modal-overlay"></div>
|
||||
<div class="modal-content" style="max-width:700px;">
|
||||
<div class="modal-header">
|
||||
<h3 id="page-modal-title">Edit Page</h3>
|
||||
<button class="modal-close" onclick="closePageModal()"><i class="fas fa-times"></i></button>
|
||||
<button class="modal-close" id="page-modal-close-btn"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="modal-body" style="max-height:70vh;overflow-y:auto;">
|
||||
<form id="page-form">
|
||||
@@ -249,19 +255,19 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closePageModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="savePage()"><i class="fas fa-save"></i> Save Changes</button>
|
||||
<button class="btn btn-outline" id="page-modal-cancel-btn">Cancel</button>
|
||||
<button class="btn btn-primary" id="page-modal-save-btn"><i class="fas fa-save"></i> Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Section Editor Modal -->
|
||||
<div class="modal" id="global-modal">
|
||||
<div class="modal-overlay" onclick="closeGlobalModal()"></div>
|
||||
<div class="modal-overlay" id="global-modal-overlay"></div>
|
||||
<div class="modal-content" style="max-width:550px;">
|
||||
<div class="modal-header">
|
||||
<h3 id="global-modal-title">Edit Section</h3>
|
||||
<button class="modal-close" onclick="closeGlobalModal()"><i class="fas fa-times"></i></button>
|
||||
<button class="modal-close" id="global-modal-close-btn"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="global-form">
|
||||
@@ -270,8 +276,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeGlobalModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveGlobalSection()"><i class="fas fa-save"></i> Save</button>
|
||||
<button class="btn btn-outline" id="global-modal-cancel-btn">Cancel</button>
|
||||
<button class="btn btn-primary" id="global-modal-save-btn"><i class="fas fa-save"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -283,10 +289,44 @@
|
||||
let currentPageData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupPageEventListeners();
|
||||
loadPages();
|
||||
loadGlobalSections();
|
||||
});
|
||||
|
||||
function setupPageEventListeners() {
|
||||
document.getElementById('page-modal-overlay')?.addEventListener('click', closePageModal);
|
||||
document.getElementById('page-modal-close-btn')?.addEventListener('click', closePageModal);
|
||||
document.getElementById('page-modal-cancel-btn')?.addEventListener('click', closePageModal);
|
||||
document.getElementById('page-modal-save-btn')?.addEventListener('click', savePage);
|
||||
|
||||
document.getElementById('global-modal-overlay')?.addEventListener('click', closeGlobalModal);
|
||||
document.getElementById('global-modal-close-btn')?.addEventListener('click', closeGlobalModal);
|
||||
document.getElementById('global-modal-cancel-btn')?.addEventListener('click', closeGlobalModal);
|
||||
document.getElementById('global-modal-save-btn')?.addEventListener('click', saveGlobalSection);
|
||||
|
||||
document.getElementById('pages-grid')?.addEventListener('click', function(e) {
|
||||
const editBtn = e.target.closest('[data-action="edit-page"]');
|
||||
if (!editBtn) return;
|
||||
const slug = editBtn.dataset.slug;
|
||||
if (slug) editPage(slug);
|
||||
});
|
||||
|
||||
document.getElementById('global-sections-grid')?.addEventListener('click', function(e) {
|
||||
const editBtn = e.target.closest('[data-action="edit-global-section"]');
|
||||
if (!editBtn) return;
|
||||
const slug = editBtn.dataset.slug;
|
||||
if (slug) editGlobalSection(slug);
|
||||
});
|
||||
|
||||
document.getElementById('page-sections-editor')?.addEventListener('click', function(e) {
|
||||
const toggle = e.target.closest('.section-toggle');
|
||||
if (!toggle) return;
|
||||
const body = toggle.nextElementSibling;
|
||||
if (body) body.classList.toggle('collapsed');
|
||||
});
|
||||
}
|
||||
|
||||
// ── Load & Render ────────────────────────────────────
|
||||
|
||||
async function loadPages() {
|
||||
@@ -327,7 +367,7 @@
|
||||
<a href="${page.url || '#'}" target="_blank" class="btn btn-sm btn-outline">
|
||||
<i class="fas fa-eye"></i> View
|
||||
</a>
|
||||
<button class="btn btn-sm btn-primary" onclick="editPage('${page.slug}')">
|
||||
<button class="btn btn-sm btn-primary" data-action="edit-page" data-slug="${MSPE.escapeHtml(page.slug || '')}">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
</div>
|
||||
@@ -356,7 +396,7 @@
|
||||
<h4>${s.label}</h4>
|
||||
<p>${s.description}</p>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline" onclick="editGlobalSection('${s.slug}')">Edit</button>
|
||||
<button class="btn btn-sm btn-outline" data-action="edit-global-section" data-slug="${MSPE.escapeHtml(s.slug || '')}">Edit</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
@@ -395,7 +435,7 @@
|
||||
}).join('');
|
||||
|
||||
return `<div style="margin-bottom:1.5rem;border:1px solid var(--gray-200);border-radius:var(--radius-sm);overflow:hidden;">
|
||||
<div style="background:var(--gray-100);padding:.625rem 1rem;font-size:.8rem;font-weight:600;color:var(--dark);display:flex;align-items:center;gap:.5rem;cursor:pointer;" onclick="this.parentElement.querySelector('.sect-body').classList.toggle('collapsed')">
|
||||
<div class="section-toggle" style="background:var(--gray-100);padding:.625rem 1rem;font-size:.8rem;font-weight:600;color:var(--dark);display:flex;align-items:center;gap:.5rem;cursor:pointer;">
|
||||
<i class="fas fa-puzzle-piece" style="color:var(--primary);"></i> ${section.label}
|
||||
<i class="fas fa-chevron-down" style="margin-left:auto;font-size:.65rem;color:var(--gray-400);"></i>
|
||||
</div>
|
||||
|
||||
+28
-4
@@ -112,6 +112,12 @@
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -387,6 +393,24 @@
|
||||
document.getElementById('back-to-list').addEventListener('click', showList);
|
||||
document.getElementById('publish-btn').addEventListener('click', () => saveProject('published'));
|
||||
document.getElementById('save-draft-btn').addEventListener('click', () => saveProject('draft'));
|
||||
|
||||
document.getElementById('portfolio-grid').addEventListener('click', function(event) {
|
||||
const actionBtn = event.target.closest('[data-action]');
|
||||
if (!actionBtn) return;
|
||||
|
||||
const { action, id } = actionBtn.dataset;
|
||||
if (action === 'edit' && id) {
|
||||
editProject(id);
|
||||
} else if (action === 'delete' && id) {
|
||||
deleteProject(id);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('gallery-preview').addEventListener('click', function(event) {
|
||||
const removeBtn = event.target.closest('[data-action="remove-gallery-item"]');
|
||||
if (!removeBtn) return;
|
||||
removeGalleryItem(removeBtn);
|
||||
});
|
||||
|
||||
// Filters
|
||||
document.getElementById('category-filter').addEventListener('change', filterProjects);
|
||||
@@ -450,10 +474,10 @@
|
||||
<p class="portfolio-category">${project.category}</p>
|
||||
<p class="portfolio-client">Client: ${project.client || 'N/A'}</p>
|
||||
<div class="portfolio-admin-actions">
|
||||
<button class="btn btn-sm btn-outline" onclick="editProject('${project.id}')">
|
||||
<button class="btn btn-sm btn-outline" data-action="edit" data-id="${project.id}">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline btn-danger" onclick="deleteProject('${project.id}')">
|
||||
<button class="btn btn-sm btn-outline btn-danger" data-action="delete" data-id="${project.id}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -585,7 +609,7 @@
|
||||
galleryPreview.innerHTML = existingGallery.map(url =>
|
||||
`<div class="gallery-preview-item" data-url="${url}">
|
||||
<img src="/${url}" alt="Gallery image">
|
||||
<button type="button" class="gallery-remove-btn" onclick="removeGalleryItem(this)"><i class="fas fa-times"></i></button>
|
||||
<button type="button" class="gallery-remove-btn" data-action="remove-gallery-item"><i class="fas fa-times"></i></button>
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
@@ -621,7 +645,7 @@
|
||||
const item = document.createElement('div');
|
||||
item.className = 'gallery-preview-item';
|
||||
item.dataset.galleryIdx = idx;
|
||||
item.innerHTML = `<img src="${ev.target.result}" alt=""><button type="button" class="gallery-remove-btn" onclick="removeGalleryItem(this)"><i class="fas fa-times"></i></button>`;
|
||||
item.innerHTML = `<img src="${ev.target.result}" alt=""><button type="button" class="gallery-remove-btn" data-action="remove-gallery-item"><i class="fas fa-times"></i></button>`;
|
||||
preview.appendChild(item);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
+40
-6
@@ -112,6 +112,12 @@
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -201,7 +207,7 @@
|
||||
<div class="modal-content modal-lg">
|
||||
<div class="modal-header">
|
||||
<h2>Edit Service</h2>
|
||||
<button class="modal-close" onclick="closeModal()">
|
||||
<button class="modal-close" id="service-modal-close-btn">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -251,8 +257,8 @@ Network Management"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveService()">Save Changes</button>
|
||||
<button class="btn btn-outline" id="service-modal-cancel-btn">Cancel</button>
|
||||
<button class="btn btn-primary" id="service-modal-save-btn">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,6 +270,34 @@ Network Management"></textarea>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadServices();
|
||||
document.getElementById('add-service-btn').addEventListener('click', openAddServiceModal);
|
||||
|
||||
document.getElementById('service-modal-close-btn')?.addEventListener('click', closeModal);
|
||||
document.getElementById('service-modal-cancel-btn')?.addEventListener('click', closeModal);
|
||||
document.getElementById('service-modal-save-btn')?.addEventListener('click', saveService);
|
||||
document.querySelector('#service-modal .modal-overlay')?.addEventListener('click', closeModal);
|
||||
|
||||
const servicesGrid = document.getElementById('services-admin-grid');
|
||||
servicesGrid?.addEventListener('click', function(e) {
|
||||
const editBtn = e.target.closest('[data-action="edit-service"]');
|
||||
if (editBtn) {
|
||||
const id = editBtn.dataset.id;
|
||||
if (id) editService(id);
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteBtn = e.target.closest('[data-action="delete-service"]');
|
||||
if (deleteBtn) {
|
||||
const id = deleteBtn.dataset.id;
|
||||
if (id) deleteService(id);
|
||||
}
|
||||
});
|
||||
|
||||
servicesGrid?.addEventListener('change', function(e) {
|
||||
const toggle = e.target.closest('[data-action="toggle-service"]');
|
||||
if (!toggle) return;
|
||||
const id = toggle.dataset.id;
|
||||
if (id) toggleService(id, toggle.checked);
|
||||
});
|
||||
});
|
||||
|
||||
async function loadServices() {
|
||||
@@ -297,14 +331,14 @@ Network Management"></textarea>
|
||||
<i class="fas ${service.icon || 'fa-cogs'}"></i>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<button class="btn btn-sm btn-icon" onclick="editService('${service.id}')">
|
||||
<button class="btn btn-sm btn-icon" data-action="edit-service" data-id="${MSPE.escapeHtml(service.id || '')}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" ${service.active ? 'checked' : ''} onchange="toggleService('${service.id}', this.checked)">
|
||||
<input type="checkbox" ${service.active ? 'checked' : ''} data-action="toggle-service" data-id="${MSPE.escapeHtml(service.id || '')}">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<button class="btn btn-sm btn-icon text-danger" onclick="deleteService('${service.id}')">
|
||||
<button class="btn btn-sm btn-icon text-danger" data-action="delete-service" data-id="${MSPE.escapeHtml(service.id || '')}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+25
-3
@@ -112,6 +112,12 @@
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -231,15 +237,15 @@
|
||||
<div class="logo-upload">
|
||||
<img src="/images/logo/logo.png" alt="Logo" class="current-logo">
|
||||
<input type="file" id="logo-input" accept="image/*" style="display:none">
|
||||
<button class="btn btn-outline btn-sm" onclick="document.getElementById('logo-input').click()">Change Logo</button>
|
||||
<button class="btn btn-outline btn-sm" id="logo-upload-trigger">Change Logo</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Favicon</label>
|
||||
<div class="logo-upload">
|
||||
<img src="/images/logo/logo.png" alt="Favicon" class="current-favicon" onerror="this.style.display='none'">
|
||||
<img src="/images/logo/logo.png" alt="Favicon" class="current-favicon" id="current-favicon">
|
||||
<input type="file" id="favicon-input" accept="image/*,.ico" style="display:none">
|
||||
<button class="btn btn-outline btn-sm" onclick="document.getElementById('favicon-input').click()">Upload Favicon</button>
|
||||
<button class="btn btn-outline btn-sm" id="favicon-upload-trigger">Upload Favicon</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -639,8 +645,24 @@ Sitemap: https://mspe.pro/sitemap.xml</textarea>
|
||||
setupColorPickers();
|
||||
setupSaveButtons();
|
||||
setupEmailTest();
|
||||
setupAssetControls();
|
||||
});
|
||||
|
||||
function setupAssetControls() {
|
||||
document.getElementById('logo-upload-trigger')?.addEventListener('click', function() {
|
||||
document.getElementById('logo-input')?.click();
|
||||
});
|
||||
|
||||
document.getElementById('favicon-upload-trigger')?.addEventListener('click', function() {
|
||||
document.getElementById('favicon-input')?.click();
|
||||
});
|
||||
|
||||
const favicon = document.getElementById('current-favicon');
|
||||
favicon?.addEventListener('error', function() {
|
||||
this.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function setupTabs() {
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
|
||||
+17
-2
@@ -243,6 +243,12 @@
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -283,7 +289,7 @@
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<button class="btn btn-primary export-btn" onclick="exportSubscribers()">
|
||||
<button class="btn btn-primary export-btn" id="export-subscribers-btn">
|
||||
<i class="fas fa-download"></i> Export CSV
|
||||
</button>
|
||||
|
||||
@@ -371,6 +377,15 @@
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('search-input').addEventListener('input', MSPE.debounce(filterSubscribers, 300));
|
||||
document.getElementById('export-subscribers-btn')?.addEventListener('click', exportSubscribers);
|
||||
|
||||
const tableBody = document.getElementById('subscribers-table-body');
|
||||
tableBody?.addEventListener('click', function(e) {
|
||||
const deleteBtn = e.target.closest('[data-action="delete-subscriber"]');
|
||||
if (!deleteBtn) return;
|
||||
const id = deleteBtn.dataset.id;
|
||||
if (id) deleteSubscriber(id);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSubscribers() {
|
||||
@@ -453,7 +468,7 @@
|
||||
<span class="status-badge ${statusClass}">${subscriber.status}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="action-btn delete" onclick="deleteSubscriber('${subscriber.id}')">
|
||||
<button class="action-btn delete" data-action="delete-subscriber" data-id="${MSPE.escapeHtml(subscriber.id || '')}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
+39
-7
@@ -112,6 +112,12 @@
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -154,7 +160,7 @@
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<button class="btn btn-primary" onclick="openAddMemberModal()">
|
||||
<button class="btn btn-primary" id="add-member-btn">
|
||||
<i class="fas fa-plus"></i> Add Team Member
|
||||
</button>
|
||||
|
||||
@@ -216,7 +222,7 @@
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-title">Add Team Member</h2>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
<button class="modal-close" id="member-modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="member-form">
|
||||
@@ -298,8 +304,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveMember()">
|
||||
<button class="btn btn-outline" id="member-cancel-btn">Cancel</button>
|
||||
<button class="btn btn-primary" id="member-save-btn">
|
||||
<i class="fas fa-save"></i> Save Member
|
||||
</button>
|
||||
</div>
|
||||
@@ -311,6 +317,7 @@
|
||||
let teamMembers = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupEventListeners();
|
||||
loadTeam();
|
||||
document.getElementById('department-filter').addEventListener('change', filterTeam);
|
||||
|
||||
@@ -329,6 +336,31 @@
|
||||
});
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('add-member-btn')?.addEventListener('click', openAddMemberModal);
|
||||
document.getElementById('member-modal-close')?.addEventListener('click', closeModal);
|
||||
document.getElementById('member-cancel-btn')?.addEventListener('click', closeModal);
|
||||
document.getElementById('member-save-btn')?.addEventListener('click', saveMember);
|
||||
|
||||
const modalBackdrop = document.querySelector('#member-modal .modal-backdrop');
|
||||
modalBackdrop?.addEventListener('click', closeModal);
|
||||
|
||||
const grid = document.getElementById('team-grid');
|
||||
grid?.addEventListener('click', function(event) {
|
||||
const actionBtn = event.target.closest('[data-action]');
|
||||
if (!actionBtn) return;
|
||||
|
||||
const { action, id } = actionBtn.dataset;
|
||||
if (action === 'edit' && id) {
|
||||
editMember(id);
|
||||
} else if (action === 'delete' && id) {
|
||||
deleteMember(id);
|
||||
} else if (action === 'change-photo' && id) {
|
||||
changePhoto(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTeam() {
|
||||
try {
|
||||
const response = await MSPE.API.get('team.php');
|
||||
@@ -364,7 +396,7 @@
|
||||
<div class="team-member-photo">
|
||||
<img src="${member.photo || 'https://ui-avatars.com/api/?name=' + encodeURIComponent(member.name) + '&background=random'}" alt="${member.name}">
|
||||
<div class="photo-overlay">
|
||||
<button class="btn btn-sm btn-outline" onclick="editMember('${member.id}')">
|
||||
<button class="btn btn-sm btn-outline" data-action="change-photo" data-id="${member.id}">
|
||||
<i class="fas fa-camera"></i> Change
|
||||
</button>
|
||||
</div>
|
||||
@@ -380,10 +412,10 @@
|
||||
${member.email ? `<a href="mailto:${member.email}" class="social-icon email"><i class="fas fa-envelope"></i></a>` : ''}
|
||||
</div>
|
||||
<div class="team-admin-actions">
|
||||
<button class="btn btn-sm btn-outline" onclick="editMember('${member.id}')">
|
||||
<button class="btn btn-sm btn-outline" data-action="edit" data-id="${member.id}">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteMember('${member.id}')">
|
||||
<button class="btn btn-sm btn-danger" data-action="delete" data-id="${member.id}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+40
-8
@@ -112,6 +112,12 @@
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -154,7 +160,7 @@
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<button class="btn btn-primary" onclick="openAddTestimonialModal()">
|
||||
<button class="btn btn-primary" id="add-testimonial-btn">
|
||||
<i class="fas fa-plus"></i> Add Testimonial
|
||||
</button>
|
||||
|
||||
@@ -243,7 +249,7 @@
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-title">Add Testimonial</h2>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
<button class="modal-close" id="testimonial-modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="testimonial-form">
|
||||
@@ -317,8 +323,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveTestimonial()">
|
||||
<button class="btn btn-outline" id="testimonial-cancel-btn">Cancel</button>
|
||||
<button class="btn btn-primary" id="testimonial-save-btn">
|
||||
<i class="fas fa-save"></i> Save Testimonial
|
||||
</button>
|
||||
</div>
|
||||
@@ -330,9 +336,35 @@
|
||||
let allTestimonials = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupEventListeners();
|
||||
loadTestimonials();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('add-testimonial-btn')?.addEventListener('click', openAddTestimonialModal);
|
||||
document.getElementById('testimonial-modal-close')?.addEventListener('click', closeModal);
|
||||
document.getElementById('testimonial-cancel-btn')?.addEventListener('click', closeModal);
|
||||
document.getElementById('testimonial-save-btn')?.addEventListener('click', saveTestimonial);
|
||||
|
||||
const modalBackdrop = document.querySelector('#testimonial-modal .modal-backdrop');
|
||||
modalBackdrop?.addEventListener('click', closeModal);
|
||||
|
||||
const grid = document.getElementById('testimonials-grid');
|
||||
grid?.addEventListener('click', function(event) {
|
||||
const actionBtn = event.target.closest('[data-action]');
|
||||
if (!actionBtn) return;
|
||||
|
||||
const { action, id, status } = actionBtn.dataset;
|
||||
if (action === 'update-status' && id && status) {
|
||||
updateStatus(id, status);
|
||||
} else if (action === 'edit' && id) {
|
||||
editTestimonial(id);
|
||||
} else if (action === 'delete' && id) {
|
||||
deleteTestimonial(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTestimonials() {
|
||||
try {
|
||||
const response = await MSPE.API.get('testimonials.php');
|
||||
@@ -387,18 +419,18 @@
|
||||
</div>
|
||||
<div class="testimonial-admin-actions">
|
||||
${item.status === 'pending' ? `
|
||||
<button class="btn btn-sm btn-primary" onclick="updateStatus('${item.id}', 'published')">
|
||||
<button class="btn btn-sm btn-primary" data-action="update-status" data-id="${item.id}" data-status="published">
|
||||
<i class="fas fa-check"></i> Approve
|
||||
</button>
|
||||
` : `
|
||||
<button class="btn btn-sm btn-outline" onclick="updateStatus('${item.id}', 'pending')">
|
||||
<button class="btn btn-sm btn-outline" data-action="update-status" data-id="${item.id}" data-status="pending">
|
||||
<i class="fas fa-eye-slash"></i> Unpublish
|
||||
</button>
|
||||
`}
|
||||
<button class="btn btn-sm btn-outline" onclick="editTestimonial('${item.id}')">
|
||||
<button class="btn btn-sm btn-outline" data-action="edit" data-id="${item.id}">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteTestimonial('${item.id}')">
|
||||
<button class="btn btn-sm btn-danger" data-action="delete" data-id="${item.id}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+97
-79
@@ -112,6 +112,12 @@
|
||||
<span>Bookings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/availability.html" class="nav-link">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Availability</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -154,7 +160,7 @@
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<button class="btn btn-primary" onclick="openAddUserModal()">
|
||||
<button class="btn btn-primary" id="add-user-btn">
|
||||
<i class="fas fa-user-plus"></i> Add User
|
||||
</button>
|
||||
|
||||
@@ -223,8 +229,8 @@
|
||||
<div class="roles-grid">
|
||||
<div class="role-card">
|
||||
<div class="role-header">
|
||||
<span class="role-badge super-admin">Super Admin</span>
|
||||
<span class="role-count">1 user</span>
|
||||
<span class="role-badge admin">Administrator</span>
|
||||
<span class="role-count" id="count-admin">— users</span>
|
||||
</div>
|
||||
<ul class="permissions-list">
|
||||
<li><i class="fas fa-check"></i> Full system access</li>
|
||||
@@ -238,7 +244,7 @@
|
||||
<div class="role-card">
|
||||
<div class="role-header">
|
||||
<span class="role-badge editor">Editor</span>
|
||||
<span class="role-count">1 user</span>
|
||||
<span class="role-count" id="count-editor">— users</span>
|
||||
</div>
|
||||
<ul class="permissions-list">
|
||||
<li><i class="fas fa-check"></i> Create/edit content</li>
|
||||
@@ -248,20 +254,6 @@
|
||||
<li><i class="fas fa-times"></i> Site settings</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="role-card">
|
||||
<div class="role-header">
|
||||
<span class="role-badge contributor">Contributor</span>
|
||||
<span class="role-count">1 user</span>
|
||||
</div>
|
||||
<ul class="permissions-list">
|
||||
<li><i class="fas fa-check"></i> Create content</li>
|
||||
<li><i class="fas fa-times"></i> Publish content</li>
|
||||
<li><i class="fas fa-check"></i> Upload media</li>
|
||||
<li><i class="fas fa-times"></i> Delete content</li>
|
||||
<li><i class="fas fa-times"></i> Site settings</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -270,48 +262,12 @@
|
||||
<div class="card" style="margin-top: 2rem;">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-history"></i> Recent Activity</h3>
|
||||
<a href="#" class="card-action">View All</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="activity-timeline">
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon login">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<p><strong>Super Admin</strong> logged in</p>
|
||||
<span class="activity-time">10 minutes ago</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<p><strong>Sarah Johnson</strong> edited <em>"New IT Security Features"</em></p>
|
||||
<span class="activity-time">2 hours ago</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon create">
|
||||
<i class="fas fa-plus"></i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<p><strong>Sarah Johnson</strong> created new news article</p>
|
||||
<span class="activity-time">Yesterday</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon settings">
|
||||
<i class="fas fa-cog"></i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<p><strong>Super Admin</strong> updated site settings</p>
|
||||
<span class="activity-time">2 days ago</span>
|
||||
</div>
|
||||
<div class="activity-timeline" id="activity-timeline">
|
||||
<div class="empty-state" style="padding: 2rem; text-align: center;">
|
||||
<i class="fas fa-history" style="font-size: 2rem; opacity: 0.3; margin-bottom: 0.5rem; display:block;"></i>
|
||||
<p>Activity is recorded in the server error log under <code>[MSPE_AUDIT]</code> entries.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -326,7 +282,7 @@
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-title">Add Admin User</h2>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
<button class="modal-close" id="user-modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="user-form">
|
||||
@@ -361,9 +317,8 @@
|
||||
<div class="form-group">
|
||||
<label>Role *</label>
|
||||
<select class="form-select" name="role" required>
|
||||
<option value="contributor">Contributor</option>
|
||||
<option value="editor">Editor</option>
|
||||
<option value="super-admin">Super Admin</option>
|
||||
<option value="admin">Administrator</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -384,8 +339,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveUser()">
|
||||
<button class="btn btn-outline" id="user-cancel-btn">Cancel</button>
|
||||
<button class="btn btn-primary" id="user-save-btn">
|
||||
<i class="fas fa-save"></i> Save User
|
||||
</button>
|
||||
</div>
|
||||
@@ -398,7 +353,7 @@
|
||||
<div class="modal-container modal-sm">
|
||||
<div class="modal-header">
|
||||
<h2>Change Password</h2>
|
||||
<button class="modal-close" onclick="closePasswordModal()">×</button>
|
||||
<button class="modal-close" id="password-modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="password-form">
|
||||
@@ -419,8 +374,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closePasswordModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="updatePassword()">
|
||||
<button class="btn btn-outline" id="password-cancel-btn">Cancel</button>
|
||||
<button class="btn btn-primary" id="password-save-btn">
|
||||
<i class="fas fa-key"></i> Update Password
|
||||
</button>
|
||||
</div>
|
||||
@@ -432,15 +387,49 @@
|
||||
let allUsers = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupEventListeners();
|
||||
loadUsers();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('add-user-btn')?.addEventListener('click', openAddUserModal);
|
||||
document.getElementById('user-modal-close')?.addEventListener('click', closeModal);
|
||||
document.getElementById('user-cancel-btn')?.addEventListener('click', closeModal);
|
||||
document.getElementById('user-save-btn')?.addEventListener('click', saveUser);
|
||||
|
||||
document.getElementById('password-modal-close')?.addEventListener('click', closePasswordModal);
|
||||
document.getElementById('password-cancel-btn')?.addEventListener('click', closePasswordModal);
|
||||
document.getElementById('password-save-btn')?.addEventListener('click', updatePassword);
|
||||
|
||||
const userBackdrop = document.querySelector('#user-modal .modal-backdrop');
|
||||
userBackdrop?.addEventListener('click', closeModal);
|
||||
|
||||
const passwordBackdrop = document.querySelector('#password-modal .modal-backdrop');
|
||||
passwordBackdrop?.addEventListener('click', closePasswordModal);
|
||||
|
||||
const tbody = document.querySelector('tbody');
|
||||
tbody?.addEventListener('click', function(event) {
|
||||
const actionBtn = event.target.closest('[data-action]');
|
||||
if (!actionBtn) return;
|
||||
|
||||
const { action, id } = actionBtn.dataset;
|
||||
if (action === 'edit' && id) {
|
||||
editUser(id);
|
||||
} else if (action === 'password' && id) {
|
||||
changePassword(id);
|
||||
} else if (action === 'delete' && id) {
|
||||
deleteUser(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const response = await MSPE.API.get('users.php');
|
||||
if (response && response.success) {
|
||||
allUsers = response.data;
|
||||
renderUsers(allUsers);
|
||||
updateRoleCounts(allUsers);
|
||||
} else {
|
||||
document.querySelector('tbody').innerHTML = '<tr><td colspan="6" class="text-center">Failed to load users</td></tr>';
|
||||
}
|
||||
@@ -450,6 +439,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updateRoleCounts(users) {
|
||||
const counts = users.reduce((acc, u) => { acc[u.role] = (acc[u.role] || 0) + 1; return acc; }, {});
|
||||
const adminCount = document.getElementById('count-admin');
|
||||
const editorCount = document.getElementById('count-editor');
|
||||
if (adminCount) adminCount.textContent = `${counts['admin'] || 0} user(s)`;
|
||||
if (editorCount) editorCount.textContent = `${counts['editor'] || 0} user(s)`;
|
||||
}
|
||||
|
||||
function renderUsers(users) {
|
||||
const tbody = document.querySelector('tbody');
|
||||
if (users.length === 0) {
|
||||
@@ -457,36 +454,43 @@
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = users.map(user => `
|
||||
tbody.innerHTML = users.map(user => {
|
||||
const safeName = MSPE.escapeHtml(user.name);
|
||||
const safeUsername = MSPE.escapeHtml(user.username);
|
||||
const safeEmail = MSPE.escapeHtml(user.email);
|
||||
const safeRole = MSPE.escapeHtml(user.role);
|
||||
const safeStatus = MSPE.escapeHtml(user.status);
|
||||
const safeId = MSPE.escapeHtml(user.id);
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-info">
|
||||
<img src="https://ui-avatars.com/api/?name=${encodeURIComponent(user.name)}&background=random" alt="${user.name}">
|
||||
<img src="https://ui-avatars.com/api/?name=${encodeURIComponent(user.name)}&background=random" alt="${safeName}">
|
||||
<div>
|
||||
<span class="name">${user.name}</span>
|
||||
<span class="username">${user.username}</span>
|
||||
<span class="name">${safeName}</span>
|
||||
<span class="username">${safeUsername}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>${user.email}</td>
|
||||
<td><span class="role-badge ${user.role}">${user.role}</span></td>
|
||||
<td>${safeEmail}</td>
|
||||
<td><span class="role-badge ${safeRole}">${safeRole}</span></td>
|
||||
<td>${user.last_login ? MSPE.formatDate(user.last_login) : 'Never'}</td>
|
||||
<td><span class="status-badge ${user.status}">${user.status}</span></td>
|
||||
<td><span class="status-badge ${safeStatus}">${safeStatus}</span></td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-sm btn-outline" onclick="editUser('${user.id}')">
|
||||
<button class="btn btn-sm btn-outline" data-action="edit" data-id="${safeId}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="changePassword('${user.id}')">
|
||||
<button class="btn btn-sm btn-outline" data-action="password" data-id="${safeId}">
|
||||
<i class="fas fa-key"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteUser('${user.id}')">
|
||||
<button class="btn btn-sm btn-danger" data-action="delete" data-id="${safeId}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function openAddUserModal() {
|
||||
@@ -552,7 +556,21 @@
|
||||
const form = document.getElementById('user-form');
|
||||
const formData = new FormData(form);
|
||||
const id = form.dataset.id;
|
||||
|
||||
|
||||
// Client-side password match check for new users
|
||||
if (!id) {
|
||||
const pw = form.querySelector('[name="password"]').value;
|
||||
const cpw = form.querySelector('[name="password_confirm"]').value;
|
||||
if (pw !== cpw) {
|
||||
MSPE.showNotification('Passwords do not match', 'error');
|
||||
return;
|
||||
}
|
||||
if (pw.length < 12) {
|
||||
MSPE.showNotification('Password must be at least 12 characters', 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (id) formData.append('id', id);
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user