Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
83.91% |
438 / 522 |
|
22.73% |
10 / 44 |
CRAP | |
0.00% |
0 / 1 |
| ReportCapacityService | |
83.91% |
438 / 522 |
|
22.73% |
10 / 44 |
404.83 | |
0.00% |
0 / 1 |
| getExchangeCapacityData | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
| getScopeDateBoundsByScopeId | |
70.83% |
17 / 24 |
|
0.00% |
0 / 1 |
12.48 | |||
| getSelectedScopeSlotTimes | |
77.78% |
21 / 27 |
|
0.00% |
0 / 1 |
11.10 | |||
| formatScopeSlotTimeHint | |
88.89% |
32 / 36 |
|
0.00% |
0 / 1 |
10.14 | |||
| formatGroupedScopeSlotTimeHint | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
6.05 | |||
| resolveScopeDisplayName | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
| resolveScopeSlotTimeMinutes | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
7.33 | |||
| getCapacityPeriod | |
57.14% |
4 / 7 |
|
0.00% |
0 / 1 |
3.71 | |||
| getExchangeCapacityForDateRange | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
4.37 | |||
| getExchangeCapacityForPeriod | |
80.00% |
12 / 15 |
|
0.00% |
0 / 1 |
6.29 | |||
| buildSparseChartExchange | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| buildChartExchange | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| applyChartVisualizationSettings | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
| resolveChartLabelIntervalHours | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
6.02 | |||
| shouldFetchHourlyFromApi | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
5.12 | |||
| resolveRangeDurationHours | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| fetchAggregatedReport | |
73.33% |
11 / 15 |
|
0.00% |
0 / 1 |
5.47 | |||
| buildCapacityFetchParams | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
| resolveCapacityFetchUrlPeriod | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| normalizeFetchedCapacityExchange | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
| aggregateRowsByDate | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
6.09 | |||
| fillMissingTimeline | |
88.64% |
39 / 44 |
|
0.00% |
0 / 1 |
12.21 | |||
| exchangeDataLooksHourly | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
5.58 | |||
| rowDateValue | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| rowNumericValue | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| emptyCapacityRow | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| exchangeSupportsCapacityChannel | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
| normalizeDataRow | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
1 | |||
| normalizeTimelineKey | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
4.25 | |||
| resolveTimelineBounds | |
68.18% |
15 / 22 |
|
0.00% |
0 / 1 |
10.06 | |||
| filterRowsByBounds | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
| finalizeExchange | |
44.44% |
4 / 9 |
|
0.00% |
0 / 1 |
6.74 | |||
| dayFromString | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| enrichPeriodList | |
86.67% |
26 / 30 |
|
0.00% |
0 / 1 |
13.40 | |||
| buildDownloadFilename | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
| buildDownloadDateRangePart | |
57.14% |
4 / 7 |
|
0.00% |
0 / 1 |
13.04 | |||
| buildDownloadExchange | |
93.88% |
46 / 49 |
|
0.00% |
0 / 1 |
8.01 | |||
| exchangeSupportsMinutes | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
| buildCapacityMetricLabel | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| resolveCapacityChannelLabel | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
| resolveChannelMetric | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
8.04 | |||
| formatDownloadDate | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
| formatUtilizationPercent | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| prepareDownloadArgs | |
87.50% |
21 / 24 |
|
0.00% |
0 / 1 |
8.12 | |||
| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * @package Zmsstatistic |
| 5 | * @copyright BerlinOnline Stadtportal GmbH & Co. KG |
| 6 | **/ |
| 7 | |
| 8 | namespace BO\Zmsstatistic\Service; |
| 9 | |
| 10 | use BO\Zmsentities\Day; |
| 11 | use BO\Zmsentities\Exchange; |
| 12 | use DateTimeImmutable; |
| 13 | |
| 14 | /** |
| 15 | * @SuppressWarnings(TooManyMethods) |
| 16 | */ |
| 17 | class ReportCapacityService |
| 18 | { |
| 19 | /** Fetch hour-level warehouse data up to this range length (chart buckets are derived in PHP). */ |
| 20 | private const MAX_HOURLY_FETCH_HOURS = 336; |
| 21 | |
| 22 | /** Above this count, slot-time hints group by duration instead of listing scope names. */ |
| 23 | private const SLOT_TIME_HINT_MAX_NAMED_SCOPES = 4; |
| 24 | |
| 25 | public function getExchangeCapacityData(string $scopeId, ?array $dateRange, array $args): mixed |
| 26 | { |
| 27 | if ($scopeId === '') { |
| 28 | return null; |
| 29 | } |
| 30 | |
| 31 | if ($dateRange) { |
| 32 | return $this->getExchangeCapacityForDateRange($scopeId, $dateRange); |
| 33 | } |
| 34 | |
| 35 | if (isset($args['period'])) { |
| 36 | return $this->getExchangeCapacityForPeriod($scopeId, $args['period']); |
| 37 | } |
| 38 | |
| 39 | return null; |
| 40 | } |
| 41 | |
| 42 | /** Available date range per scope from warehouse subject list (periodstart / periodend). */ |
| 43 | public function getScopeDateBoundsByScopeId(): array |
| 44 | { |
| 45 | try { |
| 46 | $result = \App::$http->readGetResult('/warehouse/capacityscope/'); |
| 47 | if (!$result) { |
| 48 | return []; |
| 49 | } |
| 50 | |
| 51 | $subjectList = $result->getEntity(); |
| 52 | |
| 53 | if (!$subjectList instanceof Exchange || empty($subjectList->data)) { |
| 54 | return []; |
| 55 | } |
| 56 | |
| 57 | $dateBoundsByScopeId = []; |
| 58 | foreach ($subjectList->data as $row) { |
| 59 | $scopeId = (string) ($row[0] ?? ''); |
| 60 | $periodStart = (string) ($row[1] ?? ''); |
| 61 | $periodEnd = (string) ($row[2] ?? ''); |
| 62 | |
| 63 | if ($scopeId === '' || $periodStart === '' || $periodEnd === '') { |
| 64 | continue; |
| 65 | } |
| 66 | |
| 67 | if (!isset($dateBoundsByScopeId[$scopeId])) { |
| 68 | $dateBoundsByScopeId[$scopeId] = [ |
| 69 | 'min' => $periodStart, |
| 70 | 'max' => $periodEnd, |
| 71 | ]; |
| 72 | continue; |
| 73 | } |
| 74 | |
| 75 | $dateBoundsByScopeId[$scopeId]['min'] = min($dateBoundsByScopeId[$scopeId]['min'], $periodStart); |
| 76 | $dateBoundsByScopeId[$scopeId]['max'] = max($dateBoundsByScopeId[$scopeId]['max'], $periodEnd); |
| 77 | } |
| 78 | |
| 79 | return $dateBoundsByScopeId; |
| 80 | } catch (\Throwable $exception) { |
| 81 | return []; |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | public function getSelectedScopeSlotTimes(array $scopeIds): array |
| 86 | { |
| 87 | if ($scopeIds === []) { |
| 88 | return []; |
| 89 | } |
| 90 | |
| 91 | try { |
| 92 | $result = \App::$http->readGetResult('/scope/'); |
| 93 | if (!$result) { |
| 94 | return []; |
| 95 | } |
| 96 | |
| 97 | $scopeList = $result->getData(); |
| 98 | if (!is_array($scopeList) && !($scopeList instanceof \Traversable)) { |
| 99 | return []; |
| 100 | } |
| 101 | |
| 102 | $scopeById = []; |
| 103 | foreach ($scopeList as $scope) { |
| 104 | $id = (string) ($scope->id ?? ''); |
| 105 | if ($id !== '') { |
| 106 | $scopeById[$id] = $scope; |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | $scopeSlotTimeEntries = []; |
| 111 | foreach ($scopeIds as $scopeId) { |
| 112 | $id = (string) $scopeId; |
| 113 | if (!isset($scopeById[$id])) { |
| 114 | continue; |
| 115 | } |
| 116 | |
| 117 | $scope = $scopeById[$id]; |
| 118 | $scopeSlotTimeEntries[] = [ |
| 119 | 'id' => $id, |
| 120 | 'name' => $this->resolveScopeDisplayName($scope, $id), |
| 121 | 'slotTimeInMinutes' => $this->resolveScopeSlotTimeMinutes($scope), |
| 122 | ]; |
| 123 | } |
| 124 | |
| 125 | return $scopeSlotTimeEntries; |
| 126 | } catch (\Throwable $exception) { |
| 127 | return []; |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | public function formatScopeSlotTimeHint(array $scopeSlotTimes): ?string |
| 132 | { |
| 133 | if ($scopeSlotTimes === []) { |
| 134 | return null; |
| 135 | } |
| 136 | |
| 137 | $slotTimeMinutesList = array_values(array_filter( |
| 138 | array_map( |
| 139 | static fn (array $item): ?int => $item['slotTimeInMinutes'] ?? null, |
| 140 | $scopeSlotTimes |
| 141 | ), |
| 142 | static fn (?int $minutes): bool => $minutes !== null |
| 143 | )); |
| 144 | |
| 145 | if ($slotTimeMinutesList === []) { |
| 146 | return null; |
| 147 | } |
| 148 | |
| 149 | if (count($scopeSlotTimes) === 1) { |
| 150 | return sprintf( |
| 151 | 'Zeitschlitzdauer laut Öffnungszeit: %d Min.', |
| 152 | $slotTimeMinutesList[0] |
| 153 | ); |
| 154 | } |
| 155 | |
| 156 | $uniqueSlotTimeMinutes = array_values(array_unique($slotTimeMinutesList)); |
| 157 | if (count($uniqueSlotTimeMinutes) === 1 && count($slotTimeMinutesList) === count($scopeSlotTimes)) { |
| 158 | return sprintf( |
| 159 | 'Zeitschlitzdauer laut Öffnungszeit: %d Min. (alle ausgewählten Standorte)', |
| 160 | $uniqueSlotTimeMinutes[0] |
| 161 | ); |
| 162 | } |
| 163 | |
| 164 | if (count($scopeSlotTimes) > self::SLOT_TIME_HINT_MAX_NAMED_SCOPES) { |
| 165 | return $this->formatGroupedScopeSlotTimeHint($scopeSlotTimes); |
| 166 | } |
| 167 | |
| 168 | $hintParts = []; |
| 169 | foreach ($scopeSlotTimes as $item) { |
| 170 | if (($item['slotTimeInMinutes'] ?? null) === null) { |
| 171 | continue; |
| 172 | } |
| 173 | |
| 174 | $hintParts[] = sprintf( |
| 175 | '%s: %d Min.', |
| 176 | $item['name'], |
| 177 | $item['slotTimeInMinutes'] |
| 178 | ); |
| 179 | } |
| 180 | |
| 181 | return $hintParts === [] |
| 182 | ? null |
| 183 | : 'Zeitschlitzdauer laut Öffnungszeit: ' . implode('; ', $hintParts); |
| 184 | } |
| 185 | |
| 186 | private function formatGroupedScopeSlotTimeHint(array $scopeSlotTimes): ?string |
| 187 | { |
| 188 | $scopeCountBySlotMinutes = []; |
| 189 | foreach ($scopeSlotTimes as $item) { |
| 190 | $minutes = $item['slotTimeInMinutes'] ?? null; |
| 191 | if ($minutes === null) { |
| 192 | continue; |
| 193 | } |
| 194 | |
| 195 | $scopeCountBySlotMinutes[$minutes] = ($scopeCountBySlotMinutes[$minutes] ?? 0) + 1; |
| 196 | } |
| 197 | |
| 198 | if ($scopeCountBySlotMinutes === []) { |
| 199 | return null; |
| 200 | } |
| 201 | |
| 202 | ksort($scopeCountBySlotMinutes, SORT_NUMERIC); |
| 203 | |
| 204 | $hintParts = []; |
| 205 | foreach ($scopeCountBySlotMinutes as $minutes => $scopeCount) { |
| 206 | $hintParts[] = sprintf( |
| 207 | '%d Min. (%d %s)', |
| 208 | $minutes, |
| 209 | $scopeCount, |
| 210 | $scopeCount === 1 ? 'Standort' : 'Standorte' |
| 211 | ); |
| 212 | } |
| 213 | |
| 214 | return 'Zeitschlitzdauer laut Öffnungszeit: ' . implode(', ', $hintParts); |
| 215 | } |
| 216 | |
| 217 | private function resolveScopeDisplayName(mixed $scope, string $id): string |
| 218 | { |
| 219 | if (!is_object($scope)) { |
| 220 | return 'Standort ' . $id; |
| 221 | } |
| 222 | |
| 223 | $contactName = ''; |
| 224 | if (isset($scope->contact)) { |
| 225 | $contactName = (string) ($scope->contact->name ?? ''); |
| 226 | } |
| 227 | |
| 228 | $shortName = (string) ($scope->shortName ?? ''); |
| 229 | $name = trim($contactName . ' ' . $shortName); |
| 230 | |
| 231 | return $name !== '' ? $name : 'Standort ' . $id; |
| 232 | } |
| 233 | |
| 234 | private function resolveScopeSlotTimeMinutes(mixed $scope): ?int |
| 235 | { |
| 236 | if (!is_object($scope) || !isset($scope->provider) || !is_object($scope->provider)) { |
| 237 | return null; |
| 238 | } |
| 239 | |
| 240 | if (!method_exists($scope->provider, 'getSlotTimeInMinutes')) { |
| 241 | return null; |
| 242 | } |
| 243 | |
| 244 | $slotTime = $scope->provider->getSlotTimeInMinutes(); |
| 245 | |
| 246 | return $slotTime !== null ? (int) $slotTime : null; |
| 247 | } |
| 248 | |
| 249 | /** |
| 250 | * Period list for navigation; derived from capacityscope report dates when API only returns "_". |
| 251 | */ |
| 252 | public function getCapacityPeriod(string $scopeId): mixed |
| 253 | { |
| 254 | try { |
| 255 | $result = \App::$http->readGetResult('/warehouse/capacityscope/' . $scopeId . '/'); |
| 256 | if (!$result) { |
| 257 | return null; |
| 258 | } |
| 259 | |
| 260 | $periodList = $result->getEntity(); |
| 261 | |
| 262 | return $this->enrichPeriodList($periodList, $scopeId); |
| 263 | } catch (\Throwable $exception) { |
| 264 | return null; |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | public function getExchangeCapacityForDateRange(string $scopeId, array $dateRange): mixed |
| 269 | { |
| 270 | if (!isset($dateRange['from'], $dateRange['to'])) { |
| 271 | return null; |
| 272 | } |
| 273 | |
| 274 | $exchange = $this->fetchAggregatedReport($scopeId, $dateRange, null); |
| 275 | if (!$exchange || empty($exchange->data)) { |
| 276 | return null; |
| 277 | } |
| 278 | |
| 279 | $exchange->data = $this->filterRowsByBounds($exchange->data, $dateRange); |
| 280 | |
| 281 | return $this->finalizeExchange($exchange, $dateRange['from'], $dateRange['to']); |
| 282 | } |
| 283 | |
| 284 | public function getExchangeCapacityForPeriod(string $scopeId, string $period): mixed |
| 285 | { |
| 286 | $exchange = $this->fetchAggregatedReport($scopeId, null, $period); |
| 287 | if (!$exchange || empty($exchange->data)) { |
| 288 | return null; |
| 289 | } |
| 290 | |
| 291 | $timelineBounds = $this->resolveTimelineBounds(null, $period); |
| 292 | if ($timelineBounds) { |
| 293 | $exchange->data = $this->filterRowsByBounds($exchange->data, $timelineBounds); |
| 294 | } |
| 295 | |
| 296 | if (empty($exchange->data)) { |
| 297 | return null; |
| 298 | } |
| 299 | |
| 300 | if ($timelineBounds) { |
| 301 | return $this->finalizeExchange( |
| 302 | $exchange, |
| 303 | $timelineBounds['from'], |
| 304 | $timelineBounds['to'] |
| 305 | ); |
| 306 | } |
| 307 | |
| 308 | return $this->finalizeExchange($exchange); |
| 309 | } |
| 310 | |
| 311 | /** |
| 312 | * Sparse API rows for the chart (same as legacy warehouse reports). |
| 313 | */ |
| 314 | public function buildSparseChartExchange(Exchange $exchange, ?array $dateRange, ?string $period): Exchange |
| 315 | { |
| 316 | return $this->applyChartVisualizationSettings(clone $exchange, $dateRange, $period); |
| 317 | } |
| 318 | |
| 319 | /** |
| 320 | * Full timeline for the chart (zeros for closed hours/days). Table uses sparse API data. |
| 321 | */ |
| 322 | public function buildChartExchange(Exchange $exchange, ?array $dateRange, ?string $period): Exchange |
| 323 | { |
| 324 | $chartExchange = clone $exchange; |
| 325 | $useHourlyTimeline = $this->shouldFetchHourlyFromApi($dateRange, $period); |
| 326 | |
| 327 | $chartExchange->data = $this->fillMissingTimeline( |
| 328 | $chartExchange->data, |
| 329 | $dateRange, |
| 330 | $period, |
| 331 | $useHourlyTimeline |
| 332 | ); |
| 333 | |
| 334 | return $this->applyChartVisualizationSettings($chartExchange, $dateRange, $period); |
| 335 | } |
| 336 | |
| 337 | private function applyChartVisualizationSettings( |
| 338 | Exchange $chartExchange, |
| 339 | ?array $dateRange, |
| 340 | ?string $period |
| 341 | ): Exchange { |
| 342 | $visualization = $chartExchange['visualization'] ?? []; |
| 343 | if (!is_array($visualization)) { |
| 344 | $visualization = []; |
| 345 | } |
| 346 | $visualization['labelIntervalHours'] = $this->resolveChartLabelIntervalHours($dateRange, $period); |
| 347 | $visualization['allowSparseTimeline'] = true; |
| 348 | if (!isset($visualization['allowCapacityChannel'])) { |
| 349 | $visualization['allowCapacityChannel'] = $this->exchangeSupportsCapacityChannel($chartExchange); |
| 350 | } |
| 351 | $chartExchange['visualization'] = $visualization; |
| 352 | |
| 353 | return $chartExchange; |
| 354 | } |
| 355 | |
| 356 | /** |
| 357 | * X-axis tick label spacing in hours, or null for daily labels (data stays hourly/daily respectively). |
| 358 | */ |
| 359 | public function resolveChartLabelIntervalHours(?array $dateRange, ?string $period): ?int |
| 360 | { |
| 361 | $rangeDurationHours = $this->resolveRangeDurationHours($dateRange, $period); |
| 362 | if ($rangeDurationHours === null) { |
| 363 | return null; |
| 364 | } |
| 365 | |
| 366 | if ($rangeDurationHours <= 24) { |
| 367 | return 1; |
| 368 | } |
| 369 | |
| 370 | if ($rangeDurationHours <= 48) { |
| 371 | return 2; |
| 372 | } |
| 373 | |
| 374 | if ($rangeDurationHours <= 168) { |
| 375 | return 6; |
| 376 | } |
| 377 | |
| 378 | if ($rangeDurationHours <= self::MAX_HOURLY_FETCH_HOURS) { |
| 379 | return 12; |
| 380 | } |
| 381 | |
| 382 | return null; |
| 383 | } |
| 384 | |
| 385 | public function shouldFetchHourlyFromApi(?array $dateRange, ?string $period): bool |
| 386 | { |
| 387 | $rangeDurationHours = $this->resolveRangeDurationHours($dateRange, $period); |
| 388 | if ($rangeDurationHours !== null) { |
| 389 | return $rangeDurationHours <= self::MAX_HOURLY_FETCH_HOURS; |
| 390 | } |
| 391 | |
| 392 | if ($period && $period !== '_' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $period)) { |
| 393 | return true; |
| 394 | } |
| 395 | |
| 396 | return false; |
| 397 | } |
| 398 | |
| 399 | /** Length of selected range in hours (inclusive end day). */ |
| 400 | public function resolveRangeDurationHours(?array $dateRange, ?string $period): ?float |
| 401 | { |
| 402 | $bounds = $this->resolveTimelineBounds($dateRange, $period); |
| 403 | if (!$bounds) { |
| 404 | return null; |
| 405 | } |
| 406 | |
| 407 | $rangeStart = new DateTimeImmutable($bounds['from'] . ' 00:00:00'); |
| 408 | $rangeEnd = new DateTimeImmutable($bounds['to'] . ' 23:59:59'); |
| 409 | |
| 410 | return ($rangeEnd->getTimestamp() - $rangeStart->getTimestamp()) / 3600; |
| 411 | } |
| 412 | |
| 413 | private function fetchAggregatedReport(string $scopeId, ?array $dateRange, ?string $period): ?Exchange |
| 414 | { |
| 415 | try { |
| 416 | $useHourlyGrouping = $this->shouldFetchHourlyFromApi($dateRange, $period); |
| 417 | $warehouseFetchParams = $this->buildCapacityFetchParams($dateRange, $period, $useHourlyGrouping); |
| 418 | $warehouseUrlPeriodSegment = $this->resolveCapacityFetchUrlPeriod($dateRange, $period); |
| 419 | |
| 420 | $result = \App::$http->readGetResult( |
| 421 | '/warehouse/capacityscope/' . $scopeId . '/' . $warehouseUrlPeriodSegment . '/', |
| 422 | $warehouseFetchParams === [] ? null : $warehouseFetchParams |
| 423 | ); |
| 424 | if (!$result) { |
| 425 | return null; |
| 426 | } |
| 427 | |
| 428 | $exchange = $result->getEntity(); |
| 429 | if (!$exchange instanceof Exchange) { |
| 430 | return null; |
| 431 | } |
| 432 | |
| 433 | return $this->normalizeFetchedCapacityExchange($exchange, $useHourlyGrouping); |
| 434 | } catch (\Throwable $exception) { |
| 435 | return null; |
| 436 | } |
| 437 | } |
| 438 | |
| 439 | private function buildCapacityFetchParams(?array $dateRange, ?string $period, bool $useHourlyGrouping): array |
| 440 | { |
| 441 | $warehouseFetchParams = []; |
| 442 | |
| 443 | if ($dateRange) { |
| 444 | $warehouseFetchParams['fromDate'] = $dateRange['from']; |
| 445 | $warehouseFetchParams['toDate'] = $dateRange['to']; |
| 446 | } |
| 447 | |
| 448 | if ($useHourlyGrouping) { |
| 449 | $warehouseFetchParams['groupby'] = 'hour'; |
| 450 | } elseif ($dateRange !== null || ($period !== null && $period !== '_')) { |
| 451 | $warehouseFetchParams['groupby'] = 'day'; |
| 452 | } |
| 453 | |
| 454 | return $warehouseFetchParams; |
| 455 | } |
| 456 | |
| 457 | private function resolveCapacityFetchUrlPeriod(?array $dateRange, ?string $period): string |
| 458 | { |
| 459 | if ($period && $period !== '_') { |
| 460 | return $period; |
| 461 | } |
| 462 | |
| 463 | if ($dateRange) { |
| 464 | return $dateRange['from']; |
| 465 | } |
| 466 | |
| 467 | return '_'; |
| 468 | } |
| 469 | |
| 470 | private function normalizeFetchedCapacityExchange(Exchange $exchange, bool $useHourlyGrouping): Exchange |
| 471 | { |
| 472 | $exchange->data = $this->aggregateRowsByDate($exchange->data, $useHourlyGrouping); |
| 473 | |
| 474 | if (!$useHourlyGrouping) { |
| 475 | if ($this->exchangeDataLooksHourly($exchange->data)) { |
| 476 | $exchange->data = $this->aggregateRowsByDate($exchange->data, false); |
| 477 | } |
| 478 | $exchange->period = 'day'; |
| 479 | } elseif (($exchange->period ?? 'day') === 'hour') { |
| 480 | $exchange->period = 'hour'; |
| 481 | } |
| 482 | |
| 483 | return $exchange; |
| 484 | } |
| 485 | |
| 486 | /** Sum booked/planned counts per date (or hour) across multiple scopes. */ |
| 487 | public function aggregateRowsByDate(array $rows, bool $useHourlyKeys): array |
| 488 | { |
| 489 | $rowsByTimelineKey = []; |
| 490 | |
| 491 | foreach ($rows as $row) { |
| 492 | if (!is_array($row)) { |
| 493 | continue; |
| 494 | } |
| 495 | |
| 496 | $normalizedRow = $this->normalizeDataRow($row, $useHourlyKeys); |
| 497 | $timelineKey = $normalizedRow[1]; |
| 498 | if ($timelineKey === '') { |
| 499 | continue; |
| 500 | } |
| 501 | |
| 502 | if (!isset($rowsByTimelineKey[$timelineKey])) { |
| 503 | $rowsByTimelineKey[$timelineKey] = $normalizedRow; |
| 504 | continue; |
| 505 | } |
| 506 | |
| 507 | for ($metricColumnIndex = 2; $metricColumnIndex <= 9; $metricColumnIndex++) { |
| 508 | $rowsByTimelineKey[$timelineKey][$metricColumnIndex] += $normalizedRow[$metricColumnIndex]; |
| 509 | } |
| 510 | } |
| 511 | |
| 512 | ksort($rowsByTimelineKey); |
| 513 | |
| 514 | return array_values($rowsByTimelineKey); |
| 515 | } |
| 516 | |
| 517 | /** |
| 518 | * Insert zero rows for each hour or day in the selected range so the chart shows |
| 519 | * closed periods and gaps between days (not only timestamps with slot data). |
| 520 | */ |
| 521 | private function fillMissingTimeline( |
| 522 | array $rows, |
| 523 | ?array $dateRange, |
| 524 | ?string $period, |
| 525 | bool $useHourlyTimeline |
| 526 | ): array { |
| 527 | $timelineBounds = $this->resolveTimelineBounds($dateRange, $period); |
| 528 | if (!$timelineBounds) { |
| 529 | return $rows; |
| 530 | } |
| 531 | |
| 532 | $rowsByTimelineKey = []; |
| 533 | $defaultSubjectId = ''; |
| 534 | |
| 535 | foreach ($rows as $row) { |
| 536 | if (!is_array($row)) { |
| 537 | continue; |
| 538 | } |
| 539 | |
| 540 | $normalizedRow = $this->normalizeDataRow($row, $useHourlyTimeline); |
| 541 | $timelineKey = $normalizedRow[1]; |
| 542 | if ($timelineKey === '') { |
| 543 | continue; |
| 544 | } |
| 545 | |
| 546 | if (!isset($rowsByTimelineKey[$timelineKey])) { |
| 547 | $rowsByTimelineKey[$timelineKey] = $normalizedRow; |
| 548 | if ($defaultSubjectId === '' && $normalizedRow[0] !== '') { |
| 549 | $defaultSubjectId = $normalizedRow[0]; |
| 550 | } |
| 551 | continue; |
| 552 | } |
| 553 | |
| 554 | for ($metricColumnIndex = 2; $metricColumnIndex <= 9; $metricColumnIndex++) { |
| 555 | $rowsByTimelineKey[$timelineKey][$metricColumnIndex] += $normalizedRow[$metricColumnIndex]; |
| 556 | } |
| 557 | } |
| 558 | |
| 559 | $rangeStartDate = $timelineBounds['from']; |
| 560 | $rangeEndDate = $timelineBounds['to']; |
| 561 | $completeTimelineRows = []; |
| 562 | |
| 563 | if ($useHourlyTimeline) { |
| 564 | $timelineStart = new DateTimeImmutable($rangeStartDate . ' 00:00:00'); |
| 565 | $timelineEnd = new DateTimeImmutable($rangeEndDate . ' 23:00:00'); |
| 566 | $timelineCursor = $timelineStart; |
| 567 | |
| 568 | while ($timelineCursor <= $timelineEnd) { |
| 569 | $timelineKey = $this->normalizeTimelineKey( |
| 570 | $timelineCursor->format('Y-m-d') . ' ' . $timelineCursor->format('H') . ':00', |
| 571 | true |
| 572 | ); |
| 573 | $completeTimelineRows[] = $rowsByTimelineKey[$timelineKey] |
| 574 | ?? $this->emptyCapacityRow($defaultSubjectId, $timelineKey); |
| 575 | $timelineCursor = $timelineCursor->modify('+1 hour'); |
| 576 | } |
| 577 | |
| 578 | return $completeTimelineRows; |
| 579 | } |
| 580 | |
| 581 | $timelineStart = new DateTimeImmutable($rangeStartDate); |
| 582 | $timelineEnd = new DateTimeImmutable($rangeEndDate); |
| 583 | $timelineCursor = $timelineStart; |
| 584 | |
| 585 | while ($timelineCursor <= $timelineEnd) { |
| 586 | $timelineKey = $timelineCursor->format('Y-m-d'); |
| 587 | $completeTimelineRows[] = $rowsByTimelineKey[$timelineKey] |
| 588 | ?? $this->emptyCapacityRow($defaultSubjectId, $timelineKey); |
| 589 | $timelineCursor = $timelineCursor->modify('+1 day'); |
| 590 | } |
| 591 | |
| 592 | return $completeTimelineRows; |
| 593 | } |
| 594 | |
| 595 | private function exchangeDataLooksHourly(array $rows): bool |
| 596 | { |
| 597 | foreach ($rows as $row) { |
| 598 | if (!is_array($row)) { |
| 599 | continue; |
| 600 | } |
| 601 | |
| 602 | $dateValue = $this->rowDateValue($row); |
| 603 | if ($dateValue !== '' && preg_match('/\d{2}:\d{2}/', $dateValue)) { |
| 604 | return true; |
| 605 | } |
| 606 | } |
| 607 | |
| 608 | return false; |
| 609 | } |
| 610 | |
| 611 | private function rowDateValue(array $row): string |
| 612 | { |
| 613 | if (isset($row['date'])) { |
| 614 | return (string) $row['date']; |
| 615 | } |
| 616 | |
| 617 | return (string) ($row[1] ?? ''); |
| 618 | } |
| 619 | |
| 620 | private function rowNumericValue(array $row, string $variable, int $position): int |
| 621 | { |
| 622 | if (isset($row[$variable])) { |
| 623 | return (int) $row[$variable]; |
| 624 | } |
| 625 | |
| 626 | return (int) ($row[$position] ?? 0); |
| 627 | } |
| 628 | |
| 629 | private function emptyCapacityRow(string $subjectId, string $timelineKey): array |
| 630 | { |
| 631 | return [$subjectId, $timelineKey, 0, 0, 0, 0, 0, 0, 0, 0]; |
| 632 | } |
| 633 | |
| 634 | public function exchangeSupportsCapacityChannel(Exchange $exchange): bool |
| 635 | { |
| 636 | foreach ($exchange->dictionary ?? [] as $entry) { |
| 637 | if (($entry['variable'] ?? null) === 'bookedcount_public') { |
| 638 | return true; |
| 639 | } |
| 640 | } |
| 641 | |
| 642 | return false; |
| 643 | } |
| 644 | |
| 645 | private function normalizeDataRow(array $row, bool $useHourlyKeys): array |
| 646 | { |
| 647 | $rowDateValue = $this->rowDateValue($row); |
| 648 | |
| 649 | return [ |
| 650 | (string) ($row['subjectid'] ?? $row[0] ?? ''), |
| 651 | $this->normalizeTimelineKey($rowDateValue, $useHourlyKeys), |
| 652 | $this->rowNumericValue($row, 'bookedcount', 2), |
| 653 | $this->rowNumericValue($row, 'plannedcount', 3), |
| 654 | $this->rowNumericValue($row, 'bookedminutes', 4), |
| 655 | $this->rowNumericValue($row, 'plannedminutes', 5), |
| 656 | $this->rowNumericValue($row, 'bookedcount_public', 6), |
| 657 | $this->rowNumericValue($row, 'plannedcount_public', 7), |
| 658 | $this->rowNumericValue($row, 'bookedminutes_public', 8), |
| 659 | $this->rowNumericValue($row, 'plannedminutes_public', 9), |
| 660 | ]; |
| 661 | } |
| 662 | |
| 663 | private function normalizeTimelineKey(string $rowDateValue, bool $useHourlyKeys): string |
| 664 | { |
| 665 | if ($rowDateValue === '') { |
| 666 | return ''; |
| 667 | } |
| 668 | |
| 669 | if (!$useHourlyKeys) { |
| 670 | return substr($rowDateValue, 0, 10); |
| 671 | } |
| 672 | |
| 673 | $timestamp = strtotime($rowDateValue); |
| 674 | if ($timestamp === false) { |
| 675 | return $rowDateValue; |
| 676 | } |
| 677 | |
| 678 | return date('Y-m-d H', $timestamp) . ':00'; |
| 679 | } |
| 680 | |
| 681 | private function resolveTimelineBounds(?array $dateRange, ?string $period): ?array |
| 682 | { |
| 683 | if ($dateRange && isset($dateRange['from'], $dateRange['to'])) { |
| 684 | return [ |
| 685 | 'from' => $dateRange['from'], |
| 686 | 'to' => $dateRange['to'], |
| 687 | ]; |
| 688 | } |
| 689 | |
| 690 | if (!$period || $period === '_') { |
| 691 | return null; |
| 692 | } |
| 693 | |
| 694 | if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $period)) { |
| 695 | return ['from' => $period, 'to' => $period]; |
| 696 | } |
| 697 | |
| 698 | if (preg_match('/^\d{4}-\d{2}$/', $period)) { |
| 699 | $start = new DateTimeImmutable($period . '-01'); |
| 700 | $end = $start->modify('last day of this month'); |
| 701 | |
| 702 | return [ |
| 703 | 'from' => $start->format('Y-m-d'), |
| 704 | 'to' => $end->format('Y-m-d'), |
| 705 | ]; |
| 706 | } |
| 707 | |
| 708 | if (preg_match('/^\d{4}$/', $period)) { |
| 709 | return [ |
| 710 | 'from' => $period . '-01-01', |
| 711 | 'to' => $period . '-12-31', |
| 712 | ]; |
| 713 | } |
| 714 | |
| 715 | return null; |
| 716 | } |
| 717 | |
| 718 | private function filterRowsByBounds(array $rows, array $bounds): array |
| 719 | { |
| 720 | $rangeStart = $bounds['from']; |
| 721 | $rangeEnd = $bounds['to']; |
| 722 | |
| 723 | return array_values(array_filter($rows, static function ($row) use ($rangeStart, $rangeEnd) { |
| 724 | $date = (string) ($row[1] ?? ''); |
| 725 | if ($date === '') { |
| 726 | return false; |
| 727 | } |
| 728 | |
| 729 | $day = substr($date, 0, 10); |
| 730 | |
| 731 | return $day >= $rangeStart && $day <= $rangeEnd; |
| 732 | })); |
| 733 | } |
| 734 | |
| 735 | private function finalizeExchange(Exchange $exchange, ?string $fromDate = null, ?string $toDate = null): Exchange |
| 736 | { |
| 737 | if ($fromDate && $toDate) { |
| 738 | $exchange->firstDay = $this->dayFromString($fromDate); |
| 739 | $exchange->lastDay = $this->dayFromString($toDate); |
| 740 | } elseif (!empty($exchange->data)) { |
| 741 | $firstRowDate = (string) $exchange->data[0][1]; |
| 742 | $lastRowDate = (string) $exchange->data[count($exchange->data) - 1][1]; |
| 743 | $exchange->firstDay = $this->dayFromString(substr($firstRowDate, 0, 10)); |
| 744 | $exchange->lastDay = $this->dayFromString(substr($lastRowDate, 0, 10)); |
| 745 | } |
| 746 | |
| 747 | return $exchange; |
| 748 | } |
| 749 | |
| 750 | private function dayFromString(string $date): Day |
| 751 | { |
| 752 | $dateTime = new \DateTime(substr($date, 0, 10)); |
| 753 | |
| 754 | return (new Day())->setDateTime($dateTime); |
| 755 | } |
| 756 | |
| 757 | private function enrichPeriodList(mixed $periodList, string $scopeId): mixed |
| 758 | { |
| 759 | if (!$periodList instanceof Exchange) { |
| 760 | return $periodList; |
| 761 | } |
| 762 | |
| 763 | $hasOnlyAggregatePeriod = count($periodList->data) === 1 |
| 764 | && isset($periodList->data[0][0]) |
| 765 | && $periodList->data[0][0] === '_'; |
| 766 | |
| 767 | if (!$hasOnlyAggregatePeriod) { |
| 768 | return $periodList; |
| 769 | } |
| 770 | |
| 771 | $exchange = $this->fetchAggregatedReport($scopeId, null, null); |
| 772 | if (!$exchange || empty($exchange->data)) { |
| 773 | return $periodList; |
| 774 | } |
| 775 | |
| 776 | $years = []; |
| 777 | $months = []; |
| 778 | |
| 779 | foreach ($exchange->data as $row) { |
| 780 | $rowDateValue = (string) ($row[1] ?? ''); |
| 781 | if ($rowDateValue === '') { |
| 782 | continue; |
| 783 | } |
| 784 | |
| 785 | $year = substr($rowDateValue, 0, 4); |
| 786 | $month = substr($rowDateValue, 0, 7); |
| 787 | |
| 788 | if (!in_array($year, $years, true)) { |
| 789 | $years[] = $year; |
| 790 | } |
| 791 | |
| 792 | if (!in_array($month, $months, true)) { |
| 793 | $months[] = $month; |
| 794 | } |
| 795 | } |
| 796 | |
| 797 | rsort($years); |
| 798 | rsort($months); |
| 799 | |
| 800 | $periodList->data = []; |
| 801 | foreach ($years as $year) { |
| 802 | $periodList->data[] = [$year]; |
| 803 | } |
| 804 | foreach ($months as $month) { |
| 805 | $periodList->data[] = [$month]; |
| 806 | } |
| 807 | |
| 808 | return $periodList; |
| 809 | } |
| 810 | |
| 811 | public function buildDownloadFilename(?array $dateRange, ?string $period, string $valueMode = 'slots'): string |
| 812 | { |
| 813 | $valueSuffix = $valueMode === 'minutes' ? '-minuten' : '-zeitschlitze'; |
| 814 | $rangePart = $this->buildDownloadDateRangePart($dateRange, $period); |
| 815 | $baseName = 'terminkapazitaet' . $valueSuffix; |
| 816 | |
| 817 | if ($rangePart === '') { |
| 818 | return $baseName; |
| 819 | } |
| 820 | |
| 821 | return $baseName . '_' . $rangePart; |
| 822 | } |
| 823 | |
| 824 | private function buildDownloadDateRangePart(?array $dateRange, ?string $period): string |
| 825 | { |
| 826 | if ($dateRange !== null && !empty($dateRange['from']) && !empty($dateRange['to'])) { |
| 827 | return $dateRange['from'] . '-bis-' . $dateRange['to']; |
| 828 | } |
| 829 | |
| 830 | if ($period === null || $period === '' || $period === '_') { |
| 831 | return ''; |
| 832 | } |
| 833 | |
| 834 | if (preg_match('/^(\d{4}-\d{2}-\d{2})_(\d{4}-\d{2}-\d{2})$/', $period, $matches)) { |
| 835 | return $matches[1] . '-bis-' . $matches[2]; |
| 836 | } |
| 837 | |
| 838 | return preg_replace('/[^0-9-]/', '', $period); |
| 839 | } |
| 840 | |
| 841 | public function buildDownloadExchange( |
| 842 | Exchange $exchange, |
| 843 | string $channelMode = 'total', |
| 844 | string $valueMode = 'slots' |
| 845 | ): Exchange { |
| 846 | $channelMode = in_array($channelMode, ['total', 'public', 'intern_only'], true) |
| 847 | ? $channelMode |
| 848 | : 'total'; |
| 849 | $useMinutes = $valueMode === 'minutes' && $this->exchangeSupportsMinutes($exchange); |
| 850 | $isHourly = ($exchange->period ?? '') === 'hour'; |
| 851 | |
| 852 | $download = new Exchange(); |
| 853 | $download->addDictionaryEntry( |
| 854 | 'date', |
| 855 | 'string', |
| 856 | $isHourly ? 'Zeitpunkt' : 'Datum' |
| 857 | ); |
| 858 | $download->addDictionaryEntry( |
| 859 | 'planned', |
| 860 | 'number', |
| 861 | $this->buildCapacityMetricLabel('planned', $channelMode, $useMinutes) |
| 862 | ); |
| 863 | $download->addDictionaryEntry( |
| 864 | 'booked', |
| 865 | 'number', |
| 866 | $this->buildCapacityMetricLabel('booked', $channelMode, $useMinutes) |
| 867 | ); |
| 868 | $download->addDictionaryEntry('utilization', 'string', 'Auslastung'); |
| 869 | |
| 870 | $totalPlanned = 0; |
| 871 | $totalBooked = 0; |
| 872 | |
| 873 | foreach ($exchange->data as $row) { |
| 874 | if (!is_array($row)) { |
| 875 | continue; |
| 876 | } |
| 877 | |
| 878 | $normalizedRow = $this->normalizeDataRow($row, $isHourly); |
| 879 | $planned = $this->resolveChannelMetric($normalizedRow, 'planned', $channelMode, $useMinutes); |
| 880 | $booked = $this->resolveChannelMetric($normalizedRow, 'booked', $channelMode, $useMinutes); |
| 881 | $totalPlanned += $planned; |
| 882 | $totalBooked += $booked; |
| 883 | $utilization = $planned > 0 ? round(($booked / $planned) * 1000) / 10 : 0; |
| 884 | |
| 885 | $download->addDataSet([ |
| 886 | $this->formatDownloadDate((string) $normalizedRow[1], $isHourly), |
| 887 | (string) $planned, |
| 888 | (string) $booked, |
| 889 | $this->formatUtilizationPercent($utilization), |
| 890 | ]); |
| 891 | } |
| 892 | |
| 893 | $totalUtilization = $totalPlanned > 0 |
| 894 | ? round(($totalBooked / $totalPlanned) * 1000) / 10 |
| 895 | : 0; |
| 896 | $download->addDataSet([ |
| 897 | 'Summe', |
| 898 | (string) $totalPlanned, |
| 899 | (string) $totalBooked, |
| 900 | $this->formatUtilizationPercent($totalUtilization), |
| 901 | ]); |
| 902 | |
| 903 | return $download; |
| 904 | } |
| 905 | |
| 906 | private function exchangeSupportsMinutes(Exchange $exchange): bool |
| 907 | { |
| 908 | foreach ($exchange->dictionary ?? [] as $entry) { |
| 909 | if (($entry['variable'] ?? null) === 'bookedminutes') { |
| 910 | return true; |
| 911 | } |
| 912 | } |
| 913 | |
| 914 | return false; |
| 915 | } |
| 916 | |
| 917 | private function buildCapacityMetricLabel(string $kind, string $channelMode, bool $useMinutes): string |
| 918 | { |
| 919 | $unit = $useMinutes ? 'Minuten' : 'Zeitschlitze'; |
| 920 | $prefix = $kind === 'planned' ? 'Geplante' : 'Gebuchte'; |
| 921 | |
| 922 | return $prefix . ' Kapazität ' . $this->resolveCapacityChannelLabel($channelMode) . ' (' . $unit . ')'; |
| 923 | } |
| 924 | |
| 925 | private function resolveCapacityChannelLabel(string $channelMode): string |
| 926 | { |
| 927 | if ($channelMode === 'public') { |
| 928 | return 'Internet'; |
| 929 | } |
| 930 | if ($channelMode === 'intern_only') { |
| 931 | return 'nur intern'; |
| 932 | } |
| 933 | |
| 934 | return 'insgesamt'; |
| 935 | } |
| 936 | |
| 937 | private function resolveChannelMetric( |
| 938 | array $normalizedRow, |
| 939 | string $metric, |
| 940 | string $channelMode, |
| 941 | bool $useMinutes |
| 942 | ): int { |
| 943 | if ($useMinutes) { |
| 944 | $totalPosition = $metric === 'planned' ? 5 : 4; |
| 945 | $publicPosition = $metric === 'planned' ? 9 : 8; |
| 946 | } else { |
| 947 | $totalPosition = $metric === 'planned' ? 3 : 2; |
| 948 | $publicPosition = $metric === 'planned' ? 7 : 6; |
| 949 | } |
| 950 | |
| 951 | $total = (int) ($normalizedRow[$totalPosition] ?? 0); |
| 952 | $public = (int) ($normalizedRow[$publicPosition] ?? 0); |
| 953 | |
| 954 | if ($channelMode === 'public') { |
| 955 | return $public; |
| 956 | } |
| 957 | if ($channelMode === 'intern_only') { |
| 958 | return max(0, $total - $public); |
| 959 | } |
| 960 | |
| 961 | return $total; |
| 962 | } |
| 963 | |
| 964 | private function formatDownloadDate(string $dateValue, bool $isHourly): string |
| 965 | { |
| 966 | if ($isHourly || !preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateValue, $matches)) { |
| 967 | return $dateValue; |
| 968 | } |
| 969 | |
| 970 | return $matches[3] . '.' . $matches[2] . '.' . $matches[1]; |
| 971 | } |
| 972 | |
| 973 | private function formatUtilizationPercent(float $utilization): string |
| 974 | { |
| 975 | $formatted = number_format($utilization, 1, ',', '.'); |
| 976 | if (str_ends_with($formatted, ',0')) { |
| 977 | $formatted = substr($formatted, 0, -2); |
| 978 | } |
| 979 | |
| 980 | return $formatted . ' %'; |
| 981 | } |
| 982 | |
| 983 | /** |
| 984 | * Prepare download arguments for capacity report Excel export. |
| 985 | */ |
| 986 | public function prepareDownloadArgs( |
| 987 | array $args, |
| 988 | string $scopeId, |
| 989 | mixed $exchangeCapacity, |
| 990 | ?array $dateRange, |
| 991 | array $selectedScopes = [], |
| 992 | string $valueMode = 'slots', |
| 993 | string $channelMode = 'total' |
| 994 | ): array { |
| 995 | $args['category'] = 'capacityscope'; |
| 996 | $args['subject'] = 'capacityscope'; |
| 997 | $args['subjectid'] = $scopeId; |
| 998 | |
| 999 | if ($dateRange) { |
| 1000 | $args['period'] = $dateRange['from'] . '_' . $dateRange['to']; |
| 1001 | } elseif (!isset($args['period']) || $args['period'] === null || $args['period'] === '') { |
| 1002 | $args['period'] = '_'; |
| 1003 | } |
| 1004 | |
| 1005 | $args['downloadTitle'] = $this->buildDownloadFilename( |
| 1006 | $dateRange, |
| 1007 | $args['period'] ?? null, |
| 1008 | $valueMode |
| 1009 | ); |
| 1010 | |
| 1011 | if (!empty($selectedScopes)) { |
| 1012 | $args['selectedScopes'] = $selectedScopes; |
| 1013 | } elseif ($scopeId !== '') { |
| 1014 | $args['selectedScopes'] = array_values(array_filter(explode(',', $scopeId))); |
| 1015 | } |
| 1016 | |
| 1017 | if ($exchangeCapacity instanceof Exchange) { |
| 1018 | $args['reports'] = [$exchangeCapacity]; |
| 1019 | $args['report'] = $this->buildDownloadExchange( |
| 1020 | $exchangeCapacity, |
| 1021 | $channelMode, |
| 1022 | $valueMode |
| 1023 | ); |
| 1024 | } |
| 1025 | |
| 1026 | return $args; |
| 1027 | } |
| 1028 | } |