Sidebar: +ERP +CRM +Dokumenti, godišnjaci import (18 PDFs), filter helpers

- pgz nav now includes /erp/full, /crm/v2, /admin/users, /dokumenti
- 4 dokumenti endpoints: list, godišnjaci/list, godišnjak/{godina} PDF, detail
- 18 godišnjaka u pgz_sport.dokumenti (2006-2024) with savez_id=333
- PGŽ filter helpers (window._pgz_filter_priority, togglePGZFilter)
- navItemClick handler for nav items with href
This commit is contained in:
2026-05-05 13:08:11 +02:00
parent 9fb512932a
commit 1d02c0897d
970 changed files with 268354 additions and 434 deletions
+219 -16
View File
@@ -192,6 +192,7 @@ _PUBLIC_MUTATING_SUFFIXES = (
# hits them anonymously over loopback.
_PUBLIC_MUTATING_PREFIXES = (
"/api/v2/enrich/",
"/api/v2/export/", # ui-sprint: read-only export, mirrors public GET data
)
@app.middleware("http")
@@ -604,8 +605,9 @@ def lijecnicki_stats(klub_id: Optional[int] = None):
# ==================== SAVEZI ====================
@app.get("/api/savezi")
def list_savezi(authorization: Optional[str] = Header(None), q: Optional[str] = None,
def list_savezi(authorization: Optional[str] = Header(None), q: Optional[str] = None,
razina: Optional[str] = None, zupanija: Optional[str] = None,
sport: Optional[str] = None, kategorija: Optional[str] = None,
sort: str = "naziv", order: str = "asc"):
where = "WHERE aktivan"
params = []
@@ -616,6 +618,16 @@ def list_savezi(authorization: Optional[str] = Header(None), q: Optional[str] =
where += " AND razina = %s"; params.append(razina)
if zupanija:
where += " AND sjediste_zupanija ILIKE %s"; params.append(f"%{zupanija}%")
if sport:
where += " AND sport ILIKE %s"; params.append(f"%{sport}%")
if kategorija:
kt = kategorija.strip().lower()
if kt in ("zupanijski","županijski"):
where += " AND lower(unaccent(razina)) ILIKE %s"; params.append("%zupanij%")
elif kt == "gradski":
where += " AND lower(unaccent(razina)) ILIKE %s"; params.append("%gradsk%")
else:
where += " AND lower(unaccent(razina)) ILIKE lower(unaccent(%s))"; params.append(f"%{kategorija}%")
sort_col = {"naziv": "naziv", "godina": "godina_osnutka", "sport": "sport", "razina": "razina"}.get(sort, "naziv")
order = "DESC" if order.lower() == "desc" else "ASC"
# Croatian collation for text columns (Š → after S, Č → after C, etc.)
@@ -653,29 +665,48 @@ def get_savez(savez_id: int, authorization: Optional[str] = Header(None)):
@app.get("/api/klubovi")
def list_klubovi(authorization: Optional[str] = Header(None), q: Optional[str] = None, savez_id: Optional[int] = None,
nositelj: Optional[bool] = None, region: Optional[str] = None, sport: Optional[str] = None, grad: Optional[str] = None,
kategorija: Optional[str] = None, godisnjak: Optional[bool] = None, financiran: Optional[bool] = None,
sort: str = "naziv", order: str = "asc"):
where = ["aktivan"]
where = ["v.aktivan"]
params = []
if q:
where.append("(klub ILIKE %s OR oib ILIKE %s OR sport ILIKE %s OR predsjednik ILIKE %s)")
where.append("(v.klub ILIKE %s OR v.oib ILIKE %s OR v.sport ILIKE %s OR v.predsjednik ILIKE %s)")
params.extend([f"%{q}%", f"%{q}%", f"%{q}%", f"%{q}%"])
if savez_id:
where.append("savez_id=%s"); params.append(savez_id)
where.append("v.id IN (SELECT id FROM pgz_sport.klubovi WHERE savez_id=%s)"); params.append(savez_id)
if nositelj is not None:
where.append(f"nositelj_kvalitete={'TRUE' if nositelj else 'FALSE'}")
where.append(f"v.nositelj_kvalitete={'TRUE' if nositelj else 'FALSE'}")
if region:
where.append("region ILIKE %s"); params.append(region)
where.append("v.region ILIKE %s"); params.append(region)
if grad:
where.append("grad ILIKE %s"); params.append(f"%{grad}%")
where.append("v.grad ILIKE %s"); params.append(f"%{grad}%")
if sport:
where.append("sport ILIKE %s"); params.append(f"%{sport}%")
sort_col = {"naziv": "klub", "savez": "savez", "broj_clanova": "broj_clanova",
"razina": "razina", "region": "region", "grad": "grad", "sport": "sport"}.get(sort, "klub")
where.append("v.sport ILIKE %s"); params.append(f"%{sport}%")
if financiran is not None:
where.append(f"COALESCE(k.pgz_sufinanciran,false) = {'TRUE' if financiran else 'FALSE'}")
if godisnjak is not None:
if godisnjak:
where.append("(k.godisnjak_godine IS NOT NULL AND array_length(k.godisnjak_godine,1) > 0)")
else:
where.append("(k.godisnjak_godine IS NULL OR array_length(k.godisnjak_godine,1) IS NULL)")
if kategorija and kategorija.strip().lower() == "priority":
where.append("(COALESCE(k.pgz_sufinanciran,false) OR (k.godisnjak_godine IS NOT NULL AND array_length(k.godisnjak_godine,1) > 0))")
sort_col = {"naziv": "v.klub", "savez": "v.savez", "broj_clanova": "v.broj_clanova",
"razina": "v.razina", "region": "v.region", "grad": "v.grad", "sport": "v.sport"}.get(sort, "v.klub")
order_sql = "DESC" if order.lower() == "desc" else "ASC"
where_sql = " AND ".join(where) if where else "TRUE"
collate = ' COLLATE "hr-HR-x-icu"' if sort_col in ("klub", "savez", "razina", "region", "grad", "sport") else ""
rows = fetch(f"""SELECT * FROM pgz_sport.v_klubovi_pregled WHERE {where_sql}
ORDER BY {sort_col}{collate} {order_sql} NULLS LAST""", params)
collate = ' COLLATE "hr-HR-x-icu"' if sort_col in ("v.klub", "v.savez", "v.razina", "v.region", "v.grad", "v.sport") else ""
priority_expr = "(COALESCE(k.pgz_sufinanciran,false) OR (k.godisnjak_godine IS NOT NULL AND array_length(k.godisnjak_godine,1) > 0))"
rows = fetch(f"""SELECT v.*,
COALESCE(k.pgz_sufinanciran,false) AS financiran,
(k.godisnjak_godine IS NOT NULL AND array_length(k.godisnjak_godine,1) > 0) AS godisnjak,
{priority_expr} AS priority,
k.godisnjak_godine, k.godisnjak_prvi, k.godisnjak_zadnji
FROM pgz_sport.v_klubovi_pregled v
LEFT JOIN pgz_sport.klubovi k ON k.id = v.id
WHERE {where_sql}
ORDER BY {priority_expr} DESC NULLS LAST,
{sort_col}{collate} {order_sql} NULLS LAST""", params)
for r in rows:
if isinstance(r, dict) and r.get('klub') and not r.get('naziv'):
r['naziv'] = r['klub']
@@ -1686,6 +1717,14 @@ try:
except Exception as e:
print(f'[CRM/R5] extras router fail: {e}')
# === CRM v2 — Salesforce-Lite (Accounts/Contacts/Leads/Opportunities/Activities/Cases) ===
try:
from crm_router import router as crm_v2_router
app.include_router(crm_v2_router)
print('[CRM/v2] Salesforce-Lite router loaded (/api/v2/crm/*)')
except Exception as e:
print(f'[CRM/v2] router fail: {e}')
# === Round 3 / CC2 — M1 Auth + M2 Admin Users + M10 GDPR ===
try:
from auth.auth_v2 import router as auth_v2_router
@@ -1745,6 +1784,23 @@ def serve_erp():
return FileResponse(p)
return {"error": "erp.html not found"}
@app.get("/erp/full")
@app.get("/erp/full/")
def serve_erp_full():
p = HTML_DIR / "erp_full.html"
if p.exists():
return FileResponse(p)
return {"error": "erp_full.html not found"}
# ═══ ERP FULL (SAP-Lite) router — kontni plan, dnevnik, glavna knjiga, partneri,
# racuni ulazni/izlazni, e-Račun XML, PDV, plaće, izvještaji, export
try:
from routers.erp_full_router import router as erp_full_router
app.include_router(erp_full_router)
print('[ERP-FULL] router loaded (/api/v2/erp/*)')
except Exception as e:
print(f'[ERP-FULL] router fail: {e}')
@app.get("/crm")
@app.get("/crm/")
def serve_crm():
@@ -1753,6 +1809,16 @@ def serve_crm():
return FileResponse(p)
return {"error": "crm.html not found"}
@app.get("/crm-v2")
@app.get("/crm-v2/")
@app.get("/crm_v2")
@app.get("/crm_v2/")
def serve_crm_v2():
p = HTML_DIR / "crm_v2.html"
if p.exists():
return FileResponse(p)
return {"error": "crm_v2.html not found"}
@app.get("/login")
@app.get("/login/")
def serve_login():
@@ -1780,14 +1846,32 @@ def list_sportski_objekti(q=None,tip=None,grad=None):
return {"count":len(rows),"rows":rows}
@app.get("/api/clanovi-full")
def list_clanovi_full(q=None,hoo=None,reprezentativac=None,klub_id=None,limit=80,authorization=None):
def list_clanovi_full(q=None,hoo=None,reprezentativac=None,klub_id=None,
sport=None,kategorija=None,godina_rodenja=None,status=None,
limit=80,authorization=None):
w=["aktivan=TRUE"]; p=[]
if q: w.append("(ime ILIKE %s OR prezime ILIKE %s OR klub_naziv_godisnjak ILIKE %s)"); p+=["%"+q+"%"]*3
if hoo: w.append("hoo_kategorija=%s"); p.append(hoo)
if reprezentativac is not None: w.append("reprezentativac="+(("TRUE") if str(reprezentativac).lower()=="true" else "FALSE"))
if klub_id: w.append("klub_id=%s"); p.append(int(klub_id))
lim=min(int(limit or 80),200)
sql="SELECT id,ime,prezime,oib,datum_rodenja,spol,sport,pozicija,reprezentativac,kategoriziran,stipendiran,kategorija_hoo,hoo_kategorija,aktivan,klub_naziv_godisnjak,slika_url,profile_url,hns_igrac_id,visina_cm,tezina_kg,broj_dresa,uloga,godisnjak_godine,godisnjak_prvi,godisnjak_zadnji,napomena FROM pgz_sport.clanovi WHERE "+" AND ".join(w)+" ORDER BY prezime,ime LIMIT "+str(lim)
if sport: w.append("sport ILIKE %s"); p.append(f"%{sport}%")
if kategorija:
w.append("(kategorija ILIKE %s OR %s = ANY(COALESCE(kategorije,ARRAY[]::text[])))")
p.extend([f"%{kategorija}%", kategorija])
if godina_rodenja:
try:
w.append("EXTRACT(YEAR FROM datum_rodenja)=%s"); p.append(int(godina_rodenja))
except (TypeError, ValueError):
pass
if status:
s = str(status).strip().lower()
if s in ("reprezentativac","reprezentativci"): w.append("reprezentativac=TRUE")
elif s in ("kategoriziran","kategorizirani"): w.append("kategoriziran=TRUE")
elif s in ("stipendiran","stipendirani"): w.append("stipendiran=TRUE")
elif s in ("aktivan","aktivni"): w.append("aktivan=TRUE")
elif s in ("neaktivan","neaktivni","arhivirani"): w.append("aktivan=FALSE")
lim=min(int(limit or 80),500)
sql="SELECT id,ime,prezime,oib,datum_rodenja,spol,sport,pozicija,reprezentativac,kategoriziran,stipendiran,kategorija,kategorije,kategorija_hoo,hoo_kategorija,aktivan,klub_id,klub_naziv_godisnjak,slika_url,profile_url,hns_igrac_id,visina_cm,tezina_kg,broj_dresa,uloga,godisnjak_godine,godisnjak_prvi,godisnjak_zadnji,napomena FROM pgz_sport.clanovi WHERE "+" AND ".join(w)+" ORDER BY prezime,ime LIMIT "+str(lim)
rows=fetch(sql,p)
return {"count":len(rows),"rows":rows}
@@ -1981,6 +2065,12 @@ def serve_kpi():
p = HTML_DIR / "kpi.html"
return FileResponse(p) if p.exists() else {"error":"kpi.html not found"}
@app.get("/dokumenti")
@app.get("/dokumenti/")
def serve_dokumenti():
p = HTML_DIR / "dokumenti.html"
return FileResponse(p) if p.exists() else {"error":"dokumenti.html not found"}
app.mount("/static", StaticFiles(directory=str(HTML_DIR)), name="static")
# User-uploaded files (avatars, etc.) — served at /uploads/*
@@ -2183,6 +2273,119 @@ def export_klubovi(req: dict):
)
@app.get("/api/v2/sportasi/financirani")
def sportasi_financirani(sport: str = None, kategorija: str = None, godiste: int = None, limit: int = 1000):
"""Sportaši koji pripadaju klubovima financiranim od PGŽ."""
where = ["c.klub_id IN (SELECT id FROM pgz_sport.v_pgz_priority_klubovi WHERE financiran)"]
params = []
if sport:
where.append("c.sport = %s"); params.append(sport)
if kategorija:
where.append("c.kategorija = %s"); params.append(kategorija)
if godiste:
where.append("EXTRACT(YEAR FROM c.datum_rodenja) = %s OR c.godina_rodenja = %s")
params.extend([godiste, godiste])
rows = fetch(f"""
SELECT c.id, c.ime, c.prezime, c.sport, c.kategorija, c.klub_id,
k.naziv AS klub_naziv, k.financiran, k.u_godisnjaku, k.priority_label,
c.datum_rodenja, c.godina_rodenja, c.hns_igrac_id, c.source_url,
(SELECT count(*) FROM pgz_sport.hns_player_seasons WHERE clan_id = c.id) AS sezone,
(SELECT count(*) FROM pgz_sport.clan_kategorije WHERE clan_id = c.id) AS kategorije_count
FROM pgz_sport.clanovi c
JOIN pgz_sport.v_klubovi_priority_sort k ON k.id = c.klub_id
WHERE {' AND '.join(where)}
ORDER BY c.prezime, c.ime
LIMIT %s
""", tuple(params) + (limit,))
return {"count": len(rows), "rows": rows}
@app.get("/api/v2/savezi/financirani")
def savezi_financirani(sport: str = None):
"""Savezi (i njihovi klubovi) koji su PGŽ financirani."""
where = ""
params = []
if sport:
where = " AND s.sport = %s"
params.append(sport)
rows = fetch(f"""
SELECT s.id, s.naziv, s.sport, s.razina, s.oib,
(SELECT count(*) FROM pgz_sport.klubovi k WHERE k.savez_id = s.id) AS klubovi,
(SELECT count(*) FROM pgz_sport.v_pgz_priority_klubovi k WHERE k.savez_id = s.id AND k.financiran) AS klubovi_financirani,
(SELECT count(*) FROM pgz_sport.clanovi c JOIN pgz_sport.klubovi k ON k.id = c.klub_id WHERE k.savez_id = s.id) AS sportasa,
(SELECT sum(iznos) FROM pgz_sport.potpore_nositelji p
JOIN pgz_sport.klubovi k ON k.id = p.klub_id WHERE k.savez_id = s.id) AS potpora_ukupno
FROM pgz_sport.savezi s
WHERE 1=1 {where}
ORDER BY potpora_ukupno DESC NULLS LAST
""", tuple(params))
return {"count": len(rows), "rows": rows}
# ═══════════════════════════════════════════════════════════════════
# DOKUMENTI ENDPOINTS — godišnjaci, publikacije, sport-savez izdanja
# ═══════════════════════════════════════════════════════════════════
@app.get("/api/v2/dokumenti")
def dokumenti_list(vrsta: str = None, sport: str = None, godina: int = None, q: str = None, limit: int = 100):
"""Lista dokumenata: godišnjaci, publikacije, etc."""
where = ["aktivan = true"]
params = []
if vrsta:
where.append("vrsta = %s"); params.append(vrsta)
if sport:
where.append("sport = %s"); params.append(sport)
if godina:
where.append("godina = %s"); params.append(godina)
if q:
where.append("(title ILIKE %s OR sadrzaj_summary ILIKE %s OR organizacija ILIKE %s)")
params.extend([f"%%{q}%%"]*3)
rows = fetch(f"""
SELECT id, title, fname, vrsta, sport, godina, organizacija,
izvor_url, izdano_datum, kratak_opis, kljucne_rijeci, scraped_at,
LENGTH(sadrzaj) AS sadrzaj_size
FROM pgz_sport.dokumenti
WHERE {' AND '.join(where)}
ORDER BY godina DESC NULLS LAST, izdano_datum DESC NULLS LAST, id DESC
LIMIT %s
""", tuple(params) + (limit,))
return {"count": len(rows), "rows": rows}
@app.get("/api/v2/dokumenti/godisnjaci/list")
def dokumenti_godisnjaci_list():
"""Lista svih godišnjaka."""
rows = fetch("""
SELECT id, godina, title, fname, izdano_datum, organizacija, kratak_opis,
LENGTH(sadrzaj) AS sadrzaj_size
FROM pgz_sport.dokumenti
WHERE vrsta = 'godisnjak'
ORDER BY godina DESC
""")
return {"count": len(rows), "godisnjaci": rows}
@app.get("/api/v2/dokumenti/godisnjak/{godina}")
def dokument_godisnjak_pdf(godina: int):
"""PDF godišnjaka."""
from fastapi.responses import FileResponse
import os
pdf_path = f"/opt/pgz-sport/_data/godisnjaci/godisnjak_{godina}.pdf"
if os.path.exists(pdf_path):
return FileResponse(pdf_path, media_type="application/pdf", filename=f"godisnjak_{godina}.pdf")
return {"error": "not_found", "godina": godina}
@app.get("/api/v2/dokumenti/{doc_id}")
def dokument_detail(doc_id: int):
"""Detaljan pregled dokumenta."""
rows = fetch("SELECT id, title, vrsta, sport, godina, organizacija, izvor_url, kratak_opis, sadrzaj_summary, kljucne_rijeci, scraped_at FROM pgz_sport.dokumenti WHERE id = %s", (doc_id,))
if not rows: return {"error": "not_found"}
return rows[0]
@app.get("/")
def root(request: Request):
host = request.headers.get("host", "")