* оптимизировал запросы выдачи пациентов, сохранения снапшотов

* доработал страницу отчета дежурного
* переделал "действия" над пациентом
* подключил виджеты на странице отчета дежурного
This commit is contained in:
brusnitsyn
2026-05-08 17:04:56 +09:00
parent 6cf1ffbb2b
commit 90e0d04dfd
17 changed files with 818 additions and 292 deletions

View File

@@ -52,52 +52,31 @@ class DutyReportController extends Controller
if ($hasReport) {
$inDepartmentHistories = $this->dutyMedicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id);
$plannedHistories = collect([ 'data' => [] ]);
$emergencyHistories = collect([ 'data' => [] ]);
$recipientHistories = $this->dutyMedicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id);
$dischargedHistories = $this->dutyMedicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id);
$deceasedHistories = $this->dutyMedicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id);
$transferredHistories = $this->dutyMedicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id);
$reanimationHistories = collect([ 'data' => [] ]);
} else if ($this->dateRangeService->isPastPeriod($dateRange)) {
$inDepartmentHistories = collect([]);
$recipientHistories = collect([]);
$dischargedHistories = collect([]);
$deceasedHistories = collect([]);
$transferredHistories = collect([]);
$inDepartmentHistories = collect([ 'data' => [] ]);
$plannedHistories = collect([ 'data' => [] ]);
$emergencyHistories = collect([ 'data' => [] ]);
$recipientHistories = collect([ 'data' => [] ]);
$dischargedHistories = collect([ 'data' => [] ]);
$deceasedHistories = collect([ 'data' => [] ]);
$transferredHistories = collect([ 'data' => [] ]);
$reanimationHistories = collect([ 'data' => [] ]);
} else {
$inDepartmentHistories = MedicalHistoryResource::collection(
$this->medicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id)
);
$plannedHistories = MedicalHistoryResource::collection(
$this->medicalHistoryService->getPlannedHistories($dateRange, $department->rf_mis_department_id)
);
$emergencyHistories = MedicalHistoryResource::collection(
$this->medicalHistoryService->getEmergencyHistories($dateRange, $department->rf_mis_department_id)
);
$recipientHistories = MedicalHistoryResource::collection(
$this->medicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id)
);
$dischargedHistories = MedicalHistoryResource::collection(
$this->medicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id)
);
$deceasedHistories = MedicalHistoryResource::collection(
$this->medicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id)
);
$transferredHistories = MedicalHistoryResource::collection(
$this->medicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id)
);
$reanimationHistories = MedicalHistoryResource::collection(
$this->medicalHistoryService->getReanimationHistories($dateRange, $department->rf_mis_department_id)
);
$patients = $this->medicalHistoryService->getGroupedHistories($dateRange, $department->rf_mis_department_id);
}
return Inertia::render('Report/Index', [
'inDepartmentHistories' => $inDepartmentHistories,
'plannedHistories' => $plannedHistories,
'emergencyHistories' => $emergencyHistories,
'recipientHistories' => $recipientHistories,
'dischargedHistories' => $dischargedHistories,
'deceasedHistories' => $deceasedHistories,
'transferredHistories' => $transferredHistories,
'reanimationHistories' => $reanimationHistories,
'patients' => $patients,
'departmentInfo' => [
// TODO: Добавить вывод информации из шапки
],
'dates' => [
$dateRange->startDate->getTimestampMs(),
$dateRange->endDate->getTimestampMs(),

View File

@@ -44,32 +44,30 @@ class NurseReportController extends Controller
$hasReport = $existsReport && $isPastPeriod;
if ($hasReport) {
$inDepartmentHistories = $this->nurseMedicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id);
$recipientHistories = $this->nurseMedicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id);
$dischargedHistories = $this->nurseMedicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id);
$deceasedHistories = $this->nurseMedicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id);
$transferredHistories = $this->nurseMedicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id);
} else if ($this->dateRangeService->isPastPeriod($dateRange)) {
$inDepartmentHistories = collect([]);
$recipientHistories = collect([]);
$dischargedHistories = collect([]);
$deceasedHistories = collect([]);
$transferredHistories = collect([]);
} else {
$inDepartmentHistories = $this->unifiedMedicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id);
$recipientHistories = $this->unifiedMedicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id);
$dischargedHistories = $this->unifiedMedicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id);
$deceasedHistories = $this->unifiedMedicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id);
$transferredHistories = $this->unifiedMedicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id);
}
// if ($hasReport) {
// $inDepartmentHistories = $this->nurseMedicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id);
// $recipientHistories = $this->nurseMedicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id);
// $dischargedHistories = $this->nurseMedicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id);
// $deceasedHistories = $this->nurseMedicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id);
// $transferredHistories = $this->nurseMedicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id);
// } else if ($this->dateRangeService->isPastPeriod($dateRange)) {
// $inDepartmentHistories = collect([]);
// $recipientHistories = collect([]);
// $dischargedHistories = collect([]);
// $deceasedHistories = collect([]);
// $transferredHistories = collect([]);
// } else {
// $inDepartmentHistories = $this->unifiedMedicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id);
// $recipientHistories = $this->unifiedMedicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id);
// $dischargedHistories = $this->unifiedMedicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id);
// $deceasedHistories = $this->unifiedMedicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id);
// $transferredHistories = $this->unifiedMedicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id);
// }
$data = $this->unifiedMedicalHistoryService->getGroupedHistories($dateRange, $department->rf_mis_department_id);
return Inertia::render('Nurse/Report/Index', [
'inDepartmentHistories' => $inDepartmentHistories,
'recipientHistories' => $recipientHistories,
'dischargedHistories' => $dischargedHistories,
'deceasedHistories' => $deceasedHistories,
'transferredHistories' => $transferredHistories,
'patients' => $data,
'dates' => [
$dateRange->startDate->getTimestampMs(),
$dateRange->endDate->getTimestampMs(),

View File

@@ -104,7 +104,7 @@ class MigrationPatient extends MaterializedViewModel
->where('ingoing_date', '<', $dateRange->startSql())
->wherehas('medicalHistory', function ($query) use ($dateRange) {
$query->whereNull('extract_date');
}); // опционально: поступил не раньше 2 лет назад
});
})
->orWhere(function ($q) use ($dateRange) {
$q->where('ingoing_date', '<=', $dateRange->endSql())

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Services\DateRange;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class UnifiedMigrationPatient extends Model
@@ -89,4 +90,23 @@ class UnifiedMigrationPatient extends Model
->orderBy('ingoing_date', 'desc')
->limit(1)->first();
}
public function getAdmittedInCurrentAttribute(): bool
{
// Получаем дату поступления из последнего движения
$ingoing = $this->ingoing_date;
if (!$ingoing) {
return false;
}
$ingoingLocal = Carbon::parse($ingoing)->setTimezone(config('app.timezone', 'Europe/Moscow'));
$now = Carbon::now(config('app.timezone', 'Europe/Moscow'));
// Окно смены: вчера 09:00 → сегодня 09:00
$shiftStart = $now->copy()->subDay()->setTime(9, 0);
$shiftEnd = $now->copy()->setTime(9, 0);
return $ingoingLocal->between($shiftStart, $shiftEnd);
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace App\Services\Classification;
use App\Models\MedicalHistory;
use App\Models\UnifiedMedicalHistory;
use App\Services\DateRange;
use Illuminate\Support\Carbon;
class PatientStatusClassifier
{
public const STATUS_IN_DEPARTMENT = 'in_department';
public const STATUS_RECIPIENT = 'recipient';
public const STATUS_DISCHARGED = 'discharged';
public const STATUS_TRANSFERRED = 'transferred';
public const STATUS_DECEASED = 'deceased';
public const URGENCY_URGENT = 'urgent'; // urgency_id = 1
public const URGENCY_PLANNED = 'planned'; // urgency_id = 2
public const URGENCY_UNKNOWN = 'unknown';
/**
* Определяет срочность пациента.
*/
public static function classifyUrgency(?int $urgencyId): string
{
return match ($urgencyId) {
1 => self::URGENCY_URGENT,
2 => self::URGENCY_PLANNED,
default => self::URGENCY_UNKNOWN,
};
}
/**
* Определяет поступил ли пациент в диапазоне.
*/
public static function classifyAdmitted(Carbon|string|null $ingoingDate): bool
{
if (is_null($ingoingDate)) {return false;}
$ingoingLocal = Carbon::parse($ingoingDate);
$now = Carbon::now();
// Окно смены: вчера 09:00 → сегодня 09:00
$shiftStart = $now->copy()->subDay()->setTime(9, 0);
$shiftEnd = $now->copy()->setTime(9, 0);
return $ingoingLocal->between($shiftStart, $shiftEnd);
}
/**
* Определяет статус пациента на основе "сырых" полей из БД.
* Логика изолирована и может быть легко протестирована.
*/
public static function classify(UnifiedMedicalHistory|MedicalHistory $history, DateRange $dateRange): string
{
// 1. Смерть — приоритет №1 (не зависит от дат)
if (!empty($history->death_date)) {
return self::STATUS_DECEASED;
}
// 2. Если есть дата выбытия
if (!empty($history->extract_date)) {
// Переведён (коды 3, 4)
if (in_array($history->visit_result_id, [3, 4], true)) {
return self::STATUS_TRANSFERRED;
}
// Выписан домой/иное (исключаем коды 3-6)
if (in_array($history->visit_result_id, [1], true)) {
return self::STATUS_DISCHARGED;
}
}
// 3. Поступившие
if ($history->latestMigration?->getAdmittedInCurrentAttribute()) {
return self::STATUS_RECIPIENT;
}
// 4. В отделении
if (empty($history->latestMigration?->out_date) && $history->latestMigration?->ingoing_date < $dateRange->startDate) {
return self::STATUS_IN_DEPARTMENT;
}
return 'unknown';
}
}

View File

@@ -64,10 +64,10 @@ class DutyReportService
/**
* Сохранить снимок пациентов за период
*/
public function saveSnapshot(DateRange $dateRange, ReportNurse $reportNurse, ?int $departmentId = null, ?int $userId = null): array
public function saveSnapshot(DateRange $dateRange, ReportDuty $reportDuty, ?int $departmentId = null, ?int $userId = null): array
{
$departmentId = $departmentId ?? $reportNurse->department->rf_mis_department_id;
$userId = $userId ?? $reportNurse->rf_user_id;
$departmentId = $departmentId ?? $reportDuty->department->rf_mis_department_id;
$userId = $userId ?? $reportDuty->rf_user_id;
$startYear = Carbon::now()->startOfYear()->format('Y-m-d');
$query = MedicalHistory::query()
@@ -87,7 +87,7 @@ class DutyReportService
// Получаем данные (chunk для памяти, если пациентов > 1000)
$patients = $query->cursor();
$savedStats = $this->saveReportSnapshot($reportNurse->id, $patients, $userId);
$savedStats = $this->saveReportSnapshot($reportDuty->id, $patients, $userId);
return [
...$savedStats,
@@ -109,9 +109,9 @@ class DutyReportService
foreach ($patients as $patient) {
// Подготовка данных пациента
$patientBatch[] = [
'report_nurse_id' => $reportDutyId,
'source_type' => $patient->source_type,
'original_id' => $patient->original_id,
'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,
@@ -184,7 +184,7 @@ class DutyReportService
$patientUniqueBy = ['report_duty_id', 'source_type', 'original_id'];
$patientUpdateColumns = array_diff(array_keys($patientBatch[0]), $patientUniqueBy);
DB::table('report_nurse_patients')->upsert(
DB::table('report_duty_patients')->upsert(
$patientBatch,
$patientUniqueBy,
$patientUpdateColumns

View File

@@ -4,10 +4,74 @@ namespace App\Services;
use App\Models\MedicalHistory;
use App\Models\MigrationPatient;
use App\Services\Classification\PatientStatusClassifier;
use Illuminate\Support\Carbon;
class MedicalHistoryService
{
public function getGroupedHistories(DateRange $dateRange, int $departmentId): array
{
$startYear = Carbon::now()->startOfYear()->format('Y-m-d');
// 1. Один запрос: получаем "сырые" данные (без вычисляемых статусов)
$all = MedicalHistory::query()->whereHas('latestMigration', function ($q) use ($departmentId, $dateRange, $startYear) {
$q->where('department_id', $departmentId)
// пребывание пересекается с отчётным периодом
->where('ingoing_date', '<=', $dateRange->endSql())
->where('ingoing_date', '>=', $startYear)
->where(function ($sub) use ($dateRange) {
$sub->whereNull('out_date')
->orWhere('out_date', '>=', $dateRange->startSql())
->where('out_date', '<=', $dateRange->endSql());
});
})->with(['latestMigration' => function ($q) use ($departmentId) {
$q->where('department_id', $departmentId);
}, 'latestMigration.operations', 'latestMigration.reanimations'])->get();
// 2. Добавляем вычисляемые поля и превращаем в плоский массив
$prepared = $all->map(function (MedicalHistory $h) use ($dateRange) {
$patientStatus = PatientStatusClassifier::classify($h, $dateRange);
$patientUrgency = null;
if (!in_array($patientStatus, [
PatientStatusClassifier::STATUS_DECEASED,
PatientStatusClassifier::STATUS_DISCHARGED,
PatientStatusClassifier::STATUS_TRANSFERRED
])) $patientUrgency = PatientStatusClassifier::classifyUrgency($h->urgency_id);
return [
// Все исходные поля модели (автоматически через toArray)
...$h->toArray(),
// + вычисляемые мета-поля для фронтенда
'patient_status' => $patientStatus,
'patient_urgency' => $patientUrgency,
'admitted_today' => PatientStatusClassifier::classifyAdmitted($h->latestMigration?->ingoing_date),
];
});
// 3. Сортировка
$sortBy = 'recipient_date';
$sortOrder = 'desc';
$sorted = $prepared->sortBy($sortBy, SORT_REGULAR, $sortOrder === 'desc')->values();
// 4. Возвращаем плоский массив + метаданные для фронтенда
return [
'data' => $sorted->toArray(),
'meta' => [
'total' => $sorted->count(),
'sortBy' => $sortBy,
'sortOrder' => $sortOrder,
// Статистика для фильтров/бейджей (опционально)
'counts' => [
'in_department' => $sorted->where('patient_status', 'in_department')->count(),
'discharged' => $sorted->where('patient_status', 'discharged')->count(),
'urgent' => $sorted->where('patient_urgency', 'urgent')->count(),
'planned' => $sorted->where('patient_urgency', 'planned')->count(),
]
]
];
}
public function getHistories(DateRange $dateRange, int $departmentId)
{
$query = MedicalHistory::query();

View File

@@ -5,10 +5,67 @@ namespace App\Services;
use App\Models\MedicalHistory;
use App\Models\MigrationPatient;
use App\Models\UnifiedMedicalHistory;
use App\Services\Classification\PatientStatusClassifier;
use Illuminate\Support\Carbon;
class UnifiedMedicalHistoryService
{
public function getGroupedHistories(DateRange $dateRange, int $departmentId): array
{
// 1. Один запрос: получаем "сырые" данные (без вычисляемых статусов)
$all = UnifiedMedicalHistory::with([
'latestMigration' => fn($q) => $q->where(function ($q) use ($dateRange) {
// Вариант А: Пациент уже лежит (текущий)
$q->whereNull('out_date')
->whereNotNull('medical_history_id')
->where('ingoing_date', '<', $dateRange->startSql());
})
->orWhere(function ($q) use ($dateRange) {
$q->where('ingoing_date', '<=', $dateRange->endSql())
->where('ingoing_date', '>', $dateRange->startSql());
})
])
->whereNull('extract_date')
->get()
// Фильтр по отделению в памяти (быстро для <1000 записей)
->filter(fn($h) => $h->latestMigration?->department_id === $departmentId);
// 2. Добавляем вычисляемые поля и превращаем в плоский массив
$prepared = $all->map(function (UnifiedMedicalHistory $h) use ($dateRange) {
return [
// Все исходные поля модели (автоматически через toArray)
...$h->toArray(),
// + вычисляемые мета-поля для фронтенда
'patient_status' => PatientStatusClassifier::classify($h, $dateRange),
'patient_urgency' => PatientStatusClassifier::classifyUrgency($h->urgency_id),
'admitted_today' => PatientStatusClassifier::classifyAdmitted($h->latestMigration?->ingoing_date),
];
});
// 3. Сортировка
$sortBy = 'recipient_date';
$sortOrder = 'desc';
$sorted = $prepared->sortBy($sortBy, SORT_REGULAR, $sortOrder === 'desc')->values();
// 4. Возвращаем плоский массив + метаданные для фронтенда
return [
'data' => $sorted->toArray(),
'meta' => [
'total' => $sorted->count(),
'sortBy' => $sortBy,
'sortOrder' => $sortOrder,
// Статистика для фильтров/бейджей (опционально)
'counts' => [
'in_department' => $sorted->where('patient_status', 'in_department')->count(),
'discharged' => $sorted->where('patient_status', 'discharged')->count(),
'urgent' => $sorted->where('patient_urgency', 'urgent')->count(),
'planned' => $sorted->where('patient_urgency', 'planned')->count(),
]
]
];
}
public function getHistories(DateRange $dateRange, int $departmentId)
{
$query = UnifiedMedicalHistory::query();
@@ -49,12 +106,10 @@ class UnifiedMedicalHistoryService
public function getDepartmentHistories(DateRange $dateRange, int $departmentId)
{
return UnifiedMedicalHistory::query()
->whereHas('migrations', function ($q) use ($departmentId, $dateRange) {
->whereHas('latestMigration', function ($q) use ($departmentId, $dateRange) {
$q->department($departmentId)->current($dateRange);
})
->with(['latestMigration' => function ($q) use ($departmentId, $dateRange) {
$q->department($departmentId)->current($dateRange); // подгружаем только отфильтрованные движения
}])
->with(['latestMigration'])
->get()
// Сортировка по дате поступления в отделение (поле дочерней таблицы)
->sortByDesc(fn ($mh) => $mh->latestMigration->ingoing_date ?? $mh->recipient_date)
@@ -71,48 +126,40 @@ class UnifiedMedicalHistoryService
$now = Carbon::now();
return UnifiedMedicalHistory::query()
->whereHas('migrations', function ($q) use ($departmentId, $dateRange) {
->whereHas('latestMigration', function ($q) use ($departmentId, $dateRange) {
$q->department($departmentId)->admitted($dateRange->startSql(), $dateRange->endSql());
})
->with(['latestMigration' => function ($q) use ($departmentId) {
$q->department($departmentId);
}])
->with(['latestMigration'])
->get();
}
public function getDischargedHistories(DateRange $dateRange, int $departmentId)
{
return UnifiedMedicalHistory::query()
->whereHas('migrations', function ($q) use ($departmentId, $dateRange) {
->whereHas('latestMigration', function ($q) use ($departmentId, $dateRange) {
$q->department($departmentId)->discharged($dateRange->startSql(), $dateRange->endSql());
})
->with(['latestMigration' => function ($q) use ($departmentId) {
$q->department($departmentId);
}])
->with(['latestMigration'])
->get();
}
public function getDeceasedHistories(DateRange $dateRange, int $departmentId)
{
return UnifiedMedicalHistory::query()
->whereHas('migrations', function ($q) use ($departmentId, $dateRange) {
->whereHas('latestMigration', function ($q) use ($departmentId, $dateRange) {
$q->department($departmentId)->deceased($dateRange->startSql(), $dateRange->endSql());
})
->with(['latestMigration' => function ($q) use ($departmentId) {
$q->department($departmentId);
}])
->with(['latestMigration'])
->get();
}
public function getTransferredHistories(DateRange $dateRange, int $departmentId)
{
return UnifiedMedicalHistory::query()
->whereHas('migrations', function ($q) use ($departmentId, $dateRange) {
->whereHas('latestMigration', function ($q) use ($departmentId, $dateRange) {
$q->department($departmentId)->transferred($dateRange->startSql(), $dateRange->endSql());
})
->with(['latestMigration' => function ($q) use ($departmentId) {
$q->department($departmentId);
}])
->with(['latestMigration'])
->get();
}
}