Lines 84.82% 218 / 257
Methods 74.07% 20 / 27
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 createAndPopulateSheet 100.00% 7 / 7 100.00% 1 / 1 3
 readResponse 100.00% 8 / 8 100.00% 1 / 1 1
 assertValidCustomerType 66.66% 6 / 9 0.00% 0 / 1 2.15
 writeWaitingReport 100.00% 12 / 12 100.00% 1 / 1 4
 writeHeader 100.00% 16 / 16 100.00% 1 / 1 4
 getCustomerTypeKeys 100.00% 19 / 19 100.00% 1 / 1 1
 writeTotals 100.00% 17 / 17 100.00% 1 / 1 2
 writeReportPart 100.00% 25 / 25 100.00% 1 / 1 9
 [BO\Zmsstatistic\Download\Base] sanitizeDownloadFilenamePart 100.00% 1 / 1 100.00% 1 / 1 1
 [BO\Zmsstatistic\Download\Base] resolveDictionaryHeader 33.33% 2 / 6 0.00% 0 / 1 5.67
 [BO\Zmsstatistic\Download\Base] writeRawReport 100.00% 9 / 9 100.00% 1 / 1 5
 [BO\Zmsstatistic\Download\Base] writeFilteredExchangeReport 93.75% 15 / 16 0.00% 0 / 1 6.01
 [BO\Zmsstatistic\Download\Base] writeInfoHeader 92.59% 25 / 27 0.00% 0 / 1 10.04
 [BO\Zmsstatistic\Download\Base] setDateTime 75.00% 3 / 4 0.00% 0 / 1 3.14
 [BO\Zmsstatistic\Download\Base] getFormatedDates 100.00% 9 / 9 100.00% 1 / 1 1
 [BO\Zmsstatistic\BaseController] __invoke 100.00% 5 / 5 100.00% 1 / 1 2
 [BO\Zmsstatistic\Helper\Access] initAccessRights 100.00% 6 / 6 100.00% 1 / 1 4
 [BO\Zmsstatistic\Helper\Access] readWorkstation 100.00% 2 / 2 100.00% 1 / 1 1
 [BO\Zmsstatistic\Helper\Access] readDepartment 100.00% 5 / 5 100.00% 1 / 1 2
 [BO\Zmsstatistic\Helper\Access] readOrganisation 100.00% 5 / 5 100.00% 1 / 1 2
 [BO\Zmsstatistic\Helper\Access] readOwner 100.00% 5 / 5 100.00% 1 / 1 2
 [BO\Zmsstatistic\Helper\Access] validateAccessRights 100.00% 3 / 3 100.00% 1 / 1 1
 [BO\Zmsstatistic\Helper\Access] validateAccess 100.00% 4 / 4 100.00% 1 / 1 7
 [BO\Zmsstatistic\Helper\Access] validateScope 66.66% 2 / 3 0.00% 0 / 1 4.59
 [BO\Zmsstatistic\Helper\Access] isPathWithoutScope 100.00% 5 / 5 100.00% 1 / 1 4
 [BO\Zmsstatistic\Helper\Access] testLogin 0.00% 0 / 27 0.00% 0 / 1 42
 [BO\Zmsstatistic\Helper\Access] exceptionTemplateExists 100.00% 2 / 2 100.00% 1 / 1 1
18class WaitingReport extends Base
19{
20    private const string CUSTOMER_TYPE_GESAMT = 'gesamt';
21    private const string CUSTOMER_TYPE_TERMIN = 'termin';
22    private const string CUSTOMER_TYPE_SPONTAN = 'spontan';
23
24    protected array $reportPartsGesamt = [
25        'waitingtime_total' => 'Durchschnittliche Wartezeit in Min. (Gesamt)',
26        'waitingcount_total' => 'Wartende Gesamtkunden',
27        'waytime_total' => 'Durchschnittliche Wegezeit in Min. (Gesamt)',
28    ];
29
30    protected array $reportPartsTermin = [
31        'waitingtime_termin' => 'Durchschnittliche Wartezeit in Min. (Terminkunden)',
32        'waitingcount_termin' => 'Wartende Terminkunden',
33        'waytime_termin' => 'Durchschnittliche Wegezeit in Min. (Terminkunden)',
34    ];
35
36    protected array $reportPartsSpontan = [
37        'waitingtime' => 'Durchschnittliche Wartezeit in Min. (Spontankunden)',
38        'waitingcount' => 'Wartende Spontankunden',
39        'waytime' => 'Durchschnittliche Wegezeit in Min. (Spontankunden)',
40    ];
41
42    /**
43     * @SuppressWarnings(Param)
44     */
45    private function createAndPopulateSheet(
46        Spreadsheet $spreadsheet,
47        string $sheetTitle,
48        array $args,
49        string $customerType,
50        bool $isFirstSheet = false
51    ): void {
52        if (!$isFirstSheet) {
53            $spreadsheet->createSheet()->setTitle($sheetTitle);
54        } else {
55            $spreadsheet->getActiveSheet()->setTitle($sheetTitle);
56        }
57        $spreadsheet->setActiveSheetIndexByName($sheetTitle);
58        $this->writeInfoHeader($args, $spreadsheet);
59        foreach ($args['reports'] as $report) {
60            $this->writeWaitingReport($report, $spreadsheet, $customerType, 'dd.MM.yyyy');
61        }
62    }
63
64    #[\Override]
65    public function readResponse(
66        RequestInterface $request,
67        ResponseInterface $response,
68        array $args
69    ): mixed {
70        $title = 'waitingstatistic_' . $args['period'];
71        $download = (new Download($request))->setSpreadSheet($title);
72        $spreadsheet = $download->getSpreadSheet();
73
74        $this->createAndPopulateSheet($spreadsheet, 'Gesamt', $args, self::CUSTOMER_TYPE_GESAMT, true);
75        $this->createAndPopulateSheet($spreadsheet, 'Terminkunden', $args, self::CUSTOMER_TYPE_TERMIN);
76        $this->createAndPopulateSheet($spreadsheet, 'Spontankunden', $args, self::CUSTOMER_TYPE_SPONTAN);
77
78        // Für den Download das erste Blatt aktiv lassen
79        $spreadsheet->setActiveSheetIndexByName('Gesamt');
80
81        return $download->writeDownload($response);
82    }
83
84    private function assertValidCustomerType(string $customerType): void
85    {
86        $validTypes = [
87            self::CUSTOMER_TYPE_GESAMT,
88            self::CUSTOMER_TYPE_TERMIN,
89            self::CUSTOMER_TYPE_SPONTAN,
90        ];
91
92        if (!in_array($customerType, $validTypes, true)) {
93            throw new \InvalidArgumentException(
94                "Invalid customer type: {$customerType}. Must be one of: " . implode(', ', $validTypes)
95            );
96        }
97    }
98
99    public function writeWaitingReport(
100        ReportEntity $report,
101        Spreadsheet $spreadsheet,
102        string $customerType,
103        string $datePatternCol = 'dd.MM.yyyy',
104    ): Spreadsheet {
105        $this->assertValidCustomerType($customerType);
106
107        $sheet = $spreadsheet->getActiveSheet();
108        $this->writeHeader($report, $sheet, $datePatternCol);
109        $this->writeTotals($report, $sheet, $customerType);
110        if ($customerType === self::CUSTOMER_TYPE_TERMIN) {
111            $parts = $this->reportPartsTermin;
112        } elseif ($customerType === self::CUSTOMER_TYPE_SPONTAN) {
113            $parts = $this->reportPartsSpontan;
114        } else {
115            $parts = $this->reportPartsGesamt;
116        }
117        foreach ($parts as $partName => $headline) {
118            $this->writeReportPart($report, $sheet, $partName, $headline);
119        }
120
121        return $spreadsheet;
122    }
123
124    public function writeHeader(
125        ReportEntity $report,
126        \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet,
127        string $datePatternCol
128    ): void {
129        $reportHeader = [];
130        $reportHeader[] = null;
131        $reportHeader[] = 'Max.';
132        $dates = array_keys($report->data);
133        sort($dates);
134        foreach ($dates as $date) {
135            if (! in_array($date, static::$ignoreColumns)) {
136                $date = $this->getFormatedDates($this->setDateTime($date), $datePatternCol);
137                $reportHeader[] = $date;
138            }
139        }
140        $startRow = $sheet->getHighestRow() + 2;
141        $sheet->fromArray($reportHeader, null, 'A' . $startRow);
142        // Datumszellen (ab B) in dieser Zeile fett schreiben
143        $lastColIdx = count($reportHeader);              // Anzahl der geschriebenen Zellen
144        if ($lastColIdx >= 2) {
145            $start = "B{$startRow}";
146            $end   = Coordinate::stringFromColumnIndex($lastColIdx) . $startRow;
147            $sheet->getStyle("$start:$end")->getFont()->setBold(true);
148        }
149    }
150
151    private function getCustomerTypeKeys(string $customerType): array
152    {
153        $this->assertValidCustomerType($customerType);
154        $keyMappings = [
155            self::CUSTOMER_TYPE_TERMIN => [
156                'max' => 'max_waitingtime_termin',
157                'avg' => 'average_waitingtime_termin',
158                'avg_way' => 'average_waytime_termin',
159            ],
160            self::CUSTOMER_TYPE_SPONTAN => [
161                'max' => 'max_waitingtime',
162                'avg' => 'average_waitingtime',
163                'avg_way' => 'average_waytime',
164            ],
165            self::CUSTOMER_TYPE_GESAMT => [
166                'max' => 'max_waitingtime_total',
167                'avg' => 'average_waitingtime_total',
168                'avg_way' => 'average_waytime_total',
169            ],
170        ];
171
172        return $keyMappings[$customerType];
173    }
174
175    public function writeTotals(ReportEntity $report, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet, string $customerType): void
176    {
177        $this->assertValidCustomerType($customerType);
178
179        $entity = clone $report;
180        $totals = $entity->data['max'];
181        unset($entity->data['max']);
182
183        $keys = $this->getCustomerTypeKeys($customerType);
184
185        $reportTotal = [];
186        $reportTotal['max'][] = 'Stunden-Max (Spaltenmaximum) der Wartezeit in Min.';
187        $reportTotal['average'][] = 'Stundendurchschnitt (Spalten) der Wartezeit in Min.';
188        $reportTotal['average_waytime'][] = 'Stundendurchschnitt (Spalten) der Wegezeit in Min.';
189
190        $reportTotal['max'][] = ReportHelper::formatTimeValue($totals[$keys['max']]);
191        $reportTotal['average'][] = ReportHelper::formatTimeValue($totals[$keys['avg']]);
192        $reportTotal['average_waytime'][] = ReportHelper::formatTimeValue($totals[$keys['avg_way']]);
193
194        foreach ($entity->data as $entry) {
195            $reportTotal['max'][] = ReportHelper::formatTimeValue($entry[$keys['max']]);
196            $reportTotal['average'][] = ReportHelper::formatTimeValue($entry[$keys['avg']]);
197            $reportTotal['average_waytime'][] = ReportHelper::formatTimeValue($entry[$keys['avg_way']]);
198        }
199        $sheet->fromArray($reportTotal, null, 'A' . ($sheet->getHighestRow() + 1));
200    }
201
202    public function writeReportPart(
203        ReportEntity $report,
204        \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet,
205        string $rangeName,
206        string $headline
207    ): void {
208        $entity = clone $report;
209        $totals = $entity->data['max'];
210        unset($entity->data['max']);
211        $reportData = [];
212        $reportData['headline'] = ['Zeitabschnitte','Tagesmaximum (Zeilenmaximum)',$headline];
213        $formatAsTime = strpos($rangeName, 'waitingcount') === false;
214        foreach ($entity->data as $entry) {
215            foreach ($entry as $hour => $item) {
216                if (5 < $hour && 19 > $hour) {
217                    if (! isset($reportData[$hour])) {
218                        $reportData[$hour] = [];
219                    }
220                    $range = $hour . '-' . ($hour + 1) . ' Uhr';
221                    if (! in_array($range, $reportData[$hour])) {
222                        $reportData[$hour][] = $range;
223                        $totalValue = $totals[$hour][$rangeName];
224                        $reportData[$hour][] = $formatAsTime
225                            ? ReportHelper::formatTimeValue($totalValue)
226                            : $totalValue;
227                    }
228                    $value = $item[$rangeName] ?? '-';
229                    $reportData[$hour][] = $formatAsTime
230                        ? ReportHelper::formatTimeValue($value)
231                        : $value;
232                }
233            }
234        }
235        $startRow = $sheet->getHighestRow() + 2;
236        $sheet->fromArray($reportData, null, 'A' . $startRow);
237        $sheet->getStyle("A{$startRow}:C{$startRow}")->getFont()->setBold(true);
238    }
239}

Inherited from BO\Zmsstatistic\Download\Base

70    protected function sanitizeDownloadFilenamePart(string $part): string
71    {
72        return str_replace(',', '_', $part);
73    }
75    protected function resolveDictionaryHeader(array $item): string
76    {
77        if (!empty($item['description'])) {
78            return (string) $item['description'];
79        }
80
81        $variable = $item['variable'] ?? '';
82        if (isset(static::$headlines[$variable])) {
83            return static::$headlines[$variable];
84        }
85
86        return (string) $variable;
87    }
89    protected function writeRawReport(Exchange $report, Spreadsheet $spreadsheet): Spreadsheet
90    {
91        $sheet = $spreadsheet->getActiveSheet();
92        $reportData = [];
93        foreach ($report->dictionary as $item) {
94            $reportData['header'][] = $this->resolveDictionaryHeader($item);
95        }
96        foreach ($report->data as $row => $entry) {
97            foreach ($entry as $item) {
98                $reportData[$row][] = is_numeric($item) ? (string) $item : $item;
99            }
100        }
101        $sheet->fromArray($reportData, null, 'A' . ($sheet->getHighestRow()));
102
103        return $spreadsheet;
104    }
106    protected function writeFilteredExchangeReport(
107        Exchange $report,
108        Spreadsheet $spreadsheet,
109        array $skipVariables = [],
110        int $rowGap = 0
111    ): Spreadsheet {
112        $sheet = $spreadsheet->getActiveSheet();
113        $columnIndexes = [];
114        $reportData = [];
115
116        foreach ($report->dictionary as $index => $item) {
117            $variable = $item['variable'] ?? '';
118            if (in_array($variable, $skipVariables, true)) {
119                continue;
120            }
121            $columnIndexes[] = $index;
122            $reportData['header'][] = $this->resolveDictionaryHeader($item);
123        }
124
125        foreach ($report->data as $row => $entry) {
126            foreach ($columnIndexes as $index) {
127                $value = $entry[$index] ?? '';
128                $reportData[$row][] = is_numeric($value) ? (string) $value : $value;
129            }
130        }
131
132        $startRow = $sheet->getHighestRow() + $rowGap;
133        $sheet->fromArray($reportData, null, 'A' . $startRow);
134
135        return $spreadsheet;
136    }
138    protected function writeInfoHeader(array $args, Spreadsheet $spreadsheet): Spreadsheet|null
139    {
140        $sheet = $spreadsheet->getActiveSheet();
141        $infoData = [];
142        $infoData[] = static::$subjectTranslations[$args['category']];
143        if (isset($args['selectedScopes'])) {
144            try {
145                $scopesResult = \App::http()->readGetResult('/scope/')->getData();
146                $scopeMap = [];
147                foreach ($scopesResult ?? [] as $scope) {
148                    $scopeMap[$scope->id] = $scope;
149                }
150                foreach ($args['selectedScopes'] as $scopeId) {
151                    if (isset($scopeMap[$scopeId])) {
152                        $infoData[] = $scopeMap[$scopeId]->provider->name . " " . $scopeMap[$scopeId]->shortName;
153                    }
154                }
155            } catch (Exception $exception) {
156                return null;
157            }
158        } else {
159            if (isset($args['organisation'])) {
160                $infoData[] = $args['organisation']['name'] ;
161            }
162            if (isset($args['department'])) {
163                $infoData[] = $args['department']['name'];
164            }
165            if (isset($args['scope'])) {
166                $infoData[] = $args['scope']['contact']['name'] . ' ' . $args['scope']['shortName'];
167            }
168        }
169
170        $infoData = array_chunk($infoData, 1);
171        $sheet->fromArray($infoData, null, 'A' . $sheet->getHighestRow());
172
173        if (isset($args['reports'][0]->firstDay)) {
174            $firstDay = $args['reports'][0]->firstDay->toDateTime()->format('d.m.Y');
175            $lastDay = $args['reports'][0]->lastDay->toDateTime()->format('d.m.Y');
176            $range = array('Zeitraum:', $firstDay, 'bis', $lastDay);
177            $sheet->fromArray($range, null, 'A' . ($sheet->getHighestRow() + 1));
178        }
179
180        return $spreadsheet;
181    }
184    protected function setDateTime(string $dateString): \DateTime
185    {
186        $dateArr = explode('-', $dateString);
187        if (2 == count($dateArr)) {
188            $dateString = $dateString . '-01';
189        }
190        /* ignore because not in use now */
191        //@codeCoverageIgnoreStart
192        if (1 == count($dateArr)) {
193            $dateString = $dateString . '-01-01';
194        }
195        //@codeCoverageIgnoreEnd
196        return new \DateTime($dateString);
197    }
203    protected function getFormatedDates(\DateTimeInterface $date, string $pattern = 'MMMM'): string|false
204    {
205        $dateFormatter = new \IntlDateFormatter(
206            'de-DE',
207            \IntlDateFormatter::MEDIUM,
208            \IntlDateFormatter::MEDIUM,
209            \App::TIMEZONE,
210            \IntlDateFormatter::GREGORIAN,
211            $pattern
212        );
213
214        return $dateFormatter->format($date->getTimestamp());
215    }

Inherited from BO\Zmsstatistic\BaseController

20    public function __invoke(RequestInterface $request, ResponseInterface $response, array $args)
21    {
22        $request = $this->initRequest($request);
23        if ($this->withAccess) {
24            $this->initAccessRights($request);
25        }
26        $noCacheResponse = \BO\Slim\Render::withLastModified($response, time(), '0');
27        return $this->readResponse($request, $noCacheResponse, $args);
28    }

Inherited from BO\Zmsstatistic\Helper\Access

33    protected function initAccessRights(RequestInterface $request): void
34    {
35        $this->workstation = $this->readWorkstation();
36        if ($this->workstation && isset($this->workstation->scope['id']) && $this->workstation->scope['id'] > 0) {
37            $this->department = $this->readDepartment();
38            $this->organisation = $this->readOrganisation();
39            $this->owner = $this->readOwner();
40        }
41        $this->validateAccessRights($request);
42    }
44    protected function readWorkstation(): mixed
45    {
46        $workstation = \App::http()->readGetResult('/workstation/', ['resolveReferences' => $this->resolveLevel]);
47        return $workstation->getEntity();
48    }
50    protected function readDepartment(): mixed
51    {
52        if ($this->workstation->getUseraccount()->hasPermissions(['statistic'])) {
53            return \App::http()
54                ->readGetResult('/scope/' . $this->workstation->scope['id'] . '/department/')
55                ->getEntity();
56        }
57        return null;
58    }
60    protected function readOrganisation(): mixed
61    {
62        if ($this->workstation->getUseraccount()->isSuperUser()) {
63            return \App::http()
64                ->readGetResult('/department/' . $this->department->getId() . '/organisation/')
65                ->getEntity();
66        }
67        return null;
68    }
70    protected function readOwner(): mixed
71    {
72        if ($this->workstation->getUseraccount()->isSuperUser()) {
73            return \App::http()
74                ->readGetResult('/organisation/' . $this->organisation->getId() . '/owner/')
75                ->getEntity();
76        }
77        return null;
78    }
80    protected function validateAccessRights(RequestInterface $request): void
81    {
82        $path = $request->getUri()->getPath();
83        $this->validateAccess($path);
84        $this->validateScope($path);
85    }
87    protected function validateAccess(string $path): void
88    {
89        if (
90            (false !== strpos($path, 'owner') && ! $this->owner) ||
91            (false !== strpos($path, 'organisation') && ! $this->organisation) ||
92            (false !== strpos($path, 'department') && ! $this->department)
93        ) {
94            throw new UserAccountAccessRightsFailed();
95        }
96    }
98    protected function validateScope(string $path): void
99    {
100        if (
101            $this->isPathWithoutScope($path)
102            && (! isset($this->workstation['scope']) || ! isset($this->workstation['scope']['id']))
103        ) {
104            throw new WorkstationMissingScope();
105        }
106    }
108    protected function isPathWithoutScope(string $path): bool
109    {
110        // TODO: refactor to integrate these access rules in the controller to make them visible
111        return (false === strpos($path, 'select')
112            && false === strpos($path, 'warehouse')
113            && false === strpos($path, 'logout')
114            && false === strpos($path, 'report')
115        );
116    }
121    protected function testLogin(mixed $input): mixed
122    {
123        $userAccount = new Useraccount(array(
124            'id' => $input['loginName'],
125            'password' => $input['password'],
126            'departments' => array('id' => 0) // required in schema validation
127        ));
128        try {
129            /** @var Workstation $workstation */
130            $workstation = \App::http()->readPostResult('/workstation/login/', $userAccount)->getEntity();
131            return $workstation;
132        } catch (\BO\Zmsclient\Exception $exception) {
133            $template = TwigExceptionHandler::getExceptionTemplate($exception);
134            if ('BO\Zmsentities\Exception\SchemaValidation' == $exception->template) {
135                $exceptionData = [
136                  'template' => 'exception/bo/zmsbackend/useraccount/exception/invalidcredentials.twig'
137                ];
138                $exceptionData['data']['password']['messages'] = [
139                    'Der Nutzername oder das Passwort wurden falsch eingegeben'
140                ];
141            } elseif ('BO\Zmsbackend\Useraccount\Exception\UserAlreadyLoggedIn' == $exception->template) {
142                Auth::setKey($exception->data['authkey'], time() + \App::SESSION_DURATION);
143                throw $exception;
144            } elseif (
145                '' != $exception->template
146                && $this->exceptionTemplateExists($template)
147            ) {
148                $exceptionData = [
149                  'template' => $template,
150                  'data' => $exception->data
151                ];
152            } else {
153                throw $exception;
154            }
155        }
156        return $exceptionData;
157    }
159    protected function exceptionTemplateExists(string $template): bool
160    {
161        /** @var mixed $container */
162        $container = \App::$slim->getContainer();
163        return $container->get('view')->getLoader()->exists($template);
164    }