#!/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()