Compare commits

..

5 Commits

Author SHA1 Message Date
administrator beeeaa242f z230 2026-05-13 13:33:52 +02:00
administrator 57e60b4a47 Report_AgendaPozadavky: přidán list Požadavky kompletní (všechny záznamy)
- Nový sheet "Požadavky kompletní" — všechny požadavky z pozadavky bez filtru
- Sloupce: Date, Title, Patient, DOB, Request_ID, doneAt, removedAt
- 11 415 záznamů včetně uzavřených a smazaných

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:32:25 +02:00
administrator 7e2be6e495 Report_AgendaPozadavky: přidán list Agenda kompletní z medevio_agenda
- Nový sheet "Agenda kompletní" načtený z MySQL tabulky medevio_agenda
- Stejný formát a styling jako ostatní listy
- 4545 řádků (historická + budoucí agenda)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:30:08 +02:00
administrator 46b7227b18 Přidán sync_agenda_to_mysql.py — historický a inkrementální sync agendy do MySQL
- Nový skript pro ukládání Medevio rezervací do tabulky medevio_agenda
- Režim FULL: dávkový načet od 2025-01-01 po měsících
- Režim INCREMENTAL: jeden dotaz, -7 dní až +10 let dopředu
- Tabulka medevio_agenda se vytvoří automaticky (CREATE TABLE IF NOT EXISTS)
- UPSERT logika — bezpečné opakované spouštění

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:27:15 +02:00
administrator f2b3ebe0b4 Report_AgendaPozadavky: fix deduplication, UTF-8 output, export path
- Fix deduplication: deduplicate only by Request_ID (not Patient+Title combo)
- Reservations without Request_ID are kept as-is
- Add UTF-8 stdout fix for Windows console (emoji support)
- Change export dir to u:\Dropbox\!!!Days\Downloads Z230
- Rename response variable r → response to avoid collision with loop variable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:17:26 +02:00
2 changed files with 319 additions and 11 deletions
@@ -6,9 +6,19 @@ Full Medevio Report:
- Agenda (API, next 30 days)
- Otevřené požadavky (MySQL)
- Merged (Agenda + Open, deduplicated)
- Agenda kompletní (MySQL medevio_agenda, všechny záznamy)
- Vaccine sheets (from merged data)
"""
import sys
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")
import re
import json
import pymysql
@@ -38,7 +48,7 @@ DB_CONFIG = {
"cursorclass": pymysql.cursors.DictCursor,
}
EXPORT_DIR = Path(r"u:\Dropbox\Ordinace\Reporty")
EXPORT_DIR = Path(r"u:\Dropbox\!!!Days\Downloads Z230")
EXPORT_DIR.mkdir(exist_ok=True, parents=True)
# Delete previous reports
@@ -143,9 +153,9 @@ payload = {
}""",
}
r = requests.post(GRAPHQL_URL, headers=headers, data=json.dumps(payload))
r.raise_for_status()
resp = r.json()
response = requests.post(GRAPHQL_URL, headers=headers, data=json.dumps(payload))
response.raise_for_status()
resp = response.json()
if "errors" in resp or "data" not in resp:
print("❌ API response:")
print(json.dumps(resp, indent=2, ensure_ascii=False))
@@ -187,13 +197,13 @@ 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...")
# ==================== 2️⃣ LOAD OPEN REQUESTS + COMPLETE AGENDA (MySQL) ====================
print("📡 Loading open requests and complete agenda from MySQL...")
conn = pymysql.connect(**DB_CONFIG)
with conn.cursor() as cur:
cur.execute(
"""
SELECT
SELECT
id AS Request_ID,
displayTitle AS Title,
pacient_prijmeni AS Pacient_Prijmeni,
@@ -206,6 +216,46 @@ with conn.cursor() as cur:
"""
)
rows = cur.fetchall()
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,
doneAt,
removedAt
FROM pozadavky
ORDER BY createdAt DESC
"""
)
pozadavky_all_rows = cur.fetchall()
with conn.cursor() as cur:
cur.execute(
"""
SELECT
date AS Date,
time_interval AS Time,
request_title AS Title,
patient_surname AS surname,
patient_name AS name,
patient_dob AS DOB,
insurance_short AS Insurance,
note AS Note,
color AS Color,
request_id AS Request_ID,
reservation_id AS Reservation_ID
FROM medevio_agenda
ORDER BY date, time_interval
"""
)
agenda_full_rows = cur.fetchall()
conn.close()
df_open = pd.DataFrame(rows)
@@ -237,6 +287,30 @@ if not df_open.empty:
]
print(f"✅ Loaded {len(df_open)} open requests.")
df_agenda_full = pd.DataFrame(agenda_full_rows)
if not df_agenda_full.empty:
df_agenda_full["Patient"] = (
df_agenda_full["surname"].fillna("") + " " + df_agenda_full["name"].fillna("")
).str.strip()
df_agenda_full = df_agenda_full[
["Date", "Time", "Title", "Patient", "DOB", "Insurance", "Note", "Color", "Request_ID", "Reservation_ID"]
].fillna("")
df_agenda_full["Date"] = df_agenda_full["Date"].astype(str)
print(f"✅ Loaded {len(df_agenda_full)} rows from medevio_agenda.")
df_pozadavky_all = pd.DataFrame(pozadavky_all_rows)
if not df_pozadavky_all.empty:
df_pozadavky_all["Patient"] = (
df_pozadavky_all["Pacient_Prijmeni"].fillna("") + " " + df_pozadavky_all["Pacient_Jmeno"].fillna("")
).str.strip()
df_pozadavky_all["Date"] = df_pozadavky_all["Created"].astype(str).str[:10]
df_pozadavky_all["doneAt"] = df_pozadavky_all["doneAt"].astype(str).str[:10].replace("None", "")
df_pozadavky_all["removedAt"] = df_pozadavky_all["removedAt"].astype(str).str[:10].replace("None", "")
df_pozadavky_all = df_pozadavky_all[
["Date", "Title", "Patient", "DOB", "Request_ID", "doneAt", "removedAt"]
].fillna("")
print(f"✅ Loaded {len(df_pozadavky_all)} total requests from pozadavky.")
# ==================== 3️⃣ MERGE + DEDUPLICATE ====================
print("🟢 Merging and deduplicating (Agenda preferred)...")
@@ -245,12 +319,15 @@ df_agenda["Source"] = "Agenda"
df_open["Source"] = "Open"
df_merged = pd.concat([df_agenda, df_open], ignore_index=True).fillna("")
# "Agenda" < "Open" alphabetically → Agenda rows come first after sort
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"
)
# Deduplicate by Request_ID only where non-empty (Agenda preferred over Open)
# Rows without Request_ID (pure reservations) are always kept as-is
mask_has_id = df_merged["Request_ID"] != ""
df_with_id = df_merged[mask_has_id].drop_duplicates(subset=["Request_ID"], keep="first")
df_no_id = df_merged[~mask_has_id]
df_merged = pd.concat([df_with_id, df_no_id], ignore_index=True)
df_merged = df_merged.drop(columns=["Source"], errors="ignore")
df_merged = df_merged.sort_values(["Date", "Time"], na_position="last").reset_index(
@@ -264,12 +341,16 @@ 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)
df_agenda_full.to_excel(writer, sheet_name="Agenda kompletní", index=False)
df_pozadavky_all.to_excel(writer, sheet_name="Požadavky kompletní", index=False)
wb = load_workbook(xlsx_path)
for name, df_ref in [
("Agenda", df_agenda),
("Otevřené požadavky", df_open),
("Merged", df_merged),
("Agenda kompletní", df_agenda_full),
("Požadavky kompletní", df_pozadavky_all),
]:
ws = wb[name]
format_ws(ws, df_ref)
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Sync Medevio agenda reservations to MySQL table medevio_agenda.
Modes (set MODE below):
FULL Load all reservations from 2025-01-01 to today (monthly batches)
INCREMENTAL Last 7 days to +30 days ahead
"""
import sys
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")
import json
import time
import pymysql
import requests
from pathlib import Path
from datetime import datetime, date
from dateutil import parser as dtparser, tz
from dateutil.relativedelta import relativedelta
# ==================== CONFIG ====================
GRAPHQL_URL = "https://api.medevio.cz/graphql"
CALENDAR_ID = "144c4e12-347c-49ca-9ec0-8ca965a4470d"
CLINIC_SLUG = "mudr-buzalkova"
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,
}
PRAGUE_TZ = tz.gettz("Europe/Prague")
FULL_SINCE = date(2025, 1, 1)
# ==================== MODE ====================
MODE = "INCREMENTAL" # "FULL" nebo "INCREMENTAL"
# ==================== TABLE ====================
CREATE_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS medevio_agenda (
reservation_id VARCHAR(36) NOT NULL PRIMARY KEY,
start_dt DATETIME,
end_dt DATETIME,
date DATE,
time_interval VARCHAR(20),
note TEXT,
done TINYINT(1),
color VARCHAR(50),
request_id VARCHAR(36),
request_title VARCHAR(500),
patient_name VARCHAR(200),
patient_surname VARCHAR(200),
patient_dob VARCHAR(50),
insurance_short VARCHAR(50),
synced_at DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
# ==================== UPSERT ====================
UPSERT_SQL = """
INSERT INTO medevio_agenda (
reservation_id, start_dt, end_dt, date, time_interval,
note, done, color,
request_id, request_title,
patient_name, patient_surname, patient_dob, insurance_short,
synced_at
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
start_dt = VALUES(start_dt),
end_dt = VALUES(end_dt),
date = VALUES(date),
time_interval = VALUES(time_interval),
note = VALUES(note),
done = VALUES(done),
color = VALUES(color),
request_id = VALUES(request_id),
request_title = VALUES(request_title),
patient_name = VALUES(patient_name),
patient_surname = VALUES(patient_surname),
patient_dob = VALUES(patient_dob),
insurance_short = VALUES(insurance_short),
synced_at = VALUES(synced_at)
"""
def parse_dt(iso_str):
"""Parse ISO datetime, convert to Europe/Prague, return naive datetime for MySQL."""
if not iso_str:
return None
try:
return dtparser.isoparse(iso_str).astimezone(PRAGUE_TZ).replace(tzinfo=None)
except Exception:
return None
def fetch_reservations(headers, since: datetime, until: datetime):
payload = {
"operationName": "ClinicAgenda_ListClinicReservations",
"variables": {
"calendarIds": [CALENDAR_ID],
"clinicSlug": CLINIC_SLUG,
"since": since.strftime("%Y-%m-%dT%H:%M:%S") + "Z",
"until": until.strftime("%Y-%m-%dT%H:%M:%S") + "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 }
}
}
}
}""",
}
response = requests.post(GRAPHQL_URL, headers=headers, json=payload, timeout=30)
response.raise_for_status()
resp = response.json()
if "errors" in resp or "data" not in resp:
print(f"❌ API error: {resp}")
return []
return resp["data"]["reservations"] or []
def upsert_reservations(conn, reservations):
now = datetime.now().replace(microsecond=0)
rows = []
for r in reservations:
req = r.get("request") or {}
patient = req.get("extendedPatient") or {}
insurance = patient.get("insuranceCompanyObject") or {}
start_dt = parse_dt(r.get("start"))
end_dt = parse_dt(r.get("end"))
rows.append((
r["id"],
start_dt,
end_dt,
start_dt.date() if start_dt else None,
f"{start_dt.strftime('%H:%M')}-{end_dt.strftime('%H:%M')}" if start_dt and end_dt else None,
r.get("note") or None,
1 if r.get("done") else 0,
r.get("color") or None,
req.get("id") or None,
req.get("displayTitle") or None,
patient.get("name") or None,
patient.get("surname") or None,
patient.get("dob") or None,
insurance.get("shortName") or None,
now,
))
with conn.cursor() as cur:
cur.executemany(UPSERT_SQL, rows)
conn.commit()
return len(rows)
def main():
token = TOKEN_PATH.read_text(encoding="utf-8").strip()
if token.startswith("Bearer "):
token = token.split(" ", 1)[1]
headers = {
"content-type": "application/json",
"authorization": f"Bearer {token}",
"origin": "https://my.medevio.cz",
"referer": "https://my.medevio.cz/",
}
conn = pymysql.connect(**DB_CONFIG)
with conn.cursor() as cur:
cur.execute(CREATE_TABLE_SQL)
conn.commit()
print("✅ Tabulka medevio_agenda připravena.")
today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
if MODE == "FULL":
batch_start = datetime(FULL_SINCE.year, FULL_SINCE.month, FULL_SINCE.day)
print(f"📥 Režim FULL: {FULL_SINCE}{today.date()}")
total = 0
while batch_start < today:
batch_end = min(batch_start + relativedelta(months=1), today)
reservations = fetch_reservations(headers, batch_start, batch_end)
count = upsert_reservations(conn, reservations)
total += count
print(f" {batch_start.date()}{batch_end.date()}: {count} rezervací")
batch_start = batch_end
time.sleep(0.5)
print(f"\n✅ FULL hotovo — celkem {total} rezervací uloženo.")
else:
since = today - relativedelta(days=7)
until = today + relativedelta(years=10)
print(f"🔄 Režim INCREMENTAL: {since.date()}{until.date()}")
reservations = fetch_reservations(headers, since, until)
count = upsert_reservations(conn, reservations)
print(f"✅ INCREMENTAL hotovo — {count} rezervací upsertováno.")
conn.close()
if __name__ == "__main__":
main()