diff --git a/zmsstatistic/src/Zmsstatistic/Download/WaitingReport.php b/zmsstatistic/src/Zmsstatistic/Download/WaitingReport.php index b9e05a79a1..9a8ded2b74 100644 --- a/zmsstatistic/src/Zmsstatistic/Download/WaitingReport.php +++ b/zmsstatistic/src/Zmsstatistic/Download/WaitingReport.php @@ -17,6 +17,16 @@ class WaitingReport extends Base { + private const CUSTOMER_TYPE_GESAMT = 'gesamt'; + private const CUSTOMER_TYPE_TERMIN = 'termin'; + private const CUSTOMER_TYPE_SPONTAN = 'spontan'; + + protected $reportPartsGesamt = [ + 'waitingtime_total' => 'Durchschnittliche Wartezeit in Min. (Gesamt)', + 'waitingcount_total' => 'Wartende Gesamtkunden', + 'waytime_total' => 'Durchschnittliche Wegezeit in Min. (Gesamt)', + ]; + protected $reportPartsTermin = [ 'waitingtime_termin' => 'Durchschnittliche Wartezeit in Min. (Terminkunden)', 'waitingcount_termin' => 'Wartende Terminkunden', @@ -33,6 +43,25 @@ class WaitingReport extends Base * @SuppressWarnings(Param) * @return ResponseInterface */ + private function createAndPopulateSheet( + Spreadsheet $spreadsheet, + string $sheetTitle, + array $args, + string $customerType, + bool $isFirstSheet = false + ): void { + if (!$isFirstSheet) { + $spreadsheet->createSheet()->setTitle($sheetTitle); + } else { + $spreadsheet->getActiveSheet()->setTitle($sheetTitle); + } + $spreadsheet->setActiveSheetIndexByName($sheetTitle); + $this->writeInfoHeader($args, $spreadsheet); + foreach ($args['reports'] as $report) { + $this->writeWaitingReport($report, $spreadsheet, $customerType, 'dd.MM.yyyy'); + } + } + public function readResponse( RequestInterface $request, ResponseInterface $response, @@ -42,41 +71,48 @@ public function readResponse( $download = (new Download($request))->setSpreadSheet($title); $spreadsheet = $download->getSpreadSheet(); - // Blatt 1: Terminkunden - $spreadsheet->getActiveSheet()->setTitle('Terminkunden'); - $spreadsheet->setActiveSheetIndexByName('Terminkunden'); - $this->writeInfoHeader($args, $spreadsheet); - foreach ($args['reports'] as $report) { - $this->writeWaitingReport($report, $spreadsheet, /*isTermin*/ true, 'dd.MM.yyyy'); - } - - // Blatt 2: Spontankunden - $spreadsheet->createSheet()->setTitle('Spontankunden'); - $spreadsheet->setActiveSheetIndexByName('Spontankunden'); - $this->writeInfoHeader($args, $spreadsheet); - foreach ($args['reports'] as $report) { - $this->writeWaitingReport($report, $spreadsheet, /*isTermin*/ false, 'dd.MM.yyyy'); - } + $this->createAndPopulateSheet($spreadsheet, 'Gesamt', $args, self::CUSTOMER_TYPE_GESAMT, true); + $this->createAndPopulateSheet($spreadsheet, 'Terminkunden', $args, self::CUSTOMER_TYPE_TERMIN); + $this->createAndPopulateSheet($spreadsheet, 'Spontankunden', $args, self::CUSTOMER_TYPE_SPONTAN); // Für den Download das erste Blatt aktiv lassen - $spreadsheet->setActiveSheetIndexByName('Terminkunden'); + $spreadsheet->setActiveSheetIndexByName('Gesamt'); return $download->writeDownload($response); } + private function assertValidCustomerType(string $customerType): void + { + $validTypes = [ + self::CUSTOMER_TYPE_GESAMT, + self::CUSTOMER_TYPE_TERMIN, + self::CUSTOMER_TYPE_SPONTAN, + ]; + + if (!in_array($customerType, $validTypes, true)) { + throw new \InvalidArgumentException( + "Invalid customer type: {$customerType}. Must be one of: " . implode(', ', $validTypes) + ); + } + } + public function writeWaitingReport( ReportEntity $report, Spreadsheet $spreadsheet, - bool $isTermin, + string $customerType, $datePatternCol = 'dd.MM.yyyy', ) { + $this->assertValidCustomerType($customerType); + $sheet = $spreadsheet->getActiveSheet(); $this->writeHeader($report, $sheet, $datePatternCol); - $this->writeTotals($report, $sheet, $isTermin); - if ($isTermin) { + $this->writeTotals($report, $sheet, $customerType); + if ($customerType === self::CUSTOMER_TYPE_TERMIN) { $parts = $this->reportPartsTermin; - } else { + } elseif ($customerType === self::CUSTOMER_TYPE_SPONTAN) { $parts = $this->reportPartsSpontan; + } else { + $parts = $this->reportPartsGesamt; } foreach ($parts as $partName => $headline) { $this->writeReportPart($report, $sheet, $partName, $headline); @@ -109,25 +145,40 @@ public function writeHeader(ReportEntity $report, $sheet, $datePatternCol) } } - public function writeTotals(ReportEntity $report, $sheet, bool $isTermin) + private function getCustomerTypeKeys(string $customerType): array { - $entity = clone $report; - $totals = $entity->data['max']; - unset($entity->data['max']); - - if ($isTermin) { - $keys = [ + $this->assertValidCustomerType($customerType); + $keyMappings = [ + self::CUSTOMER_TYPE_TERMIN => [ 'max' => 'max_waitingtime_termin', 'avg' => 'average_waitingtime_termin', 'avg_way' => 'average_waytime_termin', - ]; - } else { - $keys = [ + ], + self::CUSTOMER_TYPE_SPONTAN => [ 'max' => 'max_waitingtime', 'avg' => 'average_waitingtime', 'avg_way' => 'average_waytime', - ]; - } + ], + self::CUSTOMER_TYPE_GESAMT => [ + 'max' => 'max_waitingtime_total', + 'avg' => 'average_waitingtime_total', + 'avg_way' => 'average_waytime_total', + ], + ]; + + return $keyMappings[$customerType]; + } + + public function writeTotals(ReportEntity $report, $sheet, string $customerType) + { + $this->assertValidCustomerType($customerType); + + $entity = clone $report; + $totals = $entity->data['max']; + unset($entity->data['max']); + + $keys = $this->getCustomerTypeKeys($customerType); + $reportTotal['max'][] = 'Stunden-Max (Spaltenmaximum) der Wartezeit in Min.'; $reportTotal['average'][] = 'Stundendurchschnitt (Spalten) der Wartezeit in Min.'; $reportTotal['average_waytime'][] = 'Stundendurchschnitt (Spalten) der Wegezeit in Min.'; diff --git a/zmsstatistic/src/Zmsstatistic/Helper/ReportHelper.php b/zmsstatistic/src/Zmsstatistic/Helper/ReportHelper.php index 71a500586b..affc73b0df 100644 --- a/zmsstatistic/src/Zmsstatistic/Helper/ReportHelper.php +++ b/zmsstatistic/src/Zmsstatistic/Helper/ReportHelper.php @@ -29,6 +29,86 @@ public static function withMaxAndAverage($entity, $targetKey) return $entity; } + public static function withTotalCustomers($entity) + { + foreach ($entity->data as $dateKey => $dateItems) { + if (!is_array($dateItems)) { + continue; + } + + foreach ($dateItems as $hour => $hourItems) { + if (!is_array($hourItems)) { + continue; + } + + $countSpontan = (int) ($hourItems['waitingcount'] ?? 0); + $countTermin = (int) ($hourItems['waitingcount_termin'] ?? 0); + $countTotal = $countSpontan + $countTermin; + + $waitSpontan = (float) ($hourItems['waitingtime'] ?? 0); + $waitTermin = (float) ($hourItems['waitingtime_termin'] ?? 0); + + $waySpontan = (float) ($hourItems['waytime'] ?? 0); + $wayTermin = (float) ($hourItems['waytime_termin'] ?? 0); + + $entity->data[$dateKey][$hour]['waitingcount_total'] = $countTotal; + + $entity->data[$dateKey][$hour]['waitingtime_total'] = ($countTotal > 0) + ? (($waitSpontan * $countSpontan) + ($waitTermin * $countTermin)) / $countTotal + : 0; + + $entity->data[$dateKey][$hour]['waytime_total'] = ($countTotal > 0) + ? (($waySpontan * $countSpontan) + ($wayTermin * $countTermin)) / $countTotal + : 0; + } + } + + return $entity; + } + + public static function withGlobalMaxAndAverage($entity, string $targetKey) + { + $maxima = 0; + $total = 0; + $count = 0; + + foreach ($entity->data as $dateItems) { + if (!is_array($dateItems)) { + continue; + } + foreach ($dateItems as $hourItems) { + if (!is_array($hourItems)) { + continue; + } + $value = $hourItems[$targetKey] ?? null; + if (is_numeric($value) && $value > 0) { + $value = (float) $value; + $maxima = ($maxima > $value) ? $maxima : $value; + $total += $value; + $count++; + } + } + } + + $average = ($count > 0) ? ($total / $count) : 0; + + if (is_object($entity->data)) { + if (!isset($entity->data->max) || !is_array($entity->data->max)) { + $entity->data->max = []; + } + $entity->data->max['max_' . $targetKey] = $maxima; + $entity->data->max['average_' . $targetKey] = $average; + } elseif (is_array($entity->data)) { + if (!isset($entity->data['max']) || !is_array($entity->data['max'])) { + $entity->data['max'] = []; + } + $entity->data['max']['max_' . $targetKey] = $maxima; + $entity->data['max']['average_' . $targetKey] = $average; + } + + return $entity; + } + public static function formatTimeValue($value) { if (!is_numeric($value)) { diff --git a/zmsstatistic/src/Zmsstatistic/ReportWaitingDepartment.php b/zmsstatistic/src/Zmsstatistic/ReportWaitingDepartment.php index e9a71d29f6..f116a4fca0 100644 --- a/zmsstatistic/src/Zmsstatistic/ReportWaitingDepartment.php +++ b/zmsstatistic/src/Zmsstatistic/ReportWaitingDepartment.php @@ -23,6 +23,9 @@ class ReportWaitingDepartment extends BaseController 'waitingcalculated_termin', 'waytime', 'waytime_termin', + 'waitingcount_total', + 'waitingtime_total', + 'waytime_total', ]; protected $groupfields = [ @@ -46,16 +49,25 @@ public function readResponse( $exchangeWaiting = null; if (isset($args['period'])) { $exchangeWaiting = \App::$http - ->readGetResult('/warehouse/waitingdepartment/' . $this->department->id . '/' . $args['period'] . '/') - ->getEntity() - ->toGrouped($this->groupfields, $this->hashset) - ->withMaxByHour($this->hashset) - ->withMaxAndAverageFromWaitingTime(); + ->readGetResult('/warehouse/waitingdepartment/' . $this->department->id . '/' . $args['period'] . '/') + ->getEntity() + ->toGrouped($this->groupfields, $this->hashset); + + $exchangeWaiting = ReportHelper::withTotalCustomers($exchangeWaiting); + + $exchangeWaiting = $exchangeWaiting + ->withMaxByHour($this->hashset) + ->withMaxAndAverageFromWaitingTime(); $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waitingtime'); $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waitingtime_termin'); $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waytime'); $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waytime_termin'); + + $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waitingtime_total'); + $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waytime_total'); + $exchangeWaiting = ReportHelper::withGlobalMaxAndAverage($exchangeWaiting, 'waitingtime_total'); + $exchangeWaiting = ReportHelper::withGlobalMaxAndAverage($exchangeWaiting, 'waytime_total'); } $type = $validator->getParameter('type')->isString()->getValue(); diff --git a/zmsstatistic/src/Zmsstatistic/ReportWaitingOrganisation.php b/zmsstatistic/src/Zmsstatistic/ReportWaitingOrganisation.php index d504d68ed4..0837602520 100644 --- a/zmsstatistic/src/Zmsstatistic/ReportWaitingOrganisation.php +++ b/zmsstatistic/src/Zmsstatistic/ReportWaitingOrganisation.php @@ -23,6 +23,9 @@ class ReportWaitingOrganisation extends BaseController 'waitingcalculated_termin', 'waytime', 'waytime_termin', + 'waitingcount_total', + 'waitingtime_total', + 'waytime_total', ]; protected $groupfields = [ @@ -46,16 +49,25 @@ public function readResponse( $exchangeWaiting = null; if (isset($args['period'])) { $exchangeWaiting = \App::$http - ->readGetResult('/warehouse/waitingorganisation/' . $this->organisation->id . '/' . $args['period'] . '/') - ->getEntity() - ->toGrouped($this->groupfields, $this->hashset) - ->withMaxByHour($this->hashset) - ->withMaxAndAverageFromWaitingTime(); + ->readGetResult('/warehouse/waitingorganisation/' . $this->organisation->id . '/' . $args['period'] . '/') + ->getEntity() + ->toGrouped($this->groupfields, $this->hashset); + + $exchangeWaiting = ReportHelper::withTotalCustomers($exchangeWaiting); + + $exchangeWaiting = $exchangeWaiting + ->withMaxByHour($this->hashset) + ->withMaxAndAverageFromWaitingTime(); $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waitingtime'); $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waitingtime_termin'); $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waytime'); $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waytime_termin'); + + $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waitingtime_total'); + $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waytime_total'); + $exchangeWaiting = ReportHelper::withGlobalMaxAndAverage($exchangeWaiting, 'waitingtime_total'); + $exchangeWaiting = ReportHelper::withGlobalMaxAndAverage($exchangeWaiting, 'waytime_total'); } $type = $validator->getParameter('type')->isString()->getValue(); diff --git a/zmsstatistic/src/Zmsstatistic/Service/ReportWaitingService.php b/zmsstatistic/src/Zmsstatistic/Service/ReportWaitingService.php index 62d44608dd..c284e012fe 100644 --- a/zmsstatistic/src/Zmsstatistic/Service/ReportWaitingService.php +++ b/zmsstatistic/src/Zmsstatistic/Service/ReportWaitingService.php @@ -23,6 +23,9 @@ class ReportWaitingService 'waitingcalculated_termin', 'waytime', 'waytime_termin', + 'waitingcount_total', + 'waitingtime_total', + 'waytime_total', ]; protected $groupfields = [ @@ -85,7 +88,11 @@ public function getExchangeWaitingForPeriod(string $scopeId, string $period): mi $exchangeWaiting = \App::$http ->readGetResult('/warehouse/waitingscope/' . $scopeId . '/' . $period . '/') ->getEntity() - ->toGrouped($this->groupfields, $this->hashset) + ->toGrouped($this->groupfields, $this->hashset); + + $exchangeWaiting = ReportHelper::withTotalCustomers($exchangeWaiting); + + $exchangeWaiting = $exchangeWaiting ->withMaxByHour($this->hashset) ->withMaxAndAverageFromWaitingTime(); @@ -95,6 +102,15 @@ public function getExchangeWaitingForPeriod(string $scopeId, string $period): mi $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waytime'); $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waytime_termin'); + // per-date max/avg for total + $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waitingtime_total'); + $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waytime_total'); + + // global max/avg for total + $exchangeWaiting = ReportHelper::withGlobalMaxAndAverage($exchangeWaiting, 'waitingtime_total'); + $exchangeWaiting = ReportHelper::withGlobalMaxAndAverage($exchangeWaiting, 'waytime_total'); + + return $exchangeWaiting; } catch (Exception $exception) { return null; @@ -178,6 +194,8 @@ private function createFilteredExchangeWaiting( $exchangeWaiting = $exchangeWaiting ->toGrouped($this->groupfields, $this->hashset); + $exchangeWaiting = ReportHelper::withTotalCustomers($exchangeWaiting); + $exchangeWaiting = $exchangeWaiting->withMaxByHour($this->hashset) ->withMaxAndAverageFromWaitingTime(); @@ -186,6 +204,12 @@ private function createFilteredExchangeWaiting( $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waytime'); $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waytime_termin'); + $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waitingtime_total'); + $exchangeWaiting = ReportHelper::withMaxAndAverage($exchangeWaiting, 'waytime_total'); + $exchangeWaiting = ReportHelper::withGlobalMaxAndAverage($exchangeWaiting, 'waitingtime_total'); + $exchangeWaiting = ReportHelper::withGlobalMaxAndAverage($exchangeWaiting, 'waytime_total'); + + return $exchangeWaiting; } diff --git a/zmsstatistic/templates/page/reportWaitingIndex.twig b/zmsstatistic/templates/page/reportWaitingIndex.twig index a83d5ac700..47cefbf377 100644 --- a/zmsstatistic/templates/page/reportWaitingIndex.twig +++ b/zmsstatistic/templates/page/reportWaitingIndex.twig @@ -18,6 +18,7 @@ {% set reportSections = [ + {'title': 'Gesamt', 'suffix': '_total'}, {'title': 'Terminkunden', 'suffix': '_termin'}, {'title': 'Spontankunden', 'suffix': ''} ] %} diff --git a/zmsstatistic/tests/Zmsstatistic/ReportWaitingDepartmentTest.php b/zmsstatistic/tests/Zmsstatistic/ReportWaitingDepartmentTest.php index 10b55cbbc0..359153b40d 100644 --- a/zmsstatistic/tests/Zmsstatistic/ReportWaitingDepartmentTest.php +++ b/zmsstatistic/tests/Zmsstatistic/ReportWaitingDepartmentTest.php @@ -1,6 +1,7 @@ render(['period' => '2016-03'], [], []); + $body = (string) $response->getBody(); + + $this->assertStringContainsString('Gesamt', $body); + $this->assertStringContainsString('Terminkunden', $body); + $this->assertStringContainsString('Spontankunden', $body); + + $this->assertTrue(strpos($body, 'Gesamt') < strpos($body, 'Terminkunden')); + $this->assertTrue(strpos($body, 'Terminkunden') < strpos($body, 'Spontankunden')); $this->assertStringContainsString('Zeilenmaximum', (string) $response->getBody()); $this->assertStringContainsString( 'Auswertung für Bürgeramt im Zeitraum März 2016', @@ -153,6 +162,20 @@ public function testWithDownloadXLSX() ); $response = $this->render(['period' => '2016-03'], ['type' => 'xlsx'], []); $this->assertStringContainsString('xlsx', $response->getHeaderLine('Content-Disposition')); + + if (method_exists($response->getBody(), 'rewind')) { + $response->getBody()->rewind(); + } + + $tmp = tempnam(sys_get_temp_dir(), 'waiting_xlsx_'); + file_put_contents($tmp, (string) $response->getBody()); + + $spreadsheet = IOFactory::load($tmp); + + $this->assertSame(['Gesamt', 'Terminkunden', 'Spontankunden'], $spreadsheet->getSheetNames()); + $this->assertSame('Gesamt', $spreadsheet->getActiveSheet()->getTitle()); + + @unlink($tmp); // Clean up output buffer (discard any captured output) ob_end_clean(); diff --git a/zmsstatistic/tests/Zmsstatistic/ReportWaitingOrganisationTest.php b/zmsstatistic/tests/Zmsstatistic/ReportWaitingOrganisationTest.php index a6497528c6..9b007aea0d 100644 --- a/zmsstatistic/tests/Zmsstatistic/ReportWaitingOrganisationTest.php +++ b/zmsstatistic/tests/Zmsstatistic/ReportWaitingOrganisationTest.php @@ -1,6 +1,7 @@ render(['period' => '2016-03'], [], []); + $body = (string) $response->getBody(); + + $this->assertStringContainsString('Gesamt', $body); + $this->assertStringContainsString('Terminkunden', $body); + $this->assertStringContainsString('Spontankunden', $body); + + // Reihenfolge: Gesamt vor Termin vor Spontan + $this->assertTrue(strpos($body, 'Gesamt') < strpos($body, 'Terminkunden')); + $this->assertTrue(strpos($body, 'Terminkunden') < strpos($body, 'Spontankunden')); $this->assertStringContainsString('Zeilenmaximum', (string) $response->getBody()); $this->assertStringContainsString( 'Auswertung für Charlottenburg-Wilmersdorf im Zeitraum März 2016', @@ -153,7 +163,19 @@ public function testWithDownloadXLSX() ); $response = $this->render(['period' => '2016-03'], ['type' => 'xlsx'], []); $this->assertStringContainsString('xlsx', $response->getHeaderLine('Content-Disposition')); - + if (method_exists($response->getBody(), 'rewind')) { + $response->getBody()->rewind(); + } + + $tmp = tempnam(sys_get_temp_dir(), 'waiting_xlsx_'); + file_put_contents($tmp, (string) $response->getBody()); + + $spreadsheet = IOFactory::load($tmp); + + $this->assertSame(['Gesamt', 'Terminkunden', 'Spontankunden'], $spreadsheet->getSheetNames()); + $this->assertSame('Gesamt', $spreadsheet->getActiveSheet()->getTitle()); + + @unlink($tmp); // Clean up output buffer (discard any captured output) ob_end_clean(); }