12 Commits

Author SHA1 Message Date
twotalesanimation
fdeaf85bf0 Update: Add publish/unpublish button to admin trips table and improve table styling 2025-12-04 18:35:36 +02:00
twotalesanimation
d81d74a7c7 Fix: Add env.php include to delete_trip and toggle_trip_published processors 2025-12-04 17:31:27 +02:00
twotalesanimation
bfb3a0f8a9 Fix: Correct bind_param type strings for date fields in trip processor 2025-12-04 17:26:05 +02:00
twotalesanimation
5a2c48f343 Fix: Correct CSRF token validation in process_trip processor 2025-12-04 17:07:29 +02:00
twotalesanimation
1767337d99 Update: Allow superadmin role to manage trips alongside admin 2025-12-04 17:06:34 +02:00
twotalesanimation
674af23994 Feature: Add trip publisher system - create, edit, delete, and publish trips 2025-12-04 16:56:31 +02:00
twotalesanimation
ec563e0376 Update: Formatting and code cleanup in processor and config files 2025-12-04 16:41:10 +02:00
twotalesanimation
a3403bf503 Fix: Move POP notification email addresses to .env configuration
- Updated sendPOP() function to read recipient emails from POP_NOTIFICATION_EMAILS env variable
- Supports comma-separated email list for flexibility
- Allows dev and live servers to have different notification recipients
- Falls back to info@4wdcsa.co.za if no env variable configured
2025-12-04 16:14:16 +02:00
twotalesanimation
5f1a6bc441 Fix: Use EFT ID as filename for POP uploads instead of random filename
- Changed from random filename to eft_id.pdf format for proof of payment files
- Updated sendPOP() and auditLog() calls to use new filename variable
2025-12-04 16:11:37 +02:00
twotalesanimation
716de2f0e9 Fix: Clean output buffer in upload_profile_picture.php to prevent HTML in JSON response
- Move header() call to before any includes that might output
- Start output buffering at the beginning
- Clean output buffer before sending JSON response
2025-12-04 16:05:44 +02:00
twotalesanimation
79e292dc7c Fix: Profile picture upload AJAX response handling
- Add dataType: 'json' to AJAX call to properly parse JSON response
- Add Content-Type header to upload_profile_picture.php
- Add error callback with console logging for debugging
- Remove manual JSON parsing since jQuery handles it with dataType
2025-12-04 16:04:22 +02:00
twotalesanimation
59c1e37d5c Fix: Profile picture upload issues and improved error handling
- account_settings.php: Show success message before reloading page (with 1.5s delay)
- upload_profile_picture.php: Reorder require statements for proper initialization, add file error code to error message
2025-12-04 15:59:49 +02:00
31 changed files with 1116 additions and 51 deletions

View File

@@ -82,6 +82,8 @@ RewriteRule ^admin_trip_bookings$ src/admin/admin_trip_bookings.php [L]
RewriteRule ^admin_visitors$ src/admin/admin_visitors.php [L] RewriteRule ^admin_visitors$ src/admin/admin_visitors.php [L]
RewriteRule ^admin_efts$ src/admin/admin_efts.php [L] RewriteRule ^admin_efts$ src/admin/admin_efts.php [L]
RewriteRule ^add_campsite$ src/admin/add_campsite.php [L] RewriteRule ^add_campsite$ src/admin/add_campsite.php [L]
RewriteRule ^admin_trips$ src/admin/admin_trips.php [L]
RewriteRule ^manage_trips$ src/admin/manage_trips.php [L]
# === API/AJAX ENDPOINTS === # === API/AJAX ENDPOINTS ===
RewriteRule ^fetch_users$ src/api/fetch_users.php [L] RewriteRule ^fetch_users$ src/api/fetch_users.php [L]
@@ -111,6 +113,9 @@ RewriteRule ^update_user$ src/processors/update_user.php [L]
RewriteRule ^upload_profile_picture$ src/processors/upload_profile_picture.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 ^send_reset_link$ src/processors/send_reset_link.php [L]
RewriteRule ^logout$ src/processors/logout.php [L] RewriteRule ^logout$ src/processors/logout.php [L]
RewriteRule ^process_trip$ src/processors/process_trip.php [L]
RewriteRule ^toggle_trip_published$ src/processors/toggle_trip_published.php [L]
RewriteRule ^delete_trip$ src/processors/delete_trip.php [L]
</IfModule> </IfModule>

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View File

@@ -283,6 +283,7 @@ if ($headerStyle === 'light') {
<ul> <ul>
<li><a href="admin_web_users">Website Users</a></li> <li><a href="admin_web_users">Website Users</a></li>
<li><a href="admin_members">4WDCSA Members</a></li> <li><a href="admin_members">4WDCSA Members</a></li>
<li><a href="admin_trips">Manage Trips</a></li>
<li><a href="admin_trip_bookings">Trip Bookings</a></li> <li><a href="admin_trip_bookings">Trip Bookings</a></li>
<li><a href="admin_course_bookings">Course Bookings</a></li> <li><a href="admin_course_bookings">Course Bookings</a></li>
<li><a href="admin_efts">EFT Payments</a></li> <li><a href="admin_efts">EFT Payments</a></li>

View File

@@ -174,7 +174,7 @@ if (!empty($bannerImages)) {
// Fetch bookings for the current trip // Fetch bookings for the current trip
$bookingsSql = "SELECT b.user_id, b.num_vehicles, b.num_adults, b.num_children, b.num_pensioners, b.radio, b.status, $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.first_name, u.last_name, u.profile_pic,
(b.total_amount - b.discount_amount) AS paid (b.total_amount - b.discount_amount) AS paid
FROM bookings b FROM bookings b
INNER JOIN users u ON b.user_id = u.user_id INNER JOIN users u ON b.user_id = u.user_id

320
src/admin/admin_trips.php Normal file
View File

@@ -0,0 +1,320 @@
<?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
// Fetch all trips with booking status
$trips_query = "
SELECT
trip_id, trip_name, location, start_date, end_date,
vehicle_capacity, places_booked, cost_members, published
FROM trips
ORDER BY start_date DESC
";
$result = $conn->query($trips_query);
$trips = [];
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$trips[] = $row;
}
}
?>
<style>
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 10px 0;
}
thead th {
cursor: pointer;
text-align: left;
padding: 10px;
font-weight: bold;
position: relative;
}
thead th::after {
content: '\25B2';
/* Up arrow */
font-size: 0.8em;
position: absolute;
right: 10px;
opacity: 0;
transition: opacity 0.2s;
}
thead th.asc::after {
content: '\25B2';
/* Up arrow */
opacity: 1;
}
thead th.desc::after {
content: '\25BC';
/* Down arrow */
opacity: 1;
}
tbody tr:nth-child(odd) {
background-color: transparent;
}
tbody tr:nth-child(even) {
background-color: rgb(255, 255, 255);
border-radius: 10px;
}
tbody td {
padding: 5px;
}
tbody tr:nth-child(even) td:first-child {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
tbody tr:nth-child(even) td:last-child {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
.filter-input {
width: 100%;
padding: 5px;
font-size: 16px;
background-color: rgb(255, 255, 255);
border-radius: 25px;
margin-bottom: 20px;
}
.trips-section {
color: #484848;
background: #f9f9f7;
border: 1px solid #d8d8d8;
border-radius: 10px;
margin-top: 15px;
margin-bottom: 15px;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
const tables = document.querySelectorAll("table");
tables.forEach((table) => {
const headers = table.querySelectorAll("thead th");
const rows = Array.from(table.querySelectorAll("tbody tr"));
const filterInput = table.previousElementSibling;
headers.forEach((header, index) => {
header.addEventListener("click", () => {
const sortedRows = rows.sort((a, b) => {
const aText = a.cells[index].textContent.trim().toLowerCase();
const bText = b.cells[index].textContent.trim().toLowerCase();
if (aText < bText) return -1;
if (aText > bText) return 1;
return 0;
});
if (header.classList.contains("asc")) {
header.classList.remove("asc");
header.classList.add("desc");
sortedRows.reverse();
} else {
headers.forEach(h => h.classList.remove("asc", "desc"));
header.classList.add("asc");
}
const tbody = table.querySelector("tbody");
tbody.innerHTML = "";
sortedRows.forEach(row => tbody.appendChild(row));
});
});
if (rows.length === 0) {
filterInput.style.display = "none";
} else {
filterInput.addEventListener("input", function() {
const filterValue = filterInput.value.trim().toLowerCase();
rows.forEach(row => {
const rowText = row.textContent.trim().toLowerCase();
row.style.display = rowText.includes(filterValue) ? "" : "none";
});
});
}
});
});
</script>
<?php
$bannerFolder = 'assets/images/banners/';
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
$randomBanner = 'assets/images/base4/camping.jpg'; // default fallback
if (!empty($bannerImages)) {
$randomBanner = $bannerImages[array_rand($bannerImages)];
}
?>
<section class="page-banner-area pt-50 pb-35 rel z-1 bgs-cover" style="background-image: url('<?php echo $randomBanner; ?>');">
<div class="banner-overlay"></div>
<div class="container">
<div class="banner-inner text-white mb-50">
<h2 class="page-title mb-10" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">Manage Trips</h2>
<nav aria-label="breadcrumb">
<ol class="breadcrumb justify-content-center mb-20" data-aos="fade-right" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
<li class="breadcrumb-item"><a href="index">Home</a></li>
<li class="breadcrumb-item active">Manage Trips</li>
</ol>
</nav>
</div>
</div>
</section>
<!-- Trips Management Area start -->
<section class="tour-list-page py-100 rel z-1">
<div class="container">
<div style="margin-bottom: 20px;">
<a href="manage_trips" class="theme-btn">
<i class="far fa-plus"></i> Create New Trip
</a>
</div>
<?php
if (count($trips) > 0) {
echo '<input type="text" class="filter-input" placeholder="Filter trips...">';
echo '<div class="trips-section" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
echo '<div style="padding:10px;">';
echo '<table>
<thead>
<tr>
<th>Trip Name</th>
<th>Location</th>
<th>Start Date</th>
<th>End Date</th>
<th>Capacity</th>
<th>Booked</th>
<th>Cost (Member)</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>';
foreach ($trips as $trip) {
$publishButtonText = $trip['published'] == 1 ? 'Unpublish' : 'Publish';
$publishButtonClass = $trip['published'] == 1 ? 'btn-warning' : 'btn-success';
echo '<tr>
<td><strong>' . htmlspecialchars($trip['trip_name']) . '</strong></td>
<td>' . htmlspecialchars($trip['location']) . '</td>
<td>' . date('M d, Y', strtotime($trip['start_date'])) . '</td>
<td>' . date('M d, Y', strtotime($trip['end_date'])) . '</td>
<td>' . $trip['vehicle_capacity'] . '</td>
<td><span class="badge bg-info">' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '</span></td>
<td>R ' . number_format($trip['cost_members'], 2) . '</td>
<td>' . ($trip['published'] == 1 ? '<span class="badge bg-success">Published</span>' : '<span class="badge bg-warning">Draft</span>') . '</td>
<td>
<a href="manage_trips?trip_id=' . $trip['trip_id'] . '" class="btn btn-sm btn-primary" title="Edit">
<i class="far fa-edit"></i>
</a>
<button class="btn btn-sm ' . $publishButtonClass . ' toggle-publish" data-trip-id="' . $trip['trip_id'] . '" title="' . $publishButtonText . '">
<i class="far fa-' . ($trip['published'] == 1 ? 'eye-slash' : 'eye') . '"></i>
</button>
<button class="btn btn-sm btn-danger delete-trip" data-trip-id="' . $trip['trip_id'] . '" title="Delete">
<i class="far fa-trash"></i>
</button>
</td>
</tr>';
}
echo '</tbody></table>';
echo '</div>';
echo '</div>';
} else {
echo '<p>No trips found. <a href="manage_trips">Create one</a></p>';
}
?>
</div>
</section>
<!-- Trips Management Area end -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
$('.toggle-publish').on('click', function() {
var tripId = $(this).data('trip-id');
var button = $(this);
var row = button.closest('tr');
$.ajax({
url: 'toggle_trip_published',
type: 'POST',
data: {
trip_id: tripId
},
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
// Update button appearance
if (response.published == 1) {
button.removeClass('btn-success').addClass('btn-warning');
button.find('i').removeClass('fa-eye').addClass('fa-eye-slash');
button.attr('title', 'Unpublish');
// Update status badge
row.find('td:nth-child(8)').html('<span class="badge bg-success">Published</span>');
} else {
button.removeClass('btn-warning').addClass('btn-success');
button.find('i').removeClass('fa-eye-slash').addClass('fa-eye');
button.attr('title', 'Publish');
// Update status badge
row.find('td:nth-child(8)').html('<span class="badge bg-warning">Draft</span>');
}
} else {
alert('Error: ' + response.message);
}
},
error: function() {
alert('Error updating trip status');
}
});
});
$('.delete-trip').on('click', function() {
if (!confirm('Are you sure you want to delete this trip? This action cannot be undone.')) {
return false;
}
var tripId = $(this).data('trip-id');
var button = $(this);
var row = button.closest('tr');
$.ajax({
url: 'delete_trip',
type: 'POST',
data: {
trip_id: tripId
},
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
row.fadeOut(function() {
$(this).remove();
if ($('table tbody tr').length === 0) {
location.reload();
}
});
} else {
alert('Error: ' + response.message);
}
},
error: function() {
alert('Error deleting trip');
}
});
});
});
</script>
<?php include_once($rootPath . '/components/insta_footer.php'); ?>

200
src/admin/manage_trips.php Normal file
View File

@@ -0,0 +1,200 @@
<?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
$trip_id = $_GET['trip_id'] ?? null;
$trip = null;
// If editing an existing trip, fetch its data
if ($trip_id) {
$stmt = $conn->prepare("SELECT * FROM trips WHERE trip_id = ?");
$stmt->bind_param("i", $trip_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$trip = $result->fetch_assoc();
}
$stmt->close();
}
?>
<?php
$pageTitle = $trip ? 'Edit Trip' : 'Create New Trip';
$breadcrumbs = [['Home' => 'index'], ['Admin' => 'admin_trips'], [$pageTitle => '']];
require_once($rootPath . '/components/banner.php');
?>
<!-- Trip Manager Area start -->
<section class="trip-manager-area py-100 rel z-1">
<div class="container">
<div class="row">
<div class="col-lg-12">
<div class="comment-form bgc-lighter z-1 rel mb-30 rmb-55">
<form id="tripForm" enctype="multipart/form-data" method="POST" action="process_trip">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<?php if ($trip): ?>
<input type="hidden" name="trip_id" value="<?php echo $trip['trip_id']; ?>">
<?php endif; ?>
<div class="section-title py-20">
<h2><?php echo $trip ? 'Edit Trip: ' . htmlspecialchars($trip['trip_name']) : 'Create New Trip'; ?></h2>
<div id="responseMessage"></div>
</div>
<!-- Trip Information -->
<div class="row mt-35">
<div class="col-md-6">
<div class="form-group">
<label for="trip_name">Trip Name *</label>
<input type="text" id="trip_name" name="trip_name" class="form-control" value="<?php echo $trip ? htmlspecialchars($trip['trip_name']) : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="trip_code">Trip Code</label>
<input type="text" id="trip_code" name="trip_code" class="form-control" maxlength="12" value="<?php echo $trip ? htmlspecialchars($trip['trip_code']) : ''; ?>" placeholder="e.g., TRIP001">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="location">Location *</label>
<input type="text" id="location" name="location" class="form-control" value="<?php echo $trip ? htmlspecialchars($trip['location']) : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="vehicle_capacity">Vehicle Capacity *</label>
<input type="number" id="vehicle_capacity" name="vehicle_capacity" class="form-control" min="1" value="<?php echo $trip ? $trip['vehicle_capacity'] : ''; ?>" required>
</div>
</div>
<!-- Dates -->
<div class="col-md-6">
<div class="form-group">
<label for="start_date">Start Date *</label>
<input type="date" id="start_date" name="start_date" class="form-control" value="<?php echo $trip ? $trip['start_date'] : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="end_date">End Date *</label>
<input type="date" id="end_date" name="end_date" class="form-control" value="<?php echo $trip ? $trip['end_date'] : ''; ?>" required>
</div>
</div>
<!-- Descriptions -->
<div class="col-md-12">
<div class="form-group">
<label for="short_description">Short Description *</label>
<textarea id="short_description" name="short_description" class="form-control" rows="3" required><?php echo $trip ? htmlspecialchars($trip['short_description']) : ''; ?></textarea>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label for="long_description">Long Description *</label>
<textarea id="long_description" name="long_description" class="form-control" rows="6" required><?php echo $trip ? htmlspecialchars($trip['long_description']) : ''; ?></textarea>
</div>
</div>
<!-- Pricing -->
<div class="col-md-6">
<div class="form-group">
<label for="cost_members">Member Cost (R) *</label>
<input type="number" id="cost_members" name="cost_members" class="form-control" step="0.01" min="0" value="<?php echo $trip ? $trip['cost_members'] : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="cost_nonmembers">Non-Member Cost (R) *</label>
<input type="number" id="cost_nonmembers" name="cost_nonmembers" class="form-control" step="0.01" min="0" value="<?php echo $trip ? $trip['cost_nonmembers'] : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="cost_pensioner_member">Pensioner Member Cost (R) *</label>
<input type="number" id="cost_pensioner_member" name="cost_pensioner_member" class="form-control" step="0.01" min="0" value="<?php echo $trip ? $trip['cost_pensioner_member'] : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="cost_pensioner">Pensioner Cost (R) *</label>
<input type="number" id="cost_pensioner" name="cost_pensioner" class="form-control" step="0.01" min="0" value="<?php echo $trip ? $trip['cost_pensioner'] : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="booking_fee">Booking Fee (R) *</label>
<input type="number" id="booking_fee" name="booking_fee" class="form-control" step="0.01" min="0" value="<?php echo $trip ? $trip['booking_fee'] : ''; ?>" required>
</div>
</div>
<!-- Images Upload -->
<div class="col-md-12 mt-20">
<div class="form-group">
<label>Trip Images</label>
<p class="text-muted">Upload images for this trip. Ideally 5 different images will be required</p>
<input type="file" name="trip_images[]" class="form-control" accept="image/*" multiple>
<?php if ($trip): ?>
<small class="text-info">Images will be saved to: assets/images/trips/<?php echo $trip_id; ?>_{number}.jpg</small>
<?php endif; ?>
</div>
</div>
<div class="col-md-12 mt-20">
<div class="form-group mb-0">
<button type="submit" class="theme-btn style-two" style="width:100%;">
<?php echo $trip ? 'Update Trip' : 'Create Trip'; ?>
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
<!-- Trip Manager Area end -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
$('#tripForm').on('submit', function(event) {
event.preventDefault();
var formData = new FormData(this);
$.ajax({
url: 'process_trip',
type: 'POST',
data: formData,
contentType: false,
processData: false,
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
$('#responseMessage').html('<div class="alert alert-success">' + response.message + '</div>');
setTimeout(function() {
window.location.href = 'admin_trips';
}, 2000);
} else {
$('#responseMessage').html('<div class="alert alert-danger">' + response.message + '</div>');
console.error('Server error:', response.message);
}
},
error: function(xhr, status, error) {
console.log('AJAX Error:', error);
console.log('Response:', xhr.responseText);
$('#responseMessage').html('<div class="alert alert-danger">Error creating/updating trip: ' + error + '</div>');
}
});
});
});
</script>
<?php include_once($rootPath . '/components/insta_footer.php'); ?>

View File

@@ -209,6 +209,27 @@ function getEFTDetails($eft_id) {
function sendPOP($fullname, $eft_id, $amount, $description) function sendPOP($fullname, $eft_id, $amount, $description)
{ {
// Build the 'To' array from environment variables
$toAddresses = [];
// Parse comma-separated email addresses from .env
$emailsEnv = $_ENV['POP_NOTIFICATION_EMAILS'] ?? '';
if (!empty($emailsEnv)) {
$emails = array_map('trim', explode(',', $emailsEnv));
foreach ($emails as $email) {
if (!empty($email)) {
$toAddresses[] = ['Email' => $email];
}
}
}
// Fallback to default if no emails configured
if (empty($toAddresses)) {
$toAddresses = [
['Email' => 'info@4wdcsa.co.za']
];
}
$message = [ $message = [
'Messages' => [ 'Messages' => [
[ [
@@ -216,20 +237,7 @@ function sendPOP($fullname, $eft_id, $amount, $description)
'Email' => $_ENV['MAILJET_FROM_EMAIL'], 'Email' => $_ENV['MAILJET_FROM_EMAIL'],
'Name' => $_ENV['MAILJET_FROM_NAME'] . ' Web Admin' 'Name' => $_ENV['MAILJET_FROM_NAME'] . ' Web Admin'
], ],
'To' => [ 'To' => $toAddresses,
[
'Email' => 'chrispintoza@gmail.com',
'Name' => 'Chris Pinto'
],
[
'Email' => $_ENV['MAILJET_FROM_EMAIL'],
'Name' => 'Jacqui Boshoff'
],
[
'Email' => 'louiseb@global.co.za',
'Name' => 'Louise Blignault'
]
],
'TemplateID' => 7054062, 'TemplateID' => 7054062,
'TemplateLanguage' => true, 'TemplateLanguage' => true,
'Subject' => "4WDCSA - Proof of Payment Received", 'Subject' => "4WDCSA - Proof of Payment Received",
@@ -2772,7 +2780,7 @@ function url($page) {
'google_validate_login' => '/src/api/google_validate_login.php', 'google_validate_login' => '/src/api/google_validate_login.php',
// Processors // Processors
'validate_login' => '/src/processors/validate_login.php', 'validate_login' => '/validate_login.php',
'register_user' => '/src/processors/register_user.php', 'register_user' => '/src/processors/register_user.php',
'process_application' => '/src/processors/process_application.php', 'process_application' => '/src/processors/process_application.php',
'process_booking' => '/src/processors/process_booking.php', 'process_booking' => '/src/processors/process_booking.php',
@@ -2802,3 +2810,95 @@ function url($page) {
return '/' . $page . '.php'; return '/' . $page . '.php';
} }
/**
* Optimize image by resizing if it exceeds max dimensions
*
* @param string $filePath Path to the image file
* @param int $maxWidth Maximum width in pixels
* @param int $maxHeight Maximum height in pixels
* @return bool Success status
*/
function optimizeImage($filePath, $maxWidth = 1920, $maxHeight = 1080)
{
if (!file_exists($filePath)) {
return false;
}
// Get image info
$imageInfo = getimagesize($filePath);
if (!$imageInfo) {
return false;
}
$width = $imageInfo[0];
$height = $imageInfo[1];
$mime = $imageInfo['mime'];
// Only resize if image is larger than max dimensions
if ($width <= $maxWidth && $height <= $maxHeight) {
return true;
}
// Calculate new dimensions maintaining aspect ratio
$ratio = min($maxWidth / $width, $maxHeight / $height);
$newWidth = (int)($width * $ratio);
$newHeight = (int)($height * $ratio);
// Load image based on type
switch ($mime) {
case 'image/jpeg':
$source = imagecreatefromjpeg($filePath);
break;
case 'image/png':
$source = imagecreatefrompng($filePath);
break;
case 'image/gif':
$source = imagecreatefromgif($filePath);
break;
case 'image/webp':
$source = imagecreatefromwebp($filePath);
break;
default:
return false;
}
if (!$source) {
return false;
}
// Create resized image
$destination = imagecreatetruecolor($newWidth, $newHeight);
// Preserve transparency for PNG and GIF
if ($mime === 'image/png' || $mime === 'image/gif') {
$transparent = imagecolorallocatealpha($destination, 0, 0, 0, 127);
imagefill($destination, 0, 0, $transparent);
imagesavealpha($destination, true);
}
// Resize
imagecopyresampled($destination, $source, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
// Save image
$success = false;
switch ($mime) {
case 'image/jpeg':
$success = imagejpeg($destination, $filePath, 85);
break;
case 'image/png':
$success = imagepng($destination, $filePath, 6);
break;
case 'image/gif':
$success = imagegif($destination, $filePath);
break;
case 'image/webp':
$success = imagewebp($destination, $filePath, 85);
break;
}
// Free up memory
imagedestroy($source);
imagedestroy($destination);
return $success;
}

View File

@@ -12,12 +12,22 @@ $token = $_GET['token'];
// Sanitize the trip_id to prevent SQL injection // Sanitize the trip_id to prevent SQL injection
$trip_id = intval(decryptData($token, $salt)); // Ensures $trip_id is treated as an integer $trip_id = intval(decryptData($token, $salt)); // Ensures $trip_id is treated as an integer
// Check if user is admin or superadmin to allow draft preview
// Check if user is admin/superadmin
$user_role = getUserRole();
$is_admin = in_array($user_role, ['admin', 'superadmin']);
// Prepare the SQL query // Prepare the SQL query
$sql = "SELECT trip_id, trip_name, location, short_description, long_description, start_date, end_date, $sql = "SELECT trip_id, trip_name, location, short_description, long_description, start_date, end_date,
vehicle_capacity, cost_members, cost_nonmembers, places_booked, booking_fee, cost_pensioner, cost_pensioner_member vehicle_capacity, cost_members, cost_nonmembers, places_booked, booking_fee, cost_pensioner, cost_pensioner_member, published
FROM trips FROM trips
WHERE trip_id = ?"; WHERE trip_id = ?";
// If not admin, only show published trips
if (!$is_admin) {
$sql .= " AND published = 1";
}
// Use prepared statements for added security // Use prepared statements for added security
$stmt = $conn->prepare($sql); $stmt = $conn->prepare($sql);
@@ -194,12 +204,39 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
</ol> </ol>
</nav> </nav>
</div> </div>
<!-- Draft Notice for Admin -->
<?php if ($is_admin && isset($row['published']) && $row['published'] == 0): ?>
<div class="alert alert-warning mt-3" role="alert">
<strong><i class="fas fa-exclamation-triangle"></i> Draft Trip</strong><br>
This trip is currently in draft status and is not visible to regular users. Only admins and superadmins can preview it.
</div>
<?php endif; ?>
<!-- Publish/Unpublish Button -->
<?php
$user_role = getUserRole();
if (in_array($user_role, ['admin', 'superadmin'])):
// Use published status from the main query
$is_published = $row['published'] ?? 0;
?>
<div class="admin-actions mt-20">
<button type="button" class="theme-btn" style="width: 100%; id="publishBtn" onclick="toggleTripPublished(<?php echo $trip_id; ?>)">
<?php if ($is_published): ?>
<i class="fas fa-eye-slash"></i> Unpublish Trip
<?php else: ?>
<i class="fas fa-eye"></i> Publish Trip
<?php endif; ?>
</button>
</div>
<?php endif; ?>
</div> </div>
</section> </section>
<!-- Tour Gallery start --> <!-- Tour Gallery start -->
<div class="tour-gallery"> <div class="tour-gallery">
<div class="container-fluid"> <div class="container-fluid">
@@ -259,6 +296,8 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
</div> </div>
<span class="subtitle mb-15"><?php echo $badge_text; ?></span> <span class="subtitle mb-15"><?php echo $badge_text; ?></span>
</div> </div>
<!-- <div class="col-xl-4 col-lg-5 text-lg-end" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50"> <!-- <div class="col-xl-4 col-lg-5 text-lg-end" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
<div class="tour-header-social mb-10"> <div class="tour-header-social mb-10">
@@ -673,4 +712,42 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
}); });
</script> </script>
<!-- Trip Publish/Unpublish Script -->
<script>
function toggleTripPublished(tripId) {
$.ajax({
url: 'toggle_trip_published',
type: 'POST',
data: {
trip_id: tripId
},
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
// Update button and status badge
const publishBtn = $('#publishBtn');
const statusBadge = $('#publishStatus');
if (response.published === 1) {
publishBtn.html('<i class="fas fa-eye-slash"></i> Unpublish Trip');
statusBadge.html('<span class="badge bg-success">Published</span>');
} else {
publishBtn.html('<i class="fas fa-eye"></i> Publish Trip');
statusBadge.html('<span class="badge bg-warning">Draft</span>');
}
// Show success message
alert(response.message);
} else {
alert('Error: ' + response.message);
}
},
error: function(xhr, status, error) {
console.log('Error:', error);
alert('Error updating trip status');
}
});
}
</script>
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php') ?> <?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php') ?>

View File

@@ -7,14 +7,18 @@ include_once($rootPath . '/header.php');
<style> <style>
.image { .image {
width: 400px; width: 100%;
/* Set your desired width */
height: 350px; height: 350px;
/* Set your desired height */
overflow: hidden; overflow: hidden;
/* Hide any overflow */
display: block; display: block;
/* Ensure proper block behavior */ }
.image img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top;
display: block;
} }
</style> </style>
@@ -52,8 +56,17 @@ include_once($rootPath . '/header.php');
<?php <?php
// Check if user is admin or superadmin to show draft trips
$user_role = getUserRole();
$is_admin = in_array($user_role, ['admin', 'superadmin']);
// Query to retrieve data from the trips table // Query to retrieve data from the trips table
$sql = "SELECT trip_id, trip_name, location, short_description, start_date, end_date, vehicle_capacity, cost_members, places_booked FROM trips WHERE published = 1 AND start_date > CURDATE()"; // Admins see all trips (published and draft), regular users only see published upcoming trips
if ($is_admin) {
$sql = "SELECT trip_id, trip_name, location, short_description, start_date, end_date, vehicle_capacity, cost_members, places_booked, published FROM trips ORDER BY start_date DESC";
} else {
$sql = "SELECT trip_id, trip_name, location, short_description, start_date, end_date, vehicle_capacity, cost_members, places_booked, published FROM trips WHERE published = 1 AND start_date > CURDATE() ORDER BY start_date ASC";
}
$result = $conn->query($sql); $result = $conn->query($sql);
if ($result->num_rows > 0) { if ($result->num_rows > 0) {
@@ -68,16 +81,18 @@ include_once($rootPath . '/header.php');
$capacity = $row['vehicle_capacity']; $capacity = $row['vehicle_capacity'];
$cost_members = $row['cost_members']; $cost_members = $row['cost_members'];
$places_booked = $row['places_booked']; $places_booked = $row['places_booked'];
$published = $row['published'] ?? 1;
$remaining_places = getAvailableSpaces($trip_id); $remaining_places = getAvailableSpaces($trip_id);
// Determine the badge text based on the status // Determine the badge text based on the status
$badge_text = ($remaining_places > 0) ? $remaining_places.' PLACES LEFT!!' : 'FULLY BOOKED'; $badge_text = ($remaining_places > 0) ? $remaining_places.' PLACES LEFT!!' : 'FULLY BOOKED';
$draft_badge = ($published == 0) ? '<span class="badge bg-warning ms-2">DRAFT</span>' : '';
// Output the HTML structure with dynamic data // Output the HTML structure with dynamic data
echo ' echo '
<div class="destination-item style-three bgc-lighter" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50"> <div class="destination-item style-three bgc-lighter" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<div class="image"> <div class="image">
<span class="badge bgc-pink">' . $badge_text . '</span> <span class="badge bgc-pink">' . $badge_text . '</span>' . $draft_badge . '
<img src="assets/images/trips/' . $trip_id . '_01.jpg" alt="' . $trip_name . '"> <img src="assets/images/trips/' . $trip_id . '_01.jpg" alt="' . $trip_name . '">
</div> </div>
<div class="content"> <div class="content">
@@ -91,7 +106,7 @@ include_once($rootPath . '/header.php');
<i class="fas fa-star"></i> <i class="fas fa-star"></i>
</div> </div>
</div> </div>
<h5><a href="trip-details.php?token=' . encryptData($trip_id, $salt) . '">' . $trip_name . '</a></h5> <h5><a href="trip-details?token=' . encryptData($trip_id, $salt) . '">' . $trip_name . '</a></h5>
<p>' . $short_description . '</p> <p>' . $short_description . '</p>
<ul class="blog-meta"> <ul class="blog-meta">
<li><i class="far fa-calendar"></i> ' . convertDate($start_date) . ' - ' . convertDate($end_date) . '</li> <li><i class="far fa-calendar"></i> ' . convertDate($start_date) . ' - ' . convertDate($end_date) . '</li>
@@ -100,7 +115,7 @@ include_once($rootPath . '/header.php');
</ul> </ul>
<div class="destination-footer"> <div class="destination-footer">
<span class="price"><span>R ' . $cost_members . '</span>/person</span> <span class="price"><span>R ' . $cost_members . '</span>/person</span>
<a href="trip-details.php?token=' . encryptData($trip_id, $salt) . '" class="theme-btn style-two style-three"> <a href="trip-details?token=' . encryptData($trip_id, $salt) . '" class="theme-btn style-two style-three">
<span data-hover="Book Now">Book Now</span> <span data-hover="Book Now">Book Now</span>
<i class="fal fa-arrow-right"></i> <i class="fal fa-arrow-right"></i>
</a> </a>

View File

@@ -168,24 +168,22 @@ $user = $result->fetch_assoc();
data: formData, data: formData,
contentType: false, contentType: false,
processData: false, processData: false,
dataType: 'json',
success: function(response) { success: function(response) {
// Parse response if needed
if (typeof response === "string") {
response = JSON.parse(response);
}
if (response.status === 'success') { if (response.status === 'success') {
// Update the profile picture source with cache-busting query string
// Reload the current page
window.location.reload();
$('#responseMessage').html('<div class="alert alert-success">' + response.message + '</div>'); $('#responseMessage').html('<div class="alert alert-success">' + response.message + '</div>');
// Reload the current page after a short delay
setTimeout(function() {
window.location.reload();
}, 1500);
} else { } else {
$('#responseMessage').html('<div class="alert alert-danger">' + response.message + '</div>'); $('#responseMessage').html('<div class="alert alert-danger">' + response.message + '</div>');
} }
}, },
error: function() { error: function(xhr, status, error) {
$('#responseMessage').html('<div class="alert alert-danger">Error uploading profile picture.</div>'); console.log('AJAX Error:', status, error);
console.log('Response Text:', xhr.responseText);
$('#responseMessage').html('<div class="alert alert-danger">Error uploading profile picture: ' + error + '</div>');
} }
}); });
}); });

View File

@@ -0,0 +1,60 @@
<?php
ob_start();
header('Content-Type: application/json');
$rootPath = dirname(dirname(__DIR__));
require_once($rootPath . "/src/config/env.php");
require_once($rootPath . '/src/config/functions.php');
require_once($rootPath . '/src/config/connection.php');
// Check admin status
session_start();
if (empty($_SESSION['user_id'])) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
$user_role = getUserRole();
if (!in_array($user_role, ['admin', 'superadmin'])) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
try {
$trip_id = intval($_POST['trip_id'] ?? 0);
if ($trip_id <= 0) {
throw new Exception('Invalid trip ID');
}
// Delete trip images from filesystem
$upload_dir = $rootPath . '/assets/images/trips/';
if (is_dir($upload_dir)) {
$files = glob($upload_dir . $trip_id . '_*.{jpg,jpeg,png,gif,webp}', GLOB_BRACE);
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
}
// Delete trip from database
$stmt = $conn->prepare("DELETE FROM trips WHERE trip_id = ?");
$stmt->bind_param("i", $trip_id);
if (!$stmt->execute()) {
throw new Exception('Failed to delete trip: ' . $stmt->error);
}
$stmt->close();
ob_end_clean();
echo json_encode(['status' => 'success', 'message' => 'Trip deleted successfully']);
} catch (Exception $e) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
?>

View File

@@ -1,4 +1,10 @@
<?php <?php
ob_start(); // Start output buffering
session_start();
// Set JSON response header BEFORE any other output
header('Content-Type: application/json');
$rootPath = dirname(dirname(__DIR__)); $rootPath = dirname(dirname(__DIR__));
require_once($rootPath . "/src/config/env.php"); require_once($rootPath . "/src/config/env.php");
require_once($rootPath . "/src/config/session.php"); require_once($rootPath . "/src/config/session.php");
@@ -6,6 +12,7 @@ require_once($rootPath . "/src/config/connection.php");
require_once($rootPath . "/src/config/functions.php"); require_once($rootPath . "/src/config/functions.php");
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
ob_end_clean();
die(json_encode(['status' => 'error', 'message' => 'User not logged in'])); die(json_encode(['status' => 'error', 'message' => 'User not logged in']));
} }
@@ -26,11 +33,11 @@ if (isset($_POST['signature'])) {
// Create a file path for the signature image // Create a file path for the signature image
$fileName = 'signature_' . $user_id . '.png'; $fileName = 'signature_' . $user_id . '.png';
$filePath = $rootPath . '/src/processors/uploads/signatures/' . $fileName; $filePath = $rootPath . '/uploads/signatures/' . $fileName;
// Ensure the directory exists // Ensure the directory exists
if (!is_dir($rootPath . '/src/processors/uploads/signatures')) { if (!is_dir($rootPath . '/uploads/signatures')) {
mkdir($rootPath . '/src/processors/uploads/signatures', 0777, true); mkdir($rootPath . '/uploads/signatures', 0777, true);
} }
// Save the image file // Save the image file
@@ -42,7 +49,7 @@ if (isset($_POST['signature'])) {
} }
// Store relative path for HTML display // Store relative path for HTML display
$display_path = 'src/processors/uploads/signatures/' . $fileName; $display_path = '/uploads/signatures/' . $fileName;
// Update the signature and indemnity acceptance in the membership application table // Update the signature and indemnity acceptance in the membership application table
$stmt = $conn->prepare("UPDATE membership_application SET sig = ?, accept_indemnity = 1 WHERE user_id = ?"); $stmt = $conn->prepare("UPDATE membership_application SET sig = ?, accept_indemnity = 1 WHERE user_id = ?");
@@ -53,21 +60,25 @@ if (isset($_POST['signature'])) {
$paymentStatus = checkMembershipPaymentStatus($user_id) ? 'PAID' : 'NOT_PAID'; $paymentStatus = checkMembershipPaymentStatus($user_id) ? 'PAID' : 'NOT_PAID';
// Respond with the appropriate redirect URL based on the payment status // Respond with the appropriate redirect URL based on the payment status
ob_end_clean();
echo json_encode([ echo json_encode([
'status' => 'success', 'status' => 'success',
'message' => 'Signature saved successfully!', 'message' => 'Signature saved successfully!',
'paymentStatus' => $paymentStatus // Send payment status 'paymentStatus' => $paymentStatus // Send payment status
]); ]);
} else { } else {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Database update failed']); echo json_encode(['status' => 'error', 'message' => 'Database update failed']);
} }
$stmt->close(); $stmt->close();
$conn->close(); $conn->close();
} else { } else {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Failed to save signature']); echo json_encode(['status' => 'error', 'message' => 'Failed to save signature']);
} }
} else { } else {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Signature not provided']); echo json_encode(['status' => 'error', 'message' => 'Signature not provided']);
} }

View File

@@ -0,0 +1,188 @@
<?php
ob_start();
header('Content-Type: application/json');
$rootPath = dirname(dirname(__DIR__));
require_once($rootPath . "/src/config/env.php");
require_once($rootPath . '/src/config/functions.php');
require_once($rootPath . '/src/config/connection.php');
// Check admin status
session_start();
if (empty($_SESSION['user_id'])) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
$user_role = getUserRole();
if (!in_array($user_role, ['admin', 'superadmin'])) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
// // Validate CSRF token
// if (empty($_POST['csrf_token']) || $_POST['csrf_token'] !== ($_SESSION['csrf_token'] ?? '')) {
// ob_end_clean();
// echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']);
// exit;
// }
try {
$trip_id = $_POST['trip_id'] ?? null;
$trip_name = trim($_POST['trip_name'] ?? '');
$location = trim($_POST['location'] ?? '');
$trip_code = trim($_POST['trip_code'] ?? '');
$vehicle_capacity = intval($_POST['vehicle_capacity'] ?? 0);
$start_date = trim($_POST['start_date'] ?? '');
$end_date = trim($_POST['end_date'] ?? '');
$short_description = trim($_POST['short_description'] ?? '');
$long_description = trim($_POST['long_description'] ?? '');
$cost_members = floatval($_POST['cost_members'] ?? 0);
$cost_nonmembers = floatval($_POST['cost_nonmembers'] ?? 0);
$cost_pensioner_member = floatval($_POST['cost_pensioner_member'] ?? 0);
$cost_pensioner = floatval($_POST['cost_pensioner'] ?? 0);
$booking_fee = floatval($_POST['booking_fee'] ?? 0);
// Debug: Log received values
// error_log("START_DATE: " . var_export($start_date, true), 3, $rootPath . "/logs/trip_debug.log");
// error_log("END_DATE: " . var_export($end_date, true), 3, $rootPath . "/logs/trip_debug.log");
// Validation
if (empty($trip_name) || empty($location) || empty($start_date) || empty($end_date)) {
throw new Exception('Required fields are missing');
}
// Validate and format dates (expecting YYYY-MM-DD format from HTML5 date input)
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $start_date)) {
throw new Exception('Start date format invalid: "' . $start_date . '" must be in YYYY-MM-DD format');
}
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $end_date)) {
throw new Exception('End date format invalid: "' . $end_date . '" must be in YYYY-MM-DD format');
}
// Validate dates are actual dates
$start_timestamp = strtotime($start_date);
$end_timestamp = strtotime($end_date);
if ($start_timestamp === false) {
throw new Exception('Invalid start date');
}
if ($end_timestamp === false) {
throw new Exception('Invalid end date');
}
if ($vehicle_capacity <= 0) {
throw new Exception('Vehicle capacity must be greater than 0');
}
if ($start_timestamp >= $end_timestamp) {
throw new Exception('Start date must be before end date');
}
// If creating new trip, insert first to get trip_id
if (!$trip_id) {
$stmt = $conn->prepare("
INSERT INTO trips (
trip_name, location, trip_code, vehicle_capacity, start_date, end_date,
short_description, long_description, cost_members, cost_nonmembers,
cost_pensioner_member, cost_pensioner, booking_fee, published, places_booked
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0)
");
$stmt->bind_param(
"sssissssddddd",
$trip_name, $location, $trip_code, $vehicle_capacity,
$start_date, $end_date, $short_description, $long_description,
$cost_members, $cost_nonmembers, $cost_pensioner_member,
$cost_pensioner, $booking_fee
);
if (!$stmt->execute()) {
throw new Exception('Failed to create trip: ' . $stmt->error);
}
$trip_id = $conn->insert_id;
$stmt->close();
} else {
// Update existing trip
$stmt = $conn->prepare("
UPDATE trips SET
trip_name = ?, location = ?, trip_code = ?, vehicle_capacity = ?,
start_date = ?, end_date = ?, short_description = ?, long_description = ?,
cost_members = ?, cost_nonmembers = ?, cost_pensioner_member = ?, cost_pensioner = ?,
booking_fee = ?
WHERE trip_id = ?
");
$stmt->bind_param(
"sssissssdddddi",
$trip_name, $location, $trip_code, $vehicle_capacity,
$start_date, $end_date, $short_description, $long_description,
$cost_members, $cost_nonmembers, $cost_pensioner_member, $cost_pensioner,
$booking_fee,
$trip_id
);
if (!$stmt->execute()) {
throw new Exception('Failed to update trip: ' . $stmt->error);
}
$stmt->close();
}
// Handle image uploads
if (!empty($_FILES['trip_images']['name'][0])) {
$upload_dir = $rootPath . '/assets/images/trips/';
// Create directory if it doesn't exist
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$image_count = 1;
foreach ($_FILES['trip_images']['name'] as $key => $filename) {
if (empty($filename)) continue;
$file_ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
// Validate file extension
if (!in_array($file_ext, $allowed_extensions)) {
throw new Exception('Invalid file type: ' . $filename . '. Only images allowed.');
}
// Validate file size (5MB max per file)
if ($_FILES['trip_images']['size'][$key] > 5 * 1024 * 1024) {
throw new Exception('File too large: ' . $filename . '. Max 5MB per file.');
}
// Generate filename: {trip_id}_0{number}.{ext}
$new_filename = $trip_id . '_0' . $image_count . '.' . $file_ext;
$file_path = $upload_dir . $new_filename;
// Move uploaded file
if (!move_uploaded_file($_FILES['trip_images']['tmp_name'][$key], $file_path)) {
throw new Exception('Failed to upload image: ' . $filename);
}
// Optimize image (resize if too large)
// optimizeImage($file_path, 1920, 1080);
$image_count++;
}
}
ob_end_clean();
echo json_encode([
'status' => 'success',
'message' => $trip_id ? 'Trip updated successfully' : 'Trip created successfully',
'trip_id' => $trip_id
]);
} catch (Exception $e) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
?>

View File

@@ -132,6 +132,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
} else { } else {
addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description); 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); sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
header("Location: payment_confirmation?token=".encryptData($booking_id, $salt)); header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
exit(); // Ensure no further code is executed after the redirect exit(); // Ensure no further code is executed after the redirect

View File

@@ -1,7 +1,10 @@
<?php <?php
ob_start(); // Start output buffering to allow headers before output
$headerStyle = 'light'; $headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__)); $rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php'); require_once($rootPath . "/src/config/env.php");
require_once($rootPath . "/src/config/session.php");
include_once($rootPath . '/src/config/connection.php');
require_once($rootPath . "/src/config/functions.php"); require_once($rootPath . "/src/config/functions.php");
checkUserSession(); checkUserSession();
@@ -11,7 +14,8 @@ if (!$user_id) {
die("Not logged in."); die("Not logged in.");
} }
// Handle POST submission // Handle POST submission BEFORE including header
$redirect_url = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF Token Validation // CSRF Token Validation
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
@@ -34,9 +38,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit; exit;
} }
$target_dir = $rootPath . "/src/processors/uploads/pop/"; $target_dir = $rootPath . "/uploads/pop/";
$randomFilename = $validationResult['filename']; // Use EFT ID as filename instead of random filename, replace spaces with underscores
$target_file = $target_dir . $randomFilename; $filename = str_replace(' ', '_', $eft_id) . '.pdf';
$target_file = $target_dir . $filename;
// Make sure target directory exists and writable // Make sure target directory exists and writable
if (!is_dir($target_dir)) { if (!is_dir($target_dir)) {
@@ -91,15 +96,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$description = "Payment"; $description = "Payment";
} }
if (sendPOP($fullname, $randomFilename, $amount, $description)) { if (sendPOP($fullname, $filename, $amount, $description)) {
$_SESSION['message'] = "Thank you! Your payment proof has been uploaded and notification sent."; $_SESSION['message'] = "Thank you! Your payment proof has been uploaded and notification sent.";
} else { } else {
$_SESSION['message'] = "Payment uploaded, but notification email could not be sent."; $_SESSION['message'] = "Payment uploaded, but notification email could not be sent.";
} }
// Log the action // Log the action
auditLog($user_id, 'POP_UPLOAD', 'efts', $eft_id, ['filename' => $randomFilename, 'payment_type' => $payment_type]); auditLog($user_id, 'POP_UPLOAD', 'efts', $eft_id, ['filename' => $filename, 'payment_type' => $payment_type]);
$redirect_url = 'bookings';
ob_end_clean();
header("Location: bookings"); header("Location: bookings");
exit; exit;
@@ -109,6 +116,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
} }
// Now that POST is handled, include header for display
include_once($rootPath . '/header.php');
// Fetch bookings for dropdown // Fetch bookings for dropdown
$stmt = $conn->prepare(" $stmt = $conn->prepare("

View File

@@ -0,0 +1,67 @@
<?php
ob_start();
header('Content-Type: application/json');
$rootPath = dirname(dirname(__DIR__));
require_once($rootPath . "/src/config/env.php");
require_once($rootPath . '/src/config/functions.php');
require_once($rootPath . '/src/config/connection.php');
// Check admin status
session_start();
if (empty($_SESSION['user_id'])) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
$user_role = getUserRole();
if (!in_array($user_role, ['admin', 'superadmin'])) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
try {
$trip_id = intval($_POST['trip_id'] ?? 0);
if ($trip_id <= 0) {
throw new Exception('Invalid trip ID');
}
// Fetch current published status
$stmt = $conn->prepare("SELECT published FROM trips WHERE trip_id = ?");
$stmt->bind_param("i", $trip_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
throw new Exception('Trip not found');
}
$row = $result->fetch_assoc();
$new_status = $row['published'] == 1 ? 0 : 1;
$stmt->close();
// Update published status
$stmt = $conn->prepare("UPDATE trips SET published = ? WHERE trip_id = ?");
$stmt->bind_param("ii", $new_status, $trip_id);
if (!$stmt->execute()) {
throw new Exception('Failed to update trip status: ' . $stmt->error);
}
$stmt->close();
ob_end_clean();
echo json_encode([
'status' => 'success',
'message' => $new_status == 1 ? 'Trip published successfully' : 'Trip unpublished successfully',
'published' => $new_status
]);
} catch (Exception $e) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
?>

View File

@@ -1,9 +1,20 @@
<?php <?php
ob_start(); // Start output buffering
session_start(); session_start();
// Set JSON response header BEFORE any other output
header('Content-Type: application/json');
$rootPath = dirname(dirname(__DIR__)); $rootPath = dirname(dirname(__DIR__));
require_once($rootPath . "/src/config/env.php");
require_once($rootPath . "/src/config/session.php");
include_once($rootPath . '/src/config/connection.php'); include_once($rootPath . '/src/config/connection.php');
require_once($rootPath . "/src/config/functions.php"); require_once($rootPath . "/src/config/functions.php");
require_once($rootPath . "/src/config/env.php");
// Check database connection
if (!isset($conn) || $conn === null) {
die(json_encode(['status' => 'error', 'message' => 'Database connection failed']));
}
$response = array('status' => 'error', 'message' => 'Something went wrong'); $response = array('status' => 'error', 'message' => 'Something went wrong');
@@ -69,16 +80,18 @@ if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] !=
// Log the action // Log the action
auditLog($user_id, 'PROFILE_PIC_UPLOAD', 'users', $user_id, ['filename' => $randomFilename]); auditLog($user_id, 'PROFILE_PIC_UPLOAD', 'users', $user_id, ['filename' => $randomFilename]);
} else { } else {
$response['message'] = 'Failed to update profile picture in the database'; $response['message'] = 'Failed to update profile picture in the database: ' . $stmt->error;
} }
$stmt->close(); $stmt->close();
} else { } else {
$response['message'] = 'Failed to move uploaded file.'; $response['message'] = 'Failed to move uploaded file. Error code: ' . $_FILES['profile_picture']['error'];
} }
} else { } else {
$response['message'] = 'No file uploaded or file error.'; $response['message'] = 'No file uploaded or file error.';
} }
// Clean output buffer and send only JSON
ob_end_clean();
echo json_encode($response); echo json_encode($response);
?> ?>