3 Commits

Author SHA1 Message Date
twotalesanimation
05f74f1b86 feat: prevent duplicate membership applications and fees
- Add UNIQUE constraint on membership_application.user_id (one app per user)
- Add UNIQUE constraint on membership_fees.user_id (one fee record per user)
- Add validation checks in process_application.php before inserting
- Improve error messages for duplicate submission attempts
- Add migration script to clean up existing duplicates before constraints
- Update checkMembershipApplication to set session message on redirect
- Add comprehensive documentation of duplicate prevention architecture

Individual payments/EFTs are tracked separately in payments table
2025-12-05 09:42:42 +02:00
twotalesanimation
9133b7bbc6 feat: improve campsites and events management UX
- Add map-based location picker with centered pin for campsites (two-step process)
- Hide edit buttons for campsites not owned by current user
- Allow numbers in campsite names (fix validateName function)
- Prepopulate edit form with existing campsite data
- Preserve country/province selection when confirming location
- Add real-time filter functionality to campsites table
- Fix events publish button error handling (use output buffering cleanup)
- Improve AJAX response handling with complete callback

Changes:
- src/pages/bookings/campsites.php: Location mode UI, filter, edit form improvements
- src/config/functions.php: Allow numbers in validateName regex
- src/admin/toggle_event_published.php: Clean output buffers before JSON response
- src/admin/admin_events.php: Use complete callback instead of success/error handlers
2025-12-05 09:20:48 +02:00
twotalesanimation
b52c46b67c feat: add campsites link to members area menu with membership access control
- Replace 'Coming Soon!' with 'Campsites' link in Members Area dropdown
- Add membership verification check to campsites.php
- Redirect non-logged-in users to login page
- Redirect non-members to index page
- Only active members can access campsites feature
2025-12-04 23:01:28 +02:00
17 changed files with 537 additions and 80 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View 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.

View 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);

View File

@@ -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>

View File

@@ -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');
}
});
});

View File

@@ -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()]);
}

View File

@@ -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;
}

View File

@@ -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>

View File

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

View File

@@ -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