Compare commits
22 Commits
be2b757f4e
...
feature/ev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32651ed433 | ||
|
|
f522b84fc1 | ||
|
|
2b136c4b06 | ||
|
|
7f0964009a | ||
|
|
5be946f78f | ||
|
|
cb588d20ee | ||
|
|
fdeaf85bf0 | ||
|
|
d81d74a7c7 | ||
|
|
bfb3a0f8a9 | ||
|
|
5a2c48f343 | ||
|
|
1767337d99 | ||
|
|
674af23994 | ||
|
|
ec563e0376 | ||
|
|
a3403bf503 | ||
|
|
5f1a6bc441 | ||
|
|
716de2f0e9 | ||
|
|
79e292dc7c | ||
|
|
59c1e37d5c | ||
|
|
0c068eeb69 | ||
|
|
6fd3b8d082 | ||
|
|
902291d8d1 | ||
|
|
ac460ef97f |
12
.htaccess
@@ -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>
|
||||
|
||||
|
||||
BIN
assets/images/pp/2f40af86bfbe04a5c83bbb6cdf1c1e6b.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
assets/images/pp/424b31c09e1543a922deb690bfbb57c8.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
assets/images/pp/4b8bd95296e082031c8ae8c4b35fed88.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
assets/images/pp/5f9036058b40b2c23052d8226711ac5c.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
assets/images/pp/7a7b9965853213ea1e4ed1aec4e18ad0.jpg
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
assets/images/pp/8bc567fbcdffcf5823845740a54d5e6d.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
assets/images/pp/9a1f344bc68815fa15bb0a1e16017ee6.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
assets/images/pp/b8d7fa81c1ab3e67dc86441b09d927cd.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
assets/images/pp/cc83c3045d2b41073f0939f298d06459.jpg
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
assets/images/pp/e607963d306a19d1df94c50d577ea439.jpg
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
assets/images/trips/8_01.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/images/trips/8_02.jpg
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
assets/images/trips/8_03.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
assets/images/trips/8_04.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/images/trips/8_05.jpg
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
assets/uploads/campsites/274d8e71982307bc5a699125966d5731.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
assets/uploads/campsites/3dd0636b3ed6926e10f0387a747d58c1.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/uploads/campsites/ae16ea8e89bb83dc3b85c54aa0e3fcec.jpg
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
assets/uploads/campsites/c613066cd83537a874355671e0213539.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/uploads/campsites/d21ae51aec635de07883d9586a1542df.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
176
docs/EVENTS_ADMIN_SYSTEM.md
Normal 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
|
||||
14
docs/migrations/001_add_events_tracking_columns.sql
Normal 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`);
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
$rootPath = dirname(__FILE__);
|
||||
$headerStyle = 'dark';
|
||||
include_once('header.php');
|
||||
include_once($rootPath . '/header.php');
|
||||
$indemnityPending = false;
|
||||
|
||||
if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) {
|
||||
|
||||
357
src/admin/admin_events.php
Normal 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'); ?>
|
||||
@@ -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
@@ -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'); ?>
|
||||
46
src/admin/delete_event.php
Normal 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
@@ -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
@@ -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
@@ -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()]);
|
||||
}
|
||||
61
src/admin/toggle_event_published.php
Normal 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()]);
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
require 'env.php';
|
||||
require 'connection.php';
|
||||
|
||||
$conn = openDatabaseConnection();
|
||||
|
||||
if (!$conn) {
|
||||
die('Database connection failed');
|
||||
}
|
||||
|
||||
$sql = file_get_contents('migrations/001_phase1_security_schema.sql');
|
||||
|
||||
if ($conn->multi_query($sql)) {
|
||||
echo "✓ Migration executed successfully\n";
|
||||
} else {
|
||||
echo "✗ Migration error: " . $conn->error . "\n";
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
?>
|
||||
118
src/pages/add_campsite.php
Normal 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");
|
||||
?>
|
||||
@@ -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'); ?>
|
||||
@@ -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') ?>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,804 +0,0 @@
|
||||
<?php
|
||||
$headerStyle = 'dark';
|
||||
include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
|
||||
$indemnityPending = false;
|
||||
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$userId = $_SESSION['user_id'];
|
||||
$stmt = $conn->prepare("SELECT user_id FROM membership_application WHERE user_id = ? AND accept_indemnity = 0 LIMIT 1");
|
||||
$stmt->bind_param("i", $userId);
|
||||
$stmt->execute();
|
||||
$stmt->store_result();
|
||||
|
||||
if ($stmt->num_rows > 0) {
|
||||
$indemnityPending = true;
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
?>
|
||||
<style>
|
||||
.countdown-container {
|
||||
width: 100%;
|
||||
/* background: #111; */
|
||||
text-align: center;
|
||||
padding: 40px 10px;
|
||||
/* font-family: Arial, sans-serif; */
|
||||
}
|
||||
|
||||
.countdown-container h1 {
|
||||
font-size: 3rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.countdown-container h1 {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<?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="hero-area bgc-black pt-200 rpt-120 rel z-2">
|
||||
<div style="padding-bottom:30px;" class="container-fluid">
|
||||
<div style="text-align: center; position: relative; border-radius: 20px; overflow: hidden; background: linear-gradient(rgba(28, 35, 31, 1), rgba(28, 35, 31, 0.5)), url('<?php echo $randomBanner; ?>'); background-size: cover; background-position: center;">
|
||||
<div style="padding-top: 50px; padding-bottom: 50px;">
|
||||
<img style="width: 250px; margin-bottom: 20px;" src="assets/images/logos/weblogo2.png" alt="Logo">
|
||||
<h1 class="hero-title" data-aos="flip-up" data-aos-delay="50" data-aos-duration="1500" data-aos-offset="50">
|
||||
Welcome to<br>the Four Wheel Drive Club<br>of Southern Africa
|
||||
</h1>
|
||||
<a href="membership" class="theme-btn style-two bgc-secondary" style="margin-top: 20px; background-color: #e90000; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;">
|
||||
<span data-hover="Become a Member">Become a Member</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Hero Area End -->
|
||||
<!-- Destinations Area start -->
|
||||
<?php
|
||||
if (countUpcomingTrips() > 0) { ?>
|
||||
<section class="destinations-area bgc-black pt-100 pb-70 rel z-1">
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-12">
|
||||
<div class="section-title text-white text-center counter-text-wrap mb-70" data-aos="fade-up"
|
||||
data-aos-duration="1500" data-aos-offset="50">
|
||||
<h2>Discover Africa's Treasures with 4WDCSA</h2>
|
||||
<p>Join us on the following trips:</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<?php
|
||||
// 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 ORDER BY trip_id DESC LIMIT 4";
|
||||
$result = $conn->query($sql);
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
// Loop through each row
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$trip_id = $row['trip_id'];
|
||||
$trip_name = $row['trip_name'];
|
||||
$location = $row['location'];
|
||||
$short_description = $row['short_description'];
|
||||
$start_date = $row['start_date'];
|
||||
$end_date = $row['end_date'];
|
||||
$capacity = $row['vehicle_capacity'];
|
||||
$cost_members = $row['cost_members'];
|
||||
$places_booked = $row['places_booked'];
|
||||
$remaining_places = $capacity - $places_booked;
|
||||
|
||||
// Determine the badge text based on the status
|
||||
$badge_text = ($remaining_places > 0) ? $remaining_places . ' PLACES LEFT!!' : 'FULLY BOOKED';
|
||||
echo '
|
||||
<div class="col-xxl-3 col-xl-4 col-md-6">
|
||||
<div class="destination-item" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image">
|
||||
<img src="assets/images/trips/' . $trip_id . '_01.jpg" alt="' . $trip_name . '">
|
||||
</div>
|
||||
<div class="content">
|
||||
<span class="location"><i class="fal fa-map-marker-alt"></i> ' . $location . '</span>
|
||||
<h5><a href="trip-details.php?trip_id=' . $trip_id . '">' . $trip_name . '</a></h5>
|
||||
<span class="time">' . convertDate($start_date) . ' - ' . convertDate($end_date) . '</span><br>
|
||||
<span class="time">' . calculateDaysAndNights($start_date, $end_date) . '</span>
|
||||
</div>
|
||||
<div class="destination-footer">
|
||||
<span class="price"><span>R ' . $cost_members . '</span>/per member</span>
|
||||
<a href="trip-details.php?trip_id=' . $trip_id . '" class="read-more">Book Now <i class="fal fa-angle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
} else {
|
||||
echo "No trips available.";
|
||||
}
|
||||
?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Destinations Area end -->
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
|
||||
|
||||
<!-- About Us Area start -->
|
||||
<section class="about-us-area py-100 rpb-90 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-xl-5 col-lg-6">
|
||||
<div class="about-us-content rmb-55" data-aos="fade-left" data-aos-duration="1500"
|
||||
data-aos-offset="50">
|
||||
<div class="section-title mb-25">
|
||||
<h2>Become a member of 4WDCSA</h2>
|
||||
<p>Sign up for an annual membership and receive:</p>
|
||||
<ul class="list-style-two mt-35 mb-30">
|
||||
<li>Year round access to BASE4</li>
|
||||
<li>FREE Camping at BASE4</li>
|
||||
<li>Up to 95% Discount on Training Courses</li>
|
||||
<li>Exclusive Member discounts for all trips and events</li>
|
||||
<li>... and many more!</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>We go above and beyond to make your travel dreams reality hidden gems and must-see
|
||||
attractions</p>
|
||||
|
||||
<a href="membership" class="theme-btn mt-10 style-two">
|
||||
<span data-hover="Become A Member">Become A Member</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-7 col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="about-us-image">
|
||||
<!-- <div class="shape"><img src="assets/images/about/shape1.png" alt="Shape"></div>
|
||||
<div class="shape"><img src="assets/images/about/shape2.png" alt="Shape"></div>
|
||||
<div class="shape"><img src="assets/images/about/shape3.png" alt="Shape"></div>
|
||||
<div class="shape"><img src="assets/images/about/shape4.png" alt="Shape"></div>
|
||||
<div class="shape"><img src="assets/images/about/shape5.png" alt="Shape"></div>
|
||||
<div class="shape"><img src="assets/images/about/shape6.png" alt="Shape"></div>
|
||||
<div class="shape"><img src="assets/images/about/shape7.png" alt="Shape"></div> -->
|
||||
<img src="assets/images/logos/weblogo.png" alt="About">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- About Us Area end -->
|
||||
|
||||
<section class="hotel-area bgc-black py-100 rel z-1">
|
||||
<div class="countdown-container">
|
||||
<h1 style="color: #e5f5e0;" id="countdown">Loading countdown...</h1>
|
||||
<a href="events" class="theme-btn style-two bgc-secondary" style="margin-top: 20px; background-color: #e90000; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;">
|
||||
<span data-hover="Events">Find out more!</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Popular Destinations Area start -->
|
||||
<!-- <section class="popular-destinations-area rel z-1">
|
||||
<div class="container-fluid">
|
||||
<div class="popular-destinations-wrap br-20 bgc-lighter pt-100 pb-70">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-12">
|
||||
<div class="section-title text-center counter-text-wrap mb-70" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<h2>Explore Popular Destinations</h2>
|
||||
<p>One site <span class="count-text plus" data-speed="3000" data-stop="34500">0</span> most popular experience</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="destination-item style-two" data-aos="flip-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image">
|
||||
<a href="#" class="heart"><i class="fas fa-heart"></i></a>
|
||||
<img src="assets/images/destinations/destination1.jpg" alt="Destination">
|
||||
</div>
|
||||
<div class="content">
|
||||
<h6><a href="destination-details.html">Thailand beach</a></h6>
|
||||
<span class="time">5352+ tours & 856+ Activity</span>
|
||||
<a href="#" class="more"><i class="fas fa-chevron-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="destination-item style-two" data-aos="flip-up" data-aos-delay="100" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image">
|
||||
<a href="#" class="heart"><i class="fas fa-heart"></i></a>
|
||||
<img src="assets/images/destinations/destination2.jpg" alt="Destination">
|
||||
</div>
|
||||
<div class="content">
|
||||
<h6><a href="destination-details.html">Parga, Greece</a></h6>
|
||||
<span class="time">5352+ tours & 856+ Activity</span>
|
||||
<a href="#" class="more"><i class="fas fa-chevron-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="destination-item style-two" data-aos="flip-up" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image">
|
||||
<a href="#" class="heart"><i class="fas fa-heart"></i></a>
|
||||
<img src="assets/images/destinations/destination3.jpg" alt="Destination">
|
||||
</div>
|
||||
<div class="content">
|
||||
<h6><a href="destination-details.html">Castellammare del Golfo, Italy</a></h6>
|
||||
<span class="time">5352+ tours & 856+ Activity</span>
|
||||
<a href="#" class="more"><i class="fas fa-chevron-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="destination-item style-two" data-aos="flip-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image">
|
||||
<a href="#" class="heart"><i class="fas fa-heart"></i></a>
|
||||
<img src="assets/images/destinations/destination4.jpg" alt="Destination">
|
||||
</div>
|
||||
<div class="content">
|
||||
<h6><a href="destination-details.html">Reserve of Canada, Canada</a></h6>
|
||||
<span class="time">5352+ tours & 856+ Activity</span>
|
||||
<a href="#" class="more"><i class="fas fa-chevron-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="destination-item style-two" data-aos="flip-up" data-aos-delay="100" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image">
|
||||
<a href="#" class="heart"><i class="fas fa-heart"></i></a>
|
||||
<img src="assets/images/destinations/destination5.jpg" alt="Destination">
|
||||
</div>
|
||||
<div class="content">
|
||||
<h6><a href="destination-details.html">Dubai united states</a></h6>
|
||||
<span class="time">5352+ tours & 856+ Activity</span>
|
||||
<a href="#" class="more"><i class="fas fa-chevron-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="destination-item style-two" data-aos="flip-up" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image">
|
||||
<a href="#" class="heart"><i class="fas fa-heart"></i></a>
|
||||
<img src="assets/images/destinations/destination6.jpg" alt="Destination">
|
||||
</div>
|
||||
<div class="content">
|
||||
<h6><a href="destination-details.html">Milos, Greece</a></h6>
|
||||
<span class="time">5352+ tours & 856+ Activity</span>
|
||||
<a href="#" class="more"><i class="fas fa-chevron-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section> -->
|
||||
<!-- Popular Destinations Area end -->
|
||||
|
||||
|
||||
<!-- Features Area start -->
|
||||
<section class="features-area pt-100 pb-45 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-xl-6">
|
||||
<div class="features-content-part mb-55" data-aos="fade-left" data-aos-duration="1500"
|
||||
data-aos-offset="50">
|
||||
<div class="section-title mb-20">
|
||||
<h2><b>BASE 4:</b> The home of 4WDCSA.</h2>
|
||||
<p>Situated near the Hennops river, in Doornrandjie, Centurion.</p>
|
||||
<div class="image">
|
||||
<img style="border-radius:10px;" src="assets/images/base4/base4.jpg" alt="Hotel">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="menu-btns py-10">
|
||||
<a href="membership.php" class="theme-btn style-two bgc-secondary">
|
||||
<span data-hover="Become a Member">Become a Member</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
<!-- <div class="menu-btns py-10">
|
||||
<a href="campsite_booking" class="theme-btn style-two bgc-secondary">
|
||||
<span data-hover="Book a Campsite">Book a Campsite</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div> -->
|
||||
|
||||
|
||||
|
||||
<!-- <div class="features-customer-box">
|
||||
<div class="image">
|
||||
<img src="assets/images/features/features-box.jpg" alt="Features">
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="feature-authors mb-15">
|
||||
<img src="assets/images/features/feature-author1.jpg" alt="Author">
|
||||
<img src="assets/images/features/feature-author2.jpg" alt="Author">
|
||||
<img src="assets/images/features/feature-author3.jpg" alt="Author">
|
||||
<span>4k+</span>
|
||||
</div>
|
||||
<h6>850K+ Happy Customer</h6>
|
||||
<div class="divider style-two counter-text-wrap my-25"><span><span class="count-text plus" data-speed="3000" data-stop="25">0</span> Years</span></div>
|
||||
<p>We pride ourselves offering personalized itineraries</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="row pb-25">
|
||||
<div class="col-md-6">
|
||||
<div class="feature-item">
|
||||
<div class="icon"><i class="flaticon-tent"></i></div>
|
||||
<div class="content">
|
||||
<h5><a href="trip-details.php">Club House</a></h5>
|
||||
<p>We are currently in the process of building a new club house since the previous club house tragically burnt down in November of 2024.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="icon"><i class="flaticon-tent"></i></div>
|
||||
<div class="content">
|
||||
<h5><a href="trip-details.php">4x4 Training Track</a></h5>
|
||||
<p>Test your offroad driving skills on our training track with many obstacles
|
||||
from rocky climbs, daring axle twisters, log bridge, side slopes and more!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="feature-item mt-20">
|
||||
<div class="icon"><i class="flaticon-tent"></i></div>
|
||||
<div class="content">
|
||||
<h5><a href="trip-details.php">24/7 Camping</a></h5>
|
||||
<p>Pristene Camping grounds situated next to a stream, with ablutions, lapa and
|
||||
communal fire pits.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="icon"><i class="flaticon-tent"></i></div>
|
||||
<div class="content">
|
||||
<h5><a href="trip-details.php">Swimming pool & Braai areas</a></h5>
|
||||
<p>Unwind with a refreshing dip in our crystal-clear swimming pool or gather around the braai area for good food and great company</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Features Area end -->
|
||||
|
||||
|
||||
<!-- Hotel Area start -->
|
||||
<section class="hotel-area bgc-black py-100 rel z-1">
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-12">
|
||||
<div class="section-title text-white text-center counter-text-wrap mb-70" data-aos="fade-up"
|
||||
data-aos-duration="1500" data-aos-offset="50">
|
||||
<h2>Driver Training Courses</h2>
|
||||
<p>Discover the in's and out's of your Four Wheel Drive with one of our dedicated training
|
||||
courses:</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xxl-6 col-xl-8 col-lg-10">
|
||||
<div class="destination-item style-three" data-aos="fade-up" data-aos-duration="1500"
|
||||
data-aos-offset="50">
|
||||
<div class="image">
|
||||
<!-- <div class="ratting"><i class="fas fa-star"></i> 4.8</div> -->
|
||||
<!-- <a href="#" class="heart"><i class="fas fa-heart"></i></a> -->
|
||||
<img src="assets/images/courses/driver_training.png" alt="Hotel">
|
||||
</div>
|
||||
<div class="content">
|
||||
<span class="location"><i class="fal fa-map-marker-alt"></i> BASE4, Hennops</span>
|
||||
<h5><a href="driver_training">Basic 4X4 Driver Training</a></h5>
|
||||
<ul class="list-style-three">
|
||||
<li>Master Off-Road Confidence</li>
|
||||
<li>Hands-On Training</li>
|
||||
<li>Safety First</li>
|
||||
<!-- <li><i class="fal fa-router"></i> Internet</li> -->
|
||||
</ul>
|
||||
<div class="destination-footer">
|
||||
<span class="price"><span>R <?= getPrice('driver_training', 'member'); ?></span>/for members</span>
|
||||
<span class="price"><span>R <?= getPrice('driver_training', 'nonmember'); ?></span>/for non-members</span>
|
||||
<a href="driver_training" class="read-more">Book Now <i class="fal fa-angle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xxl-6 col-xl-8 col-lg-10">
|
||||
<div class="destination-item style-three" data-aos="fade-up" data-aos-delay="50"
|
||||
data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image">
|
||||
<!-- <div class="ratting"><i class="fas fa-star"></i> 4.8</div> -->
|
||||
<!-- <a href="#" class="heart"><i class="fas fa-heart"></i></a> -->
|
||||
<img src="assets/images/courses/bush_mechanics.png" alt="Hotel">
|
||||
</div>
|
||||
<div class="content">
|
||||
<span class="location"><i class="fal fa-map-marker-alt"></i> BASE4, Hennops</span>
|
||||
<h5><a href="bush_mechanics">Bush Mechanics Course</a></h5>
|
||||
<ul class="list-style-three">
|
||||
<li>Fix Your Vehicle in the Wild</li>
|
||||
<li>Survival Skills for Off-Roaders</li>
|
||||
<li>Hands-On Experience</li>
|
||||
<!-- <li><i class="fal fa-router"></i> Internet</li> -->
|
||||
</ul>
|
||||
<div class="destination-footer">
|
||||
<span class="price"><span>R <?= getPrice('bush_mechanics', 'member'); ?></span>/for members</span>
|
||||
<span class="price"><span>R <?= getPrice('bush_mechanics', 'nonmember'); ?></span>/for non-members</span>
|
||||
<a href="bush_mechanics" class="read-more">Book Now <i class="fal fa-angle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xxl-6 col-xl-8 col-lg-10">
|
||||
<div class="destination-item style-three" data-aos="fade-up" data-aos-duration="1500"
|
||||
data-aos-offset="50">
|
||||
<div class="content">
|
||||
<span class="location"><i class="fal fa-map-marker-alt"></i> BASE4, Hennops</span>
|
||||
<h5><a href="rescue_recovery">Rescue & Recovery Course</a></h5>
|
||||
<ul class="list-style-three">
|
||||
<li>Master Advanced Recovery Techniques</li>
|
||||
<li>Gain Confidence in High-Stress Situations</li>
|
||||
<li>Teamwork and Communication</li>
|
||||
<!-- <li><i class="fal fa-router"></i> Internet</li> -->
|
||||
</ul>
|
||||
<div class="destination-footer">
|
||||
<span class="price"><span>R <?= getPrice('rescue_recovery', 'member'); ?></span>/for members</span>
|
||||
<span class="price"><span>R <?= getPrice('rescue_recovery', 'nonmember'); ?></span>/for non-members</span>
|
||||
<a href="rescue_recovery" class="read-more">Book Now <i class="fal fa-angle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image">
|
||||
<!-- <div class="ratting"><i class="fas fa-star"></i> 4.8</div> -->
|
||||
<!-- <a href="#" class="heart"><i class="fas fa-heart"></i></a> -->
|
||||
<img src="assets/images/courses/rescue_recovery.png" alt="Hotel">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="hotel-more-btn text-center mt-40">
|
||||
<a href="destination2.html" class="theme-btn style-four">
|
||||
<span data-hover="Explore More Hotel">Explore More Hotel</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div> -->
|
||||
</div>
|
||||
</section>
|
||||
<!-- Hotel Area end -->
|
||||
|
||||
<!-- CTA Area start -->
|
||||
<!-- <section class="cta-area pt-100 rel z-1">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xl-4 col-md-6" data-aos="zoom-in-down" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="cta-item" style="background-image: url(assets/images/cta/cta1.jpg);">
|
||||
<span class="category">Tent Camping</span>
|
||||
<h2>Explore the world best tourism</h2>
|
||||
<a href="trip-details.php" class="theme-btn style-two bgc-secondary">
|
||||
<span data-hover="Explore Tours">Explore Tours</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4 col-md-6" data-aos="zoom-in-down" data-aos-delay="50" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="cta-item" style="background-image: url(assets/images/cta/cta2.jpg);">
|
||||
<span class="category">Sea Beach</span>
|
||||
<h2>World largest Sea Beach in Thailand</h2>
|
||||
<a href="trip-details.php" class="theme-btn style-two">
|
||||
<span data-hover="Explore Tours">Explore Tours</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4 col-md-6" data-aos="zoom-in-down" data-aos-delay="100" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="cta-item" style="background-image: url(assets/images/cta/cta3.jpg);">
|
||||
<span class="category">Water Falls</span>
|
||||
<h2>Largest Water falls Bali, Indonesia</h2>
|
||||
<a href="trip-details.php" class="theme-btn style-two bgc-secondary">
|
||||
<span data-hover="Explore Tours">Explore Tours</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section> -->
|
||||
<!-- CTA Area end -->
|
||||
|
||||
|
||||
<!-- Blog Area start -->
|
||||
<section class="blog-area py-70 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-12">
|
||||
<div class="section-title text-center counter-text-wrap mb-70" data-aos="fade-up"
|
||||
data-aos-duration="1500" data-aos-offset="50">
|
||||
<h2>Read about our past trips and events</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<?php
|
||||
$sql = "SELECT blog_id, title, date, category, image, description, author, link, members_only FROM blogs ORDER BY date DESC LIMIT 3";
|
||||
$result = $conn->query($sql);
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
// Loop through each row
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$blog_id = $row['blog_id'];
|
||||
$blog_title = $row['title'];
|
||||
$blog_date = $row['date'];
|
||||
$blog_category = $row['category'];
|
||||
$blog_image = $row['image'];
|
||||
$blog_description = $row['description'];
|
||||
$blog_author = $row['author'];
|
||||
$members_only = $row['members_only'];
|
||||
if ($members_only) {
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
$blog_link = "login.php";
|
||||
$button_hover = "Members Only";
|
||||
$icon = "fa-lock";
|
||||
} else {
|
||||
if (getUserMemberStatus($_SESSION['user_id'])) {
|
||||
$blog_link = $row['link'];
|
||||
$button_hover = "Read More";
|
||||
$icon = "fa-arrow-right";
|
||||
} else {
|
||||
$blog_link = "membership.php";
|
||||
$button_hover = "Members Only";
|
||||
$icon = "fa-lock";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$blog_link = $row['link'];
|
||||
$button_hover = "Read More";
|
||||
$icon = "fa-arrow-right";
|
||||
}
|
||||
|
||||
|
||||
|
||||
echo '
|
||||
<div class="col-xl-4 col-md-6">
|
||||
<div class="blog-item" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="content">
|
||||
<a href="#" class="category">' . $blog_category . '</a>
|
||||
<h5><a href="' . $blog_link . '">' . $blog_title . '</a></h5>
|
||||
<ul class="blog-meta">
|
||||
<li><i class="far fa-calendar-alt"></i> <a href="#">' . $blog_date . '</a></li>
|
||||
<li><i class="far fa-user"></i>' . getFullName($blog_author) . '</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="image">
|
||||
<img style="border-radius:20px;" src="assets/images/blog/' . $blog_id . '/' . $blog_image . '" alt="Blog List">
|
||||
</div>
|
||||
<a style="width:100%;" href="' . $blog_link . '" class="theme-btn">
|
||||
<span style="width:100%;" data-hover="' . $button_hover . '">Read More</span>
|
||||
<i class="fal ' . $icon . '"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
// Close connection
|
||||
$conn->close();
|
||||
} ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Blog Area end -->
|
||||
|
||||
<section class="bgc-black py-20 rel z-1">
|
||||
|
||||
<?php include_once('ad_banner.php'); ?>
|
||||
|
||||
</section>
|
||||
<section class="py-20 rel z-1">
|
||||
|
||||
<?php include_once('logos.php'); ?>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<!-- footer area start -->
|
||||
<footer class="main-footer bgs-cover overlay rel z-1 pb-25"
|
||||
style="background-image: url(assets/images/backgrounds/footer.jpg);">
|
||||
<div class="container">
|
||||
|
||||
<div class="footer-top pt-100 pb-30">
|
||||
<div class="row justify-content-between">
|
||||
<div class="col-xl-5 col-lg-6" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="footer-widget footer-contact">
|
||||
<a href="https://chat.whatsapp.com/JD9xQuJlVX5AAJwcLrpl2B" target="_blank" style="text-decoration: none; color: inherit;">
|
||||
<div style="display: flex; align-items: center; background-color: #e5f5e0; border-radius: 10px; padding: 10px; max-width: 100%; box-shadow: 0 2px 6px rgba(0,0,0,0.1);">
|
||||
<img src="assets/images/icons/whatsapp.png" alt="WhatsApp" style="width: 64px; height: 64px; margin-right: 15px;">
|
||||
<h1 style="margin: 0; font-size: 24px;">Join our WhatsApp Group</h1>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="footer-widget footer-contact">
|
||||
<div class="footer-title">
|
||||
<h5>Get In Touch</h5>
|
||||
</div>
|
||||
<ul class="list-style-one">
|
||||
<li><i class="fal fa-map-marked-alt"></i> Plot 50 Gemstone Rd, Doornrandje, Centurion, 0157</li>
|
||||
<li><i class="fal fa-envelope"></i> <a
|
||||
href="mailto:info@4wdcsa.co.za">info@4wdcsa.co.za</a></li>
|
||||
<li><i class="fal fa-clock"></i> Mon - Fri, 09:00 - 17:00</li>
|
||||
<li><i class="fal fa-phone-volume"></i> <a href="callto:+2779 065 2795">079 065 2795</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-5 col-lg-6" data-aos="fade-up" data-aos-delay="50" data-aos-duration="1500"
|
||||
data-aos-offset="50">
|
||||
<div class="section-title counter-text-wrap mb-35">
|
||||
<h2>Subscribe to our Mailing List</h2>
|
||||
<p>Receive news and updates about upcoming trips and events.</p>
|
||||
</div>
|
||||
|
||||
<div id="mc_embed_shell">
|
||||
|
||||
<div id="mc_embed_signup">
|
||||
<form class="newsletter-form mb-50" action="https://fwdcsa.us17.list-manage.com/subscribe/post?u=3c26590bcc200ef52edc0bec2&id=3c370893eb&f_id=0099ebe3f0" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_self" novalidate="">
|
||||
<div id="mc_embed_signup_scroll" style="width:100%;">
|
||||
<div class="mc-field-group"></label><input type="email" name="EMAIL" class="required email" id="mce-EMAIL" required="" value="" placeholder="Email"></div>
|
||||
<div class="mc-field-group"><input type="text" name="FNAME" class=" text" id="mce-FNAME" value="" placeholder="First Name"></div>
|
||||
<div class="mc-field-group"><input type="text" name="LNAME" class=" text" id="mce-LNAME" value="" placeholder="Last Name"></div>
|
||||
<div class="mc-field-group"><input type="text" name="PHONE" class="REQ_CSS" id="mce-PHONE" value="" placeholder="Phone Number"></div>
|
||||
<div hidden=""><input type="hidden" name="tags" value="8324220"></div>
|
||||
<div id="mce-responses" class="clear">
|
||||
<div class="response" id="mce-error-response" style="display: none;"></div>
|
||||
<div class="response" id="mce-success-response" style="display: none;"></div>
|
||||
</div>
|
||||
<div aria-hidden="true" style="position: absolute; left: -5000px;"><input type="text" name="b_3c26590bcc200ef52edc0bec2_3c370893eb" tabindex="-1" value=""></div>
|
||||
<div class="clear"><input style="width:100%;" type="submit" name="subscribe" id="mc-embedded-subscribe" class="theme-btn bgc-secondary style-two" value="Subscribe"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
<form class="newsletter-form mb-50" action="#">
|
||||
<input id="news-email" type="email" placeholder="Email Address" required>
|
||||
<button type="submit" class="theme-btn bgc-secondary style-two">
|
||||
<span data-hover="Subscribe">Subscribe</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
</form> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom pt-20 pb-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-5">
|
||||
<div class="copyright-text text-center text-lg-start">
|
||||
<p>Copyright © <?php echo date("Y"); ?> <a href="index.html">4WDCSA</a> | All rights reserved.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-7 text-center text-lg-end">
|
||||
<ul class="footer-bottom-nav">
|
||||
<li><a href="privacy_policy">Privacy Policy</a></li>
|
||||
<!-- <li><a href="about.html">Terms</a></li> -->
|
||||
<!-- <li><a href="about.html">Privacy Policy</a></li> -->
|
||||
<!-- <li><a href="about.html">Legal notice</a></li> -->
|
||||
<!-- <li><a href="about.html">Accessibility</a></li> -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Scroll Top Button -->
|
||||
<button class="scroll-top scroll-to-target" data-target="html"><img
|
||||
src="assets/images/icons/scroll-up.png" alt="Scroll Up"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
<!-- footer area end -->
|
||||
|
||||
</div>
|
||||
<!--End pagewrapper-->
|
||||
<?php if ($indemnityPending): ?>
|
||||
<!-- Bootstrap Modal -->
|
||||
<div class="modal fade" id="indemnityModal" tabindex="-1" aria-labelledby="indemnityModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-secondary">
|
||||
<div class="modal-header bg-secondary text-white">
|
||||
<h5 class="modal-title" id="indemnityModalLabel">Membership Application Incomplete</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
To link your existing FWDCSA membership, you need to sign and accept the indemnity aggreement before proceeding.<br>
|
||||
<a style="width:100%; border-radius:20px;" href="indemnity" class="btn btn-danger mt-3">Review and Accept</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Show modal when page loads
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
var indemnityModal = new bootstrap.Modal(document.getElementById('indemnityModal'));
|
||||
indemnityModal.show();
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<!-- Jquery -->
|
||||
<script src="assets/js/jquery-3.6.0.min.js"></script>
|
||||
<!-- Bootstrap -->
|
||||
<script src="assets/js/bootstrap.min.js"></script>
|
||||
<!-- Appear Js -->
|
||||
<script src="assets/js/appear.min.js"></script>
|
||||
<!-- Slick -->
|
||||
<script src="assets/js/slick.min.js"></script>
|
||||
<!-- Magnific Popup -->
|
||||
<script src="assets/js/jquery.magnific-popup.min.js"></script>
|
||||
<!-- Nice Select -->
|
||||
<script src="assets/js/jquery.nice-select.min.js"></script>
|
||||
<!-- Image Loader -->
|
||||
<script src="assets/js/imagesloaded.pkgd.min.js"></script>
|
||||
<!-- Skillbar -->
|
||||
<script src="assets/js/skill.bars.jquery.min.js"></script>
|
||||
<!-- Isotope -->
|
||||
<script src="assets/js/isotope.pkgd.min.js"></script>
|
||||
<!-- AOS Animation -->
|
||||
<script src="assets/js/aos.js"></script>
|
||||
<!-- Custom script -->
|
||||
<script src="assets/js/script.js"></script>
|
||||
<script>
|
||||
// Set your target date and time
|
||||
const targetDate = new Date("<?php echo getNextOpenDayDate(); ?>T08:00:00"); // yyyy-mm-ddThh:mm:ss
|
||||
|
||||
function updateCountdown() {
|
||||
const now = new Date();
|
||||
const diff = targetDate - now;
|
||||
|
||||
if (diff <= 0) {
|
||||
document.getElementById("countdown").innerHTML = "We're open now!";
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((diff / (1000 * 60 * 60)) % 24);
|
||||
const minutes = Math.floor((diff / (1000 * 60)) % 60);
|
||||
const seconds = Math.floor((diff / 1000) % 60);
|
||||
|
||||
document.getElementById("countdown").innerHTML =
|
||||
`${String(days).padStart(2, '0')} days ` +
|
||||
`${String(hours).padStart(2, '0')} hours ` +
|
||||
`${String(minutes).padStart(2, '0')} minutes ` +
|
||||
`${String(seconds).padStart(2, '0')} seconds<br>` +
|
||||
`till our next BASE4 Open Day!`;
|
||||
}
|
||||
|
||||
updateCountdown(); // initial call
|
||||
setInterval(updateCountdown, 1000);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
54
src/processors/create_bar_tab.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/session.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
// CSRF Token Validation
|
||||
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Security token validation failed.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Check if user_id is set in the POST request
|
||||
if (isset($_POST['user_id']) && !empty($_POST['user_id'])) {
|
||||
// Validate user_id as integer
|
||||
$user_id = intval($_POST['user_id']);
|
||||
if ($user_id <= 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid user ID.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$created_at = date('Y-m-d H:i:s'); // Use current date and time for created_at
|
||||
|
||||
// First, check if a bar tab already exists for this user_id
|
||||
$stmt = $conn->prepare("SELECT * FROM bar_tabs WHERE user_id = ? LIMIT 1");
|
||||
$stmt->bind_param("i", $user_id);
|
||||
$stmt->execute();
|
||||
$checkResult = $stmt->get_result();
|
||||
|
||||
if ($checkResult->num_rows > 0) {
|
||||
// If a bar tab already exists for this user_id, return an error message
|
||||
echo json_encode(['status' => 'error', 'message' => 'A bar tab already exists for this user.']);
|
||||
} else {
|
||||
// Prepare the SQL query to insert a new record into the bar_tabs table
|
||||
$stmt = $conn->prepare("INSERT INTO bar_tabs (user_id) VALUES (?)");
|
||||
$stmt->bind_param("i", $user_id);
|
||||
|
||||
// Execute the query
|
||||
if ($stmt->execute()) {
|
||||
// If the insertion is successful, return a success message
|
||||
echo json_encode(['status' => 'success', 'message' => 'Bar tab created successfully.']);
|
||||
} else {
|
||||
// If there's an error, return an error message
|
||||
echo json_encode(['status' => 'error', 'message' => 'Error: ' . $conn->error]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If user_id is not provided, return an error message
|
||||
echo json_encode(['status' => 'error', 'message' => 'User ID is required.']);
|
||||
}
|
||||
?>
|
||||
|
||||
|
||||
60
src/processors/delete_trip.php
Normal 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()]);
|
||||
}
|
||||
?>
|
||||
14
src/processors/logout.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
|
||||
|
||||
// Destroy the session
|
||||
session_destroy();
|
||||
|
||||
|
||||
|
||||
// Redirect to login page
|
||||
header("Location: index"); // Replace with your actual login page URL
|
||||
exit();
|
||||
?>
|
||||
202
src/processors/process_application.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/session.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
$eft_id = strtoupper($user_id." SUBS ".date("Y")." ".getInitialSurname($user_id));
|
||||
$status = 'AWAITING PAYMENT';
|
||||
$description = 'Membership Fees '.date("Y")." ".getInitialSurname($user_id);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF Token Validation
|
||||
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
||||
auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'membership_application', null, ['endpoint' => 'process_application.php']);
|
||||
http_response_code(403);
|
||||
die('Security token validation failed. Please try again.');
|
||||
}
|
||||
|
||||
// Get all the form fields with validation
|
||||
$first_name = validateName($_POST['first_name'] ?? '');
|
||||
if ($first_name === false) {
|
||||
die('Invalid first name format.');
|
||||
}
|
||||
|
||||
$last_name = validateName($_POST['last_name'] ?? '');
|
||||
if ($last_name === false) {
|
||||
die('Invalid last name format.');
|
||||
}
|
||||
|
||||
$id_number = validateSAIDNumber($_POST['id_number'] ?? '');
|
||||
if ($id_number === false) {
|
||||
die('Invalid ID number format.');
|
||||
}
|
||||
|
||||
$dob = validateDate($_POST['dob'] ?? '');
|
||||
if ($dob === false) {
|
||||
die('Invalid date of birth format.');
|
||||
}
|
||||
|
||||
$occupation = sanitizeTextInput($_POST['occupation'] ?? '', 100);
|
||||
$tel_cell = validatePhoneNumber($_POST['tel_cell'] ?? '');
|
||||
if ($tel_cell === false) {
|
||||
die('Invalid phone number format.');
|
||||
}
|
||||
|
||||
$email = validateEmail($_POST['email'] ?? '');
|
||||
if ($email === false) {
|
||||
die('Invalid email format.');
|
||||
}
|
||||
|
||||
// Spouse or Partner details (optional)
|
||||
$spouse_first_name = !empty($_POST['spouse_first_name']) ? validateName($_POST['spouse_first_name']) : null;
|
||||
$spouse_last_name = !empty($_POST['spouse_last_name']) ? validateName($_POST['spouse_last_name']) : null;
|
||||
$spouse_id_number = !empty($_POST['spouse_id_number']) ? validateSAIDNumber($_POST['spouse_id_number']) : null;
|
||||
$spouse_dob = !empty($_POST['spouse_dob']) ? validateDate($_POST['spouse_dob']) : NULL;
|
||||
$spouse_occupation = !empty($_POST['spouse_occupation']) ? sanitizeTextInput($_POST['spouse_occupation'], 100) : null;
|
||||
$spouse_tel_cell = !empty($_POST['spouse_tel_cell']) ? validatePhoneNumber($_POST['spouse_tel_cell']) : null;
|
||||
$spouse_email = !empty($_POST['spouse_email']) ? validateEmail($_POST['spouse_email']) : null;
|
||||
|
||||
// Children details (optional)
|
||||
$child_name1 = !empty($_POST['child_name1']) ? $_POST['child_name1'] : null;
|
||||
$child_dob1 = !empty($_POST['child_dob1']) ? $_POST['child_dob1'] : null;
|
||||
$child_name2 = !empty($_POST['child_name2']) ? $_POST['child_name2'] : null;
|
||||
$child_dob2 = !empty($_POST['child_dob2']) ? $_POST['child_dob2'] : null;
|
||||
$child_name3 = !empty($_POST['child_name3']) ? $_POST['child_name3'] : null;
|
||||
$child_dob3 = !empty($_POST['child_dob3']) ? $_POST['child_dob3'] : null;
|
||||
|
||||
// Address and other details
|
||||
$physical_address = $_POST['physical_address'];
|
||||
$postal_address = $_POST['postal_address'];
|
||||
$interests_hobbies = $_POST['interests_hobbies'];
|
||||
|
||||
// Primary vehicle details
|
||||
$vehicle_make = $_POST['vehicle_make'];
|
||||
$vehicle_model = $_POST['vehicle_model'];
|
||||
$vehicle_year = $_POST['vehicle_year'];
|
||||
$vehicle_registration = $_POST['vehicle_registration'];
|
||||
|
||||
// Secondary vehicle details (optional)
|
||||
$secondary_vehicle_make = !empty($_POST['secondary_vehicle_make']) ? $_POST['secondary_vehicle_make'] : null;
|
||||
$secondary_vehicle_model = !empty($_POST['secondary_vehicle_model']) ? $_POST['secondary_vehicle_model'] : null;
|
||||
$secondary_vehicle_year = !empty($_POST['secondary_vehicle_year']) ? $_POST['secondary_vehicle_year'] : null;
|
||||
$secondary_vehicle_registration = !empty($_POST['secondary_vehicle_registration']) ? $_POST['secondary_vehicle_registration'] : null;
|
||||
|
||||
// Start a transaction to ensure data consistency
|
||||
$conn->begin_transaction();
|
||||
|
||||
try {
|
||||
// Insert into the member application table
|
||||
$stmt = $conn->prepare("INSERT INTO membership_application (
|
||||
user_id, first_name, last_name, id_number, dob, occupation, tel_cell, email,
|
||||
spouse_first_name, spouse_last_name, spouse_id_number, spouse_dob, spouse_occupation, spouse_tel_cell, spouse_email,
|
||||
child_name1, child_dob1, child_name2, child_dob2, child_name3, child_dob3,
|
||||
physical_address, postal_address, interests_hobbies, vehicle_make, vehicle_model, vehicle_year, vehicle_registration,
|
||||
secondary_vehicle_make, secondary_vehicle_model, secondary_vehicle_year, secondary_vehicle_registration
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
|
||||
// Check if preparation was successful
|
||||
if (!$stmt) {
|
||||
die("SQL error: " . $conn->error);
|
||||
}
|
||||
|
||||
$stmt->bind_param(
|
||||
"isssssssssssssssssssssssssssssss",
|
||||
$user_id,
|
||||
$first_name,
|
||||
$last_name,
|
||||
$id_number,
|
||||
$dob,
|
||||
$occupation,
|
||||
$tel_cell,
|
||||
$email,
|
||||
$spouse_first_name,
|
||||
$spouse_last_name,
|
||||
$spouse_id_number,
|
||||
$spouse_dob,
|
||||
$spouse_occupation,
|
||||
$spouse_tel_cell,
|
||||
$spouse_email,
|
||||
$child_name1,
|
||||
$child_dob1,
|
||||
$child_name2,
|
||||
$child_dob2,
|
||||
$child_name3,
|
||||
$child_dob3,
|
||||
$physical_address,
|
||||
$postal_address,
|
||||
$interests_hobbies,
|
||||
$vehicle_make,
|
||||
$vehicle_model,
|
||||
$vehicle_year,
|
||||
$vehicle_registration,
|
||||
$secondary_vehicle_make,
|
||||
$secondary_vehicle_model,
|
||||
$secondary_vehicle_year,
|
||||
$secondary_vehicle_registration
|
||||
);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Insert into the membership fees table
|
||||
$payment_amount = calculateProrata(210); // Assuming a fixed membership fee, adjust as needed
|
||||
$payment_date = date('Y-m-d');
|
||||
$membership_start_date = $payment_date;
|
||||
// $membership_end_date = date('Y-12-31');
|
||||
|
||||
// Get today's date
|
||||
$today = new DateTime();
|
||||
|
||||
// Determine the target February
|
||||
if ($today->format('n') > 2) {
|
||||
// If we're past February, target is next year's Feb 28/29
|
||||
$year = $today->format('Y') + 1;
|
||||
} else {
|
||||
// Otherwise, this year's February
|
||||
$year = $today->format('Y');
|
||||
}
|
||||
|
||||
// Handle leap year (Feb 29) automatically
|
||||
$membership_end_date = (new DateTime("$year-02-01"))
|
||||
->modify('last day of this month')
|
||||
->format('Y-m-d');
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, payment_status, payment_id)
|
||||
VALUES (?, ?, ?, ?, ?, 'PENDING', ?)");
|
||||
$stmt->bind_param("idssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $eft_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Commit the transaction
|
||||
$conn->commit();
|
||||
addSubsEFT($eft_id, $user_id, $status, $payment_amount, $description);
|
||||
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
sendAdminNotification('4WDCSA.co.za - New Membership Application - '.$last_name , 'A new member has signed up, '.$first_name.' '.$last_name);
|
||||
header("Location: indemnity");
|
||||
// Success message
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
'message' => 'Your membership application has been submitted successfully!'
|
||||
];
|
||||
} else {
|
||||
throw new Exception("Failed to insert membership fee. SQL error: " . $conn->error);
|
||||
}
|
||||
} else {
|
||||
throw new Exception("Failed to insert member application.SQL error: " . $conn->error);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Rollback the transaction in case of error
|
||||
$conn->rollback();
|
||||
|
||||
// Error response
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'message' => 'Error: ' . $e->getMessage()
|
||||
];
|
||||
}
|
||||
|
||||
// Return the response in JSON format
|
||||
echo json_encode($response);
|
||||
}
|
||||
?>
|
||||
|
||||
95
src/processors/process_booking.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
// Start session to retrieve the logged-in user's ID
|
||||
session_start();
|
||||
|
||||
// Get user ID from session (assuming user is logged in)
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
|
||||
// Check if the form has been submitted
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF Token Validation
|
||||
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
||||
auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'bookings', null, ['endpoint' => 'process_booking.php']);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Security token validation failed. Please try again.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Validate dates and integers
|
||||
$from_date = validateDate($_POST['from_date'] ?? '');
|
||||
if ($from_date === false) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid from date format.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$to_date = validateDate($_POST['to_date'] ?? '');
|
||||
if ($to_date === false) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid to date format.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$num_vehicles = validateInteger($_POST['vehicles'] ?? 0, 1, 10);
|
||||
if ($num_vehicles === false) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid number of vehicles.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$num_adults = validateInteger($_POST['adults'] ?? 0, 0, 20);
|
||||
if ($num_adults === false) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid number of adults.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$num_children = validateInteger($_POST['children'] ?? 0, 0, 20);
|
||||
if ($num_children === false) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid number of children.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Get values from the form
|
||||
$add_firewood = isset($_POST['AddExtra']) ? 1 : 0; // Checkbox for extras
|
||||
$is_member = isset($_POST['is_member']) ? (int)$_POST['is_member'] : 0; // Hidden member status
|
||||
$type = "camping";
|
||||
|
||||
// Calculate the total number of nights
|
||||
$date1 = new DateTime($from_date);
|
||||
$date2 = new DateTime($to_date);
|
||||
$nights = $date2->diff($date1)->days;
|
||||
|
||||
// Determine rate per night
|
||||
$rate_per_night = ($is_member) ? 0 : 200; // Free for members, R200 for non-members
|
||||
|
||||
// Calculate the total cost
|
||||
$vehicle_cost = $rate_per_night * $num_vehicles * $nights;
|
||||
$firewood_cost = $add_firewood ? 50 : 0;
|
||||
$total_amount = $vehicle_cost + $firewood_cost;
|
||||
|
||||
// Calculate discount if the user is a member
|
||||
$discount_amount = ($is_member) ? $vehicle_cost : 0;
|
||||
|
||||
// Insert booking into the database
|
||||
$sql = "INSERT INTO bookings (booking_type, user_id, from_date, to_date, num_vehicles, num_adults, num_children, add_firewood, total_amount, discount_amount)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param('sissiiiidd', $type, $user_id, $from_date, $to_date, $num_vehicles, $num_adults, $num_children, $add_firewood, $total_amount, $discount_amount);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Redirect to success page or display success message
|
||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'booking.php';</script>";
|
||||
} else {
|
||||
// Handle error if insert fails
|
||||
echo "<script>alert('Error processing booking. Please try again later.');</script>";
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
} else {
|
||||
echo "Invalid request.";
|
||||
}
|
||||
?>
|
||||
|
||||
146
src/processors/process_camp_booking.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
// Start session to retrieve the logged-in user's ID
|
||||
session_start();
|
||||
|
||||
// Get user ID from session (assuming user is logged in)
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
|
||||
// Validate user session
|
||||
if (!$user_id) {
|
||||
echo "<script>alert('User is not logged in. Please log in to make a booking.'); window.location.href = 'login.php';</script>";
|
||||
exit();
|
||||
}
|
||||
$is_member = getUserMemberStatus($user_id);
|
||||
|
||||
// Check if the form has been submitted
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF Token Validation
|
||||
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
||||
auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'bookings', null, ['endpoint' => 'process_camp_booking.php']);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Security token validation failed. Please try again.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Validate dates and integers
|
||||
$from_date = validateDate($_POST['from_date'] ?? '');
|
||||
if ($from_date === false) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid from date format.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$to_date = validateDate($_POST['to_date'] ?? '');
|
||||
if ($to_date === false) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid to date format.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$num_vehicles = validateInteger($_POST['vehicles'] ?? 1, 1, 10);
|
||||
if ($num_vehicles === false) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid number of vehicles.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$num_adults = validateInteger($_POST['adults'] ?? 0, 0, 20);
|
||||
if ($num_adults === false) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid number of adults.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$num_children = validateInteger($_POST['children'] ?? 0, 0, 20);
|
||||
if ($num_children === false) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid number of children.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Get values from the form
|
||||
$add_firewood = isset($_POST['AddExtra']) ? 1 : 0; // Checkbox for extras
|
||||
// $is_member = isset($_POST['is_member']) ? (int)$_POST['is_member'] : 0; // Hidden member status
|
||||
$type = "camping";
|
||||
|
||||
// Calculate the total number of nights
|
||||
$date1 = new DateTime($from_date);
|
||||
$date2 = new DateTime($to_date);
|
||||
$nights = $date2->diff($date1)->days;
|
||||
|
||||
// Validate date range
|
||||
if ($nights <= 0) {
|
||||
echo "<script>alert('Invalid date range. Please select valid dates.'); window.history.back();</script>";
|
||||
exit();
|
||||
}
|
||||
|
||||
// Determine rate per night
|
||||
$rate_per_night = 200; // Free for members, R200 for non-members
|
||||
|
||||
// Calculate the total cost
|
||||
$vehicle_cost = $rate_per_night * $num_vehicles * $nights;
|
||||
$total_discount = $is_member ? $vehicle_cost : 0;
|
||||
$firewood_cost = $add_firewood ? 50 : 0;
|
||||
$total_amount = $vehicle_cost + $firewood_cost;
|
||||
$payment_amount = $total_amount - $total_discount;
|
||||
$status = "AWAITING PAYMENT";
|
||||
$description = "BASE4 Camping";
|
||||
|
||||
$payment_id = uniqid();
|
||||
$eft_id = strtoupper($trip_code." ".getLastName($user_id));
|
||||
|
||||
// Insert booking into the database
|
||||
$sql = "INSERT INTO bookings (booking_type, user_id, from_date, to_date, num_vehicles, num_adults, num_children, add_firewood, total_amount, discount_amount, status, payment_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param('sissiiiiddss', $type, $user_id, $from_date, $to_date, $num_vehicles, $num_adults, $num_children, $add_firewood, $total_amount, $total_discount, $status, $payment_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$booking_id = $conn->insert_id;
|
||||
|
||||
if ($payment_amount < 1) {
|
||||
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||
} else {
|
||||
$error_message = $stmt->error;
|
||||
echo "Error processing booking: $error_message";
|
||||
}
|
||||
} else {
|
||||
addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description);
|
||||
header("Location: payment_confirmation?booking_id=".$booking_id);
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
}
|
||||
} else {
|
||||
// Handle error if insert fails and echo the MySQL error
|
||||
$error_message = $stmt->error;
|
||||
echo "Error processing booking: $error_message";
|
||||
}
|
||||
|
||||
// if ($stmt->execute()) {
|
||||
// if ($payment_amount < 1) {
|
||||
// if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||
// echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||
// } else {
|
||||
// $error_message = $stmt->error;
|
||||
// echo "Error processing booking: $error_message";
|
||||
// }
|
||||
// } else {
|
||||
// if (processPayment($payment_id, $payment_amount, $description)) {
|
||||
// echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||
// } else {
|
||||
// $error_message = $stmt->error;
|
||||
// echo "Error processing booking: $error_message";
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // Handle error if insert fails and echo the MySQL error
|
||||
// $error_message = $stmt->error;
|
||||
// echo "Error processing booking: $error_message";
|
||||
// }
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
} else {
|
||||
echo "Invalid request.";
|
||||
}
|
||||
|
||||
145
src/processors/process_course_booking.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
session_start();
|
||||
|
||||
|
||||
// Get user ID from session (assuming user is logged in)
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
|
||||
// Validate user session
|
||||
if (!$user_id) {
|
||||
echo "<script>alert('User is not logged in. Please log in to make a booking.'); window.location.href = 'login.php';</script>";
|
||||
exit();
|
||||
}
|
||||
$is_member = getUserMemberStatus($user_id);
|
||||
$pending_member = getUserMemberStatusPending($user_id);
|
||||
|
||||
// Check if the form has been submitted
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF Token Validation
|
||||
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
||||
auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'bookings', null, ['endpoint' => 'process_course_booking.php']);
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Security token validation failed.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Input variables from the form (use default values if not provided)
|
||||
$additional_members = validateInteger($_POST['members'] ?? 0, 0, 20);
|
||||
if ($additional_members === false) $additional_members = 0;
|
||||
|
||||
$num_adults = validateInteger($_POST['non-members'] ?? 0, 0, 20);
|
||||
if ($num_adults === false) $num_adults = 0;
|
||||
|
||||
$course_id = validateInteger($_POST['course_id'] ?? 0, 1, 999999);
|
||||
if ($course_id === false) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Invalid course ID.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
checkAndRedirectCourseBooking($course_id);
|
||||
// Fetch trip costs from the database
|
||||
$query = "SELECT date, cost_members, cost_nonmembers, course_type FROM courses WHERE course_id = ?";
|
||||
$stmt = $conn->prepare($query);
|
||||
$stmt->bind_param('i', $course_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
// Check if trip exists
|
||||
if ($result->num_rows === 0) {
|
||||
$response = ['error' => 'Trip not found.'];
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch trip details
|
||||
$course = $result->fetch_assoc();
|
||||
$type = $course['course_type'];
|
||||
$date = $course['date'];
|
||||
$cost_members = intval($course['cost_members']);
|
||||
$cost_nonmembers = intval($course['cost_nonmembers']);
|
||||
|
||||
if ($type === "driver_training") {
|
||||
$description = "Basic 4X4 Driver Training Course " . $date;
|
||||
} elseif ($type === "bush_mechanics") {
|
||||
$description = "Bush Mechanics Course " . $date;
|
||||
} elseif ($type === "rescue_recovery") {
|
||||
$description = "Rescue & Recovery Training Course " . $date;
|
||||
} else {
|
||||
$description = "General Course " . $date; // Default fallback description
|
||||
}
|
||||
|
||||
// Initialize total and discount amount
|
||||
$total = 0;
|
||||
|
||||
// Calculate total based on membership
|
||||
if ($is_member || $pending_member) {
|
||||
$num_members = 1 + $additional_members;
|
||||
$total = ($num_members * $cost_members) + ($num_adults * $cost_nonmembers);
|
||||
$payment_amount = $total;
|
||||
} else {
|
||||
$num_members = 0;
|
||||
$total = (($cost_nonmembers) + ($num_adults * $cost_nonmembers));
|
||||
$payment_amount = $total;
|
||||
$num_adults = $num_adults + 1;
|
||||
}
|
||||
|
||||
$status = "AWAITING PAYMENT";
|
||||
$type = 'course';
|
||||
$payment_id = uniqid();
|
||||
$num_vehicles = 1;
|
||||
$discountAmount = 0;
|
||||
$eft_id = strtoupper("COURSE ".date("m-d", strtotime($date))." ".getInitialSurname($user_id));
|
||||
$notes = "";
|
||||
if ($pending_member){
|
||||
$notes = "Membership Payment pending at time of booking. Please confirm payment has been received.";
|
||||
}
|
||||
|
||||
|
||||
// Insert booking into the database
|
||||
$sql = "INSERT INTO bookings (booking_type, user_id, from_date, to_date, num_vehicles, num_adults, total_amount, discount_amount, status, payment_id, course_id, course_non_members, eft_id, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
$stmt = $conn->prepare($sql);
|
||||
|
||||
if (!$stmt) {
|
||||
die("Preparation failed: " . $conn->error);
|
||||
}
|
||||
|
||||
$stmt->bind_param('sissiiddssiiss', $type, $user_id, $date, $date, $num_vehicles, $num_members, $total, $discountAmount, $status, $payment_id, $course_id, $num_adults, $eft_id, $notes);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$booking_id = $conn->insert_id;
|
||||
|
||||
if ($payment_amount < 1) {
|
||||
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||
} else {
|
||||
$error_message = $stmt->error;
|
||||
echo "Error processing booking: $error_message";
|
||||
}
|
||||
} else {
|
||||
addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description);
|
||||
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
sendAdminNotification('New Course Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||
header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
}
|
||||
} else {
|
||||
// Handle error if insert fails and echo the MySQL error
|
||||
$error_message = $stmt->error;
|
||||
echo "Error processing booking: $error_message";
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
} else {
|
||||
echo "Invalid request.";
|
||||
}
|
||||
|
||||
99
src/processors/process_eft.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/session.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
checkAdmin();
|
||||
|
||||
// CSRF Token Validation for POST requests
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
||||
auditLog($_SESSION['user_id'] ?? null, 'CSRF_VALIDATION_FAILED', 'efts', null, ['endpoint' => 'process_eft.php']);
|
||||
http_response_code(403);
|
||||
die('Security token validation failed.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($_GET['token']) || empty($_GET['token'])) {
|
||||
die("Invalid request.");
|
||||
}
|
||||
|
||||
$token = $_GET['token'];
|
||||
// echo $token;
|
||||
$eft_id = decryptData($token, $salt);
|
||||
$user = getUserIdFromEFT($eft_id);
|
||||
|
||||
// echo $eft_id;
|
||||
// Start transaction for atomicity
|
||||
$conn->begin_transaction();
|
||||
|
||||
try {
|
||||
// Update the efts table to set status = 'PAID'
|
||||
$updateEFT = "UPDATE efts SET status = 'PAID' WHERE eft_id = ?";
|
||||
$stmt = $conn->prepare($updateEFT);
|
||||
if (!$stmt) {
|
||||
throw new Exception("Prepare failed: " . $conn->error);
|
||||
}
|
||||
|
||||
$stmt->bind_param("s", $eft_id);
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception("EFT update failed: " . $stmt->error);
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
// Retrieve the booking_id from efts table
|
||||
$getBooking = "SELECT booking_id FROM efts WHERE eft_id = ?";
|
||||
$stmt = $conn->prepare($getBooking);
|
||||
if (!$stmt) {
|
||||
throw new Exception("Prepare failed: " . $conn->error);
|
||||
}
|
||||
|
||||
$stmt->bind_param("s", $eft_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($booking_id);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
if (!empty($booking_id)) {
|
||||
// Update the bookings table if booking_id exists
|
||||
$updateBooking = "UPDATE bookings SET status = 'PAID' WHERE booking_id = ?";
|
||||
$stmt = $conn->prepare($updateBooking);
|
||||
if (!$stmt) {
|
||||
throw new Exception("Prepare failed: " . $conn->error);
|
||||
}
|
||||
|
||||
$stmt->bind_param("i", $booking_id);
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception("Booking update failed: " . $stmt->error);
|
||||
}
|
||||
} else {
|
||||
// If no booking_id is found, update membership_fees instead
|
||||
$updateMembership = "UPDATE membership_fees SET payment_status = 'PAID' WHERE payment_id = ?";
|
||||
$stmt = $conn->prepare($updateMembership);
|
||||
if (!$stmt) {
|
||||
throw new Exception("Prepare failed: " . $conn->error);
|
||||
}
|
||||
|
||||
$stmt->bind_param("s", $eft_id);
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception("Membership fee update failed: " . $stmt->error);
|
||||
}
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
// Commit transaction if everything was successful
|
||||
$conn->commit();
|
||||
sendPaymentConfirmation(getEmail($user), getFullName($user), getEftDescription($eft_id));
|
||||
header("Location: admin_efts");
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
} catch (Exception $e) {
|
||||
// Rollback transaction if an error occurs
|
||||
$conn->rollback();
|
||||
echo "Error: " . $e->getMessage();
|
||||
}
|
||||
|
||||
|
||||
// Close database connection
|
||||
$conn->close();
|
||||
|
||||
78
src/processors/process_membership_payment.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
// Start session to retrieve the logged-in user's ID
|
||||
session_start();
|
||||
|
||||
// Get user ID from session (assuming user is logged in)
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
|
||||
// Validate user session
|
||||
if (!$user_id) {
|
||||
echo "<script>alert('User is not logged in. Please log in to make a booking.'); window.location.href = 'login.php';</script>";
|
||||
exit();
|
||||
}
|
||||
$is_member = getUserMemberStatus($user_id);
|
||||
|
||||
$query = "SELECT payment_amount, payment_status, membership_end_date FROM membership_fees WHERE user_id = ?";
|
||||
$stmt = $conn->prepare($query);
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
// Check if trip exists
|
||||
if ($result->num_rows === 0) {
|
||||
$response = ['error' => 'Application Fee not found.'];
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch trip details
|
||||
$fee = $result->fetch_assoc();
|
||||
$payment_status = $fee['payment_status'];
|
||||
$membership_end_date = $fee['membership_end_date'];
|
||||
$payment_amount = intval($fee['payment_amount']);
|
||||
|
||||
$description = "4WDCSA: Membership Fee " . getFullName($user_id) . " " . date("Y");
|
||||
$payment_id = uniqid();
|
||||
$eft_id = "SUBS 2025 ".getLastName($user_id);
|
||||
|
||||
// Update the membership_fees table to set payment_id
|
||||
$stmt = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE user_id = ?");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("ss", $payment_id, $user_id);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception("Failed to update membership_fees table.");
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
} else {
|
||||
throw new Exception("Failed to prepare statement for membership_fees table: " . $conn->error);
|
||||
}
|
||||
|
||||
// Get the current date
|
||||
$current_date = new DateTime();
|
||||
|
||||
// Convert $membership_end_date to a DateTime object
|
||||
$membership_end_date_obj = DateTime::createFromFormat('Y-m-d', $membership_end_date);
|
||||
|
||||
// Check if the current date is after membership_end_date
|
||||
// OR if the current date is before or on membership_end_date AND payment_status is "PENDING"
|
||||
if (
|
||||
$current_date > $membership_end_date_obj ||
|
||||
($current_date <= $membership_end_date_obj && $payment_status === "PENDING")
|
||||
) {
|
||||
|
||||
// Call the processMembershipPayment function
|
||||
// processMembershipPayment($payment_id, $payment_amount, $description);
|
||||
addMembershipEFT($eft_id, $user_id, $status, $amount, $description, $membershipfee_id);
|
||||
header("Location: payment_confirmation?booking_id=" . $booking_id);
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
}
|
||||
|
||||
152
src/processors/process_payments.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
include_once($rootPath . '/header.php');
|
||||
checkAdmin();
|
||||
checkUserSession();
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
?>
|
||||
<style>
|
||||
.image {
|
||||
width: 400px;
|
||||
/* Set your desired width */
|
||||
height: 350px;
|
||||
/* Set your desired height */
|
||||
overflow: hidden;
|
||||
/* Hide any overflow */
|
||||
display: block;
|
||||
/* Ensure proper block behavior */
|
||||
}
|
||||
|
||||
.image img {
|
||||
width: 100%;
|
||||
/* Image scales to fill the container */
|
||||
height: 100%;
|
||||
/* Image scales to fill the container */
|
||||
object-fit: cover;
|
||||
/* Fills the container while maintaining aspect ratio */
|
||||
object-position: top;
|
||||
/* Aligns the top of the image with the top of the container */
|
||||
display: block;
|
||||
/* Prevents inline whitespace issues */
|
||||
}
|
||||
|
||||
.message-box {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
padding-right: 35px;
|
||||
/* Ensures text doesn't overlap with the close button */
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
/* Centers vertically */
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
<?php
|
||||
$status = "PROCESSING";
|
||||
$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">Process Payments</h2>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb justify-content-center mb-20" data-aos="fade-right" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
||||
<li class="breadcrumb-item"><a href="index.php">Home</a></li>
|
||||
<li class="breadcrumb-item active">Process Payments</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tour List Area start -->
|
||||
<section class="tour-list-page py-100 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-lg-12">
|
||||
<?php if (isset($_SESSION['message'])): ?>
|
||||
<div class="alert alert-warning message-box">
|
||||
<?php echo $_SESSION['message']; ?>
|
||||
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||
</div>
|
||||
<?php unset($_SESSION['message']); ?>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
// Query to retrieve data from the bookings table
|
||||
$sql = "SELECT * FROM efts WHERE status = ? ORDER BY timestamp DESC";
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param("s", $status);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
// Loop through each row
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$eft_id = $row['eft_id'];
|
||||
$file_name = str_replace(' ', '_', $eft_id);
|
||||
$eft_user = $row['user_id'];
|
||||
$eft_amount = $row['amount'];
|
||||
$eft_description = $row['description'];
|
||||
|
||||
// Output the HTML structure with dynamic data
|
||||
echo '
|
||||
<div class="destination-item style-three bgc-lighter booking " data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="p-4" >
|
||||
<iframe src="uploads/pop/'.$file_name.'.pdf#toolbar=0" width="400px" height="200px"></iframe>
|
||||
<p><a href="uploads/pop/'.$file_name.'.pdf" target="_new" class="theme-btn style-three" style="width:100%;">View Full PDF</a></p>
|
||||
|
||||
</div>
|
||||
<div style="width:100%;" class="content">
|
||||
<h5>' . htmlspecialchars($eft_description) . '</a></h5>
|
||||
<h5>' . getFullName($eft_user) . '</a></h5>
|
||||
<div class="destination-footer">
|
||||
<span class="price"><span>Booking Total: R ' . number_format($eft_amount, 2) . '</span></span>
|
||||
<a href="process_eft.php?token=' . encryptData($eft_id, $salt) . '" class="theme-btn style-three"><span data-hover="POP RECEIVED">PROCESS</span></a>
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
} else {
|
||||
echo '<p>There are no pending payments for processing.</p>';
|
||||
}
|
||||
// Close connection
|
||||
$conn->close();
|
||||
?>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Tour List Area end -->
|
||||
|
||||
|
||||
|
||||
|
||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||
84
src/processors/process_signature.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?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");
|
||||
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']));
|
||||
}
|
||||
|
||||
if (isset($_POST['signature'])) {
|
||||
// CSRF Token Validation
|
||||
// if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
||||
// auditLog($_SESSION['user_id'], 'CSRF_VALIDATION_FAILED', 'membership_application', null, ['endpoint' => 'process_signature.php']);
|
||||
// die(json_encode(['status' => 'error', 'message' => 'Security token validation failed']));
|
||||
// }
|
||||
|
||||
$user_id = $_SESSION['user_id']; // Get the user ID from the session
|
||||
$signature = $_POST['signature']; // Base64 image data
|
||||
|
||||
// Decode the base64 image
|
||||
$signature = str_replace('data:image/png;base64,', '', $signature);
|
||||
$signature = str_replace(' ', '+', $signature);
|
||||
$signatureData = base64_decode($signature);
|
||||
|
||||
// Create a file path for the signature image
|
||||
$fileName = 'signature_' . $user_id . '.png';
|
||||
$filePath = $rootPath . '/uploads/signatures/' . $fileName;
|
||||
|
||||
// Ensure the directory exists
|
||||
if (!is_dir($rootPath . '/uploads/signatures')) {
|
||||
mkdir($rootPath . '/uploads/signatures', 0777, true);
|
||||
}
|
||||
|
||||
// Save the image file
|
||||
if (file_put_contents($filePath, $signatureData)) {
|
||||
// Update the database
|
||||
|
||||
if ($conn->connect_error) {
|
||||
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', $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']);
|
||||
}
|
||||
|
||||
188
src/processors/process_trip.php
Normal 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()]);
|
||||
}
|
||||
?>
|
||||
173
src/processors/process_trip_booking.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
session_start();
|
||||
|
||||
// Get the trip_id from the request (ensure it's sanitized)
|
||||
$trip_id = isset($_POST['trip_id']) ? intval($_POST['trip_id']) : 0;
|
||||
|
||||
checkAndRedirectBooking($trip_id);
|
||||
|
||||
// Check available spaces
|
||||
$available_spaces = getAvailableSpaces($trip_id); // Assuming you're using MySQLi and the function is updated for it
|
||||
|
||||
if ($available_spaces < 1) {
|
||||
// Redirect back to trips.php with an error message
|
||||
header("Location: trips?error=fully_booked");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Get user ID from session (assuming user is logged in)
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
|
||||
// Validate user session
|
||||
if (!$user_id) {
|
||||
echo "<script>alert('User is not logged in. Please log in to make a booking.'); window.location.href = 'login.php';</script>";
|
||||
exit();
|
||||
}
|
||||
$is_member = getUserMemberStatus($user_id);
|
||||
|
||||
// Check if the form has been submitted
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF Token Validation
|
||||
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
||||
auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'bookings', null, ['endpoint' => 'process_trip_booking.php']);
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Security token validation failed.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Input variables from the form (use default values if not provided)
|
||||
$num_vehicles = validateInteger($_POST['vehicles'] ?? 1, 1, 10);
|
||||
if ($num_vehicles === false) $num_vehicles = 1;
|
||||
|
||||
$num_adults = validateInteger($_POST['adults'] ?? 1, 1, 20);
|
||||
if ($num_adults === false) $num_adults = 1;
|
||||
|
||||
$num_children = validateInteger($_POST['children'] ?? 0, 0, 20);
|
||||
if ($num_children === false) $num_children = 0;
|
||||
|
||||
$num_pensioners = validateInteger($_POST['pensioners'] ?? 0, 0, 20);
|
||||
if ($num_pensioners === false) $num_pensioners = 0;
|
||||
// Fetch trip costs from the database
|
||||
$query = "SELECT trip_name, cost_members, cost_nonmembers, cost_pensioner_member, cost_pensioner, booking_fee, start_date, end_date, trip_code FROM trips WHERE trip_id = ?";
|
||||
$stmt = $conn->prepare($query);
|
||||
$stmt->bind_param('i', $trip_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
// Check if trip exists
|
||||
if ($result->num_rows === 0) {
|
||||
$response = ['error' => 'Trip not found.'];
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch trip details
|
||||
$trip = $result->fetch_assoc();
|
||||
$trip_code = $trip['trip_code'];
|
||||
$trip_name = $trip['trip_name'];
|
||||
$cost_members = intval($trip['cost_members']);
|
||||
$cost_nonmembers = intval($trip['cost_nonmembers']);
|
||||
$cost_pensioner_member = intval($trip['cost_pensioner_member']);
|
||||
$cost_pensioner = intval($trip['cost_pensioner']);
|
||||
$member_discount = $cost_nonmembers - $cost_members;
|
||||
$member_discount_pensioner = $cost_pensioner - $cost_pensioner_member;
|
||||
$booking_fee = $trip['booking_fee'];
|
||||
$radioCost = $radio ? 50 : 0;
|
||||
$start_date = $trip['start_date']; // Start date of the trip
|
||||
$end_date = $trip['end_date']; // End date of the trip
|
||||
|
||||
|
||||
// Assume the membership status is determined elsewhere
|
||||
$is_member = getUserMemberStatus($user_id);
|
||||
|
||||
// Initialize total and discount amount
|
||||
$total = 0;
|
||||
$discountAmount = 0;
|
||||
|
||||
// Calculate total based on membership
|
||||
if ($is_member) {
|
||||
$total = (($num_adults + $num_children) * $cost_nonmembers) + ($num_pensioners * $cost_pensioner) + $radioCost + ($num_vehicles * $booking_fee);
|
||||
$discountAmount = (($num_adults + $num_children) * $member_discount) + ($num_pensioners * $member_discount_pensioner );
|
||||
$payment_amount = $total - $discountAmount;
|
||||
} else {
|
||||
$total = (($num_adults + $num_children) * $cost_nonmembers) + ($num_pensioners * $cost_pensioner) + $radioCost + ($num_vehicles * $booking_fee);
|
||||
$payment_amount = $total;
|
||||
}
|
||||
|
||||
$status = "AWAITING PAYMENT";
|
||||
$description = $trip_name;
|
||||
$type = 'trip';
|
||||
$payment_id = uniqid();
|
||||
// $eft_id = strtoupper(base_convert(time(), 10, 36)); // Convert timestamp to base36
|
||||
$eft_id = strtoupper($trip_code." ".getInitialSurname($user_id));
|
||||
|
||||
|
||||
// Insert booking into the database
|
||||
$sql = "INSERT INTO bookings (booking_type, user_id, from_date, to_date, num_vehicles, num_adults, num_children, total_amount, discount_amount, status, payment_id, trip_id, radio, eft_id, num_pensioners)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
$stmt = $conn->prepare($sql);
|
||||
|
||||
if (!$stmt) {
|
||||
die("Preparation failed: " . $conn->error);
|
||||
}
|
||||
|
||||
$stmt->bind_param('sissiiiddssiisi', $type, $user_id, $start_date, $end_date, $num_vehicles, $num_adults, $num_children, $total, $discountAmount, $status, $payment_id, $trip_id, $radio, $eft_id, $num_pensioners);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Get the generated booking_id
|
||||
$booking_id = $conn->insert_id;
|
||||
|
||||
if ($payment_amount < 1) {
|
||||
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||
} else {
|
||||
$error_message = $stmt->error;
|
||||
echo "Error processing booking: $error_message";
|
||||
}
|
||||
} else {
|
||||
addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description);
|
||||
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||
header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
}
|
||||
} else {
|
||||
// Handle error if insert fails and echo the MySQL error
|
||||
$error_message = $stmt->error;
|
||||
echo "Error processing booking: $error_message";
|
||||
}
|
||||
|
||||
// if ($stmt->execute()) {
|
||||
// if ($payment_amount < 1) {
|
||||
// if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||
// echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||
// } else {
|
||||
// $error_message = $stmt->error;
|
||||
// echo "Error processing booking: $error_message";
|
||||
// }
|
||||
// } else {
|
||||
// if (processPayment($payment_id, $payment_amount, $description)) {
|
||||
// echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||
// } else {
|
||||
// $error_message = $stmt->error;
|
||||
// echo "Error processing booking: $error_message";
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // Handle error if insert fails and echo the MySQL error
|
||||
// $error_message = $stmt->error;
|
||||
// echo "Error processing booking: $error_message";
|
||||
// }
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
} else {
|
||||
echo "Invalid request.";
|
||||
}
|
||||
|
||||
147
src/processors/register_user.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/session.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
require_once($rootPath . "/vendor/autoload.php");
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
// Create connection
|
||||
$conn = openDatabaseConnection();
|
||||
|
||||
// Check connection
|
||||
if ($conn->connect_error) {
|
||||
die("Connection failed: " . $conn->connect_error);
|
||||
}
|
||||
|
||||
|
||||
// Form processing
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF Token Validation
|
||||
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
||||
auditLog(null, 'CSRF_VALIDATION_FAILED', 'users', null, ['endpoint' => 'register_user.php']);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Security token validation failed. Please try again.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Check rate limiting on registration endpoint (by IP)
|
||||
$ip = getClientIPAddress();
|
||||
$cutoffTime = date('Y-m-d H:i:s', time() - (3600)); // Last hour
|
||||
|
||||
$stmt = $conn->prepare("SELECT COUNT(*) as count FROM audit_log WHERE action = 'REGISTRATION_ATTEMPT' AND ip_address = ? AND created_at > ?");
|
||||
$stmt->bind_param('ss', $ip, $cutoffTime);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($regAttempts);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
// Allow max 5 registration attempts per IP per hour
|
||||
if ($regAttempts >= 5) {
|
||||
auditLog(null, 'REGISTRATION_RATE_LIMIT_EXCEEDED', 'users', null, ['ip' => $ip, 'attempts' => $regAttempts]);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Too many registration attempts. Please try again later.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Validate and sanitize first name
|
||||
$first_name = validateName($_POST['first_name'] ?? '');
|
||||
if ($first_name === false) {
|
||||
auditLog(null, 'REGISTRATION_INVALID_FIRST_NAME', 'users');
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid first name. Only letters, spaces, hyphens, and apostrophes allowed (2-100 characters).']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Validate and sanitize last name
|
||||
$last_name = validateName($_POST['last_name'] ?? '');
|
||||
if ($last_name === false) {
|
||||
auditLog(null, 'REGISTRATION_INVALID_LAST_NAME', 'users');
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid last name. Only letters, spaces, hyphens, and apostrophes allowed (2-100 characters).']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Validate and sanitize phone number
|
||||
$phone_number = validatePhoneNumber($_POST['phone_number'] ?? '');
|
||||
if ($phone_number === false) {
|
||||
auditLog(null, 'REGISTRATION_INVALID_PHONE', 'users');
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid phone number format.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Validate email
|
||||
$email = validateEmail($_POST['email'] ?? '');
|
||||
if ($email === false) {
|
||||
auditLog(null, 'REGISTRATION_INVALID_EMAIL', 'users');
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$password = $_POST['password'] ?? '';
|
||||
$password_confirm = $_POST['password_confirm'] ?? '';
|
||||
|
||||
// Validate password strength (minimum 8 characters, must contain uppercase, lowercase, number, special char)
|
||||
if (strlen($password) < 8) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Password must be at least 8 characters long.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!preg_match('/[A-Z]/', $password) || !preg_match('/[a-z]/', $password) ||
|
||||
!preg_match('/[0-9]/', $password) || !preg_match('/[!@#$%^&*]/', $password)) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Password must contain uppercase, lowercase, number, and special character (!@#$%^&*).']);
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($password !== $password_confirm) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Passwords do not match.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Check if the email is already registered
|
||||
$stmt = $conn->prepare('SELECT user_id FROM users WHERE email = ?');
|
||||
$stmt->bind_param('s', $email);
|
||||
$stmt->execute();
|
||||
$stmt->store_result();
|
||||
|
||||
if ($stmt->num_rows > 0) {
|
||||
auditLog(null, 'REGISTRATION_EMAIL_EXISTS', 'users', null, ['email' => $email]);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Email is already registered.']);
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
|
||||
// Hash password
|
||||
$hashed_password = password_hash($password, PASSWORD_BCRYPT);
|
||||
|
||||
// Generate email verification token
|
||||
$token = bin2hex(random_bytes(50));
|
||||
|
||||
// Prepare and execute query
|
||||
$stmt = $conn->prepare('INSERT INTO users (first_name, last_name, phone_number, email, password, token, is_verified, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
$is_verified = 0; // Not verified
|
||||
$type = 'credentials';
|
||||
$stmt->bind_param('ssssssis', $first_name, $last_name, $phone_number, $email, $hashed_password, $token, $is_verified, $type);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$newUser_id = $conn->insert_id;
|
||||
processLegacyMembership($newUser_id);
|
||||
auditLog($newUser_id, 'USER_REGISTRATION', 'users', $newUser_id, ['email' => $email]);
|
||||
|
||||
if (sendVerificationEmail($email, $first_name . ' ' . $last_name, $token)) {
|
||||
sendEmail($_ENV['ADMIN_EMAIL'], '4WDCSA: New User Registration', $first_name . ' ' . $last_name . ' (' . $email . ') has just created an account using Credentials.');
|
||||
echo json_encode(['status' => 'success', 'message' => 'Registration successful. Please check your email to verify your account.']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to send verification email.']);
|
||||
}
|
||||
} else {
|
||||
auditLog(null, 'REGISTRATION_DATABASE_ERROR', 'users', null, ['email' => $email]);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to register user.']);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
|
||||
49
src/processors/send_reset_link.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
$response = array('status' => 'error', 'message' => 'Something went wrong');
|
||||
|
||||
if (isset($_POST['email'])) {
|
||||
$email = $_POST['email'];
|
||||
|
||||
// Check if the email exists
|
||||
$sql = "SELECT user_id FROM users WHERE email = ?";
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param("s", $email);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
$user = $result->fetch_assoc();
|
||||
$user_id = $user['user_id'];
|
||||
|
||||
// Generate a unique token
|
||||
$token = bin2hex(random_bytes(50));
|
||||
|
||||
// Store the token and expiration time in the database
|
||||
$expiry = date("Y-m-d H:i:s", strtotime('+3 hour')); // Token expires in 1 hour
|
||||
$sql = "INSERT INTO password_resets (user_id, token, expires_at) VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE token = VALUES(token), expires_at = VALUES(expires_at)";
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param("iss", $user_id, $token, $expiry);
|
||||
$stmt->execute();
|
||||
|
||||
// Send the reset link to the user
|
||||
$reset_link = "https://www.4wdcsa.co.za/reset_password.php?token=$token";
|
||||
$subject = "Password Reset Request";
|
||||
$message = "Click the following link to reset your password: $reset_link";
|
||||
sendEmail($email, $subject, $message);
|
||||
|
||||
$response['status'] = 'success';
|
||||
$response['message'] = 'Password reset link has been sent to your email.';
|
||||
} else {
|
||||
$response['message'] = 'Email not found.';
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
?>
|
||||
|
||||
48
src/processors/submit_order.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
session_start();
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
// CSRF Token Validation
|
||||
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Security token validation failed.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
if (isset($_POST['tab_id']) && isset($_SESSION['cart'][$_POST['tab_id']])) {
|
||||
$tab_id = (int) $_POST['tab_id']; // Ensure it's an integer
|
||||
$drinks = $_SESSION['cart'][$tab_id];
|
||||
$created_at = date('Y-m-d H:i:s');
|
||||
|
||||
$errors = []; // Array to store SQL errors
|
||||
|
||||
foreach ($drinks as $drink) {
|
||||
$drink_id = (int) $drink['item_id']; // Ensure drink ID is an integer
|
||||
$drink_name = $drink['item_name']; // No escaping needed with prepared statements
|
||||
$drink_price = (float) $drink['item_price']; // Ensure price is a float
|
||||
$user_id = (int) $drink['user_id']; // Convert to integer
|
||||
|
||||
// Insert each drink into the bar_transactions table using prepared statement
|
||||
$stmt = $conn->prepare("INSERT INTO bar_transactions (user_id, tab_id, item_id, item_name, item_price) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param("iiisi", $user_id, $tab_id, $drink_id, $drink_name, $drink_price);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
$errors[] = "Error inserting drink ID $drink_id: " . $conn->error;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
// Clear the cart for this tab after successful submission
|
||||
unset($_SESSION['cart'][$tab_id]);
|
||||
echo json_encode(['status' => 'success', 'message' => 'Order submitted successfully!']);
|
||||
} else {
|
||||
// Log all errors and return failure message
|
||||
error_log(implode("\n", $errors)); // Log errors to the server
|
||||
echo json_encode(['status' => 'error', 'message' => 'Some items failed to be added.', 'errors' => $errors]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Cart is empty or tab ID is invalid.']);
|
||||
}
|
||||
|
||||
224
src/processors/submit_pop.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
ob_start(); // Start output buffering to allow headers before output
|
||||
$headerStyle = 'light';
|
||||
$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");
|
||||
checkUserSession();
|
||||
|
||||
$user_id = $_SESSION['user_id'] ?? null;
|
||||
|
||||
if (!$user_id) {
|
||||
die("Not logged in.");
|
||||
}
|
||||
|
||||
// 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'])) {
|
||||
http_response_code(403);
|
||||
die('Security token validation failed. Please try again.');
|
||||
}
|
||||
|
||||
$eft_id = $_POST['eft_id'] ?? null;
|
||||
|
||||
if (!$eft_id || !isset($_FILES['pop_file'])) {
|
||||
echo "<div class='alert alert-danger'>Invalid submission: missing eft_id or file.</div>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate file using hardened validation function
|
||||
$validationResult = validateFileUpload($_FILES['pop_file'], 'proof_of_payment');
|
||||
|
||||
if ($validationResult === false) {
|
||||
echo "<div class='alert alert-danger'>Invalid file. Only PDF files under 10MB are allowed.</div>";
|
||||
exit;
|
||||
}
|
||||
|
||||
$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)) {
|
||||
mkdir($target_dir, 0755, true);
|
||||
}
|
||||
|
||||
if (!is_writable($target_dir)) {
|
||||
echo "<div class='alert alert-danger'>Upload directory is not writable: $target_dir</div>";
|
||||
exit;
|
||||
}
|
||||
|
||||
if (move_uploaded_file($_FILES['pop_file']['tmp_name'], $target_file)) {
|
||||
chmod($target_file, 0644);
|
||||
|
||||
// Update EFT and booking status
|
||||
$payment_type = $_POST['payment_type'] ?? 'booking';
|
||||
|
||||
if ($payment_type === 'membership') {
|
||||
// Update EFT and booking status
|
||||
$stmt1 = $conn->prepare("UPDATE efts SET status = 'PROCESSING' WHERE eft_id = ?");
|
||||
$stmt1->bind_param("s", $eft_id);
|
||||
$stmt1->execute();
|
||||
$stmt1->close();
|
||||
|
||||
// Update membership fee status
|
||||
$stmt = $conn->prepare("UPDATE membership_fees SET payment_status = 'PROCESSING' WHERE payment_id = ?");
|
||||
$stmt->bind_param("s", $eft_id);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
} else {
|
||||
// Update EFT and booking status
|
||||
$stmt1 = $conn->prepare("UPDATE efts SET status = 'PROCESSING' WHERE eft_id = ?");
|
||||
$stmt1->bind_param("s", $eft_id);
|
||||
$stmt1->execute();
|
||||
$stmt1->close();
|
||||
|
||||
$stmt2 = $conn->prepare("UPDATE bookings SET status = 'PROCESSING' WHERE eft_id = ?");
|
||||
$stmt2->bind_param("s", $eft_id);
|
||||
$stmt2->execute();
|
||||
$stmt2->close();
|
||||
}
|
||||
|
||||
// Send notification email using sendPOP()
|
||||
$fullname = getFullName($user_id);
|
||||
$eftDetails = getEFTDetails($eft_id);
|
||||
|
||||
if ($eftDetails) {
|
||||
$amount = "R" . number_format($eftDetails['amount'], 2);
|
||||
$description = $eftDetails['description'];
|
||||
} else {
|
||||
$amount = "R0.00";
|
||||
$description = "Payment";
|
||||
}
|
||||
|
||||
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' => $filename, 'payment_type' => $payment_type]);
|
||||
|
||||
$redirect_url = 'bookings';
|
||||
ob_end_clean();
|
||||
header("Location: bookings");
|
||||
exit;
|
||||
|
||||
} else {
|
||||
echo "<div class='alert alert-danger'>Unable to move uploaded file.</div>";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Now that POST is handled, include header for display
|
||||
include_once($rootPath . '/header.php');
|
||||
|
||||
// Fetch bookings for dropdown
|
||||
$stmt = $conn->prepare("
|
||||
SELECT eft_id AS id, 'booking' AS type FROM bookings WHERE user_id = ? AND status = 'AWAITING PAYMENT'
|
||||
UNION
|
||||
SELECT payment_id AS id, 'membership' AS type FROM membership_fees WHERE user_id = ? AND payment_status = 'PENDING'
|
||||
");
|
||||
$stmt->bind_param("ii", $user_id, $user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$items = $result->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
|
||||
|
||||
|
||||
$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">Submit Proof of Payment</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">Submit Proof of Payment</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tour List Area start -->
|
||||
<section class="tour-list-page py-100 rel z-1">
|
||||
<div class="container" style="max-width:600px;">
|
||||
<div class="row">
|
||||
<div class="comment-form bgc-lighter z-1 rel mb-30 rmb-55" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="widget widget-booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="section-title">
|
||||
<h3>Submit Proof of Payment</h3>
|
||||
<div style="text-align: center;" id="responseMessage"></div>
|
||||
<p>To finalise your booking/membership, select the payment reference below, and then upload your PDF proof of payment.</p> <!-- Message display area -->
|
||||
</div>
|
||||
<?php if (count($items) > 0) {?>
|
||||
|
||||
<form enctype="multipart/form-data" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
|
||||
<div class="row mt-35">
|
||||
<ul class="tickets clearfix">
|
||||
<li>
|
||||
Select Payment Reference:
|
||||
<select name="eft_id" id="eft_id" required onchange="updatePaymentType(this)">
|
||||
<?php
|
||||
if (count($items) > 0) {
|
||||
foreach ($items as $item) {
|
||||
$label = strtoupper($item['type']) . ' - ' . htmlspecialchars($item['id']);
|
||||
echo '<option value="' . htmlspecialchars($item['id']) . '" data-type="' . $item['type'] . '">' . $label . '</option>';
|
||||
}
|
||||
} else {
|
||||
echo '<option value="" disabled selected>No payments available</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<input type="hidden" name="payment_type" id="payment_type">
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<input style="border-radius:30px;" type="file" name="pop_file" id="pop_file" accept="application/pdf" class="form-control" required>
|
||||
</li>
|
||||
</div>
|
||||
<div class="mt-10 mb-0">
|
||||
<button type="submit" class="theme-btn style-two" style="width:100%;">Submit POP</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<?php
|
||||
}else{
|
||||
echo 'No unpaid bookings';
|
||||
}?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function updatePaymentType(selectEl) {
|
||||
const selectedOption = selectEl.options[selectEl.selectedIndex];
|
||||
const type = selectedOption.getAttribute('data-type');
|
||||
document.getElementById('payment_type').value = type;
|
||||
}
|
||||
window.onload = function() {
|
||||
const dropdown = document.getElementById('eft_id');
|
||||
updatePaymentType(dropdown); // set default value on page load
|
||||
};
|
||||
</script>
|
||||
|
||||
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||
67
src/processors/toggle_trip_published.php
Normal 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()]);
|
||||
}
|
||||
?>
|
||||
129
src/processors/update_application.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/session.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// Get all the form fields
|
||||
$first_name = $_POST['first_name'];
|
||||
$last_name = $_POST['last_name'];
|
||||
$id_number = $_POST['id_number'];
|
||||
$dob = $_POST['dob'];
|
||||
$occupation = $_POST['occupation'];
|
||||
$tel_cell = $_POST['tel_cell'];
|
||||
$email = $_POST['email'];
|
||||
|
||||
// Spouse or Partner details (optional)
|
||||
$spouse_first_name = !empty($_POST['spouse_first_name']) ? $_POST['spouse_first_name'] : null;
|
||||
$spouse_last_name = !empty($_POST['spouse_last_name']) ? $_POST['spouse_last_name'] : null;
|
||||
$spouse_id_number = !empty($_POST['spouse_id_number']) ? $_POST['spouse_id_number'] : null;
|
||||
$spouse_dob = !empty($_POST['spouse_dob']) ? $_POST['spouse_dob'] : NULL; // if empty, set to NULL
|
||||
$spouse_occupation = !empty($_POST['spouse_occupation']) ? $_POST['spouse_occupation'] : null;
|
||||
$spouse_tel_cell = !empty($_POST['spouse_tel_cell']) ? $_POST['spouse_tel_cell'] : null;
|
||||
$spouse_email = !empty($_POST['spouse_email']) ? $_POST['spouse_email'] : null;
|
||||
|
||||
// Children details (optional)
|
||||
$child_name1 = !empty($_POST['child_name1']) ? $_POST['child_name1'] : null;
|
||||
$child_dob1 = !empty($_POST['child_dob1']) ? $_POST['child_dob1'] : null;
|
||||
$child_name2 = !empty($_POST['child_name2']) ? $_POST['child_name2'] : null;
|
||||
$child_dob2 = !empty($_POST['child_dob2']) ? $_POST['child_dob2'] : null;
|
||||
$child_name3 = !empty($_POST['child_name3']) ? $_POST['child_name3'] : null;
|
||||
$child_dob3 = !empty($_POST['child_dob3']) ? $_POST['child_dob3'] : null;
|
||||
|
||||
// Address and other details
|
||||
$physical_address = $_POST['physical_address'];
|
||||
$postal_address = $_POST['postal_address'];
|
||||
$interests_hobbies = $_POST['interests_hobbies'];
|
||||
|
||||
// Primary vehicle details
|
||||
$vehicle_make = $_POST['vehicle_make'];
|
||||
$vehicle_model = $_POST['vehicle_model'];
|
||||
$vehicle_year = $_POST['vehicle_year'];
|
||||
$vehicle_registration = $_POST['vehicle_registration'];
|
||||
|
||||
// Secondary vehicle details (optional)
|
||||
$secondary_vehicle_make = !empty($_POST['secondary_vehicle_make']) ? $_POST['secondary_vehicle_make'] : null;
|
||||
$secondary_vehicle_model = !empty($_POST['secondary_vehicle_model']) ? $_POST['secondary_vehicle_model'] : null;
|
||||
$secondary_vehicle_year = !empty($_POST['secondary_vehicle_year']) ? $_POST['secondary_vehicle_year'] : null;
|
||||
$secondary_vehicle_registration = !empty($_POST['secondary_vehicle_registration']) ? $_POST['secondary_vehicle_registration'] : null;
|
||||
|
||||
// Start a transaction to ensure data consistency
|
||||
$conn->begin_transaction();
|
||||
|
||||
try {
|
||||
// Prepare the SQL update statement
|
||||
$stmt = $conn->prepare("UPDATE membership_application SET
|
||||
first_name = ?, last_name = ?, id_number = ?, dob = ?, occupation = ?, tel_cell = ?, email = ?,
|
||||
spouse_first_name = ?, spouse_last_name = ?, spouse_id_number = ?, spouse_dob = ?, spouse_occupation = ?, spouse_tel_cell = ?, spouse_email = ?,
|
||||
child_name1 = ?, child_dob1 = ?, child_name2 = ?, child_dob2 = ?, child_name3 = ?, child_dob3 = ?,
|
||||
physical_address = ?, postal_address = ?, interests_hobbies = ?, vehicle_make = ?, vehicle_model = ?, vehicle_year = ?, vehicle_registration = ?,
|
||||
secondary_vehicle_make = ?, secondary_vehicle_model = ?, secondary_vehicle_year = ?, secondary_vehicle_registration = ?
|
||||
WHERE user_id = ?");
|
||||
|
||||
// Check if preparation was successful
|
||||
if (!$stmt) {
|
||||
die("SQL error: " . $conn->error);
|
||||
}
|
||||
|
||||
$stmt->bind_param(
|
||||
"sssssssssssssssssssssssssssssssi",
|
||||
$first_name,
|
||||
$last_name,
|
||||
$id_number,
|
||||
$dob,
|
||||
$occupation,
|
||||
$tel_cell,
|
||||
$email,
|
||||
$spouse_first_name,
|
||||
$spouse_last_name,
|
||||
$spouse_id_number,
|
||||
$spouse_dob,
|
||||
$spouse_occupation,
|
||||
$spouse_tel_cell,
|
||||
$spouse_email,
|
||||
$child_name1,
|
||||
$child_dob1,
|
||||
$child_name2,
|
||||
$child_dob2,
|
||||
$child_name3,
|
||||
$child_dob3,
|
||||
$physical_address,
|
||||
$postal_address,
|
||||
$interests_hobbies,
|
||||
$vehicle_make,
|
||||
$vehicle_model,
|
||||
$vehicle_year,
|
||||
$vehicle_registration,
|
||||
$secondary_vehicle_make,
|
||||
$secondary_vehicle_model,
|
||||
$secondary_vehicle_year,
|
||||
$secondary_vehicle_registration,
|
||||
$user_id // User ID for WHERE condition
|
||||
);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$conn->commit();
|
||||
header("Location: membership_details");
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
} else {
|
||||
throw new Exception("Failed to update member application. SQL error: " . $conn->error);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Rollback the transaction in case of error
|
||||
$conn->rollback();
|
||||
|
||||
// Error response
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'message' => 'Error: ' . $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
42
src/processors/update_user.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/session.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
$response = array('status' => 'error', 'message' => 'Something went wrong');
|
||||
|
||||
// Check if the user is logged in
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
$response['message'] = 'You are not logged in.';
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
// Handle updating user details (excluding profile picture)
|
||||
if (isset($_POST['first_name'], $_POST['last_name'], $_POST['phone_number'], $_POST['email'])) {
|
||||
$first_name = ucwords(strtolower($_POST['first_name']));
|
||||
$last_name = ucwords(strtolower($_POST['last_name']));
|
||||
$phone_number = $_POST['phone_number'];
|
||||
$email = $_POST['email'];
|
||||
|
||||
// Update user details in the database
|
||||
$sql = "UPDATE users SET first_name = ?, last_name = ?, phone_number = ?, email = ? WHERE user_id = ?";
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param("ssssi", $first_name, $last_name, $phone_number, $email, $user_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$response['status'] = 'success';
|
||||
$response['message'] = 'User details updated successfully';
|
||||
} else {
|
||||
$response['message'] = 'Failed to update user details';
|
||||
}
|
||||
} else {
|
||||
$response['message'] = 'Invalid form submission';
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
|
||||
97
src/processors/upload_profile_picture.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?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");
|
||||
|
||||
// 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');
|
||||
|
||||
// Check if the user is logged in
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
$response['message'] = 'You are not logged in.';
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
// Handle profile picture upload
|
||||
if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] != UPLOAD_ERR_NO_FILE) {
|
||||
// Validate file using hardened validation function
|
||||
$validationResult = validateFileUpload($_FILES['profile_picture'], 'profile_picture');
|
||||
|
||||
if ($validationResult === false) {
|
||||
$response['message'] = 'Invalid file. Only JPG, JPEG, PNG, GIF, and WEBP images under 5MB are allowed.';
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Extract validated filename
|
||||
$randomFilename = $validationResult['filename'];
|
||||
$target_dir = $rootPath . "/assets/images/pp/";
|
||||
$target_file = $target_dir . $randomFilename;
|
||||
|
||||
// Ensure upload directory exists and is writable
|
||||
if (!is_dir($target_dir)) {
|
||||
mkdir($target_dir, 0755, true);
|
||||
}
|
||||
|
||||
if (!is_writable($target_dir)) {
|
||||
$response['message'] = 'Upload directory is not writable.';
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Move the uploaded file
|
||||
if (move_uploaded_file($_FILES['profile_picture']['tmp_name'], $target_file)) {
|
||||
// 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);
|
||||
if (!$stmt) {
|
||||
$response['message'] = 'Database error.';
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt->bind_param("si", $display_path, $user_id);
|
||||
if ($stmt->execute()) {
|
||||
$_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: ' . $stmt->error;
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
$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);
|
||||
?>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
require_once("env.php");
|
||||
require_once("session.php");
|
||||
require_once("connection.php");
|
||||
require_once("functions.php");
|
||||
require_once 'google-client/vendor/autoload.php'; // Add this line for Google Client
|
||||
$rootPath = dirname(__FILE__);
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/session.php");
|
||||
require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
require_once($rootPath . '/google-client/vendor/autoload.php'); // Add this line for Google Client
|
||||
|
||||
// Check if connection is established
|
||||
if (!$conn) {
|
||||
|
||||