Compare commits
4 Commits
ebd7efe21c
...
d5feaacddf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5feaacddf | ||
|
|
927f9f3fe1 | ||
|
|
1b47cb0a69 | ||
|
|
7ebc2f64cf |
@@ -86,6 +86,8 @@ RewriteRule ^failure$ src/pages/payment/failure.php [L]
|
|||||||
RewriteRule ^cancel$ src/pages/payment/cancel.php [L]
|
RewriteRule ^cancel$ src/pages/payment/cancel.php [L]
|
||||||
|
|
||||||
# === ADMIN PAGES ===
|
# === ADMIN PAGES ===
|
||||||
|
RewriteRule ^admin_trips_events_courses$ src/admin/admin_trips_events_courses.php [L]
|
||||||
|
RewriteRule ^admin_bookings$ src/admin/admin_bookings.php [L]
|
||||||
RewriteRule ^admin_members$ src/admin/admin_members.php [L]
|
RewriteRule ^admin_members$ src/admin/admin_members.php [L]
|
||||||
RewriteRule ^admin_payments$ src/admin/admin_payments.php [L]
|
RewriteRule ^admin_payments$ src/admin/admin_payments.php [L]
|
||||||
RewriteRule ^admin_web_users$ src/admin/admin_web_users.php [L]
|
RewriteRule ^admin_web_users$ src/admin/admin_web_users.php [L]
|
||||||
@@ -147,7 +149,7 @@ RewriteRule ^link_membership_user$ src/processors/link_membership_user.php [L]
|
|||||||
RewriteRule ^unlink_membership_user$ src/processors/unlink_membership_user.php [L]
|
RewriteRule ^unlink_membership_user$ src/processors/unlink_membership_user.php [L]
|
||||||
|
|
||||||
# Blog routes
|
# Blog routes
|
||||||
RewriteRule ^admin_blogs$ src/pages/blog/admin_blogs.php [L]
|
RewriteRule ^admin_blogs$ src/admin/admin_blogs.php [L]
|
||||||
RewriteRule ^user_blogs$ src/pages/blog/user_blogs.php [L]
|
RewriteRule ^user_blogs$ src/pages/blog/user_blogs.php [L]
|
||||||
RewriteRule ^blog_read$ src/pages/blog/blog_read.php [L]
|
RewriteRule ^blog_read$ src/pages/blog/blog_read.php [L]
|
||||||
RewriteRule ^blog_edit$ src/pages/blog/blog_edit.php [L]
|
RewriteRule ^blog_edit$ src/pages/blog/blog_edit.php [L]
|
||||||
@@ -161,7 +163,7 @@ RewriteRule ^autosave$ src/processors/blog/autosave.php [L]
|
|||||||
|
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
php_flag display_errors Off
|
php_flag display_errors On
|
||||||
# php_value error_reporting -1
|
# php_value error_reporting -1
|
||||||
RedirectMatch 403 ^/\.well-known
|
RedirectMatch 403 ^/\.well-known
|
||||||
Options -Indexes
|
Options -Indexes
|
||||||
|
|||||||
215
.htaccess copy
215
.htaccess copy
@@ -1,215 +0,0 @@
|
|||||||
# URL Rewrite Rules - Maps old URLs to new directory structure during migration
|
|
||||||
<IfModule mod_rewrite.c>
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteBase /
|
|
||||||
|
|
||||||
# Don't rewrite existing files or directories
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
|
|
||||||
# === STRIP .PHP EXTENSION ===
|
|
||||||
# Redirect /page.php to /page (301 permanent redirect)
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^(.+)\.php$ /$1 [R=301,L]
|
|
||||||
# Internally rewrite /page to /page.php if page.php exists
|
|
||||||
RewriteCond %{REQUEST_FILENAME}\.php -f
|
|
||||||
RewriteRule ^(.+)$ $1.php [L]
|
|
||||||
|
|
||||||
# === AUTH PAGES ===
|
|
||||||
RewriteRule ^login$ src/pages/auth/login.php [L]
|
|
||||||
RewriteRule ^register$ src/pages/auth/register.php [L]
|
|
||||||
RewriteRule ^forgot_password$ src/pages/auth/forgot_password.php [L]
|
|
||||||
RewriteRule ^reset_password$ src/pages/auth/reset_password.php [L]
|
|
||||||
RewriteRule ^verify$ src/pages/auth/verify.php [L]
|
|
||||||
RewriteRule ^resend_verification$ src/pages/auth/resend_verification.php [L]
|
|
||||||
RewriteRule ^change_password$ src/pages/auth/change_password.php [L]
|
|
||||||
RewriteRule ^update_password$ src/pages/auth/update_password.php [L]
|
|
||||||
|
|
||||||
# === MEMBERSHIP PAGES ===
|
|
||||||
RewriteRule ^membership$ src/pages/memberships/membership.php [L]
|
|
||||||
RewriteRule ^membership_details$ src/pages/memberships/membership_details.php [L]
|
|
||||||
RewriteRule ^membership_application$ src/pages/memberships/membership_application.php [L]
|
|
||||||
RewriteRule ^membership_payment$ src/pages/memberships/membership_payment.php [L]
|
|
||||||
RewriteRule ^renew_membership$ src/pages/memberships/renew_membership.php [L]
|
|
||||||
RewriteRule ^member_info$ src/pages/memberships/member_info.php [L]
|
|
||||||
|
|
||||||
# === BOOKING PAGES ===
|
|
||||||
RewriteRule ^bookings$ src/pages/bookings/bookings.php [L]
|
|
||||||
RewriteRule ^campsites$ src/pages/bookings/campsites.php [L]
|
|
||||||
RewriteRule ^campsite_booking$ src/pages/bookings/campsite_booking.php [L]
|
|
||||||
RewriteRule ^add_campsite$ src/pages/add_campsite.php [L]
|
|
||||||
RewriteRule ^trips$ src/pages/bookings/trips.php [L]
|
|
||||||
RewriteRule ^trip-details$ src/pages/bookings/trip-details.php [L]
|
|
||||||
RewriteRule ^course_details$ src/pages/bookings/course_details.php [L]
|
|
||||||
RewriteRule ^driver_training$ src/pages/bookings/driver_training.php [L]
|
|
||||||
|
|
||||||
# === SHOP PAGES ===
|
|
||||||
RewriteRule ^view_cart$ src/pages/shop/view_cart.php [L]
|
|
||||||
RewriteRule ^add_to_cart$ src/pages/shop/add_to_cart.php [L]
|
|
||||||
RewriteRule ^bar_tabs$ src/pages/shop/bar_tabs.php [L]
|
|
||||||
RewriteRule ^payment_confirmation$ src/pages/shop/payment_confirmation.php [L]
|
|
||||||
RewriteRule ^confirm$ src/pages/shop/confirm.php [L]
|
|
||||||
RewriteRule ^confirm2$ src/pages/shop/confirm2.php [L]
|
|
||||||
|
|
||||||
# === GALLERY PAGES ===
|
|
||||||
RewriteRule ^gallery$ src/pages/gallery/gallery.php [L]
|
|
||||||
RewriteRule ^create_album$ src/pages/gallery/create_album.php [L]
|
|
||||||
RewriteRule ^edit_album$ src/pages/gallery/create_album.php [L]
|
|
||||||
RewriteRule ^view_album$ src/pages/gallery/view_album.php [L]
|
|
||||||
|
|
||||||
# === EVENTS & BLOG PAGES ===
|
|
||||||
RewriteRule ^events$ src/pages/events/events.php [L]
|
|
||||||
RewriteRule ^blog$ src/pages/blog/blog.php [L]
|
|
||||||
RewriteRule ^blog_details$ src/pages/blog/blog_details.php [L]
|
|
||||||
RewriteRule ^best_of_the_eastern_cape_2024$ src/pages/events/best_of_the_eastern_cape_2024.php [L]
|
|
||||||
RewriteRule ^2025_agm_minutes$ src/pages/events/2025_agm_minutes.php [L]
|
|
||||||
RewriteRule ^agm_content$ src/pages/events/agm_content.php [L]
|
|
||||||
RewriteRule ^instapage$ src/pages/events/instapage.php [L]
|
|
||||||
|
|
||||||
# === OTHER PAGES ===
|
|
||||||
RewriteRule ^about$ src/pages/other/about.php [L]
|
|
||||||
RewriteRule ^contact$ src/pages/other/contact.php [L]
|
|
||||||
RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.php [L]
|
|
||||||
RewriteRule ^track-map$ src/pages/track-map.php [L]
|
|
||||||
RewriteRule ^404$ src/pages/other/404.php [L]
|
|
||||||
RewriteRule ^account_settings$ src/pages/other/account_settings.php [L]
|
|
||||||
RewriteRule ^rescue_recovery$ src/pages/other/rescue_recovery.php [L]
|
|
||||||
RewriteRule ^bush_mechanics$ src/pages/other/bush_mechanics.php [L]
|
|
||||||
RewriteRule ^indemnity$ src/pages/other/indemnity.php [L]
|
|
||||||
RewriteRule ^indemnity_waiver$ src/pages/other/indemnity_waiver.php [L]
|
|
||||||
RewriteRule ^basic_indemnity$ src/pages/other/basic_indemnity.php [L]
|
|
||||||
RewriteRule ^view_indemnity$ src/pages/other/view_indemnity.php [L]
|
|
||||||
|
|
||||||
# === ADMIN PAGES ===
|
|
||||||
RewriteRule ^admin_members$ src/admin/admin_members.php [L]
|
|
||||||
RewriteRule ^admin_payments$ src/admin/admin_payments.php [L]
|
|
||||||
RewriteRule ^admin_web_users$ src/admin/admin_web_users.php [L]
|
|
||||||
RewriteRule ^admin_events$ src/admin/admin_events.php [L]
|
|
||||||
RewriteRule ^admin_course_bookings$ src/admin/admin_course_bookings.php [L]
|
|
||||||
RewriteRule ^admin_camp_bookings$ src/admin/admin_camp_bookings.php [L]
|
|
||||||
RewriteRule ^admin_trip_bookings$ src/admin/admin_trip_bookings.php [L]
|
|
||||||
RewriteRule ^admin_visitors$ src/admin/admin_visitors.php [L]
|
|
||||||
RewriteRule ^admin_efts$ src/admin/admin_efts.php [L]
|
|
||||||
RewriteRule ^admin_trips$ src/admin/admin_trips.php [L]
|
|
||||||
RewriteRule ^manage_events$ src/admin/manage_events.php [L]
|
|
||||||
RewriteRule ^manage_trips$ src/admin/manage_trips.php [L]
|
|
||||||
|
|
||||||
# === API/AJAX ENDPOINTS ===
|
|
||||||
RewriteRule ^fetch_users$ src/api/fetch_users.php [L]
|
|
||||||
RewriteRule ^fetch_drinks$ src/api/fetch_drinks.php [L]
|
|
||||||
RewriteRule ^fetch_bar_tabs$ src/api/fetch_bar_tabs.php [L]
|
|
||||||
RewriteRule ^get_campsites$ src/api/get_campsites.php [L]
|
|
||||||
RewriteRule ^get_tab_total$ src/api/get_tab_total.php [L]
|
|
||||||
RewriteRule ^google_validate_login$ src/api/google_validate_login.php [L]
|
|
||||||
|
|
||||||
# === PROCESSORS ===
|
|
||||||
RewriteRule ^validate_login$ src/processors/validate_login.php [L]
|
|
||||||
RewriteRule ^register_user$ src/processors/register_user.php [L]
|
|
||||||
RewriteRule ^process_application$ src/processors/process_application.php [L]
|
|
||||||
RewriteRule ^process_booking$ src/processors/process_booking.php [L]
|
|
||||||
RewriteRule ^process_camp_booking$ src/processors/process_camp_booking.php [L]
|
|
||||||
RewriteRule ^process_course_booking$ src/processors/process_course_booking.php [L]
|
|
||||||
RewriteRule ^process_trip_booking$ src/processors/process_trip_booking.php [L]
|
|
||||||
RewriteRule ^process_membership_payment$ src/processors/process_membership_payment.php [L]
|
|
||||||
RewriteRule ^process_payments$ src/processors/process_payments.php [L]
|
|
||||||
RewriteRule ^process_eft$ src/processors/process_eft.php [L]
|
|
||||||
RewriteRule ^submit_order$ src/processors/submit_order.php [L]
|
|
||||||
RewriteRule ^submit_pop$ src/processors/submit_pop.php [L]
|
|
||||||
RewriteRule ^process_signature$ src/processors/process_signature.php [L]
|
|
||||||
RewriteRule ^create_bar_tab$ src/processors/create_bar_tab.php [L]
|
|
||||||
RewriteRule ^update_application$ src/processors/update_application.php [L]
|
|
||||||
RewriteRule ^update_user$ src/processors/update_user.php [L]
|
|
||||||
RewriteRule ^upload_profile_picture$ src/processors/upload_profile_picture.php [L]
|
|
||||||
RewriteRule ^send_reset_link$ src/processors/send_reset_link.php [L]
|
|
||||||
RewriteRule ^logout$ src/processors/logout.php [L]
|
|
||||||
RewriteRule ^process_trip$ src/processors/process_trip.php [L]
|
|
||||||
RewriteRule ^process_event$ src/processors/process_event.php [L]
|
|
||||||
RewriteRule ^toggle_trip_published$ src/processors/toggle_trip_published.php [L]
|
|
||||||
RewriteRule ^toggle_event_published$ src/processors/toggle_event_published.php [L]
|
|
||||||
RewriteRule ^delete_trip$ src/processors/delete_trip.php [L]
|
|
||||||
RewriteRule ^delete_event$ src/processors/delete_event.php [L]
|
|
||||||
RewriteRule ^save_album$ src/processors/save_album.php [L]
|
|
||||||
RewriteRule ^update_album$ src/processors/update_album.php [L]
|
|
||||||
RewriteRule ^delete_album$ src/processors/delete_album.php [L]
|
|
||||||
RewriteRule ^delete_photo$ src/processors/delete_photo.php [L]
|
|
||||||
RewriteRule ^get_album_photos$ src/processors/get_album_photos.php [L]
|
|
||||||
RewriteRule ^link_membership_user$ src/processors/link_membership_user.php [L]
|
|
||||||
RewriteRule ^unlink_membership_user$ src/processors/unlink_membership_user.php [L]
|
|
||||||
|
|
||||||
# Blog routes
|
|
||||||
RewriteRule ^admin_blogs$ src/pages/blog/admin_blogs.php [L]
|
|
||||||
RewriteRule ^user_blogs$ src/pages/blog/user_blogs.php [L]
|
|
||||||
RewriteRule ^blog_read$ src/pages/blog/blog_read.php [L]
|
|
||||||
RewriteRule ^blog_edit$ src/pages/blog/blog_edit.php [L]
|
|
||||||
RewriteRule ^blog_create$ src/processors/blog/blog_create.php [L]
|
|
||||||
RewriteRule ^blog_delete$ src/processors/blog/blog_delete.php [L]
|
|
||||||
RewriteRule ^publish_blog$ src/processors/blog/publish_blog.php [L]
|
|
||||||
RewriteRule ^blog_unpublish$ src/processors/blog/blog_unpublish.php [L]
|
|
||||||
RewriteRule ^submit_blog$ src/processors/blog/submit_blog.php [L]
|
|
||||||
RewriteRule ^upload_blog_image$ src/processors/blog/upload_blog_image.php [L]
|
|
||||||
RewriteRule ^autosave$ src/processors/blog/autosave.php [L]
|
|
||||||
|
|
||||||
</IfModule>
|
|
||||||
|
|
||||||
php_flag display_errors On
|
|
||||||
# php_value error_reporting -1
|
|
||||||
RedirectMatch 403 ^/\.well-known
|
|
||||||
Options -Indexes
|
|
||||||
|
|
||||||
<FilesMatch "\.(env|sql|bak|zip|tar|gz|ini)$">
|
|
||||||
Require all denied
|
|
||||||
</FilesMatch>
|
|
||||||
|
|
||||||
ErrorDocument 404 /404.php
|
|
||||||
|
|
||||||
<RequireAll>
|
|
||||||
Require all granted
|
|
||||||
Require not ip 4.222.252.98
|
|
||||||
Require not ip 4.222.252.97
|
|
||||||
</RequireAll>
|
|
||||||
|
|
||||||
<Files .env>
|
|
||||||
Order allow,deny
|
|
||||||
Deny from all
|
|
||||||
</Files>
|
|
||||||
|
|
||||||
|
|
||||||
# ALL CUSTOM ENTRIES SHOULD GO ABOVE THIS LINE
|
|
||||||
# BEGIN IWORX header
|
|
||||||
# This file was created by InterWorx-CP
|
|
||||||
# You may modify this file, but any changes made between
|
|
||||||
# BEGIN IWORX and END IWORX tags may be lost on future
|
|
||||||
# updates. Additionally, changes NOT made between these
|
|
||||||
# tags will not be recognized in the SiteWorx interface.
|
|
||||||
# END IWORX header
|
|
||||||
|
|
||||||
# BEGIN IWORX accesscontrol
|
|
||||||
# END IWORX accesscontrol
|
|
||||||
|
|
||||||
# BEGIN IWORX errordocs
|
|
||||||
# END IWORX errordocs
|
|
||||||
|
|
||||||
# BEGIN IWORX mimetypes
|
|
||||||
# END IWORX mimetypes
|
|
||||||
|
|
||||||
# BEGIN IWORX handlers
|
|
||||||
# END IWORX handlers
|
|
||||||
|
|
||||||
# BEGIN IWORX charset
|
|
||||||
# END IWORX charset
|
|
||||||
|
|
||||||
# BEGIN IWORX redirects
|
|
||||||
# END IWORX redirects
|
|
||||||
|
|
||||||
# BEGIN IWORX phpvars
|
|
||||||
# END IWORX phpvars
|
|
||||||
|
|
||||||
# BEGIN IWORX dirindex
|
|
||||||
# END IWORX dirindex
|
|
||||||
|
|
||||||
# BEGIN IWORX hotlink
|
|
||||||
# END IWORX hotlink
|
|
||||||
|
|
||||||
# BEGIN IWORX passwordprotection
|
|
||||||
# END IWORX passwordprotection
|
|
||||||
|
|
||||||
73
assets/css/notifications.css
Normal file
73
assets/css/notifications.css
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
.notif-avatar-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.notif-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -6px;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: none;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.notif-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 44px;
|
||||||
|
width: 320px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.notif-panel .notif-empty {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.notif-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #f1f1f1;
|
||||||
|
}
|
||||||
|
.notif-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.notif-item-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.notif-item-body {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.notif-item-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.notif-item-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.notif-close {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
103
assets/js/notifications_panel.js
Normal file
103
assets/js/notifications_panel.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/* notifications.js - small admin notification panel
|
||||||
|
Requires jQuery. */
|
||||||
|
(function($){
|
||||||
|
function timeAgo(ts){
|
||||||
|
var seconds = Math.floor((Date.now() - (new Date(ts)).getTime())/1000);
|
||||||
|
if (seconds < 60) return seconds + 's ago';
|
||||||
|
var minutes = Math.floor(seconds/60);
|
||||||
|
if (minutes < 60) return minutes + 'm ago';
|
||||||
|
var hours = Math.floor(minutes/60);
|
||||||
|
if (hours < 24) return hours + 'h ago';
|
||||||
|
var days = Math.floor(hours/24);
|
||||||
|
return days + 'd ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNotifications(list){
|
||||||
|
var $panel = $('#notif-panel');
|
||||||
|
$panel.empty();
|
||||||
|
if (!list || list.length === 0) {
|
||||||
|
$panel.append('<div class="notif-empty">No notifications</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.forEach(function(n){
|
||||||
|
var actorAvatar = (n.data && n.data.actor_avatar) ? n.data.actor_avatar : 'assets/images/icons/user.png';
|
||||||
|
// Prefer the notification payload title (n.data.title). Do NOT fall back to the event string.
|
||||||
|
var title = (n.data && n.data.title) ? n.data.title : 'Notification';
|
||||||
|
var time = n.time_created || new Date().toISOString();
|
||||||
|
var read = (n.read_by && Array.isArray(n.read_by) && n.read_by.length>0);
|
||||||
|
var $item = $('<div class="notif-item" data-id="'+n.id+'">');
|
||||||
|
$item.append('<img class="notif-item-avatar" src="'+actorAvatar+'" alt="avatar">');
|
||||||
|
var $body = $('<div class="notif-item-body">');
|
||||||
|
$body.append('<div class="notif-item-title">'+escapeHtml(title)+'</div>');
|
||||||
|
$body.append('<div class="notif-item-meta">'+timeAgo(time)+'</div>');
|
||||||
|
$item.append($body);
|
||||||
|
$item.append('<button class="notif-close" title="Mark read">×</button>');
|
||||||
|
$panel.append($item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return String(str).replace(/[&<>"'`]/g, function(s){ return {'&':'&','<':'<','>':'>','"':'"',"'":"'",'`':'`'}[s]; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchAndRender(adminId){
|
||||||
|
$.getJSON('/src/api/notifications.php', { action: 'fetch' }, function(resp){
|
||||||
|
if (resp && resp.success) {
|
||||||
|
renderNotifications(resp.notifications);
|
||||||
|
if (resp.unread_count && resp.unread_count > 0) {
|
||||||
|
$('#notif-badge').text(resp.unread_count).show();
|
||||||
|
} else {
|
||||||
|
$('#notif-badge').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch only unread count (used on page load so badge shows without opening panel)
|
||||||
|
function fetchUnreadCount(){
|
||||||
|
$.getJSON('/src/api/notifications.php', { action: 'fetch' }, function(resp){
|
||||||
|
if (resp && resp.success) {
|
||||||
|
if (resp.unread_count && resp.unread_count > 0) {
|
||||||
|
$('#notif-badge').text(resp.unread_count).show();
|
||||||
|
} else {
|
||||||
|
$('#notif-badge').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function(){
|
||||||
|
var $container = $('.notif-avatar-container');
|
||||||
|
if (!$container.length) return;
|
||||||
|
var adminId = $container.data('admin-id');
|
||||||
|
// ensure badge is populated on page load
|
||||||
|
fetchUnreadCount();
|
||||||
|
$container.on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$('#notif-panel').toggle();
|
||||||
|
if ($('#notif-panel').is(':visible')) fetchAndRender(adminId);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.notif-close', function(e){
|
||||||
|
e.stopPropagation();
|
||||||
|
var $it = $(this).closest('.notif-item');
|
||||||
|
var id = $it.data('id');
|
||||||
|
if (!id) return;
|
||||||
|
$.post('/src/api/notifications.php', { action: 'mark_read', id: id }, function(resp){
|
||||||
|
if (resp && resp.success) {
|
||||||
|
$it.remove();
|
||||||
|
// refresh count
|
||||||
|
fetchAndRender(adminId);
|
||||||
|
}
|
||||||
|
}, 'json');
|
||||||
|
});
|
||||||
|
|
||||||
|
// click outside to close
|
||||||
|
$(document).on('click', function(e){
|
||||||
|
if (!$(e.target).closest('#notif-panel, .notif-avatar-container').length) {
|
||||||
|
$('#notif-panel').hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
13
docs/migrations/006b_create_notifications_table.sql
Normal file
13
docs/migrations/006b_create_notifications_table.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
-- Migration: create notifications table (corrected: `read_by` nullable)
|
||||||
|
CREATE TABLE IF NOT EXISTS `notifications` (
|
||||||
|
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`user_id` INT DEFAULT NULL,
|
||||||
|
`event` VARCHAR(100) NOT NULL,
|
||||||
|
`sub_feed` VARCHAR(100) DEFAULT NULL,
|
||||||
|
`data` TEXT DEFAULT NULL,
|
||||||
|
`target_url` VARCHAR(1024) DEFAULT NULL,
|
||||||
|
`read_by` TEXT DEFAULT NULL,
|
||||||
|
`time_created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX (`user_id`),
|
||||||
|
INDEX (`sub_feed`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
55
header.php
55
header.php
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UNIFIED HEADER TEMPLATE
|
* UNIFIED HEADER TEMPLATE
|
||||||
*
|
*
|
||||||
@@ -25,6 +26,7 @@ require_once($rootDir . "/src/config/env.php");
|
|||||||
require_once($rootDir . "/src/config/session.php");
|
require_once($rootDir . "/src/config/session.php");
|
||||||
require_once($rootDir . "/src/config/connection.php");
|
require_once($rootDir . "/src/config/connection.php");
|
||||||
require_once($rootDir . "/src/config/functions.php");
|
require_once($rootDir . "/src/config/functions.php");
|
||||||
|
require_once($rootDir . "/src/helpers/notification_helper.php");
|
||||||
|
|
||||||
$is_logged_in = isset($_SESSION['user_id']);
|
$is_logged_in = isset($_SESSION['user_id']);
|
||||||
if (isset($_SESSION['user_id'])) {
|
if (isset($_SESSION['user_id'])) {
|
||||||
@@ -186,8 +188,7 @@ if ($headerStyle === 'light') {
|
|||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
<?php if ($headerStyle === 'light'): ?>
|
<?php if ($headerStyle === 'light'): ?>.page-banner-area {
|
||||||
.page-banner-area {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
@@ -214,6 +215,7 @@ if ($headerStyle === 'light') {
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -283,12 +285,8 @@ if ($headerStyle === 'light') {
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="admin_web_users">Website Users</a></li>
|
<li><a href="admin_web_users">Website Users</a></li>
|
||||||
<li><a href="admin_members">4WDCSA Members</a></li>
|
<li><a href="admin_members">4WDCSA Members</a></li>
|
||||||
<li><a href="admin_blogs">Manage Blogs</a></li>
|
<li><a href="admin_trips_events_courses">Trips, Events & Courses</a></li>
|
||||||
<li><a href="admin_events">Manage Events</a></li>
|
<li><a href="admin_bookings">Bookings</a></li>
|
||||||
<li><a href="admin_trips">Manage Trips</a></li>
|
|
||||||
<li><a href="admin_courses">Manage Courses</a></li>
|
|
||||||
<li><a href="admin_trip_bookings">Trip Bookings</a></li>
|
|
||||||
<li><a href="admin_course_bookings">Course Bookings</a></li>
|
|
||||||
<li><a href="admin_transactions">iKhokha Payment History</a></li>
|
<li><a href="admin_transactions">iKhokha Payment History</a></li>
|
||||||
<!-- <li><a href="process_payments">Process Payments</a></li> -->
|
<!-- <li><a href="process_payments">Process Payments</a></li> -->
|
||||||
<?php if ($role === 'superadmin') { ?>
|
<?php if ($role === 'superadmin') { ?>
|
||||||
@@ -348,11 +346,19 @@ if ($headerStyle === 'light') {
|
|||||||
<div class="profile-menu">
|
<div class="profile-menu">
|
||||||
<div class="profile-info">
|
<div class="profile-info">
|
||||||
<span style="color: <?php echo $textColor; ?>;">Welcome, <?php echo $_SESSION['first_name']; ?></span>
|
<span style="color: <?php echo $textColor; ?>;">Welcome, <?php echo $_SESSION['first_name']; ?></span>
|
||||||
|
<div class="notif-avatar-container" data-admin-id="<?php echo intval($_SESSION['user_id'] ?? 0); ?>">
|
||||||
<a href="account_settings">
|
<a href="account_settings">
|
||||||
<img src="<?php echo $_SESSION['profile_pic']; ?>?v=<?php echo time(); ?>" alt="Profile Picture" class="profile-pic">
|
<img src="<?php echo $_SESSION['profile_pic']; ?>?v=<?php echo time(); ?>" alt="Profile Picture" class="profile-pic">
|
||||||
</a>
|
</a>
|
||||||
|
<span id="notif-badge" class="notif-badge"></span>
|
||||||
|
</div>
|
||||||
|
<div id="notif-panel" class="notif-panel" style="display:none;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if ($role === 'admin' || $role === 'superadmin') { ?>
|
||||||
|
<link rel="stylesheet" href="assets/css/notifications.css">
|
||||||
|
<script src="assets/js/notifications_panel.js" defer></script>
|
||||||
|
<?php } ?>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<a href="login" class="theme-btn style-two bgc-secondary">
|
<a href="login" class="theme-btn style-two bgc-secondary">
|
||||||
<span data-hover="Log In">Log In</span>
|
<span data-hover="Log In">Log In</span>
|
||||||
@@ -367,11 +373,44 @@ if ($headerStyle === 'light') {
|
|||||||
<!--End Header Upper-->
|
<!--End Header Upper-->
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<a href="https://wa.me/27790652795?text=Hi,%20I%20would%20like%20to%20know%20more%20about%20your%20club!"
|
||||||
|
class="whatsapp-float"
|
||||||
|
target="_blank"
|
||||||
|
aria-label="Chat on WhatsApp">
|
||||||
|
<i class="fab fa-whatsapp"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.whatsapp-float {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background-color: #25D366;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 60px;
|
||||||
|
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whatsapp-float:hover {
|
||||||
|
background-color: #1ebe5d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const profileInfo = document.querySelector('.profile-info');
|
const profileInfo = document.querySelector('.profile-info');
|
||||||
if (profileInfo) {
|
if (profileInfo) {
|
||||||
profileInfo.addEventListener('click', function(event) {
|
profileInfo.addEventListener('click', function(event) {
|
||||||
|
// Ignore clicks on the notifications avatar so the notif panel
|
||||||
|
// can handle its own toggle without also toggling the profile dropdown.
|
||||||
|
if (event.target.closest && event.target.closest('.notif-avatar-container')) return;
|
||||||
const dropdownMenu = document.querySelector('.dropdown-menu2');
|
const dropdownMenu = document.querySelector('.dropdown-menu2');
|
||||||
if (dropdownMenu) {
|
if (dropdownMenu) {
|
||||||
dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block';
|
dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block';
|
||||||
|
|||||||
@@ -180,8 +180,6 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- About Us Area start -->
|
<!-- About Us Area start -->
|
||||||
<section class="about-us-area py-100 rpb-90 rel z-1">
|
<section class="about-us-area py-100 rpb-90 rel z-1">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -305,7 +303,6 @@ if (countUpcomingTrips() > 0) { ?>
|
|||||||
</section>
|
</section>
|
||||||
<!-- Features Area end -->
|
<!-- Features Area end -->
|
||||||
|
|
||||||
|
|
||||||
<!-- Hotel Area start -->
|
<!-- Hotel Area start -->
|
||||||
<section class="hotel-area bgc-black py-100 rel z-1">
|
<section class="hotel-area bgc-black py-100 rel z-1">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|||||||
@@ -120,3 +120,9 @@
|
|||||||
[2025-12-15 15:48:43] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
[2025-12-15 15:48:43] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
[2025-12-15 15:48:43] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
[2025-12-15 15:48:43] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
[2025-12-15 15:48:43] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
[2025-12-15 15:48:43] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-16 22:38:31] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-16 22:38:31] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-16 22:38:31] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-17 12:35:13] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-17 12:35:13] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-17 12:35:13] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
|||||||
230
src/admin/admin_bookings.php
Normal file
230
src/admin/admin_bookings.php
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
<?php
|
||||||
|
$headerStyle = 'light';
|
||||||
|
$rootPath = dirname(dirname(__DIR__));
|
||||||
|
include_once($rootPath . '/header.php');
|
||||||
|
checkAdmin();
|
||||||
|
|
||||||
|
// Ensure $conn is available; show a friendly message if DB is down
|
||||||
|
$conn = $conn ?? null;
|
||||||
|
if (!$conn) {
|
||||||
|
// Render a simple error notice inside the page layout and stop further DB queries
|
||||||
|
echo '<section class="tour-list-page py-10 rel z-1">\n <div class="container">\n <div class="alert alert-danger">Database connection unavailable. Please check your configuration and logs.</div>\n </div>\n</section>';
|
||||||
|
include_once($rootPath . '/components/insta_footer.php');
|
||||||
|
// Stop execution to prevent subsequent fatal errors from using $conn
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
/* Shared table and tab styles */
|
||||||
|
.admin-tabs { text-align: center; display: flex; margin-bottom: 20px; margin-top: 20px; }
|
||||||
|
.admin-tab { padding: 10px 30px; margin-right: 20px; cursor: pointer; border: 2px solid; border-radius: 50px; background: none; font-size: 18px; color: #484848; }
|
||||||
|
.admin-tab.active { color: white; background: #63ab45; font-weight: bold; }
|
||||||
|
.tab-content { display: none; }
|
||||||
|
.tab-content.active { display: block; }
|
||||||
|
table { width: 100%; border-collapse: separate; border-spacing: 0; margin: 10px 0; }
|
||||||
|
thead th { cursor: pointer; text-align: left; padding: 10px; font-weight: bold; position: relative; }
|
||||||
|
thead th::after { content: '\25B2'; font-size: 0.8em; position: absolute; right: 10px; opacity: 0; transition: opacity 0.2s; }
|
||||||
|
thead th.asc::after { content: '\25B2'; opacity: 1; }
|
||||||
|
thead th.desc::after { content: '\25BC'; opacity: 1; }
|
||||||
|
tbody tr:nth-child(odd) { background-color: transparent; }
|
||||||
|
tbody tr:nth-child(even) { background-color: rgb(255, 255, 255); border-radius: 10px; }
|
||||||
|
tbody td { padding: 5px; }
|
||||||
|
tbody tr:nth-child(even) td:first-child { border-top-left-radius: 10px; border-bottom-left-radius: 10px; }
|
||||||
|
tbody tr:nth-child(even) td:last-child { border-top-right-radius: 10px; border-bottom-right-radius: 10px; }
|
||||||
|
.filter-input { width: 100%; padding: 5px; font-size: 16px; background-color: rgb(255, 255, 255); border-radius: 25px; }
|
||||||
|
.trip-booking { color: #484848; background: #f9f9f7; border: 1px solid #d8d8d8; border-radius: 10px; margin-top: 15px; margin-bottom: 15px; }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
// Tab switching
|
||||||
|
const tabs = document.querySelectorAll('.admin-tab');
|
||||||
|
const contents = document.querySelectorAll('.tab-content');
|
||||||
|
tabs.forEach((tab, idx) => {
|
||||||
|
tab.addEventListener('click', function() {
|
||||||
|
tabs.forEach(t => t.classList.remove('active'));
|
||||||
|
contents.forEach(c => c.classList.remove('active'));
|
||||||
|
tab.classList.add('active');
|
||||||
|
contents[idx].classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Table sorting/filtering (same as before)
|
||||||
|
document.querySelectorAll("table").forEach((table) => {
|
||||||
|
const headers = table.querySelectorAll("thead th");
|
||||||
|
const rows = Array.from(table.querySelectorAll("tbody tr"));
|
||||||
|
const filterInput = table.previousElementSibling;
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
header.addEventListener("click", () => {
|
||||||
|
const sortedRows = rows.sort((a, b) => {
|
||||||
|
const aText = a.cells[index].textContent.trim().toLowerCase();
|
||||||
|
const bText = b.cells[index].textContent.trim().toLowerCase();
|
||||||
|
if (aText < bText) return -1;
|
||||||
|
if (aText > bText) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
if (header.classList.contains("asc")) {
|
||||||
|
header.classList.remove("asc");
|
||||||
|
header.classList.add("desc");
|
||||||
|
sortedRows.reverse();
|
||||||
|
} else {
|
||||||
|
headers.forEach(h => h.classList.remove("asc", "desc"));
|
||||||
|
header.classList.add("asc");
|
||||||
|
}
|
||||||
|
const tbody = table.querySelector("tbody");
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
sortedRows.forEach(row => tbody.appendChild(row));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (rows.length === 0) {
|
||||||
|
filterInput.style.display = "none";
|
||||||
|
} else {
|
||||||
|
filterInput.addEventListener("input", function() {
|
||||||
|
const filterValue = filterInput.value.trim().toLowerCase();
|
||||||
|
rows.forEach(row => {
|
||||||
|
const rowText = row.textContent.trim().toLowerCase();
|
||||||
|
row.style.display = rowText.includes(filterValue) ? "" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
$pageTitle = 'Manage Bookings';
|
||||||
|
$breadcrumbs = [['Home' => 'index']];
|
||||||
|
require_once($rootPath . '/components/banner.php');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<section class="tour-list-page py-10 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="admin-tabs">
|
||||||
|
<button class="admin-tab active">Trip Bookings</button>
|
||||||
|
<button class="admin-tab">Course Bookings</button>
|
||||||
|
<button class="admin-tab">Camp Bookings</button>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content active">
|
||||||
|
<?php // Trip Bookings Tab
|
||||||
|
$tripsSql = "SELECT trip_id, trip_name FROM trips";
|
||||||
|
$tripsResult = $conn->query($tripsSql);
|
||||||
|
if ($tripsResult && $tripsResult->num_rows > 0) {
|
||||||
|
while ($trip = $tripsResult->fetch_assoc()) {
|
||||||
|
$tripId = $trip['trip_id'];
|
||||||
|
$tripName = htmlspecialchars($trip['trip_name']);
|
||||||
|
echo "<div class='trip-booking' data-aos='fade-up' data-aos-duration='1500' data-aos-offset='50'>";
|
||||||
|
echo "<div style='padding:10px;'>";
|
||||||
|
echo "<h4>{$tripName}</h4>";
|
||||||
|
$bookingsSql = "SELECT b.user_id, b.num_vehicles, b.num_adults, b.num_children, b.num_pensioners, b.radio, b.status, u.first_name, u.last_name, u.profile_pic, (b.total_amount - b.discount_amount) AS paid FROM bookings b INNER JOIN users u ON b.user_id = u.user_id WHERE b.trip_id = ?";
|
||||||
|
$stmt = $conn->prepare($bookingsSql);
|
||||||
|
$stmt->bind_param('i', $tripId);
|
||||||
|
$stmt->execute();
|
||||||
|
$bookingsResult = $stmt->get_result();
|
||||||
|
if ($bookingsResult->num_rows > 0) {
|
||||||
|
echo '<input type="text" class="filter-input" placeholder="Filter results...">';
|
||||||
|
echo '<table><thead><tr><th></th><th>Name</th><th>Vehicles</th><th>Adults</th><th>Children</th><th>Pensioners</th><th>Radio</th><th>Status</th><th>Amount</th></tr></thead><tbody>';
|
||||||
|
while ($booking = $bookingsResult->fetch_assoc()) {
|
||||||
|
$userName = htmlspecialchars($booking['first_name'] . ' ' . $booking['last_name']);
|
||||||
|
$numVehicles = htmlspecialchars($booking['num_vehicles']);
|
||||||
|
$numAdults = htmlspecialchars($booking['num_adults']);
|
||||||
|
$numPensioners = htmlspecialchars($booking['num_pensioners']);
|
||||||
|
$numChildren = htmlspecialchars($booking['num_children']);
|
||||||
|
$radio = $booking['radio'] == 1 ? "YES" : "NO";
|
||||||
|
$status = htmlspecialchars($booking['status']);
|
||||||
|
$paid = "R " . number_format($booking['paid'], 2);
|
||||||
|
echo "<tr><td><img src='".$booking['profile_pic']."' alt='Profile Picture' class='profile-pic'></td><td>{$userName}</td><td>{$numVehicles}</td><td>{$numAdults}</td><td>{$numChildren}</td><td>{$numPensioners}</td><td>{$radio}</td><td>{$status}</td><td>{$paid}</td></tr>";
|
||||||
|
}
|
||||||
|
echo '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
echo '<p>No bookings found for this trip.</p>';
|
||||||
|
}
|
||||||
|
echo "</div></div>";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo '<p>No trips found.</p>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<?php // Course Bookings Tab
|
||||||
|
$courseSql = "SELECT date, course_id, course_type FROM courses";
|
||||||
|
$courseResult = $conn->query($courseSql);
|
||||||
|
if ($courseResult && $courseResult->num_rows > 0) {
|
||||||
|
while ($course = $courseResult->fetch_assoc()) {
|
||||||
|
$course_id = $course['course_id'];
|
||||||
|
$date = $course['date'];
|
||||||
|
$type = htmlspecialchars($course['course_type']);
|
||||||
|
if ($type === "driver_training") {
|
||||||
|
$course_name = "Basic 4X4 Driver Training Course ".$date;
|
||||||
|
} elseif ($type === "bush_mechanics") {
|
||||||
|
$course_name = "Bush Mechanics Course ".$date;
|
||||||
|
} elseif ($type === "rescue_recovery") {
|
||||||
|
$course_name = "Rescue & Recovery Training Course ".$date;
|
||||||
|
} else {
|
||||||
|
$course_name = "General Course ".$date;
|
||||||
|
}
|
||||||
|
echo "<div class='trip-booking' data-aos='fade-up' data-aos-duration='1500' data-aos-offset='50'>";
|
||||||
|
echo "<div style='padding:10px;'>";
|
||||||
|
echo "<h4>{$course_name}</h4>";
|
||||||
|
$bookingsSql = "SELECT b.user_id, b.num_adults, b.total_amount, b.status, b.course_non_members, u.first_name, u.last_name, u.profile_pic FROM bookings b INNER JOIN users u ON b.user_id = u.user_id WHERE b.course_id = ?";
|
||||||
|
if ($stmt = $conn->prepare($bookingsSql)) {
|
||||||
|
$stmt->bind_param('i', $course_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$bookingsResult = $stmt->get_result();
|
||||||
|
} else {
|
||||||
|
echo "Error in prepared statement: " . $conn->error;
|
||||||
|
}
|
||||||
|
if ($bookingsResult && $bookingsResult->num_rows > 0) {
|
||||||
|
echo '<input type="text" class="filter-input" placeholder="Filter results...">';
|
||||||
|
echo '<table><thead><tr><th></th><th>Name</th><th>Members</th><th>Non-Members</th><th>Status</th><th>Amount</th></tr></thead><tbody>';
|
||||||
|
while ($booking = $bookingsResult->fetch_assoc()) {
|
||||||
|
$userName = htmlspecialchars($booking['first_name'] . ' ' . $booking['last_name']);
|
||||||
|
$members = htmlspecialchars($booking['num_adults']);
|
||||||
|
$non_members = htmlspecialchars($booking['course_non_members']);
|
||||||
|
$status = htmlspecialchars($booking['status']);
|
||||||
|
$paid = "R " . number_format($booking['total_amount'], 2);
|
||||||
|
echo "<tr><td><img src='".$booking['profile_pic']."' alt='Profile Picture' class='profile-pic'></td><td>{$userName}</td><td>{$members}</td><td>{$non_members}</td><td>{$status}</td><td>{$paid}</td></tr>";
|
||||||
|
}
|
||||||
|
echo '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
echo '<p>No bookings found for this course.</p>';
|
||||||
|
}
|
||||||
|
echo "</div></div>";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo '<p>No courses found.</p>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<?php // Camp Bookings Tab
|
||||||
|
echo "<div class='trip-booking' data-aos='fade-up' data-aos-duration='1500' data-aos-offset='50'>";
|
||||||
|
echo "<div style='padding:10px;'>";
|
||||||
|
echo "<h4>BASE4 Camping</h4>";
|
||||||
|
$bookingsSql = "SELECT b.user_id, b.from_date, b.to_date, b.num_vehicles, b.num_adults, b.num_children, b.add_firewood, b.status, u.first_name, u.last_name, (b.total_amount - b.discount_amount) AS paid FROM bookings b INNER JOIN users u ON b.user_id = u.user_id WHERE b.booking_type = 'camping'";
|
||||||
|
$stmt = $conn->prepare($bookingsSql);
|
||||||
|
$stmt->execute();
|
||||||
|
$bookingsResult = $stmt->get_result();
|
||||||
|
if ($bookingsResult && $bookingsResult->num_rows > 0) {
|
||||||
|
echo '<input type="text" class="filter-input" placeholder="Filter results...">';
|
||||||
|
echo '<table><thead><tr><th>Name</th><th>From</th><th>To</th><th>Vehicles</th><th>Adults</th><th>Children</th><th>Add Firewood</th><th>Status</th><th>Amount</th></tr></thead><tbody>';
|
||||||
|
while ($booking = $bookingsResult->fetch_assoc()) {
|
||||||
|
$userName = htmlspecialchars($booking['first_name'] . ' ' . $booking['last_name']);
|
||||||
|
$numVehicles = htmlspecialchars($booking['num_vehicles']);
|
||||||
|
$from = htmlspecialchars($booking['from_date']);
|
||||||
|
$to = htmlspecialchars($booking['to_date']);
|
||||||
|
$numAdults = htmlspecialchars($booking['num_adults']);
|
||||||
|
$numChildren = htmlspecialchars($booking['num_children']);
|
||||||
|
$radio = $booking['add_firewood'] == 1 ? "YES" : "NO";
|
||||||
|
$status = htmlspecialchars($booking['status']);
|
||||||
|
$paid = "R " . number_format($booking['paid'], 2);
|
||||||
|
echo "<tr><td>{$userName}</td><td>{$from}</td><td>{$to}</td><td>{$numVehicles}</td><td>{$numAdults}</td><td>{$numChildren}</td><td>{$radio}</td><td>{$status}</td><td>{$paid}</td></tr>";
|
||||||
|
}
|
||||||
|
echo '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
echo '<p>No bookings found for this trip.</p>';
|
||||||
|
}
|
||||||
|
echo "</div></div>";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
<?php
|
|
||||||
$headerStyle = 'light';
|
|
||||||
$rootPath = dirname(dirname(__DIR__));
|
|
||||||
include_once($rootPath . '/header.php');
|
|
||||||
checkAdmin();
|
|
||||||
|
|
||||||
?>
|
|
||||||
<style>
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: separate;
|
|
||||||
border-spacing: 0;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th {
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
padding: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th::after {
|
|
||||||
content: '\25B2';
|
|
||||||
/* Up arrow */
|
|
||||||
font-size: 0.8em;
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th.asc::after {
|
|
||||||
content: '\25B2';
|
|
||||||
/* Up arrow */
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th.desc::after {
|
|
||||||
content: '\25BC';
|
|
||||||
/* Down arrow */
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(odd) {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(even) {
|
|
||||||
background-color: rgb(255, 255, 255);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody td {
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(even) td:first-child {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(even) td:last-child {
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
border-bottom-right-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px;
|
|
||||||
/* margin-bottom: 20px; */
|
|
||||||
font-size: 16px;
|
|
||||||
background-color: rgb(255, 255, 255);
|
|
||||||
border-radius: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trip-booking {
|
|
||||||
color: #484848;
|
|
||||||
background: #f9f9f7;
|
|
||||||
border: 1px solid #d8d8d8;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const tables = document.querySelectorAll("table");
|
|
||||||
tables.forEach((table) => {
|
|
||||||
const headers = table.querySelectorAll("thead th");
|
|
||||||
const rows = Array.from(table.querySelectorAll("tbody tr"));
|
|
||||||
const filterInput = table.previousElementSibling;
|
|
||||||
|
|
||||||
headers.forEach((header, index) => {
|
|
||||||
header.addEventListener("click", () => {
|
|
||||||
const sortedRows = rows.sort((a, b) => {
|
|
||||||
const aText = a.cells[index].textContent.trim().toLowerCase();
|
|
||||||
const bText = b.cells[index].textContent.trim().toLowerCase();
|
|
||||||
|
|
||||||
if (aText < bText) return -1;
|
|
||||||
if (aText > bText) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (header.classList.contains("asc")) {
|
|
||||||
header.classList.remove("asc");
|
|
||||||
header.classList.add("desc");
|
|
||||||
sortedRows.reverse();
|
|
||||||
} else {
|
|
||||||
headers.forEach(h => h.classList.remove("asc", "desc"));
|
|
||||||
header.classList.add("asc");
|
|
||||||
}
|
|
||||||
|
|
||||||
const tbody = table.querySelector("tbody");
|
|
||||||
tbody.innerHTML = "";
|
|
||||||
sortedRows.forEach(row => tbody.appendChild(row));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (rows.length === 0) {
|
|
||||||
filterInput.style.display = "none";
|
|
||||||
} else {
|
|
||||||
filterInput.addEventListener("input", function() {
|
|
||||||
const filterValue = filterInput.value.trim().toLowerCase();
|
|
||||||
rows.forEach(row => {
|
|
||||||
const rowText = row.textContent.trim().toLowerCase();
|
|
||||||
row.style.display = rowText.includes(filterValue) ? "" : "none";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<?php
|
|
||||||
$bannerFolder = 'assets/images/banners/';
|
|
||||||
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
|
||||||
|
|
||||||
$randomBanner = 'assets/images/base4/camping.jpg'; // default fallback
|
|
||||||
if (!empty($bannerImages)) {
|
|
||||||
$randomBanner = $bannerImages[array_rand($bannerImages)];
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<section class="page-banner-area pt-50 pb-35 rel z-1 bgs-cover" style="background-image: url('<?php echo $randomBanner; ?>');">
|
|
||||||
<div class="banner-overlay"></div>
|
|
||||||
<div class="container">
|
|
||||||
<div class="banner-inner text-white mb-50">
|
|
||||||
<h2 class="page-title mb-10" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">4WDCSA Camping Bookings</h2>
|
|
||||||
<nav aria-label="breadcrumb">
|
|
||||||
<ol class="breadcrumb justify-content-center mb-20" data-aos="fade-right" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
|
||||||
<li class="breadcrumb-item"><a href="index.php">Home</a></li>
|
|
||||||
<li class="breadcrumb-item active">Camping Bookings</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="tour-list-page py-10 rel z-1">
|
|
||||||
<div class="container">
|
|
||||||
<?php
|
|
||||||
|
|
||||||
echo "<div class='trip-booking' data-aos='fade-up' data-aos-duration='1500' data-aos-offset='50'>";
|
|
||||||
echo "<div style='padding:10px;'>";
|
|
||||||
echo "<h4>BASE4 Camping</h4>";
|
|
||||||
|
|
||||||
// Fetch bookings for the current trip
|
|
||||||
$bookingsSql = "SELECT b.user_id, b.from_date, b.to_date, b.num_vehicles, b.num_adults, b.num_children, b.add_firewood, b.status,
|
|
||||||
u.first_name, u.last_name,
|
|
||||||
(b.total_amount - b.discount_amount) AS paid
|
|
||||||
FROM bookings b
|
|
||||||
INNER JOIN users u ON b.user_id = u.user_id
|
|
||||||
WHERE b.booking_type = 'camping'";
|
|
||||||
$stmt = $conn->prepare($bookingsSql);
|
|
||||||
$stmt->execute();
|
|
||||||
$bookingsResult = $stmt->get_result();
|
|
||||||
|
|
||||||
if ($bookingsResult->num_rows > 0) {
|
|
||||||
echo '<input type="text" class="filter-input" placeholder="Filter results...">';
|
|
||||||
echo '<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>From</th>
|
|
||||||
<th>To</th>
|
|
||||||
<th>Vehicles</th>
|
|
||||||
<th>Adults</th>
|
|
||||||
<th>Children</th>
|
|
||||||
<th>Add Firewood</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>';
|
|
||||||
while ($booking = $bookingsResult->fetch_assoc()) {
|
|
||||||
$userName = htmlspecialchars($booking['first_name'] . ' ' . $booking['last_name']);
|
|
||||||
$numVehicles = htmlspecialchars($booking['num_vehicles']);
|
|
||||||
$from = htmlspecialchars($booking['from_date']);
|
|
||||||
$to = htmlspecialchars($booking['to_date']);
|
|
||||||
$numAdults = htmlspecialchars($booking['num_adults']);
|
|
||||||
$numChildren = htmlspecialchars($booking['num_children']);
|
|
||||||
$radio = $booking['add_firewood'] == 1 ? "YES" : "NO";
|
|
||||||
$status = htmlspecialchars($booking['status']);
|
|
||||||
$paid = "R " . number_format($booking['paid'], 2);
|
|
||||||
|
|
||||||
echo "<tr>
|
|
||||||
<td>{$userName}</td>
|
|
||||||
<td>{$from}</td>
|
|
||||||
<td>{$to}</td>
|
|
||||||
<td>{$numVehicles}</td>
|
|
||||||
<td>{$numAdults}</td>
|
|
||||||
<td>{$numChildren}</td>
|
|
||||||
<td>{$radio}</td>
|
|
||||||
<td>{$status}</td>
|
|
||||||
<td>{$paid}</td>
|
|
||||||
</tr>";
|
|
||||||
}
|
|
||||||
echo '</tbody></table>';
|
|
||||||
} else {
|
|
||||||
echo '<p>No bookings found for this trip.</p>';
|
|
||||||
}
|
|
||||||
echo "</div>";
|
|
||||||
echo "</div>";
|
|
||||||
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
<?php
|
|
||||||
$headerStyle = 'light';
|
|
||||||
$rootPath = dirname(dirname(__DIR__));
|
|
||||||
include_once($rootPath . '/header.php');
|
|
||||||
checkAdmin();
|
|
||||||
|
|
||||||
// Fetch all trips
|
|
||||||
$courseSql = "SELECT date, course_id, course_type FROM courses";
|
|
||||||
|
|
||||||
$courseResult = $conn->query($courseSql);
|
|
||||||
if (!$courseResult) {
|
|
||||||
echo "Error in SQL query: " . $conn->error;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<style>
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: separate;
|
|
||||||
border-spacing: 0;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th {
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
padding: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th::after {
|
|
||||||
content: '\25B2';
|
|
||||||
/* Up arrow */
|
|
||||||
font-size: 0.8em;
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th.asc::after {
|
|
||||||
content: '\25B2';
|
|
||||||
/* Up arrow */
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th.desc::after {
|
|
||||||
content: '\25BC';
|
|
||||||
/* Down arrow */
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(odd) {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(even) {
|
|
||||||
background-color: rgb(255, 255, 255);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody td {
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(even) td:first-child {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(even) td:last-child {
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
border-bottom-right-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px;
|
|
||||||
/* margin-bottom: 20px; */
|
|
||||||
font-size: 16px;
|
|
||||||
background-color: rgb(255, 255, 255);
|
|
||||||
border-radius: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trip-booking {
|
|
||||||
color: #484848;
|
|
||||||
background: #f9f9f7;
|
|
||||||
border: 1px solid #d8d8d8;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const tables = document.querySelectorAll("table");
|
|
||||||
tables.forEach((table) => {
|
|
||||||
const headers = table.querySelectorAll("thead th");
|
|
||||||
const rows = Array.from(table.querySelectorAll("tbody tr"));
|
|
||||||
const filterInput = table.previousElementSibling;
|
|
||||||
|
|
||||||
headers.forEach((header, index) => {
|
|
||||||
header.addEventListener("click", () => {
|
|
||||||
const sortedRows = rows.sort((a, b) => {
|
|
||||||
const aText = a.cells[index].textContent.trim().toLowerCase();
|
|
||||||
const bText = b.cells[index].textContent.trim().toLowerCase();
|
|
||||||
|
|
||||||
if (aText < bText) return -1;
|
|
||||||
if (aText > bText) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (header.classList.contains("asc")) {
|
|
||||||
header.classList.remove("asc");
|
|
||||||
header.classList.add("desc");
|
|
||||||
sortedRows.reverse();
|
|
||||||
} else {
|
|
||||||
headers.forEach(h => h.classList.remove("asc", "desc"));
|
|
||||||
header.classList.add("asc");
|
|
||||||
}
|
|
||||||
|
|
||||||
const tbody = table.querySelector("tbody");
|
|
||||||
tbody.innerHTML = "";
|
|
||||||
sortedRows.forEach(row => tbody.appendChild(row));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (rows.length === 0) {
|
|
||||||
filterInput.style.display = "none";
|
|
||||||
} else {
|
|
||||||
filterInput.addEventListener("input", function() {
|
|
||||||
const filterValue = filterInput.value.trim().toLowerCase();
|
|
||||||
rows.forEach(row => {
|
|
||||||
const rowText = row.textContent.trim().toLowerCase();
|
|
||||||
row.style.display = rowText.includes(filterValue) ? "" : "none";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<?php
|
|
||||||
$bannerFolder = 'assets/images/banners/';
|
|
||||||
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
|
||||||
|
|
||||||
$randomBanner = 'assets/images/base4/camping.jpg'; // default fallback
|
|
||||||
if (!empty($bannerImages)) {
|
|
||||||
$randomBanner = $bannerImages[array_rand($bannerImages)];
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<section class="page-banner-area pt-50 pb-35 rel z-1 bgs-cover" style="background-image: url('<?php echo $randomBanner; ?>');">
|
|
||||||
<div class="banner-overlay"></div>
|
|
||||||
<div class="container">
|
|
||||||
<div class="banner-inner text-white mb-50">
|
|
||||||
<h2 class="page-title mb-10" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">4WDCSA Course Bookings</h2>
|
|
||||||
<nav aria-label="breadcrumb">
|
|
||||||
<ol class="breadcrumb justify-content-center mb-20" data-aos="fade-right" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
|
||||||
<li class="breadcrumb-item"><a href="index.php">Home</a></li>
|
|
||||||
<li class="breadcrumb-item active">Course Bookings</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="tour-list-page py-10 rel z-1">
|
|
||||||
<div class="container">
|
|
||||||
<?php
|
|
||||||
if ($courseResult->num_rows > 0) {
|
|
||||||
while ($course = $courseResult->fetch_assoc()) {
|
|
||||||
$course_id = $course['course_id'];
|
|
||||||
$date = $course['date'];
|
|
||||||
$type = htmlspecialchars($course['course_type']);
|
|
||||||
if ($type === "driver_training") {
|
|
||||||
$course_name = "Basic 4X4 Driver Training Course ".$date;
|
|
||||||
} elseif ($type === "bush_mechanics") {
|
|
||||||
$course_name = "Bush Mechanics Course ".$date;
|
|
||||||
} elseif ($type === "rescue_recovery") {
|
|
||||||
$course_name = "Rescue & Recovery Training Course ".$date;
|
|
||||||
} else {
|
|
||||||
$course_name = "General Course ".$date; // Default fallback description
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "<div class='trip-booking' data-aos='fade-up' data-aos-duration='1500' data-aos-offset='50'>";
|
|
||||||
echo "<div style='padding:10px;'>";
|
|
||||||
echo "<h4>{$course_name}</h4>";
|
|
||||||
|
|
||||||
// Fetch bookings for the current trip
|
|
||||||
$bookingsSql = "SELECT b.user_id, b.num_adults, b.total_amount, b.status, b.course_non_members,
|
|
||||||
u.first_name, u.last_name, u.profile_pic
|
|
||||||
FROM bookings b
|
|
||||||
INNER JOIN users u ON b.user_id = u.user_id
|
|
||||||
WHERE b.course_id = ?";
|
|
||||||
if ($stmt = $conn->prepare($bookingsSql)) {
|
|
||||||
$stmt->bind_param('i', $course_id);
|
|
||||||
$stmt->execute();
|
|
||||||
$bookingsResult = $stmt->get_result();
|
|
||||||
} else {
|
|
||||||
echo "Error in prepared statement: " . $conn->error;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if ($bookingsResult->num_rows > 0) {
|
|
||||||
echo '<input type="text" class="filter-input" placeholder="Filter results...">';
|
|
||||||
echo '<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Members</th>
|
|
||||||
<th>Non-Members</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>';
|
|
||||||
while ($booking = $bookingsResult->fetch_assoc()) {
|
|
||||||
$userName = htmlspecialchars($booking['first_name'] . ' ' . $booking['last_name']);
|
|
||||||
$members = htmlspecialchars($booking['num_adults']);
|
|
||||||
$non_members = htmlspecialchars($booking['course_non_members']);
|
|
||||||
$status = htmlspecialchars($booking['status']);
|
|
||||||
$paid = "R " . number_format($booking['total_amount'], 2);
|
|
||||||
|
|
||||||
echo "<tr>
|
|
||||||
<td><img src=".$booking['profile_pic']." alt='Profile Picture' class='profile-pic'></td>
|
|
||||||
<td>{$userName}</td>
|
|
||||||
<td>{$members}</td>
|
|
||||||
<td>{$non_members}</td>
|
|
||||||
<td>{$status}</td>
|
|
||||||
<td>{$paid}</td>
|
|
||||||
</tr>";
|
|
||||||
}
|
|
||||||
echo '</tbody></table>';
|
|
||||||
} else {
|
|
||||||
echo '<p>No bookings found for this trip.</p>';
|
|
||||||
}
|
|
||||||
echo "</div>";
|
|
||||||
echo "</div>";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
echo '<p>No courses found.</p>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
<?php
|
|
||||||
$rootPath = dirname(dirname(__DIR__));
|
|
||||||
require_once($rootPath . "/src/config/env.php");
|
|
||||||
require_once($rootPath . '/src/config/connection.php');
|
|
||||||
require_once($rootPath . '/src/config/functions.php');
|
|
||||||
require_once($rootPath . '/header.php');
|
|
||||||
checkAdmin();
|
|
||||||
checkUserSession();
|
|
||||||
|
|
||||||
$pageTitle = 'Manage Courses';
|
|
||||||
$breadcrumbs = [['Home' => 'index']];
|
|
||||||
require_once($rootPath . '/components/banner.php');
|
|
||||||
|
|
||||||
// Fetch all courses
|
|
||||||
$courses_query = "
|
|
||||||
SELECT
|
|
||||||
course_id, course_type, date, capacity, booked, cost_members, cost_nonmembers, instructor, instructor_email, code
|
|
||||||
FROM courses
|
|
||||||
ORDER BY date DESC
|
|
||||||
";
|
|
||||||
|
|
||||||
$result = $conn->query($courses_query);
|
|
||||||
$courses = [];
|
|
||||||
if ($result && $result->num_rows > 0) {
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$courses[] = $row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- Courses Management Area start -->
|
|
||||||
<section class="blog-list-page py-100 rel z-1">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
|
||||||
<h2 style="margin: 0;">Manage Courses</h2>
|
|
||||||
<a href="manage_courses" class="theme-btn create-album-btn">
|
|
||||||
<i class="far fa-plus"></i> New Course
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (isset($_SESSION['message'])): ?>
|
|
||||||
<div class="alert alert-warning message-box">
|
|
||||||
<?php echo $_SESSION['message']; ?>
|
|
||||||
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
|
||||||
</div>
|
|
||||||
<?php unset($_SESSION['message']);
|
|
||||||
endif;?>
|
|
||||||
|
|
||||||
<?php if (count($courses) > 0): ?>
|
|
||||||
<input type="text" class="filter-input" placeholder="Filter courses...">
|
|
||||||
<div class="courses-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
|
||||||
<?php foreach ($courses as $course):
|
|
||||||
$available = intval($course['capacity']) - intval($course['booked']);
|
|
||||||
$type_label = strtoupper($course['course_type']);
|
|
||||||
if ($course['course_type'] == 'driver_training') {
|
|
||||||
$type_label = 'Driver Training';
|
|
||||||
} elseif ($course['course_type'] == 'bush_mechanics') {
|
|
||||||
$type_label = 'Bush Mechanics';
|
|
||||||
} elseif ($course['course_type'] == 'rescue_recovery') {
|
|
||||||
$type_label = 'Rescue & Recovery';
|
|
||||||
} elseif ($course['course_type'] == 'ladies_driver_training') {
|
|
||||||
$type_label = 'Ladies Driver Training';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
|
||||||
<div class="content" style="width:100%;">
|
|
||||||
<div class="destination-header d-flex align-items-start gap-3">
|
|
||||||
<div>
|
|
||||||
<h5 class="mb-0"><?php echo $type_label; ?></h5>
|
|
||||||
<small class="text-muted"><?php echo htmlspecialchars($course['course_type']); ?> — <?php echo date('M d, Y', strtotime($course['date'])); ?></small><br
|
|
||||||
<small class="text-muted"><?php echo $course['code'] ? 'Code: ' . htmlspecialchars($course['code']) : ''; ?></small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p style="margin: 10px 0;">
|
|
||||||
<strong>Instructor:</strong> <?php echo htmlspecialchars($course['instructor']); ?> (<?php echo htmlspecialchars($course['instructor_email']); ?>)<br>
|
|
||||||
<strong>Capacity:</strong> <?php echo intval($course['booked']); ?> / <?php echo intval($course['capacity']); ?> <strong>Available:</strong> <?php echo $available; ?><br>
|
|
||||||
<strong>Costs:</strong> Members: R <?php echo number_format($course['cost_members'],2); ?> | Non-Members: R <?php echo number_format($course['cost_nonmembers'],2); ?>
|
|
||||||
</p>
|
|
||||||
<div class="destination-footer">
|
|
||||||
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
|
||||||
<a href="manage_courses?course_id=<?php echo $course['course_id']; ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
|
||||||
<button type="button" class="delete-course" data-course-id="<?php echo $course['course_id']; ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="no-courses">
|
|
||||||
<p>No courses found. <a href="manage_courses">Create one</a></p>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<!-- Courses Management Area end -->
|
|
||||||
|
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const filterInput = document.querySelector('.filter-input');
|
|
||||||
const cards = document.querySelectorAll('.destination-item');
|
|
||||||
|
|
||||||
if (cards.length === 0 && filterInput) {
|
|
||||||
filterInput.style.display = "none";
|
|
||||||
} else if (filterInput) {
|
|
||||||
filterInput.addEventListener("input", function() {
|
|
||||||
const filterValue = filterInput.value.trim().toLowerCase();
|
|
||||||
cards.forEach(card => {
|
|
||||||
const cardText = card.textContent.trim().toLowerCase();
|
|
||||||
card.style.display = cardText.includes(filterValue) ? "" : "none";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle delete button clicks
|
|
||||||
document.querySelectorAll('.delete-course').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
if (!confirm('Are you sure you want to delete this course? This action cannot be undone.')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const courseId = this.dataset.courseId;
|
|
||||||
const card = this.closest('.destination-item');
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('course_id', courseId);
|
|
||||||
|
|
||||||
fetch('delete_course', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'success') {
|
|
||||||
alert('Course deleted successfully!');
|
|
||||||
card.remove();
|
|
||||||
if (document.querySelectorAll('.destination-item').length === 0) {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
alert('Error: ' + data.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Error:', err);
|
|
||||||
alert('Delete failed due to network error.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
<?php
|
|
||||||
$rootPath = dirname(dirname(__DIR__));
|
|
||||||
require_once($rootPath . "/src/config/env.php");
|
|
||||||
require_once($rootPath . "/src/config/connection.php");
|
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
|
||||||
require_once($rootPath . "/header.php");
|
|
||||||
checkAdmin();
|
|
||||||
checkUserSession();
|
|
||||||
|
|
||||||
$pageTitle = 'Manage Events';
|
|
||||||
$breadcrumbs = [['Home' => 'index']];
|
|
||||||
require_once($rootPath . '/components/banner.php');
|
|
||||||
|
|
||||||
// Fetch all events
|
|
||||||
$events_query = "
|
|
||||||
SELECT
|
|
||||||
event_id, name, type, location, date, image, published
|
|
||||||
FROM events
|
|
||||||
ORDER BY date DESC
|
|
||||||
";
|
|
||||||
|
|
||||||
$result = $conn->query($events_query);
|
|
||||||
$events = [];
|
|
||||||
if ($result && $result->num_rows > 0) {
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$events[] = $row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.image {
|
|
||||||
width: 300px;
|
|
||||||
/* Set your desired width */
|
|
||||||
height: 250px;
|
|
||||||
/* Set your desired height */
|
|
||||||
overflow: hidden;
|
|
||||||
/* Hide any overflow */
|
|
||||||
display: block;
|
|
||||||
/* Ensure proper block behavior */
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image img {
|
|
||||||
width: 100%;
|
|
||||||
/* Image scales to fill the container */
|
|
||||||
height: 100%;
|
|
||||||
/* Image scales to fill the container */
|
|
||||||
object-fit: cover;
|
|
||||||
/* Fills the container while maintaining aspect ratio */
|
|
||||||
object-position: center;
|
|
||||||
/* Aligns the center of the image with the center of the container */
|
|
||||||
display: block;
|
|
||||||
/* Prevents inline whitespace issues */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const filterInput = document.querySelector('.filter-input');
|
|
||||||
const cards = document.querySelectorAll('.destination-item');
|
|
||||||
|
|
||||||
if (cards.length === 0 && filterInput) {
|
|
||||||
filterInput.style.display = "none";
|
|
||||||
} else if (filterInput) {
|
|
||||||
filterInput.addEventListener("input", function() {
|
|
||||||
const filterValue = filterInput.value.trim().toLowerCase();
|
|
||||||
cards.forEach(card => {
|
|
||||||
const cardText = card.textContent.trim().toLowerCase();
|
|
||||||
card.style.display = cardText.includes(filterValue) ? "" : "none";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$pageTitle = 'Manage Events';
|
|
||||||
$breadcrumbs = [['Home' => 'index']];
|
|
||||||
require_once($rootPath . '/components/banner.php');
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- Events Management Area start -->
|
|
||||||
<section class="blog-list-page py-100 rel z-1">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
|
||||||
<h2 style="margin: 0;">Manage Events</h2>
|
|
||||||
<a href="manage_events" class="theme-btn create-album-btn">
|
|
||||||
<i class="far fa-plus"></i> New Event
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<?php if (isset($_SESSION['message'])): ?>
|
|
||||||
<div class="alert alert-warning message-box">
|
|
||||||
<?php echo $_SESSION['message']; ?>
|
|
||||||
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
|
||||||
</div>
|
|
||||||
<?php unset($_SESSION['message']);
|
|
||||||
endif;
|
|
||||||
|
|
||||||
if (count($events) > 0) {
|
|
||||||
echo '<input type="text" class="filter-input" placeholder="Filter events...">';
|
|
||||||
echo '<div class="events-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
|
|
||||||
|
|
||||||
foreach ($events as $event) {
|
|
||||||
$eventImagePath = $event['image'] ? htmlspecialchars($event['image']) : 'assets/images/placeholder.jpg';
|
|
||||||
$publishStatusBadge = $event['published'] == 1 ? 'PUBLISHED' : 'DRAFT';
|
|
||||||
|
|
||||||
echo '
|
|
||||||
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
|
||||||
<div class="image" style="width:300px;height:250px;">
|
|
||||||
<img src="' . $eventImagePath . '" alt="' . htmlspecialchars($event['name']) . '">
|
|
||||||
</div>
|
|
||||||
<div class="content" style="width:100%;">
|
|
||||||
<div class="destination-header d-flex align-items-start gap-3">
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-dark mb-1">' . strtoupper($publishStatusBadge) . '</span>
|
|
||||||
<h5 class="mb-0">' . htmlspecialchars($event['name']) . '</h5>
|
|
||||||
<small class="text-muted">📍 ' . htmlspecialchars($event['location']) . '</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p style="margin: 10px 0;">
|
|
||||||
<strong>Type:</strong> ' . htmlspecialchars($event['type']) . '<br>
|
|
||||||
<strong>Date:</strong> ' . convertDate($event['date']) . '
|
|
||||||
</p>
|
|
||||||
<div class="destination-footer">
|
|
||||||
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
|
||||||
<a href="manage_events?event_id=' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
|
||||||
<button type="button" class="toggle-publish" data-event-id="' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($event['published'] == 1 ? 'Unpublish' : 'Publish') . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($event['published'] == 1 ? 'cloud_off' : 'cloud_upload') . '</span></button>
|
|
||||||
<button type="button" class="delete-event" data-event-id="' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '</div>';
|
|
||||||
} else {
|
|
||||||
echo '<div class="no-events">
|
|
||||||
<p>No events found. <a href="manage_events">Create one</a></p>
|
|
||||||
</div>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<!-- Events Management Area end -->
|
|
||||||
|
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
|
||||||
<script>
|
|
||||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
||||||
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
|
|
||||||
|
|
||||||
// Handle publish/unpublish button clicks
|
|
||||||
document.querySelectorAll('.toggle-publish').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
const eventId = this.dataset.eventId;
|
|
||||||
const currentIcon = this.querySelector('.material-icons').textContent;
|
|
||||||
const isPublished = currentIcon === 'cloud_off';
|
|
||||||
const action = isPublished ? 'Unpublish' : 'Publish';
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('event_id', eventId);
|
|
||||||
|
|
||||||
fetch('toggle_event_published', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'success') {
|
|
||||||
alert(action + ' successful!');
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
alert(action + ' failed: ' + data.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Error:', err);
|
|
||||||
alert(action + ' failed due to network error.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle delete button clicks
|
|
||||||
document.querySelectorAll('.delete-event').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
if (!confirm('Are you sure you want to delete this event? This action cannot be undone.')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventId = this.dataset.eventId;
|
|
||||||
const card = this.closest('.destination-item');
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('event_id', eventId);
|
|
||||||
|
|
||||||
fetch('delete_event', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'success') {
|
|
||||||
card.style.animation = 'fadeOut 0.3s ease-out';
|
|
||||||
setTimeout(() => {
|
|
||||||
card.remove();
|
|
||||||
if (document.querySelectorAll('.destination-item').length === 0) {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
alert('Error: ' + data.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Error:', err);
|
|
||||||
alert('Delete failed due to network error.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
<?php
|
|
||||||
$headerStyle = 'light';
|
|
||||||
$rootPath = dirname(dirname(__DIR__));
|
|
||||||
include_once($rootPath . '/header.php');
|
|
||||||
checkAdmin();
|
|
||||||
|
|
||||||
// Fetch all trips
|
|
||||||
$tripsSql = "SELECT trip_id, trip_name FROM trips";
|
|
||||||
$tripsResult = $conn->query($tripsSql);
|
|
||||||
|
|
||||||
?>
|
|
||||||
<style>
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: separate;
|
|
||||||
border-spacing: 0;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th {
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
padding: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th::after {
|
|
||||||
content: '\25B2';
|
|
||||||
/* Up arrow */
|
|
||||||
font-size: 0.8em;
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th.asc::after {
|
|
||||||
content: '\25B2';
|
|
||||||
/* Up arrow */
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th.desc::after {
|
|
||||||
content: '\25BC';
|
|
||||||
/* Down arrow */
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(odd) {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(even) {
|
|
||||||
background-color: rgb(255, 255, 255);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody td {
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(even) td:first-child {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:nth-child(even) td:last-child {
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
border-bottom-right-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px;
|
|
||||||
/* margin-bottom: 20px; */
|
|
||||||
font-size: 16px;
|
|
||||||
background-color: rgb(255, 255, 255);
|
|
||||||
border-radius: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trip-booking {
|
|
||||||
color: #484848;
|
|
||||||
background: #f9f9f7;
|
|
||||||
border: 1px solid #d8d8d8;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const tables = document.querySelectorAll("table");
|
|
||||||
tables.forEach((table) => {
|
|
||||||
const headers = table.querySelectorAll("thead th");
|
|
||||||
const rows = Array.from(table.querySelectorAll("tbody tr"));
|
|
||||||
const filterInput = table.previousElementSibling;
|
|
||||||
|
|
||||||
headers.forEach((header, index) => {
|
|
||||||
header.addEventListener("click", () => {
|
|
||||||
const sortedRows = rows.sort((a, b) => {
|
|
||||||
const aText = a.cells[index].textContent.trim().toLowerCase();
|
|
||||||
const bText = b.cells[index].textContent.trim().toLowerCase();
|
|
||||||
|
|
||||||
if (aText < bText) return -1;
|
|
||||||
if (aText > bText) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (header.classList.contains("asc")) {
|
|
||||||
header.classList.remove("asc");
|
|
||||||
header.classList.add("desc");
|
|
||||||
sortedRows.reverse();
|
|
||||||
} else {
|
|
||||||
headers.forEach(h => h.classList.remove("asc", "desc"));
|
|
||||||
header.classList.add("asc");
|
|
||||||
}
|
|
||||||
|
|
||||||
const tbody = table.querySelector("tbody");
|
|
||||||
tbody.innerHTML = "";
|
|
||||||
sortedRows.forEach(row => tbody.appendChild(row));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (rows.length === 0) {
|
|
||||||
filterInput.style.display = "none";
|
|
||||||
} else {
|
|
||||||
filterInput.addEventListener("input", function() {
|
|
||||||
const filterValue = filterInput.value.trim().toLowerCase();
|
|
||||||
rows.forEach(row => {
|
|
||||||
const rowText = row.textContent.trim().toLowerCase();
|
|
||||||
row.style.display = rowText.includes(filterValue) ? "" : "none";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<?php
|
|
||||||
$bannerFolder = 'assets/images/banners/';
|
|
||||||
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
|
||||||
|
|
||||||
$randomBanner = 'assets/images/base4/camping.jpg'; // default fallback
|
|
||||||
if (!empty($bannerImages)) {
|
|
||||||
$randomBanner = $bannerImages[array_rand($bannerImages)];
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<section class="page-banner-area pt-50 pb-35 rel z-1 bgs-cover" style="background-image: url('<?php echo $randomBanner; ?>');">
|
|
||||||
<div class="banner-overlay"></div>
|
|
||||||
<div class="container">
|
|
||||||
<div class="banner-inner text-white mb-50">
|
|
||||||
<h2 class="page-title mb-10" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">4WDCSA Trip Bookings</h2>
|
|
||||||
<nav aria-label="breadcrumb">
|
|
||||||
<ol class="breadcrumb justify-content-center mb-20" data-aos="fade-right" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
|
||||||
<li class="breadcrumb-item"><a href="index.php">Home</a></li>
|
|
||||||
<li class="breadcrumb-item active">Trip Bookings</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="tour-list-page py-10 rel z-1">
|
|
||||||
<div class="container">
|
|
||||||
<?php
|
|
||||||
if ($tripsResult->num_rows > 0) {
|
|
||||||
while ($trip = $tripsResult->fetch_assoc()) {
|
|
||||||
$tripId = $trip['trip_id'];
|
|
||||||
$tripName = htmlspecialchars($trip['trip_name']);
|
|
||||||
|
|
||||||
echo "<div class='trip-booking' data-aos='fade-up' data-aos-duration='1500' data-aos-offset='50'>";
|
|
||||||
echo "<div style='padding:10px;'>";
|
|
||||||
echo "<h4>{$tripName}</h4>";
|
|
||||||
|
|
||||||
// Fetch bookings for the current trip
|
|
||||||
$bookingsSql = "SELECT b.user_id, b.num_vehicles, b.num_adults, b.num_children, b.num_pensioners, b.radio, b.status,
|
|
||||||
u.first_name, u.last_name, u.profile_pic,
|
|
||||||
(b.total_amount - b.discount_amount) AS paid
|
|
||||||
FROM bookings b
|
|
||||||
INNER JOIN users u ON b.user_id = u.user_id
|
|
||||||
WHERE b.trip_id = ?";
|
|
||||||
$stmt = $conn->prepare($bookingsSql);
|
|
||||||
$stmt->bind_param('i', $tripId);
|
|
||||||
$stmt->execute();
|
|
||||||
$bookingsResult = $stmt->get_result();
|
|
||||||
|
|
||||||
|
|
||||||
if ($bookingsResult->num_rows > 0) {
|
|
||||||
echo '<input type="text" class="filter-input" placeholder="Filter results...">';
|
|
||||||
echo '<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Vehicles</th>
|
|
||||||
<th>Adults</th>
|
|
||||||
<th>Children</th>
|
|
||||||
<th>Pensioners</th>
|
|
||||||
<th>Radio</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>';
|
|
||||||
while ($booking = $bookingsResult->fetch_assoc()) {
|
|
||||||
$userName = htmlspecialchars($booking['first_name'] . ' ' . $booking['last_name']);
|
|
||||||
$numVehicles = htmlspecialchars($booking['num_vehicles']);
|
|
||||||
$numAdults = htmlspecialchars($booking['num_adults']);
|
|
||||||
$numPensioners = htmlspecialchars($booking['num_pensioners']);
|
|
||||||
$numChildren = htmlspecialchars($booking['num_children']);
|
|
||||||
$radio = $booking['radio'] == 1 ? "YES" : "NO";
|
|
||||||
$status = htmlspecialchars($booking['status']);
|
|
||||||
$paid = "R " . number_format($booking['paid'], 2);
|
|
||||||
|
|
||||||
echo "<tr>
|
|
||||||
<td><img src=".$booking['profile_pic']." alt='Profile Picture' class='profile-pic'></td>
|
|
||||||
<td>{$userName}</td>
|
|
||||||
<td>{$numVehicles}</td>
|
|
||||||
<td>{$numAdults}</td>
|
|
||||||
<td>{$numChildren}</td>
|
|
||||||
<td>{$numPensioners}</td>
|
|
||||||
<td>{$radio}</td>
|
|
||||||
<td>{$status}</td>
|
|
||||||
<td>{$paid}</td>
|
|
||||||
</tr>";
|
|
||||||
}
|
|
||||||
echo '</tbody></table>';
|
|
||||||
} else {
|
|
||||||
echo '<p>No bookings found for this trip.</p>';
|
|
||||||
}
|
|
||||||
echo "</div>";
|
|
||||||
echo "</div>";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
echo '<p>No trips found.</p>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
<?php
|
|
||||||
$rootPath = dirname(dirname(__DIR__));
|
|
||||||
require_once($rootPath . "/src/config/env.php");
|
|
||||||
require_once($rootPath . "/src/config/connection.php");
|
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
|
||||||
require_once($rootPath . "/header.php");
|
|
||||||
checkAdmin();
|
|
||||||
checkUserSession();
|
|
||||||
|
|
||||||
$pageTitle = 'Manage Trips';
|
|
||||||
$breadcrumbs = [['Home' => 'index']];
|
|
||||||
require_once($rootPath . '/components/banner.php');
|
|
||||||
|
|
||||||
// Fetch all trips with booking status
|
|
||||||
$trips_query = "
|
|
||||||
SELECT
|
|
||||||
trip_id, trip_name, location, start_date, end_date,
|
|
||||||
vehicle_capacity, places_booked, cost_members, cost_nonmembers,
|
|
||||||
cost_pensioner_member, cost_pensioner, published
|
|
||||||
FROM trips
|
|
||||||
ORDER BY start_date DESC
|
|
||||||
";
|
|
||||||
|
|
||||||
$result = $conn->query($trips_query);
|
|
||||||
$trips = [];
|
|
||||||
if ($result && $result->num_rows > 0) {
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$trips[] = $row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.image {
|
|
||||||
width: 200px;
|
|
||||||
/* Set your desired width */
|
|
||||||
height: 200px;
|
|
||||||
/* Set your desired height */
|
|
||||||
overflow: hidden;
|
|
||||||
/* Hide any overflow */
|
|
||||||
display: block;
|
|
||||||
/* Ensure proper block behavior */
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image img {
|
|
||||||
width: 100%;
|
|
||||||
/* Image scales to fill the container */
|
|
||||||
height: 100%;
|
|
||||||
/* Image scales to fill the container */
|
|
||||||
object-fit: cover;
|
|
||||||
/* Fills the container while maintaining aspect ratio */
|
|
||||||
object-position: center;
|
|
||||||
/* Aligns the center of the image with the center of the container */
|
|
||||||
display: block;
|
|
||||||
/* Prevents inline whitespace issues */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const filterInput = document.querySelector('.filter-input');
|
|
||||||
const cards = document.querySelectorAll('.destination-item');
|
|
||||||
|
|
||||||
if (cards.length === 0 && filterInput) {
|
|
||||||
filterInput.style.display = "none";
|
|
||||||
} else if (filterInput) {
|
|
||||||
filterInput.addEventListener("input", function() {
|
|
||||||
const filterValue = filterInput.value.trim().toLowerCase();
|
|
||||||
cards.forEach(card => {
|
|
||||||
const cardText = card.textContent.trim().toLowerCase();
|
|
||||||
card.style.display = cardText.includes(filterValue) ? "" : "none";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$bannerFolder = 'assets/images/banners/';
|
|
||||||
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
|
||||||
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- Trips Management Area start -->
|
|
||||||
<section class="blog-list-page py-100 rel z-1">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
|
||||||
<h2 style="margin: 0;">Manage Trips</h2>
|
|
||||||
<a href="manage_trips" class="theme-btn create-album-btn">
|
|
||||||
<i class="far fa-plus"></i> New Event
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<?php if (isset($_SESSION['message'])): ?>
|
|
||||||
<div class="alert alert-warning message-box">
|
|
||||||
<?php echo $_SESSION['message']; ?>
|
|
||||||
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
|
||||||
</div>
|
|
||||||
<?php unset($_SESSION['message']);
|
|
||||||
endif;
|
|
||||||
if (count($trips) > 0) {
|
|
||||||
echo '<input type="text" class="filter-input" placeholder="Filter trips...">';
|
|
||||||
echo '<div class="trips-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
|
|
||||||
|
|
||||||
foreach ($trips as $trip) {
|
|
||||||
$available = $trip['vehicle_capacity'] - $trip['places_booked'];
|
|
||||||
$publishStatus = $trip['published'] == 1 ? 'published' : 'draft';
|
|
||||||
$publishStatusBadge = $trip['published'] == 1 ? 'PUBLISHED' : 'DRAFT';
|
|
||||||
|
|
||||||
// Get trip image - look for assets/images/trips/$trip_id_{number}.jpg
|
|
||||||
$tripImagePath = '';
|
|
||||||
$tripImagesGlob = glob($rootPath . '/assets/images/trips/' . $trip['trip_id'] . '_*.jpg');
|
|
||||||
if (!empty($tripImagesGlob)) {
|
|
||||||
$tripImagePath = str_replace($rootPath, '', $tripImagesGlob[0]);
|
|
||||||
} else {
|
|
||||||
// Fallback to placeholder icon if no image found
|
|
||||||
$tripImagePath = 'assets/images/placeholder.jpg';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '
|
|
||||||
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
|
||||||
<div class="image" style="width:300px;height:250px;">
|
|
||||||
<img src="' . htmlspecialchars($tripImagePath) . '" alt="' . htmlspecialchars($trip['trip_name']) . '">
|
|
||||||
</div>
|
|
||||||
<div class="content" style="width:100%;">
|
|
||||||
<div class="destination-header d-flex align-items-start gap-3">
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-dark mb-1">' . strtoupper($publishStatusBadge) . '</span>
|
|
||||||
<h5 class="mb-0">' . htmlspecialchars($trip['trip_name']) . '</h5>
|
|
||||||
<small class="text-muted">📍 ' . htmlspecialchars($trip['location']) . '</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p style="margin: 10px 0;">
|
|
||||||
<strong>Dates:</strong> ' . date('M d', strtotime($trip['start_date'])) . ' - ' . date('M d, Y', strtotime($trip['end_date'])) . '<br>
|
|
||||||
<strong>Capacity:</strong> ' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '<br>
|
|
||||||
<strong>Costs:</strong> Members: R ' . number_format($trip['cost_members'], 2) . ' | Non-Members: R ' . number_format($trip['cost_nonmembers'], 2) . ' | Pensioner Members: R ' . number_format($trip['cost_pensioner_member'], 2) . ' | Pensioners: R ' . number_format($trip['cost_pensioner'], 2) . '
|
|
||||||
</p>
|
|
||||||
<div class="destination-footer">
|
|
||||||
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
|
||||||
<a href="manage_trips?trip_id=' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
|
||||||
<button type="button" class="toggle-publish" data-trip-id="' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($trip['published'] == 1 ? 'Unpublish' : 'Publish') . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($trip['published'] == 1 ? 'cloud_off' : 'cloud_upload') . '</span></button>
|
|
||||||
<button type="button" class="delete-trip" data-trip-id="' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '</div>';
|
|
||||||
} else {
|
|
||||||
echo '<div class="no-trips">
|
|
||||||
<p>No trips found. <a href="manage_trips">Create one</a></p>
|
|
||||||
</div>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<!-- Trips Management Area end -->
|
|
||||||
|
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
|
||||||
<script>
|
|
||||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
||||||
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
|
|
||||||
|
|
||||||
// Handle publish/unpublish button clicks
|
|
||||||
document.querySelectorAll('.toggle-publish').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
const tripId = this.dataset.tripId;
|
|
||||||
const currentIcon = this.querySelector('.material-icons').textContent;
|
|
||||||
const isPublished = currentIcon === 'cloud_off';
|
|
||||||
const action = isPublished ? 'Unpublish' : 'Publish';
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('trip_id', tripId);
|
|
||||||
|
|
||||||
fetch('toggle_trip_published', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'success') {
|
|
||||||
alert(action + ' successful!');
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
alert(action + ' failed: ' + data.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Error:', err);
|
|
||||||
alert(action + ' failed due to network error.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle delete button clicks
|
|
||||||
document.querySelectorAll('.delete-trip').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
if (!confirm('Are you sure you want to delete this trip? This action cannot be undone.')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tripId = this.dataset.tripId;
|
|
||||||
const card = this.closest('.destination-item');
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('trip_id', tripId);
|
|
||||||
|
|
||||||
fetch('delete_trip', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'success') {
|
|
||||||
alert('Trip deleted successfully!');
|
|
||||||
card.style.animation = 'fadeOut 0.3s ease-out';
|
|
||||||
setTimeout(() => {
|
|
||||||
card.remove();
|
|
||||||
if (document.querySelectorAll('.destination-item').length === 0) {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
alert('Error: ' + data.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Error:', err);
|
|
||||||
alert('Delete failed due to network error.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
|
||||||
441
src/admin/admin_trips_events_courses.php
Normal file
441
src/admin/admin_trips_events_courses.php
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
<?php
|
||||||
|
$headerStyle = 'light';
|
||||||
|
$rootPath = dirname(dirname(__DIR__));
|
||||||
|
include_once($rootPath . '/header.php');
|
||||||
|
checkAdmin();
|
||||||
|
|
||||||
|
// Ensure $conn is available; show a friendly message if DB is down
|
||||||
|
$conn = $conn ?? null;
|
||||||
|
if (!$conn) {
|
||||||
|
// Render a simple error notice inside the page layout and stop further DB queries
|
||||||
|
echo '<section class="tour-list-page py-10 rel z-1">\n <div class="container">\n <div class="alert alert-danger">Database connection unavailable. Please check your configuration and logs.</div>\n </div>\n</section>';
|
||||||
|
include_once($rootPath . '/components/insta_footer.php');
|
||||||
|
// Stop execution to prevent subsequent fatal errors from using $conn
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
/* Shared table and tab styles */
|
||||||
|
.admin-tabs { text-align: center; display: flex; margin-bottom: 20px; margin-top: 20px; }
|
||||||
|
.admin-tab { padding: 10px 30px; margin-right: 20px; cursor: pointer; border: 2px solid; border-radius: 50px; background: none; font-size: 18px; color: #484848; }
|
||||||
|
.admin-tab.active { color: white; background: #63ab45; font-weight: bold; }
|
||||||
|
.tab-content { display: none; }
|
||||||
|
.tab-content.active { display: block; }
|
||||||
|
table { width: 100%; border-collapse: separate; border-spacing: 0; margin: 10px 0; }
|
||||||
|
thead th { cursor: pointer; text-align: left; padding: 10px; font-weight: bold; position: relative; }
|
||||||
|
thead th::after { content: '\25B2'; font-size: 0.8em; position: absolute; right: 10px; opacity: 0; transition: opacity 0.2s; }
|
||||||
|
thead th.asc::after { content: '\25B2'; opacity: 1; }
|
||||||
|
thead th.desc::after { content: '\25BC'; opacity: 1; }
|
||||||
|
tbody tr:nth-child(odd) { background-color: transparent; }
|
||||||
|
tbody tr:nth-child(even) { background-color: rgb(255, 255, 255); border-radius: 10px; }
|
||||||
|
tbody td { padding: 5px; }
|
||||||
|
tbody tr:nth-child(even) td:first-child { border-top-left-radius: 10px; border-bottom-left-radius: 10px; }
|
||||||
|
tbody tr:nth-child(even) td:last-child { border-top-right-radius: 10px; border-bottom-right-radius: 10px; }
|
||||||
|
.filter-input { width: 100%; padding: 5px; font-size: 16px; background-color: rgb(255, 255, 255); border-radius: 25px; }
|
||||||
|
.trip-booking { color: #484848; background: #f9f9f7; border: 1px solid #d8d8d8; border-radius: 10px; margin-top: 15px; margin-bottom: 15px; }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
// Tab switching
|
||||||
|
const tabs = document.querySelectorAll('.admin-tab');
|
||||||
|
const contents = document.querySelectorAll('.tab-content');
|
||||||
|
tabs.forEach((tab, idx) => {
|
||||||
|
tab.addEventListener('click', function() {
|
||||||
|
tabs.forEach(t => t.classList.remove('active'));
|
||||||
|
contents.forEach(c => c.classList.remove('active'));
|
||||||
|
tab.classList.add('active');
|
||||||
|
contents[idx].classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Table sorting/filtering (same as before)
|
||||||
|
document.querySelectorAll("table").forEach((table) => {
|
||||||
|
const headers = table.querySelectorAll("thead th");
|
||||||
|
const rows = Array.from(table.querySelectorAll("tbody tr"));
|
||||||
|
const filterInput = table.previousElementSibling;
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
header.addEventListener("click", () => {
|
||||||
|
const sortedRows = rows.sort((a, b) => {
|
||||||
|
const aText = a.cells[index].textContent.trim().toLowerCase();
|
||||||
|
const bText = b.cells[index].textContent.trim().toLowerCase();
|
||||||
|
if (aText < bText) return -1;
|
||||||
|
if (aText > bText) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
if (header.classList.contains("asc")) {
|
||||||
|
header.classList.remove("asc");
|
||||||
|
header.classList.add("desc");
|
||||||
|
sortedRows.reverse();
|
||||||
|
} else {
|
||||||
|
headers.forEach(h => h.classList.remove("asc", "desc"));
|
||||||
|
header.classList.add("asc");
|
||||||
|
}
|
||||||
|
const tbody = table.querySelector("tbody");
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
sortedRows.forEach(row => tbody.appendChild(row));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (rows.length === 0) {
|
||||||
|
filterInput.style.display = "none";
|
||||||
|
} else {
|
||||||
|
filterInput.addEventListener("input", function() {
|
||||||
|
const filterValue = filterInput.value.trim().toLowerCase();
|
||||||
|
rows.forEach(row => {
|
||||||
|
const rowText = row.textContent.trim().toLowerCase();
|
||||||
|
row.style.display = rowText.includes(filterValue) ? "" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
$pageTitle = 'Manage Trips, Courses & Events';
|
||||||
|
$breadcrumbs = [['Home' => 'index']];
|
||||||
|
require_once($rootPath . '/components/banner.php');
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<section class="tour-list-page py-10 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="admin-tabs">
|
||||||
|
<button class="admin-tab active">Manage Trips</button>
|
||||||
|
<button class="admin-tab">Manage Events</button>
|
||||||
|
<button class="admin-tab">Manage Courses</button>
|
||||||
|
<button class="admin-tab">Manage Blogs</button>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content active">
|
||||||
|
<?php
|
||||||
|
// Trips management content (adapted from admin_trips.php)
|
||||||
|
// Fetch trips (already available if connection present)
|
||||||
|
$trips_query = "
|
||||||
|
SELECT
|
||||||
|
trip_id, trip_name, location, start_date, end_date,
|
||||||
|
vehicle_capacity, places_booked, cost_members, cost_nonmembers,
|
||||||
|
cost_pensioner_member, cost_pensioner, published
|
||||||
|
FROM trips
|
||||||
|
ORDER BY start_date DESC
|
||||||
|
";
|
||||||
|
|
||||||
|
$result = $conn->query($trips_query);
|
||||||
|
$trips = [];
|
||||||
|
if ($result && $result->num_rows > 0) {
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$trips[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
||||||
|
<h2 style="margin: 0;">Manage Trips</h2>
|
||||||
|
<a href="manage_trips" class="theme-btn create-album-btn">
|
||||||
|
<i class="far fa-plus"></i> New Trip
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php if (isset($_SESSION['message'])): ?>
|
||||||
|
<div class="alert alert-warning message-box">
|
||||||
|
<?php echo $_SESSION['message']; ?>
|
||||||
|
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||||
|
</div>
|
||||||
|
<?php unset($_SESSION['message']);
|
||||||
|
endif;
|
||||||
|
if (count($trips) > 0) {
|
||||||
|
echo '<input type="text" class="filter-input" placeholder="Filter trips...">';
|
||||||
|
echo '<div class="trips-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
|
||||||
|
foreach ($trips as $trip) {
|
||||||
|
$available = $trip['vehicle_capacity'] - $trip['places_booked'];
|
||||||
|
$publishStatusBadge = $trip['published'] == 1 ? 'PUBLISHED' : 'DRAFT';
|
||||||
|
$tripImagePath = '';
|
||||||
|
$tripImagesGlob = glob($rootPath . '/assets/images/trips/' . $trip['trip_id'] . '_*.jpg');
|
||||||
|
if (!empty($tripImagesGlob)) {
|
||||||
|
$tripImagePath = str_replace($rootPath, '', $tripImagesGlob[0]);
|
||||||
|
} else {
|
||||||
|
$tripImagePath = 'assets/images/placeholder.jpg';
|
||||||
|
}
|
||||||
|
echo '
|
||||||
|
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image" style="width:300px;height:250px;">
|
||||||
|
<img src="' . htmlspecialchars($tripImagePath) . '" alt="' . htmlspecialchars($trip['trip_name']) . '">
|
||||||
|
</div>
|
||||||
|
<div class="content" style="width:100%;">
|
||||||
|
<div class="destination-header d-flex align-items-start gap-3">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-dark mb-1">' . strtoupper($publishStatusBadge) . '</span>
|
||||||
|
<h5 class="mb-0">' . htmlspecialchars($trip['trip_name']) . '</h5>
|
||||||
|
<small class="text-muted">📍 ' . htmlspecialchars($trip['location']) . '</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="margin: 10px 0;">
|
||||||
|
<strong>Dates:</strong> ' . date('M d', strtotime($trip['start_date'])) . ' - ' . date('M d, Y', strtotime($trip['end_date'])) . '<br>
|
||||||
|
<strong>Capacity:</strong> ' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '<br>
|
||||||
|
<strong>Costs:</strong> Members: R ' . number_format($trip['cost_members'], 2) . ' | Non-Members: R ' . number_format($trip['cost_nonmembers'], 2) . ' | Pensioner Members: R ' . number_format($trip['cost_pensioner_member'], 2) . ' | Pensioners: R ' . number_format($trip['cost_pensioner'], 2) . '
|
||||||
|
</p>
|
||||||
|
<div class="destination-footer">
|
||||||
|
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
||||||
|
<a href="manage_trips?trip_id=' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
||||||
|
<button type="button" class="toggle-publish" data-trip-id="' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($trip['published'] == 1 ? 'Unpublish' : 'Publish') . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($trip['published'] == 1 ? 'cloud_off' : 'cloud_upload') . '</span></button>
|
||||||
|
<button type="button" class="delete-trip" data-trip-id="' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
} else {
|
||||||
|
echo "<div class=\"no-trips\"><p>No trips found. <a href=\"manage_trips\">Create one</a></p></div>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<?php
|
||||||
|
// Events management content (adapted from admin_events.php)
|
||||||
|
$events_query = "
|
||||||
|
SELECT
|
||||||
|
event_id, name, type, location, date, image, published
|
||||||
|
FROM events
|
||||||
|
ORDER BY date DESC
|
||||||
|
";
|
||||||
|
$result = $conn->query($events_query);
|
||||||
|
$events = [];
|
||||||
|
if ($result && $result->num_rows > 0) {
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$events[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
||||||
|
<h2 style="margin: 0;">Manage Events</h2>
|
||||||
|
<a href="manage_events" class="theme-btn create-album-btn">
|
||||||
|
<i class="far fa-plus"></i> New Event
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php if (count($events) > 0) {
|
||||||
|
echo '<input type="text" class="filter-input" placeholder="Filter events...">';
|
||||||
|
echo '<div class="events-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
|
||||||
|
foreach ($events as $event) {
|
||||||
|
$eventImagePath = $event['image'] ? htmlspecialchars($event['image']) : 'assets/images/placeholder.jpg';
|
||||||
|
$publishStatusBadge = $event['published'] == 1 ? 'PUBLISHED' : 'DRAFT';
|
||||||
|
echo '
|
||||||
|
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image" style="width:300px;height:250px;">
|
||||||
|
<img src="' . $eventImagePath . '" alt="' . htmlspecialchars($event['name']) . '">
|
||||||
|
</div>
|
||||||
|
<div class="content" style="width:100%;">
|
||||||
|
<div class="destination-header d-flex align-items-start gap-3">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-dark mb-1">' . strtoupper($publishStatusBadge) . '</span>
|
||||||
|
<h5 class="mb-0">' . htmlspecialchars($event['name']) . '</h5>
|
||||||
|
<small class="text-muted">📍 ' . htmlspecialchars($event['location']) . '</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="margin: 10px 0;">
|
||||||
|
<strong>Type:</strong> ' . htmlspecialchars($event['type']) . '<br>
|
||||||
|
<strong>Date:</strong> ' . convertDate($event['date']) . '
|
||||||
|
</p>
|
||||||
|
<div class="destination-footer">
|
||||||
|
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
||||||
|
<a href="manage_events?event_id=' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
||||||
|
<button type="button" class="toggle-publish" data-event-id="' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($event['published'] == 1 ? 'Unpublish' : 'Publish') . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($event['published'] == 1 ? 'cloud_off' : 'cloud_upload') . '</span></button>
|
||||||
|
<button type="button" class="delete-event" data-event-id="' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
} else {
|
||||||
|
echo '<div class="no-events"><p>No events found. <a href="manage_events">Create one</a></p></div>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<?php
|
||||||
|
// Courses management content (adapted from admin_courses.php)
|
||||||
|
$courses_query = "
|
||||||
|
SELECT
|
||||||
|
course_id, course_type, date, capacity, booked, cost_members, cost_nonmembers, instructor, instructor_email, code
|
||||||
|
FROM courses
|
||||||
|
ORDER BY date DESC
|
||||||
|
";
|
||||||
|
$result = $conn->query($courses_query);
|
||||||
|
$courses = [];
|
||||||
|
if ($result && $result->num_rows > 0) {
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$courses[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
||||||
|
<h2 style="margin: 0;">Manage Courses</h2>
|
||||||
|
<a href="manage_courses" class="theme-btn create-album-btn">
|
||||||
|
<i class="far fa-plus"></i> New Course
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php if (isset($_SESSION['message'])): ?>
|
||||||
|
<div class="alert alert-warning message-box">
|
||||||
|
<?php echo $_SESSION['message']; ?>
|
||||||
|
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||||
|
</div>
|
||||||
|
<?php unset($_SESSION['message']);
|
||||||
|
endif; ?>
|
||||||
|
|
||||||
|
<?php if (count($courses) > 0): ?>
|
||||||
|
<input type="text" class="filter-input" placeholder="Filter courses...">
|
||||||
|
<div class="courses-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<?php foreach ($courses as $course):
|
||||||
|
$available = intval($course['capacity']) - intval($course['booked']);
|
||||||
|
$type_label = strtoupper($course['course_type']);
|
||||||
|
if ($course['course_type'] == 'driver_training') {
|
||||||
|
$type_label = 'Driver Training';
|
||||||
|
} elseif ($course['course_type'] == 'bush_mechanics') {
|
||||||
|
$type_label = 'Bush Mechanics';
|
||||||
|
} elseif ($course['course_type'] == 'rescue_recovery') {
|
||||||
|
$type_label = 'Rescue & Recovery';
|
||||||
|
} elseif ($course['course_type'] == 'ladies_driver_training') {
|
||||||
|
$type_label = 'Ladies Driver Training';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="content" style="width:100%;">
|
||||||
|
<div class="destination-header d-flex align-items-start gap-3">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0"><?php echo $type_label; ?></h5>
|
||||||
|
<small class="text-muted"><?php echo htmlspecialchars($course['course_type']); ?> — <?php echo date('M d, Y', strtotime($course['date'])); ?></small><br>
|
||||||
|
<small class="text-muted"><?php echo $course['code'] ? 'Code: ' . htmlspecialchars($course['code']) : ''; ?></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="margin: 10px 0;">
|
||||||
|
<strong>Instructor:</strong> <?php echo htmlspecialchars($course['instructor']); ?> (<?php echo htmlspecialchars($course['instructor_email']); ?>)<br>
|
||||||
|
<strong>Capacity:</strong> <?php echo intval($course['booked']); ?> / <?php echo intval($course['capacity']); ?> <strong>Available:</strong> <?php echo $available; ?><br>
|
||||||
|
<strong>Costs:</strong> Members: R <?php echo number_format($course['cost_members'],2); ?> | Non-Members: R <?php echo number_format($course['cost_nonmembers'],2); ?>
|
||||||
|
</p>
|
||||||
|
<div class="destination-footer">
|
||||||
|
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
||||||
|
<a href="manage_courses?course_id=<?php echo $course['course_id']; ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
||||||
|
<button type="button" class="delete-course" data-course-id="<?php echo $course['course_id']; ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="no-courses">
|
||||||
|
<p>No courses found. <a href="manage_courses">Create one</a></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<?php
|
||||||
|
// Blogs management content (adapted from src/admin/admin_blogs.php)
|
||||||
|
$posts = [];
|
||||||
|
if ($conn) {
|
||||||
|
$stmt = $conn->prepare("SELECT b.blog_id, b.title, b.description, b.status, b.date, b.image, CONCAT(u.first_name, ' ', u.last_name) AS author_name, u.email AS author_email, u.profile_pic FROM blogs b JOIN users u ON b.author = u.user_id WHERE b.status = 'published' ORDER BY b.date DESC");
|
||||||
|
if ($stmt) {
|
||||||
|
$stmt->execute();
|
||||||
|
$res = $stmt->get_result();
|
||||||
|
if ($res && $res->num_rows > 0) {
|
||||||
|
while ($r = $res->fetch_assoc()) $posts[] = $r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:30px;">
|
||||||
|
<h2 style="margin:0;">Manage Blogs</h2>
|
||||||
|
<a href="blog_create" class="theme-btn create-album-btn"><i class="far fa-plus"></i> New Blog</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (count($posts) > 0): ?>
|
||||||
|
<input type="text" class="filter-input" placeholder="Filter blogs...">
|
||||||
|
<div class="blogs-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<?php foreach ($posts as $post):
|
||||||
|
$coverImage = $post['image'] ? $post['image'] : 'assets/images/placeholder.jpg';
|
||||||
|
?>
|
||||||
|
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="image" style="width:200px;height:200px;">
|
||||||
|
<img src="<?php echo htmlspecialchars($coverImage); ?>" alt="<?php echo htmlspecialchars($post['title']); ?>">
|
||||||
|
</div>
|
||||||
|
<div class="content" style="width:100%;">
|
||||||
|
<div class="destination-header d-flex align-items-start gap-3">
|
||||||
|
<img src="<?php echo htmlspecialchars($post['profile_pic'] ?? 'assets/images/placeholder.jpg'); ?>" alt="Author" class="rounded-circle border" width="80" height="80">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-dark mb-1"><?php echo $post['status'] == 1 || $post['status'] === 'published' ? 'PUBLISHED' : 'DRAFT'; ?></span>
|
||||||
|
<h5 class="mb-0"><?php echo htmlspecialchars($post['title']); ?></h5>
|
||||||
|
<small class="text-muted"><?php echo htmlspecialchars($post['author_name']); ?></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p><?php echo htmlspecialchars($post['description']); ?></p>
|
||||||
|
<div class="destination-footer">
|
||||||
|
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
||||||
|
<a href="blog_edit.php?token=<?php echo encryptData($post['blog_id'], $salt); ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
||||||
|
<a href="blog_read.php?token=<?php echo encryptData($post['blog_id'], $salt); ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Preview"><span class="material-icons">visibility</span></a>
|
||||||
|
<button type="button" class="publish-btn" data-blog-id="<?php echo $post['blog_id']; ?>" data-status="<?php echo htmlspecialchars($post['status']); ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Publish/Unpublish" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons"><?php echo ($post['status'] == 1 || $post['status'] === 'published') ? 'cloud_off' : 'cloud_upload'; ?></span></button>
|
||||||
|
<a href="blog_delete.php?token=<?php echo encryptData($post['blog_id'], $salt); ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete"><span class="material-icons">delete</span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="no-blogs"><p>No blogs found. <a href="manage_blogs">Create one</a></p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
const filterInput = document.querySelector('.filter-input');
|
||||||
|
const cards = document.querySelectorAll('.destination-item');
|
||||||
|
if (cards.length === 0 && filterInput) filterInput.style.display = "none";
|
||||||
|
else if (filterInput) {
|
||||||
|
filterInput.addEventListener('input', function() {
|
||||||
|
const filterValue = filterInput.value.trim().toLowerCase();
|
||||||
|
cards.forEach(card => {
|
||||||
|
const cardText = card.textContent.trim().toLowerCase();
|
||||||
|
card.style.display = cardText.includes(filterValue) ? "" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tooltips
|
||||||
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||||
|
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
|
||||||
|
|
||||||
|
// Publish/unpublish
|
||||||
|
document.querySelectorAll('.publish-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const blogId = this.dataset.blogId;
|
||||||
|
const status = this.dataset.status;
|
||||||
|
const endpoint = (status == 1 || status === 'published') ? 'blog_unpublish' : 'publish_blog';
|
||||||
|
const formData = new FormData(); formData.append('id', blogId);
|
||||||
|
fetch(endpoint, { method: 'POST', body: formData })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => { if (data.status === 'success') location.reload(); else alert('Action failed'); })
|
||||||
|
.catch(()=> alert('Network error'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||||
@@ -4,6 +4,7 @@ $rootPath = dirname(dirname(__DIR__));
|
|||||||
require_once($rootPath . "/src/config/env.php");
|
require_once($rootPath . "/src/config/env.php");
|
||||||
require_once($rootPath . "/src/config/connection.php");
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
require_once($rootPath . "/src/helpers/notification_helper.php");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ==========================================================
|
* ==========================================================
|
||||||
@@ -272,6 +273,14 @@ if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) {
|
|||||||
nl2br($message)
|
nl2br($message)
|
||||||
);
|
);
|
||||||
sendAdminNotification($subject, nl2br($message));
|
sendAdminNotification($subject, nl2br($message));
|
||||||
|
$event = 'new_payment_received';
|
||||||
|
$sub_feed = 'payments';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'] ?? null,
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'] ?? null, // used by UI to show avatar
|
||||||
|
'title' => "New Payment Received for Payment ID: {$localPaymentId}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
41
src/api/notifications.php
Normal file
41
src/api/notifications.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
// Ensure environment is loaded before attempting DB connection
|
||||||
|
require_once __DIR__ . '/../config/env.php';
|
||||||
|
require_once __DIR__ . '/../config/connection.php';
|
||||||
|
// helper filename uses singular in this repo: notification_helper.php
|
||||||
|
require_once __DIR__ . '/../helpers/notification_helper.php';
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$admin_id = $_SESSION['user_id'] ?? null;
|
||||||
|
$action = $_REQUEST['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'fetch') {
|
||||||
|
$subs = getAdminSubscriptions($admin_id);
|
||||||
|
$notes = fetchNotifications($admin_id, $subs, 50);
|
||||||
|
echo json_encode(['success' => true, 'notifications' => $notes, 'unread_count' => getUnreadCount($admin_id, $subs)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'mark_read') {
|
||||||
|
if (!$admin_id) { echo json_encode(['success' => false, 'error' => 'unauthenticated']); exit; }
|
||||||
|
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||||
|
if (!$id) { echo json_encode(['success' => false, 'error' => 'missing_id']); exit; }
|
||||||
|
$ok = markNotificationRead($id, $admin_id);
|
||||||
|
echo json_encode(['success' => (bool)$ok]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'add') {
|
||||||
|
// internal use: create a notification
|
||||||
|
$target = isset($_POST['user_id']) ? intval($_POST['user_id']) : null;
|
||||||
|
$event = $_POST['event'] ?? '';
|
||||||
|
$sub_feed = $_POST['sub_feed'] ?? null;
|
||||||
|
$data = isset($_POST['data']) ? json_decode($_POST['data'], true) : [];
|
||||||
|
$target_url = $_POST['target_url'] ?? null;
|
||||||
|
$id = addNotification($target, $event, $sub_feed, $data, $target_url);
|
||||||
|
echo json_encode(['success' => (bool)$id, 'id' => $id]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => false, 'error' => 'invalid_action']);
|
||||||
@@ -3,11 +3,12 @@
|
|||||||
// Disable mysqli exceptions so we can handle connection errors gracefully
|
// Disable mysqli exceptions so we can handle connection errors gracefully
|
||||||
mysqli_report(MYSQLI_REPORT_OFF);
|
mysqli_report(MYSQLI_REPORT_OFF);
|
||||||
|
|
||||||
$dbhost = $_ENV['DB_HOST'];
|
// Read from environment or fallback to getenv; keep empty string if not set to avoid PHP warnings
|
||||||
$dbuser = $_ENV['DB_USER'];
|
$dbhost = $_ENV['DB_HOST'] ?? getenv('DB_HOST') ?? '';
|
||||||
$dbpass = $_ENV['DB_PASS'];
|
$dbuser = $_ENV['DB_USER'] ?? getenv('DB_USER') ?? '';
|
||||||
$dbname = $_ENV['DB_NAME'];
|
$dbpass = $_ENV['DB_PASS'] ?? getenv('DB_PASS') ?? '';
|
||||||
$salt = $_ENV['SALT'];
|
$dbname = $_ENV['DB_NAME'] ?? getenv('DB_NAME') ?? '';
|
||||||
|
$salt = $_ENV['SALT'] ?? getenv('SALT') ?? '';
|
||||||
|
|
||||||
// echo "hello. ". $dbhost;
|
// echo "hello. ". $dbhost;
|
||||||
|
|
||||||
|
|||||||
151
src/helpers/notification_helper.php
Normal file
151
src/helpers/notification_helper.php
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
// Notifications helper
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config/env.php';
|
||||||
|
require_once __DIR__ . '/../config/connection.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../classes/DatabaseService.php';
|
||||||
|
|
||||||
|
function addNotification($target_user_id, $event, $sub_feed = null, $data = [], $target_url = null) {
|
||||||
|
global $db, $conn;
|
||||||
|
if (!isset($db) && isset($conn)) {
|
||||||
|
$db = new DatabaseService($conn);
|
||||||
|
}
|
||||||
|
$ds = $db;
|
||||||
|
if (!$ds) {
|
||||||
|
// Try to initialize DatabaseService if connection exists
|
||||||
|
if (isset($conn) && $conn) {
|
||||||
|
$ds = new DatabaseService($conn);
|
||||||
|
} else {
|
||||||
|
// No DB available
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$data_json = $data ? json_encode($data) : null;
|
||||||
|
$read_by_json = json_encode([]);
|
||||||
|
$query = "INSERT INTO notifications (user_id, event, sub_feed, data, target_url, read_by) VALUES (?,?,?,?,?,?)";
|
||||||
|
return $ds->insert($query, [$target_user_id, $event, $sub_feed, $data_json, $target_url, $read_by_json], "isssss");
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchNotifications($admin_user_id = null, $subscriptions = [], $limit = 50) {
|
||||||
|
global $db, $conn;
|
||||||
|
if (!isset($db) && isset($conn)) {
|
||||||
|
$db = new DatabaseService($conn);
|
||||||
|
}
|
||||||
|
$ds = $db;
|
||||||
|
if (!$ds) {
|
||||||
|
if (isset($conn) && $conn) {
|
||||||
|
$ds = new DatabaseService($conn);
|
||||||
|
} else {
|
||||||
|
// No DB available — return empty list to avoid fatal error
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$params = [];
|
||||||
|
$types = "";
|
||||||
|
$sql = "SELECT * FROM notifications";
|
||||||
|
$where = [];
|
||||||
|
// Admin-only: fetch notifications targeted to admins (user_id IS NULL) or global, or specifically to an admin
|
||||||
|
if ($admin_user_id) {
|
||||||
|
$where[] = "(user_id IS NULL OR user_id = ?)";
|
||||||
|
$params[] = $admin_user_id;
|
||||||
|
$types .= "i";
|
||||||
|
}
|
||||||
|
if (!empty($subscriptions)) {
|
||||||
|
// build IN (...) list safely by placeholders
|
||||||
|
$placeholders = implode(',', array_fill(0, count($subscriptions), '?'));
|
||||||
|
$where[] = "(sub_feed IN ($placeholders))";
|
||||||
|
foreach ($subscriptions as $s) { $params[] = $s; $types .= "s"; }
|
||||||
|
}
|
||||||
|
if (!empty($where)) {
|
||||||
|
$sql .= " WHERE " . implode(' AND ', $where);
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY time_created DESC LIMIT ?";
|
||||||
|
$params[] = $limit; $types .= "i";
|
||||||
|
|
||||||
|
$results = $ds->select($sql, $params, $types);
|
||||||
|
if ($results === false) {
|
||||||
|
// Query error - return empty list so UI doesn't break
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// decode data JSON and include read_by as array
|
||||||
|
$filtered = [];
|
||||||
|
foreach ($results as $r) {
|
||||||
|
$r['data'] = $r['data'] ? json_decode($r['data'], true) : null;
|
||||||
|
$r['read_by'] = $r['read_by'] ? json_decode($r['read_by'], true) : [];
|
||||||
|
if (!is_array($r['read_by'])) $r['read_by'] = [];
|
||||||
|
// If admin_user_id is provided, skip notifications this admin already read
|
||||||
|
if ($admin_user_id && in_array((int)$admin_user_id, $r['read_by'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$filtered[] = $r;
|
||||||
|
}
|
||||||
|
return $filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markNotificationRead($id, $admin_user_id) {
|
||||||
|
global $conn;
|
||||||
|
if (!$id || !$admin_user_id) return false;
|
||||||
|
if (!isset($conn) || !$conn) return false;
|
||||||
|
$stmt = $conn->prepare("SELECT read_by FROM notifications WHERE id = ? LIMIT 1");
|
||||||
|
$stmt->bind_param("i", $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->bind_result($read_by_json);
|
||||||
|
$found = $stmt->fetch();
|
||||||
|
$stmt->close();
|
||||||
|
if (!$found) return false;
|
||||||
|
$read_by = $read_by_json ? json_decode($read_by_json, true) : [];
|
||||||
|
if (!is_array($read_by)) $read_by = [];
|
||||||
|
if (!in_array($admin_user_id, $read_by)) {
|
||||||
|
$read_by[] = (int)$admin_user_id;
|
||||||
|
$new_json = json_encode(array_values($read_by));
|
||||||
|
$u = $conn->prepare("UPDATE notifications SET read_by = ? WHERE id = ?");
|
||||||
|
$u->bind_param("si", $new_json, $id);
|
||||||
|
$res = $u->execute();
|
||||||
|
$u->close();
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUnreadCount($admin_user_id, $subscriptions = []) {
|
||||||
|
global $conn;
|
||||||
|
if (!isset($conn) || !$conn) return 0;
|
||||||
|
$sql = "SELECT id, read_by, sub_feed FROM notifications";
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
$types = "";
|
||||||
|
if ($admin_user_id) {
|
||||||
|
$where[] = "(user_id IS NULL OR user_id = ?)"; $params[] = $admin_user_id; $types .= "i";
|
||||||
|
}
|
||||||
|
if (!empty($subscriptions)) {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($subscriptions), '?'));
|
||||||
|
$where[] = "(sub_feed IN ($placeholders))";
|
||||||
|
foreach ($subscriptions as $s) { $params[] = $s; $types .= "s"; }
|
||||||
|
}
|
||||||
|
if (!empty($where)) $sql .= " WHERE " . implode(' AND ', $where);
|
||||||
|
$sql .= " ORDER BY time_created DESC";
|
||||||
|
$stmt = $conn->prepare($sql);
|
||||||
|
if ($types) {
|
||||||
|
// bind params dynamically
|
||||||
|
$refs = [];
|
||||||
|
$refs[] = &$types;
|
||||||
|
foreach ($params as $k => $v) { $refs[] = &$params[$k]; }
|
||||||
|
call_user_func_array([$stmt, 'bind_param'], $refs);
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
|
$res = $stmt->get_result();
|
||||||
|
$count = 0;
|
||||||
|
while ($row = $res->fetch_assoc()) {
|
||||||
|
$read_by = $row['read_by'] ? json_decode($row['read_by'], true) : [];
|
||||||
|
if (!is_array($read_by)) $read_by = [];
|
||||||
|
if (!in_array((int)$admin_user_id, $read_by)) $count++;
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdminSubscriptions($admin_user_id) {
|
||||||
|
// Placeholder: by default return empty array (all sub_feeds). Implement subscription table later.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,165 +0,0 @@
|
|||||||
<?php
|
|
||||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
|
||||||
require_once($rootPath . "/src/config/env.php");
|
|
||||||
require_once($rootPath . "/src/config/connection.php");
|
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
|
||||||
require_once($rootPath . "/header.php");
|
|
||||||
checkAdmin();
|
|
||||||
checkUserSession();
|
|
||||||
|
|
||||||
$pageTitle = 'Manage Blog Posts';
|
|
||||||
$breadcrumbs = [['Home' => 'index']];
|
|
||||||
require_once($rootPath . '/components/banner.php');
|
|
||||||
|
|
||||||
$result = $conn->prepare("
|
|
||||||
SELECT
|
|
||||||
b.blog_id,
|
|
||||||
b.title,
|
|
||||||
b.description,
|
|
||||||
b.status,
|
|
||||||
b.date,
|
|
||||||
b.image,
|
|
||||||
CONCAT(u.first_name, ' ', u.last_name) AS author_name,
|
|
||||||
u.email AS author_email,
|
|
||||||
u.profile_pic
|
|
||||||
FROM blogs b
|
|
||||||
JOIN users u ON b.author = u.user_id
|
|
||||||
WHERE b.status = 'published'
|
|
||||||
ORDER BY b.date DESC
|
|
||||||
");
|
|
||||||
|
|
||||||
$result->execute();
|
|
||||||
$posts = $result->get_result();
|
|
||||||
|
|
||||||
|
|
||||||
?>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.image {
|
|
||||||
width: 400px;
|
|
||||||
/* Set your desired width */
|
|
||||||
height: 350px;
|
|
||||||
/* Set your desired height */
|
|
||||||
overflow: hidden;
|
|
||||||
/* Hide any overflow */
|
|
||||||
display: block;
|
|
||||||
/* Ensure proper block behavior */
|
|
||||||
}
|
|
||||||
|
|
||||||
.image img {
|
|
||||||
width: 100%;
|
|
||||||
/* Image scales to fill the container */
|
|
||||||
height: 100%;
|
|
||||||
/* Image scales to fill the container */
|
|
||||||
object-fit: cover;
|
|
||||||
/* Fills the container while maintaining aspect ratio */
|
|
||||||
object-position: top;
|
|
||||||
/* Aligns the top of the image with the top of the container */
|
|
||||||
display: block;
|
|
||||||
/* Prevents inline whitespace issues */
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$bannerFolder = 'assets/images/banners/';
|
|
||||||
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
|
||||||
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- Blog List Area start -->
|
|
||||||
<section class="blog-list-page py-100 rel z-1">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
|
|
||||||
<h2>Manage Blog Posts</h2>
|
|
||||||
<?php if (isset($_SESSION['message'])): ?>
|
|
||||||
<div class="alert alert-warning message-box">
|
|
||||||
<?php echo $_SESSION['message']; ?>
|
|
||||||
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
|
||||||
</div>
|
|
||||||
<?php unset($_SESSION['message']); ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
<a href="blog_create.php">+ New Post</a>
|
|
||||||
|
|
||||||
<?php while ($post = $posts->fetch_assoc()):
|
|
||||||
// Determine cover image - use provided image or fallback placeholder
|
|
||||||
$coverImage = $post["image"] ? $post["image"] : 'assets/images/placeholder.jpg';
|
|
||||||
// Output the HTML structure with dynamic data
|
|
||||||
echo '
|
|
||||||
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
|
||||||
<div class="image" style="width:200px;height:200px;">
|
|
||||||
<img src="' . htmlspecialchars($coverImage) . '" alt="' . htmlspecialchars($post["title"]) . '">
|
|
||||||
</div>
|
|
||||||
<div class="content" style="width:100%;">
|
|
||||||
<div class="destination-header d-flex align-items-start gap-3">
|
|
||||||
<img src="' . $post["profile_pic"] . '" alt="Author" class="rounded-circle border" width="80" height="80">
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-dark mb-1">' . strtoupper($post["status"]) . '</span>
|
|
||||||
<h5 class="mb-0">' . $post["title"] . '</h5>
|
|
||||||
<small class="text-muted">' . $post["author_name"] . '</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>' . $post["description"] . '</p>
|
|
||||||
<div class="destination-footer">
|
|
||||||
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
|
||||||
<a href="blog_edit.php?token=' . encryptData($post["blog_id"], $salt) . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
|
||||||
<a href="blog_read.php?token=' . encryptData($post["blog_id"], $salt) . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Preview"><span class="material-icons">visibility</span></a>
|
|
||||||
<button type="button" class="publish-btn" data-blog-id="' . $post["blog_id"] . '" data-status="' . $post["status"] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($post["status"] == "published" ? "Unpublish" : "Publish") . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($post["status"] == "published" ? "cloud_off" : "cloud_upload") . '</span></button>
|
|
||||||
<a href="blog_delete.php?token=' . encryptData($post["blog_id"], $salt) . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete"><span class="material-icons">delete</span></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
';
|
|
||||||
endwhile; ?>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<!-- Blog List Area end -->
|
|
||||||
<script>
|
|
||||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
||||||
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
|
|
||||||
|
|
||||||
// Handle publish/unpublish button clicks
|
|
||||||
document.querySelectorAll('.publish-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
const blogId = this.dataset.blogId;
|
|
||||||
const status = this.dataset.status;
|
|
||||||
const action = status === 'published' ? 'unpublish' : 'publish';
|
|
||||||
const endpoint = status === 'published' ? 'blog_unpublish' : 'publish_blog';
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('id', blogId);
|
|
||||||
|
|
||||||
fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
alert(action.charAt(0).toUpperCase() + action.slice(1) + ' successful!');
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
alert(action + ' failed.');
|
|
||||||
console.error('Error:', response.statusText);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Error:', err);
|
|
||||||
alert(action + ' failed due to network error.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
|
||||||
@@ -6,11 +6,14 @@ include_once($rootPath . '/header.php');
|
|||||||
$is_logged_in = isset($_SESSION['user_id']);
|
$is_logged_in = isset($_SESSION['user_id']);
|
||||||
if (isset($_SESSION['user_id'])) {
|
if (isset($_SESSION['user_id'])) {
|
||||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
header('Location: login.php');
|
header('Location: login.php');
|
||||||
exit(); // Stop further script execution
|
exit(); // Stop further script execution
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$full_name = getFullName($user_id);
|
||||||
|
|
||||||
//if membership_fees payment_status is PENDING RENEWAL, redirect to membership_details.php
|
//if membership_fees payment_status is PENDING RENEWAL, redirect to membership_details.php
|
||||||
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
|
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
|
||||||
$stmt->bind_param("i", $user_id);
|
$stmt->bind_param("i", $user_id);
|
||||||
@@ -46,6 +49,16 @@ if ($stmt->execute()) {
|
|||||||
auditLog($user_id, 'MEMBERSHIP_RENEWAL_INITIATED', 'membership_fees', null, ['payment_id' => $payment_id, 'amount' => $payment_amount]);
|
auditLog($user_id, 'MEMBERSHIP_RENEWAL_INITIATED', 'membership_fees', null, ['payment_id' => $payment_id, 'amount' => $payment_amount]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send Notification
|
||||||
|
$event = 'membership_renewal_initiated';
|
||||||
|
$sub_feed = 'membership_renewal';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'],
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'], // used by UI to show avatar
|
||||||
|
'title' => "Membership Renewal Initiated by {$full_name}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
|
|
||||||
$checkP = $conn->prepare("SELECT COUNT(*) AS cnt FROM payments WHERE payment_id = ? LIMIT 1");
|
$checkP = $conn->prepare("SELECT COUNT(*) AS cnt FROM payments WHERE payment_id = ? LIMIT 1");
|
||||||
if ($checkP) {
|
if ($checkP) {
|
||||||
$checkP->bind_param('s', $payment_id);
|
$checkP->bind_param('s', $payment_id);
|
||||||
|
|||||||
@@ -224,6 +224,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
// Audit: membership application submitted
|
// Audit: membership application submitted
|
||||||
if (function_exists('auditLog')) {
|
if (function_exists('auditLog')) {
|
||||||
auditLog($user_id, 'MEMBERSHIP_APPLICATION_SUBMITTED', 'membership_application', null, ['payment_id' => $payment_id, 'amount' => $payment_amount ?? null]);
|
auditLog($user_id, 'MEMBERSHIP_APPLICATION_SUBMITTED', 'membership_application', null, ['payment_id' => $payment_id, 'amount' => $payment_amount ?? null]);
|
||||||
|
$event = 'new_application_submitted';
|
||||||
|
$sub_feed = 'membership_applications';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'],
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'], // used by UI to show avatar
|
||||||
|
'title' => "New Membership Application from {$first_name} {$last_name}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
}
|
}
|
||||||
header("Location: indemnity");
|
header("Location: indemnity");
|
||||||
// Success message
|
// Success message
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ $rootPath = dirname(dirname(__DIR__));
|
|||||||
require_once($rootPath . "/src/config/env.php");
|
require_once($rootPath . "/src/config/env.php");
|
||||||
require_once($rootPath . "/src/config/connection.php");
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
require_once($rootPath . "/src/helpers/notification_helper.php");
|
||||||
|
|
||||||
// Start session to retrieve the logged-in user's ID
|
// Start session to retrieve the logged-in user's ID
|
||||||
session_start();
|
session_start();
|
||||||
@@ -84,6 +85,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (function_exists('auditLog')) {
|
if (function_exists('auditLog')) {
|
||||||
auditLog($user_id, 'BOOKING_CREATED', 'bookings', $booking_id, ['total_amount' => $total_amount, 'from' => $from_date, 'to' => $to_date]);
|
auditLog($user_id, 'BOOKING_CREATED', 'bookings', $booking_id, ['total_amount' => $total_amount, 'from' => $from_date, 'to' => $to_date]);
|
||||||
}
|
}
|
||||||
|
$event = 'new_booking_created';
|
||||||
|
$sub_feed = 'bookings';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'] ?? null,
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'] ?? null, // used by UI to show avatar
|
||||||
|
'title' => "New Booking Created with Booking ID: {$booking_id}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
// Redirect to success page or display success message
|
// Redirect to success page or display success message
|
||||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'booking.php';</script>";
|
echo "<script>alert('Booking successfully created!'); window.location.href = 'booking.php';</script>";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ $rootPath = dirname(dirname(__DIR__));
|
|||||||
require_once($rootPath . "/src/config/env.php");
|
require_once($rootPath . "/src/config/env.php");
|
||||||
require_once($rootPath . "/src/config/connection.php");
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
require_once($rootPath . "/src/helpers/notification_helper.php");
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
|
|
||||||
@@ -122,6 +123,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (function_exists('auditLog')) {
|
if (function_exists('auditLog')) {
|
||||||
auditLog($user_id, 'COURSE_BOOKING_CREATED', 'bookings', $booking_id, ['course_id' => $course_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
|
auditLog($user_id, 'COURSE_BOOKING_CREATED', 'bookings', $booking_id, ['course_id' => $course_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
|
||||||
}
|
}
|
||||||
|
$event = 'new_course_booking_created';
|
||||||
|
$sub_feed = 'bookings';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'] ?? null,
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'] ?? null, // used by UI to show avatar
|
||||||
|
'title' => "New Course Booking Created : {$payment_id}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
|
|
||||||
if ($payment_amount < 1) {
|
if ($payment_amount < 1) {
|
||||||
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||||
@@ -143,7 +152,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
||||||
|
|
||||||
// Send invoice and admin notification (keep for records)
|
// Send invoice and admin notification (keep for records)
|
||||||
// sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
|
||||||
sendAdminNotification('New Course Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
sendAdminNotification('New Course Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||||
|
|
||||||
// Redirect user to payment link if available
|
// Redirect user to payment link if available
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ $rootPath = dirname(dirname(__DIR__));
|
|||||||
require_once($rootPath . "/src/config/env.php");
|
require_once($rootPath . "/src/config/env.php");
|
||||||
require_once($rootPath . "/src/config/connection.php");
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
require_once($rootPath . "/src/helpers/notification_helper.php");
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
// Get the trip_id from the request (ensure it's sanitized)
|
// Get the trip_id from the request (ensure it's sanitized)
|
||||||
@@ -131,6 +132,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
auditLog($user_id, 'TRIP_BOOKING_CREATED', 'bookings', $booking_id, ['trip_id' => $trip_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
|
auditLog($user_id, 'TRIP_BOOKING_CREATED', 'bookings', $booking_id, ['trip_id' => $trip_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create notification for new booking
|
||||||
|
$event = 'new_trip_booking_created';
|
||||||
|
$sub_feed = 'bookings';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'] ?? null,
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'] ?? null, // used by UI to show avatar
|
||||||
|
'title' => "New Trip Booking Created: {$payment_id}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
|
|
||||||
if ($payment_amount < 1) {
|
if ($payment_amount < 1) {
|
||||||
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||||
@@ -151,7 +162,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
||||||
|
|
||||||
// Send invoice and admin notification
|
// Send invoice and admin notification
|
||||||
// sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
|
||||||
sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||||
|
|
||||||
// Redirect to payment link if available
|
// Redirect to payment link if available
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ require_once($rootPath . "/src/config/session.php");
|
|||||||
require_once($rootPath . "/src/config/connection.php");
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
require_once($rootPath . '/google-client/vendor/autoload.php'); // Add this line for Google Client
|
require_once($rootPath . '/google-client/vendor/autoload.php'); // Add this line for Google Client
|
||||||
|
require_once($rootPath . "/src/helpers/notification_helper.php");
|
||||||
|
|
||||||
|
|
||||||
// Check if connection is established
|
// Check if connection is established
|
||||||
if (!$conn) {
|
if (!$conn) {
|
||||||
@@ -37,6 +39,8 @@ if (isset($_GET['code'])) {
|
|||||||
$last_name = $google_account_info->family_name;
|
$last_name = $google_account_info->family_name;
|
||||||
$picture = $google_account_info->picture;
|
$picture = $google_account_info->picture;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Check if the user exists in the database
|
// Check if the user exists in the database
|
||||||
$query = "SELECT * FROM users WHERE email = ?";
|
$query = "SELECT * FROM users WHERE email = ?";
|
||||||
$stmt = $conn->prepare($query);
|
$stmt = $conn->prepare($query);
|
||||||
@@ -53,12 +57,20 @@ if (isset($_GET['code'])) {
|
|||||||
$stmt->bind_param("sssssi", $email, $first_name, $last_name, $picture, $password, $is_verified);
|
$stmt->bind_param("sssssi", $email, $first_name, $last_name, $picture, $password, $is_verified);
|
||||||
if ($stmt->execute()) {
|
if ($stmt->execute()) {
|
||||||
// User successfully registered, set session and redirect
|
// User successfully registered, set session and redirect
|
||||||
sendEmail('chrispintoza@gmail.com', '4WDCSA: New User Login', $name.' has just created an account using Google Login.');
|
|
||||||
$_SESSION['user_id'] = $conn->insert_id;
|
$_SESSION['user_id'] = $conn->insert_id;
|
||||||
$_SESSION['first_name'] = $first_name;
|
$_SESSION['first_name'] = $first_name;
|
||||||
$_SESSION['profile_pic'] = $picture;
|
$_SESSION['profile_pic'] = $picture;
|
||||||
processLegacyMembership($_SESSION['user_id']);
|
|
||||||
// echo json_encode(['status' => 'success', 'message' => 'Google login successful']);
|
// Send Notification
|
||||||
|
$event = 'user_login';
|
||||||
|
$sub_feed = 'logins';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $conn->insert_id,
|
||||||
|
'actor_avatar' => $picture, // used by UI to show avatar
|
||||||
|
'title' => "User Login by {$first_name} {$last_name}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
|
|
||||||
header("Location: index.php");
|
header("Location: index.php");
|
||||||
exit();
|
exit();
|
||||||
} else {
|
} else {
|
||||||
@@ -72,8 +84,17 @@ if (isset($_GET['code'])) {
|
|||||||
$_SESSION['user_id'] = $row['user_id'];
|
$_SESSION['user_id'] = $row['user_id'];
|
||||||
$_SESSION['first_name'] = $row['first_name'];
|
$_SESSION['first_name'] = $row['first_name'];
|
||||||
$_SESSION['profile_pic'] = $row['profile_pic'];
|
$_SESSION['profile_pic'] = $row['profile_pic'];
|
||||||
sendEmail('chrispintoza@gmail.com', '4WDCSA: New User Login', $name.' has just logged in using Google Login.');
|
|
||||||
// echo json_encode(['status' => 'success', 'message' => 'Google login successful']);
|
// Send Notification
|
||||||
|
$event = 'user_login';
|
||||||
|
$sub_feed = 'logins';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'],
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'], // used by UI to show avatar
|
||||||
|
'title' => "User Login by {$first_name} {$last_name}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
|
|
||||||
header("Location: index.php");
|
header("Location: index.php");
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
@@ -182,6 +203,16 @@ if (isset($_POST['email']) && isset($_POST['password'])) {
|
|||||||
$_SESSION['login_time'] = time();
|
$_SESSION['login_time'] = time();
|
||||||
$_SESSION['session_timeout'] = 1800; // 30 minutes in seconds
|
$_SESSION['session_timeout'] = 1800; // 30 minutes in seconds
|
||||||
|
|
||||||
|
// Send Notification
|
||||||
|
$event = 'user_login';
|
||||||
|
$sub_feed = 'logins';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'],
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'], // used by UI to show avatar
|
||||||
|
'title' => "User Login by {$first_name} {$last_name}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
|
|
||||||
auditLog($row['user_id'], 'LOGIN_SUCCESS', 'users', $row['user_id']);
|
auditLog($row['user_id'], 'LOGIN_SUCCESS', 'users', $row['user_id']);
|
||||||
echo json_encode(['status' => 'success', 'message' => 'Successful Login']);
|
echo json_encode(['status' => 'success', 'message' => 'Successful Login']);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user