Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 113
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
CalculateDailyWaitingStatisticByCron
0.00% covered (danger)
0.00%
0 / 113
0.00% covered (danger)
0.00%
0 / 12
1406
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 / 16
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 / 16
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
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              wartezeit,
39              wegezeit,
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 wartezeit NULL oder leer ist => storniert oder hatte keine echte Wartezeit => überspringen
60            if (empty($br['wartezeit'])) {
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['wartezeit']);
72            $wayMins = is_numeric($br['wegezeit']) ? round($br['wegezeit'] / 60, 2) : 0.0;
73
74            $dateStr = $br['Datum'];
75            $this->initializeStatsIfNeeded($statsByScopeDate, $scopeId, $dateStr);
76
77            // Eintrag zur Stats hinzufügen
78            $statsByScopeDate[$scopeId][$dateStr][$hour][$type]['count'] += 1;
79            $statsByScopeDate[$scopeId][$dateStr][$hour][$type]['sumWait'] += $waitMins;
80            $statsByScopeDate[$scopeId][$dateStr][$hour][$type]['sumWay'] += $wayMins;
81        }
82
83        return $statsByScopeDate;
84    }
85
86    // StandortID korrigieren, falls 0. => Wir parsen bei Bedarf aus Anmerkung / custom_text_field.
87    private function determineValidScopeId(array $buergerRecord): int
88    {
89        $scopeId = (int)$buergerRecord['StandortID'];
90        if ($scopeId <= 0) {
91            $parsedScope = $this->extractScopeFromAnmerkung(
92                $buergerRecord['Anmerkung'],
93                $buergerRecord['custom_text_field'],
94                $buergerRecord['custom_text_field2'],
95            );
96
97            if ($parsedScope) {
98                $scopeId = (int)$parsedScope;
99            }
100        }
101
102        return $scopeId;
103    }
104
105    // Unterscheidung zwischen "spontan" und "termin"
106    // - Wenn 'Uhrzeit'=='00:00:00', behandeln wir es als spontan angekommen => Stunde aus wsm_aufnahmezeit
107    private function determineHourAndType(array $buergerRecord): array
108    {
109        $type = 'termin';
110        $hourStr = $buergerRecord['Uhrzeit'];
111
112        if ($buergerRecord['Uhrzeit'] === '00:00:00') {
113            $type = 'spontan';
114            $hourStr = $buergerRecord['wsm_aufnahmezeit'];
115        }
116
117        $parts = explode(':', $hourStr);
118        $hour = (int)$parts[0];
119
120        return [$hour, $type];
121    }
122
123    // Falls für diesen StandortID das Datum noch nicht existiert
124    private function initializeStatsIfNeeded(array &$statsByScopeDate, int $scopeId, string $dateStr): void
125    {
126        if (!isset($statsByScopeDate[$scopeId])) {
127            $statsByScopeDate[$scopeId] = [];
128        }
129
130        if (!isset($statsByScopeDate[$scopeId][$dateStr])) {
131            $statsByScopeDate[$scopeId][$dateStr] = [];
132            foreach (range(0, 23) as $h) {
133                $statsByScopeDate[$scopeId][$dateStr][$h] = [
134                    'spontan' => ['count' => 0, 'sumWait' => 0.0, 'sumWay' => 0.0],
135                    'termin'  => ['count' => 0, 'sumWait' => 0.0, 'sumWay' => 0.0],
136                ];
137            }
138        }
139    }
140
141    private function saveStatistics(array $statsByScopeDate, bool $commit): void
142    {
143        foreach ($statsByScopeDate as $scopeId => $dateArray) {
144            foreach ($dateArray as $dateStr => $hoursData) {
145                $this->insertStatisticsRow($scopeId, $dateStr, $commit);
146                $this->updateStatisticsValues($scopeId, $dateStr, $hoursData, $commit);
147            }
148        }
149    }
150
151    //Eine Zeile in wartenrstatistik für jeden (Standort, Datum) einfügen
152    private function insertStatisticsRow(int $scopeId, string $dateStr, bool $commit): void
153    {
154        if ($commit) {
155            $insertSql = "
156                INSERT IGNORE INTO wartenrstatistik (standortid, datum)
157                VALUES (:sid, :d)
158            ";
159            $this->perform($insertSql, [
160                'sid' => $scopeId,
161                'd'   => $dateStr
162            ]);
163        }
164    }
165
166    private function updateStatisticsValues(int $scopeId, string $dateStr, array $hoursData, bool $commit): void
167    {
168        // Eine einzelne UPDATE-Anweisung für alle 24 Stundenspalten erstellen
169        $updateParams = [
170            'sid' => $scopeId,
171            'd'   => $dateStr
172        ];
173        $updateCols = [];
174
175        // Für jede Stunde 0..23 Spalten für "spontan" und "termin" füllen
176        foreach (range(0, 23) as $hour) {
177            $this->addHourUpdateColumns($updateCols, $updateParams, $hour, $hoursData, 'spontan');
178            $this->addHourUpdateColumns($updateCols, $updateParams, $hour, $hoursData, 'termin');
179        }
180
181        $sqlUpdate = sprintf(
182            "UPDATE wartenrstatistik
183             SET %s
184             WHERE standortid = :sid
185               AND datum = :d
186             LIMIT 1",
187            implode(', ', $updateCols)
188        );
189
190        if ($commit) {
191            $this->perform($sqlUpdate, $updateParams);
192        } else {
193            echo "[DRY RUN] update scope=$scopeId, date=$dateStr with stats.\n";
194        }
195    }
196
197    private function addHourUpdateColumns(
198        array &$updateCols,
199        array &$updateParams,
200        int $hour,
201        array $hoursData,
202        string $type
203    ): void {
204        $colWaitCount = sprintf('wartende_ab_%02d_%s', $hour, $type);
205        $colWaitTime = sprintf('echte_zeit_ab_%02d_%s', $hour, $type);
206        $colWayTime = sprintf('wegezeit_ab_%02d_%s', $hour, $type);
207
208        $count = $hoursData[$hour][$type]['count'];
209        $avgWait = ($count > 0)
210            ? round($hoursData[$hour][$type]['sumWait'] / $count, 2)
211            : 0.0;
212        $avgWay = ($count > 0)
213            ? round($hoursData[$hour][$type]['sumWay'] / $count, 2)
214            : 0.0;
215
216        $updateCols[] = "`$colWaitCount` = :$colWaitCount";
217        $updateCols[] = "`$colWaitTime` = :$colWaitTime";
218        $updateCols[] = "`$colWayTime` = :$colWayTime";
219
220        $updateParams[$colWaitCount] = $count;
221        $updateParams[$colWaitTime] = $avgWait;
222        $updateParams[$colWayTime] = $avgWay;
223    }
224
225    private function extractScopeFromAnmerkung(?string $anmerkung, ?string $customText, ?string $customText2): ?int
226    {
227        if (!$anmerkung && !$customText && !$customText2) {
228            return null;
229        }
230        $pattern = "/'StandortID' => '(\d+)'/";
231        foreach ([$anmerkung, $customText, $customText2] as $txt) {
232            if (preg_match($pattern, (string)$txt, $matches)) {
233                return (int)$matches[1];
234            }
235        }
236        return null;
237    }
238
239    private function timeToMinutes(?string $timeStr): float
240    {
241        if (!$timeStr || $timeStr === '00:00:00') {
242            return 0.0;
243        }
244        $parts = explode(':', $timeStr);
245        if (count($parts) !== 3) {
246            return 0.0;
247        }
248        $h = (int)$parts[0];
249        $m = (int)$parts[1];
250        $s = (int)$parts[2];
251        $totalSeconds = $h * 3600 + $m * 60 + $s;
252        return round($totalSeconds / 60, 2);
253    }
254}