1197 lines
47 KiB
JavaScript
1197 lines
47 KiB
JavaScript
/**
|
|
* Initialises the facility data table with the provided data
|
|
* @param {Array} data - Array of facility objects to display
|
|
* @param {boolean} force - Whether to force reinitialization
|
|
*/
|
|
function initialiseFacilityData(data, force = false) {
|
|
// Only prevent multiple initializations if not forcing
|
|
if (!force && isInitialized) {
|
|
console.debug('Facility data already initialized, skipping...');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Validate data format
|
|
if (!Array.isArray(data)) {
|
|
throw new Error('Invalid data format: expected array');
|
|
}
|
|
// Store the data in sessionStorage for persistence
|
|
sessionStorage.setItem('facilityData', JSON.stringify(data));
|
|
// Ensure table exists
|
|
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
|
|
const tbody = table.querySelector('tbody');
|
|
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
|
|
filteredData = data;
|
|
// Calculate total pages
|
|
totalPages = Math.ceil(filteredData.length / itemsPerPage);
|
|
// Set current page to 1
|
|
currentPage = 1;
|
|
|
|
// Update table with paginated data
|
|
updateTableWithPagination();
|
|
|
|
// Set up table controls (sorting and filtering)
|
|
setupTableControls();
|
|
|
|
// Mark as initialized
|
|
isInitialized = true;
|
|
} catch (error) {
|
|
error_log('Error initialising facility data:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the facility data in the table
|
|
* @param {Array} data - Array of facility objects to display
|
|
*/
|
|
function renderFacilityTable(data) {
|
|
try {
|
|
const tbody = document.querySelector('#facilityTable tbody');
|
|
if (!tbody) {
|
|
return;
|
|
}
|
|
|
|
// Clear existing table content
|
|
tbody.innerHTML = '';
|
|
|
|
// Render each row
|
|
data.forEach((facility, index) => {
|
|
if (!facility) return;
|
|
|
|
const row = document.createElement('tr');
|
|
row.className = 'facility-row';
|
|
row.style.minHeight = '60px'; // Add minimum height for consistent row sizing
|
|
|
|
// Format coordinates to be more readable
|
|
const coordinates = `${parseFloat(facility.lat).toFixed(4)}, ${parseFloat(facility.lng).toFixed(4)}`;
|
|
|
|
// Create category badge with color based on category
|
|
const categoryClass = getCategoryColorClass(facility.category);
|
|
|
|
row.innerHTML = `
|
|
<td class="d-none">${escapeHtml(facility.id)}</td>
|
|
<td class="fw-medium align-middle" style="width: 15%;">
|
|
<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>
|
|
</div>
|
|
<span class="text-truncate" style="max-width: calc(100% - 35px);">${escapeHtml(facility.title)}</span>
|
|
</div>
|
|
</td>
|
|
<td class="text-center align-middle" style="width: 10%;">
|
|
<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="width: 25%;">
|
|
<div class="description-container d-flex flex-column justify-content-center">
|
|
<div class="cell-content" data-full-text="${escapeHtml(facility.description)}">
|
|
${escapeHtml(facility.description)}
|
|
</div>
|
|
${facility.description.length > 100 ?
|
|
`<button class="btn btn-link btn-sm p-0 mt-1 text-decoration-none toggle-content-btn">
|
|
<small>Show more</small>
|
|
</button>` : ''}
|
|
</div>
|
|
</td>
|
|
<td class="small align-middle" style="width: 20%;">
|
|
<div class="d-flex align-items-center h-100">
|
|
<i class="bi bi-geo-alt text-secondary me-1" style="min-width: 14px;"></i>
|
|
<div class="address-content" data-full-text="${escapeHtml(formatAddress(facility))}">
|
|
${escapeHtml(formatAddress(facility))}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="small fw-medium text-center align-middle" style="width: 8%;" hidden>${escapeHtml(facility.postcode)}</td>
|
|
<td class="small text-nowrap text-center align-middle" style="width: 12%;">
|
|
<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="width: 10%;">
|
|
<div class="d-flex justify-content-center gap-1">
|
|
${isAdmin() ? `
|
|
<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">
|
|
<span class="bi bi-pen-fill"></span>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-danger delete-btn rounded-circle d-flex align-items-center justify-content-center" style="width: 30px; height: 30px;" data-bs-toggle="modal" data-bs-target="#deleteModal" data-facility-id="${facility.id}" title="Delete">
|
|
<span class="bi bi-trash-fill"></span>
|
|
</button>
|
|
` : ''}
|
|
<button type="button" class="btn btn-sm btn-outline-info comments-btn rounded-circle d-flex align-items-center justify-content-center" style="width: 30px; height: 30px;" data-bs-toggle="modal" data-bs-target="#statusModal" data-facility-id="${facility.id}" title="Comments">
|
|
<span class="bi bi-chat-dots-fill"></span>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
`;
|
|
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// If no data, show a message
|
|
if (data.length === 0) {
|
|
const emptyRow = document.createElement('tr');
|
|
emptyRow.innerHTML = `
|
|
<td colspan="9" class="text-center py-4 text-muted">
|
|
<div class="d-flex flex-column align-items-center">
|
|
<span class="bi bi-inbox fs-2 mb-2"></span>
|
|
<p class="mb-0">No facilities found</p>
|
|
</div>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(emptyRow);
|
|
}
|
|
} catch (error) {
|
|
error_log('Error in renderFacilityTable:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safely escapes HTML special characters to prevent XSS attacks
|
|
* @param {*} unsafe - The value to escape
|
|
* @returns {string} The escaped string
|
|
*/
|
|
function escapeHtml(unsafe) {
|
|
if (unsafe === null || unsafe === undefined) {
|
|
return '';
|
|
}
|
|
return unsafe
|
|
.toString()
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/**
|
|
* (helper function) Formats the facility address from its components
|
|
* @param {Object} facility - The facility object containing address components
|
|
* @returns {string} The formatted address
|
|
*/
|
|
function formatAddress(facility) {
|
|
if (!facility) return '';
|
|
|
|
const parts = [
|
|
facility.houseNumber,
|
|
facility.streetName,
|
|
facility.town,
|
|
facility.county,
|
|
facility.postcode
|
|
].filter(Boolean);
|
|
return parts.join(', ');
|
|
}
|
|
|
|
/**
|
|
* (helper function) Checks if the current user has admin privileges
|
|
* @returns {boolean} True if user is admin, false otherwise
|
|
*/
|
|
function isAdmin() {
|
|
// Check if auth service is available and has user data
|
|
if (window.auth && window.auth.getUser()) {
|
|
const authUser = window.auth.getUser();
|
|
console.log('Auth service user data:', authUser);
|
|
if (authUser && (authUser.accessLevel === 1 || authUser.accessLevel === 0)) {
|
|
console.log('User is admin according to auth service');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Fallback to localStorage
|
|
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
|
console.log('Checking admin status from localStorage:', user);
|
|
const isAdminUser = user && (user.accessLevel === 1 || user.accessLevel === 0);
|
|
console.log('Is admin according to localStorage:', isAdminUser);
|
|
|
|
return isAdminUser;
|
|
}
|
|
|
|
/**
|
|
* Checks if the current user is authenticated
|
|
* @returns {boolean} True if authenticated, false otherwise
|
|
*/
|
|
function isAuthenticated() {
|
|
const token = localStorage.getItem('token');
|
|
return !!token;
|
|
}
|
|
|
|
// Pagination state
|
|
let currentPage = 1;
|
|
let itemsPerPage = 10;
|
|
let totalPages = 1;
|
|
let filteredData = [];
|
|
|
|
// Store the pagination handler function
|
|
let paginationHandler = null;
|
|
|
|
// Add initialization state tracking
|
|
let isInitialized = false;
|
|
|
|
// Initialize modals once
|
|
let updateModal, deleteModal, createModal;
|
|
let formHandlersInitialized = false;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
// Initialize modals once
|
|
const modals = document.querySelectorAll('.modal');
|
|
modals.forEach((modal, index) => {
|
|
if (modal.id === 'updateModal') {
|
|
updateModal = new bootstrap.Modal(modal);
|
|
|
|
// Add event listener for when update modal is shown
|
|
modal.addEventListener('show.bs.modal', async function(event) {
|
|
// 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');
|
|
if (!facilityId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Get facility data from session storage
|
|
const storedData = JSON.parse(sessionStorage.getItem('facilityData') || '[]');
|
|
const facility = storedData.find(f => f.id === parseInt(facilityId));
|
|
|
|
if (!facility) {
|
|
return;
|
|
}
|
|
|
|
// Pre-fill the form with facility data
|
|
const form = this.querySelector('#updateForm');
|
|
if (!form) {
|
|
return;
|
|
}
|
|
|
|
// Map facility data to form fields
|
|
const fieldMappings = {
|
|
'idUpdate': facility.id,
|
|
'titlUpdate': facility.title,
|
|
'cateUpdate': facility.category,
|
|
'descUpdate': facility.description,
|
|
'hnumUpdate': facility.houseNumber,
|
|
'strtUpdate': facility.streetName,
|
|
'cntyUpdate': facility.county,
|
|
'townUpdate': facility.town,
|
|
'postUpdate': facility.postcode,
|
|
'lngUpdate': facility.lng,
|
|
'latUpdate': facility.lat,
|
|
'contUpdate': facility.contributor
|
|
};
|
|
|
|
// Set each field value
|
|
Object.entries(fieldMappings).forEach(([fieldName, value]) => {
|
|
const input = form.querySelector(`[name="${fieldName}"]`);
|
|
if (input) {
|
|
input.value = value || '';
|
|
} else {
|
|
console.warn(`Field ${fieldName} not found in form`);
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error pre-filling update form:', error);
|
|
}
|
|
});
|
|
} else if (modal.id === 'deleteModal') {
|
|
deleteModal = new bootstrap.Modal(modal);
|
|
|
|
// Add event listener for when delete modal is shown
|
|
modal.addEventListener('show.bs.modal', function(event) {
|
|
// 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');
|
|
// Set the facility ID in the form
|
|
const idInput = this.querySelector('[name="idDelete"]');
|
|
if (idInput && facilityId) {
|
|
idInput.value = facilityId;
|
|
// Get facility data from session storage for confirmation text
|
|
const storedData = JSON.parse(sessionStorage.getItem('facilityData') || '[]');
|
|
const facility = storedData.find(f => f.id === parseInt(facilityId));
|
|
|
|
if (facility) {
|
|
const confirmationText = document.getElementById('deleteConfirmationText');
|
|
if (confirmationText) {
|
|
confirmationText.textContent = `${facility.title} (${facility.category})`;
|
|
}
|
|
}
|
|
} else {
|
|
console.error('Could not find id input or facility ID in delete modal');
|
|
}
|
|
});
|
|
} else if (modal.id === 'createModal') {
|
|
createModal = new bootstrap.Modal(modal);
|
|
} else if (modal.id === 'statusModal') {
|
|
console.log('Status modal will be handled by comments.js');
|
|
}
|
|
});
|
|
|
|
// Set up form handlers with a small delay to ensure DOM is fully loaded
|
|
setTimeout(() => {
|
|
if (!formHandlersInitialized) {
|
|
console.log('Setting up form handlers...');
|
|
setupFormHandlers();
|
|
formHandlersInitialized = true;
|
|
}
|
|
}, 100);
|
|
|
|
// Initialize facility data if not already initialized
|
|
if (!isInitialized) {
|
|
const storedData = sessionStorage.getItem('facilityData');
|
|
if (storedData) {
|
|
try {
|
|
const parsedData = JSON.parse(storedData);
|
|
initialiseFacilityData(parsedData);
|
|
} catch (error) {
|
|
error_log('Error parsing stored facility data:', error);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle create form submission
|
|
function setupFormHandlers() {
|
|
// Only look for create form if user is admin
|
|
if (isAdmin()) {
|
|
// Create form handler
|
|
const createForm = document.getElementById('createForm');
|
|
if (createForm) {
|
|
console.log('Found create form, attaching submit handler');
|
|
createForm.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
// Prevent duplicate submissions
|
|
if (this.submitting) {
|
|
return;
|
|
}
|
|
this.submitting = true;
|
|
|
|
if (!isAdmin()) {
|
|
alert('You must be an admin to create facilities');
|
|
this.submitting = false;
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData(this);
|
|
// Set the contributor to the current user's username
|
|
formData.set('contCreate', JSON.parse(localStorage.getItem('user'))?.username);
|
|
try {
|
|
// Use authFetch instead of regular fetch
|
|
const response = await window.authFetch('/facilitycontroller.php', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('Server response:', errorText);
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
const storedData = JSON.parse(sessionStorage.getItem('facilityData') || '[]');
|
|
storedData.push(data.facility);
|
|
sessionStorage.setItem('facilityData', JSON.stringify(storedData));
|
|
initialiseFacilityData(storedData, true);
|
|
|
|
// Close modal using multiple methods to ensure it closes
|
|
const modalElement = document.getElementById('createModal');
|
|
if (modalElement) {
|
|
// Method 1: Using Bootstrap's modal instance
|
|
if (createModal) {
|
|
createModal.hide();
|
|
}
|
|
|
|
// Method 2: Using Bootstrap's static method
|
|
const modal = bootstrap.Modal.getInstance(modalElement);
|
|
if (modal) {
|
|
modal.hide();
|
|
}
|
|
|
|
// Method 3: Direct DOM manipulation
|
|
modalElement.classList.remove('show');
|
|
document.body.classList.remove('modal-open');
|
|
const modalBackdrop = document.querySelector('.modal-backdrop');
|
|
if (modalBackdrop) {
|
|
modalBackdrop.remove();
|
|
}
|
|
}
|
|
|
|
// Reset the form
|
|
createForm.reset();
|
|
} else {
|
|
console.error('Create failed:', data.error);
|
|
alert(data.error || 'Failed to create facility');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating facility:', error);
|
|
alert('Failed to create facility: ' + error.message);
|
|
} finally {
|
|
this.submitting = false;
|
|
}
|
|
});
|
|
} else {
|
|
console.log('Create form not found in DOM - this is expected for non-admin users or if the form is not yet loaded');
|
|
}
|
|
}
|
|
|
|
// Update form handler
|
|
const updateForm = document.getElementById('updateForm');
|
|
if (updateForm) {
|
|
updateForm.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
// Prevent duplicate submissions
|
|
if (this.submitting) {
|
|
return;
|
|
}
|
|
this.submitting = true;
|
|
|
|
if (!isAdmin()) {
|
|
alert('You must be an admin to update facilities');
|
|
this.submitting = false;
|
|
return;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
try {
|
|
// Use authFetch instead of regular fetch
|
|
const response = await window.authFetch('/facilitycontroller.php', {
|
|
method: 'POST',
|
|
body: serverFormData
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('Server response:', errorText);
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const storedData = JSON.parse(sessionStorage.getItem('facilityData') || '[]');
|
|
const index = storedData.findIndex(f => f.id === parseInt(data.facility.id));
|
|
if (index !== -1) {
|
|
storedData[index] = data.facility;
|
|
sessionStorage.setItem('facilityData', JSON.stringify(storedData));
|
|
initialiseFacilityData(storedData, true);
|
|
}
|
|
|
|
// Close modal using multiple methods to ensure it closes
|
|
const modalElement = document.getElementById('updateModal');
|
|
if (modalElement) {
|
|
// Method 1: Using Bootstrap's modal instance
|
|
if (updateModal) {
|
|
updateModal.hide();
|
|
}
|
|
|
|
// Method 2: Using Bootstrap's static method
|
|
const modal = bootstrap.Modal.getInstance(modalElement);
|
|
if (modal) {
|
|
modal.hide();
|
|
}
|
|
|
|
// Method 3: Direct DOM manipulation
|
|
modalElement.classList.remove('show');
|
|
document.body.classList.remove('modal-open');
|
|
const modalBackdrop = document.querySelector('.modal-backdrop');
|
|
if (modalBackdrop) {
|
|
modalBackdrop.remove();
|
|
}
|
|
}
|
|
} else {
|
|
console.error('Update failed:', data.error);
|
|
alert(data.error || 'Failed to update facility');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating facility:', error);
|
|
alert('Failed to update facility: ' + error.message);
|
|
} finally {
|
|
this.submitting = false;
|
|
}
|
|
});
|
|
} else {
|
|
console.error('Update form not found in DOM');
|
|
}
|
|
|
|
// Delete form handler
|
|
const deleteForm = document.getElementById('deleteForm');
|
|
if (deleteForm) {
|
|
deleteForm.addEventListener('submit', async function(e) {
|
|
e.preventDefault(); // Prevent default form submission
|
|
|
|
// Prevent duplicate submissions
|
|
if (this.submitting) {
|
|
return;
|
|
}
|
|
this.submitting = true;
|
|
|
|
if (!isAdmin()) {
|
|
alert('You must be an admin to delete facilities');
|
|
this.submitting = false;
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData(this);
|
|
|
|
// Create a new FormData with the correct field names for the server
|
|
const serverFormData = new FormData();
|
|
serverFormData.append('action', 'delete');
|
|
serverFormData.append('id', formData.get('idDelete')); // Map idDelete to id for the server
|
|
|
|
console.log('Deleting facility with ID:', formData.get('idDelete'));
|
|
|
|
try {
|
|
// Check if token is valid
|
|
if (!window.auth) {
|
|
throw new Error('Auth service not available');
|
|
}
|
|
|
|
// Validate token with server before proceeding
|
|
console.log('Validating token with server...');
|
|
const isValid = await window.auth.validateToken();
|
|
if (!isValid) {
|
|
throw new Error('Authentication token is invalid or expired');
|
|
}
|
|
|
|
// Get token after validation to ensure it's fresh
|
|
const token = window.auth.getToken();
|
|
console.log('Using token for delete request:', token);
|
|
|
|
if (!token) {
|
|
throw new Error('No authentication token available');
|
|
}
|
|
|
|
// Decode token to check payload
|
|
if (window.auth.parseJwt) {
|
|
const payload = window.auth.parseJwt(token);
|
|
console.log('Token payload:', payload);
|
|
console.log('Access level:', payload.accessLevel);
|
|
console.log('Is admin check:', payload.accessLevel === 0 || payload.accessLevel === 1);
|
|
}
|
|
|
|
// Use direct fetch with manual token inclusion
|
|
console.log('Sending delete request to server...');
|
|
const response = await fetch('/facilitycontroller.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
body: serverFormData
|
|
});
|
|
|
|
console.log('Delete response status:', response.status);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('Server response:', errorText);
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('Delete response data:', data);
|
|
|
|
if (data.success) {
|
|
console.log('Delete successful, updating UI...');
|
|
const storedData = JSON.parse(sessionStorage.getItem('facilityData') || '[]');
|
|
const filteredData = storedData.filter(f => f.id !== parseInt(data.facilityId));
|
|
sessionStorage.setItem('facilityData', JSON.stringify(filteredData));
|
|
initialiseFacilityData(filteredData, true);
|
|
|
|
// Close modal using multiple methods to ensure it closes
|
|
const modalElement = document.getElementById('deleteModal');
|
|
if (modalElement) {
|
|
// Method 1: Using Bootstrap's modal instance
|
|
if (deleteModal) {
|
|
deleteModal.hide();
|
|
}
|
|
|
|
// Method 2: Using Bootstrap's static method
|
|
const modal = bootstrap.Modal.getInstance(modalElement);
|
|
if (modal) {
|
|
modal.hide();
|
|
}
|
|
|
|
// Method 3: Direct DOM manipulation
|
|
modalElement.classList.remove('show');
|
|
document.body.classList.remove('modal-open');
|
|
const modalBackdrop = document.querySelector('.modal-backdrop');
|
|
if (modalBackdrop) {
|
|
modalBackdrop.remove();
|
|
}
|
|
}
|
|
|
|
// Reset the form
|
|
deleteForm.reset();
|
|
} else {
|
|
console.error('Delete failed:', data.error);
|
|
alert(data.error || 'Failed to delete facility');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting facility:', error);
|
|
console.error('Error stack:', error.stack);
|
|
alert('Failed to delete facility: ' + error.message);
|
|
} finally {
|
|
this.submitting = false;
|
|
}
|
|
});
|
|
} else {
|
|
console.error('Delete form not found in DOM');
|
|
}
|
|
|
|
// Note: Status/comment form handlers are now in comments.js
|
|
}
|
|
|
|
/**
|
|
* Updates the pagination controls based on current state
|
|
*/
|
|
function updatePaginationControls() {
|
|
const paginationList = document.getElementById('paginationControls');
|
|
const firstButton = document.getElementById('firstPage');
|
|
const prevButton = document.getElementById('prevPage');
|
|
const nextButton = document.getElementById('nextPage');
|
|
const lastButton = document.getElementById('lastPage');
|
|
|
|
// Update button states
|
|
firstButton.parentElement.classList.toggle('disabled', currentPage === 1);
|
|
prevButton.parentElement.classList.toggle('disabled', currentPage === 1);
|
|
nextButton.parentElement.classList.toggle('disabled', currentPage === totalPages);
|
|
lastButton.parentElement.classList.toggle('disabled', currentPage === totalPages);
|
|
|
|
// Remove existing page numbers
|
|
Array.from(paginationList.children).forEach(child => {
|
|
if (!child.contains(firstButton) &&
|
|
!child.contains(prevButton) &&
|
|
!child.contains(nextButton) &&
|
|
!child.contains(lastButton)) {
|
|
child.remove();
|
|
}
|
|
});
|
|
|
|
// Calculate page range
|
|
const maxVisiblePages = 5;
|
|
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
|
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
|
|
|
// Adjust start page if we're near the end
|
|
if (endPage - startPage + 1 < maxVisiblePages) {
|
|
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
|
}
|
|
|
|
// Insert new page numbers
|
|
const insertBeforeElement = nextButton.parentElement;
|
|
|
|
// First page and ellipsis
|
|
if (startPage > 1) {
|
|
const firstPageItem = createPageNumberElement(1, false);
|
|
paginationList.insertBefore(firstPageItem, insertBeforeElement);
|
|
|
|
if (startPage > 2) {
|
|
const ellipsisItem = createEllipsisElement();
|
|
paginationList.insertBefore(ellipsisItem, insertBeforeElement);
|
|
}
|
|
}
|
|
|
|
// Page numbers
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
const pageItem = createPageNumberElement(i, i === currentPage);
|
|
paginationList.insertBefore(pageItem, insertBeforeElement);
|
|
}
|
|
|
|
// Last page and ellipsis
|
|
if (endPage < totalPages) {
|
|
if (endPage < totalPages - 1) {
|
|
const ellipsisItem = createEllipsisElement();
|
|
paginationList.insertBefore(ellipsisItem, insertBeforeElement);
|
|
}
|
|
const lastPageItem = createPageNumberElement(totalPages, false);
|
|
paginationList.insertBefore(lastPageItem, insertBeforeElement);
|
|
}
|
|
|
|
// Update pagination info text
|
|
const paginationInfo = document.getElementById('paginationInfo');
|
|
if (paginationInfo) {
|
|
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
|
const endItem = Math.min(startItem + itemsPerPage - 1, filteredData.length);
|
|
|
|
if (filteredData.length === 0) {
|
|
paginationInfo.querySelector('span').textContent = 'No facilities found';
|
|
} else {
|
|
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) {
|
|
const li = document.createElement('li');
|
|
li.className = `page-item${isActive ? ' active' : ''}`;
|
|
|
|
const a = document.createElement('a');
|
|
a.className = 'page-link border-0';
|
|
a.href = '#';
|
|
a.dataset.page = pageNum;
|
|
a.textContent = pageNum;
|
|
|
|
// Add aria attributes for accessibility
|
|
if (isActive) {
|
|
a.setAttribute('aria-current', 'page');
|
|
a.classList.add('bg-success', 'text-white');
|
|
} else {
|
|
a.classList.add('text-success');
|
|
}
|
|
|
|
li.appendChild(a);
|
|
return li;
|
|
}
|
|
|
|
function createEllipsisElement() {
|
|
const li = document.createElement('li');
|
|
li.className = 'page-item disabled';
|
|
|
|
const span = document.createElement('span');
|
|
span.className = 'page-link border-0';
|
|
span.innerHTML = '<i class="bi bi-three-dots"></i>';
|
|
span.setAttribute('aria-hidden', 'true');
|
|
|
|
li.appendChild(span);
|
|
return li;
|
|
}
|
|
|
|
/**
|
|
* Gets the current page of data
|
|
* @returns {Array} Array of items for the current page
|
|
*/
|
|
function getCurrentPageData() {
|
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
|
const endIndex = Math.min(startIndex + itemsPerPage, filteredData.length);
|
|
const pageData = filteredData.slice(startIndex, endIndex);
|
|
return pageData;
|
|
}
|
|
|
|
/**
|
|
* Updates the table with current page data
|
|
*/
|
|
function updateTableWithPagination() {
|
|
const pageData = getCurrentPageData();
|
|
renderFacilityTable(pageData);
|
|
updatePaginationControls();
|
|
}
|
|
|
|
/**
|
|
* Sets up pagination event listeners
|
|
*/
|
|
function setupPaginationControls() {
|
|
const paginationList = document.getElementById('paginationControls');
|
|
|
|
if (!paginationList) {
|
|
console.error('Pagination controls not found');
|
|
return;
|
|
}
|
|
|
|
// Remove existing handler if it exists
|
|
if (paginationHandler) {
|
|
paginationList.removeEventListener('click', paginationHandler);
|
|
}
|
|
|
|
// Create new handler
|
|
paginationHandler = (e) => {
|
|
e.preventDefault();
|
|
const target = e.target.closest('a');
|
|
if (!target) return;
|
|
|
|
let newPage = currentPage;
|
|
|
|
switch(target.id) {
|
|
case 'firstPage':
|
|
newPage = 1;
|
|
break;
|
|
case 'prevPage':
|
|
newPage = Math.max(1, currentPage - 1);
|
|
break;
|
|
case 'nextPage':
|
|
newPage = Math.min(totalPages, currentPage + 1);
|
|
break;
|
|
case 'lastPage':
|
|
newPage = totalPages;
|
|
break;
|
|
default:
|
|
if (target.dataset.page) {
|
|
newPage = parseInt(target.dataset.page);
|
|
}
|
|
}
|
|
|
|
// Only update if the page actually changed and is valid
|
|
if (newPage !== currentPage && newPage >= 1 && newPage <= totalPages) {
|
|
currentPage = newPage;
|
|
updateTableWithPagination();
|
|
}
|
|
};
|
|
|
|
// Add the new handler
|
|
paginationList.addEventListener('click', paginationHandler);
|
|
}
|
|
|
|
/**
|
|
* Updates the table based on current filter values
|
|
*/
|
|
function updateTable() {
|
|
try {
|
|
const data = JSON.parse(sessionStorage.getItem('facilityData') || '[]');
|
|
if (!data.length) {
|
|
error_log('No facility data found');
|
|
return;
|
|
}
|
|
|
|
// 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();
|
|
|
|
// Apply filters and sorting
|
|
filteredData = filterData(data, filterCategory, searchTerm);
|
|
filteredData = sortData(filteredData, sortBy, sortDir);
|
|
|
|
// Update pagination
|
|
totalPages = Math.ceil(filteredData.length / itemsPerPage);
|
|
currentPage = 1; // Reset to first page when filters change
|
|
|
|
// Update table with current page
|
|
updateTableWithPagination();
|
|
} catch (error) {
|
|
error_log('Error updating table:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets up table controls for filtering and sorting
|
|
*/
|
|
function setupTableControls() {
|
|
// Get form elements
|
|
const filterForm = document.querySelector('form[role="search"]');
|
|
if (!filterForm) {
|
|
error_log('Filter form not found');
|
|
return;
|
|
}
|
|
|
|
// 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');
|
|
return;
|
|
}
|
|
|
|
// Prevent form submission and handle it properly
|
|
filterForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
updateTable();
|
|
});
|
|
|
|
// Add event listeners for immediate updates
|
|
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 pagination controls
|
|
setupPaginationControls();
|
|
}
|
|
|
|
/**
|
|
* Filters the facility data based on current filter values
|
|
* @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) {
|
|
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;
|
|
|
|
// 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;
|
|
}
|
|
|
|
return searchValue.includes(searchTerm.toLowerCase());
|
|
});
|
|
return filtered;
|
|
}
|
|
|
|
/**
|
|
* Sorts the facility data based on current sort order
|
|
* @param {Array} data - Array of facility objects
|
|
* @param {string} sortBy - Sort by field name
|
|
* @param {string} sortDir - Sort direction
|
|
* @returns {Array} Sorted array of facility objects
|
|
*/
|
|
function sortData(data, sortBy, sortDir) {
|
|
if (!sortBy) return data;
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
|
|
return [...data].sort((a, b) => {
|
|
if (!a || !b) return 0;
|
|
|
|
let valueA = '';
|
|
let 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;
|
|
}
|
|
|
|
const comparison = valueA.localeCompare(valueB);
|
|
return sortDir === 'asc' ? comparison : -comparison;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Logs errors to the console
|
|
* @param {string} message - Error message
|
|
* @param {Error} error - Error object
|
|
*/
|
|
function error_log(message, error) {
|
|
console.error(message, error);
|
|
|
|
// Add more detailed error logging
|
|
if (error instanceof Error) {
|
|
console.error('Error name:', error.name);
|
|
console.error('Error message:', error.message);
|
|
console.error('Error stack:', error.stack);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets an appropriate icon for a facility category
|
|
* @param {string} category - The facility category
|
|
* @returns {string} The Bootstrap icon class
|
|
*/
|
|
function getFacilityIcon(category) {
|
|
const categoryLower = (category || '').toLowerCase();
|
|
|
|
if (categoryLower.includes('recycling')) return 'bi bi-recycle';
|
|
if (categoryLower.includes('green') || categoryLower.includes('roof')) return 'bi bi-tree-fill';
|
|
if (categoryLower.includes('solar') || categoryLower.includes('power')) return 'bi bi-sun-fill';
|
|
if (categoryLower.includes('water') || categoryLower.includes('rain')) return 'bi bi-droplet-fill';
|
|
if (categoryLower.includes('battery')) return 'bi bi-battery-charging';
|
|
if (categoryLower.includes('bench')) return 'bi bi-bench';
|
|
|
|
// Default icon
|
|
return 'bi bi-geo-alt-fill';
|
|
}
|
|
|
|
/**
|
|
* Gets an appropriate color class for a facility category
|
|
* @param {string} category - The facility category
|
|
* @returns {string} The 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';
|
|
|
|
// Default color
|
|
return 'secondary';
|
|
}
|
|
|
|
/**
|
|
* Sets up expandable content for a table row
|
|
* @param {HTMLElement} row - The table row element
|
|
*/
|
|
function setupExpandableContent(row) {
|
|
// Setup description expansion
|
|
const descriptionContent = row.querySelector('.cell-content');
|
|
const toggleBtn = row.querySelector('.toggle-content-btn');
|
|
|
|
if (descriptionContent) {
|
|
// Make description expandable on click
|
|
descriptionContent.addEventListener('click', function() {
|
|
this.classList.toggle('expanded');
|
|
|
|
// Update button text if it exists
|
|
if (toggleBtn) {
|
|
toggleBtn.querySelector('small').textContent =
|
|
this.classList.contains('expanded') ? 'Show less' : 'Show more';
|
|
}
|
|
|
|
// Ensure proper alignment when expanded
|
|
const container = this.closest('.description-container');
|
|
if (container) {
|
|
if (this.classList.contains('expanded')) {
|
|
container.classList.remove('justify-content-center');
|
|
container.classList.add('justify-content-start');
|
|
} else {
|
|
container.classList.remove('justify-content-start');
|
|
container.classList.add('justify-content-center');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Setup toggle button if it exists
|
|
if (toggleBtn) {
|
|
toggleBtn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const content = this.closest('.description-container').querySelector('.cell-content');
|
|
content.classList.toggle('expanded');
|
|
|
|
this.querySelector('small').textContent =
|
|
content.classList.contains('expanded') ? 'Show less' : 'Show more';
|
|
|
|
// Ensure proper alignment when expanded
|
|
const container = content.closest('.description-container');
|
|
if (container) {
|
|
if (content.classList.contains('expanded')) {
|
|
container.classList.remove('justify-content-center');
|
|
container.classList.add('justify-content-start');
|
|
} else {
|
|
container.classList.remove('justify-content-start');
|
|
container.classList.add('justify-content-center');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Setup address expansion
|
|
const addressContent = row.querySelector('.address-content');
|
|
if (addressContent) {
|
|
addressContent.addEventListener('click', function() {
|
|
this.classList.toggle('expanded');
|
|
|
|
// Ensure proper alignment when expanded
|
|
const container = this.closest('.d-flex');
|
|
if (container) {
|
|
if (this.classList.contains('expanded')) {
|
|
container.classList.remove('align-items-center');
|
|
container.classList.add('align-items-start');
|
|
} else {
|
|
container.classList.remove('align-items-start');
|
|
container.classList.add('align-items-center');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Export the initialization function
|
|
window.initializeFacilityData = initialiseFacilityData; |