Files
onboard/app/Services/DutyReportService.php

952 lines
46 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;
use App\Models\DepartmentMetrikaDefault;
use App\Models\DutyReportMetricResult;
use App\Models\DutyUnwantedEvent;
use App\Models\MedicalHistory;
use App\Models\MetrikaResult;
use App\Models\ObservableMedicalHistory;
use App\Models\ReportDuty;
use App\Models\ReportNurse;
use App\Models\UnifiedMedicalHistory;
use App\Models\User;
use App\Services\Classification\PatientStatusClassifier;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class DutyReportService
{
public function __construct(
protected DateRangeService $dateRangeService,
) { }
/**
* Базовый запрос для всех отчётов
* Фильтрует по отделению и периоду, подгружает связи
*/
protected function baseQuery(DateRange $dateRange, int $departmentId): Builder
{
return UnifiedMedicalHistory::query()
->whereHas('migrations', function ($q) use ($departmentId, $dateRange) {
$q->department($departmentId)->dateRange($dateRange->startSql(), $dateRange->endSql());
})
->with(['latestMigration' => function ($q) use ($departmentId, $dateRange) {
$q->department($departmentId)->dateRange($dateRange->startSql(), $dateRange->endSql());
}]);
}
/**
* Сохранить отчет
*/
public function saveReport(DateRange $dateRange, ?int $userId = null, ?int $lpuDoctorId = null, ?int $departmentId = null)
{
$user = $userId ? User::find($userId) : auth()->user();
$lpuDoctorId = $lpuDoctorId ?? $user->rf_lpudoctor_id;
$departmentId = $departmentId ?? $user->rf_department_id;
$data = [
'report_date' => $dateRange->endSql(),
'sent_at' => Carbon::now()->format('Y-m-d H:i:s'),
'period_type' => 'day',
'period_start' => $dateRange->startSql(),
'period_end' => $dateRange->endSql(),
'status_id' => 2, // опубликован
'rf_lpudoctor_id' => $lpuDoctorId,
'rf_department_id' => $departmentId,
'rf_user_id' => $user->id,
];
$report = ReportDuty::updateOrCreate(
[
'report_date' => $data['report_date'],
'period_start' => $data['period_start'],
'period_end' => $data['period_end'],
'rf_department_id' => $data['rf_department_id'],
],
$data
);
return $report;
}
/**
* Сохранить снимок пациентов за период
*/
public function saveSnapshot(DateRange $dateRange, ReportDuty $reportDuty, ?int $departmentId = null, ?int $userId = null): array
{
$departmentId = $departmentId ?? $reportDuty->department->rf_mis_department_id;
$userId = $userId ?? $reportDuty->rf_user_id;
$startYear = Carbon::now()->startOfYear()->format('Y-m-d');
$patients = MedicalHistory::query()
->whereHas('migrations', function ($q) use ($departmentId, $dateRange, $startYear) {
$q->where('department_id', $departmentId)
->where('ingoing_date', '<=', $dateRange->endSql())
->where(function ($sub) use ($dateRange, $startYear) {
// Миграции без out_date (еще лежат)
$sub->whereNull('out_date')
->where('ingoing_date', '>', $startYear);
// Миграции с out_date (закрытые)
$sub->orWhere(function ($sub2) use ($dateRange, $startYear) {
$sub2->whereNotNull('out_date')
->where('out_date', '>', $dateRange->startSql())
->where('out_date', '>', $startYear);
});
});
})
->with([
'latestMigration' => function ($q) use ($departmentId) {
$q->where('department_id', $departmentId);
},
'latestMigration.operations',
'latestMigration.reanimations',
'migrations' => function ($q) use ($departmentId, $dateRange, $startYear) {
// Загружаем только нужные миграции с фильтром по отделению
$q->where('department_id', $departmentId)
->where('ingoing_date', '<=', $dateRange->endSql())
->where(function ($sub) use ($dateRange, $startYear) {
// Миграции без out_date (еще лежат)
$sub->whereNull('out_date')
->where('ingoing_date', '>', $startYear);
// Миграции с out_date (закрытые)
$sub->orWhere(function ($sub2) use ($dateRange, $startYear) {
$sub2->whereNotNull('out_date')
->where('out_date', '>', $dateRange->startSql())
->where('out_date', '>', $startYear);
});
});
},
'migrations.reanimations' => function ($q) use ($dateRange) {
// Фильтруем реанимации по периоду (опционально)
$q->where(function ($sub) use ($dateRange) {
$sub->whereNull('out_date')
->orWhere('out_date', '>=', $dateRange->startSql());
});
},
'operations' => function ($q) use ($departmentId, $dateRange) {
$q->where('department_id', $departmentId);
}
])
->lazy()->map(function (MedicalHistory $h) use ($dateRange) {
$patientStatus = PatientStatusClassifier::classify($h, $dateRange);
$periodFlags = PatientStatusClassifier::classifyPeriodFlags($h, $dateRange);
$patientUrgency = null;
$patientReanimation = null;
if (!in_array($patientStatus, [
PatientStatusClassifier::STATUS_DECEASED,
PatientStatusClassifier::STATUS_DISCHARGED,
PatientStatusClassifier::STATUS_TRANSFERRED
])) {
$patientUrgency = PatientStatusClassifier::classifyUrgency($h->urgency_id);
$patientReanimation = PatientStatusClassifier::classifyReanimation($h->latestMigration?->reanimations, $dateRange);
}
return [
// Все исходные поля модели (автоматически через toArray)
...$h->toArray(),
'operations' => $h->operations->toArray(),
// + вычисляемые мета-поля для фронтенда
'patient_status' => $patientStatus,
'patient_urgency' => $patientUrgency,
'period_flags' => $periodFlags,
'in_reanimation' => $patientReanimation,
'admitted_today' => PatientStatusClassifier::classifyAdmitted($h->latestMigration?->ingoing_date, $dateRange),
'in_observable' => PatientStatusClassifier::classifyObservable($h->observable, $dateRange),
];
});
$savedStats = $this->saveReportSnapshot($reportDuty->id, $patients, $userId, $departmentId, $dateRange);
return [
...$savedStats,
'report_date' => $dateRange->startSql(),
'department_id' => $departmentId,
];
}
public function saveReportSnapshot(
int $reportDutyId,
iterable $patients,
int $userId,
int $departmentId,
DateRange $dateRange
): array
{
if (empty($patients)) {
return ['saved_patients' => 0, 'saved_migrations' => 0];
}
$patientBatch = [];
$migrationBatch = [];
$reanimationBatch = [];
$batchSize = 100;
// Инициализация агрегатора статистики
$totalStats = [
'saved_patients' => 0,
'saved_migrations' => 0,
'saved_reanimations' => 0,
'by_status' => [],
'by_urgency' => [],
'admitted' => [
'today' => 0,
'planned' => 0,
'urgent' => 0,
],
'in_reanimation' => 0,
'admitted_today' => 0,
'in_department' => 0,
'planned' => 0,
'deceased' => 0,
'transferred' => 0,
'discharged' => 0,
'outcome' => 0,
'total_bed_days' => 0,
'total_preop_bed_days' => 0,
'patients_with_operations' => 0,
'total_operations' => 0,
'planned_operations' => 0,
'urgent_operations' => 0,
'total_patients' => 0,
];
$outcomeList = [];
// Статусы операций
$operationPlanned = [6];
$operationUrgent = [4, 5];
// Получаем границы периода
$periodStart = $dateRange->startSql();
$periodEnd = $dateRange->endSql();
$periodStartCarbon = Carbon::parse($periodStart);
$periodEndCarbon = Carbon::parse($periodEnd);
$uniqueOperationIds = [];
foreach ($patients as $patient) {
// ========== 1. ПОДГОТОВКА ДАННЫХ ДЛЯ БД (с обнулением) ==========
// Подготовка данных пациента
$extractDate = $patient['extract_date'] ? Carbon::parse($patient['extract_date']) : null;
$hasExtractInPeriod = $extractDate && $this->dateRangeService->dateInPeriod($extractDate, $dateRange);
$patientData = [
'report_duty_id' => $reportDutyId,
'source_type' => 'mis',
'original_id' => $patient['id'],
'medical_card_number' => $patient['medical_card_number'],
'full_name' => $patient['full_name'],
'birth_date' => $patient['birth_date'],
'recipient_date' => $patient['recipient_date'],
'male' => $patient['male'],
'urgency_id' => $patient['urgency_id'],
'comment' => $patient['comment'] ?? null,
'user_id' => $userId,
];
// ДОПОЛНИТЕЛЬНО: проверяем миграции на наличие выбытия в периоде
$hasDischargeInMigration = false;
$migrationVisitResultId = null;
$migrationOutDate = null;
// Подготовка данных миграции
$preparedMigrations = [];
foreach ($patient['migrations'] as $migration) {
// Пропускаем миграции не в нашем отделении
if (($migration['department_id'] ?? null) != $departmentId) {
continue;
}
$outDate = $migration['out_date'] ? Carbon::parse($migration['out_date']) : null;
$hasOutDateInPeriod = $outDate && $this->dateRangeService->dateInPeriod($outDate, $dateRange);
$migrationItem = [
'_temp_key' => [
'report_duty_id' => $reportDutyId,
'source_type' => 'mis',
'original_id' => $patient['id'],
],
'original_id' => $migration['id'],
'ingoing_date' => $migration['ingoing_date'],
'diagnosis_id' => $migration['diagnosis_id'],
'diagnosis_code' => $migration['diagnosis_code'],
'diagnosis_name' => $migration['diagnosis_name'],
'interrupted_event_id' => $migration['interrupted_event_id'],
'stationar_branch_id' => $migration['stationar_branch_id'],
'department_id' => $migration['department_id'],
'user_id' => $migration['user_id'] ?? null,
'mis_user_id' => $migration['mis_user_id'] ?? null,
'comment' => $migration['comment'] ?? null,
];
// Добавляем поля ТОЛЬКО если есть out_date в периоде
if ($hasOutDateInPeriod) {
$hasDischargeInMigration = true;
$migrationVisitResultId = $migration['visit_result_id'] ?? null;
$migrationOutDate = $migration['out_date'] ?? null;
$migrationItem['visit_result_id'] = $migration['visit_result_id'] ?? null;
$migrationItem['stat_cure_result_id'] = $migration['stat_cure_result_id'] ?? null;
$migrationItem['out_date'] = $migration['out_date'] ?? null;
} else {
$migrationItem['visit_result_id'] = null;
$migrationItem['stat_cure_result_id'] = null;
$migrationItem['out_date'] = null;
}
$preparedMigrations[] = $migrationItem;
// Подготовка данных реанимации
if (!empty($migration['reanimations'])) {
foreach ($migration['reanimations'] as $reanimation) {
$reanimationBatch[] = [
'_temp_key' => [
'report_duty_id' => $reportDutyId,
'source_type' => 'mis',
'patient_id' => $patient['id'],
'migration_id' => $migration['id'],
],
'original_id' => $reanimation['id'],
'migration_patient_id' => $reanimation['migration_patient_id'],
'medical_history_id' => $reanimation['medical_history_id'],
'in_date' => $reanimation['in_date'],
'out_date' => $reanimation['out_date'],
'description' => $reanimation['description'],
'stationar_branch_id' => $reanimation['stationar_branch_id'],
'migration_stationar_branch_id' => $reanimation['migration_stationar_branch_id'],
'migration_department_id' => $reanimation['migration_department_id'],
'doctor_id' => $reanimation['doctor_id'],
'user_id' => $reanimation['user_id'] ?? null,
'mis_user_id' => $reanimation['mis_user_id'] ?? null,
'comment' => $reanimation['comment'] ?? null,
];
}
}
}
// Если есть выбытие по миграции ИЛИ по карте
$hasOutcomeInPeriod = $hasExtractInPeriod || $hasDischargeInMigration;
if ($hasOutcomeInPeriod) {
// Приоритет: данные из миграции (если есть), иначе из карты
$patientData['visit_result_id'] = $migrationVisitResultId ?? $patient['visit_result_id'] ?? null;
$patientData['extract_date'] = $migrationOutDate ?? $patient['extract_date'] ?? null;
$patientData['death_date'] = $patient['death_date'] ?? null;
} else {
$patientData['visit_result_id'] = null;
$patientData['extract_date'] = null;
$patientData['death_date'] = null;
}
// ========== 2. РАСЧЕТ СТАТИСТИКИ НА ОСНОВЕ ПОДГОТОВЛЕННЫХ ДАННЫХ ==========
// Флаги для пациента
$hasRecipientInPeriod = false;
$hasDeathInPeriod = false;
$hasTransferInPeriod = false;
$hasDischargeInPeriod = false;
$hasActiveMigrationInPeriod = false;
// Проверяем каждую миграцию для определения статусов
foreach ($preparedMigrations as $migration) {
$ingoingDate = $migration['ingoing_date'] ? Carbon::parse($migration['ingoing_date']) : null;
$outDate = $migration['out_date'] ? Carbon::parse($migration['out_date']) : null;
$visitResultId = $migration['visit_result_id'] ?? null;
$statCureResultId = $migration['stat_cure_result_id'] ?? null;
// Поступление в периоде
if ($ingoingDate && $this->dateRangeService->dateInPeriod($ingoingDate, $dateRange)) {
$hasRecipientInPeriod = true;
}
// Проверяем активную миграцию на конец периода
if ($ingoingDate && $ingoingDate <= $periodEndCarbon) {
if (!$outDate || $outDate > $periodEndCarbon) {
$hasActiveMigrationInPeriod = true;
}
}
// Умер в периоде
if (!empty($patient['death_date'])) {
$deathDateCarbon = Carbon::parse($patient['death_date']);
if ($deathDateCarbon >= $periodStartCarbon && $deathDateCarbon <= $periodEndCarbon) {
$hasDeathInPeriod = true;
}
}
// Выбытие в периоде (есть out_date в периоде)
if ($outDate && $this->dateRangeService->dateInPeriod($outDate, $dateRange)) {
// Перевод (коды 4, 14)
if (in_array($visitResultId, [4, 14])) {
$hasTransferInPeriod = true;
}
// Выписка
else {
$hasDischargeInPeriod = true;
}
}
}
// Если нет исхода по миграциям, проверяем extract_date на уровне карты
// if (!$hasDeathInPeriod && !$hasTransferInPeriod && !$hasDischargeInPeriod) {
// if ($hasExtractInPeriod) {
// $visitResultId = $patient['visit_result_id'] ?? null;
// $deathDate = $patient['death_date'] ? Carbon::parse($patient['death_date']) : null;
//
// if ($deathDate && $deathDate->lte($periodEndCarbon)) {
// $hasDeathInPeriod = true;
// } elseif (in_array($visitResultId, [4, 14])) {
// $hasTransferInPeriod = true;
// } else {
// $hasDischargeInPeriod = true;
// }
// }
// }
// Заполнение статистики по статусам
// Умершие
if ($hasDeathInPeriod) {
$totalStats['deceased']++;
$totalStats['by_status']['deceased'] = ($totalStats['by_status']['deceased'] ?? 0) + 1;
}
// Переведенные
elseif ($hasTransferInPeriod) {
$totalStats['transferred']++;
$totalStats['by_status']['transferred'] = ($totalStats['by_status']['transferred'] ?? 0) + 1;
}
// Выписанные
elseif ($hasDischargeInPeriod) {
$totalStats['discharged']++;
$totalStats['outcome']++;
$totalStats['by_status']['discharged'] = ($totalStats['by_status']['discharged'] ?? 0) + 1;
}
// В отделении на конец периода
elseif ($hasActiveMigrationInPeriod) {
$totalStats['in_department']++;
$totalStats['by_status']['in_department'] = ($totalStats['by_status']['in_department'] ?? 0) + 1;
}
// Поступившие (на основе ingoing_date в периоде)
if ($hasRecipientInPeriod) {
$totalStats['admitted']['today']++;
$totalStats['by_status']['recipient'] = ($totalStats['by_status']['recipient'] ?? 0) + 1;
if ($patient['urgency_id'] == 1) {
$totalStats['admitted']['planned']++;
$totalStats['planned']++;
}
if ($patient['urgency_id'] == 2) {
$totalStats['admitted']['urgent']++;
}
}
// Срочность
if (!empty($patient['patient_urgency'])) {
$totalStats['by_urgency'][$patient['patient_urgency']] = ($totalStats['by_urgency'][$patient['patient_urgency']] ?? 0) + 1;
}
// Реанимация
if (!empty($patient['in_reanimation'])) {
$totalStats['in_reanimation']++;
}
// Поступил сегодня
if (!empty($patient['admitted_today'])) {
$totalStats['admitted_today']++;
}
// ========== 3. КОЙКО-ДНИ И ОПЕРАЦИИ ==========
$patientBedDays = 0;
$preOpBedDays = 0;
$hasOperation = false;
$patientOperationsCount = 0;
$patientOperationsPlannedCount = 0;
$patientOperationsUrgentCount = 0;
try {
$migration = $patient['migrations'][0];
$migrationStart = $migration['ingoing_date'] ? Carbon::parse($migration['ingoing_date']) : null;
$migrationEnd = $migration['out_date'] ? Carbon::parse($migration['out_date']) : null;
if (!$migrationStart) {
continue;
}
// Проверяем пересечение с отчетным периодом
$hasIntersection = $migrationStart <= $periodEndCarbon &&
($migrationEnd === null || $migrationEnd >= $periodStartCarbon);
// if (!$hasIntersection) {
// continue;
// }
// ===== КОЙКО-ДНИ =====
$calcStart = $migrationStart > $periodStartCarbon ? $migrationStart : $periodStartCarbon;
$calcEnd = $migrationEnd && $migrationEnd < $periodEndCarbon ? $migrationEnd : $periodEndCarbon;
$bedDays = $calcStart->diffInDays($calcEnd);
$patientBedDays += max(0, $bedDays);
// ===== ПРЕДОПЕРАЦИОННЫЕ ДНИ =====
$opsInPeriod = collect($patient['operations'] ?? [])
->filter(function ($op) use ($dateRange) {
$opStart = $op['start_date'] ? Carbon::parse($op['start_date']) : null;
return $opStart
&& $opStart->gte($dateRange->start())
&& $opStart->lt($dateRange->end());
});
if ($opsInPeriod->isNotEmpty()) {
$hasOperation = true;
$firstOpInPeriod = $opsInPeriod->sortBy('start_date')->first();
if ($firstOpInPeriod && $migrationStart) {
$opDate = Carbon::parse($firstOpInPeriod['start_date']);
if ($opDate > $migrationStart) {
$preOpDays = $migrationStart->copy()->startOfDay()
->diffInDays($opDate->copy()->startOfDay());
$preOpBedDays += max(0, $preOpDays);
}
}
}
// ===== ОПЕРАЦИИ =====
if (count($patient['operations']) > 0)
foreach ($patient['operations'] as $operation) {
$opStart = $operation['start_date'] ? Carbon::parse($operation['start_date']) : null;
$opEnd = $operation['end_date'] ? Carbon::parse($operation['end_date']) : null;
if ($opStart && $opStart->gte($dateRange->start()) && $opStart->lt($dateRange->end())) {
// Уникализируем по ID операции
$uniqueOperationIds[$operation['id']] = [
'id' => $operation['id'],
'urgent_status' => $operation['urgent_status'],
];
}
}
} catch (\Exception $e) {
\Log::error('DutyReportService: ошибка обработки пациента', [
'patient_id' => $patient['id'] ?? null,
'error' => $e->getMessage(),
]);
} finally {
}
// Агрегация койко-дней и операций
$totalStats['total_bed_days'] += $patientBedDays;
$totalStats['total_preop_bed_days'] += $preOpBedDays;
$totalStats['patients_with_operations'] += $hasOperation ? 1 : 0;
$totalStats['total_operations'] = count($uniqueOperationIds);
$totalStats['planned_operations'] = collect($uniqueOperationIds)->where('urgent_status', 6)->count();
$totalStats['urgent_operations'] = collect($uniqueOperationIds)->whereIn('urgent_status', [4,5])->count();
$totalStats['total_patients']++;
// ========== 4. ДОБАВЛЯЕМ В БАТЧИ ==========
$patientBatch[] = $patientData;
$migrationBatch = array_merge($migrationBatch, $preparedMigrations);
// Контроль размера батча
if (count($patientBatch) >= $batchSize) {
$batchStats = $this->upsertBatches($reportDutyId, $patientBatch, $migrationBatch, $reanimationBatch);
$totalStats = $this->mergeStats($totalStats, $batchStats);
$patientBatch = [];
$migrationBatch = [];
$reanimationBatch = [];
}
}
// Сохраняем остаток
if (!empty($patientBatch)) {
$batchStats = $this->upsertBatches($reportDutyId, $patientBatch, $migrationBatch, $reanimationBatch);
$totalStats = $this->mergeStats($totalStats, $batchStats);
}
return $totalStats;
}
/**
* Вспомогательный метод: слияние статистики двух массивов
*/
private function mergeStats(array $total, array $new): array
{
$total['saved_patients'] += $new['saved_patients'] ?? 0;
$total['saved_migrations'] += $new['saved_migrations'] ?? 0;
$total['in_reanimation'] += $new['in_reanimation'] ?? 0;
$total['admitted_today'] += $new['admitted_today'] ?? 0;
$total['in_department'] += $new['in_department'] ?? 0;
$total['planned'] += $new['planned'] ?? 0;
$total['deceased'] += $new['deceased'] ?? 0;
$total['transferred'] += $new['transferred'] ?? 0;
$total['outcome'] += $new['outcome'] ?? 0;
// Койко-дни
$total['total_bed_days'] += $new['total_bed_days'] ?? 0;
$total['total_preop_bed_days'] += $new['total_preop_bed_days'] ?? 0;
// Операции
$total['patients_with_operations'] += $new['patients_with_operations'] ?? 0;
$total['total_operations'] += $new['total_operations'] ?? 0;
$total['planned_operations'] += $new['planned_operations'] ?? 0;
$total['urgent_operations'] += $new['urgent_operations'] ?? 0;
// Для расчётов
$total['total_patients'] += $new['total_patients'] ?? 0;
// Объединение счётчиков по статусам
foreach ($new['by_status'] ?? [] as $status => $count) {
$total['by_status'][$status] = ($total['by_status'][$status] ?? 0) + $count;
}
foreach ($new['by_urgency'] ?? [] as $urgency => $count) {
$total['by_urgency'][$urgency] = ($total['by_urgency'][$urgency] ?? 0) + $count;
}
foreach ($new['admitted'] ?? [] as $status => $count) {
$total['admitted'][$status] = ($total['admitted'][$status] ?? 0) + $count;
}
return $total;
}
/**
* Вспомогательный метод: выполняет upsert для пациентов, миграций и реанимаций
*/
private function upsertBatches(
int $reportDutyId,
array $patientBatch,
array $migrationBatch,
array $reanimationBatch = []
): array
{
if (empty($patientBatch)) {
return ['saved_patients' => 0, 'saved_migrations' => 0, 'saved_reanimations' => 0];
}
$savedPatients = 0;
$savedMigrations = 0;
$savedReanimations = 0;
DB::transaction(function () use (
$reportDutyId,
$patientBatch,
$migrationBatch,
$reanimationBatch,
&$savedPatients,
&$savedMigrations,
&$savedReanimations
) {
// Получаем дату отчета
$reportDate = DB::table('report_duties')
->where('id', $reportDutyId)
->value('period_end');
// === 1. UPSERT пациентов ===
$patientUniqueBy = ['report_duty_id', 'source_type', 'original_id'];
// Модифицируем patientBatch перед сохранением
$filteredPatientBatch = array_map(function($patient) use ($reportDutyId, $reportDate) {
// Очищаем death_date, если она есть
// (заплатка для данных из МИС - бывает что дата смерти не устанавливается)
// Пример карт - 329609 и 325529
if (!empty($patient['death_date'])) {
$deathDate = Carbon::parse($patient['death_date']);
} else {
$deathDate = Carbon::parse($patient['extract_date']);
}
$visitResultId = $patient['visit_result_id'];
if (in_array($visitResultId, [5, 15])) {
// Если дата смерти ПОЗЖЕ даты отчета - удаляем
if ($deathDate->gt($reportDate)) {
$patient['death_date'] = null;
} else {
// Если дата смерти в тот же день или раньше - оставляем
$patient['death_date'] = $deathDate;
}
}
// extract_date оставляем всегда (это дата перевода/выбытия из отделения)
// Она может быть в пределах отчетного периода
return $patient;
}, $patientBatch);
$patientUpdateColumns = array_diff(array_keys($filteredPatientBatch[0]), $patientUniqueBy);
DB::table('report_duty_patients')->upsert(
$filteredPatientBatch,
$patientUniqueBy,
$patientUpdateColumns
);
$savedPatients = count($filteredPatientBatch);
// === 2. Получаем ID сохранённых пациентов для связи с миграциями ===
$patientIds = [];
if (!empty($migrationBatch) || !empty($reanimationBatch)) {
$patientIds = DB::table('report_duty_patients')
->where('report_duty_id', $reportDutyId)
->pluck('id', 'original_id')
->toArray();
}
// === 3. UPSERT миграций ===
$migrationIds = []; // [original_id => migration_db_id]
if (!empty($migrationBatch)) {
$finalMigrations = [];
foreach ($migrationBatch as $m) {
$originalId = $m['_temp_key']['original_id'];
if (isset($patientIds[$originalId])) {
$finalMigrations[] = [
'medical_history_id' => $patientIds[$originalId],
'original_id' => $m['original_id'] ?? null,
'ingoing_date' => $m['ingoing_date'],
'out_date' => $m['out_date'],
'diagnosis_id' => $m['diagnosis_id'],
'diagnosis_code' => $m['diagnosis_code'],
'diagnosis_name' => $m['diagnosis_name'],
'interrupted_event_id' => $m['interrupted_event_id'],
'stationar_branch_id' => $m['stationar_branch_id'],
'department_id' => $m['department_id'],
'visit_result_id' => $m['visit_result_id'],
'stat_cure_result_id' => $m['stat_cure_result_id'],
'user_id' => $m['user_id'],
'mis_user_id' => $m['mis_user_id'],
'comment' => $m['comment'],
];
}
}
if (!empty($finalMigrations)) {
$migrationUniqueBy = ['medical_history_id', 'ingoing_date'];
$migrationUpdateColumns = array_diff(array_keys($finalMigrations[0]), $migrationUniqueBy);
DB::table('report_duty_migration_patients')->upsert(
$finalMigrations,
$migrationUniqueBy,
$migrationUpdateColumns
);
$savedMigrations = count($finalMigrations);
// === 4. Получаем ID сохранённых миграций для связи с реанимациями ===
// Ключ: [medical_history_id][original_id] => migration_db_id
$savedMigrationRecords = DB::table('report_duty_migration_patients')
->whereIn('medical_history_id', array_values($patientIds))
->get(['id', 'medical_history_id', 'original_id']);
foreach ($savedMigrationRecords as $record) {
$migrationIds[$record->medical_history_id][$record->original_id] = $record->id;
}
}
}
// === 5. UPSERT реанимаций ===
if (!empty($reanimationBatch) && !empty($patientIds) && !empty($migrationIds)) {
$finalReanimations = [];
foreach ($reanimationBatch as $r) {
$tempKey = $r['_temp_key'];
$patientOriginalId = $tempKey['patient_id'] ?? $tempKey['original_id'] ?? null;
$migrationOriginalId = $tempKey['migration_id'] ?? null;
$medicalHistoryId = $patientIds[$patientOriginalId] ?? null;
// Ищем ID миграции по medical_history_id + original_id
$migrationDbId = $migrationIds[$medicalHistoryId][$migrationOriginalId] ?? null;
if ($medicalHistoryId && $migrationDbId) {
$finalReanimations[] = [
'migration_patient_id' => $migrationDbId,
'medical_history_id' => $medicalHistoryId,
'original_id' => $r['original_id'] ?? null,
'in_date' => $r['in_date'],
'out_date' => $r['out_date'],
'description' => $r['description'],
'stationar_branch_id' => $r['stationar_branch_id'],
'migration_stationar_branch_id' => $r['migration_stationar_branch_id'],
'migration_department_id' => $r['migration_department_id'],
'doctor_id' => $r['doctor_id'],
'user_id' => $r['user_id'],
'mis_user_id' => $r['mis_user_id'],
'comment' => $r['comment'],
];
}
}
if (!empty($finalReanimations)) {
$reanimationUniqueBy = ['migration_patient_id', 'in_date'];
$reanimationUpdateColumns = array_diff(array_keys($finalReanimations[0]), $reanimationUniqueBy);
DB::table('report_duty_reanimations')->upsert(
$finalReanimations,
$reanimationUniqueBy,
$reanimationUpdateColumns
);
$savedReanimations = count($finalReanimations);
}
}
});
return [
'saved_patients' => $savedPatients,
'saved_migrations' => $savedMigrations,
'saved_reanimations' => $savedReanimations,
// Эти поля считаются в saveReportSnapshot, здесь передаём 0
'total_bed_days' => 0,
'total_preop_bed_days' => 0,
'patients_with_operations' => 0,
'total_operations' => 0,
'planned_operations' => 0,
'urgent_operations' => 0,
'total_patients' => 0,
];
}
public function saveMetrics(array $stats, ReportDuty $reportDuty, int $staff = 0)
{
$byStatus = $stats['by_status'] ?? [];
$byUrgency = $stats['by_urgency'] ?? [];
$admitted = $stats['admitted'] ?? [];
// === Базовые счётчики ===
$patientsIsRecipient = $byStatus['recipient'] ?? 0;
$patientsInDepartment = $byStatus['in_department'] ?? 0;
$patientsIsDischarged = $stats['outcome'] ?? 0;
$patientsIsTransferred = $stats['transferred'] ?? 0;
$patientsIsDeceased = $byStatus['deceased'] ?? 0;
// Ср. койко-день (по закрытым эпизодам: выписка/перевод/смерть)
$totalPatients = $stats['total_patients'] ?? 0;
$totalBedDays = $stats['total_bed_days'] ?? 0;
$avgBedDay = $totalPatients > 0
? round($totalBedDays / $totalPatients, 2)
: 0;
// Пред. опер. койко-день (средний по пациентам с операциями)
$patientsWithOps = $stats['patients_with_operations'] ?? 0;
$totalPreOpDays = $stats['total_preop_bed_days'] ?? 0;
$avgPreOpBedDay = $patientsWithOps > 0
? round($totalPreOpDays / $patientsWithOps, 2)
: 0;
// % загруженности
$bedsInDepartment = DepartmentMetrikaDefault::where('rf_department_id', $reportDuty->rf_department_id)
->where('rf_metrika_item_id', 1) // ID метрики "коек"
->where('date_end', '>', Carbon::now())
->value('value') ?? 0;
$occupancyPercent = $bedsInDepartment > 0
? round(($patientsInDepartment * 100) / $bedsInDepartment, 2)
: 0;
// % летальности
$totalPatients = $stats['total_patients'] ?? 0;
$mortalityPercent = $totalPatients > 0
? round(($patientsIsDeceased * 100) / $totalPatients, 2)
: 0;
// Операции (общее количество)
$totalOperations = $stats['total_operations'] ?? 0;
$plannedOperations = $stats['planned_operations'] ?? 0;
$urgentOperations = $stats['urgent_operations'] ?? 0;
// === СОХРАНЕНИЕ МЕТРИК ===
$this->saveMetric($reportDuty->id, 1, $bedsInDepartment); // Кол-во коек
$this->saveMetric($reportDuty->id, 8, $patientsInDepartment); // Пациентов в отделении
$this->saveMetric($reportDuty->id, 3, $patientsIsRecipient); // Поступило
$this->saveMetric($reportDuty->id, 15, $patientsIsDischarged); // Выписано
$this->saveMetric($reportDuty->id, 7, $patientsIsDischarged); // Выписано
$this->saveMetric($reportDuty->id, 13, $patientsIsTransferred); // Переведено
$this->saveMetric($reportDuty->id, 9, $patientsIsDeceased); // Умерло
$this->saveMetric($reportDuty->id, 4, $admitted['planned'] ?? 0); // Планово поступило
$this->saveMetric($reportDuty->id, 12, $admitted['urgent'] ?? 0); // Экстренно поступило
$this->saveMetric($reportDuty->id, 22, $occupancyPercent); // % загруженности
$this->saveMetric($reportDuty->id, 25, round($totalBedDays, 2)); // Всего койко-дней
$this->saveMetric($reportDuty->id, 18, $avgBedDay); // Ср. койко-день
$this->saveMetric($reportDuty->id, 26, round($totalPreOpDays, 2)); // Пред. опер. койко-день (сумма)
$this->saveMetric($reportDuty->id, 27, $patientsWithOps); // Пациентов с операциями (знаменатель)
$this->saveMetric($reportDuty->id, 21, $avgPreOpBedDay); // Ср. Пред. опер. койко-день
$this->saveMetric($reportDuty->id, 19, $mortalityPercent); // % летальности
$this->saveMetric($reportDuty->id, 11, $plannedOperations); // Плановых операций
$this->saveMetric($reportDuty->id, 10, $urgentOperations); // Экстренных операций
$this->saveMetric($reportDuty->id, 17, $staff); // Мед. персонал
}
/**
* Рассчитать количество дней пересечения двух периодов
*/
private function calculateBedDays(
?string $ingoingDate,
?string $outDate,
string $periodStart,
string $periodEnd
): int {
if (!$ingoingDate || !$outDate) {
return 0;
}
$start = Carbon::parse($ingoingDate);
$end = Carbon::parse($outDate);
return max(1, $start->diffInDays($end, true));
}
private function saveMetric(int $reportId, int $metricId, int|float $value)
{
DutyReportMetricResult::updateOrCreate(
['rf_report_id' => $reportId, 'rf_metrika_item_id' => $metricId],
['value' => $value]
);
}
public function saveObservables(array $observables, ReportDuty $reportDuty)
{
foreach ($observables as $observable) {
ObservableMedicalHistory::updateOrCreate([
'original_id' => $observable['original_id'] ?? $observable['id'],
'source_type' => $observable['source_type'],
'observable_in' => $observable['observable_in'] ?? $reportDuty->period_start,
], [
'source_type' => $observable['source_type'],
'original_id' => $observable['original_id'],
'observable_in' => $observable['observable_in'] ?? $reportDuty->period_start,
'observable_out' => $observable['observable_out'] ?? null,
'observable_reason' => $observable['observable_reason'],
'out_reason' => $observable['out_reason'] ?? null,
'medical_card_number' => $observable['medical_card_number'],
'full_name' => $observable['full_name'],
'birth_date' => $observable['birth_date'],
'recipient_date' => $observable['recipient_date'],
'extract_date' => $observable['extract_date'],
'death_date' => $observable['death_date'],
'male' => $observable['male'],
'urgency_id' => $observable['urgency_id'],
'hospital_result_id' => $observable['hospital_result_id'],
'visit_result_id' => $observable['visit_result_id'],
'comment' => $observable['comment'],
'user_id' => $observable['user_id'],
]);
}
return $reportDuty->unwantedEvents()->count();
}
public function saveUnwantedEvents(array $unwantedEvents, ReportDuty $reportDuty)
{
foreach ($unwantedEvents as $unwantedEvent) {
DutyUnwantedEvent::updateOrCreate([
'id' => $unwantedEvent['id'] ?? null,
'report_duty_id' => $reportDuty->id,
], [
'report_duty_id' => $reportDuty->id,
'title' => $unwantedEvent['title'],
'comment' => $unwantedEvent['comment']
]);
}
return $reportDuty->unwantedEvents()->count();
}
}