reportMetricsFinalizer = $reportMetricsFinalizer ?? app(ReportMetricsFinalizer::class); $this->calculatedMetricsSynchronizer = $calculatedMetricsSynchronizer ?? app(CalculatedMetricsSynchronizer::class); $this->autoFillReportPayloadBuilder = $autoFillReportPayloadBuilder ?? app(AutoFillReportPayloadBuilder::class); $this->reportPatientsReadService = $reportPatientsReadService ?? app(ReportPatientsReadService::class); } protected ReportMetricsFinalizer $reportMetricsFinalizer; protected CalculatedMetricsSynchronizer $calculatedMetricsSynchronizer; protected AutoFillReportPayloadBuilder $autoFillReportPayloadBuilder; protected ReportPatientsReadService $reportPatientsReadService; /** * Получить статистику для отчета */ public function getReportStatistics(Department $department, User $user, DateRange $dateRange): array { $misDepartmentId = $department->rf_mis_department_id; $branchId = $this->getBranchId($misDepartmentId); // Определяем, используем ли мы снапшоты $useSnapshots = $this->shouldUseSnapshots($department, $user, $dateRange); if ($useSnapshots) { return $this->getStatisticsFromSnapshots($department, $dateRange, $branchId); } return $this->getStatisticsFromReplica($department, $user, $dateRange, $branchId); } /** * Создать или обновить отчет */ public function storeReport(array $data, User $user, $fillableAuto = false): Report { $this->prepareMemoryForHeavySave(); try { $report = DB::transaction(function () use ($data, $user, $fillableAuto) { $report = $this->createOrUpdateReport($data, $user); // Сохраняем все, что НЕ зависит от других отчетов $this->saveMetrics($report, $data['metrics'] ?? []); $this->saveUnwantedEvents($report, $data['unwantedEvents'] ?? []); $this->saveObservationPatients($report, $data['observationPatients'] ?? [], $user->rf_department_id); $this->snapshotService->createPatientSnapshots($report, $user, $data['dates'], $fillableAuto); $this->syncCalculatedMetrics($report, $user, $data); return $report; }); DB::transaction(function () use ($report) { $this->reportMetricsFinalizer->finalize($report); $this->saveLethalMetricFromSnapshots($report); }); } catch (\Throwable $e) { throw $e; } $this->clearCacheAfterReportCreation($user, $report); return $report; } public function prepareForHeavySave(): void { $this->prepareMemoryForHeavySave(); } public function syncCalculatedMetricsForStoredReport(Report $report, User $user, array $data): void { $this->calculatedMetricsSynchronizer->sync($report, $user, $data); } public function finalizeStoredReport(Report $report): void { $this->reportMetricsFinalizer->finalize($report); } public function saveLethalMetricForStoredReport(Report $report): void { $this->saveLethalMetricFromSnapshots($report); } public function clearCacheAfterStoredReport(User $user, Report $report): void { $this->clearCacheAfterReportCreation($user, $report); } private function prepareMemoryForHeavySave(): void { $connectionNames = array_unique(array_filter([ DB::getDefaultConnection(), (new MisMedicalHistory)->getConnectionName(), (new MisMigrationPatient)->getConnectionName(), (new MisStationarBranch)->getConnectionName(), ])); foreach ($connectionNames as $connectionName) { try { $connection = DB::connection($connectionName); $connection->disableQueryLog(); $connection->flushQueryLog(); } catch (\Throwable) { // best-effort cleanup only } } if (function_exists('gc_collect_cycles')) { gc_collect_cycles(); } } public function buildAutoFillReportPayload(User $user, Department $department, DateRange $dateRange): array { return $this->autoFillReportPayloadBuilder->build($user, $department, $dateRange); } /** * Сохранить метрику койко-дня из снапшотов отчета */ protected function saveBedDaysMetric(Report $report): void { try { $result = $this->calculateBedDaysFromSnapshots($report); MetrikaResult::updateOrCreate( [ 'rf_report_id' => $report->report_id, 'rf_metrika_item_id' => MetrikaConfig::TOTAL_BED_DAYS, ], ['value' => $result['total_days']] ); MetrikaResult::updateOrCreate( [ 'rf_report_id' => $report->report_id, 'rf_metrika_item_id' => MetrikaConfig::AVERAGE_BED_DAYS, ], ['value' => $result['avg_days']] ); } catch (\Throwable $e) { \Log::error('Failed to save bed days metric: '.$e->getMessage()); } } protected function calculateBedDaysFromSnapshots(Report $report): array { $snapshots = MedicalHistorySnapshot::where('rf_report_id', $report->report_id) ->whereIn('patient_type', ['discharged', 'deceased']) ->with('medicalHistory') ->get(); $totalDays = 0; $patientCount = 0; foreach ($snapshots as $snapshot) { $history = $snapshot->medicalHistory; if (! $history) { continue; } $start = $history->DateRecipientHS ?? $history->DateRecipient ?? null; if (! $start) { continue; } $end = null; if ($snapshot->patient_type === 'deceased') { if ($history->DateDeath && ! in_array($history->DateDeath->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) { $end = $history->DateDeath; } elseif ($history->DateExtract && ! in_array($history->DateExtract->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) { $end = $history->DateExtract; } } else { if ($history->DateExtract && ! in_array($history->DateExtract->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) { $end = $history->DateExtract; } } if (! $end) { continue; } $start = Carbon::parse($start); $end = Carbon::parse($end); if ($end->lt($start)) { continue; } // Календарные койко-дни $days = $start->startOfDay()->diffInDays($end->startOfDay()); $totalDays += $days; $patientCount++; } return [ 'total_days' => $totalDays, 'patient_count' => $patientCount, 'avg_days' => $patientCount > 0 ? round($totalDays / $patientCount, 2) : 0, ]; } /** * Рассчитать предоперационные койко-дни по снапшотам отчета * * Возвращает: * - total_days: общее количество предоперационных койко-дней * - patient_count: количество пациентов, вошедших в расчет * - avg_days: средний предоперационный койко-день */ protected function calculatePreoperativeDaysFromSnapshots(Report $report): array { $patientIds = MedicalHistorySnapshot::where('rf_report_id', $report->report_id) ->whereIn('patient_type', ['discharged', 'deceased']) ->pluck('rf_medicalhistory_id') ->unique() ->values(); if ($patientIds->isEmpty()) { return [ 'total_days' => 0, 'patient_count' => 0, 'avg_days' => 0, ]; } $rows = DB::table('stt_medicalhistory as mh') ->join('stt_surgicaloperation as so', 'so.rf_MedicalHistoryID', '=', 'mh.MedicalHistoryID') ->whereIn('mh.MedicalHistoryID', $patientIds) ->whereNotNull('so.Date') ->select( 'mh.MedicalHistoryID', DB::raw('MIN(so."Date") as first_operation'), 'mh.DateRecipientHS', 'mh.DateRecipient' ) ->groupBy('mh.MedicalHistoryID', 'mh.DateRecipientHS', 'mh.DateRecipient') ->get(); if ($rows->isEmpty()) { return [ 'total_days' => 0, 'patient_count' => 0, 'avg_days' => 0, ]; } $totalDays = 0; $patientCount = 0; foreach ($rows as $row) { $startRaw = $row->DateRecipientHS ?? $row->DateRecipient ?? null; $operationRaw = $row->first_operation ?? null; if (! $startRaw || ! $operationRaw) { continue; } $start = Carbon::parse($startRaw); $operation = Carbon::parse($operationRaw); if ($operation->lt($start)) { continue; } // Разница календарных дат $days = $start->startOfDay()->diffInDays($operation->startOfDay()); $totalDays += $days; $patientCount++; } return [ 'total_days' => $totalDays, 'patient_count' => $patientCount, 'avg_days' => $patientCount > 0 ? round($totalDays / $patientCount, 1) : 0, ]; } protected function saveLethalMetricFromSnapshots(Report $report): void { // Получаем все снапшоты выписанных пациентов из этого отчета $snapshots = MedicalHistorySnapshot::where('rf_report_id', $report->report_id) ->whereIn('patient_type', ['discharged', 'deceased']) // выписанные и умершие ->with('medicalHistory') ->get(); if ($snapshots->isEmpty()) { // Если нет выписанных, сохраняем 0 MetrikaResult::updateOrCreate( [ 'rf_report_id' => $report->report_id, 'rf_metrika_item_id' => MetrikaConfig::AVERAGE_BED_DAYS, ], ['value' => 0] ); \Log::info("No discharged patients in report {$report->report_id}, saved 0"); return; } } /** * Сохранить предоперационный койко-день из снапшотов */ protected function savePreoperativeMetric(Report $report): void { try { $result = $this->calculatePreoperativeDaysFromSnapshots($report); $this->saveMetric($report, MetrikaConfig::TOTAL_PREOPERATIVE_DAYS, $result['total_days']); $this->saveMetric($report, MetrikaConfig::PREOPERATIVE_PATIENT_COUNT, $result['patient_count']); $this->saveMetric($report, MetrikaConfig::PREOPERATIVE_AVERAGE_DAYS, $result['avg_days']); } catch (\Throwable $e) { \Log::error('Failed to save preoperative total metric: '.$e->getMessage()); } } /** * Сохранить % загруженности */ protected function saveDepartmentLoadedMetric(Report $report): void { // Получаем все снапшоты выписанных пациентов из этого отчета $currentCount = $report->metrikaResults()->where('rf_metrika_item_id', MetrikaConfig::CURRENT)->value('value'); $bedsCount = $report->metrikaResults()->where('rf_metrika_item_id', MetrikaConfig::BEDS)->value('value'); $percentLoaded = $bedsCount > 0 ? round($currentCount * 100 / $bedsCount) : 0; $this->saveMetric($report, MetrikaConfig::DEPARTMENT_LOADED, $percentLoaded); } /** * Очистить кэш после создания отчета */ private function clearCacheAfterReportCreation(User $user, Report $report): void { // Очищаем кэш статистики для пользователя // $this->statisticsService->clearStatisticsCache($user); // Также можно очистить кэш для всех пользователей отдела // $this->statisticsService->clearDepartmentStatisticsCache($user->rf_department_id); // Очищаем кэш за сегодня и вчера (так как отчеты влияют на эти даты) $this->clearDailyCache($user, $report->created_at); } /** * Очистить дневной кэш */ private function clearDailyCache(User $user, $reportDate): void { $datesToClear = [ Carbon::parse($reportDate)->format('Y-m-d'), Carbon::parse($reportDate)->subDay()->format('Y-m-d'), ]; foreach ($datesToClear as $date) { $cacheKey = $this->generateDailyCacheKey($user, $date); Cache::forget($cacheKey); } } private function generateDailyCacheKey(User $user, string $date): string { return 'daily_stats:'.$user->rf_department_id.':'.$date; } /** * Получить пациентов по статусу */ public function getPatientsByStatus( Department $department, User $user, string $status, DateRange $dateRange, bool $onlyIds = false, bool $beforeCreate = false, ?bool $includeCurrentPatients = null ) { return $this->reportPatientsReadService->getPatientsByStatus( $department, $user, $status, $dateRange, $onlyIds, $beforeCreate, $includeCurrentPatients ); } /** * Получить количество пациентов по статусу */ public function getPatientsCountByStatus( Department $department, User $user, string $status, DateRange $dateRange ): int { return $this->reportPatientsReadService->getPatientsCountByStatus($department, $user, $status, $dateRange); } public function getPatientsCountsMap(Department $department, User $user, DateRange $dateRange): array { return $this->reportPatientsReadService->getPatientsCountsMap($department, $user, $dateRange); } /** * Получить ID отделения из стационарного отделения */ private function getBranchId(int $misDepartmentId): ?int { return MisStationarBranch::where('rf_DepartmentID', $misDepartmentId) ->value('StationarBranchID'); } /** * Определить, нужно ли использовать снапшоты */ private function shouldUseSnapshots(Department $department, User $user, DateRange $dateRange, bool $beforeCreate = false): bool { if ($beforeCreate) { return false; } $report = $this->getReportForPeriod($department->department_id, $dateRange); if (! $report) { return false; } if ($report->status !== 'submitted') { return false; } return true; } /** * Создать или обновить отчет */ private function createOrUpdateReport(array $data, User $user): Report { $rangeStartAt = isset($data['dates'][0]) ? $this->dateRangeService->toSqlFormat($data['dates'][0]) : null; $rangeEndAt = isset($data['dates'][1]) ? $this->dateRangeService->toSqlFormat($data['dates'][1]) : null; $dateRange = $this->dateRangeService->createDateRangeForDate($this->dateRangeService->toCarbon($data['dates'][1]), $user); $sentAt = $data['sent_at'] ?? $rangeEndAt ?? $this->dateRangeService->toSqlFormat(\Illuminate\Support\Carbon::now()); $createdAt = $data['created_at'] ?? $rangeEndAt ?? $this->dateRangeService->toSqlFormat(\Illuminate\Support\Carbon::now()); $reportData = [ 'rf_department_id' => $data['departmentId'], 'rf_user_id' => $user->id, 'rf_lpudoctor_id' => $data['userId'], 'sent_at' => $sentAt, 'period_start' => $dateRange->startSql(), 'period_end' => $dateRange->endSql(), 'created_at' => $createdAt, 'status' => $data['status'] ?? 'draft', ]; if (isset($data['reportId']) && $data['reportId']) { $report = Report::updateOrCreate( ['report_id' => $data['reportId']], $reportData ); } else { $report = Report::create($reportData); $department = Department::where('department_id', $reportData['rf_department_id'])->first(); $beds = $department->metrikaDefault->where('rf_metrika_item_id', 1)->first(); MetrikaResult::create([ 'rf_report_id' => $report->report_id, 'rf_metrika_item_id' => MetrikaConfig::BEDS, 'value' => $beds->value, ]); } return $report; } /** * Сохранить метрики отчета */ private function saveMetrics(Report $report, array $metrics): void { foreach ($metrics as $key => $value) { $metrikaId = (int) str_replace('metrika_item_', '', $key); MetrikaResult::updateOrCreate( [ 'rf_report_id' => $report->report_id, 'rf_metrika_item_id' => $metrikaId, ], [ 'value' => $value, ] ); } } /** * Сохранить метрику отчета */ private function saveMetric(Report $report, int $metrikaId, float $value): void { MetrikaResult::updateOrCreate( [ 'rf_report_id' => $report->report_id, 'rf_metrika_item_id' => $metrikaId, ], [ 'value' => $value, ] ); } /** * Сохранить нежелательные события */ private function saveUnwantedEvents(Report $report, array $unwantedEvents): void { if (empty($unwantedEvents)) { $report->unwantedEvents()->delete(); $this->saveMetric($report, MetrikaConfig::UNWANTED_EVENTS, 0); return; } foreach ($unwantedEvents as $event) { if (isset($event['unwanted_event_id']) && $event['unwanted_event_id']) { UnwantedEvent::updateOrCreate( ['unwanted_event_id' => $event['unwanted_event_id']], [ 'rf_report_id' => $report->report_id, 'comment' => $event['comment'] ?? '', 'title' => $event['title'] ?? '', 'is_visible' => $event['is_visible'] ?? true, ] ); } else { UnwantedEvent::create([ 'rf_report_id' => $report->report_id, 'comment' => $event['comment'] ?? '', 'title' => $event['title'] ?? '', 'is_visible' => $event['is_visible'] ?? true, ]); } } // Обновить метрику $this->saveMetric($report, MetrikaConfig::UNWANTED_EVENTS, count($unwantedEvents)); } /** * Сохранить пациентов под наблюдением */ private function saveObservationPatients( Report $report, array $observationPatients, int $departmentId ): void { if (empty($observationPatients)) { ObservationPatient::where('rf_department_id', $departmentId) ->where('rf_report_id', $report->report_id) ->delete(); // Обновить метрику $this->saveMetric($report, MetrikaConfig::OBSERVATION, 0); return; } foreach ($observationPatients as $patient) { ObservationPatient::updateOrCreate( [ 'rf_medicalhistory_id' => $patient['medical_history_id'] ?? null, 'rf_department_patient_id' => $patient['department_patient_id'] ?? null, 'rf_department_id' => $departmentId, ], [ 'rf_report_id' => $report->report_id, 'rf_mkab_id' => null, 'comment' => $patient['comment'] ?? null, ] ); } // Обновить метрику $this->saveMetric($report, MetrikaConfig::OBSERVATION, count($observationPatients)); } private function syncCalculatedMetrics(Report $report, User $user, array $data): void { $this->calculatedMetricsSynchronizer->sync($report, $user, $data); } /** * Получить информацию о текущем отчете */ public function getCurrentReportInfo(Department $department, User $user, DateRange $dateRange): array { $reportToday = $this->getReportForPeriod($department->department_id, $dateRange); $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); $useSnapshots = $isHeadOrAdmin || ! $dateRange->isEndDateToday() || $reportToday; // Получаем ID пользователя для заполнения отчета if ($useSnapshots && $isHeadOrAdmin && $reportToday) { $fillableUserId = $reportToday->rf_lpudoctor_id ?? null; } else { $fillableUserId = request()->query('userId', $user->rf_lpudoctor_id); } // Получаем нежелательные события $unwantedEvents = $this->getUnwantedEvents($department, $dateRange); // Определяем активность кнопки отправки $isActiveSendButton = $this->isSendButtonActive($user, $dateRange, $reportToday, $fillableUserId); $message = null; if ($reportToday) { $reportDoctor = $reportToday->lpuDoctor; $message = "Отчет создан пользователем: $reportDoctor->FAM_V $reportDoctor->IM_V $reportDoctor->OT_V"; } $statusMessage = $reportToday ? ($reportToday->status === 'submitted' ? 'Этот отчет в статусе: опубликован' : 'Этот отчет в статусе: черновик') : null; // Получаем информацию о враче $lpuDoctor = $this->getDoctorInfo($fillableUserId, $dateRange); // Проверяем, является ли диапазон одним днем // $isRangeOneDay = $this->dateRangeService->isRangeOneDay( // $endDate->copy()->subDay()->format('Y-m-d H:i:s'), // $endDate->format('Y-m-d H:i:s') // ); // Формируем даты для ответа // $date = $isHeadOrAdmin ? [ // $endDate->copy()->subDay()->getTimestampMs(), // $endDate->getTimestampMs() // ] : $endDate->getTimestampMs(); $date = $isHeadOrAdmin ? [ $dateRange->startDate->getTimestampMs(), $dateRange->endDate->getTimestampMs(), ] : $dateRange->endDate->getTimestampMs(); return [ 'report_id' => $reportToday?->report_id, 'unwantedEvents' => $unwantedEvents, 'isActiveSendButton' => $isActiveSendButton, 'message' => $dateRange->isOneDay ? $message : null, 'status' => $reportToday?->status ?? 'draft', 'statusMessage' => $dateRange->isOneDay ? $statusMessage : null, 'canPublish' => (bool) $reportToday && ($reportToday->status === 'draft') && $isActiveSendButton, 'isOneDay' => $dateRange->isOneDay, 'isHeadOrAdmin' => $isHeadOrAdmin, 'dates' => $date, 'userId' => $fillableUserId, 'userName' => $lpuDoctor ? "$lpuDoctor->FAM_V $lpuDoctor->IM_V $lpuDoctor->OT_V" : null, ]; } /** * Удалить пациента из наблюдения */ public function removeObservationPatient(string $patientId): void { [$sourceType, $id] = explode(':', $patientId) + [null, null]; if ($sourceType === 'manual') { ObservationPatient::where('rf_department_patient_id', $id)->delete(); return; } ObservationPatient::where('rf_medicalhistory_id', $id)->delete(); } public function createManualPatient(Department $department, User $user, array $data) { $report = $this->resolveReportForManualPatient($department, $user, $data); return $this->unifiedPatientService->createManualPatient($department, $user, $data, $report->report_id); } public function setManualPatientOutcome(User $user, int $departmentPatientId, array $data) { $patient = \App\Models\DepartmentPatient::where('department_patient_id', $departmentPatientId)->firstOrFail(); $updatedPatient = $this->unifiedPatientService->recordManualOutcome($patient, $data); $this->syncManualPatientSnapshots($updatedPatient, $user, []); return $updatedPatient; } public function updateManualPatient(User $user, int $departmentPatientId, array $data) { $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); $updatedPatient = $this->unifiedPatientService->updateManualPatient($patient, $data); $this->syncManualPatientSnapshots($updatedPatient, $user, $data); return $updatedPatient; } public function linkManualPatientToMis(int $departmentPatientId, int $medicalHistoryId) { $patient = \App\Models\DepartmentPatient::where('department_patient_id', $departmentPatientId)->firstOrFail(); return $this->unifiedPatientService->linkManualPatientToMis($patient, $medicalHistoryId); } public function getManualPatientOperations(User $user, int $departmentPatientId) { $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); return $patient->operations() ->with('serviceMedical') ->orderByDesc('started_at') ->get(); } public function createManualPatientOperation(User $user, int $departmentPatientId, array $data): DepartmentPatientOperation { $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); $service = MisServiceMedical::query() ->where('ServiceMedicalID', $data['service_id']) ->firstOrFail(); return $patient->operations()->create([ 'rf_kl_service_medical_id' => $service->ServiceMedicalID, 'service_code' => $service->ServiceMedicalCode, 'service_name' => $service->ServiceMedicalName, 'urgency' => $data['urgency'], 'started_at' => $data['started_at'], 'ended_at' => $data['ended_at'], 'created_by' => $user->id, ])->load('serviceMedical'); } public function updateManualPatientOperation(User $user, int $departmentPatientId, int $operationId, array $data): DepartmentPatientOperation { $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); $service = MisServiceMedical::query() ->where('ServiceMedicalID', $data['service_id']) ->firstOrFail(); $operation = $patient->operations() ->where('department_patient_operation_id', $operationId) ->firstOrFail(); $operation->update([ 'rf_kl_service_medical_id' => $service->ServiceMedicalID, 'service_code' => $service->ServiceMedicalCode, 'service_name' => $service->ServiceMedicalName, 'urgency' => $data['urgency'], 'started_at' => $data['started_at'], 'ended_at' => $data['ended_at'], ]); return $operation->fresh()->load('serviceMedical'); } public function deleteManualPatientOperation(User $user, int $departmentPatientId, int $operationId): void { $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); $patient->operations() ->where('department_patient_operation_id', $operationId) ->firstOrFail() ->delete(); } public function saveReanimationIndicator( User $user, int $departmentId, int $medicalHistoryId, string $indicator, ?string $comment = null, ?int $reportId = null ): ReanimationPatientIndicator { return ReanimationPatientIndicator::create([ 'rf_department_id' => $departmentId, 'rf_report_id' => $reportId, 'rf_medicalhistory_id' => $medicalHistoryId, 'indicator' => $indicator, 'comment' => $comment, 'created_by' => $user->id, ]); } public function getLatestReanimationIndicators(int $departmentId, array $medicalHistoryIds) { if (empty($medicalHistoryIds)) { return collect(); } $subQuery = ReanimationPatientIndicator::query() ->selectRaw('MAX(reanimation_patient_indicator_id) as max_id, rf_medicalhistory_id') ->where('rf_department_id', $departmentId) ->whereIn('rf_medicalhistory_id', $medicalHistoryIds) ->groupBy('rf_medicalhistory_id'); return ReanimationPatientIndicator::query() ->joinSub($subQuery, 'latest', function ($join) { $join->on('reanimation_patient_indicators.reanimation_patient_indicator_id', '=', 'latest.max_id'); }) ->get([ 'reanimation_patient_indicators.rf_medicalhistory_id', 'reanimation_patient_indicators.indicator', 'reanimation_patient_indicators.comment', ]) ->keyBy('rf_medicalhistory_id'); } public function getReanimationIndicatorsHistory( int $departmentId, int $medicalHistoryId, int $limit = 50 ) { return ReanimationPatientIndicator::query() ->where('rf_department_id', $departmentId) ->where('rf_medicalhistory_id', $medicalHistoryId) ->orderByDesc('reanimation_patient_indicator_id') ->limit($limit) ->get([ 'reanimation_patient_indicator_id', 'rf_report_id', 'rf_medicalhistory_id', 'indicator', 'comment', 'created_by', 'created_at', ]); } public function searchMisPatientsForDepartment(Department $department, string $query) { return $this->unifiedPatientService->searchMisPatients($department, $query); } private function resolveManageableManualPatient(User $user, int $departmentPatientId): DepartmentPatient { $query = DepartmentPatient::query() ->where('department_patient_id', $departmentPatientId) ->whereIn('source_type', ['manual', 'special']); if (! $user->isAdmin() && ! $user->isHeadOfDepartment()) { $query->where('rf_department_id', $user->department->department_id); } return $query->firstOrFail(); } private function syncManualPatientSnapshots(DepartmentPatient $patient, User $user, array $data): void { $reportIds = $patient->rf_report_id ? [$patient->rf_report_id] : (isset($data['startAt'], $data['endAt']) && $data['startAt'] && $data['endAt'] ? $this->getReportsForDateRange( $patient->rf_department_id, $this->dateRangeService->getNormalizedDateRange( $user, (string) $data['startAt'], (string) $data['endAt'] ) )->pluck('report_id')->values()->all() : []); if (empty($reportIds)) { return; } MedicalHistorySnapshot::query() ->whereIn('rf_report_id', $reportIds) ->where('rf_department_patient_id', $patient->department_patient_id) ->update([ 'patient_kind' => $patient->patient_kind, 'full_name' => $patient->full_name, 'birth_date' => $patient->birth_date, 'diagnosis_code' => $patient->diagnosis_code, 'diagnosis_name' => $patient->diagnosis_name, 'admitted_at' => $patient->admitted_at, 'outcome_type' => $patient->is_current ? null : $patient->outcome_type, 'outcome_at' => $patient->is_current ? null : $patient->outcome_at, 'updated_at' => now(), ]); } private function resolveReportForManualPatient(Department $department, User $user, array $data): Report { $reportId = $data['report_id'] ?? null; if ($reportId) { return Report::query() ->where('report_id', $reportId) ->where('rf_department_id', $department->department_id) ->firstOrFail(); } if (! isset($data['startAt'], $data['endAt']) || ! $data['startAt'] || ! $data['endAt']) { throw new \InvalidArgumentException('Не указан отчет или диапазон для привязки спецконтингента'); } $dateRange = $this->dateRangeService->getNormalizedDateRange( $user, (string) $data['startAt'], (string) $data['endAt'] ); $existingReport = Report::query() ->where('rf_department_id', $department->department_id) ->exactPeriod($dateRange->startSql(), $dateRange->endSql()) ->first(); if ($existingReport) { return $existingReport; } return Report::query()->create([ 'rf_department_id' => $department->department_id, 'rf_user_id' => $user->id, 'rf_lpudoctor_id' => $data['user_id'] ?? $user->rf_lpudoctor_id, 'sent_at' => $dateRange->endSql(), 'created_at' => $dateRange->endSql(), 'period_start' => $dateRange->startSql(), 'period_end' => $dateRange->endSql(), 'status' => 'draft', ]); } /** * Получить статистику из снапшотов */ private function getStatisticsFromSnapshots(Department $department, DateRange $dateRange, int $branchId): array { // Получаем отчеты за период $reports = $this->getReportsForDateRange( $department->department_id, $dateRange ); $reportIds = $reports->pluck('report_id')->toArray(); $lastReport = array_first($reportIds); $recipientReportIds = $this->getSnapshotRecipientReportIds($reportIds); // Получаем статистику из снапшотов $snapshotStats = [ 'plan' => $this->getMetrikaResultCount(4, $reportIds), 'emergency' => $this->getMetrikaResultCount(12, $reportIds), 'outcome' => $this->getMetrikaResultCount(7, $reportIds), 'deceased' => $this->getMetrikaResultCount(9, $reportIds), 'current' => $this->getMetrikaResultCount(8, $reportIds, false), // 'discharged' => $this->getMetrikaResultCount('discharged', $reportIds), 'transferred' => $this->getMetrikaResultCount(13, $reportIds), 'recipient' => $this->getMetrikaResultCount(3, $reportIds), 'beds' => $this->getMetrikaResultCount(1, $reportIds, false), 'countStaff' => $this->getMetrikaResultCount(17, [$lastReport], false), ]; // Получаем ID поступивших пациентов $recipientIds = $this->snapshotService ->getPatientsFromSnapshots('recipient', $recipientReportIds) ->pluck('id') ->all(); // Получаем количество операций из метрик $surgicalCount = [ $this->getMetrikaResultCount(10, $reportIds), // экстренные операции $this->getMetrikaResultCount(11, $reportIds), // плановые операции ]; if ($snapshotStats['outcome'] == 0) { $percentDead = 0; } else { $percentDead = ($snapshotStats['deceased'] / $snapshotStats['outcome']) * 100; $percentDead = round($percentDead, 2); } return [ 'recipientCount' => $snapshotStats['recipient'] ?? 0, 'extractCount' => $snapshotStats['outcome'] ?? 0, 'currentCount' => $snapshotStats['current'] ?? 0, // $this->calculateCurrentPatientsFromSnapshots($reportIds, $branchId), 'deadCount' => $snapshotStats['deceased'] ?? 0, 'countStaff' => $snapshotStats['countStaff'] ?? 0, 'surgicalCount' => $surgicalCount, 'recipientIds' => $recipientIds, 'beds' => $snapshotStats['beds'] ?? 0, 'percentDead' => $percentDead, ]; } /** * Получить статистику из реплики БД */ private function getStatisticsFromReplica(Department $department, User $user, DateRange $dateRange, int $branchId): array { $planCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'plan', $dateRange, $branchId, true); $emergencyCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'emergency', $dateRange, $branchId, true); $currentCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'current', $dateRange, $branchId); $recipientCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'recipient', $dateRange, $branchId); $outcomeCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'outcome', $dateRange, $branchId); $deadCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'outcome-deceased', $dateRange, $branchId); // Операции $misSurgicalCount = [ $this->patientQueryService->getSurgicalPatients( 'emergency', $branchId, $dateRange, true ), $this->patientQueryService->getSurgicalPatients( 'plan', $branchId, $dateRange, true ), ]; $manualSurgicalCount = $this->getManualSurgicalCounts($department, $dateRange); $surgicalCount = [ ($misSurgicalCount[0] ?? 0) + ($manualSurgicalCount[0] ?? 0), ($misSurgicalCount[1] ?? 0) + ($manualSurgicalCount[1] ?? 0), ]; // ID поступивших сегодня (для отметки в таблице) $recipientIds = $this->unifiedPatientService ->getRecipientIdsForReport($department, $user, $dateRange, $branchId); $misBranch = MisStationarBranch::where('StationarBranchID', $branchId)->first(); $beds = Department::where('rf_mis_department_id', $misBranch->rf_DepartmentID) ->first()->metrikaDefault->where('rf_metrika_item_id', 1)->first(); if ($outcomeCount == 0) { $percentDead = 0; } else { $percentDead = ($deadCount / $outcomeCount) * 100; $percentDead = round($percentDead, 2); } return [ 'recipientCount' => $recipientCount, // только поступившие сегодня 'extractCount' => $outcomeCount, 'currentCount' => $currentCount, // все в отделении 'deadCount' => $deadCount, 'surgicalCount' => $surgicalCount, 'recipientIds' => $recipientIds, // ID поступивших сегодня 'planCount' => $planCount, // плановые (поступившие + уже лечащиеся) 'emergencyCount' => $emergencyCount, // экстренные (поступившие + уже лечащиеся) 'percentDead' => $percentDead, 'beds' => $beds->value, ]; } /** * Получить пациентов из снапшотов */ public function getPatientsFromSnapshots( Department $department, string $status, DateRange $dateRange, int $branchId, bool $onlyIds = false ) { return $this->reportPatientsReadService->getPatientsFromSnapshots( $department, $status, $dateRange, $branchId, $onlyIds ); } private function getSnapshotRecipientReportIds(array $reportIds): array { if (empty($reportIds)) { return []; } return [reset($reportIds)]; } /** * Получить нежелательные события за дату */ public function getUnwantedEvents(Department $department, DateRange $dateRange) { return UnwantedEvent::whereHas('report', function ($query) use ($department, $dateRange) { $query->where('rf_department_id', $department->department_id); if ($dateRange->isOneDay) { $query->exactPeriod($dateRange->startSql(), $dateRange->endSql()); } else { $query->withinPeriod($dateRange->startSql(), $dateRange->endSql()); } }) ->get() ->map(function ($item) { return [ ...$item->toArray(), 'created_at' => Carbon::parse($item->created_at)->format('Создано d.m.Y в H:i'), ]; }); } /** * Проверить активность кнопки отправки отчета */ private function isSendButtonActive(User $user, DateRange $dateRange, ?Report $reportToday, ?int $fillableUserId): bool { // Для врача: только сегодня и если отчета еще нет if (! $user->isHeadOfDepartment() && ! $user->isAdmin()) { if ($reportToday && $reportToday->status === 'submitted') { return false; } return $dateRange->isEndDateToday(); } // Для заведующего/админа: можно редактировать любой отчет за сутки (включая submitted) if ( $reportToday && $dateRange->isOneDay ) { return true; } return false; } private function getReportForPeriod(int $departmentId, DateRange $dateRange): ?Report { $query = Report::query() ->where('rf_department_id', $departmentId) ->exactPeriod($dateRange->startSql(), $dateRange->endSql()) ->orderByDesc('report_id'); if ($dateRange->isOneDay) { return $query->first(); } else { return $query->onlySubmitted()->first(); } } /** * Получить информацию о враче */ private function getDoctorInfo(?int $doctorId, DateRange $dateRange): ?MisLpuDoctor { if (! $doctorId) { return null; } // Если дата это период, не показываем врача if (! $dateRange->isOneDay) { return null; } return MisLpuDoctor::where('LPUDoctorID', $doctorId)->first(); } /** * Получить отчеты за диапазон дат */ public function getReportsForDateRange(int $departmentId, DateRange $dateRange) { if ($dateRange->isOneDay) { return Report::where('rf_department_id', $departmentId) ->exactPeriod($dateRange->startSql(), $dateRange->endSql()) ->onlySubmitted() ->orderBy('period_end', 'DESC') ->get(); } return Report::where('rf_department_id', $departmentId) ->withinPeriod($dateRange->startSql(), $dateRange->endSql()) ->onlySubmitted() ->orderBy('period_end', 'DESC') ->get(); } /** * Получить количество из метрик */ private function getMetrikaResultCount(int $metrikaItemId, array $reportIds, bool $sum = true): int { $count = 0; $reports = Report::whereIn('report_id', $reportIds) ->with('metrikaResults') ->orderBy('created_at', 'DESC') ->get(); if (! $sum) { foreach ($reports as $report) { $metric = $report->metrikaResults ->firstWhere('rf_metrika_item_id', $metrikaItemId); if ($metric) { return intval($metric->value) ?? 0; } } return 0; } foreach ($reports as $report) { foreach ($report->metrikaResults as $metrikaResult) { if ($metrikaResult->rf_metrika_item_id === $metrikaItemId) { $count += intval($metrikaResult->value) ?? 0; } } } return $count; } /** * Получить статистику выполнения плана по госпитализации */ public function getRecipientPlanOfYear(Department $department, DateRange $dateRange): array { $periodPlanModel = $department->recipientPlanOfYear(); // Рассчитываем коэффициент периода (округляем в большую сторону) $monthsInPeriod = ceil($dateRange->startDate->diffInMonths($dateRange->endDate)); $annualPlan = $periodPlanModel ? (int) $periodPlanModel->value : 0; $oneMonthPlan = ceil($annualPlan / 12); $periodPlan = round($oneMonthPlan * $monthsInPeriod); $progress = 0; $query = $department->reports() ->with('metrikaResults') ->where('period_start', '>', $dateRange->startSql()) ->where('period_end', '<=', $dateRange->endSql()); if ($dateRange->isOneDay) { $query->where('period_start', '>=', $dateRange->startFirstOfMonth()) ->where('period_end', '<=', $dateRange->endSql()); } else { $query->where('period_start', '>', $dateRange->startSql()) ->where('period_end', '<=', $dateRange->endSql()); } $reports = $query->get(); foreach ($reports as $report) { $outcome = $report->metrikaResults()->where('rf_metrika_item_id', 7)->first(); if ($outcome) { $progress += (int) $outcome->value; } } return [ 'plan' => $periodPlan, 'progress' => $progress, ]; } }