Compare commits
19 Commits
6fd3b8d082
...
feature/ev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32651ed433 | ||
|
|
f522b84fc1 | ||
|
|
2b136c4b06 | ||
|
|
7f0964009a | ||
|
|
5be946f78f | ||
|
|
cb588d20ee | ||
|
|
fdeaf85bf0 | ||
|
|
d81d74a7c7 | ||
|
|
bfb3a0f8a9 | ||
|
|
5a2c48f343 | ||
|
|
1767337d99 | ||
|
|
674af23994 | ||
|
|
ec563e0376 | ||
|
|
a3403bf503 | ||
|
|
5f1a6bc441 | ||
|
|
716de2f0e9 | ||
|
|
79e292dc7c | ||
|
|
59c1e37d5c | ||
|
|
0c068eeb69 |
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>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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'); ?>
|
||||
<?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>');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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()]);
|
||||
}
|
||||
?>
|
||||
@@ -1,4 +1,10 @@
|
||||
<?php
|
||||
ob_start(); // Start output buffering
|
||||
session_start();
|
||||
|
||||
// Set JSON response header BEFORE any other output
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/session.php");
|
||||
@@ -6,6 +12,7 @@ require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
ob_end_clean();
|
||||
die(json_encode(['status' => 'error', 'message' => 'User not logged in']));
|
||||
}
|
||||
|
||||
@@ -26,11 +33,11 @@ if (isset($_POST['signature'])) {
|
||||
|
||||
// Create a file path for the signature image
|
||||
$fileName = 'signature_' . $user_id . '.png';
|
||||
$filePath = 'uploads/signatures/' . $fileName;
|
||||
$filePath = $rootPath . '/uploads/signatures/' . $fileName;
|
||||
|
||||
// Ensure the directory exists
|
||||
if (!is_dir('uploads/signatures')) {
|
||||
mkdir('uploads/signatures', 0777, true);
|
||||
if (!is_dir($rootPath . '/uploads/signatures')) {
|
||||
mkdir($rootPath . '/uploads/signatures', 0777, true);
|
||||
}
|
||||
|
||||
// Save the image file
|
||||
@@ -41,30 +48,37 @@ if (isset($_POST['signature'])) {
|
||||
die(json_encode(['status' => 'error', 'message' => 'Database connection failed']));
|
||||
}
|
||||
|
||||
// Store relative path for HTML display
|
||||
$display_path = '/uploads/signatures/' . $fileName;
|
||||
|
||||
// Update the signature and indemnity acceptance in the membership application table
|
||||
$stmt = $conn->prepare("UPDATE membership_application SET sig = ?, accept_indemnity = 1 WHERE user_id = ?");
|
||||
$stmt->bind_param('si', $filePath, $user_id);
|
||||
$stmt->bind_param('si', $display_path, $user_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Check the payment status
|
||||
$paymentStatus = checkMembershipPaymentStatus($user_id) ? 'PAID' : 'NOT_PAID';
|
||||
|
||||
// Respond with the appropriate redirect URL based on the payment status
|
||||
ob_end_clean();
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Signature saved successfully!',
|
||||
'paymentStatus' => $paymentStatus // Send payment status
|
||||
]);
|
||||
} else {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Database update failed']);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
} else {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to save signature']);
|
||||
}
|
||||
} else {
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Signature not provided']);
|
||||
}
|
||||
|
||||
|
||||
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()]);
|
||||
}
|
||||
?>
|
||||
@@ -132,6 +132,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
} else {
|
||||
addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description);
|
||||
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||
header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?php
|
||||
ob_start(); // Start output buffering to allow headers before output
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
include_once($rootPath . '/header.php');
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/session.php");
|
||||
include_once($rootPath . '/src/config/connection.php');
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
checkUserSession();
|
||||
|
||||
@@ -11,7 +14,8 @@ if (!$user_id) {
|
||||
die("Not logged in.");
|
||||
}
|
||||
|
||||
// Handle POST submission
|
||||
// Handle POST submission BEFORE including header
|
||||
$redirect_url = null;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF Token Validation
|
||||
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
||||
@@ -34,9 +38,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
$target_dir = "uploads/pop/";
|
||||
$randomFilename = $validationResult['filename'];
|
||||
$target_file = $target_dir . $randomFilename;
|
||||
$target_dir = $rootPath . "/uploads/pop/";
|
||||
// Use EFT ID as filename instead of random filename, replace spaces with underscores
|
||||
$filename = str_replace(' ', '_', $eft_id) . '.pdf';
|
||||
$target_file = $target_dir . $filename;
|
||||
|
||||
// Make sure target directory exists and writable
|
||||
if (!is_dir($target_dir)) {
|
||||
@@ -91,15 +96,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$description = "Payment";
|
||||
}
|
||||
|
||||
if (sendPOP($fullname, $randomFilename, $amount, $description)) {
|
||||
if (sendPOP($fullname, $filename, $amount, $description)) {
|
||||
$_SESSION['message'] = "Thank you! Your payment proof has been uploaded and notification sent.";
|
||||
} else {
|
||||
$_SESSION['message'] = "Payment uploaded, but notification email could not be sent.";
|
||||
}
|
||||
|
||||
// Log the action
|
||||
auditLog($user_id, 'POP_UPLOAD', 'efts', $eft_id, ['filename' => $randomFilename, 'payment_type' => $payment_type]);
|
||||
auditLog($user_id, 'POP_UPLOAD', 'efts', $eft_id, ['filename' => $filename, 'payment_type' => $payment_type]);
|
||||
|
||||
$redirect_url = 'bookings';
|
||||
ob_end_clean();
|
||||
header("Location: bookings");
|
||||
exit;
|
||||
|
||||
@@ -109,6 +116,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
|
||||
// Now that POST is handled, include header for display
|
||||
include_once($rootPath . '/header.php');
|
||||
|
||||
// Fetch bookings for dropdown
|
||||
$stmt = $conn->prepare("
|
||||
|
||||
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()]);
|
||||
}
|
||||
?>
|
||||
@@ -1,9 +1,20 @@
|
||||
<?php
|
||||
ob_start(); // Start output buffering
|
||||
session_start();
|
||||
|
||||
// Set JSON response header BEFORE any other output
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
require_once($rootPath . "/src/config/session.php");
|
||||
include_once($rootPath . '/src/config/connection.php');
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
require_once($rootPath . "/src/config/env.php");
|
||||
|
||||
// Check database connection
|
||||
if (!isset($conn) || $conn === null) {
|
||||
die(json_encode(['status' => 'error', 'message' => 'Database connection failed']));
|
||||
}
|
||||
|
||||
$response = array('status' => 'error', 'message' => 'Something went wrong');
|
||||
|
||||
@@ -29,7 +40,7 @@ if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] !=
|
||||
|
||||
// Extract validated filename
|
||||
$randomFilename = $validationResult['filename'];
|
||||
$target_dir = "assets/images/pp/";
|
||||
$target_dir = $rootPath . "/assets/images/pp/";
|
||||
$target_file = $target_dir . $randomFilename;
|
||||
|
||||
// Ensure upload directory exists and is writable
|
||||
@@ -48,6 +59,9 @@ if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] !=
|
||||
// Set secure file permissions (readable but not executable)
|
||||
chmod($target_file, 0644);
|
||||
|
||||
// Store relative path for HTML display
|
||||
$display_path = "assets/images/pp/" . $randomFilename;
|
||||
|
||||
// Update the profile picture path in the database
|
||||
$sql = "UPDATE users SET profile_pic = ? WHERE user_id = ?";
|
||||
$stmt = $conn->prepare($sql);
|
||||
@@ -57,25 +71,27 @@ if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] !=
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt->bind_param("si", $target_file, $user_id);
|
||||
$stmt->bind_param("si", $display_path, $user_id);
|
||||
if ($stmt->execute()) {
|
||||
$_SESSION['profile_pic'] = $target_file;
|
||||
$_SESSION['profile_pic'] = $display_path;
|
||||
$response['status'] = 'success';
|
||||
$response['message'] = 'Profile picture updated successfully';
|
||||
|
||||
// Log the action
|
||||
auditLog($user_id, 'PROFILE_PIC_UPLOAD', 'users', $user_id, ['filename' => $randomFilename]);
|
||||
} else {
|
||||
$response['message'] = 'Failed to update profile picture in the database';
|
||||
$response['message'] = 'Failed to update profile picture in the database: ' . $stmt->error;
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
$response['message'] = 'Failed to move uploaded file.';
|
||||
$response['message'] = 'Failed to move uploaded file. Error code: ' . $_FILES['profile_picture']['error'];
|
||||
}
|
||||
} else {
|
||||
$response['message'] = 'No file uploaded or file error.';
|
||||
}
|
||||
|
||||
// Clean output buffer and send only JSON
|
||||
ob_end_clean();
|
||||
echo json_encode($response);
|
||||
?>
|
||||
|
||||
|
||||