353 lines
12 KiB
PHP
353 lines
12 KiB
PHP
<?php
|
||
namespace App\Services\Classification;
|
||
|
||
use App\Models\MedicalHistory;
|
||
use App\Models\ObservableMedicalHistory;
|
||
use App\Models\ReportDutyPatient;
|
||
use App\Models\ReportNursePatient;
|
||
use App\Models\UnifiedMedicalHistory;
|
||
use App\Services\DateRange;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Collection;
|
||
|
||
class PatientStatusClassifier
|
||
{
|
||
public const STATUS_IN_DEPARTMENT = 'in_department';
|
||
public const STATUS_RECIPIENT = 'recipient';
|
||
public const STATUS_DISCHARGED = 'discharged';
|
||
public const STATUS_TRANSFERRED = 'transferred';
|
||
public const STATUS_DECEASED = 'deceased';
|
||
public const URGENCY_URGENT = 'urgent'; // urgency_id = 2
|
||
public const URGENCY_PLANNED = 'planned'; // urgency_id = 1
|
||
public const URGENCY_UNKNOWN = 'unknown';
|
||
|
||
/**
|
||
* Определяет срочность пациента.
|
||
*/
|
||
public static function classifyUrgency(?int $urgencyId): string
|
||
{
|
||
return match ($urgencyId) {
|
||
1 => self::URGENCY_PLANNED,
|
||
2 => self::URGENCY_URGENT,
|
||
default => self::URGENCY_UNKNOWN,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Определяет поступил ли пациент в диапазоне.
|
||
*/
|
||
public static function classifyAdmitted(Carbon|string|null $ingoingDate, DateRange $dateRange): bool
|
||
{
|
||
if (is_null($ingoingDate)) {
|
||
return false;
|
||
}
|
||
|
||
$ingoingLocal = Carbon::parse($ingoingDate);
|
||
|
||
// Начало диапазона (с 09:00 первого дня)
|
||
$rangeStart = $dateRange->start()->copy()->setTime(9, 0, 0);
|
||
|
||
// Конец диапазона (до 09:00 последнего дня)
|
||
$rangeEnd = $dateRange->end()->copy()->setTime(9, 0, 0);
|
||
|
||
return $ingoingLocal->between($rangeStart, $rangeEnd);
|
||
}
|
||
|
||
/**
|
||
* @param Collection|null $reanimations
|
||
* @param DateRange $dateRange
|
||
* @return bool
|
||
*/
|
||
public static function classifyReanimation(Collection|null $reanimations, DateRange $dateRange): bool
|
||
{
|
||
if ($reanimations === null || $reanimations->isEmpty()) return false;
|
||
if ($reanimations
|
||
->where('out_date', '<', $dateRange->endSql())
|
||
->where('out_date', '>=', $dateRange->startSql())
|
||
->isNotEmpty()) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public static function classifyObservable(?ObservableMedicalHistory $observable, DateRange $dateRange): bool
|
||
{
|
||
if (empty($observable)) {
|
||
return false;
|
||
}
|
||
|
||
$start = $dateRange->start();
|
||
$end = $dateRange->end();
|
||
|
||
// Наблюдение началось после окончания диапазона
|
||
if ($observable->observable_in > $end) {
|
||
return false;
|
||
}
|
||
|
||
// Наблюдение закончилось до начала диапазона
|
||
if ($observable->observable_out !== null && $observable->observable_out <= $start) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public static function classifyPeriodFlags(
|
||
UnifiedMedicalHistory|MedicalHistory|ReportDutyPatient|ReportNursePatient $history,
|
||
DateRange $dateRange
|
||
): array {
|
||
// Получаем все миграции
|
||
$migrations = self::historyMigrations($history);
|
||
|
||
if ($migrations->isEmpty()) {
|
||
return self::emptyFlags($history);
|
||
}
|
||
|
||
// Приводим даты к Carbon
|
||
$deathDate = self::toCarbon($history->death_date ?? null);
|
||
|
||
// Получаем границы периода
|
||
$periodStart = $dateRange->start();
|
||
$periodEnd = $dateRange->end();
|
||
|
||
// ========== 1. СМЕРТЬ ==========
|
||
// if ($deathDate && $deathDate->lte($periodEnd)) {
|
||
// return [
|
||
// 'recipient' => false,
|
||
// 'discharged' => false,
|
||
// 'deceased' => true,
|
||
// 'transferred' => false,
|
||
// 'outcome' => true,
|
||
// 'current_at_end' => false,
|
||
// 'planned' => (int) ($history->urgency_id ?? 0) === 2,
|
||
// 'urgent' => (int) ($history->urgency_id ?? 0) === 1,
|
||
// ];
|
||
// }
|
||
|
||
// ========== 2. АНАЛИЗ МИГРАЦИЙ ==========
|
||
$hasRecipientInPeriod = false;
|
||
$hasDeathInPeriod = false;
|
||
$hasTransferInPeriod = false;
|
||
$hasDischargeInPeriod = false;
|
||
$hasActiveMigrationAtEnd = false;
|
||
$transferred = [];
|
||
|
||
foreach ($migrations as $migration) {
|
||
$ingoingDate = self::toCarbon($migration->ingoing_date ?? null);
|
||
$outDate = self::toCarbon($migration->out_date ?? null);
|
||
$visitResultId = (int) ($migration->visit_result_id ?? 0);
|
||
$statCureResultId = (int) ($migration->stat_cure_result_id ?? 0);
|
||
|
||
// Поступление в периоде
|
||
if ($ingoingDate && self::dateInPeriod($ingoingDate, $dateRange)) {
|
||
$hasRecipientInPeriod = true;
|
||
}
|
||
|
||
// Проверка на активную миграцию в конце периода
|
||
if ($ingoingDate && $ingoingDate <= $periodEnd) {
|
||
if (!$outDate || $outDate > $periodEnd) {
|
||
$hasActiveMigrationAtEnd = true;
|
||
}
|
||
}
|
||
|
||
// Выбытие в периоде (есть out_date в периоде)
|
||
if ($outDate && self::dateInPeriod($outDate, $dateRange)) {
|
||
// Смерть по исходу лечения (5, 15)
|
||
if (in_array($visitResultId, [5, 15], true)) {
|
||
$hasDeathInPeriod = true;
|
||
}
|
||
// Перевод (коды 4, 14)
|
||
elseif (in_array($visitResultId, [4, 14], true)) {
|
||
$hasTransferInPeriod = true;
|
||
}
|
||
// Выписка
|
||
else {
|
||
$hasDischargeInPeriod = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========== 3. ЕСЛИ НЕТ ИСХОДА ПО МИГРАЦИЯМ, ПРОВЕРЯЕМ EXTRACT_DATE ==========
|
||
// if (!$hasDeathInPeriod && !$hasTransferInPeriod && !$hasDischargeInPeriod) {
|
||
// $extractDate = self::toCarbon($history->extract_date ?? null);
|
||
// $visitResultId = (int) ($history->visit_result_id ?? 0);
|
||
//
|
||
// if ($extractDate && $extractDate->lte($periodEnd)) {
|
||
// // Смерть
|
||
// if ($deathDate && $deathDate->lte($periodEnd)) {
|
||
// $hasDeathInPeriod = true;
|
||
// }
|
||
// // Перевод
|
||
// elseif (in_array($visitResultId, [4, 14], true)) {
|
||
// $hasTransferInPeriod = true;
|
||
// }
|
||
// // Выписка
|
||
// else {
|
||
// $hasDischargeInPeriod = true;
|
||
// }
|
||
// }
|
||
// }
|
||
|
||
// ========== 4. ФОРМИРОВАНИЕ РЕЗУЛЬТАТА ==========
|
||
|
||
// Смерть
|
||
if ($hasDeathInPeriod) {
|
||
return [
|
||
'recipient' => $hasRecipientInPeriod,
|
||
'discharged' => false,
|
||
'deceased' => true,
|
||
'transferred' => false,
|
||
'outcome' => false,
|
||
'current_at_end' => false,
|
||
'planned' => (int) ($history->urgency_id ?? 0) === 2,
|
||
'urgent' => (int) ($history->urgency_id ?? 0) === 1,
|
||
];
|
||
}
|
||
|
||
// Перевод
|
||
if ($hasTransferInPeriod) {
|
||
return [
|
||
'recipient' => $hasRecipientInPeriod,
|
||
'discharged' => false,
|
||
'deceased' => false,
|
||
'transferred' => true,
|
||
'outcome' => false,
|
||
'current_at_end' => false,
|
||
'planned' => (int) ($history->urgency_id ?? 0) === 2,
|
||
'urgent' => (int) ($history->urgency_id ?? 0) === 1,
|
||
];
|
||
}
|
||
|
||
// Выписка
|
||
if ($hasDischargeInPeriod) {
|
||
return [
|
||
'recipient' => $hasRecipientInPeriod,
|
||
'discharged' => true,
|
||
'deceased' => false,
|
||
'transferred' => false,
|
||
'outcome' => true,
|
||
'current_at_end' => false,
|
||
'planned' => (int) ($history->urgency_id ?? 0) === 2,
|
||
'urgent' => (int) ($history->urgency_id ?? 0) === 1,
|
||
];
|
||
}
|
||
|
||
// В отделении на конец периода
|
||
if ($hasActiveMigrationAtEnd) {
|
||
return [
|
||
'recipient' => $hasRecipientInPeriod,
|
||
'discharged' => false,
|
||
'deceased' => false,
|
||
'transferred' => false,
|
||
'outcome' => false,
|
||
'current_at_end' => true,
|
||
'planned' => (int) ($history->urgency_id ?? 0) === 2,
|
||
'urgent' => (int) ($history->urgency_id ?? 0) === 1,
|
||
];
|
||
}
|
||
|
||
// По умолчанию - поступивший, но не в отделении (например, выписан до периода)
|
||
return [
|
||
'recipient' => $hasRecipientInPeriod,
|
||
'discharged' => false,
|
||
'deceased' => false,
|
||
'transferred' => false,
|
||
'outcome' => false,
|
||
'current_at_end' => false,
|
||
'planned' => (int) ($history->urgency_id ?? 0) === 2,
|
||
'urgent' => (int) ($history->urgency_id ?? 0) === 1,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Пустые флаги для пациента без миграций
|
||
*/
|
||
private static function emptyFlags($history): array
|
||
{
|
||
return [
|
||
'recipient' => false,
|
||
'discharged' => false,
|
||
'deceased' => false,
|
||
'transferred' => false,
|
||
'outcome' => false,
|
||
'current_at_end' => false,
|
||
'planned' => (int) ($history->urgency_id ?? 0) === 2,
|
||
'urgent' => (int) ($history->urgency_id ?? 0) === 1,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Определяет статус пациента на основе "сырых" полей из БД.
|
||
*/
|
||
public static function classify(UnifiedMedicalHistory|MedicalHistory|ReportDutyPatient|ReportNursePatient $history, DateRange $dateRange): string
|
||
{
|
||
$flags = self::classifyPeriodFlags($history, $dateRange);
|
||
|
||
if ($flags['deceased']) {
|
||
return self::STATUS_DECEASED;
|
||
}
|
||
if ($flags['transferred']) {
|
||
return self::STATUS_TRANSFERRED;
|
||
}
|
||
if ($flags['discharged']) {
|
||
return self::STATUS_DISCHARGED;
|
||
}
|
||
if ($flags['recipient']) {
|
||
return self::STATUS_RECIPIENT;
|
||
}
|
||
if ($flags['current_at_end']) {
|
||
return self::STATUS_IN_DEPARTMENT;
|
||
}
|
||
|
||
return 'unknown';
|
||
}
|
||
|
||
private static function historyMigrations(
|
||
UnifiedMedicalHistory|MedicalHistory|ReportDutyPatient|ReportNursePatient $history
|
||
): Collection {
|
||
if (method_exists($history, 'relationLoaded') && $history->relationLoaded('migrations')) {
|
||
return $history->migrations ?? collect();
|
||
}
|
||
|
||
return collect([$history->latestMigration])->filter();
|
||
}
|
||
|
||
private static function isCurrentAtPeriodEnd(
|
||
UnifiedMedicalHistory|MedicalHistory|ReportDutyPatient|ReportNursePatient $history,
|
||
Collection $migrations,
|
||
DateRange $dateRange
|
||
): bool {
|
||
if (self::toCarbon($history->death_date ?? null)?->lte($dateRange->endDate)) {
|
||
return false;
|
||
}
|
||
|
||
if (self::toCarbon($history->extract_date ?? null)?->lte($dateRange->endDate)) {
|
||
return false;
|
||
}
|
||
|
||
return $migrations->contains(function ($migration) use ($dateRange) {
|
||
$ingoingDate = self::toCarbon($migration->ingoing_date ?? null);
|
||
$outDate = self::toCarbon($migration->out_date ?? null);
|
||
|
||
return $ingoingDate
|
||
&& $ingoingDate->lte($dateRange->endDate)
|
||
&& (! $outDate || $outDate->gt($dateRange->endDate));
|
||
});
|
||
}
|
||
|
||
public static function dateInPeriod(Carbon|string|null $date, DateRange $dateRange): bool
|
||
{
|
||
$date = self::toCarbon($date);
|
||
|
||
return $date
|
||
&& $date->gt($dateRange->startDate)
|
||
&& $date->lte($dateRange->endDate);
|
||
}
|
||
|
||
private static function toCarbon(Carbon|string|null $date): ?Carbon
|
||
{
|
||
return $date ? Carbon::parse($date) : null;
|
||
}
|
||
}
|