pre-clean

Signed-off-by: boris <boris@borishub.co.uk>
This commit is contained in:
boris
2025-04-21 21:24:46 +01:00
parent 78508a7cbd
commit 8877faa631
15 changed files with 1302 additions and 407 deletions

View File

@@ -133,21 +133,30 @@ class ApiClient {
});
try {
// Validate auth state before making request
if (!this.authFetch) {
throw new Error('Auth fetch not available');
}
if (action === 'status' && (!data.facilityId || !data.statusComment)) {
throw new Error('Missing required data for status update');
}
// Use authenticated fetch for all facility requests
const response = await this.authFetch('/facilitycontroller.php', {
method: 'POST',
body: formData,
requireAuth: true // Explicitly require authentication
requireAuth: true
});
// Parse the response
const jsonData = await response.json();
// Check if response is ok
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
throw new Error(`HTTP error! status: ${response.status}, message: ${jsonData.error || 'Unknown error'}`);
}
// Parse the JSON response
const jsonData = await response.json();
console.log('Facility API response:', { action, data: jsonData });
return jsonData;
} catch (error) {
console.error('Facility API error:', error);
@@ -242,7 +251,11 @@ class ApiClient {
* @returns {Promise<Object>} The response data
*/
async updateFacilityStatus(statusId, editStatus, facilityId) {
return this.facility('editStatus', { statusId, editStatus, facilityId });
return this.facility('editStatus', {
statusId: statusId,
statusComment: editStatus,
facilityId: facilityId
});
}
/**
@@ -259,7 +272,7 @@ class ApiClient {
}
}
// Initialize API client
// initialise API client
const api = new ApiClient();
// Export API client

View File

@@ -1,46 +1,46 @@
/**
* Comments functionality for facility management
* Facility status (comments) manager for adding, removing and editing user comments.
*/
// Create a namespace for comments functionality
// Create a namespace to avoid global scope conflicts with facilityData.js
const CommentsManager = {
// Track initialization states
// Initialization states
state: {
isInitializing: false,
isInitialized: false,
isinitialised: false,
isDomReady: false,
isAuthReady: false
},
/**
* Initialize comments functionality
* initialise status functionality
*/
initialize() {
if (this.state.isInitialized) return;
initialise() {
if (this.state.isinitialised) return;
console.log('Initializing comments...');
// Initialize comment modal handlers
this.initializeCommentModals();
// initialise comment modal handlers
this.initialiseCommentModals();
// Set up form handlers
this.setupCommentFormHandlers();
console.log('Comments initialized with auth state:', {
console.log('Comments initialised with auth state:', {
isAuthenticated: this.isAuthenticated(),
user: window.simpleAuth.getUser()
});
this.state.isInitialized = true;
this.state.isinitialised = true;
},
/**
* Check if we can initialize
* Check if initialisation possible
*/
checkInitialize() {
checkinitialise() {
if (this.state.isDomReady && this.state.isAuthReady && !this.state.isInitializing) {
this.state.isInitializing = true;
this.initialize();
this.initialise();
this.state.isInitializing = false;
}
},
@@ -53,33 +53,33 @@ const CommentsManager = {
},
/**
* Initialize comment modals
* initialise comment modals
*/
initializeCommentModals() {
initialiseCommentModals() {
// Status modal (comments view)
const statusModal = document.getElementById('statusModal');
if (statusModal) {
statusModal.addEventListener('show.bs.modal', (event) => {
console.log('Comments modal is about to show');
// Get the button that triggered the modal
const button = event.relatedTarget;
// Get the facility ID from the data attribute
const facilityId = button.getAttribute('data-facility-id');
console.log('Facility ID for comments:', facilityId);
// Get facility ID from either the button or the modal's data attribute
let facilityId;
// First try to get it from the button that triggered the modal
if (event.relatedTarget) {
facilityId = event.relatedTarget.getAttribute('data-facility-id');
}
// If not found in button, try the modal's data attribute
if (!facilityId && statusModal.hasAttribute('data-facility-id')) {
facilityId = statusModal.getAttribute('data-facility-id');
}
if (!facilityId) {
console.error('No facility ID found for comments');
return;
}
// Set the facility ID in the comment form
const commentForm = document.getElementById('commentForm');
if (commentForm) {
const facilityIdInput = commentForm.querySelector('#commentFacilityId');
if (facilityIdInput) {
facilityIdInput.value = facilityId;
}
}
// Store the facility ID on the modal for later use
statusModal.setAttribute('data-facility-id', facilityId);
// Load facility comments
this.loadFacilityComments(facilityId);
@@ -90,13 +90,10 @@ const CommentsManager = {
const editCommentModal = document.getElementById('editCommentModal');
if (editCommentModal) {
editCommentModal.addEventListener('show.bs.modal', (event) => {
console.log('Edit comment modal is about to show');
const button = event.relatedTarget;
const commentId = button.getAttribute('data-comment-id');
const commentText = button.getAttribute('data-comment-text');
console.log('Comment ID:', commentId, 'Comment text:', commentText);
// Set the comment ID and text in the form
const editForm = document.getElementById('editCommentForm');
if (editForm) {
@@ -151,27 +148,34 @@ const CommentsManager = {
const formData = new FormData(commentForm);
// Get form data
// Get form data and ensure proper types
const statusComment = formData.get('commentText');
const facilityId = formData.get('facilityId');
console.log('Comment form data:', { facilityId, statusComment });
// Validate form data
if (!facilityId) {
console.error('No facility ID found in form');
alert('Error: No facility ID found');
commentForm.submitting = false;
return;
}
if (!statusComment) {
alert('Please enter a comment');
commentForm.submitting = false;
return;
}
try {
console.log('Sending comment request...');
// Use the API client to add a status comment
const data = await window.api.addFacilityStatus(facilityId, statusComment);
console.log('Comment response:', data);
const data = await window.api.addFacilityStatus(facilityId.toString(), statusComment);
if (data.success) {
console.log('Comment added successfully');
// Reset the form
commentForm.reset();
// Reload comments to show the new one
this.loadFacilityComments(facilityId);
this.loadFacilityComments(facilityId.toString());
} else {
console.error('Comment failed:', data.error);
alert(data.error || 'Failed to add comment');
@@ -249,21 +253,8 @@ const CommentsManager = {
* Creates a comment form dynamically for authenticated users
*/
createCommentFormForAuthenticatedUser(facilityId) {
// Add detailed logging of auth state
console.log('Creating comment form with auth state:', {
simpleAuthExists: !!window.simpleAuth,
simpleAuthMethods: window.simpleAuth ? Object.keys(window.simpleAuth) : null,
token: window.simpleAuth ? window.simpleAuth.getToken() : null,
user: window.simpleAuth ? window.simpleAuth.getUser() : null,
localStorage: {
token: localStorage.getItem('token'),
user: localStorage.getItem('user')
}
});
// First check if simpleAuth is available
if (!window.simpleAuth) {
console.warn('SimpleAuth not initialized yet');
return `
<div class="alert alert-warning mb-0">
<i class="bi bi-hourglass-split me-2"></i>
@@ -278,14 +269,7 @@ const CommentsManager = {
const user = window.simpleAuth.getUser();
const isAuthenticated = window.simpleAuth.isAuthenticated();
console.log('Authentication validation:', {
hasToken: !!token,
hasUser: !!user,
isAuthenticated: isAuthenticated
});
if (!isAuthenticated || !token || !user) {
console.log('User not authenticated:', { isAuthenticated, token: !!token, user: !!user });
return `
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle me-2"></i>
@@ -295,7 +279,6 @@ const CommentsManager = {
}
// User is authenticated, create the comment form
console.log('User is authenticated, creating comment form');
return `
<form id="commentForm" class="mt-3">
<input type="hidden" id="commentFacilityId" name="facilityId" value="${this.escapeHtml(facilityId)}">
@@ -327,24 +310,30 @@ const CommentsManager = {
*/
async loadFacilityComments(facilityId) {
try {
console.log('Loading comments for facility:', facilityId);
if (!facilityId) {
throw new Error('No facility ID provided');
}
// Ensure facilityId is a string
facilityId = facilityId.toString();
// Show loading indicator
const commentsContainer = document.getElementById('commentsContainer');
if (commentsContainer) {
commentsContainer.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading comments...</p>
</div>
`;
if (!commentsContainer) {
throw new Error('Comments container not found');
}
commentsContainer.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading comments...</p>
</div>
`;
// Use the API client to get facility statuses
const data = await window.api.getFacilityStatuses(facilityId);
console.log('Comments API response:', data);
// Validate the response
if (!data || typeof data !== 'object') {
@@ -364,7 +353,6 @@ const CommentsManager = {
} catch (error) {
console.error('Error loading comments:', error);
console.error('Error stack:', error.stack);
const commentsContainer = document.getElementById('commentsContainer');
if (commentsContainer) {
@@ -383,22 +371,32 @@ const CommentsManager = {
*/
renderComments(comments, facilityId) {
const commentsContainer = document.getElementById('commentsContainer');
if (!commentsContainer) return;
if (!commentsContainer) {
console.error('Comments container not found');
return;
}
// Clear the container
commentsContainer.innerHTML = '';
// Add the comment form for authenticated users
commentsContainer.innerHTML += this.createCommentFormForAuthenticatedUser(facilityId);
commentsContainer.innerHTML = this.createCommentFormForAuthenticatedUser(facilityId);
// Re-initialise the comment form handler immediately after creating the form
const commentForm = document.getElementById('commentForm');
if (commentForm) {
this.setupCommentFormHandler(commentForm);
}
// If no comments, show a message
if (!comments || comments.length === 0) {
commentsContainer.innerHTML += `
<div class="alert alert-light mt-3">
<i class="bi bi-chat-dots me-2"></i>
No comments yet. Be the first to add a comment!
</div>
const noCommentsDiv = document.createElement('div');
noCommentsDiv.className = 'alert alert-light mt-3';
noCommentsDiv.innerHTML = `
<i class="bi bi-chat-dots me-2"></i>
No comments yet. Be the first to add a comment!
`;
commentsContainer.appendChild(noCommentsDiv);
return;
}
@@ -453,12 +451,6 @@ const CommentsManager = {
});
commentsContainer.appendChild(commentsList);
// Re-initialize the comment form handler
const commentForm = document.getElementById('commentForm');
if (commentForm) {
this.setupCommentFormHandler(commentForm);
}
},
/**
@@ -471,16 +463,10 @@ const CommentsManager = {
}
try {
console.log('Deleting comment:', commentId, 'for facility:', facilityId);
// Use the API client to delete a status comment
const data = await window.api.deleteFacilityStatus(commentId, facilityId);
console.log('Delete comment response:', data);
if (data.success) {
console.log('Comment deleted successfully');
// Reload comments to reflect the deletion
this.loadFacilityComments(facilityId);
} else {
@@ -529,22 +515,22 @@ const CommentsManager = {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
CommentsManager.state.isDomReady = true;
CommentsManager.checkInitialize();
CommentsManager.checkinitialise();
});
} else {
CommentsManager.state.isDomReady = true;
CommentsManager.checkInitialize();
CommentsManager.checkinitialise();
}
// Listen for simpleAuth ready
if (window.simpleAuth) {
CommentsManager.state.isAuthReady = true;
CommentsManager.checkInitialize();
CommentsManager.checkinitialise();
} else {
window.addEventListener('simpleAuthReady', () => {
console.log('SimpleAuth is now ready');
CommentsManager.state.isAuthReady = true;
CommentsManager.checkInitialize();
CommentsManager.checkinitialise();
});
// Fallback timeout in case the event doesn't fire
@@ -552,10 +538,10 @@ if (window.simpleAuth) {
if (!CommentsManager.state.isAuthReady && window.simpleAuth) {
console.log('SimpleAuth found via timeout check');
CommentsManager.state.isAuthReady = true;
CommentsManager.checkInitialize();
CommentsManager.checkinitialise();
}
}, 1000);
}
// Export the CommentsManager to the window object
// Export the CommentsManager to the window
window.CommentsManager = CommentsManager;

View File

@@ -5,8 +5,7 @@
*/
function initialiseFacilityData(data, force = false) {
// Only prevent multiple initializations if not forcing
if (!force && isInitialized) {
console.debug('Facility data already initialized, skipping...');
if (!force && isinitialised) {
return;
}
@@ -21,11 +20,9 @@ function initialiseFacilityData(data, force = false) {
// Check if we're on the map page
const isMapPage = window.location.pathname.includes('map.php');
if (!isMapPage) {
// Only try to initialize table if we're not on the map page
// Only try to initialise table if we're not on the map page
const table = document.querySelector('#facilityTable');
if (!table) {
console.error('Table not found in DOM. Available elements:',
Array.from(document.querySelectorAll('table')).map(t => t.id || 'no-id'));
throw new Error('Facility table not found in DOM');
}
// Clear existing table content
@@ -33,27 +30,30 @@ function initialiseFacilityData(data, force = false) {
if (tbody) {
tbody.innerHTML = '';
} else {
console.warn('No tbody found in table, creating one');
const newTbody = document.createElement('tbody');
table.appendChild(newTbody);
}
// Initialize filteredData with all data
// initialise filteredData with all data
filteredData = data;
// Calculate total pages
totalPages = Math.ceil(filteredData.length / itemsPerPage);
// Set current page to 1
currentPage = 1;
// Update table with paginated data
updateTable();
// Reset sorting state
currentSortField = null;
currentSortOrder = null;
// Set up table controls (sorting and filtering)
setupTableControls();
// Update table with paginated data
updateTableWithPagination();
}
// Mark as initialized
isInitialized = true;
// Mark as initialised
isinitialised = true;
} catch (error) {
console.error('Error initialising facility data:', error);
// Don't throw error if we're on map page, as table errors are expected
@@ -79,7 +79,52 @@ function renderFacilityTable(data) {
// Check if user is admin
const userIsAdmin = isAdmin();
console.log('renderFacilityTable - userIsAdmin:', userIsAdmin);
// Set up table headers first
const tableHeaderRow = document.getElementById('tableHeaderRow');
if (tableHeaderRow) {
// Define header configuration
const headers = [
{ field: 'title', label: 'Title', width: '17%' },
{ field: 'category', label: 'Category', width: '11%', center: true },
{ field: 'description', label: 'Description', width: '27%' },
{ field: 'address', label: 'Address', width: '20%' },
{ field: 'coordinates', label: 'Coordinates', width: '10%', center: true },
{ field: 'contributor', label: 'Contributor', width: '7%', center: true },
{ field: 'actions', label: 'Actions', width: '8%', center: true, sortable: false }
];
// Clear existing headers
tableHeaderRow.innerHTML = '';
// Create header cells
headers.forEach(header => {
const th = document.createElement('th');
th.className = 'fw-semibold' + (header.center ? ' text-center' : '');
th.style.width = header.width;
if (header.sortable !== false) {
th.classList.add('sortable');
th.style.cursor = 'pointer';
th.dataset.field = header.field;
// Create header content with sort indicator
th.innerHTML = `
<div class="d-flex align-items-center gap-1 ${header.center ? 'justify-content-center' : ''}">
<span>${header.label}</span>
<i class="bi bi-arrow-down-up sort-icon"></i>
</div>
`;
// Add click handler
th.addEventListener('click', () => handleHeaderClick(header.field));
} else {
th.textContent = header.label;
}
tableHeaderRow.appendChild(th);
});
}
// Render each row
data.forEach((facility, index) => {
@@ -98,19 +143,9 @@ function renderFacilityTable(data) {
// Start building the row HTML
let rowHtml = '';
// Only show ID column for admins
if (userIsAdmin) {
console.log('Adding ID column for facility:', facility.id);
rowHtml += `
<td class="fw-medium align-middle text-center" style="width: 40px;">
${escapeHtml(facility.id)}
</td>
`;
}
// Add the rest of the columns
rowHtml += `
<td class="fw-medium align-middle" style="${userIsAdmin ? 'width: 15%;' : 'width: 17%;'}">
<td class="fw-medium align-middle" style="width: 17%;">
<div class="d-flex align-items-center h-100">
<div class="facility-icon me-2 rounded-circle bg-light d-flex align-items-center justify-content-center" style="width: 28px; height: 28px; min-width: 28px;">
<i class="${getFacilityIcon(facility.category)} text-${categoryClass}"></i>
@@ -118,14 +153,14 @@ function renderFacilityTable(data) {
<span class="text-truncate" style="max-width: calc(100% - 35px);">${escapeHtml(facility.title)}</span>
</div>
</td>
<td class="text-center align-middle" style="${userIsAdmin ? 'width: 10%;' : 'width: 11%;'}">
<td class="text-center align-middle" style="width: 11%;">
<div class="d-flex align-items-center justify-content-center h-100">
<span class="badge bg-${categoryClass} bg-opacity-10 text-${categoryClass} px-2 py-1 rounded-pill">
${escapeHtml(facility.category)}
</span>
</div>
</td>
<td class="align-middle" style="${userIsAdmin ? 'width: 25%;' : 'width: 27%;'}">
<td class="align-middle" style="width: 27%;">
<div class="description-container d-flex flex-column justify-content-center">
<div class="cell-content" data-full-text="${escapeHtml(facility.description)}">
${escapeHtml(facility.description)}
@@ -144,13 +179,13 @@ function renderFacilityTable(data) {
</div>
</div>
</td>
<td class="small text-nowrap text-center align-middle" style="${userIsAdmin ? 'width: 12%;' : 'width: 12%;'}">
<td class="small text-nowrap text-center align-middle" style="width: 10%;">
<span class="badge bg-light text-dark border">
${escapeHtml(coordinates)}
</span>
</td>
<td class="small text-center align-middle" style="width: 8%;">${escapeHtml(facility.contributor)}</td>
<td class="text-center align-middle" style="${userIsAdmin ? 'width: 10%;' : 'width: 5%;'}">
<td class="small text-center align-middle" style="width: 5%;">${escapeHtml(facility.contributor)}</td>
<td class="text-center align-middle" style="width: 8%;">
<div class="d-flex justify-content-center gap-1">
${userIsAdmin ? `
<button type="button" class="btn btn-sm btn-outline-primary update-btn rounded-circle d-flex align-items-center justify-content-center" style="width: 30px; height: 30px;" data-bs-toggle="modal" data-bs-target="#updateModal" data-facility-id="${facility.id}" title="Edit">
@@ -185,6 +220,9 @@ function renderFacilityTable(data) {
`;
tbody.appendChild(emptyRow);
}
// Update sort indicators
updateSortIndicators();
} catch (error) {
error_log('Error in renderFacilityTable:', error);
}
@@ -283,15 +321,19 @@ let filteredData = [];
let paginationHandler = null;
// Add initialization state tracking
let isInitialized = false;
let isinitialised = false;
// Initialize modals once
// initialise modals once
let updateModal, deleteModal, createModal;
let formHandlersInitialized = false;
let formHandlersinitialised = false;
// Add sorting state variables
let currentSortField = null;
let currentSortOrder = null; // null = unsorted, 'asc' = ascending, 'desc' = descending
document.addEventListener('DOMContentLoaded', function() {
// Initialize modals once
// initialise modals once
const modals = document.querySelectorAll('.modal');
modals.forEach((modal, index) => {
if (modal.id === 'updateModal') {
@@ -388,15 +430,15 @@ document.addEventListener('DOMContentLoaded', function() {
// Set up form handlers with a small delay to ensure DOM is fully loaded
setTimeout(() => {
if (!formHandlersInitialized) {
if (!formHandlersinitialised) {
console.log('Setting up form handlers...');
setupFormHandlers();
formHandlersInitialized = true;
formHandlersinitialised = true;
}
}, 100);
// Initialize facility data if not already initialized
if (!isInitialized) {
// initialise facility data if not already initialised
if (!isinitialised) {
const storedData = sessionStorage.getItem('facilityData');
if (storedData) {
try {
@@ -407,6 +449,23 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
}
// Add CSS styles for sort indicators
const style = document.createElement('style');
style.textContent = `
.sortable:hover {
background-color: rgba(25, 135, 84, 0.1);
}
.sort-icon {
opacity: 0.5;
font-size: 0.8em;
}
.sortable:hover .sort-icon,
.text-success .sort-icon {
opacity: 1;
}
`;
document.head.appendChild(style);
});
// Handle create form submission
@@ -435,6 +494,8 @@ function setupFormHandlers() {
const formData = new FormData(this);
// Set the contributor to the current user's username
formData.set('contCreate', JSON.parse(localStorage.getItem('user'))?.username);
// Set the action to 'create'
formData.set('action', 'create');
try {
// Use simpleAuth.fetchAuth for authenticated requests
const response = await simpleAuth.fetchAuth('/facilitycontroller.php', {
@@ -517,20 +578,30 @@ function setupFormHandlers() {
const formData = new FormData(this);
// Create a new FormData with the correct field names for the server
// This is due to the contributor field being disabled in the form
// disallowing it to be included in the form data
const serverFormData = new FormData();
serverFormData.append('action', 'update');
// Copy all fields from the form to the server form data
for (const [key, value] of formData.entries()) {
serverFormData.append(key, value);
}
// Map form fields to server field names
const fieldMappings = {
'idUpdate': 'id',
'titlUpdate': 'title',
'cateUpdate': 'category',
'descUpdate': 'description',
'hnumUpdate': 'houseNumber',
'strtUpdate': 'streetName',
'cntyUpdate': 'county',
'townUpdate': 'town',
'postUpdate': 'postcode',
'lngUpdate': 'lng',
'latUpdate': 'lat',
'contUpdate': 'contributor'
};
// Ensure the contributor field is included (it might be disabled in the form)
const contUpdateField = document.getElementById('contUpdate');
if (contUpdateField) {
serverFormData.append('contUpdate', contUpdateField.value);
// Copy and transform fields from the form to the server form data
for (const [key, value] of formData.entries()) {
if (fieldMappings[key]) {
serverFormData.append(fieldMappings[key], value);
}
}
try {
@@ -801,12 +872,6 @@ function updatePaginationControls() {
paginationInfo.querySelector('span').textContent = `Showing ${startItem}-${endItem} of ${filteredData.length} facilities`;
}
}
// Update facility count badge
const facilityCount = document.getElementById('facilityCount');
if (facilityCount) {
facilityCount.textContent = `${filteredData.length} facilities`;
}
}
function createPageNumberElement(pageNum, isActive) {
@@ -862,6 +927,8 @@ function updateTableWithPagination() {
const pageData = getCurrentPageData();
renderFacilityTable(pageData);
updatePaginationControls();
// Update sort indicators after rendering table
updateSortIndicators();
}
/**
@@ -930,14 +997,13 @@ function updateTable() {
}
// Get current filter values
const sortBy = document.getElementById('sort').value;
const sortDir = document.getElementById('dir').value;
const filterCategory = document.getElementById('filterCat').value;
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const searchTerm = document.getElementById('searchInput').value;
// Apply filters and sorting
filteredData = filterData(data, filterCategory, searchTerm);
filteredData = sortData(filteredData, sortBy, sortDir);
filteredData = filterData(data, searchTerm);
if (currentSortField && currentSortOrder) {
filteredData = sortData(filteredData, currentSortField, currentSortOrder);
}
// Update pagination
totalPages = Math.ceil(filteredData.length / itemsPerPage);
@@ -962,12 +1028,9 @@ function setupTableControls() {
}
// Get control elements
const filterControls = document.querySelectorAll('.filter-control');
const sortControls = document.querySelectorAll('.sort-control');
const searchInput = document.getElementById('searchInput');
if (!filterControls.length || !sortControls.length || !searchInput) {
error_log('Missing filter or sort controls');
if (!searchInput) {
error_log('Missing search input');
return;
}
@@ -978,66 +1041,142 @@ function setupTableControls() {
updateTable();
});
// Add event listeners for immediate updates
// Add event listener for search input
searchInput.addEventListener('input', updateTable);
// Add change event listeners for select elements
filterControls.forEach(control => {
control.addEventListener('change', updateTable);
});
sortControls.forEach(control => {
control.addEventListener('change', updateTable);
});
// Set up table headers for sorting
setupSortableHeaders();
// Set up pagination controls
setupPaginationControls();
}
/**
* Filters the facility data based on current filter values
* Sets up sortable table headers
*/
function setupSortableHeaders() {
const tableHeaderRow = document.getElementById('tableHeaderRow');
if (!tableHeaderRow) return;
// Define header configuration
const headers = [
{ field: 'title', label: 'Title', width: '17%' },
{ field: 'category', label: 'Category', width: '11%', center: true },
{ field: 'description', label: 'Description', width: '27%' },
{ field: 'address', label: 'Address', width: '20%' },
{ field: 'coordinates', label: 'Coordinates', width: '12%', center: true },
{ field: 'contributor', label: 'Contributor', width: '8%', center: true },
{ field: 'actions', label: 'Actions', width: '5%', center: true, sortable: false }
];
// Clear existing headers
tableHeaderRow.innerHTML = '';
// Create header cells
headers.forEach(header => {
const th = document.createElement('th');
th.className = 'fw-semibold' + (header.center ? ' text-center' : '');
th.style.width = header.width;
if (header.sortable !== false) {
th.classList.add('sortable');
th.style.cursor = 'pointer';
th.dataset.field = header.field;
// Create header content with sort indicator
th.innerHTML = `
<div class="d-flex align-items-center gap-1 ${header.center ? 'justify-content-center' : ''}">
<span>${header.label}</span>
<i class="bi bi-arrow-down-up sort-icon"></i>
</div>
`;
// Add click handler
th.addEventListener('click', () => handleHeaderClick(header.field));
} else {
th.textContent = header.label;
}
tableHeaderRow.appendChild(th);
});
// initialise sort indicators
updateSortIndicators();
}
/**
* Handles click on sortable header
* @param {string} field - The field to sort by
*/
function handleHeaderClick(field) {
console.log('Header clicked:', field); // Debug log
// Rotate through sort orders: none -> asc -> desc -> none
if (currentSortField === field) {
if (currentSortOrder === 'asc') {
currentSortOrder = 'desc';
} else if (currentSortOrder === 'desc') {
currentSortField = null;
currentSortOrder = null;
}
} else {
currentSortField = field;
currentSortOrder = 'asc';
}
console.log('New sort state:', { field: currentSortField, order: currentSortOrder }); // Debug log
// Update table
updateTable();
}
/**
* Updates sort indicators in table headers
*/
function updateSortIndicators() {
const headers = document.querySelectorAll('#tableHeaderRow th.sortable');
headers.forEach(header => {
const icon = header.querySelector('.sort-icon');
if (header.dataset.field === currentSortField) {
icon.classList.remove('bi-arrow-down-up');
icon.classList.add(currentSortOrder === 'asc' ? 'bi-arrow-up' : 'bi-arrow-down');
header.classList.add('text-success');
} else {
icon.classList.remove('bi-arrow-up', 'bi-arrow-down');
icon.classList.add('bi-arrow-down-up');
header.classList.remove('text-success');
}
});
}
/**
* Filters the facility data based on search term
* @param {Array} data - Array of facility objects
* @param {string} category - Filter category
* @param {string} searchTerm - Search term
* @returns {Array} Filtered array of facility objects
*/
function filterData(data, category, searchTerm) {
function filterData(data, searchTerm) {
const filtered = data.filter(facility => {
if (!facility) return false;
// If no category selected or no search term, show all results
if (!category || !searchTerm) return true;
// If no search term, show all results
if (!searchTerm) return true;
// Get the value to search in based on the selected category
let searchValue = '';
switch(category) {
case 'title':
searchValue = (facility.title || '').toLowerCase();
break;
case 'category':
searchValue = (facility.category || '').toLowerCase();
break;
case 'description':
searchValue = (facility.description || '').toLowerCase();
break;
case 'streetName':
searchValue = (facility.streetName || '').toLowerCase();
break;
case 'county':
searchValue = (facility.county || '').toLowerCase();
break;
case 'town':
searchValue = (facility.town || '').toLowerCase();
break;
case 'postcode':
searchValue = (facility.postcode || '').toLowerCase();
break;
case 'contributor':
searchValue = (facility.contributor || '').toLowerCase();
break;
}
// Convert search term to lowercase for case-insensitive search
searchTerm = searchTerm.toLowerCase();
return searchValue.includes(searchTerm.toLowerCase());
// Search across all relevant fields
return (
(facility.title || '').toLowerCase().includes(searchTerm) ||
(facility.category || '').toLowerCase().includes(searchTerm) ||
(facility.description || '').toLowerCase().includes(searchTerm) ||
(facility.streetName || '').toLowerCase().includes(searchTerm) ||
(facility.county || '').toLowerCase().includes(searchTerm) ||
(facility.town || '').toLowerCase().includes(searchTerm) ||
(facility.postcode || '').toLowerCase().includes(searchTerm) ||
(facility.contributor || '').toLowerCase().includes(searchTerm) ||
(facility.houseNumber || '').toLowerCase().includes(searchTerm)
);
});
return filtered;
}
@@ -1051,51 +1190,50 @@ function filterData(data, category, searchTerm) {
*/
function sortData(data, sortBy, sortDir) {
if (!sortBy) return data;
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
console.log('Sorting by:', sortBy, 'Direction:', sortDir); // Debug log
return [...data].sort((a, b) => {
if (!a || !b) return 0;
let valueA = '';
let valueB = '';
let valueA, valueB;
// Get the values to compare based on the selected field
switch (sortBy) {
case 'title':
valueA = (a.title || '').toLowerCase();
valueB = (b.title || '').toLowerCase();
break;
case 'category':
valueA = (a.category || '').toLowerCase();
valueB = (b.category || '').toLowerCase();
break;
case 'description':
valueA = (a.description || '').toLowerCase();
valueB = (b.description || '').toLowerCase();
break;
case 'streetName':
valueA = (a.streetName || '').toLowerCase();
valueB = (b.streetName || '').toLowerCase();
break;
case 'county':
valueA = (a.county || '').toLowerCase();
valueB = (b.county || '').toLowerCase();
break;
case 'town':
valueA = (a.town || '').toLowerCase();
valueB = (b.town || '').toLowerCase();
break;
case 'postcode':
valueA = (a.postcode || '').toLowerCase();
valueB = (b.postcode || '').toLowerCase();
break;
case 'contributor':
valueA = (a.contributor || '').toLowerCase();
valueB = (b.contributor || '').toLowerCase();
break;
// Special handling for address field which is composed of multiple fields
if (sortBy === 'address') {
valueA = [
a.houseNumber || '',
a.streetName || '',
a.town || '',
a.county || '',
a.postcode || ''
].filter(Boolean).join(', ').toLowerCase();
valueB = [
b.houseNumber || '',
b.streetName || '',
b.town || '',
b.county || '',
b.postcode || ''
].filter(Boolean).join(', ').toLowerCase();
}
// Special handling for coordinates field
else if (sortBy === 'coordinates') {
// Sort by latitude first, then longitude
valueA = `${parseFloat(a.lat || 0)},${parseFloat(a.lng || 0)}`;
valueB = `${parseFloat(b.lat || 0)},${parseFloat(b.lng || 0)}`;
}
// Default handling for other fields
else {
valueA = (a[sortBy] || '').toString().toLowerCase();
valueB = (b[sortBy] || '').toString().toLowerCase();
}
const comparison = valueA.localeCompare(valueB);
return sortDir === 'asc' ? comparison : -comparison;
console.log('Comparing:', valueA, valueB); // Debug log
// Compare the values
if (valueA < valueB) return sortDir === 'asc' ? -1 : 1;
if (valueA > valueB) return sortDir === 'asc' ? 1 : -1;
return 0;
});
}
@@ -1154,4 +1292,4 @@ function getCategoryColorClass(category) {
}
// Export the initialization function
window.initializeFacilityData = initialiseFacilityData;
window.initialiseFacilityData = initialiseFacilityData;

634
public/js/mapHandler.js Normal file
View File

@@ -0,0 +1,634 @@
/**
* Map Handler for EcoBuddy
* Handles map initialization, postcode validation, and facility display
*/
// initialise map variables
let map = null;
let markers = [];
let circle = null;
let facilities = [];
let currentPostcode = null;
let currentRadius = 10; // Default radius in miles
// initialise map on document load
document.addEventListener('DOMContentLoaded', function() {
// initialise the map centered on UK
map = L.map('map', {
scrollWheelZoom: true, // Enable scroll wheel zoom
zoomControl: true // Show zoom controls
}).setView([54.5, -2], 6);
// Add OpenStreetMap tiles
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Get facilities data from sessionStorage
facilities = JSON.parse(sessionStorage.getItem('facilityData') || '[]');
// Set up form handlers
setupFormHandlers();
// Set up search handler from header
setupHeaderSearchHandler();
});
/**
* Set up form handlers for postcode and radius inputs
*/
function setupFormHandlers() {
const postcodeForm = document.getElementById('postcodeForm');
const radiusSelect = document.getElementById('radius');
if (postcodeForm) {
postcodeForm.addEventListener('submit', async function(e) {
e.preventDefault();
const postcode = document.getElementById('postcode').value;
const radius = parseFloat(document.getElementById('radius').value);
// Show loading state
const submitButton = this.querySelector('button[type="submit"]');
const originalButtonContent = `<i class="bi bi-search me-1"></i>Search...`;
submitButton.disabled = true;
submitButton.innerHTML = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="ms-2">Searching...</span>
`;
// Validate postcode format first
if (!isValidPostcode(postcode)) {
submitButton.disabled = false;
submitButton.innerHTML = originalButtonContent;
alert('Please enter a valid UK postcode');
return;
}
try {
// Get coordinates for postcode
const coords = await getPostcodeCoordinates(postcode);
if (!coords) {
throw new Error('Could not find coordinates for this postcode');
}
// Update map with new location and radius
updateMapLocation(coords, radius);
// Remove overlay once we have a valid postcode
const overlay = document.getElementById('mapOverlay');
if (overlay) {
overlay.classList.add('hidden');
}
// Store current postcode
currentPostcode = postcode;
currentRadius = radius;
} catch (error) {
console.error('Error processing postcode:', error);
alert(error.message || 'Error processing postcode');
} finally {
// Always reset button state
submitButton.disabled = false;
submitButton.innerHTML = originalButtonContent;
}
});
}
if (radiusSelect) {
radiusSelect.addEventListener('change', function() {
const radius = parseFloat(this.value);
if (currentPostcode) {
updateMapLocation(null, radius); // null coords means use existing center
}
});
}
}
/**
* Validate UK postcode format
* @param {string} postcode - The postcode to validate
* @returns {boolean} True if valid, false otherwise
*/
function isValidPostcode(postcode) {
// Basic UK postcode regex
const postcodeRegex = /^[A-Z]{1,2}[0-9][A-Z0-9]? ?[0-9][A-Z]{2}$/i;
return postcodeRegex.test(postcode.trim());
}
/**
* Get coordinates for a UK postcode using postcodes.io API
* @param {string} postcode - The postcode to geocode
* @returns {Promise<{lat: number, lng: number}>} The coordinates
*/
async function getPostcodeCoordinates(postcode) {
try {
const response = await fetch(`https://api.postcodes.io/postcodes/${encodeURIComponent(postcode)}`);
if (!response.ok) {
throw new Error('Postcode not found');
}
const data = await response.json();
if (data.status === 200 && data.result) {
return {
lat: data.result.latitude,
lng: data.result.longitude
};
}
throw new Error('Invalid response from postcode API');
} catch (error) {
console.error('Error getting postcode coordinates:', error);
throw error;
}
}
/**
* Update map location and display facilities within radius
* @param {Object} coords - The coordinates to center on (null to use existing)
* @param {number} radius - The radius in miles
*/
function updateMapLocation(coords, radius) {
// Clear existing markers and circle
clearMapOverlays();
// Get center coordinates (either new or existing)
const center = coords || map.getCenter();
// Convert radius from miles to meters (1 mile = 1609.34 meters)
const radiusMeters = radius * 1609.34;
// Add circle for radius
circle = L.circle([center.lat, center.lng], {
color: '#198754',
fillColor: '#198754',
fillOpacity: 0.1,
radius: radiusMeters
}).addTo(map);
// Find facilities within radius
const facilitiesInRange = findFacilitiesInRange(center, radius);
// Add markers for facilities
facilitiesInRange.forEach(facility => {
const marker = L.marker([facility.lat, facility.lng])
.bindPopup(createPopupContent(facility))
.addTo(map);
markers.push(marker);
});
// Fit map bounds to circle
map.fitBounds(circle.getBounds());
// Update facility list
updateFacilityList(facilitiesInRange);
}
/**
* Clear all markers and circle from map
*/
function clearMapOverlays() {
// Clear markers
markers.forEach(marker => marker.remove());
markers = [];
// Clear circle
if (circle) {
circle.remove();
circle = null;
}
}
/**
* Find facilities within specified radius of center point
* @param {Object} center - The center coordinates
* @param {number} radius - The radius in miles
* @returns {Array} Array of facilities within range
*/
function findFacilitiesInRange(center, radius) {
return facilities.filter(facility => {
const distance = calculateDistance(
center.lat,
center.lng,
parseFloat(facility.lat),
parseFloat(facility.lng)
);
return distance <= radius;
});
}
/**
* Calculate distance between two points using Haversine formula
* @param {number} lat1 - Latitude of first point
* @param {number} lon1 - Longitude of first point
* @param {number} lat2 - Latitude of second point
* @param {number} lon2 - Longitude of second point
* @returns {number} Distance in miles
*/
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 3959; // Earth's radius in miles
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
/**
* Convert degrees to radians
* @param {number} degrees - Value in degrees
* @returns {number} Value in radians
*/
function toRad(degrees) {
return degrees * (Math.PI / 180);
}
/**
* Create popup content for facility marker
* @param {Object} facility - The facility data
* @returns {string} HTML content for popup
*/
function createPopupContent(facility) {
const isAuthenticated = window.simpleAuth && window.simpleAuth.isAuthenticated();
return `
<div class="facility-popup">
<h6 class="mb-1">${escapeHtml(facility.title)}</h6>
<p class="mb-1 small">
<span class="badge bg-${getCategoryColorClass(facility.category)} bg-opacity-10 text-${getCategoryColorClass(facility.category)}">
${escapeHtml(facility.category)}
</span>
</p>
<p class="mb-2 small">${escapeHtml(facility.description)}</p>
<p class="mb-2 small">
<strong>Address:</strong><br>
${escapeHtml(formatAddress(facility))}
</p>
<p class="mb-0 small">
<strong>Added by:</strong> ${escapeHtml(facility.contributor)}
</p>
${isAuthenticated ? `
<div class="comment-form">
<form onsubmit="return handleCommentSubmit(event, ${facility.id})">
<div class="mb-2">
<textarea class="form-control form-control-sm"
placeholder="Add a comment..."
required
rows="2"></textarea>
</div>
<div class="d-flex justify-content-between align-items-center">
<button type="submit" class="btn btn-sm btn-success">
<i class="bi bi-chat-dots me-1"></i>Add Comment
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="openCommentsModal('${facility.id}')">
<i class="bi bi-info-circle me-1"></i>View All Comments
</button>
</div>
</form>
</div>
` : `
<div class="comment-form">
<div class="alert alert-light mb-0 small">
<i class="bi bi-info-circle me-1"></i>
Please <a href="#" data-bs-toggle="modal" data-bs-target="#loginModal">login</a> to add comments
</div>
</div>
`}
</div>
`;
}
/**
* Open the comments modal for a facility
* @param {string} facilityId - The facility ID
*/
function openCommentsModal(facilityId) {
// Find the facility
const facility = facilities.find(f => f.id === parseInt(facilityId));
if (!facility) {
console.error('Facility not found:', facilityId);
return;
}
// Get the modal
const modal = document.getElementById('statusModal');
if (!modal) {
console.error('Status modal not found');
return;
}
// Set the facility ID on the modal
modal.setAttribute('data-facility-id', facilityId);
// Set the facility ID in the comment form
const facilityIdInput = modal.querySelector('#commentFacilityId');
if (facilityIdInput) {
facilityIdInput.value = facilityId;
}
// Show the modal
const modalInstance = new bootstrap.Modal(modal);
modalInstance.show();
// Load the comments using CommentsManager
CommentsManager.loadFacilityComments(facilityId);
}
/**
* Handle comment form submission
* @param {Event} event - The form submit event
* @param {number} facilityId - The facility ID
* @returns {boolean} False to prevent form submission
*/
async function handleCommentSubmit(event, facilityId) {
event.preventDefault();
// Check authentication
if (!window.simpleAuth || !window.simpleAuth.isAuthenticated()) {
alert('You must be logged in to add comments');
return false;
}
const form = event.target;
const textarea = form.querySelector('textarea');
const submitButton = form.querySelector('button[type="submit"]');
const originalButtonContent = submitButton.innerHTML;
// Show loading state
submitButton.disabled = true;
submitButton.innerHTML = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="ms-2">Adding...</span>
`;
try {
// Add the comment using the API
const response = await window.api.addFacilityStatus(
facilityId.toString(), // Ensure facilityId is a string
textarea.value
);
if (response.success) {
// Clear the textarea
textarea.value = '';
// Show success message
const successMessage = document.createElement('div');
successMessage.className = 'alert alert-success mt-2 mb-0 py-2 small';
successMessage.innerHTML = `
<i class="bi bi-check-circle me-1"></i>
Comment added successfully
`;
form.appendChild(successMessage);
// Remove success message after 3 seconds
setTimeout(() => {
successMessage.remove();
// Open the comments modal to show the new comment
openCommentsModal(facilityId);
}, 3000);
} else {
throw new Error(response.error || 'Failed to add comment');
}
} catch (error) {
console.error('Error adding comment:', error);
alert('Error adding comment: ' + error.message);
} finally {
// Reset button state
submitButton.disabled = false;
submitButton.innerHTML = originalButtonContent;
}
return false;
}
/**
* Update facility list display
* @param {Array} facilities - Array of facilities to display
*/
function updateFacilityList(facilities) {
const listElement = document.getElementById('facilityList');
if (!listElement) return;
listElement.innerHTML = '';
facilities.forEach(facility => {
const distance = calculateDistance(
circle.getLatLng().lat,
circle.getLatLng().lng,
parseFloat(facility.lat),
parseFloat(facility.lng)
);
const item = document.createElement('div');
item.className = 'list-group-item list-group-item-action';
item.innerHTML = `
<div class="d-flex w-100 justify-content-between align-items-start">
<div>
<h6 class="mb-1">${escapeHtml(facility.title)}</h6>
<p class="mb-1 small">
<span class="badge bg-${getCategoryColorClass(facility.category)} bg-opacity-10 text-${getCategoryColorClass(facility.category)}">
${escapeHtml(facility.category)}
</span>
</p>
<small class="text-muted">
<i class="bi bi-geo-alt me-1"></i>${distance.toFixed(1)} miles away
</small>
</div>
<button class="btn btn-sm btn-outline-success" onclick="showFacilityDetails('${facility.id}')">
<i class="bi bi-info-circle"></i>
</button>
</div>
`;
listElement.appendChild(item);
});
}
/**
* Set up header search handler
*/
function setupHeaderSearchHandler() {
const searchInput = document.querySelector('input#searchInput');
const filterCat = document.querySelector('select#filterCat');
if (searchInput && filterCat) {
const handleSearch = () => {
const searchTerm = searchInput.value.toLowerCase();
const filterCategory = filterCat.value;
if (!currentPostcode) return; // Only filter if map is active
// Get all facilities in current radius
const center = circle ? circle.getLatLng() : null;
if (!center) return;
const facilitiesInRange = findFacilitiesInRange(center, currentRadius);
// Filter facilities based on search term and category
const filteredFacilities = facilitiesInRange.filter(facility => {
if (!facility) return false;
if (filterCategory && searchTerm) {
let searchValue = '';
switch(filterCategory) {
case 'title':
searchValue = (facility.title || '').toLowerCase();
break;
case 'category':
searchValue = (facility.category || '').toLowerCase();
break;
case 'description':
searchValue = (facility.description || '').toLowerCase();
break;
case 'streetName':
searchValue = (facility.streetName || '').toLowerCase();
break;
case 'county':
searchValue = (facility.county || '').toLowerCase();
break;
case 'town':
searchValue = (facility.town || '').toLowerCase();
break;
case 'postcode':
searchValue = (facility.postcode || '').toLowerCase();
break;
case 'contributor':
searchValue = (facility.contributor || '').toLowerCase();
break;
}
return searchValue.includes(searchTerm);
}
return true;
});
// Update markers and list
updateMapMarkers(filteredFacilities);
updateFacilityList(filteredFacilities);
};
searchInput.addEventListener('input', handleSearch);
filterCat.addEventListener('change', handleSearch);
}
}
/**
* Update map markers without changing the circle or center
* @param {Array} facilities - Facilities to show on map
*/
function updateMapMarkers(facilities) {
// Clear existing markers
markers.forEach(marker => marker.remove());
markers = [];
// Add new markers
facilities.forEach(facility => {
const marker = L.marker([facility.lat, facility.lng])
.bindPopup(createPopupContent(facility))
.addTo(map);
markers.push(marker);
});
}
/**
* Show facility details in a popover
* @param {string} facilityId - The facility ID
*/
function showFacilityDetails(facilityId) {
const facility = facilities.find(f => f.id === parseInt(facilityId));
if (!facility) return;
// Create popover content
const content = `
<div class="facility-details p-2">
<h6 class="mb-2">${escapeHtml(facility.title)}</h6>
<p class="mb-2">
<span class="badge bg-${getCategoryColorClass(facility.category)} bg-opacity-10 text-${getCategoryColorClass(facility.category)}">
${escapeHtml(facility.category)}
</span>
</p>
<p class="mb-2 small">${escapeHtml(facility.description)}</p>
<p class="mb-2 small">
<strong>Address:</strong><br>
${escapeHtml(formatAddress(facility))}
</p>
<p class="mb-0 small">
<strong>Added by:</strong> ${escapeHtml(facility.contributor)}
</p>
</div>
`;
// Find the marker for this facility
const marker = markers.find(m =>
m.getLatLng().lat === parseFloat(facility.lat) &&
m.getLatLng().lng === parseFloat(facility.lng)
);
if (marker) {
marker.bindPopup(content, {
maxWidth: 300,
className: 'facility-popup'
}).openPopup();
}
}
/**
* Format facility address
* @param {Object} facility - The facility data
* @returns {string} Formatted address
*/
function formatAddress(facility) {
const parts = [
facility.houseNumber,
facility.streetName,
facility.town,
facility.county,
facility.postcode
].filter(Boolean);
return parts.join(', ');
}
/**
* Get color class for facility category
* @param {string} category - The facility category
* @returns {string} Bootstrap color class
*/
function getCategoryColorClass(category) {
const categoryLower = (category || '').toLowerCase();
if (categoryLower.includes('recycling')) return 'success';
if (categoryLower.includes('green') || categoryLower.includes('roof')) return 'success';
if (categoryLower.includes('solar') || categoryLower.includes('power')) return 'warning';
if (categoryLower.includes('water') || categoryLower.includes('rain')) return 'info';
if (categoryLower.includes('battery')) return 'danger';
if (categoryLower.includes('bench')) return 'primary';
return 'secondary';
}
/**
* Escape HTML to prevent XSS
* @param {string} unsafe - Unsafe string
* @returns {string} Escaped string
*/
function escapeHtml(unsafe) {
if (unsafe === null || unsafe === undefined) {
return '';
}
return unsafe
.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

View File

@@ -6,7 +6,7 @@
*/
class SimpleAuth {
/**
* Initialize the authentication helper
* initialise the authentication helper
*/
constructor() {
this.token = localStorage.getItem('token');
@@ -25,12 +25,6 @@ class SimpleAuth {
console.warn('Browser fingerprint mismatch - clearing authentication');
this.logout(false); // Silent logout (no redirect)
}
// Log initialization
console.log('SimpleAuth initialized:', {
isAuthenticated: this.isAuthenticated(),
user: this.user
});
}
/**