Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.32% covered (warning)
77.32%
624 / 807
72.22% covered (warning)
72.22%
52 / 72
CRAP
0.00% covered (danger)
0.00%
0 / 1
Process
77.32% covered (warning)
77.32%
624 / 807
72.22% covered (warning)
72.22%
52 / 72
706.66
0.00% covered (danger)
0.00%
0 / 1
 getQueryNewProcessId
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getLockProcessId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLockAssignedWorkstationId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addJoin
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 addJoinAvailability
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 addJoinScope
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 addConditionDate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addConditionDisplayNumber
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 calculateStatus
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
506
 getEntityMapping
100.00% covered (success)
100.00%
71 / 71
100.00% covered (success)
100.00%
1 / 1
3
 addCountValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 addConditionHasTelephone
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 addConditionProcessDeleteInterval
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 addConditionProcessDeleteBeforeDate
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 addConditionProcessExpiredIPTimeStamp
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 addConditionProcessReminderInterval
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 addConditionProcessMailReminder
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
1 / 1
1
 addConditionProcessId
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 addConditionProcessIdFollow
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 addConditionIgnoreSlots
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 addConditionScopeId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 addConditionScopeIds
45.45% covered (danger)
45.45%
5 / 11
0.00% covered (danger)
0.00%
0 / 1
4.46
 addConditionQueueNumber
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addConditionWorkstationId
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 addConditionTime
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addConditionTimeframe
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 addConditionAuthKey
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addConditionAssigned
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addConditionStatus
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addConditionIsReserved
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 addConditionSearch
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
4
 appendGeneralSearchTermGroup
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
5
 addConditionUpcomingOnly
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 addOrderByAppointmentDate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 addOrderBySearchRelevance
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
20
 parseSearchTerms
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
6.35
 appendNamePartLikeGroup
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
 isNumericSearchQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 escapeLikeValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 escapeLikePatternForSqlLiteral
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 escapeSqlStringLiteral
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addConditionName
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 addConditionMail
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
2.21
 addConditionCustomTextfield
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 addConditionCustomTextfield2
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 addConditionAmendment
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 addConditionRequestId
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 addConditionScopeNameSearch
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
1
 addConditionServiceNameSearch
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 addConditionDeallocate
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 addValuesNewProcess
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 getNewDisplayNumber
13.33% covered (danger)
13.33%
2 / 15
0.00% covered (danger)
0.00%
0 / 1
8.86
 checkIfDisplayNumberOnSameDateExists
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 addValuesUpdateProcess
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 addValuesIPAdress
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addValuesFollowingProcessData
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 addValuesAppointmentData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 addValuesScopeData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addValuesStatusData
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
13
 addValuesClientData
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
11
 addValueDisplayNumber
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addProcessingTimeData
40.00% covered (danger)
40.00%
20 / 50
0.00% covered (danger)
0.00%
0 / 1
87.98
 addValuesQueueData
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
10
 addValuesWaitingTimeData
84.62% covered (warning)
84.62%
33 / 39
0.00% covered (danger)
0.00%
0 / 1
19.18
 addValuesWayTimeData
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 addValuesWasMissed
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 addValuesPriority
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 addValuesExternalUserId
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 postProcess
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
7
 removeDuplicates
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addRequiredJoins
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 addConditionExternalUserId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace BO\Zmsbackend\Process\Repository;
4
5use BO\Zmsbackend\Scope\Service\Scope as ScopeEntity;
6use BO\Zmsentities\Process as ProcessEntity;
7use BO\Zmsdldb\Helper\DateTime;
8use DateTimeImmutable;
9
10/**
11 * @SuppressWarnings(Methods)
12 * @SuppressWarnings(Complexity)
13 */
14class Process extends \BO\Zmsbackend\Query\Base implements \BO\Zmsbackend\Query\MappingInterface
15{
16    /**
17     *     * @var String TABLE mysql table reference
18     */
19    const string TABLE = 'buerger';
20
21    const string QUERY_DEREFERENCED = "UPDATE `buerger` process LEFT JOIN `standort` s USING(StandortID)
22        SET
23            process.Anmerkung = ?,
24            process.custom_text_field = ?,
25            process.custom_text_field2 = ?,
26            process.StandortID = 0,
27            process.AbholortID = 0,
28            process.Abholer = 0,
29            process.Name = 'dereferenced',
30            process.IPadresse = '',
31            process.IPTimeStamp = 0,
32            process.NutzerID = 0,
33            process.vorlaeufigeBuchung = 0,
34            process.bestaetigt = 1,
35            process.absagecode = 'deref!0',
36            process.EMail = '',
37            process.NutzerID = 0,
38            process.priority = null,
39            process.status = 'blocked'
40        WHERE
41            (process.BuergerID = ? AND process.absagecode = ?)
42            OR process.istFolgeterminvon = ?
43        ";
44
45    const string QUERY_CANCELED = "
46        UPDATE `buerger` process LEFT JOIN `standort` s USING(StandortID)
47            SET
48                process.Anmerkung = CONCAT(
49                    'Abgesagter Termin gebucht am: ',
50                    FROM_UNIXTIME(process.IPTimeStamp,'%d.%m.%Y, %H:%i'),' Uhr | ',
51                    IFNULL(process.Anmerkung,'')
52                ),              
53                process.Name = '(abgesagt)',
54                process.IPadresse = '',
55                process.IPTimeStamp = :canceledTimestamp + (IFNULL(s.loeschdauer, 15) * 60),
56                process.NutzerID = 0,
57                process.vorlaeufigeBuchung = 1,
58                process.status = 'deleted',
59                process.absagecode = :newAuthKey
60            WHERE
61                (process.BuergerID = :processId AND process.absagecode = :authKey)
62                OR process.istFolgeterminvon = :processId
63        ";
64
65    const string QUERY_DELETE = "DELETE FROM `buerger`
66        WHERE
67            BuergerID = ?
68            OR istFolgeterminvon = ?
69        ";
70
71    const string QUERY_REASSIGN_PROCESS_CREDENTIALS = "UPDATE `buerger` process
72       SET 
73            process.BuergerID = :newProcessId, 
74            process.absagecode = :newAuthKey
75        WHERE BuergerID = :processId
76    ";
77
78    const string QUERY_REASSIGN_PROCESS_REQUESTS = "UPDATE `buergeranliegen` requests
79        SET 
80            requests.BuergerID = :newProcessId
81        WHERE BuergerID = :processId
82    ";
83
84    const string QUERY_REASSIGN_FOLLWING_PROCESS = "UPDATE `buerger` process
85        SET process.istFolgeterminvon = :newProcessId
86        WHERE istFolgeterminvon = :processId
87    ";
88
89    const string QUERY_UPDATE_FOLLOWING_PROCESS = "UPDATE buerger 
90        SET vorlaeufigeBuchung = :reserved 
91        WHERE istFolgeterminvon = :processID
92        ";
93
94    public function getQueryNewProcessId(): string
95    {
96        $random = rand(20, 999);
97        return 'SELECT pseq.processId AS `nextid`
98            FROM process_sequence pseq
99            WHERE pseq.processId = (
100                SELECT ps.processID FROM `process_sequence` ps LEFT JOIN `' . self::getTablename() . '` p
101                    ON ps.processId = p.BuergerID
102                WHERE p.`BuergerID` IS NULL
103                LIMIT ' . $random . ',1)
104            FOR UPDATE';
105    }
106
107    public function getLockProcessId(): string
108    {
109        return 'SELECT p.`BuergerID` FROM `' . self::getTablename() . '` p WHERE p.`BuergerID` = :processId FOR UPDATE';
110    }
111
112    public function getLockAssignedWorkstationId(): string
113    {
114        return 'SELECT p.`NutzerID`
115            FROM `' . self::getTablename() . '` p
116            WHERE p.`BuergerID` = :processId
117            FOR UPDATE';
118    }
119
120    #[\Override]
121    public function addJoin()
122    {
123        $joins = [];
124
125        if ($this->shouldLoadEntity('availability')) {
126            $joins[] = $this->addJoinAvailability();
127        }
128
129        if ($this->shouldLoadEntity('scope')) {
130            $joins[] = $this->addJoinScope();
131        }
132
133        return $joins;
134    }
135
136    /**
137     * Add Availability to the dataset
138     */
139    protected function addJoinAvailability(): \BO\Zmsbackend\Availability\Repository\Availability
140    {
141        $this->leftJoin(
142            new \BO\Zmsbackend\Query\Alias(\BO\Zmsbackend\Availability\Repository\Availability::TABLE, 'availability'),
143            \BO\Zmsbackend\Availability\Repository\Availability::getJoinExpression('`process`', '`availability`')
144        );
145        $joinQuery = new \BO\Zmsbackend\Availability\Repository\Availability($this, $this->getPrefixed('appointments__0__availability__'));
146        return $joinQuery;
147    }
148
149    /**
150     * Add Scope to the dataset
151     */
152    protected function addJoinScope(): \BO\Zmsbackend\Query\Scope
153    {
154        $this->leftJoin(
155            new \BO\Zmsbackend\Query\Alias(\BO\Zmsbackend\Query\Scope::TABLE, 'scope'),
156            self::expression(
157                'IF(`process`.`AbholortID`,
158                    `process`.`AbholortID`,
159                    `process`.`StandortID`
160                )'
161            ),
162            '=',
163            'scope.StandortID'
164        );
165        $joinQuery = new \BO\Zmsbackend\Query\Scope($this, $this->getPrefixed('scope__'));
166        return $joinQuery;
167    }
168
169    public function addConditionDate($date): static
170    {
171        $this->query->where('process.Datum', '=', $date);
172        return $this;
173    }
174
175    /**
176     * @psalm-api
177     */
178    public function addConditionDisplayNumber($displayNumber): static
179    {
180        $this->query->where('process.displayNumber', '=', $displayNumber);
181        return $this;
182    }
183
184    /**
185     * @psalm-api
186     *
187     * @return null|string
188     */
189    protected function calculateStatus(): string|null
190    {
191        if ($this->query->value('Name') === '(abgesagt)') {
192            return 'deleted';
193        }
194
195        if (
196            $this->query->value('StandortID') == 0
197            && $this->query->value('AbholortID') == 0
198        ) {
199            return 'blocked';
200        }
201
202        if (
203            $this->query->value('vorlaeufigeBuchung') == 1
204            && $this->query->value('bestaetigt') == 0
205        ) {
206            return 'reserved';
207        }
208
209        if ($this->query->value('nicht_erschienen') != 0) {
210            return 'missed';
211        }
212
213        if ($this->query->value('parked') != 0) {
214            return 'parked';
215        }
216
217        if (
218            $this->query->value('Abholer') != 0
219            && $this->query->value('AbholortID') != 0
220            && $this->query->value('NutzerID') == 0
221        ) {
222            return 'pending';
223        }
224
225        if (
226            $this->query->value('AbholortID') == 0
227            && $this->query->value('aufruferfolgreich') != 0
228            && $this->query->value('NutzerID') != 0
229        ) {
230            return 'processing';
231        }
232
233        if (
234            $this->query->value('aufrufzeit') != "00:00:00"
235            && $this->query->value('NutzerID') != 0
236            && $this->query->value('AbholortID') == 0
237        ) {
238            return 'called';
239        }
240
241        if ($this->query->value('Uhrzeit') == "00:00:00") {
242            return 'queued';
243        }
244
245        if (
246            $this->query->value('vorlaeufigeBuchung') == 0
247            && $this->query->value('bestaetigt') == 0
248        ) {
249            return 'preconfirmed';
250        }
251
252        if (
253            $this->query->value('vorlaeufigeBuchung') == 0
254            && $this->query->value('bestaetigt') == 1
255        ) {
256            return 'confirmed';
257        }
258
259        return null;
260    }
261
262    /**
263     * @return (\BO\Zmsbackend\Query\Builder\Expression|string)[]
264     *
265     */
266    #[\Override]
267    public function getEntityMapping()
268    {
269        $status_expression = self::expression(
270            'CASE
271                WHEN process.status = "called" AND process.aufrufzeit != "00:00:00" AND process.NutzerID != 0 AND process.AbholortID = 0
272                    THEN "called"
273                WHEN process.status = "called" AND process.Uhrzeit = "00:00:00"
274                    THEN "queued"
275                WHEN process.status = "called" AND process.vorlaeufigeBuchung = 0 AND process.bestaetigt = 1
276                    THEN "confirmed"
277                ELSE process.status
278            END'
279        );
280
281        return array_filter([
282            'amendment' => 'process.Anmerkung',
283            'id' => 'process.BuergerID',
284            'appointments__0__date' => self::expression(
285                'CONCAT(`process`.`Datum`, " ", `process`.`Uhrzeit`)'
286            ),
287            'scope__id' => self::expression(
288                'IF(`process`.`AbholortID`,
289                    `process`.`AbholortID`,
290                    `process`.`StandortID`
291)'
292            ),
293            'appointments__0__scope__id' => 'process.StandortID',
294            // 'appointments__0__slotCount' => 'process.hatFolgetermine',
295            'appointments__0__slotCount' => self::expression('process.hatFolgetermine + 1'),
296            'authKey' => 'process.absagecode',
297            'clients__0__email' => 'process.EMail',
298            'clients__0__emailSendCount' => 'process.EMailverschickt',
299            'clients__0__familyName' => 'process.Name',
300            'clients__0__surveyAccepted' => 'process.zustimmung_kundenbefragung',
301            'clients__0__telephone' => self::expression(
302                'IF(`process`.`telefonnummer_fuer_rueckfragen`!="",
303                    `process`.`telefonnummer_fuer_rueckfragen`,
304                    `process`.`Telefonnummer`
305                )'
306            ),
307            'customTextfield' => 'process.custom_text_field',
308            'customTextfield2' => 'process.custom_text_field2',
309            'createIP' => 'process.IPAdresse',
310            'priority' => 'process.priority',
311            'createTimestamp' => 'process.IPTimeStamp',
312            'lastChange' => 'process.updateTimestamp',
313            'showUpTime' => 'process.showUpTime',
314            'processingTime' => 'process.processing_time',
315            'timeoutTime' => 'process.timeoutTime',
316            'finishTime' => 'process.finishTime',
317            'status' => $status_expression,
318            'queue__status' => $status_expression,
319            'queue__arrivalTime' => self::expression(
320                'CONCAT(
321                    `process`.`Datum`,
322                    " ",
323                    IF(`process`.`wsm_aufnahmezeit`, `process`.`wsm_aufnahmezeit`, `process`.`Uhrzeit`)
324                )'
325            ),
326            'queue__callCount' => 'process.AnzahlAufrufe',
327            'queue__callTime' => 'process.aufrufzeit',
328            'queue__lastCallTime' => 'process.Timestamp',
329            'displayNumber' => self::expression(
330                'COALESCE(
331                    `process`.`displayNumber`,
332                    IF(`process`.`wartenummer`,
333                        `process`.`wartenummer`,
334                        `process`.`BuergerID`
335                    )
336                )'
337            ),
338            'queue__number' => self::expression(
339                'IF(`process`.`wartenummer`,
340                    `process`.`wartenummer`,
341                    `process`.`BuergerID`
342                )'
343            ),
344            'queue__destination' => $this->shouldLoadEntity('processuser')
345                ? 'processuser.Arbeitsplatznr'
346                : '',
347            'queue__destinationHint' => $this->shouldLoadEntity('processuser')
348                ? 'processuser.aufrufzusatz'
349                : '',
350            'queue__waitingTime' => 'process.waiting_time',
351            'queue__wayTime' => 'process.way_time',
352            'queue__withAppointment' => self::expression(
353                'IF(`process`.`wartenummer`,
354                    "0",
355                    "1"
356                )'
357            ),
358            'reminderTimestamp' => 'process.Erinnerungszeitpunkt',
359            '__clientsCount' => 'process.AnzahlPersonen',
360            'wasMissed' => 'process.wasMissed',
361            'externalUserId' => 'process.external_user_id',
362            'isTicketprinter' => 'process.is_ticketprinter',
363            'parkedBy' => 'process.parkedBy',
364        ], 'strlen');
365    }
366
367    public function addCountValue(): static
368    {
369        $this->query->select([
370            'processCount' => self::expression('COUNT(*)'),
371        ]);
372        return $this;
373    }
374
375    /**
376     * @psalm-api
377     */
378    public function addConditionHasTelephone(): static
379    {
380        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $condition) {
381            $condition
382                ->andWith('process.telefonnummer_fuer_rueckfragen', '!=', '')
383                ->orWith('process.Telefonnummer', '!=', '');
384        });
385        return $this;
386    }
387
388    public function addConditionProcessDeleteInterval(\DateTimeInterface $expirationDate): static
389    {
390        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($expirationDate) {
391            $query->andWith(
392                self::expression(
393                    'CONCAT(`process`.`Datum`, " ", `process`.`Uhrzeit`)'
394                ),
395                '<=',
396                $expirationDate->format('Y-m-d H:i:s')
397            );
398        });
399        $this->query->orderBy('appointments__0__date', 'ASC');
400        return $this;
401    }
402
403    public function addConditionProcessDeleteBeforeDate(\DateTimeInterface $date): static
404    {
405        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($date) {
406            $query->andWith(
407                self::expression(
408                    'CONCAT(`process`.`Datum`, " ", `process`.`Uhrzeit`)'
409                ),
410                '<',
411                $date->format('Y-m-d') . ' 00:00:00'
412            );
413        });
414        $this->query->orderBy('appointments__0__date', 'ASC');
415        return $this;
416    }
417
418    /**
419     * @psalm-api
420     */
421    public function addConditionProcessExpiredIPTimeStamp(\DateTimeInterface $expirationDate): static
422    {
423        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($expirationDate) {
424            $query->andWith('process.IPTimeStamp', '<=', $expirationDate->getTimestamp());
425        });
426        $this->query->orderBy('appointments__0__date', 'ASC');
427        return $this;
428    }
429
430    /**
431     * @psalm-api
432     */
433    public function addConditionProcessReminderInterval(\DateTimeInterface $dateTime): static
434    {
435        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($dateTime) {
436            $query
437                ->andWith('process.Erinnerungszeitpunkt', '<=', $dateTime->getTimestamp())
438                ->andWith('process.Erinnerungszeitpunkt', '>=', $dateTime->modify("-5 Minutes")->getTimestamp());
439        });
440        $this->query->orderBy('reminderTimestamp', 'ASC');
441        return $this;
442    }
443
444    public function addConditionProcessMailReminder(
445        \DateTimeInterface $now,
446        \DateTimeInterface $lastRun,
447        $defaultReminderInMinutes
448    ): static {
449        $this->query
450            ->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($now, $lastRun, $defaultReminderInMinutes) {
451                $query
452                    ->andWith(
453                        self::expression(
454                            'CONCAT(`process`.`Datum`, " ", `process`.`Uhrzeit`)'
455                        ),
456                        '>',
457                        $lastRun->format('Y-m-d H:i:s')
458                    )
459                    ->andWith(
460                        self::expression(
461                            'CONCAT(`process`.`Datum`, " ", `process`.`Uhrzeit`)'
462                        ),
463                        '>',
464                        $now->format('Y-m-d H:i:s')
465                    )
466                    ->andWith(
467                        'scopemail.send_reminder',
468                        '=',
469                        1
470                    )
471                    ->andWith(
472                        'process.EMail',
473                        '<>',
474                        ""
475                    )
476                    ->andWith(
477                        'process.EMailverschickt',
478                        '=',
479                        0
480                    )
481                    ->andWith(
482                        self::expression(
483                            'DATE_SUB(CONCAT(`process`.`Datum`, " ", `process`.`Uhrzeit`), INTERVAL '
484                                . 'IFNULL(scopemail.send_reminder_minutes_before, ' . $defaultReminderInMinutes
485                                . ') MINUTE)'
486                        ),
487                        '<=',
488                        $now->format('Y-m-d H:i:s')
489                    );
490            });
491        $this->query->orderBy('appointments__0__date', 'ASC');
492        return $this;
493    }
494
495    public function addConditionProcessId(int $processId): static
496    {
497        $this->query->where('process.BuergerID', '=', $processId);
498        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $condition) {
499            $condition
500                ->andWith('process.istFolgeterminvon', 'IS', null)
501                ->orWith('process.istFolgeterminvon', '=', 0);
502        });
503        return $this;
504    }
505
506    public function addConditionProcessIdFollow($processId): static
507    {
508        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $condition) use ($processId) {
509            $condition
510                ->andWith('process.BuergerID', '=', $processId)
511                ->orWith('process.istFolgeterminvon', '=', $processId);
512        });
513        return $this;
514    }
515
516    /**
517     * @psalm-api
518     */
519    public function addConditionIgnoreSlots(): static
520    {
521        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $condition) {
522            $condition
523                ->andWith('process.istFolgeterminvon', 'IS', null)
524                ->orWith('process.istFolgeterminvon', '=', 0);
525        });
526        return $this;
527    }
528
529    public function addConditionScopeId(int $scopeId): static
530    {
531        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($scopeId) {
532            $query
533                ->andWith('process.StandortID', '=', $scopeId)
534                ->orWith('process.AbholortID', '=', $scopeId);
535        });
536        return $this;
537    }
538
539    public function addConditionScopeIds(array|int $scopeIds)
540    {
541        if (count($scopeIds) === 0) {
542            $this->query->where(self::expression('1'), '=', 0);
543            return $this;
544        }
545
546        if (count($scopeIds) === 1) {
547            return $this->addConditionScopeId($scopeIds[0]);
548        }
549
550        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($scopeIds) {
551            $query
552                ->andWith('process.StandortID', 'IN', $scopeIds)
553                ->orWith('process.AbholortID', 'IN', $scopeIds);
554        });
555
556        return $this;
557    }
558
559    /**
560     * @psalm-api
561     */
562    public function addConditionQueueNumber($queueNumber, $queueLimit = 10000): static
563    {
564        ($queueLimit > $queueNumber)
565            ? $this->query->where('process.wartenummer', '=', $queueNumber)
566            : $this->query->where('process.BuergerID', '=', $queueNumber);
567        return $this;
568    }
569
570    public function addConditionWorkstationId($workstationId): static
571    {
572        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($workstationId) {
573            $query->andWith('process.NutzerID', '=', $workstationId);
574            $query->andWith('process.StandortID', '>', 0);
575        });
576        return $this;
577    }
578
579    public function addConditionTime(\DateTimeInterface|null $dateTime): static
580    {
581        $this->query->where('process.Datum', '=', $dateTime->format('Y-m-d'));
582        return $this;
583    }
584
585    /**
586     * Identify processes between two dates
587     *
588     * @psalm-api
589     */
590    public function addConditionTimeframe(\DateTimeInterface $startDate, \DateTimeInterface $endDate): static
591    {
592        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $condition) use ($startDate, $endDate) {
593            $condition
594                ->andWith('process.Datum', '<=', $endDate->format('Y-m-d'))
595                ->andWith('process.Datum', '>=', $startDate->format('Y-m-d'));
596        });
597        return $this;
598    }
599
600    public function addConditionAuthKey($authKey): static
601    {
602        $authKey = urldecode($authKey);
603        $this->query->where('process.absagecode', '=', $authKey);
604        return $this;
605    }
606
607    /**
608     * @return static
609     */
610    public function addConditionAssigned()
611    {
612        $this->query->where('process.StandortID', '!=', "0");
613        return $this;
614    }
615
616    public function addConditionStatus(string $status): static
617    {
618        $this->query->where('process.status', '=', $status);
619        return $this;
620    }
621
622    /**
623     * @psalm-api
624     */
625    public function addConditionIsReserved(): static
626    {
627        $this->query->where('process.name', 'NOT IN', array(
628            'dereferenced',
629            '(abgesagt)'
630        ))
631            ->where('process.vorlaeufigeBuchung', '=', 1)
632            ->where('process.StandortID', '>', 0);
633        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $condition) {
634            $condition
635                ->andWith('process.istFolgeterminvon', 'IS', null)
636                ->orWith('process.istFolgeterminvon', '=', 0);
637        });
638        return $this;
639    }
640
641    public function addConditionSearch($queryString, bool $orWhere = false): static
642    {
643        $queryString = trim((string) $queryString);
644        $terms = $this->parseSearchTerms($queryString);
645
646        if ($terms === []) {
647            return $this;
648        }
649
650        $condition = function (
651            \BO\Zmsbackend\Query\Builder\ConditionBuilder $query
652        ) use ($terms) {
653            foreach ($terms as $term) {
654                $query->andWith(
655                    function (
656                        \BO\Zmsbackend\Query\Builder\ConditionBuilder $inner
657                    ) use ($term) {
658                        $this->appendGeneralSearchTermGroup(
659                            $inner,
660                            $term['value'],
661                            $term['quoted']
662                        );
663                    }
664                );
665            }
666        };
667
668        if ($orWhere) {
669            $this->query->orWhere($condition);
670        } else {
671            $this->query->where($condition);
672        }
673
674        return $this;
675    }
676
677    private function appendGeneralSearchTermGroup(\BO\Zmsbackend\Query\Builder\ConditionBuilder $query, string $term, bool $quoted = false): void
678    {
679        $likeContains = '%' . $this->escapeLikeValue($term) . '%';
680
681        $useNameWordBoundary = !$quoted
682            && !$this->isNumericSearchQuery($term) && mb_strlen($term) <= 3;
683
684        if ($quoted) {
685            $this->appendNamePartLikeGroup(
686                $query,
687                $term,
688                true
689            );
690        } elseif ($useNameWordBoundary) {
691            $this->appendNamePartLikeGroup($query, $term, true, true);
692        } else {
693            $query->orWith('process.Name', 'LIKE', $likeContains);
694        }
695
696        $query->orWith('process.EMail', 'LIKE', $likeContains);
697        $query->orWith('process.Telefonnummer', 'LIKE', $likeContains);
698        $query->orWith('process.telefonnummer_fuer_rueckfragen', 'LIKE', $likeContains);
699        $query->orWith('process.displayNumber', 'LIKE', $likeContains);
700        $query->orWith('process.custom_text_field', 'LIKE', $likeContains);
701        $query->orWith('process.custom_text_field2', 'LIKE', $likeContains);
702    }
703
704    public function addConditionUpcomingOnly(\DateTimeInterface $now): static
705    {
706        $today = $now->format('Y-m-d');
707        $time = $now->format('H:i:s');
708        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $condition) use ($today, $time) {
709            $condition
710                ->andWith('process.Datum', '>', $today)
711                ->orWith(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $inner) use ($today, $time) {
712                    $inner
713                        ->andWith('process.Datum', '=', $today)
714                        ->andWith('process.Uhrzeit', '>=', $time);
715                });
716        });
717        return $this;
718    }
719
720    /** @psalm-api */
721    public function addOrderByAppointmentDate(): static
722    {
723        $this->query->orderBy('process.Datum', 'ASC');
724        $this->query->orderBy('process.Uhrzeit', 'ASC');
725        $this->query->orderBy('process.BuergerID', 'ASC');
726        return $this;
727    }
728
729    /** @psalm-api */
730    public function addOrderBySearchRelevance(string $queryString): static
731    {
732        $queryString = trim($queryString);
733        if ($queryString === '') {
734            return $this;
735        }
736
737        $terms = $this->parseSearchTerms($queryString);
738        if ($terms === []) {
739            return $this;
740        }
741        $fullQuery = $this->escapeLikePatternForSqlLiteral($queryString);
742        $fullQueryExact = $this->escapeSqlStringLiteral($queryString);
743
744        $allTermsAsWordsConditions = [];
745        $allTermsInNameConditions = [];
746        $anyTermInNameConditions = [];
747
748        foreach ($terms as $term) {
749            $escapedTerm = $this->escapeLikePatternForSqlLiteral($term['value']);
750            $exactTerm = $this->escapeSqlStringLiteral($term['value']);
751            $allTermsAsWordsConditions[] = "("
752                . "process.Name = '{$exactTerm}'"
753                . " OR process.Name LIKE '{$escapedTerm} %'"
754                . " OR process.Name LIKE '% {$escapedTerm}'"
755                . " OR process.Name LIKE '% {$escapedTerm} %'"
756                . ")";
757            $allTermsInNameConditions[] = "process.Name LIKE '%{$escapedTerm}%'";
758            $anyTermInNameConditions[] = "process.Name LIKE '%{$escapedTerm}%'";
759        }
760        $allTermsAsWords = implode(' AND ', $allTermsAsWordsConditions);
761        $allTermsInName = implode(' AND ', $allTermsInNameConditions);
762        $anyTermInName = implode(' OR ', $anyTermInNameConditions);
763
764        $this->query->orderBy(self::expression(
765            "CASE
766                   WHEN process.Name LIKE '{$fullQueryExact}' THEN 0
767                   WHEN process.Name LIKE '{$fullQuery} %' THEN 1
768                   WHEN {$allTermsAsWords} THEN 2
769                   WHEN {$allTermsInName} THEN 3
770                   WHEN {$anyTermInName} THEN 4
771                   ELSE 5
772                END"
773        ));
774
775        $this->query->orderBy('process.Datum', 'ASC');
776        $this->query->orderBy('process.Uhrzeit', 'ASC');
777        $this->query->orderBy('process.BuergerID', 'ASC');
778
779        return $this;
780    }
781
782    /**
783     * @return array<int, array{value: string, quoted: bool}>
784     */
785    private function parseSearchTerms(string $queryString): array
786    {
787        if ($queryString === '') {
788            return [];
789        }
790
791        if (!preg_match_all('/"([^"]+)"|(\S+)/u', $queryString, $matches, PREG_SET_ORDER)) {
792            return [['value' => $queryString, 'quoted' => false]];
793        }
794
795        $terms = [];
796        foreach ($matches as $match) {
797            $value = trim($match[1] !== '' ? $match[1] : $match[2]);
798            if ($value === '') {
799                continue;
800            }
801            $terms[] = [
802                'value' => $value,
803                'quoted' => $match[1] !== '',
804            ];
805        }
806
807        return $terms;
808    }
809
810    private function appendNamePartLikeGroup(
811        \BO\Zmsbackend\Query\Builder\ConditionBuilder $query,
812        string $term,
813        bool $wordBoundaryOnly = false,
814        bool $includeWordPrefix = false
815    ): void {
816        $escaped = $this->escapeLikeValue($term);
817        $query->orWith('process.Name', 'LIKE', $escaped . ' %');
818        $query->orWith('process.Name', 'LIKE', '% ' . $escaped);
819        $query->orWith('process.Name', 'LIKE', '% ' . $escaped . ' %');
820        $query->orWith('process.Name', '=', $term);
821        if ($wordBoundaryOnly) {
822            if ($includeWordPrefix) {
823                $query->orWith('process.Name', 'LIKE', $escaped . '%');
824                $query->orWith('process.Name', 'LIKE', '% ' . $escaped . '%');
825            }
826        } else {
827            $query->orWith('process.Name', 'LIKE', '%' . $escaped . '%');
828        }
829    }
830
831    private function isNumericSearchQuery(string $queryString): bool
832    {
833        return (bool) preg_match('#^\d+$#', $queryString);
834    }
835
836    private function escapeLikeValue(string $value): string
837    {
838        return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value);
839    }
840
841    private function escapeLikePatternForSqlLiteral(string $value): string
842    {
843        return $this->escapeSqlStringLiteral($this->escapeLikeValue($value));
844    }
845
846    private function escapeSqlStringLiteral(string $value): string
847    {
848        return str_replace("'", "''", $value);
849    }
850
851    public function addConditionName($name, $exactMatching = false): static
852    {
853        if ($exactMatching) {
854            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($name) {
855                $query->andWith('process.Name', '=', $name);
856            });
857        } else {
858            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($name) {
859                $query->andWith('process.Name', 'LIKE', "%$name%");
860            });
861        }
862        return $this;
863    }
864
865    public function addConditionMail(string $mailAddress, bool $exactMatching = false): static
866    {
867        if ($exactMatching) {
868            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($mailAddress) {
869                $query->andWith('process.Email', '=', $mailAddress);
870            });
871        } else {
872            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($mailAddress) {
873                $query->andWith('process.Email', 'LIKE', "%$mailAddress%");
874            });
875        }
876        return $this;
877    }
878
879    /**
880     * @psalm-api
881     */
882    public function addConditionCustomTextfield($customText, $exactMatching = false): static
883    {
884        if ($exactMatching) {
885            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($customText) {
886                $query->andWith('process.custom_text_field', '=', $customText);
887            });
888        } else {
889            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($customText) {
890                $query->andWith('process.custom_text_field', 'LIKE', "%$customText%");
891            });
892        }
893        return $this;
894    }
895
896    /**
897     * @psalm-api
898     */
899    public function addConditionCustomTextfield2($customText2, $exactMatching = false): static
900    {
901        if ($exactMatching) {
902            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($customText2) {
903                $query->andWith('process.custom_text_field2', '=', $customText2);
904            });
905        } else {
906            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($customText2) {
907                $query->andWith('process.custom_text_field2', 'LIKE', "%$customText2%");
908            });
909        }
910        return $this;
911    }
912
913    public function addConditionAmendment($amendment): static
914    {
915        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($amendment) {
916            $query->andWith('process.Anmerkung', 'LIKE', "%$amendment%");
917        });
918        return $this;
919    }
920
921    /**
922     * Add Requests Join
923     */
924    public function addConditionRequestId($requestId): static
925    {
926        $this->leftJoin(
927            new \BO\Zmsbackend\Query\Alias("buergeranliegen", 'buergeranliegen'),
928            'buergeranliegen.BuergerID',
929            '=',
930            'process.BuergerID'
931        );
932        $this->query->where('buergeranliegen.AnliegenID', '=', $requestId);
933        return $this;
934    }
935
936    public function addConditionScopeNameSearch(string $scopeName): static
937    {
938        $likeValue = '%' . $this->escapeLikeValue($scopeName) . '%';
939        $this->leftJoin(
940            new \BO\Zmsbackend\Query\Alias(\BO\Zmsbackend\Query\Scope::TABLE, 'search_scope'),
941            self::expression(
942                'IF(`process`.`AbholortID`, `process`.`AbholortID`, `process`.`StandortID`)'
943            ),
944            '=',
945            'search_scope.StandortID'
946        );
947        $this->leftJoin(
948            new \BO\Zmsbackend\Query\Alias(\BO\Zmsbackend\Provider\Repository\Provider::TABLE, 'search_scope_provider'),
949            self::expression(
950                'search_scope.InfoDienstleisterID = search_scope_provider.id
951                AND search_scope.source = search_scope_provider.source'
952            )
953        );
954        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($likeValue) {
955            $query->orWith('search_scope.Bezeichnung', 'LIKE', $likeValue);
956            $query->orWith('search_scope.standortinfozeile', 'LIKE', $likeValue);
957            $query->orWith('search_scope.standortkuerzel', 'LIKE', $likeValue);
958            $query->orWith('search_scope_provider.name', 'LIKE', $likeValue);
959        });
960
961        return $this;
962    }
963
964    public function addConditionServiceNameSearch(string $serviceName): static
965    {
966        $likeValue = '%' . $this->escapeLikeValue($serviceName) . '%';
967        $this->leftJoin(
968            new \BO\Zmsbackend\Query\Alias('buergeranliegen', 'search_request_link'),
969            'search_request_link.BuergerID',
970            '=',
971            'process.BuergerID'
972        );
973        $this->leftJoin(
974            new \BO\Zmsbackend\Query\Alias(\BO\Zmsbackend\Request\Repository\Request::TABLE, 'search_request'),
975            self::expression(
976                'search_request_link.AnliegenID = search_request.id
977                AND search_request_link.source = search_request.source'
978            )
979        );
980        $this->query->where('search_request.name', 'LIKE', $likeValue);
981
982        return $this;
983    }
984
985    /**
986     * add condition to get process if deallocation time < now
987     */
988    public function addConditionDeallocate(\DateTimeInterface $now): static
989    {
990        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($now) {
991            $query
992                ->andWith('process.Name', '=', '(abgesagt)')
993                ->andWith('process.IPTimeStamp', '<', $now->getTimestamp());
994        });
995        $this->query->orderBy('process.IPTimeStamp', 'ASC');
996        return $this;
997    }
998
999    public function addValuesNewProcess(ProcessEntity $process, $parentProcess = 0, $childProcessCount = 0): void
1000    {
1001        $values = [
1002            'BuergerID' => $process->id,
1003            'IPTimeStamp' => $process->createTimestamp,
1004            'absagecode' => $process->authKey,
1005            'hatFolgetermine' => $childProcessCount,
1006            'istFolgeterminvon' => $parentProcess,
1007            'displayNumber' => $parentProcess === 0 && empty($process->queue['number'])
1008                ? $this->getNewDisplayNumber($process)
1009                : null,
1010            'wartenummer' => $process->queue['number']
1011        ];
1012        if ($process->toProperty()->apiclient->apiClientID->isAvailable()) {
1013            $values['apiClientID'] = $process->apiclient->apiClientID;
1014        }
1015        if (isset($process->isTicketprinter) && $process->isTicketprinter) {
1016            $values['is_ticketprinter'] = 1;
1017        }
1018        $this->addValues($values);
1019    }
1020
1021    public function getNewDisplayNumber(ProcessEntity $process)
1022    {
1023        if (empty($process->scope->getPreference('queue', 'displayNumberPrefix'))) {
1024            return $process->id;
1025        }
1026
1027        $newDisplayNumber = $process->scope->getPreference('queue', 'displayNumberPrefix') . str_pad(
1028            (new ScopeEntity())->readDisplayNumberUpdated($process->scope->id),
1029            4,
1030            '0',
1031            STR_PAD_LEFT
1032        );
1033
1034        if (
1035            $this->checkIfDisplayNumberOnSameDateExists(
1036                $process->scope->id,
1037                $newDisplayNumber,
1038                \DateTime::createFromFormat('U', (int) $process->getFirstAppointment()->date)
1039            )
1040        ) {
1041            return $this->getNewDisplayNumber($process);
1042        }
1043
1044        return $newDisplayNumber;
1045    }
1046
1047    public function checkIfDisplayNumberOnSameDateExists($scopeId, string $displayNumber, \DateTime|false $date): bool
1048    {
1049        $processWithDisplayNumber = (new \BO\Zmsbackend\Process\Service\Process())->readProcessWithSameDayAndDisplayNumber(
1050            $scopeId,
1051            $displayNumber,
1052            $date->format('Y-m-d')
1053        );
1054
1055        return !empty($processWithDisplayNumber->getId());
1056    }
1057
1058    public function addValuesUpdateProcess(
1059        ProcessEntity $process,
1060        \DateTimeInterface $dateTime,
1061        int $parentProcess = 0,
1062        $previousStatus = null
1063    ): void {
1064        $this->addValuesIPAdress($process);
1065        if (0 === $parentProcess) {
1066            $this->addValuesClientData($process);
1067            $this->addProcessingTimeData($process, $dateTime, $previousStatus);
1068            $this->addValuesQueueData($process);
1069            $this->addValuesWaitingTimeData($process, $previousStatus);
1070            $this->addValuesWayTimeData($process);
1071        }
1072        if ($process->isWithAppointment()) {
1073            $this->addValuesFollowingProcessData($process, $parentProcess);
1074        }
1075        $this->addValuesWasMissed($process);
1076        $this->addValuesPriority($process);
1077        $this->addValuesStatusData($process, $dateTime);
1078        $this->addValuesExternalUserId($process);
1079    }
1080
1081    public function addValuesIPAdress(ProcessEntity $process): void
1082    {
1083        $data = array();
1084        $data['IPAdresse'] = $process->createIP;
1085        $this->addValues($data);
1086    }
1087
1088    public function addValuesFollowingProcessData(ProcessEntity $process, $parentProcess): void
1089    {
1090        $data = array();
1091        if (0 === $parentProcess) {
1092            $data['hatFolgetermine'] = (1 <= $process->getFirstAppointment()->getSlotCount()) ?
1093                $process->getFirstAppointment()->getSlotCount() - 1 :
1094                0;
1095        } else {
1096            $data['Name'] = '(Folgetermin)';
1097        }
1098        $this->addValues($data);
1099    }
1100
1101    public function addValuesAppointmentData(
1102        ProcessEntity $process
1103    ): void {
1104        $data = array();
1105        $appointment = $process->getFirstAppointment();
1106        $datetime = $appointment->toDateTime();
1107        $data['Datum'] = $datetime->format('Y-m-d');
1108        $data['Uhrzeit'] = $datetime->format('H:i:s');
1109        $this->addValues($data);
1110    }
1111
1112    public function addValuesScopeData(
1113        ProcessEntity $process
1114    ): void {
1115        $data = array();
1116        $data['StandortID'] = $process->getScopeId();
1117        $this->addValues($data);
1118    }
1119
1120    public function addValuesStatusData(ProcessEntity $process, \DateTimeInterface $dateTime): void
1121    {
1122        $data = array();
1123        $data['vorlaeufigeBuchung'] = ($process->status == 'reserved') ? 1 : 0;
1124        $data['aufruferfolgreich'] = ($process->status == 'processing') ? 1 : 0;
1125        if ($process->status == 'called') {
1126            $data['parked'] = 0;
1127            $data['nicht_erschienen'] = 0;
1128        }
1129        if ($process->status == 'pending') {
1130            $data['AbholortID'] = $process->scope->id;
1131            $data['Abholer'] = 1;
1132            $data['nicht_erschienen'] = 0;
1133            $data['parked'] = 0;
1134        }
1135        if ($process->status == 'queued') {
1136            $data['nicht_erschienen'] = 0;
1137            $data['parked'] = 0;
1138            if (
1139                $process->hasArrivalTime() &&
1140                (isset($process->queue->withAppointment) && $process->queue->withAppointment)
1141            ) {
1142                $data['wsm_aufnahmezeit'] = $dateTime->format('H:i:s');
1143            }
1144        }
1145        if ($process->status == 'missed') {
1146            $data['nicht_erschienen'] = 1;
1147        }
1148        if ($process->status == 'parked') {
1149            $data['parked'] = 1;
1150        }
1151        if ($process->status == 'confirmed') {
1152            $data['bestaetigt'] = 1;
1153        }
1154        if ($process->status == 'preconfirmed') {
1155            $data['bestaetigt'] = 0;
1156        }
1157        $data['status'] = $process->status;
1158        $data['parkedBy'] = $process->getParkedBy();
1159
1160        $this->addValues($data);
1161    }
1162
1163    protected function addValuesClientData(ProcessEntity $process): void
1164    {
1165        $data = array();
1166        $client = $process->getFirstClient();
1167        if ($client && $client->hasFamilyName()) {
1168            $data['Name'] = $client->familyName;
1169        }
1170        if ($client && $client->hasEmail()) {
1171            $data['EMail'] = $client->email;
1172        }
1173        if ($client && $client->offsetExists('telephone')) {
1174            $data['telefonnummer_fuer_rueckfragen'] = $client->telephone;
1175            $data['Telefonnummer'] = $client->telephone; // to stay compatible with ZMS1
1176        }
1177        if ($client && $client->offsetExists('emailSendCount')) {
1178            $data['EMailverschickt'] = ('-1' == $client->emailSendCount) ? 0 : $client->emailSendCount;
1179        }
1180        $data['Anmerkung'] = $process->getAmendment();
1181        $data['custom_text_field'] = $process->getCustomTextfield();
1182        $data['custom_text_field2'] = $process->getCustomTextfield2();
1183        $data['zustimmung_kundenbefragung'] = ($client->surveyAccepted) ? 1 : 0;
1184        $data['Erinnerungszeitpunkt'] = $process->getReminderTimestamp();
1185        $data['AnzahlPersonen'] = $process->getClients()->count();
1186        $this->addValues($data);
1187    }
1188
1189    public function addValueDisplayNumber(ProcessEntity $process): void
1190    {
1191        $data['displayNumber'] = $process->displayNumber;
1192        $this->addValues($data);
1193    }
1194
1195    protected function addProcessingTimeData(ProcessEntity $process, \DateTimeInterface $dateTime, $previousStatus = null): void
1196    {
1197        $data = array();
1198        $timeoutTime = null;
1199        $finishTime = null;
1200
1201        if (
1202            isset($previousStatus) &&
1203            (($process->status == 'called' && $previousStatus == 'called') ||
1204                ($process->status == 'processing' && $previousStatus == 'processing'))
1205        ) {
1206            $timeoutTime = $dateTime->format('Y-m-d H:i:s');
1207            $data['timeoutTime'] = $timeoutTime;
1208        } elseif ($process->status == 'processing' && empty($process->showUpTime)) {
1209            // Do not reset showUpTime on re-save (preserves Bearbeitungszeit)
1210            $showUpTime = $dateTime->format('Y-m-d H:i:s');
1211            $data['showUpTime'] = $showUpTime;
1212        } elseif ($process->status == 'finished') {
1213            $finishTime = $dateTime->format('Y-m-d H:i:s');
1214            $data['finishTime'] = $finishTime;
1215        } elseif (
1216            isset($previousStatus)
1217            && in_array($previousStatus, ['called', 'processing'], true)
1218            && in_array($process->status, ['queued', 'parked'], true)
1219        ) {
1220            // Drop open processing segment when leaving to queue/park so a later
1221            // resume starts a fresh showUpTime (park-and-resume ATAF expectation).
1222            $data['showUpTime'] = null;
1223            $data['timeoutTime'] = null;
1224        }
1225
1226
1227        if (isset($finishTime) && isset($process->showUpTime)) {
1228            $showUpDateTime = new \DateTime($process->showUpTime);
1229            $finishTime = new \DateTime($finishTime);
1230
1231            $processingTimeStr = $process->getProcessingTime();
1232            $previousProcessingTimeInSeconds = 0; // Default to 0 if not set
1233
1234            if (!empty($processingTimeStr)) {
1235                // Assume the format is HH:MM:SS and parse it
1236                list($hours, $minutes, $seconds) = explode(':', $processingTimeStr);
1237                // Convert hours, minutes, and seconds to total seconds
1238                $previousProcessingTimeInSeconds = (int)$hours * 3600 + (int)$minutes * 60 + (int)$seconds;
1239            }
1240
1241            $interval = $showUpDateTime->diff($finishTime);
1242            $totalSeconds = ($interval->days * 24 * 60 * 60) + ($interval->h * 60 * 60) + ($interval->i * 60) + $interval->s;
1243
1244            $totalSeconds += $previousProcessingTimeInSeconds;
1245
1246            $hours = intdiv($totalSeconds, 3600);
1247            $minutes = intdiv($totalSeconds % 3600, 60);
1248            $seconds = $totalSeconds % 60;
1249
1250            $data['processing_time'] = sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
1251        } elseif (isset($timeoutTime) && isset($process->showUpTime)) {
1252            $showUpDateTime = new \DateTime($process->showUpTime);
1253            $timeoutDateTime = new \DateTime($timeoutTime);
1254            $processingTimeStr = $process->getProcessingTime();
1255
1256            $previousProcessingTimeInSeconds = 0; // Default to 0 if not set
1257            if (!empty($processingTimeStr)) {
1258                // Assume the format is HH:MM:SS and parse it
1259                list($hours, $minutes, $seconds) = explode(':', $processingTimeStr);
1260                // Convert hours, minutes, and seconds to total seconds
1261                $previousProcessingTimeInSeconds = (int)$hours * 3600 + (int)$minutes * 60 + (int)$seconds;
1262            }
1263            $interval = $showUpDateTime->diff($timeoutDateTime);
1264            $totalSeconds = ($interval->days * 24 * 60 * 60) + ($interval->h * 60 * 60) + ($interval->i * 60) + $interval->s;
1265
1266            $totalSeconds += $previousProcessingTimeInSeconds;
1267
1268            $hours = intdiv($totalSeconds, 3600);
1269            $minutes = intdiv($totalSeconds % 3600, 60);
1270            $seconds = $totalSeconds % 60;
1271
1272            $data['processing_time'] = sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
1273        }
1274
1275        $this->addValues($data);
1276    }
1277
1278    protected function addValuesQueueData(ProcessEntity $process): void
1279    {
1280        $data = array();
1281        $appointmentTime = $process->getFirstAppointment()->toDateTime()->format('H:i:s');
1282
1283        if (isset($process->queue['callCount'])) {
1284            $data['AnzahlAufrufe'] = (int) $process->queue['callCount'];
1285        }
1286        if (isset($process->queue['callTime']) && $process->queue['callTime']) {
1287            $data['aufrufzeit'] = (new \DateTimeImmutable())
1288                ->setTimestamp($process->queue['callTime'])->format('H:i:s');
1289        }
1290        if (isset($process->queue['lastCallTime']) && $process->queue['lastCallTime']) {
1291            $data['Timestamp'] = (new \DateTimeImmutable())
1292                ->setTimestamp($process->queue['lastCallTime'])->format('H:i:s');
1293        }
1294        if (isset($process->queue['arrivalTime']) && $process->queue['arrivalTime']) {
1295            $data['wsm_aufnahmezeit'] = (new \DateTimeImmutable())
1296                ->setTimestamp($process->queue['arrivalTime'])->format('H:i:s');
1297        }
1298        if (isset($data['wsm_aufnahmezeit']) && $data['wsm_aufnahmezeit'] == $appointmentTime) {
1299            // Do not save arrivalTime if it is an appointment
1300            $data['wsm_aufnahmezeit'] = 0;
1301        }
1302        $this->addValues($data);
1303    }
1304
1305    protected function addValuesWaitingTimeData(ProcessEntity $process, $previousStatus = null): void
1306    {
1307        $data = array();
1308        $hasWaitingTimeAlready = (
1309            isset($process->queue->waitingTime)
1310            && $process->queue->waitingTime
1311            && $process->queue->waitingTime !== '00:00:00'
1312        );
1313
1314        if (
1315            !$hasWaitingTimeAlready
1316            && (
1317            (
1318                // Szenario 1: Vorheriger Status ist queued, missed oder confirmed und aktueller Status ist called
1319                in_array($previousStatus, ['queued', 'missed', 'confirmed'])
1320                && $process->status == 'called'
1321                && (
1322                    (
1323                        isset($process->queue->callCount)
1324                        && $process->queue->callCount <= 0
1325                    )
1326                    || (
1327                        ($previousStatus === 'queued' || $previousStatus === 'confirmed')
1328                        && (
1329                            !isset($process->queue->callTime)
1330                            || (int) $process->queue->callTime === 0
1331                        )
1332                    )
1333                )
1334            )
1335            ||
1336            (
1337                // Szenario 2: Vorheriger Status ist missed, aktueller Status ist queued,
1338                // es gibt den Hinweis wasMissed und die Queue sowie waitingTime sind gesetzt
1339                $previousStatus == 'missed'
1340                && $process->status == 'queued'
1341                && !empty($process->wasMissed)
1342                && isset($process->queue)
1343                && isset($process->queue->waitingTime)
1344            )
1345            )
1346        ) {
1347            $wartezeitInSeconds = $process->getWaitedSeconds();
1348            $wartezeitInSeconds = $wartezeitInSeconds > 0 ? $wartezeitInSeconds : 0;
1349
1350            // Convert total seconds into HH:MM:SS format
1351            $hours = intdiv($wartezeitInSeconds, 3600);
1352            $minutes = intdiv($wartezeitInSeconds % 3600, 60);
1353            $seconds = $wartezeitInSeconds % 60;
1354
1355            $data['waiting_time'] = sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
1356        }
1357
1358        $this->addValues($data);
1359    }
1360
1361
1362    protected function addValuesWayTimeData(ProcessEntity $process): void
1363    {
1364        $data = array();
1365        if ($process->status == 'processing') {
1366            $wayTimeInSeconds = max(0, (int) $process->getWaySeconds());
1367            $hours = intdiv($wayTimeInSeconds, 3600);
1368            $minutes = intdiv($wayTimeInSeconds % 3600, 60);
1369            $seconds = $wayTimeInSeconds % 60;
1370            $data['way_time'] = sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
1371        }
1372        $this->addValues($data);
1373    }
1374
1375    protected function addValuesWasMissed(ProcessEntity $process): static
1376    {
1377        $data = [
1378            'wasMissed' => $process->wasMissed ? 1 : 0,
1379        ];
1380
1381        $this->addValues($data);
1382        return $this;
1383    }
1384
1385    protected function addValuesPriority(ProcessEntity $process): static
1386    {
1387        $data = [
1388            'priority' => $process->priority,
1389        ];
1390
1391        $this->addValues($data);
1392        return $this;
1393    }
1394
1395    protected function addValuesExternalUserId(ProcessEntity $process): static
1396    {
1397        $data = [
1398            'external_user_id' => $process->externalUserId,
1399        ];
1400
1401        $this->addValues($data);
1402        return $this;
1403    }
1404
1405    #[\Override]
1406    public function postProcess($data)
1407    {
1408        $data[$this->getPrefixed("appointments__0__date")] =
1409            strtotime($data[$this->getPrefixed("appointments__0__date")]);
1410        if ('00:00:00' != $data[$this->getPrefixed("queue__callTime")]) {
1411            $time = explode(':', $data[$this->getPrefixed("queue__callTime")]);
1412            $data[$this->getPrefixed("queue__callTime")] = (new \DateTimeImmutable())
1413                ->setTimestamp($data[$this->getPrefixed("appointments__0__date")])
1414                ->setTime($time[0], $time[1], $time[2])
1415                ->getTimestamp();
1416        } else {
1417            $data[$this->getPrefixed("queue__callTime")] = 0;
1418        }
1419        if ('00:00:00' != $data[$this->getPrefixed("queue__lastCallTime")]) {
1420            $time = explode(':', $data[$this->getPrefixed("queue__lastCallTime")]);
1421            $data[$this->getPrefixed("queue__lastCallTime")] = (new \DateTimeImmutable())
1422                ->setTimestamp($data[$this->getPrefixed("appointments__0__date")])
1423                ->setTime($time[0], $time[1], $time[2])
1424                ->getTimestamp();
1425        } else {
1426            $data[$this->getPrefixed("queue__lastCallTime")] = 0;
1427        }
1428        $data[$this->getPrefixed("queue__arrivalTime")] =
1429            strtotime($data[$this->getPrefixed("queue__arrivalTime")]);
1430        if (
1431            isset($data[$this->getPrefixed('scope__provider__data')])
1432            && $data[$this->getPrefixed('scope__provider__data')]
1433        ) {
1434            $data[$this->getPrefixed('scope__provider__data')] =
1435                json_decode($data[$this->getPrefixed('scope__provider__data')], true);
1436        }
1437        if (isset($data[$this->getPrefixed('__clientsCount')])) {
1438            $clientsCount = $data[$this->getPrefixed('__clientsCount')];
1439            unset($data[$this->getPrefixed('__clientsCount')]);
1440            while (--$clientsCount > 0) {
1441                $data[$this->getPrefixed('clients__' . $clientsCount . '__familyName')] = 'Unbekannt';
1442            }
1443        }
1444        $data[$this->getPrefixed("lastChange")] =
1445            (new \DateTimeImmutable($data[$this->getPrefixed("lastChange")] .
1446                \BO\Zmsbackend\Connection\Select::$connectionTimezone))->getTimestamp();
1447        return $data;
1448    }
1449
1450    /**
1451     * @psalm-api
1452     */
1453    public function removeDuplicates(): static
1454    {
1455        $this->query->groupBy('process.BuergerID');
1456        return $this;
1457    }
1458
1459    /**
1460     * @return void
1461     */
1462    #[\Override]
1463    protected function addRequiredJoins()
1464    {
1465        if ($this->shouldLoadEntity('processuser')) {
1466            $this->leftJoin(
1467                new \BO\Zmsbackend\Query\Alias(\BO\Zmsbackend\Useraccount\Repository\Useraccount::TABLE, 'processuser'),
1468                'process.NutzerID',
1469                '=',
1470                'processuser.NutzerID'
1471            );
1472        }
1473
1474        if ($this->shouldLoadEntity('processscope')) {
1475            $this->leftJoin(
1476                new \BO\Zmsbackend\Query\Alias(\BO\Zmsbackend\Query\Scope::TABLE, 'processscope'),
1477                'process.StandortID',
1478                '=',
1479                'processscope.StandortID'
1480            );
1481        }
1482    }
1483
1484    public function addConditionExternalUserId(string $externalUserId): static
1485    {
1486        $this->query->where('process.external_user_id', '=', $externalUserId);
1487        return $this;
1488    }
1489}