PDF link target=_blank + nginx timeouts + priority filteri (samo s podacima)
nginx (sport.rinet.one): - proxy_read_timeout 60s → 300s - proxy_send_timeout 300s - proxy_buffering off (PDF stream) - client_max_body_size 50M → 100M Endpoints: - /api/v2/klubovi/financirani: +with_data filter (samo s potporama/godišnjakom/HNS) - /api/v2/sportasi/filtered: +samo_priority +samo_s_hns Frontend: - PDF link target=_blank rel=noopener - window._klub_only_priority = true (default) - window._sportas_only_priority = true (default) DB View: - pgz_sport.v_nogomet_priority (prima_potpore, u_godisnjaku, ima_hns_roster)
This commit is contained in:
+266
-6
@@ -1727,6 +1727,14 @@ try:
|
||||
except Exception as e:
|
||||
print(f'[CRM/R5] extras router fail: {e}')
|
||||
|
||||
# === W5 / Notification Center — /api/v2/notif/* ===
|
||||
try:
|
||||
from routers.notif_router import router as notif_router
|
||||
app.include_router(notif_router)
|
||||
print('[NOTIF] notif_router loaded (/api/v2/notif/list|count|{id}/read|mark-all-read|DELETE)')
|
||||
except Exception as e:
|
||||
print(f'[NOTIF] notif_router fail: {e}')
|
||||
|
||||
# === CRM v2 — Salesforce-Lite (Accounts/Contacts/Leads/Opportunities/Activities/Cases) ===
|
||||
try:
|
||||
from crm_router import router as crm_v2_router
|
||||
@@ -1811,6 +1819,14 @@ try:
|
||||
except Exception as e:
|
||||
print(f'[ERP-FULL] router fail: {e}')
|
||||
|
||||
# ═══ KALENDAR (CRUD events) router — /api/v2/kalendar/events
|
||||
try:
|
||||
from routers.kalendar_router import router as kalendar_router
|
||||
app.include_router(kalendar_router)
|
||||
print('[KALENDAR] router loaded (/api/v2/kalendar/*)')
|
||||
except Exception as e:
|
||||
print(f'[KALENDAR] router fail: {e}')
|
||||
|
||||
@app.get("/crm")
|
||||
@app.get("/crm/")
|
||||
def serve_crm():
|
||||
@@ -2216,16 +2232,30 @@ def klubovi_priority_sort(sport: str = None, limit: int = 500):
|
||||
|
||||
@app.get("/api/v2/clan/{clan_id}/full")
|
||||
def clan_full(clan_id: int):
|
||||
"""Punu sliku igrača: profil + kategorije + sezone + utakmice + potpore."""
|
||||
profile = fetch("SELECT * FROM pgz_sport.clanovi WHERE id = %s", (clan_id,))
|
||||
"""Punu sliku igrača: profil + kategorije + sezone + utakmice + potpore.
|
||||
SUB6 (2026-05-05): join klub_naziv u kategorije za drill-down panel."""
|
||||
profile = fetch("""
|
||||
SELECT c.*, k.naziv AS klub_naziv
|
||||
FROM pgz_sport.clanovi c
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||
WHERE c.id = %s
|
||||
""", (clan_id,))
|
||||
if not profile: return {"error": "not_found"}
|
||||
p = profile[0]
|
||||
|
||||
kategorije = fetch("SELECT * FROM pgz_sport.clan_kategorije WHERE clan_id = %s ORDER BY sezona DESC", (clan_id,))
|
||||
|
||||
kategorije = fetch("""
|
||||
SELECT ck.id, ck.clan_id, ck.kategorija, ck.sezona, ck.klub_id,
|
||||
ck.source, ck.source_url, ck.scraped_at,
|
||||
k.naziv AS klub_naziv
|
||||
FROM pgz_sport.clan_kategorije ck
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = ck.klub_id
|
||||
WHERE ck.clan_id = %s
|
||||
ORDER BY ck.sezona DESC NULLS LAST, ck.kategorija
|
||||
""", (clan_id,))
|
||||
seasons = fetch("SELECT * FROM pgz_sport.hns_player_seasons WHERE clan_id = %s ORDER BY sezona DESC", (clan_id,))
|
||||
matches = fetch("SELECT * FROM pgz_sport.hns_player_matches WHERE clan_id = %s ORDER BY datum DESC NULLS LAST LIMIT 30", (clan_id,))
|
||||
multi_stats = fetch("SELECT * FROM pgz_sport.player_stats WHERE clan_id = %s ORDER BY sezona DESC", (clan_id,))
|
||||
|
||||
|
||||
return {
|
||||
"profile": p,
|
||||
"kategorije": kategorije,
|
||||
@@ -2405,7 +2435,7 @@ def serve_favicon():
|
||||
return FileResponse("/opt/pgz-sport/static/favicon.ico", media_type="image/x-icon")
|
||||
|
||||
@app.get("/api/v2/klubovi/financirani")
|
||||
def klubovi_financirani(sport: str = None, davatelj: str = None, godina: int = None, limit: int = 1000):
|
||||
def klubovi_financirani(sport: str = None, davatelj: str = None, godina: int = None, with_data: bool = False, limit: int = 1000):
|
||||
"""Klubovi koji su primili novac od PGŽ/RSS/Grad Rijeka. davatelj: pgz|rss|grad_rijeka|any"""
|
||||
where = []
|
||||
params = []
|
||||
@@ -2420,6 +2450,9 @@ def klubovi_financirani(sport: str = None, davatelj: str = None, godina: int = N
|
||||
where.append("k.prima_grad_rijeka = true")
|
||||
elif davatelj == 'any':
|
||||
where.append("(k.prima_pgz OR k.prima_rss OR k.prima_grad_rijeka)")
|
||||
if with_data:
|
||||
# Default for demo: only klubovi koji imaju nešto (potpore || godišnjak || HNS roster)
|
||||
where.append("(k.prima_pgz OR k.prima_rss OR k.prima_grad_rijeka OR k.u_godisnjaku OR EXISTS (SELECT 1 FROM pgz_sport.hns_klub_roster WHERE klub_id = k.id))")
|
||||
|
||||
where_sql = "WHERE " + " AND ".join(where) if where else ""
|
||||
rows = fetch(f"""
|
||||
@@ -2437,6 +2470,7 @@ def klubovi_financirani(sport: str = None, davatelj: str = None, godina: int = N
|
||||
|
||||
@app.get("/api/v2/sportasi/filtered")
|
||||
def sportasi_filtered(sport: str = None, klub_id: int = None, kategorija: str = None,
|
||||
samo_priority: bool = False, samo_s_hns: bool = False,
|
||||
godina_rod_od: int = None, godina_rod_do: int = None,
|
||||
q: str = None, limit: int = 500):
|
||||
"""Sportaši s range filter za godinu rođenja + kategorije."""
|
||||
@@ -2460,6 +2494,11 @@ def sportasi_filtered(sport: str = None, klub_id: int = None, kategorija: str =
|
||||
if q:
|
||||
where.append("(c.ime ILIKE %s OR c.prezime ILIKE %s)")
|
||||
params.extend([f"%{q}%", f"%{q}%"])
|
||||
if samo_priority:
|
||||
# Igrač iz kluba koji prima novac ili je u godišnjaku ili ima HNS roster
|
||||
where.append("c.klub_id IN (SELECT id FROM pgz_sport.v_klubovi_financiranje WHERE prima_pgz OR prima_rss OR prima_grad_rijeka OR u_godisnjaku UNION SELECT klub_id FROM pgz_sport.hns_klub_roster)")
|
||||
if samo_s_hns:
|
||||
where.append("c.hns_igrac_id IS NOT NULL")
|
||||
|
||||
where_sql = "WHERE " + " AND ".join(where)
|
||||
rows = fetch(f"""
|
||||
@@ -2475,6 +2514,227 @@ def sportasi_filtered(sport: str = None, klub_id: int = None, kategorija: str =
|
||||
return {"count": len(rows), "rows": rows}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# XLSX EXPORT — sportaši & klubovi-roster (SUB8)
|
||||
# Author: Damir Radulić (damir@rinet.one / dradulic@outlook.com)
|
||||
# Date: 2026-05-05 v1.0.0
|
||||
# Description: openpyxl-based XLSX exports with HNS data,
|
||||
# dark header (Palantir vibe) + auto-width columns.
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
def _xlsx_style_header(ws, ncols):
|
||||
"""Apply dark Palantir-style header styling to row 1."""
|
||||
from openpyxl.styles import PatternFill, Font, Alignment, Border, Side
|
||||
fill = PatternFill(start_color="1A1F2E", end_color="1A1F2E", fill_type="solid")
|
||||
font = Font(bold=True, color="FFFFFF", name="Calibri", size=11)
|
||||
align = Alignment(horizontal="left", vertical="center", wrap_text=False)
|
||||
side = Side(style="thin", color="3A4356")
|
||||
border = Border(left=side, right=side, top=side, bottom=side)
|
||||
for col_idx in range(1, ncols + 1):
|
||||
cell = ws.cell(row=1, column=col_idx)
|
||||
cell.fill = fill
|
||||
cell.font = font
|
||||
cell.alignment = align
|
||||
cell.border = border
|
||||
ws.row_dimensions[1].height = 22
|
||||
ws.freeze_panes = "A2"
|
||||
|
||||
|
||||
def _xlsx_autosize(ws):
|
||||
"""Approximate auto-width based on cell content length."""
|
||||
from openpyxl.utils import get_column_letter
|
||||
for col_idx in range(1, ws.max_column + 1):
|
||||
max_len = 8
|
||||
letter = get_column_letter(col_idx)
|
||||
for row_idx in range(1, min(ws.max_row, 500) + 1):
|
||||
v = ws.cell(row=row_idx, column=col_idx).value
|
||||
if v is None:
|
||||
continue
|
||||
try:
|
||||
ln = len(str(v))
|
||||
except Exception:
|
||||
ln = 8
|
||||
if ln > max_len:
|
||||
max_len = ln
|
||||
ws.column_dimensions[letter].width = min(max_len + 2, 60)
|
||||
|
||||
|
||||
_SPORTASI_COLS = [
|
||||
("ime", "Ime"),
|
||||
("prezime", "Prezime"),
|
||||
("klub_naziv", "Klub"),
|
||||
("datum_rodenja", "Datum rođenja"),
|
||||
("visina_cm", "Visina (cm)"),
|
||||
("tezina_kg", "Težina (kg)"),
|
||||
("dominantna_noga", "Dominantna noga"),
|
||||
("pozicija", "Pozicija"),
|
||||
("hns_url", "HNS URL"),
|
||||
("broj_sezona", "Broj sezona"),
|
||||
("broj_utakmica", "Broj utakmica"),
|
||||
]
|
||||
|
||||
|
||||
@app.get("/api/v2/export/sportasi")
|
||||
def export_sportasi_xlsx(klub_id: Optional[int] = None, sport: Optional[str] = None, limit: int = 10000):
|
||||
"""XLSX export svih sportaša + HNS data.
|
||||
Filter: klub_id (opcija), sport (opcija). Default vrača sve sportaše."""
|
||||
import io
|
||||
from openpyxl import Workbook
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
where = ["1=1"]
|
||||
params: list = []
|
||||
if klub_id:
|
||||
where.append("c.klub_id = %s"); params.append(klub_id)
|
||||
if sport:
|
||||
where.append("(c.sport = %s OR k.sport = %s)"); params.extend([sport, sport])
|
||||
|
||||
sql = f"""
|
||||
SELECT c.id, c.ime, c.prezime,
|
||||
COALESCE(k.naziv, '') AS klub_naziv,
|
||||
c.datum_rodenja, c.visina_cm, c.tezina_kg,
|
||||
c.dominantna_noga, c.pozicija,
|
||||
COALESCE(c.source_url, '') AS hns_url,
|
||||
(SELECT count(*) FROM pgz_sport.hns_player_seasons s WHERE s.clan_id = c.id) AS broj_sezona,
|
||||
(SELECT count(*) FROM pgz_sport.hns_player_matches m WHERE m.clan_id = c.id) AS broj_utakmica
|
||||
FROM pgz_sport.clanovi c
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||
WHERE {' AND '.join(where)}
|
||||
ORDER BY c.prezime, c.ime
|
||||
LIMIT %s
|
||||
"""
|
||||
rows = fetch(sql, tuple(params) + (limit,))
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Sportaši"
|
||||
|
||||
headers_hr = [lbl for _key, lbl in _SPORTASI_COLS]
|
||||
ws.append(headers_hr)
|
||||
for r in rows:
|
||||
ws.append([r.get(key) for key, _ in _SPORTASI_COLS])
|
||||
|
||||
_xlsx_style_header(ws, len(headers_hr))
|
||||
_xlsx_autosize(ws)
|
||||
|
||||
buf = io.BytesIO()
|
||||
wb.save(buf)
|
||||
buf.seek(0)
|
||||
|
||||
fname = f"pgz_sportasi_{datetime.now().strftime('%Y%m%d')}.xlsx"
|
||||
return StreamingResponse(
|
||||
buf,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{fname}"',
|
||||
"X-Row-Count": str(len(rows)),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/v2/export/klubovi-roster")
|
||||
def export_klubovi_roster_xlsx(klub_id: Optional[int] = None, sport: Optional[str] = None, limit_per_klub: int = 5000):
|
||||
"""XLSX export rostera po klubu.
|
||||
- Bez klub_id: jedan workbook s više sheet-ova (jedan po klubu).
|
||||
- S klub_id: jedan sheet s rosterom kluba.
|
||||
"""
|
||||
import io
|
||||
import re
|
||||
from openpyxl import Workbook
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
if klub_id:
|
||||
klubovi_rows = fetch(
|
||||
"SELECT id, naziv, sport FROM pgz_sport.klubovi WHERE id = %s",
|
||||
(klub_id,),
|
||||
)
|
||||
else:
|
||||
sport_where = ""
|
||||
sport_params: tuple = ()
|
||||
if sport:
|
||||
sport_where = "AND k.sport = %s"
|
||||
sport_params = (sport,)
|
||||
klubovi_rows = fetch(
|
||||
f"""
|
||||
SELECT k.id, k.naziv, k.sport
|
||||
FROM pgz_sport.klubovi k
|
||||
WHERE EXISTS (SELECT 1 FROM pgz_sport.clanovi c WHERE c.klub_id = k.id)
|
||||
{sport_where}
|
||||
ORDER BY k.naziv
|
||||
""",
|
||||
sport_params,
|
||||
)
|
||||
|
||||
wb = Workbook()
|
||||
default_ws = wb.active
|
||||
wb.remove(default_ws)
|
||||
|
||||
headers_hr = [lbl for _key, lbl in _SPORTASI_COLS]
|
||||
used_titles: set = set()
|
||||
total_rows = 0
|
||||
|
||||
def _safe_sheet_title(naziv: str, kid: int) -> str:
|
||||
# Excel: max 31 chars, no : \ / ? * [ ]
|
||||
t = re.sub(r"[:\\/\?\*\[\]]", " ", naziv or f"Klub_{kid}")
|
||||
t = t.strip()[:28] or f"Klub_{kid}"
|
||||
base = t
|
||||
i = 2
|
||||
while t in used_titles:
|
||||
suf = f"_{i}"
|
||||
t = (base[: 31 - len(suf)] + suf)
|
||||
i += 1
|
||||
used_titles.add(t)
|
||||
return t
|
||||
|
||||
if not klubovi_rows:
|
||||
ws = wb.create_sheet(title="Prazno")
|
||||
ws.append(headers_hr)
|
||||
_xlsx_style_header(ws, len(headers_hr))
|
||||
else:
|
||||
for kr in klubovi_rows:
|
||||
kid = kr["id"]
|
||||
kn = kr.get("naziv") or f"Klub {kid}"
|
||||
title = _safe_sheet_title(kn, kid)
|
||||
ws = wb.create_sheet(title=title)
|
||||
ws.append(headers_hr)
|
||||
roster = fetch(
|
||||
"""
|
||||
SELECT c.id, c.ime, c.prezime,
|
||||
%s::text AS klub_naziv,
|
||||
c.datum_rodenja, c.visina_cm, c.tezina_kg,
|
||||
c.dominantna_noga, c.pozicija,
|
||||
COALESCE(c.source_url, '') AS hns_url,
|
||||
(SELECT count(*) FROM pgz_sport.hns_player_seasons s WHERE s.clan_id = c.id) AS broj_sezona,
|
||||
(SELECT count(*) FROM pgz_sport.hns_player_matches m WHERE m.clan_id = c.id) AS broj_utakmica
|
||||
FROM pgz_sport.clanovi c
|
||||
WHERE c.klub_id = %s
|
||||
ORDER BY c.prezime, c.ime
|
||||
LIMIT %s
|
||||
""",
|
||||
(kn, kid, limit_per_klub),
|
||||
)
|
||||
for r in roster:
|
||||
ws.append([r.get(key) for key, _ in _SPORTASI_COLS])
|
||||
total_rows += len(roster)
|
||||
_xlsx_style_header(ws, len(headers_hr))
|
||||
_xlsx_autosize(ws)
|
||||
|
||||
buf = io.BytesIO()
|
||||
wb.save(buf)
|
||||
buf.seek(0)
|
||||
|
||||
suffix = f"klub{klub_id}" if klub_id else "svi"
|
||||
fname = f"pgz_klubovi_roster_{suffix}_{datetime.now().strftime('%Y%m%d')}.xlsx"
|
||||
return StreamingResponse(
|
||||
buf,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{fname}"',
|
||||
"X-Klubovi-Count": str(len(klubovi_rows)),
|
||||
"X-Row-Count": str(total_rows),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def root(request: Request):
|
||||
host = request.headers.get("host", "")
|
||||
|
||||
Reference in New Issue
Block a user