This commit is contained in:
2025-11-16 07:53:29 +01:00
parent 02cb5bb9f8
commit 585e38284b
9 changed files with 1223 additions and 153 deletions

2
.idea/Medevio.iml generated
View File

@@ -4,7 +4,7 @@
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" /> <excludeFolder url="file://$MODULE_DIR$/.venv" />
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.12 virtualenv at U:\Medevio\.venv" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.12 (Medevio)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

2
.idea/misc.xml generated
View File

@@ -3,5 +3,5 @@
<component name="Black"> <component name="Black">
<option name="sdkName" value="Python 3.12 (Medevio)" /> <option name="sdkName" value="Python 3.12 (Medevio)" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 virtualenv at U:\Medevio\.venv" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (Medevio)" project-jdk-type="Python SDK" />
</project> </project>

View File

@@ -4,8 +4,9 @@
import pymysql import pymysql
import requests import requests
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime, timezone
import time import time
from dateutil import parser
# ================================ # ================================
# 🔧 CONFIGURATION # 🔧 CONFIGURATION
@@ -24,21 +25,22 @@ DB_CONFIG = {
"cursorclass": pymysql.cursors.DictCursor, "cursorclass": pymysql.cursors.DictCursor,
} }
# ⭐ NOVÝ TESTOVANÝ DOTAZ obsahuje lastMessage.createdAt
GRAPHQL_QUERY = r""" GRAPHQL_QUERY = r"""
query ClinicRequestGrid_ListPatientRequestsForClinic2( query ClinicRequestList2(
$clinicSlug: String!, $clinicSlug: String!,
$queueId: String, $queueId: String,
$queueAssignment: QueueAssignmentFilter!, $queueAssignment: QueueAssignmentFilter!,
$state: PatientRequestState,
$pageInfo: PageInfo!, $pageInfo: PageInfo!,
$locale: Locale!, $locale: Locale!
$state: PatientRequestState
) { ) {
requestsResponse: listPatientRequestsForClinic2( requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug, clinicSlug: $clinicSlug,
queueId: $queueId, queueId: $queueId,
queueAssignment: $queueAssignment, queueAssignment: $queueAssignment,
pageInfo: $pageInfo, state: $state,
state: $state pageInfo: $pageInfo
) { ) {
count count
patientRequests { patientRequests {
@@ -53,40 +55,71 @@ query ClinicRequestGrid_ListPatientRequestsForClinic2(
surname surname
identificationNumber identificationNumber
} }
lastMessage {
createdAt
}
} }
} }
} }
""" """
# ================================
# 🧿 SAFE DATETIME PARSER (ALWAYS UTC → LOCAL)
# ================================
def to_mysql_dt_utc(iso_str):
"""
Parse Medevio timestamps safely.
Treat timestamps WITHOUT timezone as UTC.
Convert to local time before saving to MySQL.
"""
if not iso_str:
return None
try:
dt = parser.isoparse(iso_str)
# If tz is missing → assume UTC
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
# Convert to local timezone
dt_local = dt.astimezone()
return dt_local.strftime("%Y-%m-%d %H:%M:%S")
except:
return None
# ================================ # ================================
# 🔑 TOKEN # 🔑 TOKEN
# ================================ # ================================
def read_token(path: Path) -> str: def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip() tok = path.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "): if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1] return tok.split(" ", 1)[1]
return tok return tok
# ================================ # ================================
# 🕒 DATETIME FORMAT # 💾 UPSERT (včetně správného updatedAt)
# ================================
def to_mysql_dt(iso_str):
if not iso_str:
return None
try:
dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
return dt.strftime("%Y-%m-%d %H:%M:%S")
except:
return None
# ================================
# 💾 UPSERT
# ================================ # ================================
def upsert(conn, r): def upsert(conn, r):
p = r.get("extendedPatient") or {} p = r.get("extendedPatient") or {}
# raw timestamps z API nyní přes nový parser
api_updated = to_mysql_dt_utc(r.get("updatedAt"))
last_msg = r.get("lastMessage") or {}
msg_updated = to_mysql_dt_utc(last_msg.get("createdAt"))
# nejnovější změna
def max_dt(a, b):
if a and b:
return max(a, b)
return a or b
final_updated = max_dt(api_updated, msg_updated)
sql = """ sql = """
INSERT INTO pozadavky ( INSERT INTO pozadavky (
id, displayTitle, createdAt, updatedAt, doneAt, removedAt, id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
@@ -105,10 +138,10 @@ def upsert(conn, r):
vals = ( vals = (
r.get("id"), r.get("id"),
r.get("displayTitle"), r.get("displayTitle"),
to_mysql_dt(r.get("createdAt")), to_mysql_dt_utc(r.get("createdAt")),
to_mysql_dt(r.get("updatedAt")), final_updated,
to_mysql_dt(r.get("doneAt")), to_mysql_dt_utc(r.get("doneAt")),
to_mysql_dt(r.get("removedAt")), to_mysql_dt_utc(r.get("removedAt")),
p.get("name"), p.get("name"),
p.get("surname"), p.get("surname"),
p.get("identificationNumber"), p.get("identificationNumber"),
@@ -133,15 +166,15 @@ def fetch_active(headers, offset):
} }
payload = { payload = {
"operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2", "operationName": "ClinicRequestList2",
"query": GRAPHQL_QUERY, "query": GRAPHQL_QUERY,
"variables": variables, "variables": variables,
} }
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
r.raise_for_status() r.raise_for_status()
data = r.json().get("data", {}).get("requestsResponse", {})
data = r.json().get("data", {}).get("requestsResponse", {})
return data.get("patientRequests", []), data.get("count", 0) return data.get("patientRequests", []), data.get("count", 0)
@@ -160,9 +193,6 @@ def main():
print(f"\n=== Sync ACTIVE požadavků @ {datetime.now():%Y-%m-%d %H:%M:%S} ===") print(f"\n=== Sync ACTIVE požadavků @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
# -------------------------------
# 🚀 FETCH ALL ACTIVE REQUESTS
# -------------------------------
offset = 0 offset = 0
total_processed = 0 total_processed = 0
total_count = None total_count = None
@@ -193,5 +223,6 @@ def main():
print("\n✅ ACTIVE sync hotovo!\n") print("\n✅ ACTIVE sync hotovo!\n")
# ================================
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -2,11 +2,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Read conversation messages for pozadavky where messagesProcessed IS NULL Stáhne konverzaci pro požadavky, kde:
(Optionally filtered by createdAt), insert them into `medevio_conversation`, messagesProcessed IS NULL OR messagesProcessed < updatedAt.
and if a message has an attachment (medicalRecord), download it and save into
`medevio_downloads` (same logic as your attachments script). Vloží do medevio_conversation a přílohy do medevio_downloads.
Finally, mark pozadavky.messagesProcessed = NOW().
""" """
import zlib import zlib
@@ -21,6 +20,7 @@ import time
# 🔧 CONFIGURATION # 🔧 CONFIGURATION
# ============================== # ==============================
TOKEN_PATH = Path("token.txt") TOKEN_PATH = Path("token.txt")
DB_CONFIG = { DB_CONFIG = {
"host": "192.168.1.76", "host": "192.168.1.76",
"port": 3307, "port": 3307,
@@ -31,9 +31,6 @@ DB_CONFIG = {
"cursorclass": pymysql.cursors.DictCursor, "cursorclass": pymysql.cursors.DictCursor,
} }
# ✅ Optional: Only process requests created after this date ("" = no limit)
CREATED_AFTER = "2024-01-01"
GRAPHQL_QUERY_MESSAGES = r""" GRAPHQL_QUERY_MESSAGES = r"""
query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) { query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) { messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) {
@@ -64,75 +61,58 @@ query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
""" """
# ============================== # ==============================
# 🧮 HELPERS # ⏱ DATETIME PARSER
# ============================== # ==============================
def short_crc8(uuid_str: str) -> str:
return f"{zlib.crc32(uuid_str.encode('utf-8')) & 0xffffffff:08x}"
def extract_filename_from_url(url: str) -> str:
try:
return url.split("/")[-1].split("?")[0]
except Exception:
return "unknown_filename"
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
def parse_dt(s): def parse_dt(s):
if not s: if not s:
return None return None
# handle both "YYYY-mm-ddTHH:MM:SS" and "YYYY-mm-dd HH:MM:SS"
s = s.replace("T", " ")
fmts = ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M")
for f in fmts:
try: try:
return datetime.strptime(s[:19], f) return datetime.fromisoformat(s.replace("Z", "+00:00"))
except Exception: except:
pass pass
try:
return datetime.strptime(s[:19], "%Y-%m-%dT%H:%M:%S")
except:
return None return None
# ==============================
# 🔐 TOKEN
# ==============================
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
return tok.replace("Bearer ", "")
# ============================== # ==============================
# 📡 FETCH MESSAGES # 📡 FETCH MESSAGES
# ============================== # ==============================
def fetch_messages(headers, request_id): def fetch_messages(headers, request_id):
variables = {"requestId": request_id, "updatedSince": None}
payload = { payload = {
"operationName": "UseMessages_ListMessages", "operationName": "UseMessages_ListMessages",
"query": GRAPHQL_QUERY_MESSAGES, "query": GRAPHQL_QUERY_MESSAGES,
"variables": variables, "variables": {"requestId": request_id, "updatedSince": None},
} }
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
if r.status_code != 200: if r.status_code != 200:
print(f"❌ HTTP {r.status_code} for messages of request {request_id}") print("❌ HTTP", r.status_code, "for request", request_id)
return [] return []
data = r.json().get("data", {}).get("messages", []) return r.json().get("data", {}).get("messages", []) or []
return data or []
# ============================== # ==============================
# 💾 SAVE: conversation row # 💾 SAVE MESSAGE
# ============================== # ==============================
def insert_message(cur, req_id, msg): def insert_message(cur, req_id, msg):
sender = msg.get("sender") or {} sender = msg.get("sender") or {}
sender_name = " ".join(x for x in [sender.get("name"), sender.get("surname")] if x).strip() or None sender_name = " ".join(
sender_id = sender.get("id") x for x in [sender.get("name"), sender.get("surname")] if x
sender_clinic_id = sender.get("clinicId") ) or None
text = msg.get("text")
created_at = parse_dt(msg.get("createdAt"))
read_at = parse_dt(msg.get("readAt"))
updated_at = parse_dt(msg.get("updatedAt"))
mr = msg.get("medicalRecord") or {}
attachment_url = mr.get("downloadUrl") or mr.get("url")
attachment_description = mr.get("description")
attachment_content_type = mr.get("contentType")
sql = """ sql = """
INSERT INTO medevio_conversation ( INSERT INTO medevio_conversation (
id, request_id, sender_name, sender_id, sender_clinic_id, id, request_id,
sender_name, sender_id, sender_clinic_id,
text, created_at, read_at, updated_at, text, created_at, read_at, updated_at,
attachment_url, attachment_description, attachment_content_type attachment_url, attachment_description, attachment_content_type
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
@@ -148,57 +128,57 @@ def insert_message(cur, req_id, msg):
attachment_description = VALUES(attachment_description), attachment_description = VALUES(attachment_description),
attachment_content_type = VALUES(attachment_content_type) attachment_content_type = VALUES(attachment_content_type)
""" """
mr = msg.get("medicalRecord") or {}
cur.execute(sql, ( cur.execute(sql, (
msg.get("id"), msg.get("id"),
req_id, req_id,
sender_name, sender_name,
sender_id, sender.get("id"),
sender_clinic_id, sender.get("clinicId"),
text, msg.get("text"),
created_at, parse_dt(msg.get("createdAt")),
read_at, parse_dt(msg.get("readAt")),
updated_at, parse_dt(msg.get("updatedAt")),
attachment_url, mr.get("downloadUrl") or mr.get("url"),
attachment_description, mr.get("description"),
attachment_content_type mr.get("contentType")
)) ))
# ============================== # ==============================
# 💾 SAVE: download attachment (from message) # 💾 DOWNLOAD MESSAGE ATTACHMENT
# ============================== # ==============================
def insert_download_from_message(cur, req_id, msg, existing_ids): def insert_download(cur, req_id, msg, existing_ids):
mr = msg.get("medicalRecord") or {} mr = msg.get("medicalRecord") or {}
attachment_id = mr.get("id") attachment_id = mr.get("id")
if not attachment_id: if not attachment_id:
return False return
if attachment_id in existing_ids: if attachment_id in existing_ids:
print(f" ⏭️ Skipping already downloaded message-attachment {attachment_id}") return # skip duplicates
return False
url = mr.get("downloadUrl") or mr.get("url") url = mr.get("downloadUrl") or mr.get("url")
if not url: if not url:
return False return
try: try:
r = requests.get(url, timeout=30) r = requests.get(url, timeout=30)
r.raise_for_status() r.raise_for_status()
content = r.content data = r.content
except Exception as e: except Exception as e:
print(f" ⚠️ Failed to download message attachment {attachment_id}: {e}") print("⚠️ Failed to download:", e)
return False return
filename = extract_filename_from_url(url) filename = url.split("/")[-1].split("?")[0]
content_type = mr.get("contentType")
file_size = len(content)
created_date = parse_dt(msg.get("createdAt"))
# We don't have patient names on the message level here; keep NULLs.
cur.execute(""" cur.execute("""
INSERT INTO medevio_downloads ( INSERT INTO medevio_downloads (
request_id, attachment_id, attachment_type, filename, request_id, attachment_id, attachment_type,
content_type, file_size, pacient_jmeno, pacient_prijmeni, filename, content_type, file_size, created_at, file_content
created_at, file_content ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
file_content = VALUES(file_content), file_content = VALUES(file_content),
file_size = VALUES(file_size), file_size = VALUES(file_size),
@@ -208,21 +188,20 @@ def insert_download_from_message(cur, req_id, msg, existing_ids):
attachment_id, attachment_id,
"MESSAGE_ATTACHMENT", "MESSAGE_ATTACHMENT",
filename, filename,
content_type, mr.get("contentType"),
file_size, len(data),
None, parse_dt(msg.get("createdAt")),
None, data
created_date,
content
)) ))
existing_ids.add(attachment_id) existing_ids.add(attachment_id)
print(f" 💾 Saved msg attachment {filename} ({file_size/1024:.1f} kB)")
return True
# ============================== # ==============================
# 🧠 MAIN # 🧠 MAIN
# ============================== # ==============================
def main(): def main():
token = read_token(TOKEN_PATH) token = read_token(TOKEN_PATH)
headers = { headers = {
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
@@ -232,65 +211,49 @@ def main():
conn = pymysql.connect(**DB_CONFIG) conn = pymysql.connect(**DB_CONFIG)
# Load existing download IDs to skip duplicates (same logic as your script) # ---- Load existing attachments
print("📦 Loading list of already downloaded attachments...")
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute("SELECT attachment_id FROM medevio_downloads") cur.execute("SELECT attachment_id FROM medevio_downloads")
existing_ids = {row["attachment_id"] for row in cur.fetchall()} existing_ids = {row["attachment_id"] for row in cur.fetchall()}
print(f"✅ Found {len(existing_ids)} attachments already saved.")
# Pull pozadavky where messagesProcessed IS NULL (optionally by createdAt) print(f"📦 Already downloaded attachments: {len(existing_ids)}\n")
# ---- Select pozadavky needing message sync
sql = """ sql = """
SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno, createdAt SELECT id
FROM pozadavky FROM pozadavky
WHERE messagesProcessed IS NULL WHERE messagesProcessed IS NULL
OR messagesProcessed < updatedAt
""" """
params = []
if CREATED_AFTER:
sql += " AND createdAt >= %s"
params.append(CREATED_AFTER)
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute(sql, params) cur.execute(sql)
rows = cur.fetchall() requests_to_process = cur.fetchall()
print(f"📋 Found {len(rows)} pozadavky to process (messagesProcessed IS NULL" print(f"📋 Found {len(requests_to_process)} pozadavků requiring message sync.\n")
+ (f", created >= {CREATED_AFTER}" if CREATED_AFTER else "") + ")")
for i, row in enumerate(rows, 1): # ---- Process each pozadavek
for idx, row in enumerate(requests_to_process, 1):
req_id = row["id"] req_id = row["id"]
prijmeni = row.get("pacient_prijmeni") or "Neznamy" print(f"[{idx}/{len(requests_to_process)}] Processing {req_id}")
jmeno = row.get("pacient_jmeno") or ""
print(f"\n[{i}/{len(rows)}] 💬 {prijmeni}, {jmeno} ({req_id})")
messages = fetch_messages(headers, req_id) messages = fetch_messages(headers, req_id)
if not messages:
print(" ⚠️ No messages found")
with conn.cursor() as cur:
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
continue
inserted = 0
with conn.cursor() as cur: with conn.cursor() as cur:
for msg in messages: for msg in messages:
insert_message(cur, req_id, msg) insert_message(cur, req_id, msg)
# also pull any message attachments into downloads table insert_download(cur, req_id, msg, existing_ids)
insert_download_from_message(cur, req_id, msg, existing_ids)
inserted += 1
conn.commit() conn.commit()
# mark processed
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,)) cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit() conn.commit()
print(f"{inserted} messages processed for {prijmeni}, {jmeno}") print(f"{len(messages)} messages saved\n")
time.sleep(0.3) # polite API delay time.sleep(0.25)
conn.close() conn.close()
print("\n✅ Done! All new conversations processed and pozadavky updated.") print("🎉 Done!")
# ==============================
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -0,0 +1,226 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import openpyxl
from openpyxl.styles import PatternFill, Font
from openpyxl.utils import get_column_letter
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
OUTPUT_PATH = fr"U:\Dropbox\!!!Days\Downloads Z230\{timestamp} medevio_patients_report.xlsx"
# ============================
# CONFIGURATION
# ============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
# ============================
# FUNCTIONS
# ============================
from openpyxl.styles import Border, Side
thin_border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
def apply_thin_borders(ws):
"""Apply thin borders to all cells in the worksheet."""
for row in ws.iter_rows():
for cell in row:
cell.border = thin_border
def autofit_columns(ws):
"""Auto-adjust column widths based on longest cell content."""
for col in ws.columns:
max_length = 0
col_letter = get_column_letter(col[0].column)
for cell in col:
try:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
except:
pass
ws.column_dimensions[col_letter].width = max_length + 2
def apply_header_style(ws):
"""Make header BRIGHT YELLOW and bold."""
fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
font = Font(bold=True)
for cell in ws[1]:
cell.fill = fill
cell.font = font
def create_compact_row(row):
"""Produce compact record with merged pojistovna, with user_relationship after prijmeni."""
# insurance merged
code = row.get("pojistovna_code") or ""
naz = row.get("pojistovna_nazev") or ""
if code and naz:
poj = f"{code} ({naz})"
elif code:
poj = code
elif naz:
poj = naz
else:
poj = ""
return {
"id": row["id"],
"jmeno": row["jmeno"],
"prijmeni": row["prijmeni"],
# 🔹 inserted here
"user_relationship": row.get("user_relationship"),
"rodne_cislo": row["rodne_cislo"],
"dob": row["dob"],
"telefon": row["telefon"],
"email": row["email"],
"pojistovna": poj,
"status": row["status"],
"has_mobile_app": row["has_mobile_app"],
"registration_time": row["registration_time"],
"last_update": row["last_update"],
}
def create_pozadavky_rows(rows):
"""Convert raw pozadavky SQL rows into rows for the Excel sheet."""
output = []
for r in rows:
output.append({
# 🔹 First the ID
"id": r["id"],
# 🔹 Your 3 patient columns immediately after ID
"pacient_jmeno": r["pacient_jmeno"],
"pacient_prijmeni": r["pacient_prijmeni"],
"pacient_rodnecislo": r["pacient_rodnecislo"],
# 🔹 Then all other fields in any order you prefer
"displayTitle": r["displayTitle"],
"createdAt": r["createdAt"],
"updatedAt": r["updatedAt"],
"doneAt": r["doneAt"],
"removedAt": r["removedAt"],
"attachmentsProcessed": r["attachmentsProcessed"],
"messagesProcessed": r["messagesProcessed"],
"communicationprocessed": r["communicationprocessed"],
"questionnaireprocessed": r["questionnaireprocessed"],
"lastSync": r["lastSync"],
})
return output
# ============================
# MAIN
# ============================
def main():
print("📥 Connecting to MySQL...")
conn = pymysql.connect(**DB_CONFIG)
with conn:
with conn.cursor() as cur:
cur.execute("SELECT * FROM medevio_pacienti ORDER BY prijmeni, jmeno")
patients = cur.fetchall()
print(f"📊 Loaded {len(patients)} patients.")
# Load pozadavky
with conn.cursor() as cur:
cur.execute("SELECT * FROM pozadavky ORDER BY createdAt DESC")
pozadavky_rows = cur.fetchall()
print(f"📄 Loaded {len(pozadavky_rows)} pozadavky.")
wb = openpyxl.Workbook()
# ---------------------------------
# 1) FULL SHEET
# ---------------------------------
ws_full = wb.active
ws_full.title = "Patients FULL"
if patients:
headers = list(patients[0].keys())
ws_full.append(headers)
for row in patients:
ws_full.append([row.get(h) for h in headers])
apply_header_style(ws_full)
ws_full.freeze_panes = "A2"
ws_full.auto_filter.ref = ws_full.dimensions
autofit_columns(ws_full)
apply_thin_borders(ws_full)
# ---------------------------------
# 2) COMPACT SHEET
# ---------------------------------
ws_compact = wb.create_sheet("Patients COMPACT")
compact_rows = [create_compact_row(r) for r in patients]
compact_headers = list(compact_rows[0].keys())
ws_compact.append(compact_headers)
for row in compact_rows:
ws_compact.append([row.get(h) for h in compact_headers])
apply_header_style(ws_compact)
ws_compact.freeze_panes = "A2"
ws_compact.auto_filter.ref = ws_compact.dimensions
autofit_columns(ws_compact)
# >>> ADD THIS <<<
ur_col_index = compact_headers.index("user_relationship") + 1
col_letter = get_column_letter(ur_col_index)
ws_compact.column_dimensions[col_letter].width = 7.14
apply_thin_borders(ws_compact)
# ---------------------------------
# 3) POZADAVKY SHEET
# ---------------------------------
ws_p = wb.create_sheet("Pozadavky")
poz_list = create_pozadavky_rows(pozadavky_rows)
headers_p = list(poz_list[0].keys()) if poz_list else []
if headers_p:
ws_p.append(headers_p)
for row in poz_list:
ws_p.append([row.get(h) for h in headers_p])
apply_header_style(ws_p)
ws_p.freeze_panes = "A2"
ws_p.auto_filter.ref = ws_p.dimensions
autofit_columns(ws_p)
apply_thin_borders(ws_p)
# ---------------------------------
# SAVE
# ---------------------------------
wb.save(OUTPUT_PATH)
print(f"✅ Excel report saved to:\n{OUTPUT_PATH}")
if __name__ == "__main__":
main()

227
Testy/14 Testy updateat.py Normal file
View File

@@ -0,0 +1,227 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import requests
from pathlib import Path
import json
TOKEN_PATH = Path("token.txt")
CLINIC_SLUG = "mudr-buzalkova"
BATCH_SIZE = 100
# přesně tvůj původní dotaz, beze změn
# GRAPHQL_QUERY = r"""
# query ClinicRequestGrid_ListPatientRequestsForClinic2(
# $clinicSlug: String!,
# $queueId: String,
# $queueAssignment: QueueAssignmentFilter!,
# $pageInfo: PageInfo!,
# $locale: Locale!,
# $state: PatientRequestState
# ) {
# requestsResponse: listPatientRequestsForClinic2(
# clinicSlug: $clinicSlug,
# queueId: $queueId,
# queueAssignment: $queueAssignment,
# pageInfo: $pageInfo,
# state: $state
# ) {
# count
# patientRequests {
# id
# displayTitle(locale: $locale)
# createdAt
# updatedAt
# doneAt
# removedAt
# extendedPatient {
# name
# surname
# identificationNumber
# }
# }
# }
# }
# """
GRAPHQL_QUERY = r"""
query ClinicRequestGrid_ListPatientRequestsForClinic2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$state: PatientRequestState,
$pageInfo: PageInfo!,
$locale: Locale!
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug
queueId: $queueId
queueAssignment: $queueAssignment
state: $state
pageInfo: $pageInfo
) {
count
patientRequests {
id
displayTitle(locale: $locale)
### TIME FIELDS ADDED
createdAt
updatedAt
doneAt
removedAt
extendedPatient {
id
identificationNumber
name
surname
kind
key
type
user {
id
name
surname
}
owner {
name
surname
}
dob
premiumPlanPatient {
id
premiumPlan {
id
}
}
status2
tags(onlyImportant: true) {
id
}
isUnknownPatient
}
invoice {
id
status
amount
currency
dueAmount
isOverdue
refundedAmount
settledAmount
}
lastMessage {
createdAt
id
readAt
sender {
id
name
surname
clinicId
}
text
}
priority
queue {
id
name
clinicPatientRequestQueueUsers {
accountable {
id
name
surname
}
id
}
}
reservations {
calendar {
id
internalName
name
}
id
canceledAt
done
start
}
tags(onlyImportant: true) {
id
}
userECRF(locale: $locale) {
id
sid
icon {
color
id
urlSvg
}
ecrfSet {
id
name
}
}
priceWhenCreated
currencyWhenCreated
createdByDoctor
eventType
clinicNotes {
id
}
clinicMedicalRecord
}
}
}
"""
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"pageInfo": {"first": BATCH_SIZE, "offset": 0},
"locale": "cs",
"state": "ACTIVE",
}
payload = {
"operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
print("\n===== ČISTÁ ODPOVĚĎ SERVERU =====\n")
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
print(f"HTTP {r.status_code}\n")
print(r.text) # <-- TISK NEUPRAVENÉHO JSONU
print("\n===== KONEC ČISTÉ ODPOVĚDI =====\n")
if __name__ == "__main__":
main()

136
Testy/15 test.py Normal file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import requests
from pathlib import Path
TOKEN_PATH = Path("token.txt")
CLINIC_SLUG = "mudr-buzalkova"
BATCH_SIZE = 100
TARGET_ID = "cbf6000d-a6ca-4059-88b7-dfdc27220762" # ← sem tvoje ID
# ⭐ Updated GraphQL with lastMessage included
GRAPHQL_QUERY = r"""
query ClinicRequestGrid_ListPatientRequestsForClinic2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$pageInfo: PageInfo!,
$locale: Locale!,
$state: PatientRequestState
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
pageInfo: $pageInfo,
state: $state
) {
count
patientRequests {
id
displayTitle(locale: $locale)
createdAt
updatedAt
doneAt
removedAt
lastMessage {
id
createdAt
updatedAt
}
extendedPatient {
name
surname
identificationNumber
}
}
}
}
"""
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
def fetch_active(headers, offset):
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
"locale": "cs",
"state": "ACTIVE",
}
payload = {
"operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
if r.status_code != 200:
print("HTTP status:", r.status_code)
print(r.text)
r.raise_for_status()
data = r.json().get("data", {}).get("requestsResponse", {})
return data.get("patientRequests", []), data.get("count", 0)
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
print(f"=== Hledám updatedAt a lastMessage pro pozadavek {TARGET_ID} ===\n")
offset = 0
total_count = None
found = False
while True:
batch, count = fetch_active(headers, offset)
if total_count is None:
total_count = count
if not batch:
break
for r in batch:
if r["id"] == TARGET_ID:
print("Nalezeno!\n")
print(f"id: {r['id']}")
print(f"updatedAt: {r['updatedAt']}")
lm = r.get("lastMessage") or {}
print(f"lastMessage.createdAt: {lm.get('createdAt')}")
print(f"lastMessage.updatedAt: {lm.get('updatedAt')}")
found = True
break
if found:
break
if offset + BATCH_SIZE >= count:
break
offset += BATCH_SIZE
if not found:
print("❌ Požadavek nebyl nalezen mezi ACTIVE.")
print("\n=== HOTOVO ===")
if __name__ == "__main__":
main()

228
Testy/16 test.py Normal file
View File

@@ -0,0 +1,228 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import requests
from pathlib import Path
from datetime import datetime, timezone
import time
from dateutil import parser
# ================================
# 🔧 CONFIGURATION
# ================================
TOKEN_PATH = Path("token.txt")
CLINIC_SLUG = "mudr-buzalkova"
BATCH_SIZE = 100
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
# ⭐ NOVÝ TESTOVANÝ DOTAZ obsahuje lastMessage.createdAt
GRAPHQL_QUERY = r"""
query ClinicRequestList2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$state: PatientRequestState,
$pageInfo: PageInfo!,
$locale: Locale!
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
state: $state,
pageInfo: $pageInfo
) {
count
patientRequests {
id
displayTitle(locale: $locale)
createdAt
updatedAt
doneAt
removedAt
extendedPatient {
name
surname
identificationNumber
}
lastMessage {
createdAt
}
}
}
}
"""
# ================================
# 🧿 SAFE DATETIME PARSER (ALWAYS UTC → LOCAL)
# ================================
def to_mysql_dt_utc(iso_str):
"""
Parse Medevio timestamps safely.
Treat timestamps WITHOUT timezone as UTC.
Convert to local time before saving to MySQL.
"""
if not iso_str:
return None
try:
dt = parser.isoparse(iso_str)
# If tz is missing → assume UTC
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
# Convert to local timezone
dt_local = dt.astimezone()
return dt_local.strftime("%Y-%m-%d %H:%M:%S")
except:
return None
# ================================
# 🔑 TOKEN
# ================================
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
return tok.split(" ", 1)[1]
return tok
# ================================
# 💾 UPSERT (včetně správného updatedAt)
# ================================
def upsert(conn, r):
p = r.get("extendedPatient") or {}
# raw timestamps z API nyní přes nový parser
api_updated = to_mysql_dt_utc(r.get("updatedAt"))
last_msg = r.get("lastMessage") or {}
msg_updated = to_mysql_dt_utc(last_msg.get("createdAt"))
# nejnovější změna
def max_dt(a, b):
if a and b:
return max(a, b)
return a or b
final_updated = max_dt(api_updated, msg_updated)
sql = """
INSERT INTO pozadavky (
id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
pacient_jmeno, pacient_prijmeni, pacient_rodnecislo
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
displayTitle=VALUES(displayTitle),
updatedAt=VALUES(updatedAt),
doneAt=VALUES(doneAt),
removedAt=VALUES(removedAt),
pacient_jmeno=VALUES(pacient_jmeno),
pacient_prijmeni=VALUES(pacient_prijmeni),
pacient_rodnecislo=VALUES(pacient_rodnecislo)
"""
vals = (
r.get("id"),
r.get("displayTitle"),
to_mysql_dt_utc(r.get("createdAt")),
final_updated,
to_mysql_dt_utc(r.get("doneAt")),
to_mysql_dt_utc(r.get("removedAt")),
p.get("name"),
p.get("surname"),
p.get("identificationNumber"),
)
with conn.cursor() as cur:
cur.execute(sql, vals)
conn.commit()
# ================================
# 📡 FETCH ACTIVE PAGE
# ================================
def fetch_active(headers, offset):
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
"locale": "cs",
"state": "ACTIVE",
}
payload = {
"operationName": "ClinicRequestList2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
r.raise_for_status()
data = r.json().get("data", {}).get("requestsResponse", {})
return data.get("patientRequests", []), data.get("count", 0)
# ================================
# 🧠 MAIN
# ================================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
print(f"\n=== Sync ACTIVE požadavků @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
offset = 0
total_processed = 0
total_count = None
while True:
batch, count = fetch_active(headers, offset)
if total_count is None:
total_count = count
print(f"📡 Celkem ACTIVE v Medevio: {count}")
if not batch:
break
for r in batch:
upsert(conn, r)
total_processed += len(batch)
print(f"{total_processed}/{total_count} ACTIVE processed")
if offset + BATCH_SIZE >= count:
break
offset += BATCH_SIZE
time.sleep(0.4)
conn.close()
print("\n✅ ACTIVE sync hotovo!\n")
# ================================
if __name__ == "__main__":
main()

259
Testy/17 test.py Normal file
View File

@@ -0,0 +1,259 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Stáhne konverzaci pro požadavky, kde:
messagesProcessed IS NULL OR messagesProcessed < updatedAt.
Vloží do medevio_conversation a přílohy do medevio_downloads.
"""
import zlib
import json
import requests
import pymysql
from pathlib import Path
from datetime import datetime
import time
# ==============================
# 🔧 CONFIGURATION
# ==============================
TOKEN_PATH = Path("token.txt")
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY_MESSAGES = r"""
query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) {
id
createdAt
updatedAt
readAt
text
type
sender {
id
name
surname
clinicId
}
medicalRecord {
id
description
contentType
url
downloadUrl
token
createdAt
updatedAt
}
}
}
"""
# ==============================
# ⏱ DATETIME PARSER
# ==============================
def parse_dt(s):
if not s:
return None
try:
return datetime.fromisoformat(s.replace("Z", "+00:00"))
except:
pass
try:
return datetime.strptime(s[:19], "%Y-%m-%dT%H:%M:%S")
except:
return None
# ==============================
# 🔐 TOKEN
# ==============================
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
return tok.replace("Bearer ", "")
# ==============================
# 📡 FETCH MESSAGES
# ==============================
def fetch_messages(headers, request_id):
payload = {
"operationName": "UseMessages_ListMessages",
"query": GRAPHQL_QUERY_MESSAGES,
"variables": {"requestId": request_id, "updatedSince": None},
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
if r.status_code != 200:
print("❌ HTTP", r.status_code, "for request", request_id)
return []
return r.json().get("data", {}).get("messages", []) or []
# ==============================
# 💾 SAVE MESSAGE
# ==============================
def insert_message(cur, req_id, msg):
sender = msg.get("sender") or {}
sender_name = " ".join(
x for x in [sender.get("name"), sender.get("surname")] if x
) or None
sql = """
INSERT INTO medevio_conversation (
id, request_id,
sender_name, sender_id, sender_clinic_id,
text, created_at, read_at, updated_at,
attachment_url, attachment_description, attachment_content_type
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
sender_name = VALUES(sender_name),
sender_id = VALUES(sender_id),
sender_clinic_id = VALUES(sender_clinic_id),
text = VALUES(text),
created_at = VALUES(created_at),
read_at = VALUES(read_at),
updated_at = VALUES(updated_at),
attachment_url = VALUES(attachment_url),
attachment_description = VALUES(attachment_description),
attachment_content_type = VALUES(attachment_content_type)
"""
mr = msg.get("medicalRecord") or {}
cur.execute(sql, (
msg.get("id"),
req_id,
sender_name,
sender.get("id"),
sender.get("clinicId"),
msg.get("text"),
parse_dt(msg.get("createdAt")),
parse_dt(msg.get("readAt")),
parse_dt(msg.get("updatedAt")),
mr.get("downloadUrl") or mr.get("url"),
mr.get("description"),
mr.get("contentType")
))
# ==============================
# 💾 DOWNLOAD MESSAGE ATTACHMENT
# ==============================
def insert_download(cur, req_id, msg, existing_ids):
mr = msg.get("medicalRecord") or {}
attachment_id = mr.get("id")
if not attachment_id:
return
if attachment_id in existing_ids:
return # skip duplicates
url = mr.get("downloadUrl") or mr.get("url")
if not url:
return
try:
r = requests.get(url, timeout=30)
r.raise_for_status()
data = r.content
except Exception as e:
print("⚠️ Failed to download:", e)
return
filename = url.split("/")[-1].split("?")[0]
cur.execute("""
INSERT INTO medevio_downloads (
request_id, attachment_id, attachment_type,
filename, content_type, file_size, created_at, file_content
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
file_content = VALUES(file_content),
file_size = VALUES(file_size),
downloaded_at = NOW()
""", (
req_id,
attachment_id,
"MESSAGE_ATTACHMENT",
filename,
mr.get("contentType"),
len(data),
parse_dt(msg.get("createdAt")),
data
))
existing_ids.add(attachment_id)
# ==============================
# 🧠 MAIN
# ==============================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
# ---- Load existing attachments
with conn.cursor() as cur:
cur.execute("SELECT attachment_id FROM medevio_downloads")
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
print(f"📦 Already downloaded attachments: {len(existing_ids)}\n")
# ---- Select pozadavky needing message sync
sql = """
SELECT id
FROM pozadavky
WHERE messagesProcessed IS NULL
OR messagesProcessed < updatedAt
"""
with conn.cursor() as cur:
cur.execute(sql)
requests_to_process = cur.fetchall()
print(f"📋 Found {len(requests_to_process)} pozadavků requiring message sync.\n")
# ---- Process each pozadavek
for idx, row in enumerate(requests_to_process, 1):
req_id = row["id"]
print(f"[{idx}/{len(requests_to_process)}] Processing {req_id}")
messages = fetch_messages(headers, req_id)
with conn.cursor() as cur:
for msg in messages:
insert_message(cur, req_id, msg)
insert_download(cur, req_id, msg, existing_ids)
conn.commit()
with conn.cursor() as cur:
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
print(f"{len(messages)} messages saved\n")
time.sleep(0.25)
conn.close()
print("🎉 Done!")
if __name__ == "__main__":
main()