Перевод на доменную архитектуру

This commit is contained in:
brusnitsyn
2026-04-26 23:37:50 +09:00
parent 75ca01ffd8
commit f107ebd167
70 changed files with 4656 additions and 2070 deletions

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Infrastructure\Reports\Sources;
use App\Domain\Reports\Contracts\PatientSource;
use App\Domain\Reports\Models\PatientCollection;
use App\Domain\Reports\Models\ReportContext;
use App\Models\Department;
use App\Models\User;
use App\Services\DateRange;
use App\Infrastructure\Reports\Adapters\LegacyReportServiceAdapter;
use Carbon\Carbon;
final readonly class LegacyAutoFillPatientSource implements PatientSource
{
public function __construct(
private LegacyReportServiceAdapter $legacyAdapter,
) {}
public function load(ReportContext $context): PatientCollection
{
$department = Department::query()->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),
],
);
}
}

View File

@@ -0,0 +1,323 @@
<?php
namespace App\Infrastructure\Reports\Sources;
use App\Services\DateRange;
use App\Services\OutcomePatientService;
use App\Services\RecipientPatientService;
use App\Services\CurrentPatientService;
use App\Models\MisMedicalHistory;
use App\Models\MisMigrationPatient;
use App\Models\MisReanimation;
use App\Models\MisSurgicalOperation;
use Illuminate\Support\Collection;
/**
* Query-источник для пациентских выборок из МИС и связанных клинических агрегатов.
*
* Этот класс удерживает детали Eloquent/query-builder вне application/domain слоёв
* и централизует read-model логику, специфичную для реплики МИС.
*/
class MisClinicalDataSource
{
public function __construct(
private readonly RecipientPatientService $recipientPatientService,
private readonly CurrentPatientService $currentPatientService,
private readonly OutcomePatientService $outcomePatientService,
) {}
/**
* Загрузить плановых или экстренных пациентов, при необходимости объединив с текущими в отделении.
*/
public function getPlanOrEmergencyPatients(
?string $type,
bool $isHeadOrAdmin,
int $branchId,
DateRange $dateRange,
bool $countOnly = false,
bool $onlyIds = false,
bool $includeCurrent = false,
bool $fillableAuto = false
) {
$medicalHistoryIds = $this->recipientPatientService->resolvePlanOrEmergencyMedicalHistoryIds(
$type,
$isHeadOrAdmin,
$branchId,
$dateRange,
$includeCurrent,
$fillableAuto
);
if (empty($medicalHistoryIds)) {
return $countOnly ? 0 : collect();
}
if ($countOnly) {
return count($medicalHistoryIds);
}
if ($onlyIds) {
return collect($medicalHistoryIds);
}
$recipientIds = $this->recipientPatientService->getRecipientMedicalHistoryIds(
$type,
$isHeadOrAdmin,
$branchId,
$dateRange
);
return $this->buildPatientCardsQuery($medicalHistoryIds, $branchId)
->get()
->map(function ($patient) use ($recipientIds) {
$patient->is_recipient_today = in_array($patient->MedicalHistoryID, $recipientIds, true);
return $patient;
});
}
/**
* Загрузить всех MIS-пациентов, входящих в выборку отчётного отделения.
*/
public function getAllPatientsInDepartment(
bool $isHeadOrAdmin,
int $branchId,
DateRange $dateRange,
bool $countOnly = false,
bool $onlyIds = false,
bool $fillableAuto = false
) {
$recipientIds = $this->recipientPatientService
->buildRecipientQuery(null, $isHeadOrAdmin, $branchId, $dateRange, $fillableAuto)
->pluck('rf_MedicalHistoryID')
->toArray();
$currentIds = $fillableAuto
? $this->currentPatientService->getHistoricalCurrentMedicalHistoryIds(null, $branchId, $dateRange)
: MisMigrationPatient::currentlyInTreatment($branchId)->pluck('rf_MedicalHistoryID')->toArray();
$allIds = array_unique(array_merge($recipientIds, $currentIds));
if (empty($allIds)) {
return $countOnly ? 0 : collect();
}
if ($countOnly) {
return count($allIds);
}
if ($onlyIds) {
return collect($allIds);
}
return $this->buildPatientCardsQuery($allIds, $branchId)
->get()
->map(function ($patient) use ($recipientIds) {
$patient->is_recipient_today = in_array($patient->MedicalHistoryID, $recipientIds, true);
return $patient;
});
}
/**
* Загрузить пациентов с записями о реанимации, входящих в отчётную выборку.
*/
public function getReanimationPatients(
int $branchId,
DateRange $dateRange,
bool $onlyIds = false
) {
$currentIds = $this->getAllPatientsInDepartment(true, $branchId, $dateRange, false, true, false)->all();
$outcomeIds = $this->outcomePatientService->getOutcomePatients($branchId, $dateRange, 'all', true)->all();
$reportCohortIds = array_values(array_unique(array_merge($currentIds, $outcomeIds)));
if (empty($reportCohortIds)) {
return collect();
}
$reanimationByMedicalHistory = MisReanimation::query()
->join('stt_migrationpatient as mp', 'mp.MigrationPatientID', '=', 'stt_reanimation.rf_MigrationPatientID')
->where('mp.rf_StationarBranchID', $branchId)
->where('mp.rf_MedicalHistoryID', '<>', 0)
->whereIn('mp.rf_MedicalHistoryID', $reportCohortIds)
->selectRaw('
mp."rf_MedicalHistoryID" as medical_history_id,
MAX(stt_reanimation."DateIn") as reanimation_date_in,
BOOL_OR(COALESCE(stt_reanimation."isComplete", false)) as reanimation_is_complete
')
->groupBy('mp.rf_MedicalHistoryID')
->get();
$medicalHistoryIds = $reanimationByMedicalHistory
->pluck('medical_history_id')
->map(fn ($id) => (int) $id)
->values()
->all();
if (empty($medicalHistoryIds)) {
return collect();
}
if ($onlyIds) {
return collect($medicalHistoryIds);
}
$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) {
$patient->DateRecipient = $reanimationDateIn;
}
$patient->reanimation_is_complete = (bool) $reanimationCompleteByMedicalHistory->get($patient->MedicalHistoryID, false);
return $patient;
});
}
/**
* Загрузить или посчитать хирургические операции в отчётном периоде.
*/
public function getSurgicalPatients(
string $type,
int $branchId,
DateRange $dateRange,
bool $countOnly = false
) {
$query = MisSurgicalOperation::where('rf_StationarBranchID', $branchId)
->completed()
->where('Date', '>=', $dateRange->startSql())
->where('Date', '<=', $dateRange->endSql());
if ($type === 'plan') {
$query->where('rf_TypeSurgOperationInTimeID', 6);
} else {
$query->whereIn('rf_TypeSurgOperationInTimeID', [4, 5]);
}
return $countOnly ? $query->count() : $query->get();
}
/**
* Посчитать плановых или экстренных пациентов, когда нужно включать текущих.
*/
public function getPatientsCountWithCurrent(
?string $type,
bool $isHeadOrAdmin,
int $branchId,
DateRange $dateRange
): int {
return $this->getPlanOrEmergencyPatients(
$type,
$isHeadOrAdmin,
$branchId,
$dateRange,
true,
false,
true
);
}
/**
* Общий MIS-запрос карточек пациентов с операциями и диагнозами.
*/
public function buildPatientCardsQuery(array $medicalHistoryIds, int $branchId)
{
return MisMedicalHistory::query()
->whereIn('MedicalHistoryID', $medicalHistoryIds)
->select($this->patientSelect())
->with($this->patientRelations($branchId))
->orderBy('DateRecipient', 'DESC');
}
/**
* Базовый select, используемый во всех выборках пациентов из МИС.
*/
private function patientSelect(): array
{
return [
'MedicalHistoryID',
'FAMILY',
'Name',
'OT',
'BD',
'DateRecipient',
'DateExtract',
'DateDeath',
'rf_EmerSignID',
'rf_kl_VisitResultID',
];
}
/**
* Общие eager-loaded связи для MIS-карточек пациентов.
*/
private function patientRelations(int $branchId): array
{
return [
'surgicalOperations' => function ($q) {
$q->select([
'SurgicalOperationID',
'rf_MedicalHistoryID',
'rf_kl_ServiceMedicalID',
'Date',
])->with(['serviceMedical' => function ($serviceQuery) {
$serviceQuery->select([
'ServiceMedicalID',
'ServiceMedicalCode',
'ServiceMedicalName',
]);
}]);
},
'outcomeMigration' => function ($q) {
$q->select([
'stt_migrationpatient.MigrationPatientID',
'stt_migrationpatient.rf_MedicalHistoryID',
'stt_migrationpatient.DateOut',
'stt_migrationpatient.rf_DiagnosID',
])->with(['mainDiagnosis' => function ($diagnosisQuery) {
$diagnosisQuery->select([
'DiagnosID',
'rf_MKBID',
])->with(['mkb' => function ($mkbQuery) {
$mkbQuery->select([
'MKBID',
'DS',
'NAME',
]);
}]);
}]);
},
'migrations' => function ($q) use ($branchId) {
$q->where('rf_StationarBranchID', $branchId)
->select([
'MigrationPatientID',
'rf_MedicalHistoryID',
'rf_DiagnosID',
'DateIngoing',
'rf_StationarBranchID',
])
->orderByDesc('DateIngoing')
->with(['mainDiagnosis' => function ($diagnosisQuery) {
$diagnosisQuery->select([
'DiagnosID',
'rf_MKBID',
'rf_MigrationPatientID',
])->with(['mkb' => function ($mkbQuery) {
$mkbQuery->select([
'MKBID',
'DS',
'NAME',
]);
}]);
}]);
},
];
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace App\Infrastructure\Reports\Sources;
use App\Data\UnifiedPatientData;
use App\Models\User;
use App\Services\DateRange;
use App\Services\PatientService;
use Illuminate\Support\Collection;
class MisPatientSource
{
public function __construct(
private readonly PatientService $patientService,
) {}
/**
* Загрузить пациентов из МИС и нормализовать их в DTO для отчёта.
*/
public function getDtos(
User $user,
string $status,
DateRange $dateRange,
int $branchId,
?bool $includeCurrent = null,
bool $fillableAuto = false,
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),
))
->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')
->values();
}
/**
* Загрузить сырые MIS-модели пациентов для запрошенного статуса отчёта.
*/
public function getPatients(
User $user,
string $status,
DateRange $dateRange,
int $branchId,
?bool $includeCurrent = null,
bool $fillableAuto = false,
bool $forSnapshots = false
): Collection {
$isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
$includeCurrent = $includeCurrent ?? in_array($status, ['plan', 'emergency'], true);
return match ($status) {
'plan', 'emergency' => $this->patientService->getPlanOrEmergencyPatients(
$status,
$isHeadOrAdmin,
$branchId,
$dateRange,
false,
false,
$includeCurrent,
$fillableAuto
),
'current' => $this->patientService->getAllPatientsInDepartment(
$isHeadOrAdmin,
$branchId,
$dateRange,
false,
false,
$fillableAuto
),
'recipient' => $this->patientService->getPlanOrEmergencyPatients(
null,
$isHeadOrAdmin,
$branchId,
$dateRange,
false,
false,
false,
$fillableAuto
),
'outcome' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'without-transferred'),
'outcome-discharged' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'discharged'),
'outcome-transferred' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'transferred'),
'outcome-deceased' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'deceased'),
'reanimation' => $this->patientService->getReanimationPatients($branchId, $dateRange),
default => collect(),
};
}
/**
* Посчитать MIS-пациентов без материализации DTO.
*/
public function getCount(
User $user,
string $status,
DateRange $dateRange,
int $branchId,
?bool $includeCurrent = null,
bool $fillableAuto = false
): int {
$isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
$includeCurrent = $includeCurrent ?? in_array($status, ['plan', 'emergency'], true);
return match ($status) {
'plan', 'emergency' => $includeCurrent
? $this->patientService->getPatientsCountWithCurrent($status, $isHeadOrAdmin, $branchId, $dateRange)
: $this->patientService->getPlanOrEmergencyPatients(
$status,
$isHeadOrAdmin,
$branchId,
$dateRange,
true,
false,
false,
$fillableAuto
),
'current' => $this->patientService->getAllPatientsInDepartment(
$isHeadOrAdmin,
$branchId,
$dateRange,
true,
false,
$fillableAuto
),
'recipient' => $this->patientService->getPlanOrEmergencyPatients(
null,
$isHeadOrAdmin,
$branchId,
$dateRange,
true,
false,
false,
$fillableAuto
),
'outcome' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'without-transferred', true)->count(),
'outcome-discharged' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'discharged', true)->count(),
'outcome-transferred' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'transferred', true)->count(),
'outcome-deceased' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'deceased', true)->count(),
'reanimation' => $this->patientService->getReanimationPatients($branchId, $dateRange, true)->count(),
default => 0,
};
}
}

View File

@@ -0,0 +1,218 @@
<?php
namespace App\Infrastructure\Reports\Sources;
use App\Data\UnifiedPatientData;
use App\Models\DepartmentPatientOperation;
use App\Models\MedicalHistorySnapshot;
use App\Models\MisMedicalHistory;
use Illuminate\Support\Collection;
class SnapshotPatientSource
{
public function getStatisticsFromSnapshots(array $reportIds): array
{
return [
'plan' => $this->getCountFromSnapshots('plan', $reportIds),
'emergency' => $this->getCountFromSnapshots('emergency', $reportIds),
'outcome' => $this->getCountFromSnapshots('outcome', $reportIds),
'deceased' => $this->getCountFromSnapshots('deceased', $reportIds),
'discharged' => $this->getCountFromSnapshots('discharged', $reportIds),
'transferred' => $this->getCountFromSnapshots('transferred', $reportIds),
'recipient' => $this->getCountFromSnapshots('recipient', $reportIds),
];
}
public function getPatientsFromSnapshots(
string $type,
array $reportIds,
bool $onlyIds = false,
bool $markRecipients = false,
?array $recipientReportIds = null
): Collection {
$snapshots = MedicalHistorySnapshot::query()
->whereIn('rf_report_id', $reportIds)
->where('patient_type', $type)
->get()
->unique(fn (MedicalHistorySnapshot $snapshot) => $this->snapshotIdentity($snapshot))
->values();
if ($snapshots->isEmpty()) {
return collect();
}
$recipientIds = [];
if ($markRecipients) {
$recipientReportIds ??= $reportIds;
$recipientIds = MedicalHistorySnapshot::query()
->whereIn('rf_report_id', $recipientReportIds)
->where('patient_type', 'recipient')
->get()
->map(fn (MedicalHistorySnapshot $snapshot) => UnifiedPatientData::fromSnapshot($snapshot)->id)
->unique()
->values()
->all();
}
$operationsByHistoryId = $this->getOperationsByMedicalHistoryId($snapshots);
$operationsByDepartmentPatientId = $this->getOperationsByDepartmentPatientId($snapshots);
$patients = $snapshots->map(function (MedicalHistorySnapshot $snapshot) use ($recipientIds, $operationsByHistoryId, $operationsByDepartmentPatientId) {
$patientId = $snapshot->rf_department_patient_id
? "manual:{$snapshot->rf_department_patient_id}"
: ($snapshot->patient_uid ?: "mis:{$snapshot->rf_medicalhistory_id}");
$misOperations = $snapshot->rf_medicalhistory_id
? ($operationsByHistoryId[$snapshot->rf_medicalhistory_id] ?? [])
: [];
$manualOperations = $snapshot->rf_department_patient_id
? ($operationsByDepartmentPatientId[$snapshot->rf_department_patient_id] ?? [])
: [];
$operations = collect($misOperations)
->merge($manualOperations)
->unique(fn (array $operation) => ($operation['code'] ?? '').'|'.($operation['name'] ?? ''))
->values()
->all();
return UnifiedPatientData::fromSnapshot(
$snapshot,
in_array($patientId, $recipientIds, true),
$operations
);
})->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')->values();
return $onlyIds ? $patients->pluck('id') : $patients;
}
public function getPatientsFromOneDayCurrentSnapshots(
string $type,
array $reportIds,
bool $onlyIds = false,
?array $recipientReportIds = null
): Collection {
$snapshots = MedicalHistorySnapshot::query()
->whereIn('rf_report_id', $reportIds)
->where('patient_type', 'current')
->get();
if ($snapshots->isEmpty()) {
return $this->getPatientsFromSnapshots(
$type,
$reportIds,
$onlyIds,
true,
$recipientReportIds
);
}
$recipientReportIds ??= $reportIds;
$recipientIds = MedicalHistorySnapshot::query()
->whereIn('rf_report_id', $recipientReportIds)
->where('patient_type', 'recipient')
->get()
->map(fn (MedicalHistorySnapshot $snapshot) => UnifiedPatientData::fromSnapshot($snapshot)->id)
->unique()
->values()
->all();
$operationsByHistoryId = $this->getOperationsByMedicalHistoryId($snapshots);
$operationsByDepartmentPatientId = $this->getOperationsByDepartmentPatientId($snapshots);
$patients = $snapshots
->filter(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_kind === $type)
->unique(fn (MedicalHistorySnapshot $snapshot) => $this->snapshotIdentity($snapshot))
->map(function (MedicalHistorySnapshot $snapshot) use ($recipientIds, $operationsByHistoryId, $operationsByDepartmentPatientId) {
$misOperations = $snapshot->rf_medicalhistory_id
? ($operationsByHistoryId[$snapshot->rf_medicalhistory_id] ?? [])
: [];
$manualOperations = $snapshot->rf_department_patient_id
? ($operationsByDepartmentPatientId[$snapshot->rf_department_patient_id] ?? [])
: [];
$operations = collect($misOperations)
->merge($manualOperations)
->unique(fn (array $operation) => ($operation['code'] ?? '').'|'.($operation['name'] ?? ''))
->values()
->all();
$patient = UnifiedPatientData::fromSnapshot(
$snapshot,
false,
$operations
);
$patient->isRecipientToday = in_array($patient->id, $recipientIds, true);
return $patient;
})
->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')
->values();
return $onlyIds ? $patients->pluck('id') : $patients;
}
private function getCountFromSnapshots(string $type, array $reportIds): int
{
$query = MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds);
if ($type === 'outcome') {
$query->whereIn('patient_type', ['discharged', 'deceased']);
} else {
$query->where('patient_type', $type);
}
return $query->get()
->map(fn (MedicalHistorySnapshot $snapshot) => $this->snapshotIdentity($snapshot))
->unique()
->count();
}
private function snapshotIdentity(MedicalHistorySnapshot $snapshot): string
{
return $snapshot->patient_uid
?: ($snapshot->rf_medicalhistory_id
? "mis:{$snapshot->rf_medicalhistory_id}"
: "snapshot:{$snapshot->medical_history_snapshot_id}");
}
private function getOperationsByMedicalHistoryId(Collection $snapshots): array
{
$historyIds = $snapshots->pluck('rf_medicalhistory_id')->filter()->unique()->values();
if ($historyIds->isEmpty()) {
return [];
}
return MisMedicalHistory::query()
->whereIn('MedicalHistoryID', $historyIds)
->with(['surgicalOperations.serviceMedical'])
->get()
->mapWithKeys(function (MisMedicalHistory $history) {
return [
$history->MedicalHistoryID => $history->surgicalOperations->map(fn ($operation) => [
'code' => $operation->serviceMedical?->ServiceMedicalCode,
'name' => $operation->serviceMedical?->ServiceMedicalName,
])->values()->all(),
];
})
->all();
}
private function getOperationsByDepartmentPatientId(Collection $snapshots): array
{
$departmentPatientIds = $snapshots->pluck('rf_department_patient_id')->filter()->unique()->values();
if ($departmentPatientIds->isEmpty()) {
return [];
}
return DepartmentPatientOperation::query()
->whereIn('rf_department_patient_id', $departmentPatientIds)
->with('serviceMedical')
->get()
->groupBy('rf_department_patient_id')
->map(fn (Collection $operations) => $operations->map(fn (DepartmentPatientOperation $operation) => [
'code' => $operation->serviceMedical?->ServiceMedicalCode ?? $operation->service_code,
'name' => $operation->serviceMedical?->ServiceMedicalName ?? $operation->service_name,
])->values()->all())
->all();
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace App\Infrastructure\Reports\Sources;
use App\Data\UnifiedPatientData;
use App\Models\Department;
use App\Models\DepartmentPatient;
use App\Models\ObservationPatient;
use App\Models\Report;
use App\Services\DateRange;
use Illuminate\Support\Collection;
class SpecialPatientSource
{
/**
* Загрузить manual/special пациентов и нормализовать их в DTO для отчёта.
*
* @param array<int, string>|null $sourceTypes
*/
public function getDtos(
Department $department,
string $status,
DateRange $dateRange,
?array $sourceTypes = ['manual', 'special'],
bool $forSnapshots = false
): Collection {
return $this->mapManualPatients(
$this->getPatients($department, $status, $dateRange, $sourceTypes, ! $forSnapshots),
$dateRange
);
}
/**
* Загрузить сырые manual/special записи пациентов для запрошенного статуса отчёта.
*
* @param array<int, string>|null $sourceTypes
*/
public function getPatients(
Department $department,
string $status,
DateRange $dateRange,
?array $sourceTypes = ['manual', 'special'],
bool $withOperations = true
): Collection {
$query = $this->buildManualPatientsQuery($department, $dateRange, $sourceTypes, $withOperations);
return match ($status) {
'plan', 'emergency' => $query->current()->where('patient_kind', $status)->get(),
'current' => $query->current()->get(),
'recipient' => $query->whereBetween('admitted_at', [$dateRange->startSql(), $dateRange->endSql()])->get(),
'outcome' => $query
->whereNotNull('outcome_type')
->whereIn('outcome_type', ['discharged', 'deceased'])
->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()])
->get(),
'outcome-discharged', 'outcome-transferred', 'outcome-deceased' => $query
->where('outcome_type', str_replace('outcome-', '', $status))
->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()])
->get(),
'reanimation' => collect(),
default => collect(),
};
}
/**
* Посчитать manual/special пациентов для статуса отчёта.
*
* @param array<int, string>|null $sourceTypes
*/
public function getCount(
Department $department,
string $status,
DateRange $dateRange,
?array $sourceTypes = ['manual', 'special']
): int {
$query = $this->buildManualPatientsQuery($department, $dateRange, $sourceTypes, false);
return match ($status) {
'plan', 'emergency' => $query->current()->where('patient_kind', $status)->count(),
'current' => $query->current()->count(),
'recipient' => $query->whereBetween('admitted_at', [$dateRange->startSql(), $dateRange->endSql()])->count(),
'outcome' => $query
->whereNotNull('outcome_type')
->whereIn('outcome_type', ['discharged', 'deceased'])
->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()])
->count(),
'outcome-discharged', 'outcome-transferred', 'outcome-deceased' => $query
->where('outcome_type', str_replace('outcome-', '', $status))
->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()])
->count(),
default => 0,
};
}
/**
* Загрузить manual-пациентов, уже связанных с MIS-картами в пределах отчётного окна.
*/
public function getLinkedManualPatientsForPeriod(Department $department, DateRange $dateRange): Collection
{
$reportIds = $this->getReportIdsForDepartmentPeriod($department, $dateRange);
return DepartmentPatient::query()
->where(function ($builder) use ($department, $reportIds) {
if (! empty($reportIds)) {
$builder->whereIn('rf_report_id', $reportIds);
}
$builder->orWhere(function ($legacyQuery) use ($department) {
$legacyQuery->whereNull('rf_report_id')
->where('rf_department_id', $department->department_id);
});
})
->whereIn('source_type', ['manual', 'special'])
->whereNotNull('rf_medicalhistory_id')
->get()
->keyBy('rf_medicalhistory_id');
}
/**
* @param array<int, string>|null $sourceTypes
*/
private function buildManualPatientsQuery(
Department $department,
DateRange $dateRange,
?array $sourceTypes,
bool $withOperations
) {
$reportIds = $this->getReportIdsForDepartmentPeriod($department, $dateRange);
$query = DepartmentPatient::query()
->where(function ($builder) use ($department, $reportIds) {
if (! empty($reportIds)) {
$builder->whereIn('rf_report_id', $reportIds);
}
$builder->orWhere(function ($legacyQuery) use ($department) {
$legacyQuery->whereNull('rf_report_id')
->where('rf_department_id', $department->department_id);
});
});
if ($withOperations) {
$query->with(['operations.serviceMedical']);
}
if ($sourceTypes !== null) {
$query->whereIn('source_type', $sourceTypes);
}
return $query;
}
private function getReportIdsForDepartmentPeriod(Department $department, DateRange $dateRange): array
{
return Report::query()
->where('rf_department_id', $department->department_id)
->when(
$dateRange->isOneDay,
fn ($query) => $query->exactPeriod($dateRange->startSql(), $dateRange->endSql()),
fn ($query) => $query->withinPeriod($dateRange->startSql(), $dateRange->endSql()),
)
->pluck('report_id')
->all();
}
private function mapManualPatients(Collection $manualPatients, DateRange $dateRange): Collection
{
return $manualPatients
->map(function (DepartmentPatient $patient) use ($dateRange) {
$operationsRelation = $patient->relationLoaded('operations')
? $patient->operations
: collect();
$operations = $operationsRelation->map(fn ($operation) => [
'id' => $operation->department_patient_operation_id,
'code' => $operation->serviceMedical?->ServiceMedicalCode ?? $operation->service_code,
'name' => $operation->serviceMedical?->ServiceMedicalName ?? $operation->service_name,
'startAt' => $operation->started_at?->toIso8601String(),
'endAt' => $operation->ended_at?->toIso8601String(),
])->filter(fn ($operation) => $operation['code'] || $operation['name'])->values()->all();
return UnifiedPatientData::fromDepartmentPatient(
$patient,
$patient->admitted_at?->betweenIncluded($dateRange->startDate, $dateRange->endDate) ?? false,
$operations,
$this->resolveObservationComment(null, $patient->department_patient_id)
);
})
->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')
->values();
}
private function resolveObservationComment(?int $medicalHistoryId, ?int $departmentPatientId): ?string
{
$query = ObservationPatient::query();
if ($departmentPatientId) {
$query->where('rf_department_patient_id', $departmentPatientId);
} elseif ($medicalHistoryId) {
$query->where('rf_medicalhistory_id', $medicalHistoryId);
} else {
return null;
}
return $query->pluck('comment')->filter()->implode('; ') ?: null;
}
}