Merge branch 'codex/latest-appointment-filter'

This commit is contained in:
2026-06-06 15:59:19 +08:00
3 changed files with 329 additions and 2 deletions
@@ -28,8 +28,10 @@ use app\common\model\tcm\CallRecord;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminDept;
use app\common\model\auth\AdminRole;
use app\common\model\dict\DictData;
use app\common\lists\ListsSearchInterface;
use app\common\lists\Traits\HasDataScopeFilter;
use think\facade\Db;
/**
* 中医辨房病因诊单列表
@@ -39,6 +41,10 @@ use app\common\lists\Traits\HasDataScopeFilter;
class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
{
use HasDataScopeFilter;
/** 最近挂号筛选只统计这些有效状态:已预约、已完成、已过号 */
private const EFFECTIVE_APPOINTMENT_STATUSES = [1, 3, 4];
/**
* @notes 设置搜索条件
* @return array
@@ -133,6 +139,8 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
$query->whereExists("SELECT 1 FROM {$aptTbl} apt WHERE apt.patient_id = {$diagTbl}.id AND apt.status = 3");
}
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
$this->applyPendingAssignBusinessOrderMonthFilter($query);
// 仅已开方(待分配+关键词检索时不限制)
@@ -210,6 +218,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
->order('appointment_time', 'asc')
->order('id', 'asc');
$appointments = $subQuery->select()->toArray();
$latestAppointmentMap = $this->buildLatestAppointmentMap($diagnosisIds);
foreach ($appointments as $apt) {
$did = (int) ($apt['patient_id'] ?? 0);
@@ -240,6 +249,8 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
}
// 合并到诊单列表
foreach ($lists as &$item) {
$latestAppointment = $latestAppointmentMap[(int) $item['id']] ?? null;
$this->appendLatestAppointmentSummary($item, $latestAppointment);
$aptList = $appointmentMap[(int) $item['id']] ?? [];
if (!empty($aptList)) {
$apt = $aptList[0];
@@ -280,6 +291,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
}
} else {
foreach ($lists as &$item) {
$this->appendLatestAppointmentSummary($item, null);
$item['has_appointment'] = 0;
$item['appointment_id'] = null;
$item['appointment_status'] = 0;
@@ -558,6 +570,8 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
$query->whereExists("SELECT 1 FROM {$aptTbl} apt WHERE apt.patient_id = {$diagTbl}.id AND apt.status = 3");
}
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
// 仅已开方(待分配+关键词检索时不限制)
if (!$pendingWideSearch && isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') {
$rxTbl = (new Prescription())->getTable();
@@ -587,6 +601,204 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
return $query->count();
}
/**
* 最近一次有效挂号过滤:按 appointment_date DESC, appointment_time DESC, id DESC 取一条。
*
* @param mixed $query
*/
private function applyLatestAppointmentFilters($query, bool $pendingWideSearch): void
{
if ($pendingWideSearch) {
return;
}
$startDate = $this->normalizeYmd($this->params['latest_appointment_start_date'] ?? '');
$endDate = $this->normalizeYmd($this->params['latest_appointment_end_date'] ?? '');
$channelSource = trim((string) ($this->params['latest_appointment_channel_source'] ?? ''));
if ($startDate === '' && $endDate === '' && $channelSource === '') {
return;
}
$aptTbl = (new Appointment())->getTable();
$diagTbl = (new Diagnosis())->getTable();
$latestIdSql = $this->latestAppointmentIdSubSql($aptTbl, $diagTbl);
$conditions = ["latest_apt.id = ({$latestIdSql})"];
if ($startDate !== '') {
$conditions[] = "latest_apt.appointment_date >= '" . addslashes($startDate) . "'";
}
if ($endDate !== '') {
$conditions[] = "latest_apt.appointment_date <= '" . addslashes($endDate) . "'";
}
if ($channelSource !== '') {
$channelCond = $this->appointmentChannelConditionSql('latest_apt', $channelSource);
if ($channelCond === '') {
$query->whereRaw('0 = 1');
return;
}
$conditions[] = $channelCond;
}
$query->whereExists("SELECT 1 FROM {$aptTbl} latest_apt WHERE " . implode(' AND ', $conditions));
}
private function latestAppointmentIdSubSql(string $aptTbl, string $diagTbl): string
{
$statuses = implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES);
return "SELECT apt_latest.id FROM {$aptTbl} apt_latest "
. "WHERE apt_latest.patient_id = {$diagTbl}.id "
. "AND apt_latest.status IN ({$statuses}) "
. "ORDER BY apt_latest.appointment_date DESC, "
. "IFNULL(NULLIF(TRIM(apt_latest.appointment_time), ''), '00:00:00') DESC, "
. "apt_latest.id DESC LIMIT 1";
}
private function appointmentChannelConditionSql(string $alias, string $channelSource): string
{
$cols = $this->appointmentTableFields();
$hasChannelSource = in_array('channel_source', $cols, true);
$hasChannels = in_array('channels', $cols, true);
if (!$hasChannelSource && !$hasChannels) {
return '';
}
$quoted = "'" . addslashes($channelSource) . "'";
$parts = [];
if ($hasChannelSource) {
$parts[] = "{$alias}.channel_source = {$quoted}";
}
if ($hasChannels) {
$parts[] = "{$alias}.channels = {$quoted}";
if (is_numeric($channelSource)) {
$parts[] = "{$alias}.channels = " . (int) $channelSource;
}
}
$parts = array_values(array_unique($parts));
return $parts === [] ? '' : '(' . implode(' OR ', $parts) . ')';
}
/**
* @param array<int|string, mixed> $diagnosisIds
* @return array<int, array<string, mixed>>
*/
private function buildLatestAppointmentMap(array $diagnosisIds): array
{
$diagnosisIds = array_values(array_filter(array_unique(array_map('intval', $diagnosisIds)), static function (int $id): bool {
return $id > 0;
}));
if ($diagnosisIds === []) {
return [];
}
$cols = $this->appointmentTableFields();
$fields = ['id', 'patient_id', 'appointment_date', 'appointment_time', 'status'];
foreach (['channel_source', 'channel_source_detail', 'channels'] as $col) {
if (in_array($col, $cols, true)) {
$fields[] = $col;
}
}
$appointments = Appointment::whereIn('patient_id', $diagnosisIds)
->whereIn('status', self::EFFECTIVE_APPOINTMENT_STATUSES)
->field($fields)
->order('patient_id', 'asc')
->order('appointment_date', 'desc')
->orderRaw("IFNULL(NULLIF(TRIM(appointment_time), ''), '00:00:00') DESC")
->order('id', 'desc')
->select()
->toArray();
$map = [];
$channelNameByValue = DictData::where('type_value', 'channels')->column('name', 'value');
foreach ($appointments as $appointment) {
$diagnosisId = (int) ($appointment['patient_id'] ?? 0);
if ($diagnosisId <= 0 || isset($map[$diagnosisId])) {
continue;
}
$appointment['channel_source_desc'] = $this->appointmentChannelDesc($appointment, $channelNameByValue ?: []);
$map[$diagnosisId] = $appointment;
}
return $map;
}
/**
* @param array<string, mixed> $item
* @param array<string, mixed>|null $appointment
*/
private function appendLatestAppointmentSummary(array &$item, ?array $appointment): void
{
$item['latest_appointment_id'] = null;
$item['latest_appointment_time_text'] = '';
$item['latest_appointment_channel_source'] = '';
$item['latest_appointment_channel_source_desc'] = '';
$item['latest_appointment_channel_source_detail'] = '';
if ($appointment === null) {
return;
}
$timePart = (string) ($appointment['appointment_time'] ?? '');
if (strlen($timePart) > 5) {
$timePart = substr($timePart, 0, 5);
}
$rawChannel = trim((string) ($appointment['channel_source'] ?? ''));
if ($rawChannel === '' && isset($appointment['channels']) && $appointment['channels'] !== '' && $appointment['channels'] !== null) {
$rawChannel = trim((string) $appointment['channels']);
}
$item['latest_appointment_id'] = (int) ($appointment['id'] ?? 0);
$item['latest_appointment_time_text'] = trim((string) ($appointment['appointment_date'] ?? '') . ' ' . $timePart);
$item['latest_appointment_channel_source'] = $rawChannel;
$item['latest_appointment_channel_source_desc'] = (string) ($appointment['channel_source_desc'] ?? '');
$item['latest_appointment_channel_source_detail'] = trim((string) ($appointment['channel_source_detail'] ?? ''));
}
/**
* @param array<string, mixed> $appointment
* @param array<string, string> $channelNameByValue
*/
private function appointmentChannelDesc(array $appointment, array $channelNameByValue): string
{
$srcKey = trim((string) ($appointment['channel_source'] ?? ''));
if ($srcKey === '' && isset($appointment['channels']) && $appointment['channels'] !== '' && $appointment['channels'] !== null) {
$srcKey = trim((string) $appointment['channels']);
}
if ($srcKey === '') {
return '';
}
return (string) ($channelNameByValue[$srcKey] ?? $channelNameByValue[(string) (int) $srcKey] ?? $srcKey);
}
/**
* @return string[]
*/
private function appointmentTableFields(): array
{
static $fields = null;
if ($fields !== null) {
return $fields;
}
$tblFields = Db::name('doctor_appointment')->getTableFields();
$fields = is_array($tblFields) ? array_values(array_map('strval', $tblFields)) : [];
return $fields;
}
private function normalizeYmd($value): string
{
$date = trim((string) $value);
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) ? $date : '';
}
/**
* 待分配 Tab 且有关键词时:放宽列表条件(与 `applyDiagnosisListKeywordFilter` 判定一致)。
*/