Обновлен стартовый экран

Переписаны запросы для статистики, отчетов
Добавлена интеграция отчета сестры
This commit is contained in:
brusnitsyn
2026-05-28 22:10:00 +09:00
parent 90e0d04dfd
commit 739168d427
96 changed files with 6663 additions and 1465 deletions

View File

@@ -2,9 +2,13 @@
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
{
@@ -32,53 +36,317 @@ class PatientStatusClassifier
/**
* Определяет поступил ли пациент в диапазоне.
*/
public static function classifyAdmitted(Carbon|string|null $ingoingDate): bool
public static function classifyAdmitted(Carbon|string|null $ingoingDate, DateRange $dateRange): bool
{
if (is_null($ingoingDate)) {return false;}
if (is_null($ingoingDate)) {
return false;
}
$ingoingLocal = Carbon::parse($ingoingDate);
$now = Carbon::now();
// Окно смены: вчера 09:00 сегодня 09:00
$shiftStart = $now->copy()->subDay()->setTime(9, 0);
$shiftEnd = $now->copy()->setTime(9, 0);
// Начало диапазона (с 09:00 первого дня)
$rangeStart = $dateRange->start()->copy()->setTime(9, 0, 0);
return $ingoingLocal->between($shiftStart, $shiftEnd);
// Конец диапазона (до 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 $history, DateRange $dateRange): string
public static function classify(UnifiedMedicalHistory|MedicalHistory|ReportDutyPatient|ReportNursePatient $history, DateRange $dateRange): string
{
// 1. Смерть — приоритет №1 (не зависит от дат)
if (!empty($history->death_date)) {
$flags = self::classifyPeriodFlags($history, $dateRange);
if ($flags['deceased']) {
return self::STATUS_DECEASED;
}
// 2. Если есть дата выбытия
if (!empty($history->extract_date)) {
// Переведён (коды 3, 4)
if (in_array($history->visit_result_id, [3, 4], true)) {
return self::STATUS_TRANSFERRED;
}
// Выписан домой/иное (исключаем коды 3-6)
if (in_array($history->visit_result_id, [1], true)) {
return self::STATUS_DISCHARGED;
}
if ($flags['transferred']) {
return self::STATUS_TRANSFERRED;
}
// 3. Поступившие
if ($history->latestMigration?->getAdmittedInCurrentAttribute()) {
if ($flags['discharged']) {
return self::STATUS_DISCHARGED;
}
if ($flags['recipient']) {
return self::STATUS_RECIPIENT;
}
// 4. В отделении
if (empty($history->latestMigration?->out_date) && $history->latestMigration?->ingoing_date < $dateRange->startDate) {
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;
}
}