Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
CalculateDailyWaitingStatisticByCron
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 13
1560
0.00% covered (danger)
0.00%
0 / 1
 run
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 fetchBuergerData
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 processBuergerRows
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 determineValidScopeId
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 determineHourAndType
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 initializeStatsIfNeeded
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 saveStatistics
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 insertStatisticsRow
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 updateStatisticsValues
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 addHourUpdateColumns
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 extractScopeFromAnmerkung
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
 timeToMinutes
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 hourSuffixForStatistic
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BO\Zmsdb\Helper;
4
5use BO\Zmsdb\Base;
6use DateTimeImmutable;
7
8/**
9 * Berechnung der Wartezeiten pro Standort und Stunde,
10 *
11 * Liest alle 'buerger'-Einträge für ein Datum, filtert stornierte/ohne Wartezeit,
12 * korrigiert StandortIDs aus Anmerkungen und speichert die Durchschnittswerte
13 * in 'wartenrstatistik'.
14 */
15class CalculateDailyWaitingStatisticByCron extends Base
16{
17    public function run(DateTimeImmutable $day, bool $commit = false)
18    {
19        echo "CalculateDailyWaitingStatisticByCron->run for date={$day->format('Y-m-d')}\n";
20
21        $buergerRows = $this->fetchBuergerData($day);
22        $statsByScopeDate = $this->processBuergerRows($buergerRows);
23        $this->saveStatistics($statsByScopeDate, $commit);
24
25        echo "Done collecting stats for {$day->format('Y-m-d')}\n";
26    }
27
28    // Alle 'buerger'-Einträge für das Datum laden (außer stornierte bzw. gelöschte).
29    private function fetchBuergerData(DateTimeImmutable $day): array
30    {
31        $sql = "
32            SELECT
33              BuergerID,
34              StandortID,
35              Datum,
36              Uhrzeit,
37              wsm_aufnahmezeit,
38              waiting_time,
39              way_time,
40              Name,
41              Anmerkung,
42              custom_text_field,
43              custom_text_field2
44            FROM buerger
45            WHERE Datum = :theDay
46              AND Name NOT IN ('(abgesagt)')  
47        ";
48        return $this->getReader()->fetchAll($sql, [
49            'theDay' => $day->format('Y-m-d'),
50        ]);
51    }
52
53
54    private function processBuergerRows(array $buergerRows): array
55    {
56        $statsByScopeDate = [];
57
58        foreach ($buergerRows as $br) {
59            // Wenn waiting_time NULL oder leer ist => storniert oder hatte keine echte Wartezeit => überspringen
60            if (empty($br['waiting_time'])) {
61                continue;
62            }
63
64            $scopeId = $this->determineValidScopeId($br);
65            if ($scopeId <= 0) {
66                continue;
67            }
68
69            [$hour, $type] = $this->determineHourAndType($br);
70
71            $waitMins = $this->timeToMinutes($br['waiting_time']);
72            $wayMins = is_numeric($br['way_time'])
73                ? (float) $br['way_time']
74                : $this->timeToMinutes($br['way_time']);
75
76            $dateStr = $br['Datum'];
77            $this->initializeStatsIfNeeded($statsByScopeDate, $scopeId, $dateStr);
78
79            // Eintrag zur Stats hinzufügen
80            $statsByScopeDate[$scopeId][$dateStr][$hour][$type]['count'] += 1;
81            $statsByScopeDate[$scopeId][$dateStr][$hour][$type]['sumWait'] += $waitMins;
82            $statsByScopeDate[$scopeId][$dateStr][$hour][$type]['sumWay'] += $wayMins;
83        }
84
85        return $statsByScopeDate;
86    }
87
88    // StandortID korrigieren, falls 0. => Wir parsen bei Bedarf aus Anmerkung / custom_text_field.
89    private function determineValidScopeId(array $buergerRecord): int
90    {
91        $scopeId = (int)$buergerRecord['StandortID'];
92        if ($scopeId <= 0) {
93            $parsedScope = $this->extractScopeFromAnmerkung(
94                $buergerRecord['Anmerkung'],
95                $buergerRecord['custom_text_field'],
96                $buergerRecord['custom_text_field2'],
97            );
98
99            if ($parsedScope) {
100                $scopeId = (int)$parsedScope;
101            }
102        }
103
104        return $scopeId;
105    }
106
107    // Unterscheidung zwischen "spontan" und "termin"
108    // - Wenn 'Uhrzeit'=='00:00:00', behandeln wir es als spontan angekommen => Stunde aus wsm_aufnahmezeit
109    private function determineHourAndType(array $buergerRecord): array
110    {
111        $type = 'termin';
112        $hourStr = $buergerRecord['Uhrzeit'];
113
114        if ($buergerRecord['Uhrzeit'] === '00:00:00') {
115            $type = 'spontan';
116            $hourStr = $buergerRecord['wsm_aufnahmezeit'];
117        }
118
119        $parts = explode(':', $hourStr);
120        $hour = (int)$parts[0];
121
122        return [$hour, $type];
123    }
124
125    // Falls für diesen StandortID das Datum noch nicht existiert
126    private function initializeStatsIfNeeded(array &$statsByScopeDate, int $scopeId, string $dateStr): void
127    {
128        if (!isset($statsByScopeDate[$scopeId])) {
129            $statsByScopeDate[$scopeId] = [];
130        }
131
132        if (!isset($statsByScopeDate[$scopeId][$dateStr])) {
133            $statsByScopeDate[$scopeId][$dateStr] = [];
134            foreach (range(0, 23) as $h) {
135                $statsByScopeDate[$scopeId][$dateStr][$h] = [
136                    'spontan' => ['count' => 0, 'sumWait' => 0.0, 'sumWay' => 0.0],
137                    'termin'  => ['count' => 0, 'sumWait' => 0.0, 'sumWay' => 0.0],
138                ];
139            }
140        }
141    }
142
143    private function saveStatistics(array $statsByScopeDate, bool $commit): void
144    {
145        foreach ($statsByScopeDate as $scopeId => $dateArray) {
146            foreach ($dateArray as $dateStr => $hoursData) {
147                $this->insertStatisticsRow($scopeId, $dateStr, $commit);
148                $this->updateStatisticsValues($scopeId, $dateStr, $hoursData, $commit);
149            }
150        }
151    }
152
153    //Eine Zeile in wartenrstatistik für jeden (Standort, Datum) einfügen
154    private function insertStatisticsRow(int $scopeId, string $dateStr, bool $commit): void
155    {
156        if ($commit) {
157            $insertSql = "
158                INSERT IGNORE INTO wartenrstatistik (standortid, datum)
159                VALUES (:sid, :d)
160            ";
161            $this->perform($insertSql, [
162                'sid' => $scopeId,
163                'd'   => $dateStr
164            ]);
165        }
166    }
167
168    private function updateStatisticsValues(int $scopeId, string $dateStr, array $hoursData, bool $commit): void
169    {
170        // Eine einzelne UPDATE-Anweisung für alle 24 Stundenspalten erstellen
171        $updateParams = [
172            'sid' => $scopeId,
173            'd'   => $dateStr
174        ];
175        $updateCols = [];
176
177        // Für jede Stunde 0..23 Spalten für "spontan" und "termin" füllen
178        foreach (range(0, 23) as $hour) {
179            $this->addHourUpdateColumns($updateCols, $updateParams, $hour, $hoursData, 'spontan');
180            $this->addHourUpdateColumns($updateCols, $updateParams, $hour, $hoursData, 'termin');
181        }
182
183        $sqlUpdate = sprintf(
184            "UPDATE wartenrstatistik
185             SET %s
186             WHERE standortid = :sid
187               AND datum = :d
188             LIMIT 1",
189            implode(', ', $updateCols)
190        );
191
192        if ($commit) {
193            $this->perform($sqlUpdate, $updateParams);
194        } else {
195            echo "[DRY RUN] update scope=$scopeId, date=$dateStr with stats.\n";
196        }
197    }
198
199    private function addHourUpdateColumns(
200        array &$updateCols,
201        array &$updateParams,
202        int $hour,
203        array $hoursData,
204        string $type
205    ): void {
206        $hourSuffix = $this->hourSuffixForStatistic($type);
207        $colWaitCount = sprintf('hour_%02d_waiting_count_%s', $hour, $hourSuffix);
208        $colWaitTime = sprintf('hour_%02d_waiting_time_%s', $hour, $hourSuffix);
209        $colWayTime = sprintf('hour_%02d_way_time_%s', $hour, $hourSuffix);
210
211        $count = $hoursData[$hour][$type]['count'];
212        $avgWait = ($count > 0)
213            ? round($hoursData[$hour][$type]['sumWait'] / $count, 2)
214            : 0.0;
215        // sumWay is total fractional minutes from buerger.way_time (same unit as waiting_time); hourly column is average minutes
216        $avgWay = ($count > 0)
217            ? round($hoursData[$hour][$type]['sumWay'] / $count, 2)
218            : 0.0;
219
220        $updateCols[] = "`$colWaitCount` = :$colWaitCount";
221        $updateCols[] = "`$colWaitTime` = :$colWaitTime";
222        $updateCols[] = "`$colWayTime` = :$colWayTime";
223
224        $updateParams[$colWaitCount] = $count;
225        $updateParams[$colWaitTime] = $avgWait;
226        $updateParams[$colWayTime] = $avgWay;
227    }
228
229    private function extractScopeFromAnmerkung(?string $anmerkung, ?string $customText, ?string $customText2): ?int
230    {
231        if (!$anmerkung && !$customText && !$customText2) {
232            return null;
233        }
234        // Match StandortID from var_export-style array: both quoted and unquoted integers
235        $pattern = "/'StandortID'\s*=>\s*'?([0-9]+)'?/";
236        foreach ([$anmerkung, $customText, $customText2] as $txt) {
237            if (preg_match($pattern, (string)$txt, $matches)) {
238                return (int)$matches[1];
239            }
240        }
241        return null;
242    }
243
244    private function timeToMinutes(?string $timeStr): float
245    {
246        if (!$timeStr || $timeStr === '00:00:00') {
247            return 0.0;
248        }
249        $parts = explode(':', $timeStr);
250        if (count($parts) !== 3) {
251            return 0.0;
252        }
253        $h = (int)$parts[0];
254        $m = (int)$parts[1];
255        $s = (int)$parts[2];
256        $totalSeconds = $h * 3600 + $m * 60 + $s;
257        return round($totalSeconds / 60, 2);
258    }
259
260    private function hourSuffixForStatistic(string $type): string
261    {
262        return $type === 'termin' ? 'appointment' : 'spontaneous';
263    }
264}