Compare commits
26 Commits
feature/bl
...
9653443c09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9653443c09 | ||
|
|
782d343243 | ||
|
|
c618fd4506 | ||
|
|
d5feaacddf | ||
|
|
927f9f3fe1 | ||
|
|
1b47cb0a69 | ||
|
|
7ebc2f64cf | ||
|
|
ebd7efe21c | ||
|
|
6ff20c1ffc | ||
|
|
35c177b11d | ||
|
|
acd7f563b1 | ||
|
|
5768d8a7af | ||
|
|
0e6ecd127f | ||
|
|
702e04e9bf | ||
|
|
d2c99e86b4 | ||
|
|
f4934e9c13 | ||
|
|
477c2f2e04 | ||
|
|
a66382661d | ||
|
|
32e50ffc39 | ||
|
|
cce181e2d0 | ||
|
|
48ee7592b2 | ||
|
|
abb8eb23e5 | ||
|
|
2acbeac7ca | ||
|
|
5808788b9e | ||
|
|
bbc0aecbcb | ||
|
|
752ea6e5e9 |
25
.htaccess
@@ -30,6 +30,7 @@ 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 ^renewal_payment$ src/pages/memberships/renewal_payment.php [L]
|
||||
RewriteRule ^renew_membership$ src/pages/memberships/renew_membership.php [L]
|
||||
RewriteRule ^member_info$ src/pages/memberships/member_info.php [L]
|
||||
|
||||
@@ -68,8 +69,10 @@ RewriteRule ^instapage$ src/pages/events/instapage.php [L]
|
||||
|
||||
# === OTHER PAGES ===
|
||||
RewriteRule ^about$ src/pages/other/about.php [L]
|
||||
RewriteRule ^base4$ src/pages/other/base4.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]
|
||||
@@ -79,7 +82,14 @@ 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]
|
||||
|
||||
# === PAYMENT RETURN PAGES ===
|
||||
RewriteRule ^success$ src/pages/payment/success.php [L]
|
||||
RewriteRule ^failure$ src/pages/payment/failure.php [L]
|
||||
RewriteRule ^cancel$ src/pages/payment/cancel.php [L]
|
||||
|
||||
# === 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_payments$ src/admin/admin_payments.php [L]
|
||||
RewriteRule ^admin_web_users$ src/admin/admin_web_users.php [L]
|
||||
@@ -88,10 +98,13 @@ 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_transactions$ src/admin/admin_transactions.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]
|
||||
RewriteRule ^admin_courses$ /src/admin/admin_courses.php [L,QSA]
|
||||
RewriteRule ^manage_courses$ /src/admin/manage_courses.php [L,QSA]
|
||||
|
||||
|
||||
# === API/AJAX ENDPOINTS ===
|
||||
RewriteRule ^fetch_users$ src/api/fetch_users.php [L]
|
||||
@@ -102,6 +115,8 @@ RewriteRule ^get_tab_total$ src/api/get_tab_total.php [L]
|
||||
RewriteRule ^google_validate_login$ src/api/google_validate_login.php [L]
|
||||
|
||||
# === PROCESSORS ===
|
||||
RewriteRule ^process_course$ /src/processors/process_course.php [L,QSA]
|
||||
RewriteRule ^delete_course$ /src/processors/delete_course.php [L,QSA]
|
||||
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]
|
||||
@@ -122,11 +137,11 @@ RewriteRule ^upload_profile_picture$ src/processors/upload_profile_picture.php [
|
||||
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/admin/process_event.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/admin/toggle_event_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/admin/delete_event.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]
|
||||
@@ -136,7 +151,7 @@ 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 ^admin_blogs$ src/admin/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]
|
||||
|
||||
4
.user.ini
Normal file
@@ -0,0 +1,4 @@
|
||||
; memory_limit = 512M
|
||||
upload_max_filesize = 64M
|
||||
post_max_size = 64M
|
||||
max_execution_time = 120
|
||||
@@ -1,3 +0,0 @@
|
||||
<?php
|
||||
// Redirector file - loads the actual page from src/pages/other/
|
||||
require_once __DIR__ . '/src/pages/other/about.php';
|
||||
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;
|
||||
}
|
||||
@@ -1,12 +1,6 @@
|
||||
@charset "UTF-8";
|
||||
/*----------------------------------------------------------------------
|
||||
Template Name: Ravelo - Travel & Tour Booking HTML Template
|
||||
Template URI: https://webtend.net/demo/html/ravelo/
|
||||
Author: WebTend
|
||||
Author URI: https://webtend.net/
|
||||
Version: 1.0
|
||||
|
||||
Note: This is Main Style CSS File. */
|
||||
4WDCSA.co.za CSS Stylesheet
|
||||
/*----------------------------------------------------------------------
|
||||
CSS INDEX
|
||||
----------------------
|
||||
@@ -7124,7 +7118,8 @@ blockquote {
|
||||
/* Comments */
|
||||
.comments {
|
||||
border-radius: 10px;
|
||||
/* border: 1px solid var(--border-color); } */
|
||||
/* border: 1px solid var(--border-color); */
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
padding: 50px; }
|
||||
|
||||
BIN
assets/images/base4/01.jpeg
Normal file
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 494 KiB |
BIN
assets/images/logos/ikhokha.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
assets/images/obstacles/01_03.png
Normal file
|
After Width: | Height: | Size: 6.6 MiB |
BIN
assets/images/pp/6318a13edd2e79cf13ff60a74ebcb858.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
assets/images/pp/857004ff86e047673beaafba95a1ebc6.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
assets/images/pp/c07abeef5590d7e141080a53581bb5cb.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
assets/images/track-aerial.jpg
Normal file
|
After Width: | Height: | Size: 4.2 MiB |
115
assets/images/track-route.svg
Normal file
|
After Width: | Height: | Size: 24 KiB |
13126
assets/images/track-route2.svg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
66
assets/js/map.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* TRACK MAP WITH LEAFLET.JS
|
||||
*
|
||||
* Basic Leaflet map test
|
||||
*/
|
||||
|
||||
console.log('Track map script loaded2');
|
||||
|
||||
// Check if Leaflet is available
|
||||
if (typeof L === 'undefined') {
|
||||
console.error('Leaflet library not loaded!');
|
||||
} else {
|
||||
console.log('Leaflet library is available, version:', L.version);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('DOM loaded, initializing map...');
|
||||
|
||||
const mapElement = document.getElementById('map');
|
||||
console.log('Map element:', mapElement);
|
||||
|
||||
if (!mapElement) {
|
||||
console.error('Map element not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Map element dimensions:', mapElement.offsetWidth, 'x', mapElement.offsetHeight);
|
||||
|
||||
try {
|
||||
// Image dimensions: 2876 x 2035 pixels
|
||||
const imageWidth = 2876;
|
||||
const imageHeight = 2035;
|
||||
|
||||
// Create map with simple CRS (pixel coordinates)
|
||||
// Note: Leaflet uses [y, x] format, so bounds are [[0, 0], [height, width]]
|
||||
const bounds = [[0, 0], [imageHeight, imageWidth]];
|
||||
const map = L.map('map', {
|
||||
crs: L.CRS.Simple,
|
||||
minZoom: -2,
|
||||
maxZoom: 2,
|
||||
center: [imageHeight / 2, imageWidth / 2],
|
||||
zoom: -1
|
||||
});
|
||||
console.log('Map object created with CRS.Simple:', map);
|
||||
|
||||
// Add aerial image overlay
|
||||
const imageUrl = '/assets/images/track-aerial.jpg';
|
||||
L.imageOverlay(imageUrl, bounds).addTo(map);
|
||||
console.log('Aerial image overlay added');
|
||||
|
||||
// Add SVG overlay
|
||||
const svgUrl = '/assets/images/track-route.svg';
|
||||
L.imageOverlay(svgUrl, bounds, {
|
||||
opacity: 0.8,
|
||||
interactive: false
|
||||
}).addTo(map);
|
||||
console.log('SVG route overlay added');
|
||||
|
||||
// Fit map to image bounds
|
||||
map.fitBounds(bounds);
|
||||
|
||||
console.log('Map initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Error initializing map:', error);
|
||||
}
|
||||
});
|
||||
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);
|
||||
120
classes/iKhokhaClient.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
class IkhokhaClient {
|
||||
private string $appId;
|
||||
private string $appSecret;
|
||||
private string $apiUrl;
|
||||
|
||||
public function __construct() {
|
||||
// Try getenv first, then fallback to $_ENV if available
|
||||
$this->appId = getenv('IKHOKHA_APP_ID') ?: ($_ENV['IKHOKHA_APP_ID'] ?? '');
|
||||
$this->appSecret = getenv('IKHOKHA_APP_SECRET') ?: ($_ENV['IKHOKHA_APP_SECRET'] ?? '');
|
||||
$this->apiUrl = getenv('IKHOKHA_API_URL') ?: ($_ENV['IKHOKHA_API_URL'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request to the iKhokha API. Signs the payload per API docs.
|
||||
* $endpoint should be the path portion starting with '/public-api/...'
|
||||
*/
|
||||
private function request(string $endpoint, array $data, string $method = 'POST') {
|
||||
// Validate apiUrl
|
||||
if (empty($this->apiUrl)) {
|
||||
return ['error' => true, 'errno' => 3, 'message' => 'IKHOKHA_API_URL is not configured in environment'];
|
||||
}
|
||||
|
||||
// If the configured API URL already contains the endpoint path, use it as-is.
|
||||
if ((function_exists('str_ends_with') && str_ends_with($this->apiUrl, $endpoint)) ||
|
||||
(substr_compare($this->apiUrl, $endpoint, -strlen($endpoint)) === 0)) {
|
||||
$url = $this->apiUrl;
|
||||
} else {
|
||||
$url = rtrim($this->apiUrl, '/') . $endpoint;
|
||||
}
|
||||
$body = json_encode($data);
|
||||
|
||||
// Build payload to sign: path + body and apply escape rules per iKhokha docs
|
||||
$parsed = parse_url($url);
|
||||
$path = $parsed['path'] ?? $endpoint;
|
||||
$payloadToSign = $path . $body;
|
||||
|
||||
// Escape function from iKhokha example
|
||||
$escapeString = function ($str) {
|
||||
$escaped = preg_replace(['/[\\\"\'\"]/u', '/\x00/'], ['\\\\$0', '\\0'], (string)$str);
|
||||
$cleaned = str_replace('\/', '/', $escaped);
|
||||
return $cleaned;
|
||||
};
|
||||
|
||||
$escapedPayload = $escapeString($payloadToSign);
|
||||
$signature = hash_hmac('sha256', $escapedPayload, $this->appSecret);
|
||||
|
||||
$ch = curl_init($url);
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
"IK-APPID: {$this->appId}",
|
||||
"IK-SIGN: {$signature}"
|
||||
];
|
||||
|
||||
// Optional debug logging to logs/ikhokha.log when IKHOKHA_DEBUG_LOG is true
|
||||
$debugLog = getenv('IKHOKHA_DEBUG_LOG') ?: ($_ENV['IKHOKHA_DEBUG_LOG'] ?? null);
|
||||
if ($debugLog) {
|
||||
$logPath = dirname(__DIR__) . '/logs/ikhokha.log';
|
||||
$logEntry = [
|
||||
'time' => date('c'),
|
||||
'url' => $url,
|
||||
'headers' => $headers,
|
||||
'body' => $data,
|
||||
'signature' => $signature
|
||||
];
|
||||
@file_put_contents($logPath, json_encode(['request' => $logEntry]) . PHP_EOL, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
if (strtoupper($method) === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
// Log response if debug enabled
|
||||
if (!empty($debugLog)) {
|
||||
$logPath = dirname(__DIR__) . '/logs/ikhokha.log';
|
||||
$respEntry = [
|
||||
'time' => date('c'),
|
||||
'http_code' => $httpCode,
|
||||
'errno' => $errno,
|
||||
'error' => $error,
|
||||
'response' => $response
|
||||
];
|
||||
@file_put_contents($logPath, json_encode(['response' => $respEntry]) . PHP_EOL, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
if ($response === false) {
|
||||
return ['error' => true, 'message' => $error, 'errno' => $errno];
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a payment link using the iKhokha create payment endpoint.
|
||||
* $body must match iKhokha request schema (amount in smallest unit, urls, externalTransactionID, etc.)
|
||||
*/
|
||||
public function createPaymentLink(array $body) {
|
||||
return $this->request('/public-api/v1/api/payment', $body, 'POST');
|
||||
}
|
||||
|
||||
public function getPaymentStatus($paymentId) {
|
||||
// Use the GET status endpoint
|
||||
$endpoint = '/public-api/v1/api/getStatus/' . urlencode($paymentId);
|
||||
return $this->request($endpoint, [], 'GET');
|
||||
}
|
||||
}
|
||||
25
docs/migrations/005_create_track_obstacles_table.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- ============================================================================
|
||||
-- MIGRATION: Create Track Obstacles Table
|
||||
-- Date: 2025-12-12
|
||||
-- Description: Create table to store 4x4 track obstacles with positioning and details
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS track_obstacles (
|
||||
obstacle_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL COMMENT 'Obstacle name (e.g., "Rock Crawl", "Water Crossing")',
|
||||
x_position INT NOT NULL COMMENT 'X pixel position on the track map',
|
||||
y_position INT NOT NULL COMMENT 'Y pixel position on the track map',
|
||||
difficulty VARCHAR(20) NOT NULL COMMENT 'Difficulty level (easy, medium, hard, extreme)',
|
||||
description TEXT COMMENT 'Detailed description of the obstacle',
|
||||
image_path VARCHAR(255) COMMENT 'Path to obstacle image (e.g., assets/images/obstacles/obstacle1.jpg)',
|
||||
marker_color VARCHAR(20) NOT NULL COMMENT 'Marker color: red, green, black, or split (red-green)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_name (name),
|
||||
INDEX idx_difficulty (difficulty)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ============================================================================
|
||||
-- ROLLBACK INSTRUCTIONS (if needed)
|
||||
-- ============================================================================
|
||||
-- DROP TABLE IF EXISTS track_obstacles;
|
||||
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;
|
||||
69
header.php
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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/connection.php");
|
||||
require_once($rootDir . "/src/config/functions.php");
|
||||
require_once($rootDir . "/src/helpers/notification_helper.php");
|
||||
|
||||
$is_logged_in = isset($_SESSION['user_id']);
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
@@ -186,8 +188,7 @@ if ($headerStyle === 'light') {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
<?php if ($headerStyle === 'light'): ?>
|
||||
.page-banner-area {
|
||||
<?php if ($headerStyle === 'light'): ?>.page-banner-area {
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
@@ -214,6 +215,7 @@ if ($headerStyle === 'light') {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
<?php endif; ?>
|
||||
</style>
|
||||
|
||||
@@ -258,6 +260,7 @@ if ($headerStyle === 'light') {
|
||||
<ul class="navigation clearfix">
|
||||
<li><a href="index">Home</a></li>
|
||||
<li><a href="about">About</a></li>
|
||||
<li><a href="base4">BASE 4</a></li>
|
||||
<li><a href="trips">Trips</a>
|
||||
<?php if ($headerStyle === 'dark'): ?>
|
||||
<ul>
|
||||
@@ -282,13 +285,10 @@ if ($headerStyle === 'light') {
|
||||
<ul>
|
||||
<li><a href="admin_web_users">Website Users</a></li>
|
||||
<li><a href="admin_members">4WDCSA Members</a></li>
|
||||
<li><a href="admin_blogs">Manage Blogs</a></li>
|
||||
<li><a href="admin_events">Manage Events</a></li>
|
||||
<li><a href="admin_trips">Manage Trips</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_efts">EFT Payments</a></li>
|
||||
<li><a href="process_payments">Process Payments</a></li>
|
||||
<li><a href="admin_trips_events_courses">Trips, Events & Courses</a></li>
|
||||
<li><a href="admin_bookings">Bookings</a></li>
|
||||
<li><a href="admin_transactions">iKhokha Payment History</a></li>
|
||||
<!-- <li><a href="process_payments">Process Payments</a></li> -->
|
||||
<?php if ($role === 'superadmin') { ?>
|
||||
<li><a href="admin_visitors">Visitor Log</a></li>
|
||||
<?php } ?>
|
||||
@@ -319,8 +319,14 @@ if ($headerStyle === 'light') {
|
||||
<li><a href="account_settings">Account Settings</a></li>
|
||||
<li><a href="membership_details">Membership</a></li>
|
||||
<li><a href="bookings">My Bookings</a></li>
|
||||
<li><a href="user_blogs">My Blog Posts</a></li>
|
||||
<li><a href="submit_pop">Submit P.O.P</a></li>
|
||||
<?php
|
||||
if (getUserMemberStatus($_SESSION['user_id'])) {
|
||||
echo "<li><a href=\"user_blogs\">My Blog Posts</a></li>";
|
||||
} else {
|
||||
echo "<li><a href=\"membership\">My Blog Posts</a><i class='fal fa-lock'></i></li>";
|
||||
}
|
||||
?>
|
||||
<!-- <li><a href="submit_pop">Submit P.O.P</a></li> -->
|
||||
<li><a href="logout">Log Out</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -340,11 +346,19 @@ if ($headerStyle === 'light') {
|
||||
<div class="profile-menu">
|
||||
<div class="profile-info">
|
||||
<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">
|
||||
<img src="<?php echo $_SESSION['profile_pic']; ?>?v=<?php echo time(); ?>" alt="Profile Picture" class="profile-pic">
|
||||
</a>
|
||||
<span id="notif-badge" class="notif-badge"></span>
|
||||
</div>
|
||||
<div id="notif-panel" class="notif-panel" style="display:none;"></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 : ?>
|
||||
<a href="login" class="theme-btn style-two bgc-secondary">
|
||||
<span data-hover="Log In">Log In</span>
|
||||
@@ -359,11 +373,44 @@ if ($headerStyle === 'light') {
|
||||
<!--End Header Upper-->
|
||||
</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>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const profileInfo = document.querySelector('.profile-info');
|
||||
if (profileInfo) {
|
||||
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');
|
||||
if (dropdownMenu) {
|
||||
dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block';
|
||||
|
||||
307
index.php
@@ -4,6 +4,63 @@ $headerStyle = 'dark';
|
||||
include_once($rootPath . '/header.php');
|
||||
$indemnityPending = false;
|
||||
|
||||
// Set session flag for updates modal - only show once per session and before Jan 1, 2026
|
||||
if (!isset($_SESSION['updates_modal_shown'])) {
|
||||
$currentDate = new DateTime();
|
||||
$endDate = new DateTime('2026-01-01');
|
||||
|
||||
if ($currentDate < $endDate) {
|
||||
$_SESSION['updates_modal_shown'] = true;
|
||||
$showUpdatesModal = true;
|
||||
} else {
|
||||
$showUpdatesModal = false;
|
||||
}
|
||||
} else {
|
||||
$showUpdatesModal = false;
|
||||
}
|
||||
|
||||
// Show renew membership modal for logged-in users and where membership_fees payment_status is not PENDING RENEWAL. only show once per session
|
||||
$showRenewModal = isset($_SESSION['user_id']) ? true : false;
|
||||
if ($showRenewModal) {
|
||||
if (!isset($_SESSION['renew_modal_shown'])) {
|
||||
$_SESSION['renew_modal_shown'] = true;
|
||||
} else {
|
||||
$showRenewModal = false;
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
// Ensure we have a DB connection
|
||||
if (!isset($conn) || $conn === null) {
|
||||
$showRenewModal = false;
|
||||
} else {
|
||||
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
|
||||
$stmt->bind_param("i", $user_id);
|
||||
$stmt->execute();
|
||||
// store_result so we can check num_rows
|
||||
$stmt->store_result();
|
||||
|
||||
// If there's no membership_fees record for this user, don't show the renew modal
|
||||
if ($stmt->num_rows === 0) {
|
||||
$showRenewModal = false;
|
||||
} else {
|
||||
$stmt->bind_result($payment_status);
|
||||
$stmt->fetch();
|
||||
|
||||
if ($payment_status === 'PENDING RENEWAL') {
|
||||
$showRenewModal = false;
|
||||
}
|
||||
if (isMembershipExpiringSoon($user_id)) {
|
||||
$showRenewModal = true;
|
||||
} else {
|
||||
$showRenewModal = false;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) {
|
||||
$userId = $_SESSION['user_id'];
|
||||
$stmt = $conn->prepare("SELECT user_id FROM membership_application WHERE user_id = ? AND accept_indemnity = 0 LIMIT 1");
|
||||
@@ -144,8 +201,6 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
}
|
||||
?>
|
||||
|
||||
|
||||
|
||||
<!-- About Us Area start -->
|
||||
<section class="about-us-area py-100 rpb-90 rel z-1">
|
||||
<div class="container">
|
||||
@@ -269,7 +324,6 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
</section>
|
||||
<!-- Features Area end -->
|
||||
|
||||
|
||||
<!-- Hotel Area start -->
|
||||
<section class="hotel-area bgc-black py-100 rel z-1">
|
||||
<div class="container-fluid">
|
||||
@@ -636,8 +690,255 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
|
||||
updateCountdown(); // initial call
|
||||
setInterval(updateCountdown, 1000);
|
||||
|
||||
// Show updates modal on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const modal = document.getElementById('updatesModal');
|
||||
const closeBtn = document.querySelector('.updates-modal-close');
|
||||
const showModal = <?php echo $showUpdatesModal ? 'true' : 'false'; ?>;
|
||||
const showRenewModal = <?php echo $showRenewModal ? 'true' : 'false'; ?>;
|
||||
|
||||
if (showModal && modal) {
|
||||
// Show updates modal after a short delay for better UX
|
||||
setTimeout(function() {
|
||||
modal.style.display = 'flex';
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Close updates modal when X is clicked
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', function() {
|
||||
if (modal) modal.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Close updates modal when clicking outside the modal content
|
||||
if (modal) {
|
||||
modal.addEventListener('click', function(event) {
|
||||
if (event.target === modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show renew membership Bootstrap modal for logged-in users
|
||||
try {
|
||||
const renewModalEl = document.getElementById('renewModal');
|
||||
if (showRenewModal && renewModalEl && typeof bootstrap !== 'undefined') {
|
||||
setTimeout(function() {
|
||||
const renewModal = new bootstrap.Modal(renewModalEl);
|
||||
renewModal.show();
|
||||
}, 700);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Renew modal show failed', e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Updates Modal -->
|
||||
<!-- Renew Membership Modal (shown to logged-in users) -->
|
||||
<div class="modal fade" id="renewModal" tabindex="-1" aria-labelledby="renewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<!-- <div class="modal-header bg-secondary text-white">
|
||||
<h5 class="modal-title" id="renewModalLabel">Membership Renewal Reminder</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div> -->
|
||||
<div class="modal-body">
|
||||
Your membership will be expiring soon. Click below to renew now.
|
||||
<a style="width:100%; display:block;" href="renewal_payment" class="theme-btn style-two style-three mt-3">Renew Now</a>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" style="width:100%; display:block;" class="theme-btn" data-bs-dismiss="modal">Remind Me Later</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Updates Modal -->
|
||||
<div id="updatesModal" class="updates-modal">
|
||||
<div class="updates-modal-content">
|
||||
<span class="updates-modal-close">×</span>
|
||||
<div class="updates-modal-header">
|
||||
<h2>What's New on 4WDCSA.co.za</h2>
|
||||
</div>
|
||||
<div class="updates-modal-body">
|
||||
<div class="update-item">
|
||||
<h3><i class="fas fa-images" style="margin-right: 10px; color: #e90000;"></i>Track Map</h3>
|
||||
<p>Interactive map of the BASE4 4x4 Training Track.</p>
|
||||
</div>
|
||||
<div class="update-item">
|
||||
<h3><i class="fas fa-images" style="margin-right: 10px; color: #e90000;"></i>Photo Gallery</h3>
|
||||
<p>Explore and share memories from club events and trips. Members can now upload and view photos from past adventures.</p>
|
||||
</div>
|
||||
<div class="update-item">
|
||||
<h3><i class="fas fa-map-location-dot" style="margin-right: 10px; color: #e90000;"></i>Campsites Directory</h3>
|
||||
<p>Discover recommended campsites and accommodation options for your next adventure. Browse detailed information and member reviews.</p>
|
||||
</div>
|
||||
<div class="update-item">
|
||||
<h3><i class="fas fa-users" style="margin-right: 10px; color: #e90000;"></i>Linked Membership</h3>
|
||||
<p>Link a second user to your profile so both can book trips and receive member benefits together.</p>
|
||||
</div>
|
||||
<div class="update-item">
|
||||
<h3><i class="fas fa-pen-fancy" style="margin-right: 10px; color: #e90000;"></i>Blog Posts</h3>
|
||||
<p>Members can now post blogs, reviews, and trip reports to share experiences with the community.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="updates-modal-footer">
|
||||
<button class="theme-btn style-two updates-modal-btn" onclick="document.getElementById('updatesModal').style.display='none'">
|
||||
<span>Got It</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.updates-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.updates-modal-content {
|
||||
background-color: white;
|
||||
padding: 40px;
|
||||
border-radius: 15px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
/* Limit height so the modal never exceeds the viewport and allow internal scrolling */
|
||||
max-height: calc(100vh - 80px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(-50px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.updates-modal-close {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.updates-modal-close:hover {
|
||||
color: #e90000;
|
||||
}
|
||||
|
||||
.updates-modal-header {
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.updates-modal-header h2 {
|
||||
color: #1c231f;
|
||||
font-size: 28px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.updates-modal-body {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.update-item {
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 25px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.update-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.update-item h3 {
|
||||
color: #1c231f;
|
||||
font-size: 18px;
|
||||
margin: 0 0 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.update-item p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.updates-modal-footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.updates-modal-btn {
|
||||
padding: 10px 30px !important;
|
||||
background-color: #e90000 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.updates-modal-btn:hover {
|
||||
background-color: #c70000 !important;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.updates-modal {
|
||||
/* Align to top on small screens so content's top (and close button) is visible */
|
||||
align-items: flex-start;
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.updates-modal-content {
|
||||
padding: 20px;
|
||||
max-width: 92%;
|
||||
width: 92%;
|
||||
max-height: calc(100vh - 36px);
|
||||
}
|
||||
|
||||
.updates-modal-header h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.update-item h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
128
progress.log
Normal file
@@ -0,0 +1,128 @@
|
||||
[2025-12-15 12:32:19] AJAX BLOCK ENTERED
|
||||
[2025-12-15 12:32:19] startDate=2025-10-16
|
||||
[2025-12-15 12:32:19] endDate=2025-12-15
|
||||
[2025-12-15 12:32:19] APP ID present: YES
|
||||
[2025-12-15 12:32:19] APP SECRET present: YES
|
||||
[2025-12-15 12:32:19] PAYLOAD: https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||
[2025-12-15 12:32:19] IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
|
||||
[2025-12-15 12:32:19] CURL HTTP CODE: 422
|
||||
[2025-12-15 12:32:19] CURL ERROR: none
|
||||
[2025-12-15 12:32:19] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||
[2025-12-15 12:33:31] AJAX BLOCK ENTERED
|
||||
[2025-12-15 12:33:31] startDate=2025-10-16
|
||||
[2025-12-15 12:33:31] endDate=2025-12-15
|
||||
[2025-12-15 12:33:31] APP ID present: YES
|
||||
[2025-12-15 12:33:31] APP SECRET present: YES
|
||||
[2025-12-15 12:33:31] IKHOKHA PAYLOAD (FULL URL): https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||
[2025-12-15 12:33:31] IKHOKHA IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
|
||||
[2025-12-15 12:33:31] CURL HTTP CODE: 422
|
||||
[2025-12-15 12:33:31] CURL ERROR: none
|
||||
[2025-12-15 12:33:31] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||
[2025-12-15 12:33:59] AJAX BLOCK ENTERED
|
||||
[2025-12-15 12:33:59] startDate=2025-10-16
|
||||
[2025-12-15 12:33:59] endDate=2025-12-15
|
||||
[2025-12-15 12:33:59] APP ID present: YES
|
||||
[2025-12-15 12:33:59] APP SECRET present: YES
|
||||
[2025-12-15 12:33:59] IKHOKHA PAYLOAD (FULL URL): https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||
[2025-12-15 12:33:59] IKHOKHA IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
|
||||
[2025-12-15 12:34:00] CURL HTTP CODE: 422
|
||||
[2025-12-15 12:34:00] CURL ERROR: none
|
||||
[2025-12-15 12:34:00] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||
[2025-12-15 12:37:06] AJAX BLOCK ENTERED
|
||||
[2025-12-15 12:37:06] startDate=2025-10-16
|
||||
[2025-12-15 12:37:06] endDate=2025-12-15
|
||||
[2025-12-15 12:37:06] APP ID present: YES
|
||||
[2025-12-15 12:37:06] APP SECRET present: YES
|
||||
[2025-12-15 12:37:06] IKHOKHA ENDPOINT (REQUEST): https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||
[2025-12-15 12:37:06] IKHOKHA PAYLOAD (SIGNED): https://api.ikhokha.com/public-api/v1/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||
[2025-12-15 12:37:06] IKHOKHA IK-SIGN: 418e48921e566e5804b58f65e1ca4a28dba4d69de3611d1cf7f90f865490f42d
|
||||
[2025-12-15 12:37:06] CURL HTTP CODE: 422
|
||||
[2025-12-15 12:37:06] CURL ERROR: none
|
||||
[2025-12-15 12:37:06] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||
[2025-12-15 12:56:21] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 12:56:21] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 12:56:37] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 12:56:37] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 12:57:04] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 12:57:04] 13e6e02a7ccad937bc27b31038373d48d8ba2700a7ba8d9a7a2e4f9b07378692 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 12:57:30] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 12:57:30] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 12:57:32] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 12:57:32] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 12:57:34] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 12:57:34] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 12:58:00] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA Endpoint
|
||||
[2025-12-15 12:58:00] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 12:58:00] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 12:58:04] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA Endpoint
|
||||
[2025-12-15 12:58:04] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 12:58:04] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 12:58:17] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 12:58:17] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 12:58:17] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 12:58:48] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 12:58:48] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 12:58:48] 13e6e02a7ccad937bc27b31038373d48d8ba2700a7ba8d9a7a2e4f9b07378692 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 13:00:13] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 13:00:13] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 13:00:13] 13e6e02a7ccad937bc27b31038373d48d8ba2700a7ba8d9a7a2e4f9b07378692 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 13:00:29] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 13:00:29] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 13:00:29] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 13:03:10] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 13:03:10] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 13:03:10] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 13:03:19] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 13:03:19] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 13:03:19] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 13:05:51] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
|
||||
[2025-12-15 13:05:51] "{\"paylinkID\":\"ys5225k4z56x0mm\",\"status\":\"SUCCESS\",\"externalTransactionID\":\"693efeaca71a9\",\"responseCode\":\"00\",\"text\":null}" | CONTEXT: IKHOKHA Stringified Body
|
||||
[2025-12-15 13:06:29] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
|
||||
[2025-12-15 13:06:29] "{\"paylinkID\":\"ys5225k4z56x0mm\",\"status\":\"SUCCESS\",\"externalTransactionID\":\"693efeaca71a9\",\"responseCode\":\"00\",\"text\":null}" | CONTEXT: IKHOKHA Stringified Body
|
||||
[2025-12-15 13:06:29] /src/api/ikhokha_webhook.php\"{\\"paylinkID\\":\\"ys5225k4z56x0mm\\",\\"status\\":\\"SUCCESS\\",\\"externalTransactionID\\":\\"693efeaca71a9\\",\\"responseCode\\":\\"00\\",\\"text\\":null}\" | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 13:06:29] 43a6a56af31c276174953e115eb41402f12969fedab5b673dd34327cd7135a75 | CONTEXT: IKHOKHA Generated Signature
|
||||
[2025-12-15 13:06:42] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
|
||||
[2025-12-15 13:06:42] {"paylinkID":"ys5225k4z56x0mm","status":"SUCCESS","externalTransactionID":"693efeaca71a9","responseCode":"00","text":null} | CONTEXT: IKHOKHA Stringified Body
|
||||
[2025-12-15 13:06:42] /src/api/ikhokha_webhook.php\"{\\"paylinkID\\":\\"ys5225k4z56x0mm\\",\\"status\\":\\"SUCCESS\\",\\"externalTransactionID\\":\\"693efeaca71a9\\",\\"responseCode\\":\\"00\\",\\"text\\":null}\" | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 13:06:42] 43a6a56af31c276174953e115eb41402f12969fedab5b673dd34327cd7135a75 | CONTEXT: IKHOKHA Generated Signature
|
||||
[2025-12-15 13:07:09] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
|
||||
[2025-12-15 13:07:09] {"paylinkID":"ys5225k4z56x0mm","status":"SUCCESS","externalTransactionID":"693efeaca71a9","responseCode":"00","text":null} | CONTEXT: IKHOKHA Stringified Body
|
||||
[2025-12-15 13:07:09] /src/api/ikhokha_webhook.php\"{\\"paylinkID\\":\\"ys5225k4z56x0mm\\",\\"status\\":\\"SUCCESS\\",\\"externalTransactionID\\":\\"693efeaca71a9\\",\\"responseCode\\":\\"00\\",\\"text\\":null}\" | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 13:07:09] 43a6a56af31c276174953e115eb41402f12969fedab5b673dd34327cd7135a75 | CONTEXT: IKHOKHA Generated Signature
|
||||
[2025-12-15 13:09:39] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
|
||||
[2025-12-15 13:19:12] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 13:19:12] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 13:19:12] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 13:36:28] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 13:36:28] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 13:36:28] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 13:36:54] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 13:36:54] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 13:36:54] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 15:41:25] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 15:41:25] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 15:41:25] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 15:43:53] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 15:43:53] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 15:43:53] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 15:44:29] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 15:44:29] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 15:44:29] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 15:46:02] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 15:46:02] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 15:46:02] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 15:47:46] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 15:47:46] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 15:47:46] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[2025-12-15 15:47:51] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||
[2025-12-15 15:47:51] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||
[2025-12-15 15:47:51] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||
[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] 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
|
||||
@@ -1,107 +0,0 @@
|
||||
<?php
|
||||
// Migration runner - creates membership linking tables if they don't exist
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/src/config/env.php';
|
||||
require_once __DIR__ . '/src/config/functions.php';
|
||||
|
||||
$conn = openDatabaseConnection();
|
||||
|
||||
if (!$conn) {
|
||||
die("Database connection failed\n");
|
||||
}
|
||||
|
||||
echo "Connected to database successfully.\n\n";
|
||||
|
||||
// Check if membership_links table exists
|
||||
$checkTable = $conn->query("SHOW TABLES LIKE 'membership_links'");
|
||||
if ($checkTable->num_rows > 0) {
|
||||
echo "✓ membership_links table already exists\n";
|
||||
} else {
|
||||
echo "Creating membership_links table...\n";
|
||||
|
||||
$createLink = $conn->query("
|
||||
CREATE TABLE IF NOT EXISTS `membership_links` (
|
||||
`link_id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`primary_user_id` INT NOT NULL,
|
||||
`secondary_user_id` INT NOT NULL,
|
||||
`relationship` VARCHAR(50) NOT NULL DEFAULT 'spouse',
|
||||
`linked_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT `fk_membership_links_primary` FOREIGN KEY (`primary_user_id`)
|
||||
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_membership_links_secondary` FOREIGN KEY (`secondary_user_id`)
|
||||
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
|
||||
INDEX `idx_primary_user` (`primary_user_id`),
|
||||
INDEX `idx_secondary_user` (`secondary_user_id`),
|
||||
|
||||
UNIQUE KEY `unique_link` (`primary_user_id`, `secondary_user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
");
|
||||
|
||||
if ($createLink) {
|
||||
echo "✓ membership_links table created successfully\n";
|
||||
} else {
|
||||
echo "✗ Error creating membership_links table: " . $conn->error . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Check if membership_permissions table exists
|
||||
$checkTable = $conn->query("SHOW TABLES LIKE 'membership_permissions'");
|
||||
if ($checkTable->num_rows > 0) {
|
||||
echo "✓ membership_permissions table already exists\n";
|
||||
} else {
|
||||
echo "Creating membership_permissions table...\n";
|
||||
|
||||
$createPerm = $conn->query("
|
||||
CREATE TABLE IF NOT EXISTS `membership_permissions` (
|
||||
`permission_id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`link_id` INT NOT NULL,
|
||||
`permission_name` VARCHAR(100) NOT NULL,
|
||||
`granted_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT `fk_membership_permissions_link` FOREIGN KEY (`link_id`)
|
||||
REFERENCES `membership_links`(`link_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
|
||||
INDEX `idx_link` (`link_id`),
|
||||
|
||||
UNIQUE KEY `unique_permission` (`link_id`, `permission_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
");
|
||||
|
||||
if ($createPerm) {
|
||||
echo "✓ membership_permissions table created successfully\n";
|
||||
} else {
|
||||
echo "✗ Error creating membership_permissions table: " . $conn->error . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Create or replace the view
|
||||
echo "\nCreating linked_membership_users view...\n";
|
||||
$createView = $conn->query("
|
||||
CREATE OR REPLACE VIEW `linked_membership_users` AS
|
||||
SELECT
|
||||
primary_user_id,
|
||||
secondary_user_id,
|
||||
relationship,
|
||||
linked_at
|
||||
FROM membership_links
|
||||
UNION ALL
|
||||
SELECT
|
||||
primary_user_id,
|
||||
primary_user_id as secondary_user_id,
|
||||
'primary' as relationship,
|
||||
linked_at
|
||||
FROM membership_links
|
||||
");
|
||||
|
||||
if ($createView) {
|
||||
echo "✓ View created successfully\n";
|
||||
} else {
|
||||
echo "✗ Error creating view: " . $conn->error . "\n";
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
echo "\n✓ Migration completed successfully!\n";
|
||||
?>
|
||||
61
scripts/ikhokha_migrations/001_add_payment_columns.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
-- Migration: add iKhokha / provider metadata to payments table
|
||||
-- Migration: add iKhokha / provider metadata to payments table
|
||||
-- Compatible with MySQL versions that do not support `ADD COLUMN IF NOT EXISTS`.
|
||||
-- Run on staging first. Make a DB backup before running on production.
|
||||
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE add_payment_columns_if_missing()
|
||||
BEGIN
|
||||
-- provider
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND COLUMN_NAME = 'provider') = 0 THEN
|
||||
ALTER TABLE `payments` ADD COLUMN `provider` VARCHAR(50) NULL AFTER `status`;
|
||||
END IF;
|
||||
|
||||
-- provider_payment_id
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND COLUMN_NAME = 'provider_payment_id') = 0 THEN
|
||||
ALTER TABLE `payments` ADD COLUMN `provider_payment_id` VARCHAR(128) NULL AFTER `provider`;
|
||||
END IF;
|
||||
|
||||
-- payment_link
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND COLUMN_NAME = 'payment_link') = 0 THEN
|
||||
ALTER TABLE `payments` ADD COLUMN `payment_link` VARCHAR(512) NULL AFTER `provider_payment_id`;
|
||||
END IF;
|
||||
|
||||
-- provider_status
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND COLUMN_NAME = 'provider_status') = 0 THEN
|
||||
ALTER TABLE `payments` ADD COLUMN `provider_status` VARCHAR(50) NULL AFTER `payment_link`;
|
||||
END IF;
|
||||
|
||||
-- provider_response (JSON)
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND COLUMN_NAME = 'provider_response') = 0 THEN
|
||||
ALTER TABLE `payments` ADD COLUMN `provider_response` JSON NULL AFTER `provider_status`;
|
||||
END IF;
|
||||
|
||||
-- booking_id
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND COLUMN_NAME = 'booking_id') = 0 THEN
|
||||
ALTER TABLE `payments` ADD COLUMN `booking_id` INT NULL AFTER `user_id`;
|
||||
END IF;
|
||||
|
||||
-- index idx_provider_payment_id
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND INDEX_NAME = 'idx_provider_payment_id') = 0 THEN
|
||||
ALTER TABLE `payments` ADD INDEX `idx_provider_payment_id` (`provider_payment_id`(128));
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
CALL add_payment_columns_if_missing();
|
||||
DROP PROCEDURE IF EXISTS add_payment_columns_if_missing;
|
||||
|
||||
-- Notes:
|
||||
-- 1) This script creates a short stored procedure which performs existence checks
|
||||
-- against INFORMATION_SCHEMA before applying each ALTER TABLE. Run in the
|
||||
-- MySQL client or via your migration tool. It avoids syntax not supported on
|
||||
-- older MySQL versions.
|
||||
-- 2) Test on staging and make a DB dump before running on production.
|
||||
14
scripts/ikhokha_migrations/002_migrate_efts_to_payments.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- Migration: copy existing efts records into payments for historical continuity
|
||||
-- This inserts EFT records into payments table as payments with provider='eft'.
|
||||
-- Run only after verifying step 001 has been applied and a backup exists.
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
INSERT IGNORE INTO `payments` (`payment_id`, `user_id`, `amount`, `status`, `date`, `description`, `provider`, `provider_payment_id`, `provider_status`)
|
||||
SELECT `eft_id`, `user_id`, `amount`, `status`, `timestamp`, `description`, 'eft', `eft_id`, `status` FROM `efts`;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Notes:
|
||||
-- 1) `INSERT IGNORE` prevents duplicate primary-key errors (payments.payment_id is PK).
|
||||
-- 2) After running, review migrated rows and ensure admin workflows still operate.
|
||||
21
scripts/ikhokha_migrations/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# iKhokha Migration SQL
|
||||
|
||||
This folder contains SQL migration files to add iKhokha/provider metadata to the `payments` table and to migrate legacy `efts` records.
|
||||
|
||||
Order to run:
|
||||
|
||||
1. Backup your database (mysqldump or preferred tool).
|
||||
2. Apply `001_add_payment_columns.sql` on staging and verify schema changes.
|
||||
3. (Optional) Apply `002_migrate_efts_to_payments.sql` to copy legacy `efts` into `payments`.
|
||||
|
||||
Commands (example using MySQL client):
|
||||
|
||||
```bash
|
||||
mysql -u dbuser -p databasename < scripts/ikhokha_migrations/001_add_payment_columns.sql
|
||||
mysql -u dbuser -p databasename < scripts/ikhokha_migrations/002_migrate_efts_to_payments.sql
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Always test on staging before running in production.
|
||||
- `provider_response` is a JSON column used to store raw provider responses for audit.
|
||||
- If you prefer not to migrate `efts`, skip step 3 and keep legacy POP handling.
|
||||
139
sitemap.xml
@@ -1,53 +1,124 @@
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
|
||||
<!-- Updated: 2025-12-13 -->
|
||||
|
||||
<!-- Homepage -->
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/</loc>
|
||||
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>1.00</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
|
||||
<!-- Main Pages -->
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/about</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.90</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/index.php</loc>
|
||||
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
||||
<loc>https://4wdcsa.co.za/contact</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.90</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/track-map</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.85</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
|
||||
<!-- Trips & Events -->
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/trips</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.95</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/events</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.95</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/driver_training</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.90</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
|
||||
<!-- Blog & Gallery -->
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/blog</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.85</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/gallery</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
|
||||
<!-- Membership -->
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/membership</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.95</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/about.php</loc>
|
||||
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
||||
<loc>https://4wdcsa.co.za/membership_details</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.85</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
|
||||
<!-- Campsites -->
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/campsites</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.90</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
|
||||
<!-- Special Pages -->
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/rescue_recovery</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/trips.php</loc>
|
||||
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
||||
<loc>https://4wdcsa.co.za/bush_mechanics</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
|
||||
<!-- Auth Pages (Lower Priority) -->
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/login</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.60</priority>
|
||||
<changefreq>yearly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/events.php</loc>
|
||||
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
<loc>https://4wdcsa.co.za/register</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.60</priority>
|
||||
<changefreq>yearly</changefreq>
|
||||
</url>
|
||||
|
||||
<!-- Legal -->
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/blog.php</loc>
|
||||
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/login.php</loc>
|
||||
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/membership.php</loc>
|
||||
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/register.php</loc>
|
||||
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://4wdcsa.co.za/forgot_password.php</loc>
|
||||
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
<loc>https://4wdcsa.co.za/privacy_policy</loc>
|
||||
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
|
||||
<priority>0.50</priority>
|
||||
<changefreq>yearly</changefreq>
|
||||
</url>
|
||||
|
||||
</urlset>
|
||||
|
||||
56
src/admin/_admin_tx_debug.log
Normal file
@@ -0,0 +1,56 @@
|
||||
[2025-12-15 12:28:42] FILE HIT
|
||||
[2025-12-15 12:28:42] AJAX BLOCK ENTERED
|
||||
[2025-12-15 12:28:42] startDate=2025-10-16
|
||||
[2025-12-15 12:28:42] endDate=2025-12-15
|
||||
[2025-12-15 12:28:42] APP ID present: YES
|
||||
[2025-12-15 12:28:42] APP SECRET present: YES
|
||||
[2025-12-15 12:28:42] PAYLOAD: /public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||
[2025-12-15 12:28:42] IK-SIGN: 3d610c60c8306cd1d5c99b2639f0e810594f8ffb9306a98d703f691173dab47d
|
||||
[2025-12-15 12:28:44] CURL HTTP CODE: 422
|
||||
[2025-12-15 12:28:44] CURL ERROR: none
|
||||
[2025-12-15 12:28:44] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||
[2025-12-15 12:28:51] FILE HIT
|
||||
[2025-12-15 12:28:51] AJAX BLOCK ENTERED
|
||||
[2025-12-15 12:28:51] startDate=2025-10-16
|
||||
[2025-12-15 12:28:51] endDate=2025-12-15
|
||||
[2025-12-15 12:28:51] APP ID present: YES
|
||||
[2025-12-15 12:28:51] APP SECRET present: YES
|
||||
[2025-12-15 12:28:51] PAYLOAD: /public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||
[2025-12-15 12:28:51] IK-SIGN: 3d610c60c8306cd1d5c99b2639f0e810594f8ffb9306a98d703f691173dab47d
|
||||
[2025-12-15 12:28:51] CURL HTTP CODE: 422
|
||||
[2025-12-15 12:28:51] CURL ERROR: none
|
||||
[2025-12-15 12:28:51] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||
[2025-12-15 12:30:54] FILE HIT
|
||||
[2025-12-15 12:30:54] AJAX BLOCK ENTERED
|
||||
[2025-12-15 12:30:54] startDate=2025-10-16
|
||||
[2025-12-15 12:30:54] endDate=2025-12-15
|
||||
[2025-12-15 12:30:54] APP ID present: YES
|
||||
[2025-12-15 12:30:54] APP SECRET present: YES
|
||||
[2025-12-15 12:30:54] PAYLOAD: https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||
[2025-12-15 12:30:54] IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
|
||||
[2025-12-15 12:30:55] CURL HTTP CODE: 422
|
||||
[2025-12-15 12:30:55] CURL ERROR: none
|
||||
[2025-12-15 12:30:55] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||
[2025-12-15 12:31:13] FILE HIT
|
||||
[2025-12-15 12:31:13] AJAX BLOCK ENTERED
|
||||
[2025-12-15 12:31:13] startDate=2025-10-16
|
||||
[2025-12-15 12:31:13] endDate=2025-12-15
|
||||
[2025-12-15 12:31:13] APP ID present: YES
|
||||
[2025-12-15 12:31:13] APP SECRET present: YES
|
||||
[2025-12-15 12:31:13] PAYLOAD: https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||
[2025-12-15 12:31:13] IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
|
||||
[2025-12-15 12:31:13] CURL HTTP CODE: 422
|
||||
[2025-12-15 12:31:13] CURL ERROR: none
|
||||
[2025-12-15 12:31:13] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||
[2025-12-15 12:31:21] FILE HIT
|
||||
[2025-12-15 12:31:47] FILE HIT
|
||||
[2025-12-15 12:31:47] FILE HIT
|
||||
[2025-12-15 12:31:58] FILE HIT
|
||||
[2025-12-15 12:32:18] FILE HIT
|
||||
[2025-12-15 12:32:19] FILE HIT
|
||||
[2025-12-15 12:33:30] FILE HIT
|
||||
[2025-12-15 12:33:31] FILE HIT
|
||||
[2025-12-15 12:33:59] FILE HIT
|
||||
[2025-12-15 12:33:59] FILE HIT
|
||||
[2025-12-15 12:37:05] FILE HIT
|
||||
[2025-12-15 12:37:06] FILE HIT
|
||||
@@ -38,13 +38,8 @@ if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] !== UPLOAD_ERR_
|
||||
}
|
||||
|
||||
$uploadDir = "assets/uploads/campsites/";
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
if (!is_writable($uploadDir)) {
|
||||
http_response_code(500);
|
||||
die('Upload directory is not writable.');
|
||||
if (!file_exists($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
$randomFilename = $validationResult['filename'];
|
||||
|
||||
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,361 +0,0 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
include_once($rootPath . '/header.php');
|
||||
checkAdmin();
|
||||
|
||||
// Fetch all events
|
||||
$events_query = "
|
||||
SELECT
|
||||
event_id, name, type, location, date, 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>
|
||||
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: 10px;
|
||||
}
|
||||
|
||||
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: 10px;
|
||||
font-size: 16px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-radius: 25px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
margin: 2px;
|
||||
font-size: 14px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: #ffc107;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background-color: #e0a800;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: #ffc107;
|
||||
color: black;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Sorting functionality
|
||||
const table = document.querySelector('table');
|
||||
if (table) {
|
||||
const headers = table.querySelectorAll('thead th');
|
||||
const rows = Array.from(table.querySelectorAll('tbody tr'));
|
||||
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
||||
// Filter functionality
|
||||
const filterInput = document.querySelector('.filter-input');
|
||||
if (filterInput) {
|
||||
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';
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Publish/Unpublish toggle
|
||||
$('.toggle-publish').on('click', function() {
|
||||
var eventId = $(this).data('event-id');
|
||||
var button = $(this);
|
||||
var row = button.closest('tr');
|
||||
|
||||
$.ajax({
|
||||
url: 'toggle_event_published',
|
||||
type: 'POST',
|
||||
data: {
|
||||
event_id: eventId
|
||||
},
|
||||
dataType: 'json',
|
||||
complete: function(xhr, status) {
|
||||
// Handle all response codes
|
||||
try {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (response.status === 'success') {
|
||||
if (response.published == 1) {
|
||||
button.removeClass('btn-success').addClass('btn-warning');
|
||||
button.find('i').removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
button.attr('title', 'Unpublish');
|
||||
row.find('td:nth-child(5)').html('<span class="badge bg-success">Published</span>');
|
||||
} else {
|
||||
button.removeClass('btn-warning').addClass('btn-success');
|
||||
button.find('i').removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
button.attr('title', 'Publish');
|
||||
row.find('td:nth-child(5)').html('<span class="badge bg-warning">Draft</span>');
|
||||
}
|
||||
} else {
|
||||
alert('Error: ' + response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error updating event status. Response: ' + xhr.responseText);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete event
|
||||
$('.delete-event').on('click', function() {
|
||||
if (!confirm('Are you sure you want to delete this event? This action cannot be undone.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var eventId = $(this).data('event-id');
|
||||
var button = $(this);
|
||||
var row = button.closest('tr');
|
||||
|
||||
$.ajax({
|
||||
url: 'delete_event',
|
||||
type: 'POST',
|
||||
data: {
|
||||
event_id: eventId
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.status === 'success') {
|
||||
row.fadeOut(300, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
} else {
|
||||
alert('Error: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error deleting event');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
$pageTitle = 'Manage Events';
|
||||
$breadcrumbs = [['Home' => 'index'], [$pageTitle => '']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
|
||||
<?php
|
||||
$pageTitle = 'Manage Events';
|
||||
$breadcrumbs = [['Home' => 'index'], [$pageTitle => '']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
|
||||
<!-- Events Management Area start -->
|
||||
<section class="events-management-area py-100 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row mb-30">
|
||||
<div class="col-lg-12">
|
||||
<a href="manage_events" class="theme-btn style-two">+ Create New Event</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
if (!empty($events)) {
|
||||
echo '<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="form-group mb-20">
|
||||
<input type="text" class="filter-input" placeholder="Search events...">
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Type</th>
|
||||
<th>Location</th>
|
||||
<th>Date</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
foreach ($events as $event) {
|
||||
$publishButtonText = $event['published'] == 1 ? 'Unpublish' : 'Publish';
|
||||
$publishButtonClass = $event['published'] == 1 ? 'btn-warning' : 'btn-success';
|
||||
echo '<tr>
|
||||
<td><strong>' . htmlspecialchars($event['name']) . '</strong></td>
|
||||
<td>' . htmlspecialchars($event['type']) . '</td>
|
||||
<td>' . htmlspecialchars($event['location']) . '</td>
|
||||
<td>' . convertDate($event['date']) . '</td>
|
||||
<td>' . ($event['published'] == 1 ? '<span class="badge bg-success">Published</span>' : '<span class="badge bg-warning">Draft</span>') . '</td>
|
||||
<td>
|
||||
<a href="manage_events?event_id=' . $event['event_id'] . '" class="btn btn-sm btn-primary" title="Edit">
|
||||
<i class="far fa-edit"></i>
|
||||
</a>
|
||||
<button class="btn btn-sm ' . $publishButtonClass . ' toggle-publish" data-event-id="' . $event['event_id'] . '" title="' . $publishButtonText . '">
|
||||
<i class="far fa-' . ($event['published'] == 1 ? 'eye-slash' : 'eye') . '"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger delete-event" data-event-id="' . $event['event_id'] . '" title="Delete">
|
||||
<i class="far fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<p>No events found. <a href="manage_events">Create one</a></p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Events Management Area end -->
|
||||
|
||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||
248
src/admin/admin_transactions.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
.infobox {
|
||||
color: #484848;
|
||||
background: #f9f9f7;
|
||||
border: 1px solid #d8d8d8;
|
||||
border-radius: 10px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const table = document.querySelector("table");
|
||||
const headers = table.querySelectorAll("thead th");
|
||||
const rows = Array.from(table.querySelectorAll("tbody tr"));
|
||||
const filterInput = document.getElementById("filterInput");
|
||||
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
||||
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>
|
||||
<!-- Page Banner Start -->
|
||||
<?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">iKhokha Payments</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">iKhokha Payments</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tour List Area start -->
|
||||
<section class="tour-list-page py-10 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class='infobox' data-aos='fade-up' data-aos-duration='1500' data-aos-offset='50'>
|
||||
<div style='padding:10px;'>
|
||||
<?php
|
||||
// Fetch transactions from iKhokha API instead of DB
|
||||
$startDate = isset($_GET['start']) ? $_GET['start'] : date('Y-m-d', strtotime('-30 days'));
|
||||
$endDate = isset($_GET['end']) ? $_GET['end'] : date('Y-m-d');
|
||||
|
||||
// getIkhokhaTransactionHistory should return JSON (string) or an array
|
||||
$raw = getIkhokhaTransactionHistory($startDate, $endDate);
|
||||
$transactions = [];
|
||||
if (is_string($raw)) {
|
||||
$transactions = json_decode($raw, true);
|
||||
} elseif (is_array($raw)) {
|
||||
$transactions = $raw;
|
||||
}
|
||||
|
||||
if (!empty($transactions)) {
|
||||
echo '<input id="filterInput" type="text" class="filter-input" placeholder="Filter results...">';
|
||||
echo '<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>ID</th>
|
||||
<th>PaylinkID</th>
|
||||
<th>Description</th>
|
||||
<th>Amount</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
$printed = false;
|
||||
foreach ($transactions as $row) {
|
||||
$createdAt = isset($row['createdAt']) ? htmlspecialchars($row['createdAt']) : '';
|
||||
// prefer externalTransactionID when available, fallback to paylinkID
|
||||
$txId = isset($row['externalTransactionID']) ? $row['externalTransactionID'] : (isset($row['paylinkID']) ? $row['paylinkID'] : '');
|
||||
$ikhokhaTxId = isset($row['paylinkID']) ? $row['paylinkID'] : '';
|
||||
$description = isset($row['description']) ? $row['description'] : '';
|
||||
$amount = isset($row['amount']) ? $row['amount'] : '';
|
||||
$status = isset($row['status']) ? $row['status'] : '';
|
||||
|
||||
// Skip unpaid transactions
|
||||
if (strcasecmp($status, 'UNPAID') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "<tr>
|
||||
<td>" . htmlspecialchars($createdAt) . "</td>
|
||||
<td>" . htmlspecialchars($txId) . "</td>
|
||||
<td>" . htmlspecialchars($ikhokhaTxId) . "</td>
|
||||
<td>" . htmlspecialchars($description) . "</td>
|
||||
<td>R " . htmlspecialchars($amount/100) . ".00</td>
|
||||
<td>" . htmlspecialchars($status) . "</td>
|
||||
</tr>";
|
||||
|
||||
$printed = true;
|
||||
}
|
||||
|
||||
if (!$printed) {
|
||||
echo '<tr><td colspan="6">No records found</td></tr>';
|
||||
}
|
||||
} else {
|
||||
echo '<input id="filterInput" type="text" class="filter-input" placeholder="Filter results...">';
|
||||
echo '<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>ID</th>
|
||||
<th>Description</th>
|
||||
<th>Amount</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
echo '<tr><td colspan="5">No records found</td></tr>';
|
||||
} ?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Tour List Area end -->
|
||||
|
||||
|
||||
<?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,320 +0,0 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
include_once($rootPath . '/header.php');
|
||||
checkAdmin();
|
||||
|
||||
// Fetch all trips with booking status
|
||||
$trips_query = "
|
||||
SELECT
|
||||
trip_id, trip_name, location, start_date, end_date,
|
||||
vehicle_capacity, places_booked, cost_members, 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>
|
||||
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;
|
||||
font-size: 16px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-radius: 25px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.trips-section {
|
||||
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">Manage Trips</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">Home</a></li>
|
||||
<li class="breadcrumb-item active">Manage Trips</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Trips Management Area start -->
|
||||
<section class="tour-list-page py-100 rel z-1">
|
||||
<div class="container">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<a href="manage_trips" class="theme-btn">
|
||||
<i class="far fa-plus"></i> Create New Trip
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
if (count($trips) > 0) {
|
||||
echo '<input type="text" class="filter-input" placeholder="Filter trips...">';
|
||||
echo '<div class="trips-section" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
|
||||
echo '<div style="padding:10px;">';
|
||||
echo '<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Trip Name</th>
|
||||
<th>Location</th>
|
||||
<th>Start Date</th>
|
||||
<th>End Date</th>
|
||||
<th>Capacity</th>
|
||||
<th>Booked</th>
|
||||
<th>Cost (Member)</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
foreach ($trips as $trip) {
|
||||
$publishButtonText = $trip['published'] == 1 ? 'Unpublish' : 'Publish';
|
||||
$publishButtonClass = $trip['published'] == 1 ? 'btn-warning' : 'btn-success';
|
||||
echo '<tr>
|
||||
<td><strong>' . htmlspecialchars($trip['trip_name']) . '</strong></td>
|
||||
<td>' . htmlspecialchars($trip['location']) . '</td>
|
||||
<td>' . date('M d, Y', strtotime($trip['start_date'])) . '</td>
|
||||
<td>' . date('M d, Y', strtotime($trip['end_date'])) . '</td>
|
||||
<td>' . $trip['vehicle_capacity'] . '</td>
|
||||
<td><span class="badge bg-info">' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '</span></td>
|
||||
<td>R ' . number_format($trip['cost_members'], 2) . '</td>
|
||||
<td>' . ($trip['published'] == 1 ? '<span class="badge bg-success">Published</span>' : '<span class="badge bg-warning">Draft</span>') . '</td>
|
||||
<td>
|
||||
<a href="manage_trips?trip_id=' . $trip['trip_id'] . '" class="btn btn-sm btn-primary" title="Edit">
|
||||
<i class="far fa-edit"></i>
|
||||
</a>
|
||||
<button class="btn btn-sm ' . $publishButtonClass . ' toggle-publish" data-trip-id="' . $trip['trip_id'] . '" title="' . $publishButtonText . '">
|
||||
<i class="far fa-' . ($trip['published'] == 1 ? 'eye-slash' : 'eye') . '"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger delete-trip" data-trip-id="' . $trip['trip_id'] . '" title="Delete">
|
||||
<i class="far fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<p>No trips found. <a href="manage_trips">Create one</a></p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Trips Management Area end -->
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.toggle-publish').on('click', function() {
|
||||
var tripId = $(this).data('trip-id');
|
||||
var button = $(this);
|
||||
var row = button.closest('tr');
|
||||
|
||||
$.ajax({
|
||||
url: 'toggle_trip_published',
|
||||
type: 'POST',
|
||||
data: {
|
||||
trip_id: tripId
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.status === 'success') {
|
||||
// Update button appearance
|
||||
if (response.published == 1) {
|
||||
button.removeClass('btn-success').addClass('btn-warning');
|
||||
button.find('i').removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
button.attr('title', 'Unpublish');
|
||||
// Update status badge
|
||||
row.find('td:nth-child(8)').html('<span class="badge bg-success">Published</span>');
|
||||
} else {
|
||||
button.removeClass('btn-warning').addClass('btn-success');
|
||||
button.find('i').removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
button.attr('title', 'Publish');
|
||||
// Update status badge
|
||||
row.find('td:nth-child(8)').html('<span class="badge bg-warning">Draft</span>');
|
||||
}
|
||||
} else {
|
||||
alert('Error: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error updating trip status');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.delete-trip').on('click', function() {
|
||||
if (!confirm('Are you sure you want to delete this trip? This action cannot be undone.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var tripId = $(this).data('trip-id');
|
||||
var button = $(this);
|
||||
var row = button.closest('tr');
|
||||
|
||||
$.ajax({
|
||||
url: 'delete_trip',
|
||||
type: 'POST',
|
||||
data: {
|
||||
trip_id: tripId
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.status === 'success') {
|
||||
row.fadeOut(function() {
|
||||
$(this).remove();
|
||||
if ($('table tbody tr').length === 0) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert('Error: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error deleting trip');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||
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'); ?>
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
include_once($rootPath . '/header.php');
|
||||
checkAdmin();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$event_id = $_POST['event_id'] ?? null;
|
||||
|
||||
if (!$event_id) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Event ID is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get event details to delete associated files
|
||||
$stmt = $conn->prepare("SELECT image, promo FROM events WHERE event_id = ?");
|
||||
$stmt->bind_param("i", $event_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
$event = $result->fetch_assoc();
|
||||
|
||||
// Delete image files
|
||||
if ($event['image'] && file_exists($rootPath . '/' . $event['image'])) {
|
||||
unlink($rootPath . '/' . $event['image']);
|
||||
}
|
||||
if ($event['promo'] && file_exists($rootPath . '/' . $event['promo'])) {
|
||||
unlink($rootPath . '/' . $event['promo']);
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
$delete_stmt = $conn->prepare("DELETE FROM events WHERE event_id = ?");
|
||||
$delete_stmt->bind_param("i", $event_id);
|
||||
|
||||
if ($delete_stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Event deleted successfully']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to delete event']);
|
||||
}
|
||||
$delete_stmt->close();
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Event not found']);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
221
src/admin/manage_courses.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
include_once($rootPath . '/header.php');
|
||||
checkAdmin();
|
||||
|
||||
$course_id = $_GET['course_id'] ?? null;
|
||||
$course = null;
|
||||
|
||||
// If editing an existing course, fetch its data
|
||||
if ($course_id) {
|
||||
$stmt = $conn->prepare("SELECT * FROM courses WHERE course_id = ?");
|
||||
$stmt->bind_param("i", $course_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
if ($result->num_rows > 0) {
|
||||
$course = $result->fetch_assoc();
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
?>
|
||||
|
||||
<?php
|
||||
$pageTitle = $course ? 'Edit Course' : 'Create New Course';
|
||||
$breadcrumbs = [['Home' => 'index'], ['Admin' => 'admin_courses'], [$pageTitle => '']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
|
||||
<!-- Course Manager Area start -->
|
||||
<section class="trip-manager-area py-100 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="comment-form bgc-lighter z-1 rel mb-30 rmb-55">
|
||||
<form id="courseForm" method="POST" action="process_course">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
|
||||
<?php if ($course): ?>
|
||||
<input type="hidden" name="course_id" value="<?php echo $course['course_id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="section-title py-20">
|
||||
<h2><?php echo $course ? 'Edit Course: ' . htmlspecialchars($course['code'] ?: $course['course_type']) : 'Create New Course'; ?></h2>
|
||||
<div id="responseMessage"></div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-35">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="course_type">Course Type *</label>
|
||||
<select id="course_type" name="course_type" class="form-control" required>
|
||||
<?php
|
||||
$types = ['driver_training' => 'Driver Training', 'bush_mechanics' => 'Bush Mechanics', 'rescue_recovery' => 'Rescue & Recovery', 'ladies_driver_training' => 'Ladies Driver Training'];
|
||||
foreach ($types as $key => $label) {
|
||||
$sel = ($course && $course['course_type'] === $key) ? 'selected' : '';
|
||||
echo "<option value=\"$key\" $sel>$label</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="code">Course Code</label>
|
||||
<input type="text" id="code" name="code" class="form-control" maxlength="12" value="<?php echo $course ? htmlspecialchars($course['code']) : ''; ?>" placeholder="Optional code e.g., CRSE001" data-manual="0">
|
||||
<small class="form-text text-muted">Auto-generated from type + date (you can edit manually)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="date">Date *</label>
|
||||
<input type="date" id="date" name="date" class="form-control" value="<?php echo $course ? $course['date'] : ''; ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="capacity">Capacity *</label>
|
||||
<input type="number" id="capacity" name="capacity" class="form-control" min="1" value="<?php echo $course ? intval($course['capacity']) : ''; ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cost_members">Member Cost (R) *</label>
|
||||
<input type="number" id="cost_members" name="cost_members" class="form-control" step="0.01" min="0" value="<?php echo $course ? $course['cost_members'] : ''; ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cost_nonmembers">Non-Member Cost (R) *</label>
|
||||
<input type="number" id="cost_nonmembers" name="cost_nonmembers" class="form-control" step="0.01" min="0" value="<?php echo $course ? $course['cost_nonmembers'] : ''; ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="instructor">Instructor *</label>
|
||||
<input type="text" id="instructor" name="instructor" class="form-control" value="<?php echo $course ? htmlspecialchars($course['instructor']) : ''; ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="instructor_email">Instructor Email</label>
|
||||
<input type="email" id="instructor_email" name="instructor_email" class="form-control" value="<?php echo $course ? htmlspecialchars($course['instructor_email']) : ''; ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mt-20">
|
||||
<div class="form-group mb-0">
|
||||
<button type="submit" class="theme-btn style-two" style="width:100%;">
|
||||
<?php echo $course ? 'Update Course' : 'Create Course'; ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Course Manager Area end -->
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#courseForm').on('submit', function(event) {
|
||||
event.preventDefault();
|
||||
var formData = $(this).serialize();
|
||||
|
||||
$.ajax({
|
||||
url: 'process_course',
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.status === 'success') {
|
||||
$('#responseMessage').html('<div class="alert alert-success">' + response.message + '</div>');
|
||||
setTimeout(function() {
|
||||
window.location.href = 'admin_courses';
|
||||
}, 1200);
|
||||
} else {
|
||||
$('#responseMessage').html('<div class="alert alert-danger">' + response.message + '</div>');
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX Error:', error);
|
||||
$('#responseMessage').html('<div class="alert alert-danger">Error creating/updating course: ' + error + '</div>');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Auto-generate course code from type and date: ABBREVIATION_MMDD
|
||||
(function(){
|
||||
var typeMap = {
|
||||
'driver_training': 'DRVTRN',
|
||||
'bush_mechanics': 'BUSHMEC',
|
||||
'rescue_recovery': 'RESREC',
|
||||
'ladies_driver_training': 'LADYTRN'
|
||||
};
|
||||
|
||||
var $type = document.getElementById('course_type');
|
||||
var $date = document.getElementById('date');
|
||||
var $code = document.getElementById('code');
|
||||
|
||||
function getMMDDFromISO(isoDate) {
|
||||
if (!isoDate) return '';
|
||||
// expecting YYYY-MM-DD
|
||||
var parts = isoDate.split('-');
|
||||
if (parts.length !== 3) return '';
|
||||
return parts[1] + parts[2];
|
||||
}
|
||||
|
||||
function generateCode() {
|
||||
try {
|
||||
var manual = $code.getAttribute('data-manual') === '1';
|
||||
if (manual) return; // user has manually edited
|
||||
var t = $type.value;
|
||||
var d = $date.value;
|
||||
if (!t || !d) return;
|
||||
var abbr = typeMap[t] || t.toUpperCase().replace(/[^A-Z0-9]/g,'').substring(0,7);
|
||||
var mmdd = getMMDDFromISO(d);
|
||||
if (!mmdd) return;
|
||||
var newCode = abbr + '_' + mmdd;
|
||||
$code.value = newCode;
|
||||
} catch (e) {
|
||||
console.error('generateCode error', e);
|
||||
}
|
||||
}
|
||||
|
||||
// mark manual when user types
|
||||
$code.addEventListener('input', function(){
|
||||
var val = $code.value.trim();
|
||||
if (val.length === 0) {
|
||||
$code.setAttribute('data-manual','0');
|
||||
} else {
|
||||
// if value matches auto pattern for currently selected type+date, keep as auto; otherwise mark manual
|
||||
var expected = '';
|
||||
try { expected = (typeMap[$type.value] || $type.value.toUpperCase().replace(/[^A-Z0-9]/g,'').substring(0,7)) + '_' + ( ($date.value) ? $date.value.split('-')[1] + $date.value.split('-')[2] : '' ); } catch(e){ expected=''; }
|
||||
if (val === expected) {
|
||||
$code.setAttribute('data-manual','0');
|
||||
} else {
|
||||
$code.setAttribute('data-manual','1');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$type.addEventListener('change', generateCode);
|
||||
$date.addEventListener('change', generateCode);
|
||||
|
||||
// generate on load if empty
|
||||
if ($code.value.trim().length === 0) {
|
||||
generateCode();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||
292
src/api/ikhokha_webhook.php
Normal file
@@ -0,0 +1,292 @@
|
||||
<?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 . "/src/helpers/notification_helper.php");
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* JS-equivalent escaping (matches iKhokha docs exactly)
|
||||
* ==========================================================
|
||||
*/
|
||||
function jsStringEscape(string $str): string
|
||||
{
|
||||
$str = preg_replace('/([\\\\\"\'])/', '\\\\$1', $str);
|
||||
$str = str_replace("\0", "\\0", $str);
|
||||
return $str;
|
||||
}
|
||||
|
||||
function createPayloadToSign(string $path, string $body): string
|
||||
{
|
||||
return jsStringEscape($path . $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Read raw request body (DO NOT MODIFY)
|
||||
* ==========================================================
|
||||
*/
|
||||
$raw = file_get_contents('php://input');
|
||||
|
||||
if ($raw === false || $raw === '') {
|
||||
http_response_code(400);
|
||||
progress_log('iKhokha webhook: empty body');
|
||||
exit('No body');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Read headers
|
||||
* ==========================================================
|
||||
*/
|
||||
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
||||
$headers = array_change_key_case($headers, CASE_LOWER);
|
||||
|
||||
$ikSign = $headers['ik-sign'] ?? null;
|
||||
$ikAppId = $headers['ik-appid'] ?? null;
|
||||
|
||||
if (!$ikSign || !$ikAppId) {
|
||||
http_response_code(400);
|
||||
progress_log('iKhokha webhook: missing headers');
|
||||
exit('Missing headers');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Signature verification (JS-equivalent)
|
||||
* ==========================================================
|
||||
*/
|
||||
$secret = $_ENV['IKHOKHA_APP_SECRET'] ?? null;
|
||||
$callbackUrl = $_ENV['IKHOKHA_CALLBACK_URL'] ?? null;
|
||||
$bypass = ($_ENV['IKHOKHA_BYPASS_SIGNATURE'] ?? 'false') === 'true';
|
||||
|
||||
if (!$secret || !$callbackUrl) {
|
||||
http_response_code(500);
|
||||
exit('Server misconfigured');
|
||||
}
|
||||
|
||||
progress_log('--- iKhokha WEBHOOK DEBUG ---');
|
||||
progress_log('RAW BODY: ' . $raw);
|
||||
progress_log('IK-SIGN: ' . $ikSign);
|
||||
|
||||
// Decode body so we can remove `text`
|
||||
$bodyArray = json_decode($raw, true);
|
||||
if (!is_array($bodyArray)) {
|
||||
http_response_code(400);
|
||||
exit('Invalid JSON');
|
||||
}
|
||||
|
||||
// iKhokha JS deletes `text`
|
||||
unset($bodyArray['text']);
|
||||
|
||||
// JS-style JSON (no escaped slashes)
|
||||
$jsonBody = json_encode($bodyArray, JSON_UNESCAPED_SLASHES);
|
||||
|
||||
|
||||
// Now sign the SAME payload JS signs
|
||||
$payloadToSign = createPayloadToSign($callbackUrl, $jsonBody);
|
||||
|
||||
$expected = generateSignature($payloadToSign, $secret);
|
||||
|
||||
progress_log('JS PAYLOAD: ' . $payloadToSign);
|
||||
progress_log('EXPECTED SIGN: ' . $expected);
|
||||
progress_log('RECEIVED SIGN: ' . $ikSign);
|
||||
|
||||
if (!$bypass) {
|
||||
|
||||
if (!hash_equals($expected, $ikSign)) {
|
||||
http_response_code(403);
|
||||
progress_log('iKhokha webhook: signature mismatch');
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog(null, 'IKHOKHA_SIGNATURE_MISMATCH', 'webhook', null, [
|
||||
'expected' => $expected,
|
||||
'received' => $ikSign
|
||||
]);
|
||||
}
|
||||
exit('Invalid signature');
|
||||
}
|
||||
|
||||
} else {
|
||||
progress_log('⚠️ IKHOKHA SIGNATURE CHECK BYPASSED');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Decode payload
|
||||
* ==========================================================
|
||||
*/
|
||||
$payload = json_decode($raw, true);
|
||||
$data = $payload['data'] ?? $payload;
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Extract fields safely
|
||||
* ==========================================================
|
||||
*/
|
||||
$externalTransactionID =
|
||||
$data['externalTransactionID']
|
||||
?? $data['externalTransactionId']
|
||||
?? $data['externalTxId']
|
||||
?? null;
|
||||
|
||||
$providerPaymentId =
|
||||
$data['paylinkID']
|
||||
?? $data['id']
|
||||
?? null;
|
||||
|
||||
$providerStatus =
|
||||
$data['status']
|
||||
?? $payload['status']
|
||||
?? null;
|
||||
|
||||
progress_log('Parsed externalTransactionID: ' . $externalTransactionID);
|
||||
progress_log('Parsed providerPaymentId: ' . $providerPaymentId);
|
||||
progress_log('Parsed providerStatus: ' . $providerStatus);
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Locate payment
|
||||
* ==========================================================
|
||||
*/
|
||||
$localPaymentId = null;
|
||||
$booking_id = null;
|
||||
$user_id = null;
|
||||
$description = null;
|
||||
|
||||
if ($externalTransactionID) {
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT payment_id, user_id, booking_id, description
|
||||
FROM payments
|
||||
WHERE payment_id = ?
|
||||
LIMIT 1"
|
||||
);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('s', $externalTransactionID);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
extract($row);
|
||||
$localPaymentId = $row['payment_id'];
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$localPaymentId && $providerPaymentId) {
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT payment_id, user_id, booking_id, description
|
||||
FROM payments
|
||||
WHERE provider_payment_id = ?
|
||||
LIMIT 1"
|
||||
);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('s', $providerPaymentId);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
extract($row);
|
||||
$localPaymentId = $row['payment_id'];
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$localPaymentId) {
|
||||
http_response_code(404);
|
||||
exit('Payment not found');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Persist provider response
|
||||
* ==========================================================
|
||||
*/
|
||||
$update = $conn->prepare(
|
||||
"UPDATE payments
|
||||
SET provider_payment_id = ?,
|
||||
provider_status = ?,
|
||||
provider_response = ?
|
||||
WHERE payment_id = ?"
|
||||
);
|
||||
|
||||
if ($update) {
|
||||
$update->bind_param(
|
||||
'ssss',
|
||||
$providerPaymentId,
|
||||
$providerStatus,
|
||||
$raw,
|
||||
$localPaymentId
|
||||
);
|
||||
$update->execute();
|
||||
$update->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Business logic
|
||||
* ==========================================================
|
||||
*/
|
||||
$normalized = strtoupper(trim((string)$providerStatus));
|
||||
|
||||
if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) {
|
||||
|
||||
$conn->prepare(
|
||||
"UPDATE payments SET status = 'PAID' WHERE payment_id = ?"
|
||||
)->bind_param('s', $localPaymentId)->execute();
|
||||
|
||||
if ($booking_id) {
|
||||
$conn->prepare(
|
||||
"UPDATE bookings SET status = 'PAID' WHERE booking_id = ?"
|
||||
)->bind_param('i', $booking_id)->execute();
|
||||
} else {
|
||||
$conn->prepare(
|
||||
"UPDATE membership_fees SET payment_status = 'PAID' WHERE payment_id = ?"
|
||||
)->bind_param('s', $localPaymentId)->execute();
|
||||
}
|
||||
|
||||
sendPaymentConfirmation(
|
||||
getEmail($user_id),
|
||||
getFullName($user_id),
|
||||
$description
|
||||
);
|
||||
|
||||
//generate $message for admin payment confirmation with payment details
|
||||
$message = "Payment Confirmation\n\n";
|
||||
$message .= "Payment ID: " . $localPaymentId . "\n";
|
||||
$message .= "Amount: " . getPaymentAmount($localPaymentId) . "\n";
|
||||
$message .= "Status: PAID\n";
|
||||
$message .= "Description: " . $description . "\n";
|
||||
$message .= "Thank you.\n";
|
||||
$subject = "4WDCSA.co.za Payment Confirmation for Payment ID: " . $localPaymentId;
|
||||
progress_log('Payment confirmation sent for payment ID: ' . $localPaymentId);
|
||||
|
||||
sendEmail(
|
||||
$_ENV['FINANCE_EMAIL'],
|
||||
$subject,
|
||||
nl2br($message)
|
||||
);
|
||||
sendEmail(
|
||||
'chrispintoza@gmail.com',
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Acknowledge webhook
|
||||
* ==========================================================
|
||||
*/
|
||||
http_response_code(200);
|
||||
echo 'OK';
|
||||
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']);
|
||||
10
src/api/test_log.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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");
|
||||
|
||||
echo "Test log entry attempt...";
|
||||
progress_log('Testing Log Entry at ' . date('Y-m-d H:i:s'));
|
||||
echo "Test log entry created.";
|
||||
46
src/api/track/get_obstable.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
// /api/track/get_obstacle.php
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
|
||||
// Read JSON POST body
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? '';
|
||||
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo "<h3>Error</h3><p>Invalid obstacle id.</p>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// TODO: Replace this with DB lookup (mysqli) by id.
|
||||
// For demo return stubbed content:
|
||||
$fake = [
|
||||
'obst-camp' => [
|
||||
'title' => 'Base Camp',
|
||||
'img' => '/assets/images/camp.jpg',
|
||||
'difficulty' => 'easy',
|
||||
'desc' => 'Flat campsite with shade and water point.'
|
||||
],
|
||||
'obst-water' => [
|
||||
'title' => 'Water Crossing',
|
||||
'img' => '/assets/images/water.jpg',
|
||||
'difficulty' => 'hard',
|
||||
'desc' => 'Deep crossing after heavy rain, check depth first.'
|
||||
]
|
||||
];
|
||||
|
||||
$data = $fake[$id] ?? null;
|
||||
|
||||
if (!$data) {
|
||||
http_response_code(404);
|
||||
echo "<h3>Not found</h3><p>No details for '{$id}'.</p>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// render HTML snippet for Magnific
|
||||
?>
|
||||
<img src="<?= htmlspecialchars($data['img']) ?>" alt="<?= htmlspecialchars($data['title']) ?>" style="width:100%; height:220px; object-fit:cover; border-radius:6px; margin-bottom:12px;">
|
||||
<h3><?= htmlspecialchars($data['title']) ?></h3>
|
||||
<span class="difficulty-badge <?= htmlspecialchars($data['difficulty']) ?>"><?= htmlspecialchars(ucfirst($data['difficulty'])) ?></span>
|
||||
<div class="description" style="margin-top:10px;"><?= htmlspecialchars($data['desc']) ?></div>
|
||||
<?php
|
||||
@@ -3,11 +3,12 @@
|
||||
// Disable mysqli exceptions so we can handle connection errors gracefully
|
||||
mysqli_report(MYSQLI_REPORT_OFF);
|
||||
|
||||
$dbhost = $_ENV['DB_HOST'];
|
||||
$dbuser = $_ENV['DB_USER'];
|
||||
$dbpass = $_ENV['DB_PASS'];
|
||||
$dbname = $_ENV['DB_NAME'];
|
||||
$salt = $_ENV['SALT'];
|
||||
// Read from environment or fallback to getenv; keep empty string if not set to avoid PHP warnings
|
||||
$dbhost = $_ENV['DB_HOST'] ?? getenv('DB_HOST') ?? '';
|
||||
$dbuser = $_ENV['DB_USER'] ?? getenv('DB_USER') ?? '';
|
||||
$dbpass = $_ENV['DB_PASS'] ?? getenv('DB_PASS') ?? '';
|
||||
$dbname = $_ENV['DB_NAME'] ?? getenv('DB_NAME') ?? '';
|
||||
$salt = $_ENV['SALT'] ?? getenv('SALT') ?? '';
|
||||
|
||||
// echo "hello. ". $dbhost;
|
||||
|
||||
|
||||
@@ -29,6 +29,159 @@ function openDatabaseConnection()
|
||||
return $conn;
|
||||
}
|
||||
|
||||
//function to determine whether membership_end_date is within 3 months from current date where user_id = ?, if so return true, else false
|
||||
function isMembershipExpiringSoon($user_id)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
if ($conn === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("SELECT membership_end_date FROM membership_fees WHERE user_id = ? LIMIT 1");
|
||||
if (!$stmt) {
|
||||
$conn->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($result->num_rows === 0) {
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = $result->fetch_assoc();
|
||||
$membership_end_date = new DateTime($row['membership_end_date']);
|
||||
$current_date = new DateTime();
|
||||
$interval = $current_date->diff($membership_end_date);
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
|
||||
return ($interval->days <= 90 && $membership_end_date > $current_date);
|
||||
}
|
||||
|
||||
function normalizeName($name) {
|
||||
$name = strtolower($name);
|
||||
$name = preg_replace("/[^a-z\s]/", "", $name); // remove punctuation
|
||||
$name = preg_replace("/\s+/", " ", $name); // normalize spaces
|
||||
return trim($name);
|
||||
}
|
||||
|
||||
// function to checkif first name + last name matches names in an array. names may have slight variations or spelling mistakes.
|
||||
function validateHonoraryMemberName($first_name, $last_name)
|
||||
{
|
||||
$honorary_names = [
|
||||
"robin hood",
|
||||
"marc rademaker",
|
||||
"clive robinson",
|
||||
"joern kuebler",
|
||||
"maurice compton",
|
||||
"jenny cole",
|
||||
"geoff joubert",
|
||||
"alan exton",
|
||||
"dave bell",
|
||||
"karl hoffman",
|
||||
"gerald obrian"
|
||||
// Add more honorary member names as needed
|
||||
];
|
||||
|
||||
$full_name = normalizeName($first_name . ' ' . $last_name);
|
||||
$full_name = strtolower($full_name);
|
||||
foreach ($honorary_names as $name) {
|
||||
similar_text($full_name, strtolower(normalizeName($name)), $percent);
|
||||
if ($percent >= 80) { // 80% similarity threshold
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//get membership_type from membership_applications table for user_id = ?
|
||||
function getMembershipType($user_id)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
if ($conn === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("SELECT membership_type FROM membership_applications WHERE user_id = ? ORDER BY id DESC LIMIT 1");
|
||||
if (!$stmt) {
|
||||
$conn->close();
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($membership_type);
|
||||
if ($stmt->fetch()) {
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
return $membership_type;
|
||||
} else {
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function progress_log($message, $context = null)
|
||||
{
|
||||
try {
|
||||
// Site root (same logic you already use elsewhere)
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
$logFile = $rootPath . '/progress.log';
|
||||
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
|
||||
// Normalize message
|
||||
if (is_array($message) || is_object($message)) {
|
||||
$message = json_encode($message, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
// Normalize context (optional extra data)
|
||||
if ($context !== null) {
|
||||
if (is_array($context) || is_object($context)) {
|
||||
$context = json_encode($context, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
}
|
||||
$message .= ' | CONTEXT: ' . $context;
|
||||
}
|
||||
|
||||
$line = "[{$timestamp}] {$message}" . PHP_EOL;
|
||||
|
||||
// Append atomically
|
||||
file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
// Never allow logging failures to break execution
|
||||
// Silent by design
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getPriceByDescription($description)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
$stmt = $conn->prepare("SELECT amount FROM prices WHERE description = ? LIMIT 1");
|
||||
if (!$stmt) {
|
||||
return null;
|
||||
}
|
||||
$stmt->bind_param("s", $description);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($amount);
|
||||
if ($stmt->fetch()) {
|
||||
$stmt->close();
|
||||
return $amount;
|
||||
} else {
|
||||
$stmt->close();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getTripCount()
|
||||
{
|
||||
// Database connection
|
||||
@@ -464,7 +617,7 @@ function getUserMemberStatus($user_id)
|
||||
}
|
||||
|
||||
// Step 3: Check membership fees table for valid payment status and membership_end_date
|
||||
$queryFees = "SELECT payment_status, membership_end_date FROM membership_fees WHERE user_id = ?";
|
||||
$queryFees = "SELECT payment_status, membership_end_date, renewal_period_end FROM membership_fees WHERE user_id = ?";
|
||||
$stmtFees = $conn->prepare($queryFees);
|
||||
if (!$stmtFees) {
|
||||
error_log("Failed to prepare fees query: " . $conn->error);
|
||||
@@ -487,6 +640,7 @@ function getUserMemberStatus($user_id)
|
||||
$fees = $resultFees->fetch_assoc();
|
||||
$payment_status = $fees['payment_status'];
|
||||
$membership_end_date = $fees['membership_end_date'];
|
||||
$renewal_period_end = $fees['renewal_period_end'];
|
||||
|
||||
// Validate payment status and membership_end_date
|
||||
$current_date = new DateTime();
|
||||
@@ -495,6 +649,12 @@ function getUserMemberStatus($user_id)
|
||||
if ($payment_status === "PAID" && $current_date <= $membership_end_date_obj) {
|
||||
$conn->close();
|
||||
return true; // Direct membership is active
|
||||
}elseif ($payment_status === "PENDING RENEWAL") {
|
||||
$renewal_period_end_obj = DateTime::createFromFormat('Y-m-d', $renewal_period_end);
|
||||
if ($current_date <= $renewal_period_end_obj) {
|
||||
$conn->close();
|
||||
return true; // Direct membership is in renewal period
|
||||
}
|
||||
} else {
|
||||
// Direct membership is not active, check if user is linked to another active membership
|
||||
error_log("Direct membership not active for user_id: $user_id - checking linked memberships");
|
||||
@@ -705,6 +865,173 @@ function processPayment($payment_id, $amount, $description)
|
||||
|
||||
}
|
||||
|
||||
function createIkhokhaPayment($payment_id, $amount, $description, $publicRef)
|
||||
{
|
||||
|
||||
// Base requester URL: prefer explicit env var, otherwise build from request
|
||||
$baseUrl = rtrim($_ENV['IKHOKHA_REQUESTER_URL'] ?? ($_SERVER['REQUEST_SCHEME'] ?? 'https') . '://' . ($_SERVER['HTTP_HOST'] ?? ''), '/');
|
||||
|
||||
$endpoint = $_ENV['IKHOKHA_ENDPOINT'];
|
||||
$appID = $_ENV['IKHOKHA_APP_ID'];
|
||||
$appSecret = $_ENV['IKHOKHA_APP_SECRET'];
|
||||
$requestBody = [
|
||||
"entityID" => $payment_id,
|
||||
"externalEntityID" => $payment_id,
|
||||
"amount" => $amount * 100,
|
||||
"currency" => "ZAR",
|
||||
"requesterUrl" => $_ENV['IKHOKHA_REQUESTER_URL'] ?? $baseUrl,
|
||||
"description" => $description,
|
||||
"paymentReference" => $description,
|
||||
"mode" => $_ENV['IKHOKHA_MODE'] ?? 'live',
|
||||
"externalTransactionID" => $payment_id,
|
||||
"urls" => [
|
||||
"callbackUrl" => $_ENV['IKHOKHA_CALLBACK_URL'],
|
||||
|
||||
"successPageUrl" => $_ENV['IKHOKHA_SUCCESS_URL']
|
||||
. "?ref=" . urlencode($publicRef),
|
||||
|
||||
"failurePageUrl" => $_ENV['IKHOKHA_FAILURE_URL']
|
||||
. "?ref=" . urlencode($publicRef),
|
||||
|
||||
"cancelUrl" => $_ENV['IKHOKHA_CANCEL_URL']
|
||||
. "?ref=" . urlencode($publicRef),
|
||||
]
|
||||
|
||||
];
|
||||
$stringifiedBody = json_encode($requestBody);
|
||||
$payloadToSign = createPayloadToSign($endpoint, $stringifiedBody);
|
||||
$ikSign = generateSignature($payloadToSign, $appSecret);
|
||||
// Initialize cURL session
|
||||
$ch = curl_init($endpoint);
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $stringifiedBody);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Content-Type: application/json",
|
||||
"IK-APPID: $appID",
|
||||
"IK-SIGN: $ikSign"
|
||||
]);
|
||||
// Execute cURL session
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
// Decode and output the response
|
||||
$resp = json_decode($response, true);
|
||||
|
||||
// Persist provider metadata into payments table if we have a response
|
||||
$conn = openDatabaseConnection();
|
||||
if ($conn === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$provider = 'ikhokha';
|
||||
$provider_payment_id = $resp['paylinkID'] ?? $resp['paylinkId'] ?? $resp['paylink_id'] ?? null;
|
||||
$payment_link = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
|
||||
$provider_status = $resp['responseCode'] ?? ($resp['status'] ?? null);
|
||||
$provider_response = json_encode($resp);
|
||||
|
||||
// Update payments row with provider info. If a paylink was created (responseCode == '00'), keep status awaiting payment.
|
||||
$newStatus = null;
|
||||
if (!empty($payment_link) && ($provider_status === '00' || $provider_status === '0' || $provider_status === 0)) {
|
||||
$newStatus = 'AWAITING PAYMENT';
|
||||
}
|
||||
|
||||
if ($newStatus) {
|
||||
$stmt = $conn->prepare("UPDATE payments SET provider = ?, provider_payment_id = ?, payment_link = ?, provider_status = ?, provider_response = ?, status = ? WHERE payment_id = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('sssssss', $provider, $provider_payment_id, $payment_link, $provider_status, $provider_response, $newStatus, $payment_id);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
} else {
|
||||
$stmt = $conn->prepare("UPDATE payments SET provider = ?, provider_payment_id = ?, payment_link = ?, provider_status = ?, provider_response = ? WHERE payment_id = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('ssssss', $provider, $provider_payment_id, $payment_link, $provider_status, $provider_response, $payment_id);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
|
||||
return $resp;
|
||||
}
|
||||
|
||||
function getIkhokhaTransactionHistory($startDate, $endDate,)
|
||||
{
|
||||
|
||||
// Base requester URL: prefer explicit env var, otherwise build from request
|
||||
$endpoint = "https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=".$startDate."&endDate=".$endDate;
|
||||
// $endpoint = "https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2024-02-01&endDate=2026-03-07";
|
||||
$appID = $_ENV['IKHOKHA_APP_ID'];
|
||||
progress_log($appID, "IKHOKHA App ID");
|
||||
$appSecret = $_ENV['IKHOKHA_APP_SECRET'];
|
||||
|
||||
// $stringifiedBody = json_encode($requestBody);
|
||||
$payloadToSign = createPayloadToSign($endpoint, null);
|
||||
progress_log($payloadToSign, "IKHOKHA Payload to Sign");
|
||||
|
||||
$ikSign = generateSignature($payloadToSign, $appSecret);
|
||||
progress_log($ikSign, "IKHOKHA Signature");
|
||||
|
||||
// Initialize cURL session
|
||||
$ch = curl_init($endpoint);
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
|
||||
// curl_setopt($ch, CURLOPT_POSTFIELDS, $stringifiedBody);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Content-Type: application/json",
|
||||
"IK-APPID: $appID",
|
||||
"IK-SIGN: $ikSign"
|
||||
]);
|
||||
// Execute cURL session
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
// Decode and output the response
|
||||
$resp = json_decode($response, true);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
function escapeString($str) {
|
||||
$escaped = preg_replace(['/[\\"\'\"]/u', '/\x00/'], ['\\\\$0', '\\0'], (string)$str);
|
||||
$cleaned = str_replace('\/', '/', $escaped);
|
||||
return $cleaned;
|
||||
}
|
||||
|
||||
function createPayloadToSign($urlPath, $body) {
|
||||
$parsedUrl = parse_url($urlPath);
|
||||
$basePath = $parsedUrl['path'];
|
||||
if (!$basePath) {
|
||||
throw new Exception("No path present in the URL");
|
||||
}
|
||||
$payload = $basePath . $body;
|
||||
$escapedPayloadString = escapeString($payload);
|
||||
return $escapedPayloadString;
|
||||
}
|
||||
|
||||
function generateSignature($payloadToSign, $secret) {
|
||||
return hash_hmac('sha256', $payloadToSign, $secret);
|
||||
}
|
||||
|
||||
function getPaymentAmount($localPaymentId) {
|
||||
$conn = openDatabaseConnection();
|
||||
$stmt = $conn->prepare("SELECT amount FROM payments WHERE payment_id = ? LIMIT 1");
|
||||
$stmt->bind_param("s", $localPaymentId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($row = $result->fetch_assoc()) {
|
||||
return $row['amount'];
|
||||
} else {
|
||||
return false; // Payment not found
|
||||
}
|
||||
}
|
||||
|
||||
function processMembershipPayment($payment_id, $amount, $description)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
@@ -1293,6 +1620,89 @@ function getInitialSurname($user_id)
|
||||
}
|
||||
}
|
||||
|
||||
function generatePaymentRef(string $type, ?int $course_trip_id, int $user_id): string
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
|
||||
// 1. Normalize type
|
||||
$type = strtoupper($type);
|
||||
|
||||
// 2. Build prefix
|
||||
switch ($type) {
|
||||
case 'SUBS':
|
||||
$year = (int)date('Y');
|
||||
$month = (int)date('n');
|
||||
|
||||
// If December, subscriptions are for next year
|
||||
if ($month === 12) {
|
||||
$year++;
|
||||
}
|
||||
|
||||
$prefix = "SUBS_" . $year;
|
||||
break;
|
||||
|
||||
case 'COURSE':
|
||||
if (!$course_trip_id) {
|
||||
throw new Exception("course_trip_id is required for COURSE payments");
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT code FROM courses WHERE course_id = ?"
|
||||
);
|
||||
$stmt->bind_param("i", $course_trip_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($code);
|
||||
|
||||
if (!$stmt->fetch()) {
|
||||
throw new Exception("Invalid course_id: {$course_trip_id}");
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$prefix = "COURSE_" . strtoupper($code);
|
||||
break;
|
||||
|
||||
case 'TRIP':
|
||||
if (!$course_trip_id) {
|
||||
throw new Exception("course_trip_id is required for TRIP payments");
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT trip_code FROM trips WHERE trip_id = ?"
|
||||
);
|
||||
$stmt->bind_param("i", $course_trip_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($trip_code);
|
||||
|
||||
if (!$stmt->fetch()) {
|
||||
throw new Exception("Invalid trip_id: {$course_trip_id}");
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$prefix = "TRIP_" . strtoupper($trip_code);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception("Unknown payment type: {$type}");
|
||||
}
|
||||
|
||||
// 3. Get user initials + surname
|
||||
$namePart = strtoupper(getInitialSurname($user_id));
|
||||
|
||||
if (!$namePart) {
|
||||
throw new Exception("User not found for user_id: {$user_id}");
|
||||
}
|
||||
|
||||
// 4. Add short entropy (trimmed for aesthetics)
|
||||
$entropy = substr(shortEntropy(), -3);
|
||||
|
||||
return "{$prefix}_{$namePart}_{$entropy}";
|
||||
}
|
||||
|
||||
function shortEntropy(): string {
|
||||
return strtoupper(base_convert((string)(microtime(true) * 1000), 10, 36));
|
||||
}
|
||||
|
||||
|
||||
function getLastName($user_id)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
@@ -1719,12 +2129,25 @@ function formatCurrency($amount, $currency = 'R')
|
||||
|
||||
function guessCountry($ip)
|
||||
{
|
||||
$response = file_get_contents("http://ip-api.com/json/$ip");
|
||||
// Use cURL instead of file_get_contents for compatibility with allow_url_fopen=0
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, "http://ip-api.com/json/$ip");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if ($data['status'] == 'success') {
|
||||
if ($data && isset($data['status']) && $data['status'] == 'success') {
|
||||
return $data['country']; // e.g., South Africa
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getUserIdFromEFT($eft_id)
|
||||
@@ -1951,6 +2374,8 @@ function processLegacyMembership($user_id) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* SECURITY WARNING: This function uses dynamic table/column names which makes it vulnerable to SQL injection.
|
||||
* ONLY call this function with whitelisted table and column names.
|
||||
@@ -2436,12 +2861,13 @@ function validateFileUpload($file, $fileType = 'document') {
|
||||
}
|
||||
|
||||
// ===== CHECK 5: MIME Type Validation =====
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
if ($finfo === false) {
|
||||
error_log("Failed to open fileinfo resource");
|
||||
return false;
|
||||
}
|
||||
// Skip MIME type validation if finfo_open is not available (shared hosting compatibility)
|
||||
// Extension validation in CHECK 4 provides sufficient security
|
||||
$mimeType = 'application/octet-stream'; // Default fallback
|
||||
|
||||
if (function_exists('finfo_open')) {
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
if ($finfo !== false) {
|
||||
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
@@ -2449,6 +2875,8 @@ function validateFileUpload($file, $fileType = 'document') {
|
||||
error_log("Invalid MIME type '$mimeType' for type: $fileType. Expected: " . implode(', ', $config['mimeTypes']));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CHECK 6: Additional Image Validation (for images) =====
|
||||
if (in_array($fileType, ['profile_picture'])) {
|
||||
@@ -3189,3 +3617,73 @@ function unlinkSecondaryUser($link_id, $primary_user_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the payment_link for a given internal payment_id from the payments table.
|
||||
* Returns the payment_link string on success or null if not found / on error.
|
||||
*
|
||||
* @param string $payment_id
|
||||
* @return string|null
|
||||
*/
|
||||
function getPaymentLinkByPaymentId($payment_id)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
if ($conn === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("SELECT payment_link FROM payments WHERE payment_id = ? LIMIT 1");
|
||||
if (!$stmt) {
|
||||
$conn->close();
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt->bind_param('s', $payment_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($payment_link);
|
||||
$found = $stmt->fetch();
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
|
||||
if ($found) {
|
||||
return $payment_link;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the membership_end_date for a given user_id from membership_fees.
|
||||
* Returns the date string (Y-m-d) or null if not found.
|
||||
*
|
||||
* @param int $user_id
|
||||
* @return string|null
|
||||
*/
|
||||
function getMembershipEndDate($user_id)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
if ($conn === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("SELECT membership_end_date FROM membership_fees WHERE user_id = ? LIMIT 1");
|
||||
if (!$stmt) {
|
||||
$conn->close();
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($membership_end_date);
|
||||
$found = $stmt->fetch();
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
|
||||
if ($found) {
|
||||
return $membership_end_date;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
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 [];
|
||||
}
|
||||
1
src/logs/db_errors.log
Normal file
@@ -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'); ?>
|
||||
@@ -100,6 +100,7 @@ include_once($rootPath . '/header.php');
|
||||
|
||||
// Output the HTML structure with dynamic data
|
||||
echo '
|
||||
<a href="' . $blog_link . '" style="text-decoration: none; color: inherit;">
|
||||
<div class="blog-item style-three" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image" style="border-radius:20px; width:300px;height: 250px;margin-right:0px;">
|
||||
<img src="' . htmlspecialchars($blog_image) . '" alt="' . htmlspecialchars($post["title"]) . '">
|
||||
@@ -117,6 +118,7 @@ include_once($rootPath . '/header.php');
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
';
|
||||
endwhile;
|
||||
|
||||
@@ -192,12 +192,15 @@ $stmt->close();
|
||||
document.getElementById("autosave-status").innerText = "Draft autosaved at " + new Date().toLocaleTimeString();
|
||||
return true;
|
||||
} else {
|
||||
document.getElementById("autosave-status").innerText = "Autosave failed";
|
||||
console.error("Autosave failed", response.statusText);
|
||||
return response.text().then(errorText => {
|
||||
document.getElementById("autosave-status").innerText = "Autosave failed: " + errorText;
|
||||
console.error("Autosave failed", response.status, errorText);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error("Autosave error:", err);
|
||||
document.getElementById("autosave-status").innerText = "Autosave error: " + err.message;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,19 @@ require_once($rootPath . "/header.php");
|
||||
|
||||
checkUserSession();
|
||||
|
||||
// Check if user has active membership
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$is_member = getUserMemberStatus($_SESSION['user_id']);
|
||||
if (!$is_member) {
|
||||
$_SESSION['message'] = "My Blog Posts is only available to active members. Please contact info@4wdcsa.co.za for more information.";
|
||||
header('Location: membership_details');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pageTitle = 'My Blog Posts';
|
||||
$breadcrumbs = [['Home' => 'index'], ['Blog' => 'blog']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
@@ -46,32 +59,27 @@ $posts = $result->get_result();
|
||||
}
|
||||
</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>My Posts</h2>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
||||
<h2 style="margin: 0;">My Blog Posts</h2>
|
||||
<a href="blog_create" class="theme-btn create-album-btn">
|
||||
<i class="far fa-plus"></i> Create New Post
|
||||
</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']); ?>
|
||||
<?php endif; ?>
|
||||
<a href="blog_create.php">+ New Post</a>
|
||||
<?php unset($_SESSION['message']);
|
||||
endif;
|
||||
|
||||
<?php while ($post = $posts->fetch_assoc()):
|
||||
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
|
||||
|
||||
@@ -114,6 +114,7 @@ $user_id = $_SESSION['user_id'];
|
||||
// Loop through each row
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$booking_id = $row['booking_id'];
|
||||
$payment_id = $row['payment_id'];
|
||||
$booking_type = $row['booking_type'];
|
||||
$from_date = $row['from_date'];
|
||||
$to_date = $row['to_date'];
|
||||
@@ -267,8 +268,8 @@ $user_id = $_SESSION['user_id'];
|
||||
<div class="destination-footer">
|
||||
<span class="price"><span>Booking Total: R ' . number_format($amount, 2) . '</span></span>';
|
||||
if ($status == "AWAITING PAYMENT") {
|
||||
echo '<a href="' . url('payment_confirmation') . '?token=' . encryptData($booking_id, $salt) . '" class="theme-btn style-two style-three">
|
||||
<span data-hover="PAYMENT INFO">' . $status . '</span>
|
||||
echo '<a href="' . getPaymentLinkByPaymentId($payment_id) . '" class="theme-btn style-two style-three">
|
||||
<span data-hover="PAY NOW">' . $status . '</span>
|
||||
</a>';
|
||||
} else {
|
||||
echo '<a href="" class="theme-btn style-two style-three">
|
||||
|
||||
@@ -177,7 +177,7 @@ $page_id = 'driver_training';
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
|
||||
<?php
|
||||
$button_text = "Book Now";
|
||||
$button_text = "PROCEED TO PAYMENT";
|
||||
$button_disabled = "";
|
||||
if (!$result || $result->num_rows == 0) {
|
||||
$button_text = "No booking dates available";
|
||||
@@ -189,8 +189,9 @@ $page_id = 'driver_training';
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
<div class="text-center">
|
||||
<a href="contact">Need some help?</a>
|
||||
<a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
|
||||
</div>
|
||||
<img src="assets/images/logos/ikhokha.png"alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -594,13 +594,14 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5">
|
||||
<span data-hover="Book Now">Book Now</span>
|
||||
<span data-hover="PROCEED TO PAYMENT">PROCEED TO PAYMENT</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<div class="text-center">
|
||||
<a href="contact">Need some help?</a>
|
||||
<a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
|
||||
</div>
|
||||
<img src="assets/images/logos/ikhokha.png" alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -70,24 +70,7 @@ include_once($rootPath . '/header.php');
|
||||
<div class="row">
|
||||
|
||||
<div class="col-lg-12">
|
||||
<div class="shop-shorter rel z-3 mb-20">
|
||||
<!-- <ul class="grid-list mb-15 me-2">
|
||||
<li><a href="#"><i class="fal fa-border-all"></i></a></li>
|
||||
<li><a href="#"><i class="far fa-list"></i></a></li>
|
||||
</ul>
|
||||
<div class="sort-text mb-15 me-4 me-xl-auto">
|
||||
</div> -->
|
||||
<div class="sort-text mb-15 me-4">
|
||||
Sort By
|
||||
</div>
|
||||
<select>
|
||||
<option value="default" selected="">Sort By</option>
|
||||
<option value="new">Newness</option>
|
||||
<option value="old">Oldest</option>
|
||||
<option value="hight-to-low">High To Low</option>
|
||||
<option value="low-to-high">Low To High</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<?php
|
||||
// Query to retrieve upcoming published events only
|
||||
|
||||
@@ -58,20 +58,13 @@ $conn->close();
|
||||
}
|
||||
|
||||
.album-card {
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.album-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.album-image-wrapper {
|
||||
@@ -86,11 +79,6 @@ $conn->close();
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.album-card:hover .album-image-wrapper img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.album-image-wrapper .no-image {
|
||||
@@ -163,49 +151,21 @@ $conn->close();
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.album-view-btn {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
.album-edit-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
display: block;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
font-size: 1.2rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.album-view-btn:hover {
|
||||
background: #764ba2;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.album-edit-btn {
|
||||
padding: 8px 12px;
|
||||
background: white;
|
||||
.album-edit-icon:hover {
|
||||
color: #667eea;
|
||||
border: 1px solid #667eea;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.album-edit-btn:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.create-album-btn {
|
||||
@@ -289,11 +249,11 @@ require_once($rootPath . '/components/banner.php');
|
||||
</div>
|
||||
|
||||
<div class="album-actions">
|
||||
<a href="view_album?id=<?php echo $album['album_id']; ?>" class="album-view-btn">
|
||||
<a href="view_album?id=<?php echo $album['album_id']; ?>" class="theme-btn" style="width: 100%;">
|
||||
View
|
||||
</a>
|
||||
<?php if ($album['user_id'] == $current_user_id): ?>
|
||||
<a href="edit_album?id=<?php echo $album['album_id']; ?>" class="album-edit-btn">
|
||||
<a href="edit_album?id=<?php echo $album['album_id']; ?>" class="album-edit-icon" title="Edit">
|
||||
<i class="far fa-edit"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -39,7 +39,8 @@ if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) {
|
||||
<li>... and many more!</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h2>R 2,500/year</h2>
|
||||
<?php $annualFee = getPriceByDescription('membership_fees'); ?>
|
||||
<h2>R <?php echo number_format($annualFee, 0); ?>/year</h2>
|
||||
<p>We go above and beyond to make your travel dreams reality hidden gems and must-see
|
||||
attractions</p>
|
||||
<a href="membership_application" class="theme-btn mt-10 style-two">
|
||||
|
||||
@@ -39,6 +39,31 @@ $user = $result->fetch_assoc();
|
||||
<div class="section-title">
|
||||
<div id="responseMessage"></div> <!-- Message display area -->
|
||||
</div>
|
||||
<!-- Membership Type -->
|
||||
<h3>Membership Type</h3>
|
||||
<div class="row mt-35">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="country_membership" name="country_membership" value="1">
|
||||
<label style="margin-left:20px;" for="country_membership">Country Membership - if you reside more than 150km from BASE4 and qualify for country membership.</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="radio" name="membership_type" id="membership_full" value="full" checked>
|
||||
<label style="margin-left:20px;" for="membership_full">Full Membership</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="radio" name="membership_type" id="membership_single" value="single">
|
||||
<label style="margin-left:20px;" for="membership_single">Single Membership</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Personal Details Section -->
|
||||
<h3>Main Member</h3>
|
||||
<div class="row mt-35">
|
||||
@@ -88,6 +113,7 @@ $user = $result->fetch_assoc();
|
||||
</div>
|
||||
|
||||
<!-- Spouse / Partner Details Section -->
|
||||
<div id="spouseSection">
|
||||
<h3>Spouse / Life Partner / Other Details</h3>
|
||||
<div class="row mt-35">
|
||||
<div class="col-md-6">
|
||||
@@ -135,7 +161,11 @@ $user = $result->fetch_assoc();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- end spouse row -->
|
||||
<!-- </div> end spouseSection -->
|
||||
|
||||
<!-- Children Section -->
|
||||
<div id="childrenSection">
|
||||
<h3>Children's Names</h3>
|
||||
<div class="row mt-35">
|
||||
<div class="col-md-6">
|
||||
@@ -176,6 +206,7 @@ $user = $result->fetch_assoc();
|
||||
</div>
|
||||
<!-- Repeat for other children if needed -->
|
||||
</div>
|
||||
</div> <!-- end childrenSection -->
|
||||
|
||||
<!-- Address Section -->
|
||||
<h3>Address</h3>
|
||||
@@ -282,3 +313,43 @@ $user = $result->fetch_assoc();
|
||||
|
||||
|
||||
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>
|
||||
|
||||
<script>
|
||||
// Toggle spouse and children sections when 'Single Membership' is selected
|
||||
(function() {
|
||||
function setSectionState(isSingle) {
|
||||
var spouse = document.getElementById('spouseSection');
|
||||
var children = document.getElementById('childrenSection');
|
||||
[spouse, children].forEach(function(sec) {
|
||||
if (!sec) return;
|
||||
var inputs = sec.querySelectorAll('input, select, textarea, button');
|
||||
if (isSingle) {
|
||||
sec.style.display = 'none';
|
||||
inputs.forEach(function(i) {
|
||||
i.disabled = true;
|
||||
});
|
||||
} else {
|
||||
sec.style.display = '';
|
||||
inputs.forEach(function(i) {
|
||||
i.disabled = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var full = document.getElementById('membership_full');
|
||||
var single = document.getElementById('membership_single');
|
||||
|
||||
// initialize state
|
||||
setSectionState(single && single.checked);
|
||||
|
||||
if (full) full.addEventListener('change', function() {
|
||||
if (this.checked) setSectionState(false);
|
||||
});
|
||||
if (single) single.addEventListener('change', function() {
|
||||
if (this.checked) setSectionState(true);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -19,6 +19,8 @@ $result = $stmt->get_result();
|
||||
// Fetch single record
|
||||
$membership = $result->fetch_assoc();
|
||||
|
||||
$payment_link = getPaymentLinkByPaymentId($membership['payment_id']);
|
||||
|
||||
// Fetch membership application data using mysqli
|
||||
$query = "SELECT * FROM membership_application WHERE user_id = ?";
|
||||
$stmt = $conn->prepare($query);
|
||||
@@ -186,8 +188,8 @@ if (empty($application['id_number'])) {
|
||||
|
||||
<td><?php echo htmlspecialchars($membership['payment_amount']); ?></td>
|
||||
<td><?php echo htmlspecialchars($membership['payment_id']); ?></td>
|
||||
<?php if ($membership['payment_status'] == "PENDING") { ?>
|
||||
<td><a href='membership_payment' class='theme-btn style-two style-three' style='padding: 0px 14px;'><span data-hover='VIEW PAYMENT INFO'>AWAITING PAYMENT</span></a></td>
|
||||
<?php if ($membership['payment_status'] == "AWAITING PAYMENT" || $membership['payment_status'] == "PENDING RENEWAL") { ?>
|
||||
<td><a href='<?= $payment_link; ?>' class='theme-btn style-two style-three' style='padding: 0px 14px;'><span data-hover='<?= $membership['payment_status'] ?>'><?= $membership['payment_status'] ?></span></a></td>
|
||||
<?php } else { ?>
|
||||
<td><?php echo htmlspecialchars($membership['payment_status']); ?></td>
|
||||
<?php } ?>
|
||||
@@ -204,17 +206,27 @@ if (empty($application['id_number'])) {
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Check if membership has expired
|
||||
// Show renew button when current date is within 3 months of membership end
|
||||
$membership_end_date = $membership ? $membership['membership_end_date'] : null;
|
||||
$today = date('Y-m-d');
|
||||
|
||||
if ($membership_end_date && strtotime($today) > strtotime($membership_end_date)) {
|
||||
if ($membership_end_date) {
|
||||
try {
|
||||
$end = new DateTime($membership_end_date);
|
||||
$threeMonthsBefore = (clone $end)->modify('-3 months')->format('Y-m-d');
|
||||
} catch (Exception $e) {
|
||||
// Fallback using strtotime if DateTime parsing fails
|
||||
$threeMonthsBefore = date('Y-m-d', strtotime($membership_end_date . ' -3 months'));
|
||||
}
|
||||
|
||||
if (strtotime($today) >= strtotime($threeMonthsBefore)) {
|
||||
echo '
|
||||
<a href="renew_membership" class="theme-btn style-two bgc-secondary" style="width:100%; margin-top: 20px; background-color: #63ab45; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;">
|
||||
<a href="renewal_payment" class="theme-btn style-two bgc-secondary" style="width:100%; margin-top: 20px; background-color: #63ab45; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;">
|
||||
<span data-hover="Renew Membership">Renew Membership</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
|
||||
|
||||
@@ -67,7 +67,33 @@ $stmt->bind_result($user_email);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
$conn->close();
|
||||
// If request includes payment_id, fetch provider paylink from payments table
|
||||
if (!isset($_GET['token']) || empty($_GET['token'])) {
|
||||
header("Location: membership_details");
|
||||
exit();
|
||||
}
|
||||
$token = $_GET['token'];
|
||||
// echo $token;
|
||||
|
||||
// Sanitize the trip_id to prevent SQL injection
|
||||
$payment_id = decryptData($token, $_ENV['SALT']);
|
||||
$payment_link = null;
|
||||
if ($payment_id) {
|
||||
$pstmt = $conn->prepare("SELECT payment_link, amount, status, provider FROM payments WHERE payment_id = ? LIMIT 1");
|
||||
if ($pstmt) {
|
||||
$pstmt->bind_param('s', $payment_id);
|
||||
$pstmt->execute();
|
||||
$pres = $pstmt->get_result();
|
||||
if ($prow = $pres->fetch_assoc()) {
|
||||
$payment_link = $prow['payment_link'];
|
||||
// prefer payments.amount if present
|
||||
if (!empty($prow['amount'])) {
|
||||
$payment_amount = $prow['amount'];
|
||||
}
|
||||
}
|
||||
$pstmt->close();
|
||||
}
|
||||
}
|
||||
?><?php
|
||||
$pageTitle = 'Membership Payment';
|
||||
$breadcrumbs = [['Home' => 'index.php'], ['Membership' => 'membership.php']];
|
||||
@@ -83,13 +109,28 @@ $conn->close();
|
||||
<?php echo
|
||||
'<h5>Membership Start Date: ' . $membership_start_date . '<br>Membership Renewal Date: ' . $membership_end_date . '</h5>'; ?>
|
||||
</div>
|
||||
<p>Your invoice has been sent to <b><?php echo htmlspecialchars($user_email); ?></b>. Please upload your proof of payment below.</p>
|
||||
|
||||
<?php if (!empty($payment_link)) { ?>
|
||||
<h5>Payment Details:</h5>
|
||||
<p>Amount: R <?php echo number_format($payment_amount, 2); ?></p>
|
||||
<p>Reference: <?php echo htmlspecialchars($payment_id); ?></p>
|
||||
<a href="<?php echo htmlspecialchars($payment_link); ?>" class="theme-btn style-two style-three" style="width:100%;" target="_blank" rel="noopener noreferrer">
|
||||
<span data-hover="Pay Now with iKhokha">Pay Now with iKhokha</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
<div class="text-center">
|
||||
<p>You will be redirected to iKhokha's Secure payment gateway.</p>
|
||||
</div>
|
||||
<img src="assets/images/logos/ikhokha.png" alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
|
||||
<?php } else { ?>
|
||||
<p>Please upload your proof of payment below.</p>
|
||||
<h5>Payment Details:</h5>
|
||||
<p>The Four Wheel Drive Club of Southern Africa<br>FNB<br>Account Number: 58810022334<br>Branch code: 250655<br>Reference: <?php echo htmlspecialchars($eft_id); ?><br>Amount: R <?php echo number_format($payment_amount, 2); ?></p>
|
||||
<a href="submit_pop" class="theme-btn style-two style-three" style="width:100%;">
|
||||
<span data-hover="Submit Proof of Payment">Submit Proof of Payment</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
|
||||
@@ -1,26 +1,162 @@
|
||||
<?php
|
||||
require_once("env.php");
|
||||
require_once("session.php");
|
||||
require_once("connection.php");
|
||||
require_once("functions.php");
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||
include_once($rootPath . '/header.php');
|
||||
|
||||
$is_logged_in = isset($_SESSION['user_id']);
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
$eft_id = strtoupper("SUBS " . date("Y") . " " . getLastName($user_id));
|
||||
$status = 'AWAITING PAYMENT';
|
||||
$description = 'Membership Fees ' . date("Y") . " " . getLastName($user_id);
|
||||
|
||||
$payment_amount = 2600; // Assuming a fixed membership fee, adjust as needed
|
||||
} else {
|
||||
header('Location: login.php');
|
||||
exit(); // Stop further script execution
|
||||
}
|
||||
|
||||
$full_name = getFullName($user_id);
|
||||
|
||||
if (isset($_POST['membership_type'])) {
|
||||
$membership_type = $_POST['membership_type'];
|
||||
echo $membership_type;
|
||||
//update membership_type in membership_application
|
||||
$stmt = $conn->prepare("UPDATE membership_application SET membership_type = ? WHERE user_id = ? ");
|
||||
$stmt->bind_param("si", $membership_type, $user_id);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
} else {
|
||||
//get user membership type from membership_applications
|
||||
$stmt = $conn->prepare("SELECT membership_type FROM membership_application WHERE user_id = ? ");
|
||||
$stmt->bind_param("i", $user_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($membership_type);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
//check memberhsip_applications for user_id, if 0 rows, redirect to membership_application.php
|
||||
$stmt = $conn->prepare("SELECT COUNT(*) AS cnt FROM membership_application WHERE user_id = ? LIMIT 1");
|
||||
$stmt->bind_param("i", $user_id);
|
||||
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($application_count);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
if ($application_count == 0) {
|
||||
header("Location: membership_application.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
//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->bind_param("i", $user_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($payment_status);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
if ($payment_status === 'PENDING RENEWAL') {
|
||||
header("Location: membership_details.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if ($membership_type === 'country') {
|
||||
$payment_amount = getPriceByDescription('country_membership');
|
||||
} elseif ($membership_type === 'single') {
|
||||
$payment_amount = getPriceByDescription('single');
|
||||
} else {
|
||||
$payment_amount = getPriceByDescription('membership_fees');
|
||||
}
|
||||
|
||||
if ($membership_type === 'honorary') {
|
||||
// Honorary members do not pay fees, redirect to membership details
|
||||
header("Location: membership_details.php");
|
||||
exit();
|
||||
|
||||
}
|
||||
|
||||
$payment_id = generatePaymentRef('SUBS', null, $user_id);
|
||||
$payment_date = date('Y-m-d');
|
||||
$membership_start_date = date('Y-01-01');
|
||||
$membership_end_date = date('Y-12-31');
|
||||
$renewal_period_end = getMembershipEndDate($user_id);
|
||||
// Hardcode membership start date to 2026-03-01 per request
|
||||
$renewed_membership_start_date = '2026-03-01';
|
||||
|
||||
$stmt = $conn->prepare("UPDATE membership_fees SET payment_amount = ?, payment_date = ?, membership_start_date = ?, membership_end_date = ?, payment_status = 'PENDING', payment_id = ? WHERE user_id = ?");
|
||||
$stmt->bind_param("dssssi", $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $eft_id, $user_id);
|
||||
// Set membership_end_date to the last day of February in the following year
|
||||
$renewed_membership_end_date = '2027-02-28';
|
||||
|
||||
$stmt = $conn->prepare("UPDATE membership_fees SET payment_amount = ?, payment_date = ?, membership_start_date = ?, membership_end_date = ?, renewal_period_end = ?, payment_status = 'PENDING RENEWAL', payment_id = ? WHERE user_id = ?");
|
||||
$stmt->bind_param("dsssssi", $payment_amount, $payment_date, $renewed_membership_start_date, $renewed_membership_end_date, $renewal_period_end, $payment_id, $user_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Commit the transaction
|
||||
$conn->commit();
|
||||
addSubsEFT($eft_id, $user_id, $status, $payment_amount, $description);
|
||||
|
||||
// Audit: user initiated membership renewal
|
||||
if (function_exists('auditLog')) {
|
||||
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");
|
||||
if ($checkP) {
|
||||
$checkP->bind_param('s', $payment_id);
|
||||
$checkP->execute();
|
||||
$r = $checkP->get_result()->fetch_assoc();
|
||||
$exists = intval($r['cnt']) > 0;
|
||||
$checkP->close();
|
||||
} else {
|
||||
$exists = false;
|
||||
}
|
||||
|
||||
if (!$exists) {
|
||||
$publicRef = bin2hex(random_bytes(16));
|
||||
// If current month is December, attribute the membership year to the next year
|
||||
$currentYear = intval(date('Y'));
|
||||
$month = intval(date('n'));
|
||||
if ($month === 12) {
|
||||
$membershipYear = $currentYear + 1;
|
||||
} else {
|
||||
$membershipYear = $currentYear;
|
||||
}
|
||||
$description = 'Membership Fees ' . $membershipYear . ' ' . getInitialSurname($user_id);
|
||||
$status = 'AWAITING PAYMENT';
|
||||
$ins = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, public_ref) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
if ($ins) {
|
||||
$ins->bind_param('sidsss', $payment_id, $user_id, $payment_amount, $status, $description, $publicRef);
|
||||
$ins->execute();
|
||||
$ins->close();
|
||||
}
|
||||
}
|
||||
|
||||
// Create iKhokha paylink via helper (functions.php)
|
||||
try {
|
||||
$publicRef = $publicRef ?? bin2hex(random_bytes(16));
|
||||
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
||||
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
|
||||
$token = encryptData($payment_id, $_ENV['SALT']);
|
||||
if ($paylink) {
|
||||
header('Location: membership_payment?token=' . $token);
|
||||
exit();
|
||||
} else {
|
||||
header("Location: membership_details");
|
||||
exit();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Log but do not fail signature save
|
||||
error_log('iKhokha create error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
|
||||
header("Location:membership_payment.php");
|
||||
// Success message
|
||||
$response = [
|
||||
|
||||
182
src/pages/memberships/renewal_payment.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||
include_once($rootPath . '/header.php');
|
||||
// Assuming you have the user ID stored in the session
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$user_id = $_SESSION['user_id'];
|
||||
} else {
|
||||
header('Location: login.php');
|
||||
exit(); // Stop further script execution
|
||||
}
|
||||
|
||||
// Initialize variables
|
||||
$payment_amount = null;
|
||||
$membership_start_date = null;
|
||||
$membership_end_date = null;
|
||||
|
||||
$continue_processing = isMembershipExpiringSoon($user_id);
|
||||
if (!$continue_processing) {
|
||||
header("Location: membership_details.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Determine current membership type (default) and available renewal prices
|
||||
$membership_type = getMembershipType($user_id);
|
||||
if ($membership_type === 'honorary') {
|
||||
// Honorary members do not renew
|
||||
header("Location: membership_details.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch prices for all types so we can show dynamic updates client-side
|
||||
$price_full = getPriceByDescription('membership_fees');
|
||||
$price_single = getPriceByDescription('single');
|
||||
$price_country = getPriceByDescription('country_membership');
|
||||
|
||||
// Set the initially displayed renewal amount based on current membership type
|
||||
switch ($membership_type) {
|
||||
case 'country':
|
||||
$current_renewal_amount = $price_country;
|
||||
break;
|
||||
case 'single':
|
||||
$current_renewal_amount = $price_single;
|
||||
break;
|
||||
default:
|
||||
$current_renewal_amount = $price_full;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Get the user_id from the session
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
|
||||
if ($user_id) {
|
||||
// Prepare the SQL query to fetch data
|
||||
$query = "SELECT payment_amount, membership_start_date, membership_end_date, payment_id
|
||||
FROM membership_fees
|
||||
WHERE user_id = ?";
|
||||
|
||||
if ($stmt = $conn->prepare($query)) {
|
||||
// Bind the user_id parameter to the query
|
||||
$stmt->bind_param("i", $user_id);
|
||||
|
||||
// Execute the query
|
||||
$stmt->execute();
|
||||
|
||||
// Bind the results to variables
|
||||
$stmt->bind_result($payment_amount, $membership_start_date, $membership_end_date, $eft_id);
|
||||
|
||||
// Fetch the data
|
||||
if ($stmt->fetch()) {
|
||||
// Values are now assigned to $payment_amount, $membership_start_date, and $membership_end_date
|
||||
} else {
|
||||
// Handle case where no records are found
|
||||
$error_message = "No records found for the given user ID.";
|
||||
}
|
||||
|
||||
// Close the statement
|
||||
$stmt->close();
|
||||
} else {
|
||||
// Handle query preparation failure
|
||||
$error_message = "Query preparation failed: " . $conn->error;
|
||||
}
|
||||
} else {
|
||||
// Handle case where user_id is not found in session
|
||||
$error_message = "User ID not found in session.";
|
||||
}
|
||||
?>
|
||||
|
||||
<?php
|
||||
$pageTitle = 'Membership Renewal';
|
||||
$breadcrumbs = [['Home' => 'index.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
<!-- Contact Form Area start -->
|
||||
<section class="about-us-area py-100 rpb-90 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="section-title mb-25">
|
||||
<span class="h2 mb-15">Membership Renewal:</span>
|
||||
<?php echo
|
||||
'<h5>Membership Expiration Date: ' . $membership_end_date . '</h5>'; ?>
|
||||
</div>
|
||||
|
||||
<h5>Renewal Amount:</h5>
|
||||
|
||||
<form method="post" action="renew_membership" id="renewForm">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="radio" name="membership_type" id="radio_full" value="full" <?php echo ($membership_type === 'full' || $membership_type === 'member' || $membership_type === null) ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="radio_full">Family Membership</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="membership_type" id="radio_single" value="single" <?php echo ($membership_type === 'single') ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="radio_single">Single Membership</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="membership_type" id="radio_country" value="country" <?php echo ($membership_type === 'country') ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="radio_country">Country Membership</label>
|
||||
<small>You need to reside more than 150km from BASE4 to qualify.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Amount:</h5>
|
||||
|
||||
<h2>R <span id="renewAmount"><?php echo number_format($current_renewal_amount, 2); ?></span></h2>
|
||||
|
||||
<button type="submit" class="theme-btn style-two style-three" style="width:100%;">
|
||||
<span data-hover="Renew Membership">Renew Membership</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
<div class="text-center mt-2">
|
||||
<p>You will be redirected to iKhokha's Secure payment gateway.</p>
|
||||
</div>
|
||||
<img src="assets/images/logos/ikhokha.png" alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// Prices from server
|
||||
var prices = {
|
||||
full: <?php echo json_encode((float)$price_full); ?>,
|
||||
single: <?php echo json_encode((float)$price_single); ?>,
|
||||
country: <?php echo json_encode((float)$price_country); ?>
|
||||
};
|
||||
|
||||
function updateAmount(type){
|
||||
var amt = prices[type] !== undefined ? prices[type] : prices.full;
|
||||
document.getElementById('renewAmount').textContent = amt.toFixed(2);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
var radios = document.querySelectorAll('input[name="membership_type"]');
|
||||
radios.forEach(function(r){
|
||||
r.addEventListener('change', function(){
|
||||
updateAmount(this.value);
|
||||
});
|
||||
});
|
||||
// initialize
|
||||
var checked = document.querySelector('input[name="membership_type"]:checked');
|
||||
if(checked) updateAmount(checked.value);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="about-us-image">
|
||||
<img src="assets/images/logos/weblogo.png" alt="About">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>
|
||||
@@ -159,13 +159,18 @@ require_once($rootPath . '/components/banner.php');
|
||||
<h2>4WDCSA Committee and Other Office Bearers</h2>
|
||||
<div>
|
||||
<h3>Committee</h3>
|
||||
<li>Chairman - John Runciman</li>
|
||||
<li>Chairman - Peter Hutchison</li>
|
||||
<li>Vice Chairman - Davin Webster</li>
|
||||
<li>National Liaison - Peter Hutchison</li>
|
||||
<li>Treasurer - Doug Timm</li>
|
||||
<li>Outings - John Runciman</li>
|
||||
<li>Events - Noelene Runciman</li>
|
||||
<li>Driver Training - John Runciman</li>
|
||||
<li>Events - Noelene Koertzen</li>
|
||||
<li>Driver Training - VACANT</li>
|
||||
<li>Digital Media - Christopher Pinto</li>
|
||||
<li>Marketing - Janet Erasmus</li>
|
||||
<li>Outdoor - Carla Holtzhausen</li>
|
||||
<li>Clubhouse - Tree Stiebel</li>
|
||||
<li>Maintenance - Kit Muirhead</li>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="pt-30 pb-20">
|
||||
@@ -238,7 +243,7 @@ require_once($rootPath . '/components/banner.php');
|
||||
<div class="cta-item" style="background-image: url(assets/images/trips/1_01.jpg);">
|
||||
<span class="category">Extended Trips</span>
|
||||
<h2>Come and Explore Africa and beyond</h2>
|
||||
<a href="<?= url('trips') ?>" class="theme-btn style-two bgc-secondary">
|
||||
<a href="trips" class="theme-btn style-two bgc-secondary">
|
||||
<span data-hover="Explore Tours">Explore Trips</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
@@ -248,7 +253,7 @@ require_once($rootPath . '/components/banner.php');
|
||||
<div class="cta-item" style="background-image: url(assets/images/courses/driver_training.png);">
|
||||
<span class="category">Driver Training</span>
|
||||
<h2>Level up your 4x4 Driving Skills</h2>
|
||||
<a href="<?= url('driver_training') ?>" class="theme-btn style-two">
|
||||
<a href="driver_training" class="theme-btn style-two">
|
||||
<span data-hover="Explore Tours">Explore Training</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
@@ -258,7 +263,7 @@ require_once($rootPath . '/components/banner.php');
|
||||
<div class="cta-item" style="background-image: url(assets/images/base4/camping.jpg);">
|
||||
<span class="category">Events</span>
|
||||
<h2>See whats cooking at BASE4!</h2>
|
||||
<a href="<?= url('events') ?>" class="theme-btn style-two bgc-secondary">
|
||||
<a href="events" class="theme-btn style-two bgc-secondary">
|
||||
<span data-hover="Explore Tours">Explore Events</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
|
||||
827
src/pages/other/base4.php
Normal file
@@ -0,0 +1,827 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||
include_once($rootPath . '/header.php');
|
||||
?>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin="" />
|
||||
<style>
|
||||
.gallery-slider-active {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
/* spacing between images */
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gallery-three-item {
|
||||
width: 520px;
|
||||
height: 300px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
background: #f9f9f9;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gallery-three-item .image {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gallery-three-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
/* ensures aspect ratio while filling container */
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.track-map-section {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.track-info-box {
|
||||
background: #f9f9f9;
|
||||
padding: 30px;
|
||||
margin: 20px auto;
|
||||
max-width: 1200px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.track-info-box h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.track-info-box p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 30px;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Shared marker styling for both legend and map obstacles */
|
||||
.legend-marker,
|
||||
.obstacle-marker {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.legend-marker span,
|
||||
.obstacle-marker span {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.legend-marker.red,
|
||||
.obstacle-marker.red {
|
||||
background: #e61e25;
|
||||
}
|
||||
|
||||
.legend-marker.green,
|
||||
.obstacle-marker.green {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.legend-marker.black,
|
||||
.obstacle-marker.black {
|
||||
background: #343a40;
|
||||
}
|
||||
|
||||
.legend-marker.split,
|
||||
.obstacle-marker.split {
|
||||
background: linear-gradient(45deg, #e61e25 50%, #28a745 50%);
|
||||
}
|
||||
|
||||
.obstacle-marker {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Leaflet marker container */
|
||||
.custom-marker-container {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#map {
|
||||
/* width: 100%; */
|
||||
height: 700px;
|
||||
margin: 50px;
|
||||
padding: 20px;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#map {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 500px !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.obstacle-popup h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.obstacle-popup .difficulty-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.obstacle-popup .difficulty-badge.easy {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.obstacle-popup .difficulty-badge.medium {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.obstacle-popup .difficulty-badge.hard {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.obstacle-popup .difficulty-badge.extreme {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.obstacle-popup img {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.obstacle-popup .description {
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.obstacle-marker:hover {
|
||||
transform: rotate(45deg) scale(1.1);
|
||||
}
|
||||
|
||||
.obstacle-marker span {
|
||||
transform: rotate(-45deg);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Obstacle Form Modal */
|
||||
.obstacle-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 10000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.obstacle-modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.obstacle-modal-content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.obstacle-modal-content h3 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.obstacle-modal-content .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.obstacle-modal-content label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.obstacle-modal-content input,
|
||||
.obstacle-modal-content select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.obstacle-modal-content .btn-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.obstacle-modal-content .btn-group button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10001;
|
||||
display: none;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert-message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php
|
||||
$pageTitle = 'BASE4 & 4X4 Track';
|
||||
$breadcrumbs = [['Home' => 'index.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
|
||||
<!-- Features Area start -->
|
||||
<section class="features-area pt-100 pb-45 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-xl-7">
|
||||
<div class=" mb-55" data-aos="fade-left" data-aos-duration="1500"
|
||||
data-aos-offset="50">
|
||||
<div class="section-title mb-20">
|
||||
<h2><b>BASE 4:</b> The home of 4WDCSA.</h2>
|
||||
<p>Nestled near the Hennops river, in Doornradje, Centurion, BASE4 is the ultimate weekend getaway for 4x4 enthusiasts and outdoor lovers. This vibrant hub offers an array of exciting activities, including a challenging 4x4 test track, relaxing camping spots, and a clubhouse with food and refreshments. Take a dip in the swimming pool, fire up the braai, or unwind our brand new clubhouse. Whether you're here for adventure or relaxation, BASE4 provides the perfect setting for all your off-road and outdoor adventures. Join the Four Wheel Drive Club of Southern Africa and be part of the thrill!</p>
|
||||
<div class="image">
|
||||
<img style="border-radius:10px;" src="assets/images/base4/01.jpeg" alt="Hotel">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="menu-btns py-10">
|
||||
<a href="membership" class="theme-btn style-two bgc-secondary">
|
||||
<span data-hover="Become a Member">Become a Member</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-5" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="row pb-25">
|
||||
<div class="section-title text-center counter-text-wrap mb-70" data-aos="fade-up"
|
||||
data-aos-duration="1500" data-aos-offset="50">
|
||||
<h2><b>BASE4</b><br>Non Member Fees:</h2>
|
||||
<div class="pt-20 pb-20">
|
||||
<h3>Day visitors*:</h3>
|
||||
<h4>R 50.00 per vehicle</h4>
|
||||
</div>
|
||||
<div class="pt-20 pb-20">
|
||||
<h3>Day visit & Track Pass*:</h3>
|
||||
<h4>R 150.00 per vehicle</h4>
|
||||
</div>
|
||||
<div class="pt-20 pb-20">
|
||||
<h3>Camping:</h3>
|
||||
<h4>R 250.00 per vehicle</h4>
|
||||
<p>Single night camping. Includes access to the track.</p>
|
||||
</div>
|
||||
<div class="pt-20 pb-20">
|
||||
<h3>BASE4 Weekend Pass:</h3>
|
||||
<h4>R 400.00 per vehicle</h4>
|
||||
<p>Camping from Friday till Sunday. Includes access to the track</p>
|
||||
</div>
|
||||
<p style="font-size:0.8rem;">
|
||||
*Day visitor charge not applicable on Open Days. Non-members require a 4WDCSA member to accompany them on the track at all times. Indemnity waiver must be signed at the clubhouse upon entry.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Features Area end -->
|
||||
|
||||
<!-- Hotel Area start -->
|
||||
<section class="hotel-area bgc-black py-100 rel z-1">
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-12">
|
||||
<div class="section-title text-white text-center counter-text-wrap mb-70" data-aos="fade-up"
|
||||
data-aos-duration="1500" data-aos-offset="50">
|
||||
<h2>BASE4 Open Days</h2>
|
||||
<p style="max-width: 60%; margin: auto;">Whether you're a member or just curious, everyone's welcome at our monthly open events. Come camp with us, enjoy guest speakers, take your rig for a spin on the 4x4 track, or just relax by the swimming pool. Food and refreshments are available all weekend, plus braai fires ready to go—just bring your tongs! It’s the perfect way to experience the spirit of the club and connect with fellow adventurers. </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-slider-active">
|
||||
<?php
|
||||
$folder = $rootPath . '/assets/images/opendays/';
|
||||
$images = glob($folder . '*.{jpg,jpeg,png,gif}', GLOB_BRACE);
|
||||
// Convert absolute paths to web-relative paths
|
||||
$images = array_map(function ($path) use ($rootPath) {
|
||||
return str_replace($rootPath, '', $path);
|
||||
}, $images);
|
||||
|
||||
// Shuffle and pick first 5
|
||||
shuffle($images);
|
||||
$selected = array_slice($images, 0, 10);
|
||||
|
||||
foreach ($selected as $image) {
|
||||
echo '<div class="gallery-three-item" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image">
|
||||
<img src="' . $image . '" alt="Gallery">
|
||||
</div>
|
||||
|
||||
</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="hotel-more-btn text-center mt-40">
|
||||
<a href="destination2.html" class="theme-btn style-four">
|
||||
<span data-hover="Explore More Hotel">Explore More Hotel</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div> -->
|
||||
</div>
|
||||
</section>
|
||||
<!-- Hotel Area end -->
|
||||
<!-- Track Map Section -->
|
||||
<section class="track-map-section">
|
||||
<div class="container">
|
||||
<div class="track-info-box">
|
||||
<div class="section-title text-center counter-text-wrap mb-70" data-aos="fade-up"
|
||||
data-aos-duration="1500" data-aos-offset="50">
|
||||
<h2>BASE4 4x4 Training Track</h2>
|
||||
<p>The training track at BASE4 was first created when the property was acquired in 2000. It has since been developed to provide a variety of obstacles and terrain challenges suitable for all skill levels. Open to all members. Join us on our next Driver Training Course to enhance your off-road skills and confidence and put your vehicle to the test.</p>
|
||||
</div>
|
||||
<?php if ($role === 'superadmin'): ?>
|
||||
<div style="margin: 20px 0; padding: 15px; background: #fff3cd; border-radius: 8px; border-left: 4px solid #ffc107;">
|
||||
<button id="toggleEditMode" class="btn btn-warning" style="margin-bottom: 10px;">
|
||||
🔧 Enable Edit Mode
|
||||
</button>
|
||||
<p id="editModeStatus" style="margin: 0; color: #856404; font-weight: bold; display: none;">
|
||||
✏️ Edit Mode Active - Click on map to place new markers, drag to reposition
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-marker green"><span></span></div>
|
||||
<span>Beginner</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-marker red"><span></span></div>
|
||||
<span>Intermediate</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-marker black"><span></span></div>
|
||||
<span>Advanced</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="map"></div>
|
||||
</section>
|
||||
|
||||
<!-- Obstacle Form Modal -->
|
||||
<div id="obstacleModal" class="obstacle-modal">
|
||||
<div class="obstacle-modal-content">
|
||||
<h3>Add New Obstacle</h3>
|
||||
<form id="obstacleForm">
|
||||
<input type="hidden" id="clickedLat" name="clickedLat">
|
||||
<input type="hidden" id="clickedLng" name="clickedLng">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="obstacleNumber">Obstacle Number *</label>
|
||||
<input type="number" id="obstacleNumber" name="obstacleNumber" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="markerColor">Marker Color *</label>
|
||||
<select id="markerColor" name="markerColor" required>
|
||||
<option value="green">Green (Beginner)</option>
|
||||
<option value="red">Red (Intermediate)</option>
|
||||
<option value="black">Black (Advanced)</option>
|
||||
<option value="split">Split (Mixed)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="obstacleName">Name</label>
|
||||
<input type="text" id="obstacleName" name="obstacleName" value="New Obstacle">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="obstacleDifficulty">Difficulty</label>
|
||||
<select id="obstacleDifficulty" name="obstacleDifficulty">
|
||||
<option value="Easy">Easy</option>
|
||||
<option value="Medium" selected>Medium</option>
|
||||
<option value="Hard">Hard</option>
|
||||
<option value="Extreme">Extreme</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeObstacleModal()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Obstacle</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Message -->
|
||||
<div id="alertMessage" class="alert-message"></div>
|
||||
|
||||
<?php
|
||||
require_once($rootPath . '/components/insta_footer.php');
|
||||
?>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
|
||||
<!-- Track Map JavaScript -->
|
||||
<script>
|
||||
console.log('Track map script loaded');
|
||||
|
||||
// Check if Leaflet is available
|
||||
if (typeof L === 'undefined') {
|
||||
console.error('Leaflet library not loaded!');
|
||||
} else {
|
||||
console.log('Leaflet library is available, version:', L.version);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('DOM loaded, initializing map...');
|
||||
|
||||
const mapElement = document.getElementById('map');
|
||||
console.log('Map element:', mapElement);
|
||||
|
||||
if (!mapElement) {
|
||||
console.error('Map element not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Map element dimensions:', mapElement.offsetWidth, 'x', mapElement.offsetHeight);
|
||||
|
||||
try {
|
||||
// Image dimensions: 2876 x 2035 pixels
|
||||
const imageWidth = 7942;
|
||||
const imageHeight = 3913;
|
||||
|
||||
// Create map with simple CRS (pixel coordinates)
|
||||
// Note: Leaflet uses [y, x] format, so bounds are [[0, 0], [height, width]]
|
||||
const bounds = [
|
||||
[0, 0],
|
||||
[imageHeight, imageWidth]
|
||||
];
|
||||
const map = L.map('map', {
|
||||
crs: L.CRS.Simple,
|
||||
minZoom: -1.5,
|
||||
maxZoom: 2,
|
||||
center: [imageHeight / 2, imageWidth / 2],
|
||||
zoom: -1,
|
||||
maxBounds: bounds,
|
||||
maxBoundsViscosity: 1.0
|
||||
});
|
||||
console.log('Map object created with CRS.Simple:', map);
|
||||
|
||||
// Add aerial image overlay
|
||||
const imageUrl = '/assets/images/track-aerial.jpg';
|
||||
L.imageOverlay(imageUrl, bounds).addTo(map);
|
||||
console.log('Aerial image overlay added');
|
||||
|
||||
// Add SVG overlay
|
||||
const svgUrl = '/assets/images/track-route.svg';
|
||||
L.imageOverlay(svgUrl, bounds, {
|
||||
opacity: 1,
|
||||
interactive: false
|
||||
}).addTo(map);
|
||||
console.log('SVG route overlay added');
|
||||
|
||||
// Fit map to image bounds
|
||||
map.fitBounds(bounds);
|
||||
|
||||
console.log('Map initialized successfully');
|
||||
|
||||
// Edit mode state
|
||||
let editMode = false;
|
||||
let markers = [];
|
||||
|
||||
// Edit mode toggle (only for admins)
|
||||
const toggleBtn = document.getElementById('toggleEditMode');
|
||||
const statusText = document.getElementById('editModeStatus');
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
editMode = !editMode;
|
||||
if (editMode) {
|
||||
toggleBtn.textContent = '🔒 Disable Edit Mode';
|
||||
toggleBtn.classList.remove('btn-warning');
|
||||
toggleBtn.classList.add('btn-success');
|
||||
statusText.style.display = 'block';
|
||||
|
||||
// Make existing markers draggable
|
||||
markers.forEach(m => m.marker.dragging.enable());
|
||||
} else {
|
||||
toggleBtn.textContent = '🔧 Enable Edit Mode';
|
||||
toggleBtn.classList.remove('btn-success');
|
||||
toggleBtn.classList.add('btn-warning');
|
||||
statusText.style.display = 'none';
|
||||
|
||||
// Disable dragging
|
||||
markers.forEach(m => m.marker.dragging.disable());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Click on map to add new marker (only in edit mode)
|
||||
map.on('click', (e) => {
|
||||
if (!editMode) return;
|
||||
|
||||
const coords = e.latlng;
|
||||
|
||||
// Store clicked coordinates and show modal
|
||||
document.getElementById('clickedLat').value = coords.lat;
|
||||
document.getElementById('clickedLng').value = coords.lng;
|
||||
document.getElementById('obstacleModal').classList.add('show');
|
||||
});
|
||||
|
||||
// Modal functions
|
||||
window.closeObstacleModal = function() {
|
||||
document.getElementById('obstacleModal').classList.remove('show');
|
||||
document.getElementById('obstacleForm').reset();
|
||||
};
|
||||
|
||||
window.showAlert = function(message, type = 'success') {
|
||||
const alertDiv = document.getElementById('alertMessage');
|
||||
alertDiv.textContent = message;
|
||||
alertDiv.className = 'alert-message ' + type + ' show';
|
||||
|
||||
setTimeout(() => {
|
||||
alertDiv.classList.remove('show');
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('obstacleForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const lat = parseFloat(document.getElementById('clickedLat').value);
|
||||
const lng = parseFloat(document.getElementById('clickedLng').value);
|
||||
const obstacleNumber = document.getElementById('obstacleNumber').value;
|
||||
const markerColor = document.getElementById('markerColor').value;
|
||||
const name = document.getElementById('obstacleName').value;
|
||||
const difficulty = document.getElementById('obstacleDifficulty').value;
|
||||
|
||||
// Create temporary marker
|
||||
const markerHtml = `
|
||||
<div class="obstacle-marker ${markerColor}">
|
||||
<span>${obstacleNumber}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const customIcon = L.divIcon({
|
||||
html: markerHtml,
|
||||
className: 'custom-marker-container',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 20]
|
||||
});
|
||||
|
||||
const marker = L.marker([lat, lng], {
|
||||
icon: customIcon,
|
||||
draggable: true
|
||||
}).addTo(map);
|
||||
|
||||
// Save to database
|
||||
const obstacleData = {
|
||||
obstacle_number: obstacleNumber,
|
||||
x_position: Math.round(lng),
|
||||
y_position: Math.round(lat),
|
||||
marker_color: markerColor,
|
||||
name: name,
|
||||
difficulty: difficulty,
|
||||
description: 'New obstacle - edit details in admin panel'
|
||||
};
|
||||
|
||||
saveObstacle(obstacleData, marker);
|
||||
closeObstacleModal();
|
||||
});
|
||||
|
||||
function saveObstacle(data, marker) {
|
||||
fetch('/src/processors/track-obstacles.php?action=create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.status === 'success') {
|
||||
showAlert('Obstacle #' + data.obstacle_number + ' created successfully!', 'success');
|
||||
marker.obstacleId = result.obstacle_id;
|
||||
markers.push({
|
||||
marker,
|
||||
data: {
|
||||
...data,
|
||||
obstacle_id: result.obstacle_id
|
||||
}
|
||||
});
|
||||
|
||||
// Add dragend event to update position
|
||||
marker.on('dragend', function() {
|
||||
const pos = marker.getLatLng();
|
||||
updateObstaclePosition(marker.obstacleId, Math.round(pos.lng), Math.round(pos.lat));
|
||||
});
|
||||
} else {
|
||||
showAlert('Error: ' + result.message, 'error');
|
||||
map.removeLayer(marker);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('Error creating obstacle: ' + error, 'error');
|
||||
map.removeLayer(marker);
|
||||
});
|
||||
}
|
||||
|
||||
function updateObstaclePosition(obstacleId, x, y) {
|
||||
fetch('/src/processors/track-obstacles.php?action=updatePosition', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
obstacle_id: obstacleId,
|
||||
x_position: x,
|
||||
y_position: y
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.status === 'success') {
|
||||
showAlert('Position updated', 'success');
|
||||
} else {
|
||||
showAlert('Error updating position: ' + result.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch and add obstacle markers
|
||||
fetch('/src/processors/track-obstacles.php?action=getAll')
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
console.log('Obstacles data:', result);
|
||||
|
||||
if (result.status === 'success' && result.data) {
|
||||
result.data.forEach((obstacle, index) => {
|
||||
// Leaflet uses [y, x] format for coordinates
|
||||
const position = [obstacle.y_position, obstacle.x_position];
|
||||
|
||||
// Create custom marker HTML
|
||||
const markerHtml = `
|
||||
<div class="obstacle-marker ${obstacle.marker_color}">
|
||||
<span>${obstacle.obstacle_number}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create custom icon
|
||||
const customIcon = L.divIcon({
|
||||
html: markerHtml,
|
||||
className: 'custom-marker-container',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 20]
|
||||
});
|
||||
|
||||
// Create popup content
|
||||
const popupContent = `
|
||||
<div class="obstacle-popup">
|
||||
<h4>${obstacle.name}</h4>
|
||||
<span class="difficulty-badge ${obstacle.difficulty.toLowerCase()}">${obstacle.difficulty}</span>
|
||||
${obstacle.image_path ? `<img src="${obstacle.image_path}" alt="${obstacle.name}" style="width: 100%; max-width: 300px; margin: 10px 0; border-radius: 8px;">` : ''}
|
||||
<p>${obstacle.description}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add marker to map
|
||||
const marker = L.marker(position, {
|
||||
icon: customIcon,
|
||||
draggable: false
|
||||
})
|
||||
.addTo(map)
|
||||
.bindPopup(popupContent, {
|
||||
maxWidth: 350,
|
||||
className: 'obstacle-popup-container'
|
||||
});
|
||||
|
||||
marker.obstacleId = obstacle.obstacle_id;
|
||||
markers.push({
|
||||
marker,
|
||||
data: obstacle
|
||||
});
|
||||
|
||||
// Add dragend event for position updates
|
||||
marker.on('dragend', function() {
|
||||
const pos = marker.getLatLng();
|
||||
updateObstaclePosition(obstacle.obstacle_id, Math.round(pos.lng), Math.round(pos.lat));
|
||||
});
|
||||
});
|
||||
|
||||
console.log('Added ' + result.data.length + ' obstacle markers');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading obstacles:', error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error initializing map:', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php ob_end_flush(); ?>
|
||||
@@ -156,7 +156,7 @@ $page_id = 'bush_mechanics';
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
|
||||
<?php
|
||||
$button_text = "Book Now";
|
||||
$button_text = "PROCEED TO PAYMENT";
|
||||
$button_disabled = "";
|
||||
if (!$result || $result->num_rows == 0) {
|
||||
$button_text = "No booking dates available";
|
||||
@@ -168,8 +168,9 @@ $page_id = 'bush_mechanics';
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
<div class="text-center">
|
||||
<a href="contact">Need some help?</a>
|
||||
<a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
|
||||
</div>
|
||||
<img src="assets/images/logos/ikhokha.png"alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -105,17 +105,29 @@ if (isset($_SESSION['user_id'])) {
|
||||
response = JSON.parse(response);
|
||||
}
|
||||
if (response.status === 'success') {
|
||||
// Check if the user has paid
|
||||
// If provider returned a direct paylink, go there immediately
|
||||
if (response.paylinkUrl) {
|
||||
window.location.href = 'membership_payment?token=' + encodeURIComponent(response.token);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a payment_id, redirect to membership_payment with it
|
||||
// if (response.payment_id) {
|
||||
// setTimeout(function() {
|
||||
// window.location.href = 'membership_payment.php?payment_id=' + encodeURIComponent(response.token);
|
||||
// }, 800);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Fallback behaviour: check paymentStatus
|
||||
if (response.paymentStatus === 'PAID') {
|
||||
// Redirect to membership_details.php if paid
|
||||
setTimeout(function() {
|
||||
window.location.href = 'membership_details.php';
|
||||
}, 2000); // 2-second delay before redirecting
|
||||
}, 1200);
|
||||
} else {
|
||||
// Redirect to membership_payment.php if not paid
|
||||
setTimeout(function() {
|
||||
window.location.href = 'membership_payment.php';
|
||||
}, 2000); // 2-second delay before redirecting
|
||||
}, 1200);
|
||||
}
|
||||
} else {
|
||||
$('#responseMessage').html('<div class="alert alert-danger">' + response.message + '</div>');
|
||||
|
||||
@@ -154,7 +154,7 @@ $page_id = 'rescue_recovery';
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$button_text = "Book Now";
|
||||
$button_text = "PROCEED TO PAYMENT";
|
||||
$button_disabled = "";
|
||||
if (!$result || $result->num_rows == 0) {
|
||||
$button_text = "No booking dates available";
|
||||
@@ -165,9 +165,11 @@ $page_id = 'rescue_recovery';
|
||||
<span data-hover="<?php echo $button_text; ?>"><?php echo $button_text; ?></span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="mailto:info@4wdcsa.co.za">Need some help?</a>
|
||||
<a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
|
||||
</div>
|
||||
<img src="assets/images/logos/ikhokha.png"alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
73
src/pages/payment/cancel.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||
include_once($rootPath . '/header.php');
|
||||
|
||||
$ref = $_GET['ref'] ?? null;
|
||||
$payment = null;
|
||||
$error_message = null;
|
||||
|
||||
if ($ref) {
|
||||
$stmt = $conn->prepare("SELECT payment_id, amount, payment_link, status, provider, provider_payment_id, public_ref, description FROM payments WHERE public_ref = ? OR payment_id = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('ss', $ref, $ref);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
$payment = $row;
|
||||
} else {
|
||||
$error_message = 'Payment record not found for the supplied reference.';
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
$error_message = 'Database error: ' . $conn->error;
|
||||
}
|
||||
} else {
|
||||
$error_message = 'No reference supplied.';
|
||||
}
|
||||
|
||||
$pageTitle = 'Payment Cancelled';
|
||||
$breadcrumbs = [['Home' => 'index.php'], ['Payment' => 'membership_payment.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
<section class="about-us-area py-100 rpb-90 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="section-title mb-25">
|
||||
<span class="h2 mb-15">Payment Cancelled</span>
|
||||
<h5>Your payment was cancelled or you returned without completing it.</h5>
|
||||
</div>
|
||||
|
||||
<?php if ($error_message) { ?>
|
||||
<div class="alert alert-warning"><?php echo htmlspecialchars($error_message); ?></div>
|
||||
<?php } else { ?>
|
||||
<p>Your payment appears to have been cancelled. If this was a mistake you can try again below.</p>
|
||||
<ul>
|
||||
<li><strong>Reference:</strong> <?php echo htmlspecialchars($payment['payment_id'] ?? $payment['public_ref']); ?></li>
|
||||
<li><strong>Amount:</strong> R <?php echo number_format($payment['amount'] ?? 0, 2); ?></li>
|
||||
<li><strong>Description:</strong> <?php echo htmlspecialchars($payment['description'] ?? ''); ?></li>
|
||||
</ul>
|
||||
|
||||
<?php if (!empty($payment['payment_id'])) { ?>
|
||||
<a href="<?php echo $payment['payment_link']; ?>" class="theme-btn style-two style-three" style="width:100%;">
|
||||
<span data-hover="Retry Payment">Retry Payment</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
<?php } ?>
|
||||
|
||||
<p style="margin-top:10px;">Contact <a href="mailto:info@4wdcsa.co.za">info@4wdcsa.co.za</a> if you need assistance.</p>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="about-us-image">
|
||||
<img src="/assets/images/logos/weblogo.png" alt="Logo">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>
|
||||
75
src/pages/payment/failure.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||
include_once($rootPath . '/header.php');
|
||||
|
||||
$ref = $_GET['ref'] ?? null;
|
||||
$payment = null;
|
||||
$error_message = null;
|
||||
|
||||
if ($ref) {
|
||||
$stmt = $conn->prepare("SELECT payment_id, amount, payment_link, status, provider, provider_payment_id, public_ref, description FROM payments WHERE public_ref = ? OR payment_id = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('ss', $ref, $ref);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
$payment = $row;
|
||||
} else {
|
||||
$error_message = 'Payment record not found for the supplied reference.';
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
$error_message = 'Database error: ' . $conn->error;
|
||||
}
|
||||
} else {
|
||||
$error_message = 'No reference supplied.';
|
||||
}
|
||||
|
||||
$pageTitle = 'Payment Failed';
|
||||
$breadcrumbs = [['Home' => 'index.php'], ['Payment' => 'membership_payment.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
<section class="about-us-area py-100 rpb-90 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="section-title mb-25">
|
||||
<span class="h2 mb-15">Payment Failed</span>
|
||||
<h5>Unfortunately your payment could not be completed.</h5>
|
||||
</div>
|
||||
|
||||
<?php if ($error_message) { ?>
|
||||
<div class="alert alert-warning"><?php echo htmlspecialchars($error_message); ?></div>
|
||||
<?php } else { ?>
|
||||
<p>We were unable to process your payment. You can try again or contact support for assistance.</p>
|
||||
<ul>
|
||||
<li><strong>Reference:</strong> <?php echo htmlspecialchars($payment['payment_id'] ?? $payment['public_ref']); ?></li>
|
||||
<li><strong>Amount:</strong> R <?php echo number_format($payment['amount'] ?? 0, 2); ?></li>
|
||||
<li><strong>Provider:</strong> <?php echo htmlspecialchars($payment['provider'] ?? ''); ?></li>
|
||||
<li><strong>Description:</strong> <?php echo htmlspecialchars($payment['description'] ?? ''); ?></li>
|
||||
<li><strong>Status:</strong> <?php echo htmlspecialchars($payment['status'] ?? ''); ?></li>
|
||||
</ul>
|
||||
|
||||
<?php if (!empty($payment['payment_id'])) { ?>
|
||||
<a href="<?php echo htmlspecialchars($payment['payment_link']); ?>" class="theme-btn style-two style-three" style="width:100%;">
|
||||
<span data-hover="Try Again">Try Again</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
<?php } ?>
|
||||
|
||||
<p style="margin-top:10px;">Or contact <a href="mailto:info@4wdcsa.co.za">info@4wdcsa.co.za</a> for help.</p>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="about-us-image">
|
||||
<img src="/assets/images/logos/weblogo.png" alt="Logo">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>
|
||||
84
src/pages/payment/success.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||
include_once($rootPath . '/header.php');
|
||||
|
||||
$ref = $_GET['ref'] ?? null;
|
||||
$payment = null;
|
||||
$error_message = null;
|
||||
|
||||
if ($ref) {
|
||||
$stmt = $conn->prepare("SELECT payment_id, amount, payment_link, status, provider, provider_payment_id, public_ref, description, booking_id FROM payments WHERE public_ref = ? OR payment_id = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('ss', $ref, $ref);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
$payment = $row;
|
||||
} else {
|
||||
$error_message = 'Payment record not found for the supplied reference.';
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
$error_message = 'Database error: ' . $conn->error;
|
||||
}
|
||||
} else {
|
||||
$error_message = 'No reference supplied.';
|
||||
}
|
||||
|
||||
$pageTitle = 'Payment Successful';
|
||||
$breadcrumbs = [['Home' => 'index.php'], ['Payment' => 'membership_payment.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
<section class="about-us-area py-100 rpb-90 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="section-title mb-25">
|
||||
<span class="h2 mb-15">Payment Successful</span>
|
||||
<h5>Thank you — your payment was received.</h5>
|
||||
</div>
|
||||
<?php
|
||||
$booking_id = $payment['booking_id'] ?? null;
|
||||
|
||||
if($booking_id == null) { ?>
|
||||
<h5>MEMBERSHIP STATUS: <?= getUserMemberStatus($user_id) ? 'ACTIVE' : 'INACTIVE'; ?></h5>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($error_message) { ?>
|
||||
<div class="alert alert-warning"><?php echo htmlspecialchars($error_message); ?></div>
|
||||
<?php } else { ?>
|
||||
<p>Your payment has been processed successfully. Below are the details we received:</p>
|
||||
<ul>
|
||||
<li><strong>Reference:</strong> <?php echo htmlspecialchars($payment['payment_id'] ?? $payment['public_ref']); ?></li>
|
||||
<li><strong>Amount:</strong> R <?php echo number_format($payment['amount'] ?? 0, 2); ?></li>
|
||||
<li><strong>Provider:</strong> <?php echo htmlspecialchars($payment['provider'] ?? ''); ?></li>
|
||||
<li><strong>Description:</strong> <?php echo htmlspecialchars($payment['description'] ?? ''); ?></li>
|
||||
<li><strong>Status:</strong> <?php echo htmlspecialchars($payment['status'] ?? ''); ?></li>
|
||||
</ul>
|
||||
|
||||
<?php if($booking_id == null) { ?>
|
||||
<a href="/membership_details.php" class="theme-btn style-two style-three" style="width:100%;">
|
||||
<span data-hover="Go to Membership Details">Go to Membership Details</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
<a href="/bookings.php" class="theme-btn style-two style-three" style="width:100%;">
|
||||
<span data-hover="Go to my Bookings">Go to my Bookings</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
<?php }
|
||||
}?>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="about-us-image">
|
||||
<img src="/assets/images/logos/weblogo.png" alt="Logo">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>
|
||||
@@ -5,6 +5,11 @@ require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
session_start();
|
||||
|
||||
// Enable error reporting for debugging
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0); // Don't display, but log them
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
http_response_code(401);
|
||||
echo "Not authorized";
|
||||
@@ -32,36 +37,42 @@ echo $author_id;
|
||||
$cover_image_path = null;
|
||||
|
||||
// Only attempt upload if a file was submitted
|
||||
if (!empty($_FILES['cover_image']['name'])) {
|
||||
if (!empty($_FILES['cover_image']['name']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
|
||||
$uploadDir = $rootPath . "/uploads/blogs/" . $article_id . "/";
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
|
||||
// Create directory if it doesn't exist (match working pattern)
|
||||
if (!file_exists($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
// Validate file using existing function
|
||||
$file_result = validateFileUpload($_FILES['cover_image'], 'profile_picture');
|
||||
if ($file_result === false) {
|
||||
// Simple validation - check extension
|
||||
$extension = strtolower(pathinfo($_FILES['cover_image']['name'], PATHINFO_EXTENSION));
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
if (!in_array($extension, $allowedExtensions)) {
|
||||
http_response_code(400);
|
||||
echo "Invalid file upload";
|
||||
echo "Invalid file type. Allowed: jpg, jpeg, png, gif, webp";
|
||||
exit;
|
||||
}
|
||||
|
||||
// Use fixed filename "cover" to avoid creating multiple copies on autosave
|
||||
$extension = $file_result['extension'];
|
||||
$filename = "cover." . $extension;
|
||||
|
||||
// Delete old cover if it exists with different extension
|
||||
array_map('unlink', glob($uploadDir . "cover.*"));
|
||||
$oldCovers = glob($uploadDir . "cover.*");
|
||||
if ($oldCovers) {
|
||||
foreach ($oldCovers as $oldCover) {
|
||||
@unlink($oldCover);
|
||||
}
|
||||
}
|
||||
|
||||
$targetPath = $uploadDir . $filename;
|
||||
$cover_image_path = "/uploads/blogs/" . $article_id . "/" . $filename;
|
||||
|
||||
// Move the uploaded file
|
||||
if (move_uploaded_file($_FILES['cover_image']['tmp_name'], $targetPath)) {
|
||||
// File moved successfully, $cover_image_path is set
|
||||
} else {
|
||||
if (!move_uploaded_file($_FILES['cover_image']['tmp_name'], $targetPath)) {
|
||||
http_response_code(500);
|
||||
echo "Failed to move uploaded file.";
|
||||
echo "Failed to move uploaded file";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_
|
||||
$upload_dir = $rootPath . '/uploads/blogs/' . $folder_id . '/';
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!is_dir($upload_dir)) {
|
||||
mkdir($upload_dir, 0755, true);
|
||||
if (!file_exists($upload_dir)) {
|
||||
mkdir($upload_dir, 0777, true);
|
||||
}
|
||||
|
||||
// Validate and process the file
|
||||
|
||||
49
src/processors/delete_course.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
ob_start();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . '/src/config/functions.php');
|
||||
require_once($rootPath . '/src/config/connection.php');
|
||||
|
||||
// Check admin status
|
||||
session_start();
|
||||
if (empty($_SESSION['user_id'])) {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_role = getUserRole();
|
||||
if (!in_array($user_role, ['admin', 'superadmin'])) {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$course_id = intval($_POST['course_id'] ?? 0);
|
||||
|
||||
if ($course_id <= 0) {
|
||||
throw new Exception('Invalid course ID');
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM courses WHERE course_id = ?");
|
||||
$stmt->bind_param("i", $course_id);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception('Failed to delete course: ' . $stmt->error);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'success', 'message' => 'Course deleted successfully']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
?>
|
||||
76
src/processors/delete_event.php
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
<?php
|
||||
ob_start();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . '/src/config/functions.php');
|
||||
require_once($rootPath . '/src/config/connection.php');
|
||||
|
||||
// Start session if not already started
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Check admin status
|
||||
if (empty($_SESSION['user_id'])) {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_role = getUserRole();
|
||||
if (!in_array($user_role, ['admin', 'superadmin'])) {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$event_id = intval($_POST['event_id'] ?? 0);
|
||||
|
||||
if ($event_id <= 0) {
|
||||
throw new Exception('Invalid event ID');
|
||||
}
|
||||
|
||||
// Get event details to delete associated files
|
||||
$stmt = $conn->prepare("SELECT image, promo FROM events WHERE event_id = ?");
|
||||
$stmt->bind_param("i", $event_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
$event = $result->fetch_assoc();
|
||||
|
||||
// Delete image files
|
||||
if ($event['image'] && file_exists($rootPath . '/' . $event['image'])) {
|
||||
unlink($rootPath . '/' . $event['image']);
|
||||
}
|
||||
if ($event['promo'] && file_exists($rootPath . '/' . $event['promo'])) {
|
||||
unlink($rootPath . '/' . $event['promo']);
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
$delete_stmt = $conn->prepare("DELETE FROM events WHERE event_id = ?");
|
||||
$delete_stmt->bind_param("i", $event_id);
|
||||
|
||||
if ($delete_stmt->execute()) {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'success', 'message' => 'Event deleted successfully']);
|
||||
} else {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to delete event']);
|
||||
}
|
||||
$delete_stmt->close();
|
||||
} else {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Event not found']);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
|
||||
} catch (Exception $e) {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -4,11 +4,20 @@ require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/session.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
require_once($rootPath . "/src/helpers/notification_helper.php");
|
||||
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
$eft_id = strtoupper($user_id." SUBS ".date("Y")." ".getInitialSurname($user_id));
|
||||
$status = 'AWAITING PAYMENT';
|
||||
$description = 'Membership Fees '.date("Y")." ".getInitialSurname($user_id);
|
||||
$payment_id = generatePaymentRef('SUBS', null, $user_id);
|
||||
|
||||
// If current month is December, attribute the membership year to the next year
|
||||
$currentYear = intval(date('Y'));
|
||||
$month = intval(date('n'));
|
||||
if ($month === 12) {
|
||||
$membershipYear = $currentYear + 1;
|
||||
} else {
|
||||
$membershipYear = $currentYear;
|
||||
}
|
||||
$description = 'Membership Fees ' . $membershipYear . ' ' . getInitialSurname($user_id);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF Token Validation
|
||||
@@ -84,6 +93,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
die('Invalid email format.');
|
||||
}
|
||||
|
||||
//MEMBERSHIP TYPE
|
||||
$country_membership = isset($_POST['country_membership']) ? 1 : 0;
|
||||
$membership_type = in_array($_POST['membership_type'] ?? '', ['full', 'single']) ? $_POST['membership_type'] : 'full';
|
||||
$honorary_member = validateHonoraryMemberName($first_name, $last_name);
|
||||
if ($honorary_member) {
|
||||
$membership_type = 'honorary';
|
||||
} elseif ($country_membership) {
|
||||
$membership_type = 'country';
|
||||
} else {
|
||||
$membership_type = $membership_type;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Spouse or Partner details (optional)
|
||||
$spouse_first_name = !empty($_POST['spouse_first_name']) ? validateName($_POST['spouse_first_name']) : null;
|
||||
$spouse_last_name = !empty($_POST['spouse_last_name']) ? validateName($_POST['spouse_last_name']) : null;
|
||||
@@ -128,8 +151,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
spouse_first_name, spouse_last_name, spouse_id_number, spouse_dob, spouse_occupation, spouse_tel_cell, spouse_email,
|
||||
child_name1, child_dob1, child_name2, child_dob2, child_name3, child_dob3,
|
||||
physical_address, postal_address, interests_hobbies, vehicle_make, vehicle_model, vehicle_year, vehicle_registration,
|
||||
secondary_vehicle_make, secondary_vehicle_model, secondary_vehicle_year, secondary_vehicle_registration
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
secondary_vehicle_make, secondary_vehicle_model, secondary_vehicle_year, secondary_vehicle_registration, membership_type
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
|
||||
// Check if preparation was successful
|
||||
if (!$stmt) {
|
||||
@@ -137,7 +160,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
$stmt->bind_param(
|
||||
"isssssssssssssssssssssssssssssss",
|
||||
"issssssssssssssssssssssssssssssss",
|
||||
$user_id,
|
||||
$first_name,
|
||||
$last_name,
|
||||
@@ -169,43 +192,84 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$secondary_vehicle_make,
|
||||
$secondary_vehicle_model,
|
||||
$secondary_vehicle_year,
|
||||
$secondary_vehicle_registration
|
||||
$secondary_vehicle_registration,
|
||||
$membership_type
|
||||
);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Insert into the membership fees table
|
||||
$payment_amount = calculateProrata(210); // Assuming a fixed membership fee, adjust as needed
|
||||
$payment_date = date('Y-m-d');
|
||||
$membership_start_date = $payment_date;
|
||||
// $membership_end_date = date('Y-12-31');
|
||||
|
||||
// Get today's date
|
||||
$today = new DateTime();
|
||||
|
||||
// Determine the target February
|
||||
if ($today->format('n') > 2) {
|
||||
// If we're past February, target is next year's Feb 28/29
|
||||
$year = $today->format('Y') + 1;
|
||||
$month = (int)$today->format('n');
|
||||
$year = (int)$today->format('Y');
|
||||
$payment_date = $today->format('Y-m-d');
|
||||
$membership_start_date = $payment_date;
|
||||
$status = 'AWAITING PAYMENT';
|
||||
if ($membership_type === 'honorary') {
|
||||
// Honorary members do not pay fees, set amount to 0 and end date far in future
|
||||
$payment_amount = 0.00;
|
||||
$status = 'PAID';
|
||||
} elseif ($membership_type === 'country') {
|
||||
$payment_amount = getPriceByDescription('country_membership');
|
||||
$prorata_amount = calculateProrata(getPriceByDescription('country_prorata'));
|
||||
} elseif ($membership_type === 'single') {
|
||||
$payment_amount = getPriceByDescription('single');
|
||||
$prorata_amount = calculateProrata(getPriceByDescription('single_prorata'));
|
||||
} else {
|
||||
// Otherwise, this year's February
|
||||
$year = $today->format('Y');
|
||||
$payment_amount = getPriceByDescription('membership_fees');
|
||||
$prorata_amount = calculateProrata(getPriceByDescription('pro_rata'));
|
||||
}
|
||||
|
||||
// Handle leap year (Feb 29) automatically
|
||||
$membership_end_date = (new DateTime("$year-02-01"))
|
||||
|
||||
|
||||
if ($month == 12 || $month == 1 || $month == 2) {
|
||||
// December, January, February: charge full fee, valid till end of next Feb
|
||||
$payment_amount = $payment_amount;
|
||||
// If Dec, Jan, Feb, set end to next year's Feb
|
||||
$end_year = ($month == 12) ? $year + 2 : $year + 1;
|
||||
$membership_end_date = (new DateTime("$end_year-02-01"))
|
||||
->modify('last day of this month')
|
||||
->format('Y-m-d');
|
||||
} else {
|
||||
// Prorata for Mar-Nov
|
||||
$payment_amount = $prorata_amount;
|
||||
// End of next Feb if after Feb, else this Feb
|
||||
if ($month > 2) {
|
||||
$end_year = $year + 1;
|
||||
} else {
|
||||
$end_year = $year;
|
||||
}
|
||||
$membership_end_date = (new DateTime("$end_year-02-01"))
|
||||
->modify('last day of this month')
|
||||
->format('Y-m-d');
|
||||
}
|
||||
if ($membership_type === 'honorary') {
|
||||
// Honorary members do not pay fees, set amount to 0 and end date far in future
|
||||
$membership_end_date = '2099-12-31';
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, payment_status, payment_id)
|
||||
VALUES (?, ?, ?, ?, ?, 'PENDING', ?)");
|
||||
$stmt->bind_param("idssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $eft_id);
|
||||
$stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, renewal_period_end, payment_status, payment_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param("idssssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $membership_end_date, $status, $payment_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Commit the transaction
|
||||
$conn->commit();
|
||||
addSubsEFT($eft_id, $user_id, $status, $payment_amount, $description);
|
||||
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
sendAdminNotification('4WDCSA.co.za - New Membership Application - '.$last_name , 'A new member has signed up, '.$first_name.' '.$last_name);
|
||||
// Do not create legacy EFTs. Create a payments-ready membership fee and notify admin.
|
||||
// Optionally send an invoice referencing the internal payment id
|
||||
// sendInvoice(getEmail($user_id), getFullName($user_id), $payment_id, formatCurrency($payment_amount), $description);
|
||||
// sendAdminNotification('4WDCSA.co.za - New Membership Application - '.$last_name , 'A new member has signed up, '.$first_name.' '.$last_name);
|
||||
// Audit: membership application submitted
|
||||
if (function_exists('auditLog')) {
|
||||
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");
|
||||
// Success message
|
||||
$response = [
|
||||
|
||||
@@ -3,6 +3,7 @@ $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 . "/src/helpers/notification_helper.php");
|
||||
|
||||
// Start session to retrieve the logged-in user's ID
|
||||
session_start();
|
||||
@@ -79,6 +80,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$stmt->bind_param('sissiiiidd', $type, $user_id, $from_date, $to_date, $num_vehicles, $num_adults, $num_children, $add_firewood, $total_amount, $discount_amount);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Get booking id and audit
|
||||
$booking_id = $conn->insert_id;
|
||||
if (function_exists('auditLog')) {
|
||||
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
|
||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'booking.php';</script>";
|
||||
} else {
|
||||
|
||||
100
src/processors/process_course.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
ob_start();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . '/src/config/functions.php');
|
||||
require_once($rootPath . '/src/config/connection.php');
|
||||
|
||||
// Check admin status
|
||||
session_start();
|
||||
if (empty($_SESSION['user_id'])) {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_role = getUserRole();
|
||||
if (!in_array($user_role, ['admin', 'superadmin'])) {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$course_id = $_POST['course_id'] ?? null;
|
||||
$course_type = trim($_POST['course_type'] ?? '');
|
||||
$code = trim($_POST['code'] ?? '');
|
||||
$date = trim($_POST['date'] ?? '');
|
||||
$capacity = intval($_POST['capacity'] ?? 0);
|
||||
$cost_members = floatval($_POST['cost_members'] ?? 0);
|
||||
$cost_nonmembers = floatval($_POST['cost_nonmembers'] ?? 0);
|
||||
$instructor = trim($_POST['instructor'] ?? '');
|
||||
$instructor_email = trim($_POST['instructor_email'] ?? '');
|
||||
|
||||
$allowed_types = ['driver_training','bush_mechanics','rescue_recovery','ladies_driver_training'];
|
||||
|
||||
if (!in_array($course_type, $allowed_types)) {
|
||||
throw new Exception('Invalid course type');
|
||||
}
|
||||
|
||||
if (empty($date) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
throw new Exception('Invalid date format');
|
||||
}
|
||||
|
||||
// If code not provided, generate from type + date using ABBR_MMDD format
|
||||
if (empty($code)) {
|
||||
$abbrMap = [
|
||||
'driver_training' => 'DRVTRN',
|
||||
'bush_mechanics' => 'BUSHMEC',
|
||||
'rescue_recovery' => 'RESREC',
|
||||
'ladies_driver_training' => 'LADYTRN'
|
||||
];
|
||||
|
||||
$abbr = $abbrMap[$course_type] ?? strtoupper(preg_replace('/[^A-Z0-9]/', '', $course_type));
|
||||
// ensure abbr fits (reserve 1 char for underscore and 4 for MMDD)
|
||||
$abbr = substr($abbr, 0, 7);
|
||||
$mmdd = date('md', strtotime($date));
|
||||
$code = strtoupper(substr($abbr . '_' . $mmdd, 0, 12));
|
||||
}
|
||||
|
||||
if ($capacity <= 0) {
|
||||
throw new Exception('Capacity must be greater than 0');
|
||||
}
|
||||
|
||||
if (empty($instructor)) {
|
||||
throw new Exception('Instructor name is required');
|
||||
}
|
||||
|
||||
if ($course_id) {
|
||||
// Update
|
||||
$stmt = $conn->prepare("UPDATE courses SET course_type = ?, code = ?, date = ?, capacity = ?, cost_members = ?, cost_nonmembers = ?, instructor = ?, instructor_email = ? WHERE course_id = ?");
|
||||
$stmt->bind_param("sssiddssi", $course_type, $code, $date, $capacity, $cost_members, $cost_nonmembers, $instructor, $instructor_email, $course_id);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception('Failed to update course: ' . $stmt->error);
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
// Insert - booked defaults to 0
|
||||
$stmt = $conn->prepare("INSERT INTO courses (course_type, code, date, capacity, booked, cost_members, cost_nonmembers, instructor, instructor_email) VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?)");
|
||||
$stmt->bind_param("sssiddss", $course_type, $code, $date, $capacity, $cost_members, $cost_nonmembers, $instructor, $instructor_email);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception('Failed to create course: ' . $stmt->error);
|
||||
}
|
||||
|
||||
$course_id = $conn->insert_id;
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'success', 'message' => $course_id ? 'Course saved successfully' : 'Course created successfully', 'course_id' => $course_id]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -3,6 +3,7 @@ $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 . "/src/helpers/notification_helper.php");
|
||||
session_start();
|
||||
|
||||
|
||||
@@ -93,10 +94,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
$status = "AWAITING PAYMENT";
|
||||
$type = 'course';
|
||||
$payment_id = uniqid();
|
||||
$payment_id = generatePaymentRef('COURSE', $course_id, $user_id);
|
||||
$publicRef = bin2hex(random_bytes(16));
|
||||
$num_vehicles = 1;
|
||||
$discountAmount = 0;
|
||||
$eft_id = strtoupper("COURSE ".date("m-d", strtotime($date))." ".getInitialSurname($user_id));
|
||||
$eft_id = $payment_id;
|
||||
$notes = "";
|
||||
if ($pending_member){
|
||||
$notes = "Membership Payment pending at time of booking. Please confirm payment has been received.";
|
||||
@@ -117,6 +119,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($stmt->execute()) {
|
||||
$booking_id = $conn->insert_id;
|
||||
|
||||
// Audit booking creation
|
||||
if (function_exists('auditLog')) {
|
||||
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 (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||
@@ -125,11 +140,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
echo "Error processing booking: $error_message";
|
||||
}
|
||||
} else {
|
||||
addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description);
|
||||
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
// Create payments row
|
||||
$pstmt = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, booking_id, public_ref) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
if ($pstmt) {
|
||||
$pstmt->bind_param('sidssis', $payment_id, $user_id, $payment_amount, $status, $description, $booking_id, $publicRef);
|
||||
$pstmt->execute();
|
||||
$pstmt->close();
|
||||
}
|
||||
|
||||
// Create iKhokha payment link
|
||||
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
||||
|
||||
// Send invoice and admin notification (keep for records)
|
||||
sendAdminNotification('New Course Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||
|
||||
// Redirect user to payment link if available
|
||||
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
|
||||
if ($paylink) {
|
||||
header('Location: ' . $paylink);
|
||||
exit();
|
||||
} else {
|
||||
// Fallback: redirect to legacy payment confirmation page
|
||||
header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
exit();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle error if insert fails and echo the MySQL error
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
include_once($rootPath . '/header.php');
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/session.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
// session_start();
|
||||
|
||||
checkAdmin();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
@@ -49,7 +54,7 @@ if ($_GET['action'] ?? null === 'delete') {
|
||||
}
|
||||
|
||||
// Check CSRF token
|
||||
if (!isset($_POST['csrf_token']) || !verifyCsrfToken($_POST['csrf_token'])) {
|
||||
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'CSRF token validation failed']);
|
||||
exit;
|
||||
}
|
||||
@@ -73,17 +78,17 @@ if (!$name || !$type || !$location || !$date || !$time || !$feature || !$descrip
|
||||
$image_path = null;
|
||||
if (!empty($_FILES['image']['name'])) {
|
||||
$upload_dir = $rootPath . '/assets/images/events/';
|
||||
if (!is_dir($upload_dir)) {
|
||||
mkdir($upload_dir, 0755, true);
|
||||
if (!file_exists($upload_dir)) {
|
||||
mkdir($upload_dir, 0777, true);
|
||||
}
|
||||
|
||||
$file_name = uniqid() . '_' . basename($_FILES['image']['name']);
|
||||
$target_file = $upload_dir . $file_name;
|
||||
$file_type = mime_content_type($_FILES['image']['tmp_name']);
|
||||
|
||||
// Validate image file
|
||||
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (!in_array($file_type, $allowed_types)) {
|
||||
// Validate file extension
|
||||
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
if (!in_array($ext, $allowed_extensions)) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid image file type. Only JPEG, PNG, GIF, and WebP are allowed']);
|
||||
exit;
|
||||
}
|
||||
@@ -103,17 +108,17 @@ if (!empty($_FILES['image']['name'])) {
|
||||
$promo_path = null;
|
||||
if (!empty($_FILES['promo']['name'])) {
|
||||
$upload_dir = $rootPath . '/assets/images/events/';
|
||||
if (!is_dir($upload_dir)) {
|
||||
mkdir($upload_dir, 0755, true);
|
||||
if (!file_exists($upload_dir)) {
|
||||
mkdir($upload_dir, 0777, true);
|
||||
}
|
||||
|
||||
$file_name = uniqid() . '_promo_' . basename($_FILES['promo']['name']);
|
||||
$target_file = $upload_dir . $file_name;
|
||||
$file_type = mime_content_type($_FILES['promo']['tmp_name']);
|
||||
|
||||
// Validate image file
|
||||
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (!in_array($file_type, $allowed_types)) {
|
||||
// Validate file extension
|
||||
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
if (!in_array($ext, $allowed_extensions)) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid promo image file type. Only JPEG, PNG, GIF, and WebP are allowed']);
|
||||
exit;
|
||||
}
|
||||
@@ -15,64 +15,92 @@ if (!$user_id) {
|
||||
echo "<script>alert('User is not logged in. Please log in to make a booking.'); window.location.href = 'login.php';</script>";
|
||||
exit();
|
||||
}
|
||||
$is_member = getUserMemberStatus($user_id);
|
||||
|
||||
$query = "SELECT payment_amount, payment_status, membership_end_date FROM membership_fees WHERE user_id = ?";
|
||||
// Fetch the membership fee record for this user
|
||||
$query = "SELECT fee_id, payment_amount, payment_status, membership_end_date FROM membership_fees WHERE user_id = ?";
|
||||
$stmt = $conn->prepare($query);
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Server error preparing statement']);
|
||||
exit();
|
||||
}
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
// Check if trip exists
|
||||
// Check if membership fee exists
|
||||
if ($result->num_rows === 0) {
|
||||
$response = ['error' => 'Application Fee not found.'];
|
||||
$response = ['error' => 'Membership fee not found.'];
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch trip details
|
||||
// Fetch fee details
|
||||
$fee = $result->fetch_assoc();
|
||||
$fee_id = isset($fee['fee_id']) ? intval($fee['fee_id']) : null;
|
||||
$payment_status = $fee['payment_status'];
|
||||
$membership_end_date = $fee['membership_end_date'];
|
||||
$payment_amount = intval($fee['payment_amount']);
|
||||
$payment_amount = floatval($fee['payment_amount']);
|
||||
$publicRef = bin2hex(random_bytes(16));
|
||||
|
||||
$description = "4WDCSA: Membership Fee " . getFullName($user_id) . " " . date("Y");
|
||||
$payment_id = uniqid();
|
||||
$eft_id = "SUBS 2025 ".getLastName($user_id);
|
||||
|
||||
// Update the membership_fees table to set payment_id
|
||||
$stmt = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE user_id = ?");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("ss", $payment_id, $user_id);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception("Failed to update membership_fees table.");
|
||||
// Persist the generated payment_id back to the membership_fees row (use fee_id to be precise)
|
||||
$updateStmt = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE fee_id = ?");
|
||||
if ($updateStmt) {
|
||||
$updateStmt->bind_param("si", $payment_id, $fee_id);
|
||||
if (!$updateStmt->execute()) {
|
||||
throw new Exception("Failed to update membership_fees table: " . $updateStmt->error);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
$updateStmt->close();
|
||||
} else {
|
||||
throw new Exception("Failed to prepare statement for membership_fees table: " . $conn->error);
|
||||
}
|
||||
|
||||
// Get the current date
|
||||
$current_date = new DateTime();
|
||||
|
||||
// Convert $membership_end_date to a DateTime object
|
||||
$membership_end_date_obj = DateTime::createFromFormat('Y-m-d', $membership_end_date);
|
||||
|
||||
// Check if the current date is after membership_end_date
|
||||
// OR if the current date is before or on membership_end_date AND payment_status is "PENDING"
|
||||
if (
|
||||
$current_date > $membership_end_date_obj ||
|
||||
($current_date <= $membership_end_date_obj && $payment_status === "PENDING")
|
||||
) {
|
||||
|
||||
// Call the processMembershipPayment function
|
||||
// processMembershipPayment($payment_id, $payment_amount, $description);
|
||||
addMembershipEFT($eft_id, $user_id, $status, $amount, $description, $membershipfee_id);
|
||||
header("Location: payment_confirmation?booking_id=" . $booking_id);
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
// If the amount is zero, treat as paid immediately
|
||||
if ($payment_amount < 1) {
|
||||
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||
// Update membership_fees status to PAID
|
||||
$paidStmt = $conn->prepare("UPDATE membership_fees SET payment_status = 'PAID' WHERE fee_id = ?");
|
||||
if ($paidStmt) {
|
||||
$paidStmt->bind_param('i', $fee_id);
|
||||
$paidStmt->execute();
|
||||
$paidStmt->close();
|
||||
}
|
||||
echo "<script>alert('Membership payment recorded.'); window.location.href = 'memberships.php';</script>";
|
||||
exit();
|
||||
} else {
|
||||
echo "<script>alert('Failed to process membership payment.'); window.location.href = 'memberships.php';</script>";
|
||||
exit();
|
||||
}
|
||||
} else {
|
||||
// Create payments row
|
||||
$status = "AWAITING PAYMENT";
|
||||
$pstmt = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, public_ref) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
if ($pstmt) {
|
||||
$pstmt->bind_param('sidsss', $payment_id, $user_id, $payment_amount, $status, $description, $publicRef);
|
||||
$pstmt->execute();
|
||||
$pstmt->close();
|
||||
}
|
||||
|
||||
// Create iKhokha payment link
|
||||
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
||||
|
||||
// Send invoice and admin notification if desired
|
||||
// sendInvoice(getEmail($user_id), getFullName($user_id), 'MEMBERSHIP-'.date('Y'), formatCurrency($payment_amount), $description);
|
||||
sendAdminNotification('Membership Payment Initiated - '.getFullName($user_id), getFullName($user_id).' initiated a membership payment.');
|
||||
|
||||
// Redirect user to payment link if available
|
||||
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
|
||||
if ($paylink) {
|
||||
header('Location: ' . $paylink);
|
||||
exit();
|
||||
} else {
|
||||
// Fallback: redirect to a membership page with an encrypted token
|
||||
header("Location: membership_confirmation?token=" . encryptData($payment_id, $salt));
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ if (isset($_POST['signature'])) {
|
||||
$filePath = $rootPath . '/uploads/signatures/' . $fileName;
|
||||
|
||||
// Ensure the directory exists
|
||||
if (!is_dir($rootPath . '/uploads/signatures')) {
|
||||
if (!file_exists($rootPath . '/uploads/signatures')) {
|
||||
mkdir($rootPath . '/uploads/signatures', 0777, true);
|
||||
}
|
||||
|
||||
@@ -56,17 +56,122 @@ if (isset($_POST['signature'])) {
|
||||
$stmt->bind_param('si', $display_path, $user_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Audit: signature saved
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'SIGNATURE_SAVED', 'membership_application', null, ['path' => $display_path]);
|
||||
}
|
||||
// Check the payment status
|
||||
$paymentStatus = checkMembershipPaymentStatus($user_id) ? 'PAID' : 'NOT_PAID';
|
||||
|
||||
// Respond with the appropriate redirect URL based on the payment status
|
||||
// If not paid, create a payments row (if missing) and initiate iKhokha paylink
|
||||
$paylink = null;
|
||||
if ($paymentStatus !== 'PAID') {
|
||||
// Fetch the membership fee row to get amount and payment_id
|
||||
$mfStmt = $conn->prepare("SELECT fee_id, payment_amount, payment_id FROM membership_fees WHERE user_id = ? ORDER BY fee_id DESC LIMIT 1");
|
||||
if ($mfStmt) {
|
||||
$mfStmt->bind_param('i', $user_id);
|
||||
$mfStmt->execute();
|
||||
$mfRes = $mfStmt->get_result();
|
||||
$mf = $mfRes->fetch_assoc();
|
||||
$mfStmt->close();
|
||||
} else {
|
||||
$mf = null;
|
||||
}
|
||||
|
||||
if ($mf && isset($mf['payment_amount'])) {
|
||||
$amount = floatval($mf['payment_amount']);
|
||||
// Use existing payment_id or generate one
|
||||
$payment_id = $mf['payment_id'] ?? generatePaymentRef('SUBS', null, $user_id);;
|
||||
|
||||
if (empty($mf['payment_id'])) {
|
||||
// Persist generated payment_id back to membership_fees
|
||||
$u = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE fee_id = ?");
|
||||
if ($u) {
|
||||
$u->bind_param('si', $payment_id, $mf['fee_id']);
|
||||
$u->execute();
|
||||
$u->close();
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure a payments row exists
|
||||
$checkP = $conn->prepare("SELECT COUNT(*) AS cnt FROM payments WHERE payment_id = ? LIMIT 1");
|
||||
if ($checkP) {
|
||||
$checkP->bind_param('s', $payment_id);
|
||||
$checkP->execute();
|
||||
$r = $checkP->get_result()->fetch_assoc();
|
||||
$exists = intval($r['cnt']) > 0;
|
||||
$checkP->close();
|
||||
} else {
|
||||
$exists = false;
|
||||
}
|
||||
|
||||
if (!$exists) {
|
||||
$publicRef = bin2hex(random_bytes(16));
|
||||
// If current month is December, attribute the membership year to the next year
|
||||
$currentYear = intval(date('Y'));
|
||||
$month = intval(date('n'));
|
||||
if ($month === 12) {
|
||||
$membershipYear = $currentYear + 1;
|
||||
} else {
|
||||
$membershipYear = $currentYear;
|
||||
}
|
||||
$description = 'Membership Fees ' . $membershipYear . ' ' . getInitialSurname($user_id);
|
||||
$status = 'AWAITING PAYMENT';
|
||||
$ins = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, public_ref) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
if ($ins) {
|
||||
$ins->bind_param('sidsss', $payment_id, $user_id, $amount, $status, $description, $publicRef);
|
||||
if ($ins->execute()) {
|
||||
// Audit: payment row created for membership
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'MEMBERSHIP_PAYMENT_CREATED', 'payments', null, ['payment_id' => $payment_id, 'amount' => $amount]);
|
||||
}
|
||||
}
|
||||
$ins->close();
|
||||
}
|
||||
}
|
||||
|
||||
// Create iKhokha paylink via helper (functions.php)
|
||||
try {
|
||||
$publicRef = $publicRef ?? bin2hex(random_bytes(16));
|
||||
$resp = createIkhokhaPayment($payment_id, $amount, $desc ?? ('Membership Fee ' . date('Y')), $publicRef);
|
||||
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
|
||||
// After creating paylink, update paymentStatus to AWAITING PAYMENT
|
||||
$paymentStatus = $paylink ? 'AWAITING PAYMENT' : $paymentStatus;
|
||||
$token = encryptData($payment_id, $_ENV['SALT']);
|
||||
// Audit: paylink created (or attempted)
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'IKHOKHA_PAYLINK_CREATED', 'payments', null, ['payment_id' => $payment_id, 'paylink' => $paylink]);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Log but do not fail signature save
|
||||
error_log('iKhokha create error: ' . $e->getMessage());
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'IKHOKHA_PAYLINK_FAILED', 'payments', null, ['payment_id' => $payment_id, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Respond with the appropriate redirect URL and paylink (if created)
|
||||
ob_end_clean();
|
||||
echo json_encode([
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
'message' => 'Signature saved successfully!',
|
||||
'paymentStatus' => $paymentStatus // Send payment status
|
||||
]);
|
||||
'paymentStatus' => $paymentStatus,
|
||||
'token' => $token ?? null
|
||||
];
|
||||
if (!empty($paylink)) {
|
||||
$response['paylinkUrl'] = $paylink;
|
||||
}
|
||||
if (!empty($payment_id)) {
|
||||
$response['payment_id'] = $payment_id;
|
||||
}
|
||||
echo json_encode($response);
|
||||
} else {
|
||||
// Audit: signature save failed
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'SIGNATURE_SAVE_FAILED', 'membership_application', null, ['user_id' => $user_id]);
|
||||
}
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Database update failed']);
|
||||
}
|
||||
@@ -78,6 +183,10 @@ if (isset($_POST['signature'])) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to save signature']);
|
||||
}
|
||||
} else {
|
||||
// Audit: no signature provided in request
|
||||
if (function_exists('auditLog') && isset($_SESSION['user_id'])) {
|
||||
auditLog($_SESSION['user_id'], 'SIGNATURE_NOT_PROVIDED', 'membership_application', null, ['endpoint' => 'process_signature.php']);
|
||||
}
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Signature not provided']);
|
||||
}
|
||||
|
||||
@@ -136,8 +136,8 @@ try {
|
||||
$upload_dir = $rootPath . '/assets/images/trips/';
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!is_dir($upload_dir)) {
|
||||
mkdir($upload_dir, 0755, true);
|
||||
if (!file_exists($upload_dir)) {
|
||||
mkdir($upload_dir, 0777, true);
|
||||
}
|
||||
|
||||
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
@@ -3,6 +3,7 @@ $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 . "/src/helpers/notification_helper.php");
|
||||
session_start();
|
||||
|
||||
// Get the trip_id from the request (ensure it's sanitized)
|
||||
@@ -78,6 +79,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$member_discount = $cost_nonmembers - $cost_members;
|
||||
$member_discount_pensioner = $cost_pensioner - $cost_pensioner_member;
|
||||
$booking_fee = $trip['booking_fee'];
|
||||
// Radio option (boolean/int) — ensure defined from POST
|
||||
$radio = isset($_POST['radio']) ? intval($_POST['radio']) : 0;
|
||||
$radioCost = $radio ? 50 : 0;
|
||||
$start_date = $trip['start_date']; // Start date of the trip
|
||||
$end_date = $trip['end_date']; // End date of the trip
|
||||
@@ -103,9 +106,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$status = "AWAITING PAYMENT";
|
||||
$description = $trip_name;
|
||||
$type = 'trip';
|
||||
$payment_id = uniqid();
|
||||
$payment_id = generatePaymentRef('TRIP', $trip_id, $user_id);
|
||||
$publicRef = bin2hex(random_bytes(16));
|
||||
// $eft_id = strtoupper(base_convert(time(), 10, 36)); // Convert timestamp to base36
|
||||
$eft_id = strtoupper($trip_code." ".getInitialSurname($user_id));
|
||||
// $eft_id = strtoupper($trip_code." ".getInitialSurname($user_id));
|
||||
|
||||
|
||||
// Insert booking into the database
|
||||
@@ -123,6 +127,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Get the generated booking_id
|
||||
$booking_id = $conn->insert_id;
|
||||
|
||||
// Audit booking creation
|
||||
if (function_exists('auditLog')) {
|
||||
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 (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||
@@ -131,11 +150,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
echo "Error processing booking: $error_message";
|
||||
}
|
||||
} else {
|
||||
addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description);
|
||||
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
// Create payments row
|
||||
$pstmt = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, booking_id, public_ref) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
if ($pstmt) {
|
||||
$pstmt->bind_param('sidssis', $payment_id, $user_id, $payment_amount, $status, $description, $booking_id, $publicRef);
|
||||
$pstmt->execute();
|
||||
$pstmt->close();
|
||||
}
|
||||
|
||||
// Create iKhokha payment link
|
||||
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
||||
|
||||
// Send invoice and admin notification
|
||||
sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||
|
||||
// Redirect to payment link if available
|
||||
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
|
||||
if ($paylink) {
|
||||
header('Location: ' . $paylink);
|
||||
exit();
|
||||
} else {
|
||||
header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
exit();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle error if insert fails and echo the MySQL error
|
||||
|
||||
@@ -52,26 +52,25 @@ try {
|
||||
|
||||
// Create album directory
|
||||
$albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id;
|
||||
if (!is_dir($albumDir)) {
|
||||
if (!mkdir($albumDir, 0755, true)) {
|
||||
throw new Exception('Failed to create album directory');
|
||||
}
|
||||
if (!file_exists($albumDir)) {
|
||||
mkdir($albumDir, 0777, true);
|
||||
}
|
||||
|
||||
// Handle cover image upload
|
||||
$coverImagePath = null;
|
||||
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] !== UPLOAD_ERR_NO_FILE) {
|
||||
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
|
||||
$maxSize = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
$fileName = $_FILES['cover_image']['name'];
|
||||
$fileTmpName = $_FILES['cover_image']['tmp_name'];
|
||||
$fileSize = $_FILES['cover_image']['size'];
|
||||
$fileMime = mime_content_type($fileTmpName);
|
||||
|
||||
// Validate file
|
||||
if (!in_array($fileMime, $allowedMimes)) {
|
||||
throw new Exception('Invalid cover image file type');
|
||||
// Validate file extension
|
||||
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
if (!in_array($ext, $allowedExtensions)) {
|
||||
throw new Exception('Invalid cover image file type. Allowed: jpg, jpeg, png, gif, webp');
|
||||
}
|
||||
|
||||
if ($fileSize > $maxSize) {
|
||||
@@ -96,8 +95,7 @@ try {
|
||||
}
|
||||
|
||||
// Handle photo uploads
|
||||
if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] !== UPLOAD_ERR_NO_FILE) {
|
||||
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] === UPLOAD_ERR_OK) {
|
||||
$maxSize = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
$displayOrder = 1;
|
||||
@@ -111,11 +109,13 @@ try {
|
||||
$fileName = $_FILES['photos']['name'][$i];
|
||||
$fileTmpName = $_FILES['photos']['tmp_name'][$i];
|
||||
$fileSize = $_FILES['photos']['size'][$i];
|
||||
$fileMime = mime_content_type($fileTmpName);
|
||||
|
||||
// Validate file
|
||||
if (!in_array($fileMime, $allowedMimes)) {
|
||||
throw new Exception('Invalid file type: ' . $fileName);
|
||||
// Validate file extension
|
||||
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
if (!in_array($ext, $allowedExtensions)) {
|
||||
throw new Exception('Invalid file type: ' . $fileName . '. Allowed: jpg, jpeg, png, gif, webp');
|
||||
}
|
||||
|
||||
if ($fileSize > $maxSize) {
|
||||
|
||||
@@ -43,14 +43,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$filename = str_replace(' ', '_', $eft_id) . '.pdf';
|
||||
$target_file = $target_dir . $filename;
|
||||
|
||||
// Make sure target directory exists and writable
|
||||
if (!is_dir($target_dir)) {
|
||||
mkdir($target_dir, 0755, true);
|
||||
}
|
||||
|
||||
if (!is_writable($target_dir)) {
|
||||
echo "<div class='alert alert-danger'>Upload directory is not writable: $target_dir</div>";
|
||||
exit;
|
||||
// Make sure target directory exists
|
||||
if (!file_exists($target_dir)) {
|
||||
mkdir($target_dir, 0777, true);
|
||||
}
|
||||
|
||||
if (move_uploaded_file($_FILES['pop_file']['tmp_name'], $target_file)) {
|
||||
|
||||
164
src/processors/track-obstacles.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
/**
|
||||
* TRACK OBSTACLES API ENDPOINT
|
||||
*
|
||||
* Returns all track obstacles as JSON for the interactive map.
|
||||
*
|
||||
* Usage:
|
||||
* GET /src/processors/track-obstacles.php?action=getAll
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* "status": "success",
|
||||
* "data": [
|
||||
* {
|
||||
* "obstacle_id": 1,
|
||||
* "name": "Rock Crawl",
|
||||
* "x_position": 150,
|
||||
* "y_position": 200,
|
||||
* "difficulty": "medium",
|
||||
* "description": "Navigate through rocky terrain...",
|
||||
* "image_path": "assets/images/obstacles/obstacle1.jpg",
|
||||
* "marker_color": "green"
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
|
||||
// Set headers for JSON response
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
// Load configuration and database
|
||||
$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 . "/classes/DatabaseService.php");
|
||||
|
||||
// Get database instance
|
||||
$db = new DatabaseService($conn);
|
||||
|
||||
try {
|
||||
// Get action from query string
|
||||
$action = $_GET['action'] ?? 'getAll';
|
||||
|
||||
if ($action === 'getAll') {
|
||||
// Fetch all obstacles from the database
|
||||
$sql = "SELECT
|
||||
obstacle_id,
|
||||
obstacle_number,
|
||||
name,
|
||||
x_position,
|
||||
y_position,
|
||||
difficulty,
|
||||
description,
|
||||
image_path,
|
||||
marker_color
|
||||
FROM track_obstacles
|
||||
ORDER BY obstacle_id ASC";
|
||||
|
||||
$result = $conn->query($sql);
|
||||
|
||||
if ($result === false) {
|
||||
throw new Exception("Database query failed: " . $conn->error);
|
||||
}
|
||||
|
||||
$obstacles = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$obstacles[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => $obstacles
|
||||
]);
|
||||
|
||||
} elseif ($action === 'create') {
|
||||
// Create new obstacle (superadmin only)
|
||||
$role = getUserRole();
|
||||
if ($role !== 'superadmin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$sql = "INSERT INTO track_obstacles
|
||||
(obstacle_number, name, x_position, y_position, difficulty, description, marker_color)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
$insertId = $db->insert($sql, [
|
||||
$input['obstacle_number'],
|
||||
$input['name'],
|
||||
$input['x_position'],
|
||||
$input['y_position'],
|
||||
$input['difficulty'],
|
||||
$input['description'],
|
||||
$input['marker_color']
|
||||
], 'ssiisss');
|
||||
|
||||
if ($insertId) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Obstacle created',
|
||||
'obstacle_id' => $insertId
|
||||
]);
|
||||
} else {
|
||||
throw new Exception("Failed to create obstacle: " . $db->getLastError());
|
||||
}
|
||||
|
||||
} elseif ($action === 'updatePosition') {
|
||||
// Update obstacle position (superadmin only)
|
||||
$role = getUserRole();
|
||||
if ($role !== 'superadmin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$sql = "UPDATE track_obstacles
|
||||
SET x_position = ?, y_position = ?
|
||||
WHERE obstacle_id = ?";
|
||||
|
||||
$result = $db->update($sql, [
|
||||
$input['x_position'],
|
||||
$input['y_position'],
|
||||
$input['obstacle_id']
|
||||
], 'iii');
|
||||
|
||||
if ($result !== false) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Position updated'
|
||||
]);
|
||||
} else {
|
||||
throw new Exception("Failed to update position: " . $db->getLastError());
|
||||
}
|
||||
|
||||
} else {
|
||||
// Invalid action
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Invalid action specified'
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Return error response
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Server error: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
exit();
|
||||
?>
|
||||
@@ -76,26 +76,30 @@ try {
|
||||
$updateStmt->close();
|
||||
|
||||
// Handle cover image upload if provided
|
||||
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] !== UPLOAD_ERR_NO_FILE) {
|
||||
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
$maxSize = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
|
||||
$fileName = $_FILES['cover_image']['name'];
|
||||
$fileTmpName = $_FILES['cover_image']['tmp_name'];
|
||||
$fileSize = $_FILES['cover_image']['size'];
|
||||
$fileMime = mime_content_type($fileTmpName);
|
||||
|
||||
// Validate file
|
||||
if (!in_array($fileMime, $allowedMimes)) {
|
||||
throw new Exception('Invalid cover image file type');
|
||||
// Validate file extension
|
||||
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
if (!in_array($ext, $allowedExtensions)) {
|
||||
throw new Exception('Invalid cover image file type. Allowed: jpg, jpeg, png, gif, webp');
|
||||
}
|
||||
|
||||
if ($fileSize > $maxSize) {
|
||||
if ($fileSize > 5 * 1024 * 1024) {
|
||||
throw new Exception('Cover image file too large (max 5MB)');
|
||||
}
|
||||
|
||||
$albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id;
|
||||
|
||||
// Create directory if it doesn't exist (match working pattern)
|
||||
if (!file_exists($albumDir)) {
|
||||
mkdir($albumDir, 0777, true);
|
||||
}
|
||||
|
||||
// Delete old cover if it exists
|
||||
$oldCoverStmt = $conn->prepare("SELECT cover_image FROM photo_albums WHERE album_id = ?");
|
||||
$oldCoverStmt->bind_param("i", $album_id);
|
||||
@@ -104,16 +108,15 @@ try {
|
||||
if ($oldCoverResult->num_rows > 0) {
|
||||
$oldCover = $oldCoverResult->fetch_assoc();
|
||||
if ($oldCover['cover_image']) {
|
||||
$oldCoverPath = $_SERVER['DOCUMENT_ROOT'] . $oldCover['cover_image'];
|
||||
$oldCoverPath = $rootPath . $oldCover['cover_image'];
|
||||
if (file_exists($oldCoverPath)) {
|
||||
unlink($oldCoverPath);
|
||||
@unlink($oldCoverPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
$oldCoverStmt->close();
|
||||
|
||||
// Generate unique filename
|
||||
$ext = pathinfo($fileName, PATHINFO_EXTENSION);
|
||||
$newFileName = 'cover_' . uniqid() . '.' . $ext;
|
||||
$filePath = $albumDir . '/' . $newFileName;
|
||||
$coverImagePath = '/assets/uploads/gallery/' . $album_id . '/' . $newFileName;
|
||||
@@ -130,12 +133,15 @@ try {
|
||||
}
|
||||
|
||||
// Handle photo uploads if any
|
||||
if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] !== UPLOAD_ERR_NO_FILE) {
|
||||
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] === UPLOAD_ERR_OK) {
|
||||
$maxSize = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
$albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id;
|
||||
|
||||
// Create directory if it doesn't exist (match working pattern)
|
||||
if (!file_exists($albumDir)) {
|
||||
mkdir($albumDir, 0777, true);
|
||||
}
|
||||
|
||||
// Get current max display order
|
||||
$orderStmt = $conn->prepare("SELECT MAX(display_order) as max_order FROM photos WHERE album_id = ?");
|
||||
$orderStmt->bind_param("i", $album_id);
|
||||
@@ -153,15 +159,17 @@ try {
|
||||
$fileName = $_FILES['photos']['name'][$i];
|
||||
$fileTmpName = $_FILES['photos']['tmp_name'][$i];
|
||||
$fileSize = $_FILES['photos']['size'][$i];
|
||||
$fileMime = mime_content_type($fileTmpName);
|
||||
|
||||
// Validate file
|
||||
if (!in_array($fileMime, $allowedMimes)) {
|
||||
throw new Exception('Invalid file type: ' . $fileName);
|
||||
// Validate file extension
|
||||
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
if (!in_array($ext, $allowedExtensions)) {
|
||||
throw new Exception('Invalid file type: ' . $fileName . '. Allowed: jpg, jpeg, png, gif, webp');
|
||||
}
|
||||
|
||||
if ($fileSize > $maxSize) {
|
||||
throw new Exception('File too large: ' . $fileName);
|
||||
throw new Exception('File too large: ' . $fileName . ' (max 5MB)');
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
|
||||
@@ -43,15 +43,9 @@ if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] !=
|
||||
$target_dir = $rootPath . "/assets/images/pp/";
|
||||
$target_file = $target_dir . $randomFilename;
|
||||
|
||||
// Ensure upload directory exists and is writable
|
||||
if (!is_dir($target_dir)) {
|
||||
mkdir($target_dir, 0755, true);
|
||||
}
|
||||
|
||||
if (!is_writable($target_dir)) {
|
||||
$response['message'] = 'Upload directory is not writable.';
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
// Ensure upload directory exists
|
||||
if (!file_exists($target_dir)) {
|
||||
mkdir($target_dir, 0777, true);
|
||||
}
|
||||
|
||||
// Move the uploaded file
|
||||
|
||||
60
test_payment.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
$endpoint = "https://api.ikhokha.com/public-api/v1/api/payment";
|
||||
$appID = "IKFLESZTKFM4HWWS76131L8HK9BYF96P";
|
||||
$appSecret = "gfoQTvXRXuzq6ArPHUS2CBFxtHtH1bxM";
|
||||
$requestBody = [
|
||||
"entityID" => "4",
|
||||
"externalEntityID" => "4",
|
||||
"amount" => 1000,
|
||||
"currency" => "ZAR",
|
||||
"requesterUrl" => "https://beta.4wdcsa.co.za/requester",
|
||||
"description" => "Test Description 1",
|
||||
"paymentReference" => "4",
|
||||
"mode" => "sandbox",
|
||||
"externalTransactionID" => "5",
|
||||
"urls" => [
|
||||
"callbackUrl" => "https://beta.4wdcsa.co.za/callback",
|
||||
"successPageUrl" => "https://beta.4wdcsa.co.za/success",
|
||||
"failurePageUrl" => "https://beta.4wdcsa.co.za/failure",
|
||||
"cancelUrl" => "https://beta.4wdcsa.co.za/cancel"
|
||||
]
|
||||
];
|
||||
function escapeString($str) {
|
||||
$escaped = preg_replace(['/[\\"\'\"]/u', '/\x00/'], ['\\\\$0', '\\0'], (string)$str);
|
||||
$cleaned = str_replace('\/', '/', $escaped);
|
||||
return $cleaned;
|
||||
}
|
||||
function createPayloadToSign($urlPath, $body) {
|
||||
$parsedUrl = parse_url($urlPath);
|
||||
$basePath = $parsedUrl['path'];
|
||||
if (!$basePath) {
|
||||
throw new Exception("No path present in the URL");
|
||||
}
|
||||
$payload = $basePath . $body;
|
||||
$escapedPayloadString = escapeString($payload);
|
||||
return $escapedPayloadString;
|
||||
}
|
||||
function generateSignature($payloadToSign, $secret) {
|
||||
return hash_hmac('sha256', $payloadToSign, $secret);
|
||||
}
|
||||
$stringifiedBody = json_encode($requestBody);
|
||||
$payloadToSign = createPayloadToSign($endpoint, $stringifiedBody);
|
||||
$ikSign = generateSignature($payloadToSign, $appSecret);
|
||||
// Initialize cURL session
|
||||
$ch = curl_init($endpoint);
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $stringifiedBody);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Content-Type: application/json",
|
||||
"IK-APPID: $appID",
|
||||
"IK-SIGN: $ikSign"
|
||||
]);
|
||||
// Execute cURL session
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
// Decode and output the response
|
||||
$responseData = json_decode($response, true);
|
||||
echo print_r($responseData, true);
|
||||
?>
|
||||
BIN
uploads/pop/103_SUBS_2025_E._BESTER.pdf
Normal file
BIN
uploads/pop/105_SUBS_2025_D._KLADIS.pdf
Normal file
BIN
uploads/pop/109_SUBS_2025_A._MAHON.pdf
Normal file
|
After Width: | Height: | Size: 42 KiB |