Compare commits
5 Commits
fe698a4232
...
beeeaa242f
| Author | SHA1 | Date | |
|---|---|---|---|
| beeeaa242f | |||
| 57e60b4a47 | |||
| 7e2be6e495 | |||
| 46b7227b18 | |||
| f2b3ebe0b4 |
@@ -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,8 +197,8 @@ 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(
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user