# 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. --- ## 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)) ``` --- ## Key IDs | Entity | ID | |--------|----| | Clinic | `25f24970-dae3-4f80-9337-d3616e53fb10` | | Clinic slug | `mudr-buzalkova` | | Calendar MUDr. Buzalkova (manzelka) | `144c4e12-347c-49ca-9ec0-8ca965a4470d` | | Calendar Vlado | `b6555c7e-4e95-4657-b441-87c2c9a7b2ca` | | AIS entity (Medicus) | `ef1549a5-d266-4f52-9a4d-7275e79ac82e` | --- ## Complete GraphQL Operations Catalog ### Session & Auth | Operation | Purpose | |-----------|---------| | `AccessToken_AuthSelf` | Verify or refresh session | ### Search | Operation | Variables | Response | |-----------|-----------|----------| | `Search` | `clinicSlug`, `locale`, `query` | `clinic`, `results` (patients + requests) | ### Patient Card (modal) | Operation | Variables | Response | |-----------|-----------|----------| | `ClinicPatientDetailModal_GetData` | `clinicSlug`, `patientId`, `patientUuid`, `locale`, `challengesStatus` | `clinic`, `calendars`, `patient` (full detail), `challenges`, `patientRequestsResponse`, `treatmentPlanPatients`, `premiumPlans`, `insuranceCards`, `pinnedRequests`, `canBeRemoved` | | `ClinicPatientRequestHistory_FilterPatientRequestsForClinic` | `clinicSlug`, `locale`, `patientId`, `patientRequestFilter`, `pageInfo {first, offset}` | `filterResponse`, `clinic`, `queues`, `calendars`, `userECRFs`, `substates`, `tags` | | `ClinicPatientDetailModal_FindMergeSuggestions` | `clinicSlug`, `patientUuid` | `mergeSuggestions` | #### Patient object structure (from GetData) ``` patient { id, name, surname, identificationNumber (rodne cislo bez lomitka), sex (Male/Female), dob (YYYY-MM-DD), email, phone, kind (Human), type (Human), status (ACTIVE), isInClinic, isInOrganizationOnly, hasMobileApp, anamnesisShared, anamnesisStatusForClinic { updatedAt }, insuranceCompanyObject { id, code, name, shortName }, tags [], familyMembers [], clinics [], note, organizationNotes [], city, street, houseNumber, user { id, name, surname, email, phone, registrationCompletedTime }, createdAt, editableByDoctor, userRelationship, premiumPlanPatient } ``` ### Patient Edit | Operation | Variables | Response | |-----------|-----------|----------| | `EditPatientModal_GetClinic` | `clinicSlug` | `clinic` | | `ClinicEditPatient_GetPatientForClinic` | `clinicSlug`, `patientId`, `includePatient` | `patient`, `clinic` | | `ClinicEditPatient_ListInsuranceCompanies` | `country` | `insuranceCompanies []` (id, code, name) | | `UseSupportedCountryCodes_Codes` | (none) | `codes` (phone country codes) | ### Patient List (Kartoteka) | Operation | Variables | Response | |-----------|-----------|----------| | `LegacyClinicPatientListPage_ListClinicPatients` | `clinicSlug`, `criteria {premiumPlanId, tagId}`, `pageInfo {first: 50, offset}` | `count`, `patients`, `clinic` | #### Fungující dotaz pro stránkovaný seznam pacientů (sync) Pozor: `listClinicPatients` nefunguje pro hromadný export. Fungující query je `listPatients`: ```graphql query SyncListPatients($clinicSlug: String!, $pageInfo: PageInfo!, $filter: ListPatientFilter!) { patientsList: listPatients(clinicSlug: $clinicSlug, filter: $filter, pageInfo: $pageInfo) { count patients { id identificationNumber name surname sex phone status2 isInClinic locale insuranceCompanyObject { id shortName } user { id phone name surname } } } } ``` Variables: ```json { "clinicSlug": "mudr-buzalkova", "filter": {}, "pageInfo": { "first": 50, "offset": 0 } } ``` Stránkování: `offset` se zvyšuje po 50, `count` v odpovědi = celkový počet pacientů. #### Dotaz pro detail jednoho pacienta ```graphql query GetPatientDetail($clinicSlug: String!, $patientId: String!) { patient: getPatientForClinic(clinicSlug: $clinicSlug, patientId: $patientId) { id name surname identificationNumber sex dob email phone status isInClinic hasMobileApp anamnesisShared note city street houseNumber createdAt insuranceCompanyObject { code name } user { id email phone } } } ``` Variables: ```json { "clinicSlug": "mudr-buzalkova", "patientId": "" } ``` ### Sync pacientů do MySQL Dvoustupňový proces: 1. `listPatients` stránkovaně stáhne seznam všech patient IDs (po 50) 2. Pro každého pacienta se zavolá `getPatientForClinic` pro detail 3. UPSERT do `medevio.medevio_pacient` (23 sloupců) Skript: `Medevio/80 Pacienti/sync_patients_to_mysql.py` Výsledek: 1963 pacientů synchronizováno (květen 2026). **Má pacient Medevio účet?** → `user_id IS NOT NULL` (žádný redundantní boolean sloupec) - 1 230 pacientů má účet (63 %), 733 nemá (37 %) — stav květen 2026 #### Známé problémy - **`hasMobileApp` je nefunkční na úrovni pacienta** — ověřeno na více endpointech: - `getPatientForClinic` → vždy `false` ❌ - `ClinicPatientDetailModal_GetData` → `patient.hasMobileApp` vždy `false` ❌ - `listPatients` → pole vůbec neexistuje v GraphQL schema ❌ - **Správná hodnota `hasMobileApp` je na objektu `request`**, nikoli pacienta: - `ClinicRequestDetail_GetPatientRequest2` → `request.hasMobileApp` vrací správně ✅ - `request.extendedPatient.hasMobileApp` je stále vždy `false` ❌ - UI komponenta `PatientRequest.MessageSend` čte hodnotu z `request.hasMobileApp` a rozhoduje: - `true` → zobrazí "Odeslat do aplikace" - `false` + telefon existuje → zobrazí "Odeslat přes SMS" - `false` + žádný telefon → tlačítko se nezobrazí vůbec - **Praktické získání `hasMobileApp` pro pacienty:** - Stáhnout všechny požadavky přes `ClinicLegacyRequestList_ListPatientRequestsForClinic` (aktivní i vyřízené) - Pro každého pacienta vzít nejnovější `hasMobileApp` z jeho požadavků - Nevýhoda: pacienti bez jediného požadavku budou `NULL`; hodnota odráží stav k době posledního požadavku ### Requests (Pozadavky) | Operation | Variables | Response | |-----------|-----------|----------| | `ClinicLegacyRequestList_ListPatientRequestsForClinic` | `clinicSlug`, `locale`, `pageInfo {first: 30, offset}`, `queueAssignment`, `queueId`, `state` (ACTIVE/DONE) | `requests`, `clinic` | | `ClinicRequestList2` | `clinicSlug`, `queueId`, `queueAssignment`, `state` (ACTIVE/DONE), `pageInfo {first, offset}`, `locale` | `requestsResponse { count, patientRequests [] }` | `ClinicRequestList2` volá `listPatientRequestsForClinic2` — novější endpoint, vrací `count` a plně stránkovaný seznam. Struktura položky: ``` patientRequest { id, displayTitle, createdAt, updatedAt, doneAt, removedAt, extendedPatient { name, surname, identificationNumber }, lastMessage { createdAt } } ``` Skript: `Medevio/10ReadPozadavky/PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py` #### Request list item structure ``` request { id, createdAt, dueDate, displayTitle, doneAt, removedAt, priority, evaluationResult, clinicId, extendedPatient { id, identificationNumber, kind, name, surname, note, owner, key, premiumPlanPatient, status, tags [] }, queue { id, name }, substate, pinType, ecrf { id, name, iconMaskColor { name } }, flags [], hasMobileApp } ``` ### Request Creation (Vytvoření požadavku "Recept na léky") — ODCHYCENO/OVĚŘENO 2026-06-13 Přes API **lze založit požadavek s plně vyplněným pacientským dotazníkem** (oba fieldy), takže vypadá jako reálné podání pacientem. Funkce: `mcp_medevio.zaloz_pozadavek_recept`. (Pozn.: lékařské UI „Nový požadavek" pole dotazníku NEzobrazí — ale API je přijme.) **Dvoukrok (+ volitelně štítek):** ```graphql # 1) vyplň ECRF formulář → vrátí ecrfFill.id mutation Step_FillECRFForm($input: FillECRFFormInput!) { patientEcrfFill: fillECRFForm(input: $input) { id } } # input: { # patientId, sid: "ERECEPT_SIMPLEST_BEZ_DAVKOVANI", stepId: "erecept-gp-request", # byDoctor: false, # fields: [{ fieldName: "nazev-leku", value: "", checkedEnumerations: [] }] # } → pole "Název léků" v dotazníku # 2) vytvoř požadavek mutation ...CreatePatientRequestWithoutReservation($clinicSlug: String!, $input: ...) { patientRequest: createPatientRequestWithoutReservation(clinicSlug: $clinicSlug, input: $input) { id } } # input: { # patientId, userECRFId, ecrfFillIds: [], medicalRecordIds: [], challengeId: null, # userNote: "", ← zobrazí se jako pole "Poznámka" v dotazníku # createdByDoctor: false # } ``` POZOR: `createPatientRequest` (bez „WithoutReservation") požadavek vytvoří, ale NEZOBRAZÍ se v žádné frontě — používat `createPatientRequestWithoutReservation`. | Klíč | Hodnota | |------|---------| | ECRF „Recept na léky" `userECRFId` | `79488e86-e9e5-47e3-8b19-7e5229427f23` | | ECRF `sid` | `ERECEPT_SIMPLEST_BEZ_DAVKOVANI` | | ECRF `stepId` | `erecept-gp-request` | | pole 1 `fieldName` | `nazev-leku` (→ „Název léků") | | pole 2 | `userNote` v create inputu (→ „Poznámka") | Seznam typů požadavků: `UserEcrfAutocomplete_ListUserECRFsByClinic`. ### Tagy / štítky požadavku — ODCHYCENO 2026-06-13 ```graphql query TagRequestEditModal_ListTags($clinicSlug: String!, $requestId: UUID!) { ... } # seznam štítků + zda jsou přiřazené mutation TagRequestEditModal_AssignTagToRequest($clinicSlug: String!, $requestId: UUID!, $tagId: UUID!) { tagRequest: assignTagToPatientRequest(clinicSlug: $clinicSlug, patientRequestId: $requestId, tagId: $tagId) { id } } # Vytvoření nového štítku: mutation TagEditModal_CreateTag($clinicSlug: String!, $input: CreateTagInput!) { tag: createTag(clinicSlug: $clinicSlug, input: $input) { id name color icon important isOrganizationWide } } # input: { name, color (např. "SKY"/"ORCHID"), icon: null, important: false, type: "patient_request", isOrganizationWide: false } ``` | Štítek | tagId | barva | |--------|-------|-------| | `CLAUDE` | `c136aeca-0625-4c43-b81f-fc3949ec6ba6` | ORCHID | | `OVĚŘIT PACIENTA` | `9d3271b3-309d-4d20-93ee-285f3e56ba42` | SKY | | `NEZAPOMENOUT` | `5bced917-83d2-46db-896c-c8e615de1a69` | GREY | ### Request Detail | Operation | Variables | Response | |-----------|-----------|----------| | `ClinicRequestDetail_GetPatientRequest2` | `clinicSlug`, `isDoctor`, `requestId`, `locale` | `request` (full detail) | | `UseMessages_ListMessages` | `requestId`, `updatedSince` | `messages` | | `Communication_GetClinicFooter` | `clinicSlug` | `footer` | | `ClinicRequestNotes_Get` | `patientRequestId` | `notes []` | | `ClinicRequestNotes_Update` | `noteInput { id, content }` | `{ id }` | | `ClinicRequestNotes_Create` | `noteInput { requestId, content }` | `{ id }` | | `ClinicRequestDetail_GetMessages` | `clinicSlug`, `requestId` | zprávy (alternativní endpoint) | #### Interní poznámky k požadavku (klinické notes) ```graphql # Čtení query ClinicRequestNotes_Get($patientRequestId: String!) { notes: getClinicPatientRequestNotes(requestId: $patientRequestId) { id content createdAt updatedAt createdBy { id name surname } } } # Aktualizace existující mutation ClinicRequestNotes_Update($noteInput: UpdateClinicPatientRequestNoteInput!) { updateClinicPatientRequestNote(noteInput: $noteInput) { id } } # Vytvoření nové mutation ClinicRequestNotes_Create($noteInput: CreateClinicPatientRequestNoteInput!) { createClinicPatientRequestNote(noteInput: $noteInput) { id } } ``` K jednomu požadavku existuje typicky jedna interní poznámka. Pokud neexistuje → Create, pokud existuje → Update. Skript: `Medevio/30 ManipulacePoznámek/101 JednoducheDoplneniInterniPoznamky.py` #### Alternativní endpoint pro zprávy konverzace ```graphql query ClinicRequestDetail_GetMessages($clinicSlug: String!, $requestId: ID!) { clinicRequestDetail_GetPatientRequestMessages(clinicSlug: $clinicSlug, requestId: $requestId) { id text createdAt sender { id name } extendedPatient { name surname identificationNumber } } } ``` Skript: `Medevio/10ReadPozadavky/10 UpdateMessageswithJmeno.py` #### Request detail structure ``` request { id, doneAt, doneBy { id, name, surname }, removedAt, queue, isInClinic, clinic { id, paymentEnabled, hasInvoicingEnabled, slug, features }, clinicMedicalRecord, clinicMedicalRecordVisibleToPatient, extendedPatient { ... (same as patient) }, ecrf, ecrfFilledData, eventType (PATIENT_REQUEST), flags [], invoice, userNote, paymentData, recording, reservations [], evaluationResult, tags [], createdAt, createdBy, customTitle, createdByDoctor, displayTitle, referringClinic, pinType, dueDate, substate, hasMobileApp, priceWhenCreated, currencyWhenCreated } ``` ### Calendar (Kalendar) | Operation | Variables | Response | |-----------|-----------|----------| | `ClinicCalendar_ListClinicReservations` | `calendarIds []`, `clinicCountry`, `clinicSlug`, `locale`, `since` (ISO), `until` (ISO), `showTimeSlots`, `schedulePatientId`, `emptyCalendarIds` | `holidays`, `reservations`, `vacations` | | `ClinicCalendar_GetWindows` | `calendarIds []`, `clinicSlug`, `locale`, `since`, `until` | `calendarWindows` (ordinacni hodiny) | | `Agenda_ListAll` | `calendarIds []`, `clinicSlug`, `locale`, `since`, `until` | `reservations` (jednorazove) + `recurringReservations` (opakujici) | `Agenda_ListAll` volá dva endpointy zároveň: - `listClinicReservations` → jednorazové rezervace (pacienti + poznámky lékaře) - `listClinicRecurringReservations` → opakující se rezervace; vrací `recurringReservation { id calendarId color note rrule { frequency interval dtstart tzid byweekday bymonthday byweekno } }` a `instances { start end note color }` Fungující skript: `Medevio/agenda_dne.py` — funkce `list_agendu(start, end, calendar)` #### Vytvoření poznámky lékaře v kalendáři ```graphql mutation CreateReservation_MakeReservationByDoctor( $clinicSlug: String!, $color: ECRFIconColor, $note: String!, $timeSlotInput: TimeSlotInput! ) { reservation: makeReservationByDoctor( clinicSlug: $clinicSlug color: $color note: $note timeSlotInput: $timeSlotInput ) { id __typename } } ``` Variables: `clinicSlug`, `color` (např. `"CHARCOAL"`), `note` (text), `timeSlotInput { calendarId, start (UTC ISO), end (UTC ISO) }` Vrací: `reservation.id` — UUID nové rezervace. Skript: `Medevio/zapis_poznamky.py` — funkce `zapis_poznamku(calendar, den, cas, trvani_min, poznamka, color)` #### Smazání / zrušení rezervace ```graphql # Jednorazová: mutation UpdateReservation_CancelReservationByDoctor( $clinicSlug: String!, $reservationId: UUID! ) { reservation: cancelReservationByDoctor(clinicSlug: $clinicSlug, reservationId: $reservationId) { id __typename } } # Opakující se: mutation UpdateReservation_CancelRecurringReservationByDoctor($input: RemoveRecurringReservationInput!) { success: removeDateFromRecurringReservation(input: $input) } ``` Pro opakující se: `input { clinicSlug, recurringReservationId, date (UTC ISO), updateType: "Single"|"ThisAndFuture"|"All" }` Skript: `Medevio/smaz_poznamku.py` — funkce `smaz_jednorazovou(reservation_id)`, `smaz_opakujici(recurring_id, date, update_type)` #### Reservation structure ``` reservation { id, calendarId, clinic { id, slug, aisEntity { id, slug } }, color, done, canceledAt, end (ISO), note, request { displayTitle, id, ecrf { id, name, iconMaskColor { name } }, extendedPatient { dob, email, id, identificationNumber, kind, name, surname, note, phone, sex, status, tags [], insuranceCompanyObject { code, name } } } } ``` ### Patient Status Change (Zmena stavu) | Operation | Variables | Response | |-----------|-----------|----------| | `ClinicPatientEditStatusModal_UpdateClinicPatientStatus` | `clinicSlug`, `patientId` (UUID), `status` | `updated` (boolean) | #### Dostupné stavy (`ClinicPatientStatus`) | UI název | API hodnota | |----------|-------------| | Aktivní | `ACTIVE` | | Odmítnutý | `DECLINED` | | Vyřazený | `REMOVED` | | Čekající | `PENDING` | #### Mutation ```graphql mutation ClinicPatientEditStatusModal_UpdateClinicPatientStatus($clinicSlug: String!, $patientId: String!, $status: ClinicPatientStatus!) { updated: updateClinicPatientStatus( clinicSlug: $clinicSlug patientId: $patientId status: $status ) } ``` Odpověď: `{ "data": { "updated": true } }` Skript pro hromadnou změnu: `Medevio/80 Pacienti/bulk_set_removed.py` --- ### References (Referovani) | Operation | Variables | Response | |-----------|-----------|----------| | `ClinicReferences_GetClinic` | `clinicSlug` | `clinic`, `substates` | | `ClinicReferences_ListReferencesSentByClinic` | `clinicSlug`, `locale`, `pageInfo {first: 51, offset}` | `references` | ### Treatment Plans (Lecebne plany) | Operation | Variables | Response | |-----------|-----------|----------| | `PlanPatientsPage_GetClinic` | `clinicSlug` | `clinic` | | `PlanPatientsPage_ListPlanPatients` | `clinicSlug`, `pageInfo {first: 51, offset}` | `planPatients` | ### Settings (Nastaveni) | Operation | Purpose | |-----------|---------| | `ClinicSettingsSubNavigation_GetClinic` | Navigation state for settings | | `ClinicRequestSettings_GetClinic` | Request settings | | `UserEcrfListing_GetClinicECRFMenu` | List request types (ECRF forms) | | `ClinicEcrfSet_ListECRFSets` | List request type groups | | `ClinicOpeningHoursEdit_GetClinicOpeningHours2` | Opening hours settings | | `ClinicPaymentsSettings_GetClinic` | Payment settings | | `StripeVerificationContext_GetClinicCompanyInfo` | Stripe payment verification | | `SettingsPage` | General settings | | `InviteSettings` | Invite settings | | `InviteSettings_GetInviteContent` | Invite content/templates | | `ImportAisSettings_GetClinic` | AIS integration settings | | `ImportAisSettings_GetAises` | Available AIS systems | | `ClinicUserGrid_Columns` | User grid columns (queues, calendars) | | `ClinicUserGrid_Rows` | User list for clinic | ### Clinic Info | Operation | Purpose | |-----------|---------| | `ClinicNavigation_GetClinic` | Basic clinic info | | `ClinicAgenda_GetCalendarsForClinic` | List calendars within clinic | --- ## 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']` | --- ## URL Patterns | Page | URL | |------|-----| | Search | `/mudr-buzalkova/klinika/hledat/?q={query}` | | Patient card | `/mudr-buzalkova/klinika/hledat/?q={query}&pacient={uuid}` | | Request detail | `/mudr-buzalkova/klinika/hledat/?q={query}&pozadavek={uuid}` | | Requests (active) | `/mudr-buzalkova/klinika/pozadavky/` | | Requests (done) | `/mudr-buzalkova/klinika/pozadavky?vyrizene=1` | | Calendar | `/mudr-buzalkova/klinika/kalendar/?kalendar={calendarId}&pohled=pracovni-tyden` | | Patient list | `/mudr-buzalkova/klinika/kartoteka/` | | References | `/mudr-buzalkova/klinika/reference` | | Treatment plans | `/mudr-buzalkova/klinika/lecebne-plany/pacienti` | | Settings - requests | `/mudr-buzalkova/klinika/nastaveni/pozadavky` | | Settings - groups | `/mudr-buzalkova/klinika/nastaveni/skupiny-pozadavku` | | Settings - invites | `/mudr-buzalkova/klinika/nastaveni/pozvanky` | | Settings - integrations | `/mudr-buzalkova/klinika/nastaveni/propojky` | | Settings - hours | `/mudr-buzalkova/klinika/nastaveni/ordinacni-doba` | | Settings - payments | `/mudr-buzalkova/klinika/nastaveni/platby` | | Users | `/mudr-buzalkova/klinika/uzivatele/` | --- ## Insurance Companies (CZ) | Code | Name | |------|------| | 0 | Ministerstvo zdravotnictvi CR | | 111 | VZP | | 201 | VoZP | | 205 | CPZP | | 207 | OZP | | 209 | ZPS | | 211 | ZPMV | | 213 | RBP | | 999 | Jina | --- ## Clinic Features (enabled) RESERVATION_REMINDERS, PUSH_NOTIFICATIONS, INVITES, ON_DEMAND_SLOT_GENERATOR, REQUEST_TYPES_ACCESS_RULES, CAN_REFER_PATIENTS, TREATMENT_PLANS_V2, MEDICAL_RECORDS, CAMPAIGNS, PATIENT_SEGMENTATION, ADVANCED_CALENDAR_FUNCTIONS, PATIENT_CLINIC_STATUS, STANDARD_TARIFF_FUNCTIONS, CLINIC_CLOSED_WARNINGS, ADVANCED_SWITCHBOARD, REQUEST_AUTOMATIONS, REQUEST_PINNING, AI_SWITCHBOARD_PROMO, PATIENT_REQUEST_SUBSTATES --- ## 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 `ClinicCalendar_ListClinicReservations` - `get_reservation_detail(reservation_id)` - `get_patient(patient_id)` using `ClinicPatientDetailModal_GetData` - `list_patients(offset)` using `LegacyClinicPatientListPage_ListClinicPatients` - `search_patients(query)` using `Search` - `get_requests(state, offset)` using `ClinicLegacyRequestList_ListPatientRequestsForClinic` - `get_request_detail(request_id)` using `ClinicRequestDetail_GetPatientRequest2` - Map vaccine keywords. - Export to Excel/MySQL for vaccine order planning.