diff --git a/Medevio/.env b/Medevio/.env new file mode 100644 index 0000000..037f15b --- /dev/null +++ b/Medevio/.env @@ -0,0 +1 @@ +ANTHROPIC_API_KEY=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 diff --git a/Medevio/.gitignore b/Medevio/.gitignore new file mode 100644 index 0000000..26397e5 --- /dev/null +++ b/Medevio/.gitignore @@ -0,0 +1,16 @@ +# Virtual environment +.venv/ + +# Python +__pycache__/ +*.pyc + +# PyCharm / IDE +.idea/ + +# Claude worktrees +.claude/worktrees/ + +# OS +.DS_Store +Thumbs.db diff --git a/Medevio/10ReadPozadavky/01 Read pozadavky.py b/Medevio/10ReadPozadavky/01 Read pozadavky.py new file mode 100644 index 0000000..99423e4 --- /dev/null +++ b/Medevio/10ReadPozadavky/01 Read pozadavky.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Fetch Medevio pending (ACTIVE) patient requests and return a pandas DataFrame. +Reads Bearer token from token.txt (single line, token only). +""" + +import requests +import pandas as pd +import time +from pathlib import Path +from typing import List, Dict, Any + +# CONFIG --------------------------------------------------------------------- +TOKEN_FILE = str(Path(__file__).resolve().parent.parent / "token.txt") # centralized token +GRAPHQL_URL = "https://app.medevio.cz/graphql" +CLINIC_SLUG = "mudr-buzalkova" # adjust if needed +LOCALE = "cs" +PAGE_SIZE = 50 # how many items to request per page +REQUEST_WAIT = 0.2 # seconds between requests to be polite +# --------------------------------------------------------------------------- + +GRAPHQL_QUERY = r""" +query ClinicLegacyRequestList_ListPatientRequestsForClinic( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $state: PatientRequestState, + $pageInfo: PageInfo!, + $locale: Locale! +) { + requests: listPatientRequestsForClinic( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + state: $state, + pageInfo: $pageInfo + ) { + id + createdAt + dueDate + displayTitle(locale: $locale) + doneAt + removedAt + priority + evaluationResult(locale: $locale) { + fields { + name + value + } + } + clinicId + extendedPatient { + id + identificationNumber + kind + name + note + owner { name surname } + key + status + surname + type + user { id name surname } + isUnknownPatient + } + lastMessage { + createdAt + id + readAt + sender { id name surname clinicId } + text + } + queue { id name } + reservations { id canceledAt done start } + tags(onlyImportant: true) { id name color icon } + priceWhenCreated + currencyWhenCreated + } +} +""" + +def read_token(path: str) -> str: + with open(path, "r", encoding="utf-8") as f: + t = f.read().strip() + if t.startswith("Bearer "): + t = t.split(" ", 1)[1] + return t + +def fetch_requests(token: str, + clinic_slug: str = CLINIC_SLUG, + locale: str = LOCALE, + page_size: int = PAGE_SIZE) -> List[Dict[str, Any]]: + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + all_items: List[Dict[str, Any]] = [] + offset = 0 + + while True: + variables = { + "clinicSlug": clinic_slug, + "queueId": None, + "queueAssignment": "ANY", + "state": "ACTIVE", + "pageInfo": {"first": page_size, "offset": offset}, + "locale": locale, + } + payload = {"query": GRAPHQL_QUERY, "variables": variables, "operationName": "ClinicLegacyRequestList_ListPatientRequestsForClinic"} + + r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=30) + r.raise_for_status() + js = r.json() + + # Basic error handling + if "errors" in js: + raise RuntimeError(f"GraphQL returned errors: {js['errors']}") + + items = js.get("data", {}).get("requests", []) + if not items: + break + + all_items.extend(items) + + # If fewer than requested, we are at the end + if len(items) < page_size: + break + + offset += page_size + time.sleep(REQUEST_WAIT) + + return all_items + +def flatten_item(item: Dict[str, Any]) -> Dict[str, Any]: + patient = item.get("extendedPatient") or {} + last_msg = item.get("lastMessage") or {} + queue = item.get("queue") or {} + + # evaluationResult fields -> map of name:value (if exists) + eval_map = {} + eval_block = item.get("evaluationResult") or {} + for fld in (eval_block.get("fields") or []): + name = fld.get("name") + value = fld.get("value") + if name: + eval_map[name] = value + + flat = { + "id": item.get("id"), + "createdAt": item.get("createdAt"), + "dueDate": item.get("dueDate"), + "displayTitle": item.get("displayTitle"), + "doneAt": item.get("doneAt"), + "removedAt": item.get("removedAt"), + "priority": item.get("priority"), + "clinicId": item.get("clinicId"), + "patient_id": patient.get("id"), + "patient_identificationNumber": patient.get("identificationNumber"), + "patient_name": patient.get("name"), + "patient_surname": patient.get("surname"), + "patient_status": patient.get("status"), + "lastMessage_id": last_msg.get("id"), + "lastMessage_createdAt": last_msg.get("createdAt"), + "lastMessage_text": last_msg.get("text"), + "queue_id": queue.get("id"), + "queue_name": queue.get("name"), + "priceWhenCreated": item.get("priceWhenCreated"), + "currencyWhenCreated": item.get("currencyWhenCreated"), + } + + # merge evaluation fields (if any) prefixed by "eval_" + for k, v in eval_map.items(): + flat[f"eval_{k}"] = v + + return flat + +def to_dataframe(items: List[Dict[str, Any]]) -> pd.DataFrame: + rows = [flatten_item(it) for it in items] + df = pd.DataFrame(rows) + # try parsing dates + for col in ("createdAt", "dueDate", "doneAt", "lastMessage_createdAt", "removedAt"): + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + return df + +def main(): + token = read_token(TOKEN_FILE) + print("Fetching pending (ACTIVE) requests from Medevio...") + items = fetch_requests(token) + print(f"Fetched {len(items)} items.") + df = to_dataframe(items) + pd.set_option("display.max_rows", 20) + pd.set_option("display.max_colwidth", 160) + print(df.head(50)) + # optionally save + df.to_excel("medevio_pending_requests.xlsx", index=False) + print("Saved medevio_pending_requests.xlsx") + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/02 Start query creation.py b/Medevio/10ReadPozadavky/02 Start query creation.py new file mode 100644 index 0000000..7e87095 --- /dev/null +++ b/Medevio/10ReadPozadavky/02 Start query creation.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +from pathlib import Path +import requests # 👈 this is new + +# --- Settings ---------------------------------------------------- +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" +SHOW_FULL_TOKEN = False # set True if you want to print the full token +# ----------------------------------------------------------------- + +GRAPHQL_QUERY = r""" +query ClinicLegacyRequestList_ListPatientRequestsForClinic( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $state: PatientRequestState, + $pageInfo: PageInfo!, + $locale: Locale! +) { + requests: listPatientRequestsForClinic( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + state: $state, + pageInfo: $pageInfo + ) { + id + createdAt + dueDate + displayTitle(locale: $locale) + doneAt + removedAt + priority + evaluationResult(locale: $locale) { fields { name value } } + clinicId + extendedPatient { + id + identificationNumber + kind + name + surname + status + isUnknownPatient + } + lastMessage { id text createdAt } + queue { id name } + reservations { id canceledAt done start } + tags(onlyImportant: true) { id name color icon } + priceWhenCreated + currencyWhenCreated + } +} +""" + +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + +def main(): + token = read_token(TOKEN_PATH) + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "state": "ACTIVE", # pending / nevyřízené + "pageInfo": {"first": 30, "offset": 0}, + "locale": "cs", + } + + payload = { + "operationName": "ClinicLegacyRequestList_ListPatientRequestsForClinic", + "query": GRAPHQL_QUERY, + "variables": variables, + } + + # === Actually call Medevio API ================================== + print("📡 Querying Medevio GraphQL API...\n") + url = "https://api.medevio.cz/graphql" + r = requests.post(url, json=payload, headers=headers) + print(f"HTTP status: {r.status_code}\n") + + # --- Try to decode JSON + try: + data = r.json() + print("=== Raw JSON response ===") + print(json.dumps(data, indent=2, ensure_ascii=False)) + except Exception as e: + print("❌ Failed to decode JSON:", e) + print("Raw text:\n", r.text) + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/03 Read pozadavek simple.py b/Medevio/10ReadPozadavky/03 Read pozadavek simple.py new file mode 100644 index 0000000..4025479 --- /dev/null +++ b/Medevio/10ReadPozadavky/03 Read pozadavek simple.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +from pathlib import Path +import requests + +# --- Settings ---------------------------------------------------- +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" +# ----------------------------------------------------------------- + +GRAPHQL_QUERY = r""" +query ClinicLegacyRequestList_ListPatientRequestsForClinic( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $state: PatientRequestState, + $pageInfo: PageInfo!, + $locale: Locale! +) { + requests: listPatientRequestsForClinic( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + state: $state, + pageInfo: $pageInfo + ) { + id + displayTitle(locale: $locale) + extendedPatient { + name + surname + identificationNumber + } + } +} +""" + +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + +def main(): + token = read_token(TOKEN_PATH) + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "state": "ACTIVE", # pending / nevyřízené + "pageInfo": {"first": 30, "offset": 0}, + "locale": "cs", + } + + payload = { + "operationName": "ClinicLegacyRequestList_ListPatientRequestsForClinic", + "query": GRAPHQL_QUERY, + "variables": variables, + } + + url = "https://api.medevio.cz/graphql" + print("📡 Querying Medevio GraphQL API...\n") + r = requests.post(url, json=payload, headers=headers) + print(f"HTTP status: {r.status_code}\n") + + # --- Parse JSON safely + try: + data = r.json() + except Exception as e: + print("❌ Failed to decode JSON:", e) + print("Raw text:\n", r.text) + return + + requests_data = data.get("data", {}).get("requests", []) + if not requests_data: + print("⚠️ No requests found or invalid response.") + return + + print(f"📋 Found {len(requests_data)} active requests:\n") + for req in requests_data: + patient = req.get("extendedPatient", {}) + print(f"- {patient.get('surname','')} {patient.get('name','')} " + f"({patient.get('identificationNumber','')}) " + f"→ {req.get('displayTitle','')} [ID: {req.get('id')}]") + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/04 Dalsi.py b/Medevio/10ReadPozadavky/04 Dalsi.py new file mode 100644 index 0000000..944254a --- /dev/null +++ b/Medevio/10ReadPozadavky/04 Dalsi.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +from pathlib import Path +import requests + +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" + +# --- Try including `updatedAt` field directly --- +GRAPHQL_QUERY = r""" +query ClinicRequestGrid_ListPatientRequestsForClinic2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $pageInfo: PageInfo!, + $locale: Locale! +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + pageInfo: $pageInfo + ) { + count + patientRequests { + id + createdAt + updatedAt # 👈 TESTUJEME, jestli Medevio toto pole podporuje + doneAt + removedAt + displayTitle(locale: $locale) + lastMessage { createdAt } + extendedPatient { + name + surname + identificationNumber + } + } + } +} +""" + +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + + +def main(): + token = read_token(TOKEN_PATH) + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "pageInfo": {"first": 3, "offset": 0}, + "locale": "cs", + } + + payload = { + "operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + + url = "https://api.medevio.cz/graphql" + print("📡 Querying Medevio GraphQL API (testing `updatedAt` field)...\n") + + r = requests.post(url, json=payload, headers=headers) + print(f"HTTP status: {r.status_code}\n") + + try: + data = r.json() + except Exception as e: + print("❌ Failed to parse JSON:", e) + print("Raw text:\n", r.text) + return + + print("=== JSON response ===") + print(json.dumps(data, indent=2, ensure_ascii=False)) + + # Quick check: did it return an error message about updatedAt? + errors = data.get("errors") + if errors: + print("\n⚠️ Medevio returned GraphQL error:") + for e in errors: + print(f" → {e.get('message')}") + else: + print("\n✅ No errors, `updatedAt` might exist in schema!") + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/05 Další.py b/Medevio/10ReadPozadavky/05 Další.py new file mode 100644 index 0000000..5596579 --- /dev/null +++ b/Medevio/10ReadPozadavky/05 Další.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +from pathlib import Path +import requests + +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" + +GRAPHQL_QUERY = r""" +query ClinicRequestGrid_ListPatientRequestsForClinic2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $pageInfo: PageInfo!, + $locale: Locale! +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug + queueId: $queueId + queueAssignment: $queueAssignment + pageInfo: $pageInfo + ) { + count + patientRequests { + id + createdAt + doneAt + displayTitle(locale: $locale) + extendedPatient { + name + surname + identificationNumber + } + } + } +} +""" + +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + +def main(): + token = read_token(TOKEN_PATH) + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + # 👇 state zcela vynechán + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "pageInfo": {"first": 10, "offset": 0}, + "locale": "cs", + } + + payload = { + "operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + + url = "https://api.medevio.cz/graphql" + print("📡 Querying Medevio GraphQL API (no state argument)...\n") + r = requests.post(url, json=payload, headers=headers) + print(f"HTTP status: {r.status_code}\n") + + try: + data = r.json() + print("=== JSON response ===") + print(json.dumps(data, indent=2, ensure_ascii=False)) + except Exception as e: + print("❌ Failed to parse JSON:", e) + print("Raw text:\n", r.text) + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/06 Načti všechny požadavky do mySQL.py b/Medevio/10ReadPozadavky/06 Načti všechny požadavky do mySQL.py new file mode 100644 index 0000000..da23380 --- /dev/null +++ b/Medevio/10ReadPozadavky/06 Načti všechny požadavky do mySQL.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +import time +import pymysql +import requests +from pathlib import Path +from datetime import datetime + +# ================================ +# 🔧 CONFIGURATION +# ================================ +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" +BATCH_SIZE = 100 +STATES = ["ACTIVE", "DONE"] # optionally add "REMOVED" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY = r""" +query ClinicRequestGrid_ListPatientRequestsForClinic2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $pageInfo: PageInfo!, + $locale: Locale!, + $state: PatientRequestState +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + pageInfo: $pageInfo, + state: $state + ) { + count + patientRequests { + id + displayTitle(locale: $locale) + createdAt + updatedAt + doneAt + removedAt + extendedPatient { + name + surname + identificationNumber + } + } + } +} +""" + +# ================================ +# 🔑 TOKEN +# ================================ +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + +# ================================ +# 🕒 DATETIME CONVERSION +# ================================ +def to_mysql_dt(iso_str): + """Convert ISO 8601 (with Z) to MySQL DATETIME.""" + if not iso_str: + return None + try: + dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00")) + return dt.strftime("%Y-%m-%d %H:%M:%S") + except Exception: + return None + +# ================================ +# 💾 UPSERT TO MYSQL +# ================================ +def upsert(conn, r): + p = (r.get("extendedPatient") or {}) + sql = """ + INSERT INTO pozadavky ( + id, displayTitle, createdAt, updatedAt, doneAt, removedAt, + pacient_jmeno, pacient_prijmeni, pacient_rodnecislo + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + displayTitle=VALUES(displayTitle), + updatedAt=VALUES(updatedAt), + doneAt=VALUES(doneAt), + removedAt=VALUES(removedAt), + pacient_jmeno=VALUES(pacient_jmeno), + pacient_prijmeni=VALUES(pacient_prijmeni), + pacient_rodnecislo=VALUES(pacient_rodnecislo) + """ + vals = ( + r.get("id"), + r.get("displayTitle"), + to_mysql_dt(r.get("createdAt")), + to_mysql_dt(r.get("updatedAt")), + to_mysql_dt(r.get("doneAt")), + to_mysql_dt(r.get("removedAt")), + p.get("name"), + p.get("surname"), + p.get("identificationNumber"), + ) + with conn.cursor() as cur: + cur.execute(sql, vals) + conn.commit() + +# ================================ +# 📡 FETCH ONE BATCH +# ================================ +def fetch_batch(headers, state, offset): + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "pageInfo": {"first": BATCH_SIZE, "offset": offset}, + "locale": "cs", + "state": state, + } + payload = { + "operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) + r.raise_for_status() + data = r.json().get("data", {}).get("requestsResponse", {}) + return data.get("patientRequests", []), data.get("count", 0) + +# ================================ +# 🧠 MAIN +# ================================ +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + total_downloaded = 0 + total_upserted = 0 + + for state in STATES: + print(f"\n📡 STATE = {state}") + offset = 0 + state_total = None + while True: + batch, count_total = fetch_batch(headers, state, offset) + if state_total is None: + state_total = count_total + print(f" • Total from server: {state_total}") + if not batch: + break + print(f" • Offset {offset:>5}: got {len(batch)}") + for r in batch: + upsert(conn, r) + total_upserted += 1 + total_downloaded += len(batch) + offset += BATCH_SIZE + if offset >= state_total: + break + time.sleep(0.4) # respect API + + conn.close() + print(f"\n✅ Done. Downloaded {total_downloaded} items, upserted {total_upserted} rows (states: {', '.join(STATES)}).") + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/0601 Plně funkční stáhne všechny požadavky do mySQl.py b/Medevio/10ReadPozadavky/0601 Plně funkční stáhne všechny požadavky do mySQl.py new file mode 100644 index 0000000..f4fb0a2 --- /dev/null +++ b/Medevio/10ReadPozadavky/0601 Plně funkční stáhne všechny požadavky do mySQl.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +import time +import pymysql +import requests +from pathlib import Path +from datetime import datetime + +# ================================ +# 🔧 CONFIGURATION +# ================================ +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" +BATCH_SIZE = 1000 +STATES = ["ACTIVE", "DONE"] # optionally add "REMOVED" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY = r""" +query ClinicRequestGrid_ListPatientRequestsForClinic2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $pageInfo: PageInfo!, + $locale: Locale!, + $state: PatientRequestState +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + pageInfo: $pageInfo, + state: $state + ) { + count + patientRequests { + id + displayTitle(locale: $locale) + createdAt + updatedAt + doneAt + removedAt + extendedPatient { + name + surname + identificationNumber + } + } + } +} +""" + +# ================================ +# 🔑 TOKEN +# ================================ +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + +# ================================ +# 🕒 DATETIME CONVERSION +# ================================ +def to_mysql_dt(iso_str): + """Convert ISO 8601 (with Z) to MySQL DATETIME.""" + if not iso_str: + return None + try: + dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00")) + return dt.strftime("%Y-%m-%d %H:%M:%S") + except Exception: + return None + + + + +# ================================ +# 💾 UPSERT TO MYSQL +# ================================ +def upsert_many(conn, batch): + """Upsert multiple records in one commit.""" + if not batch: + return + sql = """ + INSERT INTO pozadavky ( + id, displayTitle, createdAt, updatedAt, doneAt, removedAt, + pacient_jmeno, pacient_prijmeni, pacient_rodnecislo + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + displayTitle=VALUES(displayTitle), + updatedAt=VALUES(updatedAt), + doneAt=VALUES(doneAt), + removedAt=VALUES(removedAt), + pacient_jmeno=VALUES(pacient_jmeno), + pacient_prijmeni=VALUES(pacient_prijmeni), + pacient_rodnecislo=VALUES(pacient_rodnecislo) + """ + vals = [] + for r in batch: + p = (r.get("extendedPatient") or {}) + vals.append(( + r.get("id"), + r.get("displayTitle"), + to_mysql_dt(r.get("createdAt")), + to_mysql_dt(r.get("updatedAt")), + to_mysql_dt(r.get("doneAt")), + to_mysql_dt(r.get("removedAt")), + p.get("name"), + p.get("surname"), + p.get("identificationNumber"), + )) + + with conn.cursor() as cur: + cur.executemany(sql, vals) + conn.commit() + +def upsert(conn, r): + p = (r.get("extendedPatient") or {}) + sql = """ + INSERT INTO pozadavky ( + id, displayTitle, createdAt, updatedAt, doneAt, removedAt, + pacient_jmeno, pacient_prijmeni, pacient_rodnecislo + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + displayTitle=VALUES(displayTitle), + updatedAt=VALUES(updatedAt), + doneAt=VALUES(doneAt), + removedAt=VALUES(removedAt), + pacient_jmeno=VALUES(pacient_jmeno), + pacient_prijmeni=VALUES(pacient_prijmeni), + pacient_rodnecislo=VALUES(pacient_rodnecislo) + """ + vals = ( + r.get("id"), + r.get("displayTitle"), + to_mysql_dt(r.get("createdAt")), + to_mysql_dt(r.get("updatedAt")), + to_mysql_dt(r.get("doneAt")), + to_mysql_dt(r.get("removedAt")), + p.get("name"), + p.get("surname"), + p.get("identificationNumber"), + ) + with conn.cursor() as cur: + cur.execute(sql, vals) + conn.commit() + +# ================================ +# 📡 FETCH ONE BATCH +# ================================ +def fetch_batch(headers, state, offset): + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "pageInfo": {"first": BATCH_SIZE, "offset": offset}, + "locale": "cs", + "state": state, + } + payload = { + "operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) + r.raise_for_status() + data = r.json().get("data", {}).get("requestsResponse", {}) + return data.get("patientRequests", []), data.get("count", 0) + +# ================================ +# 🧠 MAIN +# ================================ +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + total_downloaded = 0 + total_upserted = 0 + + for state in STATES: + print(f"\n📡 STATE = {state}") + offset = 0 + state_total = None + while True: + batch, count_total = fetch_batch(headers, state, offset) + if state_total is None: + state_total = count_total + print(f" • Total from server: {state_total}") + if not batch: + break + print(f" • Offset {offset:>5}: got {len(batch)}") + + # Perform one efficient upsert for the entire batch + upsert_many(conn, batch) + + total_upserted += len(batch) + total_downloaded += len(batch) + offset += BATCH_SIZE + if offset >= state_total: + break + time.sleep(10) # respect API + + conn.close() + print(f"\n✅ Done. Downloaded {total_downloaded} items, upserted {total_upserted} rows (states: {', '.join(STATES)}).") + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/07 Ulož přílohy požadavku do filesystem.py b/Medevio/10ReadPozadavky/07 Ulož přílohy požadavku do filesystem.py new file mode 100644 index 0000000..a3be66b --- /dev/null +++ b/Medevio/10ReadPozadavky/07 Ulož přílohy požadavku do filesystem.py @@ -0,0 +1,92 @@ +import requests +import json +from pathlib import Path + +# === Nastavení === +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +REQUEST_ID = "092a0c63-28be-4c6b-ab3b-204e1e2641d4" +OUTPUT_DIR = Path(r"u:\Dropbox\!!!Days\Downloads Z230\Medevio_přílohy") + +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + +GRAPHQL_QUERY = r""" +query ClinicRequestDetail_GetPatientRequest2( + $requestId: UUID!, + $isDoctor: Boolean! +) { + patientRequestMedicalRecords: listMedicalRecordsForPatientRequest( + attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT] + patientRequestId: $requestId + pageInfo: {first: 100, offset: 0} + ) { + attachmentType + id + medicalRecord { + contentType + description + downloadUrl + id + url + visibleToPatient @include(if: $isDoctor) + } + } +} +""" + +variables = { + "isDoctor": True, + "requestId": REQUEST_ID, +} + +headers = { + "Authorization": f"Bearer {read_token(TOKEN_PATH)}", + "Content-Type": "application/json", + "Accept": "application/json", +} + +payload = { + "operationName": "ClinicRequestDetail_GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": variables, +} + +print("📡 Querying Medevio API for attachments...\n") +r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) +print(f"HTTP status: {r.status_code}\n") + +data = r.json() +records = data.get("data", {}).get("patientRequestMedicalRecords", []) +if not records: + print("⚠️ No attachments found.") + exit() + +# === Uložení === +OUTPUT_DIR.mkdir(parents=True, exist_ok=True) +print(f"📂 Saving {len(records)} attachments to: {OUTPUT_DIR}\n") + +for rec in records: + med = rec.get("medicalRecord", {}) + url = med.get("downloadUrl") + name = med.get("description", med.get("id")) or "unknown.pdf" + + if not url: + print(f"❌ Skipped {name} (no download URL)") + continue + + safe_name = name.replace("/", "_").replace("\\", "_") + out_path = OUTPUT_DIR / safe_name + + print(f"⬇️ Downloading: {safe_name}") + try: + file_data = requests.get(url, timeout=30) + file_data.raise_for_status() + out_path.write_bytes(file_data.content) + print(f"✅ Saved: {out_path.name} ({len(file_data.content)/1024:.1f} KB)") + except Exception as e: + print(f"❌ Error saving {safe_name}: {e}") + +print("\n🎉 Done!") diff --git a/Medevio/10ReadPozadavky/0701 test.py b/Medevio/10ReadPozadavky/0701 test.py new file mode 100644 index 0000000..a0dc452 --- /dev/null +++ b/Medevio/10ReadPozadavky/0701 test.py @@ -0,0 +1,59 @@ +import requests +import json +from pathlib import Path + +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +REQUEST_ID = "092a0c63-28be-4c6b-ab3b-204e1e2641d4" +CLINIC_SLUG = "mudr-buzalkova" + +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + +GRAPHQL_QUERY = r""" +query ClinicRequestDetail_GetPatientRequest2( + $requestId: UUID!, + +) { + patientRequestMedicalRecords: listMedicalRecordsForPatientRequest( + attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT] + patientRequestId: $requestId + pageInfo: {first: 100, offset: 0} + ) { + attachmentType + id + medicalRecord { + contentType + description + downloadUrl + id + url + visibleToPatient + } + } +} +""" + + +variables = { + "requestId": REQUEST_ID, + } + +headers = { + "Authorization": f"Bearer {read_token(TOKEN_PATH)}", + "Content-Type": "application/json", + "Accept": "application/json", +} + +payload = { + "operationName": "ClinicRequestDetail_GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": variables, +} + +print("📡 Querying Medevio API...\n") +r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) +print(f"HTTP status: {r.status_code}\n") +print(json.dumps(r.json(), indent=2, ensure_ascii=False)) diff --git a/Medevio/10ReadPozadavky/0702 Plně funkční uložení přílohyfilesystem.py b/Medevio/10ReadPozadavky/0702 Plně funkční uložení přílohyfilesystem.py new file mode 100644 index 0000000..6355aa9 --- /dev/null +++ b/Medevio/10ReadPozadavky/0702 Plně funkční uložení přílohyfilesystem.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os,zlib +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time +import shutil + +# ============================== +# 🔧 CONFIGURATION +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" +BASE_DIR = Path(r"u:\Dropbox\ordinace\Dokumentace_ke_zpracování\Medevio_přílohy") +BASE_DIR.mkdir(parents=True, exist_ok=True) + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY = r""" +query ClinicRequestDetail_GetPatientRequest2( + $requestId: UUID!, +) { + patientRequestMedicalRecords: listMedicalRecordsForPatientRequest( + attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT] + patientRequestId: $requestId + pageInfo: {first: 100, offset: 0} + ) { + attachmentType + id + medicalRecord { + contentType + description + downloadUrl + id + url + visibleToPatient + } + } +} +""" + +def short_crc8(uuid_str: str) -> str: + """Return deterministic 8-char hex string from any input string (CRC32).""" + return f"{zlib.crc32(uuid_str.encode('utf-8')) & 0xffffffff:08x}" + +def extract_filename_from_url(url: str) -> str: + """Extracts filename from S3-style URL (between last '/' and first '?').""" + try: + filename = url.split("/")[-1].split("?")[0] + return filename + except Exception: + return "unknown_filename" + +def safe_rename(src: Path, dst: Path, retries: int = 5, delay: float = 3.0): + """Rename a folder with retries to avoid Dropbox/OneDrive sync lock issues.""" + for attempt in range(1, retries + 1): + try: + src.rename(dst) + return # success + except PermissionError as e: + print(f" ⚠️ Rename attempt {attempt}/{retries} failed ({e}) — waiting {delay}s...") + time.sleep(delay) + except Exception as e: + print(f" ❌ Unexpected rename error: {e}") + break + print(f" 🚫 Failed to rename '{src}' → '{dst}' after {retries} attempts.") + +# ============================== +# 🔑 TOKEN +# ============================== +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + +# ============================== +# 💾 DOWNLOAD FILE +# ============================== +def download_file(url: str, out_path: Path): + try: + r = requests.get(url, timeout=30) + r.raise_for_status() + out_path.parent.mkdir(parents=True, exist_ok=True) + with open(out_path, "wb") as f: + f.write(r.content) + print(f" 💾 Saved: {out_path.relative_to(BASE_DIR)}") + except Exception as e: + print(f" ⚠️ Failed to download {out_path.name}: {e}") + +# ============================== +# 📡 FETCH ATTACHMENTS +# ============================== +def fetch_attachments(headers, request_id): + variables = { + "requestId": request_id, + } + payload = { + "operationName": "ClinicRequestDetail_GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) + if r.status_code != 200: + print(f"❌ HTTP {r.status_code}") + return [] + data = r.json().get("data", {}).get("patientRequestMedicalRecords", []) + return data + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + with conn.cursor() as cur: + cur.execute(""" + SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno, createdAt + FROM pozadavky + WHERE displayTitle = 'Odeslat lékařskou zprávu' + """) + rows = cur.fetchall() + + print(f"📋 Found {len(rows)} 'Odeslat lékařskou zprávu' requests") + + for i, row in enumerate(rows, 1): + req_id = row["id"] + print(req_id) + prijmeni = row.get("pacient_prijmeni") or "Neznamy" + jmeno = row.get("pacient_jmeno") or "" + created = row.get("createdAt") + created_date = None + if created: + try: + created_date = datetime.strptime(str(created), "%Y-%m-%d %H:%M:%S").strftime("%Y-%m-%d") + except Exception: + created_date = "unknown" + + patient_dir = BASE_DIR / f"{prijmeni}, {jmeno}" / created_date + print(f"\n[{i}/{len(rows)}] 📂 {patient_dir.relative_to(BASE_DIR)}") + + attachments = fetch_attachments(headers, req_id) + # print(attachments) + + + if not attachments: + print(" ⚠️ No attachments") + continue + + + # vytvoř krátký CRC32 hash z UUID + uuid_short = short_crc8(str(req_id)) + + # Dočasná složka bez počtu + temp_dir = BASE_DIR / f"{prijmeni}, {jmeno}" / f"{created_date} {uuid_short}" + temp_dir.mkdir(parents=True, exist_ok=True) + + for a in attachments: + m = a.get("medicalRecord") or {} + # fname = m.get("description") or f"{m.get('id')}.bin" + url = m.get("downloadUrl") + fname = extract_filename_from_url(url) + + if url: + out_path = temp_dir / fname + download_file(url, out_path) + + + # Po stažení všech příloh spočítej skutečné soubory + real_count = len([f for f in temp_dir.iterdir() if f.is_file()]) + + # Přejmenuj složku na finální název s počtem + final_dir = temp_dir.parent / f"{temp_dir.name} ({real_count})" + if real_count != 0: + safe_rename(temp_dir, final_dir) + print(f" 📎 Saved {real_count} attachments → {final_dir.relative_to(BASE_DIR)}") + else: + print(f" ⚠️ No attachments for {temp_dir.name}") + temp_dir.rmdir() # smaž prázdnou složku + + + conn.close() + print("\n✅ Done!") + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/0704 Plne funkční uložení do mysql.py b/Medevio/10ReadPozadavky/0704 Plne funkční uložení do mysql.py new file mode 100644 index 0000000..8dbb117 --- /dev/null +++ b/Medevio/10ReadPozadavky/0704 Plne funkční uložení do mysql.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Download all 'Odeslat lékařskou zprávu' attachments from Medevio API +and store them (including binary content) directly into MySQL table `medevio_downloads`. + +Each attachment (PDF, image, etc.) is fetched once and saved as LONGBLOB. +Duplicate protection is ensured via UNIQUE KEY on `attachment_id`. +""" + +import zlib +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time + +# ============================== +# 🔧 CONFIGURATION +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY = r""" +query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) { + patientRequestMedicalRecords: listMedicalRecordsForPatientRequest( + attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT] + patientRequestId: $requestId + pageInfo: {first: 100, offset: 0} + ) { + attachmentType + id + medicalRecord { + contentType + description + downloadUrl + id + url + visibleToPatient + } + } +} +""" + +# ============================== +# 🧮 HELPERS +# ============================== +def short_crc8(uuid_str: str) -> str: + """Return deterministic 8-char hex string from any input string (CRC32).""" + return f"{zlib.crc32(uuid_str.encode('utf-8')) & 0xffffffff:08x}" + +def extract_filename_from_url(url: str) -> str: + """Extracts filename from S3-style URL (between last '/' and first '?').""" + try: + return url.split("/")[-1].split("?")[0] + except Exception: + return "unknown_filename" + +def read_token(p: Path) -> str: + """Read Bearer token from file.""" + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + +# ============================== +# 📡 FETCH ATTACHMENTS +# ============================== +def fetch_attachments(headers, request_id): + variables = {"requestId": request_id} + payload = { + "operationName": "ClinicRequestDetail_GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + if r.status_code != 200: + print(f"❌ HTTP {r.status_code} for request {request_id}") + return [] + data = r.json().get("data", {}).get("patientRequestMedicalRecords", []) + return data + +# ============================== +# 💾 SAVE TO MYSQL (with skip) +# ============================== +def insert_download(cur, req_id, a, m, jmeno, prijmeni, created_date, existing_ids): + attachment_id = a.get("id") + if attachment_id in existing_ids: + print(f" ⏭️ Skipping already downloaded attachment {attachment_id}") + return + + url = m.get("downloadUrl") + if not url: + print(" ⚠️ No download URL") + return + + filename = extract_filename_from_url(url) + try: + r = requests.get(url, timeout=30) + r.raise_for_status() + content = r.content + except Exception as e: + print(f" ⚠️ Failed to download {url}: {e}") + return + + file_size = len(content) + attachment_type = a.get("attachmentType") + content_type = m.get("contentType") + + cur.execute(""" + INSERT INTO medevio_downloads ( + request_id, attachment_id, attachment_type, filename, + content_type, file_size, pacient_jmeno, pacient_prijmeni, + created_at, file_content + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + file_content = VALUES(file_content), + file_size = VALUES(file_size), + downloaded_at = NOW() + """, ( + req_id, + attachment_id, + attachment_type, + filename, + content_type, + file_size, + jmeno, + prijmeni, + created_date, + content + )) + print(f" 💾 Saved {filename} ({file_size/1024:.1f} kB)") + existing_ids.add(attachment_id) # add to skip list + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + print("📦 Loading list of already downloaded attachments...") + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {row["attachment_id"] for row in cur.fetchall()} + print(f"✅ Found {len(existing_ids)} attachments already saved.") + + + with conn.cursor() as cur: + cur.execute(""" + SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno, createdAt + FROM pozadavky + WHERE displayTitle = 'Odeslat lékařskou zprávu' + """) + rows = cur.fetchall() + + print(f"📋 Found {len(rows)} 'Odeslat lékařskou zprávu' requests") + + for i, row in enumerate(rows, 1): + req_id = row["id"] + prijmeni = row.get("pacient_prijmeni") or "Neznamy" + jmeno = row.get("pacient_jmeno") or "" + created = row.get("createdAt") + + try: + created_date = datetime.strptime(str(created), "%Y-%m-%d %H:%M:%S") + except Exception: + created_date = None + + print(f"\n[{i}/{len(rows)}] 🧾 {prijmeni}, {jmeno} ({req_id})") + + attachments = fetch_attachments(headers, req_id) + if not attachments: + print(" ⚠️ No attachments") + continue + + with conn.cursor() as cur: + for a in attachments: + m = a.get("medicalRecord") or {} + insert_download(cur, req_id, a, m, jmeno, prijmeni, created_date, existing_ids) + conn.commit() + + print(f" ✅ {len(attachments)} attachments saved for {prijmeni}, {jmeno}") + time.sleep(0.5) # be nice to the API + + conn.close() + print("\n✅ Done! All attachments stored in MySQL table `medevio_downloads`.") + +# ============================== +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/08 Read communication.py b/Medevio/10ReadPozadavky/08 Read communication.py new file mode 100644 index 0000000..b226794 --- /dev/null +++ b/Medevio/10ReadPozadavky/08 Read communication.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Fetch communication threads (messages) from Medevio API +for pozadavky where communicationprocessed IS NULL or outdated, +optionally filtered by creation date. +Stores results in MySQL table `medevio_messages`. +""" + +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time + +# ============================== +# 🔧 CONFIGURATION +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +# ✅ Optional: Only process requests created after this date +# Leave empty ("") to process all +CREATED_AFTER = "2025-11-09" # 🕓 Adjust freely, or set to "" for no limit + +# ============================== +# 🔐 TOKEN +# ============================== +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok + + +headers = { + "Authorization": f"Bearer {read_token(TOKEN_PATH)}", + "Content-Type": "application/json", +} + +# ============================== +# 🧩 GRAPHQL QUERY +# ============================== +GRAPHQL_QUERY = """ +query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) { + messages: listMessages( + patientRequestId: $requestId + updatedSince: $updatedSince + ) { + id + createdAt + text + updatedAt + readAt + sender { id name surname clinicId } + medicalRecord { downloadUrl description contentType } + } +} +""" + +# ============================== +# 🧮 HELPERS +# ============================== +def normalize_ts(ts: str): + """Convert ISO 8601 string to MySQL DATETIME format.""" + if not ts: + return None + ts = ts.replace("T", " ").replace("Z", "") + if "." in ts: + ts = ts.split(".")[0] + return ts + + +# ============================== +# 📡 FETCH MESSAGES +# ============================== +def fetch_messages(request_id): + payload = { + "operationName": "UseMessages_ListMessages", + "variables": {"requestId": request_id, "updatedSince": None}, + "query": GRAPHQL_QUERY, + } + r = requests.post(GRAPHQL_URL, headers=headers, json=payload, timeout=30) + if r.status_code != 200: + print(f"❌ HTTP {r.status_code}: {r.text}") + return [] + return r.json().get("data", {}).get("messages", []) or [] + + +# ============================== +# 💾 CREATE TABLE IF NEEDED +# ============================== +def ensure_table_exists(conn): + with conn.cursor() as cur: + cur.execute(""" + CREATE TABLE IF NOT EXISTS medevio_messages ( + id VARCHAR(64) PRIMARY KEY, + request_id VARCHAR(64), + sender_name VARCHAR(255), + sender_id VARCHAR(64), + sender_clinic_id VARCHAR(64), + text TEXT, + created_at DATETIME NULL, + read_at DATETIME NULL, + updated_at DATETIME NULL, + attachment_url TEXT, + attachment_description TEXT, + attachment_content_type VARCHAR(128), + inserted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + """) + conn.commit() + + +# ============================== +# 💾 INSERT MESSAGE +# ============================== +def insert_message(cur, req_id, msg): + sender = msg.get("sender") or {} + medrec = msg.get("medicalRecord") or {} + + cur.execute(""" + REPLACE INTO medevio_messages ( + id, request_id, sender_name, sender_id, sender_clinic_id, text, + created_at, read_at, updated_at, + attachment_url, attachment_description, attachment_content_type + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + """, ( + msg.get("id"), + req_id, + f"{sender.get('name','')} {sender.get('surname','')}".strip(), + sender.get("id"), + sender.get("clinicId"), + msg.get("text"), + normalize_ts(msg.get("createdAt")), + normalize_ts(msg.get("readAt")), + normalize_ts(msg.get("updatedAt")), + medrec.get("downloadUrl"), + medrec.get("description"), + medrec.get("contentType") + )) + + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + conn = pymysql.connect(**DB_CONFIG) + ensure_table_exists(conn) + + with conn.cursor() as cur: + sql = """ + SELECT id, createdAt, updatedAt, communicationprocessed + FROM pozadavky + WHERE (communicationprocessed IS NULL OR communicationprocessed < updatedAt) + """ + if CREATED_AFTER: + sql += " AND createdAt >= %s" + cur.execute(sql, (CREATED_AFTER,)) + else: + cur.execute(sql) + + rows = cur.fetchall() + + if not rows: + print("✅ No pending communication updates.") + return + + print(f"📋 Found {len(rows)} requests needing communication check.") + + for i, row in enumerate(rows, 1): + req_id = row["id"] + print(f"\n[{i}/{len(rows)}] 🔍 Fetching communication for {req_id} ...") + + messages = fetch_messages(req_id) + print(f" 💬 {len(messages)} messages found.") + + # Update timestamp even if none found + with conn.cursor() as cur: + if messages: + for msg in messages: + insert_message(cur, req_id, msg) + cur.execute(""" + UPDATE pozadavky + SET communicationprocessed = NOW() + WHERE id = %s + """, (req_id,)) + conn.commit() + + print(f" ✅ Processed {len(messages)} messages for {req_id}") + time.sleep(0.5) # avoid hammering the API + + conn.close() + print("\n✅ All communication threads processed and timestamps updated.") + + +# ============================== +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/09 Read dotaznik.py b/Medevio/10ReadPozadavky/09 Read dotaznik.py new file mode 100644 index 0000000..c017c99 --- /dev/null +++ b/Medevio/10ReadPozadavky/09 Read dotaznik.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Download and store Medevio questionnaires (userNote + eCRF) for all patient requests. +Uses the verified working query "GetPatientRequest2". +""" + +import json +import requests +import pymysql +from datetime import datetime +from pathlib import Path +import time + +# ============================== +# 🔧 CONFIGURATION +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + + +from datetime import datetime + +def fix_datetime(dt_str): + """Convert ISO 8601 string with 'Z' or ms into MySQL DATETIME format.""" + if not dt_str: + return None + try: + # Remove trailing Z and parse flexible ISO format + return datetime.fromisoformat(dt_str.replace("Z", "").replace("+00:00", "")) + except Exception: + return None + +# ✅ Optional: limit which requests to process +CREATED_AFTER = "2025-11-09" # set "" to disable + +# ============================== +# 🧮 HELPERS +# ============================== +def read_token(p: Path) -> str: + """Read Bearer token from file.""" + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + + +GRAPHQL_QUERY = r""" +query GetPatientRequest2($requestId: UUID!, $clinicSlug: String!, $locale: Locale!) { + request: getPatientRequest2(patientRequestId: $requestId, clinicSlug: $clinicSlug) { + id + displayTitle(locale: $locale) + createdAt + updatedAt + userNote + eventType + extendedPatient(clinicSlug: $clinicSlug) { + name + surname + identificationNumber + } + ecrfFilledData(locale: $locale) { + name + groups { + label + fields { + name + label + type + value + } + } + } + } +} +""" + + +def fetch_questionnaire(headers, request_id, clinic_slug): + """Fetch questionnaire for given request ID.""" + payload = { + "operationName": "GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": { + "requestId": request_id, + "clinicSlug": clinic_slug, + "locale": "cs", + }, + } + r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=40) + if r.status_code != 200: + print(f"❌ HTTP {r.status_code} for {request_id}: {r.text}") + return None + return r.json().get("data", {}).get("request") + + +def insert_questionnaire(cur, req): + """Insert questionnaire data into MySQL.""" + if not req: + return + + patient = req.get("extendedPatient") or {} + ecrf_data = req.get("ecrfFilledData") + + created_at = fix_datetime(req.get("createdAt")) + updated_at = fix_datetime(req.get("updatedAt")) + + cur.execute(""" + INSERT INTO medevio_questionnaires ( + request_id, patient_name, patient_surname, patient_identification, + created_at, updated_at, user_note, ecrf_json + ) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + updated_at = VALUES(updated_at), + user_note = VALUES(user_note), + ecrf_json = VALUES(ecrf_json), + updated_local = NOW() + """, ( + req.get("id"), + patient.get("name"), + patient.get("surname"), + patient.get("identificationNumber"), + created_at, + updated_at, + req.get("userNote"), + json.dumps(ecrf_data, ensure_ascii=False), + )) + print(f" 💾 Stored questionnaire for {patient.get('surname','')} {patient.get('name','')}") + + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + with conn.cursor() as cur: + sql = """ + SELECT id, pacient_jmeno, pacient_prijmeni, createdAt, updatedAt, questionnaireprocessed + FROM pozadavky + WHERE (questionnaireprocessed IS NULL OR questionnaireprocessed < updatedAt) + """ + if CREATED_AFTER: + sql += " AND createdAt >= %s" + cur.execute(sql, (CREATED_AFTER,)) + else: + cur.execute(sql) + + rows = cur.fetchall() + + print(f"📋 Found {len(rows)} requests needing questionnaire check.") + + for i, row in enumerate(rows, 1): + req_id = row["id"] + print(f"\n[{i}/{len(rows)}] 🔍 Fetching questionnaire for {req_id} ...") + + req = fetch_questionnaire(headers, req_id, CLINIC_SLUG) + if not req: + print(" ⚠️ No questionnaire data found.") + continue + + with conn.cursor() as cur: + insert_questionnaire(cur, req) + cur.execute("UPDATE pozadavky SET questionnaireprocessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + + time.sleep(0.4) # polite pacing + + conn.close() + print("\n✅ Done! All questionnaires stored in MySQL table `medevio_questionnaires`.") + + +# ============================== +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/10 ReportpozadavkyExcel.py b/Medevio/10ReadPozadavky/10 ReportpozadavkyExcel.py new file mode 100644 index 0000000..ff5dd4e --- /dev/null +++ b/Medevio/10ReadPozadavky/10 ReportpozadavkyExcel.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pandas as pd +import pymysql +from datetime import datetime, timedelta + +# ================================ +# ⚙️ CONFIGURATION +# ================================ +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", +} + +# kam uložit výstup +OUTPUT_DIR = r"U:\Dropbox\!!!Days\Downloads Z230" +DAYS_BACK = 700 # posledních X dní + +# ================================ +# 📘 SQL dotaz +# ================================ +SQL = f""" +SELECT + m.id AS Message_ID, + m.request_id AS Request_ID, + m.created_at AS Datum_vytvoření, + m.sender_name AS Odesílatel, + m.text AS Text_zprávy, + m.pacient_jmeno AS Pacient_jméno, + m.pacient_prijmeni AS Pacient_příjmení, + m.pacient_rodnecislo AS Rodné_číslo +FROM medevio_messages m +WHERE m.created_at >= NOW() - INTERVAL {DAYS_BACK} DAY +ORDER BY m.created_at DESC; +""" + +# ================================ +# 🧠 MAIN +# ================================ +def main(): + conn = pymysql.connect(**DB_CONFIG) + df = pd.read_sql(SQL, conn) + conn.close() + + today = datetime.now().strftime("%Y-%m-%d") + output_path = f"{OUTPUT_DIR}\\Medevio_messages_report_{today}.xlsx" + + df.to_excel(output_path, index=False) + + print(f"✅ Export hotov: {output_path}") + print(f"📄 Počet řádků: {len(df)}") + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/10 UpdateMessageswithJmeno.py b/Medevio/10ReadPozadavky/10 UpdateMessageswithJmeno.py new file mode 100644 index 0000000..8cc9a60 --- /dev/null +++ b/Medevio/10ReadPozadavky/10 UpdateMessageswithJmeno.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pymysql +import requests +from datetime import datetime +from pathlib import Path +import time, socket + +# =============================== +# ⚙️ CONFIG +# =============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY = r""" +query ClinicRequestDetail_GetMessages( + $clinicSlug: String!, + $requestId: ID! +) { + clinicRequestDetail_GetPatientRequestMessages( + clinicSlug: $clinicSlug, + requestId: $requestId + ) { + id + text + createdAt + sender { + id + name + } + extendedPatient { + name + surname + identificationNumber + } + } +} +""" + +# =============================== +# 🔑 Token reader +# =============================== +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok + +# =============================== +# 🕒 Helper +# =============================== +def to_mysql_dt(iso_str): + if not iso_str: + return None + try: + dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00")) + return dt.strftime("%Y-%m-%d %H:%M:%S") + except Exception: + return None + +# =============================== +# 💾 Upsert +# =============================== +def upsert_message(conn, msg, request_id): + s = msg.get("sender") or {} + p = msg.get("extendedPatient") or {} + + sql = """ + INSERT INTO medevio_messages ( + id, request_id, sender_name, sender_id, text, created_at, + pacient_jmeno, pacient_prijmeni, pacient_rodnecislo + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + text=VALUES(text), + created_at=VALUES(created_at), + pacient_jmeno=VALUES(pacient_jmeno), + pacient_prijmeni=VALUES(pacient_prijmeni), + pacient_rodnecislo=VALUES(pacient_rodnecislo) + """ + + vals = ( + msg.get("id"), + request_id, + s.get("name"), + s.get("id"), + msg.get("text"), + to_mysql_dt(msg.get("createdAt")), + p.get("name"), + p.get("surname"), + p.get("identificationNumber"), + ) + + with conn.cursor() as cur: + cur.execute(sql, vals) + conn.commit() + +# =============================== +# 📡 Fetch messages for one request +# =============================== +def fetch_messages(headers, request_id): + payload = { + "operationName": "ClinicRequestDetail_GetMessages", + "query": GRAPHQL_QUERY, + "variables": {"clinicSlug": CLINIC_SLUG, "requestId": request_id}, + } + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) + r.raise_for_status() + data = r.json().get("data", {}).get("clinicRequestDetail_GetPatientRequestMessages", []) + return data + +# =============================== +# 🧠 Main +# =============================== +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + cur = conn.cursor() + + # vezmeme všechny request_id z tabulky pozadavky + cur.execute("SELECT id FROM pozadavky ORDER BY updatedAt DESC") + request_ids = [r["id"] for r in cur.fetchall()] + print(f"📋 Found {len(request_ids)} požadavků.") + + for i, rid in enumerate(request_ids, 1): + try: + msgs = fetch_messages(headers, rid) + for msg in msgs: + upsert_message(conn, msg, rid) + print(f"[{i}/{len(request_ids)}] {rid} → {len(msgs)} zpráv uloženo.") + time.sleep(0.4) + except Exception as e: + print(f"❌ Chyba při načítání {rid}: {e}") + + conn.close() + print("\n✅ Hotovo, všechny zprávy synchronizovány.") + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/10 test2.py b/Medevio/10ReadPozadavky/10 test2.py new file mode 100644 index 0000000..f0f2605 --- /dev/null +++ b/Medevio/10ReadPozadavky/10 test2.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pymysql +import requests +from pathlib import Path +from datetime import datetime +import time + +# ================================ +# ⚙️ CONFIGURATION +# ================================ +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" +BATCH_SIZE = 100 + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY = r""" +query ClinicRequestGrid_ListPatientRequestsForClinic2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $pageInfo: PageInfo!, + $locale: Locale!, + $state: PatientRequestState +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + pageInfo: $pageInfo, + state: $state + ) { + count + patientRequests { + id + displayTitle(locale: $locale) + createdAt + updatedAt + doneAt + removedAt + extendedPatient { + name + surname + identificationNumber + } + } + } +} +""" + +# ================================ +# 🔑 TOKEN +# ================================ +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok + +# ================================ +# 📡 FETCH FUNCTION +# ================================ +def fetch_requests(headers, state, offset=0): + """Fetch a batch of patient requests for a given state.""" + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "pageInfo": {"first": BATCH_SIZE, "offset": offset}, + "locale": "cs", + "state": state, + } + payload = { + "operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + + for attempt in range(3): # up to 3 attempts + try: + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + r.raise_for_status() + resp = r.json().get("data", {}).get("requestsResponse", {}) + return resp.get("patientRequests", []), resp.get("count", 0) + except requests.exceptions.RequestException as e: + print(f"⚠️ Chyba při načítání (pokus {attempt+1}/3): {e}") + time.sleep(5) + return [], 0 + +# ================================ +# 💾 UPDATE ALL MESSAGES BY PATIENT DATA +# ================================ +def update_all_messages(conn, patient): + """Update all messages belonging to this request with patient data.""" + p = patient.get("extendedPatient") or {} + if not p: + return 0 + + sql = """ + UPDATE medevio_messages + SET pacient_jmeno=%s, + pacient_prijmeni=%s, + pacient_rodnecislo=%s + WHERE request_id=%s + """ + vals = (p.get("name"), p.get("surname"), p.get("identificationNumber"), patient.get("id")) + with conn.cursor() as cur: + cur.execute(sql, vals) + affected = cur.rowcount + conn.commit() + return affected + +# ================================ +# 🧠 MAIN +# ================================ +def process_state(conn, headers, state): + print(f"\n=== 🟦 Zpracovávám {state} požadavky ===") + offset = 0 + total_processed = 0 + total_updated = 0 + + while True: + batch, total_count = fetch_requests(headers, state, offset) + if not batch: + break + + print(f"📦 Dávka od offsetu {offset} ({len(batch)} záznamů z {total_count})") + for r in batch: + updated = update_all_messages(conn, r) + total_processed += 1 + total_updated += updated + if updated: + print(f" ↳ {r.get('id')} → {updated} zpráv aktualizováno") + + offset += BATCH_SIZE + if offset >= total_count: + break + + time.sleep(0.4) + + print(f"✅ {state}: zpracováno {total_processed} požadavků, aktualizováno {total_updated} zpráv.") + return total_processed, total_updated + +# ================================ +# 🚀 ENTRY POINT +# ================================ +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + print(f"\n=== Medevio mass patient sync @ {datetime.now():%Y-%m-%d %H:%M:%S} ===") + + total_p, total_u = process_state(conn, headers, "ACTIVE") + done_p, done_u = process_state(conn, headers, "DONE") + + conn.close() + + print("\n=== 🧾 SOUHRN ===") + print(f"ACTIVE: {total_p} požadavků, {total_u} zpráv aktualizováno") + print(f"DONE: {done_p} požadavků, {done_u} zpráv aktualizováno") + print("===========================================") + print(f"CELKEM: {total_p + done_p} požadavků, {total_u + done_u} zpráv aktualizováno ✅") + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/705 Plně funkční uložení do mysql.py b/Medevio/10ReadPozadavky/705 Plně funkční uložení do mysql.py new file mode 100644 index 0000000..25be2c7 --- /dev/null +++ b/Medevio/10ReadPozadavky/705 Plně funkční uložení do mysql.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Download all attachments for pozadavky where attachmentsProcessed IS NULL +and (optionally) createdAt is newer than a configurable cutoff date. +Store them in MySQL table `medevio_downloads`, and update pozadavky.attachmentsProcessed = NOW(). +""" + +import zlib +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time + +# ============================== +# 🔧 CONFIGURATION +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +# ✅ Optional: Only process requests created after this date +# Leave empty ("") to process all +CREATED_AFTER = "2025-01-01" # 🕓 Adjust freely, or set to "" for no limit + +GRAPHQL_QUERY = r""" +query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) { + patientRequestMedicalRecords: listMedicalRecordsForPatientRequest( + attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT] + patientRequestId: $requestId + pageInfo: {first: 100, offset: 0} + ) { + attachmentType + id + medicalRecord { + contentType + description + downloadUrl + id + url + visibleToPatient + } + } +} +""" + +# ============================== +# 🧮 HELPERS +# ============================== +def short_crc8(uuid_str: str) -> str: + """Return deterministic 8-char hex string from any input string (CRC32).""" + return f"{zlib.crc32(uuid_str.encode('utf-8')) & 0xffffffff:08x}" + +def extract_filename_from_url(url: str) -> str: + """Extracts filename from S3-style URL (between last '/' and first '?').""" + try: + return url.split("/")[-1].split("?")[0] + except Exception: + return "unknown_filename" + +def read_token(p: Path) -> str: + """Read Bearer token from file.""" + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + +# ============================== +# 📡 FETCH ATTACHMENTS +# ============================== +def fetch_attachments(headers, request_id): + variables = {"requestId": request_id} + payload = { + "operationName": "ClinicRequestDetail_GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + if r.status_code != 200: + print(f"❌ HTTP {r.status_code} for request {request_id}") + return [] + data = r.json().get("data", {}).get("patientRequestMedicalRecords", []) + return data + +# ============================== +# 💾 SAVE TO MYSQL (with skip) +# ============================== +def insert_download(cur, req_id, a, m, jmeno, prijmeni, created_date, existing_ids): + attachment_id = a.get("id") + if attachment_id in existing_ids: + print(f" ⏭️ Skipping already downloaded attachment {attachment_id}") + return False + + url = m.get("downloadUrl") + if not url: + print(" ⚠️ No download URL") + return False + + filename = extract_filename_from_url(url) + try: + r = requests.get(url, timeout=30) + r.raise_for_status() + content = r.content + except Exception as e: + print(f" ⚠️ Failed to download {url}: {e}") + return False + + file_size = len(content) + attachment_type = a.get("attachmentType") + content_type = m.get("contentType") + + cur.execute(""" + INSERT INTO medevio_downloads ( + request_id, attachment_id, attachment_type, filename, + content_type, file_size, pacient_jmeno, pacient_prijmeni, + created_at, file_content + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + file_content = VALUES(file_content), + file_size = VALUES(file_size), + downloaded_at = NOW() + """, ( + req_id, + attachment_id, + attachment_type, + filename, + content_type, + file_size, + jmeno, + prijmeni, + created_date, + content + )) + existing_ids.add(attachment_id) + print(f" 💾 Saved {filename} ({file_size/1024:.1f} kB)") + return True + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + print("📦 Loading list of already downloaded attachments...") + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {row["attachment_id"] for row in cur.fetchall()} + print(f"✅ Found {len(existing_ids)} attachments already saved.") + + # ✅ Dynamic SQL with optional createdAt filter + sql = """ + SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno, createdAt + FROM pozadavky + WHERE attachmentsProcessed IS NULL + """ + params = [] + if CREATED_AFTER: + sql += " AND createdAt >= %s" + params.append(CREATED_AFTER) + + with conn.cursor() as cur: + cur.execute(sql, params) + rows = cur.fetchall() + + print(f"📋 Found {len(rows)} pozadavky to process (attachmentsProcessed IS NULL" + + (f", created >= {CREATED_AFTER}" if CREATED_AFTER else "") + ")") + + for i, row in enumerate(rows, 1): + time.sleep(1) # polite API delay + req_id = row["id"] + prijmeni = row.get("pacient_prijmeni") or "Neznamy" + jmeno = row.get("pacient_jmeno") or "" + created = row.get("createdAt") + + try: + created_date = datetime.strptime(str(created), "%Y-%m-%d %H:%M:%S") + except Exception: + created_date = None + + print(f"\n[{i}/{len(rows)}] 🧾 {prijmeni}, {jmeno} ({req_id})") + + attachments = fetch_attachments(headers, req_id) + if not attachments: + print(" ⚠️ No attachments found") + with conn.cursor() as cur: + cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + continue + + with conn.cursor() as cur: + for a in attachments: + m = a.get("medicalRecord") or {} + insert_download(cur, req_id, a, m, jmeno, prijmeni, created_date, existing_ids) + conn.commit() + + # ✅ mark processed + with conn.cursor() as cur: + cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + + print(f" ✅ {len(attachments)} attachments processed for {prijmeni}, {jmeno}") + time.sleep(0.3) # polite API delay + + conn.close() + print("\n✅ Done! All new attachments processed and pozadavky updated.") + +# ============================== +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py b/Medevio/10ReadPozadavky/PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py new file mode 100644 index 0000000..d6e613c --- /dev/null +++ b/Medevio/10ReadPozadavky/PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pymysql +import requests +from pathlib import Path +from datetime import datetime, timezone +import time +from dateutil import parser + +# Force UTF-8 output even under Windows Task Scheduler +import sys +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + # Python < 3.7 fallback (not needed for you, but safe) + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +# ================================ +# 🔧 CONFIGURATION +# ================================ +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" +BATCH_SIZE = 100 + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +# ⭐ NOVÝ TESTOVANÝ DOTAZ – obsahuje lastMessage.createdAt +GRAPHQL_QUERY = r""" +query ClinicRequestList2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $state: PatientRequestState, + $pageInfo: PageInfo!, + $locale: Locale! +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + state: $state, + pageInfo: $pageInfo + ) { + count + patientRequests { + id + displayTitle(locale: $locale) + createdAt + updatedAt + doneAt + removedAt + extendedPatient { + name + surname + identificationNumber + } + lastMessage { + createdAt + } + } + } +} +""" + + +# ================================ +# 🧿 SAFE DATETIME PARSER (ALWAYS UTC → LOCAL) +# ================================ +def to_mysql_dt_utc(iso_str): + """ + Parse Medevio timestamps safely. + Treat timestamps WITHOUT timezone as UTC. + Convert to local time before saving to MySQL. + """ + if not iso_str: + return None + try: + dt = parser.isoparse(iso_str) + + # If tz is missing → assume UTC + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + + # Convert to local timezone + dt_local = dt.astimezone() + + return dt_local.strftime("%Y-%m-%d %H:%M:%S") + except: + return None + + +# ================================ +# 🔑 TOKEN +# ================================ +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + return tok.split(" ", 1)[1] + return tok + + +# ================================ +# 💾 UPSERT (včetně správného updatedAt) +# ================================ +def upsert(conn, r): + p = r.get("extendedPatient") or {} + + # raw timestamps z API – nyní přes nový parser + api_updated = to_mysql_dt_utc(r.get("updatedAt")) + + last_msg = r.get("lastMessage") or {} + msg_updated = to_mysql_dt_utc(last_msg.get("createdAt")) + + # nejnovější změna + def max_dt(a, b): + if a and b: + return max(a, b) + return a or b + + final_updated = max_dt(api_updated, msg_updated) + + sql = """ + INSERT INTO pozadavky ( + id, displayTitle, createdAt, updatedAt, doneAt, removedAt, + pacient_jmeno, pacient_prijmeni, pacient_rodnecislo + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + displayTitle=VALUES(displayTitle), + updatedAt=VALUES(updatedAt), + doneAt=VALUES(doneAt), + removedAt=VALUES(removedAt), + pacient_jmeno=VALUES(pacient_jmeno), + pacient_prijmeni=VALUES(pacient_prijmeni), + pacient_rodnecislo=VALUES(pacient_rodnecislo) + """ + + vals = ( + r.get("id"), + r.get("displayTitle"), + to_mysql_dt_utc(r.get("createdAt")), + final_updated, + to_mysql_dt_utc(r.get("doneAt")), + to_mysql_dt_utc(r.get("removedAt")), + p.get("name"), + p.get("surname"), + p.get("identificationNumber"), + ) + + with conn.cursor() as cur: + cur.execute(sql, vals) + conn.commit() + + +# ================================ +# 📡 FETCH ACTIVE PAGE +# ================================ +def fetch_active(headers, offset): + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "pageInfo": {"first": BATCH_SIZE, "offset": offset}, + "locale": "cs", + "state": "ACTIVE", + } + + payload = { + "operationName": "ClinicRequestList2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) + r.raise_for_status() + + data = r.json().get("data", {}).get("requestsResponse", {}) + return data.get("patientRequests", []), data.get("count", 0) + + +# ================================ +# 🧠 MAIN +# ================================ +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + print(f"\n=== Sync ACTIVE požadavků @ {datetime.now():%Y-%m-%d %H:%M:%S} ===") + + offset = 0 + total_processed = 0 + total_count = None + + while True: + batch, count = fetch_active(headers, offset) + + if total_count is None: + total_count = count + print(f"📡 Celkem ACTIVE v Medevio: {count}") + + if not batch: + break + + for r in batch: + upsert(conn, r) + + total_processed += len(batch) + print(f" • {total_processed}/{total_count} ACTIVE processed") + + if offset + BATCH_SIZE >= count: + break + + offset += BATCH_SIZE + time.sleep(0.4) + + conn.close() + print("\n✅ ACTIVE sync hotovo!\n") + + +# ================================ +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/PRAVIDELNE_1_ReadLast300DonePozadavku.py b/Medevio/10ReadPozadavky/PRAVIDELNE_1_ReadLast300DonePozadavku.py new file mode 100644 index 0000000..c7663d8 --- /dev/null +++ b/Medevio/10ReadPozadavky/PRAVIDELNE_1_ReadLast300DonePozadavku.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pymysql +import requests +from pathlib import Path +from datetime import datetime +from dateutil import parser + +# ================================ +# 🔧 CONFIGURATION +# ================================ +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" + +LIMIT = 500 # batch size / number of records +FULL_DOWNLOAD = False # 🔥 TOGGLE: False = last X, True = ALL batches + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +# ⭐ Query with lastMessage +GRAPHQL_QUERY = r""" +query ClinicRequestList2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $state: PatientRequestState, + $pageInfo: PageInfo!, + $locale: Locale! +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + state: $state, + pageInfo: $pageInfo + ) { + count + patientRequests { + id + displayTitle(locale: $locale) + createdAt + updatedAt + doneAt + removedAt + extendedPatient { + name + surname + identificationNumber + } + lastMessage { + createdAt + } + } + } +} +""" + +# ================================ +# TOKEN +# ================================ +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok + +# ================================ +# DATETIME PARSER (UTC → MySQL) +# ================================ +def to_mysql_dt(iso_str): + if not iso_str: + return None + try: + dt = parser.isoparse(iso_str) # ISO8601 → aware datetime (UTC) + dt = dt.astimezone() # convert to local timezone + return dt.strftime("%Y-%m-%d %H:%M:%S") + except: + return None + +# ================================ +# UPSERT REQUEST +# ================================ +def upsert(conn, r): + p = r.get("extendedPatient") or {} + + api_updated = to_mysql_dt(r.get("updatedAt")) + last_msg = r.get("lastMessage") or {} + msg_at = to_mysql_dt(last_msg.get("createdAt")) + + def max_dt(a, b): + if a and b: + return max(a, b) + return a or b + + final_updated = max_dt(api_updated, msg_at) + + sql = """ + INSERT INTO pozadavky ( + id, displayTitle, createdAt, updatedAt, doneAt, removedAt, + pacient_jmeno, pacient_prijmeni, pacient_rodnecislo + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + displayTitle=VALUES(displayTitle), + updatedAt=VALUES(updatedAt), + doneAt=VALUES(doneAt), + removedAt=VALUES(removedAt), + pacient_jmeno=VALUES(pacient_jmeno), + pacient_prijmeni=VALUES(pacient_prijmeni), + pacient_rodnecislo=VALUES(pacient_rodnecislo) + """ + + vals = ( + r["id"], + r.get("displayTitle"), + to_mysql_dt(r.get("createdAt")), + final_updated, + to_mysql_dt(r.get("doneAt")), + to_mysql_dt(r.get("removedAt")), + p.get("name"), + p.get("surname"), + p.get("identificationNumber"), + ) + + with conn.cursor() as cur: + cur.execute(sql, vals) + + conn.commit() + +# ================================ +# FETCH DONE REQUESTS (one batch) +# ================================ +def fetch_done(headers, offset): + vars = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "pageInfo": {"first": LIMIT, "offset": offset}, + "locale": "cs", + "state": "DONE", + } + + payload = { + "operationName": "ClinicRequestList2", + "query": GRAPHQL_QUERY, + "variables": vars, + } + + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) + r.raise_for_status() + + data = r.json()["data"]["requestsResponse"] + return data.get("patientRequests", []), data.get("count", 0) + +# ================================ +# MAIN +# ================================ +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + print(f"\n=== Sync CLOSED requests @ {datetime.now():%Y-%m-%d %H:%M:%S} ===") + + offset = 0 + total_count = None + total_processed = 0 + + while True: + batch, count = fetch_done(headers, offset) + + if total_count is None: + total_count = count + print(f"📡 Total DONE in Medevio: {count}") + + if not batch: + break + + print(f" • Processing batch offset={offset} size={len(batch)}") + + for r in batch: + upsert(conn, r) + total_processed += len(batch) + + if not FULL_DOWNLOAD: + # process only last LIMIT records + break + + # FULL DOWNLOAD → fetch next batch + offset += LIMIT + if offset >= count: + break + + conn.close() + print(f"\n✅ DONE — {total_processed} requests synced.\n") + + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/PRAVIDELNE_2_ReadPoznamky.py b/Medevio/10ReadPozadavky/PRAVIDELNE_2_ReadPoznamky.py new file mode 100644 index 0000000..9913ce2 --- /dev/null +++ b/Medevio/10ReadPozadavky/PRAVIDELNE_2_ReadPoznamky.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Download and store Medevio questionnaires (userNote + eCRF) for all patient requests. +Uses the verified working query "GetPatientRequest2". +""" + +import json +import requests +import pymysql +from datetime import datetime +from pathlib import Path +import time +import sys + +# Force UTF-8 output even under Windows Task Scheduler +import sys +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + # Python < 3.7 fallback (not needed for you, but safe) + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +# ============================== +# 🛡 SAFE PRINT FOR CP1250 / EMOJI +# ============================== +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc.lower().startswith("utf"): + # strip emoji + anything above BMP + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + # final ASCII fallback + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ============================== +# 🔧 CONFIGURATION +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + + +# ============================== +# 🕒 DATETIME FIXER +# ============================== +def fix_datetime(dt_str): + """Convert ISO 8601 string with 'Z' or ms into MySQL DATETIME format.""" + if not dt_str: + return None + try: + return datetime.fromisoformat(dt_str.replace("Z", "").replace("+00:00", "")) + except Exception: + return None + + +# Optional filter +CREATED_AFTER = "2025-01-01" + + +# ============================== +# 🧮 HELPERS +# ============================== +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + return tok.split(" ", 1)[1] + return tok + + +GRAPHQL_QUERY = r""" +query GetPatientRequest2($requestId: UUID!, $clinicSlug: String!, $locale: Locale!) { + request: getPatientRequest2(patientRequestId: $requestId, clinicSlug: $clinicSlug) { + id + displayTitle(locale: $locale) + createdAt + updatedAt + userNote + eventType + extendedPatient(clinicSlug: $clinicSlug) { + name + surname + identificationNumber + } + ecrfFilledData(locale: $locale) { + name + groups { + label + fields { + name + label + type + value + } + } + } + } +} +""" + + +def fetch_questionnaire(headers, request_id, clinic_slug): + """Fetch questionnaire for given request ID.""" + payload = { + "operationName": "GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": { + "requestId": request_id, + "clinicSlug": clinic_slug, + "locale": "cs", + }, + } + r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=40) + if r.status_code != 200: + safe_print(f"❌ HTTP {r.status_code} for {request_id}: {r.text}") + return None + return r.json().get("data", {}).get("request") + + +def insert_questionnaire(cur, req): + """Insert questionnaire data into MySQL.""" + if not req: + return + + patient = req.get("extendedPatient") or {} + ecrf_data = req.get("ecrfFilledData") + + created_at = fix_datetime(req.get("createdAt")) + updated_at = fix_datetime(req.get("updatedAt")) + + cur.execute(""" + INSERT INTO medevio_questionnaires ( + request_id, created_at, updated_at, user_note, ecrf_json + ) + VALUES (%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + updated_at = VALUES(updated_at), + user_note = VALUES(user_note), + ecrf_json = VALUES(ecrf_json), + updated_local = NOW() + """, ( + req.get("id"), + created_at, + updated_at, + req.get("userNote"), + json.dumps(ecrf_data, ensure_ascii=False), + )) + + safe_print(f" 💾 Stored questionnaire for {patient.get('surname','')} {patient.get('name','')}") + + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + # load list of requests + with conn.cursor() as cur: + sql = """ + SELECT id, pacient_jmeno, pacient_prijmeni, createdAt, updatedAt, questionnaireprocessed + FROM pozadavky + WHERE (questionnaireprocessed IS NULL OR questionnaireprocessed < updatedAt) + """ + if CREATED_AFTER: + sql += " AND createdAt >= %s" + cur.execute(sql, (CREATED_AFTER,)) + else: + cur.execute(sql) + + rows = cur.fetchall() + + safe_print(f"📋 Found {len(rows)} requests needing questionnaire check.") + + # process each one + for i, row in enumerate(rows, 1): + req_id = row["id"] + safe_print(f"\n[{i}/{len(rows)}] 🔍 Fetching questionnaire for {req_id} ...") + + req = fetch_questionnaire(headers, req_id, CLINIC_SLUG) + if not req: + safe_print(" ⚠️ No questionnaire data found.") + continue + + with conn.cursor() as cur: + insert_questionnaire(cur, req) + cur.execute( + "UPDATE pozadavky SET questionnaireprocessed = NOW() WHERE id = %s", + (req_id,) + ) + conn.commit() + + time.sleep(0.6) + + conn.close() + safe_print("\n✅ Done! All questionnaires stored in MySQL table `medevio_questionnaires`.") + + +# ============================== +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaci.py b/Medevio/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaci.py new file mode 100644 index 0000000..97712b0 --- /dev/null +++ b/Medevio/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaci.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Stáhne konverzaci pro požadavky, kde: +messagesProcessed IS NULL OR messagesProcessed < updatedAt. + +Vloží do medevio_conversation a přílohy do medevio_downloads. +""" + +import zlib +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time +import sys + +# Force UTF-8 output even under Windows Task Scheduler +import sys +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + # Python < 3.7 fallback (not needed for you, but safe) + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +# ============================== +# 🛡 SAFE PRINT FOR CP1250 / EMOJI +# ============================== +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc or not enc.lower().startswith("utf"): + # strip emoji + characters outside BMP for Task Scheduler (CP1250) + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + # fallback pure ASCII + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ============================== +# 🔧 CONFIGURATION +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY_MESSAGES = r""" +query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) { + messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) { + id + createdAt + updatedAt + readAt + text + type + sender { + id + name + surname + clinicId + } + medicalRecord { + id + description + contentType + url + downloadUrl + token + createdAt + updatedAt + } + } +} +""" + +# ============================== +# ⏱ DATETIME PARSER +# ============================== +def parse_dt(s): + if not s: + return None + try: + return datetime.fromisoformat(s.replace("Z", "+00:00")) + except: + pass + try: + return datetime.strptime(s[:19], "%Y-%m-%dT%H:%M:%S") + except: + return None + +# ============================== +# 🔐 TOKEN +# ============================== +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + return tok.replace("Bearer ", "") + +# ============================== +# 📡 FETCH MESSAGES +# ============================== +def fetch_messages(headers, request_id): + payload = { + "operationName": "UseMessages_ListMessages", + "query": GRAPHQL_QUERY_MESSAGES, + "variables": {"requestId": request_id, "updatedSince": None}, + } + + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + if r.status_code != 200: + safe_print(f"❌ HTTP {r.status_code} for request {request_id}") + return [] + return r.json().get("data", {}).get("messages", []) or [] + + +# ============================== +# 💾 SAVE MESSAGE +# ============================== +def insert_message(cur, req_id, msg): + + sender = msg.get("sender") or {} + sender_name = " ".join( + x for x in [sender.get("name"), sender.get("surname")] if x + ) or None + + sql = """ + INSERT INTO medevio_conversation ( + id, request_id, + sender_name, sender_id, sender_clinic_id, + text, created_at, read_at, updated_at, + attachment_url, attachment_description, attachment_content_type + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + sender_name = VALUES(sender_name), + sender_id = VALUES(sender_id), + sender_clinic_id = VALUES(sender_clinic_id), + text = VALUES(text), + created_at = VALUES(created_at), + read_at = VALUES(read_at), + updated_at = VALUES(updated_at), + attachment_url = VALUES(attachment_url), + attachment_description = VALUES(attachment_description), + attachment_content_type = VALUES(attachment_content_type) + """ + + mr = msg.get("medicalRecord") or {} + + cur.execute(sql, ( + msg.get("id"), + req_id, + sender_name, + sender.get("id"), + sender.get("clinicId"), + msg.get("text"), + parse_dt(msg.get("createdAt")), + parse_dt(msg.get("readAt")), + parse_dt(msg.get("updatedAt")), + mr.get("downloadUrl") or mr.get("url"), + mr.get("description"), + mr.get("contentType") + )) + + +# ============================== +# 💾 DOWNLOAD MESSAGE ATTACHMENT +# ============================== +def insert_download(cur, req_id, msg, existing_ids): + + mr = msg.get("medicalRecord") or {} + attachment_id = mr.get("id") + if not attachment_id: + return + + if attachment_id in existing_ids: + return + + url = mr.get("downloadUrl") or mr.get("url") + if not url: + return + + try: + r = requests.get(url, timeout=30) + r.raise_for_status() + data = r.content + except Exception as e: + safe_print(f"⚠️ Failed to download: {e}") + return + + filename = url.split("/")[-1].split("?")[0] + + cur.execute(""" + INSERT INTO medevio_downloads ( + request_id, attachment_id, attachment_type, + filename, content_type, file_size, created_at, file_content + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + file_content = VALUES(file_content), + file_size = VALUES(file_size), + downloaded_at = NOW() + """, ( + req_id, + attachment_id, + "MESSAGE_ATTACHMENT", + filename, + mr.get("contentType"), + len(data), + parse_dt(msg.get("createdAt")), + data + )) + + existing_ids.add(attachment_id) + + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + # ---- Load existing attachments + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {row["attachment_id"] for row in cur.fetchall()} + + safe_print(f"📦 Already downloaded attachments: {len(existing_ids)}\n") + + # ---- Select pozadavky needing message sync + sql = """ + SELECT id + FROM pozadavky + WHERE messagesProcessed IS NULL + OR messagesProcessed < updatedAt + """ + with conn.cursor() as cur: + cur.execute(sql) + requests_to_process = cur.fetchall() + + safe_print(f"📋 Found {len(requests_to_process)} pozadavků requiring message sync.\n") + + # ---- Process each record + for idx, row in enumerate(requests_to_process, 1): + req_id = row["id"] + safe_print(f"[{idx}/{len(requests_to_process)}] Processing {req_id} …") + + messages = fetch_messages(headers, req_id) + + with conn.cursor() as cur: + for msg in messages: + insert_message(cur, req_id, msg) + insert_download(cur, req_id, msg, existing_ids) + conn.commit() + + with conn.cursor() as cur: + cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + + safe_print(f" ✅ {len(messages)} messages saved\n") + time.sleep(0.25) + + conn.close() + safe_print("🎉 Done!") + + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaciDELTA.py b/Medevio/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaciDELTA.py new file mode 100644 index 0000000..fb2c3cb --- /dev/null +++ b/Medevio/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaciDELTA.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Delta sync Medevio communication. +Stáhne pouze zprávy změněné po messagesProcessed pro každý požadavek. +""" + +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time +import sys + +# ============================== +# UTF-8 SAFE OUTPUT +# ============================== +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc.lower().startswith("utf"): + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ============================== +# CONFIG +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY_MESSAGES = r""" +query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) { + messages: listMessages( + patientRequestId: $requestId, + updatedSince: $updatedSince + ) { + id + createdAt + updatedAt + readAt + text + type + sender { + id + name + surname + clinicId + } + medicalRecord { + id + description + contentType + url + downloadUrl + createdAt + updatedAt + } + } +} +""" + + +# ============================== +# HELPERS +# ============================== +def parse_dt(s): + if not s: + return None + try: + return datetime.fromisoformat(s.replace("Z", "+00:00")) + except Exception: + return None + + +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + return tok.replace("Bearer ", "") + + +# ============================== +# FETCH MESSAGES (DELTA) +# ============================== +def fetch_messages(headers, request_id, updated_since): + payload = { + "operationName": "UseMessages_ListMessages", + "query": GRAPHQL_QUERY_MESSAGES, + "variables": { + "requestId": request_id, + "updatedSince": updated_since, + }, + } + + r = requests.post( + "https://api.medevio.cz/graphql", + json=payload, + headers=headers, + timeout=30 + ) + + if r.status_code != 200: + safe_print(f"❌ HTTP {r.status_code} for request {request_id}") + return [] + + j = r.json() + if "errors" in j: + safe_print(f"❌ GraphQL error for {request_id}: {j['errors']}") + return [] + + return j.get("data", {}).get("messages", []) or [] + + +# ============================== +# INSERT MESSAGE +# ============================== +def insert_message(cur, req_id, msg): + sender = msg.get("sender") or {} + sender_name = " ".join( + x for x in [sender.get("name"), sender.get("surname")] if x + ) or None + + mr = msg.get("medicalRecord") or {} + + sql = """ + INSERT INTO medevio_conversation ( + id, request_id, + sender_name, sender_id, sender_clinic_id, + text, created_at, read_at, updated_at, + attachment_url, attachment_description, attachment_content_type + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + sender_name = VALUES(sender_name), + sender_id = VALUES(sender_id), + sender_clinic_id = VALUES(sender_clinic_id), + text = VALUES(text), + created_at = VALUES(created_at), + read_at = VALUES(read_at), + updated_at = VALUES(updated_at), + attachment_url = VALUES(attachment_url), + attachment_description = VALUES(attachment_description), + attachment_content_type = VALUES(attachment_content_type) + """ + + cur.execute(sql, ( + msg.get("id"), + req_id, + sender_name, + sender.get("id"), + sender.get("clinicId"), + msg.get("text"), + parse_dt(msg.get("createdAt")), + parse_dt(msg.get("readAt")), + parse_dt(msg.get("updatedAt")), + mr.get("downloadUrl") or mr.get("url"), + mr.get("description"), + mr.get("contentType") + )) + + +# ============================== +# INSERT ATTACHMENT (DEDUP) +# ============================== +def insert_download(cur, req_id, msg, existing_ids): + mr = msg.get("medicalRecord") or {} + attachment_id = mr.get("id") + if not attachment_id or attachment_id in existing_ids: + return + + url = mr.get("downloadUrl") or mr.get("url") + if not url: + return + + try: + r = requests.get(url, timeout=30) + r.raise_for_status() + data = r.content + except Exception as e: + safe_print(f"⚠️ Attachment download failed: {e}") + return + + filename = url.split("/")[-1].split("?")[0] + + cur.execute(""" + INSERT INTO medevio_downloads ( + request_id, attachment_id, attachment_type, + filename, content_type, file_size, created_at, file_content + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + file_content = VALUES(file_content), + file_size = VALUES(file_size), + downloaded_at = NOW() + """, ( + req_id, + attachment_id, + "MESSAGE_ATTACHMENT", + filename, + mr.get("contentType"), + len(data), + parse_dt(msg.get("createdAt")), + data + )) + + existing_ids.add(attachment_id) + + +# ============================== +# MAIN +# ============================== +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + # existing attachments + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {r["attachment_id"] for r in cur.fetchall()} + + # select requests needing sync + with conn.cursor() as cur: + cur.execute(""" + SELECT id, messagesProcessed + FROM pozadavky + WHERE messagesProcessed IS NULL + OR messagesProcessed < updatedAt + """) + rows = cur.fetchall() + + safe_print(f"📋 Found {len(rows)} requests for message delta-sync\n") + + for i, row in enumerate(rows, 1): + req_id = row["id"] + updated_since = row["messagesProcessed"] + if updated_since: + updated_since = updated_since.replace(microsecond=0).isoformat() + "Z" + + safe_print(f"[{i}/{len(rows)}] {req_id}") + + messages = fetch_messages(headers, req_id, updated_since) + if not messages: + safe_print(" ⏭ No new messages") + else: + with conn.cursor() as cur: + for msg in messages: + insert_message(cur, req_id, msg) + insert_download(cur, req_id, msg, existing_ids) + conn.commit() + safe_print(f" ✅ {len(messages)} new/updated messages") + + with conn.cursor() as cur: + cur.execute( + "UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", + (req_id,) + ) + conn.commit() + + time.sleep(0.25) + + conn.close() + safe_print("\n🎉 Delta message sync DONE") + + +# ============================== +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py b/Medevio/10ReadPozadavky/PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py new file mode 100644 index 0000000..4b8fad9 --- /dev/null +++ b/Medevio/10ReadPozadavky/PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Download all attachments for pozadavky where attachmentsProcessed IS NULL +and (optionally) createdAt is newer than a cutoff date. +Store them in MySQL table `medevio_downloads`, and update pozadavky.attachmentsProcessed. +""" + +import zlib +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time +import sys + +# Force UTF-8 output even under Windows Task Scheduler +import sys +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + # Python < 3.7 fallback (not needed for you, but safe) + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +# ============================== +# 🛡 SAFE PRINT FOR CP1250 / EMOJI +# ============================== +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc or not enc.lower().startswith("utf"): + # strip emoji + characters outside BMP + text = ''.join(ch for ch in text if ord(ch) < 65536) + + try: + print(text) + except UnicodeEncodeError: + # ASCII fallback + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ============================== +# 🔧 CONFIGURATION +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +CREATED_AFTER = "2024-12-01" # optional filter + +GRAPHQL_QUERY = r""" +query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) { + patientRequestMedicalRecords: listMedicalRecordsForPatientRequest( + attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT] + patientRequestId: $requestId + pageInfo: {first: 100, offset: 0} + ) { + attachmentType + id + medicalRecord { + contentType + description + downloadUrl + id + url + visibleToPatient + } + } +} +""" + +# ============================== +# 🧮 HELPERS +# ============================== +def extract_filename_from_url(url: str) -> str: + try: + return url.split("/")[-1].split("?")[0] + except: + return "unknown_filename" + +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok + + +# ============================== +# 📡 FETCH ATTACHMENTS +# ============================== +def fetch_attachments(headers, request_id): + payload = { + "operationName": "ClinicRequestDetail_GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": {"requestId": request_id}, + } + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + if r.status_code != 200: + safe_print(f"❌ HTTP {r.status_code} for request {request_id}") + return [] + return r.json().get("data", {}).get("patientRequestMedicalRecords", []) + + +# ============================== +# 💾 SAVE TO MYSQL +# ============================== +def insert_download(cur, req_id, a, m, created_date, existing_ids): + + attachment_id = a.get("id") + if attachment_id in existing_ids: + safe_print(f" ⏭️ Already downloaded {attachment_id}") + return False + + url = m.get("downloadUrl") + if not url: + safe_print(" ⚠️ Missing download URL") + return False + + filename = extract_filename_from_url(url) + + try: + r = requests.get(url, timeout=30) + r.raise_for_status() + content = r.content + except Exception as e: + safe_print(f" ⚠️ Download failed {url}: {e}") + return False + + file_size = len(content) + attachment_type = a.get("attachmentType") + content_type = m.get("contentType") + + cur.execute(""" + INSERT INTO medevio_downloads ( + request_id, attachment_id, attachment_type, + filename, content_type, file_size, + created_at, file_content + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + file_content = VALUES(file_content), + file_size = VALUES(file_size), + downloaded_at = NOW() + """, ( + req_id, + attachment_id, + attachment_type, + filename, + content_type, + file_size, + created_date, + content, + )) + + existing_ids.add(attachment_id) + safe_print(f" 💾 Saved {filename} ({file_size/1024:.1f} kB)") + return True + + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + # Load existing attachments + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {row["attachment_id"] for row in cur.fetchall()} + + safe_print(f"✅ {len(existing_ids)} attachments already saved.") + + # Build query for pozadavky + sql = """ + SELECT id, pacient_prijmeni, pacient_jmeno, createdAt, updatedAt, attachmentsProcessed + FROM pozadavky + WHERE attachmentsProcessed IS NULL + OR updatedAt > attachmentsProcessed + """ + params = [] + if CREATED_AFTER: + sql += " AND createdAt >= %s" + params.append(CREATED_AFTER) + + with conn.cursor() as cur: + cur.execute(sql, params) + req_rows = cur.fetchall() + + safe_print(f"📋 Found {len(req_rows)} pozadavky to process.") + + # Process each pozadavek + for i, row in enumerate(req_rows, 1): + req_id = row["id"] + prijmeni = row.get("pacient_prijmeni") or "Neznamy" + jmeno = row.get("pacient_jmeno") or "" + created_date = row.get("createdAt") or datetime.now() + + safe_print(f"\n[{i}/{len(req_rows)}] 🧾 {prijmeni}, {jmeno} ({req_id})") + + attachments = fetch_attachments(headers, req_id) + + if not attachments: + safe_print(" ⚠️ No attachments found") + with conn.cursor() as cur: + cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + continue + + with conn.cursor() as cur: + for a in attachments: + m = a.get("medicalRecord") or {} + insert_download(cur, req_id, a, m, created_date, existing_ids) + conn.commit() + + with conn.cursor() as cur: + cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + + safe_print(f" ✅ Done ({len(attachments)} attachments)") + time.sleep(0.3) + + conn.close() + safe_print("\n🎯 All attachments processed.") + + +# ============================== +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/PRAVIDELNE_5_SaveToFileSystem incremental.py b/Medevio/10ReadPozadavky/PRAVIDELNE_5_SaveToFileSystem incremental.py new file mode 100644 index 0000000..f4225cb --- /dev/null +++ b/Medevio/10ReadPozadavky/PRAVIDELNE_5_SaveToFileSystem incremental.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import shutil +import pymysql +import re +from pathlib import Path +from datetime import datetime +import time +import sys + +# Force UTF-8 output even under Windows Task Scheduler +import sys +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + # Python < 3.7 fallback (not needed for you, but safe) + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +# ============================== +# 🛡 SAFE PRINT FOR CP1250 / EMOJI +# ============================== +def safe_print(text: str = ""): + enc = sys.stdout.encoding or "" + if not enc.lower().startswith("utf"): + # Strip emoji and characters outside BMP for Task Scheduler + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + # ASCII fallback + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ============================== +# ⚙️ CONFIGURATION +# ============================== +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", +} + +BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP") +BASE_DIR.mkdir(parents=True, exist_ok=True) + + +def sanitize_name(name: str) -> str: + """Replace invalid filename characters with underscore.""" + return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip() + + +def make_abbrev(title: str) -> str: + if not title: + return "" + words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title) + abbr = "" + for w in words: + if w.isdigit(): + abbr += w + else: + abbr += w[0] + return abbr.upper() + + +# ============================== +# 🧹 DELETE UNEXPECTED FILES +# ============================== +def clean_folder(folder: Path, valid_files: set): + if not folder.exists(): + return + + for f in folder.iterdir(): + if f.is_file(): + if f.name.startswith("▲"): + continue + sanitized = sanitize_name(f.name) + if sanitized not in valid_files: + safe_print(f"🗑️ Removing unexpected file: {f.name}") + try: + f.unlink() + except Exception as e: + safe_print(f"⚠️ Could not delete {f}: {e}") + + +# ============================== +# 📦 DB CONNECTION +# ============================== +conn = pymysql.connect(**DB_CONFIG) + +cur_meta = conn.cursor(pymysql.cursors.DictCursor) +cur_blob = conn.cursor() + +safe_print("🔍 Loading metadata from DB (FAST)…") + +cur_meta.execute(""" + SELECT d.id AS download_id, + d.request_id, + d.filename, + d.created_at, + p.updatedAt AS req_updated_at, + p.pacient_jmeno AS jmeno, + p.pacient_prijmeni AS prijmeni, + p.displayTitle + FROM medevio_downloads d + JOIN pozadavky p ON d.request_id = p.id + ORDER BY p.updatedAt DESC +""") + +rows = cur_meta.fetchall() +safe_print(f"📋 Found {len(rows)} attachment records.\n") + +# ============================== +# 🧠 MAIN LOOP WITH PROGRESS +# ============================== + +unique_request_ids = [] +seen = set() +for r in rows: + req_id = r["request_id"] + if req_id not in seen: + unique_request_ids.append(req_id) + seen.add(req_id) + +total_requests = len(unique_request_ids) +safe_print(f"🔄 Processing {total_requests} unique requests...\n") + +processed_requests = set() +current_index = 0 + +for r in rows: + req_id = r["request_id"] + + if req_id in processed_requests: + continue + processed_requests.add(req_id) + + current_index += 1 + percent = (current_index / total_requests) * 100 + + safe_print(f"\n[ {percent:5.1f}% ] Processing request {current_index} / {total_requests} → {req_id}") + + # ========== FETCH VALID FILENAMES ========== + cur_meta.execute( + "SELECT filename FROM medevio_downloads WHERE request_id=%s", + (req_id,) + ) + valid_files = {sanitize_name(row["filename"]) for row in cur_meta.fetchall()} + + # ========== BUILD FOLDER NAME ========== + updated_at = r["req_updated_at"] or datetime.now() + date_str = updated_at.strftime("%Y-%m-%d") + + prijmeni = sanitize_name(r["prijmeni"] or "Unknown") + jmeno = sanitize_name(r["jmeno"] or "") + title = r.get("displayTitle") or "" + abbr = make_abbrev(title) + + clean_folder_name = sanitize_name( + f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}" + ) + + # ========== DETECT EXISTING FOLDER ========== + existing_folder = None + + for f in BASE_DIR.iterdir(): + if f.is_dir() and req_id in f.name: + existing_folder = f + break + + main_folder = existing_folder if existing_folder else BASE_DIR / clean_folder_name + + # ========== MERGE DUPLICATES ========== + possible_dups = [ + f for f in BASE_DIR.iterdir() + if f.is_dir() and req_id in f.name and f != main_folder + ] + + for dup in possible_dups: + safe_print(f"♻️ Merging duplicate folder: {dup.name}") + + clean_folder(dup, valid_files) + main_folder.mkdir(parents=True, exist_ok=True) + + for f in dup.iterdir(): + if f.is_file(): + target = main_folder / f.name + if not target.exists(): + f.rename(target) + + shutil.rmtree(dup, ignore_errors=True) + + # ========== CLEAN MAIN FOLDER ========== + clean_folder(main_folder, valid_files) + + # ========== DOWNLOAD MISSING FILES ========== + added_new_file = False + main_folder.mkdir(parents=True, exist_ok=True) + + for filename in valid_files: + dest_plain = main_folder / filename + dest_marked = main_folder / ("▲" + filename) + + if dest_plain.exists() or dest_marked.exists(): + continue + + added_new_file = True + + cur_blob.execute( + "SELECT file_content FROM medevio_downloads " + "WHERE request_id=%s AND filename=%s", + (req_id, filename) + ) + row = cur_blob.fetchone() + if not row: + continue + + content = row[0] + if not content: + continue + + with open(dest_plain, "wb") as f: + f.write(content) + + safe_print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}") + + # ========== REMOVE ▲ FLAG IF NEW FILES ADDED ========== + if added_new_file and "▲" in main_folder.name: + new_name = main_folder.name.replace("▲", "").strip() + new_path = main_folder.parent / new_name + + if new_path != main_folder: + try: + main_folder.rename(new_path) + safe_print(f"🔄 Folder flag ▲ removed → {new_name}") + main_folder = new_path + except Exception as e: + safe_print(f"⚠️ Could not rename folder: {e}") + +safe_print("\n🎯 Export complete.\n") + +cur_blob.close() +cur_meta.close() +conn.close() diff --git a/Medevio/10ReadPozadavky/PRAVIDELNE_5_SaveToFileSystem incremental1.py b/Medevio/10ReadPozadavky/PRAVIDELNE_5_SaveToFileSystem incremental1.py new file mode 100644 index 0000000..ca07716 --- /dev/null +++ b/Medevio/10ReadPozadavky/PRAVIDELNE_5_SaveToFileSystem incremental1.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import shutil +import pymysql +import re +from pathlib import Path +from datetime import datetime +from collections import defaultdict + +# ============================== +# ⚙️ CONFIGURATION +# ============================== + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", +} + +BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP") +BASE_DIR.mkdir(parents=True, exist_ok=True) + + +# ============================== +# 🔧 HELPERS +# ============================== + +def sanitize_name(name: str) -> str: + """Replace invalid Windows filename characters.""" + return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip() + + +def make_abbrev(title: str) -> str: + """Create abbreviation from title.""" + if not title: + return "" + words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title) + abbr = "" + for w in words: + if w.isdigit(): + abbr += w + else: + abbr += w[0] + return abbr.upper() + + +def clean_folder(folder: Path, valid_files: set): + """Remove unexpected files except ▲ files.""" + if not folder.exists(): + return + + for f in folder.iterdir(): + if f.is_file(): + if f.name.startswith("▲"): + continue + sanitized = sanitize_name(f.name) + if sanitized not in valid_files: + print(f"🗑️ Removing unexpected file: {f.name}") + try: + f.unlink() + except Exception as e: + print(f"⚠️ Could not delete {f}: {e}") + + +# ============================== +# 📦 DB CONNECTION +# ============================== + +conn = pymysql.connect(**DB_CONFIG) +cur_meta = conn.cursor(pymysql.cursors.DictCursor) +cur_blob = conn.cursor() + +print("🔍 Loading only requests with NEW attachments…") + +cur_meta.execute(""" + SELECT + p.id AS request_id, + p.displayTitle, + p.pacient_jmeno, + p.pacient_prijmeni, + p.updatedAt, + p.attachmentsProcessed, + d.filename, + d.created_at + FROM pozadavky p + JOIN medevio_downloads d ON d.request_id = p.id + LEFT JOIN ( + SELECT request_id, MAX(created_at) AS last_attachment_ts + FROM medevio_downloads + GROUP BY request_id + ) x ON x.request_id = p.id + WHERE p.attachmentsProcessed IS NULL + OR p.attachmentsProcessed < x.last_attachment_ts + ORDER BY p.updatedAt DESC; +""") + +rows = cur_meta.fetchall() +print(f"📋 Found {len(rows)} attachment rows belonging to requests needing processing.\n") + +# ============================== +# 🧠 PREPARE REQUEST GROUPING +# ============================== + +grouped = defaultdict(list) +for r in rows: + grouped[r["request_id"]].append(r) + +unique_request_ids = list(grouped.keys()) +total_requests = len(unique_request_ids) + +print(f"🔄 Processing {total_requests} requests needing updates…\n") + +# ============================== +# 🧠 MAIN LOOP +# ============================== + +index = 0 + +for req_id in unique_request_ids: + index += 1 + pct = (index / total_requests) * 100 + + print(f"\n[ {pct:5.1f}% ] Processing request {index}/{total_requests} → {req_id}") + + req_rows = grouped[req_id] + first = req_rows[0] + + # Build folder name + updated_at = first["updatedAt"] or datetime.now() + date_str = updated_at.strftime("%Y-%m-%d") + + prijmeni = sanitize_name(first["pacient_prijmeni"] or "Unknown") + jmeno = sanitize_name(first["pacient_jmeno"] or "") + abbr = make_abbrev(first["displayTitle"]) + + desired_folder_name = sanitize_name(f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}") + + # Detect existing folder for request + main_folder = None + for f in BASE_DIR.iterdir(): + if f.is_dir() and req_id in f.name: + main_folder = f + break + + if not main_folder: + main_folder = BASE_DIR / desired_folder_name + + main_folder.mkdir(parents=True, exist_ok=True) + + # Build valid filename set + valid_files = {sanitize_name(r["filename"]) for r in req_rows} + + # Clean unexpected non-▲ files + clean_folder(main_folder, valid_files) + + # Track if ANY new files were downloaded + added_new_file = False + + # DOWNLOAD MISSING FILES + for r in req_rows: + filename = sanitize_name(r["filename"]) + dest_plain = main_folder / filename + dest_flag = main_folder / ("▲" + filename) + + # Skip if file already exists (plain or ▲) + if dest_plain.exists() or dest_flag.exists(): + continue + + # Fetch content + cur_blob.execute(""" + SELECT file_content + FROM medevio_downloads + WHERE request_id=%s AND filename=%s + """, (req_id, r["filename"])) + + row = cur_blob.fetchone() + if not row or not row[0]: + continue + + with open(dest_plain, "wb") as f: + f.write(row[0]) + + print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}") + added_new_file = True + + # ------------------------------------ + # 🟦 FOLDER ▲ LOGIC (IMPORTANT) + # ------------------------------------ + if added_new_file: + # If folder contains ▲ in its name → remove it + if "▲" in main_folder.name: + new_name = main_folder.name.replace("▲", "").strip() + new_path = main_folder.parent / new_name + + try: + main_folder.rename(new_path) + print(f"🔄 Folder flag ▲ removed → {new_name}") + main_folder = new_path + except Exception as e: + print(f"⚠️ Could not rename folder: {e}") + else: + # NO new files → NEVER rename folder + pass + + # Mark request as processed + cur_meta.execute( + "UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id=%s", + (req_id,) + ) + conn.commit() + +# ============================== +# 🏁 DONE +# ============================== + +print("\n🎯 Export complete.\n") +cur_blob.close() +cur_meta.close() +conn.close() diff --git a/Medevio/10ReadPozadavky/PRAVIDELNE_5_SaveToFileSystem incremental2.py b/Medevio/10ReadPozadavky/PRAVIDELNE_5_SaveToFileSystem incremental2.py new file mode 100644 index 0000000..5d2b313 --- /dev/null +++ b/Medevio/10ReadPozadavky/PRAVIDELNE_5_SaveToFileSystem incremental2.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import shutil +import pymysql +import re +from pathlib import Path +from datetime import datetime + +# ============================== +# ⚙️ CONFIGURATION +# ============================== +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", +} + +BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP") +BASE_DIR.mkdir(parents=True, exist_ok=True) + + +def sanitize_name(name: str) -> str: + """Replace invalid filename characters with underscore.""" + return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip() + + +def make_abbrev(title: str) -> str: + """Create abbreviation from displayTitle.""" + if not title: + return "" + words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title) + abbr = "" + for w in words: + abbr += w if w.isdigit() else w[0] + return abbr.upper() + + +# ============================== +# 🧹 DELETE UNEXPECTED FILES +# ============================== +def clean_folder(folder: Path, valid_files: set): + if not folder.exists(): + return + + for f in folder.iterdir(): + if f.is_file(): + if f.name.startswith("▲"): + continue + sanitized = sanitize_name(f.name) + if sanitized not in valid_files: + print(f"🗑️ Removing unexpected file: {f.name}") + try: + f.unlink() + except Exception as e: + print(f"⚠️ Could not delete {f}: {e}") + + +# ============================== +# 📦 DB CONNECTION +# ============================== +conn = pymysql.connect(**DB_CONFIG) +cur_meta = conn.cursor(pymysql.cursors.DictCursor) +cur_blob = conn.cursor() + +print("🔍 Loading ALL metadata without file_content…") + +# ⭐ Load ALL metadata once (NO BLOBs) +cur_meta.execute(""" + SELECT + d.request_id, + d.filename, + d.created_at, + p.updatedAt AS req_updated_at, + p.pacient_jmeno AS jmeno, + p.pacient_prijmeni AS prijmeni, + p.displayTitle + FROM medevio_downloads d + JOIN pozadavky p ON d.request_id = p.id + ORDER BY p.updatedAt DESC; +""") + +rows = cur_meta.fetchall() +print(f"📋 Found {len(rows)} metadata rows.\n") + +# ============================== +# 🧠 PRE-GROUP METADATA +# ============================== + +# Build dictionary: request_id → all metadata rows for that request +grouped = {} +for row in rows: + grouped.setdefault(row["request_id"], []).append(row) + +unique_request_ids = list(grouped.keys()) +total_requests = len(unique_request_ids) + +print(f"🔄 Processing {total_requests} unique requests…\n") + +# ============================== +# 🧠 MAIN LOOP +# ============================== + +for idx, req_id in enumerate(unique_request_ids, start=1): + pct = (idx / total_requests) * 100 + req_rows = grouped[req_id] + first = req_rows[0] + + print(f"\n[ {pct:5.1f}% ] Processing request {idx}/{total_requests} → {req_id}") + + # ====================== + # Build folder name + # ====================== + updated_at = first["req_updated_at"] or datetime.now() + date_str = updated_at.strftime("%Y-%m-%d") + prijmeni = sanitize_name(first["prijmeni"] or "Unknown") + jmeno = sanitize_name(first["jmeno"] or "") + abbr = make_abbrev(first["displayTitle"] or "") + + clean_folder_name = sanitize_name(f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}") + + # Detect existing folder + existing_folder = None + for f in BASE_DIR.iterdir(): + if f.is_dir() and req_id in f.name: + existing_folder = f + break + + main_folder = existing_folder if existing_folder else BASE_DIR / clean_folder_name + main_folder.mkdir(parents=True, exist_ok=True) + + # ====================== + # Valid files for this request + # ====================== + valid_files = {sanitize_name(r["filename"]) for r in req_rows} + + # Clean unexpected files + clean_folder(main_folder, valid_files) + + # ====================== + # DOWNLOAD MISSING FILES → only now load BLOBs + # ====================== + added_new_file = False + + for r in req_rows: + filename = sanitize_name(r["filename"]) + dest_plain = main_folder / filename + dest_marked = main_folder / ("▲" + filename) + + if dest_plain.exists() or dest_marked.exists(): + continue + + added_new_file = True + + # ⭐ Load BLOB only when needed + cur_blob.execute(""" + SELECT file_content + FROM medevio_downloads + WHERE request_id=%s AND filename=%s + """, (req_id, r["filename"])) + + row = cur_blob.fetchone() + if not row or not row[0]: + continue + + with open(dest_plain, "wb") as f: + f.write(row[0]) + + print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}") + + # ====================== + # Folder-level ▲ logic + # ====================== + if added_new_file and "▲" in main_folder.name: + new_name = main_folder.name.replace("▲", "").strip() + new_path = main_folder.parent / new_name + + try: + main_folder.rename(new_path) + main_folder = new_path + print(f"🔄 Folder flag ▲ removed → {new_name}") + except Exception as e: + print(f"⚠️ Could not rename folder: {e}") + +cur_blob.close() +cur_meta.close() +conn.close() + +print("\n🎯 Export complete.\n") diff --git a/Medevio/10ReadPozadavky/PRAVIDELNE_5_SaveToFileSystem single step.py b/Medevio/10ReadPozadavky/PRAVIDELNE_5_SaveToFileSystem single step.py new file mode 100644 index 0000000..dba54c1 --- /dev/null +++ b/Medevio/10ReadPozadavky/PRAVIDELNE_5_SaveToFileSystem single step.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import shutil +import pymysql +import re +from pathlib import Path +from datetime import datetime + +# ============================== +# ⚙️ CONFIGURATION +# ============================== +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", +} + +BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP") +BASE_DIR.mkdir(parents=True, exist_ok=True) + + +def sanitize_name(name: str) -> str: + return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip() + + +def clean_folder(folder: Path, valid_files: set): + """Remove files that do NOT exist in MySQL for this request.""" + if not folder.exists(): + return + + for f in folder.iterdir(): + if f.is_file() and sanitize_name(f.name) not in valid_files: + print(f"🗑️ Removing unexpected file: {f.name}") + try: + f.unlink() + except Exception as e: + print(f"⚠️ Cannot delete {f}: {e}") + + +# ============================== +# 📥 LOAD EVERYTHING IN ONE QUERY +# ============================== +conn = pymysql.connect(**DB_CONFIG) +cur = conn.cursor(pymysql.cursors.DictCursor) + +print("📥 Loading ALL metadata + BLOBs with ONE MySQL query…") + +cur.execute(""" + SELECT + d.id AS download_id, + d.request_id, + d.filename, + d.file_content, + p.updatedAt AS req_updated_at, + p.pacient_jmeno AS jmeno, + p.pacient_prijmeni AS prijmeni + FROM medevio_downloads d + JOIN pozadavky p ON d.request_id = p.id + ORDER BY p.updatedAt DESC, d.created_at ASC +""") + +rows = cur.fetchall() +print(f"📦 Loaded {len(rows)} total file rows.\n") + +conn.close() + +# ============================== +# 🔄 ORGANIZE ROWS PER REQUEST +# ============================== +requests = {} # req_id → list of file dicts + +for r in rows: + req_id = r["request_id"] + if req_id not in requests: + requests[req_id] = [] + requests[req_id].append(r) + +print(f"📌 Unique requests: {len(requests)}\n") + +# ============================== +# 🧠 MAIN LOOP – SAME LOGIC AS BEFORE +# ============================== +for req_id, filelist in requests.items(): + + # ========== GET UPDATEDAT (same logic) ========== + any_row = filelist[0] + updated_at = any_row["req_updated_at"] or datetime.now() + date_str = updated_at.strftime("%Y-%m-%d") + + prijmeni = sanitize_name(any_row["prijmeni"] or "Unknown") + jmeno = sanitize_name(any_row["jmeno"] or "") + + folder_name = sanitize_name(f"{date_str} {prijmeni}, {jmeno} {req_id}") + main_folder = BASE_DIR / folder_name + + # ========== VALID FILES ========== + valid_files = {sanitize_name(r["filename"]) for r in filelist} + + # ========== FIND OLD FOLDERS ========== + possible_dups = [ + f for f in BASE_DIR.iterdir() + if f.is_dir() and req_id in f.name and f != main_folder + ] + + # ========== MERGE OLD FOLDERS ========== + for dup in possible_dups: + print(f"♻️ Merging folder: {dup.name}") + + clean_folder(dup, valid_files) + main_folder.mkdir(parents=True, exist_ok=True) + + for f in dup.iterdir(): + if f.is_file(): + target = main_folder / f.name + if not target.exists(): + f.rename(target) + + shutil.rmtree(dup, ignore_errors=True) + + # ========== CLEAN MAIN FOLDER ========== + main_folder.mkdir(parents=True, exist_ok=True) + clean_folder(main_folder, valid_files) + + # ========== SAVE FILES (fast now) ========== + for r in filelist: + filename = sanitize_name(r["filename"]) + dest = main_folder / filename + + if dest.exists(): + continue + + content = r["file_content"] + if not content: + continue + + with open(dest, "wb") as f: + f.write(content) + + print(f"💾 Saved: {dest.relative_to(BASE_DIR)}") + +print("\n🎯 Export complete.\n") diff --git a/Medevio/10ReadPozadavky/PRAVIDELNE_PLNYSCRIPT.py b/Medevio/10ReadPozadavky/PRAVIDELNE_PLNYSCRIPT.py new file mode 100644 index 0000000..65c1f78 --- /dev/null +++ b/Medevio/10ReadPozadavky/PRAVIDELNE_PLNYSCRIPT.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import importlib.util +import sys +from pathlib import Path + +# Load FunctionsLoader +FUNCTIONS_LOADER_PATH = Path(r"C:\Reporting\Functions\FunctionsLoader.py") +spec = importlib.util.spec_from_file_location("FunctionsLoader", FUNCTIONS_LOADER_PATH) +FunctionsLoader = importlib.util.module_from_spec(spec) +sys.modules["FunctionsLoader"] = FunctionsLoader +spec.loader.exec_module(FunctionsLoader) + +""" +Spustí všechny PRAVIDELNÉ skripty v daném pořadí: + +0) PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py +1) PRAVIDELNE_1_ReadLast300DonePozadavku.py +2) PRAVIDELNE_2_ReadPoznamky.py +3) PRAVIDELNE_3_StahniKomunikaci.py +4) PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py +5) PRAVIDELNE_5_SaveToFileSystem incremental.py +""" + +import time, socket +for _ in range(30): + try: + socket.create_connection(("192.168.1.76", 3307), timeout=3).close() + break + except OSError: + time.sleep(10) + +import sys +import subprocess +from pathlib import Path +from datetime import datetime + +# složka, kde leží tento skript i všechny PRAVIDELNE_*.py +BASE_DIR = Path(__file__).resolve().parent + +SCRIPTS_IN_ORDER = [ + "PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py", + "PRAVIDELNE_1_ReadLast300DonePozadavku.py", + "PRAVIDELNE_2_ReadPoznamky.py", + "PRAVIDELNE_3_StahniKomunikaci.py", + "PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py", + "PRAVIDELNE_5_SaveToFileSystem incremental.py", # má mezeru v názvu, ale v listu je to OK +] + +LOG_FILE = BASE_DIR / "PRAVIDELNE_log.txt" + + +def log(msg: str): + """Zapíše zprávu do log souboru i na konzoli.""" + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + line = f"[{ts}] {msg}" + print(line) + try: + with LOG_FILE.open("a", encoding="utf-8") as f: + f.write(line + "\n") + except Exception: + # log nesmí shodit běh + pass + + +def main(): + log("=== START pravidelného běhu ===") + + for script_name in SCRIPTS_IN_ORDER: + script_path = BASE_DIR / script_name + + if not script_path.exists(): + log(f"❌ Skript nenalezen: {script_path}") + continue + + log(f"▶ Spouštím: {script_path.name}") + + # spustíme stejným interpretem, kterým běží tento orchestr + try: + result = subprocess.run( + [sys.executable, str(script_path)], + cwd=str(BASE_DIR), + capture_output=True, + text=True, + encoding="utf-8", + errors="ignore", # NEZKAZÍ SE, NEZBOŘÍ SE, PROSTĚ IGNORUJE CP1252 NEZÁKONNÉ BYTES + ) + except Exception as e: + log(f" 💥 Chyba při spouštění {script_path.name}: {e}") + continue + + # vypíšeme návratový kód + log(f" ↳ return code: {result.returncode}") + + # pokud něco skript vypsal na stderr, logneme + if result.stderr: + log(f" ⚠ stderr {script_path.name}:\n{result.stderr.strip()}") + + # stdout můžeš podle chuti také logovat (někdy je toho moc): + # if result.stdout: + # log(f" ℹ stdout {script_path.name}:\n{result.stdout.strip()}") + + log("=== KONEC pravidelného běhu ===\n") + + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/PRAVIDELNE_log.txt b/Medevio/10ReadPozadavky/PRAVIDELNE_log.txt new file mode 100644 index 0000000..663cf6d --- /dev/null +++ b/Medevio/10ReadPozadavky/PRAVIDELNE_log.txt @@ -0,0 +1,29 @@ +[2025-12-01 06:37:41] === START pravidelného běhu === +[2025-12-01 06:37:42] ▶ Spouštím: PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py +[2025-12-01 06:37:44] ↳ PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py return code: 0 +[2025-12-01 06:37:44] ▶ Spouštím: PRAVIDELNE_1_ReadLast300DonePozadavku.py +[2025-12-01 06:37:48] ↳ PRAVIDELNE_1_ReadLast300DonePozadavku.py return code: 0 +[2025-12-01 06:37:48] ▶ Spouštím: PRAVIDELNE_2_ReadPoznamky.py +[2025-12-01 06:37:49] ↳ PRAVIDELNE_2_ReadPoznamky.py return code: 0 +[2025-12-01 06:37:50] ▶ Spouštím: PRAVIDELNE_3_StahniKomunikaci.py +[2025-12-01 06:37:51] ↳ PRAVIDELNE_3_StahniKomunikaci.py return code: 0 +[2025-12-01 06:37:52] ▶ Spouštím: PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py +[2025-12-01 06:37:53] ↳ PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py return code: 0 +[2025-12-01 06:37:53] ▶ Spouštím: PRAVIDELNE_5_SaveToFileSystem incremental.py +[2025-12-01 06:38:42] ↳ PRAVIDELNE_5_SaveToFileSystem incremental.py return code: 0 +[2025-12-01 06:38:43] === KONEC pravidelného běhu === +[2025-12-02 07:04:34] === START pravidelného běhu === +[2025-12-02 07:04:34] ▶ Spouštím: PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py +[2025-12-02 07:04:35] ↳ return code: 0 +[2025-12-02 07:04:35] ▶ Spouštím: PRAVIDELNE_1_ReadLast300DonePozadavku.py +[2025-12-02 07:04:39] ↳ return code: 0 +[2025-12-02 07:04:39] ▶ Spouštím: PRAVIDELNE_2_ReadPoznamky.py +[2025-12-02 07:04:40] ↳ return code: 0 +[2025-12-02 07:04:40] ▶ Spouštím: PRAVIDELNE_3_StahniKomunikaci.py +[2025-12-02 07:04:40] ↳ return code: 0 +[2025-12-02 07:04:40] ▶ Spouštím: PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py +[2025-12-02 07:04:40] ↳ return code: 0 +[2025-12-02 07:04:40] ▶ Spouštím: PRAVIDELNE_5_SaveToFileSystem incremental.py +[2025-12-02 07:05:28] ↳ return code: 0 +[2025-12-02 07:05:28] === KONEC pravidelného běhu === + diff --git a/Medevio/10ReadPozadavky/test.py b/Medevio/10ReadPozadavky/test.py new file mode 100644 index 0000000..60efc51 --- /dev/null +++ b/Medevio/10ReadPozadavky/test.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Orchestrator for all PRAVIDELNE scripts in exact order. +""" + +import time, socket +for _ in range(30): + try: + socket.create_connection(("192.168.1.76", 3307), timeout=3).close() + break + except OSError: + time.sleep(10) + +import sys +import subprocess +from pathlib import Path +from datetime import datetime + +# ===================================================================== +# Import EXACT Functions.py from: C:\Reporting\Fio\Functions.py +# This bypasses all other Functions.py files in the system. +# ===================================================================== + +import importlib.util + +FUNCTIONS_FILE = Path(r"C:\Reporting\Fio\Functions.py") + +spec = importlib.util.spec_from_file_location("Functions_FIO", FUNCTIONS_FILE) +Functions_FIO = importlib.util.module_from_spec(spec) +sys.modules["Functions_FIO"] = Functions_FIO +spec.loader.exec_module(Functions_FIO) + +# correct WhatsApp function +SendWhatsAppMessage = Functions_FIO.SendWhatsAppMessage + + +# ===================================================================== +# General Orchestrator Settings +# ===================================================================== + +# folder where orchestrator + sub-scripts live +BASE_DIR = Path(__file__).resolve().parent + +SCRIPTS_IN_ORDER = [ + "PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py", + "PRAVIDELNE_1_ReadLast300DonePozadavku.py", + "PRAVIDELNE_2_ReadPoznamky.py", + "PRAVIDELNE_3_StahniKomunikaci.py", + "PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py", + "PRAVIDELNE_5_SaveToFileSystem incremental.py", +] + +LOG_FILE = BASE_DIR / "PRAVIDELNE_log.txt" + + +# ===================================================================== +# Logging + WhatsApp wrappers +# ===================================================================== + +def log(msg: str): + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + line = f"[{ts}] {msg}" + print(line) + try: + with LOG_FILE.open("a", encoding="utf-8") as f: + f.write(line + "\n") + except: + pass + + +def whatsapp_notify(text: str): + """WhatsApp message wrapper — never allowed to crash orchestrator""" + try: + SendWhatsAppMessage(text) + except: + pass + + +# ===================================================================== +# Main orchestrator +# ===================================================================== + +def main(): + log("=== START pravidelného běhu ===") + whatsapp_notify("🏁 *PRAVIDELNÉ skripty: START*") + + for script_name in SCRIPTS_IN_ORDER: + script_path = BASE_DIR / script_name + + if not script_path.exists(): + err = f"❌ Skript nenalezen: {script_path}" + log(err) + whatsapp_notify(err) + continue + + log(f"▶ Spouštím: {script_path.name}") + whatsapp_notify(f"▶ *Spouštím:* {script_path.name}") + + try: + result = subprocess.run( + [sys.executable, str(script_path)], + cwd=str(BASE_DIR), + capture_output=True, + text=True, + encoding="utf-8", + errors="ignore", + ) + except Exception as e: + err = f"💥 Chyba při spouštění {script_path.name}: {e}" + log(err) + whatsapp_notify(err) + continue + + # return code + rc_msg = f"↳ {script_path.name} return code: {result.returncode}" + log(rc_msg) + whatsapp_notify(rc_msg) + + # stderr (warnings/errors) + if result.stderr: + err_msg = f"⚠ stderr v {script_path.name}:\n{result.stderr.strip()}" + log(err_msg) + whatsapp_notify(err_msg) + + log("=== KONEC pravidelného běhu ===") + whatsapp_notify("✅ *PRAVIDELNÉ skripty: KONEC*\n") + + +# ===================================================================== +# Entry point +# ===================================================================== + +if __name__ == "__main__": + main() diff --git a/Medevio/10ReadPozadavky/x0703 Plne funkční uložení do mysql.py b/Medevio/10ReadPozadavky/x0703 Plne funkční uložení do mysql.py new file mode 100644 index 0000000..2a22478 --- /dev/null +++ b/Medevio/10ReadPozadavky/x0703 Plne funkční uložení do mysql.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Download all 'Odeslat lékařskou zprávu' attachments from Medevio API +and store them (including binary content) directly into MySQL table `medevio_downloads`. + +Each attachment (PDF, image, etc.) is fetched once and saved as LONGBLOB. +Duplicate protection is ensured via UNIQUE KEY on `attachment_id`. +""" + +import zlib +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time + +# ============================== +# 🔧 CONFIGURATION +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY = r""" +query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) { + patientRequestMedicalRecords: listMedicalRecordsForPatientRequest( + attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT] + patientRequestId: $requestId + pageInfo: {first: 100, offset: 0} + ) { + attachmentType + id + medicalRecord { + contentType + description + downloadUrl + id + url + visibleToPatient + } + } +} +""" + +# ============================== +# 🧮 HELPERS +# ============================== +def short_crc8(uuid_str: str) -> str: + """Return deterministic 8-char hex string from any input string (CRC32).""" + return f"{zlib.crc32(uuid_str.encode('utf-8')) & 0xffffffff:08x}" + +def extract_filename_from_url(url: str) -> str: + """Extracts filename from S3-style URL (between last '/' and first '?').""" + try: + return url.split("/")[-1].split("?")[0] + except Exception: + return "unknown_filename" + +def read_token(p: Path) -> str: + """Read Bearer token from file.""" + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + +# ============================== +# 📡 FETCH ATTACHMENTS +# ============================== +def fetch_attachments(headers, request_id): + variables = {"requestId": request_id} + payload = { + "operationName": "ClinicRequestDetail_GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + if r.status_code != 200: + print(f"❌ HTTP {r.status_code} for request {request_id}") + return [] + data = r.json().get("data", {}).get("patientRequestMedicalRecords", []) + return data + +# ============================== +# 💾 SAVE TO MYSQL +# ============================== +def insert_download(cur, req_id, a, m, jmeno, prijmeni, created_date): + url = m.get("downloadUrl") + if not url: + print(" ⚠️ No download URL") + return + + try: + r = requests.get(url, timeout=45) + r.raise_for_status() + content = r.content + except Exception as e: + print(f" ⚠️ Failed to download {url}: {e}") + return + + file_size = len(content) + filename = extract_filename_from_url(url) + attachment_id = a.get("id") + attachment_type = a.get("attachmentType") + content_type = m.get("contentType") + + cur.execute(""" + INSERT INTO medevio_downloads ( + request_id, attachment_id, attachment_type, filename, + content_type, file_size, pacient_jmeno, pacient_prijmeni, + created_at, file_content + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + file_content = VALUES(file_content), + file_size = VALUES(file_size), + downloaded_at = NOW() + """, ( + req_id, + attachment_id, + attachment_type, + filename, + content_type, + file_size, + jmeno, + prijmeni, + created_date, + content + )) + print(f" 💾 Saved {filename} ({file_size/1024:.1f} kB)") + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + with conn.cursor() as cur: + cur.execute(""" + SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno, createdAt + FROM pozadavky + WHERE displayTitle = 'Odeslat lékařskou zprávu' + """) + rows = cur.fetchall() + + print(f"📋 Found {len(rows)} 'Odeslat lékařskou zprávu' requests") + + for i, row in enumerate(rows, 1): + req_id = row["id"] + prijmeni = row.get("pacient_prijmeni") or "Neznamy" + jmeno = row.get("pacient_jmeno") or "" + created = row.get("createdAt") + + try: + created_date = datetime.strptime(str(created), "%Y-%m-%d %H:%M:%S") + except Exception: + created_date = None + + print(f"\n[{i}/{len(rows)}] 🧾 {prijmeni}, {jmeno} ({req_id})") + + attachments = fetch_attachments(headers, req_id) + if not attachments: + print(" ⚠️ No attachments") + continue + + with conn.cursor() as cur: + for a in attachments: + m = a.get("medicalRecord") or {} + insert_download(cur, req_id, a, m, jmeno, prijmeni, created_date) + conn.commit() + + print(f" ✅ {len(attachments)} attachments saved for {prijmeni}, {jmeno}") + time.sleep(0.5) # be nice to the API + + conn.close() + print("\n✅ Done! All attachments stored in MySQL table `medevio_downloads`.") + +# ============================== +if __name__ == "__main__": + main() diff --git a/Medevio/12 Readallinbatches/10 Readallpozadavkyinbatches.py b/Medevio/12 Readallinbatches/10 Readallpozadavkyinbatches.py new file mode 100644 index 0000000..f767de5 --- /dev/null +++ b/Medevio/12 Readallinbatches/10 Readallpozadavkyinbatches.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pymysql +import requests +from pathlib import Path +from datetime import datetime +from dateutil import parser +import time +import sys + +# ================================ +# UTF-8 SAFE OUTPUT (Windows friendly) +# ================================ +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc.lower().startswith("utf"): + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ================================ +# 🔧 CONFIG +# ================================ +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" + +BATCH_SIZE = 500 +STATES = ["ACTIVE", "DONE"] # explicitně – jinak API vrací jen ACTIVE + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY = r""" +query ClinicRequestList2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $state: PatientRequestState, + $pageInfo: PageInfo!, + $locale: Locale! +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + state: $state, + pageInfo: $pageInfo + ) { + count + patientRequests { + id + displayTitle(locale: $locale) + createdAt + updatedAt + doneAt + removedAt + extendedPatient { + name + surname + identificationNumber + } + lastMessage { + createdAt + } + } + } +} +""" + + +# ================================ +# TOKEN +# ================================ +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + return tok.split(" ", 1)[1] + return tok + + +# ================================ +# DATETIME PARSER +# ================================ +def to_mysql_dt(iso_str): + if not iso_str: + return None + try: + dt = parser.isoparse(iso_str) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=datetime.now().astimezone().tzinfo) + return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S") + except Exception: + return None + + +# ================================ +# UPSERT +# ================================ +def upsert(conn, r): + p = r.get("extendedPatient") or {} + + api_updated = to_mysql_dt(r.get("updatedAt")) + msg_updated = to_mysql_dt((r.get("lastMessage") or {}).get("createdAt")) + + final_updated = max(filter(None, [api_updated, msg_updated]), default=None) + + sql = """ + INSERT INTO pozadavky ( + id, displayTitle, createdAt, updatedAt, doneAt, removedAt, + pacient_jmeno, pacient_prijmeni, pacient_rodnecislo + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + displayTitle=VALUES(displayTitle), + updatedAt=VALUES(updatedAt), + doneAt=VALUES(doneAt), + removedAt=VALUES(removedAt), + pacient_jmeno=VALUES(pacient_jmeno), + pacient_prijmeni=VALUES(pacient_prijmeni), + pacient_rodnecislo=VALUES(pacient_rodnecislo) + """ + + vals = ( + r.get("id"), + r.get("displayTitle"), + to_mysql_dt(r.get("createdAt")), + final_updated, + to_mysql_dt(r.get("doneAt")), + to_mysql_dt(r.get("removedAt")), + p.get("name"), + p.get("surname"), + p.get("identificationNumber"), + ) + + with conn.cursor() as cur: + cur.execute(sql, vals) + conn.commit() + + +# ================================ +# FETCH PAGE (per state) +# ================================ +def fetch_state(headers, state, offset): + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "state": state, + "pageInfo": {"first": BATCH_SIZE, "offset": offset}, + "locale": "cs", + } + + payload = { + "operationName": "ClinicRequestList2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) + r.raise_for_status() + + data = r.json()["data"]["requestsResponse"] + return data.get("patientRequests", []), data.get("count", 0) + + +# ================================ +# MAIN +# ================================ +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + safe_print(f"\n=== FULL Medevio READ-ALL sync @ {datetime.now():%Y-%m-%d %H:%M:%S} ===") + + grand_total = 0 + + for state in STATES: + safe_print(f"\n🔁 STATE = {state}") + offset = 0 + total = None + processed = 0 + + while True: + batch, count = fetch_state(headers, state, offset) + + if total is None: + total = count + safe_print(f"📡 {state}: celkem {total}") + + if not batch: + break + + for r in batch: + upsert(conn, r) + + processed += len(batch) + safe_print(f" • {processed}/{total}") + + offset += BATCH_SIZE + if offset >= count: + break + + time.sleep(0.4) + + grand_total += processed + + conn.close() + safe_print(f"\n✅ HOTOVO – celkem zpracováno {grand_total} požadavků\n") + + +# ================================ +if __name__ == "__main__": + main() diff --git a/Medevio/12 Readallinbatches/PRAVIDELNE_3_StahniKomunikacifull.py b/Medevio/12 Readallinbatches/PRAVIDELNE_3_StahniKomunikacifull.py new file mode 100644 index 0000000..bcf4102 --- /dev/null +++ b/Medevio/12 Readallinbatches/PRAVIDELNE_3_StahniKomunikacifull.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Fetches messages from Medevio API. + +Modes: +- Incremental (default): Only requests where messagesProcessed IS NULL or < updatedAt +- Full resync (--full): Fetches ALL messages for ALL pozadavky +""" + +import zlib +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time +import argparse + +# ============================== +# 🔧 CONFIGURATION +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY_MESSAGES = r""" +query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) { + messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) { + id + createdAt + updatedAt + readAt + text + type + sender { + id + name + surname + clinicId + } + medicalRecord { + id + description + contentType + url + downloadUrl + token + createdAt + updatedAt + } + } +} +""" + +# ============================== +# ⏱ DATETIME PARSER +# ============================== +def parse_dt(s): + if not s: + return None + try: + return datetime.fromisoformat(s.replace("Z", "+00:00")) + except: + pass + try: + return datetime.strptime(s[:19], "%Y-%m-%dT%H:%M:%S") + except: + return None + +# ============================== +# 🔐 TOKEN +# ============================== +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + return tok.replace("Bearer ", "") + +# ============================== +# 📡 FETCH MESSAGES +# ============================== +def fetch_messages(headers, request_id): + payload = { + "operationName": "UseMessages_ListMessages", + "query": GRAPHQL_QUERY_MESSAGES, + "variables": {"requestId": request_id, "updatedSince": None}, + } + + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + if r.status_code != 200: + print("❌ HTTP", r.status_code, "for request", request_id) + return [] + return r.json().get("data", {}).get("messages", []) or [] + +# ============================== +# 💾 SAVE MESSAGE +# ============================== +def insert_message(cur, req_id, msg): + + sender = msg.get("sender") or {} + sender_name = " ".join( + x for x in [sender.get("name"), sender.get("surname")] if x + ) or None + + sql = """ + INSERT INTO medevio_conversation ( + id, request_id, + sender_name, sender_id, sender_clinic_id, + text, created_at, read_at, updated_at, + attachment_url, attachment_description, attachment_content_type + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + sender_name = VALUES(sender_name), + sender_id = VALUES(sender_id), + sender_clinic_id = VALUES(sender_clinic_id), + text = VALUES(text), + created_at = VALUES(created_at), + read_at = VALUES(read_at), + updated_at = VALUES(updated_at), + attachment_url = VALUES(attachment_url), + attachment_description = VALUES(attachment_description), + attachment_content_type = VALUES(attachment_content_type) + """ + + mr = msg.get("medicalRecord") or {} + + cur.execute(sql, ( + msg.get("id"), + req_id, + sender_name, + sender.get("id"), + sender.get("clinicId"), + msg.get("text"), + parse_dt(msg.get("createdAt")), + parse_dt(msg.get("readAt")), + parse_dt(msg.get("updatedAt")), + mr.get("downloadUrl") or mr.get("url"), + mr.get("description"), + mr.get("contentType") + )) + +# ============================== +# 💾 DOWNLOAD MESSAGE ATTACHMENT +# ============================== +def insert_download(cur, req_id, msg, existing_ids): + + mr = msg.get("medicalRecord") or {} + attachment_id = mr.get("id") + if not attachment_id: + return + + if attachment_id in existing_ids: + return # skip duplicates + + url = mr.get("downloadUrl") or mr.get("url") + if not url: + return + + try: + r = requests.get(url, timeout=30) + r.raise_for_status() + data = r.content + except Exception as e: + print("⚠️ Failed to download:", e) + return + + filename = url.split("/")[-1].split("?")[0] + + cur.execute(""" + INSERT INTO medevio_downloads ( + request_id, attachment_id, attachment_type, + filename, content_type, file_size, created_at, file_content + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + file_content = VALUES(file_content), + file_size = VALUES(file_size), + downloaded_at = NOW() + """, ( + req_id, + attachment_id, + "MESSAGE_ATTACHMENT", + filename, + mr.get("contentType"), + len(data), + parse_dt(msg.get("createdAt")), + data + )) + + existing_ids.add(attachment_id) + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + + parser = argparse.ArgumentParser() + parser.add_argument("--full", action="store_true", help="Load messages for ALL pozadavky") + # Force full mode ON + args = parser.parse_args(args=["--full"]) + # args = parser.parse_args() + + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + # ---- Load existing attachments + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {row["attachment_id"] for row in cur.fetchall()} + + print(f"📦 Already downloaded attachments: {len(existing_ids)}\n") + + # ---- Select pozadavky to process + with conn.cursor() as cur: + if args.full: + print("🔁 FULL REFRESH MODE: Fetching messages for ALL pozadavky!\n") + cur.execute("SELECT id FROM pozadavky") + else: + print("📥 Incremental mode: Only syncing updated pozadavky.\n") + cur.execute(""" + SELECT id FROM pozadavky + WHERE messagesProcessed IS NULL + OR messagesProcessed < updatedAt + """) + requests_to_process = cur.fetchall() + + # ================================= + # ⏩ SKIP FIRST 3100 AS YESTERDAY + # ================================= + + SKIP = 3100 + if len(requests_to_process) > SKIP: + print(f"⏩ Skipping first {SKIP} pozadavky (already processed yesterday).") + requests_to_process = requests_to_process[SKIP:] + else: + print("⚠️ Not enough pozadavky to skip!") + + + print(f"📋 Requests to process: {len(requests_to_process)}\n") + + # ---- Process each request + for idx, row in enumerate(requests_to_process, 1): + req_id = row["id"] + print(f"[{idx}/{len(requests_to_process)}] Processing {req_id} …") + + messages = fetch_messages(headers, req_id) + + with conn.cursor() as cur: + for msg in messages: + insert_message(cur, req_id, msg) + insert_download(cur, req_id, msg, existing_ids) + conn.commit() + + with conn.cursor() as cur: + cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + + print(f" ✅ {len(messages)} messages saved\n") + time.sleep(0.25) + + conn.close() + print("🎉 Done!") + + +if __name__ == "__main__": + main() diff --git a/Medevio/12 Readallinbatches/medevio_storage.json b/Medevio/12 Readallinbatches/medevio_storage.json new file mode 100644 index 0000000..a0dc794 --- /dev/null +++ b/Medevio/12 Readallinbatches/medevio_storage.json @@ -0,0 +1 @@ +{"cookies": [{"name": "gateway-access-token", "value": "YwBgkf8McREDKs7vCZj0EZD2fJsuV8RyDPtYx7WiDoz0nFJ9kxId8kcNEPBLFSwM+Tiz80+SOdFwo+oj", "domain": "my.medevio.cz", "path": "/", "expires": 1763372319, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "aws-waf-token", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c6zAAAA:OYwXLY5OyitSQPl5v2oIlS+hIxsrb5LxV4VjCyE2gJCFFE5PQu+0Zbxse2ZIofrNv5QKs0TYUDTmxPhZyTr9Qtjnq2gsVQxWHXzrbebv3Z7RbzB63u6Ymn3Fo8IbDev3CfCNcNuxCKltFEXLqSCjI2vqNY+7HZkgQBIqy2wMgzli3aSLq0w8lWYtZzyyot7q8RPXWMGTfaBUo2reY0SOSffm9rAivE9PszNfPid71CvNrGAAoxRbwb25eVujlyIcDVWe5vZ9Iw==", "domain": ".my.medevio.cz", "path": "/", "expires": 1761125920, "httpOnly": false, "secure": true, "sameSite": "Lax"}], "origins": [{"origin": "https://my.medevio.cz", "localStorage": [{"name": "awswaf_token_refresh_timestamp", "value": "1760780309860"}, {"name": "awswaf_session_storage", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c+zAAAA:+vw//1NzmePjPpbGCJzUB+orCRivtJd098DbDX4AnABiGRw/+ql6ShqvFY4YdCY7w2tegb5mEPBdAmc4sNi22kNR9BuEoAgCUiMhkU1AZWfzM51zPfTh7SveCrREZ7xdvxcqKPMmfVLRYX5E4+UWh22z/LKQ7+d9VERp3J+wWCUW3dFFirkezy3N7b2FVjTlY/RxsZwhejQziTG/L3CkIFFP3mOReNgBvDpj7aKoM1knY4IL4TZ8E7zNv3nTsvzACLYvnUutVOUcofN1TfOzwZshSKsEXsMzrQn8PzLccX1jM5VSzce7gfEzl0zSPsT8NB3Sna+rhMIttDNYgvbW1HsfG2LIeKMR27Zf8hkslDRVVkcU/Kp2jLOEdhhrBKGjKY2o9/uX3NExdzh5MEKQSSRtmue01BpWYILPH23rMsz4YSmF+Ough5OeQoC95rkcYwVXMhwvUN9Zfp9UZ4xCNfFUex5dOrg9aJntYRnaceeocGUttNI5AdT0i3+osV6XHXzKxeqO8zLCS9BIsCzxaHfdqqem5DorMceuGKz+QqksatIQAA=="}, {"name": "Application.Intl.locale", "value": "cs"}, {"name": "Password.prefill", "value": "{\"username\":\"vladimir.buzalka@buzalka.cz\",\"type\":\"email\"}"}]}]} \ No newline at end of file diff --git a/Medevio/12 Tower1/10 ReadAllActive.py b/Medevio/12 Tower1/10 ReadAllActive.py new file mode 100644 index 0000000..5b4b161 --- /dev/null +++ b/Medevio/12 Tower1/10 ReadAllActive.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pymysql +import requests +from pathlib import Path +from datetime import datetime, timezone +import time +from dateutil import parser +import sys + +# Force UTF-8 output +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +# ================================ +# 🔧 CONFIGURATION +# ================================ +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" +BATCH_SIZE = 100 + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +# ⭐ NOVÝ TESTOVANÝ DOTAZ – obsahuje lastMessage.createdAt +GRAPHQL_QUERY = r""" +query ClinicRequestList2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $state: PatientRequestState, + $pageInfo: PageInfo!, + $locale: Locale! +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + state: $state, + pageInfo: $pageInfo + ) { + count + patientRequests { + id + displayTitle(locale: $locale) + createdAt + updatedAt + doneAt + removedAt + extendedPatient { + name + surname + identificationNumber + } + lastMessage { + createdAt + } + } + } +} +""" + +# ================================ +# 🧿 SAFE DATETIME PARSER (ALWAYS UTC → LOCAL) +# ================================ +def to_mysql_dt_utc(iso_str): + if not iso_str: + return None + try: + dt = parser.isoparse(iso_str) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + dt_local = dt.astimezone() + return dt_local.strftime("%Y-%m-%d %H:%M:%S") + except: + return None + +# ================================ +# 🔑 TOKEN +# ================================ +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + return tok.split(" ", 1)[1] + return tok + +# ================================ +# 💾 UPSERT +# ================================ +def upsert(conn, r): + p = r.get("extendedPatient") or {} + api_updated = to_mysql_dt_utc(r.get("updatedAt")) + last_msg = r.get("lastMessage") or {} + msg_updated = to_mysql_dt_utc(last_msg.get("createdAt")) + + def max_dt(a, b): + if a and b: + return max(a, b) + return a or b + + final_updated = max_dt(api_updated, msg_updated) + + sql = """ + INSERT INTO pozadavky ( + id, displayTitle, createdAt, updatedAt, doneAt, removedAt, + pacient_jmeno, pacient_prijmeni, pacient_rodnecislo + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + displayTitle=VALUES(displayTitle), + updatedAt=VALUES(updatedAt), + doneAt=VALUES(doneAt), + removedAt=VALUES(removedAt), + pacient_jmeno=VALUES(pacient_jmeno), + pacient_prijmeni=VALUES(pacient_prijmeni), + pacient_rodnecislo=VALUES(pacient_rodnecislo) + """ + + vals = ( + r.get("id"), + r.get("displayTitle"), + to_mysql_dt_utc(r.get("createdAt")), + final_updated, + to_mysql_dt_utc(r.get("doneAt")), + to_mysql_dt_utc(r.get("removedAt")), + p.get("name"), + p.get("surname"), + p.get("identificationNumber"), + ) + + with conn.cursor() as cur: + cur.execute(sql, vals) + conn.commit() + +# ================================ +# 📡 FETCH ACTIVE PAGE +# ================================ +def fetch_active(headers, offset): + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "pageInfo": {"first": BATCH_SIZE, "offset": offset}, + "locale": "cs", + "state": "ACTIVE", + } + + payload = { + "operationName": "ClinicRequestList2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) + r.raise_for_status() + data = r.json().get("data", {}).get("requestsResponse", {}) + return data.get("patientRequests", []), data.get("count", 0) + +# ================================ +# 🧠 MAIN +# ================================ +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + print(f"\n=== Sync ACTIVE požadavků @ {datetime.now():%Y-%m-%d %H:%M:%S} ===") + + offset = 0 + total_processed = 0 + total_count = None + + while True: + batch, count = fetch_active(headers, offset) + if total_count is None: + total_count = count + print(f"📡 Celkem ACTIVE v Medevio: {count}") + + if not batch: + break + + for r in batch: + upsert(conn, r) + + total_processed += len(batch) + print(f" • {total_processed}/{total_count} ACTIVE processed") + + if offset + BATCH_SIZE >= count: + break + + offset += BATCH_SIZE + time.sleep(0.4) + + conn.close() + print("\n✅ ACTIVE sync hotovo!\n") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Medevio/12 Tower1/12 ReadAllinBatches.py b/Medevio/12 Tower1/12 ReadAllinBatches.py new file mode 100644 index 0000000..fc7fee7 --- /dev/null +++ b/Medevio/12 Tower1/12 ReadAllinBatches.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pymysql +import requests +from pathlib import Path +from datetime import datetime +from dateutil import parser +import time +import sys + +# ================================ +# UTF-8 SAFE OUTPUT (Windows friendly) +# ================================ +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc.lower().startswith("utf"): + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ================================ +# 🔧 CONFIG +# ================================ +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" + +BATCH_SIZE = 500 +STATES = ["ACTIVE", "DONE"] # explicitně – jinak API vrací jen ACTIVE + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY = r""" +query ClinicRequestList2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $state: PatientRequestState, + $pageInfo: PageInfo!, + $locale: Locale! +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + state: $state, + pageInfo: $pageInfo + ) { + count + patientRequests { + id + displayTitle(locale: $locale) + createdAt + updatedAt + doneAt + removedAt + extendedPatient { + name + surname + identificationNumber + } + lastMessage { + createdAt + } + } + } +} +""" + + +# ================================ +# TOKEN +# ================================ +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + return tok.split(" ", 1)[1] + return tok + + +# ================================ +# DATETIME PARSER +# ================================ +def to_mysql_dt(iso_str): + if not iso_str: + return None + try: + dt = parser.isoparse(iso_str) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=datetime.now().astimezone().tzinfo) + return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S") + except Exception: + return None + + +# ================================ +# UPSERT +# ================================ +def upsert(conn, r): + p = r.get("extendedPatient") or {} + + api_updated = to_mysql_dt(r.get("updatedAt")) + msg_updated = to_mysql_dt((r.get("lastMessage") or {}).get("createdAt")) + + final_updated = max(filter(None, [api_updated, msg_updated]), default=None) + + sql = """ + INSERT INTO pozadavky ( + id, displayTitle, createdAt, updatedAt, doneAt, removedAt, + pacient_jmeno, pacient_prijmeni, pacient_rodnecislo + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + displayTitle=VALUES(displayTitle), + updatedAt=VALUES(updatedAt), + doneAt=VALUES(doneAt), + removedAt=VALUES(removedAt), + pacient_jmeno=VALUES(pacient_jmeno), + pacient_prijmeni=VALUES(pacient_prijmeni), + pacient_rodnecislo=VALUES(pacient_rodnecislo) + """ + + vals = ( + r.get("id"), + r.get("displayTitle"), + to_mysql_dt(r.get("createdAt")), + final_updated, + to_mysql_dt(r.get("doneAt")), + to_mysql_dt(r.get("removedAt")), + p.get("name"), + p.get("surname"), + p.get("identificationNumber"), + ) + + with conn.cursor() as cur: + cur.execute(sql, vals) + conn.commit() + + +# ================================ +# FETCH PAGE (per state) +# ================================ +def fetch_state(headers, state, offset): + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "state": state, + "pageInfo": {"first": BATCH_SIZE, "offset": offset}, + "locale": "cs", + } + + payload = { + "operationName": "ClinicRequestList2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) + r.raise_for_status() + + data = r.json()["data"]["requestsResponse"] + return data.get("patientRequests", []), data.get("count", 0) + + +# ================================ +# MAIN +# ================================ +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + safe_print(f"\n=== FULL Medevio READ-ALL sync @ {datetime.now():%Y-%m-%d %H:%M:%S} ===") + + grand_total = 0 + + for state in STATES: + safe_print(f"\n🔁 STATE = {state}") + offset = 0 + total = None + processed = 0 + + while True: + batch, count = fetch_state(headers, state, offset) + + if total is None: + total = count + safe_print(f"📡 {state}: celkem {total}") + + if not batch: + break + + for r in batch: + upsert(conn, r) + + processed += len(batch) + safe_print(f" • {processed}/{total}") + + offset += BATCH_SIZE + if offset >= count: + break + + time.sleep(0.4) + + grand_total += processed + + conn.close() + safe_print(f"\n✅ HOTOVO – celkem zpracováno {grand_total} požadavků\n") + + +# ================================ +if __name__ == "__main__": + main() diff --git a/Medevio/12 Tower1/20 ReadPoznamky.py b/Medevio/12 Tower1/20 ReadPoznamky.py new file mode 100644 index 0000000..f74fdbf --- /dev/null +++ b/Medevio/12 Tower1/20 ReadPoznamky.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Download and store Medevio questionnaires (userNote + eCRF) for all patient requests. +Uses the verified working query "GetPatientRequest2". +""" + +import json +import requests +import pymysql +from datetime import datetime +from pathlib import Path +import time +import sys + +# Force UTF-8 output even under Windows Task Scheduler +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +# ============================== +# 🛡 SAFE PRINT FOR CP1250 / EMOJI +# ============================== +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc.lower().startswith("utf"): + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ============================== +# 🔧 CONFIGURATION (UPDATED TO 192.168.1.50) +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + + +# ============================== +# 🕒 DATETIME FIXER +# ============================== +def fix_datetime(dt_str): + if not dt_str: + return None + try: + return datetime.fromisoformat(dt_str.replace("Z", "").replace("+00:00", "")) + except Exception: + return None + + +# Optional filter +CREATED_AFTER = "2025-01-01" + + +# ============================== +# 🧮 HELPERS +# ============================== +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + return tok.split(" ", 1)[1] + return tok + + +GRAPHQL_QUERY = r""" +query GetPatientRequest2($requestId: UUID!, $clinicSlug: String!, $locale: Locale!) { + request: getPatientRequest2(patientRequestId: $requestId, clinicSlug: $clinicSlug) { + id + displayTitle(locale: $locale) + createdAt + updatedAt + userNote + eventType + extendedPatient(clinicSlug: $clinicSlug) { + name + surname + identificationNumber + } + ecrfFilledData(locale: $locale) { + name + groups { + label + fields { + name + label + type + value + } + } + } + } +} +""" + + +def fetch_questionnaire(headers, request_id, clinic_slug): + payload = { + "operationName": "GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": { + "requestId": request_id, + "clinicSlug": clinic_slug, + "locale": "cs", + }, + } + r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=40) + if r.status_code != 200: + safe_print(f"❌ HTTP {r.status_code} for {request_id}: {r.text}") + return None + return r.json().get("data", {}).get("request") + + +def insert_questionnaire(cur, req): + if not req: + return + + patient = req.get("extendedPatient") or {} + ecrf_data = req.get("ecrfFilledData") + created_at = fix_datetime(req.get("createdAt")) + updated_at = fix_datetime(req.get("updatedAt")) + + cur.execute(""" + INSERT INTO medevio_questionnaires ( + request_id, created_at, updated_at, user_note, ecrf_json + ) + VALUES (%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + updated_at = VALUES(updated_at), + user_note = VALUES(user_note), + ecrf_json = VALUES(ecrf_json), + updated_local = NOW() + """, ( + req.get("id"), + created_at, + updated_at, + req.get("userNote"), + json.dumps(ecrf_data, ensure_ascii=False), + )) + + safe_print(f" 💾 Stored questionnaire for {patient.get('surname','')} {patient.get('name','')}") + + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + # load list of requests from the table we just filled + with conn.cursor() as cur: + sql = """ + SELECT id, pacient_jmeno, pacient_prijmeni, createdAt, updatedAt, questionnaireprocessed + FROM pozadavky + WHERE (questionnaireprocessed IS NULL OR questionnaireprocessed < updatedAt) + """ + if CREATED_AFTER: + sql += " AND createdAt >= %s" + cur.execute(sql, (CREATED_AFTER,)) + else: + cur.execute(sql) + + rows = cur.fetchall() + + safe_print(f"📋 Found {len(rows)} requests needing questionnaire check.") + + for i, row in enumerate(rows, 1): + req_id = row["id"] + safe_print(f"\n[{i}/{len(rows)}] 🔍 Fetching questionnaire for {req_id} ...") + + req = fetch_questionnaire(headers, req_id, CLINIC_SLUG) + if not req: + safe_print(" ⚠️ No questionnaire data found.") + continue + + with conn.cursor() as cur: + insert_questionnaire(cur, req) + cur.execute( + "UPDATE pozadavky SET questionnaireprocessed = NOW() WHERE id = %s", + (req_id,) + ) + conn.commit() + + time.sleep(0.6) + + conn.close() + safe_print("\n✅ Done! All questionnaires stored in MySQL table `medevio_questionnaires`.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Medevio/12 Tower1/30 ReadConversation.py b/Medevio/12 Tower1/30 ReadConversation.py new file mode 100644 index 0000000..e1d73e9 --- /dev/null +++ b/Medevio/12 Tower1/30 ReadConversation.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time +import sys + +# UTF-8 SAFE OUTPUT +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + pass + +# ============================== +# CONFIG (.50) +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY_MESSAGES = r""" +query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) { + messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) { + id createdAt updatedAt readAt text type + sender { id name surname clinicId } + medicalRecord { id description contentType url downloadUrl createdAt updatedAt } + } +} +""" + +def parse_dt(s): + if not s: return None + try: return datetime.fromisoformat(s.replace("Z", "+00:00")) + except: return None + +def read_token(path: Path) -> str: + return path.read_text(encoding="utf-8").strip().replace("Bearer ", "") + +def main(): + token = read_token(TOKEN_PATH) + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + conn = pymysql.connect(**DB_CONFIG) + + # 1. Seznam již stažených příloh (prevence duplicit) + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {r["attachment_id"] for r in cur.fetchall()} + + # 2. Seznam požadavků k synchronizaci + with conn.cursor() as cur: + cur.execute(""" + SELECT id, messagesProcessed FROM pozadavky + WHERE messagesProcessed IS NULL OR messagesProcessed < updatedAt + """) + rows = cur.fetchall() + + print(f"📋 Počet požadavků k synchronizaci zpráv: {len(rows)}") + + for i, row in enumerate(rows, 1): + req_id = row["id"] + updated_since = row["messagesProcessed"] + if updated_since: + updated_since = updated_since.replace(microsecond=0).isoformat() + "Z" + + print(f"[{i}/{len(rows)}] Synchronizuji: {req_id}") + + payload = { + "operationName": "UseMessages_ListMessages", + "query": GRAPHQL_QUERY_MESSAGES, + "variables": {"requestId": req_id, "updatedSince": updated_since} + } + + try: + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + messages = r.json().get("data", {}).get("messages", []) or [] + + if messages: + with conn.cursor() as cur: + for msg in messages: + # Uložení zprávy + sender = msg.get("sender") or {} + sender_name = " ".join(filter(None, [sender.get("name"), sender.get("surname")])) + mr = msg.get("medicalRecord") or {} + + cur.execute(""" + INSERT INTO medevio_conversation ( + id, request_id, sender_name, sender_id, sender_clinic_id, + text, created_at, read_at, updated_at, + attachment_url, attachment_description, attachment_content_type + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + text = VALUES(text), updated_at = VALUES(updated_at), read_at = VALUES(read_at) + """, ( + msg.get("id"), req_id, sender_name, sender.get("id"), sender.get("clinicId"), + msg.get("text"), parse_dt(msg.get("createdAt")), parse_dt(msg.get("readAt")), + parse_dt(msg.get("updatedAt")), mr.get("downloadUrl") or mr.get("url"), + mr.get("description"), mr.get("contentType") + )) + + # Uložení přílohy (pokud existuje a nemáme ji) + attachment_id = mr.get("id") + if attachment_id and attachment_id not in existing_ids: + url = mr.get("downloadUrl") or mr.get("url") + if url: + att_r = requests.get(url, timeout=30) + if att_r.status_code == 200: + cur.execute(""" + INSERT INTO medevio_downloads ( + request_id, attachment_id, attachment_type, + filename, content_type, file_size, created_at, file_content + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s) + """, ( + req_id, attachment_id, "MESSAGE_ATTACHMENT", + url.split("/")[-1].split("?")[0], mr.get("contentType"), + len(att_r.content), parse_dt(msg.get("createdAt")), att_r.content + )) + existing_ids.add(attachment_id) + + cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + else: + with conn.cursor() as cur: + cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + + time.sleep(0.3) + except Exception as e: + print(f" ❌ Chyba u {req_id}: {e}") + + conn.close() + print("\n🎉 Delta sync zpráv a příloh DOKONČEN") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Medevio/12 Tower1/40 DownloadPrilohy.py b/Medevio/12 Tower1/40 DownloadPrilohy.py new file mode 100644 index 0000000..cd3d539 --- /dev/null +++ b/Medevio/12 Tower1/40 DownloadPrilohy.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Download all attachments for pozadavky where attachmentsProcessed IS NULL +Store them in MySQL table `medevio_downloads` on 192.168.1.50. +""" + +import zlib +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time +import sys + +# Force UTF-8 output +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + pass + + +# ============================== +# 🛡 SAFE PRINT +# ============================== +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc or not enc.lower().startswith("utf"): + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ============================== +# 🔧 CONFIGURATION (.50) +# ============================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +CLINIC_SLUG = "mudr-buzalkova" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +CREATED_AFTER = "2024-12-01" + +GRAPHQL_QUERY = r""" +query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) { + patientRequestMedicalRecords: listMedicalRecordsForPatientRequest( + attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT] + patientRequestId: $requestId + pageInfo: {first: 100, offset: 0} + ) { + attachmentType + id + medicalRecord { + contentType + description + downloadUrl + id + url + visibleToPatient + } + } +} +""" + + +def extract_filename_from_url(url: str) -> str: + try: + return url.split("/")[-1].split("?")[0] + except: + return "unknown_filename" + + +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok + + +def main(): + token = read_token(TOKEN_PATH) + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + conn = pymysql.connect(**DB_CONFIG) + + # 1. Načtení ID již stažených příloh + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {row["attachment_id"] for row in cur.fetchall()} + + safe_print(f"✅ V databázi již máme {len(existing_ids)} příloh.") + + # 2. Výběr požadavků ke zpracování + sql = "SELECT id, pacient_prijmeni, pacient_jmeno, createdAt FROM pozadavky WHERE attachmentsProcessed IS NULL" + params = [] + if CREATED_AFTER: + sql += " AND createdAt >= %s" + params.append(CREATED_AFTER) + + with conn.cursor() as cur: + cur.execute(sql, params) + req_rows = cur.fetchall() + + safe_print(f"📋 Počet požadavků ke stažení příloh: {len(req_rows)}") + + for i, row in enumerate(req_rows, 1): + req_id = row["id"] + prijmeni = row.get("pacient_prijmeni") or "Neznamy" + created_date = row.get("createdAt") or datetime.now() + + safe_print(f"\n[{i}/{len(req_rows)}] 🧾 {prijmeni} ({req_id})") + + payload = { + "operationName": "ClinicRequestDetail_GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": {"requestId": req_id}, + } + + try: + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + attachments = r.json().get("data", {}).get("patientRequestMedicalRecords", []) + + if attachments: + with conn.cursor() as cur: + for a in attachments: + m = a.get("medicalRecord") or {} + att_id = a.get("id") + + if att_id in existing_ids: + continue + + url = m.get("downloadUrl") + if url: + att_r = requests.get(url, timeout=30) + if att_r.status_code == 200: + content = att_r.content + filename = extract_filename_from_url(url) + + cur.execute(""" + INSERT INTO medevio_downloads ( + request_id, attachment_id, attachment_type, + filename, content_type, file_size, + created_at, file_content + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s) + """, (req_id, att_id, a.get("attachmentType"), filename, + m.get("contentType"), len(content), created_date, content)) + existing_ids.add(att_id) + safe_print(f" 💾 Uloženo: {filename} ({len(content) / 1024:.1f} kB)") + + conn.commit() + + # Označíme jako zpracované i když nebyly nalezeny žádné přílohy + with conn.cursor() as cur: + cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + + time.sleep(0.3) + except Exception as e: + print(f" ❌ Chyba u {req_id}: {e}") + + conn.close() + safe_print("\n🎯 Všechny přílohy byly zpracovány.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Medevio/12 Tower1/50 SaveToFileSystem incremental.py b/Medevio/12 Tower1/50 SaveToFileSystem incremental.py new file mode 100644 index 0000000..ee6925a --- /dev/null +++ b/Medevio/12 Tower1/50 SaveToFileSystem incremental.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import shutil +import pymysql +import re +from pathlib import Path +from datetime import datetime +from collections import defaultdict +import time +import sys + +# Force UTF-8 output even under Windows Task Scheduler +import sys +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + # Python < 3.7 fallback (not needed for you, but safe) + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +# ============================== +# 🛡 SAFE PRINT FOR CP1250 / EMOJI +# ============================== +def safe_print(text: str = ""): + enc = sys.stdout.encoding or "" + if not enc.lower().startswith("utf"): + # Strip emoji and characters outside BMP for Task Scheduler + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + # ASCII fallback + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ============================== +# ⚙️ CONFIGURATION +# ============================== +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", +} + +BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP") +BASE_DIR.mkdir(parents=True, exist_ok=True) + + +def sanitize_name(name: str) -> str: + """Replace invalid filename characters with underscore.""" + return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip() + + +def make_abbrev(title: str) -> str: + if not title: + return "" + words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title) + abbr = "" + for w in words: + if w.isdigit(): + abbr += w + else: + abbr += w[0] + return abbr.upper() + + +# ============================== +# 🧹 DELETE UNEXPECTED FILES +# ============================== +def clean_folder(folder: Path, valid_files: set): + if not folder.exists(): + return + + for f in folder.iterdir(): + if f.is_file(): + if f.name.startswith("▲"): + continue + sanitized = sanitize_name(f.name) + if sanitized not in valid_files: + safe_print(f"🗑️ Removing unexpected file: {f.name}") + try: + f.unlink() + except Exception as e: + safe_print(f"⚠️ Could not delete {f}: {e}") + + +# ============================== +# 📦 DB CONNECTION +# ============================== +conn = pymysql.connect(**DB_CONFIG) + +cur_meta = conn.cursor(pymysql.cursors.DictCursor) +cur_blob = conn.cursor() + +safe_print("🔍 Loading metadata from DB (FAST)…") + +cur_meta.execute(""" + SELECT d.id AS download_id, + d.request_id, + d.filename, + d.created_at, + p.updatedAt AS req_updated_at, + p.pacient_jmeno AS jmeno, + p.pacient_prijmeni AS prijmeni, + p.displayTitle + FROM medevio_downloads d + JOIN pozadavky p ON d.request_id = p.id + WHERE p.updatedAt >= DATE_SUB(NOW(), INTERVAL 14 DAY) + ORDER BY p.updatedAt DESC +""") + +rows = cur_meta.fetchall() +safe_print(f"📋 Found {len(rows)} attachment records.\n") + +# ============================== +# 🧠 MAIN LOOP WITH PROGRESS +# ============================== + +# Group rows by request_id in Python — avoids N extra SELECT filename queries +rows_by_request = defaultdict(list) +for r in rows: + rows_by_request[r["request_id"]].append(r) + +total_requests = len(rows_by_request) +safe_print(f"🔄 Processing {total_requests} unique requests...\n") + +# Pre-index BASE_DIR once — avoids iterdir() called twice per request +folder_list = [(f, f.name) for f in BASE_DIR.iterdir() if f.is_dir()] + +for current_index, (req_id, req_rows) in enumerate(rows_by_request.items(), 1): + percent = (current_index / total_requests) * 100 + safe_print(f"\n[ {percent:5.1f}% ] Processing request {current_index} / {total_requests} → {req_id}") + + # ========== VALID FILENAMES from already-loaded rows ========== + # original filename → sanitized name (needed for DB query later) + file_map = {sanitize_name(r["filename"]): r["filename"] for r in req_rows} + valid_files = set(file_map.keys()) + + # ========== BUILD FOLDER NAME ========== + r = req_rows[0] + updated_at = r["req_updated_at"] or datetime.now() + date_str = updated_at.strftime("%Y-%m-%d") + + prijmeni = sanitize_name(r["prijmeni"] or "Unknown") + jmeno = sanitize_name(r["jmeno"] or "") + title = r.get("displayTitle") or "" + abbr = make_abbrev(title) + + clean_folder_name = sanitize_name( + f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}" + ) + + # ========== DETECT EXISTING FOLDER from pre-built index ========== + req_id_str = str(req_id) + matching = [f for f, name in folder_list if req_id_str in name] + existing_folder = matching[0] if matching else None + + main_folder = existing_folder if existing_folder else BASE_DIR / clean_folder_name + + # ========== MERGE DUPLICATES ========== + possible_dups = [f for f, name in folder_list if req_id_str in name and f != main_folder] + + for dup in possible_dups: + safe_print(f"♻️ Merging duplicate folder: {dup.name}") + + clean_folder(dup, valid_files) + main_folder.mkdir(parents=True, exist_ok=True) + + for f in dup.iterdir(): + if f.is_file(): + target = main_folder / f.name + if not target.exists(): + f.rename(target) + + shutil.rmtree(dup, ignore_errors=True) + + # ========== CLEAN MAIN FOLDER ========== + clean_folder(main_folder, valid_files) + + # ========== DOWNLOAD MISSING FILES (batch blob fetch per request) ========== + main_folder.mkdir(parents=True, exist_ok=True) + added_new_file = False + + missing_san = [ + fn for fn in valid_files + if not (main_folder / fn).exists() and not (main_folder / ("▲" + fn)).exists() + ] + + if missing_san: + # Fetch all missing blobs in a single query instead of one per file + missing_orig = [file_map[fn] for fn in missing_san] + placeholders = ",".join(["%s"] * len(missing_orig)) + cur_blob.execute( + f"SELECT filename, file_content FROM medevio_downloads " + f"WHERE request_id=%s AND filename IN ({placeholders})", + [req_id] + missing_orig, + ) + for blob_filename, content in cur_blob.fetchall(): + if not content: + continue + dest_plain = main_folder / sanitize_name(blob_filename) + with open(dest_plain, "wb") as fh: + fh.write(content) + safe_print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}") + added_new_file = True + + # ========== REMOVE ▲ FLAG IF NEW FILES ADDED ========== + if added_new_file and "▲" in main_folder.name: + new_name = main_folder.name.replace("▲", "").strip() + new_path = main_folder.parent / new_name + + if new_path != main_folder: + try: + main_folder.rename(new_path) + safe_print(f"🔄 Folder flag ▲ removed → {new_name}") + main_folder = new_path + except Exception as e: + safe_print(f"⚠️ Could not rename folder: {e}") + +safe_print("\n🎯 Export complete.\n") + +cur_blob.close() +cur_meta.close() +conn.close() diff --git a/Medevio/12 Tower1/50 SaveToFileSystem single step.py b/Medevio/12 Tower1/50 SaveToFileSystem single step.py new file mode 100644 index 0000000..be2a042 --- /dev/null +++ b/Medevio/12 Tower1/50 SaveToFileSystem single step.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import shutil +import pymysql +import re +from pathlib import Path +from datetime import datetime + +# ============================== +# ⚙️ CONFIGURATION +# ============================== +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", +} + +BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP") +BASE_DIR.mkdir(parents=True, exist_ok=True) + + +def sanitize_name(name: str) -> str: + return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip() + + +def clean_folder(folder: Path, valid_files: set): + """Remove files that do NOT exist in MySQL for this request.""" + if not folder.exists(): + return + + for f in folder.iterdir(): + if f.is_file() and sanitize_name(f.name) not in valid_files: + print(f"🗑️ Removing unexpected file: {f.name}") + try: + f.unlink() + except Exception as e: + print(f"⚠️ Cannot delete {f}: {e}") + + +# ============================== +# 📥 LOAD EVERYTHING IN ONE QUERY +# ============================== +conn = pymysql.connect(**DB_CONFIG) +cur = conn.cursor(pymysql.cursors.DictCursor) + +print("📥 Loading ALL metadata + BLOBs with ONE MySQL query…") + +cur.execute(""" + SELECT + d.id AS download_id, + d.request_id, + d.filename, + d.file_content, + p.updatedAt AS req_updated_at, + p.pacient_jmeno AS jmeno, + p.pacient_prijmeni AS prijmeni + FROM medevio_downloads d + JOIN pozadavky p ON d.request_id = p.id + ORDER BY p.updatedAt DESC, d.created_at ASC +""") + +rows = cur.fetchall() +print(f"📦 Loaded {len(rows)} total file rows.\n") + +conn.close() + +# ============================== +# 🔄 ORGANIZE ROWS PER REQUEST +# ============================== +requests = {} # req_id → list of file dicts + +for r in rows: + req_id = r["request_id"] + if req_id not in requests: + requests[req_id] = [] + requests[req_id].append(r) + +print(f"📌 Unique requests: {len(requests)}\n") + +# ============================== +# 🧠 MAIN LOOP – SAME LOGIC AS BEFORE +# ============================== +for req_id, filelist in requests.items(): + + # ========== GET UPDATEDAT (same logic) ========== + any_row = filelist[0] + updated_at = any_row["req_updated_at"] or datetime.now() + date_str = updated_at.strftime("%Y-%m-%d") + + prijmeni = sanitize_name(any_row["prijmeni"] or "Unknown") + jmeno = sanitize_name(any_row["jmeno"] or "") + + folder_name = sanitize_name(f"{date_str} {prijmeni}, {jmeno} {req_id}") + main_folder = BASE_DIR / folder_name + + # ========== VALID FILES ========== + valid_files = {sanitize_name(r["filename"]) for r in filelist} + + # ========== FIND OLD FOLDERS ========== + possible_dups = [ + f for f in BASE_DIR.iterdir() + if f.is_dir() and req_id in f.name and f != main_folder + ] + + # ========== MERGE OLD FOLDERS ========== + for dup in possible_dups: + print(f"♻️ Merging folder: {dup.name}") + + clean_folder(dup, valid_files) + main_folder.mkdir(parents=True, exist_ok=True) + + for f in dup.iterdir(): + if f.is_file(): + target = main_folder / f.name + if not target.exists(): + f.rename(target) + + shutil.rmtree(dup, ignore_errors=True) + + # ========== CLEAN MAIN FOLDER ========== + main_folder.mkdir(parents=True, exist_ok=True) + clean_folder(main_folder, valid_files) + + # ========== SAVE FILES (fast now) ========== + for r in filelist: + filename = sanitize_name(r["filename"]) + dest = main_folder / filename + + if dest.exists(): + continue + + content = r["file_content"] + if not content: + continue + + with open(dest, "wb") as f: + f.write(content) + + print(f"💾 Saved: {dest.relative_to(BASE_DIR)}") + +print("\n🎯 Export complete.\n") diff --git a/Medevio/20SaveDownloads/10 SaveToFilesystem.py b/Medevio/20SaveDownloads/10 SaveToFilesystem.py new file mode 100644 index 0000000..075f45f --- /dev/null +++ b/Medevio/20SaveDownloads/10 SaveToFilesystem.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import zlib +import pymysql +import re +from pathlib import Path +from datetime import datetime + +# ============================== +# ⚙️ CONFIGURATION +# ============================== +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", +} + +BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP") +BASE_DIR.mkdir(parents=True, exist_ok=True) + + +def sanitize_name(name: str) -> str: + """Replace invalid filename characters with underscore.""" + return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip() + + +# ============================== +# 📦 STREAMING EXPORT WITH TRIANGLE CHECK +# ============================== +conn = pymysql.connect(**DB_CONFIG) +cur_meta = conn.cursor(pymysql.cursors.DictCursor) +cur_blob = conn.cursor() + +cur_meta.execute(""" + SELECT id, request_id, attachment_id, filename, pacient_jmeno, + pacient_prijmeni, created_at, downloaded_at + FROM medevio_downloads + WHERE file_content IS NOT NULL; +""") + +rows = cur_meta.fetchall() +print(f"📋 Found {len(rows)} records to check/export") + +skipped, exported = 0, 0 + +for r in rows: + try: + created = r["created_at"] or r["downloaded_at"] or datetime.now() + date_str = created.strftime("%Y-%m-%d") + + prijmeni = sanitize_name(r["pacient_prijmeni"] or "Unknown") + jmeno = sanitize_name(r["pacient_jmeno"] or "") + + crc = f"{zlib.crc32(r['request_id'].encode('utf-8')) & 0xFFFFFFFF:08X}" + + # Base (non-triangle) and processed (triangle) folder variants + base_folder = sanitize_name(f"{date_str} {prijmeni}, {jmeno} {crc}") + tri_folder = sanitize_name(f"{date_str}▲ {prijmeni}, {jmeno} {crc}") + + base_path = BASE_DIR / base_folder + tri_path = BASE_DIR / tri_folder + + filename = sanitize_name(r["filename"] or f"unknown_{r['id']}.bin") + file_path_base = base_path / filename + file_path_tri = tri_path / filename + + # 🟡 Skip if exists in either version + if file_path_base.exists() or file_path_tri.exists(): + skipped += 1 + found_in = "▲" if file_path_tri.exists() else "" + print(f"⏭️ Skipping existing{found_in}: {filename}") + continue + + # Make sure base folder exists before saving + base_path.mkdir(parents=True, exist_ok=True) + + # 2️⃣ Fetch blob + cur_blob.execute("SELECT file_content FROM medevio_downloads WHERE id = %s", (r["id"],)) + blob = cur_blob.fetchone()[0] + + if blob: + with open(file_path_base, "wb") as f: + f.write(blob) + exported += 1 + print(f"✅ Saved: {file_path_base.relative_to(BASE_DIR)}") + else: + print(f"⚠️ No content for id={r['id']}") + + except Exception as e: + print(f"❌ Error for id={r['id']}: {e}") + +cur_blob.close() +cur_meta.close() +conn.close() + +print(f"\n🎯 Export complete — {exported} new files saved, {skipped} skipped.\n") diff --git a/Medevio/20SaveDownloads/20 SaveToFileSystem.py b/Medevio/20SaveDownloads/20 SaveToFileSystem.py new file mode 100644 index 0000000..fd636c4 --- /dev/null +++ b/Medevio/20SaveDownloads/20 SaveToFileSystem.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import zlib +import pymysql +import re +from pathlib import Path +from datetime import datetime + +# ============================== +# ⚙️ CONFIGURATION +# ============================== +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", +} + +BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP") +BASE_DIR.mkdir(parents=True, exist_ok=True) + + +def sanitize_name(name: str) -> str: + """Replace invalid filename characters with underscore.""" + return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip() + + +# ============================== +# 📦 EXPORT WITH JOIN TO POZADAVKY +# ============================== +conn = pymysql.connect(**DB_CONFIG) +cur_meta = conn.cursor(pymysql.cursors.DictCursor) +cur_blob = conn.cursor() + +# 🎯 JOIN medevio_downloads → pozadavky +cur_meta.execute(""" + SELECT d.id, d.request_id, d.attachment_id, d.filename, + d.created_at, d.downloaded_at, + p.pacient_jmeno AS jmeno, + p.pacient_prijmeni AS prijmeni + FROM medevio_downloads d + JOIN pozadavky p ON d.request_id = p.id + WHERE d.file_content IS NOT NULL; +""") + +rows = cur_meta.fetchall() +print(f"📋 Found {len(rows)} records to check/export") + +skipped, exported = 0, 0 + +for r in rows: + try: + created = r["created_at"] or r["downloaded_at"] or datetime.now() + date_str = created.strftime("%Y-%m-%d") + + # 👍 Now always correct from pozadavky + prijmeni = sanitize_name(r["prijmeni"] or "Unknown") + jmeno = sanitize_name(r["jmeno"] or "") + + # 🔥 Full request_id for folder identification + full_req_id = sanitize_name(r["request_id"]) + + # Folder names (normal and triangle) + base_folder = f"{date_str} {prijmeni}, {jmeno} {full_req_id}" + tri_folder = f"{date_str}▲ {prijmeni}, {jmeno} {full_req_id}" + + base_folder = sanitize_name(base_folder) + tri_folder = sanitize_name(tri_folder) + + base_path = BASE_DIR / base_folder + tri_path = BASE_DIR / tri_folder + + filename = sanitize_name(r["filename"] or f"unknown_{r['id']}.bin") + file_path_base = base_path / filename + file_path_tri = tri_path / filename + + # 🟡 Skip if file already exists + if file_path_base.exists() or file_path_tri.exists(): + skipped += 1 + found_in = "▲" if file_path_tri.exists() else "" + print(f"⏭️ Skipping existing{found_in}: {filename}") + continue + + # Ensure directory exists + base_path.mkdir(parents=True, exist_ok=True) + + # 2️⃣ Fetch blob content + cur_blob.execute( + "SELECT file_content FROM medevio_downloads WHERE id = %s", + (r["id"],) + ) + blob = cur_blob.fetchone()[0] + + if blob: + with open(file_path_base, "wb") as f: + f.write(blob) + exported += 1 + print(f"✅ Saved: {file_path_base.relative_to(BASE_DIR)}") + else: + print(f"⚠️ No content for id={r['id']}") + + except Exception as e: + print(f"❌ Error for id={r['id']}: {e}") + +cur_blob.close() +cur_meta.close() +conn.close() + +print(f"\n🎯 Export complete — {exported} new files saved, {skipped} skipped.\n") diff --git a/Medevio/30 ManipulacePoznámek/100 JednoducheCteni1poznamky.py b/Medevio/30 ManipulacePoznámek/100 JednoducheCteni1poznamky.py new file mode 100644 index 0000000..3d8b0ab --- /dev/null +++ b/Medevio/30 ManipulacePoznámek/100 JednoducheCteni1poznamky.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import requests +from pathlib import Path +import sys + +# UTF-8 safety +try: + sys.stdout.reconfigure(encoding='utf-8') +except: + pass + +# === CONFIG === + +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +GRAPHQL_URL = "https://api.medevio.cz/graphql" +REQUEST_ID = "e17536c4-ed22-4242-ada5-d03713e0b7ac" # požadavek který sledujeme + + +def read_token(path: Path) -> str: + t = path.read_text().strip() + if t.startswith("Bearer "): + return t.split(" ", 1)[1] + return t + + +# === QUERY === + +QUERY = r""" +query ClinicRequestNotes_Get($patientRequestId: String!) { + notes: getClinicPatientRequestNotes(requestId: $patientRequestId) { + id + content + createdAt + updatedAt + createdBy { + id + name + surname + } + } +} +""" + + +def run_query(request_id, token): + payload = { + "operationName": "ClinicRequestNotes_Get", + "query": QUERY, + "variables": {"patientRequestId": request_id}, + } + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + r = requests.post(GRAPHQL_URL, json=payload, headers=headers) + r.raise_for_status() + + return r.json() + + +def main(): + token = read_token(TOKEN_PATH) + + print(f"🔍 Čtu interní klinické poznámky k požadavku {REQUEST_ID} ...\n") + + data = run_query(REQUEST_ID, token) + + notes = data.get("data", {}).get("notes", []) + if not notes: + print("📭 Žádné klinické poznámky nejsou uložené.") + return + + print(f"📌 Nalezeno {len(notes)} poznámek:\n") + + for n in notes: + print("──────────────────────────────") + print(f"🆔 ID: {n['id']}") + print(f"👤 Vytvořil: {n['createdBy']['surname']} {n['createdBy']['name']}") + print(f"📅 createdAt: {n['createdAt']}") + print(f"🕒 updatedAt: {n['updatedAt']}") + print("📝 Obsah:") + print(n['content']) + print("") + + +if __name__ == "__main__": + main() diff --git a/Medevio/30 ManipulacePoznámek/101 JednoducheDoplneniInterniPoznamky.py b/Medevio/30 ManipulacePoznámek/101 JednoducheDoplneniInterniPoznamky.py new file mode 100644 index 0000000..8a2a135 --- /dev/null +++ b/Medevio/30 ManipulacePoznámek/101 JednoducheDoplneniInterniPoznamky.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import requests +from pathlib import Path +import sys + +# UTF-8 handling +try: + sys.stdout.reconfigure(encoding='utf-8') +except: + pass + +# === CONFIG === + +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +REQUEST_ID = "e17536c4-ed22-4242-ada5-d03713e0b7ac" # požadavek +NOTE_PREPEND_TEXT = "🔥 NOVÝ TESTOVACÍ ŘÁDEK\n" # text, který se přidá NA ZAČÁTEK + + +# === Helpers === + +def read_token(p: Path) -> str: + t = p.read_text().strip() + if t.startswith("Bearer "): + return t.split(" ", 1)[1] + return t + + +# === Queries === + +QUERY_GET_NOTES = r""" +query ClinicRequestNotes_Get($patientRequestId: String!) { + notes: getClinicPatientRequestNotes(requestId: $patientRequestId) { + id + content + createdAt + updatedAt + createdBy { + id + name + surname + } + } +} +""" + +MUTATION_UPDATE_NOTE = r""" +mutation ClinicRequestNotes_Update($noteInput: UpdateClinicPatientRequestNoteInput!) { + updateClinicPatientRequestNote(noteInput: $noteInput) { + id + } +} +""" + + +# === Core functions === + +def gql(query, variables, token): + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + payload = {"query": query, "variables": variables} + + r = requests.post(GRAPHQL_URL, json=payload, headers=headers) + r.raise_for_status() + return r.json() + + +def get_internal_note(request_id, token): + data = gql(QUERY_GET_NOTES, {"patientRequestId": request_id}, token) + notes = data.get("data", {}).get("notes", []) + return notes[0] if notes else None + + +def update_internal_note(note_id, new_content, token): + variables = {"noteInput": {"id": note_id, "content": new_content}} + return gql(MUTATION_UPDATE_NOTE, variables, token) + + +# === MAIN === + +def main(): + token = read_token(TOKEN_PATH) + + print(f"🔍 Načítám interní poznámku pro požadavek {REQUEST_ID}...\n") + + note = get_internal_note(REQUEST_ID, token) + if not note: + print("❌ Nebyla nalezena žádná interní klinická poznámka!") + return + + note_id = note["id"] + old_content = note["content"] or "" + + print("📄 Původní obsah:") + print(old_content) + print("────────────────────────────\n") + + # =============================== + # PREPEND new text + # =============================== + new_content = NOTE_PREPEND_TEXT + old_content + + print("📝 Nový obsah který odešlu:") + print(new_content) + print("────────────────────────────\n") + + # UPDATE + result = update_internal_note(note_id, new_content, token) + + print(f"✅ Hotovo! Poznámka {note_id} aktualizována.") + print(result) + + +if __name__ == "__main__": + main() diff --git a/Medevio/30 ManipulacePoznámek/102 Zapiš ID request do nejnovějšího otevřeného požadavku.py b/Medevio/30 ManipulacePoznámek/102 Zapiš ID request do nejnovějšího otevřeného požadavku.py new file mode 100644 index 0000000..fd89f5b --- /dev/null +++ b/Medevio/30 ManipulacePoznámek/102 Zapiš ID request do nejnovějšího otevřeného požadavku.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import requests +import mysql.connector +from pathlib import Path +import sys +from datetime import datetime + +# UTF-8 handling +try: + sys.stdout.reconfigure(encoding='utf-8') +except: + pass + +# === KONFIGURACE === + +# --- Medevio API --- +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +# --- ZPRACOVÁNÍ --- +# Zadejte počet požadavků ke zpracování. +# 0 znamená zpracovat VŠECHNY nesynchronizované požadavky. +PROCESS_LIMIT = 10 # <-- Používáme PROCESS_LIMIT + +# --- MySQL DB --- +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", +} + + +# === Helpers === + +def read_token(p: Path) -> str: + """Načte Bearer token z textového souboru.""" + t = p.read_text().strip() + if t.startswith("Bearer "): + return t.split(" ", 1)[1] + return t + + +# === DB Funkce === + +def get_requests_to_process_from_db(limit): + """ + Získá seznam požadavků (ID, Titul, Jméno, Příjmení) k synchronizaci z MySQL. + Použije LIMIT, pokud limit > 0. + """ + if limit == 0: + print("🔍 Připojuji se k MySQL a hledám **VŠECHNY** nesynchronizované požadavky...") + else: + print(f"🔍 Připojuji se k MySQL a hledám **{limit}** nesynchronizovaných požadavků...") + + requests_list = [] + conn = None + try: + conn = mysql.connector.connect(**DB_CONFIG) + cursor = conn.cursor() + + # Základní SQL dotaz + query = """ + SELECT id, displayTitle, pacient_jmeno, pacient_prijmeni + FROM pozadavky + WHERE doneAt IS NULL + AND noteSyncedAt IS NULL + ORDER BY updatedAt DESC + """ + + # Podmíněné přidání LIMIT klauzule + if limit > 0: + query += f"LIMIT {limit};" + else: + query += ";" + + cursor.execute(query) + results = cursor.fetchall() + + for result in results: + request_id, display_title, jmeno, prijmeni = result + requests_list.append({ + "id": request_id, + "displayTitle": display_title, + "jmeno": jmeno, + "prijmeni": prijmeni + }) + + cursor.close() + + if requests_list: + print(f"✅ Nalezeno {len(requests_list)} požadavků ke zpracování.") + else: + print("❌ Nebyl nalezen žádný nesynchronizovaný otevřený požadavek v DB.") + + return requests_list + + except mysql.connector.Error as err: + print(f"❌ Chyba při připojení/dotazu MySQL: {err}") + return [] + finally: + if conn and conn.is_connected(): + conn.close() + + +def update_db_sync_time(request_id, conn): + """Aktualizuje sloupec noteSyncedAt v tabulce pozadavky. Používá existující připojení.""" + cursor = conn.cursor() + + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + update_query = """ + UPDATE pozadavky + SET noteSyncedAt = %s + WHERE id = %s; + """ + + cursor.execute(update_query, (current_time, request_id)) + conn.commit() + + cursor.close() + print(f" (DB: Čas synchronizace pro {request_id} uložen)") + + +# === GraphQL Operace (Beze Změny) === + +QUERY_GET_NOTE = r""" +query ClinicRequestNotes_Get($patientRequestId: String!) { + notes: getClinicPatientRequestNotes(requestId: $patientRequestId) { + id + content + } +} +""" + +MUTATION_UPDATE_NOTE = r""" +mutation ClinicRequestNotes_Update($noteInput: UpdateClinicPatientRequestNoteInput!) { + updateClinicPatientRequestNote(noteInput: $noteInput) { + id + } +} +""" + +MUTATION_CREATE_NOTE = r""" +mutation ClinicRequestNotes_Create($noteInput: CreateClinicPatientRequestNoteInput!) { + createClinicPatientRequestNote(noteInput: $noteInput) { + id + } +} +""" + + +def gql(query, variables, token): + """Obecná funkce pro volání GraphQL endpointu.""" + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + payload = {"query": query, "variables": variables} + + r = requests.post(GRAPHQL_URL, json=payload, headers=headers) + r.raise_for_status() + return r.json() + + +def get_internal_note(request_id, token): + """Získá jedinou interní poznámku (obsah a ID) pro daný požadavek.""" + data = gql(QUERY_GET_NOTE, {"patientRequestId": request_id}, token) + notes = data.get("data", {}).get("notes", []) + return notes[0] if notes else None + + +def update_internal_note(note_id, new_content, token): + """Aktualizuje obsah poznámky v Medeviu.""" + variables = {"noteInput": {"id": note_id, "content": new_content}} + return gql(MUTATION_UPDATE_NOTE, variables, token) + + +def create_internal_note(request_id, content, token): + """Vytvoří novou interní poznámku k požadavku v Medeviu.""" + variables = {"noteInput": {"requestId": request_id, "content": content}} + return gql(MUTATION_CREATE_NOTE, variables, token) + + +# === MAIN === + +def main(): + token = read_token(TOKEN_PATH) + + # 1. Získat seznam ID požadavků ke zpracování (používáme PROCESS_LIMIT) + requests_to_process = get_requests_to_process_from_db(PROCESS_LIMIT) + + if not requests_to_process: + return + + # Pro update DB time otevřeme připojení jednou a použijeme ho v cyklu + conn = mysql.connector.connect(**DB_CONFIG) + + print("\n=============================================") + print(f"START ZPRACOVÁNÍ {len(requests_to_process)} POŽADAVKŮ") + print("=============================================\n") + + for idx, request in enumerate(requests_to_process, 1): + request_id = request["id"] + + print( + f"[{idx}/{len(requests_to_process)}] Zpracovávám požadavek: {request['prijmeni']} {request['jmeno']} (ID: {request_id})") + + # 2. Vytvořit text, který chceme přidat/vytvořit + prepend_text = f"ID: {request_id}\n" + + # 3. Pokusit se získat existující interní poznámku z Medevia + note = get_internal_note(request_id, token) + + medevio_update_success = False + + if note: + # A) POZNÁMKA EXISTUJE -> AKTUALIZOVAT + note_id = note["id"] + old_content = note["content"] or "" + new_content = prepend_text + old_content + + try: + # Odeslání aktualizace + update_internal_note(note_id, new_content, token) + print(f" (Medevio: Poznámka {note_id} **aktualizována**.)") + medevio_update_success = True + except requests.exceptions.HTTPError as e: + print(f" ❌ Chyba při aktualizaci Medevio API: {e}") + + else: + # B) POZNÁMKA NEEXISTUJE -> VYTVOŘIT + new_content = prepend_text.strip() + + try: + # Odeslání vytvoření + result = create_internal_note(request_id, new_content, token) + new_note_id = result.get("data", {}).get("createClinicPatientRequestNote", {}).get("id", "N/A") + print(f" (Medevio: Nová poznámka {new_note_id} **vytvořena**.)") + medevio_update_success = True + except requests.exceptions.HTTPError as e: + print(f" ❌ Chyba při vytváření Medevio API: {e}") + + # 4. AKTUALIZACE ČASOVÉHO RAZÍTKA V DB + if medevio_update_success: + update_db_sync_time(request_id, conn) + + print("---------------------------------------------") + + # Uzavřeme připojení k DB po dokončení cyklu + if conn and conn.is_connected(): + conn.close() + print("\n✅ Všechny požadavky zpracovány. Připojení k DB uzavřeno.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Medevio/40 agenda a požadavky/Report_AgendaPozadavky.py b/Medevio/40 agenda a požadavky/Report_AgendaPozadavky.py new file mode 100644 index 0000000..35fd17a --- /dev/null +++ b/Medevio/40 agenda a požadavky/Report_AgendaPozadavky.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Full Medevio Report: +- Agenda (API, next 30 days) +- Otevřené požadavky (MySQL) +- Merged (Agenda + Open, deduplicated) +- Vaccine sheets (from merged data) +""" + +import re +import json +import pymysql +import requests +import pandas as pd +from pathlib import Path +from datetime import datetime +from dateutil import parser, tz +from dateutil.relativedelta import relativedelta +from openpyxl import load_workbook +from openpyxl.styles import Font, Alignment, PatternFill, Border, Side +from openpyxl.utils import get_column_letter +from openpyxl.utils.dataframe import dataframe_to_rows + +# ==================== CONFIG ==================== +GRAPHQL_URL = "https://api.medevio.cz/graphql" +CALENDAR_ID = "144c4e12-347c-49ca-9ec0-8ca965a4470d" +CLINIC_SLUG = "mudr-buzalkova" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +EXPORT_DIR = Path(r"u:\Dropbox\Ordinace\Reporty") +EXPORT_DIR.mkdir(exist_ok=True, parents=True) + +# Delete previous reports +for old in EXPORT_DIR.glob("* Agenda + Požadavky.xlsx"): + old.unlink() + print(f"🗑️ Deleted old report: {old.name}") + +timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S") +xlsx_path = EXPORT_DIR / f"{timestamp} Agenda + Požadavky.xlsx" + +# ==================== LOAD TOKEN ==================== +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" +gateway_token = TOKEN_PATH.read_text(encoding="utf-8").strip() + +headers = { + "content-type": "application/json", + "authorization": f"Bearer {gateway_token}", + "origin": "https://my.medevio.cz", + "referer": "https://my.medevio.cz/", +} + +# ==================== STYLING ==================== +widths = {1: 11, 2: 13, 3: 45, 4: 30, 5: 15, 6: 15, 7: 30, 8: 15, 9: 37, 10: 37} +header_fill = PatternFill("solid", fgColor="FFFF00") +alt_fill = PatternFill("solid", fgColor="F2F2F2") +thin_border = Border( + left=Side(style="thin", color="000000"), + right=Side(style="thin", color="000000"), + top=Side(style="thin", color="000000"), + bottom=Side(style="thin", color="000000"), +) + + +REQUEST_URL_TEMPLATE = "https://my.medevio.cz/mudr-buzalkova/klinika/pozadavky?pozadavek={}" +link_font = Font(color="0563C1", underline="single") + + +def format_ws(ws, df): + """Apply unified formatting to a worksheet.""" + # Find Request_ID column index (1-based) + req_id_col = None + columns = list(df.columns) + if "Request_ID" in columns: + req_id_col = columns.index("Request_ID") + 1 + + for col_idx in range(1, len(df.columns) + 1): + col_letter = get_column_letter(col_idx) + cell = ws.cell(row=1, column=col_idx) + cell.font = Font(bold=True) + cell.alignment = Alignment(horizontal="center", vertical="center") + cell.fill = header_fill + cell.value = str(cell.value).upper() + cell.border = thin_border + ws.column_dimensions[col_letter].width = widths.get(col_idx, 20) + for r_idx, row in enumerate(ws.iter_rows(min_row=2, max_row=ws.max_row), start=2): + for cell in row: + cell.border = thin_border + if r_idx % 2 == 0: + cell.fill = alt_fill + # Add hyperlink to Request_ID cells + if req_id_col and cell.column == req_id_col and cell.value: + cell.hyperlink = REQUEST_URL_TEMPLATE.format(cell.value) + cell.font = link_font + ws.freeze_panes = "A2" + ws.auto_filter.ref = ws.dimensions + + +# ==================== 1️⃣ LOAD AGENDA (API) ==================== +print("📡 Querying Medevio API for agenda...") + +dnes = datetime.utcnow().date() +since = datetime.combine(dnes, datetime.min.time()) +until = since + relativedelta(months=1) + +payload = { + "operationName": "ClinicAgenda_ListClinicReservations", + "variables": { + "calendarIds": [CALENDAR_ID], + "clinicSlug": CLINIC_SLUG, + "since": since.isoformat() + "Z", + "until": until.isoformat() + "Z", + "locale": "cs", + "emptyCalendarIds": False, + }, + "query": """query ClinicAgenda_ListClinicReservations( + $calendarIds: [UUID!], $clinicSlug: String!, + $locale: Locale!, $since: DateTime!, $until: DateTime!, + $emptyCalendarIds: Boolean! + ) { + reservations: listClinicReservations( + clinicSlug: $clinicSlug, calendarIds: $calendarIds, + since: $since, until: $until + ) @skip(if: $emptyCalendarIds) { + id start end note done color + request { + id displayTitle(locale: $locale) + extendedPatient { + name surname dob insuranceCompanyObject { shortName } + } + } + } + }""", +} + +r = requests.post(GRAPHQL_URL, headers=headers, data=json.dumps(payload)) +r.raise_for_status() +resp = r.json() +if "errors" in resp or "data" not in resp: + print("❌ API response:") + print(json.dumps(resp, indent=2, ensure_ascii=False)) + raise SystemExit("API call failed - check token or query.") +reservations = resp["data"]["reservations"] + +rows = [] +for r in reservations: + req = r.get("request") or {} + patient = req.get("extendedPatient") or {} + insurance = patient.get("insuranceCompanyObject") or {} + try: + start_dt = parser.isoparse(r.get("start")).astimezone(tz.gettz("Europe/Prague")) + end_dt = parser.isoparse(r.get("end")).astimezone(tz.gettz("Europe/Prague")) + except Exception: + start_dt = end_dt = None + date_str = start_dt.strftime("%Y-%m-%d") if start_dt else "" + time_interval = ( + f"{start_dt.strftime('%H:%M')}-{end_dt.strftime('%H:%M')}" + if start_dt and end_dt + else "" + ) + rows.append( + { + "Date": date_str, + "Time": time_interval, + "Title": req.get("displayTitle") or "", + "Patient": f"{patient.get('surname','')} {patient.get('name','')}".strip(), + "DOB": patient.get("dob") or "", + "Insurance": insurance.get("shortName") or "", + "Note": r.get("note") or "", + "Color": r.get("color") or "", + "Request_ID": req.get("id") or "", + "Reservation_ID": r.get("id"), + } + ) + +df_agenda = pd.DataFrame(rows).sort_values(["Date", "Time"]) +print(f"✅ Loaded {len(df_agenda)} agenda rows.") + + +# ==================== 2️⃣ LOAD OPEN REQUESTS (MySQL) ==================== +print("📡 Loading open requests from MySQL...") +conn = pymysql.connect(**DB_CONFIG) +with conn.cursor() as cur: + cur.execute( + """ + SELECT + id AS Request_ID, + displayTitle AS Title, + pacient_prijmeni AS Pacient_Prijmeni, + pacient_jmeno AS Pacient_Jmeno, + pacient_rodnecislo AS DOB, + createdAt AS Created + FROM pozadavky + WHERE doneAt IS NULL AND removedAt IS NULL + ORDER BY createdAt DESC + """ + ) + rows = cur.fetchall() +conn.close() + +df_open = pd.DataFrame(rows) +if not df_open.empty: + df_open["Patient"] = ( + df_open["Pacient_Prijmeni"].fillna("") + + " " + + df_open["Pacient_Jmeno"].fillna("") + ).str.strip() + df_open["Date"] = df_open["Created"].astype(str).str[:10] + df_open["Time"] = "" + df_open["Insurance"] = "" + df_open["Note"] = "" + df_open["Color"] = "" + df_open["Reservation_ID"] = "" + df_open = df_open[ + [ + "Date", + "Time", + "Title", + "Patient", + "DOB", + "Insurance", + "Note", + "Color", + "Request_ID", + "Reservation_ID", + ] + ] +print(f"✅ Loaded {len(df_open)} open requests.") + + +# ==================== 3️⃣ MERGE + DEDUPLICATE ==================== +print("🟢 Merging and deduplicating (Agenda preferred)...") + +df_agenda["Source"] = "Agenda" +df_open["Source"] = "Open" + +df_merged = pd.concat([df_agenda, df_open], ignore_index=True).fillna("") +df_merged = df_merged.sort_values(["Source"], ascending=[True]) + +# drop duplicates — prefer Agenda if same Request_ID or same (Patient+Title) +df_merged = df_merged.drop_duplicates( + subset=["Request_ID", "Patient", "Title"], keep="first" +) + +df_merged = df_merged.drop(columns=["Source"], errors="ignore") +df_merged = df_merged.sort_values(["Date", "Time"], na_position="last").reset_index( + drop=True +) +print(f"✅ Total merged rows after deduplication: {len(df_merged)}") + + +# ==================== 4️⃣ WRITE BASE SHEETS ==================== +with pd.ExcelWriter(xlsx_path, engine="openpyxl") as writer: + df_agenda.to_excel(writer, sheet_name="Agenda", index=False) + df_open.to_excel(writer, sheet_name="Otevřené požadavky", index=False) + df_merged.to_excel(writer, sheet_name="Merged", index=False) + +wb = load_workbook(xlsx_path) +for name, df_ref in [ + ("Agenda", df_agenda), + ("Otevřené požadavky", df_open), + ("Merged", df_merged), +]: + ws = wb[name] + format_ws(ws, df_ref) + + +# ==================== 5️⃣ VACCINE SHEETS (from MERGED) ==================== +VACCINE_SHEETS = { + "Chřipka": ["očkování", "chřipka"], + "COVID": ["očkování", "covid"], + "Pneumokok": ["očkování", "pneumo"], + "Hep A": ["očkování", "žloutenka a"], + "Hep B": ["očkování", "žloutenka b"], + "Hep A+B": ["očkování", "žloutenka a+b"], + "Klíšťovka": ["očkování", "klíšť"], +} + + +def kw_pattern(kw): + return rf"(? '{new_name}'") + folder.rename(new_folder_path) + except Exception as e: + print(f"Could not rename {original_name}: {e}") + + +if __name__ == "__main__": + rename_folders() \ No newline at end of file diff --git a/Medevio/50 Různé testy/MinimizeOptimizePDF/compress_pdf.py b/Medevio/50 Různé testy/MinimizeOptimizePDF/compress_pdf.py new file mode 100644 index 0000000..18b0a88 --- /dev/null +++ b/Medevio/50 Různé testy/MinimizeOptimizePDF/compress_pdf.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Compress PDF — output DPI and JPEG quality are chosen automatically +based on the detected resolution of the source PDF. + +Usage: python compress_pdf.py [output.pdf] + python compress_pdf.py (processes all PDFs in current folder) +Output filename: original_name (139 kB).pdf +""" + +import sys +import fitz +from pathlib import Path + +# ============================== +# COMPRESSION TABLE +# Detected source DPI -> (output DPI, JPEG quality) +# Rows are evaluated top-to-bottom; first match wins. +# ============================== +# +# src_dpi_min src_dpi_max out_dpi jpeg_quality +COMPRESSION_TABLE = [ + ( 0, 99, 72, 60), # very low res — already small, compress hard + ( 100, 149, 100, 70), # low res + ( 150, 249, 150, 80), # standard scan (our tested sweet spot) + ( 250, 399, 150, 80), # good scan — downsample to 150 is fine + ( 400, 599, 200, 85), # high res scan + ( 600, 9999, 150, 80), # very high res / professional scan +] + + +def detect_source_dpi(src: fitz.Document) -> int: + """Estimate source DPI from the largest image on the first page.""" + page = src[0] + images = page.get_images(full=True) + if not images: + return 150 # no raster images — use default + + # Find the largest image by pixel area + best = max(images, key=lambda img: img[2] * img[3]) # width * height + img_w_px, img_h_px = best[2], best[3] + + # Page size in inches (1 point = 1/72 inch) + page_w_in = page.rect.width / 72.0 + page_h_in = page.rect.height / 72.0 + + dpi_x = img_w_px / page_w_in if page_w_in else 0 + dpi_y = img_h_px / page_h_in if page_h_in else 0 + return round((dpi_x + dpi_y) / 2) + + +def pick_settings(source_dpi: int) -> tuple[int, int]: + for min_dpi, max_dpi, out_dpi, quality in COMPRESSION_TABLE: + if min_dpi <= source_dpi <= max_dpi: + return out_dpi, quality + # fallback to last row + return COMPRESSION_TABLE[-1][2], COMPRESSION_TABLE[-1][3] + + +def compress(input_path: Path, output_path: Path = None): + src = fitz.open(input_path) + + source_dpi = detect_source_dpi(src) + out_dpi, jpeg_quality = pick_settings(source_dpi) + + print(f" zdroj ~{source_dpi} DPI -> komprese {out_dpi} DPI / JPEG q{jpeg_quality}") + + zoom = out_dpi / 72.0 + mat = fitz.Matrix(zoom, zoom) + + out_doc = fitz.open() + for page in src: + pix = page.get_pixmap(matrix=mat, colorspace=fitz.csRGB) + img_bytes = pix.tobytes("jpeg", jpg_quality=jpeg_quality) + img_doc = fitz.open("pdf", fitz.open("jpeg", img_bytes).convert_to_pdf()) + rect = page.rect + new_page = out_doc.new_page(width=rect.width, height=rect.height) + new_page.show_pdf_page(new_page.rect, img_doc, 0) + src.close() + + tmp = input_path.with_suffix(".tmp.pdf") + out_doc.save(tmp, deflate=True, garbage=4) + out_doc.close() + + size_kb = round(tmp.stat().st_size / 1024) + + if output_path is None: + output_path = input_path.parent / f"{input_path.stem} ({size_kb} kB).pdf" + + if output_path.exists(): + output_path.unlink() + tmp.rename(output_path) + + orig_kb = round(input_path.stat().st_size / 1024) + saving = (1 - size_kb / orig_kb) * 100 + print(f" {input_path.name} -> {output_path.name} (bylo {orig_kb} kB, uspora {saving:.0f}%)") + + +if __name__ == "__main__": + if len(sys.argv) >= 2: + inp = Path(sys.argv[1]) + out = Path(sys.argv[2]) if len(sys.argv) >= 3 else None + compress(inp, out) + else: + folder = Path(__file__).parent + pdfs = [p for p in folder.glob("*.pdf") if not p.name.endswith(").pdf") and p.stem != Path(__file__).stem] + if not pdfs: + print("Zadne PDF k zpracovani.") + for pdf in pdfs: + compress(pdf) diff --git a/Medevio/50 Různé testy/MinimizeOptimizePDF/compress_variants.py b/Medevio/50 Různé testy/MinimizeOptimizePDF/compress_variants.py new file mode 100644 index 0000000..227092a --- /dev/null +++ b/Medevio/50 Různé testy/MinimizeOptimizePDF/compress_variants.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Compress a PDF into multiple variants at different DPI / JPEG quality settings. +Uses PyMuPDF (fitz) — renders each page as JPEG image, saves back as PDF. +""" + +import sys +import fitz # PyMuPDF +from pathlib import Path + +INPUT = Path(r"u:\Medevio\50 Různé testy\MinimizeOptimizePDF\afd1823b-8277-44a2-84e1-db89a0ccd134.pdf") +OUT_DIR = INPUT.parent + +VARIANTS = [ + # (label, dpi, jpeg_quality) + ("300dpi_q90", 300, 90), + ("200dpi_q85", 200, 85), + ("150dpi_q80", 150, 80), + ("120dpi_q75", 120, 75), + ("96dpi_q70", 96, 70), + ("72dpi_q60", 72, 60), +] + +src = fitz.open(INPUT) +original_size = INPUT.stat().st_size +print(f"Originál: {INPUT.name} ({original_size / 1024:.0f} KB)\n") +print(f"{'Varianta':<20} {'DPI':>5} {'Kvalita':>8} {'Velikost':>12} {'Úspora':>8}") +print("-" * 58) + +for label, dpi, quality in VARIANTS: + out_path = OUT_DIR / f"{INPUT.stem}_{label}.pdf" + zoom = dpi / 72.0 + mat = fitz.Matrix(zoom, zoom) + + out_doc = fitz.open() + for page in src: + pix = page.get_pixmap(matrix=mat, colorspace=fitz.csRGB) + img_bytes = pix.tobytes("jpeg", jpg_quality=quality) + + # Create a new PDF page with the same physical dimensions + img_doc = fitz.open("pdf", fitz.open("jpeg", img_bytes).convert_to_pdf()) + # Scale page back to original size + rect = page.rect + new_page = out_doc.new_page(width=rect.width, height=rect.height) + new_page.show_pdf_page(new_page.rect, img_doc, 0) + + out_doc.save(out_path, deflate=True, garbage=4) + out_doc.close() + + size = out_path.stat().st_size + size_kb = round(size / 1024) + final_path = OUT_DIR / f"{INPUT.stem}_{label} ({size_kb} kB).pdf" + out_path.rename(final_path) + + saving = (1 - size / original_size) * 100 + print(f"{label:<20} {dpi:>5} {quality:>8} {size_kb:>9} kB {saving:>7.0f}%") + +src.close() +print("\nHotovo.") diff --git a/Medevio/60 ScansProcessing/CLAUDE.md b/Medevio/60 ScansProcessing/CLAUDE.md new file mode 100644 index 0000000..7ae0874 --- /dev/null +++ b/Medevio/60 ScansProcessing/CLAUDE.md @@ -0,0 +1,46 @@ +# 60 ScansProcessing + +Agent pro zpracování naskenovaných lékařských zpráv (PDF i JPG/PNG). + +## Skripty + +### `extract_patient_info.py` — hlavní agent +Spuštění: `python extract_patient_info.py` (bez argumentů = celá složka ToProcess) + +**Workflow:** +1. Načte soubory z `ToProcess/` +2. Claude Vision API (sonnet-4-6) extrahuje: jméno, RČ, datum, typ dokumentu, poznámku, navržený název, rotaci +3. Ověří pacienta v Medicus Firebird (tabulka KAR, pole RODCIS/PRIJMENI/JMENO) +4. Fuzzy matching RČ při nenalezení: vynechání cifry + záměna podobných (0↔8, 1↔7, 5↔6, 3↔8) + checksum /11 +5. Upozorní na duplicitu v `U:\Dropbox\Ordinace\Dokumentace_zpracovaná\` +6. Interaktivní schválení / oprava názvu +7. JPG/PNG → skutečné PDF (správná orientace, DPI=150, quality=80) +8. Přesun do `Processed/`, smazání z `ToProcess/` +9. Opravy názvů se ukládají do `corrections.json` jako few-shot příklady + +**Formát názvu souboru:** +`{RČ} {YYYY-MM-DD} {Příjmení}, {Jméno} [{typ dokumentu}] [{poznámka}].pdf` + +Příklady typů: `LZ chirurgie`, `LZ kardiologie`, `Laboratoř`, `CT břicha`, `kolonoskopie`, `poukaz FT` + +### `jpg_to_pdf.py` — konverze obrázku na PDF +``` +python jpg_to_pdf.py soubor.jpg [vystup.pdf] [rotace_ccw] +``` +- Opravuje EXIF orientaci +- Rotace: 0 / 90 / 180 / 270 (CCW) +- A4, DPI=150, quality=80, bez okrajů +- Používá se i interně z `extract_patient_info.py` + +## Složky + +| Složka | Účel | +|---|---| +| `ToProcess/` | Sem se házejí nové skeny (PDF, JPG, PNG) | +| `Processed/` | Správně pojmenované PDF po schválení | +| `U:\Dropbox\Ordinace\Dokumentace_zpracovaná\` | Finální archiv | + +## Konfigurace +- API klíč: `U:\Medevio\.env` → `ANTHROPIC_API_KEY` +- Medicus: `localhost:c:\medicus 3\data\medicus.fdb` (Firebird, SYSDBA) +- Few-shot korekce: `corrections.json` diff --git a/Medevio/60 ScansProcessing/Testy/extract_patient_info.py b/Medevio/60 ScansProcessing/Testy/extract_patient_info.py new file mode 100644 index 0000000..6867c17 --- /dev/null +++ b/Medevio/60 ScansProcessing/Testy/extract_patient_info.py @@ -0,0 +1,577 @@ +""" +Agent pro extrakci a pojmenování naskenovaných PDF lékařských zpráv. +- Claude Vision API — bez OCR, správná čeština s diakritikou +- Ověření pacienta proti Medicus (KAR), fuzzy matching RČ +- Interaktivní schválení / oprava názvu +- Few-shot learning z uložených korekcí +""" + +import base64 +import gc +import io +import json +import os +import re +import shutil +import subprocess +import sys +import time +from pathlib import Path + +# Windows: nastav stdout/stderr na UTF-8 +if sys.platform == "win32": + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") + +import anthropic +from pdf2image import convert_from_path + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from Knihovny.najdi_dropbox import get_dropbox_root +from Knihovny.najdi_medicus import get_medicus_config + +POPPLER_PATH = r"C:/Poppler/Library/bin" +CORRECTIONS_FILE = Path(__file__).parent / "corrections.json" +_DROPBOX = Path(get_dropbox_root()) +TO_PROCESS = _DROPBOX / r"Ordinace\Dokumentace_ke_zpracování\Ricoh Fi-8040\KeZpracování" +PROCESSED = _DROPBOX / r"Ordinace\Dokumentace_ke_zpracování\Ricoh Fi-8040\Zpracováno" +DOKUMENTACE = _DROPBOX / r"Ordinace\Dokumentace_zpracovaná" + + +# ─── Konfigurace ────────────────────────────────────────────────────────────── + +def _load_env(): + env_path = Path(__file__).parent.parent / ".env" + if env_path.exists(): + for line in env_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + os.environ[k.strip()] = v.strip() + +_load_env() + + +# ─── Korekce (few-shot příklady) ────────────────────────────────────────────── + +def load_corrections() -> list[dict]: + if CORRECTIONS_FILE.exists(): + return json.loads(CORRECTIONS_FILE.read_text(encoding="utf-8")) + return [] + +def save_correction(original: str, corrected: str): + corrections = load_corrections() + for c in corrections: + if c["original"] == original and c["corrected"] == corrected: + return + corrections.append({"original": original, "corrected": corrected}) + CORRECTIONS_FILE.write_text( + json.dumps(corrections, ensure_ascii=False, indent=2), encoding="utf-8" + ) + print(f" ✓ Korekce uložena ({len(corrections)} celkem)") + +def build_corrections_prompt() -> str: + corrections = load_corrections() + if not corrections: + return "" + lines = ["Příklady korekcí z minulých běhů (uč se z nich):"] + for c in corrections[-10:]: + lines.append(f' - špatně: "{c["original"]}"') + lines.append(f' správně: "{c["corrected"]}"') + return "\n".join(lines) + "\n\n" + + +# ─── Kontrola duplicit ─────────────────────────────────────────────────────── + +def check_duplicates(rc: str, datum: str) -> list[str]: + """ + Hledá v Dokumentace_zpracovaná soubory se stejným RČ a datem. + Vrátí seznam názvů nalezených souborů. + """ + if not DOKUMENTACE.exists(): + return [] + prefix = f"{rc} {datum}" + return [f.name for f in DOKUMENTACE.iterdir() if f.name.startswith(prefix)] + + +# ─── Medicus ověření ────────────────────────────────────────────────────────── + +def _medicus_connect(): + try: + import fdb + cfg = get_medicus_config() + return fdb.connect( + dsn=cfg.dsn, + user="SYSDBA", password="masterkey", charset="win1250" + ) + except Exception as e: + print(f" [Medicus] Nepřipojeno: {e}") + return None + +def _lookup_by_rc(cur, rc_digits: str) -> dict | None: + """Přesné vyhledání podle RČ (bez lomítka).""" + cur.execute( + "SELECT IDPAC, PRIJMENI, JMENO, RODCIS FROM KAR " + "WHERE REPLACE(RODCIS, '/', '') = ?", + (rc_digits,) + ) + row = cur.fetchone() + if row: + return {"idpac": row[0], "prijmeni": row[1].strip(), "jmeno": row[2].strip(), "rodcis": row[3].strip()} + return None + +def _rc_candidates(rc: str) -> list[str]: + """ + Generuje kandidáty RČ pro fuzzy matching: + - vynechání každé cifry (OCR přečetlo znak navíc) + - vložení nuly na každou pozici (OCR přehlédlo nulu v sekvenci 00) + - záměna podobně vypadajících číslic na každé pozici + Vrátí unikátní seznam kandidátů bez původního RČ. + """ + similar = {"0": "8", "8": "0", "1": "7", "7": "1", "5": "6", "6": "5", "3": "8"} + candidates = set() + + # Vynechání jedné cifry (OCR přečetlo znak navíc) + for i in range(len(rc)): + candidates.add(rc[:i] + rc[i+1:]) + + # Vložení nuly na každou pozici (nejčastější chyba: sekvence 00 přečtena jako 0) + for i in range(len(rc) + 1): + candidates.add(rc[:i] + "0" + rc[i:]) + + # Záměna podobné cifry na každé pozici + for i, ch in enumerate(rc): + if ch in similar: + candidates.add(rc[:i] + similar[ch] + rc[i+1:]) + + candidates.discard(rc) + candidates = {c for c in candidates if len(c) in (9, 10)} + return sorted(candidates) + +def _rc_checksum_ok(rc: str) -> bool: + """Ověří dělitelnost 11 pro 10místná RČ (platí pro narozené po 1.1.1954).""" + digits = re.sub(r"\D", "", rc) + if len(digits) == 10: + return int(digits) % 11 == 0 + return True # 9místná RČ nemají checksum + +def verify_patient(rc_raw: str) -> dict: + """ + Ověří pacienta v Medicus. + Vrací: + status: "ok" | "fuzzy" | "not_found" | "offline" + patient: dict nebo None + rc_corrected: opravené RČ (pokud fuzzy) nebo None + """ + rc = re.sub(r"\D", "", rc_raw or "") + if not rc: + return {"status": "not_found", "patient": None, "rc_corrected": None} + + con = _medicus_connect() + if con is None: + return {"status": "offline", "patient": None, "rc_corrected": None} + + try: + cur = con.cursor() + + # 1. Přesná shoda + patient = _lookup_by_rc(cur, rc) + if patient: + return {"status": "ok", "patient": patient, "rc_corrected": None} + + # 2. Fuzzy matching — zkus kandidáty, preferuj ty s platným checksumem + candidates = _rc_candidates(rc) + matches = [] + for cand in candidates: + p = _lookup_by_rc(cur, cand) + if p: + matches.append((cand, p)) + + if not matches: + return {"status": "not_found", "patient": None, "rc_corrected": None} + + # Seřaď: platný checksum na prvním místě + matches.sort(key=lambda x: (0 if _rc_checksum_ok(x[0]) else 1)) + best_rc, best_patient = matches[0] + return {"status": "fuzzy", "patient": best_patient, "rc_corrected": best_rc, "all_matches": matches} + + finally: + con.close() + + +# ─── PDF → obrázek ──────────────────────────────────────────────────────────── + +def pdf_to_images(pdf_path: str) -> list: + return convert_from_path(pdf_path, poppler_path=POPPLER_PATH, dpi=300) + +def image_to_base64(image) -> str: + buf = io.BytesIO() + image.save(buf, format="JPEG", quality=95) + return base64.standard_b64encode(buf.getvalue()).decode("utf-8") + + +# ─── Extrakce Claude Vision ─────────────────────────────────────────────────── + +def extract_patient_info(pdf_path: str) -> dict: + pdf_path = Path(pdf_path) + if not pdf_path.exists(): + raise FileNotFoundError(f"Soubor nenalezen: {pdf_path}") + + print(f"\nNačítám: {pdf_path.name}") + suffix = pdf_path.suffix.lower() + if suffix in (".jpg", ".jpeg", ".png"): + from PIL import Image + img = Image.open(pdf_path) + image_b64 = image_to_base64(img) + img.close() + else: + images = pdf_to_images(str(pdf_path)) + image_b64 = image_to_base64(images[0]) + del images + gc.collect() + + prompt = ( + build_corrections_prompt() + + "Toto je naskenovaná lékařská zpráva v češtině. " + "Vrať JSON s těmito poli:\n" + "- \"jmeno\": celé jméno pacienta (příjmení + jméno + případný titul)\n" + "- \"rodne_cislo\": rodné číslo pacienta BEZ lomítka (pouze číslice)\n" + "- \"datum_zpravy\": datum zprávy ve formátu YYYY-MM-DD\n" + "- \"typ_dokumentu\": typ dokumentu — " + "\"LZ {oddělení}\" = ambulantní/lékařská zpráva (např. \"LZ chirurgie\", \"LZ kardiologie\", \"LZ plicní\", \"LZ ORL\"); " + "\"PZ {oddělení}\" = propouštěcí zpráva z hospitalizace (např. \"PZ interna\", \"PZ neurologie\"). " + "Jiné typy: \"Laboratoř\", \"CT břicha\", \"MRI páteře\", \"kolonoskopie\", " + "\"operační protokol oční\", \"poukaz FT\", \"diagnostická mamografie\" atd.\n" + "- \"poznamka\": krátká klinická poznámka česky, max 80 znaků. " + "DŮLEŽITÉ: pokud zpráva obsahuje sekci \"Závěr:\" nebo \"Závěr vyšetření:\", " + "použij VÝHRADNĚ obsah této sekce — je nejdůležitější. " + "Teprve pokud závěr chybí, shrň obsah z celé zprávy.\n" + "- \"nazev_souboru\": název souboru ve formátu " + "\"{rodne_cislo} {datum_zpravy} {Příjmení}, {Jméno} [{typ_dokumentu}] [{poznamka}].pdf\" " + "(jméno bez titulu, RČ bez lomítka)\n" + "- \"rotace\": o kolik stupňů CCW je třeba otočit obrázek aby byl text čitelně na výšku nebo šířku " + "(hodnoty: 0, 90, 180, 270). Pokud je text již správně orientovaný, vrať 0.\n\n" + "Pokud pole nenajdeš, použij null. Nepiš nic jiného než JSON." + ) + + print(" Volám Claude Vision API...") + client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY")) + response = client.messages.create( + model="claude-sonnet-4-6", + max_tokens=400, + messages=[{ + "role": "user", + "content": [ + {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_b64}}, + {"type": "text", "text": prompt}, + ], + }], + ) + + usage = response.usage + cost_input = usage.input_tokens * 3 / 1_000_000 + cost_output = usage.output_tokens * 15 / 1_000_000 + print(f" Tokeny: {usage.input_tokens} in + {usage.output_tokens} out = ${cost_input + cost_output:.4f}") + + raw = response.content[0].text.strip() + if raw.startswith("```"): + raw = raw.split("```")[1] + if raw.startswith("json"): + raw = raw[4:] + try: + return json.loads(raw.strip()) + except json.JSONDecodeError: + print(f" VAROVÁNÍ: nelze parsovat JSON: {raw!r}") + return {"nazev_souboru": None, "raw": raw} + + +# ─── Interaktivní schválení ─────────────────────────────────────────────────── + +def sanitize_filename(name: str) -> str: + return re.sub(r'[<>:"/\\|?*]', '', name) + + +def _open_preview(root, pdf_path: Path): + """Otevře náhledové okno PDF/obrázku jako Toplevel. Pracuje s temp kopií — žádné zamykání originálu.""" + import tkinter as tk + import tempfile + import shutil as _shutil + try: + from PIL import Image, ImageTk + import fitz + except ImportError: + return + + # Temp kopie — prohlížeč nikdy nesahá na originál + tmp = Path(tempfile.mktemp(suffix=pdf_path.suffix)) + _shutil.copy2(pdf_path, tmp) + + suffix = pdf_path.suffix.lower() + if suffix in (".jpg", ".jpeg", ".png"): + pil_pages = [Image.open(tmp)] + doc = None + else: + try: + doc = fitz.open(str(tmp)) + except Exception: + tmp.unlink(missing_ok=True) + return + pil_pages = [] + + def render(n) -> Image.Image: + if doc is not None: + page = doc[n] + zoom = min(700 / page.rect.width, (sh - 150) / page.rect.height) + pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom)) + return Image.frombytes("RGB", (pix.width, pix.height), pix.samples) + else: + img = pil_pages[0].copy() + img.thumbnail((700, sh - 150), Image.LANCZOS) + return img + + def on_close(): + try: + if doc: + doc.close() + except Exception: + pass + tmp.unlink(missing_ok=True) + win.destroy() + + page_count = len(doc) if doc else 1 + sh = root.winfo_screenheight() + current = [0] + photo_ref = [None] + + win = tk.Toplevel(root) + win.title(pdf_path.name) + win.attributes("-topmost", True) + win.resizable(False, False) + win.protocol("WM_DELETE_WINDOW", on_close) + + lbl_img = tk.Label(win) + lbl_img.pack() + + frame_nav = tk.Frame(win) + frame_nav.pack(pady=4) + + lbl_page = tk.Label(frame_nav, font=("Segoe UI", 9)) + lbl_page.pack(side="left", padx=10) + + def show(n): + current[0] = n + img = render(n) + photo_ref[0] = ImageTk.PhotoImage(img) + lbl_img.config(image=photo_ref[0]) + lbl_page.config(text=f"Strana {n + 1} / {page_count}") + btn_prev.config(state="normal" if n > 0 else "disabled") + btn_next.config(state="normal" if n < page_count - 1 else "disabled") + + btn_prev = tk.Button(frame_nav, text="◄ Předchozí", + command=lambda: show(current[0] - 1)) + btn_prev.pack(side="left") + btn_next = tk.Button(frame_nav, text="Další ►", + command=lambda: show(current[0] + 1)) + btn_next.pack(side="left") + + show(0) + + win.update_idletasks() + win.geometry(f"+0+0") + + +def _rename_dialog(nazev: str, info_lines: list[str]) -> str | None: + """ + Spustí rename_dialog.py jako subprocess — vyhneme se Tkinter konfliktům s PyCharm. + Vrátí finální název (s .pdf) nebo None = přeskočit. + """ + import tempfile + + data = {"nazev": nazev, "info_lines": info_lines} + tmp = Path(tempfile.mktemp(suffix=".json")) + tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") + + dialog_script = Path(__file__).parent / "rename_dialog.py" + try: + proc = subprocess.run( + [sys.executable, str(dialog_script), str(tmp)], + capture_output=True, text=True, encoding="utf-8", + ) + output = proc.stdout.strip() + if output: + return json.loads(output).get("value") + return None + finally: + tmp.unlink(missing_ok=True) + + +def print_verification(verif: dict, rc_from_scan: str): + """Vypíše výsledek ověření proti Medicus.""" + status = verif["status"] + patient = verif.get("patient") + + if status == "ok": + print(f" ✓ Medicus: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}") + elif status == "fuzzy": + rc_corr = verif["rc_corrected"] + print(f" ⚠ Medicus: RČ ze skenu '{rc_from_scan}' nenalezeno") + print(f" → Nalezen podobný pacient: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}") + print(f" → Pravděpodobná oprava RČ: {rc_from_scan} → {rc_corr} (OCR chyba)") + if len(verif.get("all_matches", [])) > 1: + print(f" → Další shody: {[m[0] for m in verif['all_matches'][1:]]}") + elif status == "not_found": + print(f" ✗ Medicus: RČ '{rc_from_scan}' nenalezeno ani při fuzzy hledání") + elif status == "offline": + print(f" — Medicus: nedostupný (offline), ověření přeskočeno") + + +def interactive_rename(pdf_path: Path, info: dict, verif: dict) -> bool: + """ + Otevře tkinter dialog pro schválení / opravu názvu. + Schválený soubor přesune do Processed/ a smaže z ToProcess/. + """ + rc = re.sub(r"\D", "", verif["patient"]["rodcis"] if verif.get("patient") else info.get("rodne_cislo") or "") + datum = info.get("datum_zpravy") or "" + duplicity = check_duplicates(rc, datum) + + # Oprava RČ při fuzzy matchi + nazev = info.get("nazev_souboru") + if verif["status"] == "fuzzy" and verif.get("rc_corrected") and nazev: + rc_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "") + nazev = nazev.replace(rc_scan, verif["rc_corrected"], 1) + print(f" → Název aktualizován s opraveným RČ") + + # Sestavení info řádků pro dialog + rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "") + status = verif["status"] + patient = verif.get("patient") + info_lines = [] + if status == "ok": + info_lines.append(f"✓ Medicus: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}") + elif status == "fuzzy": + info_lines.append(f"⚠ RČ ze skenu '{rc_from_scan}' → opraveno na {verif['rc_corrected']}") + info_lines.append(f" Pacient: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}") + elif status == "not_found": + info_lines.append(f"✗ RČ '{rc_from_scan}' nenalezeno v Medicus") + else: + info_lines.append("— Medicus nedostupný (offline)") + if duplicity: + info_lines.append(f"⚠ DUPLICITA: {', '.join(duplicity)}") + + print() + print("─" * 70) + if nazev: + print(f" Navržený název: {nazev}") + print(" Otevírám dialog...") + + odpoved = _rename_dialog(nazev or "", info_lines) + + if odpoved is None: + print(" Přeskočeno.") + return False + + if not odpoved.endswith(".pdf"): + odpoved += ".pdf" + final_name = sanitize_filename(odpoved) + + if nazev and nazev != final_name: + save_correction(nazev, final_name) + + if not final_name or final_name == ".pdf": + print(" Název je prázdný, přeskakuji.") + return False + + dest = PROCESSED / final_name + if dest.exists(): + print(f" VAROVÁNÍ: '{final_name}' již existuje v Processed, přeskakuji.") + return False + + if pdf_path.suffix.lower() in (".jpg", ".jpeg", ".png"): + from jpg_to_pdf import image_to_pdf + image_to_pdf(pdf_path, dest, rotate_ccw=info.get("rotace") or 0) + else: + shutil.copy2(pdf_path, dest) + + pdf_path.unlink() + print(f" ✓ Uloženo: Processed/{final_name}") + return True + + +# ─── Hlavní logika ──────────────────────────────────────────────────────────── + +def _start_preview_process(pdf_path: Path): + """ + Otevře náhled PDF jako samostatný subprocess (žádné tkinter threading problémy). + Pracuje s temp kopií — originál zůstane volný. + Vrátí funkci close() pro ukončení procesu. + """ + import tempfile + import shutil as _shutil + + tmp = Path(tempfile.mktemp(suffix=pdf_path.suffix)) + _shutil.copy2(pdf_path, tmp) + + viewer = Path(__file__).parent / "preview_viewer.py" + proc = subprocess.Popen( + [sys.executable, str(viewer), str(tmp), "--delete-on-close"], + ) + + def close(): + try: + proc.terminate() + proc.wait(timeout=3) + except Exception: + pass + try: + tmp.unlink(missing_ok=True) + except Exception: + pass + + return close + + +def process_file(pdf_path: Path): + close_preview = _start_preview_process(pdf_path) + try: + info = extract_patient_info(str(pdf_path)) + rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "") + print(f" Ověřuji v Medicus (RČ: {rc_from_scan})...") + verif = verify_patient(rc_from_scan) + print_verification(verif, rc_from_scan) + interactive_rename(pdf_path, info, verif) + finally: + close_preview() + +def process_folder(folder: Path): + pdf_files = sorted(f for f in folder.iterdir() + if f.suffix.lower() in (".pdf", ".jpg", ".jpeg", ".png")) + if not pdf_files: + print(f"Žádná PDF nenalezena v: {folder}") + return + + print(f"Nalezeno {len(pdf_files)} PDF soubor(ů).\n") + for pdf_file in pdf_files: + try: + process_file(pdf_file) + except Exception as e: + print(f" CHYBA: {e}") + + print("\nHotovo.") + + +if __name__ == "__main__": + if len(sys.argv) > 1: + target = Path(sys.argv[1]) + else: + target = TO_PROCESS + + PROCESSED.mkdir(exist_ok=True) + TO_PROCESS.mkdir(exist_ok=True) + + if target.is_file() and target.suffix.lower() in (".pdf", ".jpg", ".jpeg", ".png"): + process_file(target) + elif target.is_dir(): + process_folder(target) + else: + print("Použití: python extract_patient_info.py [soubor.pdf nebo složka]") + sys.exit(1) diff --git a/Medevio/60 ScansProcessing/affd3ab5-fa29-4e8c-8555-c1374d4d9cc8.jpeg b/Medevio/60 ScansProcessing/affd3ab5-fa29-4e8c-8555-c1374d4d9cc8.jpeg new file mode 100644 index 0000000..cf58b87 Binary files /dev/null and b/Medevio/60 ScansProcessing/affd3ab5-fa29-4e8c-8555-c1374d4d9cc8.jpeg differ diff --git a/Medevio/60 ScansProcessing/corrections.json b/Medevio/60 ScansProcessing/corrections.json new file mode 100644 index 0000000..2b2db19 --- /dev/null +++ b/Medevio/60 ScansProcessing/corrections.json @@ -0,0 +1,538 @@ +[ + { + "original": "505228025 2026-05-14 Titlbachová, Božena [Žádanka předoperační vyšetření GYNA] [Předop. vyšetření, dg. N890, malý výkon A, anestezie CA].pdf", + "corrected": "505228025 2026-05-14 Titlbachová, Božena [žádanka předoperační vyšetření] [gynekologie, dg. N890, malý výkon A, anestezie CA].pdf" + }, + { + "original": "6860241553 2026-02-12 Šímová, Helena [LZ neurologie] [VAS L páteře, iritačně zánikový radik sy L5/S1 vpravo, dg. M511].pdf", + "corrected": "6860241553 2026-02-12 Šímová, Helena [LZ neurologie] [VAS L páteře, po PRT pod CT, krásné zlepšení, iritačně zánikový radik sy L5/S1 vpravo, dg. M511].pdf" + }, + { + "original": "6860241553 2026-02-10 Šímová, Helena [denzitometrie] [osteopenie, L1-4 T-score -1,4, krček fem. l T-1,8, r T-2,3].pdf", + "corrected": "6860241553 2026-02-10 Šímová, Helena [DXA] [osteopenie, L1-4 T-score -1.4, krček fem. l T-1.8, r T-2.3].pdf" + }, + { + "original": "470629074 2026-03-31 Šebesta, Jaroslav [LZ kardiologie] [ECHO: EF 50%, hypokineza IVS a sp. stěny, dilatace LS, MR 1-2/4].pdf", + "corrected": "470629074 2026-03-31 Šebesta, Jaroslav [LZ kardiologie] [ECHO: EF 50%, hypokineza IVS a sp. stěny, dilatace LS, MR 1-2/4, indikace lázně II_3].pdf" + }, + { + "original": "505809020 2026-01-14 Šebestová, Zdenka [LZ ortopedie] [TEP kyčle l.sin., kontrola 6 týdnů, chůze 2FH, doporučení lázně].pdf", + "corrected": "505809020 2026-01-14 Šebestová, Zdenka [LZ ortopedie] [TEP kyčle l.sin., kontrola 6 týdnů, chůze 2FH, indikace lázně VII_10].pdf" + }, + { + "original": "505809020 2025-12-10 Šebestová, Zdenka [LZ ortopedie] [Fct. colli femor. l.sin., TEP kyčle l.sin., propuštění na RHB].pdf", + "corrected": "505809020 2025-12-10 Šebestová, Zdenka [PZ ortopedie] [29NOV-10DEC2025 Fct. colli femor. l.sin., TEP kyčle l.sin., propuštění na RHB].pdf" + }, + { + "original": "7308100426 2026-04-15 Janda, Martin [Laboratoř] [hypercholesterolemie, S_Urea 9,18, glukóza 5,9, moč erytrocyty 6/ul].pdf", + "corrected": "7308100426 2026-04-15 Janda, Martin [Laboratoř] [Z000 hypercholesterolemie, S_Urea 9,18, glukóza 5,9, moč erytrocyty 6ul].pdf" + }, + { + "original": "7454230454 2026-04-15 Zíková, Jana [Laboratoř] [moč kultivace negativní, dg. N309].pdf", + "corrected": "7454230454 2026-04-15 Zíková, Jana [Laboratoř] [N309 moč kultivace negativní].pdf" + }, + { + "original": "0460142969 2026-04-15 Strnadová, Natálie [Laboratoř] [E660 koagulace: PT ratio 1,09, INR 1,10, aPTT 1,11, Fibrinogen 3,01].pdf", + "corrected": "0460142969 2026-04-15 Strnadová, Natálie [Laboratoř] [E660 koagulace PT ratio 1.09, INR 1.10, aPTT 1.11, Fibrinogen 3.01, parametry koagulace normální].pdf" + }, + { + "original": "5855280013 2026-04-15 Holubová, Daniela [Laboratoř] [Z000 CKD G2, DM, P_Glukóza 6.8, HbA1c 44, TG 1.77, leukocyty 11.9, moč bakterie].pdf", + "corrected": "5855280013 2026-04-15 Holubová, Daniela [Laboratoř] [Z000 CKD G2, DM (prediabetes), P_Glukóza 6.8, HbA1c 44, TG 1.77, leukocyty 11.9, moč bakterie záplava].pdf" + }, + { + "original": "5954110184 2026-04-15 Holečková, Hana [Laboratoř] [E119 CKD G2, P_Glukóza 6.7, HbA1c 44, bili 28.13, moč hlen].pdf", + "corrected": "5954110184 2026-04-15 Holečková, Hana [Laboratoř] [E119 CKD G2, P_Glukóza 6.7 (prediabetes), HbA1c 44, bili 28.13, moč hlen].pdf" + }, + { + "original": "7556220452 2026-04-09 Štěpánová, Lenka [Laboratoř] [K20 Anti HAV IgM+total pozit, CKD G2, bili konj 5.58].pdf", + "corrected": "7556220452 2026-04-09 Štěpánová, Lenka [Laboratoř] [K20 Anti HAV IgM+, total pozit, postoèkovací protilátky, CKD G2, bili konj 5.58].pdf" + }, + { + "original": "5862236435 2026-04-14 Kopřivová, Erika [Laboratoř] [Z000 CKD G2, hypercholesterol, TG 2.16, HbA1c 50, ALT 0.80, moč leukocyty 75ul].pdf", + "corrected": "5862236435 2026-04-14 Kopřivová, Erika [Laboratoř] [Z000 CKD G2, smíšená hypercholesterolémie, TG 2.16, HbA1c 50 (prediabetes), ALT 0.80, moč leukocyty 75ul].pdf" + }, + { + "original": "6258130637 2026-04-14 Hofmannová, Oldřiška [Laboratoř] [D500 CKD G2, cholesterol 6.38, LDL 4.37, glukóza 7.1, moč bakterie záplava, nitrity poz].pdf", + "corrected": "6258130637 2026-04-14 Hofmannová, Oldřiška [Laboratoř] [D500 CKD G2, èistá hypercholesterolémie 6.38, LDL 4.37, glukóza 7.1 (DM), moč bakterie záplava, nitrity+].pdf" + }, + { + "original": "8256060021 2026-04-10 Karešová, Barbora [Laboratoř] [Z000 hypercholesterol 6.54, LDL 3.73, ALT 1.00, CKD G2, moč erytrocyty 11ul].pdf", + "corrected": "8256060021 2026-04-10 Karešová, Barbora [Laboratoř] [Z000 čistá hypercholesterolémie 6.54, LDL 3.73, ALT 1.00, CKD G2, moč erytrocyty 11ul].pdf" + }, + { + "original": "400424003 2026-04-07 Faměra, Jiří [Laboratoř] [I10 CKD G3b, anemie, S_Urea 16.45, kreatinin 155, glukóza 5.7].pdf", + "corrected": "400424003 2026-04-07 Faměra, Jiří [Laboratoř] [I10 CKD G3b, anemie 116, S_Urea 16.45, kreatinin 155, glukóza 5.7].pdf" + }, + { + "original": "0460142969 2026-04-08 Strnadová, Natálie [Laboratoř] [E660 S_Urea 2.35, AST 0.18, CKD G1, krevní obraz v normě].pdf", + "corrected": "0460142969 2026-04-08 Strnadová, Natálie [Laboratoř] [E660 krevní obraz v normě].pdf" + }, + { + "original": "6055052157 2026-04-08 Frýdlová, Jana [Laboratoř] [Z000 CKD G2, prediabetes HbA1c 43, GGT 6.48, cholesterol 5.69, moč leukocyty 42ul].pdf", + "corrected": "6055052157 2026-04-08 Frýdlová, Jana [Laboratoř] [Z000 CKD G2, prediabetes HbA1c 43, GGT 6.48 (susp. alkohol), čistá hypercholesterolémie 5.69, moč leukocyty 42ul].pdf" + }, + { + "original": "7160239911 2026-04-01 Čenanovičová Krkičov, Sanja [Laboratoř] [E789 čistá hypercholesterolémie 6.68, LDL 4.76, Non-HDL 5.0].pdf", + "corrected": "7160239911 2026-04-01 Čenanovičová Krkičov, Sanja [Laboratoř] [E789 čistá hypercholesterolémie 6.68, LDL 4.76, Non-HDL 5.0, CK v pořádku].pdf" + }, + { + "original": "6212231861 2026-04-01 Novotný, Vladimír [Laboratoř] [D50 sideropenní anemie 105, Fe 5.9, TIBC 88.2, trombocytóza 609, mikrocytóza].pdf", + "corrected": "6212231861 2026-04-01 Novotný, Vladimír [Laboratoř] [D50 sideropenická anémie 105, Fe 5.9, TIBC 88.2, trombocytóza 609 (známý stav), mikrocytóza].pdf" + }, + { + "original": "466103013 2026-04-01 Sixtová, Blanka [Laboratoř] [C188 CKD G3a, urea 12.32, kyselina močová 425, GGT 1.41, trombocytopenie 138].pdf", + "corrected": "466103013 2026-04-01 Sixtová, Blanka [Laboratoř] [C188 CKD G3a, urea 12.32, kyselina močová 425, GGT 1.41, trombocytopenie 138, železa je stále nedostatek].pdf" + }, + { + "original": "5601090550 2026-02-25 Psohlavec, Miroslav [Laboratoř] [D53 B12 134 (nízký), folát 7.90, albumin 41.3, celk. bílk. 78].pdf", + "corrected": "5601090550 2026-02-25 Psohlavec, Miroslav [Laboratoř] [D53 B12 134 (nízký), folát 7.90 (nízký), albumin 41.3 OK, celk. bílk. 78 OK].pdf" + }, + { + "original": "0061010422 2026-04-10 Brabcová, Barbora [Laboratoř] [Z000 foláty 9.4 (nízké), CKD G2, TSH 2.030, krevní obraz v normě].pdf", + "corrected": "0061010422 2026-04-10 Brabcová, Barbora [Laboratoř] [Z000 foláty 9.4 (nízké), B12 nízké, CKD G2, TSH 2.030, krevní obraz v normě].pdf" + }, + { + "original": "486020212 2025-03-27 Krausová, Anna [EGD] [K30 biliární duodenogastrický reflux, jinak přiměřený nález].pdf", + "corrected": "486020212 2025-03-27 Krausová, Anna [gastroskopie] [K30 biliární duodenogastrický reflux, jinak přiměřený nález].pdf" + }, + { + "original": "466225409 2026-03-09 Teršová, Eva [Laboratoř] [E118 CKD G3b, glukóza 10.5, HbA1c 34, C-peptid nízký, LDL 3.32, TG 1.52].pdf", + "corrected": "466225409 2026-03-09 Teršová, Eva [Laboratoř] [E118 CKD G3b, glukóza 10.5 (DM), HbA1c 34, C-peptid nízký, LDL 3.32, TG 1.52].pdf" + }, + { + "original": "476027162 2026-02-25 Buňková, Zuzana [LZ endokrinologie] [E063 kompenz. imunogenní hypothyreoza, uzlová přestavba, TSH 0.818].pdf", + "corrected": "476027162 2026-02-25 Buňková, Zuzana [LZ endokrinologie] [E063 kompenz. imunogenní hypothyreoza, uzlová přestavba, TSH 0.818, na substituci].pdf" + }, + { + "original": "385312025 2026-03-30 Aubrechtová, Iva [medikace] [Furon, Eliquis, Cordarone, Digoxin, Tezeo, Dilatrend, Betaserc, Xalacom].pdf", + "corrected": "385312025 2026-03-30 Aubrechtová, Iva [přehled užívané medikace] [od pacientky].pdf" + }, + { + "original": "480529193 2026-01-22 Klikorka, Václav [LZ kardiologie] [I482, EF LK 60%, konc. hypertrofie LK, diastol. dysfunkce I, Mi regurg. 2+].pdf", + "corrected": "480529193 2026-01-22 Klikorka, Václav [LZ kardiologie] [I482, EF LK 60%, konc. hypertrofie LK, diastol. dysfunkce I, Mi regurg. 2+, kontrola +6m].pdf" + }, + { + "original": "480529193 2025-09-04 Klikorka, Václav [LZ oční] [Z961 pseudofakie, ERM ok. dx., kontrola OCTA za 4M].pdf", + "corrected": "480529193 2025-09-04 Klikorka, Václav [LZ oční] [Z961 pseudofakie, ERM ok. dx., kontrola OCTA za 4m].pdf" + }, + { + "original": "6258130637 2026-02-23 Hofmannová, Oldřiška [LZ interní] [Z039 atypická bolest na hrudi, epigastralgie, vertigo, BNH].pdf", + "corrected": "6258130637 2026-02-23 Hofmannová, Oldřiška [LZ interní] [Z039 atypická bolest na hrudi, přivezla RZS, epigastralgie, vertigo, bez známek ICHS].pdf" + }, + { + "original": "436212054 2026-04-01 Těšitelová, Jana [LZ rehabilitační] [M159 st.p. TEP gen l.dx., TEP coxae l.sin., amputace PDK, inkontinence II.st.].pdf", + "corrected": "436212054 2026-04-01 Těšitelová, Jana [PZ Lázně Velichovky] [11MAR-01APR2026, indikace VII_8, M159 st.p. TEP gen l.dx., TEP coxae l.sin., amputace PDK, inkontinence II.st., všechno v lázních OK].pdf" + }, + { + "original": "6104260668 2026-04-09 Neuwirth, Richard [LZ neurologie] [R42 vertigo, etanol 1.19 g/l, CT mozku bez ak. změn, kongenit. nystagmus].pdf", + "corrected": "6104260668 2026-04-09 Neuwirth, Richard [LZ neurologie] [RZS, R42 vertigo, etanol 1.19 gl, CT mozku bez ak. změn, kongenit. nystagmus].pdf" + }, + { + "original": "5954110184 2015-02-10 Holečková, Hana [LZ radiační onkologie] [C50 l.dx., st.p. ablaci 2004, kompletní remise, MMG norm.].pdf", + "corrected": "5954110184 2015-02-10 Holečková, Hana [LZ radiační onkologie] [vyšetření pro posudkovou komisi, C50 l.dx., st.p. ablaci 2004, kompletní remise, MMG norm.].pdf" + }, + { + "original": "5954110184 2011-11-03 Holečková, Hana [LZ chirurgie] [K800 cholecystolithiasis, cholecystektomie laparoskopicky 31.10.2011].pdf", + "corrected": "5954110184 2011-11-03 Holečková, Hana [PZ chirurgie] [30OCT-03NOV2025, K800 cholecystolithiasis, cholecystektomie laparoskopicky 31.10.2011].pdf" + }, + { + "original": "5954110184 2023-09-12 Holečková, Hana [LZ endokrinologie] [eutyreoza, malá štítnice, stacion. uzel v PL benigní, BMI 26.7].pdf", + "corrected": "5954110184 2023-09-12 Holečková, Hana [LZ endokrinologie] [eutyreoza, malá štítnice, stacion. uzel v PL benigní, BMI 26.7, kontrola +1r].pdf" + }, + { + "original": "5954110184 2023-08-28 Holečková, Hana [LZ rehabilitační] [M7737 ostruha patní kosti, pes planus, ultrazvuk 10x].pdf", + "corrected": "5954110184 2023-08-28 Holečková, Hana [LZ rehabilitace] [M7737 ostruha patní kosti, pes planus, ultrazvuk 10x].pdf" + }, + { + "original": "285703963 2026-04-08 Bartáková, Hilde [Laboratoř] [biochemie, KO, Fe: mírná anémie, kreatinin↑, CKD G3b, B12↑].pdf", + "corrected": "285703963 2026-04-08 Bartáková, Hilde [Laboratoř] [biochemie, KO, Fe mírná anémie, kreatinin↑, CKD G3b, B12↑].pdf" + }, + { + "original": "495831175 2026-04-07 Kazdová, Daniela [Laboratoř] [biochemie, hepatitidy: ALT↑, GGT↑, ALP↑, anti HAV total pozitivní].pdf", + "corrected": "495831175 2026-04-07 Kazdová, Daniela [Laboratoř] [biochemie, hepatitidy ALT↑, GGT↑, ALP↑, anti HAV total pozitivní, antiHAV IgM negativní, anamnestické protilátky].pdf" + }, + { + "original": "5954110184 2024-04-23 Holečková, Hana [LZ radiační onkologie] [C50 l.dx., st.p. ablaci 2004, CR 20 let, MMG benigní, předání PL].pdf", + "corrected": "5954110184 2024-04-23 Holečková, Hana [LZ radiační onkologie] [C50 l.dx., st.p. ablaci 2004, CR 20 let, MMG benigní, předání PL, konec jejich dispenzarizace].pdf" + }, + { + "original": "6104260668 2026-04-08 Neuwirth, Richard [Laboratoř] [biochemie, moč: cholesterol↑, TG↑, HDL↓, glukóza↑, CKD G1 A1].pdf", + "corrected": "6104260668 2026-04-08 Neuwirth, Richard [Laboratoř] [biochemie, moč cholesterol↑, TG↑, HDL↓, glukóza↑, CKD G1 A1].pdf" + }, + { + "original": "475915002 2026-04-13 Protivová, Lidmila [Laboratoř] [biochemie, moč, KO cholesterol↑, TG↑, glukóza↑, CKD G2, leukocyty↓].pdf", + "corrected": "475915002 2026-04-13 Protivová, Lidmila [Laboratoř] [biochemie, moč, KO smíšená hyperlipidémie, cholesterol↑, TG↑, glukóza↑ (prediabetes), CKD G2, leukocyty↓].pdf" + }, + { + "original": "5954110184 2024-03-25 Holečková, Hana [EKG vyšetření] [sinusový rytmus 76/min, intermed poloha, fyziologický záznam].pdf", + "corrected": "5954110184 2024-03-25 Holečková, Hana [EKG] [sinusový rytmus 76min, intermed poloha, fyziologický záznam].pdf" + }, + { + "original": "5954110184 2020-03-10 Holečková, Hana [EKG] [sinusový rytmus 64/min, intermed poloha, fyziologický záznam].pdf", + "corrected": "5954110184 2020-03-10 Holečková, Hana [EKG] [sinusový rytmus 64min, intermed poloha, fyziologický záznam].pdf" + }, + { + "original": "null 2026-04-20 null [Laboratoř] [moč LEU+2 125 WBC/uL, ERY+3 200 RBC/uL, pH 6, SG 1.005].pdf", + "corrected": "475915054 Žabová, Vìra 2026-04-20 [uritex] [moč LEU+2 125 WBCuL, ERY+3 200 RBCuL, pH 6, SG 1.005].pdf" + }, + { + "original": "7409240399 2026-04-17 Bukvář, Martin [LZ ortopedie] [M7126 cystis politelais gensu, st.p. achilodyniam, punkce 14ml, Diop+Kort].pdf", + "corrected": "7409240399 2026-04-17 Bukvář, Martin [LZ ortopedie] [M7126 cystis politelais genu, st.p. achilodyniam, punkce 14ml, aplikace kortikoidu].pdf" + }, + { + "original": "515705039 2026-04-01 Cahová, Daniela [LZ neurologie] [G20 Parkinson, klidový třes LHK+LDK, wearing off, bolest pravého ramene, m.deltoideus].pdf", + "corrected": "515705039 2026-04-01 Cahová, Daniela [LZ neurologie] [G20 Parkinson, klidový třes LHK+LDK, wearing off, bolest pravého ramene, m.deltoideus, ad MRI].pdf" + }, + { + "original": "7862150351 2018-10-30 Braunspergerová, Eva [RTG LS páteře] [dextrokonvexní skolióza, lordóza, osteochondróza L5-S1, spina bifida oculta S1].pdf", + "corrected": "7862150351 2018-10-30 Braunspergerová, Eva [RTG LS páteře] [dextrokonvexní skolióza, lordóza, osteochondróza L5-S1, spina bifida oculta S1, pooperaèní svorky].pdf" + }, + { + "original": "7862150351 2008-09-23 Braunspergerová, Eva [PZ infekční] [hepatitida A, ikterus, BLR↑, HAV IgM poz., anti HCV neg., zlepšení].pdf", + "corrected": "7862150351 2008-09-23 Braunspergerová, Eva [PZ infekce] [19-23SEP2008, hepatitida A, ikterus, BLR↑, HAV IgM poz., anti HCV neg., zlepšení].pdf" + }, + { + "original": "5855280013 2023-06-12 Holubová, Daniela [LZ endokrinologie] [adenom l.nadledviny 12x17x12mm, DM2 PAD+inzulin, dyslipidémie].pdf", + "corrected": "5855280013 2023-06-12 Holubová, Daniela [LZ endokrinologie] [adenom l.nadledviny 12x17x12mm, DM2 PAD+inzulin, dyslipidémie, konzultace s výsledky].pdf" + }, + { + "original": "5855280013 2026-03-03 Holubová, Daniela [CT břicha] [kalykolitiáza l.ledviny, stacionární adenom l.nadledviny 10mm].pdf", + "corrected": "5855280013 2026-02-27 Holubová, Daniela [CT břicha] [kalykolitiáza l.ledviny, stacionární adenom l.nadledviny 10mm, vyšetøeno pro hematurii].pdf" + }, + { + "original": "7862150351 2024-06-12 Braunspergerová, Eva [EKG] [sinusový rytmus 60min, hraniční i.v. vedení, bez čerstvých změn].pdf", + "corrected": "7862150351 2024-06-12 Braunspergerová, Eva [EKG] [sinusový rytmus 60min, hraniční i.v. vedení, bez čerstvých změn, stacionární].pdf" + }, + { + "original": "7755260271 2026-04-16 Straková, Barbara [žádost o předání zdravotních informací] [převzetí do péče, žádost o zaslání zdravotní dokumentace].pdf", + "corrected": "7755260271 2026-04-16 Straková, Barbara [žádost o předání zdravotních informací] [převzetí do péče, žádost o zaslání zdravotní dokumentace, VeleòMedic s.r.o.].pdf" + }, + { + "original": "7602044780 2026-04-18 Suchý, Vladimír [PZ nefrologie] [SLE+APS, renální biopsie, 2.puls Endoxan, VRE/ESBL, defekt PDK].pdf", + "corrected": "7602044780 2026-04-18 Suchý, Vladimír [PZ nefrologie] [14-18APR2026, SLE+APS, renální biopsie, 2.puls Endoxan, VREESBL, defekt PDK].pdf" + }, + { + "original": "6709150613 2026-04-15 Rutrle, Petr [LZ ORL] [laryngitis chr., vestibul.sy vpravo, percepční porucha sluchu bil.].pdf", + "corrected": "6709150613 2026-04-15 Rutrle, Petr [LZ ORL] [laryngitis chr., vestibul.sy vpravo, percepční porucha sluchu bil., ad sono karotid, Rp Helicid, zakoupí Tanakan].pdf" + }, + { + "original": "460614110 null Galus, Karel [LZ nefrologie] [CHOPN III.st, DM2, ICHS, CKD-EPI 35ml/s/kor, incipientní dia nefropatie].pdf", + "corrected": "460614110 2026-04-09 Galus, Karel [LZ nefrologie] [CHOPN III.st, DM2, ICHS, CKD-EPI 35mlskor, incipientní dia nefropatie].pdf" + }, + { + "original": "435624102 2026-03-31 Hovorková, Eva [PZ ortopedie] [26-31MAR2026, gonartróza l.dx., TEP kolene, Zimmer Nexgen CR F/6/10].pdf", + "corrected": "435624102 2026-03-31 Hovorková, Eva [PZ ortopedie] [26-31MAR2026, gonartróza l.dx., TEP kolene].pdf" + }, + { + "original": "470629074 2026-04-21 Šebesta, Jaroslav [oznámení ZP správní řízení] [zahájení správního řízení, LRPéče indikace II/3 hypertenzní choroba II-III.st].pdf", + "corrected": "470629074 2026-04-21 Šebesta, Jaroslav [oznámení ZP správní řízení] [zahájení správního řízení, návrh lázně, indikace II3 hypertenzní choroba II-III.st].pdf" + }, + { + "original": "5503040026 2026-02-17 Koubek, Jiří [LZ kardiologie] [ECHO: EF 65%, konc.hypertrofie, diastol.dysfunkce I.st, Bevimlar 20mg].pdf", + "corrected": "5503040026 2026-02-17 Koubek, Jiří [LZ kardiologie] [ECHO EF 65%, konc.hypertrofie, diastol.dysfunkce I.st, Bevimlar 20mg].pdf" + }, + { + "original": "480529219 2026-04-17 Nytra, Vlastimil [Laboratoř] [osteomarkery, Ca, P, ALP, vit.D 67,1 snížen, PTH, Beta-CrossLaps].pdf", + "corrected": "480529219 2026-04-17 Nytra, Vlastimil [Laboratoř] [osteomarkery, Ca, P, ALP, vit.D 67.1 snížen, PTH, Beta-CrossLaps].pdf" + }, + { + "original": "435520110 2026-04-20 Nechodomová, Marie [sonografie břicha] [hypersekr.žaludku, lipomatoza pankreatu, steatoza jat., cholecystolithiaza, splenomegalie].pdf", + "corrected": "435520110 2026-04-20 Nechodomová, Marie [sonografie břicha] [zesílení stěny žaludku - dovyšetřit, hypersekr.žaludku, lipomatoza pankreatu, steatoza jat., cholecystolithiaza, splenomegalie].pdf" + }, + { + "original": "6903020080 2026-04-20 Novotný, Martin [Laboratoř] [cholesterol 5.54, LDL 3.25, TG 2.06, glukoza 6.1, HbA1c 38].pdf", + "corrected": "6903020080 2026-04-20 Novotný, Martin [Laboratoř] [smíšená hyperlipidémie, prediabetes, cholesterol 5.54, LDL 3.25, TG 2.06, glukoza 6.1, HbA1c 38].pdf" + }, + { + "original": "480529219 2026-04-17 Nytra, Vlastimil [Laboratoř] [ELFO bílkovin, bílkovina 69.0, albumin 0.581, gama-globuliny 0.125].pdf", + "corrected": "480529219 2026-04-17 Nytra, Vlastimil [Laboratoř] [ELFO bílkovin OK, bílkovina 69.0, albumin 0.581, gama-globuliny 0.125].pdf" + }, + { + "original": "5556046672 2026-04-07 Simionová, Lýdia [Laboratoř] [močový konkrement, whewellit 100%, 6x3mm, hnědý, bradavičnatý].pdf", + "corrected": "5556046672 2026-04-07 Simionová, Lýdia [Laboratoř] [močový konkrement analýza, whewellit 100%, 6x3mm, hnědý, bradavičnatý].pdf" + }, + { + "original": "510802325 2026-04-20 Simion, Vladimír [LZ chirurgie] [chronický vřed kůže, TMT amputace IV.+V.prstu PDK, defekt LDK 5x3.5cm].pdf", + "corrected": "510802325 2026-04-20 Simion, Vladimír [LZ chirurgie] [chronický vřed kůže, TMT amputace IV.+V.prstu PDK, defekt LDK 5x3.5cm, DP 3xt].pdf" + }, + { + "original": "436114002 2026-03-17 Petrovská, Eliška [LZ kardiologie] [fibrilace síní paroxysmální, sinus, st.p.kardioverzi, rivaroxaban].pdf", + "corrected": "436114002 2026-03-17 Petrovská, Eliška [LZ kardiologie] [fibrilace síní paroxysmální, sinus, st.p.kardioverzi, rivaroxaban, ad Holter EKG, bisoprolol vysadí].pdf" + }, + { + "original": "436114002 2026-03-14 Petrovská, Eliška [LZ interna] [fibrilace síní paroxysmální, kardioverze, sinusový rytmus, rivaroxaban].pdf", + "corrected": "436114002 2026-03-14 Petrovská, Eliška [LZ interna urgent] [fibrilace síní paroxysmální, kardioverze, sinusový rytmus, rivaroxaban].pdf" + }, + { + "original": "6008091738 2026-04-20 Nikitin, Petro [Laboratoř] [urea 9.47 zvýš, CKD-EPI G2, glukoza 6.6, osmolalita 296, MCV 81.5].pdf", + "corrected": "6008091738 2026-04-20 Nikitin, Petro [Laboratoř] [Z000 prediabetes, mikrocyty, urea 9.47 zvýš, CKD-EPI G2, glukoza 6.6, osmolalita 296, MCV 81.5].pdf" + }, + { + "original": "440802018 2026-04-20 Havelka, Miroslav [Laboratoř] [CKD-EPI G2, NT-proBNP 6128 zvýš, CRP 6.6, MCV 81.8, MCHC 314].pdf", + "corrected": "440802018 2026-04-20 Havelka, Miroslav [Laboratoř] [srdeční selhání, mikrocyty, CKD-EPI G2, NT-proBNP 6128 zvýš, CRP 6.6, MCV 81.8, MCHC 314].pdf" + }, + { + "original": "7857260422 2023-02-28 Jindrová, Kateřina [LZ ORL] [st.p. incizi inflam aterom P tváře - zhojeno, extirpace atheromu P tváře].pdf", + "corrected": "7857260422 2023-02-28 Jindrová, Kateřina [LZ ORL] [st.p. incizi inflam aterom P tváře - zhojeno, extirpace atheromu P tváře domluveno].pdf" + }, + { + "original": "7857260422 2021-05-06 Jindrová, Kateřina [LZ angiologie] [CVI II. st. dle CEAP C4, ortostáza, flebitida/flebotrombóza bilat. neprokázána].pdf", + "corrected": "7857260422 2021-05-06 Jindrová, Kateřina [LZ angiologie] [CVI II. st. dle CEAP C4, ortostáza, flebitidaflebotrombóza bilat. neprokázána].pdf" + }, + { + "original": "7857260422 2021-05-20 Jindrová, Kateřina [LZ neurologie] [VAS C-pá, porucha statodynamiky C úseku, tinitus auric. bilat., ad rehab].pdf", + "corrected": "7857260422 2021-05-20 Jindrová, Kateřina [LZ neurologie] [VAS Cp, porucha statodynamiky C úseku, tinitus auric. bilat., ad RHB].pdf" + }, + { + "original": "7857260422 2024-02-12 Jindrová, Kateřina [EKG] [sinusový rytmus 70/min, semivertik poloha, osa 55st, fyziol záznam].pdf", + "corrected": "7857260422 2024-02-12 Jindrová, Kateřina [EKG] [sinusový rytmus 70min, semivertik poloha, osa 55st, fyziol záznam].pdf" + }, + { + "original": "5958260660 2026-02-04 Masopustová, Ivana [LZ interna] [viroza 1/25, RTH, CRP norm, klacid, únava, kašel, dušnost, zlepšuje se].pdf", + "corrected": "5958260660 2026-02-04 Masopustová, Ivana [LZ interna] [viroza 125, RTH, CRP norm, klacid, únava, kašel, dušnost, zlepšuje se].pdf" + }, + { + "original": "5958260660 2026-03-03 Masopustová, Ivana [LZ kardiologie] [Benig. kom. extrasystolie, Art. hypertenze komp., HLP, thyreopathie disp. + OA].pdf", + "corrected": "5958260660 2026-03-03 Masopustová, Ivana [LZ kardiologie] [Benig. kom. extrasystolie, Art. hypertenze komp., HLP, thyreopathie disp. + OA, důvodu nerozumím].pdf" + }, + { + "original": "7857260422 2026-04-21 Jindrová, Kateřina [Laboratoř] [Z000, erytrocyty 5.27 zvýš, hemoglobin 161 zvýš, hematokrit 0.475 zvýš].pdf", + "corrected": "7857260422 2026-04-21 Jindrová, Kateřina [Laboratoř] [Z000, erytrocyty 5.27 zvýš, hemoglobin 161 zvýš, hematokrit 0.475 zvýš, nic zvláštního].pdf" + }, + { + "original": "7555270085 2026-04-20 Křížová, Lucie [Laboratoř] [CKD-EPI G2, hemoglobin 113 sníž, MCV 78 sníž, MCH 23.9 sníž, MCHC 307 sníž].pdf", + "corrected": "7555270085 2026-04-20 Křížová, Lucie [Laboratoř] [E119, CKD-EPI G2, hemoglobin 113 sníž, MCV 78 sníž, MCH 23.9 sníž, MCHC 307 sníž, sideropenická anémie].pdf" + }, + { + "original": "505218025 2026-04-14 Beznosková, Milena [LZ diabetologie] [DM 2.typu, zlepšení kompenzace, HbA1c 49 mmol/mol, léčba PAD a dieta].pdf", + "corrected": "505218025 2026-04-14 Beznosková, Milena [LZ diabetologie] [DM 2.typu, zlepšení kompenzace, HbA1c 49 mmolmol, léčba PAD a dieta].pdf" + }, + { + "original": "495524246 2026-03-30 Dusilová, Jana [LZ urologie] [RCC pT1G1 st.p. NE I26, restaging: bez obtíží, nál. přiměř., CT s kontrastem plán].pdf", + "corrected": "495524246 2026-03-30 Dusilová, Jana [LZ urologie] [RCC pT1G1 st.p. NE I26, restaging bez obtíží, nál. přiměř., CT s kontrastem plán].pdf" + }, + { + "original": "5452020420 2026-04-20 Uhlířová, Jana [RTG LS páteře] [anterolistéza L5 11mm gr.2, snížení disků L4-S1, spondylofyty L2-S1, spondyloartróza bilat.].pdf", + "corrected": "5452020420 2026-04-20 Uhlířová, Jana [RTG LSp] [revize nálezu, anterolistéza L5 11mm gr.2, snížení disků L4-S1, spondylofyty L2-S1, spondyloartróza bilat.].pdf" + }, + { + "original": "9451210054 2026-04-21 Bódisová, Barbara [LZ interna] [bolest Thp po zvedání břemene, DDimer slabě poz., EKG ektop. síňový rytmus].pdf", + "corrected": "9451210054 2026-04-21 Bódisová, Barbara [LZ interna] [bolest Thp po zvedání břemene, DDimer slabě poz., EKG ektop. síňový rytmus, není kardiologické].pdf" + }, + { + "original": "6761150341 2026-04-21 Písaříková, Helena [Laboratoř] [Z000, CKD-EPI G2, cholesterol 5.76 zvýš, Non-HDL 3.9 zvýš, glukóza 5.7 zvýš].pdf", + "corrected": "6761150341 2026-04-21 Písaříková, Helena [Laboratoř] [Z000, CKD-EPI G2, cholesterol 5.76 zvýš, Non-HDL 3.9 zvýš, glukóza 5.7 zvýš, prediabetes, hypercholesterolémie].pdf" + }, + { + "original": "5558270113 2026-03-03 Knejslíková, Alena [LZ radiační onkologie] [pokračuje v CHRT, sliznice G0, urogen G0, dolní GIT G0, další CHT 12.3.26].pdf", + "corrected": "5558270113 2026-03-03 Knejslíková, Alena [LZ radiační onkologie] [kontrola při radioterapii, pokračuje v CHRT, sliznice G0, urogen G0, dolní GIT G0, další CHT 12.3.26].pdf" + }, + { + "original": "5558270113 2026-02-12 Knejslíková, Alena [LZ radiační onkologie] [Ca endometria pT1b pN1 FIGO IIIC1, indik. adjuv. CHRT PORTEC3, CHT CDDP+CBDCA+PTX].pdf", + "corrected": "5558270113 2026-02-12 Knejslíková, Alena [LZ radiační onkologie] [chemopohovor, Ca endometria pT1b pN1 FIGO IIIC1, indik. adjuv. CHRT PORTEC3, CHT CDDP+CBDCA+PTX].pdf" + }, + { + "original": "465201175 2026-02-05 Voříšková, Helena [PZ kardiologie] [FIS a Flutter síní, paroxysmus Flutteru, AK Clexane, art. hypertenze].pdf", + "corrected": "465201175 2026-02-05 Voříšková, Helena [PZ kardiologie] [4-5FEB2026, FIS a Flutter síní, paroxysmus Flutteru, AK Clexane, art. hypertenze].pdf" + }, + { + "original": "465201175 2026-01-07 Voříšková, Helena [PZ gynekologie-porodnictví] [I480 paroxyzmální FiS, pád, bolest kyčlí a levého hemithoraxu, analgetika].pdf", + "corrected": "465201175 2026-01-07 Voříšková, Helena [PZ gynekologie] [z interny pro lůžkovou tíseň, I480 paroxyzmální FiS, pád, bolest kyčlí a levého hemithoraxu, analgetika].pdf" + }, + { + "original": "470916013 2026-04-14 Dvořák, Josef [LZ plicní] [IPF dg. HRCT+klinika, MDT 2/2024, Ofev 100mg, Vigantol 3 kapky].pdf", + "corrected": "470916013 2026-04-14 Dvořák, Josef [LZ plicní] [IPF dg. HRCT+klinika, MDT 22024, Ofev 100mg, Vigantol 3 kapky, ad kožní].pdf" + }, + { + "original": "470916013 2026-04-02 Dvořák, Josef [Návrh na lázeňskou léčbu] [intersticiální plicní fibróza, indikace V/6, J84.1, Luhačovice+Mariánské Lázně].pdf", + "corrected": "470916013 2026-04-02 Dvořák, Josef [Návrh na lázeňskou léčbu] [intersticiální plicní fibróza, indikace V6, J84.1, Luhačovice+Mariánské Lázně].pdf" + }, + { + "original": "470916013 2026-04-02 Dvořák, Josef [Návrh na lázeňskou léčbu] [intersticiální plicní fibróza, indikace V6, J84.1, Luhačovice+Mariánské Lázně].pdf", + "corrected": "470916013 2026-04-02 Dvořák, Josef [Návrh na lázeňskou léčbu příloha] [intersticiální plicní fibróza, indikace V6, J84.1, Luhačovice+Mariánské Lázně].pdf" + }, + { + "original": "470916013 2026-04-21 Dvořák, Josef [LZ interna] [revize před lázněmi, IPF, AVNRT, AH, DM2, dyslipidémie, hypotyreóza].pdf", + "corrected": "470916013 2026-04-21 Dvořák, Josef [LZ interna] [vyšetření před lázněmi, IPF, AVNRT, AH, DM2, dyslipidémie, hypotyreóza].pdf" + }, + { + "original": "445318078 2026-04-23 Kusáková, Jaroslava [LZ revmatologie] [gonartróza III-IV st., susp. atypická PMR, klesající zánět, v plánu TEP 5/2026].pdf", + "corrected": "445318078 2026-04-23 Kusáková, Jaroslava [LZ revmatologie] [gonartróza III-IV st., susp. atypická PMR, klesající zánět, v plánu TEP 52026].pdf" + }, + { + "original": "5521946540 2025-12-30 Peterková, Eliška [PZ chirurgie] [S02.00 Fissura calvae, pád na eskalátoru, odlomení kost. fragmentu okcipitalně].pdf", + "corrected": "5521946540 2025-12-30 Peterková, Eliška [PZ chirurgie] [29-30DEC2025, pád v metru, bezvìdomí, S02.00 Fissura calvae, pád na eskalátoru, odlomení kost. fragmentu okcipitalně].pdf" + }, + { + "original": "0552194654 2025-11-07 Peterková, Eliška [PZ psychiatrie] [F432, TS intox. venlafaxinem, 5x Epi záchvat, emočně nestab. osobnost].pdf", + "corrected": "0552194654 2025-11-07 Peterková, Eliška [PZ psychiatrie] [31OCT-07NOV2025, pokus o sebevraždu, F432, TS intox. venlafaxinem, 5x Epi záchvat, emočně nestab. osobnost].pdf" + }, + { + "original": "5505290252 2026-04-21 Flek, Zbyněk [LZ urologie] [Ca prostatae pT2cGS 3+4 po dvRP 6/2019, iPSA 6,26, PSA 0,081, drobné parapelv. cysty ledvin].pdf", + "corrected": "5505290252 2026-04-21 Flek, Zbyněk [LZ urologie] [kontrola, Ca prostatae pT2cGS 3+4 po dvRP 62019, iPSA 6,26, PSA 0,081, drobné parapelv. cysty ledvin, trvá complete remission].pdf" + }, + { + "original": "8452 2026-04-02 Věkrbeová [Laboratoř] [moč chemicky: ERY trace, ostatní neg., pH 6, SG 1.020].pdf", + "corrected": "8755120429 2026-04-02 [uritex] [moč chemicky ERY trace, ostatní neg., pH 6, SG 1.020].pdf" + }, + { + "original": "461001479 2026-04-21 Šťastný, Libor [LZ endokrinologie] [St.p. TTE dx a STE sin 5/18, strumiprivní hypotyreóza substituovaná, Letrox 150ug].pdf", + "corrected": "461001479 2026-04-21 Šťastný, Libor [LZ endokrinologie] [St.p. TTE dx a STE sin 518, strumiprivní hypotyreóza substituovaná, Letrox 150ug].pdf" + }, + { + "original": "9901040000 2026-04-26 Tvrz, Matěj [export zdraví krevní tlak] [prům. 153/74 mmHg, hypertenze 5d, emergentní hypertenzní stav 1d].pdf", + "corrected": "9901040000 2026-04-26 Tvrz, Matěj [export zdraví krevní tlak] [prům. 15374 mmHg, hypertenze 5d, emergentní hypertenzní stav 1d].pdf" + }, + { + "original": "395907022 2026-04-10 Herzová, Marie [LZ ortopedie] [Gonarthrosis bilat., obstr. kortik. +M i.a. vlevo, indik. lázně VII7, M179].pdf", + "corrected": "395907022 2026-04-10 Herzová, Marie [LZ ortopedie] [indikace lázně VII7, M179, gonarthrosis bilat., obstr. kortik. +M i.a. vlevo].pdf" + }, + { + "original": "6008091738 2020-07-15 Nikitin, Petro [LZ gastroenterologie] [Antrumgastritida, inkompetentní kardie, gastroesophageální reflux].pdf", + "corrected": "6008091738 2020-07-15 Nikitin, Petro [LZ gastro] [gastroskopie, antrumgastritida, inkompetentní kardie, gastroesophageální reflux].pdf" + }, + { + "original": "6008091738 2025-11-24 Nikitin, Petro [LZ dermatologie] [seboroická verruka].pdf", + "corrected": "6008091738 2025-11-24 Nikitin, Petro [LZ kožní [seboroická verruka L tváøe, abraze].pdf" + }, + { + "original": "6008091738 2025-08-25 Nikitin, Petro [LZ kožní] [pigmentové névy tč. klidné, bez onkosuspekce].pdf", + "corrected": "6008091738 2025-08-25 Nikitin, Petro [LZ kožní] [vyšetøení dermatoskopem, pigmentové névy tč. klidné, bez onkosuspekce].pdf" + }, + { + "original": "6008091738 2025-05-20 Nikitin, Petro [LZ kardiologie] [ICHS, po PCI RIA 2018, EF LK 65%, mírná dilatace aort. kořene bez progrese].pdf", + "corrected": "6008091738 2025-05-20 Nikitin, Petro [LZ kardiologie] [kontrola, ICHS, po PCI RIA 2018, EF LK 65%, mírná dilatace aort. kořene bez progrese].pdf" + }, + { + "original": "6008091738 2018-08-24 Nikitin, Petro [RTG páteře] [C páteř: lordosa oploštělá, C56 zúžen, spondylóza; Th: skolióza, kyfóza, Th7-10].pdf", + "corrected": "6008091738 2018-08-24 Nikitin, Petro [RTG páteře] [C páteř lordosa oploštělá, C56 zúžen, spondylóza; Th skolióza, kyfóza, Th7-10].pdf" + }, + { + "original": "7109203893 2026-04-07 Deyak, Mykhaylo [Laboratoř] [glukóza 7,1, HbA1c 36, chol. 4,49, LDL 3,07, HDL 0,99, osmolalita 301, PSA 1,438].pdf", + "corrected": "7109203893 2026-04-07 Deyak, Mykhaylo [Laboratoř] [Z000, glukóza 7,1, HbA1c 36, chol. 4,49, LDL 3,07, HDL 0,99, osmolalita 301, PSA 1,438].pdf" + }, + { + "original": "415414073 2026-04-21 Pekárková, Vlasta [Laboratoř] [Z000, K 5,8, osmolalita 296, glukóza 5,7, HbA1c 41, CKD-EPI 0,92 G3a, trombocyty 140].pdf", + "corrected": "415414073 2026-04-21 Pekárková, Vlasta [Laboratoř] [Z000, prediabetes, K 5,8, osmolalita 296, glukóza 5,7, HbA1c 41, CKD-EPI 0,92 G3a, trombocyty 140].pdf" + }, + { + "original": "505218025 2026-04-22 Beznosková, Milena [Laboratoř] [E789, urea 8,31, CKD-EPI 1,33 G2, osmolalita 302, glukóza 7,5, CK 5,49].pdf", + "corrected": "505218025 2026-04-22 Beznosková, Milena [Laboratoř] [E789, diabetes, urea 8,31, CKD-EPI 1,33 G2, osmolalita 302, glukóza 7,5, CK 5,49].pdf" + }, + { + "original": "500206172 2026-04-22 Beznoska, Miloslav [Laboratoř] [E789, CKD-EPI 1,21 G2, glukóza 5,9, HbA1c 41, LDL 3,29].pdf", + "corrected": "500206172 2026-04-22 Beznoska, Miloslav [Laboratoř] [E789, prediabetes, CKD-EPI 1,21 G2, glukóza 5,9, HbA1c 41, LDL 3,29].pdf" + }, + { + "original": "475915054 2026-04-20 Žabová, Věra [Laboratoř] [moč: E. coli 10E5 CFU/ml, citlivá na ampicilin, cefuroxim, cotrimoxazol, pivmecilinam].pdf", + "corrected": "475915054 2026-04-20 Žabová, Věra [Laboratoř] [N309, kultivace a citlivost, moč E. coli 10E5 CFUml, citlivá na ampicilin, cefuroxim, cotrimoxazol, pivmecilinam].pdf" + }, + { + "original": "7059087629 2026-04-13 Tůmová, Renáta [Laboratoř] [E789, chol. 7,34, LDL 4,52, non-HDL 5,53, glukóza 5,83, CKD-EPI 1,42 G2].pdf", + "corrected": "7059087629 2026-04-13 Tůmová, Renáta [Laboratoř] [E789, smíšená hyperlipidémie, prediabetes, chol. 7,34, LDL 4,52, non-HDL 5,53, glukóza 5,83, CKD-EPI 1,42 G2].pdf" + }, + { + "original": "7352200328 2026-04-10 Vališová, Gabriela [Laboratoř] [Z000, chol. 5,62, LDL 3,19, HDL 1,13, TG 4,29, non-HDL 4,5, glukóza 5,4].pdf", + "corrected": "7352200328 2026-04-10 Vališová, Gabriela [Laboratoř] [Z000, smíšená hyperlipidémie, chol. 5,62, LDL 3,19, HDL 1,13, TG 4,29, non-HDL 4,5, glukóza 5,4].pdf" + }, + { + "original": "6757100592 2026-04-16 Slabá, Radka [Laboratoř] [E789, CKD-EPI 1,31 G2, TG 1,90, glukóza 5,8, HbA1c 36, chol. 4,35, LDL 2,10].pdf", + "corrected": "6757100592 2026-04-16 Slabá, Radka [Laboratoř] [E789, prediabetes, CKD-EPI 1,31 G2, TG 1,90, glukóza 5,8, HbA1c 36, chol. 4,35, LDL 2,10].pdf" + }, + { + "original": "395907022 2026-04-10 Herzová, Marie [LZ ortopedie] [gonarthrosis bilat, obstřik kortik+M i.a., indik. lázeňská terapie VII/7 M179].pdf", + "corrected": "395907022 2026-04-10 Herzová, Marie [LZ ortopedie] [gonarthrosis bilat, obstřik kortik+M i.a., indik. lázeňská terapie VII7 M179].pdf" + }, + { + "original": "356031017 2025-10-27 Mejstříková, Marcela [LZ gastroenterologie] [inkompetence kardie, lehce polyp. GE junkce, antrální gastropatie, biopsie].pdf", + "corrected": "356031017 2025-10-27 Mejstříková, Marcela [LZ gastroenterologie] [gastroskopie, inkompetence kardie, lehce polyp. GE junkce, antrální gastropatie, biopsie].pdf" + }, + { + "original": "356031017 2026-01-27 Mejstříková, Marcela [SONO krku] [drobné koloidní uzlíky a spongiformní uzel levého laloku š.ž.].pdf", + "corrected": "356031017 2026-01-27 Mejstříková, Marcela [sono ŠŽ] [drobné koloidní uzlíky a spongiformní uzel levého laloku ŠŽ].pdf" + }, + { + "original": "346204097 2025-11-14 Kopřivíková, Jarmila [PZ neurologie] [11–14NOV2025 embolus M2 ACM sin, trombektomie TICl2c, iCMP].pdf", + "corrected": "346204097 2025-11-14 Kopřivíková, Jarmila [PZ neurologie] [11–14NOV2025, iktus, embolus M2 ACM sin, trombektomie TICl2c, iCMP].pdf" + }, + { + "original": "8351112693 2026-04-27 Zelenková, Petra [sono mamm.] [fibrozní dysplazie, vícečetné fibromy bilat., expanzivní proces ZHQ vlevo s benigními markantami].pdf", + "corrected": "8351112693 2026-04-27 Zelenková, Petra [sono prsù] [fibrozní dysplazie, vícečetné fibromy bilat., expanzivní proces ZHQ vlevo s benigními markantami].pdf" + }, + { + "original": "450113005 2025-01-16 Fiala, Václav [LZ angiologie] [Ektazie AP bilat., sono žil DKK: bil. hluboký žilní syst. bez trombozy, varikozity bilat.].pdf", + "corrected": "450113005 2025-01-16 Fiala, Václav [LZ angiologie] [Ektazie AP bilat., sono žil DKK bil. hluboký žilní syst. bez trombozy, varikozity bilat.].pdf" + }, + { + "original": "450113005 2025-03-28 Fiala, Václav [CT krku, hrudníku, břicha a pánve] [progrese nadbráničníí lymfadenopatie, NHL MZL KS IV A, 1. relaps].pdf", + "corrected": "450113005 2025-03-28 Fiala, Václav [CT krku, hrudníku, břicha a pánve] [progrese nadbráničníí lymfadenopatie, NHL MZL KS IV A, 1. relaps, nekompletní zpráva].pdf" + }, + { + "original": "450113005 2019-02-10 Fiala, Václav [LZ denzitometrie] [snížení kostní denzity v pásmu osteopenie, nehomogenní rozložení denzity].pdf", + "corrected": "450113005 2019-02-10 Fiala, Václav [LZ denzitometrie] [T-1.6, osteopenie, snížení kostní denzity v pásmu osteopenie, nehomogenní rozložení denzity].pdf" + }, + { + "original": "450113005 2026-02-18 Fiala, Václav [LZ hematologie] [MZL, lymfocytóza 42.98, B-NHL CD20+ 65.4%, lymfadenopatie, FIS po kardioverzi].pdf", + "corrected": "450113005 2026-02-18 Fiala, Václav [LZ hematologie] [NHL, lymfocytóza 42.98, B-NHL CD20+ 65.4%, lymfadenopatie, FIS po kardioverzi].pdf" + }, + { + "original": "5862236435 2026-03-18 Kopřivová, Erika [sono břicha] [st.p. IK resekci, IK anastomóza, neo-TI, jaterní steatóza, korové cysty pravé ledviny].pdf", + "corrected": "5862236435 2026-03-18 Kopřivová, Erika [sono břicha] [Crohnova nemoc, st.p. IK resekci, IK anastomóza, neo-TI, jaterní steatóza, korové cysty pravé ledviny].pdf" + }, + { + "original": "5862236435 2026-01-12 Kopřivová, Erika [LZ gastroenterologie] [Crohnova nemoc, st.p. LPSK IK resekci, terapie Entyvio, switch z Remsima 1/2021].pdf", + "corrected": "5862236435 2026-01-12 Kopřivová, Erika [LZ gastroenterologie] [Crohnova nemoc, st.p. LPSK IK resekci, terapie Entyvio, switch z Remsima 12021].pdf" + }, + { + "original": "365123089 2026-04-22 Opršalová, Libuše [Laboratoř] [dg. I839, warfarin, PT ratio 2.32*, INR 2.48*].pdf", + "corrected": "365123089 2026-04-22 Opršalová, Libuše [Laboratoř] [dg. I839, warfarin, PT ratio 2.32, INR 2.48].pdf" + }, + { + "original": "7361130040 2021-12-07 Šilhavá, Simona [LZ plicní] [FVP: FEV1 61%, FEV1/FVC 79%, PEF 48%, TLco 65%, obstrukce].pdf", + "corrected": "7361130040 2021-12-07 Šilhavá, Simona [LZ plicní] [FVP FEV1 61%, FEV1FVC 79%, PEF 48%, TLco 65%, obstrukce].pdf" + }, + { + "original": "5862236435 2026-02-23 Kopřivová, Erika [LZ interna] [hypertenze TK 161/95, BMI 31.87, Crohn-Entyvio, DM2, HLP, hyperurikémie, CPAP].pdf", + "corrected": "5862236435 2026-02-23 Kopřivová, Erika [LZ interna] [hypertenze TK 16195, BMI 31.87, Crohn-Entyvio, DM2, HLP, hyperurikémie, CPAP, ad urologie].pdf" + }, + { + "original": "425412434 null Hornofová, Helena [LZ algologie] [P ramenní funkční, min. bolestivý; L ramenní bolestivý, hybnost dobrá; Biofenac, RTG ramen].pdf", + "corrected": "425412434 2026-04-28 Hornofová, Helena [LZ ambulance bolesti] [nevím datum, P ramenní funkční, min. bolestivý; L ramenní bolestivý, hybnost dobrá; Biofenac, RTG ramen].pdf" + }, + { + "original": "7701120955 2026-04-21 Moudrý, Michal [LZ interna] [EKG sinusový rytmus 64/min, křivka v mezích normy, arteriální hypertenze].pdf", + "corrected": "7701120955 2026-04-21 Moudrý, Michal [LZ interna] [EKG sinusový rytmus 64min, křivka v mezích normy, arteriální hypertenze].pdf" + }, + { + "original": "471126130 2026-04-07 Procházka, Vladimír [LZ angiologie] [CVI bez progrese, bez TEN, varikozity přibývají, Duplex UŽ DK bez obliterace].pdf", + "corrected": "471126130 2026-04-07 Procházka, Vladimír [LZ cévní] [CVI bez progrese, bez TEN, varikozity přibývají, Duplex UŽ DK bez obliterace].pdf" + }, + { + "original": "471126130 2026-02-12 Procházka, Vladimír [LZ kardiologie] [FS 114min, QRS 110, LAH, indikována reablace FS].pdf", + "corrected": "471126130 2026-02-12 Procházka, Vladimír [LZ kardiologie] [plánovaná kontrola, FS 114min, QRS 110, LAH, indikována reablace FS].pdf" + }, + { + "original": "null 2026-04-27 Drakpelova [Laboratoř] [DM2, Glucophage XR 1000 0-0-1, HbA1c 4.5-4.2, Chol 3.07-3.77].pdf", + "corrected": "515820013 2026-04-27 Drakselová, Daniela [INR karta] [07APR2025-10MAR2026].pdf" + }, + { + "original": "5951231044 2026-04-21 Vašinová, Jiřina [PZ ortopedie] [13–21APR2026 impl. TEP coxae l.dx., koxartróza l.dx. KL IV.].pdf", + "corrected": "5951231044 2026-04-21 Vašinová, Jiřina [PZ ortopedie] [13–21APR2026 impl. TEP coxae l.dx., koxartróza l.dx., indikace KLL VII_10].pdf" + }, + { + "original": "480416072 2026-03-09 Štrup, Petr [žádanka OZP] [žádanka o vyšetření zdravotního stavu pro průkaz OZP, komplexní vyšetření 958 Kč].pdf", + "corrected": "480416072 2026-03-09 Štrup, Petr [žádanka IPZS] [žádanka o vyšetření zdravotního stavu pro průkaz OZP, komplexní vyšetření 958 Kč].pdf" + }, + { + "original": "9301280417 2026-03-25 Vaňous, Jakub [žádost o předání zdravotních informací] [registrace u MUDr. Panáčkové, žádost o zaslání dokumentace].pdf", + "corrected": "9301280417 2026-03-25 Vaňous, Jakub [žádost o předání zdravotních informací] [ResTrial s.r.o.].pdf" + }, + { + "original": "Binder1.pdf", + "corrected": "8056010149 2026-04-28 [výbìr nekompletních zpráv] [od pacientky].pdf" + }, + { + "original": "6709150613 2026-04-28 Rutrle, Petr [LZ ORL] [PVS - v.s m. Menier, t.č hypaksuis perc. apicochlearis].pdf", + "corrected": "6709150613 2026-04-28 Rutrle, Petr [LZ ORL] [PVS - v.s m. Menier, t.č hypakusis perc. apicochlearis, doporučena hyperbarická komora].pdf" + } +] \ No newline at end of file diff --git a/Medevio/60 ScansProcessing/extract_patient_info_novy.py b/Medevio/60 ScansProcessing/extract_patient_info_novy.py new file mode 100644 index 0000000..70bda53 --- /dev/null +++ b/Medevio/60 ScansProcessing/extract_patient_info_novy.py @@ -0,0 +1,468 @@ +""" +Zpracování naskenovaných PDF — nová verze. +1. Preview originálu + Claude Vision API +2. Rename dialog +3. 5 variant komprese → uživatel vybere +4. Uložit do Processed, smazat originál +""" +import base64 +import gc +import io +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +if sys.platform == "win32": + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") + +import anthropic +from pdf2image import convert_from_path + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from Knihovny.najdi_dropbox import get_dropbox_root +from Knihovny.najdi_medicus import get_medicus_config + +def _load_env(): + env_path = Path(__file__).parent.parent / ".env" + if env_path.exists(): + for line in env_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + os.environ[k.strip()] = v.strip() + +_load_env() + +POPPLER_PATH = r"C:/Poppler/Library/bin" +CORRECTIONS = True # True = corrections.json se načítá a ukládá; False = ignorovat + +_DROPBOX = Path(get_dropbox_root()) +TO_PROCESS = _DROPBOX / r"Ordinace\Dokumentace_ke_zpracování\Ricoh Fi-8040\KeZpracování" +PROCESSED = _DROPBOX / r"Ordinace\Dokumentace_ke_zpracování\Ricoh Fi-8040\Zpracováno" +CORRECTIONS_FILE = Path(__file__).parent / "corrections.json" +NAMING_RULES_FILE = Path(__file__).parent / "naming_rules.md" +DOKUMENTACE = _DROPBOX / r"Ordinace\Dokumentace_zpracovaná" + +import threading + +_dokumentace_index: set[str] = set() +_dokumentace_ready = threading.Event() + +def _load_dokumentace_index_bg(): + if DOKUMENTACE.exists(): + names = {f.name for f in DOKUMENTACE.iterdir() if f.is_file()} + else: + names = set() + global _dokumentace_index + _dokumentace_index = names + _dokumentace_ready.set() + print(f" Index dokumentace: {len(names)} souborů načteno.") + +def start_dokumentace_index(): + t = threading.Thread(target=_load_dokumentace_index_bg, daemon=True) + t.start() + +VIEWER = Path(__file__).parent / "preview_viewer.py" +RENAME_DIALOG = Path(__file__).parent / "rename_dialog.py" +VARIANT_PICKER = Path(__file__).parent / "variant_picker.py" + +# 5 kompresních variant +COMPRESS_VARIANTS = [ + ("300 DPI / q90", 300, 90), + ("200 DPI / q85", 200, 85), + ("150 DPI / q80", 150, 80), + ("120 DPI / q75", 120, 75), + ( "96 DPI / q70", 96, 70), +] + + +# ─── Komprese jedné varianty ────────────────────────────────────────────────── + +def compress_to_temp(pdf_path: Path, dpi: int, quality: int) -> Path: + import fitz + src = fitz.open(str(pdf_path)) + mat = fitz.Matrix(dpi / 72.0, dpi / 72.0) + out = fitz.open() + for page in src: + pix = page.get_pixmap(matrix=mat, colorspace=fitz.csRGB) + img_bytes = pix.tobytes("jpeg", jpg_quality=quality) + img_doc = fitz.open("pdf", fitz.open("jpeg", img_bytes).convert_to_pdf()) + rect = page.rect + np = out.new_page(width=rect.width, height=rect.height) + np.show_pdf_page(np.rect, img_doc, 0) + src.close() + tmp = Path(tempfile.mktemp(suffix=".pdf")) + out.save(tmp, deflate=True, garbage=4) + out.close() + return tmp + + +# ─── Medicus ověření ───────────────────────────────────────────────────────── + +def _medicus_connect(): + try: + import fdb + cfg = get_medicus_config() + return fdb.connect(dsn=cfg.dsn, user="SYSDBA", password="masterkey", charset="win1250") + except Exception as e: + print(f" [Medicus] Nepřipojeno: {e}") + return None + +def _lookup_by_rc(cur, rc_digits: str) -> dict | None: + cur.execute( + "SELECT IDPAC, PRIJMENI, JMENO, RODCIS FROM KAR " + "WHERE REPLACE(RODCIS, '/', '') = ?", (rc_digits,) + ) + row = cur.fetchone() + if row: + return {"idpac": row[0], "prijmeni": row[1].strip(), "jmeno": row[2].strip(), "rodcis": row[3].strip()} + return None + +def _rc_candidates(rc: str) -> list[str]: + similar = {"0": "8", "8": "0", "1": "7", "7": "1", "5": "6", "6": "5", "3": "8"} + candidates = set() + for i in range(len(rc)): + candidates.add(rc[:i] + rc[i+1:]) + for i in range(len(rc) + 1): + candidates.add(rc[:i] + "0" + rc[i:]) + for i, ch in enumerate(rc): + if ch in similar: + candidates.add(rc[:i] + similar[ch] + rc[i+1:]) + candidates.discard(rc) + return sorted(c for c in candidates if len(c) in (9, 10)) + +def _rc_checksum_ok(rc: str) -> bool: + digits = re.sub(r"\D", "", rc) + if len(digits) == 10: + return int(digits) % 11 == 0 + return True + +def verify_patient(rc_raw: str) -> dict: + rc = re.sub(r"\D", "", rc_raw or "") + if not rc: + return {"status": "not_found", "patient": None, "rc_corrected": None} + con = _medicus_connect() + if con is None: + return {"status": "offline", "patient": None, "rc_corrected": None} + try: + cur = con.cursor() + patient = _lookup_by_rc(cur, rc) + if patient: + return {"status": "ok", "patient": patient, "rc_corrected": None} + candidates = _rc_candidates(rc) + matches = [(c, _lookup_by_rc(cur, c)) for c in candidates] + matches = [(c, p) for c, p in matches if p] + if not matches: + return {"status": "not_found", "patient": None, "rc_corrected": None} + matches.sort(key=lambda x: (0 if _rc_checksum_ok(x[0]) else 1)) + best_rc, best_patient = matches[0] + return {"status": "fuzzy", "patient": best_patient, "rc_corrected": best_rc, "all_matches": matches} + finally: + con.close() + +def check_duplicates(rc: str, datum: str) -> list[str]: + if not rc or not datum: + return [] + # Počkej max 15s na dokončení indexu (typicky hotovo za dobu volání Claude) + _dokumentace_ready.wait(timeout=15) + prefix = f"{rc} {datum}" + return [name for name in _dokumentace_index if name.startswith(prefix)] + + +# ─── Korekce (few-shot příklady) ───────────────────────────────────────────── + +def load_corrections() -> list[dict]: + if CORRECTIONS_FILE.exists(): + return json.loads(CORRECTIONS_FILE.read_text(encoding="utf-8")) + return [] + +def save_correction(original: str, corrected: str): + if not CORRECTIONS: + return + corrections = load_corrections() + for c in corrections: + if c["original"] == original and c["corrected"] == corrected: + return + corrections.append({"original": original, "corrected": corrected}) + CORRECTIONS_FILE.write_text( + json.dumps(corrections, ensure_ascii=False, indent=2), encoding="utf-8" + ) + print(f" ✓ Korekce uložena ({len(corrections)} celkem)") + +def load_naming_rules() -> str: + if NAMING_RULES_FILE.exists(): + content = NAMING_RULES_FILE.read_text(encoding="utf-8").strip() + if content: + return f"Pravidla pro pojmenování souborů (dodržuj vždy):\n{content}\n\n" + return "" + +def build_corrections_prompt() -> str: + if not CORRECTIONS: + return "" + corrections = load_corrections() + if not corrections: + return "" + lines = ["Příklady korekcí z minulých běhů (uč se z nich):"] + for c in corrections[-10:]: + lines.append(f' - špatně: "{c["original"]}"') + lines.append(f' správně: "{c["corrected"]}"') + return "\n".join(lines) + "\n\n" + + +# ─── Claude Vision API ──────────────────────────────────────────────────────── + +def extract_info(pdf_path: Path) -> dict: + print(" Převádím na obrázek...") + suffix = pdf_path.suffix.lower() + if suffix in (".jpg", ".jpeg", ".png"): + from PIL import Image + img = Image.open(pdf_path) + buf = io.BytesIO() + img.save(buf, format="JPEG", quality=95) + img.close() + else: + images = convert_from_path(str(pdf_path), poppler_path=POPPLER_PATH, dpi=300) + buf = io.BytesIO() + images[0].save(buf, format="JPEG", quality=95) + del images + gc.collect() + image_b64 = base64.standard_b64encode(buf.getvalue()).decode("utf-8") + + prompt = ( + load_naming_rules() + + build_corrections_prompt() + + "Toto je naskenovaná lékařská zpráva v češtině. " + "Vrať JSON s těmito poli:\n" + "- \"jmeno\": celé jméno pacienta (příjmení + jméno + případný titul)\n" + "- \"rodne_cislo\": rodné číslo pacienta BEZ lomítka (pouze číslice)\n" + "- \"datum_zpravy\": datum zprávy ve formátu YYYY-MM-DD\n" + "- \"typ_dokumentu\": typ dokumentu — " + "\"LZ {oddělení}\" = ambulantní/lékařská zpráva (např. \"LZ chirurgie\", \"LZ kardiologie\", \"LZ plicní\", \"LZ ORL\"); " + "\"PZ {oddělení}\" = propouštěcí zpráva z hospitalizace (např. \"PZ interna\", \"PZ neurologie\"). " + "Jiné typy: \"Laboratoř\", \"CT břicha\", \"MRI páteře\", \"kolonoskopie\", " + "\"operační protokol oční\", \"poukaz FT\", \"diagnostická mamografie\" atd.\n" + "- \"poznamka\": krátká klinická poznámka česky, max 80 znaků. " + "DŮLEŽITÉ: pokud zpráva obsahuje sekci \"Závěr:\" nebo \"Závěr vyšetření:\", " + "použij VÝHRADNĚ obsah této sekce — je nejdůležitější. " + "Teprve pokud závěr chybí, shrň obsah z celé zprávy.\n" + "- \"nazev_souboru\": název souboru ve formátu " + "\"{rodne_cislo} {datum_zpravy} {Příjmení}, {Jméno} [{typ_dokumentu}] [{poznamka}].pdf\" " + "(jméno bez titulu, RČ bez lomítka)\n" + "- \"rotace\": o kolik stupňů CCW je třeba otočit obrázek aby byl text čitelně na výšku nebo šířku " + "(hodnoty: 0, 90, 180, 270). Pokud je text již správně orientovaný, vrať 0.\n\n" + "Pokud pole nenajdeš, použij null. Nepiš nic jiného než JSON." + ) + + print(" Volám Claude Vision API...") + try: + client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY")) + response = client.messages.create( + model="claude-sonnet-4-6", + max_tokens=400, + messages=[{"role": "user", "content": [ + {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_b64}}, + {"type": "text", "text": prompt}, + ]}], + ) + usage = response.usage + print(f" Tokeny: {usage.input_tokens} in + {usage.output_tokens} out = ${usage.input_tokens*3/1e6 + usage.output_tokens*15/1e6:.4f}") + + raw = response.content[0].text.strip() + if raw.startswith("```"): + raw = raw.split("```")[1] + if raw.startswith("json"): + raw = raw[4:] + try: + return json.loads(raw.strip()) + except json.JSONDecodeError: + print(f" VAROVÁNÍ: nelze parsovat JSON: {raw!r}") + return {"nazev_souboru": None, "raw": raw} + except Exception as e: + print(f" VAROVÁNÍ: Claude API selhalo ({e}) — otevírám dialog pro ruční vyplnění.") + return {"nazev_souboru": None} + + +# ─── Subprocess helpers ─────────────────────────────────────────────────────── + +def open_preview(pdf_path: Path) -> tuple[subprocess.Popen, Path]: + geom_file = Path(tempfile.mktemp(suffix=".json")) + proc = subprocess.Popen([sys.executable, str(VIEWER), str(pdf_path), f"--write-geometry={geom_file}"]) + return proc, geom_file + + +def read_preview_bottom(geom_file: Path, timeout: float = 5.0) -> int: + import time + deadline = time.time() + timeout + while time.time() < deadline: + if geom_file.exists(): + geom = json.loads(geom_file.read_text(encoding="utf-8")) + geom_file.unlink(missing_ok=True) + return geom["y"] + geom["h"] + 30 # +30 pro title bar + time.sleep(0.1) + geom_file.unlink(missing_ok=True) + return None + + +def run_rename_dialog(nazev: str, info_lines: list, below_y: int = None) -> str | None: + tmp = Path(tempfile.mktemp(suffix=".json")) + tmp.write_text(json.dumps({"nazev": nazev, "info_lines": info_lines}, ensure_ascii=False), encoding="utf-8") + args = [sys.executable, str(RENAME_DIALOG), str(tmp)] + if below_y is not None: + args.append(f"--below-y={below_y}") + env = {**os.environ, "PYTHONIOENCODING": "utf-8", "PYTHONUTF8": "1"} + proc = subprocess.run(args, capture_output=True, text=True, encoding="utf-8", env=env) + tmp.unlink(missing_ok=True) + out = proc.stdout.strip() + return json.loads(out).get("value") if out else None + + +def run_variant_picker(variants_data: list) -> str | None: + tmp = Path(tempfile.mktemp(suffix=".json")) + tmp.write_text(json.dumps(variants_data, ensure_ascii=False), encoding="utf-8") + proc = subprocess.run( + [sys.executable, str(VARIANT_PICKER), str(tmp)], + capture_output=True, text=True, encoding="utf-8", + ) + tmp.unlink(missing_ok=True) + if proc.returncode != 0 or not proc.stdout.strip(): + print(f" [variant_picker] returncode={proc.returncode}") + if proc.stderr.strip(): + print(f" [variant_picker] CHYBA:\n{proc.stderr.strip()}") + out = proc.stdout.strip() + return json.loads(out).get("chosen") if out else None + + +# ─── Hlavní flow ────────────────────────────────────────────────────────────── + +def process_file(pdf_path: Path): + print(f"\nSoubor: {pdf_path.name}") + + # Spusť načítání indexu dokumentace na pozadí — hotovo za dobu volání Claude + start_dokumentace_index() + + # 1. Otevři preview originálu + preview, geom_file = open_preview(pdf_path) + below_y = read_preview_bottom(geom_file) + + # 2. Claude Vision API + info = extract_info(pdf_path) + nazev = info.get("nazev_souboru") or pdf_path.name + + # 3. Medicus ověření + fuzzy matching RČ + rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "") + print(f" Ověřuji v Medicus (RČ: {rc_from_scan})...") + verif = verify_patient(rc_from_scan) + + # Oprava RČ při fuzzy matchi + if verif["status"] == "fuzzy" and verif.get("rc_corrected") and nazev: + nazev = nazev.replace(rc_from_scan, verif["rc_corrected"], 1) + print(f" → RČ opraveno: {rc_from_scan} → {verif['rc_corrected']}") + + # Info řádky pro dialog + status = verif["status"] + patient = verif.get("patient") + info_lines = [] + if status == "ok": + info_lines.append(f"✓ Medicus: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}") + elif status == "fuzzy": + info_lines.append(f"⚠ RČ ze skenu '{rc_from_scan}' → opraveno na {verif['rc_corrected']}") + info_lines.append(f" Pacient: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}") + elif status == "not_found": + info_lines.append(f"✗ RČ '{rc_from_scan}' nenalezeno v Medicus") + else: + info_lines.append("— Medicus nedostupný (offline)") + + # Duplicity + rc_final = re.sub(r"\D", "", verif["patient"]["rodcis"] if patient else rc_from_scan) + duplicity = check_duplicates(rc_final, info.get("datum_zpravy") or "") + if duplicity: + info_lines.append(f"⚠ DUPLICITA: {', '.join(duplicity)}") + + if not info_lines: + info_lines = ["[Claude nevrátil název — uprav ručně]"] + print(" Otevírám dialog pro schválení názvu...") + final_name = run_rename_dialog(nazev, info_lines, below_y=below_y) + + preview.terminate() + + if not final_name: + print(" Přeskočeno.") + return + + if not final_name.endswith(".pdf"): + final_name += ".pdf" + final_name = re.sub(r'[<>:"/\\|?*]', '', final_name) + + if nazev and final_name != nazev: + save_correction(nazev, final_name) + + print(f" Schválený název: {final_name}") + + # 4. Generuj kompresní varianty (originál + 5 variant) + print(" Generuji kompresní varianty...") + temp_files = [] + orig_kb = round(pdf_path.stat().st_size / 1024) + variants_data = [{"path": str(pdf_path), "label": "Originál", "size_kb": orig_kb}] + for label, dpi, quality in COMPRESS_VARIANTS: + tmp = compress_to_temp(pdf_path, dpi, quality) + size_kb = round(tmp.stat().st_size / 1024) + temp_files.append(tmp) + variants_data.append({"path": str(tmp), "label": label, "size_kb": size_kb}) + print(f" {label}: {size_kb} kB") + + # 5. Vyber variantu + print(" Vyber variantu v okně...") + chosen = run_variant_picker(variants_data) + + if not chosen: + print(" Žádná varianta nevybrána, přeskakuji.") + for t in temp_files: + t.unlink(missing_ok=True) + return + + # 6. Ulož do Processed + PROCESSED.mkdir(exist_ok=True) + dest = PROCESSED / final_name + if dest.exists(): + print(f" Přepisuji existující: {dest.name}") + shutil.copy2(chosen, dest) + pdf_path.unlink() + print(f" ✓ Uloženo: {dest.name}") + + for t in temp_files: + t.unlink(missing_ok=True) # originál mezi temp_files není, je bezpečné + + +def process_folder(folder: Path): + files = sorted(f for f in folder.iterdir() if f.suffix.lower() in (".pdf", ".jpg", ".jpeg", ".png")) + if not files: + print(f"Žádné soubory v: {folder}") + return + print(f"Nalezeno {len(files)} soubor(ů).") + for f in files: + try: + process_file(f) + except Exception as e: + print(f" CHYBA: {e}") + print("\nHotovo.") + + +if __name__ == "__main__": + PROCESSED.mkdir(exist_ok=True) + TO_PROCESS.mkdir(exist_ok=True) + + target = Path(sys.argv[1]) if len(sys.argv) > 1 else TO_PROCESS + + if target.is_file(): + process_file(target) + elif target.is_dir(): + process_folder(target) + else: + print("Použití: python extract_patient_info_novy.py [soubor.pdf nebo složka]") + sys.exit(1) diff --git a/Medevio/60 ScansProcessing/jpg_to_pdf.py b/Medevio/60 ScansProcessing/jpg_to_pdf.py new file mode 100644 index 0000000..051c15c --- /dev/null +++ b/Medevio/60 ScansProcessing/jpg_to_pdf.py @@ -0,0 +1,101 @@ +""" +Konverze JPG/PNG → PDF se správnou orientací stránky (A4). + +Řeší: +- EXIF orientaci (fotky z telefonu/skeneru bývají otočené) +- Správné umístění na A4 stránce (na výšku nebo na šířku dle obsahu) +- Zachování kvality + +Použití: + python jpg_to_pdf.py soubor.jpg + python jpg_to_pdf.py soubor.jpg vystup.pdf +""" + +import io +import sys +from pathlib import Path + +from PIL import Image, ImageOps + +# A4 rozměry v mm +A4_W_MM = 210 +A4_H_MM = 297 +MARGIN_MM = 0 # bez okraje, tisk si řeší Acrobat (Fit to Print) + + +def fix_orientation(img: Image.Image) -> Image.Image: + """Opraví rotaci podle EXIF dat (tag 274).""" + return ImageOps.exif_transpose(img) + + +def image_to_pdf(src: Path, dst: Path, dpi: int = 150, quality: int = 80, rotate_ccw: int = 0): + img = Image.open(src) + print(f" Originál: {img.size[0]}×{img.size[1]} px, mode={img.mode}, format={img.format}") + + # 1. Oprav EXIF orientaci + img = fix_orientation(img) + print(f" Po EXIF korekci: {img.size[0]}×{img.size[1]} px") + + # 2. Rotace dle parametru (od Claude nebo ručně) + if rotate_ccw and rotate_ccw != 0: + img = img.rotate(rotate_ccw, expand=True) + print(f" Po rotaci {rotate_ccw}° CCW: {img.size[0]}×{img.size[1]} px") + + # 2. Převeď na RGB (PDF nepodporuje RGBA/P) + if img.mode in ("RGBA", "P", "LA"): + img = img.convert("RGB") + + # 3. Urči orientaci stránky podle poměru stran obrázku + img_w, img_h = img.size + if img_w > img_h: + # Obrázek na šířku → stránka na šířku (A4 landscape) + page_w_mm, page_h_mm = A4_H_MM, A4_W_MM + print(f" Orientace stránky: na šířku (landscape)") + else: + # Obrázek na výšku → stránka na výšku (A4 portrait) + page_w_mm, page_h_mm = A4_W_MM, A4_H_MM + print(f" Orientace stránky: na výšku (portrait)") + + # 4. Vypočti cílovou velikost s okrajem (mm → px při daném DPI) + mm_to_px = dpi / 25.4 + max_w_px = int((page_w_mm - 2 * MARGIN_MM) * mm_to_px) + max_h_px = int((page_h_mm - 2 * MARGIN_MM) * mm_to_px) + + # 5. Škáluj obrázek na stránku (zachovej poměr stran) + img.thumbnail((max_w_px, max_h_px), Image.LANCZOS) + print(f" Výsledná velikost obrázku: {img.size[0]}×{img.size[1]} px") + + # 6. Vlož obrázek na bílé A4 plátno + page_w_px = int(page_w_mm * mm_to_px) + page_h_px = int(page_h_mm * mm_to_px) + canvas = Image.new("RGB", (page_w_px, page_h_px), "white") + + offset_x = (page_w_px - img.size[0]) // 2 + offset_y = (page_h_px - img.size[1]) // 2 + canvas.paste(img, (offset_x, offset_y)) + + # 7. Ulož jako PDF + canvas.save(dst, "PDF", resolution=dpi, quality=quality) + print(f" ✓ Uloženo: {dst.name} ({dst.stat().st_size // 1024} KB)") + + +if __name__ == "__main__": + if sys.platform == "win32": + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") + + if len(sys.argv) < 2: + print("Použití: python jpg_to_pdf.py soubor.jpg [vystup.pdf] [rotace_ccw]") + print(" rotace_ccw: 0 / 90 / 180 / 270 (výchozí: 0)") + sys.exit(1) + + src = Path(sys.argv[1]) + if not src.exists(): + print(f"Soubor nenalezen: {src}") + sys.exit(1) + + dst = Path(sys.argv[2]) if len(sys.argv) > 2 else src.with_suffix(".pdf") + rotate_ccw = int(sys.argv[3]) if len(sys.argv) > 3 else 0 + + print(f"Konvertuji: {src.name} → {dst.name}") + image_to_pdf(src, dst, rotate_ccw=rotate_ccw) diff --git a/Medevio/60 ScansProcessing/naming_rules.md b/Medevio/60 ScansProcessing/naming_rules.md new file mode 100644 index 0000000..42f785f --- /dev/null +++ b/Medevio/60 ScansProcessing/naming_rules.md @@ -0,0 +1,26 @@ +# Pravidla pro přejmenování souborů + +Tato pravidla platí vždy při generování polí `poznamka` a `nazev_souboru`. + +1. Název souboru má vždy tvar: `RODNECISLO YYYY-MM-DD Příjmení, Jméno [TYP ODBORNOST] [popis].pdf` + - TYP je vždy buď `LZ` (lékařská zpráva / ambulantní zpráva) nebo `PZ` (propouštěcí zpráva z hospitalizace). + - Jiné typy dokumentů (Laboratoř, CT, MRI, kolonoskopie, poukaz FT apod.) nemají TYP prefix — píší se celým názvem: `[Laboratoř]`, `[CT břicha]` atd. + - Příklady: `[LZ chirurgie]`, `[PZ interna]`, `[Laboratoř]`, `[CT břicha]` + +2. Když je typ dokumentu PZ (propouštěcí zpráva), umísti do druhé závorky jako první věc data hospitalizace ve tvaru `DDMMMYYYY–DDMMMYYYY` (měsíc třemi písmeny anglicky, velká, bez mezer), za pomlčkou pak popis. + - Příklad: `[PZ interna] [12–15APR2026 srdeční selhání]` + - Pokud je datum přijetí a propuštění ve stejném měsíci, stačí: `[12–15APR2026 ...]` + - Pokud datum hospitalizace nelze určit, druhou závorku napiš bez datumu. + +3. Když je dokument typ "Laboratoř", do `poznamka` uváděj POUZE hodnoty mimo normu (patologické nálezy) — hodnoty v normě vynech. Osmolalitu séra nikdy nezmiňuj, ani když je mimo normu. +4. Pokud laboratorní výsledky obsahují glomerulární filtraci — bývá označena jako eGFR, CKD-EPI nebo CK-EPI — do `poznamka` nikdy nepiš číselnou hodnotu eGFR. Místo toho uveď pouze klasifikaci: eGFR ≥ 90 → CHRIG1, 60–89 → CHRIG2, 45–59 → CHRIG3a, 30–44 → CHRIG3b, 15–29 → CHRIG4, < 15 → CHRIG5. Klasifikaci uváděj pouze pokud je CHRIG2 nebo horší (tj. eGFR < 90) — CHRIG1 je v normě, nezmiňuj ho. +5. Když je dokument typ "Laboratoř" a zpráva obsahuje diagnózu (dg., dg:, diagnóza), umísti ji do `nazev_souboru` jako první část druhé závorky, tedy: `[Laboratoř] [dg. XY00 - stručná poznamka]`. +6. Zkratky a pojmenování: slovo „sono" (sonografie/ultrazvuk) piš vždy malými písmeny — `sono břicha`, `sono ŠŽ`, nikoli `SONO`. Štítnou žlázu označuj vždy zkratkou `ŠŽ`. Sonografii prsu/prsů (sono mamm., sono mamografie, sono mamma apod.) piš vždy jako `sono prsů`. Denzitometrii (DEXA, DXA, denzitometrie) piš vždy pouze jako `[DXA]` — bez prefixu LZ. Algologii piš vždy jako `[LZ léčba bolesti]`. Dermatovenerologii (dermatologie, dermatovenerologie, kožní) piš vždy jako `[LZ kožní]`. Angiologii piš vždy jako `[LZ cévní]`. +7. V číselných hodnotách VŽDY používej desetinnou tečku, nikoli desetinnou čárku. Toto pravidlo platí absolutně pro všechna čísla v `poznamka` i `nazev_souboru` — např. `TG 4.73`, nikoli `TG 4,73`. + +8. Rozpoznávání vzorců — sideropenická anémie: Pokud laboratorní výsledky splňují typický obraz sideropenické (železo-deficitní) anémie, přidej diagnózu jako první část druhé závorky ve tvaru `[sideropenická anémie, ...]`. + Typický obraz (stačí kombinace několika z těchto nálezů): + - Krevní obraz: ↓ Hb, ↓ Htk, ↓ MCV (mikrocytóza), ↓ MCH nebo ↓ MCHC (hypochromie), ↑ RDW (anisocytóza) + - Metabolismus železa: ↓ sérové Fe (železo), ↓ ferritin, ↑ transferrin (nebo TIBC), ↓ saturace transferrinu + - Diagnózu uveď pouze pokud je obraz dostatečně přesvědčivý (alespoň ↓ Hb + ↓ MCV nebo ↓ Fe/ferritin). + - Příklad výsledného názvu: `[Laboratoř] [sideropenická anémie, Hb 98, MCV 71, Fe 5.2]` diff --git a/Medevio/60 ScansProcessing/preview_viewer.py b/Medevio/60 ScansProcessing/preview_viewer.py new file mode 100644 index 0000000..cbd9b78 --- /dev/null +++ b/Medevio/60 ScansProcessing/preview_viewer.py @@ -0,0 +1,111 @@ +""" +Standalone PDF/obrázek náhled — spouští se jako subprocess z extract_patient_info.py. +Argumenty: preview_viewer.py [--delete-on-close] +""" +import sys +from pathlib import Path +import tkinter as tk + + +def main(): + if len(sys.argv) < 2: + sys.exit(1) + + pdf_path = Path(sys.argv[1]) + delete_on_close = "--delete-on-close" in sys.argv + + try: + from PIL import Image, ImageTk + import fitz + except ImportError: + sys.exit(2) + + suffix = pdf_path.suffix.lower() + if suffix in (".jpg", ".jpeg", ".png"): + pil_img = Image.open(pdf_path) + doc = None + else: + doc = fitz.open(str(pdf_path)) + pil_img = None + + root = tk.Tk() + root.tk.call("encoding", "system", "utf-8") + + sh = root.winfo_screenheight() + page_count = len(doc) if doc else 1 + current = [0] + photo_ref = [None] + + def render(n) -> Image.Image: + if doc is not None: + page = doc[n] + zoom = min(700 / page.rect.width, (sh - 150) / page.rect.height) + pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom)) + return Image.frombytes("RGB", (pix.width, pix.height), pix.samples) + else: + img = pil_img.copy() + img.thumbnail((700, sh - 150), Image.LANCZOS) + return img + + def on_close(): + if doc: + try: + doc.close() + except Exception: + pass + if delete_on_close: + try: + pdf_path.unlink(missing_ok=True) + except Exception: + pass + root.destroy() + + root.title(pdf_path.stem) + root.attributes("-topmost", True) + root.resizable(False, False) + root.protocol("WM_DELETE_WINDOW", on_close) + + lbl_img = tk.Label(root) + lbl_img.pack() + + frame_nav = tk.Frame(root) + frame_nav.pack(pady=4) + + lbl_page = tk.Label(frame_nav, font=("Segoe UI", 9)) + lbl_page.pack(side="left", padx=10) + + def show(n): + current[0] = n + img = render(n) + photo_ref[0] = ImageTk.PhotoImage(img) + lbl_img.config(image=photo_ref[0]) + lbl_page.config(text=f"Strana {n + 1} / {page_count}") + btn_prev.config(state="normal" if n > 0 else "disabled") + btn_next.config(state="normal" if n < page_count - 1 else "disabled") + + btn_prev = tk.Button(frame_nav, text="◄ Předchozí", command=lambda: show(current[0] - 1)) + btn_prev.pack(side="left") + btn_next = tk.Button(frame_nav, text="Další ►", command=lambda: show(current[0] + 1)) + btn_next.pack(side="left") + + show(0) + root.update_idletasks() + sw = root.winfo_screenwidth() + w = root.winfo_width() + h = root.winfo_height() + x = (sw - w) // 2 + root.geometry(f"+{x}+0") + + # Zapiš geometrii do souboru pokud byl předán argument --write-geometry= + import json as _json + for arg in sys.argv: + if arg.startswith("--write-geometry="): + geom_path = Path(arg.split("=", 1)[1]) + geom_path.write_text(_json.dumps({"x": x, "y": 0, "w": w, "h": h}), encoding="utf-8") + break + + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/Medevio/60 ScansProcessing/rename_dialog.py b/Medevio/60 ScansProcessing/rename_dialog.py new file mode 100644 index 0000000..6b3c4e0 --- /dev/null +++ b/Medevio/60 ScansProcessing/rename_dialog.py @@ -0,0 +1,105 @@ +""" +Standalone dialog pro schválení / opravu názvu souboru. +Spouští se jako subprocess z extract_patient_info.py. +Argumenty: rename_dialog.py +JSON vstup: { "nazev": "...", "info_lines": [...] } +JSON výstup: { "value": "..." } nebo { "value": null } +""" +import json +import os +import sys +from pathlib import Path +import tkinter as tk + +if sys.platform == "win32": + try: + from ctypes import windll + windll.shcore.SetProcessDpiAwareness(1) + except Exception: + pass + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") + + +def main(): + if len(sys.argv) < 2: + print(json.dumps({"value": None})) + sys.exit(0) + + data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + nazev = data.get("nazev") or "" + info_lines = data.get("info_lines") or [] + + result = {"value": None} + + root = tk.Tk() + root.title("Schválení názvu souboru") + root.resizable(True, False) + root.attributes("-topmost", True) + root.tk.call("encoding", "system", "utf-8") + os.environ.setdefault("TCL_ENCODING", "utf-8") + + pad = {"padx": 12, "pady": 6} + + frame_info = tk.Frame(root, bg="#f0f0f0", bd=1, relief="sunken") + frame_info.pack(fill="x", **pad) + for line in info_lines: + color = "#b00000" if line.startswith("⚠") else "#004080" if line.startswith("✓") else "#333" + tk.Label(frame_info, text=line, anchor="w", bg="#f0f0f0", + fg=color, font=("Segoe UI", 10)).pack(fill="x", padx=8, pady=1) + + tk.Label(root, text="Název souboru (bez .pdf):", anchor="w", + font=("Segoe UI", 9, "bold")).pack(fill="x", padx=12, pady=(10, 2)) + + nazev_bez = nazev[:-4] if nazev.endswith(".pdf") else nazev + var = tk.StringVar(value=nazev_bez) + entry = tk.Entry(root, textvariable=var, font=("Segoe UI", 10), width=135) + entry.pack(fill="x", padx=12, pady=(0, 10)) + entry.icursor(tk.END) + entry.focus_set() + + frame_btn = tk.Frame(root) + frame_btn.pack(pady=(0, 12)) + + def schvalit(event=None): + result["value"] = var.get().strip() + root.destroy() + + def preskocit(event=None): + result["value"] = None + root.destroy() + + tk.Button(frame_btn, text="✓ Schválit (Enter)", command=schvalit, + bg="#2a7a2a", fg="white", font=("Segoe UI", 10, "bold"), + padx=16, pady=6).pack(side="left", padx=8) + tk.Button(frame_btn, text="✗ Přeskočit (Esc)", command=preskocit, + bg="#7a2a2a", fg="white", font=("Segoe UI", 10), + padx=16, pady=6).pack(side="left", padx=8) + + root.bind("", schvalit) + root.bind("", preskocit) + + root.update_idletasks() + sw = root.winfo_screenwidth() + w = root.winfo_width() + x = (sw - w) // 2 + + # Pozice pod preview oknem pokud byl předán argument --below-y=N + below_y = None + for arg in sys.argv: + if arg.startswith("--below-y="): + below_y = int(arg.split("=", 1)[1]) + break + y = below_y if below_y is not None else (root.winfo_screenheight() - root.winfo_height() - 60) + root.geometry(f"+{x}+{y}") + + root.lift() + root.focus_force() + root.mainloop() + + print(json.dumps({"value": result["value"]}, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/Medevio/60 ScansProcessing/variant_picker.py b/Medevio/60 ScansProcessing/variant_picker.py new file mode 100644 index 0000000..b4cb5e5 --- /dev/null +++ b/Medevio/60 ScansProcessing/variant_picker.py @@ -0,0 +1,148 @@ +""" +Jedno okno pro výběr kompresní varianty PDF. +Nahoře tlačítka 1–N pro přepínání, tlačítko "Tohle beru" pro potvrzení. +Argumenty: variant_picker.py +JSON vstup: [{"path": "...", "label": "150 DPI / q80", "size_kb": 139}, ...] +JSON výstup (stdout): {"chosen": "cesta/k/souboru"} +""" +import json +import sys +from pathlib import Path +import tkinter as tk +from PIL import Image, ImageTk +import fitz + + +def main(): + if len(sys.argv) < 2: + sys.exit(1) + + variants = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + chosen = {"path": None} + docs = [fitz.open(v["path"]) for v in variants] + current = [0] + photo_ref = [None] + + root = tk.Tk() + root.tk.call("encoding", "system", "utf-8") + root.attributes("-topmost", True) + + sh = root.winfo_screenheight() + sw = root.winfo_screenwidth() + win_h = sh - 80 # odečteme taskbar + title bar + img_h = win_h - 160 + img_w = sw // 2 # šířka okna = polovina monitoru + + x = (sw - img_w) // 2 + root.geometry(f"{img_w}x{win_h}+{x}+0") + root.resizable(False, False) + + # ── Horní panel s tlačítky variant ── + frame_top = tk.Frame(root, bg="#222") + frame_top.pack(fill="x") + + btn_variants = [] + current_page = [0] + + def show(n, page_n=0): + current[0] = n + current_page[0] = page_n + doc = docs[n] + page = doc[page_n] + zoom = min(img_w / page.rect.width, img_h / page.rect.height) + pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom)) + img = Image.frombytes("RGB", (pix.width, pix.height), pix.samples) + photo_ref[0] = ImageTk.PhotoImage(img) + lbl_img.config(image=photo_ref[0]) + page_count = len(doc) + root.title(f"Varianta {n+1}: {variants[n]['label']} ({variants[n]['size_kb']} kB) — strana {page_n+1}/{page_count}") + for i, b in enumerate(btn_variants): + b.config(bg="#2a5a9a" if i == n else "#444") + btn_prev_page.config(state="normal" if page_n > 0 else "disabled") + btn_next_page.config(state="normal" if page_n < page_count - 1 else "disabled") + + for i, v in enumerate(variants): + b = tk.Button( + frame_top, + text=f"{i+1}. {v['label']}\n{v['size_kb']} kB", + font=("Segoe UI", 9, "bold"), + bg="#444", fg="white", + relief="flat", padx=8, pady=6, + command=lambda n=i: show(n), + ) + b.pack(side="left", padx=2, pady=4) + btn_variants.append(b) + + # ── Tlačítka Beru / Přeskočit — stejný styl jako varianty ── + def beru(): + chosen["path"] = variants[current[0]]["path"] + root.destroy() + + def preskocit(): + root.destroy() + + tk.Button( + frame_top, + text="✓ Tohle beru\n", + command=beru, + bg="#2a7a2a", fg="white", + font=("Segoe UI", 9, "bold"), + relief="flat", padx=8, pady=6, + ).pack(side="left", padx=2, pady=4) + + tk.Button( + frame_top, + text="✗ Přeskočit\n", + command=preskocit, + bg="#7a2a2a", fg="white", + font=("Segoe UI", 9, "bold"), + relief="flat", padx=8, pady=6, + ).pack(side="left", padx=2, pady=4) + + # ── Navigace stran — úplně vpravo ── + btn_next_page = tk.Button( + frame_top, + text="Další ►\n", + command=lambda: show(current[0], current_page[0] + 1), + bg="#555", fg="white", + font=("Segoe UI", 9, "bold"), + relief="flat", padx=8, pady=6, + ) + btn_next_page.pack(side="right", padx=2, pady=4) + + btn_prev_page = tk.Button( + frame_top, + text="◄ Před.\n", + command=lambda: show(current[0], current_page[0] - 1), + bg="#555", fg="white", + font=("Segoe UI", 9, "bold"), + relief="flat", padx=8, pady=6, + ) + btn_prev_page.pack(side="right", padx=2, pady=4) + + # ── Obrázek ── + lbl_img = tk.Label(root, bg="black") + lbl_img.pack(fill="both", expand=True) + + root.bind("", lambda e: show(0)) + root.bind("", lambda e: show(1)) + root.bind("", lambda e: show(2)) + root.bind("", lambda e: show(3)) + root.bind("", lambda e: show(4)) + root.bind("", lambda e: beru()) + root.bind("", lambda e: preskocit()) + + show(0) + root.mainloop() + + for d in docs: + try: + d.close() + except Exception: + pass + + print(json.dumps({"chosen": chosen["path"]}, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/Medevio/86 Agenda měsíc dopředu.py b/Medevio/86 Agenda měsíc dopředu.py new file mode 100644 index 0000000..6ffdd63 --- /dev/null +++ b/Medevio/86 Agenda měsíc dopředu.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Query Medevio for the full agenda of 17 Oct 2025, +print raw API response, and export to Excel. +""" + +import json +import time +from pathlib import Path +import requests +import pandas as pd +from openpyxl import load_workbook +from openpyxl.styles import Font, Alignment +from openpyxl.utils import get_column_letter +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta # přidá měsíce správně +from Functions import get_reports_folder + +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +CALENDAR_ID = "144c4e12-347c-49ca-9ec0-8ca965a4470d" +CLINIC_SLUG = "mudr-buzalkova" + +# ==================== Load Token ==================== +def load_gateway_token(storage_path="medevio_storage.json"): + """Return Medevio gateway-access-token from saved Playwright storage.""" + from pathlib import Path + + path = Path(storage_path) + if not path.exists(): + raise SystemExit(f"❌ Storage file not found: {path}") + + with path.open("r", encoding="utf-8") as f: + state = json.load(f) + + token = next( + (c["value"] for c in state["cookies"] + if c["name"] == "gateway-access-token"), None + ) + + if not token: + raise SystemExit("❌ gateway-access-token not found in storage file.") + + return token + + +gateway_token = load_gateway_token() + +headers = { + "content-type": "application/json", + "origin": "https://my.medevio.cz", + "referer": "https://my.medevio.cz/", + "authorization": f"Bearer {gateway_token}", +} +# === Dynamické datumy === +dnes = datetime.utcnow().date() +since = datetime.combine(dnes, datetime.min.time()).replace(microsecond=1) +until = since + relativedelta(months=1) - timedelta(milliseconds=1) + +# ISO formát s "Z" (UTC) +since_iso = since.isoformat() + "Z" +until_iso = until.isoformat() + "Z" + +payload = { + "operationName": "ClinicAgenda_ListClinicReservations", + "variables": { + "calendarIds": [CALENDAR_ID], + "clinicSlug": CLINIC_SLUG, + "since": since_iso, + "until": until_iso, + "locale": "cs", + "emptyCalendarIds": False, + }, + "query": """query ClinicAgenda_ListClinicReservations( + $calendarIds: [UUID!], + $clinicSlug: String!, + $locale: Locale!, + $since: DateTime!, + $until: DateTime!, + $emptyCalendarIds: Boolean! + ) { + reservations: listClinicReservations( + clinicSlug: $clinicSlug, + calendarIds: $calendarIds, + since: $since, + until: $until + ) @skip(if: $emptyCalendarIds) { + id + start + end + note + done + color + request { + id + displayTitle(locale: $locale) + extendedPatient { + name + surname + dob + insuranceCompanyObject { shortName } + } + } + } + }""", +} + +print("since:", since_iso) +print("until:", until_iso) +# ==================== Query API ==================== +print("📡 Querying Medevio API for agenda...") +r = requests.post(GRAPHQL_URL, headers=headers, data=json.dumps(payload)) +print("Status:", r.status_code) + +try: + data = r.json() +except Exception as e: + print("❌ Could not parse JSON:", e) + print(r.text) + raise SystemExit() + +if "data" not in data or "reservations" not in data["data"]: + raise SystemExit("⚠️ No 'reservations' data found in response.") + +reservations = data["data"]["reservations"] +from datetime import datetime +from dateutil import parser, tz + +# ===== Process reservations into table ===== +rows = [] +for r in reservations: + req = r.get("request") or {} + patient = req.get("extendedPatient") or {} + insurance = patient.get("insuranceCompanyObject") or {} + + # parse datetimes (convert to local time) + try: + start_dt = parser.isoparse(r.get("start")).astimezone(tz.gettz("Europe/Prague")) + end_dt = parser.isoparse(r.get("end")).astimezone(tz.gettz("Europe/Prague")) + except Exception: + start_dt = end_dt = None + + date_str = start_dt.strftime("%Y-%m-%d") if start_dt else "" + time_interval = f"{start_dt.strftime('%H:%M')}-{end_dt.strftime('%H:%M')}" if start_dt and end_dt else "" + + rows.append({ + "Date": date_str, + "Time": time_interval, + "Title": req.get("displayTitle") or "", + "Patient": f"{patient.get('surname','')} {patient.get('name','')}".strip(), + "DOB": patient.get("dob") or "", + "Insurance": insurance.get("shortName") or "", + "Note": r.get("note") or "", + "Color": r.get("color") or "", + "Request_ID": req.get("id") or "", + "Reservation_ID": r.get("id"), + }) + +df = pd.DataFrame(rows).sort_values(["Date", "Time"]) + +# ===== Excel export ===== +EXPORT_DIR = Path(get_reports_folder()) +# EXPORT_DIR = Path(r"C:\Users\vlado\PycharmProjects\Medevio\exports") +EXPORT_DIR.mkdir(exist_ok=True) +timestamp = time.strftime("%Y-%m-%d %H-%M-%S") +xlsx_path = EXPORT_DIR / f"Medevio_agenda_{timestamp}.xlsx" + +# remove old files +for old in EXPORT_DIR.glob("Medevio_agenda_*.xlsx"): + try: + old.unlink() + except Exception: + pass + +df.to_excel(xlsx_path, index=False) +wb = load_workbook(xlsx_path) +ws = wb.active + +# === Apply styling and custom column widths === +widths = { + 1: 11, # A - Date + 2: 13, # B - Time + 3: 45, # C - Title + 4: 30, # D - Patient + 5: 15, # E - DOB + 6: 15, # F - Insurance + 7: 30, # G - Note + 8: 15, # H - Color + 9: 37, # I - Request_ID + 10: 37 # J - Reservation_ID +} + +for col_idx in range(1, len(df.columns) + 1): + col_letter = get_column_letter(col_idx) + c = ws.cell(row=1, column=col_idx) + c.font = Font(bold=True) + c.alignment = Alignment(horizontal="center") + ws.column_dimensions[col_letter].width = widths.get(col_idx, 20) + +ws.freeze_panes = "A2" + +wb.save(xlsx_path) +print(f"📘 Exported clean agenda view to:\n{xlsx_path}") diff --git a/Medevio/Emailtest/10 EmailTest.py b/Medevio/Emailtest/10 EmailTest.py new file mode 100644 index 0000000..14754f9 --- /dev/null +++ b/Medevio/Emailtest/10 EmailTest.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import smtplib +from email.message import EmailMessage + +# ========= CONFIG ========= +SMTP_SERVER = "smtp.office365.com" +SMTP_PORT = 587 +EMAIL_FROM = "ordinace@buzalkova.cz" +EMAIL_TO = "vladimir.buzalka@buzalka.cz" +SMTP_USER = "ordinace@buzalkova.cz" +SMTP_PASS = "********" # <- your Office365 APP PASSWORD (see note below) +# ========================== + +# Create the email +msg = EmailMessage() +msg["Subject"] = "Test zpráva z Pythonu" +msg["From"] = EMAIL_FROM +msg["To"] = EMAIL_TO +msg.set_content("Dobrý den,\n\ntoto je testovací e-mail odeslaný z Python skriptu.\n\n--\nOrdinace MUDr. Buzalková") + +# Send the email +with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: + server.starttls() # enable TLS encryption + server.login(SMTP_USER, SMTP_PASS) + server.send_message(msg) + +print("✅ E-mail byl úspěšně odeslán!") diff --git a/Medevio/Functions.py b/Medevio/Functions.py new file mode 100644 index 0000000..8c5cc9e --- /dev/null +++ b/Medevio/Functions.py @@ -0,0 +1,98 @@ +import socket,fdb,pymysql +from pymysql.cursors import DictCursor + +import pymysql +from pymysql.cursors import DictCursor +import socket + +def get_path_ciselniky(): + hostname = socket.gethostname().strip().upper() + if hostname in ("NTBVBHP470G10", "Z230"): + return r"u:\Dropbox\!!!Days\Downloads Z230\Pracuji_na\Import" + elif hostname == "SESTRA": + return r"z:\Dropbox\!!!Days\Downloads Z230\Pracuji_na\Import" + else: + print(f"Unknown host: {hostname}") + return None + +def get_reports_folder(): + hostname = socket.gethostname().strip().upper() + if hostname in ("NTBVBHP470G10", "Z230"): + return r"u:\Dropbox\!!!Days\Downloads Z230" + elif hostname in ["SESTRA","POHODA","LEKAR"]: + return r"z:\Dropbox\Ordinace\Reporty" + else: + print(f"Unknown host: {hostname}") + return None + +def get_mysql_connection(cursor_mode=None): + """ + Return a PyMySQL connection. + If cursor_mode == "DICT", return connection with DictCursor. + Otherwise, return default tuple cursor connection. + """ + hostname = socket.gethostname().strip() + + # decide cursor class + cursor_cls = DictCursor if cursor_mode == "DICT" else None + + if hostname in ("NTBVBHP470G10", "Z230"): + MYSQL_CFG = dict( + host="192.168.1.76", + port=3307, + user="root", + password="Vlado9674+", + database="medevio", + autocommit=True, + ) + elif hostname == "SESTRA": + MYSQL_CFG = dict( + host="127.0.0.1", + port=3307, + user="root", + password="Vlado9674+", + database="medevio", + autocommit=True, + ) + else: + print(f"Unknown host: {hostname}") + return None + + # include cursorclass only if we want a dict cursor + if cursor_cls is not None: + MYSQL_CFG["cursorclass"] = cursor_cls + + try: + return pymysql.connect(**MYSQL_CFG) + except pymysql.MySQLError as e: + print(f"MySQL connection failed: {e}") + return None + + +def get_medicus_connection(): + """ + Attempt to create a Firebird connection to the Medicus database. + Returns: + fdb.Connection object on success + None on failure + """ + if socket.gethostname().strip() in ("NTBVBHP470G10","Z230"): + MEDICUS_CFG = dict( + dsn=r"192.168.1.4:z:\medicus 3\data\medicus.fdb", + user="SYSDBA", + password="masterkey", + charset="win1250", + ) + elif socket.gethostname().strip()=="SESTRA": + MEDICUS_CFG = dict( + dsn=r"192.168.1.10:m:\medicus\data\medicus.fdb", + user="SYSDBA", + password="masterkey", + charset="win1250", + ) + + try: + return fdb.connect(**MEDICUS_CFG) + except fdb.fbcore.DatabaseError as e: + print(f"Medicus DB connection failed: {e}") + return None \ No newline at end of file diff --git a/Medevio/Knihovny/__init__.py b/Medevio/Knihovny/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Medevio/Knihovny/najdi_dropbox.py b/Medevio/Knihovny/najdi_dropbox.py new file mode 100644 index 0000000..a6444d7 --- /dev/null +++ b/Medevio/Knihovny/najdi_dropbox.py @@ -0,0 +1,62 @@ +import winreg +import json +import os + + +def get_dropbox_root() -> str: + """ + Vrátí kořenovou cestu složky Dropbox na tomto počítači. + + Dropbox může být nainstalován na různých discích (C:, U:, Z: …), + ale struktura složek uvnitř zůstává vždy stejná. Tato funkce zjistí + aktuální umístění, takže ostatní skripty nemusí cestu napevno zadávat. + + Postup hledání (v tomto pořadí): + 1. Registr HKCU\\Software\\Dropbox\\ks — hlavní klíč, hodnota "Personal" + je uložena jako byte array v kódování UTF-16 LE. + 2. Registr HKCU\\Software\\Dropbox\\ks1 — alternativní klíč používaný + novějšími verzemi klienta Dropbox. + 3. Soubor info.json v %APPDATA%\\Dropbox\\ nebo %LOCALAPPDATA%\\Dropbox\\ + — záložní metoda, pokud registr cestu neobsahuje. + + Vrací: + str: Absolutní cesta ke kořenové složce Dropboxu, např. "U:\\Dropbox". + + Vyvolá: + RuntimeError: Pokud se cestu nepodaří zjistit žádnou z metod. + + Příklad použití: + from Knihovny.najdi_dropbox import get_dropbox_root + import os + + ROOT = get_dropbox_root() + PACIENTI = os.path.join(ROOT, "Ordinace", "Pacienti") + """ + + # Metoda 1 a 2: registr HKCU\Software\Dropbox\ks a ks1 + for subkey in (r"Software\Dropbox\ks", r"Software\Dropbox\ks1"): + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, subkey) as key: + value, _ = winreg.QueryValueEx(key, "Personal") + path = bytes(value).decode("utf-16-le").rstrip("\x00") + if path: + return path + except (OSError, UnicodeDecodeError): + continue + + # Metoda 3: záložní — info.json v AppData + for base in (os.getenv("APPDATA", ""), os.getenv("LOCALAPPDATA", "")): + info_path = os.path.join(base, "Dropbox", "info.json") + if os.path.isfile(info_path): + with open(info_path, encoding="utf-8") as f: + info = json.load(f) + path = (info.get("personal") or info.get("business") or {}).get("path", "") + if path: + return path + + raise RuntimeError("Nepodařilo se zjistit cestu k Dropboxu.") + + +if __name__ == "__main__": + root = get_dropbox_root() + print(f"Dropbox root: {root}") diff --git a/Medevio/Knihovny/najdi_medicus.py b/Medevio/Knihovny/najdi_medicus.py new file mode 100644 index 0000000..e0b4543 --- /dev/null +++ b/Medevio/Knihovny/najdi_medicus.py @@ -0,0 +1,49 @@ +import socket +from dataclasses import dataclass + + +@dataclass +class MedicusConfig: + server: str + path: str + + @property + def dsn(self) -> str: + return f"{self.server}:{self.path}" + + +def get_medicus_config() -> MedicusConfig: + """ + Vrátí konfiguraci připojení k databázi Medicus podle jména počítače. + + Logika: + - LEKAR → localhost, M:\\Medicus\\Data\\Medicus.fdb (ostrý lokální) + - SESTRA → 192.168.1.10, M:\\Medicus\\Data\\Medicus.fdb (ostrý přes síť) + - LENOVO → 192.168.1.10, M:\\Medicus\\Data\\Medicus.fdb (testovací přes síť) + - ostatní → localhost, C:\\Medicus 3\\Data\\Medicus.fdb (testovací lokální) + + Vrací: + MedicusConfig s atributy server, path a vlastností dsn ("server:path"). + + Příklad použití: + from Knihovny.najdi_medicus import get_medicus_config + + cfg = get_medicus_config() + con = fdb.connect(dsn=cfg.dsn, user="SYSDBA", password="masterkey") + """ + hostname = socket.gethostname().upper() + + if hostname == "LEKAR": + return MedicusConfig(server="localhost", path=r"M:\Medicus\Data\Medicus.fdb") + elif hostname in ("SESTRA", "LENOVO"): + return MedicusConfig(server="192.168.1.10", path=r"M:\Medicus\Data\Medicus.fdb") + else: + return MedicusConfig(server="localhost", path=r"C:\Medicus 3\Data\Medicus.fdb") + + +if __name__ == "__main__": + cfg = get_medicus_config() + print(f"Hostname : {socket.gethostname()}") + print(f"Server : {cfg.server}") + print(f"Path : {cfg.path}") + print(f"DSN : {cfg.dsn}") diff --git a/Medevio/Testy/01 Test b/Medevio/Testy/01 Test new file mode 100644 index 0000000..9da1805 --- /dev/null +++ b/Medevio/Testy/01 Test @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Stáhne konverzaci pro požadavky, kde: +messagesProcessed IS NULL OR messagesProcessed < updatedAt. + +Vloží do medevio_conversation a přílohy do medevio_downloads. +""" + +import zlib +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time + +# ============================== +# 🔧 CONFIGURATION +# ============================== +TOKEN_PATH = Path("token.txt") + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY_MESSAGES = r""" +query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) { + messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) { + id + createdAt + updatedAt + readAt + text + type + sender { + id + name + surname + clinicId + } + medicalRecord { + id + description + contentType + url + downloadUrl + token + createdAt + updatedAt + } + } +} +""" + +# ============================== +# ⏱ DATETIME PARSER +# ============================== +def parse_dt(s): + if not s: + return None + try: + return datetime.fromisoformat(s.replace("Z", "+00:00")) + except: + pass + try: + return datetime.strptime(s[:19], "%Y-%m-%dT%H:%M:%S") + except: + return None + +# ============================== +# 🔐 TOKEN +# ============================== +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + return tok.replace("Bearer ", "") + +# ============================== +# 📡 FETCH MESSAGES +# ============================== +def fetch_messages(headers, request_id): + payload = { + "operationName": "UseMessages_ListMessages", + "query": GRAPHQL_QUERY_MESSAGES, + "variables": {"requestId": request_id, "updatedSince": None}, + } + + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + if r.status_code != 200: + print("❌ HTTP", r.status_code, "for request", request_id) + return [] + + try: + data = r.json() + except Exception as e: + print(f"❌ Failed to parse JSON for {request_id}: {e}") + print(" Response text:", r.text[:500]) + return [] + + messages = data.get("data", {}).get("messages", []) or [] + print(f" 🌐 API returned {len(messages)} messages for {request_id}") + return messages + + + +# ============================== +# 💾 SAVE MESSAGE +# ============================== +def insert_message(cur, req_id, msg): + + sender = msg.get("sender") or {} + sender_name = " ".join( + x for x in [sender.get("name"), sender.get("surname")] if x + ) or None + + sql = """ + INSERT INTO medevio_conversation ( + id, request_id, + sender_name, sender_id, sender_clinic_id, + text, created_at, read_at, updated_at, + attachment_url, attachment_description, attachment_content_type + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + sender_name = VALUES(sender_name), + sender_id = VALUES(sender_id), + sender_clinic_id = VALUES(sender_clinic_id), + text = VALUES(text), + created_at = VALUES(created_at), + read_at = VALUES(read_at), + updated_at = VALUES(updated_at), + attachment_url = VALUES(attachment_url), + attachment_description = VALUES(attachment_description), + attachment_content_type = VALUES(attachment_content_type) + """ + + mr = msg.get("medicalRecord") or {} + + cur.execute(sql, ( + msg.get("id"), + req_id, + sender_name, + sender.get("id"), + sender.get("clinicId"), + msg.get("text"), + parse_dt(msg.get("createdAt")), + parse_dt(msg.get("readAt")), + parse_dt(msg.get("updatedAt")), + mr.get("downloadUrl") or mr.get("url"), + mr.get("description"), + mr.get("contentType") + )) + + +# ============================== +# 💾 DOWNLOAD MESSAGE ATTACHMENT +# ============================== +def insert_download(cur, req_id, msg, existing_ids): + + mr = msg.get("medicalRecord") or {} + attachment_id = mr.get("id") + if not attachment_id: + return + + if attachment_id in existing_ids: + return # skip duplicates + + url = mr.get("downloadUrl") or mr.get("url") + if not url: + return + + try: + r = requests.get(url, timeout=30) + r.raise_for_status() + data = r.content + except Exception as e: + print("⚠️ Failed to download:", e) + return + + filename = url.split("/")[-1].split("?")[0] + + cur.execute(""" + INSERT INTO medevio_downloads ( + request_id, attachment_id, attachment_type, + filename, content_type, file_size, created_at, file_content + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + file_content = VALUES(file_content), + file_size = VALUES(file_size), + downloaded_at = NOW() + """, ( + req_id, + attachment_id, + "MESSAGE_ATTACHMENT", + filename, + mr.get("contentType"), + len(data), + parse_dt(msg.get("createdAt")), + data + )) + + existing_ids.add(attachment_id) + + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + # ---- Load existing attachments + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {row["attachment_id"] for row in cur.fetchall()} + + print(f"📦 Already downloaded attachments: {len(existing_ids)}\n") + + # ---- Select 10 oldest pozadavky (regardless of messagesProcessed) + sql = """ + SELECT id + FROM pozadavky + ORDER BY updatedAt ASC + LIMIT 10 + """ + with conn.cursor() as cur: + cur.execute(sql) + requests_to_process = cur.fetchall() + + print(f"📋 Will process {len(requests_to_process)} oldest pozadavků.\n") + + + # ---- Process each pozadavek + for idx, row in enumerate(requests_to_process, 1): + req_id = row["id"] + print(f"[{idx}/{len(requests_to_process)}] Processing {req_id} …") + + messages = fetch_messages(headers, req_id) + + with conn.cursor() as cur: + for msg in messages: + insert_message(cur, req_id, msg) + insert_download(cur, req_id, msg, existing_ids) + conn.commit() + + with conn.cursor() as cur: + cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + + print(f" ✅ {len(messages)} messages saved\n") + time.sleep(0.25) + + conn.close() + print("🎉 Done!") + + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/07VytvorPozadavekChripka.py b/Medevio/Testy/07VytvorPozadavekChripka.py new file mode 100644 index 0000000..29b2f70 --- /dev/null +++ b/Medevio/Testy/07VytvorPozadavekChripka.py @@ -0,0 +1,125 @@ +#fcb2414b-067b-4ca2-91b2-6c36a86d4cbb = Vladimir Buzalka +#0210db7b-8fb0-4b47-b1d8-ec7a10849a63 = Vladko - testovací aplikace + +#tento kód otevře pacienta podle jeho UUID a založí mu požadavek chřipka a finito + +from pathlib import Path +from datetime import datetime +from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout +import time + +STATE_FILE = Path("../medevio_storage.json") +PATIENT_UUID = "0210db7b-8fb0-4b47-b1d8-ec7a10849a63" +PATIENT_URL = f"https://my.medevio.cz/mudr-buzalkova/klinika/pacienti?pacient={PATIENT_UUID}" +MESSAGE_TEXT = "Dobrý den, vakcína proti chřipce je k dispozici, zítra (úterý 23.9) budeme očkovat od 13-17 hodin, prosím potvrďte jestli můžete přijít a jaký čas se Vám hodí." + +def savepage(name: str, page): + """ + Save the current HTML of a Playwright Page to + U:\Dropbox\!!!Days\Downloads Z230\Pages\.html + """ + folder = Path(r"U:\Dropbox\!!!Days\Downloads Z230\Pages") + folder.mkdir(parents=True, exist_ok=True) # ensure the folder exists + + # create sortable timestamp like 2025-09-19_14-05-33 + ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + + filepath = folder / f"{ts}_{name}.html" + with filepath.open("w", encoding="utf-8") as f: + f.write(page.content()) + print(f"Saved page snapshot to {filepath}") + +def main(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=False, slow_mo=200) + context = browser.new_context(storage_state=str(STATE_FILE)) + + # ---- keep a stable reference to the patient card page ---- + ptcard = context.new_page() + ptcard.goto(PATIENT_URL, wait_until="networkidle") + #saving ptcard1 + # savepage("ptcard1",ptcard) + + ptcard.get_by_text("Historie požadavků").wait_for(timeout=15_000) + + # 1) Create new request on the patient card + ptcard.get_by_role("button", name="Nový požadavek").click() + ptcard.wait_for_timeout(300) # small settle + + # cursor is already in the "Začněte psát…" field + ptcard.keyboard.type("očkování - chřipka") + ptcard.locator("[role='option']", has_text="Očkování - Chřipka").first.click() + ptcard.get_by_role("button", name="Vytvořit požadavek").click() + + #saving ptcard1 + # savepage("ptcard2",ptcard) + + # 2) Ensure we are back on the patient card again + # (some UIs rerender; either way we want a fresh list) + try: + ptcard.get_by_text("Historie požadavků").wait_for(timeout=7_000) + except PWTimeout: + # If for any reason we are not on the card, navigate back explicitly + ptcard.goto(PATIENT_URL, wait_until="networkidle") + ptcard.get_by_text("Historie požadavků").wait_for(timeout=10_000) + + # Optional: hard refresh to get the just-created request at the top + ptcard.reload(wait_until="networkidle") + ptcard.get_by_text("Historie požadavků").wait_for(timeout=10_000) + time.sleep(5) + + # 3) Open the “Očkování – Chřipka …” request card by its H4 text + # (click the whole card container, not just the heading) + try: + # wait until at least one request card is rendered + ptcard.locator("div[data-testid='patient-request-item']").first.wait_for(timeout=10_000) + + # locate the specific card that contains the H4 with "Očkování - Chřipka" + chripka_card = ptcard.locator("div[data-testid='patient-request-item']").filter( + has=ptcard.locator("h4:has-text('Očkování - Chřipka')") + ).first + + # ensure it's attached/visible then click it + chripka_card.wait_for(timeout=10_000) + chripka_card.click(timeout=5_000) + + except Exception as e: + # Fallback: click the very first card on the list (newest) + try: + first_card = ptcard.locator("div[data-testid='patient-request-item']").first + first_card.click(timeout=5_000) + except Exception: + # if even that fails, save snapshot for inspection and raise + savepage("ptcard_click_fail", ptcard) + raise + + # 4) Wait for request detail and send the message + # We’re now on the detail page + try: + ptcard.wait_for_url("**/pozadavky?pozadavek=*", timeout=10_000) + except PWTimeout: + pass # URL may be SPA; rely on textarea presence + + ptcard.get_by_placeholder("Napište odpověď").wait_for(timeout=10_000) + ptcard.get_by_placeholder("Napište odpověď").fill(MESSAGE_TEXT) + + sent = False + for sel in ["button:has-text('Odeslat')", + "button:has-text('Odeslat zprávu')", + "button:has-text('Odeslat SMS')", + "button:has-text('Odeslat do aplikace')"]: + try: + ptcard.click(sel, timeout=4000) + sent = True + break + except Exception: + continue + + if not sent: + raise RuntimeError("Nepodařilo se najít/kliknout tlačítko Odeslat.") + + ptcard.wait_for_timeout(2000) + print("✅ Požadavek vytvořen, otevřen a zpráva odeslána.") + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/07VytvorPozadavekChripka01.py b/Medevio/Testy/07VytvorPozadavekChripka01.py new file mode 100644 index 0000000..724e356 --- /dev/null +++ b/Medevio/Testy/07VytvorPozadavekChripka01.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from pathlib import Path +from datetime import datetime +import re +import time +import pymysql +from pymysql.cursors import DictCursor +from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout + +# ===================================================== +STATE_FILE = Path("../medevio_storage.json") # saved login state from your login script + +MYSQL_CFG = dict( + host="192.168.1.76", + port=3307, + user="root", + password="Vlado9674+", + database="medevio", + cursorclass=DictCursor, + autocommit=False, +) + +UUID_COLUMN = "rid" # column with Medevio UUID +FLAG_COLUMN = "pozchripkavytvoren" # bool flag we update after success +FLAG_TS_COL = "pozchripka_vytv_at" # optional timestamp when request created + +MESSAGE_TEXT = ( + "Dobrý den, vakcína proti chřipce je k dispozici, " + "zítra (úterý 23.9) budeme očkovat od 13-17 hodin, " + "prosím potvrďte jestli můžete přijít a jaký čas se Vám hodí." +) +PATIENT_URL_TMPL = "https://my.medevio.cz/mudr-buzalkova/klinika/pacienti?pacient={uuid}" +RC_DIGITS = re.compile(r"\D+") +# ===================================================== + +def normalize_rc(rc: str) -> str: + """Return digits-only RC (removes slash/spaces).""" + return RC_DIGITS.sub("", rc or "") + +def ensure_flag_columns(conn): + """Create required columns if missing (works for all MySQL/MariaDB).""" + needed = { + FLAG_COLUMN: "TINYINT(1) NULL", + FLAG_TS_COL: "DATETIME NULL", + } + with conn.cursor() as cur: + for col, coldef in needed.items(): + cur.execute(""" + SELECT COUNT(*) AS cnt + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'patients_extracted' + AND COLUMN_NAME = %s + """, (col,)) + if cur.fetchone()["cnt"] == 0: + ddl = f"ALTER TABLE `patients_extracted` ADD COLUMN `{col}` {coldef}" + print("Adding column:", ddl) + cur.execute(ddl) + conn.commit() + +def fetch_uuid_by_rc(conn, rc_digits: str) -> dict | None: + """ + Return row with rid (primary key), medevio UUID, jmeno, prijmeni for the given RC. + Prints the query and parameter for debugging. + """ + sql = ( + f"SELECT rid, `{UUID_COLUMN}` AS uuid, jmeno, prijmeni, rc " + "FROM patients_extracted " + "WHERE REPLACE(REPLACE(rc,'/',''),' ','') = %s " + "LIMIT 1" + ) + print("DEBUG SQL:", sql, "| param:", rc_digits) + + with conn.cursor() as cur: + cur.execute(sql, (rc_digits,)) + row = cur.fetchone() + + print("DEBUG result:", row) + return row + +def mark_flag_success(conn, rid: str): + """Update the flag once the Medevio request is created.""" + with conn.cursor() as cur: + cur.execute( + f"UPDATE patients_extracted " + f"SET {FLAG_COLUMN}=1, {FLAG_TS_COL}=NOW() " + f"WHERE rid=%s", + (rid,) + ) + conn.commit() + +def create_flu_request_for_uuid(uuid: str) -> bool: + """Automates Medevio UI to create 'Očkování - Chřipka' request and send MESSAGE_TEXT.""" + with sync_playwright() as p: + browser = p.chromium.launch(headless=False, slow_mo=200) + context = browser.new_context(storage_state=str(STATE_FILE)) + ptcard = context.new_page() + + url = PATIENT_URL_TMPL.format(uuid=uuid) + ptcard.goto(url, wait_until="networkidle") + + # ensure patient card loaded + ptcard.get_by_text("Historie požadavků").wait_for(timeout=15_000) + + # create new request + ptcard.get_by_role("button", name="Nový požadavek").click() + ptcard.wait_for_timeout(300) + ptcard.keyboard.type("očkování - chřipka") + ptcard.locator("[role='option']", has_text="Očkování - Chřipka").first.click() + ptcard.get_by_role("button", name="Vytvořit požadavek").click() + time.sleep(5) + # wait until back on card + try: + ptcard.get_by_text("Historie požadavků").wait_for(timeout=7_000) + except PWTimeout: + ptcard.goto(url, wait_until="networkidle") + ptcard.get_by_text("Historie požadavků").wait_for(timeout=10_000) + + ptcard.reload(wait_until="networkidle") + ptcard.get_by_text("Historie požadavků").wait_for(timeout=10_000) + time.sleep(2) + + # open the new request + try: + ptcard.locator("div[data-testid='patient-request-item']").first.wait_for(timeout=10_000) + chripka_card = ptcard.locator("div[data-testid='patient-request-item']").filter( + has=ptcard.locator("h4:has-text('Očkování - Chřipka')") + ).first + chripka_card.click(timeout=5_000) + except Exception: + ptcard.locator("div[data-testid='patient-request-item']").first.click(timeout=5_000) + + # send the message + try: + ptcard.wait_for_url("**/pozadavky?pozadavek=*", timeout=10_000) + except PWTimeout: + pass + ptcard.get_by_placeholder("Napište odpověď").wait_for(timeout=10_000) + ptcard.get_by_placeholder("Napište odpověď").fill(MESSAGE_TEXT) + + for sel in [ + "button:has-text('Odeslat')", + "button:has-text('Odeslat zprávu')", + "button:has-text('Odeslat SMS')", + "button:has-text('Odeslat do aplikace')", + ]: + try: + ptcard.click(sel, timeout=4000) + browser.close() + return True + except Exception: + continue + + browser.close() + return False + +def main(): + rc_input = input("Zadejte RC (s/bez lomítka, Enter pro konec): ").strip() + # rc_input="320312460" + if not rc_input: + print("Konec.") + return + rc = normalize_rc(rc_input) + + conn = pymysql.connect(**MYSQL_CFG) + try: + ensure_flag_columns(conn) + + row = fetch_uuid_by_rc(conn, rc) + if not row or not row.get("uuid"): + print(f"✗ Pacient s RC {rc} nenalezen nebo nemá sloupec {UUID_COLUMN}.") + return + + print(f"→ Nalezen: {row.get('prijmeni','')} {row.get('jmeno','')} " + f"| RC {row.get('rc','')} | UUID {row['uuid']} | rid {row['rid']}") + + if create_flu_request_for_uuid(row["uuid"]): + mark_flag_success(conn, row["rid"]) + print("✅ Požadavek chřipka vytvořen a DB aktualizována.") + else: + print("✗ Nepodařilo se odeslat zprávu v požadavku.") + + finally: + conn.close() + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/07VytvorPozadavekChripka02majiucet.py b/Medevio/Testy/07VytvorPozadavekChripka02majiucet.py new file mode 100644 index 0000000..2a3c896 --- /dev/null +++ b/Medevio/Testy/07VytvorPozadavekChripka02majiucet.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from pathlib import Path +import re +import time +import pymysql +from pymysql.cursors import DictCursor +from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout + +# ================== CONFIG ================== +STATE_FILE = Path("../medevio_storage.json") + +MYSQL_CFG = dict( + host="192.168.1.76", + port=3307, + user="root", + password="Vlado9674+", + database="medevio", + cursorclass=DictCursor, + autocommit=False, +) + +# Column that goes into the Medevio URL. +# If your Medevio patient UUID is stored in a different column, change this: +UUID_COLUMN = "rid" # Medevio UUID in your table +FLAG_COLUMN = "pozchripkavytvoren" # set to 1 on success +FLAG_TS_COL = "pozchripka_vytv_at" # timestamp when created + +MESSAGE_TEXT = ( + "Dobrý den, vakcína proti chřipce je k dispozici, " + "dnes (úterý 23.9) budeme očkovat od 13-17 hodin, " + "prosím, otevřete si tento požadavek a vyberte si termín. Můžete si samozřejmě vybrat i kterýkoliv jiný den, ale hromadně očkujeme další 4 úterky. Další 4 úterky najdete spoustu termínů." +) + +PATIENT_URL_TMPL = "https://my.medevio.cz/mudr-buzalkova/klinika/pacienti?pacient={uuid}" +BATCH_LIMIT = 2 # change if you want to limit how many to process in one run +PAUSE_BETWEEN = 1.0 # seconds between patients (UI courtesy) +# =========================================== + +RC_DIGITS = re.compile(r"\D+") + +def ensure_flag_columns(conn): + """Create required columns if missing (portable).""" + needed = { + FLAG_COLUMN: "TINYINT(1) NULL", + FLAG_TS_COL: "DATETIME NULL", + } + with conn.cursor() as cur: + for col, coldef in needed.items(): + cur.execute(""" + SELECT COUNT(*) AS cnt + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'patients_extracted' + AND COLUMN_NAME = %s + """, (col,)) + if cur.fetchone()["cnt"] == 0: + cur.execute(f"ALTER TABLE `patients_extracted` ADD COLUMN `{col}` {coldef}") + conn.commit() + +def fetch_batch(conn): + """ + Get patients where: + - flu_reply = 'ano' + - mamedevioucet is true-ish + - rc starts with '8' (after removing slash/spaces) + - pozchripkavytvoren is NULL + - uuid column is present + """ + sql = f""" + SELECT + rid, jmeno, prijmeni, rc, + `{UUID_COLUMN}` AS uuid + FROM patients_extracted + WHERE flu_reply = 'ano' + AND (mamedevioucet = 1 OR mamedevioucet = TRUE OR mamedevioucet = '1') + AND REPLACE(REPLACE(rc,'/',''),' ','') LIKE '7%%' + AND {FLAG_COLUMN} IS NULL + AND `{UUID_COLUMN}` IS NOT NULL + AND `{UUID_COLUMN}` <> '' + ORDER BY prijmeni, jmeno + LIMIT %s + """ + with conn.cursor() as cur: + cur.execute(sql, (BATCH_LIMIT,)) + return cur.fetchall() + +def mark_flag_success(conn, rid: str): + with conn.cursor() as cur: + cur.execute( + f"UPDATE patients_extracted " + f"SET {FLAG_COLUMN}=1, {FLAG_TS_COL}=NOW() " + f"WHERE rid=%s", + (rid,) + ) + conn.commit() + +def create_flu_request_for_uuid(uuid: str) -> bool: + """Automate Medevio UI for one patient: create 'Očkování - Chřipka' and send MESSAGE_TEXT.""" + with sync_playwright() as p: + browser = p.chromium.launch(headless=False, slow_mo=200) + context = browser.new_context(storage_state=str(STATE_FILE)) + ptcard = context.new_page() + + url = PATIENT_URL_TMPL.format(uuid=uuid) + ptcard.goto(url, wait_until="networkidle") + + # ensure patient card loaded + ptcard.get_by_text("Historie požadavků").wait_for(timeout=15_000) + + # create new request + ptcard.get_by_role("button", name="Nový požadavek").click() + ptcard.wait_for_timeout(300) + ptcard.keyboard.type("očkování - chřipka") + ptcard.locator("[role='option']", has_text="Očkování - Chřipka").first.click() + ptcard.get_by_role("button", name="Vytvořit požadavek").click() + time.sleep(2) + # # wait until back on card + # try: + # ptcard.get_by_text("Historie požadavků").wait_for(timeout=7_000) + # except PWTimeout: + # ptcard.goto(url, wait_until="networkidle") + # ptcard.get_by_text("Historie požadavků").wait_for(timeout=10_000) + + # ptcard.reload(wait_until="networkidle") + ptcard.get_by_text("Historie požadavků").wait_for(timeout=10_000) + time.sleep(2) + + # open the new request + try: + ptcard.locator("div[data-testid='patient-request-item']").first.wait_for(timeout=10_000) + chripka_card = ptcard.locator("div[data-testid='patient-request-item']").filter( + has=ptcard.locator("h4:has-text('Očkování - Chřipka')") + ).first + chripka_card.click(timeout=5_000) + except Exception: + ptcard.locator("div[data-testid='patient-request-item']").first.click(timeout=5_000) + + # send the message + # try: + # ptcard.wait_for_url("**/pozadavky?pozadavek=*", timeout=10_000) + # except PWTimeout: + # pass + ptcard.get_by_placeholder("Napište odpověď").wait_for(timeout=10_000) + ptcard.get_by_placeholder("Napište odpověď").fill(MESSAGE_TEXT) + time.sleep(2) + for sel in [ + "button:has-text('Odeslat')", + "button:has-text('Odeslat zprávu')", + "button:has-text('Odeslat SMS')", + "button:has-text('Odeslat do aplikace')", + ]: + try: + ptcard.click(sel, timeout=4000) + browser.close() + return True + except Exception: + continue + + browser.close() + return False + +def main(): + conn = pymysql.connect(**MYSQL_CFG) + try: + ensure_flag_columns(conn) + + rows = fetch_batch(conn) + if not rows: + print("Nenalezen žádný pacient pro zpracování.") + return + + print(f"Zpracujeme {len(rows)} pacientů…") + + processed = ok = fail = 0 + for r in rows: + processed += 1 + rid = r["rid"] + uuid = r["uuid"] + name = f"{r.get('prijmeni','')}, {r.get('jmeno','')}" + rc = r.get("rc","") + + print(f"[{processed:>3}] {name} | RC {rc} | UUID {uuid}") + + try: + success = create_flu_request_for_uuid(uuid) + if success: + mark_flag_success(conn, rid) + ok += 1 + print(" ✓ vytvořeno + odesláno, DB flag nastaven") + else: + fail += 1 + print(" ✗ nepodařilo se odeslat zprávu (tlačítko 'Odeslat' nenalezeno)") + except Exception as e: + fail += 1 + conn.rollback() + print(f" ✗ chyba: {type(e).__name__}: {e}") + + time.sleep(PAUSE_BETWEEN) + + print(f"Hotovo. processed={processed}, ok={ok}, fail={fail}") + finally: + conn.close() + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/07VytvorPozadavekChripka03Checkexisting.py b/Medevio/Testy/07VytvorPozadavekChripka03Checkexisting.py new file mode 100644 index 0000000..e68c137 --- /dev/null +++ b/Medevio/Testy/07VytvorPozadavekChripka03Checkexisting.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from pathlib import Path +import re +import time +import unicodedata +import pymysql +from pymysql.cursors import DictCursor +from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout, Page + +# ================== CONFIG ================== +STATE_FILE = Path("../medevio_storage.json") + +MYSQL_CFG = dict( + host="192.168.1.76", + port=3307, + user="root", + password="Vlado9674+", + database="medevio", + cursorclass=DictCursor, + autocommit=False, +) + +# Column that goes into the Medevio URL. +# If your Medevio patient UUID is stored in a different column, change this: +UUID_COLUMN = "rid" # Medevio UUID in your table +FLAG_COLUMN = "pozchripkavytvoren" # set to 1 on success +FLAG_TS_COL = "pozchripka_vytv_at" # timestamp when created + +# Optional: set your personal RID here to test on a single card; set to None for batch mode +TEST_RID = None # e.g. "fcb2414b-067b-4ca2-91b2-6c36a86d4cbb" +# TEST_RID = "fcb2414b-067b-4ca2-91b2-6c36a86d4cbb" + +MESSAGE_TEXT = ( + "Dobrý den, vakcína proti chřipce je k dispozici. " + "Vy nemáte účet v Medeviu a tedy si nemůžete vybrat termín, takže to zkusíme udělat manuálně. " + "Hlavní očkovací dny jsou úterý 07/10 a úterý 14/10, kdy očkujeme i COVID, kdo chce. Chřipku samostatně možno i kdykoliv jindy. Tak dejte vědět, jaký termín se Vám hodí a já si to poznamenám." +) + +PATIENT_URL_TMPL = "https://my.medevio.cz/mudr-buzalkova/klinika/pacienti?pacient={uuid}" +BATCH_LIMIT = 50 # change if you want to limit how many to process in one run +PAUSE_BETWEEN = 1.0 # seconds between patients (UI courtesy) +# =========================================== + +RC_DIGITS = re.compile(r"\D+") + +def mark_flag_skipped(conn, rid: str): + """ + Pokud už požadavek na chřipku existuje: + - nastaví pozchripkavytvoren = 1 + - zapíše aktuální čas do pozchripka_vytv_at + """ + with conn.cursor() as cur: + cur.execute( + f"UPDATE patients_extracted " + f"SET {FLAG_COLUMN}=1, {FLAG_TS_COL}=NOW() " + f"WHERE rid=%s", + (rid,) + ) + conn.commit() + +# ---------- DB helpers ---------- +def ensure_flag_columns(conn): + """Create required columns if missing (portable).""" + needed = { + FLAG_COLUMN: "TINYINT(1) NULL", + FLAG_TS_COL: "DATETIME NULL", + } + with conn.cursor() as cur: + for col, coldef in needed.items(): + cur.execute(""" + SELECT COUNT(*) AS cnt + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'patients_extracted' + AND COLUMN_NAME = %s + """, (col,)) + if cur.fetchone()["cnt"] == 0: + cur.execute(f"ALTER TABLE `patients_extracted` ADD COLUMN `{col}` {coldef}") + conn.commit() + + +def fetch_batch(conn): + """ + Batch mode (TEST_RID is None): + - flu_reply = 'ano' + - mamedevioucet true-ish + - rc starts with '7' (after removing slash/spaces) <-- keep/adjust as you need + - pozchripkavytvoren is NULL + - uuid column present + Test mode (TEST_RID set): returns only that rid. + """ + if TEST_RID: + sql = f""" + SELECT + rid, jmeno, prijmeni, rc, + `{UUID_COLUMN}` AS uuid + FROM patients_extracted + WHERE rid = %s + LIMIT 1 + """ + with conn.cursor() as cur: + cur.execute(sql, (TEST_RID,)) + return cur.fetchall() + + sql = f""" + SELECT + rid, jmeno, prijmeni, rc, + `{UUID_COLUMN}` AS uuid + FROM patients_extracted + WHERE flu_reply = 'ano' + AND mamedevioucet = 0 + AND {FLAG_COLUMN} IS NULL + AND `{UUID_COLUMN}` IS NOT NULL + AND `{UUID_COLUMN}` <> '' + ORDER BY prijmeni, jmeno + LIMIT %s + """ + with conn.cursor() as cur: + cur.execute(sql, (BATCH_LIMIT,)) + return cur.fetchall() + + +def mark_flag_success(conn, rid: str): + with conn.cursor() as cur: + cur.execute( + f"UPDATE patients_extracted " + f"SET {FLAG_COLUMN}=1, {FLAG_TS_COL}=NOW() " + f"WHERE rid=%s", + (rid,) + ) + conn.commit() + + +# ---------- UI helpers ---------- +def _strip_diacritics(s: str) -> str: + """Return s without diacritics (e.g., 'chřipka' -> 'chripka').""" + return ''.join(c for c in unicodedata.normalize('NFKD', s) if not unicodedata.combining(c)) + + +def has_existing_chripka_request(page: Page, timeout_ms: int = 15000) -> bool: + """ + Detect an existing 'Očkování - Chřipka' request on the patient card. + - Checks both card view (data-testid='patient-request-item' h4) and + table/row view (data-testid='patient-request-row' strong). + - Case/diacritics-insensitive. + """ + try: + page.get_by_text("Historie požadavků").wait_for(timeout=timeout_ms) + except PWTimeout: + # Some layouts may render without this exact header – proceed anyway. + pass + + # Let the list render + page.wait_for_timeout(600) + + titles = [] + try: + titles += page.locator("[data-testid='patient-request-item'] h4").all_text_contents() + except Exception: + pass + try: + titles += page.locator("[data-testid='patient-request-row'] strong").all_text_contents() + except Exception: + pass + + # Fallback if no headings were captured: read whole items/rows + if not titles: + try: + titles += page.locator("[data-testid='patient-request-item']").all_text_contents() + except Exception: + pass + try: + titles += page.locator("[data-testid='patient-request-row']").all_text_contents() + except Exception: + pass + + if not titles: + return False + + pat = re.compile(r"\bchripka\b", re.IGNORECASE) + for t in titles: + if pat.search(_strip_diacritics(t)): + return True + return False + + +def create_flu_request_for_uuid(uuid: str) -> str: + """ + Automate Medevio UI for one patient: + - Open patient card + - If a Chřipka request already exists, return 'skipped' + - Else create 'Očkování - Chřipka' and send MESSAGE_TEXT -> return 'created' + - On failure to send, return 'failed' + """ + with sync_playwright() as p: + browser = p.chromium.launch(headless=False, slow_mo=200) + context = browser.new_context(storage_state=str(STATE_FILE)) + ptcard = context.new_page() + + url = PATIENT_URL_TMPL.format(uuid=uuid) + ptcard.goto(url, wait_until="networkidle") + + # Ensure the card loaded (best-effort) + try: + ptcard.get_by_text("Historie požadavků").wait_for(timeout=7_000) + except PWTimeout: + pass + + # ----- pre-check for existing Chřipka request ----- + if has_existing_chripka_request(ptcard): + browser.close() + return "skipped" + + # ----- Create new request ----- + ptcard.get_by_role("button", name="Nový požadavek").click() + ptcard.wait_for_timeout(300) + ptcard.keyboard.type("očkování - chřipka") + ptcard.locator("[role='option']", has_text="Očkování - Chřipka").first.click() + ptcard.get_by_role("button", name="Vytvořit požadavek").click() + time.sleep(2) + + # Wait until back on card and the list is visible again + try: + ptcard.get_by_text("Historie požadavků").wait_for(timeout=10_000) + except PWTimeout: + pass + time.sleep(1.0) + + # Open the new request (prefer the tile that mentions Chřipka) + try: + ptcard.locator("div[data-testid='patient-request-item']").first.wait_for(timeout=10_000) + chripka_card = ptcard.locator("div[data-testid='patient-request-item']").filter( + has=ptcard.locator("h4", has_text=re.compile(r"(?i)ch[řr]ipka")) + ).first + if chripka_card.count() == 0: + ptcard.locator("div[data-testid='patient-request-item']").first.click(timeout=5_000) + else: + chripka_card.click(timeout=5_000) + except Exception: + # fallback: try the first request item + try: + ptcard.locator("div[data-testid='patient-request-item']").first.click(timeout=5_000) + except Exception: + browser.close() + return "failed" + + # ----- Send the message ----- + try: + ptcard.get_by_placeholder("Napište odpověď").wait_for(timeout=10_000) + ptcard.get_by_placeholder("Napište odpověď").fill(MESSAGE_TEXT) + time.sleep(1.2) + for sel in [ + "button:has-text('Odeslat')", + "button:has-text('Odeslat zprávu')", + "button:has-text('Odeslat SMS')", + "button:has-text('Odeslat do aplikace')", + ]: + try: + ptcard.click(sel, timeout=4000) + browser.close() + return "created" + except Exception: + continue + except Exception: + pass + + browser.close() + return "failed" + + +# ---------- main ---------- +def main(): + conn = pymysql.connect(**MYSQL_CFG) + try: + ensure_flag_columns(conn) + + rows = fetch_batch(conn) + if not rows: + print("Nenalezen žádný pacient pro zpracování.") + return + + print(f"Zpracujeme {len(rows)} pacientů…") + + processed = ok = fail = skipped = 0 + for r in rows: + processed += 1 + rid = r["rid"] + uuid = r["uuid"] + name = f"{r.get('prijmeni','')}, {r.get('jmeno','')}" + rc = r.get("rc", "") + + print(f"[{processed:>3}] {name} | RC {rc} | UUID {uuid}") + + try: + result = create_flu_request_for_uuid(uuid) + if result == "created": + mark_flag_success(conn, rid) + ok += 1 + print(" ✓ vytvořeno + odesláno, DB flag nastaven") + elif result == "skipped": + mark_flag_skipped(conn, rid) + skipped += 1 + print(" ↷ již existuje požadavek na chřipku – přeskočeno") + else: + fail += 1 + print(" ✗ nepodařilo se odeslat zprávu (tlačítko 'Odeslat' nenalezeno?)") + except Exception as e: + fail += 1 + conn.rollback() + print(f" ✗ chyba: {type(e).__name__}: {e}") + + time.sleep(PAUSE_BETWEEN) + + print(f"Hotovo. processed={processed}, ok={ok}, skipped={skipped}, fail={fail}") + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/20 Test.py b/Medevio/Testy/20 Test.py new file mode 100644 index 0000000..c089671 --- /dev/null +++ b/Medevio/Testy/20 Test.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import shutil +import pymysql +import re +from pathlib import Path +from datetime import datetime +import time + +# ============================== +# ⚙️ CONFIGURATION +# ============================== +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", +} + +BASE_DIR = Path(r"d:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP") +BASE_DIR.mkdir(parents=True, exist_ok=True) + + +def sanitize_name(name: str) -> str: + """Replace invalid filename characters with underscore.""" + return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip() + + +def make_abbrev(title: str) -> str: + """ + Create abbreviation from displayTitle: + - First letter of each word + - Keep digits together + - Uppercase + """ + if not title: + return "" + + words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title) + + abbr = "" + for w in words: + if w.isdigit(): + abbr += w + else: + abbr += w[0] + + return abbr.upper() + + +# ============================== +# 🧹 DELETE UNEXPECTED FILES +# ============================== +def clean_folder(folder: Path, valid_files: set): + """ + Remove unexpected files. + RULE: + - Files starting with `▲` are ALWAYS kept. + """ + if not folder.exists(): + return + + for f in folder.iterdir(): + if f.is_file(): + + # zpracované soubory (▲filename.pdf) nikdy nemažeme + if f.name.startswith("▲"): + continue + + sanitized = sanitize_name(f.name) + if sanitized not in valid_files: + print(f"🗑️ Removing unexpected file: {f.name}") + try: + f.unlink() + except Exception as e: + print(f"⚠️ Could not delete {f}: {e}") + + +# ============================== +# 📦 DB CONNECTION +# ============================== +conn = pymysql.connect(**DB_CONFIG) + +cur_meta = conn.cursor(pymysql.cursors.DictCursor) +cur_blob = conn.cursor() + +print("🔍 Loading metadata from DB (FAST)…") + +cur_meta.execute(""" + SELECT d.id AS download_id, + d.request_id, + d.filename, + d.created_at, + p.updatedAt AS req_updated_at, + p.pacient_jmeno AS jmeno, + p.pacient_prijmeni AS prijmeni, + p.displayTitle + FROM medevio_downloads d + JOIN pozadavky p ON d.request_id = p.id + ORDER BY p.updatedAt DESC +""") + +rows = cur_meta.fetchall() +print(f"📋 Found {len(rows)} attachment records.\n") + + +# ============================== +# 🧠 MAIN LOOP +# ============================== +processed_requests = set() + +for r in rows: + req_id = r["request_id"] + + if req_id in processed_requests: + continue + processed_requests.add(req_id) + + # ========== FETCH ALL VALID FILES FOR THIS REQUEST ========== + cur_meta.execute( + "SELECT filename FROM medevio_downloads WHERE request_id=%s", + (req_id,) + ) + valid_files = {sanitize_name(row["filename"]) for row in cur_meta.fetchall()} + + # ========== FOLDER NAME BASED ON UPDATEDAT ========== + updated_at = r["req_updated_at"] or datetime.now() + date_str = updated_at.strftime("%Y-%m-%d") + + prijmeni = sanitize_name(r["prijmeni"] or "Unknown") + jmeno = sanitize_name(r["jmeno"] or "") + title = r.get("displayTitle") or "" + abbr = make_abbrev(title) + + clean_folder_name = sanitize_name( + f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}" + ) + + # ========== DETECT EXISTING FOLDER (WITH OR WITHOUT ▲) ========== + existing_folder = None + folder_has_flag = False + + for f in BASE_DIR.iterdir(): + if f.is_dir() and req_id in f.name: + existing_folder = f + folder_has_flag = ("▲" in f.name) + break + + # pokud složka existuje → pracujeme v ní + main_folder = existing_folder if existing_folder else BASE_DIR / clean_folder_name + + # ========== MERGE DUPLICATES ========== + possible_dups = [ + f for f in BASE_DIR.iterdir() + if f.is_dir() and req_id in f.name and f != main_folder + ] + + for dup in possible_dups: + print(f"♻️ Merging duplicate folder: {dup.name}") + + clean_folder(dup, valid_files) + main_folder.mkdir(parents=True, exist_ok=True) + + for f in dup.iterdir(): + if f.is_file(): + # prostě přesuneme, ▲ případně zůstane v názvu + target = main_folder / f.name + if not target.exists(): + f.rename(target) + + shutil.rmtree(dup, ignore_errors=True) + + # ========== CLEAN MAIN FOLDER ========== + clean_folder(main_folder, valid_files) + + # ========== DOWNLOAD MISSING FILES ========== + added_new_file = False + main_folder.mkdir(parents=True, exist_ok=True) + + for filename in valid_files: + dest_plain = main_folder / filename + dest_marked = main_folder / ("▲" + filename) + + # soubor už existuje (buď filename, nebo ▲filename) + if dest_plain.exists() or dest_marked.exists(): + continue + + # stáhneme nový soubor → znamená že se má odstranit ▲ složky + added_new_file = True + + cur_blob.execute( + "SELECT file_content FROM medevio_downloads " + "WHERE request_id=%s AND filename=%s", + (req_id, filename) + ) + row = cur_blob.fetchone() + if not row: + continue + + content = row[0] + if not content: + continue + + with open(dest_plain, "wb") as f: + f.write(content) + + print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}") + + # ============================== + # 🔵 REMOVE FOLDER-LEVEL ▲ ONLY IF NEW FILE ADDED + # ============================== + if added_new_file: + # složka se má přejmenovat bez ▲ + if "▲" in main_folder.name: + new_name = main_folder.name.replace("▲", "") + new_name = new_name.strip() # pro jistotu + new_path = main_folder.parent / new_name + + if new_path != main_folder: + try: + main_folder.rename(new_path) + print(f"🔄 Folder flag ▲ removed → {new_name}") + main_folder = new_path + except Exception as e: + print(f"⚠️ Could not rename folder: {e}") + else: + # žádné nové soubory → NIKDY nesahat na název složky + pass + + +print("\n🎯 Export complete.\n") + +cur_blob.close() +cur_meta.close() +conn.close() diff --git a/Medevio/Testy/8 medevio read calendar.py b/Medevio/Testy/8 medevio read calendar.py new file mode 100644 index 0000000..bbb2a28 --- /dev/null +++ b/Medevio/Testy/8 medevio read calendar.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Open the Medevio daily agenda calendar, +inspect the rendered HTML, and probe JS memory +to see what data is exposed. +""" + +from playwright.sync_api import sync_playwright + +STATE_FILE = "../medevio_storage.json" +AGENDA_URL = ( + "https://my.medevio.cz/mudr-buzalkova/klinika/kalendar/agenda-dne/" + "?kalendar=144c4e12-347c-49ca-9ec0-8ca965a4470d&datum=2025-10-17" +) + +def main(): + with sync_playwright() as pw: + browser = pw.chromium.launch(headless=False, slow_mo=150) + context = browser.new_context(storage_state=STATE_FILE) + page = context.new_page() + + print("🔗 Opening agenda-day calendar...") + page.goto(AGENDA_URL, wait_until="networkidle", timeout=90_000) + + # -------- Check login -------- + body = (page.text_content("body") or "").lower() + if any(x in body for x in ["přihlášení", "přihlásit", "sign in", "login"]): + raise SystemExit("❌ Not logged in – refresh medevio_storage.json.") + + # -------- Wait for appointments to render -------- + page.wait_for_timeout(4000) + + # -------- Dump a few appointment blocks -------- + blocks = page.locator("div.rbc-event-inner-content, div[data-testid='Reservation']").evaluate_all( + "(els) => els.map(e => e.outerHTML)" + ) + print(f"\n✅ Found {len(blocks)} appointment blocks. Showing first 3:\n") + for snippet in blocks[:3]: + print(snippet) + print("-" * 80) + + # -------- Explore window memory -------- + print("\n🔍 Inspecting global JS variables...") + keys = page.evaluate("Object.keys(window)") + interesting = [k for k in keys if any(w in k.lower() for w in ["mede", "cal", "react", "state", "reserv"])] + print("Interesting keys:", interesting[:20]) + + for candidate in [ + "window.__INITIAL_STATE__", + "window.__INITIAL_DATA__", + "window.__REACT_DEVTOOLS_GLOBAL_HOOK__", + "window.medevioCalendar", + "window.calendarStore", + "window.reduxStore", + "window.reactProps", + ]: + try: + data = page.evaluate(f"JSON.stringify({candidate}, null, 2)") + if data and len(data) > 200: + print(f"\n===== {candidate} =====\n{data[:1000]}...\n") + except Exception: + pass + + # -------- Optionally: listen for network requests while you click -------- + def log_request(req): + url = req.url + if any(x in url for x in ["pozadavek", "request", "api"]): + print("📡", url) + + page.on("request", log_request) + print("\n👉 Now click manually on a few agenda items to open their detail cards.") + print(" Any backend calls will appear below.\n") + + page.wait_for_timeout(40000) # give yourself ~40s to click around + browser.close() + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/81 Read agenda and give 1.py b/Medevio/Testy/81 Read agenda and give 1.py new file mode 100644 index 0000000..ba4f2f9 --- /dev/null +++ b/Medevio/Testy/81 Read agenda and give 1.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from playwright.sync_api import sync_playwright +import json +import requests + +STATE_FILE = "../medevio_storage.json" +AGENDA_URL = ( + "https://my.medevio.cz/mudr-buzalkova/klinika/kalendar/agenda-dne/" + "?kalendar=144c4e12-347c-49ca-9ec0-8ca965a4470d&datum=2025-10-17" +) +GRAPHQL_URL = "https://api.medevio.cz/graphql" + + +def extract_agenda_rows(page): + rows = page.locator("div[data-testid='reservation-row']") + if rows.count() == 0: + raise SystemExit("❌ No rows found — check selector or login session.") + + results = [] + print(f"\nFound {rows.count()} rows, showing sample structure:") + for row in rows.all()[:3]: + print("-" * 80) + print(row.evaluate("el => el.outerHTML")[:500], "...\n") + + # Now extract data safely + for row in rows.all(): + rid = row.get_attribute("data-id") or "" + # Try to read each cell dynamically + cells = row.locator("div.MuiDataGrid-cell") + record = {"id": rid} + for c in cells.all(): + field = c.get_attribute("data-field") or "unknown" + text = (c.text_content() or "").strip() + record[field] = text + results.append(record) + return results + + +def step1_extract_appointments(): + with sync_playwright() as pw: + browser = pw.chromium.launch(headless=False, slow_mo=150) + context = browser.new_context(storage_state=STATE_FILE) + page = context.new_page() + print("🔗 Opening Medevio agenda-day page...") + page.goto(AGENDA_URL, wait_until="networkidle", timeout=90_000) + page.wait_for_selector("div[data-testid='reservation-row']", timeout=30_000) + + appointments = extract_agenda_rows(page) + browser.close() + print(f"✅ Extracted {len(appointments)} appointments") + for a in appointments: + print(f"— {a.get('StartDateTime','?')} {a.get('Patient','?')}: {a.get('Reason','?')} ({a['id']})") + return appointments + + +def step2_fetch_detail(session_cookies, reservation_id): + headers = { + "content-type": "application/json", + "origin": "https://my.medevio.cz", + "referer": "https://my.medevio.cz/", + } + + query = { + "operationName": "ReservationDetail", + "variables": {"id": reservation_id}, + "query": """ + query ReservationDetail($id: ID!) { + reservation(id: $id) { + id + reason + startDateTime + endDateTime + status + note + patient { id name age } + doctor { id name } + location { name } + } + } + """, + } + + print(f"\n📡 Fetching GraphQL detail for {reservation_id}...") + response = requests.post(GRAPHQL_URL, headers=headers, cookies=session_cookies, data=json.dumps(query)) + print("Status:", response.status_code) + print(json.dumps(response.json(), indent=2, ensure_ascii=False)) + + +if __name__ == "__main__": + appointments = step1_extract_appointments() + if not appointments: + raise SystemExit("No appointments found.") + + # Use first appointment for detail fetch + reservation_id = appointments[0]["id"] + + # Load session cookies from storage + with open(STATE_FILE, "r", encoding="utf-8") as f: + state = json.load(f) + cookies = {c["name"]: c["value"] for c in state.get("cookies", []) if "medevio" in c["domain"]} + + step2_fetch_detail(cookies, reservation_id) diff --git a/Medevio/Testy/82 test.py b/Medevio/Testy/82 test.py new file mode 100644 index 0000000..b1ef8fc --- /dev/null +++ b/Medevio/Testy/82 test.py @@ -0,0 +1,44 @@ +import json, requests + +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +FULL_INTROSPECTION_QUERY = """ +query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + } +} +fragment FullType on __Type { + kind + name + fields(includeDeprecated: true) { + name + } +} +""" + +headers = { + "content-type": "application/json", + "origin": "https://my.medevio.cz", + "referer": "https://my.medevio.cz/", +} + +# Load cookies from storage +state = json.load(open("../medevio_storage.json", encoding="utf-8")) +cookies = {c["name"]: c["value"] for c in state["cookies"] if "medevio" in c["domain"]} + +payload = {"operationName": "IntrospectionQuery", "query": FULL_INTROSPECTION_QUERY} + +r = requests.post(GRAPHQL_URL, headers=headers, cookies=cookies, data=json.dumps(payload)) +print("Status:", r.status_code) +try: + data = r.json() + print(json.dumps(data, indent=2, ensure_ascii=False)[:2000]) +except Exception as e: + print("Could not decode response:", e) + print(r.text) diff --git a/Medevio/Testy/83_capture_graphql.py b/Medevio/Testy/83_capture_graphql.py new file mode 100644 index 0000000..0974e9c --- /dev/null +++ b/Medevio/Testy/83_capture_graphql.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from playwright.sync_api import sync_playwright +import json, os, time + +STATE_FILE = "../medevio_storage.json" +GRAPHQL_LOG = f"graphql_capture_{int(time.time())}.jsonl" + +with sync_playwright() as pw: + browser = pw.chromium.launch(headless=False, slow_mo=200) + context = browser.new_context(storage_state=STATE_FILE) + page = context.new_page() + + def log_graphql(req): + if "graphql" in req.url and req.method == "POST": + try: + body = req.post_data or "" + data = json.loads(body) + with open(GRAPHQL_LOG, "a", encoding="utf-8") as f: + f.write(json.dumps(data, ensure_ascii=False) + "\n") + print(f"📡 {data.get('operationName')} saved") + except Exception: + pass + + page.on("request", log_graphql) + + print("🔗 Opening Medevio main page...") + page.goto("https://my.medevio.cz/mudr-buzalkova/klinika/kalendar/agenda-dne/" + "?kalendar=144c4e12-347c-49ca-9ec0-8ca965a4470d", wait_until="networkidle") + + print("\n👉 Click various items in Medevio (calendar, reservations, requests, etc.).") + print(" Every GraphQL call will be saved to", GRAPHQL_LOG) + print(" Press Ctrl+C or close the browser when done.\n") + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + browser.close() + print(f"\n✅ Finished — GraphQL calls saved to {GRAPHQL_LOG}") diff --git a/Medevio/Testy/84 test.py b/Medevio/Testy/84 test.py new file mode 100644 index 0000000..a5cc801 --- /dev/null +++ b/Medevio/Testy/84 test.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Query Medevio for the full agenda of 17 Oct 2025 and print raw API response. +""" + +import json +import requests + +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +CALENDAR_ID = "144c4e12-347c-49ca-9ec0-8ca965a4470d" +CLINIC_SLUG = "mudr-buzalkova" + +def load_gateway_token(storage_path="medevio_storage.json"): + """Return Medevio gateway-access-token from saved Playwright storage.""" + import json + from pathlib import Path + + path = Path(storage_path) + if not path.exists(): + raise SystemExit(f"❌ Storage file not found: {path}") + + with path.open("r", encoding="utf-8") as f: + state = json.load(f) + + token = next( + (c["value"] for c in state["cookies"] + if c["name"] == "gateway-access-token"), None + ) + + if not token: + raise SystemExit("❌ gateway-access-token not found in storage file.") + + return token + +gateway_token = load_gateway_token() + +headers = { + "content-type": "application/json", + "origin": "https://my.medevio.cz", + "referer": "https://my.medevio.cz/", + "authorization": f"Bearer {gateway_token}", +} + +payload = { + "operationName": "ClinicAgenda_ListClinicReservations", + "variables": { + "calendarIds": [CALENDAR_ID], + "clinicSlug": CLINIC_SLUG, + "since": "2025-10-16T22:00:00.000Z", + "until": "2025-10-17T21:59:59.999Z", + "locale": "cs", + "emptyCalendarIds": False, + }, + "query": """query ClinicAgenda_ListClinicReservations( + $calendarIds: [UUID!], + $clinicSlug: String!, + $locale: Locale!, + $since: DateTime!, + $until: DateTime!, + $emptyCalendarIds: Boolean! + ) { + reservations: listClinicReservations( + clinicSlug: $clinicSlug, + calendarIds: $calendarIds, + since: $since, + until: $until + ) @skip(if: $emptyCalendarIds) { + id + start + end + note + done + color + request { + id + displayTitle(locale: $locale) + extendedPatient { + name + surname + dob + insuranceCompanyObject { shortName } + } + } + } + }""", +} + +print("📡 Querying Medevio API for agenda...") +r = requests.post(GRAPHQL_URL, headers=headers, data=json.dumps(payload)) +print("Status:", r.status_code) + +try: + data = r.json() + print(json.dumps(data, indent=2, ensure_ascii=False)) +except Exception as e: + print("❌ Could not parse JSON:", e) + print(r.text) diff --git a/Medevio/Testy/85 test.py b/Medevio/Testy/85 test.py new file mode 100644 index 0000000..480e583 --- /dev/null +++ b/Medevio/Testy/85 test.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Query Medevio for the full agenda of 17 Oct 2025, +print raw API response, and export to Excel. +""" + +import json +import time +from pathlib import Path +import requests +import pandas as pd +from openpyxl import load_workbook +from openpyxl.styles import Font, Alignment +from openpyxl.utils import get_column_letter + +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +CALENDAR_ID = "144c4e12-347c-49ca-9ec0-8ca965a4470d" +CLINIC_SLUG = "mudr-buzalkova" + +# ==================== Load Token ==================== +def load_gateway_token(storage_path="medevio_storage.json"): + """Return Medevio gateway-access-token from saved Playwright storage.""" + from pathlib import Path + + path = Path(storage_path) + if not path.exists(): + raise SystemExit(f"❌ Storage file not found: {path}") + + with path.open("r", encoding="utf-8") as f: + state = json.load(f) + + token = next( + (c["value"] for c in state["cookies"] + if c["name"] == "gateway-access-token"), None + ) + + if not token: + raise SystemExit("❌ gateway-access-token not found in storage file.") + + return token + + +gateway_token = load_gateway_token() + +headers = { + "content-type": "application/json", + "origin": "https://my.medevio.cz", + "referer": "https://my.medevio.cz/", + "authorization": f"Bearer {gateway_token}", +} + +payload = { + "operationName": "ClinicAgenda_ListClinicReservations", + "variables": { + "calendarIds": [CALENDAR_ID], + "clinicSlug": CLINIC_SLUG, + "since": "2025-10-20T00:00:00.001Z", + "until": "2025-10-27T21:59:59.999Z", + "locale": "cs", + "emptyCalendarIds": False, + }, + "query": """query ClinicAgenda_ListClinicReservations( + $calendarIds: [UUID!], + $clinicSlug: String!, + $locale: Locale!, + $since: DateTime!, + $until: DateTime!, + $emptyCalendarIds: Boolean! + ) { + reservations: listClinicReservations( + clinicSlug: $clinicSlug, + calendarIds: $calendarIds, + since: $since, + until: $until + ) @skip(if: $emptyCalendarIds) { + id + start + end + note + done + color + request { + id + displayTitle(locale: $locale) + extendedPatient { + name + surname + dob + insuranceCompanyObject { shortName } + } + } + } + }""", +} + +# ==================== Query API ==================== +print("📡 Querying Medevio API for agenda...") +r = requests.post(GRAPHQL_URL, headers=headers, data=json.dumps(payload)) +print("Status:", r.status_code) + +try: + data = r.json() +except Exception as e: + print("❌ Could not parse JSON:", e) + print(r.text) + raise SystemExit() + +if "data" not in data or "reservations" not in data["data"]: + raise SystemExit("⚠️ No 'reservations' data found in response.") + +reservations = data["data"]["reservations"] +from datetime import datetime +from dateutil import parser, tz + +# ===== Process reservations into table ===== +rows = [] +for r in reservations: + req = r.get("request") or {} + patient = req.get("extendedPatient") or {} + insurance = patient.get("insuranceCompanyObject") or {} + + # parse datetimes (convert to local time) + try: + start_dt = parser.isoparse(r.get("start")).astimezone(tz.gettz("Europe/Prague")) + end_dt = parser.isoparse(r.get("end")).astimezone(tz.gettz("Europe/Prague")) + except Exception: + start_dt = end_dt = None + + date_str = start_dt.strftime("%Y-%m-%d") if start_dt else "" + time_interval = f"{start_dt.strftime('%H:%M')}-{end_dt.strftime('%H:%M')}" if start_dt and end_dt else "" + + rows.append({ + "Date": date_str, + "Time": time_interval, + "Title": req.get("displayTitle") or "", + "Patient": f"{patient.get('surname','')} {patient.get('name','')}".strip(), + "DOB": patient.get("dob") or "", + "Insurance": insurance.get("shortName") or "", + "Note": r.get("note") or "", + "Color": r.get("color") or "", + "Request_ID": req.get("id") or "", + "Reservation_ID": r.get("id"), + }) + +df = pd.DataFrame(rows).sort_values(["Date", "Time"]) + +# ===== Excel export ===== +EXPORT_DIR = Path(r"C:\Users\vlado\PycharmProjects\Medevio\exports") +EXPORT_DIR.mkdir(exist_ok=True) +timestamp = time.strftime("%Y-%m-%d %H-%M-%S") +xlsx_path = EXPORT_DIR / f"Medevio_agenda_{timestamp}.xlsx" + +# remove old files +for old in EXPORT_DIR.glob("Medevio_agenda_*.xlsx"): + try: + old.unlink() + except Exception: + pass + +df.to_excel(xlsx_path, index=False) +wb = load_workbook(xlsx_path) +ws = wb.active + +# style header +for col in range(1, len(df.columns) + 1): + c = ws.cell(row=1, column=col) + c.font = Font(bold=True) + c.alignment = Alignment(horizontal="center") + ws.column_dimensions[get_column_letter(col)].width = 20 +ws.freeze_panes = "A2" + +wb.save(xlsx_path) +print(f"📘 Exported clean agenda view to:\n{xlsx_path}") diff --git a/Medevio/Testy/861 improvement formatting.py b/Medevio/Testy/861 improvement formatting.py new file mode 100644 index 0000000..a69a3cb --- /dev/null +++ b/Medevio/Testy/861 improvement formatting.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Query Medevio for the full agenda of 17 Oct 2025, +print raw API response, and export to Excel. +""" +import re +import json +import time +from pathlib import Path +import requests +import pandas as pd +from openpyxl import load_workbook +from openpyxl.styles import Font, Alignment, PatternFill, Border, Side +from openpyxl.utils import get_column_letter +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta +from Functions import get_reports_folder +from openpyxl.utils.dataframe import dataframe_to_rows + + +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +CALENDAR_ID = "144c4e12-347c-49ca-9ec0-8ca965a4470d" +CLINIC_SLUG = "mudr-buzalkova" + +# ==================== Load Token ==================== +def load_gateway_token(storage_path="medevio_storage.json"): + """Return Medevio gateway-access-token from saved Playwright storage.""" + path = Path(storage_path) + if not path.exists(): + raise SystemExit(f"❌ Storage file not found: {path}") + + with path.open("r", encoding="utf-8") as f: + state = json.load(f) + + token = next( + (c["value"] for c in state["cookies"] + if c["name"] == "gateway-access-token"), None + ) + + if not token: + raise SystemExit("❌ gateway-access-token not found in storage file.") + + return token + + +gateway_token = load_gateway_token() + +headers = { + "content-type": "application/json", + "origin": "https://my.medevio.cz", + "referer": "https://my.medevio.cz/", + "authorization": f"Bearer {gateway_token}", +} + +# === Dynamic date range === +dnes = datetime.utcnow().date() +since = datetime.combine(dnes, datetime.min.time()).replace(microsecond=1) +until = since + relativedelta(months=1) - timedelta(milliseconds=1) + +since_iso = since.isoformat() + "Z" +until_iso = until.isoformat() + "Z" + +payload = { + "operationName": "ClinicAgenda_ListClinicReservations", + "variables": { + "calendarIds": [CALENDAR_ID], + "clinicSlug": CLINIC_SLUG, + "since": since_iso, + "until": "2025-11-30T21:59:59.999Z", + "locale": "cs", + "emptyCalendarIds": False, + }, + "query": """query ClinicAgenda_ListClinicReservations( + $calendarIds: [UUID!], + $clinicSlug: String!, + $locale: Locale!, + $since: DateTime!, + $until: DateTime!, + $emptyCalendarIds: Boolean! + ) { + reservations: listClinicReservations( + clinicSlug: $clinicSlug, + calendarIds: $calendarIds, + since: $since, + until: $until + ) @skip(if: $emptyCalendarIds) { + id + start + end + note + done + color + request { + id + displayTitle(locale: $locale) + extendedPatient { + name + surname + dob + insuranceCompanyObject { shortName } + } + } + } + }""", +} + +print("since:", since_iso) +print("until:", until_iso) + +# ==================== Query API ==================== +print("📡 Querying Medevio API for agenda...") +r = requests.post(GRAPHQL_URL, headers=headers, data=json.dumps(payload)) +print("Status:", r.status_code) + +try: + data = r.json() +except Exception as e: + print("❌ Could not parse JSON:", e) + print(r.text) + raise SystemExit() + +if "data" not in data or "reservations" not in data["data"]: + raise SystemExit("⚠️ No 'reservations' data found in response.") + +reservations = data["data"]["reservations"] +from dateutil import parser, tz + +# ===== Process reservations into table ===== +rows = [] +for r in reservations: + req = r.get("request") or {} + patient = req.get("extendedPatient") or {} + insurance = patient.get("insuranceCompanyObject") or {} + + try: + start_dt = parser.isoparse(r.get("start")).astimezone(tz.gettz("Europe/Prague")) + end_dt = parser.isoparse(r.get("end")).astimezone(tz.gettz("Europe/Prague")) + except Exception: + start_dt = end_dt = None + + date_str = start_dt.strftime("%Y-%m-%d") if start_dt else "" + time_interval = f"{start_dt.strftime('%H:%M')}-{end_dt.strftime('%H:%M')}" if start_dt and end_dt else "" + + rows.append({ + "Date": date_str, + "Time": time_interval, + "Title": req.get("displayTitle") or "", + "Patient": f"{patient.get('surname','')} {patient.get('name','')}".strip(), + "DOB": patient.get("dob") or "", + "Insurance": insurance.get("shortName") or "", + "Note": r.get("note") or "", + "Color": r.get("color") or "", + "Request_ID": req.get("id") or "", + "Reservation_ID": r.get("id"), + }) + +df = pd.DataFrame(rows).sort_values(["Date", "Time"]) + + + +def kw_pattern(kw: str) -> str: + """ + Match the exact phrase kw (case-insensitive), + not as part of a '+something' continuation. + Examples: + 'žloutenka a' ✅ matches '… žloutenka a …' + ❌ NOT '… žloutenka a+b …' + 'žloutenka a+b' ✅ matches exactly that phrase + """ + # start boundary: not preceded by a word char + # end guard: not followed by optional spaces + '+' + word + return rf"(?.json next to the script. + +from pathlib import Path +import json, sys, time, re +from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout + +STATE_FILE = r"../medevio_storage.json" +BASE_URL = "https://my.medevio.cz/mudr-buzalkova/klinika/pacienti" +PATIENT_ID = "fcb2414b-067b-4ca2-91b2-6c36a86d4cbb" # <-- put target ID here + +# ---------- helpers ---------- + +def wait_for_grid(page, timeout=15000): + try: + page.wait_for_selector("div[role='rowgroup']", timeout=timeout) + except PWTimeout: + pass + page.wait_for_selector("div[role='row'][data-id]", timeout=timeout) + +def open_detail_via_query(page, patient_id): + # Try opening page with ?pacient=... (SPA should open drawer/detail) + target = f"{BASE_URL}?pacient={patient_id}" + page.goto(target, wait_until="domcontentloaded") + # Wait briefly for the drawer/dialog to render + if not wait_for_detail_open(page, quick=True): + # Some apps need a tiny delay to mount the panel + time.sleep(0.8) + return is_detail_open(page) + +def is_detail_open(page): + # Look for a dialog/drawer that likely contains patient detail. + # Typical MUI patterns: role="dialog", or an aside/div with aria-modal etc. + selectors = [ + "[role='dialog']", + "div.MuiDrawer-paper", + "div.MuiModal-root [role='dialog']", + "div[aria-modal='true']", + ] + for sel in selectors: + loc = page.locator(sel) + if loc.count() and loc.first.is_visible(): + return True + return False + +def wait_for_detail_open(page, quick=False): + timeout = 4000 if quick else 15000 + selectors = [ + "[role='dialog']", + "div.MuiDrawer-paper", + "div.MuiModal-root [role='dialog']", + "div[aria-modal='true']", + ] + for sel in selectors: + try: + page.wait_for_selector(sel, timeout=timeout, state="visible") + return True + except PWTimeout: + continue + return False + +def open_detail_by_click(page, patient_id): + # Click the row with matching data-id (fallback) + wait_for_grid(page, timeout=15000) + row = page.locator(f"div[role='row'][data-id='{patient_id}']").first + if not row.count(): + return False + row.click() + return wait_for_detail_open(page) + +def find_detail_root(page): + # Return the locator that represents the open detail container + for sel in ["[role='dialog']", "div.MuiDrawer-paper", "div[aria-modal='true']"]: + loc = page.locator(sel) + if loc.count() and loc.first.is_visible(): + return loc.first + # Fallback to the last visible modal-ish container + return page.locator("div.MuiModal-root, div.MuiDrawer-paper").last + +def extract_text(el): + try: + return el.inner_text().strip() + except Exception: + return "" + +def extract_field_by_label(root, label_texts): + """ + Try to find a field value by its label text (CZ/EN variants). + Looks for elements containing the label and then a sibling/value element. + """ + labels_xpath = " | ".join([f".//*[normalize-space()='{t}']" for t in label_texts]) + loc = root.locator(f"xpath=({labels_xpath})") + if not loc.count(): + # Try contains(label) + labels_xpath2 = " | ".join([f".//*[contains(normalize-space(), '{t}')]" for t in label_texts]) + loc = root.locator(f"xpath=({labels_xpath2})") + if not loc.count(): + return None + + candidate = loc.first + # Value might be in parent/next sibling + parent = candidate.locator("xpath=..") + siblings = [ + parent.locator("xpath=following-sibling::*[1]"), + candidate.locator("xpath=following-sibling::*[1]"), + parent.locator(".//*[(self::span or self::div) and string-length(normalize-space())>0]"), + ] + + for s in siblings: + if s.count(): + text = extract_text(s.first) + # Clean common label-value formatting like "E-mail\nx@y.cz" + if text: + # If the label text is included, strip it + for t in label_texts: + text = re.sub(rf"^{re.escape(t)}\s*[::]?\s*", "", text, flags=re.I) + text = re.sub(r"\s+\n\s+", " — ", text).strip() + return text + + # As a last fallback, try reading the parent block's text minus the label + block_text = extract_text(parent) + if block_text: + for t in label_texts: + block_text = re.sub(rf"{re.escape(t)}\s*[::]?\s*", "", block_text, flags=re.I) + return block_text.strip() + return None + +def extract_all_text_pairs(root): + """ + Generic key-value sweep for components that render details as 2-column grids. + Returns a dict of guessed label->value pairs. + """ + result = {} + # Try common MUI grid/list patterns + blocks = root.locator("div.MuiGrid-container, dl, ul.MuiList-root") + for i in range(min(20, blocks.count())): + block = blocks.nth(i) + text = extract_text(block) + if not text: + continue + # naive split by newlines, pair neighbors "Label\nValue" + parts = [t.strip() for t in text.splitlines() if t.strip()] + for j in range(len(parts) - 1): + label, value = parts[j], parts[j+1] + # Heuristic: labels usually short, values not identical, ignore obvious noise + if len(label) <= 32 and label != value and ":" not in value: + if label not in result: + result[label] = value + return result + +def extract_patient_detail(page, patient_id): + root = find_detail_root(page) + if not root: + return {"id": patient_id, "error": "detail_not_found"} + + # Try to get a headline with the name + name = None + for sel in ["h1", "h2", "h3", "header h2", "[data-testid='PatientName']"]: + loc = root.locator(sel) + if loc.count(): + nm = extract_text(loc.first) + if nm and len(nm) > 1: + name = nm + break + + # Targeted fields (CZ + EN aliases) + fields = { + "Datum narození / Born": extract_field_by_label(root, ["Datum narození", "Datum nar.", "Date of birth", "Born"]), + "Rodné číslo": extract_field_by_label(root, ["Rodné číslo", "RČ", "Personal ID"]), + "Telefon": extract_field_by_label(root, ["Telefon", "Tel.", "Phone", "Mobile"]), + "E-mail": extract_field_by_label(root, ["E-mail", "Email", "E-mail"]), + "Zdravotní pojišťovna": extract_field_by_label(root, ["Pojišťovna", "Zdravotní pojišťovna", "Insurer", "Insurance"]), + "Adresa": extract_field_by_label(root, ["Adresa", "Address"]), + "Poznámka": extract_field_by_label(root, ["Poznámka", "Note", "Notes"]), + "Pohlaví": extract_field_by_label(root, ["Pohlaví", "Gender", "Sex"]), + "Praktický lékař": extract_field_by_label(root, ["Praktický lékař", "GP", "General practitioner"]), + } + + # Sweep for any extra key→value pairs we didn’t explicitly target + extras = extract_all_text_pairs(root) + + # Merge non-empty fields + data = {"id": patient_id} + if name: data["name"] = name + for k, v in fields.items(): + if v and v.strip(): + data[k] = v.strip() + + # Add extras that aren't already present + for k, v in extras.items(): + if k not in data and v and v.strip(): + data[k] = v.strip() + + return data + +# ---------- main ---------- + +def main(): + if not PATIENT_ID or len(PATIENT_ID) < 8: + print("Set PATIENT_ID to a valid patient UUID.") + sys.exit(1) + + sf = Path(STATE_FILE) + if not sf.exists(): + print(f"Storage file not found: {sf}") + sys.exit(1) + + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) # set False to watch + context = browser.new_context(storage_state=str(sf)) + context.set_default_navigation_timeout(30000) + context.set_default_timeout(15000) + page = context.new_page() + + # Try via query param first + opened = open_detail_via_query(page, PATIENT_ID) + + # If not opened, go to base list and click the row + if not opened: + # Ensure the base grid exists + page.goto(BASE_URL, wait_until="domcontentloaded") + if "/prihlaseni" in page.url.lower(): + print("Redirected to login — refresh your medevio_storage.json.") + browser.close() + return + if not open_detail_by_click(page, PATIENT_ID): + print("Could not open detail panel (neither via query nor by clicking).") + browser.close() + return + + # At this point, detail should be open + data = extract_patient_detail(page, PATIENT_ID) + + print("\n=== Patient detail ===") + print(json.dumps(data, ensure_ascii=False, indent=2)) + + out = Path(f"patient_{PATIENT_ID}.json") + out.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + print(f"\nSaved → {out.resolve()}") + + browser.close() + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/Medevio3CapturePatients.py b/Medevio/Testy/Medevio3CapturePatients.py new file mode 100644 index 0000000..a4dd798 --- /dev/null +++ b/Medevio/Testy/Medevio3CapturePatients.py @@ -0,0 +1,111 @@ +from playwright.sync_api import sync_playwright +import mysql.connector +import time + +MYSQL_CFG = dict( + host="192.168.1.76", + port=3307, + user="root", + password="Vlado9674+", + database="medevio", +) + +# --- load 3 patients from DB --- +conn = mysql.connector.connect(**MYSQL_CFG) +with conn.cursor() as cur: + cur.execute(""" + SELECT rid, prijmeni, jmeno, rc + FROM patients_extracted + WHERE prijmeni IS NOT NULL and mamedevioucet is null + ORDER BY prijmeni ASC + LIMIT 3 + """) + rows = cur.fetchall() + if not rows: + raise RuntimeError("No entries found in patients_extracted") + +STATE_FILE = r"../medevio_storage.json" +BASE_URL = "https://my.medevio.cz/mudr-buzalkova/klinika/pacienti" + +NOT_FOUND_SEL = "div[role='alert']:has-text('Pacient nebyl nalezen'), div:has-text('Pacient nebyl nalezen')" +DIALOG_SEL = "[role='dialog'], div.MuiDrawer-paper, div[aria-modal='true']" + +def close_dialog_if_open(page): + dlg = page.locator(DIALOG_SEL) + try: + if dlg.count(): + # Try a close button; if not, press Escape + try: + dlg.locator("button:has-text('Zavřít'), [aria-label='Zavřít'], [aria-label='Close'], [data-testid='CloseIcon']").first.click(timeout=1000) + except: + page.keyboard.press("Escape") + page.wait_for_selector(DIALOG_SEL, state="detached", timeout=1500) + except: + pass # best-effort close + +def main(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + try: + context = browser.new_context(storage_state=STATE_FILE) + page = context.new_page() + + for rid, surname, name, rc in rows: + # 0) close any previous dialog to avoid stale matches + close_dialog_if_open(page) + + target_url = f"{BASE_URL}?pacient={rid}" + page.goto(target_url, wait_until="domcontentloaded") + page.wait_for_load_state("networkidle") + + # 1) Not-found toast? + try: + page.wait_for_selector(NOT_FOUND_SEL, timeout=3000) + print(f"{surname} {name} {rc} – ⚠️ pacient s RID {rid} nebyl nalezen, přeskočeno") + # (optional) set mamedevioucet=NULL for this rid here + continue + except: + pass + + # 2) Detail panel + try: + page.wait_for_selector(DIALOG_SEL, timeout=6000) + except: + print(f"⚠️ {surname} {name} {rc}: detailový panel se nenačetl, přeskočeno") + continue + + # 3) Verify dialog belongs to current patient (avoid stale dialog) + detail = page.locator(DIALOG_SEL).first + detail_text = detail.inner_text() + + if (surname not in detail_text) and (rc not in detail_text): + # Still looks wrong; give UI a moment and re-check once + page.wait_for_timeout(500) + detail_text = detail.inner_text() + if (surname not in detail_text) and (rc not in detail_text): + print(f"⚠️ {surname} {name} {rc}: detail neodpovídá (stará karta?), přeskočeno") + continue + + # 4) Check Medevio account text + if "zatím nemá Medevio účet" in detail_text: + has_account = 0 + print(f"{surname} {name} {rc} – ❌ zatím nemá Medevio účet") + else: + has_account = 1 + print(f"{surname} {name} {rc} – ✅ má Medevio účet") + + # Update DB by RID (or swap to rc if you prefer) + with conn.cursor() as c: + c.execute( + "UPDATE patients_extracted SET mamedevioucet = %s WHERE rid = %s", + (has_account, rid), + ) + conn.commit() + + time.sleep(0.5) # gentle pacing + + finally: + browser.close() + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/Medevio3CapturePatients01.py b/Medevio/Testy/Medevio3CapturePatients01.py new file mode 100644 index 0000000..890fb1e --- /dev/null +++ b/Medevio/Testy/Medevio3CapturePatients01.py @@ -0,0 +1,101 @@ +import time +from playwright.sync_api import sync_playwright +import mysql.connector + +MYSQL_CFG = dict( + host="192.168.1.76", + port=3307, + user="root", + password="Vlado9674+", + database="medevio", +) + +# --- load 3 patients from DB --- +conn = mysql.connector.connect(**MYSQL_CFG) +with conn.cursor() as cur: + cur.execute(""" + SELECT rid, prijmeni, jmeno, rc + FROM patients_extracted + WHERE prijmeni IS NOT NULL and mamedevioucet is null + ORDER BY prijmeni ASC + LIMIT 300 + """) + rows = cur.fetchall() + if not rows: + raise RuntimeError("No entries found in patients_extracted") + +STATE_FILE = r"../medevio_storage.json" +BASE_URL = "https://my.medevio.cz/mudr-buzalkova/klinika/pacienti" + +NOT_FOUND_SEL = "div[role='alert']:has-text('Pacient nebyl nalezen'), div:has-text('Pacient nebyl nalezen')" +DIALOG_SEL = "[role='dialog'], div.MuiDrawer-paper, div[aria-modal='true']" + +def close_dialog_if_open(page): + dlg = page.locator(DIALOG_SEL) + try: + if dlg.count(): + # Try a close button; if not, press Escape + try: + dlg.locator("button:has-text('Zavřít'), [aria-label='Zavřít'], [aria-label='Close'], [data-testid='CloseIcon']").first.click(timeout=1000) + except: + page.keyboard.press("Escape") + page.wait_for_selector(DIALOG_SEL, state="detached", timeout=1500) + except: + pass # best-effort close + +def main(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + context = browser.new_context(storage_state=STATE_FILE) + page = context.new_page() + + for rid, surname, name, rc in rows: + print(f"\nProcessing {surname} {name} {rc}") + + # 1️⃣ Navigation time + t0 = time.perf_counter() + page.goto(f"{BASE_URL}?pacient={rid}", wait_until="domcontentloaded") + # page.wait_for_load_state("networkidle") + t_nav = time.perf_counter() - t0 + print(f" ⏱️ page.goto + networkidle: {t_nav:.2f}s") + + # 2️⃣ Toast / dialog detection + t1 = time.perf_counter() + not_found = False + try: + page.wait_for_selector(NOT_FOUND_SEL, timeout=2500) + not_found = True + except: + pass + if not_found: + print(f" ⚠️ not-found toast detected after {time.perf_counter() - t1:.2f}s") + continue + + try: + page.wait_for_selector(DIALOG_SEL, timeout=8000) + except: + print(f" ⚠️ dialog not found (waited {time.perf_counter() - t1:.2f}s)") + continue + t_dialog = time.perf_counter() - t1 + print(f" ⏱️ toast/dialog detection: {t_dialog:.2f}s") + + # 3️⃣ Account check + DB update + t2 = time.perf_counter() + text = page.locator(DIALOG_SEL).first.inner_text() + has_account = 0 if "zatím nemá Medevio účet" in text else 1 + with conn.cursor() as c: + c.execute("UPDATE patients_extracted SET mamedevioucet=%s WHERE rid=%s", + (has_account, rid)) + conn.commit() + t_db = time.perf_counter() - t2 + print(f" ⏱️ DB update & text parse: {t_db:.2f}s") + + # 4️⃣ Optional pacing + t3 = time.perf_counter() + # time.sleep(0.5) + print(f" ⏱️ explicit sleep: {time.perf_counter() - t3:.2f}s") + + browser.close() + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/Medevio4-readandsavekartoteka.py b/Medevio/Testy/Medevio4-readandsavekartoteka.py new file mode 100644 index 0000000..187f908 --- /dev/null +++ b/Medevio/Testy/Medevio4-readandsavekartoteka.py @@ -0,0 +1,177 @@ +#Tento kod se pripoji do kartoteky Medevio, zmeni na 100 pacientu na stranu, nactene + + +# medevio_dump_patients_html_to_mysql.py +import time +import json +from pathlib import Path +from datetime import datetime +from typing import Set + +import mysql.connector +from mysql.connector import errorcode +from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout + +# ---------- CONFIG ---------- +STATE_FILE = r"../medevio_storage.json" +BASE_LIST_URL = "https://my.medevio.cz/mudr-buzalkova/klinika/pacienti" +SAVE_DELAY_SECONDS = 10 # throttle: 10 sec per patient + +# MySQL connection settings (fill in) +MYSQL_CFG = dict( + host="192.168.1.76", + port=3307, + user="root", + password="Vlado9674+", + database="medevio", +) + +# ---------- DB helpers ---------- +def db_connect(): + try: + conn = mysql.connector.connect(**MYSQL_CFG) + return conn + except mysql.connector.Error as e: + raise SystemExit(f"MySQL connection failed: {e}") + +# ---------- Playwright helpers ---------- +def wait_for_grid_ready(page): + # grid present & at least one row (be generous on timeout) + page.wait_for_selector("div[role='rowgroup']", timeout=20000) + page.wait_for_selector("div[role='row'][data-id]", timeout=20000) + +def set_page_size_100(page): #zde se nastavuje hodnota pacientu na stranu na 100, toto je jedno volani + # Click the page-size combobox (CZ/EN + generic) + for loc in [ + page.get_by_role("combobox", name="Řádků na stránce:"), + page.get_by_role("combobox", name="Rows per page:"), + page.locator("div.MuiTablePagination-root [role='combobox']"), + ]: + if loc.count(): + loc.first.click() + break + # Select 100 (MUI menu often renders in a portal) + opt = page.get_by_role("option", name="100") + if not opt.count(): + opt = page.locator("//li[normalize-space(.)='100']") + opt.first.wait_for(state="visible", timeout=5000) + opt.first.click() + # Wait for rows to refresh + try: + page.wait_for_selector("div[role='row'][data-id]", timeout=10000) + except PWTimeout: + time.sleep(0.8) + +def click_next_page(page) -> bool: #toto je kliknuti, aby se nacetla dalsi stranka se 100 zaznamy + # Prefer ARIA label + nxt = page.get_by_role("button", name="Go to next page") + if nxt.count(): + try: + if nxt.first.is_enabled(): + nxt.first.click() + return True + except Exception: + pass + # Fallback (CZ) + nxt2 = page.get_by_role("button", name="Další") + if nxt2.count(): + try: + if nxt2.first.is_enabled(): + nxt2.first.click() + return True + except Exception: + pass + return False + +# ---------- Main workflow ---------- +def save_all_patient_htmls(conn,context,next_round): #toto ulozi do mysql vsechny html stranky z kartoteky, takze cca 19 + page = context.new_page() + page.set_default_timeout(15000) + page.set_default_navigation_timeout(30000) + + # Use domcontentloaded (SPAs often keep network busy) + page.goto(BASE_LIST_URL, wait_until="domcontentloaded") + if "/prihlaseni" in page.url.lower(): + raise SystemExit("Session expired → refresh medevio_storage.json via the login script.") + + wait_for_grid_ready(page) + + # optional: print label like "1–25 z 1856" + try: + label = page.locator("p.MuiTablePagination-displayedRows").first.inner_text() + print("Pagination label BEFORE:", label) + except Exception: + pass + + # Set 100/page + try: + set_page_size_100(page) + try: + label = page.locator("p.MuiTablePagination-displayedRows").first.inner_text() + print("Pagination label AFTER :", label) + except Exception: + pass + except Exception as e: + print(f"Warning: could not set page size to 100: {e!r}") + + page_index = 1 + + while True: + wait_for_grid_ready(page) + + #here I need code to save page into kartoteka_html + cur = conn.cursor() + cur.execute( + f"""INSERT INTO kartoteka_html (html,round) + VALUES (%s,%s)""", + (page.content(),next_round), + ) + conn.commit() + cur.close() + print(f"DB saved page index {page_index}") + # Try to go next; if cannot, break + if not click_next_page(page): + break + # Wait for DOM to actually update (new rows) + try: + page.wait_for_load_state("domcontentloaded", timeout=10000) + except PWTimeout: + pass + time.sleep(0.5) + page_index += 1 + + page.close() + print(f"Total pages colleceted collected: {page_index}") + return + + +def main(): + # Check storage exists + if not Path(STATE_FILE).exists(): + raise SystemExit(f"Storage not found: {STATE_FILE}") + + # DB ready + conn = db_connect() + + #vymazat vsechny zaznamy z kartoteka_html, ktere nemaji hodnotu round + cur=conn.cursor() + cur.execute("delete from kartoteka_html where round=0") + conn.commit() + + with conn.cursor() as cur: + cur.execute("SELECT MAX(`round`) AS max_round FROM kartoteka_html") + result = cur.fetchone() + # If table empty, use 0 as fallback + next_round = (result[0] or 0) + 1 + print("Next round will be:", next_round) + + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) # set False to watch + context = browser.new_context(storage_state=STATE_FILE) + save_all_patient_htmls(conn, context,next_round) + browser.close() + conn.close() + print("Done.") + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/Medevio4.py b/Medevio/Testy/Medevio4.py new file mode 100644 index 0000000..db15b89 --- /dev/null +++ b/Medevio/Testy/Medevio4.py @@ -0,0 +1,262 @@ +#Tento kod se pripoji do kartoteky Medevio, zmeni na 100 pacientu na stranu, nactene + + +# medevio_dump_patients_html_to_mysql.py +import time +import json +from pathlib import Path +from datetime import datetime +from typing import Set + +import mysql.connector +from mysql.connector import errorcode +from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout + +# ---------- CONFIG ---------- +STATE_FILE = r"../medevio_storage.json" +BASE_LIST_URL = "https://my.medevio.cz/mudr-buzalkova/klinika/pacienti" +SAVE_DELAY_SECONDS = 10 # throttle: 10 sec per patient + +# MySQL connection settings (fill in) +MYSQL_CFG = dict( + host="192.168.1.76", + port=3307, + user="root", + password="Vlado9674+", + database="medevio", +) + +TABLE_NAME = "patients_html" # schema created automatically + + +# ---------- DB helpers ---------- +def db_connect(): + try: + conn = mysql.connector.connect(**MYSQL_CFG) + return conn + except mysql.connector.Error as e: + raise SystemExit(f"MySQL connection failed: {e}") + +def db_ensure_table(conn): + ddl = f""" + CREATE TABLE IF NOT EXISTS `{TABLE_NAME}` ( + patient_id VARCHAR(64) PRIMARY KEY, + html LONGTEXT NOT NULL, + fetched_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + """ + cur = conn.cursor() + cur.execute(ddl) + conn.commit() + cur.close() + +def db_existing_ids(conn) -> Set[str]: + ids = set() + cur = conn.cursor() + cur.execute(f"SELECT patient_id FROM `{TABLE_NAME}`") + for (pid,) in cur.fetchall(): + ids.add(pid) + cur.close() + return ids + +def db_upsert_html(conn, patient_id: str, html: str): + cur = conn.cursor() + cur.execute( + f"""INSERT INTO `{TABLE_NAME}` (patient_id, html, fetched_at) + VALUES (%s, %s, NOW()) + ON DUPLICATE KEY UPDATE html = VALUES(html), fetched_at = VALUES(fetched_at)""", + (patient_id, html), + ) + conn.commit() + cur.close() + + +# ---------- Playwright helpers ---------- +def wait_for_grid_ready(page): + # grid present & at least one row (be generous on timeout) + page.wait_for_selector("div[role='rowgroup']", timeout=20000) + page.wait_for_selector("div[role='row'][data-id]", timeout=20000) + +def set_page_size_100(page): + # Click the page-size combobox (CZ/EN + generic) + for loc in [ + page.get_by_role("combobox", name="Řádků na stránce:"), + page.get_by_role("combobox", name="Rows per page:"), + page.locator("div.MuiTablePagination-root [role='combobox']"), + ]: + if loc.count(): + loc.first.click() + break + # Select 100 (MUI menu often renders in a portal) + opt = page.get_by_role("option", name="100") + if not opt.count(): + opt = page.locator("//li[normalize-space(.)='100']") + opt.first.wait_for(state="visible", timeout=5000) + opt.first.click() + # Wait for rows to refresh + try: + page.wait_for_selector("div[role='row'][data-id]", timeout=10000) + except PWTimeout: + time.sleep(0.8) + +def harvest_ids_on_current_page(page) -> Set[str]: + ids = set() + for sel in ["div[role='row'][data-id]", "div.MuiDataGrid-row[data-id]"]: + for row in page.locator(sel).all(): + pid = row.get_attribute("data-id") + if pid: + ids.add(pid) + return ids + +def click_next_page(page) -> bool: + # Prefer ARIA label + nxt = page.get_by_role("button", name="Go to next page") + if nxt.count(): + try: + if nxt.first.is_enabled(): + nxt.first.click() + return True + except Exception: + pass + # Fallback (CZ) + nxt2 = page.get_by_role("button", name="Další") + if nxt2.count(): + try: + if nxt2.first.is_enabled(): + nxt2.first.click() + return True + except Exception: + pass + return False + +def ensure_detail_open(page) -> bool: + # Detail drawer/dialog visible? + for sel in ["[role='dialog']", "div.MuiDrawer-paper", "div[aria-modal='true']"]: + loc = page.locator(sel) + if loc.count() and loc.first.is_visible(): + return True + return False + + +# ---------- Main workflow ---------- +def collect_all_patient_ids(context) -> Set[str]: + page = context.new_page() + page.set_default_timeout(15000) + page.set_default_navigation_timeout(30000) + + # Use domcontentloaded (SPAs often keep network busy) + page.goto(BASE_LIST_URL, wait_until="domcontentloaded") + if "/prihlaseni" in page.url.lower(): + raise SystemExit("Session expired → refresh medevio_storage.json via the login script.") + + wait_for_grid_ready(page) + + # optional: print label like "1–25 z 1856" + try: + label = page.locator("p.MuiTablePagination-displayedRows").first.inner_text() + print("Pagination label BEFORE:", label) + except Exception: + pass + + # Set 100/page + try: + set_page_size_100(page) + try: + label = page.locator("p.MuiTablePagination-displayedRows").first.inner_text() + print("Pagination label AFTER :", label) + except Exception: + pass + except Exception as e: + print(f"Warning: could not set page size to 100: {e!r}") + + all_ids: Set[str] = set() + page_index = 1 + + while True: + wait_for_grid_ready(page) + ids_now = harvest_ids_on_current_page(page) + print(f"Page {page_index}: harvested {len(ids_now)} ids") + all_ids |= ids_now + + # Try to go next; if cannot, break + if not click_next_page(page): + break + + # Wait for DOM to actually update (new rows) + try: + page.wait_for_load_state("domcontentloaded", timeout=10000) + except PWTimeout: + pass + time.sleep(0.5) + page_index += 1 + + page.close() + print(f"Total unique IDs collected: {len(all_ids)}") + return all_ids + +def fetch_and_store_patient_html(context, conn, patient_id: str): + page = context.new_page() + page.set_default_timeout(15000) + page.set_default_navigation_timeout(30000) + + url = f"{BASE_LIST_URL}?pacient={patient_id}" + page.goto(url, wait_until="domcontentloaded") + + # If detail didn’t open, fallback: go to list, click row + if not ensure_detail_open(page): + page.goto(BASE_LIST_URL, wait_until="domcontentloaded") + try: + page.wait_for_selector(f"div[role='row'][data-id='{patient_id}']", timeout=15000) + page.locator(f"div[role='row'][data-id='{patient_id}']").first.click() + # wait for drawer/dialog + page.wait_for_selector("[role='dialog'], div.MuiDrawer-paper, div[aria-modal='true']", timeout=12000) + except PWTimeout: + print(f"[{patient_id}] detail panel did not open — skipping") + page.close() + return + + # Save full HTML of the page (includes the open detail drawer) + html = page.content() + db_upsert_html(conn, patient_id, html) + print(f"[{patient_id}] saved HTML ({len(html)} bytes) at {datetime.now().isoformat(timespec='seconds')}") + + page.close() + # Throttle per your requirement + time.sleep(SAVE_DELAY_SECONDS) + + +def main(): + # Check storage exists + if not Path(STATE_FILE).exists(): + raise SystemExit(f"Storage not found: {STATE_FILE}") + + # DB ready + conn = db_connect() + db_ensure_table(conn) + already = db_existing_ids(conn) + print(f"Already in DB: {len(already)} ids") + + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) # set False to watch + context = browser.new_context(storage_state=STATE_FILE) + + # 1) Collect all IDs from the listing (all pages) + # all_ids = collect_all_patient_ids(context) + all_ids=db_existing_ids(conn) + + # 2) Iterate and store HTML (skip existing) + todo = [pid for pid in sorted(all_ids) if pid not in already] + print(f"To fetch now: {len(todo)} ids (skipping {len(all_ids)-len(todo)} already saved)") + + for i, pid in enumerate(todo, 1): + try: + fetch_and_store_patient_html(context, conn, pid) + except Exception as e: + print(f"[{pid}] ERROR: {e!r} — continuing with next") + + browser.close() + conn.close() + print("Done.") + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/Medevio4_newrid.py b/Medevio/Testy/Medevio4_newrid.py new file mode 100644 index 0000000..e0a20e8 --- /dev/null +++ b/Medevio/Testy/Medevio4_newrid.py @@ -0,0 +1,258 @@ +# medevio_dump_patients_html_to_mysql.py +import time +import json +from pathlib import Path +from datetime import datetime +from typing import Set + +import mysql.connector +from mysql.connector import errorcode +from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout + +# ---------- CONFIG ---------- +STATE_FILE = r"../medevio_storage.json" +BASE_LIST_URL = "https://my.medevio.cz/mudr-buzalkova/klinika/pacienti" +SAVE_DELAY_SECONDS = 10 # throttle: 10 sec per patient + +# MySQL connection settings (fill in) +MYSQL_CFG = dict( + host="192.168.1.74", + port=3307, + user="root", + password="Vlado9674+", + database="medevio", +) + +TABLE_NAME = "patients_html" # schema created automatically + + +# ---------- DB helpers ---------- +def db_connect(): + try: + conn = mysql.connector.connect(**MYSQL_CFG) + return conn + except mysql.connector.Error as e: + raise SystemExit(f"MySQL connection failed: {e}") + +def db_ensure_table(conn): + ddl = f""" + CREATE TABLE IF NOT EXISTS `{TABLE_NAME}` ( + patient_id VARCHAR(64) PRIMARY KEY, + html LONGTEXT NOT NULL, + fetched_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + """ + cur = conn.cursor() + cur.execute(ddl) + conn.commit() + cur.close() + +def db_existing_ids(conn) -> Set[str]: + ids = set() + cur = conn.cursor() + cur.execute(f"SELECT patient_id FROM `{TABLE_NAME}`") + for (pid,) in cur.fetchall(): + ids.add(pid) + cur.close() + return ids + +def db_upsert_html(conn, patient_id: str, html: str): + cur = conn.cursor() + cur.execute( + f"""INSERT INTO `{TABLE_NAME}` (patient_id, html, fetched_at) + VALUES (%s, %s, NOW()) + ON DUPLICATE KEY UPDATE html = VALUES(html), fetched_at = VALUES(fetched_at)""", + (patient_id, html), + ) + conn.commit() + cur.close() + + +# ---------- Playwright helpers ---------- +def wait_for_grid_ready(page): + # grid present & at least one row (be generous on timeout) + page.wait_for_selector("div[role='rowgroup']", timeout=20000) + page.wait_for_selector("div[role='row'][data-id]", timeout=20000) + +def set_page_size_100(page): + # Click the page-size combobox (CZ/EN + generic) + for loc in [ + page.get_by_role("combobox", name="Řádků na stránce:"), + page.get_by_role("combobox", name="Rows per page:"), + page.locator("div.MuiTablePagination-root [role='combobox']"), + ]: + if loc.count(): + loc.first.click() + break + # Select 100 (MUI menu often renders in a portal) + opt = page.get_by_role("option", name="100") + if not opt.count(): + opt = page.locator("//li[normalize-space(.)='100']") + opt.first.wait_for(state="visible", timeout=5000) + opt.first.click() + # Wait for rows to refresh + try: + page.wait_for_selector("div[role='row'][data-id]", timeout=10000) + except PWTimeout: + time.sleep(0.8) + +def harvest_ids_on_current_page(page) -> Set[str]: + ids = set() + for sel in ["div[role='row'][data-id]", "div.MuiDataGrid-row[data-id]"]: + for row in page.locator(sel).all(): + pid = row.get_attribute("data-id") + if pid: + ids.add(pid) + return ids + +def click_next_page(page) -> bool: + # Prefer ARIA label + nxt = page.get_by_role("button", name="Go to next page") + if nxt.count(): + try: + if nxt.first.is_enabled(): + nxt.first.click() + return True + except Exception: + pass + # Fallback (CZ) + nxt2 = page.get_by_role("button", name="Další") + if nxt2.count(): + try: + if nxt2.first.is_enabled(): + nxt2.first.click() + return True + except Exception: + pass + return False + +def ensure_detail_open(page) -> bool: + # Detail drawer/dialog visible? + for sel in ["[role='dialog']", "div.MuiDrawer-paper", "div[aria-modal='true']"]: + loc = page.locator(sel) + if loc.count() and loc.first.is_visible(): + return True + return False + + +# ---------- Main workflow ---------- +def collect_all_patient_ids(context) -> Set[str]: + page = context.new_page() + page.set_default_timeout(15000) + page.set_default_navigation_timeout(30000) + + # Use domcontentloaded (SPAs often keep network busy) + page.goto(BASE_LIST_URL, wait_until="domcontentloaded") + if "/prihlaseni" in page.url.lower(): + raise SystemExit("Session expired → refresh medevio_storage.json via the login script.") + + wait_for_grid_ready(page) + + # optional: print label like "1–25 z 1856" + try: + label = page.locator("p.MuiTablePagination-displayedRows").first.inner_text() + print("Pagination label BEFORE:", label) + except Exception: + pass + + # Set 100/page + try: + set_page_size_100(page) + try: + label = page.locator("p.MuiTablePagination-displayedRows").first.inner_text() + print("Pagination label AFTER :", label) + except Exception: + pass + except Exception as e: + print(f"Warning: could not set page size to 100: {e!r}") + + all_ids: Set[str] = set() + page_index = 1 + + while True: + wait_for_grid_ready(page) + ids_now = harvest_ids_on_current_page(page) + print(f"Page {page_index}: harvested {len(ids_now)} ids") + all_ids |= ids_now + + # Try to go next; if cannot, break + if not click_next_page(page): + break + + # Wait for DOM to actually update (new rows) + try: + page.wait_for_load_state("domcontentloaded", timeout=10000) + except PWTimeout: + pass + time.sleep(0.5) + page_index += 1 + + page.close() + print(f"Total unique IDs collected: {len(all_ids)}") + return all_ids + +def fetch_and_store_patient_html(context, conn, patient_id: str): + page = context.new_page() + page.set_default_timeout(15000) + page.set_default_navigation_timeout(30000) + + url = f"{BASE_LIST_URL}?pacient={patient_id}" + page.goto(url, wait_until="domcontentloaded") + + # If detail didn’t open, fallback: go to list, click row + if not ensure_detail_open(page): + page.goto(BASE_LIST_URL, wait_until="domcontentloaded") + try: + page.wait_for_selector(f"div[role='row'][data-id='{patient_id}']", timeout=15000) + page.locator(f"div[role='row'][data-id='{patient_id}']").first.click() + # wait for drawer/dialog + page.wait_for_selector("[role='dialog'], div.MuiDrawer-paper, div[aria-modal='true']", timeout=12000) + except PWTimeout: + print(f"[{patient_id}] detail panel did not open — skipping") + page.close() + return + + # Save full HTML of the page (includes the open detail drawer) + html = page.content() + db_upsert_html(conn, patient_id, html) + print(f"[{patient_id}] saved HTML ({len(html)} bytes) at {datetime.now().isoformat(timespec='seconds')}") + + page.close() + # Throttle per your requirement + time.sleep(SAVE_DELAY_SECONDS) + + +def main(): + # Check storage exists + if not Path(STATE_FILE).exists(): + raise SystemExit(f"Storage not found: {STATE_FILE}") + + # DB ready + conn = db_connect() + db_ensure_table(conn) + already = db_existing_ids(conn) + print(f"Already in DB: {len(already)} ids") + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) # set False to watch + context = browser.new_context(storage_state=STATE_FILE) + + # 1) Collect all IDs from the listing (all pages) + all_ids = collect_all_patient_ids(context) + + # 2) Iterate and store HTML (skip existing) + todo = [pid for pid in sorted(all_ids) if pid not in already] + print(f"To fetch now: {len(todo)} ids (skipping {len(all_ids)-len(todo)} already saved)") + + for i, pid in enumerate(todo, 1): + try: + fetch_and_store_patient_html(context, conn, pid) + except Exception as e: + print(f"[{pid}] ERROR: {e!r} — continuing with next") + + browser.close() + conn.close() + print("Done.") + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/Medevio5_ReadNamesFromKartoteka_html.py b/Medevio/Testy/Medevio5_ReadNamesFromKartoteka_html.py new file mode 100644 index 0000000..79893d7 --- /dev/null +++ b/Medevio/Testy/Medevio5_ReadNamesFromKartoteka_html.py @@ -0,0 +1,110 @@ +import mysql.connector +from bs4 import BeautifulSoup +import re +import time + +MYSQL_CFG = dict( + host="192.168.1.76", + port=3307, + user="root", + password="Vlado9674+", + database="medevio", +) + +#Helper functions +def is_valid_rc(rc: str) -> bool: + """ + Very basic RC check: + – remove any slash + – must be 9 or 10 digits + """ + rc_clean = rc.replace("/", "") + return bool(re.fullmatch(r"\d{9,10}", rc_clean)) + +conn = mysql.connector.connect(**MYSQL_CFG) + +# --- get latest HTML (single-row result) --- +with conn.cursor() as cur: + cur.execute(""" + SELECT html + FROM kartoteka_html + where round=3 + ORDER BY `fetched-at` DESC + """) + rows = cur.fetchall() + if not rows: + raise RuntimeError("No HTML found in kartoteka_html") + +for row in rows: + + html = row[0] + + soup = BeautifulSoup(html, "html.parser") + + records = [] + for row in soup.find_all("div", attrs={"role": "row", "data-id": True}): + data_id = row["data-id"] + + # full name -> surname + rest + name_btn = row.find("button", class_="MuiTypography-root") + fullname = name_btn.get_text(strip=True) if name_btn else "" + parts = fullname.split() + surname = parts[0] if parts else "" + name = " ".join(parts[1:]) if len(parts) > 1 else "" + + # RC + id_cell = row.find("div", attrs={"data-field": "IdentificationNumber"}) + rc = (id_cell.get("title", "") if id_cell else "") + rc = rc.replace("/", "").replace("\\", "") + + # Phone + ph_cell = row.find("div", attrs={"data-field": "Phone"}) + raw_phone = ph_cell.get("title", "") if ph_cell else "" + raw_phone = raw_phone.replace("\u00A0", " ") # NBSP -> space + phone = re.sub(r"[^\d+]", "", raw_phone) # keep + and digits + + # Insurance + ins_cell = row.find("div", attrs={"data-field": "InsuranceCompany"}) + poj = ins_cell.get("title", "") if ins_cell else "" + + # Skip rows with no name or no RC or not valid TC + if not fullname or not rc: + continue + if not is_valid_rc(rc): + continue + + records.append((data_id, fullname, rc, phone, poj)) + + # --- per-patient lookup: use a fresh cursor each time (or buffered=True) --- + with conn.cursor(buffered=True) as cur2: + cur2.execute( + """ + SELECT * + FROM patients_extracted + WHERE rc=%s + """, + (rc,), + ) + rows = cur2.fetchall() + + # print(surname, name, rc, len(rows)) + + if len(rows) > 1: + print(f"Pacient {surname} {name} {rc} je v medeviu {len(rows)}x") + time.sleep(1) + if len(rows)==0: + print(f"Pacient {surname} {name} {rc} je v medeviu {len(rows)}x") + time.sleep(1) + if len(rows)==1 and rows[0][0]!=data_id: + print(f"Pacient {surname} {name} {rc} má v medeviu jiný id, v db je {rows[0][0]} and nyní je {data_id}") + time.sleep(.1) + + if len(rows) == 1: + cur2.execute(""" + Update patients_extracted set rid=%s where rc=%s""",(data_id,rc)) + conn.commit() + # preview + # for r in records[:10]: + # print(f"ID: {r[0]} Name: {r[1]} RC: {r[2]} Phone: {r[3]} Pojistovna: {r[4]}") + # + # print("Total patients:", len(records)) diff --git a/Medevio/Testy/Medevio6 ReadPatientCards.py b/Medevio/Testy/Medevio6 ReadPatientCards.py new file mode 100644 index 0000000..e69de29 diff --git a/Medevio/Testy/Merevio05ReadWhetherRegisteredMedevio.py b/Medevio/Testy/Merevio05ReadWhetherRegisteredMedevio.py new file mode 100644 index 0000000..e1b593e --- /dev/null +++ b/Medevio/Testy/Merevio05ReadWhetherRegisteredMedevio.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import time +from pathlib import Path +from datetime import datetime +import pymysql +from pymysql.cursors import DictCursor +from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout + +# ========= CONFIG ========= +MYSQL_CFG = dict( + host="192.168.1.76", + port=3307, + user="root", + password="Vlado9674+", + database="medevio", + cursorclass=DictCursor, + autocommit=False, # we commit in batches +) + +# Column in patients_extracted that stores Medevio UUID used in the URL: +UUID_COLUMN = "rid" # <-- change if your column name differs + +# Output columns (will be created if missing; MySQL 8.0+ supports IF NOT EXISTS): +REGISTERED_COL = "medevio_registered" # TINYINT(1) NULL/0/1 +CHECKED_AT_COL = "medevio_checked_at" # DATETIME NULL +ERROR_COL = "medevio_check_error" # TEXT NULL (optional) + +# Medevio routing +PATIENT_URL_TMPL = "https://my.medevio.cz/mudr-buzalkova/klinika/pacienti?pacient={uuid}" + +# Login session (created earlier with your script) +STATE_FILE = Path("../medevio_storage.json") + +# Batch/pace +BATCH_LIMIT = 5 # how many patients per run +SLEEP_SECONDS = 3 # wait between patients (requested) +NAV_TIMEOUT = 20_000 # ms +TEXT_TIMEOUT = 15_000 # ms (for main area/heading) + +# Texts indicating NOT registered: +NOT_REGISTERED_STRINGS = [ + "Pacientka zatím nemá Medevio účet.", + "Pacient zatím nemá Medevio účet.", +] +# ========================== + + + + +SELECT_SQL = f""" +SELECT {UUID_COLUMN} AS uuid, jmeno, prijmeni, rc +FROM patients_extracted +WHERE {UUID_COLUMN} IS NOT NULL + AND {UUID_COLUMN} <> '' + AND {REGISTERED_COL} IS NULL +LIMIT {BATCH_LIMIT}; +""" + +UPDATE_OK_SQL = f""" +UPDATE patients_extracted +SET {REGISTERED_COL}=%s, {CHECKED_AT_COL}=NOW(), {ERROR_COL}=NULL +WHERE {UUID_COLUMN}=%s +""" + +UPDATE_ERR_SQL = f""" +UPDATE patients_extracted +SET {REGISTERED_COL}=NULL, {CHECKED_AT_COL}=NOW(), {ERROR_COL}=%s +WHERE {UUID_COLUMN}=%s +""" + +DDL_SQLS = [ + f"ALTER TABLE patients_extracted ADD COLUMN {REGISTERED_COL} TINYINT(1) NULL", + f"ALTER TABLE patients_extracted ADD COLUMN {CHECKED_AT_COL} DATETIME NULL", + f"ALTER TABLE patients_extracted ADD COLUMN {ERROR_COL} TEXT NULL", +] + +CHECKS_FOR_DDL_SQLS=[ + f"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'patients_extracted' AND COLUMN_NAME = '{REGISTERED_COL}'", + f"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'patients_extracted' AND COLUMN_NAME = '{CHECKED_AT_COL}'", + f"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'patients_extracted' AND COLUMN_NAME = '{ERROR_COL}'", +] +def ensure_columns(conn): + with conn.cursor() as cur: + for ddl,ddlcheck in zip(DDL_SQLS,CHECKS_FOR_DDL_SQLS): + cur.execute(ddlcheck) + row = cur.fetchone() + if row["cnt"] == 0: + print("Column missing") + cur.execute(ddl) + print(f"✓ Executed: {ddl}") + else: + print("Column exists") + conn.commit() + + +def pick_registered_flag(page_text: str) -> int: + text = page_text or "" + # If any NOT-registered phrase is present → 0; otherwise assume registered → 1 + for marker in NOT_REGISTERED_STRINGS: + if marker in text: + return 0 + return 1 + + +def main(): + # --- DB: fetch a batch to process --- + conn = pymysql.connect(**MYSQL_CFG) + try: + ensure_columns(conn) + with conn.cursor() as cur: + cur.execute("SET NAMES utf8mb4 COLLATE utf8mb4_czech_ci") + cur.execute("SET collation_connection = 'utf8mb4_czech_ci'") + + cur.execute(SELECT_SQL) + rows = cur.fetchall() + + if not rows: + print("No patients to check (all have medevio_registered filled).") + return + + print(f"Will process {len(rows)} patients…") + + # --- Playwright session --- + with sync_playwright() as p: + browser = p.chromium.launch(headless=False, slow_mo=0) + context = browser.new_context(storage_state=str(STATE_FILE)) + page = context.new_page() + page.set_default_timeout(NAV_TIMEOUT) + + processed = ok = errs = 0 + + for r in rows: + processed += 1 + # pid = r["id"] + uuid = r["uuid"] + name = f"{r.get('prijmeni','')}, {r.get('jmeno','')}" + rc = r.get("rc","") + + url = PATIENT_URL_TMPL.format(uuid=uuid) + print(f"URL pro otevření pacienta je: {url}0") + print(f"[{processed:>3}] {name} | RC {rc} | {uuid} → {url}") + + try: + page.goto(url, wait_until="domcontentloaded") + # Optionally wait for a stable anchor; fallback to sleep + try: + # A stable bit we saw earlier + page.get_by_text("Historie požadavků").wait_for(timeout=TEXT_TIMEOUT) + except PWTimeout: + pass + + # Wait the requested 3 seconds for the UI to settle + time.sleep(SLEEP_SECONDS) + + # Get full text and detect + full_text = page.content() # HTML; safer to check visible text too: + vis_text = page.inner_text("body") + + registered = pick_registered_flag(full_text) if full_text else pick_registered_flag(vis_text) + + with conn.cursor() as cur: + cur.execute(UPDATE_OK_SQL, (registered, pid)) + conn.commit() + ok += 1 + state = "REGISTERED" if registered == 1 else "NOT REGISTERED" + print(f" → {state}") + + except Exception as e: + conn.rollback() + errs += 1 + msg = f"{type(e).__name__}: {e}" + with conn.cursor() as cur: + cur.execute(UPDATE_ERR_SQL, (msg[:1000], pid)) + conn.commit() + print(f" ! ERROR → {msg}") + + browser.close() + + print(f"Done. processed={processed}, ok={ok}, errors={errs}") + + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/ScrapePozadavkyJustManualScrolling.py b/Medevio/Testy/ScrapePozadavkyJustManualScrolling.py new file mode 100644 index 0000000..1f7de4a --- /dev/null +++ b/Medevio/Testy/ScrapePozadavkyJustManualScrolling.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import re +from urllib.parse import urlparse, parse_qs +from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout, Page + +STATE_FILE = "../medevio_storage.json" +POZADAVKY_URL = "https://my.medevio.cz/mudr-buzalkova/klinika/pozadavky?neprirazene=1" + +# ---------- helpers ---------- + +def get_uuid_from_href(href: str) -> str | None: + try: + q = parse_qs(urlparse(href).query) + val = q.get("pozadavek", [None])[0] + return val if val else None + except Exception: + return None + +def is_flu_request(text: str) -> bool: + return bool(re.search(r"ch(r|ř)ipk", text, re.IGNORECASE)) + +def scrape_visible_rows(page: Page, seen: set) -> list[dict]: + """Collect all *new* visible rows on the current screen.""" + bucket: list[dict] = [] + rows = page.locator('tr[data-testid="patient-request-row"]') + n = rows.count() + + for i in range(n): + row = rows.nth(i) + href_el = row.locator('a[href*="pozadavky?pozadavek="]').first + href = href_el.get_attribute("href") if href_el.count() else None + req_id = get_uuid_from_href(href) if href else None + if not req_id or req_id in seen: + continue + + name = (row.locator('td:nth-child(2) a span').first.text_content(timeout=0) or "").strip() + rc = (row.locator('a.MuiTypography-overline2').first.text_content(timeout=0) or "").strip() + + text_p = row.locator('td:nth-child(3) p.MuiTypography-body1, td:nth-child(4) p.MuiTypography-body1').first + text_req = (text_p.text_content(timeout=0) or "").strip() + if not text_req: + aria = row.locator('td:nth-child(3) [aria-label], td:nth-child(4) [aria-label]').first + text_req = (aria.get_attribute("aria-label") or "").strip() if aria.count() else "" + + avatar = row.locator('[data-testid="queue-avatar"]').first + assigned_to = (avatar.get_attribute("aria-label") or "").strip() if avatar.count() else "" + initials = (avatar.text_content(timeout=0) or "").strip() if avatar.count() else "" + + seen.add(req_id) + bucket.append({ + "id": req_id, + "name": name, + "rc": rc, + "text": text_req, + "assigned_to": assigned_to, + "initials": initials, + }) + return bucket + +def assign_request_to_buzalka(page: Page, request_uuid: str) -> None: + """Open request detail by UUID and assign it to MUDr. Buzalka (já).""" + url = f"{POZADAVKY_URL.split('?')[0]}?pozadavek={request_uuid}" + page.goto(url, wait_until="domcontentloaded", timeout=60_000) + + combo = page.locator('div[role="combobox"][aria-labelledby="queue-select-label"]') + combo.wait_for(state="visible") + combo.click() + + option = page.get_by_role("option", name=re.compile(r"MUDr\.?\s*Buzalka", re.I)) + option.click() + + page.wait_for_load_state("networkidle") + page.locator("button.MuiDialog-close").click() + + print(f"✔ Assigned to MUDr. Buzalka: {request_uuid}") + +# ---------- main ---------- + +def main(): + with sync_playwright() as pw: + browser = pw.chromium.launch(headless=False) # we want to see the page + context = browser.new_context(storage_state=STATE_FILE) + page = context.new_page() + + page.goto(POZADAVKY_URL, wait_until="domcontentloaded", timeout=60_000) + + # check login + body = (page.text_content("body") or "").lower() + if any(x in body for x in ["přihlášení", "přihlásit", "sign in", "login"]): + raise SystemExit("Not logged in – refresh medevio_storage.json.") + + try: + page.wait_for_selector('tr[data-testid="patient-request-row"]', timeout=20_000) + except PWTimeout: + raise SystemExit("Rows not found: tr[data-testid=patient-request-row].") + + seen: set[str] = set() + assigned_count = 0 + + print("\n>>> Scroll the page manually. Press Enter here any time to scrape current view.") + print(" Press Ctrl+C to finish.\n") + + while True: + input("Press Enter to scan visible rows...") + for item in scrape_visible_rows(page, seen): + text = item["text"] + initials = (item["initials"] or "").upper() + assigned_to = (item["assigned_to"] or "").lower() + + if is_flu_request(text) and not ("buzalka" in assigned_to or initials == "VB"): + assign_request_to_buzalka(page, item["id"]) + assigned_count += 1 + print(f"Total newly assigned so far: {assigned_count}") + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/ScrapePozadavkyPrint01.py b/Medevio/Testy/ScrapePozadavkyPrint01.py new file mode 100644 index 0000000..e9b1ed6 --- /dev/null +++ b/Medevio/Testy/ScrapePozadavkyPrint01.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from urllib.parse import urlparse, parse_qs +import re +import time +from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout, Page + +# ===== funkce pro přiřazení jednoho požadavku ===== +def assign_request_to_buzalka(page: Page, request_uuid: str) -> None: + """ + Otevře kartu požadavku podle UUID a přiřadí ji MUDr. Buzalka (já). + Po uložení změny zavře dialog a vypíše potvrzení. + """ + url = f"https://my.medevio.cz/mudr-buzalkova/klinika/pozadavky?pozadavek={request_uuid}" + page.goto(url, wait_until="domcontentloaded", timeout=60_000) + + combo = page.locator('div[role="combobox"][aria-labelledby="queue-select-label"]') + combo.wait_for(state="visible") + combo.click() + + option = page.get_by_role("option", name=re.compile(r"MUDr\.?\s*Buzalka", re.I)) + option.click() + + page.wait_for_load_state("networkidle") + + page.locator("button.MuiDialog-close").click() + print(f"✔ Požadavek {request_uuid} přiřazen: MUDr. Buzalka (já)") + +# ===== hlavní část: projít listing a řešit chřipku ===== +POZADAVKY_URL = "https://my.medevio.cz/mudr-buzalkova/klinika/pozadavky" +# POZADAVKY_URL = "https://my.medevio.cz/mudr-buzalkova/klinika/pozadavky?neprirazene=1" + +STATE_FILE = "../medevio_storage.json" + +from playwright.sync_api import Page +import time + +def _find_scroll_container(page: Page): + """Return an ElementHandle of the real scrollable container, or None -> use window.""" + handle = page.evaluate_handle(""" +() => { + const isScrollable = el => !!el && (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth); + + const row = document.querySelector('tr[data-testid="patient-request-row"]'); + if (row) { + let el = row.parentElement; + while (el) { + const style = getComputedStyle(el); + const overflowY = style.overflowY; + if (isScrollable(el) && (overflowY === 'auto' || overflowY === 'scroll')) return el; + el = el.parentElement; + } + } + const guesses = [ + '[role="rowgroup"]', '[role="table"]', '.MuiTableContainer-root', + '[data-testid="requests-table"]', '.MuiContainer-root', 'main' + ]; + for (const sel of guesses) { + const el = document.querySelector(sel); + if (el) { + const style = getComputedStyle(el); + const overflowY = style.overflowY; + if (isScrollable(el) && (overflowY === 'auto' || overflowY === 'scroll')) return el; + } + } + return null; +} +""") + # If JS returned null, convert to Python None + try: + if handle is None or handle.json_value() is None: + return None + except Exception: + return None + return handle + +def _has_handle(page: Page, handle) -> bool: + """Check the handle still points to a live element; else False -> use window.""" + if not handle: + return False + try: + return bool(page.evaluate("(el)=>!!el", handle)) + except Exception: + return False + +def _scroll_step(page: Page, container_handle, px=800): + if _has_handle(page, container_handle): + try: + page.evaluate( + "(args) => { const [el, dy] = args; el.scrollBy(0, dy); }", + [container_handle, px] + ) + return + except Exception: + pass + # Fallback to window + page.evaluate("dy => window.scrollBy(0, dy)", px) + +def _scroll_to_bottom(page: Page, container_handle): + if _has_handle(page, container_handle): + try: + page.evaluate("(el) => el.scrollTo(0, el.scrollHeight)", container_handle) + return + except Exception: + pass + page.evaluate("() => window.scrollTo(0, document.body.scrollHeight)") + +def _click_load_more_if_any(page: Page) -> bool: + btn = page.locator("button:has-text('Načíst více'), button:has-text('Zobrazit další'), button:has-text('Load more')") + if btn.count() and btn.is_visible(): + btn.click() + return True + return False + +def load_all_requests(page: Page, max_rounds: int = 200, stagnation_limit: int = 4) -> None: + """ + Incrementally loads the entire list of requests. + Stops after 'stagnation_limit' rounds without row growth, or after max_rounds. + """ + page.wait_for_selector('tr[data-testid="patient-request-row"]', timeout=20000) + + container = _find_scroll_container(page) + prev_count = page.locator('tr[data-testid="patient-request-row"]').count() + stagnant = 0 + + for _ in range(max_rounds): + if _click_load_more_if_any(page): + page.wait_for_load_state("networkidle") + + # small incremental scrolls + for _ in range(4): + _scroll_step(page, container, px=800) + time.sleep(0.15) + + # touch bottom at least once + _scroll_to_bottom(page, container) + + # settle + page.wait_for_load_state("networkidle") + spinners = page.locator('[role="progressbar"], .MuiCircularProgress-root') + if spinners.count(): + try: + spinners.first.wait_for(state="detached", timeout=5000) + except Exception: + pass + + # growth check + curr_count = page.locator('tr[data-testid="patient-request-row"]').count() + if curr_count <= prev_count: + stagnant += 1 + else: + stagnant = 0 + prev_count = curr_count + + if stagnant >= stagnation_limit: + break + + +def get_uuid_from_href(href: str) -> str | None: + try: + q = parse_qs(urlparse(href).query) + val = q.get("pozadavek", [None])[0] + return val + except Exception: + return None + +def is_flu_request(text: str) -> bool: + # libovolná varianta slova „chřipk“ (chřipka, chřipky, …), case-insensitive, s diakritikou i bez + return bool(re.search(r"ch(r|ř)ipk", text, re.IGNORECASE)) + +def main(): + with sync_playwright() as pw: + browser = pw.chromium.launch(headless=False) + context = browser.new_context(storage_state=STATE_FILE) + page = context.new_page() + + page.goto(POZADAVKY_URL, wait_until="domcontentloaded", timeout=60_000) + + body = (page.text_content("body") or "").lower() + if any(x in body for x in ["přihlášení", "přihlásit", "sign in", "login"]): + raise SystemExit("Vypadá to, že nejsi přihlášený – obnov prosím medevio_storage.json.") + + try: + page.wait_for_selector('tr[data-testid="patient-request-row"]', timeout=20_000) + except PWTimeout: + raise SystemExit("Nenašel jsem řádky požadavků (selector tr[data-testid=patient-request-row]).") + + + + # after navigating to the listing and ensuring first rows are visible: + load_all_requests(page) + rows = page.locator('tr[data-testid="patient-request-row"]') + print("Loaded rows:", rows.count()) + + for i in range(count): + row = rows.nth(i) + + # UUID z href + a_with_req = row.locator('a[href*="pozadavky?pozadavek="]').first + href = a_with_req.get_attribute("href") if a_with_req.count() else None + req_id = get_uuid_from_href(href) if href else None + if not req_id: + continue + + # Text požadavku (pro filtr „chřipka“) + text_p = row.locator('td:nth-child(3) p.MuiTypography-body1, td:nth-child(4) p.MuiTypography-body1').first + text_req = text_p.inner_text().strip() if text_p.count() else "" + if not text_req: + aria = row.locator('td:nth-child(3) [aria-label], td:nth-child(4) [aria-label]').first + text_req = (aria.get_attribute("aria-label") or "").strip() if aria.count() else "" + + if not is_flu_request(text_req): + # není to chřipkový požadavek – přeskočit + continue + + # Zjištění přiřazení z avatara v listingu + avatar = row.locator('[data-testid="queue-avatar"]').first + assigned_to = (avatar.get_attribute("aria-label") or "").strip() if avatar.count() else "" + initials = avatar.inner_text().strip() if avatar.count() else "" + + already_mine = ("buzalka" in assigned_to.lower()) or (initials.upper() == "VB") + + if already_mine: + print(f"= SKIP (už přiřazeno mně): {req_id} | {text_req}") + continue + + print(f"→ Přiřazuji chřipkový požadavek: {req_id} | {text_req}") + assign_request_to_buzalka(page, req_id) + time.sleep(1) + + context.close() + browser.close() + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/ScrapePozadavkyZmenJeden.py b/Medevio/Testy/ScrapePozadavkyZmenJeden.py new file mode 100644 index 0000000..45cac59 --- /dev/null +++ b/Medevio/Testy/ScrapePozadavkyZmenJeden.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import re +from playwright.sync_api import sync_playwright + +STATE_FILE = "../medevio_storage.json" +REQUEST_URL = ( + "https://my.medevio.cz/mudr-buzalkova/klinika/" + "pozadavky?pozadavek=e28cbf71-8280-4078-a881-c44119bbccc2" +) + +def main(): + with sync_playwright() as pw: + browser = pw.chromium.launch(headless=False) + context = browser.new_context(storage_state=STATE_FILE) + page = context.new_page() + + # otevři konkrétní požadavek + page.goto(REQUEST_URL, wait_until="domcontentloaded", timeout=60_000) + + # combobox Fronta + combo = page.locator('div[role="combobox"][aria-labelledby="queue-select-label"]') + combo.wait_for(state="visible") + combo.click() + + # vyber „MUDr. Buzalka (já)“ + option = page.get_by_role("option", name=re.compile(r"MUDr\.?\s*Buzalka", re.I)) + option.click() + + # počkej, dokud síť neutichne (změna je odeslaná/uložená) + page.wait_for_load_state("networkidle") + + # zavři dialog + page.locator("button.MuiDialog-close").click() + + context.close() + browser.close() + +if __name__ == "__main__": + main() diff --git a/Medevio/Testy/activate_this.py b/Medevio/Testy/activate_this.py new file mode 100644 index 0000000..cdef4d7 --- /dev/null +++ b/Medevio/Testy/activate_this.py @@ -0,0 +1,36 @@ +""" +Activate virtualenv for current interpreter: + +Use exec(open(this_file).read(), {'__file__': this_file}). + +This can be used when you must use an existing Python interpreter, not the virtualenv bin/python. +""" # noqa: D415 +from __future__ import annotations + +import os +import site +import sys + +try: + abs_file = os.path.abspath(__file__) +except NameError as exc: + msg = "You must use exec(open(this_file).read(), {'__file__': this_file}))" + raise AssertionError(msg) from exc + +bin_dir = os.path.dirname(abs_file) +base = bin_dir[: -len("Scripts") - 1] # strip away the bin part from the __file__, plus the path separator + +# prepend bin to PATH (this file is inside the bin directory) +os.environ["PATH"] = os.pathsep.join([bin_dir, *os.environ.get("PATH", "").split(os.pathsep)]) +os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory +os.environ["VIRTUAL_ENV_PROMPT"] = "" or os.path.basename(base) # noqa: SIM222 + +# add the virtual environments libraries to the host python import mechanism +prev_length = len(sys.path) +for lib in "..\\Lib\\site-packages".split(os.pathsep): + path = os.path.realpath(os.path.join(bin_dir, lib)) + site.addsitedir(path.decode("utf-8") if "" else path) +sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length] + +sys.real_prefix = sys.prefix +sys.prefix = base diff --git a/Medevio/Testy/capture_patient_0117c85c-630d-44c4-a5ca-dd9f3b28e25e/detail.html b/Medevio/Testy/capture_patient_0117c85c-630d-44c4-a5ca-dd9f3b28e25e/detail.html new file mode 100644 index 0000000..0412b72 --- /dev/null +++ b/Medevio/Testy/capture_patient_0117c85c-630d-44c4-a5ca-dd9f3b28e25e/detail.html @@ -0,0 +1,12 @@ + + Kartotéka | Medevio + + + + + + + + + + \ No newline at end of file diff --git a/Medevio/Testy/capture_patient_0117c85c-630d-44c4-a5ca-dd9f3b28e25e/detail.png b/Medevio/Testy/capture_patient_0117c85c-630d-44c4-a5ca-dd9f3b28e25e/detail.png new file mode 100644 index 0000000..51c5a85 Binary files /dev/null and b/Medevio/Testy/capture_patient_0117c85c-630d-44c4-a5ca-dd9f3b28e25e/detail.png differ diff --git a/Medevio/Testy/dddddd.py b/Medevio/Testy/dddddd.py new file mode 100644 index 0000000..f5f717e --- /dev/null +++ b/Medevio/Testy/dddddd.py @@ -0,0 +1,315 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +FAST FILE HASH INDEXER – UNRAID (BLAKE3 ONLY, ALL SHARES) +- HARDCODED SINGLE SHARE MODE +- SQL OPTIMIZATION +- STRICT MODE (NO TOLERANCE) - Updates DB on any mismatch +""" + +import os +import pymysql +import socket +import platform +from blake3 import blake3 + +# ============================== +# ENV / HOST +# ============================== + +HOSTNAME = socket.gethostname() +OS_NAME = platform.system() + +# ZDE JE TO NATVRDO PRO TESTOVÁNÍ: +# SCAN_ONLY_THIS = None #"#Fotky" +SCAN_ONLY_THIS = '#Library' # "#Fotky" + +# ============================== +# CONFIG +# ============================== + +EXCLUDED_SHARES = {"domains", "appdata", "system", "isos"} + +# --- File size limits (bytes) --- +FILE_MIN_SIZE = 0 +FILE_MAX_SIZE = 1024 * 1024 * 1024 * 1024 # 50MB + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "torrents", + "charset": "utf8mb4", + "autocommit": True, +} + +CHUNK_SIZE = 4 * 1024 * 1024 # 4 MB +PRINT_SKIPPED = False + + +# ============================== +# HASH +# ============================== + +def compute_blake3(path: str) -> bytes: + h = blake3() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(CHUNK_SIZE), b""): + h.update(chunk) + return h.digest() + + +# ============================== +# SHARE / PATH HELPERS +# ============================== + +def get_user_shares(): + if SCAN_ONLY_THIS: + path = f"/mnt/user/{SCAN_ONLY_THIS}" + if os.path.isdir(path): + print(f"🎯 SINGLE SHARE MODE ACTIVE: Scanning only '{SCAN_ONLY_THIS}'") + return [SCAN_ONLY_THIS] + else: + print(f"⚠️ ERROR: Requested share '{SCAN_ONLY_THIS}' not found in /mnt/user!") + return [] + + shares = [] + if not os.path.exists("/mnt/user"): + return [] + + for name in os.listdir("/mnt/user"): + if name.startswith("."): + continue + if name in EXCLUDED_SHARES: + continue + path = f"/mnt/user/{name}" + if os.path.isdir(path): + shares.append(name) + return sorted(shares) + + +def find_physical_roots(shares): + roots = [] + if not os.path.exists("/mnt"): + return [] + for disk in os.listdir("/mnt"): + if not disk.startswith("disk"): + continue + for share in shares: + path = f"/mnt/{disk}/{share}" + if os.path.isdir(path): + roots.append((share, path)) + return sorted(roots) + + +def logical_path_from_disk_path(disk_path: str) -> str: + if not disk_path.startswith("/mnt/disk"): + raise ValueError(f"Unexpected disk path: {disk_path}") + parts = disk_path.split("/", 3) + return f"/mnt/user/{parts[3]}" + + +def size_allowed(size: int) -> bool: + if FILE_MIN_SIZE is not None and size < FILE_MIN_SIZE: + return False + if FILE_MAX_SIZE is not None and size > FILE_MAX_SIZE: + return False + return True + + +# ============================== +# MAIN +# ============================== + +def main(): + print("🚀 BLAKE3 indexer starting", flush=True) + print(f"🖥 Host: {HOSTNAME} | OS: {OS_NAME}", flush=True) + + if FILE_MIN_SIZE or FILE_MAX_SIZE: + print(f"📏 File size limits: min={FILE_MIN_SIZE} max={FILE_MAX_SIZE}", flush=True) + + shares = get_user_shares() + if not shares: + print("❌ No user shares to index!", flush=True) + return + + print("📦 User shares to index:", flush=True) + for s in shares: + print(f" - {s}", flush=True) + + scan_roots = find_physical_roots(shares) + if not scan_roots: + print("❌ No physical disk roots found!", flush=True) + return + + print("📂 Physical scan roots:", flush=True) + for _, path in scan_roots: + print(f" - {path}", flush=True) + + try: + db = pymysql.connect(**DB_CONFIG) + cur = db.cursor() + # === TOTO JE TEN PŘÍKAZ "NEPŘEMÝŠLEJ" === + # Nastaví relaci na UTC. MySQL přestane posouvat časy o hodinu sem a tam. + # cur.execute("SET time_zone = '+00:00'") + # ========================================= + except Exception as e: + print(f"❌ Database connection failed: {e}") + return + + print("📥 Loading already indexed files into memory...", flush=True) + + # === OPTIMALIZACE SQL === + if SCAN_ONLY_THIS: + search_pattern = f"/mnt/user/{SCAN_ONLY_THIS}%" + print(f"⚡ OPTIMIZATION: Fetching only DB records for '{search_pattern}'", flush=True) + cur.execute(""" + SELECT full_path, file_size, UNIX_TIMESTAMP(mtime) + FROM file_md5_index + WHERE host_name = %s AND full_path LIKE %s + """, (HOSTNAME, search_pattern)) + else: + cur.execute(""" + SELECT full_path, file_size, UNIX_TIMESTAMP(mtime) + FROM file_md5_index + WHERE host_name = %s + """, (HOSTNAME,)) + + # Načteme do slovníku pro rychlé vyhledávání + # Formát: { "cesta": (velikost, mtime) } + indexed_map = {row[0]: (row[1], row[2]) for row in cur.fetchall()} + print(f"✅ Loaded {len(indexed_map):,} indexed entries", flush=True) + print("======================================", flush=True) + + new_files = 0 + skipped = 0 + filtered = 0 + seen_paths = set() + + # --- SCAN --- + for share, scan_root in scan_roots: + for root, _, files in os.walk(scan_root): + for fname in files: + disk_path = os.path.join(root, fname) + + try: + stat = os.stat(disk_path) + except OSError: + continue + + size = stat.st_size + if not size_allowed(size): + filtered += 1 + continue + + logical_path = logical_path_from_disk_path(disk_path) + + if logical_path in seen_paths: + continue + seen_paths.add(logical_path) + + mtime = int(stat.st_mtime) + + # === PŘÍSNÁ KONTROLA (ŽÁDNÁ TOLERANCE) === + # Pokud soubor v DB existuje a přesně sedí velikost i čas, přeskočíme ho. + # Vše ostatní (včetně posunu času o 1s) se považuje za změnu a aktualizuje se. + + is_match = False + if logical_path in indexed_map: + db_size, db_mtime = indexed_map[logical_path] + if size == db_size and mtime == db_mtime: + is_match = True + + if is_match: + skipped += 1 + if PRINT_SKIPPED: + print(f"⏭ SKIP {logical_path}", flush=True) + continue + # ============================================ + + print("➕ NEW / UPDATED", flush=True) + print(f" File: {logical_path}", flush=True) + print(f" Size: {size:,} B", flush=True) + + try: + b3 = compute_blake3(disk_path) + except Exception as e: + print(f"❌ BLAKE3 failed: {e}", flush=True) + continue + + # Zde proběhne UPDATE mtime na hodnotu z disku + cur.execute(""" + INSERT INTO file_md5_index + (os_name, host_name, full_path, file_name, directory, + file_size, mtime, blake3) + VALUES (%s, %s, %s, %s, %s, %s, FROM_UNIXTIME(%s), %s) + ON DUPLICATE KEY UPDATE + file_size = VALUES(file_size), + mtime = VALUES(mtime), + blake3 = VALUES(blake3), + updated_at = CURRENT_TIMESTAMP + """, ( + OS_NAME, + HOSTNAME, + logical_path, + fname, + os.path.dirname(logical_path), + size, + mtime, + b3, + )) + + new_files += 1 + print(f" B3 : {b3.hex()}", flush=True) + print("--------------------------------------", flush=True) + + print("======================================", flush=True) + print(f"✅ New / updated : {new_files}", flush=True) + print(f"⏭ Skipped : {skipped}", flush=True) + print(f"🚫 Size filtered: {filtered}", flush=True) + print("🏁 Script finished", flush=True) + + + # ============================== + # DB CLEANUP – REMOVE DELETED FILES + # ============================== + + print("🧹 Checking for deleted files in DB...", flush=True) + + db_paths = set(indexed_map.keys()) + deleted_paths = db_paths - seen_paths + + # Omezíme jen na aktuální share (pokud je aktivní) + if SCAN_ONLY_THIS: + prefix = f"/mnt/user/{SCAN_ONLY_THIS}/" + deleted_paths = {p for p in deleted_paths if p.startswith(prefix)} + + if deleted_paths: + print(f"🗑 Removing {len(deleted_paths):,} deleted files from DB", flush=True) + + BATCH_SIZE = 1000 + deleted_paths = list(deleted_paths) + + for i in range(0, len(deleted_paths), BATCH_SIZE): + batch = deleted_paths[i:i + BATCH_SIZE] + placeholders = ",".join(["%s"] * len(batch)) + + sql = f""" + DELETE FROM file_md5_index + WHERE host_name = %s + AND full_path IN ({placeholders}) + """ + + cur.execute(sql, (HOSTNAME, *batch)) + + print("✅ DB cleanup completed", flush=True) + else: + print("✅ No deleted files found in DB", flush=True) + + cur.close() + db.close() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Medevio/Testy/diafaan.py b/Medevio/Testy/diafaan.py new file mode 100644 index 0000000..7266d4a --- /dev/null +++ b/Medevio/Testy/diafaan.py @@ -0,0 +1,23 @@ +import requests +from datetime import datetime + +# Diafaan SMS Server settings +server_url = "http://192.168.1.138:9710/http/send-message" +username = "admin" # adjust if you changed it +password = "" # adjust if you changed it +phone_number = "+420775735276" + +for i in range(1, 11): + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + message_text = f"{i}/10 {timestamp}" + params = { + "username": username, + "password": password, + "to": phone_number, + "message": message_text + } + try: + response = requests.get(server_url, params=params) + print(f"Sent '{message_text}': {response.text}") + except Exception as e: + print(f"Error sending {message_text}: {e}") diff --git a/Medevio/Testy/medevio.py b/Medevio/Testy/medevio.py new file mode 100644 index 0000000..c510d7e --- /dev/null +++ b/Medevio/Testy/medevio.py @@ -0,0 +1,18 @@ +from playwright.sync_api import sync_playwright + +LOGIN_URL = "https://my.medevio.cz/prihlaseni" +STATE_FILE = "../medevio_storage.json" + +with sync_playwright() as p: + browser = p.chromium.launch(headless=False, slow_mo=150) # zobrazí prohlížeč + context = browser.new_context() + page = context.new_page() + page.goto(LOGIN_URL, wait_until="load") + + print(">>> Přihlas se v otevřeném okně (Medevio).") + print(">>> Jakmile jsi na hlavní stránce po přihlášení, vrať se do PyCharm konzole a stiskni Enter.") + input() + + context.storage_state(path=STATE_FILE) + print(f"Session uložena do: {STATE_FILE}") + browser.close() diff --git a/Medevio/Testy/patient_detail.graphql b/Medevio/Testy/patient_detail.graphql new file mode 100644 index 0000000..a602fa9 --- /dev/null +++ b/Medevio/Testy/patient_detail.graphql @@ -0,0 +1,201 @@ +query ClinicPatientDetailModal_GetData( + $clinicSlug: String!, + $patientId: String!, + $patientUuid: UUID!, + $challengesStatus: ECRFChallengeStatus!, + $locale: Locale! +) { + clinic: getClinic(clinicSlug: $clinicSlug) { + id + features + sslSUKLCertificateId + type + ais + slug + ...ClinicWithTypeAndFeatures_Clinic + ...PatientInfo_Clinic + __typename + } + patient: getPatientForClinic(clinicSlug: $clinicSlug, patientId: $patientId) { + ...ClinicPatientDetailModal_Patient + __typename + } + challenges: listPatientChallenges2( + clinicSlug: $clinicSlug + patientId: $patientId + status: $challengesStatus + ) { + ...ChallengeTableList_EcrfChallenge + __typename + } + patientRequestsResponse: filterPatientRequestsForClinic( + clinicSlug: $clinicSlug + filter: {patientId: $patientUuid} + pageInfo: {first: 1, offset: 0} + ) { + count + items { id __typename } + __typename + } + treatmentPlanPatients: listTreatmentPlanPatients( + clinicSlug: $clinicSlug + patientId: $patientUuid + ) { + ...ClinicPlanPatientList_PlanPatient + __typename + } + premiumPlans: listClinicPremiumPlans(clinicSlug: $clinicSlug) { + id + __typename + } + mergeSuggestions: findMergeSuggestions( + clinicSlug: $clinicSlug + input: {existingPatientId: $patientUuid} + ) { + ...MergeSuggestionAlert_MergeSuggestionResult + __typename + } + insuranceCards: getPatientDocuments( + patientId: $patientUuid + type: InsuranceCard + ) { + ...PatientInfo_InsuranceCard + __typename + } +} + +# — fragmenty (přesně tak, jak jsi měl) — +fragment ClinicWithTypeAndFeatures_Clinic on Clinic { + id + type + features + __typename +} + +fragment PatientInfo_Clinic on Clinic { + country + id + slug + ais + ...ClinicWithTypeAndFeatures_Clinic + __typename +} + +fragment ClinicPatientDetailModal_Patient on ExtendedPatient { + id + isInClinic + kind + name + isUnknownPatient + sex + surname + identificationNumber + editableByDoctor + type + key + user { id name surname __typename } + ...ClinicPatientDetail_Patient + ...PatientInfo_AccountPatient + ...ClinicPatientInfo_Patient + __typename +} + +fragment ClinicPatientDetail_Patient on ExtendedPatient { + name + surname + email + id + identificationNumber + isInClinic + key + phone + sex + type + dob + user { id __typename } + isUnknownPatient + hasMobileApp + __typename +} + +fragment PatientInfo_AccountPatient on ExtendedPatient { + id + createdAt + key + user { + registrationCompletedTime + deactivatedTime + __typename + } + __typename +} + +fragment ClinicPatientInfo_Patient on ExtendedPatient { + anamnesisShared + anamnesisStatusForClinic { updatedAt __typename } + clinics { id name slug __typename } + id + isInClinic + dob + city + familyMembers: family { __typename } + houseNumber + identificationNumber + insuranceCompanyObject { id code name shortName __typename } + kind + name + note + owner { name surname __typename } + key + status + street + surname + user { id email name phone surname __typename } + userRelationship + premiumPlanPatient { id __typename } + sex + tags(onlyImportant: false) { id name color icon __typename } + type + isUnknownPatient + hasMobileApp + __typename +} + +fragment ChallengeTableList_EcrfChallenge on ECRFChallenge { + id + createdAt + sentAt + issuedToPatient { + id + identificationNumber + name + surname + __typename + } + userECRF(locale: $locale) { id name __typename } + patientRequestId + status + __typename +} + +fragment MergeSuggestionAlert_MergeSuggestionResult on MergeSuggestionResult { + extendedPatient { id __typename } + matchResult + __typename +} + +fragment ClinicPlanPatientList_PlanPatient on TreatmentPlanPatient { + id + createdAt + listPatient { id identificationNumber name key status surname __typename } + treatmentPlan { id slug name __typename } + __typename +} + +fragment PatientInfo_InsuranceCard on PatientDocument { + id + contentType + url + downloadUrl + __typename +} diff --git a/Medevio/exports/Medevio_agenda_2025-11-02 07-52-00.xlsx b/Medevio/exports/Medevio_agenda_2025-11-02 07-52-00.xlsx new file mode 100644 index 0000000..7cccd86 Binary files /dev/null and b/Medevio/exports/Medevio_agenda_2025-11-02 07-52-00.xlsx differ diff --git a/Medevio/graphql_capture_1760777115.jsonl b/Medevio/graphql_capture_1760777115.jsonl new file mode 100644 index 0000000..4bb8ee4 --- /dev/null +++ b/Medevio/graphql_capture_1760777115.jsonl @@ -0,0 +1,17 @@ +{"operationName": "AccessToken_AuthSelf", "variables": {}, "query": "query AccessToken_AuthSelf {\n authResponse: authSelf {\n ...MappedAuthResponse\n __typename\n }\n}\n\nfragment MappedAuthResponse on AuthResponse {\n accessToken\n expiresAt\n overrides {\n path: selector\n props\n __typename\n }\n user {\n accountFlags\n clinic {\n ...MappedClinic\n __typename\n }\n email\n id\n name\n locale\n surname\n phone\n userClinics {\n id\n clinic {\n ...MappedClinic\n __typename\n }\n __typename\n }\n __typename\n }\n roles\n __typename\n}\n\nfragment MappedClinic on Clinic {\n city\n currentThumbnail {\n id\n fullUrl\n __typename\n }\n description\n features\n name\n id\n slug\n __typename\n}"} +{"operationName": "ClinicNavigation_GetClinic", "variables": {"clinicSlug": "mudr-buzalkova", "locale": "cs"}, "query": "query ClinicNavigation_GetClinic($clinicSlug: String!, $locale: Locale!) {\n clinic: getClinic(clinicSlug: $clinicSlug) {\n ...ClinicNavigation_Clinic\n __typename\n }\n userECRFs: listUserECRFsByClinic(clinicSlug: $clinicSlug, locale: $locale) {\n id\n __typename\n }\n}\n\nfragment ClinicNavigation_Clinic on Clinic {\n features\n type\n ...ClinicDropdown_Clinic\n __typename\n}\n\nfragment ClinicDropdown_Clinic on Clinic {\n currentThumbnail {\n id\n fullUrl\n __typename\n }\n id\n name\n slug\n searchableSince\n type\n __typename\n}"} +{"operationName": "ClinicDocumentTitlePrefix_GetClinic", "variables": {"clinicSlug": "mudr-buzalkova"}, "query": "query ClinicDocumentTitlePrefix_GetClinic($clinicSlug: String!) {\n clinic: getClinic(clinicSlug: $clinicSlug) {\n id\n internalId\n __typename\n }\n}"} +{"operationName": "ClinicAgenda_GetCalendarsForClinic", "variables": {"clinicSlug": "mudr-buzalkova"}, "query": "query ClinicAgenda_GetCalendarsForClinic($clinicSlug: String!) {\n clinic: getClinic(clinicSlug: $clinicSlug) {\n ...ClinicAgenda_Clinic\n __typename\n }\n calendarGroups: listCalendarGroups(clinicSlug: $clinicSlug) {\n ...ClinicAgenda_CalendarGroup\n __typename\n }\n calendars: getCalendarsForClinic(clinicSlug: $clinicSlug) {\n ...ClinicAgenda_Calendar\n __typename\n }\n defaultCalendar: getDefaultCalendarForClinic2(clinicSlug: $clinicSlug) {\n id\n __typename\n }\n}\n\nfragment ClinicAgenda_Clinic on Clinic {\n id\n features\n type\n country\n __typename\n}\n\nfragment ClinicAgenda_Calendar on Calendar {\n ...CalendarTabs_Calendar\n ...GroupsControl_Calendar_Calendar\n calendarSettings {\n ignoreHolidays\n __typename\n }\n name\n userId\n __typename\n}\n\nfragment ClinicAgenda_CalendarGroup on CalendarGroup {\n ...GroupsControl_Calendar_CalendarGroup\n __typename\n}\n\nfragment CalendarTabs_Calendar on Calendar {\n id\n name\n internalName\n __typename\n}\n\nfragment GroupsControl_Calendar_Calendar on Calendar {\n id\n internalName\n name\n __typename\n}\n\nfragment GroupsControl_Calendar_CalendarGroup on CalendarGroup {\n calendars {\n ...GroupsControl_Calendar_Calendar\n __typename\n }\n id\n name\n __typename\n}"} +{"operationName": "ClinicAgenda_ListClinicReservations", "variables": {"clinicCountry": "CZ", "calendarIds": ["144c4e12-347c-49ca-9ec0-8ca965a4470d"], "clinicSlug": "mudr-buzalkova", "emptyCalendarIds": false, "since": "2025-10-17T22:00:00.000Z", "until": "2025-10-18T21:59:59.999Z", "locale": "cs", "skipCalendarWindows": true}, "query": "query ClinicAgenda_ListClinicReservations($calendarIds: [UUID!], $clinicSlug: String!, $locale: Locale!, $since: DateTime!, $until: DateTime!, $emptyCalendarIds: Boolean!, $skipCalendarWindows: Boolean!, $clinicCountry: Country = CZ) {\n calendarWindows: getCalendarWindows(\n clinicSlug: $clinicSlug\n calendarIds: $calendarIds\n pageInfo: {first: 10000, offset: 0}\n ) @skip(if: $skipCalendarWindows) {\n ...ClinicAgenda_CalendarSlotWindow\n __typename\n }\n holidays: getHolidays(since: $since, until: $until, countries: [$clinicCountry]) {\n ...AgendaGrid_Holiday\n __typename\n }\n recurringReservations: listClinicRecurringReservations(\n clinicSlug: $clinicSlug\n calendarIds: $calendarIds\n since: $since\n until: $until\n ) @skip(if: $emptyCalendarIds) {\n ...AgendaGrid_RecurringReservationResponse\n __typename\n }\n reservations: listClinicReservations(\n clinicSlug: $clinicSlug\n calendarIds: $calendarIds\n since: $since\n until: $until\n ) @skip(if: $emptyCalendarIds) {\n ...AgendaGrid_Reservation\n __typename\n }\n vacations: findVacations(\n clinicSlug: $clinicSlug\n calendarIds: $calendarIds\n since: $since\n until: $until\n ) @skip(if: $emptyCalendarIds) {\n ...AgendaGrid_Vacation\n __typename\n }\n}\n\nfragment ClinicAgenda_CalendarSlotWindow on CalendarSlotWindow {\n ...AgendaGrid_CalendarSlotWindow\n ...CalendarSlotWindowToResolveReservationLength\n __typename\n}\n\nfragment CalendarSlotWindowToResolveReservationLength on CalendarSlotWindow {\n supportedUserEcrfs(locale: $locale) {\n id\n reservationLength\n __typename\n }\n windowUserECRFs {\n id\n clinicPatientRequestSettingsId\n reservationLength\n __typename\n }\n __typename\n}\n\nfragment AgendaGrid_CalendarSlotWindow on CalendarSlotWindow {\n id\n start\n end\n validFrom\n validTill\n dayOfWeek\n weekParity\n calendar {\n ...AgendaGrid_Calendar\n __typename\n }\n __typename\n}\n\nfragment AgendaGrid_Holiday on Holiday {\n endAt\n name\n startAt\n __typename\n}\n\nfragment AgendaGrid_RecurringReservationResponse on RecurringReservationResponse {\n instances {\n ...AgendaGrid_RecurringReservationInstance\n __typename\n }\n recurringReservation {\n ...AgendaGrid_RecurringReservation\n __typename\n }\n __typename\n}\n\nfragment AgendaGrid_RecurringReservationInstance on RecurringReservationInstance {\n start\n end\n color\n note\n __typename\n}\n\nfragment AgendaGrid_RecurringReservation on RecurringReservation {\n id\n note\n calendar {\n ...AgendaGrid_Calendar\n __typename\n }\n color\n __typename\n}\n\nfragment AgendaGrid_Calendar on Calendar {\n id\n name\n internalName\n __typename\n}\n\nfragment AgendaGrid_Reservation on Reservation {\n id\n start\n end\n color\n done\n note\n canceledAt\n request {\n ...AgendaGrid_PatientRequest\n __typename\n }\n remindDaysBefore\n calendar {\n ...AgendaGrid_Calendar\n __typename\n }\n additionalCalendars {\n ...AgendaGrid_Calendar\n __typename\n }\n videoCallDoctorUrl\n done\n __typename\n}\n\nfragment AgendaGrid_PatientRequest on PatientRequest {\n id\n displayTitle(locale: $locale)\n tags(onlyImportant: true) {\n ...Tag\n __typename\n }\n ecrf: userECRF(locale: $locale) {\n id\n icon {\n id\n color\n urlSvg\n __typename\n }\n name\n __typename\n }\n extendedPatient {\n ...AgendaGrid_Patient\n __typename\n }\n eventType\n flags\n clinicNotes {\n ...AgendaGrid_ClinicNote\n __typename\n }\n invoice {\n id\n amount\n currency\n status\n __typename\n }\n clinicMedicalRecord\n __typename\n}\n\nfragment AgendaGrid_ClinicNote on ClinicPatientRequestNote {\n id\n content\n __typename\n}\n\nfragment Tag on Tag {\n id\n name\n color\n icon\n __typename\n}\n\nfragment AgendaGrid_Patient on ExtendedPatient {\n id\n kind\n name\n surname\n owner {\n name\n surname\n __typename\n }\n key\n tags(onlyImportant: true) {\n ...Tag\n __typename\n }\n type\n isUnknownPatient\n user {\n id\n name\n surname\n __typename\n }\n identificationNumber\n insuranceCompanyObject {\n id\n shortName\n __typename\n }\n dob\n __typename\n}\n\nfragment AgendaGrid_Vacation on ClinicVacation {\n id\n since\n until\n calendarId\n calendars {\n ...AgendaGrid_Calendar\n __typename\n }\n __typename\n}"} +{"operationName": "ClinicAgenda_ListClinicReservations", "variables": {"clinicCountry": "CZ", "calendarIds": ["144c4e12-347c-49ca-9ec0-8ca965a4470d"], "clinicSlug": "mudr-buzalkova", "emptyCalendarIds": false, "since": "2025-10-16T22:00:00.000Z", "until": "2025-10-17T21:59:59.999Z", "locale": "cs", "skipCalendarWindows": true}, "query": "query ClinicAgenda_ListClinicReservations($calendarIds: [UUID!], $clinicSlug: String!, $locale: Locale!, $since: DateTime!, $until: DateTime!, $emptyCalendarIds: Boolean!, $skipCalendarWindows: Boolean!, $clinicCountry: Country = CZ) {\n calendarWindows: getCalendarWindows(\n clinicSlug: $clinicSlug\n calendarIds: $calendarIds\n pageInfo: {first: 10000, offset: 0}\n ) @skip(if: $skipCalendarWindows) {\n ...ClinicAgenda_CalendarSlotWindow\n __typename\n }\n holidays: getHolidays(since: $since, until: $until, countries: [$clinicCountry]) {\n ...AgendaGrid_Holiday\n __typename\n }\n recurringReservations: listClinicRecurringReservations(\n clinicSlug: $clinicSlug\n calendarIds: $calendarIds\n since: $since\n until: $until\n ) @skip(if: $emptyCalendarIds) {\n ...AgendaGrid_RecurringReservationResponse\n __typename\n }\n reservations: listClinicReservations(\n clinicSlug: $clinicSlug\n calendarIds: $calendarIds\n since: $since\n until: $until\n ) @skip(if: $emptyCalendarIds) {\n ...AgendaGrid_Reservation\n __typename\n }\n vacations: findVacations(\n clinicSlug: $clinicSlug\n calendarIds: $calendarIds\n since: $since\n until: $until\n ) @skip(if: $emptyCalendarIds) {\n ...AgendaGrid_Vacation\n __typename\n }\n}\n\nfragment ClinicAgenda_CalendarSlotWindow on CalendarSlotWindow {\n ...AgendaGrid_CalendarSlotWindow\n ...CalendarSlotWindowToResolveReservationLength\n __typename\n}\n\nfragment CalendarSlotWindowToResolveReservationLength on CalendarSlotWindow {\n supportedUserEcrfs(locale: $locale) {\n id\n reservationLength\n __typename\n }\n windowUserECRFs {\n id\n clinicPatientRequestSettingsId\n reservationLength\n __typename\n }\n __typename\n}\n\nfragment AgendaGrid_CalendarSlotWindow on CalendarSlotWindow {\n id\n start\n end\n validFrom\n validTill\n dayOfWeek\n weekParity\n calendar {\n ...AgendaGrid_Calendar\n __typename\n }\n __typename\n}\n\nfragment AgendaGrid_Holiday on Holiday {\n endAt\n name\n startAt\n __typename\n}\n\nfragment AgendaGrid_RecurringReservationResponse on RecurringReservationResponse {\n instances {\n ...AgendaGrid_RecurringReservationInstance\n __typename\n }\n recurringReservation {\n ...AgendaGrid_RecurringReservation\n __typename\n }\n __typename\n}\n\nfragment AgendaGrid_RecurringReservationInstance on RecurringReservationInstance {\n start\n end\n color\n note\n __typename\n}\n\nfragment AgendaGrid_RecurringReservation on RecurringReservation {\n id\n note\n calendar {\n ...AgendaGrid_Calendar\n __typename\n }\n color\n __typename\n}\n\nfragment AgendaGrid_Calendar on Calendar {\n id\n name\n internalName\n __typename\n}\n\nfragment AgendaGrid_Reservation on Reservation {\n id\n start\n end\n color\n done\n note\n canceledAt\n request {\n ...AgendaGrid_PatientRequest\n __typename\n }\n remindDaysBefore\n calendar {\n ...AgendaGrid_Calendar\n __typename\n }\n additionalCalendars {\n ...AgendaGrid_Calendar\n __typename\n }\n videoCallDoctorUrl\n done\n __typename\n}\n\nfragment AgendaGrid_PatientRequest on PatientRequest {\n id\n displayTitle(locale: $locale)\n tags(onlyImportant: true) {\n ...Tag\n __typename\n }\n ecrf: userECRF(locale: $locale) {\n id\n icon {\n id\n color\n urlSvg\n __typename\n }\n name\n __typename\n }\n extendedPatient {\n ...AgendaGrid_Patient\n __typename\n }\n eventType\n flags\n clinicNotes {\n ...AgendaGrid_ClinicNote\n __typename\n }\n invoice {\n id\n amount\n currency\n status\n __typename\n }\n clinicMedicalRecord\n __typename\n}\n\nfragment AgendaGrid_ClinicNote on ClinicPatientRequestNote {\n id\n content\n __typename\n}\n\nfragment Tag on Tag {\n id\n name\n color\n icon\n __typename\n}\n\nfragment AgendaGrid_Patient on ExtendedPatient {\n id\n kind\n name\n surname\n owner {\n name\n surname\n __typename\n }\n key\n tags(onlyImportant: true) {\n ...Tag\n __typename\n }\n type\n isUnknownPatient\n user {\n id\n name\n surname\n __typename\n }\n identificationNumber\n insuranceCompanyObject {\n id\n shortName\n __typename\n }\n dob\n __typename\n}\n\nfragment AgendaGrid_Vacation on ClinicVacation {\n id\n since\n until\n calendarId\n calendars {\n ...AgendaGrid_Calendar\n __typename\n }\n __typename\n}"} +{"operationName": "UpdateReservation_GetReservation", "variables": {"clinicSlug": "mudr-buzalkova", "includeReservation": true, "recurringReservationId": "00000000-0000-0000-0000-000000000000", "reservationId": "473f0072-fa88-4ce5-a160-e7abf030aff7", "since": "1970-01-01T00:00:00.000Z"}, "query": "query UpdateReservation_GetReservation($clinicSlug: String!, $includeReservation: Boolean!, $recurringReservationId: UUID!, $reservationId: UUID!, $since: DateTime!) {\n recurringReservation: getRecurringReservationResponse(\n clinicSlug: $clinicSlug\n id: $recurringReservationId\n since: $since\n until: $since\n ) @skip(if: $includeReservation) {\n ...UpdateReservationForm_RecurringReservation\n __typename\n }\n reservation: getReservation(\n clinicSlug: $clinicSlug\n reservationId: $reservationId\n ) @include(if: $includeReservation) {\n ...UpdateReservationForm_Reservation\n __typename\n }\n calendars: getCalendarsForClinic(clinicSlug: $clinicSlug) {\n ...CalendarSelect_Calendar\n __typename\n }\n}\n\nfragment UpdateReservationForm_RecurringReservation on RecurringReservationResponse {\n recurringReservation {\n calendarId\n duration\n id\n rrule {\n ...UpdateReservationForm_RecurringReservationSettings\n __typename\n }\n note\n color\n __typename\n }\n instances {\n start\n end\n note\n color\n __typename\n }\n __typename\n}\n\nfragment UpdateReservationForm_RecurringReservationSettings on RecurringReservationSettings {\n byhour\n byminute\n bymonth\n bymonthday\n bynmonthday\n bynweekday\n bysetpos\n byweekday\n byweekno\n byyearday\n count\n dtstart\n frequency\n interval\n tzid\n until\n wkst\n __typename\n}\n\nfragment UpdateReservationForm_Reservation on Reservation {\n calendarId\n color\n id\n note\n end\n start\n __typename\n}\n\nfragment CalendarSelect_Calendar on Calendar {\n id\n name\n __typename\n}"} +{"operationName": "ClinicRequestDetail_GetPatientRequest2", "variables": {"clinicSlug": "mudr-buzalkova", "isDoctor": true, "requestId": "f2724013-2e25-4558-906b-56b41f346588", "locale": "cs"}, "query": "query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!, $clinicSlug: String!, $isDoctor: Boolean!, $locale: Locale!) {\n patientRequestMedicalRecords: listMedicalRecordsForPatientRequest(\n attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT]\n patientRequestId: $requestId\n pageInfo: {first: 100, offset: 0}\n ) {\n ...ClinicRequestDetail_PatientRequestMedicalRecord\n __typename\n }\n request: getPatientRequest2(\n patientRequestId: $requestId\n clinicSlug: $clinicSlug\n ) {\n id\n ...ClinicRequestDetail_Request\n createdBy {\n id\n __typename\n }\n ecrf: userECRF(locale: $locale) {\n id\n sid\n __typename\n }\n eventType\n hasMobileApp\n extendedPatient(clinicSlug: $clinicSlug) {\n ...ClinicRequestDetail_Patient\n ...ClinicPatientInfo_Patient\n __typename\n }\n __typename\n }\n}\n\nfragment ClinicRequestDetail_Patient on ExtendedPatient {\n id\n user {\n id\n phone\n __typename\n }\n phone\n editableByDoctor\n name\n surname\n type\n isUnknownPatient\n __typename\n}\n\nfragment ClinicRequestDetail_Request on PatientRequest {\n id\n doneAt\n doneBy {\n id\n name\n surname\n __typename\n }\n removedAt\n queue {\n id\n __typename\n }\n isInClinic\n clinic {\n id\n paymentEnabled\n hasInvoicingEnabled\n slug\n features\n __typename\n }\n clinicMedicalRecord\n clinicMedicalRecordVisibleToPatient\n extendedPatient(clinicSlug: $clinicSlug) {\n id\n kind\n identificationNumber\n name\n key\n type\n user {\n id\n name\n surname\n __typename\n }\n surname\n __typename\n }\n ecrf: userECRF(locale: $locale) {\n id\n sid\n name\n flags {\n isReference\n __typename\n }\n __typename\n }\n ecrfFilledData(locale: $locale) {\n ...EcrfFilledSteps_EcrfFilledStep\n __typename\n }\n eventType\n flags\n invoice {\n ...InvoiceStatusBar_Invoice\n __typename\n }\n userNote\n paymentData {\n ...InvoicePaymentStatusBar_PaymentSummaryData\n __typename\n }\n recording {\n ...ClinicRequestContent_Recording\n __typename\n }\n reservations {\n ...Reservations_Reservation\n ...VideoCall_Reservation\n __typename\n }\n evaluationResult(locale: $locale) {\n ...ClinicRequest_ECRFEvaluationResult\n __typename\n }\n tags(onlyImportant: false) {\n id\n ...Tag\n __typename\n }\n ...ClinicRequestHeading_Request\n ...ClinicRequestControls_Request\n ...InvoiceStatusBar_PatientRequest\n ...InvoicePriceBar_Request\n __typename\n}\n\nfragment ClinicRequestContent_Recording on Recording {\n fileUrl\n type\n __typename\n}\n\nfragment ClinicRequestControls_Request on PatientRequest {\n ...ClinicRequestControlsBar_Request\n isInClinic\n extendedPatient(clinicSlug: $clinicSlug) {\n id\n identificationNumber\n kind\n name\n owner {\n name\n surname\n __typename\n }\n key\n type\n user {\n id\n name\n surname\n __typename\n }\n status\n surname\n isUnknownPatient\n __typename\n }\n __typename\n}\n\nfragment ClinicRequestControlsBar_Request on PatientRequest {\n id\n dueDate\n doneAt\n doneBy {\n name\n surname\n __typename\n }\n queue {\n id\n __typename\n }\n __typename\n}\n\nfragment EcrfFilledSteps_EcrfFilledStep on ECRFFilledStep {\n name\n groups {\n ...EcrfFilledSteps_EcrfFilledGroup\n __typename\n }\n __typename\n}\n\nfragment EcrfFilledSteps_EcrfFilledGroup on ECRFFilledGroup {\n label\n name\n fields {\n ...EcrfFilledSteps_EcrfFilledField\n __typename\n }\n __typename\n}\n\nfragment EcrfFilledSteps_EcrfFilledField on ECRFFilledField {\n ...EcrfFilledSteps_ShallowECRFFilledField\n enumerations {\n ...EcrfFilledSteps_EcrfFilledEnumeration\n __typename\n }\n __typename\n}\n\nfragment EcrfFilledSteps_EcrfFilledEnumeration on ECRFFilledEnumeration {\n ...EcrfFilledSteps_ShallowEcrfFilledEnumeration\n children {\n ...EcrfFilledSteps_ShallowECRFFilledField\n enumerations {\n ...EcrfFilledSteps_ShallowEcrfFilledEnumeration\n children {\n ...EcrfFilledSteps_ShallowECRFFilledField\n enumerations {\n ...EcrfFilledSteps_ShallowEcrfFilledEnumeration\n children {\n ...EcrfFilledSteps_ShallowECRFFilledField\n enumerations {\n ...EcrfFilledSteps_ShallowEcrfFilledEnumeration\n children {\n ...EcrfFilledSteps_ShallowECRFFilledField\n enumerations {\n ...EcrfFilledSteps_ShallowEcrfFilledEnumeration\n children {\n ...EcrfFilledSteps_ShallowECRFFilledField\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment EcrfFilledSteps_ShallowECRFFilledField on ECRFFilledField {\n name\n label\n type\n value\n __typename\n}\n\nfragment EcrfFilledSteps_ShallowEcrfFilledEnumeration on ECRFFilledEnumeration {\n name\n label\n checked\n __typename\n}\n\nfragment ClinicRequest_ECRFEvaluationResult on ECRFEvaluationResult {\n contentForDoctor\n fields {\n name\n label\n type\n value\n min\n max\n __typename\n }\n __typename\n}\n\nfragment ClinicRequestHeading_Request on PatientRequest {\n clinic {\n id\n name\n slug\n currentThumbnail {\n id\n fullUrl\n __typename\n }\n __typename\n }\n isInClinic\n createdAt\n createdBy {\n id\n name\n surname\n __typename\n }\n customTitle\n createdByDoctor\n displayTitle(locale: $locale)\n ecrf: userECRF(locale: $locale) {\n id\n name\n ecrfSet {\n id\n name\n __typename\n }\n __typename\n }\n eventType\n flags\n id\n extendedPatient(clinicSlug: $clinicSlug) {\n id\n name\n key\n surname\n __typename\n }\n referringClinic {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment Reservations_Reservation on Reservation {\n calendar {\n ...Reservations_Calendar\n __typename\n }\n additionalCalendars {\n ...Reservations_Calendar\n __typename\n }\n calendarId\n canceledAt\n canceledBy {\n id\n clinicId\n name\n surname\n __typename\n }\n createdAt\n createdBy {\n id\n clinicId\n name\n surname\n __typename\n }\n clinic {\n id\n slug\n __typename\n }\n done\n end\n id\n note\n start\n videoCallDoctorUrl\n additionalCalendars {\n id\n __typename\n }\n __typename\n}\n\nfragment Reservations_Calendar on Calendar {\n id\n internalName\n name\n __typename\n}\n\nfragment InvoiceStatusBar_Invoice on Invoice {\n amount\n currency\n dueAmount\n id\n isOverdue\n link\n payments {\n ...InvoiceStatusBar_Payment\n __typename\n }\n refundedAmount\n status\n settledAmount\n isOnlinePayable\n __typename\n}\n\nfragment InvoiceStatusBar_Payment on Payment {\n id\n status\n __typename\n}\n\nfragment InvoiceStatusBar_PatientRequest on PatientRequest {\n extendedPatient(clinicSlug: $clinicSlug) {\n id\n user {\n id\n phone\n __typename\n }\n key\n phone\n __typename\n }\n hasMobileApp\n paymentData {\n status\n __typename\n }\n id\n priceWhenCreated\n currencyWhenCreated\n __typename\n}\n\nfragment VideoCall_Reservation on Reservation {\n canceledAt\n end\n id\n showVideoCall\n start\n videoCallDoctorUrl\n __typename\n}\n\nfragment Tag on Tag {\n id\n name\n color\n icon\n __typename\n}\n\nfragment InvoicePaymentStatusBar_PaymentSummaryData on PaymentSummaryData {\n status\n amount\n currency\n __typename\n}\n\nfragment InvoicePriceBar_Request on PatientRequest {\n priceWhenCreated\n currencyWhenCreated\n __typename\n}\n\nfragment ClinicPatientInfo_Patient on ExtendedPatient {\n anamnesisShared\n anamnesisStatusForClinic {\n updatedAt\n __typename\n }\n clinics {\n ...ClinicsTinyList_Clinic\n __typename\n }\n id\n isInClinic\n dob\n city\n familyMembers: family {\n ...FamilyMemberList_FamilyMember\n __typename\n }\n houseNumber\n identificationNumber\n insuranceCompanyObject {\n id\n code\n name\n shortName\n __typename\n }\n kind\n name\n note\n owner {\n ...PatientInfo_ContactPatientOwner\n __typename\n }\n key\n status\n street\n surname\n user {\n ...PatientInfo_ContactPatientUser\n __typename\n }\n userRelationship\n premiumPlanPatient {\n id\n ...PremiumPlanTag_PremiumPlanPatient\n __typename\n }\n sex\n tags(onlyImportant: false) {\n ...Tag\n __typename\n }\n type\n isUnknownPatient\n hasMobileApp\n ...MergeSuggestionAlert_ExtendedPatient\n ...PatientInfo_ContactPatient\n __typename\n}\n\nfragment ClinicsTinyList_Clinic on Clinic {\n currentThumbnail {\n id\n fullUrl\n __typename\n }\n id\n name\n slug\n __typename\n}\n\nfragment PatientInfo_ContactPatient on ExtendedPatient {\n id\n city\n email\n houseNumber\n key\n phone\n street\n __typename\n}\n\nfragment PatientInfo_ContactPatientOwner on PatientOwner {\n name\n surname\n __typename\n}\n\nfragment PatientInfo_ContactPatientUser on PatientUser {\n id\n email\n name\n phone\n surname\n __typename\n}\n\nfragment FamilyMemberList_FamilyMember on FamilyPatientUserUnion {\n ...FamilyMemberList_Patient\n ...FamilyMemberList_PatientUser\n __typename\n}\n\nfragment FamilyMemberList_Patient on ExtendedPatient {\n dob\n id\n identificationNumber\n isInClinic\n kind\n name\n key\n surname\n sex\n type\n isUnknownPatient\n __typename\n}\n\nfragment FamilyMemberList_PatientUser on PatientUser {\n name\n surname\n __typename\n}\n\nfragment MergeSuggestionAlert_ExtendedPatient on ExtendedPatient {\n id\n key\n isInClinic\n __typename\n}\n\nfragment PremiumPlanTag_PremiumPlanPatient on PremiumPlanPatient {\n id\n autorenew\n validTo\n premiumPlan {\n ...PremiumPlanTag_PremiumPlan\n __typename\n }\n __typename\n}\n\nfragment PremiumPlanTag_PremiumPlan on PremiumPlan {\n id\n name\n color\n __typename\n}\n\nfragment ClinicRequestDetail_PatientRequestMedicalRecord on PatientRequestMedicalRecord {\n attachmentType\n id\n medicalRecord {\n ...FileItem_MedicalRecord\n __typename\n }\n __typename\n}\n\nfragment FileItem_MedicalRecord on MedicalRecord {\n contentType\n description\n downloadUrl\n id\n url\n visibleToPatient @include(if: $isDoctor)\n __typename\n}"} +{"operationName": "ClinicRequestDetailModalContent", "variables": {"clinicSlug": "mudr-buzalkova", "patientUuid": "e0640ebb-7542-4fe9-b899-9025d15979bc", "locale": "cs"}, "query": "query ClinicRequestDetailModalContent($clinicSlug: String!, $patientUuid: UUID!, $locale: Locale!) {\n clinic: getClinic(clinicSlug: $clinicSlug) {\n features\n id\n type\n ais\n ...ClinicRequestControls_Clinic\n ...PatientInfo_Clinic\n __typename\n }\n calendars: getCalendarsForClinic(clinicSlug: $clinicSlug) {\n ...ClinicRequestDetailModalContent_Calendar\n __typename\n }\n mergeSuggestions: findMergeSuggestions(\n clinicSlug: $clinicSlug\n input: {existingPatientId: $patientUuid}\n ) {\n ...MergeSuggestionAlert_MergeSuggestionResult\n __typename\n }\n queues: listQueues(clinicSlug: $clinicSlug) {\n ...ClinicRequestControlsBar_Queue\n __typename\n }\n patientRequestsResponse: filterPatientRequestsForClinic(\n clinicSlug: $clinicSlug\n filter: {patientId: $patientUuid}\n pageInfo: {first: 4, offset: 0}\n ) {\n count\n items {\n ...ClinicRequestDetail_RequestList_PatientRequest\n __typename\n }\n __typename\n }\n premiumPlans: listClinicPremiumPlans(clinicSlug: $clinicSlug) {\n id\n __typename\n }\n insuranceCards: getPatientDocuments(\n patientId: $patientUuid\n type: InsuranceCard\n ) {\n ...PatientInfo_InsuranceCard\n __typename\n }\n}\n\nfragment ClinicRequestDetailModalContent_Calendar on Calendar {\n id\n __typename\n}\n\nfragment ClinicRequestControlsBar_Queue on ClinicPatientRequestQueue {\n id\n name\n mine\n clinicPatientRequestQueueUsers {\n accountable {\n id\n name\n surname\n __typename\n }\n id\n __typename\n }\n __typename\n}\n\nfragment ClinicRequestControls_Clinic on Clinic {\n id\n slug\n canSendToEzprava\n __typename\n}\n\nfragment MergeSuggestionAlert_MergeSuggestionResult on MergeSuggestionResult {\n extendedPatient {\n ...MergeSuggestionAlert_ExtendedPatient\n __typename\n }\n matchResult\n __typename\n}\n\nfragment MergeSuggestionAlert_ExtendedPatient on ExtendedPatient {\n id\n key\n isInClinic\n __typename\n}\n\nfragment ClinicRequestDetail_RequestList_PatientRequest on PatientRequest {\n id\n createdAt\n displayTitle(locale: $locale)\n doneAt\n reservations {\n id\n canceledAt\n calendar {\n id\n name\n internalName\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PatientInfo_Clinic on Clinic {\n id\n slug\n ais\n ...ClinicWithTypeAndFeatures_Clinic\n __typename\n}\n\nfragment ClinicWithTypeAndFeatures_Clinic on Clinic {\n id\n type\n features\n __typename\n}\n\nfragment PatientInfo_InsuranceCard on PatientDocument {\n id\n contentType\n url\n downloadUrl\n __typename\n}"} +{"operationName": "ClinicRequestNotes_Get", "variables": {"patientRequestId": "f2724013-2e25-4558-906b-56b41f346588"}, "query": "query ClinicRequestNotes_Get($patientRequestId: String!) {\n notes: getClinicPatientRequestNotes(requestId: $patientRequestId) {\n ...NotesList_ClinicPatientRequestNote\n __typename\n }\n}\n\nfragment NotesList_ClinicPatientRequestNote on ClinicPatientRequestNote {\n id\n content\n createdAt\n updatedAt\n createdBy {\n id\n name\n surname\n __typename\n }\n __typename\n}"} +{"operationName": "UseMessages_ListMessages", "variables": {"requestId": "f2724013-2e25-4558-906b-56b41f346588", "updatedSince": null}, "query": "query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {\n messages: listMessages(\n patientRequestId: $requestId\n updatedSince: $updatedSince\n ) {\n ...UseMessages_Message\n __typename\n }\n}\n\nfragment UseMessages_Message on Message {\n createdAt\n id\n medicalRecord {\n ...UseMessages_MedicalRecord\n __typename\n }\n readAt\n sender {\n clinicId\n id\n name\n surname\n __typename\n }\n text\n type\n updatedAt\n __typename\n}\n\nfragment UseMessages_MedicalRecord on MedicalRecord {\n contentType\n createdAt\n description\n downloadUrl\n id\n token\n updatedAt\n url\n __typename\n}"} +{"operationName": "Communication_GetClinicFooter", "variables": {"clinicSlug": "mudr-buzalkova"}, "query": "query Communication_GetClinicFooter($clinicSlug: String!) {\n footer: getClinicFooter(clinicSlug: $clinicSlug)\n}"} +{"operationName": "ClinicRequestDetail_GetPatientRequest2", "variables": {"clinicSlug": "mudr-buzalkova", "isDoctor": true, "requestId": "61c90032-255e-4c94-a4b5-00a48f717fe5", "locale": "cs"}, "query": "query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!, $clinicSlug: String!, $isDoctor: Boolean!, $locale: Locale!) {\n patientRequestMedicalRecords: listMedicalRecordsForPatientRequest(\n attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT]\n patientRequestId: $requestId\n pageInfo: {first: 100, offset: 0}\n ) {\n ...ClinicRequestDetail_PatientRequestMedicalRecord\n __typename\n }\n request: getPatientRequest2(\n patientRequestId: $requestId\n clinicSlug: $clinicSlug\n ) {\n id\n ...ClinicRequestDetail_Request\n createdBy {\n id\n __typename\n }\n ecrf: userECRF(locale: $locale) {\n id\n sid\n __typename\n }\n eventType\n hasMobileApp\n extendedPatient(clinicSlug: $clinicSlug) {\n ...ClinicRequestDetail_Patient\n ...ClinicPatientInfo_Patient\n __typename\n }\n __typename\n }\n}\n\nfragment ClinicRequestDetail_Patient on ExtendedPatient {\n id\n user {\n id\n phone\n __typename\n }\n phone\n editableByDoctor\n name\n surname\n type\n isUnknownPatient\n __typename\n}\n\nfragment ClinicRequestDetail_Request on PatientRequest {\n id\n doneAt\n doneBy {\n id\n name\n surname\n __typename\n }\n removedAt\n queue {\n id\n __typename\n }\n isInClinic\n clinic {\n id\n paymentEnabled\n hasInvoicingEnabled\n slug\n features\n __typename\n }\n clinicMedicalRecord\n clinicMedicalRecordVisibleToPatient\n extendedPatient(clinicSlug: $clinicSlug) {\n id\n kind\n identificationNumber\n name\n key\n type\n user {\n id\n name\n surname\n __typename\n }\n surname\n __typename\n }\n ecrf: userECRF(locale: $locale) {\n id\n sid\n name\n flags {\n isReference\n __typename\n }\n __typename\n }\n ecrfFilledData(locale: $locale) {\n ...EcrfFilledSteps_EcrfFilledStep\n __typename\n }\n eventType\n flags\n invoice {\n ...InvoiceStatusBar_Invoice\n __typename\n }\n userNote\n paymentData {\n ...InvoicePaymentStatusBar_PaymentSummaryData\n __typename\n }\n recording {\n ...ClinicRequestContent_Recording\n __typename\n }\n reservations {\n ...Reservations_Reservation\n ...VideoCall_Reservation\n __typename\n }\n evaluationResult(locale: $locale) {\n ...ClinicRequest_ECRFEvaluationResult\n __typename\n }\n tags(onlyImportant: false) {\n id\n ...Tag\n __typename\n }\n ...ClinicRequestHeading_Request\n ...ClinicRequestControls_Request\n ...InvoiceStatusBar_PatientRequest\n ...InvoicePriceBar_Request\n __typename\n}\n\nfragment ClinicRequestContent_Recording on Recording {\n fileUrl\n type\n __typename\n}\n\nfragment ClinicRequestControls_Request on PatientRequest {\n ...ClinicRequestControlsBar_Request\n isInClinic\n extendedPatient(clinicSlug: $clinicSlug) {\n id\n identificationNumber\n kind\n name\n owner {\n name\n surname\n __typename\n }\n key\n type\n user {\n id\n name\n surname\n __typename\n }\n status\n surname\n isUnknownPatient\n __typename\n }\n __typename\n}\n\nfragment ClinicRequestControlsBar_Request on PatientRequest {\n id\n dueDate\n doneAt\n doneBy {\n name\n surname\n __typename\n }\n queue {\n id\n __typename\n }\n __typename\n}\n\nfragment EcrfFilledSteps_EcrfFilledStep on ECRFFilledStep {\n name\n groups {\n ...EcrfFilledSteps_EcrfFilledGroup\n __typename\n }\n __typename\n}\n\nfragment EcrfFilledSteps_EcrfFilledGroup on ECRFFilledGroup {\n label\n name\n fields {\n ...EcrfFilledSteps_EcrfFilledField\n __typename\n }\n __typename\n}\n\nfragment EcrfFilledSteps_EcrfFilledField on ECRFFilledField {\n ...EcrfFilledSteps_ShallowECRFFilledField\n enumerations {\n ...EcrfFilledSteps_EcrfFilledEnumeration\n __typename\n }\n __typename\n}\n\nfragment EcrfFilledSteps_EcrfFilledEnumeration on ECRFFilledEnumeration {\n ...EcrfFilledSteps_ShallowEcrfFilledEnumeration\n children {\n ...EcrfFilledSteps_ShallowECRFFilledField\n enumerations {\n ...EcrfFilledSteps_ShallowEcrfFilledEnumeration\n children {\n ...EcrfFilledSteps_ShallowECRFFilledField\n enumerations {\n ...EcrfFilledSteps_ShallowEcrfFilledEnumeration\n children {\n ...EcrfFilledSteps_ShallowECRFFilledField\n enumerations {\n ...EcrfFilledSteps_ShallowEcrfFilledEnumeration\n children {\n ...EcrfFilledSteps_ShallowECRFFilledField\n enumerations {\n ...EcrfFilledSteps_ShallowEcrfFilledEnumeration\n children {\n ...EcrfFilledSteps_ShallowECRFFilledField\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment EcrfFilledSteps_ShallowECRFFilledField on ECRFFilledField {\n name\n label\n type\n value\n __typename\n}\n\nfragment EcrfFilledSteps_ShallowEcrfFilledEnumeration on ECRFFilledEnumeration {\n name\n label\n checked\n __typename\n}\n\nfragment ClinicRequest_ECRFEvaluationResult on ECRFEvaluationResult {\n contentForDoctor\n fields {\n name\n label\n type\n value\n min\n max\n __typename\n }\n __typename\n}\n\nfragment ClinicRequestHeading_Request on PatientRequest {\n clinic {\n id\n name\n slug\n currentThumbnail {\n id\n fullUrl\n __typename\n }\n __typename\n }\n isInClinic\n createdAt\n createdBy {\n id\n name\n surname\n __typename\n }\n customTitle\n createdByDoctor\n displayTitle(locale: $locale)\n ecrf: userECRF(locale: $locale) {\n id\n name\n ecrfSet {\n id\n name\n __typename\n }\n __typename\n }\n eventType\n flags\n id\n extendedPatient(clinicSlug: $clinicSlug) {\n id\n name\n key\n surname\n __typename\n }\n referringClinic {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment Reservations_Reservation on Reservation {\n calendar {\n ...Reservations_Calendar\n __typename\n }\n additionalCalendars {\n ...Reservations_Calendar\n __typename\n }\n calendarId\n canceledAt\n canceledBy {\n id\n clinicId\n name\n surname\n __typename\n }\n createdAt\n createdBy {\n id\n clinicId\n name\n surname\n __typename\n }\n clinic {\n id\n slug\n __typename\n }\n done\n end\n id\n note\n start\n videoCallDoctorUrl\n additionalCalendars {\n id\n __typename\n }\n __typename\n}\n\nfragment Reservations_Calendar on Calendar {\n id\n internalName\n name\n __typename\n}\n\nfragment InvoiceStatusBar_Invoice on Invoice {\n amount\n currency\n dueAmount\n id\n isOverdue\n link\n payments {\n ...InvoiceStatusBar_Payment\n __typename\n }\n refundedAmount\n status\n settledAmount\n isOnlinePayable\n __typename\n}\n\nfragment InvoiceStatusBar_Payment on Payment {\n id\n status\n __typename\n}\n\nfragment InvoiceStatusBar_PatientRequest on PatientRequest {\n extendedPatient(clinicSlug: $clinicSlug) {\n id\n user {\n id\n phone\n __typename\n }\n key\n phone\n __typename\n }\n hasMobileApp\n paymentData {\n status\n __typename\n }\n id\n priceWhenCreated\n currencyWhenCreated\n __typename\n}\n\nfragment VideoCall_Reservation on Reservation {\n canceledAt\n end\n id\n showVideoCall\n start\n videoCallDoctorUrl\n __typename\n}\n\nfragment Tag on Tag {\n id\n name\n color\n icon\n __typename\n}\n\nfragment InvoicePaymentStatusBar_PaymentSummaryData on PaymentSummaryData {\n status\n amount\n currency\n __typename\n}\n\nfragment InvoicePriceBar_Request on PatientRequest {\n priceWhenCreated\n currencyWhenCreated\n __typename\n}\n\nfragment ClinicPatientInfo_Patient on ExtendedPatient {\n anamnesisShared\n anamnesisStatusForClinic {\n updatedAt\n __typename\n }\n clinics {\n ...ClinicsTinyList_Clinic\n __typename\n }\n id\n isInClinic\n dob\n city\n familyMembers: family {\n ...FamilyMemberList_FamilyMember\n __typename\n }\n houseNumber\n identificationNumber\n insuranceCompanyObject {\n id\n code\n name\n shortName\n __typename\n }\n kind\n name\n note\n owner {\n ...PatientInfo_ContactPatientOwner\n __typename\n }\n key\n status\n street\n surname\n user {\n ...PatientInfo_ContactPatientUser\n __typename\n }\n userRelationship\n premiumPlanPatient {\n id\n ...PremiumPlanTag_PremiumPlanPatient\n __typename\n }\n sex\n tags(onlyImportant: false) {\n ...Tag\n __typename\n }\n type\n isUnknownPatient\n hasMobileApp\n ...MergeSuggestionAlert_ExtendedPatient\n ...PatientInfo_ContactPatient\n __typename\n}\n\nfragment ClinicsTinyList_Clinic on Clinic {\n currentThumbnail {\n id\n fullUrl\n __typename\n }\n id\n name\n slug\n __typename\n}\n\nfragment PatientInfo_ContactPatient on ExtendedPatient {\n id\n city\n email\n houseNumber\n key\n phone\n street\n __typename\n}\n\nfragment PatientInfo_ContactPatientOwner on PatientOwner {\n name\n surname\n __typename\n}\n\nfragment PatientInfo_ContactPatientUser on PatientUser {\n id\n email\n name\n phone\n surname\n __typename\n}\n\nfragment FamilyMemberList_FamilyMember on FamilyPatientUserUnion {\n ...FamilyMemberList_Patient\n ...FamilyMemberList_PatientUser\n __typename\n}\n\nfragment FamilyMemberList_Patient on ExtendedPatient {\n dob\n id\n identificationNumber\n isInClinic\n kind\n name\n key\n surname\n sex\n type\n isUnknownPatient\n __typename\n}\n\nfragment FamilyMemberList_PatientUser on PatientUser {\n name\n surname\n __typename\n}\n\nfragment MergeSuggestionAlert_ExtendedPatient on ExtendedPatient {\n id\n key\n isInClinic\n __typename\n}\n\nfragment PremiumPlanTag_PremiumPlanPatient on PremiumPlanPatient {\n id\n autorenew\n validTo\n premiumPlan {\n ...PremiumPlanTag_PremiumPlan\n __typename\n }\n __typename\n}\n\nfragment PremiumPlanTag_PremiumPlan on PremiumPlan {\n id\n name\n color\n __typename\n}\n\nfragment ClinicRequestDetail_PatientRequestMedicalRecord on PatientRequestMedicalRecord {\n attachmentType\n id\n medicalRecord {\n ...FileItem_MedicalRecord\n __typename\n }\n __typename\n}\n\nfragment FileItem_MedicalRecord on MedicalRecord {\n contentType\n description\n downloadUrl\n id\n url\n visibleToPatient @include(if: $isDoctor)\n __typename\n}"} +{"operationName": "ClinicRequestDetailModalContent", "variables": {"clinicSlug": "mudr-buzalkova", "patientUuid": "7f492f8d-af27-4a08-ab2c-f72e46c5509c", "locale": "cs"}, "query": "query ClinicRequestDetailModalContent($clinicSlug: String!, $patientUuid: UUID!, $locale: Locale!) {\n clinic: getClinic(clinicSlug: $clinicSlug) {\n features\n id\n type\n ais\n ...ClinicRequestControls_Clinic\n ...PatientInfo_Clinic\n __typename\n }\n calendars: getCalendarsForClinic(clinicSlug: $clinicSlug) {\n ...ClinicRequestDetailModalContent_Calendar\n __typename\n }\n mergeSuggestions: findMergeSuggestions(\n clinicSlug: $clinicSlug\n input: {existingPatientId: $patientUuid}\n ) {\n ...MergeSuggestionAlert_MergeSuggestionResult\n __typename\n }\n queues: listQueues(clinicSlug: $clinicSlug) {\n ...ClinicRequestControlsBar_Queue\n __typename\n }\n patientRequestsResponse: filterPatientRequestsForClinic(\n clinicSlug: $clinicSlug\n filter: {patientId: $patientUuid}\n pageInfo: {first: 4, offset: 0}\n ) {\n count\n items {\n ...ClinicRequestDetail_RequestList_PatientRequest\n __typename\n }\n __typename\n }\n premiumPlans: listClinicPremiumPlans(clinicSlug: $clinicSlug) {\n id\n __typename\n }\n insuranceCards: getPatientDocuments(\n patientId: $patientUuid\n type: InsuranceCard\n ) {\n ...PatientInfo_InsuranceCard\n __typename\n }\n}\n\nfragment ClinicRequestDetailModalContent_Calendar on Calendar {\n id\n __typename\n}\n\nfragment ClinicRequestControlsBar_Queue on ClinicPatientRequestQueue {\n id\n name\n mine\n clinicPatientRequestQueueUsers {\n accountable {\n id\n name\n surname\n __typename\n }\n id\n __typename\n }\n __typename\n}\n\nfragment ClinicRequestControls_Clinic on Clinic {\n id\n slug\n canSendToEzprava\n __typename\n}\n\nfragment MergeSuggestionAlert_MergeSuggestionResult on MergeSuggestionResult {\n extendedPatient {\n ...MergeSuggestionAlert_ExtendedPatient\n __typename\n }\n matchResult\n __typename\n}\n\nfragment MergeSuggestionAlert_ExtendedPatient on ExtendedPatient {\n id\n key\n isInClinic\n __typename\n}\n\nfragment ClinicRequestDetail_RequestList_PatientRequest on PatientRequest {\n id\n createdAt\n displayTitle(locale: $locale)\n doneAt\n reservations {\n id\n canceledAt\n calendar {\n id\n name\n internalName\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PatientInfo_Clinic on Clinic {\n id\n slug\n ais\n ...ClinicWithTypeAndFeatures_Clinic\n __typename\n}\n\nfragment ClinicWithTypeAndFeatures_Clinic on Clinic {\n id\n type\n features\n __typename\n}\n\nfragment PatientInfo_InsuranceCard on PatientDocument {\n id\n contentType\n url\n downloadUrl\n __typename\n}"} +{"operationName": "ClinicRequestNotes_Get", "variables": {"patientRequestId": "61c90032-255e-4c94-a4b5-00a48f717fe5"}, "query": "query ClinicRequestNotes_Get($patientRequestId: String!) {\n notes: getClinicPatientRequestNotes(requestId: $patientRequestId) {\n ...NotesList_ClinicPatientRequestNote\n __typename\n }\n}\n\nfragment NotesList_ClinicPatientRequestNote on ClinicPatientRequestNote {\n id\n content\n createdAt\n updatedAt\n createdBy {\n id\n name\n surname\n __typename\n }\n __typename\n}"} +{"operationName": "UseMessages_ListMessages", "variables": {"requestId": "61c90032-255e-4c94-a4b5-00a48f717fe5", "updatedSince": null}, "query": "query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {\n messages: listMessages(\n patientRequestId: $requestId\n updatedSince: $updatedSince\n ) {\n ...UseMessages_Message\n __typename\n }\n}\n\nfragment UseMessages_Message on Message {\n createdAt\n id\n medicalRecord {\n ...UseMessages_MedicalRecord\n __typename\n }\n readAt\n sender {\n clinicId\n id\n name\n surname\n __typename\n }\n text\n type\n updatedAt\n __typename\n}\n\nfragment UseMessages_MedicalRecord on MedicalRecord {\n contentType\n createdAt\n description\n downloadUrl\n id\n token\n updatedAt\n url\n __typename\n}"} +{"operationName": "Communication_GetClinicFooter", "variables": {"clinicSlug": "mudr-buzalkova"}, "query": "query Communication_GetClinicFooter($clinicSlug: String!) {\n footer: getClinicFooter(clinicSlug: $clinicSlug)\n}"} diff --git a/Medevio/medevio_api_notes.md b/Medevio/medevio_api_notes.md new file mode 100644 index 0000000..42fa5d6 --- /dev/null +++ b/Medevio/medevio_api_notes.md @@ -0,0 +1,135 @@ +# 🧭 MEDEVIO API – Working Notes + +## General Architecture +- **Frontend:** React + Material‑UI (MUI) + Apollo GraphQL +- **Backend:** GraphQL API → `https://api.medevio.cz/graphql` +- **Authentication:** cookies/tokens stored in `medevio_storage.json` (Playwright session) +- **Frontend base:** `https://my.medevio.cz/mudr-buzalkova/klinika/...` +- **Session handling:** token refreshed through `AccessToken_AuthSelf` calls + +--- + +## Authentication and Session +Reuse session cookies from Playwright: + +```python +state = json.load(open("medevio_storage.json")) +cookies = {c["name"]: c["value"] for c in state["cookies"] if "medevio" in c["domain"]} +``` + +Use these cookies in every `requests.post(GRAPHQL_URL, headers, cookies, data=...)` call. + +--- + +## Key GraphQL Operations + +| Operation name | Purpose | +|----------------|----------| +| `AccessToken_AuthSelf` | Verify or refresh session | +| `ClinicNavigation_GetClinic` | Get basic clinic info | +| `ClinicAgenda_GetCalendarsForClinic` | List calendars within clinic | +| **`ClinicAgenda_ListClinicReservations`** | **List reservations (appointments)** | +| **`UpdateReservation_GetReservation`** | **Get full detail of one reservation** | +| **`ClinicRequestDetail_GetPatientRequest2`** | **Fetch detailed “Požadavek” (request card)** | +| `ClinicRequestNotes_Get` | Load Požadavek notes | +| `UseMessages_ListMessages` | Load chat messages | +| `Communication_GetClinicFooter` | UI footer text | + +--- + +## Workflow for Automation + +### 1️⃣ Extract reservations (agenda overview) +```graphql +operationName: "ClinicAgenda_ListClinicReservations" +variables: { + "clinicId": "", + "dateFrom": "YYYY-MM-DDT00:00:00Z", + "dateTo": "YYYY-MM-DDT23:59:59Z" +} +``` +→ returns `id`, `startDateTime`, `endDateTime`, `patient {name, age}`, `reason`, `status`. + +### 2️⃣ Get detail of a single reservation +```graphql +operationName: "UpdateReservation_GetReservation" +variables: {"id": ""} +``` +→ returns reason, time, doctor, patient contact, note, etc. + +### 3️⃣ Get detail of Požadavek +```graphql +operationName: "ClinicRequestDetail_GetPatientRequest2" +variables: {"id": ""} +``` +→ returns text, attachments, communication, etc. + +--- + +## HTTP Setup (Python) + +```python +GRAPHQL_URL = "https://api.medevio.cz/graphql" +headers = { + "content-type": "application/json", + "origin": "https://my.medevio.cz", + "referer": "https://my.medevio.cz/", +} +response = requests.post(GRAPHQL_URL, headers=headers, cookies=cookies, data=json.dumps(payload)) +``` + +--- + +## Reservation Data Example + +```json +{ + "id": "0e3a4f5c-1c15-40e6-b1b1-325de9269ef5", + "reason": "Očkování - Chřipka", + "startDateTime": "2025-10-17T09:10:00Z", + "endDateTime": "2025-10-17T09:20:00Z", + "status": "confirmed", + "note": "přijde i s taťkou", + "patient": {"id": "...", "name": "Černík Pavel", "age": 53}, + "doctor": {"id": "...", "name": "MUDr. Buzalková"}, + "location": {"name": "Ordinace Prosek"} +} +``` + +--- + +## Playwright Selectors + +| Purpose | Selector | +|----------|-----------| +| Agenda rows | `div[data-testid='reservation-row']` | +| Row ID | `data-id` | +| Agenda fields | `div[data-field='Time']`, `div[data-field='Patient']`, `div[data-field='Reason']` | + +--- + +## Recommended Automation Pipeline + +1. **Agenda scrape (Playwright)** → extract `data-id`, patient, time, reason. +2. **GraphQL fetch (Python)** → use `ClinicAgenda_ListClinicReservations`. +3. **Detail enrichment** → call `UpdateReservation_GetReservation` and `ClinicRequestDetail_GetPatientRequest2`. +4. **Storage/reporting** → save to MySQL/Excel for vaccine planning. + +--- + +## Security / Maintenance Notes + +- GraphQL introspection disabled. +- Refresh session cookies periodically. +- Keep data GDPR-compliant. + +--- + +## Future Tasks + +- Automate cookie renewal. +- Build helper functions: + - `get_reservations(date)` using `ClinicAgenda_ListClinicReservations` + - `get_reservation_detail(reservation_id)` +- Map vaccine keywords (`Očkování – Chřipka`, `COVID`, `Hepatitida`, …). +- Export to Excel/MySQL for vaccine order planning. diff --git a/Medevio/medevio_api_notes.txt b/Medevio/medevio_api_notes.txt new file mode 100644 index 0000000..e8abd7c --- /dev/null +++ b/Medevio/medevio_api_notes.txt @@ -0,0 +1,113 @@ +MEDEVIO API – WORKING NOTES +============================ + +GENERAL ARCHITECTURE +-------------------- +Frontend: React + Material-UI (MUI) + Apollo GraphQL +Backend: GraphQL API at https://api.medevio.cz/graphql +Authentication: via cookies / tokens stored in medevio_storage.json (Playwright session file) +Frontend base: https://my.medevio.cz/mudr-buzalkova/klinika/... +Session handling: token refreshed through AccessToken_AuthSelf calls + +AUTHENTICATION AND SESSION +-------------------------- +Reuse session cookies from medevio_storage.json. + +Example: + state = json.load(open("medevio_storage.json")) + cookies = {c["name"]: c["value"] for c in state["cookies"] if "medevio" in c["domain"]} + +Use these cookies in all requests.post(GRAPHQL_URL, headers, cookies, data=...) calls. + +KEY API ENDPOINTS (GraphQL OPERATIONS) +-------------------------------------- +Operation name Purpose +------------------------------------ ------------------------------------------ +AccessToken_AuthSelf Verify or refresh current user session +ClinicNavigation_GetClinic Get basic clinic info (name, ID) +ClinicAgenda_GetCalendarsForClinic List available calendars within your clinic +ClinicAgenda_ListClinicReservations List reservations (appointments) for date or range +UpdateReservation_GetReservation Get full detail of one reservation +ClinicRequestDetail_GetPatientRequest2 Fetch detailed “Požadavek” (request card) +ClinicRequestNotes_Get Load notes/comments on Požadavek +UseMessages_ListMessages Load chat messages with patient +Communication_GetClinicFooter UI info only + +WORKFLOW FOR AUTOMATION +----------------------- +1. Extract reservations (agenda overview) + operationName: "ClinicAgenda_ListClinicReservations" + variables: {"clinicId": "", "dateFrom": "YYYY-MM-DDT00:00:00Z", "dateTo": "YYYY-MM-DDT23:59:59Z"} + + Returns: id, startDateTime, endDateTime, patient {name, age}, reason, status + +2. Get detail of a single reservation + operationName: "UpdateReservation_GetReservation" + variables: {"id": ""} + + Returns: reason, time, doctor, patient contact, note, etc. + +3. Get detail of Požadavek (request card) + operationName: "ClinicRequestDetail_GetPatientRequest2" + variables: {"id": ""} + + Returns: text of request, attachments, communication, etc. + +HTTP SETUP FOR ALL GRAPHQL CALLS +-------------------------------- +GRAPHQL_URL = "https://api.medevio.cz/graphql" +headers = { + "content-type": "application/json", + "origin": "https://my.medevio.cz", + "referer": "https://my.medevio.cz/", +} + +response = requests.post(GRAPHQL_URL, headers=headers, cookies=cookies, data=json.dumps(payload)) + +DATA STRUCTURE HINTS (Reservation) +---------------------------------- +{ + "id": "0e3a4f5c-1c15-40e6-b1b1-325de9269ef5", + "reason": "Očkování - Chřipka", + "startDateTime": "2025-10-17T09:10:00Z", + "endDateTime": "2025-10-17T09:20:00Z", + "status": "confirmed", + "note": "přijde i s taťkou", + "patient": {"id": "...", "name": "Černík Pavel", "age": 53}, + "doctor": {"id": "...", "name": "MUDr. Buzalková"}, + "location": {"name": "Ordinace Prosek"} +} + +PLAYWRIGHT SELECTORS (REFERENCE) +-------------------------------- +Purpose Selector +---------------- ------------------------------------------- +Agenda rows div[data-testid='reservation-row'] +Row ID data-id +Agenda fields div[data-field='Time'], div[data-field='Patient'], div[data-field='Reason'] + +RECOMMENDED AUTOMATION PIPELINE +-------------------------------- +1. Step 1 – Agenda scrape (Playwright) + Open agenda-dne → extract data-id, patient, time, reason. +2. Step 2 – GraphQL fetch (Python requests) + Use ClinicAgenda_ListClinicReservations to get full dataset. +3. Step 3 – Detail enrichment + For each appointment → call UpdateReservation_GetReservation. + For linked Požadavek → call ClinicRequestDetail_GetPatientRequest2. +4. Step 4 – Storage / reporting + Save to MySQL or Excel for vaccine inventory, reminders, analytics. + +SECURITY / MAINTENANCE NOTES +---------------------------- +- GraphQL introspection is disabled. +- Session cookies expire periodically; refresh via Playwright login. +- Keep data secure (GDPR compliance). + +FUTURE TASKS +------------ +- Automate login refresh and cookie renewal. +- Build function get_reservations(date) using ClinicAgenda_ListClinicReservations. +- Build function get_reservation_detail(reservation_id). +- Map vaccine keywords (Očkování – Chřipka, COVID, Hepatitida, …). +- Export to Excel/MySQL for vaccine order planning. diff --git a/Medevio/medevio_storage.json b/Medevio/medevio_storage.json new file mode 100644 index 0000000..a0dc794 --- /dev/null +++ b/Medevio/medevio_storage.json @@ -0,0 +1 @@ +{"cookies": [{"name": "gateway-access-token", "value": "YwBgkf8McREDKs7vCZj0EZD2fJsuV8RyDPtYx7WiDoz0nFJ9kxId8kcNEPBLFSwM+Tiz80+SOdFwo+oj", "domain": "my.medevio.cz", "path": "/", "expires": 1763372319, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "aws-waf-token", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c6zAAAA:OYwXLY5OyitSQPl5v2oIlS+hIxsrb5LxV4VjCyE2gJCFFE5PQu+0Zbxse2ZIofrNv5QKs0TYUDTmxPhZyTr9Qtjnq2gsVQxWHXzrbebv3Z7RbzB63u6Ymn3Fo8IbDev3CfCNcNuxCKltFEXLqSCjI2vqNY+7HZkgQBIqy2wMgzli3aSLq0w8lWYtZzyyot7q8RPXWMGTfaBUo2reY0SOSffm9rAivE9PszNfPid71CvNrGAAoxRbwb25eVujlyIcDVWe5vZ9Iw==", "domain": ".my.medevio.cz", "path": "/", "expires": 1761125920, "httpOnly": false, "secure": true, "sameSite": "Lax"}], "origins": [{"origin": "https://my.medevio.cz", "localStorage": [{"name": "awswaf_token_refresh_timestamp", "value": "1760780309860"}, {"name": "awswaf_session_storage", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c+zAAAA:+vw//1NzmePjPpbGCJzUB+orCRivtJd098DbDX4AnABiGRw/+ql6ShqvFY4YdCY7w2tegb5mEPBdAmc4sNi22kNR9BuEoAgCUiMhkU1AZWfzM51zPfTh7SveCrREZ7xdvxcqKPMmfVLRYX5E4+UWh22z/LKQ7+d9VERp3J+wWCUW3dFFirkezy3N7b2FVjTlY/RxsZwhejQziTG/L3CkIFFP3mOReNgBvDpj7aKoM1knY4IL4TZ8E7zNv3nTsvzACLYvnUutVOUcofN1TfOzwZshSKsEXsMzrQn8PzLccX1jM5VSzce7gfEzl0zSPsT8NB3Sna+rhMIttDNYgvbW1HsfG2LIeKMR27Zf8hkslDRVVkcU/Kp2jLOEdhhrBKGjKY2o9/uX3NExdzh5MEKQSSRtmue01BpWYILPH23rMsz4YSmF+Ough5OeQoC95rkcYwVXMhwvUN9Zfp9UZ4xCNfFUex5dOrg9aJntYRnaceeocGUttNI5AdT0i3+osV6XHXzKxeqO8zLCS9BIsCzxaHfdqqem5DorMceuGKz+QqksatIQAA=="}, {"name": "Application.Intl.locale", "value": "cs"}, {"name": "Password.prefill", "value": "{\"username\":\"vladimir.buzalka@buzalka.cz\",\"type\":\"email\"}"}]}]} \ No newline at end of file diff --git a/Medevio/token.txt b/Medevio/token.txt new file mode 100644 index 0000000..d31188b --- /dev/null +++ b/Medevio/token.txt @@ -0,0 +1 @@ +nYvrvgflIKcDiQg8Hhpud+qG8iGZ8eH8su4nyT/Mgcm7XQp65ygY9s39+O01wIpk/7sKd6fBHkiKvsqH \ No newline at end of file