diff --git a/Medevio/Testy/90_recon_recept.py b/Medevio/Testy/90_recon_recept.py new file mode 100644 index 0000000..2b1a6dd --- /dev/null +++ b/Medevio/Testy/90_recon_recept.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +RECON ONLY — nic nezakládá, nic neodesílá. +Otevře testovacího pacienta Vladko, otevře "Nový požadavek", +zachytí dostupné typy požadavků a podívá se na formulář "Recept". +Ukládá: screenshoty, HTML, plný GraphQL provoz (request + response). +""" +from pathlib import Path +from datetime import datetime +import sys, json, time +from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout + +try: + sys.stdout.reconfigure(encoding="utf-8", errors="replace") +except Exception: + pass + +HERE = Path(__file__).resolve().parent +STATE_FILE = HERE.parent / "medevio_storage.json" +PATIENT_UUID = "0210db7b-8fb0-4b47-b1d8-ec7a10849a63" # Vladko - testovaci aplikace +PATIENT_URL = f"https://my.medevio.cz/mudr-buzalkova/klinika/pacienti?pacient={PATIENT_UUID}" + +OUT = HERE / "recon_recept" +OUT.mkdir(exist_ok=True) +GQL_LOG = OUT / f"graphql_{int(time.time())}.jsonl" + + +def log(msg): + print(f"[{datetime.now():%H:%M:%S}] {msg}", flush=True) + + +def main(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=False, slow_mo=150) + context = browser.new_context(storage_state=str(STATE_FILE)) + page = context.new_page() + + # ---- capture GraphQL request + response bodies ---- + def on_response(resp): + try: + req = resp.request + if "graphql" in req.url and req.method == "POST": + rec = {"op": None, "request": None, "response": None, + "status": resp.status} + try: + rec["request"] = json.loads(req.post_data or "{}") + rec["op"] = rec["request"].get("operationName") + except Exception: + pass + try: + rec["response"] = resp.json() + except Exception: + pass + with open(GQL_LOG, "a", encoding="utf-8") as f: + f.write(json.dumps(rec, ensure_ascii=False) + "\n") + except Exception: + pass + + page.on("response", on_response) + + log(f"Otevírám kartu pacienta…") + page.goto(PATIENT_URL, wait_until="networkidle") + time.sleep(2) + page.screenshot(path=str(OUT / "01_card.png"), full_page=True) + + # ---- detect login / session expiry ---- + url_now = page.url + if "login" in url_now or "prihlaseni" in url_now or "auth" in url_now: + log(f"!!! Vypadá to na odhlášení / propadlou session. URL: {url_now}") + (OUT / "_SESSION_EXPIRED.txt").write_text(url_now, encoding="utf-8") + browser.close() + return + + # is the card actually visible? + card_ok = False + try: + page.get_by_text("Historie požadavků").wait_for(timeout=8000) + card_ok = True + log("Karta pacienta načtena (vidím 'Historie požadavků').") + except PWTimeout: + log("!!! Nevidím 'Historie požadavků' — možná jiný layout nebo session.") + + (OUT / "01_card.html").write_text(page.content(), encoding="utf-8") + + if not card_ok: + browser.close() + return + + # ---- open "Nový požadavek" ---- + try: + page.get_by_role("button", name="Nový požadavek").click() + time.sleep(1.0) + page.screenshot(path=str(OUT / "02_new_request_open.png"), full_page=True) + (OUT / "02_new_request_open.html").write_text(page.content(), encoding="utf-8") + log("Kliknuto 'Nový požadavek'.") + except Exception as e: + log(f"!!! Nepodařilo se kliknout 'Nový požadavek': {e}") + browser.close() + return + + # ---- capture all available request-type options (empty query) ---- + try: + opts = page.locator("[role='option']").all_text_contents() + (OUT / "03_all_options.txt").write_text( + "\n".join(opts), encoding="utf-8") + log(f"Dostupných typů (bez filtru): {len(opts)}") + except Exception as e: + log(f"options(all) chyba: {e}") + + # ---- type 'recept' and capture filtered options ---- + try: + page.keyboard.type("recept") + time.sleep(1.0) + opts2 = page.locator("[role='option']").all_text_contents() + (OUT / "04_recept_options.txt").write_text( + "\n".join(opts2), encoding="utf-8") + page.screenshot(path=str(OUT / "04_recept_options.png"), full_page=True) + (OUT / "04_recept_options.html").write_text(page.content(), encoding="utf-8") + log(f"Po napsání 'recept' nabízí: {opts2}") + except Exception as e: + log(f"options(recept) chyba: {e}") + + log("RECON hotovo — NIC nezaloženo. Zavírám za 3s.") + time.sleep(3) + browser.close() + log(f"Artefakty v: {OUT}") + log(f"GraphQL log: {GQL_LOG}") + + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/91_login_save_session.py b/Medevio/Testy/91_login_save_session.py new file mode 100644 index 0000000..54083ed --- /dev/null +++ b/Medevio/Testy/91_login_save_session.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Otevře přihlašovací okno Medevia. PŘIHLAŠ SE RUČNĚ. +Skript sám pozná, že už nejsi na přihlašovací stránce, počká na ustálení +a uloží session do medevio_storage.json. Žádné stisknutí Enter není třeba. +""" +from pathlib import Path +from datetime import datetime +import sys, time +from playwright.sync_api import sync_playwright + +try: + sys.stdout.reconfigure(encoding="utf-8", errors="replace") +except Exception: + pass + +HERE = Path(__file__).resolve().parent +STATE_FILE = HERE.parent / "medevio_storage.json" +LOGIN_URL = "https://my.medevio.cz/prihlaseni" +TIMEOUT_S = 300 # 5 minut na přihlášení + + +def log(msg): + print(f"[{datetime.now():%H:%M:%S}] {msg}", flush=True) + + +def is_logged_in(url: str) -> bool: + return ("medevio.cz" in url + and "prihlaseni" not in url + and "auth" not in url + and "login" not in url) + + +def main(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=False, slow_mo=80) + context = browser.new_context() + page = context.new_page() + page.goto(LOGIN_URL, wait_until="load") + + log("=== PŘIHLAS SE v otevřeném okně Medevia ===") + log("Skript čeká, až opustíš přihlašovací stránku…") + + deadline = time.time() + TIMEOUT_S + logged = False + while time.time() < deadline: + try: + if is_logged_in(page.url): + # počkej na ustálení redirectů + time.sleep(4) + if is_logged_in(page.url): + logged = True + break + except Exception: + pass + time.sleep(2) + + if not logged: + log("!!! Nepřihlášeno do limitu. Session NEULOŽENA.") + browser.close() + return + + log(f"Přihlášeno (URL: {page.url}). Ukládám session…") + context.storage_state(path=str(STATE_FILE)) + log(f"Session uložena: {STATE_FILE}") + time.sleep(1) + browser.close() + + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/recon_recept/01_card.png b/Medevio/Testy/recon_recept/01_card.png new file mode 100644 index 0000000..92706e5 Binary files /dev/null and b/Medevio/Testy/recon_recept/01_card.png differ diff --git a/Medevio/Testy/recon_recept/_SESSION_EXPIRED.txt b/Medevio/Testy/recon_recept/_SESSION_EXPIRED.txt new file mode 100644 index 0000000..3099ba9 --- /dev/null +++ b/Medevio/Testy/recon_recept/_SESSION_EXPIRED.txt @@ -0,0 +1 @@ +https://my.medevio.cz/prihlaseni \ No newline at end of file diff --git a/Medevio/medevio_api_notes.md b/Medevio/medevio_api_notes.md index 2cc8632..1245743 100644 --- a/Medevio/medevio_api_notes.md +++ b/Medevio/medevio_api_notes.md @@ -241,6 +241,51 @@ request { } ``` +### Request Creation (Vytvoření požadavku lékařem) — ODCHYCENO 2026-06-13 + +Lékařský účet (klinický token) **NEumí vyplnit pacientský dotazník** smysluplně — formulář +„Recept na léky" má z lékařské strany (`sid: ERECEPT_SIMPLEST_BEZ_DAVKOVANI`) jen jedno +pole `nazev-leku`, kdežto pacient v appce vyplní dvě pole („Název léků" + „Poznámka"). +**Proto: obsah z e-mailu zapisujeme do INTERNÍ POZNÁMKY, ne do dotazníku.** + +Vytvoření prázdného požadavku „Recept na léky" je **dvoukrok**: + +```graphql +# 1) vytvoř (prázdný) ECRF fill → vrátí ecrfFill.id +mutation ClinicRequestCreateModal_FillECRFForm($input: FillECRFFormInput!) { + ecrfFill: fillECRFForm(input: $input) { id } +} +# input: { byDoctor: true, fields: [], patientId, sid: "ERECEPT_SIMPLEST_BEZ_DAVKOVANI", stepId: "erecept-gp-request" } + +# 2) vytvoř požadavek s odkazem na ecrfFill +mutation ClinicRequestCreateModal_CreateRequest($clinicSlug: String!, $input: CreatePatientRequestWithoutReservationInput!) { + patientRequest: createPatientRequestWithoutReservation(clinicSlug: $clinicSlug, input: $input) { id } +} +# input: { patientId, userECRFId, ecrfFillIds: [], createdByDoctor: true, shouldInvitePatient: false } +``` + +| Klíč | Hodnota | +|------|---------| +| ECRF „Recept na léky" `userECRFId` | `79488e86-e9e5-47e3-8b19-7e5229427f23` | +| ECRF `sid` | `ERECEPT_SIMPLEST_BEZ_DAVKOVANI` | +| ECRF `stepId` | `erecept-gp-request` | + +Seznam typů požadavků: `UserEcrfAutocomplete_ListUserECRFsByClinic`. + +### Tagy / štítky požadavku — ODCHYCENO 2026-06-13 + +```graphql +query TagRequestEditModal_ListTags($clinicSlug: String!, $requestId: UUID!) { ... } # seznam štítků + zda jsou přiřazené +mutation TagRequestEditModal_AssignTagToRequest($clinicSlug: String!, $requestId: UUID!, $tagId: UUID!) { + tagRequest: assignTagToPatientRequest(clinicSlug: $clinicSlug, patientRequestId: $requestId, tagId: $tagId) { id } +} +``` + +| Štítek | tagId | barva | +|--------|-------|-------| +| `CLAUDE` | `c136aeca-0625-4c43-b81f-fc3949ec6ba6` | ORCHID | +| `NEZAPOMENOUT` | `5bced917-83d2-46db-896c-c8e615de1a69` | GREY | + ### Request Detail | Operation | Variables | Response | diff --git a/OrdinaceAgentEmail/NOTES.md b/OrdinaceAgentEmail/NOTES.md index da70dc4..5e4c88a 100644 --- a/OrdinaceAgentEmail/NOTES.md +++ b/OrdinaceAgentEmail/NOTES.md @@ -57,6 +57,33 @@ přesuny, odpovědi), žádný `state.json`. počítače, na Z230 → `reporter:c:\medicus\medicus.fdb`). - `ANTHROPIC_API_KEY` z `Medevio/.env`. +## Vytvoření požadavku v Medeviu — `medevio_recept.py` + +Funkce pro agenta: jakmile je pacient + léky správně identifikován, založí mu +v Medeviu požadavek **„Recept na léky"**, aby ho lékař viděl (Medevio kontrolujeme +průběžně, e-mail zřídka). + +```python +from medevio_recept import vytvor_recept +rid = vytvor_recept(rodne_cislo="730920/8104", + nazev_leku="Euthyrox 100 µg", + poznamka="docházejí mi léky") +``` + +Co se stane (vše odchyceno z webu Medevia 2026-06-13, ověřeno na testovacím pacientovi): +1. **RČ → patient UUID** přes MySQL `medevio.medevio_pacient` (`identification_number` → `patient_id`). +2. `fillECRFForm` (prázdný) → `createPatientRequestWithoutReservation` → založí „Recept na léky". +3. `createClinicPatientRequestNote` → obě pole do **interní poznámky** (formátováno „Název léků / Poznámka"). +4. `assignTagToPatientRequest` → štítek **CLAUDE** (`pridat_stitek=False` vypne). + +**Proč interní poznámka, ne dotazník:** lékařský přístup neumí vyplnit pacientský +ECRF dotazník smysluplně (z lékařské strany má jen 1 pole `nazev-leku`), proto obsah +jde do interní poznámky (viditelná jen pro ordinaci). + +Auth: Bearer token z `Medevio/token.txt` (dlouhodobý). Test: `python medevio_recept.py` +(založí testovací Recept na Vladkovi `0210db7b-…`). Pro test bez DB lookup je parametr +`patient_uuid=`. Mutace + konstanty jsou v `Medevio/medevio_api_notes.md`. + ## Známé limity / TODO - E-mailových kontaktů je v kartotéce málo (~70 z 6300 pacientů) — párování diff --git a/OrdinaceAgentEmail/medevio_recept.py b/OrdinaceAgentEmail/medevio_recept.py new file mode 100644 index 0000000..eb4a5b8 --- /dev/null +++ b/OrdinaceAgentEmail/medevio_recept.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +medevio_recept.py — vytvoření požadavku "Recept na léky" v Medeviu pro pacienta. + +Určeno k volání z e-mailového agenta (recepty_agent.py): jakmile agent správně +identifikuje pacienta a co chce, založí mu v Medeviu požadavek, aby ho lékař viděl +(e-mail kontrolujeme zřídka, Medevio pořád). + +Hlavní funkce: + vytvor_recept(rodne_cislo, nazev_leku, poznamka) -> request_id + +Co dělá (vše ověřeno/odchyceno 2026-06-13 na testovacím pacientovi Vladko): + 1. RČ -> patient UUID (MySQL medevio.medevio_pacient) + 2. fillECRFForm -> ecrfFill.id (prázdný formulář) + 3. createPatientRequestWithoutReservation -> založí "Recept na léky" + 4. createClinicPatientRequestNote -> obě pole do INTERNÍ POZNÁMKY + 5. assignTagToPatientRequest -> štítek CLAUDE + +POZOR: lékařský (klinický) přístup neumí vyplnit pacientský dotazník smysluplně, +proto obsah ("Název léků" + "Poznámka") jde do interní poznámky, ne do dotazníku. + +Auth: Bearer token z Medevio/token.txt (dlouhodobý API token). +""" +import sys +import re +from pathlib import Path + +import requests +import pymysql +from pymysql.cursors import DictCursor + +try: + sys.stdout.reconfigure(encoding="utf-8") +except Exception: + pass + +# ============================================================ +# Konstanty odchycené z Medevia (klinika mudr-buzalkova) +# ============================================================ +CLINIC_SLUG = "mudr-buzalkova" +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +RECEPT_USER_ECRF_ID = "79488e86-e9e5-47e3-8b19-7e5229427f23" # typ "Recept na léky" +RECEPT_SID = "ERECEPT_SIMPLEST_BEZ_DAVKOVANI" +RECEPT_STEP_ID = "erecept-gp-request" +CLAUDE_TAG_ID = "c136aeca-0625-4c43-b81f-fc3949ec6ba6" # štítek CLAUDE + +TOKEN_PATH = Path(__file__).resolve().parent.parent / "Medevio" / "token.txt" + +# MySQL je na různých strojích na různém portu — zkusíme kandidáty po řadě. +_MYSQL_BASE = dict(user="root", password="Vlado9674+", database="medevio") +_MYSQL_CANDIDATES = [ + dict(host="192.168.1.76", port=3306), + dict(host="192.168.1.76", port=3307), + dict(host="127.0.0.1", port=3307), + dict(host="127.0.0.1", port=3306), +] + +# ============================================================ +# GraphQL operace (přesně jak je posílá web Medevia) +# ============================================================ +M_FILL_ECRF = r""" +mutation ClinicRequestCreateModal_FillECRFForm($input: FillECRFFormInput!) { + ecrfFill: fillECRFForm(input: $input) { id } +} +""" + +M_CREATE_REQUEST = r""" +mutation ClinicRequestCreateModal_CreateRequest($clinicSlug: String!, $input: CreatePatientRequestWithoutReservationInput!) { + patientRequest: createPatientRequestWithoutReservation(clinicSlug: $clinicSlug, input: $input) { id } +} +""" + +M_CREATE_NOTE = r""" +mutation ClinicRequestNotes_Create($noteInput: CreateClinicPatientRequestNoteInput!) { + createClinicPatientRequestNote(noteInput: $noteInput) { id } +} +""" + +M_ASSIGN_TAG = r""" +mutation TagRequestEditModal_AssignTagToRequest($clinicSlug: String!, $requestId: UUID!, $tagId: UUID!) { + tagRequest: assignTagToPatientRequest(clinicSlug: $clinicSlug, patientRequestId: $requestId, tagId: $tagId) { id } +} +""" + + +# ============================================================ +# Pomocné funkce +# ============================================================ +def _read_token() -> str: + t = TOKEN_PATH.read_text(encoding="utf-8").strip() + return t[7:].strip() if t.lower().startswith("bearer ") else t + + +def _gql(query: str, variables: dict, token: str) -> dict: + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + r = requests.post(GRAPHQL_URL, json={"query": query, "variables": variables}, + headers=headers, timeout=30) + r.raise_for_status() + data = r.json() + if data.get("errors"): + raise RuntimeError(f"GraphQL chyba: {data['errors']}") + return data["data"] + + +def _mysql(): + last = None + for cand in _MYSQL_CANDIDATES: + try: + return pymysql.connect(**cand, **_MYSQL_BASE, + cursorclass=DictCursor, connect_timeout=4) + except Exception as e: + last = e + raise RuntimeError(f"MySQL (medevio) nedostupné na žádném kandidátovi: {last}") + + +def najdi_uuid_dle_rc(rodne_cislo: str) -> dict: + """RČ (s lomítkem i bez) -> řádek pacienta z medevio_pacient. + Vyhazuje LookupError když pacient není nalezen nebo je nejednoznačný.""" + rc = re.sub(r"\D", "", rodne_cislo or "") + if not rc: + raise ValueError("Prázdné / neplatné rodné číslo.") + conn = _mysql() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT patient_id, name, surname, status, user_id " + "FROM medevio_pacient " + "WHERE REPLACE(identification_number, '/', '') = %s", + (rc,), + ) + rows = cur.fetchall() + finally: + conn.close() + if not rows: + raise LookupError(f"Pacient s RČ {rc} není v Medevio databázi (medevio_pacient).") + if len(rows) > 1: + raise LookupError(f"RČ {rc} odpovídá více pacientům — nejednoznačné, řeš ručně.") + return rows[0] + + +def _format_note(nazev_leku: str, poznamka: str) -> str: + return ( + "Žádost o recept (zpracováno e-mailovým agentem).\n\n" + f"Název léků:\n{(nazev_leku or '').strip() or '—'}\n\n" + f"Poznámka:\n{(poznamka or '').strip() or '—'}" + ) + + +# ============================================================ +# Hlavní funkce +# ============================================================ +def vytvor_recept(rodne_cislo: str = None, nazev_leku: str = "", poznamka: str = "", + *, patient_uuid: str = None, pridat_stitek: bool = True, + token: str = None, verbose: bool = True) -> str: + """ + Založí pacientovi požadavek "Recept na léky" + interní poznámku + štítek CLAUDE. + + Parametry: + rodne_cislo – RČ pacienta (s lomítkem i bez); přeloží se na UUID přes MySQL. + nazev_leku – text 1. pole ("Název léků"). + poznamka – text 2. pole ("Poznámka"). + patient_uuid – volitelně rovnou UUID pacienta (obejde lookup dle RČ; pro testy). + pridat_stitek – True = přiřadí štítek CLAUDE. + token – volitelně Bearer token; jinak se načte z token.txt. + + Vrací: request_id založeného požadavku (str). + """ + token = token or _read_token() + + if patient_uuid: + uuid = patient_uuid + kdo = f"UUID {uuid}" + else: + pac = najdi_uuid_dle_rc(rodne_cislo) + uuid = pac["patient_id"] + kdo = f"{pac['surname']} {pac['name']} (RČ {rodne_cislo}, status {pac['status']})" + + if verbose: + print(f"→ Pacient: {kdo}") + + # 1) prázdný ECRF fill + fill = _gql(M_FILL_ECRF, {"input": { + "byDoctor": True, + "fields": [], + "patientId": uuid, + "sid": RECEPT_SID, + "stepId": RECEPT_STEP_ID, + }}, token) + ecrf_fill_id = fill["ecrfFill"]["id"] + + # 2) vytvoř požadavek "Recept na léky" + created = _gql(M_CREATE_REQUEST, {"clinicSlug": CLINIC_SLUG, "input": { + "patientId": uuid, + "userECRFId": RECEPT_USER_ECRF_ID, + "ecrfFillIds": [ecrf_fill_id], + "createdByDoctor": True, + "shouldInvitePatient": False, + }}, token) + request_id = created["patientRequest"]["id"] + if verbose: + print(f"✓ Požadavek vytvořen: {request_id}") + + # 3) interní poznámka s oběma poli + _gql(M_CREATE_NOTE, {"noteInput": { + "requestId": request_id, + "content": _format_note(nazev_leku, poznamka), + }}, token) + if verbose: + print("✓ Interní poznámka zapsána") + + # 4) štítek CLAUDE + if pridat_stitek: + _gql(M_ASSIGN_TAG, { + "clinicSlug": CLINIC_SLUG, + "requestId": request_id, + "tagId": CLAUDE_TAG_ID, + }, token) + if verbose: + print("✓ Štítek CLAUDE přiřazen") + + return request_id + + +# ============================================================ +# Test (na testovacím pacientovi Vladko) +# ============================================================ +if __name__ == "__main__": + VLADKO_UUID = "0210db7b-8fb0-4b47-b1d8-ec7a10849a63" # Vladko - testovací aplikace + rid = vytvor_recept( + patient_uuid=VLADKO_UUID, + nazev_leku="Euthyrox 100 µg (TEST z medevio_recept.py)", + poznamka="Testovací požadavek z funkce vytvor_recept — možno zavřít.", + ) + print(f"\nHOTOVO. request_id = {rid}") + print(f"https://my.medevio.cz/mudr-buzalkova/klinika/pacienti?pozadavek={rid}")