新增
This commit is contained in:
@@ -155,7 +155,7 @@ export function firstVisitConversionOverview(params: FirstVisitConversionParams)
|
||||
}
|
||||
|
||||
export interface FirstVisitRegistrationStatsParams {
|
||||
time_type: 'today' | 'week' | 'month'
|
||||
time_type: 'today' | 'yesterday' | 'week' | 'month'
|
||||
dept_id?: number
|
||||
assistant_id?: number
|
||||
}
|
||||
@@ -169,7 +169,7 @@ export function firstVisitRegistrationStatsOverview(params: FirstVisitRegistrati
|
||||
}
|
||||
|
||||
export interface FirstVisitDoctorDashboardParams {
|
||||
time_type: 'today' | 'week' | 'month' | 'custom'
|
||||
time_type: 'today' | 'yesterday' | 'week' | 'month' | 'custom'
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
dept_id?: number
|
||||
|
||||
@@ -321,6 +321,7 @@ const query = reactive<FirstVisitDoctorDashboardParams>({
|
||||
const customDateRange = ref<string[]>([])
|
||||
const timeOptions = [
|
||||
{ label: '今日', value: 'today' },
|
||||
{ label: '昨天', value: 'yesterday' },
|
||||
{ label: '本周', value: 'week' },
|
||||
{ label: '本月', value: 'month' },
|
||||
{ label: '自定义', value: 'custom' }
|
||||
|
||||
@@ -350,7 +350,7 @@ const ProgressPanel = defineAsyncComponent(() => import('./components/ProgressPa
|
||||
const PaibanPanel = defineAsyncComponent(() => import('./components/PaibanPanel.vue'))
|
||||
|
||||
type StatusFilter = '' | 'unbooked' | 'pending_interview' | 'completed' | 'missed'
|
||||
type DateType = 'all' | 'today' | 'tomorrow' | 'day_after' | 'last7' | 'last30' | 'custom'
|
||||
type DateType = 'all' | 'yesterday' | 'today' | 'tomorrow' | 'day_after' | 'last7' | 'last30' | 'custom'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -401,6 +401,7 @@ const statusOptions: Array<{ label: string; value: StatusFilter }> = [
|
||||
|
||||
const dateOptions: Array<{ label: string; value: DateType }> = [
|
||||
{ label: '全部时间', value: 'all' },
|
||||
{ label: '昨天', value: 'yesterday' },
|
||||
{ label: '今日预约', value: 'today' },
|
||||
{ label: '明日', value: 'tomorrow' },
|
||||
{ label: '后天', value: 'day_after' },
|
||||
@@ -466,6 +467,9 @@ function selectDateType(type: DateType) {
|
||||
if (type === 'all') {
|
||||
formData.start_date = ''
|
||||
formData.end_date = ''
|
||||
} else if (type === 'yesterday') {
|
||||
formData.start_date = today.subtract(1, 'day').format('YYYY-MM-DD')
|
||||
formData.end_date = formData.start_date
|
||||
} else if (type === 'today') {
|
||||
formData.start_date = today.format('YYYY-MM-DD')
|
||||
formData.end_date = formData.start_date
|
||||
|
||||
@@ -325,6 +325,7 @@ const dashboard = reactive(emptyDashboard())
|
||||
const query = reactive<FirstVisitRegistrationStatsParams>({ time_type: 'today' })
|
||||
const timeOptions = [
|
||||
{ label: '今日', value: 'today' },
|
||||
{ label: '昨天', value: 'yesterday' },
|
||||
{ label: '本周', value: 'week' },
|
||||
{ label: '本月', value: 'month' }
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,39 @@ def _truthy(value: Any) -> bool:
|
||||
return bool(value)
|
||||
|
||||
|
||||
def _row_mapping(value: Any) -> dict[str, Any]:
|
||||
"""Return only fields actually supplied by an API row when possible."""
|
||||
|
||||
if isinstance(value, Mapping):
|
||||
return dict(value)
|
||||
raw = getattr(value, "raw", None)
|
||||
if isinstance(raw, Mapping) and raw:
|
||||
return dict(raw)
|
||||
fields = getattr(value, "__dataclass_fields__", {})
|
||||
return {
|
||||
name: getattr(value, name, None)
|
||||
for name in fields
|
||||
if name != "raw"
|
||||
}
|
||||
|
||||
|
||||
def merge_prescription_detail(row: Any, detail: Any) -> dict[str, Any]:
|
||||
"""Match the admin editor's ``{...listRow, ...detail}`` state merge."""
|
||||
|
||||
merged = _row_mapping(row)
|
||||
merged.update(_row_mapping(detail))
|
||||
# Repository models retain the original API row in ``raw`` and expose
|
||||
# canonical alias fields (for example ``patient_phone`` -> ``phone``) on
|
||||
# the dataclass. Add those aliases without letting an omitted detail field
|
||||
# overwrite a list-only audit/void value with its model default.
|
||||
for source in (detail, row):
|
||||
fields = getattr(source, "__dataclass_fields__", {})
|
||||
for name in fields:
|
||||
if name != "raw" and name not in merged:
|
||||
merged[name] = getattr(source, name, None)
|
||||
return merged
|
||||
|
||||
|
||||
def prescription_status(row: Any) -> tuple[str, str]:
|
||||
"""Return the admin audit-column status; void state has its own column."""
|
||||
|
||||
@@ -730,7 +763,11 @@ class PrescriptionsPage(QWidget):
|
||||
or not can_edit_or_delete(row)
|
||||
):
|
||||
return
|
||||
self._load_detail(row, self._open_editor, message="正在准备编辑处方…")
|
||||
self._load_detail(
|
||||
row,
|
||||
lambda detail: self._open_editor(merge_prescription_detail(row, detail)),
|
||||
message="正在准备编辑处方…",
|
||||
)
|
||||
|
||||
def _open_editor(self, detail: Any) -> None:
|
||||
dialog = PrescriptionEditorDialog(
|
||||
|
||||
@@ -69,13 +69,13 @@ def test_editor_is_a_fixed_header_body_footer_right_drawer(
|
||||
|
||||
origin = host.mapToGlobal(QPoint(0, 0))
|
||||
assert editor.windowFlags() & Qt.WindowType.FramelessWindowHint
|
||||
assert editor.width() == 1200
|
||||
assert editor.width() == 860
|
||||
assert editor.height() == host.height()
|
||||
assert editor.x() + editor.width() == origin.x() + host.width()
|
||||
assert editor.y() == origin.y()
|
||||
assert editor.save_button.text() == "确定"
|
||||
assert editor.windowTitle() == "新增处方"
|
||||
assert not editor.findChildren(QTabWidget)
|
||||
assert editor.tabs.count() == 4
|
||||
assert editor.tabs.tabText(2) == "剂型与用法"
|
||||
assert editor.body_scroll.verticalScrollBar().maximum() > 0
|
||||
assert editor.body_scroll.horizontalScrollBar().maximum() == 0
|
||||
|
||||
@@ -110,7 +110,35 @@ def test_drawer_uses_full_width_on_a_narrow_host(application: QApplication) -> N
|
||||
assert editor.width() == host.width()
|
||||
assert editor.x() == origin.x()
|
||||
assert editor.body_scroll.horizontalScrollBar().maximum() == 0
|
||||
assert editor.herbs._grid_columns == 3
|
||||
assert editor.herbs.table_mode
|
||||
assert editor.add_main_button.text() == "添加主方药材"
|
||||
|
||||
editor.close()
|
||||
host.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_decoction_usage_fields_pack_without_a_hidden_column_gap(
|
||||
application: QApplication,
|
||||
) -> None:
|
||||
host = QWidget()
|
||||
host.resize(1440, 900)
|
||||
host.show()
|
||||
source = _seed()
|
||||
source["prescription_type"] = "饮片"
|
||||
editor = PrescriptionEditorDialog(_repository(), source, parent=host)
|
||||
_show(editor, application)
|
||||
|
||||
assert editor._main_decoction_field.isVisible()
|
||||
assert editor._main_bags_field.isVisible()
|
||||
assert editor._main_bag_field.isHidden()
|
||||
assert editor._main_dosage_field.y() == editor._main_decoction_field.y()
|
||||
gap = editor._main_decoction_field.x() - (
|
||||
editor._main_dosage_field.x() + editor._main_dosage_field.width()
|
||||
)
|
||||
assert 0 <= gap <= 24
|
||||
assert abs(editor._main_dosage_field.width() - editor._main_decoction_field.width()) < 40
|
||||
assert editor.cancel_button.x() < editor.save_button.x()
|
||||
|
||||
editor.close()
|
||||
host.close()
|
||||
|
||||
@@ -74,6 +74,8 @@ class _DiagnosisRepository:
|
||||
"height": 162.5,
|
||||
"weight": 52.0,
|
||||
"fasting_blood_sugar": "6.2",
|
||||
"diagnosis_type": "first_visit",
|
||||
"local_hospital_name": "杭州市第一人民医院",
|
||||
"chief_complaint": "乏力",
|
||||
"symptoms": "口干",
|
||||
"appetite": ["一般", "少食"],
|
||||
|
||||
@@ -7,13 +7,13 @@ from typing import Any
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
import pytest
|
||||
from PySide6.QtCore import QEvent, QObject, QSize, Qt
|
||||
from PySide6.QtCore import QDate, QEvent, QObject, QSize, Qt
|
||||
from PySide6.QtGui import QColor, QImage, QPainter
|
||||
from PySide6.QtPdf import QPdfDocument
|
||||
from PySide6.QtWidgets import QApplication, QDialog, QWidget
|
||||
from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QPushButton, QWidget
|
||||
|
||||
from doctor_workstation import app as app_module
|
||||
from doctor_workstation.core import PermissionSet
|
||||
from doctor_workstation.core import PermissionSet, Prescription
|
||||
from doctor_workstation.services import DemoDoctorRepository
|
||||
from doctor_workstation.ui.dialogs import prescription as dialog_module
|
||||
from doctor_workstation.ui.dialogs.prescription import (
|
||||
@@ -325,6 +325,8 @@ def test_editor_builds_complete_add_payload(
|
||||
)
|
||||
user = SimpleNamespace(id=7, name="周医生")
|
||||
editor = PrescriptionEditorDialog(repository, mode="add", current_user=user)
|
||||
# The admin editor starts with an empty table; adding a herb is explicit.
|
||||
editor.herbs.add_row(formula_type="主方")
|
||||
editor.patient_name.setText("林晓岚")
|
||||
editor.clinical_diagnosis.setPlainText("脾气虚")
|
||||
editor.herbs.rows[0].medicine.set_value(31, "黄芪")
|
||||
@@ -465,6 +467,7 @@ def test_editor_matches_admin_four_observation_fields_and_edit_context(
|
||||
"clinical_diagnosis": "气阴两虚",
|
||||
"doctor_name": "周医生",
|
||||
"audit_status": 2,
|
||||
"audit_remark": "请补充辨证依据",
|
||||
"void_status": 1,
|
||||
"is_system_auto": 1,
|
||||
"business_prescription_audit_rejected": 1,
|
||||
@@ -473,6 +476,12 @@ def test_editor_matches_admin_four_observation_fields_and_edit_context(
|
||||
}
|
||||
editor = PrescriptionEditorDialog(Repository(), source, mode="edit")
|
||||
|
||||
assert editor.windowTitle() == "编辑处方"
|
||||
assert editor.patient_name.isReadOnly()
|
||||
assert editor.visit_no.isReadOnly()
|
||||
assert not hasattr(editor, "phone")
|
||||
assert editor.gender_male.isChecked()
|
||||
assert editor.payload()["gender"] == 1
|
||||
assert editor.tongue.text() == "面象哨兵"
|
||||
assert editor.tongue_image.text() == "舌象哨兵"
|
||||
assert editor.pulse.text() == "脉象哨兵"
|
||||
@@ -483,11 +492,17 @@ def test_editor_matches_admin_four_observation_fields_and_edit_context(
|
||||
assert payload["pulse"] == "脉象哨兵"
|
||||
assert payload["pulse_condition"] == "脉象详情哨兵"
|
||||
assert "取消作废、清除驳回" in editor.context_banner.label.text()
|
||||
assert "空白处方" in editor.context_banner.label.text()
|
||||
assert "系统自动生成" in editor.context_banner.label.text()
|
||||
assert "请补充辨证依据" in editor.context_banner.label.text()
|
||||
assert "业务订单侧" not in editor.context_banner.label.text()
|
||||
assert calls == [{"page_no": 1, "page_size": 5, "prescription_id": 91}]
|
||||
assert "PO-901" in editor.linked_order_banner.label.text()
|
||||
assert "14 天" in editor.linked_order_banner.label.text()
|
||||
assert "按两周疗程复核" in editor.linked_order_banner.label.text()
|
||||
assert editor.linked_order_banner.objectName() == "MessageBanner"
|
||||
assert editor.linked_order_banner.property("kind") == "warning"
|
||||
assert "#FDF6EC" in editor.drawer_surface.styleSheet()
|
||||
assert 'QFrame#MessageBanner[kind="warning"]' in editor.drawer_surface.styleSheet()
|
||||
editor.close()
|
||||
application.processEvents()
|
||||
|
||||
@@ -516,6 +531,9 @@ def test_editor_preserves_global_lock_and_admin_type_constraints(
|
||||
)
|
||||
assert editor.herbs.locked
|
||||
assert all(row.locked for row in editor.herbs.rows)
|
||||
locked_payload = editor.payload()["herbs"]
|
||||
assert locked_payload[0]["locked"] is True
|
||||
assert "locked" not in locked_payload[1]
|
||||
assert not editor.add_main_button.isEnabled()
|
||||
assert editor.import_library_button.isHidden()
|
||||
assert editor._aux_name_field.isHidden()
|
||||
@@ -537,6 +555,144 @@ def test_editor_preserves_global_lock_and_admin_type_constraints(
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_editor_usage_fields_are_dropdowns_and_date_opens_calendar(
|
||||
application: QApplication,
|
||||
) -> None:
|
||||
editor = PrescriptionEditorDialog(
|
||||
SimpleNamespace(),
|
||||
{
|
||||
"prescription_type": "饮片",
|
||||
"prescription_date": "2026-08-01",
|
||||
"herbs": [{"medicine_id": 31, "name": "黄芪", "dosage": 15}],
|
||||
},
|
||||
mode="edit",
|
||||
)
|
||||
|
||||
assert isinstance(editor.dosage_amount, QComboBox)
|
||||
assert not editor.dosage_amount.isEditable()
|
||||
assert editor.dosage_amount.itemText(0).endswith("ml")
|
||||
assert isinstance(editor.bags_per_dose, QComboBox)
|
||||
assert editor.bags_per_dose.itemText(0) == "1包"
|
||||
assert editor.bags_per_dose.maximum() == 9
|
||||
assert isinstance(editor.dosage_bag_count, QComboBox)
|
||||
assert editor.dosage_bag_count.itemText(0) == "1袋"
|
||||
assert isinstance(editor.prescription_type, QComboBox)
|
||||
assert isinstance(editor.usage_time, QComboBox)
|
||||
assert isinstance(editor.usage_way, QComboBox)
|
||||
assert "down-arrow" in editor.drawer_surface.styleSheet()
|
||||
assert "__CHEVRON_URL__" not in editor.drawer_surface.styleSheet()
|
||||
assert editor.prescription_type.cursor().shape() == Qt.CursorShape.PointingHandCursor
|
||||
assert editor.date_edit.cursor().shape() == Qt.CursorShape.PointingHandCursor
|
||||
assert editor.date_edit.calendarPopup()
|
||||
calendar_button = editor.date_edit.findChild(QPushButton, "PrescriptionDateButton")
|
||||
assert calendar_button is not None
|
||||
assert not calendar_button.icon().isNull()
|
||||
assert editor.date_edit.date() == QDate(2026, 8, 1)
|
||||
editor.show()
|
||||
application.processEvents()
|
||||
editor.date_edit.open_calendar()
|
||||
application.processEvents()
|
||||
assert editor.date_edit.calendarWidget().isVisible()
|
||||
editor.date_edit.calendarWidget().clicked.emit(QDate(2026, 8, 13))
|
||||
application.processEvents()
|
||||
assert not editor.date_edit.calendarWidget().isVisible()
|
||||
assert editor.payload()["prescription_date"] == "2026-08-13"
|
||||
editor.bags_per_dose.setValue(4)
|
||||
assert editor.payload()["bags_per_dose"] == 4
|
||||
|
||||
editor.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_editor_matches_admin_empty_rows_dosage_choices_and_detail_merge(
|
||||
application: QApplication,
|
||||
) -> None:
|
||||
editor = PrescriptionEditorDialog(
|
||||
SimpleNamespace(),
|
||||
mode="add",
|
||||
current_user=SimpleNamespace(id=7, name="周医生"),
|
||||
)
|
||||
|
||||
assert editor.herbs.rows == []
|
||||
editor.herbs.add_row(formula_type="主方")
|
||||
assert editor.payload()["herbs"] == [
|
||||
{"name": "", "dosage": 0.0, "formula_type": "主方"}
|
||||
]
|
||||
|
||||
editor.need_decoction.setChecked(True)
|
||||
editor.prescription_type.setCurrentIndex(editor.prescription_type.findData("饮片"))
|
||||
assert editor.need_decoction.isChecked()
|
||||
assert [
|
||||
editor.dosage_amount.itemData(index)
|
||||
for index in range(editor.dosage_amount.count())
|
||||
] == [50.0, 100.0, 120.0, 150.0, 180.0, 200.0, 250.0]
|
||||
|
||||
editor.prescription_type.setCurrentIndex(editor.prescription_type.findData("颗粒"))
|
||||
assert editor.dosage_amount.optional_value() is None
|
||||
assert "dosage_amount" not in editor.payload()
|
||||
|
||||
merged = prescription_module.merge_prescription_detail(
|
||||
{
|
||||
"id": 91,
|
||||
"audit_status": 1,
|
||||
"void_status": 1,
|
||||
"business_prescription_audit_rejected": 1,
|
||||
"business_prescription_audit_remark": "订单驳回",
|
||||
},
|
||||
{"id": 91, "patient_name": "林晓岚", "herbs": []},
|
||||
)
|
||||
assert merged["patient_name"] == "林晓岚"
|
||||
assert merged["void_status"] == 1
|
||||
assert merged["business_prescription_audit_rejected"] == 1
|
||||
assert merged["business_prescription_audit_remark"] == "订单驳回"
|
||||
|
||||
model_detail = Prescription.from_dict(
|
||||
{"id": 91, "patient_phone": "13800138000", "patient_name": "林晓岚"}
|
||||
)
|
||||
model_merged = prescription_module.merge_prescription_detail(
|
||||
{"id": 91, "void_status": 1},
|
||||
model_detail,
|
||||
)
|
||||
assert model_merged["phone"] == "13800138000"
|
||||
assert model_merged["void_status"] == 1
|
||||
|
||||
editor.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_paste_parser_matches_admin_zero_and_note_rules() -> None:
|
||||
assert parse_pasted_herbs("黄芪0g") == []
|
||||
assert parse_pasted_herbs("黄芪(炙)、党参各6g") == [
|
||||
{"name": "黄芪", "dosage": 6.0},
|
||||
{"name": "党参", "dosage": 6.0},
|
||||
]
|
||||
|
||||
|
||||
def test_editor_only_shows_business_rejection_for_consumer_approved_rows(
|
||||
application: QApplication,
|
||||
) -> None:
|
||||
editor = PrescriptionEditorDialog(
|
||||
SimpleNamespace(),
|
||||
{
|
||||
"id": 92,
|
||||
"audit_status": 1,
|
||||
"business_prescription_audit_rejected": 1,
|
||||
"business_prescription_audit_remark": "重新核对剂量",
|
||||
"patient_name": "林晓岚",
|
||||
"clinical_diagnosis": "气阴两虚",
|
||||
"doctor_name": "周医生",
|
||||
"herbs": [{"name": "黄芪", "dosage": 15, "formula_type": "主方"}],
|
||||
},
|
||||
mode="edit",
|
||||
)
|
||||
|
||||
message = editor.context_banner.label.text()
|
||||
assert "业务订单侧「处方审核」已驳回" in message
|
||||
assert "重新核对剂量" in message
|
||||
editor.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_audit_reject_requires_remark(
|
||||
application: QApplication,
|
||||
) -> None:
|
||||
|
||||
@@ -150,7 +150,7 @@ class FirstVisitDoctorDashboardLogic
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
$type = (string) ($params['time_type'] ?? 'month');
|
||||
if (!in_array($type, ['today', 'week', 'month', 'custom'], true)) {
|
||||
if (!in_array($type, ['today', 'yesterday', 'week', 'month', 'custom'], true)) {
|
||||
$type = 'month';
|
||||
}
|
||||
|
||||
@@ -175,6 +175,11 @@ class FirstVisitDoctorDashboardLogic
|
||||
if ($type === 'today') {
|
||||
return ['type' => 'today', 'label' => '今日', 'start' => $today, 'end' => $today];
|
||||
}
|
||||
if ($type === 'yesterday') {
|
||||
$yesterday = date('Y-m-d', strtotime('-1 day'));
|
||||
|
||||
return ['type' => 'yesterday', 'label' => '昨天', 'start' => $yesterday, 'end' => $yesterday];
|
||||
}
|
||||
if ($type === 'week') {
|
||||
return [
|
||||
'type' => 'week', 'label' => '本周',
|
||||
|
||||
@@ -146,6 +146,17 @@ class FirstVisitRegistrationStatsLogic
|
||||
$today = date('Y-m-d');
|
||||
$tomorrow = date('Y-m-d', strtotime('+1 day'));
|
||||
$dayAfterTomorrow = date('Y-m-d', strtotime('+2 days'));
|
||||
if ($type === 'yesterday') {
|
||||
$yesterday = date('Y-m-d', strtotime('-1 day'));
|
||||
$dayBefore = date('Y-m-d', strtotime('-2 days'));
|
||||
|
||||
return [
|
||||
'type' => 'yesterday', 'label' => '昨天', 'start' => $yesterday, 'end' => $yesterday,
|
||||
'compare_start' => $dayBefore,
|
||||
'compare_end' => $dayBefore,
|
||||
'tomorrow' => $tomorrow, 'day_after_tomorrow' => $dayAfterTomorrow,
|
||||
];
|
||||
}
|
||||
if ($type === 'week') {
|
||||
$start = date('Y-m-d', strtotime('monday this week'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user