/** * 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 = `
${header.label}
`; // 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 += `
${escapeHtml(facility.title)}
${escapeHtml(facility.category)}
${escapeHtml(facility.description)}
${facility.description.length > 100 ? `` : ''}
${escapeHtml(formatAddress(facility))}
${escapeHtml(coordinates)} ${escapeHtml(facility.contributor)}
${userIsAdmin ? ` ` : ''}
`; 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 = `

No facilities found

`; 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, "&") .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() { console.log('Checking admin status...'); // Check if auth service is available and has user data if (auth && auth.getUser()) { const authUser = auth.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 (auth) { return auth.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 auth.fetchAuth for authenticated requests const response = await auth.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 auth.fetchAuth for authenticated requests const response = await auth.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 (!auth) { throw new Error('Auth service not available'); } // Validate token with server before proceeding console.log('Validating token with server...'); const isValid = await auth.validateToken(); if (!isValid) { throw new Error('Authentication token is invalid or expired'); } // Get token after validation to ensure it's fresh const token = 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 (auth.parseJwt) { const payload = 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 auth.fetchAuth for authenticated requests console.log('Sending delete request to server...'); const response = await auth.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 = ''; 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 = `
${header.label}
`; // 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;