Update site: auth fixes, new admin pages, payment flow, membership renewal, helpers

This commit is contained in:
twotalesanimation
2026-06-11 10:01:48 +02:00
parent 32e50ffc39
commit bc8d923221
56 changed files with 5155 additions and 534 deletions
+441
View File
@@ -0,0 +1,441 @@
<?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
// Ensure $conn is available; show a friendly message if DB is down
$conn = $conn ?? null;
if (!$conn) {
// Render a simple error notice inside the page layout and stop further DB queries
echo '<section class="tour-list-page py-10 rel z-1">\n <div class="container">\n <div class="alert alert-danger">Database connection unavailable. Please check your configuration and logs.</div>\n </div>\n</section>';
include_once($rootPath . '/components/insta_footer.php');
// Stop execution to prevent subsequent fatal errors from using $conn
exit;
}
?>
<style>
/* Shared table and tab styles */
.admin-tabs { text-align: center; display: flex; margin-bottom: 20px; margin-top: 20px; }
.admin-tab { padding: 10px 30px; margin-right: 20px; cursor: pointer; border: 2px solid; border-radius: 50px; background: none; font-size: 18px; color: #484848; }
.admin-tab.active { color: white; background: #63ab45; font-weight: bold; }
.tab-content { display: none; }
.tab-content.active { display: block; }
table { width: 100%; border-collapse: separate; border-spacing: 0; margin: 10px 0; }
thead th { cursor: pointer; text-align: left; padding: 10px; font-weight: bold; position: relative; }
thead th::after { content: '\25B2'; font-size: 0.8em; position: absolute; right: 10px; opacity: 0; transition: opacity 0.2s; }
thead th.asc::after { content: '\25B2'; opacity: 1; }
thead th.desc::after { content: '\25BC'; opacity: 1; }
tbody tr:nth-child(odd) { background-color: transparent; }
tbody tr:nth-child(even) { background-color: rgb(255, 255, 255); border-radius: 10px; }
tbody td { padding: 5px; }
tbody tr:nth-child(even) td:first-child { border-top-left-radius: 10px; border-bottom-left-radius: 10px; }
tbody tr:nth-child(even) td:last-child { border-top-right-radius: 10px; border-bottom-right-radius: 10px; }
.filter-input { width: 100%; padding: 5px; font-size: 16px; background-color: rgb(255, 255, 255); border-radius: 25px; }
.trip-booking { color: #484848; background: #f9f9f7; border: 1px solid #d8d8d8; border-radius: 10px; margin-top: 15px; margin-bottom: 15px; }
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Tab switching
const tabs = document.querySelectorAll('.admin-tab');
const contents = document.querySelectorAll('.tab-content');
tabs.forEach((tab, idx) => {
tab.addEventListener('click', function() {
tabs.forEach(t => t.classList.remove('active'));
contents.forEach(c => c.classList.remove('active'));
tab.classList.add('active');
contents[idx].classList.add('active');
});
});
// Table sorting/filtering (same as before)
document.querySelectorAll("table").forEach((table) => {
const headers = table.querySelectorAll("thead th");
const rows = Array.from(table.querySelectorAll("tbody tr"));
const filterInput = table.previousElementSibling;
headers.forEach((header, index) => {
header.addEventListener("click", () => {
const sortedRows = rows.sort((a, b) => {
const aText = a.cells[index].textContent.trim().toLowerCase();
const bText = b.cells[index].textContent.trim().toLowerCase();
if (aText < bText) return -1;
if (aText > bText) return 1;
return 0;
});
if (header.classList.contains("asc")) {
header.classList.remove("asc");
header.classList.add("desc");
sortedRows.reverse();
} else {
headers.forEach(h => h.classList.remove("asc", "desc"));
header.classList.add("asc");
}
const tbody = table.querySelector("tbody");
tbody.innerHTML = "";
sortedRows.forEach(row => tbody.appendChild(row));
});
});
if (rows.length === 0) {
filterInput.style.display = "none";
} else {
filterInput.addEventListener("input", function() {
const filterValue = filterInput.value.trim().toLowerCase();
rows.forEach(row => {
const rowText = row.textContent.trim().toLowerCase();
row.style.display = rowText.includes(filterValue) ? "" : "none";
});
});
}
});
});
</script>
<?php
$pageTitle = 'Manage Trips, Courses & Events';
$breadcrumbs = [['Home' => 'index']];
require_once($rootPath . '/components/banner.php');
?>
<section class="tour-list-page py-10 rel z-1">
<div class="container">
<div class="admin-tabs">
<button class="admin-tab active">Manage Trips</button>
<button class="admin-tab">Manage Events</button>
<button class="admin-tab">Manage Courses</button>
<button class="admin-tab">Manage Blogs</button>
</div>
<div class="tab-content active">
<?php
// Trips management content (adapted from admin_trips.php)
// Fetch trips (already available if connection present)
$trips_query = "
SELECT
trip_id, trip_name, location, start_date, end_date,
vehicle_capacity, places_booked, cost_members, cost_nonmembers,
cost_pensioner_member, cost_pensioner, published
FROM trips
ORDER BY start_date DESC
";
$result = $conn->query($trips_query);
$trips = [];
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$trips[] = $row;
}
}
?>
<div class="row">
<div class="col-lg-12">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
<h2 style="margin: 0;">Manage Trips</h2>
<a href="manage_trips" class="theme-btn create-album-btn">
<i class="far fa-plus"></i> New Trip
</a>
</div>
<?php if (isset($_SESSION['message'])): ?>
<div class="alert alert-warning message-box">
<?php echo $_SESSION['message']; ?>
<span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</span>
</div>
<?php unset($_SESSION['message']);
endif;
if (count($trips) > 0) {
echo '<input type="text" class="filter-input" placeholder="Filter trips...">';
echo '<div class="trips-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
foreach ($trips as $trip) {
$available = $trip['vehicle_capacity'] - $trip['places_booked'];
$publishStatusBadge = $trip['published'] == 1 ? 'PUBLISHED' : 'DRAFT';
$tripImagePath = '';
$tripImagesGlob = glob($rootPath . '/assets/images/trips/' . $trip['trip_id'] . '_*.jpg');
if (!empty($tripImagesGlob)) {
$tripImagePath = str_replace($rootPath, '', $tripImagesGlob[0]);
} else {
$tripImagePath = 'assets/images/placeholder.jpg';
}
echo '
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<div class="image" style="width:300px;height:250px;">
<img src="' . htmlspecialchars($tripImagePath) . '" alt="' . htmlspecialchars($trip['trip_name']) . '">
</div>
<div class="content" style="width:100%;">
<div class="destination-header d-flex align-items-start gap-3">
<div>
<span class="badge bg-dark mb-1">' . strtoupper($publishStatusBadge) . '</span>
<h5 class="mb-0">' . htmlspecialchars($trip['trip_name']) . '</h5>
<small class="text-muted">📍 ' . htmlspecialchars($trip['location']) . '</small>
</div>
</div>
<p style="margin: 10px 0;">
<strong>Dates:</strong> ' . date('M d', strtotime($trip['start_date'])) . ' - ' . date('M d, Y', strtotime($trip['end_date'])) . '<br>
<strong>Capacity:</strong> ' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '<br>
<strong>Costs:</strong> Members: R ' . number_format($trip['cost_members'], 2) . ' | Non-Members: R ' . number_format($trip['cost_nonmembers'], 2) . ' | Pensioner Members: R ' . number_format($trip['cost_pensioner_member'], 2) . ' | Pensioners: R ' . number_format($trip['cost_pensioner'], 2) . '
</p>
<div class="destination-footer">
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
<a href="manage_trips?trip_id=' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
<button type="button" class="toggle-publish" data-trip-id="' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($trip['published'] == 1 ? 'Unpublish' : 'Publish') . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($trip['published'] == 1 ? 'cloud_off' : 'cloud_upload') . '</span></button>
<button type="button" class="delete-trip" data-trip-id="' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
</div>
</div>
</div>
</div>
';
}
echo '</div>';
} else {
echo "<div class=\"no-trips\"><p>No trips found. <a href=\"manage_trips\">Create one</a></p></div>";
}
?>
</div>
</div>
</div>
<div class="tab-content">
<?php
// Events management content (adapted from admin_events.php)
$events_query = "
SELECT
event_id, name, type, location, date, image, published
FROM events
ORDER BY date DESC
";
$result = $conn->query($events_query);
$events = [];
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$events[] = $row;
}
}
?>
<div class="row">
<div class="col-lg-12">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
<h2 style="margin: 0;">Manage Events</h2>
<a href="manage_events" class="theme-btn create-album-btn">
<i class="far fa-plus"></i> New Event
</a>
</div>
<?php if (count($events) > 0) {
echo '<input type="text" class="filter-input" placeholder="Filter events...">';
echo '<div class="events-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
foreach ($events as $event) {
$eventImagePath = $event['image'] ? htmlspecialchars($event['image']) : 'assets/images/placeholder.jpg';
$publishStatusBadge = $event['published'] == 1 ? 'PUBLISHED' : 'DRAFT';
echo '
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<div class="image" style="width:300px;height:250px;">
<img src="' . $eventImagePath . '" alt="' . htmlspecialchars($event['name']) . '">
</div>
<div class="content" style="width:100%;">
<div class="destination-header d-flex align-items-start gap-3">
<div>
<span class="badge bg-dark mb-1">' . strtoupper($publishStatusBadge) . '</span>
<h5 class="mb-0">' . htmlspecialchars($event['name']) . '</h5>
<small class="text-muted">📍 ' . htmlspecialchars($event['location']) . '</small>
</div>
</div>
<p style="margin: 10px 0;">
<strong>Type:</strong> ' . htmlspecialchars($event['type']) . '<br>
<strong>Date:</strong> ' . convertDate($event['date']) . '
</p>
<div class="destination-footer">
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
<a href="manage_events?event_id=' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
<button type="button" class="toggle-publish" data-event-id="' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($event['published'] == 1 ? 'Unpublish' : 'Publish') . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($event['published'] == 1 ? 'cloud_off' : 'cloud_upload') . '</span></button>
<button type="button" class="delete-event" data-event-id="' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
</div>
</div>
</div>
</div>
';
}
echo '</div>';
} else {
echo '<div class="no-events"><p>No events found. <a href="manage_events">Create one</a></p></div>';
}
?>
</div>
</div>
</div>
<div class="tab-content">
<?php
// Courses management content (adapted from admin_courses.php)
$courses_query = "
SELECT
course_id, course_type, date, capacity, booked, cost_members, cost_nonmembers, instructor, instructor_email, code
FROM courses
ORDER BY date DESC
";
$result = $conn->query($courses_query);
$courses = [];
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$courses[] = $row;
}
}
?>
<div class="row">
<div class="col-lg-12">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
<h2 style="margin: 0;">Manage Courses</h2>
<a href="manage_courses" class="theme-btn create-album-btn">
<i class="far fa-plus"></i> New Course
</a>
</div>
<?php if (isset($_SESSION['message'])): ?>
<div class="alert alert-warning message-box">
<?php echo $_SESSION['message']; ?>
<span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</span>
</div>
<?php unset($_SESSION['message']);
endif; ?>
<?php if (count($courses) > 0): ?>
<input type="text" class="filter-input" placeholder="Filter courses...">
<div class="courses-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<?php foreach ($courses as $course):
$available = intval($course['capacity']) - intval($course['booked']);
$type_label = strtoupper($course['course_type']);
if ($course['course_type'] == 'driver_training') {
$type_label = 'Driver Training';
} elseif ($course['course_type'] == 'bush_mechanics') {
$type_label = 'Bush Mechanics';
} elseif ($course['course_type'] == 'rescue_recovery') {
$type_label = 'Rescue & Recovery';
} elseif ($course['course_type'] == 'ladies_driver_training') {
$type_label = 'Ladies Driver Training';
}
?>
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<div class="content" style="width:100%;">
<div class="destination-header d-flex align-items-start gap-3">
<div>
<h5 class="mb-0"><?php echo $type_label; ?></h5>
<small class="text-muted"><?php echo htmlspecialchars($course['course_type']); ?> — <?php echo date('M d, Y', strtotime($course['date'])); ?></small><br>
<small class="text-muted"><?php echo $course['code'] ? 'Code: ' . htmlspecialchars($course['code']) : ''; ?></small>
</div>
</div>
<p style="margin: 10px 0;">
<strong>Instructor:</strong> <?php echo htmlspecialchars($course['instructor']); ?> (<?php echo htmlspecialchars($course['instructor_email']); ?>)<br>
<strong>Capacity:</strong> <?php echo intval($course['booked']); ?> / <?php echo intval($course['capacity']); ?> &nbsp; <strong>Available:</strong> <?php echo $available; ?><br>
<strong>Costs:</strong> Members: R <?php echo number_format($course['cost_members'],2); ?> | Non-Members: R <?php echo number_format($course['cost_nonmembers'],2); ?>
</p>
<div class="destination-footer">
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
<a href="manage_courses?course_id=<?php echo $course['course_id']; ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
<button type="button" class="delete-course" data-course-id="<?php echo $course['course_id']; ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="no-courses">
<p>No courses found. <a href="manage_courses">Create one</a></p>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="tab-content">
<?php
// Blogs management content (adapted from src/admin/admin_blogs.php)
$posts = [];
if ($conn) {
$stmt = $conn->prepare("SELECT b.blog_id, b.title, b.description, b.status, b.date, b.image, CONCAT(u.first_name, ' ', u.last_name) AS author_name, u.email AS author_email, u.profile_pic FROM blogs b JOIN users u ON b.author = u.user_id WHERE b.status = 'published' ORDER BY b.date DESC");
if ($stmt) {
$stmt->execute();
$res = $stmt->get_result();
if ($res && $res->num_rows > 0) {
while ($r = $res->fetch_assoc()) $posts[] = $r;
}
}
}
?>
<div class="row">
<div class="col-lg-12">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:30px;">
<h2 style="margin:0;">Manage Blogs</h2>
<a href="blog_create" class="theme-btn create-album-btn"><i class="far fa-plus"></i> New Blog</a>
</div>
<?php if (count($posts) > 0): ?>
<input type="text" class="filter-input" placeholder="Filter blogs...">
<div class="blogs-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<?php foreach ($posts as $post):
$coverImage = $post['image'] ? $post['image'] : 'assets/images/placeholder.jpg';
?>
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<div class="image" style="width:200px;height:200px;">
<img src="<?php echo htmlspecialchars($coverImage); ?>" alt="<?php echo htmlspecialchars($post['title']); ?>">
</div>
<div class="content" style="width:100%;">
<div class="destination-header d-flex align-items-start gap-3">
<img src="<?php echo htmlspecialchars($post['profile_pic'] ?? 'assets/images/placeholder.jpg'); ?>" alt="Author" class="rounded-circle border" width="80" height="80">
<div>
<span class="badge bg-dark mb-1"><?php echo $post['status'] == 1 || $post['status'] === 'published' ? 'PUBLISHED' : 'DRAFT'; ?></span>
<h5 class="mb-0"><?php echo htmlspecialchars($post['title']); ?></h5>
<small class="text-muted"><?php echo htmlspecialchars($post['author_name']); ?></small>
</div>
</div>
<p><?php echo htmlspecialchars($post['description']); ?></p>
<div class="destination-footer">
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
<a href="blog_edit.php?token=<?php echo encryptData($post['blog_id'], $salt); ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
<a href="blog_read.php?token=<?php echo encryptData($post['blog_id'], $salt); ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Preview"><span class="material-icons">visibility</span></a>
<button type="button" class="publish-btn" data-blog-id="<?php echo $post['blog_id']; ?>" data-status="<?php echo htmlspecialchars($post['status']); ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Publish/Unpublish" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons"><?php echo ($post['status'] == 1 || $post['status'] === 'published') ? 'cloud_off' : 'cloud_upload'; ?></span></button>
<a href="blog_delete.php?token=<?php echo encryptData($post['blog_id'], $salt); ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete"><span class="material-icons">delete</span></a>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="no-blogs"><p>No blogs found. <a href="manage_blogs">Create one</a></p></div>
<?php endif; ?>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const filterInput = document.querySelector('.filter-input');
const cards = document.querySelectorAll('.destination-item');
if (cards.length === 0 && filterInput) filterInput.style.display = "none";
else if (filterInput) {
filterInput.addEventListener('input', function() {
const filterValue = filterInput.value.trim().toLowerCase();
cards.forEach(card => {
const cardText = card.textContent.trim().toLowerCase();
card.style.display = cardText.includes(filterValue) ? "" : "none";
});
});
}
// Tooltips
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
// Publish/unpublish
document.querySelectorAll('.publish-btn').forEach(btn => {
btn.addEventListener('click', function() {
const blogId = this.dataset.blogId;
const status = this.dataset.status;
const endpoint = (status == 1 || status === 'published') ? 'blog_unpublish' : 'publish_blog';
const formData = new FormData(); formData.append('id', blogId);
fetch(endpoint, { method: 'POST', body: formData })
.then(r => r.json())
.then(data => { if (data.status === 'success') location.reload(); else alert('Action failed'); })
.catch(()=> alert('Network error'));
});
});
});
</script>
</div>
</div>
</div>
</section>
<?php include_once($rootPath . '/components/insta_footer.php'); ?>