Files
onboard/app/Services/Reports/ReportSourceRegistry.php
2026-06-21 23:40:55 +09:00

343 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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;
}
}