2 Commits

Author SHA1 Message Date
boris
183cca3fd3 pre-clean x2
Signed-off-by: boris <boris@borishub.co.uk>
2025-04-21 23:02:08 +01:00
boris
8877faa631 pre-clean
Signed-off-by: boris <boris@borishub.co.uk>
2025-04-21 21:24:46 +01:00
28 changed files with 1463 additions and 19804 deletions

View File

@@ -1,232 +0,0 @@
import sqlite3
import random
# Connect to the SQLite database
conn = sqlite3.connect('Databases/ecobuddy.sqlite')
cursor = conn.cursor()
# Check if we need to add any new categories
cursor.execute("SELECT id FROM ecoCategories")
existing_categories = [row[0] for row in cursor.fetchall()]
# Add two new categories
new_categories = [
(16, "Urban Farms"),
(17, "Rainwater Harvesting Systems")
]
for category in new_categories:
if category[0] not in existing_categories:
cursor.execute("INSERT INTO ecoCategories (id, name) VALUES (?, ?)", category)
print(f"Added new category: {category[1]}")
# Get list of user IDs for contributors
cursor.execute("SELECT id FROM ecoUser")
user_ids = [row[0] for row in cursor.fetchall()]
# Define 10 new ecological facilities in the UK with accurate location data
new_facilities = [
{
"title": "Community Garden Hackney",
"category": 12, # Pollinator Gardens
"description": "Urban garden with native plants to support local pollinators",
"houseNumber": "45",
"streetName": "Dalston Lane",
"county": "Greater London",
"town": "London",
"postcode": "E8 3AH",
"lng": -0.0612,
"lat": 51.5476,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently expanded with new wildflower section",
"Volunteer days every Saturday"
]
},
{
"title": "Rooftop Solar Farm",
"category": 8, # Green Roofs
"description": "Combined green roof and solar panel installation on commercial building",
"houseNumber": "120",
"streetName": "Deansgate",
"county": "Greater Manchester",
"town": "Manchester",
"postcode": "M3 2QJ",
"lng": -2.2484,
"lat": 53.4808,
"contributor": random.choice(user_ids),
"status_comments": [
"Generates power for the entire building"
]
},
{
"title": "Edinburgh Tool Library",
"category": 15, # Community Tool Libraries
"description": "Borrow tools instead of buying them - reducing waste and consumption",
"houseNumber": "25",
"streetName": "Leith Walk",
"county": "Edinburgh",
"town": "Edinburgh",
"postcode": "EH6 8LN",
"lng": -3.1752,
"lat": 55.9677,
"contributor": random.choice(user_ids),
"status_comments": []
},
{
"title": "Cardiff Bay Water Refill Station",
"category": 9, # Public Water Refill Stations
"description": "Free water refill station to reduce plastic bottle usage",
"houseNumber": "3",
"streetName": "Mermaid Quay",
"county": "Cardiff",
"town": "Cardiff",
"postcode": "CF10 5BZ",
"lng": -3.1644,
"lat": 51.4644,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently cleaned and maintained",
"High usage during summer months"
]
},
{
"title": "Bristol Urban Farm",
"category": 16, # Urban Farms (new category)
"description": "Community-run urban farm providing local produce and education",
"houseNumber": "18",
"streetName": "Stapleton Road",
"county": "Bristol",
"town": "Bristol",
"postcode": "BS5 0RA",
"lng": -2.5677,
"lat": 51.4635,
"contributor": random.choice(user_ids),
"status_comments": [
"Open for volunteers Tuesday-Sunday"
]
},
{
"title": "Newcastle Rainwater Collection System",
"category": 17, # Rainwater Harvesting Systems (new category)
"description": "Public demonstration of rainwater harvesting for garden irrigation",
"houseNumber": "55",
"streetName": "Northumberland Street",
"county": "Tyne and Wear",
"town": "Newcastle upon Tyne",
"postcode": "NE1 7DH",
"lng": -1.6178,
"lat": 54.9783,
"contributor": random.choice(user_ids),
"status_comments": []
},
{
"title": "Brighton Beach Solar Bench",
"category": 7, # Solar-Powered Benches
"description": "Solar-powered bench with USB charging ports and WiFi",
"houseNumber": "",
"streetName": "Kings Road",
"county": "East Sussex",
"town": "Brighton",
"postcode": "BN1 2FN",
"lng": -0.1426,
"lat": 50.8214,
"contributor": random.choice(user_ids),
"status_comments": [
"Popular spot for tourists",
"One USB port currently not working"
]
},
{
"title": "Leeds Community Compost Hub",
"category": 6, # Community Compost Bins
"description": "Large-scale community composting facility for local residents",
"houseNumber": "78",
"streetName": "Woodhouse Lane",
"county": "West Yorkshire",
"town": "Leeds",
"postcode": "LS2 9JT",
"lng": -1.5491,
"lat": 53.8067,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently expanded capacity"
]
},
{
"title": "Glasgow EV Charging Hub",
"category": 4, # Public EV Charging Stations
"description": "Multi-vehicle EV charging station with fast chargers",
"houseNumber": "42",
"streetName": "Buchanan Street",
"county": "Glasgow",
"town": "Glasgow",
"postcode": "G1 3JX",
"lng": -4.2526,
"lat": 55.8621,
"contributor": random.choice(user_ids),
"status_comments": [
"6 charging points available",
"24/7 access"
]
},
{
"title": "Oxford E-Waste Collection Center",
"category": 13, # E-Waste Collection Bins
"description": "Dedicated facility for proper disposal and recycling of electronic waste",
"houseNumber": "15",
"streetName": "St Aldate's",
"county": "Oxfordshire",
"town": "Oxford",
"postcode": "OX1 1BX",
"lng": -1.2577,
"lat": 51.7520,
"contributor": random.choice(user_ids),
"status_comments": []
}
]
# Insert facilities into the database
for facility in new_facilities:
cursor.execute("""
INSERT INTO ecoFacilities
(title, category, description, houseNumber, streetName, county, town, postcode, lng, lat, contributor)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
facility["title"],
facility["category"],
facility["description"],
facility["houseNumber"],
facility["streetName"],
facility["county"],
facility["town"],
facility["postcode"],
facility["lng"],
facility["lat"],
facility["contributor"]
))
# Get the ID of the inserted facility
facility_id = cursor.lastrowid
# Add status comments if any
for comment in facility["status_comments"]:
cursor.execute("""
INSERT INTO ecoFacilityStatus (facilityId, statusComment)
VALUES (?, ?)
""", (facility_id, comment))
print(f"Added facility: {facility['title']} in {facility['town']}")
# Commit the changes and close the connection
conn.commit()
conn.close()
print("\nSuccessfully added 10 new ecological facilities to the database.")
Using the test data in the ecoCategories, ecoFacilities, and ecoFacilityStatus, please create 10 new ecological facilities with the following constraints:
Must be located in the United Kingdom (Names of areas, towns, cities, counties)
Postcode, Latitude and Longitude must be fairly accurate to the location you have generated for the area.
More categories may be made, it is up to you, but make sure foreign keys are respected.
Contributor may be of any user in the database.
Not all facilities must have statusComments, however facilities may have multiple statusComments.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,66 +0,0 @@
Starting facility generation at 2025-03-20 12:34:00.832910
Target: 1000 new facilities
Generating facilities...
Generated 100 facilities so far...
Generated 200 facilities so far...
Generated 300 facilities so far...
Generated 400 facilities so far...
Generated 500 facilities so far...
Generated 600 facilities so far...
Generated 700 facilities so far...
Generated 800 facilities so far...
Generated 900 facilities so far...
Generated 1000 facilities so far...
Inserting facilities into database...
Inserted batch 1/20
Inserted batch 2/20
Inserted batch 3/20
Inserted batch 4/20
Inserted batch 5/20
Inserted batch 6/20
Inserted batch 7/20
Inserted batch 8/20
Inserted batch 9/20
Inserted batch 10/20
Inserted batch 11/20
Inserted batch 12/20
Inserted batch 13/20
Inserted batch 14/20
Inserted batch 15/20
Inserted batch 16/20
Inserted batch 17/20
Inserted batch 18/20
Inserted batch 19/20
Inserted batch 20/20
Inserting status comments...
Inserted comment batch 1/23
Inserted comment batch 2/23
Inserted comment batch 3/23
Inserted comment batch 4/23
Inserted comment batch 5/23
Inserted comment batch 6/23
Inserted comment batch 7/23
Inserted comment batch 8/23
Inserted comment batch 9/23
Inserted comment batch 10/23
Inserted comment batch 11/23
Inserted comment batch 12/23
Inserted comment batch 13/23
Inserted comment batch 14/23
Inserted comment batch 15/23
Inserted comment batch 16/23
Inserted comment batch 17/23
Inserted comment batch 18/23
Inserted comment batch 19/23
Inserted comment batch 20/23
Inserted comment batch 21/23
Inserted comment batch 22/23
Inserted comment batch 23/23
Generation complete at 2025-03-20 12:34:00.860477
Total facilities in database: 12025
Total status comments in database: 13385
Generated facilities saved to generated_facilities.csv for reference

View File

@@ -1,568 +0,0 @@
import sqlite3
import random
import csv
import os
from datetime import datetime
# Connect to the SQLite database
conn = sqlite3.connect('ecobuddy.sqlite')
cursor = conn.cursor()
# Get current max facility ID
cursor.execute("SELECT MAX(id) FROM ecoFacilities")
max_facility_id = cursor.fetchone()[0] or 0
# Get list of user IDs for contributors
cursor.execute("SELECT id FROM ecoUser")
user_ids = [row[0] for row in cursor.fetchall()]
# Get list of categories
cursor.execute("SELECT id, name FROM ecoCategories")
categories = {row[0]: row[1] for row in cursor.fetchall()}
# UK Cities and Towns with their counties and approximate coordinates
uk_locations = [
# Format: Town/City, County, Latitude, Longitude, Postcode Area
("London", "Greater London", 51.5074, -0.1278, "EC"),
("Birmingham", "West Midlands", 52.4862, -1.8904, "B"),
("Manchester", "Greater Manchester", 53.4808, -2.2426, "M"),
("Glasgow", "Glasgow", 55.8642, -4.2518, "G"),
("Liverpool", "Merseyside", 53.4084, -2.9916, "L"),
("Bristol", "Bristol", 51.4545, -2.5879, "BS"),
("Edinburgh", "Edinburgh", 55.9533, -3.1883, "EH"),
("Leeds", "West Yorkshire", 53.8008, -1.5491, "LS"),
("Sheffield", "South Yorkshire", 53.3811, -1.4701, "S"),
("Newcastle upon Tyne", "Tyne and Wear", 54.9783, -1.6178, "NE"),
("Nottingham", "Nottinghamshire", 52.9548, -1.1581, "NG"),
("Cardiff", "Cardiff", 51.4816, -3.1791, "CF"),
("Belfast", "Belfast", 54.5973, -5.9301, "BT"),
("Brighton", "East Sussex", 50.8225, -0.1372, "BN"),
("Leicester", "Leicestershire", 52.6369, -1.1398, "LE"),
("Aberdeen", "Aberdeen", 57.1497, -2.0943, "AB"),
("Portsmouth", "Hampshire", 50.8198, -1.0880, "PO"),
("York", "North Yorkshire", 53.9599, -1.0873, "YO"),
("Swansea", "Swansea", 51.6214, -3.9436, "SA"),
("Oxford", "Oxfordshire", 51.7520, -1.2577, "OX"),
("Cambridge", "Cambridgeshire", 52.2053, 0.1218, "CB"),
("Exeter", "Devon", 50.7184, -3.5339, "EX"),
("Bath", "Somerset", 51.3751, -2.3617, "BA"),
("Reading", "Berkshire", 51.4543, -0.9781, "RG"),
("Preston", "Lancashire", 53.7632, -2.7031, "PR"),
("Coventry", "West Midlands", 52.4068, -1.5197, "CV"),
("Hull", "East Yorkshire", 53.7676, -0.3274, "HU"),
("Stoke-on-Trent", "Staffordshire", 53.0027, -2.1794, "ST"),
("Wolverhampton", "West Midlands", 52.5870, -2.1288, "WV"),
("Plymouth", "Devon", 50.3755, -4.1427, "PL"),
("Derby", "Derbyshire", 52.9225, -1.4746, "DE"),
("Sunderland", "Tyne and Wear", 54.9069, -1.3830, "SR"),
("Southampton", "Hampshire", 50.9097, -1.4044, "SO"),
("Norwich", "Norfolk", 52.6309, 1.2974, "NR"),
("Bournemouth", "Dorset", 50.7192, -1.8808, "BH"),
("Middlesbrough", "North Yorkshire", 54.5742, -1.2350, "TS"),
("Blackpool", "Lancashire", 53.8175, -3.0357, "FY"),
("Bolton", "Greater Manchester", 53.5785, -2.4299, "BL"),
("Ipswich", "Suffolk", 52.0567, 1.1482, "IP"),
("Telford", "Shropshire", 52.6784, -2.4453, "TF"),
("Dundee", "Dundee", 56.4620, -2.9707, "DD"),
("Peterborough", "Cambridgeshire", 52.5695, -0.2405, "PE"),
("Huddersfield", "West Yorkshire", 53.6458, -1.7850, "HD"),
("Luton", "Bedfordshire", 51.8787, -0.4200, "LU"),
("Warrington", "Cheshire", 53.3900, -2.5970, "WA"),
("Southend-on-Sea", "Essex", 51.5459, 0.7077, "SS"),
("Swindon", "Wiltshire", 51.5557, -1.7797, "SN"),
("Slough", "Berkshire", 51.5105, -0.5950, "SL"),
("Watford", "Hertfordshire", 51.6565, -0.3903, "WD"),
("Carlisle", "Cumbria", 54.8952, -2.9335, "CA")
]
# Street name components for generating realistic street names
street_prefixes = ["High", "Main", "Church", "Park", "Mill", "Station", "London", "Victoria", "Queen", "King", "North", "South", "East", "West", "New", "Old", "Castle", "Bridge", "Green", "Market", "School", "Manor", "Abbey", "Priory", "Cathedral", "University", "College", "Hospital", "Railway", "Canal", "River", "Forest", "Wood", "Hill", "Mount", "Valley", "Meadow", "Field", "Farm", "Garden", "Orchard", "Vineyard", "Grange", "Lodge", "Court", "Hall", "House", "Cottage", "Barn", "Mill", "Windmill", "Watermill", "Forge", "Quarry", "Mine", "Pit", "Well", "Spring", "Brook", "Stream", "Lake", "Pond", "Pool", "Reservoir", "Bay", "Cove", "Beach", "Cliff", "Rock", "Stone", "Granite", "Marble", "Slate", "Clay", "Sand", "Gravel", "Chalk", "Flint", "Coal", "Iron", "Steel", "Copper", "Silver", "Gold", "Tin", "Lead", "Zinc", "Brass", "Bronze", "Pewter", "Nickel", "Cobalt", "Chromium", "Titanium", "Aluminium", "Silicon", "Carbon", "Oxygen", "Hydrogen", "Nitrogen", "Helium", "Neon", "Argon", "Krypton", "Xenon", "Radon"]
street_suffixes = ["Street", "Road", "Lane", "Avenue", "Drive", "Boulevard", "Way", "Place", "Square", "Court", "Terrace", "Close", "Crescent", "Gardens", "Grove", "Mews", "Alley", "Walk", "Path", "Trail", "Hill", "Rise", "View", "Heights", "Park", "Green", "Meadow", "Field", "Common", "Heath", "Moor", "Down", "Fell", "Pike", "Tor", "Crag", "Cliff", "Ridge", "Edge", "Top", "Bottom", "Side", "End", "Corner", "Junction", "Cross", "Gate", "Bridge", "Ford", "Ferry", "Wharf", "Quay", "Dock", "Harbor", "Port", "Bay", "Cove", "Beach", "Shore", "Bank", "Strand", "Esplanade", "Parade", "Promenade", "Embankment", "Causeway", "Viaduct", "Tunnel", "Passage", "Arcade", "Gallery", "Mall", "Market", "Bazaar", "Fair", "Exchange", "Mart", "Emporium", "Center", "Circle", "Oval", "Triangle", "Pentagon", "Hexagon", "Octagon", "Circus", "Ring", "Loop", "Bend", "Curve", "Turn", "Twist", "Spiral", "Coil", "Helix", "Maze", "Labyrinth"]
# Facility descriptions by category
category_descriptions = {
1: [ # Recycling Bins
"Public recycling point for paper, glass, plastic, and metal",
"Community recycling station with separate bins for different materials",
"Recycling center with facilities for household waste separation",
"Public access recycling bins for common household recyclables",
"Multi-material recycling point with clear instructions for proper sorting"
],
2: [ # e-Scooters
"Dockless e-scooter rental station with multiple vehicles available",
"E-scooter parking and charging zone for public use",
"Designated e-scooter pickup and drop-off point",
"E-scooter sharing station with app-based rental system",
"Electric scooter hub with maintenance and charging facilities"
],
3: [ # Bike Share Stations
"Public bicycle sharing station with multiple bikes available",
"Bike rental hub with secure docking stations",
"Community bike share point with regular and electric bicycles",
"Cycle hire station with self-service rental system",
"Bike sharing facility with maintenance and repair services"
],
4: [ # Public EV Charging Stations
"Electric vehicle charging point with multiple connectors",
"Fast-charging station for electric vehicles",
"Public EV charging facility with covered waiting area",
"Multi-vehicle electric charging hub with different power options",
"EV charging station with renewable energy source"
],
5: [ # Battery Recycling Points
"Dedicated collection point for used batteries of all sizes",
"Battery recycling bin with separate compartments for different types",
"Safe disposal facility for household and small electronics batteries",
"Battery collection point with educational information about recycling",
"Secure battery recycling station to prevent environmental contamination"
],
6: [ # Community Compost Bins
"Neighborhood composting facility for food and garden waste",
"Community compost bins with educational signage",
"Public composting station with separate sections for different stages",
"Shared compost facility managed by local volunteers",
"Urban composting hub turning food waste into valuable soil"
],
7: [ # Solar-Powered Benches
"Solar bench with USB charging ports and WiFi connectivity",
"Public seating with integrated solar panels and device charging",
"Smart bench powered by solar energy with digital information display",
"Solar-powered rest area with phone charging capabilities",
"Eco-friendly bench with solar panels and LED lighting"
],
8: [ # Green Roofs
"Building with extensive green roof system visible from public areas",
"Accessible green roof garden with native plant species",
"Public building showcasing sustainable rooftop vegetation",
"Green roof installation with educational tours available",
"Biodiverse roof garden with insect habitats and rainwater collection"
],
9: [ # Public Water Refill Stations
"Free water refill station to reduce plastic bottle usage",
"Public drinking fountain with bottle filling capability",
"Water refill point with filtered water options",
"Accessible water station encouraging reusable bottles",
"Community water dispenser with usage counter display"
],
10: [ # Waste Oil Collection Points
"Cooking oil recycling point for residential use",
"Used oil collection facility with secure containers",
"Waste oil drop-off point for conversion to biodiesel",
"Community oil recycling station with spill prevention measures",
"Cooking oil collection facility with educational information"
],
11: [ # Book Swap Stations
"Community book exchange point with weatherproof shelving",
"Public book sharing library in repurposed phone box",
"Free book swap station encouraging reading and reuse",
"Neighborhood book exchange with rotating collection",
"Little free library with take-one-leave-one system"
],
12: [ # Pollinator Gardens
"Public garden designed to support bees and butterflies",
"Pollinator-friendly planting area with native flowering species",
"Community garden dedicated to supporting local insect populations",
"Bee-friendly garden with educational signage about pollinators",
"Urban wildflower meadow supporting biodiversity"
],
13: [ # E-Waste Collection Bins
"Secure collection point for electronic waste and small appliances",
"E-waste recycling bin for phones, computers, and small electronics",
"Electronic waste drop-off point with data security assurance",
"Community e-waste collection facility with regular collection schedule",
"Dedicated bin for responsible disposal of electronic items"
],
14: [ # Clothing Donation Bins
"Textile recycling point for clothes and household fabrics",
"Clothing donation bin supporting local charities",
"Secure collection point for reusable clothing and textiles",
"Community clothing recycling bin with regular collection",
"Textile donation point preventing landfill waste"
],
15: [ # Community Tool Libraries
"Tool lending library for community use and sharing",
"Shared equipment facility reducing need for individual ownership",
"Community resource center for borrowing tools and equipment",
"Tool sharing hub with membership system and workshops",
"Public tool library with wide range of equipment available"
],
16: [ # Urban Farms
"Community-run urban farm providing local produce",
"City farming project with volunteer opportunities",
"Urban agriculture site with educational programs",
"Local food growing initiative in repurposed urban space",
"Community garden with vegetable plots and fruit trees"
],
17: [ # Rainwater Harvesting Systems
"Public demonstration of rainwater collection for irrigation",
"Rainwater harvesting system with educational displays",
"Community rainwater collection facility for shared gardens",
"Visible rainwater storage and filtration system",
"Urban water conservation project with storage tanks"
]
}
# Status comments by category
status_comments = {
1: [ # Recycling Bins
"Recently emptied and cleaned",
"Some bins are nearly full",
"All bins in good condition",
"Paper bin is currently full",
"New signage installed to improve sorting"
],
2: [ # e-Scooters
"All scooters fully charged",
"Three scooters currently available",
"Maintenance scheduled for next week",
"New scooters added to this location",
"High usage area, scooters frequently unavailable"
],
3: [ # Bike Share Stations
"All docking stations operational",
"Five bikes currently available",
"Some bikes need maintenance",
"New electric bikes added",
"Popular station with high turnover"
],
4: [ # Public EV Charging Stations
"All charging points operational",
"Fast charger currently under repair",
"Peak usage during business hours",
"New charging point added last month",
"Payment system recently upgraded"
],
5: [ # Battery Recycling Points
"Collection bin recently emptied",
"Secure container in good condition",
"New signage explaining battery types",
"High usage from local businesses",
"Additional capacity added"
],
6: [ # Community Compost Bins
"Compost ready for collection",
"Needs more brown material",
"Recently turned and aerated",
"New bins added to increase capacity",
"Volunteer day scheduled for maintenance"
],
7: [ # Solar-Powered Benches
"All charging ports working",
"Solar panels recently cleaned",
"WiFi currently unavailable",
"LED lights need replacement",
"High usage during lunch hours"
],
8: [ # Green Roofs
"Plants thriving after recent rain",
"Maintenance scheduled next month",
"New species added to increase biodiversity",
"Irrigation system working well",
"Open for public tours on weekends"
],
9: [ # Public Water Refill Stations
"Water quality tested weekly",
"Fountain cleaned daily",
"Bottle filler counter shows high usage",
"New filter installed recently",
"Popular during summer months"
],
10: [ # Waste Oil Collection Points
"Container recently emptied",
"Secure lid in good condition",
"New funnel system installed",
"Collection schedule posted",
"Area kept clean and tidy"
],
11: [ # Book Swap Stations
"Good selection currently available",
"Children's books needed",
"Recently reorganized by volunteers",
"Weatherproof cover working well",
"High turnover of popular titles"
],
12: [ # Pollinator Gardens
"Plants in full bloom",
"Many bees and butterflies observed",
"New native species planted",
"Volunteer day for maintenance scheduled",
"Educational tours available"
],
13: [ # E-Waste Collection Bins
"Bin recently emptied",
"Secure deposit system working",
"Collection schedule posted",
"New items accepted now include small appliances",
"Data destruction guaranteed"
],
14: [ # Clothing Donation Bins
"Bin recently emptied",
"Clean and well-maintained",
"High quality donations appreciated",
"Winter clothing especially needed",
"Please bag items before donating"
],
15: [ # Community Tool Libraries
"New inventory system implemented",
"Popular tools often unavailable on weekends",
"Tool maintenance workshop scheduled",
"New donations recently added to collection",
"Extended hours during summer"
],
16: [ # Urban Farms
"Seasonal produce currently available",
"Volunteer opportunities posted",
"Educational workshops on weekends",
"New growing area being developed",
"Composting system recently expanded"
],
17: [ # Rainwater Harvesting Systems
"System working efficiently after recent rainfall",
"Water quality monitoring in place",
"Educational tours available by appointment",
"System capacity recently expanded",
"Used for irrigation of nearby community garden"
]
}
# Generate a realistic UK postcode based on area code
def generate_postcode(area_code):
# Format: Area + District + Space + Sector + Unit
# e.g., M1 1AA or SW1A 1AA
district = random.randint(1, 99)
sector = random.randint(1, 9)
unit = ''.join(random.choices('ABCDEFGHJKLMNPQRSTUVWXYZ', k=2)) # Excluding I and O as they're not used
if len(area_code) == 1:
return f"{area_code}{district} {sector}{unit}"
else:
return f"{area_code}{district} {sector}{unit}"
# Generate a realistic street name
def generate_street_name():
prefix = random.choice(street_prefixes)
suffix = random.choice(street_suffixes)
return f"{prefix} {suffix}"
# Generate a realistic house number
def generate_house_number():
# 80% chance of a simple number, 20% chance of a letter suffix or unit
if random.random() < 0.8:
return str(random.randint(1, 200))
else:
options = [
f"{random.randint(1, 200)}{random.choice('ABCDEFG')}", # e.g., 42A
f"Unit {random.randint(1, 20)}",
f"Flat {random.randint(1, 50)}",
f"Suite {random.randint(1, 10)}"
]
return random.choice(options)
# Add small random variation to coordinates to avoid facilities at exact same location
def vary_coordinates(lat, lng):
# Add variation of up to ~500 meters
lat_variation = random.uniform(-0.004, 0.004)
lng_variation = random.uniform(-0.006, 0.006)
return lat + lat_variation, lng + lng_variation
# Generate facility title based on category and location
def generate_title(category_name, location_name, street_name):
templates = [
f"{location_name} {category_name}",
f"{category_name} at {street_name}",
f"{street_name} {category_name}",
f"Community {category_name} {location_name}",
f"{location_name} Central {category_name}",
f"{location_name} {street_name} {category_name}"
]
return random.choice(templates)
# Create a log file to track progress
log_file = open("facility_generation_log.txt", "w")
log_file.write(f"Starting facility generation at {datetime.now()}\n")
log_file.write(f"Target: 1000 new facilities\n\n")
# Create a CSV file to store all generated facilities for reference
csv_file = open("generated_facilities.csv", "w", newline='')
csv_writer = csv.writer(csv_file)
csv_writer.writerow(["ID", "Title", "Category", "Description", "Address", "Postcode", "Latitude", "Longitude", "Contributor"])
# Prepare for batch insertion to improve performance
facilities_to_insert = []
status_comments_to_insert = []
# Track unique titles to avoid duplicates
existing_titles = set()
cursor.execute("SELECT title FROM ecoFacilities")
for row in cursor.fetchall():
existing_titles.add(row[0])
# Generate 1000 facilities
num_facilities = 1000
facilities_created = 0
log_file.write("Generating facilities...\n")
while facilities_created < num_facilities:
# Select a random location
location = random.choice(uk_locations)
location_name, county, base_lat, base_lng, postcode_area = location
# Generate 5-25 facilities per location to create clusters
facilities_per_location = min(random.randint(5, 25), num_facilities - facilities_created)
for _ in range(facilities_per_location):
# Select a random category
category_id = random.choice(list(categories.keys()))
category_name = categories[category_id]
# Generate address components
street_name = generate_street_name()
house_number = generate_house_number()
lat, lng = vary_coordinates(base_lat, base_lng)
postcode = generate_postcode(postcode_area)
# Generate title
title_base = generate_title(category_name, location_name, street_name)
title = title_base
# Ensure title is unique by adding a suffix if needed
suffix = 2
while title in existing_titles:
title = f"{title_base} {suffix}"
suffix += 1
existing_titles.add(title)
# Select description
description = random.choice(category_descriptions[category_id])
# Select contributor
contributor_id = random.choice(user_ids)
# Add to batch for insertion
facilities_to_insert.append((
title,
category_id,
description,
house_number,
street_name,
county,
location_name,
postcode,
lng,
lat,
contributor_id
))
# Log progress periodically
facilities_created += 1
if facilities_created % 100 == 0:
log_message = f"Generated {facilities_created} facilities so far..."
print(log_message)
log_file.write(log_message + "\n")
if facilities_created >= num_facilities:
break
# Insert facilities in batches for better performance
log_file.write("\nInserting facilities into database...\n")
print("Inserting facilities into database...")
batch_size = 50
for i in range(0, len(facilities_to_insert), batch_size):
batch = facilities_to_insert[i:i+batch_size]
cursor.executemany("""
INSERT INTO ecoFacilities
(title, category, description, houseNumber, streetName, county, town, postcode, lng, lat, contributor)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", batch)
# Get the IDs of the inserted facilities
cursor.execute("SELECT last_insert_rowid()")
last_id = cursor.fetchone()[0]
first_id_in_batch = last_id - len(batch) + 1
# Generate status comments for each facility
for j, facility in enumerate(batch):
facility_id = first_id_in_batch + j
category_id = facility[1] # Category ID is the second element
# Write to CSV for reference
csv_writer.writerow([
facility_id,
facility[0], # title
categories[category_id], # category name
facility[2], # description
f"{facility[3]} {facility[4]}, {facility[6]}, {facility[5]}", # address
facility[7], # postcode
facility[9], # lat
facility[8], # lng
facility[10] # contributor
])
# Decide how many status comments to add (0-3)
num_comments = random.choices([0, 1, 2, 3], weights=[30, 40, 20, 10])[0]
if num_comments > 0:
# Get relevant comments for this category
relevant_comments = status_comments.get(category_id, status_comments[1]) # Default to recycling bin comments
# Select random comments without repetition
selected_comments = random.sample(relevant_comments, min(num_comments, len(relevant_comments)))
# Add to batch for insertion
for comment in selected_comments:
status_comments_to_insert.append((facility_id, comment))
# Commit after each batch
conn.commit()
log_message = f"Inserted batch {i//batch_size + 1}/{(len(facilities_to_insert)-1)//batch_size + 1}"
print(log_message)
log_file.write(log_message + "\n")
# Insert status comments in batches
if status_comments_to_insert:
log_file.write("\nInserting status comments...\n")
print("Inserting status comments...")
for i in range(0, len(status_comments_to_insert), batch_size):
batch = status_comments_to_insert[i:i+batch_size]
cursor.executemany("""
INSERT INTO ecoFacilityStatus (facilityId, statusComment)
VALUES (?, ?)
""", batch)
conn.commit()
log_message = f"Inserted comment batch {i//batch_size + 1}/{(len(status_comments_to_insert)-1)//batch_size + 1}"
print(log_message)
log_file.write(log_message + "\n")
# Get final counts
cursor.execute("SELECT COUNT(*) FROM ecoFacilities")
total_facilities = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM ecoFacilityStatus")
total_comments = cursor.fetchone()[0]
# Log completion
completion_message = f"\nGeneration complete at {datetime.now()}"
print(completion_message)
log_file.write(completion_message + "\n")
summary = f"Total facilities in database: {total_facilities}\n"
summary += f"Total status comments in database: {total_comments}\n"
summary += f"Generated facilities saved to generated_facilities.csv for reference"
print(summary)
log_file.write(summary)
# Close connections
log_file.close()
csv_file.close()
conn.commit()
conn.close()
print("\nSuccessfully added 1000 new ecological facilities to the database.")
print("A detailed log and CSV export have been created for reference.")

View File

@@ -1,79 +0,0 @@
<?php
// Connect to the SQLite database
$db = new PDO('sqlite:Databases/ecobuddy.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// List of real first names for usernames
$firstNames = [
'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Charles',
'Christopher', 'Daniel', 'Matthew', 'Anthony', 'Mark', 'Donald', 'Steven', 'Paul', 'Andrew', 'Joshua',
'Kenneth', 'Kevin', 'Brian', 'George', 'Timothy', 'Ronald', 'Edward', 'Jason', 'Jeffrey', 'Ryan',
'Jacob', 'Gary', 'Nicholas', 'Eric', 'Jonathan', 'Stephen', 'Larry', 'Justin', 'Scott', 'Brandon',
'Benjamin', 'Samuel', 'Gregory', 'Alexander', 'Frank', 'Patrick', 'Raymond', 'Jack', 'Dennis', 'Jerry',
'Tyler', 'Aaron', 'Jose', 'Adam', 'Nathan', 'Henry', 'Douglas', 'Zachary', 'Peter', 'Kyle',
'Ethan', 'Walter', 'Noah', 'Jeremy', 'Christian', 'Keith', 'Roger', 'Terry', 'Gerald', 'Harold',
'Sean', 'Austin', 'Carl', 'Arthur', 'Lawrence', 'Dylan', 'Jesse', 'Jordan', 'Bryan', 'Billy',
'Joe', 'Bruce', 'Gabriel', 'Logan', 'Albert', 'Willie', 'Alan', 'Juan', 'Wayne', 'Elijah',
'Randy', 'Roy', 'Vincent', 'Ralph', 'Eugene', 'Russell', 'Bobby', 'Mason', 'Philip', 'Louis'
];
// List of common words for password generation
$words = [
'Apple', 'Banana', 'Cherry', 'Dragon', 'Eagle', 'Forest', 'Garden', 'Harbor', 'Island', 'Jungle',
'Kingdom', 'Lemon', 'Mountain', 'Nature', 'Ocean', 'Planet', 'Queen', 'River', 'Summer', 'Tiger',
'Universe', 'Volcano', 'Winter', 'Yellow', 'Zebra', 'Castle', 'Diamond', 'Emerald', 'Flower', 'Galaxy',
'Horizon', 'Iceberg', 'Journey', 'Knight', 'Legend', 'Meadow', 'Nebula', 'Oasis', 'Palace', 'Quasar',
'Rainbow', 'Sapphire', 'Thunder', 'Unicorn', 'Victory', 'Whisper', 'Xylophone', 'Yacht', 'Zephyr', 'Autumn',
'Breeze', 'Cascade', 'Dolphin', 'Eclipse', 'Falcon', 'Glacier', 'Harmony', 'Infinity', 'Jasmine', 'Kaleidoscope',
'Lighthouse', 'Mirage', 'Nightfall', 'Orchard', 'Phoenix', 'Quicksilver', 'Radiance', 'Serenity', 'Twilight', 'Umbrella'
];
// Check if we already have users with these names
$existingUsers = [];
$stmt = $db->query("SELECT username FROM ecoUser");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$existingUsers[] = $row['username'];
}
// Prepare the SQL statement for inserting users
$insertStmt = $db->prepare("INSERT INTO ecoUser (username, password, userType) VALUES (?, ?, ?)");
// Create a file to store usernames and passwords
$file = fopen("user_credentials.txt", "w");
fwrite($file, "Username,Password\n");
// Generate 50 users
$usersAdded = 0;
$usedNames = [];
shuffle($firstNames);
foreach ($firstNames as $name) {
if ($usersAdded >= 50) break;
// Skip if the name is already in the database
if (in_array($name, $existingUsers) || in_array($name, $usedNames)) {
continue;
}
// Generate password in format (Word)(Word)(Word)(Digit)
$password = $words[array_rand($words)] . $words[array_rand($words)] . $words[array_rand($words)] . rand(0, 9);
// Hash the password using SHA-256
$hashedPassword = hash('sha256', $password);
// Insert the user into the database (userType 2 is for standard users)
$insertStmt->execute([$name, $hashedPassword, 2]);
// Write the credentials to the file
fwrite($file, "$name,$password\n");
$usedNames[] = $name;
$usersAdded++;
echo "Added user: $name\n";
}
fclose($file);
echo "\nSuccessfully added $usersAdded users to the database.\n";
echo "Usernames and passwords have been saved to user_credentials.txt\n";

View File

@@ -1,79 +0,0 @@
import sqlite3
import random
import hashlib
import os
# Connect to the SQLite database
conn = sqlite3.connect('Databases/ecobuddy.sqlite')
cursor = conn.cursor()
# List of real first names for usernames
first_names = [
'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Charles',
'Christopher', 'Daniel', 'Matthew', 'Anthony', 'Mark', 'Donald', 'Steven', 'Paul', 'Andrew', 'Joshua',
'Kenneth', 'Kevin', 'Brian', 'George', 'Timothy', 'Ronald', 'Edward', 'Jason', 'Jeffrey', 'Ryan',
'Jacob', 'Gary', 'Nicholas', 'Eric', 'Jonathan', 'Stephen', 'Larry', 'Justin', 'Scott', 'Brandon',
'Benjamin', 'Samuel', 'Gregory', 'Alexander', 'Frank', 'Patrick', 'Raymond', 'Jack', 'Dennis', 'Jerry',
'Tyler', 'Aaron', 'Jose', 'Adam', 'Nathan', 'Henry', 'Douglas', 'Zachary', 'Peter', 'Kyle',
'Ethan', 'Walter', 'Noah', 'Jeremy', 'Christian', 'Keith', 'Roger', 'Terry', 'Gerald', 'Harold',
'Sean', 'Austin', 'Carl', 'Arthur', 'Lawrence', 'Dylan', 'Jesse', 'Jordan', 'Bryan', 'Billy',
'Joe', 'Bruce', 'Gabriel', 'Logan', 'Albert', 'Willie', 'Alan', 'Juan', 'Wayne', 'Elijah',
'Randy', 'Roy', 'Vincent', 'Ralph', 'Eugene', 'Russell', 'Bobby', 'Mason', 'Philip', 'Louis'
]
# List of common words for password generation
words = [
'Apple', 'Banana', 'Cherry', 'Dragon', 'Eagle', 'Forest', 'Garden', 'Harbor', 'Island', 'Jungle',
'Kingdom', 'Lemon', 'Mountain', 'Nature', 'Ocean', 'Planet', 'Queen', 'River', 'Summer', 'Tiger',
'Universe', 'Volcano', 'Winter', 'Yellow', 'Zebra', 'Castle', 'Diamond', 'Emerald', 'Flower', 'Galaxy',
'Horizon', 'Iceberg', 'Journey', 'Knight', 'Legend', 'Meadow', 'Nebula', 'Oasis', 'Palace', 'Quasar',
'Rainbow', 'Sapphire', 'Thunder', 'Unicorn', 'Victory', 'Whisper', 'Xylophone', 'Yacht', 'Zephyr', 'Autumn',
'Breeze', 'Cascade', 'Dolphin', 'Eclipse', 'Falcon', 'Glacier', 'Harmony', 'Infinity', 'Jasmine', 'Kaleidoscope',
'Lighthouse', 'Mirage', 'Nightfall', 'Orchard', 'Phoenix', 'Quicksilver', 'Radiance', 'Serenity', 'Twilight', 'Umbrella'
]
# Check if we already have users with these names
cursor.execute("SELECT username FROM ecoUser")
existing_users = [row[0] for row in cursor.fetchall()]
# Create a file to store usernames and passwords
with open("user_credentials.txt", "w") as file:
file.write("Username,Password\n")
# Generate 50 users
users_added = 0
used_names = []
random.shuffle(first_names)
for name in first_names:
if users_added >= 50:
break
# Skip if the name is already in the database
if name in existing_users or name in used_names:
continue
# Generate password in format (Word)(Word)(Word)(Digit)
password = random.choice(words) + random.choice(words) + random.choice(words) + str(random.randint(0, 9))
# Hash the password using SHA-256
hashed_password = hashlib.sha256(password.encode()).hexdigest()
# Insert the user into the database (userType 2 is for standard users)
cursor.execute("INSERT INTO ecoUser (username, password, userType) VALUES (?, ?, ?)",
(name, hashed_password, 2))
# Write the credentials to the file
file.write(f"{name},{password}\n")
used_names.append(name)
users_added += 1
print(f"Added user: {name}")
# Commit the changes and close the connection
conn.commit()
conn.close()
print(f"\nSuccessfully added {users_added} users to the database.")
print("Usernames and passwords have been saved to user_credentials.txt")

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,9 @@
require_once('UserDataSet.php');
/**
* Authentication service for handling JWT-based authentication
* Backend Authentication service for handling JWT authentication
* https://jwt.io/introduction
* This cost me blood, sweat and tears, mostly tears.
*/
class AuthService {
private string $secretKey;
@@ -14,7 +16,7 @@ class AuthService {
* @throws Exception if OpenSSL extension is not loaded
*/
public function __construct() {
// Load environment variables from .env file
// Load environment variables from .env file (:D more configuration needs to be added to .env, but scope creep already huge)
$envFile = __DIR__ . '/../.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
@@ -37,14 +39,14 @@ class AuthService {
$this->secretKey = getenv('JWT_SECRET_KEY') ?: 'your-256-bit-secret';
$this->tokenExpiry = (int)(getenv('JWT_TOKEN_EXPIRY') ?: 3600);
// Verify OpenSSL extension is available
// Verify OpenSSL extension is available. This should be on by default regardless, but just in case.
if (!extension_loaded('openssl')) {
throw new Exception('OpenSSL extension is required for JWT');
}
}
/**
* Generates a JWT token for a user
* Generates a JWT token
* @param array $userData User information to include in token
* @return string The generated JWT token
*/
@@ -52,6 +54,7 @@ class AuthService {
$issuedAt = time();
$expire = $issuedAt + $this->tokenExpiry;
// Create payload with user data
$payload = [
'iat' => $issuedAt,
'exp' => $expire,
@@ -101,7 +104,7 @@ class AuthService {
$signature = hash_hmac('sha256', "$header.$payload", $this->secretKey, true);
$signature = $this->base64UrlEncode($signature);
return "$header.$payload.$signature";
return "$header.$payload.$signature"; //Wooooooo!!! JWT is a thing!
}
/**

View File

@@ -1,15 +1,11 @@
<?php
/**
* Represents a facility in the EcoBuddy system
* Represents a singular facility
*
* This class serves as a data model for facilities, encapsulating all
* the properties and behaviours of a single facility. It follows the
* Data Transfer Object (DTO) pattern that I learned about in my
* software architecture module.
* Data model for facilities, encapsulating all
* properties and behaviours of a single facility.
*
* Each facility has location data, descriptive information, and metadata
* about who contributed it. This class provides a clean interface for
* accessing this data throughout the application.
* Each facility has location data, descriptive info, and metadata.
*/
class FacilityData {
/**
@@ -77,10 +73,6 @@ class FacilityData {
/**
* Gets the facility's title
*
* The title is the primary name or label for the facility that
* is displayed to users in the interface.
*
* @return string The facility title
*/
public function getTitle() {
@@ -89,10 +81,6 @@ class FacilityData {
/**
* Gets the facility's category
*
* The category helps classify facilities by type, such as
* recycling centre, community garden, etc.
*
* @return string The facility category
*/
public function getCategory() {
@@ -101,10 +89,6 @@ class FacilityData {
/**
* Gets the facility's current status
*
* The status indicates whether the facility is operational,
* under maintenance, closed, etc.
*
* @return string The facility status
*/
public function getStatus() {
@@ -113,10 +97,6 @@ class FacilityData {
/**
* Gets the facility's description
*
* The description provides detailed information about the facility,
* its purpose, services offered, etc.
*
* @return string The facility description
*/
public function getDescription() {
@@ -125,9 +105,6 @@ class FacilityData {
/**
* Gets the facility's house/building number
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The house/building number
*/
public function getHouseNumber() {
@@ -136,9 +113,6 @@ class FacilityData {
/**
* Gets the facility's street name
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The street name
*/
public function getStreetName() {
@@ -147,9 +121,6 @@ class FacilityData {
/**
* Gets the facility's county
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The county
*/
public function getCounty() {
@@ -158,9 +129,6 @@ class FacilityData {
/**
* Gets the facility's town or city
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The town or city
*/
public function getTown() {
@@ -169,10 +137,6 @@ class FacilityData {
/**
* Gets the facility's postcode
*
* This is part of the facility's address and helps locate it physically.
* It's also useful for searching facilities by location.
*
* @return string The postcode
*/
public function getPostcode() {
@@ -181,10 +145,6 @@ class FacilityData {
/**
* Gets the facility's longitude coordinate
*
* This is used for displaying the facility on a map and
* for calculating distances between facilities.
*
* @return float The longitude coordinate
*/
public function getLng() {
@@ -193,10 +153,6 @@ class FacilityData {
/**
* Gets the facility's latitude coordinate
*
* This is used for displaying the facility on a map and
* for calculating distances between facilities.
*
* @return float The latitude coordinate
*/
public function getLat() {
@@ -205,10 +161,6 @@ class FacilityData {
/**
* Gets the username of the facility's contributor
*
* This tracks who added the facility to the system,
* which is useful for auditing and attribution.
*
* @return string The contributor's username
*/
public function getContributor() {

View File

@@ -1,57 +0,0 @@
<?php
require_once('FacilityDataSet.php');
class Paginator {
protected $_pages, $_totalPages, $_rowLimit, $_pageMatrix, $_rowCount;
public function __construct($rowLimit, $dataset) {
$this->_rowLimit = $rowLimit;
$this->_totalPages = $this->calculateTotalPages($dataset['count']);
$this->_rowCount = $dataset['count'];
$this->_pages = $dataset['dataset'];
$this->_pageMatrix = $this->Paginate();
}
public function getTotalPages() {
return $this->_totalPages;
}
private function calculateTotalPages(int $count): int {
return $count > 0 ? ceil($count / $this->_rowLimit) : 0;
}
public function Paginate(): array {
$pageMatrix = [];
for ($i = 0; $i < $this->_totalPages; $i++) {
$page = [];
$start = $i * $this->_rowLimit;
$end = min($start + $this->_rowLimit, $this->_rowCount); // Ensure within bounds
for ($j = $start; $j < $end; $j++) {
$page[] = $this->_pages[$j];
}
$pageMatrix[$i] = $page;
}
return $pageMatrix;
}
public function getPageFromUri(): int {
// Retrieve 'page' parameter and default to 0 if missing or invalid
return filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [
'options' => ['default' => 0, 'min_range' => 0] // Default to 1 if invalid or missing
]);
}
public function getPage(int $pageNumber): array {
if ($pageNumber < 0 || $pageNumber >= $this->_totalPages) {
return []; // Return an empty array if the page number is invalid
}
return $this->_pageMatrix[$pageNumber];
}
public function countPageResults(int $pageNumber): int {
if ($pageNumber < 0 || $pageNumber >= $this->_totalPages) {
return 0; // Return 0 if the page number is invalid
}
return count($this->_pageMatrix[$pageNumber]);
}
}

View File

@@ -70,16 +70,6 @@ class User {
}
}
/**
* Sets the user's access level
*
* @param int $level The access level to set (admin = 1, regular user = 2)
* @return void
*/
private function setAccessLevel($level) {
$this->_accessLevel = $level;
}
/**
* Gets the user's access level
*
@@ -128,33 +118,6 @@ class User {
return false;
}
}
/**
* Logs the user out
*
* Resets all user properties to their default values.
* Note: This doesn't invalidate the JWT token - handled client-side
* by removing the token from storage.
*
* @return void
*/
public function logout() {
// Reset user properties
$this->_loggedIn = false;
$this->_username = "None";
$this->_userId = "0";
$this->_accessLevel = null;
}
/**
* Checks if the user is currently logged in
*
* @return bool True if the user is logged in, false otherwise
*/
public function isLoggedIn(): bool
{
return $this->_loggedIn;
}
/**
* Static method to check if a request is authenticated

View File

@@ -21,11 +21,17 @@ require('template/header.phtml')
<div class="card-header bg-light py-3">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<h5 class="mb-0 fw-bold text-primary">
<i class="bi bi-geo-alt-fill me-2 text-success"></i>Facilities
</h5>
<!-- Badge showing the number of facilities -->
<span class="badge bg-success rounded-pill ms-2" id="facilityCount"></span>
<!-- Search and filter controls -->
<div class="d-flex flex-column flex-lg-row search-controls mx-auto">
<form class="d-flex flex-column flex-lg-row gap-2 w-100" role="search" action="" method="POST">
<div class="input-group flex-grow-1">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-search text-success"></i>
</span>
<input class="form-control border-start-0" id="searchInput" type="search" name="filter" placeholder="Search..." aria-label="Search">
</div>
</form>
</div>
</div>
<!-- Admin-only buttons -->
<div id="adminButtons" style="display: none;">
@@ -99,33 +105,6 @@ require('template/header.phtml')
regularUserView.style.display = isAdmin ? 'none' : 'block';
}
// Update table headers based on user role :DDD (it just shows the ID column for admins...)
if (tableHeaderRow) {
if (isAdmin) {
// Admin view - show all columns and bigger management actions
tableHeaderRow.innerHTML = `
<th class="fw-semibold" style="width: 40px;">ID</th>
<th class="fw-semibold" style="width: 15%;">Title</th>
<th class="fw-semibold text-center" style="width: 10%;">Category</th>
<th class="fw-semibold" style="width: 25%;">Description</th>
<th class="fw-semibold" style="width: 20%;">Address</th>
<th class="fw-semibold text-center" style="width: 12%;">Coordinates</th>
<th class="fw-semibold text-center" style="width: 8%;">Contributor</th>
<th class="fw-semibold text-center" style="width: 10%;">Actions</th>
`;
} else {
// Regular user view - hide ID column and make management actions smaller
tableHeaderRow.innerHTML = `
<th class="fw-semibold" style="width: 17%;">Title</th>
<th class="fw-semibold text-center" style="width: 11%;">Category</th>
<th class="fw-semibold" style="width: 27%;">Description</th>
<th class="fw-semibold" style="width: 20%;">Address</th>
<th class="fw-semibold text-center" style="width: 12%;">Coordinates</th>
<th class="fw-semibold text-center" style="width: 8%;">Contributor</th>
<th class="fw-semibold text-center" style="width: 5%;">Actions</th>
`;
}
}
}
// Update UI when the page loads
@@ -139,28 +118,4 @@ require('template/header.phtml')
});
</script>
<!-- Script to update the facility count badge -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Update facility count badge based on data in sessionStorage
const updateFacilityCount = () => {
const facilityData = JSON.parse(sessionStorage.getItem('facilityData') || '[]');
const countBadge = document.getElementById('facilityCount');
if (countBadge) {
countBadge.textContent = `${facilityData.length} facilities`;
}
};
// Initial count update when the page loads
updateFacilityCount();
// Listen for changes in facility data to update the count
window.addEventListener('storage', function(e) {
if (e.key === 'facilityData') {
updateFacilityCount();
}
});
});
</script>
<?php require('template/footer.phtml');?>

195
Views/map.phtml Normal file
View File

@@ -0,0 +1,195 @@
<?php require_once('template/header.phtml') ?>
<style>
#mapOverlay {
transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
opacity: 1;
visibility: visible;
pointer-events: auto;
}
#mapOverlay.hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
/* Custom styles for facility popups */
.facility-popup .leaflet-popup-content-wrapper {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.facility-popup .leaflet-popup-content {
margin: 0;
min-width: 250px;
}
.facility-details {
max-height: 300px;
overflow-y: auto;
}
.facility-details::-webkit-scrollbar {
width: 6px;
}
.facility-details::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.facility-details::-webkit-scrollbar-thumb {
background: #198754;
border-radius: 3px;
}
/* Comment form styles */
.comment-form {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #dee2e6;
}
.comment-form textarea {
resize: vertical;
min-height: 60px;
}
/* Facility list container styles */
.facility-list-container {
height: calc(100vh - 400px); /* Adjust based on your layout */
display: flex;
flex-direction: column;
}
.facility-list {
overflow-y: auto;
flex-grow: 1;
margin-bottom: 0;
}
.facility-list::-webkit-scrollbar {
width: 6px;
}
.facility-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.facility-list::-webkit-scrollbar-thumb {
background: #198754;
border-radius: 3px;
}
</style>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="alert alert-warning">
Notice: Facility locations are currently limited to UK Cities.
</div>
</div>
</div>
</div>
<div class="container-fluid pt-0 py-4">
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-md-4 col-lg-3">
<!-- Postcode and radius controls -->
<div class="mb-4">
<h5 class="mb-3">Location Settings</h5>
<form id="postcodeForm" class="mb-3">
<div class="mb-3">
<label for="postcode" class="form-label">Enter Postcode</label>
<div class="input-group">
<span class="input-group-text bg-light">
<i class="bi bi-geo-alt text-success"></i>
</span>
<input type="text" class="form-control" id="postcode" name="postcode"
placeholder="e.g. M1 5GD" required>
<button class="btn btn-success" type="submit">
<i class="bi bi-crosshair"></i>
</button>
</div>
</div>
<div class="mb-3">
<label for="radius" class="form-label">Search Radius (miles)</label>
<select class="form-select" id="radius" name="radius">
<option value="1">1 mile</option>
<option value="5">5 miles</option>
<option value="10" selected>10 miles</option>
<option value="25">25 miles</option>
</select>
</div>
</form>
<!-- Facility list search and container -->
<div class="facility-list-container">
<div class="input-group mb-3">
<span class="input-group-text bg-light">
<i class="bi bi-search text-success"></i>
</span>
<input type="text" class="form-control" id="facilitySearch"
placeholder="Search facilities...">
</div>
<div id="facilityList" class="list-group list-group-flush facility-list"></div>
</div>
</div>
</div>
<div class="col-md-8 col-lg-9">
<!-- Map container with blur overlay -->
<div class="position-relative" style="height: 700px;">
<div id="map" style="height: 100%; width: 100%; z-index: 1;"></div>
<div id="mapOverlay" class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
style="backdrop-filter: blur(8px); z-index: 2; background: rgba(255,255,255,0.5);">
<div class="text-center">
<i class="bi bi-geo-alt text-success" style="font-size: 3rem;"></i>
<h4 class="mt-3">Enter a Postcode</h4>
<p class="text-muted mb-0">Please enter a postcode to view facilities on the map</p>
<p class="text-muted mt-0">or use the search button to find facilities near you</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Leaflet CSS and JS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
<!-- Add our map handler -->
<script src="/public/js/mapHandler.js"></script>
<script>
// Add facility search functionality
document.addEventListener('DOMContentLoaded', function() {
const facilitySearch = document.getElementById('facilitySearch');
const facilityList = document.getElementById('facilityList');
if (facilitySearch && facilityList) {
facilitySearch.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const items = facilityList.getElementsByClassName('list-group-item');
Array.from(items).forEach(item => {
const text = item.textContent.toLowerCase();
item.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
}
});
</script>
<?php require('template/statusModal.phtml') ?>
<?php require_once('template/footer.phtml') ?>

View File

@@ -10,7 +10,7 @@
</div>
<div class="modal-body p-4">
<!-- Create facility form -->
<form id="createFacilityForm">
<form id="createForm">
<!-- Form fields -->
<div class="mb-3">
<label for="createTitle" class="form-label">Facility Name</label>
@@ -41,53 +41,49 @@
<div class="row">
<div class="col-md-6 mb-3">
<label for="createLatitude" class="form-label">Latitude</label>
<input type="number" step="any" class="form-control" id="createLatitude" name="latitude" required>
<input type="number" step="any" class="form-control" id="createLatitude" name="lat" required>
</div>
<div class="col-md-6 mb-3">
<label for="createLongitude" class="form-label">Longitude</label>
<input type="number" step="any" class="form-control" id="createLongitude" name="longitude" required>
<input type="number" step="any" class="form-control" id="createLongitude" name="lng" required>
</div>
</div>
<div class="mb-3">
<label for="createAddress" class="form-label">Address</label>
<input type="text" class="form-control" id="createAddress" name="address" required>
<label for="createHouseNumber" class="form-label">House Number/Name</label>
<input type="text" class="form-control" id="createHouseNumber" name="houseNumber" required>
</div>
<div class="mb-3">
<label for="createContact" class="form-label">Contact Information</label>
<input type="text" class="form-control" id="createContact" name="contact">
<label for="createStreetName" class="form-label">Street Name</label>
<input type="text" class="form-control" id="createStreetName" name="streetName" required>
</div>
<div class="mb-3">
<label for="createWebsite" class="form-label">Website</label>
<input type="url" class="form-control" id="createWebsite" name="website" placeholder="https://">
<label for="createTown" class="form-label">Town/City</label>
<input type="text" class="form-control" id="createTown" name="town" required>
</div>
<div class="mb-3">
<label for="createHours" class="form-label">Operating Hours</label>
<input type="text" class="form-control" id="createHours" name="hours" placeholder="e.g., Mon-Fri: 9am-5pm">
<label for="createCounty" class="form-label">County</label>
<input type="text" class="form-control" id="createCounty" name="county" required>
</div>
<div class="mb-3">
<label for="createStatus" class="form-label">Status</label>
<select class="form-select" id="createStatus" name="status" required>
<option value="operational">Operational</option>
<option value="limited">Limited Service</option>
<option value="closed">Temporarily Closed</option>
<option value="planned">Planned</option>
</select>
<label for="createPostcode" class="form-label">Postcode</label>
<input type="text" class="form-control" id="createPostcode" name="postcode" required>
</div>
<div id="createError" class="alert alert-danger" style="display: none;"></div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success">
<i class="bi bi-plus-circle me-1"></i>Create Facility
</button>
</div>
</form>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" id="createFacilityButton">
<i class="bi bi-plus-circle me-1"></i>Create Facility
</button>
</div>
</div>
</div>
</div>

View File

@@ -19,16 +19,16 @@
<!-- Note: facilityData.js is already included in the header -->
<script src="/public/js/comments.js"></script>
<!-- Initialize components -->
<!-- initialise components -->
<script>
// Only run initialization if not already done
if (!window.initializationComplete) {
document.addEventListener('DOMContentLoaded', function() {
// Initialize auth service
// initialise auth service
const loginButton = document.querySelector('[data-bs-toggle="modal"]');
const loginModal = document.getElementById('loginModal');
// Initialize all modals
// initialise all modals
try {
const modalElements = document.querySelectorAll('.modal');
modalElements.forEach(modalElement => {
@@ -53,7 +53,20 @@
console.error('Error initializing modals:', error);
}
// Initialize auth form handlers
// initialise CommentsManager
CommentsManager.state.isDomReady = true;
if (window.simpleAuth) {
CommentsManager.state.isAuthReady = true;
CommentsManager.checkinitialise();
} else {
window.addEventListener('simpleAuthReady', () => {
console.log('SimpleAuth is now ready');
CommentsManager.state.isAuthReady = true;
CommentsManager.checkinitialise();
});
}
// initialise auth form handlers
const loginForm = document.querySelector('#loginModal form');
const loginError = document.querySelector('#loginError');
const captchaContainer = document.querySelector('.captcha-container');

View File

@@ -12,7 +12,7 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- CSS theme -->
<link href="/public/css/my-style.css" rel="stylesheet">
<link href="/public/css/default.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="/public/css/bootstrap-icons.css" rel="stylesheet">
@@ -49,7 +49,7 @@
if (Array.isArray(initialData) && initialData.length > 0) {
sessionStorage.setItem('facilityData', JSON.stringify(initialData));
// Initialize based on DOM state to ensure scripts run at the right time
// initialise based on DOM state to ensure scripts run at the right time
if (document.readyState === 'complete' || document.readyState === 'interactive') {
if (typeof initialiseFacilityData === 'function') {
initialiseFacilityData(initialData);
@@ -159,60 +159,8 @@
<i class="bi bi-map-fill me-1"></i>Map
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about.php">
<i class="bi bi-info-circle-fill me-1"></i>About
</a>
</li>
</ul>
<!-- Search and filter controls -->
<div class="d-flex flex-column flex-lg-row search-controls mx-auto">
<form class="d-flex flex-column flex-lg-row gap-2 w-100" role="search" action="" method="POST">
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-sort-alpha-down text-success"></i>
</span>
<select name="sort" class="form-select border-start-0 filter-control" id="sort">
<option selected value="title">Title</option>
<option value="category">Category</option>
<option value="description">Description</option>
<option value="streetName">Street</option>
<option value="county">County</option>
<option value="town">Town</option>
<option value="postcode">Postcode</option>
<option value="contributor">Contributor</option>
</select>
<select class="form-select sort-control" name="dir" id="dir" data-order="asc">
<option value="asc">Asc</option>
<option value="desc">Desc</option>
</select>
</div>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-filter-circle-fill text-success"></i>
</span>
<select name="filterCat" class="form-select border-start-0 filter-control" id="filterCat">
<option value="title">Title</option>
<option value="category">Category</option>
<option value="description">Description</option>
<option value="streetName">Street</option>
<option value="county">County</option>
<option value="town">Town</option>
<option value="postcode">Postcode</option>
<option value="contributor">Contributor</option>
</select>
</div>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-search text-success"></i>
</span>
<input class="form-control border-start-0" id="searchInput" type="search" name="filter" placeholder="Search..." aria-label="Search">
</div>
</form>
</div>
<!-- User account section -->
<div class="ms-lg-3 mt-3 mt-lg-0" id="userAuthSection">
@@ -374,14 +322,14 @@
</form>
</div>
<div class="modal-footer bg-light">
<p class="small text-muted mb-0">Don't have an account? <a href="#" class="text-success">Register</a></p>
<p class="small text-muted mb-0">Don't have an account? <a href="#" onclick="alert('Please contact the administrator to create an account.');" class="text-success">Register</a></p>
</div>
</div>
</div>
</div>
<script>
// Initialize login modal functionality
// initialise login modal functionality
document.addEventListener('DOMContentLoaded', function() {
const loginModal = document.getElementById('loginModal');
const loginForm = document.getElementById('loginForm');

View File

@@ -1,4 +1,4 @@
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center gap-2">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center gap-3">
<div class="text-muted small">
<span id="paginationInfo" class="d-flex align-items-center">
<i class="bi bi-info-circle me-2 text-success"></i>
@@ -7,8 +7,8 @@
</div>
<!-- Pagination controls -->
<nav aria-label="Facility table pagination">
<ul class="pagination pagination-sm mb-0" id="paginationControls">
<nav class="bg-transparent" aria-label="Facility table pagination">
<ul class="pagination pagination-sm mb-0 border-2 rounded border-success" id="paginationControls">
<!-- First page button -->
<li class="page-item">
<a class="page-link border-0 text-success" href="#" aria-label="First" id="firstPage">
@@ -46,7 +46,6 @@
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>

View File

@@ -1,225 +0,0 @@
import sqlite3
import random
# Connect to the SQLite database
conn = sqlite3.connect('Databases/ecobuddy.sqlite')
cursor = conn.cursor()
# Check if we need to add any new categories
cursor.execute("SELECT id FROM ecoCategories")
existing_categories = [row[0] for row in cursor.fetchall()]
# Add two new categories
new_categories = [
(16, "Urban Farms"),
(17, "Rainwater Harvesting Systems")
]
for category in new_categories:
if category[0] not in existing_categories:
cursor.execute("INSERT INTO ecoCategories (id, name) VALUES (?, ?)", category)
print(f"Added new category: {category[1]}")
# Get list of user IDs for contributors
cursor.execute("SELECT id FROM ecoUser")
user_ids = [row[0] for row in cursor.fetchall()]
# Define 10 new ecological facilities in the UK with accurate location data
new_facilities = [
{
"title": "Community Garden Hackney",
"category": 12, # Pollinator Gardens
"description": "Urban garden with native plants to support local pollinators",
"houseNumber": "45",
"streetName": "Dalston Lane",
"county": "Greater London",
"town": "London",
"postcode": "E8 3AH",
"lng": -0.0612,
"lat": 51.5476,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently expanded with new wildflower section",
"Volunteer days every Saturday"
]
},
{
"title": "Rooftop Solar Farm",
"category": 8, # Green Roofs
"description": "Combined green roof and solar panel installation on commercial building",
"houseNumber": "120",
"streetName": "Deansgate",
"county": "Greater Manchester",
"town": "Manchester",
"postcode": "M3 2QJ",
"lng": -2.2484,
"lat": 53.4808,
"contributor": random.choice(user_ids),
"status_comments": [
"Generates power for the entire building"
]
},
{
"title": "Edinburgh Tool Library",
"category": 15, # Community Tool Libraries
"description": "Borrow tools instead of buying them - reducing waste and consumption",
"houseNumber": "25",
"streetName": "Leith Walk",
"county": "Edinburgh",
"town": "Edinburgh",
"postcode": "EH6 8LN",
"lng": -3.1752,
"lat": 55.9677,
"contributor": random.choice(user_ids),
"status_comments": []
},
{
"title": "Cardiff Bay Water Refill Station",
"category": 9, # Public Water Refill Stations
"description": "Free water refill station to reduce plastic bottle usage",
"houseNumber": "3",
"streetName": "Mermaid Quay",
"county": "Cardiff",
"town": "Cardiff",
"postcode": "CF10 5BZ",
"lng": -3.1644,
"lat": 51.4644,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently cleaned and maintained",
"High usage during summer months"
]
},
{
"title": "Bristol Urban Farm",
"category": 16, # Urban Farms (new category)
"description": "Community-run urban farm providing local produce and education",
"houseNumber": "18",
"streetName": "Stapleton Road",
"county": "Bristol",
"town": "Bristol",
"postcode": "BS5 0RA",
"lng": -2.5677,
"lat": 51.4635,
"contributor": random.choice(user_ids),
"status_comments": [
"Open for volunteers Tuesday-Sunday"
]
},
{
"title": "Newcastle Rainwater Collection System",
"category": 17, # Rainwater Harvesting Systems (new category)
"description": "Public demonstration of rainwater harvesting for garden irrigation",
"houseNumber": "55",
"streetName": "Northumberland Street",
"county": "Tyne and Wear",
"town": "Newcastle upon Tyne",
"postcode": "NE1 7DH",
"lng": -1.6178,
"lat": 54.9783,
"contributor": random.choice(user_ids),
"status_comments": []
},
{
"title": "Brighton Beach Solar Bench",
"category": 7, # Solar-Powered Benches
"description": "Solar-powered bench with USB charging ports and WiFi",
"houseNumber": "",
"streetName": "Kings Road",
"county": "East Sussex",
"town": "Brighton",
"postcode": "BN1 2FN",
"lng": -0.1426,
"lat": 50.8214,
"contributor": random.choice(user_ids),
"status_comments": [
"Popular spot for tourists",
"One USB port currently not working"
]
},
{
"title": "Leeds Community Compost Hub",
"category": 6, # Community Compost Bins
"description": "Large-scale community composting facility for local residents",
"houseNumber": "78",
"streetName": "Woodhouse Lane",
"county": "West Yorkshire",
"town": "Leeds",
"postcode": "LS2 9JT",
"lng": -1.5491,
"lat": 53.8067,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently expanded capacity"
]
},
{
"title": "Glasgow EV Charging Hub",
"category": 4, # Public EV Charging Stations
"description": "Multi-vehicle EV charging station with fast chargers",
"houseNumber": "42",
"streetName": "Buchanan Street",
"county": "Glasgow",
"town": "Glasgow",
"postcode": "G1 3JX",
"lng": -4.2526,
"lat": 55.8621,
"contributor": random.choice(user_ids),
"status_comments": [
"6 charging points available",
"24/7 access"
]
},
{
"title": "Oxford E-Waste Collection Center",
"category": 13, # E-Waste Collection Bins
"description": "Dedicated facility for proper disposal and recycling of electronic waste",
"houseNumber": "15",
"streetName": "St Aldate's",
"county": "Oxfordshire",
"town": "Oxford",
"postcode": "OX1 1BX",
"lng": -1.2577,
"lat": 51.7520,
"contributor": random.choice(user_ids),
"status_comments": []
}
]
# Insert facilities into the database
for facility in new_facilities:
cursor.execute("""
INSERT INTO ecoFacilities
(title, category, description, houseNumber, streetName, county, town, postcode, lng, lat, contributor)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
facility["title"],
facility["category"],
facility["description"],
facility["houseNumber"],
facility["streetName"],
facility["county"],
facility["town"],
facility["postcode"],
facility["lng"],
facility["lat"],
facility["contributor"]
))
# Get the ID of the inserted facility
facility_id = cursor.lastrowid
# Add status comments if any
for comment in facility["status_comments"]:
cursor.execute("""
INSERT INTO ecoFacilityStatus (facilityId, statusComment)
VALUES (?, ?)
""", (facility_id, comment))
print(f"Added facility: {facility['title']} in {facility['town']}")
# Commit the changes and close the connection
conn.commit()
conn.close()
print("\nSuccessfully added 10 new ecological facilities to the database.")

16988
debug.log

File diff suppressed because it is too large Load Diff

19
map.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
// load dataset
require_once('Models/FacilityDataSet.php');
// make a view class
$view = new stdClass();
$view->pageTitle = 'Map';
// initialise facility data
$facilityDataSet = new FacilityDataSet();
$view->facilityDataSet = $facilityDataSet->fetchAll();
// Log any critical errors
if ($view->facilityDataSet === false) {
error_log('Error fetching facility data');
}
// load main view
require_once('Views/map.phtml');

View File

@@ -6,9 +6,8 @@
*
* The client uses JWT tokens for authentication, which are automatically
* included in requests via the fetchAuth function provided by the simpleAuth service.
*
* NOTE: For authentication (login, logout, token validation), please use the simpleAuth
* service directly instead of this API client.
*
* Similar to AuthService.php, great pain and countless tears. And learning woooo!!!!!!!!
*/
class ApiClient {
/**
@@ -133,21 +132,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 +250,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 +271,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;

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

@@ -0,0 +1,781 @@
/**
* 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') || '[]');
// Add location found handler
map.on('locationfound', function(e) {
try {
const { lat, lng } = e.latlng;
// Update the map directly with the coordinates
updateMapLocation({ lat, lng }, currentRadius);
// Remove overlay once we have a valid location
const overlay = document.getElementById('mapOverlay');
if (overlay) {
overlay.classList.add('hidden');
}
// Get postcode from coordinates
fetch(`https://api.postcodes.io/postcodes?lon=${lng}&lat=${lat}`)
.then(response => response.json())
.then(data => {
if (data.status === 200 && data.result && data.result.length > 0) {
const postcode = data.result[0].postcode;
const postcodeInput = document.getElementById('postcode');
if (postcodeInput) {
postcodeInput.value = postcode;
}
}
})
.catch(error => {
console.error('Error getting postcode:', error);
});
} catch (error) {
console.error('Error processing location:', error);
alert('Error getting your location: ' + error.message);
}
});
// Add location error handler
map.on('locationerror', function(e) {
console.error('Geolocation error:', e);
let message = 'Error getting your location: ';
switch(e.code) {
case 1: // PERMISSION_DENIED
message += 'Please enable location access in your browser settings.';
break;
case 2: // POSITION_UNAVAILABLE
message += 'Location information is unavailable.';
break;
case 3: // TIMEOUT
message += 'Location request timed out.';
break;
default:
message += 'An unknown error occurred.';
}
alert(message);
});
// Set up form handlers
setupFormHandlers();
// Set up search handler from header
setupHeaderSearchHandler();
});
/**
* Get postcode from coordinates using postcodes.io API
* @param {number} lat - Latitude
* @param {number} lng - Longitude
* @returns {Promise<string>} The postcode
*/
async function getPostcodeFromCoordinates(lat, lng) {
try {
const response = await fetch(`https://api.postcodes.io/postcodes?lon=${lng}&lat=${lat}`);
if (!response.ok) {
throw new Error('Could not find postcode for coordinates');
}
const data = await response.json();
if (data.status === 200 && data.result && data.result.length > 0) {
return data.result[0].postcode;
}
throw new Error('No postcode found for coordinates');
} catch (error) {
console.error('Error getting postcode from coordinates:', error);
throw error;
}
}
/**
* Handle geolocation success
* @param {GeolocationPosition} position - The position object
*/
async function handleGeolocationSuccess(position) {
try {
const { latitude, longitude } = position.coords;
// Get postcode from coordinates
const postcode = await getPostcodeFromCoordinates(latitude, longitude);
// Update the postcode input
const postcodeInput = document.getElementById('postcode');
if (postcodeInput) {
postcodeInput.value = postcode;
// Submit the form to update the map
const postcodeForm = document.getElementById('postcodeForm');
if (postcodeForm) {
postcodeForm.dispatchEvent(new Event('submit'));
}
}
} catch (error) {
console.error('Error processing geolocation:', error);
alert('Error getting your location: ' + error.message);
}
}
/**
* Handle geolocation error
* @param {GeolocationPositionError} error - The error object
*/
function handleGeolocationError(error) {
console.error('Geolocation error:', error);
let message = 'Error getting your location: ';
switch(error.code) {
case error.PERMISSION_DENIED:
message += 'Please enable location access in your browser settings.';
break;
case error.POSITION_UNAVAILABLE:
message += 'Location information is unavailable.';
break;
case error.TIMEOUT:
message += 'Location request timed out.';
break;
default:
message += 'An unknown error occurred.';
}
alert(message);
}
/**
* Set up form handlers for postcode and radius inputs
*/
function setupFormHandlers() {
const postcodeForm = document.getElementById('postcodeForm');
const radiusSelect = document.getElementById('radius');
if (postcodeForm) {
// Add geolocation functionality to the search button
const searchButton = postcodeForm.querySelector('button[type="submit"]');
if (searchButton) {
searchButton.onclick = (e) => {
// If the postcode input is empty, use geolocation
const postcodeInput = document.getElementById('postcode');
if (!postcodeInput.value.trim()) {
e.preventDefault();
map.locate({
setView: false,
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
});
}
};
}
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"]');
submitButton.disabled = true;
// 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;
}
});
}
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
});
}
/**