343 lines
13 KiB
PHP
343 lines
13 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Reports;
|
||
|
||
use App\Domain\Reports\ValueObjects\MetrikaConfig;
|
||
use App\Models\Department;
|
||
use App\Models\DutyReportMetricResult;
|
||
use App\Models\DutyUnwantedEvent;
|
||
use App\Models\ReportDuty;
|
||
use App\Models\ReportDutyPatient;
|
||
use App\Models\ReportNurse;
|
||
use App\Models\ReportNursePatient;
|
||
use App\Services\DateRange;
|
||
use Illuminate\Database\Eloquent\Builder;
|
||
use Illuminate\Database\Eloquent\Collection;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\DB;
|
||
use InvalidArgumentException;
|
||
|
||
/**
|
||
* Реестр источников данных для отчётов. Каждый источник умеет вернуть набор строк
|
||
* по отделению и периоду — на этих же источниках строятся и встроенные отчёты
|
||
* (дежурный врач / старшая медсестра), и пользовательские шаблоны из конструктора.
|
||
*/
|
||
class ReportSourceRegistry
|
||
{
|
||
private const URGENCY_OPTIONS = [1 => 'Планово', 2 => 'Экстренно'];
|
||
|
||
/** @var array<string,ReportSource> */
|
||
private array $sources;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->sources = [
|
||
'duty_metrics' => $this->dutyMetricsSource(),
|
||
'duty_patients' => $this->dutyPatientsSource(),
|
||
'nurse_patients' => $this->nursePatientsSource(),
|
||
'unwanted_events' => $this->unwantedEventsSource(),
|
||
'observable_patients' => $this->observablePatientsSource(),
|
||
];
|
||
}
|
||
|
||
/** @return array<string,ReportSource> */
|
||
public function all(): array
|
||
{
|
||
return $this->sources;
|
||
}
|
||
|
||
public function get(string $key): ReportSource
|
||
{
|
||
if (! isset($this->sources[$key])) {
|
||
throw new InvalidArgumentException("Неизвестный источник отчёта: {$key}");
|
||
}
|
||
|
||
return $this->sources[$key];
|
||
}
|
||
|
||
/**
|
||
* Сданные дежурные отчёты отделения, пересекающиеся с периодом.
|
||
*/
|
||
public function dutyReports(Department $department, DateRange $dateRange): Collection
|
||
{
|
||
return ReportDuty::where('rf_department_id', $department->department_id)
|
||
->withinPeriod($dateRange->startSql(), $dateRange->endSql())
|
||
->onlySubmitted()
|
||
->orderBy('period_end')
|
||
->get();
|
||
}
|
||
|
||
/**
|
||
* Сданные отчёты (журналы) медсестры отделения, пересекающиеся с периодом.
|
||
*/
|
||
public function nurseReports(Department $department, DateRange $dateRange): Collection
|
||
{
|
||
return ReportNurse::where('rf_department_id', $department->department_id)
|
||
->where('status_id', 2)
|
||
->where('period_end', '>=', $dateRange->startSql())
|
||
->where('period_start', '<=', $dateRange->endSql())
|
||
->orderBy('period_end')
|
||
->get();
|
||
}
|
||
|
||
private function dutyMetricsSource(): ReportSource
|
||
{
|
||
$columns = [
|
||
'period' => 'Период',
|
||
'beds' => 'Коек',
|
||
'recipient_plan' => 'Поступило плановых',
|
||
'recipient_emergency' => 'Поступило экстренных',
|
||
'discharged' => 'Выписано',
|
||
'transferred' => 'Переведено',
|
||
'deceased' => 'Умерло',
|
||
'occupancy_percent' => 'Занятость, %',
|
||
'avg_bed_days' => 'Ср. койко-день',
|
||
'lethality_percent' => 'Летальность, %',
|
||
'surgery_plan' => 'Операции плановые',
|
||
'surgery_emergency' => 'Операции экстренные',
|
||
'staff_count' => 'Мед. персонал',
|
||
];
|
||
|
||
$metricMap = [
|
||
'beds' => MetrikaConfig::BEDS,
|
||
'recipient_plan' => MetrikaConfig::PLAN,
|
||
'recipient_emergency' => MetrikaConfig::EMERGENCY,
|
||
'discharged' => MetrikaConfig::DISCHARGED,
|
||
'transferred' => MetrikaConfig::TRANSFERRED,
|
||
'deceased' => MetrikaConfig::DECEASED,
|
||
'occupancy_percent' => MetrikaConfig::DEPARTMENT_LOADED,
|
||
'avg_bed_days' => MetrikaConfig::AVERAGE_BED_DAYS,
|
||
'lethality_percent' => MetrikaConfig::LETHALITY,
|
||
'surgery_plan' => MetrikaConfig::PLAN_SURGERY,
|
||
'surgery_emergency' => MetrikaConfig::EMERGENCY_SURGERY,
|
||
'staff_count' => MetrikaConfig::STAFF_COUNT,
|
||
];
|
||
|
||
$resolver = function (Department $department, DateRange $dateRange, array $columns) use ($metricMap) {
|
||
$reports = $this->dutyReports($department, $dateRange);
|
||
|
||
if ($reports->isEmpty()) {
|
||
return [];
|
||
}
|
||
|
||
$values = DutyReportMetricResult::whereIn('rf_report_id', $reports->pluck('id'))
|
||
->get(['rf_report_id', 'rf_metrika_item_id', 'value'])
|
||
->groupBy('rf_report_id');
|
||
|
||
$rows = [];
|
||
foreach ($reports as $report) {
|
||
$byMetric = ($values->get($report->id) ?? collect())->pluck('value', 'rf_metrika_item_id');
|
||
|
||
$row = [];
|
||
foreach ($columns as $key) {
|
||
if ($key === 'period') {
|
||
$row[$key] = $report->period_start?->format('d.m.Y H:i').' — '.$report->period_end?->format('d.m.Y H:i');
|
||
|
||
continue;
|
||
}
|
||
|
||
$metricId = $metricMap[$key] ?? null;
|
||
$row[$key] = $metricId !== null ? ($byMetric->get($metricId) ?? 0) : null;
|
||
}
|
||
|
||
$rows[] = $row;
|
||
}
|
||
|
||
return $rows;
|
||
};
|
||
|
||
return new ReportSource('duty_metrics', 'Показатели смены (дежурный врач)', $columns, [], $resolver);
|
||
}
|
||
|
||
private function patientColumns(): array
|
||
{
|
||
return [
|
||
'full_name' => 'ФИО',
|
||
'birth_date' => 'Дата рождения',
|
||
'medical_card_number' => '№ карты',
|
||
'recipient_date' => 'Дата поступления',
|
||
'extract_date' => 'Дата выбытия',
|
||
'diagnosis_code' => 'Код диагноза',
|
||
'diagnosis_name' => 'Диагноз',
|
||
'urgency' => 'Срочность',
|
||
'outcome' => 'Исход',
|
||
];
|
||
}
|
||
|
||
private function patientFilterableFields(): array
|
||
{
|
||
return [
|
||
'urgency_id' => ['label' => 'Срочность', 'options' => self::URGENCY_OPTIONS],
|
||
'visit_result_id' => ['label' => 'Код исхода', 'options' => null],
|
||
];
|
||
}
|
||
|
||
private function dutyPatientsSource(): ReportSource
|
||
{
|
||
$columns = $this->patientColumns();
|
||
$filterable = $this->patientFilterableFields();
|
||
|
||
$resolver = function (Department $department, DateRange $dateRange, array $columns, array $filters) {
|
||
$reportIds = $this->dutyReports($department, $dateRange)->pluck('id');
|
||
|
||
if ($reportIds->isEmpty()) {
|
||
return [];
|
||
}
|
||
|
||
$query = ReportDutyPatient::whereIn('report_duty_id', $reportIds)->with('latestMigration');
|
||
$this->applyEqualityFilters($query, $filters, ['urgency_id', 'visit_result_id']);
|
||
|
||
return $query->get()->map(fn ($patient) => $this->mapPatientRow($patient, $columns))->all();
|
||
};
|
||
|
||
return new ReportSource('duty_patients', 'Пациенты (дежурный врач)', $columns, $filterable, $resolver);
|
||
}
|
||
|
||
private function nursePatientsSource(): ReportSource
|
||
{
|
||
$columns = $this->patientColumns();
|
||
$filterable = $this->patientFilterableFields();
|
||
|
||
$resolver = function (Department $department, DateRange $dateRange, array $columns, array $filters) {
|
||
$reportIds = $this->nurseReports($department, $dateRange)->pluck('id');
|
||
|
||
if ($reportIds->isEmpty()) {
|
||
return [];
|
||
}
|
||
|
||
$query = ReportNursePatient::whereIn('report_nurse_id', $reportIds)->with('latestMigration');
|
||
$this->applyEqualityFilters($query, $filters, ['urgency_id', 'visit_result_id']);
|
||
|
||
return $query->get()->map(fn ($patient) => $this->mapPatientRow($patient, $columns))->all();
|
||
};
|
||
|
||
return new ReportSource('nurse_patients', 'Журнал пациентов (старшая медсестра)', $columns, $filterable, $resolver);
|
||
}
|
||
|
||
private function unwantedEventsSource(): ReportSource
|
||
{
|
||
$columns = [
|
||
'title' => 'Событие',
|
||
'comment' => 'Комментарий',
|
||
'created_at' => 'Дата',
|
||
];
|
||
|
||
$resolver = function (Department $department, DateRange $dateRange) {
|
||
$reportIds = $this->dutyReports($department, $dateRange)->pluck('id');
|
||
|
||
if ($reportIds->isEmpty()) {
|
||
return [];
|
||
}
|
||
|
||
return DutyUnwantedEvent::whereIn('report_duty_id', $reportIds)->get()
|
||
->map(fn ($event) => [
|
||
'title' => $event->title,
|
||
'comment' => $event->comment,
|
||
'created_at' => $event->created_at?->format('d.m.Y H:i'),
|
||
])->all();
|
||
};
|
||
|
||
return new ReportSource('unwanted_events', 'Нежелательные события', $columns, [], $resolver);
|
||
}
|
||
|
||
private function observablePatientsSource(): ReportSource
|
||
{
|
||
$columns = [
|
||
'full_name' => 'ФИО',
|
||
'birth_date' => 'Дата рождения',
|
||
'observable_in' => 'Начало наблюдения',
|
||
'observable_out' => 'Конец наблюдения',
|
||
'observable_reason' => 'Причина',
|
||
];
|
||
|
||
$resolver = function (Department $department, DateRange $dateRange) {
|
||
$reportIds = $this->dutyReports($department, $dateRange)->pluck('id');
|
||
|
||
if ($reportIds->isEmpty()) {
|
||
return [];
|
||
}
|
||
|
||
return DB::table('observable_medical_histories as omh')
|
||
->join('report_duty_patients as rdp', 'rdp.original_id', '=', 'omh.original_id')
|
||
->whereIn('rdp.report_duty_id', $reportIds)
|
||
->where('omh.observable_in', '>=', $dateRange->startSql())
|
||
->where('omh.observable_in', '<=', $dateRange->endSql())
|
||
->select('omh.full_name', 'omh.birth_date', 'omh.observable_in', 'omh.observable_out', 'omh.observable_reason')
|
||
->distinct()
|
||
->get()
|
||
->map(fn ($row) => [
|
||
'full_name' => $row->full_name,
|
||
'birth_date' => $this->formatDate($row->birth_date, 'd.m.Y'),
|
||
'observable_in' => $this->formatDate($row->observable_in, 'd.m.Y H:i'),
|
||
'observable_out' => $this->formatDate($row->observable_out, 'd.m.Y H:i'),
|
||
'observable_reason' => $row->observable_reason,
|
||
])->all();
|
||
};
|
||
|
||
return new ReportSource('observable_patients', 'Пациенты на контроле', $columns, [], $resolver);
|
||
}
|
||
|
||
private function mapPatientRow(ReportDutyPatient|ReportNursePatient $patient, array $columns): array
|
||
{
|
||
$migration = $patient->latestMigration;
|
||
|
||
$row = [];
|
||
foreach ($columns as $key) {
|
||
$row[$key] = match ($key) {
|
||
'full_name' => $patient->full_name,
|
||
'birth_date' => $patient->birth_date?->format('d.m.Y'),
|
||
'medical_card_number' => $patient->medical_card_number,
|
||
'recipient_date' => $patient->recipient_date?->format('d.m.Y H:i'),
|
||
'extract_date' => $patient->extract_date?->format('d.m.Y H:i'),
|
||
'diagnosis_code' => $migration?->diagnosis_code,
|
||
'diagnosis_name' => $migration?->diagnosis_name,
|
||
'urgency' => self::URGENCY_OPTIONS[(int) $patient->urgency_id] ?? '—',
|
||
'outcome' => $this->outcomeLabel($patient),
|
||
default => null,
|
||
};
|
||
}
|
||
|
||
return $row;
|
||
}
|
||
|
||
private function outcomeLabel(ReportDutyPatient|ReportNursePatient $patient): string
|
||
{
|
||
if ($patient->death_date) {
|
||
return 'Умер';
|
||
}
|
||
|
||
if (in_array((int) $patient->visit_result_id, [4, 14], true)) {
|
||
return 'Переведён';
|
||
}
|
||
|
||
if ($patient->extract_date) {
|
||
return 'Выписан';
|
||
}
|
||
|
||
return 'В отделении';
|
||
}
|
||
|
||
/**
|
||
* @param array<int,array{field:string,value:mixed}> $filters
|
||
* @param array<int,string> $allowedFields allow-list полей источника — защита от произвольных имён колонок
|
||
*/
|
||
private function applyEqualityFilters(Builder $query, array $filters, array $allowedFields): void
|
||
{
|
||
foreach ($filters as $filter) {
|
||
$field = $filter['field'] ?? null;
|
||
$value = $filter['value'] ?? null;
|
||
|
||
if ($field === null || $value === null || $value === '' || ! in_array($field, $allowedFields, true)) {
|
||
continue;
|
||
}
|
||
|
||
$query->where($field, $value);
|
||
}
|
||
}
|
||
|
||
private function formatDate(?string $value, string $format): ?string
|
||
{
|
||
return $value ? Carbon::parse($value)->format($format) : null;
|
||
}
|
||
}
|