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