Files
onboard/app/Infrastructure/Reports/Services/ReportPatientsReadService.php

399 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

<?php
namespace App\Infrastructure\Reports\Services;
use App\Data\UnifiedPatientData;
use App\Models\Department;
use App\Models\MedicalHistorySnapshot;
use App\Models\User;
use App\Services\DateRange;
use App\Services\SnapshotService;
use App\Services\UnifiedPatientService;
use Illuminate\Support\Collection;
/**
* Сервис чтения пациентских выборок для отчётов.
*
* Он решает, откуда обслуживать запрос: из submitted-снапшотов или из live-реплики,
* и при этом сохраняет API близким к старым методам ReportService для
* постепенной strangler-миграции.
*/
class ReportPatientsReadService
{
public function __construct(
private readonly UnifiedPatientService $unifiedPatientService,
private readonly SnapshotService $snapshotService,
private readonly ReportReadContextResolver $contextResolver,
) {}
/**
* Получить пациентов отчёта по запрошенному статусу и области источника.
*/
public function getPatientsByStatus(
Department $department,
User $user,
string $status,
DateRange $dateRange,
bool $onlyIds = false,
bool $beforeCreate = false,
?bool $includeCurrentPatients = null
) {
[$baseStatus, $sourceScope] = $this->parseScopedStatus($status);
$branchId = $this->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,
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'];
}
}