Sportski objekti: API + Leaflet map page + address enrichment

DB: pgz_sport.sportski_objekti (103 objekti, 103 s geo, 60 s adresom, 31 tip)

API:
- /api/v2/sportski-objekti (filter: tip, grad, sport, q)
- /api/v2/sportski-objekti/meta (tipovi, gradovi, sportovi, ukupno)

Frontend:
- /static/objekti.html — Leaflet (OpenStreetMap) interactive map
- 3 dropdown filter (tip, grad, sport) + search
- Side panel s listom + map markers s ikonama (🏟️🏊🎿🎳⛸️🎯🥌🏃)
- Popup: naziv, tip, kapacitet, adresa, upravitelj, izgradeno, sportovi, web link, Google Maps link
- /objekti, /sport/objekti, /sport/api/v2/sportski-objekti routes

Sidebar app.html: +Sportski objekti link
Background: scripts/objekti_enrich_address.py (Nominatim reverse-geocode 60 objekata bez adrese)
This commit is contained in:
2026-05-05 18:35:04 +02:00
parent 6e5ada8517
commit ae9c4e2bfd
15 changed files with 767 additions and 4 deletions
+50
View File
@@ -2835,6 +2835,56 @@ def auth_me_v2_alias(authorization: str = Header(None)):
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/v2/sportski-objekti")
def sportski_objekti_v2_list(tip: str = None, grad: str = None, sport: str = None, q: str = None, limit: int = 500):
"""Sportski objekti PGŽ s filterima."""
where = ["aktivan = true"]
params = []
if tip:
where.append("tip = %s"); params.append(tip)
if grad:
where.append("grad = %s"); params.append(grad)
if sport:
where.append("%s = ANY(sportovi)"); params.append(sport)
if q:
where.append("(naziv ILIKE %s OR adresa ILIKE %s OR upravitelj ILIKE %s)")
params.extend([f"%{q}%"]*3)
rows = fetch(f"""
SELECT id, naziv, tip, grad, adresa, lat, lng, upravitelj, kapacitet,
sportovi, izgradeno, obnovljeno_god, "veličina" AS velicina, natkrita,
napomena, web
FROM pgz_sport.sportski_objekti
WHERE {' AND '.join(where)}
ORDER BY grad, naziv
LIMIT %s
""", tuple(params) + (limit,))
return {"count": len(rows), "rows": rows}
@app.get("/api/v2/sportski-objekti/meta")
def sportski_objekti_meta():
"""Dropdown options za filter."""
tipovi = fetch("SELECT tip, count(*) AS broj FROM pgz_sport.sportski_objekti WHERE aktivan = true AND tip IS NOT NULL GROUP BY tip ORDER BY broj DESC")
gradovi = fetch("SELECT grad, count(*) AS broj FROM pgz_sport.sportski_objekti WHERE aktivan = true AND grad IS NOT NULL GROUP BY grad ORDER BY broj DESC")
sportovi = fetch("SELECT DISTINCT unnest(sportovi) AS sport, count(*) AS broj FROM pgz_sport.sportski_objekti WHERE aktivan = true AND sportovi IS NOT NULL GROUP BY sport ORDER BY broj DESC LIMIT 50")
return {
"tipovi": tipovi,
"gradovi": gradovi,
"sportovi": sportovi,
"ukupno": (fetch("SELECT count(*) AS n FROM pgz_sport.sportski_objekti WHERE aktivan = true")[0])["n"]
}
@app.get("/objekti")
@app.get("/objekti/")
@app.get("/sport/objekti")
@app.get("/sport/objekti/")
def serve_objekti():
from fastapi.responses import FileResponse
return FileResponse("/opt/pgz-sport/static/objekti.html")
@app.get("/")
def root(request: Request):
host = request.headers.get("host", "")