indexByKey($this->dimensions()); $measures = $this->indexByKey([...$this->measures(), ...$query->customMeasures]); $filters = $this->indexByKey($this->filters()); $selDims = $this->pick($query->dimensions, $dimensions); $selMeasures = $this->pick($query->measures, $measures); $builder = $this->baseQuery($query); $this->applyJoins($builder, $selDims); $this->applyFilters($builder, $query->filters, $filters); if ($query->mode === 'dynamics') { return $this->buildDynamics($builder, $query, $selDims, $selMeasures); } return $this->buildPeriod($builder, $selDims, $selMeasures); } /** * Режим «За период»: группировка по измерениям, агрегаты-показатели. * * @param Dimension[] $selDims * @param Measure[] $selMeasures */ private function buildPeriod(Builder $builder, array $selDims, array $selMeasures): AnalyticsResult { foreach ($selDims as $dim) { $builder->selectRaw("{$dim->select} as {$dim->key}")->groupByRaw($dim->groupByExpr()); } foreach ($selMeasures as $measure) { $builder->selectRaw("{$measure->select} as {$measure->key}"); } if ($selMeasures !== []) { $builder->orderByRaw($selMeasures[0]->select.' desc'); } $raw = $builder->get(); $columns = []; foreach ($selDims as $dim) { $columns[] = new Column($dim->key, $dim->label, 'dimension', $dim->type); } foreach ($selMeasures as $measure) { $columns[] = new Column($measure->key, $measure->label, 'measure', 'number', $measure->unit); } $labelMaps = $this->resolveLabelMaps($selDims, $raw); $rows = []; $totals = []; foreach ($raw as $record) { $row = []; foreach ($selDims as $dim) { $row[$dim->key] = $dim->display($record->{$dim->key} ?? null, $labelMaps[$dim->key] ?? null); } foreach ($selMeasures as $measure) { $value = $this->number($record->{$measure->key} ?? 0); $row[$measure->key] = $value; $totals[$measure->key] = ($totals[$measure->key] ?? 0) + $value; } $rows[] = $row; } foreach ($totals as $key => $value) { $totals[$key] = $this->number($value); } return new AnalyticsResult($columns, $rows, $totals); } /** * Режим «В динамике»: строки — комбинации измерений, колонки — временные интервалы, * значение — выбранный показатель (chart.metric или первый из выбранных). * * @param Dimension[] $selDims * @param Measure[] $selMeasures */ private function buildDynamics(Builder $builder, AnalyticsQuery $query, array $selDims, array $selMeasures): AnalyticsResult { $measure = $this->resolveDynamicsMeasure($query, $selMeasures); if ($measure === null) { return new AnalyticsResult([], []); } $bucket = $this->bucketExpr($query->detalization); foreach ($selDims as $dim) { $builder->selectRaw("{$dim->select} as {$dim->key}")->groupByRaw($dim->groupByExpr()); } $builder->selectRaw("{$bucket} as __period")->groupByRaw($bucket); $builder->selectRaw("{$measure->select} as __value"); $builder->orderByRaw($bucket); $raw = $builder->get(); // Уникальные интервалы по возрастанию — это будущие колонки. $buckets = $raw->pluck('__period')->filter()->unique()->sort()->values()->all(); $labelMaps = $this->resolveLabelMaps($selDims, $raw); $columns = []; if ($selDims === []) { $columns[] = new Column('__metric', $measure->label, 'dimension', 'string'); } else { foreach ($selDims as $dim) { $columns[] = new Column($dim->key, $dim->label, 'dimension', $dim->type); } } foreach ($buckets as $bucketValue) { $columns[] = new Column('p_'.$bucketValue, $this->bucketLabel($bucketValue, $query->detalization), 'period', 'number', $measure->unit); } // Группируем сырые строки в строки таблицы по комбинации измерений. $grouped = []; foreach ($raw as $record) { $dimKey = $selDims === [] ? '__all' : implode('|', array_map(fn (Dimension $d) => (string) ($record->{$d->key} ?? ''), $selDims)); if (! isset($grouped[$dimKey])) { $row = []; if ($selDims === []) { $row['__metric'] = $measure->label; } else { foreach ($selDims as $dim) { $row[$dim->key] = $dim->display($record->{$dim->key} ?? null, $labelMaps[$dim->key] ?? null); } } foreach ($buckets as $b) { $row['p_'.$b] = 0; } $grouped[$dimKey] = $row; } if ($record->__period !== null) { $grouped[$dimKey]['p_'.$record->__period] = $this->number($record->__value ?? 0); } } return new AnalyticsResult($columns, array_values($grouped)); } /** * @param Measure[] $selMeasures */ private function resolveDynamicsMeasure(AnalyticsQuery $query, array $selMeasures): ?Measure { $wanted = $query->chart['metric'] ?? null; if ($wanted !== null) { foreach ($selMeasures as $measure) { if ($measure->key === $wanted) { return $measure; } } } return $selMeasures[0] ?? null; } private function bucketExpr(string $detalization): string { $field = $this->dateField(); return match ($detalization) { 'day' => "to_char($field, 'YYYY-MM-DD')", 'week' => "to_char($field, 'IYYY-IW')", default => "to_char($field, 'YYYY-MM')", }; } private function bucketLabel(string $value, string $detalization): string { return match ($detalization) { 'day' => Carbon::parse($value)->format('d.m.Y'), 'week' => 'Нед. '.$value, default => $this->monthLabel($value), }; } private function monthLabel(string $value): string { [$year, $month] = array_pad(explode('-', $value), 2, '1'); $months = [1 => 'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь']; return ($months[(int) $month] ?? $value).' '.$year; } /** * Сбор карт «сырое значение => подпись» для измерений-справочников (врач, отделение). * * @param Dimension[] $selDims * @return array> */ private function resolveLabelMaps(array $selDims, \Illuminate\Support\Collection $raw): array { $maps = []; foreach ($selDims as $dim) { if ($dim->labels === null) { continue; } $values = $raw->pluck($dim->key)->filter(fn ($v) => $v !== null && $v !== '')->unique()->values()->all(); $maps[$dim->key] = $values === [] ? [] : ($dim->labels)($values); } return $maps; } /** * @param Dimension[] $selDims */ private function applyJoins(Builder $builder, array $selDims): void { $applied = []; foreach ($selDims as $dim) { if ($dim->applyJoin !== null && ! isset($applied[$dim->key])) { ($dim->applyJoin)($builder); $applied[$dim->key] = true; } } } /** * @param array $filters * @param array $defs */ private function applyFilters(Builder $builder, array $filters, array $defs): void { foreach ($filters as $filter) { $key = $filter['key'] ?? null; $value = $filter['value'] ?? null; if ($key === null || $value === null || $value === '' || ! isset($defs[$key])) { continue; } $def = $defs[$key]; if (is_array($value)) { $builder->whereIn(DB::raw($def->column), $value); } else { $builder->where(DB::raw($def->column), $value); } } } /** * @template T * @param array $keys * @param array $available * @return array */ private function pick(array $keys, array $available): array { $picked = []; foreach ($keys as $key) { if (isset($available[$key])) { $picked[] = $available[$key]; } } return $picked; } /** * @param array $items * @return array */ private function indexByKey(array $items): array { $indexed = []; foreach ($items as $item) { $indexed[$item->key] = $item; } return $indexed; } private function number(mixed $value): float { return round((float) $value, 2); } }