diff --git a/.gitignore b/.gitignore index d9e8c5dd..e285b9cf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,8 @@ .htaccess /assets/uploads/gallery/ /assets/uploads/ +*.sql +error_log +/src/logs/ +/src/admin/_admin_tx_debug.log +/src/api/_webhook_hit.txt diff --git a/.htaccess b/.htaccess index 68702625..5f73e1ef 100644 --- a/.htaccess +++ b/.htaccess @@ -3,6 +3,9 @@ RewriteEngine On RewriteBase / +# Allow LetsEncrypt validation +# RewriteRule ^\.well-known/acme-challenge/ - [L] + # Don't rewrite existing files or directories RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d @@ -30,6 +33,7 @@ RewriteRule ^membership$ src/pages/memberships/membership.php [L] RewriteRule ^membership_details$ src/pages/memberships/membership_details.php [L] RewriteRule ^membership_application$ src/pages/memberships/membership_application.php [L] RewriteRule ^membership_payment$ src/pages/memberships/membership_payment.php [L] +RewriteRule ^renewal_payment$ src/pages/memberships/renewal_payment.php [L] RewriteRule ^renew_membership$ src/pages/memberships/renew_membership.php [L] RewriteRule ^member_info$ src/pages/memberships/member_info.php [L] @@ -68,6 +72,7 @@ RewriteRule ^instapage$ src/pages/events/instapage.php [L] # === OTHER PAGES === RewriteRule ^about$ src/pages/other/about.php [L] +RewriteRule ^base4$ src/pages/other/base4.php [L] RewriteRule ^contact$ src/pages/other/contact.php [L] RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.php [L] RewriteRule ^track-map$ src/pages/track-map.php [L] @@ -80,7 +85,14 @@ RewriteRule ^indemnity_waiver$ src/pages/other/indemnity_waiver.php [L] RewriteRule ^basic_indemnity$ src/pages/other/basic_indemnity.php [L] RewriteRule ^view_indemnity$ src/pages/other/view_indemnity.php [L] +# === PAYMENT RETURN PAGES === +RewriteRule ^success$ src/pages/payment/success.php [L] +RewriteRule ^failure$ src/pages/payment/failure.php [L] +RewriteRule ^cancel$ src/pages/payment/cancel.php [L] + # === ADMIN PAGES === +RewriteRule ^admin_trips_events_courses$ src/admin/admin_trips_events_courses.php [L] +RewriteRule ^admin_bookings$ src/admin/admin_bookings.php [L] RewriteRule ^admin_members$ src/admin/admin_members.php [L] RewriteRule ^admin_payments$ src/admin/admin_payments.php [L] RewriteRule ^admin_web_users$ src/admin/admin_web_users.php [L] @@ -89,10 +101,16 @@ RewriteRule ^admin_course_bookings$ src/admin/admin_course_bookings.php [L] RewriteRule ^admin_camp_bookings$ src/admin/admin_camp_bookings.php [L] RewriteRule ^admin_trip_bookings$ src/admin/admin_trip_bookings.php [L] RewriteRule ^admin_visitors$ src/admin/admin_visitors.php [L] -RewriteRule ^admin_efts$ src/admin/admin_efts.php [L] +RewriteRule ^admin_transactions$ src/admin/admin_transactions.php [L] RewriteRule ^admin_trips$ src/admin/admin_trips.php [L] +RewriteRule ^admin_efts$ src/admin/admin_efts.php [L] +RewriteRule ^admin_prices$ src/admin/admin_prices.php [L] +RewriteRule ^admin_blogs$ src/pages/blog/admin_blogs.php [L] RewriteRule ^manage_events$ src/admin/manage_events.php [L] RewriteRule ^manage_trips$ src/admin/manage_trips.php [L] +RewriteRule ^admin_courses$ /src/admin/admin_courses.php [L,QSA] +RewriteRule ^manage_courses$ /src/admin/manage_courses.php [L,QSA] + # === API/AJAX ENDPOINTS === RewriteRule ^fetch_users$ src/api/fetch_users.php [L] @@ -103,6 +121,8 @@ RewriteRule ^get_tab_total$ src/api/get_tab_total.php [L] RewriteRule ^google_validate_login$ src/api/google_validate_login.php [L] # === PROCESSORS === +RewriteRule ^process_course$ /src/processors/process_course.php [L,QSA] +RewriteRule ^delete_course$ /src/processors/delete_course.php [L,QSA] RewriteRule ^validate_login$ src/processors/validate_login.php [L] RewriteRule ^register_user$ src/processors/register_user.php [L] RewriteRule ^process_application$ src/processors/process_application.php [L] @@ -137,7 +157,7 @@ RewriteRule ^link_membership_user$ src/processors/link_membership_user.php [L] RewriteRule ^unlink_membership_user$ src/processors/unlink_membership_user.php [L] # Blog routes -RewriteRule ^admin_blogs$ src/pages/blog/admin_blogs.php [L] +RewriteRule ^admin_blogs$ src/admin/admin_blogs.php [L] RewriteRule ^user_blogs$ src/pages/blog/user_blogs.php [L] RewriteRule ^blog_read$ src/pages/blog/blog_read.php [L] RewriteRule ^blog_edit$ src/pages/blog/blog_edit.php [L] @@ -153,7 +173,7 @@ RewriteRule ^autosave$ src/processors/blog/autosave.php [L] php_flag display_errors On # php_value error_reporting -1 -RedirectMatch 403 ^/\.well-known +# RedirectMatch 403 ^/\.well-known Options -Indexes diff --git a/.htaccess1 b/.htaccess1 new file mode 100644 index 00000000..46c835d3 --- /dev/null +++ b/.htaccess1 @@ -0,0 +1,215 @@ +# URL Rewrite Rules - Maps old URLs to new directory structure during migration + +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] + + + +php_flag display_errors On +# php_value error_reporting -1 +RedirectMatch 403 ^/\.well-known +Options -Indexes + + + Require all denied + + +ErrorDocument 404 /404.php + + + Require all granted + Require not ip 4.222.252.98 + Require not ip 4.222.252.97 + + + + Order allow,deny + Deny from all + + + +# 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 + diff --git a/components/insta_footer.php b/components/insta_footer.php index 1f447744..0b2d8068 100644 --- a/components/insta_footer.php +++ b/components/insta_footer.php @@ -110,6 +110,7 @@ + diff --git a/header.php b/header.php index bf89d396..840276f7 100644 --- a/header.php +++ b/header.php @@ -1,4 +1,5 @@ - .page-banner-area { + .page-banner-area { position: relative; background-size: cover; background-position: center; @@ -214,6 +215,7 @@ if ($headerStyle === 'light') { position: relative; z-index: 3; } + @@ -258,16 +260,16 @@ if ($headerStyle === 'light') { - - + + @@ -341,11 +346,19 @@ if ($headerStyle === 'light') {
Welcome, - - Profile Picture - +
+ + Profile Picture + + +
+
+ + + + Log In @@ -360,11 +373,44 @@ if ($headerStyle === 'light') { + + + + + + + + \ No newline at end of file diff --git a/index.php b/index.php index b6f7fe3d..901f05fc 100644 --- a/index.php +++ b/index.php @@ -19,6 +19,48 @@ if (!isset($_SESSION['updates_modal_shown'])) { $showUpdatesModal = false; } +// Show renew membership modal for logged-in users and where membership_fees payment_status is not PENDING RENEWAL. only show once per session +$showRenewModal = isset($_SESSION['user_id']) ? true : false; +if ($showRenewModal) { + if (!isset($_SESSION['renew_modal_shown'])) { + $_SESSION['renew_modal_shown'] = true; + } else { + $showRenewModal = false; + } + + $user_id = $_SESSION['user_id']; + + // Ensure we have a DB connection + if (!isset($conn) || $conn === null) { + $showRenewModal = false; + } else { + $stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1"); + $stmt->bind_param("i", $user_id); + $stmt->execute(); + // store_result so we can check num_rows + $stmt->store_result(); + + // If there's no membership_fees record for this user, don't show the renew modal + if ($stmt->num_rows === 0) { + $showRenewModal = false; + } else { + $stmt->bind_result($payment_status); + $stmt->fetch(); + + if ($payment_status === 'PENDING RENEWAL') { + $showRenewModal = false; + } + if (isMembershipExpiringSoon($user_id)) { + $showRenewModal = true; + } else { + $showRenewModal = false; + } + } + + $stmt->close(); + } +} + if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) { $userId = $_SESSION['user_id']; $stmt = $conn->prepare("SELECT user_id FROM membership_application WHERE user_id = ? AND accept_indemnity = 0 LIMIT 1"); @@ -69,7 +111,7 @@ if (!empty($bannerImages)) {
Logo

- Welcome to
the 4 Wheel Drive Club
of Southern Africa + Welcome to
the Four Wheel Drive Club
of Southern Africa

Become a Member @@ -159,8 +201,6 @@ if (countUpcomingTrips() > 0) { ?> } ?> - -
@@ -284,7 +324,6 @@ if (countUpcomingTrips() > 0) { ?>
-
@@ -657,34 +696,72 @@ if (countUpcomingTrips() > 0) { ?> const modal = document.getElementById('updatesModal'); const closeBtn = document.querySelector('.updates-modal-close'); const showModal = ; - - if (showModal) { - // Show modal after a short delay for better UX + const showRenewModal = ; + + if (showModal && modal) { + // Show updates modal after a short delay for better UX setTimeout(function() { modal.style.display = 'flex'; }, 500); } - - // Close modal when X is clicked - closeBtn.addEventListener('click', function() { - modal.style.display = 'none'; - }); - - // Close modal when clicking outside the modal content - modal.addEventListener('click', function(event) { - if (event.target === modal) { - modal.style.display = 'none'; + + // 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); + } }); + + + +
×
-

✨ What's New

+

What's New on 4WDCSA.co.za

@@ -728,6 +805,7 @@ if (countUpcomingTrips() > 0) { ?> background-color: rgba(0, 0, 0, 0.6); align-items: center; justify-content: center; + padding: 20px; animation: fadeIn 0.3s ease-in-out; } @@ -749,6 +827,10 @@ if (countUpcomingTrips() > 0) { ?> box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); animation: slideDown 0.3s ease-out; position: relative; + box-sizing: border-box; + /* Limit height so the modal never exceeds the viewport and allow internal scrolling */ + max-height: calc(100vh - 80px); + overflow-y: auto; } @keyframes slideDown { @@ -834,8 +916,17 @@ if (countUpcomingTrips() > 0) { ?> } @media (max-width: 600px) { + .updates-modal { + /* Align to top on small screens so content's top (and close button) is visible */ + align-items: flex-start; + padding-top: 18px; + } + .updates-modal-content { - padding: 30px 20px; + padding: 20px; + max-width: 92%; + width: 92%; + max-height: calc(100vh - 36px); } .updates-modal-header h2 { diff --git a/src/admin/add_campsite.php b/src/admin/add_campsite.php index 2fb27e9b..c1ccc35a 100644 --- a/src/admin/add_campsite.php +++ b/src/admin/add_campsite.php @@ -38,13 +38,8 @@ if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] !== UPLOAD_ERR_ } $uploadDir = "assets/uploads/campsites/"; - if (!is_dir($uploadDir)) { - mkdir($uploadDir, 0755, true); - } - - if (!is_writable($uploadDir)) { - http_response_code(500); - die('Upload directory is not writable.'); + if (!file_exists($uploadDir)) { + mkdir($uploadDir, 0777, true); } $randomFilename = $validationResult['filename']; diff --git a/src/admin/admin_bookings.php b/src/admin/admin_bookings.php new file mode 100644 index 00000000..c89f3c61 --- /dev/null +++ b/src/admin/admin_bookings.php @@ -0,0 +1,230 @@ +\n
\n
Database connection unavailable. Please check your configuration and logs.
\n
\n
'; + include_once($rootPath . '/components/insta_footer.php'); + // Stop execution to prevent subsequent fatal errors from using $conn + exit; +} + +?> + + + 'index']]; +require_once($rootPath . '/components/banner.php'); +?> + +
+
+
+ + + +
+
+ query($tripsSql); + if ($tripsResult && $tripsResult->num_rows > 0) { + while ($trip = $tripsResult->fetch_assoc()) { + $tripId = $trip['trip_id']; + $tripName = htmlspecialchars($trip['trip_name']); + echo "
"; + echo "
"; + echo "

{$tripName}

"; + $bookingsSql = "SELECT b.user_id, b.num_vehicles, b.num_adults, b.num_children, b.num_pensioners, b.radio, b.status, u.first_name, u.last_name, u.profile_pic, (b.total_amount - b.discount_amount) AS paid FROM bookings b INNER JOIN users u ON b.user_id = u.user_id WHERE b.trip_id = ?"; + $stmt = $conn->prepare($bookingsSql); + $stmt->bind_param('i', $tripId); + $stmt->execute(); + $bookingsResult = $stmt->get_result(); + if ($bookingsResult->num_rows > 0) { + echo ''; + echo ''; + while ($booking = $bookingsResult->fetch_assoc()) { + $userName = htmlspecialchars($booking['first_name'] . ' ' . $booking['last_name']); + $numVehicles = htmlspecialchars($booking['num_vehicles']); + $numAdults = htmlspecialchars($booking['num_adults']); + $numPensioners = htmlspecialchars($booking['num_pensioners']); + $numChildren = htmlspecialchars($booking['num_children']); + $radio = $booking['radio'] == 1 ? "YES" : "NO"; + $status = htmlspecialchars($booking['status']); + $paid = "R " . number_format($booking['paid'], 2); + echo ""; + } + echo '
NameVehiclesAdultsChildrenPensionersRadioStatusAmount
Profile Picture{$userName}{$numVehicles}{$numAdults}{$numChildren}{$numPensioners}{$radio}{$status}{$paid}
'; + } else { + echo '

No bookings found for this trip.

'; + } + echo "
"; + } + } else { + echo '

No trips found.

'; + } + ?> +
+
+ query($courseSql); + if ($courseResult && $courseResult->num_rows > 0) { + while ($course = $courseResult->fetch_assoc()) { + $course_id = $course['course_id']; + $date = $course['date']; + $type = htmlspecialchars($course['course_type']); + if ($type === "driver_training") { + $course_name = "Basic 4X4 Driver Training Course ".$date; + } elseif ($type === "bush_mechanics") { + $course_name = "Bush Mechanics Course ".$date; + } elseif ($type === "rescue_recovery") { + $course_name = "Rescue & Recovery Training Course ".$date; + } else { + $course_name = "General Course ".$date; + } + echo "
"; + echo "
"; + echo "

{$course_name}

"; + $bookingsSql = "SELECT b.user_id, b.num_adults, b.total_amount, b.status, b.course_non_members, u.first_name, u.last_name, u.profile_pic FROM bookings b INNER JOIN users u ON b.user_id = u.user_id WHERE b.course_id = ?"; + if ($stmt = $conn->prepare($bookingsSql)) { + $stmt->bind_param('i', $course_id); + $stmt->execute(); + $bookingsResult = $stmt->get_result(); + } else { + echo "Error in prepared statement: " . $conn->error; + } + if ($bookingsResult && $bookingsResult->num_rows > 0) { + echo ''; + echo ''; + while ($booking = $bookingsResult->fetch_assoc()) { + $userName = htmlspecialchars($booking['first_name'] . ' ' . $booking['last_name']); + $members = htmlspecialchars($booking['num_adults']); + $non_members = htmlspecialchars($booking['course_non_members']); + $status = htmlspecialchars($booking['status']); + $paid = "R " . number_format($booking['total_amount'], 2); + echo ""; + } + echo '
NameMembersNon-MembersStatusAmount
Profile Picture{$userName}{$members}{$non_members}{$status}{$paid}
'; + } else { + echo '

No bookings found for this course.

'; + } + echo "
"; + } + } else { + echo '

No courses found.

'; + } + ?> +
+
+ "; + echo "
"; + echo "

BASE4 Camping

"; + $bookingsSql = "SELECT b.user_id, b.from_date, b.to_date, b.num_vehicles, b.num_adults, b.num_children, b.add_firewood, b.status, u.first_name, u.last_name, (b.total_amount - b.discount_amount) AS paid FROM bookings b INNER JOIN users u ON b.user_id = u.user_id WHERE b.booking_type = 'camping'"; + $stmt = $conn->prepare($bookingsSql); + $stmt->execute(); + $bookingsResult = $stmt->get_result(); + if ($bookingsResult && $bookingsResult->num_rows > 0) { + echo ''; + echo ''; + while ($booking = $bookingsResult->fetch_assoc()) { + $userName = htmlspecialchars($booking['first_name'] . ' ' . $booking['last_name']); + $numVehicles = htmlspecialchars($booking['num_vehicles']); + $from = htmlspecialchars($booking['from_date']); + $to = htmlspecialchars($booking['to_date']); + $numAdults = htmlspecialchars($booking['num_adults']); + $numChildren = htmlspecialchars($booking['num_children']); + $radio = $booking['add_firewood'] == 1 ? "YES" : "NO"; + $status = htmlspecialchars($booking['status']); + $paid = "R " . number_format($booking['paid'], 2); + echo ""; + } + echo '
NameFromToVehiclesAdultsChildrenAdd FirewoodStatusAmount
{$userName}{$from}{$to}{$numVehicles}{$numAdults}{$numChildren}{$radio}{$status}{$paid}
'; + } else { + echo '

No bookings found for this trip.

'; + } + echo "
"; + ?> +
+
+ + diff --git a/src/admin/admin_prices.php b/src/admin/admin_prices.php new file mode 100644 index 00000000..f36e5f94 --- /dev/null +++ b/src/admin/admin_prices.php @@ -0,0 +1,183 @@ +prepare("UPDATE prices SET amount = ?, amount_nonmembers = ?, detail = ? WHERE price_id = ?"); + $stmt->bind_param("ddsi", $amount, $amount_nonmembers, $detail, $price_id); + $stmt->execute(); + $stmt->close(); + + $success_message = 'Price updated successfully.'; +} + +// Fetch all prices +$stmt = $conn->prepare("SELECT price_id, description, type, amount, amount_nonmembers, detail FROM prices ORDER BY price_id ASC"); +$stmt->execute(); +$prices = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); +$stmt->close(); +?> + + + + +
+ +
+ +
+
+ + +
+
+
+
+
+
+

Price Settings

+

Update membership fees, pro-rata rates, and course pricing. Changes take effect immediately for new applications.

+ + +
+ + + +
+
+ + + + +
+
+ + +
+
+ + +
+ +
+ + +
+ + + + +
+ + +
+ + + +
+ +
+
+
+
+ + +
+
+
+
+
+
+ + diff --git a/src/admin/admin_transactions.php b/src/admin/admin_transactions.php new file mode 100644 index 00000000..e0bdc6cd --- /dev/null +++ b/src/admin/admin_transactions.php @@ -0,0 +1,248 @@ + + + + + +
+ +
+ +
+
+ + +
+
+
+
+
+
+ '; + echo ' + + + + + + + + + + + '; + + $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 " + + + + + + + "; + + $printed = true; + } + + if (!$printed) { + echo ''; + } + } else { + echo ''; + echo '
DateIDPaylinkIDDescriptionAmountStatus
" . htmlspecialchars($createdAt) . "" . htmlspecialchars($txId) . "" . htmlspecialchars($ikhokhaTxId) . "" . htmlspecialchars($description) . "R " . htmlspecialchars($amount/100) . ".00" . htmlspecialchars($status) . "
No records found
+ + + + + + + + + + '; + echo ''; + } ?> + + +
DateIDDescriptionAmountStatus
No records found
+
+
+
+
+
+
+ + + + diff --git a/src/admin/admin_trips_events_courses.php b/src/admin/admin_trips_events_courses.php new file mode 100644 index 00000000..6d13f261 --- /dev/null +++ b/src/admin/admin_trips_events_courses.php @@ -0,0 +1,441 @@ +\n
\n
Database connection unavailable. Please check your configuration and logs.
\n
\n'; + include_once($rootPath . '/components/insta_footer.php'); + // Stop execution to prevent subsequent fatal errors from using $conn + exit; +} + +?> + + + 'index']]; +require_once($rootPath . '/components/banner.php'); +?> + + + +
+
+
+ + + + +
+
+ query($trips_query); + $trips = []; + if ($result && $result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + $trips[] = $row; + } + } + ?> + +
+
+
+

Manage Trips

+ + New Trip + +
+ +
+ + × +
+ 0) { + echo ''; + echo '
'; + foreach ($trips as $trip) { + $available = $trip['vehicle_capacity'] - $trip['places_booked']; + $publishStatusBadge = $trip['published'] == 1 ? 'PUBLISHED' : 'DRAFT'; + $tripImagePath = ''; + $tripImagesGlob = glob($rootPath . '/assets/images/trips/' . $trip['trip_id'] . '_*.jpg'); + if (!empty($tripImagesGlob)) { + $tripImagePath = str_replace($rootPath, '', $tripImagesGlob[0]); + } else { + $tripImagePath = 'assets/images/placeholder.jpg'; + } + echo ' +
+
+ ' . htmlspecialchars($trip['trip_name']) . ' +
+
+
+
+ ' . strtoupper($publishStatusBadge) . ' +
' . htmlspecialchars($trip['trip_name']) . '
+ 📍 ' . htmlspecialchars($trip['location']) . ' +
+
+

+ Dates: ' . date('M d', strtotime($trip['start_date'])) . ' - ' . date('M d, Y', strtotime($trip['end_date'])) . '
+ Capacity: ' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '
+ Costs: 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 '
'; + } else { + echo "

No trips found. Create one

"; + } + ?> +
+
+
+
+ query($events_query); + $events = []; + if ($result && $result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + $events[] = $row; + } + } + ?> +
+
+
+

Manage Events

+ + New Event + +
+ 0) { + echo ''; + echo '
'; + foreach ($events as $event) { + $eventImagePath = $event['image'] ? htmlspecialchars($event['image']) : 'assets/images/placeholder.jpg'; + $publishStatusBadge = $event['published'] == 1 ? 'PUBLISHED' : 'DRAFT'; + echo ' +
+
+ ' . htmlspecialchars($event['name']) . ' +
+
+
+
+ ' . strtoupper($publishStatusBadge) . ' +
' . htmlspecialchars($event['name']) . '
+ 📍 ' . htmlspecialchars($event['location']) . ' +
+
+

+ Type: ' . htmlspecialchars($event['type']) . '
+ Date: ' . convertDate($event['date']) . ' +

+ +
+
+ '; + } + echo '
'; + } else { + echo '

No events found. Create one

'; + } + ?> +
+
+
+
+ query($courses_query); + $courses = []; + if ($result && $result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + $courses[] = $row; + } + } + ?> +
+
+
+

Manage Courses

+ + New Course + +
+ +
+ + × +
+ + + 0): ?> + +
+ +
+
+
+
+
+
+ +
+
+

+ Instructor: ()
+ Capacity: /   Available:
+ Costs: Members: R | Non-Members: R +

+ +
+
+ +
+ +
+

No courses found. Create one

+
+ +
+
+
+
+ prepare("SELECT b.blog_id, b.title, b.description, b.status, b.date, b.image, CONCAT(u.first_name, ' ', u.last_name) AS author_name, u.email AS author_email, u.profile_pic FROM blogs b JOIN users u ON b.author = u.user_id WHERE b.status = 'published' ORDER BY b.date DESC"); + if ($stmt) { + $stmt->execute(); + $res = $stmt->get_result(); + if ($res && $res->num_rows > 0) { + while ($r = $res->fetch_assoc()) $posts[] = $r; + } + } + } + ?> +
+
+
+

Manage Blogs

+ New Blog +
+ + 0): ?> + +
+ +
+
+ <?php echo htmlspecialchars($post['title']); ?> +
+
+
+ Author +
+ +
+ +
+
+

+ +
+
+ +
+ +

No blogs found. Create one

+ + +
+
+ +
+
+ +
+ diff --git a/src/admin/manage_courses.php b/src/admin/manage_courses.php new file mode 100644 index 00000000..3809bc4b --- /dev/null +++ b/src/admin/manage_courses.php @@ -0,0 +1,221 @@ +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(); +} +?> + + 'index'], ['Admin' => 'admin_courses'], [$pageTitle => '']]; + require_once($rootPath . '/components/banner.php'); +?> + + +
+
+
+
+
+
+ + + + + +
+

+
+
+ +
+
+
+ + +
+
+
+
+ + + Auto-generated from type + date (you can edit manually) +
+
+ +
+
+ + +
+
+
+
+ + +
+
+ +
+
+ + +
+
+
+
+ + +
+
+ +
+
+ + +
+
+
+
+ + +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ + + + + + + + diff --git a/src/api/ikhokha_webhook.php b/src/api/ikhokha_webhook.php new file mode 100644 index 00000000..61bc27e8 --- /dev/null +++ b/src/api/ikhokha_webhook.php @@ -0,0 +1,319 @@ + $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(); + } +} + +progress_log('Located localPaymentId: ' . $localPaymentId); + +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(); + } +} +progress_log('Located localPaymentId by providerPaymentId: ' . $localPaymentId); + +if (!$localPaymentId) { + http_response_code(404); + progress_log('iKhokha webhook: payment not found'); + 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)); +progress_log('Normalized provider status: ' . $normalized); +if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) { + + $stmt = $conn->prepare( + "UPDATE payments SET status = 'PAID' WHERE payment_id = ?" + ); + + if ($stmt === false) { + progress_log('Failed to prepare payments status update: ' . $conn->error); + } else { + $stmt->bind_param('s', $localPaymentId); + $stmt->execute(); + $stmt->close(); + } + + if ($booking_id) { + $stmt = $conn->prepare( + "UPDATE bookings SET status = 'PAID' WHERE booking_id = ?" + ); + if ($stmt === false) { + progress_log('Failed to prepare bookings status update: ' . $conn->error); + } else { + $stmt->bind_param('i', $booking_id); + $stmt->execute(); + $stmt->close(); + } + } else { + $stmt = $conn->prepare( + "UPDATE membership_fees SET payment_status = 'PAID' WHERE payment_id = ?" + ); + if ($stmt === false) { + progress_log('Failed to prepare membership_fees status update: ' . $conn->error); + } else { + $stmt->bind_param('s', $localPaymentId); + $stmt->execute(); + $stmt->close(); + } + } + + 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( + 'chrispintoza@gmail.com', + $subject, + nl2br($message) + ); + + sendFinanceNotification($subject, nl2br($message)); + sendAdminNotification($subject, nl2br($message)); + + $event = 'new_payment_received'; + $sub_feed = 'payments'; + $data = [ + 'actor_id' => $_SESSION['user_id'] ?? null, + 'actor_avatar' => $_SESSION['profile_pic'] ?? null, // used by UI to show avatar + 'title' => "New Payment Received for Payment ID: {$localPaymentId}" + ]; + addNotification(null, $event, $sub_feed, $data, null); + + progress_log('iKhokha webhook: payment marked as PAID'); +} + +/** + * ========================================================== + * Acknowledge webhook + * ========================================================== + */ +http_response_code(200); +echo 'OK'; diff --git a/src/api/notifications.php b/src/api/notifications.php new file mode 100644 index 00000000..cf2435df --- /dev/null +++ b/src/api/notifications.php @@ -0,0 +1,41 @@ + true, 'notifications' => $notes, 'unread_count' => getUnreadCount($admin_id, $subs)]); + exit; +} + +if ($action === 'mark_read') { + if (!$admin_id) { echo json_encode(['success' => false, 'error' => 'unauthenticated']); exit; } + $id = isset($_POST['id']) ? intval($_POST['id']) : 0; + if (!$id) { echo json_encode(['success' => false, 'error' => 'missing_id']); exit; } + $ok = markNotificationRead($id, $admin_id); + echo json_encode(['success' => (bool)$ok]); + exit; +} + +if ($action === 'add') { + // internal use: create a notification + $target = isset($_POST['user_id']) ? intval($_POST['user_id']) : null; + $event = $_POST['event'] ?? ''; + $sub_feed = $_POST['sub_feed'] ?? null; + $data = isset($_POST['data']) ? json_decode($_POST['data'], true) : []; + $target_url = $_POST['target_url'] ?? null; + $id = addNotification($target, $event, $sub_feed, $data, $target_url); + echo json_encode(['success' => (bool)$id, 'id' => $id]); + exit; +} + +echo json_encode(['success' => false, 'error' => 'invalid_action']); diff --git a/src/api/test_log.php b/src/api/test_log.php new file mode 100644 index 00000000..3d333f26 --- /dev/null +++ b/src/api/test_log.php @@ -0,0 +1,10 @@ +prepare("SELECT membership_end_date FROM membership_fees WHERE user_id = ? LIMIT 1"); + if (!$stmt) { + $conn->close(); + return false; + } + + $stmt->bind_param('i', $user_id); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($result->num_rows === 0) { + $stmt->close(); + $conn->close(); + return false; + } + + $row = $result->fetch_assoc(); + $membership_end_date = new DateTime($row['membership_end_date']); + $current_date = new DateTime(); + $interval = $current_date->diff($membership_end_date); + + $stmt->close(); + $conn->close(); + + return ($interval->days <= 90 && $membership_end_date > $current_date); +} + +function normalizeName($name) { + $name = strtolower($name); + $name = preg_replace("/[^a-z\s]/", "", $name); // remove punctuation + $name = preg_replace("/\s+/", " ", $name); // normalize spaces + return trim($name); +} + +// function to checkif first name + last name matches names in an array. names may have slight variations or spelling mistakes. +function validateHonoraryMemberName($first_name, $last_name) +{ + $honorary_names = [ + "robin hood", + "marc rademaker", + "clive robinson", + "joern kuebler", + "maurice compton", + "jenny cole", + "geoff joubert", + "alan exton", + "dave bell", + "karl hoffman", + "gerald obrian" + // Add more honorary member names as needed + ]; + + $full_name = normalizeName($first_name . ' ' . $last_name); + $full_name = strtolower($full_name); + foreach ($honorary_names as $name) { + similar_text($full_name, strtolower(normalizeName($name)), $percent); + if ($percent >= 80) { // 80% similarity threshold + return true; + } + } + return false; +} + +//get membership_type from membership_applications table for user_id = ? +function getMembershipType($user_id) +{ + $conn = openDatabaseConnection(); + if ($conn === null) { + return null; + } + + $stmt = $conn->prepare("SELECT membership_type FROM membership_applications WHERE user_id = ? ORDER BY id DESC LIMIT 1"); + if (!$stmt) { + $conn->close(); + return null; + } + + $stmt->bind_param('i', $user_id); + $stmt->execute(); + $stmt->bind_result($membership_type); + if ($stmt->fetch()) { + $stmt->close(); + $conn->close(); + return $membership_type; + } else { + $stmt->close(); + $conn->close(); + return null; + } +} + + +function progress_log($message, $context = null) +{ + try { + // Site root (same logic you already use elsewhere) + $rootPath = dirname(dirname(__DIR__)); + $logFile = $rootPath . '/progress.log'; + + $timestamp = date('Y-m-d H:i:s'); + + // Normalize message + if (is_array($message) || is_object($message)) { + $message = json_encode($message, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + } + + // Normalize context (optional extra data) + if ($context !== null) { + if (is_array($context) || is_object($context)) { + $context = json_encode($context, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + } + $message .= ' | CONTEXT: ' . $context; + } + + $line = "[{$timestamp}] {$message}" . PHP_EOL; + + // Append atomically + file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX); + + } catch (Throwable $e) { + // Never allow logging failures to break execution + // Silent by design + } +} + + +function getPriceByDescription($description) +{ + $conn = openDatabaseConnection(); + $stmt = $conn->prepare("SELECT amount FROM prices WHERE description = ? LIMIT 1"); + if (!$stmt) { + return null; + } + $stmt->bind_param("s", $description); + $stmt->execute(); + $stmt->bind_result($amount); + if ($stmt->fetch()) { + $stmt->close(); + return $amount; + } else { + $stmt->close(); + return null; + } +} + function getTripCount() { // Database connection @@ -356,6 +509,51 @@ function sendAdminNotification($subject, $message) } } +function sendFinanceNotification($subject, $message) +{ + $mail = [ + 'Messages' => [ + [ + 'From' => [ + 'Email' => $_ENV['MAILJET_FROM_EMAIL'], + 'Name' => $_ENV['MAILJET_FROM_NAME'] + ], + 'To' => [ + [ + 'Email' => $_ENV['FINANCE_EMAIL'], + 'Name' => 'Finance Team' + ] + ], + 'TemplateID' => 6896720, + 'TemplateLanguage' => true, + 'Subject' => $subject, + 'Variables' => [ + 'message' => $message, + ] + ] + ] + ]; + + $client = new Client([ + 'base_uri' => 'https://api.mailjet.com/v3.1/', + ]); + + $response = $client->request('POST', 'send', [ + 'json' => $mail, + 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']] + ]); + + if ($response->getStatusCode() == 200) { + $body = $response->getBody(); + $response = json_decode($body); + if ($response->Messages[0]->Status == 'success') { + return true; + } else { + return false; + } + } +} + function sendPaymentConfirmation($email, $name, $description) { $message = [ @@ -464,7 +662,7 @@ function getUserMemberStatus($user_id) } // Step 3: Check membership fees table for valid payment status and membership_end_date - $queryFees = "SELECT payment_status, membership_end_date FROM membership_fees WHERE user_id = ?"; + $queryFees = "SELECT payment_status, membership_end_date, renewal_period_end FROM membership_fees WHERE user_id = ?"; $stmtFees = $conn->prepare($queryFees); if (!$stmtFees) { error_log("Failed to prepare fees query: " . $conn->error); @@ -487,6 +685,7 @@ function getUserMemberStatus($user_id) $fees = $resultFees->fetch_assoc(); $payment_status = $fees['payment_status']; $membership_end_date = $fees['membership_end_date']; + $renewal_period_end = $fees['renewal_period_end']; // Validate payment status and membership_end_date $current_date = new DateTime(); @@ -495,6 +694,12 @@ function getUserMemberStatus($user_id) if ($payment_status === "PAID" && $current_date <= $membership_end_date_obj) { $conn->close(); return true; // Direct membership is active + }elseif ($payment_status === "PENDING RENEWAL") { + $renewal_period_end_obj = DateTime::createFromFormat('Y-m-d', $renewal_period_end); + if ($current_date <= $renewal_period_end_obj) { + $conn->close(); + return true; // Direct membership is in renewal period + } } else { // Direct membership is not active, check if user is linked to another active membership error_log("Direct membership not active for user_id: $user_id - checking linked memberships"); @@ -705,6 +910,173 @@ function processPayment($payment_id, $amount, $description) } +function createIkhokhaPayment($payment_id, $amount, $description, $publicRef) +{ + + // Base requester URL: prefer explicit env var, otherwise build from request + $baseUrl = rtrim($_ENV['IKHOKHA_REQUESTER_URL'] ?? ($_SERVER['REQUEST_SCHEME'] ?? 'https') . '://' . ($_SERVER['HTTP_HOST'] ?? ''), '/'); + + $endpoint = $_ENV['IKHOKHA_ENDPOINT']; + $appID = $_ENV['IKHOKHA_APP_ID']; + $appSecret = $_ENV['IKHOKHA_APP_SECRET']; + $requestBody = [ + "entityID" => $payment_id, + "externalEntityID" => $payment_id, + "amount" => $amount * 100, + "currency" => "ZAR", + "requesterUrl" => $_ENV['IKHOKHA_REQUESTER_URL'] ?? $baseUrl, + "description" => $description, + "paymentReference" => $description, + "mode" => $_ENV['IKHOKHA_MODE'] ?? 'live', + "externalTransactionID" => $payment_id, + "urls" => [ + "callbackUrl" => $_ENV['IKHOKHA_CALLBACK_URL'], + + "successPageUrl" => $_ENV['IKHOKHA_SUCCESS_URL'] + . "?ref=" . urlencode($publicRef), + + "failurePageUrl" => $_ENV['IKHOKHA_FAILURE_URL'] + . "?ref=" . urlencode($publicRef), + + "cancelUrl" => $_ENV['IKHOKHA_CANCEL_URL'] + . "?ref=" . urlencode($publicRef), + ] + + ]; + $stringifiedBody = json_encode($requestBody); + $payloadToSign = createPayloadToSign($endpoint, $stringifiedBody); + $ikSign = generateSignature($payloadToSign, $appSecret); + // Initialize cURL session + $ch = curl_init($endpoint); + // Set cURL options + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($ch, CURLOPT_POSTFIELDS, $stringifiedBody); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Content-Type: application/json", + "IK-APPID: $appID", + "IK-SIGN: $ikSign" + ]); + // Execute cURL session + $response = curl_exec($ch); + curl_close($ch); + + // Decode and output the response + $resp = json_decode($response, true); + + // Persist provider metadata into payments table if we have a response + $conn = openDatabaseConnection(); + if ($conn === null) { + return false; + } + + $provider = 'ikhokha'; + $provider_payment_id = $resp['paylinkID'] ?? $resp['paylinkId'] ?? $resp['paylink_id'] ?? null; + $payment_link = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null; + $provider_status = $resp['responseCode'] ?? ($resp['status'] ?? null); + $provider_response = json_encode($resp); + + // Update payments row with provider info. If a paylink was created (responseCode == '00'), keep status awaiting payment. + $newStatus = null; + if (!empty($payment_link) && ($provider_status === '00' || $provider_status === '0' || $provider_status === 0)) { + $newStatus = 'AWAITING PAYMENT'; + } + + if ($newStatus) { + $stmt = $conn->prepare("UPDATE payments SET provider = ?, provider_payment_id = ?, payment_link = ?, provider_status = ?, provider_response = ?, status = ? WHERE payment_id = ? LIMIT 1"); + if ($stmt) { + $stmt->bind_param('sssssss', $provider, $provider_payment_id, $payment_link, $provider_status, $provider_response, $newStatus, $payment_id); + $stmt->execute(); + $stmt->close(); + } + } else { + $stmt = $conn->prepare("UPDATE payments SET provider = ?, provider_payment_id = ?, payment_link = ?, provider_status = ?, provider_response = ? WHERE payment_id = ? LIMIT 1"); + if ($stmt) { + $stmt->bind_param('ssssss', $provider, $provider_payment_id, $payment_link, $provider_status, $provider_response, $payment_id); + $stmt->execute(); + $stmt->close(); + } + } + + $conn->close(); + + return $resp; +} + +function getIkhokhaTransactionHistory($startDate, $endDate,) +{ + + // Base requester URL: prefer explicit env var, otherwise build from request + $endpoint = "https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=".$startDate."&endDate=".$endDate; + // $endpoint = "https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2024-02-01&endDate=2026-03-07"; + $appID = $_ENV['IKHOKHA_APP_ID']; + progress_log($appID, "IKHOKHA App ID"); + $appSecret = $_ENV['IKHOKHA_APP_SECRET']; + + // $stringifiedBody = json_encode($requestBody); + $payloadToSign = createPayloadToSign($endpoint, null); + progress_log($payloadToSign, "IKHOKHA Payload to Sign"); + + $ikSign = generateSignature($payloadToSign, $appSecret); + progress_log($ikSign, "IKHOKHA Signature"); + + // Initialize cURL session + $ch = curl_init($endpoint); + // Set cURL options + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET"); + // curl_setopt($ch, CURLOPT_POSTFIELDS, $stringifiedBody); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Content-Type: application/json", + "IK-APPID: $appID", + "IK-SIGN: $ikSign" + ]); + // Execute cURL session + $response = curl_exec($ch); + curl_close($ch); + + // Decode and output the response + $resp = json_decode($response, true); + + return $response; +} + + +function escapeString($str) { + $escaped = preg_replace(['/[\\"\'\"]/u', '/\x00/'], ['\\\\$0', '\\0'], (string)$str); + $cleaned = str_replace('\/', '/', $escaped); + return $cleaned; +} + +function createPayloadToSign($urlPath, $body) { + $parsedUrl = parse_url($urlPath); + $basePath = $parsedUrl['path']; + if (!$basePath) { + throw new Exception("No path present in the URL"); + } + $payload = $basePath . $body; + $escapedPayloadString = escapeString($payload); + return $escapedPayloadString; +} + +function generateSignature($payloadToSign, $secret) { + return hash_hmac('sha256', $payloadToSign, $secret); +} + +function getPaymentAmount($localPaymentId) { + $conn = openDatabaseConnection(); + $stmt = $conn->prepare("SELECT amount FROM payments WHERE payment_id = ? LIMIT 1"); + $stmt->bind_param("s", $localPaymentId); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($row = $result->fetch_assoc()) { + return $row['amount']; + } else { + return false; // Payment not found + } +} + function processMembershipPayment($payment_id, $amount, $description) { $conn = openDatabaseConnection(); @@ -1149,8 +1521,25 @@ function checkSuperAdmin() $conn->close(); } -function calculateProrata($prorata) +function calculateProrata() { + $conn = openDatabaseConnection(); + $stmt = $conn->prepare("SELECT description, amount FROM prices WHERE description IN ('pro_rata', 'membership_fees')"); + $stmt->execute(); + $rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); + $stmt->close(); + $conn->close(); + + $monthlyRate = 220.00; + $fullRate = 2600.00; + foreach ($rows as $row) { + if ($row['description'] === 'pro_rata') { + $monthlyRate = (float)$row['amount']; + } elseif ($row['description'] === 'membership_fees') { + $fullRate = (float)$row['amount']; + } + } + // Get current month number (1 = January, 12 = December) $currentMonth = date('n'); @@ -1158,14 +1547,16 @@ function calculateProrata($prorata) // (March=1, April=2, ..., February=12) $shiftedMonth = ($currentMonth - 3 + 12) % 12 + 1; - // Total months in a "March to February" year - $totalMonths = 12; + // Full rate applies March–October (shifted months 1–8) + // Pro-rata applies November–February (shifted months 9–12) + if ($shiftedMonth <= 8) { + return $fullRate; + } // Calculate remaining months including the current month - $remainingMonths = $totalMonths - $shiftedMonth + 1; + $remainingMonths = 12 - $shiftedMonth + 1; - // Multiply by prorata value - return $remainingMonths * $prorata; + return $remainingMonths * $monthlyRate; } function getFullName($user_id) @@ -1282,7 +1673,7 @@ function getInitialSurname($user_id) if ($stmt->fetch()) { $initial = strtoupper(substr($first_name, 0, 1)); - return $initial . ". " . $last_name; + return $initial . "." . $last_name; } else { return null; } @@ -1293,6 +1684,89 @@ function getInitialSurname($user_id) } } +function generatePaymentRef(string $type, ?int $course_trip_id, int $user_id): string +{ + $conn = openDatabaseConnection(); + + // 1. Normalize type + $type = strtoupper($type); + + // 2. Build prefix + switch ($type) { + case 'SUBS': + $year = (int)date('Y'); + $month = (int)date('n'); + + // If December, subscriptions are for next year + if ($month === 12) { + $year++; + } + + $prefix = "SUBS_" . $year; + break; + + case 'COURSE': + if (!$course_trip_id) { + throw new Exception("course_trip_id is required for COURSE payments"); + } + + $stmt = $conn->prepare( + "SELECT code FROM courses WHERE course_id = ?" + ); + $stmt->bind_param("i", $course_trip_id); + $stmt->execute(); + $stmt->bind_result($code); + + if (!$stmt->fetch()) { + throw new Exception("Invalid course_id: {$course_trip_id}"); + } + + $stmt->close(); + $prefix = "COURSE_" . strtoupper($code); + break; + + case 'TRIP': + if (!$course_trip_id) { + throw new Exception("course_trip_id is required for TRIP payments"); + } + + $stmt = $conn->prepare( + "SELECT trip_code FROM trips WHERE trip_id = ?" + ); + $stmt->bind_param("i", $course_trip_id); + $stmt->execute(); + $stmt->bind_result($trip_code); + + if (!$stmt->fetch()) { + throw new Exception("Invalid trip_id: {$course_trip_id}"); + } + + $stmt->close(); + $prefix = "TRIP_" . strtoupper($trip_code); + break; + + default: + throw new Exception("Unknown payment type: {$type}"); + } + + // 3. Get user initials + surname + $namePart = strtoupper(getInitialSurname($user_id)); + + if (!$namePart) { + throw new Exception("User not found for user_id: {$user_id}"); + } + + // 4. Add short entropy (trimmed for aesthetics) + $entropy = substr(shortEntropy(), -3); + + return "{$prefix}_{$namePart}_{$entropy}"; +} + +function shortEntropy(): string { + return strtoupper(base_convert((string)(microtime(true) * 1000), 10, 36)); +} + + function getLastName($user_id) { $conn = openDatabaseConnection(); @@ -1719,12 +2193,25 @@ function formatCurrency($amount, $currency = 'R') function guessCountry($ip) { - $response = file_get_contents("http://ip-api.com/json/$ip"); + // Use cURL instead of file_get_contents for compatibility with allow_url_fopen=0 + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, "http://ip-api.com/json/$ip"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + $response = curl_exec($ch); + curl_close($ch); + + if ($response === false) { + return null; + } + $data = json_decode($response, true); - if ($data['status'] == 'success') { + if ($data && isset($data['status']) && $data['status'] == 'success') { return $data['country']; // e.g., South Africa } + + return null; } function getUserIdFromEFT($eft_id) @@ -1951,6 +2438,8 @@ function processLegacyMembership($user_id) { } } + + /** * SECURITY WARNING: This function uses dynamic table/column names which makes it vulnerable to SQL injection. * ONLY call this function with whitelisted table and column names. @@ -2436,18 +2925,21 @@ function validateFileUpload($file, $fileType = 'document') { } // ===== CHECK 5: MIME Type Validation ===== - $finfo = finfo_open(FILEINFO_MIME_TYPE); - if ($finfo === false) { - error_log("Failed to open fileinfo resource"); - return false; - } + // Skip MIME type validation if finfo_open is not available (shared hosting compatibility) + // Extension validation in CHECK 4 provides sufficient security + $mimeType = 'application/octet-stream'; // Default fallback - $mimeType = finfo_file($finfo, $file['tmp_name']); - finfo_close($finfo); - - if (!in_array($mimeType, $config['mimeTypes'], true)) { - error_log("Invalid MIME type '$mimeType' for type: $fileType. Expected: " . implode(', ', $config['mimeTypes'])); - return false; + if (function_exists('finfo_open')) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + if ($finfo !== false) { + $mimeType = finfo_file($finfo, $file['tmp_name']); + finfo_close($finfo); + + if (!in_array($mimeType, $config['mimeTypes'], true)) { + error_log("Invalid MIME type '$mimeType' for type: $fileType. Expected: " . implode(', ', $config['mimeTypes'])); + return false; + } + } } // ===== CHECK 6: Additional Image Validation (for images) ===== @@ -3189,3 +3681,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; +} + diff --git a/src/helpers/notification_helper.php b/src/helpers/notification_helper.php new file mode 100644 index 00000000..935a0ebb --- /dev/null +++ b/src/helpers/notification_helper.php @@ -0,0 +1,151 @@ +insert($query, [$target_user_id, $event, $sub_feed, $data_json, $target_url, $read_by_json], "isssss"); +} + +function fetchNotifications($admin_user_id = null, $subscriptions = [], $limit = 50) { + global $db, $conn; + if (!isset($db) && isset($conn)) { + $db = new DatabaseService($conn); + } + $ds = $db; + if (!$ds) { + if (isset($conn) && $conn) { + $ds = new DatabaseService($conn); + } else { + // No DB available — return empty list to avoid fatal error + return []; + } + } + $params = []; + $types = ""; + $sql = "SELECT * FROM notifications"; + $where = []; + // Admin-only: fetch notifications targeted to admins (user_id IS NULL) or global, or specifically to an admin + if ($admin_user_id) { + $where[] = "(user_id IS NULL OR user_id = ?)"; + $params[] = $admin_user_id; + $types .= "i"; + } + if (!empty($subscriptions)) { + // build IN (...) list safely by placeholders + $placeholders = implode(',', array_fill(0, count($subscriptions), '?')); + $where[] = "(sub_feed IN ($placeholders))"; + foreach ($subscriptions as $s) { $params[] = $s; $types .= "s"; } + } + if (!empty($where)) { + $sql .= " WHERE " . implode(' AND ', $where); + } + $sql .= " ORDER BY time_created DESC LIMIT ?"; + $params[] = $limit; $types .= "i"; + + $results = $ds->select($sql, $params, $types); + if ($results === false) { + // Query error - return empty list so UI doesn't break + return []; + } + // decode data JSON and include read_by as array + $filtered = []; + foreach ($results as $r) { + $r['data'] = $r['data'] ? json_decode($r['data'], true) : null; + $r['read_by'] = $r['read_by'] ? json_decode($r['read_by'], true) : []; + if (!is_array($r['read_by'])) $r['read_by'] = []; + // If admin_user_id is provided, skip notifications this admin already read + if ($admin_user_id && in_array((int)$admin_user_id, $r['read_by'])) { + continue; + } + $filtered[] = $r; + } + return $filtered; +} + +function markNotificationRead($id, $admin_user_id) { + global $conn; + if (!$id || !$admin_user_id) return false; + if (!isset($conn) || !$conn) return false; + $stmt = $conn->prepare("SELECT read_by FROM notifications WHERE id = ? LIMIT 1"); + $stmt->bind_param("i", $id); + $stmt->execute(); + $stmt->bind_result($read_by_json); + $found = $stmt->fetch(); + $stmt->close(); + if (!$found) return false; + $read_by = $read_by_json ? json_decode($read_by_json, true) : []; + if (!is_array($read_by)) $read_by = []; + if (!in_array($admin_user_id, $read_by)) { + $read_by[] = (int)$admin_user_id; + $new_json = json_encode(array_values($read_by)); + $u = $conn->prepare("UPDATE notifications SET read_by = ? WHERE id = ?"); + $u->bind_param("si", $new_json, $id); + $res = $u->execute(); + $u->close(); + return $res; + } + return true; +} + +function getUnreadCount($admin_user_id, $subscriptions = []) { + global $conn; + if (!isset($conn) || !$conn) return 0; + $sql = "SELECT id, read_by, sub_feed FROM notifications"; + $where = []; + $params = []; + $types = ""; + if ($admin_user_id) { + $where[] = "(user_id IS NULL OR user_id = ?)"; $params[] = $admin_user_id; $types .= "i"; + } + if (!empty($subscriptions)) { + $placeholders = implode(',', array_fill(0, count($subscriptions), '?')); + $where[] = "(sub_feed IN ($placeholders))"; + foreach ($subscriptions as $s) { $params[] = $s; $types .= "s"; } + } + if (!empty($where)) $sql .= " WHERE " . implode(' AND ', $where); + $sql .= " ORDER BY time_created DESC"; + $stmt = $conn->prepare($sql); + if ($types) { + // bind params dynamically + $refs = []; + $refs[] = &$types; + foreach ($params as $k => $v) { $refs[] = &$params[$k]; } + call_user_func_array([$stmt, 'bind_param'], $refs); + } + $stmt->execute(); + $res = $stmt->get_result(); + $count = 0; + while ($row = $res->fetch_assoc()) { + $read_by = $row['read_by'] ? json_decode($row['read_by'], true) : []; + if (!is_array($read_by)) $read_by = []; + if (!in_array((int)$admin_user_id, $read_by)) $count++; + } + $stmt->close(); + return $count; +} + +function getAdminSubscriptions($admin_user_id) { + // Placeholder: by default return empty array (all sub_feeds). Implement subscription table later. + return []; +} diff --git a/src/logs/db_errors.log b/src/logs/db_errors.log deleted file mode 100644 index 4a23ee8e..00000000 --- a/src/logs/db_errors.log +++ /dev/null @@ -1 +0,0 @@ -Database Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directory \ No newline at end of file diff --git a/src/pages/blog/blog_edit.php b/src/pages/blog/blog_edit.php index 5d1b7b6c..4b2dd81d 100644 --- a/src/pages/blog/blog_edit.php +++ b/src/pages/blog/blog_edit.php @@ -192,12 +192,15 @@ $stmt->close(); document.getElementById("autosave-status").innerText = "Draft autosaved at " + new Date().toLocaleTimeString(); return true; } else { - document.getElementById("autosave-status").innerText = "Autosave failed"; - console.error("Autosave failed", response.statusText); - return false; + return response.text().then(errorText => { + document.getElementById("autosave-status").innerText = "Autosave failed: " + errorText; + console.error("Autosave failed", response.status, errorText); + return false; + }); } }).catch(err => { console.error("Autosave error:", err); + document.getElementById("autosave-status").innerText = "Autosave error: " + err.message; return false; }); } diff --git a/src/pages/blog/user_blogs.php b/src/pages/blog/user_blogs.php index c061c56d..e5b8eb55 100644 --- a/src/pages/blog/user_blogs.php +++ b/src/pages/blog/user_blogs.php @@ -7,6 +7,19 @@ require_once($rootPath . "/header.php"); checkUserSession(); +// Check if user has active membership +if (!isset($_SESSION['user_id'])) { + header('Location: login'); + exit; +} + +$is_member = getUserMemberStatus($_SESSION['user_id']); +if (!$is_member) { + $_SESSION['message'] = "My Blog Posts is only available to active members. Please contact info@4wdcsa.co.za for more information."; + header('Location: membership_details'); + exit; +} + $pageTitle = 'My Blog Posts'; $breadcrumbs = [['Home' => 'index'], ['Blog' => 'blog']]; require_once($rootPath . '/components/banner.php'); diff --git a/src/pages/bookings/bookings.php b/src/pages/bookings/bookings.php index 9e215473..02406bb9 100644 --- a/src/pages/bookings/bookings.php +++ b/src/pages/bookings/bookings.php @@ -114,6 +114,7 @@ $user_id = $_SESSION['user_id']; // Loop through each row while ($row = $result->fetch_assoc()) { $booking_id = $row['booking_id']; + $payment_id = $row['payment_id']; $booking_type = $row['booking_type']; $from_date = $row['from_date']; $to_date = $row['to_date']; @@ -267,8 +268,8 @@ $user_id = $_SESSION['user_id']; num_rows == 0) { $button_text = "No booking dates available"; @@ -189,8 +189,9 @@ $page_id = 'driver_training';
- Need some help? + You will be redirected to iKhokha's Secure payment gateway.
+ Secure Payment Badges diff --git a/src/pages/bookings/trip-details.php b/src/pages/bookings/trip-details.php index 54071272..3e4e39d5 100644 --- a/src/pages/bookings/trip-details.php +++ b/src/pages/bookings/trip-details.php @@ -205,30 +205,30 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php'); - - - - - - -
- -
- + + + + + + +
+ +
+ @@ -296,8 +296,8 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php'); - - + + + +

Membership Type

+
+
+
+
+ + +
+
+
+ + +
+ +
+
+
+
+ + +

Main Member

@@ -88,193 +113,199 @@ $user = $result->fetch_assoc();
-

Spouse / Life Partner / Other Details

-
-
-
- - +
+

Spouse / Life Partner / Other Details

+
+
+
+ + +
-
-
-
- - +
+
+ + +
+
+ +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
+
+ + + +
+

Children's Names

+
+
+
+ +
- - -

Children's Names

-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
- -
- - -

Address

-
-
-
- - -
-
-
-
- - -
+
+
+ +
- - -

Interests and Hobbies

-
-
-
- -
+
+
+ +
- - -

Primary Vehicle

-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
+
+
+ +
-

Secondary Vehicle

-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
+
+
+ +
+
+
+ + +
+
+ +
+
-
- - -
-
- - + +

Address

+
+
+
+ + +
+
+
+
+ + +
+
+
+ + +

Interests and Hobbies

+
+
+
+ +
+
+
+ + +

Primary Vehicle

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+

Secondary Vehicle

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
-
+ + +
+
+ + +
+
+ +
+
@@ -282,3 +313,43 @@ $user = $result->fetch_assoc(); + + \ No newline at end of file diff --git a/src/pages/memberships/membership_details.php b/src/pages/memberships/membership_details.php index 0f434567..7b4956d7 100644 --- a/src/pages/memberships/membership_details.php +++ b/src/pages/memberships/membership_details.php @@ -19,6 +19,8 @@ $result = $stmt->get_result(); // Fetch single record $membership = $result->fetch_assoc(); +$payment_link = getPaymentLinkByPaymentId($membership['payment_id']); + // Fetch membership application data using mysqli $query = "SELECT * FROM membership_application WHERE user_id = ?"; $stmt = $conn->prepare($query); @@ -186,8 +188,8 @@ if (empty($application['id_number'])) { - - AWAITING PAYMENT + + '> @@ -204,16 +206,26 @@ if (empty($application['id_number'])) {
strtotime($membership_end_date)) { - echo ' - + if ($membership_end_date) { + try { + $end = new DateTime($membership_end_date); + $threeMonthsBefore = (clone $end)->modify('-3 months')->format('Y-m-d'); + } catch (Exception $e) { + // Fallback using strtotime if DateTime parsing fails + $threeMonthsBefore = date('Y-m-d', strtotime($membership_end_date . ' -3 months')); + } + + if (strtotime($today) >= strtotime($threeMonthsBefore)) { + echo ' + Renew Membership '; + } } ?> diff --git a/src/pages/memberships/membership_payment.php b/src/pages/memberships/membership_payment.php index bb4bb9ff..8fc03e9e 100644 --- a/src/pages/memberships/membership_payment.php +++ b/src/pages/memberships/membership_payment.php @@ -67,29 +67,70 @@ $stmt->bind_result($user_email); $stmt->fetch(); $stmt->close(); -$conn->close(); +// If request includes payment_id, fetch provider paylink from payments table +if (!isset($_GET['token']) || empty($_GET['token'])) { + header("Location: membership_details"); + exit(); +} +$token = $_GET['token']; +// echo $token; + +// Sanitize the trip_id to prevent SQL injection +$payment_id = decryptData($token, $_ENV['SALT']); +$payment_link = null; +if ($payment_id) { + $pstmt = $conn->prepare("SELECT payment_link, amount, status, provider FROM payments WHERE payment_id = ? LIMIT 1"); + if ($pstmt) { + $pstmt->bind_param('s', $payment_id); + $pstmt->execute(); + $pres = $pstmt->get_result(); + if ($prow = $pres->fetch_assoc()) { + $payment_link = $prow['payment_link']; + // prefer payments.amount if present + if (!empty($prow['amount'])) { + $payment_amount = $prow['amount']; + } + } + $pstmt->close(); + } +} ?> 'index.php'], ['Membership' => 'membership.php']]; require_once($rootPath . '/components/banner.php'); -?> + ?>
-
- New Membership Payment: - Membership Start Date: ' . $membership_start_date . '
Membership Renewal Date: ' . $membership_end_date . ''; ?> +
+ New Membership Payment: + Membership Start Date: ' . $membership_start_date . '
Membership Renewal Date: ' . $membership_end_date . ''; ?> +
+ + +
Payment Details:
+

Amount: R

+

Reference:

+ + Pay Now with iKhokha + + +
+

You will be redirected to iKhokha's Secure payment gateway.

-

Your invoice has been sent to . Please upload your proof of payment below.

+ Secure Payment Badges + +

Please upload your proof of payment below.

Payment Details:

The Four Wheel Drive Club of Southern Africa
FNB
Account Number: 58810022334
Branch code: 250655
Reference:
Amount: R

- Submit Proof of Payment - - + Submit Proof of Payment + + +
@@ -102,4 +143,4 @@ $conn->close();
- + \ No newline at end of file diff --git a/src/pages/memberships/renew_membership.php b/src/pages/memberships/renew_membership.php index f6d85f38..b8458f06 100644 --- a/src/pages/memberships/renew_membership.php +++ b/src/pages/memberships/renew_membership.php @@ -9,7 +9,11 @@ $eft_id = strtoupper("SUBS " . date("Y") . " " . getLastName($user_id)); $status = 'AWAITING PAYMENT'; $description = 'Membership Fees ' . date("Y") . " " . getLastName($user_id); -$payment_amount = 2600; // Assuming a fixed membership fee, adjust as needed +$price_stmt = $conn->prepare("SELECT amount FROM prices WHERE description = 'membership_fees' LIMIT 1"); +$price_stmt->execute(); +$price_result = $price_stmt->get_result()->fetch_assoc(); +$price_stmt->close(); +$payment_amount = $price_result ? (float)$price_result['amount'] : 2600.00; $payment_date = date('Y-m-d'); $membership_start_date = date('Y-01-01'); $membership_end_date = date('Y-12-31'); diff --git a/src/pages/memberships/renewal_payment.php b/src/pages/memberships/renewal_payment.php new file mode 100644 index 00000000..e94eeff9 --- /dev/null +++ b/src/pages/memberships/renewal_payment.php @@ -0,0 +1,182 @@ +prepare($query)) { + // Bind the user_id parameter to the query + $stmt->bind_param("i", $user_id); + + // Execute the query + $stmt->execute(); + + // Bind the results to variables + $stmt->bind_result($payment_amount, $membership_start_date, $membership_end_date, $eft_id); + + // Fetch the data + if ($stmt->fetch()) { + // Values are now assigned to $payment_amount, $membership_start_date, and $membership_end_date + } else { + // Handle case where no records are found + $error_message = "No records found for the given user ID."; + } + + // Close the statement + $stmt->close(); + } else { + // Handle query preparation failure + $error_message = "Query preparation failed: " . $conn->error; + } +} else { + // Handle case where user_id is not found in session + $error_message = "User ID not found in session."; +} +?> + + 'index.php']]; + require_once($rootPath . '/components/banner.php'); + ?> + +
+
+
+
+
+ Membership Renewal: + Membership Expiration Date: ' . $membership_end_date . ''; ?> +
+ +
Renewal Amount:
+ +
+ + +
+
+ > + +
+
+ > + +
+
+ > + + You need to reside more than 150km from BASE4 to qualify. +
+
+ +
Amount:
+ +

R

+ + +
+

You will be redirected to iKhokha's Secure payment gateway.

+
+ Secure Payment Badges +
+ + + +
+ +
+
+ About +
+
+ +
+
+
+ + \ No newline at end of file diff --git a/src/pages/other/about.php b/src/pages/other/about.php index 59d8a450..40cd0a04 100644 --- a/src/pages/other/about.php +++ b/src/pages/other/about.php @@ -159,13 +159,13 @@ require_once($rootPath . '/components/banner.php');

4WDCSA Committee and Other Office Bearers

Committee

-
  • Chairman - John Runciman
  • +
  • Vice Chairman - Davin Webster
  • National Liaison - Peter Hutchison
  • -
  • Treasurer - Doug Timm
  • -
  • Outings - John Runciman
  • -
  • Events - Noelene Runciman
  • -
  • Driver Training - John Runciman
  • -
  • Digital Media - Christopher Pinto
  • +
  • Driver Training - VACANT
  • +
  • Marketing - Janet Erasmus
  • +
  • Outdoor - Carla Holtzhausen
  • +
  • Maintenance - Kit Muirhead
  • +
    @@ -238,7 +238,7 @@ require_once($rootPath . '/components/banner.php');
    Extended Trips

    Come and Explore Africa and beyond

    - + Explore Trips @@ -248,7 +248,7 @@ require_once($rootPath . '/components/banner.php');
    Driver Training

    Level up your 4x4 Driving Skills

    - + Explore Training @@ -258,7 +258,7 @@ require_once($rootPath . '/components/banner.php');
    Events

    See whats cooking at BASE4!

    - + Explore Events diff --git a/src/pages/other/base4.php b/src/pages/other/base4.php new file mode 100644 index 00000000..5eeb9af6 --- /dev/null +++ b/src/pages/other/base4.php @@ -0,0 +1,829 @@ + + + + + + + + 'index.php']]; +require_once($rootPath . '/components/banner.php'); +?> + + +
    +
    +
    +
    +
    +
    +

    BASE 4: The home of 4WDCSA.

    +

    Nestled near the Hennops river, in Doornradje, Centurion, BASE4 is the ultimate weekend getaway for 4x4 enthusiasts and outdoor lovers. This vibrant hub offers an array of exciting activities, including a challenging 4x4 test track, relaxing camping spots, and a clubhouse with food and refreshments. Take a dip in the swimming pool, fire up the braai, or unwind our brand new clubhouse. Whether you're here for adventure or relaxation, BASE4 provides the perfect setting for all your off-road and outdoor adventures. Join the Four Wheel Drive Club of Southern Africa and be part of the thrill!

    +
    + Hotel +
    + +
    + +
    +
    +
    +
    +
    +

    BASE4
    Non Member Fees:

    +
    +

    Day visitors*:

    +

    R 50.00 per vehicle

    +

    4 pax max

    +
    +
    +

    Day visit & Track Pass*:

    +

    R 150.00 per vehicle

    +

    4 pax max

    +
    +
    +

    Camping:

    +

    R 250.00 per vehicle

    +

    Single night camping. Includes access to the track. 4 pax & 2 tents max.

    +
    +
    +

    BASE4 Weekend Pass:

    +

    R 400.00 per vehicle

    +

    Camping from Friday till Sunday. Includes access to the track. 4 pax & 2 tents max.

    +
    +

    + *Day visitor charge not applicable on Open Days. Non-members require a 4WDCSA member to accompany them on the track at all times. Payment due and indemnity waiver must be signed at the clubhouse upon entry.

    +
    +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +

    BASE4 Open Days

    +

    Whether you're a member or just curious, everyone's welcome at our monthly open events. Come camp with us, enjoy guest speakers, take your rig for a spin on the 4x4 track, or just relax by the swimming pool. Food and refreshments are available all weekend, plus braai fires ready to go—just bring your tongs! It’s the perfect way to experience the spirit of the club and connect with fellow adventurers.

    +
    +
    +
    + '; + } + ?> +
    +
    + +
    + + + +
    +
    +
    +
    +

    BASE4 4x4 Training Track

    +

    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.

    +
    + +
    + + +
    + + +
    +
    +
    + Beginner +
    +
    +
    + Intermediate +
    +
    +
    + Advanced +
    +
    +
    +
    + +
    +
    + + +
    +
    +

    Add New Obstacle

    +
    + + + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    +
    +
    + + +
    + + + + + + + + + + \ No newline at end of file diff --git a/src/pages/other/bush_mechanics.php b/src/pages/other/bush_mechanics.php index 431dfb86..19e47008 100644 --- a/src/pages/other/bush_mechanics.php +++ b/src/pages/other/bush_mechanics.php @@ -156,7 +156,7 @@ $page_id = 'bush_mechanics';
    num_rows == 0) { $button_text = "No booking dates available"; @@ -168,8 +168,9 @@ $page_id = 'bush_mechanics'; + Secure Payment Badges
    diff --git a/src/pages/other/indemnity.php b/src/pages/other/indemnity.php index c6f1c43e..ba49c32f 100644 --- a/src/pages/other/indemnity.php +++ b/src/pages/other/indemnity.php @@ -105,17 +105,29 @@ if (isset($_SESSION['user_id'])) { response = JSON.parse(response); } if (response.status === 'success') { - // Check if the user has paid + // If provider returned a direct paylink, go there immediately + if (response.paylinkUrl) { + window.location.href = 'membership_payment?token=' + encodeURIComponent(response.token); + return; + } + + // If we have a payment_id, redirect to membership_payment with it + // if (response.payment_id) { + // setTimeout(function() { + // window.location.href = 'membership_payment.php?payment_id=' + encodeURIComponent(response.token); + // }, 800); + // return; + // } + + // Fallback behaviour: check paymentStatus if (response.paymentStatus === 'PAID') { - // Redirect to membership_details.php if paid setTimeout(function() { window.location.href = 'membership_details.php'; - }, 2000); // 2-second delay before redirecting + }, 1200); } else { - // Redirect to membership_payment.php if not paid setTimeout(function() { window.location.href = 'membership_payment.php'; - }, 2000); // 2-second delay before redirecting + }, 1200); } } else { $('#responseMessage').html('
    ' + response.message + '
    '); diff --git a/src/pages/other/rescue_recovery.php b/src/pages/other/rescue_recovery.php index 85c12514..3fd83f12 100644 --- a/src/pages/other/rescue_recovery.php +++ b/src/pages/other/rescue_recovery.php @@ -154,7 +154,7 @@ $page_id = 'rescue_recovery';
    num_rows == 0) { $button_text = "No booking dates available"; @@ -165,9 +165,11 @@ $page_id = 'rescue_recovery'; + + Secure Payment Badges
    diff --git a/src/pages/payment/cancel.php b/src/pages/payment/cancel.php new file mode 100644 index 00000000..0aab44f6 --- /dev/null +++ b/src/pages/payment/cancel.php @@ -0,0 +1,73 @@ +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'); +?> +
    +
    +
    +
    +
    + Payment Cancelled +
    Your payment was cancelled or you returned without completing it.
    +
    + + +
    + +

    Your payment appears to have been cancelled. If this was a mistake you can try again below.

    +
      +
    • Reference:
    • +
    • Amount: R
    • +
    • Description:
    • +
    + + + + Retry Payment + + + + +

    Contact info@4wdcsa.co.za if you need assistance.

    + +
    + +
    +
    + Logo +
    +
    + +
    +
    +
    + + diff --git a/src/pages/payment/failure.php b/src/pages/payment/failure.php new file mode 100644 index 00000000..1491b4ec --- /dev/null +++ b/src/pages/payment/failure.php @@ -0,0 +1,75 @@ +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'); +?> +
    +
    +
    +
    +
    + Payment Failed +
    Unfortunately your payment could not be completed.
    +
    + + +
    + +

    We were unable to process your payment. You can try again or contact support for assistance.

    +
      +
    • Reference:
    • +
    • Amount: R
    • +
    • Provider:
    • +
    • Description:
    • +
    • Status:
    • +
    + + + + Try Again + + + + +

    Or contact info@4wdcsa.co.za for help.

    + +
    + +
    +
    + Logo +
    +
    + +
    +
    +
    + + diff --git a/src/pages/payment/success.php b/src/pages/payment/success.php new file mode 100644 index 00000000..11b36dad --- /dev/null +++ b/src/pages/payment/success.php @@ -0,0 +1,84 @@ +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'); +?> +
    +
    +
    +
    +
    + Payment Successful +
    Thank you — your payment was received.
    +
    + +
    MEMBERSHIP STATUS:
    + + + +
    + +

    Your payment has been processed successfully. Below are the details we received:

    +
      +
    • Reference:
    • +
    • Amount: R
    • +
    • Provider:
    • +
    • Description:
    • +
    • Status:
    • +
    + + + + Go to Membership Details + + + + + Go to my Bookings + + + +
    + +
    +
    + Logo +
    +
    + +
    +
    +
    + + diff --git a/src/processors/blog/autosave.php b/src/processors/blog/autosave.php index 81040696..3d8e2c8d 100644 --- a/src/processors/blog/autosave.php +++ b/src/processors/blog/autosave.php @@ -5,6 +5,11 @@ require_once($rootPath . "/src/config/connection.php"); require_once($rootPath . "/src/config/functions.php"); session_start(); +// Enable error reporting for debugging +error_reporting(E_ALL); +ini_set('display_errors', 0); // Don't display, but log them +ini_set('log_errors', 1); + if (!isset($_SESSION['user_id'])) { http_response_code(401); echo "Not authorized"; @@ -32,36 +37,42 @@ echo $author_id; $cover_image_path = null; // Only attempt upload if a file was submitted -if (!empty($_FILES['cover_image']['name'])) { +if (!empty($_FILES['cover_image']['name']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) { $uploadDir = $rootPath . "/uploads/blogs/" . $article_id . "/"; - if (!is_dir($uploadDir)) { - mkdir($uploadDir, 0755, true); + + // Create directory if it doesn't exist (match working pattern) + if (!file_exists($uploadDir)) { + mkdir($uploadDir, 0777, true); } - // Validate file using existing function - $file_result = validateFileUpload($_FILES['cover_image'], 'profile_picture'); - if ($file_result === false) { + // Simple validation - check extension + $extension = strtolower(pathinfo($_FILES['cover_image']['name'], PATHINFO_EXTENSION)); + $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + + if (!in_array($extension, $allowedExtensions)) { http_response_code(400); - echo "Invalid file upload"; + echo "Invalid file type. Allowed: jpg, jpeg, png, gif, webp"; exit; } // Use fixed filename "cover" to avoid creating multiple copies on autosave - $extension = $file_result['extension']; $filename = "cover." . $extension; // Delete old cover if it exists with different extension - array_map('unlink', glob($uploadDir . "cover.*")); + $oldCovers = glob($uploadDir . "cover.*"); + if ($oldCovers) { + foreach ($oldCovers as $oldCover) { + @unlink($oldCover); + } + } $targetPath = $uploadDir . $filename; $cover_image_path = "/uploads/blogs/" . $article_id . "/" . $filename; // Move the uploaded file - if (move_uploaded_file($_FILES['cover_image']['tmp_name'], $targetPath)) { - // File moved successfully, $cover_image_path is set - } else { + if (!move_uploaded_file($_FILES['cover_image']['tmp_name'], $targetPath)) { http_response_code(500); - echo "Failed to move uploaded file."; + echo "Failed to move uploaded file"; exit; } } diff --git a/src/processors/blog/submit_blog.php b/src/processors/blog/submit_blog.php index 4fa4b94e..a0872b6a 100644 --- a/src/processors/blog/submit_blog.php +++ b/src/processors/blog/submit_blog.php @@ -26,8 +26,8 @@ if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ $upload_dir = $rootPath . '/uploads/blogs/' . $folder_id . '/'; // Create directory if it doesn't exist - if (!is_dir($upload_dir)) { - mkdir($upload_dir, 0755, true); + if (!file_exists($upload_dir)) { + mkdir($upload_dir, 0777, true); } // Validate and process the file diff --git a/src/processors/delete_course.php b/src/processors/delete_course.php new file mode 100644 index 00000000..5bf809b6 --- /dev/null +++ b/src/processors/delete_course.php @@ -0,0 +1,49 @@ + '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()]); +} + +?> diff --git a/src/processors/delete_event.php b/src/processors/delete_event.php index 152ae71f..b1225752 100644 --- a/src/processors/delete_event.php +++ b/src/processors/delete_event.php @@ -1,46 +1,76 @@ - 'error', 'message' => 'Event ID is required']); +// 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; } -// 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']); +$user_role = getUserRole(); +if (!in_array($user_role, ['admin', 'superadmin'])) { + ob_end_clean(); + echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']); + exit; } -$stmt->close(); +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()]); +} diff --git a/src/processors/process_application.php b/src/processors/process_application.php index f9263c82..3d794425 100644 --- a/src/processors/process_application.php +++ b/src/processors/process_application.php @@ -174,7 +174,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($stmt->execute()) { // Insert into the membership fees table - $payment_amount = calculateProrata(210); // Assuming a fixed membership fee, adjust as needed + $payment_amount = calculateProrata(); $payment_date = date('Y-m-d'); $membership_start_date = $payment_date; // $membership_end_date = date('Y-12-31'); diff --git a/src/processors/process_booking.php b/src/processors/process_booking.php index fb9d9c13..3ed8bc41 100644 --- a/src/processors/process_booking.php +++ b/src/processors/process_booking.php @@ -3,6 +3,7 @@ $rootPath = dirname(dirname(__DIR__)); require_once($rootPath . "/src/config/env.php"); require_once($rootPath . "/src/config/connection.php"); require_once($rootPath . "/src/config/functions.php"); +require_once($rootPath . "/src/helpers/notification_helper.php"); // Start session to retrieve the logged-in user's ID session_start(); @@ -79,6 +80,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt->bind_param('sissiiiidd', $type, $user_id, $from_date, $to_date, $num_vehicles, $num_adults, $num_children, $add_firewood, $total_amount, $discount_amount); if ($stmt->execute()) { + // Get booking id and audit + $booking_id = $conn->insert_id; + if (function_exists('auditLog')) { + auditLog($user_id, 'BOOKING_CREATED', 'bookings', $booking_id, ['total_amount' => $total_amount, 'from' => $from_date, 'to' => $to_date]); + } + $event = 'new_booking_created'; + $sub_feed = 'bookings'; + $data = [ + 'actor_id' => $_SESSION['user_id'] ?? null, + 'actor_avatar' => $_SESSION['profile_pic'] ?? null, // used by UI to show avatar + 'title' => "New Booking Created with Booking ID: {$booking_id}" + ]; + addNotification(null, $event, $sub_feed, $data, null); // Redirect to success page or display success message echo ""; } else { diff --git a/src/processors/process_course.php b/src/processors/process_course.php new file mode 100644 index 00000000..be546a78 --- /dev/null +++ b/src/processors/process_course.php @@ -0,0 +1,100 @@ + '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()]); +} + +?> diff --git a/src/processors/process_course_booking.php b/src/processors/process_course_booking.php index 098abab0..cc256514 100644 --- a/src/processors/process_course_booking.php +++ b/src/processors/process_course_booking.php @@ -3,6 +3,7 @@ $rootPath = dirname(dirname(__DIR__)); require_once($rootPath . "/src/config/env.php"); require_once($rootPath . "/src/config/connection.php"); require_once($rootPath . "/src/config/functions.php"); +require_once($rootPath . "/src/helpers/notification_helper.php"); session_start(); @@ -93,10 +94,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $status = "AWAITING PAYMENT"; $type = 'course'; - $payment_id = uniqid(); + $payment_id = generatePaymentRef('COURSE', $course_id, $user_id); + $publicRef = bin2hex(random_bytes(16)); $num_vehicles = 1; $discountAmount = 0; - $eft_id = strtoupper("COURSE ".date("m-d", strtotime($date))." ".getInitialSurname($user_id)); + $eft_id = $payment_id; $notes = ""; if ($pending_member){ $notes = "Membership Payment pending at time of booking. Please confirm payment has been received."; @@ -117,6 +119,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($stmt->execute()) { $booking_id = $conn->insert_id; + // Audit booking creation + if (function_exists('auditLog')) { + auditLog($user_id, 'COURSE_BOOKING_CREATED', 'bookings', $booking_id, ['course_id' => $course_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]); + } + $event = 'new_course_booking_created'; + $sub_feed = 'bookings'; + $data = [ + 'actor_id' => $_SESSION['user_id'] ?? null, + 'actor_avatar' => $_SESSION['profile_pic'] ?? null, // used by UI to show avatar + 'title' => "New Course Booking Created : {$payment_id}" + ]; + addNotification(null, $event, $sub_feed, $data, null); + if ($payment_amount < 1) { if (processZeroPayment($payment_id, $payment_amount, $description)) { echo ""; @@ -125,11 +140,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { echo "Error processing booking: $error_message"; } } else { - addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description); - sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description); + // Create payments row + $pstmt = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, booking_id, public_ref) VALUES (?, ?, ?, ?, ?, ?, ?)"); + if ($pstmt) { + $pstmt->bind_param('sidssis', $payment_id, $user_id, $payment_amount, $status, $description, $booking_id, $publicRef); + $pstmt->execute(); + $pstmt->close(); + } + + // Create iKhokha payment link + $resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef); + + // Send invoice and admin notification (keep for records) sendAdminNotification('New Course Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description); - header("Location: payment_confirmation?token=".encryptData($booking_id, $salt)); - exit(); // Ensure no further code is executed after the redirect + + // Redirect user to payment link if available + $paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null; + if ($paylink) { + header('Location: ' . $paylink); + exit(); + } else { + // Fallback: redirect to legacy payment confirmation page + header("Location: payment_confirmation?token=".encryptData($booking_id, $salt)); + exit(); + } } } else { // Handle error if insert fails and echo the MySQL error diff --git a/src/processors/process_event.php b/src/processors/process_event.php index 3a4e2575..cc352ab4 100644 --- a/src/processors/process_event.php +++ b/src/processors/process_event.php @@ -78,19 +78,17 @@ if (!$name || !$type || !$location || !$date || !$time || !$feature || !$descrip $image_path = null; if (!empty($_FILES['image']['name'])) { $upload_dir = $rootPath . '/assets/images/events/'; - if (!is_dir($upload_dir)) { - mkdir($upload_dir, 0755, true); + if (!file_exists($upload_dir)) { + mkdir($upload_dir, 0777, true); } $file_name = uniqid() . '_' . basename($_FILES['image']['name']); $target_file = $upload_dir . $file_name; - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $file_type = finfo_file($finfo, $_FILES['image']['tmp_name']); - finfo_close($finfo); - // Validate image file - $allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; - if (!in_array($file_type, $allowed_types)) { + // Validate file extension + $ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION)); + $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + if (!in_array($ext, $allowed_extensions)) { echo json_encode(['status' => 'error', 'message' => 'Invalid image file type. Only JPEG, PNG, GIF, and WebP are allowed']); exit; } @@ -110,19 +108,17 @@ if (!empty($_FILES['image']['name'])) { $promo_path = null; if (!empty($_FILES['promo']['name'])) { $upload_dir = $rootPath . '/assets/images/events/'; - if (!is_dir($upload_dir)) { - mkdir($upload_dir, 0755, true); + if (!file_exists($upload_dir)) { + mkdir($upload_dir, 0777, true); } $file_name = uniqid() . '_promo_' . basename($_FILES['promo']['name']); $target_file = $upload_dir . $file_name; - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $file_type = finfo_file($finfo, $_FILES['promo']['tmp_name']); - finfo_close($finfo); - // Validate image file - $allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; - if (!in_array($file_type, $allowed_types)) { + // Validate file extension + $ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION)); + $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + if (!in_array($ext, $allowed_extensions)) { echo json_encode(['status' => 'error', 'message' => 'Invalid promo image file type. Only JPEG, PNG, GIF, and WebP are allowed']); exit; } diff --git a/src/processors/process_membership_payment.php b/src/processors/process_membership_payment.php index cede548e..8d81ddfa 100644 --- a/src/processors/process_membership_payment.php +++ b/src/processors/process_membership_payment.php @@ -15,64 +15,92 @@ if (!$user_id) { echo ""; exit(); } -$is_member = getUserMemberStatus($user_id); -$query = "SELECT payment_amount, payment_status, membership_end_date FROM membership_fees WHERE user_id = ?"; +// Fetch the membership fee record for this user +$query = "SELECT fee_id, payment_amount, payment_status, membership_end_date FROM membership_fees WHERE user_id = ?"; $stmt = $conn->prepare($query); +if (!$stmt) { + http_response_code(500); + echo json_encode(['error' => 'Server error preparing statement']); + exit(); +} $stmt->bind_param('i', $user_id); $stmt->execute(); $result = $stmt->get_result(); -// Check if trip exists +// Check if membership fee exists if ($result->num_rows === 0) { - $response = ['error' => 'Application Fee not found.']; + $response = ['error' => 'Membership fee not found.']; header('Content-Type: application/json'); echo json_encode($response); exit(); } -// Fetch trip details +// Fetch fee details $fee = $result->fetch_assoc(); +$fee_id = isset($fee['fee_id']) ? intval($fee['fee_id']) : null; $payment_status = $fee['payment_status']; $membership_end_date = $fee['membership_end_date']; -$payment_amount = intval($fee['payment_amount']); +$payment_amount = floatval($fee['payment_amount']); +$publicRef = bin2hex(random_bytes(16)); $description = "4WDCSA: Membership Fee " . getFullName($user_id) . " " . date("Y"); $payment_id = uniqid(); -$eft_id = "SUBS 2025 ".getLastName($user_id); -// Update the membership_fees table to set payment_id -$stmt = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE user_id = ?"); -if ($stmt) { - $stmt->bind_param("ss", $payment_id, $user_id); - - if (!$stmt->execute()) { - throw new Exception("Failed to update membership_fees table."); +// Persist the generated payment_id back to the membership_fees row (use fee_id to be precise) +$updateStmt = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE fee_id = ?"); +if ($updateStmt) { + $updateStmt->bind_param("si", $payment_id, $fee_id); + if (!$updateStmt->execute()) { + throw new Exception("Failed to update membership_fees table: " . $updateStmt->error); } - - $stmt->close(); - $conn->close(); + $updateStmt->close(); } else { throw new Exception("Failed to prepare statement for membership_fees table: " . $conn->error); } -// Get the current date -$current_date = new DateTime(); +// If the amount is zero, treat as paid immediately +if ($payment_amount < 1) { + if (processZeroPayment($payment_id, $payment_amount, $description)) { + // Update membership_fees status to PAID + $paidStmt = $conn->prepare("UPDATE membership_fees SET payment_status = 'PAID' WHERE fee_id = ?"); + if ($paidStmt) { + $paidStmt->bind_param('i', $fee_id); + $paidStmt->execute(); + $paidStmt->close(); + } + echo ""; + exit(); + } else { + echo ""; + exit(); + } +} else { + // Create payments row + $status = "AWAITING PAYMENT"; + $pstmt = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, public_ref) VALUES (?, ?, ?, ?, ?, ?)"); + if ($pstmt) { + $pstmt->bind_param('sidsss', $payment_id, $user_id, $payment_amount, $status, $description, $publicRef); + $pstmt->execute(); + $pstmt->close(); + } -// Convert $membership_end_date to a DateTime object -$membership_end_date_obj = DateTime::createFromFormat('Y-m-d', $membership_end_date); + // Create iKhokha payment link + $resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef); -// Check if the current date is after membership_end_date -// OR if the current date is before or on membership_end_date AND payment_status is "PENDING" -if ( - $current_date > $membership_end_date_obj || - ($current_date <= $membership_end_date_obj && $payment_status === "PENDING") -) { + // Send invoice and admin notification if desired + // sendInvoice(getEmail($user_id), getFullName($user_id), 'MEMBERSHIP-'.date('Y'), formatCurrency($payment_amount), $description); + sendAdminNotification('Membership Payment Initiated - '.getFullName($user_id), getFullName($user_id).' initiated a membership payment.'); - // Call the processMembershipPayment function - // processMembershipPayment($payment_id, $payment_amount, $description); - addMembershipEFT($eft_id, $user_id, $status, $amount, $description, $membershipfee_id); - header("Location: payment_confirmation?booking_id=" . $booking_id); - exit(); // Ensure no further code is executed after the redirect + // Redirect user to payment link if available + $paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null; + if ($paylink) { + header('Location: ' . $paylink); + exit(); + } else { + // Fallback: redirect to a membership page with an encrypted token + header("Location: membership_confirmation?token=" . encryptData($payment_id, $salt)); + exit(); + } } diff --git a/src/processors/process_signature.php b/src/processors/process_signature.php index 9ff21efb..f0c6694a 100644 --- a/src/processors/process_signature.php +++ b/src/processors/process_signature.php @@ -36,7 +36,7 @@ if (isset($_POST['signature'])) { $filePath = $rootPath . '/uploads/signatures/' . $fileName; // Ensure the directory exists - if (!is_dir($rootPath . '/uploads/signatures')) { + if (!file_exists($rootPath . '/uploads/signatures')) { mkdir($rootPath . '/uploads/signatures', 0777, true); } @@ -56,17 +56,122 @@ if (isset($_POST['signature'])) { $stmt->bind_param('si', $display_path, $user_id); if ($stmt->execute()) { + // Audit: signature saved + if (function_exists('auditLog')) { + auditLog($user_id, 'SIGNATURE_SAVED', 'membership_application', null, ['path' => $display_path]); + } // Check the payment status $paymentStatus = checkMembershipPaymentStatus($user_id) ? 'PAID' : 'NOT_PAID'; - // Respond with the appropriate redirect URL based on the payment status + // If not paid, create a payments row (if missing) and initiate iKhokha paylink + $paylink = null; + if ($paymentStatus !== 'PAID') { + // Fetch the membership fee row to get amount and payment_id + $mfStmt = $conn->prepare("SELECT fee_id, payment_amount, payment_id FROM membership_fees WHERE user_id = ? ORDER BY fee_id DESC LIMIT 1"); + if ($mfStmt) { + $mfStmt->bind_param('i', $user_id); + $mfStmt->execute(); + $mfRes = $mfStmt->get_result(); + $mf = $mfRes->fetch_assoc(); + $mfStmt->close(); + } else { + $mf = null; + } + + if ($mf && isset($mf['payment_amount'])) { + $amount = floatval($mf['payment_amount']); + // Use existing payment_id or generate one + $payment_id = $mf['payment_id'] ?? generatePaymentRef('SUBS', null, $user_id);; + + if (empty($mf['payment_id'])) { + // Persist generated payment_id back to membership_fees + $u = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE fee_id = ?"); + if ($u) { + $u->bind_param('si', $payment_id, $mf['fee_id']); + $u->execute(); + $u->close(); + } + } + + // Ensure a payments row exists + $checkP = $conn->prepare("SELECT COUNT(*) AS cnt FROM payments WHERE payment_id = ? LIMIT 1"); + if ($checkP) { + $checkP->bind_param('s', $payment_id); + $checkP->execute(); + $r = $checkP->get_result()->fetch_assoc(); + $exists = intval($r['cnt']) > 0; + $checkP->close(); + } else { + $exists = false; + } + + if (!$exists) { + $publicRef = bin2hex(random_bytes(16)); + // If current month is December, attribute the membership year to the next year + $currentYear = intval(date('Y')); + $month = intval(date('n')); + if ($month === 12) { + $membershipYear = $currentYear + 1; + } else { + $membershipYear = $currentYear; + } + $description = 'Membership Fees ' . $membershipYear . ' ' . getInitialSurname($user_id); + $status = 'AWAITING PAYMENT'; + $ins = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, public_ref) VALUES (?, ?, ?, ?, ?, ?)"); + if ($ins) { + $ins->bind_param('sidsss', $payment_id, $user_id, $amount, $status, $description, $publicRef); + if ($ins->execute()) { + // Audit: payment row created for membership + if (function_exists('auditLog')) { + auditLog($user_id, 'MEMBERSHIP_PAYMENT_CREATED', 'payments', null, ['payment_id' => $payment_id, 'amount' => $amount]); + } + } + $ins->close(); + } + } + + // Create iKhokha paylink via helper (functions.php) + try { + $publicRef = $publicRef ?? bin2hex(random_bytes(16)); + $resp = createIkhokhaPayment($payment_id, $amount, $desc ?? ('Membership Fee ' . date('Y')), $publicRef); + $paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null; + // After creating paylink, update paymentStatus to AWAITING PAYMENT + $paymentStatus = $paylink ? 'AWAITING PAYMENT' : $paymentStatus; + $token = encryptData($payment_id, $_ENV['SALT']); + // Audit: paylink created (or attempted) + if (function_exists('auditLog')) { + auditLog($user_id, 'IKHOKHA_PAYLINK_CREATED', 'payments', null, ['payment_id' => $payment_id, 'paylink' => $paylink]); + } + } catch (Exception $e) { + // Log but do not fail signature save + error_log('iKhokha create error: ' . $e->getMessage()); + if (function_exists('auditLog')) { + auditLog($user_id, 'IKHOKHA_PAYLINK_FAILED', 'payments', null, ['payment_id' => $payment_id, 'error' => $e->getMessage()]); + } + } + } + } + + // Respond with the appropriate redirect URL and paylink (if created) ob_end_clean(); - echo json_encode([ + $response = [ 'status' => 'success', 'message' => 'Signature saved successfully!', - 'paymentStatus' => $paymentStatus // Send payment status - ]); + 'paymentStatus' => $paymentStatus, + 'token' => $token ?? null + ]; + if (!empty($paylink)) { + $response['paylinkUrl'] = $paylink; + } + if (!empty($payment_id)) { + $response['payment_id'] = $payment_id; + } + echo json_encode($response); } else { + // Audit: signature save failed + if (function_exists('auditLog')) { + auditLog($user_id, 'SIGNATURE_SAVE_FAILED', 'membership_application', null, ['user_id' => $user_id]); + } ob_end_clean(); echo json_encode(['status' => 'error', 'message' => 'Database update failed']); } @@ -78,6 +183,10 @@ if (isset($_POST['signature'])) { echo json_encode(['status' => 'error', 'message' => 'Failed to save signature']); } } else { + // Audit: no signature provided in request + if (function_exists('auditLog') && isset($_SESSION['user_id'])) { + auditLog($_SESSION['user_id'], 'SIGNATURE_NOT_PROVIDED', 'membership_application', null, ['endpoint' => 'process_signature.php']); + } ob_end_clean(); echo json_encode(['status' => 'error', 'message' => 'Signature not provided']); } diff --git a/src/processors/process_trip.php b/src/processors/process_trip.php index 2d8ef848..7f4a2901 100644 --- a/src/processors/process_trip.php +++ b/src/processors/process_trip.php @@ -136,8 +136,8 @@ try { $upload_dir = $rootPath . '/assets/images/trips/'; // Create directory if it doesn't exist - if (!is_dir($upload_dir)) { - mkdir($upload_dir, 0755, true); + if (!file_exists($upload_dir)) { + mkdir($upload_dir, 0777, true); } $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; diff --git a/src/processors/process_trip_booking.php b/src/processors/process_trip_booking.php index fe7916c8..0e0183d4 100644 --- a/src/processors/process_trip_booking.php +++ b/src/processors/process_trip_booking.php @@ -3,6 +3,7 @@ $rootPath = dirname(dirname(__DIR__)); require_once($rootPath . "/src/config/env.php"); require_once($rootPath . "/src/config/connection.php"); require_once($rootPath . "/src/config/functions.php"); +require_once($rootPath . "/src/helpers/notification_helper.php"); session_start(); // Get the trip_id from the request (ensure it's sanitized) @@ -78,6 +79,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $member_discount = $cost_nonmembers - $cost_members; $member_discount_pensioner = $cost_pensioner - $cost_pensioner_member; $booking_fee = $trip['booking_fee']; + // Radio option (boolean/int) — ensure defined from POST + $radio = isset($_POST['radio']) ? intval($_POST['radio']) : 0; $radioCost = $radio ? 50 : 0; $start_date = $trip['start_date']; // Start date of the trip $end_date = $trip['end_date']; // End date of the trip @@ -103,9 +106,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $status = "AWAITING PAYMENT"; $description = $trip_name; $type = 'trip'; - $payment_id = uniqid(); + $payment_id = generatePaymentRef('TRIP', $trip_id, $user_id); + $publicRef = bin2hex(random_bytes(16)); // $eft_id = strtoupper(base_convert(time(), 10, 36)); // Convert timestamp to base36 - $eft_id = strtoupper($trip_code." ".getInitialSurname($user_id)); + // $eft_id = strtoupper($trip_code." ".getInitialSurname($user_id)); // Insert booking into the database @@ -123,6 +127,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Get the generated booking_id $booking_id = $conn->insert_id; + // Audit booking creation + if (function_exists('auditLog')) { + auditLog($user_id, 'TRIP_BOOKING_CREATED', 'bookings', $booking_id, ['trip_id' => $trip_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]); + } + + // Create notification for new booking + $event = 'new_trip_booking_created'; + $sub_feed = 'bookings'; + $data = [ + 'actor_id' => $_SESSION['user_id'] ?? null, + 'actor_avatar' => $_SESSION['profile_pic'] ?? null, // used by UI to show avatar + 'title' => "New Trip Booking Created: {$payment_id}" + ]; + addNotification(null, $event, $sub_feed, $data, null); + if ($payment_amount < 1) { if (processZeroPayment($payment_id, $payment_amount, $description)) { echo ""; @@ -131,11 +150,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { echo "Error processing booking: $error_message"; } } else { - addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description); - sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description); - sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description); - header("Location: payment_confirmation?token=".encryptData($booking_id, $salt)); - exit(); // Ensure no further code is executed after the redirect + // Create payments row + $pstmt = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, booking_id, public_ref) VALUES (?, ?, ?, ?, ?, ?, ?)"); + if ($pstmt) { + $pstmt->bind_param('sidssis', $payment_id, $user_id, $payment_amount, $status, $description, $booking_id, $publicRef); + $pstmt->execute(); + $pstmt->close(); + } + + // Create iKhokha payment link + $resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef); + + // Send invoice and admin notification + sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description); + + // Redirect to payment link if available + $paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null; + if ($paylink) { + header('Location: ' . $paylink); + exit(); + } else { + header("Location: payment_confirmation?token=".encryptData($booking_id, $salt)); + exit(); + } } } else { // Handle error if insert fails and echo the MySQL error diff --git a/src/processors/save_album.php b/src/processors/save_album.php index 8338ea59..2c8a031e 100644 --- a/src/processors/save_album.php +++ b/src/processors/save_album.php @@ -52,26 +52,25 @@ try { // Create album directory $albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id; - if (!is_dir($albumDir)) { - if (!mkdir($albumDir, 0755, true)) { - throw new Exception('Failed to create album directory'); - } + if (!file_exists($albumDir)) { + mkdir($albumDir, 0777, true); } // Handle cover image upload $coverImagePath = null; - if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] !== UPLOAD_ERR_NO_FILE) { - $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) { $maxSize = 5 * 1024 * 1024; // 5MB $fileName = $_FILES['cover_image']['name']; $fileTmpName = $_FILES['cover_image']['tmp_name']; $fileSize = $_FILES['cover_image']['size']; - $fileMime = mime_content_type($fileTmpName); + + // Validate file extension + $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; - // Validate file - if (!in_array($fileMime, $allowedMimes)) { - throw new Exception('Invalid cover image file type'); + if (!in_array($ext, $allowedExtensions)) { + throw new Exception('Invalid cover image file type. Allowed: jpg, jpeg, png, gif, webp'); } if ($fileSize > $maxSize) { @@ -96,8 +95,7 @@ try { } // Handle photo uploads - if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] !== UPLOAD_ERR_NO_FILE) { - $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] === UPLOAD_ERR_OK) { $maxSize = 5 * 1024 * 1024; // 5MB $displayOrder = 1; @@ -111,11 +109,13 @@ try { $fileName = $_FILES['photos']['name'][$i]; $fileTmpName = $_FILES['photos']['tmp_name'][$i]; $fileSize = $_FILES['photos']['size'][$i]; - $fileMime = mime_content_type($fileTmpName); + + // Validate file extension + $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; - // Validate file - if (!in_array($fileMime, $allowedMimes)) { - throw new Exception('Invalid file type: ' . $fileName); + if (!in_array($ext, $allowedExtensions)) { + throw new Exception('Invalid file type: ' . $fileName . '. Allowed: jpg, jpeg, png, gif, webp'); } if ($fileSize > $maxSize) { diff --git a/src/processors/submit_pop.php b/src/processors/submit_pop.php index cf6db027..4bc4415b 100644 --- a/src/processors/submit_pop.php +++ b/src/processors/submit_pop.php @@ -43,14 +43,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $filename = str_replace(' ', '_', $eft_id) . '.pdf'; $target_file = $target_dir . $filename; - // Make sure target directory exists and writable - if (!is_dir($target_dir)) { - mkdir($target_dir, 0755, true); - } - - if (!is_writable($target_dir)) { - echo "
    Upload directory is not writable: $target_dir
    "; - exit; + // Make sure target directory exists + if (!file_exists($target_dir)) { + mkdir($target_dir, 0777, true); } if (move_uploaded_file($_FILES['pop_file']['tmp_name'], $target_file)) { diff --git a/src/processors/update_album.php b/src/processors/update_album.php index e225cac1..0da0c73c 100644 --- a/src/processors/update_album.php +++ b/src/processors/update_album.php @@ -76,25 +76,29 @@ try { $updateStmt->close(); // Handle cover image upload if provided - if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] !== UPLOAD_ERR_NO_FILE) { - $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; - $maxSize = 5 * 1024 * 1024; // 5MB - + if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) { $fileName = $_FILES['cover_image']['name']; $fileTmpName = $_FILES['cover_image']['tmp_name']; $fileSize = $_FILES['cover_image']['size']; - $fileMime = mime_content_type($fileTmpName); - - // Validate file - if (!in_array($fileMime, $allowedMimes)) { - throw new Exception('Invalid cover image file type'); + + // Validate file extension + $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + + if (!in_array($ext, $allowedExtensions)) { + throw new Exception('Invalid cover image file type. Allowed: jpg, jpeg, png, gif, webp'); } - if ($fileSize > $maxSize) { + if ($fileSize > 5 * 1024 * 1024) { throw new Exception('Cover image file too large (max 5MB)'); } $albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id; + + // Create directory if it doesn't exist (match working pattern) + if (!file_exists($albumDir)) { + mkdir($albumDir, 0777, true); + } // Delete old cover if it exists $oldCoverStmt = $conn->prepare("SELECT cover_image FROM photo_albums WHERE album_id = ?"); @@ -104,16 +108,15 @@ try { if ($oldCoverResult->num_rows > 0) { $oldCover = $oldCoverResult->fetch_assoc(); if ($oldCover['cover_image']) { - $oldCoverPath = $_SERVER['DOCUMENT_ROOT'] . $oldCover['cover_image']; + $oldCoverPath = $rootPath . $oldCover['cover_image']; if (file_exists($oldCoverPath)) { - unlink($oldCoverPath); + @unlink($oldCoverPath); } } } $oldCoverStmt->close(); // Generate unique filename - $ext = pathinfo($fileName, PATHINFO_EXTENSION); $newFileName = 'cover_' . uniqid() . '.' . $ext; $filePath = $albumDir . '/' . $newFileName; $coverImagePath = '/assets/uploads/gallery/' . $album_id . '/' . $newFileName; @@ -130,12 +133,15 @@ try { } // Handle photo uploads if any - if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] !== UPLOAD_ERR_NO_FILE) { - $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] === UPLOAD_ERR_OK) { $maxSize = 5 * 1024 * 1024; // 5MB - $albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id; + // Create directory if it doesn't exist (match working pattern) + if (!file_exists($albumDir)) { + mkdir($albumDir, 0777, true); + } + // Get current max display order $orderStmt = $conn->prepare("SELECT MAX(display_order) as max_order FROM photos WHERE album_id = ?"); $orderStmt->bind_param("i", $album_id); @@ -153,15 +159,17 @@ try { $fileName = $_FILES['photos']['name'][$i]; $fileTmpName = $_FILES['photos']['tmp_name'][$i]; $fileSize = $_FILES['photos']['size'][$i]; - $fileMime = mime_content_type($fileTmpName); - - // Validate file - if (!in_array($fileMime, $allowedMimes)) { - throw new Exception('Invalid file type: ' . $fileName); + + // Validate file extension + $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + + if (!in_array($ext, $allowedExtensions)) { + throw new Exception('Invalid file type: ' . $fileName . '. Allowed: jpg, jpeg, png, gif, webp'); } if ($fileSize > $maxSize) { - throw new Exception('File too large: ' . $fileName); + throw new Exception('File too large: ' . $fileName . ' (max 5MB)'); } // Generate unique filename diff --git a/src/processors/upload_profile_picture.php b/src/processors/upload_profile_picture.php index 7a174e39..f3868fff 100644 --- a/src/processors/upload_profile_picture.php +++ b/src/processors/upload_profile_picture.php @@ -43,15 +43,9 @@ if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] != $target_dir = $rootPath . "/assets/images/pp/"; $target_file = $target_dir . $randomFilename; - // Ensure upload directory exists and is writable - if (!is_dir($target_dir)) { - mkdir($target_dir, 0755, true); - } - - if (!is_writable($target_dir)) { - $response['message'] = 'Upload directory is not writable.'; - echo json_encode($response); - exit(); + // Ensure upload directory exists + if (!file_exists($target_dir)) { + mkdir($target_dir, 0777, true); } // Move the uploaded file diff --git a/validate_login.php b/validate_login.php index 59e3bdbd..e23a6ad3 100644 --- a/validate_login.php +++ b/validate_login.php @@ -5,6 +5,8 @@ require_once($rootPath . "/src/config/session.php"); require_once($rootPath . "/src/config/connection.php"); require_once($rootPath . "/src/config/functions.php"); require_once($rootPath . '/google-client/vendor/autoload.php'); // Add this line for Google Client +require_once($rootPath . "/src/helpers/notification_helper.php"); + // Check if connection is established if (!$conn) { @@ -37,6 +39,8 @@ if (isset($_GET['code'])) { $last_name = $google_account_info->family_name; $picture = $google_account_info->picture; + + // Check if the user exists in the database $query = "SELECT * FROM users WHERE email = ?"; $stmt = $conn->prepare($query); @@ -53,12 +57,20 @@ if (isset($_GET['code'])) { $stmt->bind_param("sssssi", $email, $first_name, $last_name, $picture, $password, $is_verified); if ($stmt->execute()) { // User successfully registered, set session and redirect - sendEmail('chrispintoza@gmail.com', '4WDCSA: New User Login', $name.' has just created an account using Google Login.'); $_SESSION['user_id'] = $conn->insert_id; $_SESSION['first_name'] = $first_name; $_SESSION['profile_pic'] = $picture; - processLegacyMembership($_SESSION['user_id']); - // echo json_encode(['status' => 'success', 'message' => 'Google login successful']); + + // Send Notification + $event = 'user_login'; + $sub_feed = 'logins'; + $data = [ + 'actor_id' => $conn->insert_id, + 'actor_avatar' => $picture, // used by UI to show avatar + 'title' => "User Login by {$first_name} {$last_name}" + ]; + addNotification(null, $event, $sub_feed, $data, null); + header("Location: index.php"); exit(); } else { @@ -72,8 +84,17 @@ if (isset($_GET['code'])) { $_SESSION['user_id'] = $row['user_id']; $_SESSION['first_name'] = $row['first_name']; $_SESSION['profile_pic'] = $row['profile_pic']; - sendEmail('chrispintoza@gmail.com', '4WDCSA: New User Login', $name.' has just logged in using Google Login.'); - // echo json_encode(['status' => 'success', 'message' => 'Google login successful']); + + // Send Notification + $event = 'user_login'; + $sub_feed = 'logins'; + $data = [ + 'actor_id' => $_SESSION['user_id'], + 'actor_avatar' => $_SESSION['profile_pic'], // used by UI to show avatar + 'title' => "User Login by {$first_name} {$last_name}" + ]; + addNotification(null, $event, $sub_feed, $data, null); + header("Location: index.php"); exit(); } @@ -181,6 +202,16 @@ if (isset($_POST['email']) && isset($_POST['password'])) { // Set session timeout (30 minutes) $_SESSION['login_time'] = time(); $_SESSION['session_timeout'] = 1800; // 30 minutes in seconds + + // Send Notification + $event = 'user_login'; + $sub_feed = 'logins'; + $data = [ + 'actor_id' => $_SESSION['user_id'], + 'actor_avatar' => $_SESSION['profile_pic'], // used by UI to show avatar + 'title' => "User Login by {$row['first_name']} {$row['last_name']}" + ]; + addNotification(null, $event, $sub_feed, $data, null); auditLog($row['user_id'], 'LOGIN_SUCCESS', 'users', $row['user_id']); echo json_encode(['status' => 'success', 'message' => 'Successful Login']);