buildQuery(true) ->field([ 'a.id', 'a.patient_id AS diagnosis_id', 'a.doctor_id', 'a.appointment_date', 'a.appointment_time', 'a.appointment_type', 'a.status', 'a.create_time', 'd.patient_id AS source_patient_id', 'd.patient_name', 'd.phone', 'd.gender', 'd.age', 'd.assistant_id', 'doctor_admin.name AS doctor_name', 'assistant_admin.name AS assistant_name', ]) ->order('a.appointment_date', 'asc') ->order('a.appointment_time', 'asc') ->order('a.id', 'asc') ->limit($this->limitOffset, $this->limitLength) ->select() ->toArray(); return $this->appendProgress($rows); } public function count(): int { return (int) $this->buildQuery(true)->count('a.id'); } public function extend(): array { $query = $this->buildQuery(false); [$startDate, $endDate] = $this->dateRange(); $summary = [ 'total' => (int) (clone $query)->count('a.id'), 'booked' => (int) (clone $query)->where('a.status', 1)->count('a.id'), 'completed' => (int) (clone $query)->where('a.status', 3)->count('a.id'), 'missed' => (int) (clone $query)->where('a.status', 4)->count('a.id'), ]; $scheduleMode = $this->usesOwnershipSchedule() ? 'ownership' : 'roster'; $weekSchedule = $scheduleMode === 'ownership' ? $this->ownershipWeekSchedule() : $this->weekSchedule(); $todaySchedule = $weekSchedule[0] ?? $this->emptyScheduleDay(date('Y-m-d')); $todayOverview = $scheduleMode === 'ownership' ? [ 'total_visits' => (int) ($todaySchedule['total_appointments'] ?? 0), 'booked' => (int) ($todaySchedule['waiting_appointments'] ?? 0), 'completed' => (int) ($todaySchedule['completed_appointments'] ?? 0), 'missed' => (int) ($todaySchedule['missed_appointments'] ?? 0), 'empty_slots' => 0, 'doctor_count' => (int) ($todaySchedule['doctor_count'] ?? 0), ] : [ 'total_visits' => (int) ($todaySchedule['total_slots'] ?? 0), 'booked' => (int) ($todaySchedule['booked_slots'] ?? 0), 'completed' => 0, 'missed' => 0, 'empty_slots' => (int) ($todaySchedule['empty_slots'] ?? 0), 'doctor_count' => (int) ($todaySchedule['doctor_count'] ?? 0), ]; return [ 'summary' => $summary, 'schedule_mode' => $scheduleMode, 'today_overview' => $todayOverview, 'week_schedule' => $weekSchedule, 'scope' => MyPatientLogic::scopeMeta($this->adminId, $this->adminInfo), 'dates' => ['start' => $startDate, 'end' => $endDate], ]; } private function buildQuery(bool $applyStatus): Query { $query = Appointment::alias('a') ->join('tcm_diagnosis d', 'a.patient_id = d.id') ->leftJoin('admin doctor_admin', 'a.doctor_id = doctor_admin.id') ->leftJoin('admin assistant_admin', 'CAST(d.assistant_id AS UNSIGNED) = assistant_admin.id') ->whereNull('d.delete_time') ->where('d.status', 1); MyPatientLogic::applyScope($query, $this->adminId, $this->adminInfo); $this->applyKeyword($query); $this->applyDateFilter($query); if ($applyStatus) { $status = $this->params['status'] ?? ''; if ($status !== '' && $status !== null && in_array((int) $status, self::EFFECTIVE_STATUSES, true)) { $query->where('a.status', (int) $status); } else { $query->whereIn('a.status', self::EFFECTIVE_STATUSES); } } else { $query->whereIn('a.status', self::EFFECTIVE_STATUSES); } return $query; } private function applyKeyword(Query $query): void { $keyword = trim((string) ($this->params['keyword'] ?? '')); if ($keyword === '') { return; } $query->where(function ($q) use ($keyword) { $like = '%' . $keyword . '%'; $q->whereLike('d.patient_name', $like) ->whereOr('d.phone', 'like', $like) ->whereOr('doctor_admin.name', 'like', $like) ->whereOr('assistant_admin.name', 'like', $like); if (preg_match('/^\d+$/', $keyword)) { $id = (int) $keyword; if ($id > 0) { $q->whereOr('a.id', $id)->whereOr('d.id', $id); } } }); } private function applyDateFilter(Query $query): void { [$startDate, $endDate] = $this->dateRange(); $query->whereBetween('a.appointment_date', [$startDate, $endDate]); } /** @return array{0:string,1:string} */ private function dateRange(): array { $today = date('Y-m-d'); $startDate = $this->normalizeDate($this->params['start_date'] ?? '') ?: $today; $endDate = $this->normalizeDate($this->params['end_date'] ?? '') ?: $startDate; if ($startDate > $endDate) { [$startDate, $endDate] = [$endDate, $startDate]; } $startTs = strtotime($startDate); $endTs = strtotime($endDate); if ($startTs !== false && $endTs !== false && $endTs - $startTs > 31 * 86400) { $endDate = date('Y-m-d', $startTs + 31 * 86400); } return [$startDate, $endDate]; } private function normalizeDate($value): string { $value = trim((string) $value); return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) ? $value : ''; } /** * @param array> $rows * @return array> */ private function appendProgress(array $rows): array { if ($rows === []) { return []; } $diagnosisIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'diagnosis_id'))))); $appointmentIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'id'))))); $queuePositionMap = $this->queuePositionMap($rows); $confirmedSet = []; if ($diagnosisIds !== []) { $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); } $prescriptionMap = []; if ($appointmentIds !== []) { $prescriptions = Prescription::whereIn('appointment_id', $appointmentIds) ->whereNull('delete_time') ->where('void_status', 0) ->field(['id', 'appointment_id', 'audit_status', 'is_system_auto']) ->order('id', 'desc') ->select() ->toArray(); foreach ($prescriptions as $prescription) { $appointmentId = (int) ($prescription['appointment_id'] ?? 0); if ($appointmentId > 0 && !isset($prescriptionMap[$appointmentId])) { $prescriptionMap[$appointmentId] = $prescription; } } } foreach ($rows as &$row) { $appointmentId = (int) ($row['id'] ?? 0); $diagnosisId = (int) ($row['diagnosis_id'] ?? 0); $status = (int) ($row['status'] ?? 0); $prescription = $prescriptionMap[$appointmentId] ?? []; $confirmed = isset($confirmedSet[$diagnosisId]); $prescribed = $prescription !== []; $aheadCount = $status === 1 ? (int) ($queuePositionMap[$appointmentId] ?? 0) : 0; $row['phone_masked'] = $this->maskPhone((string) ($row['phone'] ?? '')); unset($row['phone']); $row['gender_desc'] = (int) ($row['gender'] ?? 0) === 1 ? '男' : '女'; $row['assistant_name'] = trim((string) ($row['assistant_name'] ?? '')) ?: '未分配'; $row['doctor_name'] = trim((string) ($row['doctor_name'] ?? '')) ?: '未知医生'; $row['appointment_time_text'] = $this->appointmentTimeText($row); $row['status_text'] = $this->appointmentStatusText($status); $row['appointment_type_text'] = $this->appointmentTypeText((string) ($row['appointment_type'] ?? '')); $row['registered'] = 1; $row['diagnosis_confirmed'] = $confirmed ? 1 : 0; $row['visit_completed'] = $status === 3 ? 1 : 0; $row['has_prescription'] = $prescribed ? 1 : 0; $row['prescription_id'] = (int) ($prescription['id'] ?? 0); $row['prescription_audit_status'] = $prescribed ? (int) ($prescription['audit_status'] ?? 0) : -1; $row['progress_text'] = $this->progressText($confirmed, $status === 3, $prescribed, $status); $row['queue_no'] = $status === 1 ? $aheadCount + 1 : 0; $row['ahead_count'] = $aheadCount; $row['estimated_wait_minutes'] = $aheadCount * self::AVG_MINUTES_PER_VISIT; $row['queue_status'] = $this->queueStatus($status, $confirmed, $aheadCount); $row['queue_status_text'] = $this->queueStatusText((string) $row['queue_status']); $row['is_self_patient'] = ( (int) ($row['assistant_id'] ?? 0) === $this->adminId || (int) ($row['doctor_id'] ?? 0) === $this->adminId ) ? 1 : 0; } unset($row); return $rows; } /** * 候诊位次按 progress.vue 的真实规则计算:同医生、同日、待就诊,按预约时刻和挂号 ID 升序。 * 队列计算读取完整医生队列,只向当前范围列表返回人数,不暴露范围外患者身份。 * * @param array> $rows * @return array */ private function queuePositionMap(array $rows): array { $doctorIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'doctor_id'))))); $dates = array_values(array_unique(array_filter(array_map('strval', array_column($rows, 'appointment_date'))))); if ($doctorIds === [] || $dates === []) { return []; } $queueRows = Appointment::whereIn('doctor_id', $doctorIds) ->whereIn('appointment_date', $dates) ->where('status', 1) ->field(['id', 'doctor_id', 'appointment_date', 'appointment_time']) ->order('doctor_id', 'asc') ->order('appointment_date', 'asc') ->order('appointment_time', 'asc') ->order('id', 'asc') ->select() ->toArray(); $groupCounts = []; $positions = []; foreach ($queueRows as $queueRow) { $group = (int) ($queueRow['doctor_id'] ?? 0) . '|' . (string) ($queueRow['appointment_date'] ?? ''); $positions[(int) ($queueRow['id'] ?? 0)] = (int) ($groupCounts[$group] ?? 0); $groupCounts[$group] = (int) ($groupCounts[$group] ?? 0) + 1; } return $positions; } /** * 未来七天号源:完全复用 paiban/availableSlots 的生成口径,按医生+日期+时刻去重。 * * @return array> */ private function weekSchedule(): array { $startDate = date('Y-m-d'); $endDate = date('Y-m-d', strtotime($startDate . ' +6 days')); $days = []; for ($offset = 0; $offset < 7; $offset++) { $date = date('Y-m-d', strtotime($startDate . " +{$offset} days")); $days[$date] = $this->emptyScheduleDay($date); } $doctorIds = $this->visibleDoctorIds($startDate, $endDate); if ($doctorIds === []) { return array_values($days); } $rosters = Roster::whereIn('doctor_id', $doctorIds) ->whereBetween('date', [$startDate, $endDate]) ->where('status', 1) ->whereNull('delete_time') ->field(['doctor_id', 'date', 'period', 'start_time', 'end_time', 'slot_minutes', 'quota']) ->select() ->toArray(); $doctorNames = Admin::whereIn('id', $doctorIds)->column('name', 'id'); $doctorSlotSets = []; $doctorWindowSets = []; foreach ($rosters as $roster) { $date = (string) ($roster['date'] ?? ''); $doctorId = (int) ($roster['doctor_id'] ?? 0); $window = RosterSegmentService::resolveWindow($roster); if (!isset($days[$date]) || $doctorId <= 0 || $window === null) { continue; } [$startTime, $endTime] = $window; $times = RosterSegmentService::generateSlotTimes( $startTime, $endTime, RosterSegmentService::normalizeSlotMinutes($roster['slot_minutes'] ?? 15) ); $times = RosterSegmentService::applyQuotaCap($times, (int) ($roster['quota'] ?? 0)); foreach ($times as $time) { $doctorSlotSets[$date][$doctorId][$time] = true; } $doctorWindowSets[$date][$doctorId][$startTime . '-' . $endTime] = true; } $appointments = Appointment::whereIn('doctor_id', $doctorIds) ->whereBetween('appointment_date', [$startDate, $endDate]) ->where('status', 1) ->field(['doctor_id', 'appointment_date', 'appointment_time']) ->select() ->toArray(); $doctorBookedSets = []; foreach ($appointments as $appointment) { $date = (string) ($appointment['appointment_date'] ?? ''); $doctorId = (int) ($appointment['doctor_id'] ?? 0); $time = substr((string) ($appointment['appointment_time'] ?? ''), 0, 5); if (isset($doctorSlotSets[$date][$doctorId][$time])) { $doctorBookedSets[$date][$doctorId][$time] = true; } } foreach ($days as $date => &$day) { $doctorDetails = []; $total = 0; $booked = 0; foreach ($doctorSlotSets[$date] ?? [] as $doctorId => $slotSet) { $doctorTotal = count($slotSet); $doctorBooked = count($doctorBookedSets[$date][$doctorId] ?? []); $total += $doctorTotal; $booked += $doctorBooked; $scheduleWindows = array_values(array_keys($doctorWindowSets[$date][$doctorId] ?? [])); sort($scheduleWindows, SORT_STRING); $doctorDetails[] = [ 'doctor_id' => (int) $doctorId, 'doctor_name' => trim((string) ($doctorNames[$doctorId] ?? '')) ?: '未知医生', 'schedule_windows' => $scheduleWindows, 'total_slots' => $doctorTotal, 'booked_slots' => $doctorBooked, 'empty_slots' => max(0, $doctorTotal - $doctorBooked), ]; } usort($doctorDetails, static function (array $left, array $right): int { return $right['booked_slots'] <=> $left['booked_slots'] ?: $right['total_slots'] <=> $left['total_slots'] ?: strcmp((string) $left['doctor_name'], (string) $right['doctor_name']); }); $day['total_slots'] = $total; $day['booked_slots'] = $booked; $day['empty_slots'] = max(0, $total - $booked); $day['doctor_count'] = count($doctorDetails); $day['doctors'] = $doctorDetails; } unset($day); return array_values($days); } /** * 医助“本人归属”只统计其患者的真实挂号,不再把历史接诊医生的整周号源算到本人名下。 * * @return array> */ private function ownershipWeekSchedule(): array { $startDate = date('Y-m-d'); $endDate = date('Y-m-d', strtotime($startDate . ' +6 days')); $days = []; for ($offset = 0; $offset < 7; $offset++) { $date = date('Y-m-d', strtotime($startDate . " +{$offset} days")); $days[$date] = $this->emptyScheduleDay($date); } $query = Appointment::alias('ownership_a') ->join('tcm_diagnosis d', 'ownership_a.patient_id = d.id') ->leftJoin('admin ownership_doctor', 'ownership_a.doctor_id = ownership_doctor.id') ->whereNull('d.delete_time') ->where('d.status', 1) ->whereBetween('ownership_a.appointment_date', [$startDate, $endDate]) ->whereIn('ownership_a.status', self::EFFECTIVE_STATUSES); MyPatientLogic::applyScope($query, $this->adminId, $this->adminInfo); $appointments = $query ->field([ 'ownership_a.id', 'ownership_a.doctor_id', 'ownership_a.appointment_date', 'ownership_a.appointment_time', 'ownership_a.status', 'ownership_doctor.name AS doctor_name', ]) ->order('ownership_a.appointment_date', 'asc') ->order('ownership_a.appointment_time', 'asc') ->order('ownership_a.id', 'asc') ->select() ->toArray(); $doctorDetails = []; foreach ($appointments as $appointment) { $date = (string) ($appointment['appointment_date'] ?? ''); $doctorId = (int) ($appointment['doctor_id'] ?? 0); if (!isset($days[$date]) || $doctorId <= 0) { continue; } if (!isset($doctorDetails[$date][$doctorId])) { $doctorDetails[$date][$doctorId] = [ 'doctor_id' => $doctorId, 'doctor_name' => trim((string) ($appointment['doctor_name'] ?? '')) ?: '未知医生', 'appointment_time_set' => [], 'total_appointments' => 0, 'waiting_appointments' => 0, 'completed_appointments' => 0, 'missed_appointments' => 0, ]; } $time = substr((string) ($appointment['appointment_time'] ?? ''), 0, 5); if ($time !== '') { $doctorDetails[$date][$doctorId]['appointment_time_set'][$time] = true; } $status = (int) ($appointment['status'] ?? 0); $doctorDetails[$date][$doctorId]['total_appointments']++; if ($status === 1) { $doctorDetails[$date][$doctorId]['waiting_appointments']++; } elseif ($status === 3) { $doctorDetails[$date][$doctorId]['completed_appointments']++; } elseif ($status === 4) { $doctorDetails[$date][$doctorId]['missed_appointments']++; } } foreach ($days as $date => &$day) { $rows = []; foreach ($doctorDetails[$date] ?? [] as $doctor) { $times = array_values(array_keys($doctor['appointment_time_set'] ?? [])); sort($times, SORT_STRING); unset($doctor['appointment_time_set']); $doctor['appointment_times'] = $times; $rows[] = $doctor; } usort($rows, static function (array $left, array $right): int { return $right['waiting_appointments'] <=> $left['waiting_appointments'] ?: $right['total_appointments'] <=> $left['total_appointments'] ?: strcmp((string) $left['doctor_name'], (string) $right['doctor_name']); }); $day['total_appointments'] = array_sum(array_column($rows, 'total_appointments')); $day['waiting_appointments'] = array_sum(array_column($rows, 'waiting_appointments')); $day['completed_appointments'] = array_sum(array_column($rows, 'completed_appointments')); $day['missed_appointments'] = array_sum(array_column($rows, 'missed_appointments')); $day['doctor_count'] = count($rows); $day['doctors'] = $rows; } unset($day); return array_values($days); } private function usesOwnershipSchedule(): bool { if ((int) ($this->adminInfo['root'] ?? 0) === 1) { return false; } $roleIds = $this->currentRoleIds(); return in_array(2, $roleIds, true) && array_intersect($roleIds, [3, 7, 8]) === []; } /** @return int[] */ private function visibleDoctorIds(string $startDate, string $endDate): array { if ((int) ($this->adminInfo['root'] ?? 0) === 1) { $doctorIds = array_values(array_unique(array_map('intval', Roster::whereBetween('date', [$startDate, $endDate]) ->where('status', 1) ->whereNull('delete_time') ->column('doctor_id')))); return $this->activeDoctorIds($doctorIds); } $roleIds = $this->currentRoleIds(); $isTeamRole = array_intersect($roleIds, [3, 7, 8]) !== []; $isDoctor = in_array(1, $roleIds, true); $isAssistant = in_array(2, $roleIds, true); // 纯医生账号的概览只统计本人排班,避免同一患者曾由其他医生接诊时放大到其他医生。 if (!$isTeamRole && $isDoctor && !$isAssistant) { return $this->activeDoctorIds([$this->adminId]); } $query = Appointment::alias('scope_a') ->join('tcm_diagnosis d', 'scope_a.patient_id = d.id') ->whereNull('d.delete_time') ->where('d.status', 1) ->whereIn('scope_a.status', self::EFFECTIVE_STATUSES) ->where('scope_a.doctor_id', '>', 0); MyPatientLogic::applyScope($query, $this->adminId, $this->adminInfo); $doctorIds = array_values(array_unique(array_filter(array_map('intval', $query->distinct(true)->column('scope_a.doctor_id'))))); if (!$isTeamRole && $isDoctor) { $doctorIds[] = $this->adminId; } return $this->activeDoctorIds(array_values(array_unique($doctorIds))); } /** @return int[] */ private function currentRoleIds(): array { return array_values(array_unique(array_filter(array_map('intval', AdminRole::where('admin_id', $this->adminId)->column('role_id'))))); } /** @param int[] $doctorIds @return int[] */ private function activeDoctorIds(array $doctorIds): array { $doctorIds = array_values(array_unique(array_filter(array_map('intval', $doctorIds)))); if ($doctorIds === []) { return []; } $roleDoctorIds = array_values(array_unique(array_map('intval', AdminRole::whereIn('admin_id', $doctorIds) ->where('role_id', 1) ->column('admin_id')))); if ($roleDoctorIds === []) { return []; } $activeSet = array_fill_keys(array_map('intval', Admin::whereIn('id', $roleDoctorIds) ->where('disable', 0) ->column('id')), true); return array_values(array_filter($doctorIds, static function (int $doctorId) use ($activeSet): bool { return isset($activeSet[$doctorId]); })); } /** @return array */ private function emptyScheduleDay(string $date): array { $weekdayLabels = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; $timestamp = strtotime($date) ?: time(); return [ 'date' => $date, 'date_text' => date('m-d', $timestamp), 'weekday' => $weekdayLabels[(int) date('w', $timestamp)], 'total_slots' => 0, 'booked_slots' => 0, 'empty_slots' => 0, 'doctor_count' => 0, 'doctors' => [], 'total_appointments' => 0, 'waiting_appointments' => 0, 'completed_appointments' => 0, 'missed_appointments' => 0, ]; } private function queueStatus(int $status, bool $confirmed, int $aheadCount): string { if ($status === 3) { return 'completed'; } if ($status === 4) { return 'missed'; } if ($confirmed) { return 'consulting'; } return $aheadCount === 0 ? 'next' : 'waiting'; } private function queueStatusText(string $status): string { return [ 'completed' => '已完成', 'missed' => '已过号', 'consulting' => '就诊中', 'next' => '待确认', 'waiting' => '等待中', ][$status] ?? '等待中'; } private function maskPhone(string $phone): string { return preg_replace('/^(\d{3})\d{4}(\d{4})$/', '$1****$2', $phone) ?: $phone; } private function appointmentTimeText(array $row): string { $time = trim((string) ($row['appointment_time'] ?? '')); if (strlen($time) > 5) { $time = substr($time, 0, 5); } return trim((string) ($row['appointment_date'] ?? '') . ' ' . $time); } private function appointmentStatusText(int $status): string { return [1 => '已挂号', 3 => '已完成', 4 => '已过号'][$status] ?? '未知'; } private function appointmentTypeText(string $type): string { return ['video' => '视频问诊', 'text' => '图文问诊', 'phone' => '电话问诊'][$type] ?? '面诊'; } private function progressText(bool $confirmed, bool $completed, bool $prescribed, int $status): string { if ($status === 4) { return '已过号'; } if (!$confirmed) { return '待确认诊单'; } if (!$completed) { return '待完诊'; } return $prescribed ? '已开方' : '待开方'; } }