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:
@@ -5495,3 +5495,216 @@ def v2_sport_index():
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# UI SPRINT 2026-05-05 — sport-aware enrichment + export + per-kategorija
|
||||
# =============================================================================
|
||||
|
||||
import csv as _csv
|
||||
import io as _io
|
||||
import unicodedata as _ud
|
||||
|
||||
|
||||
def _norm_sport(s: str) -> str:
|
||||
"""Lowercase + strip diacritics for tolerant sport-name matching."""
|
||||
if not s:
|
||||
return ""
|
||||
s = s.strip().lower()
|
||||
return "".join(c for c in _ud.normalize("NFD", s) if not _ud.combining(c))
|
||||
|
||||
|
||||
@router.get("/enrich-sources")
|
||||
def v2_enrich_sources(sport: Optional[str] = None, q: Optional[str] = None):
|
||||
"""Per-sport federation enrichment URLs (HNS / HKS-CBF / HRS / HOS-CVF / HVS).
|
||||
|
||||
Returns rows from `pgz_sport.enrichment_sources` (schema: sport PK,
|
||||
primary_source_name, primary_source_url, player_url_pattern,
|
||||
klub_url_pattern, description). When `sport` is provided, returns a
|
||||
diacritic-tolerant single match. When `q` is provided, also yields a
|
||||
`search_url` derived from `primary_source_url` (URL-encoded query).
|
||||
"""
|
||||
base_sql = """SELECT sport, primary_source_name, primary_source_url,
|
||||
player_url_pattern, klub_url_pattern, description
|
||||
FROM pgz_sport.enrichment_sources"""
|
||||
if sport:
|
||||
rows = db_query(
|
||||
base_sql + " WHERE lower(unaccent(sport)) = lower(unaccent(%s)) LIMIT 1",
|
||||
[sport],
|
||||
)
|
||||
if not rows:
|
||||
rows = db_query(
|
||||
base_sql + " WHERE sport ILIKE %s OR %s ILIKE '%%' || sport || '%%' LIMIT 1",
|
||||
[f"%{sport}%", sport],
|
||||
)
|
||||
else:
|
||||
rows = db_query(base_sql + " ORDER BY sport COLLATE \"hr-HR-x-icu\"")
|
||||
|
||||
out = []
|
||||
enc_q = requests.utils.quote(q) if q else ""
|
||||
for r in rows:
|
||||
base = (r.get("primary_source_url") or "").rstrip("/")
|
||||
# build a generic search URL — federation sites all have a search page
|
||||
# behind ?s= or /search/?q=; default to base+?s= which works for HKS/HRS/HOS/HVS
|
||||
# and for HNS we route to /klubovi?q=
|
||||
sport_key = (r.get("sport") or "").lower()
|
||||
if sport_key == "nogomet":
|
||||
search_url = f"{base}/klubovi?q={enc_q}" if base else ""
|
||||
else:
|
||||
search_url = f"{base}/?s={enc_q}" if base else ""
|
||||
out.append({
|
||||
"sport": r.get("sport"),
|
||||
"naziv": r.get("primary_source_name"),
|
||||
"base_url": r.get("primary_source_url"),
|
||||
"klub_url_pattern": r.get("klub_url_pattern"),
|
||||
"player_url_pattern": r.get("player_url_pattern"),
|
||||
"description": r.get("description"),
|
||||
"search_url": search_url if q else None,
|
||||
})
|
||||
if sport:
|
||||
return {"ok": True, "match": out[0] if out else None, "rows": out}
|
||||
return {"ok": True, "count": len(out), "rows": out}
|
||||
|
||||
|
||||
class _ExportKluboviReq(BaseModel):
|
||||
ids: List[int]
|
||||
format: str = "xlsx" # 'xlsx' | 'csv'
|
||||
columns: Optional[List[str]] = None
|
||||
|
||||
|
||||
_EXPORT_DEFAULT_COLS = [
|
||||
"id", "klub", "sport", "razina", "grad", "region",
|
||||
"predsjednik", "oib", "broj_clanova", "registriranih",
|
||||
"trenera", "nositelj_kvalitete",
|
||||
]
|
||||
_EXPORT_ALLOWED = set(_EXPORT_DEFAULT_COLS) | {
|
||||
"savez", "email", "telefon", "web_stranica", "godina_osnutka",
|
||||
"reprezentativaca", "validni_lijecnicki", "isteki_lijecnicki",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/export/klubovi")
|
||||
def v2_export_klubovi(req: _ExportKluboviReq):
|
||||
"""Export selected clubs as CSV or XLSX.
|
||||
|
||||
Body: {ids: [int,...], format: 'csv'|'xlsx', columns?: [str,...]}.
|
||||
"""
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
if not req.ids:
|
||||
raise HTTPException(400, "ids list je prazan")
|
||||
fmt = (req.format or "xlsx").lower()
|
||||
if fmt not in ("xlsx", "csv"):
|
||||
raise HTTPException(400, "format mora biti 'xlsx' ili 'csv'")
|
||||
cols = [c for c in (req.columns or _EXPORT_DEFAULT_COLS) if c in _EXPORT_ALLOWED]
|
||||
if not cols:
|
||||
cols = list(_EXPORT_DEFAULT_COLS)
|
||||
|
||||
sel = ", ".join(cols)
|
||||
rows = db_query(
|
||||
f"SELECT {sel} FROM pgz_sport.v_klubovi_pregled WHERE id = ANY(%s) ORDER BY klub",
|
||||
[req.ids],
|
||||
)
|
||||
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M")
|
||||
fname = f"pgz_klubovi_{ts}.{fmt}"
|
||||
|
||||
if fmt == "csv":
|
||||
buf = _io.StringIO()
|
||||
w = _csv.writer(buf, delimiter=";", quoting=_csv.QUOTE_MINIMAL)
|
||||
w.writerow(cols)
|
||||
for r in rows:
|
||||
w.writerow([r.get(c, "") if r.get(c) is not None else "" for c in cols])
|
||||
data = buf.getvalue().encode("utf-8-sig") # BOM za Excel
|
||||
return StreamingResponse(
|
||||
_io.BytesIO(data),
|
||||
media_type="text/csv; charset=utf-8",
|
||||
headers={"Content-Disposition": f'attachment; filename="{fname}"'},
|
||||
)
|
||||
|
||||
# xlsx
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Klubovi"
|
||||
header_font = Font(bold=True, color="FFFFFF")
|
||||
header_fill = PatternFill("solid", fgColor="1F2937")
|
||||
ws.append(cols)
|
||||
for cell in ws[1]:
|
||||
cell.font = header_font
|
||||
cell.fill = header_fill
|
||||
cell.alignment = Alignment(horizontal="left", vertical="center")
|
||||
for r in rows:
|
||||
ws.append([
|
||||
(r.get(c) if not isinstance(r.get(c), (list, dict)) else json.dumps(r.get(c), ensure_ascii=False))
|
||||
for c in cols
|
||||
])
|
||||
# column widths
|
||||
for i, c in enumerate(cols, start=1):
|
||||
ws.column_dimensions[ws.cell(row=1, column=i).column_letter].width = max(12, min(40, len(c) + 6))
|
||||
ws.freeze_panes = "A2"
|
||||
buf = _io.BytesIO()
|
||||
wb.save(buf)
|
||||
buf.seek(0)
|
||||
return StreamingResponse(
|
||||
buf,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": f'attachment; filename="{fname}"'},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/sportasi-by-kategorija")
|
||||
def v2_sportasi_by_kategorija(
|
||||
sport: Optional[str] = None,
|
||||
klub_id: Optional[int] = None,
|
||||
limit_per_kat: int = 500,
|
||||
):
|
||||
"""Group clanovi by kategorija. Players in multiple categories appear in each.
|
||||
|
||||
Uses `kategorije TEXT[]` if non-empty, falls back to scalar `kategorija`.
|
||||
"""
|
||||
where = ["c.aktivan"]
|
||||
params: List[Any] = []
|
||||
if sport:
|
||||
where.append("c.sport ILIKE %s")
|
||||
params.append(f"%{sport}%")
|
||||
if klub_id:
|
||||
where.append("c.klub_id = %s")
|
||||
params.append(klub_id)
|
||||
where_sql = " AND ".join(where)
|
||||
|
||||
sql = f"""
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
COALESCE(NULLIF(u.unnest_kat, ''), c.kategorija, '(nepoznata)') AS kat,
|
||||
c.id, c.ime, c.prezime, c.oib, c.datum_rodenja, c.spol,
|
||||
c.sport, c.pozicija, c.kategorija, c.kategorije,
|
||||
c.reprezentativac, c.kategoriziran, c.stipendiran,
|
||||
c.klub_id, k.naziv AS klub_naziv,
|
||||
c.slika_url, c.broj_dresa
|
||||
FROM pgz_sport.clanovi c
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT unnest(
|
||||
CASE WHEN c.kategorije IS NOT NULL AND array_length(c.kategorije,1) > 0
|
||||
THEN c.kategorije
|
||||
ELSE ARRAY[COALESCE(c.kategorija,'(nepoznata)')]
|
||||
END
|
||||
) AS unnest_kat
|
||||
) u ON TRUE
|
||||
WHERE {where_sql}
|
||||
) sub
|
||||
ORDER BY kat COLLATE "hr-HR-x-icu", prezime, ime
|
||||
"""
|
||||
rows = db_query(sql, params)
|
||||
groups: Dict[str, Dict[str, Any]] = {}
|
||||
for r in rows:
|
||||
kat = r.pop("kat") or "(nepoznata)"
|
||||
g = groups.setdefault(kat, {"kategorija": kat, "count": 0, "rows": []})
|
||||
if g["count"] < limit_per_kat:
|
||||
g["rows"].append(r)
|
||||
g["count"] += 1
|
||||
out = sorted(groups.values(), key=lambda x: x["kategorija"])
|
||||
return {"ok": True, "groups": out, "total_kategorija": len(out)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user