Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.92% covered (success)
92.92%
105 / 113
85.71% covered (warning)
85.71%
12 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReportHelper
92.92% covered (success)
92.92%
105 / 113
85.71% covered (warning)
85.71%
12 / 14
67.55
0.00% covered (danger)
0.00%
0 / 1
 withMaxAndAverage
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
11
 withTotalCustomers
90.48% covered (success)
90.48%
19 / 21
0.00% covered (danger)
0.00%
0 / 1
7.04
 withGlobalMaxAndAverage
77.78% covered (warning)
77.78%
21 / 27
0.00% covered (danger)
0.00%
0 / 1
17.47
 formatTimeValue
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 extractSelectedScopes
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getWorkstationScopeId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 resolveScopeIdParam
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 extractDateRange
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 isValidDateFormat
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getYearsForDateRange
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getYearDateBounds
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 hasValues
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 hasText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 hasNamedPeriod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace BO\Zmsstatistic\Helper;
4
5use DateTime;
6use DateTimeImmutable;
7
8class ReportHelper
9{
10    public static function withMaxAndAverage(mixed $entity, string $targetKey): mixed
11    {
12        foreach ($entity->data as $date => $dateItems) {
13            $maxima = 0;
14            $total = 0;
15            $count = 0;
16            foreach ($dateItems as $hourItems) {
17                if (is_array($hourItems)) { // Check if $hourItems is an array
18                    foreach ($hourItems as $key => $value) {
19                        if (is_numeric($value) && $targetKey == $key && 0 < $value) {
20                            $total += $value;
21                            $count += 1;
22                            $maxima = ($maxima > $value) ? $maxima : $value;
23                        }
24                    }
25                }
26            }
27            $entity->data[$date]['max_' . $targetKey] = $maxima;
28            $entity->data[$date]['average_' . $targetKey] = (! $total || ! $count) ? 0 : $total / $count;
29        }
30        return $entity;
31    }
32
33    public static function withTotalCustomers(mixed $entity): mixed
34    {
35        foreach ($entity->data as $dateKey => $dateItems) {
36            if (!is_array($dateItems)) {
37                continue;
38            }
39
40            foreach ($dateItems as $hour => $hourItems) {
41                if (!is_array($hourItems)) {
42                    continue;
43                }
44
45                $countSpontan = (int) ($hourItems['waitingcount'] ?? 0);
46                $countTermin  = (int) ($hourItems['waitingcount_termin'] ?? 0);
47                $countTotal   = $countSpontan + $countTermin;
48
49                $waitSpontan = (float) ($hourItems['waitingtime'] ?? 0);
50                $waitTermin  = (float) ($hourItems['waitingtime_termin'] ?? 0);
51
52                $waySpontan  = (float) ($hourItems['waytime'] ?? 0);
53                $wayTermin   = (float) ($hourItems['waytime_termin'] ?? 0);
54
55                $entity->data[$dateKey][$hour]['waitingcount_total'] = $countTotal;
56
57                $entity->data[$dateKey][$hour]['waitingtime_total'] = ($countTotal > 0)
58                    ? (($waitSpontan * $countSpontan) + ($waitTermin * $countTermin)) / $countTotal
59                    : 0;
60
61                $entity->data[$dateKey][$hour]['waytime_total'] = ($countTotal > 0)
62                    ? (($waySpontan * $countSpontan) + ($wayTermin * $countTermin)) / $countTotal
63                    : 0;
64            }
65        }
66
67        return $entity;
68    }
69
70    public static function withGlobalMaxAndAverage(mixed $entity, string $targetKey): mixed
71    {
72        $maxima = 0;
73        $total  = 0;
74        $count  = 0;
75
76        foreach ($entity->data as $dateItems) {
77            if (!is_array($dateItems)) {
78                continue;
79            }
80            foreach ($dateItems as $hourItems) {
81                if (!is_array($hourItems)) {
82                    continue;
83                }
84                $value = $hourItems[$targetKey] ?? null;
85                if (is_numeric($value) && $value > 0) {
86                    $value  = (float) $value;
87                    $maxima = ($maxima > $value) ? $maxima : $value;
88                    $total += $value;
89                    $count++;
90                }
91            }
92        }
93
94        $average = ($count > 0) ? ($total / $count) : 0;
95
96        if (is_object($entity->data)) {
97            if (!isset($entity->data->max) || !is_array($entity->data->max)) {
98                $entity->data->max = [];
99            }
100            $entity->data->max['max_' . $targetKey] = $maxima;
101            $entity->data->max['average_' . $targetKey] = $average;
102        } elseif (is_array($entity->data)) {
103            if (!isset($entity->data['max']) || !is_array($entity->data['max'])) {
104                $entity->data['max'] = [];
105            }
106            $entity->data['max']['max_' . $targetKey] = $maxima;
107            $entity->data['max']['average_' . $targetKey] = $average;
108        }
109
110        return $entity;
111    }
112
113    /**
114     * Format minutes as mm:ss, rounding the full duration to the nearest second.
115     * Same rule as the Twig formatMinutesToTime macro, including its string cast
116     * via |replace, which changes some .5-second floats (e.g. 3:27 vs 3:28).
117     */
118    public static function formatTimeValue(mixed $value): mixed
119    {
120        if (!is_numeric($value)) {
121            return $value;
122        }
123        $totalMinutes = (float) str_replace(',', '.', (string) $value);
124        $totalSeconds = (int) round($totalMinutes * 60);
125        if ($totalSeconds <= 0) {
126            return '00:00';
127        }
128
129        return sprintf('%02d:%02d', intdiv($totalSeconds, 60), $totalSeconds % 60);
130    }
131
132    /**
133     * Extract selected scope IDs from request parameters
134     */
135    public function extractSelectedScopes(array $scopes): array
136    {
137        if (!empty($scopes)) {
138            $validScopes = array_filter($scopes, function ($scopeId) {
139                return is_numeric($scopeId) && $scopeId > 0;
140            });
141
142            if (!empty($validScopes)) {
143                return array_map('intval', $validScopes);
144            }
145        }
146
147        return [];
148    }
149
150    /**
151     * Workstation scope id when the user has selected a default location, otherwise null.
152     */
153    public function getWorkstationScopeId(mixed $workstation): ?int
154    {
155        $scopeId = (int) ($workstation->scope['id'] ?? 0);
156
157        return $scopeId > 0 ? $scopeId : null;
158    }
159
160    /**
161     * Scope id(s) for warehouse report queries from form selection or workstation default.
162     */
163    public function resolveScopeIdParam(array $selectedScopes, ?int $workstationScopeId): string
164    {
165        if (!empty($selectedScopes)) {
166            return implode(',', $selectedScopes);
167        }
168
169        return $workstationScopeId !== null ? (string) $workstationScopeId : '';
170    }
171
172    /**
173     * Extract and validate date range from request parameters
174     */
175    public function extractDateRange(?string $fromDate, ?string $toDate): ?array
176    {
177        if (
178            self::hasText($fromDate) && self::hasText($toDate)
179            && $this->isValidDateFormat($fromDate) && $this->isValidDateFormat($toDate)
180        ) {
181            return [
182                'from' => $fromDate,
183                'to' => $toDate
184            ];
185        }
186
187        return null;
188    }
189
190    /**
191     * Validate if the given string is a valid date format (YYYY-MM-DD)
192     */
193    public function isValidDateFormat(string $date): bool
194    {
195        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
196            return false;
197        }
198
199        $dateTime = DateTime::createFromFormat('Y-m-d', $date);
200        return $dateTime && $dateTime->format('Y-m-d') === $date;
201    }
202
203    /**
204     * Get all years that need to be fetched for a date range
205     */
206    public function getYearsForDateRange(string $fromDate, string $toDate): array
207    {
208        $fromYear = (int) substr($fromDate, 0, 4);
209        $toYear = (int) substr($toDate, 0, 4);
210
211        $years = [];
212        for ($year = $fromYear; $year <= $toYear; $year++) {
213            $years[] = $year;
214        }
215
216        return $years;
217    }
218    public function getYearDateBounds(int|string $year, string $fromDate, string $toDate): ?array
219    {
220        $requestedFrom = new DateTimeImmutable($fromDate);
221        $requestedTo = new DateTimeImmutable($toDate);
222        $yearStart = new DateTimeImmutable($year . '-01-01');
223        $yearEnd = new DateTimeImmutable($year . '-12-31');
224
225        $yearFrom = $requestedFrom > $yearStart ? $requestedFrom : $yearStart;
226        $yearTo = $requestedTo < $yearEnd ? $requestedTo : $yearEnd;
227
228        if ($yearFrom > $yearTo) {
229            return null;
230        }
231
232        return [
233            'from' => $yearFrom->format('Y-m-d'),
234            'to' => $yearTo->format('Y-m-d'),
235        ];
236    }
237
238    /**
239     * @psalm-assert-if-true non-empty-array $value
240     */
241    public static function hasValues(?array $value): bool
242    {
243        return $value !== null && $value !== [];
244    }
245
246    /**
247     * @psalm-assert-if-true non-empty-string $value
248     */
249    public static function hasText(?string $value): bool
250    {
251        return $value !== null && $value !== '';
252    }
253
254    /**
255     * @psalm-assert-if-true non-empty-string $period
256     */
257    public static function hasNamedPeriod(?string $period): bool
258    {
259        return $period !== null && $period !== '' && $period !== '_';
260    }
261}