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(); } }