buildQuery(true, true); $appointmentTable = (new Appointment())->getTable(); $today = date('Y-m-d'); $upcomingSql = "SELECT MIN(CONCAT(sort_apt.appointment_date, ' ', IFNULL(NULLIF(TRIM(sort_apt.appointment_time), ''), '00:00:00')))" . " FROM {$appointmentTable} sort_apt" . ' WHERE sort_apt.patient_id = d.id' . ' AND sort_apt.status IN (1,4)' . " AND sort_apt.appointment_date >= '{$today}'"; $rows = $query ->field([ 'd.id', 'd.patient_id', 'd.patient_name', 'd.phone', 'd.id_card', 'd.gender', 'd.age', 'd.diagnosis_date', 'd.diagnosis_type', 'd.syndrome_type', 'd.assistant_id', 'd.assign_read_at', 'd.create_time', ]) ->orderRaw("CASE WHEN ({$upcomingSql}) IS NULL THEN 1 ELSE 0 END ASC") ->orderRaw("IFNULL(({$upcomingSql}), '9999-12-31 23:59:59') ASC") ->order('d.id', 'desc') ->limit($this->limitOffset, $this->limitLength) ->select() ->toArray(); return $this->appendRelations($rows); } public function count(): int { return (int) $this->buildQuery(true, true)->count('d.id'); } public function extend(): array { $today = date('Y-m-d'); $tomorrow = date('Y-m-d', strtotime('+1 day')); $dayAfter = date('Y-m-d', strtotime('+2 days')); return [ 'summary' => [ 'today' => $this->countByAppointmentDate($today), 'tomorrow' => $this->countByAppointmentDate($tomorrow), 'day_after' => $this->countByAppointmentDate($dayAfter), ], 'scope' => MyPatientLogic::scopeMeta($this->adminId, $this->adminInfo), 'dates' => [ 'today' => $today, 'tomorrow' => $tomorrow, 'day_after' => $dayAfter, ], ]; } private function buildQuery(bool $applyStatusFilter, bool $applyDateFilter): Query { $diagnosisTable = (new Diagnosis())->getTable(); $query = Db::table($diagnosisTable) ->alias('d') ->whereNull('d.delete_time') ->where('d.status', 1); MyPatientLogic::applyScope($query, $this->adminId, $this->adminInfo); $this->applyKeyword($query); $statusFilter = $applyStatusFilter ? trim((string) ($this->params['status_filter'] ?? '')) : ''; if ($statusFilter === 'unconfirmed') { $viewTable = (new DiagnosisViewRecord())->getTable(); $query->whereNotExists( "SELECT 1 FROM {$viewTable} confirm_row" . ' WHERE confirm_row.diagnosis_id = d.id' . ' AND confirm_row.is_confirmed = 1' . ' AND confirm_row.delete_time IS NULL' ); } $appointmentStatuses = self::EFFECTIVE_APPOINTMENT_STATUSES; if ($statusFilter === 'booked') { $appointmentStatuses = [1]; } elseif ($statusFilter === 'completed') { $appointmentStatuses = [3]; } elseif ($statusFilter === 'missed') { $appointmentStatuses = [4]; } $needsAppointmentFilter = in_array($statusFilter, ['booked', 'completed', 'missed'], true); [$startDate, $endDate] = $applyDateFilter ? $this->dateRange() : ['', '']; if ($startDate !== '' || $endDate !== '') { $needsAppointmentFilter = true; } if ($needsAppointmentFilter) { $appointmentTable = (new Appointment())->getTable(); $conditions = [ 'filter_apt.patient_id = d.id', 'filter_apt.status IN (' . implode(',', $appointmentStatuses) . ')', ]; if ($startDate !== '') { $conditions[] = "filter_apt.appointment_date >= '{$startDate}'"; } if ($endDate !== '') { $conditions[] = "filter_apt.appointment_date <= '{$endDate}'"; } $query->whereExists( "SELECT 1 FROM {$appointmentTable} filter_apt WHERE " . implode(' AND ', $conditions) ); } return $query; } private function applyKeyword(Query $query): void { $keyword = trim((string) ($this->params['keyword'] ?? '')); if ($keyword === '') { return; } $needle = addslashes($keyword); $adminTable = (new Admin())->getTable(); $appointmentTable = (new Appointment())->getTable(); $query->whereRaw( "(d.patient_name LIKE '%{$needle}%'" . " OR d.phone LIKE '%{$needle}%'" . " OR EXISTS (SELECT 1 FROM {$adminTable} assistant_admin" . ' WHERE assistant_admin.id = CAST(d.assistant_id AS UNSIGNED)' . ' AND assistant_admin.delete_time IS NULL' . " AND assistant_admin.name LIKE '%{$needle}%')" . " OR EXISTS (SELECT 1 FROM {$appointmentTable} keyword_apt" . " INNER JOIN {$adminTable} doctor_admin ON doctor_admin.id = keyword_apt.doctor_id" . ' AND doctor_admin.delete_time IS NULL' . ' WHERE keyword_apt.patient_id = d.id' . ' AND keyword_apt.status IN (1,3,4)' . " AND doctor_admin.name LIKE '%{$needle}%'))" ); } /** @return array{0:string,1:string} */ private function dateRange(): array { $startDate = $this->normalizeDate($this->params['start_date'] ?? ''); $endDate = $this->normalizeDate($this->params['end_date'] ?? ''); if ($startDate === '' && $endDate !== '') { $startDate = $endDate; } if ($endDate === '' && $startDate !== '') { $endDate = $startDate; } if ($startDate !== '' && $endDate !== '' && $startDate > $endDate) { [$startDate, $endDate] = [$endDate, $startDate]; } return [$startDate, $endDate]; } private function normalizeDate($value): string { $value = trim((string) $value); return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) ? $value : ''; } private function countByAppointmentDate(string $date): int { $query = $this->buildQuery(false, false); $appointmentTable = (new Appointment())->getTable(); $query->whereExists( "SELECT 1 FROM {$appointmentTable} summary_apt" . ' WHERE summary_apt.patient_id = d.id' . " AND summary_apt.appointment_date = '{$date}'" . ' AND summary_apt.status IN (1,3,4)' ); return (int) $query->count('d.id'); } /** * @param array> $rows * @return array> */ private function appendRelations(array $rows): array { if ($rows === []) { return []; } $diagnosisIds = array_values(array_unique(array_map('intval', array_column($rows, 'id')))); $assistantIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'assistant_id'))))); $appointmentTable = (new Appointment())->getTable(); $appointments = Db::table($appointmentTable) ->whereIn('patient_id', $diagnosisIds) ->whereIn('status', self::EFFECTIVE_APPOINTMENT_STATUSES) ->field(['id', 'patient_id', 'doctor_id', 'appointment_date', 'appointment_time', 'status']) ->order('appointment_date', 'asc') ->order('appointment_time', 'asc') ->order('id', 'asc') ->select() ->toArray(); $doctorIds = array_values(array_unique(array_filter(array_map('intval', array_column($appointments, 'doctor_id'))))); $adminIds = array_values(array_unique(array_merge($assistantIds, $doctorIds))); $adminNames = $adminIds === [] ? [] : Admin::whereIn('id', $adminIds)->whereNull('delete_time')->column('name', 'id'); $appointmentMap = []; foreach ($appointments as $appointment) { $diagnosisId = (int) ($appointment['patient_id'] ?? 0); if ($diagnosisId > 0) { $appointmentMap[$diagnosisId][] = $appointment; } } $viewTable = (new DiagnosisViewRecord())->getTable(); $confirmedIds = Db::table($viewTable) ->whereIn('diagnosis_id', $diagnosisIds) ->where('is_confirmed', 1) ->whereNull('delete_time') ->column('diagnosis_id'); $confirmedSet = array_fill_keys(array_map('intval', $confirmedIds), true); [$rangeStart, $rangeEnd] = $this->dateRange(); $today = date('Y-m-d'); $statusFilter = trim((string) ($this->params['status_filter'] ?? '')); $preferredStatuses = [ 'booked' => [1], 'completed' => [3], 'missed' => [4], ][$statusFilter] ?? []; foreach ($rows as &$row) { $diagnosisId = (int) $row['id']; $rowAppointments = $appointmentMap[$diagnosisId] ?? []; $primary = $this->pickPrimaryAppointment( $rowAppointments, $rangeStart, $rangeEnd, $today, $preferredStatuses ); $completedCount = count(array_filter($rowAppointments, static function (array $appointment): bool { return (int) ($appointment['status'] ?? 0) === 3; })); $assistantId = (int) ($row['assistant_id'] ?? 0); $row['diagnosis_id'] = $diagnosisId; $row['source_patient_id'] = (int) ($row['patient_id'] ?? 0); $row['phone_masked'] = $this->maskPhone((string) ($row['phone'] ?? '')); unset($row['phone']); $row['has_id_card'] = trim((string) ($row['id_card'] ?? '')) !== '' ? 1 : 0; unset($row['id_card']); $row['gender_desc'] = (int) ($row['gender'] ?? 0) === 1 ? '男' : '女'; $row['diagnosis_date_text'] = $this->formatDiagnosisDate($row['diagnosis_date'] ?? ''); $row['assistant_name'] = (string) ($adminNames[$assistantId] ?? '未分配'); $row['confirmed'] = isset($confirmedSet[$diagnosisId]) ? 1 : 0; $row['confirmation_text'] = $row['confirmed'] ? '已确认' : '待确认'; $row['visit_count'] = $completedCount; $row['revisit_count'] = max(0, $completedCount - 1); $row['appointment_id'] = $primary ? (int) $primary['id'] : 0; $row['appointment_status'] = $primary ? (int) $primary['status'] : 0; $row['appointment_status_text'] = $this->appointmentStatusText((int) ($primary['status'] ?? 0)); $row['appointment_doctor_id'] = $primary ? (int) $primary['doctor_id'] : 0; $row['appointment_doctor_name'] = $primary ? (string) ($adminNames[(int) $primary['doctor_id']] ?? '未知医生') : '未预约'; $row['appointment_time_text'] = $primary ? $this->appointmentTimeText($primary) : ''; $row['has_appointment'] = $primary !== null ? 1 : 0; } unset($row); return $rows; } /** * @param array> $appointments * @return array|null */ private function pickPrimaryAppointment( array $appointments, string $rangeStart, string $rangeEnd, string $today, array $preferredStatuses = [] ): ?array { if ($appointments === []) { return null; } $candidates = $appointments; if ($preferredStatuses !== []) { $candidates = array_values(array_filter($candidates, static function (array $appointment) use ($preferredStatuses): bool { return in_array((int) ($appointment['status'] ?? 0), $preferredStatuses, true); })); } if ($rangeStart !== '' || $rangeEnd !== '') { $candidates = array_values(array_filter($candidates, static function (array $appointment) use ($rangeStart, $rangeEnd): bool { $date = (string) ($appointment['appointment_date'] ?? ''); return ($rangeStart === '' || $date >= $rangeStart) && ($rangeEnd === '' || $date <= $rangeEnd); })); } if ($candidates === []) { $candidates = $appointments; } foreach ($candidates as $appointment) { $status = (int) ($appointment['status'] ?? 0); $date = (string) ($appointment['appointment_date'] ?? ''); if (in_array($status, [1, 4], true) && $date >= $today) { return $appointment; } } return $candidates[count($candidates) - 1] ?? null; } private function maskPhone(string $phone): string { return preg_replace('/^(\d{3})\d{4}(\d{4})$/', '$1****$2', $phone) ?: $phone; } private function formatDiagnosisDate($value): string { if (is_numeric($value)) { return (int) $value > 0 ? date('Y-m-d', (int) $value) : ''; } $text = trim((string) $value); return $text === '' ? '' : substr($text, 0, 10); } private function appointmentTimeText(array $appointment): string { $time = trim((string) ($appointment['appointment_time'] ?? '')); if (strlen($time) > 5) { $time = substr($time, 0, 5); } return trim((string) ($appointment['appointment_date'] ?? '') . ' ' . $time); } private function appointmentStatusText(int $status): string { return [1 => '已挂号', 3 => '已完成', 4 => '已过号'][$status] ?? '未挂号'; } }