Files
onboard/app/Services/ReportService.php

1317 lines
49 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services;
use App\Domain\Reports\ValueObjects\MetrikaConfig;
use App\Infrastructure\Reports\Services\AutoFillReportPayloadBuilder;
use App\Infrastructure\Reports\Services\CalculatedMetricsSynchronizer;
use App\Infrastructure\Reports\Services\ReportPatientsReadService;
use App\Infrastructure\Reports\Services\ReportMetricsFinalizer;
use App\Models\Department;
use App\Models\DepartmentPatient;
use App\Models\DepartmentPatientOperation;
use App\Models\MedicalHistorySnapshot;
use App\Models\MetrikaResult;
use App\Models\MisLpuDoctor;
use App\Models\MisMedicalHistory;
use App\Models\MisMigrationPatient;
use App\Models\MisServiceMedical;
use App\Models\MisStationarBranch;
use App\Models\ObservationPatient;
use App\Models\Report;
use App\Models\ReanimationPatientIndicator;
use App\Models\UnwantedEvent;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class ReportService
{
public function __construct(
protected DateRangeService $dateRangeService,
protected UnifiedPatientService $unifiedPatientService,
protected PatientService $patientQueryService,
protected SnapshotService $snapshotService,
protected StatisticsService $statisticsService,
?ReportMetricsFinalizer $reportMetricsFinalizer = null,
?CalculatedMetricsSynchronizer $calculatedMetricsSynchronizer = null,
?AutoFillReportPayloadBuilder $autoFillReportPayloadBuilder = null,
?ReportPatientsReadService $reportPatientsReadService = null,
) {
$this->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,
];
}
}