Files
Ecobuddy/public/js/facilityData.js
boris 8877faa631 pre-clean
Signed-off-by: boris <boris@borishub.co.uk>
2025-04-21 21:24:46 +01:00

1295 lines
50 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 && isinitialised) {
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));
// Check if we're on the map page
const isMapPage = window.location.pathname.includes('map.php');
if (!isMapPage) {
// Only try to initialise table if we're not on the map page
const table = document.querySelector('#facilityTable');
if (!table) {
throw new Error('Facility table not found in DOM');
}
// Clear existing table content
const tbody = table.querySelector('tbody');
if (tbody) {
tbody.innerHTML = '';
} else {
const newTbody = document.createElement('tbody');
table.appendChild(newTbody);
}
// initialise filteredData with all data
filteredData = data;
// Calculate total pages
totalPages = Math.ceil(filteredData.length / itemsPerPage);
// Set current page to 1
currentPage = 1;
// Reset sorting state
currentSortField = null;
currentSortOrder = null;
// Set up table controls (sorting and filtering)
setupTableControls();
// Update table with paginated data
updateTableWithPagination();
}
// 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
if (!window.location.pathname.includes('map.php')) {
throw 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 = '';
// Check if user is admin
const userIsAdmin = isAdmin();
// 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) => {
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);
// Start building the row HTML
let rowHtml = '';
// Add the rest of the columns
rowHtml += `
<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>
</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: 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="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)}
</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 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: 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">
<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>
`;
row.innerHTML = rowHtml;
tbody.appendChild(row);
});
// If no data, show a message
if (data.length === 0) {
const emptyRow = document.createElement('tr');
const colSpan = userIsAdmin ? 8 : 7; // Adjust colspan based on number of columns
emptyRow.innerHTML = `
<td colspan="${colSpan}" 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);
}
// Update sort indicators
updateSortIndicators();
} 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
/**
* (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() {
console.log('Checking admin status...');
// Check if auth service is available and has user data
if (simpleAuth && simpleAuth.getUser()) {
const authUser = simpleAuth.getUser();
console.log('Auth service user data:', authUser);
console.log('Auth service accessLevel:', authUser.accessLevel);
console.log('Auth service isAdmin check:', authUser.accessLevel === 1 || authUser.accessLevel === 0);
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);
console.log('localStorage accessLevel:', user.accessLevel);
console.log('localStorage isAdmin check:', user.accessLevel === 1 || user.accessLevel === 0);
const isAdminUser = user && (user.accessLevel === 1 || user.accessLevel === 0);
console.log('Final isAdmin result:', isAdminUser);
return isAdminUser;
}
/**
* (helper function) Checks if the current user is authenticated
* @returns {boolean} True if authenticated, false otherwise
*/
function isAuthenticated() {
// Check if auth service is available
if (simpleAuth) {
return simpleAuth.isAuthenticated();
}
// Fallback to localStorage
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 isinitialised = false;
// initialise modals once
let updateModal, deleteModal, createModal;
let formHandlersinitialised = false;
// Add sorting state variables
let currentSortField = null;
let currentSortOrder = null; // null = unsorted, 'asc' = ascending, 'desc' = descending
document.addEventListener('DOMContentLoaded', function() {
// initialise 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 (!formHandlersinitialised) {
console.log('Setting up form handlers...');
setupFormHandlers();
formHandlersinitialised = true;
}
}, 100);
// initialise facility data if not already initialised
if (!isinitialised) {
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);
}
}
}
// 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
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);
// Set the action to 'create'
formData.set('action', 'create');
try {
// Use simpleAuth.fetchAuth for authenticated requests
const response = await simpleAuth.fetchAuth('/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
const serverFormData = new FormData();
serverFormData.append('action', 'update');
// 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'
};
// 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 {
// Use simpleAuth.fetchAuth for authenticated requests
const response = await simpleAuth.fetchAuth('/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 (!simpleAuth) {
throw new Error('Auth service not available');
}
// Validate token with server before proceeding
console.log('Validating token with server...');
const isValid = await simpleAuth.validateToken();
if (!isValid) {
throw new Error('Authentication token is invalid or expired');
}
// Get token after validation to ensure it's fresh
const token = simpleAuth.getToken();
console.log('Using token for delete request:', token);
if (!token) {
throw new Error('No authentication token available');
}
// Decode token to check payload
if (simpleAuth.parseJwt) {
const payload = simpleAuth.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 simpleAuth.fetchAuth for authenticated requests
console.log('Sending delete request to server...');
const response = await simpleAuth.fetchAuth('/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`;
}
}
}
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();
// Update sort indicators after rendering table
updateSortIndicators();
}
/**
* 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 searchTerm = document.getElementById('searchInput').value;
// Apply filters and sorting
filteredData = filterData(data, searchTerm);
if (currentSortField && currentSortOrder) {
filteredData = sortData(filteredData, currentSortField, currentSortOrder);
}
// 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 searchInput = document.getElementById('searchInput');
if (!searchInput) {
error_log('Missing search input');
return;
}
// Prevent form submission and handle it properly
filterForm.addEventListener('submit', function(e) {
e.preventDefault();
e.stopPropagation();
updateTable();
});
// Add event listener for search input
searchInput.addEventListener('input', updateTable);
// Set up table headers for sorting
setupSortableHeaders();
// Set up pagination controls
setupPaginationControls();
}
/**
* 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} searchTerm - Search term
* @returns {Array} Filtered array of facility objects
*/
function filterData(data, searchTerm) {
const filtered = data.filter(facility => {
if (!facility) return false;
// If no search term, show all results
if (!searchTerm) return true;
// Convert search term to lowercase for case-insensitive search
searchTerm = 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;
}
/**
* 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;
console.log('Sorting by:', sortBy, 'Direction:', sortDir); // Debug log
return [...data].sort((a, b) => {
if (!a || !b) return 0;
let valueA, valueB;
// 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();
}
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;
});
}
/**
* 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';
}
// Export the initialization function
window.initialiseFacilityData = initialiseFacilityData;