b93ca9a8bf
Obrasci (M9):
- /api/crm/forms — katalog form_templates (15 templata već seedan)
- /api/crm/forms/templates — alias (kompatibilnost)
- /api/crm/forms/{code|id} — detalji + schema_json
- /api/crm/forms/{code|id}/prefill — autopopulacija polja iz baze
(klub_id/clan_id/user_id → polja na obrascu mapirana po imenima)
- /api/crm/forms/submissions [GET/POST] — lista + create draft
- /api/crm/forms/submissions/{id} — detalji s schema + klub/clan
- /api/crm/forms/submissions/{id}/submit — submit + sha256 potpis sadržaja
- /api/crm/forms/submissions/{id}/sign — re-sign / potpis bez statusa change
- /api/crm/forms/submissions/{id}/approve|reject — workflow
- /api/crm/forms/submissions/{id}/pdf — generirani PDF s metapodacima i potpisom
- /api/crm/forms/{code|id}/submit — shortcut: kreiraj+submit u jednom POST
ZZJZ PGŽ (M8 dopuna):
- /api/crm/zzjz/info — dodan online_booking probe (HTTP scrape best-effort)
- /api/crm/lijecnicki/{id}/zakazi — vraća booking URL ako postoji, inače mailto:
- /api/crm/lijecnicki/zakazi-email — generira mailto: deeplink s pred-popunjenim
podacima sportaša/kluba (fallback kad nema online termina)
- URL sportske medicine ispravljen na školska/adolescentna medicina (jedini stvarni
odjel ZZJZ PGŽ koji obavlja sportske preglede).
570 lines
22 KiB
Python
570 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# Fajl: routers/lijecnicki_router.py | v1.0.0 | 04.05.2026
|
|
# Autor: Damir Radulić <dradulic@outlook.com> / damir@rinet.one
|
|
# Lokacija: /opt/pgz-sport/routers/lijecnicki_router.py
|
|
# Svrha: M8 — CRM Liječnički pregledi + ZZJZ PGŽ scheduling integracija
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
"""M8 Liječnički router.
|
|
|
|
Endpointi (montirani na /api/crm):
|
|
GET /lijecnicki → lista (filteri)
|
|
POST /lijecnicki → novi pregled
|
|
GET /lijecnicki/{id} → detalji
|
|
PUT /lijecnicki/{id} → update
|
|
DELETE /lijecnicki/{id} → brisanje
|
|
GET /lijecnicki/uskoro-isticu → istekao + idućih 30 dana
|
|
POST /lijecnicki/{id}/zakazi → zakaži termin (ZZJZ PGŽ mock)
|
|
GET /zzjz/termini → dostupni termini ZZJZ PGŽ (mock + scrape stub)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from datetime import date, datetime, timedelta
|
|
from decimal import Decimal
|
|
from typing import Optional, List
|
|
|
|
import psycopg2
|
|
from psycopg2.extras import RealDictCursor
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
|
|
router = APIRouter(prefix="/api/crm", tags=["crm-lijecnicki"])
|
|
|
|
DSN = "host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7"
|
|
|
|
ZZJZ_BASE = "https://zzjzpgz.hr"
|
|
ZZJZ_INFO = {
|
|
"naziv": "Nastavni zavod za javno zdravstvo PGŽ",
|
|
"adresa": "Krešimirova 52a, 51000 Rijeka",
|
|
"telefon": "+385 51 358 770",
|
|
"email": "info@zzjzpgz.hr",
|
|
"web": ZZJZ_BASE,
|
|
# Najbliži postojeći odjel — sportski liječnički ide preko adolescentne medicine
|
|
"url_sportska_medicina": f"{ZZJZ_BASE}/zavod/odjeli/odjel-za-skolsku-i-adolescentnu-medicinu/",
|
|
}
|
|
|
|
|
|
def _conn():
|
|
return psycopg2.connect(DSN, cursor_factory=RealDictCursor)
|
|
|
|
|
|
def _conv(v):
|
|
if isinstance(v, (date, datetime)):
|
|
return v.isoformat()
|
|
if isinstance(v, Decimal):
|
|
return float(v)
|
|
return v
|
|
|
|
|
|
def _row(d):
|
|
return {k: _conv(v) for k, v in dict(d).items()}
|
|
|
|
|
|
# ───────────── modeli ─────────────
|
|
|
|
class LijecnickiIn(BaseModel):
|
|
clan_id: int
|
|
klub_id: Optional[int] = None
|
|
datum_pregleda: date
|
|
vrijedi_do: Optional[date] = None
|
|
vrsta_pregleda: Optional[str] = "temeljni"
|
|
ustanova: Optional[str] = "ZZJZ PGŽ"
|
|
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] = 0
|
|
iznos_zzjz: Optional[float] = 0
|
|
iznos_klub: Optional[float] = 0
|
|
iznos_clan: Optional[float] = 0
|
|
datum_placanja: Optional[date] = None
|
|
placeno: Optional[bool] = False
|
|
racun_broj: Optional[str] = None
|
|
nacin_placanja: Optional[str] = None
|
|
napomena: Optional[str] = None
|
|
|
|
|
|
class LijecnickiPatch(BaseModel):
|
|
klub_id: Optional[int] = None
|
|
datum_pregleda: Optional[date] = None
|
|
vrijedi_do: Optional[date] = None
|
|
vrsta_pregleda: Optional[str] = None
|
|
ustanova: Optional[str] = None
|
|
lijecnik: Optional[str] = None
|
|
spreman_za_natjecanje: Optional[bool] = None
|
|
ekg: Optional[bool] = None
|
|
krv: Optional[bool] = None
|
|
spirometrija: Optional[bool] = None
|
|
nalaz: Optional[str] = None
|
|
komentar_lijecnika: Optional[str] = None
|
|
preporuke: Optional[str] = None
|
|
iznos: Optional[float] = None
|
|
iznos_zzjz: Optional[float] = None
|
|
iznos_klub: Optional[float] = None
|
|
iznos_clan: Optional[float] = None
|
|
datum_placanja: Optional[date] = None
|
|
placeno: Optional[bool] = None
|
|
racun_broj: Optional[str] = None
|
|
nacin_placanja: Optional[str] = None
|
|
napomena: Optional[str] = None
|
|
|
|
|
|
class ZakaziIn(BaseModel):
|
|
datum: date
|
|
vrijeme: Optional[str] = "09:00"
|
|
ustanova: Optional[str] = "ZZJZ PGŽ"
|
|
napomena: Optional[str] = None
|
|
|
|
|
|
# ───────────── lista ─────────────
|
|
|
|
@router.get("/lijecnicki")
|
|
def list_lijecnicki(
|
|
klub_id: Optional[int] = Query(None),
|
|
clan_id: Optional[int] = Query(None),
|
|
status: Optional[str] = Query(None,
|
|
description="vazeci|uskoro|istekao"),
|
|
placeno: Optional[bool] = Query(None),
|
|
sort: str = Query("vrijedi_do"),
|
|
order: str = Query("asc"),
|
|
limit: int = Query(500, le=2000),
|
|
):
|
|
where, params = [], []
|
|
if klub_id:
|
|
where.append("l.klub_id = %s"); params.append(klub_id)
|
|
if clan_id:
|
|
where.append("l.clan_id = %s"); params.append(clan_id)
|
|
if placeno is not None:
|
|
where.append("l.placeno = %s"); params.append(placeno)
|
|
# status_calc: vazeci = >30d, uskoro = 0..30d, istekao = <0
|
|
if status == "vazeci":
|
|
where.append("l.vrijedi_do > (CURRENT_DATE + INTERVAL '30 days')")
|
|
elif status == "uskoro":
|
|
where.append("l.vrijedi_do BETWEEN CURRENT_DATE AND (CURRENT_DATE + INTERVAL '30 days')")
|
|
elif status == "istekao":
|
|
where.append("l.vrijedi_do < CURRENT_DATE")
|
|
|
|
sort_map = {
|
|
"vrijedi_do": "l.vrijedi_do",
|
|
"datum_pregleda": "l.datum_pregleda",
|
|
"klub": "k.naziv",
|
|
"clan": "cl.prezime",
|
|
"iznos": "l.iznos",
|
|
}
|
|
sort_col = sort_map.get(sort, "l.vrijedi_do")
|
|
order_sql = "DESC" if order.lower() == "desc" else "ASC"
|
|
where_sql = ("WHERE " + " AND ".join(where)) if where else ""
|
|
params.append(limit)
|
|
|
|
sql = f"""
|
|
SELECT l.*,
|
|
cl.ime || ' ' || cl.prezime AS clan,
|
|
cl.oib AS clan_oib, cl.email AS clan_email,
|
|
k.naziv AS klub, k.oib AS klub_oib,
|
|
CASE
|
|
WHEN l.vrijedi_do IS NULL THEN 'nepoznato'
|
|
WHEN l.vrijedi_do < CURRENT_DATE THEN 'istekao'
|
|
WHEN l.vrijedi_do <= (CURRENT_DATE + INTERVAL '30 days') THEN 'uskoro'
|
|
ELSE 'vazeci'
|
|
END AS status_calc,
|
|
(l.vrijedi_do - CURRENT_DATE)::int AS dana_do_isteka
|
|
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_sql}
|
|
ORDER BY {sort_col} {order_sql} NULLS LAST
|
|
LIMIT %s
|
|
"""
|
|
|
|
sum_sql = f"""
|
|
SELECT COUNT(*) AS total,
|
|
COUNT(*) FILTER (WHERE l.vrijedi_do < CURRENT_DATE) AS istekli,
|
|
COUNT(*) FILTER (WHERE l.vrijedi_do BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '30 days') AS uskoro,
|
|
COUNT(*) FILTER (WHERE l.vrijedi_do > CURRENT_DATE + INTERVAL '30 days') AS vazeci,
|
|
COUNT(*) FILTER (WHERE l.placeno IS TRUE) AS placeni,
|
|
COALESCE(SUM(l.iznos), 0)::numeric(10,2) AS total_iznos
|
|
FROM pgz_sport.lijecnicki_pregledi l
|
|
LEFT JOIN pgz_sport.klubovi k ON k.id = l.klub_id
|
|
{where_sql}
|
|
"""
|
|
|
|
with _conn() as conn, conn.cursor() as cur:
|
|
cur.execute(sql, params)
|
|
rows = [_row(r) for r in cur.fetchall()]
|
|
cur.execute(sum_sql, params[:-1])
|
|
summary = _row(cur.fetchone() or {})
|
|
|
|
return {"count": len(rows), "rows": rows, "summary": summary}
|
|
|
|
|
|
# ───────────── uskoro isticu (30 dana + istekli) ─────────────
|
|
|
|
@router.get("/lijecnicki/uskoro-isticu")
|
|
def list_uskoro_isticu(
|
|
klub_id: Optional[int] = Query(None),
|
|
days: int = Query(30, ge=1, le=180),
|
|
include_expired: bool = Query(True),
|
|
):
|
|
where = ["l.vrijedi_do IS NOT NULL"]
|
|
params: list = []
|
|
if include_expired:
|
|
where.append("l.vrijedi_do <= (CURRENT_DATE + (%s || ' days')::interval)")
|
|
else:
|
|
where.append("l.vrijedi_do BETWEEN CURRENT_DATE AND (CURRENT_DATE + (%s || ' days')::interval)")
|
|
params.append(str(days))
|
|
if klub_id:
|
|
where.append("l.klub_id = %s"); params.append(klub_id)
|
|
|
|
sql = f"""
|
|
SELECT l.id, l.clan_id, l.klub_id, l.datum_pregleda, l.vrijedi_do,
|
|
l.vrsta_pregleda, l.ustanova, l.lijecnik, l.placeno,
|
|
cl.ime || ' ' || cl.prezime AS clan,
|
|
cl.email AS clan_email, cl.telefon AS clan_telefon,
|
|
k.naziv AS klub, k.oib AS klub_oib,
|
|
(l.vrijedi_do - CURRENT_DATE)::int AS dana_do_isteka,
|
|
CASE
|
|
WHEN l.vrijedi_do < CURRENT_DATE THEN 'istekao'
|
|
ELSE 'uskoro'
|
|
END AS status_calc
|
|
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.vrijedi_do ASC
|
|
"""
|
|
|
|
with _conn() as conn, conn.cursor() as cur:
|
|
cur.execute(sql, params)
|
|
rows = [_row(r) for r in cur.fetchall()]
|
|
n_istekli = sum(1 for r in rows if (r.get("dana_do_isteka") or 0) < 0)
|
|
n_uskoro = len(rows) - n_istekli
|
|
return {"count": len(rows), "istekli": n_istekli, "uskoro": n_uskoro,
|
|
"days_window": days, "rows": rows}
|
|
|
|
|
|
# ───────────── detalji ─────────────
|
|
|
|
@router.get("/lijecnicki/{lid}")
|
|
def get_lijecnicki(lid: int):
|
|
with _conn() as conn, conn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT l.*,
|
|
cl.ime || ' ' || cl.prezime AS clan,
|
|
cl.oib AS clan_oib, cl.email AS clan_email,
|
|
cl.telefon AS clan_telefon,
|
|
k.naziv AS klub, k.oib AS klub_oib,
|
|
CASE
|
|
WHEN l.vrijedi_do IS NULL THEN 'nepoznato'
|
|
WHEN l.vrijedi_do < CURRENT_DATE THEN 'istekao'
|
|
WHEN l.vrijedi_do <= (CURRENT_DATE + INTERVAL '30 days') THEN 'uskoro'
|
|
ELSE 'vazeci'
|
|
END AS status_calc,
|
|
(l.vrijedi_do - CURRENT_DATE)::int AS dana_do_isteka
|
|
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,))
|
|
r = cur.fetchone()
|
|
if not r:
|
|
raise HTTPException(404, "Liječnički pregled ne postoji")
|
|
return _row(r)
|
|
|
|
|
|
# ───────────── kreiraj ─────────────
|
|
|
|
@router.post("/lijecnicki")
|
|
def create_lijecnicki(body: LijecnickiIn):
|
|
klub_id = body.klub_id
|
|
with _conn() as conn, conn.cursor() as cur:
|
|
if not klub_id:
|
|
cur.execute("SELECT klub_id FROM pgz_sport.clanovi WHERE id=%s", (body.clan_id,))
|
|
r = cur.fetchone()
|
|
klub_id = r["klub_id"] if r else None
|
|
# default vrijedi_do = +1 godina ako nije postavljeno
|
|
vrijedi_do = body.vrijedi_do
|
|
if vrijedi_do is None:
|
|
vrijedi_do = body.datum_pregleda + timedelta(days=365)
|
|
|
|
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 *
|
|
""", (body.clan_id, klub_id, body.datum_pregleda, vrijedi_do,
|
|
body.vrsta_pregleda, body.ustanova, body.lijecnik,
|
|
body.spreman_za_natjecanje, body.ekg, body.krv, body.spirometrija,
|
|
body.nalaz, body.komentar_lijecnika, body.preporuke,
|
|
body.iznos, body.iznos_zzjz, body.iznos_klub, body.iznos_clan,
|
|
body.datum_placanja, body.placeno, body.racun_broj,
|
|
body.nacin_placanja, body.napomena))
|
|
r = cur.fetchone()
|
|
conn.commit()
|
|
return _row(r)
|
|
|
|
|
|
# ───────────── update / delete ─────────────
|
|
|
|
@router.put("/lijecnicki/{lid}")
|
|
def update_lijecnicki(lid: int, patch: LijecnickiPatch):
|
|
fields, params = [], []
|
|
for f in ("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"):
|
|
v = getattr(patch, f)
|
|
if v is not None:
|
|
fields.append(f"{f} = %s"); params.append(v)
|
|
if not fields:
|
|
raise HTTPException(400, "Nema polja za izmjenu")
|
|
fields.append("updated_at = now()")
|
|
params.append(lid)
|
|
with _conn() as conn, conn.cursor() as cur:
|
|
cur.execute(f"UPDATE pgz_sport.lijecnicki_pregledi SET {', '.join(fields)} WHERE id=%s RETURNING *",
|
|
params)
|
|
r = cur.fetchone()
|
|
if not r:
|
|
raise HTTPException(404, "Liječnički pregled ne postoji")
|
|
conn.commit()
|
|
return _row(r)
|
|
|
|
|
|
@router.delete("/lijecnicki/{lid}")
|
|
def delete_lijecnicki(lid: int):
|
|
with _conn() as conn, conn.cursor() as cur:
|
|
cur.execute("DELETE FROM pgz_sport.lijecnicki_pregledi WHERE id=%s RETURNING id", (lid,))
|
|
r = cur.fetchone()
|
|
conn.commit()
|
|
if not r:
|
|
raise HTTPException(404, "Liječnički pregled ne postoji")
|
|
return {"ok": True, "id": lid, "deleted": True}
|
|
|
|
|
|
# ───────────── ZZJZ PGŽ scheduling ─────────────
|
|
|
|
def _mock_zzjz_termini(week_start: date) -> list[dict]:
|
|
"""
|
|
Mock dostupnih termina za sportsku medicinu.
|
|
TODO: zamijeniti realnim scrapeom iz https://zzjzpgz.hr/djelatnosti/sportska-medicina/
|
|
Format termina: po danu (pon-pet), 09:00-15:00 svakih 30 min.
|
|
"""
|
|
out = []
|
|
times = ["08:00", "08:30", "09:00", "09:30", "10:00", "10:30",
|
|
"11:00", "11:30", "12:30", "13:00", "13:30", "14:00", "14:30"]
|
|
for d in range(5):
|
|
day = week_start + timedelta(days=d)
|
|
if day.weekday() >= 5:
|
|
continue
|
|
for t in times:
|
|
# pseudo-availability deterministic by day*hour
|
|
h = int(t.split(":")[0])
|
|
available = ((day.day + h) % 3) != 0
|
|
out.append({
|
|
"datum": day.isoformat(),
|
|
"vrijeme": t,
|
|
"doktor": "Dr. Sportska medicina",
|
|
"ustanova": "ZZJZ PGŽ",
|
|
"available": available,
|
|
"iznos_eur": 25.00,
|
|
})
|
|
return out
|
|
|
|
|
|
@router.get("/zzjz/info")
|
|
def zzjz_info():
|
|
"""Vraća kontakt + provjerava ima li online termin sustav (best-effort scrape)."""
|
|
online_booking = _detect_zzjz_booking()
|
|
return {**ZZJZ_INFO, "online_booking": online_booking}
|
|
|
|
|
|
def _detect_zzjz_booking() -> dict:
|
|
"""
|
|
Best-effort detekcija da li ZZJZ PGŽ ima online termin formu na stranici.
|
|
Vraća: {available: bool, url: str|None, kind: 'iframe'|'link'|'email'}
|
|
Ne baca iznimku — uvijek vrati strukturu (fallback je email).
|
|
"""
|
|
try:
|
|
import urllib.request
|
|
import re as _re
|
|
req = urllib.request.Request(ZZJZ_INFO["url_sportska_medicina"],
|
|
headers={"User-Agent": "PGZSport/1.0"})
|
|
with urllib.request.urlopen(req, timeout=4) as resp:
|
|
html = resp.read(200_000).decode("utf-8", errors="ignore")
|
|
# tražimo standardne oznake online booking sustava
|
|
patterns = [
|
|
r'(https?://[^"\']*(?:doktor|booking|narucivanje|naruci|termin)[^"\']*)',
|
|
r'<iframe[^>]+src="([^"]+)"',
|
|
]
|
|
for p in patterns:
|
|
m = _re.search(p, html, _re.IGNORECASE)
|
|
if m:
|
|
url = m.group(1)
|
|
if "iframe" in p:
|
|
return {"available": True, "url": url, "kind": "iframe"}
|
|
return {"available": True, "url": url, "kind": "link"}
|
|
return {"available": False, "url": ZZJZ_INFO["url_sportska_medicina"],
|
|
"kind": "email",
|
|
"fallback_email": ZZJZ_INFO["email"],
|
|
"note": "Nije pronađen online sustav — koristi e-mail kontakt."}
|
|
except Exception as e:
|
|
return {"available": False, "url": ZZJZ_INFO["url_sportska_medicina"],
|
|
"kind": "email",
|
|
"fallback_email": ZZJZ_INFO["email"],
|
|
"error": str(e)[:120],
|
|
"note": "Detekcija nije uspjela — fallback na e-mail."}
|
|
|
|
|
|
@router.get("/zzjz/termini")
|
|
def zzjz_termini(
|
|
od: Optional[date] = Query(None,
|
|
description="Početak tjedna; default = ovaj tjedan"),
|
|
):
|
|
"""
|
|
Vraća dostupne termine za sportsku medicinu pri ZZJZ PGŽ.
|
|
Trenutno: mock (deterministička dostupnost). Stvarna integracija
|
|
čeka API ili scraping form-e na zzjzpgz.hr.
|
|
"""
|
|
if od is None:
|
|
today = date.today()
|
|
od = today - timedelta(days=today.weekday())
|
|
termini = _mock_zzjz_termini(od)
|
|
return {
|
|
"ustanova": ZZJZ_INFO,
|
|
"week_start": od.isoformat(),
|
|
"count": len(termini),
|
|
"available": sum(1 for t in termini if t["available"]),
|
|
"termini": termini,
|
|
"note": "Mock podaci. Realni termini čekaju ZZJZ PGŽ API ili authorizirani scraper.",
|
|
}
|
|
|
|
|
|
@router.post("/lijecnicki/{lid}/zakazi")
|
|
def zakazi_termin(lid: int, body: ZakaziIn):
|
|
"""
|
|
Zakazuje termin za pregled.
|
|
- Ako ZZJZ PGŽ ima online booking → vraća iframe/deeplink URL.
|
|
- Ako nema → vraća mailto: deeplink za zahtjev e-mailom.
|
|
Status pregleda u DB se ažurira (ustanova + napomena).
|
|
"""
|
|
with _conn() as conn, conn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT l.id, l.clan_id, l.ustanova,
|
|
cl.ime || ' ' || cl.prezime AS clan,
|
|
cl.email AS clan_email,
|
|
k.naziv AS klub
|
|
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,))
|
|
r = cur.fetchone()
|
|
if not r:
|
|
raise HTTPException(404, "Liječnički pregled ne postoji")
|
|
new_napomena = (
|
|
f"Termin zakazan: {body.datum.isoformat()} {body.vrijeme} @ "
|
|
f"{body.ustanova}. {body.napomena or ''}"
|
|
).strip()
|
|
cur.execute("""
|
|
UPDATE pgz_sport.lijecnicki_pregledi
|
|
SET ustanova = COALESCE(%s, ustanova),
|
|
napomena = %s,
|
|
updated_at = now()
|
|
WHERE id = %s
|
|
RETURNING *
|
|
""", (body.ustanova, new_napomena, lid))
|
|
upd = cur.fetchone()
|
|
conn.commit()
|
|
|
|
booking = _detect_zzjz_booking()
|
|
from urllib.parse import quote as _q
|
|
subj = _q(f"Zahtjev za termin sportske medicine — {r.get('clan') or '(sportaš)'}")
|
|
body_email = _q(
|
|
f"Poštovani,\n\nMolim Vas termin za sportski liječnički pregled.\n\n"
|
|
f"Sportaš: {r.get('clan') or ''}\n"
|
|
f"Klub: {r.get('klub') or ''}\n"
|
|
f"Željeni datum: {body.datum.isoformat()} oko {body.vrijeme}\n"
|
|
f"Kontakt: {r.get('clan_email') or '(nepoznato)'}\n\n"
|
|
f"Lijep pozdrav,\nPGŽ Sport platforma"
|
|
)
|
|
mailto = f"mailto:{ZZJZ_INFO['email']}?subject={subj}&body={body_email}"
|
|
|
|
return {
|
|
"ok": True,
|
|
"id": lid,
|
|
"zakazano_za": f"{body.datum.isoformat()} {body.vrijeme}",
|
|
"ustanova": body.ustanova,
|
|
"zzjz": ZZJZ_INFO,
|
|
"booking": booking,
|
|
"mailto": mailto,
|
|
"note": (
|
|
"Online booking detektiran — koristi 'booking.url' za iframe/redirect."
|
|
if booking.get("available") else
|
|
"Online booking nije pronađen — fallback: koristi 'mailto' za zahtjev e-mailom."
|
|
),
|
|
"pregled": _row(upd),
|
|
}
|
|
|
|
|
|
class ZakaziEmailIn(BaseModel):
|
|
klub_id: Optional[int] = None
|
|
clan_id: int
|
|
zeljeni_datum: Optional[date] = None
|
|
zeljeno_vrijeme: Optional[str] = "09:00"
|
|
napomena: Optional[str] = None
|
|
|
|
|
|
@router.post("/lijecnicki/zakazi-email")
|
|
def zakazi_email(body: ZakaziEmailIn):
|
|
"""
|
|
Bez postojećeg pregleda — generira mailto: link s pred-popunjenim
|
|
podacima sportaša/kluba za slanje zahtjeva ZZJZ PGŽ.
|
|
"""
|
|
with _conn() as conn, conn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT cl.id, cl.ime || ' ' || cl.prezime AS clan,
|
|
cl.email AS clan_email, cl.telefon AS clan_telefon,
|
|
cl.datum_rodenja, cl.oib AS clan_oib,
|
|
k.naziv AS klub, k.oib AS klub_oib
|
|
FROM pgz_sport.clanovi cl
|
|
LEFT JOIN pgz_sport.klubovi k ON k.id = cl.klub_id
|
|
WHERE cl.id=%s
|
|
""", (body.clan_id,))
|
|
r = cur.fetchone()
|
|
if not r:
|
|
raise HTTPException(404, "Član ne postoji")
|
|
|
|
from urllib.parse import quote as _q
|
|
when = (body.zeljeni_datum.isoformat() if body.zeljeni_datum else "po dogovoru")
|
|
subj = _q(f"Zahtjev za termin sportske medicine — {r['clan']}")
|
|
body_email = _q(
|
|
f"Poštovani,\n\nMolim Vas termin za sportski liječnički pregled.\n\n"
|
|
f"Sportaš: {r['clan']}\n"
|
|
f"OIB: {r['clan_oib'] or '—'}\n"
|
|
f"Datum rođenja: {r['datum_rodenja'] or '—'}\n"
|
|
f"Klub: {r['klub'] or '—'}\n"
|
|
f"Željeni termin: {when} oko {body.zeljeno_vrijeme}\n"
|
|
f"Kontakt: {r['clan_email'] or '—'} / {r['clan_telefon'] or '—'}\n\n"
|
|
f"Napomena: {body.napomena or '—'}\n\n"
|
|
f"Lijep pozdrav,\nPGŽ Sport platforma"
|
|
)
|
|
mailto = f"mailto:{ZZJZ_INFO['email']}?subject={subj}&body={body_email}"
|
|
booking = _detect_zzjz_booking()
|
|
return {
|
|
"ok": True,
|
|
"clan": r["clan"],
|
|
"zzjz": ZZJZ_INFO,
|
|
"booking": booking,
|
|
"mailto": mailto,
|
|
}
|