From e646ded338b95743e52f5d9989be9ff180ab7c55 Mon Sep 17 00:00:00 2001 From: brusnitsyn Date: Mon, 22 Jun 2026 16:57:56 +0900 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D0=BB=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=80=D1=8B=D0=B5=20=D0=BE=D1=82=D1=87=D0=B5=D1=82?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Web/Admin/ReportTemplateController.php | 148 -------- app/Models/ReportTemplate.php | 26 -- .../Reports/BuiltIn/DutyDoctorReport.php | 49 --- .../Reports/BuiltIn/HeadNurseReport.php | 106 ------ .../Reports/Contracts/ReportDefinition.php | 24 -- .../Reports/Export/ReportExcelExport.php | 62 ---- .../Reports/Export/ReportPdfExport.php | 16 - app/Services/Reports/PatientQueryBuilder.php | 118 ------ app/Services/Reports/ReportPayload.php | 16 - app/Services/Reports/ReportRegistry.php | 66 ---- app/Services/Reports/ReportSection.php | 16 - app/Services/Reports/ReportSource.php | 63 ---- app/Services/Reports/ReportSourceRegistry.php | 342 ------------------ .../Reports/TemplateReportDefinition.php | 63 ---- .../Components/SectionEditor.vue | 92 ----- .../js/Pages/Admin/ReportTemplates/Form.vue | 135 ------- .../js/Pages/Admin/ReportTemplates/Index.vue | 135 ------- resources/js/Pages/Reports/Index.vue | 306 ---------------- resources/views/reports/pdf.blade.php | 85 ----- 19 files changed, 1868 deletions(-) delete mode 100644 app/Http/Controllers/Web/Admin/ReportTemplateController.php delete mode 100644 app/Models/ReportTemplate.php delete mode 100644 app/Services/Reports/BuiltIn/DutyDoctorReport.php delete mode 100644 app/Services/Reports/BuiltIn/HeadNurseReport.php delete mode 100644 app/Services/Reports/Contracts/ReportDefinition.php delete mode 100644 app/Services/Reports/Export/ReportExcelExport.php delete mode 100644 app/Services/Reports/Export/ReportPdfExport.php delete mode 100644 app/Services/Reports/PatientQueryBuilder.php delete mode 100644 app/Services/Reports/ReportPayload.php delete mode 100644 app/Services/Reports/ReportRegistry.php delete mode 100644 app/Services/Reports/ReportSection.php delete mode 100644 app/Services/Reports/ReportSource.php delete mode 100644 app/Services/Reports/ReportSourceRegistry.php delete mode 100644 app/Services/Reports/TemplateReportDefinition.php delete mode 100644 resources/js/Pages/Admin/ReportTemplates/Components/SectionEditor.vue delete mode 100644 resources/js/Pages/Admin/ReportTemplates/Form.vue delete mode 100644 resources/js/Pages/Admin/ReportTemplates/Index.vue delete mode 100644 resources/js/Pages/Reports/Index.vue delete mode 100644 resources/views/reports/pdf.blade.php diff --git a/app/Http/Controllers/Web/Admin/ReportTemplateController.php b/app/Http/Controllers/Web/Admin/ReportTemplateController.php deleted file mode 100644 index f746937..0000000 --- a/app/Http/Controllers/Web/Admin/ReportTemplateController.php +++ /dev/null @@ -1,148 +0,0 @@ -authorizeAccess(); - - $templates = ReportTemplate::with('creator')->latest()->get()->map(fn (ReportTemplate $t) => [ - 'id' => $t->id, - 'name' => $t->name, - 'sourceLabels' => collect($t->sections ?? []) - ->map(fn (array $s) => $this->sources->all()[$s['source']]?->label ?? $s['source']) - ->unique() - ->values() - ->all(), - 'sectionsCount' => count($t->sections ?? []), - 'requiredPermissions' => $t->required_permissions ?? [], - 'creator' => $t->creator?->name, - ]); - - return Inertia::render('Admin/ReportTemplates/Index', ['templates' => $templates]); - } - - public function create() - { - $this->authorizeAccess(); - - return Inertia::render('Admin/ReportTemplates/Form', [ - 'template' => null, - 'sources' => $this->sourcesPayload(), - ]); - } - - public function store(Request $request) - { - $this->authorizeAccess(); - - ReportTemplate::create([ - ...$this->validateTemplate($request), - 'created_by' => Auth::id(), - ]); - - return redirect('/admin/report-templates')->with('success', 'Шаблон создан'); - } - - public function edit(ReportTemplate $template) - { - $this->authorizeAccess(); - - return Inertia::render('Admin/ReportTemplates/Form', [ - 'template' => [ - 'id' => $template->id, - 'name' => $template->name, - 'sections' => $template->sections, - 'requiredPermissions' => $template->required_permissions ?? [], - ], - 'sources' => $this->sourcesPayload(), - ]); - } - - public function update(ReportTemplate $template, Request $request) - { - $this->authorizeAccess(); - - $template->update($this->validateTemplate($request)); - - return redirect('/admin/report-templates')->with('success', 'Шаблон сохранён'); - } - - public function destroy(ReportTemplate $template) - { - $this->authorizeAccess(); - - $template->delete(); - - return redirect('/admin/report-templates')->with('success', 'Шаблон удалён'); - } - - private function validateTemplate(Request $request): array - { - $sourceKeys = array_keys($this->sources->all()); - - $validated = $request->validate([ - 'name' => 'required|string|max:255', - 'sections' => 'required|array|min:1', - 'sections.*.source' => ['required', 'string', 'in:'.implode(',', $sourceKeys)], - 'sections.*.title' => 'nullable|string|max:255', - 'sections.*.columns' => 'required|array|min:1', - 'sections.*.columns.*' => 'string', - 'sections.*.filters' => 'nullable|array', - 'sections.*.filters.*.field' => 'required_with:sections.*.filters|string', - 'sections.*.filters.*.value' => 'nullable', - 'required_permissions' => 'nullable|array', - 'required_permissions.*' => 'in:'.implode(',', self::PERMISSION_OPTIONS), - ]); - - // Допускаем только колонки/поля фильтров, реально существующие у источника секции — - // защита от рассинхрона формы и произвольных значений в БД. - $validated['sections'] = array_map(function (array $section) { - $source = $this->sources->get($section['source']); - - return [ - 'source' => $section['source'], - 'title' => $section['title'] ?? $source->label, - 'columns' => array_values(array_intersect($section['columns'], array_keys($source->columns))), - 'filters' => array_values(array_filter( - $section['filters'] ?? [], - fn (array $filter) => array_key_exists($filter['field'], $source->filterableFields) - )), - ]; - }, $validated['sections']); - - $validated['required_permissions'] = array_values($validated['required_permissions'] ?? []); - - return $validated; - } - - private function sourcesPayload(): array - { - return collect($this->sources->all())->map(fn ($source) => [ - 'key' => $source->key, - 'label' => $source->label, - 'columns' => $source->columns, - 'filterableFields' => $source->filterableFields, - ])->values()->all(); - } - - private function authorizeAccess(): void - { - $user = Auth::user(); - - abort_unless($user->isAdmin() || $user->isChiefDoctor() || $user->isDeputyChief(), 403); - } -} diff --git a/app/Models/ReportTemplate.php b/app/Models/ReportTemplate.php deleted file mode 100644 index fe60c8e..0000000 --- a/app/Models/ReportTemplate.php +++ /dev/null @@ -1,26 +0,0 @@ - 'array', - 'required_permissions' => 'array', - ]; - - public function creator(): BelongsTo - { - return $this->belongsTo(User::class, 'created_by'); - } -} diff --git a/app/Services/Reports/BuiltIn/DutyDoctorReport.php b/app/Services/Reports/BuiltIn/DutyDoctorReport.php deleted file mode 100644 index d854a01..0000000 --- a/app/Services/Reports/BuiltIn/DutyDoctorReport.php +++ /dev/null @@ -1,49 +0,0 @@ -sources->get('duty_metrics')->toSection($department, $dateRange), - $this->sources->get('duty_patients')->toSection($department, $dateRange), - $this->sources->get('unwanted_events')->toSection($department, $dateRange), - $this->sources->get('observable_patients')->toSection($department, $dateRange), - ]; - - return new ReportPayload( - title: $this->label(), - meta: [ - 'Отделение' => $department->name_full ?? $department->name_short, - 'Период' => $dateRange->start()->format('d.m.Y H:i').' — '.$dateRange->end()->format('d.m.Y H:i'), - 'Сформирован' => now('Asia/Yakutsk')->format('d.m.Y H:i'), - ], - sections: $sections, - ); - } -} diff --git a/app/Services/Reports/BuiltIn/HeadNurseReport.php b/app/Services/Reports/BuiltIn/HeadNurseReport.php deleted file mode 100644 index 2805ba9..0000000 --- a/app/Services/Reports/BuiltIn/HeadNurseReport.php +++ /dev/null @@ -1,106 +0,0 @@ -buildMetricsSection($department, $dateRange), - $this->sources->get('nurse_patients')->toSection($department, $dateRange, null, [], 'Журнал пациентов'), - ]; - - return new ReportPayload( - title: $this->label(), - meta: [ - 'Отделение' => $department->name_full ?? $department->name_short, - 'Период' => $dateRange->start()->format('d.m.Y H:i').' — '.$dateRange->end()->format('d.m.Y H:i'), - 'Сформирован' => now('Asia/Yakutsk')->format('d.m.Y H:i'), - ], - sections: $sections, - ); - } - - private function buildMetricsSection(Department $department, DateRange $dateRange): ReportSection - { - $reportIds = $this->sources->nurseReports($department, $dateRange)->pluck('id'); - - $counts = [ - 'recipient' => 0, - 'discharged' => 0, - 'transferred' => 0, - 'deceased' => 0, - 'in_department' => 0, - ]; - - if ($reportIds->isNotEmpty()) { - $patients = ReportNursePatient::whereIn('report_nurse_id', $reportIds)->with('migrations')->get(); - - foreach ($patients as $patient) { - match (PatientStatusClassifier::classify($patient, $dateRange)) { - PatientStatusClassifier::STATUS_RECIPIENT => $counts['recipient']++, - PatientStatusClassifier::STATUS_DISCHARGED => $counts['discharged']++, - PatientStatusClassifier::STATUS_TRANSFERRED => $counts['transferred']++, - PatientStatusClassifier::STATUS_DECEASED => $counts['deceased']++, - PatientStatusClassifier::STATUS_IN_DEPARTMENT => $counts['in_department']++, - default => null, - }; - } - } - - $beds = (int) (DepartmentMetrikaDefault::where('rf_department_id', $department->department_id) - ->where('rf_metrika_item_id', MetrikaConfig::BEDS) - ->value('value') ?? 0); - - $occupancy = $beds > 0 ? round($counts['in_department'] * 100 / $beds, 1) : 0; - - $row = [ - 'beds' => $beds, - 'recipient' => $counts['recipient'], - 'discharged' => $counts['discharged'], - 'transferred' => $counts['transferred'], - 'deceased' => $counts['deceased'], - 'in_department' => $counts['in_department'], - 'occupancy_percent' => $occupancy, - ]; - - return new ReportSection('Показатели', [ - 'beds' => 'Коек', - 'recipient' => 'Поступило', - 'discharged' => 'Выписано', - 'transferred' => 'Переведено', - 'deceased' => 'Умерло', - 'in_department' => 'В отделении', - 'occupancy_percent' => 'Занятость, %', - ], [$row]); - } -} diff --git a/app/Services/Reports/Contracts/ReportDefinition.php b/app/Services/Reports/Contracts/ReportDefinition.php deleted file mode 100644 index 8b652c2..0000000 --- a/app/Services/Reports/Contracts/ReportDefinition.php +++ /dev/null @@ -1,24 +0,0 @@ - - */ - public function requiredPermissions(): array; - - public function build(Department $department, DateRange $dateRange): ReportPayload; -} diff --git a/app/Services/Reports/Export/ReportExcelExport.php b/app/Services/Reports/Export/ReportExcelExport.php deleted file mode 100644 index 9e02af6..0000000 --- a/app/Services/Reports/Export/ReportExcelExport.php +++ /dev/null @@ -1,62 +0,0 @@ -metaRows()), - ]; - - foreach ($this->payload->sections as $section) { - $sheets[] = new ArraySheetExport($this->sheetTitle($section->title), $this->sectionRows($section)); - } - - return $sheets; - } - - private function metaRows(): array - { - $rows = [['Показатель', 'Значение']]; - - foreach ($this->payload->meta as $label => $value) { - $rows[] = [$label, $value]; - } - - return $rows; - } - - private function sectionRows(ReportSection $section): array - { - $rows = [array_values($section->columns)]; - - if (empty($section->rows)) { - $rows[] = ['Нет данных за выбранный период']; - } - - foreach ($section->rows as $row) { - $line = []; - foreach (array_keys($section->columns) as $key) { - $line[] = $row[$key] ?? ''; - } - $rows[] = $line; - } - - return $rows; - } - - private function sheetTitle(string $title): string - { - // Excel ограничивает название листа 31 символом и запрещает символы \/?*[]: - return mb_substr(preg_replace('/[\\\\\/\?\*\[\]:]/', ' ', $title), 0, 31); - } -} diff --git a/app/Services/Reports/Export/ReportPdfExport.php b/app/Services/Reports/Export/ReportPdfExport.php deleted file mode 100644 index 8c69294..0000000 --- a/app/Services/Reports/Export/ReportPdfExport.php +++ /dev/null @@ -1,16 +0,0 @@ - $payload]) - ->setPaper('a4', 'portrait'); - } -} diff --git a/app/Services/Reports/PatientQueryBuilder.php b/app/Services/Reports/PatientQueryBuilder.php deleted file mode 100644 index cb4a60e..0000000 --- a/app/Services/Reports/PatientQueryBuilder.php +++ /dev/null @@ -1,118 +0,0 @@ - $this->buildPlanEmergencyQuery($status), - 'outcome', 'outcome-transferred', 'outcome-deceased' => $this->buildOutcomeQuery($status), - 'recipient' => $this->buildRecipientQuery(), - 'current' => $this->buildCurrentQuery(), - default => throw new \InvalidArgumentException("Unknown status: $status"), - }; - - return $onlyIds ? $query->pluck('MedicalHistoryID')->values() : $query->get(); - } - - private function buildPlanEmergencyQuery(string $status) - { - // Логика из getPlanOrEmergencyPatients, но без if/else по роли внутри - $medicalHistoryIds = $this->isHeadOrAdmin - ? MisMigrationPatient::whereInDepartment($this->branchId) - ->whereBetween('DateIngoing', [$this->startDate, $this->endDate]) - ->pluck('rf_MedicalHistoryID') - : MisMigrationPatient::currentlyInTreatment($this->branchId) - ->whereBetween('DateIngoing', [$this->startDate, $this->endDate]) - ->pluck('rf_MedicalHistoryID'); - - if ($medicalHistoryIds->isEmpty()) { - return MisMedicalHistory::query()->whereRaw('1=0'); // пустой запрос - } - - $query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds) - ->with(['surgicalOperations' => fn($q) => $q->whereBetween('Date', [$this->startDate, $this->endDate])]) - ->orderBy('DateRecipient', 'DESC'); - - if ($status === 'plan') { - $query->plan(); - } elseif ($status === 'emergency') { - $query->emergency(); - } - - if (! $this->isHeadOrAdmin) { - $query->currentlyHospitalized(); - } - - return $query; - } - - private function buildOutcomeQuery(string $status): \Illuminate\Database\Eloquent\Builder - { - $visitResultIds = match ($status) { - 'outcome-transferred' => [4, 14], - 'outcome-deceased' => [5, 6, 15, 16], - default => [1, 11, 2, 12, 7, 18, 48], // discharged - }; - - return MisMedicalHistory::query() - ->whereDate('DateExtract', '>', Carbon::parse($this->startDate)->toDateString()) - ->whereDate('DateExtract', '<=', Carbon::parse($this->endDate)->toDateString()) - ->whereHas('migrations', function ($q) use ($visitResultIds) { - $q->where('rf_StationarBranchID', $this->branchId) - ->whereIn('rf_kl_VisitResultID', $visitResultIds); - }) - ->with(['surgicalOperations']) - ->orderBy('DateRecipient', 'DESC'); - } - - private function buildRecipientQuery(string $status): \Illuminate\Database\Eloquent\Builder - { - $visitResultIds = match ($status) { - 'outcome-transferred' => [4, 14], - 'outcome-deceased' => [5, 6, 15, 16], - default => [1, 11, 2, 12, 7, 18, 48], // discharged - }; - - return MisMedicalHistory::query() - ->whereDate('DateExtract', '>', Carbon::parse($this->startDate)->toDateString()) - ->whereDate('DateExtract', '<=', Carbon::parse($this->endDate)->toDateString()) - ->whereHas('migrations', function ($q) use ($visitResultIds) { - $q->where('rf_StationarBranchID', $this->branchId) - ->whereIn('rf_kl_VisitResultID', $visitResultIds); - }) - ->with(['surgicalOperations']) - ->orderBy('DateRecipient', 'DESC'); - } - private function buildCurrentQuery(string $status): \Illuminate\Database\Eloquent\Builder - { - $visitResultIds = match ($status) { - 'outcome-transferred' => [4, 14], - 'outcome-deceased' => [5, 6, 15, 16], - default => [1, 11, 2, 12, 7, 18, 48], // discharged - }; - - return MisMedicalHistory::query() - ->whereDate('DateExtract', '>', Carbon::parse($this->startDate)->toDateString()) - ->whereDate('DateExtract', '<=', Carbon::parse($this->endDate)->toDateString()) - ->whereHas('migrations', function ($q) use ($visitResultIds) { - $q->where('rf_StationarBranchID', $this->branchId) - ->whereIn('rf_kl_VisitResultID', $visitResultIds); - }) - ->with(['surgicalOperations']) - ->orderBy('DateRecipient', 'DESC'); - } -} diff --git a/app/Services/Reports/ReportPayload.php b/app/Services/Reports/ReportPayload.php deleted file mode 100644 index a0d4eb6..0000000 --- a/app/Services/Reports/ReportPayload.php +++ /dev/null @@ -1,16 +0,0 @@ - $meta пары "показатель => значение" для шапки отчёта - * @param ReportSection[] $sections - */ - public function __construct( - public string $title, - public array $meta, - public array $sections, - ) {} -} diff --git a/app/Services/Reports/ReportRegistry.php b/app/Services/Reports/ReportRegistry.php deleted file mode 100644 index f2ba795..0000000 --- a/app/Services/Reports/ReportRegistry.php +++ /dev/null @@ -1,66 +0,0 @@ -sources), - new HeadNurseReport($this->sources), - ]; - - foreach (ReportTemplate::all() as $template) { - $definitions[] = new TemplateReportDefinition($template, $this->sources); - } - - return $definitions; - } - - /** @return ReportDefinition[] */ - public function availableFor(User $user): array - { - return array_values(array_filter( - $this->all(), - fn (ReportDefinition $definition) => $this->isVisible($definition, $user) - )); - } - - public function find(string $code, User $user): ?ReportDefinition - { - foreach ($this->availableFor($user) as $definition) { - if ($definition->code() === $code) { - return $definition; - } - } - - return null; - } - - private function isVisible(ReportDefinition $definition, User $user): bool - { - $permissions = $definition->requiredPermissions(); - - if (empty($permissions)) { - return $user->currentRoleCan('report.view') || $user->currentRoleCan('nurse.report.view'); - } - - foreach ($permissions as $permission) { - if ($user->currentRoleCan($permission)) { - return true; - } - } - - return false; - } -} diff --git a/app/Services/Reports/ReportSection.php b/app/Services/Reports/ReportSection.php deleted file mode 100644 index a290946..0000000 --- a/app/Services/Reports/ReportSection.php +++ /dev/null @@ -1,16 +0,0 @@ - $columns ключ колонки => подпись - * @param array> $rows строки данных, ключи соответствуют ключам $columns - */ - public function __construct( - public string $title, - public array $columns, - public array $rows, - ) {} -} diff --git a/app/Services/Reports/ReportSource.php b/app/Services/Reports/ReportSource.php deleted file mode 100644 index c2dd6be..0000000 --- a/app/Services/Reports/ReportSource.php +++ /dev/null @@ -1,63 +0,0 @@ - $columns ключ колонки => подпись (все колонки, доступные для этого источника) - * @param array}> $filterableFields allow-list полей, по которым можно фильтровать в конструкторе шаблонов - * @param Closure(Department,DateRange,array,array):array> $resolver - */ - public function __construct( - public string $key, - public string $label, - public array $columns, - public array $filterableFields, - private Closure $resolver, - ) {} - - /** - * @param array $columns ключи выбранных колонок (по умолчанию — все) - * @param array $filters - * @return array> - */ - public function rows(Department $department, DateRange $dateRange, ?array $columns = null, array $filters = []): array - { - $columns ??= array_keys($this->columns); - - return ($this->resolver)($department, $dateRange, $columns, $filters); - } - - public function toSection( - Department $department, - DateRange $dateRange, - ?array $columns = null, - array $filters = [], - ?string $title = null, - ): ReportSection { - $columns ??= array_keys($this->columns); - - $columnDefs = []; - foreach ($columns as $key) { - if (isset($this->columns[$key])) { - $columnDefs[$key] = $this->columns[$key]; - } - } - - if (empty($columnDefs)) { - $columnDefs = $this->columns; - $columns = array_keys($this->columns); - } - - return new ReportSection( - $title ?? $this->label, - $columnDefs, - $this->rows($department, $dateRange, $columns, $filters), - ); - } -} diff --git a/app/Services/Reports/ReportSourceRegistry.php b/app/Services/Reports/ReportSourceRegistry.php deleted file mode 100644 index 3c84dea..0000000 --- a/app/Services/Reports/ReportSourceRegistry.php +++ /dev/null @@ -1,342 +0,0 @@ - 'Планово', 2 => 'Экстренно']; - - /** @var array */ - 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 */ - 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 $filters - * @param array $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; - } -} diff --git a/app/Services/Reports/TemplateReportDefinition.php b/app/Services/Reports/TemplateReportDefinition.php deleted file mode 100644 index 383d8ee..0000000 --- a/app/Services/Reports/TemplateReportDefinition.php +++ /dev/null @@ -1,63 +0,0 @@ -template->id; - } - - public function label(): string - { - return $this->template->name; - } - - public function requiredPermissions(): array - { - return $this->template->required_permissions ?? []; - } - - public function build(Department $department, DateRange $dateRange): ReportPayload - { - $sections = []; - - foreach ($this->template->sections ?? [] as $sectionConfig) { - $source = $this->sources->get($sectionConfig['source']); - - $sections[] = $source->toSection( - $department, - $dateRange, - $sectionConfig['columns'] ?? null, - $sectionConfig['filters'] ?? [], - $sectionConfig['title'] ?? null, - ); - } - - return new ReportPayload( - title: $this->template->name, - meta: [ - 'Отделение' => $department->name_full ?? $department->name_short, - 'Период' => $dateRange->start()->format('d.m.Y H:i').' — '.$dateRange->end()->format('d.m.Y H:i'), - 'Сформирован' => now('Asia/Yakutsk')->format('d.m.Y H:i'), - ], - sections: $sections, - ); - } -} diff --git a/resources/js/Pages/Admin/ReportTemplates/Components/SectionEditor.vue b/resources/js/Pages/Admin/ReportTemplates/Components/SectionEditor.vue deleted file mode 100644 index 6d28cbe..0000000 --- a/resources/js/Pages/Admin/ReportTemplates/Components/SectionEditor.vue +++ /dev/null @@ -1,92 +0,0 @@ - - - diff --git a/resources/js/Pages/Admin/ReportTemplates/Form.vue b/resources/js/Pages/Admin/ReportTemplates/Form.vue deleted file mode 100644 index 9e1da31..0000000 --- a/resources/js/Pages/Admin/ReportTemplates/Form.vue +++ /dev/null @@ -1,135 +0,0 @@ - - - diff --git a/resources/js/Pages/Admin/ReportTemplates/Index.vue b/resources/js/Pages/Admin/ReportTemplates/Index.vue deleted file mode 100644 index 2c53f14..0000000 --- a/resources/js/Pages/Admin/ReportTemplates/Index.vue +++ /dev/null @@ -1,135 +0,0 @@ - - - diff --git a/resources/js/Pages/Reports/Index.vue b/resources/js/Pages/Reports/Index.vue deleted file mode 100644 index 1a96f63..0000000 --- a/resources/js/Pages/Reports/Index.vue +++ /dev/null @@ -1,306 +0,0 @@ - - - - - diff --git a/resources/views/reports/pdf.blade.php b/resources/views/reports/pdf.blade.php deleted file mode 100644 index 2f9c1a3..0000000 --- a/resources/views/reports/pdf.blade.php +++ /dev/null @@ -1,85 +0,0 @@ - - - - - {{ $payload->title }} - - - -

{{ $payload->title }}

- - - @foreach ($payload->meta as $label => $value) - - - - - @endforeach -
{{ $label }}{{ $value }}
- - @foreach ($payload->sections as $section) -

{{ $section->title }}

- - @if (empty($section->rows)) -

Нет данных за выбранный период.

- @else - - - - @foreach ($section->columns as $label) - - @endforeach - - - - @foreach ($section->rows as $row) - - @foreach (array_keys($section->columns) as $key) - - @endforeach - - @endforeach - -
{{ $label }}
{{ $row[$key] ?? '' }}
- @endif - @endforeach - -