Compare commits
11 Commits
feature/bl
...
f4934e9c13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4934e9c13 | ||
|
|
477c2f2e04 | ||
|
|
a66382661d | ||
|
|
32e50ffc39 | ||
|
|
cce181e2d0 | ||
|
|
48ee7592b2 | ||
|
|
abb8eb23e5 | ||
|
|
2acbeac7ca | ||
|
|
5808788b9e | ||
|
|
bbc0aecbcb | ||
|
|
752ea6e5e9 |
12
.htaccess
@@ -70,6 +70,7 @@ RewriteRule ^instapage$ src/pages/events/instapage.php [L]
|
||||
RewriteRule ^about$ src/pages/other/about.php [L]
|
||||
RewriteRule ^contact$ src/pages/other/contact.php [L]
|
||||
RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.php [L]
|
||||
RewriteRule ^track-map$ src/pages/track-map.php [L]
|
||||
RewriteRule ^404$ src/pages/other/404.php [L]
|
||||
RewriteRule ^account_settings$ src/pages/other/account_settings.php [L]
|
||||
RewriteRule ^rescue_recovery$ src/pages/other/rescue_recovery.php [L]
|
||||
@@ -79,6 +80,11 @@ 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_members$ src/admin/admin_members.php [L]
|
||||
RewriteRule ^admin_payments$ src/admin/admin_payments.php [L]
|
||||
@@ -122,11 +128,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]
|
||||
|
||||
215
.htaccess copy
Normal file
@@ -0,0 +1,215 @@
|
||||
# URL Rewrite Rules - Maps old URLs to new directory structure during migration
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# Don't rewrite existing files or directories
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
|
||||
# === STRIP .PHP EXTENSION ===
|
||||
# Redirect /page.php to /page (301 permanent redirect)
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.+)\.php$ /$1 [R=301,L]
|
||||
# Internally rewrite /page to /page.php if page.php exists
|
||||
RewriteCond %{REQUEST_FILENAME}\.php -f
|
||||
RewriteRule ^(.+)$ $1.php [L]
|
||||
|
||||
# === AUTH PAGES ===
|
||||
RewriteRule ^login$ src/pages/auth/login.php [L]
|
||||
RewriteRule ^register$ src/pages/auth/register.php [L]
|
||||
RewriteRule ^forgot_password$ src/pages/auth/forgot_password.php [L]
|
||||
RewriteRule ^reset_password$ src/pages/auth/reset_password.php [L]
|
||||
RewriteRule ^verify$ src/pages/auth/verify.php [L]
|
||||
RewriteRule ^resend_verification$ src/pages/auth/resend_verification.php [L]
|
||||
RewriteRule ^change_password$ src/pages/auth/change_password.php [L]
|
||||
RewriteRule ^update_password$ src/pages/auth/update_password.php [L]
|
||||
|
||||
# === MEMBERSHIP PAGES ===
|
||||
RewriteRule ^membership$ src/pages/memberships/membership.php [L]
|
||||
RewriteRule ^membership_details$ src/pages/memberships/membership_details.php [L]
|
||||
RewriteRule ^membership_application$ src/pages/memberships/membership_application.php [L]
|
||||
RewriteRule ^membership_payment$ src/pages/memberships/membership_payment.php [L]
|
||||
RewriteRule ^renew_membership$ src/pages/memberships/renew_membership.php [L]
|
||||
RewriteRule ^member_info$ src/pages/memberships/member_info.php [L]
|
||||
|
||||
# === BOOKING PAGES ===
|
||||
RewriteRule ^bookings$ src/pages/bookings/bookings.php [L]
|
||||
RewriteRule ^campsites$ src/pages/bookings/campsites.php [L]
|
||||
RewriteRule ^campsite_booking$ src/pages/bookings/campsite_booking.php [L]
|
||||
RewriteRule ^add_campsite$ src/pages/add_campsite.php [L]
|
||||
RewriteRule ^trips$ src/pages/bookings/trips.php [L]
|
||||
RewriteRule ^trip-details$ src/pages/bookings/trip-details.php [L]
|
||||
RewriteRule ^course_details$ src/pages/bookings/course_details.php [L]
|
||||
RewriteRule ^driver_training$ src/pages/bookings/driver_training.php [L]
|
||||
|
||||
# === SHOP PAGES ===
|
||||
RewriteRule ^view_cart$ src/pages/shop/view_cart.php [L]
|
||||
RewriteRule ^add_to_cart$ src/pages/shop/add_to_cart.php [L]
|
||||
RewriteRule ^bar_tabs$ src/pages/shop/bar_tabs.php [L]
|
||||
RewriteRule ^payment_confirmation$ src/pages/shop/payment_confirmation.php [L]
|
||||
RewriteRule ^confirm$ src/pages/shop/confirm.php [L]
|
||||
RewriteRule ^confirm2$ src/pages/shop/confirm2.php [L]
|
||||
|
||||
# === GALLERY PAGES ===
|
||||
RewriteRule ^gallery$ src/pages/gallery/gallery.php [L]
|
||||
RewriteRule ^create_album$ src/pages/gallery/create_album.php [L]
|
||||
RewriteRule ^edit_album$ src/pages/gallery/create_album.php [L]
|
||||
RewriteRule ^view_album$ src/pages/gallery/view_album.php [L]
|
||||
|
||||
# === EVENTS & BLOG PAGES ===
|
||||
RewriteRule ^events$ src/pages/events/events.php [L]
|
||||
RewriteRule ^blog$ src/pages/blog/blog.php [L]
|
||||
RewriteRule ^blog_details$ src/pages/blog/blog_details.php [L]
|
||||
RewriteRule ^best_of_the_eastern_cape_2024$ src/pages/events/best_of_the_eastern_cape_2024.php [L]
|
||||
RewriteRule ^2025_agm_minutes$ src/pages/events/2025_agm_minutes.php [L]
|
||||
RewriteRule ^agm_content$ src/pages/events/agm_content.php [L]
|
||||
RewriteRule ^instapage$ src/pages/events/instapage.php [L]
|
||||
|
||||
# === OTHER PAGES ===
|
||||
RewriteRule ^about$ src/pages/other/about.php [L]
|
||||
RewriteRule ^contact$ src/pages/other/contact.php [L]
|
||||
RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.php [L]
|
||||
RewriteRule ^track-map$ src/pages/track-map.php [L]
|
||||
RewriteRule ^404$ src/pages/other/404.php [L]
|
||||
RewriteRule ^account_settings$ src/pages/other/account_settings.php [L]
|
||||
RewriteRule ^rescue_recovery$ src/pages/other/rescue_recovery.php [L]
|
||||
RewriteRule ^bush_mechanics$ src/pages/other/bush_mechanics.php [L]
|
||||
RewriteRule ^indemnity$ src/pages/other/indemnity.php [L]
|
||||
RewriteRule ^indemnity_waiver$ src/pages/other/indemnity_waiver.php [L]
|
||||
RewriteRule ^basic_indemnity$ src/pages/other/basic_indemnity.php [L]
|
||||
RewriteRule ^view_indemnity$ src/pages/other/view_indemnity.php [L]
|
||||
|
||||
# === ADMIN PAGES ===
|
||||
RewriteRule ^admin_members$ src/admin/admin_members.php [L]
|
||||
RewriteRule ^admin_payments$ src/admin/admin_payments.php [L]
|
||||
RewriteRule ^admin_web_users$ src/admin/admin_web_users.php [L]
|
||||
RewriteRule ^admin_events$ src/admin/admin_events.php [L]
|
||||
RewriteRule ^admin_course_bookings$ src/admin/admin_course_bookings.php [L]
|
||||
RewriteRule ^admin_camp_bookings$ src/admin/admin_camp_bookings.php [L]
|
||||
RewriteRule ^admin_trip_bookings$ src/admin/admin_trip_bookings.php [L]
|
||||
RewriteRule ^admin_visitors$ src/admin/admin_visitors.php [L]
|
||||
RewriteRule ^admin_efts$ src/admin/admin_efts.php [L]
|
||||
RewriteRule ^admin_trips$ src/admin/admin_trips.php [L]
|
||||
RewriteRule ^manage_events$ src/admin/manage_events.php [L]
|
||||
RewriteRule ^manage_trips$ src/admin/manage_trips.php [L]
|
||||
|
||||
# === API/AJAX ENDPOINTS ===
|
||||
RewriteRule ^fetch_users$ src/api/fetch_users.php [L]
|
||||
RewriteRule ^fetch_drinks$ src/api/fetch_drinks.php [L]
|
||||
RewriteRule ^fetch_bar_tabs$ src/api/fetch_bar_tabs.php [L]
|
||||
RewriteRule ^get_campsites$ src/api/get_campsites.php [L]
|
||||
RewriteRule ^get_tab_total$ src/api/get_tab_total.php [L]
|
||||
RewriteRule ^google_validate_login$ src/api/google_validate_login.php [L]
|
||||
|
||||
# === PROCESSORS ===
|
||||
RewriteRule ^validate_login$ src/processors/validate_login.php [L]
|
||||
RewriteRule ^register_user$ src/processors/register_user.php [L]
|
||||
RewriteRule ^process_application$ src/processors/process_application.php [L]
|
||||
RewriteRule ^process_booking$ src/processors/process_booking.php [L]
|
||||
RewriteRule ^process_camp_booking$ src/processors/process_camp_booking.php [L]
|
||||
RewriteRule ^process_course_booking$ src/processors/process_course_booking.php [L]
|
||||
RewriteRule ^process_trip_booking$ src/processors/process_trip_booking.php [L]
|
||||
RewriteRule ^process_membership_payment$ src/processors/process_membership_payment.php [L]
|
||||
RewriteRule ^process_payments$ src/processors/process_payments.php [L]
|
||||
RewriteRule ^process_eft$ src/processors/process_eft.php [L]
|
||||
RewriteRule ^submit_order$ src/processors/submit_order.php [L]
|
||||
RewriteRule ^submit_pop$ src/processors/submit_pop.php [L]
|
||||
RewriteRule ^process_signature$ src/processors/process_signature.php [L]
|
||||
RewriteRule ^create_bar_tab$ src/processors/create_bar_tab.php [L]
|
||||
RewriteRule ^update_application$ src/processors/update_application.php [L]
|
||||
RewriteRule ^update_user$ src/processors/update_user.php [L]
|
||||
RewriteRule ^upload_profile_picture$ src/processors/upload_profile_picture.php [L]
|
||||
RewriteRule ^send_reset_link$ src/processors/send_reset_link.php [L]
|
||||
RewriteRule ^logout$ src/processors/logout.php [L]
|
||||
RewriteRule ^process_trip$ src/processors/process_trip.php [L]
|
||||
RewriteRule ^process_event$ src/processors/process_event.php [L]
|
||||
RewriteRule ^toggle_trip_published$ src/processors/toggle_trip_published.php [L]
|
||||
RewriteRule ^toggle_event_published$ src/processors/toggle_event_published.php [L]
|
||||
RewriteRule ^delete_trip$ src/processors/delete_trip.php [L]
|
||||
RewriteRule ^delete_event$ src/processors/delete_event.php [L]
|
||||
RewriteRule ^save_album$ src/processors/save_album.php [L]
|
||||
RewriteRule ^update_album$ src/processors/update_album.php [L]
|
||||
RewriteRule ^delete_album$ src/processors/delete_album.php [L]
|
||||
RewriteRule ^delete_photo$ src/processors/delete_photo.php [L]
|
||||
RewriteRule ^get_album_photos$ src/processors/get_album_photos.php [L]
|
||||
RewriteRule ^link_membership_user$ src/processors/link_membership_user.php [L]
|
||||
RewriteRule ^unlink_membership_user$ src/processors/unlink_membership_user.php [L]
|
||||
|
||||
# Blog routes
|
||||
RewriteRule ^admin_blogs$ src/pages/blog/admin_blogs.php [L]
|
||||
RewriteRule ^user_blogs$ src/pages/blog/user_blogs.php [L]
|
||||
RewriteRule ^blog_read$ src/pages/blog/blog_read.php [L]
|
||||
RewriteRule ^blog_edit$ src/pages/blog/blog_edit.php [L]
|
||||
RewriteRule ^blog_create$ src/processors/blog/blog_create.php [L]
|
||||
RewriteRule ^blog_delete$ src/processors/blog/blog_delete.php [L]
|
||||
RewriteRule ^publish_blog$ src/processors/blog/publish_blog.php [L]
|
||||
RewriteRule ^blog_unpublish$ src/processors/blog/blog_unpublish.php [L]
|
||||
RewriteRule ^submit_blog$ src/processors/blog/submit_blog.php [L]
|
||||
RewriteRule ^upload_blog_image$ src/processors/blog/upload_blog_image.php [L]
|
||||
RewriteRule ^autosave$ src/processors/blog/autosave.php [L]
|
||||
|
||||
</IfModule>
|
||||
|
||||
php_flag display_errors On
|
||||
# php_value error_reporting -1
|
||||
RedirectMatch 403 ^/\.well-known
|
||||
Options -Indexes
|
||||
|
||||
<FilesMatch "\.(env|sql|bak|zip|tar|gz|ini)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
ErrorDocument 404 /404.php
|
||||
|
||||
<RequireAll>
|
||||
Require all granted
|
||||
Require not ip 4.222.252.98
|
||||
Require not ip 4.222.252.97
|
||||
</RequireAll>
|
||||
|
||||
<Files .env>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</Files>
|
||||
|
||||
|
||||
# ALL CUSTOM ENTRIES SHOULD GO ABOVE THIS LINE
|
||||
# BEGIN IWORX header
|
||||
# This file was created by InterWorx-CP
|
||||
# You may modify this file, but any changes made between
|
||||
# BEGIN IWORX and END IWORX tags may be lost on future
|
||||
# updates. Additionally, changes NOT made between these
|
||||
# tags will not be recognized in the SiteWorx interface.
|
||||
# END IWORX header
|
||||
|
||||
# BEGIN IWORX accesscontrol
|
||||
# END IWORX accesscontrol
|
||||
|
||||
# BEGIN IWORX errordocs
|
||||
# END IWORX errordocs
|
||||
|
||||
# BEGIN IWORX mimetypes
|
||||
# END IWORX mimetypes
|
||||
|
||||
# BEGIN IWORX handlers
|
||||
# END IWORX handlers
|
||||
|
||||
# BEGIN IWORX charset
|
||||
# END IWORX charset
|
||||
|
||||
# BEGIN IWORX redirects
|
||||
# END IWORX redirects
|
||||
|
||||
# BEGIN IWORX phpvars
|
||||
# END IWORX phpvars
|
||||
|
||||
# BEGIN IWORX dirindex
|
||||
# END IWORX dirindex
|
||||
|
||||
# BEGIN IWORX hotlink
|
||||
# END IWORX hotlink
|
||||
|
||||
# BEGIN IWORX passwordprotection
|
||||
# END IWORX passwordprotection
|
||||
|
||||
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';
|
||||
@@ -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; }
|
||||
|
||||
|
After Width: | Height: | Size: 494 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);
|
||||
}
|
||||
});
|
||||
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;
|
||||
11
header.php
@@ -258,6 +258,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="track-map">Track Map</a></li>
|
||||
<li><a href="trips">Trips</a>
|
||||
<?php if ($headerStyle === 'dark'): ?>
|
||||
<ul>
|
||||
@@ -319,8 +320,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>
|
||||
|
||||
212
index.php
@@ -4,6 +4,21 @@ $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;
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -54,7 +69,7 @@ if (!empty($bannerImages)) {
|
||||
<div style="padding-top: 50px; padding-bottom: 50px;">
|
||||
<img style="width: 250px; margin-bottom: 20px;" src="assets/images/logos/weblogo2.png" alt="Logo">
|
||||
<h1 class="hero-title" data-aos="flip-up" data-aos-delay="50" data-aos-duration="1500" data-aos-offset="50">
|
||||
Welcome to<br>the Four Wheel Drive Club<br>of Southern Africa
|
||||
Welcome to<br>the 4 Wheel Drive Club<br>of Southern Africa
|
||||
</h1>
|
||||
<a href="membership.php" class="theme-btn style-two bgc-secondary" style="margin-top: 20px; background-color: #e90000; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;">
|
||||
<span data-hover="Become a Member">Become a Member</span>
|
||||
@@ -636,8 +651,203 @@ 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'; ?>;
|
||||
|
||||
if (showModal) {
|
||||
// Show modal after a short delay for better UX
|
||||
setTimeout(function() {
|
||||
modal.style.display = 'flex';
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Close modal when X is clicked
|
||||
closeBtn.addEventListener('click', function() {
|
||||
modal.style.display = 'none';
|
||||
});
|
||||
|
||||
// Close modal when clicking outside the modal content
|
||||
modal.addEventListener('click', function(event) {
|
||||
if (event.target === modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 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</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;
|
||||
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;
|
||||
}
|
||||
|
||||
@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-content {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.updates-modal-header h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.update-item h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
48
progress.log
Normal file
@@ -0,0 +1,48 @@
|
||||
[2025-12-14 16:27:25] Testing Log Entry at 2025-12-14 16:27:25
|
||||
[2025-12-14 16:28:03] Testing Log Entry at 2025-12-14 16:28:03
|
||||
[2025-12-14 16:28:18] Testing Log Entry at 2025-12-14 16:28:18
|
||||
[2025-12-14 18:30:36] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 18:30:36] PATH: /src/api/ikhokha_webhook.php
|
||||
[2025-12-14 18:30:36] RAW BODY: {"paylinkID":"cwm225jy497j0qc","status":"SUCCESS","externalTransactionID":"693ee604c8e38","responseCode":"00","text":null}
|
||||
[2025-12-14 18:30:36] IK-SIGN: 3a2b8c9da4f1aff63c011adec10552450c5b6605f587e82ab1272914715fd4cf
|
||||
[2025-12-14 18:30:36] iKhokha webhook: signature mismatch
|
||||
[2025-12-14 18:30:36] EXPECTED SIGN: f8356ee6b609e9bd44221fa71851b06171bbb7523c52dca4bde605e3bd93f654
|
||||
[2025-12-14 18:30:37] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 18:30:37] PATH: /src/api/ikhokha_webhook.php
|
||||
[2025-12-14 18:30:37] RAW BODY: {"paylinkID":"cwm225jy497j0qc","status":"SUCCESS","externalTransactionID":"693ee604c8e38","responseCode":"00","text":null}
|
||||
[2025-12-14 18:30:37] IK-SIGN: 3a2b8c9da4f1aff63c011adec10552450c5b6605f587e82ab1272914715fd4cf
|
||||
[2025-12-14 18:30:37] iKhokha webhook: signature mismatch
|
||||
[2025-12-14 18:30:37] EXPECTED SIGN: f8356ee6b609e9bd44221fa71851b06171bbb7523c52dca4bde605e3bd93f654
|
||||
[2025-12-14 18:36:19] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 18:36:19] PATH: /api/ikhokha_webhook.php
|
||||
[2025-12-14 18:36:19] RAW BODY: {"paylinkID":"xx6225jyhx6x5n1","status":"SUCCESS","externalTransactionID":"693ee75cec963","responseCode":"00","text":null}
|
||||
[2025-12-14 18:36:19] IK-SIGN: 8fb11ceb8ea6b2cd6fec1773719927889f02764b0f04ad3c8543edbc9f74cf43
|
||||
[2025-12-14 18:36:19] iKhokha webhook: signature mismatch
|
||||
[2025-12-14 18:36:19] EXPECTED SIGN: 7a594957e7dc8775a6f9e1f8e3a1292f4dcd10268facaec57735f937b1cd9949
|
||||
[2025-12-14 18:36:19] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 18:36:19] PATH: /api/ikhokha_webhook.php
|
||||
[2025-12-14 18:36:19] RAW BODY: {"paylinkID":"xx6225jyhx6x5n1","status":"SUCCESS","externalTransactionID":"693ee75cec963","responseCode":"00","text":null}
|
||||
[2025-12-14 18:36:19] IK-SIGN: 8fb11ceb8ea6b2cd6fec1773719927889f02764b0f04ad3c8543edbc9f74cf43
|
||||
[2025-12-14 18:36:19] iKhokha webhook: signature mismatch
|
||||
[2025-12-14 18:36:19] EXPECTED SIGN: 7a594957e7dc8775a6f9e1f8e3a1292f4dcd10268facaec57735f937b1cd9949
|
||||
[2025-12-14 18:55:49] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 18:55:49] RAW BODY: {"paylinkID":"433225jzs9rvk5n","status":"SUCCESS","externalTransactionID":"693eebf1a9f75","responseCode":"00","text":null}
|
||||
[2025-12-14 18:55:49] IK-SIGN: 5cb01840f3516c964bdffc34cb1da41130bd668deffc2e4ea5a9f4cbb89b9807
|
||||
[2025-12-14 18:55:49] SIGN BASE STRING: | CONTEXT: https://beta.4wdcsa.co.za/src/api/ikhokha_webhook.php{"paylinkID":"433225jzs9rvk5n","status":"SUCCESS","externalTransactionID":"693eebf1a9f75","responseCode":"00","text":null}
|
||||
[2025-12-14 18:55:49] iKhokha webhook: signature mismatch
|
||||
[2025-12-14 18:55:49] EXPECTED SIGN: 75c491d05ac5006b3ad4fcec20c870e860a66ea03ed5a3e8900d6e29cd601445
|
||||
[2025-12-14 18:55:49] RECEIVED SIGN: 5cb01840f3516c964bdffc34cb1da41130bd668deffc2e4ea5a9f4cbb89b9807
|
||||
[2025-12-14 18:55:49] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 18:55:49] RAW BODY: {"paylinkID":"433225jzs9rvk5n","status":"SUCCESS","externalTransactionID":"693eebf1a9f75","responseCode":"00","text":null}
|
||||
[2025-12-14 18:55:49] IK-SIGN: 5cb01840f3516c964bdffc34cb1da41130bd668deffc2e4ea5a9f4cbb89b9807
|
||||
[2025-12-14 18:55:49] SIGN BASE STRING: | CONTEXT: https://beta.4wdcsa.co.za/src/api/ikhokha_webhook.php{"paylinkID":"433225jzs9rvk5n","status":"SUCCESS","externalTransactionID":"693eebf1a9f75","responseCode":"00","text":null}
|
||||
[2025-12-14 18:55:49] iKhokha webhook: signature mismatch
|
||||
[2025-12-14 18:55:49] EXPECTED SIGN: 75c491d05ac5006b3ad4fcec20c870e860a66ea03ed5a3e8900d6e29cd601445
|
||||
[2025-12-14 18:55:49] RECEIVED SIGN: 5cb01840f3516c964bdffc34cb1da41130bd668deffc2e4ea5a9f4cbb89b9807
|
||||
[2025-12-14 20:15:55] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 20:15:55] RAW BODY: {"paylinkID":"ys5225k4z56x0mm","status":"SUCCESS","externalTransactionID":"693efeaca71a9","responseCode":"00","text":null}
|
||||
[2025-12-14 20:15:55] IK-SIGN: bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557
|
||||
[2025-12-14 20:15:55] ⚠️ IKHOKHA SIGNATURE CHECK BYPASSED
|
||||
[2025-12-14 20:15:55] Parsed externalTransactionID: 693efeaca71a9
|
||||
[2025-12-14 20:15:55] Parsed providerPaymentId: ys5225k4z56x0mm
|
||||
[2025-12-14 20:15:55] Parsed providerStatus: SUCCESS
|
||||
@@ -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>
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
include_once($rootPath . '/header.php');
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
require_once($rootPath . "/header.php");
|
||||
checkAdmin();
|
||||
checkUserSession();
|
||||
|
||||
$pageTitle = 'Manage Events';
|
||||
$breadcrumbs = [['Home' => 'index']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
|
||||
// Fetch all events
|
||||
$events_query = "
|
||||
SELECT
|
||||
event_id, name, type, location, date, published
|
||||
event_id, name, type, location, date, image, published
|
||||
FROM events
|
||||
ORDER BY date DESC
|
||||
";
|
||||
@@ -22,340 +29,202 @@ if ($result && $result->num_rows > 0) {
|
||||
?>
|
||||
|
||||
<style>
|
||||
table {
|
||||
.image {
|
||||
width: 300px;
|
||||
/* Set your desired width */
|
||||
height: 250px;
|
||||
/* Set your desired height */
|
||||
overflow: hidden;
|
||||
/* Hide any overflow */
|
||||
display: block;
|
||||
/* Ensure proper block behavior */
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image img {
|
||||
width: 100%;
|
||||
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;
|
||||
/* Image scales to fill the container */
|
||||
height: 100%;
|
||||
/* Image scales to fill the container */
|
||||
object-fit: cover;
|
||||
/* Fills the container while maintaining aspect ratio */
|
||||
object-position: center;
|
||||
/* Aligns the center of the image with the center of the container */
|
||||
display: block;
|
||||
/* Prevents inline whitespace issues */
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<script>
|
||||
$(document).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'));
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const filterInput = document.querySelector('.filter-input');
|
||||
const cards = document.querySelectorAll('.destination-item');
|
||||
|
||||
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 (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";
|
||||
});
|
||||
});
|
||||
|
||||
// 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 => '']];
|
||||
$breadcrumbs = [['Home' => 'index']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
|
||||
<!-- Events Management Area start -->
|
||||
<section class="events-management-area py-100 rel z-1">
|
||||
<section class="blog-list-page py-100 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row mb-30">
|
||||
<div class="row">
|
||||
<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 style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
||||
<h2 style="margin: 0;">Manage Events</h2>
|
||||
<a href="manage_events" class="theme-btn create-album-btn">
|
||||
<i class="far fa-plus"></i> New Event
|
||||
</a>
|
||||
</div>
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-warning message-box">
|
||||
<?php echo $_SESSION['message']; ?>
|
||||
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||
</div>
|
||||
<?php unset($_SESSION['message']);
|
||||
endif;
|
||||
|
||||
if (count($events) > 0) {
|
||||
echo '<input type="text" class="filter-input" placeholder="Filter events...">';
|
||||
echo '<div class="events-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
|
||||
|
||||
foreach ($events as $event) {
|
||||
$eventImagePath = $event['image'] ? htmlspecialchars($event['image']) : 'assets/images/placeholder.jpg';
|
||||
$publishStatusBadge = $event['published'] == 1 ? 'PUBLISHED' : 'DRAFT';
|
||||
|
||||
echo '
|
||||
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image" style="width:300px;height:250px;">
|
||||
<img src="' . $eventImagePath . '" alt="' . htmlspecialchars($event['name']) . '">
|
||||
</div>
|
||||
<div class="content" style="width:100%;">
|
||||
<div class="destination-header d-flex align-items-start gap-3">
|
||||
<div>
|
||||
<span class="badge bg-dark mb-1">' . strtoupper($publishStatusBadge) . '</span>
|
||||
<h5 class="mb-0">' . htmlspecialchars($event['name']) . '</h5>
|
||||
<small class="text-muted">📍 ' . htmlspecialchars($event['location']) . '</small>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin: 10px 0;">
|
||||
<strong>Type:</strong> ' . htmlspecialchars($event['type']) . '<br>
|
||||
<strong>Date:</strong> ' . convertDate($event['date']) . '
|
||||
</p>
|
||||
<div class="destination-footer">
|
||||
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
||||
<a href="manage_events?event_id=' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
||||
<button type="button" class="toggle-publish" data-event-id="' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($event['published'] == 1 ? 'Unpublish' : 'Publish') . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($event['published'] == 1 ? 'cloud_off' : 'cloud_upload') . '</span></button>
|
||||
<button type="button" class="delete-event" data-event-id="' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>';
|
||||
}
|
||||
?>
|
||||
';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<div class="no-events">
|
||||
<p>No events found. <a href="manage_events">Create one</a></p>
|
||||
</div>';
|
||||
}
|
||||
?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Events Management Area end -->
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<script>
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
|
||||
|
||||
// Handle publish/unpublish button clicks
|
||||
document.querySelectorAll('.toggle-publish').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const eventId = this.dataset.eventId;
|
||||
const currentIcon = this.querySelector('.material-icons').textContent;
|
||||
const isPublished = currentIcon === 'cloud_off';
|
||||
const action = isPublished ? 'Unpublish' : 'Publish';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('event_id', eventId);
|
||||
|
||||
fetch('toggle_event_published', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
alert(action + ' successful!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert(action + ' failed: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error:', err);
|
||||
alert(action + ' failed due to network error.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Handle delete button clicks
|
||||
document.querySelectorAll('.delete-event').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
if (!confirm('Are you sure you want to delete this event? This action cannot be undone.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const eventId = this.dataset.eventId;
|
||||
const card = this.closest('.destination-item');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('event_id', eventId);
|
||||
|
||||
fetch('delete_event', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
card.style.animation = 'fadeOut 0.3s ease-out';
|
||||
setTimeout(() => {
|
||||
card.remove();
|
||||
if (document.querySelectorAll('.destination-item').length === 0) {
|
||||
location.reload();
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error:', err);
|
||||
alert('Delete failed due to network error.');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
include_once($rootPath . '/header.php');
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
require_once($rootPath . "/header.php");
|
||||
checkAdmin();
|
||||
checkUserSession();
|
||||
|
||||
$pageTitle = 'Manage Trips';
|
||||
$breadcrumbs = [['Home' => 'index']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
|
||||
// Fetch all trips with booking status
|
||||
$trips_query = "
|
||||
SELECT
|
||||
trip_id, trip_name, location, start_date, end_date,
|
||||
vehicle_capacity, places_booked, cost_members, published
|
||||
vehicle_capacity, places_booked, cost_members, cost_nonmembers,
|
||||
cost_pensioner_member, cost_pensioner, published
|
||||
FROM trips
|
||||
ORDER BY start_date DESC
|
||||
";
|
||||
@@ -23,131 +31,48 @@ if ($result && $result->num_rows > 0) {
|
||||
?>
|
||||
|
||||
<style>
|
||||
table {
|
||||
.image {
|
||||
width: 200px;
|
||||
/* Set your desired width */
|
||||
height: 200px;
|
||||
/* Set your desired height */
|
||||
overflow: hidden;
|
||||
/* Hide any overflow */
|
||||
display: block;
|
||||
/* Ensure proper block behavior */
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image img {
|
||||
width: 100%;
|
||||
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;
|
||||
/* Image scales to fill the container */
|
||||
height: 100%;
|
||||
/* Image scales to fill the container */
|
||||
object-fit: cover;
|
||||
/* Fills the container while maintaining aspect ratio */
|
||||
object-position: center;
|
||||
/* Aligns the center of the image with the center of the container */
|
||||
display: block;
|
||||
/* Prevents inline whitespace issues */
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const 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;
|
||||
const filterInput = document.querySelector('.filter-input');
|
||||
const cards = document.querySelectorAll('.destination-item');
|
||||
|
||||
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 (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";
|
||||
});
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
@@ -155,164 +80,163 @@ if ($result && $result->num_rows > 0) {
|
||||
$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">
|
||||
<section class="blog-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>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
|
||||
<?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 style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
||||
<h2 style="margin: 0;">Manage Trips</h2>
|
||||
<a href="manage_trips" class="theme-btn create-album-btn">
|
||||
<i class="far fa-plus"></i> New Event
|
||||
</a>
|
||||
</div>
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-warning message-box">
|
||||
<?php echo $_SESSION['message']; ?>
|
||||
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||
</div>
|
||||
<?php unset($_SESSION['message']);
|
||||
endif;
|
||||
if (count($trips) > 0) {
|
||||
echo '<input type="text" class="filter-input" placeholder="Filter trips...">';
|
||||
echo '<div class="trips-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
|
||||
|
||||
foreach ($trips as $trip) {
|
||||
$available = $trip['vehicle_capacity'] - $trip['places_booked'];
|
||||
$publishStatus = $trip['published'] == 1 ? 'published' : 'draft';
|
||||
$publishStatusBadge = $trip['published'] == 1 ? 'PUBLISHED' : 'DRAFT';
|
||||
|
||||
// Get trip image - look for assets/images/trips/$trip_id_{number}.jpg
|
||||
$tripImagePath = '';
|
||||
$tripImagesGlob = glob($rootPath . '/assets/images/trips/' . $trip['trip_id'] . '_*.jpg');
|
||||
if (!empty($tripImagesGlob)) {
|
||||
$tripImagePath = str_replace($rootPath, '', $tripImagesGlob[0]);
|
||||
} else {
|
||||
// Fallback to placeholder icon if no image found
|
||||
$tripImagePath = 'assets/images/placeholder.jpg';
|
||||
}
|
||||
|
||||
echo '
|
||||
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image" style="width:300px;height:250px;">
|
||||
<img src="' . htmlspecialchars($tripImagePath) . '" alt="' . htmlspecialchars($trip['trip_name']) . '">
|
||||
</div>
|
||||
<div class="content" style="width:100%;">
|
||||
<div class="destination-header d-flex align-items-start gap-3">
|
||||
<div>
|
||||
<span class="badge bg-dark mb-1">' . strtoupper($publishStatusBadge) . '</span>
|
||||
<h5 class="mb-0">' . htmlspecialchars($trip['trip_name']) . '</h5>
|
||||
<small class="text-muted">📍 ' . htmlspecialchars($trip['location']) . '</small>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin: 10px 0;">
|
||||
<strong>Dates:</strong> ' . date('M d', strtotime($trip['start_date'])) . ' - ' . date('M d, Y', strtotime($trip['end_date'])) . '<br>
|
||||
<strong>Capacity:</strong> ' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '<br>
|
||||
<strong>Costs:</strong> Members: R ' . number_format($trip['cost_members'], 2) . ' | Non-Members: R ' . number_format($trip['cost_nonmembers'], 2) . ' | Pensioner Members: R ' . number_format($trip['cost_pensioner_member'], 2) . ' | Pensioners: R ' . number_format($trip['cost_pensioner'], 2) . '
|
||||
</p>
|
||||
<div class="destination-footer">
|
||||
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
||||
<a href="manage_trips?trip_id=' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
||||
<button type="button" class="toggle-publish" data-trip-id="' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($trip['published'] == 1 ? 'Unpublish' : 'Publish') . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($trip['published'] == 1 ? 'cloud_off' : 'cloud_upload') . '</span></button>
|
||||
<button type="button" class="delete-trip" data-trip-id="' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<div class="no-trips">
|
||||
<p>No trips found. <a href="manage_trips">Create one</a></p>
|
||||
</div>';
|
||||
}
|
||||
?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Trips Management Area end -->
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.toggle-publish').on('click', function() {
|
||||
var tripId = $(this).data('trip-id');
|
||||
var button = $(this);
|
||||
var row = button.closest('tr');
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
|
||||
|
||||
$.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>');
|
||||
}
|
||||
// Handle publish/unpublish button clicks
|
||||
document.querySelectorAll('.toggle-publish').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const tripId = this.dataset.tripId;
|
||||
const currentIcon = this.querySelector('.material-icons').textContent;
|
||||
const isPublished = currentIcon === 'cloud_off';
|
||||
const action = isPublished ? 'Unpublish' : 'Publish';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('trip_id', tripId);
|
||||
|
||||
fetch('toggle_trip_published', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
alert(action + ' successful!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + response.message);
|
||||
alert(action + ' failed: ' + data.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error updating trip status');
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error:', err);
|
||||
alert(action + ' failed due to network error.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('.delete-trip').on('click', function() {
|
||||
// Handle delete button clicks
|
||||
document.querySelectorAll('.delete-trip').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
if (!confirm('Are you sure you want to delete this trip? This action cannot be undone.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var tripId = $(this).data('trip-id');
|
||||
var button = $(this);
|
||||
var row = button.closest('tr');
|
||||
const tripId = this.dataset.tripId;
|
||||
const card = this.closest('.destination-item');
|
||||
|
||||
$.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) {
|
||||
const formData = new FormData();
|
||||
formData.append('trip_id', tripId);
|
||||
|
||||
fetch('delete_trip', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
alert('Trip deleted successfully!');
|
||||
card.style.animation = 'fadeOut 0.3s ease-out';
|
||||
setTimeout(() => {
|
||||
card.remove();
|
||||
if (document.querySelectorAll('.destination-item').length === 0) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
} else {
|
||||
alert('Error: ' + response.message);
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error deleting trip');
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error:', err);
|
||||
alert('Delete failed due to network error.');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -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();
|
||||
274
src/api/ikhokha_webhook.php
Normal file
@@ -0,0 +1,274 @@
|
||||
<?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");
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Read raw request and headers (DO NOT MODIFY RAW BODY)
|
||||
* ==========================================================
|
||||
*/
|
||||
$raw = file_get_contents('php://input');
|
||||
|
||||
if ($raw === false) {
|
||||
http_response_code(400);
|
||||
progress_log('iKhokha webhook: unable to read raw input');
|
||||
exit('No body');
|
||||
}
|
||||
|
||||
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
||||
$headers = array_change_key_case($headers, CASE_LOWER);
|
||||
|
||||
$ikSign = $headers['ik-sign'] ?? null;
|
||||
$ikAppId = $headers['ik-appid'] ?? null;
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Basic header presence check
|
||||
* ==========================================================
|
||||
*/
|
||||
if (empty($ikSign) || empty($ikAppId)) {
|
||||
http_response_code(400);
|
||||
progress_log('iKhokha webhook: missing IK-SIGN or IK-APPID');
|
||||
exit('Missing headers');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Signature verification
|
||||
* HMAC_SHA256( path + raw_body, app_secret )
|
||||
* ==========================================================
|
||||
*/
|
||||
$secret = $_ENV['IKHOKHA_APP_SECRET'] ?? null;
|
||||
|
||||
if (empty($secret)) {
|
||||
http_response_code(500);
|
||||
progress_log('iKhokha webhook: app secret not configured');
|
||||
exit('Server misconfigured');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Debug logging (disable once stable)
|
||||
progress_log('--- iKhokha WEBHOOK DEBUG ---');
|
||||
progress_log('RAW BODY: ' . $raw);
|
||||
progress_log('IK-SIGN: ' . $ikSign);
|
||||
|
||||
$callbackUrl = $_ENV['IKHOKHA_CALLBACK_URL'] ?? null;
|
||||
$bypass = ($_ENV['IKHOKHA_BYPASS_SIGNATURE'] ?? 'false') === 'true';
|
||||
|
||||
if (!$bypass) {
|
||||
|
||||
if (empty($callbackUrl)) {
|
||||
http_response_code(500);
|
||||
progress_log('iKhokha webhook: callback URL not configured');
|
||||
exit('Server misconfigured');
|
||||
}
|
||||
|
||||
$expected = hash_hmac(
|
||||
'sha256',
|
||||
$callbackUrl . $raw,
|
||||
$_ENV['IKHOKHA_APP_SECRET']
|
||||
);
|
||||
|
||||
if (!hash_equals($expected, $ikSign)) {
|
||||
http_response_code(403);
|
||||
progress_log('iKhokha webhook: signature mismatch');
|
||||
progress_log('EXPECTED SIGN: ' . $expected);
|
||||
progress_log('RECEIVED SIGN: ' . $ikSign);
|
||||
exit('Invalid signature');
|
||||
}
|
||||
|
||||
} else {
|
||||
progress_log('⚠️ IKHOKHA SIGNATURE CHECK BYPASSED');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Decode payload
|
||||
* ==========================================================
|
||||
*/
|
||||
$payload = json_decode($raw, true);
|
||||
|
||||
if (!is_array($payload)) {
|
||||
http_response_code(400);
|
||||
progress_log('iKhokha webhook: invalid JSON');
|
||||
exit('Invalid JSON');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Extract data safely (iKhokha is inconsistent)
|
||||
* ==========================================================
|
||||
*/
|
||||
$data = $payload['data'] ?? $payload;
|
||||
|
||||
$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: ' . print_r($providerStatus, true));
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Locate local 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()) {
|
||||
$localPaymentId = $row['payment_id'];
|
||||
$booking_id = $row['booking_id'];
|
||||
$user_id = $row['user_id'];
|
||||
$description = $row['description'];
|
||||
}
|
||||
$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()) {
|
||||
$localPaymentId = $row['payment_id'];
|
||||
$booking_id = $row['booking_id'];
|
||||
$user_id = $row['user_id'];
|
||||
$description = $row['description'];
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$localPaymentId) {
|
||||
http_response_code(404);
|
||||
progress_log('iKhokha webhook: payment not found');
|
||||
progress_log(json_encode([$externalTransactionID, $providerPaymentId]));
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Normalize status and apply business logic
|
||||
* ==========================================================
|
||||
*/
|
||||
$normalized = strtoupper(trim((string)$providerStatus));
|
||||
|
||||
if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) {
|
||||
|
||||
// Mark payment as PAID
|
||||
$setPaid = $conn->prepare(
|
||||
"UPDATE payments SET status = 'PAID' WHERE payment_id = ?"
|
||||
);
|
||||
if ($setPaid) {
|
||||
$setPaid->bind_param('s', $localPaymentId);
|
||||
$setPaid->execute();
|
||||
$setPaid->close();
|
||||
}
|
||||
|
||||
// Booking or membership update
|
||||
if (!empty($booking_id)) {
|
||||
$upd = $conn->prepare(
|
||||
"UPDATE bookings SET status = 'PAID' WHERE booking_id = ?"
|
||||
);
|
||||
if ($upd) {
|
||||
$upd->bind_param('i', $booking_id);
|
||||
$upd->execute();
|
||||
$upd->close();
|
||||
sendAdminNotification('4WDCSA.co.za - New Booking - '.getFullName($user_id) , 'We have received a payment for a new booking for '.$description.' from '.getFullName($user_id));
|
||||
}
|
||||
} else {
|
||||
$upd = $conn->prepare(
|
||||
"UPDATE membership_fees
|
||||
SET payment_status = 'PAID'
|
||||
WHERE payment_id = ?"
|
||||
);
|
||||
if ($upd) {
|
||||
$upd->bind_param('s', $localPaymentId);
|
||||
$upd->execute();
|
||||
$upd->close();
|
||||
sendAdminNotification('4WDCSA.co.za - New Membership Application - '.getFullName($user_id) , 'A new member has signed up, '.getFullName($user_id));
|
||||
}
|
||||
}
|
||||
|
||||
// Send confirmation email
|
||||
if (!empty($user_id)) {
|
||||
sendPaymentConfirmation(
|
||||
getEmail($user_id),
|
||||
getFullName($user_id),
|
||||
$description
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Acknowledge webhook
|
||||
* ==========================================================
|
||||
*/
|
||||
http_response_code(200);
|
||||
echo 'OK';
|
||||
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
|
||||
@@ -29,6 +29,59 @@ function openDatabaseConnection()
|
||||
return $conn;
|
||||
}
|
||||
|
||||
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 +517,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 +540,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 +549,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 +765,120 @@ 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 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 processMembershipPayment($payment_id, $amount, $description)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
@@ -1719,12 +1893,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 +2138,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,18 +2625,21 @@ 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
|
||||
|
||||
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (!in_array($mimeType, $config['mimeTypes'], true)) {
|
||||
error_log("Invalid MIME type '$mimeType' for type: $fileType. Expected: " . implode(', ', $config['mimeTypes']));
|
||||
return false;
|
||||
if (function_exists('finfo_open')) {
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
if ($finfo !== false) {
|
||||
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (!in_array($mimeType, $config['mimeTypes'], true)) {
|
||||
error_log("Invalid MIME type '$mimeType' for type: $fileType. Expected: " . implode(', ', $config['mimeTypes']));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CHECK 6: Additional Image Validation (for images) =====
|
||||
@@ -3189,3 +3381,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;
|
||||
}
|
||||
|
||||
|
||||
1
src/logs/db_errors.log
Normal file
@@ -0,0 +1 @@
|
||||
Database Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directory
|
||||
@@ -76,8 +76,8 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
|
||||
<h2>Manage Blog Posts</h2>
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<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>
|
||||
@@ -90,7 +90,7 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||
// 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 '
|
||||
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"]) . '">
|
||||
@@ -107,58 +107,58 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||
<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>
|
||||
<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>
|
||||
<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));
|
||||
<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';
|
||||
// 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);
|
||||
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.');
|
||||
});
|
||||
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>
|
||||
|
||||
|
||||
|
||||
@@ -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 false;
|
||||
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');
|
||||
@@ -19,63 +32,58 @@ $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 {
|
||||
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 */
|
||||
.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">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
|
||||
<h2>My 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>
|
||||
<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']);
|
||||
endif;
|
||||
|
||||
<?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 '
|
||||
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%;">
|
||||
@@ -88,25 +96,25 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||
<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>
|
||||
<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>
|
||||
<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>';
|
||||
endwhile; ?>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Blog List Area end -->
|
||||
<script>
|
||||
<script>
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
|
||||
|
||||
@@ -122,22 +130,22 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||
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.');
|
||||
});
|
||||
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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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='PAY NOW'>PENDING RENEWAL</span></a></td>
|
||||
<?php } else { ?>
|
||||
<td><?php echo htmlspecialchars($membership['payment_status']); ?></td>
|
||||
<?php } ?>
|
||||
@@ -204,16 +206,26 @@ 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)) {
|
||||
echo '
|
||||
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;">
|
||||
<span data-hover="Renew Membership">Renew Membership</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
|
||||
@@ -67,7 +67,25 @@ $stmt->bind_result($user_email);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
$conn->close();
|
||||
// If request includes payment_id, fetch provider paylink from payments table
|
||||
$payment_id = $_GET['payment_id'] ?? null;
|
||||
$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 +101,25 @@ $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>
|
||||
<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 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>
|
||||
<p style="margin-top:10px;">You will be redirected to iKhokha's Secure Payment Gateway.</p>
|
||||
<?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,83 @@
|
||||
<?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');
|
||||
|
||||
$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);
|
||||
$is_logged_in = isset($_SESSION['user_id']);
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
} else {
|
||||
header('Location: login.php');
|
||||
exit(); // Stop further script execution
|
||||
}
|
||||
|
||||
$payment_amount = 2600; // Assuming a fixed membership fee, adjust as needed
|
||||
|
||||
$payment_id = uniqid();
|
||||
$payment_amount = getPriceByDescription('membership_fees');
|
||||
$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);
|
||||
|
||||
$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;
|
||||
if ($paylink) {
|
||||
header('Location: membership_payment?payment_id=' . $payment_id);
|
||||
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 = [
|
||||
|
||||
@@ -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.php?payment_id=' + encodeURIComponent(response.payment_id);
|
||||
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.payment_id);
|
||||
// }, 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>');
|
||||
|
||||
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'); ?>
|
||||
660
src/pages/track-map.php
Normal file
@@ -0,0 +1,660 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = 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>
|
||||
.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: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.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 Training Track';
|
||||
$breadcrumbs = [['Home' => 'index.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
|
||||
<!-- Track Map Section -->
|
||||
<section class="track-map-section">
|
||||
<div class="container">
|
||||
<div class="track-info-box">
|
||||
<h3>BASE4 4x4 Training Track</h3>
|
||||
<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>
|
||||
|
||||
<?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(); ?>
|
||||
@@ -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
|
||||
|
||||
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()]);
|
||||
}
|
||||
@@ -6,9 +6,17 @@ require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
$eft_id = strtoupper($user_id." SUBS ".date("Y")." ".getInitialSurname($user_id));
|
||||
$payment_id = uniqid();
|
||||
$status = 'AWAITING PAYMENT';
|
||||
$description = 'Membership Fees '.date("Y")." ".getInitialSurname($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
|
||||
@@ -174,38 +182,45 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
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();
|
||||
$month = (int)$today->format('n');
|
||||
$year = (int)$today->format('Y');
|
||||
$payment_date = $today->format('Y-m-d');
|
||||
$membership_start_date = $payment_date;
|
||||
|
||||
// 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;
|
||||
if ($month == 12 || $month == 1 || $month == 2) {
|
||||
// December, January, February: charge full fee, valid till end of next Feb
|
||||
$payment_amount = getPriceByDescription('membership_fees');
|
||||
// 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 {
|
||||
// Otherwise, this year's February
|
||||
$year = $today->format('Y');
|
||||
// Prorata for Mar-Nov
|
||||
$payment_amount = calculateProrata(getPriceByDescription('pro_rata'));
|
||||
// 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');
|
||||
}
|
||||
|
||||
// Handle leap year (Feb 29) automatically
|
||||
$membership_end_date = (new DateTime("$year-02-01"))
|
||||
->modify('last day of this month')
|
||||
->format('Y-m-d');
|
||||
|
||||
$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);
|
||||
VALUES (?, ?, ?, ?, ?, 'AWAITING PAYMENT', ?)");
|
||||
$stmt->bind_param("idssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $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);
|
||||
header("Location: indemnity");
|
||||
// Success message
|
||||
$response = [
|
||||
|
||||
@@ -94,6 +94,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$status = "AWAITING PAYMENT";
|
||||
$type = 'course';
|
||||
$payment_id = uniqid();
|
||||
$publicRef = bin2hex(random_bytes(16));
|
||||
$num_vehicles = 1;
|
||||
$discountAmount = 0;
|
||||
$eft_id = strtoupper("COURSE ".date("m-d", strtotime($date))." ".getInitialSurname($user_id));
|
||||
@@ -125,11 +126,31 @@ 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)
|
||||
// sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
sendAdminNotification('New Course Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||
header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
} 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();
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Convert $membership_end_date to a DateTime object
|
||||
$membership_end_date_obj = DateTime::createFromFormat('Y-m-d', $membership_end_date);
|
||||
// Create iKhokha payment link
|
||||
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
||||
|
||||
// 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")
|
||||
) {
|
||||
// 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.');
|
||||
|
||||
// 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
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -59,13 +59,96 @@ if (isset($_POST['signature'])) {
|
||||
// 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'] ?? uniqid('mem_', true);
|
||||
|
||||
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);
|
||||
$ins->execute();
|
||||
$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;
|
||||
} catch (Exception $e) {
|
||||
// Log but do not fail signature save
|
||||
error_log('iKhokha create 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
|
||||
];
|
||||
if (!empty($paylink)) {
|
||||
$response['paylinkUrl'] = $paylink;
|
||||
}
|
||||
if (!empty($payment_id)) {
|
||||
$response['payment_id'] = $payment_id;
|
||||
}
|
||||
echo json_encode($response);
|
||||
} else {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Database update failed']);
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -78,6 +78,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
|
||||
@@ -104,6 +106,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$description = $trip_name;
|
||||
$type = 'trip';
|
||||
$payment_id = uniqid();
|
||||
$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));
|
||||
|
||||
@@ -131,11 +134,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);
|
||||
sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||
header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
// 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
|
||||
// sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
} 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 extension
|
||||
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
// Validate file
|
||||
if (!in_array($fileMime, $allowedMimes)) {
|
||||
throw new Exception('Invalid cover image file type');
|
||||
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 extension
|
||||
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
// Validate file
|
||||
if (!in_array($fileMime, $allowedMimes)) {
|
||||
throw new Exception('Invalid file type: ' . $fileName);
|
||||
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,25 +76,29 @@ 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 = ?");
|
||||
@@ -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
|
||||
|
||||
41
test.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
require_once("./src/config/env.php");
|
||||
|
||||
/**
|
||||
* EXACT escape function from iKhokha docs
|
||||
*/
|
||||
function escapeString($str) {
|
||||
$escaped = preg_replace(
|
||||
['/[\\"\'\"]/u', '/\x00/'],
|
||||
['\\\\$0', '\\0'],
|
||||
(string)$str
|
||||
);
|
||||
$cleaned = str_replace('\/', '/', $escaped);
|
||||
return $cleaned;
|
||||
}
|
||||
|
||||
$callbackUrl = $_ENV['IKHOKHA_CALLBACK_URL'] ?? null;
|
||||
$path = '/src/api/ikhokha_webhook.php';
|
||||
$secret = $_ENV['IKHOKHA_APP_SECRET'] ?? null;
|
||||
|
||||
// Simulated raw webhook body (EXACT, no whitespace changes)
|
||||
$raw = '{"paylinkID":"ys5225k4z56x0mm","status":"SUCCESS","externalTransactionID":"693efeaca71a9","responseCode":"00","text":null}';
|
||||
|
||||
echo "<strong>IK-SIGN FROM WEBHOOK:</strong><br>";
|
||||
echo "bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557<br><br>";
|
||||
|
||||
$payloadToSign = $path . $raw;
|
||||
|
||||
// Generate signature using hash_hmac directly on the constructed string
|
||||
$expected = hash_hmac('sha256', $payloadToSign, $secret);
|
||||
|
||||
// --- Output debug info (UPDATED) ---
|
||||
echo "<strong>DEBUG INFO</strong><br>";
|
||||
echo "Callback URL: $callbackUrl<br><br>";
|
||||
|
||||
echo "<strong>Payload to Sign (Un-escaped):</strong><br>";
|
||||
echo htmlspecialchars($payloadToSign) . "<br><br>";
|
||||
|
||||
echo "<strong>EXPECTED SIGNATURE:</strong><br>";
|
||||
echo $expected . "<br>";
|
||||
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 |
BIN
uploads/pop/122_SUBS_2025_M._BUYS_.pdf
Normal file
BIN
uploads/pop/127_SUBS_2025_J._MATTHEUS.pdf
Normal file
BIN
uploads/pop/129_SUBS_2025_C._DE_JESUS.pdf
Normal file
BIN
uploads/pop/130_SUBS_2025_J._HALL.pdf
Normal file
BIN
uploads/pop/134_SUBS_2025_J._EARLE.pdf
Normal file
BIN
uploads/pop/142_SUBS_2025_N._COETZEE.pdf
Normal file
BIN
uploads/pop/COURSE_07-26_D._KLADIS.pdf
Normal file
BIN
uploads/pop/COURSE_07-26_K._SKEE.pdf
Normal file
BIN
uploads/pop/COURSE_07-26_M._MABASO.pdf
Normal file
BIN
uploads/pop/COURSE_07-26_M._NICHOLLS.pdf
Normal file
BIN
uploads/pop/COURSE_08-23_A._FERENCZY_.pdf
Normal file
BIN
uploads/pop/COURSE_08-23_E._HOLTZHAUSEN.pdf
Normal file
BIN
uploads/pop/COURSE_09-20_I._KOORSEN.pdf
Normal file
BIN
uploads/signatures/signature_103.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
uploads/signatures/signature_105.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
uploads/signatures/signature_109.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
uploads/signatures/signature_122.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
uploads/signatures/signature_123.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
uploads/signatures/signature_126.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
uploads/signatures/signature_127.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
uploads/signatures/signature_129.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
uploads/signatures/signature_130.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
uploads/signatures/signature_131.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
uploads/signatures/signature_134.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
uploads/signatures/signature_136.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
uploads/signatures/signature_137.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
uploads/signatures/signature_142.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
uploads/signatures/signature_143.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
uploads/signatures/signature_144.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
uploads/signatures/signature_147.png
Normal file
|
After Width: | Height: | Size: 14 KiB |