snapshotPatientSource = $snapshotPatientSource ?? app(SnapshotPatientSource::class); $this->autoFillReportPayloadBuilder = $autoFillReportPayloadBuilder ?? app(AutoFillReportPayloadBuilder::class); $this->reportReadContextResolver = $reportReadContextResolver ?? app(ReportReadContextResolver::class); $this->reportRuntimeService = $reportRuntimeService ?? app(ReportRuntimeService::class); $this->metrikaService = $metrikaService ?? app(MetrikaService::class); } protected SnapshotPatientSource $snapshotPatientSource; protected AutoFillReportPayloadBuilder $autoFillReportPayloadBuilder; protected ReportReadContextResolver $reportReadContextResolver; protected ReportRuntimeService $reportRuntimeService; protected MetrikaService $metrikaService; /** * Получить статистику для отчета */ public function getReportStatistics(Department $department, User $user, DateRange $dateRange): array { $branchId = $this->resolveBranchId($department); if (! $branchId) { return $this->emptyStatistics(); } if ($this->shouldUseSnapshots($department, $dateRange)) { return $this->getStatisticsFromSnapshots($department, $dateRange); } return $this->getStatisticsFromReplica($department, $user, $dateRange, $branchId); } /** * Создать или обновить отчет */ public function storeReport(array $data, User $user, $fillableAuto = false): Report { $fillableAuto = (bool) $fillableAuto; $this->prepareForHeavySave(); $dateRange = $this->resolveDateRangeFromPayload($user, $data); $report = DB::transaction(function () use ($data, $user, $dateRange) { $report = $this->createOrUpdateReportModel($data, $user, $dateRange); $this->saveReportMetrics($report, (array) ($data['metrics'] ?? [])); $this->saveReportUnwantedEvents($report, (array) ($data['unwantedEvents'] ?? [])); $this->saveReportObservationPatients($report, (array) ($data['observationPatients'] ?? [])); return $report; }); $this->saveSnapshot($dateRange, $report, $user, $fillableAuto); $this->syncCalculatedMetricsForStoredReport($report, $user, $data); $this->finalizeStoredReport($report); $this->saveLethalMetricForStoredReport($report); $this->clearCacheAfterStoredReport($user, $report); return $report; } public function saveReport( DateRange $dateRange, ?int $userId = null, ?int $lpuDoctorId = null, ?int $departmentId = null, string $status = 'submitted' ): Report { $user = $userId ? User::query()->findOrFail($userId) : auth()->user(); $departmentId = $departmentId ?? $user->rf_department_id; $lpuDoctorId = $lpuDoctorId ?? $user->rf_lpudoctor_id; return Report::query()->updateOrCreate( [ 'rf_department_id' => $departmentId, 'period_start' => $dateRange->startSql(), 'period_end' => $dateRange->endSql(), ], [ 'created_at' => $dateRange->endSql(), 'sent_at' => $dateRange->endSql(), 'rf_user_id' => $user->id, 'rf_lpudoctor_id' => $lpuDoctorId, 'status' => $status, ] ); } public function prepareForHeavySave(): void { $this->reportRuntimeService->prepareForHeavySave(); } public function syncCalculatedMetricsForStoredReport(Report $report, User $user, array $data): void { if (! isset($data['dates'][0], $data['dates'][1])) { return; } $department = Department::query()->where('department_id', $report->rf_department_id)->first(); if (! $department) { return; } $dateRange = $this->dateRangeService->getNormalizedDateRange( $user, (string) $data['dates'][0], (string) $data['dates'][1] ); $branchId = $this->resolveBranchId($department); $planCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['plan']); $emergencyCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['emergency']); $recipientCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['recipient']); $dischargedCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['discharged']); $transferredCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['transferred']); $deceasedCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['deceased']); $currentCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['current']); $outcomeCount = $dischargedCount + $deceasedCount; $manualSurgicalCount = $this->getManualSurgicalCounts($department, $dateRange); $misEmergencySurgery = $branchId ? $this->patientQueryService->getSurgicalPatients('emergency', $branchId, $dateRange, true) : 0; $misPlanSurgery = $branchId ? $this->patientQueryService->getSurgicalPatients('plan', $branchId, $dateRange, true) : 0; $observationCount = ObservationPatient::query() ->where('rf_department_id', $department->department_id) ->where('rf_report_id', $report->report_id) ->count(); $unwantedEventsCount = UnwantedEvent::query() ->where('rf_report_id', $report->report_id) ->count(); $this->saveMetric($report, MetrikaConfig::RECIPIENT, $recipientCount); $this->saveMetric($report, MetrikaConfig::PLAN, $planCount); $this->saveMetric($report, MetrikaConfig::OUTCOME, $outcomeCount); $this->saveMetric($report, MetrikaConfig::CURRENT, $currentCount); $this->saveMetric($report, MetrikaConfig::DECEASED, $deceasedCount); $this->saveMetric($report, MetrikaConfig::EMERGENCY_SURGERY, $misEmergencySurgery + ($manualSurgicalCount[0] ?? 0)); $this->saveMetric($report, MetrikaConfig::PLAN_SURGERY, $misPlanSurgery + ($manualSurgicalCount[1] ?? 0)); $this->saveMetric($report, MetrikaConfig::EMERGENCY, $emergencyCount); $this->saveMetric($report, MetrikaConfig::TRANSFERRED, $transferredCount); $this->saveMetric($report, MetrikaConfig::OBSERVATION, $observationCount); $this->saveMetric($report, MetrikaConfig::DISCHARGED, $dischargedCount); $this->saveMetric($report, MetrikaConfig::UNWANTED_EVENTS, $unwantedEventsCount); } public function finalizeStoredReport(Report $report): void { $this->saveBedDaysMetrics($report); $this->savePreoperativeMetrics($report); $this->saveDepartmentLoadMetric($report); } public function saveLethalMetricForStoredReport(Report $report): void { $snapshots = MedicalHistorySnapshot::query() ->where('rf_report_id', $report->report_id) ->whereIn('patient_type', ['discharged', 'deceased']) ->with('medicalHistory') ->get(); if ($snapshots->isEmpty()) { $this->saveMetric($report, MetrikaConfig::AVERAGE_BED_DAYS, 0); } } public function clearCacheAfterStoredReport(User $user, Report $report): void { $this->reportRuntimeService->clearCacheAfterReportCreation($user, $report); } public function buildAutoFillReportPayload(User $user, Department $department, DateRange $dateRange): array { return $this->autoFillReportPayloadBuilder->build($user, $department, $dateRange); } public function saveSnapshot(DateRange $dateRange, Report $report, User $user, bool $fillableAuto = false): array { $department = Department::query()->where('department_id', $report->rf_department_id)->first() ?? $user->department; $branchId = $department ? $this->resolveBranchId($department) : null; if (! $department || ! $branchId) { return ['saved_snapshots' => 0]; } MedicalHistorySnapshot::query() ->where('rf_report_id', $report->report_id) ->delete(); $snapshotMap = [ 'plan' => ['status' => 'plan', 'includeCurrent' => ! $fillableAuto], 'emergency' => ['status' => 'emergency', 'includeCurrent' => ! $fillableAuto], 'discharged' => ['status' => 'outcome-discharged', 'includeCurrent' => null], 'transferred' => ['status' => 'outcome-transferred', 'includeCurrent' => null], 'deceased' => ['status' => 'outcome-deceased', 'includeCurrent' => null], 'recipient' => ['status' => 'recipient', 'includeCurrent' => null], 'current' => ['status' => 'current', 'includeCurrent' => null], ]; $savedSnapshots = 0; foreach ($snapshotMap as $type => $config) { $patients = $this->unifiedPatientService->getLivePatientsByStatus( $department, $user, $config['status'], $dateRange, $branchId, false, $config['includeCurrent'], $fillableAuto, true ); $savedSnapshots += $this->saveReportSnapshot($report->report_id, $patients, $type); } return [ 'saved_snapshots' => $savedSnapshots, 'report_id' => $report->report_id, 'department_id' => $department->department_id, ]; } public function saveReportSnapshot(int $reportId, iterable $patients, string $type): int { $savedSnapshots = 0; $snapshotBatch = []; $batchSize = 100; foreach ($patients as $patient) { if (! $patient instanceof UnifiedPatientData) { continue; } $snapshotBatch[] = [ 'rf_report_id' => $reportId, ...$patient->toSnapshotPayload($type), ]; if (count($snapshotBatch) >= $batchSize) { $savedSnapshots += $this->upsertSnapshotBatches($snapshotBatch); $snapshotBatch = []; } } if ($snapshotBatch !== []) { $savedSnapshots += $this->upsertSnapshotBatches($snapshotBatch); } return $savedSnapshots; } /** * Получить пациентов по статусу */ public function getPatientsByStatus( Department $department, User $user, string $status, DateRange $dateRange, bool $onlyIds = false, bool $beforeCreate = false, ?bool $includeCurrentPatients = null ) { [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); $branchId = $this->resolveBranchId($department); if (! $branchId) { return collect(); } if ($sourceScope === 'special' || $baseStatus === 'reanimation') { return $this->getPatientsFromReplica( $department, $user, $status, $dateRange, $branchId, $onlyIds, $includeCurrentPatients ); } $useSnapshots = ! $this->shouldUseReplicaForLiveStatus($user, $baseStatus, $dateRange) && $this->shouldUseSnapshots($department, $dateRange, $beforeCreate); if ($useSnapshots) { return $this->getPatientsFromSnapshots( $department, $status, $dateRange, $branchId, $onlyIds ); } return $this->getPatientsFromReplica( $department, $user, $status, $dateRange, $branchId, $onlyIds, $includeCurrentPatients ); } /** * Получить количество пациентов по статусу */ public function getPatientsCountByStatus( Department $department, User $user, string $status, DateRange $dateRange ): int { [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); $branchId = $this->resolveBranchId($department); if (! $branchId) { return 0; } if ($sourceScope === 'special' || $baseStatus === 'reanimation') { return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId); } $useSnapshots = ! $this->shouldUseReplicaForLiveStatus($user, $baseStatus, $dateRange) && $this->shouldUseSnapshots($department, $dateRange); if ($useSnapshots) { return $this->getPatientsCountFromSnapshots($department, $status, $dateRange, $branchId); } return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId); } public function getPatientsCountsMap(Department $department, User $user, DateRange $dateRange): array { $baseStatuses = [ 'plan', 'emergency', 'observation', 'reanimation', 'outcome-discharged', 'outcome-deceased', 'outcome-transferred', ]; $counts = [ 'mis-plan' => 0, 'mis-emergency' => 0, 'mis-observation' => 0, 'mis-reanimation' => 0, 'mis-outcome' => 0, 'mis-outcome-discharged' => 0, 'mis-outcome-deceased' => 0, 'mis-outcome-transferred' => 0, 'special-plan' => 0, 'special-emergency' => 0, 'special-observation' => 0, 'special-reanimation' => 0, 'special-outcome' => 0, 'special-outcome-discharged' => 0, 'special-outcome-deceased' => 0, 'special-outcome-transferred' => 0, ]; foreach ($baseStatuses as $baseStatus) { $counts["mis-{$baseStatus}"] = $this->getPatientsCountByStatus( $department, $user, "mis-{$baseStatus}", $dateRange ); $counts["special-{$baseStatus}"] = $this->getPatientsCountByStatus( $department, $user, "special-{$baseStatus}", $dateRange ); } $counts['mis-outcome'] = ($counts['mis-outcome-discharged'] ?? 0) + ($counts['mis-outcome-deceased'] ?? 0); $counts['special-outcome'] = ($counts['special-outcome-discharged'] ?? 0) + ($counts['special-outcome-deceased'] ?? 0); return $counts; } /** * Получить информацию о текущем отчете */ public function getCurrentReportInfo(Department $department, User $user, DateRange $dateRange): array { $reportToday = $this->resolveReport($department->department_id, $dateRange); $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); $useSnapshots = $isHeadOrAdmin || ! $dateRange->isEndDateToday() || $reportToday; 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); $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); $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::query() ->where('rf_department_patient_id', $id) ->delete(); return; } ObservationPatient::query() ->where('rf_medicalhistory_id', $id) ->delete(); } public function createManualPatient(Department $department, User $user, array $data) { $report = $this->resolveReportForManualPatient($department, $user, $data); return DepartmentPatient::create([ 'rf_department_id' => $department->department_id, 'rf_report_id' => $report->report_id, 'source_type' => 'special', 'full_name' => $data['full_name'], 'birth_date' => $data['birth_date'], 'patient_kind' => $data['patient_kind'], 'diagnosis_code' => $data['diagnosis_code'] ?? null, 'diagnosis_name' => $data['diagnosis_name'] ?? null, 'admitted_at' => $data['admitted_at'] ?? now(), 'is_current' => true, 'created_by' => $user->id, ]); } public function setManualPatientOutcome(User $user, int $departmentPatientId, array $data) { $patient = DepartmentPatient::query() ->where('department_patient_id', $departmentPatientId) ->firstOrFail(); $patient->update([ 'is_current' => false, 'outcome_type' => $data['outcome_type'], 'outcome_at' => $data['outcome_at'] ?? now(), ]); $updatedPatient = $patient->fresh(); $this->syncManualPatientSnapshots($updatedPatient, $user, []); return $updatedPatient; } public function updateManualPatient(User $user, int $departmentPatientId, array $data) { $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); $manualStatus = $data['manual_status'] ?? null; $isCurrent = $manualStatus === 'current' || $manualStatus === null; $outcomeType = match ($manualStatus) { 'discharged', 'deceased', 'transferred' => $manualStatus, default => null, }; $patient->update([ 'full_name' => $data['full_name'], 'birth_date' => $data['birth_date'], 'patient_kind' => $data['patient_kind'], 'diagnosis_code' => $data['diagnosis_code'] ?? null, 'diagnosis_name' => $data['diagnosis_name'] ?? null, 'admitted_at' => $data['admitted_at'] ?? $patient->admitted_at, 'is_current' => $isCurrent, 'outcome_type' => $outcomeType, 'outcome_at' => $isCurrent ? null : ($data['outcome_at'] ?? now()), ]); $updatedPatient = $patient->fresh(); $this->syncManualPatientSnapshots($updatedPatient, $user, $data); return $updatedPatient; } public function linkManualPatientToMis(int $departmentPatientId, int $medicalHistoryId) { $patient = DepartmentPatient::query() ->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 = $this->resolveMedicalService((int) $data['service_id']); 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 = $this->resolveMedicalService((int) $data['service_id']); $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::query()->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); } /** * Получить пациентов из снапшотов */ public function getPatientsFromSnapshots( Department $department, string $status, DateRange $dateRange, int $branchId, bool $onlyIds = false ) { [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); $reportIds = $this->getReportsForDateRange($department->department_id, $dateRange) ->pluck('report_id') ->all(); $recipientReportIds = $this->getRecipientReportIds($reportIds); $patientTypeMap = [ 'plan' => 'plan', 'emergency' => 'emergency', 'current' => 'current', 'recipient' => 'recipient', 'outcome-discharged' => 'discharged', 'outcome-transferred' => 'transferred', 'outcome-deceased' => 'deceased', 'observation' => 'observation', ]; $patientType = $patientTypeMap[$baseStatus] ?? null; if ($patientType === 'observation') { return $this->unifiedPatientService->getObservationPatients($department, $onlyIds, $sourceScope); } if ($baseStatus === 'outcome') { $discharged = $this->loadSnapshotPatients( 'discharged', $reportIds, false, false, $recipientReportIds ); $deceased = $this->loadSnapshotPatients( 'deceased', $reportIds, false, false, $recipientReportIds ); $merged = \App\Data\UnifiedPatientData::unique($discharged->concat($deceased)) ->sortByDesc(fn (\App\Data\UnifiedPatientData $patient) => $patient->admittedAt ?? '') ->values(); return $this->filterSnapshotPatientsByScope($merged, $sourceScope, $onlyIds); } if (! $patientType) { return collect(); } if ($dateRange->isOneDay && in_array($baseStatus, ['plan', 'emergency'], true)) { $patients = $this->loadOneDayCurrentSnapshotPatients( $patientType, $reportIds, false, $recipientReportIds ); return $this->filterSnapshotPatientsByScope($patients, $sourceScope, $onlyIds); } $patients = $this->loadSnapshotPatients( $patientType, $reportIds, false, in_array($baseStatus, ['plan', 'emergency'], true), $recipientReportIds ); return $this->filterSnapshotPatientsByScope($patients, $sourceScope, $onlyIds); } /** * Получить нежелательные события за дату */ public function getUnwantedEvents(Department $department, DateRange $dateRange) { return UnwantedEvent::query() ->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 (UnwantedEvent $item) { return [ ...$item->toArray(), 'created_at' => Carbon::parse($item->created_at)->format('Создано d.m.Y в H:i'), ]; }); } /** * Получить отчеты за диапазон дат */ public function getReportsForDateRange(int $departmentId, DateRange $dateRange) { return $this->reportReadContextResolver->getReportsForDateRange($departmentId, $dateRange); } /** * Получить статистику выполнения плана по госпитализации */ 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); $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()); } $progress = 0; foreach ($query->get() as $report) { $outcome = $report->metrikaResults() ->where('rf_metrika_item_id', MetrikaConfig::OUTCOME) ->first(); if ($outcome) { $progress += (int) $outcome->value; } } return [ 'plan' => $periodPlan, 'progress' => $progress, ]; } public function getReportInfo(User $user, Department $department, DateRange $dateRange) { $report = $this->resolveReport($department->department_id, $dateRange); $metrics = $this->metrikaService->getMetricsForReport($report); } private function resolveReport(int $departmentId, DateRange $dateRange) { return $this->reportReadContextResolver->resolveReportForPeriod($departmentId, $dateRange); } private function resolveBranchId(Department $department): ?int { return $this->reportReadContextResolver->resolveBranchId($department); } private function shouldUseSnapshots(Department $department, DateRange $dateRange, bool $beforeCreate = false): bool { return $this->reportReadContextResolver->shouldUseSnapshots($department, $dateRange, $beforeCreate); } private function shouldUseReplicaForLiveStatus(User $user, string $status, DateRange $dateRange): bool { return $this->reportReadContextResolver->shouldUseReplicaForLiveStatus($user, $status, $dateRange); } private function getRecipientReportIds(array $reportIds): array { return $this->reportReadContextResolver->getRecipientReportIds($reportIds); } private function getPatientsFromReplica( Department $department, User $user, string $status, DateRange $dateRange, int $branchId, bool $onlyIds = false, ?bool $includeCurrent = null ) { [$baseStatus] = $this->parseScopedStatus($status); $includeCurrent ??= in_array($baseStatus, ['plan', 'emergency', 'reanimation'], true); return $this->unifiedPatientService->getLivePatientsByStatus( $department, $user, $status, $dateRange, $branchId, $onlyIds, $includeCurrent ); } private function getPatientsCountFromSnapshots( Department $department, string $status, DateRange $dateRange, int $branchId ): int { [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); $reportIds = $this->getReportsForDateRange($department->department_id, $dateRange) ->pluck('report_id') ->all(); if ($baseStatus === 'outcome') { if ($sourceScope !== 'all') { return $this->getPatientsFromSnapshots($department, $status, $dateRange, $branchId)->count(); } return MedicalHistorySnapshot::query() ->whereIn('rf_report_id', $reportIds) ->whereIn('patient_type', ['discharged', 'deceased']) ->distinct('rf_medicalhistory_id') ->count('rf_medicalhistory_id'); } $patientTypeMap = [ 'plan' => 'plan', 'emergency' => 'emergency', 'observation' => 'observation', 'outcome-discharged' => 'discharged', 'outcome-transferred' => 'transferred', 'outcome-deceased' => 'deceased', ]; $patientType = $patientTypeMap[$baseStatus] ?? null; if (! $patientType) { return 0; } if ($patientType === 'observation') { return $this->unifiedPatientService->getObservationPatients($department, false, $sourceScope)->count(); } if ($sourceScope !== 'all') { return $this->getPatientsFromSnapshots($department, $status, $dateRange, $branchId)->count(); } return MedicalHistorySnapshot::query() ->whereIn('rf_report_id', $reportIds) ->where('patient_type', $patientType) ->distinct('rf_medicalhistory_id') ->count('rf_medicalhistory_id'); } private function getPatientsCountFromReplica( Department $department, User $user, string $status, DateRange $dateRange, int $branchId ): int { [$baseStatus] = $this->parseScopedStatus($status); return match ($status) { 'plan', 'emergency', 'observation', 'reanimation', 'outcome', 'outcome-discharged', 'outcome-transferred', 'outcome-deceased', 'current', 'recipient' => $this->unifiedPatientService->getLivePatientCountByStatus( $department, $user, $status, $dateRange, $branchId, in_array($status, ['plan', 'emergency'], true) ), default => $this->unifiedPatientService->getLivePatientCountByStatus( $department, $user, $status, $dateRange, $branchId, in_array($baseStatus, ['plan', 'emergency'], true) ), }; } private function filterSnapshotPatientsByScope(Collection $patients, string $sourceScope, bool $onlyIds = false) { if ($sourceScope === 'all') { return $onlyIds ? $patients->pluck('id') : $patients; } $filtered = $patients->filter(function ($patient) use ($sourceScope) { return match ($sourceScope) { 'mis' => $patient->sourceType === 'mis', 'special' => in_array($patient->sourceType, ['manual', 'special'], true), default => true, }; })->values(); return $onlyIds ? $filtered->pluck('id') : $filtered; } private function parseScopedStatus(string $status): array { foreach (['mis', 'special'] as $scope) { $prefix = "{$scope}-"; if (str_starts_with($status, $prefix)) { return [substr($status, strlen($prefix)), $scope]; } } return [$status, 'all']; } private function getStatisticsFromSnapshots(Department $department, DateRange $dateRange): array { $reports = $this->getReportsForDateRange($department->department_id, $dateRange); $reportIds = $reports->pluck('report_id')->all(); $lastReportId = $reportIds[0] ?? null; $recipientReportIds = $this->getRecipientReportIds($reportIds); $snapshotStats = [ 'plan' => $this->getMetrikaResultCount(MetrikaConfig::PLAN, $reportIds), 'emergency' => $this->getMetrikaResultCount(MetrikaConfig::EMERGENCY, $reportIds), 'outcome' => $this->getMetrikaResultCount(MetrikaConfig::OUTCOME, $reportIds), 'deceased' => $this->getMetrikaResultCount(MetrikaConfig::DECEASED, $reportIds), 'current' => $this->getMetrikaResultCount(MetrikaConfig::CURRENT, $reportIds, false), 'transferred' => $this->getMetrikaResultCount(MetrikaConfig::TRANSFERRED, $reportIds), 'recipient' => $this->getMetrikaResultCount(MetrikaConfig::RECIPIENT, $reportIds), 'beds' => $this->getMetrikaResultCount(MetrikaConfig::BEDS, $reportIds, false), 'countStaff' => $lastReportId ? $this->getMetrikaResultCount(MetrikaConfig::STAFF_COUNT, [$lastReportId], false) : 0, ]; $recipientIds = $this->loadSnapshotPatients('recipient', $recipientReportIds) ->pluck('id') ->all(); $surgicalCount = [ $this->getMetrikaResultCount(MetrikaConfig::EMERGENCY_SURGERY, $reportIds), $this->getMetrikaResultCount(MetrikaConfig::PLAN_SURGERY, $reportIds), ]; return [ 'recipientCount' => $snapshotStats['recipient'] ?? 0, 'extractCount' => $snapshotStats['outcome'] ?? 0, 'currentCount' => $snapshotStats['current'] ?? 0, 'deadCount' => $snapshotStats['deceased'] ?? 0, 'countStaff' => $snapshotStats['countStaff'] ?? 0, 'surgicalCount' => $surgicalCount, 'recipientIds' => $recipientIds, 'beds' => $snapshotStats['beds'] ?? 0, 'percentDead' => $this->calculatePercentDead($snapshotStats['deceased'], $snapshotStats['outcome']), ]; } private function loadSnapshotPatients( string $type, array $reportIds, bool $onlyIds = false, bool $markRecipients = false, ?array $recipientReportIds = null ): Collection { if (get_class($this->snapshotService) !== SnapshotService::class) { return $this->snapshotService->getPatientsFromSnapshots( $type, $reportIds, null, $onlyIds, $markRecipients, $recipientReportIds ); } return $this->snapshotPatientSource->getPatientsFromSnapshots( $type, $reportIds, $onlyIds, $markRecipients, $recipientReportIds ); } private function loadOneDayCurrentSnapshotPatients( string $type, array $reportIds, bool $onlyIds = false, ?array $recipientReportIds = null ): Collection { if (get_class($this->snapshotService) !== SnapshotService::class) { return $this->snapshotService->getPatientsFromOneDayCurrentSnapshots( $type, $reportIds, $onlyIds, $recipientReportIds ); } return $this->snapshotPatientSource->getPatientsFromOneDayCurrentSnapshots( $type, $reportIds, $onlyIds, $recipientReportIds ); } 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), ]; $recipientIds = $this->unifiedPatientService ->getRecipientIdsForReport($department, $user, $dateRange, $branchId); return [ 'recipientCount' => $recipientCount, 'extractCount' => $outcomeCount, 'currentCount' => $currentCount, 'deadCount' => $deadCount, 'surgicalCount' => $surgicalCount, 'recipientIds' => $recipientIds, 'planCount' => $planCount, 'emergencyCount' => $emergencyCount, 'percentDead' => $this->calculatePercentDead($deadCount, $outcomeCount), 'beds' => (int) ($department->metrikaDefault ->where('rf_metrika_item_id', MetrikaConfig::BEDS) ->first() ?->value ?? 0), ]; } private function getMetrikaResultCount(int $metrikaItemId, array $reportIds, bool $sum = true): int { if (empty($reportIds)) { return 0; } $reports = Report::query() ->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 (int) $metric->value; } } return 0; } $count = 0; foreach ($reports as $report) { foreach ($report->metrikaResults as $metrikaResult) { if ((int) $metrikaResult->rf_metrika_item_id === $metrikaItemId) { $count += (int) $metrikaResult->value; } } } return $count; } private function calculatePercentDead(int $deadCount, int $outcomeCount): float|int { if ($outcomeCount === 0) { return 0; } return round(($deadCount / $outcomeCount) * 100, 2); } private function emptyStatistics(): array { return [ 'recipientCount' => 0, 'extractCount' => 0, 'currentCount' => 0, 'deadCount' => 0, 'surgicalCount' => [0, 0], 'recipientIds' => [], 'planCount' => 0, 'emergencyCount' => 0, 'percentDead' => 0, 'beds' => 0, 'countStaff' => 0, ]; } private function isSendButtonActive(User $user, DateRange $dateRange, ?Report $reportToday): bool { if (! $user->isHeadOfDepartment() && ! $user->isAdmin()) { if ($reportToday && $reportToday->status === 'submitted') { return false; } return $dateRange->isEndDateToday(); } return (bool) $reportToday && $dateRange->isOneDay; } private function getDoctorInfo(?int $doctorId, DateRange $dateRange): ?MisLpuDoctor { if (! $doctorId || ! $dateRange->isOneDay) { return null; } return MisLpuDoctor::query() ->where('LPUDoctorID', $doctorId) ->first(); } private function upsertSnapshotBatches(array $snapshotBatch): int { if ($snapshotBatch === []) { return 0; } $uniqueBy = ['rf_report_id', 'patient_uid', 'patient_type']; $updateColumns = array_values(array_diff(array_keys($snapshotBatch[0]), $uniqueBy)); DB::table('medical_history_snapshots')->upsert( $snapshotBatch, $uniqueBy, $updateColumns ); return count($snapshotBatch); } private function resolveDateRangeFromPayload(User $user, array $data): DateRange { return $this->dateRangeService->getNormalizedDateRange( $user, (string) ($data['dates'][0] ?? null), (string) ($data['dates'][1] ?? null) ); } private function createOrUpdateReportModel(array $data, User $user, DateRange $dateRange): Report { $reportData = [ 'rf_department_id' => (int) $data['departmentId'], 'rf_user_id' => $user->id, 'rf_lpudoctor_id' => (int) $data['userId'], 'sent_at' => $data['sent_at'] ?? $dateRange->endSql(), 'period_start' => $dateRange->startSql(), 'period_end' => $dateRange->endSql(), 'created_at' => $data['created_at'] ?? $dateRange->endSql(), 'status' => (string) ($data['status'] ?? 'draft'), ]; if (! empty($data['reportId'])) { return Report::query()->updateOrCreate( ['report_id' => (int) $data['reportId']], $reportData ); } $report = Report::query()->create($reportData); $department = Department::query()->find((int) $data['departmentId']); $beds = $department?->metrikaDefault->where('rf_metrika_item_id', MetrikaConfig::BEDS)->first(); if ($beds) { $this->saveMetric($report, MetrikaConfig::BEDS, $beds->value); } return $report; } private function saveReportMetrics(Report $report, array $metrics): void { foreach (MetrikaConfig::normalizeMetrics($metrics) as $metricId => $value) { $this->saveMetric($report, $metricId, $value); } } private function saveMetric(Report $report, int $metricId, int|float|string|null $value): void { MetrikaResult::query()->updateOrCreate( [ 'rf_report_id' => $report->report_id, 'rf_metrika_item_id' => $metricId, ], ['value' => $value] ); } private function saveReportUnwantedEvents(Report $report, array $events): void { if ($events === []) { $report->unwantedEvents()->delete(); $this->saveMetric($report, MetrikaConfig::UNWANTED_EVENTS, 0); return; } $report->unwantedEvents() ->whereNotIn('unwanted_event_id', array_values(array_filter(array_map( static fn (array $event): ?int => isset($event['unwanted_event_id']) ? (int) $event['unwanted_event_id'] : null, $events )))) ->delete(); foreach ($events as $event) { $payload = [ 'rf_report_id' => $report->report_id, 'comment' => (string) ($event['comment'] ?? ''), 'title' => (string) ($event['title'] ?? ''), 'is_visible' => (bool) ($event['is_visible'] ?? true), ]; if (! empty($event['unwanted_event_id'])) { UnwantedEvent::query()->updateOrCreate( ['unwanted_event_id' => (int) $event['unwanted_event_id']], $payload ); continue; } UnwantedEvent::query()->create($payload); } $this->saveMetric($report, MetrikaConfig::UNWANTED_EVENTS, count($events)); } private function saveReportObservationPatients(Report $report, array $patients): void { if ($patients === []) { ObservationPatient::query() ->where('rf_department_id', $report->rf_department_id) ->where('rf_report_id', $report->report_id) ->delete(); $this->saveMetric($report, MetrikaConfig::OBSERVATION, 0); return; } $observedKeys = []; foreach ($patients as $patient) { $medicalHistoryId = isset($patient['medical_history_id']) ? (int) $patient['medical_history_id'] : null; $departmentPatientId = isset($patient['department_patient_id']) ? (int) $patient['department_patient_id'] : null; $observedKeys[] = $medicalHistoryId.'-'.$departmentPatientId; ObservationPatient::query()->updateOrCreate( [ 'rf_medicalhistory_id' => $medicalHistoryId, 'rf_department_patient_id' => $departmentPatientId, 'rf_department_id' => $report->rf_department_id, ], [ 'rf_report_id' => $report->report_id, 'rf_mkab_id' => null, 'comment' => $patient['comment'] ?? null, ] ); } ObservationPatient::query() ->where('rf_department_id', $report->rf_department_id) ->where('rf_report_id', $report->report_id) ->get() ->filter(fn (ObservationPatient $patient) => ! in_array( ($patient->rf_medicalhistory_id ?? '').'-'.($patient->rf_department_patient_id ?? ''), $observedKeys, true )) ->each ->delete(); $this->saveMetric($report, MetrikaConfig::OBSERVATION, count($patients)); } private function countUniqueSnapshotsForTypes(int $reportId, array $patientTypes): int { return MedicalHistorySnapshot::query() ->where('rf_report_id', $reportId) ->whereIn('patient_type', $patientTypes) ->get(['medical_history_snapshot_id', 'patient_uid', 'rf_medicalhistory_id']) ->map(function (MedicalHistorySnapshot $snapshot) { return $snapshot->patient_uid ?: ($snapshot->rf_medicalhistory_id ? "mis:{$snapshot->rf_medicalhistory_id}" : "snapshot:{$snapshot->medical_history_snapshot_id}"); }) ->unique() ->count(); } private function getManualSurgicalCounts(Department $department, DateRange $dateRange): array { $baseQuery = DepartmentPatientOperation::query() ->whereBetween('started_at', [$dateRange->startSql(), $dateRange->endSql()]) ->whereHas('patient', function ($query) use ($department) { $query->where('rf_department_id', $department->department_id) ->whereIn('source_type', ['manual', 'special']); }); $emergencyCount = (clone $baseQuery) ->where(function ($query) { $query->where('urgency', 'emergency') ->orWhere(function ($fallback) { $fallback->whereNull('urgency') ->whereHas('patient', fn ($patientQuery) => $patientQuery->where('patient_kind', 'emergency')); }); }) ->count(); $planCount = (clone $baseQuery) ->where(function ($query) { $query->where('urgency', 'plan') ->orWhere(function ($fallback) { $fallback->whereNull('urgency') ->whereHas('patient', fn ($patientQuery) => $patientQuery->where('patient_kind', 'plan')); }); }) ->count(); return [$emergencyCount, $planCount]; } private function saveBedDaysMetrics(Report $report): void { $snapshots = MedicalHistorySnapshot::query() ->where('rf_report_id', $report->report_id) ->whereIn('patient_type', ['discharged', 'deceased']) ->with('medicalHistory') ->get(); $intervalDays = []; foreach ($snapshots as $snapshot) { $history = $snapshot->medicalHistory; if (! $history) { continue; } $startRaw = $history->recipient_date ?? null; $endRaw = null; if ($snapshot->patient_type === 'deceased') { if ($this->isRealDate($history->death_date ?? null)) { $endRaw = $history->death_date; } elseif ($this->isRealDate($history->extract_date ?? null)) { $endRaw = $history->extract_date; } } elseif ($this->isRealDate($history->extract_date ?? null)) { $endRaw = $history->extract_date; } if (! $startRaw || ! $endRaw) { continue; } $intervalDays[] = Carbon::parse($startRaw)->diffInDays(Carbon::parse($endRaw)); } $total = array_sum($intervalDays); $average = count($intervalDays) > 0 ? round($total / count($intervalDays), 1) : 0; $this->saveMetric($report, MetrikaConfig::TOTAL_BED_DAYS, $total); $this->saveMetric($report, MetrikaConfig::AVERAGE_BED_DAYS, $average); } private function savePreoperativeMetrics(Report $report): void { $historyIds = MedicalHistorySnapshot::query() ->where('rf_report_id', $report->report_id) ->whereIn('patient_type', ['discharged', 'deceased']) ->pluck('rf_medicalhistory_id') ->unique() ->values(); if ($historyIds->isEmpty()) { $this->saveMetric($report, MetrikaConfig::TOTAL_PREOPERATIVE_DAYS, 0); $this->saveMetric($report, MetrikaConfig::PREOPERATIVE_PATIENT_COUNT, 0); $this->saveMetric($report, MetrikaConfig::PREOPERATIVE_AVERAGE_DAYS, 0); return; } $rows = \App\Models\MedicalHistory::query() ->whereIn('original_id', $historyIds) ->with(['operations']) ->get(); $days = []; foreach ($rows as $row) { $startRaw = $row->recipient_date ?? null; $operationRaw = $row->operations ->pluck('operation_date') ->filter() ->sort() ->first(); if (! $startRaw || ! $operationRaw) { continue; } $days[] = Carbon::parse($startRaw)->diffInDays(Carbon::parse($operationRaw)); } $total = array_sum($days); $count = count($days); $average = $count > 0 ? round($total / $count, 1) : 0; $this->saveMetric($report, MetrikaConfig::TOTAL_PREOPERATIVE_DAYS, $total); $this->saveMetric($report, MetrikaConfig::PREOPERATIVE_PATIENT_COUNT, $count); $this->saveMetric($report, MetrikaConfig::PREOPERATIVE_AVERAGE_DAYS, $average); } private function saveDepartmentLoadMetric(Report $report): void { $currentCount = (float) ($report->metrikaResults()->where('rf_metrika_item_id', MetrikaConfig::CURRENT)->value('value') ?? 0); $bedsCount = (float) ($report->metrikaResults()->where('rf_metrika_item_id', MetrikaConfig::BEDS)->value('value') ?? 0); $value = $bedsCount > 0 ? round(($currentCount / $bedsCount) * 100, 2) : 0; $this->saveMetric($report, MetrikaConfig::DEPARTMENT_LOADED, $value); } private function isRealDate(mixed $value): bool { if (! $value) { return false; } $date = Carbon::parse($value)->format('Y-m-d'); return ! in_array($date, ['1900-01-01', '2222-01-01'], true); } 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 resolveMedicalService(int $serviceId): MisServiceMedical { return MisServiceMedical::query() ->where('ServiceMedicalID', $serviceId) ->firstOrFail(); } }