1295 lines
50 KiB
JavaScript
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, "&")
|
|
.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 (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; |