From 52e8853639c170c04bfe22e0990c8147c1b2a142 Mon Sep 17 00:00:00 2001 From: "vladimir.buzalka" Date: Wed, 24 Sep 2025 16:40:34 +0200 Subject: [PATCH] Z230 --- ScrapePozadavkyJustManualScrolling.py | 118 +++++++++++++ ScrapePozadavkyPrint01.py | 236 ++++++++++++++++++++++++++ ScrapePozadavkyZmenJeden.py | 41 +++++ 3 files changed, 395 insertions(+) create mode 100644 ScrapePozadavkyJustManualScrolling.py create mode 100644 ScrapePozadavkyPrint01.py create mode 100644 ScrapePozadavkyZmenJeden.py diff --git a/ScrapePozadavkyJustManualScrolling.py b/ScrapePozadavkyJustManualScrolling.py new file mode 100644 index 0000000..ca1a307 --- /dev/null +++ b/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/ScrapePozadavkyPrint01.py b/ScrapePozadavkyPrint01.py new file mode 100644 index 0000000..d69f3e3 --- /dev/null +++ b/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/ScrapePozadavkyZmenJeden.py b/ScrapePozadavkyZmenJeden.py new file mode 100644 index 0000000..b1880ee --- /dev/null +++ b/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()