Files
onboard/app/Services/Classification/PatientStatusClassifier.php

353 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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;
}
}