3451 lines
141 KiB
Python
3451 lines
141 KiB
Python
"""Doctor-scoped diagnosis list matching the canonical admin workflow."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from collections.abc import Mapping, Sequence
|
||
from contextlib import suppress
|
||
from copy import deepcopy
|
||
from html import escape
|
||
from types import MappingProxyType
|
||
from typing import Any
|
||
|
||
from PySide6.QtCore import QDate, Qt, QTimer, QUrl, Signal
|
||
from PySide6.QtGui import QDesktopServices, QPixmap
|
||
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
|
||
from PySide6.QtWidgets import (
|
||
QComboBox,
|
||
QDateEdit,
|
||
QDialog,
|
||
QDialogButtonBox,
|
||
QDoubleSpinBox,
|
||
QFormLayout,
|
||
QFrame,
|
||
QHBoxLayout,
|
||
QHeaderView,
|
||
QInputDialog,
|
||
QLabel,
|
||
QLineEdit,
|
||
QMessageBox,
|
||
QPushButton,
|
||
QScrollArea,
|
||
QSizePolicy,
|
||
QSpinBox,
|
||
QTableWidget,
|
||
QTableWidgetItem,
|
||
QToolButton,
|
||
QVBoxLayout,
|
||
QWidget,
|
||
)
|
||
|
||
from ..diagnosis_index_widgets import (
|
||
DIAGNOSIS_INDEX_QSS,
|
||
DiagnosisChip,
|
||
DiagnosisLoadingOverlay,
|
||
DiagnosisPager,
|
||
DiagnosisTableHost,
|
||
FlowWidget,
|
||
prescription_action,
|
||
video_call_is_live,
|
||
)
|
||
from ..dialogs import DiagnosisDialog
|
||
from ..dialogs.ai_consult import can_open_ai_consult, present_ai_consult
|
||
from ..dialogs.prescription import (
|
||
PrescriptionDetailDialog,
|
||
PrescriptionEditorDialog,
|
||
build_prescription_clinical_diagnosis,
|
||
build_prescription_visit_no,
|
||
)
|
||
from ..theme import mark_business_dialog
|
||
from ..widgets import (
|
||
MessageBanner,
|
||
PageHeader,
|
||
display_text,
|
||
first_value,
|
||
friendly_error,
|
||
gender_text,
|
||
get_value,
|
||
invoke,
|
||
page_items,
|
||
page_total,
|
||
run_async,
|
||
show_toast,
|
||
)
|
||
|
||
_PAGE_HEADER_HEIGHT = 62
|
||
_STATUS_CARD_HEIGHT = 50
|
||
_FILTERS_COLLAPSED_HEIGHT = 90
|
||
|
||
CONSULTATIONS_REFERENCE_QSS = """
|
||
#DiagnosisIndex QWidget#PageHeader { min-height: 62px; max-height: 62px; }
|
||
#DiagnosisIndex QLabel[role="pageTitle"] {
|
||
color: #15224A; font-size: 22px; font-weight: 700;
|
||
}
|
||
#DiagnosisIndex QLabel[role="muted"] { color: #7481A3; font-size: 12px; }
|
||
#DiagnosisIndex QLabel[role="breadcrumb"],
|
||
#DiagnosisIndex QLabel[role="breadcrumbCurrent"] { font-size: 12px; }
|
||
#DiagnosisIndex QFrame#DiagnosisStatusCard,
|
||
#DiagnosisIndex QFrame#DiagnosisFilterCard,
|
||
#DiagnosisIndex QFrame#DiagnosisListCard {
|
||
background: #FFFFFF; border: 1px solid #E2E7F4; border-radius: 13px;
|
||
}
|
||
#DiagnosisIndex QFrame#DiagnosisStatusCard {
|
||
min-height: 50px; max-height: 50px;
|
||
}
|
||
#DiagnosisIndex QFrame#DiagnosisStatusCard QToolButton[diagnosisChip="true"] {
|
||
min-height: 34px; max-height: 34px; min-width: 56px;
|
||
padding: 0 8px; border: 0; border-radius: 8px;
|
||
background: transparent; color: #25345E; font-size: 13px; font-weight: 600;
|
||
}
|
||
#DiagnosisIndex QFrame#DiagnosisStatusCard QToolButton[diagnosisChip="true"]:hover {
|
||
background: #F7F8FF; color: #5265F6;
|
||
}
|
||
#DiagnosisIndex QFrame#DiagnosisStatusCard QToolButton[diagnosisChip="true"]:checked {
|
||
background: #F0F1FF; color: #5265F6;
|
||
border-bottom: 2px solid #6573F7;
|
||
}
|
||
#DiagnosisIndex QWidget#DiagnosisStatusSearch QLineEdit {
|
||
min-width: 78px; max-width: 138px;
|
||
}
|
||
#DiagnosisIndex QDateEdit#DiagnosisCustomDate {
|
||
min-width: 116px; max-width: 116px;
|
||
}
|
||
#DiagnosisIndex QFrame#DiagnosisFilterCard {
|
||
min-height: 88px;
|
||
}
|
||
#DiagnosisIndex QWidget#DiagnosisDateFilters,
|
||
#DiagnosisIndex QWidget#DiagnosisSecondaryFilters {
|
||
background: transparent; border: 0;
|
||
}
|
||
#DiagnosisIndex QWidget#DiagnosisSecondaryFilters QLineEdit#DiagnosisKeyword {
|
||
min-width: 260px; max-width: 380px;
|
||
}
|
||
#DiagnosisIndex QComboBox#DiagnosisConfirmationFilter { min-width: 136px; max-width: 136px; }
|
||
#DiagnosisIndex QComboBox#DiagnosisDepartmentFilter { min-width: 210px; max-width: 230px; }
|
||
#DiagnosisIndex QToolButton[diagnosisChip="true"] {
|
||
min-height: 20px; padding: 6px 12px; border-radius: 8px;
|
||
background: #F7F9FE; color: #405074; font-size: 12px;
|
||
}
|
||
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked {
|
||
background: #EEF1FF; color: #5265F6; border-color: #C9D0FF;
|
||
}
|
||
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked[semantic="primary"],
|
||
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked[semantic="info"] {
|
||
background: #EEF1FF; color: #5265F6; border-color: #C9D0FF;
|
||
}
|
||
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked[semantic="success"] {
|
||
background: #EAF9F4; color: #159C79; border-color: #BFE9DC;
|
||
}
|
||
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked[semantic="warning"] {
|
||
background: #FFF5E4; color: #C17A16; border-color: #F2D49D;
|
||
}
|
||
#DiagnosisIndex QToolButton[diagnosisChip="true"][small="true"] {
|
||
min-height: 18px; padding: 4px 10px; font-size: 12px;
|
||
}
|
||
#DiagnosisIndex QLineEdit,
|
||
#DiagnosisIndex QComboBox,
|
||
#DiagnosisIndex QDateEdit,
|
||
#DiagnosisIndex QSpinBox {
|
||
min-height: 32px; max-height: 32px; border-radius: 8px;
|
||
background: #FFFFFF; border-color: #E2E7F4; color: #15224A;
|
||
font-size: 12px;
|
||
}
|
||
#DiagnosisIndex QPushButton {
|
||
min-height: 34px; max-height: 34px; padding: 0 14px;
|
||
border-radius: 8px; font-size: 12px;
|
||
}
|
||
#DiagnosisIndex QFrame#DiagnosisListToolbar {
|
||
min-height: 44px; max-height: 44px; background: #FFFFFF;
|
||
border-bottom: 1px solid #E7EBF5;
|
||
}
|
||
#DiagnosisIndex QFrame#DiagnosisListToolbar QPushButton {
|
||
padding: 0 13px;
|
||
}
|
||
#DiagnosisIndex QFrame#DiagnosisListToolbar QPushButton[consultationTool="true"] {
|
||
color: #5265F6; background: #F7F8FF; border: 1px solid #E0E5F8;
|
||
}
|
||
#DiagnosisIndex QFrame#DiagnosisListToolbar QPushButton[consultationDanger="true"] {
|
||
color: #F15B67; background: #FFF7F8; border: 1px solid #FFD6DB;
|
||
}
|
||
#DiagnosisIndex QFrame#DiagnosisListCard { border-radius: 12px; }
|
||
#DiagnosisIndex QHeaderView::section {
|
||
min-height: 38px; max-height: 38px; padding: 0 8px;
|
||
background: #F7F9FE; color: #7481A3;
|
||
border-bottom: 1px solid #E7EBF5; font-size: 12px;
|
||
}
|
||
#DiagnosisIndex QTableView { background: #FFFFFF; alternate-background-color: #FBFCFF; }
|
||
#DiagnosisIndex QToolButton[rowLink] { font-size: 11px; padding: 2px; }
|
||
#DiagnosisIndex QWidget#DiagnosisPager { min-height: 42px; max-height: 42px; }
|
||
#DiagnosisIndex QToolButton[pagerButton="true"] {
|
||
min-width: 32px; min-height: 32px; max-height: 32px;
|
||
border: 1px solid #E2E7F4; border-radius: 7px; background: #FFFFFF;
|
||
}
|
||
#DiagnosisIndex QToolButton[pagerButton="true"][active="true"] {
|
||
background: #5265F6; color: #FFFFFF; border-color: #5265F6;
|
||
}
|
||
"""
|
||
|
||
APPOINTMENT_STATUS = {
|
||
1: ("已预约", "warning"),
|
||
3: ("已完成", "success"),
|
||
4: ("已过号", "danger"),
|
||
}
|
||
# Kept as a compatibility name for callers that imported the old module constant.
|
||
CONSULTATION_STATUS = APPOINTMENT_STATUS
|
||
_OPTIONAL_DATE_MINIMUM = QDate(2000, 1, 1)
|
||
_ID_CARD_18_RE = re.compile(
|
||
r"^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$"
|
||
)
|
||
_ID_CARD_15_RE = re.compile(r"^[1-9]\d{5}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}$")
|
||
|
||
|
||
def _as_int(value: Any, default: int = 0) -> int:
|
||
try:
|
||
return int(value)
|
||
except (TypeError, ValueError):
|
||
return default
|
||
|
||
|
||
def _as_bool(value: Any) -> bool:
|
||
if isinstance(value, str):
|
||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||
return bool(value)
|
||
|
||
|
||
def _canonical_allowed(permissions: Any, code: str, *, default: bool = True) -> bool:
|
||
"""Check the exact admin permission code, without dot/slash aliases."""
|
||
|
||
if permissions is None:
|
||
return default
|
||
for method_name in ("allows", "has", "can", "contains", "has_permission"):
|
||
method = getattr(permissions, method_name, None)
|
||
if callable(method):
|
||
try:
|
||
return bool(method(code))
|
||
except (TypeError, ValueError):
|
||
continue
|
||
raw = permissions
|
||
for attr in ("codes", "permissions", "values"):
|
||
candidate = getattr(permissions, attr, None)
|
||
if candidate is not None and not callable(candidate):
|
||
raw = candidate
|
||
break
|
||
if isinstance(raw, Mapping):
|
||
nested = first_value(raw, "codes", "permissions", "values", default=None)
|
||
if nested is not None:
|
||
raw = nested
|
||
if isinstance(raw, Mapping):
|
||
available = {str(key) for key, enabled in raw.items() if enabled}
|
||
elif isinstance(raw, str):
|
||
available = {raw}
|
||
else:
|
||
try:
|
||
available = {str(item) for item in raw}
|
||
except TypeError:
|
||
return False
|
||
if "*" in available or code in available:
|
||
return True
|
||
return any(grant.endswith("/*") and code.startswith(grant[:-1]) for grant in available)
|
||
|
||
|
||
def is_valid_id_card(value: Any) -> bool:
|
||
"""Match the admin's 15/18-digit preflight validation contract."""
|
||
|
||
candidate = str(value or "").strip()
|
||
return bool(_ID_CARD_18_RE.fullmatch(candidate) or _ID_CARD_15_RE.fullmatch(candidate))
|
||
|
||
|
||
def _supports_native_video(repository: Any) -> bool:
|
||
"""Fail closed unless the repository exposes the full doctor-call lifecycle."""
|
||
|
||
return all(
|
||
callable(getattr(repository, method_name, None))
|
||
for method_name in ("get_call_ticket", "start_call", "bind_call_room", "end_call")
|
||
)
|
||
|
||
|
||
def appointment_rows(record: Any) -> list[Any]:
|
||
"""Return canonical ``appointments[]`` with the list-row fallback."""
|
||
|
||
rows = get_value(record, "appointments", None)
|
||
if isinstance(rows, Sequence) and not isinstance(rows, (str, bytes)) and rows:
|
||
return list(rows)
|
||
if not _as_bool(first_value(record, "has_appointment", default=False)):
|
||
return []
|
||
latest = get_value(record, "latest_appointment", None)
|
||
if latest:
|
||
return [latest]
|
||
return [
|
||
{
|
||
"id": first_value(record, "appointment_id"),
|
||
"status": first_value(record, "appointment_status"),
|
||
"doctor_id": first_value(record, "appointment_doctor_id", "doctor_id"),
|
||
"doctor_name": first_value(
|
||
record, "appointment_doctor_name", "doctor_name", default=""
|
||
),
|
||
"appointment_date": first_value(record, "appointment_date", default=""),
|
||
"time_text": first_value(
|
||
record,
|
||
"appointment_time_text",
|
||
"appointment_time",
|
||
"period",
|
||
default="",
|
||
),
|
||
}
|
||
]
|
||
|
||
|
||
def _appointment_status(record: Any) -> Any:
|
||
status = first_value(record, "appointment_status", default=None)
|
||
if status is not None:
|
||
return status
|
||
latest = get_value(record, "latest_appointment", None)
|
||
status = first_value(latest, "status", "appointment_status", default=None)
|
||
if status is not None:
|
||
return status
|
||
rows = appointment_rows(record)
|
||
return first_value(rows[0], "status", "appointment_status", default=None) if rows else None
|
||
|
||
|
||
def _appointment_id(record: Any) -> int:
|
||
value = first_value(record, "appointment_id", default=None)
|
||
if value is None:
|
||
value = first_value(get_value(record, "latest_appointment", None), "id", default=None)
|
||
if value is None:
|
||
rows = appointment_rows(record)
|
||
value = first_value(rows[0], "id", "appointment_id", default=0) if rows else 0
|
||
return _as_int(value)
|
||
|
||
|
||
def _single_cancellable_appointment(record: Any) -> Any | None:
|
||
if not _as_bool(first_value(record, "has_appointment", default=False)):
|
||
return None
|
||
if _as_int(_appointment_status(record), -1) not in {1, 4}:
|
||
return None
|
||
rows = appointment_rows(record)
|
||
if len(rows) != 1:
|
||
return None
|
||
appointment = rows[0]
|
||
appointment_id = _as_int(first_value(appointment, "id", "appointment_id", default=0))
|
||
status = _as_int(first_value(appointment, "status", "appointment_status", default=-1), -1)
|
||
return appointment if appointment_id > 0 and status in {1, 4} else None
|
||
|
||
|
||
def _cancellable_appointment_by_id(record: Any, appointment_id: int) -> Any | None:
|
||
"""Resolve one cancellable appointment from the visible nested records only."""
|
||
|
||
target_id = _as_int(appointment_id)
|
||
if target_id <= 0:
|
||
return None
|
||
matches = [
|
||
appointment
|
||
for appointment in appointment_rows(record)
|
||
if _as_int(first_value(appointment, "id", "appointment_id", default=0)) == target_id
|
||
and _as_int(first_value(appointment, "status", "appointment_status", default=-1), -1)
|
||
in {1, 4}
|
||
]
|
||
return matches[0] if len(matches) == 1 else None
|
||
|
||
|
||
def _repository_method_name(repository: Any, *candidates: str) -> str | None:
|
||
"""Return the first concrete repository capability without manufacturing a fallback."""
|
||
|
||
return next(
|
||
(name for name in candidates if callable(getattr(repository, name, None))),
|
||
None,
|
||
)
|
||
|
||
|
||
def is_video_available(record: Any) -> bool:
|
||
"""Only an existing, currently booked appointment may enter video."""
|
||
|
||
return bool(
|
||
record is not None
|
||
and _as_bool(first_value(record, "has_appointment", default=False))
|
||
and _as_int(_appointment_status(record), -1) == 1
|
||
)
|
||
|
||
|
||
def _video_payload(record: Any) -> dict[str, Any]:
|
||
"""Build call identifiers without confusing patient, diagnosis and appointment IDs."""
|
||
|
||
return {
|
||
"source": "consultations",
|
||
"appointment_id": _appointment_id(record),
|
||
"patient_id": first_value(record, "patient_id", "source_patient_id"),
|
||
"diagnosis_id": first_value(record, "diagnosis_id", "id"),
|
||
"patient_name": first_value(record, "patient_name", default="患者"),
|
||
# This page is the assistant/admin receiver. IM mode logs in and waits
|
||
# for the doctor's existing invitation; it must never call startCall.
|
||
"mode": "im",
|
||
"record": record,
|
||
}
|
||
|
||
|
||
def is_diagnosis_confirmed(record: Any) -> bool:
|
||
"""Use view records first, as the admin table does."""
|
||
|
||
records = get_value(record, "DiagnosisViewRecord", None)
|
||
if records is None:
|
||
records = get_value(record, "diagnosis_view_records", None)
|
||
if isinstance(records, Sequence) and not isinstance(records, (str, bytes)) and records:
|
||
return any(_as_int(first_value(item, "is_confirmed", default=0)) == 1 for item in records)
|
||
return _as_bool(first_value(record, "diagnosis_confirmed", "confirmed", default=False))
|
||
|
||
|
||
def prescription_action_label(record: Any) -> str:
|
||
return prescription_action(record)[0]
|
||
|
||
|
||
def can_void_prescription(record: Any) -> bool:
|
||
if record is None:
|
||
return False
|
||
if _as_int(first_value(record, "prescription_void_status", "void_status"), 0) == 1:
|
||
return False
|
||
if _as_int(first_value(record, "prescription_audit_status", "audit_status"), -1) == 1:
|
||
return False
|
||
return not _as_bool(first_value(record, "has_prescription_order", default=False))
|
||
|
||
|
||
def _status_value(row: Any) -> tuple[str, str]:
|
||
status = _as_int(_appointment_status(row), -1)
|
||
fallback = APPOINTMENT_STATUS.get(status)
|
||
if fallback is not None:
|
||
return fallback
|
||
return (
|
||
display_text(
|
||
first_value(row, "appointment_status_text", "appointment_status_desc", default="—")
|
||
),
|
||
"neutral",
|
||
)
|
||
|
||
|
||
def _appointment_status_label(status: Any) -> str:
|
||
return APPOINTMENT_STATUS.get(_as_int(status), (display_text(status), "neutral"))[0]
|
||
|
||
|
||
def _appointment_cell(_value: Any, row: Any) -> str:
|
||
rows = appointment_rows(row)
|
||
if not rows:
|
||
return "未挂号"
|
||
lines: list[str] = []
|
||
for appointment in rows:
|
||
status = _appointment_status_label(first_value(appointment, "status", default=1))
|
||
doctor = display_text(first_value(appointment, "doctor_name"), "—")
|
||
date_text = display_text(first_value(appointment, "appointment_date", "date"), "")
|
||
time_text = display_text(
|
||
first_value(appointment, "time_text", "appointment_time", "period"), ""
|
||
)
|
||
when = " ".join(value for value in (date_text, time_text) if value) or "—"
|
||
lines.append(f"{status} · {doctor} · {when}")
|
||
channel = first_value(
|
||
row,
|
||
"latest_appointment_channel_source_desc",
|
||
"latest_appointment_channel_source",
|
||
default="",
|
||
)
|
||
detail = first_value(row, "latest_appointment_channel_source_detail", default="")
|
||
if channel:
|
||
lines.append(f"渠道:{channel}{f'({detail})' if detail else ''}")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _id_cell(_value: Any, row: Any) -> str:
|
||
diagnosis_id = display_text(first_value(row, "id", "diagnosis_id"))
|
||
unread = get_value(row, "assign_read_at", "missing") is None
|
||
assigned = _as_int(first_value(row, "assistant_id", "assistant", default=0)) > 0
|
||
return f"{diagnosis_id} NEW" if unread and assigned else diagnosis_id
|
||
|
||
|
||
def _gender_age_cell(_value: Any, row: Any) -> str:
|
||
gender = first_value(row, "gender_desc", default=None)
|
||
if gender is None:
|
||
gender = gender_text(first_value(row, "gender"))
|
||
age = display_text(first_value(row, "age"))
|
||
return f"{gender} · {age}岁"
|
||
|
||
|
||
def _confirmation_cell(_value: Any, row: Any) -> str:
|
||
return "已确认" if is_diagnosis_confirmed(row) else "未确认"
|
||
|
||
|
||
def _revisit_cell(_value: Any, row: Any) -> str:
|
||
if not _as_bool(first_value(row, "has_prescription", default=False)):
|
||
return "无"
|
||
when = display_text(first_value(row, "followup_time_text", "followup_time"))
|
||
doctor = display_text(first_value(row, "followup_doctor_name"))
|
||
return f"{when}\n{doctor}"
|
||
|
||
|
||
def _void_cell(_value: Any, row: Any) -> str:
|
||
voided = _as_int(
|
||
first_value(row, "followup_rx_voided", "prescription_void_status", "void_status"),
|
||
0,
|
||
)
|
||
return "已作废" if voided == 1 else "—"
|
||
|
||
|
||
def _prescription_cell(_value: Any, row: Any) -> str:
|
||
return "已开方" if _as_bool(first_value(row, "has_prescription", default=False)) else "未开方"
|
||
|
||
|
||
def _unserved_cell(_value: Any, row: Any) -> str:
|
||
days = first_value(row, "unserved_days", "unserved_day_count", default=None)
|
||
latest = first_value(
|
||
row,
|
||
"last_face_to_face_at",
|
||
"last_face_to_face_time",
|
||
"last_interview_at",
|
||
"last_appointment_at",
|
||
"last_blood_record_at",
|
||
default="",
|
||
)
|
||
if days is None and not latest:
|
||
return "—"
|
||
first_line = f"{display_text(days)} 天" if days is not None else "—"
|
||
return f"{first_line}\n上次面诊:{display_text(latest)}" if latest else first_line
|
||
|
||
|
||
def _watch_cell(_value: Any, row: Any) -> str:
|
||
state = first_value(row, "video_call_hint.state", "video_hint", default="none")
|
||
label = first_value(row, "video_call_hint.label", default="")
|
||
return {
|
||
"live": "通话中",
|
||
"pending_room": "等待接通",
|
||
"none": display_text(label, "暂无通话"),
|
||
}.get(str(state), display_text(label or state))
|
||
|
||
|
||
def _option_rows(value: Any, dictionary_type: str = "") -> list[Any]:
|
||
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
||
return list(value)
|
||
if isinstance(value, Mapping):
|
||
candidates = (
|
||
value.get(dictionary_type),
|
||
value.get("items"),
|
||
value.get("lists"),
|
||
value.get("data"),
|
||
)
|
||
for candidate in candidates:
|
||
if isinstance(candidate, Sequence) and not isinstance(candidate, (str, bytes)):
|
||
return list(candidate)
|
||
if isinstance(candidate, Mapping):
|
||
nested = candidate.get(dictionary_type)
|
||
if isinstance(nested, Sequence) and not isinstance(nested, (str, bytes)):
|
||
return list(nested)
|
||
return []
|
||
|
||
|
||
class _DiagnosisCreateDialog(QDialog):
|
||
"""Narrow add form containing the admin page's required identity fields."""
|
||
|
||
def __init__(self, parent: QWidget | None = None) -> None:
|
||
super().__init__(parent)
|
||
self.setWindowTitle("新增诊单")
|
||
self.resize(540, 530)
|
||
root = QVBoxLayout(self)
|
||
root.setContentsMargins(24, 22, 24, 18)
|
||
root.setSpacing(14)
|
||
title = QLabel("新增诊单")
|
||
title.setProperty("dialogRole", "title")
|
||
root.addWidget(title)
|
||
form = QFormLayout()
|
||
self.patient_name = QLineEdit()
|
||
self.patient_name.setMaxLength(50)
|
||
form.addRow("患者姓名 *", self.patient_name)
|
||
self.phone = QLineEdit()
|
||
self.phone.setMaxLength(20)
|
||
form.addRow("手机号 *", self.phone)
|
||
self.gender = QComboBox()
|
||
self.gender.addItem("男", 1)
|
||
self.gender.addItem("女", 2)
|
||
form.addRow("性别 *", self.gender)
|
||
self.age = QSpinBox()
|
||
self.age.setRange(0, 150)
|
||
self.age.setValue(30)
|
||
form.addRow("年龄 *", self.age)
|
||
self.fasting_blood_sugar = QDoubleSpinBox()
|
||
self.fasting_blood_sugar.setRange(0, 50)
|
||
self.fasting_blood_sugar.setDecimals(1)
|
||
self.fasting_blood_sugar.setValue(5.0)
|
||
self.fasting_blood_sugar.setSuffix(" mmol/L")
|
||
form.addRow("空腹血糖 *", self.fasting_blood_sugar)
|
||
self.diagnosis_type = QLineEdit()
|
||
self.diagnosis_type.setPlaceholderText("填写后台诊断类型值")
|
||
form.addRow("诊断类型 *", self.diagnosis_type)
|
||
self.syndrome_type = QLineEdit()
|
||
form.addRow("证型", self.syndrome_type)
|
||
self.local_hospital_diagnosis = QLineEdit()
|
||
form.addRow("当地医院诊断 *", self.local_hospital_diagnosis)
|
||
root.addLayout(form)
|
||
self.banner = MessageBanner()
|
||
root.addWidget(self.banner)
|
||
buttons = QDialogButtonBox(
|
||
QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Save
|
||
)
|
||
buttons.button(QDialogButtonBox.StandardButton.Save).setText("创建诊单")
|
||
buttons.button(QDialogButtonBox.StandardButton.Save).setProperty("variant", "primary")
|
||
buttons.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
|
||
buttons.rejected.connect(self.reject)
|
||
buttons.accepted.connect(self.accept)
|
||
root.addWidget(buttons)
|
||
mark_business_dialog(self, "DiagnosisCreateDialog")
|
||
|
||
def payload(self) -> dict[str, Any]:
|
||
local_diagnosis = self.local_hospital_diagnosis.text().strip()
|
||
return {
|
||
"patient_name": self.patient_name.text().strip(),
|
||
"name": self.patient_name.text().strip(),
|
||
"phone": self.phone.text().strip(),
|
||
"patient_phone": self.phone.text().strip(),
|
||
"gender": self.gender.currentData(),
|
||
"age": self.age.value(),
|
||
"fasting_blood_sugar": self.fasting_blood_sugar.value(),
|
||
"diagnosis_type": self.diagnosis_type.text().strip(),
|
||
"syndrome_type": self.syndrome_type.text().strip(),
|
||
"local_hospital_diagnosis": [local_diagnosis] if local_diagnosis else [],
|
||
"diagnosis_date": QDate.currentDate().toString("yyyy-MM-dd"),
|
||
"status": 1,
|
||
}
|
||
|
||
def accept(self) -> None:
|
||
payload = self.payload()
|
||
if not payload["patient_name"]:
|
||
self.banner.show_message("请输入患者姓名。", "warning")
|
||
return
|
||
if not re.fullmatch(r"1[3-9]\d{9}", str(payload["phone"])):
|
||
self.banner.show_message("请输入有效的 11 位手机号。", "warning")
|
||
return
|
||
if not payload["diagnosis_type"]:
|
||
self.banner.show_message("请输入诊断类型。", "warning")
|
||
return
|
||
if not payload["local_hospital_diagnosis"]:
|
||
self.banner.show_message("请输入当地医院诊断。", "warning")
|
||
return
|
||
self.banner.clear()
|
||
super().accept()
|
||
|
||
|
||
class _ToastBanner(MessageBanner):
|
||
"""Compatibility message target that renders as a non-layout-shifting toast."""
|
||
|
||
def __init__(self, owner: QWidget, parent: QWidget | None = None) -> None:
|
||
self._owner = owner
|
||
super().__init__(parent=parent)
|
||
self.setFixedSize(0, 0)
|
||
self.hide()
|
||
|
||
def show_message(self, text: str, kind: str = "info") -> None:
|
||
super().show_message(text, kind)
|
||
self.hide()
|
||
if text:
|
||
show_toast(self._owner, text, kind)
|
||
|
||
|
||
class _DiagnosisOrderDialog(QDialog):
|
||
"""Small admin-parity form for the diagnosis list's generic business order."""
|
||
|
||
ORDER_TYPES = (
|
||
("挂号费", 1),
|
||
("问诊费", 2),
|
||
("药品费用", 3),
|
||
("首付费用", 4),
|
||
("尾款费用", 5),
|
||
("其他费用", 6),
|
||
("全部费用", 7),
|
||
("驼奶费用", 8),
|
||
)
|
||
|
||
def __init__(self, record: Any, parent: QWidget | None = None) -> None:
|
||
super().__init__(parent)
|
||
self._patient_id = _as_int(first_value(record, "patient_id", "source_patient_id"))
|
||
self.setWindowTitle("创建订单")
|
||
self.resize(500, 360)
|
||
root = QVBoxLayout(self)
|
||
root.setContentsMargins(24, 22, 24, 18)
|
||
root.setSpacing(14)
|
||
title = QLabel("创建订单")
|
||
title.setProperty("dialogRole", "title")
|
||
root.addWidget(title)
|
||
form = QFormLayout()
|
||
patient = QLabel(
|
||
f"{display_text(first_value(record, 'patient_name'), '患者')} (#{self._patient_id})"
|
||
)
|
||
patient.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||
form.addRow("患者", patient)
|
||
self.order_type = QComboBox()
|
||
self.order_type.addItem("请选择", None)
|
||
for label, value in self.ORDER_TYPES:
|
||
self.order_type.addItem(label, value)
|
||
form.addRow("订单类型", self.order_type)
|
||
self.amount = QDoubleSpinBox()
|
||
self.amount.setDecimals(2)
|
||
self.amount.setRange(0.01, 9_999_999.99)
|
||
self.amount.setSingleStep(0.01)
|
||
self.amount.setPrefix("¥ ")
|
||
form.addRow("订单金额", self.amount)
|
||
self.remark = QLineEdit()
|
||
self.remark.setPlaceholderText("请输入备注(可选)")
|
||
form.addRow("备注", self.remark)
|
||
root.addLayout(form)
|
||
buttons = QDialogButtonBox(
|
||
QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Ok
|
||
)
|
||
primary_button = buttons.button(QDialogButtonBox.StandardButton.Ok)
|
||
primary_button.setText("创建订单")
|
||
primary_button.setProperty("variant", "primary")
|
||
buttons.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
|
||
buttons.accepted.connect(self._accept_checked)
|
||
buttons.rejected.connect(self.reject)
|
||
root.addWidget(buttons)
|
||
mark_business_dialog(self, "DiagnosisOrderDialog")
|
||
|
||
def _accept_checked(self) -> None:
|
||
if self._patient_id <= 0:
|
||
QMessageBox.warning(self, "创建订单", "患者标识不完整,无法创建订单。")
|
||
return
|
||
if self.order_type.currentData() is None:
|
||
QMessageBox.warning(self, "创建订单", "请选择订单类型。")
|
||
return
|
||
self.accept()
|
||
|
||
def payload(self) -> dict[str, Any]:
|
||
return {
|
||
"patient_id": self._patient_id,
|
||
"order_type": _as_int(self.order_type.currentData()),
|
||
"amount": float(self.amount.value()),
|
||
"remark": self.remark.text().strip(),
|
||
}
|
||
|
||
|
||
class _AppointmentLogDialog(QDialog):
|
||
"""Read-only appointment audit log backed by the real diagnosis log endpoint."""
|
||
|
||
def __init__(self, record: Any, rows: Sequence[Any], parent: QWidget | None = None) -> None:
|
||
super().__init__(parent)
|
||
self.setWindowTitle("挂号操作日志")
|
||
self.resize(680, 420)
|
||
root = QVBoxLayout(self)
|
||
root.setContentsMargins(24, 22, 24, 18)
|
||
root.setSpacing(12)
|
||
title = QLabel("挂号操作日志")
|
||
title.setProperty("dialogRole", "title")
|
||
root.addWidget(title)
|
||
context = QLabel(
|
||
f"{display_text(first_value(record, 'patient_name'), '患者')} | "
|
||
f"诊单 #{_as_int(first_value(record, 'diagnosis_id', 'id'))}"
|
||
)
|
||
context.setProperty("dialogRole", "subtitle")
|
||
context.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||
root.addWidget(context)
|
||
table = QTableWidget(0, 4, self)
|
||
table.setHorizontalHeaderLabels(["时间", "动作", "操作人", "摘要"])
|
||
table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||
table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||
table.verticalHeader().hide()
|
||
table.horizontalHeader().setStretchLastSection(True)
|
||
for row in rows:
|
||
index = table.rowCount()
|
||
table.insertRow(index)
|
||
operator = first_value(row, "admin_name", "operator_name", default="")
|
||
if not operator:
|
||
operator_id = _as_int(first_value(row, "admin_id", "operator_id", default=0))
|
||
operator = f"管理员 #{operator_id}" if operator_id > 0 else "—"
|
||
values = (
|
||
first_value(row, "create_time_text", "created_at", "create_time", default="—"),
|
||
first_value(row, "action_desc", "action", default="—"),
|
||
operator,
|
||
first_value(row, "summary", "remark", "content", default="—"),
|
||
)
|
||
for column, value in enumerate(values):
|
||
table.setItem(index, column, QTableWidgetItem(display_text(value)))
|
||
if not rows:
|
||
table.setRowCount(1)
|
||
empty = QTableWidgetItem("暂无挂号/取消挂号操作记录")
|
||
empty.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
table.setItem(0, 0, empty)
|
||
table.setSpan(0, 0, 1, 4)
|
||
root.addWidget(table)
|
||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||
buttons.button(QDialogButtonBox.StandardButton.Close).setText("关闭")
|
||
buttons.rejected.connect(self.reject)
|
||
root.addWidget(buttons)
|
||
mark_business_dialog(self, "AppointmentLogDialog")
|
||
|
||
|
||
class _QrResultDialog(QDialog):
|
||
"""Expose the server-issued QR asset without pretending to generate it locally."""
|
||
|
||
def __init__(
|
||
self,
|
||
title: str,
|
||
record: Any,
|
||
qrcode_url: str,
|
||
parent: QWidget | None = None,
|
||
) -> None:
|
||
super().__init__(parent)
|
||
self._url = qrcode_url
|
||
self.setWindowTitle(title)
|
||
self.resize(480, 310)
|
||
root = QVBoxLayout(self)
|
||
root.setContentsMargins(24, 22, 24, 18)
|
||
root.setSpacing(12)
|
||
heading = QLabel(title)
|
||
heading.setProperty("dialogRole", "title")
|
||
root.addWidget(heading)
|
||
description = QLabel(
|
||
f"患者:{display_text(first_value(record, 'patient_name'), '患者')}\n"
|
||
"二维码由服务器生成,请打开后使用企业微信转发给患者。"
|
||
)
|
||
description.setProperty("dialogRole", "subtitle")
|
||
root.addWidget(description)
|
||
link = QLabel(f'<a href="{escape(qrcode_url, quote=True)}">打开服务器二维码</a>')
|
||
link.setOpenExternalLinks(True)
|
||
link.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||
root.addWidget(link)
|
||
url_text = QLineEdit(qrcode_url)
|
||
url_text.setReadOnly(True)
|
||
url_text.setToolTip(qrcode_url)
|
||
root.addWidget(url_text)
|
||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||
open_button = buttons.addButton("打开二维码", QDialogButtonBox.ButtonRole.ActionRole)
|
||
open_button.setProperty("variant", "primary")
|
||
open_button.clicked.connect(lambda: QDesktopServices.openUrl(QUrl.fromUserInput(self._url)))
|
||
buttons.button(QDialogButtonBox.StandardButton.Close).setText("关闭")
|
||
buttons.rejected.connect(self.reject)
|
||
root.addWidget(buttons)
|
||
mark_business_dialog(self, "DiagnosisQrResultDialog")
|
||
open_button.style().unpolish(open_button)
|
||
open_button.style().polish(open_button)
|
||
|
||
|
||
class _QrImagePreview(QLabel):
|
||
"""Load the exact server-issued QR asset while keeping its URL visible."""
|
||
|
||
def __init__(self, parent: QWidget | None = None) -> None:
|
||
super().__init__(parent)
|
||
self.setObjectName("DiagnosisOrderQrPreview")
|
||
self.setFixedSize(258, 258)
|
||
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self.setWordWrap(True)
|
||
self.setStyleSheet(
|
||
"QLabel#DiagnosisOrderQrPreview{background:#FFFFFF;border:1px solid #E1E6F2;"
|
||
"border-radius:12px;color:#7481A3;padding:8px;}"
|
||
)
|
||
self._manager = QNetworkAccessManager(self)
|
||
self._reply: QNetworkReply | None = None
|
||
self._generation = 0
|
||
self.show_loading()
|
||
|
||
def show_loading(self) -> None:
|
||
self._generation += 1
|
||
if self._reply is not None:
|
||
self._reply.abort()
|
||
self._reply.deleteLater()
|
||
self._reply = None
|
||
self.clear()
|
||
self.setText("正在加载服务器二维码...")
|
||
|
||
def show_failure(self, message: str = "二维码图片加载失败") -> None:
|
||
self._generation += 1
|
||
if self._reply is not None:
|
||
self._reply.abort()
|
||
self._reply.deleteLater()
|
||
self._reply = None
|
||
self.clear()
|
||
self.setText(f"{message}\n可使用下方地址打开")
|
||
|
||
def load_url(self, source: str) -> None:
|
||
self.show_loading()
|
||
generation = self._generation
|
||
url = QUrl.fromUserInput(source)
|
||
if not url.isValid() or url.scheme().lower() not in {"http", "https"}:
|
||
self.show_failure("二维码地址无效")
|
||
return
|
||
request = QNetworkRequest(url)
|
||
request.setTransferTimeout(10_000)
|
||
reply = self._manager.get(request)
|
||
self._reply = reply
|
||
reply.finished.connect(lambda: self._loaded(reply, generation))
|
||
|
||
def _loaded(self, reply: QNetworkReply, generation: int) -> None:
|
||
if reply is not self._reply or generation != self._generation:
|
||
reply.deleteLater()
|
||
return
|
||
self._reply = None
|
||
data = bytes(reply.readAll())
|
||
error = reply.error()
|
||
reply.deleteLater()
|
||
if error != QNetworkReply.NetworkError.NoError:
|
||
self.show_failure()
|
||
return
|
||
pixmap = QPixmap()
|
||
if not data or not pixmap.loadFromData(data):
|
||
self.show_failure()
|
||
return
|
||
self.setText("")
|
||
self.setPixmap(
|
||
pixmap.scaled(
|
||
240,
|
||
240,
|
||
Qt.AspectRatioMode.KeepAspectRatio,
|
||
Qt.TransformationMode.SmoothTransformation,
|
||
)
|
||
)
|
||
|
||
|
||
class _DiagnosisOrderQrDialog(QDialog):
|
||
"""Payment QR state that retains the immutable order number for retries."""
|
||
|
||
retry_requested = Signal(str)
|
||
|
||
def __init__(
|
||
self,
|
||
record: Any,
|
||
order_no: str,
|
||
parent: QWidget | None = None,
|
||
) -> None:
|
||
super().__init__(parent)
|
||
self.order_no = order_no
|
||
self.qrcode_url = ""
|
||
self.setObjectName("DiagnosisOrderQrDialog")
|
||
self.setWindowTitle("付款二维码")
|
||
self.setWindowModality(Qt.WindowModality.WindowModal)
|
||
self.resize(500, 610)
|
||
root = QVBoxLayout(self)
|
||
root.setContentsMargins(24, 22, 24, 18)
|
||
root.setSpacing(14)
|
||
|
||
heading = QLabel("订单已创建", self)
|
||
heading.setProperty("dialogRole", "title")
|
||
heading.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
root.addWidget(heading)
|
||
context = QLabel(
|
||
f"患者:{display_text(first_value(record, 'patient_name'), '患者')}\n"
|
||
f"订单号:{order_no}",
|
||
self,
|
||
)
|
||
context.setObjectName("DiagnosisOrderQrContext")
|
||
context.setProperty("dialogRole", "subtitle")
|
||
context.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||
context.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
root.addWidget(context)
|
||
self.preview = _QrImagePreview(self)
|
||
preview_row = QHBoxLayout()
|
||
preview_row.addStretch(1)
|
||
preview_row.addWidget(self.preview)
|
||
preview_row.addStretch(1)
|
||
root.addLayout(preview_row)
|
||
self.status_label = QLabel("正在生成付款二维码...", self)
|
||
self.status_label.setObjectName("DiagnosisOrderQrStatus")
|
||
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self.status_label.setWordWrap(True)
|
||
root.addWidget(self.status_label)
|
||
self.url_edit = QLineEdit(self)
|
||
self.url_edit.setObjectName("DiagnosisOrderQrUrl")
|
||
self.url_edit.setReadOnly(True)
|
||
self.url_edit.setPlaceholderText("服务器二维码地址将在生成后显示")
|
||
root.addWidget(self.url_edit)
|
||
hint = QLabel("请使用企业微信扫描二维码,然后转发给患者", self)
|
||
hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
hint.setProperty("dialogRole", "subtitle")
|
||
root.addWidget(hint)
|
||
|
||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close, self)
|
||
self.open_button = buttons.addButton(
|
||
"打开二维码地址", QDialogButtonBox.ButtonRole.ActionRole
|
||
)
|
||
self.retry_button = buttons.addButton("重新生成", QDialogButtonBox.ButtonRole.ActionRole)
|
||
self.open_button.setProperty("variant", "primary")
|
||
self.retry_button.setProperty("variant", "secondary")
|
||
self.open_button.clicked.connect(self._open_url)
|
||
self.retry_button.clicked.connect(lambda: self.retry_requested.emit(self.order_no))
|
||
buttons.button(QDialogButtonBox.StandardButton.Close).setText("关闭")
|
||
buttons.rejected.connect(self.reject)
|
||
root.addWidget(buttons)
|
||
mark_business_dialog(self, "DiagnosisOrderQrDialog")
|
||
for button in (self.open_button, self.retry_button):
|
||
button.style().unpolish(button)
|
||
button.style().polish(button)
|
||
self.set_loading()
|
||
|
||
def set_loading(self) -> None:
|
||
self.qrcode_url = ""
|
||
self.url_edit.clear()
|
||
self.status_label.setText("正在生成付款二维码...")
|
||
self.status_label.setStyleSheet("color:#7481A3;")
|
||
self.preview.show_loading()
|
||
self.open_button.setEnabled(False)
|
||
self.retry_button.setEnabled(False)
|
||
|
||
def set_failure(self, message: str, *, retryable: bool = True) -> None:
|
||
self.qrcode_url = ""
|
||
self.url_edit.clear()
|
||
self.status_label.setText(message)
|
||
self.status_label.setStyleSheet("color:#EC5266;")
|
||
self.preview.show_failure("付款二维码生成失败")
|
||
self.open_button.setEnabled(False)
|
||
self.retry_button.setEnabled(retryable)
|
||
|
||
def set_result(self, qrcode_url: str) -> None:
|
||
self.qrcode_url = qrcode_url
|
||
self.url_edit.setText(qrcode_url)
|
||
self.url_edit.setCursorPosition(0)
|
||
self.url_edit.setToolTip(qrcode_url)
|
||
self.status_label.setText("付款二维码已生成")
|
||
self.status_label.setStyleSheet("color:#159C79;")
|
||
self.preview.load_url(qrcode_url)
|
||
self.open_button.setEnabled(True)
|
||
self.retry_button.setEnabled(True)
|
||
|
||
def _open_url(self) -> None:
|
||
if self.qrcode_url:
|
||
QDesktopServices.openUrl(QUrl.fromUserInput(self.qrcode_url))
|
||
|
||
|
||
class ConsultationsPage(QWidget):
|
||
"""Diagnosis workspace with canonical filters and guarded row actions."""
|
||
|
||
video_requested = Signal(dict)
|
||
|
||
def __init__(
|
||
self,
|
||
repository: Any,
|
||
permissions: Any = None,
|
||
current_user: Any = None,
|
||
parent: QWidget | None = None,
|
||
) -> None:
|
||
super().__init__(parent)
|
||
self.setObjectName("DiagnosisIndex")
|
||
self.setStyleSheet(DIAGNOSIS_INDEX_QSS + CONSULTATIONS_REFERENCE_QSS)
|
||
self.repository = repository
|
||
self.permissions = permissions
|
||
self.current_user = current_user
|
||
self._page = 1
|
||
self._page_size = 15
|
||
self._generation = 0
|
||
self._count_generation = 0
|
||
self._options_generation = 0
|
||
self._prescription_generation = 0
|
||
self._mutation_generation = 0
|
||
self._menu_generation = 0
|
||
self._order_generation = 0
|
||
self._order_flow_generation: int | None = None
|
||
self._order_qr_request_pending = False
|
||
self._loading = False
|
||
self._mutation_pending = False
|
||
self._prescription_busy = False
|
||
self._options_loaded = False
|
||
self._visible_loading_generation = 0
|
||
self._appointment_date = QDate.currentDate().toString("yyyy-MM-dd")
|
||
self._appointment_status_filter = "1"
|
||
self._pending_booking = ""
|
||
self._completed_appointment = ""
|
||
self._pending_assign = ""
|
||
self._date_counts: dict[str, int] = {}
|
||
self._status_counts: dict[str, int] = {}
|
||
self._assistant_names: dict[int, str] = {}
|
||
self._last_prescription: Any = None
|
||
self._appointment_logs_dialog: QDialog | None = None
|
||
self._qr_result_dialog: QDialog | None = None
|
||
self._order_qr_dialog: _DiagnosisOrderQrDialog | None = None
|
||
self._native_video_capable = _supports_native_video(repository)
|
||
self._repository_methods = {
|
||
"cancel_appointment": _repository_method_name(
|
||
repository, "cancel_diagnosis_appointment"
|
||
),
|
||
"video_qr": _repository_method_name(repository, "generate_video_qrcode"),
|
||
"confirm_qr": _repository_method_name(repository, "generate_diagnosis_qrcode"),
|
||
"appointment_logs": _repository_method_name(repository, "list_appointment_logs"),
|
||
"create_order": _repository_method_name(repository, "create_diagnosis_order"),
|
||
"order_qr": _repository_method_name(repository, "generate_order_qrcode"),
|
||
}
|
||
|
||
root = QVBoxLayout(self)
|
||
root.setContentsMargins(0, 0, 0, 0)
|
||
root.setSpacing(0)
|
||
self.page_scroll = QScrollArea(self)
|
||
self.page_scroll.setObjectName("DiagnosisPageScroll")
|
||
self.page_scroll.setWidgetResizable(True)
|
||
self.page_scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||
self.page_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||
self.page_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||
root.addWidget(self.page_scroll)
|
||
content = QWidget()
|
||
content.setObjectName("DiagnosisIndexContent")
|
||
content.setAutoFillBackground(False)
|
||
self.page_scroll.setWidget(content)
|
||
page_layout = QVBoxLayout(content)
|
||
page_layout.setContentsMargins(18, 10, 18, 10)
|
||
page_layout.setSpacing(8)
|
||
|
||
self.page_header = PageHeader(
|
||
"问诊列表",
|
||
"按状态与日期管理患者队列,完成通话、开方与接诊闭环。",
|
||
content,
|
||
)
|
||
self.page_header.setFixedHeight(_PAGE_HEADER_HEIGHT)
|
||
page_layout.addWidget(self.page_header)
|
||
|
||
status_card = QFrame()
|
||
status_card.setObjectName("DiagnosisStatusCard")
|
||
status_card.setFixedHeight(_STATUS_CARD_HEIGHT)
|
||
self.status_card = status_card
|
||
status_card_layout = QHBoxLayout(status_card)
|
||
status_card_layout.setContentsMargins(12, 5, 12, 5)
|
||
status_card_layout.setSpacing(10)
|
||
|
||
status_tabs = QWidget(status_card)
|
||
status_tabs.setObjectName("DiagnosisStatusTabs")
|
||
status_tabs_layout = QHBoxLayout(status_tabs)
|
||
status_tabs_layout.setContentsMargins(0, 0, 0, 0)
|
||
status_tabs_layout.setSpacing(4)
|
||
self.status_buttons: dict[str, DiagnosisChip] = {}
|
||
self._status_button_labels = {
|
||
"1": "待接诊",
|
||
"": "全部",
|
||
"4": "已过号",
|
||
"2": "已取消",
|
||
"3": "已完成",
|
||
}
|
||
for value, label in self._status_button_labels.items():
|
||
button = DiagnosisChip(label, parent=status_tabs)
|
||
button.setProperty("statusTab", True)
|
||
button.clicked.connect(
|
||
lambda _checked=False, selected=value: self._choose_status(selected)
|
||
)
|
||
status_tabs_layout.addWidget(button, 1)
|
||
self.status_buttons[value] = button
|
||
self.completed_button = self.status_buttons["3"]
|
||
status_card_layout.addWidget(status_tabs, 3)
|
||
|
||
status_search = QWidget(status_card)
|
||
status_search.setObjectName("DiagnosisStatusSearch")
|
||
status_search_layout = QHBoxLayout(status_search)
|
||
status_search_layout.setContentsMargins(0, 0, 0, 0)
|
||
status_search_layout.setSpacing(6)
|
||
self.patient_name_edit = QLineEdit(status_search)
|
||
self.patient_name_edit.setObjectName("DiagnosisPatientName")
|
||
self.patient_name_edit.setPlaceholderText("患者姓名")
|
||
self.patient_name_edit.setClearButtonEnabled(True)
|
||
self.patient_name_edit.returnPressed.connect(self._search)
|
||
status_search_layout.addWidget(self.patient_name_edit, 1)
|
||
self.doctor_edit = QLineEdit(status_search)
|
||
self.doctor_edit.setObjectName("DiagnosisDoctorName")
|
||
self.doctor_edit.setPlaceholderText("医生")
|
||
self.doctor_edit.setClearButtonEnabled(True)
|
||
self.doctor_edit.returnPressed.connect(self._search)
|
||
status_search_layout.addWidget(self.doctor_edit, 1)
|
||
self.search_button = QPushButton("查询", status_search)
|
||
self.search_button.setProperty("variant", "primary")
|
||
self.search_button.setFixedWidth(62)
|
||
self.search_button.clicked.connect(self._search)
|
||
status_search_layout.addWidget(self.search_button)
|
||
self.custom_date_edit = QDateEdit(status_search)
|
||
self.custom_date_edit.setObjectName("DiagnosisCustomDate")
|
||
self.custom_date_edit.setCalendarPopup(True)
|
||
self.custom_date_edit.setDisplayFormat("yyyy-MM-dd")
|
||
self.custom_date_edit.setMinimumDate(_OPTIONAL_DATE_MINIMUM)
|
||
self.custom_date_edit.setSpecialValueText("自定义日期")
|
||
self.custom_date_edit.setDate(_OPTIONAL_DATE_MINIMUM)
|
||
self.custom_date_edit.setToolTip("自定义挂号日期")
|
||
self.custom_date_edit.dateChanged.connect(self._custom_date_changed)
|
||
status_search_layout.addWidget(self.custom_date_edit)
|
||
self.reset_button = QPushButton("重置", status_search)
|
||
self.reset_button.setFixedWidth(58)
|
||
self.reset_button.clicked.connect(self._reset)
|
||
status_search_layout.addWidget(self.reset_button)
|
||
status_card_layout.addWidget(status_search, 2)
|
||
page_layout.addWidget(status_card)
|
||
|
||
filters = QFrame()
|
||
filters.setObjectName("DiagnosisFilterCard")
|
||
self.filters_card = filters
|
||
filter_layout = QVBoxLayout(filters)
|
||
filter_layout.setContentsMargins(12, 6, 12, 6)
|
||
filter_layout.setSpacing(4)
|
||
|
||
date_filters = QWidget(filters)
|
||
date_filters.setObjectName("DiagnosisDateFilters")
|
||
main_filters = QHBoxLayout(date_filters)
|
||
main_filters.setContentsMargins(0, 0, 0, 0)
|
||
main_filters.setSpacing(8)
|
||
main_filters.setAlignment(Qt.AlignmentFlag.AlignVCenter)
|
||
main_filters.addWidget(self._filter_label("日期:"))
|
||
self.main_chip_flow = FlowWidget(horizontal_spacing=6, vertical_spacing=6)
|
||
self.date_buttons: dict[str, DiagnosisChip] = {}
|
||
self._date_button_labels: dict[str, str] = {}
|
||
for label, offset in (
|
||
("昨天挂号", -1),
|
||
("前天挂号", -2),
|
||
("当天挂号", 0),
|
||
("明天挂号", 1),
|
||
("后天挂号", 2),
|
||
):
|
||
value = QDate.currentDate().addDays(offset).toString("yyyy-MM-dd")
|
||
button = DiagnosisChip(label)
|
||
button.clicked.connect(
|
||
lambda _checked=False, date_value=value: self._choose_date(date_value)
|
||
)
|
||
self.date_buttons[value] = button
|
||
self._date_button_labels[value] = label
|
||
self.main_chip_flow.flow.addWidget(button)
|
||
all_button = DiagnosisChip("全部")
|
||
all_button.clicked.connect(lambda _checked=False: self._choose_date(""))
|
||
self.date_buttons[""] = all_button
|
||
self._date_button_labels[""] = "全部"
|
||
self.main_chip_flow.flow.addWidget(all_button)
|
||
self.pending_booking_button = DiagnosisChip("待预约", semantic="info")
|
||
self.pending_booking_button.clicked.connect(self._choose_pending_booking)
|
||
self.pending_assign_wrap = QWidget(self)
|
||
pending_assign_wrap_layout = QHBoxLayout(self.pending_assign_wrap)
|
||
pending_assign_wrap_layout.setContentsMargins(8, 0, 0, 0)
|
||
pending_assign_wrap_layout.setSpacing(8)
|
||
self.pending_assign_button = DiagnosisChip("待分配医助", semantic="warning")
|
||
self.pending_assign_button.clicked.connect(self._choose_pending_assign)
|
||
pending_assign_wrap_layout.addWidget(self.pending_assign_button)
|
||
self.pending_assign_filters = QWidget()
|
||
pending_assign_layout = QHBoxLayout(self.pending_assign_filters)
|
||
pending_assign_layout.setContentsMargins(0, 0, 0, 0)
|
||
pending_assign_layout.setSpacing(8)
|
||
self.pending_assign_month = QDateEdit(QDate.currentDate())
|
||
self.pending_assign_month.setObjectName("DiagnosisPendingAssignMonth")
|
||
self.pending_assign_month.setCalendarPopup(True)
|
||
self.pending_assign_month.setDisplayFormat("yyyy-MM")
|
||
self.pending_assign_month.setFixedWidth(128)
|
||
self.pending_assign_month.setToolTip("订单月份")
|
||
self.pending_assign_month.setAccessibleName("订单月份")
|
||
self.pending_assign_month.dateChanged.connect(
|
||
lambda _value: self._pending_assign_month_changed()
|
||
)
|
||
pending_assign_layout.addWidget(self.pending_assign_month)
|
||
self.pending_assign_keyword = QLineEdit()
|
||
self.pending_assign_keyword.setPlaceholderText("搜身份证/诊单ID/患者号/备注…")
|
||
self.pending_assign_keyword.setClearButtonEnabled(True)
|
||
self.pending_assign_keyword.setFixedWidth(220)
|
||
self.pending_assign_keyword.returnPressed.connect(self._search)
|
||
pending_assign_layout.addWidget(self.pending_assign_keyword)
|
||
pending_assign_wrap_layout.addWidget(self.pending_assign_filters)
|
||
self.pending_assign_filters.hide()
|
||
self.pending_assign_wrap.setVisible(_canonical_allowed(permissions, "tcm.diagnosis/assign"))
|
||
main_filters.addWidget(self.main_chip_flow, 1)
|
||
filter_layout.addWidget(date_filters)
|
||
|
||
self.has_appointment_combo = self._fixed_combo(
|
||
(("全部挂号", ""), ("已挂号", "1"), ("未挂号", "0"))
|
||
)
|
||
self.has_appointment_combo.setParent(filters)
|
||
self.has_appointment_combo.hide()
|
||
self.diagnosis_confirmed_combo = self._fixed_combo(
|
||
(("全部", ""), ("已确认", "1"), ("未确认", "0"))
|
||
)
|
||
self.diagnosis_confirmed_combo.setObjectName("DiagnosisConfirmationFilter")
|
||
self.diagnosis_confirmed_combo.activated.connect(self._confirmation_combo_changed)
|
||
self.confirmed_combo = self.diagnosis_confirmed_combo
|
||
self.department_combo = self._fixed_combo((("全部部门", ""),))
|
||
self.department_combo.setObjectName("DiagnosisDepartmentFilter")
|
||
department_name = display_text(
|
||
first_value(current_user, "department_name", "department", "dept_name", default=""),
|
||
"",
|
||
)
|
||
department_id = first_value(current_user, "department_id", "dept_id", default="")
|
||
if department_name:
|
||
self.department_combo.addItem(department_name, department_id or department_name)
|
||
self.unserved_sort_combo = self._fixed_combo(
|
||
(
|
||
("未服务天数默认排序", ""),
|
||
("未服务天数从多到少", "desc"),
|
||
("未服务天数从少到多", "asc"),
|
||
)
|
||
)
|
||
|
||
secondary_filters = QWidget(filters)
|
||
secondary_filters.setObjectName("DiagnosisSecondaryFilters")
|
||
secondary_layout = QHBoxLayout(secondary_filters)
|
||
secondary_layout.setContentsMargins(0, 0, 0, 0)
|
||
secondary_layout.setSpacing(8)
|
||
secondary_layout.addWidget(self._filter_label("确认诊单:"))
|
||
secondary_layout.addWidget(self.diagnosis_confirmed_combo)
|
||
secondary_layout.addWidget(self._filter_label("部门:"))
|
||
secondary_layout.addWidget(self.department_combo)
|
||
self.keyword_edit = QLineEdit(secondary_filters)
|
||
self.keyword_edit.setObjectName("DiagnosisKeyword")
|
||
self.keyword_edit.setPlaceholderText("搜索患者姓名、手机号、病历号")
|
||
self.keyword_edit.setClearButtonEnabled(True)
|
||
self.keyword_edit.setMinimumWidth(240)
|
||
self.keyword_edit.setMaximumWidth(380)
|
||
self.keyword_edit.returnPressed.connect(self._search)
|
||
secondary_layout.addWidget(self.keyword_edit, 1)
|
||
secondary_layout.addStretch(1)
|
||
self.more_filter_button = QToolButton(secondary_filters)
|
||
self.more_filter_button.setObjectName("DiagnosisMoreFilter")
|
||
self.more_filter_button.setText("更多筛选")
|
||
self.more_filter_button.setArrowType(Qt.ArrowType.DownArrow)
|
||
self.more_filter_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
||
self.more_filter_button.setCheckable(True)
|
||
self.more_filter_button.clicked.connect(self._toggle_advanced_filters)
|
||
secondary_layout.addWidget(self.more_filter_button)
|
||
filter_layout.addWidget(secondary_filters)
|
||
|
||
quick_frame = QFrame()
|
||
quick_frame.setObjectName("DiagnosisQuickFilters")
|
||
quick_layout = QVBoxLayout(quick_frame)
|
||
quick_layout.setContentsMargins(0, 0, 0, 0)
|
||
quick_layout.setSpacing(0)
|
||
self.quick_filter_flow = FlowWidget(horizontal_spacing=6, vertical_spacing=6)
|
||
self.quick_filter_flow.flow.addWidget(self._filter_label("挂号"))
|
||
self.appointment_filter_buttons: dict[str, DiagnosisChip] = {}
|
||
for label, value in (("全部", ""), ("已挂号", "1"), ("未挂号", "0")):
|
||
button = DiagnosisChip(label, small=True)
|
||
button.clicked.connect(
|
||
lambda _checked=False, selected=value: self._choose_appointment_filter(selected)
|
||
)
|
||
self.appointment_filter_buttons[value] = button
|
||
self.quick_filter_flow.flow.addWidget(button)
|
||
divider = QFrame()
|
||
divider.setObjectName("DiagnosisFilterDivider")
|
||
self.quick_filter_flow.flow.addWidget(divider)
|
||
self.quick_filter_flow.flow.addWidget(self._filter_label("确认"))
|
||
self.confirmation_filter_buttons: dict[str, DiagnosisChip] = {}
|
||
for label, value in (("全部", ""), ("已确认", "1"), ("未确认", "0")):
|
||
button = DiagnosisChip(label, small=True)
|
||
button.clicked.connect(
|
||
lambda _checked=False, selected=value: self._choose_confirmation_filter(selected)
|
||
)
|
||
self.confirmation_filter_buttons[value] = button
|
||
self.quick_filter_flow.flow.addWidget(button)
|
||
secondary_divider = QFrame()
|
||
secondary_divider.setObjectName("DiagnosisFilterDivider")
|
||
self.quick_filter_flow.flow.addWidget(secondary_divider)
|
||
self.quick_filter_flow.flow.addWidget(self.pending_booking_button)
|
||
self.quick_filter_flow.flow.addWidget(self.pending_assign_wrap)
|
||
quick_layout.addWidget(self.quick_filter_flow)
|
||
|
||
self.advanced_filters = QFrame()
|
||
self.advanced_filters.setObjectName("DiagnosisAdvancedFilters")
|
||
advanced_layout = QVBoxLayout(self.advanced_filters)
|
||
advanced_layout.setContentsMargins(0, 8, 0, 0)
|
||
advanced_layout.setSpacing(8)
|
||
advanced_layout.addWidget(quick_frame)
|
||
self.advanced_filter_flow = FlowWidget(horizontal_spacing=10, vertical_spacing=10)
|
||
self.diagnosis_type_combo = self._editable_combo("诊断类型")
|
||
self.diagnosis_type_combo.setMinimumWidth(148)
|
||
self.syndrome_combo = self._editable_combo("证型")
|
||
self.syndrome_combo.setMinimumWidth(148)
|
||
self.assistant_combo = self._editable_combo("医助")
|
||
self.assistant_combo.setMinimumWidth(148)
|
||
for combo in (
|
||
self.diagnosis_type_combo,
|
||
self.syndrome_combo,
|
||
self.assistant_combo,
|
||
):
|
||
combo.setProperty("smallControl", True)
|
||
self.latest_appointment_start_date = self._optional_date("最近挂号开始")
|
||
self.latest_appointment_end_date = self._optional_date("最近挂号结束")
|
||
self.channel_combo = self._editable_combo("最近挂号渠道")
|
||
self.channel_combo.setProperty("smallControl", True)
|
||
self.channel_combo.setFixedWidth(170)
|
||
self.latest_assign_start_date = self._optional_date("最近指派开始")
|
||
self.latest_assign_end_date = self._optional_date("最近指派结束")
|
||
self.advanced_filter_flow.flow.addWidget(self.diagnosis_type_combo)
|
||
self.advanced_filter_flow.flow.addWidget(self.syndrome_combo)
|
||
self.advanced_filter_flow.flow.addWidget(self.assistant_combo)
|
||
self.advanced_filter_flow.flow.addWidget(
|
||
self._date_range_field(
|
||
"最近挂号", self.latest_appointment_start_date, self.latest_appointment_end_date
|
||
)
|
||
)
|
||
self.advanced_filter_flow.flow.addWidget(
|
||
self._date_range_field(
|
||
"最近指派", self.latest_assign_start_date, self.latest_assign_end_date
|
||
)
|
||
)
|
||
self.advanced_filter_flow.flow.addWidget(self.channel_combo)
|
||
self.unserved_sort_combo.setParent(self.advanced_filters)
|
||
self.unserved_sort_combo.hide()
|
||
self.unserved_sort_combo.currentIndexChanged.connect(self._server_sort_changed)
|
||
advanced_layout.addWidget(self.advanced_filter_flow)
|
||
self.advanced_filters.hide()
|
||
filter_layout.addWidget(self.advanced_filters)
|
||
filters.setFixedHeight(_FILTERS_COLLAPSED_HEIGHT)
|
||
page_layout.addWidget(filters)
|
||
|
||
card = QFrame()
|
||
card.setObjectName("DiagnosisListCard")
|
||
card.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||
self.list_card = card
|
||
card_layout = QVBoxLayout(card)
|
||
card_layout.setContentsMargins(0, 0, 0, 0)
|
||
card_layout.setSpacing(0)
|
||
toolbar = QFrame()
|
||
toolbar.setObjectName("DiagnosisListToolbar")
|
||
toolbar_layout = QHBoxLayout(toolbar)
|
||
toolbar_layout.setContentsMargins(12, 4, 12, 4)
|
||
toolbar_layout.setSpacing(6)
|
||
self.add_button = QPushButton("+ 新增患者", toolbar)
|
||
self.add_button.setProperty("variant", "primary")
|
||
self.add_button.setVisible(_canonical_allowed(permissions, "tcm.diagnosis/add"))
|
||
self.add_button.clicked.connect(self._add_diagnosis)
|
||
toolbar_layout.addWidget(self.add_button)
|
||
self.video_qr_toolbar_button = QPushButton("视频二维码", toolbar)
|
||
self.video_qr_toolbar_button.setProperty("consultationTool", True)
|
||
self.video_qr_toolbar_button.setVisible(
|
||
bool(
|
||
_canonical_allowed(permissions, "tcm.diagnosis/videoQr")
|
||
and self._repository_methods["video_qr"]
|
||
)
|
||
)
|
||
self.video_qr_toolbar_button.clicked.connect(self._request_video_qr)
|
||
toolbar_layout.addWidget(self.video_qr_toolbar_button)
|
||
self.call_toolbar_button = QPushButton("通话", toolbar)
|
||
self.call_toolbar_button.setProperty("consultationTool", True)
|
||
self.call_toolbar_button.setVisible(self._native_video_capable)
|
||
self.call_toolbar_button.clicked.connect(self._request_video)
|
||
toolbar_layout.addWidget(self.call_toolbar_button)
|
||
self.complete_toolbar_button = QPushButton("完成", toolbar)
|
||
self.complete_toolbar_button.setProperty("consultationTool", True)
|
||
self.complete_toolbar_button.setToolTip("查看当前问诊详情并完成问诊")
|
||
self.complete_toolbar_button.clicked.connect(self._open_readonly)
|
||
toolbar_layout.addWidget(self.complete_toolbar_button)
|
||
self.prescription_toolbar_button = QPushButton("开方", toolbar)
|
||
self.prescription_toolbar_button.setProperty("consultationTool", True)
|
||
self.prescription_toolbar_button.setVisible(
|
||
_canonical_allowed(permissions, "tcm.diagnosis/kaifang")
|
||
)
|
||
self.prescription_toolbar_button.clicked.connect(self._open_prescription)
|
||
toolbar_layout.addWidget(self.prescription_toolbar_button)
|
||
self.case_toolbar_button = QPushButton("病历", toolbar)
|
||
self.case_toolbar_button.setProperty("consultationTool", True)
|
||
self.case_toolbar_button.clicked.connect(self._open_readonly)
|
||
toolbar_layout.addWidget(self.case_toolbar_button)
|
||
self.cancel_toolbar_button = QPushButton("取消挂号", toolbar)
|
||
self.cancel_toolbar_button.setProperty("consultationDanger", True)
|
||
self.cancel_toolbar_button.setVisible(
|
||
bool(
|
||
_canonical_allowed(permissions, "tcm.diagnosis/guahao")
|
||
and self._repository_methods["cancel_appointment"]
|
||
)
|
||
)
|
||
self.cancel_toolbar_button.clicked.connect(self._cancel_selected_appointment)
|
||
toolbar_layout.addWidget(self.cancel_toolbar_button)
|
||
self.batch_assign_button = QPushButton("批量指派医助", toolbar)
|
||
self.batch_assign_button.setProperty("variant", "success")
|
||
self.batch_assign_button.setVisible(_canonical_allowed(permissions, "tcm.diagnosis/assign"))
|
||
self.batch_assign_button.clicked.connect(self._batch_assign)
|
||
toolbar_layout.addWidget(self.batch_assign_button)
|
||
self.batch_assign_button.hide()
|
||
self.batch_cancel_assign_button = QPushButton("批量取消指派", toolbar)
|
||
self.batch_cancel_assign_button.setProperty("variant", "warning")
|
||
self.batch_cancel_assign_button.setVisible(
|
||
_canonical_allowed(permissions, "tcm.diagnosis/assign")
|
||
)
|
||
self.batch_cancel_assign_button.clicked.connect(self._batch_cancel_assign)
|
||
toolbar_layout.addWidget(self.batch_cancel_assign_button)
|
||
self.batch_cancel_assign_button.hide()
|
||
toolbar_layout.addStretch(1)
|
||
self.selected_label = QLabel("")
|
||
self.selected_label.setProperty("pagerMuted", True)
|
||
self.selected_label.hide()
|
||
toolbar_layout.addWidget(self.selected_label)
|
||
card_layout.addWidget(toolbar)
|
||
|
||
self.banner = _ToastBanner(self, card)
|
||
self.summary_label = QLabel("等待加载", card)
|
||
self.summary_label.hide()
|
||
|
||
# These buttons retain the public compatibility API used by the shell
|
||
# and tests. The visible equivalents live in each fixed action cell.
|
||
self._compat_actions = QWidget(card)
|
||
self._compat_actions.hide()
|
||
self.view_button = QPushButton("查看", self._compat_actions)
|
||
self.view_button.setVisible(_canonical_allowed(permissions, "tcm.diagnosis/readonlyDetail"))
|
||
self.view_button.clicked.connect(self._open_readonly)
|
||
self.edit_button = QPushButton("诊单", self._compat_actions)
|
||
self.edit_button.setVisible(_canonical_allowed(permissions, "tcm.diagnosis/edit"))
|
||
self.edit_button.clicked.connect(self._open_edit)
|
||
self.prescription_button = QPushButton("开方", self._compat_actions)
|
||
self.prescription_button.setVisible(
|
||
_canonical_allowed(permissions, "tcm.diagnosis/kaifang")
|
||
)
|
||
self.prescription_button.clicked.connect(self._open_prescription)
|
||
self.void_button = QPushButton("作废处方", self._compat_actions)
|
||
self.void_button.setVisible(_canonical_allowed(permissions, "tcm.diagnosis/kaifang"))
|
||
self.void_button.clicked.connect(self._void_selected_prescription)
|
||
self.video_button = QPushButton("进入视频问诊", self._compat_actions)
|
||
self.video_button.setVisible(self._native_video_capable)
|
||
self.video_button.clicked.connect(self._request_video)
|
||
self.delete_button = QPushButton("删除", self._compat_actions)
|
||
self.delete_button.setVisible(_canonical_allowed(permissions, "tcm.diagnosis/delete"))
|
||
self.delete_button.clicked.connect(self._delete_selected)
|
||
self.refresh_button = QPushButton("刷新", toolbar)
|
||
self.refresh_button.clicked.connect(lambda: self.refresh())
|
||
toolbar_layout.addWidget(self.refresh_button)
|
||
|
||
table_wrap = QWidget()
|
||
table_wrap.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||
table_wrap_layout = QVBoxLayout(table_wrap)
|
||
table_wrap_layout.setContentsMargins(12, 4, 12, 0)
|
||
table_wrap_layout.setSpacing(0)
|
||
self.table_host = DiagnosisTableHost(
|
||
action_policy={
|
||
"view": _canonical_allowed(permissions, "tcm.diagnosis/readonlyDetail"),
|
||
"edit": _canonical_allowed(permissions, "tcm.diagnosis/edit"),
|
||
"prescription": _canonical_allowed(permissions, "tcm.diagnosis/kaifang"),
|
||
"ai_consult": can_open_ai_consult(permissions),
|
||
"appointment": _canonical_allowed(permissions, "tcm.diagnosis/guahao"),
|
||
"assign": _canonical_allowed(permissions, "tcm.diagnosis/assign"),
|
||
"delete": _canonical_allowed(permissions, "tcm.diagnosis/delete"),
|
||
"video_call": self._native_video_capable,
|
||
"appointment_cancel": bool(
|
||
_canonical_allowed(permissions, "tcm.diagnosis/guahao")
|
||
and self._repository_methods["cancel_appointment"]
|
||
),
|
||
"video_qr": bool(
|
||
_canonical_allowed(permissions, "tcm.diagnosis/videoQr")
|
||
and self._repository_methods["video_qr"]
|
||
),
|
||
"confirm_qr": bool(
|
||
_canonical_allowed(permissions, "tcm.diagnosis/guahao")
|
||
and self._repository_methods["confirm_qr"]
|
||
),
|
||
"appointment_logs": bool(
|
||
_canonical_allowed(permissions, "tcm.diagnosis/guahaoLogList")
|
||
and self._repository_methods["appointment_logs"]
|
||
),
|
||
"create_order": bool(
|
||
_canonical_allowed(permissions, "tcm.diagnosis/order")
|
||
and self._repository_methods["create_order"]
|
||
and self._repository_methods["order_qr"]
|
||
),
|
||
}
|
||
)
|
||
self.table = self.table_host.main
|
||
self.table.horizontalHeader().setSectionResizeMode(9, QHeaderView.ResizeMode.Stretch)
|
||
# The shared host's gradient is useful on dark pages but renders as a
|
||
# heavy black strip in this light reference layout.
|
||
self.table_host.fixed_shadow.hide()
|
||
self.table.itemSelectionChanged.connect(self._selection_changed)
|
||
self.table.itemDoubleClicked.connect(lambda _item: self._open_readonly())
|
||
self.table_host.action_requested.connect(self._row_action)
|
||
self.table_host.appointment_cancel_requested.connect(self._cancel_appointment_item)
|
||
self.table_host.video_requested.connect(self._row_video)
|
||
self.table_host.checked_changed.connect(self._checked_changed)
|
||
self.table_host.sort_unserved_requested.connect(self._sort_unserved)
|
||
table_wrap_layout.addWidget(self.table_host, 1)
|
||
self.loading_overlay = DiagnosisLoadingOverlay(self.table_host)
|
||
card_layout.addWidget(table_wrap, 1)
|
||
self.pager = DiagnosisPager(self._page_size)
|
||
self.pager.page_changed.connect(self._change_page)
|
||
self.pager.page_size_changed.connect(self._change_page_size)
|
||
card_layout.addWidget(self.pager)
|
||
page_layout.addWidget(card, 1)
|
||
|
||
self.poll_timer = QTimer(self)
|
||
self.poll_timer.setInterval(20_000)
|
||
self.poll_timer.timeout.connect(self._poll_refresh)
|
||
self._update_quick_buttons()
|
||
self._update_secondary_chips()
|
||
self._checked_changed(0)
|
||
self._selection_changed()
|
||
|
||
@staticmethod
|
||
def _fixed_combo(options: Sequence[tuple[str, Any]]) -> QComboBox:
|
||
combo = QComboBox()
|
||
for label, value in options:
|
||
combo.addItem(label, value)
|
||
return combo
|
||
|
||
@staticmethod
|
||
def _editable_combo(placeholder: str) -> QComboBox:
|
||
combo = QComboBox()
|
||
combo.setEditable(True)
|
||
combo.addItem(placeholder, "")
|
||
if combo.lineEdit() is not None:
|
||
combo.lineEdit().setPlaceholderText(placeholder)
|
||
return combo
|
||
|
||
@staticmethod
|
||
def _optional_date(tooltip: str) -> QDateEdit:
|
||
editor = QDateEdit()
|
||
editor.setCalendarPopup(True)
|
||
editor.setDisplayFormat("yyyy-MM-dd")
|
||
editor.setMinimumDate(_OPTIONAL_DATE_MINIMUM)
|
||
editor.setSpecialValueText(tooltip)
|
||
editor.setDate(_OPTIONAL_DATE_MINIMUM)
|
||
editor.setToolTip(tooltip)
|
||
return editor
|
||
|
||
@staticmethod
|
||
def _filter_label(text: str) -> QLabel:
|
||
label = QLabel(text)
|
||
label.setProperty("filterGroup", True)
|
||
return label
|
||
|
||
def _filter_field(self, label_text: str, control: QWidget) -> QWidget:
|
||
host = QWidget()
|
||
layout = QHBoxLayout(host)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
layout.setSpacing(5)
|
||
layout.addWidget(self._filter_label(label_text))
|
||
layout.addWidget(control)
|
||
return host
|
||
|
||
def _date_range_field(
|
||
self,
|
||
label_text: str,
|
||
start: QDateEdit,
|
||
end: QDateEdit,
|
||
) -> QWidget:
|
||
start.setProperty("diagnosisRangePart", True)
|
||
end.setProperty("diagnosisRangePart", True)
|
||
host = QFrame()
|
||
host.setObjectName("DiagnosisDateRange")
|
||
host.setAccessibleName(label_text)
|
||
host.setFixedWidth(260)
|
||
layout = QHBoxLayout(host)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
layout.setSpacing(0)
|
||
layout.addWidget(start, 1)
|
||
separator = QLabel("—")
|
||
separator.setProperty("diagnosisRangeSeparator", True)
|
||
separator.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
separator.setFixedWidth(14)
|
||
layout.addWidget(separator)
|
||
layout.addWidget(end, 1)
|
||
return host
|
||
|
||
def _pending_assign_month_value(self) -> str:
|
||
return self.pending_assign_month.date().toString("yyyy-MM")
|
||
|
||
def _pending_assign_month_changed(self) -> None:
|
||
if self._pending_assign:
|
||
self._page = 1
|
||
self.refresh()
|
||
|
||
@staticmethod
|
||
def _date_value(editor: QDateEdit) -> str:
|
||
if editor.date() == _OPTIONAL_DATE_MINIMUM:
|
||
return ""
|
||
return editor.date().toString("yyyy-MM-dd")
|
||
|
||
@staticmethod
|
||
def _combo_value(combo: QComboBox) -> Any:
|
||
data = combo.currentData()
|
||
if combo.isEditable() and combo.currentIndex() < 0:
|
||
return combo.currentText().strip()
|
||
return "" if data is None else data
|
||
|
||
def _assistant_cell(self, _value: Any, row: Any) -> str:
|
||
name = first_value(row, "assistant_name", default="")
|
||
if name:
|
||
return display_text(name)
|
||
assistant_id = _as_int(first_value(row, "assistant_id", "assistant", default=0))
|
||
return self._assistant_names.get(assistant_id, "—")
|
||
|
||
def _custom_date_changed(self, value: QDate) -> None:
|
||
if value == _OPTIONAL_DATE_MINIMUM:
|
||
return
|
||
self._choose_date(value.toString("yyyy-MM-dd"))
|
||
|
||
def _choose_status(self, value: str) -> None:
|
||
if value not in self.status_buttons:
|
||
return
|
||
self._appointment_status_filter = value
|
||
self._pending_booking = ""
|
||
self._pending_assign = ""
|
||
self._completed_appointment = "1" if value == "3" else ""
|
||
self.has_appointment_combo.setCurrentIndex(1 if value else 0)
|
||
self._update_quick_buttons()
|
||
self._update_secondary_chips()
|
||
self._page = 1
|
||
self.refresh()
|
||
|
||
def _confirmation_combo_changed(self, _index: int) -> None:
|
||
self._pending_booking = ""
|
||
self._pending_assign = ""
|
||
self._completed_appointment = "1" if self._appointment_status_filter == "3" else ""
|
||
self._update_quick_buttons()
|
||
self._update_secondary_chips()
|
||
self._page = 1
|
||
self.refresh()
|
||
|
||
def _choose_appointment_filter(self, value: str) -> None:
|
||
index = self.has_appointment_combo.findData(value)
|
||
self.has_appointment_combo.setCurrentIndex(max(0, index))
|
||
self._appointment_status_filter = ""
|
||
self._pending_booking = ""
|
||
self._completed_appointment = ""
|
||
if value == "0":
|
||
self._clear_latest_appointment_filters()
|
||
self._update_quick_buttons()
|
||
self._update_secondary_chips()
|
||
self._page = 1
|
||
self.refresh()
|
||
|
||
def _choose_confirmation_filter(self, value: str) -> None:
|
||
index = self.diagnosis_confirmed_combo.findData(value)
|
||
self.diagnosis_confirmed_combo.setCurrentIndex(max(0, index))
|
||
self._pending_booking = ""
|
||
self._pending_assign = ""
|
||
self._completed_appointment = "1" if self._appointment_status_filter == "3" else ""
|
||
self._update_quick_buttons()
|
||
self._update_secondary_chips()
|
||
self._page = 1
|
||
self.refresh()
|
||
|
||
def _update_secondary_chips(self) -> None:
|
||
appointment_value = str(self._combo_value(self.has_appointment_combo))
|
||
confirmation_value = str(self._combo_value(self.diagnosis_confirmed_combo))
|
||
for value, button in self.appointment_filter_buttons.items():
|
||
button.setChecked(value == appointment_value)
|
||
for value, button in self.confirmation_filter_buttons.items():
|
||
button.setChecked(value == confirmation_value)
|
||
direction = str(self._combo_value(self.unserved_sort_combo))
|
||
if hasattr(self, "table_host"):
|
||
self.table_host.set_sort_direction(direction)
|
||
|
||
def _toggle_advanced_filters(self, checked: bool) -> None:
|
||
self.advanced_filters.setVisible(checked)
|
||
self.filters_card.setFixedHeight(
|
||
_FILTERS_COLLAPSED_HEIGHT + self.advanced_filters.sizeHint().height() + 8
|
||
if checked
|
||
else _FILTERS_COLLAPSED_HEIGHT
|
||
)
|
||
self.more_filter_button.setText("收起" if checked else "更多筛选")
|
||
self.more_filter_button.setArrowType(
|
||
Qt.ArrowType.UpArrow if checked else Qt.ArrowType.DownArrow
|
||
)
|
||
self.main_chip_flow.updateGeometry()
|
||
self.quick_filter_flow.updateGeometry()
|
||
|
||
def _server_sort_changed(self, _index: int) -> None:
|
||
if not hasattr(self, "table_host"):
|
||
return
|
||
direction = str(self._combo_value(self.unserved_sort_combo))
|
||
self.table_host.set_sort_direction(direction)
|
||
|
||
def _sort_unserved(self, direction: str) -> None:
|
||
index = self.unserved_sort_combo.findData(direction)
|
||
self.unserved_sort_combo.setCurrentIndex(max(0, index))
|
||
self._page = 1
|
||
self.refresh(silent=True)
|
||
|
||
def _choose_date(self, value: str) -> None:
|
||
self._appointment_date = value
|
||
self._pending_booking = ""
|
||
self._pending_assign = ""
|
||
self._completed_appointment = "1" if self._appointment_status_filter == "3" else ""
|
||
self.has_appointment_combo.setCurrentIndex(1 if self._appointment_status_filter else 0)
|
||
self._clear_latest_appointment_filters()
|
||
blocked = self.custom_date_edit.blockSignals(True)
|
||
if value not in self.date_buttons:
|
||
parsed = QDate.fromString(value, "yyyy-MM-dd")
|
||
self.custom_date_edit.setDate(
|
||
parsed if parsed.isValid() else _OPTIONAL_DATE_MINIMUM
|
||
)
|
||
else:
|
||
self.custom_date_edit.setDate(_OPTIONAL_DATE_MINIMUM)
|
||
self.custom_date_edit.blockSignals(blocked)
|
||
self._update_quick_buttons()
|
||
self._update_secondary_chips()
|
||
self._page = 1
|
||
self.refresh()
|
||
|
||
def _choose_pending_booking(self) -> None:
|
||
self._appointment_date = ""
|
||
self._appointment_status_filter = ""
|
||
self._pending_booking = "1"
|
||
self._completed_appointment = ""
|
||
self._pending_assign = ""
|
||
self.has_appointment_combo.setCurrentIndex(2)
|
||
self._clear_latest_appointment_filters()
|
||
self._update_quick_buttons()
|
||
self._update_secondary_chips()
|
||
self._page = 1
|
||
self.refresh()
|
||
|
||
def _choose_completed(self) -> None:
|
||
self._choose_status("3")
|
||
|
||
def _choose_pending_assign(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/assign"):
|
||
return
|
||
self._appointment_date = ""
|
||
self._appointment_status_filter = ""
|
||
self._pending_booking = ""
|
||
self._completed_appointment = ""
|
||
self._pending_assign = "1"
|
||
self.has_appointment_combo.setCurrentIndex(0)
|
||
self._clear_latest_appointment_filters()
|
||
if not self.pending_assign_month.date().isValid():
|
||
self.pending_assign_month.setDate(QDate.currentDate())
|
||
self._update_quick_buttons()
|
||
self._update_secondary_chips()
|
||
self._page = 1
|
||
self.refresh()
|
||
|
||
def _update_quick_buttons(self) -> None:
|
||
normal_date_mode = not any((self._pending_booking, self._pending_assign))
|
||
for value, button in self.date_buttons.items():
|
||
button.setChecked(normal_date_mode and self._appointment_date == value)
|
||
base = self._date_button_labels[value]
|
||
button.setText(base)
|
||
for value, button in self.status_buttons.items():
|
||
button.setChecked(
|
||
not self._pending_booking
|
||
and not self._pending_assign
|
||
and value == self._appointment_status_filter
|
||
)
|
||
count = self._status_counts.get(value)
|
||
base = self._status_button_labels[value]
|
||
button.setText(f"{base} ({count})" if count is not None else base)
|
||
self.pending_booking_button.setChecked(bool(self._pending_booking))
|
||
self.pending_assign_button.setChecked(bool(self._pending_assign))
|
||
pending_count = self._date_counts.get("pending_booking")
|
||
assign_count = self._date_counts.get("pending_assign")
|
||
self.pending_booking_button.setText(
|
||
f"待预约 {pending_count}" if pending_count is not None else "待预约"
|
||
)
|
||
self.pending_assign_button.setText(
|
||
f"待分配医助 {assign_count}" if assign_count is not None else "待分配医助"
|
||
)
|
||
self.pending_assign_filters.setVisible(bool(self._pending_assign))
|
||
for flow in (self.main_chip_flow, self.quick_filter_flow):
|
||
flow.updateGeometry()
|
||
|
||
def _clear_latest_appointment_filters(self) -> None:
|
||
self.latest_appointment_start_date.setDate(_OPTIONAL_DATE_MINIMUM)
|
||
self.latest_appointment_end_date.setDate(_OPTIONAL_DATE_MINIMUM)
|
||
self.channel_combo.setCurrentIndex(0)
|
||
|
||
def _reset(self) -> None:
|
||
self.keyword_edit.clear()
|
||
self.patient_name_edit.clear()
|
||
self.doctor_edit.clear()
|
||
for combo in (
|
||
self.has_appointment_combo,
|
||
self.diagnosis_confirmed_combo,
|
||
self.department_combo,
|
||
self.diagnosis_type_combo,
|
||
self.syndrome_combo,
|
||
self.assistant_combo,
|
||
self.channel_combo,
|
||
self.unserved_sort_combo,
|
||
):
|
||
combo.setCurrentIndex(0)
|
||
for editor in (
|
||
self.latest_appointment_start_date,
|
||
self.latest_appointment_end_date,
|
||
self.latest_assign_start_date,
|
||
self.latest_assign_end_date,
|
||
):
|
||
editor.setDate(_OPTIONAL_DATE_MINIMUM)
|
||
self._appointment_date = ""
|
||
self._appointment_status_filter = ""
|
||
self._pending_booking = ""
|
||
self._completed_appointment = ""
|
||
self._pending_assign = ""
|
||
blocked = self.pending_assign_month.blockSignals(True)
|
||
self.pending_assign_month.setDate(QDate.currentDate())
|
||
self.pending_assign_month.blockSignals(blocked)
|
||
self.pending_assign_keyword.clear()
|
||
blocked = self.custom_date_edit.blockSignals(True)
|
||
self.custom_date_edit.setDate(_OPTIONAL_DATE_MINIMUM)
|
||
self.custom_date_edit.blockSignals(blocked)
|
||
self._update_quick_buttons()
|
||
self._update_secondary_chips()
|
||
self._page = 1
|
||
self.refresh()
|
||
|
||
def _validate_ranges(self) -> bool:
|
||
pairs = (
|
||
(
|
||
self._date_value(self.latest_appointment_start_date),
|
||
self._date_value(self.latest_appointment_end_date),
|
||
"最近挂号",
|
||
),
|
||
(
|
||
self._date_value(self.latest_assign_start_date),
|
||
self._date_value(self.latest_assign_end_date),
|
||
"最近指派",
|
||
),
|
||
)
|
||
for start, end, label in pairs:
|
||
if start and end and start > end:
|
||
self.banner.show_message(f"{label}开始日期不能晚于结束日期。", "warning")
|
||
return False
|
||
return True
|
||
|
||
def _search(self) -> None:
|
||
if not self._validate_ranges():
|
||
return
|
||
if self._pending_assign and (
|
||
self.pending_assign_keyword.text().strip()
|
||
or self.patient_name_edit.text().strip()
|
||
or self.keyword_edit.text().strip()
|
||
):
|
||
self._appointment_date = ""
|
||
self._pending_booking = ""
|
||
self._completed_appointment = ""
|
||
self.diagnosis_confirmed_combo.setCurrentIndex(0)
|
||
self._clear_latest_appointment_filters()
|
||
if (
|
||
self._date_value(self.latest_appointment_start_date)
|
||
or self._date_value(self.latest_appointment_end_date)
|
||
or self._combo_value(self.channel_combo)
|
||
):
|
||
self._appointment_date = ""
|
||
self._pending_booking = ""
|
||
self._completed_appointment = (
|
||
"1" if self._appointment_status_filter == "3" else ""
|
||
)
|
||
if self._combo_value(self.has_appointment_combo) == "0":
|
||
self.has_appointment_combo.setCurrentIndex(0)
|
||
self._update_quick_buttons()
|
||
self._update_secondary_chips()
|
||
self._page = 1
|
||
self.refresh()
|
||
|
||
def _change_page(self, page: int) -> None:
|
||
self._page = page
|
||
self.refresh(silent=True)
|
||
|
||
def _change_page_size(self, page_size: int) -> None:
|
||
if page_size not in {15, 20, 30, 40}:
|
||
return
|
||
self._page_size = page_size
|
||
self._page = 1
|
||
self.refresh(silent=True)
|
||
|
||
def _shared_filters(self) -> dict[str, Any]:
|
||
return {
|
||
"keyword": self.keyword_edit.text().strip(),
|
||
"patient_name": self.patient_name_edit.text().strip(),
|
||
"doctor_name": self.doctor_edit.text().strip(),
|
||
"department_id": self._combo_value(self.department_combo),
|
||
"has_appointment": self._combo_value(self.has_appointment_combo),
|
||
"diagnosis_confirmed": self._combo_value(self.diagnosis_confirmed_combo),
|
||
"diagnosis_type": self._combo_value(self.diagnosis_type_combo),
|
||
"syndrome_type": self._combo_value(self.syndrome_combo),
|
||
"assistant_id": self._combo_value(self.assistant_combo),
|
||
"latest_appointment_start_date": self._date_value(self.latest_appointment_start_date),
|
||
"latest_appointment_end_date": self._date_value(self.latest_appointment_end_date),
|
||
"latest_appointment_channel_source": self._combo_value(self.channel_combo),
|
||
"latest_assign_start_date": self._date_value(self.latest_assign_start_date),
|
||
"latest_assign_end_date": self._date_value(self.latest_assign_end_date),
|
||
}
|
||
|
||
def _filters(self) -> dict[str, Any]:
|
||
filters = self._shared_filters()
|
||
filters.update(
|
||
{
|
||
"appointment_date": self._appointment_date,
|
||
"appointment_status": (
|
||
self._appointment_status_filter
|
||
if self._appointment_status_filter != "3"
|
||
else ""
|
||
),
|
||
"pending_booking": self._pending_booking,
|
||
"completed_appointment": self._completed_appointment,
|
||
"pending_assign": self._pending_assign,
|
||
"sort_unserved_days": self._combo_value(self.unserved_sort_combo),
|
||
}
|
||
)
|
||
if self._pending_assign:
|
||
pending_keyword = self.pending_assign_keyword.text().strip()
|
||
keyword = (
|
||
self.patient_name_edit.text().strip() or self.keyword_edit.text().strip()
|
||
)
|
||
if pending_keyword or keyword:
|
||
return {
|
||
"pending_assign": "1",
|
||
"pending_assign_keyword": pending_keyword or keyword,
|
||
}
|
||
filters["pending_assign_order_month"] = self._pending_assign_month_value()
|
||
filters["pending_assign_keyword"] = ""
|
||
return filters
|
||
|
||
def refresh(self, silent: bool = False) -> None:
|
||
"""Refresh without allowing an older async request to overwrite newer filters."""
|
||
|
||
if self._order_flow_generation is not None:
|
||
return
|
||
if not self._validate_ranges():
|
||
return
|
||
self._menu_generation += 1
|
||
self._generation += 1
|
||
generation = self._generation
|
||
self._loading = True
|
||
self.refresh_button.setEnabled(False)
|
||
filters = MappingProxyType(self._filters())
|
||
page = self._page
|
||
if not silent:
|
||
self._visible_loading_generation = generation
|
||
self.table_host.begin_loading()
|
||
self.loading_overlay.start()
|
||
self._refresh_counts()
|
||
elif self.loading_overlay.isVisible():
|
||
self._visible_loading_generation = generation
|
||
run_async(
|
||
lambda: invoke(
|
||
self.repository,
|
||
"consultations",
|
||
**filters,
|
||
page=page,
|
||
page_size=self._page_size,
|
||
),
|
||
on_success=lambda result: self._apply_result(result, generation),
|
||
on_error=lambda error: self._load_error(error, generation),
|
||
on_finished=lambda: self._load_finished(generation),
|
||
)
|
||
|
||
def _apply_result(self, result: Any, generation: int) -> None:
|
||
if generation != self._generation:
|
||
return
|
||
rows = page_items(result)
|
||
total = page_total(result, len(rows))
|
||
last_page = max(1, (total + self._page_size - 1) // self._page_size)
|
||
if self._page > last_page:
|
||
self._page = last_page
|
||
self.refresh(silent=True)
|
||
return
|
||
self.table_host.force_open_prescription = bool(self._pending_assign)
|
||
self.table_host.set_rows(rows)
|
||
self.pager.update_state(self._page, total)
|
||
self.summary_label.setText(
|
||
f"共 {total} 条 · 第 {self._page} 页 · 每页 {self._page_size} 条"
|
||
)
|
||
self.banner.clear()
|
||
self._selection_changed()
|
||
|
||
def _load_error(self, error: Exception, generation: int) -> None:
|
||
if generation == self._generation:
|
||
message = friendly_error(error)
|
||
self.banner.show_message(message, "danger")
|
||
if self.table.rowCount() == 0:
|
||
self.table_host.show_error(f"列表加载失败,请重试\n{message}")
|
||
|
||
def _load_finished(self, generation: int) -> None:
|
||
if generation == self._generation:
|
||
self._loading = False
|
||
self.refresh_button.setEnabled(True)
|
||
if self._visible_loading_generation == generation:
|
||
self.loading_overlay.stop()
|
||
self._visible_loading_generation = 0
|
||
|
||
def _refresh_counts(self) -> None:
|
||
self._count_generation += 1
|
||
generation = self._count_generation
|
||
shared = self._shared_filters()
|
||
dates = list(self.date_buttons)
|
||
requests: list[tuple[str, Mapping[str, Any]]] = []
|
||
for value in dates:
|
||
payload = dict(shared)
|
||
payload.update(
|
||
{
|
||
"appointment_date": value,
|
||
"appointment_status": (
|
||
self._appointment_status_filter
|
||
if self._appointment_status_filter != "3"
|
||
else ""
|
||
),
|
||
"pending_booking": "",
|
||
"completed_appointment": (
|
||
"1" if self._appointment_status_filter == "3" else ""
|
||
),
|
||
"pending_assign": "",
|
||
"sort_unserved_days": "",
|
||
}
|
||
)
|
||
requests.append((value, MappingProxyType(payload)))
|
||
for value in self.status_buttons:
|
||
payload = dict(shared)
|
||
payload.update(
|
||
{
|
||
"appointment_date": self._appointment_date,
|
||
"appointment_status": value if value != "3" else "",
|
||
"pending_booking": "",
|
||
"completed_appointment": "1" if value == "3" else "",
|
||
"pending_assign": "",
|
||
"sort_unserved_days": "",
|
||
}
|
||
)
|
||
requests.append((f"status:{value or 'all'}", MappingProxyType(payload)))
|
||
pending = dict(shared)
|
||
pending.update({"appointment_date": "", "has_appointment": "0"})
|
||
requests.append(("pending_booking", MappingProxyType(pending)))
|
||
if _canonical_allowed(self.permissions, "tcm.diagnosis/assign"):
|
||
pending_keyword = self.pending_assign_keyword.text().strip()
|
||
shared_keyword = self.keyword_edit.text().strip()
|
||
if self._pending_assign and (pending_keyword or shared_keyword):
|
||
assign = {
|
||
"pending_assign": "1",
|
||
"pending_assign_keyword": pending_keyword or shared_keyword,
|
||
}
|
||
else:
|
||
assign = dict(shared)
|
||
assign.update(
|
||
{
|
||
"appointment_date": "",
|
||
"has_appointment": "",
|
||
"pending_assign": "1",
|
||
"pending_assign_order_month": self._pending_assign_month_value()
|
||
if self._pending_assign
|
||
else QDate.currentDate().toString("yyyy-MM"),
|
||
"pending_assign_keyword": pending_keyword,
|
||
}
|
||
)
|
||
requests.append(("pending_assign", MappingProxyType(assign)))
|
||
|
||
def load_counts() -> dict[str, int]:
|
||
counts: dict[str, int] = {}
|
||
for key, payload in requests:
|
||
try:
|
||
result = invoke(
|
||
self.repository,
|
||
"consultations",
|
||
**payload,
|
||
page=1,
|
||
page_size=1,
|
||
)
|
||
except Exception:
|
||
continue
|
||
counts[key] = page_total(result, len(page_items(result)))
|
||
return counts
|
||
|
||
run_async(
|
||
load_counts,
|
||
on_success=lambda counts: self._apply_counts(counts, generation),
|
||
on_error=lambda _error: None,
|
||
)
|
||
|
||
def _apply_counts(self, counts: Any, generation: int) -> None:
|
||
if generation != self._count_generation or not isinstance(counts, Mapping):
|
||
return
|
||
self._date_counts = {
|
||
str(key): _as_int(value)
|
||
for key, value in counts.items()
|
||
if value is not None and not str(key).startswith("status:")
|
||
}
|
||
self._status_counts = {
|
||
("" if str(key) == "status:all" else str(key).removeprefix("status:")): _as_int(
|
||
value
|
||
)
|
||
for key, value in counts.items()
|
||
if value is not None and str(key).startswith("status:")
|
||
}
|
||
self._update_quick_buttons()
|
||
|
||
def _load_filter_options(self) -> None:
|
||
if self._options_loaded:
|
||
return
|
||
self._options_loaded = True
|
||
self._options_generation += 1
|
||
generation = self._options_generation
|
||
|
||
def load() -> dict[str, list[Any]]:
|
||
result: dict[str, list[Any]] = {}
|
||
dictionary = getattr(self.repository, "get_dictionary", None)
|
||
if callable(dictionary):
|
||
for key in (
|
||
"diagnosis_type",
|
||
"syndrome_type",
|
||
"channels",
|
||
):
|
||
try:
|
||
value = invoke(self.repository, "get_dictionary", dictionary_type=key)
|
||
except Exception:
|
||
continue
|
||
result[key] = _option_rows(value, key)
|
||
assistants = getattr(self.repository, "list_diagnosis_assistants", None)
|
||
if callable(assistants):
|
||
with suppress(Exception):
|
||
result["assistants"] = _option_rows(
|
||
invoke(self.repository, "list_diagnosis_assistants")
|
||
)
|
||
return result
|
||
|
||
run_async(
|
||
load,
|
||
on_success=lambda options: self._apply_filter_options(options, generation),
|
||
on_error=lambda _error: None,
|
||
)
|
||
|
||
def _apply_filter_options(self, options: Any, generation: int) -> None:
|
||
if generation != self._options_generation or not isinstance(options, Mapping):
|
||
return
|
||
channel_rows = options.get("channels", options.get("appointment_channel_source", []))
|
||
if isinstance(channel_rows, Sequence) and not isinstance(channel_rows, (str, bytes)):
|
||
channel_rows = sorted(
|
||
(
|
||
row
|
||
for row in channel_rows
|
||
if _as_int(first_value(row, "status", default=1), 1) != 0
|
||
),
|
||
key=lambda row: (
|
||
_as_int(first_value(row, "sort", default=0)),
|
||
_as_int(first_value(row, "id", default=0)),
|
||
),
|
||
reverse=True,
|
||
)
|
||
mappings = (
|
||
(self.diagnosis_type_combo, options.get("diagnosis_type", [])),
|
||
(self.syndrome_combo, options.get("syndrome_type", [])),
|
||
(
|
||
self.channel_combo,
|
||
channel_rows,
|
||
),
|
||
(self.assistant_combo, options.get("assistants", [])),
|
||
)
|
||
for combo, rows in mappings:
|
||
current = self._combo_value(combo)
|
||
placeholder = combo.itemText(0)
|
||
combo.clear()
|
||
combo.addItem(placeholder, "")
|
||
for row in rows if isinstance(rows, Sequence) else []:
|
||
label = display_text(first_value(row, "name", "label", "text", default=""), "")
|
||
value = first_value(row, "value", "id", "code", default=label)
|
||
if label:
|
||
combo.addItem(label, value)
|
||
index = combo.findData(current)
|
||
if index >= 0:
|
||
combo.setCurrentIndex(index)
|
||
elif current:
|
||
combo.setEditText(str(current))
|
||
self._assistant_names = {
|
||
_as_int(first_value(row, "id", "value")): display_text(
|
||
first_value(row, "name", "label")
|
||
)
|
||
for row in options.get("assistants", [])
|
||
if _as_int(first_value(row, "id", "value")) > 0
|
||
}
|
||
|
||
def _checked_changed(self, count: int) -> None:
|
||
self.selected_label.setText(f"已选 {count} 条")
|
||
self.selected_label.setVisible(count > 0)
|
||
if hasattr(self, "batch_assign_button"):
|
||
self.batch_assign_button.setEnabled(count > 0 and not self._mutation_pending)
|
||
if hasattr(self, "batch_cancel_assign_button"):
|
||
rows = self.table_host.selected_records()
|
||
has_assigned = any(
|
||
_as_int(first_value(row, "assistant_id", "assistant", default=0)) > 0
|
||
for row in rows
|
||
)
|
||
self.batch_cancel_assign_button.setEnabled(
|
||
count > 0 and has_assigned and not self._mutation_pending
|
||
)
|
||
|
||
def _select_record(self, record: Any) -> bool:
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||
for row, candidate in enumerate(self.table_host.model.rows):
|
||
if _as_int(first_value(candidate, "diagnosis_id", "id", default=0)) == diagnosis_id:
|
||
self.table.selectRow(row)
|
||
return True
|
||
return False
|
||
|
||
def _row_action(self, action: str, record: Any) -> None:
|
||
if not self._select_record(record):
|
||
return
|
||
actions = {
|
||
"view": self._open_readonly,
|
||
"edit": self._open_edit,
|
||
"prescription": self._open_prescription,
|
||
"ai_consult": self._open_ai_consult,
|
||
"appointment": self._book_selected_appointment,
|
||
"fill_id_card": self._fill_selected_id_card,
|
||
"assign": self._assign_selected,
|
||
"cancel_assign": self._cancel_selected_assign,
|
||
"cancel_appointment": self._cancel_selected_appointment,
|
||
"video_qr": self._request_video_qr,
|
||
"confirm_qr": self._request_confirm_qr,
|
||
"appointment_logs": self._request_appointment_logs,
|
||
"create_order": self._create_diagnosis_order,
|
||
"delete": self._delete_selected,
|
||
}
|
||
handler = actions.get(action)
|
||
if handler is not None:
|
||
handler()
|
||
|
||
def _row_video(self, record: Any) -> None:
|
||
if self._select_record(record):
|
||
self._request_video()
|
||
|
||
def _batch_assign(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/assign"):
|
||
return
|
||
records = self.table_host.selected_records()
|
||
if not records:
|
||
show_toast(self, "请先选择要指派的诊单。", "warning")
|
||
return
|
||
self._choose_assistant(records)
|
||
|
||
def _assign_selected(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/assign"):
|
||
return
|
||
record = self.table.current_data()
|
||
if record is not None:
|
||
self._choose_assistant([record])
|
||
|
||
def _choose_assistant(self, records: Sequence[Any]) -> None:
|
||
snapshot = [deepcopy(record) for record in records]
|
||
if self._assistant_names:
|
||
self._show_assistant_picker(snapshot, self._assistant_names)
|
||
return
|
||
|
||
def load() -> list[Any]:
|
||
return page_items(invoke(self.repository, "list_diagnosis_assistants"))
|
||
|
||
run_async(
|
||
load,
|
||
on_success=lambda rows: self._assistant_options_loaded_for_picker(rows, snapshot),
|
||
on_error=lambda error: show_toast(self, friendly_error(error), "danger", 4200),
|
||
)
|
||
|
||
def _assistant_options_loaded_for_picker(self, rows: Any, records: Sequence[Any]) -> None:
|
||
options = {
|
||
_as_int(first_value(row, "id", "value")): display_text(
|
||
first_value(row, "name", "label"), ""
|
||
)
|
||
for row in rows
|
||
if isinstance(rows, Sequence)
|
||
if _as_int(first_value(row, "id", "value")) > 0
|
||
}
|
||
options = {key: value for key, value in options.items() if value}
|
||
self._assistant_names.update(options)
|
||
self._show_assistant_picker(records, options)
|
||
|
||
def _show_assistant_picker(
|
||
self,
|
||
records: Sequence[Any],
|
||
options: Mapping[int, str],
|
||
) -> None:
|
||
if not options:
|
||
show_toast(self, "暂无可指派医助。", "warning")
|
||
return
|
||
labels = [f"{name}(#{assistant_id})" for assistant_id, name in options.items()]
|
||
selected, accepted = QInputDialog.getItem(
|
||
self,
|
||
"指派医助",
|
||
f"为已选择的 {len(records)} 条诊单指派:",
|
||
labels,
|
||
0,
|
||
False,
|
||
)
|
||
if not accepted:
|
||
return
|
||
assistant_id = next(
|
||
(key for key, name in options.items() if selected == f"{name}(#{key})"),
|
||
0,
|
||
)
|
||
diagnosis_ids = tuple(
|
||
_as_int(first_value(record, "diagnosis_id", "id")) for record in records
|
||
)
|
||
|
||
def assign() -> None:
|
||
for diagnosis_id in diagnosis_ids:
|
||
invoke(
|
||
self.repository,
|
||
"assign_diagnosis",
|
||
diagnosis_id=diagnosis_id,
|
||
assistant_id=assistant_id,
|
||
is_inherit=0,
|
||
)
|
||
|
||
self._run_mutation(assign, "医助已指派。")
|
||
|
||
def _batch_cancel_assign(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/assign"):
|
||
return
|
||
records = [
|
||
row
|
||
for row in self.table_host.selected_records()
|
||
if _as_int(first_value(row, "assistant_id", "assistant", default=0)) > 0
|
||
]
|
||
self._cancel_assign_records(records)
|
||
|
||
def _cancel_selected_assign(self) -> None:
|
||
record = self.table.current_data()
|
||
records = [record] if record is not None else []
|
||
self._cancel_assign_records(records)
|
||
|
||
def _cancel_assign_records(self, records: Sequence[Any]) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/assign"):
|
||
return
|
||
assigned = [
|
||
row
|
||
for row in records
|
||
if _as_int(first_value(row, "assistant_id", "assistant", default=0)) > 0
|
||
]
|
||
if not assigned:
|
||
show_toast(self, "所选诊单没有已指派的医助。", "warning")
|
||
return
|
||
answer = QMessageBox.question(
|
||
self,
|
||
"取消指派",
|
||
f"确定取消 {len(assigned)} 条诊单的医助指派吗?",
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
|
||
QMessageBox.StandardButton.Cancel,
|
||
)
|
||
if answer != QMessageBox.StandardButton.Yes:
|
||
return
|
||
diagnosis_ids = tuple(
|
||
_as_int(first_value(record, "diagnosis_id", "id")) for record in assigned
|
||
)
|
||
|
||
def cancel() -> None:
|
||
for diagnosis_id in diagnosis_ids:
|
||
invoke(
|
||
self.repository,
|
||
"assign_diagnosis",
|
||
diagnosis_id=diagnosis_id,
|
||
assistant_id=0,
|
||
)
|
||
|
||
self._run_mutation(cancel, "医助指派已取消。")
|
||
|
||
def _book_selected_appointment(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/guahao"):
|
||
return
|
||
record = self.table.current_data()
|
||
if record is None:
|
||
return
|
||
# Kept lazy to avoid making the two page modules import each other at startup.
|
||
from .patients import _AppointmentDialog
|
||
|
||
dialog = _AppointmentDialog(record, repository=self.repository, parent=self)
|
||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||
return
|
||
payload = MappingProxyType(dialog.payload())
|
||
self._run_mutation(
|
||
lambda: invoke(
|
||
self.repository,
|
||
"book_patient_appointment",
|
||
payload=payload,
|
||
**payload,
|
||
),
|
||
"预约已保存。",
|
||
)
|
||
|
||
def _cancel_selected_appointment(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/guahao"):
|
||
return
|
||
record = self.table.current_data()
|
||
appointment = _single_cancellable_appointment(record)
|
||
if appointment is None:
|
||
if len(appointment_rows(record)) > 1:
|
||
show_toast(self, "该诊单有多条挂号,请在具体挂号记录中取消。", "warning")
|
||
return
|
||
show_toast(self, "当前挂号不可取消。", "warning")
|
||
return
|
||
appointment_id = _as_int(first_value(appointment, "id", "appointment_id", default=0))
|
||
self._cancel_appointment_item(record, appointment_id)
|
||
|
||
def _cancel_appointment_item(self, record: Any, appointment_id: int) -> None:
|
||
"""Cancel the exact appointment exposed by one card, with a second pre-mutation check."""
|
||
|
||
if (
|
||
not _canonical_allowed(self.permissions, "tcm.diagnosis/guahao")
|
||
or self._mutation_pending
|
||
or self._repository_methods.get("cancel_appointment") is None
|
||
):
|
||
return
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||
if diagnosis_id <= 0 or not self._select_record(record):
|
||
show_toast(self, "诊单已刷新,请重新选择挂号。", "warning")
|
||
return
|
||
current = self.table.current_data()
|
||
appointment = _cancellable_appointment_by_id(current, appointment_id)
|
||
if appointment is None:
|
||
show_toast(self, "该挂号已变化或当前不可取消。", "warning")
|
||
return
|
||
patient_name = display_text(first_value(current, "patient_name"), "患者")
|
||
doctor_name = display_text(first_value(appointment, "doctor_name"), "医生")
|
||
time_text = display_text(
|
||
first_value(appointment, "time_text", "appointment_time"), "时间未标注"
|
||
)
|
||
answer = QMessageBox.question(
|
||
self,
|
||
"取消挂号",
|
||
f"确定取消挂号 #{appointment_id} 吗?\n"
|
||
f"医生/时间:{doctor_name} · {time_text}\n患者:{patient_name}",
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
|
||
QMessageBox.StandardButton.Cancel,
|
||
)
|
||
if answer != QMessageBox.StandardButton.Yes:
|
||
return
|
||
current = next(
|
||
(
|
||
item
|
||
for item in self.table_host.model.rows
|
||
if _as_int(first_value(item, "diagnosis_id", "id", default=0)) == diagnosis_id
|
||
),
|
||
None,
|
||
)
|
||
if _cancellable_appointment_by_id(current, appointment_id) is None:
|
||
show_toast(self, "挂号状态已刷新,本次未执行取消。", "warning")
|
||
return
|
||
method_name = self._repository_methods["cancel_appointment"]
|
||
assert method_name is not None
|
||
self._run_mutation(
|
||
lambda: invoke(
|
||
self.repository,
|
||
method_name,
|
||
appointment_id=appointment_id,
|
||
),
|
||
"挂号已取消。",
|
||
)
|
||
|
||
def _fill_selected_id_card(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/edit"):
|
||
return
|
||
record = self.table.current_data()
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||
if diagnosis_id <= 0:
|
||
return
|
||
id_card, accepted = QInputDialog.getText(
|
||
self,
|
||
"补全身份证",
|
||
"身份证号:",
|
||
text=display_text(first_value(record, "id_card", default=""), ""),
|
||
)
|
||
id_card = id_card.strip()
|
||
if not accepted:
|
||
return
|
||
if not id_card:
|
||
show_toast(self, "请输入身份证号。", "warning")
|
||
return
|
||
if not is_valid_id_card(id_card):
|
||
show_toast(self, "请输入15或18位有效身份证号。", "warning")
|
||
return
|
||
self._run_mutation(
|
||
lambda: invoke(
|
||
self.repository,
|
||
"fill_diagnosis_id_card",
|
||
diagnosis_id=diagnosis_id,
|
||
id_card=id_card,
|
||
),
|
||
"身份证已补全。",
|
||
)
|
||
|
||
def _run_menu_request(self, operation: Any, on_success: Any) -> None:
|
||
"""Run a menu request and reject results for a newer row selection."""
|
||
|
||
self._menu_generation += 1
|
||
generation = self._menu_generation
|
||
run_async(
|
||
operation,
|
||
on_success=lambda result: (
|
||
on_success(result) if generation == self._menu_generation else None
|
||
),
|
||
on_error=lambda error: (
|
||
show_toast(self, friendly_error(error), "danger", 4200)
|
||
if generation == self._menu_generation
|
||
else None
|
||
),
|
||
)
|
||
|
||
@staticmethod
|
||
def _qr_url(result: Any) -> str:
|
||
if isinstance(result, str):
|
||
return result.strip()
|
||
return str(
|
||
first_value(
|
||
result,
|
||
"qrcode_url",
|
||
"data.qrcode_url",
|
||
"url",
|
||
"data.url",
|
||
default="",
|
||
)
|
||
or ""
|
||
).strip()
|
||
|
||
def _request_video_qr(self) -> None:
|
||
self._request_qr(
|
||
capability="video_qr",
|
||
permission="tcm.diagnosis/videoQr",
|
||
title="视频二维码",
|
||
)
|
||
|
||
def _request_confirm_qr(self) -> None:
|
||
self._request_qr(
|
||
capability="confirm_qr",
|
||
permission="tcm.diagnosis/guahao",
|
||
title="诊单二维码",
|
||
)
|
||
|
||
def _request_qr(
|
||
self,
|
||
*,
|
||
capability: str,
|
||
permission: str,
|
||
title: str,
|
||
) -> None:
|
||
method_name = self._repository_methods.get(capability)
|
||
record = self.table.current_data()
|
||
if not _canonical_allowed(self.permissions, permission) or method_name is None:
|
||
return
|
||
if record is None or not is_video_available(record):
|
||
show_toast(self, f"仅“已预约”状态可生成{title}。", "warning")
|
||
return
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||
patient_id = _as_int(first_value(record, "patient_id", "source_patient_id", default=0))
|
||
appointments = appointment_rows(record)
|
||
doctor_id = _as_int(
|
||
first_value(
|
||
record,
|
||
"appointment_doctor_id",
|
||
default=first_value(
|
||
appointments[0] if appointments else None, "doctor_id", default=0
|
||
),
|
||
)
|
||
)
|
||
if diagnosis_id <= 0 or patient_id <= 0 or doctor_id <= 0:
|
||
show_toast(self, "患者、诊单或预约医生标识不完整,无法生成二维码。", "warning")
|
||
return
|
||
share_user_id = _as_int(first_value(self.current_user, "id", default=0))
|
||
if share_user_id <= 0:
|
||
show_toast(self, "当前用户标识不完整,无法生成二维码。", "warning")
|
||
return
|
||
payload: dict[str, Any] = {
|
||
"diagnosis_id": diagnosis_id,
|
||
"patient_id": patient_id,
|
||
"doctor_id": doctor_id,
|
||
"share_user_id": share_user_id,
|
||
}
|
||
snapshot = deepcopy(record)
|
||
frozen_payload = MappingProxyType(payload)
|
||
self._run_menu_request(
|
||
lambda: invoke(
|
||
self.repository,
|
||
method_name,
|
||
**frozen_payload,
|
||
),
|
||
lambda result: self._show_qr_result(title, snapshot, result),
|
||
)
|
||
|
||
def _show_qr_result(self, title: str, record: Any, result: Any) -> None:
|
||
qrcode_url = self._qr_url(result)
|
||
if not qrcode_url:
|
||
show_toast(self, "服务器未返回可用的二维码地址。", "danger", 4200)
|
||
return
|
||
dialog = _QrResultDialog(title, record, qrcode_url, self)
|
||
self._qr_result_dialog = dialog
|
||
dialog.finished.connect(lambda _code: setattr(self, "_qr_result_dialog", None))
|
||
dialog.open()
|
||
|
||
def _request_appointment_logs(self) -> None:
|
||
method_name = self._repository_methods.get("appointment_logs")
|
||
record = self.table.current_data()
|
||
if (
|
||
not _canonical_allowed(self.permissions, "tcm.diagnosis/guahaoLogList")
|
||
or method_name is None
|
||
or record is None
|
||
):
|
||
return
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||
if diagnosis_id <= 0:
|
||
show_toast(self, "诊单标识不完整,无法查询挂号日志。", "warning")
|
||
return
|
||
snapshot = deepcopy(record)
|
||
self._run_menu_request(
|
||
lambda: invoke(
|
||
self.repository,
|
||
method_name,
|
||
diagnosis_id=diagnosis_id,
|
||
id=diagnosis_id,
|
||
),
|
||
lambda result: self._show_appointment_logs(snapshot, result),
|
||
)
|
||
|
||
def _show_appointment_logs(self, record: Any, result: Any) -> None:
|
||
dialog = _AppointmentLogDialog(record, page_items(result), self)
|
||
self._appointment_logs_dialog = dialog
|
||
dialog.finished.connect(lambda _code: setattr(self, "_appointment_logs_dialog", None))
|
||
dialog.open()
|
||
|
||
def _create_diagnosis_order(self) -> None:
|
||
method_name = self._repository_methods.get("create_order")
|
||
qr_method_name = self._repository_methods.get("order_qr")
|
||
record = self.table.current_data()
|
||
if (
|
||
not _canonical_allowed(self.permissions, "tcm.diagnosis/order")
|
||
or method_name is None
|
||
or qr_method_name is None
|
||
or record is None
|
||
or self._mutation_pending
|
||
or self._order_flow_generation is not None
|
||
):
|
||
return
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||
patient_id = _as_int(first_value(record, "patient_id", "source_patient_id", default=0))
|
||
if diagnosis_id <= 0 or patient_id <= 0:
|
||
show_toast(self, "患者或诊单标识不完整,无法创建订单。", "warning")
|
||
return
|
||
dialog = _DiagnosisOrderDialog(record, self)
|
||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||
return
|
||
current = self.table.current_data()
|
||
if (
|
||
not _canonical_allowed(self.permissions, "tcm.diagnosis/order")
|
||
or not callable(getattr(self.repository, method_name, None))
|
||
or not callable(getattr(self.repository, qr_method_name, None))
|
||
or _as_int(first_value(current, "diagnosis_id", "id", default=0)) != diagnosis_id
|
||
or _as_int(first_value(current, "patient_id", "source_patient_id", default=0))
|
||
!= patient_id
|
||
):
|
||
show_toast(self, "权限或当前诊单已变化,本次未创建订单。", "warning")
|
||
return
|
||
payload = MappingProxyType(dialog.payload())
|
||
snapshot = deepcopy(record)
|
||
generation = self._begin_order_flow()
|
||
run_async(
|
||
lambda: invoke(
|
||
self.repository,
|
||
method_name,
|
||
payload=payload,
|
||
**payload,
|
||
),
|
||
on_success=lambda result: self._diagnosis_order_created(
|
||
result,
|
||
snapshot,
|
||
diagnosis_id,
|
||
patient_id,
|
||
generation,
|
||
),
|
||
on_error=lambda error: self._diagnosis_order_create_error(error, generation),
|
||
)
|
||
|
||
@staticmethod
|
||
def _order_no(result: Any) -> str:
|
||
value = first_value(result, "order_no", "data.order_no", default="")
|
||
return str(value or "").strip()
|
||
|
||
def _begin_order_flow(self) -> int:
|
||
self._mutation_pending = True
|
||
self.table_host.setEnabled(False)
|
||
self._checked_changed(len(self.table_host.selected_records()))
|
||
self._selection_changed()
|
||
self._order_generation += 1
|
||
self._order_flow_generation = self._order_generation
|
||
self._order_qr_request_pending = False
|
||
return self._order_generation
|
||
|
||
def _order_flow_guard(
|
||
self,
|
||
generation: int,
|
||
diagnosis_id: int,
|
||
patient_id: int,
|
||
) -> bool:
|
||
if generation != self._order_flow_generation:
|
||
return False
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/order"):
|
||
return False
|
||
method_name = self._repository_methods.get("order_qr")
|
||
if method_name is None or not callable(getattr(self.repository, method_name, None)):
|
||
return False
|
||
current = self.table.current_data()
|
||
return bool(
|
||
_as_int(first_value(current, "diagnosis_id", "id", default=0)) == diagnosis_id
|
||
and _as_int(first_value(current, "patient_id", "source_patient_id", default=0))
|
||
== patient_id
|
||
)
|
||
|
||
def _diagnosis_order_created(
|
||
self,
|
||
result: Any,
|
||
record: Any,
|
||
diagnosis_id: int,
|
||
patient_id: int,
|
||
generation: int,
|
||
) -> None:
|
||
if generation != self._order_flow_generation:
|
||
return
|
||
if not self._order_flow_guard(generation, diagnosis_id, patient_id):
|
||
self.banner.show_message("权限或当前诊单已变化,未展示付款二维码。", "danger")
|
||
self._finish_order_flow(generation, refresh=True)
|
||
return
|
||
order_no = self._order_no(result)
|
||
if not order_no:
|
||
self.banner.show_message(
|
||
"订单已创建,但服务器未返回订单号,无法生成付款二维码。",
|
||
"danger",
|
||
)
|
||
self._finish_order_flow(generation, refresh=True)
|
||
return
|
||
dialog = _DiagnosisOrderQrDialog(record, order_no, self)
|
||
self._order_qr_dialog = dialog
|
||
dialog.retry_requested.connect(
|
||
lambda retry_order_no: self._request_order_qr(
|
||
retry_order_no,
|
||
diagnosis_id,
|
||
patient_id,
|
||
generation,
|
||
dialog,
|
||
)
|
||
)
|
||
dialog.finished.connect(lambda _code: self._order_qr_dialog_finished(dialog, generation))
|
||
dialog.open()
|
||
self._request_order_qr(
|
||
order_no,
|
||
diagnosis_id,
|
||
patient_id,
|
||
generation,
|
||
dialog,
|
||
)
|
||
|
||
def _request_order_qr(
|
||
self,
|
||
order_no: str,
|
||
diagnosis_id: int,
|
||
patient_id: int,
|
||
generation: int,
|
||
dialog: _DiagnosisOrderQrDialog,
|
||
) -> None:
|
||
if dialog is not self._order_qr_dialog or generation != self._order_flow_generation:
|
||
return
|
||
if order_no != dialog.order_no:
|
||
dialog.set_failure("订单号已变化,无法生成付款二维码。", retryable=False)
|
||
return
|
||
if not self._order_flow_guard(generation, diagnosis_id, patient_id):
|
||
dialog.set_failure(
|
||
"权限或当前诊单已变化,无法生成付款二维码。",
|
||
retryable=False,
|
||
)
|
||
return
|
||
if self._order_qr_request_pending:
|
||
return
|
||
method_name = self._repository_methods["order_qr"]
|
||
self._order_qr_request_pending = True
|
||
dialog.set_loading()
|
||
run_async(
|
||
lambda: invoke(
|
||
self.repository,
|
||
method_name,
|
||
order_no=order_no,
|
||
),
|
||
on_success=lambda result: self._diagnosis_order_qr_success(
|
||
result,
|
||
diagnosis_id,
|
||
patient_id,
|
||
generation,
|
||
dialog,
|
||
),
|
||
on_error=lambda error: self._diagnosis_order_qr_error(
|
||
error,
|
||
diagnosis_id,
|
||
patient_id,
|
||
generation,
|
||
dialog,
|
||
),
|
||
on_finished=lambda: self._diagnosis_order_qr_finished(generation),
|
||
)
|
||
|
||
def _diagnosis_order_qr_success(
|
||
self,
|
||
result: Any,
|
||
diagnosis_id: int,
|
||
patient_id: int,
|
||
generation: int,
|
||
dialog: _DiagnosisOrderQrDialog,
|
||
) -> None:
|
||
if dialog is not self._order_qr_dialog or generation != self._order_flow_generation:
|
||
return
|
||
if not self._order_flow_guard(generation, diagnosis_id, patient_id):
|
||
dialog.set_failure(
|
||
"权限或当前诊单已变化,未展示付款二维码。",
|
||
retryable=False,
|
||
)
|
||
return
|
||
qrcode_url = self._qr_url(result)
|
||
if not qrcode_url:
|
||
dialog.set_failure("服务器未返回可用的付款二维码地址,请重试。")
|
||
return
|
||
dialog.set_result(qrcode_url)
|
||
show_toast(self, "订单已创建,付款二维码已生成。", "success")
|
||
|
||
def _diagnosis_order_qr_error(
|
||
self,
|
||
error: Exception,
|
||
diagnosis_id: int,
|
||
patient_id: int,
|
||
generation: int,
|
||
dialog: _DiagnosisOrderQrDialog,
|
||
) -> None:
|
||
if dialog is not self._order_qr_dialog or generation != self._order_flow_generation:
|
||
return
|
||
retryable = self._order_flow_guard(generation, diagnosis_id, patient_id)
|
||
message = (
|
||
f"付款二维码生成失败:{friendly_error(error)}"
|
||
if retryable
|
||
else "权限或当前诊单已变化,未展示付款二维码。"
|
||
)
|
||
dialog.set_failure(message, retryable=retryable)
|
||
|
||
def _diagnosis_order_qr_finished(self, generation: int) -> None:
|
||
if generation == self._order_flow_generation:
|
||
self._order_qr_request_pending = False
|
||
|
||
def _diagnosis_order_create_error(self, error: Exception, generation: int) -> None:
|
||
if generation != self._order_flow_generation:
|
||
return
|
||
self.banner.show_message(friendly_error(error), "danger")
|
||
self._finish_order_flow(generation, refresh=False)
|
||
|
||
def _order_qr_dialog_finished(
|
||
self,
|
||
dialog: _DiagnosisOrderQrDialog,
|
||
generation: int,
|
||
) -> None:
|
||
if dialog is self._order_qr_dialog:
|
||
self._order_qr_dialog = None
|
||
self._finish_order_flow(generation, refresh=True)
|
||
|
||
def _finish_order_flow(self, generation: int, *, refresh: bool) -> None:
|
||
if generation != self._order_flow_generation:
|
||
return
|
||
self._order_flow_generation = None
|
||
self._order_generation += 1
|
||
self._order_qr_request_pending = False
|
||
self._mutation_pending = False
|
||
self.table_host.setEnabled(True)
|
||
self._checked_changed(len(self.table_host.selected_records()))
|
||
self._selection_changed()
|
||
if refresh:
|
||
self.refresh(silent=True)
|
||
|
||
def _selection_changed(self) -> None:
|
||
self._menu_generation += 1
|
||
self._prescription_generation += 1
|
||
# A row change invalidates the old worker and must also release its UI lock.
|
||
self._prescription_busy = False
|
||
record = self.table.current_data()
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||
has_record = record is not None and diagnosis_id > 0 and not self._mutation_pending
|
||
self.view_button.setEnabled(has_record)
|
||
self.edit_button.setEnabled(has_record)
|
||
self.delete_button.setEnabled(has_record)
|
||
self.prescription_button.setText(
|
||
prescription_action(
|
||
record,
|
||
force_open=self.table_host.force_open_prescription,
|
||
)[0]
|
||
if record is not None
|
||
else "开方"
|
||
)
|
||
self.prescription_button.setEnabled(has_record and not self._prescription_busy)
|
||
row_has_prescription = _as_bool(
|
||
first_value(
|
||
record,
|
||
"current_has_prescription",
|
||
default=first_value(record, "has_prescription", default=False),
|
||
)
|
||
)
|
||
self.void_button.setEnabled(
|
||
has_record
|
||
and row_has_prescription
|
||
and can_void_prescription(record)
|
||
and not self._prescription_busy
|
||
)
|
||
payload = _video_payload(record) if record is not None else {}
|
||
valid_ids = all(
|
||
_as_int(payload.get(key), 0) > 0
|
||
for key in ("appointment_id", "patient_id", "diagnosis_id")
|
||
)
|
||
self.video_button.setEnabled(
|
||
has_record
|
||
and is_video_available(record)
|
||
and video_call_is_live(record)
|
||
and valid_ids
|
||
)
|
||
|
||
@property
|
||
def _diagnosis_dialog(self) -> DiagnosisDialog:
|
||
dialog = getattr(self, "_diagnosis_dialog_impl", None)
|
||
if dialog is None:
|
||
dialog = DiagnosisDialog(self.repository, self)
|
||
dialog.saved.connect(lambda: self.refresh(silent=True))
|
||
self._diagnosis_dialog_impl = dialog
|
||
return dialog
|
||
|
||
def _open_readonly(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/readonlyDetail"):
|
||
return
|
||
record = self.table.current_data()
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||
if diagnosis_id <= 0:
|
||
return
|
||
self._diagnosis_dialog.open_view_only(diagnosis_id, seed=record)
|
||
|
||
def open_selected_ai_consult(self) -> bool:
|
||
"""Open the assistant with the selected diagnosis as its authority key."""
|
||
|
||
record = self.table.current_data()
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||
if (
|
||
record is None
|
||
or diagnosis_id <= 0
|
||
or not can_open_ai_consult(self.permissions)
|
||
):
|
||
return False
|
||
present_ai_consult(
|
||
self.repository,
|
||
self.permissions,
|
||
self,
|
||
diagnosis_id=diagnosis_id,
|
||
patient_id=_as_int(
|
||
first_value(record, "source_patient_id", "patient_id", default=0)
|
||
),
|
||
seed=record,
|
||
source_title="问诊列表",
|
||
)
|
||
return True
|
||
|
||
def _open_ai_consult(self) -> None:
|
||
if not self.open_selected_ai_consult():
|
||
show_toast(self, "请先选择一条有效诊单。", "warning")
|
||
|
||
def _open_edit(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/edit"):
|
||
return
|
||
record = self.table.current_data()
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||
if diagnosis_id <= 0:
|
||
return
|
||
self._diagnosis_dialog.open_for(diagnosis_id, editable=True, seed=record)
|
||
|
||
def _add_diagnosis(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/add"):
|
||
return
|
||
dialog = _DiagnosisCreateDialog(self)
|
||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||
return
|
||
payload = dialog.payload()
|
||
payload.setdefault("doctor_id", first_value(self.current_user, "id", "user_id"))
|
||
payload.setdefault("doctor_name", first_value(self.current_user, "name", "real_name"))
|
||
self._run_mutation(
|
||
lambda: invoke(self.repository, "create_diagnosis", diagnosis=payload),
|
||
"诊单已创建。",
|
||
)
|
||
|
||
def _delete_selected(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/delete"):
|
||
return
|
||
record = self.table.current_data()
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||
if diagnosis_id <= 0:
|
||
return
|
||
answer = QMessageBox.warning(
|
||
self,
|
||
"删除诊单",
|
||
f"确定删除诊单 #{diagnosis_id} 吗?此操作无法撤销。",
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
|
||
QMessageBox.StandardButton.Cancel,
|
||
)
|
||
if answer != QMessageBox.StandardButton.Yes:
|
||
return
|
||
self._run_mutation(
|
||
lambda: invoke(
|
||
self.repository, "delete_diagnosis", diagnosis_id=diagnosis_id, id=diagnosis_id
|
||
),
|
||
"诊单已删除。",
|
||
)
|
||
|
||
def _load_context_prescription(self, record: Any) -> Any:
|
||
appointment_id = _appointment_id(record)
|
||
if appointment_id <= 0:
|
||
return None
|
||
# This endpoint is the sole authority for the selected appointment. A
|
||
# transport/auth error must propagate; diagnosis history is never a fallback.
|
||
existing = invoke(
|
||
self.repository,
|
||
"get_prescription_by_appointment",
|
||
appointment_id=appointment_id,
|
||
)
|
||
if (
|
||
isinstance(existing, Mapping)
|
||
and first_value(existing, "id", "prescription_id") is None
|
||
and "data" in existing
|
||
):
|
||
existing = existing.get("data")
|
||
if existing is None or (
|
||
isinstance(existing, (Mapping, Sequence))
|
||
and not isinstance(existing, (str, bytes))
|
||
and not existing
|
||
):
|
||
return None
|
||
return existing
|
||
|
||
def _load_case_record(self, record: Any) -> Any:
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id", default=0))
|
||
if diagnosis_id <= 0:
|
||
raise ValueError("诊单编号不完整,无法建立处方病例快照")
|
||
detail = invoke(
|
||
self.repository,
|
||
"get_diagnosis_detail",
|
||
diagnosis_id=diagnosis_id,
|
||
id=diagnosis_id,
|
||
readonly=False,
|
||
)
|
||
actual_id = _as_int(
|
||
first_value(detail, "diagnosis.id", "diagnosis_id", "id", default=diagnosis_id)
|
||
)
|
||
if actual_id != diagnosis_id:
|
||
raise ValueError("服务端诊单与当前行不一致,已停止开方")
|
||
return deepcopy(detail)
|
||
|
||
def _open_prescription(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/kaifang"):
|
||
return
|
||
record = self.table.current_data()
|
||
if _as_int(first_value(record, "diagnosis_id", "id", default=0)) <= 0:
|
||
return
|
||
_label, mode = prescription_action(
|
||
record,
|
||
force_open=self.table_host.force_open_prescription,
|
||
)
|
||
self._begin_prescription_load(record, mode=mode)
|
||
|
||
def _void_selected_prescription(self) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/kaifang"):
|
||
return
|
||
record = self.table.current_data()
|
||
if record is None or not can_void_prescription(record):
|
||
return
|
||
self._begin_prescription_load(record, mode="void")
|
||
|
||
def _begin_prescription_load(self, record: Any, *, mode: str) -> None:
|
||
record_snapshot = deepcopy(record)
|
||
self._prescription_generation += 1
|
||
generation = self._prescription_generation
|
||
self._prescription_busy = True
|
||
self.prescription_button.setEnabled(False)
|
||
self.void_button.setEnabled(False)
|
||
self.banner.show_message("正在核对诊单处方上下文…", "info")
|
||
run_async(
|
||
lambda: self._load_context_prescription(record_snapshot),
|
||
on_success=lambda existing: self._prescription_loaded(
|
||
existing, record_snapshot, mode, generation
|
||
),
|
||
on_error=lambda error: self._prescription_error(error, generation),
|
||
on_finished=lambda: self._prescription_finished(generation),
|
||
)
|
||
|
||
def _prescription_loaded(self, existing: Any, record: Any, mode: str, generation: int) -> None:
|
||
if generation != self._prescription_generation:
|
||
return
|
||
self.banner.clear()
|
||
self._last_prescription = existing
|
||
if mode == "void":
|
||
self._confirm_void(existing)
|
||
return
|
||
if existing is not None:
|
||
approved = _as_int(first_value(existing, "audit_status", "status"), -1) == 1
|
||
voided = _as_int(first_value(existing, "void_status", "is_void"), 0) == 1
|
||
actual_id = _as_int(first_value(existing, "id", "prescription_id", default=0))
|
||
expected_id = _as_int(
|
||
first_value(record, "current_prescription_id", default=0)
|
||
)
|
||
actual_appointment_id = _as_int(
|
||
first_value(existing, "appointment_id", default=0)
|
||
)
|
||
expected_appointment_id = _appointment_id(record)
|
||
identity_changed = (
|
||
(expected_id > 0 and actual_id != expected_id)
|
||
or (
|
||
actual_appointment_id > 0
|
||
and expected_appointment_id > 0
|
||
and actual_appointment_id != expected_appointment_id
|
||
)
|
||
)
|
||
if mode == "view" and (not approved or voided or identity_changed):
|
||
self.banner.show_message(
|
||
"当前挂号的处方状态已变化,请刷新列表后重试。",
|
||
"warning",
|
||
)
|
||
return
|
||
if not approved or voided:
|
||
self._open_existing_prescription_editor(existing)
|
||
else:
|
||
detail = PrescriptionDetailDialog(
|
||
existing,
|
||
can_open_diagnosis=_canonical_allowed(
|
||
self.permissions, "tcm.diagnosis/readonlyDetail"
|
||
),
|
||
parent=self,
|
||
)
|
||
detail.diagnosis_requested.connect(self._open_diagnosis_id)
|
||
detail.exec()
|
||
return
|
||
if mode == "view":
|
||
self.banner.show_message(
|
||
"当前挂号没有可查看的处方,请刷新列表后重试。",
|
||
"warning",
|
||
)
|
||
return
|
||
self._begin_case_record_load(record)
|
||
|
||
def _open_existing_prescription_editor(self, prescription: Any) -> None:
|
||
prescription_id = _as_int(first_value(prescription, "id", "prescription_id", default=0))
|
||
if prescription_id <= 0:
|
||
self.banner.show_message("处方编号不完整,无法编辑。", "danger")
|
||
return
|
||
dialog = PrescriptionEditorDialog(
|
||
self.repository,
|
||
prescription,
|
||
mode="edit",
|
||
current_user=self.current_user,
|
||
parent=self,
|
||
)
|
||
diagnosis_signal = getattr(dialog, "diagnosis_requested", None)
|
||
if diagnosis_signal is not None:
|
||
diagnosis_signal.connect(self._open_diagnosis_id)
|
||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||
return
|
||
frozen_payload = MappingProxyType(dialog.payload())
|
||
self._run_mutation(
|
||
lambda: invoke(
|
||
self.repository,
|
||
"update_prescription",
|
||
prescription=prescription_id,
|
||
changes=frozen_payload,
|
||
),
|
||
"处方已保存并重新进入待审核。",
|
||
)
|
||
|
||
def _begin_case_record_load(self, record: Any) -> None:
|
||
record_snapshot = deepcopy(record)
|
||
self._prescription_generation += 1
|
||
generation = self._prescription_generation
|
||
self._prescription_busy = True
|
||
self.prescription_button.setEnabled(False)
|
||
self.void_button.setEnabled(False)
|
||
self.banner.show_message("正在生成不可变病例快照…", "info")
|
||
run_async(
|
||
lambda: self._load_case_record(record_snapshot),
|
||
on_success=lambda case_record: self._case_record_loaded(
|
||
case_record, record_snapshot, generation
|
||
),
|
||
on_error=lambda error: self._prescription_error(error, generation),
|
||
on_finished=lambda: self._prescription_finished(generation),
|
||
)
|
||
|
||
def _case_record_loaded(self, case_record: Any, record: Any, generation: int) -> None:
|
||
if generation != self._prescription_generation:
|
||
return
|
||
self.banner.clear()
|
||
self._open_prescription_editor(record, deepcopy(case_record))
|
||
|
||
def _prescription_error(self, error: Exception, generation: int) -> None:
|
||
if generation == self._prescription_generation:
|
||
self.banner.show_message(friendly_error(error), "danger")
|
||
|
||
def _prescription_finished(self, generation: int) -> None:
|
||
if generation == self._prescription_generation:
|
||
self._prescription_busy = False
|
||
self._selection_changed()
|
||
|
||
def _open_diagnosis_id(self, diagnosis_id: int) -> None:
|
||
if not _canonical_allowed(self.permissions, "tcm.diagnosis/readonlyDetail"):
|
||
return
|
||
if diagnosis_id > 0:
|
||
self._diagnosis_dialog.open_view_only(diagnosis_id)
|
||
|
||
def _prescription_seed(self, record: Any, case_record: Any) -> dict[str, Any]:
|
||
diagnosis = get_value(case_record, "diagnosis", None) or case_record or {}
|
||
patient = get_value(case_record, "patient", None) or {}
|
||
|
||
def authoritative(*keys: str, default: Any = None) -> Any:
|
||
return first_value(
|
||
diagnosis,
|
||
*keys,
|
||
default=first_value(
|
||
patient, *keys, default=first_value(record, *keys, default=default)
|
||
),
|
||
)
|
||
|
||
gender = authoritative("gender", default=1)
|
||
if _as_int(gender, 1) == 2:
|
||
gender = 0
|
||
diagnosis_id = _as_int(first_value(record, "diagnosis_id", "id"))
|
||
appointment_id = _appointment_id(record)
|
||
return {
|
||
"diagnosis_id": diagnosis_id,
|
||
"appointment_id": appointment_id,
|
||
"case_record": deepcopy(case_record),
|
||
"patient_name": authoritative("patient_name", "name", default=""),
|
||
"gender": gender,
|
||
"age": _as_int(authoritative("age", default=0)),
|
||
"phone": authoritative("phone", "patient_phone", default=""),
|
||
"visit_no": build_prescription_visit_no(
|
||
diagnosis_id=diagnosis_id, appointment_id=appointment_id
|
||
),
|
||
"tongue": authoritative("tongue", "tongue_coating", default=""),
|
||
"tongue_image": authoritative("tongue_image", default=""),
|
||
"pulse": authoritative("pulse", default=""),
|
||
"pulse_condition": authoritative("pulse_condition", default=""),
|
||
"clinical_diagnosis": build_prescription_clinical_diagnosis(
|
||
diagnosis, patient, record, case_record
|
||
),
|
||
"doctor_name": first_value(
|
||
self.current_user,
|
||
"name",
|
||
"real_name",
|
||
default=authoritative("doctor_name", default=""),
|
||
),
|
||
}
|
||
|
||
def _open_prescription_editor(self, record: Any, case_record: Any) -> None:
|
||
seed = self._prescription_seed(record, case_record)
|
||
dialog = PrescriptionEditorDialog(
|
||
self.repository,
|
||
seed,
|
||
mode="add",
|
||
current_user=self.current_user,
|
||
parent=self,
|
||
)
|
||
diagnosis_signal = getattr(dialog, "diagnosis_requested", None)
|
||
if diagnosis_signal is not None:
|
||
diagnosis_signal.connect(self._open_diagnosis_id)
|
||
if dialog.exec() != QDialog.DialogCode.Accepted:
|
||
return
|
||
payload = dialog.payload()
|
||
payload["diagnosis_id"] = seed["diagnosis_id"]
|
||
payload["appointment_id"] = seed["appointment_id"]
|
||
payload["case_record"] = deepcopy(case_record)
|
||
frozen_payload = MappingProxyType(payload)
|
||
self._run_mutation(
|
||
lambda: invoke(self.repository, "create_prescription", prescription=frozen_payload),
|
||
"处方已开具并提交审核。",
|
||
)
|
||
|
||
def _confirm_void(self, prescription: Any) -> None:
|
||
if prescription is None:
|
||
self.banner.show_message("当前诊单没有可作废的处方。", "warning")
|
||
return
|
||
if _as_bool(first_value(prescription, "has_prescription_order", default=False)):
|
||
self.banner.show_message("该处方已有业务订单,不能作废。", "warning")
|
||
return
|
||
if _as_int(first_value(prescription, "void_status", "is_void"), 0) == 1:
|
||
self.banner.show_message("该处方已经作废。", "warning")
|
||
return
|
||
if _as_int(first_value(prescription, "audit_status", "status"), -1) == 1:
|
||
self.banner.show_message("审核通过且有效的处方仅供查看,不能在此作废。", "warning")
|
||
return
|
||
prescription_id = _as_int(first_value(prescription, "id", "prescription_id"))
|
||
if prescription_id <= 0:
|
||
self.banner.show_message("处方信息不完整,无法作废。", "warning")
|
||
return
|
||
answer = QMessageBox.warning(
|
||
self,
|
||
"作废处方",
|
||
"确定作废该处方吗?作废后不可恢复。",
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
|
||
QMessageBox.StandardButton.Cancel,
|
||
)
|
||
if answer != QMessageBox.StandardButton.Yes:
|
||
return
|
||
self._run_mutation(
|
||
lambda: invoke(
|
||
self.repository,
|
||
"void_prescription",
|
||
prescription_id=prescription_id,
|
||
id=prescription_id,
|
||
),
|
||
"处方已作废。",
|
||
)
|
||
|
||
def _run_mutation(self, operation: Any, success_message: str) -> None:
|
||
self._mutation_generation += 1
|
||
generation = self._mutation_generation
|
||
self._mutation_pending = True
|
||
self.table_host.setEnabled(False)
|
||
self._checked_changed(len(self.table_host.selected_records()))
|
||
self._selection_changed()
|
||
run_async(
|
||
operation,
|
||
on_success=lambda _result: self._mutation_success(success_message, generation),
|
||
on_error=lambda error: self._mutation_error(error, generation),
|
||
on_finished=lambda: self._mutation_finished(generation),
|
||
)
|
||
|
||
def _mutation_success(self, message: str, generation: int) -> None:
|
||
if generation != self._mutation_generation:
|
||
return
|
||
self.table_host.clear_checks()
|
||
show_toast(self, message, "success")
|
||
self.refresh(silent=True)
|
||
|
||
def _mutation_error(self, error: Exception, generation: int) -> None:
|
||
if generation == self._mutation_generation:
|
||
self.banner.show_message(friendly_error(error), "danger")
|
||
|
||
def _mutation_finished(self, generation: int) -> None:
|
||
if generation == self._mutation_generation:
|
||
self._mutation_pending = False
|
||
self.table_host.setEnabled(True)
|
||
self._checked_changed(len(self.table_host.selected_records()))
|
||
self._selection_changed()
|
||
|
||
def _request_video(self) -> None:
|
||
record = self.table.current_data()
|
||
if not self._native_video_capable:
|
||
self.banner.show_message("当前工作站未配置完整的视频问诊能力。", "warning")
|
||
return
|
||
if record is None or not is_video_available(record):
|
||
self.banner.show_message("仅当前“已预约”的挂号可进入视频问诊。", "warning")
|
||
return
|
||
if not video_call_is_live(record):
|
||
self.banner.show_message("医生尚未发起视频会话,请等待会话开始。", "warning")
|
||
return
|
||
payload = _video_payload(record)
|
||
if not all(
|
||
_as_int(payload.get(key), 0) > 0
|
||
for key in ("appointment_id", "patient_id", "diagnosis_id")
|
||
):
|
||
self.banner.show_message("患者、诊单或挂号标识不完整,无法进入视频问诊。", "warning")
|
||
return
|
||
self.video_requested.emit(payload)
|
||
|
||
def _poll_refresh(self) -> None:
|
||
"""Refresh rows and chip counts without showing the table mask."""
|
||
|
||
if (
|
||
not self.isVisible()
|
||
or self._loading
|
||
or self._order_flow_generation is not None
|
||
):
|
||
return
|
||
self.refresh(silent=True)
|
||
self._refresh_counts()
|
||
|
||
def showEvent(self, event: Any) -> None:
|
||
super().showEvent(event)
|
||
self._load_filter_options()
|
||
if not self.poll_timer.isActive():
|
||
self.poll_timer.start()
|
||
if self.table.rowCount() == 0 and not self._loading:
|
||
self.refresh()
|
||
|
||
def hideEvent(self, event: Any) -> None:
|
||
self.poll_timer.stop()
|
||
generation = self._order_flow_generation
|
||
dialog = self._order_qr_dialog
|
||
if generation is not None:
|
||
self._order_qr_dialog = None
|
||
self._finish_order_flow(generation, refresh=False)
|
||
if dialog is not None:
|
||
dialog.reject()
|
||
super().hideEvent(event)
|
||
|
||
|
||
__all__ = [
|
||
"ConsultationsPage",
|
||
"appointment_rows",
|
||
"can_void_prescription",
|
||
"is_diagnosis_confirmed",
|
||
"is_valid_id_card",
|
||
"is_video_available",
|
||
"prescription_action_label",
|
||
]
|