Compare commits
3 Commits
feature/ev
...
05f74f1b86
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05f74f1b86 | ||
|
|
9133b7bbc6 | ||
|
|
b52c46b67c |
BIN
assets/images/trips/9_01.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/images/trips/9_02.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
assets/images/trips/9_03.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/images/trips/9_04.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
BIN
assets/uploads/campsites/5a72387fdd1f6fc891e406c55b4b4723.jpg
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
assets/uploads/campsites/785baf57034bf35bb3dc7954ca5789b7.jpg
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
assets/uploads/campsites/aa2e5d1f0a9a81823b915d203ffadab2.jpg
Normal file
|
After Width: | Height: | Size: 168 KiB |
86
docs/MEMBERSHIP_DUPLICATE_PREVENTION.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Membership Application Duplicate Prevention
|
||||
|
||||
## Overview
|
||||
Implemented comprehensive validation to prevent users from submitting multiple membership applications or creating multiple membership fee records. Each user can have exactly one application and one membership fee record. Individual payments are tracked separately in the payments/efts table.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User (1) ---> Membership Application (1) ---> Membership Fee (1) ---> Multiple Payments/EFTs
|
||||
```
|
||||
|
||||
- **Membership Application**: Stores user details and application information (one per user)
|
||||
- **Membership Fee**: Stores the total fee amount and dates (one per user, linked to application)
|
||||
- **Payments/EFTs**: Tracks individual payment transactions for the membership fee (many per fee)
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Database Level Protection
|
||||
**File:** `docs/migrations/002_add_unique_constraints_membership.sql`
|
||||
|
||||
- Added `UNIQUE` constraint on `membership_application.user_id` - ensures each user can only have one application
|
||||
- Added `UNIQUE` constraint on `membership_fees.user_id` - ensures each user can only have one membership fee record
|
||||
- Cleans up any duplicate records before adding constraints
|
||||
|
||||
### 2. Application Level Validation
|
||||
**File:** `src/processors/process_application.php`
|
||||
|
||||
Added pre-submission checks:
|
||||
- Check if user already has a membership application in the database
|
||||
- Check if user already has a membership fee record
|
||||
- Return clear error message if either check fails
|
||||
- Catch database constraint violations and provide user-friendly message
|
||||
|
||||
**File:** `src/config/functions.php`
|
||||
|
||||
- Improved `checkMembershipApplication()` to set session message before redirecting
|
||||
- Message displayed: "You have already submitted a membership application."
|
||||
|
||||
### 3. Error Handling
|
||||
If a user somehow bypasses checks:
|
||||
- Server validates before processing
|
||||
- Returns HTTP 400 error with JSON response
|
||||
- User sees clear message directing them to support or check email
|
||||
- Database constraints prevent data corruption (duplicate key violation)
|
||||
|
||||
## User Flow
|
||||
|
||||
1. **First Visit to Application Page:**
|
||||
- `checkMembershipApplication()` checks database
|
||||
- If no application exists, shows form
|
||||
- If application exists, redirects to `membership_details.php`
|
||||
|
||||
2. **Form Submission:**
|
||||
- Server checks for existing application
|
||||
- Server checks for existing membership fee
|
||||
- If checks pass, inserts application and fee in transaction
|
||||
- On success, redirects to indemnity page
|
||||
- On error, returns JSON error response
|
||||
|
||||
3. **Payment Process:**
|
||||
- Individual payment records are created in payments/efts table
|
||||
- Multiple payments can be made against the single membership_fee record
|
||||
- Payment status is tracked independently from application
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
1. Test creating a membership application - should succeed
|
||||
2. Try applying again - should be redirected to membership_details
|
||||
3. Try submitting the form multiple times rapidly - should fail on 2nd attempt
|
||||
4. Verify payments can be made against the single membership fee record
|
||||
5. Check database constraints: `SHOW INDEX FROM membership_application;` and `SHOW INDEX FROM membership_fees;`
|
||||
|
||||
## Database Constraints
|
||||
|
||||
```sql
|
||||
-- One application per user
|
||||
ALTER TABLE membership_application
|
||||
ADD CONSTRAINT uk_membership_application_user_id UNIQUE (user_id);
|
||||
|
||||
-- One membership fee record per user
|
||||
ALTER TABLE membership_fees
|
||||
ADD CONSTRAINT uk_membership_fees_user_id UNIQUE (user_id);
|
||||
```
|
||||
|
||||
## Backwards Compatibility
|
||||
The migration script cleans up any existing duplicate records before adding constraints, ensuring no data loss.
|
||||
37
docs/migrations/002_add_unique_constraints_membership.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- Migration: Add UNIQUE constraints to prevent duplicate membership applications and fees
|
||||
-- Date: 2025-12-05
|
||||
-- Purpose: Ensure each user can only have one application and one membership fee record
|
||||
-- Note: Individual payments are tracked in the payments/efts table, not here
|
||||
|
||||
-- Add UNIQUE constraint to membership_application table
|
||||
-- First, delete any duplicate applications keeping the most recent one
|
||||
DELETE FROM membership_application
|
||||
WHERE application_id NOT IN (
|
||||
SELECT MAX(application_id)
|
||||
FROM (
|
||||
SELECT application_id
|
||||
FROM membership_application
|
||||
) tmp
|
||||
GROUP BY user_id
|
||||
);
|
||||
|
||||
-- Add UNIQUE constraint on user_id in membership_application
|
||||
ALTER TABLE membership_application
|
||||
ADD CONSTRAINT uk_membership_application_user_id UNIQUE (user_id);
|
||||
|
||||
-- Add UNIQUE constraint to membership_fees table
|
||||
-- First, delete any duplicate fees keeping the most recent one
|
||||
DELETE FROM membership_fees
|
||||
WHERE fee_id NOT IN (
|
||||
SELECT MAX(fee_id)
|
||||
FROM (
|
||||
SELECT fee_id
|
||||
FROM membership_fees
|
||||
) tmp
|
||||
GROUP BY user_id
|
||||
);
|
||||
|
||||
-- Add UNIQUE constraint on user_id in membership_fees
|
||||
ALTER TABLE membership_fees
|
||||
ADD CONSTRAINT uk_membership_fees_user_id UNIQUE (user_id);
|
||||
|
||||
14
header.php
@@ -296,15 +296,21 @@ if ($headerStyle === 'light') {
|
||||
</li>
|
||||
<?php } ?>
|
||||
<li><a href="contact">Contact</a></li>
|
||||
<?php if ($is_member) : ?>
|
||||
<?php if ($is_logged_in) : ?>
|
||||
<li class="dropdown"><a href="#">Members Area</a>
|
||||
<ul>
|
||||
<li><a href="#">Coming Soon!</a></li>
|
||||
<?php
|
||||
if (getUserMemberStatus($_SESSION['user_id'])) {
|
||||
echo "<li><a href=\"campsites\">Campsites Directory</a></li>";
|
||||
} else {
|
||||
echo "<li><a href=\"membership\">Campsites Directory</a><i class='fal fa-lock'></i></li>";
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($is_logged_in) : ?>
|
||||
|
||||
|
||||
<li class="dropdown"><a href="#">My Account</a>
|
||||
<ul>
|
||||
<li><a href="account_settings">Account Settings</a></li>
|
||||
|
||||
@@ -224,25 +224,29 @@ if ($result && $result->num_rows > 0) {
|
||||
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>');
|
||||
complete: function(xhr, status) {
|
||||
// Handle all response codes
|
||||
try {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
|
||||
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 {
|
||||
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>');
|
||||
alert('Error: ' + response.message);
|
||||
}
|
||||
} else {
|
||||
alert('Error: ' + response.message);
|
||||
} catch (e) {
|
||||
alert('Error updating event status. Response: ' + xhr.responseText);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error updating event status');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
<?php
|
||||
// Set JSON header FIRST before any includes that might output
|
||||
header('Content-Type: application/json');
|
||||
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
|
||||
// Clean any output buffers before including header
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
include_once($rootPath . '/header.php');
|
||||
checkAdmin();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
// Clean output buffer again in case header.php added content
|
||||
ob_clean();
|
||||
|
||||
$event_id = $_POST['event_id'] ?? null;
|
||||
|
||||
@@ -44,6 +56,7 @@ try {
|
||||
$update_stmt->bind_param("ii", $new_status, $event_id);
|
||||
|
||||
if ($update_stmt->execute()) {
|
||||
ob_clean(); // Clean any buffered output before sending JSON
|
||||
http_response_code(200);
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
@@ -55,6 +68,7 @@ try {
|
||||
}
|
||||
$update_stmt->close();
|
||||
} catch (Exception $e) {
|
||||
ob_clean(); // Clean any buffered output before sending JSON
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
|
||||
@@ -1434,6 +1434,10 @@ function checkMembershipApplication($user_id)
|
||||
|
||||
// Check if the record exists and redirect
|
||||
if ($count > 0) {
|
||||
// Set a session message before redirecting
|
||||
if (!isset($_SESSION['message'])) {
|
||||
$_SESSION['message'] = 'You have already submitted a membership application.';
|
||||
}
|
||||
header("Location: membership_details.php");
|
||||
exit();
|
||||
}
|
||||
@@ -2195,8 +2199,8 @@ function validateName($name, $minLength = 2, $maxLength = 100) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only allow letters, spaces, hyphens, and apostrophes
|
||||
if (!preg_match('/^[a-zA-Z\s\'-]+$/', $name)) {
|
||||
// Allow letters, numbers, spaces, hyphens, and apostrophes
|
||||
if (!preg_match('/^[a-zA-Z0-9\s\'-]+$/', $name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,18 @@ $headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||
include_once($rootPath . '/header.php');
|
||||
|
||||
// Check if user has active membership
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$is_member = getUserMemberStatus($_SESSION['user_id']);
|
||||
if (!$is_member) {
|
||||
header('Location: index');
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = openDatabaseConnection();
|
||||
$stmt = $conn->prepare("SELECT * FROM campsites");
|
||||
$stmt->execute();
|
||||
@@ -17,14 +29,72 @@ while ($row = $result->fetch_assoc()) {
|
||||
#map {
|
||||
height: 600px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gm-style .info-box {
|
||||
max-width: 250px;
|
||||
/* Center pin overlay */
|
||||
.map-center-pin {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.info-box img {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
/* Location mode indicator */
|
||||
.location-mode-indicator {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
z-index: 11;
|
||||
font-weight: 500;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Confirm location button */
|
||||
.confirm-location-btn {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
z-index: 11;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.confirm-location-btn:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.cancel-location-btn {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
background: #f44336;
|
||||
color: white;
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
z-index: 11;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cancel-location-btn:hover {
|
||||
background: #da190b;
|
||||
}
|
||||
|
||||
/* Form styling to match manage_trips */
|
||||
@@ -159,7 +229,7 @@ while ($row = $result->fetch_assoc()) {
|
||||
</style>
|
||||
|
||||
<?php
|
||||
$pageTitle = 'Campsites';
|
||||
$pageTitle = 'Campsites Directory';
|
||||
$breadcrumbs = [['Home' => 'index.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
@@ -170,11 +240,29 @@ require_once($rootPath . '/components/banner.php');
|
||||
<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()">
|
||||
<button class="theme-btn" id="toggleFormBtn" onclick="startLocationMode()">
|
||||
<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>
|
||||
<p style="color: #666; margin-bottom: 15px;">Click on a marker to view details, or use the "Add Campsite" button to add a new location.</p>
|
||||
|
||||
<!-- Map with location mode UI -->
|
||||
<div style="position: relative; margin-bottom: 20px;">
|
||||
<div id="map" style="width: 100%; height: 500px;"></div>
|
||||
|
||||
<!-- Location Mode Indicator -->
|
||||
<div class="location-mode-indicator">
|
||||
📍 Position the map center pin over your campsite location
|
||||
</div>
|
||||
|
||||
<!-- Confirm and Cancel Buttons -->
|
||||
<button type="button" class="confirm-location-btn" onclick="confirmLocation()">
|
||||
✓ Confirm Location
|
||||
</button>
|
||||
<button type="button" class="cancel-location-btn" onclick="cancelLocationMode()">
|
||||
✕ Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible Campsite Form -->
|
||||
<div class="campsite-form-container" id="campsiteFormContainer">
|
||||
@@ -270,8 +358,6 @@ require_once($rootPath . '/components/banner.php');
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="map" style="width: 100%; height: 500px;"></div>
|
||||
|
||||
<!-- Campsites Table -->
|
||||
<div style="margin-top: 40px;">
|
||||
<h4 style="margin-bottom: 20px;">All Campsites</h4>
|
||||
@@ -282,7 +368,7 @@ require_once($rootPath . '/components/banner.php');
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Website</th>
|
||||
<th>Booking Website</th>
|
||||
<th>Phone</th>
|
||||
<th>Added By</th>
|
||||
<th>Actions</th>
|
||||
@@ -302,9 +388,113 @@ require_once($rootPath . '/components/banner.php');
|
||||
|
||||
<script>
|
||||
let map;
|
||||
let centerPinMarker;
|
||||
let isLocationMode = false;
|
||||
const currentUserId = <?php echo $_SESSION['user_id']; ?>;
|
||||
const campsites = <?php echo json_encode($campsites); ?>;
|
||||
|
||||
function startLocationMode() {
|
||||
if (isLocationMode) return;
|
||||
|
||||
isLocationMode = true;
|
||||
|
||||
// Show location mode UI elements
|
||||
document.querySelector(".location-mode-indicator").style.display = "block";
|
||||
document.querySelector(".confirm-location-btn").style.display = "block";
|
||||
document.querySelector(".cancel-location-btn").style.display = "block";
|
||||
document.getElementById("toggleFormBtn").disabled = true;
|
||||
|
||||
// Create invisible marker at map center
|
||||
const mapCenter = map.getCenter();
|
||||
centerPinMarker = new google.maps.Marker({
|
||||
position: mapCenter,
|
||||
map: map,
|
||||
title: "Campsite Location",
|
||||
draggable: true,
|
||||
icon: 'http://maps.google.com/mapfiles/ms/icons/red-dot.png'
|
||||
});
|
||||
|
||||
// Update coordinates when marker is dragged
|
||||
centerPinMarker.addListener('drag', function() {
|
||||
const position = centerPinMarker.getPosition();
|
||||
updateCoordinatesDisplay(position.lat(), position.lng());
|
||||
});
|
||||
|
||||
// Set initial coordinates
|
||||
updateCoordinatesDisplay(mapCenter.lat(), mapCenter.lng());
|
||||
|
||||
// Update coordinates when map is moved
|
||||
const moveListener = map.addListener('center_changed', function() {
|
||||
const mapCenter = map.getCenter();
|
||||
centerPinMarker.setPosition(mapCenter);
|
||||
updateCoordinatesDisplay(mapCenter.lat(), mapCenter.lng());
|
||||
});
|
||||
|
||||
// Store listener for cleanup
|
||||
window.mapMoveListener = moveListener;
|
||||
}
|
||||
|
||||
function updateCoordinatesDisplay(lat, lng) {
|
||||
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);
|
||||
}
|
||||
|
||||
function confirmLocation() {
|
||||
if (!isLocationMode) return;
|
||||
|
||||
isLocationMode = false;
|
||||
|
||||
// Hide location mode UI elements
|
||||
document.querySelector(".location-mode-indicator").style.display = "none";
|
||||
document.querySelector(".confirm-location-btn").style.display = "none";
|
||||
document.querySelector(".cancel-location-btn").style.display = "none";
|
||||
document.getElementById("toggleFormBtn").disabled = false;
|
||||
|
||||
// Remove map move listener
|
||||
if (window.mapMoveListener) {
|
||||
google.maps.event.removeListener(window.mapMoveListener);
|
||||
}
|
||||
|
||||
// Remove the center marker
|
||||
if (centerPinMarker) {
|
||||
centerPinMarker.setMap(null);
|
||||
centerPinMarker = null;
|
||||
}
|
||||
|
||||
// Reset form fields and show form (for new campsite only)
|
||||
resetFormForNewCampsite();
|
||||
document.getElementById("campsiteFormContainer").style.display = "block";
|
||||
document.getElementById("campsiteFormContainer").scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
function cancelLocationMode() {
|
||||
if (!isLocationMode) return;
|
||||
|
||||
isLocationMode = false;
|
||||
|
||||
// Hide location mode UI elements
|
||||
document.querySelector(".location-mode-indicator").style.display = "none";
|
||||
document.querySelector(".confirm-location-btn").style.display = "none";
|
||||
document.querySelector(".cancel-location-btn").style.display = "none";
|
||||
document.getElementById("toggleFormBtn").disabled = false;
|
||||
|
||||
// Remove map move listener
|
||||
if (window.mapMoveListener) {
|
||||
google.maps.event.removeListener(window.mapMoveListener);
|
||||
}
|
||||
|
||||
// Remove the center marker
|
||||
if (centerPinMarker) {
|
||||
centerPinMarker.setMap(null);
|
||||
centerPinMarker = null;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCampsiteForm() {
|
||||
if (isLocationMode) return;
|
||||
|
||||
const container = document.getElementById("campsiteFormContainer");
|
||||
container.style.display = container.style.display === "none" ? "block" : "none";
|
||||
if (container.style.display === "block") {
|
||||
@@ -312,14 +502,40 @@ require_once($rootPath . '/components/banner.php');
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
// Clear the form
|
||||
document.getElementById("addCampsiteForm").reset();
|
||||
function resetFormForNewCampsite() {
|
||||
// This is called when confirming location for a NEW campsite
|
||||
// Only clears text fields and removes ID, but keeps country/province selections
|
||||
document.querySelector("#addCampsiteForm input[name='name']").value = '';
|
||||
document.querySelector("#addCampsiteForm textarea[name='description']").value = '';
|
||||
document.querySelector("#addCampsiteForm input[name='website']").value = '';
|
||||
document.querySelector("#addCampsiteForm input[name='telephone']").value = '';
|
||||
|
||||
// Remove the ID input if it exists
|
||||
let idInput = document.querySelector("#addCampsiteForm input[name='id']");
|
||||
if (idInput) {
|
||||
idInput.remove();
|
||||
}
|
||||
|
||||
// Change form heading
|
||||
document.querySelector("#campsiteFormContainer h5").textContent = "Add New Campsite";
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
// This is called when canceling the form - fully resets everything
|
||||
document.querySelector("#campsiteFormContainer h5").textContent = "Add New Campsite";
|
||||
|
||||
// Clear the form completely
|
||||
document.getElementById("addCampsiteForm").reset();
|
||||
|
||||
// Remove the ID input if it exists
|
||||
let idInput = document.querySelector("#addCampsiteForm input[name='id']");
|
||||
if (idInput) {
|
||||
idInput.remove();
|
||||
}
|
||||
|
||||
// Clear coordinate displays
|
||||
document.getElementById("latitude_display").value = '';
|
||||
document.getElementById("longitude_display").value = '';
|
||||
}
|
||||
|
||||
function initMap() {
|
||||
@@ -327,25 +543,10 @@ require_once($rootPath . '/components/banner.php');
|
||||
center: {
|
||||
lat: -28.0,
|
||||
lng: 24.0
|
||||
}, // SA center
|
||||
},
|
||||
zoom: 6,
|
||||
});
|
||||
|
||||
map.addListener("click", function(e) {
|
||||
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);
|
||||
|
||||
// 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")
|
||||
.then(response => response.json())
|
||||
@@ -366,7 +567,7 @@ require_once($rootPath . '/components/banner.php');
|
||||
${site.description ? site.description + "<br>" : ""}
|
||||
${site.website ? `<a href="${site.website}" target="_blank">Visit Website</a><br>` : ""}
|
||||
${site.telephone ? `Phone: ${site.telephone}<br>` : ""}
|
||||
${site.thumbnail ? `<img src="${site.thumbnail}" style="width: 100%; max-width: 200px; border-radius: 8px; margin-top: 5px;">` : ""}
|
||||
${site.thumbnail ? `<img src="${site.thumbnail}" style="width: 100%; max-width: 200px; border-radius: 8px; margin-top: 5px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);">` : ""}
|
||||
${site.user && site.user.first_name ? `
|
||||
<div class="user-info mt-2 d-flex align-items-center">
|
||||
<img src="${site.user.profile_pic}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover; margin-right: 10px;">
|
||||
@@ -451,6 +652,11 @@ require_once($rootPath . '/components/banner.php');
|
||||
? `${site.user.first_name} ${site.user.last_name}`
|
||||
: "Unknown";
|
||||
|
||||
// Only show edit button if current user is the owner
|
||||
const editButtonHTML = site.user_id == currentUserId
|
||||
? `<button class="btn btn-sm btn-warning" onclick='editCampsite(${JSON.stringify(site)})'>Edit</button>`
|
||||
: '';
|
||||
|
||||
row.innerHTML = `
|
||||
<td><strong>${site.name}</strong></td>
|
||||
<td>${site.description ? site.description.substring(0, 50) + (site.description.length > 50 ? '...' : '') : '-'}</td>
|
||||
@@ -458,7 +664,7 @@ require_once($rootPath . '/components/banner.php');
|
||||
<td>${site.telephone || '-'}</td>
|
||||
<td><small>${userName}</small></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-warning" onclick='editCampsite(${JSON.stringify(site)})'>Edit</button>
|
||||
${editButtonHTML}
|
||||
<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>
|
||||
`;
|
||||
@@ -469,32 +675,89 @@ require_once($rootPath . '/components/banner.php');
|
||||
}
|
||||
|
||||
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);
|
||||
// Change form heading to indicate editing
|
||||
document.querySelector("#campsiteFormContainer h5").textContent = "Edit Campsite";
|
||||
|
||||
// Add hidden ID input
|
||||
let idInput = document.querySelector("#addCampsiteForm input[name='id']");
|
||||
if (!idInput) {
|
||||
idInput = document.createElement("input");
|
||||
idInput.type = "hidden";
|
||||
idInput.name = "id";
|
||||
document.querySelector("#addCampsiteForm").appendChild(idInput);
|
||||
}
|
||||
idInput.value = site.id;
|
||||
// Pre-fill form with a slight delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
document.querySelector("#addCampsiteForm input[name='name']").value = site.name;
|
||||
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);
|
||||
|
||||
// Set country and province LAST to ensure they stick
|
||||
document.querySelector("#addCampsiteForm select[name='country']").value = site.country || '';
|
||||
document.querySelector("#addCampsiteForm select[name='province']").value = site.province || '';
|
||||
|
||||
// Add hidden ID input
|
||||
let idInput = document.querySelector("#addCampsiteForm input[name='id']");
|
||||
if (!idInput) {
|
||||
idInput = document.createElement("input");
|
||||
idInput.type = "hidden";
|
||||
idInput.name = "id";
|
||||
document.querySelector("#addCampsiteForm").appendChild(idInput);
|
||||
}
|
||||
idInput.value = site.id;
|
||||
}, 0);
|
||||
|
||||
// Show the form container
|
||||
document.getElementById("campsiteFormContainer").style.display = "block";
|
||||
document.getElementById("campsiteFormContainer").scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
function filterCampsites() {
|
||||
const filterInput = document.getElementById("campsitesFilter");
|
||||
const filterValue = filterInput.value.toLowerCase();
|
||||
const tableBody = document.getElementById("campsitesTableBody");
|
||||
const rows = tableBody.getElementsByTagName("tr");
|
||||
|
||||
let visibleRows = 0;
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
const text = row.textContent.toLowerCase();
|
||||
|
||||
// Show rows that match the filter or are group headers
|
||||
if (text.includes(filterValue) || row.innerHTML.includes('fas fa-globe')) {
|
||||
row.style.display = "";
|
||||
if (row.innerHTML.includes('fas fa-globe') === false) {
|
||||
visibleRows++;
|
||||
}
|
||||
} else {
|
||||
row.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Hide group headers if no campsites match in that group
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
if (row.innerHTML.includes('fas fa-globe')) {
|
||||
// Check if next visible row is also a header
|
||||
let hasVisibleChildren = false;
|
||||
for (let j = i + 1; j < rows.length; j++) {
|
||||
if (rows[j].style.display !== "none") {
|
||||
if (!rows[j].innerHTML.includes('fas fa-globe')) {
|
||||
hasVisibleChildren = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
row.style.display = hasVisibleChildren ? "" : "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add filter event listener when page loads
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const filterInput = document.getElementById("campsitesFilter");
|
||||
if (filterInput) {
|
||||
filterInput.addEventListener("keyup", filterCampsites);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyC-JuvnbUYc8WGjQBFFVZtKiv5_bFJoWLU&callback=initMap" async defer></script>
|
||||
|
||||
@@ -9,7 +9,7 @@ $eft_id = strtoupper("SUBS " . date("Y") . " " . getLastName($user_id));
|
||||
$status = 'AWAITING PAYMENT';
|
||||
$description = 'Membership Fees ' . date("Y") . " " . getLastName($user_id);
|
||||
|
||||
$payment_amount = 2500; // Assuming a fixed membership fee, adjust as needed
|
||||
$payment_amount = 2600; // Assuming a fixed membership fee, adjust as needed
|
||||
$payment_date = date('Y-m-d');
|
||||
$membership_start_date = date('Y-01-01');
|
||||
$membership_end_date = date('Y-12-31');
|
||||
|
||||
@@ -18,6 +18,40 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
die('Security token validation failed. Please try again.');
|
||||
}
|
||||
|
||||
// Check if user already has a membership application
|
||||
$check_stmt = $conn->prepare("SELECT COUNT(*) as count FROM membership_application WHERE user_id = ?");
|
||||
$check_stmt->bind_param("i", $user_id);
|
||||
$check_stmt->execute();
|
||||
$check_result = $check_stmt->get_result();
|
||||
$check_row = $check_result->fetch_assoc();
|
||||
$check_stmt->close();
|
||||
|
||||
if ($check_row['count'] > 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'You have already submitted a membership application. Please check your email for membership details.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if user already has a membership fee record
|
||||
$fee_check_stmt = $conn->prepare("SELECT COUNT(*) as count FROM membership_fees WHERE user_id = ?");
|
||||
$fee_check_stmt->bind_param("i", $user_id);
|
||||
$fee_check_stmt->execute();
|
||||
$fee_result = $fee_check_stmt->get_result();
|
||||
$fee_row = $fee_result->fetch_assoc();
|
||||
$fee_check_stmt->close();
|
||||
|
||||
if ($fee_row['count'] > 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'You already have a membership fee record. Please contact support if you need to update your application.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get all the form fields with validation
|
||||
$first_name = validateName($_POST['first_name'] ?? '');
|
||||
if ($first_name === false) {
|
||||
@@ -188,11 +222,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Rollback the transaction in case of error
|
||||
$conn->rollback();
|
||||
|
||||
// Error response
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'message' => 'Error: ' . $e->getMessage()
|
||||
];
|
||||
// Check for duplicate key error
|
||||
$errorMessage = $e->getMessage();
|
||||
if (strpos($errorMessage, 'Duplicate') !== false || strpos($errorMessage, '1062') !== false) {
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'message' => 'You have already submitted a membership application. Please check your email for membership details.'
|
||||
];
|
||||
} else {
|
||||
// Error response
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'message' => 'Error: ' . $errorMessage
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Return the response in JSON format
|
||||
|
||||