Files
onboard/app/Http/Controllers/Web/ReportsController.php
2026-06-22 16:54:00 +09:00

400 lines
16 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\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Models\CustomMeasure;
use App\Models\Department;
use App\Models\MisLpuDoctor;
use App\Models\ReportDocument;
use App\Models\User;
use App\Services\Analytics\AnalyticsQuery;
use App\Services\Analytics\DataSetRegistry;
use App\Services\Analytics\Export\AnalyticsExcelExport;
use App\Services\Analytics\Export\AnalyticsPdfExport;
use App\Services\Analytics\Measure;
use App\Services\Analytics\ReportPresetRegistry;
use App\Services\DateRangeService;
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 DataSetRegistry $datasets,
protected ReportPresetRegistry $presets,
protected DateRangeService $dateRangeService,
) {}
/** Каталог: системные шаблоны + сохранённые отчёты филиала. */
public function index()
{
$user = Auth::user();
$documents = ReportDocument::with('creator')->latest('updated_at')->get()->map(fn (ReportDocument $d) => [
'id' => $d->id,
'name' => $d->name,
'description' => $d->description,
'datasetLabel' => $this->datasets->has($d->dataset) ? $this->datasets->get($d->dataset)->label() : $d->dataset,
'author' => $d->creator?->name,
'updatedAt' => $d->updated_at?->format('d.m.Y'),
]);
return Inertia::render('Analytics/Index', [
'documents' => $documents,
'presets' => $this->presets->all(),
'categories' => $this->presets->categories(),
'canManage' => $this->canManage($user),
]);
}
/** Редактор нового отчёта (с нуля или из пресета). */
public function create(Request $request)
{
$user = Auth::user();
abort_unless($this->canManage($user), 403);
$preset = $request->query('preset') ? $this->presets->find($request->query('preset')) : null;
$config = $preset['config'] ?? $this->presets->find('blank')['config'];
return $this->renderBuilder($request, $user, [
'document' => null,
'name' => $preset && $preset['key'] !== 'blank' ? $preset['label'] : 'Новый отчёт',
'description' => '',
'config' => $config,
]);
}
/** Редактор существующего отчёта. */
public function show(Request $request, ReportDocument $document)
{
$user = Auth::user();
return $this->renderBuilder($request, $user, [
'document' => ['id' => $document->id],
'name' => $document->name,
'description' => $document->description,
// dataset хранится отдельной колонкой — возвращаем его внутри config для редактора
'config' => [...($document->config ?? []), 'dataset' => $document->dataset],
]);
}
/** Построение отчёта (живой предпросмотр) — JSON. */
public function run(Request $request)
{
$user = Auth::user();
$query = $this->buildQuery($request, $user);
return response()->json($this->datasets->get($query->datasetKey)->run($query)->toArray());
}
public function store(Request $request)
{
$user = Auth::user();
abort_unless($this->canManage($user), 403);
$document = ReportDocument::create([
...$this->validateDocument($request),
'created_by' => Auth::id(),
]);
return redirect("/reports/{$document->id}")->with('success', 'Отчёт сохранён');
}
public function update(Request $request, ReportDocument $document)
{
$user = Auth::user();
abort_unless($this->canManage($user), 403);
$document->update($this->validateDocument($request));
return back()->with('success', 'Отчёт сохранён');
}
public function destroy(ReportDocument $document)
{
abort_unless($this->canManage(Auth::user()), 403);
$document->delete();
return redirect('/reports')->with('success', 'Отчёт удалён');
}
public function duplicate(ReportDocument $document)
{
abort_unless($this->canManage(Auth::user()), 403);
$copy = $document->replicate(['created_by']);
$copy->name = $document->name.' (копия)';
$copy->created_by = Auth::id();
$copy->save();
return redirect("/reports/{$copy->id}")->with('success', 'Создана копия');
}
public function exportExcel(Request $request)
{
$result = $this->buildResult($request);
return Excel::download(new AnalyticsExcelExport($result['result'], $result['title']), $this->fileName($result['title'], 'xlsx'));
}
public function exportPdf(Request $request)
{
$result = $this->buildResult($request);
return AnalyticsPdfExport::render($result['result'], $result['title'])->download($this->fileName($result['title'], 'pdf'));
}
// --- helpers ---
private function renderBuilder(Request $request, User $user, array $payload): \Inertia\Response
{
$department = $this->resolveDepartment($request, $user);
$dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user);
return Inertia::render('Analytics/Builder', [
...$payload,
'datasets' => $this->datasetsMetadata($department),
'presets' => $this->presets->all(),
'categories' => $this->presets->categories(),
'departments' => collect($user->misDepartments)->map(fn (Department $d) => [
'id' => $d->department_id,
'name' => $d->name_full ?? $d->name_short,
])->values()->all(),
'selectedDepartmentId' => $department->department_id,
'isHeadOrAdmin' => $user->isSeniorStaff(),
'canManage' => $this->canManage($user),
'date' => [$dateRange->start()->getTimestampMs(), $dateRange->end()->getTimestampMs()],
]);
}
/**
* Метаданные датасетов: + пользовательские показатели и динамические опции
* фильтров (например, список врачей отделения).
*
* @return array<int,array<string,mixed>>
*/
private function datasetsMetadata(Department $department): array
{
$meta = $this->datasets->metadata();
$options = []; // ленивый кэш динамических опций по источнику
foreach ($meta as &$dataset) {
foreach ($this->customMeasuresFor($dataset['key']) as $custom) {
$dataset['measures'][] = ['key' => $custom->key, 'label' => $custom->label, 'unit' => $custom->unit];
}
foreach ($dataset['filters'] as &$filter) {
$source = $filter['optionsSource'] ?? null;
if ($source === 'doctors' || $source === 'nurse_doctors') {
$options[$source] ??= $this->staffOptions($department, $source === 'nurse_doctors' ? 'nurse' : 'duty');
$filter['options'] = $options[$source];
}
}
unset($filter);
}
return $meta;
}
/**
* Врачи/медсёстры отделения из справочника МИС (по должностям в МИС-отделении).
* Список не зависит от того, проставлен ли врач в уже сданных сменах — он готов
* к работе сразу, фильтрация идёт по rf_lpudoctor_id = LPUDoctorID.
*
* @param 'duty'|'nurse' $kind
* @return array<int,array{label:string,value:int}>
*/
private function staffOptions(Department $department, string $kind): array
{
$misIds = array_filter([$department->rf_mis_department_id]);
if ($misIds === []) {
return [];
}
$prvdIds = $kind === 'nurse' ? [1567, 1629] : [];
$fallback = $kind === 'nurse' ? 'Медсестра' : 'Врач';
return MisLpuDoctor::active()
->whereNotIn('LPUDoctorID', [0, 1])
->whereHas('prvds', function ($q) use ($misIds, $prvdIds) {
$q->whereIn('rf_DepartmentID', $misIds)
->whereDate('D_END', '2222-01-01 00:00:00.000000')
->when($prvdIds !== [], fn ($qq) => $qq->whereIn('rf_PRVDID', $prvdIds));
})
->orderBy('FAM_V')
->get(['LPUDoctorID', 'FAM_V', 'IM_V', 'OT_V'])
->map(fn ($d) => [
'label' => trim("{$d->FAM_V} {$d->IM_V} {$d->OT_V}") ?: "{$fallback} #{$d->LPUDoctorID}",
'value' => (int) $d->LPUDoctorID,
])
->all();
}
private function buildQuery(Request $request, User $user): AnalyticsQuery
{
$datasetKey = (string) $request->input('dataset');
abort_unless($this->datasets->has($datasetKey), 422);
$dataset = $this->datasets->get($datasetKey);
$customMeasures = $this->customMeasuresFor($datasetKey);
$dimKeys = array_map(fn ($d) => $d->key, $dataset->dimensions());
$measureKeys = [
...array_map(fn ($m) => $m->key, $dataset->measures()),
...array_map(fn (Measure $m) => $m->key, $customMeasures),
];
$filterKeys = array_map(fn ($f) => $f->key, $dataset->filters());
$dimensions = array_values(array_intersect((array) $request->input('dimensions', []), $dimKeys));
$measures = array_values(array_intersect((array) $request->input('measures', []), $measureKeys));
$filters = [];
foreach ((array) $request->input('filters', []) as $filter) {
if (isset($filter['key']) && in_array($filter['key'], $filterKeys, true)) {
$filters[] = ['key' => $filter['key'], 'value' => $filter['value'] ?? null];
}
}
$mode = $request->input('mode') === 'dynamics' ? 'dynamics' : 'period';
$detalization = in_array($request->input('detalization'), ['day', 'week', 'month'], true)
? $request->input('detalization') : 'month';
return new AnalyticsQuery(
datasetKey: $datasetKey,
dimensions: $dimensions,
measures: $measures,
filters: $filters,
mode: $mode,
detalization: $detalization,
department: $this->resolveDepartment($request, $user),
dateRange: $this->dateRangeService->getDateRangeFromRequest($request, $user),
chart: (array) $request->input('chart', []),
customMeasures: $customMeasures,
user: $user,
);
}
/**
* Пользовательские показатели датасета как derived-Measure (базовый показатель × коэффициент).
*
* @return Measure[]
*/
private function customMeasuresFor(string $datasetKey): array
{
if (! $this->datasets->has($datasetKey)) {
return [];
}
$baseByKey = [];
foreach ($this->datasets->get($datasetKey)->measures() as $measure) {
$baseByKey[$measure->key] = $measure;
}
$result = [];
foreach (CustomMeasure::where('dataset', $datasetKey)->get() as $custom) {
$base = $baseByKey[$custom->base_measure] ?? null;
if ($base === null) {
continue;
}
$factor = (float) ($custom->factor ?? 1);
$result[] = new Measure('custom_'.$custom->id, $custom->name, $custom->unit, "({$base->select}) * {$factor}");
}
return $result;
}
public function storeMeasure(Request $request)
{
$user = Auth::user();
abort_unless($this->canManage($user), 403);
$datasetKeys = array_keys($this->datasets->all());
$data = $request->validate([
'name' => 'required|string|max:50',
'dataset' => 'required|in:'.implode(',', $datasetKeys),
'base_measure' => 'required|string',
'factor' => 'nullable|numeric',
'unit' => 'nullable|in:count,percent,money',
]);
$baseKeys = array_map(fn ($m) => $m->key, $this->datasets->get($data['dataset'])->measures());
abort_unless(in_array($data['base_measure'], $baseKeys, true), 422);
CustomMeasure::create([
...$data,
'factor' => $data['factor'] ?? 1,
'created_by' => Auth::id(),
]);
return back()->with('success', 'Показатель создан');
}
/** @return array{result: \App\Services\Analytics\AnalyticsResult, title: string} */
private function buildResult(Request $request): array
{
$user = Auth::user();
$query = $this->buildQuery($request, $user);
$title = (string) ($request->input('name') ?: $this->datasets->get($query->datasetKey)->label());
return ['result' => $this->datasets->get($query->datasetKey)->run($query), 'title' => $title];
}
private function validateDocument(Request $request): array
{
$datasetKeys = array_keys($this->datasets->all());
$validated = $request->validate([
'name' => 'required|string|max:50',
'description' => 'nullable|string|max:200',
'dataset' => 'required|string|in:'.implode(',', $datasetKeys),
'config' => 'required|array',
'period' => 'nullable|array',
]);
// Чистим config от ключей, которых нет в датасете.
$dataset = $this->datasets->get($validated['dataset']);
$dimKeys = array_map(fn ($d) => $d->key, $dataset->dimensions());
$measureKeys = [
...array_map(fn ($m) => $m->key, $dataset->measures()),
...array_map(fn (Measure $m) => $m->key, $this->customMeasuresFor($validated['dataset'])),
];
$config = $validated['config'];
$config['dimensions'] = array_values(array_intersect($config['dimensions'] ?? [], $dimKeys));
$config['measures'] = array_values(array_intersect($config['measures'] ?? [], $measureKeys));
$config['mode'] = ($config['mode'] ?? 'period') === 'dynamics' ? 'dynamics' : 'period';
$config['detalization'] = in_array($config['detalization'] ?? 'month', ['day', 'week', 'month'], true) ? $config['detalization'] : 'month';
$validated['config'] = $config;
return $validated;
}
private function resolveDepartment(Request $request, User $user): Department
{
$departmentId = $request->input('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();
}
private function canManage(User $user): bool
{
return $user->isAdmin() || $user->isChiefDoctor() || $user->isDeputyChief();
}
private function fileName(string $title, string $extension): string
{
$slug = str_replace([' ', ':', '/', '\\'], '_', $title);
return sprintf('%s_%s.%s', $slug, now('Asia/Yakutsk')->format('Ymd_His'), $extension);
}
}