notebookvb

This commit is contained in:
2026-04-08 07:24:17 +02:00
parent 1f690810b3
commit 24635b955d
9 changed files with 52710 additions and 9 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "report-lekovy-zaznam",
"runtimeExecutable": "C:/Users/vlado/PycharmProjects/Recepty/.venv/Scripts/python",
"runtimeArgs": ["C:/Users/vlado/PycharmProjects/Recepty/report_server.py"],
"port": 8765
}
]
}
+51983
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -21,7 +21,7 @@ JMENA = "Vladimír"
DATUM_NAROZENI = "1973-09-20" DATUM_NAROZENI = "1973-09-20"
POCET_ZNAKU_ATC = 7 POCET_ZNAKU_ATC = 7
POCET_MESICU = 60 POCET_MESICU =60
VYSTUP = Path(__file__).parent / "odpoved_lekovy_zaznam.xml" VYSTUP = Path(__file__).parent / "odpoved_lekovy_zaznam.xml"
+178 -5
View File
@@ -54,6 +54,8 @@ DDL_TABULKY = [
"DROP TABLE IF EXISTS vydej", "DROP TABLE IF EXISTS vydej",
"DROP TABLE IF EXISTS predpis", "DROP TABLE IF EXISTS predpis",
"DROP TABLE IF EXISTS zprava", "DROP TABLE IF EXISTS zprava",
"DROP TABLE IF EXISTS predepisujici",
"DROP TABLE IF EXISTS vydavajici",
# ── zprava ──────────────────────────────────────────────────────────────── # ── zprava ────────────────────────────────────────────────────────────────
# zprava_odpoved_type + zprava_type: # zprava_odpoved_type + zprava_type:
@@ -186,6 +188,50 @@ DDL_TABULKY = [
INDEX idx_nazev (nazev) INDEX idx_nazev (nazev)
) ENGINE=InnoDB ) ENGINE=InnoDB
""", """,
# ── predepisujici ─────────────────────────────────────────────────────────
# PredepisujiciSeznam > Predepisujici:
# Lekar: Kod CHAR(36), Jmeno: Prijmeni(35), Jmena(24)
# ICZ CHAR(8)?, ICP CHAR(8)?
# PZS: Nazev(200), Adresa: NazevUlice, CisloPopisne, CisloOrientacni, NazevObce, PSC
# Telefon(20)?
# lekar_kod = predpis.kod_predepisujiciho
"""
CREATE TABLE predepisujici (
id INT AUTO_INCREMENT PRIMARY KEY,
lekar_kod CHAR(36) NOT NULL UNIQUE,
prijmeni VARCHAR(35),
jmena VARCHAR(24),
icz CHAR(8),
icp CHAR(8),
pzs_nazev VARCHAR(200),
ulice VARCHAR(150),
mesto VARCHAR(100),
psc CHAR(5),
telefon VARCHAR(20),
INDEX idx_icp (icp),
INDEX idx_icz (icz)
) ENGINE=InnoDB
""",
# ── vydavajici ────────────────────────────────────────────────────────────
# VydavajiciSeznam > Vydavajici:
# Lekarnik: Kod CHAR(36), Jmeno: Prijmeni(35), Jmena(24)
# PZS: Nazev(200), Telefon(20)?, Adresa: NazevUlice, CisloPopisne, CisloOrientacni, NazevObce, PSC
# lekarnik_kod = vydej.kod_vydavajiciho
"""
CREATE TABLE vydavajici (
id INT AUTO_INCREMENT PRIMARY KEY,
lekarnik_kod CHAR(36) NOT NULL UNIQUE,
prijmeni VARCHAR(35),
jmena VARCHAR(24),
pzs_nazev VARCHAR(200),
ulice VARCHAR(150),
mesto VARCHAR(100),
psc CHAR(5),
telefon VARCHAR(20)
) ENGINE=InnoDB
""",
] ]
@@ -199,6 +245,79 @@ def vytvor_schema(conn):
print("Schema OK (5 tabulek smazano a vytvoreno znovu)") print("Schema OK (5 tabulek smazano a vytvoreno znovu)")
# ── parsovani predepisujicich a vydavajicich ─────────────────────────────────
def _parsuj_adresu(pzs_el):
"""Ze elementu PZS vraci (ulice, mesto, psc)."""
adr = pzs_el.find(f"{{{NS}}}Adresa") if pzs_el is not None else None
if adr is None:
return None, None, None
ulice_parts = [
t(adr, "NazevUlice") or "",
t(adr, "CisloPopisne") or "",
t(adr, "CisloOrientacni") or "",
]
ulice = " ".join(p for p in ulice_parts if p).strip() or None
psc = t(adr, "PSC")
if psc and len(psc) > 5:
psc = psc[:5]
return ulice, t(adr, "NazevObce"), psc
def parsuj_predepisujici(doklad):
"""Vraci seznam slovniku pro tabulku predepisujici."""
seznam = []
sez_el = doklad.find(f"{{{NS}}}PredepisujiciSeznam")
if sez_el is None:
return seznam
for el in sez_el.findall(f"{{{NS}}}Predepisujici"):
lekar = el.find(f"{{{NS}}}Lekar")
if lekar is None:
continue
jmeno = lekar.find(f"{{{NS}}}Jmeno")
pzs = el.find(f"{{{NS}}}PZS")
ulice, mesto, psc = _parsuj_adresu(pzs)
seznam.append(dict(
lekar_kod = t(lekar, "Kod"),
prijmeni = t(jmeno, "Prijmeni") if jmeno is not None else None,
jmena = t(jmeno, "Jmena") if jmeno is not None else None,
icz = t(el, "ICZ"),
icp = t(el, "ICP"),
pzs_nazev = t(pzs, "Nazev") if pzs is not None else None,
ulice = ulice,
mesto = mesto,
psc = psc,
telefon = t(el, "Telefon"),
))
return seznam
def parsuj_vydavajici(doklad):
"""Vraci seznam slovniku pro tabulku vydavajici."""
seznam = []
sez_el = doklad.find(f"{{{NS}}}VydavajiciSeznam")
if sez_el is None:
return seznam
for el in sez_el.findall(f"{{{NS}}}Vydavajici"):
lekarnik = el.find(f"{{{NS}}}Lekarnik")
if lekarnik is None:
continue
jmeno = lekarnik.find(f"{{{NS}}}Jmeno")
pzs = el.find(f"{{{NS}}}PZS")
ulice, mesto, psc = _parsuj_adresu(pzs)
seznam.append(dict(
lekarnik_kod = t(lekarnik, "Kod"),
prijmeni = t(jmeno, "Prijmeni") if jmeno is not None else None,
jmena = t(jmeno, "Jmena") if jmeno is not None else None,
pzs_nazev = t(pzs, "Nazev") if pzs is not None else None,
ulice = ulice,
mesto = mesto,
psc = psc,
telefon = t(pzs, "Telefon") if pzs is not None else None,
))
return seznam
# ── parsovani leku ──────────────────────────────────────────────────────────── # ── parsovani leku ────────────────────────────────────────────────────────────
def parsuj_slozky_predpis(lek_el): def parsuj_slozky_predpis(lek_el):
@@ -385,7 +504,10 @@ def parsuj_xml(xml_soubor):
row.update(lek_fields) row.update(lek_fields)
vydeji.append((row, slozky)) vydeji.append((row, slozky))
return zprava, predpisy, vydeji predepisujici = parsuj_predepisujici(doklad)
vydavajici = parsuj_vydavajici(doklad)
return zprava, predpisy, vydeji, predepisujici, vydavajici
# ── ulozeni do DB ───────────────────────────────────────────────────────────── # ── ulozeni do DB ─────────────────────────────────────────────────────────────
@@ -397,7 +519,7 @@ def _najdi_id(cur, tabulka, sloupec, hodnota):
return row["id"] if row else None return row["id"] if row else None
def uloz(conn, zprava, predpisy, vydeji): def uloz(conn, zprava, predpisy, vydeji, predepisujici, vydavajici):
iplp_predpisu = 0 iplp_predpisu = 0
iplp_vydejuu = 0 iplp_vydejuu = 0
@@ -495,6 +617,56 @@ def uloz(conn, zprava, predpisy, vydeji):
print(f" vydeji: {vlozeno_v} novych (celkem {len(vydeji)})") print(f" vydeji: {vlozeno_v} novych (celkem {len(vydeji)})")
print(f" vydej_slozka: {vlozeno_vs} slozek z {iplp_vydejuu} IPLP vydejuu") print(f" vydej_slozka: {vlozeno_vs} slozek z {iplp_vydejuu} IPLP vydejuu")
# ── predepisujici ─────────────────────────────────────────────────────
vlozeno_pre = 0
for row in predepisujici:
if not row.get("lekar_kod"):
continue
cur.execute("""
INSERT INTO predepisujici
(lekar_kod, prijmeni, jmena, icz, icp,
pzs_nazev, ulice, mesto, psc, telefon)
VALUES
(%(lekar_kod)s, %(prijmeni)s, %(jmena)s, %(icz)s, %(icp)s,
%(pzs_nazev)s, %(ulice)s, %(mesto)s, %(psc)s, %(telefon)s)
ON DUPLICATE KEY UPDATE
prijmeni = VALUES(prijmeni),
jmena = VALUES(jmena),
icz = VALUES(icz),
icp = VALUES(icp),
pzs_nazev = VALUES(pzs_nazev),
ulice = VALUES(ulice),
mesto = VALUES(mesto),
psc = VALUES(psc),
telefon = VALUES(telefon)
""", row)
vlozeno_pre += cur.rowcount
print(f" predepisujici: {vlozeno_pre} radku (celkem {len(predepisujici)})")
# ── vydavajici ────────────────────────────────────────────────────────
vlozeno_vyd = 0
for row in vydavajici:
if not row.get("lekarnik_kod"):
continue
cur.execute("""
INSERT INTO vydavajici
(lekarnik_kod, prijmeni, jmena,
pzs_nazev, ulice, mesto, psc, telefon)
VALUES
(%(lekarnik_kod)s, %(prijmeni)s, %(jmena)s,
%(pzs_nazev)s, %(ulice)s, %(mesto)s, %(psc)s, %(telefon)s)
ON DUPLICATE KEY UPDATE
prijmeni = VALUES(prijmeni),
jmena = VALUES(jmena),
pzs_nazev = VALUES(pzs_nazev),
ulice = VALUES(ulice),
mesto = VALUES(mesto),
psc = VALUES(psc),
telefon = VALUES(telefon)
""", row)
vlozeno_vyd += cur.rowcount
print(f" vydavajici: {vlozeno_vyd} radku (celkem {len(vydavajici)})")
conn.commit() conn.commit()
@@ -505,15 +677,16 @@ def main():
print(f"XML: {xml} ({xml.stat().st_size // 1024} KB)") print(f"XML: {xml} ({xml.stat().st_size // 1024} KB)")
print("Parsovani XML ...") print("Parsovani XML ...")
zprava, predpisy, vydeji = parsuj_xml(xml) zprava, predpisy, vydeji, predepisujici, vydavajici = parsuj_xml(xml)
print(f" -> {len(predpisy)} predpisu, {len(vydeji)} vydejuu") print(f" -> {len(predpisy)} predpisu, {len(vydeji)} vydejuu, "
f"{len(predepisujici)} predepisujicich, {len(vydavajici)} vydavajicich")
print("Pripojeni k MySQL ...") print("Pripojeni k MySQL ...")
conn = pymysql.connect(**DB) conn = pymysql.connect(**DB)
try: try:
vytvor_schema(conn) vytvor_schema(conn)
print("Ukladani ...") print("Ukladani ...")
uloz(conn, zprava, predpisy, vydeji) uloz(conn, zprava, predpisy, vydeji, predepisujici, vydavajici)
print("Hotovo OK") print("Hotovo OK")
finally: finally:
conn.close() conn.close()
File diff suppressed because one or more lines are too long
+103
View File
@@ -0,0 +1,103 @@
# Report: Pravidelná medikace z lékového záznamu
## Soubor
`report_server.py` — HTTP server na portu 8765, generuje HTML report z MySQL (`medicus`).
Spuštění: `.venv/Scripts/python report_server.py`
---
## Zdroj dat
Tabulky v MySQL (db `medicus`):
| Tabulka | Obsah |
|---------|-------|
| `predpis` | předepsané léky, ATC, datum, množství, návod |
| `vydej` | vydání léku lékárnou |
| `predepisujici` | lékař — UUID, jméno, IČZ, IČP |
| `vydavajici` | lékárník — UUID, jméno, PZS |
| `vzp_pracoviste` | VZP číselník IČP → odbornost (import týdně) |
---
## Algoritmus pravidelnosti
### Krok 1 — Základní metriky (SQL)
```sql
GROUP BY LEFT(atc, 5) -- ATC na 5. místě (4. úroveň)
pocet_predpisu = COUNT(DISTINCT predpis.id)
celkove_mnozstvi = SUM(mnozstvi)
avg_mnozstvi = AVG(mnozstvi)
rozpeti_dni = DATEDIFF(MAX(datum), MIN(datum))
```
### Krok 2 — Výpočet normalizovaného intervalu (Python)
```
avg_interval = rozpeti_dni / (pocet_predpisu - 1) # průměr dní mezi předpisy
norm_interval = avg_interval / avg_mnozstvi # normalizováno na 1 balení
```
`norm_interval` = odhad, na kolik dní jedno balení vystačí.
### Krok 3 — Klasifikace
| Kategorie | Podmínka |
|-----------|----------|
| **nepravidelný** | `pocet_predpisu < 2` nebo `rozpeti_dni = 0` |
| **dle potřeby** (PRN) | většina návodů obsahuje klíčová slova: *dle potřeby, p.p., D.p., při bolesti* |
| **pravidelný** | `norm_interval ≤ 100 dní` |
| **možná** | `norm_interval 100185 dní` |
| **epizodický** | `norm_interval > 185 dní` |
> **Důležité:** `rozpeti_dni = 0` znamená, že všechny předpisy byly vystaveny ve stejný den
> (lékař dal více balení najednou). Není to opakované předepisování — musí být klasifikováno
> jako nepravidelné.
### Krok 4 — Řazení výstupu
1. Pravidelné
2. Možná pravidelné
3. Epizodické
4. PRN
V rámci kategorie sestupně dle počtu vydání.
---
## Odbornost předepisujícího
JOIN: `predpis → predepisujici → vzp_pracoviste`
Číselník VZP je aktuální snapshot — neobsahuje historická data. Recepty starší než
`platnost_od` nejnovějšího záznamu pro dané IČP by JOIN na datum nesplnily.
**Řešení:** vždy vzít nejnovější záznam pro IČP bez ohledu na datum předpisu:
```sql
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
)
```
Odbornost se prakticky nemění — IČP ordinace praktického lékaře zůstane `001` bez ohledu
na datum předpisu.
---
## Barevné kódování reportu
| Barva řádku | Kategorie |
|-------------|-----------|
| Zelená | pravidelný |
| Žlutá | možná pravidelný |
| Šedá | epizodický |
| Červená | dle potřeby (PRN) |
| Odznak odbornosti | Barva |
|-------------------|-------|
| Praktický lékař (001, 002) | zelená |
| Specialista | modrá |
| Neznámá | šedá |
+163
View File
@@ -0,0 +1,163 @@
"""
Import VZP číselníku pracovišť (soubory *.Lh7) do MySQL tabulky vzp_pracoviste.
Spouštět každý týden po stažení nového souboru do složky Import/.
"""
import csv
import glob
import os
import sys
from datetime import date, datetime
# Windows konzole - povol UTF-8 výstup
if sys.stdout.encoding != "utf-8":
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
import mysql.connector
DB_CONFIG = {
"host": "192.168.1.76",
"user": "root",
"password": "Vlado9674+",
"database": "medicus",
"charset": "utf8mb4",
}
IMPORT_DIR = os.path.join(os.path.dirname(__file__), "Import")
CREATE_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS vzp_pracoviste (
id INT NOT NULL AUTO_INCREMENT,
ico CHAR(8) NOT NULL,
icz CHAR(8) NOT NULL,
icp CHAR(8) NOT NULL,
odbornost VARCHAR(4) NOT NULL,
platnost_od DATE NOT NULL,
platnost_do DATE NOT NULL,
nazev_zarizeni VARCHAR(200),
nazev_pracoviste VARCHAR(200),
ulice VARCHAR(150),
mesto VARCHAR(100),
psc CHAR(5),
PRIMARY KEY (id),
INDEX idx_icp (icp),
INDEX idx_icz (icz),
INDEX idx_odbornost (odbornost),
INDEX idx_platnost (platnost_od, platnost_do)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
BATCH_SIZE = 1000
def parse_date(s: str) -> date | None:
"""Převede DDMMYYYY na date. Rok 3000 → 9999-12-31."""
s = s.strip()
if len(s) != 8:
return None
try:
d, m, y = int(s[0:2]), int(s[2:4]), int(s[4:8])
if y >= 3000:
return date(9999, 12, 31)
return date(y, m, d)
except ValueError:
return None
def find_latest_file() -> str:
files = glob.glob(os.path.join(IMPORT_DIR, "*.Lh7"))
if not files:
raise FileNotFoundError(f"Žádný *.Lh7 soubor nenalezen v {IMPORT_DIR}")
return max(files, key=os.path.getmtime)
def import_file(filepath: str, conn: mysql.connector.MySQLConnection) -> int:
cursor = conn.cursor()
cursor.execute("DROP TABLE IF EXISTS vzp_pracoviste")
cursor.execute(CREATE_TABLE_SQL)
conn.commit()
insert_sql = """
INSERT INTO vzp_pracoviste
(ico, icz, icp, odbornost, platnost_od, platnost_do,
nazev_zarizeni, nazev_pracoviste, ulice, mesto, psc)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
batch = []
total = 0
skipped = 0
with open(filepath, encoding="cp1250", errors="replace", newline="") as f:
reader = csv.reader(f, quotechar='"', skipinitialspace=True)
for row in reader:
if len(row) < 10:
skipped += 1
continue
platnost_od = parse_date(row[4])
platnost_do = parse_date(row[5])
if platnost_od is None or platnost_do is None:
skipped += 1
continue
# ulice = název ulice + číslo popisné + číslo orientační (sloupce 11,12,13)
ulice_parts = [row[11].strip(), row[12].strip(), row[13].strip()] if len(row) > 13 else []
ulice = " ".join(p for p in ulice_parts if p) or row[8].strip()
psc = row[14].strip() if len(row) > 14 else ""
if len(psc) > 5:
psc = psc[:5]
batch.append((
row[0].strip(), # ico
row[1].strip(), # icz
row[2].strip(), # icp
row[3].strip(), # odbornost
platnost_od,
platnost_do,
row[6].strip()[:200] if len(row) > 6 else "", # nazev_zarizeni
row[7].strip()[:200] if len(row) > 7 else "", # nazev_pracoviste
ulice[:150],
row[9].strip()[:100] if len(row) > 9 else "", # mesto
psc,
))
if len(batch) >= BATCH_SIZE:
cursor.executemany(insert_sql, batch)
conn.commit()
total += len(batch)
batch.clear()
if batch:
cursor.executemany(insert_sql, batch)
conn.commit()
total += len(batch)
cursor.close()
return total, skipped
def main():
filepath = sys.argv[1] if len(sys.argv) > 1 else find_latest_file()
filename = os.path.basename(filepath)
print(f"Soubor: {filename}")
print(f"Databáze: {DB_CONFIG['host']}/{DB_CONFIG['database']}")
print(f"Začátek: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
conn = mysql.connector.connect(**DB_CONFIG)
try:
total, skipped = import_file(filepath, conn)
finally:
conn.close()
print(f"Importováno: {total} záznamů")
if skipped:
print(f"Přeskočeno: {skipped} řádků (neúplná data)")
print(f"Hotovo: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if __name__ == "__main__":
main()
+1
View File
File diff suppressed because one or more lines are too long
+267
View File
@@ -0,0 +1,267 @@
"""
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()