Binary file not shown.
@@ -21,11 +21,17 @@ require('template/header.phtml')
|
||||
<div class="card-header bg-light py-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<h5 class="mb-0 fw-bold text-primary">
|
||||
<i class="bi bi-geo-alt-fill me-2 text-success"></i>Facilities
|
||||
</h5>
|
||||
<!-- Badge showing the number of facilities -->
|
||||
<span class="badge bg-success rounded-pill ms-2" id="facilityCount"></span>
|
||||
<!-- Search and filter controls -->
|
||||
<div class="d-flex flex-column flex-lg-row search-controls mx-auto">
|
||||
<form class="d-flex flex-column flex-lg-row gap-2 w-100" role="search" action="" method="POST">
|
||||
<div class="input-group flex-grow-1">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<i class="bi bi-search text-success"></i>
|
||||
</span>
|
||||
<input class="form-control border-start-0" id="searchInput" type="search" name="filter" placeholder="Search..." aria-label="Search">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Admin-only buttons -->
|
||||
<div id="adminButtons" style="display: none;">
|
||||
@@ -99,33 +105,6 @@ require('template/header.phtml')
|
||||
regularUserView.style.display = isAdmin ? 'none' : 'block';
|
||||
}
|
||||
|
||||
// Update table headers based on user role :DDD (it just shows the ID column for admins...)
|
||||
if (tableHeaderRow) {
|
||||
if (isAdmin) {
|
||||
// Admin view - show all columns and bigger management actions
|
||||
tableHeaderRow.innerHTML = `
|
||||
<th class="fw-semibold" style="width: 40px;">ID</th>
|
||||
<th class="fw-semibold" style="width: 15%;">Title</th>
|
||||
<th class="fw-semibold text-center" style="width: 10%;">Category</th>
|
||||
<th class="fw-semibold" style="width: 25%;">Description</th>
|
||||
<th class="fw-semibold" style="width: 20%;">Address</th>
|
||||
<th class="fw-semibold text-center" style="width: 12%;">Coordinates</th>
|
||||
<th class="fw-semibold text-center" style="width: 8%;">Contributor</th>
|
||||
<th class="fw-semibold text-center" style="width: 10%;">Actions</th>
|
||||
`;
|
||||
} else {
|
||||
// Regular user view - hide ID column and make management actions smaller
|
||||
tableHeaderRow.innerHTML = `
|
||||
<th class="fw-semibold" style="width: 17%;">Title</th>
|
||||
<th class="fw-semibold text-center" style="width: 11%;">Category</th>
|
||||
<th class="fw-semibold" style="width: 27%;">Description</th>
|
||||
<th class="fw-semibold" style="width: 20%;">Address</th>
|
||||
<th class="fw-semibold text-center" style="width: 12%;">Coordinates</th>
|
||||
<th class="fw-semibold text-center" style="width: 8%;">Contributor</th>
|
||||
<th class="fw-semibold text-center" style="width: 5%;">Actions</th>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI when the page loads
|
||||
@@ -139,28 +118,4 @@ require('template/header.phtml')
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Script to update the facility count badge -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Update facility count badge based on data in sessionStorage
|
||||
const updateFacilityCount = () => {
|
||||
const facilityData = JSON.parse(sessionStorage.getItem('facilityData') || '[]');
|
||||
const countBadge = document.getElementById('facilityCount');
|
||||
if (countBadge) {
|
||||
countBadge.textContent = `${facilityData.length} facilities`;
|
||||
}
|
||||
};
|
||||
|
||||
// Initial count update when the page loads
|
||||
updateFacilityCount();
|
||||
|
||||
// Listen for changes in facility data to update the count
|
||||
window.addEventListener('storage', function(e) {
|
||||
if (e.key === 'facilityData') {
|
||||
updateFacilityCount();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require('template/footer.phtml');?>
|
194
Views/map.phtml
Normal file
194
Views/map.phtml
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php require_once('template/header.phtml') ?>
|
||||
|
||||
<style>
|
||||
#mapOverlay {
|
||||
transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#mapOverlay.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Custom styles for facility popups */
|
||||
.facility-popup .leaflet-popup-content-wrapper {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.facility-popup .leaflet-popup-content {
|
||||
margin: 0;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.facility-details {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.facility-details::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.facility-details::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.facility-details::-webkit-scrollbar-thumb {
|
||||
background: #198754;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Comment form styles */
|
||||
.comment-form {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.comment-form textarea {
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
/* Facility list container styles */
|
||||
.facility-list-container {
|
||||
height: calc(100vh - 400px); /* Adjust based on your layout */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.facility-list {
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.facility-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.facility-list::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.facility-list::-webkit-scrollbar-thumb {
|
||||
background: #198754;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">
|
||||
Notice: Facility locations are currently limited to UK Cities.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid pt-0 py-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<!-- Postcode and radius controls -->
|
||||
<div class="mb-4">
|
||||
<h5 class="mb-3">Location Settings</h5>
|
||||
<form id="postcodeForm" class="mb-3">
|
||||
<div class="mb-3">
|
||||
<label for="postcode" class="form-label">Enter Postcode</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light">
|
||||
<i class="bi bi-geo-alt text-success"></i>
|
||||
</span>
|
||||
<input type="text" class="form-control" id="postcode" name="postcode"
|
||||
placeholder="e.g. M1 5GD" required>
|
||||
<button class="btn btn-success" type="submit">
|
||||
<i class="bi bi-search me-1"></i>Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="radius" class="form-label">Search Radius (miles)</label>
|
||||
<select class="form-select" id="radius" name="radius">
|
||||
<option value="1">1 mile</option>
|
||||
<option value="5">5 miles</option>
|
||||
<option value="10" selected>10 miles</option>
|
||||
<option value="25">25 miles</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Facility list search and container -->
|
||||
<div class="facility-list-container">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text bg-light">
|
||||
<i class="bi bi-search text-success"></i>
|
||||
</span>
|
||||
<input type="text" class="form-control" id="facilitySearch"
|
||||
placeholder="Search facilities...">
|
||||
</div>
|
||||
<div id="facilityList" class="list-group list-group-flush facility-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 col-lg-9">
|
||||
<!-- Map container with blur overlay -->
|
||||
<div class="position-relative" style="height: 700px;">
|
||||
<div id="map" style="height: 100%; width: 100%; z-index: 1;"></div>
|
||||
<div id="mapOverlay" class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
|
||||
style="backdrop-filter: blur(8px); z-index: 2; background: rgba(255,255,255,0.5);">
|
||||
<div class="text-center">
|
||||
<i class="bi bi-geo-alt text-success" style="font-size: 3rem;"></i>
|
||||
<h4 class="mt-3">Enter a Postcode</h4>
|
||||
<p class="text-muted">Please enter a postcode to view facilities on the map</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Leaflet CSS and JS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||||
|
||||
<!-- Add our map handler -->
|
||||
<script src="/public/js/mapHandler.js"></script>
|
||||
|
||||
<script>
|
||||
// Add facility search functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const facilitySearch = document.getElementById('facilitySearch');
|
||||
const facilityList = document.getElementById('facilityList');
|
||||
|
||||
if (facilitySearch && facilityList) {
|
||||
facilitySearch.addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
const items = facilityList.getElementsByClassName('list-group-item');
|
||||
|
||||
Array.from(items).forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(searchTerm) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require('template/statusModal.phtml') ?>
|
||||
<?php require_once('template/footer.phtml') ?>
|
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<!-- Create facility form -->
|
||||
<form id="createFacilityForm">
|
||||
<form id="createForm">
|
||||
<!-- Form fields -->
|
||||
<div class="mb-3">
|
||||
<label for="createTitle" class="form-label">Facility Name</label>
|
||||
@@ -41,53 +41,49 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="createLatitude" class="form-label">Latitude</label>
|
||||
<input type="number" step="any" class="form-control" id="createLatitude" name="latitude" required>
|
||||
<input type="number" step="any" class="form-control" id="createLatitude" name="lat" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="createLongitude" class="form-label">Longitude</label>
|
||||
<input type="number" step="any" class="form-control" id="createLongitude" name="longitude" required>
|
||||
<input type="number" step="any" class="form-control" id="createLongitude" name="lng" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="createAddress" class="form-label">Address</label>
|
||||
<input type="text" class="form-control" id="createAddress" name="address" required>
|
||||
<label for="createHouseNumber" class="form-label">House Number/Name</label>
|
||||
<input type="text" class="form-control" id="createHouseNumber" name="houseNumber" required>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="createContact" class="form-label">Contact Information</label>
|
||||
<input type="text" class="form-control" id="createContact" name="contact">
|
||||
<label for="createStreetName" class="form-label">Street Name</label>
|
||||
<input type="text" class="form-control" id="createStreetName" name="streetName" required>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="createWebsite" class="form-label">Website</label>
|
||||
<input type="url" class="form-control" id="createWebsite" name="website" placeholder="https://">
|
||||
<label for="createTown" class="form-label">Town/City</label>
|
||||
<input type="text" class="form-control" id="createTown" name="town" required>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="createHours" class="form-label">Operating Hours</label>
|
||||
<input type="text" class="form-control" id="createHours" name="hours" placeholder="e.g., Mon-Fri: 9am-5pm">
|
||||
<label for="createCounty" class="form-label">County</label>
|
||||
<input type="text" class="form-control" id="createCounty" name="county" required>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="createStatus" class="form-label">Status</label>
|
||||
<select class="form-select" id="createStatus" name="status" required>
|
||||
<option value="operational">Operational</option>
|
||||
<option value="limited">Limited Service</option>
|
||||
<option value="closed">Temporarily Closed</option>
|
||||
<option value="planned">Planned</option>
|
||||
</select>
|
||||
<label for="createPostcode" class="form-label">Postcode</label>
|
||||
<input type="text" class="form-control" id="createPostcode" name="postcode" required>
|
||||
</div>
|
||||
|
||||
<div id="createError" class="alert alert-danger" style="display: none;"></div>
|
||||
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-plus-circle me-1"></i>Create Facility
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" id="createFacilityButton">
|
||||
<i class="bi bi-plus-circle me-1"></i>Create Facility
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -19,16 +19,16 @@
|
||||
<!-- Note: facilityData.js is already included in the header -->
|
||||
<script src="/public/js/comments.js"></script>
|
||||
|
||||
<!-- Initialize components -->
|
||||
<!-- initialise components -->
|
||||
<script>
|
||||
// Only run initialization if not already done
|
||||
if (!window.initializationComplete) {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize auth service
|
||||
// initialise auth service
|
||||
const loginButton = document.querySelector('[data-bs-toggle="modal"]');
|
||||
const loginModal = document.getElementById('loginModal');
|
||||
|
||||
// Initialize all modals
|
||||
// initialise all modals
|
||||
try {
|
||||
const modalElements = document.querySelectorAll('.modal');
|
||||
modalElements.forEach(modalElement => {
|
||||
@@ -53,7 +53,20 @@
|
||||
console.error('Error initializing modals:', error);
|
||||
}
|
||||
|
||||
// Initialize auth form handlers
|
||||
// initialise CommentsManager
|
||||
CommentsManager.state.isDomReady = true;
|
||||
if (window.simpleAuth) {
|
||||
CommentsManager.state.isAuthReady = true;
|
||||
CommentsManager.checkinitialise();
|
||||
} else {
|
||||
window.addEventListener('simpleAuthReady', () => {
|
||||
console.log('SimpleAuth is now ready');
|
||||
CommentsManager.state.isAuthReady = true;
|
||||
CommentsManager.checkinitialise();
|
||||
});
|
||||
}
|
||||
|
||||
// initialise auth form handlers
|
||||
const loginForm = document.querySelector('#loginModal form');
|
||||
const loginError = document.querySelector('#loginError');
|
||||
const captchaContainer = document.querySelector('.captcha-container');
|
||||
|
@@ -12,7 +12,7 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
|
||||
<!-- CSS theme -->
|
||||
<link href="/public/css/my-style.css" rel="stylesheet">
|
||||
<link href="/public/css/default.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link href="/public/css/bootstrap-icons.css" rel="stylesheet">
|
||||
@@ -49,7 +49,7 @@
|
||||
if (Array.isArray(initialData) && initialData.length > 0) {
|
||||
sessionStorage.setItem('facilityData', JSON.stringify(initialData));
|
||||
|
||||
// Initialize based on DOM state to ensure scripts run at the right time
|
||||
// initialise based on DOM state to ensure scripts run at the right time
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
if (typeof initialiseFacilityData === 'function') {
|
||||
initialiseFacilityData(initialData);
|
||||
@@ -165,54 +165,7 @@
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Search and filter controls -->
|
||||
<div class="d-flex flex-column flex-lg-row search-controls mx-auto">
|
||||
<form class="d-flex flex-column flex-lg-row gap-2 w-100" role="search" action="" method="POST">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<i class="bi bi-sort-alpha-down text-success"></i>
|
||||
</span>
|
||||
<select name="sort" class="form-select border-start-0 filter-control" id="sort">
|
||||
<option selected value="title">Title</option>
|
||||
<option value="category">Category</option>
|
||||
<option value="description">Description</option>
|
||||
<option value="streetName">Street</option>
|
||||
<option value="county">County</option>
|
||||
<option value="town">Town</option>
|
||||
<option value="postcode">Postcode</option>
|
||||
<option value="contributor">Contributor</option>
|
||||
</select>
|
||||
<select class="form-select sort-control" name="dir" id="dir" data-order="asc">
|
||||
<option value="asc">Asc</option>
|
||||
<option value="desc">Desc</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<i class="bi bi-filter-circle-fill text-success"></i>
|
||||
</span>
|
||||
<select name="filterCat" class="form-select border-start-0 filter-control" id="filterCat">
|
||||
<option value="title">Title</option>
|
||||
<option value="category">Category</option>
|
||||
<option value="description">Description</option>
|
||||
<option value="streetName">Street</option>
|
||||
<option value="county">County</option>
|
||||
<option value="town">Town</option>
|
||||
<option value="postcode">Postcode</option>
|
||||
<option value="contributor">Contributor</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<i class="bi bi-search text-success"></i>
|
||||
</span>
|
||||
<input class="form-control border-start-0" id="searchInput" type="search" name="filter" placeholder="Search..." aria-label="Search">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- User account section -->
|
||||
<div class="ms-lg-3 mt-3 mt-lg-0" id="userAuthSection">
|
||||
@@ -374,14 +327,14 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<p class="small text-muted mb-0">Don't have an account? <a href="#" class="text-success">Register</a></p>
|
||||
<p class="small text-muted mb-0">Don't have an account? <a href="#" onclick="alert('Please contact the administrator to create an account.');" class="text-success">Register</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize login modal functionality
|
||||
// initialise login modal functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const loginModal = document.getElementById('loginModal');
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center gap-2">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center gap-3">
|
||||
<div class="text-muted small">
|
||||
<span id="paginationInfo" class="d-flex align-items-center">
|
||||
<i class="bi bi-info-circle me-2 text-success"></i>
|
||||
@@ -7,8 +7,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Pagination controls -->
|
||||
<nav aria-label="Facility table pagination">
|
||||
<ul class="pagination pagination-sm mb-0" id="paginationControls">
|
||||
<nav class="bg-transparent" aria-label="Facility table pagination">
|
||||
<ul class="pagination pagination-sm mb-0 border-2 rounded border-success" id="paginationControls">
|
||||
<!-- First page button -->
|
||||
<li class="page-item">
|
||||
<a class="page-link border-0 text-success" href="#" aria-label="First" id="firstPage">
|
||||
@@ -46,7 +46,6 @@
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
19
map.php
Normal file
19
map.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
// load dataset
|
||||
require_once('Models/FacilityDataSet.php');
|
||||
|
||||
// make a view class
|
||||
$view = new stdClass();
|
||||
$view->pageTitle = 'Map';
|
||||
|
||||
// initialise facility data
|
||||
$facilityDataSet = new FacilityDataSet();
|
||||
$view->facilityDataSet = $facilityDataSet->fetchAll();
|
||||
|
||||
// Log any critical errors
|
||||
if ($view->facilityDataSet === false) {
|
||||
error_log('Error fetching facility data');
|
||||
}
|
||||
|
||||
// load main view
|
||||
require_once('Views/map.phtml');
|
@@ -133,21 +133,30 @@ class ApiClient {
|
||||
});
|
||||
|
||||
try {
|
||||
// Validate auth state before making request
|
||||
if (!this.authFetch) {
|
||||
throw new Error('Auth fetch not available');
|
||||
}
|
||||
|
||||
if (action === 'status' && (!data.facilityId || !data.statusComment)) {
|
||||
throw new Error('Missing required data for status update');
|
||||
}
|
||||
|
||||
// Use authenticated fetch for all facility requests
|
||||
const response = await this.authFetch('/facilitycontroller.php', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
requireAuth: true // Explicitly require authentication
|
||||
requireAuth: true
|
||||
});
|
||||
|
||||
// Parse the response
|
||||
const jsonData = await response.json();
|
||||
|
||||
// Check if response is ok
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
throw new Error(`HTTP error! status: ${response.status}, message: ${jsonData.error || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
// Parse the JSON response
|
||||
const jsonData = await response.json();
|
||||
console.log('Facility API response:', { action, data: jsonData });
|
||||
return jsonData;
|
||||
} catch (error) {
|
||||
console.error('Facility API error:', error);
|
||||
@@ -242,7 +251,11 @@ class ApiClient {
|
||||
* @returns {Promise<Object>} The response data
|
||||
*/
|
||||
async updateFacilityStatus(statusId, editStatus, facilityId) {
|
||||
return this.facility('editStatus', { statusId, editStatus, facilityId });
|
||||
return this.facility('editStatus', {
|
||||
statusId: statusId,
|
||||
statusComment: editStatus,
|
||||
facilityId: facilityId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,7 +272,7 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize API client
|
||||
// initialise API client
|
||||
const api = new ApiClient();
|
||||
|
||||
// Export API client
|
||||
|
@@ -1,46 +1,46 @@
|
||||
/**
|
||||
* Comments functionality for facility management
|
||||
* Facility status (comments) manager for adding, removing and editing user comments.
|
||||
*/
|
||||
|
||||
// Create a namespace for comments functionality
|
||||
// Create a namespace to avoid global scope conflicts with facilityData.js
|
||||
const CommentsManager = {
|
||||
// Track initialization states
|
||||
// Initialization states
|
||||
state: {
|
||||
isInitializing: false,
|
||||
isInitialized: false,
|
||||
isinitialised: false,
|
||||
isDomReady: false,
|
||||
isAuthReady: false
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize comments functionality
|
||||
* initialise status functionality
|
||||
*/
|
||||
initialize() {
|
||||
if (this.state.isInitialized) return;
|
||||
initialise() {
|
||||
if (this.state.isinitialised) return;
|
||||
|
||||
console.log('Initializing comments...');
|
||||
|
||||
// Initialize comment modal handlers
|
||||
this.initializeCommentModals();
|
||||
// initialise comment modal handlers
|
||||
this.initialiseCommentModals();
|
||||
|
||||
// Set up form handlers
|
||||
this.setupCommentFormHandlers();
|
||||
|
||||
console.log('Comments initialized with auth state:', {
|
||||
console.log('Comments initialised with auth state:', {
|
||||
isAuthenticated: this.isAuthenticated(),
|
||||
user: window.simpleAuth.getUser()
|
||||
});
|
||||
|
||||
this.state.isInitialized = true;
|
||||
this.state.isinitialised = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if we can initialize
|
||||
* Check if initialisation possible
|
||||
*/
|
||||
checkInitialize() {
|
||||
checkinitialise() {
|
||||
if (this.state.isDomReady && this.state.isAuthReady && !this.state.isInitializing) {
|
||||
this.state.isInitializing = true;
|
||||
this.initialize();
|
||||
this.initialise();
|
||||
this.state.isInitializing = false;
|
||||
}
|
||||
},
|
||||
@@ -53,33 +53,33 @@ const CommentsManager = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize comment modals
|
||||
* initialise comment modals
|
||||
*/
|
||||
initializeCommentModals() {
|
||||
initialiseCommentModals() {
|
||||
// Status modal (comments view)
|
||||
const statusModal = document.getElementById('statusModal');
|
||||
if (statusModal) {
|
||||
statusModal.addEventListener('show.bs.modal', (event) => {
|
||||
console.log('Comments modal is about to show');
|
||||
// Get the button that triggered the modal
|
||||
const button = event.relatedTarget;
|
||||
// Get the facility ID from the data attribute
|
||||
const facilityId = button.getAttribute('data-facility-id');
|
||||
console.log('Facility ID for comments:', facilityId);
|
||||
// Get facility ID from either the button or the modal's data attribute
|
||||
let facilityId;
|
||||
|
||||
// First try to get it from the button that triggered the modal
|
||||
if (event.relatedTarget) {
|
||||
facilityId = event.relatedTarget.getAttribute('data-facility-id');
|
||||
}
|
||||
|
||||
// If not found in button, try the modal's data attribute
|
||||
if (!facilityId && statusModal.hasAttribute('data-facility-id')) {
|
||||
facilityId = statusModal.getAttribute('data-facility-id');
|
||||
}
|
||||
|
||||
if (!facilityId) {
|
||||
console.error('No facility ID found for comments');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the facility ID in the comment form
|
||||
const commentForm = document.getElementById('commentForm');
|
||||
if (commentForm) {
|
||||
const facilityIdInput = commentForm.querySelector('#commentFacilityId');
|
||||
if (facilityIdInput) {
|
||||
facilityIdInput.value = facilityId;
|
||||
}
|
||||
}
|
||||
// Store the facility ID on the modal for later use
|
||||
statusModal.setAttribute('data-facility-id', facilityId);
|
||||
|
||||
// Load facility comments
|
||||
this.loadFacilityComments(facilityId);
|
||||
@@ -90,13 +90,10 @@ const CommentsManager = {
|
||||
const editCommentModal = document.getElementById('editCommentModal');
|
||||
if (editCommentModal) {
|
||||
editCommentModal.addEventListener('show.bs.modal', (event) => {
|
||||
console.log('Edit comment modal is about to show');
|
||||
const button = event.relatedTarget;
|
||||
const commentId = button.getAttribute('data-comment-id');
|
||||
const commentText = button.getAttribute('data-comment-text');
|
||||
|
||||
console.log('Comment ID:', commentId, 'Comment text:', commentText);
|
||||
|
||||
// Set the comment ID and text in the form
|
||||
const editForm = document.getElementById('editCommentForm');
|
||||
if (editForm) {
|
||||
@@ -151,27 +148,34 @@ const CommentsManager = {
|
||||
|
||||
const formData = new FormData(commentForm);
|
||||
|
||||
// Get form data
|
||||
// Get form data and ensure proper types
|
||||
const statusComment = formData.get('commentText');
|
||||
const facilityId = formData.get('facilityId');
|
||||
|
||||
console.log('Comment form data:', { facilityId, statusComment });
|
||||
// Validate form data
|
||||
if (!facilityId) {
|
||||
console.error('No facility ID found in form');
|
||||
alert('Error: No facility ID found');
|
||||
commentForm.submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!statusComment) {
|
||||
alert('Please enter a comment');
|
||||
commentForm.submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Sending comment request...');
|
||||
// Use the API client to add a status comment
|
||||
const data = await window.api.addFacilityStatus(facilityId, statusComment);
|
||||
|
||||
console.log('Comment response:', data);
|
||||
const data = await window.api.addFacilityStatus(facilityId.toString(), statusComment);
|
||||
|
||||
if (data.success) {
|
||||
console.log('Comment added successfully');
|
||||
|
||||
// Reset the form
|
||||
commentForm.reset();
|
||||
|
||||
// Reload comments to show the new one
|
||||
this.loadFacilityComments(facilityId);
|
||||
this.loadFacilityComments(facilityId.toString());
|
||||
} else {
|
||||
console.error('Comment failed:', data.error);
|
||||
alert(data.error || 'Failed to add comment');
|
||||
@@ -249,21 +253,8 @@ const CommentsManager = {
|
||||
* Creates a comment form dynamically for authenticated users
|
||||
*/
|
||||
createCommentFormForAuthenticatedUser(facilityId) {
|
||||
// Add detailed logging of auth state
|
||||
console.log('Creating comment form with auth state:', {
|
||||
simpleAuthExists: !!window.simpleAuth,
|
||||
simpleAuthMethods: window.simpleAuth ? Object.keys(window.simpleAuth) : null,
|
||||
token: window.simpleAuth ? window.simpleAuth.getToken() : null,
|
||||
user: window.simpleAuth ? window.simpleAuth.getUser() : null,
|
||||
localStorage: {
|
||||
token: localStorage.getItem('token'),
|
||||
user: localStorage.getItem('user')
|
||||
}
|
||||
});
|
||||
|
||||
// First check if simpleAuth is available
|
||||
if (!window.simpleAuth) {
|
||||
console.warn('SimpleAuth not initialized yet');
|
||||
return `
|
||||
<div class="alert alert-warning mb-0">
|
||||
<i class="bi bi-hourglass-split me-2"></i>
|
||||
@@ -278,14 +269,7 @@ const CommentsManager = {
|
||||
const user = window.simpleAuth.getUser();
|
||||
const isAuthenticated = window.simpleAuth.isAuthenticated();
|
||||
|
||||
console.log('Authentication validation:', {
|
||||
hasToken: !!token,
|
||||
hasUser: !!user,
|
||||
isAuthenticated: isAuthenticated
|
||||
});
|
||||
|
||||
if (!isAuthenticated || !token || !user) {
|
||||
console.log('User not authenticated:', { isAuthenticated, token: !!token, user: !!user });
|
||||
return `
|
||||
<div class="alert alert-info mb-0">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
@@ -295,7 +279,6 @@ const CommentsManager = {
|
||||
}
|
||||
|
||||
// User is authenticated, create the comment form
|
||||
console.log('User is authenticated, creating comment form');
|
||||
return `
|
||||
<form id="commentForm" class="mt-3">
|
||||
<input type="hidden" id="commentFacilityId" name="facilityId" value="${this.escapeHtml(facilityId)}">
|
||||
@@ -327,24 +310,30 @@ const CommentsManager = {
|
||||
*/
|
||||
async loadFacilityComments(facilityId) {
|
||||
try {
|
||||
console.log('Loading comments for facility:', facilityId);
|
||||
if (!facilityId) {
|
||||
throw new Error('No facility ID provided');
|
||||
}
|
||||
|
||||
// Ensure facilityId is a string
|
||||
facilityId = facilityId.toString();
|
||||
|
||||
// Show loading indicator
|
||||
const commentsContainer = document.getElementById('commentsContainer');
|
||||
if (commentsContainer) {
|
||||
commentsContainer.innerHTML = `
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-success" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2 text-muted">Loading comments...</p>
|
||||
</div>
|
||||
`;
|
||||
if (!commentsContainer) {
|
||||
throw new Error('Comments container not found');
|
||||
}
|
||||
|
||||
commentsContainer.innerHTML = `
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-success" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2 text-muted">Loading comments...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Use the API client to get facility statuses
|
||||
const data = await window.api.getFacilityStatuses(facilityId);
|
||||
console.log('Comments API response:', data);
|
||||
|
||||
// Validate the response
|
||||
if (!data || typeof data !== 'object') {
|
||||
@@ -364,7 +353,6 @@ const CommentsManager = {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading comments:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
|
||||
const commentsContainer = document.getElementById('commentsContainer');
|
||||
if (commentsContainer) {
|
||||
@@ -383,22 +371,32 @@ const CommentsManager = {
|
||||
*/
|
||||
renderComments(comments, facilityId) {
|
||||
const commentsContainer = document.getElementById('commentsContainer');
|
||||
if (!commentsContainer) return;
|
||||
if (!commentsContainer) {
|
||||
console.error('Comments container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the container
|
||||
commentsContainer.innerHTML = '';
|
||||
|
||||
// Add the comment form for authenticated users
|
||||
commentsContainer.innerHTML += this.createCommentFormForAuthenticatedUser(facilityId);
|
||||
commentsContainer.innerHTML = this.createCommentFormForAuthenticatedUser(facilityId);
|
||||
|
||||
// Re-initialise the comment form handler immediately after creating the form
|
||||
const commentForm = document.getElementById('commentForm');
|
||||
if (commentForm) {
|
||||
this.setupCommentFormHandler(commentForm);
|
||||
}
|
||||
|
||||
// If no comments, show a message
|
||||
if (!comments || comments.length === 0) {
|
||||
commentsContainer.innerHTML += `
|
||||
<div class="alert alert-light mt-3">
|
||||
<i class="bi bi-chat-dots me-2"></i>
|
||||
No comments yet. Be the first to add a comment!
|
||||
</div>
|
||||
const noCommentsDiv = document.createElement('div');
|
||||
noCommentsDiv.className = 'alert alert-light mt-3';
|
||||
noCommentsDiv.innerHTML = `
|
||||
<i class="bi bi-chat-dots me-2"></i>
|
||||
No comments yet. Be the first to add a comment!
|
||||
`;
|
||||
commentsContainer.appendChild(noCommentsDiv);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -453,12 +451,6 @@ const CommentsManager = {
|
||||
});
|
||||
|
||||
commentsContainer.appendChild(commentsList);
|
||||
|
||||
// Re-initialize the comment form handler
|
||||
const commentForm = document.getElementById('commentForm');
|
||||
if (commentForm) {
|
||||
this.setupCommentFormHandler(commentForm);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -471,16 +463,10 @@ const CommentsManager = {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Deleting comment:', commentId, 'for facility:', facilityId);
|
||||
|
||||
// Use the API client to delete a status comment
|
||||
const data = await window.api.deleteFacilityStatus(commentId, facilityId);
|
||||
|
||||
console.log('Delete comment response:', data);
|
||||
|
||||
if (data.success) {
|
||||
console.log('Comment deleted successfully');
|
||||
|
||||
// Reload comments to reflect the deletion
|
||||
this.loadFacilityComments(facilityId);
|
||||
} else {
|
||||
@@ -529,22 +515,22 @@ const CommentsManager = {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
CommentsManager.state.isDomReady = true;
|
||||
CommentsManager.checkInitialize();
|
||||
CommentsManager.checkinitialise();
|
||||
});
|
||||
} else {
|
||||
CommentsManager.state.isDomReady = true;
|
||||
CommentsManager.checkInitialize();
|
||||
CommentsManager.checkinitialise();
|
||||
}
|
||||
|
||||
// Listen for simpleAuth ready
|
||||
if (window.simpleAuth) {
|
||||
CommentsManager.state.isAuthReady = true;
|
||||
CommentsManager.checkInitialize();
|
||||
CommentsManager.checkinitialise();
|
||||
} else {
|
||||
window.addEventListener('simpleAuthReady', () => {
|
||||
console.log('SimpleAuth is now ready');
|
||||
CommentsManager.state.isAuthReady = true;
|
||||
CommentsManager.checkInitialize();
|
||||
CommentsManager.checkinitialise();
|
||||
});
|
||||
|
||||
// Fallback timeout in case the event doesn't fire
|
||||
@@ -552,10 +538,10 @@ if (window.simpleAuth) {
|
||||
if (!CommentsManager.state.isAuthReady && window.simpleAuth) {
|
||||
console.log('SimpleAuth found via timeout check');
|
||||
CommentsManager.state.isAuthReady = true;
|
||||
CommentsManager.checkInitialize();
|
||||
CommentsManager.checkinitialise();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Export the CommentsManager to the window object
|
||||
// Export the CommentsManager to the window
|
||||
window.CommentsManager = CommentsManager;
|
@@ -5,8 +5,7 @@
|
||||
*/
|
||||
function initialiseFacilityData(data, force = false) {
|
||||
// Only prevent multiple initializations if not forcing
|
||||
if (!force && isInitialized) {
|
||||
console.debug('Facility data already initialized, skipping...');
|
||||
if (!force && isinitialised) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,11 +20,9 @@ function initialiseFacilityData(data, force = false) {
|
||||
// Check if we're on the map page
|
||||
const isMapPage = window.location.pathname.includes('map.php');
|
||||
if (!isMapPage) {
|
||||
// Only try to initialize table if we're not on the map page
|
||||
// Only try to initialise table if we're not on the map page
|
||||
const table = document.querySelector('#facilityTable');
|
||||
if (!table) {
|
||||
console.error('Table not found in DOM. Available elements:',
|
||||
Array.from(document.querySelectorAll('table')).map(t => t.id || 'no-id'));
|
||||
throw new Error('Facility table not found in DOM');
|
||||
}
|
||||
// Clear existing table content
|
||||
@@ -33,27 +30,30 @@ function initialiseFacilityData(data, force = false) {
|
||||
if (tbody) {
|
||||
tbody.innerHTML = '';
|
||||
} else {
|
||||
console.warn('No tbody found in table, creating one');
|
||||
const newTbody = document.createElement('tbody');
|
||||
table.appendChild(newTbody);
|
||||
}
|
||||
|
||||
// Initialize filteredData with all data
|
||||
// initialise filteredData with all data
|
||||
filteredData = data;
|
||||
// Calculate total pages
|
||||
totalPages = Math.ceil(filteredData.length / itemsPerPage);
|
||||
// Set current page to 1
|
||||
currentPage = 1;
|
||||
|
||||
// Update table with paginated data
|
||||
updateTable();
|
||||
|
||||
// Reset sorting state
|
||||
currentSortField = null;
|
||||
currentSortOrder = null;
|
||||
|
||||
// Set up table controls (sorting and filtering)
|
||||
setupTableControls();
|
||||
|
||||
// Update table with paginated data
|
||||
updateTableWithPagination();
|
||||
}
|
||||
|
||||
// Mark as initialized
|
||||
isInitialized = true;
|
||||
// Mark as initialised
|
||||
isinitialised = true;
|
||||
} catch (error) {
|
||||
console.error('Error initialising facility data:', error);
|
||||
// Don't throw error if we're on map page, as table errors are expected
|
||||
@@ -79,7 +79,52 @@ function renderFacilityTable(data) {
|
||||
|
||||
// Check if user is admin
|
||||
const userIsAdmin = isAdmin();
|
||||
console.log('renderFacilityTable - userIsAdmin:', userIsAdmin);
|
||||
|
||||
// Set up table headers first
|
||||
const tableHeaderRow = document.getElementById('tableHeaderRow');
|
||||
if (tableHeaderRow) {
|
||||
// Define header configuration
|
||||
const headers = [
|
||||
{ field: 'title', label: 'Title', width: '17%' },
|
||||
{ field: 'category', label: 'Category', width: '11%', center: true },
|
||||
{ field: 'description', label: 'Description', width: '27%' },
|
||||
{ field: 'address', label: 'Address', width: '20%' },
|
||||
{ field: 'coordinates', label: 'Coordinates', width: '10%', center: true },
|
||||
{ field: 'contributor', label: 'Contributor', width: '7%', center: true },
|
||||
{ field: 'actions', label: 'Actions', width: '8%', center: true, sortable: false }
|
||||
];
|
||||
|
||||
// Clear existing headers
|
||||
tableHeaderRow.innerHTML = '';
|
||||
|
||||
// Create header cells
|
||||
headers.forEach(header => {
|
||||
const th = document.createElement('th');
|
||||
th.className = 'fw-semibold' + (header.center ? ' text-center' : '');
|
||||
th.style.width = header.width;
|
||||
|
||||
if (header.sortable !== false) {
|
||||
th.classList.add('sortable');
|
||||
th.style.cursor = 'pointer';
|
||||
th.dataset.field = header.field;
|
||||
|
||||
// Create header content with sort indicator
|
||||
th.innerHTML = `
|
||||
<div class="d-flex align-items-center gap-1 ${header.center ? 'justify-content-center' : ''}">
|
||||
<span>${header.label}</span>
|
||||
<i class="bi bi-arrow-down-up sort-icon"></i>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add click handler
|
||||
th.addEventListener('click', () => handleHeaderClick(header.field));
|
||||
} else {
|
||||
th.textContent = header.label;
|
||||
}
|
||||
|
||||
tableHeaderRow.appendChild(th);
|
||||
});
|
||||
}
|
||||
|
||||
// Render each row
|
||||
data.forEach((facility, index) => {
|
||||
@@ -98,19 +143,9 @@ function renderFacilityTable(data) {
|
||||
// Start building the row HTML
|
||||
let rowHtml = '';
|
||||
|
||||
// Only show ID column for admins
|
||||
if (userIsAdmin) {
|
||||
console.log('Adding ID column for facility:', facility.id);
|
||||
rowHtml += `
|
||||
<td class="fw-medium align-middle text-center" style="width: 40px;">
|
||||
${escapeHtml(facility.id)}
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
|
||||
// Add the rest of the columns
|
||||
rowHtml += `
|
||||
<td class="fw-medium align-middle" style="${userIsAdmin ? 'width: 15%;' : 'width: 17%;'}">
|
||||
<td class="fw-medium align-middle" style="width: 17%;">
|
||||
<div class="d-flex align-items-center h-100">
|
||||
<div class="facility-icon me-2 rounded-circle bg-light d-flex align-items-center justify-content-center" style="width: 28px; height: 28px; min-width: 28px;">
|
||||
<i class="${getFacilityIcon(facility.category)} text-${categoryClass}"></i>
|
||||
@@ -118,14 +153,14 @@ function renderFacilityTable(data) {
|
||||
<span class="text-truncate" style="max-width: calc(100% - 35px);">${escapeHtml(facility.title)}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center align-middle" style="${userIsAdmin ? 'width: 10%;' : 'width: 11%;'}">
|
||||
<td class="text-center align-middle" style="width: 11%;">
|
||||
<div class="d-flex align-items-center justify-content-center h-100">
|
||||
<span class="badge bg-${categoryClass} bg-opacity-10 text-${categoryClass} px-2 py-1 rounded-pill">
|
||||
${escapeHtml(facility.category)}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle" style="${userIsAdmin ? 'width: 25%;' : 'width: 27%;'}">
|
||||
<td class="align-middle" style="width: 27%;">
|
||||
<div class="description-container d-flex flex-column justify-content-center">
|
||||
<div class="cell-content" data-full-text="${escapeHtml(facility.description)}">
|
||||
${escapeHtml(facility.description)}
|
||||
@@ -144,13 +179,13 @@ function renderFacilityTable(data) {
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="small text-nowrap text-center align-middle" style="${userIsAdmin ? 'width: 12%;' : 'width: 12%;'}">
|
||||
<td class="small text-nowrap text-center align-middle" style="width: 10%;">
|
||||
<span class="badge bg-light text-dark border">
|
||||
${escapeHtml(coordinates)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="small text-center align-middle" style="width: 8%;">${escapeHtml(facility.contributor)}</td>
|
||||
<td class="text-center align-middle" style="${userIsAdmin ? 'width: 10%;' : 'width: 5%;'}">
|
||||
<td class="small text-center align-middle" style="width: 5%;">${escapeHtml(facility.contributor)}</td>
|
||||
<td class="text-center align-middle" style="width: 8%;">
|
||||
<div class="d-flex justify-content-center gap-1">
|
||||
${userIsAdmin ? `
|
||||
<button type="button" class="btn btn-sm btn-outline-primary update-btn rounded-circle d-flex align-items-center justify-content-center" style="width: 30px; height: 30px;" data-bs-toggle="modal" data-bs-target="#updateModal" data-facility-id="${facility.id}" title="Edit">
|
||||
@@ -185,6 +220,9 @@ function renderFacilityTable(data) {
|
||||
`;
|
||||
tbody.appendChild(emptyRow);
|
||||
}
|
||||
|
||||
// Update sort indicators
|
||||
updateSortIndicators();
|
||||
} catch (error) {
|
||||
error_log('Error in renderFacilityTable:', error);
|
||||
}
|
||||
@@ -283,15 +321,19 @@ let filteredData = [];
|
||||
let paginationHandler = null;
|
||||
|
||||
// Add initialization state tracking
|
||||
let isInitialized = false;
|
||||
let isinitialised = false;
|
||||
|
||||
// Initialize modals once
|
||||
// initialise modals once
|
||||
let updateModal, deleteModal, createModal;
|
||||
let formHandlersInitialized = false;
|
||||
let formHandlersinitialised = false;
|
||||
|
||||
// Add sorting state variables
|
||||
let currentSortField = null;
|
||||
let currentSortOrder = null; // null = unsorted, 'asc' = ascending, 'desc' = descending
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Initialize modals once
|
||||
// initialise modals once
|
||||
const modals = document.querySelectorAll('.modal');
|
||||
modals.forEach((modal, index) => {
|
||||
if (modal.id === 'updateModal') {
|
||||
@@ -388,15 +430,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Set up form handlers with a small delay to ensure DOM is fully loaded
|
||||
setTimeout(() => {
|
||||
if (!formHandlersInitialized) {
|
||||
if (!formHandlersinitialised) {
|
||||
console.log('Setting up form handlers...');
|
||||
setupFormHandlers();
|
||||
formHandlersInitialized = true;
|
||||
formHandlersinitialised = true;
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Initialize facility data if not already initialized
|
||||
if (!isInitialized) {
|
||||
// initialise facility data if not already initialised
|
||||
if (!isinitialised) {
|
||||
const storedData = sessionStorage.getItem('facilityData');
|
||||
if (storedData) {
|
||||
try {
|
||||
@@ -407,6 +449,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add CSS styles for sort indicators
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.sortable:hover {
|
||||
background-color: rgba(25, 135, 84, 0.1);
|
||||
}
|
||||
.sort-icon {
|
||||
opacity: 0.5;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.sortable:hover .sort-icon,
|
||||
.text-success .sort-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
|
||||
// Handle create form submission
|
||||
@@ -435,6 +494,8 @@ function setupFormHandlers() {
|
||||
const formData = new FormData(this);
|
||||
// Set the contributor to the current user's username
|
||||
formData.set('contCreate', JSON.parse(localStorage.getItem('user'))?.username);
|
||||
// Set the action to 'create'
|
||||
formData.set('action', 'create');
|
||||
try {
|
||||
// Use simpleAuth.fetchAuth for authenticated requests
|
||||
const response = await simpleAuth.fetchAuth('/facilitycontroller.php', {
|
||||
@@ -517,20 +578,30 @@ function setupFormHandlers() {
|
||||
const formData = new FormData(this);
|
||||
|
||||
// Create a new FormData with the correct field names for the server
|
||||
// This is due to the contributor field being disabled in the form
|
||||
// disallowing it to be included in the form data
|
||||
const serverFormData = new FormData();
|
||||
serverFormData.append('action', 'update');
|
||||
|
||||
// Copy all fields from the form to the server form data
|
||||
for (const [key, value] of formData.entries()) {
|
||||
serverFormData.append(key, value);
|
||||
}
|
||||
// Map form fields to server field names
|
||||
const fieldMappings = {
|
||||
'idUpdate': 'id',
|
||||
'titlUpdate': 'title',
|
||||
'cateUpdate': 'category',
|
||||
'descUpdate': 'description',
|
||||
'hnumUpdate': 'houseNumber',
|
||||
'strtUpdate': 'streetName',
|
||||
'cntyUpdate': 'county',
|
||||
'townUpdate': 'town',
|
||||
'postUpdate': 'postcode',
|
||||
'lngUpdate': 'lng',
|
||||
'latUpdate': 'lat',
|
||||
'contUpdate': 'contributor'
|
||||
};
|
||||
|
||||
// Ensure the contributor field is included (it might be disabled in the form)
|
||||
const contUpdateField = document.getElementById('contUpdate');
|
||||
if (contUpdateField) {
|
||||
serverFormData.append('contUpdate', contUpdateField.value);
|
||||
// Copy and transform fields from the form to the server form data
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (fieldMappings[key]) {
|
||||
serverFormData.append(fieldMappings[key], value);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -801,12 +872,6 @@ function updatePaginationControls() {
|
||||
paginationInfo.querySelector('span').textContent = `Showing ${startItem}-${endItem} of ${filteredData.length} facilities`;
|
||||
}
|
||||
}
|
||||
|
||||
// Update facility count badge
|
||||
const facilityCount = document.getElementById('facilityCount');
|
||||
if (facilityCount) {
|
||||
facilityCount.textContent = `${filteredData.length} facilities`;
|
||||
}
|
||||
}
|
||||
|
||||
function createPageNumberElement(pageNum, isActive) {
|
||||
@@ -862,6 +927,8 @@ function updateTableWithPagination() {
|
||||
const pageData = getCurrentPageData();
|
||||
renderFacilityTable(pageData);
|
||||
updatePaginationControls();
|
||||
// Update sort indicators after rendering table
|
||||
updateSortIndicators();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -930,14 +997,13 @@ function updateTable() {
|
||||
}
|
||||
|
||||
// Get current filter values
|
||||
const sortBy = document.getElementById('sort').value;
|
||||
const sortDir = document.getElementById('dir').value;
|
||||
const filterCategory = document.getElementById('filterCat').value;
|
||||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||||
const searchTerm = document.getElementById('searchInput').value;
|
||||
|
||||
// Apply filters and sorting
|
||||
filteredData = filterData(data, filterCategory, searchTerm);
|
||||
filteredData = sortData(filteredData, sortBy, sortDir);
|
||||
filteredData = filterData(data, searchTerm);
|
||||
if (currentSortField && currentSortOrder) {
|
||||
filteredData = sortData(filteredData, currentSortField, currentSortOrder);
|
||||
}
|
||||
|
||||
// Update pagination
|
||||
totalPages = Math.ceil(filteredData.length / itemsPerPage);
|
||||
@@ -962,12 +1028,9 @@ function setupTableControls() {
|
||||
}
|
||||
|
||||
// Get control elements
|
||||
const filterControls = document.querySelectorAll('.filter-control');
|
||||
const sortControls = document.querySelectorAll('.sort-control');
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
|
||||
if (!filterControls.length || !sortControls.length || !searchInput) {
|
||||
error_log('Missing filter or sort controls');
|
||||
if (!searchInput) {
|
||||
error_log('Missing search input');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -978,66 +1041,142 @@ function setupTableControls() {
|
||||
updateTable();
|
||||
});
|
||||
|
||||
// Add event listeners for immediate updates
|
||||
// Add event listener for search input
|
||||
searchInput.addEventListener('input', updateTable);
|
||||
|
||||
// Add change event listeners for select elements
|
||||
filterControls.forEach(control => {
|
||||
control.addEventListener('change', updateTable);
|
||||
});
|
||||
|
||||
sortControls.forEach(control => {
|
||||
control.addEventListener('change', updateTable);
|
||||
});
|
||||
|
||||
// Set up table headers for sorting
|
||||
setupSortableHeaders();
|
||||
|
||||
// Set up pagination controls
|
||||
setupPaginationControls();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the facility data based on current filter values
|
||||
* Sets up sortable table headers
|
||||
*/
|
||||
function setupSortableHeaders() {
|
||||
const tableHeaderRow = document.getElementById('tableHeaderRow');
|
||||
if (!tableHeaderRow) return;
|
||||
|
||||
// Define header configuration
|
||||
const headers = [
|
||||
{ field: 'title', label: 'Title', width: '17%' },
|
||||
{ field: 'category', label: 'Category', width: '11%', center: true },
|
||||
{ field: 'description', label: 'Description', width: '27%' },
|
||||
{ field: 'address', label: 'Address', width: '20%' },
|
||||
{ field: 'coordinates', label: 'Coordinates', width: '12%', center: true },
|
||||
{ field: 'contributor', label: 'Contributor', width: '8%', center: true },
|
||||
{ field: 'actions', label: 'Actions', width: '5%', center: true, sortable: false }
|
||||
];
|
||||
|
||||
// Clear existing headers
|
||||
tableHeaderRow.innerHTML = '';
|
||||
|
||||
// Create header cells
|
||||
headers.forEach(header => {
|
||||
const th = document.createElement('th');
|
||||
th.className = 'fw-semibold' + (header.center ? ' text-center' : '');
|
||||
th.style.width = header.width;
|
||||
|
||||
if (header.sortable !== false) {
|
||||
th.classList.add('sortable');
|
||||
th.style.cursor = 'pointer';
|
||||
th.dataset.field = header.field;
|
||||
|
||||
// Create header content with sort indicator
|
||||
th.innerHTML = `
|
||||
<div class="d-flex align-items-center gap-1 ${header.center ? 'justify-content-center' : ''}">
|
||||
<span>${header.label}</span>
|
||||
<i class="bi bi-arrow-down-up sort-icon"></i>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add click handler
|
||||
th.addEventListener('click', () => handleHeaderClick(header.field));
|
||||
} else {
|
||||
th.textContent = header.label;
|
||||
}
|
||||
|
||||
tableHeaderRow.appendChild(th);
|
||||
});
|
||||
|
||||
// initialise sort indicators
|
||||
updateSortIndicators();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click on sortable header
|
||||
* @param {string} field - The field to sort by
|
||||
*/
|
||||
function handleHeaderClick(field) {
|
||||
console.log('Header clicked:', field); // Debug log
|
||||
|
||||
// Rotate through sort orders: none -> asc -> desc -> none
|
||||
if (currentSortField === field) {
|
||||
if (currentSortOrder === 'asc') {
|
||||
currentSortOrder = 'desc';
|
||||
} else if (currentSortOrder === 'desc') {
|
||||
currentSortField = null;
|
||||
currentSortOrder = null;
|
||||
}
|
||||
} else {
|
||||
currentSortField = field;
|
||||
currentSortOrder = 'asc';
|
||||
}
|
||||
|
||||
console.log('New sort state:', { field: currentSortField, order: currentSortOrder }); // Debug log
|
||||
|
||||
// Update table
|
||||
updateTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates sort indicators in table headers
|
||||
*/
|
||||
function updateSortIndicators() {
|
||||
const headers = document.querySelectorAll('#tableHeaderRow th.sortable');
|
||||
headers.forEach(header => {
|
||||
const icon = header.querySelector('.sort-icon');
|
||||
if (header.dataset.field === currentSortField) {
|
||||
icon.classList.remove('bi-arrow-down-up');
|
||||
icon.classList.add(currentSortOrder === 'asc' ? 'bi-arrow-up' : 'bi-arrow-down');
|
||||
header.classList.add('text-success');
|
||||
} else {
|
||||
icon.classList.remove('bi-arrow-up', 'bi-arrow-down');
|
||||
icon.classList.add('bi-arrow-down-up');
|
||||
header.classList.remove('text-success');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the facility data based on search term
|
||||
* @param {Array} data - Array of facility objects
|
||||
* @param {string} category - Filter category
|
||||
* @param {string} searchTerm - Search term
|
||||
* @returns {Array} Filtered array of facility objects
|
||||
*/
|
||||
function filterData(data, category, searchTerm) {
|
||||
function filterData(data, searchTerm) {
|
||||
const filtered = data.filter(facility => {
|
||||
if (!facility) return false;
|
||||
|
||||
// If no category selected or no search term, show all results
|
||||
if (!category || !searchTerm) return true;
|
||||
// If no search term, show all results
|
||||
if (!searchTerm) return true;
|
||||
|
||||
// Get the value to search in based on the selected category
|
||||
let searchValue = '';
|
||||
switch(category) {
|
||||
case 'title':
|
||||
searchValue = (facility.title || '').toLowerCase();
|
||||
break;
|
||||
case 'category':
|
||||
searchValue = (facility.category || '').toLowerCase();
|
||||
break;
|
||||
case 'description':
|
||||
searchValue = (facility.description || '').toLowerCase();
|
||||
break;
|
||||
case 'streetName':
|
||||
searchValue = (facility.streetName || '').toLowerCase();
|
||||
break;
|
||||
case 'county':
|
||||
searchValue = (facility.county || '').toLowerCase();
|
||||
break;
|
||||
case 'town':
|
||||
searchValue = (facility.town || '').toLowerCase();
|
||||
break;
|
||||
case 'postcode':
|
||||
searchValue = (facility.postcode || '').toLowerCase();
|
||||
break;
|
||||
case 'contributor':
|
||||
searchValue = (facility.contributor || '').toLowerCase();
|
||||
break;
|
||||
}
|
||||
// Convert search term to lowercase for case-insensitive search
|
||||
searchTerm = searchTerm.toLowerCase();
|
||||
|
||||
return searchValue.includes(searchTerm.toLowerCase());
|
||||
// Search across all relevant fields
|
||||
return (
|
||||
(facility.title || '').toLowerCase().includes(searchTerm) ||
|
||||
(facility.category || '').toLowerCase().includes(searchTerm) ||
|
||||
(facility.description || '').toLowerCase().includes(searchTerm) ||
|
||||
(facility.streetName || '').toLowerCase().includes(searchTerm) ||
|
||||
(facility.county || '').toLowerCase().includes(searchTerm) ||
|
||||
(facility.town || '').toLowerCase().includes(searchTerm) ||
|
||||
(facility.postcode || '').toLowerCase().includes(searchTerm) ||
|
||||
(facility.contributor || '').toLowerCase().includes(searchTerm) ||
|
||||
(facility.houseNumber || '').toLowerCase().includes(searchTerm)
|
||||
);
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
@@ -1051,51 +1190,50 @@ function filterData(data, category, searchTerm) {
|
||||
*/
|
||||
function sortData(data, sortBy, sortDir) {
|
||||
if (!sortBy) return data;
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
|
||||
|
||||
console.log('Sorting by:', sortBy, 'Direction:', sortDir); // Debug log
|
||||
|
||||
return [...data].sort((a, b) => {
|
||||
if (!a || !b) return 0;
|
||||
|
||||
let valueA = '';
|
||||
let valueB = '';
|
||||
let valueA, valueB;
|
||||
|
||||
// Get the values to compare based on the selected field
|
||||
switch (sortBy) {
|
||||
case 'title':
|
||||
valueA = (a.title || '').toLowerCase();
|
||||
valueB = (b.title || '').toLowerCase();
|
||||
break;
|
||||
case 'category':
|
||||
valueA = (a.category || '').toLowerCase();
|
||||
valueB = (b.category || '').toLowerCase();
|
||||
break;
|
||||
case 'description':
|
||||
valueA = (a.description || '').toLowerCase();
|
||||
valueB = (b.description || '').toLowerCase();
|
||||
break;
|
||||
case 'streetName':
|
||||
valueA = (a.streetName || '').toLowerCase();
|
||||
valueB = (b.streetName || '').toLowerCase();
|
||||
break;
|
||||
case 'county':
|
||||
valueA = (a.county || '').toLowerCase();
|
||||
valueB = (b.county || '').toLowerCase();
|
||||
break;
|
||||
case 'town':
|
||||
valueA = (a.town || '').toLowerCase();
|
||||
valueB = (b.town || '').toLowerCase();
|
||||
break;
|
||||
case 'postcode':
|
||||
valueA = (a.postcode || '').toLowerCase();
|
||||
valueB = (b.postcode || '').toLowerCase();
|
||||
break;
|
||||
case 'contributor':
|
||||
valueA = (a.contributor || '').toLowerCase();
|
||||
valueB = (b.contributor || '').toLowerCase();
|
||||
break;
|
||||
// Special handling for address field which is composed of multiple fields
|
||||
if (sortBy === 'address') {
|
||||
valueA = [
|
||||
a.houseNumber || '',
|
||||
a.streetName || '',
|
||||
a.town || '',
|
||||
a.county || '',
|
||||
a.postcode || ''
|
||||
].filter(Boolean).join(', ').toLowerCase();
|
||||
|
||||
valueB = [
|
||||
b.houseNumber || '',
|
||||
b.streetName || '',
|
||||
b.town || '',
|
||||
b.county || '',
|
||||
b.postcode || ''
|
||||
].filter(Boolean).join(', ').toLowerCase();
|
||||
}
|
||||
// Special handling for coordinates field
|
||||
else if (sortBy === 'coordinates') {
|
||||
// Sort by latitude first, then longitude
|
||||
valueA = `${parseFloat(a.lat || 0)},${parseFloat(a.lng || 0)}`;
|
||||
valueB = `${parseFloat(b.lat || 0)},${parseFloat(b.lng || 0)}`;
|
||||
}
|
||||
// Default handling for other fields
|
||||
else {
|
||||
valueA = (a[sortBy] || '').toString().toLowerCase();
|
||||
valueB = (b[sortBy] || '').toString().toLowerCase();
|
||||
}
|
||||
|
||||
const comparison = valueA.localeCompare(valueB);
|
||||
return sortDir === 'asc' ? comparison : -comparison;
|
||||
console.log('Comparing:', valueA, valueB); // Debug log
|
||||
|
||||
// Compare the values
|
||||
if (valueA < valueB) return sortDir === 'asc' ? -1 : 1;
|
||||
if (valueA > valueB) return sortDir === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1154,4 +1292,4 @@ function getCategoryColorClass(category) {
|
||||
}
|
||||
|
||||
// Export the initialization function
|
||||
window.initializeFacilityData = initialiseFacilityData;
|
||||
window.initialiseFacilityData = initialiseFacilityData;
|
634
public/js/mapHandler.js
Normal file
634
public/js/mapHandler.js
Normal file
@@ -0,0 +1,634 @@
|
||||
/**
|
||||
* Map Handler for EcoBuddy
|
||||
* Handles map initialization, postcode validation, and facility display
|
||||
*/
|
||||
|
||||
// initialise map variables
|
||||
let map = null;
|
||||
let markers = [];
|
||||
let circle = null;
|
||||
let facilities = [];
|
||||
let currentPostcode = null;
|
||||
let currentRadius = 10; // Default radius in miles
|
||||
|
||||
// initialise map on document load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// initialise the map centered on UK
|
||||
map = L.map('map', {
|
||||
scrollWheelZoom: true, // Enable scroll wheel zoom
|
||||
zoomControl: true // Show zoom controls
|
||||
}).setView([54.5, -2], 6);
|
||||
|
||||
// Add OpenStreetMap tiles
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Get facilities data from sessionStorage
|
||||
facilities = JSON.parse(sessionStorage.getItem('facilityData') || '[]');
|
||||
|
||||
// Set up form handlers
|
||||
setupFormHandlers();
|
||||
|
||||
// Set up search handler from header
|
||||
setupHeaderSearchHandler();
|
||||
});
|
||||
|
||||
/**
|
||||
* Set up form handlers for postcode and radius inputs
|
||||
*/
|
||||
function setupFormHandlers() {
|
||||
const postcodeForm = document.getElementById('postcodeForm');
|
||||
const radiusSelect = document.getElementById('radius');
|
||||
|
||||
if (postcodeForm) {
|
||||
postcodeForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const postcode = document.getElementById('postcode').value;
|
||||
const radius = parseFloat(document.getElementById('radius').value);
|
||||
|
||||
// Show loading state
|
||||
const submitButton = this.querySelector('button[type="submit"]');
|
||||
const originalButtonContent = `<i class="bi bi-search me-1"></i>Search...`;
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = `
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span class="ms-2">Searching...</span>
|
||||
`;
|
||||
|
||||
// Validate postcode format first
|
||||
if (!isValidPostcode(postcode)) {
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonContent;
|
||||
alert('Please enter a valid UK postcode');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get coordinates for postcode
|
||||
const coords = await getPostcodeCoordinates(postcode);
|
||||
if (!coords) {
|
||||
throw new Error('Could not find coordinates for this postcode');
|
||||
}
|
||||
|
||||
// Update map with new location and radius
|
||||
updateMapLocation(coords, radius);
|
||||
|
||||
// Remove overlay once we have a valid postcode
|
||||
const overlay = document.getElementById('mapOverlay');
|
||||
if (overlay) {
|
||||
overlay.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Store current postcode
|
||||
currentPostcode = postcode;
|
||||
currentRadius = radius;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing postcode:', error);
|
||||
alert(error.message || 'Error processing postcode');
|
||||
} finally {
|
||||
// Always reset button state
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonContent;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (radiusSelect) {
|
||||
radiusSelect.addEventListener('change', function() {
|
||||
const radius = parseFloat(this.value);
|
||||
if (currentPostcode) {
|
||||
updateMapLocation(null, radius); // null coords means use existing center
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate UK postcode format
|
||||
* @param {string} postcode - The postcode to validate
|
||||
* @returns {boolean} True if valid, false otherwise
|
||||
*/
|
||||
function isValidPostcode(postcode) {
|
||||
// Basic UK postcode regex
|
||||
const postcodeRegex = /^[A-Z]{1,2}[0-9][A-Z0-9]? ?[0-9][A-Z]{2}$/i;
|
||||
return postcodeRegex.test(postcode.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coordinates for a UK postcode using postcodes.io API
|
||||
* @param {string} postcode - The postcode to geocode
|
||||
* @returns {Promise<{lat: number, lng: number}>} The coordinates
|
||||
*/
|
||||
async function getPostcodeCoordinates(postcode) {
|
||||
try {
|
||||
const response = await fetch(`https://api.postcodes.io/postcodes/${encodeURIComponent(postcode)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Postcode not found');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status === 200 && data.result) {
|
||||
return {
|
||||
lat: data.result.latitude,
|
||||
lng: data.result.longitude
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Invalid response from postcode API');
|
||||
} catch (error) {
|
||||
console.error('Error getting postcode coordinates:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update map location and display facilities within radius
|
||||
* @param {Object} coords - The coordinates to center on (null to use existing)
|
||||
* @param {number} radius - The radius in miles
|
||||
*/
|
||||
function updateMapLocation(coords, radius) {
|
||||
// Clear existing markers and circle
|
||||
clearMapOverlays();
|
||||
|
||||
// Get center coordinates (either new or existing)
|
||||
const center = coords || map.getCenter();
|
||||
|
||||
// Convert radius from miles to meters (1 mile = 1609.34 meters)
|
||||
const radiusMeters = radius * 1609.34;
|
||||
|
||||
// Add circle for radius
|
||||
circle = L.circle([center.lat, center.lng], {
|
||||
color: '#198754',
|
||||
fillColor: '#198754',
|
||||
fillOpacity: 0.1,
|
||||
radius: radiusMeters
|
||||
}).addTo(map);
|
||||
|
||||
// Find facilities within radius
|
||||
const facilitiesInRange = findFacilitiesInRange(center, radius);
|
||||
|
||||
// Add markers for facilities
|
||||
facilitiesInRange.forEach(facility => {
|
||||
const marker = L.marker([facility.lat, facility.lng])
|
||||
.bindPopup(createPopupContent(facility))
|
||||
.addTo(map);
|
||||
markers.push(marker);
|
||||
});
|
||||
|
||||
// Fit map bounds to circle
|
||||
map.fitBounds(circle.getBounds());
|
||||
|
||||
// Update facility list
|
||||
updateFacilityList(facilitiesInRange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all markers and circle from map
|
||||
*/
|
||||
function clearMapOverlays() {
|
||||
// Clear markers
|
||||
markers.forEach(marker => marker.remove());
|
||||
markers = [];
|
||||
|
||||
// Clear circle
|
||||
if (circle) {
|
||||
circle.remove();
|
||||
circle = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find facilities within specified radius of center point
|
||||
* @param {Object} center - The center coordinates
|
||||
* @param {number} radius - The radius in miles
|
||||
* @returns {Array} Array of facilities within range
|
||||
*/
|
||||
function findFacilitiesInRange(center, radius) {
|
||||
return facilities.filter(facility => {
|
||||
const distance = calculateDistance(
|
||||
center.lat,
|
||||
center.lng,
|
||||
parseFloat(facility.lat),
|
||||
parseFloat(facility.lng)
|
||||
);
|
||||
return distance <= radius;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance between two points using Haversine formula
|
||||
* @param {number} lat1 - Latitude of first point
|
||||
* @param {number} lon1 - Longitude of first point
|
||||
* @param {number} lat2 - Latitude of second point
|
||||
* @param {number} lon2 - Longitude of second point
|
||||
* @returns {number} Distance in miles
|
||||
*/
|
||||
function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
const R = 3959; // Earth's radius in miles
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
|
||||
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
||||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
||||
Math.sin(dLon/2) * Math.sin(dLon/2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||
return R * c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert degrees to radians
|
||||
* @param {number} degrees - Value in degrees
|
||||
* @returns {number} Value in radians
|
||||
*/
|
||||
function toRad(degrees) {
|
||||
return degrees * (Math.PI / 180);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create popup content for facility marker
|
||||
* @param {Object} facility - The facility data
|
||||
* @returns {string} HTML content for popup
|
||||
*/
|
||||
function createPopupContent(facility) {
|
||||
const isAuthenticated = window.simpleAuth && window.simpleAuth.isAuthenticated();
|
||||
|
||||
return `
|
||||
<div class="facility-popup">
|
||||
<h6 class="mb-1">${escapeHtml(facility.title)}</h6>
|
||||
<p class="mb-1 small">
|
||||
<span class="badge bg-${getCategoryColorClass(facility.category)} bg-opacity-10 text-${getCategoryColorClass(facility.category)}">
|
||||
${escapeHtml(facility.category)}
|
||||
</span>
|
||||
</p>
|
||||
<p class="mb-2 small">${escapeHtml(facility.description)}</p>
|
||||
<p class="mb-2 small">
|
||||
<strong>Address:</strong><br>
|
||||
${escapeHtml(formatAddress(facility))}
|
||||
</p>
|
||||
<p class="mb-0 small">
|
||||
<strong>Added by:</strong> ${escapeHtml(facility.contributor)}
|
||||
</p>
|
||||
|
||||
${isAuthenticated ? `
|
||||
<div class="comment-form">
|
||||
<form onsubmit="return handleCommentSubmit(event, ${facility.id})">
|
||||
<div class="mb-2">
|
||||
<textarea class="form-control form-control-sm"
|
||||
placeholder="Add a comment..."
|
||||
required
|
||||
rows="2"></textarea>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<button type="submit" class="btn btn-sm btn-success">
|
||||
<i class="bi bi-chat-dots me-1"></i>Add Comment
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="openCommentsModal('${facility.id}')">
|
||||
<i class="bi bi-info-circle me-1"></i>View All Comments
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
` : `
|
||||
<div class="comment-form">
|
||||
<div class="alert alert-light mb-0 small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Please <a href="#" data-bs-toggle="modal" data-bs-target="#loginModal">login</a> to add comments
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the comments modal for a facility
|
||||
* @param {string} facilityId - The facility ID
|
||||
*/
|
||||
function openCommentsModal(facilityId) {
|
||||
// Find the facility
|
||||
const facility = facilities.find(f => f.id === parseInt(facilityId));
|
||||
if (!facility) {
|
||||
console.error('Facility not found:', facilityId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the modal
|
||||
const modal = document.getElementById('statusModal');
|
||||
if (!modal) {
|
||||
console.error('Status modal not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the facility ID on the modal
|
||||
modal.setAttribute('data-facility-id', facilityId);
|
||||
|
||||
// Set the facility ID in the comment form
|
||||
const facilityIdInput = modal.querySelector('#commentFacilityId');
|
||||
if (facilityIdInput) {
|
||||
facilityIdInput.value = facilityId;
|
||||
}
|
||||
|
||||
// Show the modal
|
||||
const modalInstance = new bootstrap.Modal(modal);
|
||||
modalInstance.show();
|
||||
|
||||
// Load the comments using CommentsManager
|
||||
CommentsManager.loadFacilityComments(facilityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle comment form submission
|
||||
* @param {Event} event - The form submit event
|
||||
* @param {number} facilityId - The facility ID
|
||||
* @returns {boolean} False to prevent form submission
|
||||
*/
|
||||
async function handleCommentSubmit(event, facilityId) {
|
||||
event.preventDefault();
|
||||
|
||||
// Check authentication
|
||||
if (!window.simpleAuth || !window.simpleAuth.isAuthenticated()) {
|
||||
alert('You must be logged in to add comments');
|
||||
return false;
|
||||
}
|
||||
|
||||
const form = event.target;
|
||||
const textarea = form.querySelector('textarea');
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
const originalButtonContent = submitButton.innerHTML;
|
||||
|
||||
// Show loading state
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = `
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span class="ms-2">Adding...</span>
|
||||
`;
|
||||
|
||||
try {
|
||||
// Add the comment using the API
|
||||
const response = await window.api.addFacilityStatus(
|
||||
facilityId.toString(), // Ensure facilityId is a string
|
||||
textarea.value
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
// Clear the textarea
|
||||
textarea.value = '';
|
||||
|
||||
// Show success message
|
||||
const successMessage = document.createElement('div');
|
||||
successMessage.className = 'alert alert-success mt-2 mb-0 py-2 small';
|
||||
successMessage.innerHTML = `
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Comment added successfully
|
||||
`;
|
||||
form.appendChild(successMessage);
|
||||
|
||||
// Remove success message after 3 seconds
|
||||
setTimeout(() => {
|
||||
successMessage.remove();
|
||||
|
||||
// Open the comments modal to show the new comment
|
||||
openCommentsModal(facilityId);
|
||||
}, 3000);
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to add comment');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding comment:', error);
|
||||
alert('Error adding comment: ' + error.message);
|
||||
} finally {
|
||||
// Reset button state
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonContent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update facility list display
|
||||
* @param {Array} facilities - Array of facilities to display
|
||||
*/
|
||||
function updateFacilityList(facilities) {
|
||||
const listElement = document.getElementById('facilityList');
|
||||
if (!listElement) return;
|
||||
|
||||
listElement.innerHTML = '';
|
||||
|
||||
facilities.forEach(facility => {
|
||||
const distance = calculateDistance(
|
||||
circle.getLatLng().lat,
|
||||
circle.getLatLng().lng,
|
||||
parseFloat(facility.lat),
|
||||
parseFloat(facility.lng)
|
||||
);
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'list-group-item list-group-item-action';
|
||||
item.innerHTML = `
|
||||
<div class="d-flex w-100 justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1">${escapeHtml(facility.title)}</h6>
|
||||
<p class="mb-1 small">
|
||||
<span class="badge bg-${getCategoryColorClass(facility.category)} bg-opacity-10 text-${getCategoryColorClass(facility.category)}">
|
||||
${escapeHtml(facility.category)}
|
||||
</span>
|
||||
</p>
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-geo-alt me-1"></i>${distance.toFixed(1)} miles away
|
||||
</small>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-success" onclick="showFacilityDetails('${facility.id}')">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
listElement.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up header search handler
|
||||
*/
|
||||
function setupHeaderSearchHandler() {
|
||||
const searchInput = document.querySelector('input#searchInput');
|
||||
const filterCat = document.querySelector('select#filterCat');
|
||||
|
||||
if (searchInput && filterCat) {
|
||||
const handleSearch = () => {
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
const filterCategory = filterCat.value;
|
||||
|
||||
if (!currentPostcode) return; // Only filter if map is active
|
||||
|
||||
// Get all facilities in current radius
|
||||
const center = circle ? circle.getLatLng() : null;
|
||||
if (!center) return;
|
||||
|
||||
const facilitiesInRange = findFacilitiesInRange(center, currentRadius);
|
||||
|
||||
// Filter facilities based on search term and category
|
||||
const filteredFacilities = facilitiesInRange.filter(facility => {
|
||||
if (!facility) return false;
|
||||
|
||||
if (filterCategory && searchTerm) {
|
||||
let searchValue = '';
|
||||
switch(filterCategory) {
|
||||
case 'title':
|
||||
searchValue = (facility.title || '').toLowerCase();
|
||||
break;
|
||||
case 'category':
|
||||
searchValue = (facility.category || '').toLowerCase();
|
||||
break;
|
||||
case 'description':
|
||||
searchValue = (facility.description || '').toLowerCase();
|
||||
break;
|
||||
case 'streetName':
|
||||
searchValue = (facility.streetName || '').toLowerCase();
|
||||
break;
|
||||
case 'county':
|
||||
searchValue = (facility.county || '').toLowerCase();
|
||||
break;
|
||||
case 'town':
|
||||
searchValue = (facility.town || '').toLowerCase();
|
||||
break;
|
||||
case 'postcode':
|
||||
searchValue = (facility.postcode || '').toLowerCase();
|
||||
break;
|
||||
case 'contributor':
|
||||
searchValue = (facility.contributor || '').toLowerCase();
|
||||
break;
|
||||
}
|
||||
return searchValue.includes(searchTerm);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Update markers and list
|
||||
updateMapMarkers(filteredFacilities);
|
||||
updateFacilityList(filteredFacilities);
|
||||
|
||||
};
|
||||
|
||||
searchInput.addEventListener('input', handleSearch);
|
||||
filterCat.addEventListener('change', handleSearch);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update map markers without changing the circle or center
|
||||
* @param {Array} facilities - Facilities to show on map
|
||||
*/
|
||||
function updateMapMarkers(facilities) {
|
||||
// Clear existing markers
|
||||
markers.forEach(marker => marker.remove());
|
||||
markers = [];
|
||||
|
||||
// Add new markers
|
||||
facilities.forEach(facility => {
|
||||
const marker = L.marker([facility.lat, facility.lng])
|
||||
.bindPopup(createPopupContent(facility))
|
||||
.addTo(map);
|
||||
markers.push(marker);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show facility details in a popover
|
||||
* @param {string} facilityId - The facility ID
|
||||
*/
|
||||
function showFacilityDetails(facilityId) {
|
||||
const facility = facilities.find(f => f.id === parseInt(facilityId));
|
||||
if (!facility) return;
|
||||
|
||||
// Create popover content
|
||||
const content = `
|
||||
<div class="facility-details p-2">
|
||||
<h6 class="mb-2">${escapeHtml(facility.title)}</h6>
|
||||
<p class="mb-2">
|
||||
<span class="badge bg-${getCategoryColorClass(facility.category)} bg-opacity-10 text-${getCategoryColorClass(facility.category)}">
|
||||
${escapeHtml(facility.category)}
|
||||
</span>
|
||||
</p>
|
||||
<p class="mb-2 small">${escapeHtml(facility.description)}</p>
|
||||
<p class="mb-2 small">
|
||||
<strong>Address:</strong><br>
|
||||
${escapeHtml(formatAddress(facility))}
|
||||
</p>
|
||||
<p class="mb-0 small">
|
||||
<strong>Added by:</strong> ${escapeHtml(facility.contributor)}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Find the marker for this facility
|
||||
const marker = markers.find(m =>
|
||||
m.getLatLng().lat === parseFloat(facility.lat) &&
|
||||
m.getLatLng().lng === parseFloat(facility.lng)
|
||||
);
|
||||
|
||||
if (marker) {
|
||||
marker.bindPopup(content, {
|
||||
maxWidth: 300,
|
||||
className: 'facility-popup'
|
||||
}).openPopup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format facility address
|
||||
* @param {Object} facility - The facility data
|
||||
* @returns {string} Formatted address
|
||||
*/
|
||||
function formatAddress(facility) {
|
||||
const parts = [
|
||||
facility.houseNumber,
|
||||
facility.streetName,
|
||||
facility.town,
|
||||
facility.county,
|
||||
facility.postcode
|
||||
].filter(Boolean);
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color class for facility category
|
||||
* @param {string} category - The facility category
|
||||
* @returns {string} Bootstrap color class
|
||||
*/
|
||||
function getCategoryColorClass(category) {
|
||||
const categoryLower = (category || '').toLowerCase();
|
||||
|
||||
if (categoryLower.includes('recycling')) return 'success';
|
||||
if (categoryLower.includes('green') || categoryLower.includes('roof')) return 'success';
|
||||
if (categoryLower.includes('solar') || categoryLower.includes('power')) return 'warning';
|
||||
if (categoryLower.includes('water') || categoryLower.includes('rain')) return 'info';
|
||||
if (categoryLower.includes('battery')) return 'danger';
|
||||
if (categoryLower.includes('bench')) return 'primary';
|
||||
|
||||
return 'secondary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
* @param {string} unsafe - Unsafe string
|
||||
* @returns {string} Escaped string
|
||||
*/
|
||||
function escapeHtml(unsafe) {
|
||||
if (unsafe === null || unsafe === undefined) {
|
||||
return '';
|
||||
}
|
||||
return unsafe
|
||||
.toString()
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
class SimpleAuth {
|
||||
/**
|
||||
* Initialize the authentication helper
|
||||
* initialise the authentication helper
|
||||
*/
|
||||
constructor() {
|
||||
this.token = localStorage.getItem('token');
|
||||
@@ -25,12 +25,6 @@ class SimpleAuth {
|
||||
console.warn('Browser fingerprint mismatch - clearing authentication');
|
||||
this.logout(false); // Silent logout (no redirect)
|
||||
}
|
||||
|
||||
// Log initialization
|
||||
console.log('SimpleAuth initialized:', {
|
||||
isAuthenticated: this.isAuthenticated(),
|
||||
user: this.user
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
1
test-admin.js
Normal file
1
test-admin.js
Normal file
@@ -0,0 +1 @@
|
||||
console.log('Testing admin check:'); console.log('User with accessLevel 1:', simpleAuth.isAdmin.call({isAuthenticated: () => true, user: {accessLevel: 1}})); console.log('User with accessLevel 0:', simpleAuth.isAdmin.call({isAuthenticated: () => true, user: {accessLevel: 0}}));
|
Reference in New Issue
Block a user