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:
2026-05-05 13:51:07 +02:00
parent c6a5ec62aa
commit f7b5114f58
289 changed files with 37204 additions and 363 deletions
+493
View File
@@ -953,3 +953,496 @@ def delete_case(cid: int, user=Depends(_require_user)):
cur.execute("DELETE FROM pgz_sport.crm_cases WHERE id=%s", (cid,))
cn.commit()
return {"ok": True, "id": cid}
# ═══════════════════════════════════════════════════════════════════
# AGENT F — Salesforce-Lite extra tabs (Članarine / Liječnički / Obrasci)
# Added: 2026-05-05 by dradulic@outlook.com / damir@rinet.one
# Tables: pgz_sport.clanarine, pgz_sport.lijecnicki_pregledi,
# pgz_sport.form_templates, pgz_sport.form_submissions
# ═══════════════════════════════════════════════════════════════════
import json as _json
CLANARINA_STATUSES = ['nepodmireno', 'djelomicno', 'podmireno', 'storno']
SUBMISSION_STATUSES = ['draft', 'submitted', 'approved', 'rejected']
# ─────────── ČLANARINE ───────────
class ClanarinaIn(BaseModel):
clan_id: Optional[int] = None
klub_id: Optional[int] = None
godina: int
razdoblje: Optional[str] = None
iznos_propisan: float
iznos_placen: Optional[float] = 0
datum_uplate: Optional[str] = None
nacin_uplate: Optional[str] = None
referenca: Optional[str] = None
racun_broj: Optional[str] = None
status: str = 'nepodmireno'
napomena: Optional[str] = None
@router.get("/clanarine")
def list_clanarine(klub_id: Optional[int] = None, clan_id: Optional[int] = None,
godina: Optional[int] = None, status: Optional[str] = None,
limit: int = 500, user=Depends(_require_user)):
where, args = ["1=1"], []
if klub_id: where.append("c.klub_id=%s"); args.append(klub_id)
if clan_id: where.append("c.clan_id=%s"); args.append(clan_id)
if godina: where.append("c.godina=%s"); args.append(godina)
if status: where.append("c.status=%s"); args.append(status)
args.append(limit)
sql = f"""
SELECT c.*,
(cl.ime||' '||cl.prezime) AS clan_naziv,
k.naziv AS klub_naziv
FROM pgz_sport.clanarine c
LEFT JOIN pgz_sport.clanovi cl ON cl.id = c.clan_id
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
WHERE {' AND '.join(where)}
ORDER BY c.godina DESC, c.id DESC
LIMIT %s
"""
with _conn() as cn, cn.cursor() as cur:
cur.execute(sql, args)
items = _rows(cur.fetchall())
return {"items": items, "count": len(items)}
@router.get("/clanarine/{cid}")
def get_clanarina(cid: int, user=Depends(_require_user)):
with _conn() as cn, cn.cursor() as cur:
cur.execute("""
SELECT c.*,
(cl.ime||' '||cl.prezime) AS clan_naziv,
k.naziv AS klub_naziv
FROM pgz_sport.clanarine c
LEFT JOIN pgz_sport.clanovi cl ON cl.id = c.clan_id
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
WHERE c.id=%s
""", (cid,))
row = cur.fetchone()
if not row:
raise HTTPException(404, "not found")
return _row(row)
@router.post("/clanarine")
def create_clanarina(req: ClanarinaIn, user=Depends(_require_user)):
if req.status not in CLANARINA_STATUSES:
raise HTTPException(400, "invalid status")
with _conn() as cn, cn.cursor() as cur:
cur.execute("""
INSERT INTO pgz_sport.clanarine
(clan_id, klub_id, godina, razdoblje, iznos_propisan, iznos_placen,
datum_uplate, nacin_uplate, referenca, racun_broj, status, napomena)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
RETURNING *
""", (req.clan_id, req.klub_id, req.godina, req.razdoblje,
req.iznos_propisan, req.iznos_placen or 0,
req.datum_uplate, req.nacin_uplate, req.referenca,
req.racun_broj, req.status, req.napomena))
cn.commit()
return _row(cur.fetchone())
@router.put("/clanarine/{cid}")
def update_clanarina(cid: int, req: ClanarinaIn, user=Depends(_require_user)):
if req.status not in CLANARINA_STATUSES:
raise HTTPException(400, "invalid status")
with _conn() as cn, cn.cursor() as cur:
cur.execute("SELECT id FROM pgz_sport.clanarine WHERE id=%s", (cid,))
if not cur.fetchone():
raise HTTPException(404, "not found")
cur.execute("""
UPDATE pgz_sport.clanarine
SET clan_id=%s, klub_id=%s, godina=%s, razdoblje=%s,
iznos_propisan=%s, iznos_placen=%s, datum_uplate=%s,
nacin_uplate=%s, referenca=%s, racun_broj=%s,
status=%s, napomena=%s, updated_at=now()
WHERE id=%s
RETURNING *
""", (req.clan_id, req.klub_id, req.godina, req.razdoblje,
req.iznos_propisan, req.iznos_placen or 0,
req.datum_uplate, req.nacin_uplate, req.referenca,
req.racun_broj, req.status, req.napomena, cid))
cn.commit()
return _row(cur.fetchone())
@router.delete("/clanarine/{cid}")
def delete_clanarina(cid: int, user=Depends(_require_user)):
if not _is_admin(user):
raise HTTPException(403, "admin only")
with _conn() as cn, cn.cursor() as cur:
cur.execute("DELETE FROM pgz_sport.clanarine WHERE id=%s", (cid,))
cn.commit()
return {"ok": True, "id": cid}
# ─────────── LIJEČNIČKI PREGLEDI ───────────
class LijecnickiIn(BaseModel):
clan_id: int
klub_id: Optional[int] = None
datum_pregleda: str
vrijedi_do: Optional[str] = None
vrsta_pregleda: Optional[str] = None
ustanova: Optional[str] = None
lijecnik: Optional[str] = None
spreman_za_natjecanje: Optional[bool] = True
ekg: Optional[bool] = False
krv: Optional[bool] = False
spirometrija: Optional[bool] = False
nalaz: Optional[str] = None
komentar_lijecnika: Optional[str] = None
preporuke: Optional[str] = None
iznos: Optional[float] = None
iznos_zzjz: Optional[float] = 0
iznos_klub: Optional[float] = 0
iznos_clan: Optional[float] = 0
datum_placanja: Optional[str] = None
placeno: Optional[bool] = False
racun_broj: Optional[str] = None
nacin_placanja: Optional[str] = None
napomena: Optional[str] = None
@router.get("/lijecnicki")
def list_lijecnicki(klub_id: Optional[int] = None, clan_id: Optional[int] = None,
expiring: Optional[bool] = None, limit: int = 500,
user=Depends(_require_user)):
"""expiring=true → vrijedi_do u idućih 30d ili manje (ili prošlo)."""
where, args = ["1=1"], []
if klub_id: where.append("l.klub_id=%s"); args.append(klub_id)
if clan_id: where.append("l.clan_id=%s"); args.append(clan_id)
if expiring:
where.append("(l.vrijedi_do IS NULL OR l.vrijedi_do <= current_date + interval '30 days')")
args.append(limit)
sql = f"""
SELECT l.*,
(cl.ime||' '||cl.prezime) AS clan_naziv,
k.naziv AS klub_naziv
FROM pgz_sport.lijecnicki_pregledi l
LEFT JOIN pgz_sport.clanovi cl ON cl.id = l.clan_id
LEFT JOIN pgz_sport.klubovi k ON k.id = l.klub_id
WHERE {' AND '.join(where)}
ORDER BY l.datum_pregleda DESC, l.id DESC
LIMIT %s
"""
with _conn() as cn, cn.cursor() as cur:
cur.execute(sql, args)
items = _rows(cur.fetchall())
return {"items": items, "count": len(items)}
@router.get("/lijecnicki/{lid}")
def get_lijecnicki(lid: int, user=Depends(_require_user)):
with _conn() as cn, cn.cursor() as cur:
cur.execute("""
SELECT l.*,
(cl.ime||' '||cl.prezime) AS clan_naziv,
k.naziv AS klub_naziv
FROM pgz_sport.lijecnicki_pregledi l
LEFT JOIN pgz_sport.clanovi cl ON cl.id = l.clan_id
LEFT JOIN pgz_sport.klubovi k ON k.id = l.klub_id
WHERE l.id=%s
""", (lid,))
row = cur.fetchone()
if not row:
raise HTTPException(404, "not found")
return _row(row)
@router.post("/lijecnicki")
def create_lijecnicki(req: LijecnickiIn, user=Depends(_require_user)):
with _conn() as cn, cn.cursor() as cur:
cur.execute("""
INSERT INTO pgz_sport.lijecnicki_pregledi
(clan_id, klub_id, datum_pregleda, vrijedi_do, vrsta_pregleda,
ustanova, lijecnik, spreman_za_natjecanje, ekg, krv, spirometrija,
nalaz, komentar_lijecnika, preporuke,
iznos, iznos_zzjz, iznos_klub, iznos_clan,
datum_placanja, placeno, racun_broj, nacin_placanja, napomena)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,
%s,%s,%s,%s,%s,%s,%s,%s,%s)
RETURNING *
""", (req.clan_id, req.klub_id, req.datum_pregleda, req.vrijedi_do,
req.vrsta_pregleda, req.ustanova, req.lijecnik,
req.spreman_za_natjecanje, req.ekg, req.krv, req.spirometrija,
req.nalaz, req.komentar_lijecnika, req.preporuke,
req.iznos, req.iznos_zzjz or 0, req.iznos_klub or 0, req.iznos_clan or 0,
req.datum_placanja, req.placeno, req.racun_broj,
req.nacin_placanja, req.napomena))
cn.commit()
return _row(cur.fetchone())
@router.put("/lijecnicki/{lid}")
def update_lijecnicki(lid: int, req: LijecnickiIn, user=Depends(_require_user)):
with _conn() as cn, cn.cursor() as cur:
cur.execute("SELECT id FROM pgz_sport.lijecnicki_pregledi WHERE id=%s", (lid,))
if not cur.fetchone():
raise HTTPException(404, "not found")
cur.execute("""
UPDATE pgz_sport.lijecnicki_pregledi
SET clan_id=%s, klub_id=%s, datum_pregleda=%s, vrijedi_do=%s,
vrsta_pregleda=%s, ustanova=%s, lijecnik=%s,
spreman_za_natjecanje=%s, ekg=%s, krv=%s, spirometrija=%s,
nalaz=%s, komentar_lijecnika=%s, preporuke=%s,
iznos=%s, iznos_zzjz=%s, iznos_klub=%s, iznos_clan=%s,
datum_placanja=%s, placeno=%s, racun_broj=%s,
nacin_placanja=%s, napomena=%s, updated_at=now()
WHERE id=%s
RETURNING *
""", (req.clan_id, req.klub_id, req.datum_pregleda, req.vrijedi_do,
req.vrsta_pregleda, req.ustanova, req.lijecnik,
req.spreman_za_natjecanje, req.ekg, req.krv, req.spirometrija,
req.nalaz, req.komentar_lijecnika, req.preporuke,
req.iznos, req.iznos_zzjz or 0, req.iznos_klub or 0, req.iznos_clan or 0,
req.datum_placanja, req.placeno, req.racun_broj,
req.nacin_placanja, req.napomena, lid))
cn.commit()
return _row(cur.fetchone())
@router.delete("/lijecnicki/{lid}")
def delete_lijecnicki(lid: int, user=Depends(_require_user)):
if not _is_admin(user):
raise HTTPException(403, "admin only")
with _conn() as cn, cn.cursor() as cur:
cur.execute("DELETE FROM pgz_sport.lijecnicki_pregledi WHERE id=%s", (lid,))
cn.commit()
return {"ok": True, "id": lid}
# ─────────── OBRASCI (templates + submissions) ───────────
class SubmissionIn(BaseModel):
template_id: Optional[int] = None
template_code: Optional[str] = None
klub_id: Optional[int] = None
clan_id: Optional[int] = None
data: Dict[str, Any] = Field(default_factory=dict)
status: str = 'draft' # draft / submitted
reference_no: Optional[str] = None
class SubmissionStatusIn(BaseModel):
status: str # submitted / approved / rejected
rejected_reason: Optional[str] = None
@router.get("/obrasci")
def list_obrasci_templates(kategorija: Optional[str] = None,
active: bool = True, user=Depends(_require_user)):
where, args = [], []
if active:
where.append("active=true")
if kategorija:
where.append("kategorija=%s"); args.append(kategorija)
sql = "SELECT id, code, naziv, kategorija, opis, schema_json, required_role, active FROM pgz_sport.form_templates"
if where:
sql += " WHERE " + " AND ".join(where)
sql += " ORDER BY kategorija NULLS LAST, naziv"
with _conn() as cn, cn.cursor() as cur:
cur.execute(sql, args)
items = _rows(cur.fetchall())
return {"items": items, "count": len(items)}
@router.get("/obrasci/submission")
def list_obrasci_submissions(status: Optional[str] = None,
klub_id: Optional[int] = None,
template_code: Optional[str] = None,
clan_id: Optional[int] = None,
limit: int = 500,
user=Depends(_require_user)):
where, args = ["1=1"], []
if status: where.append("s.status=%s"); args.append(status)
if klub_id: where.append("s.klub_id=%s"); args.append(klub_id)
if template_code: where.append("s.template_code=%s"); args.append(template_code)
if clan_id: where.append("s.clan_id=%s"); args.append(clan_id)
args.append(limit)
sql = f"""
SELECT s.id, s.template_id, s.template_code, s.klub_id, s.clan_id,
s.user_id, s.data, s.status, s.reference_no,
s.submitted_at, s.reviewed_at, s.approved_at, s.rejected_reason,
s.created_at, s.updated_at,
t.naziv AS template_naziv, t.kategorija,
k.naziv AS klub_naziv,
(cl.ime||' '||cl.prezime) AS clan_naziv,
u.email AS submitter_email
FROM pgz_sport.form_submissions s
LEFT JOIN pgz_sport.form_templates t ON t.id = s.template_id
LEFT JOIN pgz_sport.klubovi k ON k.id = s.klub_id
LEFT JOIN pgz_sport.clanovi cl ON cl.id = s.clan_id
LEFT JOIN pgz_sport.users u ON u.id = s.user_id
WHERE {' AND '.join(where)}
ORDER BY s.created_at DESC
LIMIT %s
"""
with _conn() as cn, cn.cursor() as cur:
cur.execute(sql, args)
items = _rows(cur.fetchall())
return {"items": items, "count": len(items)}
@router.get("/obrasci/submission/{sid}")
def get_obrazac_submission(sid: int, user=Depends(_require_user)):
with _conn() as cn, cn.cursor() as cur:
cur.execute("""
SELECT s.*,
t.naziv AS template_naziv, t.kategorija, t.schema_json,
k.naziv AS klub_naziv,
(cl.ime||' '||cl.prezime) AS clan_naziv,
u.email AS submitter_email
FROM pgz_sport.form_submissions s
LEFT JOIN pgz_sport.form_templates t ON t.id = s.template_id
LEFT JOIN pgz_sport.klubovi k ON k.id = s.klub_id
LEFT JOIN pgz_sport.clanovi cl ON cl.id = s.clan_id
LEFT JOIN pgz_sport.users u ON u.id = s.user_id
WHERE s.id=%s
""", (sid,))
row = cur.fetchone()
if not row:
raise HTTPException(404, "not found")
return _row(row)
@router.post("/obrasci/submission")
def create_obrazac_submission(req: SubmissionIn, user=Depends(_require_user)):
if req.status not in ('draft', 'submitted'):
raise HTTPException(400, "invalid status (draft|submitted)")
tid = req.template_id
tcode = req.template_code
with _conn() as cn, cn.cursor() as cur:
if tid and not tcode:
cur.execute("SELECT code FROM pgz_sport.form_templates WHERE id=%s", (tid,))
r = cur.fetchone()
if not r:
raise HTTPException(400, "unknown template_id")
tcode = r["code"]
elif tcode and not tid:
cur.execute("SELECT id FROM pgz_sport.form_templates WHERE code=%s", (tcode,))
r = cur.fetchone()
if not r:
raise HTTPException(400, "unknown template_code")
tid = r["id"]
elif not tid and not tcode:
raise HTTPException(400, "template_id or template_code required")
submitted_at = "now()" if req.status == 'submitted' else "NULL"
cur.execute(f"""
INSERT INTO pgz_sport.form_submissions
(template_id, template_code, klub_id, user_id, clan_id,
data, status, reference_no, submitted_at)
VALUES (%s,%s,%s,%s,%s,%s::jsonb,%s,%s,{submitted_at})
RETURNING *
""", (tid, tcode, req.klub_id, user["user_id"], req.clan_id,
_json.dumps(req.data or {}), req.status, req.reference_no))
cn.commit()
return _row(cur.fetchone())
@router.put("/obrasci/submission/{sid}")
def update_obrazac_submission(sid: int, req: SubmissionIn, user=Depends(_require_user)):
"""Submitter can update their own draft."""
with _conn() as cn, cn.cursor() as cur:
cur.execute("SELECT user_id, status FROM pgz_sport.form_submissions WHERE id=%s", (sid,))
row = cur.fetchone()
if not row:
raise HTTPException(404, "not found")
if not _is_admin(user) and row["user_id"] != user["user_id"]:
raise HTTPException(403, "not your submission")
if row["status"] not in ('draft', 'submitted') and not _is_admin(user):
raise HTTPException(400, "submission already finalized")
cur.execute("""
UPDATE pgz_sport.form_submissions
SET klub_id=%s, clan_id=%s, data=%s::jsonb,
status=%s, reference_no=%s,
submitted_at = CASE WHEN %s='submitted' AND submitted_at IS NULL THEN now() ELSE submitted_at END,
updated_at=now()
WHERE id=%s
RETURNING *
""", (req.klub_id, req.clan_id, _json.dumps(req.data or {}),
req.status, req.reference_no, req.status, sid))
cn.commit()
return _row(cur.fetchone())
@router.put("/obrasci/submission/{sid}/status")
def set_submission_status(sid: int, req: SubmissionStatusIn, user=Depends(_require_user)):
"""Approve/reject (admin) or self-submit a draft (owner)."""
if req.status not in SUBMISSION_STATUSES:
raise HTTPException(400, "invalid status")
with _conn() as cn, cn.cursor() as cur:
cur.execute("SELECT user_id, status FROM pgz_sport.form_submissions WHERE id=%s", (sid,))
row = cur.fetchone()
if not row:
raise HTTPException(404, "not found")
if req.status in ('approved', 'rejected'):
if not _is_admin(user):
raise HTTPException(403, "admin only")
elif req.status == 'submitted':
if row["user_id"] != user["user_id"] and not _is_admin(user):
raise HTTPException(403, "not your submission")
if req.status == 'approved':
cur.execute("""
UPDATE pgz_sport.form_submissions
SET status='approved', approved_by=%s, approved_at=now(),
reviewed_by=%s, reviewed_at=now(), updated_at=now()
WHERE id=%s RETURNING *
""", (user["user_id"], user["user_id"], sid))
elif req.status == 'rejected':
cur.execute("""
UPDATE pgz_sport.form_submissions
SET status='rejected', rejected_reason=%s,
reviewed_by=%s, reviewed_at=now(), updated_at=now()
WHERE id=%s RETURNING *
""", (req.rejected_reason, user["user_id"], sid))
elif req.status == 'submitted':
cur.execute("""
UPDATE pgz_sport.form_submissions
SET status='submitted',
submitted_at = COALESCE(submitted_at, now()),
updated_at=now()
WHERE id=%s RETURNING *
""", (sid,))
else: # draft
cur.execute("""
UPDATE pgz_sport.form_submissions
SET status='draft', updated_at=now()
WHERE id=%s RETURNING *
""", (sid,))
cn.commit()
return _row(cur.fetchone())
@router.delete("/obrasci/submission/{sid}")
def delete_obrazac_submission(sid: int, user=Depends(_require_user)):
with _conn() as cn, cn.cursor() as cur:
cur.execute("SELECT user_id, status FROM pgz_sport.form_submissions WHERE id=%s", (sid,))
row = cur.fetchone()
if not row:
raise HTTPException(404, "not found")
if not _is_admin(user) and row["user_id"] != user["user_id"]:
raise HTTPException(403, "not your submission")
cur.execute("DELETE FROM pgz_sport.form_submissions WHERE id=%s", (sid,))
cn.commit()
return {"ok": True, "id": sid}
@router.get("/obrasci/{tid}")
def get_obrazac_template(tid: int, user=Depends(_require_user)):
with _conn() as cn, cn.cursor() as cur:
cur.execute("SELECT * FROM pgz_sport.form_templates WHERE id=%s", (tid,))
row = cur.fetchone()
if not row:
raise HTTPException(404, "not found")
return _row(row)