getLatestRecordDateAction()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
dl 0
loc 16
rs 9.9666
c 1
b 0
f 0
cc 3
nc 2
nop 0
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\AdminCabinet\Controllers;
21
22
use DateTime;
23
use MikoPBX\Common\Providers\PBXConfModulesProvider;
24
use MikoPBX\Modules\Config\CDRConfigInterface;
25
use MikoPBX\Common\Models\PbxSettings;
26
use MikoPBX\Common\Providers\CDRDatabaseProvider;
27
use MikoPBX\Common\Models\Extensions;
28
29
class CallDetailRecordsController extends BaseController
30
{
31
32
33
    /**
34
     * Retrieves records from the call log.
35
     *
36
     * @return void
37
     */
38
    public function indexAction(): void
39
    {
40
    }
41
42
    /**
43
     * Get the date of the latest CDR record.
44
     * Used to set initial date for the date range picker.
45
     * 
46
     * @return void
47
     */
48
    public function getLatestRecordDateAction(): void
49
    {
50
        $parameters = [
51
            'columns' => 'MAX(start) as latest_date',
52
            'limit' => 1
53
        ];
54
        
55
        $result = $this->selectCDRRecordsWithFilters($parameters);
56
        $latestDate = null;
57
        
58
        if (!empty($result) && isset($result[0]['latest_date'])) {
59
            $latestDate = $result[0]['latest_date'];
60
        }
61
        
62
        $this->view->latestDate = $latestDate;
63
        $this->view->success = true;
64
    }
65
66
    /**
67
     * Requests new package of call history records for DataTable JSON.
68
     *
69
     * @return void
70
     * @throws \DateMalformedStringException
71
     */
72
    public function getNewRecordsAction(): void
73
    {
74
        $currentPage = $this->request->getPost('draw');
75
        $position = $this->request->getPost('start');
76
        $recordsPerPage = $this->request->getPost('length');
77
        $searchPhrase = $this->request->getPost('search');
78
        $this->view->draw = $currentPage;
79
        $this->view->recordsFiltered = 0;
80
        $this->view->data = [];
81
82
        $parameters = [];
83
        $parameters['columns'] = 'COUNT(DISTINCT(linkedid)) as rows';
84
        // Count the number of unique calls considering filters
85
        if (!empty($searchPhrase['value'])) {
86
            $this->prepareConditionsForSearchPhrases($searchPhrase['value'], $parameters);
87
            // If we couldn't understand the search phrase, return empty result
88
            if (empty($parameters['conditions'])) {
89
                return;
90
            }
91
        }
92
        $recordsFilteredReq = $this->selectCDRRecordsWithFilters($parameters);
93
        $this->view->recordsFiltered = $recordsFilteredReq[0]['rows'] ?? 0;
94
95
        // Find all LinkedIDs that match the specified filter
96
        $parameters['columns'] = 'DISTINCT(linkedid) as linkedid';
97
        $parameters['order'] = ['start desc'];
98
        $parameters['limit'] = $recordsPerPage;
99
        $parameters['offset'] = $position;
100
101
        $selectedLinkedIds = $this->selectCDRRecordsWithFilters($parameters);
102
        $arrIDS = [];
103
        foreach ($selectedLinkedIds as $item) {
104
            $arrIDS[] = $item['linkedid'];
105
        }
106
        if (empty($arrIDS)) {
107
            return;
108
        }
109
110
        // Retrieve all detailed records for processing and merging
111
        if (count($arrIDS) === 1) {
112
            $parameters = [
113
                'conditions' => 'linkedid = :ids:',
114
                'columns' => 'id, disposition, start, src_num, dst_num, billsec, recordingfile, did, dst_chan, linkedid, is_app, verbose_call_id',
115
                'bind' => [
116
                    'ids' => $arrIDS[0],
117
                ],
118
                'order' => ['linkedid desc', 'start asc', 'id asc'],
119
            ];
120
        } else {
121
            $parameters = [
122
                'conditions' => 'linkedid IN ({ids:array})',
123
                'columns' => 'id, disposition, start, src_num, dst_num, billsec, recordingfile, did, dst_chan, linkedid, is_app, verbose_call_id',
124
                'bind' => [
125
                    'ids' => $arrIDS,
126
                ],
127
                'order' => ['linkedid desc', 'start asc', 'id asc'],
128
            ];
129
        }
130
131
        $selectedRecords = $this->selectCDRRecordsWithFilters($parameters);
132
        $arrCdr = [];
133
        $objectLinkedCallRecord = (object)[
134
            'linkedid' => '',
135
            'disposition' => '',
136
            'start' => '',
137
            'src_num' => '',
138
            'dst_num' => '',
139
            'billsec' => 0,
140
            'answered' => [],
141
            'detail' => [],
142
            'ids' => [],
143
        ];
144
145
        foreach ($selectedRecords as $arrRecord) {
146
            $record = (object)$arrRecord;
147
            if (!array_key_exists($record->linkedid, $arrCdr)) {
148
                $arrCdr[$record->linkedid] = clone $objectLinkedCallRecord;
149
            }
150
            if ($record->is_app !== '1'
151
                && $record->billsec > 0
152
                && ($record->disposition === 'ANSWER' || $record->disposition === 'ANSWERED')) {
153
                $disposition = 'ANSWERED';
154
            } else {
155
                $disposition = 'NOANSWER';
156
            }
157
            $linkedRecord = $arrCdr[$record->linkedid];
158
            $linkedRecord->linkedid = $record->linkedid;
159
            $linkedRecord->disposition = $linkedRecord->disposition !== 'ANSWERED' ? $disposition : 'ANSWERED';
160
            $linkedRecord->start = $linkedRecord->start === '' ? $record->start : $linkedRecord->start;
161
            $linkedRecord->src_num = $linkedRecord->src_num === '' ? $record->src_num : $linkedRecord->src_num;
162
            if (!empty($record->did)) {
163
                $linkedRecord->dst_num = $record->did;
164
            } else {
165
                $linkedRecord->dst_num = $linkedRecord->dst_num === '' ? $record->dst_num : $linkedRecord->dst_num;
166
            }
167
            $linkedRecord->billsec += (int)$record->billsec;
168
            $isAppWithRecord = ($record->is_app === '1' && file_exists($record->recordingfile));
169
            if ($disposition === 'ANSWERED' || $isAppWithRecord) {
170
                $linkedRecord->answered[] = [
171
                    'id' => $record->id,
172
                    'src_num' => $record->src_num,
173
                    'dst_num' => $record->dst_num,
174
                    'recordingfile' => $record->recordingfile,
175
                ];
176
            }
177
            $linkedRecord->detail[] = $record;
178
            if (!empty($record->verbose_call_id)) {
179
                $linkedRecord->ids[] = $record->verbose_call_id;
180
            }
181
        }
182
        $output = [];
183
        foreach ($arrCdr as $cdr) {
184
            $timing = gmdate($cdr->billsec < 3600 ? 'i:s' : 'G:i:s', $cdr->billsec);
185
            $additionalClass = (empty($cdr->answered))?'ui':'detailed';
186
            $output[] = [
187
                date('d-m-Y H:i:s', strtotime($cdr->start)),
188
                $cdr->src_num,
189
                $cdr->dst_num,
190
                $timing === '00:00' ? '' : $timing,
191
                $cdr->answered,
192
                $cdr->disposition,
193
                'DT_RowId' => $cdr->linkedid,
194
                'DT_RowClass' => trim($additionalClass.' '.('NOANSWER' === $cdr->disposition ? 'negative' : '')),
195
                'ids' => rawurlencode(implode('&', array_unique($cdr->ids))),
196
            ];
197
        }
198
199
        $this->view->data = $output;
200
    }
201
202
    /**
203
     * Prepares query parameters for filtering CDR records.
204
     *
205
     * @param string $searchPhrase The search phrase entered by the user.
206
     * @param array $parameters The CDR query parameters.
207
     * @return void
208
     * @throws \DateMalformedStringException
209
     */
210
    private function prepareConditionsForSearchPhrases(string &$searchPhrase, array &$parameters): void
211
    {
212
        $parameters['conditions'] = '';
213
214
        // Search the linkedid, if we found it on the search string we will ignore all other parameters
215
        if (preg_match_all("/mikopbx-\d+.\d+/", $searchPhrase, $matches) && count($matches[0]) === 1) {
216
            $parameters['conditions'] = 'linkedid = :SearchPhrase:';
217
            $parameters['bind']['SearchPhrase'] = $matches[0][0];
218
219
            return;
220
        }
221
        
222
        // Store the original search phrase for employee name search
223
        $employeeNumbers = [];
224
225
        // Search date ranges
226
        if (preg_match_all("/\d{2}\/\d{2}\/\d{4}/", $searchPhrase, $matches)) {
227
            if (count($matches[0]) === 1) {
228
                $date = DateTime::createFromFormat('d/m/Y', $matches[0][0]);
229
                $requestedDate = $date->format('Y-m-d');
230
                $tomorrowDate = $date->modify('+1 day')->format('Y-m-d');
231
                $parameters['conditions'] .= 'start BETWEEN :dateFromPhrase1: AND :dateFromPhrase2:';
232
                $parameters['bind']['dateFromPhrase1'] = $requestedDate;
233
                $parameters['bind']['dateFromPhrase2'] = $tomorrowDate;
234
                $searchPhrase = str_replace($matches[0][0], "", $searchPhrase);
235
            } elseif (count($matches[0]) === 2) {
236
                $parameters['conditions'] .= 'start BETWEEN :dateFromPhrase1: AND :dateFromPhrase2:';
237
                $date = DateTime::createFromFormat('d/m/Y', $matches[0][0]);
238
                $requestedDate = $date->format('Y-m-d');
239
                $parameters['bind']['dateFromPhrase1'] = $requestedDate;
240
                $date = DateTime::createFromFormat('d/m/Y', $matches[0][1]);
241
                $tomorrowDate = $date->modify('+1 day')->format('Y-m-d');
242
                $parameters['bind']['dateFromPhrase2'] = $tomorrowDate;
243
                $searchPhrase = str_replace(
244
                    [$matches[0][0], $matches[0][1]],
245
                    '',
246
                    $searchPhrase
247
                );
248
            }
249
        }
250
        
251
        // Search phone numbers
252
        $searchPhrase = trim(str_replace(['(', ')', '-', '+'], '', $searchPhrase));
253
254
        // Look for employee name in the search phrase
255
        if (!empty($searchPhrase)) {
256
            $searchPhrase = mb_strtolower($searchPhrase, 'UTF-8');
257
            // Search for employee by name and get their numbers
258
            $extensionsParams = [
259
                'conditions' => 'search_index LIKE :SearchPhrase:',
260
                'bind' => [
261
                    'SearchPhrase' => "%$searchPhrase%"
262
                ],
263
                'columns' => 'number'
264
            ];
265
            
266
            $extensionsResult = Extensions::find($extensionsParams);
267
            foreach ($extensionsResult as $extension) {
268
                $employeeNumbers[] = $extension->number;
269
            }
270
        }
271
272
273
        if (preg_match_all("/\d+/", $searchPhrase, $matches)) {
274
            $needCloseAnd = false;
275
            $extensionsLength = PbxSettings::getValueByKey(PbxSettings::PBX_INTERNAL_EXTENSION_LENGTH);
276
            if ($parameters['conditions'] !== '') {
277
                $parameters['conditions'] .= ' AND (';
278
                $needCloseAnd = true;
279
            }
280
            if (count($matches[0]) === 1) {
281
                if ($extensionsLength == strlen($matches[0][0])) {
282
                    $parameters['conditions'] .= 'src_num = :SearchPhrase1: OR dst_num = :SearchPhrase2:';
283
                    $parameters['bind']['SearchPhrase1'] = $matches[0][0];
284
                    $parameters['bind']['SearchPhrase2'] = $matches[0][0];
285
                } else {
286
                    $seekNumber = substr($matches[0][0], -9);
287
                    $parameters['conditions'] .= 'src_num LIKE :SearchPhrase1: OR dst_num LIKE :SearchPhrase2: OR did LIKE :SearchPhrase3:';
288
                    $parameters['bind']['SearchPhrase1'] = "%$seekNumber%";
289
                    $parameters['bind']['SearchPhrase2'] = "%$seekNumber%";
290
                    $parameters['bind']['SearchPhrase3'] = "%$seekNumber%";
291
                }
292
293
                $searchPhrase = str_replace($matches[0][0], '', $searchPhrase);
294
            } elseif (count($matches[0]) === 2) {
295
                if ($extensionsLength == strlen($matches[0][0]) && $extensionsLength === strlen($matches[0][1])) {
296
                    $parameters['conditions'] .= '(src_num = :SearchPhrase1: AND dst_num = :SearchPhrase2:)';
297
                    $parameters['conditions'] .= ' OR (src_num = :SearchPhrase3: AND dst_num = :SearchPhrase4:)';
298
                    $parameters['conditions'] .= ' OR (src_num = :SearchPhrase5: AND did = :SearchPhrase6:)';
299
                    $parameters['conditions'] .= ' OR (src_num = :SearchPhrase8: AND did = :SearchPhrase7:)';
300
                    $parameters['bind']['SearchPhrase1'] = $matches[0][0];
301
                    $parameters['bind']['SearchPhrase2'] = $matches[0][1];
302
                    $parameters['bind']['SearchPhrase3'] = $matches[0][1];
303
                    $parameters['bind']['SearchPhrase4'] = $matches[0][0];
304
                    $parameters['bind']['SearchPhrase5'] = $matches[0][1];
305
                    $parameters['bind']['SearchPhrase6'] = $matches[0][0];
306
                    $parameters['bind']['SearchPhrase7'] = $matches[0][1];
307
                    $parameters['bind']['SearchPhrase8'] = $matches[0][0];
308
                } elseif ($extensionsLength == strlen($matches[0][0]) && $extensionsLength !== strlen(
309
                    $matches[0][1]
310
                )) {
311
                    $seekNumber = substr($matches[0][1], -9);
312
                    $parameters['conditions'] .= '(src_num = :SearchPhrase1: AND dst_num LIKE :SearchPhrase2:)';
313
                    $parameters['conditions'] .= ' OR (src_num LIKE :SearchPhrase3: AND dst_num = :SearchPhrase4:)';
314
                    $parameters['conditions'] .= ' OR (src_num LIKE :SearchPhrase5: AND did = :SearchPhrase6:)';
315
                    $parameters['conditions'] .= ' OR (src_num = :SearchPhrase8: AND did LIKE :SearchPhrase7:)';
316
                    $parameters['bind']['SearchPhrase1'] = $matches[0][0];
317
                    $parameters['bind']['SearchPhrase2'] = "%$seekNumber%";
318
                    $parameters['bind']['SearchPhrase3'] = "%$seekNumber%";
319
                    $parameters['bind']['SearchPhrase4'] = $matches[0][0];
320
                    $parameters['bind']['SearchPhrase5'] = "%$seekNumber%";
321
                    $parameters['bind']['SearchPhrase6'] = $matches[0][0];
322
                    $parameters['bind']['SearchPhrase7'] = "%$seekNumber%";
323
                    $parameters['bind']['SearchPhrase8'] = $matches[0][0];
324
                } elseif ($extensionsLength !== strlen($matches[0][0]) && $extensionsLength === strlen(
325
                    $matches[0][1]
326
                )) {
327
                    $seekNumber = substr($matches[0][0], -9);
328
                    $parameters['conditions'] .= '(src_num LIKE :SearchPhrase1: AND dst_num = :SearchPhrase2:)';
329
                    $parameters['conditions'] .= ' OR (src_num = :SearchPhrase3: AND dst_num LIKE :SearchPhrase4:)';
330
                    $parameters['conditions'] .= ' OR (src_num LIKE :SearchPhrase5: AND did = :SearchPhrase6:)';
331
                    $parameters['conditions'] .= ' OR (src_num = :SearchPhrase8: AND did LIKE :SearchPhrase7:)';
332
                    $parameters['bind']['SearchPhrase1'] = "%$seekNumber%";
333
                    $parameters['bind']['SearchPhrase2'] = $matches[0][1];
334
                    $parameters['bind']['SearchPhrase3'] = $matches[0][1];
335
                    $parameters['bind']['SearchPhrase4'] = "%$seekNumber%";
336
                    $parameters['bind']['SearchPhrase5'] = "%$seekNumber%";
337
                    $parameters['bind']['SearchPhrase6'] = $matches[0][1];
338
                    $parameters['bind']['SearchPhrase7'] = "%$seekNumber%";
339
                    $parameters['bind']['SearchPhrase8'] = $matches[0][1];
340
                } else {
341
                    $seekNumber0 = substr($matches[0][0], -9);
342
                    $seekNumber1 = substr($matches[0][1], -9);
343
                    $parameters['conditions'] .= '(src_num LIKE :SearchPhrase1: AND dst_num LIKE :SearchPhrase2:)';
344
                    $parameters['conditions'] .= ' OR (src_num LIKE :SearchPhrase3: AND dst_num LIKE :SearchPhrase4:)';
345
                    $parameters['conditions'] .= ' OR (src_num LIKE :SearchPhrase5: AND did LIKE :SearchPhrase6:)';
346
                    $parameters['conditions'] .= ' OR (src_num LIKE :SearchPhrase7: AND did LIKE :SearchPhrase8:)';
347
                    $parameters['bind']['SearchPhrase1'] = "%$seekNumber0%";
348
                    $parameters['bind']['SearchPhrase2'] = "%$seekNumber1%";
349
                    $parameters['bind']['SearchPhrase3'] = "%$seekNumber1%";
350
                    $parameters['bind']['SearchPhrase4'] = "%$seekNumber0%";
351
                    $parameters['bind']['SearchPhrase5'] = "%$seekNumber0%";
352
                    $parameters['bind']['SearchPhrase6'] = "%$seekNumber1%";
353
                    $parameters['bind']['SearchPhrase7'] = "%$seekNumber1%";
354
                    $parameters['bind']['SearchPhrase8'] = "%$seekNumber0%";
355
                }
356
                $searchPhrase = str_replace([$matches[0][0], $matches[0][1]], '', $searchPhrase);
357
            } elseif (count($matches[0]) > 2) {
358
                $searchPhrase = str_replace([' ', '  '], '', $searchPhrase);
359
                $parameters['conditions'] .= 'src_num = :SearchPhrase1: OR dst_num = :SearchPhrase2:';
360
                $parameters['bind']['SearchPhrase1'] = $searchPhrase;
361
                $parameters['bind']['SearchPhrase2'] = $searchPhrase;
362
            }
363
            if ($needCloseAnd) {
364
                $parameters['conditions'] .= ')';
365
            }
366
        } elseif (count($employeeNumbers) > 0) {
367
            // If no phone numbers were found in the search phrase but employee numbers were found
368
            $needCloseAnd = false;
369
            if ($parameters['conditions'] !== '') {
370
                $parameters['conditions'] .= ' AND (';
371
                $needCloseAnd = true;
372
            }
373
            
374
            // Add conditions for each employee number
375
            $employeeConditions = [];
376
            foreach ($employeeNumbers as $index => $number) {
377
                $employeeConditions[] = "src_num = :EmployeeNum{$index}src: OR dst_num = :EmployeeNum{$index}dst:";
378
                $parameters['bind']["EmployeeNum{$index}src"] = $number;
379
                $parameters['bind']["EmployeeNum{$index}dst"] = $number;
380
            }
381
            
382
            if (!empty($employeeConditions)) {
383
                $parameters['conditions'] .= '((' . implode(' OR ', $employeeConditions) . ') AND disposition = "ANSWERED")';
384
            }
385
            
386
            if ($needCloseAnd) {
387
                $parameters['conditions'] .= ')';
388
            }
389
        }
390
    }
391
392
    /**
393
     * Select CDR records with filters based on the provided parameters.
394
     *
395
     * @param array $parameters The parameters for filtering CDR records.
396
     * @return array The selected CDR records.
397
     */
398
    private function selectCDRRecordsWithFilters(array $parameters): array
399
    {
400
        // Apply ACL filters to CDR query using a hook method
401
        PBXConfModulesProvider::hookModulesMethod(CDRConfigInterface::APPLY_ACL_FILTERS_TO_CDR_QUERY, [&$parameters]);
402
403
        // Retrieve CDR records based on the filtered parameters
404
        return CDRDatabaseProvider::getCdr($parameters);
405
    }
406
}
407