Профиль хирургии

This commit is contained in:
brusnitsyn
2026-03-25 17:37:32 +09:00
parent 52a80ccd3b
commit f566ab96df
75 changed files with 3841 additions and 1009 deletions

View File

@@ -6,7 +6,9 @@ use App\Models\Department;
use App\Models\MedicalHistorySnapshot;
use App\Models\MetrikaResult;
use App\Models\MisStationarBranch;
use App\Models\ObservationPatient;
use App\Models\Report;
use App\Models\UnwantedEvent;
use App\Models\User;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
@@ -73,6 +75,8 @@ class AutoReportService
if ($existingReport && $force) {
MetrikaResult::where('rf_report_id', $existingReport->report_id)->delete();
MedicalHistorySnapshot::where('rf_report_id', $existingReport->report_id)->delete();
UnwantedEvent::where('rf_report_id', $existingReport->report_id)->delete();
ObservationPatient::where('rf_report_id', $existingReport->report_id)->delete();
$existingReport->delete();
}
@@ -228,13 +232,14 @@ class AutoReportService
'metrika_item_12' => $metrics['emergency'] ?? 0, // экстренные
'metrika_item_3' => $metrics['recipient'] ?? 0, // поступившие
// 'metrika_item_6' => ($metrics['plan_surgery'] ?? 0) + ($metrics['emergency_surgery'] ?? 0), // всего операций
'metrika_item_7' => $metrics['discharged'] ?? 0, // выписанные
'metrika_item_7' => $metrics['discharged'] + $metrics['deceased'], // выписанные
'metrika_item_8' => $metrics['current'] ?? 0, // текущие
'metrika_item_9' => $metrics['deceased'] ?? 0, // умершие
'metrika_item_11' => $metrics['plan_surgery'] ?? 0, // плановые операции
'metrika_item_10' => $metrics['emergency_surgery'] ?? 0, // экстренные операции
'metrika_item_13' => $metrics['transferred'] ?? 0, // переведенные
'metrika_item_14' => 0, // под наблюдением (будет заполнено отдельно)
'metrika_item_15' => $metrics['discharged'] ?? 0, // выбыло
];
}

View File

@@ -0,0 +1,45 @@
<?php
// app/Services/Base/BaseMetricService.php
namespace App\Services\Base;
use App\Contracts\MetricCalculatorInterface;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
abstract class BaseMetricService
{
protected array $cache = [];
protected array $config = [];
abstract protected function getMetricId(): int;
abstract protected function calculate(array $departmentIds, string $startDate, string $endDate): array;
public function getCached(array $departmentIds, string $startDate, string $endDate): array
{
$cacheKey = $this->getCacheKey($departmentIds, $startDate, $endDate);
if (isset($this->cache[$cacheKey])) {
return $this->cache[$cacheKey];
}
try {
$result = $this->calculate($departmentIds, $startDate, $endDate);
$this->cache[$cacheKey] = $result;
return $result;
} catch (\Exception $e) {
Log::error("Error in " . static::class . ": " . $e->getMessage());
return array_fill_keys($departmentIds, 0);
}
}
protected function getCacheKey(array $departmentIds, string $startDate, string $endDate): string
{
return static::class . '_' . md5(implode(',', $departmentIds) . $startDate . $endDate);
}
public function clearCache(): void
{
$this->cache = [];
}
}

View File

@@ -36,12 +36,14 @@ class BedDayService
// Для одного дня берем последние 30 дней для статистической значимости
$actualStartDate = $isRangeOneDay
? Carbon::parse($endDate)->subDays(30)->format('Y-m-d')
? Carbon::now('Asia/Yakutsk')->startOfYear()->format('Y-m-d')
: $startDate;
// Находим отчеты за период
$reports = Report::where('rf_department_id', $departmentId)
->whereBetween('created_at', [$actualStartDate, $endDate])
// ->whereBetween('created_at', [$actualStartDate, $endDate])
->where('sent_at', '>=', $actualStartDate)
->where('sent_at', '<=', $endDate)
->pluck('report_id');
if ($reports->isEmpty()) {
@@ -107,7 +109,9 @@ class BedDayService
// Находим все отчеты за период по отделениям
$reportsByDepartment = Report::whereIn('rf_department_id', $departmentIds)
->whereBetween('created_at', [$actualStartDate, $endDate])
// ->whereBetween('created_at', [$actualStartDate, $endDate])
->where('sent_at', '>=', $actualStartDate)
->where('sent_at', '<=', $endDate)
->select('report_id', 'rf_department_id')
->get()
->groupBy('rf_department_id');
@@ -186,7 +190,9 @@ class BedDayService
public function getDetailedStatsFromSnapshots(int $departmentId, string $startDate, string $endDate): array
{
$reports = Report::where('rf_department_id', $departmentId)
->whereBetween('created_at', [$startDate, $endDate])
// ->whereBetween('created_at', [$startDate, $endDate])
->where('sent_at', '>', $startDate)
->where('sent_at', '<=', $endDate)
->pluck('report_id');
if ($reports->isEmpty()) {
@@ -292,7 +298,6 @@ class BedDayService
// Для каждого отчета считаем средний койко-день за последние 30 дней до даты отчета
$endDate = $report->created_at;
$startDate = Carbon::startOfYear();
dd($startDate);
$avg = $this->getAverageBedDaysFromSnapshots(
$report->rf_department_id,

View File

@@ -63,6 +63,16 @@ readonly class DateRange
return $this->endDate->getTimestampMs();
}
public function startFirstOfMonth()
{
return $this->startDate->firstOfMonth()->setHour(6)->format('Y-m-d H:i:s');
}
public function endFirstOfMonth()
{
return $this->endDate->firstOfMonth()->setHour(6)->format('Y-m-d H:i:s');
}
/**
* Проверить, является ли дата сегодняшней
*/

View File

@@ -0,0 +1,47 @@
<?php
// app/Services/MetricCalculators/AverageBedDaysCalculator.php
namespace App\Services\MetricCalculators;
use App\Services\Base\BaseMetricService;
use App\Contracts\MetricCalculatorInterface;
use Illuminate\Support\Facades\DB;
class AverageBedDaysCalculator extends BaseMetricService implements MetricCalculatorInterface
{
public function getMetricId(): int
{
return 18;
}
public function calculate(array $departmentIds, string $startDate, string $endDate): array
{
if (empty($departmentIds)) {
return [];
}
$results = DB::table('reports as r')
->join('metrika_results as mr', 'r.report_id', '=', 'mr.rf_report_id')
->whereIn('r.rf_department_id', $departmentIds)
->where('mr.rf_metrika_item_id', 18)
// ->whereBetween('r.created_at', [$startDate, $endDate])
->where('r.sent_at', '>', $startDate)
->where('r.sent_at', '<=', $endDate)
->select(
'r.rf_department_id',
DB::raw('AVG(CAST(mr.value AS DECIMAL)) as avg_value')
)
->groupBy('r.rf_department_id')
->get()
->keyBy('rf_department_id');
$averages = [];
foreach ($departmentIds as $deptId) {
$averages[$deptId] = isset($results[$deptId])
? round((float)$results[$deptId]->avg_value, 1)
: 0;
}
return $averages;
}
}

View File

@@ -0,0 +1,58 @@
<?php
// app/Services/MetricCalculators/LethalityCalculator.php
namespace App\Services\MetricCalculators;
use App\Services\Base\BaseMetricService;
use App\Contracts\MetricCalculatorInterface;
use Illuminate\Support\Facades\DB;
class LethalityCalculator extends BaseMetricService implements MetricCalculatorInterface
{
public function getMetricId(): int
{
return 19;
}
public function calculate(array $departmentIds, string $startDate, string $endDate): array
{
if (empty($departmentIds)) {
return [];
}
$results = DB::table('reports as r')
->join('metrika_results as mr', 'r.report_id', '=', 'mr.rf_report_id')
->whereIn('r.rf_department_id', $departmentIds)
->whereIn('mr.rf_metrika_item_id', [7, 9])
// ->whereBetween('r.created_at', [$startDate, $endDate])
->where('r.sent_at', '>', $startDate)
->where('r.sent_at', '<=', $endDate)
->select(
'r.rf_department_id',
'mr.rf_metrika_item_id',
DB::raw('SUM(CAST(mr.value AS INTEGER)) as total')
)
->groupBy('r.rf_department_id', 'mr.rf_metrika_item_id')
->get()
->groupBy('rf_department_id');
$lethality = [];
foreach ($departmentIds as $deptId) {
$deceased = 0;
$discharged = 0;
if (isset($results[$deptId])) {
foreach ($results[$deptId] as $item) {
if ($item->rf_metrika_item_id == 9) $deceased = (int)$item->total;
else $discharged = (int)$item->total;
}
}
$lethality[$deptId] = ($discharged > 0 && $deceased > 0)
? round(($deceased / $discharged) * 100, 1)
: 0;
}
return $lethality;
}
}

View File

@@ -0,0 +1,99 @@
<?php
// app/Services/MetricCalculators/PreoperativeDaysCalculator.php
namespace App\Services\MetricCalculators;
use App\Services\Base\BaseMetricService;
use App\Contracts\MetricCalculatorInterface;
use App\Models\MedicalHistorySnapshot;
use App\Models\Report;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class PreoperativeDaysCalculator extends BaseMetricService implements MetricCalculatorInterface
{
public function getMetricId(): int
{
return 21;
}
public function calculate(array $departmentIds, string $startDate, string $endDate): array
{
if (empty($departmentIds)) {
return [];
}
// Получаем отчеты за период
$reports = Report::whereIn('rf_department_id', $departmentIds)
->where('sent_at', '>', $startDate)
->where('sent_at', '<=', $endDate)
// ->whereBetween('created_at', [$startDate, $endDate])
->get(['report_id', 'rf_department_id'])
->keyBy('report_id');
if ($reports->isEmpty()) {
return array_fill_keys($departmentIds, 0);
}
$reportIds = $reports->keys()->toArray();
// Получаем пациентов из снапшотов
$snapshots = MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds)
->whereIn('patient_type', ['discharged', 'deceased'])
->get(['rf_report_id', 'rf_medicalhistory_id']);
if ($snapshots->isEmpty()) {
return array_fill_keys($departmentIds, 0);
}
$historyIds = $snapshots->pluck('rf_medicalhistory_id')->unique()->toArray();
// Получаем первые операции и первые поступления одним запросом
$operations = DB::table('stt_surgicaloperation as so')
->join('stt_migrationpatient as mp', 'so.rf_MedicalHistoryID', '=', 'mp.rf_MedicalHistoryID')
->whereIn('so.rf_MedicalHistoryID', $historyIds)
->whereNotNull('so.Date')
->whereNotNull('mp.DateIngoing')
->select(
'so.rf_MedicalHistoryID',
DB::raw('MIN(so."Date") as first_operation'),
DB::raw('MIN(mp."DateIngoing") as first_admission')
)
->groupBy('so.rf_MedicalHistoryID')
->get()
->keyBy('rf_MedicalHistoryID');
// Группируем по отделениям
$results = [];
foreach ($snapshots as $snapshot) {
$deptId = $reports[$snapshot->rf_report_id]->rf_department_id;
$historyId = $snapshot->rf_medicalhistory_id;
if (!isset($operations[$historyId])) {
continue;
}
$op = $operations[$historyId];
$days = Carbon::parse($op->first_admission)
->diffInDays(Carbon::parse($op->first_operation));
if ($days >= 0) {
if (!isset($results[$deptId])) {
$results[$deptId] = ['total' => 0, 'count' => 0];
}
$results[$deptId]['total'] += $days;
$results[$deptId]['count']++;
}
}
// Усредняем по отделениям
$preoperative = [];
foreach ($departmentIds as $deptId) {
$preoperative[$deptId] = isset($results[$deptId]) && $results[$deptId]['count'] > 0
? round($results[$deptId]['total'] / $results[$deptId]['count'], 1)
: 0;
}
return $preoperative;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Services;
use App\Models\Report;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class MetrikaService
{
/**
* Рассчитать предоперационный койко-день из снапшотов
*/
public function calculatePreoperativeDaysFromSnapshots(
array $departmentIds,
string $startDate,
string $endDate
): array
{
if (empty($departmentIds)) {
return [];
}
try {
// Получаем снапшоты с операциями
$results = DB::table('medical_history_snapshots as mhs')
->join('reports as r', 'mhs.rf_report_id', '=', 'r.report_id')
->join('stt_migrationpatient as mp', 'mhs.rf_medicalhistory_id', '=', 'mp.rf_MedicalHistoryID')
->join('stt_surgicaloperation as so', 'mhs.rf_medicalhistory_id', '=', 'so.rf_MedicalHistoryID')
->whereIn('r.rf_department_id', $departmentIds)
->whereDate('r.sent_at', '>=', $startDate)
->whereDate('r.sent_at', '<=', $endDate)
->whereIn('mhs.patient_type', ['discharged', 'deceased'])
->select(
'r.rf_department_id',
'mp.rf_MedicalHistoryID',
DB::raw('MIN(mp."DateIngoing") as admission_date'),
DB::raw('MIN(so."Date") as first_operation_date')
)
->groupBy('r.rf_department_id', 'mp.rf_MedicalHistoryID')
->havingRaw('MIN(so."Date") IS NOT NULL')
->get()
->groupBy('rf_department_id');
$preoperativeDays = [];
foreach ($departmentIds as $deptId) {
if (!isset($results[$deptId]) || $results[$deptId]->isEmpty()) {
$preoperativeDays[$deptId] = 0;
continue;
}
$totalDays = 0;
$count = 0;
foreach ($results[$deptId] as $item) {
$admission = Carbon::parse($item->admission_date);
$operation = Carbon::parse($item->first_operation_date);
$days = $admission->diffInDays($operation);
if ($days >= 0) {
$totalDays += $days;
$count++;
}
}
$preoperativeDays[$deptId] = $count > 0 ? round($totalDays / $count, 1) : 0;
}
return $preoperativeDays;
} catch (\Exception $e) {
\Log::error("Error in calculatePreoperativeDaysFromSnapshots: " . $e->getMessage());
return array_fill_keys($departmentIds, 0);
}
}
}

View File

@@ -71,7 +71,9 @@ class MisPatientService
$query->with('migrations')
->whereHas('migrations', function ($q) use ($branchId, $dateRange) {
$q->where('rf_StationarBranchID', $branchId)
->whereBetween('DateIngoing', $dateRange);
->where('DateIngoing', '>', $dateRange[0])
->where('DateIngoing', '<=', $dateRange[1]);
// ->whereBetween('DateIngoing', $dateRange);
});
})
->whereIn('rf_EmerSignID', [2, 4])
@@ -92,7 +94,9 @@ class MisPatientService
$query->with('migrations')
->whereHas('migrations', function ($q) use ($branchId, $dateRange) {
$q->where('rf_StationarBranchID', $branchId)
->whereBetween('DateIngoing', $dateRange);
->where('DateIngoing', '>=', $dateRange[0])
->where('DateIngoing', '<=', $dateRange[1]);
// ->whereBetween('DateIngoing', $dateRange);
})->where('MedicalHistoryID', '<>', 0);
return $query;

View File

@@ -59,15 +59,15 @@ class PatientService
$query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds)
->with([
'surgicalOperations' => function ($q) use ($dateRange) {
$q->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]);
// $q->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]);
$q->where('Date', '>=', $dateRange->startSql())
->where('Date', '<=', $dateRange->endSql());
},
'migrations' => function ($q) use ($branchId) {
$q->where('rf_StationarBranchID', $branchId)
->take(1) // берем только одну последнюю
->with(['diagnosis' => function ($q) {
$q->where('rf_DiagnosTypeID', 3)
->take(1)
->with('mkb');
->with(['mainDiagnosis' => function ($q) {
$q->with('mkb');
}]);
}
])
@@ -133,7 +133,9 @@ class PatientService
return MisMedicalHistory::whereIn('MedicalHistoryID', $allIds)
->with(['surgicalOperations' => function ($q) use ($dateRange) {
$q->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]);
// $q->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]);
$q->where('Date', '>=', $dateRange->startSql())
->where('Date', '<=', $dateRange->endSql());
}])
->orderBy('DateRecipient', 'DESC')
->get()
@@ -161,7 +163,14 @@ class PatientService
}
if ($onlyIds) $patients = $query->pluck('MedicalHistoryID');
else $patients = $query->get();
else {
// Загрузка отношений, необходимых для FormattedPatientResource
$query->with([
'outcomeMigration.mainDiagnosis.mkb', // mkb через диагноз
'surgicalOperations.serviceMedical', // операции с услугами
]);
$patients = $query->get();
}
return $patients->map(function ($patient) {
$patient->comment = $patient->observationPatient
@@ -221,7 +230,9 @@ class PatientService
bool $countOnly = false
) {
$query = MisSurgicalOperation::where('rf_StationarBranchID', $branchId)
->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]);
->where('Date', '>=', $dateRange->startSql())
->where('Date', '<=', $dateRange->endSql());
// ->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]);
if ($type === 'plan') {
$query->where('rf_TypeSurgOperationInTimeID', 6);
@@ -273,10 +284,14 @@ class PatientService
) {
if ($isHeadOrAdmin) {
$query = MisMigrationPatient::whereInDepartment($branchId)
->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
->where('DateIngoing', '>=', $dateRange->startSql())
->where('DateIngoing', '<=', $dateRange->endSql());
// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
} else {
$query = MisMigrationPatient::currentlyInTreatment($branchId)
->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
->where('DateIngoing', '>=', $dateRange->startSql())
->where('DateIngoing', '<=', $dateRange->endSql());
// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
}
$medicalHistoryIds = $query->pluck('rf_MedicalHistoryID')->toArray();
@@ -315,19 +330,27 @@ class PatientService
// Заведующий: все поступившие за период
if ($fillableAuto) {
$query = LifeMisMigrationPatient::whereInDepartment($branchId)
->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
->where('DateIngoing', '>=', $dateRange->startSql())
->where('DateIngoing', '<=', $dateRange->endSql());
// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
} else {
$query = MisMigrationPatient::whereInDepartment($branchId)
->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
->where('DateIngoing', '>=', $dateRange->startSql())
->where('DateIngoing', '<=', $dateRange->endSql());
// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
}
} else {
// Врач: только поступившие за сутки
if ($fillableAuto) {
$query = LifeMisMigrationPatient::whereInDepartment($branchId)
->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
->where('DateIngoing', '>=', $dateRange->startSql())
->where('DateIngoing', '<=', $dateRange->endSql());
// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
} else {
$query = MisMigrationPatient::whereInDepartment($branchId)
->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
->where('DateIngoing', '>=', $dateRange->startSql())
->where('DateIngoing', '<=', $dateRange->endSql());
// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
};
}

View File

@@ -50,35 +50,36 @@ class ReportService
*/
public function storeReport(array $data, User $user, $fillableAuto = false): Report
{
DB::beginTransaction();
try {
$report = DB::transaction(function () use ($data, $user, $fillableAuto) {
$report = $this->createOrUpdateReport($data, $user);
// Сохраняем все, что НЕ зависит от других отчетов
$this->saveMetrics($report, $data['metrics'] ?? []);
$this->saveUnwantedEvents($report, $data['unwantedEvents'] ?? []);
$this->saveObservationPatients($report, $data['observationPatients'] ?? [], $user->rf_department_id);
// Сохраняем снапшоты пациентов
$this->snapshotService->createPatientSnapshots($report, $user, $data['dates'], $fillableAuto);
return $report;
});
DB::transaction(function () use ($report) {
// Сохраняем метрику среднего койко-дня из снапшотов
$this->saveAverageBedDaysMetricFromSnapshots($report);
DB::commit();
$this->saveLethalMetricFromSnapshots($report);
// ОЧИСТКА КЭША ПОСЛЕ УСПЕШНОГО СОЗДАНИЯ ОТЧЕТА
$this->clearCacheAfterReportCreation($user, $report);
$this->savePreoperativeMetric($report);
return $report;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
$this->saveDepartmentLoadedMetric($report);
});
$this->clearCacheAfterReportCreation($user, $report);
return $report;
}
/**
* Сохранить метрику среднего койко-дня из снапшотов отчета
* Сохранить метрику койко-дня из снапшотов отчета
*/
protected function saveAverageBedDaysMetricFromSnapshots(Report $report): void
{
@@ -128,7 +129,7 @@ class ReportService
}
}
$avgBedDays = $validCount > 0 ? round($totalDays / $validCount, 1) : 0;
$bedDays = $validCount > 0 ? $totalDays: 0;
// Сохраняем метрику
MetrikaResult::updateOrCreate(
@@ -136,10 +137,10 @@ class ReportService
'rf_report_id' => $report->report_id,
'rf_metrika_item_id' => 18,
],
['value' => $avgBedDays]
['value' => $bedDays]
);
\Log::info("Saved average bed days metric for report {$report->report_id}: {$avgBedDays} (from {$validCount} patients)");
//\Log::info("Saved average bed days metric for report {$report->report_id}: {$avgBedDays} (from {$validCount} patients)");
} catch (\Exception $e) {
\Log::error("Failed to save average bed days metric: " . $e->getMessage());
@@ -147,16 +148,120 @@ class ReportService
}
}
protected function saveLethalMetricFromSnapshots(Report $report): void
{
// Получаем все снапшоты выписанных пациентов из этого отчета
$snapshots = MedicalHistorySnapshot::where('rf_report_id', $report->report_id)
->whereIn('patient_type', ['discharged', 'deceased']) // выписанные и умершие
->with('medicalHistory')
->get();
if ($snapshots->isEmpty()) {
// Если нет выписанных, сохраняем 0
MetrikaResult::updateOrCreate(
[
'rf_report_id' => $report->report_id,
'rf_metrika_item_id' => 18,
],
['value' => 0]
);
\Log::info("No discharged patients in report {$report->report_id}, saved 0");
return;
}
}
/**
* Сохранить предоперационный койко-день из снапшотов
*/
protected function savePreoperativeMetric(Report $report): void
{
// 1. Получаем ВСЕ предыдущие отчеты этого отделения
$allPreviousReports = Report::where('rf_department_id', $report->rf_department_id)
->where('sent_at', '<=', $report->sent_at)
->orderBy('sent_at')
->pluck('report_id');
if ($allPreviousReports->isEmpty()) {
$this->saveMetric($report, 21, 0);
return;
}
// 2. Получаем ВСЕХ пациентов из всех отчетов (discharged + deceased)
$allPatients = MedicalHistorySnapshot::whereIn('rf_report_id', $allPreviousReports)
->whereIn('patient_type', ['discharged', 'deceased'])
->pluck('rf_medicalhistory_id')
->unique();
if ($allPatients->isEmpty()) {
$this->saveMetric($report, 21, 0);
return;
}
// 3. Получаем операции для ВСЕХ пациентов
$operations = DB::table('stt_surgicaloperation as so')
->join('stt_migrationpatient as mp', 'so.rf_MedicalHistoryID', '=', 'mp.rf_MedicalHistoryID')
->whereIn('so.rf_MedicalHistoryID', $allPatients)
->whereNotNull('so.Date')
->whereNotNull('mp.DateIngoing')
->select(
'so.rf_MedicalHistoryID',
DB::raw('MIN(so."Date") as first_operation'),
DB::raw('MIN(mp."DateIngoing") as first_admission')
)
->groupBy('so.rf_MedicalHistoryID')
->get();
if ($operations->isEmpty()) {
$this->saveMetric($report, 21, 0);
return;
}
// 4. Считаем общее количество дней и пациентов
$totalDays = 0;
$patientCount = 0;
foreach ($operations as $op) {
$days = Carbon::parse($op->first_admission)
->diffInDays(Carbon::parse($op->first_operation));
if ($days >= 0) {
$totalDays += $days;
$patientCount++;
}
}
// 5. Нарастающий итог = общее количество дней / общее количество пациентов
$avgDays = $patientCount > 0 ? round($totalDays / $patientCount, 1) : 0;
// 6. Сохраняем метрику
$this->saveMetric($report, 21, $avgDays);
}
/**
* Сохранить предоперационный койко-день из снапшотов
*/
protected function saveDepartmentLoadedMetric(Report $report): void
{
// Получаем все снапшоты выписанных пациентов из этого отчета
$currentCount = $report->metrikaResults()->where('rf_metrika_item_id', 8)->value('value');
$bedsCount = $report->metrikaResults()->where('rf_metrika_item_id', 1)->value('value');
$percentLoaded = $bedsCount > 0 ? round($currentCount * 100 / $bedsCount) : 0;
$this->saveMetric($report, 22, $percentLoaded);
}
/**
* Очистить кэш после создания отчета
*/
private function clearCacheAfterReportCreation(User $user, Report $report): void
{
// Очищаем кэш статистики для пользователя
$this->statisticsService->clearStatisticsCache($user);
// $this->statisticsService->clearStatisticsCache($user);
// Также можно очистить кэш для всех пользователей отдела
$this->statisticsService->clearDepartmentStatisticsCache($user->rf_department_id);
// $this->statisticsService->clearDepartmentStatisticsCache($user->rf_department_id);
// Очищаем кэш за сегодня и вчера (так как отчеты влияют на эти даты)
$this->clearDailyCache($user, $report->created_at);
@@ -305,6 +410,22 @@ class ReportService
}
}
/**
* Сохранить метрику отчета
*/
private function saveMetric(Report $report, int $metrikaId, float $value): void
{
MetrikaResult::updateOrCreate(
[
'rf_report_id' => $report->report_id,
'rf_metrika_item_id' => $metrikaId,
],
[
'value' => $value,
]
);
}
/**
* Сохранить нежелательные события
*/
@@ -460,6 +581,7 @@ class ReportService
);
$reportIds = $reports->pluck('report_id')->toArray();
$lastReport = array_first($reportIds);
// Получаем статистику из снапшотов
$snapshotStats = [
@@ -471,7 +593,8 @@ class ReportService
// 'discharged' => $this->getMetrikaResultCount('discharged', $reportIds),
'transferred' => $this->getMetrikaResultCount(13, $reportIds),
'recipient' => $this->getMetrikaResultCount(3, $reportIds),
'beds' => $this->getMetrikaResultCount(1, $reportIds, false)
'beds' => $this->getMetrikaResultCount(1, $reportIds, false),
'countStaff' => $this->getMetrikaResultCount(17, [$lastReport], false)
];
// Получаем ID поступивших пациентов
@@ -499,6 +622,7 @@ class ReportService
'extractCount' => $snapshotStats['outcome'] ?? 0,
'currentCount' => $snapshotStats['current'] ?? 0,//$this->calculateCurrentPatientsFromSnapshots($reportIds, $branchId),
'deadCount' => $snapshotStats['deceased'] ?? 0,
'countStaff' => $snapshotStats['countStaff'] ?? 0,
'surgicalCount' => $surgicalCount,
'recipientIds' => $recipientIds,
'beds' => $snapshotStats['beds'] ?? 0,
@@ -819,7 +943,8 @@ class ReportService
{
return UnwantedEvent::whereHas('report', function ($query) use ($department, $dateRange) {
$query->where('rf_department_id', $department->department_id)
->whereBetween('sent_at', [$dateRange->startSql(), $dateRange->endSql()]);
->whereDate('sent_at', '>=', $dateRange->startSql())
->whereDate('sent_at', '<=', $dateRange->endSql());
})
->get()
->map(function ($item) {
@@ -876,14 +1001,16 @@ class ReportService
{
if ($dateRange->isOneDay) {
return Report::where('rf_department_id', $departmentId)
->whereDate('created_at', $dateRange->endSql())
->orderBy('created_at', 'ASC')
->whereDate('sent_at', $dateRange->endSql())
->orderBy('sent_at', 'DESC')
->get();
}
return Report::where('rf_department_id', $departmentId)
->whereBetween('created_at', [$dateRange->startSql(), $dateRange->endSql()])
->orderBy('created_at', 'ASC')
// ->whereBetween('created_at', [$dateRange->startSql(), $dateRange->endSql()])
->where('sent_at', '>', $dateRange->startSql())
->where('sent_at', '<=', $dateRange->endSql())
->orderBy('sent_at', 'DESC')
->get();
}
@@ -972,4 +1099,44 @@ class ReportService
return $patient;
});
}
/**
* Получить статистику выполнения плана по госпитализации
*/
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);
$progress = 0;
$query = $department->reports()
->with('metrikaResults')
->where('sent_at', '>=', $dateRange->startSql())
->where('sent_at', '<=', $dateRange->endSql());
if ($dateRange->isOneDay) {
$query->where('sent_at', '>=', $dateRange->startFirstOfMonth())
->where('sent_at', '<=', $dateRange->endSql());
} else {
$query->where('sent_at', '>=', $dateRange->startSql())
->where('sent_at', '<=', $dateRange->endSql());
}
$reports = $query->get();
foreach ($reports as $report) {
$outcome = $report->metrikaResults()->where('rf_metrika_item_id', 7)->first();
if ($outcome) $progress += (int)$outcome->value;
}
return [
'plan' => $periodPlan,
'progress' => $progress
];
}
}

View File

@@ -11,6 +11,7 @@ use App\Models\Report;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class SnapshotService
{
@@ -182,28 +183,41 @@ class SnapshotService
?int $branchId = null,
bool $onlyIds = false
): Collection {
// Для плановых и экстренных включаем уже лечащихся
// $includeCurrent = in_array($type, ['plan', 'emergency']);
$medicalHistoryIds = MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds)
// Получаем ID историй болезни напрямую через DB::table() — это быстрее
$medicalHistoryIds = DB::table('medical_history_snapshots')
->select('rf_medicalhistory_id')
->whereIn('rf_report_id', $reportIds)
->where('patient_type', $type)
->pluck('rf_medicalhistory_id')
->unique()
->toArray();
->distinct()
->pluck('rf_medicalhistory_id');
if (empty($medicalHistoryIds)) {
if ($medicalHistoryIds->isEmpty()) {
return collect();
}
if ($onlyIds) {
return collect($medicalHistoryIds);
$query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds);
if ($type === 'plan') {
$query->plan();
} elseif ($type === 'emergency') {
$query->emergency();
}
return MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds)
->when($type === 'plan', fn($q) => $q->plan())
->when($type === 'emergency', fn($q) => $q->emergency())
->orderBy('DateRecipient', 'DESC')
->get();
// Загрузка отношений, необходимых для FormattedPatientResource
$query->with([
'outcomeMigration.mainDiagnosis.mkb', // mkb через диагноз
'surgicalOperations.serviceMedical', // операции с услугами
]);
$query->orderBy('DateRecipient', 'DESC');
$results = $query->get();
if ($onlyIds) {
return $results->pluck('MedicalHistoryID');
}
return $results;
}
/**

File diff suppressed because it is too large Load Diff