notebook
This commit is contained in:
@@ -178,12 +178,27 @@ df.to_excel(xlsx_path, index=False)
|
|||||||
wb = load_workbook(xlsx_path)
|
wb = load_workbook(xlsx_path)
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
|
|
||||||
# style header
|
# === Apply styling and custom column widths ===
|
||||||
for col in range(1, len(df.columns) + 1):
|
widths = {
|
||||||
c = ws.cell(row=1, column=col)
|
1: 11, # A - Date
|
||||||
|
2: 13, # B - Time
|
||||||
|
3: 45, # C - Title
|
||||||
|
4: 30, # D - Patient
|
||||||
|
5: 15, # E - DOB
|
||||||
|
6: 15, # F - Insurance
|
||||||
|
7: 30, # G - Note
|
||||||
|
8: 15, # H - Color
|
||||||
|
9: 37, # I - Request_ID
|
||||||
|
10: 37 # J - Reservation_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
for col_idx in range(1, len(df.columns) + 1):
|
||||||
|
col_letter = get_column_letter(col_idx)
|
||||||
|
c = ws.cell(row=1, column=col_idx)
|
||||||
c.font = Font(bold=True)
|
c.font = Font(bold=True)
|
||||||
c.alignment = Alignment(horizontal="center")
|
c.alignment = Alignment(horizontal="center")
|
||||||
ws.column_dimensions[get_column_letter(col)].width = 20
|
ws.column_dimensions[col_letter].width = widths.get(col_idx, 20)
|
||||||
|
|
||||||
ws.freeze_panes = "A2"
|
ws.freeze_panes = "A2"
|
||||||
|
|
||||||
wb.save(xlsx_path)
|
wb.save(xlsx_path)
|
||||||
|
|||||||
299
861 improvement formatting.py
Normal file
299
861 improvement formatting.py
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Query Medevio for the full agenda of 17 Oct 2025,
|
||||||
|
print raw API response, and export to Excel.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
import requests
|
||||||
|
import pandas as pd
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from Functions import get_reports_folder
|
||||||
|
from openpyxl.utils.dataframe import dataframe_to_rows
|
||||||
|
|
||||||
|
|
||||||
|
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
||||||
|
|
||||||
|
CALENDAR_ID = "144c4e12-347c-49ca-9ec0-8ca965a4470d"
|
||||||
|
CLINIC_SLUG = "mudr-buzalkova"
|
||||||
|
|
||||||
|
# ==================== Load Token ====================
|
||||||
|
def load_gateway_token(storage_path="medevio_storage.json"):
|
||||||
|
"""Return Medevio gateway-access-token from saved Playwright storage."""
|
||||||
|
path = Path(storage_path)
|
||||||
|
if not path.exists():
|
||||||
|
raise SystemExit(f"❌ Storage file not found: {path}")
|
||||||
|
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
|
state = json.load(f)
|
||||||
|
|
||||||
|
token = next(
|
||||||
|
(c["value"] for c in state["cookies"]
|
||||||
|
if c["name"] == "gateway-access-token"), None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
raise SystemExit("❌ gateway-access-token not found in storage file.")
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
gateway_token = load_gateway_token()
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"origin": "https://my.medevio.cz",
|
||||||
|
"referer": "https://my.medevio.cz/",
|
||||||
|
"authorization": f"Bearer {gateway_token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Dynamic date range ===
|
||||||
|
dnes = datetime.utcnow().date()
|
||||||
|
since = datetime.combine(dnes, datetime.min.time()).replace(microsecond=1)
|
||||||
|
until = since + relativedelta(months=1) - timedelta(milliseconds=1)
|
||||||
|
|
||||||
|
since_iso = since.isoformat() + "Z"
|
||||||
|
until_iso = until.isoformat() + "Z"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"operationName": "ClinicAgenda_ListClinicReservations",
|
||||||
|
"variables": {
|
||||||
|
"calendarIds": [CALENDAR_ID],
|
||||||
|
"clinicSlug": CLINIC_SLUG,
|
||||||
|
"since": since_iso,
|
||||||
|
"until": "2025-11-30T21:59:59.999Z",
|
||||||
|
"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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}""",
|
||||||
|
}
|
||||||
|
|
||||||
|
print("since:", since_iso)
|
||||||
|
print("until:", until_iso)
|
||||||
|
|
||||||
|
# ==================== Query API ====================
|
||||||
|
print("📡 Querying Medevio API for agenda...")
|
||||||
|
r = requests.post(GRAPHQL_URL, headers=headers, data=json.dumps(payload))
|
||||||
|
print("Status:", r.status_code)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = r.json()
|
||||||
|
except Exception as e:
|
||||||
|
print("❌ Could not parse JSON:", e)
|
||||||
|
print(r.text)
|
||||||
|
raise SystemExit()
|
||||||
|
|
||||||
|
if "data" not in data or "reservations" not in data["data"]:
|
||||||
|
raise SystemExit("⚠️ No 'reservations' data found in response.")
|
||||||
|
|
||||||
|
reservations = data["data"]["reservations"]
|
||||||
|
from dateutil import parser, tz
|
||||||
|
|
||||||
|
# ===== Process reservations into table =====
|
||||||
|
rows = []
|
||||||
|
for r in reservations:
|
||||||
|
req = r.get("request") or {}
|
||||||
|
patient = req.get("extendedPatient") or {}
|
||||||
|
insurance = patient.get("insuranceCompanyObject") or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_dt = parser.isoparse(r.get("start")).astimezone(tz.gettz("Europe/Prague"))
|
||||||
|
end_dt = parser.isoparse(r.get("end")).astimezone(tz.gettz("Europe/Prague"))
|
||||||
|
except Exception:
|
||||||
|
start_dt = end_dt = None
|
||||||
|
|
||||||
|
date_str = start_dt.strftime("%Y-%m-%d") if start_dt else ""
|
||||||
|
time_interval = f"{start_dt.strftime('%H:%M')}-{end_dt.strftime('%H:%M')}" if start_dt and end_dt else ""
|
||||||
|
|
||||||
|
rows.append({
|
||||||
|
"Date": date_str,
|
||||||
|
"Time": time_interval,
|
||||||
|
"Title": req.get("displayTitle") or "",
|
||||||
|
"Patient": f"{patient.get('surname','')} {patient.get('name','')}".strip(),
|
||||||
|
"DOB": patient.get("dob") or "",
|
||||||
|
"Insurance": insurance.get("shortName") or "",
|
||||||
|
"Note": r.get("note") or "",
|
||||||
|
"Color": r.get("color") or "",
|
||||||
|
"Request_ID": req.get("id") or "",
|
||||||
|
"Reservation_ID": r.get("id"),
|
||||||
|
})
|
||||||
|
|
||||||
|
df = pd.DataFrame(rows).sort_values(["Date", "Time"])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def kw_pattern(kw: str) -> str:
|
||||||
|
"""
|
||||||
|
Match the exact phrase kw (case-insensitive),
|
||||||
|
not as part of a '+something' continuation.
|
||||||
|
Examples:
|
||||||
|
'žloutenka a' ✅ matches '… žloutenka a …'
|
||||||
|
❌ NOT '… žloutenka a+b …'
|
||||||
|
'žloutenka a+b' ✅ matches exactly that phrase
|
||||||
|
"""
|
||||||
|
# start boundary: not preceded by a word char
|
||||||
|
# end guard: not followed by optional spaces + '+' + word
|
||||||
|
return rf"(?<!\w){re.escape(kw)}(?!\s*\+\s*\w)"
|
||||||
|
|
||||||
|
# ===== Excel export =====
|
||||||
|
EXPORT_DIR = Path(r"z:\Dropbox\Ordinace\Reporty")
|
||||||
|
EXPORT_DIR.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
|
xlsx_path = EXPORT_DIR / f"{timestamp} Agenda (30 dní dopředu).xlsx"
|
||||||
|
|
||||||
|
# Safely delete older Agenda reports
|
||||||
|
for old in EXPORT_DIR.glob("*Agenda (30 dní dopředu).xlsx"):
|
||||||
|
try:
|
||||||
|
old.unlink()
|
||||||
|
except PermissionError:
|
||||||
|
print(f"⚠️ File is open, skipping delete: {old}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Could not delete {old}: {e}")
|
||||||
|
|
||||||
|
# Export DataFrame
|
||||||
|
df.to_excel(xlsx_path, index=False)
|
||||||
|
wb = load_workbook(xlsx_path)
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "Agenda" # ✅ rename sheet
|
||||||
|
|
||||||
|
|
||||||
|
# === Apply styling and custom column widths ===
|
||||||
|
widths = {
|
||||||
|
1: 11, # Date
|
||||||
|
2: 13, # Time
|
||||||
|
3: 45, # Title
|
||||||
|
4: 30, # Patient
|
||||||
|
5: 15, # DOB
|
||||||
|
6: 15, # Insurance
|
||||||
|
7: 30, # Note
|
||||||
|
8: 15, # Color
|
||||||
|
9: 37, # Request_ID
|
||||||
|
10: 37 # Reservation_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
# Define styles
|
||||||
|
header_fill = PatternFill("solid", fgColor="FFFF00") # real yellow
|
||||||
|
alt_fill = PatternFill("solid", fgColor="F2F2F2") # light grey alternate rows
|
||||||
|
thin_border = Border(
|
||||||
|
left=Side(style="thin", color="000000"),
|
||||||
|
right=Side(style="thin", color="000000"),
|
||||||
|
top=Side(style="thin", color="000000"),
|
||||||
|
bottom=Side(style="thin", color="000000")
|
||||||
|
)
|
||||||
|
|
||||||
|
# === Format header ===
|
||||||
|
for col_idx in range(1, len(df.columns) + 1):
|
||||||
|
col_letter = get_column_letter(col_idx)
|
||||||
|
cell = ws.cell(row=1, column=col_idx)
|
||||||
|
cell.font = Font(bold=True)
|
||||||
|
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||||
|
cell.fill = header_fill
|
||||||
|
cell.value = str(cell.value).upper()
|
||||||
|
cell.border = thin_border
|
||||||
|
ws.column_dimensions[col_letter].width = widths.get(col_idx, 20)
|
||||||
|
|
||||||
|
# === Format data rows ===
|
||||||
|
for r_idx, row in enumerate(ws.iter_rows(min_row=2, max_row=ws.max_row, max_col=ws.max_column), start=2):
|
||||||
|
for cell in row:
|
||||||
|
cell.border = thin_border
|
||||||
|
if r_idx % 2 == 0: # alternate row background
|
||||||
|
cell.fill = alt_fill
|
||||||
|
|
||||||
|
ws.freeze_panes = "A2"
|
||||||
|
|
||||||
|
from openpyxl.utils.dataframe import dataframe_to_rows
|
||||||
|
|
||||||
|
# === Vaccine sheet configuration ===
|
||||||
|
VACCINE_SHEETS = {
|
||||||
|
"Chřipka": ["očkování", "chřipka"],
|
||||||
|
"COVID": ["očkování", "covid"],
|
||||||
|
"Pneumokok": ["očkování", "pneumo"],
|
||||||
|
"Hep A": ["očkování", "žloutenka a"],
|
||||||
|
"Hep B": ["očkování", "žloutenka b"],
|
||||||
|
"Hep A+B": ["očkování", "žloutenka a+b"],
|
||||||
|
"Klíšťovka": ["očkování", "klíšť"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Generate sheets based on keyword combinations ===
|
||||||
|
for sheet_name, keywords in VACCINE_SHEETS.items():
|
||||||
|
mask = pd.Series(True, index=df.index)
|
||||||
|
title_series = df["Title"].fillna("")
|
||||||
|
|
||||||
|
for kw in keywords:
|
||||||
|
pattern = kw_pattern(kw)
|
||||||
|
mask &= title_series.str.contains(pattern, flags=re.IGNORECASE, regex=True)
|
||||||
|
filtered_df = df[mask].copy()
|
||||||
|
|
||||||
|
if filtered_df.empty:
|
||||||
|
print(f"ℹ️ No matches for sheet '{sheet_name}' ({' AND '.join(keywords)})")
|
||||||
|
continue
|
||||||
|
|
||||||
|
ws_new = wb.create_sheet(title=sheet_name)
|
||||||
|
for r in dataframe_to_rows(filtered_df, index=False, header=True):
|
||||||
|
ws_new.append(r)
|
||||||
|
|
||||||
|
# === Apply formatting ===
|
||||||
|
for col_idx in range(1, len(filtered_df.columns) + 1):
|
||||||
|
col_letter = get_column_letter(col_idx)
|
||||||
|
c = ws_new.cell(row=1, column=col_idx)
|
||||||
|
c.font = Font(bold=True)
|
||||||
|
c.alignment = Alignment(horizontal="center", vertical="center")
|
||||||
|
c.fill = PatternFill("solid", fgColor="FFFF00") # bright yellow header
|
||||||
|
c.value = str(c.value).upper()
|
||||||
|
c.border = thin_border
|
||||||
|
ws_new.column_dimensions[col_letter].width = widths.get(col_idx, 20)
|
||||||
|
|
||||||
|
# Borders + alternating rows
|
||||||
|
for r_idx, row in enumerate(ws_new.iter_rows(min_row=2, max_row=ws_new.max_row, max_col=ws_new.max_column), start=2):
|
||||||
|
for cell in row:
|
||||||
|
cell.border = thin_border
|
||||||
|
if r_idx % 2 == 0:
|
||||||
|
cell.fill = PatternFill("solid", fgColor="F2F2F2")
|
||||||
|
|
||||||
|
ws_new.freeze_panes = "A2"
|
||||||
|
print(f"🟡 Created sheet '{sheet_name}' with {len(filtered_df)} rows ({' AND '.join(keywords)})")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
wb.save(xlsx_path)
|
||||||
|
print(f"📘 Exported clean agenda view to:\n{xlsx_path}")
|
||||||
Reference in New Issue
Block a user