19 Commits

Author SHA1 Message Date
twotalesanimation
ebd7efe21c added auto course codes 2025-12-15 18:07:11 +02:00
twotalesanimation
6ff20c1ffc added admin course creation 2025-12-15 16:57:11 +02:00
twotalesanimation
35c177b11d htacces update 2025-12-15 15:52:25 +02:00
twotalesanimation
acd7f563b1 added transaction table, fixed signature auth. Monitor for bugs before rmoving bypass 2025-12-15 15:51:11 +02:00
twotalesanimation
5768d8a7af Merge branch 'ikhokha2' into main 2025-12-15 10:54:31 +02:00
twotalesanimation
0e6ecd127f post auditlog implementation for bookings and payments 2025-12-15 10:52:09 +02:00
twotalesanimation
702e04e9bf pre auditlog implementations 2025-12-15 10:44:56 +02:00
twotalesanimation
d2c99e86b4 mostly complete payment system 2025-12-15 10:18:25 +02:00
twotalesanimation
f4934e9c13 iKhokha integration completerer... 2025-12-15 01:24:56 +02:00
twotalesanimation
477c2f2e04 iKhokha integration complete 2025-12-15 00:36:34 +02:00
twotalesanimation
a66382661d Fixed some bugs 2025-12-13 19:25:47 +02:00
twotalesanimation
32e50ffc39 Commit since isp push 2025-12-13 14:33:23 +02:00
twotalesanimation
cce181e2d0 Add interactive Base 4 track map with Leaflet.js
- Created new track-map page with aerial image and SVG overlay
- Implemented custom rotated square markers with obstacle numbers
- Added admin edit mode for placing and repositioning markers
- Database migration for track_obstacles table
- Modal form for adding new obstacles (replaces browser alerts)
- Drag-to-reposition functionality with auto-save
- Color-coded markers (green/red/black/split) for difficulty levels
- Clickable popups showing obstacle details
- Added track-map to navigation menu and sitemap
- URL rewrite rule for clean /track-map URL
2025-12-12 12:00:20 +02:00
twotalesanimation
48ee7592b2 Reorganize event processors and update routing
- Move process_event.php from src/admin to src/processors
- Move toggle_event_published.php from src/admin to src/processors
- Move delete_event.php from src/admin to src/processors
- Update .htaccess rewrite rules to point event processors to correct location
- Keep admin_events.php and manage_events.php in admin (display pages only)
2025-12-11 08:55:24 +02:00
twotalesanimation
abb8eb23e5 Add updates modal to homepage with session-based display and Jan 1 2026 expiry 2025-12-08 11:47:01 +02:00
twotalesanimation
2acbeac7ca fixed gallery 2025-12-08 11:39:57 +02:00
twotalesanimation
5808788b9e Make blog cards clickable - wrap in anchor tags matching gallery pattern 2025-12-08 11:35:22 +02:00
twotalesanimation
bbc0aecbcb force update CSS2 2025-12-08 10:55:08 +02:00
twotalesanimation
752ea6e5e9 fix: correct CSS syntax error in .comments rule that was breaking footer and other component styles 2025-12-08 10:37:01 +02:00
119 changed files with 18211 additions and 1241 deletions

View File

@@ -70,6 +70,7 @@ RewriteRule ^instapage$ src/pages/events/instapage.php [L]
RewriteRule ^about$ src/pages/other/about.php [L] RewriteRule ^about$ src/pages/other/about.php [L]
RewriteRule ^contact$ src/pages/other/contact.php [L] RewriteRule ^contact$ src/pages/other/contact.php [L]
RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.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 ^404$ src/pages/other/404.php [L]
RewriteRule ^account_settings$ src/pages/other/account_settings.php [L] RewriteRule ^account_settings$ src/pages/other/account_settings.php [L]
RewriteRule ^rescue_recovery$ src/pages/other/rescue_recovery.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 ^basic_indemnity$ src/pages/other/basic_indemnity.php [L]
RewriteRule ^view_indemnity$ src/pages/other/view_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 === # === ADMIN PAGES ===
RewriteRule ^admin_members$ src/admin/admin_members.php [L] RewriteRule ^admin_members$ src/admin/admin_members.php [L]
RewriteRule ^admin_payments$ src/admin/admin_payments.php [L] RewriteRule ^admin_payments$ src/admin/admin_payments.php [L]
@@ -88,10 +94,13 @@ RewriteRule ^admin_course_bookings$ src/admin/admin_course_bookings.php [L]
RewriteRule ^admin_camp_bookings$ src/admin/admin_camp_bookings.php [L] RewriteRule ^admin_camp_bookings$ src/admin/admin_camp_bookings.php [L]
RewriteRule ^admin_trip_bookings$ src/admin/admin_trip_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_visitors$ src/admin/admin_visitors.php [L]
RewriteRule ^admin_efts$ src/admin/admin_efts.php [L] RewriteRule ^admin_transactions$ src/admin/admin_transactions.php [L]
RewriteRule ^admin_trips$ src/admin/admin_trips.php [L] RewriteRule ^admin_trips$ src/admin/admin_trips.php [L]
RewriteRule ^manage_events$ src/admin/manage_events.php [L] RewriteRule ^manage_events$ src/admin/manage_events.php [L]
RewriteRule ^manage_trips$ src/admin/manage_trips.php [L] RewriteRule ^manage_trips$ src/admin/manage_trips.php [L]
RewriteRule ^admin_courses$ /src/admin/admin_courses.php [L,QSA]
RewriteRule ^manage_courses$ /src/admin/manage_courses.php [L,QSA]
# === API/AJAX ENDPOINTS === # === API/AJAX ENDPOINTS ===
RewriteRule ^fetch_users$ src/api/fetch_users.php [L] RewriteRule ^fetch_users$ src/api/fetch_users.php [L]
@@ -102,6 +111,8 @@ RewriteRule ^get_tab_total$ src/api/get_tab_total.php [L]
RewriteRule ^google_validate_login$ src/api/google_validate_login.php [L] RewriteRule ^google_validate_login$ src/api/google_validate_login.php [L]
# === PROCESSORS === # === PROCESSORS ===
RewriteRule ^process_course$ /src/processors/process_course.php [L,QSA]
RewriteRule ^delete_course$ /src/processors/delete_course.php [L,QSA]
RewriteRule ^validate_login$ src/processors/validate_login.php [L] RewriteRule ^validate_login$ src/processors/validate_login.php [L]
RewriteRule ^register_user$ src/processors/register_user.php [L] RewriteRule ^register_user$ src/processors/register_user.php [L]
RewriteRule ^process_application$ src/processors/process_application.php [L] RewriteRule ^process_application$ src/processors/process_application.php [L]
@@ -122,11 +133,11 @@ RewriteRule ^upload_profile_picture$ src/processors/upload_profile_picture.php [
RewriteRule ^send_reset_link$ src/processors/send_reset_link.php [L] RewriteRule ^send_reset_link$ src/processors/send_reset_link.php [L]
RewriteRule ^logout$ src/processors/logout.php [L] RewriteRule ^logout$ src/processors/logout.php [L]
RewriteRule ^process_trip$ src/processors/process_trip.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_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_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 ^save_album$ src/processors/save_album.php [L]
RewriteRule ^update_album$ src/processors/update_album.php [L] RewriteRule ^update_album$ src/processors/update_album.php [L]
RewriteRule ^delete_album$ src/processors/delete_album.php [L] RewriteRule ^delete_album$ src/processors/delete_album.php [L]
@@ -150,7 +161,7 @@ RewriteRule ^autosave$ src/processors/blog/autosave.php [L]
</IfModule> </IfModule>
php_flag display_errors On php_flag display_errors Off
# php_value error_reporting -1 # php_value error_reporting -1
RedirectMatch 403 ^/\.well-known RedirectMatch 403 ^/\.well-known
Options -Indexes Options -Indexes

215
.htaccess copy Normal file
View 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
View File

@@ -0,0 +1,4 @@
; memory_limit = 512M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 120

View File

@@ -1,3 +0,0 @@
<?php
// Redirector file - loads the actual page from src/pages/other/
require_once __DIR__ . '/src/pages/other/about.php';

View File

@@ -1,12 +1,6 @@
@charset "UTF-8"; @charset "UTF-8";
/*---------------------------------------------------------------------- /*----------------------------------------------------------------------
Template Name: Ravelo - Travel & Tour Booking HTML Template 4WDCSA.co.za CSS Stylesheet
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. */
/*---------------------------------------------------------------------- /*----------------------------------------------------------------------
CSS INDEX CSS INDEX
---------------------- ----------------------
@@ -7124,7 +7118,8 @@ blockquote {
/* Comments */ /* Comments */
.comments { .comments {
border-radius: 10px; border-radius: 10px;
/* border: 1px solid var(--border-color); } */ /* border: 1px solid var(--border-color); */
}
.comment-body { .comment-body {
padding: 50px; } padding: 50px; }

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

13126
assets/images/track-route2.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.6 MiB

66
assets/js/map.js Normal file
View 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
View 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');
}
}

View 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;

View File

@@ -258,6 +258,7 @@ if ($headerStyle === 'light') {
<ul class="navigation clearfix"> <ul class="navigation clearfix">
<li><a href="index">Home</a></li> <li><a href="index">Home</a></li>
<li><a href="about">About</a></li> <li><a href="about">About</a></li>
<li><a href="track-map">Track Map</a></li>
<li><a href="trips">Trips</a> <li><a href="trips">Trips</a>
<?php if ($headerStyle === 'dark'): ?> <?php if ($headerStyle === 'dark'): ?>
<ul> <ul>
@@ -285,10 +286,11 @@ if ($headerStyle === 'light') {
<li><a href="admin_blogs">Manage Blogs</a></li> <li><a href="admin_blogs">Manage Blogs</a></li>
<li><a href="admin_events">Manage Events</a></li> <li><a href="admin_events">Manage Events</a></li>
<li><a href="admin_trips">Manage Trips</a></li> <li><a href="admin_trips">Manage Trips</a></li>
<li><a href="admin_courses">Manage Courses</a></li>
<li><a href="admin_trip_bookings">Trip Bookings</a></li> <li><a href="admin_trip_bookings">Trip Bookings</a></li>
<li><a href="admin_course_bookings">Course Bookings</a></li> <li><a href="admin_course_bookings">Course Bookings</a></li>
<li><a href="admin_efts">EFT Payments</a></li> <li><a href="admin_transactions">iKhokha Payment History</a></li>
<li><a href="process_payments">Process Payments</a></li> <!-- <li><a href="process_payments">Process Payments</a></li> -->
<?php if ($role === 'superadmin') { ?> <?php if ($role === 'superadmin') { ?>
<li><a href="admin_visitors">Visitor Log</a></li> <li><a href="admin_visitors">Visitor Log</a></li>
<?php } ?> <?php } ?>
@@ -319,8 +321,14 @@ if ($headerStyle === 'light') {
<li><a href="account_settings">Account Settings</a></li> <li><a href="account_settings">Account Settings</a></li>
<li><a href="membership_details">Membership</a></li> <li><a href="membership_details">Membership</a></li>
<li><a href="bookings">My Bookings</a></li> <li><a href="bookings">My Bookings</a></li>
<li><a href="user_blogs">My Blog Posts</a></li> <?php
<li><a href="submit_pop">Submit P.O.P</a></li> 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> <li><a href="logout">Log Out</a></li>
</ul> </ul>
</li> </li>

269
index.php
View File

@@ -4,6 +4,42 @@ $headerStyle = 'dark';
include_once($rootPath . '/header.php'); include_once($rootPath . '/header.php');
$indemnityPending = false; $indemnityPending = false;
// Set session flag for updates modal - only show once per session and before Jan 1, 2026
if (!isset($_SESSION['updates_modal_shown'])) {
$currentDate = new DateTime();
$endDate = new DateTime('2026-01-01');
if ($currentDate < $endDate) {
$_SESSION['updates_modal_shown'] = true;
$showUpdatesModal = true;
} else {
$showUpdatesModal = false;
}
} else {
$showUpdatesModal = false;
}
// Show renew membership modal for logged-in users and where membership_fees payment_status is not PENDING RENEWAL. only show once per session
$showRenewModal = isset($_SESSION['user_id']) ? true : false;
if ($showRenewModal) {
if (!isset($_SESSION['renew_modal_shown'])) {
$_SESSION['renew_modal_shown'] = true;
} else {
$showRenewModal = false;
}
$user_id = $_SESSION['user_id'];
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
$stmt->bind_param("i", $user_id);
$stmt->execute();
$stmt->bind_result($payment_status);
$stmt->fetch();
$stmt->close();
if ($payment_status === 'PENDING RENEWAL') {
$showRenewModal = false;
}
}
if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) { if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) {
$userId = $_SESSION['user_id']; $userId = $_SESSION['user_id'];
$stmt = $conn->prepare("SELECT user_id FROM membership_application WHERE user_id = ? AND accept_indemnity = 0 LIMIT 1"); $stmt = $conn->prepare("SELECT user_id FROM membership_application WHERE user_id = ? AND accept_indemnity = 0 LIMIT 1");
@@ -636,8 +672,241 @@ if (countUpcomingTrips() > 0) { ?>
updateCountdown(); // initial call updateCountdown(); // initial call
setInterval(updateCountdown, 1000); setInterval(updateCountdown, 1000);
// Show updates modal on page load
document.addEventListener('DOMContentLoaded', function() {
const modal = document.getElementById('updatesModal');
const closeBtn = document.querySelector('.updates-modal-close');
const showModal = <?php echo $showUpdatesModal ? 'true' : 'false'; ?>;
const showRenewModal = <?php echo $showRenewModal ? 'true' : 'false'; ?>;
if (showModal && modal) {
// Show updates modal after a short delay for better UX
setTimeout(function() {
modal.style.display = 'flex';
}, 500);
}
// Close updates modal when X is clicked
if (closeBtn) {
closeBtn.addEventListener('click', function() {
if (modal) modal.style.display = 'none';
});
}
// Close updates modal when clicking outside the modal content
if (modal) {
modal.addEventListener('click', function(event) {
if (event.target === modal) {
modal.style.display = 'none';
}
});
}
// Show renew membership Bootstrap modal for logged-in users
try {
const renewModalEl = document.getElementById('renewModal');
if (showRenewModal && renewModalEl && typeof bootstrap !== 'undefined') {
setTimeout(function() {
const renewModal = new bootstrap.Modal(renewModalEl);
renewModal.show();
}, 700);
}
} catch (e) {
console.warn('Renew modal show failed', e);
}
});
</script> </script>
<!-- Updates Modal -->
<!-- Renew Membership Modal (shown to logged-in users) -->
<div class="modal fade" id="renewModal" tabindex="-1" aria-labelledby="renewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<!-- <div class="modal-header bg-secondary text-white">
<h5 class="modal-title" id="renewModalLabel">Membership Renewal Reminder</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> -->
<div class="modal-body">
Your membership will be expiring soon. Click below to renew now.
<a style="width:100%; display:block;" href="renew_membership" class="theme-btn style-two style-three mt-3">Renew Now</a>
</div>
<div class="modal-footer">
<button type="button" style="width:100%; display:block;" class="theme-btn" data-bs-dismiss="modal">Remind Me Later</button>
</div>
</div>
</div>
</div>
<!-- Updates Modal -->
<div id="updatesModal" class="updates-modal">
<div class="updates-modal-content">
<span class="updates-modal-close">&times;</span>
<div class="updates-modal-header">
<h2>What's New on 4WDCSA.co.za</h2>
</div>
<div class="updates-modal-body">
<div class="update-item">
<h3><i class="fas fa-images" style="margin-right: 10px; color: #e90000;"></i>Track Map</h3>
<p>Interactive map of the BASE4 4x4 Training Track.</p>
</div>
<div class="update-item">
<h3><i class="fas fa-images" style="margin-right: 10px; color: #e90000;"></i>Photo Gallery</h3>
<p>Explore and share memories from club events and trips. Members can now upload and view photos from past adventures.</p>
</div>
<div class="update-item">
<h3><i class="fas fa-map-location-dot" style="margin-right: 10px; color: #e90000;"></i>Campsites Directory</h3>
<p>Discover recommended campsites and accommodation options for your next adventure. Browse detailed information and member reviews.</p>
</div>
<div class="update-item">
<h3><i class="fas fa-users" style="margin-right: 10px; color: #e90000;"></i>Linked Membership</h3>
<p>Link a second user to your profile so both can book trips and receive member benefits together.</p>
</div>
<div class="update-item">
<h3><i class="fas fa-pen-fancy" style="margin-right: 10px; color: #e90000;"></i>Blog Posts</h3>
<p>Members can now post blogs, reviews, and trip reports to share experiences with the community.</p>
</div>
</div>
<div class="updates-modal-footer">
<button class="theme-btn style-two updates-modal-btn" onclick="document.getElementById('updatesModal').style.display='none'">
<span>Got It</span>
</button>
</div>
</div>
</div>
<style>
.updates-modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
align-items: center;
justify-content: center;
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> </body>
</html> </html>

122
progress.log Normal file
View File

@@ -0,0 +1,122 @@
[2025-12-15 12:32:19] AJAX BLOCK ENTERED
[2025-12-15 12:32:19] startDate=2025-10-16
[2025-12-15 12:32:19] endDate=2025-12-15
[2025-12-15 12:32:19] APP ID present: YES
[2025-12-15 12:32:19] APP SECRET present: YES
[2025-12-15 12:32:19] PAYLOAD: https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
[2025-12-15 12:32:19] IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
[2025-12-15 12:32:19] CURL HTTP CODE: 422
[2025-12-15 12:32:19] CURL ERROR: none
[2025-12-15 12:32:19] RAW RESPONSE: {"error":"Invalid Signature"}
[2025-12-15 12:33:31] AJAX BLOCK ENTERED
[2025-12-15 12:33:31] startDate=2025-10-16
[2025-12-15 12:33:31] endDate=2025-12-15
[2025-12-15 12:33:31] APP ID present: YES
[2025-12-15 12:33:31] APP SECRET present: YES
[2025-12-15 12:33:31] IKHOKHA PAYLOAD (FULL URL): https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
[2025-12-15 12:33:31] IKHOKHA IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
[2025-12-15 12:33:31] CURL HTTP CODE: 422
[2025-12-15 12:33:31] CURL ERROR: none
[2025-12-15 12:33:31] RAW RESPONSE: {"error":"Invalid Signature"}
[2025-12-15 12:33:59] AJAX BLOCK ENTERED
[2025-12-15 12:33:59] startDate=2025-10-16
[2025-12-15 12:33:59] endDate=2025-12-15
[2025-12-15 12:33:59] APP ID present: YES
[2025-12-15 12:33:59] APP SECRET present: YES
[2025-12-15 12:33:59] IKHOKHA PAYLOAD (FULL URL): https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
[2025-12-15 12:33:59] IKHOKHA IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
[2025-12-15 12:34:00] CURL HTTP CODE: 422
[2025-12-15 12:34:00] CURL ERROR: none
[2025-12-15 12:34:00] RAW RESPONSE: {"error":"Invalid Signature"}
[2025-12-15 12:37:06] AJAX BLOCK ENTERED
[2025-12-15 12:37:06] startDate=2025-10-16
[2025-12-15 12:37:06] endDate=2025-12-15
[2025-12-15 12:37:06] APP ID present: YES
[2025-12-15 12:37:06] APP SECRET present: YES
[2025-12-15 12:37:06] IKHOKHA ENDPOINT (REQUEST): https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
[2025-12-15 12:37:06] IKHOKHA PAYLOAD (SIGNED): https://api.ikhokha.com/public-api/v1/payments/history?startDate=2025-10-16&endDate=2025-12-15
[2025-12-15 12:37:06] IKHOKHA IK-SIGN: 418e48921e566e5804b58f65e1ca4a28dba4d69de3611d1cf7f90f865490f42d
[2025-12-15 12:37:06] CURL HTTP CODE: 422
[2025-12-15 12:37:06] CURL ERROR: none
[2025-12-15 12:37:06] RAW RESPONSE: {"error":"Invalid Signature"}
[2025-12-15 12:56:21] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 12:56:21] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 12:56:37] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 12:56:37] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 12:57:04] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 12:57:04] 13e6e02a7ccad937bc27b31038373d48d8ba2700a7ba8d9a7a2e4f9b07378692 | CONTEXT: IKHOKHA Signature
[2025-12-15 12:57:30] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 12:57:30] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 12:57:32] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 12:57:32] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 12:57:34] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 12:57:34] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 12:58:00] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA Endpoint
[2025-12-15 12:58:00] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 12:58:00] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 12:58:04] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA Endpoint
[2025-12-15 12:58:04] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 12:58:04] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 12:58:17] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 12:58:17] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 12:58:17] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 12:58:48] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 12:58:48] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 12:58:48] 13e6e02a7ccad937bc27b31038373d48d8ba2700a7ba8d9a7a2e4f9b07378692 | CONTEXT: IKHOKHA Signature
[2025-12-15 13:00:13] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 13:00:13] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 13:00:13] 13e6e02a7ccad937bc27b31038373d48d8ba2700a7ba8d9a7a2e4f9b07378692 | CONTEXT: IKHOKHA Signature
[2025-12-15 13:00:29] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 13:00:29] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 13:00:29] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 13:03:10] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 13:03:10] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 13:03:10] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 13:03:19] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 13:03:19] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 13:03:19] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 13:05:51] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
[2025-12-15 13:05:51] "{\"paylinkID\":\"ys5225k4z56x0mm\",\"status\":\"SUCCESS\",\"externalTransactionID\":\"693efeaca71a9\",\"responseCode\":\"00\",\"text\":null}" | CONTEXT: IKHOKHA Stringified Body
[2025-12-15 13:06:29] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
[2025-12-15 13:06:29] "{\"paylinkID\":\"ys5225k4z56x0mm\",\"status\":\"SUCCESS\",\"externalTransactionID\":\"693efeaca71a9\",\"responseCode\":\"00\",\"text\":null}" | CONTEXT: IKHOKHA Stringified Body
[2025-12-15 13:06:29] /src/api/ikhokha_webhook.php\"{\\"paylinkID\\":\\"ys5225k4z56x0mm\\",\\"status\\":\\"SUCCESS\\",\\"externalTransactionID\\":\\"693efeaca71a9\\",\\"responseCode\\":\\"00\\",\\"text\\":null}\" | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 13:06:29] 43a6a56af31c276174953e115eb41402f12969fedab5b673dd34327cd7135a75 | CONTEXT: IKHOKHA Generated Signature
[2025-12-15 13:06:42] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
[2025-12-15 13:06:42] {"paylinkID":"ys5225k4z56x0mm","status":"SUCCESS","externalTransactionID":"693efeaca71a9","responseCode":"00","text":null} | CONTEXT: IKHOKHA Stringified Body
[2025-12-15 13:06:42] /src/api/ikhokha_webhook.php\"{\\"paylinkID\\":\\"ys5225k4z56x0mm\\",\\"status\\":\\"SUCCESS\\",\\"externalTransactionID\\":\\"693efeaca71a9\\",\\"responseCode\\":\\"00\\",\\"text\\":null}\" | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 13:06:42] 43a6a56af31c276174953e115eb41402f12969fedab5b673dd34327cd7135a75 | CONTEXT: IKHOKHA Generated Signature
[2025-12-15 13:07:09] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
[2025-12-15 13:07:09] {"paylinkID":"ys5225k4z56x0mm","status":"SUCCESS","externalTransactionID":"693efeaca71a9","responseCode":"00","text":null} | CONTEXT: IKHOKHA Stringified Body
[2025-12-15 13:07:09] /src/api/ikhokha_webhook.php\"{\\"paylinkID\\":\\"ys5225k4z56x0mm\\",\\"status\\":\\"SUCCESS\\",\\"externalTransactionID\\":\\"693efeaca71a9\\",\\"responseCode\\":\\"00\\",\\"text\\":null}\" | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 13:07:09] 43a6a56af31c276174953e115eb41402f12969fedab5b673dd34327cd7135a75 | CONTEXT: IKHOKHA Generated Signature
[2025-12-15 13:09:39] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
[2025-12-15 13:19:12] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 13:19:12] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 13:19:12] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 13:36:28] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 13:36:28] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 13:36:28] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 13:36:54] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 13:36:54] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 13:36:54] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 15:41:25] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 15:41:25] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 15:41:25] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 15:43:53] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 15:43:53] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 15:43:53] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 15:44:29] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 15:44:29] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 15:44:29] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 15:46:02] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 15:46:02] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 15:46:02] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 15:47:46] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 15:47:46] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 15:47:46] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 15:47:51] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 15:47:51] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 15:47:51] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
[2025-12-15 15:48:43] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
[2025-12-15 15:48:43] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
[2025-12-15 15:48:43] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature

View File

@@ -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";
?>

View 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.

View 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.

View 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.

View File

@@ -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"> <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> <url>
<loc>https://4wdcsa.co.za/</loc> <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> <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>
<url> <url>
<loc>https://4wdcsa.co.za/index.php</loc> <loc>https://4wdcsa.co.za/contact</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod> <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> <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>
<url> <url>
<loc>https://4wdcsa.co.za/about.php</loc> <loc>https://4wdcsa.co.za/membership_details</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod> <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> <priority>0.80</priority>
<changefreq>monthly</changefreq>
</url> </url>
<url> <url>
<loc>https://4wdcsa.co.za/trips.php</loc> <loc>https://4wdcsa.co.za/bush_mechanics</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod> <lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.80</priority> <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>
<url> <url>
<loc>https://4wdcsa.co.za/events.php</loc> <loc>https://4wdcsa.co.za/register</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod> <lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.80</priority> <priority>0.60</priority>
<changefreq>yearly</changefreq>
</url> </url>
<!-- Legal -->
<url> <url>
<loc>https://4wdcsa.co.za/blog.php</loc> <loc>https://4wdcsa.co.za/privacy_policy</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod> <lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.80</priority> <priority>0.50</priority>
</url> <changefreq>yearly</changefreq>
<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>
</url> </url>
</urlset> </urlset>

View File

@@ -0,0 +1,56 @@
[2025-12-15 12:28:42] FILE HIT
[2025-12-15 12:28:42] AJAX BLOCK ENTERED
[2025-12-15 12:28:42] startDate=2025-10-16
[2025-12-15 12:28:42] endDate=2025-12-15
[2025-12-15 12:28:42] APP ID present: YES
[2025-12-15 12:28:42] APP SECRET present: YES
[2025-12-15 12:28:42] PAYLOAD: /public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
[2025-12-15 12:28:42] IK-SIGN: 3d610c60c8306cd1d5c99b2639f0e810594f8ffb9306a98d703f691173dab47d
[2025-12-15 12:28:44] CURL HTTP CODE: 422
[2025-12-15 12:28:44] CURL ERROR: none
[2025-12-15 12:28:44] RAW RESPONSE: {"error":"Invalid Signature"}
[2025-12-15 12:28:51] FILE HIT
[2025-12-15 12:28:51] AJAX BLOCK ENTERED
[2025-12-15 12:28:51] startDate=2025-10-16
[2025-12-15 12:28:51] endDate=2025-12-15
[2025-12-15 12:28:51] APP ID present: YES
[2025-12-15 12:28:51] APP SECRET present: YES
[2025-12-15 12:28:51] PAYLOAD: /public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
[2025-12-15 12:28:51] IK-SIGN: 3d610c60c8306cd1d5c99b2639f0e810594f8ffb9306a98d703f691173dab47d
[2025-12-15 12:28:51] CURL HTTP CODE: 422
[2025-12-15 12:28:51] CURL ERROR: none
[2025-12-15 12:28:51] RAW RESPONSE: {"error":"Invalid Signature"}
[2025-12-15 12:30:54] FILE HIT
[2025-12-15 12:30:54] AJAX BLOCK ENTERED
[2025-12-15 12:30:54] startDate=2025-10-16
[2025-12-15 12:30:54] endDate=2025-12-15
[2025-12-15 12:30:54] APP ID present: YES
[2025-12-15 12:30:54] APP SECRET present: YES
[2025-12-15 12:30:54] PAYLOAD: https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
[2025-12-15 12:30:54] IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
[2025-12-15 12:30:55] CURL HTTP CODE: 422
[2025-12-15 12:30:55] CURL ERROR: none
[2025-12-15 12:30:55] RAW RESPONSE: {"error":"Invalid Signature"}
[2025-12-15 12:31:13] FILE HIT
[2025-12-15 12:31:13] AJAX BLOCK ENTERED
[2025-12-15 12:31:13] startDate=2025-10-16
[2025-12-15 12:31:13] endDate=2025-12-15
[2025-12-15 12:31:13] APP ID present: YES
[2025-12-15 12:31:13] APP SECRET present: YES
[2025-12-15 12:31:13] PAYLOAD: https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
[2025-12-15 12:31:13] IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
[2025-12-15 12:31:13] CURL HTTP CODE: 422
[2025-12-15 12:31:13] CURL ERROR: none
[2025-12-15 12:31:13] RAW RESPONSE: {"error":"Invalid Signature"}
[2025-12-15 12:31:21] FILE HIT
[2025-12-15 12:31:47] FILE HIT
[2025-12-15 12:31:47] FILE HIT
[2025-12-15 12:31:58] FILE HIT
[2025-12-15 12:32:18] FILE HIT
[2025-12-15 12:32:19] FILE HIT
[2025-12-15 12:33:30] FILE HIT
[2025-12-15 12:33:31] FILE HIT
[2025-12-15 12:33:59] FILE HIT
[2025-12-15 12:33:59] FILE HIT
[2025-12-15 12:37:05] FILE HIT
[2025-12-15 12:37:06] FILE HIT

View File

@@ -38,13 +38,8 @@ if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] !== UPLOAD_ERR_
} }
$uploadDir = "assets/uploads/campsites/"; $uploadDir = "assets/uploads/campsites/";
if (!is_dir($uploadDir)) { if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true); mkdir($uploadDir, 0777, true);
}
if (!is_writable($uploadDir)) {
http_response_code(500);
die('Upload directory is not writable.');
} }
$randomFilename = $validationResult['filename']; $randomFilename = $validationResult['filename'];

161
src/admin/admin_courses.php Normal file
View File

@@ -0,0 +1,161 @@
<?php
$rootPath = dirname(dirname(__DIR__));
require_once($rootPath . "/src/config/env.php");
require_once($rootPath . '/src/config/connection.php');
require_once($rootPath . '/src/config/functions.php');
require_once($rootPath . '/header.php');
checkAdmin();
checkUserSession();
$pageTitle = 'Manage Courses';
$breadcrumbs = [['Home' => 'index']];
require_once($rootPath . '/components/banner.php');
// Fetch all courses
$courses_query = "
SELECT
course_id, course_type, date, capacity, booked, cost_members, cost_nonmembers, instructor, instructor_email, code
FROM courses
ORDER BY date DESC
";
$result = $conn->query($courses_query);
$courses = [];
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$courses[] = $row;
}
}
?>
<!-- Courses Management Area start -->
<section class="blog-list-page py-100 rel z-1">
<div class="container">
<div class="row">
<div class="col-lg-12">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
<h2 style="margin: 0;">Manage Courses</h2>
<a href="manage_courses" class="theme-btn create-album-btn">
<i class="far fa-plus"></i> New Course
</a>
</div>
<?php if (isset($_SESSION['message'])): ?>
<div class="alert alert-warning message-box">
<?php echo $_SESSION['message']; ?>
<span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</span>
</div>
<?php unset($_SESSION['message']);
endif;?>
<?php if (count($courses) > 0): ?>
<input type="text" class="filter-input" placeholder="Filter courses...">
<div class="courses-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<?php foreach ($courses as $course):
$available = intval($course['capacity']) - intval($course['booked']);
$type_label = strtoupper($course['course_type']);
if ($course['course_type'] == 'driver_training') {
$type_label = 'Driver Training';
} elseif ($course['course_type'] == 'bush_mechanics') {
$type_label = 'Bush Mechanics';
} elseif ($course['course_type'] == 'rescue_recovery') {
$type_label = 'Rescue & Recovery';
} elseif ($course['course_type'] == 'ladies_driver_training') {
$type_label = 'Ladies Driver Training';
}
?>
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<div class="content" style="width:100%;">
<div class="destination-header d-flex align-items-start gap-3">
<div>
<h5 class="mb-0"><?php echo $type_label; ?></h5>
<small class="text-muted"><?php echo htmlspecialchars($course['course_type']); ?> — <?php echo date('M d, Y', strtotime($course['date'])); ?></small><br
<small class="text-muted"><?php echo $course['code'] ? 'Code: ' . htmlspecialchars($course['code']) : ''; ?></small>
</div>
</div>
<p style="margin: 10px 0;">
<strong>Instructor:</strong> <?php echo htmlspecialchars($course['instructor']); ?> (<?php echo htmlspecialchars($course['instructor_email']); ?>)<br>
<strong>Capacity:</strong> <?php echo intval($course['booked']); ?> / <?php echo intval($course['capacity']); ?> &nbsp; <strong>Available:</strong> <?php echo $available; ?><br>
<strong>Costs:</strong> Members: R <?php echo number_format($course['cost_members'],2); ?> | Non-Members: R <?php echo number_format($course['cost_nonmembers'],2); ?>
</p>
<div class="destination-footer">
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
<a href="manage_courses?course_id=<?php echo $course['course_id']; ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
<button type="button" class="delete-course" data-course-id="<?php echo $course['course_id']; ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="no-courses">
<p>No courses found. <a href="manage_courses">Create one</a></p>
</div>
<?php endif; ?>
</div>
</div>
</div>
</section>
<!-- Courses Management Area end -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const filterInput = document.querySelector('.filter-input');
const cards = document.querySelectorAll('.destination-item');
if (cards.length === 0 && filterInput) {
filterInput.style.display = "none";
} else if (filterInput) {
filterInput.addEventListener("input", function() {
const filterValue = filterInput.value.trim().toLowerCase();
cards.forEach(card => {
const cardText = card.textContent.trim().toLowerCase();
card.style.display = cardText.includes(filterValue) ? "" : "none";
});
});
}
});
// Handle delete button clicks
document.querySelectorAll('.delete-course').forEach(btn => {
btn.addEventListener('click', function() {
if (!confirm('Are you sure you want to delete this course? This action cannot be undone.')) {
return false;
}
const courseId = this.dataset.courseId;
const card = this.closest('.destination-item');
const formData = new FormData();
formData.append('course_id', courseId);
fetch('delete_course', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
alert('Course deleted successfully!');
card.remove();
if (document.querySelectorAll('.destination-item').length === 0) {
location.reload();
}
} else {
alert('Error: ' + data.message);
}
})
.catch(err => {
console.error('Error:', err);
alert('Delete failed due to network error.');
});
});
});
</script>
<?php include_once($rootPath . '/components/insta_footer.php'); ?>

View File

@@ -1,13 +1,20 @@
<?php <?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__)); $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(); checkAdmin();
checkUserSession();
$pageTitle = 'Manage Events';
$breadcrumbs = [['Home' => 'index']];
require_once($rootPath . '/components/banner.php');
// Fetch all events // Fetch all events
$events_query = " $events_query = "
SELECT SELECT
event_id, name, type, location, date, published event_id, name, type, location, date, image, published
FROM events FROM events
ORDER BY date DESC ORDER BY date DESC
"; ";
@@ -22,340 +29,202 @@ if ($result && $result->num_rows > 0) {
?> ?>
<style> <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%; width: 100%;
border-collapse: separate; /* Image scales to fill the container */
border-spacing: 0; height: 100%;
margin: 10px 0; /* Image scales to fill the container */
} object-fit: cover;
/* Fills the container while maintaining aspect ratio */
thead th { object-position: center;
cursor: pointer; /* Aligns the center of the image with the center of the container */
text-align: left; display: block;
padding: 10px; /* Prevents inline whitespace issues */
font-weight: bold;
position: relative;
}
thead th::after {
content: '\25B2';
/* Up arrow */
font-size: 0.8em;
position: absolute;
right: 10px;
opacity: 0;
transition: opacity 0.2s;
}
thead th.asc::after {
content: '\25B2';
/* Up arrow */
opacity: 1;
}
thead th.desc::after {
content: '\25BC';
/* Down arrow */
opacity: 1;
}
tbody tr:nth-child(odd) {
background-color: transparent;
}
tbody tr:nth-child(even) {
background-color: rgb(255, 255, 255);
border-radius: 10px;
}
tbody td {
padding: 10px;
}
tbody tr:nth-child(even) td:first-child {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
tbody tr:nth-child(even) td:last-child {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
.filter-input {
width: 100%;
padding: 10px;
font-size: 16px;
background-color: rgb(255, 255, 255);
border-radius: 25px;
margin-bottom: 15px;
}
.btn {
display: inline-block;
padding: 6px 12px;
margin: 2px;
font-size: 14px;
border-radius: 5px;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #218838;
}
.btn-warning {
background-color: #ffc107;
color: black;
}
.btn-warning:hover {
background-color: #e0a800;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.bg-success {
background-color: #28a745;
color: white;
}
.bg-warning {
background-color: #ffc107;
color: black;
} }
</style> </style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script> <script>
$(document).ready(function() { document.addEventListener("DOMContentLoaded", function() {
// Sorting functionality const filterInput = document.querySelector('.filter-input');
const table = document.querySelector('table'); const cards = document.querySelectorAll('.destination-item');
if (table) {
const headers = table.querySelectorAll('thead th');
const rows = Array.from(table.querySelectorAll('tbody tr'));
headers.forEach((header, index) => { if (cards.length === 0 && filterInput) {
header.addEventListener('click', () => { filterInput.style.display = "none";
const sortedRows = rows.sort((a, b) => { } else if (filterInput) {
const aText = a.cells[index].textContent.trim().toLowerCase(); filterInput.addEventListener("input", function() {
const bText = b.cells[index].textContent.trim().toLowerCase(); const filterValue = filterInput.value.trim().toLowerCase();
cards.forEach(card => {
if (aText < bText) return -1; const cardText = card.textContent.trim().toLowerCase();
if (aText > bText) return 1; card.style.display = cardText.includes(filterValue) ? "" : "none";
return 0;
});
if (header.classList.contains('asc')) {
header.classList.remove('asc');
header.classList.add('desc');
sortedRows.reverse();
} else {
headers.forEach(h => h.classList.remove('asc', 'desc'));
header.classList.add('asc');
}
const tbody = table.querySelector('tbody');
tbody.innerHTML = '';
sortedRows.forEach(row => tbody.appendChild(row));
}); });
}); });
// Filter functionality
const filterInput = document.querySelector('.filter-input');
if (filterInput) {
filterInput.addEventListener('input', function() {
const filterValue = filterInput.value.trim().toLowerCase();
rows.forEach(row => {
const rowText = row.textContent.trim().toLowerCase();
row.style.display = rowText.includes(filterValue) ? '' : 'none';
});
});
}
} }
// Publish/Unpublish toggle
$('.toggle-publish').on('click', function() {
var eventId = $(this).data('event-id');
var button = $(this);
var row = button.closest('tr');
$.ajax({
url: 'toggle_event_published',
type: 'POST',
data: {
event_id: eventId
},
dataType: 'json',
complete: function(xhr, status) {
// Handle all response codes
try {
var response = JSON.parse(xhr.responseText);
if (response.status === 'success') {
if (response.published == 1) {
button.removeClass('btn-success').addClass('btn-warning');
button.find('i').removeClass('fa-eye').addClass('fa-eye-slash');
button.attr('title', 'Unpublish');
row.find('td:nth-child(5)').html('<span class="badge bg-success">Published</span>');
} else {
button.removeClass('btn-warning').addClass('btn-success');
button.find('i').removeClass('fa-eye-slash').addClass('fa-eye');
button.attr('title', 'Publish');
row.find('td:nth-child(5)').html('<span class="badge bg-warning">Draft</span>');
}
} else {
alert('Error: ' + response.message);
}
} catch (e) {
alert('Error updating event status. Response: ' + xhr.responseText);
}
}
});
});
// Delete event
$('.delete-event').on('click', function() {
if (!confirm('Are you sure you want to delete this event? This action cannot be undone.')) {
return false;
}
var eventId = $(this).data('event-id');
var button = $(this);
var row = button.closest('tr');
$.ajax({
url: 'delete_event',
type: 'POST',
data: {
event_id: eventId
},
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
row.fadeOut(300, function() {
$(this).remove();
});
} else {
alert('Error: ' + response.message);
}
},
error: function() {
alert('Error deleting event');
}
});
});
}); });
</script> </script>
<?php <?php
$pageTitle = 'Manage Events'; $pageTitle = 'Manage Events';
$breadcrumbs = [['Home' => 'index'], [$pageTitle => '']]; $breadcrumbs = [['Home' => 'index']];
require_once($rootPath . '/components/banner.php');
?>
<?php
$pageTitle = 'Manage Events';
$breadcrumbs = [['Home' => 'index'], [$pageTitle => '']];
require_once($rootPath . '/components/banner.php'); require_once($rootPath . '/components/banner.php');
?> ?>
<!-- Events Management Area start --> <!-- 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="container">
<div class="row mb-30"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<a href="manage_events" class="theme-btn style-two">+ Create New Event</a> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
</div> <h2 style="margin: 0;">Manage Events</h2>
</div> <a href="manage_events" class="theme-btn create-album-btn">
<i class="far fa-plus"></i> New Event
<?php </a>
if (!empty($events)) { </div>
echo '<div class="row"> <?php if (isset($_SESSION['message'])): ?>
<div class="col-lg-12"> <div class="alert alert-warning message-box">
<div class="form-group mb-20"> <?php echo $_SESSION['message']; ?>
<input type="text" class="filter-input" placeholder="Search events..."> <span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</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> </div>
<table> ';
<thead> }
<tr>
<th>Event Name</th> echo '</div>';
<th>Type</th> } else {
<th>Location</th> echo '<div class="no-events">
<th>Date</th> <p>No events found. <a href="manage_events">Create one</a></p>
<th>Status</th> </div>';
<th>Actions</th> }
</tr> ?>
</thead>
<tbody>'; </div>
foreach ($events as $event) {
$publishButtonText = $event['published'] == 1 ? 'Unpublish' : 'Publish'; </div>
$publishButtonClass = $event['published'] == 1 ? 'btn-warning' : 'btn-success';
echo '<tr>
<td><strong>' . htmlspecialchars($event['name']) . '</strong></td>
<td>' . htmlspecialchars($event['type']) . '</td>
<td>' . htmlspecialchars($event['location']) . '</td>
<td>' . convertDate($event['date']) . '</td>
<td>' . ($event['published'] == 1 ? '<span class="badge bg-success">Published</span>' : '<span class="badge bg-warning">Draft</span>') . '</td>
<td>
<a href="manage_events?event_id=' . $event['event_id'] . '" class="btn btn-sm btn-primary" title="Edit">
<i class="far fa-edit"></i>
</a>
<button class="btn btn-sm ' . $publishButtonClass . ' toggle-publish" data-event-id="' . $event['event_id'] . '" title="' . $publishButtonText . '">
<i class="far fa-' . ($event['published'] == 1 ? 'eye-slash' : 'eye') . '"></i>
</button>
<button class="btn btn-sm btn-danger delete-event" data-event-id="' . $event['event_id'] . '" title="Delete">
<i class="far fa-trash"></i>
</button>
</td>
</tr>';
}
echo '</tbody></table>';
echo '</div>';
echo '</div>';
} else {
echo '<p>No events found. <a href="manage_events">Create one</a></p>';
}
?>
</div> </div>
</section> </section>
<!-- Events Management Area end --> <!-- 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'); ?> <?php include_once($rootPath . '/components/insta_footer.php'); ?>

View File

@@ -0,0 +1,248 @@
<?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
?>
<style>
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 10px 0;
}
thead th {
cursor: pointer;
text-align: left;
padding: 10px;
font-weight: bold;
position: relative;
}
thead th::after {
content: '\25B2';
/* Up arrow */
font-size: 0.8em;
position: absolute;
right: 10px;
opacity: 0;
transition: opacity 0.2s;
}
thead th.asc::after {
content: '\25B2';
/* Up arrow */
opacity: 1;
}
thead th.desc::after {
content: '\25BC';
/* Down arrow */
opacity: 1;
}
tbody tr:nth-child(odd) {
background-color: transparent;
}
tbody tr:nth-child(even) {
background-color: rgb(255, 255, 255);
border-radius: 10px;
}
tbody td {
padding: 5px;
}
tbody tr:nth-child(even) td:first-child {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
tbody tr:nth-child(even) td:last-child {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
.filter-input {
width: 100%;
padding: 5px;
/* margin-bottom: 20px; */
font-size: 16px;
background-color: rgb(255, 255, 255);
border-radius: 25px;
}
.infobox {
color: #484848;
background: #f9f9f7;
border: 1px solid #d8d8d8;
border-radius: 10px;
margin-top: 15px;
margin-bottom: 15px;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
const table = document.querySelector("table");
const headers = table.querySelectorAll("thead th");
const rows = Array.from(table.querySelectorAll("tbody tr"));
const filterInput = document.getElementById("filterInput");
headers.forEach((header, index) => {
header.addEventListener("click", () => {
const sortedRows = rows.sort((a, b) => {
const aText = a.cells[index].textContent.trim().toLowerCase();
const bText = b.cells[index].textContent.trim().toLowerCase();
if (aText < bText) return -1;
if (aText > bText) return 1;
return 0;
});
if (header.classList.contains("asc")) {
header.classList.remove("asc");
header.classList.add("desc");
sortedRows.reverse();
} else {
headers.forEach(h => h.classList.remove("asc", "desc"));
header.classList.add("asc");
}
const tbody = table.querySelector("tbody");
tbody.innerHTML = "";
sortedRows.forEach(row => tbody.appendChild(row));
});
});
filterInput.addEventListener("input", function() {
const filterValue = filterInput.value.trim().toLowerCase();
rows.forEach(row => {
const rowText = row.textContent.trim().toLowerCase();
row.style.display = rowText.includes(filterValue) ? "" : "none";
});
});
});
</script>
<!-- Page Banner Start -->
<?php
$bannerFolder = 'assets/images/banners/';
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
$randomBanner = 'assets/images/base4/camping.jpg'; // default fallback
if (!empty($bannerImages)) {
$randomBanner = $bannerImages[array_rand($bannerImages)];
}
?>
<section class="page-banner-area pt-50 pb-35 rel z-1 bgs-cover" style="background-image: url('<?php echo $randomBanner; ?>');">
<div class="banner-overlay"></div>
<div class="container">
<div class="banner-inner text-white mb-50">
<h2 class="page-title mb-10" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">iKhokha Payments</h2>
<nav aria-label="breadcrumb">
<ol class="breadcrumb justify-content-center mb-20" data-aos="fade-right" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
<li class="breadcrumb-item"><a href="index.php">Home</a></li>
<li class="breadcrumb-item active">iKhokha Payments</li>
</ol>
</nav>
</div>
</div>
</section>
<!-- Tour List Area start -->
<section class="tour-list-page py-10 rel z-1">
<div class="container">
<div class="row">
<div class="col-lg-12">
<div class='infobox' data-aos='fade-up' data-aos-duration='1500' data-aos-offset='50'>
<div style='padding:10px;'>
<?php
// Fetch transactions from iKhokha API instead of DB
$startDate = isset($_GET['start']) ? $_GET['start'] : date('Y-m-d', strtotime('-30 days'));
$endDate = isset($_GET['end']) ? $_GET['end'] : date('Y-m-d');
// getIkhokhaTransactionHistory should return JSON (string) or an array
$raw = getIkhokhaTransactionHistory($startDate, $endDate);
$transactions = [];
if (is_string($raw)) {
$transactions = json_decode($raw, true);
} elseif (is_array($raw)) {
$transactions = $raw;
}
if (!empty($transactions)) {
echo '<input id="filterInput" type="text" class="filter-input" placeholder="Filter results...">';
echo '<table>
<thead>
<tr>
<th>Date</th>
<th>ID</th>
<th>PaylinkID</th>
<th>Description</th>
<th>Amount</th>
<th>Status</th>
</tr>
</thead>
<tbody>';
$printed = false;
foreach ($transactions as $row) {
$createdAt = isset($row['createdAt']) ? htmlspecialchars($row['createdAt']) : '';
// prefer externalTransactionID when available, fallback to paylinkID
$txId = isset($row['externalTransactionID']) ? $row['externalTransactionID'] : (isset($row['paylinkID']) ? $row['paylinkID'] : '');
$ikhokhaTxId = isset($row['paylinkID']) ? $row['paylinkID'] : '';
$description = isset($row['description']) ? $row['description'] : '';
$amount = isset($row['amount']) ? $row['amount'] : '';
$status = isset($row['status']) ? $row['status'] : '';
// Skip unpaid transactions
if (strcasecmp($status, 'UNPAID') === 0) {
continue;
}
echo "<tr>
<td>" . htmlspecialchars($createdAt) . "</td>
<td>" . htmlspecialchars($txId) . "</td>
<td>" . htmlspecialchars($ikhokhaTxId) . "</td>
<td>" . htmlspecialchars($description) . "</td>
<td>R " . htmlspecialchars($amount/100) . ".00</td>
<td>" . htmlspecialchars($status) . "</td>
</tr>";
$printed = true;
}
if (!$printed) {
echo '<tr><td colspan="6">No records found</td></tr>';
}
} else {
echo '<input id="filterInput" type="text" class="filter-input" placeholder="Filter results...">';
echo '<table>
<thead>
<tr>
<th>Date</th>
<th>ID</th>
<th>Description</th>
<th>Amount</th>
<th>Status</th>
</tr>
</thead>
<tbody>';
echo '<tr><td colspan="5">No records found</td></tr>';
} ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Tour List Area end -->
<?php include_once($rootPath . '/components/insta_footer.php'); ?>

View File

@@ -1,14 +1,22 @@
<?php <?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__)); $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(); checkAdmin();
checkUserSession();
$pageTitle = 'Manage Trips';
$breadcrumbs = [['Home' => 'index']];
require_once($rootPath . '/components/banner.php');
// Fetch all trips with booking status // Fetch all trips with booking status
$trips_query = " $trips_query = "
SELECT SELECT
trip_id, trip_name, location, start_date, end_date, 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 FROM trips
ORDER BY start_date DESC ORDER BY start_date DESC
"; ";
@@ -23,131 +31,48 @@ if ($result && $result->num_rows > 0) {
?> ?>
<style> <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%; width: 100%;
border-collapse: separate; /* Image scales to fill the container */
border-spacing: 0; height: 100%;
margin: 10px 0; /* Image scales to fill the container */
} object-fit: cover;
/* Fills the container while maintaining aspect ratio */
thead th { object-position: center;
cursor: pointer; /* Aligns the center of the image with the center of the container */
text-align: left; display: block;
padding: 10px; /* Prevents inline whitespace issues */
font-weight: bold;
position: relative;
}
thead th::after {
content: '\25B2';
/* Up arrow */
font-size: 0.8em;
position: absolute;
right: 10px;
opacity: 0;
transition: opacity 0.2s;
}
thead th.asc::after {
content: '\25B2';
/* Up arrow */
opacity: 1;
}
thead th.desc::after {
content: '\25BC';
/* Down arrow */
opacity: 1;
}
tbody tr:nth-child(odd) {
background-color: transparent;
}
tbody tr:nth-child(even) {
background-color: rgb(255, 255, 255);
border-radius: 10px;
}
tbody td {
padding: 5px;
}
tbody tr:nth-child(even) td:first-child {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
tbody tr:nth-child(even) td:last-child {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
.filter-input {
width: 100%;
padding: 5px;
font-size: 16px;
background-color: rgb(255, 255, 255);
border-radius: 25px;
margin-bottom: 20px;
}
.trips-section {
color: #484848;
background: #f9f9f7;
border: 1px solid #d8d8d8;
border-radius: 10px;
margin-top: 15px;
margin-bottom: 15px;
} }
</style> </style>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
const tables = document.querySelectorAll("table"); const filterInput = document.querySelector('.filter-input');
tables.forEach((table) => { const cards = document.querySelectorAll('.destination-item');
const headers = table.querySelectorAll("thead th");
const rows = Array.from(table.querySelectorAll("tbody tr"));
const filterInput = table.previousElementSibling;
headers.forEach((header, index) => { if (cards.length === 0 && filterInput) {
header.addEventListener("click", () => { filterInput.style.display = "none";
const sortedRows = rows.sort((a, b) => { } else if (filterInput) {
const aText = a.cells[index].textContent.trim().toLowerCase(); filterInput.addEventListener("input", function() {
const bText = b.cells[index].textContent.trim().toLowerCase(); const filterValue = filterInput.value.trim().toLowerCase();
cards.forEach(card => {
if (aText < bText) return -1; const cardText = card.textContent.trim().toLowerCase();
if (aText > bText) return 1; card.style.display = cardText.includes(filterValue) ? "" : "none";
return 0;
});
if (header.classList.contains("asc")) {
header.classList.remove("asc");
header.classList.add("desc");
sortedRows.reverse();
} else {
headers.forEach(h => h.classList.remove("asc", "desc"));
header.classList.add("asc");
}
const tbody = table.querySelector("tbody");
tbody.innerHTML = "";
sortedRows.forEach(row => tbody.appendChild(row));
}); });
}); });
}
if (rows.length === 0) {
filterInput.style.display = "none";
} else {
filterInput.addEventListener("input", function() {
const filterValue = filterInput.value.trim().toLowerCase();
rows.forEach(row => {
const rowText = row.textContent.trim().toLowerCase();
row.style.display = rowText.includes(filterValue) ? "" : "none";
});
});
}
});
}); });
</script> </script>
@@ -155,164 +80,163 @@ if ($result && $result->num_rows > 0) {
$bannerFolder = 'assets/images/banners/'; $bannerFolder = 'assets/images/banners/';
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE); $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 --> <!-- 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 class="container">
<div style="margin-bottom: 20px;"> <div class="row">
<a href="manage_trips" class="theme-btn"> <div class="col-lg-12">
<i class="far fa-plus"></i> Create New Trip
</a>
</div>
<?php <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
if (count($trips) > 0) { <h2 style="margin: 0;">Manage Trips</h2>
echo '<input type="text" class="filter-input" placeholder="Filter trips...">'; <a href="manage_trips" class="theme-btn create-album-btn">
echo '<div class="trips-section" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">'; <i class="far fa-plus"></i> New Event
echo '<div style="padding:10px;">'; </a>
echo '<table> </div>
<thead> <?php if (isset($_SESSION['message'])): ?>
<tr> <div class="alert alert-warning message-box">
<th>Trip Name</th> <?php echo $_SESSION['message']; ?>
<th>Location</th> <span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</span>
<th>Start Date</th> </div>
<th>End Date</th> <?php unset($_SESSION['message']);
<th>Capacity</th> endif;
<th>Booked</th> if (count($trips) > 0) {
<th>Cost (Member)</th> echo '<input type="text" class="filter-input" placeholder="Filter trips...">';
<th>Status</th> echo '<div class="trips-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
<th>Actions</th>
</tr> foreach ($trips as $trip) {
</thead> $available = $trip['vehicle_capacity'] - $trip['places_booked'];
<tbody>'; $publishStatus = $trip['published'] == 1 ? 'published' : 'draft';
foreach ($trips as $trip) { $publishStatusBadge = $trip['published'] == 1 ? 'PUBLISHED' : 'DRAFT';
$publishButtonText = $trip['published'] == 1 ? 'Unpublish' : 'Publish';
$publishButtonClass = $trip['published'] == 1 ? 'btn-warning' : 'btn-success'; // Get trip image - look for assets/images/trips/$trip_id_{number}.jpg
echo '<tr> $tripImagePath = '';
<td><strong>' . htmlspecialchars($trip['trip_name']) . '</strong></td> $tripImagesGlob = glob($rootPath . '/assets/images/trips/' . $trip['trip_id'] . '_*.jpg');
<td>' . htmlspecialchars($trip['location']) . '</td> if (!empty($tripImagesGlob)) {
<td>' . date('M d, Y', strtotime($trip['start_date'])) . '</td> $tripImagePath = str_replace($rootPath, '', $tripImagesGlob[0]);
<td>' . date('M d, Y', strtotime($trip['end_date'])) . '</td> } else {
<td>' . $trip['vehicle_capacity'] . '</td> // Fallback to placeholder icon if no image found
<td><span class="badge bg-info">' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '</span></td> $tripImagePath = 'assets/images/placeholder.jpg';
<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> echo '
<a href="manage_trips?trip_id=' . $trip['trip_id'] . '" class="btn btn-sm btn-primary" title="Edit"> <div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<i class="far fa-edit"></i> <div class="image" style="width:300px;height:250px;">
</a> <img src="' . htmlspecialchars($tripImagePath) . '" alt="' . htmlspecialchars($trip['trip_name']) . '">
<button class="btn btn-sm ' . $publishButtonClass . ' toggle-publish" data-trip-id="' . $trip['trip_id'] . '" title="' . $publishButtonText . '"> </div>
<i class="far fa-' . ($trip['published'] == 1 ? 'eye-slash' : 'eye') . '"></i> <div class="content" style="width:100%;">
</button> <div class="destination-header d-flex align-items-start gap-3">
<button class="btn btn-sm btn-danger delete-trip" data-trip-id="' . $trip['trip_id'] . '" title="Delete"> <div>
<i class="far fa-trash"></i> <span class="badge bg-dark mb-1">' . strtoupper($publishStatusBadge) . '</span>
</button> <h5 class="mb-0">' . htmlspecialchars($trip['trip_name']) . '</h5>
</td> <small class="text-muted">📍 ' . htmlspecialchars($trip['location']) . '</small>
</tr>'; </div>
} </div>
echo '</tbody></table>'; <p style="margin: 10px 0;">
echo '</div>'; <strong>Dates:</strong> ' . date('M d', strtotime($trip['start_date'])) . ' - ' . date('M d, Y', strtotime($trip['end_date'])) . '<br>
echo '</div>'; <strong>Capacity:</strong> ' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '<br>
} else { <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) . '
echo '<p>No trips found. <a href="manage_trips">Create one</a></p>'; </p>
} <div class="destination-footer">
?> <div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
<a href="manage_trips?trip_id=' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
<button type="button" class="toggle-publish" data-trip-id="' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($trip['published'] == 1 ? 'Unpublish' : 'Publish') . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($trip['published'] == 1 ? 'cloud_off' : 'cloud_upload') . '</span></button>
<button type="button" class="delete-trip" data-trip-id="' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
</div>
</div>
</div>
</div>
';
}
echo '</div>';
} else {
echo '<div class="no-trips">
<p>No trips found. <a href="manage_trips">Create one</a></p>
</div>';
}
?>
</div>
</div>
</div> </div>
</section> </section>
<!-- Trips Management Area end --> <!-- Trips Management Area end -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script> <script>
$(document).ready(function() { const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
$('.toggle-publish').on('click', function() { tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
var tripId = $(this).data('trip-id');
var button = $(this);
var row = button.closest('tr');
$.ajax({ // Handle publish/unpublish button clicks
url: 'toggle_trip_published', document.querySelectorAll('.toggle-publish').forEach(btn => {
type: 'POST', btn.addEventListener('click', function() {
data: { const tripId = this.dataset.tripId;
trip_id: tripId const currentIcon = this.querySelector('.material-icons').textContent;
}, const isPublished = currentIcon === 'cloud_off';
dataType: 'json', const action = isPublished ? 'Unpublish' : 'Publish';
success: function(response) {
if (response.status === 'success') { const formData = new FormData();
// Update button appearance formData.append('trip_id', tripId);
if (response.published == 1) {
button.removeClass('btn-success').addClass('btn-warning'); fetch('toggle_trip_published', {
button.find('i').removeClass('fa-eye').addClass('fa-eye-slash'); method: 'POST',
button.attr('title', 'Unpublish'); body: formData
// Update status badge })
row.find('td:nth-child(8)').html('<span class="badge bg-success">Published</span>'); .then(response => response.json())
} else { .then(data => {
button.removeClass('btn-warning').addClass('btn-success'); if (data.status === 'success') {
button.find('i').removeClass('fa-eye-slash').addClass('fa-eye'); alert(action + ' successful!');
button.attr('title', 'Publish'); location.reload();
// Update status badge
row.find('td:nth-child(8)').html('<span class="badge bg-warning">Draft</span>');
}
} else { } else {
alert('Error: ' + response.message); alert(action + ' failed: ' + data.message);
} }
}, })
error: function() { .catch(err => {
alert('Error updating trip status'); 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.')) { if (!confirm('Are you sure you want to delete this trip? This action cannot be undone.')) {
return false; return false;
} }
var tripId = $(this).data('trip-id'); const tripId = this.dataset.tripId;
var button = $(this); const card = this.closest('.destination-item');
var row = button.closest('tr');
$.ajax({ const formData = new FormData();
url: 'delete_trip', formData.append('trip_id', tripId);
type: 'POST',
data: { fetch('delete_trip', {
trip_id: tripId method: 'POST',
}, body: formData
dataType: 'json', })
success: function(response) { .then(response => response.json())
if (response.status === 'success') { .then(data => {
row.fadeOut(function() { if (data.status === 'success') {
$(this).remove(); alert('Trip deleted successfully!');
if ($('table tbody tr').length === 0) { card.style.animation = 'fadeOut 0.3s ease-out';
setTimeout(() => {
card.remove();
if (document.querySelectorAll('.destination-item').length === 0) {
location.reload(); location.reload();
} }
}); }, 300);
} else { } else {
alert('Error: ' + response.message); alert('Error: ' + data.message);
} }
}, })
error: function() { .catch(err => {
alert('Error deleting trip'); console.error('Error:', err);
} alert('Delete failed due to network error.');
}); });
}); });
}); });
</script> </script>

View File

@@ -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();

View File

@@ -0,0 +1,221 @@
<?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
$course_id = $_GET['course_id'] ?? null;
$course = null;
// If editing an existing course, fetch its data
if ($course_id) {
$stmt = $conn->prepare("SELECT * FROM courses WHERE course_id = ?");
$stmt->bind_param("i", $course_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$course = $result->fetch_assoc();
}
$stmt->close();
}
?>
<?php
$pageTitle = $course ? 'Edit Course' : 'Create New Course';
$breadcrumbs = [['Home' => 'index'], ['Admin' => 'admin_courses'], [$pageTitle => '']];
require_once($rootPath . '/components/banner.php');
?>
<!-- Course Manager Area start -->
<section class="trip-manager-area py-100 rel z-1">
<div class="container">
<div class="row">
<div class="col-lg-12">
<div class="comment-form bgc-lighter z-1 rel mb-30 rmb-55">
<form id="courseForm" method="POST" action="process_course">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<?php if ($course): ?>
<input type="hidden" name="course_id" value="<?php echo $course['course_id']; ?>">
<?php endif; ?>
<div class="section-title py-20">
<h2><?php echo $course ? 'Edit Course: ' . htmlspecialchars($course['code'] ?: $course['course_type']) : 'Create New Course'; ?></h2>
<div id="responseMessage"></div>
</div>
<div class="row mt-35">
<div class="col-md-6">
<div class="form-group">
<label for="course_type">Course Type *</label>
<select id="course_type" name="course_type" class="form-control" required>
<?php
$types = ['driver_training' => 'Driver Training', 'bush_mechanics' => 'Bush Mechanics', 'rescue_recovery' => 'Rescue & Recovery', 'ladies_driver_training' => 'Ladies Driver Training'];
foreach ($types as $key => $label) {
$sel = ($course && $course['course_type'] === $key) ? 'selected' : '';
echo "<option value=\"$key\" $sel>$label</option>";
}
?>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="code">Course Code</label>
<input type="text" id="code" name="code" class="form-control" maxlength="12" value="<?php echo $course ? htmlspecialchars($course['code']) : ''; ?>" placeholder="Optional code e.g., CRSE001" data-manual="0">
<small class="form-text text-muted">Auto-generated from type + date (you can edit manually)</small>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="date">Date *</label>
<input type="date" id="date" name="date" class="form-control" value="<?php echo $course ? $course['date'] : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="capacity">Capacity *</label>
<input type="number" id="capacity" name="capacity" class="form-control" min="1" value="<?php echo $course ? intval($course['capacity']) : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="cost_members">Member Cost (R) *</label>
<input type="number" id="cost_members" name="cost_members" class="form-control" step="0.01" min="0" value="<?php echo $course ? $course['cost_members'] : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="cost_nonmembers">Non-Member Cost (R) *</label>
<input type="number" id="cost_nonmembers" name="cost_nonmembers" class="form-control" step="0.01" min="0" value="<?php echo $course ? $course['cost_nonmembers'] : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="instructor">Instructor *</label>
<input type="text" id="instructor" name="instructor" class="form-control" value="<?php echo $course ? htmlspecialchars($course['instructor']) : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="instructor_email">Instructor Email</label>
<input type="email" id="instructor_email" name="instructor_email" class="form-control" value="<?php echo $course ? htmlspecialchars($course['instructor_email']) : ''; ?>">
</div>
</div>
<div class="col-md-12 mt-20">
<div class="form-group mb-0">
<button type="submit" class="theme-btn style-two" style="width:100%;">
<?php echo $course ? 'Update Course' : 'Create Course'; ?>
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
<!-- Course Manager Area end -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
$('#courseForm').on('submit', function(event) {
event.preventDefault();
var formData = $(this).serialize();
$.ajax({
url: 'process_course',
type: 'POST',
data: formData,
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
$('#responseMessage').html('<div class="alert alert-success">' + response.message + '</div>');
setTimeout(function() {
window.location.href = 'admin_courses';
}, 1200);
} else {
$('#responseMessage').html('<div class="alert alert-danger">' + response.message + '</div>');
}
},
error: function(xhr, status, error) {
console.error('AJAX Error:', error);
$('#responseMessage').html('<div class="alert alert-danger">Error creating/updating course: ' + error + '</div>');
}
});
});
});
</script>
<script>
// Auto-generate course code from type and date: ABBREVIATION_MMDD
(function(){
var typeMap = {
'driver_training': 'DRVTRN',
'bush_mechanics': 'BUSHMEC',
'rescue_recovery': 'RESREC',
'ladies_driver_training': 'LADYTRN'
};
var $type = document.getElementById('course_type');
var $date = document.getElementById('date');
var $code = document.getElementById('code');
function getMMDDFromISO(isoDate) {
if (!isoDate) return '';
// expecting YYYY-MM-DD
var parts = isoDate.split('-');
if (parts.length !== 3) return '';
return parts[1] + parts[2];
}
function generateCode() {
try {
var manual = $code.getAttribute('data-manual') === '1';
if (manual) return; // user has manually edited
var t = $type.value;
var d = $date.value;
if (!t || !d) return;
var abbr = typeMap[t] || t.toUpperCase().replace(/[^A-Z0-9]/g,'').substring(0,7);
var mmdd = getMMDDFromISO(d);
if (!mmdd) return;
var newCode = abbr + '_' + mmdd;
$code.value = newCode;
} catch (e) {
console.error('generateCode error', e);
}
}
// mark manual when user types
$code.addEventListener('input', function(){
var val = $code.value.trim();
if (val.length === 0) {
$code.setAttribute('data-manual','0');
} else {
// if value matches auto pattern for currently selected type+date, keep as auto; otherwise mark manual
var expected = '';
try { expected = (typeMap[$type.value] || $type.value.toUpperCase().replace(/[^A-Z0-9]/g,'').substring(0,7)) + '_' + ( ($date.value) ? $date.value.split('-')[1] + $date.value.split('-')[2] : '' ); } catch(e){ expected=''; }
if (val === expected) {
$code.setAttribute('data-manual','0');
} else {
$code.setAttribute('data-manual','1');
}
}
});
$type.addEventListener('change', generateCode);
$date.addEventListener('change', generateCode);
// generate on load if empty
if ($code.value.trim().length === 0) {
generateCode();
}
})();
</script>
<?php include_once($rootPath . '/components/insta_footer.php'); ?>

283
src/api/ikhokha_webhook.php Normal file
View File

@@ -0,0 +1,283 @@
<?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");
/**
* ==========================================================
* JS-equivalent escaping (matches iKhokha docs exactly)
* ==========================================================
*/
function jsStringEscape(string $str): string
{
$str = preg_replace('/([\\\\\"\'])/', '\\\\$1', $str);
$str = str_replace("\0", "\\0", $str);
return $str;
}
function createPayloadToSign(string $path, string $body): string
{
return jsStringEscape($path . $body);
}
/**
* ==========================================================
* Read raw request body (DO NOT MODIFY)
* ==========================================================
*/
$raw = file_get_contents('php://input');
if ($raw === false || $raw === '') {
http_response_code(400);
progress_log('iKhokha webhook: empty body');
exit('No body');
}
/**
* ==========================================================
* Read headers
* ==========================================================
*/
$headers = function_exists('getallheaders') ? getallheaders() : [];
$headers = array_change_key_case($headers, CASE_LOWER);
$ikSign = $headers['ik-sign'] ?? null;
$ikAppId = $headers['ik-appid'] ?? null;
if (!$ikSign || !$ikAppId) {
http_response_code(400);
progress_log('iKhokha webhook: missing headers');
exit('Missing headers');
}
/**
* ==========================================================
* Signature verification (JS-equivalent)
* ==========================================================
*/
$secret = $_ENV['IKHOKHA_APP_SECRET'] ?? null;
$callbackUrl = $_ENV['IKHOKHA_CALLBACK_URL'] ?? null;
$bypass = ($_ENV['IKHOKHA_BYPASS_SIGNATURE'] ?? 'false') === 'true';
if (!$secret || !$callbackUrl) {
http_response_code(500);
exit('Server misconfigured');
}
progress_log('--- iKhokha WEBHOOK DEBUG ---');
progress_log('RAW BODY: ' . $raw);
progress_log('IK-SIGN: ' . $ikSign);
// Decode body so we can remove `text`
$bodyArray = json_decode($raw, true);
if (!is_array($bodyArray)) {
http_response_code(400);
exit('Invalid JSON');
}
// iKhokha JS deletes `text`
unset($bodyArray['text']);
// JS-style JSON (no escaped slashes)
$jsonBody = json_encode($bodyArray, JSON_UNESCAPED_SLASHES);
// Now sign the SAME payload JS signs
$payloadToSign = createPayloadToSign($callbackUrl, $jsonBody);
$expected = generateSignature($payloadToSign, $secret);
progress_log('JS PAYLOAD: ' . $payloadToSign);
progress_log('EXPECTED SIGN: ' . $expected);
progress_log('RECEIVED SIGN: ' . $ikSign);
if (!$bypass) {
if (!hash_equals($expected, $ikSign)) {
http_response_code(403);
progress_log('iKhokha webhook: signature mismatch');
if (function_exists('auditLog')) {
auditLog(null, 'IKHOKHA_SIGNATURE_MISMATCH', 'webhook', null, [
'expected' => $expected,
'received' => $ikSign
]);
}
exit('Invalid signature');
}
} else {
progress_log('⚠️ IKHOKHA SIGNATURE CHECK BYPASSED');
}
/**
* ==========================================================
* Decode payload
* ==========================================================
*/
$payload = json_decode($raw, true);
$data = $payload['data'] ?? $payload;
/**
* ==========================================================
* Extract fields safely
* ==========================================================
*/
$externalTransactionID =
$data['externalTransactionID']
?? $data['externalTransactionId']
?? $data['externalTxId']
?? null;
$providerPaymentId =
$data['paylinkID']
?? $data['id']
?? null;
$providerStatus =
$data['status']
?? $payload['status']
?? null;
progress_log('Parsed externalTransactionID: ' . $externalTransactionID);
progress_log('Parsed providerPaymentId: ' . $providerPaymentId);
progress_log('Parsed providerStatus: ' . $providerStatus);
/**
* ==========================================================
* Locate payment
* ==========================================================
*/
$localPaymentId = null;
$booking_id = null;
$user_id = null;
$description = null;
if ($externalTransactionID) {
$stmt = $conn->prepare(
"SELECT payment_id, user_id, booking_id, description
FROM payments
WHERE payment_id = ?
LIMIT 1"
);
if ($stmt) {
$stmt->bind_param('s', $externalTransactionID);
$stmt->execute();
$res = $stmt->get_result();
if ($row = $res->fetch_assoc()) {
extract($row);
$localPaymentId = $row['payment_id'];
}
$stmt->close();
}
}
if (!$localPaymentId && $providerPaymentId) {
$stmt = $conn->prepare(
"SELECT payment_id, user_id, booking_id, description
FROM payments
WHERE provider_payment_id = ?
LIMIT 1"
);
if ($stmt) {
$stmt->bind_param('s', $providerPaymentId);
$stmt->execute();
$res = $stmt->get_result();
if ($row = $res->fetch_assoc()) {
extract($row);
$localPaymentId = $row['payment_id'];
}
$stmt->close();
}
}
if (!$localPaymentId) {
http_response_code(404);
exit('Payment not found');
}
/**
* ==========================================================
* Persist provider response
* ==========================================================
*/
$update = $conn->prepare(
"UPDATE payments
SET provider_payment_id = ?,
provider_status = ?,
provider_response = ?
WHERE payment_id = ?"
);
if ($update) {
$update->bind_param(
'ssss',
$providerPaymentId,
$providerStatus,
$raw,
$localPaymentId
);
$update->execute();
$update->close();
}
/**
* ==========================================================
* Business logic
* ==========================================================
*/
$normalized = strtoupper(trim((string)$providerStatus));
if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) {
$conn->prepare(
"UPDATE payments SET status = 'PAID' WHERE payment_id = ?"
)->bind_param('s', $localPaymentId)->execute();
if ($booking_id) {
$conn->prepare(
"UPDATE bookings SET status = 'PAID' WHERE booking_id = ?"
)->bind_param('i', $booking_id)->execute();
} else {
$conn->prepare(
"UPDATE membership_fees SET payment_status = 'PAID' WHERE payment_id = ?"
)->bind_param('s', $localPaymentId)->execute();
}
sendPaymentConfirmation(
getEmail($user_id),
getFullName($user_id),
$description
);
//generate $message for admin payment confirmation with payment details
$message = "Payment Confirmation\n\n";
$message .= "Payment ID: " . $localPaymentId . "\n";
$message .= "Amount: " . getPaymentAmount($localPaymentId) . "\n";
$message .= "Status: PAID\n";
$message .= "Description: " . $description . "\n";
$message .= "Thank you.\n";
$subject = "4WDCSA.co.za Payment Confirmation for Payment ID: " . $localPaymentId;
progress_log('Payment confirmation sent for payment ID: ' . $localPaymentId);
sendEmail(
$_ENV['FINANCE_EMAIL'],
$subject,
nl2br($message)
);
sendEmail(
'chrispintoza@gmail.com',
$subject,
nl2br($message)
);
sendAdminNotification($subject, nl2br($message));
}
/**
* ==========================================================
* Acknowledge webhook
* ==========================================================
*/
http_response_code(200);
echo 'OK';

10
src/api/test_log.php Normal file
View 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.";

View 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

View File

@@ -29,6 +29,59 @@ function openDatabaseConnection()
return $conn; 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() function getTripCount()
{ {
// Database connection // Database connection
@@ -464,7 +517,7 @@ function getUserMemberStatus($user_id)
} }
// Step 3: Check membership fees table for valid payment status and membership_end_date // 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); $stmtFees = $conn->prepare($queryFees);
if (!$stmtFees) { if (!$stmtFees) {
error_log("Failed to prepare fees query: " . $conn->error); error_log("Failed to prepare fees query: " . $conn->error);
@@ -487,6 +540,7 @@ function getUserMemberStatus($user_id)
$fees = $resultFees->fetch_assoc(); $fees = $resultFees->fetch_assoc();
$payment_status = $fees['payment_status']; $payment_status = $fees['payment_status'];
$membership_end_date = $fees['membership_end_date']; $membership_end_date = $fees['membership_end_date'];
$renewal_period_end = $fees['renewal_period_end'];
// Validate payment status and membership_end_date // Validate payment status and membership_end_date
$current_date = new DateTime(); $current_date = new DateTime();
@@ -495,6 +549,12 @@ function getUserMemberStatus($user_id)
if ($payment_status === "PAID" && $current_date <= $membership_end_date_obj) { if ($payment_status === "PAID" && $current_date <= $membership_end_date_obj) {
$conn->close(); $conn->close();
return true; // Direct membership is active 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 { } else {
// Direct membership is not active, check if user is linked to another active membership // 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"); error_log("Direct membership not active for user_id: $user_id - checking linked memberships");
@@ -705,6 +765,173 @@ function processPayment($payment_id, $amount, $description)
} }
function createIkhokhaPayment($payment_id, $amount, $description, $publicRef)
{
// Base requester URL: prefer explicit env var, otherwise build from request
$baseUrl = rtrim($_ENV['IKHOKHA_REQUESTER_URL'] ?? ($_SERVER['REQUEST_SCHEME'] ?? 'https') . '://' . ($_SERVER['HTTP_HOST'] ?? ''), '/');
$endpoint = $_ENV['IKHOKHA_ENDPOINT'];
$appID = $_ENV['IKHOKHA_APP_ID'];
$appSecret = $_ENV['IKHOKHA_APP_SECRET'];
$requestBody = [
"entityID" => $payment_id,
"externalEntityID" => $payment_id,
"amount" => $amount * 100,
"currency" => "ZAR",
"requesterUrl" => $_ENV['IKHOKHA_REQUESTER_URL'] ?? $baseUrl,
"description" => $description,
"paymentReference" => $description,
"mode" => $_ENV['IKHOKHA_MODE'] ?? 'live',
"externalTransactionID" => $payment_id,
"urls" => [
"callbackUrl" => $_ENV['IKHOKHA_CALLBACK_URL'],
"successPageUrl" => $_ENV['IKHOKHA_SUCCESS_URL']
. "?ref=" . urlencode($publicRef),
"failurePageUrl" => $_ENV['IKHOKHA_FAILURE_URL']
. "?ref=" . urlencode($publicRef),
"cancelUrl" => $_ENV['IKHOKHA_CANCEL_URL']
. "?ref=" . urlencode($publicRef),
]
];
$stringifiedBody = json_encode($requestBody);
$payloadToSign = createPayloadToSign($endpoint, $stringifiedBody);
$ikSign = generateSignature($payloadToSign, $appSecret);
// Initialize cURL session
$ch = curl_init($endpoint);
// Set cURL options
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $stringifiedBody);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"IK-APPID: $appID",
"IK-SIGN: $ikSign"
]);
// Execute cURL session
$response = curl_exec($ch);
curl_close($ch);
// Decode and output the response
$resp = json_decode($response, true);
// Persist provider metadata into payments table if we have a response
$conn = openDatabaseConnection();
if ($conn === null) {
return false;
}
$provider = 'ikhokha';
$provider_payment_id = $resp['paylinkID'] ?? $resp['paylinkId'] ?? $resp['paylink_id'] ?? null;
$payment_link = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
$provider_status = $resp['responseCode'] ?? ($resp['status'] ?? null);
$provider_response = json_encode($resp);
// Update payments row with provider info. If a paylink was created (responseCode == '00'), keep status awaiting payment.
$newStatus = null;
if (!empty($payment_link) && ($provider_status === '00' || $provider_status === '0' || $provider_status === 0)) {
$newStatus = 'AWAITING PAYMENT';
}
if ($newStatus) {
$stmt = $conn->prepare("UPDATE payments SET provider = ?, provider_payment_id = ?, payment_link = ?, provider_status = ?, provider_response = ?, status = ? WHERE payment_id = ? LIMIT 1");
if ($stmt) {
$stmt->bind_param('sssssss', $provider, $provider_payment_id, $payment_link, $provider_status, $provider_response, $newStatus, $payment_id);
$stmt->execute();
$stmt->close();
}
} else {
$stmt = $conn->prepare("UPDATE payments SET provider = ?, provider_payment_id = ?, payment_link = ?, provider_status = ?, provider_response = ? WHERE payment_id = ? LIMIT 1");
if ($stmt) {
$stmt->bind_param('ssssss', $provider, $provider_payment_id, $payment_link, $provider_status, $provider_response, $payment_id);
$stmt->execute();
$stmt->close();
}
}
$conn->close();
return $resp;
}
function getIkhokhaTransactionHistory($startDate, $endDate,)
{
// Base requester URL: prefer explicit env var, otherwise build from request
$endpoint = "https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=".$startDate."&endDate=".$endDate;
// $endpoint = "https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2024-02-01&endDate=2026-03-07";
$appID = $_ENV['IKHOKHA_APP_ID'];
progress_log($appID, "IKHOKHA App ID");
$appSecret = $_ENV['IKHOKHA_APP_SECRET'];
// $stringifiedBody = json_encode($requestBody);
$payloadToSign = createPayloadToSign($endpoint, null);
progress_log($payloadToSign, "IKHOKHA Payload to Sign");
$ikSign = generateSignature($payloadToSign, $appSecret);
progress_log($ikSign, "IKHOKHA Signature");
// Initialize cURL session
$ch = curl_init($endpoint);
// Set cURL options
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
// curl_setopt($ch, CURLOPT_POSTFIELDS, $stringifiedBody);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"IK-APPID: $appID",
"IK-SIGN: $ikSign"
]);
// Execute cURL session
$response = curl_exec($ch);
curl_close($ch);
// Decode and output the response
$resp = json_decode($response, true);
return $response;
}
function escapeString($str) {
$escaped = preg_replace(['/[\\"\'\"]/u', '/\x00/'], ['\\\\$0', '\\0'], (string)$str);
$cleaned = str_replace('\/', '/', $escaped);
return $cleaned;
}
function createPayloadToSign($urlPath, $body) {
$parsedUrl = parse_url($urlPath);
$basePath = $parsedUrl['path'];
if (!$basePath) {
throw new Exception("No path present in the URL");
}
$payload = $basePath . $body;
$escapedPayloadString = escapeString($payload);
return $escapedPayloadString;
}
function generateSignature($payloadToSign, $secret) {
return hash_hmac('sha256', $payloadToSign, $secret);
}
function getPaymentAmount($localPaymentId) {
$conn = openDatabaseConnection();
$stmt = $conn->prepare("SELECT amount FROM payments WHERE payment_id = ? LIMIT 1");
$stmt->bind_param("s", $localPaymentId);
$stmt->execute();
$result = $stmt->get_result();
if ($row = $result->fetch_assoc()) {
return $row['amount'];
} else {
return false; // Payment not found
}
}
function processMembershipPayment($payment_id, $amount, $description) function processMembershipPayment($payment_id, $amount, $description)
{ {
$conn = openDatabaseConnection(); $conn = openDatabaseConnection();
@@ -1282,7 +1509,7 @@ function getInitialSurname($user_id)
if ($stmt->fetch()) { if ($stmt->fetch()) {
$initial = strtoupper(substr($first_name, 0, 1)); $initial = strtoupper(substr($first_name, 0, 1));
return $initial . ". " . $last_name; return $initial . "." . $last_name;
} else { } else {
return null; return null;
} }
@@ -1293,6 +1520,89 @@ function getInitialSurname($user_id)
} }
} }
function generatePaymentRef(string $type, ?int $course_trip_id, int $user_id): string
{
$conn = openDatabaseConnection();
// 1. Normalize type
$type = strtoupper($type);
// 2. Build prefix
switch ($type) {
case 'SUBS':
$year = (int)date('Y');
$month = (int)date('n');
// If December, subscriptions are for next year
if ($month === 12) {
$year++;
}
$prefix = "SUBS_" . $year;
break;
case 'COURSE':
if (!$course_trip_id) {
throw new Exception("course_trip_id is required for COURSE payments");
}
$stmt = $conn->prepare(
"SELECT code FROM courses WHERE course_id = ?"
);
$stmt->bind_param("i", $course_trip_id);
$stmt->execute();
$stmt->bind_result($code);
if (!$stmt->fetch()) {
throw new Exception("Invalid course_id: {$course_trip_id}");
}
$stmt->close();
$prefix = "COURSE_" . strtoupper($code);
break;
case 'TRIP':
if (!$course_trip_id) {
throw new Exception("course_trip_id is required for TRIP payments");
}
$stmt = $conn->prepare(
"SELECT trip_code FROM trips WHERE trip_id = ?"
);
$stmt->bind_param("i", $course_trip_id);
$stmt->execute();
$stmt->bind_result($trip_code);
if (!$stmt->fetch()) {
throw new Exception("Invalid trip_id: {$course_trip_id}");
}
$stmt->close();
$prefix = "TRIP_" . strtoupper($trip_code);
break;
default:
throw new Exception("Unknown payment type: {$type}");
}
// 3. Get user initials + surname
$namePart = strtoupper(getInitialSurname($user_id));
if (!$namePart) {
throw new Exception("User not found for user_id: {$user_id}");
}
// 4. Add short entropy (trimmed for aesthetics)
$entropy = substr(shortEntropy(), -3);
return "{$prefix}_{$namePart}_{$entropy}";
}
function shortEntropy(): string {
return strtoupper(base_convert((string)(microtime(true) * 1000), 10, 36));
}
function getLastName($user_id) function getLastName($user_id)
{ {
$conn = openDatabaseConnection(); $conn = openDatabaseConnection();
@@ -1719,12 +2029,25 @@ function formatCurrency($amount, $currency = 'R')
function guessCountry($ip) 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); $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 $data['country']; // e.g., South Africa
} }
return null;
} }
function getUserIdFromEFT($eft_id) function getUserIdFromEFT($eft_id)
@@ -1951,6 +2274,8 @@ function processLegacyMembership($user_id) {
} }
} }
/** /**
* SECURITY WARNING: This function uses dynamic table/column names which makes it vulnerable to SQL injection. * 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. * ONLY call this function with whitelisted table and column names.
@@ -2436,18 +2761,21 @@ function validateFileUpload($file, $fileType = 'document') {
} }
// ===== CHECK 5: MIME Type Validation ===== // ===== CHECK 5: MIME Type Validation =====
$finfo = finfo_open(FILEINFO_MIME_TYPE); // Skip MIME type validation if finfo_open is not available (shared hosting compatibility)
if ($finfo === false) { // Extension validation in CHECK 4 provides sufficient security
error_log("Failed to open fileinfo resource"); $mimeType = 'application/octet-stream'; // Default fallback
return false;
}
$mimeType = finfo_file($finfo, $file['tmp_name']); if (function_exists('finfo_open')) {
finfo_close($finfo); $finfo = finfo_open(FILEINFO_MIME_TYPE);
if ($finfo !== false) {
if (!in_array($mimeType, $config['mimeTypes'], true)) { $mimeType = finfo_file($finfo, $file['tmp_name']);
error_log("Invalid MIME type '$mimeType' for type: $fileType. Expected: " . implode(', ', $config['mimeTypes'])); finfo_close($finfo);
return false;
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) ===== // ===== CHECK 6: Additional Image Validation (for images) =====
@@ -3189,3 +3517,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
View 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 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

View File

@@ -76,8 +76,8 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<h2>Manage Blog Posts</h2> <h2>Manage Blog Posts</h2>
<?php if (isset($_SESSION['message'])): ?> <?php if (isset($_SESSION['message'])): ?>
<div class="alert alert-warning message-box"> <div class="alert alert-warning message-box">
<?php echo $_SESSION['message']; ?> <?php echo $_SESSION['message']; ?>
<span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</span> <span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</span>
@@ -90,7 +90,7 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
// Determine cover image - use provided image or fallback placeholder // Determine cover image - use provided image or fallback placeholder
$coverImage = $post["image"] ? $post["image"] : 'assets/images/placeholder.jpg'; $coverImage = $post["image"] ? $post["image"] : 'assets/images/placeholder.jpg';
// Output the HTML structure with dynamic data // 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="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;"> <div class="image" style="width:200px;height:200px;">
<img src="' . htmlspecialchars($coverImage) . '" alt="' . htmlspecialchars($post["title"]) . '"> <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> <p>' . $post["description"] . '</p>
<div class="destination-footer"> <div class="destination-footer">
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;"> <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_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_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> <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>
</div> </div>
</div> </div>
'; ';
endwhile; ?> endwhile; ?>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- Blog List Area end --> <!-- Blog List Area end -->
<script> <script>
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el)); tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
// Handle publish/unpublish button clicks // Handle publish/unpublish button clicks
document.querySelectorAll('.publish-btn').forEach(btn => { document.querySelectorAll('.publish-btn').forEach(btn => {
btn.addEventListener('click', function() { btn.addEventListener('click', function() {
const blogId = this.dataset.blogId; const blogId = this.dataset.blogId;
const status = this.dataset.status; const status = this.dataset.status;
const action = status === 'published' ? 'unpublish' : 'publish'; const action = status === 'published' ? 'unpublish' : 'publish';
const endpoint = status === 'published' ? 'blog_unpublish' : 'publish_blog'; const endpoint = status === 'published' ? 'blog_unpublish' : 'publish_blog';
const formData = new FormData(); const formData = new FormData();
formData.append('id', blogId); formData.append('id', blogId);
fetch(endpoint, { fetch(endpoint, {
method: 'POST', method: 'POST',
body: formData body: formData
}) })
.then(response => { .then(response => {
if (response.ok) { if (response.ok) {
alert(action.charAt(0).toUpperCase() + action.slice(1) + ' successful!'); alert(action.charAt(0).toUpperCase() + action.slice(1) + ' successful!');
location.reload(); location.reload();
} else { } else {
alert(action + ' failed.'); alert(action + ' failed.');
console.error('Error:', response.statusText); console.error('Error:', response.statusText);
} }
}) })
.catch(err => { .catch(err => {
console.error('Error:', err); console.error('Error:', err);
alert(action + ' failed due to network error.'); alert(action + ' failed due to network error.');
}); });
});
}); });
});
</script> </script>

View File

@@ -100,6 +100,7 @@ include_once($rootPath . '/header.php');
// Output the HTML structure with dynamic data // Output the HTML structure with dynamic data
echo ' 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="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;"> <div class="image" style="border-radius:20px; width:300px;height: 250px;margin-right:0px;">
<img src="' . htmlspecialchars($blog_image) . '" alt="' . htmlspecialchars($post["title"]) . '"> <img src="' . htmlspecialchars($blog_image) . '" alt="' . htmlspecialchars($post["title"]) . '">
@@ -117,6 +118,7 @@ include_once($rootPath . '/header.php');
</div> </div>
</div> </div>
</a>
'; ';
endwhile; endwhile;

View File

@@ -192,12 +192,15 @@ $stmt->close();
document.getElementById("autosave-status").innerText = "Draft autosaved at " + new Date().toLocaleTimeString(); document.getElementById("autosave-status").innerText = "Draft autosaved at " + new Date().toLocaleTimeString();
return true; return true;
} else { } else {
document.getElementById("autosave-status").innerText = "Autosave failed"; return response.text().then(errorText => {
console.error("Autosave failed", response.statusText); document.getElementById("autosave-status").innerText = "Autosave failed: " + errorText;
return false; console.error("Autosave failed", response.status, errorText);
return false;
});
} }
}).catch(err => { }).catch(err => {
console.error("Autosave error:", err); console.error("Autosave error:", err);
document.getElementById("autosave-status").innerText = "Autosave error: " + err.message;
return false; return false;
}); });
} }

View File

@@ -7,6 +7,19 @@ require_once($rootPath . "/header.php");
checkUserSession(); 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'; $pageTitle = 'My Blog Posts';
$breadcrumbs = [['Home' => 'index'], ['Blog' => 'blog']]; $breadcrumbs = [['Home' => 'index'], ['Blog' => 'blog']];
require_once($rootPath . '/components/banner.php'); require_once($rootPath . '/components/banner.php');
@@ -19,63 +32,58 @@ $posts = $result->get_result();
?> ?>
<style> <style>
.image { .image {
width: 400px; width: 400px;
/* Set your desired width */ /* Set your desired width */
height: 350px; height: 350px;
/* Set your desired height */ /* Set your desired height */
overflow: hidden; overflow: hidden;
/* Hide any overflow */ /* Hide any overflow */
display: block; display: block;
/* Ensure proper block behavior */ /* Ensure proper block behavior */
} }
.image img { .image img {
width: 100%; width: 100%;
/* Image scales to fill the container */ /* Image scales to fill the container */
height: 100%; height: 100%;
/* Image scales to fill the container */ /* Image scales to fill the container */
object-fit: cover; object-fit: cover;
/* Fills the container while maintaining aspect ratio */ /* Fills the container while maintaining aspect ratio */
object-position: top; object-position: top;
/* Aligns the top of the image with the top of the container */ /* Aligns the top of the image with the top of the container */
display: block; display: block;
/* Prevents inline whitespace issues */ /* Prevents inline whitespace issues */
} }
</style> </style>
<?php
$bannerFolder = 'assets/images/banners/';
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
?>
<!-- Blog List Area start --> <!-- Blog List Area start -->
<section class="blog-list-page py-100 rel z-1"> <section class="blog-list-page py-100 rel z-1">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<h2>My Posts</h2> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
<?php if (isset($_SESSION['message'])): ?> <h2 style="margin: 0;">My Blog Posts</h2>
<div class="alert alert-warning message-box"> <a href="blog_create" class="theme-btn create-album-btn">
<?php echo $_SESSION['message']; ?> <i class="far fa-plus"></i> Create New Post
<span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</span> </a>
</div> </div>
<?php unset($_SESSION['message']); ?> <?php if (isset($_SESSION['message'])): ?>
<?php endif; ?> <div class="alert alert-warning message-box">
<a href="blog_create.php">+ New Post</a> <?php echo $_SESSION['message']; ?>
<span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</span>
</div>
<?php unset($_SESSION['message']);
endif;
<?php while ($post = $posts->fetch_assoc()): while ($post = $posts->fetch_assoc()):
// Determine cover image - use provided image or fallback placeholder // Determine cover image - use provided image or fallback placeholder
$coverImage = $post["image"] ? $post["image"] : 'assets/images/placeholder.jpg'; $coverImage = $post["image"] ? $post["image"] : 'assets/images/placeholder.jpg';
// Output the HTML structure with dynamic data // 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="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="image" style="width:200px;height:200px;"><img src="' . htmlspecialchars($coverImage) . '" alt="' . htmlspecialchars($post["title"]) . '"></div>
<div class="content" style="width:100%;"> <div class="content" style="width:100%;">
@@ -88,25 +96,25 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
<p>' . $post["description"] . '</p> <p>' . $post["description"] . '</p>
<div class="destination-footer"> <div class="destination-footer">
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;"> <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_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_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> <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>
</div> </div>
</div>'; </div>';
endwhile; ?> endwhile; ?>
</div>
</div>
</div>
</div> </div>
</div>
</section> </section>
<!-- Blog List Area end --> <!-- Blog List Area end -->
<script> <script>
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el)); tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
@@ -122,22 +130,22 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
formData.append('id', blogId); formData.append('id', blogId);
fetch(endpoint, { fetch(endpoint, {
method: 'POST', method: 'POST',
body: formData body: formData
}) })
.then(response => { .then(response => {
if (response.ok) { if (response.ok) {
alert(action.charAt(0).toUpperCase() + action.slice(1) + ' successful!'); alert(action.charAt(0).toUpperCase() + action.slice(1) + ' successful!');
location.reload(); location.reload();
} else { } else {
alert(action + ' failed.'); alert(action + ' failed.');
console.error('Error:', response.statusText); console.error('Error:', response.statusText);
} }
}) })
.catch(err => { .catch(err => {
console.error('Error:', err); console.error('Error:', err);
alert(action + ' failed due to network error.'); alert(action + ' failed due to network error.');
}); });
}); });
}); });
</script> </script>

View File

@@ -114,6 +114,7 @@ $user_id = $_SESSION['user_id'];
// Loop through each row // Loop through each row
while ($row = $result->fetch_assoc()) { while ($row = $result->fetch_assoc()) {
$booking_id = $row['booking_id']; $booking_id = $row['booking_id'];
$payment_id = $row['payment_id'];
$booking_type = $row['booking_type']; $booking_type = $row['booking_type'];
$from_date = $row['from_date']; $from_date = $row['from_date'];
$to_date = $row['to_date']; $to_date = $row['to_date'];
@@ -267,8 +268,8 @@ $user_id = $_SESSION['user_id'];
<div class="destination-footer"> <div class="destination-footer">
<span class="price"><span>Booking Total: R ' . number_format($amount, 2) . '</span></span>'; <span class="price"><span>Booking Total: R ' . number_format($amount, 2) . '</span></span>';
if ($status == "AWAITING PAYMENT") { if ($status == "AWAITING PAYMENT") {
echo '<a href="' . url('payment_confirmation') . '?token=' . encryptData($booking_id, $salt) . '" class="theme-btn style-two style-three"> echo '<a href="' . getPaymentLinkByPaymentId($payment_id) . '" class="theme-btn style-two style-three">
<span data-hover="PAYMENT INFO">' . $status . '</span> <span data-hover="PAY NOW">' . $status . '</span>
</a>'; </a>';
} else { } else {
echo '<a href="" class="theme-btn style-two style-three"> echo '<a href="" class="theme-btn style-two style-three">

View File

@@ -177,7 +177,7 @@ $page_id = 'driver_training';
</div> </div>
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>"> <input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<?php <?php
$button_text = "Book Now"; $button_text = "PROCEED TO PAYMENT";
$button_disabled = ""; $button_disabled = "";
if (!$result || $result->num_rows == 0) { if (!$result || $result->num_rows == 0) {
$button_text = "No booking dates available"; $button_text = "No booking dates available";
@@ -189,8 +189,9 @@ $page_id = 'driver_training';
<i class="fal fa-arrow-right"></i> <i class="fal fa-arrow-right"></i>
</button> </button>
<div class="text-center"> <div class="text-center">
<a href="contact">Need some help?</a> <a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
</div> </div>
<img src="assets/images/logos/ikhokha.png"alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
</form> </form>
</div> </div>

View File

@@ -205,30 +205,30 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
</nav> </nav>
</div> </div>
<!-- Draft Notice for Admin --> <!-- Draft Notice for Admin -->
<?php if ($is_admin && isset($row['published']) && $row['published'] == 0): ?> <?php if ($is_admin && isset($row['published']) && $row['published'] == 0): ?>
<div class="alert alert-warning mt-3" role="alert"> <div class="alert alert-warning mt-3" role="alert">
<strong><i class="fas fa-exclamation-triangle"></i> Draft Trip</strong><br> <strong><i class="fas fa-exclamation-triangle"></i> Draft Trip</strong><br>
This trip is currently in draft status and is not visible to regular users. Only admins and superadmins can preview it. This trip is currently in draft status and is not visible to regular users. Only admins and superadmins can preview it.
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- Publish/Unpublish Button --> <!-- Publish/Unpublish Button -->
<?php <?php
$user_role = getUserRole(); $user_role = getUserRole();
if (in_array($user_role, ['admin', 'superadmin'])): if (in_array($user_role, ['admin', 'superadmin'])):
// Use published status from the main query // Use published status from the main query
$is_published = $row['published'] ?? 0; $is_published = $row['published'] ?? 0;
?> ?>
<div class="admin-actions mt-20"> <div class="admin-actions mt-20">
<button type="button" class="theme-btn" style="width: 100%; id="publishBtn" onclick="toggleTripPublished(<?php echo $trip_id; ?>)"> <button type="button" class="theme-btn" style="width: 100%; id=" publishBtn" onclick="toggleTripPublished(<?php echo $trip_id; ?>)">
<?php if ($is_published): ?> <?php if ($is_published): ?>
<i class="fas fa-eye-slash"></i> Unpublish Trip <i class="fas fa-eye-slash"></i> Unpublish Trip
<?php else: ?> <?php else: ?>
<i class="fas fa-eye"></i> Publish Trip <i class="fas fa-eye"></i> Publish Trip
<?php endif; ?> <?php endif; ?>
</button> </button>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
</section> </section>
@@ -296,8 +296,8 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
</div> </div>
<span class="subtitle mb-15"><?php echo $badge_text; ?></span> <span class="subtitle mb-15"><?php echo $badge_text; ?></span>
</div> </div>
<!-- <div class="col-xl-4 col-lg-5 text-lg-end" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50"> <!-- <div class="col-xl-4 col-lg-5 text-lg-end" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
<div class="tour-header-social mb-10"> <div class="tour-header-social mb-10">
@@ -558,33 +558,33 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
</li> </li>
</ul> </ul>
<div style="margin: 20px 0;"> <div style="margin: 20px 0;">
<div id="indemnityBox" style="border: 1px solid #ccc; padding: 10px; height: 150px; overflow-y: scroll; background: #f9f9f9; font-size: 12px;"> <div id="indemnityBox" style="border: 1px solid #ccc; padding: 10px; height: 150px; overflow-y: scroll; background: #f9f9f9; font-size: 12px;">
<p><strong>INDEMNITY AND WAIVER</strong></p> <p><strong>INDEMNITY AND WAIVER</strong></p>
<p>1. I agree to abide by the Code of Conduct as listed below, as well as any reasonable instructions given by any Member of the Committee of the Club, or any person appointed by the Club to organise or control any event (Club Officer).</p> <p>1. I agree to abide by the Code of Conduct as listed below, as well as any reasonable instructions given by any Member of the Committee of the Club, or any person appointed by the Club to organise or control any event (Club Officer).</p>
<p>2. I acknowledge that driving the off-road track is inherently dangerous, and that I am fully aware of the dangers thereof. I warrant that I will make all members of my party aware of such dangers prior to driving the track.</p> <p>2. I acknowledge that driving the off-road track is inherently dangerous, and that I am fully aware of the dangers thereof. I warrant that I will make all members of my party aware of such dangers prior to driving the track.</p>
<p>3. While I, or any member of my party, enjoy the facilities at Base 4 including overnight camping, picnicking, driving the track, using the swimming pool facility or activity or any other activity while at Base 4, I agree that under no circumstances shall the Club be liable for any loss or damage of any kind whatsoever (including consequential loss) which I or any of my party may suffer, regardless of how such loss or damage may have been caused or sustained, and whether or not as a result of the negligence or breach of contract (whether fundamental or otherwise) or other wrongful act of the Club, or any Club Officer, or any of the Clubs agents or contractors, and I hereby indemnify and hold harmless the Club and any Club Officer against all such loss or damage.</p> <p>3. While I, or any member of my party, enjoy the facilities at Base 4 including overnight camping, picnicking, driving the track, using the swimming pool facility or activity or any other activity while at Base 4, I agree that under no circumstances shall the Club be liable for any loss or damage of any kind whatsoever (including consequential loss) which I or any of my party may suffer, regardless of how such loss or damage may have been caused or sustained, and whether or not as a result of the negligence or breach of contract (whether fundamental or otherwise) or other wrongful act of the Club, or any Club Officer, or any of the Clubs agents or contractors, and I hereby indemnify and hold harmless the Club and any Club Officer against all such loss or damage.</p>
<p>4. The expression, member of my party, means all persons who accompany me or attending any event at my specific invitation, request or suggestion, and includes without limitation, members of family, guests and invitees.</p> <p>4. The expression, member of my party, means all persons who accompany me or attending any event at my specific invitation, request or suggestion, and includes without limitation, members of family, guests and invitees.</p>
<p>5. I understand that I am responsible for ensuring my vehicle and equipment and that all members of my party have adequate health and medical insurance to cover any and all likely occurrences.</p> <p>5. I understand that I am responsible for ensuring my vehicle and equipment and that all members of my party have adequate health and medical insurance to cover any and all likely occurrences.</p>
<p>6. This indemnity is irrevocable and shall apply to me and the members of my party for any Club events in which I may participate or attend.</p> <p>6. This indemnity is irrevocable and shall apply to me and the members of my party for any Club events in which I may participate or attend.</p>
<p><strong>BASE 4 CODE OF CONDUCT</strong></p> <p><strong>BASE 4 CODE OF CONDUCT</strong></p>
<p>1. No motorbikes or quadbikes.</p> <p>1. No motorbikes or quadbikes.</p>
<p>2. No loud music (unless authorised by the Committee or its representatives).</p> <p>2. No loud music (unless authorised by the Committee or its representatives).</p>
<p>3. Dogs to be controlled by their owners who take full responsibility for the animals behaviour.</p> <p>3. Dogs to be controlled by their owners who take full responsibility for the animals behaviour.</p>
<p>4. No dogs belonging to non-members are allowed at Base 4 unless with the express permission of the Committee.</p> <p>4. No dogs belonging to non-members are allowed at Base 4 unless with the express permission of the Committee.</p>
<p>5. No person in the rear of open vehicles when driving on obstacles.</p> <p>5. No person in the rear of open vehicles when driving on obstacles.</p>
<p>6. When driving the obstacles stay on the tracks.</p> <p>6. When driving the obstacles stay on the tracks.</p>
<p>7. Engage 4WD when driving the obstacles to minimise wear and damage to the track.</p> <p>7. Engage 4WD when driving the obstacles to minimise wear and damage to the track.</p>
<p>8. No alcohol to be consumed while driving the track.</p> <p>8. No alcohol to be consumed while driving the track.</p>
<p>9. No littering (please pick up cigarette butts etc.)</p> <p>9. No littering (please pick up cigarette butts etc.)</p>
<p>10. All rubbish is to be taken away with you when leaving. Dustbins and refuse collection is not provided.</p> <p>10. All rubbish is to be taken away with you when leaving. Dustbins and refuse collection is not provided.</p>
<p>11. Use water sparingly. Please bring your own water and a little extra for the Club.</p> <p>11. Use water sparingly. Please bring your own water and a little extra for the Club.</p>
<p>I am a member of the Four Wheel Drive Club of Southern Africa and I will strive to uphold these Codes.</p> <p>I am a member of the Four Wheel Drive Club of Southern Africa and I will strive to uphold these Codes.</p>
</div>
<div style="margin-top: 10px;">
<input type="checkbox" id="agreeCheckbox" name="agree" disabled required>
<label for="agreeCheckbox" id="agreeLabel" style="color: #888;">I have read and agree to the indemnity terms</label>
</div>
</div> </div>
<div style="margin-top: 10px;">
<input type="checkbox" id="agreeCheckbox" name="agree" disabled required>
<label for="agreeCheckbox" id="agreeLabel" style="color: #888;">I have read and agree to the indemnity terms</label>
</div>
</div>
<h6>Total: <span id="booking_total" class="price">-</span></h6> <h6>Total: <span id="booking_total" class="price">-</span></h6>
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>"> <input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<?php if ($remaining_places < 1): ?> <?php if ($remaining_places < 1): ?>
@@ -594,13 +594,14 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
</button> </button>
<?php else: ?> <?php else: ?>
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5"> <button type="submit" class="theme-btn style-two w-100 mt-15 mb-5">
<span data-hover="Book Now">Book Now</span> <span data-hover="PROCEED TO PAYMENT">PROCEED TO PAYMENT</span>
<i class="fal fa-arrow-right"></i> <i class="fal fa-arrow-right"></i>
</button> </button>
<?php endif; ?> <?php endif; ?>
<div class="text-center"> <div class="text-center">
<a href="contact">Need some help?</a> <a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
</div> </div>
<img src="assets/images/logos/ikhokha.png" alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
</form> </form>
</div> </div>
@@ -727,7 +728,7 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
// Update button and status badge // Update button and status badge
const publishBtn = $('#publishBtn'); const publishBtn = $('#publishBtn');
const statusBadge = $('#publishStatus'); const statusBadge = $('#publishStatus');
if (response.published === 1) { if (response.published === 1) {
publishBtn.html('<i class="fas fa-eye-slash"></i> Unpublish Trip'); publishBtn.html('<i class="fas fa-eye-slash"></i> Unpublish Trip');
statusBadge.html('<span class="badge bg-success">Published</span>'); statusBadge.html('<span class="badge bg-success">Published</span>');
@@ -735,7 +736,7 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
publishBtn.html('<i class="fas fa-eye"></i> Publish Trip'); publishBtn.html('<i class="fas fa-eye"></i> Publish Trip');
statusBadge.html('<span class="badge bg-warning">Draft</span>'); statusBadge.html('<span class="badge bg-warning">Draft</span>');
} }
// Show success message // Show success message
alert(response.message); alert(response.message);
} else { } else {
@@ -750,4 +751,4 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
} }
</script> </script>
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php') ?> <?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php') ?>

View File

@@ -70,24 +70,7 @@ include_once($rootPath . '/header.php');
<div class="row"> <div class="row">
<div class="col-lg-12"> <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 <?php
// Query to retrieve upcoming published events only // Query to retrieve upcoming published events only

View File

@@ -58,20 +58,13 @@ $conn->close();
} }
.album-card { .album-card {
position: relative;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
background: white; background: white;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
} border: 1px solid #e0e0e0;
.album-card:hover {
transform: translateY(-8px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
} }
.album-image-wrapper { .album-image-wrapper {
@@ -86,11 +79,6 @@ $conn->close();
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
transition: transform 0.3s ease;
}
.album-card:hover .album-image-wrapper img {
transform: scale(1.05);
} }
.album-image-wrapper .no-image { .album-image-wrapper .no-image {
@@ -163,49 +151,21 @@ $conn->close();
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-top: auto; margin-top: auto;
align-items: center;
} }
.album-view-btn { .album-edit-icon {
flex: 1; background: none;
padding: 8px 12px;
background: #667eea;
color: white;
border: none; border: none;
border-radius: 30px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer; cursor: pointer;
transition: background 0.3s; color: inherit;
text-decoration: none; padding: 0;
text-align: center; font-size: 1.2rem;
display: block; transition: color 0.2s ease;
} }
.album-view-btn:hover { .album-edit-icon:hover {
background: #764ba2;
text-decoration: none;
color: white;
}
.album-edit-btn {
padding: 8px 12px;
background: white;
color: #667eea; 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 { .create-album-btn {
@@ -289,11 +249,11 @@ require_once($rootPath . '/components/banner.php');
</div> </div>
<div class="album-actions"> <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 View
</a> </a>
<?php if ($album['user_id'] == $current_user_id): ?> <?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> <i class="far fa-edit"></i>
</a> </a>
<?php endif; ?> <?php endif; ?>

View File

@@ -39,7 +39,8 @@ if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) {
<li>... and many more!</li> <li>... and many more!</li>
</ul> </ul>
</div> </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 <p>We go above and beyond to make your travel dreams reality hidden gems and must-see
attractions</p> attractions</p>
<a href="membership_application" class="theme-btn mt-10 style-two"> <a href="membership_application" class="theme-btn mt-10 style-two">

View File

@@ -19,6 +19,8 @@ $result = $stmt->get_result();
// Fetch single record // Fetch single record
$membership = $result->fetch_assoc(); $membership = $result->fetch_assoc();
$payment_link = getPaymentLinkByPaymentId($membership['payment_id']);
// Fetch membership application data using mysqli // Fetch membership application data using mysqli
$query = "SELECT * FROM membership_application WHERE user_id = ?"; $query = "SELECT * FROM membership_application WHERE user_id = ?";
$stmt = $conn->prepare($query); $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_amount']); ?></td>
<td><?php echo htmlspecialchars($membership['payment_id']); ?></td> <td><?php echo htmlspecialchars($membership['payment_id']); ?></td>
<?php if ($membership['payment_status'] == "PENDING") { ?> <?php if ($membership['payment_status'] == "AWAITING PAYMENT" || $membership['payment_status'] == "PENDING RENEWAL") { ?>
<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> <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 { ?> <?php } else { ?>
<td><?php echo htmlspecialchars($membership['payment_status']); ?></td> <td><?php echo htmlspecialchars($membership['payment_status']); ?></td>
<?php } ?> <?php } ?>
@@ -204,16 +206,26 @@ if (empty($application['id_number'])) {
</div> </div>
<?php <?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; $membership_end_date = $membership ? $membership['membership_end_date'] : null;
$today = date('Y-m-d'); $today = date('Y-m-d');
if ($membership_end_date && strtotime($today) > strtotime($membership_end_date)) { if ($membership_end_date) {
echo ' try {
$end = new DateTime($membership_end_date);
$threeMonthsBefore = (clone $end)->modify('-3 months')->format('Y-m-d');
} catch (Exception $e) {
// Fallback using strtotime if DateTime parsing fails
$threeMonthsBefore = date('Y-m-d', strtotime($membership_end_date . ' -3 months'));
}
if (strtotime($today) >= strtotime($threeMonthsBefore)) {
echo '
<a href="renew_membership" class="theme-btn style-two bgc-secondary" style="width:100%; margin-top: 20px; background-color: #63ab45; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;"> <a href="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> <span data-hover="Renew Membership">Renew Membership</span>
<i class="fal fa-arrow-right"></i> <i class="fal fa-arrow-right"></i>
</a>'; </a>';
}
} }
?> ?>

View File

@@ -67,29 +67,70 @@ $stmt->bind_result($user_email);
$stmt->fetch(); $stmt->fetch();
$stmt->close(); $stmt->close();
$conn->close(); // If request includes payment_id, fetch provider paylink from payments table
if (!isset($_GET['token']) || empty($_GET['token'])) {
header("Location: membership_details");
exit();
}
$token = $_GET['token'];
// echo $token;
// Sanitize the trip_id to prevent SQL injection
$payment_id = decryptData($token, $_ENV['SALT']);
$payment_link = null;
if ($payment_id) {
$pstmt = $conn->prepare("SELECT payment_link, amount, status, provider FROM payments WHERE payment_id = ? LIMIT 1");
if ($pstmt) {
$pstmt->bind_param('s', $payment_id);
$pstmt->execute();
$pres = $pstmt->get_result();
if ($prow = $pres->fetch_assoc()) {
$payment_link = $prow['payment_link'];
// prefer payments.amount if present
if (!empty($prow['amount'])) {
$payment_amount = $prow['amount'];
}
}
$pstmt->close();
}
}
?><?php ?><?php
$pageTitle = 'Membership Payment'; $pageTitle = 'Membership Payment';
$breadcrumbs = [['Home' => 'index.php'], ['Membership' => 'membership.php']]; $breadcrumbs = [['Home' => 'index.php'], ['Membership' => 'membership.php']];
require_once($rootPath . '/components/banner.php'); require_once($rootPath . '/components/banner.php');
?> ?>
<!-- Contact Form Area start --> <!-- Contact Form Area start -->
<section class="about-us-area py-100 rpb-90 rel z-1"> <section class="about-us-area py-100 rpb-90 rel z-1">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-lg-6"> <div class="col-lg-6">
<div class="section-title mb-25"> <div class="section-title mb-25">
<span class="h2 mb-15">New Membership Payment:</span> <span class="h2 mb-15">New Membership Payment:</span>
<?php echo <?php echo
'<h5>Membership Start Date: ' . $membership_start_date . '<br>Membership Renewal Date: ' . $membership_end_date . '</h5>'; ?> '<h5>Membership Start Date: ' . $membership_start_date . '<br>Membership Renewal Date: ' . $membership_end_date . '</h5>'; ?>
</div>
<?php if (!empty($payment_link)) { ?>
<h5>Payment Details:</h5>
<p>Amount: R <?php echo number_format($payment_amount, 2); ?></p>
<p>Reference: <?php echo htmlspecialchars($payment_id); ?></p>
<a href="<?php echo htmlspecialchars($payment_link); ?>" class="theme-btn style-two style-three" style="width:100%;" target="_blank" rel="noopener noreferrer">
<span data-hover="Pay Now with iKhokha">Pay Now with iKhokha</span>
<i class="fal fa-arrow-right"></i>
</a>
<div class="text-center">
<p>You will be redirected to iKhokha's Secure payment gateway.</p>
</div> </div>
<p>Your invoice has been sent to <b><?php echo htmlspecialchars($user_email); ?></b>. Please upload your proof of payment below.</p> <img src="assets/images/logos/ikhokha.png" alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
<?php } else { ?>
<p>Please upload your proof of payment below.</p>
<h5>Payment Details:</h5> <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> <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%;"> <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> <span data-hover="Submit Proof of Payment">Submit Proof of Payment</span>
<i class="fal fa-arrow-right"></i> <i class="fal fa-arrow-right"></i>
</a> </a>
<?php } ?>
</div> </div>
<div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50"> <div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
@@ -102,4 +143,4 @@ $conn->close();
</div> </div>
</section> </section>
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?> <?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>

View File

@@ -1,26 +1,101 @@
<?php <?php
require_once("env.php"); $headerStyle = 'light';
require_once("session.php"); $rootPath = dirname(dirname(dirname(__DIR__)));
require_once("connection.php"); include_once($rootPath . '/header.php');
require_once("functions.php");
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null; $is_logged_in = isset($_SESSION['user_id']);
$eft_id = strtoupper("SUBS " . date("Y") . " " . getLastName($user_id)); if (isset($_SESSION['user_id'])) {
$status = 'AWAITING PAYMENT'; $user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
$description = 'Membership Fees ' . date("Y") . " " . getLastName($user_id); } else {
header('Location: login.php');
exit(); // Stop further script execution
}
$payment_amount = 2600; // Assuming a fixed membership fee, adjust as needed //if membership_fees payment_status is PENDING RENEWAL, redirect to membership_details.php
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
$stmt->bind_param("i", $user_id);
$stmt->execute();
$stmt->bind_result($payment_status);
$stmt->fetch();
$stmt->close();
if ($payment_status === 'PENDING RENEWAL') {
header("Location: membership_details.php");
exit();
}
$payment_id = generatePaymentRef('SUBS', null, $user_id);
$payment_amount = getPriceByDescription('membership_fees');
$payment_date = date('Y-m-d'); $payment_date = date('Y-m-d');
$membership_start_date = date('Y-01-01'); $renewal_period_end = getMembershipEndDate($user_id);
$membership_end_date = date('Y-12-31'); // 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 = ?"); // Set membership_end_date to the last day of February in the following year
$stmt->bind_param("dssssi", $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $eft_id, $user_id); $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()) { if ($stmt->execute()) {
// Commit the transaction // Commit the transaction
$conn->commit(); $conn->commit();
addSubsEFT($eft_id, $user_id, $status, $payment_amount, $description);
// Audit: user initiated membership renewal
if (function_exists('auditLog')) {
auditLog($user_id, 'MEMBERSHIP_RENEWAL_INITIATED', 'membership_fees', null, ['payment_id' => $payment_id, 'amount' => $payment_amount]);
}
$checkP = $conn->prepare("SELECT COUNT(*) AS cnt FROM payments WHERE payment_id = ? LIMIT 1");
if ($checkP) {
$checkP->bind_param('s', $payment_id);
$checkP->execute();
$r = $checkP->get_result()->fetch_assoc();
$exists = intval($r['cnt']) > 0;
$checkP->close();
} else {
$exists = false;
}
if (!$exists) {
$publicRef = bin2hex(random_bytes(16));
// If current month is December, attribute the membership year to the next year
$currentYear = intval(date('Y'));
$month = intval(date('n'));
if ($month === 12) {
$membershipYear = $currentYear + 1;
} else {
$membershipYear = $currentYear;
}
$description = 'Membership Fees ' . $membershipYear . ' ' . getInitialSurname($user_id);
$status = 'AWAITING PAYMENT';
$ins = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, public_ref) VALUES (?, ?, ?, ?, ?, ?)");
if ($ins) {
$ins->bind_param('sidsss', $payment_id, $user_id, $payment_amount, $status, $description, $publicRef);
$ins->execute();
$ins->close();
}
}
// Create iKhokha paylink via helper (functions.php)
try {
$publicRef = $publicRef ?? bin2hex(random_bytes(16));
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
$token = encryptData($payment_id, $_ENV['SALT']);
if ($paylink) {
header('Location: membership_payment?token=' . $token);
exit();
} else {
header("Location: membership_details");
exit();
}
} catch (Exception $e) {
// Log but do not fail signature save
error_log('iKhokha create error: ' . $e->getMessage());
}
header("Location:membership_payment.php"); header("Location:membership_payment.php");
// Success message // Success message
$response = [ $response = [

View File

@@ -156,7 +156,7 @@ $page_id = 'bush_mechanics';
</div> </div>
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>"> <input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<?php <?php
$button_text = "Book Now"; $button_text = "PROCEED TO PAYMENT";
$button_disabled = ""; $button_disabled = "";
if (!$result || $result->num_rows == 0) { if (!$result || $result->num_rows == 0) {
$button_text = "No booking dates available"; $button_text = "No booking dates available";
@@ -168,8 +168,9 @@ $page_id = 'bush_mechanics';
<i class="fal fa-arrow-right"></i> <i class="fal fa-arrow-right"></i>
</button> </button>
<div class="text-center"> <div class="text-center">
<a href="contact">Need some help?</a> <a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
</div> </div>
<img src="assets/images/logos/ikhokha.png"alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
</form> </form>
</div> </div>

View File

@@ -105,17 +105,29 @@ if (isset($_SESSION['user_id'])) {
response = JSON.parse(response); response = JSON.parse(response);
} }
if (response.status === 'success') { if (response.status === 'success') {
// Check if the user has paid // If provider returned a direct paylink, go there immediately
if (response.paylinkUrl) {
window.location.href = 'membership_payment?token=' + encodeURIComponent(response.token);
return;
}
// If we have a payment_id, redirect to membership_payment with it
// if (response.payment_id) {
// setTimeout(function() {
// window.location.href = 'membership_payment.php?payment_id=' + encodeURIComponent(response.token);
// }, 800);
// return;
// }
// Fallback behaviour: check paymentStatus
if (response.paymentStatus === 'PAID') { if (response.paymentStatus === 'PAID') {
// Redirect to membership_details.php if paid
setTimeout(function() { setTimeout(function() {
window.location.href = 'membership_details.php'; window.location.href = 'membership_details.php';
}, 2000); // 2-second delay before redirecting }, 1200);
} else { } else {
// Redirect to membership_payment.php if not paid
setTimeout(function() { setTimeout(function() {
window.location.href = 'membership_payment.php'; window.location.href = 'membership_payment.php';
}, 2000); // 2-second delay before redirecting }, 1200);
} }
} else { } else {
$('#responseMessage').html('<div class="alert alert-danger">' + response.message + '</div>'); $('#responseMessage').html('<div class="alert alert-danger">' + response.message + '</div>');

View File

@@ -154,7 +154,7 @@ $page_id = 'rescue_recovery';
</div> </div>
</div> </div>
<?php <?php
$button_text = "Book Now"; $button_text = "PROCEED TO PAYMENT";
$button_disabled = ""; $button_disabled = "";
if (!$result || $result->num_rows == 0) { if (!$result || $result->num_rows == 0) {
$button_text = "No booking dates available"; $button_text = "No booking dates available";
@@ -165,9 +165,11 @@ $page_id = 'rescue_recovery';
<span data-hover="<?php echo $button_text; ?>"><?php echo $button_text; ?></span> <span data-hover="<?php echo $button_text; ?>"><?php echo $button_text; ?></span>
<i class="fal fa-arrow-right"></i> <i class="fal fa-arrow-right"></i>
</button> </button>
<div class="text-center"> <div class="text-center">
<a href="mailto:info@4wdcsa.co.za">Need some help?</a> <a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
</div> </div>
<img src="assets/images/logos/ikhokha.png"alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
</form> </form>
</div> </div>

View 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'); ?>

View 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'); ?>

View 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
View 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(); ?>

View File

@@ -5,6 +5,11 @@ require_once($rootPath . "/src/config/connection.php");
require_once($rootPath . "/src/config/functions.php"); require_once($rootPath . "/src/config/functions.php");
session_start(); 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'])) { if (!isset($_SESSION['user_id'])) {
http_response_code(401); http_response_code(401);
echo "Not authorized"; echo "Not authorized";
@@ -32,36 +37,42 @@ echo $author_id;
$cover_image_path = null; $cover_image_path = null;
// Only attempt upload if a file was submitted // 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 . "/"; $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 // Simple validation - check extension
$file_result = validateFileUpload($_FILES['cover_image'], 'profile_picture'); $extension = strtolower(pathinfo($_FILES['cover_image']['name'], PATHINFO_EXTENSION));
if ($file_result === false) { $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array($extension, $allowedExtensions)) {
http_response_code(400); http_response_code(400);
echo "Invalid file upload"; echo "Invalid file type. Allowed: jpg, jpeg, png, gif, webp";
exit; exit;
} }
// Use fixed filename "cover" to avoid creating multiple copies on autosave // Use fixed filename "cover" to avoid creating multiple copies on autosave
$extension = $file_result['extension'];
$filename = "cover." . $extension; $filename = "cover." . $extension;
// Delete old cover if it exists with different 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; $targetPath = $uploadDir . $filename;
$cover_image_path = "/uploads/blogs/" . $article_id . "/" . $filename; $cover_image_path = "/uploads/blogs/" . $article_id . "/" . $filename;
// Move the uploaded file // Move the uploaded file
if (move_uploaded_file($_FILES['cover_image']['tmp_name'], $targetPath)) { if (!move_uploaded_file($_FILES['cover_image']['tmp_name'], $targetPath)) {
// File moved successfully, $cover_image_path is set
} else {
http_response_code(500); http_response_code(500);
echo "Failed to move uploaded file."; echo "Failed to move uploaded file";
exit; exit;
} }
} }

View File

@@ -26,8 +26,8 @@ if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_
$upload_dir = $rootPath . '/uploads/blogs/' . $folder_id . '/'; $upload_dir = $rootPath . '/uploads/blogs/' . $folder_id . '/';
// Create directory if it doesn't exist // Create directory if it doesn't exist
if (!is_dir($upload_dir)) { if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0755, true); mkdir($upload_dir, 0777, true);
} }
// Validate and process the file // Validate and process the file

View File

@@ -0,0 +1,49 @@
<?php
ob_start();
header('Content-Type: application/json');
$rootPath = dirname(dirname(__DIR__));
require_once($rootPath . "/src/config/env.php");
require_once($rootPath . '/src/config/functions.php');
require_once($rootPath . '/src/config/connection.php');
// Check admin status
session_start();
if (empty($_SESSION['user_id'])) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
$user_role = getUserRole();
if (!in_array($user_role, ['admin', 'superadmin'])) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
try {
$course_id = intval($_POST['course_id'] ?? 0);
if ($course_id <= 0) {
throw new Exception('Invalid course ID');
}
$stmt = $conn->prepare("DELETE FROM courses WHERE course_id = ?");
$stmt->bind_param("i", $course_id);
if (!$stmt->execute()) {
throw new Exception('Failed to delete course: ' . $stmt->error);
}
$stmt->close();
ob_end_clean();
echo json_encode(['status' => 'success', 'message' => 'Course deleted successfully']);
} catch (Exception $e) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
?>

View 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()]);
}

View File

@@ -6,9 +6,17 @@ require_once($rootPath . "/src/config/connection.php");
require_once($rootPath . "/src/config/functions.php"); require_once($rootPath . "/src/config/functions.php");
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null; $user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
$eft_id = strtoupper($user_id." SUBS ".date("Y")." ".getInitialSurname($user_id)); $payment_id = generatePaymentRef('SUBS', null, $user_id);
$status = 'AWAITING PAYMENT'; $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') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF Token Validation // CSRF Token Validation
@@ -174,38 +182,49 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($stmt->execute()) { if ($stmt->execute()) {
// Insert into the membership fees table // 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(); $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 ($month == 12 || $month == 1 || $month == 2) {
if ($today->format('n') > 2) { // December, January, February: charge full fee, valid till end of next Feb
// If we're past February, target is next year's Feb 28/29 $payment_amount = getPriceByDescription('membership_fees');
$year = $today->format('Y') + 1; // 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 { } else {
// Otherwise, this year's February // Prorata for Mar-Nov
$year = $today->format('Y'); $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 $stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, renewal_period_end, payment_status, payment_id)
$membership_end_date = (new DateTime("$year-02-01")) VALUES (?, ?, ?, ?, ?, ?, 'AWAITING PAYMENT', ?)");
->modify('last day of this month') $stmt->bind_param("idsssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $membership_end_date, $payment_id);
->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);
if ($stmt->execute()) { if ($stmt->execute()) {
// Commit the transaction // Commit the transaction
$conn->commit(); $conn->commit();
addSubsEFT($eft_id, $user_id, $status, $payment_amount, $description); // Do not create legacy EFTs. Create a payments-ready membership fee and notify admin.
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description); // Optionally send an invoice referencing the internal payment id
sendAdminNotification('4WDCSA.co.za - New Membership Application - '.$last_name , 'A new member has signed up, '.$first_name.' '.$last_name); // sendInvoice(getEmail($user_id), getFullName($user_id), $payment_id, formatCurrency($payment_amount), $description);
// sendAdminNotification('4WDCSA.co.za - New Membership Application - '.$last_name , 'A new member has signed up, '.$first_name.' '.$last_name);
// Audit: membership application submitted
if (function_exists('auditLog')) {
auditLog($user_id, 'MEMBERSHIP_APPLICATION_SUBMITTED', 'membership_application', null, ['payment_id' => $payment_id, 'amount' => $payment_amount ?? null]);
}
header("Location: indemnity"); header("Location: indemnity");
// Success message // Success message
$response = [ $response = [

View File

@@ -79,6 +79,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt->bind_param('sissiiiidd', $type, $user_id, $from_date, $to_date, $num_vehicles, $num_adults, $num_children, $add_firewood, $total_amount, $discount_amount); $stmt->bind_param('sissiiiidd', $type, $user_id, $from_date, $to_date, $num_vehicles, $num_adults, $num_children, $add_firewood, $total_amount, $discount_amount);
if ($stmt->execute()) { if ($stmt->execute()) {
// Get booking id and audit
$booking_id = $conn->insert_id;
if (function_exists('auditLog')) {
auditLog($user_id, 'BOOKING_CREATED', 'bookings', $booking_id, ['total_amount' => $total_amount, 'from' => $from_date, 'to' => $to_date]);
}
// Redirect to success page or display success message // Redirect to success page or display success message
echo "<script>alert('Booking successfully created!'); window.location.href = 'booking.php';</script>"; echo "<script>alert('Booking successfully created!'); window.location.href = 'booking.php';</script>";
} else { } else {

View File

@@ -0,0 +1,100 @@
<?php
ob_start();
header('Content-Type: application/json');
$rootPath = dirname(dirname(__DIR__));
require_once($rootPath . "/src/config/env.php");
require_once($rootPath . '/src/config/functions.php');
require_once($rootPath . '/src/config/connection.php');
// Check admin status
session_start();
if (empty($_SESSION['user_id'])) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
$user_role = getUserRole();
if (!in_array($user_role, ['admin', 'superadmin'])) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
try {
$course_id = $_POST['course_id'] ?? null;
$course_type = trim($_POST['course_type'] ?? '');
$code = trim($_POST['code'] ?? '');
$date = trim($_POST['date'] ?? '');
$capacity = intval($_POST['capacity'] ?? 0);
$cost_members = floatval($_POST['cost_members'] ?? 0);
$cost_nonmembers = floatval($_POST['cost_nonmembers'] ?? 0);
$instructor = trim($_POST['instructor'] ?? '');
$instructor_email = trim($_POST['instructor_email'] ?? '');
$allowed_types = ['driver_training','bush_mechanics','rescue_recovery','ladies_driver_training'];
if (!in_array($course_type, $allowed_types)) {
throw new Exception('Invalid course type');
}
if (empty($date) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
throw new Exception('Invalid date format');
}
// If code not provided, generate from type + date using ABBR_MMDD format
if (empty($code)) {
$abbrMap = [
'driver_training' => 'DRVTRN',
'bush_mechanics' => 'BUSHMEC',
'rescue_recovery' => 'RESREC',
'ladies_driver_training' => 'LADYTRN'
];
$abbr = $abbrMap[$course_type] ?? strtoupper(preg_replace('/[^A-Z0-9]/', '', $course_type));
// ensure abbr fits (reserve 1 char for underscore and 4 for MMDD)
$abbr = substr($abbr, 0, 7);
$mmdd = date('md', strtotime($date));
$code = strtoupper(substr($abbr . '_' . $mmdd, 0, 12));
}
if ($capacity <= 0) {
throw new Exception('Capacity must be greater than 0');
}
if (empty($instructor)) {
throw new Exception('Instructor name is required');
}
if ($course_id) {
// Update
$stmt = $conn->prepare("UPDATE courses SET course_type = ?, code = ?, date = ?, capacity = ?, cost_members = ?, cost_nonmembers = ?, instructor = ?, instructor_email = ? WHERE course_id = ?");
$stmt->bind_param("sssiddssi", $course_type, $code, $date, $capacity, $cost_members, $cost_nonmembers, $instructor, $instructor_email, $course_id);
if (!$stmt->execute()) {
throw new Exception('Failed to update course: ' . $stmt->error);
}
$stmt->close();
} else {
// Insert - booked defaults to 0
$stmt = $conn->prepare("INSERT INTO courses (course_type, code, date, capacity, booked, cost_members, cost_nonmembers, instructor, instructor_email) VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?)");
$stmt->bind_param("sssiddss", $course_type, $code, $date, $capacity, $cost_members, $cost_nonmembers, $instructor, $instructor_email);
if (!$stmt->execute()) {
throw new Exception('Failed to create course: ' . $stmt->error);
}
$course_id = $conn->insert_id;
$stmt->close();
}
ob_end_clean();
echo json_encode(['status' => 'success', 'message' => $course_id ? 'Course saved successfully' : 'Course created successfully', 'course_id' => $course_id]);
} catch (Exception $e) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
?>

View File

@@ -93,10 +93,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$status = "AWAITING PAYMENT"; $status = "AWAITING PAYMENT";
$type = 'course'; $type = 'course';
$payment_id = uniqid(); $payment_id = generatePaymentRef('COURSE', $course_id, $user_id);
$publicRef = bin2hex(random_bytes(16));
$num_vehicles = 1; $num_vehicles = 1;
$discountAmount = 0; $discountAmount = 0;
$eft_id = strtoupper("COURSE ".date("m-d", strtotime($date))." ".getInitialSurname($user_id)); $eft_id = $payment_id;
$notes = ""; $notes = "";
if ($pending_member){ if ($pending_member){
$notes = "Membership Payment pending at time of booking. Please confirm payment has been received."; $notes = "Membership Payment pending at time of booking. Please confirm payment has been received.";
@@ -117,6 +118,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($stmt->execute()) { if ($stmt->execute()) {
$booking_id = $conn->insert_id; $booking_id = $conn->insert_id;
// Audit booking creation
if (function_exists('auditLog')) {
auditLog($user_id, 'COURSE_BOOKING_CREATED', 'bookings', $booking_id, ['course_id' => $course_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
}
if ($payment_amount < 1) { if ($payment_amount < 1) {
if (processZeroPayment($payment_id, $payment_amount, $description)) { if (processZeroPayment($payment_id, $payment_amount, $description)) {
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>"; echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
@@ -125,11 +131,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
echo "Error processing booking: $error_message"; echo "Error processing booking: $error_message";
} }
} else { } else {
addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description); // Create payments row
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description); $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); 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 { } else {
// Handle error if insert fails and echo the MySQL error // Handle error if insert fails and echo the MySQL error

View File

@@ -1,6 +1,11 @@
<?php <?php
$rootPath = dirname(dirname(__DIR__)); $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(); checkAdmin();
header('Content-Type: application/json'); header('Content-Type: application/json');
@@ -49,7 +54,7 @@ if ($_GET['action'] ?? null === 'delete') {
} }
// Check CSRF token // 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']); echo json_encode(['status' => 'error', 'message' => 'CSRF token validation failed']);
exit; exit;
} }
@@ -73,17 +78,17 @@ if (!$name || !$type || !$location || !$date || !$time || !$feature || !$descrip
$image_path = null; $image_path = null;
if (!empty($_FILES['image']['name'])) { if (!empty($_FILES['image']['name'])) {
$upload_dir = $rootPath . '/assets/images/events/'; $upload_dir = $rootPath . '/assets/images/events/';
if (!is_dir($upload_dir)) { if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0755, true); mkdir($upload_dir, 0777, true);
} }
$file_name = uniqid() . '_' . basename($_FILES['image']['name']); $file_name = uniqid() . '_' . basename($_FILES['image']['name']);
$target_file = $upload_dir . $file_name; $target_file = $upload_dir . $file_name;
$file_type = mime_content_type($_FILES['image']['tmp_name']);
// Validate image file // Validate file extension
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; $ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
if (!in_array($file_type, $allowed_types)) { $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']); echo json_encode(['status' => 'error', 'message' => 'Invalid image file type. Only JPEG, PNG, GIF, and WebP are allowed']);
exit; exit;
} }
@@ -103,17 +108,17 @@ if (!empty($_FILES['image']['name'])) {
$promo_path = null; $promo_path = null;
if (!empty($_FILES['promo']['name'])) { if (!empty($_FILES['promo']['name'])) {
$upload_dir = $rootPath . '/assets/images/events/'; $upload_dir = $rootPath . '/assets/images/events/';
if (!is_dir($upload_dir)) { if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0755, true); mkdir($upload_dir, 0777, true);
} }
$file_name = uniqid() . '_promo_' . basename($_FILES['promo']['name']); $file_name = uniqid() . '_promo_' . basename($_FILES['promo']['name']);
$target_file = $upload_dir . $file_name; $target_file = $upload_dir . $file_name;
$file_type = mime_content_type($_FILES['promo']['tmp_name']);
// Validate image file // Validate file extension
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; $ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
if (!in_array($file_type, $allowed_types)) { $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']); echo json_encode(['status' => 'error', 'message' => 'Invalid promo image file type. Only JPEG, PNG, GIF, and WebP are allowed']);
exit; exit;
} }

View File

@@ -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>"; echo "<script>alert('User is not logged in. Please log in to make a booking.'); window.location.href = 'login.php';</script>";
exit(); 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); $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->bind_param('i', $user_id);
$stmt->execute(); $stmt->execute();
$result = $stmt->get_result(); $result = $stmt->get_result();
// Check if trip exists // Check if membership fee exists
if ($result->num_rows === 0) { if ($result->num_rows === 0) {
$response = ['error' => 'Application Fee not found.']; $response = ['error' => 'Membership fee not found.'];
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode($response); echo json_encode($response);
exit(); exit();
} }
// Fetch trip details // Fetch fee details
$fee = $result->fetch_assoc(); $fee = $result->fetch_assoc();
$fee_id = isset($fee['fee_id']) ? intval($fee['fee_id']) : null;
$payment_status = $fee['payment_status']; $payment_status = $fee['payment_status'];
$membership_end_date = $fee['membership_end_date']; $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"); $description = "4WDCSA: Membership Fee " . getFullName($user_id) . " " . date("Y");
$payment_id = uniqid(); $payment_id = uniqid();
$eft_id = "SUBS 2025 ".getLastName($user_id);
// Update the membership_fees table to set payment_id // Persist the generated payment_id back to the membership_fees row (use fee_id to be precise)
$stmt = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE user_id = ?"); $updateStmt = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE fee_id = ?");
if ($stmt) { if ($updateStmt) {
$stmt->bind_param("ss", $payment_id, $user_id); $updateStmt->bind_param("si", $payment_id, $fee_id);
if (!$updateStmt->execute()) {
if (!$stmt->execute()) { throw new Exception("Failed to update membership_fees table: " . $updateStmt->error);
throw new Exception("Failed to update membership_fees table.");
} }
$updateStmt->close();
$stmt->close();
$conn->close();
} else { } else {
throw new Exception("Failed to prepare statement for membership_fees table: " . $conn->error); throw new Exception("Failed to prepare statement for membership_fees table: " . $conn->error);
} }
// Get the current date // If the amount is zero, treat as paid immediately
$current_date = new DateTime(); 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 // Create iKhokha payment link
$membership_end_date_obj = DateTime::createFromFormat('Y-m-d', $membership_end_date); $resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
// Check if the current date is after membership_end_date // Send invoice and admin notification if desired
// OR if the current date is before or on membership_end_date AND payment_status is "PENDING" // sendInvoice(getEmail($user_id), getFullName($user_id), 'MEMBERSHIP-'.date('Y'), formatCurrency($payment_amount), $description);
if ( sendAdminNotification('Membership Payment Initiated - '.getFullName($user_id), getFullName($user_id).' initiated a membership payment.');
$current_date > $membership_end_date_obj ||
($current_date <= $membership_end_date_obj && $payment_status === "PENDING")
) {
// Call the processMembershipPayment function // Redirect user to payment link if available
// processMembershipPayment($payment_id, $payment_amount, $description); $paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
addMembershipEFT($eft_id, $user_id, $status, $amount, $description, $membershipfee_id); if ($paylink) {
header("Location: payment_confirmation?booking_id=" . $booking_id); header('Location: ' . $paylink);
exit(); // Ensure no further code is executed after the redirect exit();
} else {
// Fallback: redirect to a membership page with an encrypted token
header("Location: membership_confirmation?token=" . encryptData($payment_id, $salt));
exit();
}
} }

View File

@@ -36,7 +36,7 @@ if (isset($_POST['signature'])) {
$filePath = $rootPath . '/uploads/signatures/' . $fileName; $filePath = $rootPath . '/uploads/signatures/' . $fileName;
// Ensure the directory exists // Ensure the directory exists
if (!is_dir($rootPath . '/uploads/signatures')) { if (!file_exists($rootPath . '/uploads/signatures')) {
mkdir($rootPath . '/uploads/signatures', 0777, true); mkdir($rootPath . '/uploads/signatures', 0777, true);
} }
@@ -56,17 +56,122 @@ if (isset($_POST['signature'])) {
$stmt->bind_param('si', $display_path, $user_id); $stmt->bind_param('si', $display_path, $user_id);
if ($stmt->execute()) { if ($stmt->execute()) {
// Audit: signature saved
if (function_exists('auditLog')) {
auditLog($user_id, 'SIGNATURE_SAVED', 'membership_application', null, ['path' => $display_path]);
}
// Check the payment status // Check the payment status
$paymentStatus = checkMembershipPaymentStatus($user_id) ? 'PAID' : 'NOT_PAID'; $paymentStatus = checkMembershipPaymentStatus($user_id) ? 'PAID' : 'NOT_PAID';
// Respond with the appropriate redirect URL based on the payment status // If not paid, create a payments row (if missing) and initiate iKhokha paylink
$paylink = null;
if ($paymentStatus !== 'PAID') {
// Fetch the membership fee row to get amount and payment_id
$mfStmt = $conn->prepare("SELECT fee_id, payment_amount, payment_id FROM membership_fees WHERE user_id = ? ORDER BY fee_id DESC LIMIT 1");
if ($mfStmt) {
$mfStmt->bind_param('i', $user_id);
$mfStmt->execute();
$mfRes = $mfStmt->get_result();
$mf = $mfRes->fetch_assoc();
$mfStmt->close();
} else {
$mf = null;
}
if ($mf && isset($mf['payment_amount'])) {
$amount = floatval($mf['payment_amount']);
// Use existing payment_id or generate one
$payment_id = $mf['payment_id'] ?? generatePaymentRef('SUBS', null, $user_id);;
if (empty($mf['payment_id'])) {
// Persist generated payment_id back to membership_fees
$u = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE fee_id = ?");
if ($u) {
$u->bind_param('si', $payment_id, $mf['fee_id']);
$u->execute();
$u->close();
}
}
// Ensure a payments row exists
$checkP = $conn->prepare("SELECT COUNT(*) AS cnt FROM payments WHERE payment_id = ? LIMIT 1");
if ($checkP) {
$checkP->bind_param('s', $payment_id);
$checkP->execute();
$r = $checkP->get_result()->fetch_assoc();
$exists = intval($r['cnt']) > 0;
$checkP->close();
} else {
$exists = false;
}
if (!$exists) {
$publicRef = bin2hex(random_bytes(16));
// If current month is December, attribute the membership year to the next year
$currentYear = intval(date('Y'));
$month = intval(date('n'));
if ($month === 12) {
$membershipYear = $currentYear + 1;
} else {
$membershipYear = $currentYear;
}
$description = 'Membership Fees ' . $membershipYear . ' ' . getInitialSurname($user_id);
$status = 'AWAITING PAYMENT';
$ins = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, public_ref) VALUES (?, ?, ?, ?, ?, ?)");
if ($ins) {
$ins->bind_param('sidsss', $payment_id, $user_id, $amount, $status, $description, $publicRef);
if ($ins->execute()) {
// Audit: payment row created for membership
if (function_exists('auditLog')) {
auditLog($user_id, 'MEMBERSHIP_PAYMENT_CREATED', 'payments', null, ['payment_id' => $payment_id, 'amount' => $amount]);
}
}
$ins->close();
}
}
// Create iKhokha paylink via helper (functions.php)
try {
$publicRef = $publicRef ?? bin2hex(random_bytes(16));
$resp = createIkhokhaPayment($payment_id, $amount, $desc ?? ('Membership Fee ' . date('Y')), $publicRef);
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
// After creating paylink, update paymentStatus to AWAITING PAYMENT
$paymentStatus = $paylink ? 'AWAITING PAYMENT' : $paymentStatus;
$token = encryptData($payment_id, $_ENV['SALT']);
// Audit: paylink created (or attempted)
if (function_exists('auditLog')) {
auditLog($user_id, 'IKHOKHA_PAYLINK_CREATED', 'payments', null, ['payment_id' => $payment_id, 'paylink' => $paylink]);
}
} catch (Exception $e) {
// Log but do not fail signature save
error_log('iKhokha create error: ' . $e->getMessage());
if (function_exists('auditLog')) {
auditLog($user_id, 'IKHOKHA_PAYLINK_FAILED', 'payments', null, ['payment_id' => $payment_id, 'error' => $e->getMessage()]);
}
}
}
}
// Respond with the appropriate redirect URL and paylink (if created)
ob_end_clean(); ob_end_clean();
echo json_encode([ $response = [
'status' => 'success', 'status' => 'success',
'message' => 'Signature saved successfully!', 'message' => 'Signature saved successfully!',
'paymentStatus' => $paymentStatus // Send payment status 'paymentStatus' => $paymentStatus,
]); 'token' => $token ?? null
];
if (!empty($paylink)) {
$response['paylinkUrl'] = $paylink;
}
if (!empty($payment_id)) {
$response['payment_id'] = $payment_id;
}
echo json_encode($response);
} else { } else {
// Audit: signature save failed
if (function_exists('auditLog')) {
auditLog($user_id, 'SIGNATURE_SAVE_FAILED', 'membership_application', null, ['user_id' => $user_id]);
}
ob_end_clean(); ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Database update failed']); echo json_encode(['status' => 'error', 'message' => 'Database update failed']);
} }
@@ -78,6 +183,10 @@ if (isset($_POST['signature'])) {
echo json_encode(['status' => 'error', 'message' => 'Failed to save signature']); echo json_encode(['status' => 'error', 'message' => 'Failed to save signature']);
} }
} else { } else {
// Audit: no signature provided in request
if (function_exists('auditLog') && isset($_SESSION['user_id'])) {
auditLog($_SESSION['user_id'], 'SIGNATURE_NOT_PROVIDED', 'membership_application', null, ['endpoint' => 'process_signature.php']);
}
ob_end_clean(); ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Signature not provided']); echo json_encode(['status' => 'error', 'message' => 'Signature not provided']);
} }

View File

@@ -136,8 +136,8 @@ try {
$upload_dir = $rootPath . '/assets/images/trips/'; $upload_dir = $rootPath . '/assets/images/trips/';
// Create directory if it doesn't exist // Create directory if it doesn't exist
if (!is_dir($upload_dir)) { if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0755, true); mkdir($upload_dir, 0777, true);
} }
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];

View File

@@ -78,6 +78,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$member_discount = $cost_nonmembers - $cost_members; $member_discount = $cost_nonmembers - $cost_members;
$member_discount_pensioner = $cost_pensioner - $cost_pensioner_member; $member_discount_pensioner = $cost_pensioner - $cost_pensioner_member;
$booking_fee = $trip['booking_fee']; $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; $radioCost = $radio ? 50 : 0;
$start_date = $trip['start_date']; // Start date of the trip $start_date = $trip['start_date']; // Start date of the trip
$end_date = $trip['end_date']; // End date of the trip $end_date = $trip['end_date']; // End date of the trip
@@ -103,9 +105,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$status = "AWAITING PAYMENT"; $status = "AWAITING PAYMENT";
$description = $trip_name; $description = $trip_name;
$type = 'trip'; $type = 'trip';
$payment_id = uniqid(); $payment_id = generatePaymentRef('TRIP', $trip_id, $user_id);
$publicRef = bin2hex(random_bytes(16));
// $eft_id = strtoupper(base_convert(time(), 10, 36)); // Convert timestamp to base36 // $eft_id = strtoupper(base_convert(time(), 10, 36)); // Convert timestamp to base36
$eft_id = strtoupper($trip_code." ".getInitialSurname($user_id)); // $eft_id = strtoupper($trip_code." ".getInitialSurname($user_id));
// Insert booking into the database // Insert booking into the database
@@ -123,6 +126,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Get the generated booking_id // Get the generated booking_id
$booking_id = $conn->insert_id; $booking_id = $conn->insert_id;
// Audit booking creation
if (function_exists('auditLog')) {
auditLog($user_id, 'TRIP_BOOKING_CREATED', 'bookings', $booking_id, ['trip_id' => $trip_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
}
if ($payment_amount < 1) { if ($payment_amount < 1) {
if (processZeroPayment($payment_id, $payment_amount, $description)) { if (processZeroPayment($payment_id, $payment_amount, $description)) {
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>"; echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
@@ -131,11 +139,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
echo "Error processing booking: $error_message"; echo "Error processing booking: $error_message";
} }
} else { } else {
addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description); // Create payments row
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description); $pstmt = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, booking_id, public_ref) VALUES (?, ?, ?, ?, ?, ?, ?)");
sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description); if ($pstmt) {
header("Location: payment_confirmation?token=".encryptData($booking_id, $salt)); $pstmt->bind_param('sidssis', $payment_id, $user_id, $payment_amount, $status, $description, $booking_id, $publicRef);
exit(); // Ensure no further code is executed after the redirect $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 { } else {
// Handle error if insert fails and echo the MySQL error // Handle error if insert fails and echo the MySQL error

View File

@@ -52,26 +52,25 @@ try {
// Create album directory // Create album directory
$albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id; $albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id;
if (!is_dir($albumDir)) { if (!file_exists($albumDir)) {
if (!mkdir($albumDir, 0755, true)) { mkdir($albumDir, 0777, true);
throw new Exception('Failed to create album directory');
}
} }
// Handle cover image upload // Handle cover image upload
$coverImagePath = null; $coverImagePath = null;
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] !== UPLOAD_ERR_NO_FILE) { if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB $maxSize = 5 * 1024 * 1024; // 5MB
$fileName = $_FILES['cover_image']['name']; $fileName = $_FILES['cover_image']['name'];
$fileTmpName = $_FILES['cover_image']['tmp_name']; $fileTmpName = $_FILES['cover_image']['tmp_name'];
$fileSize = $_FILES['cover_image']['size']; $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($ext, $allowedExtensions)) {
if (!in_array($fileMime, $allowedMimes)) { throw new Exception('Invalid cover image file type. Allowed: jpg, jpeg, png, gif, webp');
throw new Exception('Invalid cover image file type');
} }
if ($fileSize > $maxSize) { if ($fileSize > $maxSize) {
@@ -96,8 +95,7 @@ try {
} }
// Handle photo uploads // Handle photo uploads
if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] !== UPLOAD_ERR_NO_FILE) { if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] === UPLOAD_ERR_OK) {
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB $maxSize = 5 * 1024 * 1024; // 5MB
$displayOrder = 1; $displayOrder = 1;
@@ -111,11 +109,13 @@ try {
$fileName = $_FILES['photos']['name'][$i]; $fileName = $_FILES['photos']['name'][$i];
$fileTmpName = $_FILES['photos']['tmp_name'][$i]; $fileTmpName = $_FILES['photos']['tmp_name'][$i];
$fileSize = $_FILES['photos']['size'][$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($ext, $allowedExtensions)) {
if (!in_array($fileMime, $allowedMimes)) { throw new Exception('Invalid file type: ' . $fileName . '. Allowed: jpg, jpeg, png, gif, webp');
throw new Exception('Invalid file type: ' . $fileName);
} }
if ($fileSize > $maxSize) { if ($fileSize > $maxSize) {

View File

@@ -43,14 +43,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$filename = str_replace(' ', '_', $eft_id) . '.pdf'; $filename = str_replace(' ', '_', $eft_id) . '.pdf';
$target_file = $target_dir . $filename; $target_file = $target_dir . $filename;
// Make sure target directory exists and writable // Make sure target directory exists
if (!is_dir($target_dir)) { if (!file_exists($target_dir)) {
mkdir($target_dir, 0755, true); mkdir($target_dir, 0777, true);
}
if (!is_writable($target_dir)) {
echo "<div class='alert alert-danger'>Upload directory is not writable: $target_dir</div>";
exit;
} }
if (move_uploaded_file($_FILES['pop_file']['tmp_name'], $target_file)) { if (move_uploaded_file($_FILES['pop_file']['tmp_name'], $target_file)) {

View 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();
?>

View File

@@ -76,25 +76,29 @@ try {
$updateStmt->close(); $updateStmt->close();
// Handle cover image upload if provided // Handle cover image upload if provided
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] !== UPLOAD_ERR_NO_FILE) { if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB
$fileName = $_FILES['cover_image']['name']; $fileName = $_FILES['cover_image']['name'];
$fileTmpName = $_FILES['cover_image']['tmp_name']; $fileTmpName = $_FILES['cover_image']['tmp_name'];
$fileSize = $_FILES['cover_image']['size']; $fileSize = $_FILES['cover_image']['size'];
$fileMime = mime_content_type($fileTmpName);
// Validate file extension
// Validate file $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if (!in_array($fileMime, $allowedMimes)) { $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
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) { if ($fileSize > 5 * 1024 * 1024) {
throw new Exception('Cover image file too large (max 5MB)'); throw new Exception('Cover image file too large (max 5MB)');
} }
$albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id; $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 // Delete old cover if it exists
$oldCoverStmt = $conn->prepare("SELECT cover_image FROM photo_albums WHERE album_id = ?"); $oldCoverStmt = $conn->prepare("SELECT cover_image FROM photo_albums WHERE album_id = ?");
@@ -104,16 +108,15 @@ try {
if ($oldCoverResult->num_rows > 0) { if ($oldCoverResult->num_rows > 0) {
$oldCover = $oldCoverResult->fetch_assoc(); $oldCover = $oldCoverResult->fetch_assoc();
if ($oldCover['cover_image']) { if ($oldCover['cover_image']) {
$oldCoverPath = $_SERVER['DOCUMENT_ROOT'] . $oldCover['cover_image']; $oldCoverPath = $rootPath . $oldCover['cover_image'];
if (file_exists($oldCoverPath)) { if (file_exists($oldCoverPath)) {
unlink($oldCoverPath); @unlink($oldCoverPath);
} }
} }
} }
$oldCoverStmt->close(); $oldCoverStmt->close();
// Generate unique filename // Generate unique filename
$ext = pathinfo($fileName, PATHINFO_EXTENSION);
$newFileName = 'cover_' . uniqid() . '.' . $ext; $newFileName = 'cover_' . uniqid() . '.' . $ext;
$filePath = $albumDir . '/' . $newFileName; $filePath = $albumDir . '/' . $newFileName;
$coverImagePath = '/assets/uploads/gallery/' . $album_id . '/' . $newFileName; $coverImagePath = '/assets/uploads/gallery/' . $album_id . '/' . $newFileName;
@@ -130,12 +133,15 @@ try {
} }
// Handle photo uploads if any // Handle photo uploads if any
if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] !== UPLOAD_ERR_NO_FILE) { if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] === UPLOAD_ERR_OK) {
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB $maxSize = 5 * 1024 * 1024; // 5MB
$albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id; $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 // Get current max display order
$orderStmt = $conn->prepare("SELECT MAX(display_order) as max_order FROM photos WHERE album_id = ?"); $orderStmt = $conn->prepare("SELECT MAX(display_order) as max_order FROM photos WHERE album_id = ?");
$orderStmt->bind_param("i", $album_id); $orderStmt->bind_param("i", $album_id);
@@ -153,15 +159,17 @@ try {
$fileName = $_FILES['photos']['name'][$i]; $fileName = $_FILES['photos']['name'][$i];
$fileTmpName = $_FILES['photos']['tmp_name'][$i]; $fileTmpName = $_FILES['photos']['tmp_name'][$i];
$fileSize = $_FILES['photos']['size'][$i]; $fileSize = $_FILES['photos']['size'][$i];
$fileMime = mime_content_type($fileTmpName);
// Validate file extension
// Validate file $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if (!in_array($fileMime, $allowedMimes)) { $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
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) { if ($fileSize > $maxSize) {
throw new Exception('File too large: ' . $fileName); throw new Exception('File too large: ' . $fileName . ' (max 5MB)');
} }
// Generate unique filename // Generate unique filename

View File

@@ -43,15 +43,9 @@ if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] !=
$target_dir = $rootPath . "/assets/images/pp/"; $target_dir = $rootPath . "/assets/images/pp/";
$target_file = $target_dir . $randomFilename; $target_file = $target_dir . $randomFilename;
// Ensure upload directory exists and is writable // Ensure upload directory exists
if (!is_dir($target_dir)) { if (!file_exists($target_dir)) {
mkdir($target_dir, 0755, true); mkdir($target_dir, 0777, true);
}
if (!is_writable($target_dir)) {
$response['message'] = 'Upload directory is not writable.';
echo json_encode($response);
exit();
} }
// Move the uploaded file // Move the uploaded file

60
test_payment.php Normal file
View 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);
?>

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Some files were not shown because too many files have changed in this diff Show More