* переписал функции прототипов в сервисы

* оптимизация доставки контента до клиента
* переписал запросы выборок
* убрал из подсчета переведенных
* добавил сохранение метрикам для вывода в дашборд
This commit is contained in:
brusnitsyn
2026-02-04 17:05:13 +09:00
parent 9ee33bc517
commit eab78a0291
16 changed files with 1644 additions and 737 deletions

View File

@@ -2,119 +2,207 @@
namespace App\Services;
use App\Models\MisMigrationPatient;
use App\Models\MisMedicalHistory;
use App\Models\MisSurgicalOperation;
use App\Models\MisMigrationPatient;
use App\Models\ObservationPatient;
use Illuminate\Support\Collection;
use Carbon\Carbon;
class PatientService
{
public function getPatientsByType(
string $type,
?bool $isHeadOrAdmin,
?int $branchId,
?string $startDate = null,
?string $endDate = null,
/**
* Получить плановых или экстренных пациентов
*/
public function getPlanOrEmergencyPatients(
?string $type,
bool $isHeadOrAdmin,
int $branchId,
DateRange $dateRange,
bool $countOnly = false,
bool $onlyIds = false
): Collection|int|array {
// Обрабатываем outcome- типы
if (str_starts_with($type, 'outcome-')) {
$outcomeType = str_replace('outcome-', '', $type);
return $this->getOutcomePatients($branchId, $startDate, $endDate, $outcomeType, $countOnly, $onlyIds);
}
// Обрабатываем обычные типы
$method = 'get' . ucfirst($type) . 'Patients';
if (method_exists($this, $method)) {
return $this->$method($isHeadOrAdmin, $branchId, $startDate, $endDate, $countOnly, $onlyIds);
}
throw new \InvalidArgumentException("Unknown patient type: {$type}");
}
public function getPlanPatients(
?bool $isHeadOrAdmin,
?int $branchId,
?string $startDate,
?string $endDate,
bool $countOnly,
bool $onlyIds
): Collection|int|array {
return $this->getAdmissionPatients('plan', $isHeadOrAdmin, $branchId, $startDate, $endDate, $countOnly, $onlyIds);
}
public function getEmergencyPatients(
?bool $isHeadOrAdmin,
?int $branchId,
?string $startDate,
?string $endDate,
bool $countOnly,
bool $onlyIds
): Collection|int|array {
return $this->getAdmissionPatients('emergency', $isHeadOrAdmin, $branchId, $startDate, $endDate, $countOnly, $onlyIds);
}
private function getAdmissionPatients(
string $admissionType,
?bool $isHeadOrAdmin,
?int $branchId,
?string $startDate,
?string $endDate,
bool $countOnly,
bool $onlyIds
): Collection|int|array {
$query = $this->getBasePatientsQuery($isHeadOrAdmin, $branchId, $startDate, $endDate);
if ($admissionType === 'plan') {
$query->plan();
} else {
$query->emergency();
}
return $this->executeQuery($query, $countOnly, $onlyIds);
}
private function getBasePatientsQuery(
?bool $isHeadOrAdmin,
?int $branchId,
?string $startDate,
?string $endDate
bool $onlyIds = false,
bool $includeCurrent = false
) {
$migrationPatient = new MisMigrationPatient();
// Получаем поступивших сегодня
$recipientQuery = $this->buildRecipientQuery($type, $isHeadOrAdmin, $branchId, $dateRange);
$recipientIds = $recipientQuery->pluck('rf_MedicalHistoryID')->toArray();
if ($isHeadOrAdmin) {
$medicalHistoryIds = $migrationPatient->newQuery()
->whereInDepartment($branchId)
->whereBetween('DateIngoing', [$startDate, $endDate])
// Если нужно добавить уже находящихся в отделении
if ($includeCurrent) {
$currentIds = MisMigrationPatient::currentlyInTreatment($branchId)
->pluck('rf_MedicalHistoryID')
->toArray();
$medicalHistoryIds = array_unique(array_merge($recipientIds, $currentIds));
} else {
$medicalHistoryIds = $migrationPatient->newQuery()
->currentlyInTreatment($branchId)
->whereBetween('DateIngoing', [$startDate, $endDate])
->pluck('rf_MedicalHistoryID')
->toArray();
$medicalHistoryIds = $recipientIds;
}
if (empty($medicalHistoryIds)) {
return MisMedicalHistory::whereRaw('1=0'); // Пустой запрос
if ($countOnly) return 0;
if ($onlyIds) return [];
return collect();
}
if ($onlyIds) {
return $medicalHistoryIds;
}
// Получаем истории
$query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds)
->with([
'surgicalOperations' => function ($q) use ($dateRange) {
$q->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]);
},
'migrations' => function ($q) use ($branchId) {
$q->where('rf_StationarBranchID', $branchId) // укажите поле сортировки
->take(1) // берем только одну последнюю
->with(['diagnosis' => function ($q) {
$q->where('rf_DiagnosTypeID', 3)
->take(1)
->with('mkb');
}]);
}
])
->orderBy('DateRecipient', 'DESC');
// Фильтруем по типу (план/экстренные)
if ($type === 'plan') {
$query->plan();
} elseif ($type === 'emergency') {
$query->emergency();
}
if ($countOnly) {
return $query->count();
}
return $query->get()->map(function ($patient) use ($recipientIds, $branchId) {
// Добавляем флаг "поступил сегодня"
$patient->is_recipient_today = in_array($patient->MedicalHistoryID, $recipientIds);
return $patient;
});
}
/**
* Получить всех пациентов в отделении (поступившие сегодня + уже лечащиеся)
*/
public function getAllPatientsInDepartment(
bool $isHeadOrAdmin,
int $branchId,
DateRange $dateRange,
bool $countOnly = false
) {
// Поступившие сегодня
$recipientIds = $this->buildRecipientQuery(null, $isHeadOrAdmin, $branchId, $dateRange)
->pluck('rf_MedicalHistoryID')
->toArray();
// Уже находящиеся на лечении
$currentIds = MisMigrationPatient::currentlyInTreatment($branchId)
->pluck('rf_MedicalHistoryID')
->toArray();
// Объединяем и убираем дубли
$allIds = array_unique(array_merge($recipientIds, $currentIds));
if (empty($allIds)) {
if ($countOnly) return 0;
return collect();
}
if ($countOnly) {
return count($allIds);
}
return MisMedicalHistory::whereIn('MedicalHistoryID', $allIds)
->with(['surgicalOperations' => function ($q) use ($startDate, $endDate) {
$q->whereBetween('Date', [$startDate, $endDate]);
}])
->orderBy('DateRecipient', 'DESC')
->get()
->map(function ($patient) use ($recipientIds) {
$patient->is_recipient_today = in_array($patient->MedicalHistoryID, $recipientIds);
return $patient;
});
}
/**
* Получить пациентов под наблюдением
*/
public function getObservationPatients(int $departmentId)
{
$patients = MisMedicalHistory::whereHas('observationPatient', function ($q) use ($departmentId) {
$q->where('rf_department_id', $departmentId);
})
->with(['observationPatient' => function($query) use ($departmentId) {
$query->where('rf_department_id', $departmentId)
->select(['rf_medicalhistory_id', 'observation_patient_id', 'comment']);
}])
->orderBy('DateRecipient', 'DESC')
->get();
return $patients->map(function ($patient) {
$patient->comment = $patient->observationPatient
->pluck('comment')
->filter()
->implode('; ');
return $patient;
});
}
/**
* Получить выбывших пациентов
*/
public function getOutcomePatients(
int $branchId,
DateRange $dateRange,
string $outcomeType = 'all'
) {
$methodMap = [
'discharged' => 'discharged',
'transferred' => 'transferred',
'deceased' => 'deceasedOutcome',
'all' => 'outcomePatients',
];
$method = $methodMap[$outcomeType] ?? 'outcomePatients';
$medicalHistoryIds = MisMigrationPatient::{$method}($branchId, $dateRange)
->pluck('rf_MedicalHistoryID')
->unique()
->toArray();
if (empty($medicalHistoryIds)) {
return collect();
}
return MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds)
->with(['surgicalOperations' => function ($query) use ($startDate, $endDate) {
$query->whereBetween('Date', [$startDate, $endDate]);
}])
->orderBy('DateRecipient', 'DESC');
->with(['surgicalOperations'])
->orderBy('DateRecipient', 'DESC')
->get()
->map(function ($patient) {
return $this->addOutcomeInfo($patient);
});
}
private function executeQuery($query, bool $countOnly, bool $onlyIds): Collection|int|array
{
if ($onlyIds) {
return $query->pluck('MedicalHistoryID')->toArray();
}
/**
* Получить пациентов с операциями
*/
public function getSurgicalPatients(
string $type,
int $branchId,
DateRange $dateRange,
bool $countOnly = false
) {
$query = MisMedicalHistory::whereHas('surgicalOperations', function ($q) use ($type, $branchId, $dateRange) {
$q->where('rf_StationarBranchID', $branchId)
->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]);
if ($type === 'plan') {
$q->where('rf_TypeSurgOperationInTimeID', 6);
} else {
$q->whereIn('rf_TypeSurgOperationInTimeID', [4, 5]);
}
});
if ($countOnly) {
return $query->count();
@@ -123,224 +211,18 @@ class PatientService
return $query->get();
}
public function getObservationPatients(int $departmentId): Collection
/**
* Получить текущих пациентов
*/
public function getCurrentPatients(int $branchId, bool $countOnly = false)
{
return ObservationPatient::where('rf_department_id', $departmentId)
->with(['medicalHistory'])
->get()
->map(function ($observation) {
$patient = $observation->medicalHistory;
$patient->observation_comment = $observation->comment;
return $patient;
});
}
public function getOutcomePatients(int $branchId, string $startDate, string $endDate, string $outcomeType, $countOnly = false, $onlyIds = false): Collection|int
{
return match($outcomeType) {
'discharged' => $this->getDischargedOutcomePatients($branchId, $startDate, $endDate, $countOnly, $onlyIds),
'transferred' => $this->getTransferredOutcomePatients($branchId, $startDate, $endDate, $countOnly, $onlyIds),
'deceased' => $this->getDeceasedOutcomePatients($branchId, $startDate, $endDate, $countOnly, $onlyIds),
default => throw new \InvalidArgumentException("Неизвестный тип исхода: {$outcomeType}")
};
}
/**
* Получить всех выбывших пациентов
*/
public function getAllOutcomePatients(
int $branchId,
string $startDate,
string $endDate,
bool $countOnly = false
): Collection|int {
$migrationPatient = new MisMigrationPatient();
// Получаем миграции выбывших пациентов за период
$migrations = $migrationPatient->newQuery()
->outcomePatients($branchId, $startDate, $endDate)
->select('rf_MedicalHistoryID', 'rf_kl_VisitResultID', 'DateOut')
->get()
->groupBy('rf_MedicalHistoryID');
if ($migrations->isEmpty()) {
return $countOnly ? 0 : collect();
}
$medicalHistoryIds = $migrations->keys()->toArray();
$query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds)
->with(['surgicalOperations'])
->orderBy('DateRecipient', 'DESC');
if ($countOnly) {
return $query->count();
}
$patients = $query->get();
return $this->addOutcomeInfoToPatients($patients, $migrations);
}
/**
* Получить миграции выбывших пациентов
*/
private function getOutcomeMigrations(int $branchId, string $startDate, string $endDate): Collection
{
$migrationPatient = new MisMigrationPatient();
return $migrationPatient->newQuery()
->outcomePatients($branchId, $startDate, $endDate)
->select('rf_MedicalHistoryID', 'rf_kl_VisitResultID', 'DateOut')
->get()
->groupBy('rf_MedicalHistoryID');
}
/**
* Добавить информацию о выбытии к пациентам
*/
private function addOutcomeInfoToPatients(Collection $patients, Collection $migrations): Collection
{
return $patients->map(function ($patient) use ($migrations) {
$patientMigrations = $migrations->get($patient->MedicalHistoryID, collect());
if ($patientMigrations->isNotEmpty()) {
$latestMigration = $patientMigrations->sortByDesc('DateOut')->first();
$patient->outcome_type = $this->getOutcomeTypeName($latestMigration->rf_kl_VisitResultID);
$patient->outcome_date = $latestMigration->DateOut;
$patient->visit_result_id = $latestMigration->rf_kl_VisitResultID;
}
return $patient;
});
}
/**
* Получить понятное название типа выбытия
*/
private function getOutcomeTypeName(int $visitResultId): string
{
return match($visitResultId) {
1, 7, 8, 9, 10, 11, 48, 49, 124 => 'Выписка',
2, 3, 4, 12, 13, 14 => 'Перевод',
5, 6, 15, 16 => 'Умер',
default => 'Другое (' . $visitResultId . ')'
};
}
/**
* Получить выписанных пациентов
*/
public function getDischargedOutcomePatients(
int $branchId,
string $startDate,
string $endDate,
bool $countOnly = false,
bool $onlyIds = false
): Collection|array|int {
return $this->getSpecificOutcomePatients('discharged', $branchId, $startDate, $endDate, $onlyIds, $countOnly);
}
/**
* Получить переведенных пациентов
*/
public function getTransferredOutcomePatients(
int $branchId,
string $startDate,
string $endDate,
bool $countOnly = false,
bool $onlyIds = false
): Collection|array|int {
return $this->getSpecificOutcomePatients('transferred', $branchId, $startDate, $endDate, $onlyIds, $countOnly);
}
/**
* Получить умерших пациентов
*/
public function getDeceasedOutcomePatients(
int $branchId,
string $startDate,
string $endDate,
bool $countOnly = false,
bool $onlyIds = false
): Collection|int|array {
return $this->getSpecificOutcomePatients('deceased', $branchId, $startDate, $endDate, $onlyIds, $countOnly);
}
/**
* Общий метод для получения пациентов с определенным типом выбытия
*/
private function getSpecificOutcomePatients(
string $type,
int $branchId,
string $startDate,
string $endDate,
bool $onlyIds = false,
bool $countOnly = false
): Collection|int|array {
$migrationPatient = new MisMigrationPatient();
switch ($type) {
case 'discharged':
$query = $migrationPatient->newQuery()->discharged($branchId, $startDate, $endDate);
break;
case 'transferred':
$query = $migrationPatient->newQuery()->transferred($branchId, $startDate, $endDate);
break;
case 'deceased':
$query = $migrationPatient->newQuery()->deceasedOutcome($branchId, $startDate, $endDate);
break;
default:
throw new \InvalidArgumentException("Неизвестный тип выбытия: {$type}");
}
$medicalHistoryIds = $query->pluck('rf_MedicalHistoryID')->unique()->toArray();
if (empty($medicalHistoryIds)) {
if ($countOnly) return 0;
if ($onlyIds) return [];
return collect();
}
if ($onlyIds) {
return $medicalHistoryIds;
}
$patients = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds)
->with(['surgicalOperations'])
->orderBy('DateRecipient', 'DESC');
if ($countOnly) {
return $patients->count();
}
return $patients->get();
}
/**
* Получить пациентов, находящихся на лечении
*/
public function getCurrentPatients(
int $branchId,
bool $countOnly = false,
bool $onlyIds = false
): Collection|int|array {
$migrationPatient = new MisMigrationPatient();
$medicalHistoryIds = $migrationPatient->newQuery()
->currentlyInTreatment($branchId)
$medicalHistoryIds = MisMigrationPatient::currentlyInTreatment($branchId)
->pluck('rf_MedicalHistoryID')
->unique()
->toArray();
if (empty($medicalHistoryIds)) {
if ($countOnly) return 0;
if ($onlyIds) return [];
return collect();
}
if ($onlyIds) {
return $medicalHistoryIds;
return $countOnly ? 0 : collect();
}
$query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds)
@@ -356,94 +238,127 @@ class PatientService
}
/**
* Получить пациентов с операциями
* Собрать базовый запрос для пациентов
*/
public function getSurgicalPatients(
string $status,
private function buildPatientQuery(
?string $type,
bool $isHeadOrAdmin,
int $branchId,
string $startDate,
string $endDate,
bool $countOnly = false
): Collection|int {
$query = MisSurgicalOperation::where('rf_StationarBranchID', $branchId)
->whereBetween('Date', [$startDate, $endDate])
->orderBy('Date', 'DESC');
if ($status === 'plan') {
$query->where('rf_TypeSurgOperationInTimeID', 6);
DateRange $dateRange
) {
if ($isHeadOrAdmin) {
$query = MisMigrationPatient::whereInDepartment($branchId)
->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
} else {
$query->whereIn('rf_TypeSurgOperationInTimeID', [4, 5]);
$query = MisMigrationPatient::currentlyInTreatment($branchId)
->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
}
if ($countOnly) {
return $query->count();
$medicalHistoryIds = $query->pluck('rf_MedicalHistoryID')->toArray();
if (empty($medicalHistoryIds)) {
return MisMedicalHistory::whereRaw('1 = 0');
}
return $query->get();
}
$query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds);
/**
* Получить пациентов (плановых или экстренных), которые были в отделении в течение периода
*/
public function getPatientsInDepartmentDuringPeriod(
?string $patientType, // 'plan', 'emergency', null (все)
?bool $isHeadOrAdmin,
?int $branchId,
?string $startDate,
?string $endDate,
bool $countOnly = false,
bool $onlyIds = false,
bool $today = false
): Collection|int|array {
// Используем скоуп inDepartment из модели MisMedicalHistory
$query = MisMedicalHistory::query()
->whereHas('migrations', function ($q) use ($branchId, $startDate, $endDate) {
$q->where('rf_StationarBranchID', $branchId)
->where('rf_MedicalHistoryID', '<>', 0)
->where(function ($subQ) use ($startDate, $endDate) {
// Пациент находился в отделении в течение периода
// 1. Поступил в течение периода
$subQ->whereBetween('DateIngoing', [$startDate, $endDate])
// 2. Или выбыл в течение периода
->orWhereBetween('DateOut', [$startDate, $endDate])
// 3. Или находился в отделении в течение всего периода
->orWhere(function ($innerQ) use ($startDate, $endDate) {
$innerQ->where('DateIngoing', '<=', $startDate)
->where(function ($deepQ) use ($endDate) {
$deepQ->where('DateOut', '>=', $endDate)
->orWhereNull('DateOut')
->orWhere('DateOut', '1900-01-01');
});
});
});
})
->with(['surgicalOperations' => function ($query) use ($startDate, $endDate) {
$query->whereBetween('Date', [$startDate, $endDate]);
}])
->orderBy('DateRecipient', 'DESC');
// Фильтруем по типу (план/экстренность)
if ($patientType === 'plan') {
if ($type === 'plan') {
$query->plan();
} elseif ($patientType === 'emergency') {
} elseif ($type === 'emergency') {
$query->emergency();
}
// Для врача добавляем условие "все еще в отделении"
if (!$isHeadOrAdmin && !$today) {
if (!$isHeadOrAdmin && !in_array($type, ['discharged', 'transferred', 'deceased'])) {
$query->currentlyHospitalized();
}
if ($onlyIds) {
return $query->select('MedicalHistoryID')
->pluck('MedicalHistoryID')->values();
return $query;
}
/**
* Построить запрос для поступивших пациентов
*/
private function buildRecipientQuery(
?string $type,
bool $isHeadOrAdmin,
int $branchId,
DateRange $dateRange
) {
// Разная логика для заведующего и врача
if ($isHeadOrAdmin) {
// Заведующий: все поступившие за период
$query = MisMigrationPatient::whereInDepartment($branchId)
->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
} else {
// Врач: только поступившие за сутки
$query = MisMigrationPatient::whereInDepartment($branchId)
->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
}
if ($countOnly) {
return $query->count();
return $query;
}
/**
* Добавить информацию об исходе пациенту
*/
private function addOutcomeInfo(MisMedicalHistory $patient)
{
$latestMigration = $patient->migrations
->whereNotNull('DateOut')
->where('DateOut', '<>', '1900-01-01')
->sortByDesc('DateOut')
->first();
if ($latestMigration) {
$patient->outcome_type = $this->getOutcomeTypeName($latestMigration->rf_kl_VisitResultID);
$patient->outcome_date = $latestMigration->DateOut;
$patient->visit_result_id = $latestMigration->rf_kl_VisitResultID;
}
return $query->get();
return $patient;
}
/**
* Получить количество пациентов по типу с учетом уже находящихся в отделении
*/
public function getPatientsCountWithCurrent(
?string $type,
bool $isHeadOrAdmin,
int $branchId,
DateRange $dateRange
): int {
// Поступившие сегодня указанного типа
$recipientCount = $this->buildRecipientQuery($type, $isHeadOrAdmin, $branchId, $dateRange)
->count();
// Если нужны плановые/экстренные среди уже лечащихся
$currentCount = 0;
if ($type === 'plan' || $type === 'emergency') {
$currentIds = MisMigrationPatient::currentlyInTreatment($branchId)
->pluck('rf_MedicalHistoryID')
->toArray();
if (!empty($currentIds)) {
$currentCount = MisMedicalHistory::whereIn('MedicalHistoryID', $currentIds)
->when($type === 'plan', fn($q) => $q->plan())
->when($type === 'emergency', fn($q) => $q->emergency())
->count();
}
}
return $currentCount;
}
/**
* Получить название типа исхода
*/
private function getOutcomeTypeName(int $visitResultId): string
{
return match($visitResultId) {
1, 7, 8, 9, 10, 11, 48, 49, 124 => 'Выписка',
2, 3, 4, 12, 13, 14 => 'Перевод',
5, 6, 15, 16 => 'Умер',
default => 'Другое (' . $visitResultId . ')'
};
}
}