Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 541
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Pipelines
0.00% covered (danger)
0.00%
0 / 541
0.00% covered (danger)
0.00%
0 / 13
20592
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 list_pipelines
0.00% covered (danger)
0.00%
0 / 235
0.00% covered (danger)
0.00%
0 / 1
4970
 create_pipeline
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 update_pipeline
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 build_pipelines_table_filters
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
420
 list_pipelines_table
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 1
600
 delete_pipelines
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 get_dates
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 get_all_users
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
 get_pipeline
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 get_distincts
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
12
 get_past_added_pipelines
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 download_pipelines
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Exceptions\AppException;
6use App\Models\TblCompanyUsers;
7use App\Models\TblPipelines;
8use App\Services\ResultCache;
9use App\Services\UserCompanies;
10use Illuminate\Contracts\Routing\ResponseFactory;
11use Illuminate\Http\Request;
12use Illuminate\Http\Response;
13use Illuminate\Support\Facades\App;
14use Illuminate\Support\Facades\Cache;
15use Illuminate\Support\Facades\DB;
16
17class Pipelines extends Controller
18{
19    private $locale;
20
21    private $userId;
22
23    private $region;
24
25    private $companyIds;
26
27    private $companyId;
28
29    public function __construct()
30    {
31        $this->locale = request()->header('Locale-Id');
32        $this->userId = request()->header('User-Id');
33        $this->region = request()->header('Region');
34
35        App::setLocale($this->locale);
36
37        $this->companyIds = [];
38
39        if ($this->region != null && $this->region != '' && $this->region != 'All') {
40            $this->region = urldecode((string) $this->region);
41
42            $query = 'SELECT 
43                        b.company_id
44                    FROM 
45                        tbl_company_users a 
46                        LEFT JOIN tbl_companies b ON a.company_id = b.company_id 
47                    WHERE 
48                        a.user_id = ?
49                        AND b.region = ?';
50
51            $this->companyIds = DB::select($query, [intval($this->userId), $this->region]);
52
53            $this->companyIds = collect($this->companyIds)->pluck('company_id')->toArray();
54        } else {
55            // FIRE-1146: prefer the middleware-resolved attribute; fall back to a fresh fetch only on unattached paths.
56            $this->companyIds = request()->attributes->get('user_company_ids', UserCompanies::forUser((int) $this->userId));
57        }
58
59        $this->companyId = implode(',', $this->companyIds);
60    }
61
62    public function list_pipelines(Request $request): ResponseFactory|Response
63    {
64
65        try {
66
67            $data = $request->all();
68            $companyId = addslashes((string) $data['company_id']);
69            $userId = addslashes((string) $data['user_id']);
70            $filter = $data['filterModel'];
71            $sort = $data['sortModel'];
72            $result = [];
73            $subquery = '';
74            $where = '';
75            $having = '';
76            $orderBy = '';
77            $start = addslashes((string) $data['start']);
78            $end = addslashes((string) $data['end']);
79            $totalRowCount = 0;
80            $withFilters = '';
81
82            $filterType = [
83                'contains' => "LIKE '%[value]%'",
84                'notContains' => "NOT LIKE '%[value]%'",
85                'equals' => "= '[value]'",
86                'notEqual' => "<> '[value]'",
87                'startsWith' => "LIKE '[value]%'",
88                'endsWith' => "LIKE '%[value]'",
89                'blank' => 'IS NULL',
90                'notBlank' => 'IS NOT NULL',
91                'lessThan' => '< [value]',
92                'lessThanOrEqual' => '<= [value]',
93                'greaterThan' => '> [value]',
94                'greaterThanOrEqual' => '>= [value]',
95                'inRange' => 'BETWEEN [value1] AND [value2]',
96            ];
97
98            if (isset($data['ids']) && count($data['ids']) > 0) {
99                $ids = implode(',', $data['ids']);
100                $where = " AND a.id IN ({$ids}";
101            }
102
103            if (isset($data['ids_not_in']) && count($data['ids_not_in']) > 0) {
104                $ids = implode(',', $data['ids_not_in']);
105                $where = " AND a.id NOT IN ({$ids}";
106            }
107
108            if ($companyId != 0) {
109                $where .= " AND a.company_id = {$companyId} ";
110            } else {
111                $where .= " AND a.company_id IN ({$this->companyId}";
112            }
113
114            $matchScoreCol = '';
115            $matchScoreOrderBy = '';
116
117            if (isset($data['searchText']) && $data['searchText'] != null) {
118
119                $availableParameters = [
120                    'a.client_name',
121                    'a.client_type',
122                    'a.commercial',
123                    'a.location',
124                    'a.visit_type_id',
125                    'a.visit_date',
126                    'a.opportunity_type',
127                    'a.comments',
128                    'a.visit_call',
129                    'a.created_by',
130                    'a.created_at',
131                    'a.updated_by',
132                    'a.updated_at',
133                ];
134
135                $searchText = addslashes((string) $data['searchText']);
136                $searchTextArray = explode(' ', $searchText);
137
138                $searchArray = [];
139                $splitSearchArray = [];
140                $matchScoreArray = [];
141                $sc = 1;
142                foreach ($availableParameters as $field) {
143                    if ($field == 'a.client_name' || $field == 'a.created_at') {
144                        $sc = 3;
145                    } elseif ($field == 'a.visit_date') {
146                        $sc = 2;
147                    } else {
148                        $sc = 1;
149                    }
150
151                    $l = "{$field} LIKE '%{$searchText}%'";
152
153                    $d = "IFNULL((LENGTH(LOWER({$field})) - LENGTH(REPLACE(LOWER({$field}), LOWER('{$searchText}'), ''))) / LENGTH(LOWER('{$searchText}')), 0) * {$sc}";
154
155                    if (count($searchTextArray) > 1) {
156                        foreach ($searchTextArray as $word) {
157                            if (! is_numeric($word)) {
158                                $d .= " + IFNULL((LENGTH(LOWER({$field})) - LENGTH(REPLACE(LOWER({$field}), LOWER('{$word}'), ''))) / LENGTH(LOWER('{$word}')), 0) * {$sc}";
159                            }
160                        }
161                    }
162
163                    array_push($matchScoreArray, $d);
164
165                    if (is_numeric($searchText)) {
166                        array_push($searchArray, "({$l} OR {$field} = CAST('{$searchText}' AS UNSIGNED))");
167                    } else {
168                        array_push($searchArray, "({$l} OR DATE_FORMAT({$field}, '%d/%m/%Y') = DATE_FORMAT(STR_TO_DATE('{$searchText}', '%d/%m/%Y'), '%d/%m/%Y'))");
169                    }
170
171                    if (count($searchTextArray) > 1) {
172                        foreach ($searchTextArray as $word) {
173
174                            $l = "{$field} LIKE '%{$word}%'";
175
176                            if (is_numeric($word)) {
177                                array_push($splitSearchArray, "{$l} OR {$field} = CAST('{$word}' AS UNSIGNED)");
178                            } else {
179                                array_push($splitSearchArray, "{$l} OR DATE_FORMAT({$field}, '%d/%m/%Y') = DATE_FORMAT(STR_TO_DATE('{$word}', '%d/%m/%Y'), '%d/%m/%Y')");
180                            }
181                        }
182                    }
183
184                    $sc = 1;
185                }
186
187                if (count($splitSearchArray) > 0) {
188                    $splitSearchArray = implode(' OR ', $splitSearchArray);
189                    $splitSearchArray = " OR ({$splitSearchArray}";
190                } else {
191                    $splitSearchArray = '';
192                }
193
194                $searchArray = implode(' OR ', $searchArray);
195                $matchScoreArray = implode(',', $matchScoreArray);
196                $matchScoreCol = ", GREATEST({$matchScoreArray}) match_score";
197                $matchScoreOrderBy = 'match_score DESC,';
198                $where .= " AND ({$searchArray} {$splitSearchArray})";
199            }
200
201            if (count($sort) > 0) {
202                $field = $sort[0]['colId'];
203                $sortBy = $sort[0]['sort'];
204
205                if (str_contains((string) $field, 'translate')) {
206                    $field = str_replace('_translate', '', $field);
207                }
208
209                if ($matchScoreOrderBy) {
210                    $matchScoreOrderBy = ', match_score DESC';
211                }
212
213                $orderBy = " ORDER BY {$field} {$sortBy} {$matchScoreOrderBy}";
214            } else {
215                $orderBy = " ORDER BY {$matchScoreOrderBy} a.id DESC";
216            }
217
218            foreach ($filter as $key => $data) {
219                if (str_contains((string) $key, 'translate')) {
220
221                    $field = str_replace('_translate', '', $key);
222
223                    if ($field == 'created_at') {
224                        $field = 'a.created_at';
225                    } elseif ($field == 'updated_at') {
226                        $field = 'a.updated_at';
227                    } elseif ($field == 'visit_date') {
228                        $field = 'a.visit_date';
229                    }
230
231                    $whereDates = '';
232                    $z = 0;
233
234                    if (isset($data['filters']) && ! empty($data['filters'])) {
235                        foreach ($data['filters'] as $yearKey => $yearData) {
236                            $yearsMonths = [];
237                            $yearsWeeks = [];
238
239                            if ($z > 0) {
240                                $whereDates .= " OR (YEAR($field) = {$yearKey} ";
241                            } else {
242                                $whereDates .= " (YEAR($field) = {$yearKey} ";
243                            }
244
245                            for ($i = 0; $i < count($yearData['months']); $i++) {
246                                if ($yearData['months'][$i]['isChecked']) {
247                                    array_push($yearsMonths, $yearData['months'][$i]['value']);
248                                }
249                            }
250
251                            $yearsMonths = implode("','", $yearsMonths);
252                            $whereDates .= " AND (MONTH({$field}) IN ('{$yearsMonths}')";
253
254                            for ($i = 0; $i < count($yearData['weeks']); $i++) {
255                                if ($yearData['weeks'][$i]['isChecked']) {
256                                    array_push($yearsWeeks, $yearData['weeks'][$i]['value']);
257                                }
258                            }
259
260                            $yearsWeeks = implode("','", $yearsWeeks);
261                            if ($yearsWeeks != '') {
262                                $whereDates .= " OR WEEK({$field}) IN ('{$yearsWeeks}') ";
263                            }
264
265                            $whereDates .= ')) ';
266                            $z++;
267                        }
268                    }
269
270                    $whereBlanks = '';
271                    if (isset($data['isBlanks'])) {
272                        if ($data['isBlanks']) {
273                            $conj = 'OR';
274                            if ($whereDates == '') {
275                                $conj = '';
276                            }
277                            $whereBlanks .= " {$conj} {$field} IS NULL ";
278                        } else {
279                            $conj = 'AND';
280                            if ($whereDates == '') {
281                                $conj = '';
282                            }
283                            $whereBlanks .= " {$conj} {$field} IS NOT NULL ";
284                        }
285                    }
286
287                    $where .= " AND ({$whereDates} {$whereBlanks}";
288                } else {
289                    if ($data['filterType'] == 'number') {
290                        if (array_key_exists('operator', $data)) {
291                            if ($data['condition1']['type'] != 'blank' && $data['condition2']['type'] != 'notBlank') {
292                                $data['condition1']['filter'] = addslashes((string) $data['condition1']['filter']);
293                                $data['condition2']['filter'] = addslashes((string) $data['condition2']['filter']);
294
295                                if ($data['condition1']['type'] == 'inRange') {
296                                    $data['condition1']['filterTo'] = addslashes((string) $data['condition1']['filterTo']);
297                                    $inRange = str_replace('[value1]', $data['condition1']['filter'], $filterType['inRange']);
298                                    $val1 = str_replace('[value2]', $data['condition1']['filterTo'], $inRange);
299                                } else {
300                                    $val1 = str_replace('[value]', $data['condition1']['filter'], $filterType[$data['condition1']['type']]);
301                                }
302
303                                if ($data['condition2']['type'] == 'inRange') {
304                                    $data['condition2']['filterTo'] = addslashes((string) $data['condition2']['filterTo']);
305                                    $inRange = str_replace('[value1]', $data['condition2']['filter'], $filterType['inRange']);
306                                    $val2 = str_replace('[value2]', $data['condition2']['filterTo'], $inRange);
307                                } else {
308                                    $val2 = str_replace('[value]', $data['condition2']['filter'], $filterType[$data['condition2']['type']]);
309                                }
310
311                            } else {
312                                $val1 = $filterType[$data['condition1']['type']];
313                                $val2 = $filterType[$data['condition2']['type']];
314                            }
315
316                            $where .= " AND a.{$key} {$val1} {$data['operator']} a.{$key} {$val2} ";
317                        } else {
318                            if ($data['type'] != 'blank' && $data['type'] != 'notBlank') {
319                                $data['filter'] = addslashes((string) $data['filter']);
320
321                                if ($data['type'] == 'inRange') {
322                                    $data['filterTo'] = addslashes((string) $data['filterTo']);
323                                    $inRange = str_replace('[value1]', $data['filter'], $filterType['inRange']);
324                                    $val = str_replace('[value2]', $data['filterTo'], $inRange);
325                                } else {
326                                    $val = str_replace('[value]', $data['filter'], $filterType[$data['type']]);
327                                }
328                            } else {
329                                $val = $filterType[$data['type']];
330                            }
331
332                            $where .= " AND a.{$key} {$val} ";
333                        }
334                    }
335
336                    if ($data['filterType'] == 'text') {
337                        if (array_key_exists('operator', $data)) {
338                            if ($data['condition1']['type'] != 'blank' && $data['condition2']['type'] != 'notBlank') {
339                                $data['condition1']['filter'] = addslashes((string) $data['condition1']['filter']);
340                                $val1 = str_replace('[value]', $data['condition1']['filter'], $filterType[$data['condition1']['type']]);
341                            }
342
343                            if ($data['condition2']['type'] != 'blank' && $data['condition2']['type'] != 'notBlank') {
344                                $data['condition2']['filter'] = addslashes((string) $data['condition2']['filter']);
345                                $val2 = str_replace('[value]', $data['condition2']['filter'], $filterType[$data['condition2']['type']]);
346                            }
347
348                            $where .= " AND {$key} {$val1} {$data['operator']} {$key} {$val2} ";
349                        } else {
350                            if ($data['type'] != 'blank' && $data['type'] != 'notBlank') {
351                                $data['filter'] = addslashes((string) $data['filter']);
352                                $val = str_replace('[value]', $data['filter'], $filterType[$data['type']]);
353                            } else {
354                                $val = $filterType[$data['type']];
355                            }
356
357                            $where .= " AND {$key} {$val} ";
358                        }
359                    }
360
361                    if ($data['filterType'] == 'set') {
362                        $statusName = $key;
363
364                        if ($key == 'updated_by') {
365                            $statusName = 'a.updated_by';
366                        } elseif ($key == 'company_name') {
367                            $statusName = 'b.name';
368                        } elseif ($key == 'commercial') {
369                            $statusName = 'a.commercial';
370                        } elseif ($key == 'created_by') {
371                            $statusName = 'a.created_by';
372                        } elseif ($key == 'client_type') {
373                            $statusName = 'a.client_type';
374                        } elseif ($key == 'visit_type') {
375                            $statusName = 'a.visit_type';
376                        } elseif ($key == 'opportunity_type') {
377                            $statusName = 'a.opportunity_type';
378                        }
379
380                        $val = implode("','", $data['values']);
381
382                        if (in_array(null, $data['values'], true)) {
383                            $where .= " AND ({$statusName} IN ('{$val}') OR {$statusName} IS NULL) ";
384                        } else {
385                            $where .= " AND {$statusName} IN ('{$val}') ";
386                        }
387                    }
388                }
389            }
390
391            $offset = $start;
392            $limit = $end - $start;
393
394            $query = "SELECT 
395                        a.id,
396                        b.company_id,
397                        b.region,
398                        b.name company_name,
399                        a.client_name, 
400                        a.client_type,
401                        v.visit_type_id,
402                        v.name visit_type,
403                        vtg.name visit_type_group,
404                        vtg.visit_type_group_id,
405                        a.commercial,
406                        a.location, 
407                        a.visit_date,
408                        DATE_FORMAT(a.visit_date, '%d/%m/%Y') visit_date_translate,
409                        a.opportunity_type,
410                        a.comments,
411                        a.visit_call,
412                        a.campaign,
413                        a.contract_end_date,
414                        a.potential_amount,
415                        a.created_by,
416                        a.created_at,
417                        a.updated_by,
418                        a.updated_at
419                        {$matchScoreCol}
420                    FROM 
421                        tbl_pipelines a 
422                        LEFT JOIN tbl_companies b ON a.company_id = b.company_id
423                        LEFT JOIN tbl_visit_types v ON a.visit_type_id = v.visit_type_id
424                        LEFT JOIN tbl_visit_type_groups vtg ON v.visit_type_group_id = vtg.visit_type_group_id
425                    WHERE a.id > 0 
426                    {$where}
427                    {$orderBy}
428                    LIMIT {$offset}{$limit}
429                    ";
430
431            $value = Cache::get(base64_encode($query));
432
433            if (! $value) {
434                $result = DB::select($query);
435
436                Cache::put(base64_encode($query), $result, 600);
437            } else {
438                $result = $value;
439            }
440
441            $totalQuery = "SELECT 
442                            COUNT(a.id) totalRowCount
443                        FROM 
444                            tbl_pipelines a 
445                        WHERE a.id > 0
446                        {$where}";
447
448            $value = Cache::get(base64_encode($totalQuery));
449
450            if (! $value) {
451                $countQuery = DB::select($totalQuery);
452
453                Cache::put(base64_encode($totalQuery), $countQuery, 600);
454            } else {
455                $countQuery = $value;
456            }
457
458            return response([
459                'message' => 'OK',
460                'data' => $result,
461                'totalRowCount' => $countQuery[0]->totalRowCount,
462            ]);
463
464        } catch (\Exception $e) {
465            report(AppException::fromException($e, 'LIST_PIPELINES_EXCEPTION'));
466
467            return response(['message' => 'KO', 'error' => $e->getMessage()]);
468        }
469
470    }
471
472    public function create_pipeline(Request $request): ResponseFactory|Response
473    {
474
475        try {
476
477            $data = $request->all();
478
479            $result = TblPipelines::create($data);
480
481            // FIRE-1145: was Cache::flush() â€” create_pipeline affects list_pipelines.
482            ResultCache::forgetDomain('pipelines');
483
484            return response(['message' => 'OK', 'data' => $result]);
485
486        } catch (\Exception $e) {
487            report(AppException::fromException($e, 'CREATE_PIPELINE_EXCEPTION'));
488
489            return response(['message' => 'KO', 'error' => $e->getMessage()]);
490        }
491    }
492
493    public function update_pipeline(Request $request, $id): ResponseFactory|Response
494    {
495
496        try {
497
498            $data = $request->all();
499            $id = addslashes((string) $id);
500
501            $data['updated_at'] = date('Y-m-d H:i:s');
502            TblPipelines::where('id', $id)->update($data);
503
504            // FIRE-1145: was Cache::flush() â€” update_pipeline affects list_pipelines.
505            ResultCache::forgetDomain('pipelines');
506
507            return response([
508                'message' => 'OK',
509            ]);
510
511        } catch (\Exception $e) {
512            report(AppException::fromException($e, 'UPDATE_PIPELINE_EXCEPTION'));
513
514            return response(['message' => 'KO', 'error' => $e->getMessage()]);
515        }
516    }
517
518    /**
519     * Advanced Search â†’ SQL WHERE for the pipelines table. Mirrors
520     * Quotations::build_orders_table_filters: one AND-ed clause per field, by
521     * data type (set / text / date). The field => SQL expression map matches
522     * the list_pipelines SELECT aliases (a = tbl_pipelines, b = tbl_companies,
523     * v = tbl_visit_types).
524     */
525    private function build_pipelines_table_filters($filterModel): string
526    {
527        if (! is_array($filterModel) || empty($filterModel)) {
528            return '';
529        }
530
531        $colMap = [
532            'id' => 'a.id',
533            'client_name' => 'a.client_name',
534            'client_type' => 'a.client_type',
535            'commercial' => 'a.commercial',
536            'visit_call' => 'a.visit_call',
537            'campaign' => 'a.campaign',
538            'location' => 'a.location',
539            'opportunity_type' => 'a.opportunity_type',
540            'company_name' => 'b.name',
541            'region' => 'b.region',
542            'visit_type' => 'v.name',
543            'visit_date' => 'a.visit_date',
544        ];
545
546        $w = '';
547
548        foreach ($filterModel as $field => $f) {
549            if (! is_array($f) || empty($f['type']) || ! isset($colMap[$field])) {
550                continue;
551            }
552            $type = $f['type'];
553            $col = $colMap[$field];
554
555            if ($type === 'set') {
556                $nonBlank = [];
557                $hasBlank = false;
558                foreach ((array) ($f['values'] ?? []) as $v) {
559                    if ($v === null || $v === '' || $v === '__BLANK__') {
560                        $hasBlank = true;
561                    } else {
562                        $nonBlank[] = addslashes((string) $v);
563                    }
564                }
565                $clauses = [];
566                if ($nonBlank) {
567                    $clauses[] = "{$col} IN ('".implode("','", $nonBlank)."')";
568                }
569                if ($hasBlank) {
570                    $clauses[] = "({$col} IS NULL OR {$col} = '')";
571                }
572                if ($clauses) {
573                    $w .= ' AND ('.implode(' OR ', $clauses).')';
574                }
575            } elseif ($type === 'text') {
576                $val = trim((string) ($f['contains'] ?? ''));
577                if ($val !== '') {
578                    $val = addslashes($val);
579                    $w .= " AND {$col} LIKE '%{$val}%'";
580                }
581            } elseif ($type === 'date') {
582                $from = trim((string) ($f['from'] ?? ''));
583                $to = trim((string) ($f['to'] ?? ''));
584                if ($from !== '') {
585                    $w .= " AND DATE({$col}) >= '".addslashes($from)."'";
586                }
587                if ($to !== '') {
588                    $w .= " AND DATE({$col}) <= '".addslashes($to)."'";
589                }
590            }
591        }
592
593        return $w;
594    }
595
596    /**
597     * Data-table (Advanced Search) listing for Pipelines â€” mirrors
598     * Quotations::list_orders_table. Server-side pagination (start/end), sort,
599     * a simple OR-LIKE quick search, explicit ids / ids_not_in, and the
600     * orders-style filter model parsed by build_pipelines_table_filters. Pass
601     * ids_only=1 to return just the matching id list (+ count) for select-all.
602     * New endpoint â€” list_pipelines (the ag-grid one) is left untouched.
603     */
604    public function list_pipelines_table(Request $request): ResponseFactory|Response
605    {
606        try {
607            $data = $request->all();
608
609            $companyId = (int) ($data['company_id'] ?? 0);
610            $start = max(0, (int) ($data['start'] ?? 0));
611            $limit = (int) ($data['end'] ?? 300) - $start;
612            $limit = $limit > 0 ? $limit : 300;
613
614            // FIRE-1145 + FIRE-1174: read through the domain-tagged ResultCache
615            // (invalidated by ResultCache::forgetDomain('pipelines') on every
616            // create/update/delete). Gate the 10-min cache to avoid per-keystroke
617            // churn â€” cache the cold "no search" path and stable searches
618            // (>= 3 chars); skip the noisy 1-2 char prefixes.
619            $cacheSearch = trim((string) ($data['searchText'] ?? ''));
620            $shouldCacheList = $cacheSearch === '' || strlen($cacheSearch) >= 3;
621
622            $where = '';
623            if ($companyId != 0) {
624                $where .= " AND a.company_id = {$companyId} ";
625            } else {
626                $where .= " AND a.company_id IN ({$this->companyId}";
627            }
628
629            // Explicit selection (hand-picked delete/download) + select-all
630            // exclusions.
631            if (isset($data['ids']) && is_array($data['ids']) && count($data['ids']) > 0) {
632                $numericIds = array_values(array_filter(array_map('intval', $data['ids']), fn ($n) => $n > 0));
633                if (count($numericIds) > 0) {
634                    $where .= ' AND a.id IN ('.implode(',', $numericIds).') ';
635                }
636            }
637            if (isset($data['ids_not_in']) && is_array($data['ids_not_in']) && count($data['ids_not_in']) > 0) {
638                $numericIds = array_values(array_filter(array_map('intval', $data['ids_not_in']), fn ($n) => $n > 0));
639                if (count($numericIds) > 0) {
640                    $where .= ' AND a.id NOT IN ('.implode(',', $numericIds).') ';
641                }
642            }
643
644            // Quick search â€” OR LIKE across the meaningful fields.
645            if (isset($data['searchText']) && trim((string) $data['searchText']) !== '') {
646                $searchText = addslashes(trim((string) $data['searchText']));
647                $searchFields = [
648                    'a.id', 'b.name', 'a.client_name', 'a.client_type', 'a.commercial',
649                    'a.location', 'v.name', 'a.opportunity_type', 'a.comments',
650                    'a.visit_call', 'a.campaign', 'b.region',
651                ];
652                $likes = array_map(fn ($f) => "{$f} LIKE '%{$searchText}%'", $searchFields);
653                $where .= ' AND ('.implode(' OR ', $likes).') ';
654            }
655
656            // Advanced Search filter model (orders-style).
657            $where .= $this->build_pipelines_table_filters($data['filterModel'] ?? []);
658
659            $fromJoins = '
660                FROM tbl_pipelines a
661                LEFT JOIN tbl_companies b ON a.company_id = b.company_id
662                LEFT JOIN tbl_visit_types v ON a.visit_type_id = v.visit_type_id
663                LEFT JOIN tbl_visit_type_groups vtg ON v.visit_type_group_id = vtg.visit_type_group_id';
664
665            // ids_only mode â†’ every matching id (for header select-all-matching).
666            if (! empty($data['ids_only'])) {
667                $idsQuery = "SELECT a.id {$fromJoins} WHERE a.id > 0 {$where} ORDER BY a.id DESC";
668                $idRows = $shouldCacheList
669                    ? ResultCache::remember('pipelines', $idsQuery, 600, fn () => DB::select($idsQuery))
670                    : DB::select($idsQuery);
671                $ids = array_map(fn ($r) => (int) $r->id, $idRows);
672
673                return response([
674                    'message' => 'OK',
675                    'ids' => $ids,
676                    'totalRowCount' => count($ids),
677                ]);
678            }
679
680            // Sort.
681            $orderBy = ' ORDER BY a.id DESC';
682            if (isset($data['sortModel']) && is_array($data['sortModel']) && count($data['sortModel']) > 0) {
683                $field = str_replace('_translate', '', (string) ($data['sortModel'][0]['colId'] ?? ''));
684                $dir = strtolower((string) ($data['sortModel'][0]['sort'] ?? 'asc')) === 'desc' ? 'DESC' : 'ASC';
685                $sortMap = [
686                    'id' => 'a.id', 'company_name' => 'b.name', 'region' => 'b.region',
687                    'client_name' => 'a.client_name', 'client_type' => 'a.client_type',
688                    'commercial' => 'a.commercial', 'visit_call' => 'a.visit_call',
689                    'campaign' => 'a.campaign', 'visit_type' => 'v.name',
690                    'visit_date' => 'a.visit_date', 'location' => 'a.location',
691                    'opportunity_type' => 'a.opportunity_type',
692                ];
693                if (isset($sortMap[$field])) {
694                    $orderBy = " ORDER BY {$sortMap[$field]} {$dir}";
695                }
696            }
697
698            $query = "SELECT
699                        a.id,
700                        b.company_id,
701                        b.region,
702                        b.name company_name,
703                        a.client_name,
704                        a.client_type,
705                        v.visit_type_id,
706                        v.name visit_type,
707                        vtg.name visit_type_group,
708                        vtg.visit_type_group_id,
709                        a.commercial,
710                        a.location,
711                        a.visit_date,
712                        DATE_FORMAT(a.visit_date, '%d/%m/%Y') visit_date_translate,
713                        a.opportunity_type,
714                        a.comments,
715                        a.visit_call,
716                        a.campaign,
717                        a.contract_end_date,
718                        a.potential_amount,
719                        a.created_by,
720                        a.created_at,
721                        a.updated_by,
722                        a.updated_at
723                    {$fromJoins}
724                    WHERE a.id > 0
725                    {$where}
726                    {$orderBy}
727                    LIMIT {$start}{$limit}";
728
729            $result = $shouldCacheList
730                ? ResultCache::remember('pipelines', $query, 600, fn () => DB::select($query))
731                : DB::select($query);
732
733            $totalQuery = "SELECT COUNT(a.id) totalRowCount {$fromJoins} WHERE a.id > 0 {$where}";
734            $countQuery = $shouldCacheList
735                ? ResultCache::remember('pipelines', $totalQuery, 600, fn () => DB::select($totalQuery))
736                : DB::select($totalQuery);
737
738            return response([
739                'message' => 'OK',
740                'data' => $result,
741                'totalRowCount' => $countQuery[0]->totalRowCount,
742            ]);
743
744        } catch (\Exception $e) {
745            report(AppException::fromException($e, 'LIST_PIPELINES_TABLE_EXCEPTION'));
746
747            return response(['message' => 'KO', 'error' => $e->getMessage()]);
748        }
749    }
750
751    public function delete_pipelines(Request $request): ResponseFactory|Response
752    {
753
754        try {
755
756            $data = $request->all();
757            $result = [];
758
759            $r = new Request([
760                'filterModel' => $data['filterModel'],
761                'sortModel' => $data['sortModel'],
762                'start' => 0,
763                'end' => 999999999,
764                'company_id' => @$data['company_id'],
765                'user_id' => $data['user_id'],
766                'ids' => $data['ids'],
767                'searchText' => $data['searchText'],
768                'ids_not_in' => $data['ids_not_in'],
769            ]);
770
771            $result = $this->list_pipelines_table($r);
772            $result = $result->original['data'];
773
774            $outputArray = [];
775
776            foreach ($result as $item) {
777                if (isset($item->id)) {
778                    $outputArray[] = $item->id;
779                }
780            }
781
782            TblPipelines::whereIn('id', $outputArray)->delete();
783
784            // FIRE-1145: was Cache::flush() â€” delete_pipelines affects list_pipelines.
785            ResultCache::forgetDomain('pipelines');
786
787            return response(['message' => 'OK', 'data' => $result]);
788
789        } catch (\Exception $e) {
790            report(AppException::fromException($e, 'DELETE_PIPELINES_EXCEPTION'));
791
792            return response(['message' => 'KO', 'error' => $e->getMessage()]);
793        }
794
795    }
796
797    public function get_dates(): ResponseFactory|Response
798    {
799
800        try {
801
802            $where = '';
803
804            if ($companyId != 0) {
805                $where .= " a.company_id = {$companyId} ";
806            } else {
807                $where .= " a.company_id IN ({$this->companyId}";
808            }
809
810            $query = "SELECT
811                        DATE_FORMAT(a.visit_date, '%d/%m/%Y') visit_date_translate,
812                        DATE_FORMAT(a.created_at, '%d/%m/%Y') created_at_translate,
813                        DATE_FORMAT(a.updated_at, '%d/%m/%Y') updated_at_translate
814                    FROM tbl_pipelines a
815                    WHERE {$where}";
816
817            $result = DB::select($query);
818
819            return response([
820                'message' => 'OK',
821                'data' => $result,
822            ]);
823
824        } catch (\Exception $e) {
825            report(AppException::fromException($e, 'GET_DATES_EXCEPTION'));
826
827            return response(['message' => 'KO', 'error' => $e->getMessage()]);
828        }
829
830    }
831
832    public function get_all_users($companyId): ResponseFactory|Response
833    {
834
835        try {
836
837            $companyId = addslashes((string) $companyId);
838
839            $companyId = intval($companyId);
840
841            $where = '';
842
843            if ($companyId != 0) {
844                $where = "WHERE company_id = {$companyId} ";
845            } else {
846                $where = "WHERE company_id IN ({$this->companyId}";
847            }
848
849            $query = "SELECT 
850                        DISTINCT created_by 
851                    FROM 
852                        tbl_pipelines
853                    {$where}
854                    ORDER BY 
855                        created_by ASC";
856
857            $createdBy = DB::select($query);
858
859            $query = "SELECT 
860                        DISTINCT commercial 
861                    FROM 
862                        tbl_pipelines
863                    {$where}
864                    ORDER BY 
865                        commercial ASC";
866
867            $commercial = DB::select($query);
868
869            return response([
870                'message' => 'OK',
871                'createdBy' => $createdBy,
872                'commercial' => $commercial,
873            ]);
874
875        } catch (\Exception $e) {
876            report(AppException::fromException($e, 'GET_ALL_USERS_EXCEPTION'));
877
878            return response(['message' => 'KO', 'error' => $e->getMessage()]);
879        }
880    }
881
882    public function get_pipeline($id): ResponseFactory|Response
883    {
884
885        try {
886
887            $id = addslashes((string) $id);
888
889            $query = "SELECT 
890                        a.id,
891                        a.client_name,
892                        a.client_type,
893                        a.commercial,
894                        a.location,
895                        a.opportunity_type,
896                        a.visit_type,
897                        a.visit_date,
898                        a.visit_call,
899                        a.campaign,
900                        a.contract_end_date,
901                        a.potential_amount,
902                        a.created_by,
903                        a.created_at,
904                        a.updated_by,
905                        a.updated_at,                        
906                        DATE_FORMAT(a.visit_date, '%d/%m/%Y') last_itv_date_translate,
907                        DATE_FORMAT(a.created_at, '%d/%m/%Y') created_at_translate,
908                        DATE_FORMAT(a.updated_at, '%d/%m/%Y') updated_at_translate,
909                        a.comments
910                    FROM 
911                        tbl_pipelines a 
912                    WHERE a.id = {$id}";
913
914            $result = DB::select($query);
915
916            // FIRE-1145: was Cache::flush() â€” read-only path, zero benefit. Removed.
917
918            return response(['message' => 'OK', 'data' => $result]);
919
920        } catch (\Exception $e) {
921            report(AppException::fromException($e, 'GET_PIPELINE_EXCEPTION'));
922
923            return response(['message' => 'KO', 'error' => $e->getMessage()]);
924        }
925    }
926
927    public function get_distincts($companyId): ResponseFactory|Response
928    {
929
930        try {
931
932            $where = '';
933
934            if ($companyId != 0) {
935                $where .= " a.company_id = {$companyId} ";
936            } else {
937                $where .= " a.company_id IN ({$this->companyId}";
938            }
939
940            $query = "SELECT DISTINCT a.campaign FROM tbl_pipelines a WHERE {$where} ORDER BY a.campaign ASC";
941            $campaign = DB::select($query);
942
943            $query = "SELECT DISTINCT a.client_type FROM tbl_pipelines a WHERE {$where} ORDER BY a.client_type ASC";
944            $clientType = DB::select($query);
945
946            $query = "SELECT DISTINCT a.visit_call FROM tbl_pipelines a WHERE {$where} ORDER BY a.visit_call ASC";
947            $visitCall = DB::select($query);
948
949            $query = "SELECT DISTINCT a.commercial FROM tbl_pipelines a WHERE {$where} ORDER BY a.commercial ASC";
950            $commercial = DB::select($query);
951
952            $query = "SELECT DISTINCT a.location FROM tbl_pipelines a WHERE {$where} ORDER BY a.location ASC";
953            $location = DB::select($query);
954
955            $query = "SELECT
956                        DISTINCT b.name
957                    FROM tbl_pipelines a
958                    LEFT JOIN tbl_visit_types b
959                        ON a.visit_type_id = b.visit_type_id
960                    WHERE {$where}
961                    ORDER BY ISNULL(b.priority), b.priority ASC";
962            $visitType = DB::select($query);
963
964            $query = "SELECT DISTINCT a.opportunity_type FROM tbl_pipelines a WHERE {$where} ORDER BY a.opportunity_type ASC";
965            $opportunityType = DB::select($query);
966
967            $query = "SELECT DISTINCT a.created_by FROM tbl_pipelines a WHERE {$where} ORDER BY a.created_by ASC";
968            $createdBy = DB::select($query);
969
970            return response([
971                'message' => 'OK',
972                'campaign' => $campaign,
973                'clientType' => $clientType,
974                'commercial' => $commercial,
975                'location' => $location,
976                'visitType' => $visitType,
977                'opportunityType' => $opportunityType,
978                'createdBy' => $createdBy,
979                'visitCall' => $visitCall,
980            ]);
981
982        } catch (\Exception $e) {
983            report(AppException::fromException($e, 'GET_DISTINCT_EXCEPTION'));
984
985            return response(['message' => 'KO', 'error' => $e->getMessage()]);
986        }
987
988    }
989
990    public function get_past_added_pipelines(Request $request): ResponseFactory|Response
991    {
992
993        try {
994
995            $data = $request->all();
996            $keyword = addslashes((string) $data['keyword']);
997            $result = [];
998
999            if (! empty($keyword)) {
1000                $array = explode(' ', $keyword);
1001
1002                $where = '';
1003
1004                $availableParameters = [$data['field']];
1005
1006                $availableParameters = [$data['field']];
1007
1008                $searchArray = [];
1009                $matchScoreArray = [];
1010                foreach ($availableParameters as $field) {
1011                    foreach ($searchTextArray as $word) {
1012                        array_push($searchArray, "({$field} LIKE '%{$word}%')");
1013                        array_push($matchScoreArray, "CASE WHEN {$field} LIKE '%{$word}%' THEN 1 ELSE 0 END");
1014                    }
1015                }
1016
1017                $searchArray = implode(' OR ', $searchArray);
1018                $matchScoreArray = implode(' + ', $matchScoreArray);
1019                $matchScoreCol = "({$matchScoreArray})";
1020                $where .= " AND ({$searchArray}";
1021
1022                $query = "SELECT
1023                            id,
1024                            client_name,
1025                            campaign,
1026                            {$matchScoreCol} match_score
1027                        FROM tbl_pipelines 
1028                        WHERE client_name IS NOT NULL
1029                        {$where}
1030                        GROUP BY client_name
1031                        ORDER BY match_score DESC, client_name ASC
1032                        ";
1033
1034                $result = DB::select($query);
1035            }
1036
1037            return response(['message' => 'OK', 'data' => $result]);
1038
1039        } catch (\Exception $e) {
1040            report(AppException::fromException($e, 'GET_PAST_ADDED_PIPELINES_EXCEPTION'));
1041
1042            return response(['message' => 'KO', 'error' => $e->getMessage()]);
1043        }
1044    }
1045
1046    public function download_pipelines(Request $request): ResponseFactory|Response
1047    {
1048        ini_set('max_execution_time', 123456);
1049        $data = $request->all();
1050        $companyId = addslashes((string) $data['company_id']);
1051        $userId = addslashes((string) $data['user_id']);
1052
1053        $r = new Request([
1054            'filterModel' => $data['filterModel'],
1055            'sortModel' => $data['sortModel'],
1056            'start' => 0,
1057            'end' => 999999999,
1058            'company_id' => $data['company_id'],
1059            'user_id' => $data['user_id'],
1060            'ids' => $data['ids'],
1061            'searchText' => $data['searchText'],
1062            'ids_not_in' => $data['ids_not_in'],
1063        ]);
1064
1065        $result = $this->list_pipelines_table($r);
1066        $result = $result->original['data'];
1067
1068        return response(['data' => $result]);
1069    }
1070}