diff --git a/app/Application/Reports/CompareLegacyAndNewReportUseCase.php b/app/Application/Reports/CompareLegacyAndNewReportUseCase.php deleted file mode 100644 index 419b25a..0000000 --- a/app/Application/Reports/CompareLegacyAndNewReportUseCase.php +++ /dev/null @@ -1,72 +0,0 @@ -legacyAdapter->buildSnapshotFromInput($input); - - if ($input->persistedReportId === null) { - throw new RuntimeException('persistedReportId is required for report comparison.'); - } - - $actual = $this->reportRepository->findSnapshot($input->persistedReportId); - - if ($actual === null) { - throw new RuntimeException('Saved report snapshot was not found for comparison.'); - } - - $diff = $this->buildDiff($expected->toComparableArray(), $actual->toComparableArray()); - $status = empty($diff) ? 'matched' : 'diff'; - - return new ReportComparisonResult( - reportType: $input->reportType, - path: 'new', - status: $status, - diff: $diff, - departmentId: $input->departmentId, - userId: $input->userId, - periodStart: $input->periodStart->format('Y-m-d H:i:s'), - periodEnd: $input->periodEnd->format('Y-m-d H:i:s'), - reportId: $input->persistedReportId, - durationMs: round((microtime(true) - $startedAt) * 1000, 2), - ); - } - - /** - * @param array $expected - * @param array $actual - * @return array - */ - private function buildDiff(array $expected, array $actual): array - { - $diff = []; - - foreach ($expected as $key => $value) { - $actualValue = $actual[$key] ?? null; - - if ($actualValue !== $value) { - $diff[$key] = [ - 'expected' => $value, - 'actual' => $actualValue, - ]; - } - } - - return $diff; - } -} diff --git a/app/Application/Reports/DTO/GenerateReportInput.php b/app/Application/Reports/DTO/GenerateReportInput.php deleted file mode 100644 index 2ae9775..0000000 --- a/app/Application/Reports/DTO/GenerateReportInput.php +++ /dev/null @@ -1,117 +0,0 @@ - $metrics - * @param array> $observationPatients - * @param array> $unwantedEvents - * @param array|null $rawPayload - */ - public function __construct( - public int $departmentId, - public int $userId, - public ?int $actorUserId, - public DateTimeImmutable $periodStart, - public DateTimeImmutable $periodEnd, - public array $metrics = [], - public array $observationPatients = [], - public array $unwantedEvents = [], - public ?int $reportId = null, - public string $status = 'draft', - public string $reportType = 'daily', - public bool $autoFill = false, - public ?array $rawPayload = null, - public ?int $persistedReportId = null, - public ?DateTimeImmutable $createdAt = null, - public ?DateTimeImmutable $sentAt = null, - ) {} - - public function toContext(): ReportContext - { - return new ReportContext( - departmentId: $this->departmentId, - userId: $this->userId, - actorUserId: $this->actorUserId, - periodStart: $this->periodStart, - periodEnd: $this->periodEnd, - reportType: $this->reportType, - metadata: [ - 'auto_fill' => $this->autoFill, - ], - ); - } - - public function toSnapshot(): ReportSnapshot - { - return new ReportSnapshot( - departmentId: $this->departmentId, - userId: $this->userId, - actorUserId: $this->actorUserId, - periodStart: $this->periodStart, - periodEnd: $this->periodEnd, - status: $this->status, - autoFill: $this->autoFill, - metrics: $this->metrics, - observationPatients: $this->observationPatients, - unwantedEvents: $this->unwantedEvents, - reportId: $this->reportId, - createdAt: $this->createdAt, - sentAt: $this->sentAt, - reportType: $this->reportType, - ); - } - - /** - * @param array $payload - */ - public function withRawPayload(array $payload): self - { - return new self( - departmentId: $this->departmentId, - userId: $this->userId, - actorUserId: $this->actorUserId, - periodStart: $this->periodStart, - periodEnd: $this->periodEnd, - metrics: $this->metrics, - observationPatients: $this->observationPatients, - unwantedEvents: $this->unwantedEvents, - reportId: $this->reportId, - status: $this->status, - reportType: $this->reportType, - autoFill: $this->autoFill, - rawPayload: $payload, - persistedReportId: $this->persistedReportId, - createdAt: $this->createdAt, - sentAt: $this->sentAt, - ); - } - - public function withPersistedReportId(int $reportId): self - { - return new self( - departmentId: $this->departmentId, - userId: $this->userId, - actorUserId: $this->actorUserId, - periodStart: $this->periodStart, - periodEnd: $this->periodEnd, - metrics: $this->metrics, - observationPatients: $this->observationPatients, - unwantedEvents: $this->unwantedEvents, - reportId: $this->reportId, - status: $this->status, - reportType: $this->reportType, - autoFill: $this->autoFill, - rawPayload: $this->rawPayload, - persistedReportId: $reportId, - createdAt: $this->createdAt, - sentAt: $this->sentAt, - ); - } -} diff --git a/app/Application/Reports/DTO/GenerateReportResult.php b/app/Application/Reports/DTO/GenerateReportResult.php deleted file mode 100644 index 3859be5..0000000 --- a/app/Application/Reports/DTO/GenerateReportResult.php +++ /dev/null @@ -1,13 +0,0 @@ - $diff - */ - public function __construct( - public string $reportType, - public string $path, - public string $status, - public array $diff, - public int $departmentId, - public int $userId, - public string $periodStart, - public string $periodEnd, - public ?int $reportId, - public float $durationMs, - ) {} -} diff --git a/app/Application/Reports/GenerateReportUseCase.php b/app/Application/Reports/GenerateReportUseCase.php deleted file mode 100644 index 2b2c1b9..0000000 --- a/app/Application/Reports/GenerateReportUseCase.php +++ /dev/null @@ -1,143 +0,0 @@ - $calculators - */ - public function __construct( - private ReportRepository $reportRepository, - private AuditLogger $auditLogger, - private CompareLegacyAndNewReportUseCase $comparator, - private ?PatientSource $patientSource = null, - private iterable $calculators = [], - private bool $compareBeforeCutover = true, - ) {} - - public function handle(GenerateReportInput $input): GenerateReportResult - { - $resolvedInput = $this->resolveInput($input); - $saved = $this->reportRepository->save($resolvedInput->toSnapshot()); - - $comparison = null; - - if ($this->compareBeforeCutover) { - try { - $comparison = $this->comparator->handle( - $resolvedInput->withPersistedReportId($saved->reportId) - ); - } catch (\Throwable $exception) { - $comparison = new ReportComparisonResult( - reportType: $resolvedInput->reportType, - path: 'new', - status: 'failed', - diff: ['error' => $exception->getMessage()], - departmentId: $resolvedInput->departmentId, - userId: $resolvedInput->userId, - periodStart: $resolvedInput->periodStart->format('Y-m-d H:i:s'), - periodEnd: $resolvedInput->periodEnd->format('Y-m-d H:i:s'), - reportId: $saved->reportId, - durationMs: 0.0, - ); - } - - $this->auditLogger->logComparison($comparison); - } - - return new GenerateReportResult( - reportId: $saved->reportId, - path: 'new', - usedNewArchitecture: true, - comparison: $comparison, - ); - } - - private function resolveInput(GenerateReportInput $input): GenerateReportInput - { - if ($input->rawPayload !== null) { - return $input; - } - - if ($this->patientSource === null) { - return $input; - } - - $patients = $this->patientSource->load($input->toContext()); - $payload = $patients->metadata('payload', []); - - if (! is_array($payload) || $payload === []) { - $payload = $this->buildPayloadFromCalculators($input, $patients); - } - - return new GenerateReportInput( - departmentId: $input->departmentId, - userId: $input->userId, - actorUserId: $input->actorUserId, - periodStart: $input->periodStart, - periodEnd: $input->periodEnd, - metrics: (array) ($payload['metrics'] ?? $input->metrics), - observationPatients: (array) ($payload['observationPatients'] ?? $input->observationPatients), - unwantedEvents: (array) ($payload['unwantedEvents'] ?? $input->unwantedEvents), - reportId: isset($payload['reportId']) ? (int) $payload['reportId'] : $input->reportId, - status: (string) ($payload['status'] ?? $input->status), - reportType: $input->reportType, - autoFill: $input->autoFill, - rawPayload: $payload, - persistedReportId: $input->persistedReportId, - createdAt: isset($payload['created_at']) - ? new DateTimeImmutable((string) $payload['created_at']) - : $input->createdAt, - sentAt: isset($payload['sent_at']) - ? new DateTimeImmutable((string) $payload['sent_at']) - : $input->sentAt, - ); - } - - /** - * Переходный fallback для будущих сценариев с calculator-based flow. - * - * @return array - */ - private function buildPayloadFromCalculators(GenerateReportInput $input, PatientCollection $patients): array - { - $metrics = $input->metrics; - - foreach ($this->calculators as $calculator) { - $metrics = [ - ...$metrics, - ...$calculator->calculate($input->toContext(), $patients)->normalized(), - ]; - } - - return [ - 'departmentId' => $input->departmentId, - 'userId' => $input->userId, - 'actorUserId' => $input->actorUserId, - 'autoFill' => $input->autoFill, - 'dates' => [ - $input->periodStart->getTimestamp(), - $input->periodEnd->getTimestamp(), - ], - 'status' => $input->status, - 'created_at' => $input->createdAt?->format('Y-m-d H:i:s') ?? $input->periodEnd->format('Y-m-d H:i:s'), - 'sent_at' => $input->sentAt?->format('Y-m-d H:i:s') ?? $input->periodEnd->format('Y-m-d H:i:s'), - 'metrics' => MetrikaConfig::toPayloadMetrics($metrics), - 'observationPatients' => $input->observationPatients, - 'unwantedEvents' => $input->unwantedEvents, - 'reportId' => $input->reportId, - ]; - } -} diff --git a/app/Application/Reports/ReportFlowDecider.php b/app/Application/Reports/ReportFlowDecider.php deleted file mode 100644 index 3566f44..0000000 --- a/app/Application/Reports/ReportFlowDecider.php +++ /dev/null @@ -1,13 +0,0 @@ - $validated - */ - public function forManualSave(User $actor, array $validated, string $reportType = 'daily'): GenerateReportInput - { - $dateRange = $this->dateRangeService->getNormalizedDateRange( - $actor, - (string) ($validated['dates'][0] ?? null), - (string) ($validated['dates'][1] ?? null), - ); - - $endedAt = new DateTimeImmutable($dateRange->endSql()); - - return new GenerateReportInput( - departmentId: (int) $validated['departmentId'], - userId: (int) $validated['userId'], - actorUserId: (int) $actor->id, - periodStart: new DateTimeImmutable($dateRange->startSql()), - periodEnd: $endedAt, - metrics: (array) ($validated['metrics'] ?? []), - observationPatients: (array) ($validated['observationPatients'] ?? []), - unwantedEvents: (array) ($validated['unwantedEvents'] ?? []), - reportId: isset($validated['reportId']) ? (int) $validated['reportId'] : null, - status: (string) ($validated['status'] ?? 'draft'), - reportType: $reportType, - autoFill: false, - rawPayload: [ - ...$validated, - 'actorUserId' => $actor->id, - 'created_at' => $dateRange->endSql(), - 'sent_at' => $dateRange->endSql(), - ], - createdAt: $endedAt, - sentAt: $endedAt, - ); - } - - public function forAutoFill( - User $scopedUser, - Department $department, - DateRange $dateRange, - string $reportType = 'daily', - ): GenerateReportInput { - $endedAt = new DateTimeImmutable($dateRange->endSql()); - - return new GenerateReportInput( - departmentId: (int) $department->department_id, - userId: (int) ($scopedUser->rf_lpudoctor_id ?? $scopedUser->id), - actorUserId: (int) $scopedUser->id, - periodStart: new DateTimeImmutable($dateRange->startSql()), - periodEnd: $endedAt, - status: 'submitted', - reportType: $reportType, - autoFill: true, - createdAt: $endedAt, - sentAt: $endedAt, - ); - } -} diff --git a/app/Application/Reports/ReportSavePathService.php b/app/Application/Reports/ReportSavePathService.php deleted file mode 100644 index 2007b43..0000000 --- a/app/Application/Reports/ReportSavePathService.php +++ /dev/null @@ -1,58 +0,0 @@ -reportFlowDecider->shouldUseNewArchitecture($reportType); - } - - /** - * @param array $validated - */ - public function saveManual(User $actor, array $validated, string $reportType = 'daily'): GenerateReportResult|Report - { - if (! $this->usesNewArchitecture($reportType)) { - return $this->reportSaveOrchestrator->storeReport($validated, $actor, false); - } - - return $this->generateReportUseCase->handle( - $this->reportInputFactory->forManualSave($actor, $validated, $reportType) - ); - } - - public function saveAutoFill( - User $scopedUser, - Department $department, - DateRange $dateRange, - string $reportType = 'daily', - ): GenerateReportResult|Report { - if (! $this->usesNewArchitecture($reportType)) { - $payload = $this->autoFillReportPayloadBuilder->build($scopedUser, $department, $dateRange); - - return $this->reportSaveOrchestrator->storeReport($payload, $scopedUser, true); - } - - return $this->generateReportUseCase->handle( - $this->reportInputFactory->forAutoFill($scopedUser, $department, $dateRange, $reportType) - ); - } -} diff --git a/app/Data/UnifiedPatientData.php b/app/Data/UnifiedPatientData.php index 7104eba..b40f98b 100644 --- a/app/Data/UnifiedPatientData.php +++ b/app/Data/UnifiedPatientData.php @@ -3,7 +3,9 @@ namespace App\Data; use App\Models\DepartmentPatient; +use App\Models\MedicalHistory; use App\Models\MedicalHistorySnapshot; +use App\Models\MigrationPatient; use App\Models\MisMedicalHistory; use App\Models\MisMigrationPatient; use Carbon\Carbon; @@ -87,6 +89,51 @@ class UnifiedPatientData ); } + public static function fromMedicalHistory( + MedicalHistory $patient, + bool $isRecipientToday = false, + ?DepartmentPatient $linkedManualPatient = null, + ?string $comment = null + ): self { + $birthDate = $patient->birth_date?->format('Y-m-d'); + $manualId = $linkedManualPatient?->department_patient_id; + $historyOriginalId = (int) ($patient->original_id ?? $patient->id); + $migration = $patient->relationLoaded('migrations') + ? $patient->migrations->first() + : null; + + if (! $migration && $patient->relationLoaded('latestMigration')) { + $migration = $patient->latestMigration; + } + + $operations = self::extractOperations($patient->relationLoaded('operations') ? $patient->operations : collect()); + + return new self( + id: $manualId ? "manual:{$manualId}" : "mis:{$historyOriginalId}", + patientUid: "mis:{$historyOriginalId}", + sourceType: $manualId ? 'manual' : 'mis', + departmentPatientId: $manualId, + medicalHistoryId: $historyOriginalId, + fullname: $linkedManualPatient?->full_name ?: $patient->full_name, + birthDate: $linkedManualPatient?->birth_date?->format('Y-m-d') ?? $birthDate, + age: $patient->birth_date?->age ?? $linkedManualPatient?->birth_date?->age, + mkb: [ + 'ds' => $linkedManualPatient?->diagnosis_code ?: $migration?->diagnosis_code, + 'name' => $linkedManualPatient?->diagnosis_name ?: $migration?->diagnosis_name, + ], + operations: $operations, + patientKind: $linkedManualPatient?->patient_kind ?: self::resolvePatientKind($patient->urgency_id), + admittedAt: $linkedManualPatient?->admitted_at?->toIso8601String() ?? $patient->recipient_date?->toIso8601String(), + outcomeType: $patient->outcome_type ?? $linkedManualPatient?->outcome_type, + outcomeDate: $patient->outcome_date ?? $linkedManualPatient?->outcome_at?->toIso8601String(), + comment: $comment, + reanimationIsComplete: isset($patient->reanimation_is_complete) ? (bool) $patient->reanimation_is_complete : null, + isRecipientToday: $isRecipientToday, + isManual: (bool) $linkedManualPatient, + canManageManual: (bool) $linkedManualPatient, + ); + } + public static function fromMisMigrationPatient( MisMigrationPatient $migration, bool $isRecipientToday = false, @@ -133,6 +180,48 @@ class UnifiedPatientData ); } + public static function fromMigrationPatient( + MigrationPatient $migration, + bool $isRecipientToday = false, + ?DepartmentPatient $linkedManualPatient = null, + ?string $comment = null + ): self { + $medicalHistory = $migration->medicalHistory; + $birthDate = $medicalHistory?->birth_date?->format('Y-m-d'); + $manualId = $linkedManualPatient?->department_patient_id; + $historyOriginalId = (int) ($medicalHistory?->original_id ?? $medicalHistory?->id ?? 0); + $operations = self::extractOperations( + $medicalHistory && $medicalHistory->relationLoaded('operations') + ? $medicalHistory->operations + : collect() + ); + + return new self( + id: $manualId ? "manual:{$manualId}" : "mis:{$historyOriginalId}", + patientUid: "mis:{$historyOriginalId}", + sourceType: $manualId ? 'manual' : 'mis', + departmentPatientId: $manualId, + medicalHistoryId: $historyOriginalId, + fullname: $linkedManualPatient?->full_name ?: $medicalHistory?->full_name ?: 'Пациент без имени', + birthDate: $linkedManualPatient?->birth_date?->format('Y-m-d') ?? $birthDate, + age: $medicalHistory?->birth_date?->age ?? $linkedManualPatient?->birth_date?->age, + mkb: [ + 'ds' => $linkedManualPatient?->diagnosis_code ?: $migration->diagnosis_code, + 'name' => $linkedManualPatient?->diagnosis_name ?: $migration->diagnosis_name, + ], + operations: $operations, + patientKind: $linkedManualPatient?->patient_kind ?: self::resolvePatientKind($medicalHistory?->urgency_id), + admittedAt: $linkedManualPatient?->admitted_at?->toIso8601String() ?? $medicalHistory?->recipient_date?->toIso8601String(), + outcomeType: $migration->outcome_type ?? $linkedManualPatient?->outcome_type, + outcomeDate: $migration->outcome_date ?? $linkedManualPatient?->outcome_at?->toIso8601String(), + comment: $comment, + reanimationIsComplete: isset($medicalHistory->reanimation_is_complete) ? (bool) $medicalHistory->reanimation_is_complete : null, + isRecipientToday: $isRecipientToday, + isManual: (bool) $linkedManualPatient, + canManageManual: (bool) $linkedManualPatient, + ); + } + public static function fromDepartmentPatient( DepartmentPatient $patient, bool $isRecipientToday = false, @@ -235,6 +324,25 @@ class UnifiedPatientData }; } + private static function extractOperations(Collection $operations): array + { + return $operations->map(function ($operation) { + $code = $operation->service_medical_code + ?? $operation->serviceMedical?->ServiceMedicalCode + ?? $operation->serviceMedical?->ServiceMedicalID + ?? null; + + $name = $operation->service_medical_name + ?? $operation->serviceMedical?->ServiceMedicalName + ?? null; + + return [ + 'code' => $code, + 'name' => $name, + ]; + })->values()->all(); + } + private static function normalizeDateTime($value): ?string { if (! $value) { diff --git a/app/Domain/Reports/Calculators/BedDaysCalculator.php b/app/Domain/Reports/Calculators/BedDaysCalculator.php deleted file mode 100644 index 4ded95b..0000000 --- a/app/Domain/Reports/Calculators/BedDaysCalculator.php +++ /dev/null @@ -1,33 +0,0 @@ - $intervals - */ - public function calculate(iterable $intervals): MetricAggregate - { - $totalDays = 0; - $patientCount = 0; - - foreach ($intervals as $interval) { - if ($interval->endAt < $interval->startAt) { - continue; - } - - $totalDays += $interval->startAt->setTime(0, 0)->diff($interval->endAt->setTime(0, 0))->days; - $patientCount++; - } - - return new MetricAggregate( - total: $totalDays, - count: $patientCount, - average: $patientCount > 0 ? round($totalDays / $patientCount, 2) : 0.0, - ); - } -} diff --git a/app/Domain/Reports/Calculators/DepartmentLoadCalculator.php b/app/Domain/Reports/Calculators/DepartmentLoadCalculator.php deleted file mode 100644 index ea1b710..0000000 --- a/app/Domain/Reports/Calculators/DepartmentLoadCalculator.php +++ /dev/null @@ -1,15 +0,0 @@ - $intervals - */ - public function calculate(iterable $intervals): MetricAggregate - { - $totalDays = 0; - $patientCount = 0; - - foreach ($intervals as $interval) { - if ($interval->operationAt < $interval->admittedAt) { - continue; - } - - $totalDays += $interval->admittedAt->setTime(0, 0)->diff($interval->operationAt->setTime(0, 0))->days; - $patientCount++; - } - - return new MetricAggregate( - total: $totalDays, - count: $patientCount, - average: $patientCount > 0 ? round($totalDays / $patientCount, 1) : 0.0, - ); - } -} diff --git a/app/Domain/Reports/Contracts/AuditLogger.php b/app/Domain/Reports/Contracts/AuditLogger.php deleted file mode 100644 index 35c2c0d..0000000 --- a/app/Domain/Reports/Contracts/AuditLogger.php +++ /dev/null @@ -1,10 +0,0 @@ - $metrics - */ - public function __construct( - public array $metrics = [], - ) {} - - /** - * @return array - */ - public function normalized(): array - { - return MetrikaConfig::normalizeMetrics($this->metrics); - } -} diff --git a/app/Domain/Reports/Models/OperationInterval.php b/app/Domain/Reports/Models/OperationInterval.php deleted file mode 100644 index e8f8d0f..0000000 --- a/app/Domain/Reports/Models/OperationInterval.php +++ /dev/null @@ -1,13 +0,0 @@ -> $items - * @param array $metadata - */ - public function __construct( - public array $items = [], - public array $metadata = [], - ) {} - - public function metadata(string $key, mixed $default = null): mixed - { - return $this->metadata[$key] ?? $default; - } -} diff --git a/app/Domain/Reports/Models/ReportContext.php b/app/Domain/Reports/Models/ReportContext.php deleted file mode 100644 index c1f6a96..0000000 --- a/app/Domain/Reports/Models/ReportContext.php +++ /dev/null @@ -1,21 +0,0 @@ - $metadata - */ - public function __construct( - public int $departmentId, - public int $userId, - public ?int $actorUserId, - public DateTimeImmutable $periodStart, - public DateTimeImmutable $periodEnd, - public string $reportType = 'daily', - public array $metadata = [], - ) {} -} diff --git a/app/Domain/Reports/Models/ReportSnapshot.php b/app/Domain/Reports/Models/ReportSnapshot.php deleted file mode 100644 index 87e6a30..0000000 --- a/app/Domain/Reports/Models/ReportSnapshot.php +++ /dev/null @@ -1,85 +0,0 @@ - $metrics - * @param array> $observationPatients - * @param array> $unwantedEvents - */ - public function __construct( - public int $departmentId, - public int $userId, - public ?int $actorUserId, - public DateTimeImmutable $periodStart, - public DateTimeImmutable $periodEnd, - public string $status = 'draft', - public bool $autoFill = false, - public array $metrics = [], - public array $observationPatients = [], - public array $unwantedEvents = [], - public ?int $reportId = null, - public ?DateTimeImmutable $createdAt = null, - public ?DateTimeImmutable $sentAt = null, - public string $reportType = 'daily', - ) { - if ($this->departmentId <= 0) { - throw new InvalidArgumentException('departmentId must be positive.'); - } - - if ($this->userId <= 0) { - throw new InvalidArgumentException('userId must be positive.'); - } - - if ($this->periodEnd < $this->periodStart) { - throw new InvalidArgumentException('periodEnd must not be earlier than periodStart.'); - } - } - - /** - * @return array - */ - public function normalizedMetrics(): array - { - return MetrikaConfig::normalizeMetrics($this->metrics); - } - - /** - * @return array - */ - public function payloadMetrics(): array - { - return MetrikaConfig::toPayloadMetrics($this->normalizedMetrics()); - } - - /** - * @return array - */ - public function toComparableArray(): array - { - $observationPatients = $this->observationPatients; - $unwantedEvents = $this->unwantedEvents; - - sort($observationPatients); - sort($unwantedEvents); - - return [ - 'department_id' => $this->departmentId, - 'user_id' => $this->userId, - 'actor_user_id' => $this->actorUserId, - 'period_start' => $this->periodStart->format('Y-m-d H:i:s'), - 'period_end' => $this->periodEnd->format('Y-m-d H:i:s'), - 'status' => $this->status, - 'auto_fill' => $this->autoFill, - 'metrics' => $this->normalizedMetrics(), - 'observation_patients' => $observationPatients, - 'unwanted_events' => $unwantedEvents, - ]; - } -} diff --git a/app/Domain/Reports/Models/SavedReportResult.php b/app/Domain/Reports/Models/SavedReportResult.php deleted file mode 100644 index 701d465..0000000 --- a/app/Domain/Reports/Models/SavedReportResult.php +++ /dev/null @@ -1,11 +0,0 @@ -validate([ - 'metrics' => 'required', - 'observationPatients' => 'nullable', + 'metrics' => 'required|array', + 'observationPatients' => 'nullable|array', 'departmentId' => 'required|integer', 'unwantedEvents' => 'nullable|array', 'startAt' => 'required|integer', 'endAt' => 'required|integer', 'userId' => 'required|integer', - 'reportId' => 'nullable', + 'reportId' => 'nullable|integer', + 'status' => 'nullable|in:draft,submitted', ]); - $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user); - - if ($this->reportSavePathService->usesNewArchitecture()) { - $this->reportSavePathService->saveManual($user, [ - ...$data, - 'dates' => [(int) $data['startAt'], (int) $data['endAt']], - ]); - return response()->json([ - 'status' => 'ok', - 'path' => 'new', - ]); - } - - $misDepartmentId = $user->department->rf_mis_department_id; - - $branchId = MisStationarBranch::where('rf_DepartmentID', $misDepartmentId) - ->value('StationarBranchID'); - - // Определяем, является ли пользователь заведующим/администратором - $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); - - $metrics = $data['metrics']; - $observationPatients = $data['observationPatients']; - $unwantedEvents = $data['unwantedEvents']; - - // Определяем даты в зависимости от роли - $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user); - - $metriks = []; - foreach ($metrics as $key => $value) { - $metrika = new MetrikaResult; - $metrikaId = (int) Str::replace('metrika_item_', '', $key); - $metrika->rf_metrika_item_id = $metrikaId; - $metrika->value = $value; - - $metriks[] = $metrika; - } - - // 1. Плановые - $planIds = $this->reportService->getPatientsByStatus( - $user, - 'plan', - $dateRange, - true, - true, - ); - $planCount = $this->reportService->getPatientsCountByStatus($user, 'plan', $dateRange); - // 2. Экстренные - $emergencyIds = $this->reportService->getPatientsByStatus( - $user, - 'emergency', - $dateRange, - true, - true, - ); - $emergencyCount = $this->reportService->getPatientsCountByStatus($user, 'emergency', $dateRange); - // 3. Выписанные - $dischargedIds = $this->reportService->getPatientsByStatus( - $user, - 'outcome', - $dateRange, - true, - true - ); - // 4. Переведенные - $transferredIds = $this->reportService->getPatientsByStatus( - $user, - 'outcome-transferred', - $dateRange, - true, - true - ); - // 5. Умершие - $deceasedIds = $this->reportService->getPatientsByStatus( - $user, - 'outcome-deceased', - $dateRange, - true, - true - ); - // 6. Поступившие - $recipientIds = $this->reportService->getPatientsByStatus( - $user, - 'recipient', - $dateRange, - true, - true - ); - - \DB::beginTransaction(); - - if (isset($data['reportId']) && $data['reportId']) { - $report = Report::updateOrCreate( - [ - 'report_id' => $data['reportId'], - ], - [ - 'rf_department_id' => $data['departmentId'], - 'rf_user_id' => Auth::user()->id, - 'rf_lpudoctor_id' => $data['userId'], - 'created_at' => $dateRange->endSql(), - 'sent_at' => $dateRange->endSql(), - 'period_start' => $dateRange->startSql(), - 'period_end' => $dateRange->endSql(), - ] - ); - } else { - $report = Report::create([ - 'rf_department_id' => $data['departmentId'], - 'rf_user_id' => Auth::user()->id, - 'rf_lpudoctor_id' => $data['userId'], - 'created_at' => $dateRange->endSql(), - 'sent_at' => $dateRange->endSql(), - 'period_start' => $dateRange->startSql(), - 'period_end' => $dateRange->endSql(), - ]); - } - - if (count($unwantedEvents)) { - foreach ($unwantedEvents as $unwantedEvent) { - // Если есть ID - ищем по нему - if (isset($unwantedEvent['unwanted_event_id']) && $unwantedEvent['unwanted_event_id']) { - UnwantedEvent::updateOrCreate( - ['unwanted_event_id' => $unwantedEvent['unwanted_event_id']], - [ - 'rf_report_id' => $report->report_id, - 'comment' => $unwantedEvent['comment'] ?? '', - 'title' => $unwantedEvent['title'] ?? '', - 'is_visible' => $unwantedEvent['is_visible'] ?? true, - ] - ); - } else { - // Если нет ID - создаем новую запись - UnwantedEvent::create([ - 'rf_report_id' => $report->report_id, - 'comment' => $unwantedEvent['comment'] ?? '', - 'title' => $unwantedEvent['title'] ?? '', - 'is_visible' => $unwantedEvent['is_visible'] ?? true, - ]); - } - } - } else { - $unwantedEvents = $report->unwantedEvents; - foreach ($unwantedEvents as $unwantedEvent) { - $unwantedEvent->delete(); - } - } - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::UNWANTED_EVENTS, - ], - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::UNWANTED_EVENTS, - 'value' => count($unwantedEvents), - ] - ); - - foreach ($metriks as $metrika) { - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => $metrika->rf_metrika_item_id, - ], - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => $metrika->rf_metrika_item_id, - 'value' => $metrika->value, - ] - ); - } - - if (count($observationPatients)) { - foreach ($observationPatients as $observationPatient) { - ObservationPatient::updateOrCreate( - [ - 'rf_medicalhistory_id' => $observationPatient['id'], - 'rf_department_id' => $data['departmentId'], - ], - [ - 'rf_department_id' => $data['departmentId'], - 'rf_report_id' => $report->report_id, - 'rf_medicalhistory_id' => $observationPatient['id'], - 'rf_mkab_id' => null, - 'comment' => $observationPatient['comment'] ?? null, - ] - ); - } - } else { - foreach ($report->observationPatients as $observationPatient) { - $observationPatient->delete(); - } - } - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::OBSERVATION, - ], - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::OBSERVATION, - 'value' => count($observationPatients), - ] - ); - - // Сохраняем снимок для каждого типа пациентов - // Планово - // $this->getPlanOrEmergencyPatients('plan', false, $branchId, $dateRange->startSql(), $dateRange->endSql(), false, false, true); - foreach ($planIds as $id) { - MedicalHistorySnapshot::create([ - 'rf_report_id' => $report->report_id, - 'rf_medicalhistory_id' => $id, - 'patient_type' => 'plan', - ]); - } - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::PLAN, - ], - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::PLAN, - 'value' => $planCount, - ] - ); - - // $this->getPlanOrEmergencyPatients('emergency', false, $branchId, $startDate, $endDate, false, false, true); - // Экстренно - foreach ($emergencyIds as $id) { - MedicalHistorySnapshot::create([ - 'rf_report_id' => $report->report_id, - 'rf_medicalhistory_id' => $id, - 'patient_type' => 'emergency', - ]); - } - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::EMERGENCY, - ], - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::EMERGENCY, - 'value' => $emergencyCount, - ] - ); - - // $this->getDischargedPatients($branchId, $startDate, $endDate, true); - foreach ($dischargedIds as $id) { - MedicalHistorySnapshot::create([ - 'rf_report_id' => $report->report_id, - 'rf_medicalhistory_id' => $id, - 'patient_type' => 'discharged', - ]); - } - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::DISCHARGED, - ], - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::DISCHARGED, - 'value' => count($dischargedIds), - ] - ); - - // $this->getTransferredPatients($branchId, $startDate, $endDate, true); - foreach ($transferredIds as $id) { - MedicalHistorySnapshot::create([ - 'rf_report_id' => $report->report_id, - 'rf_medicalhistory_id' => $id, - 'patient_type' => 'transferred', - ]); - } - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::TRANSFERRED, - ], - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::TRANSFERRED, - 'value' => count($transferredIds), - ] - ); - - // $this->getDeceasedOutcomePatients($branchId, $startDate, $endDate, false, true); - foreach ($deceasedIds as $id) { - MedicalHistorySnapshot::create([ - 'rf_report_id' => $report->report_id, - 'rf_medicalhistory_id' => $id, - 'patient_type' => 'deceased', - ]); - } - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::DECEASED, - ], - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::DECEASED, - 'value' => count($deceasedIds), - ] - ); - - // $recipientIds = $this->getPlanOrEmergencyPatients( - // null, - // $isHeadOrAdmin, - // $branchId, - // $startDate, - // $endDate, - // false, - // true, - // true, - // today: true - // ); - foreach ($recipientIds as $id) { - MedicalHistorySnapshot::create([ - 'rf_report_id' => $report->report_id, - 'rf_medicalhistory_id' => $id, - 'patient_type' => 'recipient', - ]); - } - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::RECIPIENT, - ], - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::RECIPIENT, - 'value' => count($recipientIds), - ] - ); - - // 7. Находящиеся на лечении - // $currentIds = $this->getCurrentPatients($branchId, false, true); - // foreach ($currentIds as $id) { - // MedicalHistorySnapshot::create([ - // 'rf_report_id' => $report->report_id, - // 'rf_medicalhistory_id' => $id, - // 'patient_type' => 'current' - // ]); - // } - - \DB::commit(); + $report = $this->reportService->storeReport([ + ...$data, + 'dates' => [(int) $data['startAt'], (int) $data['endAt']], + 'status' => $data['status'] ?? 'draft', + ], $user); return response()->json([ 'message' => 'success', + 'report_id' => $report->report_id, ]); } diff --git a/app/Http/Controllers/Web/ReportController.php b/app/Http/Controllers/Web/ReportController.php index 264d002..e6d3864 100644 --- a/app/Http/Controllers/Web/ReportController.php +++ b/app/Http/Controllers/Web/ReportController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers\Web; -use App\Application\Reports\ReportSavePathService; use App\Exports\ReportPageExport; use App\Http\Controllers\Controller; use App\Http\Resources\Mis\FormattedPatientResource; @@ -22,7 +21,6 @@ class ReportController extends Controller protected ReportPageService $reportPageService, protected ReportService $reportService, protected DateRangeService $dateRangeService, - protected ReportSavePathService $reportSavePathService, ) {} public function index(Request $request) @@ -48,7 +46,7 @@ class ReportController extends Controller 'status' => 'nullable|in:draft,submitted', ]); - $this->reportSavePathService->saveManual(Auth::user(), $validated); + $this->reportService->storeReport($validated, Auth::user()); return redirect()->route('start'); } diff --git a/app/Infrastructure/Reports/Adapters/LegacyReportServiceAdapter.php b/app/Infrastructure/Reports/Adapters/LegacyReportServiceAdapter.php deleted file mode 100644 index 0538253..0000000 --- a/app/Infrastructure/Reports/Adapters/LegacyReportServiceAdapter.php +++ /dev/null @@ -1,150 +0,0 @@ -reportRuntimeService->prepareForHeavySave(); - } - - /** - * @param array $payload - * @throws \Exception - */ - public function buildSnapshotFromPayload(array $payload, string $reportType = 'daily'): ReportSnapshot - { - $start = $this->dateRangeService->toCarbon($payload['dates'][0] ?? $payload['period_start'] ?? null); - $end = $this->dateRangeService->toCarbon($payload['dates'][1] ?? $payload['period_end'] ?? null); - - return new ReportSnapshot( - departmentId: (int) $payload['departmentId'], - userId: (int) $payload['userId'], - actorUserId: isset($payload['actorUserId']) ? (int) $payload['actorUserId'] : null, - periodStart: new DateTimeImmutable($start->format('Y-m-d H:i:s')), - periodEnd: new DateTimeImmutable($end->format('Y-m-d H:i:s')), - status: (string) ($payload['status'] ?? 'draft'), - autoFill: (bool) ($payload['autoFill'] ?? false), - metrics: MetrikaConfig::normalizeMetrics((array) ($payload['metrics'] ?? [])), - observationPatients: $this->normalizeObservationPatients((array) ($payload['observationPatients'] ?? [])), - unwantedEvents: $this->normalizeUnwantedEvents((array) ($payload['unwantedEvents'] ?? [])), - reportId: isset($payload['reportId']) ? (int) $payload['reportId'] : null, - createdAt: isset($payload['created_at']) ? new DateTimeImmutable((string) $payload['created_at']) : null, - sentAt: isset($payload['sent_at']) ? new DateTimeImmutable((string) $payload['sent_at']) : null, - reportType: $reportType, - ); - } - - public function buildSnapshotFromInput(GenerateReportInput $input): ReportSnapshot - { - if ($input->rawPayload !== null) { - return $this->buildSnapshotFromPayload([ - ...$input->rawPayload, - 'actorUserId' => $input->actorUserId, - 'autoFill' => $input->autoFill, - ], $input->reportType); - } - - return $input->toSnapshot(); - } - - public function createPatientSnapshots(Report $report, User $user, ReportSnapshot $snapshot, bool $autoFill): void - { - $this->snapshotService->createPatientSnapshots( - report: $report, - user: $user, - dates: [ - $snapshot->periodStart->getTimestamp(), - $snapshot->periodEnd->getTimestamp(), - ], - fillableAuto: $autoFill, - ); - } - - public function syncCalculatedMetrics(Report $report, User $user, ReportSnapshot $snapshot): void - { - $this->reportSaveOrchestrator->syncCalculatedMetrics( - $report, - $user, - [ - 'dates' => [ - $snapshot->periodStart->getTimestamp(), - $snapshot->periodEnd->getTimestamp(), - ], - ], - ); - } - - public function finalizeStoredReport(Report $report): void - { - $this->reportSaveOrchestrator->finalizeStoredReport($report); - } - - public function saveLethalMetricFromSnapshots(Report $report): void - { - $this->reportSaveOrchestrator->saveLethalMetricFromSnapshots($report); - } - - public function clearCacheAfterReportCreation(User $user, Report $report): void - { - $this->reportRuntimeService->clearCacheAfterReportCreation($user, $report); - } - - public function buildAutoFillPayload(User $user, Department $department, DateRange $dateRange): array - { - return $this->autoFillReportPayloadBuilder->build($user, $department, $dateRange); - } - - /** - * @param array> $patients - * @return array> - */ - private function normalizeObservationPatients(array $patients): array - { - return array_values(array_map(static function (array $patient): array { - return [ - 'medical_history_id' => $patient['medical_history_id'] ?? $patient['id'] ?? null, - 'department_patient_id' => $patient['department_patient_id'] ?? null, - 'comment' => $patient['comment'] ?? null, - ]; - }, $patients)); - } - - /** - * @param array> $events - * @return array> - */ - private function normalizeUnwantedEvents(array $events): array - { - return array_values(array_map(static function (array $event): array { - return [ - 'title' => $event['title'] ?? '', - 'comment' => $event['comment'] ?? '', - 'is_visible' => (bool) ($event['is_visible'] ?? true), - ]; - }, $events)); - } -} diff --git a/app/Infrastructure/Reports/Logging/ReportsAuditLogger.php b/app/Infrastructure/Reports/Logging/ReportsAuditLogger.php deleted file mode 100644 index 3e66ca4..0000000 --- a/app/Infrastructure/Reports/Logging/ReportsAuditLogger.php +++ /dev/null @@ -1,26 +0,0 @@ -info('reports.comparison', [ - 'report_type' => $result->reportType, - 'path' => $result->path, - 'status' => $result->status, - 'department_id' => $result->departmentId, - 'user_id' => $result->userId, - 'period_start' => $result->periodStart, - 'period_end' => $result->periodEnd, - 'report_id' => $result->reportId, - 'diff' => $result->diff, - 'duration_ms' => $result->durationMs, - ]); - } -} diff --git a/app/Infrastructure/Reports/Repositories/EloquentReportRepository.php b/app/Infrastructure/Reports/Repositories/EloquentReportRepository.php deleted file mode 100644 index bade750..0000000 --- a/app/Infrastructure/Reports/Repositories/EloquentReportRepository.php +++ /dev/null @@ -1,113 +0,0 @@ -reportStorageService = $reportStorageService ?? app(ReportStorageService::class); - $this->reportMetricsFinalizer = $reportMetricsFinalizer ?? app(ReportMetricsFinalizer::class); - } - - private readonly ReportStorageService $reportStorageService; - - private readonly ReportMetricsFinalizer $reportMetricsFinalizer; - - public function save(ReportSnapshot $snapshot): SavedReportResult - { - $this->legacyAdapter->prepareMemoryForHeavySave(); - $actor = $snapshot->actorUserId ? User::query()->find($snapshot->actorUserId) : null; - - if (! $actor) { - throw new RuntimeException('Actor user was not found.'); - } - - $report = DB::transaction(function () use ($snapshot, $actor) { - $report = $this->reportStorageService->createOrUpdateReport($snapshot, $actor); - $this->reportStorageService->saveMetrics($report, $snapshot); - $this->reportStorageService->saveUnwantedEvents($report, $snapshot); - $this->reportStorageService->saveObservationPatients($report, $snapshot); - $this->legacyAdapter->createPatientSnapshots($report, $actor, $snapshot, $snapshot->autoFill); - $this->legacyAdapter->syncCalculatedMetrics($report, $actor, $snapshot); - - return $report; - }); - - DB::transaction(function () use ($report) { - $this->reportMetricsFinalizer->finalize($report); - $this->legacyAdapter->saveLethalMetricFromSnapshots($report); - }); - - $this->legacyAdapter->clearCacheAfterReportCreation($actor, $report); - - $savedSnapshot = $this->findSnapshot((int) $report->report_id); - - if (! $savedSnapshot) { - throw new RuntimeException('Saved report could not be reloaded for comparison.'); - } - - return new SavedReportResult( - reportId: (int) $report->report_id, - snapshot: $savedSnapshot, - ); - } - - public function findSnapshot(int $reportId): ?ReportSnapshot - { - /** @var Report|null $report */ - $report = Report::query() - ->with(['metrikaResults', 'observationPatients', 'unwantedEvents']) - ->find($reportId); - - if (! $report || ! $report->period_start || ! $report->period_end) { - return null; - } - - return new ReportSnapshot( - departmentId: (int) $report->rf_department_id, - userId: (int) ($report->rf_lpudoctor_id ?? $report->rf_user_id), - actorUserId: (int) $report->rf_user_id, - periodStart: new \DateTimeImmutable($report->period_start->format('Y-m-d H:i:s')), - periodEnd: new \DateTimeImmutable($report->period_end->format('Y-m-d H:i:s')), - status: (string) ($report->status ?? 'draft'), - autoFill: false, - metrics: $report->metrikaResults - ->mapWithKeys(fn (MetrikaResult $metric) => [(int) $metric->rf_metrika_item_id => $metric->value]) - ->all(), - observationPatients: $report->observationPatients - ->map(fn (ObservationPatient $patient) => [ - 'medical_history_id' => $patient->rf_medicalhistory_id, - 'department_patient_id' => $patient->rf_department_patient_id, - 'comment' => $patient->comment, - ])->values()->all(), - unwantedEvents: $report->unwantedEvents - ->map(fn (UnwantedEvent $event) => [ - 'title' => $event->title, - 'comment' => $event->comment, - 'is_visible' => (bool) $event->is_visible, - ])->values()->all(), - reportId: (int) $report->report_id, - createdAt: $report->created_at ? new \DateTimeImmutable($report->created_at->format('Y-m-d H:i:s')) : null, - sentAt: $report->sent_at ? new \DateTimeImmutable($report->sent_at->format('Y-m-d H:i:s')) : null, - ); - } - -} diff --git a/app/Infrastructure/Reports/Services/AutoFillReportPayloadBuilder.php b/app/Infrastructure/Reports/Services/AutoFillReportPayloadBuilder.php index 14a90d6..25c7033 100644 --- a/app/Infrastructure/Reports/Services/AutoFillReportPayloadBuilder.php +++ b/app/Infrastructure/Reports/Services/AutoFillReportPayloadBuilder.php @@ -4,6 +4,8 @@ namespace App\Infrastructure\Reports\Services; use App\Domain\Reports\ValueObjects\MetrikaConfig; use App\Models\Department; +use App\Models\MedicalHistory; +use App\Models\MigrationPatient; use App\Models\MisMedicalHistory; use App\Models\MisStationarBranch; use App\Models\User; @@ -11,6 +13,7 @@ use App\Services\DateRange; use App\Services\PatientService; use App\Services\UnifiedPatientService; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; class AutoFillReportPayloadBuilder { @@ -75,19 +78,35 @@ class AutoFillReportPayloadBuilder $deceasedCodes = [5, 6, 15, 16]; $transferCodes = [4, 14]; - $planRecipient = (clone $recipientQuery) - ->where('rf_EmerSignID', 1) - ->distinct() - ->count('MedicalHistoryID'); + if ($this->useMaterializedViews()) { + $planRecipient = (clone $recipientQuery) + ->where('urgency_id', 1) + ->distinct() + ->count('original_id'); - $emergencyRecipient = (clone $recipientQuery) - ->whereIn('rf_EmerSignID', [2, 4]) - ->distinct() - ->count('MedicalHistoryID'); + $emergencyRecipient = (clone $recipientQuery) + ->whereIn('urgency_id', [2, 4]) + ->distinct() + ->count('original_id'); - $recipientTotal = (clone $recipientQuery) - ->distinct() - ->count('MedicalHistoryID'); + $recipientTotal = (clone $recipientQuery) + ->distinct() + ->count('original_id'); + } else { + $planRecipient = (clone $recipientQuery) + ->where('rf_EmerSignID', 1) + ->distinct() + ->count('MedicalHistoryID'); + + $emergencyRecipient = (clone $recipientQuery) + ->whereIn('rf_EmerSignID', [2, 4]) + ->distinct() + ->count('MedicalHistoryID'); + + $recipientTotal = (clone $recipientQuery) + ->distinct() + ->count('MedicalHistoryID'); + } $discharged = $this->countOutcomeByVisitResultIds($branchId, $dateRange, $dischargeCodes); $deceased = $this->countOutcomeByVisitResultIds($branchId, $dateRange, $deceasedCodes); @@ -108,6 +127,13 @@ class AutoFillReportPayloadBuilder private function buildRecipientMedicalHistoryQuery(int $branchId, DateRange $dateRange) { + if ($this->useMaterializedViews()) { + return MedicalHistory::query() + ->whereHas('migrations', fn ($query) => $query + ->where('stationar_branch_id', $branchId) + ->admitted($dateRange->startSql(), $dateRange->endSql())); + } + $startAt = $dateRange->start()->copy()->subDay()->format('Y-m-d H:i:s'); $endAt = $dateRange->end()->copy()->addDay()->format('Y-m-d H:i:s'); @@ -130,6 +156,13 @@ class AutoFillReportPayloadBuilder private function buildTreatedMedicalHistoryQuery(int $branchId, DateRange $dateRange) { + if ($this->useMaterializedViews()) { + return MedicalHistory::query() + ->whereHas('migrations', fn ($query) => $query + ->where('stationar_branch_id', $branchId) + ->dateRange($dateRange->startSql(), $dateRange->endSql())); + } + $query = MisMedicalHistory::query() ->where('MedicalHistoryID', '<>', 0) ->whereExists(function ($query) use ($branchId) { @@ -155,6 +188,24 @@ class AutoFillReportPayloadBuilder private function countOutcomeByVisitResultIds(int $branchId, DateRange $dateRange, array $visitResultIds): int { + if ($this->useMaterializedViews()) { + $query = MigrationPatient::query() + ->where('stationar_branch_id', $branchId); + + if ($visitResultIds === [1, 11, 2, 12, 7, 18, 48]) { + $query->discharged($dateRange->startSql(), $dateRange->endSql()); + } elseif ($visitResultIds === [5, 6, 15, 16]) { + $query->deceased($dateRange->startSql(), $dateRange->endSql()); + } elseif ($visitResultIds === [4, 14]) { + $query->transferred($dateRange->startSql(), $dateRange->endSql()); + } else { + $query->whereIn('visit_result_id', $visitResultIds) + ->dateRange($dateRange->startSql(), $dateRange->endSql()); + } + + return $query->distinct('medical_history_id')->count('medical_history_id'); + } + return $this->buildTreatedMedicalHistoryQuery($branchId, $dateRange) ->whereExists(function ($query) use ($branchId, $visitResultIds) { $query->select(DB::raw(1)) @@ -172,4 +223,9 @@ class AutoFillReportPayloadBuilder return MisStationarBranch::where('rf_DepartmentID', $misDepartmentId) ->value('StationarBranchID'); } + + private function useMaterializedViews(): bool + { + return Schema::hasTable('mv_medicalhistory_summary') && Schema::hasTable('mv_migrationpatient_details'); + } } diff --git a/app/Infrastructure/Reports/Services/CalculatedMetricsSynchronizer.php b/app/Infrastructure/Reports/Services/CalculatedMetricsSynchronizer.php index d31c77f..b06b031 100644 --- a/app/Infrastructure/Reports/Services/CalculatedMetricsSynchronizer.php +++ b/app/Infrastructure/Reports/Services/CalculatedMetricsSynchronizer.php @@ -2,111 +2,12 @@ namespace App\Infrastructure\Reports\Services; -use App\Domain\Reports\ValueObjects\MetrikaConfig; use App\Models\Department; use App\Models\DepartmentPatientOperation; -use App\Models\MedicalHistorySnapshot; -use App\Models\MisStationarBranch; -use App\Models\ObservationPatient; -use App\Models\Report; -use App\Models\UnwantedEvent; -use App\Models\User; use App\Services\DateRange; -use App\Services\DateRangeService; -use App\Services\PatientService; class CalculatedMetricsSynchronizer { - public function __construct( - private readonly DateRangeService $dateRangeService, - private readonly PatientService $patientService, - private readonly ReportStorageService $reportStorageService, - ) {} - - /** - * @param array $data - */ - public function sync(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->getBranchId($department->rf_mis_department_id); - - $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->patientService->getSurgicalPatients('emergency', $branchId, $dateRange, true) - : 0; - $misPlanSurgery = $branchId - ? $this->patientService->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->reportStorageService->saveMetric($report, MetrikaConfig::RECIPIENT, $recipientCount); - $this->reportStorageService->saveMetric($report, MetrikaConfig::PLAN, $planCount); - $this->reportStorageService->saveMetric($report, MetrikaConfig::OUTCOME, $outcomeCount); - $this->reportStorageService->saveMetric($report, MetrikaConfig::CURRENT, $currentCount); - $this->reportStorageService->saveMetric($report, MetrikaConfig::DECEASED, $deceasedCount); - $this->reportStorageService->saveMetric($report, MetrikaConfig::EMERGENCY_SURGERY, $misEmergencySurgery + ($manualSurgicalCount[0] ?? 0)); - $this->reportStorageService->saveMetric($report, MetrikaConfig::PLAN_SURGERY, $misPlanSurgery + ($manualSurgicalCount[1] ?? 0)); - $this->reportStorageService->saveMetric($report, MetrikaConfig::EMERGENCY, $emergencyCount); - $this->reportStorageService->saveMetric($report, MetrikaConfig::TRANSFERRED, $transferredCount); - $this->reportStorageService->saveMetric($report, MetrikaConfig::OBSERVATION, $observationCount); - $this->reportStorageService->saveMetric($report, MetrikaConfig::DISCHARGED, $dischargedCount); - $this->reportStorageService->saveMetric($report, MetrikaConfig::UNWANTED_EVENTS, $unwantedEventsCount); - } - - private function getBranchId(int $misDepartmentId): ?int - { - return MisStationarBranch::where('rf_DepartmentID', $misDepartmentId) - ->value('StationarBranchID'); - } - - 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(); - } - public function getManualSurgicalCounts(Department $department, DateRange $dateRange): array { $baseQuery = DepartmentPatientOperation::query() diff --git a/app/Infrastructure/Reports/Services/ManualPatientManagementService.php b/app/Infrastructure/Reports/Services/ManualPatientManagementService.php deleted file mode 100644 index 4e9f515..0000000 --- a/app/Infrastructure/Reports/Services/ManualPatientManagementService.php +++ /dev/null @@ -1,256 +0,0 @@ - $data - */ - public function createManualPatient(Department $department, User $user, array $data): DepartmentPatient - { - $report = $this->resolveReportForManualPatient($department, $user, $data); - - return $this->unifiedPatientService->createManualPatient($department, $user, $data, $report->report_id); - } - - /** - * @param array $data - */ - public function setManualPatientOutcome(User $user, int $departmentPatientId, array $data): DepartmentPatient - { - $patient = DepartmentPatient::query() - ->where('department_patient_id', $departmentPatientId) - ->firstOrFail(); - - $updatedPatient = $this->unifiedPatientService->recordManualOutcome($patient, $data); - $this->syncManualPatientSnapshots($updatedPatient, $user, []); - - return $updatedPatient; - } - - /** - * @param array $data - */ - public function updateManualPatient(User $user, int $departmentPatientId, array $data): DepartmentPatient - { - $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): DepartmentPatient - { - $patient = DepartmentPatient::query() - ->where('department_patient_id', $departmentPatientId) - ->firstOrFail(); - - return $this->unifiedPatientService->linkManualPatientToMis($patient, $medicalHistoryId); - } - - /** - * @return Collection - */ - public function getManualPatientOperations(User $user, int $departmentPatientId): Collection - { - $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); - - return $patient->operations() - ->with('serviceMedical') - ->orderByDesc('started_at') - ->get(); - } - - /** - * @param array $data - */ - 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'); - } - - /** - * @param array $data - */ - 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(); - } - - 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(); - } - - /** - * @param array $data - */ - 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->contextResolver - ->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(), - ]); - } - - /** - * @param array $data - */ - 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(); - } -} diff --git a/app/Infrastructure/Reports/Services/ObservationPatientManagementService.php b/app/Infrastructure/Reports/Services/ObservationPatientManagementService.php deleted file mode 100644 index 56aee32..0000000 --- a/app/Infrastructure/Reports/Services/ObservationPatientManagementService.php +++ /dev/null @@ -1,28 +0,0 @@ -where('rf_department_patient_id', $id) - ->delete(); - - return; - } - - ObservationPatient::query() - ->where('rf_medicalhistory_id', $id) - ->delete(); - } -} diff --git a/app/Infrastructure/Reports/Services/ReanimationIndicatorService.php b/app/Infrastructure/Reports/Services/ReanimationIndicatorService.php deleted file mode 100644 index 9caf3d0..0000000 --- a/app/Infrastructure/Reports/Services/ReanimationIndicatorService.php +++ /dev/null @@ -1,80 +0,0 @@ -create([ - 'rf_department_id' => $departmentId, - 'rf_report_id' => $reportId, - 'rf_medicalhistory_id' => $medicalHistoryId, - 'indicator' => $indicator, - 'comment' => $comment, - 'created_by' => $user->id, - ]); - } - - /** - * @param array $medicalHistoryIds - * @return Collection - */ - public function latestByMedicalHistory(int $departmentId, array $medicalHistoryIds): Collection - { - 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'); - } - - /** - * @return Collection - */ - public function history(int $departmentId, int $medicalHistoryId, int $limit = 50): Collection - { - 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', - ]); - } -} diff --git a/app/Infrastructure/Reports/Services/ReportClinicalSearchService.php b/app/Infrastructure/Reports/Services/ReportClinicalSearchService.php deleted file mode 100644 index c1b07f7..0000000 --- a/app/Infrastructure/Reports/Services/ReportClinicalSearchService.php +++ /dev/null @@ -1,22 +0,0 @@ -unifiedPatientService->searchMisPatients($department, $query); - } -} diff --git a/app/Infrastructure/Reports/Services/ReportMetadataReadService.php b/app/Infrastructure/Reports/Services/ReportMetadataReadService.php deleted file mode 100644 index c1d0613..0000000 --- a/app/Infrastructure/Reports/Services/ReportMetadataReadService.php +++ /dev/null @@ -1,168 +0,0 @@ -contextResolver->resolveReportForPeriod($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, - ]; - } - - /** - * @return Collection> - */ - public function getUnwantedEvents(Department $department, DateRange $dateRange): Collection - { - 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'), - ]; - }); - } - - /** - * @return Collection - */ - public function getReportsForDateRange(int $departmentId, DateRange $dateRange): Collection - { - return $this->contextResolver->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, - ]; - } - - 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(); - } -} diff --git a/app/Infrastructure/Reports/Services/ReportMetricsFinalizer.php b/app/Infrastructure/Reports/Services/ReportMetricsFinalizer.php deleted file mode 100644 index 93239f1..0000000 --- a/app/Infrastructure/Reports/Services/ReportMetricsFinalizer.php +++ /dev/null @@ -1,168 +0,0 @@ -saveBedDaysMetrics($report); - $this->savePreoperativeMetrics($report); - $this->saveDepartmentLoadMetric($report); - } - - private function saveBedDaysMetrics(Report $report): void - { - $result = $this->bedDaysCalculator->calculate($this->buildStayIntervals($report)); - - $this->reportStorageService->saveMetric($report, MetrikaConfig::TOTAL_BED_DAYS, $result->total); - $this->reportStorageService->saveMetric($report, MetrikaConfig::AVERAGE_BED_DAYS, $result->average); - } - - private function savePreoperativeMetrics(Report $report): void - { - $result = $this->preoperativeDaysCalculator->calculate($this->buildOperationIntervals($report)); - - $this->reportStorageService->saveMetric($report, MetrikaConfig::TOTAL_PREOPERATIVE_DAYS, $result->total); - $this->reportStorageService->saveMetric($report, MetrikaConfig::PREOPERATIVE_PATIENT_COUNT, $result->count); - $this->reportStorageService->saveMetric($report, MetrikaConfig::PREOPERATIVE_AVERAGE_DAYS, $result->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); - - $this->reportStorageService->saveMetric( - $report, - MetrikaConfig::DEPARTMENT_LOADED, - $this->departmentLoadCalculator->calculate($currentCount, $bedsCount), - ); - } - - /** - * @return array - */ - private function buildStayIntervals(Report $report): array - { - $snapshots = MedicalHistorySnapshot::query() - ->where('rf_report_id', $report->report_id) - ->whereIn('patient_type', ['discharged', 'deceased']) - ->with('medicalHistory') - ->get(); - - $intervals = []; - - foreach ($snapshots as $snapshot) { - $history = $snapshot->medicalHistory; - - if (! $history) { - continue; - } - - $startRaw = $history->DateRecipientHS ?? $history->DateRecipient ?? null; - $endRaw = null; - - if ($snapshot->patient_type === 'deceased') { - if ($this->isRealDate($history->DateDeath)) { - $endRaw = $history->DateDeath; - } elseif ($this->isRealDate($history->DateExtract)) { - $endRaw = $history->DateExtract; - } - } elseif ($this->isRealDate($history->DateExtract)) { - $endRaw = $history->DateExtract; - } - - if (! $startRaw || ! $endRaw) { - continue; - } - - $intervals[] = new StayInterval( - startAt: new DateTimeImmutable((string) $startRaw), - endAt: new DateTimeImmutable((string) $endRaw), - ); - } - - return $intervals; - } - - private function isRealDate(mixed $value): bool - { - if (! $value) { - return false; - } - - $date = $value instanceof DateTimeInterface - ? $value->format('Y-m-d') - : (new DateTimeImmutable((string) $value))->format('Y-m-d'); - - return ! in_array($date, ['1900-01-01', '2222-01-01'], true); - } - - /** - * @return array - */ - private function buildOperationIntervals(Report $report): array - { - $patientIds = MedicalHistorySnapshot::query() - ->where('rf_report_id', $report->report_id) - ->whereIn('patient_type', ['discharged', 'deceased']) - ->pluck('rf_medicalhistory_id') - ->unique() - ->values(); - - if ($patientIds->isEmpty()) { - return []; - } - - $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(); - - $intervals = []; - - foreach ($rows as $row) { - $startRaw = $row->DateRecipientHS ?? $row->DateRecipient ?? null; - $operationRaw = $row->first_operation ?? null; - - if (! $startRaw || ! $operationRaw) { - continue; - } - - $intervals[] = new OperationInterval( - admittedAt: new DateTimeImmutable((string) $startRaw), - operationAt: new DateTimeImmutable((string) $operationRaw), - ); - } - - return $intervals; - } -} diff --git a/app/Infrastructure/Reports/Services/ReportPatientsReadService.php b/app/Infrastructure/Reports/Services/ReportPatientsReadService.php deleted file mode 100644 index ce05752..0000000 --- a/app/Infrastructure/Reports/Services/ReportPatientsReadService.php +++ /dev/null @@ -1,399 +0,0 @@ -parseScopedStatus($status); - $branchId = $this->contextResolver->resolveBranchId($department); - - if (! $branchId) { - return collect(); - } - - if ($sourceScope === 'special' || $baseStatus === 'reanimation') { - return $this->getPatientsFromReplica( - $department, - $user, - $status, - $dateRange, - $branchId, - $onlyIds, - $includeCurrentPatients - ); - } - - $useSnapshots = ! $this->contextResolver->shouldUseReplicaForLiveStatus($user, $baseStatus, $dateRange) - && $this->contextResolver->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->contextResolver->resolveBranchId($department); - - if (! $branchId) { - return 0; - } - - if ($sourceScope === 'special' || $baseStatus === 'reanimation') { - return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId); - } - - $useSnapshots = ! $this->contextResolver->shouldUseReplicaForLiveStatus($user, $baseStatus, $dateRange) - && $this->contextResolver->shouldUseSnapshots($department, $dateRange); - - if ($useSnapshots) { - return $this->getPatientsCountFromSnapshots($department, $status, $dateRange, $branchId); - } - - return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId); - } - - /** - * Построить карту счётчиков пациентов по scope для интерфейса. - */ - 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; - } - - /** - * Получить пациентскую выборку из submitted-снапшотов. - */ - public function getPatientsFromSnapshots( - Department $department, - string $status, - DateRange $dateRange, - int $branchId, - bool $onlyIds = false - ) { - [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); - $reportIds = $this->contextResolver - ->getReportsForDateRange($department->department_id, $dateRange) - ->pluck('report_id') - ->all(); - $recipientReportIds = $this->contextResolver->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->snapshotService->getPatientsFromSnapshots( - 'discharged', - $reportIds, - false, - false, - $recipientReportIds - ); - $deceased = $this->snapshotService->getPatientsFromSnapshots( - 'deceased', - $reportIds, - false, - false, - $recipientReportIds - ); - - $merged = UnifiedPatientData::unique($discharged->concat($deceased)) - ->sortByDesc(fn (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->snapshotService->getPatientsFromOneDayCurrentSnapshots( - $patientType, - $reportIds, - false, - $recipientReportIds - ); - - return $this->filterSnapshotPatientsByScope($patients, $sourceScope, $onlyIds); - } - - $patients = $this->snapshotService->getPatientsFromSnapshots( - $patientType, - $reportIds, - $branchId, - false, - in_array($baseStatus, ['plan', 'emergency'], true), - $recipientReportIds - ); - - return $this->filterSnapshotPatientsByScope($patients, $sourceScope, $onlyIds); - } - - /** - * Получить пациентов напрямую из live-реплики и manual-источников. - */ - 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 - ); - } - - /** - * Посчитать пациентов в снапшотах с той же семантикой scope, что и в legacy-сервисе. - */ - private function getPatientsCountFromSnapshots( - Department $department, - string $status, - DateRange $dateRange, - int $branchId - ): int { - [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); - $reportIds = $this->contextResolver - ->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'); - } - - /** - * Посчитать пациентов из реплики и manual-источников с legacy-правилами include-current. - */ - 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) - ), - }; - } - - /** - * Применить фильтрацию по MIS/manual scope к коллекции DTO из снапшотов. - */ - 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; - } - - /** - * Разбить scoped-статус вроде "mis-plan" на базовый статус и scope источника. - * - * @return array{0: string, 1: string} - */ - 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']; - } -} diff --git a/app/Infrastructure/Reports/Services/ReportSaveOrchestrator.php b/app/Infrastructure/Reports/Services/ReportSaveOrchestrator.php deleted file mode 100644 index 9f59492..0000000 --- a/app/Infrastructure/Reports/Services/ReportSaveOrchestrator.php +++ /dev/null @@ -1,164 +0,0 @@ - $data - */ - public function storeReport(array $data, User $user, bool $fillableAuto = false): Report - { - $this->reportRuntimeService->prepareForHeavySave(); - $snapshot = $this->buildSnapshot($data, $user, $fillableAuto); - - $report = DB::transaction(function () use ($snapshot, $user, $data, $fillableAuto) { - $report = $this->reportStorageService->createOrUpdateReport($snapshot, $user); - $this->reportStorageService->saveMetrics($report, $snapshot); - $this->reportStorageService->saveUnwantedEvents($report, $snapshot); - $this->reportStorageService->saveObservationPatients($report, $snapshot); - - $this->snapshotService->createPatientSnapshots( - $report, - $user, - [ - $snapshot->periodStart->getTimestamp(), - $snapshot->periodEnd->getTimestamp(), - ], - $fillableAuto - ); - - $this->syncCalculatedMetrics($report, $user, $data); - - return $report; - }); - - DB::transaction(function () use ($report) { - $this->finalizeStoredReport($report); - $this->saveLethalMetricFromSnapshots($report); - }); - - $this->reportRuntimeService->clearCacheAfterReportCreation($user, $report); - - return $report; - } - - public function syncCalculatedMetrics(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 saveLethalMetricFromSnapshots(Report $report): void - { - $snapshots = MedicalHistorySnapshot::query() - ->where('rf_report_id', $report->report_id) - ->whereIn('patient_type', ['discharged', 'deceased']) - ->with('medicalHistory') - ->get(); - - if ($snapshots->isNotEmpty()) { - return; - } - - MetrikaResult::query()->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"); - } - - private function buildSnapshot(array $data, User $user, bool $fillableAuto): ReportSnapshot - { - $dateRange = $this->dateRangeService->getNormalizedDateRange( - $user, - (string) ($data['dates'][0] ?? null), - (string) ($data['dates'][1] ?? null) - ); - - $rangeEndAt = $dateRange->endSql(); - $createdAt = $data['created_at'] ?? $rangeEndAt; - $sentAt = $data['sent_at'] ?? $rangeEndAt; - - return new ReportSnapshot( - departmentId: (int) $data['departmentId'], - userId: (int) $data['userId'], - actorUserId: (int) $user->id, - periodStart: new DateTimeImmutable($dateRange->startSql()), - periodEnd: new DateTimeImmutable($dateRange->endSql()), - status: (string) ($data['status'] ?? 'draft'), - autoFill: $fillableAuto, - metrics: MetrikaConfig::normalizeMetrics((array) ($data['metrics'] ?? [])), - observationPatients: $this->normalizeObservationPatients((array) ($data['observationPatients'] ?? [])), - unwantedEvents: $this->normalizeUnwantedEvents((array) ($data['unwantedEvents'] ?? [])), - reportId: isset($data['reportId']) && $data['reportId'] ? (int) $data['reportId'] : null, - createdAt: new DateTimeImmutable((string) $createdAt), - sentAt: new DateTimeImmutable((string) $sentAt), - ); - } - - /** - * @param array> $patients - * @return array> - */ - private function normalizeObservationPatients(array $patients): array - { - return array_values(array_map(static function (array $patient): array { - return [ - 'medical_history_id' => $patient['medical_history_id'] ?? $patient['id'] ?? null, - 'department_patient_id' => $patient['department_patient_id'] ?? null, - 'comment' => $patient['comment'] ?? null, - ]; - }, $patients)); - } - - /** - * @param array> $events - * @return array> - */ - private function normalizeUnwantedEvents(array $events): array - { - return array_values(array_map(static function (array $event): array { - return [ - 'unwanted_event_id' => $event['unwanted_event_id'] ?? null, - 'title' => $event['title'] ?? '', - 'comment' => $event['comment'] ?? '', - 'is_visible' => (bool) ($event['is_visible'] ?? true), - ]; - }, $events)); - } -} diff --git a/app/Infrastructure/Reports/Services/ReportStatisticsReadService.php b/app/Infrastructure/Reports/Services/ReportStatisticsReadService.php deleted file mode 100644 index f24a842..0000000 --- a/app/Infrastructure/Reports/Services/ReportStatisticsReadService.php +++ /dev/null @@ -1,213 +0,0 @@ -contextResolver->resolveBranchId($department); - - if (! $branchId) { - return $this->emptyStatistics(); - } - - if ($this->contextResolver->shouldUseSnapshots($department, $dateRange)) { - return $this->getStatisticsFromSnapshots($department, $dateRange); - } - - return $this->getStatisticsFromReplica($department, $user, $dateRange, $branchId); - } - - /** - * Получить статистику из сохранённых снапшотов submitted-отчётов. - */ - private function getStatisticsFromSnapshots(Department $department, DateRange $dateRange): array - { - $reports = $this->contextResolver->getReportsForDateRange( - $department->department_id, - $dateRange - ); - - $reportIds = $reports->pluck('report_id')->all(); - $lastReportId = $reportIds[0] ?? null; - $recipientReportIds = $this->contextResolver->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->snapshotService - ->getPatientsFromSnapshots('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']), - ]; - } - - /** - * Получить статистику из live-реплики МИС и manual-источников. - */ - 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->patientService->getSurgicalPatients('emergency', $branchId, $dateRange, true), - $this->patientService->getSurgicalPatients('plan', $branchId, $dateRange, true), - ]; - $manualSurgicalCount = $this->calculatedMetricsSynchronizer->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), - ]; - } - - /** - * Получить агрегированное значение метрики из набора отчётов. - * - * @param array $reportIds - */ - 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, - ]; - } -} diff --git a/app/Infrastructure/Reports/Services/ReportStorageService.php b/app/Infrastructure/Reports/Services/ReportStorageService.php deleted file mode 100644 index 251fe5c..0000000 --- a/app/Infrastructure/Reports/Services/ReportStorageService.php +++ /dev/null @@ -1,166 +0,0 @@ - $snapshot->departmentId, - 'rf_user_id' => $actor->id, - 'rf_lpudoctor_id' => $snapshot->userId, - 'sent_at' => $snapshot->sentAt?->format('Y-m-d H:i:s') ?? $snapshot->periodEnd->format('Y-m-d H:i:s'), - 'period_start' => $snapshot->periodStart->format('Y-m-d H:i:s'), - 'period_end' => $snapshot->periodEnd->format('Y-m-d H:i:s'), - 'created_at' => $snapshot->createdAt?->format('Y-m-d H:i:s') ?? $snapshot->periodEnd->format('Y-m-d H:i:s'), - 'status' => $snapshot->status, - ]; - - if ($snapshot->reportId) { - return Report::query()->updateOrCreate( - ['report_id' => $snapshot->reportId], - $reportData, - ); - } - - $report = Report::query()->create($reportData); - $department = Department::query()->find($snapshot->departmentId); - $beds = $department?->metrikaDefault->where('rf_metrika_item_id', MetrikaConfig::BEDS)->first(); - - if ($beds) { - MetrikaResult::query()->updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::BEDS, - ], - ['value' => $beds->value] - ); - } - - return $report; - } - - public function saveMetrics(Report $report, ReportSnapshot $snapshot): void - { - foreach ($snapshot->normalizedMetrics() as $metricId => $value) { - MetrikaResult::query()->updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => $metricId, - ], - ['value' => $value] - ); - } - } - - public function saveMetric(Report $report, int $metricId, int|float $value): void - { - MetrikaResult::query()->updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => $metricId, - ], - ['value' => $value] - ); - } - - public function saveUnwantedEvents(Report $report, ReportSnapshot $snapshot): void - { - if ($snapshot->unwantedEvents === []) { - $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, - $snapshot->unwantedEvents - )))) - ->delete(); - - foreach ($snapshot->unwantedEvents as $event) { - if (! empty($event['unwanted_event_id'])) { - UnwantedEvent::query()->updateOrCreate( - ['unwanted_event_id' => (int) $event['unwanted_event_id']], - [ - 'rf_report_id' => $report->report_id, - 'comment' => (string) ($event['comment'] ?? ''), - 'title' => (string) ($event['title'] ?? ''), - 'is_visible' => (bool) ($event['is_visible'] ?? true), - ] - ); - - continue; - } - - UnwantedEvent::query()->create([ - 'rf_report_id' => $report->report_id, - 'comment' => (string) ($event['comment'] ?? ''), - 'title' => (string) ($event['title'] ?? ''), - 'is_visible' => (bool) ($event['is_visible'] ?? true), - ]); - } - - $this->saveMetric($report, MetrikaConfig::UNWANTED_EVENTS, count($snapshot->unwantedEvents)); - } - - public function saveObservationPatients(Report $report, ReportSnapshot $snapshot): void - { - if ($snapshot->observationPatients === []) { - ObservationPatient::query() - ->where('rf_department_id', $snapshot->departmentId) - ->where('rf_report_id', $report->report_id) - ->delete(); - $this->saveMetric($report, MetrikaConfig::OBSERVATION, 0); - - return; - } - - $observedKeys = []; - - foreach ($snapshot->observationPatients 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' => $snapshot->departmentId, - ], - [ - 'rf_report_id' => $report->report_id, - 'rf_mkab_id' => null, - 'comment' => $patient['comment'] ?? null, - ] - ); - } - - ObservationPatient::query() - ->where('rf_department_id', $snapshot->departmentId) - ->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($snapshot->observationPatients)); - } -} diff --git a/app/Infrastructure/Reports/Services/SnapshotPersistenceService.php b/app/Infrastructure/Reports/Services/SnapshotPersistenceService.php deleted file mode 100644 index a504b8d..0000000 --- a/app/Infrastructure/Reports/Services/SnapshotPersistenceService.php +++ /dev/null @@ -1,80 +0,0 @@ - $metrics - */ - public function saveMetrics(Report $report, array $metrics): void - { - foreach ($metrics as $metrikaItemId => $value) { - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => $metrikaItemId, - ], - [ - 'value' => $value, - ] - ); - } - } - - public function createSnapshotsForType(Report $report, string $type, Collection $patients): void - { - foreach ($patients as $patient) { - if (! $patient instanceof UnifiedPatientData) { - continue; - } - - MedicalHistorySnapshot::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'patient_uid' => $patient->patientUid, - 'patient_type' => $type, - ], - [ - 'rf_report_id' => $report->report_id, - 'patient_type' => $type, - ...$patient->toSnapshotPayload($type), - ] - ); - } - } - - /** - * Удалить ранее построенные снапшоты перед полной перестройкой состояния отчёта. - */ - public function clearReportSnapshots(Report $report): void - { - MedicalHistorySnapshot::query() - ->where('rf_report_id', $report->report_id) - ->delete(); - } - - /** - * Сопоставить типы snapshot-пациентов с идентификаторами сохраняемых метрик. - * - * @return array - */ - public function snapshotMetricMap(): array - { - return [ - 'plan' => MetrikaConfig::PLAN, - 'emergency' => MetrikaConfig::EMERGENCY, - 'discharged' => MetrikaConfig::DISCHARGED, - 'transferred' => MetrikaConfig::TRANSFERRED, - ]; - } -} diff --git a/app/Infrastructure/Reports/Sources/LegacyAutoFillPatientSource.php b/app/Infrastructure/Reports/Sources/LegacyAutoFillPatientSource.php deleted file mode 100644 index b9f9ef5..0000000 --- a/app/Infrastructure/Reports/Sources/LegacyAutoFillPatientSource.php +++ /dev/null @@ -1,42 +0,0 @@ -findOrFail($context->departmentId); - $user = User::query()->findOrFail($context->actorUserId ?? $context->userId); - $scopedUser = clone $user; - $scopedUser->rf_department_id = $department->department_id; - $scopedUser->setRelation('department', $department); - $dateRange = new DateRange( - startDate: Carbon::parse($context->periodStart->format('Y-m-d H:i:s'), 'Asia/Yakutsk'), - endDate: Carbon::parse($context->periodEnd->format('Y-m-d H:i:s'), 'Asia/Yakutsk'), - startDateRaw: $context->periodStart->format('Y-m-d H:i:s'), - endDateRaw: $context->periodEnd->format('Y-m-d H:i:s'), - isOneDay: $context->periodStart->diff($context->periodEnd)->days <= 1, - ); - - return new PatientCollection( - items: [], - metadata: [ - 'payload' => $this->legacyAdapter->buildAutoFillPayload($scopedUser, $department, $dateRange), - ], - ); - } -} diff --git a/app/Infrastructure/Reports/Sources/MisClinicalDataSource.php b/app/Infrastructure/Reports/Sources/MisClinicalDataSource.php index 930a98e..869023b 100644 --- a/app/Infrastructure/Reports/Sources/MisClinicalDataSource.php +++ b/app/Infrastructure/Reports/Sources/MisClinicalDataSource.php @@ -6,11 +6,13 @@ use App\Services\DateRange; use App\Services\OutcomePatientService; use App\Services\RecipientPatientService; use App\Services\CurrentPatientService; +use App\Models\MedicalHistory; use App\Models\MisMedicalHistory; use App\Models\MisMigrationPatient; use App\Models\MisReanimation; use App\Models\MisSurgicalOperation; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Schema; /** * Query-источник для пациентских выборок из МИС и связанных клинических агрегатов. @@ -70,7 +72,10 @@ class MisClinicalDataSource return $this->buildPatientCardsQuery($medicalHistoryIds, $branchId) ->get() ->map(function ($patient) use ($recipientIds) { - $patient->is_recipient_today = in_array($patient->MedicalHistoryID, $recipientIds, true); + $patientId = $patient instanceof MedicalHistory + ? (int) ($patient->original_id ?? $patient->id) + : (int) $patient->MedicalHistoryID; + $patient->is_recipient_today = in_array($patientId, $recipientIds, true); return $patient; }); @@ -94,7 +99,9 @@ class MisClinicalDataSource $currentIds = $fillableAuto ? $this->currentPatientService->getHistoricalCurrentMedicalHistoryIds(null, $branchId, $dateRange) - : MisMigrationPatient::currentlyInTreatment($branchId)->pluck('rf_MedicalHistoryID')->toArray(); + : ($this->useMaterializedViews() + ? $this->currentPatientService->getCurrentMedicalHistoryIds('all', $branchId, $dateRange, false) + : MisMigrationPatient::currentlyInTreatment($branchId)->pluck('rf_MedicalHistoryID')->toArray()); $allIds = array_unique(array_merge($recipientIds, $currentIds)); @@ -113,7 +120,10 @@ class MisClinicalDataSource return $this->buildPatientCardsQuery($allIds, $branchId) ->get() ->map(function ($patient) use ($recipientIds) { - $patient->is_recipient_today = in_array($patient->MedicalHistoryID, $recipientIds, true); + $patientId = $patient instanceof MedicalHistory + ? (int) ($patient->original_id ?? $patient->id) + : (int) $patient->MedicalHistoryID; + $patient->is_recipient_today = in_array($patientId, $recipientIds, true); return $patient; }); @@ -169,20 +179,42 @@ class MisClinicalDataSource $reanimationDateByMedicalHistory = $reanimationByMedicalHistory->pluck('reanimation_date_in', 'medical_history_id'); $reanimationCompleteByMedicalHistory = $reanimationByMedicalHistory->pluck('reanimation_is_complete', 'medical_history_id'); - return MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds) - ->select($this->patientSelect()) - ->with($this->patientRelations($branchId)) - ->orderBy('DateRecipient', 'DESC') - ->get() - ->map(function ($patient) use ($reanimationDateByMedicalHistory, $reanimationCompleteByMedicalHistory) { - $reanimationDateIn = $reanimationDateByMedicalHistory->get($patient->MedicalHistoryID); - if ($reanimationDateIn) { + $patients = $this->useMaterializedViews() + ? MedicalHistory::query() + ->whereIn('original_id', $medicalHistoryIds) + ->with([ + 'operations', + 'latestMigration' => fn ($query) => $query->where('stationar_branch_id', $branchId), + 'migrations' => fn ($query) => $query + ->where('stationar_branch_id', $branchId) + ->orderByDesc('ingoing_date'), + ]) + ->orderByDesc('recipient_date') + ->get() + : MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds) + ->select($this->patientSelect()) + ->with($this->patientRelations($branchId)) + ->orderBy('DateRecipient', 'DESC') + ->get(); + + return $patients->map(function ($patient) use ($reanimationDateByMedicalHistory, $reanimationCompleteByMedicalHistory) { + $patientId = $patient instanceof MedicalHistory + ? (int) ($patient->original_id ?? $patient->id) + : (int) $patient->MedicalHistoryID; + $reanimationDateIn = $reanimationDateByMedicalHistory->get($patientId); + + if ($reanimationDateIn) { + if ($patient instanceof MedicalHistory) { + $patient->recipient_date = $reanimationDateIn; + } else { $patient->DateRecipient = $reanimationDateIn; } - $patient->reanimation_is_complete = (bool) $reanimationCompleteByMedicalHistory->get($patient->MedicalHistoryID, false); + } - return $patient; - }); + $patient->reanimation_is_complete = (bool) $reanimationCompleteByMedicalHistory->get($patientId, false); + + return $patient; + }); } /** @@ -233,6 +265,19 @@ class MisClinicalDataSource */ public function buildPatientCardsQuery(array $medicalHistoryIds, int $branchId) { + if ($this->useMaterializedViews()) { + return MedicalHistory::query() + ->whereIn('original_id', $medicalHistoryIds) + ->with([ + 'operations', + 'latestMigration' => fn ($query) => $query->where('stationar_branch_id', $branchId), + 'migrations' => fn ($query) => $query + ->where('stationar_branch_id', $branchId) + ->orderByDesc('ingoing_date'), + ]) + ->orderByDesc('recipient_date'); + } + return MisMedicalHistory::query() ->whereIn('MedicalHistoryID', $medicalHistoryIds) ->select($this->patientSelect()) @@ -324,4 +369,9 @@ class MisClinicalDataSource }, ]; } + + private function useMaterializedViews(): bool + { + return Schema::hasTable('mv_medicalhistory_summary') && Schema::hasTable('mv_migrationpatient_details'); + } } diff --git a/app/Infrastructure/Reports/Sources/MisPatientSource.php b/app/Infrastructure/Reports/Sources/MisPatientSource.php index 97680f0..52a1b3a 100644 --- a/app/Infrastructure/Reports/Sources/MisPatientSource.php +++ b/app/Infrastructure/Reports/Sources/MisPatientSource.php @@ -3,6 +3,7 @@ namespace App\Infrastructure\Reports\Sources; use App\Data\UnifiedPatientData; +use App\Models\MedicalHistory; use App\Models\User; use App\Services\DateRange; use App\Services\PatientService; @@ -27,10 +28,15 @@ class MisPatientSource bool $forSnapshots = false ): Collection { return $this->getPatients($user, $status, $dateRange, $branchId, $includeCurrent, $fillableAuto, $forSnapshots) - ->map(fn ($patient) => UnifiedPatientData::fromMisMedicalHistory( - $patient, - (bool) ($patient->is_recipient_today ?? false), - )) + ->map(fn ($patient) => $patient instanceof MedicalHistory + ? UnifiedPatientData::fromMedicalHistory( + $patient, + (bool) ($patient->is_recipient_today ?? false), + ) + : UnifiedPatientData::fromMisMedicalHistory( + $patient, + (bool) ($patient->is_recipient_today ?? false), + )) ->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '') ->values(); } diff --git a/app/Infrastructure/Reports/Sources/SnapshotPatientSource.php b/app/Infrastructure/Reports/Sources/SnapshotPatientSource.php index 8e83361..632a18a 100644 --- a/app/Infrastructure/Reports/Sources/SnapshotPatientSource.php +++ b/app/Infrastructure/Reports/Sources/SnapshotPatientSource.php @@ -4,8 +4,8 @@ namespace App\Infrastructure\Reports\Sources; use App\Data\UnifiedPatientData; use App\Models\DepartmentPatientOperation; +use App\Models\MedicalHistory; use App\Models\MedicalHistorySnapshot; -use App\Models\MisMedicalHistory; use Illuminate\Support\Collection; class SnapshotPatientSource @@ -181,15 +181,15 @@ class SnapshotPatientSource return []; } - return MisMedicalHistory::query() - ->whereIn('MedicalHistoryID', $historyIds) - ->with(['surgicalOperations.serviceMedical']) + return MedicalHistory::query() + ->whereIn('original_id', $historyIds) + ->with(['operations']) ->get() - ->mapWithKeys(function (MisMedicalHistory $history) { + ->mapWithKeys(function (MedicalHistory $history) { return [ - $history->MedicalHistoryID => $history->surgicalOperations->map(fn ($operation) => [ - 'code' => $operation->serviceMedical?->ServiceMedicalCode, - 'name' => $operation->serviceMedical?->ServiceMedicalName, + $history->original_id => $history->operations->map(fn ($operation) => [ + 'code' => $operation->service_medical_code ?? null, + 'name' => $operation->service_medical_name ?? null, ])->values()->all(), ]; }) diff --git a/app/Models/MedicalHistorySnapshot.php b/app/Models/MedicalHistorySnapshot.php index 6226ad8..18d5b9c 100644 --- a/app/Models/MedicalHistorySnapshot.php +++ b/app/Models/MedicalHistorySnapshot.php @@ -47,7 +47,7 @@ class MedicalHistorySnapshot extends Model public function medicalHistory() { - return $this->belongsTo(MisMedicalHistory::class, 'rf_medicalhistory_id', 'MedicalHistoryID'); + return $this->belongsTo(MedicalHistory::class, 'rf_medicalhistory_id', 'original_id'); } public function departmentPatient() @@ -68,8 +68,8 @@ class MedicalHistorySnapshot extends Model public function scopeByDepartment($query, $departmentId) { - return $query->whereHas('medicalHistory.migrations.branch', function ($q) use ($departmentId) { - $q->where('rf_DepartmentID', $departmentId); + return $query->whereHas('medicalHistory.migrations', function ($q) use ($departmentId) { + $q->department($departmentId); }); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c6a8767..56f4373 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,16 +2,6 @@ namespace App\Providers; -use App\Application\Reports\CompareLegacyAndNewReportUseCase; -use App\Application\Reports\GenerateReportUseCase; -use App\Application\Reports\ReportInputFactory; -use App\Domain\Reports\Contracts\AuditLogger; -use App\Domain\Reports\Contracts\PatientSource; -use App\Domain\Reports\Contracts\ReportRepository; -use App\Infrastructure\Reports\Adapters\LegacyReportServiceAdapter; -use App\Infrastructure\Reports\Logging\ReportsAuditLogger; -use App\Infrastructure\Reports\Repositories\EloquentReportRepository; -use App\Infrastructure\Reports\Sources\LegacyAutoFillPatientSource; use App\Services\Cache\CacheInvalidator; use App\Services\Cache\CacheKeyBuilder; use Illuminate\Support\ServiceProvider; @@ -28,26 +18,6 @@ class AppServiceProvider extends ServiceProvider }); $this->app->singleton(CacheInvalidator::class); - $this->app->singleton(ReportRepository::class, EloquentReportRepository::class); - $this->app->singleton(AuditLogger::class, ReportsAuditLogger::class); - $this->app->singleton(PatientSource::class, LegacyAutoFillPatientSource::class); - $this->app->singleton(CompareLegacyAndNewReportUseCase::class, function ($app) { - return new CompareLegacyAndNewReportUseCase( - $app->make(ReportRepository::class), - $app->make(LegacyReportServiceAdapter::class), - ); - }); - $this->app->singleton(ReportInputFactory::class); - $this->app->singleton(GenerateReportUseCase::class, function ($app) { - return new GenerateReportUseCase( - reportRepository: $app->make(ReportRepository::class), - auditLogger: $app->make(AuditLogger::class), - comparator: $app->make(CompareLegacyAndNewReportUseCase::class), - patientSource: $app->make(PatientSource::class), - calculators: [], - compareBeforeCutover: (bool) config('reports.use_new_arch.compare_before_cutover', true), - ); - }); } /** diff --git a/app/Services/AutoReportService.php b/app/Services/AutoReportService.php index 0d966c0..a738907 100644 --- a/app/Services/AutoReportService.php +++ b/app/Services/AutoReportService.php @@ -2,7 +2,6 @@ namespace App\Services; -use App\Application\Reports\ReportSavePathService; use App\Models\Department; use App\Models\MedicalHistorySnapshot; use App\Models\MetrikaResult; @@ -19,13 +18,12 @@ class AutoReportService public function __construct( mixed $dateRangeService = null, mixed $reportSavePathService = null, - ?ReportSavePathService $legacyReportSavePathService = null, ) { if ($dateRangeService instanceof ReportService) { $this->dateRangeService = $reportSavePathService instanceof DateRangeService ? $reportSavePathService : app(DateRangeService::class); - $this->reportSavePathService = $legacyReportSavePathService ?? app(ReportSavePathService::class); + $this->reportService = $dateRangeService; return; } @@ -33,12 +31,14 @@ class AutoReportService $this->dateRangeService = $dateRangeService instanceof DateRangeService ? $dateRangeService : app(DateRangeService::class); - $this->reportSavePathService = $reportSavePathService ?? app(ReportSavePathService::class); + $this->reportService = $reportSavePathService instanceof ReportService + ? $reportSavePathService + : app(ReportService::class); } protected DateRangeService $dateRangeService; - protected ReportSavePathService $reportSavePathService; + protected ReportService $reportService; /** * Заполнить отчеты для пользователя за период @@ -96,9 +96,8 @@ class AutoReportService $this->deleteExistingReport($existingReport); } - DB::transaction(function () use ($scopedUser, $department, $dateRange) { - $this->reportSavePathService->saveAutoFill($scopedUser, $department, $dateRange); - }); + $payload = $this->reportService->buildAutoFillReportPayload($scopedUser, $department, $dateRange); + $this->reportService->storeReport($payload, $scopedUser, true); return true; } diff --git a/app/Services/CurrentPatientService.php b/app/Services/CurrentPatientService.php index 3d621d0..20a378a 100644 --- a/app/Services/CurrentPatientService.php +++ b/app/Services/CurrentPatientService.php @@ -3,7 +3,9 @@ namespace App\Services; use App\Models\MisMigrationPatient; +use App\Models\MedicalHistory; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; class CurrentPatientService { @@ -13,6 +15,20 @@ class CurrentPatientService public function getCurrentMedicalHistoryIds(string $type, int $branchId, DateRange $dateRange, bool $fillableAuto = false) { + if (! $fillableAuto && $this->useMaterializedViews()) { + return MedicalHistory::query() + ->when($type === 'plan', fn ($query) => $query->urgency(1)) + ->when($type === 'emergency', fn ($query) => $query->whereIn('urgency_id', self::EMERGENCY_STATUSES)) + ->whereHas('migrations', fn ($query) => $query + ->where('stationar_branch_id', $branchId) + ->current($dateRange)) + ->pluck('original_id') + ->filter() + ->map(fn ($id) => (int) $id) + ->values() + ->all(); + } + if ($fillableAuto) { return $this->getHistoricalCurrentMedicalHistoryIds($type, $branchId, $dateRange); } @@ -79,4 +95,9 @@ class CurrentPatientService ->pluck('last_mp.rf_MedicalHistoryID') ->toArray(); } + + private function useMaterializedViews(): bool + { + return Schema::hasTable('mv_medicalhistory_summary') && Schema::hasTable('mv_migrationpatient_details'); + } } diff --git a/app/Services/MetrikaService.php b/app/Services/MetrikaService.php index 61d58c0..041f87d 100644 --- a/app/Services/MetrikaService.php +++ b/app/Services/MetrikaService.php @@ -2,9 +2,10 @@ namespace App\Services; +use App\Models\MedicalHistory; +use App\Models\MedicalHistorySnapshot; use App\Models\Report; use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\DB; class MetrikaService { @@ -21,40 +22,58 @@ class MetrikaService } try { - // Получаем снапшоты с операциями - $results = DB::table('medical_history_snapshots as mhs') - ->join('reports as r', 'mhs.rf_report_id', '=', 'r.report_id') - ->join('stt_migrationpatient as mp', 'mhs.rf_medicalhistory_id', '=', 'mp.rf_MedicalHistoryID') - ->join('stt_surgicaloperation as so', 'mhs.rf_medicalhistory_id', '=', 'so.rf_MedicalHistoryID') - ->whereIn('r.rf_department_id', $departmentIds) - ->where('r.period_start', '>=', $startDate) - ->where('r.period_end', '<', $endDate) - ->whereIn('mhs.patient_type', ['discharged', 'deceased']) - ->select( - 'r.rf_department_id', - 'mp.rf_MedicalHistoryID', - DB::raw('MIN(mp."DateIngoing") as admission_date'), - DB::raw('MIN(so."Date") as first_operation_date') - ) - ->groupBy('r.rf_department_id', 'mp.rf_MedicalHistoryID') - ->havingRaw('MIN(so."Date") IS NOT NULL') - ->get() + $reports = Report::query() + ->whereIn('rf_department_id', $departmentIds) + ->where('period_start', '>=', $startDate) + ->where('period_end', '<', $endDate) + ->get(['report_id', 'rf_department_id']) ->groupBy('rf_department_id'); $preoperativeDays = []; foreach ($departmentIds as $deptId) { - if (! isset($results[$deptId]) || $results[$deptId]->isEmpty()) { + $reportIds = $reports->get($deptId)?->pluck('report_id')->all() ?? []; + + if ($reportIds === []) { $preoperativeDays[$deptId] = 0; continue; } + $historyIds = MedicalHistorySnapshot::query() + ->whereIn('rf_report_id', $reportIds) + ->whereIn('patient_type', ['discharged', 'deceased']) + ->pluck('rf_medicalhistory_id') + ->filter() + ->unique() + ->values(); + + if ($historyIds->isEmpty()) { + $preoperativeDays[$deptId] = 0; + + continue; + } + + $histories = MedicalHistory::query() + ->whereIn('original_id', $historyIds) + ->with(['operations']) + ->get(); + $totalDays = 0; $count = 0; - foreach ($results[$deptId] as $item) { - $admission = Carbon::parse($item->admission_date); - $operation = Carbon::parse($item->first_operation_date); + foreach ($histories as $history) { + $operationDate = $history->operations + ->pluck('operation_date') + ->filter() + ->sort() + ->first(); + + if (! $history->recipient_date || ! $operationDate) { + continue; + } + + $admission = Carbon::parse($history->recipient_date); + $operation = Carbon::parse($operationDate); $days = $admission->diffInDays($operation); if ($days >= 0) { diff --git a/app/Services/OutcomePatientService.php b/app/Services/OutcomePatientService.php index abcf5fc..7706844 100644 --- a/app/Services/OutcomePatientService.php +++ b/app/Services/OutcomePatientService.php @@ -2,7 +2,10 @@ namespace App\Services; +use App\Models\MedicalHistory; +use App\Models\MigrationPatient; use App\Models\MisMedicalHistory; +use Illuminate\Support\Facades\Schema; class OutcomePatientService { @@ -57,6 +60,10 @@ class OutcomePatientService string $outcomeType = 'all', bool $onlyIds = false ) { + if ($this->useMaterializedViews()) { + return $this->getOutcomePatientsFromMaterializedViews($branchId, $dateRange, $outcomeType, $onlyIds); + } + return match ($outcomeType) { 'deceased' => $this->getBranchDeceasedPatients($branchId, $dateRange, $onlyIds), 'transferred' => $this->getBranchTransferredPatients($branchId, $dateRange, $onlyIds), @@ -178,4 +185,62 @@ class OutcomePatientService return $this->finalizePatientsQuery($query, $onlyIds); } + + private function getOutcomePatientsFromMaterializedViews( + int $branchId, + DateRange $dateRange, + string $outcomeType, + bool $onlyIds + ) { + $migrationQuery = MigrationPatient::query() + ->where('stationar_branch_id', $branchId); + + $migrationQuery = match ($outcomeType) { + 'deceased' => $migrationQuery->deceased($dateRange->startSql(), $dateRange->endSql()), + 'transferred' => $migrationQuery->transferred($dateRange->startSql(), $dateRange->endSql()), + 'discharged' => $migrationQuery->discharged($dateRange->startSql(), $dateRange->endSql()), + 'without-transferred' => $migrationQuery->where(function ($query) use ($dateRange) { + $query->discharged($dateRange->startSql(), $dateRange->endSql()) + ->orWhere(fn ($orQuery) => $orQuery->deceased($dateRange->startSql(), $dateRange->endSql())); + }), + default => $migrationQuery->where(function ($query) use ($dateRange) { + $query->discharged($dateRange->startSql(), $dateRange->endSql()) + ->orWhere(fn ($orQuery) => $orQuery->deceased($dateRange->startSql(), $dateRange->endSql())) + ->orWhere(fn ($orQuery) => $orQuery->transferred($dateRange->startSql(), $dateRange->endSql())); + }), + }; + + $historyIds = $migrationQuery + ->distinct() + ->pluck('medical_history_id') + ->filter() + ->values() + ->all(); + + if (empty($historyIds)) { + return $onlyIds ? collect() : collect(); + } + + $query = MedicalHistory::query() + ->whereIn('id', $historyIds) + ->with([ + 'latestMigration' => fn ($builder) => $builder->where('stationar_branch_id', $branchId), + 'migrations' => fn ($builder) => $builder + ->where('stationar_branch_id', $branchId) + ->orderByDesc('ingoing_date'), + 'operations', + ]) + ->orderByDesc('recipient_date'); + + if ($onlyIds) { + return $query->pluck('original_id'); + } + + return $query->get(); + } + + private function useMaterializedViews(): bool + { + return Schema::hasTable('mv_medicalhistory_summary') && Schema::hasTable('mv_migrationpatient_details'); + } } diff --git a/app/Services/RecipientPatientService.php b/app/Services/RecipientPatientService.php index f4653d4..9b4e875 100644 --- a/app/Services/RecipientPatientService.php +++ b/app/Services/RecipientPatientService.php @@ -3,6 +3,8 @@ namespace App\Services; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; +use App\Models\MedicalHistory; class RecipientPatientService { @@ -33,6 +35,17 @@ class RecipientPatientService DateRange $dateRange, bool $fillableAuto = false ) { + if (! $fillableAuto && $this->useMaterializedViews()) { + return MedicalHistory::query() + ->selectRaw('original_id as "rf_MedicalHistoryID"') + ->whereHas('migrations', fn ($query) => $query + ->where('stationar_branch_id', $branchId) + ->admitted($dateRange->startSql(), $dateRange->endSql())) + ->when($type === 'plan', fn ($query) => $query->urgency(1)) + ->when($type === 'emergency', fn ($query) => $query->whereIn('urgency_id', [2, 4])) + ->distinct(); + } + $startAt = $dateRange->start()->copy()->format('Y-m-d H:i:s'); $endAt = $dateRange->end()->copy()->format('Y-m-d H:i:s'); @@ -96,4 +109,9 @@ class RecipientPatientService return array_values(array_unique(array_merge($recipientIds, $currentIds))); } + + private function useMaterializedViews(): bool + { + return Schema::hasTable('mv_medicalhistory_summary') && Schema::hasTable('mv_migrationpatient_details'); + } } diff --git a/app/Services/ReportService.php b/app/Services/ReportService.php index 1e82716..245f349 100644 --- a/app/Services/ReportService.php +++ b/app/Services/ReportService.php @@ -2,21 +2,28 @@ namespace App\Services; +use App\Data\UnifiedPatientData; +use App\Domain\Reports\ValueObjects\MetrikaConfig; use App\Infrastructure\Reports\Services\AutoFillReportPayloadBuilder; -use App\Infrastructure\Reports\Services\ManualPatientManagementService; -use App\Infrastructure\Reports\Services\ObservationPatientManagementService; -use App\Infrastructure\Reports\Services\ReanimationIndicatorService; -use App\Infrastructure\Reports\Services\ReportClinicalSearchService; -use App\Infrastructure\Reports\Services\ReportMetadataReadService; -use App\Infrastructure\Reports\Services\ReportPatientsReadService; +use App\Infrastructure\Reports\Services\ReportReadContextResolver; use App\Infrastructure\Reports\Services\ReportRuntimeService; -use App\Infrastructure\Reports\Services\ReportSaveOrchestrator; -use App\Infrastructure\Reports\Services\ReportStatisticsReadService; +use App\Infrastructure\Reports\Sources\SnapshotPatientSource; use App\Models\Department; +use App\Models\DepartmentPatient; use App\Models\DepartmentPatientOperation; +use App\Models\MedicalHistorySnapshot; +use App\Models\MetrikaResult; +use App\Models\MisServiceMedical; +use App\Models\MisLpuDoctor; +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\Collection; +use Illuminate\Support\Facades\DB; +use InvalidArgumentException; class ReportService { @@ -27,55 +34,44 @@ class ReportService protected SnapshotService $snapshotService, protected StatisticsService $statisticsService, ?AutoFillReportPayloadBuilder $autoFillReportPayloadBuilder = null, - ?ReportPatientsReadService $reportPatientsReadService = null, - ?ReportStatisticsReadService $reportStatisticsReadService = null, - ?ManualPatientManagementService $manualPatientManagementService = null, - ?ObservationPatientManagementService $observationPatientManagementService = null, - ?ReanimationIndicatorService $reanimationIndicatorService = null, - ?ReportClinicalSearchService $reportClinicalSearchService = null, - ?ReportMetadataReadService $reportMetadataReadService = null, - ?ReportSaveOrchestrator $reportSaveOrchestrator = null, + ?SnapshotPatientSource $snapshotPatientSource = null, + ?ReportReadContextResolver $reportReadContextResolver = null, ?ReportRuntimeService $reportRuntimeService = null, - protected MetrikaService $metrikaService + ?MetrikaService $metrikaService = null ) { + $this->snapshotPatientSource = $snapshotPatientSource ?? app(SnapshotPatientSource::class); $this->autoFillReportPayloadBuilder = $autoFillReportPayloadBuilder ?? app(AutoFillReportPayloadBuilder::class); - $this->reportPatientsReadService = $reportPatientsReadService ?? app(ReportPatientsReadService::class); - $this->reportStatisticsReadService = $reportStatisticsReadService ?? app(ReportStatisticsReadService::class); - $this->manualPatientManagementService = $manualPatientManagementService ?? app(ManualPatientManagementService::class); - $this->observationPatientManagementService = $observationPatientManagementService ?? app(ObservationPatientManagementService::class); - $this->reanimationIndicatorService = $reanimationIndicatorService ?? app(ReanimationIndicatorService::class); - $this->reportClinicalSearchService = $reportClinicalSearchService ?? app(ReportClinicalSearchService::class); - $this->reportMetadataReadService = $reportMetadataReadService ?? app(ReportMetadataReadService::class); - $this->reportSaveOrchestrator = $reportSaveOrchestrator ?? app(ReportSaveOrchestrator::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 ReportPatientsReadService $reportPatientsReadService; - - protected ReportStatisticsReadService $reportStatisticsReadService; - - protected ManualPatientManagementService $manualPatientManagementService; - - protected ObservationPatientManagementService $observationPatientManagementService; - - protected ReanimationIndicatorService $reanimationIndicatorService; - - protected ReportClinicalSearchService $reportClinicalSearchService; - - protected ReportMetadataReadService $reportMetadataReadService; - - protected ReportSaveOrchestrator $reportSaveOrchestrator; + protected ReportReadContextResolver $reportReadContextResolver; protected ReportRuntimeService $reportRuntimeService; + protected MetrikaService $metrikaService; + /** * Получить статистику для отчета */ public function getReportStatistics(Department $department, User $user, DateRange $dateRange): array { - return $this->reportStatisticsReadService->getReportStatistics($department, $user, $dateRange); + $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); } /** @@ -83,7 +79,55 @@ class ReportService */ public function storeReport(array $data, User $user, $fillableAuto = false): Report { - return $this->reportSaveOrchestrator->storeReport($data, $user, (bool) $fillableAuto); + $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 @@ -93,17 +137,80 @@ class ReportService public function syncCalculatedMetricsForStoredReport(Report $report, User $user, array $data): void { - $this->reportSaveOrchestrator->syncCalculatedMetrics($report, $user, $data); + 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->reportSaveOrchestrator->finalizeStoredReport($report); + $this->saveBedDaysMetrics($report); + $this->savePreoperativeMetrics($report); + $this->saveDepartmentLoadMetric($report); } public function saveLethalMetricForStoredReport(Report $report): void { - $this->reportSaveOrchestrator->saveLethalMetricFromSnapshots($report); + $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 @@ -116,6 +223,83 @@ class ReportService 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; + } + /** * Получить пациентов по статусу */ @@ -128,13 +312,45 @@ class ReportService bool $beforeCreate = false, ?bool $includeCurrentPatients = null ) { - return $this->reportPatientsReadService->getPatientsByStatus( + [$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, - $beforeCreate, $includeCurrentPatients ); } @@ -148,12 +364,77 @@ class ReportService string $status, DateRange $dateRange ): int { - return $this->reportPatientsReadService->getPatientsCountByStatus($department, $user, $status, $dateRange); + [$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 { - return $this->reportPatientsReadService->getPatientsCountsMap($department, $user, $dateRange); + $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; } /** @@ -161,7 +442,52 @@ class ReportService */ public function getCurrentReportInfo(Department $department, User $user, DateRange $dateRange): array { - return $this->reportMetadataReadService->getCurrentReportInfo($department, $user, $dateRange); + $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, + ]; } /** @@ -169,47 +495,151 @@ class ReportService */ public function removeObservationPatient(string $patientId): void { - $this->observationPatientManagementService->removeObservationPatient($patientId); + [$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) { - return $this->manualPatientManagementService->createManualPatient($department, $user, $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) { - return $this->manualPatientManagementService->setManualPatientOutcome($user, $departmentPatientId, $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) { - return $this->manualPatientManagementService->updateManualPatient($user, $departmentPatientId, $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) { - return $this->manualPatientManagementService->linkManualPatientToMis($departmentPatientId, $medicalHistoryId); + $patient = DepartmentPatient::query() + ->where('department_patient_id', $departmentPatientId) + ->firstOrFail(); + + return $this->unifiedPatientService->linkManualPatientToMis($patient, $medicalHistoryId); } public function getManualPatientOperations(User $user, int $departmentPatientId) { - return $this->manualPatientManagementService->getManualPatientOperations($user, $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 { - return $this->manualPatientManagementService->createManualPatientOperation($user, $departmentPatientId, $data); + $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 { - return $this->manualPatientManagementService->updateManualPatientOperation($user, $departmentPatientId, $operationId, $data); + $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 { - $this->manualPatientManagementService->deleteManualPatientOperation($user, $departmentPatientId, $operationId); + $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); + + $patient->operations() + ->where('department_patient_operation_id', $operationId) + ->firstOrFail() + ->delete(); } public function saveReanimationIndicator( @@ -220,19 +650,38 @@ class ReportService ?string $comment = null, ?int $reportId = null ): ReanimationPatientIndicator { - return $this->reanimationIndicatorService->save( - $user, - $departmentId, - $medicalHistoryId, - $indicator, - $comment, - $reportId - ); + 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) { - return $this->reanimationIndicatorService->latestByMedicalHistory($departmentId, $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( @@ -240,12 +689,25 @@ class ReportService int $medicalHistoryId, int $limit = 50 ) { - return $this->reanimationIndicatorService->history($departmentId, $medicalHistoryId, $limit); + 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->reportClinicalSearchService->searchMisPatientsForDepartment($department, $query); + return $this->unifiedPatientService->searchMisPatients($department, $query); } /** @@ -258,13 +720,76 @@ class ReportService int $branchId, bool $onlyIds = false ) { - return $this->reportPatientsReadService->getPatientsFromSnapshots( - $department, - $status, - $dateRange, - $branchId, - $onlyIds + [$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); } /** @@ -272,7 +797,23 @@ class ReportService */ public function getUnwantedEvents(Department $department, DateRange $dateRange) { - return $this->reportMetadataReadService->getUnwantedEvents($department, $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'), + ]; + }); } /** @@ -280,7 +821,7 @@ class ReportService */ public function getReportsForDateRange(int $departmentId, DateRange $dateRange) { - return $this->reportMetadataReadService->getReportsForDateRange($departmentId, $dateRange); + return $this->reportReadContextResolver->getReportsForDateRange($departmentId, $dateRange); } /** @@ -288,7 +829,41 @@ class ReportService */ public function getRecipientPlanOfYear(Department $department, DateRange $dateRange): array { - return $this->reportMetadataReadService->getRecipientPlanOfYear($department, $dateRange); + $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) @@ -299,15 +874,804 @@ class ReportService private function resolveReport(int $departmentId, DateRange $dateRange) { - $query = Report::query() - ->where('rf_department_id', $departmentId) - ->exactPeriod($dateRange->startSql(), $dateRange->endSql()) - ->orderByDesc('report_id'); + return $this->reportReadContextResolver->resolveReportForPeriod($departmentId, $dateRange); + } - if ($dateRange->isOneDay) { - return $query->first(); + 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'); } - return $query->onlySubmitted()->first(); + $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(); } } diff --git a/app/Services/SnapshotService.php b/app/Services/SnapshotService.php index cfbdcf7..a4ea210 100644 --- a/app/Services/SnapshotService.php +++ b/app/Services/SnapshotService.php @@ -2,226 +2,22 @@ namespace App\Services; -use App\Domain\Reports\ValueObjects\MetrikaConfig; -use App\Infrastructure\Reports\Services\SnapshotPersistenceService; use App\Infrastructure\Reports\Sources\SnapshotPatientSource; -use App\Models\Department; -use App\Models\MedicalHistorySnapshot; -use App\Models\MisStationarBranch; -use App\Models\Report; -use App\Models\User; use Illuminate\Support\Collection; class SnapshotService { public function __construct( - protected UnifiedPatientService $unifiedPatientService, - protected PatientService $patientService, - protected DateRangeService $dateRangeService, - ?SnapshotPersistenceService $snapshotPersistenceService = null, + mixed $unifiedPatientService = null, + mixed $patientService = null, + mixed $dateRangeService = null, ?SnapshotPatientSource $snapshotPatientSource = null, ) { - $this->snapshotPersistenceService = $snapshotPersistenceService ?? app(SnapshotPersistenceService::class); $this->snapshotPatientSource = $snapshotPatientSource ?? app(SnapshotPatientSource::class); } - protected SnapshotPersistenceService $snapshotPersistenceService; - protected SnapshotPatientSource $snapshotPatientSource; - /** - * Создать снапшоты пациентов для отчета - */ - public function createPatientSnapshots(Report $report, User $user, array $dates, $fillableAuto = false): void - { - $this->logSnapshotMemory('snapshots:start', [ - 'report_id' => $report->report_id, - 'department_id' => $report->rf_department_id, - 'fillable_auto' => (bool) $fillableAuto, - ]); - - $department = Department::query()->where('department_id', $report->rf_department_id)->first() ?? $user->department; - $branchId = $department - ? $this->getBranchId($department->rf_mis_department_id) - : null; - - if (! $department || ! $branchId) { - return; - } - - $this->snapshotPersistenceService->clearReportSnapshots($report); - $this->logSnapshotMemory('snapshots:after_delete_old', [ - 'report_id' => $report->report_id, - ]); - - [$startDate, $endDate] = $this->parseDates($dates); - $dateRange = $this->dateRangeService->getNormalizedDateRange($user, $startDate, $endDate); - $metrics = []; - - $this->logSnapshotMemory('snapshots:before_plan_load', ['report_id' => $report->report_id]); - $planPatients = $this->unifiedPatientService->getLivePatientsByStatus( - $department, - $user, - 'plan', - $dateRange, - $branchId, - false, - ! $fillableAuto, - $fillableAuto, - true - ); - $this->logSnapshotMemory('snapshots:after_plan_load', [ - 'report_id' => $report->report_id, - 'count' => $planPatients->count(), - ]); - $this->snapshotPersistenceService->createSnapshotsForType($report, 'plan', $planPatients); - $this->logSnapshotMemory('snapshots:after_plan_save', ['report_id' => $report->report_id]); - $metrics[MetrikaConfig::PLAN] = $planPatients->count(); - - $this->logSnapshotMemory('snapshots:before_emergency_load', ['report_id' => $report->report_id]); - $emergencyPatients = $this->unifiedPatientService->getLivePatientsByStatus( - $department, - $user, - 'emergency', - $dateRange, - $branchId, - false, - ! $fillableAuto, - $fillableAuto, - true - ); - $this->logSnapshotMemory('snapshots:after_emergency_load', [ - 'report_id' => $report->report_id, - 'count' => $emergencyPatients->count(), - ]); - $this->snapshotPersistenceService->createSnapshotsForType($report, 'emergency', $emergencyPatients); - $this->logSnapshotMemory('snapshots:after_emergency_save', ['report_id' => $report->report_id]); - $metrics[MetrikaConfig::EMERGENCY] = $emergencyPatients->count(); - - $this->logSnapshotMemory('snapshots:before_discharged_load', ['report_id' => $report->report_id]); - $dischargedPatients = $this->unifiedPatientService->getLivePatientsByStatus( - $department, - $user, - 'outcome-discharged', - $dateRange, - $branchId, - false, - null, - $fillableAuto, - true - ); - $this->logSnapshotMemory('snapshots:after_discharged_load', [ - 'report_id' => $report->report_id, - 'count' => $dischargedPatients->count(), - ]); - $this->snapshotPersistenceService->createSnapshotsForType($report, 'discharged', $dischargedPatients); - $this->logSnapshotMemory('snapshots:after_discharged_save', ['report_id' => $report->report_id]); - $metrics[MetrikaConfig::DISCHARGED] = $dischargedPatients->count(); - - $this->logSnapshotMemory('snapshots:before_transferred_load', ['report_id' => $report->report_id]); - $transferredPatients = $this->unifiedPatientService->getLivePatientsByStatus( - $department, - $user, - 'outcome-transferred', - $dateRange, - $branchId, - false, - null, - $fillableAuto, - true - ); - $this->logSnapshotMemory('snapshots:after_transferred_load', [ - 'report_id' => $report->report_id, - 'count' => $transferredPatients->count(), - ]); - $this->snapshotPersistenceService->createSnapshotsForType($report, 'transferred', $transferredPatients); - $this->logSnapshotMemory('snapshots:after_transferred_save', ['report_id' => $report->report_id]); - $metrics[MetrikaConfig::TRANSFERRED] = $transferredPatients->count(); - - $this->logSnapshotMemory('snapshots:before_deceased_load', ['report_id' => $report->report_id]); - $deceasedPatients = $this->unifiedPatientService->getLivePatientsByStatus( - $department, - $user, - 'outcome-deceased', - $dateRange, - $branchId, - false, - null, - $fillableAuto, - true - ); - $this->logSnapshotMemory('snapshots:after_deceased_load', [ - 'report_id' => $report->report_id, - 'count' => $deceasedPatients->count(), - ]); - $this->snapshotPersistenceService->createSnapshotsForType($report, 'deceased', $deceasedPatients); - $this->logSnapshotMemory('snapshots:after_deceased_save', ['report_id' => $report->report_id]); - - $this->logSnapshotMemory('snapshots:before_recipient_load', ['report_id' => $report->report_id]); - $recipientPatients = $this->unifiedPatientService->getLivePatientsByStatus( - $department, - $user, - 'recipient', - $dateRange, - $branchId, - false, - null, - $fillableAuto, - true - ); - $this->logSnapshotMemory('snapshots:after_recipient_load', [ - 'report_id' => $report->report_id, - 'count' => $recipientPatients->count(), - ]); - $this->snapshotPersistenceService->createSnapshotsForType($report, 'recipient', $recipientPatients); - $this->logSnapshotMemory('snapshots:after_recipient_save', ['report_id' => $report->report_id]); - - $this->logSnapshotMemory('snapshots:before_current_load', ['report_id' => $report->report_id]); - $currentPatients = $this->unifiedPatientService->getLivePatientsByStatus( - $department, - $user, - 'current', - $dateRange, - $branchId, - false, - null, - $fillableAuto, - true - ); - $this->logSnapshotMemory('snapshots:after_current_load', [ - 'report_id' => $report->report_id, - 'count' => $currentPatients->count(), - ]); - $this->snapshotPersistenceService->createSnapshotsForType($report, 'current', $currentPatients); - $this->logSnapshotMemory('snapshots:after_current_save', ['report_id' => $report->report_id]); - - $planSurgeryCount = $this->patientService->getSurgicalPatients( - 'plan', - $branchId, - $dateRange, - true - ); - $emergencySurgeryCount = $this->patientService->getSurgicalPatients( - 'emergency', - $branchId, - $dateRange, - true - ); - - $this->snapshotPersistenceService->saveMetrics($report, $metrics); - $this->logSnapshotMemory('snapshots:after_save_metrics', ['report_id' => $report->report_id]); - } - - private function logSnapshotMemory(string $stage, array $context = []): void - { - \Log::info('report.snapshot.memory', [ - 'stage' => $stage, - 'memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2), - 'peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2), - ...$context, - ]); - } - /** * Получить статистику из снапшотов */ @@ -264,23 +60,4 @@ class SnapshotService ); } - /** - * Получить ID отделения - */ - private function getBranchId(int $misDepartmentId): ?int - { - return MisStationarBranch::where('rf_DepartmentID', $misDepartmentId) - ->value('StationarBranchID'); - } - - /** - * Разобрать даты - */ - private function parseDates(array $dates): array - { - return [ - $this->dateRangeService->parseDate($dates[0]), - $this->dateRangeService->parseDate($dates[1]), - ]; - } } diff --git a/app/Services/UnifiedPatientService.php b/app/Services/UnifiedPatientService.php index 65d8fcc..ba396f4 100644 --- a/app/Services/UnifiedPatientService.php +++ b/app/Services/UnifiedPatientService.php @@ -7,6 +7,7 @@ use App\Infrastructure\Reports\Sources\MisPatientSource; use App\Infrastructure\Reports\Sources\SpecialPatientSource; use App\Models\Department; use App\Models\DepartmentPatient; +use App\Models\MedicalHistory; use App\Models\MisMedicalHistory; use App\Models\ObservationPatient; use App\Models\Report; @@ -192,13 +193,15 @@ class UnifiedPatientService public function linkManualPatientToMis(DepartmentPatient $patient, int $medicalHistoryId): DepartmentPatient { - $misPatient = MisMedicalHistory::where('MedicalHistoryID', $medicalHistoryId)->firstOrFail(); + $history = MedicalHistory::query() + ->where('original_id', $medicalHistoryId) + ->firstOrFail(); $patient->update([ - 'rf_medicalhistory_id' => $misPatient->MedicalHistoryID, + 'rf_medicalhistory_id' => $history->original_id, 'linked_to_mis_at' => now(), - 'full_name' => $patient->full_name ?: trim("{$misPatient->FAMILY} {$misPatient->Name} {$misPatient->OT}"), - 'birth_date' => $patient->birth_date ?: $misPatient->BD, + 'full_name' => $patient->full_name ?: $history->full_name, + 'birth_date' => $patient->birth_date ?: $history->birth_date, ]); return $patient->fresh(); @@ -206,20 +209,13 @@ class UnifiedPatientService public function searchMisPatients(Department $department, string $query): Collection { - $branchId = \App\Models\MisStationarBranch::where('rf_DepartmentID', $department->rf_mis_department_id) - ->value('StationarBranchID'); - - return MisMedicalHistory::query() - ->whereHas('migrations', fn ($builder) => $builder->where('rf_StationarBranchID', $branchId)) - ->where(function ($builder) use ($query) { - $builder->where('FAMILY', 'like', "%{$query}%") - ->orWhere('Name', 'like', "%{$query}%") - ->orWhere('OT', 'like', "%{$query}%"); - }) - ->with(['outcomeMigration.mainDiagnosis.mkb']) + return MedicalHistory::query() + ->whereLike('full_name', "%{$query}%") + ->whereHas('migrations', fn ($builder) => $builder->department($department->rf_mis_department_id)) + ->with(['latestMigration', 'operations']) ->limit(20) ->get() - ->map(fn (MisMedicalHistory $patient) => UnifiedPatientData::fromMisMedicalHistory($patient)); + ->map(fn (MedicalHistory $patient) => UnifiedPatientData::fromMedicalHistory($patient)); } public function getObservationPatients( @@ -232,10 +228,11 @@ class UnifiedPatientService $misIds = $observationPatients->pluck('rf_medicalhistory_id')->filter()->unique()->values(); $manualIds = $observationPatients->pluck('rf_department_patient_id')->filter()->unique()->values(); - $misPatients = MisMedicalHistory::whereIn('MedicalHistoryID', $misIds) - ->with(['outcomeMigration.mainDiagnosis.mkb']) + $misPatients = MedicalHistory::query() + ->whereIn('original_id', $misIds) + ->with(['latestMigration', 'operations']) ->get() - ->keyBy('MedicalHistoryID'); + ->keyBy('original_id'); $manualPatients = DepartmentPatient::whereIn('department_patient_id', $manualIds)->get()->keyBy('department_patient_id'); @@ -258,7 +255,7 @@ class UnifiedPatientService return null; } - return UnifiedPatientData::fromMisMedicalHistory( + return UnifiedPatientData::fromMedicalHistory( $misPatients[$observation->rf_medicalhistory_id], false, null, @@ -290,14 +287,24 @@ class UnifiedPatientService $linkedManualPatients = $this->specialPatientSource->getLinkedManualPatientsForPeriod($department, $dateRange); $mergedMisPatients = $misPatients->map(function ($patient) use ($linkedManualPatients) { - $linkedManual = $linkedManualPatients->get($patient->MedicalHistoryID); + $medicalHistoryId = $patient instanceof MedicalHistory + ? ($patient->original_id ?? $patient->id) + : $patient->MedicalHistoryID; + $linkedManual = $linkedManualPatients->get($medicalHistoryId); - return UnifiedPatientData::fromMisMedicalHistory( - $patient, - (bool) ($patient->is_recipient_today ?? false), - $linkedManual, - $this->resolveObservationComment($patient->MedicalHistoryID, null) - ); + return $patient instanceof MedicalHistory + ? UnifiedPatientData::fromMedicalHistory( + $patient, + (bool) ($patient->is_recipient_today ?? false), + $linkedManual, + $this->resolveObservationComment($medicalHistoryId, null) + ) + : UnifiedPatientData::fromMisMedicalHistory( + $patient, + (bool) ($patient->is_recipient_today ?? false), + $linkedManual, + $this->resolveObservationComment($medicalHistoryId, null) + ); }); $manualDtos = $this->specialPatientSource->getDtos($department, $status, $dateRange, self::SPECIAL_SOURCE_TYPES, $forSnapshots);