Files
ordinaceprojekt/Recepty/report_server.py
T
Vladimir Buzalka adb84523cd Přidán podprojekt Recepty (eRecept SÚKL)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 07:06:17 +02:00

268 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Jednoduchý HTTP server — generuje HTML report léků z MySQL a servíruje ho na portu 8765.
"""
from http.server import BaseHTTPRequestHandler, HTTPServer
import pymysql, pymysql.cursors
DB = dict(host="192.168.1.76", user="root", password="Vlado9674+",
database="medicus", charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor)
ATC_POPIS = {
"N06AX": "Jiná antidepresiva",
"M04AA": "Léky proti dně (urikostatika)",
"C10BA": "Statiny v kombinaci",
"C10AA": "Statiny",
"N05BA": "Benzodiazepinová anxiolytika",
"R01BA": "Systémová dekongestiva",
"A02BC": "Inhibitory protonové pumpy",
"G04BE": "Léky na erektilní dysfunkci",
"C10AX": "Jiná hypolipidemika (ezetimib)",
"C09AA": "ACE inhibitory",
"N05BX": "Jiná anxiolytika",
"N02AJ": "Opioidy + neopioidy",
"R01AD": "Nosní kortikosteroidy",
"R06AX": "Antihistaminika",
"J01FA": "Makrolidová antibiotika",
"M03BX": "Centrální myorelaxancia",
"A10BX": "Inkretiny (GLP-1 agonisté)",
"R05CB": "Mukolytika",
"N05CF": "Z-hypnotika",
"J01CE": "Penicilinová antibiotika",
}
# Klíčová slova v navodu naznačující PRN (dle potřeby)
PRN_SLOVA = ("dle potřeby", "dle potreby", "p.p.", "d.p.", "pp ", " pp",
"při bolesti", "pri bolesti", "dle potreb", "podle potřeby",
"podle potreby", "podle potreby")
def query():
conn = pymysql.connect(**DB)
with conn.cursor() as cur:
cur.execute("""
SELECT
LEFT(p.atc, 5) AS atc_skupina,
MIN(p.nazev) AS nazev_leku,
COUNT(DISTINCT p.id) AS pocet_predpisu,
COUNT(DISTINCT v.id) AS pocet_vydani,
SUM(p.mnozstvi) AS celkove_mnozstvi,
AVG(p.mnozstvi) AS avg_mnozstvi,
MIN(p.datum_vystaveni) AS prvni_predpis,
MAX(p.datum_vystaveni) AS posledni_predpis,
DATEDIFF(MAX(p.datum_vystaveni),
MIN(p.datum_vystaveni)) AS rozpeti_dni,
MAX(p.opakovani) AS opakovani,
GROUP_CONCAT(p.navod ORDER BY p.datum_vystaveni SEPARATOR '|') AS navody,
GROUP_CONCAT(DISTINCT vp.odbornost
ORDER BY vp.odbornost) AS odbornosti,
GROUP_CONCAT(DISTINCT vp.nazev_pracoviste
ORDER BY vp.odbornost) AS pracoviste
FROM predpis p
LEFT JOIN vydej v
ON v.id_lp_predpis = p.id_lp_predpis
LEFT JOIN predepisujici pre
ON pre.lekar_kod = p.kod_predepisujiciho
LEFT JOIN vzp_pracoviste vp
ON vp.icp = pre.icp
AND vp.id = (
SELECT id FROM vzp_pracoviste
WHERE icp = pre.icp
ORDER BY platnost_od DESC LIMIT 1
)
WHERE p.atc IS NOT NULL
GROUP BY LEFT(p.atc, 5)
HAVING pocet_predpisu >= 2 OR MAX(p.opakovani) IS NOT NULL
""")
rows = cur.fetchall()
conn.close()
# Vypočítej pravidelnost v Pythonu a seřaď
for r in rows:
r["pravidelnost"], r["norm_interval"] = vypocitej_pravidelnost(r)
rows.sort(key=lambda r: (
["pravidelna", "mozna", "nepravidelna", "prn"].index(r["pravidelnost"]),
-(r["pocet_vydani"] or 0)
))
return rows
def vypocitej_pravidelnost(r):
"""Vrací (kategorie, normalizovany_interval_dni)."""
pocet = r["pocet_predpisu"] or 1
rozpeti = r["rozpeti_dni"] or 0
avg_mnoz = float(r["avg_mnozstvi"] or 1)
# PRN detekce — pokud většina návodů obsahuje PRN klíčová slova
navody = (r["navody"] or "").lower()
navod_list = navody.split("|")
prn_pocet = sum(1 for n in navod_list if any(s in n for s in PRN_SLOVA))
if prn_pocet > len(navod_list) / 2:
return "prn", None
if pocet < 2 or rozpeti == 0:
return "nepravidelna", None
avg_interval = rozpeti / (pocet - 1) # dny mezi předpisy
norm_interval = avg_interval / avg_mnoz # normalizováno na 1 balení
if norm_interval <= 40:
return "pravidelna", round(norm_interval)
elif norm_interval <= 100:
return "pravidelna", round(norm_interval)
elif norm_interval <= 185:
return "mozna", round(norm_interval)
else:
return "nepravidelna", round(norm_interval)
def badge_pravidelnost(pravidelnost, norm_interval):
if pravidelnost == "prn":
return '<span class="prav prav-prn">dle potřeby</span>'
if norm_interval is None:
return '<span class="prav prav-ne">?</span>'
if pravidelnost == "pravidelna":
return f'<span class="prav prav-ano">pravidelný · ~{norm_interval} dní/bal.</span>'
if pravidelnost == "mozna":
return f'<span class="prav prav-mozna">možná · ~{norm_interval} dní/bal.</span>'
return f'<span class="prav prav-ne">epizodický · ~{norm_interval} dní/bal.</span>'
def badge_odbornost(odbornosti, pracoviste):
if not odbornosti:
return '<span class="badge unknown">neznámá</span>'
kody = odbornosti.split(",")
nazvy = pracoviste.split(",") if pracoviste else []
parts = []
for i, kod in enumerate(kody):
nazev = nazvy[i].strip() if i < len(nazvy) else kod
cls = "gp" if kod.strip() in ("001", "002") else "spec"
parts.append(f'<span class="badge {cls}" title="{nazev}">{kod.strip()}</span>')
return " ".join(parts)
def bar(val, max_val, color):
pct = min(int(val / max_val * 100), 100)
return (f'<div class="bar-wrap">'
f'<div class="bar" style="width:{pct}%;background:{color}"></div>'
f'<span>{val}</span></div>')
def generate_html(rows):
max_vydani = max((r["pocet_vydani"] or 0 for r in rows), default=1)
radky = []
for r in rows:
atc = r["atc_skupina"] or ""
popis = ATC_POPIS.get(atc, "")
pr = r["pravidelnost"]
row_cls = {"pravidelna": "row-ano", "mozna": "row-mozna",
"prn": "row-prn", "nepravidelna": "row-ne"}.get(pr, "")
radky.append(f"""
<tr class="{row_cls}">
<td><strong>{atc}</strong><br><small>{popis}</small></td>
<td>{r['nazev_leku'] or ''}</td>
<td>{badge_pravidelnost(r['pravidelnost'], r['norm_interval'])}</td>
<td>{bar(r['pocet_predpisu'], max_vydani, '#4f8ef7')}</td>
<td>{bar(r['pocet_vydani'], max_vydani, '#34c97a')}</td>
<td style="font-size:0.82em;white-space:nowrap">
{r['prvni_predpis']}<br>→ {r['posledni_predpis']}
</td>
<td>{badge_odbornost(r['odbornosti'], r['pracoviste'])}</td>
</tr>""")
return f"""<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="utf-8">
<title>Lékový záznam — pravidelná medikace</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{ font-family: system-ui, sans-serif; background: #f4f6fb; color: #222; padding: 24px; }}
h1 {{ font-size: 1.4rem; margin-bottom: 4px; color: #1a2a4a; }}
.subtitle {{ color: #666; font-size: 0.9rem; margin-bottom: 20px; }}
table {{ width: 100%; border-collapse: collapse; background: #fff;
border-radius: 10px; overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.08); }}
th {{ background: #1a2a4a; color: #fff; padding: 10px 12px;
text-align: left; font-size: 0.82rem; white-space: nowrap; }}
td {{ padding: 9px 12px; font-size: 0.88rem; border-bottom: 1px solid #eef0f5;
vertical-align: middle; }}
tr:last-child td {{ border-bottom: none; }}
tr.row-ano td {{ background: #f0fdf4; }}
tr.row-mozna td {{ background: #fffbeb; }}
tr.row-ne td {{ background: #fafafa; }}
tr.row-prn td {{ background: #fef2f2; }}
tr:hover td {{ filter: brightness(0.96); }}
.bar-wrap {{ display: flex; align-items: center; gap: 6px; }}
.bar {{ height: 8px; border-radius: 4px; min-width: 2px; }}
.bar-wrap span {{ font-size: 0.82rem; color: #444; min-width: 20px; }}
.badge {{ display: inline-block; padding: 2px 7px; border-radius: 10px;
font-size: 0.75rem; font-weight: 600; cursor: default; }}
.badge.gp {{ background: #d1fae5; color: #065f46; }}
.badge.spec {{ background: #dbeafe; color: #1e40af; }}
.badge.unknown {{ background: #f3f4f6; color: #9ca3af; }}
.prav {{ display: inline-block; padding: 3px 9px; border-radius: 12px;
font-size: 0.78rem; font-weight: 600; white-space: nowrap; }}
.prav-ano {{ background: #dcfce7; color: #166534; }}
.prav-mozna {{ background: #fef9c3; color: #854d0e; }}
.prav-ne {{ background: #f3f4f6; color: #6b7280; }}
.prav-prn {{ background: #fee2e2; color: #991b1b; }}
.legend {{ margin-top: 16px; font-size: 0.8rem; color: #555;
display: flex; gap: 20px; flex-wrap: wrap; }}
</style>
</head>
<body>
<h1>Lékový záznam — pravidelná medikace</h1>
<p class="subtitle">
Skupiny ATC (5. místo) · seřazeno dle pravidelnosti ·
normalizovaný interval = průměrný počet dní na 1 balení
</p>
<table>
<thead>
<tr>
<th>ATC skupina</th>
<th>Lék (příklad)</th>
<th>Pravidelnost</th>
<th>Předpisů</th>
<th>Vydání</th>
<th>Rozsah</th>
<th>Odbornost</th>
</tr>
</thead>
<tbody>
{''.join(radky)}
</tbody>
</table>
<div class="legend">
<span><span class="prav prav-ano">pravidelný</span> norm. interval ≤ 100 dní/bal.</span>
<span><span class="prav prav-mozna">možná</span> 100185 dní/bal.</span>
<span><span class="prav prav-ne">epizodický</span> &gt;185 dní/bal.</span>
<span><span class="prav prav-prn">dle potřeby</span> navod obsahuje PRN</span>
<span><span class="badge gp">001</span> Praktický lékař &nbsp;
<span class="badge spec">xxx</span> Specialista</span>
</div>
</body>
</html>"""
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
html = generate_html(query()).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", len(html))
self.end_headers()
self.wfile.write(html)
def log_message(self, fmt, *args):
pass
if __name__ == "__main__":
server = HTTPServer(("localhost", 8765), Handler)
print("Report server bezi na http://localhost:8765")
server.serve_forever()