Модуль отчетов
This commit is contained in:
342
app/Services/Reports/ReportSourceRegistry.php
Normal file
342
app/Services/Reports/ReportSourceRegistry.php
Normal file
@@ -0,0 +1,342 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user