Модуль отчетов
This commit is contained in:
148
app/Http/Controllers/Web/Admin/ReportTemplateController.php
Normal file
148
app/Http/Controllers/Web/Admin/ReportTemplateController.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ReportTemplate;
|
||||
use App\Services\Reports\ReportSourceRegistry;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ReportTemplateController extends Controller
|
||||
{
|
||||
private const PERMISSION_OPTIONS = ['report.view', 'nurse.report.view'];
|
||||
|
||||
public function __construct(protected ReportSourceRegistry $sources) {}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
139
app/Http/Controllers/Web/ReportsController.php
Normal file
139
app/Http/Controllers/Web/ReportsController.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Department;
|
||||
use App\Models\User;
|
||||
use App\Services\DateRangeService;
|
||||
use App\Services\Reports\Contracts\ReportDefinition;
|
||||
use App\Services\Reports\Export\ReportExcelExport;
|
||||
use App\Services\Reports\Export\ReportPdfExport;
|
||||
use App\Services\Reports\ReportRegistry;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class ReportsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected ReportRegistry $reportRegistry,
|
||||
protected DateRangeService $dateRangeService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
$available = $this->reportRegistry->availableFor($user);
|
||||
|
||||
abort_if(empty($available), 403);
|
||||
|
||||
$department = $this->resolveDepartment($request, $user);
|
||||
$dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user);
|
||||
|
||||
// Без явного типа в запросе показываем только список отчётов — без предпросмотра.
|
||||
$type = $request->query('type');
|
||||
$definition = $type ? $this->reportRegistry->find($type, $user) : null;
|
||||
$payload = $definition?->build($department, $dateRange);
|
||||
|
||||
return Inertia::render('Reports/Index', [
|
||||
'reportTypes' => array_map(fn (ReportDefinition $d) => [
|
||||
'code' => $d->code(),
|
||||
'label' => $d->label(),
|
||||
'audience' => $this->audienceLabel($d->requiredPermissions()),
|
||||
], $available),
|
||||
'departments' => collect($user->availableDepartments())->map(fn (Department $d) => [
|
||||
'id' => $d->department_id,
|
||||
'name' => $d->name_full ?? $d->name_short,
|
||||
])->values()->all(),
|
||||
'selectedType' => $definition?->code(),
|
||||
'selectedDepartmentId' => $department->department_id,
|
||||
'isHeadOrAdmin' => $user->isSeniorStaff(),
|
||||
'date' => [
|
||||
$dateRange->start()->getTimestampMs(),
|
||||
$dateRange->end()->getTimestampMs(),
|
||||
],
|
||||
'payload' => $payload ? [
|
||||
'title' => $payload->title,
|
||||
'meta' => $payload->meta,
|
||||
'sections' => array_map(fn ($section) => [
|
||||
'title' => $section->title,
|
||||
'columns' => $section->columns,
|
||||
'rows' => $section->rows,
|
||||
], $payload->sections),
|
||||
] : null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function exportExcel(Request $request)
|
||||
{
|
||||
[$definition, $department, $dateRange] = $this->resolveExportContext($request);
|
||||
|
||||
$payload = $definition->build($department, $dateRange);
|
||||
|
||||
return Excel::download(new ReportExcelExport($payload), $this->fileName($definition, 'xlsx'));
|
||||
}
|
||||
|
||||
public function exportPdf(Request $request)
|
||||
{
|
||||
[$definition, $department, $dateRange] = $this->resolveExportContext($request);
|
||||
|
||||
$payload = $definition->build($department, $dateRange);
|
||||
|
||||
return ReportPdfExport::render($payload)->download($this->fileName($definition, 'pdf'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: ReportDefinition, 1: Department, 2: \App\Services\DateRange}
|
||||
*/
|
||||
private function resolveExportContext(Request $request): array
|
||||
{
|
||||
$user = Auth::user();
|
||||
$type = $request->query('type');
|
||||
$definition = $type ? $this->reportRegistry->find($type, $user) : null;
|
||||
|
||||
abort_if($definition === null, 403);
|
||||
|
||||
$department = $this->resolveDepartment($request, $user);
|
||||
$dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user);
|
||||
|
||||
return [$definition, $department, $dateRange];
|
||||
}
|
||||
|
||||
private function resolveDepartment(Request $request, User $user): Department
|
||||
{
|
||||
$departmentId = $request->query('departmentId');
|
||||
$available = collect($user->availableDepartments());
|
||||
|
||||
if ($departmentId && $available->contains(fn (Department $d) => (int) $d->department_id === (int) $departmentId)) {
|
||||
return $available->first(fn (Department $d) => (int) $d->department_id === (int) $departmentId);
|
||||
}
|
||||
|
||||
return $user->department ?? $available->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $permissions
|
||||
*/
|
||||
private function audienceLabel(array $permissions): string
|
||||
{
|
||||
if (empty($permissions)) {
|
||||
return 'Доступен всем';
|
||||
}
|
||||
|
||||
$labels = [
|
||||
'report.view' => 'Дежурный врач',
|
||||
'nurse.report.view' => 'Старшая медсестра',
|
||||
];
|
||||
|
||||
return implode(' · ', array_map(fn ($p) => $labels[$p] ?? $p, $permissions));
|
||||
}
|
||||
|
||||
private function fileName(ReportDefinition $definition, string $extension): string
|
||||
{
|
||||
$slug = str_replace([' ', ':', '/', '\\'], '_', $definition->label());
|
||||
|
||||
return sprintf('%s_%s.%s', $slug, now('Asia/Yakutsk')->format('Ymd_His'), $extension);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user