19 Commits

Author SHA1 Message Date
twotalesanimation
32651ed433 fix: publish toggle error alert and event visibility
- Add proper error handling to toggle_event_published.php with HTTP status codes
- Add try-catch block for database operations in toggle endpoint
- Update events.php query to only show published events (added published = 1 filter)
- Add updated_at timestamp update when toggling publish status
- Improve error messages for better debugging
2025-12-04 21:56:57 +02:00
twotalesanimation
f522b84fc1 refactor: align events admin pages with trips layout and add publish functionality
- Remove checkbox from manage_events.php form (publish via admin table instead)
- Redesign admin_events.php to match admin_trips.php layout exactly
- Add table-based actions with icon buttons (Edit, Publish/Unpublish, Delete)
- Change button styling to match trips (btn classes with colors)
- Add publish/unpublish toggle button with eye icon
- Create toggle_event_published.php endpoint for publish status switching
- Create delete_event.php endpoint for event deletion
- Add AJAX functionality for instant publish/delete without page reload
- Update .htaccess with new endpoint rewrite rules
- Badge styling updated to match trips (bg-success, bg-warning)
- Consistent sorting and filtering functionality
2025-12-04 21:40:11 +02:00
twotalesanimation
2b136c4b06 feat: add events admin navigation links and URL rewrite rules
- Add 'Manage Events' link to admin dropdown menu in header
- Add URL rewrite rules for admin_events and manage_events pages
- Add process_event endpoint rewrite rule
- Events admin pages now accessible via clean URLs
2025-12-04 20:32:49 +02:00
twotalesanimation
7f0964009a docs: add events admin system documentation 2025-12-04 20:26:17 +02:00
twotalesanimation
5be946f78f feat: create events management admin system
- Add manage_events.php form for creating/editing events
- Add process_event.php endpoint for CRUD operations with image uploads
- Add admin_events.php list view with sorting, filtering, and delete functionality
- Add database migration to add created_by, published, created_at, updated_at columns to events table
- Add event images directory structure
- All features follow same patterns as trip management system
2025-12-04 20:25:48 +02:00
twotalesanimation
cb588d20ee Feature: Campsite management system with map, form, and province/country filtering 2025-12-04 20:15:14 +02:00
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
twotalesanimation
0c068eeb69 Fix: Use absolute paths for all upload directories in processor files
- upload_profile_picture.php: Use absolute path for profile picture uploads, store relative path in DB
- submit_pop.php: Use absolute path for proof of payment uploads
- process_signature.php: Use absolute path for signature uploads, store relative path in DB
2025-12-04 15:34:15 +02:00
47 changed files with 2649 additions and 115 deletions

View File

@@ -37,6 +37,7 @@ RewriteRule ^member_info$ src/pages/memberships/member_info.php [L]
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]
@@ -76,12 +77,15 @@ RewriteRule ^view_indemnity$ src/pages/other/view_indemnity.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]
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 ^add_campsite$ src/admin/add_campsite.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]
@@ -111,6 +115,12 @@ 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/admin/process_event.php [L]
RewriteRule ^toggle_trip_published$ src/processors/toggle_trip_published.php [L]
RewriteRule ^toggle_event_published$ src/admin/toggle_event_published.php [L]
RewriteRule ^delete_trip$ src/processors/delete_trip.php [L]
RewriteRule ^delete_event$ src/admin/delete_event.php [L]
</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

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

176
docs/EVENTS_ADMIN_SYSTEM.md Normal file
View File

@@ -0,0 +1,176 @@
# Events Management Admin System
## Overview
A complete admin system for managing events on the 4WDCSA website, following the same patterns as the trip management system.
## Files Created
### 1. `/src/admin/manage_events.php`
**Purpose**: Form for creating and editing events
**Features**:
- Create new events form
- Edit existing events form
- Fields:
- Event Name (required)
- Event Type (required) - e.g., Workshop, Training, Rally
- Location (required)
- Date (required)
- Time (required)
- Feature/Category (required) - e.g., Off-Road Training, Social Event
- Description (required) - Full text description
- Event Image (required for new, optional for updates)
- Promotional Image (optional) - Displayed when users click "View Promo"
- Published Status (checkbox) - Controls visibility on website
**Technical Details**:
- AJAX form submission to `process_event` endpoint
- Image upload with validation
- CSRF token protection
- Responsive Bootstrap grid layout (col-md-6 fields)
- Success/error message display with auto-redirect
### 2. `/src/admin/process_event.php`
**Purpose**: Backend endpoint for handling event CRUD operations
**Endpoints**:
- `POST /process_event` - Create/Update event
- `GET /process_event?action=delete&event_id={id}` - Delete event
**Features**:
- Create new events with image uploads
- Update existing events with optional image replacement
- Delete events and associated image files
- CSRF token validation
- Image type validation (JPEG, PNG, GIF, WebP)
- File organization in `/assets/images/events/`
- Automatic timestamp management (created_at, updated_at)
- User tracking (created_by stores admin user_id)
**Image Handling**:
- Main event image: Stored with unique ID prefix
- Promo image: Stored with `_promo_` prefix
- Both uploaded to `/assets/images/events/`
### 3. `/src/admin/admin_events.php`
**Purpose**: Admin dashboard for managing all events
**Features**:
- List all events with sortable columns
- Real-time search/filter across all columns
- Create new event button
- Edit event link for each row
- Delete event with confirmation dialog
- Status badges (Published/Draft)
- Responsive table with alternating row colors
- Rounded corners on even rows
**Sortable Columns**:
- Event Name
- Type
- Location
- Date
- Status
**Actions**:
- Edit - Redirects to manage_events.php with event_id
- Delete - Removes event and associated files
## Database Schema Changes
### Migration File: `/docs/migrations/001_add_events_tracking_columns.sql`
**Columns Added to events table**:
- `created_by` (int) - References user who created the event
- `published` (tinyint(1)) - Boolean flag for publication status (default 0/false)
- `created_at` (timestamp) - Automatic timestamp when event is created
- `updated_at` (timestamp) - Automatic timestamp updated on modification
**Indexes Added**:
- `idx_date` - For sorting and filtering by date
- `idx_published` - For filtering published/draft events
- `idx_created_by` - For tracking who created events
## Design Patterns
### Follows Trip Management System Architecture
- Same form layout and styling (`.comment-form.bgc-lighter`)
- Same table styling with sortable headers and filters
- Same image upload and validation patterns
- AJAX submission with success/error messaging
- Auto-redirect on successful operation
### Image Organization
```
/assets/images/events/
├── {unique_id}_{original_filename}.jpg (event images)
└── {unique_id}_promo_{original_filename}.jpg (promo images)
```
### Front-end Integration
The existing `/src/pages/events/events.php` displays published events:
- Shows event image, name, location, date, time
- Feature description and full description
- "View Promo" button displays promotional image in modal
## Usage Workflow
### Creating an Event
1. Navigate to `/src/admin/manage_events.php`
2. Fill in all required fields
3. Upload event image
4. Optionally upload promotional image
5. Check "Publish Event" if ready to display
6. Submit form via AJAX
7. Redirected to admin_events.php list view
### Editing an Event
1. Click "Edit" button on admin_events.php
2. Modify any fields
3. Image upload is optional - existing image retained if not changed
4. Update timestamps and user tracking automatic
5. Submit form
6. Redirected back to list view
### Deleting an Event
1. Click "Delete" button on admin_events.php
2. Confirm deletion in dialog
3. Event and associated image files removed from server
4. Page automatically refreshes
### Publishing/Unpublishing
- Toggle "Publish Event" checkbox before saving
- Only published events appear on `/src/pages/events/events.php`
- Draft events hidden from public view
## Security Features
1. **CSRF Token Protection**: All forms include CSRF token validation
2. **Admin-only Access**: `checkAdmin()` function validates user permissions
3. **File Validation**: Image type checking (JPEG, PNG, GIF, WebP)
4. **SQL Injection Prevention**: Prepared statements with parameter binding
5. **XSS Prevention**: `htmlspecialchars()` used for output escaping
## Styling Classes
**Form Container**: `.comment-form.bgc-lighter.z-1.rel.mb-30.rmb-55`
**Action Buttons**: `.btn-edit`, `.btn-delete`
**Status Badges**: `.badge.badge-published`, `.badge.badge-draft`
**Tables**: Uses sortable header styling with visual sort indicators
## Browser Compatibility
- Modern browsers with AJAX/Fetch API support
- JavaScript enabled required for filtering and sorting
- File input accepts image MIME types
## Future Enhancement Opportunities
1. Bulk event operations (bulk delete, publish multiple)
2. Event categories/tags system
3. Event capacity limits with registrations
4. Email notifications for published events
5. Event calendar view
6. Event image gallery (multiple images per event)
7. Recurring events support
8. Event attendee tracking

View File

@@ -0,0 +1,14 @@
-- Events Table Migration
-- Add missing columns to events table for proper tracking and publishing control
-- Add columns if they don't exist (using ALTER IGNORE for compatibility)
ALTER TABLE `events`
ADD COLUMN `created_by` int DEFAULT NULL AFTER `promo`,
ADD COLUMN `published` tinyint(1) DEFAULT 0 AFTER `created_by`,
ADD COLUMN `created_at` timestamp DEFAULT CURRENT_TIMESTAMP AFTER `published`,
ADD COLUMN `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER `created_at`;
-- Add indexes for better query performance
ALTER TABLE `events` ADD INDEX `idx_date` (`date`);
ALTER TABLE `events` ADD INDEX `idx_published` (`published`);
ALTER TABLE `events` ADD INDEX `idx_created_by` (`created_by`);

View File

@@ -283,6 +283,8 @@ if ($headerStyle === 'light') {
<ul>
<li><a href="admin_web_users">Website Users</a></li>
<li><a href="admin_members">4WDCSA Members</a></li>
<li><a href="admin_events">Manage Events</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_course_bookings">Course Bookings</a></li>
<li><a href="admin_efts">EFT Payments</a></li>

357
src/admin/admin_events.php Normal file
View File

@@ -0,0 +1,357 @@
<?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
// Fetch all events
$events_query = "
SELECT
event_id, name, type, location, date, 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;
}
}
?>
<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: 10px;
}
tbody tr:nth-child(even) td:first-child {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
tbody tr:nth-child(even) td:last-child {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
.filter-input {
width: 100%;
padding: 10px;
font-size: 16px;
background-color: rgb(255, 255, 255);
border-radius: 25px;
margin-bottom: 15px;
}
.btn {
display: inline-block;
padding: 6px 12px;
margin: 2px;
font-size: 14px;
border-radius: 5px;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #218838;
}
.btn-warning {
background-color: #ffc107;
color: black;
}
.btn-warning:hover {
background-color: #e0a800;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.bg-success {
background-color: #28a745;
color: white;
}
.bg-warning {
background-color: #ffc107;
color: black;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
// Sorting functionality
const table = document.querySelector('table');
if (table) {
const headers = table.querySelectorAll('thead th');
const rows = Array.from(table.querySelectorAll('tbody tr'));
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));
});
});
// Filter functionality
const filterInput = document.querySelector('.filter-input');
if (filterInput) {
filterInput.addEventListener('input', function() {
const filterValue = filterInput.value.trim().toLowerCase();
rows.forEach(row => {
const rowText = row.textContent.trim().toLowerCase();
row.style.display = rowText.includes(filterValue) ? '' : 'none';
});
});
}
}
// Publish/Unpublish toggle
$('.toggle-publish').on('click', function() {
var eventId = $(this).data('event-id');
var button = $(this);
var row = button.closest('tr');
$.ajax({
url: 'toggle_event_published',
type: 'POST',
data: {
event_id: eventId
},
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
if (response.published == 1) {
button.removeClass('btn-success').addClass('btn-warning');
button.find('i').removeClass('fa-eye').addClass('fa-eye-slash');
button.attr('title', 'Unpublish');
row.find('td:nth-child(5)').html('<span class="badge bg-success">Published</span>');
} else {
button.removeClass('btn-warning').addClass('btn-success');
button.find('i').removeClass('fa-eye-slash').addClass('fa-eye');
button.attr('title', 'Publish');
row.find('td:nth-child(5)').html('<span class="badge bg-warning">Draft</span>');
}
} else {
alert('Error: ' + response.message);
}
},
error: function() {
alert('Error updating event status');
}
});
});
// Delete event
$('.delete-event').on('click', function() {
if (!confirm('Are you sure you want to delete this event? This action cannot be undone.')) {
return false;
}
var eventId = $(this).data('event-id');
var button = $(this);
var row = button.closest('tr');
$.ajax({
url: 'delete_event',
type: 'POST',
data: {
event_id: eventId
},
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
row.fadeOut(300, function() {
$(this).remove();
});
} else {
alert('Error: ' + response.message);
}
},
error: function() {
alert('Error deleting event');
}
});
});
});
</script>
<?php
$pageTitle = 'Manage Events';
$breadcrumbs = [['Home' => 'index'], [$pageTitle => '']];
require_once($rootPath . '/components/banner.php');
?>
<?php
$pageTitle = 'Manage Events';
$breadcrumbs = [['Home' => 'index'], [$pageTitle => '']];
require_once($rootPath . '/components/banner.php');
?>
<!-- Events Management Area start -->
<section class="events-management-area py-100 rel z-1">
<div class="container">
<div class="row mb-30">
<div class="col-lg-12">
<a href="manage_events" class="theme-btn style-two">+ Create New Event</a>
</div>
</div>
<?php
if (!empty($events)) {
echo '<div class="row">
<div class="col-lg-12">
<div class="form-group mb-20">
<input type="text" class="filter-input" placeholder="Search events...">
</div>
<table>
<thead>
<tr>
<th>Event Name</th>
<th>Type</th>
<th>Location</th>
<th>Date</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>';
foreach ($events as $event) {
$publishButtonText = $event['published'] == 1 ? 'Unpublish' : 'Publish';
$publishButtonClass = $event['published'] == 1 ? 'btn-warning' : 'btn-success';
echo '<tr>
<td><strong>' . htmlspecialchars($event['name']) . '</strong></td>
<td>' . htmlspecialchars($event['type']) . '</td>
<td>' . htmlspecialchars($event['location']) . '</td>
<td>' . convertDate($event['date']) . '</td>
<td>' . ($event['published'] == 1 ? '<span class="badge bg-success">Published</span>' : '<span class="badge bg-warning">Draft</span>') . '</td>
<td>
<a href="manage_events?event_id=' . $event['event_id'] . '" class="btn btn-sm btn-primary" title="Edit">
<i class="far fa-edit"></i>
</a>
<button class="btn btn-sm ' . $publishButtonClass . ' toggle-publish" data-event-id="' . $event['event_id'] . '" title="' . $publishButtonText . '">
<i class="far fa-' . ($event['published'] == 1 ? 'eye-slash' : 'eye') . '"></i>
</button>
<button class="btn btn-sm btn-danger delete-event" data-event-id="' . $event['event_id'] . '" title="Delete">
<i class="far fa-trash"></i>
</button>
</td>
</tr>';
}
echo '</tbody></table>';
echo '</div>';
echo '</div>';
} else {
echo '<p>No events found. <a href="manage_events">Create one</a></p>';
}
?>
</div>
</section>
<!-- Events Management Area end -->
<?php include_once($rootPath . '/components/insta_footer.php'); ?>

View File

@@ -174,7 +174,7 @@ if (!empty($bannerImages)) {
// 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,
u.first_name, u.last_name,
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

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

View File

@@ -0,0 +1,46 @@
<?php
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
header('Content-Type: application/json');
$event_id = $_POST['event_id'] ?? null;
if (!$event_id) {
echo json_encode(['status' => 'error', 'message' => 'Event ID is required']);
exit;
}
// Get event details to delete associated files
$stmt = $conn->prepare("SELECT image, promo FROM events WHERE event_id = ?");
$stmt->bind_param("i", $event_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$event = $result->fetch_assoc();
// Delete image files
if ($event['image'] && file_exists($rootPath . '/' . $event['image'])) {
unlink($rootPath . '/' . $event['image']);
}
if ($event['promo'] && file_exists($rootPath . '/' . $event['promo'])) {
unlink($rootPath . '/' . $event['promo']);
}
// Delete from database
$delete_stmt = $conn->prepare("DELETE FROM events WHERE event_id = ?");
$delete_stmt->bind_param("i", $event_id);
if ($delete_stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Event deleted successfully']);
} else {
echo json_encode(['status' => 'error', 'message' => 'Failed to delete event']);
}
$delete_stmt->close();
} else {
echo json_encode(['status' => 'error', 'message' => 'Event not found']);
}
$stmt->close();

173
src/admin/manage_events.php Normal file
View File

@@ -0,0 +1,173 @@
<?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
$event_id = $_GET['event_id'] ?? null;
$event = null;
// If editing an existing event, fetch its data
if ($event_id) {
$stmt = $conn->prepare("SELECT * 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();
}
$stmt->close();
}
?>
<?php
$pageTitle = $event ? 'Edit Event' : 'Create New Event';
$breadcrumbs = [['Home' => 'index'], ['Admin' => 'admin_events'], [$pageTitle => '']];
require_once($rootPath . '/components/banner.php');
?>
<!-- Event Manager Area start -->
<section class="event-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="eventForm" enctype="multipart/form-data" method="POST" action="process_event">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<?php if ($event): ?>
<input type="hidden" name="event_id" value="<?php echo $event['event_id']; ?>">
<?php endif; ?>
<div class="section-title py-20">
<h2><?php echo $event ? 'Edit Event: ' . htmlspecialchars($event['name']) : 'Create New Event'; ?></h2>
<div id="responseMessage"></div>
</div>
<!-- Event Information -->
<div class="row mt-35">
<div class="col-md-6">
<div class="form-group">
<label for="name">Event Name *</label>
<input type="text" id="name" name="name" class="form-control" value="<?php echo $event ? htmlspecialchars($event['name']) : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="type">Event Type *</label>
<input type="text" id="type" name="type" class="form-control" value="<?php echo $event ? htmlspecialchars($event['type']) : ''; ?>" placeholder="e.g., Workshop, Training, Rally" required>
</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 $event ? htmlspecialchars($event['location']) : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="date">Date *</label>
<input type="date" id="date" name="date" class="form-control" value="<?php echo $event ? $event['date'] : ''; ?>" required>
</div>
</div>
<!-- Time -->
<div class="col-md-6">
<div class="form-group">
<label for="time">Time *</label>
<input type="time" id="time" name="time" class="form-control" value="<?php echo $event ? $event['time'] : ''; ?>" required>
</div>
</div>
<!-- Feature/Category -->
<div class="col-md-6">
<div class="form-group">
<label for="feature">Feature/Category *</label>
<input type="text" id="feature" name="feature" class="form-control" value="<?php echo $event ? htmlspecialchars($event['feature']) : ''; ?>" placeholder="e.g., Off-Road Training, Social Event" required>
</div>
</div>
<!-- Descriptions -->
<div class="col-md-12">
<div class="form-group">
<label for="description">Description *</label>
<textarea id="description" name="description" class="form-control" rows="6" required><?php echo $event ? htmlspecialchars($event['description']) : ''; ?></textarea>
</div>
</div>
<!-- Image Upload -->
<div class="col-md-12 mt-20">
<div class="form-group">
<label for="image">Event Image *</label>
<input type="file" id="image" name="image" class="form-control" accept="image/*" <?php echo !$event ? 'required' : ''; ?>>
<?php if ($event && $event['image']): ?>
<small class="text-info d-block mt-2">Current image: <img src="<?php echo $event['image']; ?>" alt="Event Image" style="max-width: 200px; margin-top: 10px;"></small>
<?php endif; ?>
</div>
</div>
<!-- Promo Image Upload -->
<div class="col-md-12 mt-20">
<div class="form-group">
<label for="promo">Promotional Image</label>
<input type="file" id="promo" name="promo" class="form-control" accept="image/*">
<small class="text-muted">This image will be displayed when users click "View Promo"</small>
<?php if ($event && $event['promo']): ?>
<small class="text-info d-block mt-2">Current promo: <img src="<?php echo $event['promo']; ?>" alt="Promo Image" style="max-width: 200px; margin-top: 10px;"></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 $event ? 'Update Event' : 'Create Event'; ?>
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
<!-- Event Manager Area end -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
$('#eventForm').on('submit', function(event) {
event.preventDefault();
var formData = new FormData(this);
$.ajax({
url: 'process_event',
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_events';
}, 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 event: ' + error + '</div>');
}
});
});
});
</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'); ?>

193
src/admin/process_event.php Normal file
View File

@@ -0,0 +1,193 @@
<?php
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
header('Content-Type: application/json');
// Handle delete action
if ($_GET['action'] ?? null === 'delete') {
$event_id = $_GET['event_id'] ?? null;
if (!$event_id) {
echo json_encode(['status' => 'error', 'message' => 'Event ID is required']);
exit;
}
// Get event details to delete associated files
$stmt = $conn->prepare("SELECT image, promo FROM events WHERE event_id = ?");
$stmt->bind_param("i", $event_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$event = $result->fetch_assoc();
// Delete image files
if ($event['image'] && file_exists($rootPath . '/' . $event['image'])) {
unlink($rootPath . '/' . $event['image']);
}
if ($event['promo'] && file_exists($rootPath . '/' . $event['promo'])) {
unlink($rootPath . '/' . $event['promo']);
}
// Delete from database
$delete_stmt = $conn->prepare("DELETE FROM events WHERE event_id = ?");
$delete_stmt->bind_param("i", $event_id);
if ($delete_stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Event deleted successfully']);
} else {
echo json_encode(['status' => 'error', 'message' => 'Failed to delete event']);
}
$delete_stmt->close();
} else {
echo json_encode(['status' => 'error', 'message' => 'Event not found']);
}
$stmt->close();
exit;
}
// Check CSRF token
if (!isset($_POST['csrf_token']) || !verifyCsrfToken($_POST['csrf_token'])) {
echo json_encode(['status' => 'error', 'message' => 'CSRF token validation failed']);
exit;
}
$event_id = $_POST['event_id'] ?? null;
$name = $_POST['name'] ?? null;
$type = $_POST['type'] ?? null;
$location = $_POST['location'] ?? null;
$date = $_POST['date'] ?? null;
$time = $_POST['time'] ?? null;
$feature = $_POST['feature'] ?? null;
$description = $_POST['description'] ?? null;
// Validate required fields
if (!$name || !$type || !$location || !$date || !$time || !$feature || !$description) {
echo json_encode(['status' => 'error', 'message' => 'All required fields must be filled']);
exit;
}
// Handle image upload
$image_path = null;
if (!empty($_FILES['image']['name'])) {
$upload_dir = $rootPath . '/assets/images/events/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
$file_name = uniqid() . '_' . basename($_FILES['image']['name']);
$target_file = $upload_dir . $file_name;
$file_type = mime_content_type($_FILES['image']['tmp_name']);
// Validate image file
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($file_type, $allowed_types)) {
echo json_encode(['status' => 'error', 'message' => 'Invalid image file type. Only JPEG, PNG, GIF, and WebP are allowed']);
exit;
}
if (move_uploaded_file($_FILES['image']['tmp_name'], $target_file)) {
$image_path = 'assets/images/events/' . $file_name;
} else {
echo json_encode(['status' => 'error', 'message' => 'Failed to upload image']);
exit;
}
} else if (!$event_id) {
echo json_encode(['status' => 'error', 'message' => 'Image is required for new events']);
exit;
}
// Handle promo image upload
$promo_path = null;
if (!empty($_FILES['promo']['name'])) {
$upload_dir = $rootPath . '/assets/images/events/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
$file_name = uniqid() . '_promo_' . basename($_FILES['promo']['name']);
$target_file = $upload_dir . $file_name;
$file_type = mime_content_type($_FILES['promo']['tmp_name']);
// Validate image file
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($file_type, $allowed_types)) {
echo json_encode(['status' => 'error', 'message' => 'Invalid promo image file type. Only JPEG, PNG, GIF, and WebP are allowed']);
exit;
}
if (move_uploaded_file($_FILES['promo']['tmp_name'], $target_file)) {
$promo_path = 'assets/images/events/' . $file_name;
}
}
try {
if ($event_id) {
// Update existing event
$update_fields = [
'name' => $name,
'type' => $type,
'location' => $location,
'date' => $date,
'time' => $time,
'feature' => $feature,
'description' => $description,
'updated_at' => date('Y-m-d H:i:s')
];
if ($image_path) {
$update_fields['image'] = $image_path;
}
if ($promo_path) {
$update_fields['promo'] = $promo_path;
}
$set_clause = implode(', ', array_map(function($key) {
return $key . ' = ?';
}, array_keys($update_fields)));
$values = array_values($update_fields);
$values[] = $event_id;
$stmt = $conn->prepare("UPDATE events SET $set_clause WHERE event_id = ?");
// Build type string for bind_param
$type_str = str_repeat('s', count($update_fields)) . 'i';
$stmt->bind_param($type_str, ...$values);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Event updated successfully']);
} else {
echo json_encode(['status' => 'error', 'message' => 'Failed to update event: ' . $stmt->error]);
}
} else {
// Create new event
if (!$image_path) {
echo json_encode(['status' => 'error', 'message' => 'Image is required for new events']);
exit;
}
$promo_path = $promo_path ?? 'assets/images/events/default-promo.jpg';
$stmt = $conn->prepare("
INSERT INTO events (name, type, location, date, time, feature, description, image, promo, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$created_by = $_SESSION['user_id'] ?? 0;
$stmt->bind_param('sssssssssi', $name, $type, $location, $date, $time, $feature, $description, $image_path, $promo_path, $created_by);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Event created successfully']);
} else {
echo json_encode(['status' => 'error', 'message' => 'Failed to create event: ' . $stmt->error]);
}
}
$stmt->close();
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => 'An error occurred: ' . $e->getMessage()]);
}

View File

@@ -0,0 +1,61 @@
<?php
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
header('Content-Type: application/json');
$event_id = $_POST['event_id'] ?? null;
if (!$event_id) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Event ID is required']);
exit;
}
try {
// Get current published status
$stmt = $conn->prepare("SELECT published FROM events WHERE event_id = ?");
if (!$stmt) {
throw new Exception("Prepare failed: " . $conn->error);
}
$stmt->bind_param("i", $event_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
http_response_code(404);
echo json_encode(['status' => 'error', 'message' => 'Event not found']);
$stmt->close();
exit;
}
$event = $result->fetch_assoc();
$new_status = $event['published'] == 1 ? 0 : 1;
$stmt->close();
// Update published status
$update_stmt = $conn->prepare("UPDATE events SET published = ?, updated_at = NOW() WHERE event_id = ?");
if (!$update_stmt) {
throw new Exception("Prepare failed: " . $conn->error);
}
$update_stmt->bind_param("ii", $new_status, $event_id);
if ($update_stmt->execute()) {
http_response_code(200);
echo json_encode([
'status' => 'success',
'message' => $new_status == 1 ? 'Event published' : 'Event unpublished',
'published' => $new_status
]);
} else {
throw new Exception("Update failed: " . $update_stmt->error);
}
$update_stmt->close();
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
}

View File

@@ -6,7 +6,16 @@ include_once('../config/functions.php');
$conn = openDatabaseConnection();
$stmt = $conn->prepare("SELECT
c.*,
c.id,
c.name,
c.description,
c.website,
c.telephone,
c.latitude,
c.longitude,
c.thumbnail,
c.country,
c.province,
u.first_name,
u.last_name,
u.profile_pic
@@ -26,6 +35,8 @@ while ($row = $result->fetch_assoc()) {
'latitude' => $row['latitude'],
'longitude' => $row['longitude'],
'thumbnail' => $row['thumbnail'],
'country' => $row['country'],
'province' => $row['province'],
'user' => [
'first_name' => $row['first_name'],
'last_name' => $row['last_name'],

View File

@@ -209,6 +209,27 @@ function getEFTDetails($eft_id) {
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 = [
'Messages' => [
[
@@ -216,20 +237,7 @@ function sendPOP($fullname, $eft_id, $amount, $description)
'Email' => $_ENV['MAILJET_FROM_EMAIL'],
'Name' => $_ENV['MAILJET_FROM_NAME'] . ' Web Admin'
],
'To' => [
[
'Email' => 'chrispintoza@gmail.com',
'Name' => 'Chris Pinto'
],
[
'Email' => $_ENV['MAILJET_FROM_EMAIL'],
'Name' => 'Jacqui Boshoff'
],
[
'Email' => 'louiseb@global.co.za',
'Name' => 'Louise Blignault'
]
],
'To' => $toAddresses,
'TemplateID' => 7054062,
'TemplateLanguage' => true,
'Subject' => "4WDCSA - Proof of Payment Received",
@@ -2772,7 +2780,7 @@ function url($page) {
'google_validate_login' => '/src/api/google_validate_login.php',
// Processors
'validate_login' => '/src/processors/validate_login.php',
'validate_login' => '/validate_login.php',
'register_user' => '/src/processors/register_user.php',
'process_application' => '/src/processors/process_application.php',
'process_booking' => '/src/processors/process_booking.php',
@@ -2802,3 +2810,95 @@ function url($page) {
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;
}

118
src/pages/add_campsite.php Normal file
View File

@@ -0,0 +1,118 @@
<?php
$rootPath = dirname(dirname(__DIR__));
require_once($rootPath . '/src/config/env.php');
include_once($rootPath . '/src/config/connection.php');
include_once($rootPath . '/src/config/functions.php');
session_start();
$user_id = $_SESSION['user_id'] ?? null;
// CSRF Token Validation
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
http_response_code(403);
die('Security token validation failed. Please try again.');
}
// campsites.php
$conn = openDatabaseConnection();
// Get text inputs
$name = validateName($_POST['name'] ?? '') ?: '';
$desc = isset($_POST['description']) ? htmlspecialchars($_POST['description'], ENT_QUOTES, 'UTF-8') : '';
$country = isset($_POST['country']) ? htmlspecialchars($_POST['country'], ENT_QUOTES, 'UTF-8') : '';
$province = isset($_POST['province']) ? htmlspecialchars($_POST['province'], ENT_QUOTES, 'UTF-8') : '';
$lat = isset($_POST['latitude']) ? floatval($_POST['latitude']) : 0.0;
$lng = isset($_POST['longitude']) ? floatval($_POST['longitude']) : 0.0;
$website = isset($_POST['website']) ? filter_var($_POST['website'], FILTER_VALIDATE_URL) : '';
$telephone = validatePhoneNumber($_POST['telephone'] ?? '') ?: '';
if (empty($name)) {
http_response_code(400);
die('Campsite name is required.');
}
// Handle file upload
$thumbnailPath = null;
if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] !== UPLOAD_ERR_NO_FILE) {
// Validate file using hardened validation function
$validationResult = validateFileUpload($_FILES['thumbnail'], 'profile_picture');
if ($validationResult === false) {
http_response_code(400);
die('Invalid thumbnail image. Only JPG, JPEG, PNG, GIF, and WEBP images under 5MB are allowed.');
}
$uploadDir = $rootPath . "/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.');
}
$randomFilename = $validationResult['filename'];
$targetFile = $uploadDir . $randomFilename;
if (move_uploaded_file($_FILES["thumbnail"]["tmp_name"], $targetFile)) {
chmod($targetFile, 0644);
$thumbnailPath = "assets/uploads/campsites/" . $randomFilename;
} else {
http_response_code(500);
die('Failed to move uploaded file.');
}
}
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
if ($id > 0) {
// Verify ownership - check if the campsite belongs to the current user
$ownerCheckStmt = $conn->prepare("SELECT user_id FROM campsites WHERE id = ?");
$ownerCheckStmt->bind_param("i", $id);
$ownerCheckStmt->execute();
$ownerResult = $ownerCheckStmt->get_result();
if ($ownerResult->num_rows === 0) {
http_response_code(404);
die('Campsite not found.');
}
$ownerRow = $ownerResult->fetch_assoc();
if ($ownerRow['user_id'] != $user_id) {
http_response_code(403);
die('You do not have permission to edit this campsite. Only the owner can make changes.');
}
$ownerCheckStmt->close();
// UPDATE
if ($thumbnailPath) {
$stmt = $conn->prepare("UPDATE campsites SET name=?, description=?, country=?, province=?, latitude=?, longitude=?, website=?, telephone=?, thumbnail=? WHERE id=?");
$stmt->bind_param("ssssddsssi", $name, $desc, $country, $province, $lat, $lng, $website, $telephone, $thumbnailPath, $id);
} else {
$stmt = $conn->prepare("UPDATE campsites SET name=?, description=?, country=?, province=?, latitude=?, longitude=?, website=?, telephone=? WHERE id=?");
$stmt->bind_param("ssssddssi", $name, $desc, $country, $province, $lat, $lng, $website, $telephone, $id);
}
// Log the action
auditLog($user_id, 'CAMPSITE_UPDATE', 'campsites', $id, ['name' => $name]);
} else {
// INSERT
$stmt = $conn->prepare("INSERT INTO campsites (name, description, country, province, latitude, longitude, website, telephone, thumbnail, user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("ssssddsssi", $name, $desc, $country, $province, $lat, $lng, $website, $telephone, $thumbnailPath, $user_id);
// Log the action
auditLog($user_id, 'CAMPSITE_CREATE', 'campsites', 0, ['name' => $name]);
}
if (!$stmt->execute()) {
http_response_code(500);
die('Database error: ' . $stmt->error);
}
$stmt->close();
header("Location: campsites");
?>

View File

@@ -25,74 +25,303 @@ while ($row = $result->fetch_assoc()) {
.info-box img {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
/* Form styling to match manage_trips */
.campsite-form-container {
background: #f9f9f7;
border: 1px solid #d8d8d8;
border-radius: 10px;
padding: 30px;
margin: 20px 0;
display: none;
}
.campsite-form-container h5 {
color: #2c3e50;
font-weight: 600;
margin-bottom: 30px;
font-size: 1.5rem;
}
.campsite-form-container .form-group {
margin-bottom: 20px;
}
.campsite-form-container label {
font-weight: 500;
color: #34495e;
margin-bottom: 8px;
display: block;
}
.campsite-form-container .form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.campsite-form-container .form-control:focus {
border-color: #4CAF50;
box-shadow: 0 0 0 0.2rem rgba(76, 175, 80, 0.25);
outline: none;
}
.campsite-form-container .form-control select {
cursor: pointer;
}
.campsite-form-container .btn {
border-radius: 6px;
font-weight: 500;
padding: 10px 20px;
}
/* Table styling to match admin trips */
.campsites-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 10px 0;
}
.campsites-table thead th {
cursor: pointer;
text-align: left;
padding: 10px;
font-weight: bold;
position: relative;
}
.campsites-table thead th::after {
content: '\25B2';
font-size: 0.8em;
position: absolute;
right: 10px;
opacity: 0;
transition: opacity 0.2s;
}
.campsites-table thead th.asc::after {
content: '\25B2';
opacity: 1;
}
.campsites-table thead th.desc::after {
content: '\25BC';
opacity: 1;
}
.campsites-table tbody tr:nth-child(odd) {
background-color: transparent;
}
.campsites-table tbody tr:nth-child(even) {
background-color: rgb(255, 255, 255);
border-radius: 10px;
}
.campsites-table tbody td {
padding: 10px;
}
.campsites-table tbody tr:nth-child(even) td:first-child {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
.campsites-table tbody tr:nth-child(even) td:last-child {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
.filter-input {
width: 100%;
padding: 10px;
font-size: 16px;
background-color: rgb(255, 255, 255);
border-radius: 25px;
margin-bottom: 20px;
border: 1px solid #ddd;
}
.campsite-group {
color: #484848;
background: #f9f9f7;
border: 1px solid #d8d8d8;
border-radius: 10px;
margin-top: 15px;
margin-bottom: 15px;
padding: 10px;
}
</style>
<?php
$pageTitle = 'Campsites';
$breadcrumbs = [['Home' => 'index.php']];
require_once($rootPath . '/components/banner.php');
$pageTitle = 'Campsites';
$breadcrumbs = [['Home' => 'index.php']];
require_once($rootPath . '/components/banner.php');
?>
<!-- Tour List Area start -->
<section class="tour-list-page py-100 rel z-1">
<section class="tour-list-page py-100 rel">
<div class="container">
<div class="row">
<div class="col-lg-12">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3>Campsites Map</h3>
<button class="theme-btn" id="toggleFormBtn" onclick="toggleCampsiteForm()">
<i class="far fa-plus"></i> Add Campsite
</button>
</div>
<p style="color: #666; margin-bottom: 15px;">Click on the map to add a new campsite, or click on a marker to view details.</p>
<!-- Collapsible Campsite Form -->
<div class="campsite-form-container" id="campsiteFormContainer">
<h5>Add New Campsite</h5>
<form id="addCampsiteForm" method="POST" action="add_campsite" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<input type="hidden" name="latitude" id="latitude">
<input type="hidden" name="longitude" id="longitude">
<div class="row mt-35">
<div class="col-md-6">
<div class="form-group">
<label for="campsite_name">Campsite Name *</label>
<input type="text" id="campsite_name" class="form-control" name="name" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="countrySelect">Country *</label>
<select id="countrySelect" class="form-control" name="country" required>
<option value="">-- Select Country --</option>
<option value="South Africa">South Africa</option>
<option value="Botswana">Botswana</option>
<option value="Eswatini">Eswatini</option>
<option value="Lesotho">Lesotho</option>
<option value="Namibia">Namibia</option>
<option value="Zimbabwe">Zimbabwe</option>
<option value="Other">Other</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="provinceSelect">Province *</label>
<select id="provinceSelect" class="form-control" name="province" required>
<option value="">-- Select Province --</option>
<option value="Eastern Cape">Eastern Cape</option>
<option value="Free State">Free State</option>
<option value="Gauteng">Gauteng</option>
<option value="KwaZulu-Natal">KwaZulu-Natal</option>
<option value="Limpopo">Limpopo</option>
<option value="Mpumalanga">Mpumalanga</option>
<option value="Northern Cape">Northern Cape</option>
<option value="North West">North West</option>
<option value="Western Cape">Western Cape</option>
<option value="Other">Other</option>
</select>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label for="campsite_description">Description</label>
<textarea id="campsite_description" class="form-control" name="description" rows="3"></textarea>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="campsite_website">Booking URL</label>
<input type="url" id="campsite_website" class="form-control" name="website">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="campsite_phone">Phone Number</label>
<input type="text" id="campsite_phone" class="form-control" name="telephone">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="latitude_display">Latitude</label>
<input type="text" id="latitude_display" class="form-control" readonly>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="longitude_display">Longitude</label>
<input type="text" id="longitude_display" class="form-control" readonly>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label for="campsite_thumbnail">Thumbnail Image</label>
<input type="file" id="campsite_thumbnail" class="form-control" name="thumbnail" accept="image/*">
</div>
</div>
<div class="col-md-12">
<div class="form-group mb-0">
<button class="theme-btn style-two" type="submit" style="width: 100%; margin-right: 10px;">Save Campsite</button>
<button class="theme-btn" type="button" onclick="toggleCampsiteForm()" style="width: 100%; margin-top: 10px;">Cancel</button>
</div>
</div>
</div>
</form>
</div>
<div id="map" style="width: 100%; height: 500px;"></div>
<!-- Add Campsite Modal -->
<!-- Campsites Table -->
<div style="margin-top: 40px;">
<h4 style="margin-bottom: 20px;">All Campsites</h4>
<input type="text" class="filter-input" id="campsitesFilter" placeholder="Filter results...">
<div class="table-responsive">
<table class="campsites-table">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Description</th>
<th>Website</th>
<th>Phone</th>
<th>Added By</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="campsitesTableBody">
<!-- Populated by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
<div class="modal fade" id="addCampsiteModal" tabindex="-1">
<div class="modal-dialog">
<form id="addCampsiteForm" method="POST" action="add_campsite" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Campsite</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" name="latitude" id="latitude">
<input type="hidden" name="longitude" id="longitude">
<div class="mb-3">
<label class="form-label">Campsite Name</label>
<input type="text" class="form-control" name="name" required>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea class="form-control" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Booking URL</label>
<input type="url" class="form-control" name="website">
</div>
<div class="mb-3">
<label class="form-label">Phone Number</label>
<input type="text" class="form-control" name="telephone">
</div>
<div class="mb-3">
<label class="form-label">Thumbnail Image</label>
<input type="file" class="form-control" name="thumbnail" accept="image/*">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="submit">Save Campsite</button>
<button class="btn btn-secondary" type="button" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</form>
</div>
</div>
<script>
let map;
const campsites = <?php echo json_encode($campsites); ?>;
function toggleCampsiteForm() {
const container = document.getElementById("campsiteFormContainer");
container.style.display = container.style.display === "none" ? "block" : "none";
if (container.style.display === "block") {
container.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
function resetForm() {
// Clear the form
document.getElementById("addCampsiteForm").reset();
// Remove the ID input if it exists
let idInput = document.querySelector("#addCampsiteForm input[name='id']");
if (idInput) {
idInput.remove();
}
}
function initMap() {
map = new google.maps.Map(document.getElementById("map"), {
center: {
@@ -106,15 +335,19 @@ while ($row = $result->fetch_assoc()) {
const lat = e.latLng.lat();
const lng = e.latLng.lng();
resetForm();
document.getElementById("latitude").value = lat;
document.getElementById("longitude").value = lng;
document.getElementById("latitude_display").value = lat.toFixed(6);
document.getElementById("longitude_display").value = lng.toFixed(6);
const addModal = new bootstrap.Modal(document.getElementById("addCampsiteModal"));
addModal.show();
// Show the form container
document.getElementById("campsiteFormContainer").style.display = "block";
document.getElementById("campsiteFormContainer").scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
// Load existing campsites from PHP
fetch("get_campsites.php")
fetch("get_campsites")
.then(response => response.json())
.then(data => {
data.forEach(site => {
@@ -156,21 +389,97 @@ while ($row = $result->fetch_assoc()) {
infowindow.open(map, marker);
});
});
// Populate the table
populateCampsitesTable(data);
})
.catch(err => console.error("Failed to load campsites:", err));
}
function populateCampsitesTable(campsites) {
const tableBody = document.getElementById("campsitesTableBody");
tableBody.innerHTML = ""; // Clear existing rows
if (campsites.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="6" class="text-center text-muted" style="padding: 30px;">
No campsites added yet. Click on the map to add one!
</td>
</tr>
`;
return;
}
// Group campsites by country and province
const groupedByCountryAndProvince = {};
campsites.forEach(site => {
const country = site.country || 'Unknown Country';
const province = site.province || 'Unknown Province';
if (!groupedByCountryAndProvince[country]) {
groupedByCountryAndProvince[country] = {};
}
if (!groupedByCountryAndProvince[country][province]) {
groupedByCountryAndProvince[country][province] = [];
}
groupedByCountryAndProvince[country][province].push(site);
});
// Sort countries alphabetically
const sortedCountries = Object.keys(groupedByCountryAndProvince).sort();
// Populate table with grouped data
sortedCountries.forEach(country => {
// Sort provinces alphabetically for this country
const sortedProvinces = Object.keys(groupedByCountryAndProvince[country]).sort();
sortedProvinces.forEach(province => {
// Add province group header
const groupRow = document.createElement("tr");
groupRow.innerHTML = `
<td colspan="6" style="font-weight: 600; padding: 10px 8px; background-color: #f0f0f0;">
<i class="fas fa-globe" style="color: #2196F3; margin-right: 8px;"></i>${country} - ${province}
</td>
`;
tableBody.appendChild(groupRow);
// Add campsite rows for this province
groupedByCountryAndProvince[country][province].forEach(site => {
const row = document.createElement("tr");
const userName = site.user && site.user.first_name
? `${site.user.first_name} ${site.user.last_name}`
: "Unknown";
row.innerHTML = `
<td><strong>${site.name}</strong></td>
<td>${site.description ? site.description.substring(0, 50) + (site.description.length > 50 ? '...' : '') : '-'}</td>
<td>${site.website ? `<a href="${site.website}" target="_blank" class="link-primary">Visit</a>` : '-'}</td>
<td>${site.telephone || '-'}</td>
<td><small>${userName}</small></td>
<td>
<button class="btn btn-sm btn-warning" onclick='editCampsite(${JSON.stringify(site)})'>Edit</button>
<a href="https://www.google.com/maps/dir/?api=1&destination=${site.latitude},${site.longitude}" target="_blank" class="btn btn-sm btn-outline-primary">Directions</a>
</td>
`;
tableBody.appendChild(row);
});
});
});
}
function editCampsite(site) {
// Pre-fill form
document.querySelector("#addCampsiteForm input[name='name']").value = site.name;
document.querySelector("#addCampsiteForm select[name='country']").value = site.country || '';
document.querySelector("#addCampsiteForm select[name='province']").value = site.province || '';
document.querySelector("#addCampsiteForm textarea[name='description']").value = site.description || "";
document.querySelector("#addCampsiteForm input[name='website']").value = site.website || "";
document.querySelector("#addCampsiteForm input[name='telephone']").value = site.telephone || "";
document.querySelector("#addCampsiteForm input[name='latitude']").value = site.latitude;
document.querySelector("#addCampsiteForm input[name='longitude']").value = site.longitude;
document.getElementById("latitude_display").value = parseFloat(site.latitude).toFixed(6);
document.getElementById("longitude_display").value = parseFloat(site.longitude).toFixed(6);
// Add hidden ID input
let idInput = document.querySelector("#addCampsiteForm input[name='id']");
@@ -182,14 +491,13 @@ while ($row = $result->fetch_assoc()) {
}
idInput.value = site.id;
// Show the modal
const addModal = new bootstrap.Modal(document.getElementById("addCampsiteModal"));
addModal.show();
// Show the form container
document.getElementById("campsiteFormContainer").style.display = "block";
document.getElementById("campsiteFormContainer").scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyC-JuvnbUYc8WGjQBFFVZtKiv5_bFJoWLU&callback=initMap" async defer></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></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

@@ -12,12 +12,22 @@ $token = $_GET['token'];
// Sanitize the trip_id to prevent SQL injection
$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
$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
WHERE trip_id = ?";
// If not admin, only show published trips
if (!$is_admin) {
$sql .= " AND published = 1";
}
// Use prepared statements for added security
$stmt = $conn->prepare($sql);
@@ -194,12 +204,39 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
</ol>
</nav>
</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>
</section>
<!-- Tour Gallery start -->
<div class="tour-gallery">
<div class="container-fluid">
@@ -259,6 +296,8 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
</div>
<span class="subtitle mb-15"><?php echo $badge_text; ?></span>
</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="tour-header-social mb-10">
@@ -673,4 +712,42 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
});
</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') ?>

View File

@@ -7,14 +7,18 @@ include_once($rootPath . '/header.php');
<style>
.image {
width: 400px;
/* Set your desired width */
width: 100%;
height: 350px;
/* Set your desired height */
overflow: hidden;
/* Hide any overflow */
display: block;
/* Ensure proper block behavior */
}
.image img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top;
display: block;
}
</style>
@@ -52,8 +56,17 @@ include_once($rootPath . '/header.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
$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);
if ($result->num_rows > 0) {
@@ -68,16 +81,18 @@ include_once($rootPath . '/header.php');
$capacity = $row['vehicle_capacity'];
$cost_members = $row['cost_members'];
$places_booked = $row['places_booked'];
$published = $row['published'] ?? 1;
$remaining_places = getAvailableSpaces($trip_id);
// Determine the badge text based on the status
$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
echo '
<div class="destination-item style-three bgc-lighter" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<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 . '">
</div>
<div class="content">
@@ -91,7 +106,7 @@ include_once($rootPath . '/header.php');
<i class="fas fa-star"></i>
</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>
<ul class="blog-meta">
<li><i class="far fa-calendar"></i> ' . convertDate($start_date) . ' - ' . convertDate($end_date) . '</li>
@@ -100,7 +115,7 @@ include_once($rootPath . '/header.php');
</ul>
<div class="destination-footer">
<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>
<i class="fal fa-arrow-right"></i>
</a>

View File

@@ -90,8 +90,8 @@ include_once($rootPath . '/header.php');
</div>
<?php
// Query to retrieve upcoming events
$stmt = $conn->prepare("SELECT event_id, date, time, name, image, description, feature, location, type, promo FROM events WHERE date > CURDATE() ORDER BY date ASC");
// Query to retrieve upcoming published events only
$stmt = $conn->prepare("SELECT event_id, date, time, name, image, description, feature, location, type, promo FROM events WHERE date > CURDATE() AND published = 1 ORDER BY date ASC");
$stmt->execute();
$result = $stmt->get_result();

View File

@@ -168,24 +168,22 @@ $user = $result->fetch_assoc();
data: formData,
contentType: false,
processData: false,
dataType: 'json',
success: function(response) {
// Parse response if needed
if (typeof response === "string") {
response = JSON.parse(response);
}
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>');
// Reload the current page after a short delay
setTimeout(function() {
window.location.reload();
}, 1500);
} else {
$('#responseMessage').html('<div class="alert alert-danger">' + response.message + '</div>');
}
},
error: function() {
$('#responseMessage').html('<div class="alert alert-danger">Error uploading profile picture.</div>');
error: function(xhr, status, error) {
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
ob_start(); // Start output buffering
session_start();
// Set JSON response header BEFORE any other output
header('Content-Type: application/json');
$rootPath = dirname(dirname(__DIR__));
require_once($rootPath . "/src/config/env.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");
if (!isset($_SESSION['user_id'])) {
ob_end_clean();
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
$fileName = 'signature_' . $user_id . '.png';
$filePath = 'uploads/signatures/' . $fileName;
$filePath = $rootPath . '/uploads/signatures/' . $fileName;
// Ensure the directory exists
if (!is_dir('uploads/signatures')) {
mkdir('uploads/signatures', 0777, true);
if (!is_dir($rootPath . '/uploads/signatures')) {
mkdir($rootPath . '/uploads/signatures', 0777, true);
}
// Save the image file
@@ -41,30 +48,37 @@ if (isset($_POST['signature'])) {
die(json_encode(['status' => 'error', 'message' => 'Database connection failed']));
}
// Store relative path for HTML display
$display_path = '/uploads/signatures/' . $fileName;
// 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->bind_param('si', $filePath, $user_id);
$stmt->bind_param('si', $display_path, $user_id);
if ($stmt->execute()) {
// Check the payment status
$paymentStatus = checkMembershipPaymentStatus($user_id) ? 'PAID' : 'NOT_PAID';
// Respond with the appropriate redirect URL based on the payment status
ob_end_clean();
echo json_encode([
'status' => 'success',
'message' => 'Signature saved successfully!',
'paymentStatus' => $paymentStatus // Send payment status
]);
} else {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Database update failed']);
}
$stmt->close();
$conn->close();
} else {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Failed to save signature']);
}
} else {
ob_end_clean();
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 {
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

View File

@@ -1,7 +1,10 @@
<?php
ob_start(); // Start output buffering to allow headers before output
$headerStyle = 'light';
$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");
checkUserSession();
@@ -11,7 +14,8 @@ if (!$user_id) {
die("Not logged in.");
}
// Handle POST submission
// Handle POST submission BEFORE including header
$redirect_url = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF Token Validation
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
@@ -34,9 +38,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit;
}
$target_dir = "uploads/pop/";
$randomFilename = $validationResult['filename'];
$target_file = $target_dir . $randomFilename;
$target_dir = $rootPath . "/uploads/pop/";
// Use EFT ID as filename instead of random filename, replace spaces with underscores
$filename = str_replace(' ', '_', $eft_id) . '.pdf';
$target_file = $target_dir . $filename;
// Make sure target directory exists and writable
if (!is_dir($target_dir)) {
@@ -91,15 +96,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$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.";
} else {
$_SESSION['message'] = "Payment uploaded, but notification email could not be sent.";
}
// 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");
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
$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
ob_start(); // Start output buffering
session_start();
// Set JSON response header BEFORE any other output
header('Content-Type: application/json');
$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');
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');
@@ -29,7 +40,7 @@ if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] !=
// Extract validated filename
$randomFilename = $validationResult['filename'];
$target_dir = "assets/images/pp/";
$target_dir = $rootPath . "/assets/images/pp/";
$target_file = $target_dir . $randomFilename;
// Ensure upload directory exists and is writable
@@ -48,6 +59,9 @@ if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] !=
// Set secure file permissions (readable but not executable)
chmod($target_file, 0644);
// Store relative path for HTML display
$display_path = "assets/images/pp/" . $randomFilename;
// Update the profile picture path in the database
$sql = "UPDATE users SET profile_pic = ? WHERE user_id = ?";
$stmt = $conn->prepare($sql);
@@ -57,25 +71,27 @@ if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] !=
exit();
}
$stmt->bind_param("si", $target_file, $user_id);
$stmt->bind_param("si", $display_path, $user_id);
if ($stmt->execute()) {
$_SESSION['profile_pic'] = $target_file;
$_SESSION['profile_pic'] = $display_path;
$response['status'] = 'success';
$response['message'] = 'Profile picture updated successfully';
// Log the action
auditLog($user_id, 'PROFILE_PIC_UPLOAD', 'users', $user_id, ['filename' => $randomFilename]);
} 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();
} else {
$response['message'] = 'Failed to move uploaded file.';
$response['message'] = 'Failed to move uploaded file. Error code: ' . $_FILES['profile_picture']['error'];
}
} else {
$response['message'] = 'No file uploaded or file error.';
}
// Clean output buffer and send only JSON
ob_end_clean();
echo json_encode($response);
?>