399 lines
14 KiB
PHP
399 lines
14 KiB
PHP
<?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'];
|
||
}
|
||
}
|