Code Coverage  | 
      ||||||||||
Lines  | 
       Functions and Methods  | 
       Classes and Traits  | 
      ||||||||
| Total |         | 
       0.00%  | 
       0 / 113  | 
               | 
       0.00%  | 
       0 / 12  | 
       CRAP |         | 
       0.00%  | 
       0 / 1  | 
      
| CalculateDailyWaitingStatisticByCron |         | 
       0.00%  | 
       0 / 113  | 
               | 
       0.00%  | 
       0 / 12  | 
       1406 |         | 
       0.00%  | 
       0 / 1  | 
      
| run |         | 
       0.00%  | 
       0 / 5  | 
               | 
       0.00%  | 
       0 / 1  | 
       2 | |||
| fetchBuergerData |         | 
       0.00%  | 
       0 / 5  | 
               | 
       0.00%  | 
       0 / 1  | 
       2 | |||
| processBuergerRows |         | 
       0.00%  | 
       0 / 16  | 
               | 
       0.00%  | 
       0 / 1  | 
       30 | |||
| determineValidScopeId |         | 
       0.00%  | 
       0 / 10  | 
               | 
       0.00%  | 
       0 / 1  | 
       12 | |||
| determineHourAndType |         | 
       0.00%  | 
       0 / 8  | 
               | 
       0.00%  | 
       0 / 1  | 
       6 | |||
| initializeStatsIfNeeded |         | 
       0.00%  | 
       0 / 9  | 
               | 
       0.00%  | 
       0 / 1  | 
       20 | |||
| saveStatistics |         | 
       0.00%  | 
       0 / 4  | 
               | 
       0.00%  | 
       0 / 1  | 
       12 | |||
| insertStatisticsRow |         | 
       0.00%  | 
       0 / 7  | 
               | 
       0.00%  | 
       0 / 1  | 
       6 | |||
| updateStatisticsValues |         | 
       0.00%  | 
       0 / 16  | 
               | 
       0.00%  | 
       0 / 1  | 
       12 | |||
| addHourUpdateColumns |         | 
       0.00%  | 
       0 / 16  | 
               | 
       0.00%  | 
       0 / 1  | 
       12 | |||
| extractScopeFromAnmerkung |         | 
       0.00%  | 
       0 / 7  | 
               | 
       0.00%  | 
       0 / 1  | 
       42 | |||
| timeToMinutes |         | 
       0.00%  | 
       0 / 10  | 
               | 
       0.00%  | 
       0 / 1  | 
       20 | |||
| 1 | <?php | 
| 2 | |
| 3 | namespace BO\Zmsdb\Helper; | 
| 4 | |
| 5 | use BO\Zmsdb\Base; | 
| 6 | use 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 | */ | 
| 15 | class 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 | // Match StandortID from var_export-style array: both quoted and unquoted integers | 
| 231 | $pattern = "/'StandortID'\s*=>\s*'?([0-9]+)'?/"; | 
| 232 | foreach ([$anmerkung, $customText, $customText2] as $txt) { | 
| 233 | if (preg_match($pattern, (string)$txt, $matches)) { | 
| 234 | return (int)$matches[1]; | 
| 235 | } | 
| 236 | } | 
| 237 | return null; | 
| 238 | } | 
| 239 | |
| 240 | private function timeToMinutes(?string $timeStr): float | 
| 241 | { | 
| 242 | if (!$timeStr || $timeStr === '00:00:00') { | 
| 243 | return 0.0; | 
| 244 | } | 
| 245 | $parts = explode(':', $timeStr); | 
| 246 | if (count($parts) !== 3) { | 
| 247 | return 0.0; | 
| 248 | } | 
| 249 | $h = (int)$parts[0]; | 
| 250 | $m = (int)$parts[1]; | 
| 251 | $s = (int)$parts[2]; | 
| 252 | $totalSeconds = $h * 3600 + $m * 60 + $s; | 
| 253 | return round($totalSeconds / 60, 2); | 
| 254 | } | 
| 255 | } |