"""Render patient and appointment density gates in the real desktop shell.""" from __future__ import annotations import os from datetime import date from pathlib import Path from typing import Any os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") from PySide6.QtCore import QThreadPool from PySide6.QtGui import QFont, QFontDatabase from PySide6.QtWidgets import QApplication from doctor_workstation.services import DemoDoctorRepository from doctor_workstation.ui import ShellWindow, apply_theme def _drain(application: QApplication) -> None: QThreadPool.globalInstance().waitForDone(5_000) for _index in range(8): application.processEvents() def _patient_rows() -> list[dict[str, Any]]: names = ("林晓岚", "赵明远", "吴诗雨", "周安然", "程静", "许清和") rows: list[dict[str, Any]] = [] for index in range(15): rows.append( { "id": 101 + index, "diagnosis_id": 501 + index, "patient_id": 301 + index, "patient_name": names[index % len(names)], "gender": 2 if index % 2 == 0 else 1, "age": 34 + index, "phone_masked": f"138****{1200 + index:04d}", "assistant_id": 8, "assistant_name": "周医助", "appointment_id": 701 + index, "appointment_status": 1, "appointment_doctor_name": "陈医生", "appointment_time_text": f"2026-08-{17 + index % 3:02d} {9 + index % 7:02d}:00", "revisit_count": index % 4, "confirmation_text": "已确认" if index % 2 == 0 else "待确认", "diagnosis_date_text": "第 2 次复诊" if index % 3 else "初诊", "has_id_card": index % 4 != 0, } ) return rows def _appointment_rows() -> list[dict[str, Any]]: names = ("林晓岚", "赵明远", "吴诗雨", "周安然", "程静", "许清和") today = date.today().isoformat() rows: list[dict[str, Any]] = [] for index in range(8): rows.append( { "id": 801 + index, "diagnosis_id": 901 + index, "patient_id": 401 + index, "source_patient_id": 401 + index, "patient_name": names[index % len(names)], "patient_phone": f"1380013{8000 + index}", "gender": 2 if index % 2 == 0 else 1, "age": 38 + index, "doctor_name": "陈医生", "assistant_id": 8, "assistant_name": "周医助", "appointment_date": today, "appointment_time": f"{9 + index:02d}:00", "channel_name": "线上复诊", "diagnosis_confirmed": index % 2 == 0, "has_prescription": index % 3 == 0, "status": 1, "status_desc": "已挂号", "revisit_time": "复诊" if index % 2 else "初诊", "unserved_days": index, "video_call_hint": ( {"state": "live", "label": "视频通话进行中"} if index == 0 else {"state": "idle", "label": "等待医生发起"} ), } ) return rows def render() -> list[Path]: application = QApplication.instance() or QApplication([]) apply_theme(application) font_path = Path("C:/Windows/Fonts/msyh.ttc") if font_path.is_file(): font_id = QFontDatabase.addApplicationFont(str(font_path)) families = QFontDatabase.applicationFontFamilies(font_id) if families: application.setFont(QFont(families[0], 9)) output = Path(__file__).resolve().parents[1] / "artifacts" / "patient_appointment_density" output.mkdir(parents=True, exist_ok=True) paths: list[Path] = [] for width, height in ((1366, 768), (1024, 640)): repository = DemoDoctorRepository() session = repository.login(repository.DEMO_ACCOUNT, repository.DEMO_PASSWORD) shell = ShellWindow( repository, {"session": session, "demo_mode": True}, permissions=session.permissions, ) shell.resize(width, height) shell.show() _drain(application) if not shell.navigate("patients"): raise RuntimeError("patients navigation is unavailable") _drain(application) patients = shell.pages["patients"] patient_rows = _patient_rows() patients.patient_workspace._apply_result( { "lists": patient_rows, "count": len(patient_rows), "extend": { "scope": {"label": "当前医生与部门"}, "summary": {"today": 6, "tomorrow": 5, "day_after": 4}, }, }, patients.patient_workspace._generation, ) application.processEvents() patient_slots = patients.patient_workspace.table.viewport().height() // 40 if width == 1366 and patient_slots < 6: raise RuntimeError(f"patient table only exposes {patient_slots} ordinary rows") patient_path = output / f"patients_{width}x{height}.png" if not shell.grab().save(str(patient_path), "PNG"): raise RuntimeError(f"failed to save {patient_path}") paths.append(patient_path) if not shell.navigate("appointments"): raise RuntimeError("appointments navigation is unavailable") _drain(application) appointments = shell.pages["appointments"] appointments.poll_timer.stop() appointment_rows = _appointment_rows() appointments._loaded( { "lists": appointment_rows, "count": len(appointment_rows), "extend": { "status_count": {"1": len(appointment_rows), "3": 0}, "unassigned_count": 0, }, }, appointments._generation, False, ) application.processEvents() row_heights = [ appointments.table.rowHeight(index) for index in range(appointments.table.rowCount()) ] appointment_slots = appointments.table.viewport().height() // max(row_heights) if width == 1366 and appointment_slots < 4: raise RuntimeError( f"appointment table only exposes {appointment_slots} ordinary rows" ) if appointments.content_layout.count() != 1: raise RuntimeError("appointment list still reserves a secondary side panel") if appointments.table_card.width() != appointments.content_host.width(): raise RuntimeError("appointment table does not fill the content viewport") appointment_path = output / f"appointments_{width}x{height}.png" if not shell.grab().save(str(appointment_path), "PNG"): raise RuntimeError(f"failed to save {appointment_path}") paths.append(appointment_path) print( f"{width}x{height}: patient_slots={patient_slots}, " f"appointment_slots={appointment_slots}, table_full_width=True" ) shell.close() application.processEvents() return paths if __name__ == "__main__": for rendered in render(): print(rendered)