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