Compare commits
2 Commits
78508a7cbd
...
183cca3fd3
Author | SHA1 | Date | |
---|---|---|---|
|
183cca3fd3 | ||
|
8877faa631 |
@@ -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.
@@ -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
|
|
@@ -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.")
|
|
@@ -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";
|
|
@@ -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
@@ -2,7 +2,9 @@
|
|||||||
require_once('UserDataSet.php');
|
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 {
|
class AuthService {
|
||||||
private string $secretKey;
|
private string $secretKey;
|
||||||
@@ -14,7 +16,7 @@ class AuthService {
|
|||||||
* @throws Exception if OpenSSL extension is not loaded
|
* @throws Exception if OpenSSL extension is not loaded
|
||||||
*/
|
*/
|
||||||
public function __construct() {
|
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';
|
$envFile = __DIR__ . '/../.env';
|
||||||
if (file_exists($envFile)) {
|
if (file_exists($envFile)) {
|
||||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$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->secretKey = getenv('JWT_SECRET_KEY') ?: 'your-256-bit-secret';
|
||||||
$this->tokenExpiry = (int)(getenv('JWT_TOKEN_EXPIRY') ?: 3600);
|
$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')) {
|
if (!extension_loaded('openssl')) {
|
||||||
throw new Exception('OpenSSL extension is required for JWT');
|
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
|
* @param array $userData User information to include in token
|
||||||
* @return string The generated JWT token
|
* @return string The generated JWT token
|
||||||
*/
|
*/
|
||||||
@@ -52,6 +54,7 @@ class AuthService {
|
|||||||
$issuedAt = time();
|
$issuedAt = time();
|
||||||
$expire = $issuedAt + $this->tokenExpiry;
|
$expire = $issuedAt + $this->tokenExpiry;
|
||||||
|
|
||||||
|
// Create payload with user data
|
||||||
$payload = [
|
$payload = [
|
||||||
'iat' => $issuedAt,
|
'iat' => $issuedAt,
|
||||||
'exp' => $expire,
|
'exp' => $expire,
|
||||||
@@ -101,7 +104,7 @@ class AuthService {
|
|||||||
$signature = hash_hmac('sha256', "$header.$payload", $this->secretKey, true);
|
$signature = hash_hmac('sha256', "$header.$payload", $this->secretKey, true);
|
||||||
$signature = $this->base64UrlEncode($signature);
|
$signature = $this->base64UrlEncode($signature);
|
||||||
|
|
||||||
return "$header.$payload.$signature";
|
return "$header.$payload.$signature"; //Wooooooo!!! JWT is a thing!
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,15 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Represents a facility in the EcoBuddy system
|
* Represents a singular facility
|
||||||
*
|
*
|
||||||
* This class serves as a data model for facilities, encapsulating all
|
* Data model for facilities, encapsulating all
|
||||||
* the properties and behaviours of a single facility. It follows the
|
* properties and behaviours of a single facility.
|
||||||
* Data Transfer Object (DTO) pattern that I learned about in my
|
|
||||||
* software architecture module.
|
|
||||||
*
|
*
|
||||||
* Each facility has location data, descriptive information, and metadata
|
* Each facility has location data, descriptive info, and metadata.
|
||||||
* about who contributed it. This class provides a clean interface for
|
|
||||||
* accessing this data throughout the application.
|
|
||||||
*/
|
*/
|
||||||
class FacilityData {
|
class FacilityData {
|
||||||
/**
|
/**
|
||||||
@@ -77,10 +73,6 @@ class FacilityData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the facility's title
|
* 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
|
* @return string The facility title
|
||||||
*/
|
*/
|
||||||
public function getTitle() {
|
public function getTitle() {
|
||||||
@@ -89,10 +81,6 @@ class FacilityData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the facility's category
|
* Gets the facility's category
|
||||||
*
|
|
||||||
* The category helps classify facilities by type, such as
|
|
||||||
* recycling centre, community garden, etc.
|
|
||||||
*
|
|
||||||
* @return string The facility category
|
* @return string The facility category
|
||||||
*/
|
*/
|
||||||
public function getCategory() {
|
public function getCategory() {
|
||||||
@@ -101,10 +89,6 @@ class FacilityData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the facility's current status
|
* Gets the facility's current status
|
||||||
*
|
|
||||||
* The status indicates whether the facility is operational,
|
|
||||||
* under maintenance, closed, etc.
|
|
||||||
*
|
|
||||||
* @return string The facility status
|
* @return string The facility status
|
||||||
*/
|
*/
|
||||||
public function getStatus() {
|
public function getStatus() {
|
||||||
@@ -113,10 +97,6 @@ class FacilityData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the facility's description
|
* Gets the facility's description
|
||||||
*
|
|
||||||
* The description provides detailed information about the facility,
|
|
||||||
* its purpose, services offered, etc.
|
|
||||||
*
|
|
||||||
* @return string The facility description
|
* @return string The facility description
|
||||||
*/
|
*/
|
||||||
public function getDescription() {
|
public function getDescription() {
|
||||||
@@ -125,9 +105,6 @@ class FacilityData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the facility's house/building number
|
* 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
|
* @return string The house/building number
|
||||||
*/
|
*/
|
||||||
public function getHouseNumber() {
|
public function getHouseNumber() {
|
||||||
@@ -136,9 +113,6 @@ class FacilityData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the facility's street name
|
* Gets the facility's street name
|
||||||
*
|
|
||||||
* This is part of the facility's address and helps locate it physically.
|
|
||||||
*
|
|
||||||
* @return string The street name
|
* @return string The street name
|
||||||
*/
|
*/
|
||||||
public function getStreetName() {
|
public function getStreetName() {
|
||||||
@@ -147,9 +121,6 @@ class FacilityData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the facility's county
|
* Gets the facility's county
|
||||||
*
|
|
||||||
* This is part of the facility's address and helps locate it physically.
|
|
||||||
*
|
|
||||||
* @return string The county
|
* @return string The county
|
||||||
*/
|
*/
|
||||||
public function getCounty() {
|
public function getCounty() {
|
||||||
@@ -158,9 +129,6 @@ class FacilityData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the facility's town or city
|
* 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
|
* @return string The town or city
|
||||||
*/
|
*/
|
||||||
public function getTown() {
|
public function getTown() {
|
||||||
@@ -169,10 +137,6 @@ class FacilityData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the facility's postcode
|
* 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
|
* @return string The postcode
|
||||||
*/
|
*/
|
||||||
public function getPostcode() {
|
public function getPostcode() {
|
||||||
@@ -181,10 +145,6 @@ class FacilityData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the facility's longitude coordinate
|
* 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
|
* @return float The longitude coordinate
|
||||||
*/
|
*/
|
||||||
public function getLng() {
|
public function getLng() {
|
||||||
@@ -193,10 +153,6 @@ class FacilityData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the facility's latitude coordinate
|
* 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
|
* @return float The latitude coordinate
|
||||||
*/
|
*/
|
||||||
public function getLat() {
|
public function getLat() {
|
||||||
@@ -205,10 +161,6 @@ class FacilityData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the username of the facility's contributor
|
* 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
|
* @return string The contributor's username
|
||||||
*/
|
*/
|
||||||
public function getContributor() {
|
public function getContributor() {
|
||||||
|
@@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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
|
* Gets the user's access level
|
||||||
*
|
*
|
||||||
@@ -128,33 +118,6 @@ class User {
|
|||||||
return false;
|
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
|
* Static method to check if a request is authenticated
|
||||||
|
@@ -21,11 +21,17 @@ require('template/header.phtml')
|
|||||||
<div class="card-header bg-light py-3">
|
<div class="card-header bg-light py-3">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<h5 class="mb-0 fw-bold text-primary">
|
<!-- Search and filter controls -->
|
||||||
<i class="bi bi-geo-alt-fill me-2 text-success"></i>Facilities
|
<div class="d-flex flex-column flex-lg-row search-controls mx-auto">
|
||||||
</h5>
|
<form class="d-flex flex-column flex-lg-row gap-2 w-100" role="search" action="" method="POST">
|
||||||
<!-- Badge showing the number of facilities -->
|
<div class="input-group flex-grow-1">
|
||||||
<span class="badge bg-success rounded-pill ms-2" id="facilityCount"></span>
|
<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>
|
</div>
|
||||||
<!-- Admin-only buttons -->
|
<!-- Admin-only buttons -->
|
||||||
<div id="adminButtons" style="display: none;">
|
<div id="adminButtons" style="display: none;">
|
||||||
@@ -99,33 +105,6 @@ require('template/header.phtml')
|
|||||||
regularUserView.style.display = isAdmin ? 'none' : 'block';
|
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
|
// Update UI when the page loads
|
||||||
@@ -139,28 +118,4 @@ require('template/header.phtml')
|
|||||||
});
|
});
|
||||||
</script>
|
</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');?>
|
<?php require('template/footer.phtml');?>
|
195
Views/map.phtml
Normal file
195
Views/map.phtml
Normal 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') ?>
|
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body p-4">
|
<div class="modal-body p-4">
|
||||||
<!-- Create facility form -->
|
<!-- Create facility form -->
|
||||||
<form id="createFacilityForm">
|
<form id="createForm">
|
||||||
<!-- Form fields -->
|
<!-- Form fields -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="createTitle" class="form-label">Facility Name</label>
|
<label for="createTitle" class="form-label">Facility Name</label>
|
||||||
@@ -41,53 +41,49 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="createLatitude" class="form-label">Latitude</label>
|
<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>
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="createLongitude" class="form-label">Longitude</label>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="createAddress" class="form-label">Address</label>
|
<label for="createHouseNumber" class="form-label">House Number/Name</label>
|
||||||
<input type="text" class="form-control" id="createAddress" name="address" required>
|
<input type="text" class="form-control" id="createHouseNumber" name="houseNumber" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="createContact" class="form-label">Contact Information</label>
|
<label for="createStreetName" class="form-label">Street Name</label>
|
||||||
<input type="text" class="form-control" id="createContact" name="contact">
|
<input type="text" class="form-control" id="createStreetName" name="streetName" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="createWebsite" class="form-label">Website</label>
|
<label for="createTown" class="form-label">Town/City</label>
|
||||||
<input type="url" class="form-control" id="createWebsite" name="website" placeholder="https://">
|
<input type="text" class="form-control" id="createTown" name="town" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="createHours" class="form-label">Operating Hours</label>
|
<label for="createCounty" class="form-label">County</label>
|
||||||
<input type="text" class="form-control" id="createHours" name="hours" placeholder="e.g., Mon-Fri: 9am-5pm">
|
<input type="text" class="form-control" id="createCounty" name="county" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="createStatus" class="form-label">Status</label>
|
<label for="createPostcode" class="form-label">Postcode</label>
|
||||||
<select class="form-select" id="createStatus" name="status" required>
|
<input type="text" class="form-control" id="createPostcode" name="postcode" required>
|
||||||
<option value="operational">Operational</option>
|
|
||||||
<option value="limited">Limited Service</option>
|
|
||||||
<option value="closed">Temporarily Closed</option>
|
|
||||||
<option value="planned">Planned</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="createError" class="alert alert-danger" style="display: none;"></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>
|
</form>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -19,16 +19,16 @@
|
|||||||
<!-- Note: facilityData.js is already included in the header -->
|
<!-- Note: facilityData.js is already included in the header -->
|
||||||
<script src="/public/js/comments.js"></script>
|
<script src="/public/js/comments.js"></script>
|
||||||
|
|
||||||
<!-- Initialize components -->
|
<!-- initialise components -->
|
||||||
<script>
|
<script>
|
||||||
// Only run initialization if not already done
|
// Only run initialization if not already done
|
||||||
if (!window.initializationComplete) {
|
if (!window.initializationComplete) {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize auth service
|
// initialise auth service
|
||||||
const loginButton = document.querySelector('[data-bs-toggle="modal"]');
|
const loginButton = document.querySelector('[data-bs-toggle="modal"]');
|
||||||
const loginModal = document.getElementById('loginModal');
|
const loginModal = document.getElementById('loginModal');
|
||||||
|
|
||||||
// Initialize all modals
|
// initialise all modals
|
||||||
try {
|
try {
|
||||||
const modalElements = document.querySelectorAll('.modal');
|
const modalElements = document.querySelectorAll('.modal');
|
||||||
modalElements.forEach(modalElement => {
|
modalElements.forEach(modalElement => {
|
||||||
@@ -53,7 +53,20 @@
|
|||||||
console.error('Error initializing modals:', error);
|
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 loginForm = document.querySelector('#loginModal form');
|
||||||
const loginError = document.querySelector('#loginError');
|
const loginError = document.querySelector('#loginError');
|
||||||
const captchaContainer = document.querySelector('.captcha-container');
|
const captchaContainer = document.querySelector('.captcha-container');
|
||||||
|
@@ -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">
|
<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 -->
|
<!-- CSS theme -->
|
||||||
<link href="/public/css/my-style.css" rel="stylesheet">
|
<link href="/public/css/default.css" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Bootstrap Icons -->
|
<!-- Bootstrap Icons -->
|
||||||
<link href="/public/css/bootstrap-icons.css" rel="stylesheet">
|
<link href="/public/css/bootstrap-icons.css" rel="stylesheet">
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
if (Array.isArray(initialData) && initialData.length > 0) {
|
if (Array.isArray(initialData) && initialData.length > 0) {
|
||||||
sessionStorage.setItem('facilityData', JSON.stringify(initialData));
|
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 (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||||
if (typeof initialiseFacilityData === 'function') {
|
if (typeof initialiseFacilityData === 'function') {
|
||||||
initialiseFacilityData(initialData);
|
initialiseFacilityData(initialData);
|
||||||
@@ -159,60 +159,8 @@
|
|||||||
<i class="bi bi-map-fill me-1"></i>Map
|
<i class="bi bi-map-fill me-1"></i>Map
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</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 -->
|
<!-- User account section -->
|
||||||
<div class="ms-lg-3 mt-3 mt-lg-0" id="userAuthSection">
|
<div class="ms-lg-3 mt-3 mt-lg-0" id="userAuthSection">
|
||||||
@@ -374,14 +322,14 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer bg-light">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Initialize login modal functionality
|
// initialise login modal functionality
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const loginModal = document.getElementById('loginModal');
|
const loginModal = document.getElementById('loginModal');
|
||||||
const loginForm = document.getElementById('loginForm');
|
const loginForm = document.getElementById('loginForm');
|
||||||
|
@@ -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">
|
<div class="text-muted small">
|
||||||
<span id="paginationInfo" class="d-flex align-items-center">
|
<span id="paginationInfo" class="d-flex align-items-center">
|
||||||
<i class="bi bi-info-circle me-2 text-success"></i>
|
<i class="bi bi-info-circle me-2 text-success"></i>
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination controls -->
|
<!-- Pagination controls -->
|
||||||
<nav aria-label="Facility table pagination">
|
<nav class="bg-transparent" aria-label="Facility table pagination">
|
||||||
<ul class="pagination pagination-sm mb-0" id="paginationControls">
|
<ul class="pagination pagination-sm mb-0 border-2 rounded border-success" id="paginationControls">
|
||||||
<!-- First page button -->
|
<!-- First page button -->
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link border-0 text-success" href="#" aria-label="First" id="firstPage">
|
<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="10">10</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
<option value="100">100</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -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.")
|
|
19
map.php
Normal file
19
map.php
Normal 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');
|
@@ -6,9 +6,8 @@
|
|||||||
*
|
*
|
||||||
* The client uses JWT tokens for authentication, which are automatically
|
* The client uses JWT tokens for authentication, which are automatically
|
||||||
* included in requests via the fetchAuth function provided by the simpleAuth service.
|
* included in requests via the fetchAuth function provided by the simpleAuth service.
|
||||||
*
|
*
|
||||||
* NOTE: For authentication (login, logout, token validation), please use the simpleAuth
|
* Similar to AuthService.php, great pain and countless tears. And learning woooo!!!!!!!!
|
||||||
* service directly instead of this API client.
|
|
||||||
*/
|
*/
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
/**
|
/**
|
||||||
@@ -133,21 +132,30 @@ class ApiClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
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
|
// Use authenticated fetch for all facility requests
|
||||||
const response = await this.authFetch('/facilitycontroller.php', {
|
const response = await this.authFetch('/facilitycontroller.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
requireAuth: true // Explicitly require authentication
|
requireAuth: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
const jsonData = await response.json();
|
||||||
|
|
||||||
// Check if response is ok
|
// Check if response is ok
|
||||||
if (!response.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;
|
return jsonData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Facility API error:', error);
|
console.error('Facility API error:', error);
|
||||||
@@ -242,7 +250,11 @@ class ApiClient {
|
|||||||
* @returns {Promise<Object>} The response data
|
* @returns {Promise<Object>} The response data
|
||||||
*/
|
*/
|
||||||
async updateFacilityStatus(statusId, editStatus, facilityId) {
|
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();
|
const api = new ApiClient();
|
||||||
|
|
||||||
// Export API client
|
// Export API client
|
||||||
|
@@ -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 = {
|
const CommentsManager = {
|
||||||
// Track initialization states
|
// Initialization states
|
||||||
state: {
|
state: {
|
||||||
isInitializing: false,
|
isInitializing: false,
|
||||||
isInitialized: false,
|
isinitialised: false,
|
||||||
isDomReady: false,
|
isDomReady: false,
|
||||||
isAuthReady: false
|
isAuthReady: false
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize comments functionality
|
* initialise status functionality
|
||||||
*/
|
*/
|
||||||
initialize() {
|
initialise() {
|
||||||
if (this.state.isInitialized) return;
|
if (this.state.isinitialised) return;
|
||||||
|
|
||||||
console.log('Initializing comments...');
|
console.log('Initializing comments...');
|
||||||
|
|
||||||
// Initialize comment modal handlers
|
// initialise comment modal handlers
|
||||||
this.initializeCommentModals();
|
this.initialiseCommentModals();
|
||||||
|
|
||||||
// Set up form handlers
|
// Set up form handlers
|
||||||
this.setupCommentFormHandlers();
|
this.setupCommentFormHandlers();
|
||||||
|
|
||||||
console.log('Comments initialized with auth state:', {
|
console.log('Comments initialised with auth state:', {
|
||||||
isAuthenticated: this.isAuthenticated(),
|
isAuthenticated: this.isAuthenticated(),
|
||||||
user: window.simpleAuth.getUser()
|
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) {
|
if (this.state.isDomReady && this.state.isAuthReady && !this.state.isInitializing) {
|
||||||
this.state.isInitializing = true;
|
this.state.isInitializing = true;
|
||||||
this.initialize();
|
this.initialise();
|
||||||
this.state.isInitializing = false;
|
this.state.isInitializing = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -53,33 +53,33 @@ const CommentsManager = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize comment modals
|
* initialise comment modals
|
||||||
*/
|
*/
|
||||||
initializeCommentModals() {
|
initialiseCommentModals() {
|
||||||
// Status modal (comments view)
|
// Status modal (comments view)
|
||||||
const statusModal = document.getElementById('statusModal');
|
const statusModal = document.getElementById('statusModal');
|
||||||
if (statusModal) {
|
if (statusModal) {
|
||||||
statusModal.addEventListener('show.bs.modal', (event) => {
|
statusModal.addEventListener('show.bs.modal', (event) => {
|
||||||
console.log('Comments modal is about to show');
|
// Get facility ID from either the button or the modal's data attribute
|
||||||
// Get the button that triggered the modal
|
let facilityId;
|
||||||
const button = event.relatedTarget;
|
|
||||||
// Get the facility ID from the data attribute
|
// First try to get it from the button that triggered the modal
|
||||||
const facilityId = button.getAttribute('data-facility-id');
|
if (event.relatedTarget) {
|
||||||
console.log('Facility ID for comments:', facilityId);
|
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) {
|
if (!facilityId) {
|
||||||
console.error('No facility ID found for comments');
|
console.error('No facility ID found for comments');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the facility ID in the comment form
|
// Store the facility ID on the modal for later use
|
||||||
const commentForm = document.getElementById('commentForm');
|
statusModal.setAttribute('data-facility-id', facilityId);
|
||||||
if (commentForm) {
|
|
||||||
const facilityIdInput = commentForm.querySelector('#commentFacilityId');
|
|
||||||
if (facilityIdInput) {
|
|
||||||
facilityIdInput.value = facilityId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load facility comments
|
// Load facility comments
|
||||||
this.loadFacilityComments(facilityId);
|
this.loadFacilityComments(facilityId);
|
||||||
@@ -90,13 +90,10 @@ const CommentsManager = {
|
|||||||
const editCommentModal = document.getElementById('editCommentModal');
|
const editCommentModal = document.getElementById('editCommentModal');
|
||||||
if (editCommentModal) {
|
if (editCommentModal) {
|
||||||
editCommentModal.addEventListener('show.bs.modal', (event) => {
|
editCommentModal.addEventListener('show.bs.modal', (event) => {
|
||||||
console.log('Edit comment modal is about to show');
|
|
||||||
const button = event.relatedTarget;
|
const button = event.relatedTarget;
|
||||||
const commentId = button.getAttribute('data-comment-id');
|
const commentId = button.getAttribute('data-comment-id');
|
||||||
const commentText = button.getAttribute('data-comment-text');
|
const commentText = button.getAttribute('data-comment-text');
|
||||||
|
|
||||||
console.log('Comment ID:', commentId, 'Comment text:', commentText);
|
|
||||||
|
|
||||||
// Set the comment ID and text in the form
|
// Set the comment ID and text in the form
|
||||||
const editForm = document.getElementById('editCommentForm');
|
const editForm = document.getElementById('editCommentForm');
|
||||||
if (editForm) {
|
if (editForm) {
|
||||||
@@ -151,27 +148,34 @@ const CommentsManager = {
|
|||||||
|
|
||||||
const formData = new FormData(commentForm);
|
const formData = new FormData(commentForm);
|
||||||
|
|
||||||
// Get form data
|
// Get form data and ensure proper types
|
||||||
const statusComment = formData.get('commentText');
|
const statusComment = formData.get('commentText');
|
||||||
const facilityId = formData.get('facilityId');
|
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 {
|
try {
|
||||||
console.log('Sending comment request...');
|
|
||||||
// Use the API client to add a status comment
|
// Use the API client to add a status comment
|
||||||
const data = await window.api.addFacilityStatus(facilityId, statusComment);
|
const data = await window.api.addFacilityStatus(facilityId.toString(), statusComment);
|
||||||
|
|
||||||
console.log('Comment response:', data);
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
console.log('Comment added successfully');
|
|
||||||
|
|
||||||
// Reset the form
|
// Reset the form
|
||||||
commentForm.reset();
|
commentForm.reset();
|
||||||
|
|
||||||
// Reload comments to show the new one
|
// Reload comments to show the new one
|
||||||
this.loadFacilityComments(facilityId);
|
this.loadFacilityComments(facilityId.toString());
|
||||||
} else {
|
} else {
|
||||||
console.error('Comment failed:', data.error);
|
console.error('Comment failed:', data.error);
|
||||||
alert(data.error || 'Failed to add comment');
|
alert(data.error || 'Failed to add comment');
|
||||||
@@ -249,21 +253,8 @@ const CommentsManager = {
|
|||||||
* Creates a comment form dynamically for authenticated users
|
* Creates a comment form dynamically for authenticated users
|
||||||
*/
|
*/
|
||||||
createCommentFormForAuthenticatedUser(facilityId) {
|
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
|
// First check if simpleAuth is available
|
||||||
if (!window.simpleAuth) {
|
if (!window.simpleAuth) {
|
||||||
console.warn('SimpleAuth not initialized yet');
|
|
||||||
return `
|
return `
|
||||||
<div class="alert alert-warning mb-0">
|
<div class="alert alert-warning mb-0">
|
||||||
<i class="bi bi-hourglass-split me-2"></i>
|
<i class="bi bi-hourglass-split me-2"></i>
|
||||||
@@ -278,14 +269,7 @@ const CommentsManager = {
|
|||||||
const user = window.simpleAuth.getUser();
|
const user = window.simpleAuth.getUser();
|
||||||
const isAuthenticated = window.simpleAuth.isAuthenticated();
|
const isAuthenticated = window.simpleAuth.isAuthenticated();
|
||||||
|
|
||||||
console.log('Authentication validation:', {
|
|
||||||
hasToken: !!token,
|
|
||||||
hasUser: !!user,
|
|
||||||
isAuthenticated: isAuthenticated
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isAuthenticated || !token || !user) {
|
if (!isAuthenticated || !token || !user) {
|
||||||
console.log('User not authenticated:', { isAuthenticated, token: !!token, user: !!user });
|
|
||||||
return `
|
return `
|
||||||
<div class="alert alert-info mb-0">
|
<div class="alert alert-info mb-0">
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
@@ -295,7 +279,6 @@ const CommentsManager = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// User is authenticated, create the comment form
|
// User is authenticated, create the comment form
|
||||||
console.log('User is authenticated, creating comment form');
|
|
||||||
return `
|
return `
|
||||||
<form id="commentForm" class="mt-3">
|
<form id="commentForm" class="mt-3">
|
||||||
<input type="hidden" id="commentFacilityId" name="facilityId" value="${this.escapeHtml(facilityId)}">
|
<input type="hidden" id="commentFacilityId" name="facilityId" value="${this.escapeHtml(facilityId)}">
|
||||||
@@ -327,24 +310,30 @@ const CommentsManager = {
|
|||||||
*/
|
*/
|
||||||
async loadFacilityComments(facilityId) {
|
async loadFacilityComments(facilityId) {
|
||||||
try {
|
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
|
// Show loading indicator
|
||||||
const commentsContainer = document.getElementById('commentsContainer');
|
const commentsContainer = document.getElementById('commentsContainer');
|
||||||
if (commentsContainer) {
|
if (!commentsContainer) {
|
||||||
commentsContainer.innerHTML = `
|
throw new Error('Comments container not found');
|
||||||
<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>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Use the API client to get facility statuses
|
||||||
const data = await window.api.getFacilityStatuses(facilityId);
|
const data = await window.api.getFacilityStatuses(facilityId);
|
||||||
console.log('Comments API response:', data);
|
|
||||||
|
|
||||||
// Validate the response
|
// Validate the response
|
||||||
if (!data || typeof data !== 'object') {
|
if (!data || typeof data !== 'object') {
|
||||||
@@ -364,7 +353,6 @@ const CommentsManager = {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading comments:', error);
|
console.error('Error loading comments:', error);
|
||||||
console.error('Error stack:', error.stack);
|
|
||||||
|
|
||||||
const commentsContainer = document.getElementById('commentsContainer');
|
const commentsContainer = document.getElementById('commentsContainer');
|
||||||
if (commentsContainer) {
|
if (commentsContainer) {
|
||||||
@@ -383,22 +371,32 @@ const CommentsManager = {
|
|||||||
*/
|
*/
|
||||||
renderComments(comments, facilityId) {
|
renderComments(comments, facilityId) {
|
||||||
const commentsContainer = document.getElementById('commentsContainer');
|
const commentsContainer = document.getElementById('commentsContainer');
|
||||||
if (!commentsContainer) return;
|
if (!commentsContainer) {
|
||||||
|
console.error('Comments container not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the container
|
// Clear the container
|
||||||
commentsContainer.innerHTML = '';
|
commentsContainer.innerHTML = '';
|
||||||
|
|
||||||
// Add the comment form for authenticated users
|
// 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 no comments, show a message
|
||||||
if (!comments || comments.length === 0) {
|
if (!comments || comments.length === 0) {
|
||||||
commentsContainer.innerHTML += `
|
const noCommentsDiv = document.createElement('div');
|
||||||
<div class="alert alert-light mt-3">
|
noCommentsDiv.className = 'alert alert-light mt-3';
|
||||||
<i class="bi bi-chat-dots me-2"></i>
|
noCommentsDiv.innerHTML = `
|
||||||
No comments yet. Be the first to add a comment!
|
<i class="bi bi-chat-dots me-2"></i>
|
||||||
</div>
|
No comments yet. Be the first to add a comment!
|
||||||
`;
|
`;
|
||||||
|
commentsContainer.appendChild(noCommentsDiv);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,12 +451,6 @@ const CommentsManager = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
commentsContainer.appendChild(commentsList);
|
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 {
|
try {
|
||||||
console.log('Deleting comment:', commentId, 'for facility:', facilityId);
|
|
||||||
|
|
||||||
// Use the API client to delete a status comment
|
// Use the API client to delete a status comment
|
||||||
const data = await window.api.deleteFacilityStatus(commentId, facilityId);
|
const data = await window.api.deleteFacilityStatus(commentId, facilityId);
|
||||||
|
|
||||||
console.log('Delete comment response:', data);
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
console.log('Comment deleted successfully');
|
|
||||||
|
|
||||||
// Reload comments to reflect the deletion
|
// Reload comments to reflect the deletion
|
||||||
this.loadFacilityComments(facilityId);
|
this.loadFacilityComments(facilityId);
|
||||||
} else {
|
} else {
|
||||||
@@ -529,22 +515,22 @@ const CommentsManager = {
|
|||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
CommentsManager.state.isDomReady = true;
|
CommentsManager.state.isDomReady = true;
|
||||||
CommentsManager.checkInitialize();
|
CommentsManager.checkinitialise();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
CommentsManager.state.isDomReady = true;
|
CommentsManager.state.isDomReady = true;
|
||||||
CommentsManager.checkInitialize();
|
CommentsManager.checkinitialise();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for simpleAuth ready
|
// Listen for simpleAuth ready
|
||||||
if (window.simpleAuth) {
|
if (window.simpleAuth) {
|
||||||
CommentsManager.state.isAuthReady = true;
|
CommentsManager.state.isAuthReady = true;
|
||||||
CommentsManager.checkInitialize();
|
CommentsManager.checkinitialise();
|
||||||
} else {
|
} else {
|
||||||
window.addEventListener('simpleAuthReady', () => {
|
window.addEventListener('simpleAuthReady', () => {
|
||||||
console.log('SimpleAuth is now ready');
|
console.log('SimpleAuth is now ready');
|
||||||
CommentsManager.state.isAuthReady = true;
|
CommentsManager.state.isAuthReady = true;
|
||||||
CommentsManager.checkInitialize();
|
CommentsManager.checkinitialise();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fallback timeout in case the event doesn't fire
|
// Fallback timeout in case the event doesn't fire
|
||||||
@@ -552,10 +538,10 @@ if (window.simpleAuth) {
|
|||||||
if (!CommentsManager.state.isAuthReady && window.simpleAuth) {
|
if (!CommentsManager.state.isAuthReady && window.simpleAuth) {
|
||||||
console.log('SimpleAuth found via timeout check');
|
console.log('SimpleAuth found via timeout check');
|
||||||
CommentsManager.state.isAuthReady = true;
|
CommentsManager.state.isAuthReady = true;
|
||||||
CommentsManager.checkInitialize();
|
CommentsManager.checkinitialise();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export the CommentsManager to the window object
|
// Export the CommentsManager to the window
|
||||||
window.CommentsManager = CommentsManager;
|
window.CommentsManager = CommentsManager;
|
@@ -5,8 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
function initialiseFacilityData(data, force = false) {
|
function initialiseFacilityData(data, force = false) {
|
||||||
// Only prevent multiple initializations if not forcing
|
// Only prevent multiple initializations if not forcing
|
||||||
if (!force && isInitialized) {
|
if (!force && isinitialised) {
|
||||||
console.debug('Facility data already initialized, skipping...');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,11 +20,9 @@ function initialiseFacilityData(data, force = false) {
|
|||||||
// Check if we're on the map page
|
// Check if we're on the map page
|
||||||
const isMapPage = window.location.pathname.includes('map.php');
|
const isMapPage = window.location.pathname.includes('map.php');
|
||||||
if (!isMapPage) {
|
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');
|
const table = document.querySelector('#facilityTable');
|
||||||
if (!table) {
|
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');
|
throw new Error('Facility table not found in DOM');
|
||||||
}
|
}
|
||||||
// Clear existing table content
|
// Clear existing table content
|
||||||
@@ -33,27 +30,30 @@ function initialiseFacilityData(data, force = false) {
|
|||||||
if (tbody) {
|
if (tbody) {
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
} else {
|
} else {
|
||||||
console.warn('No tbody found in table, creating one');
|
|
||||||
const newTbody = document.createElement('tbody');
|
const newTbody = document.createElement('tbody');
|
||||||
table.appendChild(newTbody);
|
table.appendChild(newTbody);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize filteredData with all data
|
// initialise filteredData with all data
|
||||||
filteredData = data;
|
filteredData = data;
|
||||||
// Calculate total pages
|
// Calculate total pages
|
||||||
totalPages = Math.ceil(filteredData.length / itemsPerPage);
|
totalPages = Math.ceil(filteredData.length / itemsPerPage);
|
||||||
// Set current page to 1
|
// Set current page to 1
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
|
|
||||||
// Update table with paginated data
|
// Reset sorting state
|
||||||
updateTable();
|
currentSortField = null;
|
||||||
|
currentSortOrder = null;
|
||||||
|
|
||||||
// Set up table controls (sorting and filtering)
|
// Set up table controls (sorting and filtering)
|
||||||
setupTableControls();
|
setupTableControls();
|
||||||
|
|
||||||
|
// Update table with paginated data
|
||||||
|
updateTableWithPagination();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as initialized
|
// Mark as initialised
|
||||||
isInitialized = true;
|
isinitialised = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initialising facility data:', error);
|
console.error('Error initialising facility data:', error);
|
||||||
// Don't throw error if we're on map page, as table errors are expected
|
// 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
|
// Check if user is admin
|
||||||
const userIsAdmin = isAdmin();
|
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
|
// Render each row
|
||||||
data.forEach((facility, index) => {
|
data.forEach((facility, index) => {
|
||||||
@@ -98,19 +143,9 @@ function renderFacilityTable(data) {
|
|||||||
// Start building the row HTML
|
// Start building the row HTML
|
||||||
let rowHtml = '';
|
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
|
// Add the rest of the columns
|
||||||
rowHtml += `
|
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="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;">
|
<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>
|
<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>
|
<span class="text-truncate" style="max-width: calc(100% - 35px);">${escapeHtml(facility.title)}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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">
|
<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">
|
<span class="badge bg-${categoryClass} bg-opacity-10 text-${categoryClass} px-2 py-1 rounded-pill">
|
||||||
${escapeHtml(facility.category)}
|
${escapeHtml(facility.category)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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="description-container d-flex flex-column justify-content-center">
|
||||||
<div class="cell-content" data-full-text="${escapeHtml(facility.description)}">
|
<div class="cell-content" data-full-text="${escapeHtml(facility.description)}">
|
||||||
${escapeHtml(facility.description)}
|
${escapeHtml(facility.description)}
|
||||||
@@ -144,13 +179,13 @@ function renderFacilityTable(data) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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">
|
<span class="badge bg-light text-dark border">
|
||||||
${escapeHtml(coordinates)}
|
${escapeHtml(coordinates)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="small text-center align-middle" style="width: 8%;">${escapeHtml(facility.contributor)}</td>
|
<td class="small text-center align-middle" style="width: 5%;">${escapeHtml(facility.contributor)}</td>
|
||||||
<td class="text-center align-middle" style="${userIsAdmin ? 'width: 10%;' : 'width: 5%;'}">
|
<td class="text-center align-middle" style="width: 8%;">
|
||||||
<div class="d-flex justify-content-center gap-1">
|
<div class="d-flex justify-content-center gap-1">
|
||||||
${userIsAdmin ? `
|
${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">
|
<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);
|
tbody.appendChild(emptyRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update sort indicators
|
||||||
|
updateSortIndicators();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
error_log('Error in renderFacilityTable:', error);
|
error_log('Error in renderFacilityTable:', error);
|
||||||
}
|
}
|
||||||
@@ -283,15 +321,19 @@ let filteredData = [];
|
|||||||
let paginationHandler = null;
|
let paginationHandler = null;
|
||||||
|
|
||||||
// Add initialization state tracking
|
// Add initialization state tracking
|
||||||
let isInitialized = false;
|
let isinitialised = false;
|
||||||
|
|
||||||
// Initialize modals once
|
// initialise modals once
|
||||||
let updateModal, deleteModal, createModal;
|
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() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
// Initialize modals once
|
// initialise modals once
|
||||||
const modals = document.querySelectorAll('.modal');
|
const modals = document.querySelectorAll('.modal');
|
||||||
modals.forEach((modal, index) => {
|
modals.forEach((modal, index) => {
|
||||||
if (modal.id === 'updateModal') {
|
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
|
// Set up form handlers with a small delay to ensure DOM is fully loaded
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!formHandlersInitialized) {
|
if (!formHandlersinitialised) {
|
||||||
console.log('Setting up form handlers...');
|
console.log('Setting up form handlers...');
|
||||||
setupFormHandlers();
|
setupFormHandlers();
|
||||||
formHandlersInitialized = true;
|
formHandlersinitialised = true;
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// Initialize facility data if not already initialized
|
// initialise facility data if not already initialised
|
||||||
if (!isInitialized) {
|
if (!isinitialised) {
|
||||||
const storedData = sessionStorage.getItem('facilityData');
|
const storedData = sessionStorage.getItem('facilityData');
|
||||||
if (storedData) {
|
if (storedData) {
|
||||||
try {
|
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
|
// Handle create form submission
|
||||||
@@ -435,6 +494,8 @@ function setupFormHandlers() {
|
|||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
// Set the contributor to the current user's username
|
// Set the contributor to the current user's username
|
||||||
formData.set('contCreate', JSON.parse(localStorage.getItem('user'))?.username);
|
formData.set('contCreate', JSON.parse(localStorage.getItem('user'))?.username);
|
||||||
|
// Set the action to 'create'
|
||||||
|
formData.set('action', 'create');
|
||||||
try {
|
try {
|
||||||
// Use simpleAuth.fetchAuth for authenticated requests
|
// Use simpleAuth.fetchAuth for authenticated requests
|
||||||
const response = await simpleAuth.fetchAuth('/facilitycontroller.php', {
|
const response = await simpleAuth.fetchAuth('/facilitycontroller.php', {
|
||||||
@@ -517,20 +578,30 @@ function setupFormHandlers() {
|
|||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
|
|
||||||
// Create a new FormData with the correct field names for the server
|
// 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();
|
const serverFormData = new FormData();
|
||||||
serverFormData.append('action', 'update');
|
serverFormData.append('action', 'update');
|
||||||
|
|
||||||
// Copy all fields from the form to the server form data
|
// Map form fields to server field names
|
||||||
for (const [key, value] of formData.entries()) {
|
const fieldMappings = {
|
||||||
serverFormData.append(key, value);
|
'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)
|
// Copy and transform fields from the form to the server form data
|
||||||
const contUpdateField = document.getElementById('contUpdate');
|
for (const [key, value] of formData.entries()) {
|
||||||
if (contUpdateField) {
|
if (fieldMappings[key]) {
|
||||||
serverFormData.append('contUpdate', contUpdateField.value);
|
serverFormData.append(fieldMappings[key], value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -801,12 +872,6 @@ function updatePaginationControls() {
|
|||||||
paginationInfo.querySelector('span').textContent = `Showing ${startItem}-${endItem} of ${filteredData.length} facilities`;
|
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) {
|
function createPageNumberElement(pageNum, isActive) {
|
||||||
@@ -862,6 +927,8 @@ function updateTableWithPagination() {
|
|||||||
const pageData = getCurrentPageData();
|
const pageData = getCurrentPageData();
|
||||||
renderFacilityTable(pageData);
|
renderFacilityTable(pageData);
|
||||||
updatePaginationControls();
|
updatePaginationControls();
|
||||||
|
// Update sort indicators after rendering table
|
||||||
|
updateSortIndicators();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -930,14 +997,13 @@ function updateTable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get current filter values
|
// Get current filter values
|
||||||
const sortBy = document.getElementById('sort').value;
|
const searchTerm = document.getElementById('searchInput').value;
|
||||||
const sortDir = document.getElementById('dir').value;
|
|
||||||
const filterCategory = document.getElementById('filterCat').value;
|
|
||||||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
||||||
|
|
||||||
// Apply filters and sorting
|
// Apply filters and sorting
|
||||||
filteredData = filterData(data, filterCategory, searchTerm);
|
filteredData = filterData(data, searchTerm);
|
||||||
filteredData = sortData(filteredData, sortBy, sortDir);
|
if (currentSortField && currentSortOrder) {
|
||||||
|
filteredData = sortData(filteredData, currentSortField, currentSortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
// Update pagination
|
// Update pagination
|
||||||
totalPages = Math.ceil(filteredData.length / itemsPerPage);
|
totalPages = Math.ceil(filteredData.length / itemsPerPage);
|
||||||
@@ -962,12 +1028,9 @@ function setupTableControls() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get control elements
|
// Get control elements
|
||||||
const filterControls = document.querySelectorAll('.filter-control');
|
|
||||||
const sortControls = document.querySelectorAll('.sort-control');
|
|
||||||
const searchInput = document.getElementById('searchInput');
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
if (!searchInput) {
|
||||||
if (!filterControls.length || !sortControls.length || !searchInput) {
|
error_log('Missing search input');
|
||||||
error_log('Missing filter or sort controls');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -978,66 +1041,142 @@ function setupTableControls() {
|
|||||||
updateTable();
|
updateTable();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add event listeners for immediate updates
|
// Add event listener for search input
|
||||||
searchInput.addEventListener('input', updateTable);
|
searchInput.addEventListener('input', updateTable);
|
||||||
|
|
||||||
// Add change event listeners for select elements
|
// Set up table headers for sorting
|
||||||
filterControls.forEach(control => {
|
setupSortableHeaders();
|
||||||
control.addEventListener('change', updateTable);
|
|
||||||
});
|
|
||||||
|
|
||||||
sortControls.forEach(control => {
|
|
||||||
control.addEventListener('change', updateTable);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up pagination controls
|
// Set up pagination controls
|
||||||
setupPaginationControls();
|
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 {Array} data - Array of facility objects
|
||||||
* @param {string} category - Filter category
|
|
||||||
* @param {string} searchTerm - Search term
|
* @param {string} searchTerm - Search term
|
||||||
* @returns {Array} Filtered array of facility objects
|
* @returns {Array} Filtered array of facility objects
|
||||||
*/
|
*/
|
||||||
function filterData(data, category, searchTerm) {
|
function filterData(data, searchTerm) {
|
||||||
const filtered = data.filter(facility => {
|
const filtered = data.filter(facility => {
|
||||||
if (!facility) return false;
|
if (!facility) return false;
|
||||||
|
|
||||||
// If no category selected or no search term, show all results
|
// If no search term, show all results
|
||||||
if (!category || !searchTerm) return true;
|
if (!searchTerm) return true;
|
||||||
|
|
||||||
// Get the value to search in based on the selected category
|
// Convert search term to lowercase for case-insensitive search
|
||||||
let searchValue = '';
|
searchTerm = searchTerm.toLowerCase();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
return filtered;
|
||||||
}
|
}
|
||||||
@@ -1051,51 +1190,50 @@ function filterData(data, category, searchTerm) {
|
|||||||
*/
|
*/
|
||||||
function sortData(data, sortBy, sortDir) {
|
function sortData(data, sortBy, sortDir) {
|
||||||
if (!sortBy) return data;
|
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) => {
|
return [...data].sort((a, b) => {
|
||||||
if (!a || !b) return 0;
|
if (!a || !b) return 0;
|
||||||
|
|
||||||
let valueA = '';
|
let valueA, valueB;
|
||||||
let valueB = '';
|
|
||||||
|
|
||||||
// Get the values to compare based on the selected field
|
// Special handling for address field which is composed of multiple fields
|
||||||
switch (sortBy) {
|
if (sortBy === 'address') {
|
||||||
case 'title':
|
valueA = [
|
||||||
valueA = (a.title || '').toLowerCase();
|
a.houseNumber || '',
|
||||||
valueB = (b.title || '').toLowerCase();
|
a.streetName || '',
|
||||||
break;
|
a.town || '',
|
||||||
case 'category':
|
a.county || '',
|
||||||
valueA = (a.category || '').toLowerCase();
|
a.postcode || ''
|
||||||
valueB = (b.category || '').toLowerCase();
|
].filter(Boolean).join(', ').toLowerCase();
|
||||||
break;
|
|
||||||
case 'description':
|
valueB = [
|
||||||
valueA = (a.description || '').toLowerCase();
|
b.houseNumber || '',
|
||||||
valueB = (b.description || '').toLowerCase();
|
b.streetName || '',
|
||||||
break;
|
b.town || '',
|
||||||
case 'streetName':
|
b.county || '',
|
||||||
valueA = (a.streetName || '').toLowerCase();
|
b.postcode || ''
|
||||||
valueB = (b.streetName || '').toLowerCase();
|
].filter(Boolean).join(', ').toLowerCase();
|
||||||
break;
|
}
|
||||||
case 'county':
|
// Special handling for coordinates field
|
||||||
valueA = (a.county || '').toLowerCase();
|
else if (sortBy === 'coordinates') {
|
||||||
valueB = (b.county || '').toLowerCase();
|
// Sort by latitude first, then longitude
|
||||||
break;
|
valueA = `${parseFloat(a.lat || 0)},${parseFloat(a.lng || 0)}`;
|
||||||
case 'town':
|
valueB = `${parseFloat(b.lat || 0)},${parseFloat(b.lng || 0)}`;
|
||||||
valueA = (a.town || '').toLowerCase();
|
}
|
||||||
valueB = (b.town || '').toLowerCase();
|
// Default handling for other fields
|
||||||
break;
|
else {
|
||||||
case 'postcode':
|
valueA = (a[sortBy] || '').toString().toLowerCase();
|
||||||
valueA = (a.postcode || '').toLowerCase();
|
valueB = (b[sortBy] || '').toString().toLowerCase();
|
||||||
valueB = (b.postcode || '').toLowerCase();
|
|
||||||
break;
|
|
||||||
case 'contributor':
|
|
||||||
valueA = (a.contributor || '').toLowerCase();
|
|
||||||
valueB = (b.contributor || '').toLowerCase();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const comparison = valueA.localeCompare(valueB);
|
console.log('Comparing:', valueA, valueB); // Debug log
|
||||||
return sortDir === 'asc' ? comparison : -comparison;
|
|
||||||
|
// 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
|
// Export the initialization function
|
||||||
window.initializeFacilityData = initialiseFacilityData;
|
window.initialiseFacilityData = initialiseFacilityData;
|
781
public/js/mapHandler.js
Normal file
781
public/js/mapHandler.js
Normal 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, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
class SimpleAuth {
|
class SimpleAuth {
|
||||||
/**
|
/**
|
||||||
* Initialize the authentication helper
|
* initialise the authentication helper
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this.token = localStorage.getItem('token');
|
this.token = localStorage.getItem('token');
|
||||||
@@ -25,12 +25,6 @@ class SimpleAuth {
|
|||||||
console.warn('Browser fingerprint mismatch - clearing authentication');
|
console.warn('Browser fingerprint mismatch - clearing authentication');
|
||||||
this.logout(false); // Silent logout (no redirect)
|
this.logout(false); // Silent logout (no redirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log initialization
|
|
||||||
console.log('SimpleAuth initialized:', {
|
|
||||||
isAuthenticated: this.isAuthenticated(),
|
|
||||||
user: this.user
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user