1678 lines
61 KiB
PHP
1678 lines
61 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Data\UnifiedPatientData;
|
||
use App\Domain\Reports\ValueObjects\MetrikaConfig;
|
||
use App\Infrastructure\Reports\Services\AutoFillReportPayloadBuilder;
|
||
use App\Infrastructure\Reports\Services\ReportReadContextResolver;
|
||
use App\Infrastructure\Reports\Services\ReportRuntimeService;
|
||
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
|
||
{
|
||
public function __construct(
|
||
protected DateRangeService $dateRangeService,
|
||
protected UnifiedPatientService $unifiedPatientService,
|
||
protected PatientService $patientQueryService,
|
||
protected SnapshotService $snapshotService,
|
||
protected StatisticsService $statisticsService,
|
||
?AutoFillReportPayloadBuilder $autoFillReportPayloadBuilder = null,
|
||
?SnapshotPatientSource $snapshotPatientSource = null,
|
||
?ReportReadContextResolver $reportReadContextResolver = null,
|
||
?ReportRuntimeService $reportRuntimeService = null,
|
||
?MetrikaService $metrikaService = null
|
||
) {
|
||
$this->snapshotPatientSource = $snapshotPatientSource ?? app(SnapshotPatientSource::class);
|
||
$this->autoFillReportPayloadBuilder = $autoFillReportPayloadBuilder ?? app(AutoFillReportPayloadBuilder::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 ReportReadContextResolver $reportReadContextResolver;
|
||
|
||
protected ReportRuntimeService $reportRuntimeService;
|
||
|
||
protected MetrikaService $metrikaService;
|
||
|
||
/**
|
||
* Получить статистику для отчета
|
||
*/
|
||
public function getReportStatistics(Department $department, User $user, DateRange $dateRange): array
|
||
{
|
||
$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);
|
||
}
|
||
|
||
/**
|
||
* Создать или обновить отчет
|
||
*/
|
||
public function storeReport(array $data, User $user, $fillableAuto = false): Report
|
||
{
|
||
$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
|
||
{
|
||
$this->reportRuntimeService->prepareForHeavySave();
|
||
}
|
||
|
||
public function syncCalculatedMetricsForStoredReport(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->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->saveBedDaysMetrics($report);
|
||
$this->savePreoperativeMetrics($report);
|
||
$this->saveDepartmentLoadMetric($report);
|
||
}
|
||
|
||
public function saveLethalMetricForStoredReport(Report $report): void
|
||
{
|
||
$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
|
||
{
|
||
$this->reportRuntimeService->clearCacheAfterReportCreation($user, $report);
|
||
}
|
||
|
||
public function buildAutoFillReportPayload(User $user, Department $department, DateRange $dateRange): array
|
||
{
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* Получить пациентов по статусу
|
||
*/
|
||
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->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,
|
||
$includeCurrentPatients
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Получить количество пациентов по статусу
|
||
*/
|
||
public function getPatientsCountByStatus(
|
||
Department $department,
|
||
User $user,
|
||
string $status,
|
||
DateRange $dateRange
|
||
): int {
|
||
[$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
|
||
{
|
||
$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;
|
||
}
|
||
|
||
/**
|
||
* Получить информацию о текущем отчете
|
||
*/
|
||
public function getCurrentReportInfo(Department $department, User $user, DateRange $dateRange): array
|
||
{
|
||
$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,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Удалить пациента из наблюдения
|
||
*/
|
||
public function removeObservationPatient(string $patientId): void
|
||
{
|
||
[$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)
|
||
{
|
||
$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)
|
||
{
|
||
$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)
|
||
{
|
||
$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)
|
||
{
|
||
$patient = DepartmentPatient::query()
|
||
->where('department_patient_id', $departmentPatientId)
|
||
->firstOrFail();
|
||
|
||
return $this->unifiedPatientService->linkManualPatientToMis($patient, $medicalHistoryId);
|
||
}
|
||
|
||
public function getManualPatientOperations(User $user, int $departmentPatientId)
|
||
{
|
||
$patient = $this->resolveManageableManualPatient($user, $departmentPatientId);
|
||
|
||
return $patient->operations()
|
||
->with('serviceMedical')
|
||
->orderByDesc('started_at')
|
||
->get();
|
||
}
|
||
|
||
public function createManualPatientOperation(User $user, int $departmentPatientId, array $data): DepartmentPatientOperation
|
||
{
|
||
$patient = $this->resolveManageableManualPatient($user, $departmentPatientId);
|
||
$service = $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
|
||
{
|
||
$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();
|
||
}
|
||
|
||
public function saveReanimationIndicator(
|
||
User $user,
|
||
int $departmentId,
|
||
int $medicalHistoryId,
|
||
string $indicator,
|
||
?string $comment = null,
|
||
?int $reportId = null
|
||
): ReanimationPatientIndicator {
|
||
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)
|
||
{
|
||
if (empty($medicalHistoryIds)) {
|
||
return collect();
|
||
}
|
||
|
||
$subQuery = ReanimationPatientIndicator::query()
|
||
->selectRaw('MAX(reanimation_patient_indicator_id) as max_id, rf_medicalhistory_id')
|
||
->where('rf_department_id', $departmentId)
|
||
->whereIn('rf_medicalhistory_id', $medicalHistoryIds)
|
||
->groupBy('rf_medicalhistory_id');
|
||
|
||
return ReanimationPatientIndicator::query()
|
||
->joinSub($subQuery, 'latest', function ($join) {
|
||
$join->on('reanimation_patient_indicators.reanimation_patient_indicator_id', '=', 'latest.max_id');
|
||
})
|
||
->get([
|
||
'reanimation_patient_indicators.rf_medicalhistory_id',
|
||
'reanimation_patient_indicators.indicator',
|
||
'reanimation_patient_indicators.comment',
|
||
])
|
||
->keyBy('rf_medicalhistory_id');
|
||
}
|
||
|
||
public function getReanimationIndicatorsHistory(
|
||
int $departmentId,
|
||
int $medicalHistoryId,
|
||
int $limit = 50
|
||
) {
|
||
return ReanimationPatientIndicator::query()
|
||
->where('rf_department_id', $departmentId)
|
||
->where('rf_medicalhistory_id', $medicalHistoryId)
|
||
->orderByDesc('reanimation_patient_indicator_id')
|
||
->limit($limit)
|
||
->get([
|
||
'reanimation_patient_indicator_id',
|
||
'rf_report_id',
|
||
'rf_medicalhistory_id',
|
||
'indicator',
|
||
'comment',
|
||
'created_by',
|
||
'created_at',
|
||
]);
|
||
}
|
||
|
||
public function searchMisPatientsForDepartment(Department $department, string $query)
|
||
{
|
||
return $this->unifiedPatientService->searchMisPatients($department, $query);
|
||
}
|
||
|
||
/**
|
||
* Получить пациентов из снапшотов
|
||
*/
|
||
public function getPatientsFromSnapshots(
|
||
Department $department,
|
||
string $status,
|
||
DateRange $dateRange,
|
||
int $branchId,
|
||
bool $onlyIds = false
|
||
) {
|
||
[$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);
|
||
}
|
||
|
||
/**
|
||
* Получить нежелательные события за дату
|
||
*/
|
||
public function getUnwantedEvents(Department $department, DateRange $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'),
|
||
];
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Получить отчеты за диапазон дат
|
||
*/
|
||
public function getReportsForDateRange(int $departmentId, DateRange $dateRange)
|
||
{
|
||
return $this->reportReadContextResolver->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,
|
||
];
|
||
}
|
||
|
||
public function getReportInfo(User $user, Department $department, DateRange $dateRange)
|
||
{
|
||
$report = $this->resolveReport($department->department_id, $dateRange);
|
||
$metrics = $this->metrikaService->getMetricsForReport($report);
|
||
}
|
||
|
||
private function resolveReport(int $departmentId, DateRange $dateRange)
|
||
{
|
||
return $this->reportReadContextResolver->resolveReportForPeriod($departmentId, $dateRange);
|
||
}
|
||
|
||
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');
|
||
}
|
||
|
||
$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();
|
||
}
|
||
}
|