Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.90% covered (warning)
77.90%
652 / 837
71.23% covered (warning)
71.23%
52 / 73
CRAP
0.00% covered (danger)
0.00%
0 / 1
Process
77.90% covered (warning)
77.90%
652 / 837
71.23% covered (warning)
71.23%
52 / 73
691.73
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
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
4.01
 addConditionProcessIdOrSearch
96.67% covered (success)
96.67%
29 / 30
0.00% covered (danger)
0.00%
0 / 1
3
 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
96.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        int $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(int $queueNumber, int $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    /**
678     * Match an exact process id or a general search hit in one parenthesized OR group.
679     */
680    public function addConditionProcessIdOrSearch($queryString): static
681    {
682        $queryString = trim((string) $queryString);
683        $processId = (int) $queryString;
684        $terms = $this->parseSearchTerms($queryString);
685
686        $this->query->where(function (
687            \BO\Zmsbackend\Query\Builder\ConditionBuilder $group
688        ) use (
689            $processId,
690            $terms
691        ) {
692            $group->andWith('process.BuergerID', '=', $processId);
693
694            if ($terms === []) {
695                return;
696            }
697
698            $group->orWith(function (
699                \BO\Zmsbackend\Query\Builder\ConditionBuilder $search
700            ) use ($terms) {
701                foreach ($terms as $term) {
702                    $search->andWith(
703                        function (
704                            \BO\Zmsbackend\Query\Builder\ConditionBuilder $inner
705                        ) use ($term) {
706                            $this->appendGeneralSearchTermGroup(
707                                $inner,
708                                $term['value'],
709                                $term['quoted']
710                            );
711                        }
712                    );
713                }
714            });
715        });
716
717        return $this;
718    }
719
720    private function appendGeneralSearchTermGroup(\BO\Zmsbackend\Query\Builder\ConditionBuilder $query, string $term, bool $quoted = false): void
721    {
722        $likeContains = '%' . $this->escapeLikeValue($term) . '%';
723
724        $useNameWordBoundary = !$quoted
725            && !$this->isNumericSearchQuery($term) && mb_strlen($term) <= 3;
726
727        if ($quoted) {
728            $this->appendNamePartLikeGroup(
729                $query,
730                $term,
731                true
732            );
733        } elseif ($useNameWordBoundary) {
734            $this->appendNamePartLikeGroup($query, $term, true, true);
735        } else {
736            $query->orWith('process.Name', 'LIKE', $likeContains);
737        }
738
739        $query->orWith('process.EMail', 'LIKE', $likeContains);
740        $query->orWith('process.Telefonnummer', 'LIKE', $likeContains);
741        $query->orWith('process.telefonnummer_fuer_rueckfragen', 'LIKE', $likeContains);
742        $query->orWith('process.displayNumber', 'LIKE', $likeContains);
743        $query->orWith('process.custom_text_field', 'LIKE', $likeContains);
744        $query->orWith('process.custom_text_field2', 'LIKE', $likeContains);
745    }
746
747    public function addConditionUpcomingOnly(\DateTimeInterface $now): static
748    {
749        $today = $now->format('Y-m-d');
750        $time = $now->format('H:i:s');
751        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $condition) use ($today, $time) {
752            $condition
753                ->andWith('process.Datum', '>', $today)
754                ->orWith(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $inner) use ($today, $time) {
755                    $inner
756                        ->andWith('process.Datum', '=', $today)
757                        ->andWith('process.Uhrzeit', '>=', $time);
758                });
759        });
760        return $this;
761    }
762
763    /** @psalm-api */
764    public function addOrderByAppointmentDate(): static
765    {
766        $this->query->orderBy('process.Datum', 'ASC');
767        $this->query->orderBy('process.Uhrzeit', 'ASC');
768        $this->query->orderBy('process.BuergerID', 'ASC');
769        return $this;
770    }
771
772    /** @psalm-api */
773    public function addOrderBySearchRelevance(string $queryString): static
774    {
775        $queryString = trim($queryString);
776        if ($queryString === '') {
777            return $this;
778        }
779
780        $terms = $this->parseSearchTerms($queryString);
781        if ($terms === []) {
782            return $this;
783        }
784        $fullQuery = $this->escapeLikePatternForSqlLiteral($queryString);
785        $fullQueryExact = $this->escapeSqlStringLiteral($queryString);
786
787        $allTermsAsWordsConditions = [];
788        $allTermsInNameConditions = [];
789        $anyTermInNameConditions = [];
790
791        foreach ($terms as $term) {
792            $escapedTerm = $this->escapeLikePatternForSqlLiteral($term['value']);
793            $exactTerm = $this->escapeSqlStringLiteral($term['value']);
794            $allTermsAsWordsConditions[] = "("
795                . "process.Name = '{$exactTerm}'"
796                . " OR process.Name LIKE '{$escapedTerm} %'"
797                . " OR process.Name LIKE '% {$escapedTerm}'"
798                . " OR process.Name LIKE '% {$escapedTerm} %'"
799                . ")";
800            $allTermsInNameConditions[] = "process.Name LIKE '%{$escapedTerm}%'";
801            $anyTermInNameConditions[] = "process.Name LIKE '%{$escapedTerm}%'";
802        }
803        $allTermsAsWords = implode(' AND ', $allTermsAsWordsConditions);
804        $allTermsInName = implode(' AND ', $allTermsInNameConditions);
805        $anyTermInName = implode(' OR ', $anyTermInNameConditions);
806
807        $this->query->orderBy(self::expression(
808            "CASE
809                   WHEN process.Name LIKE '{$fullQueryExact}' THEN 0
810                   WHEN process.Name LIKE '{$fullQuery} %' THEN 1
811                   WHEN {$allTermsAsWords} THEN 2
812                   WHEN {$allTermsInName} THEN 3
813                   WHEN {$anyTermInName} THEN 4
814                   ELSE 5
815                END"
816        ));
817
818        $this->query->orderBy('process.Datum', 'ASC');
819        $this->query->orderBy('process.Uhrzeit', 'ASC');
820        $this->query->orderBy('process.BuergerID', 'ASC');
821
822        return $this;
823    }
824
825    /**
826     * @return array<int, array{value: string, quoted: bool}>
827     */
828    private function parseSearchTerms(string $queryString): array
829    {
830        if ($queryString === '') {
831            return [];
832        }
833
834        if (!preg_match_all('/"([^"]+)"|(\S+)/u', $queryString, $matches, PREG_SET_ORDER)) {
835            return [['value' => $queryString, 'quoted' => false]];
836        }
837
838        $terms = [];
839        foreach ($matches as $match) {
840            $value = trim($match[1] !== '' ? $match[1] : $match[2]);
841            if ($value === '') {
842                continue;
843            }
844            $terms[] = [
845                'value' => $value,
846                'quoted' => $match[1] !== '',
847            ];
848        }
849
850        return $terms;
851    }
852
853    private function appendNamePartLikeGroup(
854        \BO\Zmsbackend\Query\Builder\ConditionBuilder $query,
855        string $term,
856        bool $wordBoundaryOnly = false,
857        bool $includeWordPrefix = false
858    ): void {
859        $escaped = $this->escapeLikeValue($term);
860        $query->orWith('process.Name', 'LIKE', $escaped . ' %');
861        $query->orWith('process.Name', 'LIKE', '% ' . $escaped);
862        $query->orWith('process.Name', 'LIKE', '% ' . $escaped . ' %');
863        $query->orWith('process.Name', '=', $term);
864        if ($wordBoundaryOnly) {
865            if ($includeWordPrefix) {
866                $query->orWith('process.Name', 'LIKE', $escaped . '%');
867                $query->orWith('process.Name', 'LIKE', '% ' . $escaped . '%');
868            }
869        } else {
870            $query->orWith('process.Name', 'LIKE', '%' . $escaped . '%');
871        }
872    }
873
874    private function isNumericSearchQuery(string $queryString): bool
875    {
876        return (bool) preg_match('#^\d+$#', $queryString);
877    }
878
879    private function escapeLikeValue(string $value): string
880    {
881        return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value);
882    }
883
884    private function escapeLikePatternForSqlLiteral(string $value): string
885    {
886        return $this->escapeSqlStringLiteral($this->escapeLikeValue($value));
887    }
888
889    private function escapeSqlStringLiteral(string $value): string
890    {
891        return str_replace("'", "''", $value);
892    }
893
894    public function addConditionName($name, bool $exactMatching = false): static
895    {
896        if ($exactMatching) {
897            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($name) {
898                $query->andWith('process.Name', '=', $name);
899            });
900        } else {
901            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($name) {
902                $query->andWith('process.Name', 'LIKE', "%$name%");
903            });
904        }
905        return $this;
906    }
907
908    public function addConditionMail(string $mailAddress, bool $exactMatching = false): static
909    {
910        if ($exactMatching) {
911            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($mailAddress) {
912                $query->andWith('process.Email', '=', $mailAddress);
913            });
914        } else {
915            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($mailAddress) {
916                $query->andWith('process.Email', 'LIKE', "%$mailAddress%");
917            });
918        }
919        return $this;
920    }
921
922    /**
923     * @psalm-api
924     */
925    public function addConditionCustomTextfield($customText, bool $exactMatching = false): static
926    {
927        if ($exactMatching) {
928            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($customText) {
929                $query->andWith('process.custom_text_field', '=', $customText);
930            });
931        } else {
932            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($customText) {
933                $query->andWith('process.custom_text_field', 'LIKE', "%$customText%");
934            });
935        }
936        return $this;
937    }
938
939    /**
940     * @psalm-api
941     */
942    public function addConditionCustomTextfield2($customText2, bool $exactMatching = false): static
943    {
944        if ($exactMatching) {
945            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($customText2) {
946                $query->andWith('process.custom_text_field2', '=', $customText2);
947            });
948        } else {
949            $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($customText2) {
950                $query->andWith('process.custom_text_field2', 'LIKE', "%$customText2%");
951            });
952        }
953        return $this;
954    }
955
956    public function addConditionAmendment($amendment): static
957    {
958        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($amendment) {
959            $query->andWith('process.Anmerkung', 'LIKE', "%$amendment%");
960        });
961        return $this;
962    }
963
964    /**
965     * Add Requests Join
966     */
967    public function addConditionRequestId($requestId): static
968    {
969        $this->leftJoin(
970            new \BO\Zmsbackend\Query\Alias("buergeranliegen", 'buergeranliegen'),
971            'buergeranliegen.BuergerID',
972            '=',
973            'process.BuergerID'
974        );
975        $this->query->where('buergeranliegen.AnliegenID', '=', $requestId);
976        return $this;
977    }
978
979    public function addConditionScopeNameSearch(string $scopeName): static
980    {
981        $likeValue = '%' . $this->escapeLikeValue($scopeName) . '%';
982        $this->leftJoin(
983            new \BO\Zmsbackend\Query\Alias(\BO\Zmsbackend\Query\Scope::TABLE, 'search_scope'),
984            self::expression(
985                'IF(`process`.`AbholortID`, `process`.`AbholortID`, `process`.`StandortID`)'
986            ),
987            '=',
988            'search_scope.StandortID'
989        );
990        $this->leftJoin(
991            new \BO\Zmsbackend\Query\Alias(\BO\Zmsbackend\Provider\Repository\Provider::TABLE, 'search_scope_provider'),
992            self::expression(
993                'search_scope.InfoDienstleisterID = search_scope_provider.id
994                AND search_scope.source = search_scope_provider.source'
995            )
996        );
997        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($likeValue) {
998            $query->orWith('search_scope.Bezeichnung', 'LIKE', $likeValue);
999            $query->orWith('search_scope.standortinfozeile', 'LIKE', $likeValue);
1000            $query->orWith('search_scope.standortkuerzel', 'LIKE', $likeValue);
1001            $query->orWith('search_scope_provider.name', 'LIKE', $likeValue);
1002        });
1003
1004        return $this;
1005    }
1006
1007    public function addConditionServiceNameSearch(string $serviceName): static
1008    {
1009        $likeValue = '%' . $this->escapeLikeValue($serviceName) . '%';
1010        $this->leftJoin(
1011            new \BO\Zmsbackend\Query\Alias('buergeranliegen', 'search_request_link'),
1012            'search_request_link.BuergerID',
1013            '=',
1014            'process.BuergerID'
1015        );
1016        $this->leftJoin(
1017            new \BO\Zmsbackend\Query\Alias(\BO\Zmsbackend\Request\Repository\Request::TABLE, 'search_request'),
1018            self::expression(
1019                'search_request_link.AnliegenID = search_request.id
1020                AND search_request_link.source = search_request.source'
1021            )
1022        );
1023        $this->query->where('search_request.name', 'LIKE', $likeValue);
1024
1025        return $this;
1026    }
1027
1028    /**
1029     * add condition to get process if deallocation time < now
1030     */
1031    public function addConditionDeallocate(\DateTimeInterface $now): static
1032    {
1033        $this->query->where(function (\BO\Zmsbackend\Query\Builder\ConditionBuilder $query) use ($now) {
1034            $query
1035                ->andWith('process.Name', '=', '(abgesagt)')
1036                ->andWith('process.IPTimeStamp', '<', $now->getTimestamp());
1037        });
1038        $this->query->orderBy('process.IPTimeStamp', 'ASC');
1039        return $this;
1040    }
1041
1042    public function addValuesNewProcess(ProcessEntity $process, $parentProcess = 0, int $childProcessCount = 0): void
1043    {
1044        $values = [
1045            'BuergerID' => $process->id,
1046            'IPTimeStamp' => $process->createTimestamp,
1047            'absagecode' => $process->authKey,
1048            'hatFolgetermine' => $childProcessCount,
1049            'istFolgeterminvon' => $parentProcess,
1050            'displayNumber' => $parentProcess === 0 && empty($process->queue['number'])
1051                ? $this->getNewDisplayNumber($process)
1052                : null,
1053            'wartenummer' => $process->queue['number']
1054        ];
1055        if ($process->toProperty()->apiclient->apiClientID->isAvailable()) {
1056            $values['apiClientID'] = $process->apiclient->apiClientID;
1057        }
1058        if (isset($process->isTicketprinter) && $process->isTicketprinter) {
1059            $values['is_ticketprinter'] = 1;
1060        }
1061        $this->addValues($values);
1062    }
1063
1064    public function getNewDisplayNumber(ProcessEntity $process)
1065    {
1066        if (empty($process->scope->getPreference('queue', 'displayNumberPrefix'))) {
1067            return $process->id;
1068        }
1069
1070        $newDisplayNumber = $process->scope->getPreference('queue', 'displayNumberPrefix') . str_pad(
1071            (new ScopeEntity())->readDisplayNumberUpdated($process->scope->id),
1072            4,
1073            '0',
1074            STR_PAD_LEFT
1075        );
1076
1077        if (
1078            $this->checkIfDisplayNumberOnSameDateExists(
1079                $process->scope->id,
1080                $newDisplayNumber,
1081                \DateTime::createFromFormat('U', (int) $process->getFirstAppointment()->date)
1082            )
1083        ) {
1084            return $this->getNewDisplayNumber($process);
1085        }
1086
1087        return $newDisplayNumber;
1088    }
1089
1090    public function checkIfDisplayNumberOnSameDateExists($scopeId, string $displayNumber, \DateTime|false $date): bool
1091    {
1092        $processWithDisplayNumber = (new \BO\Zmsbackend\Process\Service\Process())->readProcessWithSameDayAndDisplayNumber(
1093            $scopeId,
1094            $displayNumber,
1095            $date->format('Y-m-d')
1096        );
1097
1098        return !empty($processWithDisplayNumber->getId());
1099    }
1100
1101    public function addValuesUpdateProcess(
1102        ProcessEntity $process,
1103        \DateTimeInterface $dateTime,
1104        int $parentProcess = 0,
1105        $previousStatus = null
1106    ): void {
1107        $this->addValuesIPAdress($process);
1108        if (0 === $parentProcess) {
1109            $this->addValuesClientData($process);
1110            $this->addProcessingTimeData($process, $dateTime, $previousStatus);
1111            $this->addValuesQueueData($process);
1112            $this->addValuesWaitingTimeData($process, $previousStatus);
1113            $this->addValuesWayTimeData($process);
1114        }
1115        if ($process->isWithAppointment()) {
1116            $this->addValuesFollowingProcessData($process, $parentProcess);
1117        }
1118        $this->addValuesWasMissed($process);
1119        $this->addValuesPriority($process);
1120        $this->addValuesStatusData($process, $dateTime);
1121        $this->addValuesExternalUserId($process);
1122    }
1123
1124    public function addValuesIPAdress(ProcessEntity $process): void
1125    {
1126        $data = array();
1127        $data['IPAdresse'] = $process->createIP;
1128        $this->addValues($data);
1129    }
1130
1131    public function addValuesFollowingProcessData(ProcessEntity $process, $parentProcess): void
1132    {
1133        $data = array();
1134        if (0 === $parentProcess) {
1135            $data['hatFolgetermine'] = (1 <= $process->getFirstAppointment()->getSlotCount()) ?
1136                $process->getFirstAppointment()->getSlotCount() - 1 :
1137                0;
1138        } else {
1139            $data['Name'] = '(Folgetermin)';
1140        }
1141        $this->addValues($data);
1142    }
1143
1144    public function addValuesAppointmentData(
1145        ProcessEntity $process
1146    ): void {
1147        $data = array();
1148        $appointment = $process->getFirstAppointment();
1149        $datetime = $appointment->toDateTime();
1150        $data['Datum'] = $datetime->format('Y-m-d');
1151        $data['Uhrzeit'] = $datetime->format('H:i:s');
1152        $this->addValues($data);
1153    }
1154
1155    public function addValuesScopeData(
1156        ProcessEntity $process
1157    ): void {
1158        $data = array();
1159        $data['StandortID'] = $process->getScopeId();
1160        $this->addValues($data);
1161    }
1162
1163    public function addValuesStatusData(ProcessEntity $process, \DateTimeInterface $dateTime): void
1164    {
1165        $data = array();
1166        $data['vorlaeufigeBuchung'] = ($process->status == 'reserved') ? 1 : 0;
1167        $data['aufruferfolgreich'] = ($process->status == 'processing') ? 1 : 0;
1168        if ($process->status == 'called') {
1169            $data['parked'] = 0;
1170            $data['nicht_erschienen'] = 0;
1171        }
1172        if ($process->status == 'pending') {
1173            $data['AbholortID'] = $process->scope->id;
1174            $data['Abholer'] = 1;
1175            $data['nicht_erschienen'] = 0;
1176            $data['parked'] = 0;
1177        }
1178        if ($process->status == 'queued') {
1179            $data['nicht_erschienen'] = 0;
1180            $data['parked'] = 0;
1181            if (
1182                $process->hasArrivalTime() &&
1183                (isset($process->queue->withAppointment) && $process->queue->withAppointment)
1184            ) {
1185                $data['wsm_aufnahmezeit'] = $dateTime->format('H:i:s');
1186            }
1187        }
1188        if ($process->status == 'missed') {
1189            $data['nicht_erschienen'] = 1;
1190        }
1191        if ($process->status == 'parked') {
1192            $data['parked'] = 1;
1193        }
1194        if ($process->status == 'confirmed') {
1195            $data['bestaetigt'] = 1;
1196        }
1197        if ($process->status == 'preconfirmed') {
1198            $data['bestaetigt'] = 0;
1199        }
1200        $data['status'] = $process->status;
1201        $data['parkedBy'] = $process->getParkedBy();
1202
1203        $this->addValues($data);
1204    }
1205
1206    protected function addValuesClientData(ProcessEntity $process): void
1207    {
1208        $data = array();
1209        $client = $process->getFirstClient();
1210        if ($client && $client->hasFamilyName()) {
1211            $data['Name'] = $client->familyName;
1212        }
1213        if ($client && $client->hasEmail()) {
1214            $data['EMail'] = $client->email;
1215        }
1216        if ($client && $client->offsetExists('telephone')) {
1217            $data['telefonnummer_fuer_rueckfragen'] = $client->telephone;
1218            $data['Telefonnummer'] = $client->telephone; // to stay compatible with ZMS1
1219        }
1220        if ($client && $client->offsetExists('emailSendCount')) {
1221            $data['EMailverschickt'] = ('-1' == $client->emailSendCount) ? 0 : $client->emailSendCount;
1222        }
1223        $data['Anmerkung'] = $process->getAmendment();
1224        $data['custom_text_field'] = $process->getCustomTextfield();
1225        $data['custom_text_field2'] = $process->getCustomTextfield2();
1226        $data['zustimmung_kundenbefragung'] = ($client->surveyAccepted) ? 1 : 0;
1227        $data['Erinnerungszeitpunkt'] = $process->getReminderTimestamp();
1228        $data['AnzahlPersonen'] = $process->getClients()->count();
1229        $this->addValues($data);
1230    }
1231
1232    public function addValueDisplayNumber(ProcessEntity $process): void
1233    {
1234        $data['displayNumber'] = $process->displayNumber;
1235        $this->addValues($data);
1236    }
1237
1238    protected function addProcessingTimeData(ProcessEntity $process, \DateTimeInterface $dateTime, $previousStatus = null): void
1239    {
1240        $data = array();
1241        $timeoutTime = null;
1242        $finishTime = null;
1243
1244        if (
1245            isset($previousStatus) &&
1246            (($process->status == 'called' && $previousStatus == 'called') ||
1247                ($process->status == 'processing' && $previousStatus == 'processing'))
1248        ) {
1249            $timeoutTime = $dateTime->format('Y-m-d H:i:s');
1250            $data['timeoutTime'] = $timeoutTime;
1251        } elseif ($process->status == 'processing' && ($process->showUpTime === null || $process->showUpTime === '')) {
1252            // Do not reset showUpTime on re-save (preserves Bearbeitungszeit)
1253            $showUpTime = $dateTime->format('Y-m-d H:i:s');
1254            $data['showUpTime'] = $showUpTime;
1255        } elseif ($process->status == 'finished') {
1256            $finishTime = $dateTime->format('Y-m-d H:i:s');
1257            $data['finishTime'] = $finishTime;
1258        } elseif (
1259            isset($previousStatus)
1260            && in_array($previousStatus, ['called', 'processing'], true)
1261            && in_array($process->status, ['queued', 'parked'], true)
1262        ) {
1263            // Drop open processing segment when leaving to queue/park so a later
1264            // resume starts a fresh showUpTime (park-and-resume ATAF expectation).
1265            $data['showUpTime'] = null;
1266            $data['timeoutTime'] = null;
1267        }
1268
1269
1270        if (isset($finishTime) && isset($process->showUpTime)) {
1271            $showUpDateTime = new \DateTime($process->showUpTime);
1272            $finishTime = new \DateTime($finishTime);
1273
1274            $processingTimeStr = $process->getProcessingTime();
1275            $previousProcessingTimeInSeconds = 0; // Default to 0 if not set
1276
1277            if (!empty($processingTimeStr)) {
1278                // Assume the format is HH:MM:SS and parse it
1279                list($hours, $minutes, $seconds) = explode(':', $processingTimeStr);
1280                // Convert hours, minutes, and seconds to total seconds
1281                $previousProcessingTimeInSeconds = (int)$hours * 3600 + (int)$minutes * 60 + (int)$seconds;
1282            }
1283
1284            $interval = $showUpDateTime->diff($finishTime);
1285            $totalSeconds = ($interval->days * 24 * 60 * 60) + ($interval->h * 60 * 60) + ($interval->i * 60) + $interval->s;
1286
1287            $totalSeconds += $previousProcessingTimeInSeconds;
1288
1289            $hours = intdiv($totalSeconds, 3600);
1290            $minutes = intdiv($totalSeconds % 3600, 60);
1291            $seconds = $totalSeconds % 60;
1292
1293            $data['processing_time'] = sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
1294        } elseif (isset($timeoutTime) && isset($process->showUpTime)) {
1295            $showUpDateTime = new \DateTime($process->showUpTime);
1296            $timeoutDateTime = new \DateTime($timeoutTime);
1297            $processingTimeStr = $process->getProcessingTime();
1298
1299            $previousProcessingTimeInSeconds = 0; // Default to 0 if not set
1300            if (!empty($processingTimeStr)) {
1301                // Assume the format is HH:MM:SS and parse it
1302                list($hours, $minutes, $seconds) = explode(':', $processingTimeStr);
1303                // Convert hours, minutes, and seconds to total seconds
1304                $previousProcessingTimeInSeconds = (int)$hours * 3600 + (int)$minutes * 60 + (int)$seconds;
1305            }
1306            $interval = $showUpDateTime->diff($timeoutDateTime);
1307            $totalSeconds = ($interval->days * 24 * 60 * 60) + ($interval->h * 60 * 60) + ($interval->i * 60) + $interval->s;
1308
1309            $totalSeconds += $previousProcessingTimeInSeconds;
1310
1311            $hours = intdiv($totalSeconds, 3600);
1312            $minutes = intdiv($totalSeconds % 3600, 60);
1313            $seconds = $totalSeconds % 60;
1314
1315            $data['processing_time'] = sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
1316        }
1317
1318        $this->addValues($data);
1319    }
1320
1321    protected function addValuesQueueData(ProcessEntity $process): void
1322    {
1323        $data = array();
1324        $appointmentTime = $process->getFirstAppointment()->toDateTime()->format('H:i:s');
1325
1326        if (isset($process->queue['callCount'])) {
1327            $data['AnzahlAufrufe'] = (int) $process->queue['callCount'];
1328        }
1329        if (isset($process->queue['callTime']) && $process->queue['callTime']) {
1330            $data['aufrufzeit'] = (new \DateTimeImmutable())
1331                ->setTimestamp($process->queue['callTime'])->format('H:i:s');
1332        }
1333        if (isset($process->queue['lastCallTime']) && $process->queue['lastCallTime']) {
1334            $data['Timestamp'] = (new \DateTimeImmutable())
1335                ->setTimestamp($process->queue['lastCallTime'])->format('H:i:s');
1336        }
1337        if (isset($process->queue['arrivalTime']) && $process->queue['arrivalTime']) {
1338            $data['wsm_aufnahmezeit'] = (new \DateTimeImmutable())
1339                ->setTimestamp($process->queue['arrivalTime'])->format('H:i:s');
1340        }
1341        if (isset($data['wsm_aufnahmezeit']) && $data['wsm_aufnahmezeit'] == $appointmentTime) {
1342            // Do not save arrivalTime if it is an appointment
1343            $data['wsm_aufnahmezeit'] = 0;
1344        }
1345        $this->addValues($data);
1346    }
1347
1348    protected function addValuesWaitingTimeData(ProcessEntity $process, $previousStatus = null): void
1349    {
1350        $data = array();
1351        $hasWaitingTimeAlready = (
1352            isset($process->queue->waitingTime)
1353            && $process->queue->waitingTime
1354            && $process->queue->waitingTime !== '00:00:00'
1355        );
1356
1357        if (
1358            !$hasWaitingTimeAlready
1359            && (
1360            (
1361                // Szenario 1: Vorheriger Status ist queued, missed oder confirmed und aktueller Status ist called
1362                in_array($previousStatus, ['queued', 'missed', 'confirmed'])
1363                && $process->status == 'called'
1364                && (
1365                    (
1366                        isset($process->queue->callCount)
1367                        && $process->queue->callCount <= 0
1368                    )
1369                    || (
1370                        ($previousStatus === 'queued' || $previousStatus === 'confirmed')
1371                        && (
1372                            !isset($process->queue->callTime)
1373                            || (int) $process->queue->callTime === 0
1374                        )
1375                    )
1376                )
1377            )
1378            ||
1379            (
1380                // Szenario 2: Vorheriger Status ist missed, aktueller Status ist queued,
1381                // es gibt den Hinweis wasMissed und die Queue sowie waitingTime sind gesetzt
1382                $previousStatus == 'missed'
1383                && $process->status == 'queued'
1384                && !empty($process->wasMissed)
1385                && isset($process->queue)
1386                && isset($process->queue->waitingTime)
1387            )
1388            )
1389        ) {
1390            $wartezeitInSeconds = $process->getWaitedSeconds();
1391            $wartezeitInSeconds = $wartezeitInSeconds > 0 ? $wartezeitInSeconds : 0;
1392
1393            // Convert total seconds into HH:MM:SS format
1394            $hours = intdiv($wartezeitInSeconds, 3600);
1395            $minutes = intdiv($wartezeitInSeconds % 3600, 60);
1396            $seconds = $wartezeitInSeconds % 60;
1397
1398            $data['waiting_time'] = sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
1399        }
1400
1401        $this->addValues($data);
1402    }
1403
1404
1405    protected function addValuesWayTimeData(ProcessEntity $process): void
1406    {
1407        $data = array();
1408        if ($process->status == 'processing') {
1409            $wayTimeInSeconds = max(0, (int) $process->getWaySeconds());
1410            $hours = intdiv($wayTimeInSeconds, 3600);
1411            $minutes = intdiv($wayTimeInSeconds % 3600, 60);
1412            $seconds = $wayTimeInSeconds % 60;
1413            $data['way_time'] = sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
1414        }
1415        $this->addValues($data);
1416    }
1417
1418    protected function addValuesWasMissed(ProcessEntity $process): static
1419    {
1420        $data = [
1421            'wasMissed' => $process->wasMissed ? 1 : 0,
1422        ];
1423
1424        $this->addValues($data);
1425        return $this;
1426    }
1427
1428    protected function addValuesPriority(ProcessEntity $process): static
1429    {
1430        $data = [
1431            'priority' => $process->priority,
1432        ];
1433
1434        $this->addValues($data);
1435        return $this;
1436    }
1437
1438    protected function addValuesExternalUserId(ProcessEntity $process): static
1439    {
1440        $data = [
1441            'external_user_id' => $process->externalUserId,
1442        ];
1443
1444        $this->addValues($data);
1445        return $this;
1446    }
1447
1448    #[\Override]
1449    public function postProcess($data)
1450    {
1451        $data[$this->getPrefixed("appointments__0__date")] =
1452            strtotime($data[$this->getPrefixed("appointments__0__date")]);
1453        if ('00:00:00' != $data[$this->getPrefixed("queue__callTime")]) {
1454            $time = explode(':', $data[$this->getPrefixed("queue__callTime")]);
1455            $data[$this->getPrefixed("queue__callTime")] = (new \DateTimeImmutable())
1456                ->setTimestamp($data[$this->getPrefixed("appointments__0__date")])
1457                ->setTime($time[0], $time[1], $time[2])
1458                ->getTimestamp();
1459        } else {
1460            $data[$this->getPrefixed("queue__callTime")] = 0;
1461        }
1462        if ('00:00:00' != $data[$this->getPrefixed("queue__lastCallTime")]) {
1463            $time = explode(':', $data[$this->getPrefixed("queue__lastCallTime")]);
1464            $data[$this->getPrefixed("queue__lastCallTime")] = (new \DateTimeImmutable())
1465                ->setTimestamp($data[$this->getPrefixed("appointments__0__date")])
1466                ->setTime($time[0], $time[1], $time[2])
1467                ->getTimestamp();
1468        } else {
1469            $data[$this->getPrefixed("queue__lastCallTime")] = 0;
1470        }
1471        $data[$this->getPrefixed("queue__arrivalTime")] =
1472            strtotime($data[$this->getPrefixed("queue__arrivalTime")]);
1473        if (
1474            isset($data[$this->getPrefixed('scope__provider__data')])
1475            && $data[$this->getPrefixed('scope__provider__data')]
1476        ) {
1477            $data[$this->getPrefixed('scope__provider__data')] =
1478                json_decode($data[$this->getPrefixed('scope__provider__data')], true);
1479        }
1480        if (isset($data[$this->getPrefixed('__clientsCount')])) {
1481            $clientsCount = $data[$this->getPrefixed('__clientsCount')];
1482            unset($data[$this->getPrefixed('__clientsCount')]);
1483            while (--$clientsCount > 0) {
1484                $data[$this->getPrefixed('clients__' . $clientsCount . '__familyName')] = 'Unbekannt';
1485            }
1486        }
1487        $data[$this->getPrefixed("lastChange")] =
1488            (new \DateTimeImmutable($data[$this->getPrefixed("lastChange")] .
1489                \BO\Zmsbackend\Connection\Select::$connectionTimezone))->getTimestamp();
1490        return $data;
1491    }
1492
1493    /**
1494     * @psalm-api
1495     */
1496    public function removeDuplicates(): static
1497    {
1498        $this->query->groupBy('process.BuergerID');
1499        return $this;
1500    }
1501
1502    /**
1503     * @return void
1504     */
1505    #[\Override]
1506    protected function addRequiredJoins()
1507    {
1508        if ($this->shouldLoadEntity('processuser')) {
1509            $this->leftJoin(
1510                new \BO\Zmsbackend\Query\Alias(\BO\Zmsbackend\Useraccount\Repository\Useraccount::TABLE, 'processuser'),
1511                'process.NutzerID',
1512                '=',
1513                'processuser.NutzerID'
1514            );
1515        }
1516
1517        if ($this->shouldLoadEntity('processscope')) {
1518            $this->leftJoin(
1519                new \BO\Zmsbackend\Query\Alias(\BO\Zmsbackend\Query\Scope::TABLE, 'processscope'),
1520                'process.StandortID',
1521                '=',
1522                'processscope.StandortID'
1523            );
1524        }
1525    }
1526
1527    public function addConditionExternalUserId(string $externalUserId): static
1528    {
1529        $this->query->where('process.external_user_id', '=', $externalUserId);
1530        return $this;
1531    }
1532}