更新::
This commit is contained in:
@@ -1858,8 +1858,13 @@ class ConsultationsPage(QWidget):
|
||||
self.latest_appointment_end_date.setDate(_OPTIONAL_DATE_MINIMUM)
|
||||
self.channel_combo.setCurrentIndex(0)
|
||||
|
||||
def _reset(self) -> None:
|
||||
self.keyword_edit.clear()
|
||||
def search_patient(self, query: str) -> None:
|
||||
"""Search authorized diagnoses independently of the current queue filters."""
|
||||
self.pager.invalidate()
|
||||
self._reset(keyword=query.strip())
|
||||
|
||||
def _reset(self, *, keyword: str = "") -> None:
|
||||
self.keyword_edit.setText(keyword)
|
||||
self.patient_name_edit.clear()
|
||||
self.doctor_edit.clear()
|
||||
for combo in (
|
||||
@@ -1872,7 +1877,10 @@ class ConsultationsPage(QWidget):
|
||||
self.channel_combo,
|
||||
self.unserved_sort_combo,
|
||||
):
|
||||
blocked = combo.blockSignals(True)
|
||||
combo.setCurrentIndex(0)
|
||||
combo.blockSignals(blocked)
|
||||
self.table_host.set_sort_direction("")
|
||||
for editor in (
|
||||
self.latest_appointment_start_date,
|
||||
self.latest_appointment_end_date,
|
||||
|
||||
@@ -1996,13 +1996,15 @@ class PatientListWorkspace(QWidget):
|
||||
self.custom_date_button.setChecked(True)
|
||||
self.search()
|
||||
|
||||
def reset_filters(self) -> None:
|
||||
self.keyword_edit.clear()
|
||||
def reset_filters(self, *, keyword: str = "", refresh: bool = True) -> None:
|
||||
self.keyword_edit.setText(keyword.strip())
|
||||
self.status_combo.setCurrentIndex(0)
|
||||
for key, button in self.status_buttons.items():
|
||||
button.setChecked(key == "")
|
||||
self.set_date_mode("all", refresh=False)
|
||||
self.search()
|
||||
self._page = 1
|
||||
if refresh:
|
||||
self.refresh()
|
||||
|
||||
def search(self) -> None:
|
||||
if self._date_mode != "all" and self.start_date.date() > self.end_date.date():
|
||||
@@ -3256,7 +3258,7 @@ class PatientsPage(QWidget):
|
||||
if current is self.sender() or getattr(current, "scope", None) == text:
|
||||
self.scope_badge.set_status(text, "neutral")
|
||||
|
||||
def _workspace_changed(self, index: int) -> None:
|
||||
def _workspace_changed(self, index: int, *, refresh: bool = True) -> None:
|
||||
for workspace_index, disclosure in enumerate(self.filter_disclosures):
|
||||
disclosure.button.setVisible(workspace_index == index)
|
||||
self.scope_badge.setVisible(False)
|
||||
@@ -3264,9 +3266,9 @@ class PatientsPage(QWidget):
|
||||
workspace = self.tabs.widget(index)
|
||||
scope = getattr(workspace, "scope", "按权限加载")
|
||||
self.scope_badge.set_status(str(scope), "neutral")
|
||||
refresh = getattr(workspace, "refresh", None)
|
||||
if callable(refresh) and self.isVisible():
|
||||
refresh()
|
||||
reload_workspace = getattr(workspace, "refresh", None)
|
||||
if refresh and callable(reload_workspace) and self.isVisible():
|
||||
reload_workspace()
|
||||
|
||||
@property
|
||||
def filter_disclosure(self) -> FilterDisclosure:
|
||||
@@ -3283,6 +3285,19 @@ class PatientsPage(QWidget):
|
||||
if expanded:
|
||||
splitter.setSizes(self._progress_splitter_sizes)
|
||||
|
||||
def search_patient(self, query: str) -> None:
|
||||
"""Search the patient workspace using its normal permission-scoped endpoint."""
|
||||
blocked = self.tabs.blockSignals(True)
|
||||
try:
|
||||
self.tabs.setCurrentWidget(self.patient_workspace)
|
||||
finally:
|
||||
self.tabs.blockSignals(blocked)
|
||||
self._workspace_changed(0, refresh=False)
|
||||
# A global search starts a fresh result list, even for the same keyword.
|
||||
self.patient_workspace.pager.invalidate()
|
||||
self.patient_workspace.reset_filters(keyword=query, refresh=False)
|
||||
self.refresh()
|
||||
|
||||
def refresh(self) -> None:
|
||||
workspace = self.tabs.currentWidget()
|
||||
refresh = getattr(workspace, "refresh", None)
|
||||
|
||||
@@ -1966,7 +1966,7 @@ def _queue_shows_status_badge(status_number: int, status_text: str) -> bool:
|
||||
text = str(status_text or "")
|
||||
if "问诊中" in text or "已完成" in text or "已过号" in text:
|
||||
return True
|
||||
return status_number in {3, 4}
|
||||
return status_number in {2, 3, 4}
|
||||
|
||||
|
||||
def _queue_badge_kind(status_number: int, status_text: str) -> str:
|
||||
@@ -3013,8 +3013,8 @@ class _ReceptionDateButton(QPushButton):
|
||||
)
|
||||
self._calendar.setFixedSize(292, 280)
|
||||
self._calendar.hide()
|
||||
self._calendar.clicked.connect(self._pick_date)
|
||||
self._calendar.activated.connect(self._pick_date)
|
||||
self._calendar.clicked.connect(self._pick_calendar_date)
|
||||
self._calendar.activated.connect(self._pick_calendar_date)
|
||||
self.clicked.connect(self._toggle_calendar)
|
||||
|
||||
def iso_date(self) -> str:
|
||||
@@ -3040,13 +3040,16 @@ class _ReceptionDateButton(QPushButton):
|
||||
self._calendar.show()
|
||||
self._calendar.raise_()
|
||||
|
||||
def _pick_calendar_date(self, value: QDate) -> None:
|
||||
# clicked and activated may describe the same selection; consume one popup.
|
||||
if self._calendar.isVisible():
|
||||
self._pick_date(value)
|
||||
|
||||
def _pick_date(self, value: QDate) -> None:
|
||||
self._calendar.hide()
|
||||
if not value.isValid():
|
||||
return
|
||||
iso_date = value.toString(Qt.DateFormat.ISODate)
|
||||
if iso_date == self._iso_date:
|
||||
return
|
||||
self.set_iso_date(iso_date)
|
||||
self.date_selected.emit(iso_date)
|
||||
|
||||
@@ -4264,9 +4267,9 @@ class ReceptionPage(QWidget):
|
||||
header_layout = QHBoxLayout(header)
|
||||
header_layout.setContentsMargins(2, 0, 0, 0)
|
||||
header_layout.setSpacing(8)
|
||||
title = QLabel("今日接诊", header)
|
||||
title.setObjectName("ReceptionQueueTitle")
|
||||
header_layout.addWidget(title)
|
||||
self.queue_title = QLabel("今日接诊", header)
|
||||
self.queue_title.setObjectName("ReceptionQueueTitle")
|
||||
header_layout.addWidget(self.queue_title)
|
||||
header_layout.addStretch(1)
|
||||
self.refresh_button = QPushButton("", header)
|
||||
self.refresh_button.setIcon(icons.icon("refresh", PALETTE["muted"], 18))
|
||||
@@ -4311,7 +4314,7 @@ class ReceptionPage(QWidget):
|
||||
|
||||
filter_row = QGridLayout()
|
||||
filter_row.setSpacing(6)
|
||||
for status, label in ((None, "全部"), (1, "待接诊"), (2, "问诊中"), (3, "已完成")):
|
||||
for status, label in ((None, "全部"), (1, "待接诊"), (2, "已取消"), (3, "已完成")):
|
||||
button = QPushButton(label, self.queue_filter_panel)
|
||||
button.setFixedHeight(32)
|
||||
button.setCheckable(True)
|
||||
@@ -4350,7 +4353,12 @@ class ReceptionPage(QWidget):
|
||||
self.queue_list.currentItemChanged.connect(self._on_queue_selection)
|
||||
self.queue_list.verticalScrollBar().valueChanged.connect(self._on_queue_scroll)
|
||||
self.queue_stack.addWidget(self.queue_list)
|
||||
self.queue_empty = EmptyState("队列为空", "当前筛选下没有待处理患者。", "重新加载")
|
||||
self.queue_empty = EmptyState("暂无挂号记录", "当前筛选下没有挂号记录。", "重新加载")
|
||||
self.queue_empty_description = next(
|
||||
label for label in self.queue_empty.findChildren(QLabel)
|
||||
if label.property("role") == "muted"
|
||||
)
|
||||
self._sync_queue_filter_summary()
|
||||
self.queue_empty.action_requested.connect(self._refresh_workspace)
|
||||
self.queue_stack.addWidget(self.queue_empty)
|
||||
layout.addWidget(self.queue_stack, 1)
|
||||
@@ -6941,13 +6949,21 @@ class ReceptionPage(QWidget):
|
||||
self.refresh()
|
||||
|
||||
def _sync_queue_filter_summary(self) -> None:
|
||||
status = {
|
||||
None: "全部状态", 1: "待接诊", 2: "问诊中", 3: "已完成", 4: "已过号",
|
||||
}.get(self._queue_filter_status, "全部状态")
|
||||
status = STATUS_TEXT.get(self._queue_filter_status, "全部状态")
|
||||
today = date.today().isoformat()
|
||||
self.queue_title.setText(
|
||||
"历史接诊" if self._queue_date < today else
|
||||
"今日接诊" if self._queue_date == today else "预约接诊"
|
||||
)
|
||||
summary = f"{self._queue_date} · {status}"
|
||||
keyword = self.search_edit.text().strip()
|
||||
self.queue_filter_summary.setText(summary + (" · 姓名已筛选" if keyword else ""))
|
||||
self.queue_filter_summary.setToolTip(summary + (f" · 姓名:{keyword}" if keyword else ""))
|
||||
if hasattr(self, "queue_empty_description"):
|
||||
conditions = f"{status}、当前姓名" if keyword else status
|
||||
self.queue_empty_description.setText(
|
||||
f"{self._queue_date}\n当前账号在“{conditions}”筛选下没有挂号记录。"
|
||||
)
|
||||
|
||||
def _search_changed(self, value: str) -> None:
|
||||
if not value and self.isVisible():
|
||||
@@ -6964,6 +6980,15 @@ class ReceptionPage(QWidget):
|
||||
self.refresh()
|
||||
|
||||
def _queue_date_changed(self, iso_date: str) -> None:
|
||||
# Selecting a day opens that doctor's complete daily appointment list.
|
||||
self._queue_filter_status = None
|
||||
for value, button in self._queue_filter_buttons.items():
|
||||
button.setChecked(value is None)
|
||||
blocked = self.search_edit.blockSignals(True)
|
||||
try:
|
||||
self.search_edit.clear()
|
||||
finally:
|
||||
self.search_edit.blockSignals(blocked)
|
||||
self._queue_date = iso_date
|
||||
self._clear_selection()
|
||||
self._reset_queue_state()
|
||||
@@ -7401,7 +7426,7 @@ class ReceptionPage(QWidget):
|
||||
labels = {
|
||||
None: f"全部 {total}",
|
||||
1: f"待接诊 {counts.get(1, 0)}",
|
||||
2: f"问诊中 {counts.get(2, 0)}",
|
||||
2: f"已取消 {counts.get(2, 0)}",
|
||||
3: f"已完成 {counts.get(3, 0)}",
|
||||
}
|
||||
for status, button in self._queue_filter_buttons.items():
|
||||
|
||||
@@ -1319,9 +1319,15 @@ class ShellWindow(QMainWindow):
|
||||
search_layout = QHBoxLayout(search_host)
|
||||
search_layout.setContentsMargins(4, 0, 7, 0)
|
||||
search_layout.setSpacing(3)
|
||||
self.search_mark = QLabel(search_host)
|
||||
self.search_mark = QToolButton(search_host)
|
||||
self.search_mark.setObjectName("ShellSearchMark")
|
||||
self.search_mark.setFixedSize(20, 20)
|
||||
self.search_mark.setAutoRaise(True)
|
||||
self.search_mark.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.search_mark.setAccessibleName("搜索患者")
|
||||
self.search_mark.setToolTip("搜索患者(回车)")
|
||||
self.search_mark.setStyleSheet("background: transparent; border: 0; padding: 0;")
|
||||
self.search_mark.clicked.connect(self._run_global_search)
|
||||
self.search_mark.hide()
|
||||
search_layout.addWidget(self.search_mark)
|
||||
self.global_search = QLineEdit(search_host)
|
||||
@@ -1329,12 +1335,15 @@ class ShellWindow(QMainWindow):
|
||||
self.global_search.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||||
)
|
||||
self.global_search.setPlaceholderText("搜索患者姓名、手机号、病历号")
|
||||
self.global_search.setPlaceholderText("搜索患者姓名、手机号...")
|
||||
self.global_search.setToolTip("输入患者姓名或手机号,按回车或点击放大镜搜索")
|
||||
self.global_search.setClearButtonEnabled(True)
|
||||
self._global_search_icon_action = self.global_search.addAction(
|
||||
_painted_shell_icon("search", 16),
|
||||
QLineEdit.ActionPosition.LeadingPosition,
|
||||
)
|
||||
self._global_search_icon_action.setToolTip("搜索患者(回车)")
|
||||
self._global_search_icon_action.triggered.connect(self._run_global_search)
|
||||
self.global_search.returnPressed.connect(self._run_global_search)
|
||||
search_layout.addWidget(self.global_search, 1)
|
||||
shortcut_hint = QLabel("Ctrl K", search_host)
|
||||
@@ -1614,7 +1623,7 @@ class ShellWindow(QMainWindow):
|
||||
self.search_host.layout().setSpacing(6 if active else 3)
|
||||
self.search_mark.setVisible(active)
|
||||
self._global_search_icon_action.setVisible(not active)
|
||||
self.global_search.setPlaceholderText("搜索患者姓名、手机号..." if active else "搜索患者姓名、手机号、病历号")
|
||||
self.global_search.setPlaceholderText("搜索患者姓名、手机号...")
|
||||
self.search_shortcut_hint.setText("Ctrl K")
|
||||
self.ai_top_button.setVisible(self._can_ai_assistant and not active)
|
||||
self.menu_sidebar_action.setVisible(active)
|
||||
@@ -1789,7 +1798,8 @@ class ShellWindow(QMainWindow):
|
||||
self._global_search_icon_action.setIcon(
|
||||
_painted_shell_icon("search", 20 if active else 16, reception=active)
|
||||
)
|
||||
self.search_mark.setPixmap(_painted_shell_icon("search", 20, reception=True).pixmap(20, 20))
|
||||
self.search_mark.setIcon(_painted_shell_icon("search", 20, reception=True))
|
||||
self.search_mark.setIconSize(QSize(20, 20))
|
||||
for key, button in self.nav_buttons.items():
|
||||
size = 20 if active else 18
|
||||
button.setProperty("receptionTechBlue", active)
|
||||
@@ -1819,46 +1829,20 @@ class ShellWindow(QMainWindow):
|
||||
|
||||
def _run_global_search(self) -> None:
|
||||
query = self.global_search.text().strip()
|
||||
if not query:
|
||||
self._focus_global_search()
|
||||
keys = ("patients", "consultations") if self._active_page_key == "patients" else (
|
||||
"consultations", "patients",
|
||||
)
|
||||
for key in keys:
|
||||
# Only use patient pages already registered by permission-aware navigation.
|
||||
page = self.pages.get(key)
|
||||
search = getattr(page, "search_patient", None)
|
||||
if page is None or key not in self.nav_buttons or not callable(search):
|
||||
continue
|
||||
self._navigate(
|
||||
self.stack.indexOf(page), key, on_activate=lambda search=search: search(query),
|
||||
)
|
||||
return
|
||||
page = self.stack.currentWidget()
|
||||
if page is None:
|
||||
return
|
||||
target: QLineEdit | None = None
|
||||
for attribute in (
|
||||
"patient_input",
|
||||
"keyword_edit",
|
||||
"search_edit",
|
||||
"patient_filter",
|
||||
"name_filter",
|
||||
"sn_filter",
|
||||
):
|
||||
candidate = getattr(page, attribute, None)
|
||||
if isinstance(candidate, QLineEdit) and not candidate.isHidden():
|
||||
target = candidate
|
||||
break
|
||||
if target is None:
|
||||
for candidate in page.findChildren(QLineEdit):
|
||||
placeholder = candidate.placeholderText()
|
||||
if not candidate.isHidden() and any(
|
||||
marker in placeholder
|
||||
for marker in ("患者", "姓名", "手机号", "处方")
|
||||
):
|
||||
target = candidate
|
||||
break
|
||||
if target is None:
|
||||
show_toast(self, "当前页面没有可用的快捷搜索条件。", "warning")
|
||||
return
|
||||
target.setText(query)
|
||||
search = getattr(page, "_search", None)
|
||||
if callable(search):
|
||||
search()
|
||||
else:
|
||||
refresh = getattr(page, "refresh", None)
|
||||
if callable(refresh):
|
||||
refresh()
|
||||
show_toast(self, f"正在当前页面搜索“{query}”。", "info")
|
||||
show_toast(self, "当前账号没有可用的患者搜索页面。", "warning")
|
||||
|
||||
def _open_ai_assistant(self) -> None:
|
||||
if not self._can_ai_assistant:
|
||||
@@ -2109,18 +2093,25 @@ class ShellWindow(QMainWindow):
|
||||
self._ensure_activation_refresh(page, generation)
|
||||
self._activation_page = None
|
||||
|
||||
def _navigate(self, index: int, key: str) -> None:
|
||||
def _navigate(
|
||||
self, index: int, key: str, *, on_activate: Callable[[], None] | None = None,
|
||||
) -> None:
|
||||
if index < 0 or index >= self.stack.count():
|
||||
return
|
||||
page = self.stack.widget(index)
|
||||
if page is None:
|
||||
return
|
||||
if self._active_page_key == key and self.stack.currentWidget() is page:
|
||||
if on_activate is not None:
|
||||
self._activation_refreshed = False
|
||||
on_activate()
|
||||
return
|
||||
self._activation_generation += 1
|
||||
activation_generation = self._activation_generation
|
||||
self._activation_page = page
|
||||
self._activation_refreshed = False
|
||||
if on_activate is not None:
|
||||
on_activate()
|
||||
# A stack swap is otherwise a hard cut between two frames: this is the
|
||||
# transition a doctor sees on every sidebar click all day.
|
||||
motion.switch_stack(self.stack, index)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Global patient search must not inherit a narrow queue or stale pagination."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
import pytest
|
||||
from PySide6.QtCore import QDate, Signal
|
||||
from PySide6.QtWidgets import QApplication, QWidget
|
||||
|
||||
from doctor_workstation.services.repository import RemoteDoctorRepository
|
||||
from doctor_workstation.ui.pages import consultations as module
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self) -> None:
|
||||
self.calls = []
|
||||
|
||||
def get(self, endpoint: str, params: Any = None) -> Any:
|
||||
params = dict(params or {})
|
||||
self.calls.append((endpoint, params))
|
||||
page = params.get("page_no", 1)
|
||||
return {"lists": [{"id": page, "patient_name": "测试患者"}], "count": 2}
|
||||
|
||||
|
||||
class DetailStub(QWidget):
|
||||
saved = Signal()
|
||||
|
||||
def __init__(self, _repository: Any, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def application() -> QApplication:
|
||||
return QApplication.instance() or QApplication([])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def page(application: QApplication, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(module, "DiagnosisDialog", DetailStub)
|
||||
|
||||
def immediate(function: Any, **callbacks: Any) -> None:
|
||||
callbacks["on_success"](function())
|
||||
if callbacks.get("on_finished"):
|
||||
callbacks["on_finished"]()
|
||||
|
||||
monkeypatch.setattr(module, "run_async", immediate)
|
||||
client = Client()
|
||||
view = module.ConsultationsPage(RemoteDoctorRepository(client))
|
||||
monkeypatch.setattr(view, "_refresh_counts", lambda: None)
|
||||
yield view, client
|
||||
view.poll_timer.stop()
|
||||
view.pager.invalidate()
|
||||
view.close()
|
||||
view.deleteLater()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("query", [" 林患者 ", "13800138000", "138", "", " "])
|
||||
def test_global_search_submits_only_new_keyword_and_normal_scoped_list(page, query):
|
||||
view, client = page
|
||||
view.patient_name_edit.setText("旧患者")
|
||||
view.doctor_edit.setText("旧医生")
|
||||
view.keyword_edit.setText("旧手机号")
|
||||
view.pending_assign_keyword.setText("旧待分配关键词")
|
||||
view._pending_assign = "1"
|
||||
view._pending_booking = "1"
|
||||
view._completed_appointment = "1"
|
||||
for combo in (view.department_combo, view.assistant_combo, view.channel_combo):
|
||||
combo.addItem("旧筛选", 123)
|
||||
combo.setCurrentIndex(combo.count() - 1)
|
||||
view.latest_appointment_start_date.setDate(QDate(2026, 1, 3))
|
||||
view.latest_appointment_end_date.setDate(QDate(2026, 1, 1))
|
||||
view.latest_assign_start_date.setDate(QDate(2026, 1, 3))
|
||||
view.latest_assign_end_date.setDate(QDate(2026, 1, 1))
|
||||
view.unserved_sort_combo.setCurrentIndex(1)
|
||||
client.calls.clear()
|
||||
|
||||
view.search_patient(query)
|
||||
|
||||
assert len(client.calls) == 1
|
||||
endpoint, params = client.calls[0]
|
||||
assert endpoint == "tcm.diagnosis/lists"
|
||||
assert params.get("keyword", "") == query.strip()
|
||||
assert params["page_no"] == 1
|
||||
assert all(not value for key, value in params.items() if key not in {
|
||||
"keyword", "page_no", "page_size",
|
||||
})
|
||||
assert view.keyword_edit.text() == query.strip()
|
||||
assert view.patient_name_edit.text() == view.pending_assign_keyword.text() == ""
|
||||
assert view._appointment_date == view._appointment_status_filter == ""
|
||||
assert view._pending_assign == ""
|
||||
assert view.table_host.model._sort_direction == ""
|
||||
assert view.table_host.model.rowCount() == 1
|
||||
|
||||
|
||||
def test_repeating_global_search_starts_at_first_page(page):
|
||||
view, client = page
|
||||
view.search_patient("林患者")
|
||||
view.pager.load_more()
|
||||
assert view.pager.page == 2
|
||||
client.calls.clear()
|
||||
view.search_patient("林患者")
|
||||
assert [params["page_no"] for _, params in client.calls] == [1]
|
||||
assert view.pager.page == 1
|
||||
assert view.table_host.model.rowCount() == 1
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Global patient searches enter the patient list with one scoped request."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
import pytest
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from doctor_workstation.core import PermissionSet
|
||||
from doctor_workstation.ui.pages import patients as patients_module
|
||||
from doctor_workstation.ui.pages.patients import PatientsPage
|
||||
|
||||
|
||||
class Repository:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[str, dict[str, Any]]] = []
|
||||
|
||||
def _result(self, kind: str, query: dict[str, Any]) -> dict[str, Any]:
|
||||
self.calls.append((kind, dict(query)))
|
||||
start = (query["page_no"] - 1) * query["page_size"]
|
||||
return {
|
||||
"lists": [
|
||||
{
|
||||
"id": index + 100,
|
||||
"diagnosis_id": index + 100,
|
||||
"patient_name": f"患者 {index}",
|
||||
"appointment_status": 1,
|
||||
"queue_status": "waiting",
|
||||
}
|
||||
for index in range(start, min(start + query["page_size"], 37))
|
||||
],
|
||||
"count": 37,
|
||||
"extend": {"scope": {"label": "本人患者"}, "summary": {}},
|
||||
}
|
||||
|
||||
def list_patients(self, **query: Any) -> dict[str, Any]:
|
||||
return self._result("patients", query)
|
||||
|
||||
def patient_orders(self, **query: Any) -> dict[str, Any]:
|
||||
return self._result("orders", query)
|
||||
|
||||
def patient_progress(self, **query: Any) -> dict[str, Any]:
|
||||
return self._result("progress", query)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def application() -> QApplication:
|
||||
return QApplication.instance() or QApplication([])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def page(application: QApplication, monkeypatch: pytest.MonkeyPatch):
|
||||
def immediate(function: Any, **callbacks: Any) -> object:
|
||||
result = function()
|
||||
callbacks["on_success"](result)
|
||||
if callbacks.get("on_finished"):
|
||||
callbacks["on_finished"]()
|
||||
return object()
|
||||
|
||||
monkeypatch.setattr(patients_module, "run_async", immediate)
|
||||
repository = Repository()
|
||||
widget = PatientsPage(repository, PermissionSet(["firstvisit.myPatient/lists"]))
|
||||
widget.resize(1280, 900)
|
||||
widget.show()
|
||||
application.processEvents()
|
||||
yield widget, repository
|
||||
for timer in widget.findChildren(QTimer):
|
||||
timer.stop()
|
||||
widget.close()
|
||||
widget.deleteLater()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def assert_patient_query(repository: Repository, keyword: str) -> None:
|
||||
assert repository.calls == [
|
||||
(
|
||||
"patients",
|
||||
{
|
||||
"page_no": 1,
|
||||
"page_size": 15,
|
||||
"keyword": keyword,
|
||||
"status": "",
|
||||
"start_date": "",
|
||||
"end_date": "",
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tab_index", [0, 1, 2], ids=["patients", "orders", "progress"])
|
||||
@pytest.mark.parametrize("expanded", [False, True], ids=["collapsed", "expanded"])
|
||||
def test_search_enters_patient_list_and_clears_old_filters(
|
||||
page, monkeypatch: pytest.MonkeyPatch, tab_index: int, expanded: bool
|
||||
) -> None:
|
||||
widget, repository = page
|
||||
workspace = widget.patient_workspace
|
||||
workspace.keyword_edit.setText("旧患者")
|
||||
workspace.status_combo.setCurrentIndex(2)
|
||||
workspace.set_date_mode("today", refresh=False)
|
||||
widget.filter_disclosures[0].set_expanded(expanded)
|
||||
widget.order_workspace.keyword_edit.setText("旧订单")
|
||||
widget.tabs.setCurrentIndex(tab_index)
|
||||
repository.calls.clear()
|
||||
refreshes: list[bool] = []
|
||||
original_refresh = widget.refresh
|
||||
|
||||
def refresh() -> None:
|
||||
refreshes.append(True)
|
||||
original_refresh()
|
||||
|
||||
monkeypatch.setattr(widget, "refresh", refresh)
|
||||
widget.search_patient(" 13800138000 ")
|
||||
|
||||
assert widget.tabs.currentWidget() is workspace
|
||||
assert_patient_query(repository, "13800138000")
|
||||
assert refreshes == [True]
|
||||
assert workspace.keyword_edit.text() == "13800138000"
|
||||
assert workspace._date_mode == "all"
|
||||
assert workspace.status_buttons[""].isChecked()
|
||||
assert workspace._page == workspace.pager.page == 1
|
||||
assert widget.filter_disclosures[0].expanded is expanded
|
||||
assert widget.order_workspace.keyword_edit.text() == "旧订单"
|
||||
assert not widget.progress_workspace.timer.isActive()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("query", ["", " "], ids=["empty", "whitespace"])
|
||||
def test_empty_search_clears_keyword_even_when_patient_page_is_hidden(page, query: str) -> None:
|
||||
widget, repository = page
|
||||
widget.patient_workspace.keyword_edit.setText("旧患者")
|
||||
widget.patient_workspace.status_combo.setCurrentIndex(3)
|
||||
widget.tabs.setCurrentIndex(1)
|
||||
widget.hide()
|
||||
repository.calls.clear()
|
||||
|
||||
widget.search_patient(query)
|
||||
|
||||
assert widget.tabs.currentIndex() == 0
|
||||
assert widget.patient_workspace.keyword_edit.text() == ""
|
||||
assert_patient_query(repository, "")
|
||||
|
||||
|
||||
def test_repeated_search_discards_previously_loaded_pages(page) -> None:
|
||||
widget, repository = page
|
||||
widget.search_patient("患者")
|
||||
workspace = widget.patient_workspace
|
||||
workspace.pager.load_more()
|
||||
assert workspace._page == workspace.pager.page == 2
|
||||
repository.calls.clear()
|
||||
|
||||
widget.search_patient("患者")
|
||||
|
||||
assert_patient_query(repository, "患者")
|
||||
assert workspace._page == workspace.pager.page == 1
|
||||
assert workspace.table.rowCount() == 15
|
||||
|
||||
|
||||
def test_reset_button_still_accepts_qt_clicked_signal(page) -> None:
|
||||
widget, repository = page
|
||||
workspace = widget.patient_workspace
|
||||
workspace.keyword_edit.setText("旧患者")
|
||||
workspace.status_combo.setCurrentIndex(2)
|
||||
repository.calls.clear()
|
||||
|
||||
workspace.reset_button.click()
|
||||
|
||||
assert workspace.keyword_edit.text() == ""
|
||||
assert_patient_query(repository, "")
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Historical reception queries retain their day and include finished appointments."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import date
|
||||
from typing import Any
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
import pytest
|
||||
from PySide6.QtCore import QDate, QTimer
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from doctor_workstation.core import PermissionSet
|
||||
from doctor_workstation.services.repository import RemoteDoctorRepository
|
||||
from doctor_workstation.ui.pages import reception as module
|
||||
from doctor_workstation.ui.widgets import StatusBadge
|
||||
|
||||
|
||||
class Today(date):
|
||||
@classmethod
|
||||
def today(cls):
|
||||
return cls(2026, 11, 6)
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def get(self, endpoint: str, params: Any = None):
|
||||
params = dict(params or {})
|
||||
self.calls.append((endpoint, params))
|
||||
assert endpoint == "doctor.appointment/lists"
|
||||
day = params["start_date"]
|
||||
statuses = [2, 3, 4] if day == "2026-10-17" else []
|
||||
rows = [
|
||||
{"id": status, "patient_name": "测试患者", "status": status,
|
||||
"status_desc": module.STATUS_TEXT[status], "appointment_date": day}
|
||||
for status in statuses
|
||||
if params.get("status") in (None, "", status)
|
||||
]
|
||||
offset = (params["page_no"] - 1) * params["page_size"]
|
||||
return {
|
||||
"lists": rows[offset:offset + params["page_size"]], "count": len(rows),
|
||||
"extend": {"status_count": {str(status): statuses.count(status) for status in range(1, 5)}},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def application():
|
||||
return QApplication.instance() or QApplication([])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def page(application, monkeypatch):
|
||||
def immediate(function, **callbacks):
|
||||
callbacks["on_success"](function())
|
||||
callbacks["on_finished"]()
|
||||
|
||||
monkeypatch.setattr(module, "date", Today)
|
||||
monkeypatch.setattr(module, "run_async", immediate)
|
||||
client = Client()
|
||||
widget = module.ReceptionPage(RemoteDoctorRepository(client), PermissionSet([]))
|
||||
# Keep this test on the list boundary, without loading patient clinical details.
|
||||
monkeypatch.setattr(widget, "_select_record", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(widget, "_maybe_load_more_queue", lambda: None)
|
||||
yield widget, client
|
||||
for timer in widget.findChildren(QTimer):
|
||||
timer.stop()
|
||||
widget.close()
|
||||
widget.deleteLater()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_historical_day_opens_all_statuses_and_labels_cancelled_correctly(page):
|
||||
widget, client = page
|
||||
widget.queue_date_button._pick_date(QDate(2026, 10, 17))
|
||||
assert widget.queue_title.text() == "历史接诊"
|
||||
assert widget._queue_filter_status is None
|
||||
assert widget._queue_filter_buttons[None].isChecked()
|
||||
assert [record.status for record in widget._queue_records] == [2, 3, 4]
|
||||
assert widget._queue_filter_buttons[None].text() == "全部 3"
|
||||
assert widget._queue_filter_buttons[2].text() == "已取消 1"
|
||||
row = widget.queue_list.itemWidget(widget.queue_list.item(0))
|
||||
assert any(badge.text() == "已取消" and not badge.isHidden() for badge in row.findChildren(StatusBadge))
|
||||
assert client.calls[-1][1]["start_date"] == client.calls[-1][1]["end_date"] == "2026-10-17"
|
||||
assert not client.calls[-1][1].get("status")
|
||||
assert "progress_board" not in client.calls[-1][1]
|
||||
assert "diag_scope_relax" not in client.calls[-1][1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [None, 1, 2, 3, 4])
|
||||
def test_history_status_refresh_and_poll_keep_exact_date(page, status):
|
||||
widget, client = page
|
||||
widget.queue_date_button._pick_date(QDate(2026, 10, 17))
|
||||
widget._set_queue_filter(status)
|
||||
widget.refresh(workspace_refresh=True)
|
||||
widget._poll_queue()
|
||||
assert all(params["start_date"] == params["end_date"] == "2026-10-17" for _, params in client.calls)
|
||||
assert client.calls[-1][1].get("status") == status
|
||||
assert widget._queue_filter_buttons[None].text() == "全部 3"
|
||||
if status == 2:
|
||||
assert "已取消" in widget.queue_filter_summary.text()
|
||||
|
||||
|
||||
def test_history_pagination_keeps_date_and_status(page):
|
||||
widget, client = page
|
||||
widget._queue_page_size = 2
|
||||
widget.queue_date_button._pick_date(QDate(2026, 10, 17))
|
||||
widget._load_more()
|
||||
assert [params["page_no"] for _, params in client.calls] == [1, 2]
|
||||
assert len(widget._queue_records) == 3
|
||||
assert client.calls[-1][1]["start_date"] == "2026-10-17"
|
||||
assert not client.calls[-1][1].get("status")
|
||||
|
||||
|
||||
def test_empty_day_explains_selected_date_and_explicit_today_selection_shows_all(page):
|
||||
widget, _client = page
|
||||
assert widget._queue_filter_status == 1
|
||||
widget.queue_date_button._pick_date(QDate(2026, 10, 16))
|
||||
assert widget.queue_stack.currentWidget() is widget.queue_empty
|
||||
assert "2026-10-16" in widget.queue_empty_description.text()
|
||||
assert "当前账号" in widget.queue_empty_description.text()
|
||||
assert "挂号记录" in widget.queue_empty_description.text()
|
||||
widget.queue_date_button._pick_date(QDate(2026, 11, 6))
|
||||
assert widget.queue_title.text() == "今日接诊"
|
||||
assert widget._queue_filter_status is None
|
||||
widget.queue_date_button._pick_date(QDate(2026, 11, 7))
|
||||
assert widget.queue_title.text() == "预约接诊"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("selected_date", ["2026-10-17", "2026-11-06", "2026-11-07"])
|
||||
def test_selecting_any_day_clears_old_name_and_status_with_one_request(page, application, selected_date):
|
||||
widget, client = page
|
||||
widget.show()
|
||||
application.processEvents()
|
||||
widget.search_edit.setText("之前查过的患者")
|
||||
widget._set_queue_filter(3)
|
||||
client.calls.clear()
|
||||
|
||||
widget.queue_date_button._pick_date(QDate.fromString(selected_date, "yyyy-MM-dd"))
|
||||
|
||||
assert len(client.calls) == 1
|
||||
endpoint, params = client.calls[0]
|
||||
assert endpoint == "doctor.appointment/lists"
|
||||
assert params["start_date"] == params["end_date"] == selected_date
|
||||
assert params["page_no"] == 1
|
||||
assert not params.get("patient_name")
|
||||
assert not params.get("status")
|
||||
assert widget.search_edit.text() == ""
|
||||
assert widget._queue_filter_buttons[None].isChecked()
|
||||
assert not any(key in params for key in ("progress_board", "diag_scope_relax", "assistant_id"))
|
||||
|
||||
|
||||
def test_calendar_reselects_same_day_and_deduplicates_click_activation(page, application):
|
||||
widget, client = page
|
||||
widget.show()
|
||||
application.processEvents()
|
||||
widget._set_queue_filter(3)
|
||||
widget.search_edit.setText("旧姓名")
|
||||
client.calls.clear()
|
||||
button = widget.queue_date_button
|
||||
calendar = button.calendarWidget()
|
||||
selected = QDate(2026, 11, 6)
|
||||
button.click()
|
||||
assert calendar.isVisible()
|
||||
calendar.clicked.emit(selected)
|
||||
calendar.activated.emit(selected)
|
||||
assert len(client.calls) == 1
|
||||
assert not calendar.isVisible()
|
||||
assert widget.search_edit.text() == ""
|
||||
assert widget._queue_filter_status is None
|
||||
button.click()
|
||||
calendar.activated.emit(selected)
|
||||
assert len(client.calls) == 2
|
||||
@@ -7,7 +7,8 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
import pytest
|
||||
from PySide6.QtCore import QSize, Qt
|
||||
from PySide6.QtWidgets import QApplication, QDialog, QFrame, QToolButton, QWidget
|
||||
from PySide6.QtTest import QTest
|
||||
from PySide6.QtWidgets import QApplication, QDialog, QFrame, QLineEdit, QToolButton, QWidget
|
||||
|
||||
from doctor_workstation.ui import shell as shell_module
|
||||
from doctor_workstation.ui.shell import NavigationItem, ShellWindow
|
||||
@@ -35,10 +36,15 @@ class _ShellPageDouble(QWidget):
|
||||
self.show_count = 0
|
||||
self.ai_context_available = False
|
||||
self.ai_open_count = 0
|
||||
self.search_queries: list[str] = []
|
||||
|
||||
def refresh(self) -> None:
|
||||
self.refresh_count += 1
|
||||
|
||||
def search_patient(self, query: str) -> None:
|
||||
self.search_queries.append(query)
|
||||
self.refresh()
|
||||
|
||||
def open_selected_ai_consult(self) -> bool:
|
||||
self.ai_open_count += 1
|
||||
return self.ai_context_available
|
||||
@@ -243,13 +249,75 @@ def test_registered_pages_are_not_top_level_windows(shell_window: ShellWindow) -
|
||||
assert not page.isWindow()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("origin,trigger,target", [
|
||||
("consultations", "enter", "consultations"),
|
||||
("appointments", "button", "consultations"),
|
||||
("prescriptions", "enter", "consultations"),
|
||||
("prescription_library", "button", "consultations"),
|
||||
("patients", "enter", "patients"),
|
||||
("legacy_reference", "action", "consultations"),
|
||||
])
|
||||
def test_topbar_search_routes_to_patient_page_once(
|
||||
shell_window: ShellWindow, application: QApplication, origin: str, trigger: str, target: str,
|
||||
) -> None:
|
||||
assert shell_window.navigate(origin)
|
||||
application.processEvents()
|
||||
unrelated = QLineEdit(shell_window.pages[origin])
|
||||
unrelated.setPlaceholderText("患者姓名")
|
||||
unrelated.setText("原页面条件")
|
||||
page = shell_window.pages[target]
|
||||
before = page.refresh_count
|
||||
shell_window.global_search.setText(" 13800138000 ")
|
||||
if trigger == "enter":
|
||||
QTest.keyClick(shell_window.global_search, Qt.Key.Key_Return)
|
||||
elif trigger == "button":
|
||||
QTest.mouseClick(shell_window.search_mark, Qt.MouseButton.LeftButton)
|
||||
else:
|
||||
shell_window._global_search_icon_action.trigger()
|
||||
application.processEvents()
|
||||
assert shell_window.stack.currentWidget() is page
|
||||
assert page.search_queries == ["13800138000"]
|
||||
assert page.refresh_count == before + 1
|
||||
assert unrelated.text() == "原页面条件"
|
||||
|
||||
|
||||
def test_topbar_search_uses_available_patient_page_and_empty_query_clears(
|
||||
shell_window: ShellWindow, application: QApplication,
|
||||
) -> None:
|
||||
shell_window.pages.pop("consultations")
|
||||
shell_window.global_search.setText("林患者")
|
||||
QTest.keyClick(shell_window.global_search, Qt.Key.Key_Return)
|
||||
application.processEvents()
|
||||
patient_page = shell_window.pages["patients"]
|
||||
assert shell_window.stack.currentWidget() is patient_page
|
||||
shell_window.global_search.clear()
|
||||
QTest.keyClick(shell_window.global_search, Qt.Key.Key_Return)
|
||||
assert patient_page.search_queries == ["林患者", ""]
|
||||
|
||||
|
||||
def test_topbar_search_without_patient_page_does_not_query_unrelated_pages(
|
||||
shell_window: ShellWindow, monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
shell_window.pages.pop("consultations")
|
||||
shell_window.pages.pop("patients")
|
||||
current = shell_window.stack.currentWidget()
|
||||
before = current.refresh_count
|
||||
messages = []
|
||||
monkeypatch.setattr(shell_module, "show_toast", lambda *args: messages.append(args))
|
||||
shell_window.global_search.setText("林患者")
|
||||
QTest.keyClick(shell_window.global_search, Qt.Key.Key_Return)
|
||||
assert current.refresh_count == before
|
||||
assert shell_window.stack.currentWidget() is current
|
||||
assert "没有可用的患者搜索页面" in messages[0][1]
|
||||
|
||||
|
||||
def test_reference_shell_has_integrated_search_ai_card_and_window_controls(
|
||||
shell_window: ShellWindow,
|
||||
) -> None:
|
||||
assert shell_window.navigate("legacy_reference")
|
||||
assert shell_window.windowFlags() & Qt.WindowType.FramelessWindowHint
|
||||
assert (
|
||||
shell_window.global_search.placeholderText() == "搜索患者姓名、手机号、病历号"
|
||||
shell_window.global_search.placeholderText() == "搜索患者姓名、手机号..."
|
||||
)
|
||||
assert shell_window.assistant_card.isVisible()
|
||||
assert shell_window.assistant_button.text() == "开始对话"
|
||||
|
||||
Reference in New Issue
Block a user