915 lines
44 KiB
PHP
915 lines
44 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\DepartmentMetrikaDefault;
|
||
use App\Models\DutyReportMetricResult;
|
||
use App\Models\DutyUnwantedEvent;
|
||
use App\Models\MedicalHistory;
|
||
use App\Models\MetrikaResult;
|
||
use App\Models\ObservableMedicalHistory;
|
||
use App\Models\ReportDuty;
|
||
use App\Models\ReportNurse;
|
||
use App\Models\UnifiedMedicalHistory;
|
||
use App\Models\User;
|
||
use App\Services\Classification\PatientStatusClassifier;
|
||
use Illuminate\Database\Eloquent\Builder;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
class DutyReportService
|
||
{
|
||
public function __construct(
|
||
protected DateRangeService $dateRangeService,
|
||
) { }
|
||
|
||
/**
|
||
* Базовый запрос для всех отчётов
|
||
* Фильтрует по отделению и периоду, подгружает связи
|
||
*/
|
||
protected function baseQuery(DateRange $dateRange, int $departmentId): Builder
|
||
{
|
||
return UnifiedMedicalHistory::query()
|
||
->whereHas('migrations', function ($q) use ($departmentId, $dateRange) {
|
||
$q->department($departmentId)->dateRange($dateRange->startSql(), $dateRange->endSql());
|
||
})
|
||
->with(['latestMigration' => function ($q) use ($departmentId, $dateRange) {
|
||
$q->department($departmentId)->dateRange($dateRange->startSql(), $dateRange->endSql());
|
||
}]);
|
||
}
|
||
|
||
/**
|
||
* Сохранить отчет
|
||
*/
|
||
public function saveReport(DateRange $dateRange, ?int $userId = null, ?int $lpuDoctorId = null, ?int $departmentId = null)
|
||
{
|
||
$user = $userId ? User::find($userId) : auth()->user();
|
||
|
||
$lpuDoctorId = $lpuDoctorId ?? $user->rf_lpudoctor_id;
|
||
$departmentId = $departmentId ?? $user->rf_department_id;
|
||
|
||
$data = [
|
||
'report_date' => $dateRange->endSql(),
|
||
'sent_at' => Carbon::now()->format('Y-m-d H:i:s'),
|
||
'period_type' => 'day',
|
||
'period_start' => $dateRange->startSql(),
|
||
'period_end' => $dateRange->endSql(),
|
||
'status_id' => 2, // опубликован
|
||
'rf_lpudoctor_id' => $lpuDoctorId,
|
||
'rf_department_id' => $departmentId,
|
||
'rf_user_id' => $user->id,
|
||
];
|
||
|
||
$report = ReportDuty::updateOrCreate(
|
||
[
|
||
'report_date' => $data['report_date'],
|
||
'period_start' => $data['period_start'],
|
||
'period_end' => $data['period_end'],
|
||
'rf_department_id' => $data['rf_department_id'],
|
||
],
|
||
$data
|
||
);
|
||
|
||
return $report;
|
||
}
|
||
|
||
/**
|
||
* Сохранить снимок пациентов за период
|
||
*/
|
||
public function saveSnapshot(DateRange $dateRange, ReportDuty $reportDuty, ?int $departmentId = null, ?int $userId = null): array
|
||
{
|
||
$departmentId = $departmentId ?? $reportDuty->department->rf_mis_department_id;
|
||
$userId = $userId ?? $reportDuty->rf_user_id;
|
||
$startYear = Carbon::now()->startOfYear()->format('Y-m-d');
|
||
|
||
$patients = MedicalHistory::query()
|
||
->whereHas('migrations', function ($q) use ($departmentId, $dateRange, $startYear) {
|
||
$q->where('department_id', $departmentId)
|
||
->where('ingoing_date', '<=', $dateRange->endSql())
|
||
->where(function ($sub) use ($dateRange, $startYear) {
|
||
// Миграции без out_date (еще лежат)
|
||
$sub->whereNull('out_date')
|
||
->where('ingoing_date', '>', $startYear);
|
||
|
||
// Миграции с out_date (закрытые)
|
||
$sub->orWhere(function ($sub2) use ($dateRange, $startYear) {
|
||
$sub2->whereNotNull('out_date')
|
||
->where('out_date', '>', $dateRange->startSql())
|
||
->where('out_date', '>', $startYear);
|
||
});
|
||
});
|
||
})
|
||
->with([
|
||
'latestMigration' => function ($q) use ($departmentId) {
|
||
$q->where('department_id', $departmentId);
|
||
},
|
||
'latestMigration.operations',
|
||
'latestMigration.reanimations',
|
||
'migrations' => function ($q) use ($departmentId, $dateRange, $startYear) {
|
||
// Загружаем только нужные миграции с фильтром по отделению
|
||
$q->where('department_id', $departmentId)
|
||
->where('ingoing_date', '<=', $dateRange->endSql())
|
||
->where(function ($sub) use ($dateRange, $startYear) {
|
||
// Миграции без out_date (еще лежат)
|
||
$sub->whereNull('out_date')
|
||
->where('ingoing_date', '>', $startYear);
|
||
|
||
// Миграции с out_date (закрытые)
|
||
$sub->orWhere(function ($sub2) use ($dateRange, $startYear) {
|
||
$sub2->whereNotNull('out_date')
|
||
->where('out_date', '>', $dateRange->startSql())
|
||
->where('out_date', '>', $startYear);
|
||
});
|
||
});
|
||
},
|
||
'migrations.reanimations' => function ($q) use ($dateRange) {
|
||
// Фильтруем реанимации по периоду (опционально)
|
||
$q->where(function ($sub) use ($dateRange) {
|
||
$sub->whereNull('out_date')
|
||
->orWhere('out_date', '>=', $dateRange->startSql());
|
||
});
|
||
},
|
||
'operations' => function ($q) use ($departmentId, $dateRange) {
|
||
$q->where('department_id', $departmentId);
|
||
}
|
||
])
|
||
->lazy()->map(function (MedicalHistory $h) use ($dateRange) {
|
||
$patientStatus = PatientStatusClassifier::classify($h, $dateRange);
|
||
$periodFlags = PatientStatusClassifier::classifyPeriodFlags($h, $dateRange);
|
||
$patientUrgency = null;
|
||
$patientReanimation = null;
|
||
if (!in_array($patientStatus, [
|
||
PatientStatusClassifier::STATUS_DECEASED,
|
||
PatientStatusClassifier::STATUS_DISCHARGED,
|
||
PatientStatusClassifier::STATUS_TRANSFERRED
|
||
])) {
|
||
$patientUrgency = PatientStatusClassifier::classifyUrgency($h->urgency_id);
|
||
$patientReanimation = PatientStatusClassifier::classifyReanimation($h->latestMigration?->reanimations, $dateRange);
|
||
}
|
||
return [
|
||
// Все исходные поля модели (автоматически через toArray)
|
||
...$h->toArray(),
|
||
|
||
'operations' => $h->operations->toArray(),
|
||
// + вычисляемые мета-поля для фронтенда
|
||
'patient_status' => $patientStatus,
|
||
'patient_urgency' => $patientUrgency,
|
||
'period_flags' => $periodFlags,
|
||
'in_reanimation' => $patientReanimation,
|
||
'admitted_today' => PatientStatusClassifier::classifyAdmitted($h->latestMigration?->ingoing_date, $dateRange),
|
||
'in_observable' => PatientStatusClassifier::classifyObservable($h->observable, $dateRange),
|
||
];
|
||
});
|
||
|
||
$savedStats = $this->saveReportSnapshot($reportDuty->id, $patients, $userId, $departmentId, $dateRange);
|
||
|
||
return [
|
||
...$savedStats,
|
||
'report_date' => $dateRange->startSql(),
|
||
'department_id' => $departmentId,
|
||
];
|
||
}
|
||
|
||
public function saveReportSnapshot(
|
||
int $reportDutyId,
|
||
iterable $patients,
|
||
int $userId,
|
||
int $departmentId,
|
||
DateRange $dateRange
|
||
): array
|
||
{
|
||
if (empty($patients)) {
|
||
return ['saved_patients' => 0, 'saved_migrations' => 0];
|
||
}
|
||
|
||
$patientBatch = [];
|
||
$migrationBatch = [];
|
||
$reanimationBatch = [];
|
||
$batchSize = 100;
|
||
|
||
// Инициализация агрегатора статистики
|
||
$totalStats = [
|
||
'saved_patients' => 0,
|
||
'saved_migrations' => 0,
|
||
'saved_reanimations' => 0,
|
||
'by_status' => [],
|
||
'by_urgency' => [],
|
||
'admitted' => [
|
||
'today' => 0,
|
||
'planned' => 0,
|
||
'urgent' => 0,
|
||
],
|
||
'in_reanimation' => 0,
|
||
'admitted_today' => 0,
|
||
'in_department' => 0,
|
||
'planned' => 0,
|
||
'deceased' => 0,
|
||
'transferred' => 0,
|
||
'discharged' => 0,
|
||
'outcome' => 0,
|
||
'total_bed_days' => 0,
|
||
'total_preop_bed_days' => 0,
|
||
'patients_with_operations' => 0,
|
||
'total_operations' => 0,
|
||
'planned_operations' => 0,
|
||
'urgent_operations' => 0,
|
||
'total_patients' => 0,
|
||
];
|
||
|
||
$outcomeList = [];
|
||
|
||
// Статусы операций
|
||
$operationPlanned = [6];
|
||
$operationUrgent = [4, 5];
|
||
|
||
// Получаем границы периода
|
||
$periodStart = $dateRange->startSql();
|
||
$periodEnd = $dateRange->endSql();
|
||
$periodStartCarbon = Carbon::parse($periodStart);
|
||
$periodEndCarbon = Carbon::parse($periodEnd);
|
||
|
||
$uniqueOperationIds = [];
|
||
|
||
foreach ($patients as $patient) {
|
||
// ========== 1. ПОДГОТОВКА ДАННЫХ ДЛЯ БД (с обнулением) ==========
|
||
|
||
// Подготовка данных пациента
|
||
$extractDate = $patient['extract_date'] ? Carbon::parse($patient['extract_date']) : null;
|
||
$hasExtractInPeriod = $extractDate && $this->dateRangeService->dateInPeriod($extractDate, $dateRange);
|
||
|
||
$patientData = [
|
||
'report_duty_id' => $reportDutyId,
|
||
'source_type' => 'mis',
|
||
'original_id' => $patient['id'],
|
||
'medical_card_number' => $patient['medical_card_number'],
|
||
'full_name' => $patient['full_name'],
|
||
'birth_date' => $patient['birth_date'],
|
||
'recipient_date' => $patient['recipient_date'],
|
||
'male' => $patient['male'],
|
||
'urgency_id' => $patient['urgency_id'],
|
||
'comment' => $patient['comment'] ?? null,
|
||
'user_id' => $userId,
|
||
];
|
||
|
||
// ДОПОЛНИТЕЛЬНО: проверяем миграции на наличие выбытия в периоде
|
||
$hasDischargeInMigration = false;
|
||
$migrationVisitResultId = null;
|
||
$migrationOutDate = null;
|
||
|
||
// Подготовка данных миграции
|
||
$preparedMigrations = [];
|
||
foreach ($patient['migrations'] as $migration) {
|
||
// Пропускаем миграции не в нашем отделении
|
||
if (($migration['department_id'] ?? null) != $departmentId) {
|
||
continue;
|
||
}
|
||
|
||
$outDate = $migration['out_date'] ? Carbon::parse($migration['out_date']) : null;
|
||
$hasOutDateInPeriod = $outDate && $this->dateRangeService->dateInPeriod($outDate, $dateRange);
|
||
|
||
$migrationItem = [
|
||
'_temp_key' => [
|
||
'report_duty_id' => $reportDutyId,
|
||
'source_type' => 'mis',
|
||
'original_id' => $patient['id'],
|
||
],
|
||
'original_id' => $migration['id'],
|
||
'ingoing_date' => $migration['ingoing_date'],
|
||
'diagnosis_id' => $migration['diagnosis_id'],
|
||
'diagnosis_code' => $migration['diagnosis_code'],
|
||
'diagnosis_name' => $migration['diagnosis_name'],
|
||
'interrupted_event_id' => $migration['interrupted_event_id'],
|
||
'stationar_branch_id' => $migration['stationar_branch_id'],
|
||
'department_id' => $migration['department_id'],
|
||
'user_id' => $migration['user_id'] ?? null,
|
||
'mis_user_id' => $migration['mis_user_id'] ?? null,
|
||
'comment' => $migration['comment'] ?? null,
|
||
];
|
||
|
||
// Добавляем поля ТОЛЬКО если есть out_date в периоде
|
||
if ($hasOutDateInPeriod) {
|
||
$hasDischargeInMigration = true;
|
||
$migrationVisitResultId = $migration['visit_result_id'] ?? null;
|
||
$migrationOutDate = $migration['out_date'] ?? null;
|
||
$migrationItem['visit_result_id'] = $migration['visit_result_id'] ?? null;
|
||
$migrationItem['stat_cure_result_id'] = $migration['stat_cure_result_id'] ?? null;
|
||
$migrationItem['out_date'] = $migration['out_date'] ?? null;
|
||
} else {
|
||
$migrationItem['visit_result_id'] = null;
|
||
$migrationItem['stat_cure_result_id'] = null;
|
||
$migrationItem['out_date'] = null;
|
||
}
|
||
|
||
$preparedMigrations[] = $migrationItem;
|
||
|
||
// Подготовка данных реанимации
|
||
if (!empty($migration['reanimations'])) {
|
||
foreach ($migration['reanimations'] as $reanimation) {
|
||
$reanimationBatch[] = [
|
||
'_temp_key' => [
|
||
'report_duty_id' => $reportDutyId,
|
||
'source_type' => 'mis',
|
||
'patient_id' => $patient['id'],
|
||
'migration_id' => $migration['id'],
|
||
],
|
||
'original_id' => $reanimation['id'],
|
||
'migration_patient_id' => $reanimation['migration_patient_id'],
|
||
'medical_history_id' => $reanimation['medical_history_id'],
|
||
'in_date' => $reanimation['in_date'],
|
||
'out_date' => $reanimation['out_date'],
|
||
'description' => $reanimation['description'],
|
||
'stationar_branch_id' => $reanimation['stationar_branch_id'],
|
||
'migration_stationar_branch_id' => $reanimation['migration_stationar_branch_id'],
|
||
'migration_department_id' => $reanimation['migration_department_id'],
|
||
'doctor_id' => $reanimation['doctor_id'],
|
||
'user_id' => $reanimation['user_id'] ?? null,
|
||
'mis_user_id' => $reanimation['mis_user_id'] ?? null,
|
||
'comment' => $reanimation['comment'] ?? null,
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
// Если есть выбытие по миграции ИЛИ по карте
|
||
$hasOutcomeInPeriod = $hasExtractInPeriod || $hasDischargeInMigration;
|
||
|
||
if ($hasOutcomeInPeriod) {
|
||
// Приоритет: данные из миграции (если есть), иначе из карты
|
||
$patientData['visit_result_id'] = $migrationVisitResultId ?? $patient['visit_result_id'] ?? null;
|
||
$patientData['extract_date'] = $migrationOutDate ?? $patient['extract_date'] ?? null;
|
||
$patientData['death_date'] = $patient['death_date'] ?? null;
|
||
} else {
|
||
$patientData['visit_result_id'] = null;
|
||
$patientData['extract_date'] = null;
|
||
$patientData['death_date'] = null;
|
||
}
|
||
|
||
// ========== 2. РАСЧЕТ СТАТИСТИКИ НА ОСНОВЕ ПОДГОТОВЛЕННЫХ ДАННЫХ ==========
|
||
|
||
// Флаги для пациента
|
||
$hasRecipientInPeriod = false;
|
||
$hasDeathInPeriod = false;
|
||
$hasTransferInPeriod = false;
|
||
$hasDischargeInPeriod = false;
|
||
$hasActiveMigrationInPeriod = false;
|
||
|
||
// Проверяем каждую миграцию для определения статусов
|
||
foreach ($preparedMigrations as $migration) {
|
||
$ingoingDate = $migration['ingoing_date'] ? Carbon::parse($migration['ingoing_date']) : null;
|
||
$outDate = $migration['out_date'] ? Carbon::parse($migration['out_date']) : null;
|
||
$visitResultId = $migration['visit_result_id'] ?? null;
|
||
$statCureResultId = $migration['stat_cure_result_id'] ?? null;
|
||
|
||
// Поступление в периоде
|
||
if ($ingoingDate && $this->dateRangeService->dateInPeriod($ingoingDate, $dateRange)) {
|
||
$hasRecipientInPeriod = true;
|
||
}
|
||
|
||
// Проверяем активную миграцию на конец периода
|
||
if ($ingoingDate && $ingoingDate <= $periodEndCarbon) {
|
||
if (!$outDate || $outDate > $periodEndCarbon) {
|
||
$hasActiveMigrationInPeriod = true;
|
||
}
|
||
}
|
||
|
||
// Выбытие в периоде (есть out_date в периоде)
|
||
if ($outDate && $this->dateRangeService->dateInPeriod($outDate, $dateRange)) {
|
||
// Смерть (коды 5, 15 - умер в стационаре)
|
||
if (in_array($visitResultId, [5, 15])) {
|
||
$hasDeathInPeriod = true;
|
||
}
|
||
// Перевод (коды 4, 14)
|
||
elseif (in_array($visitResultId, [4, 14])) {
|
||
$hasTransferInPeriod = true;
|
||
}
|
||
// Выписка
|
||
else {
|
||
$hasDischargeInPeriod = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Если нет исхода по миграциям, проверяем extract_date на уровне карты
|
||
// if (!$hasDeathInPeriod && !$hasTransferInPeriod && !$hasDischargeInPeriod) {
|
||
// if ($hasExtractInPeriod) {
|
||
// $visitResultId = $patient['visit_result_id'] ?? null;
|
||
// $deathDate = $patient['death_date'] ? Carbon::parse($patient['death_date']) : null;
|
||
//
|
||
// if ($deathDate && $deathDate->lte($periodEndCarbon)) {
|
||
// $hasDeathInPeriod = true;
|
||
// } elseif (in_array($visitResultId, [4, 14])) {
|
||
// $hasTransferInPeriod = true;
|
||
// } else {
|
||
// $hasDischargeInPeriod = true;
|
||
// }
|
||
// }
|
||
// }
|
||
|
||
// Заполнение статистики по статусам
|
||
// Умершие
|
||
if ($hasDeathInPeriod) {
|
||
$totalStats['deceased']++;
|
||
$totalStats['by_status']['deceased'] = ($totalStats['by_status']['deceased'] ?? 0) + 1;
|
||
}
|
||
// Переведенные
|
||
elseif ($hasTransferInPeriod) {
|
||
$totalStats['transferred']++;
|
||
$totalStats['by_status']['transferred'] = ($totalStats['by_status']['transferred'] ?? 0) + 1;
|
||
}
|
||
// Выписанные
|
||
elseif ($hasDischargeInPeriod) {
|
||
$totalStats['discharged']++;
|
||
$totalStats['outcome']++;
|
||
$totalStats['by_status']['discharged'] = ($totalStats['by_status']['discharged'] ?? 0) + 1;
|
||
}
|
||
// В отделении на конец периода
|
||
elseif ($hasActiveMigrationInPeriod) {
|
||
$totalStats['in_department']++;
|
||
$totalStats['by_status']['in_department'] = ($totalStats['by_status']['in_department'] ?? 0) + 1;
|
||
}
|
||
|
||
// Поступившие (на основе ingoing_date в периоде)
|
||
if ($hasRecipientInPeriod) {
|
||
$totalStats['admitted']['today']++;
|
||
$totalStats['by_status']['recipient'] = ($totalStats['by_status']['recipient'] ?? 0) + 1;
|
||
if ($patient['urgency_id'] == 2) {
|
||
$totalStats['admitted']['planned']++;
|
||
$totalStats['planned']++;
|
||
}
|
||
if ($patient['urgency_id'] == 1) {
|
||
$totalStats['admitted']['urgent']++;
|
||
}
|
||
}
|
||
|
||
// Срочность
|
||
if (!empty($patient['patient_urgency'])) {
|
||
$totalStats['by_urgency'][$patient['patient_urgency']] = ($totalStats['by_urgency'][$patient['patient_urgency']] ?? 0) + 1;
|
||
}
|
||
|
||
// Реанимация
|
||
if (!empty($patient['in_reanimation'])) {
|
||
$totalStats['in_reanimation']++;
|
||
}
|
||
|
||
// Поступил сегодня
|
||
if (!empty($patient['admitted_today'])) {
|
||
$totalStats['admitted_today']++;
|
||
}
|
||
|
||
// ========== 3. КОЙКО-ДНИ И ОПЕРАЦИИ ==========
|
||
$patientBedDays = 0;
|
||
$preOpBedDays = 0;
|
||
$hasOperation = false;
|
||
$patientOperationsCount = 0;
|
||
$patientOperationsPlannedCount = 0;
|
||
$patientOperationsUrgentCount = 0;
|
||
|
||
try {
|
||
$migration = $patient['migrations'][0];
|
||
$migrationStart = $migration['ingoing_date'] ? Carbon::parse($migration['ingoing_date']) : null;
|
||
$migrationEnd = $migration['out_date'] ? Carbon::parse($migration['out_date']) : null;
|
||
|
||
if (!$migrationStart) {
|
||
continue;
|
||
}
|
||
|
||
// Проверяем пересечение с отчетным периодом
|
||
$hasIntersection = $migrationStart <= $periodEndCarbon &&
|
||
($migrationEnd === null || $migrationEnd >= $periodStartCarbon);
|
||
|
||
// if (!$hasIntersection) {
|
||
// continue;
|
||
// }
|
||
|
||
// ===== КОЙКО-ДНИ =====
|
||
$calcStart = $migrationStart > $periodStartCarbon ? $migrationStart : $periodStartCarbon;
|
||
$calcEnd = $migrationEnd && $migrationEnd < $periodEndCarbon ? $migrationEnd : $periodEndCarbon;
|
||
|
||
$bedDays = $calcStart->diffInDays($calcEnd);
|
||
$patientBedDays += max(0, $bedDays);
|
||
|
||
// ===== ПРЕДОПЕРАЦИОННЫЕ ДНИ =====
|
||
$opsInPeriod = collect($patient['operations'] ?? [])
|
||
->filter(function ($op) use ($dateRange) {
|
||
$opStart = $op['start_date'] ? Carbon::parse($op['start_date']) : null;
|
||
return $opStart
|
||
&& $opStart->gte($dateRange->start())
|
||
&& $opStart->lt($dateRange->end());
|
||
});
|
||
|
||
if ($opsInPeriod->isNotEmpty()) {
|
||
$hasOperation = true;
|
||
$firstOpInPeriod = $opsInPeriod->sortBy('start_date')->first();
|
||
|
||
if ($firstOpInPeriod && $migrationStart) {
|
||
$opDate = Carbon::parse($firstOpInPeriod['start_date']);
|
||
if ($opDate > $migrationStart) {
|
||
$preOpDays = $migrationStart->copy()->startOfDay()
|
||
->diffInDays($opDate->copy()->startOfDay());
|
||
$preOpBedDays += max(0, $preOpDays);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===== ОПЕРАЦИИ =====
|
||
if (count($patient['operations']) > 0)
|
||
foreach ($patient['operations'] as $operation) {
|
||
$opStart = $operation['start_date'] ? Carbon::parse($operation['start_date']) : null;
|
||
$opEnd = $operation['end_date'] ? Carbon::parse($operation['end_date']) : null;
|
||
|
||
if ($opStart && $opStart->gte($dateRange->start()) && $opStart->lt($dateRange->end())) {
|
||
// Уникализируем по ID операции
|
||
$uniqueOperationIds[$operation['id']] = [
|
||
'id' => $operation['id'],
|
||
'urgent_status' => $operation['urgent_status'],
|
||
];
|
||
}
|
||
}
|
||
} catch (\Exception $e) {
|
||
\Log::error('DutyReportService: ошибка обработки пациента', [
|
||
'patient_id' => $patient['id'] ?? null,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
} finally {
|
||
|
||
}
|
||
|
||
// Агрегация койко-дней и операций
|
||
$totalStats['total_bed_days'] += $patientBedDays;
|
||
$totalStats['total_preop_bed_days'] += $preOpBedDays;
|
||
$totalStats['patients_with_operations'] += $hasOperation ? 1 : 0;
|
||
$totalStats['total_operations'] = count($uniqueOperationIds);
|
||
$totalStats['planned_operations'] = collect($uniqueOperationIds)->where('urgent_status', 6)->count();
|
||
$totalStats['urgent_operations'] = collect($uniqueOperationIds)->whereIn('urgent_status', [4,5])->count();
|
||
$totalStats['total_patients']++;
|
||
|
||
// ========== 4. ДОБАВЛЯЕМ В БАТЧИ ==========
|
||
$patientBatch[] = $patientData;
|
||
$migrationBatch = array_merge($migrationBatch, $preparedMigrations);
|
||
|
||
// Контроль размера батча
|
||
if (count($patientBatch) >= $batchSize) {
|
||
$batchStats = $this->upsertBatches($reportDutyId, $patientBatch, $migrationBatch, $reanimationBatch);
|
||
$totalStats = $this->mergeStats($totalStats, $batchStats);
|
||
$patientBatch = [];
|
||
$migrationBatch = [];
|
||
$reanimationBatch = [];
|
||
}
|
||
}
|
||
|
||
// Сохраняем остаток
|
||
if (!empty($patientBatch)) {
|
||
$batchStats = $this->upsertBatches($reportDutyId, $patientBatch, $migrationBatch, $reanimationBatch);
|
||
$totalStats = $this->mergeStats($totalStats, $batchStats);
|
||
}
|
||
|
||
return $totalStats;
|
||
}
|
||
|
||
/**
|
||
* Вспомогательный метод: слияние статистики двух массивов
|
||
*/
|
||
private function mergeStats(array $total, array $new): array
|
||
{
|
||
$total['saved_patients'] += $new['saved_patients'] ?? 0;
|
||
$total['saved_migrations'] += $new['saved_migrations'] ?? 0;
|
||
$total['in_reanimation'] += $new['in_reanimation'] ?? 0;
|
||
$total['admitted_today'] += $new['admitted_today'] ?? 0;
|
||
$total['in_department'] += $new['in_department'] ?? 0;
|
||
$total['planned'] += $new['planned'] ?? 0;
|
||
$total['deceased'] += $new['deceased'] ?? 0;
|
||
$total['transferred'] += $new['transferred'] ?? 0;
|
||
$total['outcome'] += $new['outcome'] ?? 0;
|
||
|
||
// Койко-дни
|
||
$total['total_bed_days'] += $new['total_bed_days'] ?? 0;
|
||
$total['total_preop_bed_days'] += $new['total_preop_bed_days'] ?? 0;
|
||
|
||
// Операции
|
||
$total['patients_with_operations'] += $new['patients_with_operations'] ?? 0;
|
||
$total['total_operations'] += $new['total_operations'] ?? 0;
|
||
$total['planned_operations'] += $new['planned_operations'] ?? 0;
|
||
$total['urgent_operations'] += $new['urgent_operations'] ?? 0;
|
||
|
||
// Для расчётов
|
||
$total['total_patients'] += $new['total_patients'] ?? 0;
|
||
|
||
// Объединение счётчиков по статусам
|
||
foreach ($new['by_status'] ?? [] as $status => $count) {
|
||
$total['by_status'][$status] = ($total['by_status'][$status] ?? 0) + $count;
|
||
}
|
||
foreach ($new['by_urgency'] ?? [] as $urgency => $count) {
|
||
$total['by_urgency'][$urgency] = ($total['by_urgency'][$urgency] ?? 0) + $count;
|
||
}
|
||
foreach ($new['admitted'] ?? [] as $status => $count) {
|
||
$total['admitted'][$status] = ($total['admitted'][$status] ?? 0) + $count;
|
||
}
|
||
|
||
return $total;
|
||
}
|
||
|
||
/**
|
||
* Вспомогательный метод: выполняет upsert для пациентов, миграций и реанимаций
|
||
*/
|
||
private function upsertBatches(
|
||
int $reportDutyId,
|
||
array $patientBatch,
|
||
array $migrationBatch,
|
||
array $reanimationBatch = []
|
||
): array
|
||
{
|
||
if (empty($patientBatch)) {
|
||
return ['saved_patients' => 0, 'saved_migrations' => 0, 'saved_reanimations' => 0];
|
||
}
|
||
|
||
$savedPatients = 0;
|
||
$savedMigrations = 0;
|
||
$savedReanimations = 0;
|
||
|
||
DB::transaction(function () use (
|
||
$reportDutyId,
|
||
$patientBatch,
|
||
$migrationBatch,
|
||
$reanimationBatch,
|
||
&$savedPatients,
|
||
&$savedMigrations,
|
||
&$savedReanimations
|
||
) {
|
||
// === 1. UPSERT пациентов ===
|
||
$patientUniqueBy = ['report_duty_id', 'source_type', 'original_id'];
|
||
$patientUpdateColumns = array_diff(array_keys($patientBatch[0]), $patientUniqueBy);
|
||
|
||
DB::table('report_duty_patients')->upsert(
|
||
$patientBatch,
|
||
$patientUniqueBy,
|
||
$patientUpdateColumns
|
||
);
|
||
$savedPatients = count($patientBatch);
|
||
|
||
// === 2. Получаем ID сохранённых пациентов для связи с миграциями ===
|
||
$patientIds = [];
|
||
if (!empty($migrationBatch) || !empty($reanimationBatch)) {
|
||
$patientIds = DB::table('report_duty_patients')
|
||
->where('report_duty_id', $reportDutyId)
|
||
->pluck('id', 'original_id')
|
||
->toArray();
|
||
}
|
||
|
||
// === 3. UPSERT миграций ===
|
||
$migrationIds = []; // [original_id => migration_db_id]
|
||
if (!empty($migrationBatch)) {
|
||
$finalMigrations = [];
|
||
foreach ($migrationBatch as $m) {
|
||
$originalId = $m['_temp_key']['original_id'];
|
||
if (isset($patientIds[$originalId])) {
|
||
$finalMigrations[] = [
|
||
'medical_history_id' => $patientIds[$originalId],
|
||
'original_id' => $m['original_id'] ?? null,
|
||
'ingoing_date' => $m['ingoing_date'],
|
||
'out_date' => $m['out_date'],
|
||
'diagnosis_id' => $m['diagnosis_id'],
|
||
'diagnosis_code' => $m['diagnosis_code'],
|
||
'diagnosis_name' => $m['diagnosis_name'],
|
||
'interrupted_event_id'=> $m['interrupted_event_id'],
|
||
'stationar_branch_id' => $m['stationar_branch_id'],
|
||
'department_id' => $m['department_id'],
|
||
'visit_result_id' => $m['visit_result_id'],
|
||
'stat_cure_result_id' => $m['stat_cure_result_id'],
|
||
'user_id' => $m['user_id'],
|
||
'mis_user_id' => $m['mis_user_id'],
|
||
'comment' => $m['comment'],
|
||
];
|
||
}
|
||
}
|
||
|
||
if (!empty($finalMigrations)) {
|
||
$migrationUniqueBy = ['medical_history_id', 'ingoing_date'];
|
||
$migrationUpdateColumns = array_diff(array_keys($finalMigrations[0]), $migrationUniqueBy);
|
||
|
||
DB::table('report_duty_migration_patients')->upsert(
|
||
$finalMigrations,
|
||
$migrationUniqueBy,
|
||
$migrationUpdateColumns
|
||
);
|
||
$savedMigrations = count($finalMigrations);
|
||
|
||
// === 4. Получаем ID сохранённых миграций для связи с реанимациями ===
|
||
// Ключ: [medical_history_id][original_id] => migration_db_id
|
||
$savedMigrationRecords = DB::table('report_duty_migration_patients')
|
||
->whereIn('medical_history_id', array_values($patientIds))
|
||
->get(['id', 'medical_history_id', 'original_id']);
|
||
|
||
foreach ($savedMigrationRecords as $record) {
|
||
$migrationIds[$record->medical_history_id][$record->original_id] = $record->id;
|
||
}
|
||
}
|
||
}
|
||
|
||
// === 5. UPSERT реанимаций ===
|
||
if (!empty($reanimationBatch) && !empty($patientIds) && !empty($migrationIds)) {
|
||
$finalReanimations = [];
|
||
foreach ($reanimationBatch as $r) {
|
||
$tempKey = $r['_temp_key'];
|
||
$patientOriginalId = $tempKey['patient_id'] ?? $tempKey['original_id'] ?? null;
|
||
$migrationOriginalId = $tempKey['migration_id'] ?? null;
|
||
$medicalHistoryId = $patientIds[$patientOriginalId] ?? null;
|
||
|
||
// Ищем ID миграции по medical_history_id + original_id
|
||
$migrationDbId = $migrationIds[$medicalHistoryId][$migrationOriginalId] ?? null;
|
||
|
||
if ($medicalHistoryId && $migrationDbId) {
|
||
$finalReanimations[] = [
|
||
'migration_patient_id' => $migrationDbId,
|
||
'medical_history_id' => $medicalHistoryId,
|
||
'original_id' => $r['original_id'] ?? null,
|
||
'in_date' => $r['in_date'],
|
||
'out_date' => $r['out_date'],
|
||
'description' => $r['description'],
|
||
'stationar_branch_id' => $r['stationar_branch_id'],
|
||
'migration_stationar_branch_id' => $r['migration_stationar_branch_id'],
|
||
'migration_department_id' => $r['migration_department_id'],
|
||
'doctor_id' => $r['doctor_id'],
|
||
'user_id' => $r['user_id'],
|
||
'mis_user_id' => $r['mis_user_id'],
|
||
'comment' => $r['comment'],
|
||
];
|
||
}
|
||
}
|
||
|
||
if (!empty($finalReanimations)) {
|
||
$reanimationUniqueBy = ['migration_patient_id', 'in_date'];
|
||
$reanimationUpdateColumns = array_diff(array_keys($finalReanimations[0]), $reanimationUniqueBy);
|
||
|
||
DB::table('report_duty_reanimations')->upsert(
|
||
$finalReanimations,
|
||
$reanimationUniqueBy,
|
||
$reanimationUpdateColumns
|
||
);
|
||
$savedReanimations = count($finalReanimations);
|
||
}
|
||
}
|
||
});
|
||
|
||
return [
|
||
'saved_patients' => $savedPatients,
|
||
'saved_migrations' => $savedMigrations,
|
||
'saved_reanimations' => $savedReanimations,
|
||
// Эти поля считаются в saveReportSnapshot, здесь передаём 0
|
||
'total_bed_days' => 0,
|
||
'total_preop_bed_days' => 0,
|
||
'patients_with_operations' => 0,
|
||
'total_operations' => 0,
|
||
'planned_operations' => 0,
|
||
'urgent_operations' => 0,
|
||
'total_patients' => 0,
|
||
];
|
||
}
|
||
|
||
public function saveMetrics(array $stats, ReportDuty $reportDuty, int $staff = 0)
|
||
{
|
||
$byStatus = $stats['by_status'] ?? [];
|
||
$byUrgency = $stats['by_urgency'] ?? [];
|
||
$admitted = $stats['admitted'] ?? [];
|
||
|
||
// === Базовые счётчики ===
|
||
$patientsIsRecipient = $byStatus['recipient'] ?? 0;
|
||
$patientsInDepartment = $byStatus['in_department'] ?? 0;
|
||
$patientsIsDischarged = $stats['outcome'] ?? 0;
|
||
$patientsIsTransferred = $stats['transferred'] ?? 0;
|
||
$patientsIsDeceased = $byStatus['deceased'] ?? 0;
|
||
|
||
// Ср. койко-день (по закрытым эпизодам: выписка/перевод/смерть)
|
||
$totalPatients = $stats['total_patients'] ?? 0;
|
||
$totalBedDays = $stats['total_bed_days'] ?? 0;
|
||
$avgBedDay = $totalPatients > 0
|
||
? round($totalBedDays / $totalPatients, 2)
|
||
: 0;
|
||
|
||
// Пред. опер. койко-день (средний по пациентам с операциями)
|
||
$patientsWithOps = $stats['patients_with_operations'] ?? 0;
|
||
$totalPreOpDays = $stats['total_preop_bed_days'] ?? 0;
|
||
$avgPreOpBedDay = $patientsWithOps > 0
|
||
? round($totalPreOpDays / $patientsWithOps, 2)
|
||
: 0;
|
||
|
||
// % загруженности
|
||
$bedsInDepartment = DepartmentMetrikaDefault::where('rf_department_id', $reportDuty->rf_department_id)
|
||
->where('rf_metrika_item_id', 1) // ID метрики "коек"
|
||
->where('date_end', '>', Carbon::now())
|
||
->value('value') ?? 0;
|
||
|
||
$occupancyPercent = $bedsInDepartment > 0
|
||
? round(($patientsInDepartment * 100) / $bedsInDepartment, 2)
|
||
: 0;
|
||
|
||
// % летальности
|
||
$totalPatients = $stats['total_patients'] ?? 0;
|
||
$mortalityPercent = $totalPatients > 0
|
||
? round(($patientsIsDeceased * 100) / $totalPatients, 2)
|
||
: 0;
|
||
|
||
// Операции (общее количество)
|
||
$totalOperations = $stats['total_operations'] ?? 0;
|
||
$plannedOperations = $stats['planned_operations'] ?? 0;
|
||
$urgentOperations = $stats['urgent_operations'] ?? 0;
|
||
|
||
// === СОХРАНЕНИЕ МЕТРИК ===
|
||
$this->saveMetric($reportDuty->id, 1, $bedsInDepartment); // Кол-во коек
|
||
$this->saveMetric($reportDuty->id, 8, $patientsInDepartment); // Пациентов в отделении
|
||
$this->saveMetric($reportDuty->id, 3, $patientsIsRecipient); // Поступило
|
||
$this->saveMetric($reportDuty->id, 15, $patientsIsDischarged); // Выписано
|
||
$this->saveMetric($reportDuty->id, 7, $patientsIsDischarged); // Выписано
|
||
$this->saveMetric($reportDuty->id, 13, $patientsIsTransferred); // Переведено
|
||
$this->saveMetric($reportDuty->id, 9, $patientsIsDeceased); // Умерло
|
||
|
||
$this->saveMetric($reportDuty->id, 4, $admitted['planned'] ?? 0); // Планово поступило
|
||
$this->saveMetric($reportDuty->id, 12, $admitted['urgent'] ?? 0); // Экстренно поступило
|
||
|
||
$this->saveMetric($reportDuty->id, 22, $occupancyPercent); // % загруженности
|
||
$this->saveMetric($reportDuty->id, 25, round($totalBedDays, 2)); // Всего койко-дней
|
||
$this->saveMetric($reportDuty->id, 18, $avgBedDay); // Ср. койко-день
|
||
$this->saveMetric($reportDuty->id, 26, round($totalPreOpDays, 2)); // Пред. опер. койко-день (сумма)
|
||
$this->saveMetric($reportDuty->id, 27, $patientsWithOps); // Пациентов с операциями (знаменатель)
|
||
$this->saveMetric($reportDuty->id, 21, $avgPreOpBedDay); // Ср. Пред. опер. койко-день
|
||
$this->saveMetric($reportDuty->id, 19, $mortalityPercent); // % летальности
|
||
$this->saveMetric($reportDuty->id, 11, $plannedOperations); // Плановых операций
|
||
$this->saveMetric($reportDuty->id, 10, $urgentOperations); // Экстренных операций
|
||
$this->saveMetric($reportDuty->id, 17, $staff); // Мед. персонал
|
||
}
|
||
|
||
/**
|
||
* Рассчитать количество дней пересечения двух периодов
|
||
*/
|
||
private function calculateBedDays(
|
||
?string $ingoingDate,
|
||
?string $outDate,
|
||
string $periodStart,
|
||
string $periodEnd
|
||
): int {
|
||
if (!$ingoingDate || !$outDate) {
|
||
return 0;
|
||
}
|
||
|
||
$start = Carbon::parse($ingoingDate);
|
||
$end = Carbon::parse($outDate);
|
||
|
||
return max(1, $start->diffInDays($end, true));
|
||
}
|
||
|
||
private function saveMetric(int $reportId, int $metricId, int|float $value)
|
||
{
|
||
DutyReportMetricResult::updateOrCreate(
|
||
['rf_report_id' => $reportId, 'rf_metrika_item_id' => $metricId],
|
||
['value' => $value]
|
||
);
|
||
}
|
||
|
||
public function saveObservables(array $observables, ReportDuty $reportDuty)
|
||
{
|
||
foreach ($observables as $observable) {
|
||
ObservableMedicalHistory::updateOrCreate([
|
||
'original_id' => $observable['original_id'],
|
||
'source_type' => $observable['source_type'],
|
||
'observable_in' => $observable['observable_in'] ?? $reportDuty->period_start,
|
||
], [
|
||
'source_type' => $observable['source_type'],
|
||
'original_id' => $observable['original_id'],
|
||
'observable_in' => $observable['observable_in'] ?? $reportDuty->period_start,
|
||
'observable_out' => $observable['observable_out'] ?? null,
|
||
'observable_reason' => $observable['observable_reason'],
|
||
'out_reason' => $observable['out_reason'] ?? null,
|
||
'medical_card_number' => $observable['medical_card_number'],
|
||
'full_name' => $observable['full_name'],
|
||
'birth_date' => $observable['birth_date'],
|
||
'recipient_date' => $observable['recipient_date'],
|
||
'extract_date' => $observable['extract_date'],
|
||
'death_date' => $observable['death_date'],
|
||
'male' => $observable['male'],
|
||
'urgency_id' => $observable['urgency_id'],
|
||
'hospital_result_id' => $observable['hospital_result_id'],
|
||
'visit_result_id' => $observable['visit_result_id'],
|
||
'comment' => $observable['comment'],
|
||
'user_id' => $observable['user_id'],
|
||
]);
|
||
}
|
||
|
||
return $reportDuty->unwantedEvents()->count();
|
||
}
|
||
|
||
public function saveUnwantedEvents(array $unwantedEvents, ReportDuty $reportDuty)
|
||
{
|
||
foreach ($unwantedEvents as $unwantedEvent) {
|
||
DutyUnwantedEvent::updateOrCreate([
|
||
'id' => $unwantedEvent['id'] ?? null,
|
||
'report_duty_id' => $reportDuty->id,
|
||
], [
|
||
'report_duty_id' => $reportDuty->id,
|
||
'title' => $unwantedEvent['title'],
|
||
'comment' => $unwantedEvent['comment']
|
||
]);
|
||
}
|
||
|
||
return $reportDuty->unwantedEvents()->count();
|
||
}
|
||
}
|