Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.12% covered (danger)
0.12%
8 / 6409
0.00% covered (danger)
0.00%
0 / 97
CRAP
0.00% covered (danger)
0.00%
0 / 1
Quotations
0.12% covered (danger)
0.12%
8 / 6409
0.00% covered (danger)
0.00%
0 / 97
2806831.17
0.00% covered (danger)
0.00%
0 / 1
 __construct
61.54% covered (warning)
61.54%
8 / 13
0.00% covered (danger)
0.00%
0 / 1
4.91
 create_quotation
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 1
182
 currency
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 send_approval_notification
0.00% covered (danger)
0.00%
0 / 113
0.00% covered (danger)
0.00%
0 / 1
756
 send_approval_margin_notification
0.00% covered (danger)
0.00%
0 / 110
0.00% covered (danger)
0.00%
0 / 1
306
 isApproverAuthorizedFor
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
110
 approve_quotation
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
272
 send_approved_notification
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 1
132
 reject_quotation
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
306
 send_rejected_notification
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 1
156
 update_quotation
0.00% covered (danger)
0.00%
0 / 519
0.00% covered (danger)
0.00%
0 / 1
30102
 compareArrays
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 isEmpty
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
20
 convertValue
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
 callDeleteQuotation
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 get_quotation
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 get_quotation_log
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 send_notification
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
56
 delete_quotation
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
90
 getBlacklistEmails
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 validate_email
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
42
 isBlacklistedEmail
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 isInvalidEmail
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 list_quotations
0.00% covered (danger)
0.00%
0 / 411
0.00% covered (danger)
0.00%
0 / 1
20022
 list_orders_table
0.00% covered (danger)
0.00%
0 / 186
0.00% covered (danger)
0.00%
0 / 1
1482
 build_orders_table_filters
0.00% covered (danger)
0.00%
0 / 102
0.00% covered (danger)
0.00%
0 / 1
2070
 orders_yes_no_clause
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 get_dates
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 list_quotation_analytics_by_source
0.00% covered (danger)
0.00%
0 / 116
0.00% covered (danger)
0.00%
0 / 1
1640
 list_quotation_analytics_send_budgets
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
342
 list_quotation_analytics_track_budgets
0.00% covered (danger)
0.00%
0 / 115
0.00% covered (danger)
0.00%
0 / 1
1260
 list_quotation_analytics_types_budgets
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 1
992
 download_quotations
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
2
 download_quotations_csv
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 bulk_upload
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
30
 list_bulk_upload
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 delete_number
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 get_number
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
132
 get_years
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
12
 human_filesize
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get_files
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 1
1892
 download_file
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
72
 delete_file
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 send_email_to_client
0.00% covered (danger)
0.00%
0 / 356
0.00% covered (danger)
0.00%
0 / 1
10506
 send_email_follow_ups
0.00% covered (danger)
0.00%
0 / 472
0.00% covered (danger)
0.00%
0 / 1
12432
 create_sender_identity
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
56
 get_sender_identity
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 get_all_sender_identity
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 delete_sender_identity
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 create_template
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
72
 get_email_files
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 download_email_template_file
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 delete_email_template_file
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 update_email_template_order
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 update_email_template
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
90
 delete_template
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 get_email_template
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 update_sender_identity
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
20
 resend_verification
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 list_quotation_analytics_by_performance
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
30
 list_orders_update_logs
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 list_g3w_orders_update_logs
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 list_g3w_orders_failed
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 update_budget_status_rejected_manual
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 update_budget_status_rejected
0.00% covered (danger)
0.00%
0 / 55
0.00% covered (danger)
0.00%
0 / 1
110
 bulk_update_quotation
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
30
 move_budget_and_job
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 1
30
 list_quotation_analytics_by_types_of_budgets_created_per_week
0.00% covered (danger)
0.00%
0 / 193
0.00% covered (danger)
0.00%
0 / 1
3782
 preview_file
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
20
 get_past_added_quotation
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 send_acceptance_notification
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 1
182
 get_total_quotations_by_budget_status
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
90
 sendgrid_webhook_receiver
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
240
 isEmailValid
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 list_email_status
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 list_quotation_analytics_commercial
0.00% covered (danger)
0.00%
0 / 182
0.00% covered (danger)
0.00%
0 / 1
2862
 clear_open_data
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
 list_quotation_analytics_order_size
0.00% covered (danger)
0.00%
0 / 190
0.00% covered (danger)
0.00%
0 / 1
4290
 send_email_template_preview
0.00% covered (danger)
0.00%
0 / 109
0.00% covered (danger)
0.00%
0 / 1
306
 list_quotation_analytics_by_types_of_budgets_company_per_week
0.00% covered (danger)
0.00%
0 / 190
0.00% covered (danger)
0.00%
0 / 1
3782
 request_permission_commercial
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 1
90
 confirm_update_commercial
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 calculateEmailRequestSize
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 list_quotation_analytics_commercial_productivity
0.00% covered (danger)
0.00%
0 / 232
0.00% covered (danger)
0.00%
0 / 1
4290
 list_quotations_deleted
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 delete_sengrid
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 download_productivity_commercial
0.00% covered (danger)
0.00%
0 / 378
0.00% covered (danger)
0.00%
0 / 1
2450
 update_commercial_numbers
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 list_quotation_analytics_by_service_type
0.00% covered (danger)
0.00%
0 / 148
0.00% covered (danger)
0.00%
0 / 1
1122
 getIdsFromInternalQuoteIds
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 checkQuotationExistByInternalQuoteId
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
72
 addUpdateLog
0.00% covered (danger)
0.00%
0 / 82
0.00% covered (danger)
0.00%
0 / 1
2070
 setSolicitudDuplicity
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 getQuoteIdOfDuplicityById
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 download_s3_files
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
72
 getQuotationStatusByInternalId
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
110
 findQuotationByInternalId
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Exceptions\AppException;
6use App\Jobs\CheckQuotationDuplicate;
7use App\Jobs\Email\SendApprovalEmail;
8use App\Jobs\Email\SendApprovedEmail;
9use App\Jobs\Email\SendRejectedEmail;
10use App\Models\Client;
11use App\Models\StructureData;
12use App\Models\TblBlockedDomains;
13use App\Models\TblBudgetStatus;
14use App\Models\TblBudgetTypeGroups;
15use App\Models\TblBudgetTypes;
16use App\Models\TblBulkUpload;
17use App\Models\TblBusinessGoals;
18use App\Models\TblCcAcceptanceNotifications;
19use App\Models\TblCcBcc;
20use App\Models\TblCompanies;
21use App\Models\TblCompanyEmails;
22use App\Models\TblCustomerTypes;
23use App\Models\TblEmailConfiguration;
24use App\Models\TblEmailFiles;
25use App\Models\TblFiles;
26use App\Models\TblFollowUpLogs;
27use App\Models\TblG3WOrdersUpdateLogs;
28use App\Models\TblLastFollowUpDate;
29use App\Models\TblNotifications;
30use App\Models\TblOngoingJobs;
31use App\Models\TblOrdersUpdateLogs;
32use App\Models\TblProjectTypes;
33use App\Models\TblQuotations;
34use App\Models\TblQuotationsLog;
35use App\Models\TblSegments;
36use App\Models\TblSendgridWebhook;
37use App\Models\TblSources;
38use App\Models\TblToAcceptanceNotifications;
39use App\Models\TblUsers;
40use App\Models\TblVisitTypeGroups;
41use App\Models\TblWorkflowQuestions;
42use App\Services\PresupuestosService;
43use App\Services\ResultCache;
44use App\Services\SendgridLogger;
45use App\Services\UserCompanies;
46use App\Services\WhatsAppService;
47use Illuminate\Contracts\Routing\ResponseFactory;
48use Illuminate\Http\Request;
49use Illuminate\Http\Response as HttpResponse;
50use Illuminate\Support\Facades\App;
51use Illuminate\Support\Facades\Cache;
52use Illuminate\Support\Facades\DB;
53use Illuminate\Support\Facades\File;
54use Illuminate\Support\Facades\Log;
55use Illuminate\Support\Facades\Response;
56use Illuminate\Support\Facades\Storage;
57use PhpOffice\PhpSpreadsheet\IOFactory;
58use PhpOffice\PhpSpreadsheet\Spreadsheet;
59use PhpOffice\PhpSpreadsheet\Style\Alignment;
60use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
61use SendGrid\Mail\Attachment;
62use SendGrid\Mail\Mail;
63use ZipArchive;
64
65class Quotations extends Controller
66{
67    private $locale;
68
69    private $userId;
70
71    private $region;
72
73    private $companyIds;
74
75    private readonly string $companyId;
76
77    public function __construct()
78    {
79        $this->locale = request()->header('Locale-Id');
80        $this->userId = request()->header('User-Id');
81        $this->region = request()->header('Region');
82
83        App::setLocale($this->locale);
84
85        $this->companyIds = [];
86
87        if ($this->region != null && $this->region != '' && $this->region != 'All') {
88            $this->region = urldecode((string) $this->region);
89
90            $query = 'SELECT
91                        b.company_id
92                    FROM
93                        tbl_company_users a
94                        LEFT JOIN tbl_companies b ON a.company_id = b.company_id
95                    WHERE
96                        a.user_id = ?
97                        AND b.region = ?';
98
99            $this->companyIds = DB::select($query, [intval($this->userId), $this->region]);
100
101            $this->companyIds = collect($this->companyIds)->pluck('company_id')->toArray();
102        } else {
103            // FIRE-1146: prefer the middleware-resolved attribute; fall back to a fresh fetch only on unattached paths.
104            $this->companyIds = request()->attributes->get('user_company_ids', UserCompanies::forUser((int) $this->userId));
105        }
106
107        $this->companyId = implode(',', $this->companyIds);
108    }
109
110    public function create_quotation(Request $request): ResponseFactory|HttpResponse
111    {
112
113        try {
114
115            $data = $request->all();
116            $data['updated_at'] = date('Y-m-d H:i:s');
117
118            if (isset($data['request_date']) && isset($data['issue_date'])) {
119                $requestDate = strtotime($data['request_date']);
120                $issueDate = strtotime($data['issue_date']);
121                $dateDiff = $issueDate - $requestDate;
122                $data['duration'] = round($dateDiff / (60 * 60 * 24));
123            }
124
125            $r = new Request([
126                'created_by' => $data['created_by'],
127            ]);
128
129            $result = $this->get_number($r, @$data['company_id']);
130            $id = $result->original['id'];
131
132            $files = $request->file('files');
133            if ($files) {
134                $uploadedFiles = [];
135
136                foreach ($files as $file) {
137                    $filename = time().'_'.$file->getClientOriginalName();
138                    $fileSize = $file->getSize();
139                    // $fileContent = file_get_contents($file->getRealPath());
140                    // $fileHash = hash('sha256', $fileContent);
141
142                    $s3path = Storage::disk('s3')->putFileAs(
143                        'uploads',
144                        $file,
145                        $filename,
146                        [
147                            'ContentType' => $file->getMimeType(),
148                        ]
149                    );
150
151                    TblFiles::create(
152                        [
153                            'quotation_id' => $id,
154                            'original_name' => $file->getClientOriginalName(),
155                            'filename' => $filename,
156                            'uploaded_by' => $data['updated_by'],
157                            // 'file' => $fileContent,
158                            'file_size' => $file->getSize(),
159                            // 'file_hash' => $fileHash,
160                            'mime_type' => $file->getMimeType(),
161                            'uploaded_at' => now(),
162                        ]
163                    );
164
165                    // $this->addUpdateLog($id, $data['updated_by'], "upload_attachment", null, $filename, 4);
166                }
167            }
168
169            $query = 'SELECT COUNT(*) as count FROM tbl_files WHERE quotation_id = ?';
170            $fileCount = DB::select($query, [$id])[0]->count;
171
172            $data['has_attachment'] = $fileCount > 0 ? 1 : 0;
173
174            $data = array_diff_key($data, array_flip(['files', '_token', 'otros_campos_no_necesarios']));
175
176            $data['for_add'] = 0;
177
178            // FIRE-864: if email is invalid (blacklisted, malformed, or
179            // empty) and status is sendable, set to 22 (Correo erroneo).
180            // Pre-fix this only checked `isBlacklistedEmail()`, so
181            // malformed emails like "no@" or "ricardo" never got
182            // reclassified.
183            if (isset($data['email']) && $this->isInvalidEmail($data['email'])
184                && isset($data['budget_status_id']) && in_array($data['budget_status_id'], [1, 2, 11, 17])) {
185                $data['budget_status_id'] = 22;
186            }
187
188            TblQuotations::where('id', $id)->update($data);
189
190            $result = TblQuotations::where('id', $id)->first();
191
192            if ($result->budget_status_id == 6) {
193                $data = [
194                    'id' => $result->id ?? null,
195                    'client' => $result->client ?? null,
196                    'email' => $result->email ?? null,
197                    'phone_number' => $result->phone_number ?? null,
198                    'last_follow_up_comment' => $result->last_follow_up_comment ?? null,
199                    'quote_id' => $result->quote_id ?? null,
200                    'request_date' => date('Y-m-d'),
201                    'updated_by' => 'IA',
202                    'user_id' => $result->getAttribute('user_id') ?? null,
203                    'commercial' => $result->commercial ?? null,
204                    'budget_status_id' => $result->budget_status_id ?? null,
205                    'internal_quote_id' => $result->internal_quote_id ?? null,
206                ];
207
208                // FIRE-1147: dispatch the Lambda duplicate-checker async.
209                // The original inline curl call blocked the HTTP response for
210                // 3-5 s but its return value was already discarded — no
211                // downstream code reads $response or $httpCode — so moving
212                // this to a job is a pure latency win with no behaviour change.
213                CheckQuotationDuplicate::dispatch($data)->onQueue('default');
214            }
215
216            $logCategory = $result->budget_status_id == 6 ? 0 : 2;
217            $this->addUpdateLog($result->id, $result->getAttribute('user_id'), 'create_quotation', null, null, $logCategory);
218            // FIRE-1145: was Cache::flush() — scoped to the domains create_quotation actually affects.
219            ResultCache::forgetDomain(['quotations', 'users']);
220
221            return response(['message' => 'OK', 'data' => $result]);
222
223        } catch (\Exception $e) {
224            report(AppException::fromException($e, 'CREATE_QUOTATION_EXCEPTION'));
225
226            return response(['message' => 'KO', 'error' => $e->getMessage()]);
227        }
228
229    }
230
231    public function currency($amount, $withEuro = '')
232    {
233
234        if ($withEuro != null) {
235            $withEuro = ' €';
236        }
237
238        return number_format($amount, 2, ',', '.').$withEuro;
239    }
240
241    public function send_approval_notification($amount, $budgetTypeId, $customerTypeId, $minimumOrderSize, string $quoteId, $id, $companyName, $createdBy, $userId, $action, $commercialEmail, $companyId, $endpoint, $isQuestion, $questionIdsNo, $n, $locale = null): void
242    {
243
244        if (! is_null($locale)) {
245            $this->locale = $locale;
246        }
247
248        if ($action != 1) {
249            if ($this->locale == 'es') {
250                $action = 'actualizado';
251            } else {
252                $action = 'updated';
253            }
254
255        } else {
256            if ($this->locale == 'es') {
257                $action = 'creado';
258            } else {
259                $action = 'created';
260            }
261        }
262
263        $fendpoint = '';
264
265        if ($endpoint == 'orders') {
266            if ($this->locale == 'es') {
267                $fendpoint = 'presupuesto';
268            } else {
269                $fendpoint = 'budget';
270            }
271
272        } else {
273            if ($this->locale == 'es') {
274                $fendpoint = 'trabajo';
275            } else {
276                $fendpoint = 'job';
277            }
278        }
279
280        $user = TblUsers::where('id', $userId)->first();
281
282        $query = "SELECT
283                    a.approver_id,
284                    a.user_id,
285                    b.name,
286                    b.email
287                FROM
288                    tbl_approvers a
289                    INNER JOIN tbl_users b ON a.user_id = b.id
290                WHERE a.company_id = {$companyId}
291                ";
292
293        if ($n == 3) {
294            $query = "SELECT
295                        a.approver_id,
296                        a.user_id,
297                        u.name,
298                        u.email
299                    FROM tbl_approvers a
300                    INNER JOIN tbl_users u ON a.user_id = u.id
301                    WHERE a.company_id = {$companyId}
302
303                    UNION ALL
304
305                    SELECT
306                        c.approver_id,
307                        c.user_id,
308                        u2.name,
309                        u2.email
310                    FROM tbl_approvers_v2 c
311                    INNER JOIN tbl_users u2 ON c.user_id = u2.id
312                    WHERE c.company_id = {$companyId}";
313        }
314
315        $approvers = DB::select($query);
316
317        if (count($approvers) > 0) {
318            $amount = $this->currency($amount, 1);
319            $minimumOrderSize = $this->currency($minimumOrderSize, 1);
320
321            $budgetType = TblBudgetTypes::where('budget_type_id', $budgetTypeId)->first();
322            $clientType = TblCustomerTypes::where('customer_type_id', $customerTypeId)->first();
323
324            $imgpath = file_get_contents(public_path('fireservicetitan.png'));
325            $base64 = 'data:image/png;base64,'.base64_encode($imgpath);
326
327            $subject = __('language.send_approval_notification.subject');
328            $subject = str_replace('{{type}}', $budgetType->name, $subject);
329            $subject = str_replace('{{amount}}', $amount, $subject);
330            $subject = str_replace('{{endpoint}}', ucfirst($fendpoint), $subject);
331
332            $url = config('app.frontend_url')."{$endpoint}/{$id}?company_id={$companyId}";
333            $href = "<a href='{$url}'>{$quoteId}</a>";
334            $cc = false;
335            foreach ($approvers as $item) {
336
337                $toEmail = $item->email;
338
339                $body = __('language.send_approval_notification.body_hello');
340                $body = str_replace('{{approver}}', $item->name, $body);
341
342                $body .= __('language.send_approval_notification.body_message');
343                $body = str_replace('{{endpoint}}', $fendpoint, $body);
344                $body = str_replace('{{creator}}', $createdBy, $body);
345                $body = str_replace('{{action}}', $action, $body);
346                $body = str_replace('{{company}}', $companyName, $body);
347                $body = str_replace('{{type}}', $budgetType->name, $body);
348                $body = str_replace('{{amount}}', $amount, $body);
349                $body = str_replace('{{quote_id}}', $href, $body);
350
351                if ($isQuestion == 1) {
352                    $body .= __('language.send_approval_notification.note_question');
353                } else {
354                    $body .= __('language.send_approval_notification.note');
355                }
356
357                $body = str_replace('{{company}}', $companyName, $body);
358                $body = str_replace('{{client_type}}', $clientType->name, $body);
359                $body = str_replace('{{project_type}}', $budgetType->name, $body);
360                $body = str_replace('{{amount}}', $minimumOrderSize, $body);
361
362                if ($isQuestion == 1) {
363                    if ($questionIdsNo) {
364                        $questions = TblWorkflowQuestions::whereIn('question_id', $questionIdsNo)->where('company_id', $companyId)->get();
365
366                        if ($questions->isNotEmpty()) {
367                            $ul = '<ul>';
368                            $li = '';
369                            foreach ($questions as $item) {
370                                $li .= "<li>{$item->question}</li>";
371                            }
372                            $ul .= $li.'</ul><br><br>';
373                            $body .= $ul;
374                        }
375                    }
376                }
377
378                $content = $body;
379
380                $body .= '<p>Fire Service Titan</p>';
381                $body .= "<img src='cid:fireservicetitan' style='height: 45px;' />";
382
383                $html = '<!DOCTYPE html>';
384                $html .= '<html>';
385                $html .= '<head>';
386                $html .= '<meta charset="UTF-8">';
387                $html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
388                $html .= '</head>';
389                $html .= '<body>';
390                $html .= $body;
391                $html .= '</body>';
392                $html .= '</html>';
393
394                if ($toEmail != null) {
395                    // $user is the commercial/creator. It can be null when the
396                    // quote came from G3W with a commercial name that doesn't
397                    // map to any tbl_users row. Notification still goes out to
398                    // the technical approver; the BCC to the commercial is
399                    // simply skipped when we don't know who they are.
400                    $userEmail = $user?->email;
401
402                    if (config('services.sendgrid.staging') && $userEmail) {
403                        $toEmail = $userEmail;
404                        $item->user_id = $userId;
405                    }
406
407                    // BCC list — only attached once (to the first approver),
408                    // not duplicated across multiple approvers.
409                    $bcc = [];
410                    if ($cc == false) {
411                        $cc = true;
412                        if ($userEmail && $userEmail != $toEmail) {
413                            $bcc[] = $userEmail;
414                            if ($userEmail != $commercialEmail && $commercialEmail != null) {
415                                $bcc[] = $commercialEmail;
416                            }
417                        } elseif (! $userEmail && $commercialEmail != null && $commercialEmail != $toEmail) {
418                            // No commercial user in FST but G3W gave us a raw
419                            // email — include it so the (unknown-to-FST) seller
420                            // at least sees the approval request.
421                            $bcc[] = $commercialEmail;
422                        }
423                    }
424
425                    // FIRE-1147: synchronous side effects FIRST (audit log +
426                    // in-app notification), then dispatch the email send.
427                    // Pre-fix these were inside the 202-branch — if SendGrid
428                    // 5xx'd the in-app notification was lost.
429                    $this->addUpdateLog($id, $userId, 'send_approval_notification', null, null, 5);
430
431                    TblNotifications::create(
432                        [
433                            'user_id' => $item->user_id,
434                            'content' => $content,
435                            'is_open' => 1,
436                            'created_by' => 'System',
437                            'link' => $url,
438                        ]
439                    );
440
441                    SendApprovalEmail::dispatch(
442                        toEmail: (string) $toEmail,
443                        subject: $subject,
444                        html: $html,
445                        bcc: $bcc,
446                        quoteId: $quoteId,
447                    )->onQueue('email');
448                }
449
450            }
451        }
452    }
453
454    public function send_approval_margin_notification($amount, $budgetTypeId, $customerTypeId, $minimumMargin, string $quoteId, $id, $companyName, $createdBy, $userId, $action, $commercialEmail, $invoiceMargin, $companyId, $endpoint): void
455    {
456
457        if ($action != 1) {
458            if ($this->locale == 'es') {
459                $action = 'actualizado';
460            } else {
461                $action = 'updated';
462            }
463
464        } else {
465            if ($this->locale == 'es') {
466                $action = 'creado';
467            } else {
468                $action = 'created';
469            }
470        }
471
472        $fendpoint = '';
473
474        if ($endpoint == 'orders') {
475            if ($this->locale == 'es') {
476                $fendpoint = 'presupuesto';
477            } else {
478                $fendpoint = 'budget';
479            }
480
481        } else {
482            if ($this->locale == 'es') {
483                $fendpoint = 'trabajo';
484            } else {
485                $fendpoint = 'job';
486            }
487        }
488
489        $invoiceMargin = number_format($invoiceMargin, 2);
490        $minimumMargin = number_format($minimumMargin, 2);
491
492        $user = TblUsers::where('id', $userId)->first();
493
494        $query = "SELECT
495                    a.approver_id,
496                    a.user_id,
497                    b.name,
498                    b.email
499                FROM
500                    tbl_approvers a
501                    INNER JOIN tbl_users b ON a.user_id = b.id
502                WHERE a.company_id = {$companyId}
503                ";
504
505        $approvers = DB::select($query);
506
507        if (count($approvers) > 0) {
508
509            $amount = $this->currency($amount, 1);
510
511            $budgetType = TblBudgetTypes::where('budget_type_id', $budgetTypeId)->first();
512            $clientType = TblCustomerTypes::where('customer_type_id', $customerTypeId)->first();
513
514            $imgpath = file_get_contents(public_path('fireservicetitan.png'));
515            $base64 = 'data:image/png;base64,'.base64_encode($imgpath);
516
517            $subject = __('language.send_approval_margin_notification.subject');
518            $subject = str_replace('{{type}}', $budgetType->name, $subject);
519            $subject = str_replace('{{amount}}', $amount, $subject);
520            $subject = str_replace('{{margin}}', $invoiceMargin, $subject);
521            $subject = str_replace('{{endpoint}}', ucfirst($fendpoint), $subject);
522
523            $url = config('app.frontend_url')."{$endpoint}/{$id}?company_id={$companyId}";
524            $href = "<a href='{$url}'>{$quoteId}</a>";
525            $cc = false;
526            foreach ($approvers as $item) {
527
528                $toEmail = $item->email;
529
530                $body = __('language.send_approval_margin_notification.body_hello');
531                $body = str_replace('{{approver}}', $item->name, $body);
532
533                $body .= __('language.send_approval_margin_notification.body_message');
534                $body = str_replace('{{endpoint}}', $fendpoint, $body);
535                $body = str_replace('{{creator}}', $createdBy, $body);
536                $body = str_replace('{{action}}', $action, $body);
537                $body = str_replace('{{company}}', $companyName, $body);
538                $body = str_replace('{{type}}', $budgetType->name, $body);
539                $body = str_replace('{{amount}}', $amount, $body);
540                $body = str_replace('{{quote_id}}', $href, $body);
541                $body = str_replace('{{margin}}', $invoiceMargin, $body);
542
543                $body .= __('language.send_approval_margin_notification.note');
544                $body = str_replace('{{company}}', $companyName, $body);
545                $body = str_replace('{{client_type}}', $clientType->name, $body);
546                $body = str_replace('{{project_type}}', $budgetType->name, $body);
547                $body = str_replace('{{margin}}', $minimumMargin, $body);
548
549                $content = $body;
550
551                $body .= '<p>Fire Service Titan</p>';
552                $body .= "<img src='cid:fireservicetitan' style='height: 45px;' />";
553
554                $html = '<!DOCTYPE html>';
555                $html .= '<html>';
556                $html .= '<head>';
557                $html .= '<meta charset="UTF-8">';
558                $html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
559                $html .= '</head>';
560                $html .= '<body>';
561                $html .= $body;
562                $html .= '</body>';
563                $html .= '</html>';
564
565                if ($toEmail != null) {
566                    $email = new Mail;
567
568                    if (config('services.sendgrid.staging')) {
569                        $toEmail = $user->email;
570                        $item->user_id = $userId;
571                    }
572
573                    if ($cc == false) {
574                        $cc = true;
575
576                        if ($user->email != $toEmail) {
577                            if ($user->email != $commercialEmail && $commercialEmail != null) {
578                                $email->addBcc($user->email);
579                                $email->addBcc($commercialEmail);
580                            } else {
581                                $email->addBcc($user->email);
582                            }
583                        }
584                    }
585
586                    $email->setFrom('fire@fire.es', 'Fire Service Titan');
587                    $email->setSubject($subject);
588                    $email->addTo($toEmail);
589                    $email->addContent('text/html', $html);
590
591                    $email->addAttachment(
592                        $imgpath,
593                        'image/png',
594                        'fireservicetitan.png',
595                        'inline',
596                        'fireservicetitan'
597                    );
598
599                    $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
600
601                    try {
602                        $response = $sendgrid->send($email);
603                        SendgridLogger::log($email, $response);
604                    } catch (\Throwable $sendException) {
605                        SendgridLogger::logException($email, $sendException);
606                        throw $sendException;
607                    }
608
609                    if ($response->statusCode() == 202) {
610                        Log::channel('email_log')->info('ID:'.$quoteId.' : '.$toEmail.' - MARGIN APPROVAL EMAIL NOTIFICATION SENT');
611
612                        $this->addUpdateLog($quoteId, $userId, 'send_approval_margin_notification', null, null, 5);
613
614                        TblNotifications::create(
615                            [
616                                'user_id' => $item->user_id,
617                                'content' => $content,
618                                'is_open' => 1,
619                                'created_by' => 'System',
620                                'link' => $url,
621                            ]
622                        );
623                    } else {
624                        $error = true;
625                        Log::channel('email_log')->error('ID:'.$quoteId.' : '.$toEmail.' - '.$response->body());
626                    }
627                }
628
629            }
630        }
631    }
632
633    /**
634     * Authorize the logged-in user to approve/reject the given quotation.
635     *
636     * Only an approver of the type the order currently NEEDS, whose slot is
637     * still open, may act:
638     *   - technical (v1, tbl_approvers): for_approval 1 or 3, v1 slot empty
639     *   - financial (v2, tbl_approvers_v2): for_approval 3, v2 slot empty
640     *
641     * Frontend gating (orders.component) mirrors this, but the UI is
642     * bypassable: without this check a non-approver / wrong-type approver
643     * could hit the endpoint and clear `for_approval` (see the single-level
644     * branch in approve_quotation / reject_quotation) with no approver recorded.
645     */
646    private function isApproverAuthorizedFor($result): bool
647    {
648        if ($result === null) {
649            return false;
650        }
651
652        $companyId = (int) $result->company_id;
653        $userId = (int) $this->userId;
654
655        $isTechnical = DB::table('tbl_approvers')
656            ->where('company_id', $companyId)
657            ->where('user_id', $userId)
658            ->exists();
659
660        $isFinancial = DB::table('tbl_approvers_v2')
661            ->where('company_id', $companyId)
662            ->where('user_id', $userId)
663            ->exists();
664
665        $technicalOpen = ($result->for_approval == 1 || $result->for_approval == 3)
666            && $result->approved_by === null
667            && $result->rejected_by === null;
668
669        $financialOpen = $result->for_approval == 3
670            && $result->approved_by_v2 === null
671            && $result->rejected_by_v2 === null;
672
673        return ($isTechnical && $technicalOpen) || ($isFinancial && $financialOpen);
674    }
675
676    public function approve_quotation($id): ResponseFactory|HttpResponse
677    {
678
679        try {
680
681            $id = addslashes((string) $id);
682
683            $result = TblQuotations::where('id', $id)->first();
684            $company = TblCompanies::where('company_id', $result->company_id)->first();
685            $budgetType = TblBudgetTypes::where('budget_type_id', $result->budget_type_id)->first();
686
687            // FIRE-1092: G3W-imported quotes commonly have budget_type_id=NULL
688            // (mapping not yet resolved), so the TblBudgetTypes lookup returns
689            // null and the downstream `$budgetType->name` crashes. Same risk
690            // on $company if the company was renamed/deleted. Fall back to a
691            // safe placeholder for the notification body; it's purely
692            // informational and the approval still proceeds.
693            $companyName = $company?->name ?? '—';
694            $budgetTypeName = $budgetType?->name ?? '—';
695            $companyIdForNotification = $company?->company_id ?? $result->company_id;
696
697            // Only the approver type the order needs (with an open slot) may act.
698            if (! $this->isApproverAuthorizedFor($result)) {
699                return response(['message' => 'KO', 'error' => __('language.approval_not_authorized')]);
700            }
701
702            if ($result->created_by != $result->commercial) {
703                $creatorAndCommercial = [$result->created_by, $result->commercial];
704                foreach ($creatorAndCommercial as $name) {
705                    $user = TblUsers::where('name', $name)->first();
706                    if ($user) {
707                        $this->send_approved_notification($user->id, $user->name, $user->email, $companyName, $budgetTypeName, $result->amount, $id, $result->quote_id, $companyIdForNotification, 'orders');
708                    }
709                }
710            } else {
711                $user = TblUsers::where('name', $result->created_by)->first();
712                if ($user) {
713                    $this->send_approved_notification($user->id, $user->name, $user->email, $companyName, $budgetTypeName, $result->amount, $id, $result->quote_id, $companyIdForNotification, 'orders');
714                }
715            }
716
717            if ($result->for_approval == 3) {
718                $result = TblQuotations::where('id', $id)->first();
719
720                $approved = 0;
721                $rejected = 0;
722
723                if ($result->approved_by !== null) {
724                    $approved++;
725                }
726                if ($result->approved_by_v2 !== null) {
727                    $approved++;
728                }
729
730                if ($result->rejected_by !== null) {
731                    $rejected++;
732                }
733                if ($result->rejected_by_v2 !== null) {
734                    $rejected++;
735                }
736
737                if ($approved === 2) {
738                    TblQuotations::where('id', $id)->update(['for_approval' => null, 'approval_type' => null]);
739                } elseif ($rejected >= 1 && ($approved + $rejected) === 2) {
740                    TblQuotations::where('id', $id)->update(['for_approval' => null, 'approval_type' => null]);
741                }
742            } elseif ($result->for_approval == 1) {
743                TblQuotations::where('id', $id)->update(['for_approval' => null, 'approval_type' => null]);
744            }
745
746            // FIRE-1092: log the APPROVER (the user clicking the button) as
747            // the actor — that's the only id guaranteed to exist for this
748            // request. Pre-fix the code wrote `$user->id`, but `$user` was
749            // the creator/commercial Eloquent lookup that returns null when
750            // `created_by`/`commercial` doesn't match any `tbl_users.name`
751            // (very common on G3W-imported rows where `created_by = 'Zala'`
752            // or `'System'`). Result: every approve attempt on those quotes
753            // threw "Attempt to read property 'id' on null".
754            $this->addUpdateLog($id, $this->userId, 'approve_quotation', null, null, 5);
755
756            // FIRE-1145: was Cache::flush() — approval affects list_quotations + commercial counters.
757            ResultCache::forgetDomain(['quotations', 'users']);
758
759            // Return the post-action `for_approval` so the frontend can tell
760            // single-level "fully approved" from dual-level "still waiting on
761            // the other approver" and show the right toast.
762            $postState = TblQuotations::where('id', $id)->first();
763
764            return response(['message' => 'OK', 'for_approval' => $postState?->for_approval]);
765
766        } catch (\Exception $e) {
767            report(AppException::fromException($e, 'APPROVE_QUOTATION_EXCEPTION'));
768
769            return response(['message' => 'KO', 'error' => $e->getMessage()]);
770        }
771
772    }
773
774    public function send_approved_notification($userId, $username, $email, $companyName, $budgetType, $amount, $id, string $quoteId, $companyId, $endpoint): void
775    {
776
777        $fendpoint = '';
778
779        if ($endpoint == 'orders') {
780            if ($this->locale == 'es') {
781                $fendpoint = 'presupuesto';
782            } else {
783                $fendpoint = 'budget';
784            }
785
786        } else {
787            if ($this->locale == 'es') {
788                $fendpoint = 'trabajo';
789            } else {
790                $fendpoint = 'job';
791            }
792        }
793
794        $query = "SELECT
795                    u.id AS user_id,
796                    u.name,
797                    u.email,
798                    u.sender_email,
799                    CASE
800                        WHEN a.user_id IS NOT NULL AND c.user_id IS NOT NULL THEN 'both'
801                        WHEN a.user_id IS NOT NULL THEN 'approvers'
802                        WHEN c.user_id IS NOT NULL THEN 'approvers_v2'
803                        ELSE 'none'
804                    END AS exists_in
805                FROM tbl_users u
806                LEFT JOIN tbl_approvers a
807                    ON u.id = a.user_id AND a.company_id = {$companyId}
808                LEFT JOIN tbl_approvers_v2 c
809                    ON u.id = c.user_id AND c.company_id = {$companyId}
810                WHERE u.id = {$this->userId}";
811
812        $user = DB::select($query);
813
814        $user = $user[0] ?? null;
815
816        // FIRE-1092: if the logged-in user isn't in tbl_users for any reason,
817        // bail before downstream `$user->exists_in` / `$user->name` reads
818        // null-deref. The caller (`approve_quotation`) doesn't care about
819        // the return value, so silently skipping is acceptable; the audit
820        // log entry in the caller still records the action.
821        if ($user === null) {
822            Log::channel('email_log')->warning("send_approved_notification: logged-in user {$this->userId} not found in tbl_users; skipping notification for quote {$quoteId}.");
823
824            return;
825        }
826
827        $toEmail = $email;
828
829        $amount = $this->currency($amount, 1);
830
831        $imgpath = file_get_contents(public_path('fireservicetitan.png'));
832        $base64 = 'data:image/png;base64,'.base64_encode($imgpath);
833
834        $url = config('app.frontend_url')."{$endpoint}/{$id}?company_id={$companyId}";
835        $href = "<a href='{$url}'>{$quoteId}</a>";
836
837        $body = __('language.send_approved_notification.body_hello');
838        $body = str_replace('{{creator}}', $username, $body);
839
840        $body .= __('language.send_approved_notification.body_message');
841        $body = str_replace('{{approver}}', $user->name, $body);
842        $body = str_replace('{{company}}', $companyName, $body);
843        $body = str_replace('{{type}}', $budgetType, $body);
844        $body = str_replace('{{amount}}', $amount, $body);
845        $body = str_replace('{{quote_id}}', $href, $body);
846        $body = str_replace('{{endpoint}}', $fendpoint, $body);
847
848        $content = $body;
849
850        $body .= '<p>Fire Service Titan</p>';
851        $body .= "<img src='cid:fireservicetitan' style='height: 45px;' />";
852
853        $html = '<!DOCTYPE html>';
854        $html .= '<html>';
855        $html .= '<head>';
856        $html .= '<meta charset="UTF-8">';
857        $html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
858        $html .= '</head>';
859        $html .= '<body>';
860        $html .= $body;
861        $html .= '</body>';
862        $html .= '</html>';
863
864        $subject = __('language.send_approved_notification.subject');
865        $subject = str_replace('{{quote_id}}', $quoteId, $subject);
866        $subject = str_replace('{{endpoint}}', ucfirst($fendpoint), $subject);
867
868        // FIRE-1092: record the approval action FIRST, independently of
869        // whether the notification email succeeds. Pre-fix, both the DB
870        // write and the in-app notification lived inside the
871        // `if ($response->statusCode() == 202)` branch, so a recipient
872        // with no email / a SendGrid failure / an approver that belonged
873        // to BOTH `tbl_approvers` and `tbl_approvers_v2` would leave the
874        // approval unrecorded — and `approve_quotation` would still clear
875        // `for_approval` (single-level) or fail to clear it (dual-level
876        // counter never reaching 2). End result: row stays in the bucket
877        // or stays in an inconsistent state.
878        if ($endpoint == 'orders') {
879            if ($user->exists_in == 'approvers' || $user->exists_in == 'both') {
880                TblQuotations::where('id', $id)->update([
881                    'approved_at' => date('Y-m-d H:i:s'),
882                    'approved_by' => $user->name,
883                ]);
884            } elseif ($user->exists_in == 'approvers_v2') {
885                TblQuotations::where('id', $id)->update([
886                    'approved_at_v2' => date('Y-m-d H:i:s'),
887                    'approved_by_v2' => $user->name,
888                ]);
889            }
890        } else {
891            TblOngoingJobs::where('id', $id)->update([
892                'approved_at' => date('Y-m-d H:i:s'),
893                'approved_by' => $user->name,
894            ]);
895        }
896
897        TblNotifications::create([
898            'user_id' => $userId,
899            'content' => $content,
900            'is_open' => 1,
901            'created_by' => 'System',
902            'link' => $url,
903        ]);
904
905        $this->addUpdateLog($id, $userId, 'send_approved_notification', null, null, 5);
906
907        // FIRE-1147: dispatch the email send to the queue. The approval
908        // record + in-app notification at L893-928 already landed
909        // synchronously, so even if SendGrid is down the action is recorded.
910        // The duplicate side-effect block that previously fired on 202 is
911        // deleted — it re-wrote rows we already wrote above.
912        if ($toEmail != null) {
913            if (config('services.sendgrid.staging')) {
914                $toEmail = $user->email;
915            }
916
917            SendApprovedEmail::dispatch(
918                toEmail: (string) $toEmail,
919                subject: $subject,
920                html: $html,
921                quoteId: $quoteId,
922            )->onQueue('email');
923        }
924
925    }
926
927    public function reject_quotation(Request $request, $id): ResponseFactory|HttpResponse
928    {
929
930        try {
931
932            $id = addslashes((string) $id);
933
934            // The rejecter writes a free-text reason that is forwarded to the
935            // sales rep (creator/commercial) in the rejection notification.
936            $reason = trim((string) $request->input('reason', ''));
937
938            $result = TblQuotations::where('id', $id)->first();
939            $company = TblCompanies::where('company_id', $result->company_id)->first();
940            $budgetType = TblBudgetTypes::where('budget_type_id', $result->budget_type_id)->first();
941
942            // FIRE-1092: same null guards as approve_quotation. budget_type_id
943            // is often NULL on G3W-imported quotes; pre-fix the call to
944            // send_rejected_notification crashed with "name on null".
945            $companyName = $company?->name ?? '—';
946            $budgetTypeName = $budgetType?->name ?? '—';
947            $companyIdForNotification = $company?->company_id ?? $result->company_id;
948
949            // Only the approver type the order needs (with an open slot) may act.
950            if (! $this->isApproverAuthorizedFor($result)) {
951                return response(['message' => 'KO', 'error' => __('language.approval_not_authorized')]);
952            }
953
954            if ($result->created_by != $result->commercial) {
955                $creatorAndCommercial = [$result->created_by, $result->commercial];
956                foreach ($creatorAndCommercial as $name) {
957                    $user = TblUsers::where('name', $name)->first();
958                    if ($user) {
959                        $this->send_rejected_notification($user->id, $user->name, $user->email, $companyName, $budgetTypeName, $result->amount, $id, $result->quote_id, $companyIdForNotification, 'orders', $reason);
960                    }
961                }
962            } else {
963                $user = TblUsers::where('name', $result->created_by)->first();
964                if ($user) {
965                    $this->send_rejected_notification($user->id, $user->name, $user->email, $companyName, $budgetTypeName, $result->amount, $id, $result->quote_id, $companyIdForNotification, 'orders', $reason);
966                }
967            }
968
969            if ($result->for_approval == 3) {
970                $result = TblQuotations::where('id', $id)->first();
971
972                $approved = 0;
973                $rejected = 0;
974
975                if ($result->approved_by !== null) {
976                    $approved++;
977                }
978                if ($result->approved_by_v2 !== null) {
979                    $approved++;
980                }
981
982                if ($result->rejected_by !== null) {
983                    $rejected++;
984                }
985                if ($result->rejected_by_v2 !== null) {
986                    $rejected++;
987                }
988
989                if ($approved === 2) {
990                    TblQuotations::where('id', $id)->update(['for_approval' => null, 'approval_type' => null]);
991                } elseif ($rejected >= 1 && ($approved + $rejected) === 2) {
992                    TblQuotations::where('id', $id)->update(['for_approval' => null, 'approval_type' => null]);
993                }
994            } elseif ($result->for_approval == 1) {
995                TblQuotations::where('id', $id)->update(['for_approval' => null, 'approval_type' => null]);
996            }
997
998            // FIRE-1092: log the REJECTER (the logged-in user) as the actor,
999            // not the creator/commercial — see the matching comment in
1000            // approve_quotation. `$user` can be null when `created_by` or
1001            // `commercial` don't match a `tbl_users.name`.
1002            // Record the rejection reason as the log's new value so it stays
1003            // traceable in the quotation change log.
1004            $this->addUpdateLog($id, $this->userId, 'reject_quotation', null, $reason !== '' ? $reason : null, 5);
1005
1006            // FIRE-1145: was Cache::flush() — rejection affects list_quotations + commercial counters.
1007            ResultCache::forgetDomain(['quotations', 'users']);
1008
1009            // Mirror approve_quotation: return post-action `for_approval` so
1010            // the frontend can distinguish "fully decided" from "still
1011            // waiting on the other approver" when showing the success toast.
1012            $postState = TblQuotations::where('id', $id)->first();
1013
1014            return response(['message' => 'OK', 'for_approval' => $postState?->for_approval]);
1015
1016        } catch (\Exception $e) {
1017            report(AppException::fromException($e, 'REJECT_QUOTATION_EXCEPTION'));
1018
1019            return response(['message' => 'KO', 'error' => $e->getMessage()]);
1020        }
1021
1022    }
1023
1024    public function send_rejected_notification($userId, $username, $email, $companyName, $budgetType, $amount, $id, string $quoteId, $companyId, $endpoint, string $reason = ''): void
1025    {
1026
1027        $fendpoint = '';
1028
1029        if ($endpoint == 'orders') {
1030            if ($this->locale == 'es') {
1031                $fendpoint = 'presupuesto';
1032            } else {
1033                $fendpoint = 'budget';
1034            }
1035
1036        } else {
1037            if ($this->locale == 'es') {
1038                $fendpoint = 'trabajo';
1039            } else {
1040                $fendpoint = 'job';
1041            }
1042        }
1043
1044        $query = "SELECT
1045                    u.id AS user_id,
1046                    u.name,
1047                    u.email,
1048                    u.sender_email,
1049                    CASE
1050                        WHEN a.user_id IS NOT NULL AND c.user_id IS NOT NULL THEN 'both'
1051                        WHEN a.user_id IS NOT NULL THEN 'approvers'
1052                        WHEN c.user_id IS NOT NULL THEN 'approvers_v2'
1053                        ELSE 'none'
1054                    END AS exists_in
1055                FROM tbl_users u
1056                LEFT JOIN tbl_approvers a
1057                    ON u.id = a.user_id AND a.company_id = {$companyId}
1058                LEFT JOIN tbl_approvers_v2 c
1059                    ON u.id = c.user_id AND c.company_id = {$companyId}
1060                WHERE u.id = {$this->userId}";
1061
1062        $user = DB::select($query);
1063
1064        $user = $user[0] ?? null;
1065
1066        // FIRE-1092: same defensive null check as send_approved_notification.
1067        if ($user === null) {
1068            Log::channel('email_log')->warning("send_rejected_notification: logged-in user {$this->userId} not found in tbl_users; skipping notification for quote {$quoteId}.");
1069
1070            return;
1071        }
1072
1073        $toEmail = $email;
1074        $amount = $this->currency($amount, 1);
1075
1076        $imgpath = file_get_contents(public_path('fireservicetitan.png'));
1077        $base64 = 'data:image/png;base64,'.base64_encode($imgpath);
1078
1079        $url = config('app.frontend_url')."{$endpoint}/{$id}?company_id={$companyId}";
1080        $href = "<a href='{$url}'>{$quoteId}</a>";
1081
1082        $body = __('language.send_rejected_notification.body_hello');
1083        $body = str_replace('{{creator}}', $username, $body);
1084
1085        $body .= __('language.send_rejected_notification.body_message');
1086        $body = str_replace('{{approver}}', $user->name, $body);
1087        $body = str_replace('{{company}}', $companyName, $body);
1088        $body = str_replace('{{type}}', $budgetType, $body);
1089        $body = str_replace('{{amount}}', $amount, $body);
1090        $body = str_replace('{{quote_id}}', $href, $body);
1091        $body = str_replace('{{endpoint}}', $fendpoint, $body);
1092
1093        // Append the reason written by the rejecter. Escape it first — the
1094        // value is shown both as an HTML email and as an in-app notification,
1095        // so unescaped input would be a stored-XSS vector.
1096        $reason = trim($reason);
1097        if ($reason !== '') {
1098            $body .= '<p><b>'.__('language.send_rejected_notification.reason').'</b><br>'.nl2br(e($reason)).'</p><br>';
1099        }
1100
1101        $content = $body;
1102
1103        $body .= '<p>Fire Service Titan</p>';
1104        $body .= "<img src='cid:fireservicetitan' style='height: 45px;' />";
1105
1106        $html = '<!DOCTYPE html>';
1107        $html .= '<html>';
1108        $html .= '<head>';
1109        $html .= '<meta charset="UTF-8">';
1110        $html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
1111        $html .= '</head>';
1112        $html .= '<body>';
1113        $html .= $body;
1114        $html .= '</body>';
1115        $html .= '</html>';
1116
1117        $subject = __('language.send_rejected_notification.subject');
1118        $subject = str_replace('{{quote_id}}', $quoteId, $subject);
1119        $subject = str_replace('{{endpoint}}', ucfirst($fendpoint), $subject);
1120
1121        // FIRE-1092: same shape as send_approved_notification — record
1122        // the rejection FIRST, send the email on best-effort. Pre-fix, a
1123        // failed SendGrid call left the row in an inconsistent state
1124        // (`for_approval` was still cleared by reject_quotation but the
1125        // `rejected_at` / `rejected_by` columns stayed empty).
1126        if ($endpoint == 'orders') {
1127            if ($user->exists_in == 'approvers' || $user->exists_in == 'both') {
1128                TblQuotations::where('id', $id)->update([
1129                    'rejected_at' => date('Y-m-d H:i:s'),
1130                    'rejected_by' => $user->name,
1131                ]);
1132            } elseif ($user->exists_in == 'approvers_v2') {
1133                TblQuotations::where('id', $id)->update([
1134                    'rejected_at_v2' => date('Y-m-d H:i:s'),
1135                    'rejected_by_v2' => $user->name,
1136                ]);
1137            }
1138        } else {
1139            TblOngoingJobs::where('id', $id)->update([
1140                'rejected_at' => date('Y-m-d H:i:s'),
1141                'rejected_by' => $user->name,
1142            ]);
1143        }
1144
1145        TblNotifications::create([
1146            'user_id' => $userId,
1147            'content' => $content,
1148            'is_open' => 1,
1149            'created_by' => 'System',
1150            'link' => $url,
1151        ]);
1152
1153        $this->addUpdateLog($id, $userId, 'send_rejected_notification', null, null, 5);
1154
1155        // FIRE-1147: dispatch the email send. The rejection record, in-app
1156        // notification and audit log at L1135-1162 already landed
1157        // synchronously, so SendGrid downtime doesn't lose the action.
1158        if ($toEmail != null) {
1159            if (config('services.sendgrid.staging')) {
1160                $toEmail = $user->email;
1161            }
1162
1163            SendRejectedEmail::dispatch(
1164                toEmail: (string) $toEmail,
1165                subject: $subject,
1166                html: $html,
1167                quoteId: $quoteId,
1168            )->onQueue('email');
1169        }
1170
1171    }
1172
1173    public function update_quotation(Request $request, $id): ResponseFactory|HttpResponse
1174    {
1175        $approvalMinimumOrderSize = null;
1176        $approvalIsQuestion = null;
1177        $approvalQuestionIdsNo = null;
1178        $approvalN = null;
1179        $approvalMinimumMargin = null;
1180        $approvalId = null;
1181        $approvalForAdd = null;
1182        $approvalInvoiceMargin = null;
1183        $sendApprovalNotification = false;
1184        $sendApprovalMarginNotification = false;
1185        $needToSendReminder = false;
1186
1187        // try {
1188
1189        $data = $request->all();
1190        $id = addslashes((string) $id);
1191        $userId = addslashes((string) $data['user_id']);
1192        if (! TblQuotationsLog::where('quotation_id', $id)->exists()) {
1193            $categoryLog = $data['budget_status_id'] == 6 ? 0 : 2;
1194            $this->addUpdateLog($id, $data['user_id'], 'create_quotation', null, null, $categoryLog);
1195        }
1196        $forApproval = null;
1197        unset($data['user_id']);
1198
1199        $r = ['amount', 'order_number', 'budget_type_id', 'acceptance_date'];
1200        $job = [];
1201
1202        foreach ($data as $key => $value) {
1203            if ($value == 'null') {
1204                $data[$key] = null;
1205            }
1206
1207            if (in_array($key, $r)) {
1208                $job[$key] = $value;
1209            }
1210        }
1211
1212        $files = $request->file('files');
1213        unset($data['files']);
1214
1215        $internalFiles = $request->file('internal_files');
1216        unset($data['internal_files']);
1217
1218        $query = '
1219            SELECT
1220                SUM(CASE WHEN is_internal IS NULL THEN 1 ELSE 0 END) as external_count,
1221                SUM(CASE WHEN is_internal = 1 THEN 1 ELSE 0 END) as internal_count
1222            FROM tbl_files
1223            WHERE quotation_id = ?';
1224
1225        $counts = DB::select($query, [$id]);
1226        $fileCount = $counts[0]->external_count;
1227        $internalFileCount = $counts[0]->internal_count;
1228
1229        if ($fileCount > 0 || ! empty($files)) {
1230            $data['has_attachment'] = 1;
1231        }
1232
1233        if ($files) {
1234            $totalFiles = $fileCount + count($files);
1235            if ($totalFiles > 10) {
1236                return response(['message' => 'KO', 'error' => __('language.file_count_exceeded')]);
1237            }
1238        }
1239
1240        if ($internalFileCount > 0 || ! empty($internalFiles)) {
1241            $data['has_attachment'] = 1;
1242        }
1243
1244        if ($internalFiles) {
1245            $totalInternalFileCount = $internalFileCount + count($internalFiles);
1246            if ($totalInternalFileCount > 10) {
1247                return response(['message' => 'KO', 'error' => __('language.file_count_exceeded')]);
1248            }
1249        }
1250
1251        if (isset($data['request_date']) && isset($data['issue_date'])) {
1252            $requestDate = strtotime($data['request_date']);
1253            $issueDate = strtotime($data['issue_date']);
1254            $dateDiff = $issueDate - $requestDate;
1255            $data['duration'] = round($dateDiff / (60 * 60 * 24));
1256        }
1257
1258        $result = TblQuotations::where('id', $id)->first();
1259
1260        // FIRE-982 follow-up: the manual-create flow can leave the
1261        // frontend with a stub id that has already been deleted (either
1262        // by the duplicate-redirect retarget below, or by a separate
1263        // cleanup). Crashing on a null `$result` here surfaces as an
1264        // unhandled "Attempt to read property quote_id on null" instead
1265        // of a recoverable error the UI can show, so guard the lookup.
1266        if (! $result) {
1267            return response([
1268                'message' => 'KO',
1269                'error' => 'quotation_not_found',
1270                'id' => $id,
1271            ], 404);
1272        }
1273
1274        if ($result->quote_id != $data['quote_id']) {
1275
1276            // Look up the OTHER row that owns this quote_id (excluding the
1277            // row being updated).
1278            $existing = TblQuotations::where('quote_id', (string) $data['quote_id'])
1279                ->where('company_id', $result->company_id)
1280                ->where('id', '<>', $id)
1281                ->first();
1282
1283            if ($existing) {
1284
1285                if ($result->for_add == 1) {
1286                    // Manual-create flow: the frontend opened the "Crear
1287                    // manualmente" form and spawned a stub row; the user
1288                    // typed a quote_id that already belongs to another
1289                    // record. Treat the submission as an edit of that
1290                    // existing record — delete the orphan stub and
1291                    // retarget the update so the rest of this function
1292                    // applies the form data to `$existing`.
1293                    //
1294                    // The manual-create form sends `null` (or the literal
1295                    // string "null") for fields it doesn't expose —
1296                    // `internal_quote_id`, `box_work_g3w`, `order_number`,
1297                    // and friends. Mass-assigning those to a G3W-imported
1298                    // existing row would silently nuke its sync metadata,
1299                    // so drop empty submitted values when the existing row
1300                    // already has a value there. Real edits (e.g. user
1301                    // typed a new client name) still go through; only
1302                    // form-blank-vs-existing-populated keys are skipped.
1303                    foreach ($data as $key => $value) {
1304                        $isFormBlank = $value === null
1305                            || $value === ''
1306                            || $value === 'null';
1307                        if ($isFormBlank && ! is_null($existing->$key) && $existing->$key !== '') {
1308                            unset($data[$key]);
1309                        }
1310                    }
1311
1312                    TblQuotations::where('id', $id)->delete();
1313                    $id = $existing->id;
1314                    $result = $existing;
1315                    // fall through into the normal update path below
1316                } else {
1317                    // Non-stub edit: a real quotation is trying to rename
1318                    // its quote_id to one already owned by another row.
1319                    // Silently merging here would clobber the other row's
1320                    // data, so we keep the original protection — return an
1321                    // error and suggest the next free number.
1322                    $latestBudget = TblQuotations::where('company_id', $result->company_id)
1323                        ->orderByRaw('CAST(quote_id AS DOUBLE) DESC')
1324                        ->first();
1325
1326                    $number = $latestBudget->quote_id;
1327                    $x = true;
1328
1329                    while ($x) {
1330
1331                        if (is_numeric(substr((string) $number, -1))) {
1332                            $number++;
1333                        } else {
1334                            $number .= '1';
1335                        }
1336
1337                        $check = TblQuotations::where('company_id', $result->company_id)
1338                            ->where('quote_id', (string) $number)
1339                            ->count();
1340
1341                        if ($check == 0) {
1342                            $x = false;
1343                        }
1344                    }
1345
1346                    return response([
1347                        'message' => 'KO',
1348                        'error' => 'quote_exists',
1349                        'number' => $number,
1350                    ]);
1351                }
1352            }
1353        }
1354
1355        // Preserve existing budget_type_id when the incoming payload would
1356        // null it out. The frontend marks the field required, but the
1357        // validator is cleared on Solicitud/Visita/Anulado statuses and
1358        // re-loading a row with no type seeds the form value to null
1359        // (orders.component.ts:2265). FormData previously serialised that
1360        // null as the literal string "null", which the foreach above at
1361        // :1098 converts to PHP null — and the bare update($data) at
1362        // :1703 would then wipe a previously-set budget_type_id. The
1363        // companion frontend fix in order-form.service.ts now omits the
1364        // field when blank instead of sending "null"; this guard backs
1365        // that up and keeps $data['budget_type_id'] defined so the
1366        // downstream comparisons at :1322 and :1402 don't trip undefined
1367        // array key errors. Solicitudes with a legitimately-null
1368        // budget_type_id still write null — only populated rows are
1369        // protected.
1370        $incomingBudgetTypeId = $data['budget_type_id'] ?? null;
1371        if (
1372            $incomingBudgetTypeId === null
1373            || $incomingBudgetTypeId === ''
1374            || $incomingBudgetTypeId === 'null'
1375        ) {
1376            $data['budget_type_id'] = $result->budget_type_id;
1377        }
1378
1379        $action = 0;
1380        if ($result->created_by == null || $result->for_add == 1) {
1381            $action = 1;
1382            $data['created_by'] = $data['updated_by'];
1383            $data['for_add'] = 0;
1384        }
1385
1386        $company = TblCompanies::where('company_id', $result->company_id)->first();
1387        $commercial = TblUsers::where('name', $data['commercial'])->first();
1388        $status = TblBudgetStatus::where('budget_status_id', $data['budget_status_id'])->first();
1389
1390        // $checkQuotation = TblQuotations::where(function($query) use ($data, $result) {
1391        //     $query->where('internal_quote_id', $data['internal_quote_id'])
1392        //         ->orWhere('internal_quote_id', 'O-25/'.$data['internal_quote_id']);
1393        // })
1394        //     ->where('company_id', $result->company_id)
1395        //     ->first();
1396
1397        // if($checkQuotation) {
1398        //     $url = "orders/" . $checkQuotation->id . "?company_id=" . $checkQuotation->company_id;
1399        //     return response([
1400        //         'message' => 'KO',
1401        //         'error' => "Presupuesto ya creado. Puedes verlo <a href='$url' target='_blank'>aquí</a>."
1402        //     ]);
1403        // }
1404
1405        $limitReminderEmails = $company->limit_reminder_emails ?? 3;
1406
1407        if ($result->total_sent == $limitReminderEmails) {
1408            $data['total_sent'] = 0;
1409        }
1410
1411        if ($result->budget_status_id != $data['budget_status_id'] || $result->commercial != $data['commercial']) {
1412            if ($data['budget_status_id'] == 12) {
1413                if ($company && $commercial) {
1414                    $inProgressCount = TblQuotations::where('budget_status_id', 12)->where('company_id', $result->company_id)->where('commercial', $data['commercial'])->count();
1415                    if ($company->process_limit <= $inProgressCount) {
1416                        return response(['message' => 'KO', 'error' => 'in_progress', 'limit' => $company->process_limit]);
1417                    }
1418                } else {
1419                    return response(['message' => 'KO', 'error' => 'in_progress', 'limit' => 0]);
1420                }
1421            }
1422        }
1423
1424        $sendNotification = false;
1425        if ($result->commercial != $data['commercial']) {
1426            if (! empty($commercial)) {
1427                $createdByX = ($result->created_by == null) ? $data['created_by'] : $result->created_by;
1428                if ($createdByX != $data['commercial']) {
1429                    $action = 0;
1430                    $sendNotification = true;
1431                }
1432            }
1433        }
1434
1435        // FIRE-1092: skip the approval recompute when the budget is in a
1436        // terminal status — Aceptado (3), Rechazado (4), Rechazado
1437        // automáticamente (7), or Rechazado-duplicado (20). Once a budget
1438        // is closed (accepted or rejected) any later edits — bookkeeping
1439        // adjustments, final invoice amount, etc. — should NOT drag the
1440        // row back into "Requiere aprobación". The resulting status is
1441        // taken from the incoming payload when present, falling back to
1442        // the row's current status so an edit that doesn't touch
1443        // `budget_status_id` still honours the terminal state.
1444        $resultingStatusId = (int) ($data['budget_status_id'] ?? $result->budget_status_id);
1445        $terminalStatuses = [3, 4, 7, 20];
1446        $isTerminalStatus = in_array($resultingStatusId, $terminalStatuses, true);
1447
1448        // A budget in "En proceso" (12) is already in execution — approval
1449        // belongs to the pre-execution stage, so edits made while the work is
1450        // underway must not drag the row back into "Requiere aprobación" nor
1451        // fire an approval notification. Skip the recompute for this status,
1452        // same as the terminal statuses above.
1453        $isInProgressStatus = $resultingStatusId === 12;
1454
1455        if (! $isTerminalStatus
1456            && ! $isInProgressStatus
1457            && (isset($data['amount']) || isset($data['budget_type_id']) || isset($data['customer_type_id']) || isset($data['invoice_margin']) || isset($data['question_enabled']))
1458        ) {
1459            if ($company) {
1460
1461                $n = 0;
1462                $invoiceMargin = 0;
1463                $minimumMargin = 0;
1464                $minimumOrderSize = 0;
1465                // $project is only assigned in the "financial fields changed"
1466                // branch below. When the user edits e.g. just the email,
1467                // none of those fields move and $project stays unset — PHP 8
1468                // then throws "Undefined variable" on line ~1312. Seed it so
1469                // the later `if ($project)` short-circuits safely.
1470                $project = null;
1471
1472                if ($result->amount != $data['amount'] ||
1473                    $result->budget_type_id != $data['budget_type_id'] ||
1474                    $result->customer_type_id != $data['customer_type_id'] ||
1475                    $result->invoice_margin != $data['invoice_margin']
1476                ) {
1477                    $project = TblProjectTypes::where('company_id', $company->company_id)->where('budget_type_id', $data['budget_type_id'])->first();
1478                    $customerTypeIds = [];
1479
1480                    if ($project) {
1481                        if (! empty($project->customer_type_ids)) {
1482                            $customerTypeIds = array_map(intval(...), explode(',', (string) $project->customer_type_ids));
1483                        }
1484                        if ($project->minimum_order_size != null && in_array($data['customer_type_id'], $customerTypeIds)) {
1485                            if ($data['amount'] >= $project->minimum_order_size) {
1486                                $data['for_approval'] = 1;
1487                                $n = 1;
1488                            }
1489                        }
1490                        $minimumOrderSize = $project->minimum_order_size;
1491
1492                        // FIRE-1092: `minimum_order_size_v2` is a decimal column
1493                        // and Eloquent returns it as the string "0.0000" when the
1494                        // operator left it at 0 (their "no tier-2 threshold" placeholder).
1495                        // PHP 8 no longer coerces numeric strings to null in loose
1496                        // comparisons, so `"0.0000" != null` is TRUE — promotion to
1497                        // dual-level approval would fire even when the operator meant
1498                        // "single level is enough", which strands budgets forever in
1499                        // companies that have no `tbl_approvers_v2` rows. Require the
1500                        // threshold to be strictly positive before promoting.
1501                        if ($n == 1 && (float) $project->minimum_order_size_v2 > 0 && in_array($data['customer_type_id'], $customerTypeIds)) {
1502                            if ($data['amount'] >= $project->minimum_order_size_v2) {
1503                                $data['for_approval'] = 3;
1504                                $n = 3;
1505                            }
1506                        }
1507                    }
1508
1509                } else {
1510                    if (! empty($company->customer_type_ids)) {
1511                        $customerTypeIds = array_map(intval(...), explode(',', (string) $company->customer_type_ids));
1512                    }
1513                    if ($company->minimum_order_size != null && in_array($data['customer_type_id'], $customerTypeIds)) {
1514                        if ($data['amount'] >= $company->minimum_order_size) {
1515                            $data['for_approval'] = 1;
1516                            $n = 1;
1517                        }
1518                        $minimumOrderSize = $company->minimum_order_size;
1519
1520                        // FIRE-1092: same `> 0` guard as the project-level branch
1521                        // above — `!= null` is unreliable for a decimal column.
1522                        if ($n == 1 && (float) $company->minimum_order_size_v2 > 0 && in_array($data['customer_type_id'], $customerTypeIds)) {
1523                            if ($data['amount'] >= $company->minimum_order_size_v2) {
1524                                $data['for_approval'] = 3;
1525                                $n = 3;
1526                            }
1527                        }
1528                    }
1529                }
1530
1531                if ($data['budget_margin_enabled'] > 0) {
1532                    $costOfLabor = $data['cost_of_labor'];
1533                    $totalCostOfJob = $data['total_cost_of_job'];
1534
1535                    if ($totalCostOfJob > 0) {
1536                        $invoiceMargin = $data['invoice_margin'] ?? 0;
1537                    }
1538
1539                    $minimumMargin = $company->minimum_margin;
1540                    if (! empty($company->customer_type_ids)) {
1541                        $customerTypeIds = array_map(intval(...), explode(',', (string) $company->customer_type_ids));
1542                    }
1543
1544                    if ($project) {
1545                        $minimumMargin = $project->minimum_margin;
1546                        $minimumOrderSize = $project->minimum_order_size;
1547                        if (! empty($project->customer_type_ids)) {
1548                            $customerTypeIds = array_map(intval(...), explode(',', (string) $project->customer_type_ids));
1549                        }
1550                    }
1551
1552                    if ($invoiceMargin < $minimumMargin && $invoiceMargin != null && $invoiceMargin != 0) {
1553                        if (in_array($data['customer_type_id'], $customerTypeIds)) {
1554                            $data['for_approval'] = 1;
1555                            $n = 2;
1556                        }
1557                    }
1558                }
1559
1560                $isQuestion = 0;
1561                $questionIdsNo = [];
1562                if (isset($data['question_enabled'])) {
1563                    if ($data['question_ids_no'] != $result->question_ids_no
1564                        || $data['budget_type_id'] != $result->budget_type_id
1565                        || $data['customer_type_id'] != $result->customer_type_id
1566                        || $data['amount'] != $result->amount) {
1567                        if ($data['question_enabled'] > 0 && $n == 0) {
1568                            if (! empty($data['question_ids_no'])) {
1569                                $questionIdsNo = array_map(intval(...), explode(',', (string) $data['question_ids_no']));
1570
1571                                if ($company->workflow_budget_size != null) {
1572                                    if ($data['amount'] >= $company->workflow_budget_size) {
1573                                        $isQuestion = 1;
1574                                        $data['for_approval'] = 1;
1575                                        $n = 1;
1576                                    }
1577                                }
1578                                $minimumOrderSize = $company->workflow_budget_size;
1579
1580                            }
1581                        }
1582                    }
1583                }
1584
1585                if (($n == 1 || $n == 3) && $result->for_approval != $n) {
1586                    $sendApprovalNotification = true;
1587                    $approvalMinimumOrderSize = $minimumOrderSize;
1588                    $approvalId = $result->id;
1589                    $approvalForAdd = $result->for_add;
1590                    $approvalIsQuestion = $isQuestion;
1591                    $approvalQuestionIdsNo = $questionIdsNo;
1592                    $approvalN = $n;
1593                }
1594
1595                if ($n == 2) {
1596                    $sendApprovalMarginNotification = true;
1597                    $approvalMinimumMargin = $minimumMargin;
1598                    $approvalId = $result->id;
1599                    $approvalForAdd = $result->for_add;
1600                    $approvalInvoiceMargin = $invoiceMargin;
1601                }
1602
1603                // Persist WHICH kind of approval this is so it stays
1604                // recoverable after save. `for_approval` alone collapses a
1605                // technical (order-size, $n=1) and a financial (margin,
1606                // $n=2) single-level approval into the same value 1, so once
1607                // posted there's no way to tell them apart — only the
1608                // dual-level case (3) is distinguishable. `approval_type`
1609                // keeps the original reason:
1610                //   1 = technical (order size / tbl_approvers, V1)
1611                //   2 = financial (margin / tbl_approvers_v2, V2)
1612                //   3 = both
1613                if ($n == 1 || $n == 2 || $n == 3) {
1614                    $data['approval_type'] = $n;
1615                }
1616            }
1617        }
1618
1619        $data['updated_at'] = date('Y-m-d H:i:s');
1620        $job['updated_at'] = $data['updated_at'];
1621
1622        $data['g3w_warning'] = 0;
1623
1624        if ($result->for_add == 1) {
1625            TblCompanies::where('company_id', $result->company_id)->update(['last_id' => $data['quote_id'], 'before_last_id' => $data['quote_id']]);
1626        }
1627
1628        $forApproval = @$data['for_approval'] ?? null;
1629
1630        if ($forApproval != null) {
1631            $data['approved_at'] = null;
1632            $data['approved_by'] = null;
1633            $data['rejected_at'] = null;
1634            $data['rejected_by'] = null;
1635            $data['approved_at_v2'] = null;
1636            $data['approved_by_v2'] = null;
1637            $data['rejected_at_v2'] = null;
1638            $data['rejected_by_v2'] = null;
1639        }
1640
1641        $actualQuotationValue = TblQuotations::where('id', $id)->first();
1642
1643        $differences = $this->compareArrays($data, $actualQuotationValue);
1644        $primaryAprovalsFields = ['budget_type_id', 'customer_type_id', 'budget_margin_enabled', 'amount', 'invoice_margin', 'margin_for_the_company', 'margin_on_invoice_per_day_per_worker', 'gross_margin'];
1645
1646        if (is_null($actualQuotationValue->customer_type_id)) {
1647            $needToSendReminder = true;
1648        }
1649
1650        foreach ($differences as $field => $value) {
1651            if (in_array($field, $primaryAprovalsFields)) {
1652                $needToSendReminder = true;
1653            }
1654            $statusId = $data['budget_status_id'] ?? $actualQuotationValue->budget_status_id;
1655            $categoryLog = $statusId == 6 ? 1 : 4;
1656            $this->addUpdateLog($id, $userId, $field, $value['oldData'], $value['newData'], $categoryLog);
1657        }
1658
1659        // check if the quotation are a solicitud and the user write the internal quote id
1660
1661        $budgetRequest = TblQuotations::where('id', $id)->first();
1662
1663        if (
1664            ($budgetRequest->budget_status_id == 6 || $budgetRequest->budget_status_id == 8)
1665            && (! is_null($data['internal_quote_id'] ?? null) && $data['internal_quote_id'] !== '')
1666            && (TblQuotations::where('internal_quote_id', $data['internal_quote_id'])
1667                ->where('company_id', $company->company_id)
1668                ->whereNotIn('budget_status_id', [6, 8])
1669                ->exists())
1670            && ($id !== '')
1671        ) {
1672            $createdAt = $budgetRequest->created_at;
1673            $lastFollowUpComment = $budgetRequest->last_follow_up_comment;
1674
1675            TblQuotations::where('id', $id)->first()->update(['internal_quote_id', $data['internal_quote_id']]);
1676
1677            $solicitudLogs = TblQuotationsLog::where('quotation_id', $id)->get();
1678
1679            $budget = TblQuotations::where('internal_quote_id', $data['internal_quote_id'])->where('company_id', $company->company_id)->first();
1680            TblFiles::where('quotation_id', $id)->where('is_internal', 1)->update(['quotation_id' => $budget->id]);
1681
1682            $this->callDeleteQuotation($id, $company->company_id, $userId, $data['updated_by']);
1683
1684            $id = $budget->id;
1685
1686            $dataToChange = array_intersect_key($data, array_flip([
1687                'internal_quote_id',
1688                'client',
1689                'phone_number',
1690                'email',
1691                'source_id',
1692                'created_by',
1693                'updated_by',
1694                'updated_at',
1695                'request_date',
1696                'last_follow_up_comment',
1697            ]));
1698
1699            // FIRE-1063: when merging a solicitud into an existing
1700            // G3W-derived budget, the solicitud's manually-entered
1701            // client info wins (it's the latest sales-side input).
1702            // Empty solicitud values fall through to the budget's
1703            // existing data, which traces back to G3W's
1704            // `servicio → cliente` chain when the budget was originally
1705            // synced — so the resulting per-field cascade is
1706            // solicitud → G3W servicio → G3W cliente. Pre-fix only
1707            // `phone_number` had this guard; `client` and `email` were
1708            // unconditionally written by the merge, so an empty
1709            // solicitud field would wipe the budget's good value.
1710            foreach (['phone_number', 'client', 'email'] as $field) {
1711                $value = $data[$field] ?? null;
1712                if ($value === null
1713                    || trim((string) $value) === ''
1714                    || strtolower(trim((string) $value)) === 'null'
1715                    || empty($dataToChange[$field])
1716                ) {
1717                    unset($dataToChange[$field]);
1718                }
1719            }
1720
1721            // FIRE-1063: pre-fix the merge built $dataToChange but
1722            // never wrote it. The duplicate `if` block below at
1723            // :1554 was the only writer, and it can never fire because
1724            // `$id` has been reassigned to the budget — its
1725            // budget_status_id is no longer 6/8. As a result, the
1726            // solicitud's client/phone/email and the merge metadata
1727            // (preserved created_at, concatenated follow-up comment)
1728            // never made it onto the budget row even though the rest
1729            // of the merge — file moves, deletion of the solicitud —
1730            // completed normally.
1731            $dataToChange['budget_status_id'] = $budget->budget_status_id;
1732            $dataToChange['created_at'] = $createdAt;
1733            $dataToChange['last_follow_up_comment'] = trim(
1734                ($budget->last_follow_up_comment ?? '')."\n".($lastFollowUpComment ?? '')
1735            );
1736
1737            // FIRE-1063: if the budget was at 22 (Correo erroneo) only
1738            // because its email was previously empty/invalid, and the
1739            // merge fills in a now-valid email with no SendGrid
1740            // evidence of a real delivery failure (`x_message_id IS
1741            // NULL`), demote out of bucket 22 to 11 (Listo para enviar)
1742            // — the natural pre-send bucket. Without this, the row
1743            // stays in "Correo erroneo" forever even though the email
1744            // problem that put it there has been resolved by the
1745            // merge.
1746            $mergedEmail = $dataToChange['email'] ?? $budget->email;
1747            if (
1748                (int) $budget->budget_status_id === 22
1749                && $budget->x_message_id === null
1750                && $mergedEmail !== null
1751                && ! $this->isInvalidEmail($mergedEmail)
1752            ) {
1753                $dataToChange['budget_status_id'] = 11;
1754            }
1755
1756            TblQuotations::where('id', $id)->update($dataToChange);
1757
1758            if ($solicitudLogs->isNotEmpty()) {
1759                $nuevosLogs = array_map(function ($log) use ($budget) {
1760                    unset($log['id']);
1761                    $log['quotation_id'] = $budget->id;
1762
1763                    return $log;
1764                }, $solicitudLogs->toArray());
1765
1766                TblQuotationsLog::insert($nuevosLogs);
1767            }
1768
1769            // check if the quotation are a solicitud and the user write the internal quote id
1770
1771            $budgetRequest = TblQuotations::where('id', $id)->first();
1772
1773            if (
1774                ($budgetRequest->budget_status_id == 6 || $budgetRequest->budget_status_id == 8)
1775                && (! is_null($data['internal_quote_id'] ?? null) && $data['internal_quote_id'] !== '')
1776                && TblQuotations::where('internal_quote_id', $data['internal_quote_id'])->where('company_id', $company->company_id)->exists()
1777                && (! is_null($id) && $id !== '')
1778            ) {
1779                $createdAt = $budgetRequest->created_at;
1780                $lastFollowUpComment = $budgetRequest->last_follow_up_comment;
1781
1782                TblQuotations::where('id', $id)->first()->update(['internal_quote_id', $data['internal_quote_id']]);
1783
1784                $solicitudLogs = TblQuotationsLog::where('quotation_id', $id)->get();
1785
1786                $budget = TblQuotations::where('internal_quote_id', $data['internal_quote_id'])->where('company_id', $company->company_id)->first();
1787                TblFiles::where('quotation_id', $id)->where('is_internal', 1)->update(['quotation_id' => $budget->id]);
1788
1789                $this->callDeleteQuotation($id, $company->company_id, $userId, $data['updated_by']);
1790
1791                $id = $budget->id;
1792
1793                $dataToChange = array_intersect_key($data, array_flip([
1794                    'internal_quote_id',
1795                    'client',
1796                    'phone_number',
1797                    'email',
1798                    'source_id',
1799                    'created_by',
1800                    'updated_by',
1801                    'updated_at',
1802                    'request_date',
1803                    'last_follow_up_comment',
1804                ]));
1805
1806                if ($data['phone_number'] === null || $data['phone_number'] === '' || empty($dataToChange['phone_number'])) {
1807                    unset($dataToChange['phone_number']);
1808                }
1809
1810                $dataToChange['budget_status_id'] = $budget->budget_status_id;
1811                $dataToChange['created_at'] = $createdAt;
1812                $dataToChange['last_follow_up_comment'] = $budget->last_follow_up_comment."\n".$lastFollowUpComment;
1813
1814                $data['g3w_warning'] = 0;
1815
1816                if (
1817                    in_array($budget->budget_status_id, [13, 14]) ||
1818                    ! $dataToChange['source_id'] ||
1819                    (! $budget->budget_type_id || $budget->budget_type_id == 0 || $budget->budget_type_id == 16) ||
1820                    empty(trim((string) $dataToChange['client'])) ||
1821                    empty(trim((string) $dataToChange['email']))
1822                ) {
1823                    $data['g3w_warning'] = 1;
1824                }
1825
1826                TblQuotations::where('id', $id)->update($dataToChange);
1827
1828                if (! empty($solicitudLogs)) {
1829                    $nuevosLogs = array_map(function (array $log) use ($budget): array {
1830                        unset($log['id']);
1831                        $log['quotation_id'] = $budget->id;
1832
1833                        return $log;
1834                    }, $solicitudLogs);
1835
1836                    TblQuotationsLog::insert($nuevosLogs);
1837                }
1838
1839                $data['g3w_warning'] = 0;
1840
1841                if (
1842                    in_array($budget->budget_status_id, [13, 14]) ||
1843                    ! $dataToChange['source_id'] ||
1844                    (! $budget->budget_type_id || $budget->budget_type_id == 16) ||
1845                    empty(trim($dataToChange['client'])) ||
1846                    empty(trim($dataToChange['email']))
1847                ) {
1848                    $data['g3w_warning'] = 1;
1849                }
1850
1851                TblQuotations::where('id', $id)->update($dataToChange);
1852
1853                if ($solicitudLogs->isNotEmpty()) {
1854                    $nuevosLogs = array_map(function ($log) use ($budget) {
1855                        unset($log['id']);
1856                        $log['quotation_id'] = $budget->id;
1857
1858                        return $log;
1859                    }, $solicitudLogs->toArray());
1860
1861                    TblQuotations::where('id', $id)->update(
1862                        [
1863                            'accepted_at' => $data['updated_at'],
1864                            'accepted_by' => $data['updated_by'],
1865                        ]
1866                    );
1867                }
1868
1869            }
1870
1871        } else {
1872            // FIRE-864: if email is invalid (blacklisted, malformed, or
1873            // empty) and status is sendable, set to 22 (Correo erroneo).
1874            if (isset($data['email']) && $this->isInvalidEmail($data['email'])
1875                && isset($data['budget_status_id']) && in_array($data['budget_status_id'], [1, 2, 11, 17])) {
1876                $data['budget_status_id'] = 22;
1877            }
1878
1879            TblQuotations::where('id', $id)->update($data);
1880        }
1881
1882        TblOngoingJobs::where('quotation_id', $id)->update($job);
1883
1884        $this->update_commercial_numbers($result->company_id);
1885
1886        if ($result->budget_status_id != $data['budget_status_id']
1887            && $data['budget_status_id'] == 3) {
1888            $this->send_acceptance_notification($result->id, $result->company_id, $userId, $data['updated_by']);
1889
1890            TblQuotations::where('id', $id)->update(
1891                [
1892                    'accepted_at' => $data['updated_at'],
1893                    'accepted_by' => $data['updated_by'],
1894                ]
1895            );
1896
1897            // Promote lead to general when quotation is accepted
1898            $quotation = TblQuotations::find($id);
1899            if ($quotation && $quotation->client_id) {
1900                Client::where('id', $quotation->client_id)
1901                    ->where('client_type_id', Client::TYPE_LEAD)
1902                    ->update(['client_type_id' => Client::TYPE_GENERAL]);
1903            }
1904        }
1905
1906        if ($files) {
1907
1908            $uploadedFiles = [];
1909            $i = 0;
1910
1911            $combinedFilesSize = 0;
1912            foreach ($files as $file) {
1913                $i++;
1914                $origFilename = $file->getClientOriginalName();
1915                $filename = $result->id.'-'.$result->company_id.'-FC'.time().'-'.$origFilename;
1916
1917                $combinedFilesSize = $combinedFilesSize + $file->getSize();
1918
1919                if ($combinedFilesSize > 25000000) {
1920                    return response(['message' => 'KO', 'error' => __('language.file_size_exceeded')]);
1921                }
1922
1923                if (in_array($origFilename, $uploadedFiles)) {
1924                    $origFilename = $origFilename.$i;
1925                }
1926
1927                // $fileContent = file_get_contents($file->getRealPath());
1928                // $fileHash = hash('sha256', $fileContent);
1929
1930                $s3path = Storage::disk('s3')->putFileAs(
1931                    'uploads',
1932                    $file,
1933                    $filename,
1934                    [
1935                        'ContentType' => $file->getMimeType(),
1936                    ]
1937                );
1938
1939                TblFiles::create(
1940                    [
1941                        'quotation_id' => $id,
1942                        'original_name' => $origFilename,
1943                        'filename' => $filename,
1944                        'uploaded_by' => $data['updated_by'],
1945                        // 'file' => $fileContent,
1946                        'file_size' => $file->getSize(),
1947                        // 'file_hash' => $fileHash,
1948                        'mime_type' => $file->getMimeType(),
1949                        'uploaded_at' => now(),
1950                    ]
1951                );
1952                $this->addUpdateLog($id, $data['updated_by'], 'upload_attachment', null, $filename, 4);
1953
1954                $uploadedFiles[] = $file->getClientOriginalName();
1955            }
1956        }
1957
1958        if ($internalFiles) {
1959
1960            $uploadedFiles = [];
1961            $i = 0;
1962
1963            $combinedFilesSize = 0;
1964            foreach ($internalFiles as $file) {
1965                $i++;
1966                $origFilename = $file->getClientOriginalName();
1967                $filename = $result->id.'-'.$result->company_id.'-FI'.time().'-'.$origFilename;
1968
1969                $combinedFilesSize = $combinedFilesSize + $file->getSize();
1970
1971                if ($combinedFilesSize > 25000000) {
1972                    return response(['message' => 'KO', 'error' => __('language.file_size_exceeded')]);
1973                }
1974
1975                if (in_array($origFilename, $uploadedFiles)) {
1976                    $origFilename = $origFilename.$i;
1977                }
1978
1979                // $fileContent = file_get_contents($file->getRealPath());
1980                // $fileHash = hash('sha256', $fileContent);
1981
1982                $s3path = Storage::disk('s3')->putFileAs(
1983                    'uploads',
1984                    $file,
1985                    $filename,
1986                    [
1987                        'ContentType' => $file->getMimeType(),
1988                    ]
1989                );
1990
1991                TblFiles::create(
1992                    [
1993                        'quotation_id' => $id,
1994                        'original_name' => $origFilename,
1995                        'filename' => $filename,
1996                        'uploaded_by' => $data['updated_by'],
1997                        // 'file' => $fileContent,
1998                        'file_size' => $file->getSize(),
1999                        // 'file_hash' => $fileHash,
2000                        'mime_type' => $file->getMimeType(),
2001                        'uploaded_at' => now(),
2002                        'is_internal' => 1,
2003                    ]
2004                );
2005                $this->addUpdateLog($id, $data['updated_by'], 'upload_attachment', null, $filename, 4);
2006
2007                $uploadedFiles[] = $file->getClientOriginalName();
2008            }
2009        }
2010
2011        $query = "SELECT
2012                        a.id,
2013                        a.quote_id,
2014                        a.company_id,
2015                        b.name company_name,
2016                        a.client,
2017                        c.name client_type,
2018                        c.customer_type_id,
2019                        a.request_date,
2020                        a.visit_date,
2021                        a.issue_date,
2022                        a.acceptance_date,
2023                        a.internal_quote_id,
2024                        DATE_FORMAT(a.request_date, '%d/%m/%Y') request_date_translate,
2025                        DATE_FORMAT(a.issue_date, '%d/%m/%Y') issue_date_translate,
2026                        DATE_FORMAT(a.acceptance_date, '%d/%m/%Y') acceptance_date_translate,
2027                        DATE_FORMAT(a.last_follow_up_date, '%d/%m/%Y') last_follow_up_date_translate,
2028                        -- DATEDIFF(a.issue_date, a.request_date) duration,
2029                        a.phone_number,
2030                        a.email,
2031                        a.duration,
2032                        a.order_number,
2033                        d.name 'type',
2034                        d.budget_type_id,
2035                        e.name 'status',
2036                        e.budget_status_id,
2037                        f.name source,
2038                        f.source_id,
2039                        a.amount,
2040                        g.name reason_for_not_following_up,
2041                        a.reason_for_not_following_up_id,
2042                        a.reason_for_rejection_id,
2043                        a.last_follow_up_date,
2044                        a.last_follow_up_comment,
2045                        CASE WHEN a.reason_for_rejection_id IS NULL THEN a.reason_for_rejection ELSE h.name END reason_for_rejection,
2046                        a.commercial,
2047                        a.created_by,
2048                        a.created_at,
2049                        a.updated_by,
2050                        a.for_approval,
2051                        a.approval_type,
2052                        a.box_work_g3w,
2053                        a.people_assigned_to_the_job,
2054                        a.duration_of_job_in_days,
2055                        a.estimated_cost_of_materials,
2056                        a.budget_margin_enabled,
2057                        a.question_enabled,
2058                        a.cost_of_labor,
2059                        a.total_cost_of_job,
2060                        CASE WHEN a.budget_margin_enabled > 0 THEN a.invoice_margin ELSE NULL END invoice_margin,
2061                        CASE WHEN a.budget_margin_enabled > 0 THEN a.margin_for_the_company ELSE NULL END margin_for_the_company,
2062                        a.margin_on_invoice_per_day_per_worker,
2063                        a.revenue_per_date_per_worked,
2064                        a.commission_cost,
2065                        a.commission_pct,
2066                        a.gross_margin,
2067                        a.labor_percentage,
2068                        a.question_ids,
2069                        a.question_ids_no,
2070                        a.approved_at,
2071                        a.approved_by,
2072                        a.rejected_at,
2073                        a.rejected_by,
2074                        a.approved_at_v2,
2075                        a.approved_by_v2,
2076                        a.rejected_at_v2,
2077                        a.rejected_by_v2,
2078                        a.accepted_at,
2079                        a.accepted_by,
2080                        a.is_validated,
2081                        a.resource_id,
2082                        a.x_status,
2083                        a.likehood,
2084                        a.updated_at
2085                    FROM
2086                        tbl_quotations a
2087                        LEFT JOIN tbl_companies b ON a.company_id = b.company_id
2088                        LEFT JOIN tbl_customer_types c ON a.customer_type_id = c.customer_type_id
2089                        LEFT JOIN tbl_budget_types d ON a.budget_type_id = d.budget_type_id
2090                        LEFT JOIN tbl_budget_status e ON a.budget_status_id = e.budget_status_id
2091                        LEFT JOIN tbl_sources f ON a.source_id = f.source_id
2092                        LEFT JOIN tbl_reason_for_not_following_up g ON a.reason_for_not_following_up_id = g.reason_for_not_following_up_id
2093                        LEFT JOIN tbl_reason_for_rejection h ON a.reason_for_rejection_id = h.reason_for_rejection_id
2094                    WHERE a.id = {$id}";
2095
2096        $result = DB::select($query);
2097
2098        if ($sendNotification) {
2099            $this->send_notification(
2100                $commercial->email,
2101                $userId,
2102                $data['quote_id'],
2103                $id,
2104                $status->name,
2105                $commercial->id,
2106                $company->name,
2107                0,
2108                $company->company_id,
2109                $data['internal_quote_id']
2110            );
2111        }
2112
2113        if ($sendApprovalNotification && ($needToSendReminder || is_null($result[0]->for_approval ?? null))) {
2114            $this->send_approval_notification(
2115                $data['amount'],
2116                $data['budget_type_id'],
2117                $data['customer_type_id'],
2118                $approvalMinimumOrderSize,
2119                $data['quote_id'],
2120                $approvalId,
2121                $company->name,
2122                @$data['created_by'] ?? @$data['updated_by'],
2123                $userId,
2124                $approvalForAdd,
2125                $commercial ? $commercial->email : null,
2126                $company->company_id,
2127                'orders',
2128                $approvalIsQuestion,
2129                $approvalQuestionIdsNo,
2130                $approvalN
2131            );
2132        }
2133
2134        if ($sendApprovalMarginNotification && $needToSendReminder) {
2135            $this->send_approval_margin_notification(
2136                $data['amount'],
2137                $data['budget_type_id'],
2138                $data['customer_type_id'],
2139                $approvalMinimumMargin,
2140                $data['quote_id'],
2141                $approvalId,
2142                $company->name,
2143                @$data['created_by'] ?? @$data['updated_by'],
2144                $userId,
2145                $approvalForAdd,
2146                $commercial ? $commercial->email : null,
2147                $approvalInvoiceMargin,
2148                $company->company_id,
2149                'orders'
2150            );
2151        }
2152
2153        if (isset($result['budget_status_id']) && $result['budget_status_id'] == 6) {
2154            $data = [
2155                'id' => $result['id'] ?? null,
2156                'client' => $result['client'] ?? null,
2157                'email' => $result['email'] ?? null,
2158                'phone_number' => $result['phone_number'] ?? null,
2159                'last_follow_up_comment' => $result['last_follow_up_comment'] ?? null,
2160                'quote_id' => $result['quote_id'] ?? null,
2161                'request_date' => date('Y-m-d'),
2162                'updated_by' => 'IA',
2163                'user_id' => $result['user_id'] ?? null,
2164                'commercial' => $result['commercial'] ?? null,
2165                'budget_status_id' => $result['budget_status_id'],
2166                'internal_quote_id' => $result['internal_quote_id'] ?? null,
2167            ];
2168
2169            // FIRE-1147: same Lambda duplicate-checker as create_quotation,
2170            // here on the update path when the row is a Solicitud (6). Async
2171            // dispatch — response was already unused inline (3-5 s wasted).
2172            CheckQuotationDuplicate::dispatch($data)->onQueue('default');
2173        }
2174
2175        // FIRE-1145: was Cache::flush() — update_quotation affects list_quotations + commercial counters.
2176        ResultCache::forgetDomain(['quotations', 'users']);
2177
2178        return response(['message' => 'OK', 'data' => $result, 'for_approval' => $forApproval]);
2179
2180        // } catch (\Exception $e) {
2181        //     return response(['message' => 'KO', 'error' => $e->getMessage()]);
2182        // }
2183
2184    }
2185
2186    /**
2187     * @return array{newData: mixed, oldData: mixed}[]
2188     */
2189    private function compareArrays($data, $quotations): array
2190    {
2191        $differences = [];
2192        $attributes = $quotations->getAttributes();
2193
2194        foreach ($data as $field => $valueData) {
2195            if (array_key_exists($field, $attributes)) {
2196                $valueQuotations = $quotations->$field;
2197
2198                $valueData = $this->convertValue($valueData);
2199                $valueQuotations = $this->convertValue($valueQuotations);
2200
2201                if ($valueData !== $valueQuotations) {
2202                    if ($this->isEmpty($valueData) && $this->isEmpty($valueQuotations)) {
2203                        continue;
2204                    }
2205
2206                    $differences[$field] = [
2207                        'newData' => $valueData,
2208                        'oldData' => $valueQuotations,
2209                    ];
2210                }
2211            }
2212        }
2213
2214        return $differences;
2215    }
2216
2217    private function isEmpty($value): bool
2218    {
2219        return is_null($value) || $value === '' || $value === 0 || $value === '0';
2220    }
2221
2222    private function convertValue($value)
2223    {
2224        if ($value === null) {
2225            return null;
2226        }
2227
2228        if (is_numeric($value)) {
2229            return (int) $value;
2230        }
2231
2232        if ($value === 'NULL' || $value === 'null') {
2233            return null;
2234        }
2235
2236        if (is_string($value)) {
2237            if (preg_match('/^(\d{4}-\d{2}-\d{2})( \d{2}:\d{2}:\d{2})?$/', $value, $matches)) {
2238                return $matches[1];
2239            }
2240        }
2241
2242        return $value;
2243    }
2244
2245    protected function callDeleteQuotation($id, $company_id, $userId, $user)
2246    {
2247        $request = new Request([
2248            'company_id' => $company_id,
2249            'user_id' => $userId,
2250            'updated_by' => $user,
2251            'ids' => [$id],
2252            'filterModel' => [],
2253            'sortModel' => [],
2254            'searchText' => '',
2255            'ids_not_in' => [],
2256        ]);
2257
2258        return $this->delete_quotation($request, true);
2259    }
2260
2261    public function get_quotation($id): ResponseFactory|HttpResponse
2262    {
2263
2264        try {
2265
2266            $id = addslashes((string) $id);
2267
2268            $query = "SELECT
2269                        a.id,
2270                        a.quote_id,
2271                        a.company_id,
2272                        b.name company_name,
2273                        a.client,
2274                        c.name client_type,
2275                        c.customer_type_id,
2276                        s.name segment,
2277                        s.segment_id,
2278                        a.request_date,
2279                        a.visit_date,
2280                        a.issue_date,
2281                        a.acceptance_date,
2282                        a.internal_quote_id,
2283                        DATE_FORMAT(a.request_date, '%d/%m/%Y') request_date_translate,
2284                        DATE_FORMAT(a.issue_date, '%d/%m/%Y') issue_date_translate,
2285                        DATE_FORMAT(a.acceptance_date, '%d/%m/%Y') acceptance_date_translate,
2286                        DATE_FORMAT(a.last_follow_up_date, '%d/%m/%Y') last_follow_up_date_translate,
2287                        a.phone_number,
2288                        a.email,
2289                        a.duration,
2290                        a.order_number,
2291                        d.name 'type',
2292                        d.budget_type_id,
2293                        e.name 'status',
2294                        e.budget_status_id,
2295                        f.name source,
2296                        f.source_id,
2297                        a.amount,
2298                        g.name reason_for_not_following_up,
2299                        a.reason_for_not_following_up_id,
2300                        a.reason_for_rejection_id,
2301                        a.last_follow_up_date,
2302                        a.last_follow_up_comment,
2303                        CASE WHEN a.reason_for_rejection_id IS NULL THEN a.reason_for_rejection ELSE h.name END reason_for_rejection,
2304                        a.commercial,
2305                        a.created_by,
2306                        a.created_at,
2307                        a.updated_by,
2308                        a.updated_at,
2309                        a.for_approval,
2310                        a.approval_type,
2311                        a.box_work_g3w,
2312                        a.people_assigned_to_the_job,
2313                        a.duration_of_job_in_days,
2314                        a.estimated_cost_of_materials,
2315                        a.budget_margin_enabled,
2316                        a.question_enabled,
2317                        a.cost_of_labor,
2318                        a.total_cost_of_job,
2319                        CASE WHEN a.budget_margin_enabled > 0 THEN a.invoice_margin ELSE NULL END invoice_margin,
2320                        CASE WHEN a.budget_margin_enabled > 0 THEN a.margin_for_the_company ELSE NULL END margin_for_the_company,
2321                        a.margin_on_invoice_per_day_per_worker,
2322                        a.revenue_per_date_per_worked,
2323                        a.commission_cost,
2324                        a.commission_pct,
2325                        a.gross_margin,
2326                        a.labor_percentage,
2327                        a.question_ids,
2328                        a.question_ids_no,
2329                        a.approved_at,
2330                        a.approved_by,
2331                        a.rejected_at,
2332                        a.rejected_by,
2333                        a.approved_at_v2,
2334                        a.approved_by_v2,
2335                        a.rejected_at_v2,
2336                        a.rejected_by_v2,
2337                        a.accepted_at,
2338                        a.accepted_by,
2339                        a.is_validated,
2340                        a.resource_id,
2341                        a.sync_import,
2342                        a.sync_import_edited,
2343                        a.user_create_by_g3w,
2344                        a.user_commercial_by_g3w,
2345                        a.g3w_warning,
2346                        a.g3w_warning_fields,
2347                        a.id_solicitud_duplicity,
2348                        a.x_status
2349                    FROM
2350                        tbl_quotations a
2351                        LEFT JOIN tbl_companies b ON a.company_id = b.company_id
2352                        LEFT JOIN tbl_customer_types c ON a.customer_type_id = c.customer_type_id
2353                        LEFT JOIN tbl_segments s ON a.segment_id = s.segment_id
2354                        LEFT JOIN tbl_budget_types d ON a.budget_type_id = d.budget_type_id
2355                        LEFT JOIN tbl_budget_status e ON a.budget_status_id = e.budget_status_id
2356                        LEFT JOIN tbl_sources f ON a.source_id = f.source_id
2357                        LEFT JOIN tbl_reason_for_not_following_up g ON a.reason_for_not_following_up_id = g.reason_for_not_following_up_id
2358                        LEFT JOIN tbl_reason_for_rejection h ON a.reason_for_rejection_id = h.reason_for_rejection_id
2359                    WHERE a.id = {$id}";
2360
2361            $result = DB::select($query);
2362
2363            // FIRE-1145: was Cache::flush() — read-only path, zero benefit. Removed.
2364
2365            return response(['message' => 'OK', 'data' => $result]);
2366
2367        } catch (\Exception $e) {
2368            report(AppException::fromException($e, 'GET_QUOTATION_EXCEPTION'));
2369
2370            return response(['message' => 'KO', 'error' => $e->getMessage()]);
2371        }
2372    }
2373
2374    public function get_quotation_log($id)
2375    {
2376        return TblQuotationsLog::where('quotation_id', $id)->get();
2377    }
2378
2379    public function send_notification($toEmail = null, $userId = null, $quoteId = null, $id = null, $status = null, $sendUserId = null, $companyName = null, $action = null, $companyId = null, $g3wQuoteId = null): void
2380    {
2381
2382        $user = TblUsers::where('id', $userId)->first();
2383
2384        $imgpath = file_get_contents(public_path('fireservicetitan.png'));
2385
2386        $url = config('app.frontend_url')."orders/{$id}?company_id={$companyId}";
2387        $href = "<a href='{$url}'>{$quoteId}</a>";
2388
2389        $body = '';
2390        $subject = '';
2391
2392        if ($g3wQuoteId) {
2393            $quoteId = $quoteId." - G3W #{$g3wQuoteId}";
2394        }
2395
2396        if ($action == 1) {
2397            $body = __('language.email_notification.body_created');
2398            $body = str_replace('{{creator}}', $user->name, $body);
2399            $subject = str_replace('{{quote_id}}', $quoteId, __('language.email_notification.subject_created'));
2400        } else {
2401            $body = __('language.email_notification.body_assigned');
2402            $body = str_replace('{{assignee}}', $user->name, $body);
2403            $subject = str_replace('{{quote_id}}', $quoteId, __('language.email_notification.subject_assigned'));
2404        }
2405
2406        $body = str_replace('{{quote_id}}', $href, $body);
2407        $body = str_replace('{{company}}', $companyName, $body);
2408        $body = str_replace('{{status}}', $status, $body);
2409
2410        $content = $body;
2411
2412        $body .= '<p>Fire Service Titan</p>';
2413        $body .= "<img src='cid:fireservicetitan' style='height: 45px;' />";
2414
2415        $html = '<!DOCTYPE html>';
2416        $html .= '<html>';
2417        $html .= '<head>';
2418        $html .= '<meta charset="UTF-8">';
2419        $html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
2420        $html .= '</head>';
2421        $html .= '<body>';
2422        $html .= $body;
2423        $html .= '</body>';
2424        $html .= '</html>';
2425
2426        if ($toEmail != null) {
2427            $email = new Mail;
2428
2429            if (config('services.sendgrid.staging')) {
2430                $toEmail = $user->email;
2431            }
2432
2433            $email->setFrom('fire@fire.es', 'Fire Service Titan');
2434            $email->setSubject($subject);
2435            $email->addTo($toEmail);
2436            $email->addContent('text/html', $html);
2437
2438            $email->addAttachment(
2439                $imgpath,
2440                'image/png',
2441                'fireservicetitan.png',
2442                'inline',
2443                'fireservicetitan'
2444            );
2445
2446            $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
2447
2448            try {
2449                $response = $sendgrid->send($email);
2450                SendgridLogger::log($email, $response);
2451            } catch (\Throwable $sendException) {
2452                SendgridLogger::logException($email, $sendException);
2453                throw $sendException;
2454            }
2455
2456            if ($response->statusCode() == 202) {
2457                Log::channel('email_log')->info('ID:'.$quoteId.' : '.$status.' : '.$toEmail.' - EMAIL NOTIFICATION SENT');
2458                $this->addUpdateLog($id, $sendUserId, 'send_notification', null, null, 5);
2459
2460                TblNotifications::create(
2461                    [
2462                        'user_id' => $sendUserId,
2463                        'content' => $content,
2464                        'is_open' => 1,
2465                        'created_by' => 'System',
2466                        'link' => $url,
2467                    ]
2468                );
2469            } else {
2470                $error = true;
2471                Log::channel('email_log')->error('ID:'.$quoteId.' : '.$status.' : '.$toEmail.' - '.$response->body());
2472            }
2473        }
2474
2475    }
2476
2477    public function delete_quotation(Request $request, $isFromDeleteCall = false): ResponseFactory|HttpResponse
2478    {
2479
2480        try {
2481
2482            $data = $request->all();
2483            $result = [];
2484
2485            if (count($data['ids']) > 1) {
2486                $u = TblUsers::where('id', $data['user_id'])->first();
2487
2488                if ($u->role_id != 1) {
2489                    return response(['message' => 'KO', 'error' => 'more_than_one']);
2490                }
2491            }
2492
2493            $r = new Request([
2494                'filterModel' => $data['filterModel'],
2495                'sortModel' => $data['sortModel'],
2496                'start' => 0,
2497                'end' => 999999999,
2498                'company_id' => $data['company_id'],
2499                'user_id' => $data['user_id'],
2500                'ids' => $data['ids'],
2501                'searchText' => $data['searchText'],
2502                'ids_not_in' => $data['ids_not_in'],
2503            ]);
2504
2505            $d = [];
2506
2507            $result = $this->list_quotations($r);
2508            $result = $result->original['data'];
2509
2510            $outputArray = [];
2511
2512            foreach ($result as $item) {
2513                if (isset($item->id)) {
2514                    $outputArray[] = $item->id;
2515                }
2516            }
2517
2518            $ids = implode(',', $outputArray);
2519
2520            if ($outputArray) {
2521
2522                TblQuotations::whereIn('id', $outputArray)->update(
2523                    [
2524                        'updated_at' => date('Y-m-d H:i:s'),
2525                        'updated_by' => $data['updated_by'],
2526                        'for_add' => $isFromDeleteCall ? 2 : ($data['for_add'] ?? 0),
2527                        'reason_id' => ($data['reason_id'] ?? null),
2528                        'reason_for_deletion' => ($data['reason_for_deletion'] ?? null),
2529                    ]
2530                );
2531
2532                $query = "INSERT INTO tbl_quotations_deleted
2533                        SELECT * FROM tbl_quotations WHERE id IN ({$ids})";
2534
2535                DB::select($query);
2536
2537                TblQuotations::whereIn('id', $outputArray)->delete();
2538                TblFiles::whereIn('quotation_id', $outputArray)->delete();
2539            }
2540
2541            foreach ($outputArray as $id) {
2542                $this->addUpdateLog($id, $data['user_id'], 'delete', null, null, 6);
2543            }
2544
2545            // FIRE-1145: was Cache::flush() — delete_quotation affects list_quotations + commercial counters.
2546            ResultCache::forgetDomain(['quotations', 'users']);
2547
2548            return response(['message' => 'OK', 'data' => $result]);
2549
2550        } catch (\Exception $e) {
2551            report(AppException::fromException($e, 'DELETE_QUOTATION_EXCEPTION'));
2552
2553            return response(['message' => 'KO', 'error' => $e->getMessage()]);
2554        }
2555
2556    }
2557
2558    public function getBlacklistEmails(): array
2559    {
2560        return [
2561            "no\.no",
2562            "tiene\.email",
2563            "test\.com",
2564            "no\.tiene",
2565            "prueba\.com",
2566            "nomail@nomail\.com",
2567            "notiene@notiene\.notiene",
2568        ];
2569    }
2570
2571    public function validate_email(Request $request)
2572    {
2573        $email = $request->input('email');
2574
2575        if (! $email || trim($email) === '') {
2576            return response()->json([
2577                'valid' => false,
2578                'reason' => 'El email está vacío',
2579            ]);
2580        }
2581
2582        $emailPattern = '/^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$/';
2583        $emails = explode(',', $email);
2584
2585        foreach ($emails as $e) {
2586            $trimmed = trim($e);
2587
2588            if (! preg_match($emailPattern, $trimmed)) {
2589                return response()->json([
2590                    'valid' => false,
2591                    'reason' => "El email '{$trimmed}' tiene un formato incorrecto",
2592                ]);
2593            }
2594
2595            if ($this->isBlacklistedEmail($trimmed)) {
2596                return response()->json([
2597                    'valid' => false,
2598                    'reason' => "El email '{$trimmed}' está en la lista de direcciones no válidas",
2599                ]);
2600            }
2601        }
2602
2603        return response()->json([
2604            'valid' => true,
2605            'reason' => null,
2606        ]);
2607    }
2608
2609    private function isBlacklistedEmail(?string $email): bool
2610    {
2611        if (! $email || trim($email) === '') {
2612            return true;
2613        }
2614
2615        // FIRE-864 (2026-04-30): use `getBlacklistEmails()` as the single
2616        // source of truth. Pre-fix this method had its own hardcoded
2617        // pattern that was inconsistent with the array — missing `no\.no`
2618        // and `no\.tiene`, and `^no@` was over-anchored (missed
2619        // `xxx@no.no`). The `Emails con error` badge filter and the
2620        // backfill migration both already consume `getBlacklistEmails()`,
2621        // so aligning here closes the gap that left rows in `Listo para
2622        // enviar` after Berta's reclassification expected to catch them.
2623        $pattern = '/'.implode('|', $this->getBlacklistEmails()).'/i';
2624
2625        $emails = explode(',', $email);
2626        foreach ($emails as $e) {
2627            if (preg_match($pattern, trim($e))) {
2628                return true;
2629            }
2630        }
2631
2632        return false;
2633    }
2634
2635    /**
2636     * FIRE-864 (2026-04-30): a row's email is "invalid" when it's empty,
2637     * malformed (failing the standard email regex), OR matches the
2638     * blacklist. The runtime classifier in `update_quotation` was only
2639     * checking `isBlacklistedEmail()`, so malformed emails like
2640     * "ricardo@" or "no email" never got reclassified to budget_status 22
2641     * and stayed in `Listo para enviar` indefinitely.
2642     *
2643     * The `invalid_email=1` filter in `list_quotations` (line 2974) and
2644     * the SQL backfill migration both use the same three-pronged check;
2645     * this mirrors that for in-process classification.
2646     */
2647    private function isInvalidEmail(?string $email): bool
2648    {
2649        if (! $email || trim($email) === '') {
2650            return true;
2651        }
2652
2653        $emailPattern = '/^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$/';
2654        foreach (explode(',', $email) as $e) {
2655            $trimmed = trim($e);
2656            if ($trimmed === '') {
2657                return true;
2658            }
2659            if (! preg_match($emailPattern, $trimmed)) {
2660                return true;
2661            }
2662        }
2663
2664        return $this->isBlacklistedEmail($email);
2665    }
2666
2667    public function list_quotations(Request $request)
2668    {
2669
2670        // try {
2671
2672        $data = $request->all();
2673        $companyId = addslashes((string) $data['company_id']);
2674        $userId = addslashes((string) $data['user_id']);
2675        $filter = $data['filterModel'];
2676        $sort = $data['sortModel'];
2677        $result = [];
2678        $subquery = '';
2679        $where = '';
2680        $having = '';
2681        $orderBy = '';
2682
2683        // FIRE-1174: gate the 10-min query cache to avoid per-keystroke churn.
2684        // Cache keys embed the literal $searchText so every short prefix the
2685        // user types (s → so → sol → soli …) is a distinct miss + write.
2686        // Caching is useful for the "page loaded, no filter yet" cold path
2687        // and for stable searches (≥ 3 chars); skip everything in between.
2688        $cacheSearch = trim((string) ($data['searchText'] ?? ''));
2689        $shouldCacheList = $cacheSearch === '' || strlen($cacheSearch) >= 3;
2690        $start = addslashes((string) $data['start']);
2691        $end = addslashes((string) $data['end']);
2692        $totalRowCount = 0;
2693        $withFilters = '';
2694        $logFilter = @$data['log_filter'];
2695        $isInvalidEmail = (isset($data['invalid_email']) && $data['invalid_email'] == 1);
2696        $isFollowUp = (isset($data['is_follow_up']) && $data['is_follow_up'] == 1);
2697        $isNoEmailFollowUp = (isset($data['is_no_email_follow_up']) && $data['is_no_email_follow_up'] == 1);
2698
2699        $filterType = [
2700            'contains' => "LIKE '%[value]%'",
2701            'notContains' => "NOT LIKE '%[value]%'",
2702            'equals' => "= '[value]'",
2703            'notEqual' => "<> '[value]'",
2704            'startsWith' => "LIKE '[value]%'",
2705            'endsWith' => "LIKE '%[value]'",
2706            'blank' => 'IS NULL',
2707            'notBlank' => 'IS NOT NULL',
2708            'lessThan' => '< [value]',
2709            'lessThanOrEqual' => '<= [value]',
2710            'greaterThan' => '> [value]',
2711            'greaterThanOrEqual' => '>= [value]',
2712            'inRange' => 'BETWEEN [value1] AND [value2]',
2713            'in' => 'IN ([value])',
2714        ];
2715
2716        /*if(isset($data['internal_quote_id']) && count($data['internal_quote_id']) > 0){
2717            $internalIds = implode(",", $data['internal_quote_id']);
2718            $where = " AND a.internal_quote_id IN ({$internalIds}) ";
2719        }*/
2720
2721        // Filter to positive integers before emitting IN clauses. Frontend
2722        // can send [null] (from [NaN]) when localStorage holds a non-numeric
2723        // string, and implode(',', [null]) produces "" — that ships
2724        // `a.id IN ()` and crashes MySQL with 1064.
2725        if (isset($data['ids']) && is_array($data['ids'])) {
2726            $numericIds = array_values(array_filter(
2727                array_map('intval', $data['ids']),
2728                fn ($n) => $n > 0
2729            ));
2730            if (count($numericIds) > 0) {
2731                $where .= ' AND a.id IN ('.implode(',', $numericIds).') ';
2732            }
2733        }
2734
2735        if (isset($data['ids_not_in']) && is_array($data['ids_not_in'])) {
2736            $numericIdsNotIn = array_values(array_filter(
2737                array_map('intval', $data['ids_not_in']),
2738                fn ($n) => $n > 0
2739            ));
2740            if (count($numericIdsNotIn) > 0) {
2741                $where .= ' AND a.id NOT IN ('.implode(',', $numericIdsNotIn).') ';
2742            }
2743        }
2744
2745        $lasLeftJoin = '';
2746        $whereBlocked = '';
2747
2748        if (isset($data['last_follow_up_date']) && ! empty($data['last_follow_up_date'])) {
2749            if ($data['last_follow_up_date'] == 1) {
2750
2751                $lasLeftJoin = " LEFT JOIN (
2752                        SELECT
2753                          a.id,
2754                          SUBSTRING_INDEX(
2755                            SUBSTRING_INDEX(a.email, ',', n.digit + 1),
2756                            ',',
2757                            -1
2758                          ) AS email_domain
2759                        FROM
2760                          tbl_quotations a
2761                          INNER JOIN (
2762                            SELECT
2763                              0 AS digit
2764                            UNION ALL
2765                            SELECT
2766                              1
2767                            UNION ALL
2768                            SELECT
2769                              2
2770                            UNION ALL
2771                            SELECT
2772                              3
2773                            UNION ALL
2774                            SELECT
2775                              4
2776                            UNION ALL
2777                            SELECT
2778                              5
2779                            UNION ALL
2780                            SELECT
2781                              6
2782                            UNION ALL
2783                            SELECT
2784                              7
2785                            UNION ALL
2786                            SELECT
2787                              8
2788                            UNION ALL
2789                            SELECT
2790                              9
2791                          ) n ON LENGTH(
2792                            REPLACE(a.email, ',', '')
2793                          ) <= LENGTH(a.email)- n.digit
2794                          GROUP BY a.id
2795                      ) temp ON a.id = temp.id ";
2796
2797                $whereBlocked = " AND a.last_follow_up_date < NOW()
2798                            AND a.budget_status_id IN (2)
2799                            AND a.email IS NOT NULL
2800                            AND a.email <> ''
2801                            AND NOT EXISTS (
2802                                SELECT
2803                                1
2804                                FROM
2805                                tbl_blocked_domains bd
2806                                WHERE
2807                                temp.email_domain LIKE CONCAT('%', bd.domain, '%')
2808                                AND bd.company_id = a.company_id
2809                            )
2810                            AND a.last_follow_up_date IS NOT NULL
2811                            AND a.reason_for_not_following_up_id IS NULL
2812                            AND a.last_follow_up_date > 0
2813                            AND a.total_sent < b.limit_reminder_emails
2814                            AND a.for_add = 0 ";
2815            }
2816        }
2817
2818        if (isset($data['visit_date']) && ! empty($data['visit_date'])) {
2819            if ($data['visit_date'] == 1) {
2820                $where = " AND DATE_FORMAT(a.visit_date, '%Y-%m-%d') <= DATE_FORMAT(NOW(), '%Y-%m-%d') ";
2821            }
2822        }
2823
2824        if ($companyId != 0) {
2825            $where .= " AND a.company_id = {$companyId} ";
2826        } elseif ($this->companyId) {
2827            $where .= " AND a.company_id IN ({$this->companyId}";
2828        }
2829
2830        $matchScoreCol = '';
2831        $matchScoreOrderBy = '';
2832
2833        if (isset($data['searchText']) && $data['searchText'] != null) {
2834
2835            $availableParameters = [
2836                'a.quote_id',
2837                'a.internal_quote_id',
2838                'a.box_work_g3w',
2839                's.name',
2840                'b.name',
2841                'a.client',
2842                'c.name',
2843                'a.phone_number',
2844                'a.email',
2845                'a.order_number',
2846                'a.request_date',
2847                'a.issue_date',
2848                'a.acceptance_date',
2849                'a.created_at',
2850                'a.updated_at',
2851                'a.rejected_at',
2852                'a.accepted_at',
2853                'd.name',
2854                'e.name',
2855                'f.name',
2856                'a.amount',
2857                'g.name',
2858                'a.last_follow_up_comment',
2859                'a.x_status',
2860                'h.name',
2861                'a.commercial',
2862                'a.user_commercial_by_g3w',
2863                'a.user_create_by_g3w',
2864                'a.created_by',
2865                'a.updated_by',
2866                'a.approved_by',
2867                'a.rejected_by',
2868                'a.accepted_by',
2869                'a.sync_import',
2870                'a.sync_import_edited',
2871                'a.g3w_warning',
2872            ];
2873
2874            $searchText = addslashes((string) $data['searchText']);
2875            $searchTextArray = explode(' ', $searchText);
2876
2877            $searchArray = [];
2878            $splitSearchArray = [];
2879            $matchScoreArray = [];
2880            $sc = 1;
2881            foreach ($availableParameters as $field) {
2882                if ($field == 'a.client' || $field == 'a.amount' || $field == 'a.created_at') {
2883                    $sc = 3;
2884                } elseif ($field == 'a.acceptance_date') {
2885                    $sc = 2;
2886                } else {
2887                    $sc = 1;
2888                }
2889
2890                $l = "{$field} LIKE '%{$searchText}%'";
2891                if ($field == 'a.last_follow_up_comment') {
2892                    $l = "{$field} = '{$searchText}'";
2893                } else {
2894
2895                    $d = "IFNULL((LENGTH(LOWER({$field})) - LENGTH(REPLACE(LOWER({$field}), LOWER('{$searchText}'), ''))) / LENGTH(LOWER('{$searchText}')), 0) * {$sc}";
2896
2897                    if (count($searchTextArray) > 1) {
2898                        foreach ($searchTextArray as $word) {
2899                            if (! is_numeric($word)) {
2900                                $d .= " + IFNULL((LENGTH(LOWER({$field})) - LENGTH(REPLACE(LOWER({$field}), LOWER('{$word}'), ''))) / LENGTH(LOWER('{$word}')), 0) * {$sc}";
2901                            }
2902                        }
2903                    }
2904
2905                    array_push($matchScoreArray, $d);
2906                }
2907
2908                if (is_numeric($searchText)) {
2909                    array_push($searchArray, "({$l} OR {$field} = CAST('{$searchText}' AS UNSIGNED))");
2910                } else {
2911                    array_push($searchArray, "({$l} OR DATE_FORMAT({$field}, '%d/%m/%Y') = DATE_FORMAT(STR_TO_DATE('{$searchText}', '%d/%m/%Y'), '%d/%m/%Y'))");
2912                }
2913
2914                if (count($searchTextArray) > 1) {
2915                    foreach ($searchTextArray as $word) {
2916
2917                        $l = "{$field} LIKE '%{$word}%'";
2918                        if ($field == 'a.last_follow_up_comment') {
2919                            $l = "{$field} = '{$word}'";
2920                        }
2921
2922                        if (is_numeric($word)) {
2923                            array_push($splitSearchArray, "{$l} OR {$field} = CAST('{$word}' AS UNSIGNED)");
2924                        } else {
2925                            array_push($splitSearchArray, "{$l} OR DATE_FORMAT({$field}, '%d/%m/%Y') = DATE_FORMAT(STR_TO_DATE('{$word}', '%d/%m/%Y'), '%d/%m/%Y')");
2926                        }
2927                    }
2928                }
2929
2930                $sc = 1;
2931            }
2932
2933            if (count($splitSearchArray) > 0) {
2934                $splitSearchArray = implode(' OR ', $splitSearchArray);
2935                $splitSearchArray = " OR ({$splitSearchArray}";
2936            } else {
2937                $splitSearchArray = '';
2938            }
2939
2940            $searchArray = implode(' OR ', $searchArray);
2941            $matchScoreArray = implode(',', $matchScoreArray);
2942            $matchScoreCol = ", GREATEST({$matchScoreArray}) match_score";
2943            $matchScoreOrderBy = 'match_score DESC,';
2944            $where .= " AND ({$searchArray} {$splitSearchArray})";
2945        }
2946
2947        if (count($sort) > 0) {
2948            $field = $sort[0]['colId'];
2949            $sortBy = $sort[0]['sort'];
2950
2951            if (strpos($field, 'translate') !== false) {
2952                $field = str_replace('_translate', '', $field);
2953            } else {
2954                if ($field == 'client_type') {
2955                    $field = 'c.name';
2956                } elseif ($field == 'segment') {
2957                    $field = 's.name';
2958                } elseif ($field == 'type') {
2959                    $field = 'd.name';
2960                } elseif ($field == 'status') {
2961                    $field = 'e.name';
2962                } elseif ($field == 'source') {
2963                    $field = 'g.name';
2964                } elseif ($field == 'reason_for_not_following_up') {
2965                    $field = 'g.name';
2966                } elseif ($field == 'reason_for_rejection') {
2967                    $field = 'h.name';
2968                } elseif ($field == 'amount') {
2969                    $field = 'CAST(a.amount AS DOUBLE)';
2970                } elseif ($field == 'duration') {
2971                    $field = 'CAST(a.duration AS DOUBLE)';
2972                } elseif ($field == 'quote_id' || $field == 'internal_quote_id') {
2973                    $field = "CAST(a.{$field} AS DOUBLE)";
2974                } elseif ($field == 'company_name') {
2975                    $field = 'b.name';
2976                }
2977
2978            }
2979
2980            if ($matchScoreOrderBy) {
2981                $matchScoreOrderBy = ', match_score DESC';
2982            }
2983
2984            $orderBy = " ORDER BY {$field} {$sortBy} {$matchScoreOrderBy}";
2985        } else {
2986            $orderBy = " ORDER BY {$matchScoreOrderBy} a.id DESC";
2987        }
2988
2989        foreach ($filter as $key => $data) {
2990            if (strpos($key, 'translate') !== false) {
2991
2992                $field = str_replace('_translate', '', $key);
2993                if ($field == 'created_at') {
2994                    $field = 'a.created_at';
2995                } elseif ($field == 'last_follow_up_date') {
2996                    $field = 'a.last_follow_up_date';
2997                } elseif ($field == 'issue_date') {
2998                    $field = 'a.issue_date';
2999                } elseif ($field == 'request_date') {
3000                    $field = 'a.request_date';
3001                } elseif ($field == 'acceptance_date') {
3002                    $field = 'a.acceptance_date';
3003                } elseif ($field == 'internal_quote_id') {
3004                    $field = 'a.internal_quote_id';
3005                }
3006
3007                $whereDates = '';
3008                $z = 0;
3009
3010                if (isset($data['filters']) && ! empty($data['filters'])) {
3011                    $yearsMonths = [];
3012                    $yearsWeeks = [];
3013                    $yearsMW = [];
3014                    foreach ($data['filters'] as $yearKey => $yearData) {
3015
3016                        if ($yearData['isChecked']) {
3017
3018                            if ($yearData['isCheckedAllMonths'] && $yearData['isCheckedAllWeeks']) {
3019                                if ($z > 0) {
3020                                    $whereDates .= " OR YEAR($field) = {$yearKey} ";
3021                                } else {
3022                                    $whereDates .= " YEAR($field) = {$yearKey} ";
3023                                }
3024                            } else {
3025
3026                                if ($yearData['isCheckedAllWeeks']) {
3027                                    for ($i = 0; $i < count($yearData['weeks']); $i++) {
3028                                        if ($yearData['weeks'][$i]['isChecked']) {
3029                                            array_push($yearsMW, " YEARWEEK({$field}, 1) = '{$yearKey}{$yearData['weeks'][$i]['value']}");
3030                                        }
3031                                    }
3032                                }
3033
3034                                if ($yearData['isCheckedAllMonths']) {
3035                                    for ($i = 0; $i < count($yearData['months']); $i++) {
3036                                        if ($yearData['months'][$i]['isChecked']) {
3037                                            array_push($yearsMW, " DATE_FORMAT({$field}, '%Y%m') = '{$yearKey}{$yearData['months'][$i]['value']}");
3038                                        }
3039                                    }
3040                                }
3041
3042                                if (! $yearsMW) {
3043                                    if (! $yearData['isCheckedAllMonths']) {
3044                                        if ($z > 0) {
3045                                            $whereDates .= " OR YEAR($field) = {$yearKey} ";
3046                                        } else {
3047                                            $whereDates .= " YEAR($field) = {$yearKey} ";
3048                                        }
3049                                    }
3050                                }
3051                            }
3052
3053                            $z++;
3054                        }
3055
3056                    }
3057
3058                    if ($yearsMW) {
3059                        $whereDates .= implode(' OR ', $yearsMW);
3060                    }
3061                }
3062
3063                $whereDataUptoToday = '';
3064                if (isset($data['isDataUptoToday'])) {
3065                    if ($data['isDataUptoToday']) {
3066                        $whereDates = '';
3067                        $whereDataUptoToday .= " AND {$field} < NOW() AND {$field} > 0 ";
3068                    }
3069                }
3070
3071                $whereBlanks = '';
3072                if (isset($data['isBlanks'])) {
3073                    if ($data['isBlanks']) {
3074                        $conj = 'OR';
3075                        if ($whereDates == '') {
3076                            $conj = '';
3077                        }
3078                        $whereBlanks .= " {$conj} {$field} IS NULL ";
3079                    } else {
3080                        $conj = 'AND';
3081                        if ($whereDates == '') {
3082                            $conj = '';
3083                        }
3084                        $whereBlanks .= " {$conj} {$field} IS NOT NULL ";
3085                    }
3086                }
3087
3088                $where .= " AND ({$whereDates} {$whereBlanks} {$whereDataUptoToday}";
3089            } else {
3090                if ($data['filterType'] == 'number') {
3091                    if (array_key_exists('operator', $data)) {
3092                        if ($data['condition1']['type'] != 'blank' && $data['condition2']['type'] != 'notBlank') {
3093                            $data['condition1']['filter'] = addslashes($data['condition1']['filter']);
3094                            $data['condition2']['filter'] = addslashes($data['condition2']['filter']);
3095
3096                            if ($data['condition1']['type'] == 'inRange') {
3097                                $data['condition1']['filterTo'] = addslashes($data['condition1']['filterTo']);
3098                                $inRange = str_replace('[value1]', $data['condition1']['filter'], $filterType['inRange']);
3099                                $val1 = str_replace('[value2]', $data['condition1']['filterTo'], $inRange);
3100                            } else {
3101                                $val1 = str_replace('[value]', $data['condition1']['filter'], $filterType[$data['condition1']['type']]);
3102                            }
3103
3104                            if ($data['condition2']['type'] == 'inRange') {
3105                                $data['condition2']['filterTo'] = addslashes($data['condition2']['filterTo']);
3106                                $inRange = str_replace('[value1]', $data['condition2']['filter'], $filterType['inRange']);
3107                                $val2 = str_replace('[value2]', $data['condition2']['filterTo'], $inRange);
3108                            } else {
3109                                $val2 = str_replace('[value]', $data['condition2']['filter'], $filterType[$data['condition2']['type']]);
3110                            }
3111
3112                        } else {
3113                            $val1 = $filterType[$data['condition1']['type']];
3114                            $val2 = $filterType[$data['condition2']['type']];
3115                        }
3116
3117                        $where .= " AND a.{$key} {$val1} {$data['operator']} a.{$key} {$val2} ";
3118                    } else {
3119                        if ($data['type'] != 'blank' && $data['type'] != 'notBlank') {
3120                            $data['filter'] = addslashes($data['filter']);
3121
3122                            if ($data['type'] == 'inRange') {
3123                                $data['filterTo'] = addslashes($data['filterTo']);
3124                                $inRange = str_replace('[value1]', $data['filter'], $filterType['inRange']);
3125                                $val = str_replace('[value2]', $data['filterTo'], $inRange);
3126                            } else {
3127                                $val = str_replace('[value]', $data['filter'], $filterType[$data['type']]);
3128                            }
3129                        } else {
3130                            $val = $filterType[$data['type']];
3131                        }
3132
3133                        $where .= " AND a.{$key} {$val} ";
3134                    }
3135                }
3136
3137                if ($data['filterType'] == 'text') {
3138                    if ($key == 'id') {
3139                        continue;
3140                    }
3141
3142                    if (array_key_exists('operator', $data)) {
3143                        $val1 = '';
3144                        $val2 = '';
3145                        if ($data['condition1']['type'] != 'blank' && $data['condition2']['type'] != 'notBlank') {
3146                            $data['condition1']['filter'] = addslashes($data['condition1']['filter']);
3147                            $val1 = str_replace('[value]', $data['condition1']['filter'], $filterType[$data['condition1']['type']]);
3148                        }
3149
3150                        if ($data['condition2']['type'] != 'blank' && $data['condition2']['type'] != 'notBlank') {
3151                            $data['condition2']['filter'] = addslashes($data['condition2']['filter']);
3152                            $val2 = str_replace('[value]', $data['condition2']['filter'], $filterType[$data['condition2']['type']]);
3153                        }
3154
3155                        $where .= " AND {$key} {$val1} {$data['operator']} {$key} {$val2} ";
3156                    } else {
3157
3158                        $type = $data['type'];
3159                        $filter = $data['filter'];
3160
3161                        if (($type === 'in' || $type === 'contains') && strpos($filter, ',') !== false) {
3162                            $values = explode(',', $filter);
3163                            $escaped = array_map('addslashes', $values);
3164                            $val = "IN ('".implode("','", $escaped)."')";
3165                        } elseif ($type !== 'blank' && $type !== 'notBlank') {
3166                            $data['filter'] = addslashes($data['filter']);
3167                            $val = str_replace('[value]', $data['filter'], $filterType[$type]);
3168                        } else {
3169                            $val = $filterType[$type];
3170                        }
3171
3172                        $where .= " AND {$key} {$val} ";
3173
3174                    }
3175                }
3176
3177                if ($data['filterType'] == 'set') {
3178                    $statusName = $key;
3179
3180                    if ($key == 'client_type') {
3181                        $statusName = 'c.name';
3182                    } elseif ($key == 'segment') {
3183                        $statusName = 's.name';
3184                    } elseif ($key == 'type') {
3185                        $statusName = 'd.name';
3186                    } elseif ($key == 'status') {
3187                        $statusName = 'e.name';
3188                    } elseif ($key == 'source') {
3189                        $statusName = 'f.name';
3190                    } elseif ($key == 'reason_for_not_following_up') {
3191                        $statusName = 'g.name';
3192                    } elseif ($key == 'reason_for_rejection') {
3193                        $statusName = 'h.name';
3194                    } elseif ($key == 'created_by') {
3195                        $statusName = 'a.created_by';
3196                    } elseif ($key == 'has_attachment') {
3197                        $statusName = 'a.has_attachment';
3198                        if ($data['values']) {
3199                            foreach ($data['values'] as $k => $v) {
3200                                if ($v == 'No') {
3201                                    $data['values'][$k] = 0;
3202                                } else {
3203                                    $data['values'][$k] = 1;
3204                                }
3205                            }
3206                        }
3207                    } elseif ($key == 'for_approval') {
3208                        $statusName = 'a.for_approval';
3209                        if ($data['values']) {
3210                            // FIRE-1092: a "Sí" selection must surface BOTH
3211                            // single-level (`for_approval = 1`) and
3212                            // dual-level (`for_approval = 3`) pending rows.
3213                            // Pre-fix it mapped to `1` only, hiding every
3214                            // dual-level pending approval from the filter.
3215                            // Expand the single value into both ids so the
3216                            // downstream IN(...) clause matches both.
3217                            $expanded = [];
3218                            foreach ($data['values'] as $v) {
3219                                if ($v == 'No') {
3220                                    $expanded[] = 0;
3221                                } else {
3222                                    $expanded[] = 1;
3223                                    $expanded[] = 3;
3224                                }
3225                            }
3226                            $data['values'] = array_values(array_unique($expanded));
3227                        }
3228                    } elseif ($key == 'sync_import') {
3229                        // FIRE-1159 follow-up #2: the "Origen" filter must
3230                        // mirror the frontend `originLabel()` rule, which is
3231                        // user_create_by_g3w-first: 'flu' -> FluencyLabs,
3232                        // 'dig' -> Diggening, regardless of sync_import; only
3233                        // when neither alias matches do we use sync_import = 1
3234                        // to mean G3W and otherwise Manual.
3235                        // Earlier iterations of this fix gated FluencyLabs /
3236                        // Diggening on `sync_import = 1`, which dropped rows
3237                        // like order 158956 (sync_import = 0, user_create_by_g3w
3238                        // = 'flu' — Titan row later matched to its G3W twin via
3239                        // internal_quote_id) out of the FluencyLabs filter
3240                        // even though it is unambiguously a FluencyLabs origin.
3241                        if (! empty($data['values'])) {
3242                            $parts = [];
3243                            foreach ($data['values'] as $v) {
3244                                if ($v === 'FluencyLabs') {
3245                                    $parts[] = "(LOWER(TRIM(a.user_create_by_g3w)) = 'flu')";
3246                                } elseif ($v === 'Diggening') {
3247                                    $parts[] = "(LOWER(TRIM(a.user_create_by_g3w)) = 'dig')";
3248                                } elseif ($v === 'G3W') {
3249                                    $parts[] = "(a.sync_import = 1 AND (a.user_create_by_g3w IS NULL OR LOWER(TRIM(a.user_create_by_g3w)) NOT IN ('flu','dig')))";
3250                                } else { // Manual (default)
3251                                    $parts[] = "((a.sync_import IS NULL OR a.sync_import != 1) AND (a.user_create_by_g3w IS NULL OR LOWER(TRIM(a.user_create_by_g3w)) NOT IN ('flu','dig')))";
3252                                }
3253                            }
3254                            $where .= ' AND ('.implode(' OR ', $parts).') ';
3255                        }
3256
3257                        continue;
3258                    } elseif ($key == 'g3w_warning') {
3259                        $statusName = 'a.g3w_warning';
3260                        if ($data['values']) {
3261                            foreach ($data['values'] as $k => $v) {
3262                                if ($v == 'No') {
3263                                    $data['values'][$k] = 0;
3264                                } else {
3265                                    $data['values'][$k] = 1;
3266                                }
3267                            }
3268                        }
3269                    } elseif ($key == 'company_name') {
3270                        $statusName = 'b.name';
3271                    }
3272
3273                    $val = implode("','", $data['values']);
3274
3275                    if (in_array(null, $data['values'], true)) {
3276                        $where .= " AND ({$statusName} IN ('{$val}') OR {$statusName} IS NULL) ";
3277                    } else {
3278                        $where .= " AND {$statusName} IN ('{$val}') ";
3279                    }
3280                }
3281            }
3282        }
3283
3284        $whereSendToClient = $where;
3285
3286        $offset = $start;
3287        $limit = $end - $start;
3288
3289        $subquery = ",(SELECT can_write FROM tbl_company_users WHERE company_id = a.company_id AND user_id = {$userId}) can_write";
3290
3291        // Quotations accepted without acceptance_date
3292        // Quotations with state "No encontrado" or "Estado no reconocido en FST"
3293        // Quotations with not comercial in out database
3294        // Phone number on null
3295        // Source on null
3296        // Client name on null
3297        // Budget Type on null
3298        if (isset($data['g3w_warning'])) {
3299            $g3w_warning = $data['g3w_warning'] == 'No' ? 0 : 1;
3300            /*$where .= "
3301            AND a.sync_import = 1
3302                AND (
3303                    a.budget_status_id IN (13, 14)
3304                    OR (
3305                        a.commercial IS NULL
3306                        OR NOT EXISTS (
3307                            SELECT 1 FROM tbl_users u WHERE u.name = a.commercial
3308                        )
3309                        OR a.phone_number IS NULL
3310                        OR a.source_id IS NULL
3311                        OR a.budget_type_id IS NULL
3312                        OR (a.client IS NULL OR TRIM(a.client) = '')
3313                        OR (a.email IS NULL OR TRIM(a.email) = '')
3314                    )
3315                )
3316             ";*/
3317            $where .= '
3318                AND (a.sync_import = 1 OR a.sync_import_edited = 1)
3319                AND a.g3w_warning = '.$g3w_warning;
3320        }
3321
3322        if ($isInvalidEmail) {
3323            $blacklist = implode('|', $this->getBlacklistEmails());
3324
3325            // FIRE-864 follow-up (04/05): regex anchored with `^...$` so
3326            // garbage-prefix-then-valid-email values like `no tiene@notien.com`
3327            // are flagged as malformed. The pre-fix un-anchored pattern matched
3328            // the substring `tiene@notien.com` and let those rows slip past the
3329            // bucket reclassifier — leaving 17 Cataluña rows in `Enviado/Nuevo/
3330            // Listo para enviar` despite showing up in the badge count.
3331            $where .= "
3332                AND
3333                (
3334                    a.x_status IN ('Error','Error - Bounce','Error - Spam')
3335                    OR (
3336                        a.email IS NULL
3337                        OR TRIM(a.email) = ''
3338                        OR a.email NOT REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}([[:space:]]*,[[:space:]]*[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})*$'
3339                        OR a.email REGEXP '($blacklist)'
3340                    )
3341                )
3342                AND a.budget_status_id IN(1, 2, 11, 17, 21, 22)";
3343        }
3344
3345        if ($isFollowUp) {
3346            $blacklist = implode('|', $this->getBlacklistEmails());
3347
3348            // FIRE-864 follow-up (04/05): anchored email pattern so `no
3349            // tiene@notien.com` and friends are correctly excluded from
3350            // follow-ups too. Mirror of the invalid_email branch above.
3351            $where .= "
3352                AND (
3353                    a.email IS NOT NULL
3354                    AND TRIM(a.email) != ''
3355                    AND a.email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}([[:space:]]*,[[:space:]]*[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})*$'
3356                    AND a.email NOT REGEXP '($blacklist)'
3357                )";
3358        }
3359
3360        if ($isNoEmailFollowUp) {
3361            // Seguimiento de clientes sin mail: pending follow-ups for
3362            // quotations whose email is missing/invalid/blacklisted, so
3363            // they can be chased through other channels (WhatsApp, phone,
3364            // regular mail). Mirrors the follow-up gating (status 2,
3365            // overdue last_follow_up_date, total_sent under company limit)
3366            // but inverts the email-quality check.
3367            $blacklist = implode('|', $this->getBlacklistEmails());
3368            $where .= "
3369                AND a.budget_status_id = 2
3370                AND a.last_follow_up_date IS NOT NULL
3371                AND a.last_follow_up_date > 0
3372                AND a.last_follow_up_date < NOW()
3373                AND a.reason_for_not_following_up_id IS NULL
3374                AND (
3375                    a.email IS NULL
3376                    OR TRIM(a.email) = ''
3377                    OR a.email NOT REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}([[:space:]]*,[[:space:]]*[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})*$'
3378                    OR a.email REGEXP '($blacklist)'
3379                )";
3380        }
3381
3382        $query = "SELECT
3383                        a.id,
3384                        a.quote_id,
3385                        a.internal_quote_id,
3386                        a.company_id,
3387                        b.name company_name,
3388                        a.client,
3389                        c.name client_type,
3390                        c.customer_type_id,
3391                        s.name segment,
3392                        s.segment_id,
3393                        a.request_date,
3394                        a.visit_date,
3395                        a.issue_date,
3396                        a.acceptance_date,
3397                        a.internal_quote_id,
3398                        DATE_FORMAT(a.request_date, '%d/%m/%Y') request_date_translate,
3399                        DATE_FORMAT(a.issue_date, '%d/%m/%Y') issue_date_translate,
3400                        DATE_FORMAT(a.acceptance_date, '%d/%m/%Y') acceptance_date_translate,
3401                        DATE_FORMAT(a.last_follow_up_date, '%d/%m/%Y') last_follow_up_date_translate,
3402                        DATE_FORMAT(a.created_at, '%d/%m/%Y') created_at_translate,
3403                        DATE_FORMAT(a.accepted_at, '%d/%m/%Y') accepted_at_translate,
3404                        a.phone_number,
3405                        a.email,
3406                        a.duration,
3407                        a.order_number,
3408                        d.name 'type',
3409                        d.budget_type_id,
3410                        e.name 'status',
3411                        e.budget_status_id,
3412                        f.name as source,
3413                        f.source_id,
3414                        a.amount,
3415                        g.name reason_for_not_following_up,
3416                        a.reason_for_not_following_up_id,
3417                        a.reason_for_rejection_id,
3418                        a.last_follow_up_date,
3419                        a.last_follow_up_comment,
3420                        CASE WHEN a.reason_for_rejection_id IS NULL THEN a.reason_for_rejection ELSE h.name END reason_for_rejection,
3421                        a.commercial,
3422                        a.user_commercial_by_g3w,
3423                        a.user_create_by_g3w,
3424                        a.created_by,
3425                        a.created_at,
3426                        a.updated_by,
3427                        a.updated_at,
3428                        a.total_sent,
3429                        a.has_attachment,
3430                        a.for_approval,
3431                        a.approval_type,
3432                        a.requires_technical_office,
3433                        a.box_work_g3w,
3434                        a.people_assigned_to_the_job,
3435                        a.duration_of_job_in_days,
3436                        a.estimated_cost_of_materials,
3437                        a.budget_margin_enabled,
3438                        a.question_enabled,
3439                        a.cost_of_labor,
3440                        a.total_cost_of_job,
3441                        CASE WHEN a.budget_margin_enabled > 0 THEN a.invoice_margin ELSE NULL END invoice_margin,
3442                        CASE WHEN a.budget_margin_enabled > 0 THEN a.margin_for_the_company ELSE NULL END margin_for_the_company,
3443                        a.margin_on_invoice_per_day_per_worker,
3444                        a.revenue_per_date_per_worked,
3445                        a.commission_cost,
3446                        a.commission_pct,
3447                        a.gross_margin,
3448                        a.labor_percentage,
3449                        a.question_ids,
3450                        a.question_ids_no,
3451                        a.approved_at,
3452                        a.approved_by,
3453                        a.rejected_at,
3454                        a.rejected_by,
3455                        a.approved_at_v2,
3456                        a.approved_by_v2,
3457                        a.rejected_at_v2,
3458                        a.rejected_by_v2,
3459                        a.accepted_at,
3460                        a.accepted_by,
3461                        a.is_validated,
3462                        a.resource_id,
3463                        a.x_status,
3464                        a.likehood,
3465                        a.sync_import,
3466                        a.sync_import_edited,
3467                        a.g3w_warning,
3468                        a.g3w_warning_fields,
3469                        a.id_solicitud_duplicity,
3470                        SUBSTRING_INDEX(a.email, '@', -1) domain
3471                        {$matchScoreCol}
3472                        {$subquery}
3473                    FROM
3474                        tbl_quotations a
3475                        LEFT JOIN tbl_companies b ON a.company_id = b.company_id
3476                        LEFT JOIN tbl_customer_types c ON a.customer_type_id = c.customer_type_id
3477                        LEFT JOIN tbl_segments s ON a.segment_id = s.segment_id
3478                        LEFT JOIN tbl_budget_types d ON a.budget_type_id = d.budget_type_id
3479                        LEFT JOIN tbl_budget_status e ON a.budget_status_id = e.budget_status_id
3480                        LEFT JOIN tbl_sources f ON a.source_id = f.source_id
3481                        LEFT JOIN tbl_reason_for_not_following_up g ON a.reason_for_not_following_up_id = g.reason_for_not_following_up_id
3482                        LEFT JOIN tbl_reason_for_rejection h ON a.reason_for_rejection_id = h.reason_for_rejection_id
3483                        {$lasLeftJoin}
3484                    WHERE a.for_add = 0 {$where} {$whereBlocked}
3485                    {$orderBy}
3486                    LIMIT {$offset}{$limit}
3487                    ";
3488        // FIRE-1174: GROUP BY a.id removed. Every LEFT JOIN above is eq_ref on
3489        // PK of a tiny lookup table — none can duplicate a.id. The
3490        // `lasLeftJoin` email-split temp also has its own inner GROUP BY a.id
3491        // before joining back on a.id, so it returns at most 1 row per a.id.
3492        // Verified on prod 2026-06-01: count with vs. without GROUP BY a.id
3493        // returned exactly 47,820 rows. The outer GROUP BY was costing a
3494        // temp table + filesort for zero dedup benefit.
3495        // return $query;
3496        // FIRE-1174 + FIRE-1145: skip the cache layer entirely for short in-progress
3497        // search inputs (1-2 chars). For empty searchText or ≥ 3 chars we still
3498        // cache via ResultCache (domain-tagged so mutations forget only the
3499        // 'quotations' bucket, not the whole app cache).
3500        if ($shouldCacheList) {
3501            $result = ResultCache::remember('quotations', $query, 600, fn () => DB::select($query));
3502        } else {
3503            $result = DB::select($query);
3504        }
3505
3506        $totalQuery = "SELECT
3507                            COUNT(a.id) totalRowCount,
3508                            SUM(CAST(a.amount AS DECIMAL(10,2))) totalAmount
3509                        FROM
3510                            tbl_quotations a
3511                            LEFT JOIN tbl_companies b ON a.company_id = b.company_id
3512                            LEFT JOIN tbl_customer_types c ON a.customer_type_id = c.customer_type_id
3513                            LEFT JOIN tbl_segments s ON a.segment_id = s.segment_id
3514                            LEFT JOIN tbl_budget_types d ON a.budget_type_id = d.budget_type_id
3515                            LEFT JOIN tbl_budget_status e ON a.budget_status_id = e.budget_status_id
3516                            LEFT JOIN tbl_sources f ON a.source_id = f.source_id
3517                            LEFT JOIN tbl_reason_for_not_following_up g ON a.reason_for_not_following_up_id = g.reason_for_not_following_up_id
3518                            LEFT JOIN tbl_reason_for_rejection h ON a.reason_for_rejection_id = h.reason_for_rejection_id
3519                            {$lasLeftJoin}
3520                        WHERE a.for_add = 0
3521                        {$where} {$whereBlocked}";
3522
3523        // FIRE-1174 + FIRE-1145: see $shouldCacheList note — same gating, tagged.
3524        if ($shouldCacheList) {
3525            $countQuery = ResultCache::remember('quotations', $totalQuery, 600, fn () => DB::select($totalQuery));
3526        } else {
3527            $countQuery = DB::select($totalQuery);
3528        }
3529
3530        $totalToFollowUpQuery = "SELECT
3531                                        COUNT(DISTINCT a.id) totalRowCount
3532                                    FROM
3533                                        tbl_quotations a
3534                                    LEFT JOIN tbl_companies b ON a.company_id = b.company_id
3535                                    LEFT JOIN tbl_customer_types c ON a.customer_type_id = c.customer_type_id
3536                                    LEFT JOIN tbl_segments s ON a.segment_id = s.segment_id
3537                                    LEFT JOIN tbl_budget_types d ON a.budget_type_id = d.budget_type_id
3538                                    LEFT JOIN tbl_budget_status e ON a.budget_status_id = e.budget_status_id
3539                                    LEFT JOIN tbl_sources f ON a.source_id = f.source_id
3540                                    LEFT JOIN tbl_reason_for_not_following_up g ON a.reason_for_not_following_up_id = g.reason_for_not_following_up_id
3541                                    LEFT JOIN tbl_reason_for_rejection h ON a.reason_for_rejection_id = h.reason_for_rejection_id
3542                                    LEFT JOIN (
3543                                        SELECT
3544                                        a.id,
3545                                        SUBSTRING_INDEX(
3546                                            SUBSTRING_INDEX(a.email, ',', n.digit + 1),
3547                                            ',',
3548                                            -1
3549                                        ) AS email_domain
3550                                        FROM
3551                                        tbl_quotations a
3552                                        INNER JOIN (
3553                                            SELECT
3554                                            0 AS digit
3555                                            UNION ALL
3556                                            SELECT
3557                                            1
3558                                            UNION ALL
3559                                            SELECT
3560                                            2
3561                                            UNION ALL
3562                                            SELECT
3563                                            3
3564                                            UNION ALL
3565                                            SELECT
3566                                            4
3567                                            UNION ALL
3568                                            SELECT
3569                                            5
3570                                            UNION ALL
3571                                            SELECT
3572                                            6
3573                                            UNION ALL
3574                                            SELECT
3575                                            7
3576                                            UNION ALL
3577                                            SELECT
3578                                            8
3579                                            UNION ALL
3580                                            SELECT
3581                                            9
3582                                        ) n ON LENGTH(
3583                                            REPLACE(a.email, ',', '')
3584                                        ) <= LENGTH(a.email)- n.digit
3585                                        GROUP BY a.id
3586                                    ) temp ON a.id = temp.id
3587                                    WHERE
3588                                    a.last_follow_up_date < NOW()
3589                                    AND a.budget_status_id IN (2)
3590                                    AND a.budget_type_id IS NOT NULL
3591                                    AND a.email IS NOT NULL
3592                                    AND a.email <> ''
3593                                    AND NOT EXISTS (
3594                                        SELECT
3595                                        1
3596                                        FROM
3597                                        tbl_blocked_domains bd
3598                                        WHERE
3599                                        temp.email_domain LIKE CONCAT('%', bd.domain, '%')
3600                                        AND bd.company_id = a.company_id
3601                                    )
3602                                    AND a.last_follow_up_date IS NOT NULL
3603                                    AND a.reason_for_not_following_up_id IS NULL
3604                                    AND a.last_follow_up_date > 0
3605                                    AND a.total_sent < b.limit_reminder_emails
3606                                    AND a.for_add = 0
3607                                    {$where}";
3608
3609        // FIRE-1174 + FIRE-1145: see $shouldCacheList note — same gating, tagged.
3610        if ($shouldCacheList) {
3611            $countToFollowUpQuery = ResultCache::remember('quotations', $totalToFollowUpQuery, 600, fn () => DB::select($totalToFollowUpQuery));
3612        } else {
3613            $countToFollowUpQuery = DB::select($totalToFollowUpQuery);
3614        }
3615
3616        $query = "SELECT
3617                            COUNT(1) as count,
3618                            SUM(a.amount) as total_amount
3619                        FROM tbl_quotations a
3620                        LEFT JOIN tbl_companies b ON a.company_id = b.company_id
3621                        LEFT JOIN tbl_customer_types c ON a.customer_type_id = c.customer_type_id
3622                        LEFT JOIN tbl_segments s ON a.segment_id = s.segment_id
3623                        LEFT JOIN tbl_budget_types d ON a.budget_type_id = d.budget_type_id
3624                        LEFT JOIN tbl_budget_status e ON a.budget_status_id = e.budget_status_id
3625                        LEFT JOIN tbl_sources f ON a.source_id = f.source_id
3626                        LEFT JOIN tbl_reason_for_not_following_up g ON a.reason_for_not_following_up_id = g.reason_for_not_following_up_id
3627                        LEFT JOIN tbl_reason_for_rejection h ON a.reason_for_rejection_id = h.reason_for_rejection_id
3628                        WHERE a.budget_status_id = 11
3629                        AND a.email IS NOT NULL
3630                        AND a.budget_type_id IS NOT NULL
3631                        {$whereSendToClient}
3632                        ";
3633
3634        // FIRE-1174 + FIRE-1145: see $shouldCacheList note — same gating, tagged.
3635        if ($shouldCacheList) {
3636            $totalSendToClient = ResultCache::remember('quotations', $query, 600, fn () => DB::select($query));
3637        } else {
3638            $totalSendToClient = DB::select($query);
3639        }
3640
3641        return response([
3642            'message' => 'OK',
3643            'data' => $result,
3644            'totalAmount' => $countQuery[0]->totalAmount,
3645            'totalRowCount' => $countQuery[0]->totalRowCount,
3646            'totalToFollowUpRowCount' => $countToFollowUpQuery[0]->totalRowCount,
3647            'totalSendToClient' => $totalSendToClient[0]->count,
3648            'totalSendToClientAmount' => $totalSendToClient[0]->total_amount,
3649        ]);
3650
3651        // } catch (\Exception $e) {
3652        //     return response(['message' => 'KO', 'error' => $e->getMessage()]);
3653        // }
3654
3655    }
3656
3657    /**
3658     * Orders table listing for the new <app-data-table>.
3659     *
3660     * Intentionally minimal: every valid budget (for_add = 0) for the user's
3661     * company scope — no search, sort or pagination. Returns the same row shape
3662     * the front end expects ({ data, totalRowCount, totalAmount }) so the table
3663     * and the edit modal keep working. The legacy list_quotations stays in
3664     * place for the many other callers that depend on its richer filter model.
3665     */
3666    public function list_orders_table(Request $request): ResponseFactory|HttpResponse
3667    {
3668        try {
3669            $data = $request->all();
3670
3671            $companyId = (int) ($data['company_id'] ?? 0);
3672
3673            // Pagination window — the table infinite-scrolls 300 rows at a time
3674            // (start = rows already loaded, end = start + 300). ORDER BY a.id
3675            // DESC + LIMIT rides idx_listquot_company_id_desc (company_id,
3676            // id DESC) → ~7ms per page regardless of table size.
3677            $start = max(0, (int) ($data['start'] ?? 0));
3678            $limit = (int) ($data['end'] ?? 300) - $start;
3679            $limit = $limit > 0 ? min($limit, 1000) : 300;
3680
3681            // FIRE-1174: gate the 10-min query cache to avoid per-keystroke churn
3682            // (same policy as list_quotations). Cache the cold "no search" path
3683            // and stable searches (>= 3 chars); skip the noisy 1-2 char prefixes.
3684            $cacheSearch = trim((string) ($data['searchText'] ?? ''));
3685            $shouldCacheList = $cacheSearch === '' || strlen($cacheSearch) >= 3;
3686
3687            // for_add = 0 (valid budgets) + server-side company scoping.
3688            $where = ' AND a.for_add = 0 ';
3689            if ($companyId != 0) {
3690                $where .= " AND a.company_id = {$companyId} ";
3691            } elseif ($this->companyId) {
3692                $where .= " AND a.company_id IN ({$this->companyId}";
3693            }
3694
3695            // Company-only scope, captured before the advanced filters are
3696            // appended — used by the distinct-values mode below so the filter
3697            // modal offers every value in scope, not just those left after a
3698            // partially-applied filter.
3699            $scopeWhere = $where;
3700
3701            // Advanced Search — explicit Order IDs.
3702            if (isset($data['ids']) && is_array($data['ids'])) {
3703                $numericIds = array_values(array_filter(
3704                    array_map('intval', $data['ids']),
3705                    fn ($n) => $n > 0
3706                ));
3707                if (count($numericIds) > 0) {
3708                    $where .= ' AND a.id IN ('.implode(',', $numericIds).') ';
3709                }
3710            }
3711
3712            // Advanced Search — "Seguimiento de clientes sin mail": overdue
3713            // follow-ups (status Enviado) whose email is missing/invalid/blacklisted
3714            // (mirrors the same clause in list_quotations).
3715            if (! empty($data['is_no_email_follow_up'])) {
3716                $blacklist = implode('|', $this->getBlacklistEmails());
3717                $emailConds = [
3718                    'a.email IS NULL',
3719                    "TRIM(a.email) = ''",
3720                    "a.email NOT REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}([[:space:]]*,[[:space:]]*[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})*$'",
3721                ];
3722                if ($blacklist !== '') {
3723                    $emailConds[] = "a.email REGEXP '($blacklist)'";
3724                }
3725                $where .= '
3726                    AND a.budget_status_id = 2
3727                    AND a.last_follow_up_date IS NOT NULL
3728                    AND a.last_follow_up_date > 0
3729                    AND a.last_follow_up_date < NOW()
3730                    AND a.reason_for_not_following_up_id IS NULL
3731                    AND ('.implode(' OR ', $emailConds).')';
3732            }
3733
3734            // Whether any clause references a joined table (forces the count /
3735            // ids_only queries to include the joins).
3736            $forceJoins = false;
3737
3738            // Advanced Search — "Pendientes de seguimiento": rows that need a
3739            // follow-up. Matches get_total_quotations_by_budget_status
3740            // (totalPendingFollowUps) EXACTLY — the authoritative "needs follow up"
3741            // total: status Enviado (2), an overdue last_follow_up_date, no "reason
3742            // for not following up", under the per-company reminder limit, a
3743            // present + valid + non-blacklisted email NOT on a company-blocked
3744            // domain. (No budget_type requirement.)
3745            if (! empty($data['is_send_follow_up'])) {
3746                $emailRegex = '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}([[:space:]]*,[[:space:]]*[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})*$';
3747                $blacklist = implode('|', $this->getBlacklistEmails());
3748                $where .= " AND a.last_follow_up_date < NOW()
3749                    AND a.budget_status_id IN (2)
3750                    AND a.email IS NOT NULL
3751                    AND a.email <> ''
3752                    AND NOT EXISTS (
3753                        SELECT 1 FROM tbl_blocked_domains bd
3754                        WHERE a.email LIKE CONCAT('%', bd.domain, '%')
3755                        AND bd.company_id = a.company_id
3756                    )
3757                    AND a.last_follow_up_date IS NOT NULL
3758                    AND a.reason_for_not_following_up_id IS NULL
3759                    AND a.last_follow_up_date > 0
3760                    AND a.total_sent < b.limit_reminder_emails
3761                    AND TRIM(a.email) <> ''
3762                    AND a.email REGEXP '{$emailRegex}";
3763                if ($blacklist !== '') {
3764                    $where .= " AND a.email NOT REGEXP '({$blacklist})' ";
3765                }
3766                $forceJoins = true; // references b.limit_reminder_emails
3767            }
3768
3769            // Advanced Search — "Pendientes de envío a cliente".
3770            if (! empty($data['is_send_to_client'])) {
3771                $where .= ' AND a.budget_status_id = 11 AND a.email IS NOT NULL AND a.budget_type_id IS NOT NULL ';
3772            }
3773
3774            // Advanced Search — "Solicitudes y visitas" (dashboard tile
3775            // totalRequestAndVisit): Solicitud (6), Visita (8), En proceso (12).
3776            if (! empty($data['is_request_visit'])) {
3777                $where .= ' AND a.budget_status_id IN (6, 8, 12) ';
3778            }
3779
3780            // Advanced Search — "Emails con error" (dashboard tile totalError):
3781            // a SendGrid error status, or a missing / malformed / blacklisted
3782            // email, among the still-in-pipeline statuses.
3783            if (! empty($data['is_email_error'])) {
3784                $emailRegex = '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}([[:space:]]*,[[:space:]]*[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})*$';
3785                $blacklist = implode('|', $this->getBlacklistEmails());
3786                $emailConds = [
3787                    'a.email IS NULL',
3788                    "TRIM(a.email) = ''",
3789                    "a.email NOT REGEXP '{$emailRegex}'",
3790                ];
3791                if ($blacklist !== '') {
3792                    $emailConds[] = "a.email REGEXP '({$blacklist})'";
3793                }
3794                $where .= "
3795                    AND (
3796                        a.x_status IN ('Error','Error - Bounce','Error - Spam')
3797                        OR (".implode(' OR ', $emailConds).')
3798                    )
3799                    AND a.budget_status_id IN (1, 2, 11, 17, 21, 22) ';
3800            }
3801
3802            // Advanced Search — "Fallos en G3W" (dashboard tile totalG3WError).
3803            if (! empty($data['is_g3w_warning'])) {
3804                $where .= ' AND a.g3w_warning = 1 ';
3805            }
3806
3807            // Global quick search — match the keyword against every meaningful
3808            // field (OR), mirroring the field set list_quotations searches.
3809            if (isset($data['searchText']) && trim((string) $data['searchText']) !== '') {
3810                $searchText = addslashes(trim((string) $data['searchText']));
3811                $searchFields = [
3812                    'a.quote_id', 'a.internal_quote_id', 'a.box_work_g3w', 's.name', 'b.name',
3813                    'a.client', 'c.name', 'a.phone_number', 'a.email', 'a.order_number',
3814                    'd.name', 'e.name', 'f.name', 'a.amount', 'g.name', 'h.name',
3815                    'a.last_follow_up_comment', 'a.x_status', 'a.likehood', 'a.commercial',
3816                    'a.user_commercial_by_g3w', 'a.user_create_by_g3w', 'a.created_by',
3817                    'a.updated_by', 'a.approved_by', 'a.rejected_by', 'a.accepted_by',
3818                    'a.g3w_warning_fields', 'a.reason_for_rejection',
3819                ];
3820                $likes = array_map(fn ($f) => "{$f} LIKE '%{$searchText}%'", $searchFields);
3821                $where .= ' AND ('.implode(' OR ', $likes).') ';
3822                $forceJoins = true; // references joined columns (b/c/d/e/f/g/h.name)
3823            }
3824
3825            // Advanced filter modal — combined per-field filters (AND-ed).
3826            $filterWhere = $this->build_orders_table_filters($data['filterModel'] ?? []);
3827            $where .= $filterWhere;
3828            // Filters may reference joined columns (e.name, d.name, …) so the
3829            // otherwise join-free count / ids_only queries need the joins too.
3830            $hasJoinFilter = $filterWhere !== '' || $forceJoins;
3831
3832            $fromJoins = '
3833                FROM tbl_quotations a
3834                LEFT JOIN tbl_companies b ON a.company_id = b.company_id
3835                LEFT JOIN tbl_customer_types c ON a.customer_type_id = c.customer_type_id
3836                LEFT JOIN tbl_segments s ON a.segment_id = s.segment_id
3837                LEFT JOIN tbl_budget_types d ON a.budget_type_id = d.budget_type_id
3838                LEFT JOIN tbl_budget_status e ON a.budget_status_id = e.budget_status_id
3839                LEFT JOIN tbl_sources f ON a.source_id = f.source_id
3840                LEFT JOIN tbl_reason_for_not_following_up g ON a.reason_for_not_following_up_id = g.reason_for_not_following_up_id
3841                LEFT JOIN tbl_reason_for_rejection h ON a.reason_for_rejection_id = h.reason_for_rejection_id ';
3842
3843            // Distinct-values mode (same endpoint): returns the distinct values
3844            // for the requested fields within the company scope, so the filter
3845            // modal can render checkboxes for fields that have no preloaded list.
3846            // Blank/empty is excluded — the UI always adds a "(Vacío)" option.
3847            if (! empty($data['distinct_fields']) && is_array($data['distinct_fields'])) {
3848                $distinctColMap = [
3849                    'accepted_by' => 'a.accepted_by',
3850                    'g3w_warning_fields' => 'a.g3w_warning_fields',
3851                    'reason_for_rejection' => 'h.name',
3852                    'reason_for_not_following_up' => 'g.name',
3853                    'likehood' => 'a.likehood',
3854                    'x_status' => 'a.x_status',
3855                ];
3856                $distinct = [];
3857                foreach ($data['distinct_fields'] as $field) {
3858                    if (! isset($distinctColMap[$field])) {
3859                        continue;
3860                    }
3861                    $col = $distinctColMap[$field];
3862                    $rows = DB::select("SELECT DISTINCT {$col} v {$fromJoins} WHERE 1=1 {$scopeWhere} AND {$col} IS NOT NULL AND {$col} <> '' ORDER BY {$col} LIMIT 1000");
3863                    $distinct[$field] = array_map(fn ($r) => $r->v, $rows);
3864                }
3865
3866                return response(['message' => 'OK', 'distinct' => $distinct]);
3867            }
3868
3869            // Eligibility-count mode: given the selection ($where already carries
3870            // the company scope + the selected ids), return how many of those
3871            // rows actually qualify to be sent — so the buttons can show the
3872            // real "will be sent" count, not the raw selection size.
3873            if (! empty($data['count_eligible'])) {
3874                // Follow-up — identical to get_total_quotations_by_budget_status
3875                // (totalPendingFollowUps) and the is_send_follow_up filter above:
3876                // status Enviado (2), overdue last_follow_up_date, no "reason for
3877                // not following up", under the per-company limit, a present + valid
3878                // + non-blacklisted email not on a company-blocked domain. No
3879                // budget_type requirement.
3880                $emailRegex = '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}([[:space:]]*,[[:space:]]*[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})*$';
3881                $blacklist = implode('|', $this->getBlacklistEmails());
3882                $fuWhere = " AND a.last_follow_up_date < NOW()
3883                    AND a.budget_status_id IN (2)
3884                    AND a.email IS NOT NULL
3885                    AND a.email <> ''
3886                    AND NOT EXISTS (
3887                        SELECT 1 FROM tbl_blocked_domains bd
3888                        WHERE a.email LIKE CONCAT('%', bd.domain, '%')
3889                        AND bd.company_id = a.company_id
3890                    )
3891                    AND a.last_follow_up_date IS NOT NULL
3892                    AND a.reason_for_not_following_up_id IS NULL
3893                    AND a.last_follow_up_date > 0
3894                    AND a.total_sent < b.limit_reminder_emails
3895                    AND TRIM(a.email) <> ''
3896                    AND a.email REGEXP '{$emailRegex}";
3897                if ($blacklist !== '') {
3898                    $fuWhere .= " AND a.email NOT REGEXP '({$blacklist})' ";
3899                }
3900                $followUp = DB::select('SELECT COUNT(a.id) c FROM tbl_quotations a JOIN tbl_companies b ON a.company_id = b.company_id WHERE 1=1 '.$where.$fuWhere)[0]->c;
3901
3902                // Send to client — same as the list_quotations totalSendToClient
3903                // count: status Listo para enviar (11), email + budget type set.
3904                $stcWhere = ' AND a.budget_status_id = 11 AND a.email IS NOT NULL AND a.budget_type_id IS NOT NULL ';
3905                $sendToClient = DB::select('SELECT COUNT(a.id) c FROM tbl_quotations a WHERE 1=1 '.$where.$stcWhere)[0]->c;
3906
3907                return response([
3908                    'message' => 'OK',
3909                    'send_follow_up_count' => (int) $followUp,
3910                    'send_to_client_count' => (int) $sendToClient,
3911                ]);
3912            }
3913
3914            // Lightweight id-only mode (same endpoint): returns every matching id
3915            // for the current scope so the table's "select all" can span all
3916            // pages, not just the rows scrolled into view.
3917            if (! empty($data['ids_only'])) {
3918                $idFrom = $hasJoinFilter ? $fromJoins : ' FROM tbl_quotations a ';
3919                $idQuery = 'SELECT a.id '.$idFrom.' WHERE 1=1 '.$where;
3920                $idRows = $shouldCacheList
3921                    ? ResultCache::remember('quotations', $idQuery, 600, fn () => DB::select($idQuery))
3922                    : DB::select($idQuery);
3923                $ids = array_map(fn ($r) => (int) $r->id, $idRows);
3924
3925                return response([
3926                    'message' => 'OK',
3927                    'ids' => $ids,
3928                    'totalRowCount' => count($ids),
3929                ]);
3930            }
3931
3932            $joins = $fromJoins.' WHERE 1=1 '.$where;
3933
3934            // Sorting — whitelisted column per table field; defaults to newest first.
3935            $sortMap = [
3936                'quote_id' => 'CAST(a.quote_id AS DOUBLE)',
3937                'internal_quote_id' => 'a.internal_quote_id',
3938                'company_name' => 'b.name',
3939                'client' => 'a.client',
3940                'amount' => 'CAST(a.amount AS DOUBLE)',
3941                // Margins are stored as strings AND masked to NULL when
3942                // budget_margin_enabled = 0 (same as the SELECT). Sort by the
3943                // masked numeric value so the order matches what's displayed.
3944                'invoice_margin' => 'CAST(CASE WHEN a.budget_margin_enabled > 0 THEN a.invoice_margin ELSE NULL END AS DECIMAL(20,4))',
3945                'margin_for_the_company' => 'CAST(CASE WHEN a.budget_margin_enabled > 0 THEN a.margin_for_the_company ELSE NULL END AS DECIMAL(20,4))',
3946                'type' => 'd.name',
3947                'status' => 'e.name',
3948                'created_by' => 'a.created_by',
3949                'commercial' => 'a.commercial',
3950                'created_at_translate' => 'a.created_at',
3951                'acceptance_date_translate' => 'a.acceptance_date',
3952                'request_date_translate' => 'a.request_date',
3953                'issue_date_translate' => 'a.issue_date',
3954                'duration' => 'CAST(a.duration AS DOUBLE)',
3955                'client_type' => 'c.name',
3956                'segment' => 's.name',
3957                'likehood' => 'a.likehood',
3958                'source' => 'f.name',
3959                'last_follow_up_date_translate' => 'a.last_follow_up_date',
3960                'reason_for_not_following_up' => 'g.name',
3961                'reason_for_rejection' => 'h.name',
3962                'email' => 'a.email',
3963                'x_status' => 'a.x_status',
3964                'phone_number' => 'a.phone_number',
3965                'order_number' => 'a.order_number',
3966                'box_work_g3w' => 'a.box_work_g3w',
3967                'for_approval' => 'a.for_approval',
3968                'approval_type' => 'a.approval_type',
3969                'has_attachment' => 'a.has_attachment',
3970                'accepted_by' => 'a.accepted_by',
3971                'accepted_at_translate' => 'a.accepted_at',
3972                'sync_import' => 'a.sync_import',
3973                'g3w_warning' => 'a.g3w_warning',
3974                'g3w_warning_fields' => 'a.g3w_warning_fields',
3975                'id' => 'a.id',
3976            ];
3977            $orderBy = ' ORDER BY a.id DESC ';
3978            $sort = $data['sortModel'] ?? [];
3979            if (is_array($sort) && count($sort) > 0 && isset($sort[0]['colId']) && isset($sortMap[$sort[0]['colId']])) {
3980                $dir = strtolower((string) ($sort[0]['sort'] ?? 'asc')) === 'desc' ? 'DESC' : 'ASC';
3981                $orderBy = ' ORDER BY '.$sortMap[$sort[0]['colId']]." {$dir} ";
3982            }
3983
3984            $query = 'SELECT
3985                        a.id,
3986                        a.quote_id,
3987                        a.internal_quote_id,
3988                        a.company_id,
3989                        b.name company_name,
3990                        a.client,
3991                        c.name client_type,
3992                        c.customer_type_id,
3993                        s.name segment,
3994                        s.segment_id,
3995                        a.request_date,
3996                        a.visit_date,
3997                        a.issue_date,
3998                        a.acceptance_date,
3999                        DATE_FORMAT(a.request_date, "%d/%m/%Y") request_date_translate,
4000                        DATE_FORMAT(a.issue_date, "%d/%m/%Y") issue_date_translate,
4001                        DATE_FORMAT(a.acceptance_date, "%d/%m/%Y") acceptance_date_translate,
4002                        DATE_FORMAT(a.last_follow_up_date, "%d/%m/%Y") last_follow_up_date_translate,
4003                        DATE_FORMAT(a.created_at, "%d/%m/%Y") created_at_translate,
4004                        DATE_FORMAT(a.accepted_at, "%d/%m/%Y") accepted_at_translate,
4005                        a.phone_number,
4006                        a.email,
4007                        a.duration,
4008                        a.order_number,
4009                        d.name `type`,
4010                        d.budget_type_id,
4011                        e.name `status`,
4012                        e.budget_status_id,
4013                        f.name as source,
4014                        f.source_id,
4015                        a.amount,
4016                        g.name reason_for_not_following_up,
4017                        a.reason_for_not_following_up_id,
4018                        a.reason_for_rejection_id,
4019                        a.last_follow_up_date,
4020                        a.last_follow_up_comment,
4021                        CASE WHEN a.reason_for_rejection_id IS NULL THEN a.reason_for_rejection ELSE h.name END reason_for_rejection,
4022                        a.commercial,
4023                        a.user_commercial_by_g3w,
4024                        a.user_create_by_g3w,
4025                        a.created_by,
4026                        a.created_at,
4027                        a.updated_by,
4028                        a.updated_at,
4029                        a.total_sent,
4030                        a.has_attachment,
4031                        a.for_approval,
4032                        a.approval_type,
4033                        a.requires_technical_office,
4034                        a.box_work_g3w,
4035                        a.people_assigned_to_the_job,
4036                        a.duration_of_job_in_days,
4037                        a.estimated_cost_of_materials,
4038                        a.budget_margin_enabled,
4039                        a.question_enabled,
4040                        a.cost_of_labor,
4041                        a.total_cost_of_job,
4042                        CASE WHEN a.budget_margin_enabled > 0 THEN a.invoice_margin ELSE NULL END invoice_margin,
4043                        CASE WHEN a.budget_margin_enabled > 0 THEN a.margin_for_the_company ELSE NULL END margin_for_the_company,
4044                        a.margin_on_invoice_per_day_per_worker,
4045                        a.revenue_per_date_per_worked,
4046                        a.commission_cost,
4047                        a.commission_pct,
4048                        a.gross_margin,
4049                        a.labor_percentage,
4050                        a.question_ids,
4051                        a.question_ids_no,
4052                        a.approved_at,
4053                        a.approved_by,
4054                        a.rejected_at,
4055                        a.rejected_by,
4056                        a.approved_at_v2,
4057                        a.approved_by_v2,
4058                        a.rejected_at_v2,
4059                        a.rejected_by_v2,
4060                        a.accepted_at,
4061                        a.accepted_by,
4062                        a.is_validated,
4063                        a.resource_id,
4064                        a.x_status,
4065                        a.likehood,
4066                        a.sync_import,
4067                        a.sync_import_edited,
4068                        a.g3w_warning,
4069                        a.g3w_warning_fields,
4070                        a.id_solicitud_duplicity,
4071                        SUBSTRING_INDEX(a.email, "@", -1) domain
4072                    '.$joins.$orderBy." LIMIT {$start}{$limit}";
4073
4074            $result = $shouldCacheList
4075                ? ResultCache::remember('quotations', $query, 600, fn () => DB::select($query))
4076                : DB::select($query);
4077
4078            // Count is join-free (~16ms vs ~255ms) when no filter references a
4079            // joined column; otherwise it must join so the filter resolves.
4080            $countFrom = $hasJoinFilter ? $fromJoins : ' FROM tbl_quotations a ';
4081            $totalQuery = 'SELECT COUNT(a.id) totalRowCount, SUM(CAST(a.amount AS DECIMAL(10,2))) totalAmount '.$countFrom.' WHERE 1=1 '.$where;
4082            $countRow = ($shouldCacheList
4083                ? ResultCache::remember('quotations', $totalQuery, 600, fn () => DB::select($totalQuery))
4084                : DB::select($totalQuery))[0];
4085
4086            return response([
4087                'message' => 'OK',
4088                'data' => $result,
4089                'totalRowCount' => (int) $countRow->totalRowCount,
4090                'totalAmount' => $countRow->totalAmount,
4091            ]);
4092
4093        } catch (\Exception $e) {
4094            report(AppException::fromException($e, 'LIST_ORDERS_TABLE_EXCEPTION'));
4095
4096            return response(['message' => 'KO', 'error' => $e->getMessage()]);
4097        }
4098    }
4099
4100    /**
4101     * Build the WHERE fragment for the Orders advanced-filter modal.
4102     *
4103     * The frontend sends a per-field map keyed by the table field, e.g.
4104     *   { status:   { type:'set',    values:['Enviado','__BLANK__'] },
4105     *     amount:   { type:'number', min:1000, max:5000 },
4106     *     issue_date:{ type:'date',  from:'2026-01-01', to:'2026-03-31' },
4107     *     client:   { type:'text',   contains:'acme' } }
4108     *
4109     * Fields are whitelisted (unknown keys ignored) and every value is escaped,
4110     * so the model is safe to interpolate. Clauses AND together.
4111     */
4112    private function build_orders_table_filters($filterModel): string
4113    {
4114        if (! is_array($filterModel) || empty($filterModel)) {
4115            return '';
4116        }
4117
4118        // field => SQL expression (mirrors the SELECT aliases / sort map).
4119        $colMap = [
4120            'quote_id' => 'a.quote_id', 'internal_quote_id' => 'a.internal_quote_id',
4121            'client' => 'a.client', 'company_name' => 'b.name', 'region' => 'b.region', 'email' => 'a.email',
4122            'phone_number' => 'a.phone_number', 'order_number' => 'a.order_number',
4123            'x_status' => 'a.x_status', 'likehood' => 'a.likehood',
4124            'reason_for_not_following_up' => 'g.name', 'reason_for_rejection' => 'h.name',
4125            'accepted_by' => 'a.accepted_by', 'box_work_g3w' => 'a.box_work_g3w',
4126            'g3w_warning_fields' => 'a.g3w_warning_fields',
4127            'status' => 'e.name', 'type' => 'd.name', 'client_type' => 'c.name',
4128            'segment' => 's.name', 'source' => 'f.name', 'commercial' => 'a.commercial',
4129            'created_by' => 'a.created_by',
4130            'amount' => 'a.amount', 'invoice_margin' => 'a.invoice_margin',
4131            'margin_for_the_company' => 'a.margin_for_the_company',
4132            'duration' => 'a.duration', 'id' => 'a.id',
4133            'created_at' => 'a.created_at', 'acceptance_date' => 'a.acceptance_date',
4134            'request_date' => 'a.request_date', 'issue_date' => 'a.issue_date',
4135            'last_follow_up_date' => 'a.last_follow_up_date', 'accepted_at' => 'a.accepted_at',
4136        ];
4137
4138        $w = '';
4139
4140        foreach ($filterModel as $field => $f) {
4141            if (! is_array($f) || empty($f['type'])) {
4142                continue;
4143            }
4144            $type = $f['type'];
4145
4146            // --- Derived set columns (no direct column / Yes-No / label) ---
4147            if ($field === 'for_approval' && $type === 'set') {
4148                $w .= $this->orders_yes_no_clause($f['values'] ?? [], 'a.for_approval IN (1,3)', '(a.for_approval NOT IN (1,3) OR a.for_approval IS NULL)');
4149
4150                continue;
4151            }
4152            if ($field === 'has_attachment' && $type === 'set') {
4153                $w .= $this->orders_yes_no_clause($f['values'] ?? [], 'a.has_attachment > 0', '(a.has_attachment = 0 OR a.has_attachment IS NULL)');
4154
4155                continue;
4156            }
4157            if ($field === 'requires_technical_office' && $type === 'set') {
4158                $w .= $this->orders_yes_no_clause($f['values'] ?? [], 'a.requires_technical_office = 1', '(a.requires_technical_office <> 1 OR a.requires_technical_office IS NULL)');
4159
4160                continue;
4161            }
4162            if ($field === 'g3w_warning' && $type === 'set') {
4163                $w .= $this->orders_yes_no_clause($f['values'] ?? [], 'a.g3w_warning = 1', '(a.g3w_warning <> 1 OR a.g3w_warning IS NULL)');
4164
4165                continue;
4166            }
4167            if ($field === 'approval_type' && $type === 'set') {
4168                $map = [
4169                    '1' => 'a.approval_type = 1',
4170                    '2' => 'a.approval_type = 2',
4171                    '3' => 'a.approval_type = 3',
4172                    '__BLANK__' => '(a.approval_type IS NULL OR a.approval_type = 0)',
4173                ];
4174                $clauses = [];
4175                foreach ((array) ($f['values'] ?? []) as $v) {
4176                    $key = (string) $v;
4177                    if (isset($map[$key])) {
4178                        $clauses[] = $map[$key];
4179                    }
4180                }
4181                if ($clauses) {
4182                    $w .= ' AND ('.implode(' OR ', array_unique($clauses)).')';
4183                }
4184
4185                continue;
4186            }
4187            if ($field === 'sync_import' && $type === 'set') {
4188                $map = [
4189                    'flu' => "LOWER(a.user_create_by_g3w) = 'flu'",
4190                    'dig' => "LOWER(a.user_create_by_g3w) = 'dig'",
4191                    'g3w' => "(a.sync_import = 1 AND (a.user_create_by_g3w IS NULL OR LOWER(a.user_create_by_g3w) NOT IN ('flu','dig')))",
4192                    'manual' => "((a.sync_import <> 1 OR a.sync_import IS NULL) AND (a.user_create_by_g3w IS NULL OR LOWER(a.user_create_by_g3w) NOT IN ('flu','dig')))",
4193                ];
4194                $clauses = [];
4195                foreach ((array) ($f['values'] ?? []) as $v) {
4196                    $key = strtolower(trim((string) $v));
4197                    if (isset($map[$key])) {
4198                        $clauses[] = $map[$key];
4199                    }
4200                }
4201                if ($clauses) {
4202                    $w .= ' AND ('.implode(' OR ', array_unique($clauses)).')';
4203                }
4204
4205                continue;
4206            }
4207
4208            if (! isset($colMap[$field])) {
4209                continue;
4210            }
4211            $col = $colMap[$field];
4212
4213            if ($type === 'set') {
4214                $nonBlank = [];
4215                $hasBlank = false;
4216                foreach ((array) ($f['values'] ?? []) as $v) {
4217                    if ($v === null || $v === '' || $v === '__BLANK__') {
4218                        $hasBlank = true;
4219                    } else {
4220                        $nonBlank[] = addslashes((string) $v);
4221                    }
4222                }
4223                $clauses = [];
4224                if ($nonBlank) {
4225                    $clauses[] = "{$col} IN ('".implode("','", $nonBlank)."')";
4226                }
4227                if ($hasBlank) {
4228                    $clauses[] = "({$col} IS NULL OR {$col} = '')";
4229                }
4230                if ($clauses) {
4231                    $w .= ' AND ('.implode(' OR ', $clauses).')';
4232                }
4233            } elseif ($type === 'text') {
4234                $val = trim((string) ($f['contains'] ?? ''));
4235                if ($val !== '') {
4236                    $val = addslashes($val);
4237                    $w .= " AND {$col} LIKE '%{$val}%'";
4238                }
4239            } elseif ($type === 'number') {
4240                if (isset($f['min']) && $f['min'] !== '' && is_numeric($f['min'])) {
4241                    $w .= " AND CAST({$col} AS DECIMAL(20,4)) >= ".(float) $f['min'];
4242                }
4243                if (isset($f['max']) && $f['max'] !== '' && is_numeric($f['max'])) {
4244                    $w .= " AND CAST({$col} AS DECIMAL(20,4)) <= ".(float) $f['max'];
4245                }
4246            } elseif ($type === 'date') {
4247                // Range picker → start (from) / end (to) day, inclusive.
4248                $from = trim((string) ($f['from'] ?? ''));
4249                $to = trim((string) ($f['to'] ?? ''));
4250                if ($from !== '') {
4251                    $w .= " AND DATE({$col}) >= '".addslashes($from)."'";
4252                }
4253                if ($to !== '') {
4254                    $w .= " AND DATE({$col}) <= '".addslashes($to)."'";
4255                }
4256            }
4257        }
4258
4259        return $w;
4260    }
4261
4262    /**
4263     * Helper for Yes/No set filters: maps the selected 'yes'/'no' tokens to the
4264     * given SQL conditions and OR-combines them.
4265     */
4266    private function orders_yes_no_clause(array $values, string $yesSql, string $noSql): string
4267    {
4268        $clauses = [];
4269        foreach ($values as $v) {
4270            $t = strtolower(trim((string) $v));
4271            if (in_array($t, ['yes', 'sí', 'si', '1', 'true'], true)) {
4272                $clauses[] = $yesSql;
4273            } else {
4274                $clauses[] = $noSql;
4275            }
4276        }
4277        $clauses = array_unique($clauses);
4278
4279        return $clauses ? ' AND ('.implode(' OR ', $clauses).')' : '';
4280    }
4281
4282    public function get_dates(Request $request): ResponseFactory|HttpResponse
4283    {
4284
4285        try {
4286
4287            $data = $request->all();
4288            $companyId = addslashes((string) $data['company_id']);
4289
4290            $where = '';
4291            if ($companyId != 0) {
4292                $where = " AND a.company_id = {$companyId} ";
4293            } else {
4294                $where = " AND a.company_id IN ({$this->companyId})";
4295            }
4296
4297            $query = "SELECT
4298                        DATE_FORMAT(a.request_date, '%d/%m/%Y') request_date_translate,
4299                        DATE_FORMAT(a.issue_date, '%d/%m/%Y') issue_date_translate,
4300                        DATE_FORMAT(a.acceptance_date, '%d/%m/%Y') acceptance_date_translate,
4301                        DATE_FORMAT(a.last_follow_up_date, '%d/%m/%Y') last_follow_up_date_translate,
4302                        DATE_FORMAT(a.created_at, '%d/%m/%Y') created_at_translate,
4303                        DATE_FORMAT(a.accepted_at, '%d/%m/%Y') accepted_at_translate
4304                    FROM tbl_quotations a
4305                    WHERE a.for_add = 0 {$where}";
4306
4307            $result = DB::select($query);
4308
4309            return response([
4310                'message' => 'OK',
4311                'data' => $result,
4312            ]);
4313
4314        } catch (\Exception $e) {
4315            report(AppException::fromException($e, 'GET_DATES_EXCEPTION'));
4316
4317            return response(['message' => 'KO', 'error' => $e->getMessage()]);
4318        }
4319
4320    }
4321
4322    public function list_quotation_analytics_by_source(Request $request): ResponseFactory|HttpResponse
4323    {
4324
4325        try {
4326
4327            $data = $request->all();
4328            $companyId = addslashes((string) $data['company_id']);
4329
4330            $where = '';
4331            $whereYear = '';
4332
4333            $dateLflArray = [];
4334            $companyIds = $this->companyIds;
4335
4336            if ($companyId != 0) {
4337                $companyIds = [$companyId];
4338            }
4339
4340            $field = 'issue_date';
4341
4342            if (isset($data['years']) && $data['years'] != null) {
4343
4344                if (count($data['years']) > 0) {
4345                    foreach ($data['years'] as $year) {
4346                        if (isset($data['week']) && $data['week'] != null) {
4347                            $w = sprintf('%02d', $data['week']);
4348                            $whereYear .= " AND YEARWEEK(q.issue_date, 1) = '{$year}{$w}'";
4349                        } else {
4350                            $whereYear .= " AND YEAR(q.issue_date) = {$year}";
4351                        }
4352                    }
4353                }
4354            }
4355
4356            foreach ($companyIds as $v) {
4357
4358                $lflWhere = " AND q.company_id = {$v} ";
4359
4360                $query = "SELECT
4361                            CONCAT(
4362                                DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field}),
4363                                ' - ',
4364                                DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field})
4365                            ) AS date_like,
4366                            YEAR(q.{$field}) 'year',
4367                            DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS min_date_like,
4368                            DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS max_date_like,
4369                            {$v} 'company_id'
4370                        FROM
4371                            tbl_quotations q
4372                        WHERE
4373                            q.{$field} IS NOT NULL
4374                            AND q.for_add = 0
4375                            {$lflWhere}
4376                            {$whereYear}
4377                        GROUP BY YEAR(q.{$field})
4378                        ORDER BY YEAR(q.{$field}) DESC";
4379
4380                $dateLike = DB::select($query);
4381
4382                $dateLflArray[$v] = $dateLike;
4383            }
4384
4385            $whereAcceptanceDate = 'q.acceptance_date IS NOT NULL ';
4386
4387            $isFy = true;
4388
4389            if (isset($data['issue_year_ytd']) && $data['issue_year_ytd'] != null && $data['issue_year_ytd'] == true) {
4390                $isFy = false;
4391                $ytdArray = [];
4392                $ytdAcceptanceArray = [];
4393                $lflCompanyIds = [];
4394                $lflCompanyIdsAcc = [];
4395                foreach ($dateLflArray as $k => $v) {
4396                    foreach ($dateLflArray[$k] as $item) {
4397                        $year = $item->year;
4398                        $now = date('m-d');
4399                        array_push($ytdAcceptanceArray, "DATE_FORMAT(q.acceptance_date, '%Y-%m-%d') BETWEEN '{$year}-01-01' AND '{$year}-{$now}' AND YEAR(q.acceptance_date) = YEAR(issue_date)");
4400                        array_push($ytdArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN '{$year}-01-01' AND '{$year}-{$now}'");
4401                    }
4402
4403                    $ytdArray = implode(' OR ', $ytdArray);
4404                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$ytdArray})");
4405                    $ytdArray = [];
4406
4407                    $ytdAcceptanceArray = implode(' OR ', $ytdAcceptanceArray);
4408                    array_push($lflCompanyIdsAcc, "q.company_id = {$k} AND ({$ytdAcceptanceArray})");
4409                    $ytdAcceptanceArray = [];
4410                }
4411
4412                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
4413                $where .= " AND ({$lflCompanyIds}";
4414
4415                $lflCompanyIdsAcc = implode(' OR ', $lflCompanyIdsAcc);
4416                $whereAcceptanceDate .= " AND ({$lflCompanyIdsAcc}";
4417            }
4418
4419            if (isset($data['issue_year_lfl']) && $data['issue_year_lfl'] != null && $data['issue_year_lfl'] == true) {
4420                $isFy = false;
4421                $lflArray = [];
4422                $ytdAcceptanceArray = [];
4423                $lflCompanyIds = [];
4424                $lflCompanyIdsAcc = [];
4425                foreach ($dateLflArray as $k => $v) {
4426                    foreach ($dateLflArray[$k] as $item) {
4427                        $year = $item->year;
4428                        $min_date_like = $item->min_date_like;
4429                        $max_date_like = $item->max_date_like;
4430                        array_push($ytdAcceptanceArray, "DATE_FORMAT(q.acceptance_date, '%Y-%m-%d') BETWEEN LEAST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND GREATEST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND YEAR(q.acceptance_date) = YEAR(issue_date)");
4431                        array_push($lflArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN LEAST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND GREATEST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}')");
4432                    }
4433
4434                    $lflArray = implode(' OR ', $lflArray);
4435                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$lflArray})");
4436                    $lflArray = [];
4437
4438                    $ytdAcceptanceArray = implode(' OR ', $ytdAcceptanceArray);
4439                    array_push($lflCompanyIdsAcc, "q.company_id = {$k} AND ({$ytdAcceptanceArray})");
4440                    $ytdAcceptanceArray = [];
4441                }
4442
4443                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
4444                $where .= " AND ({$lflCompanyIds}";
4445
4446                $lflCompanyIdsAcc = implode(' OR ', $lflCompanyIdsAcc);
4447                $whereAcceptanceDate .= " AND ({$lflCompanyIdsAcc}";
4448            }
4449
4450            if ($isFy) {
4451                if ($companyId != 0) {
4452                    $where .= " AND q.company_id = {$companyId} ";
4453                } else {
4454                    $where .= " AND q.company_id IN ({$this->companyId})";
4455                }
4456            }
4457
4458            if (isset($data['source']) && $data['source'] != null) {
4459                $where .= " AND s.name = '{$data['source']}'";
4460            }
4461
4462            if (isset($data['month']) && $data['month'] != null) {
4463                $where .= " AND MONTH(q.issue_date) = '{$data['month']}'";
4464            }
4465
4466            if (isset($data['commercial']) && $data['commercial'] != null) {
4467                $where .= " AND q.commercial = '{$data['commercial']}'";
4468            }
4469
4470            if (isset($data['created_by']) && $data['created_by'] != null) {
4471                $where .= " AND q.created_by = '{$data['created_by']}'";
4472            }
4473
4474            if (isset($data['budget_type']) && $data['budget_type'] != null) {
4475                $where .= " AND bt.budget_type_id = {$data['budget_type']}";
4476            }
4477
4478            if (isset($data['budget_type_group']) && $data['budget_type_group'] != null) {
4479                $where .= " AND bt.budget_type_group_id = {$data['budget_type_group']}";
4480            }
4481
4482            if (isset($data['budget_status']) && $data['budget_status'] != null) {
4483                $where .= " AND bs.budget_status_id = {$data['budget_status']}";
4484            }
4485
4486            if (isset($data['client_type']) && $data['client_type'] != null) {
4487                $where .= " AND ct.customer_type_id = {$data['client_type']}";
4488            }
4489
4490            if (isset($data['segment_id']) && $data['segment_id'] != null) {
4491                $where .= " AND q.segment_id = {$data['segment_id']}";
4492            }
4493
4494            $query = "SELECT
4495                        YEAR(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)) AS 'year',
4496                        LPAD(MONTH(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)), 2, 0) AS 'month',
4497                        LPAD(WEEK(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)), 2, 0) AS 'week',
4498                        DATE_FORMAT(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY), '%W, %M %e') issue_date,
4499                        COUNT(
4500                            CASE WHEN q.issue_date IS NOT NULL
4501                            THEN 1 END
4502                        ) AS totalIssue,
4503                        GROUP_CONCAT(
4504                            CASE WHEN q.issue_date IS NOT NULL
4505                            THEN q.id END
4506                        ) AS groupConcatIds,
4507                        SUM(
4508                            CASE WHEN q.issue_date IS NOT NULL THEN q.amount END
4509                        ) AS revenueIssue,
4510                        COUNT(
4511                            CASE WHEN {$whereAcceptanceDate} AND bs.name = 'Aceptado' THEN 1 END
4512                        ) AS totalAcceptance,
4513                        SUM(
4514                            CASE WHEN {$whereAcceptanceDate} AND bs.name = 'Aceptado' THEN q.amount END
4515                        ) AS revenueAcceptance,
4516                        SUM(
4517                            CASE WHEN {$whereAcceptanceDate} AND bs.name = 'Aceptado' THEN q.amount END
4518                        ) / SUM(
4519                            CASE WHEN q.issue_date IS NOT NULL THEN q.amount END
4520                        ) * 100 AS revenueAcceptanceIssuedPercentage,
4521                        COUNT(
4522                            CASE WHEN bs.name = 'Rechazado' THEN 1 END
4523                        ) AS totalRejected,
4524                        SUM(
4525                            CASE WHEN bs.name = 'Rechazado' THEN q.amount END
4526                        ) AS revenueRejected,
4527                        COUNT(
4528                            CASE WHEN bs.name = 'Rechazado - automaticamente' THEN 1 END
4529                        ) AS totalRejectedAutomatic,
4530                        SUM(
4531                            CASE WHEN bs.name = 'Rechazado - automaticamente' THEN q.amount END
4532                        ) AS revenueRejectedAutomatic
4533                    FROM
4534                        tbl_quotations q
4535                        LEFT JOIN tbl_sources s ON s.source_id = q.source_id
4536                        LEFT JOIN tbl_budget_status bs ON bs.budget_status_id = q.budget_status_id
4537                        LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
4538                        LEFT JOIN tbl_customer_types ct ON q.customer_type_id = ct.customer_type_id
4539                    WHERE
4540                        q.issue_date IS NOT NULL
4541                        AND q.budget_type_id != 7
4542                        AND q.budget_type_id IS NOT NULL
4543                        AND q.for_add = 0
4544                        AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
4545                        {$where}
4546                        {$whereYear}                        
4547                    GROUP BY
4548                        YEAR(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)),
4549                        MONTH(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)),
4550                        WEEK(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)) WITH ROLLUP
4551                    ORDER BY
4552                        YEAR DESC,
4553                        MONTH ASC,
4554                        WEEK ASC,
4555                        DATE_FORMAT(q.issue_date, '%e') ASC";
4556            // FIRE-1145: domain-tagged cache (was Cache::get/put + Cache::flush()).
4557            $result = ResultCache::remember('quotations', $query, 600, fn () => DB::select($query));
4558
4559            return response([
4560                'message' => 'OK',
4561                'data' => $result,
4562            ]);
4563
4564        } catch (\Exception $e) {
4565            report(AppException::fromException($e, 'LIST_QUOTATION_ANALYTICS_BY_SOURCE_EXCEPTION'));
4566
4567            return response(['message' => 'KO', 'error' => $e->getMessage()]);
4568        }
4569    }
4570
4571    public function list_quotation_analytics_send_budgets(Request $request): ResponseFactory|HttpResponse
4572    {
4573
4574        try {
4575
4576            $data = $request->all();
4577            $companyId = addslashes((string) $data['company_id']);
4578
4579            $where = '';
4580            $whereYear = '';
4581
4582            if ($companyId != 0) {
4583                $where = " AND q.company_id = {$companyId} ";
4584            } else {
4585                $where = " AND q.company_id IN ({$this->companyId}";
4586            }
4587
4588            if (isset($data['budget_type_group']) && $data['budget_type_group'] != null) {
4589                $where .= " AND bt.budget_type_group_id = {$data['budget_type_group']}";
4590            }
4591
4592            if (isset($data['budget_status']) && $data['budget_status'] != null) {
4593                $where .= " AND bs.budget_status_id = {$data['budget_status']}";
4594            }
4595
4596            if (isset($data['years']) && $data['years'] != null) {
4597                $years = implode(',', $data['years']);
4598                if (count($data['years']) > 0) {
4599                    $whereYear = " AND YEAR(q.issue_date) IN ({$years})";
4600                }
4601            }
4602
4603            $query = "SELECT
4604                            YEAR(q.issue_date) AS 'year',
4605                            MONTH(q.issue_date) AS 'month',
4606                            SUM(
4607                                CASE WHEN MONTH(request_date) = MONTH(issue_date) THEN 1 ELSE 0 END
4608                            ) totalRequest,
4609                            COUNT(
4610                                CASE WHEN q.issue_date IS NOT NULL THEN 1 END
4611                            ) AS totalIssue,
4612                            GROUP_CONCAT(
4613                                CASE WHEN q.issue_date IS NOT NULL
4614                                THEN q.id END
4615                            ) AS groupConcatIds,
4616                            SUM(
4617                                CASE WHEN MONTH(request_date) = MONTH(issue_date) THEN 1 ELSE 0 END
4618                            ) /
4619                            COUNT(
4620                                CASE WHEN q.issue_date IS NOT NULL THEN 1 END
4621                            ) * 100 issuePercentage,
4622                            AVG(
4623                                COALESCE(q.duration, 0)
4624                            ) AS averageDurationIssue,
4625                            COALESCE(
4626                                AVG(
4627                                    CASE WHEN bt.name = 'Mantenimiento' THEN COALESCE(q.duration, 0) ELSE NULL END
4628                                ), 0
4629                            ) AS averageDurationMaintenance,
4630                            COALESCE(
4631                                AVG(
4632                                    CASE WHEN bt.name = 'Nuevos' THEN COALESCE(q.duration, 0) ELSE NULL END
4633                                ), 0
4634                            ) AS averageDurationNew,
4635                            COALESCE(
4636                                AVG(
4637                                    CASE WHEN bt.name = 'Correctivos' THEN COALESCE(q.duration, 0) ELSE NULL END
4638                                ), 0
4639                            ) AS averageDurationCorretive,
4640                            COALESCE(
4641                                AVG(
4642                                    CASE WHEN bt.name = 'Anomalías' THEN COALESCE(q.duration, 0) ELSE NULL END
4643                                ), 0
4644                            ) AS averageDurationAnomalies,
4645                            COALESCE(
4646                                AVG(
4647                                    CASE WHEN bt.name = 'Precios Unitarios' THEN COALESCE(q.duration, 0) ELSE NULL END
4648                                ), 0
4649                            ) AS averageDurationUnitPrice,
4650                            COALESCE(
4651                                AVG(
4652                                    CASE WHEN bt.name = 'Instalaciones' THEN COALESCE(q.duration, 0) ELSE NULL END
4653                                ), 0
4654                            ) AS averageDurationFacilities,
4655                            COALESCE(
4656                                AVG(
4657                                    CASE WHEN bt.name = 'Alquiler' THEN COALESCE(q.duration, 0) ELSE NULL END
4658                                ), 0
4659                            ) AS averageDurationRent,
4660                            COALESCE(
4661                                AVG(
4662                                    CASE WHEN bt.name = 'OCA visita' THEN COALESCE(q.duration, 0) ELSE NULL END
4663                                ), 0
4664                            ) AS averageDurationOCA,
4665                            COALESCE(
4666                                AVG(
4667                                    CASE WHEN bt.name = 'Retirada y gestion de residuos' THEN COALESCE(q.duration, 0) ELSE NULL END
4668                                ), 0
4669                            ) AS averageDurationWRM,
4670                            COALESCE(
4671                                AVG(
4672                                    CASE WHEN bt.name = 'Formación' THEN COALESCE(q.duration, 0) ELSE NULL END
4673                                ), 0
4674                            ) AS averageDurationTraining,
4675                            COALESCE(
4676                                AVG(
4677                                    CASE WHEN bt.name = 'Anomalías de facilities' THEN COALESCE(q.duration, 0) ELSE NULL END
4678                                ), 0
4679                            ) AS averageDurationFacilityAnomalies
4680                        FROM
4681                            tbl_quotations q
4682                            LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
4683                            LEFT JOIN tbl_budget_type_groups btg ON bt.budget_type_id = btg.budget_type_id
4684                        WHERE
4685                            q.issue_date IS NOT NULL
4686                            AND q.budget_type_id != 7
4687                            AND q.budget_type_id IS NOT NULL
4688                            AND q.for_add = 0
4689                            AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
4690                            {$where}
4691                            {$whereYear}
4692                        GROUP BY
4693                            YEAR(q.issue_date),
4694                            MONTH(q.issue_date) WITH ROLLUP
4695                        ORDER BY
4696                            YEAR(q.issue_date) DESC,
4697                            MONTH(q.issue_date) ASC";
4698
4699            $sendBudgets = [];
4700            $sendBudgetsTotals = [];
4701
4702            // FIRE-1145: domain-tagged cache (was Cache::get/put + Cache::flush()).
4703            $result = ResultCache::remember('quotations', $query, 600, fn () => DB::select($query));
4704
4705            if (count($result) > 0) {
4706                for ($i = 0; $i < count($result); $i++) {
4707
4708                    if ($result[$i]->year == null && $result[$i]->month == null) {
4709                        $result[$i]->month = 'totalGeneral';
4710                        $sendBudgetsTotals['totalGeneral'] = $result[$i];
4711
4712                        continue;
4713                    } elseif ($result[$i]->month == null && $result[$i]->year != null) {
4714                        if (count($data['years']) > 0 || $whereYear == '') {
4715                            $result[$i]->month = 'totalSub';
4716                        } else {
4717                            continue;
4718                        }
4719                    } else {
4720                        $result[$i]->month = sprintf('%02d', $result[$i]->month);
4721                    }
4722
4723                    $sendBudgets[$result[$i]->year][$result[$i]->month] = $result[$i];
4724                }
4725            }
4726
4727            return response([
4728                'message' => 'OK',
4729                'data' => $sendBudgets,
4730                'totals' => $sendBudgetsTotals,
4731            ]);
4732
4733        } catch (\Exception $e) {
4734            report(AppException::fromException($e, 'LIST_QUOTATION_ANALYTICS_EXCEPTION'));
4735
4736            return response(['message' => 'KO', 'error' => $e->getMessage()]);
4737        }
4738    }
4739
4740    public function list_quotation_analytics_track_budgets(Request $request): ResponseFactory|HttpResponse
4741    {
4742
4743        try {
4744
4745            $data = $request->all();
4746            $companyId = addslashes((string) $data['company_id']);
4747
4748            $where = '';
4749            $whereYear = '';
4750            $isBetween = false;
4751
4752            $dateLflArray = [];
4753            $companyIds = $this->companyIds;
4754
4755            if ($companyId != 0) {
4756                $companyIds = [$companyId];
4757            }
4758
4759            if (isset($data['years']) && $data['years'] != null) {
4760                $years = implode(',', $data['years']);
4761                if (count($data['years']) > 0) {
4762                    $whereYear = " AND YEAR(q.issue_date) IN ({$years})";
4763                }
4764            }
4765
4766            $field = 'issue_date';
4767
4768            foreach ($companyIds as $v) {
4769
4770                $lflWhere = " AND q.company_id = {$v} ";
4771
4772                $query = "SELECT
4773                            CONCAT(
4774                                DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field}),
4775                                ' - ',
4776                                DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field})
4777                            ) AS date_like,
4778                            YEAR(q.{$field}) 'year',
4779                            DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS min_date_like,
4780                            DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS max_date_like,
4781                            {$v} 'company_id'
4782                        FROM
4783                            tbl_quotations q
4784                        WHERE
4785                            q.{$field} IS NOT NULL
4786                            AND q.for_add = 0
4787                            {$lflWhere}
4788                            {$whereYear}
4789                        GROUP BY YEAR(q.{$field})
4790                        ORDER BY YEAR(q.{$field}) DESC";
4791
4792                $dateLike = DB::select($query);
4793
4794                $dateLflArray[$v] = $dateLike;
4795            }
4796
4797            $whereAcceptanceDate = 'q.acceptance_date IS NOT NULL ';
4798
4799            $isFy = true;
4800
4801            if (isset($data['issue_year_ytd']) && $data['issue_year_ytd'] != null && $data['issue_year_ytd'] == true) {
4802                $isFy = false;
4803                $ytdArray = [];
4804                $ytdAcceptanceArray = [];
4805                $lflCompanyIds = [];
4806                $lflCompanyIdsAcc = [];
4807                foreach ($dateLflArray as $k => $v) {
4808                    foreach ($dateLflArray[$k] as $item) {
4809                        $year = $item->year;
4810                        $now = date('m-d');
4811                        array_push($ytdAcceptanceArray, "DATE_FORMAT(q.acceptance_date, '%Y-%m-%d') BETWEEN '{$year}-01-01' AND '{$year}-{$now}' AND YEAR(q.acceptance_date) = YEAR(issue_date)");
4812                        array_push($ytdArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN '{$year}-01-01' AND '{$year}-{$now}'");
4813                    }
4814
4815                    $ytdArray = implode(' OR ', $ytdArray);
4816                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$ytdArray})");
4817                    $ytdArray = [];
4818
4819                    $ytdAcceptanceArray = implode(' OR ', $ytdAcceptanceArray);
4820                    array_push($lflCompanyIdsAcc, "q.company_id = {$k} AND ({$ytdAcceptanceArray})");
4821                    $ytdAcceptanceArray = [];
4822                }
4823
4824                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
4825                $where .= " AND ({$lflCompanyIds}";
4826
4827                $lflCompanyIdsAcc = implode(' OR ', $lflCompanyIdsAcc);
4828                $whereAcceptanceDate .= " AND ({$lflCompanyIdsAcc}";
4829            }
4830
4831            if (isset($data['issue_year_lfl']) && $data['issue_year_lfl'] != null && $data['issue_year_lfl'] == true) {
4832                $isFy = false;
4833                $lflArray = [];
4834                $ytdAcceptanceArray = [];
4835                $lflCompanyIds = [];
4836                $lflCompanyIdsAcc = [];
4837                foreach ($dateLflArray as $k => $v) {
4838                    foreach ($dateLflArray[$k] as $item) {
4839                        $year = $item->year;
4840                        $min_date_like = $item->min_date_like;
4841                        $max_date_like = $item->max_date_like;
4842                        array_push($ytdAcceptanceArray, "DATE_FORMAT(q.acceptance_date, '%Y-%m-%d') BETWEEN LEAST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND GREATEST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND YEAR(q.acceptance_date) = YEAR(issue_date)");
4843                        array_push($lflArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN LEAST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND GREATEST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}')");
4844                    }
4845
4846                    $lflArray = implode(' OR ', $lflArray);
4847                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$lflArray})");
4848                    $lflArray = [];
4849
4850                    $ytdAcceptanceArray = implode(' OR ', $ytdAcceptanceArray);
4851                    array_push($lflCompanyIdsAcc, "q.company_id = {$k} AND ({$ytdAcceptanceArray})");
4852                    $ytdAcceptanceArray = [];
4853                }
4854
4855                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
4856                $where .= " AND ({$lflCompanyIds}";
4857
4858                $lflCompanyIdsAcc = implode(' OR ', $lflCompanyIdsAcc);
4859                $whereAcceptanceDate .= " AND ({$lflCompanyIdsAcc}";
4860            }
4861
4862            if ($isFy) {
4863                if ($companyId != 0) {
4864                    $where .= " AND q.company_id = {$companyId} ";
4865                } else {
4866                    $where .= " AND q.company_id IN ({$this->companyId})";
4867                }
4868            }
4869
4870            if (isset($data['source']) && $data['source'] != null) {
4871                $where .= " AND s.name = '{$data['source']}'";
4872            }
4873
4874            if (isset($data['commercial']) && $data['commercial'] != null) {
4875                $where .= " AND q.commercial = '{$data['commercial']}'";
4876            }
4877
4878            if (isset($data['created_by']) && $data['created_by'] != null) {
4879                $where .= " AND q.created_by = '{$data['created_by']}'";
4880            }
4881
4882            if (isset($data['budget_type']) && $data['budget_type'] != null) {
4883                $where .= " AND bt.budget_type_id = {$data['budget_type']}";
4884            }
4885
4886            if (isset($data['budget_type_group']) && $data['budget_type_group'] != null) {
4887                $where .= " AND bt.budget_type_group_id = {$data['budget_type_group']}";
4888            }
4889
4890            if (isset($data['budget_status']) && $data['budget_status'] != null) {
4891                $where .= " AND bs.budget_status_id = {$data['budget_status']}";
4892            }
4893
4894            if (isset($data['client_type']) && $data['client_type'] != null) {
4895                $where .= " AND ct.customer_type_id = {$data['client_type']}";
4896            }
4897
4898            if (isset($data['segment_id']) && $data['segment_id'] != null) {
4899                $where .= " AND q.segment_id = {$data['segment_id']}";
4900            }
4901
4902            $query = "SELECT
4903                            YEAR(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)) AS 'year',
4904                            LPAD(MONTH(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)), 2, 0) AS 'month',
4905                            LPAD(WEEK(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)), 2, 0) AS 'week',
4906                            DATE_FORMAT(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY), '%W, %M %e') issue_date,
4907                            COUNT(
4908                                CASE WHEN q.issue_date IS NOT NULL
4909                                THEN 1 END
4910                            ) AS totalIssue,
4911                            GROUP_CONCAT(
4912                                CASE WHEN q.issue_date IS NOT NULL
4913                                THEN q.id END
4914                            ) AS groupConcatIds,
4915                            COUNT(
4916                                CASE WHEN {$whereAcceptanceDate}
4917                                AND bs.name = 'Aceptado' THEN 1 END) totalAccept,
4918                            COUNT(
4919                                CASE WHEN q.acceptance_date IS NOT NULL
4920                                AND {$whereAcceptanceDate}
4921                                AND bs.name = 'Aceptado'
4922                                AND DATEDIFF(q.acceptance_date, q.issue_date) <= 10 THEN 1 END
4923                            ) / COUNT(
4924                                CASE WHEN q.issue_date IS NOT NULL
4925                                THEN 1 END
4926                            ) * 100 AS percentageOfacceptanceLessThan10,
4927                            COUNT(
4928                                CASE WHEN q.acceptance_date IS NOT NULL
4929                                AND {$whereAcceptanceDate}
4930                                AND bs.name = 'Aceptado'
4931                                AND DATEDIFF(q.acceptance_date, q.issue_date) > 10
4932                                AND DATEDIFF(q.acceptance_date, q.issue_date) < 30 THEN 1 END
4933                            ) / COUNT(
4934                                CASE WHEN q.issue_date IS NOT NULL
4935                                THEN 1 END
4936                            ) * 100 AS percentageOfacceptanceLessThan30,
4937                            COUNT(
4938                                CASE WHEN q.acceptance_date IS NOT NULL
4939                                AND {$whereAcceptanceDate}
4940                                AND bs.name = 'Aceptado'
4941                                AND DATEDIFF(q.acceptance_date, q.issue_date) >= 30 THEN 1 END
4942                            ) / COUNT(
4943                                CASE WHEN q.issue_date IS NOT NULL
4944                                THEN 1 END
4945                            ) * 100 AS percentageOfacceptanceMoreThan30,
4946                            COUNT(CASE WHEN
4947                                {$whereAcceptanceDate} THEN 1 END
4948                            ) / COUNT(
4949                                CASE WHEN q.issue_date IS NOT NULL
4950                                THEN 1 END
4951                            ) * 100 acceptedPercentage,
4952                            COALESCE(
4953                                AVG(
4954                                    CASE WHEN q.issue_date IS NOT NULL
4955                                    AND q.acceptance_date IS NOT NULL
4956                                    AND {$whereAcceptanceDate}
4957                                    AND bs.name = 'Aceptado'
4958                                    THEN DATEDIFF(q.acceptance_date, q.issue_date) ELSE NULL END
4959                                ), 0
4960                            )
4961                            AS averageAcceptedDuration,
4962                            COALESCE(
4963                                AVG(
4964                                    CASE WHEN q.issue_date IS NOT NULL
4965                                    AND q.request_date IS NOT NULL
4966                                    THEN DATEDIFF(q.issue_date, q.request_date) ELSE NULL END
4967                                ), 0
4968                            )
4969                            AS averageRequestedDays
4970                        FROM
4971                            tbl_quotations q
4972                            LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
4973                            LEFT JOIN tbl_budget_status bs ON bs.budget_status_id = q.budget_status_id
4974                            LEFT JOIN tbl_sources s ON s.source_id = q.source_id
4975                            LEFT JOIN tbl_customer_types ct ON q.customer_type_id = ct.customer_type_id
4976                        WHERE
4977                            q.issue_date IS NOT NULL
4978                            AND q.budget_type_id != 7
4979                            AND q.budget_type_id IS NOT NULL
4980                            AND q.for_add = 0
4981                            AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
4982                            {$where}
4983                            {$whereYear}
4984                        GROUP BY
4985                            YEAR(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)),
4986                            MONTH(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)),
4987                            WEEK(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)) WITH ROLLUP
4988                        ORDER BY
4989                            YEAR DESC,
4990                            MONTH ASC,
4991                            WEEK ASC,
4992                            DATE_FORMAT(q.issue_date, '%e') ASC";
4993
4994            // FIRE-1145: domain-tagged cache (was Cache::get/put + Cache::flush()).
4995            $result = ResultCache::remember('quotations', $query, 600, fn () => DB::select($query));
4996
4997            return response([
4998                'message' => 'OK',
4999                'data' => $result,
5000            ]);
5001
5002        } catch (\Exception $e) {
5003            report(AppException::fromException($e, 'LIST_QUOTATION_ANALYTICS_TRACK_BUDGETS_EXCEPTION'));
5004
5005            return response(['message' => 'KO', 'error' => $e->getMessage()]);
5006        }
5007    }
5008
5009    public function list_quotation_analytics_types_budgets(Request $request): ResponseFactory|HttpResponse
5010    {
5011
5012        try {
5013
5014            $data = $request->all();
5015            $companyId = addslashes((string) $data['company_id']);
5016
5017            $where = '';
5018            $whereYear = '';
5019            $isBetween = false;
5020
5021            $dateLflArray = [];
5022            $companyIds = $this->companyIds;
5023
5024            if ($companyId != 0) {
5025                $companyIds = [$companyId];
5026            }
5027
5028            if (isset($data['years']) && $data['years'] != null) {
5029                $years = implode(',', $data['years']);
5030                if (count($data['years']) > 0) {
5031                    $whereYear = " AND YEAR(q.issue_date) IN ({$years})";
5032                }
5033            }
5034
5035            $field = 'issue_date';
5036
5037            foreach ($companyIds as $v) {
5038
5039                $lflWhere = " AND q.company_id = {$v} ";
5040
5041                $query = "SELECT
5042                            CONCAT(
5043                                DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field}),
5044                                ' - ',
5045                                DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field})
5046                            ) AS date_like,
5047                            YEAR(q.{$field}) 'year',
5048                            DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS min_date_like,
5049                            DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS max_date_like,
5050                            {$v} 'company_id'
5051                        FROM
5052                            tbl_quotations q
5053                        WHERE
5054                            q.{$field} IS NOT NULL
5055                            AND q.for_add = 0
5056                            {$lflWhere}
5057                            {$whereYear}
5058                        GROUP BY YEAR(q.{$field})
5059                        ORDER BY YEAR(q.{$field}) DESC";
5060
5061                $dateLike = DB::select($query);
5062
5063                $dateLflArray[$v] = $dateLike;
5064            }
5065
5066            $whereAcceptanceDate = 'q.acceptance_date IS NOT NULL ';
5067
5068            $isFy = true;
5069
5070            if (isset($data['issue_year_ytd']) && $data['issue_year_ytd'] != null && $data['issue_year_ytd'] == true) {
5071                $isFy = false;
5072                $ytdArray = [];
5073                $ytdAcceptanceArray = [];
5074                $lflCompanyIds = [];
5075                $lflCompanyIdsAcc = [];
5076                foreach ($dateLflArray as $k => $v) {
5077                    foreach ($dateLflArray[$k] as $item) {
5078                        $year = $item->year;
5079                        $now = date('m-d');
5080                        array_push($ytdAcceptanceArray, "DATE_FORMAT(q.acceptance_date, '%Y-%m-%d') BETWEEN '{$year}-01-01' AND '{$year}-{$now}' AND YEAR(q.acceptance_date) = YEAR(issue_date)");
5081                        array_push($ytdArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN '{$year}-01-01' AND '{$year}-{$now}'");
5082                    }
5083
5084                    $ytdArray = implode(' OR ', $ytdArray);
5085                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$ytdArray})");
5086                    $ytdArray = [];
5087
5088                    $ytdAcceptanceArray = implode(' OR ', $ytdAcceptanceArray);
5089                    array_push($lflCompanyIdsAcc, "q.company_id = {$k} AND ({$ytdAcceptanceArray})");
5090                    $ytdAcceptanceArray = [];
5091                }
5092
5093                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
5094                $where .= " AND ({$lflCompanyIds}";
5095
5096                $lflCompanyIdsAcc = implode(' OR ', $lflCompanyIdsAcc);
5097                $whereAcceptanceDate .= " AND ({$lflCompanyIdsAcc}";
5098            }
5099
5100            if (isset($data['issue_year_lfl']) && $data['issue_year_lfl'] != null && $data['issue_year_lfl'] == true) {
5101                $isFy = false;
5102                $lflArray = [];
5103                $ytdAcceptanceArray = [];
5104                $lflCompanyIds = [];
5105                $lflCompanyIdsAcc = [];
5106                foreach ($dateLflArray as $k => $v) {
5107                    foreach ($dateLflArray[$k] as $item) {
5108                        $year = $item->year;
5109                        $min_date_like = $item->min_date_like;
5110                        $max_date_like = $item->max_date_like;
5111                        array_push($ytdAcceptanceArray, "DATE_FORMAT(q.acceptance_date, '%Y-%m-%d') BETWEEN LEAST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND GREATEST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND YEAR(q.acceptance_date) = YEAR(issue_date)");
5112                        array_push($lflArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN LEAST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND GREATEST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}')");
5113                    }
5114
5115                    $lflArray = implode(' OR ', $lflArray);
5116                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$lflArray})");
5117                    $lflArray = [];
5118
5119                    $ytdAcceptanceArray = implode(' OR ', $ytdAcceptanceArray);
5120                    array_push($lflCompanyIdsAcc, "q.company_id = {$k} AND ({$ytdAcceptanceArray})");
5121                    $ytdAcceptanceArray = [];
5122                }
5123
5124                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
5125                $where .= " AND ({$lflCompanyIds}";
5126
5127                $lflCompanyIdsAcc = implode(' OR ', $lflCompanyIdsAcc);
5128                $whereAcceptanceDate .= " AND ({$lflCompanyIdsAcc}";
5129            }
5130
5131            if ($isFy) {
5132                if ($companyId != 0) {
5133                    $where .= " AND q.company_id = {$companyId} ";
5134                } else {
5135                    $where .= " AND q.company_id IN ({$this->companyId})";
5136                }
5137            }
5138
5139            if (isset($data['budget_status']) && $data['budget_status'] != null) {
5140                $where .= " AND bs.budget_status_id = {$data['budget_status']}";
5141            }
5142
5143            if (isset($data['segment_id']) && $data['segment_id'] != null) {
5144                $where .= " AND q.segment_id = {$data['segment_id']}";
5145            }
5146
5147            $query = "SELECT
5148                            YEAR(q.issue_date) AS 'year',
5149                            bt.name,
5150                            COUNT(
5151                                CASE WHEN q.issue_date IS NOT NULL
5152                                THEN 1 END
5153                            ) AS totalIssue,
5154                            GROUP_CONCAT(
5155                                CASE WHEN q.issue_date IS NOT NULL
5156                                THEN q.id END
5157                            ) AS groupConcatIds,
5158                            COUNT(CASE WHEN {$whereAcceptanceDate} AND bs.name = 'Aceptado' THEN 1 END) totalAccept,
5159                            COUNT(
5160                                CASE WHEN q.acceptance_date IS NOT NULL
5161                                AND {$whereAcceptanceDate}
5162                                AND bs.name = 'Aceptado'
5163                                AND DATEDIFF(q.acceptance_date, q.issue_date) < 10 THEN 1 END
5164                            ) / COUNT(
5165                                CASE WHEN q.issue_date IS NOT NULL
5166                                THEN 1 END
5167                            ) * 100 AS percentageOfacceptanceLessThan10,
5168                            COUNT(
5169                                CASE WHEN q.acceptance_date IS NOT NULL
5170                                AND {$whereAcceptanceDate}
5171                                AND bs.name = 'Aceptado'
5172                                AND DATEDIFF(q.acceptance_date, q.issue_date) > 10
5173                                AND DATEDIFF(q.acceptance_date, q.issue_date) < 30 THEN 1 END
5174                            ) / COUNT(
5175                                CASE WHEN q.issue_date IS NOT NULL
5176                                THEN 1 END
5177                            ) * 100 AS percentageOfacceptanceLessThan30,
5178                            COUNT(
5179                                CASE WHEN q.acceptance_date IS NOT NULL
5180                                AND {$whereAcceptanceDate}
5181                                AND bs.name = 'Aceptado'
5182                                AND DATEDIFF(q.acceptance_date, q.issue_date) > 30 THEN 1 END
5183                            ) / COUNT(
5184                                CASE WHEN q.issue_date IS NOT NULL
5185                                THEN 1 END
5186                            ) * 100 AS percentageOfacceptanceMoreThan30,
5187                            COUNT(CASE WHEN {$whereAcceptanceDate} THEN 1 END) / COUNT(
5188                                CASE WHEN q.issue_date IS NOT NULL
5189                                THEN 1 END
5190                            ) * 100 acceptedPercentage,
5191                            COALESCE(
5192                                AVG(
5193                                    CASE WHEN q.issue_date IS NOT NULL
5194                                    AND q.acceptance_date IS NOT NULL
5195                                    AND {$whereAcceptanceDate}
5196                                    AND bs.name = 'Aceptado'
5197                                    THEN DATEDIFF(q.acceptance_date, q.issue_date) ELSE NULL END
5198                                ), 0
5199                            )
5200                            AS averageAcceptedDuration,
5201                            COALESCE(
5202                                AVG(
5203                                    CASE WHEN q.issue_date IS NOT NULL
5204                                    AND q.request_date IS NOT NULL
5205                                    AND q.issue_date > q.request_date
5206                                    THEN DATEDIFF(q.issue_date, q.request_date) ELSE NULL END
5207                                ), 0
5208                            )
5209                            AS averageRequestedDays
5210                        FROM
5211                            tbl_quotations q
5212                            LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
5213                            LEFT JOIN tbl_budget_status bs ON bs.budget_status_id = q.budget_status_id
5214                        WHERE
5215                            q.issue_date IS NOT NULL
5216                            AND q.budget_type_id != 7
5217                            AND q.budget_type_id IS NOT NULL
5218                            AND q.for_add = 0
5219                            AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
5220                            {$where}
5221                            {$whereYear}
5222                        GROUP BY
5223                            YEAR(q.issue_date),
5224                            bt.name WITH ROLLUP";
5225
5226            // FIRE-1145: domain-tagged cache (was Cache::get/put + Cache::flush()).
5227            $result = ResultCache::remember('quotations', $query, 600, fn () => DB::select($query));
5228
5229            $typesBudgets = [];
5230            $typesBudgetsTotals = [];
5231
5232            if (count($result) > 0) {
5233                for ($i = 0; $i < count($result); $i++) {
5234
5235                    if ($result[$i]->year == null && $result[$i]->name == null) {
5236                        $result[$i]->name = 'totalGeneral';
5237                        $typesBudgetsTotals['totalGeneral'] = $result[$i];
5238
5239                        continue;
5240                    } elseif ($result[$i]->name == null && $result[$i]->year != null) {
5241                        if (count($data['years']) > 0 || $whereYear == '') {
5242                            $result[$i]->name = 'totalSub';
5243                        } else {
5244                            continue;
5245                        }
5246                    }
5247
5248                    $typesBudgets[$result[$i]->year][$result[$i]->name] = $result[$i];
5249                }
5250            }
5251
5252            return response([
5253                'message' => 'OK',
5254                'data' => $typesBudgets,
5255                'totals' => $typesBudgetsTotals,
5256            ]);
5257
5258        } catch (\Exception $e) {
5259            report(AppException::fromException($e, 'LIST_QUOTATION_ANALYTICS_TYPES_BUDGETS_EXCEPTION'));
5260
5261            return response(['message' => 'KO', 'error' => $e->getMessage()]);
5262        }
5263    }
5264
5265    public function download_quotations(Request $request): ResponseFactory|HttpResponse
5266    {
5267        ini_set('max_execution_time', 123456);
5268        $data = $request->all();
5269        $companyId = addslashes($data['company_id']);
5270        $userId = addslashes((string) $data['user_id']);
5271
5272        $where = '';
5273
5274        $query = "SELECT
5275                b.name
5276            FROM tbl_users a
5277            LEFT JOIN tbl_roles b
5278                ON a.role_id = b.role_id
5279            WHERE a.id = {$userId}";
5280
5281        $role = DB::select($query);
5282
5283        $r = new Request([
5284            'filterModel' => $data['filterModel'],
5285            'sortModel' => $data['sortModel'],
5286            'start' => 0,
5287            'end' => 999999999,
5288            'company_id' => $data['company_id'],
5289            'user_id' => $data['user_id'],
5290            'ids' => $data['ids'],
5291            'searchText' => $data['searchText'],
5292            'ids_not_in' => $data['ids_not_in'],
5293        ]);
5294
5295        $result = $this->list_orders_table($r);
5296
5297        $result = $result->original['data'];
5298
5299        return response(['data' => $result]);
5300    }
5301
5302    public function download_quotations_csv($filename, $data) {}
5303
5304    public function bulk_upload(Request $request): ResponseFactory|HttpResponse
5305    {
5306
5307        try {
5308
5309            $data = $request->all();
5310            $file = $request->file('file');
5311            $exte = 'Xlsx';
5312            $companyId = $data['company_id'];
5313
5314            if ($file->getMimeType() == 'application/vnd.ms-excel') {
5315                $exte = 'Xls';
5316            }
5317
5318            $destination_path = config('app.bulk_upload_file_destination');
5319            $filename = $file->getClientOriginalName();
5320
5321            if (file_exists($destination_path.$filename)) {
5322                $filename = pathinfo($filename, PATHINFO_FILENAME).'-'.uniqid().'.'.pathinfo($filename, PATHINFO_EXTENSION);
5323            }
5324
5325            $file->move($destination_path, $filename);
5326
5327            TblBulkUpload::create(
5328                [
5329                    'company_id' => $companyId,
5330                    'filename' => $filename,
5331                    'status' => 'Uploading...',
5332                    'is_running' => 1,
5333                    'uploaded_by' => $data['created_by'],
5334                ]
5335            );
5336
5337            $command = "php BulkUploadQuotations.php '{$data['created_by']}' '{$exte}' '{$destination_path}' '{$filename}{$companyId}";
5338            exec($command.' > /dev/null &');
5339
5340            $query = '';
5341            $isRunning = 0;
5342
5343            if ($companyId == 0) {
5344                $query = 'SELECT * FROM tbl_bulk_upload ORDER BY started_at DESC';
5345                $isRunning = TblBulkUpload::where('is_running', 1)->count();
5346            } else {
5347                $query = "SELECT * FROM tbl_bulk_upload WHERE company_id = {$companyId} ORDER BY started_at DESC";
5348                $isRunning = TblBulkUpload::where('is_running', 1)->where('company_id', $companyId)->count();
5349            }
5350
5351            $result = DB::select($query);
5352
5353            return response(['message' => 'OK', 'data' => $result, 'is_running' => $isRunning]);
5354
5355        } catch (\Exception $e) {
5356            report(AppException::fromException($e, 'BULK_UPLOAD_QUOTATIONS_EXCEPTION'));
5357
5358            return response(['message' => 'KO', 'error' => $e->getMessage()]);
5359        }
5360    }
5361
5362    public function list_bulk_upload($companyId): ResponseFactory|HttpResponse
5363    {
5364
5365        try {
5366
5367            $companyId = addslashes((string) $companyId);
5368            $query = '';
5369            $isRunning = 0;
5370
5371            if ($companyId == 0) {
5372                $query = 'SELECT * FROM tbl_bulk_upload ORDER BY started_at DESC';
5373                $isRunning = TblBulkUpload::where('is_running', 1)->count();
5374            } else {
5375                $query = "SELECT * FROM tbl_bulk_upload WHERE company_id = {$companyId} ORDER BY started_at DESC";
5376                $isRunning = TblBulkUpload::where('is_running', 1)->where('company_id', $companyId)->count();
5377            }
5378
5379            $result = DB::select($query);
5380
5381            return response(['message' => 'OK', 'data' => $result, 'is_running' => $isRunning]);
5382
5383        } catch (\Exception $e) {
5384            report(AppException::fromException($e, 'LIST_BULK_UPLOAD_EXCEPTION'));
5385
5386            return response(['message' => 'KO', 'error' => $e->getMessage()]);
5387        }
5388
5389    }
5390
5391    public function delete_number(Request $request, $id): ResponseFactory|HttpResponse
5392    {
5393
5394        try {
5395
5396            $id = addslashes((string) $id);
5397            $data = $request->all();
5398
5399            $r = TblQuotations::where('id', $id)->first();
5400            $updatedAt = date('Y-m-d H:i:s');
5401            $query = "INSERT INTO tbl_quotations_deleted (id, quote_id, company_id, for_add, created_by, updated_by, updated_at)
5402                        SELECT id, quote_id, company_id, 1, created_by, '{$data['updated_by']}', '{$updatedAt}' FROM tbl_quotations WHERE id = {$id}";
5403
5404            DB::select($query);
5405
5406            TblQuotations::where('id', $id)->delete();
5407
5408            $latestBudget = TblQuotations::where('company_id', $r->company_id)->orderByRaw('id DESC')->value('quote_id');
5409
5410            $query = "UPDATE tbl_companies SET last_id = '{$latestBudget}' WHERE company_id = {$r->company_id}";
5411            DB::select($query);
5412
5413            return response(['message' => 'OK']);
5414
5415        } catch (\Exception $e) {
5416            report(AppException::fromException($e, 'DELETE_QUOTATION_EXCEPTION'));
5417
5418            return response(['message' => 'KO', 'error' => $e->getMessage()]);
5419        }
5420
5421    }
5422
5423    public function get_number(Request $request, $companyId, $n = null): ResponseFactory|HttpResponse
5424    {
5425
5426        try {
5427
5428            $companyId = addslashes((string) $companyId);
5429            $data = $request->all();
5430            $latestBudget = [];
5431            $number = 0;
5432            $beforeLastId = null;
5433
5434            $x = true;
5435
5436            if ($companyId == 0) {
5437                $latestBudget = TblQuotations::orderByRaw('CAST(quote_id AS DOUBLE) DESC')->value('quote_id');
5438            } else {
5439                $latestBudget = TblCompanies::where('company_id', $companyId)->value('last_id');
5440
5441                if ($latestBudget == null) {
5442                    $latestBudget = TblQuotations::where('company_id', $companyId)->orderByRaw('id DESC')->value('quote_id');
5443                    $beforeLastId = $latestBudget;
5444                }
5445            }
5446
5447            $number = $latestBudget;
5448
5449            while ($x) {
5450
5451                if (is_numeric(substr((string) $number, -1))) {
5452                    $number++;
5453                } else {
5454                    $number .= '1';
5455                }
5456
5457                $check = 0;
5458
5459                if ($companyId == 0) {
5460                    $check = TblQuotations::where('quote_id', (string) $number)->count();
5461                } else {
5462                    $check = TblQuotations::where('company_id', $companyId)->where('quote_id', (string) $number)->count();
5463                }
5464
5465                if ($check == 0) {
5466                    $x = false;
5467                }
5468            }
5469
5470            $result = null;
5471
5472            if ($n == null) {
5473                $result = TblQuotations::create(['quote_id' => $number, 'company_id' => $companyId, 'for_add' => 1, 'created_by' => $data['created_by']]);
5474            }
5475
5476            if ($beforeLastId == null) {
5477                $beforeLastId = $number;
5478            }
5479
5480            $query = "UPDATE tbl_companies SET last_id = '{$number}', before_last_id = CASE WHEN before_last_id IS NULL THEN '{$beforeLastId}' ELSE before_last_id END WHERE company_id = {$companyId}";
5481            DB::select($query);
5482
5483            return response([
5484                'message' => 'OK',
5485                'number' => $number,
5486                'id' => ($result != null) ? $result->id : null,
5487            ]);
5488
5489        } catch (\Exception $e) {
5490            report(AppException::fromException($e, 'GET_NUMBER_EXCEPTION'));
5491
5492            return response(['message' => 'KO', 'error' => $e->getMessage()]);
5493        }
5494
5495    }
5496
5497    public function get_years(Request $request): ResponseFactory|HttpResponse
5498    {
5499
5500        try {
5501
5502            $data = $request->all();
5503            $companyId = addslashes((string) $data['company_id']);
5504            $where = '';
5505
5506            if ($companyId != 0) {
5507                $where = " AND company_id = {$companyId} ";
5508            } else {
5509                $where = " AND company_id IN ({$this->companyId}";
5510            }
5511
5512            $query = "SELECT
5513                        CONCAT(
5514                            DATE_FORMAT((SELECT MIN(issue_date) FROM tbl_quotations WHERE issue_date IS NOT NULL {$where}), '%c/%e/'), YEAR(issue_date),
5515                            ' - ',
5516                            DATE_FORMAT((SELECT MAX(issue_date) FROM tbl_quotations WHERE issue_date IS NOT NULL {$where}), '%c/%e/'), YEAR(issue_date)
5517                        ) AS date_like,
5518                        YEAR(issue_date) 'year'
5519                        FROM tbl_quotations
5520                        WHERE issue_date IS NOT NULL {$where}
5521                        GROUP BY YEAR(issue_date)
5522                        ORDER BY YEAR(issue_date) DESC";
5523
5524            $issueDate = DB::select($query);
5525
5526            $query = "SELECT
5527                        CONCAT(
5528                            DATE_FORMAT((SELECT MIN(created_at) FROM tbl_quotations WHERE created_at IS NOT NULL {$where}), '%c/%e/'), YEAR(created_at),
5529                            ' - ',
5530                            DATE_FORMAT((SELECT MAX(created_at) FROM tbl_quotations WHERE created_at IS NOT NULL {$where}), '%c/%e/'), YEAR(created_at)
5531                        ) AS date_like,
5532                        YEAR(created_at) 'year'
5533                        FROM tbl_quotations
5534                        WHERE created_at IS NOT NULL {$where}
5535                        GROUP BY YEAR(created_at)
5536                        ORDER BY YEAR(created_at) DESC";
5537
5538            $createdAt = DB::select($query);
5539
5540            $query = "SELECT
5541                        CONCAT(
5542                            DATE_FORMAT((SELECT MIN(acceptance_date) FROM tbl_quotations WHERE acceptance_date IS NOT NULL {$where}), '%c/%e/'), YEAR(acceptance_date),
5543                            ' - ',
5544                            DATE_FORMAT((SELECT MAX(acceptance_date) FROM tbl_quotations WHERE acceptance_date IS NOT NULL {$where}), '%c/%e/'), YEAR(acceptance_date)
5545                        ) AS date_like,
5546                        YEAR(acceptance_date) 'year'
5547                        FROM tbl_quotations
5548                        WHERE acceptance_date IS NOT NULL {$where}
5549                        GROUP BY YEAR(acceptance_date)
5550                        ORDER BY YEAR(acceptance_date) DESC";
5551
5552            $acceptanceDate = DB::select($query);
5553
5554            return response([
5555                'message' => 'OK',
5556                'data' => $issueDate,
5557                'created_at' => $createdAt,
5558                'acceptance_date' => $acceptanceDate,
5559            ]);
5560
5561        } catch (\Exception $e) {
5562            report(AppException::fromException($e, 'GET_YEARS_EXCEPTION'));
5563
5564            return response(['message' => 'KO', 'error' => $e->getMessage()]);
5565        }
5566
5567    }
5568
5569    public function human_filesize($bytes, $decimals = 2): string
5570    {
5571        $size = ['B', 'KB', 'MB'];
5572
5573        $factor = floor((strlen($bytes) - 1) / 3);
5574
5575        return number_format($bytes / 1024 ** $factor, 2, ',', '.').$size[$factor];
5576    }
5577
5578    public function get_files($quoteId): ResponseFactory|HttpResponse
5579    {
5580
5581        try {
5582
5583            $quoteId = addslashes((string) $quoteId);
5584
5585            $quoteId = (int) $quoteId;
5586            $query = '
5587            SELECT
5588                file_id,
5589                quotation_id,
5590                quote_id,
5591                original_name,
5592                filename,
5593                uploaded_by,
5594                uploaded_at,
5595                is_internal,
5596                file_size,
5597                file_hash,
5598                mime_type
5599            FROM tbl_files
5600            WHERE quotation_id = ?
5601            AND is_internal IS NULL';
5602
5603            $result = DB::select($query, [$quoteId]);
5604
5605            foreach ($result as $file) {
5606                if ($file->file_size > 0) {
5607                    $file->filesize = $this->human_filesize($file->file_size);
5608                } else {
5609                    $path = 'uploads/'.$file->filename;
5610
5611                    if (Storage::disk('s3')->exists($path)) {
5612                        $fileSizeBytes = Storage::disk('s3')->size($path);
5613                    } else {
5614                        $fileSizeBytes = 0;
5615                    }
5616
5617                    $file->filesize = $this->human_filesize($fileSizeBytes);
5618                }
5619
5620                $file->original_name = $file->original_name." ({$file->filesize})";
5621            }
5622
5623            $query = '
5624            SELECT
5625                file_id,
5626                quotation_id,
5627                quote_id,
5628                original_name,
5629                filename,
5630                uploaded_by,
5631                uploaded_at,
5632                is_internal,
5633                file_size,
5634                file_hash,
5635                mime_type
5636            FROM tbl_files
5637            WHERE quotation_id = ?
5638            AND is_internal = 1';
5639
5640            $internal = DB::select($query, [$quoteId]);
5641
5642            foreach ($internal as $file) {
5643                if ($file->file_size > 0) {
5644                    $file->filesize = $this->human_filesize($file->file_size);
5645                } else {
5646                    $path = 'uploads/'.$file->filename;
5647
5648                    if (Storage::disk('s3')->exists($path)) {
5649                        $fileSizeBytes = Storage::disk('s3')->size($path);
5650                    } else {
5651                        $fileSizeBytes = 0;
5652                    }
5653
5654                    $file->filesize = $this->human_filesize($fileSizeBytes);
5655                }
5656
5657                $file->original_name = $file->original_name." ({$file->filesize})";
5658            }
5659
5660            $job = TblOngoingJobs::where('quotation_id', $quoteId)->first();
5661            $order = TblQuotations::where('id', $quoteId)->first();
5662
5663            $z = 0;
5664            $status = 2;
5665            $xStatus = 'processed';
5666            $uniqueEvents = [];
5667            $currentEvents = [];
5668            $followUpLogs = [];
5669            $sendgridFollowUpLogs = [];
5670            $projectTypes = [];
5671
5672            if ($order) {
5673                $emails = explode(',', str_replace(' ', '', $order->email));
5674
5675                // FIRE-864 (30/04): pre-fix this used the composite WHERE
5676                // `quotation_id = ? AND x_message_id = ?`. That returned
5677                // null whenever:
5678                //   - tbl_quotations.x_message_id was NULL (status 22 rows
5679                //     where SendGrid never even attempted a send), or
5680                //   - the webhook row's quotation_id was NULL or didn't
5681                //     match (legacy webhook rows from before we started
5682                //     stamping quotation_id consistently).
5683                // The "Titan intentó enviarlo y SendGrid falló" modal then
5684                // showed empty even though webhook events existed. Match
5685                // on quotation_id only, newest first — we still get the
5686                // latest SendGrid event for that quote.
5687                $sendGrid = TblSendgridWebhook::where('quotation_id', $quoteId)
5688                    ->orderByDesc('id')
5689                    ->first();
5690
5691                if ($sendGrid) {
5692                    $emailErrors = ['deferred', 'bounce', 'dropped', 'spamreport', 'invalid'];
5693                    $events = json_decode($sendGrid->json_body, true);
5694                    $xMessageId = $sendGrid->x_message_id;
5695                    $isDelivered = 0;
5696                    $isProcessed = 0;
5697                    $isError = 0;
5698
5699                    foreach ($emails as $email) {
5700
5701                        $emailEvents = array_filter($events, fn (array $event): bool => strtolower((string) $event['email']) === strtolower($email));
5702
5703                        $statuses = array_unique(array_column($emailEvents, 'event'));
5704                        $eventCount = count($statuses);
5705
5706                        if ($eventCount === 1 && in_array('processed', $statuses)) {
5707                            $xStatus = 'processed';
5708                        }
5709
5710                        if ($eventCount == 2) {
5711                            if (in_array('processed', $statuses) && in_array('delivered', $statuses)) {
5712                                $xStatus = 'delivered';
5713                            }
5714
5715                            foreach ($emailErrors as $e) {
5716                                if (in_array('processed', $statuses) && in_array($e, $statuses)) {
5717                                    $xStatus = $e;
5718                                }
5719                            }
5720
5721                        } elseif ($eventCount > 2) {
5722                            if (in_array('processed', $statuses) && in_array('delivered', $statuses)) {
5723                                $xStatus = 'delivered';
5724                            }
5725
5726                            if ($xStatus != 'delivered') {
5727                                foreach ($emailErrors as $e) {
5728                                    if (in_array('processed', $statuses) && in_array($e, $statuses)) {
5729                                        $xStatus = $e;
5730                                    }
5731                                }
5732                            }
5733                        }
5734
5735                        if ($xStatus == 'processed') {
5736                            $isProcessed++;
5737                        } elseif ($xStatus == 'delivered') {
5738                            $isDelivered++;
5739                        } else {
5740                            $isError++;
5741                        }
5742
5743                        foreach ($emailEvents as $event) {
5744                            $key = strtolower($event['email']).'|'.$event['event'];
5745                            if (! isset($uniqueEvents[$key])) {
5746                                $uniqueEvents[$key] = $event;
5747                            }
5748                        }
5749                    }
5750
5751                    if ($uniqueEvents) {
5752                        foreach (array_values($uniqueEvents) as $d) {
5753                            $d['created_at'] = date('Y-m-d H:i:s', $d['timestamp']);
5754
5755                            if (isset($d['response'])) {
5756                                $d['error'] = $d['response'];
5757                            }
5758
5759                            if (isset($d['reason'])) {
5760                                $d['error'] = $d['reason'];
5761                            }
5762
5763                            array_push($currentEvents, $d);
5764                        }
5765                    }
5766
5767                    if (count($emails) == $isDelivered) {
5768                        $status = 1;
5769                        $xStatus = 'Completed';
5770                    } elseif ($isProcessed > 0) {
5771                        $status = 2;
5772                        $xStatus = 'Processing';
5773                    } elseif ($isError > 0) {
5774                        $status = 3;
5775
5776                        if ($xStatus == 'bounce') {
5777                            $xStatus = 'Error - Bounce';
5778                        } else {
5779                            $xStatus = 'Error';
5780                        }
5781                    }
5782                }
5783
5784                if ($order->x_status != $xStatus) {
5785                    TblQuotations::where('id', $order->id)->update(
5786                        [
5787                            'x_status' => $xStatus,
5788                        ]
5789                    );
5790                    $z = 1;
5791                    // FIRE-1145: was Cache::flush() — narrowed to quotations domain
5792                    // (this branch updates x_status, so list_quotations caches are
5793                    // genuinely affected, but no other domain is).
5794                    ResultCache::forgetDomain('quotations');
5795                }
5796
5797                if (empty($currentEvents)) {
5798                    $status = 0;
5799                }
5800
5801                $followUpLogs = TblFollowUpLogs::where('quotation_id', $quoteId)->orderBy('created_at', 'desc')->get();
5802
5803                $projectTypes = TblProjectTypes::where('company_id', $order->company_id)->get();
5804
5805                if ($order->y_message_id) {
5806                    $sendgridFollowUpLogs = TblSendgridWebhook::where('x_message_id', $order->y_message_id)->first();
5807
5808                    if ($sendgridFollowUpLogs) {
5809                        $sendgridFollowUpLogs = json_decode($sendgridFollowUpLogs->json_body, true);
5810                        foreach ($sendgridFollowUpLogs as &$item) {
5811                            $item['created_at'] = date('Y-m-d H:i:s', $item['timestamp']);
5812                        }
5813                    }
5814                }
5815            }
5816
5817            return response([
5818                'message' => 'OK',
5819                'data' => $result,
5820                'followUpLogs' => $followUpLogs,
5821                'sendgridFollowUpLogs' => $sendgridFollowUpLogs,
5822                'internal' => $internal,
5823                'projectTypes' => $projectTypes,
5824                'currentEvents' => [
5825                    'status' => $status,
5826                    'email' => $currentEvents,
5827                    $uniqueEvents,
5828                    $xStatus,
5829                ],
5830                'job' => $job,
5831                'isUpdated' => $z,
5832            ]);
5833
5834        } catch (\Exception $e) {
5835            report(AppException::fromException($e, 'GET_FILES_EXCEPTION'));
5836
5837            return response(['message' => 'KO', 'error' => $e->getMessage()]);
5838        }
5839
5840    }
5841
5842    public function download_file($fileId)
5843    {
5844        try {
5845            $fileId = addslashes((string) $fileId);
5846            $file = TblFiles::where('file_id', $fileId)->first();
5847
5848            if (! $file) {
5849                return response()->json([
5850                    'message' => 'KO',
5851                    'error' => 'Archivo no encontrado',
5852                ], 404);
5853            }
5854
5855            if (! is_null($file->file_hash) && ! empty($file->file)) {
5856                $fileContent = $file->file;
5857                $mimeType = $file->mime_type ?? 'application/octet-stream';
5858                $filename = $file->original_name ?? $file->filename ?? 'download';
5859
5860                return response($fileContent)
5861                    ->header('Content-Type', $mimeType)
5862                    ->header('Content-Disposition', 'attachment; filename="'.$filename.'"')
5863                    ->header('Content-Length', strlen((string) $fileContent))
5864                    ->header('Cache-Control', 'no-cache, no-store, must-revalidate');
5865
5866            } else {
5867                if (! Storage::disk('s3')->exists('uploads/'.$file->filename)) {
5868                    return response()->json([
5869                        'message' => 'KO',
5870                        'error' => 'Archivo físico no encontrado: '.$file->filename,
5871                    ], 404);
5872                }
5873
5874                // Storage::disk('s3')->get() returns the file CONTENTS, not a
5875                // path. The old code stored that binary string in $filePath and
5876                // then passed it to filesize()/mime_content_type()/
5877                // file_get_contents() as if it were a filename — any binary
5878                // payload (PDF, image, etc.) contains a null byte, so filesize()
5879                // aborted with "Filename contains null byte." Use the contents
5880                // directly and size them with strlen().
5881                $fileContent = Storage::disk('s3')->get('uploads/'.$file->filename);
5882                $fileSize = strlen((string) $fileContent);
5883                $mimeType = $file->mime_type
5884                    ?: (Storage::disk('s3')->mimeType('uploads/'.$file->filename) ?: 'application/octet-stream');
5885                $filename = $file->original_name ?? $file->filename;
5886
5887                return response($fileContent)
5888                    ->header('Content-Type', $mimeType)
5889                    ->header('Content-Disposition', 'attachment; filename="'.$filename.'"')
5890                    ->header('Content-Length', $fileSize)
5891                    ->header('Cache-Control', 'no-cache, no-store, must-revalidate');
5892            }
5893
5894        } catch (\Exception $e) {
5895            report(AppException::fromException($e, 'DOWNLOAD_FILE_EXCEPTION'));
5896            Log::error('Error downloading file: '.$e->getMessage());
5897
5898            return response()->json([
5899                'message' => 'KO',
5900                'error' => 'Error interno del servidor: '.$e->getMessage(),
5901            ], 500);
5902        }
5903    }
5904
5905    public function delete_file($fileId): ResponseFactory|HttpResponse
5906    {
5907
5908        try {
5909
5910            $fileId = addslashes((string) $fileId);
5911            $file = TblFiles::where('file_id', $fileId)->first();
5912            $result = TblFiles::where('file_id', $fileId)->first();
5913
5914            if ($result) {
5915                TblFiles::where('file_id', $fileId)->delete();
5916                $path = storage_path('app/public/uploads/'.$result->filename);
5917                Storage::disk('public')->delete('uploads/'.$result->filename);
5918
5919                if (! config('services.sendgrid.staging')) {
5920                    Storage::disk('s3')->delete('uploads/'.$result->filename);
5921                }
5922            }
5923
5924            $fileCount = TblFiles::where('quotation_id', $file->quotation_id)->count();
5925            $data = [];
5926            if ($fileCount > 0) {
5927                $data['has_attachment'] = 1;
5928            } else {
5929                $data['has_attachment'] = 0;
5930            }
5931
5932            TblQuotations::where('quote_id', $file->quote_id)->update($data);
5933
5934            $this->addUpdateLog($file->quote_id, 'user', 'delete_attachment', $file->filename, null, 4);
5935
5936            return response(['message' => 'OK']);
5937
5938        } catch (\Exception $e) {
5939            report(AppException::fromException($e, 'DELETE_FILE_EXCEPTION'));
5940
5941            return response(['message' => 'KO', 'error' => $e->getMessage()]);
5942        }
5943    }
5944
5945    public function send_email_to_client(Request $request): ResponseFactory|HttpResponse
5946    {
5947
5948        ini_set('max_execution_time', 0);
5949
5950        try {
5951
5952            $data = $request->all();
5953
5954            // $id = addslashes($data['id']);
5955            $userId = addslashes((string) $data['user_id']);
5956            $companyId = addslashes((string) $data['company_id']);
5957
5958            $isResend = null;
5959
5960            if (isset($data['is_resend'])) {
5961                $isResend = 1;
5962                unset($data['is_resend']);
5963            }
5964
5965            $sendChannel = isset($data['send_channel']) && $data['send_channel'] === 'whatsapp' ? 'whatsapp' : 'email';
5966
5967            $where = '';
5968            $emailTemplateId = $data['email_template_id'];
5969            unset($data['email_template_id']);
5970
5971            $emailTemplates = [];
5972
5973            if (isset($data['html'])) {
5974                $emailTemplates = array_column($data['html'], null, 'id');
5975            }
5976
5977            $emailTemplate = TblEmailConfiguration::whereIn('id', $emailTemplateId)->get()->keyBy('company_id');
5978
5979            $emailCompany = [];
5980
5981            $result = [];
5982
5983            $result = [];
5984
5985            if ($companyId == 0) {
5986                $indexes = $emailTemplate->keys();
5987
5988                $emailCompany = TblCompanies::whereIn('company_id', $indexes)->get()->keyBy('company_id');
5989                $limit = 21;
5990
5991                foreach ($emailCompany as $item) {
5992                    $r = new Request([
5993                        'filterModel' => $data['filterModel'],
5994                        'sortModel' => $data['sortModel'],
5995                        'start' => 0,
5996                        'end' => 999999999,
5997                        'company_id' => $data['company_id'],
5998                        'user_id' => $data['user_id'],
5999                        'ids' => $data['ids'],
6000                        'searchText' => $data['searchText'],
6001                        'ids_not_in' => $data['ids_not_in'],
6002                        // Pull the data from the Orders table source. When not
6003                        // resending, scope to the send-to-client bucket (status 11
6004                        // + valid email + budget type) so we only fetch eligible
6005                        // rows; on resend, fetch the selection unfiltered and let
6006                        // the loop below decide.
6007                        'is_send_to_client' => $isResend == 1 ? 0 : 1,
6008                    ]);
6009
6010                    $listQuotations = $this->list_orders_table($r);
6011                    $d = $listQuotations->original['data'];
6012
6013                    if ($item->limit_send != null) {
6014                        $limit = $item->limit_send;
6015                    }
6016                    $l = 0;
6017                    for ($i = 0; $i < count($d); $i++) {
6018                        $channelOk = $sendChannel === 'whatsapp'
6019                            ? ! empty($d[$i]->phone_number)
6020                            : ($d[$i]->email != null && ! $this->isBlacklistedEmail($d[$i]->email));
6021
6022                        if ($channelOk
6023                            && $d[$i]->budget_type_id != null
6024                            && ($d[$i]->budget_status_id == 11 || $isResend == 1)
6025                            && $item->company_id == $d[$i]->company_id
6026                        ) {
6027                            if ($l == $limit) {
6028                                break;
6029                            }
6030                            array_push($result, $d[$i]);
6031                            $l++;
6032                        } elseif ($sendChannel === 'email' && $d[$i]->email != null && $this->isBlacklistedEmail($d[$i]->email)) {
6033                            TblQuotations::where('id', $d[$i]->id)->update(['budget_status_id' => 22]);
6034                        }
6035                    }
6036                }
6037            } else {
6038
6039                $emailCompany = TblCompanies::where('company_id', $companyId)->get()->keyBy('company_id');
6040
6041                $limit = 21;
6042
6043                if ($emailCompany[$companyId]->limit_send != null) {
6044                    $limit = $emailCompany[$companyId]->limit_send;
6045                }
6046
6047                $r = new Request([
6048                    'filterModel' => $data['filterModel'],
6049                    'sortModel' => $data['sortModel'],
6050                    'start' => 0,
6051                    'end' => 999999999,
6052                    'company_id' => $data['company_id'],
6053                    'user_id' => $data['user_id'],
6054                    'ids' => $data['ids'],
6055                    'searchText' => $data['searchText'],
6056                    'ids_not_in' => $data['ids_not_in'],
6057                    // Orders-table data source; scope to the send-to-client
6058                    // bucket unless resending (then fetch the selection as-is).
6059                    'is_send_to_client' => $isResend == 1 ? 0 : 1,
6060                ]);
6061
6062                $listQuotations = $this->list_orders_table($r);
6063                $d = $listQuotations->original['data'];
6064                $l = 0;
6065                for ($i = 0; $i < count($d); $i++) {
6066                    $channelOk = $sendChannel === 'whatsapp'
6067                        ? ! empty($d[$i]->phone_number)
6068                        : ($d[$i]->email != null && ! $this->isBlacklistedEmail($d[$i]->email));
6069
6070                    if ($channelOk
6071                        && ($d[$i]->budget_status_id == 11 || $isResend == 1)
6072                    ) {
6073                        if ($l == $limit) {
6074                            break;
6075                        }
6076                        array_push($result, $d[$i]);
6077                        $l++;
6078                    } elseif ($sendChannel === 'email' && $d[$i]->email != null && $this->isBlacklistedEmail($d[$i]->email)) {
6079                        // FIRE-864 follow-up (04/05): pre-fix this branch wrote
6080                        // 21 (Fallo envio - correo erroneo) for blacklisted
6081                        // emails caught BEFORE Titan ever called SendGrid. The
6082                        // bucket policy says 21 = "Titan tried, SendGrid
6083                        // rejected" — so the row's helper text claims a
6084                        // SendGrid response that never existed (no x_status,
6085                        // no x_message_id). Quote 146914 is the canonical
6086                        // example. The multi-company branch ~40 lines up
6087                        // correctly writes 22; this single-company branch was
6088                        // the typo.
6089                        TblQuotations::where('id', $d[$i]->id)->update(['budget_status_id' => 22]);
6090                    }
6091                }
6092            }
6093
6094            if (count($result) == 0) {
6095                return response(['message' => 'OK', $d ?? []]);
6096            }
6097
6098            $user = TblUsers::where('id', $userId)->first();
6099            $error = false;
6100
6101            $availableParameters = [
6102                'quote_id',
6103                'company_id',
6104                'client',
6105                'client_type',
6106                'phone_number',
6107                'email',
6108                'issue_date',
6109                'request_date',
6110                'duration',
6111                'invoice_number',
6112                'type',
6113                'acceptance_date',
6114                'status',
6115                'source',
6116                'amount',
6117                'reason_for_not_following_up',
6118                'last_follow_up_date',
6119                'last_follow_up_comment',
6120                'reason_for_rejection_id',
6121                'reason_for_rejection',
6122                'commercial',
6123                'created_at',
6124                'created_by',
6125                'updated_at',
6126                'updated_by',
6127            ];
6128
6129            $dateParameters = [
6130                'issue_date',
6131                'request_date',
6132                'acceptance_date',
6133                'last_follow_up_date',
6134                'created_at',
6135                'updated_at',
6136            ];
6137
6138            if ($this->locale == 'es') {
6139                setlocale(LC_ALL, 'es_ES', 'Spanish_Spain', 'Spanish');
6140                setlocale(LC_ALL, 'es_ES', 'Spanish_Spain', 'Spanish');
6141            }
6142
6143            $sentItems = [];
6144
6145            for ($i = 0; $i < count($result); $i++) {
6146
6147                $body = $emailTemplate[$result[$i]->company_id]->html;
6148                if (isset($data['html']) && ! empty($data['html'])) {
6149                    $body = $emailTemplates[$result[$i]->company_id]['html'];
6150                }
6151                $subject = $emailTemplate[$result[$i]->company_id]->subject;
6152                $commercialUser = $result[$i]->commercial;
6153
6154                if ($sendChannel === 'whatsapp') {
6155                    if (empty($result[$i]->phone_number)) {
6156                        continue;
6157                    }
6158
6159                    preg_match_all('/{{(.*?)}}/', (string) $body, $waMatches);
6160                    foreach ($waMatches[1] as $parameter) {
6161                        if (in_array($parameter, $dateParameters) && $result[$i]->{$parameter}) {
6162                            $result[$i]->{$parameter} = iconv('ISO-8859-2', 'UTF-8', strftime('%A, %B %d, %Y', strtotime((string) $result[$i]->{$parameter})));
6163                        }
6164                        if (in_array($parameter, $availableParameters)) {
6165                            $body = str_replace('{{'.$parameter.'}}', $result[$i]->{$parameter}, $body);
6166                        }
6167                    }
6168
6169                    $wa = app(WhatsAppService::class)->send($result[$i]->phone_number, $body);
6170
6171                    $comment = 'Se ha enviado la orden por WhatsApp manualmente al cliente el '.date('Y-m-d H:i:s').' por el usuario '.$user->name;
6172                    $result[$i]->last_follow_up_comment = $result[$i]->last_follow_up_comment."\n".$comment;
6173
6174                    TblQuotations::where('id', $result[$i]->id)->update([
6175                        'last_follow_up_comment' => $result[$i]->last_follow_up_comment,
6176                        'budget_status_id' => 2,
6177                        'wa_message_id' => $wa['sid'],
6178                        'wa_status' => $wa['ok'] ? ($wa['status'] ?: 'Processing') : ('Error: '.substr((string) $wa['error'], 0, 60)),
6179                        'updated_by' => $user->name,
6180                        'updated_at' => date('Y-m-d H:i:s'),
6181                    ]);
6182
6183                    if ($wa['ok']) {
6184                        $sentItems[$result[$i]->company_id][] = $result[$i]->id;
6185                    }
6186
6187                    continue;
6188                }
6189
6190                if ($result[$i]->email != null) {
6191
6192                    if (config('services.sendgrid.staging')) {
6193                        $toEmail = $user->email;
6194                    } else {
6195                        $toEmail = $result[$i]->email;
6196                    }
6197
6198                    preg_match_all('/{{(.*?)}}/', (string) $body, $matches);
6199
6200                    $parameters = $matches[1];
6201
6202                    foreach ($parameters as $parameter) {
6203
6204                        if (in_array($parameter, $dateParameters)) {
6205                            if ($result[$i]->{$parameter}) {
6206                                $result[$i]->{$parameter} = iconv('ISO-8859-2', 'UTF-8', strftime('%A, %B %d, %Y', strtotime((string) $result[$i]->{$parameter})));
6207                            }
6208                        }
6209
6210                        if (in_array($parameter, $availableParameters)) {
6211                            $body = str_replace('{{'.$parameter.'}}', $result[$i]->{$parameter}, $body);
6212                        }
6213                    }
6214
6215                    preg_match_all('/{{(.*?)}}/', (string) $subject, $matches);
6216
6217                    $parameters = $matches[1];
6218
6219                    foreach ($parameters as $parameter) {
6220
6221                        if (in_array($parameter, $dateParameters)) {
6222                            if ($result[$i]->{$parameter}) {
6223                                $result[$i]->{$parameter} = iconv('ISO-8859-2', 'UTF-8', strftime('%A, %B %d, %Y', strtotime((string) $result[$i]->{$parameter})));
6224                            }
6225                        }
6226
6227                        if (in_array($parameter, $availableParameters)) {
6228                            $subject = str_replace('{{'.$parameter.'}}', $result[$i]->{$parameter}, $subject);
6229                        }
6230                    }
6231
6232                    $email = new Mail;
6233                    $templateFiles = TblEmailFiles::where('email_template_id', $emailTemplate[$result[$i]->company_id]->id)->orderBy('order', 'asc')->get();
6234
6235                    foreach ($templateFiles as $item) {
6236                        $f = storage_path('app/public/uploads/'.$item->filename);
6237
6238                        if (file_exists($f)) {
6239                            $imgpath = file_get_contents($f);
6240                            $base64 = 'data:image/png;base64,'.base64_encode($imgpath);
6241                            $mimeType = mime_content_type($f);
6242
6243                            $email->addAttachment(
6244                                $imgpath,
6245                                $mimeType,
6246                                str_replace(' ', '', $item->original_name),
6247                                'inline',
6248                                str_replace(' ', '', $item->original_name),
6249                            );
6250
6251                            $body .= "<img src='cid:{$item->original_name}' style='height: 45px; padding-right: 6px' />";
6252                        } else {
6253                            Log::channel('email_failed_log')->error('File not found: '.$f);
6254                        }
6255                    }
6256
6257                    $html = '<!DOCTYPE html>';
6258                    $html .= '<html>';
6259                    $html .= '<head>';
6260                    $html .= '<meta charset="UTF-8">';
6261                    $html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
6262                    $html .= '</head>';
6263                    $html .= '<body>';
6264                    $html .= $body;
6265                    $html .= '</body>';
6266                    $html .= '</html>';
6267
6268                    if ($toEmail != null) {
6269
6270                        $toEmail = explode(',', (string) $toEmail);
6271                        $toEmail = array_map(trim(...), $toEmail);
6272
6273                        $companyEmail = null;
6274
6275                        $queryUsers = "SELECT sender_email AS from_email, `name` AS from_name FROM tbl_users WHERE sender_enabled = 1 AND response_id IS NOT NULL AND verified = 1 AND `name` = '{$commercialUser}'";
6276                        $commercialEmail = DB::select($queryUsers);
6277
6278                        if (count($commercialEmail) > 0) {
6279                            $companyEmail = $commercialEmail[0];
6280                        } else {
6281                            if ($emailTemplate[$result[$i]->company_id]->from_id != null) {
6282                                $companyEmail = TblCompanyEmails::where('id', $emailTemplate[$result[$i]->company_id]->from_id)->first();
6283                            } else {
6284                                $companyEmail = TblCompanyEmails::where('is_active', 1)->where('verified', 1)->where('company_id', $result[$i]->company_id)->first();
6285                            }
6286                        }
6287
6288                        if (! $companyEmail) {
6289                            return response(['message' => 'KO', 'error' => __('language.no_active_verified_sender')]);
6290                        }
6291
6292                        $ccBcc = TblCcBcc::where('company_id', $result[$i]->company_id)->get();
6293
6294                        $email->setFrom($companyEmail->from_email, $companyEmail->from_name);
6295                        $email->setSubject($subject);
6296
6297                        foreach ($toEmail as $clientEmail) {
6298                            $isValid = $this->isEmailValid($clientEmail);
6299                            if ($isValid) {
6300                                $email->addTo($clientEmail);
6301                            }
6302                        }
6303
6304                        if (! config('services.sendgrid.staging')) {
6305                            if (! in_array($user->email, $toEmail)) {
6306                                $email->addCc($user->email);
6307                            }
6308                            if (count($ccBcc) > 0) {
6309                                foreach ($ccBcc as $data) {
6310                                    if (! in_array($data->email, $toEmail) && $user->email != $data->email) {
6311                                        $email->addBcc($data->email);
6312                                    }
6313                                }
6314                            }
6315                        }
6316
6317                        $email->addContent('text/html', $html);
6318                        $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
6319
6320                        $files = TblFiles::where('quotation_id', $result[$i]->id)->where('is_internal', null)->get();
6321                        $requestSize = $this->calculateEmailRequestSize($email);
6322
6323                        foreach ($files as $key => $value) {
6324                            $fileContent = null;
6325                            $fileSize = 0;
6326                            $fileName = null;
6327
6328                            if (Storage::disk('s3')->exists('uploads/'.$files[$key]->filename)) {
6329                                $fileContent = Storage::disk('s3')->get('uploads/'.$files[$key]->filename);
6330                                $fileSize = (int) ceil(strlen((string) $fileContent) / 1048576);
6331                                $fileName = $files[$key]->original_name;
6332                            } elseif ($files[$key]->file) {
6333                                $fileContent = $files[$key]->file;
6334
6335                                if (is_string($fileContent) && base64_decode($fileContent, true)) {
6336                                    $fileContent = base64_decode($fileContent);
6337                                }
6338
6339                                $fileSize = strlen((string) $fileContent) / 1048576;
6340                                $fileSize = (int) ceil($fileSize);
6341                                $fileName = $files[$key]->original_name ?: $files[$key]->getAttribute('file_name') ?: 'file';
6342                            }
6343
6344                            if ($fileContent && $fileSize > 0) {
6345                                if ($requestSize + $fileSize < 25) {
6346                                    $attachment = new Attachment;
6347                                    $attachment->setFilename($fileName);
6348                                    $attachment->setDisposition('attachment');
6349
6350                                    $attachment->setContent(base64_encode((string) $fileContent));
6351
6352                                    $email->addAttachment($attachment);
6353                                    $requestSize = $this->calculateEmailRequestSize($email);
6354                                }
6355                            }
6356                        }
6357
6358                        $sentWithBackup = false;
6359
6360                        try {
6361                            $response = $sendgrid->send($email);
6362                            SendgridLogger::log($email, $response);
6363                        } catch (\Throwable $sendException) {
6364                            SendgridLogger::logException($email, $sendException);
6365                            throw $sendException;
6366                        }
6367
6368                        if (config('services.sendgrid.block') === true) {
6369
6370                            Log::warning("SendGrid failed with status {$response->statusCode()}. Initiating Failover...");
6371                            try {
6372                                \Illuminate\Support\Facades\Mail::mailer('resend')->send([], [], function ($message) use ($html, $companyEmail, $toEmail, $subject, $files, $user, $ccBcc): void {
6373                                    $message->from($companyEmail->from_email, $companyEmail->from_name)
6374                                        ->to($toEmail)
6375                                        ->subject($subject);
6376
6377                                    $message->html($html);
6378
6379                                    $message->cc($user->email);
6380
6381                                    foreach ($ccBcc as $bcc) {
6382                                        $message->bcc($bcc->email);
6383                                    }
6384
6385                                    foreach ($files as $file) {
6386                                        if (Storage::disk('s3')->exists('uploads/'.$file->filename)) {
6387                                            $content = Storage::disk('s3')->get('uploads/'.$file->filename);
6388                                            $message->attachData($content, $file->original_name);
6389                                        } elseif ($file->file) {
6390                                            $content = $file->file;
6391                                            if (is_string($content) && base64_decode($content, true)) {
6392                                                $content = base64_decode($content);
6393                                            }
6394                                            $message->attachData($content, $file->original_name ?: 'archivo.pdf');
6395                                        }
6396                                    }
6397                                });
6398
6399                                $sentWithBackup = true;
6400                            } catch (\Exception $backupException) {
6401                                $sentWithBackup = false;
6402                                Log::error('Backup server also failed: '.$backupException->getMessage());
6403                            }
6404
6405                            if (! $sentWithBackup) {
6406                                throw new \Exception("SendGrid failed with status {$response->statusCode()}. Backup server also failed: ".$backupException->getMessage());
6407                            }
6408
6409                        }
6410
6411                        if (($response->statusCode() == 202 || $sentWithBackup) && ! config('services.sendgrid.block')) {
6412                            $messageId = null;
6413
6414                            if ($response->headers()) {
6415                                foreach ($response->headers() as $header) {
6416                                    if (str_starts_with(strtolower((string) $header), 'x-message-id:')) {
6417                                        $messageId = trim(substr((string) $header, strpos((string) $header, ':') + 1));
6418                                        break;
6419                                    }
6420                                }
6421                            }
6422
6423                            $sentItems[$result[$i]->company_id][] = $result[$i]->id;
6424
6425                            $comment = 'Se ha enviado la orden por correo electrónico manualmente al cliente el '.date('Y-m-d H:i:s').' por el usuario '.$user->name;
6426                            $result[$i]->last_follow_up_comment = $result[$i]->last_follow_up_comment."\n".$comment;
6427
6428                            TblQuotations::where('id', $result[$i]->id)->update(
6429                                [
6430                                    'last_follow_up_comment' => $result[$i]->last_follow_up_comment,
6431                                    'budget_status_id' => 2,
6432                                    'x_message_id' => $messageId,
6433                                    'x_status' => 'Processing',
6434                                    'updated_by' => $user->name,
6435                                    'updated_at' => date('Y-m-d H:i:s'),
6436                                ]
6437                            );
6438
6439                            $jsonBody = [];
6440
6441                            foreach ($toEmail as $clientEmail) {
6442                                $isValid = $this->isEmailValid($clientEmail);
6443                                $eventStatus = 'processed';
6444                                $eventResponse = '';
6445                                if (! $isValid) {
6446                                    $eventStatus = 'invalid';
6447                                    $eventResponse = 'Invalid email address';
6448                                }
6449
6450                                array_push(
6451                                    $jsonBody,
6452                                    [
6453                                        'email' => $clientEmail,
6454                                        'event' => $eventStatus,
6455                                        'sg_message_id' => $messageId,
6456                                        'smtp-id' => $messageId,
6457                                        'timestamp' => strtotime(date('Y-m-d H:i:s')),
6458                                        'response' => $eventResponse,
6459                                    ]
6460                                );
6461                            }
6462
6463                            TblSendgridWebhook::create(
6464                                [
6465                                    'quotation_id' => $result[$i]->id,
6466                                    'type' => 'sendToClient',
6467                                    'json_body' => json_encode($jsonBody),
6468                                    'x_message_id' => $messageId,
6469                                ]
6470                            );
6471
6472                            Log::channel('email_log')->info("[RS-{$requestSize}] ID:".$result[$i]->id.' - EMAIL MANUALLY SENT');
6473                            $this->addUpdateLog($result[$i]->id, $userId, 'send_email_to_client', null, null, 5);
6474                        } else {
6475                            $error = true;
6476                            Log::channel('email_failed_log')->error("[RS-{$requestSize}] ID:".$result[$i]->id.' - '.$response->body());
6477                        }
6478                    }
6479                }
6480            }
6481
6482            $this->update_commercial_numbers($companyId);
6483
6484            // FIRE-1145: was Cache::flush() — send_email_to_client affects quotations list (email status).
6485            ResultCache::forgetDomain('quotations');
6486
6487            foreach ($sentItems as $k => $v) {
6488                $sentItems[$k] = [
6489                    'total' => count($sentItems[$k]),
6490                    'company' => $emailCompany[$k]->name,
6491                    'limit' => $emailCompany[$k]->limit_send,
6492                ];
6493            }
6494
6495            $totalSent = array_values($sentItems);
6496
6497            return response(['message' => 'OK', 'data' => $totalSent]);
6498
6499        } catch (\Exception $e) {
6500            report(AppException::fromException($e, 'SEND_EMAIL_TO_CLIENT_EXCEPTION'));
6501            Log::channel('email_failed_log')->error($e->getMessage());
6502
6503            return response(['message' => 'KO', 'error' => $e->getMessage()]);
6504        }
6505
6506    }
6507
6508    public function send_email_follow_ups(Request $request, $automaticSendLimit = null): ResponseFactory|HttpResponse
6509    {
6510
6511        $currentQuotationId = null;
6512        ini_set('max_execution_time', 0);
6513
6514        try {
6515
6516            $data = $request->all();
6517
6518            $startedAt = date('Y-m-d H:i:s');
6519            $toEmail = '';
6520            $userId = addslashes((string) $data['user_id']);
6521            $companyId = addslashes((string) $data['company_id']);
6522            $emailTemplateId = $data['email_template_id'];
6523            unset($data['email_template_id']);
6524
6525            $sendChannel = isset($data['send_channel']) && $data['send_channel'] === 'whatsapp' ? 'whatsapp' : 'email';
6526
6527            $emailTemplates = [];
6528
6529            if (isset($data['html']) && ! empty($data['html'])) {
6530                $emailTemplates = array_column($data['html'], null, 'id');
6531            }
6532
6533            $emailTemplate = TblEmailConfiguration::whereIn('id', $emailTemplateId)->get()->keyBy('company_id');
6534
6535            $limit = 0;
6536
6537            $blockedDomains = [];
6538            $workingDays = 10;
6539            $limitReminderEmails = 3;
6540            $emailCompany = [];
6541
6542            $result = [];
6543
6544            if ($companyId == 0) {
6545                $indexes = $emailTemplate->keys();
6546
6547                $emailCompany = TblCompanies::whereIn('company_id', $indexes)->get()->keyBy('company_id');
6548
6549                foreach ($emailCompany as $item) {
6550                    $limit = 20;
6551
6552                    $r = new Request([
6553                        'filterModel' => $data['filterModel'],
6554                        'sortModel' => $data['sortModel'],
6555                        'start' => 0,
6556                        'end' => 999999999,
6557                        'company_id' => $data['company_id'],
6558                        'user_id' => $data['user_id'],
6559                        'ids' => $data['ids'],
6560                        'searchText' => $data['searchText'],
6561                        'ids_not_in' => $data['ids_not_in'],
6562                        // Orders-table data source, scoped to the follow-up
6563                        // bucket (replaces list_quotations' last_follow_up_date=1).
6564                        'is_send_follow_up' => 1,
6565                    ]);
6566
6567                    if ($item->limit_send != null) {
6568                        $limit = $item->limit_send;
6569                    }
6570
6571                    $listQuotations = $this->list_orders_table($r);
6572                    $d = $listQuotations->original['data'];
6573                    if ($automaticSendLimit != null) {
6574                        $limit = $automaticSendLimit;
6575                    }
6576
6577                    $l = 0;
6578                    for ($i = 0; $i < count($d); $i++) {
6579                        $channelOk = $sendChannel === 'whatsapp'
6580                            ? ! empty($d[$i]->phone_number)
6581                            : $d[$i]->email != null;
6582
6583                        if ($channelOk
6584                            && $d[$i]->budget_type_id != null
6585                            && $d[$i]->budget_status_id == 2
6586                            && $d[$i]->reason_for_not_following_up_id == null
6587                            && $d[$i]->total_sent < $limitReminderEmails
6588                            && $d[$i]->last_follow_up_date != null
6589                            && $d[$i]->last_follow_up_date < date('Y-m-d H:i:s')
6590                            && $d[$i]->last_follow_up_date > 0
6591                            && $item->company_id == $d[$i]->company_id
6592                        ) {
6593
6594                            if ($l == $limit) {
6595                                break;
6596                            }
6597                            array_push($result, $d[$i]);
6598                            $l++;
6599                        }
6600                    }
6601
6602                }
6603            } else {
6604
6605                $emailCompany = TblCompanies::where('company_id', $companyId)->get()->keyBy('company_id');
6606                $workingDays = $emailCompany[$companyId]->last_follow_up_date ?? 10;
6607                $limitReminderEmails = $emailCompany[$companyId]->limit_reminder_emails ?? 3;
6608
6609                if ($emailCompany[$companyId]->limit_send != null) {
6610                    $limit = $emailCompany[$companyId]->limit_send;
6611                }
6612
6613                $r = new Request([
6614                    'filterModel' => $data['filterModel'],
6615                    'sortModel' => $data['sortModel'],
6616                    'start' => 0,
6617                    'end' => 999999999,
6618                    'company_id' => $data['company_id'],
6619                    'user_id' => $data['user_id'],
6620                    'ids' => $data['ids'],
6621                    'searchText' => $data['searchText'],
6622                    'ids_not_in' => $data['ids_not_in'],
6623                    // Orders-table data source, scoped to the follow-up bucket
6624                    // (replaces list_quotations' last_follow_up_date=1).
6625                    'is_send_follow_up' => 1,
6626                ]);
6627
6628                $listQuotations = $this->list_orders_table($r);
6629                $d = $listQuotations->original['data'];
6630
6631                if ($automaticSendLimit != null) {
6632                    $limit = $automaticSendLimit;
6633                }
6634
6635                $l = 0;
6636                for ($i = 0; $i < count($d); $i++) {
6637                    $channelOk = $sendChannel === 'whatsapp'
6638                        ? ! empty($d[$i]->phone_number)
6639                        : $d[$i]->email != null;
6640
6641                    if ($channelOk
6642                        && $d[$i]->budget_status_id == 2
6643                        && $d[$i]->reason_for_not_following_up_id == null
6644                        && $d[$i]->total_sent < $limitReminderEmails
6645                        && $d[$i]->last_follow_up_date != null
6646                        && $d[$i]->last_follow_up_date < date('Y-m-d H:i:s')
6647                        && $d[$i]->last_follow_up_date > 0
6648                    ) {
6649                        if ($l == $limit) {
6650                            break;
6651                        }
6652                        array_push($result, $d[$i]);
6653                        $l++;
6654                    }
6655                }
6656            }
6657
6658            if (count($result) == 0) {
6659                return response(['message' => 'OK']);
6660            }
6661
6662            $user = TblUsers::where('id', $userId)->first();
6663            $error = false;
6664
6665            $sentBy = $user->name;
6666
6667            $availableParameters = [
6668                'quote_id',
6669                'company_id',
6670                'client',
6671                'client_type',
6672                'phone_number',
6673                'email',
6674                'issue_date',
6675                'request_date',
6676                'duration',
6677                'invoice_number',
6678                'type',
6679                'acceptance_date',
6680                'status',
6681                'source',
6682                'amount',
6683                'reason_for_not_following_up',
6684                'last_follow_up_date',
6685                'last_follow_up_comment',
6686                'reason_for_rejection_id',
6687                'reason_for_rejection',
6688                'commercial',
6689                'created_at',
6690                'created_by',
6691                'updated_at',
6692                'updated_by',
6693            ];
6694
6695            $dateParameters = [
6696                'issue_date',
6697                'request_date',
6698                'acceptance_date',
6699                'last_follow_up_date',
6700                'created_at',
6701                'updated_at',
6702            ];
6703
6704            if ($this->locale == 'es') {
6705                setlocale(LC_ALL, 'es_ES', 'Spanish_Spain', 'Spanish');
6706            }
6707
6708            $totalSent = 0;
6709            $totalSentIds = [];
6710            $totalFailedIds = [];
6711            $totalErrorIds = [];
6712            $currentQuotationId = null;
6713
6714            $sentItems = [];
6715
6716            for ($i = 0; $i < count($result); $i++) {
6717                $budgetTypeId = $result[$i]->budget_type_id;
6718                $currentQuotationId = $result[$i]->quote_id;
6719                $body = $emailTemplate[$result[$i]->company_id]->html;
6720                if (isset($data['html']) && ! empty($data['html'])) {
6721                    $body = $emailTemplates[$result[$i]->company_id]['html'];
6722                }
6723                $subject = $emailTemplate[$result[$i]->company_id]->subject;
6724                $commercialUser = $result[$i]->commercial;
6725
6726                $blockedDomains = TblBlockedDomains::where('company_id', $result[$i]->company_id)->pluck('domain')->toArray();
6727
6728                if (config('services.sendgrid.staging')) {
6729                    $toEmail = $user->email;
6730                } else {
6731                    $toEmail = $result[$i]->email;
6732                }
6733
6734                preg_match_all('/{{(.*?)}}/', (string) $body, $matches);
6735
6736                $parameters = $matches[1];
6737
6738                foreach ($parameters as $parameter) {
6739
6740                    if (in_array($parameter, $dateParameters)) {
6741                        if ($result[$i]->{$parameter}) {
6742                            $result[$i]->{$parameter} = iconv('ISO-8859-2', 'UTF-8', strftime('%A, %B %d, %Y', strtotime((string) $result[$i]->{$parameter})));
6743                        }
6744                    }
6745
6746                    if (in_array($parameter, $availableParameters)) {
6747                        $body = str_replace('{{'.$parameter.'}}', $result[$i]->{$parameter}, $body);
6748                    }
6749                }
6750
6751                preg_match_all('/{{(.*?)}}/', (string) $subject, $matches);
6752
6753                $parameters = $matches[1];
6754
6755                foreach ($parameters as $parameter) {
6756
6757                    if (in_array($parameter, $dateParameters)) {
6758                        if ($result[$i]->{$parameter}) {
6759                            $result[$i]->{$parameter} = iconv('ISO-8859-2', 'UTF-8', strftime('%A, %B %d, %Y', strtotime((string) $result[$i]->{$parameter})));
6760                        }
6761                    }
6762
6763                    if (in_array($parameter, $availableParameters)) {
6764                        $subject = str_replace('{{'.$parameter.'}}', $result[$i]->{$parameter}, $subject);
6765                    }
6766                }
6767
6768                $email = new Mail;
6769
6770                $templateFiles = TblEmailFiles::where('email_template_id', $emailTemplate[$result[$i]->company_id]->id)->orderBy('order', 'asc')->get();
6771
6772                foreach ($templateFiles as $item) {
6773                    $f = storage_path('app/public/uploads/'.$item->filename);
6774
6775                    if (file_exists($f)) {
6776                        $imgpath = file_get_contents($f);
6777                        $base64 = 'data:image/png;base64,'.base64_encode($imgpath);
6778                        $mimeType = mime_content_type($f);
6779
6780                        $email->addAttachment(
6781                            $imgpath,
6782                            $mimeType,
6783                            str_replace(' ', '', $item->original_name),
6784                            'inline',
6785                            str_replace(' ', '', $item->original_name),
6786                        );
6787
6788                        $body .= "<img src='cid:{$item->original_name}' style='height: 45px; padding-right: 6px' />";
6789                    } else {
6790                        Log::channel('email_failed_log')->error('File not found: '.$f);
6791                    }
6792                }
6793
6794                $html = '<!DOCTYPE html>';
6795                $html .= '<html>';
6796                $html .= '<head>';
6797                $html .= '<meta charset="UTF-8">';
6798                $html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
6799                $html .= '</head>';
6800                $html .= '<body>';
6801                $html .= $body;
6802                $html .= '</body>';
6803                $html .= '</html>';
6804
6805                if ($automaticSendLimit != null) {
6806                    $sentBy = 'System';
6807                }
6808
6809                if ($sendChannel === 'whatsapp') {
6810                    if (empty($result[$i]->phone_number)) {
6811                        continue;
6812                    }
6813
6814                    $wa = app(WhatsAppService::class)->send($result[$i]->phone_number, $body);
6815
6816                    if (! $wa['ok']) {
6817                        $error = true;
6818                        $totalErrorIds[] = $result[$i]->id;
6819                        Log::channel('email_failed_log')->error('ID:'.$result[$i]->id.' - WhatsApp - '.$wa['error']);
6820
6821                        continue;
6822                    }
6823
6824                    $lastFollowUp = TblLastFollowUpDate::where('company_id', $companyId)
6825                        ->where('budget_type_id', $budgetTypeId)
6826                        ->first();
6827                    $workingDaysN = $workingDays;
6828                    if ($companyId == 0 && isset($emailCompany[$result[$i]->company_id])) {
6829                        $workingDaysN = $emailCompany[$result[$i]->company_id]->last_follow_up_date ?? 10;
6830                    }
6831                    if ($lastFollowUp != null && $lastFollowUp->last_follow_up_date) {
6832                        $workingDaysN = $lastFollowUp->last_follow_up_date;
6833                    }
6834
6835                    $comment = 'WhatsApp automático enviado el '.date('Y-m-d H:i:s').' por usuario '.$sentBy;
6836                    $result[$i]->last_follow_up_comment = $result[$i]->last_follow_up_comment."\n".$comment;
6837                    $result[$i]->last_follow_up_date = date('Y-m-d H:i:s', strtotime("{$workingDaysN} weekdays"));
6838                    $totalSentQ = $result[$i]->total_sent + 1;
6839
6840                    if ($totalSentQ >= $limitReminderEmails) {
6841                        $result[$i]->reason_for_not_following_up_id = 3;
6842                        $result[$i]->last_follow_up_date = null;
6843                    }
6844
6845                    TblQuotations::where('id', $result[$i]->id)->update([
6846                        'last_follow_up_comment' => $result[$i]->last_follow_up_comment,
6847                        'last_follow_up_date' => $result[$i]->last_follow_up_date,
6848                        'total_sent' => $totalSentQ,
6849                        'wa_y_message_id' => $wa['sid'],
6850                        'wa_y_status' => $wa['status'] ?: 'Processing',
6851                        'reason_for_not_following_up_id' => $result[$i]->reason_for_not_following_up_id,
6852                        'updated_by' => $sentBy,
6853                        'updated_at' => date('Y-m-d H:i:s'),
6854                    ]);
6855
6856                    $totalSent++;
6857                    $totalSentIds[] = $result[$i]->id;
6858                    $sentItems[$result[$i]->company_id][] = $result[$i]->id;
6859                    $this->addUpdateLog($result[$i]->id, $userId, 'send_email_follow_up', null, null, 5);
6860
6861                    continue;
6862                }
6863
6864                if ($toEmail != null) {
6865
6866                    $toEmail = explode(',', (string) $toEmail);
6867                    $toEmail = array_map(trim(...), $toEmail);
6868
6869                    $companyEmail = null;
6870
6871                    $queryUsers = "SELECT sender_email AS from_email, `name` AS from_name FROM tbl_users WHERE sender_enabled = 1 AND response_id IS NOT NULL AND verified = 1 AND `name` = '{$commercialUser}'";
6872                    $commercialEmail = DB::select($queryUsers);
6873
6874                    if (count($commercialEmail) > 0) {
6875                        $companyEmail = $commercialEmail[0];
6876                    } else {
6877                        if ($emailTemplate[$result[$i]->company_id]->from_id != null) {
6878                            $companyEmail = TblCompanyEmails::where('id', $emailTemplate[$result[$i]->company_id]->from_id)->first();
6879                        } else {
6880                            $companyEmail = TblCompanyEmails::where('is_active', 1)->where('verified', 1)->where('company_id', $result[$i]->company_id)->first();
6881                        }
6882                    }
6883
6884                    if (! $companyEmail) {
6885                        return response(['message' => 'KO', 'error' => __('language.no_active_verified_sender')]);
6886                    }
6887
6888                    $ccBcc = TblCcBcc::where('company_id', $result[$i]->company_id)->get();
6889
6890                    $email->setFrom($companyEmail->from_email, $companyEmail->from_name);
6891                    $email->setSubject($subject);
6892
6893                    $s = 0;
6894                    $addTo = [];
6895                    foreach ($toEmail as $clientEmail) {
6896                        $isValid = $this->isEmailValid($clientEmail);
6897                        if ($isValid) {
6898                            $domain = substr($clientEmail, strpos($clientEmail, '@') + 1);
6899
6900                            if (! in_array($domain, $blockedDomains)) {
6901                                $email->addTo($clientEmail);
6902                                array_push($addTo, $clientEmail);
6903                                $s++;
6904                            }
6905                        } else {
6906                            TblFollowUpLogs::create(
6907                                [
6908                                    'quotation_id' => $result[$i]->id,
6909                                    'email' => $clientEmail,
6910                                    'sent_by' => $sentBy,
6911                                    'status' => 'Invalid email',
6912                                ]
6913                            );
6914                        }
6915                    }
6916
6917                    if ($s == 0) {
6918                        array_push($totalFailedIds, $result[$i]->id);
6919                        Log::channel('email_failed_log')->error($s.'ID:'.$result[$i]->id.' - '.json_encode($toEmail));
6920
6921                        continue;
6922                    }
6923
6924                    if (! config('services.sendgrid.staging')) {
6925                        if (count($ccBcc) > 0) {
6926                            foreach ($ccBcc as $data) {
6927                                if (! in_array($data->email, $toEmail)) {
6928                                    $email->addBcc($data->email);
6929                                }
6930                            }
6931                        }
6932                    }
6933
6934                    $email->addContent('text/html', $html);
6935
6936                    $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
6937
6938                    $files = TblFiles::where('quotation_id', $result[$i]->id)->where('is_internal', null)->get();
6939                    $requestSize = $this->calculateEmailRequestSize($email);
6940
6941                    foreach ($files as $key => $value) {
6942                        if ($files[$key]->filename && Storage::disk('s3')->exists('uploads/'.$files[$key]->filename)) {
6943
6944                            $fileContent = Storage::disk('s3')->get('uploads/'.$files[$key]->filename);
6945                            $fileSize = strlen((string) $fileContent) / 1048576;
6946                            $fileSize = (int) ceil($fileSize);
6947
6948                            $originalName = $files[$key]->original_name ?: basename((string) $files[$key]->filename);
6949                        } elseif ($files[$key]->file) {
6950                            $fileContent = $files[$key]->file;
6951
6952                            if (is_string($fileContent) && base64_decode($fileContent, true)) {
6953                                $fileContent = base64_decode($fileContent);
6954                            }
6955
6956                            $fileSize = strlen((string) $fileContent) / 1048576;
6957                            $fileSize = (int) ceil($fileSize);
6958
6959                            $originalName = $files[$key]->original_name ?: ($files[$key]->getAttribute('file_name') ?: 'file');
6960                        } else {
6961                            continue;
6962                        }
6963
6964                        if ($fileSize > 0) {
6965                            if ($requestSize + $fileSize < 25) {
6966                                $attachment = new Attachment;
6967                                $attachment->setFilename($originalName);
6968                                $attachment->setDisposition('attachment');
6969
6970                                $attachment->setContent(base64_encode((string) $fileContent));
6971
6972                                if ($files[$key]->mime_type) {
6973                                    $attachment->setType($files[$key]->mime_type);
6974                                }
6975
6976                                $email->addAttachment($attachment);
6977                                $requestSize = $this->calculateEmailRequestSize($email);
6978                            } else {
6979                                Log::warning('File omitted due to size limit: '.$originalName);
6980                            }
6981                        }
6982                    }
6983
6984                    $sentWithBackup = false;
6985
6986                    try {
6987                        $response = $sendgrid->send($email);
6988                        SendgridLogger::log($email, $response);
6989                    } catch (\Throwable $sendException) {
6990                        SendgridLogger::logException($email, $sendException);
6991                        throw $sendException;
6992                    }
6993
6994                    if (config('services.sendgrid.block') === true) {
6995
6996                        Log::warning("SendGrid failed with status {$response->statusCode()}. Initiating Failover...");
6997                        try {
6998                            \Illuminate\Support\Facades\Mail::mailer('smtp_backup')->send([], [], function ($message) use ($html, $companyEmail, $toEmail, $subject, $files, $user, $ccBcc): void {
6999                                $message->from($companyEmail->from_email, $companyEmail->from_name)
7000                                    ->to($toEmail)
7001                                    ->subject($subject);
7002
7003                                $message->html($html);
7004
7005                                $message->cc($user->email);
7006
7007                                foreach ($ccBcc as $bcc) {
7008                                    $message->bcc($bcc->email);
7009                                }
7010
7011                                foreach ($files as $file) {
7012                                    if (Storage::disk('s3')->exists('uploads/'.$file->filename)) {
7013                                        $content = Storage::disk('s3')->get('uploads/'.$file->filename);
7014                                        $message->attachData($content, $file->original_name);
7015                                    } elseif ($file->file) {
7016                                        $content = $file->file;
7017                                        if (is_string($content) && base64_decode($content, true)) {
7018                                            $content = base64_decode($content);
7019                                        }
7020                                        $message->attachData($content, $file->original_name ?: 'archivo.pdf');
7021                                    }
7022                                }
7023                            });
7024
7025                            $sentWithBackup = true;
7026                        } catch (\Exception $backupException) {
7027                            $sentWithBackup = false;
7028                            Log::error('Backup server also failed: '.$backupException->getMessage());
7029                        }
7030
7031                        if (! $sentWithBackup) {
7032                            throw new \Exception("SendGrid failed with status {$response->statusCode()}. Backup server also failed: ".$backupException->getMessage());
7033                        }
7034
7035                    }
7036
7037                    if (($response->statusCode() == 202 || $sentWithBackup) && ! config('services.sendgrid.block')) {
7038
7039                        $messageId = null;
7040
7041                        foreach ($response->headers() as $header) {
7042                            if (str_starts_with(strtolower((string) $header), 'x-message-id:')) {
7043                                $messageId = trim(substr((string) $header, strpos((string) $header, ':') + 1));
7044                                break;
7045                            }
7046                        }
7047
7048                        $lastFollowUp = TblLastFollowUpDate::where('company_id', $companyId)->where('budget_type_id', $budgetTypeId)->first();
7049                        $workingDaysN = $workingDays;
7050
7051                        if ($companyId == 0) {
7052                            $workingDaysN = $emailCompany[$result[$i]->company_id]->last_follow_up_date ?? 10;
7053                        }
7054
7055                        if ($lastFollowUp != null) {
7056                            if ($lastFollowUp->last_follow_up_date) {
7057                                $workingDaysN = $lastFollowUp->last_follow_up_date;
7058                            }
7059                        }
7060
7061                        $comment = 'Email automático enviado el '.date('Y-m-d H:i:s').' por usuario '.$sentBy;
7062                        $result[$i]->last_follow_up_comment = $result[$i]->last_follow_up_comment."\n".$comment;
7063                        $date = strtotime("{$workingDaysN} weekdays");
7064                        $result[$i]->last_follow_up_date = date('Y-m-d H:i:s', $date);
7065                        $totalSentQ = $result[$i]->total_sent + 1;
7066
7067                        array_push($totalSentIds, $result[$i]->id);
7068                        $sentItems[$result[$i]->company_id][] = $result[$i]->id;
7069
7070                        foreach ($addTo as $addToEmail) {
7071                            TblFollowUpLogs::create(
7072                                [
7073                                    'quotation_id' => $result[$i]->id,
7074                                    'email' => $addToEmail,
7075                                    'sent_by' => $sentBy,
7076                                    'status' => 'OK',
7077                                ]
7078                            );
7079                            $this->addUpdateLog($result[$i]->id, $userId, 'send_email_follow_up', null, null, 5);
7080                        }
7081
7082                        if ($totalSentQ >= $limitReminderEmails) {
7083                            $result[$i]->reason_for_not_following_up_id = 3;
7084                            $result[$i]->last_follow_up_date = null;
7085                        }
7086
7087                        TblQuotations::where('id', $result[$i]->id)->update(
7088                            [
7089                                'last_follow_up_comment' => $result[$i]->last_follow_up_comment,
7090                                'last_follow_up_date' => $result[$i]->last_follow_up_date,
7091                                'total_sent' => $result[$i]->total_sent + 1,
7092                                'y_message_id' => $messageId,
7093                                'y_status' => 'Processing',
7094                                'reason_for_not_following_up_id' => $result[$i]->reason_for_not_following_up_id,
7095                                'updated_by' => $sentBy,
7096                                'updated_at' => date('Y-m-d H:i:s'),
7097                            ]
7098                        );
7099
7100                        $jsonBody = [];
7101
7102                        foreach ($toEmail as $clientEmail) {
7103                            $isValid = $this->isEmailValid($clientEmail);
7104                            $eventStatus = 'processed';
7105                            $eventResponse = '';
7106                            if (! $isValid) {
7107                                $eventStatus = 'invalid';
7108                                $eventResponse = 'Invalid email address';
7109                            }
7110
7111                            array_push(
7112                                $jsonBody,
7113                                [
7114                                    'email' => $clientEmail,
7115                                    'event' => $eventStatus,
7116                                    'sg_message_id' => $messageId,
7117                                    'smtp-id' => $messageId,
7118                                    'timestamp' => strtotime(date('Y-m-d H:i:s')),
7119                                    'response' => $eventResponse,
7120                                ]
7121                            );
7122                        }
7123
7124                        TblSendgridWebhook::create(
7125                            [
7126                                'quotation_id' => $result[$i]->id,
7127                                'type' => 'followUps',
7128                                'json_body' => json_encode($jsonBody),
7129                                'x_message_id' => $messageId,
7130                            ]
7131                        );
7132
7133                        Log::channel('email_log')->info("[RS-{$requestSize}] ID:".$result[$i]->id.' - EMAIL SENT');
7134                        $totalSent++;
7135                        $workingDays = 10;
7136                    } else {
7137                        $error = true;
7138                        array_push($totalErrorIds, $result[$i]->id);
7139                        Log::channel('email_failed_log')->error("[RS-{$requestSize}] ID:".$result[$i]->id.' - '.$response->body());
7140
7141                        foreach ($addTo as $addToEmail) {
7142                            TblFollowUpLogs::create(
7143                                [
7144                                    'quotation_id' => $result[$i]->id,
7145                                    'email' => $addToEmail,
7146                                    'sent_by' => $sentBy,
7147                                    'status' => $response->body(),
7148                                ]
7149                            );
7150                        }
7151                    }
7152
7153                    $body = '';
7154                    $subject = '';
7155                }
7156            }
7157
7158            // FIRE-1145: was Cache::flush() — send_email_follow_ups updates email status visible in list_quotations.
7159            ResultCache::forgetDomain('quotations');
7160
7161            Log::channel('send_email_follow_ups')->info(
7162                json_encode(
7163                    [
7164                        'success' => $totalSentIds,
7165                        'failed' => $totalFailedIds,
7166                        'error' => $totalErrorIds,
7167                    ]
7168                )
7169            );
7170
7171            $this->update_commercial_numbers($companyId);
7172
7173            if ($error) {
7174                return response(['message' => 'KO']);
7175            } else {
7176
7177                if ($automaticSendLimit != null) {
7178                    TblOrdersUpdateLogs::create(
7179                        [
7180                            'company_id' => $companyId,
7181                            'to_process' => 'Orders',
7182                            'status' => 'success',
7183                            'follow_ups_affected_rows' => $totalSent,
7184                            'processed_by' => $sentBy,
7185                            'started_at' => $startedAt,
7186                            'ended_at' => date('Y-m-d H:i:s'),
7187                        ]
7188                    );
7189                }
7190
7191                foreach ($sentItems as $k => $v) {
7192                    $sentItems[$k] = [
7193                        'total' => count($sentItems[$k]),
7194                        'company' => $emailCompany[$k]->name,
7195                        'limit' => $emailCompany[$k]->limit_send,
7196                    ];
7197                }
7198
7199                $totalSent = array_values($sentItems);
7200
7201                return response(['message' => 'OK', 'data' => $totalSent]);
7202            }
7203
7204        } catch (\Throwable $e) {
7205            report(AppException::fromException($e, 'SEND_EMAIL_FOLLOW_UPS_EXCEPTION'));
7206            Log::channel('email_failed_log')->error($e->getMessage());
7207
7208            return response(['message' => 'KO', 'error' => $e->getMessage(), 'quotation_id' => $currentQuotationId]);
7209        }
7210    }
7211
7212    public function create_sender_identity(Request $request): ResponseFactory|HttpResponse
7213    {
7214
7215        $data = $request->all();
7216        try {
7217
7218            $sData = $data;
7219            $companyId = $data['company_id'];
7220            $createdBy = $data['created_by'];
7221            unset($data['company_id']);
7222            unset($data['created_by']);
7223
7224            $sender = TblCompanyEmails::where('from_email', $data['from_email'])->where('verified', 1)->count();
7225
7226            if ($sender > 0) {
7227                TblCompanyEmails::create($sData);
7228
7229                return response(['message' => 'OK', 'data' => $data, 'is_verified' => 'yes']);
7230            }
7231
7232            $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
7233            $data['reply_to'] = $data['from_email'];
7234            $data['reply_to_name'] = $data['from_name'];
7235            $requestBody = $data;
7236            $error = false;
7237
7238            // @phpstan-ignore-next-line
7239            $response = $sendgrid->client->verified_senders()->post($requestBody);
7240
7241            if ($response->statusCode() == 201) {
7242                $x = json_decode((string) $response->body());
7243
7244                $data['company_id'] = $companyId;
7245                $data['created_by'] = $createdBy;
7246                $data['response_id'] = $x->id;
7247                TblCompanyEmails::create($data);
7248                Log::channel('email_log')->info('EMAIL: '.$data['from_email'].' - VERIFICATION SENT');
7249            } else {
7250                $error = true;
7251                Log::channel('email_log')->error('REQUEST BODY: - '.$response->body());
7252            }
7253
7254            $response = json_decode((string) $response->body());
7255
7256            if ($error) {
7257                if ($response->errors[0]->message == 'already exists' && $response->errors[0]->field == 'from_email') {
7258                    TblCompanyEmails::create($sData);
7259
7260                    return response(['message' => 'OK', 'data' => $data, 'is_verified' => 'yes']);
7261                }
7262
7263                $errMessage = $response->errors[0]->field.': '.$response->errors[0]->message;
7264
7265                return response(['message' => 'KO', 'error' => $errMessage]);
7266            } else {
7267                return response(['message' => 'OK', 'data' => $response, 'is_verified' => 'no']);
7268            }
7269
7270        } catch (\Exception $e) {
7271            report(AppException::fromException($e, 'CREATE_SENDER_IDENTITY_EXCEPTION'));
7272            Log::channel('email_log')->error('EMAIL:'.$data['from_email'].' - '.$e->getMessage());
7273
7274            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7275        }
7276    }
7277
7278    public function get_sender_identity($companyId): ResponseFactory|HttpResponse
7279    {
7280
7281        try {
7282
7283            $companyId = addslashes((string) $companyId);
7284
7285            $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
7286
7287            // @phpstan-ignore-next-line
7288            $response = $sendgrid->client->verified_senders()->get();
7289
7290            if ($response->statusCode() == 200) {
7291                $x = json_decode((string) $response->body())->results;
7292
7293                foreach ($x as $item) {
7294                    TblCompanyEmails::where('from_email', $item->from_email)->update([
7295                        'verified' => $item->verified,
7296                        'reply_to' => $item->reply_to,
7297                        'response_id' => $item->id,
7298                    ]);
7299                }
7300            }
7301
7302            $companyEmails = TblCompanyEmails::where('company_id', $companyId)->get();
7303
7304            return response(['message' => 'OK', 'data' => $companyEmails]);
7305
7306        } catch (\Exception $e) {
7307            report(AppException::fromException($e, 'GET_SENDER_IDENTITY_EXCEPTION'));
7308
7309            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7310        }
7311    }
7312
7313    public function get_all_sender_identity($companyId): ResponseFactory|HttpResponse
7314    {
7315
7316        try {
7317
7318            $companyId = addslashes((string) $companyId);
7319
7320            $query = "SELECT
7321                        id,
7322                        response_id,
7323                        nickname,
7324                        CONCAT(nickname, ' - ', from_email) from_email,
7325                        from_name,
7326                        reply_to,
7327                        reply_to_name,
7328                        address,
7329                        address2,
7330                        state,
7331                        city,
7332                        country,
7333                        zip,
7334                        verified,
7335                        locked,
7336                        is_active,
7337                        created_by,
7338                        created_at,
7339                        updated_by,
7340                        updated_at
7341                    FROM
7342                        tbl_company_emails
7343                    WHERE
7344                        company_id != {$companyId}
7345                        AND from_email NOT IN (
7346                        SELECT
7347                            from_email
7348                        FROM
7349                            tbl_company_emails
7350                        WHERE
7351                            company_id = {$companyId}
7352                        )
7353                    ";
7354
7355            $companyEmails = DB::select($query);
7356
7357            return response(['message' => 'OK', 'data' => $companyEmails]);
7358
7359        } catch (\Exception $e) {
7360            report(AppException::fromException($e, 'GET_ALL_SENDER_IDENTITY_EXCEPTION'));
7361
7362            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7363        }
7364    }
7365
7366    public function delete_sender_identity(Request $request): ResponseFactory|HttpResponse
7367    {
7368
7369        try {
7370
7371            $data = $request->all();
7372            $responseId = addslashes((string) $data['response_id']);
7373            $id = addslashes((string) $data['id']);
7374
7375            $sender = TblCompanyEmails::where('response_id', $responseId)->count();
7376
7377            if ($sender > 1) {
7378                TblCompanyEmails::where('id', $id)->delete();
7379            } else {
7380                $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
7381
7382                $response = $sendgrid->client->verified_senders()->_($responseId)->delete();
7383
7384                if ($response->statusCode() == 204) {
7385                    TblCompanyEmails::where('response_id', $responseId)->delete();
7386                }
7387            }
7388
7389            return response(['message' => 'OK']);
7390
7391        } catch (\Exception $e) {
7392            report(AppException::fromException($e, 'DELETE_SENDER_IDENTITY_EXCEPTION'));
7393
7394            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7395        }
7396    }
7397
7398    public function create_template(Request $request): ResponseFactory|HttpResponse
7399    {
7400
7401        try {
7402
7403            $data = $request->all();
7404
7405            $files = $request->file('files');
7406            unset($data['files']);
7407
7408            if ($files) {
7409                $totalFileCount = count($files);
7410                if ($totalFileCount > 2) {
7411                    return response(['message' => 'KO', 'error' => __('language.file_count_exceeded')]);
7412                }
7413            }
7414
7415            $result = TblEmailConfiguration::create($data);
7416            $id = $result->id;
7417
7418            $directory = 'public/uploads';
7419            $origFilename = 'fireservicetitan.png';
7420            $file = 'public/uploads/fireservicetitan.png';
7421
7422            $sourcePath = public_path($origFilename);
7423            $destinationPath = 'public/uploads/'.$origFilename;
7424
7425            $filename = $id.'-EI'.time().'-'.$origFilename;
7426
7427            Storage::putFileAs($directory, new \Illuminate\Http\File($sourcePath), $filename);
7428            Storage::disk('google')->put($filename, file_get_contents(storage_path().'/app/public/uploads/'.$filename));
7429
7430            TblEmailFiles::create(
7431                [
7432                    'email_template_id' => $id,
7433                    'original_name' => $origFilename,
7434                    'filename' => $filename,
7435                    'uploaded_by' => $data['created_by'],
7436                ]
7437            );
7438
7439            if ($files) {
7440
7441                $uploadedFiles = [];
7442                $i = 0;
7443
7444                $combinedFilesSize = 0;
7445
7446                foreach ($files as $file) {
7447                    $i++;
7448
7449                    $origFilename = str_replace(' ', '', $file->getClientOriginalName());
7450
7451                    $filename = $id.'-EI'.time().'-'.$origFilename;
7452
7453                    $combinedFilesSize = $combinedFilesSize + $file->getSize();
7454
7455                    if ($combinedFilesSize > 25000000) {
7456                        return response(['message' => 'KO', 'error' => __('language.file_size_exceeded')]);
7457                    }
7458
7459                    Storage::putFileAs($directory, $file, $filename);
7460                    Storage::disk('google')->put($filename, file_get_contents(storage_path().'/app/public/uploads/'.$filename));
7461
7462                    if (in_array($origFilename, $uploadedFiles)) {
7463                        $origFilename = $origFilename.$i;
7464                    }
7465
7466                    TblEmailFiles::create(
7467                        [
7468                            'email_template_id' => $id,
7469                            'original_name' => $origFilename,
7470                            'filename' => $filename,
7471                            'uploaded_by' => $data['created_by'],
7472                        ]
7473                    );
7474
7475                    $uploadedFiles[] = $file->getClientOriginalName();
7476                }
7477            }
7478
7479            return response(['message' => 'OK']);
7480
7481        } catch (\Exception $e) {
7482            report(AppException::fromException($e, 'CREATE_TEMPLATE_EXCEPTION'));
7483
7484            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7485        }
7486    }
7487
7488    public function get_email_files($emailTemplateId): ResponseFactory|HttpResponse
7489    {
7490
7491        try {
7492
7493            $emailTemplateId = addslashes((string) $emailTemplateId);
7494
7495            $result = TblEmailFiles::where('email_template_id', $emailTemplateId)->orderBy('order', 'asc')->get();
7496
7497            foreach ($result as $key => $value) {
7498                $path = storage_path('app/public/uploads/'.$result[$key]->filename);
7499
7500                if (File::exists($path)) {
7501                    $fileSizeBytes = File::size($path);
7502                    $result[$key]->setAttribute('filesize', $this->human_filesize($fileSizeBytes));
7503                    $result[$key]->original_name = $result[$key]->original_name." ({$result[$key]->getAttribute('filesize')})";
7504                    $result[$key]->setAttribute('img', 'data:image/png;base64,'.base64_encode(file_get_contents($path)));
7505                }
7506            }
7507
7508            return response(['message' => 'OK', 'data' => $result]);
7509
7510        } catch (\Exception $e) {
7511            report(AppException::fromException($e, 'GET_EMAIL_FILES_EXCEPTION'));
7512
7513            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7514        }
7515    }
7516
7517    public function download_email_template_file($fileId): ResponseFactory|HttpResponse
7518    {
7519
7520        try {
7521
7522            $fileId = addslashes((string) $fileId);
7523
7524            $result = TblEmailFiles::where('file_id', $fileId)->first();
7525
7526            if ($result) {
7527                $path = storage_path('app/public/uploads/'.$result->filename);
7528
7529                if (! Storage::disk('public')->exists('uploads/'.$result->filename)) {
7530                    return response(['message' => 'KO']);
7531                }
7532
7533                return response()->download($path);
7534            }
7535
7536            return response(['message' => 'KO']);
7537
7538        } catch (\Exception $e) {
7539            report(AppException::fromException($e, 'DOWNLOAD_EMAIL_TEMPLATE_FILE_EXCEPTION'));
7540
7541            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7542        }
7543
7544    }
7545
7546    public function delete_email_template_file($fileId): ResponseFactory|HttpResponse
7547    {
7548
7549        try {
7550
7551            $fileId = addslashes((string) $fileId);
7552            $file = TblEmailFiles::where('file_id', $fileId)->first();
7553            $result = TblEmailFiles::where('file_id', $fileId)->first();
7554
7555            if ($result) {
7556                TblEmailFiles::where('file_id', $fileId)->delete();
7557                $path = storage_path('app/public/uploads/'.$result->filename);
7558                Storage::disk('public')->delete('uploads/'.$result->filename);
7559            }
7560
7561            return response(['message' => 'OK']);
7562
7563        } catch (\Exception $e) {
7564            report(AppException::fromException($e, 'DELETE_EMAIL_TEMPLATE_FILE_EXCEPTION'));
7565
7566            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7567        }
7568    }
7569
7570    public function update_email_template_order(Request $request, $id)
7571    {
7572
7573        try {
7574
7575            $id = addslashes((string) $id);
7576            $data = $request->all();
7577
7578            foreach ($data as $item) {
7579                TblEmailFiles::where('file_id', $item['file_id'])->update(['order' => $item['order']]);
7580            }
7581
7582        } catch (\Exception $e) {
7583            report(AppException::fromException($e, 'UPDATE_EMAIL_TEMPLATE_ORDER_EXCEPTION'));
7584
7585            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7586        }
7587
7588    }
7589
7590    public function update_email_template(Request $request, $id): ResponseFactory|HttpResponse
7591    {
7592
7593        try {
7594
7595            $data = $request->all();
7596
7597            $id = addslashes((string) $id);
7598
7599            $files = $request->file('files');
7600            unset($data['files']);
7601
7602            $fileCount = TblEmailFiles::where('email_template_id', $id)->count();
7603
7604            if ($files) {
7605                $totalFileCount = $fileCount + count($files);
7606                if ($totalFileCount > 3) {
7607                    return response(['message' => 'KO', 'error' => __('language.file_count_exceeded')]);
7608                }
7609            }
7610
7611            if (isset($data['cron_default'])) {
7612                TblEmailConfiguration::where('company_id', $data['company_id'])->where('cron_default', 1)->update(['cron_default' => 0]);
7613            }
7614
7615            $data['updated_at'] = date('Y-m-d H:i:s');
7616            TblEmailConfiguration::where('id', $id)->update($data);
7617
7618            if ($files) {
7619
7620                $directory = 'public/uploads';
7621                $uploadedFiles = [];
7622                $i = 0;
7623
7624                $combinedFilesSize = 0;
7625                foreach ($files as $file) {
7626                    $i++;
7627                    $origFilename = str_replace(' ', '', $file->getClientOriginalName());
7628                    $filename = $id.'-EI'.time().'-'.$origFilename;
7629
7630                    $combinedFilesSize = $combinedFilesSize + $file->getSize();
7631
7632                    if ($combinedFilesSize > 25000000) {
7633                        return response(['message' => 'KO', 'error' => __('language.file_size_exceeded')]);
7634                    }
7635
7636                    Storage::putFileAs($directory, $file, $filename);
7637                    Storage::disk('google')->put($filename, file_get_contents(storage_path().'/app/public/uploads/'.$filename));
7638
7639                    if (in_array($origFilename, $uploadedFiles)) {
7640                        $origFilename = $origFilename.$i;
7641                    }
7642
7643                    TblEmailFiles::create(
7644                        [
7645                            'email_template_id' => $id,
7646                            'original_name' => $origFilename,
7647                            'filename' => $filename,
7648                            'uploaded_by' => $data['updated_by'],
7649                        ]
7650                    );
7651
7652                    $uploadedFiles[] = $file->getClientOriginalName();
7653                }
7654            }
7655
7656            return response(['message' => 'OK']);
7657
7658        } catch (\Exception $e) {
7659            report(AppException::fromException($e, 'UPDATE_EMAIL_TEMPLATE_EXCEPTION'));
7660
7661            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7662        }
7663
7664    }
7665
7666    public function delete_template($id): ResponseFactory|HttpResponse
7667    {
7668
7669        try {
7670
7671            $id = addslashes((string) $id);
7672
7673            TblEmailConfiguration::where('id', $id)->delete();
7674
7675            return response(['message' => 'OK']);
7676
7677        } catch (\Exception $e) {
7678            report(AppException::fromException($e, 'DELETE_TEMPLATE_EXCEPTION'));
7679
7680            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7681        }
7682
7683    }
7684
7685    public function get_email_template($companyId): ResponseFactory|HttpResponse
7686    {
7687
7688        try {
7689
7690            $companyId = addslashes((string) $companyId);
7691
7692            $where = '';
7693            if ($companyId != 0) {
7694                $where = " a.company_id = {$companyId} ";
7695            } else {
7696                $where = " a.company_id IN ({$this->companyId}";
7697            }
7698
7699            $query = "SELECT
7700                        a.id,
7701                        a.from_id,
7702                        CASE
7703                            WHEN a.type = 'followUps' THEN 'Follow-ups'
7704                            WHEN a.type = 'sendToClient' THEN 'Send to client'
7705                        END types,
7706                        (SELECT from_email FROM tbl_company_emails WHERE id = a.from_id) from_email,
7707                        a.name,
7708                        b.name company_name,
7709                        b.company_id,
7710                        a.subject,
7711                        a.html,
7712                        a.is_active,
7713                        a.type,
7714                        a.created_by,
7715                        a.created_at,
7716                        a.updated_by,
7717                        a.updated_at,
7718                        a.cron_default
7719                    FROM tbl_email_configuration a
7720                    LEFT JOIN tbl_companies b
7721                        ON b.company_id = a.company_id
7722                    WHERE {$where}
7723                    ORDER BY a.id DESC";
7724
7725            $result = DB::select($query);
7726
7727            return response(['message' => 'OK', 'data' => $result]);
7728
7729        } catch (\Exception $e) {
7730            report(AppException::fromException($e, 'GET_EMAIL_TEMPLATE_EXCEPTION'));
7731
7732            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7733        }
7734
7735    }
7736
7737    public function update_sender_identity(Request $request, $responseId): ResponseFactory|HttpResponse
7738    {
7739
7740        try {
7741
7742            $data = $request->all();
7743            $companyId = $data['company_id'];
7744            $updatedBy = $data['updated_by'];
7745            $active = $data['is_active'];
7746            $id = addslashes((string) $data['id']);
7747            unset($data['id']);
7748            unset($data['company_id']);
7749            unset($data['updated_by']);
7750            unset($data['is_active']);
7751
7752            $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
7753            $data['reply_to'] = $data['from_email'];
7754            $data['reply_to_name'] = $data['from_name'];
7755            $requestBody = $data;
7756            $error = false;
7757
7758            $response = $sendgrid->client->verified_senders()->_($responseId)->patch($requestBody);
7759
7760            if ($response->statusCode() == 200) {
7761                $x = json_decode((string) $response->body());
7762
7763                $data['updated_by'] = $updatedBy;
7764                $data['updated_at'] = date('Y-m-d H:i:s');
7765                $data['is_active'] = $active;
7766
7767                TblCompanyEmails::where('company_id', $companyId)->update(['is_active' => 0]);
7768                TblCompanyEmails::where('id', $id)->update($data);
7769
7770                Log::channel('email_log')->info('EMAIL: '.$data['from_email'].' - UPDATED');
7771            } else {
7772                $error = true;
7773                Log::channel('email_log')->error('REQUEST BODY: - '.$response->body());
7774            }
7775
7776            $response = json_decode((string) $response->body());
7777
7778            if ($error) {
7779                $errMessage = @$response->errors[0]->field.': '.@$response->errors[0]->message;
7780
7781                return response(['message' => 'KO', 'error' => $errMessage]);
7782            } else {
7783                return response(['message' => 'OK', 'data' => $response]);
7784            }
7785
7786        } catch (\Exception $e) {
7787            report(AppException::fromException($e, 'UPDATE_SENDER_IDENTITY_EXCEPTION'));
7788
7789            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7790        }
7791
7792    }
7793
7794    public function resend_verification($id): ResponseFactory|HttpResponse
7795    {
7796
7797        try {
7798
7799            $id = addslashes((string) $id);
7800
7801            $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
7802
7803            $response = $sendgrid->client->verified_senders()->resend()->_($id)->post();
7804
7805            if ($response->statusCode() == 204) {
7806                return response(['message' => 'OK']);
7807            } else {
7808                $response = json_decode((string) $response->body());
7809                $errMessage = $response->errors[0]->error_id.': '.$response->errors[0]->message;
7810
7811                return response(['message' => 'KO', 'error' => $errMessage]);
7812            }
7813
7814        } catch (\Exception $e) {
7815            report(AppException::fromException($e, 'RESEND_VERIFICATION_EXCEPTION'));
7816
7817            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7818        }
7819    }
7820
7821    public function list_quotation_analytics_by_performance(Request $request): ResponseFactory|HttpResponse
7822    {
7823
7824        try {
7825
7826            $data = $request->all();
7827            $companyId = addslashes((string) $data['company_id']);
7828            $where = '';
7829
7830            if ($companyId != 0) {
7831                $where .= " AND company_id = {$companyId} AND for_add = 0 ";
7832            } else {
7833                $where .= " AND company_id IN ({$this->companyId}) AND for_add = 0 ";
7834            }
7835
7836            if (isset($data['budget_status']) && $data['budget_status'] != null) {
7837                $where .= " AND bs.budget_status_id = {$data['budget_status']}";
7838            }
7839
7840            $latestYear = TblQuotations::max(DB::raw('YEAR(issue_date)'));
7841            $md = date('m-d');
7842
7843            $query = "SELECT
7844                        q.years,
7845                        q.totalIssue,
7846                        (q.total_acceptance_percentage - q.totalAcceptancePercentage) / q.totalAcceptancePercentage * 100 totalAcceptancePercentage,
7847                        q.totalAccept
7848                    FROM
7849                        (
7850                        SELECT
7851                            YEAR(a.issue_date) years,
7852                            (b.total_issued - COUNT(a.issue_date)) / COUNT(a.issue_date) * 100 totalIssue,
7853                            COUNT(
7854                                CASE WHEN a.acceptance_date IS NOT NULL
7855                                AND DATE_FORMAT(a.acceptance_date, '%Y-%m-%d') BETWEEN CONCAT(YEAR(a.acceptance_date), '-01-01')
7856                                AND CONCAT(YEAR(a.acceptance_date), '-{$md}')
7857                                AND YEAR(a.acceptance_date) = YEAR(a.issue_date)
7858                                AND a.budget_status_id = 3 THEN 1 END
7859                            ) / COUNT(a.issue_date) * 100 totalAcceptancePercentage,
7860                            b.total_acceptance_percentage,
7861                            (b.total_acceptance -
7862                                COUNT(
7863                                    CASE WHEN a.acceptance_date IS NOT NULL
7864                                    AND DATE_FORMAT(a.acceptance_date, '%Y-%m-%d') BETWEEN CONCAT(YEAR(a.acceptance_date), '-01-01')
7865                                    AND CONCAT(YEAR(a.acceptance_date), '-{$md}')
7866                                    AND YEAR(a.acceptance_date) = YEAR(a.issue_date)
7867                                    AND a.budget_status_id = 3 THEN 1 END)
7868                                    ) / COUNT(
7869                                            CASE WHEN a.acceptance_date IS NOT NULL
7870                                            AND DATE_FORMAT(a.acceptance_date, '%Y-%m-%d') BETWEEN CONCAT(YEAR(a.acceptance_date), '-01-01')
7871                                            AND CONCAT(YEAR(a.acceptance_date), '-{$md}')
7872                                            AND YEAR(a.acceptance_date) = YEAR(a.issue_date)
7873                                            AND a.budget_status_id = 3 THEN 1 END
7874                            ) * 100 totalAccept
7875                        FROM
7876                            tbl_quotations a
7877                            JOIN (
7878                            SELECT
7879                                YEAR(issue_date) current_year,
7880                                COUNT(issue_date) total_issued,
7881                                COUNT(
7882                                    CASE WHEN acceptance_date IS NOT NULL
7883                                    AND DATE_FORMAT(acceptance_date, '%Y-%m-%d') BETWEEN CONCAT(YEAR(acceptance_date), '-01-01')
7884                                    AND CONCAT(YEAR(acceptance_date), '-{$md}')
7885                                    AND YEAR(acceptance_date) = YEAR(issue_date)
7886                                    AND budget_status_id = 3 THEN 1 END
7887                                ) total_acceptance,
7888                                COUNT(
7889                                    CASE WHEN acceptance_date IS NOT NULL
7890                                    AND DATE_FORMAT(acceptance_date, '%Y-%m-%d') BETWEEN CONCAT(YEAR(acceptance_date), '-01-01')
7891                                    AND CONCAT(YEAR(acceptance_date), '-{$md}')
7892                                    AND YEAR(acceptance_date) = YEAR(issue_date)
7893                                    AND budget_status_id = 3 THEN 1 END
7894                                ) / COUNT(issue_date) * 100 total_acceptance_percentage
7895                            FROM
7896                                tbl_quotations
7897                            WHERE
7898                                issue_date IS NOT NULL
7899                                AND YEAR(issue_date) = {$latestYear}
7900                                AND DATE_FORMAT(issue_date, '%Y-%m-%d') BETWEEN CONCAT(YEAR(issue_date), '-01-01')
7901                                AND CONCAT(YEAR(issue_date), '-{$md}')
7902                                {$where}
7903                            GROUP by
7904                                1
7905                            ) b
7906                        WHERE
7907                            a.issue_date IS NOT NULL
7908                            AND a.budget_type_id != 7
7909                            AND a.budget_type_id IS NOT NULL
7910                            AND {$latestYear} > YEAR(a.issue_date)
7911                            AND DATE_FORMAT(a.issue_date, '%Y-%m-%d') BETWEEN CONCAT(YEAR(a.issue_date), '-01-01')
7912                            AND CONCAT(YEAR(a.issue_date), '-{$md}')
7913                        GROUP BY
7914                            1
7915                        ORDER BY
7916                            YEAR(issue_date) DESC
7917                        ) q
7918                    ";
7919
7920            $resultYtd = DB::select($query);
7921
7922            $query = "SELECT
7923                        YEAR(issue_date) years,
7924                        COUNT(issue_date) totalIssue,
7925                        COUNT(
7926                            CASE WHEN acceptance_date IS NOT NULL
7927                            AND DATE_FORMAT(acceptance_date, '%Y-%m-%d') BETWEEN CONCAT(YEAR(acceptance_date), '-01-01')
7928                            AND CONCAT(YEAR(acceptance_date), '-{$md}')
7929                            AND YEAR(acceptance_date) = YEAR(issue_date)
7930                            AND budget_status_id = 3 THEN 1 END
7931                        ) totalAccept,
7932                        COUNT(
7933                            CASE WHEN acceptance_date IS NOT NULL
7934                            AND DATE_FORMAT(acceptance_date, '%Y-%m-%d') BETWEEN CONCAT(YEAR(acceptance_date), '-01-01')
7935                            AND CONCAT(YEAR(acceptance_date), '-{$md}')
7936                            AND YEAR(acceptance_date) = YEAR(issue_date)
7937                            AND budget_status_id = 3 THEN 1 END
7938                        ) / COUNT(issue_date) * 100 totalAcceptancePercentage
7939                    FROM
7940                        tbl_quotations
7941                    WHERE
7942                        issue_date IS NOT NULL
7943                        AND budget_type_id != 7
7944                        AND budget_type_id IS NOT NULL
7945                        AND DATE_FORMAT(issue_date, '%Y-%m-%d') BETWEEN CONCAT(YEAR(issue_date), '-01-01')
7946                        AND CONCAT(YEAR(issue_date), '-{$md}')
7947                        {$where}
7948                    GROUP BY
7949                        1
7950                    ORDER BY
7951                        YEAR(issue_date) DESC
7952                    ";
7953
7954            $resultYears = DB::select($query);
7955
7956            return response(['message' => 'OK', 'ytdData' => $resultYtd, 'yearsData' => $resultYears]);
7957
7958        } catch (\Exception $e) {
7959            report(AppException::fromException($e, 'LIST_QUOTATION_ANALYTICS_BY_PERFORMANCE_EXCEPTION'));
7960
7961            return response(['message' => 'KO', 'error' => $e->getMessage()]);
7962        }
7963    }
7964
7965    public function list_orders_update_logs($companyId): ResponseFactory|HttpResponse
7966    {
7967
7968        try {
7969
7970            $where = '';
7971
7972            if ($companyId != 0) {
7973                $where .= " a.company_id = {$companyId}";
7974            } else {
7975                $where .= " a.company_id IN ({$this->companyId})";
7976            }
7977
7978            $query = "SELECT
7979                        a.id,
7980                        b.name company,
7981                        a.to_process,
7982                        a.rejected_affected_rows,
7983                        a.for_add_deleted_affected_rows,
7984                        a.month_change_affected_rows,
7985                        a.follow_ups_affected_rows,
7986                        a.sync_succesfull,
7987                        a.sync_error,
7988                        a.sync_error_message,
7989                        a.sync_success_ids,
7990                        a.cleared_email_errors,
7991                        a.remaining_email_errors,
7992                        CASE
7993                            WHEN a.rejected_affected_rows IS NOT NULL THEN a.rejected_affected_rows
7994                            WHEN a.for_add_deleted_affected_rows IS NOT NULL THEN a.for_add_deleted_affected_rows
7995                            WHEN a.month_change_affected_rows IS NOT NULL THEN a.month_change_affected_rows
7996                            WHEN a.follow_ups_affected_rows IS NOT NULL THEN a.follow_ups_affected_rows
7997                        END totals,
7998                        a.status,
7999                        a.processed_by,
8000                        a.started_at,
8001                        a.ended_at,
8002                        a.rejected_automatically_ids
8003                    FROM `tbl_orders_update_logs` a
8004                    LEFT JOIN tbl_companies b
8005                        ON a.company_id = b.company_id
8006                    WHERE {$where}
8007                    ORDER BY started_at DESC";
8008
8009            $result = DB::select($query);
8010
8011            return response(['message' => 'OK', 'data' => $result]);
8012
8013        } catch (\Exception $e) {
8014            report(AppException::fromException($e, 'LIST_ORDERS_UPDATE_LOGS_EXCEPTION'));
8015
8016            return response(['message' => 'KO', 'error' => $e->getMessage()]);
8017        }
8018
8019    }
8020
8021    public function list_g3w_orders_update_logs($companyId): ResponseFactory|HttpResponse
8022    {
8023
8024        try {
8025            $result = TblG3WOrdersUpdateLogs::where('company_id', $companyId)
8026                ->where('processed_by', 'System')
8027                ->get();
8028
8029            return response(['message' => 'OK', 'data' => $result]);
8030
8031        } catch (\Exception $e) {
8032            report(AppException::fromException($e, 'LIST_G3W_ORDERS_UPDATE_LOGS_EXCEPTION'));
8033
8034            return response(['message' => 'KO', 'error' => $e->getMessage()]);
8035        }
8036
8037    }
8038
8039    public function list_g3w_orders_failed($companyId): ResponseFactory|HttpResponse
8040    {
8041        try {
8042            $logs = TblG3WOrdersUpdateLogs::where('company_id', $companyId)
8043                ->where('sync_error', '>', 0)
8044                ->get();
8045
8046            $individualErrors = [];
8047
8048            foreach ($logs as $log) {
8049                $errorIds = json_decode((string) $log->sync_error_ids);
8050                $errorMessages = preg_split('/(?=Error (sincronizando|actualizando) el presupuesto)/', (string) $log->sync_error_message, -1, PREG_SPLIT_NO_EMPTY);
8051
8052                if (is_array($errorIds)) {
8053                    foreach ($errorIds as $index => $id) {
8054                        $rawMsg = $errorMessages[$index] ?? 'Error desconocido';
8055                        $cleanMsg = trim((string) preg_replace('/.*?el presupuesto \d+:/i', '', $rawMsg));
8056                        $cleanMsg = rtrim($cleanMsg, ',');
8057
8058                        $individualErrors[] = [
8059                            'quote_id' => $id,
8060                            'status' => 'Failed',
8061                            'started_at' => $log->started_at,
8062                            'ended_at' => $log->ended_at,
8063                            'reason' => $cleanMsg,
8064                            'log_id' => $log->id,
8065                        ];
8066                    }
8067                }
8068            }
8069
8070            return response(['message' => 'OK', 'data' => $individualErrors]);
8071
8072        } catch (\Exception $e) {
8073            report($e);
8074
8075            return response(['message' => 'KO', 'error' => $e->getMessage()]);
8076        }
8077    }
8078
8079    public function update_budget_status_rejected_manual(Request $request): ResponseFactory|HttpResponse
8080    {
8081
8082        try {
8083
8084            $data = $request->all();
8085
8086            $update = $this->update_budget_status_rejected($data['company_id'], $data['processed_by']);
8087
8088            if ($update) {
8089                return response(['message' => 'OK']);
8090            } else {
8091                return response(['message' => 'KO']);
8092            }
8093
8094        } catch (\Exception $e) {
8095            report(AppException::fromException($e, 'UPDATE_BUDGET_STATUS_REJECTED_MANUAL_EXCEPTION'));
8096
8097            return response(['message' => 'KO', 'error' => $e->getMessage()]);
8098        }
8099    }
8100
8101    public function update_budget_status_rejected($companyId = null, $processedBy = 'System'): ResponseFactory|HttpResponse
8102    {
8103
8104        $startedAt = date('Y-m-d H:i:s');
8105        try {
8106
8107            $startedAt = date('Y-m-d H:i:s');
8108            $companyId = addslashes((string) $companyId);
8109            $where = '';
8110
8111            $budgetTypes = TblBudgetTypes::get();
8112
8113            if (count($budgetTypes) > 0) {
8114
8115                $companies = [];
8116
8117                if ($companyId != 0) {
8118                    $companies = TblCompanies::where('company_id', $companyId)->get();
8119                    $where = "AND company_id = {$companyId}";
8120                } else {
8121                    $companies = TblCompanies::get();
8122                }
8123
8124                for ($i = 0; $i < count($budgetTypes); $i++) {
8125
8126                    $days = $budgetTypes[$i]->duration;
8127                    $id = $budgetTypes[$i]->budget_type_id;
8128
8129                    if (! $days) {
8130                        continue;
8131                    }
8132
8133                    for ($c = 0; $c < count($companies); $c++) {
8134                        $companyId = $companies[$c]->company_id;
8135
8136                        $query = "SELECT
8137                                    GROUP_CONCAT(id) ids
8138                                FROM
8139                                    tbl_quotations
8140                                WHERE
8141                                    budget_type_id = {$id}
8142                                AND budget_status_id IN (1, 2, 11)
8143                                AND DATEDIFF(NOW(), issue_date) >= {$days}
8144                                AND for_add = 0
8145                                {$where}";
8146
8147                        $result = DB::select($query);
8148
8149                        if (count($result) > 0) {
8150
8151                            $ids = $result[0]->ids;
8152
8153                            if ($ids != null || $ids != '') {
8154                                $query = "UPDATE
8155                                            tbl_quotations
8156                                        SET
8157                                            budget_status_id = 7
8158                                        WHERE
8159                                            id IN ({$ids})";
8160
8161                                DB::select($query);
8162
8163                                TblOrdersUpdateLogs::create(
8164                                    [
8165                                        'company_id' => $companyId,
8166                                        'to_process' => 'Orders',
8167                                        'status' => 'success',
8168                                        'rejected_automatically_ids' => $ids,
8169                                        'processed_by' => $processedBy,
8170                                        'started_at' => $startedAt,
8171                                        'ended_at' => date('Y-m-d H:i:s'),
8172                                    ]
8173                                );
8174                            }
8175                        }
8176                    }
8177                }
8178            }
8179
8180            // FIRE-1145: was Cache::flush() — status rejection affects list_quotations + commercial counters.
8181            ResultCache::forgetDomain(['quotations', 'users']);
8182
8183            return response(['message' => 'OK']);
8184
8185        } catch (\Exception $e) {
8186            report(AppException::fromException($e, 'UPDATE_BUDGET_STATUS_REJECTED_MANUAL_EXCEPTION'));
8187
8188            TblOrdersUpdateLogs::create(
8189                [
8190                    'company_id' => $companyId,
8191                    'to_process' => 'Orders',
8192                    'status' => $e->getMessage(),
8193                    'processed_by' => $processedBy,
8194                    'started_at' => $startedAt,
8195                    'ended_at' => date('Y-m-d H:i:s'),
8196                ]
8197            );
8198
8199            return response(['message' => 'KO', 'error' => $e->getMessage()]);
8200        }
8201
8202    }
8203
8204    public function bulk_update_quotation(Request $request): ResponseFactory|HttpResponse
8205    {
8206
8207        // try {
8208
8209        $data = $request->all();
8210
8211        $r = new Request([
8212            'filterModel' => $data['filterModel'],
8213            'sortModel' => $data['sortModel'],
8214            'start' => 0,
8215            'end' => 999999999,
8216            'company_id' => $data['company_id'],
8217            'user_id' => $data['user_id'],
8218            'ids' => $data['ids'],
8219            'searchText' => $data['searchText'],
8220            'ids_not_in' => $data['ids_not_in'],
8221        ]);
8222
8223        $listQuotations = $this->list_orders_table($r);
8224        $d = $listQuotations->original['data'];
8225
8226        if (count($d) > 0) {
8227            if (isset($data['last_follow_up_date']) && $data['last_follow_up_date'] == 1) {
8228                unset($data['last_follow_up_date']);
8229            }
8230            // unset($data['last_follow_up_date']);
8231            unset($data['filterModel']);
8232            unset($data['sortModel']);
8233            unset($data['start']);
8234            unset($data['end']);
8235            unset($data['company_id']);
8236            unset($data['user_id']);
8237            unset($data['ids']);
8238            unset($data['searchText']);
8239            unset($data['ids_not_in']);
8240            // Sent by the data-table bulk plumbing but not a real column — would
8241            // otherwise blow up with "Unknown column 'selectAll'".
8242            unset($data['selectAll']);
8243
8244            $result = [];
8245            for ($i = 0; $i < count($d); $i++) {
8246                array_push($result, $d[$i]->id);
8247            }
8248
8249            TblQuotations::whereIn('id', $result)->update($data);
8250
8251            // FIRE-1145: was Cache::flush() — bulk update affects list_quotations + commercial counters.
8252            ResultCache::forgetDomain(['quotations', 'users']);
8253        }
8254
8255        return response(['message' => 'OK', $data]);
8256
8257        // } catch (\Exception $e) {
8258        //     return response(['message' => 'KO', 'error' => $e->getMessage()]);
8259        // }
8260
8261    }
8262
8263    public function move_budget_and_job(Request $request): ResponseFactory|HttpResponse
8264    {
8265
8266        try {
8267
8268            $data = $request->all();
8269            $id = addslashes((string) $data['id']);
8270            $companyId = addslashes((string) $data['company_id']);
8271
8272            unset($data['id']);
8273            unset($data['company_id']);
8274
8275            $budget = TblQuotations::where('id', $id)->first();
8276
8277            $quotationId = $budget->id;
8278            $companyIdJob = $budget->company_id;
8279
8280            $query = "SELECT
8281                        COUNT(1) total
8282                    FROM tbl_company_users a
8283                    LEFT JOIN tbl_users b
8284                        ON a.user_id = b.id
8285                    WHERE a.company_id = {$companyId}
8286                    AND b.name = '{$budget->commercial}'
8287                    ORDER BY b.name ASC";
8288
8289            $result = DB::select($query);
8290
8291            $commercial = $budget->commercial;
8292
8293            if ($result[0]->total == 0) {
8294                $commercial = $data['created_by'];
8295            }
8296
8297            $r = new Request([
8298                'created_by' => $commercial,
8299            ]);
8300
8301            $result = $this->get_number($r, $companyId, 1);
8302            $newNumber = $result->original['number'];
8303
8304            TblQuotations::where('id', $id)->update(
8305                [
8306                    'quote_id' => $newNumber,
8307                    'company_id' => $companyId,
8308                    'from_company_id' => $budget->company_id,
8309                    'commercial' => $commercial,
8310                ]
8311            );
8312
8313            $job = TblOngoingJobs::where('quotation_id', $quotationId)->first();
8314
8315            $jobId = null;
8316
8317            if ($job) {
8318                $jobId = $job->id;
8319
8320                $query = "SELECT
8321                            COUNT(1) total
8322                        FROM tbl_company_users a
8323                        LEFT JOIN tbl_users b
8324                            ON a.user_id = b.id
8325                        WHERE a.company_id = {$companyId}
8326                        AND b.name = '{$job->responsible_for_work}'
8327                        ORDER BY b.name ASC";
8328
8329                $result = DB::select($query);
8330
8331                $responsibleForWork = $job->responsible_for_work;
8332
8333                if ($result[0]->total == 0) {
8334                    $responsibleForWork = $data['created_by'];
8335                }
8336
8337                TblOngoingJobs::where('quotation_id', $id)->update(
8338                    [
8339                        'quote_id' => $newNumber,
8340                        'company_id' => $companyId,
8341                        'responsible_for_work' => $responsibleForWork,
8342                    ]
8343                );
8344            }
8345
8346            // FIRE-1145: was Cache::flush() — move_budget_and_job touches both quotations and ongoing_jobs lists.
8347            ResultCache::forgetDomain(['quotations', 'ongoing_jobs']);
8348
8349            return response([
8350                'message' => 'OK',
8351                'quotation_id' => $quotationId,
8352                'job_id' => $jobId,
8353            ]);
8354
8355        } catch (\Exception $e) {
8356            report(AppException::fromException($e, 'MOVE_BUDGET_AND_JOB_EXCEPTION'));
8357
8358            return response(['message' => 'KO', 'error' => $e->getMessage()]);
8359        }
8360    }
8361
8362    public function list_quotation_analytics_by_types_of_budgets_created_per_week(Request $request): ResponseFactory|HttpResponse
8363    {
8364
8365        try {
8366
8367            $data = $request->all();
8368            $companyId = addslashes((string) $data['company_id']);
8369            $field = $data['field'];
8370
8371            $where = '';
8372            $whereYear = '';
8373            $dateLflArray = [];
8374            $companyIds = $this->companyIds;
8375
8376            if ($companyId != 0) {
8377                $companyIds = [$companyId];
8378            }
8379
8380            $acc = '';
8381            if ($field == 'acceptance_date') {
8382                $acc = ' AND q.acceptance_date IS NOT NULL ';
8383                // $field = 'created_at';
8384
8385                if (@$data['data_to_display'] == 4) {
8386                    $field = 'created_at';
8387                    $where .= ' AND YEAR(q.created_at) = YEAR(q.issue_date)';
8388                }
8389            } else {
8390                $field = 'created_at';
8391                $where .= ' AND YEAR(q.created_at) = YEAR(q.issue_date)';
8392            }
8393
8394            if (isset($data['years']) && $data['years'] != null) {
8395
8396                if (count($data['years']) > 0) {
8397                    foreach ($data['years'] as $year) {
8398                        if (isset($data['week']) && $data['week'] != null) {
8399                            $w = sprintf('%02d', $data['week']);
8400                            $whereYear .= " AND YEARWEEK(q.{$field}, 1) = '{$year}{$w}'";
8401                        } else {
8402                            $whereYear .= " AND YEAR(q.{$field}) = {$year}";
8403                        }
8404                    }
8405                }
8406            }
8407
8408            foreach ($companyIds as $v) {
8409
8410                $lflWhere = " AND q.company_id = {$v} ";
8411
8412                $query = "SELECT
8413                            CONCAT(
8414                                DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field}),
8415                                ' - ',
8416                                DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field})
8417                            ) AS date_like,
8418                            YEAR(q.{$field}) 'year',
8419                            DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS min_date_like,
8420                            DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS max_date_like,
8421                            {$v} 'company_id'
8422                        FROM
8423                            tbl_quotations q
8424                        WHERE
8425                            q.{$field} IS NOT NULL
8426                            AND q.for_add = 0
8427                            {$lflWhere}
8428                            {$whereYear}
8429                        GROUP BY YEAR(q.{$field})
8430                        ORDER BY YEAR(q.{$field}) DESC";
8431
8432                $dateLike = DB::select($query);
8433
8434                $dateLflArray[$v] = $dateLike;
8435            }
8436
8437            $isFy = true;
8438
8439            if (isset($data['ytd']) && $data['ytd'] != null && $data['ytd'] == true) {
8440                $isFy = false;
8441                $ytdArray = [];
8442                $ytdAcceptanceArray = [];
8443                $lflCompanyIds = [];
8444                foreach ($dateLflArray as $k => $v) {
8445                    foreach ($dateLflArray[$k] as $item) {
8446                        $year = $item->year;
8447                        $now = date('m-d');
8448                        array_push($ytdArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN '{$year}-01-01' AND '{$year}-{$now}'");
8449                    }
8450
8451                    $ytdArray = implode(' OR ', $ytdArray);
8452                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$ytdArray})");
8453                    $ytdArray = [];
8454                }
8455
8456                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
8457                $where .= " AND ({$lflCompanyIds}";
8458            }
8459
8460            if (isset($data['lfl']) && $data['lfl'] != null && $data['lfl'] == true) {
8461                $isFy = false;
8462                $lflArray = [];
8463                $ytdAcceptanceArray = [];
8464                $lflCompanyIds = [];
8465                foreach ($dateLflArray as $k => $v) {
8466                    foreach ($dateLflArray[$k] as $item) {
8467                        $year = $item->year;
8468                        $min_date_like = $item->min_date_like;
8469                        $max_date_like = $item->max_date_like;
8470                        array_push($lflArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN LEAST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND GREATEST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}')");
8471                    }
8472
8473                    $lflArray = implode(' OR ', $lflArray);
8474                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$lflArray})");
8475                    $lflArray = [];
8476                }
8477
8478                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
8479                $where .= " AND ({$lflCompanyIds}";
8480            }
8481
8482            if ($isFy) {
8483                if ($companyId != 0) {
8484                    $where .= " AND q.company_id = {$companyId} ";
8485                } else {
8486                    $where .= " AND q.company_id IN ({$this->companyId})";
8487                }
8488            }
8489
8490            if (isset($data['source']) && $data['source'] != null) {
8491                $where .= " AND s.name = '{$data['source']}'";
8492            }
8493
8494            if (isset($data['month']) && $data['month'] != null) {
8495                $where .= " AND MONTH(q.{$field}) = '{$data['month']}'";
8496            }
8497
8498            if (isset($data['commercial']) && $data['commercial'] != null) {
8499                $where .= " AND q.commercial = '{$data['commercial']}'";
8500            }
8501
8502            if (isset($data['created_by']) && $data['created_by'] != null) {
8503                $where .= " AND q.created_by = '{$data['created_by']}'";
8504            }
8505
8506            if (isset($data['budget_type']) && $data['budget_type'] != null) {
8507                $where .= " AND bt.budget_type_id = {$data['budget_type']}";
8508            }
8509
8510            if (isset($data['budget_type_group']) && $data['budget_type_group'] != null) {
8511                $where .= " AND bt.budget_type_group_id = {$data['budget_type_group']}";
8512            }
8513
8514            if (isset($data['budget_status']) && $data['budget_status'] != null) {
8515                $where .= " AND bs.budget_status_id = {$data['budget_status']}";
8516            }
8517
8518            if (isset($data['client_type']) && $data['client_type'] != null) {
8519                $where .= " AND ct.customer_type_id = {$data['client_type']}";
8520            }
8521
8522            if (isset($data['segment_id']) && $data['segment_id'] != null) {
8523                $where .= " AND q.segment_id = {$data['segment_id']}";
8524            }
8525
8526            $col = '1';
8527
8528            if (isset($data['data_to_display']) && $data['data_to_display'] != null) {
8529                if ($data['data_to_display'] == 1) {
8530                    $col = '1';
8531                }
8532
8533                if ($data['data_to_display'] == 2) {
8534                    $col = 'q.amount';
8535                }
8536            }
8537
8538            $budgetTypes = TblBudgetTypes::orderByRaw('ISNULL(priority), priority ASC')->get();
8539            $cols = '';
8540            foreach ($budgetTypes as $item) {
8541                if ($item->name == '' || $item->name == null) {
8542                    $cols .= ",COALESCE(SUM(CASE WHEN bt.name IS NULL {$acc} THEN {$col} ELSE 0 END), 0) AS 'Otros'";
8543                } else {
8544                    $cols .= ",COALESCE(SUM(CASE WHEN bt.name = '{$item->name}{$acc} THEN {$col} ELSE 0 END), 0) AS '{$item->name}'";
8545                }
8546            }
8547
8548            $budgetTypeGroups = TblBudgetTypeGroups::orderByRaw('ISNULL(priority), priority ASC')->get();
8549
8550            $colsGroups = ",COALESCE(SUM(CASE WHEN bt.name IS NULL {$acc} THEN {$col} END), 0) AS Otros";
8551
8552            foreach ($budgetTypeGroups as $item) {
8553                $budgetTypeGroupName = str_replace(' ', '', $item->name).$item->budget_type_group_id;
8554                $colsGroups .= ",GROUP_CONCAT(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) {$acc} THEN q.id END) AS 'groupConcatIds{$budgetTypeGroupName}'";
8555                $colsGroups .= ",COALESCE(SUM(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) {$acc} THEN {$col} END), 0) AS '{$budgetTypeGroupName}'";
8556            }
8557
8558            $colsGroups .= ",COALESCE(SUM(CASE WHEN (bt.budget_type_group_id IS NOT NULL OR bt.name IS NULL) {$acc} THEN {$col} END), 0) AS total";
8559
8560            $col = $colsGroups.$cols;
8561
8562            if (@$data['data_to_display'] == 3) {
8563
8564                $cols = '';
8565                foreach ($budgetTypes as $item) {
8566                    if ($item->name == '' || $item->name == null) {
8567                        $cols .= ",COALESCE(
8568                                        SUM(CASE WHEN bt.name IS NULL {$acc} THEN q.amount ELSE 0 END) /
8569                                        SUM(CASE WHEN bt.name IS NULL {$acc} THEN 1 ELSE 0 END) * 100 , 0
8570                                    ) AS 'Otros'";
8571                    } else {
8572                        $cols .= ",COALESCE(
8573                                        SUM(CASE WHEN bt.name = '{$item->name}{$acc} THEN q.amount ELSE 0 END) /
8574                                        SUM(CASE WHEN bt.name = '{$item->name}{$acc} THEN 1 ELSE 0 END), 0
8575                                    ) AS '{$item->name}'";
8576                    }
8577                }
8578
8579                $colsGroups = ",COALESCE(
8580                                (SUM(CASE WHEN bt.name IS NULL {$acc} THEN q.amount END)) /
8581                                (SUM(CASE WHEN bt.name IS NULL {$acc} THEN 1 END))
8582                            , 0) Otros";
8583
8584                foreach ($budgetTypeGroups as $item) {
8585                    $budgetTypeGroupName = str_replace(' ', '', $item->name).$item->budget_type_group_id;
8586                    $colsGroups .= ",GROUP_CONCAT(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) {$acc} THEN q.id END) AS 'groupConcatIds{$budgetTypeGroupName}'";
8587                    $colsGroups .= ",COALESCE(
8588                                        (SUM(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) {$acc} THEN q.amount END)) /
8589                                        (SUM(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) {$acc} THEN 1 END))
8590                                    , 0) '{$budgetTypeGroupName}'";
8591                }
8592
8593                $colsGroups .= ",COALESCE(
8594                                    (SUM(CASE WHEN (bt.budget_type_group_id IS NOT NULL OR bt.name IS NULL) {$acc} THEN q.amount END)) /
8595                                    (SUM(CASE WHEN (bt.budget_type_group_id IS NOT NULL OR bt.name IS NULL) {$acc} THEN 1 END))
8596                                , 0) total";
8597
8598                $col = $colsGroups.$cols;
8599            }
8600
8601            if (@$data['data_to_display'] == 4) {
8602
8603                $cols = '';
8604
8605                foreach ($budgetTypes as $item) {
8606
8607                    if ($item->name == '' || $item->name == null) {
8608                        $cols .= ",COALESCE(
8609                                        SUM(CASE WHEN bt.name IS NULL AND q.acceptance_date IS NOT NULL THEN 1 ELSE 0 END) /
8610                                        SUM(CASE WHEN bt.name IS NULL AND q.created_at IS NOT NULL THEN 1 ELSE 0 END) * 100 , 0
8611                                ) AS 'Otros'";
8612                    } else {
8613                        $cols .= ", COALESCE(
8614                                        SUM(CASE WHEN bt.name = '{$item->name}' AND q.acceptance_date IS NOT NULL THEN 1 END) /
8615                                        SUM(CASE WHEN bt.name = '{$item->name}' AND q.created_at IS NOT NULL THEN 1 END) * 100, 0
8616                                ) AS '{$item->name}'";
8617                    }
8618                }
8619
8620                $colsGroups = ',COALESCE(
8621                                    (SUM(CASE WHEN bt.name IS NULL AND q.acceptance_date IS NOT NULL THEN 1 END)) /
8622                                    (SUM(CASE WHEN bt.name IS NULL AND q.created_at IS NOT NULL THEN 1 END)) * 100
8623                                    , 0) Otros';
8624
8625                foreach ($budgetTypeGroups as $item) {
8626                    $budgetTypeGroupName = str_replace(' ', '', $item->name).$item->budget_type_group_id;
8627                    $colsGroups .= ",GROUP_CONCAT(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) {$acc} THEN q.id END) AS 'groupConcatIds{$budgetTypeGroupName}'";
8628                    $colsGroups .= ",COALESCE(
8629                                        (SUM(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) AND q.acceptance_date IS NOT NULL THEN 1 END)) /
8630                                        (SUM(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) AND q.created_at IS NOT NULL THEN 1 END)) * 100
8631                                    , 0) '{$budgetTypeGroupName}'";
8632                }
8633
8634                $colsGroups .= ',COALESCE(
8635                                    (SUM(CASE WHEN (bt.budget_type_group_id IS NOT NULL OR bt.name IS NULL) AND q.acceptance_date IS NOT NULL THEN 1 END)) /
8636                                    (SUM(CASE WHEN (bt.budget_type_group_id IS NOT NULL OR bt.name IS NULL) AND q.created_at IS NOT NULL THEN 1 END)) * 100
8637                                    , 0) total';
8638
8639                $col = $colsGroups.$cols;
8640            }
8641
8642            $query = "SELECT
8643                            YEAR(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)) AS 'year',
8644                            LPAD(MONTH(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)), 2, 0) AS 'month',
8645                            LPAD(WEEK(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)), 2, 0) AS 'week',
8646                            DATE_FORMAT(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY), '%W, %M %e') 'namedate',
8647                            GROUP_CONCAT(CASE WHEN (bt.budget_type_group_id IS NOT NULL OR bt.name IS NULL) {$acc} THEN q.id END) groupConcatIds
8648                            {$col}
8649                        FROM
8650                            tbl_quotations q
8651                            LEFT JOIN tbl_sources s ON s.source_id = q.source_id
8652                            LEFT JOIN tbl_budget_status bs ON bs.budget_status_id = q.budget_status_id
8653                            LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
8654                            LEFT JOIN tbl_budget_type_groups btg ON bt.budget_type_group_id = btg.budget_type_group_id
8655                            LEFT JOIN tbl_customer_types ct ON q.customer_type_id = ct.customer_type_id
8656                        WHERE
8657                            q.{$field} IS NOT NULL
8658                            AND q.for_add = 0
8659                            AND q.budget_type_id IS NOT NULL
8660                            AND q.budget_type_id != 7
8661                            AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1                            
8662                            {$where}
8663                            {$whereYear}
8664                        GROUP BY
8665                            YEAR(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)),
8666                            MONTH(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)),
8667                            WEEK(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)) WITH ROLLUP
8668                        ORDER BY
8669                            YEAR DESC,
8670                            MONTH ASC,
8671                            WEEK ASC,
8672                            DATE_FORMAT(q.{$field}, '%e') ASC";
8673            // return $query;
8674            $result = DB::select($query);
8675
8676            $query = "SELECT
8677                        btg.budget_type_group_id,
8678                        btg.name,
8679                        (
8680                            SELECT
8681                                GROUP_CONCAT(COALESCE(bt.name, '') ORDER BY ISNULL(bt.priority), bt.priority ASC SEPARATOR '|')
8682                            FROM
8683                                tbl_budget_types bt
8684                            WHERE
8685                                bt.budget_type_group_id = btg.budget_type_group_id
8686                        ) budget_types
8687                        FROM
8688                            tbl_budget_type_groups btg
8689                        ORDER BY
8690                            ISNULL(btg.priority),
8691                            btg.priority ASC";
8692
8693            $budgetTypeGroups = DB::select($query);
8694
8695            foreach ($budgetTypeGroups as $item) {
8696                $item->group_key_name = str_replace(' ', '', $item->name).$item->budget_type_group_id;
8697                $item->budget_types = explode('|', (string) $item->budget_types);
8698            }
8699
8700            return response([
8701                'message' => 'OK',
8702                'data' => $result,
8703                'budgetTypeGroups' => $budgetTypeGroups,
8704            ]);
8705
8706        } catch (\Exception $e) {
8707            report(AppException::fromException($e, 'LIST_QUOTATION_ANALYTICS_BY_TYPES_OF_BUDGETS_CREATED_PER_WEEK_EXCEPTION'));
8708
8709            return response(['message' => 'KO', 'error' => $e->getMessage()]);
8710        }
8711    }
8712
8713    public function preview_file($id)
8714    {
8715
8716        try {
8717
8718            $file = TblFiles::where('file_id', $id)->first();
8719
8720            if (! $file) {
8721                return response()->json([
8722                    'message' => 'KO',
8723                    'error' => __('language.file_not_found'),
8724                ], 404);
8725            }
8726
8727            if (! Storage::disk('s3')->exists('uploads/'.$file->filename)) {
8728                return response()->json(['message' => 'File not found'], 404);
8729            }
8730
8731            $url = Storage::disk('s3')->temporaryUrl(
8732                'uploads/'.$file->filename,
8733                now()->addMinutes(5)
8734            );
8735
8736            return response()->json([
8737                'filename' => $file->filename,
8738                'url' => $url,
8739                'uploaded_by' => $file->uploaded_by,
8740                'uploaded_at' => $file->uploaded_at,
8741            ]);
8742
8743        } catch (\Exception $e) {
8744            report(AppException::fromException($e, 'PREVIEW_FILE_EXCEPTION'));
8745
8746            return response()->json([
8747                'message' => 'KO',
8748                'error' => $e->getMessage(),
8749            ], 500);
8750        }
8751    }
8752
8753    public function get_past_added_quotation(Request $request): ResponseFactory|HttpResponse
8754    {
8755
8756        try {
8757
8758            $data = $request->all();
8759            $keyword = addslashes((string) $data['keyword']);
8760            $result = [];
8761
8762            if (! empty($keyword)) {
8763                $array = explode(' ', $keyword);
8764
8765                $where = '';
8766
8767                $availableParameters = ['client', 'email'];
8768
8769                $searchTextArray = explode(' ', $keyword);
8770
8771                $searchArray = [];
8772                $matchScoreArray = [];
8773                foreach ($availableParameters as $field) {
8774                    foreach ($searchTextArray as $word) {
8775                        array_push($searchArray, "({$field} LIKE '%{$word}%')");
8776                        array_push($matchScoreArray, "CASE WHEN {$field} LIKE '%{$word}%' THEN 1 ELSE 0 END");
8777                    }
8778                }
8779
8780                $searchArray = implode(' OR ', $searchArray);
8781                $matchScoreArray = implode(' + ', $matchScoreArray);
8782                $matchScoreCol = "({$matchScoreArray})";
8783                $where .= " AND ({$searchArray}";
8784
8785                $query = "SELECT
8786                            id,
8787                            client,
8788                            segment_id,
8789                            CONCAT(`client`, ' - ', email) `client_email`,
8790                            email,
8791                            phone_number,
8792                            customer_type_id,
8793                            {$matchScoreCol} match_score
8794                        FROM tbl_quotations
8795                        WHERE for_add = 0
8796                        AND email IS NOT NULL AND phone_number IS NOT NULL
8797                        {$where}
8798                        GROUP BY client, email
8799                        ORDER BY match_score DESC, client ASC
8800                        ";
8801
8802                $result = DB::select($query);
8803            }
8804
8805            return response(['message' => 'OK', 'data' => $result]);
8806
8807        } catch (\Exception $e) {
8808            report(AppException::fromException($e, 'GET_PAST_ADDED_QUOTATION_EXCEPTION'));
8809
8810            return response(['message' => 'KO', 'error' => $e->getMessage()]);
8811        }
8812    }
8813
8814    public function send_acceptance_notification($quotationId, $companyId, $userId, $updatedBy): void
8815    {
8816
8817        $budget = TblQuotations::where('id', $quotationId)->first();
8818
8819        if ($budget != null) {
8820            $to = TblToAcceptanceNotifications::where('company_id', $companyId)->get();
8821            $cc = TblCcAcceptanceNotifications::where('company_id', $companyId)->get();
8822
8823            if (count($to) > 0 && count($cc) > 0) {
8824
8825                $company = TblCompanies::where('company_id', $companyId)->first();
8826
8827                $quoteId = $budget->quote_id;
8828                $amount = $this->currency($budget->amount, 1);
8829
8830                $url = config('app.frontend_url')."orders/{$quotationId}?company_id={$companyId}";
8831                $href = "<a href='{$url}'>{$quoteId}</a>";
8832
8833                $imgpath = file_get_contents(public_path('fireservicetitan.png'));
8834                $base64 = 'data:image/png;base64,'.base64_encode($imgpath);
8835
8836                $body = __('language.send_acceptance_notification.body_hello');
8837                $body .= __('language.send_acceptance_notification.body_message');
8838
8839                $body = str_replace('{{client}}', $budget->client, $body);
8840                $body = str_replace('{{username}}', $updatedBy, $body);
8841                $body = str_replace('{{company}}', $company->name, $body);
8842                $body = str_replace('{{amount}}', $amount, $body);
8843                $body = str_replace('{{quote_id}}', $href, $body);
8844
8845                $body .= '<p>Fire Service Titan</p>';
8846                $body .= "<img src='cid:fireservicetitan' style='height: 45px;' />";
8847
8848                $html = '<!DOCTYPE html>';
8849                $html .= '<html>';
8850                $html .= '<head>';
8851                $html .= '<meta charset="UTF-8">';
8852                $html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
8853                $html .= '</head>';
8854                $html .= '<body>';
8855                $html .= $body;
8856                $html .= '</body>';
8857                $html .= '</html>';
8858
8859                $subject = __('language.send_acceptance_notification.subject');
8860                $subject = str_replace('{{quote_id}}', $quoteId, $subject);
8861
8862                $email = new Mail;
8863
8864                $user = TblUsers::where('id', $userId)->first();
8865
8866                if (config('services.sendgrid.staging')) {
8867                    $email->addTo($user->email);
8868                } else {
8869
8870                    $emails = [];
8871
8872                    foreach ($to as $item) {
8873                        if (! in_array($item->email, $emails)) {
8874                            array_push($emails, $item->email);
8875                            $email->addTo($item->email);
8876                        }
8877                    }
8878
8879                    foreach ($cc as $item) {
8880                        if (! in_array($item->email, $emails)) {
8881                            array_push($emails, $item->email);
8882                            $email->addCc($item->email);
8883                        }
8884                    }
8885
8886                    $email->addCc($user->email);
8887                    array_push($emails, $user->email);
8888
8889                    $ccUser = TblUsers::where('name', $budget->commercial)->first();
8890
8891                    if ($ccUser) {
8892                        if (! in_array($ccUser->email, $emails)) {
8893                            $email->addCc($ccUser->email);
8894                        }
8895                    }
8896                }
8897
8898                $email->setFrom('fire@fire.es', 'Fire Service Titan');
8899                $email->setSubject($subject);
8900                $email->addContent('text/html', $html);
8901
8902                $email->addAttachment(
8903                    $imgpath,
8904                    'image/png',
8905                    'fireservicetitan.png',
8906                    'inline',
8907                    'fireservicetitan'
8908                );
8909
8910                $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
8911
8912                try {
8913                    $response = $sendgrid->send($email);
8914                    SendgridLogger::log($email, $response);
8915                } catch (\Throwable $sendException) {
8916                    SendgridLogger::logException($email, $sendException);
8917                    throw $sendException;
8918                }
8919
8920                if ($response->statusCode() == 202) {
8921                    $this->addUpdateLog($quotationId, $userId, 'send_acceptance_notification', null, null, 5);
8922                    $comment = 'Email de aprobación automática enviada al equipo de opearciones el '.date('Y-m-d H:i:s');
8923                    $budget->last_follow_up_comment = $budget->last_follow_up_comment."\n".$comment;
8924
8925                    TblQuotations::where('id', $quotationId)->update(
8926                        [
8927                            'last_follow_up_comment' => $budget->last_follow_up_comment,
8928                        ]
8929                    );
8930
8931                    Log::channel('email_log')->info('ID:'.$quoteId.' - ACCEPTANCE EMAIL NOTIFICATION SENT');
8932                } else {
8933                    $error = true;
8934                    Log::channel('email_log')->error('ID:'.$quoteId.' - '.$response->body());
8935                }
8936
8937            }
8938        }
8939    }
8940
8941    public function get_total_quotations_by_budget_status(Request $request): ResponseFactory|HttpResponse
8942    {
8943
8944        try {
8945
8946            $data = $request->all();
8947
8948            $companyId = addslashes((string) $data['company_id']);
8949
8950            $where = '';
8951
8952            if ($companyId != 0) {
8953                $where = " AND a.company_id = {$companyId} ";
8954            } else {
8955                $where = " AND a.company_id IN ({$this->companyId}";
8956            }
8957
8958            $user = null;
8959
8960            if (isset($data['commercial'])) {
8961                if ($data['commercial'] != 'All') {
8962                    $user = TblUsers::where('name', $data['commercial'])->first();
8963                }
8964            } else {
8965                $user = TblUsers::where('id', $this->userId)->first();
8966            }
8967
8968            $totalPendingFollowUps = 0;
8969            $totalRequestAndVisit = 0;
8970            $totalError = 0;
8971            $totalG3WError = 0;
8972            $totalSendToClient = 0;
8973
8974            $d = false;
8975
8976            if ($user != null) {
8977                $where .= " AND a.commercial = '{$user->name}";
8978                $d = true;
8979            }
8980
8981            if ($data['commercial'] == 'All') {
8982                $d = true;
8983            }
8984
8985            if ($d) {
8986                $blacklist = implode('|', $this->getBlacklistEmails());
8987                $query = "SELECT
8988                            COUNT(DISTINCT a.id) total
8989                        FROM
8990                            tbl_quotations a
8991                            LEFT JOIN (
8992                                SELECT
8993                                a.id,
8994                                SUBSTRING_INDEX(
8995                                    SUBSTRING_INDEX(a.email, ',', n.digit + 1),
8996                                    ',',
8997                                    -1
8998                                ) AS email_domain
8999                                FROM
9000                                tbl_quotations a
9001                                INNER JOIN (
9002                                    SELECT
9003                                    0 AS digit
9004                                    UNION ALL
9005                                    SELECT
9006                                    1
9007                                    UNION ALL
9008                                    SELECT
9009                                    2
9010                                    UNION ALL
9011                                    SELECT
9012                                    3
9013                                    UNION ALL
9014                                    SELECT
9015                                    4
9016                                    UNION ALL
9017                                    SELECT
9018                                    5
9019                                    UNION ALL
9020                                    SELECT
9021                                    6
9022                                    UNION ALL
9023                                    SELECT
9024                                    7
9025                                    UNION ALL
9026                                    SELECT
9027                                    8
9028                                    UNION ALL
9029                                    SELECT
9030                                    9
9031                                ) n ON LENGTH(
9032                                    REPLACE(a.email, ',', '')
9033                                ) <= LENGTH(a.email)- n.digit
9034                                GROUP BY a.id
9035                            ) temp ON a.id = temp.id
9036                            LEFT JOIN tbl_companies b
9037                                ON a.company_id = b.company_id
9038                        WHERE
9039                            a.last_follow_up_date < NOW()
9040                            AND a.budget_status_id IN (2)
9041                            AND a.email IS NOT NULL
9042                            AND a.email <> ''
9043                            AND NOT EXISTS (
9044                                SELECT
9045                                1
9046                                FROM
9047                                tbl_blocked_domains bd
9048                                WHERE
9049                                temp.email_domain LIKE CONCAT('%', bd.domain, '%')
9050                                AND bd.company_id = a.company_id
9051                            )
9052                            AND a.last_follow_up_date IS NOT NULL
9053                            AND a.reason_for_not_following_up_id IS NULL
9054                            AND a.last_follow_up_date > 0
9055                            AND a.total_sent < b.limit_reminder_emails
9056                            AND (
9057                                a.email IS NOT NULL
9058                                AND TRIM(a.email) != ''
9059                                AND a.email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}([[:space:]]*,[[:space:]]*[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})*$'
9060                                AND a.email NOT REGEXP '($blacklist)'
9061                            )
9062                            AND a.for_add = 0 {$where}";
9063
9064                $result = DB::select($query);
9065
9066                $totalPendingFollowUps = $result[0]->total;
9067
9068                $query = "SELECT
9069                                COUNT(1) total
9070                            FROM
9071                                tbl_quotations a
9072                            WHERE
9073                                a.budget_status_id IN (6, 8, 12)
9074                                AND a.for_add = 0
9075                                {$where}
9076                            ";
9077
9078                $result = DB::select($query);
9079
9080                $totalRequestAndVisit = $result[0]->total;
9081
9082                $blacklist = implode('|', $this->getBlacklistEmails());
9083
9084                // FIRE-864 follow-up (04/05): anchored email pattern so the
9085                // badge count agrees with the bucket reclassifier on emails
9086                // like `no tiene@notien.com`. See comment at the matching
9087                // list_quotations branch above.
9088                $query = "SELECT
9089                                COUNT(1) total
9090                            FROM
9091                                tbl_quotations a
9092                            WHERE
9093                                a.for_add = 0 AND
9094                                (
9095                                    a.x_status IN ('Error','Error - Bounce','Error - Spam')
9096                                    OR (
9097                                        a.email IS NULL
9098                                        OR TRIM(a.email) = ''
9099                                        OR a.email NOT REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}([[:space:]]*,[[:space:]]*[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})*$'
9100                                        OR a.email REGEXP '($blacklist)'
9101                                    )
9102                                )
9103                                AND a.budget_status_id IN(1, 2, 11, 17, 21, 22)
9104                                    {$where}
9105                            ";
9106
9107                $result = DB::select($query);
9108
9109                $totalError = $result[0]->total;
9110
9111                $query = "SELECT
9112                                COUNT(1) total
9113                                FROM
9114                                    tbl_quotations a
9115                                WHERE
9116                                    a.g3w_warning = 1
9117                                    AND a.for_add = 0
9118                                    {$where}
9119                                ";
9120
9121                $result = DB::select($query);
9122
9123                $totalG3WError = $result[0]->total;
9124
9125                $query = "SELECT
9126                                COUNT(1) total
9127                                FROM
9128                                    tbl_quotations a
9129                                WHERE
9130                                    a.budget_status_id = (SELECT budget_status_id FROM tbl_budget_status WHERE name = 'Validado')
9131                                    AND a.total_sent = 0
9132                                    AND a.for_add = 0
9133                                    {$where}
9134                                ";
9135
9136                $result = DB::select($query);
9137
9138                $totalSendToClient = $result[0]->total;
9139            }
9140
9141            return response([
9142                'message' => 'OK',
9143                'userId' => ($user) ? $user->id : null,
9144                'totalPendingFollowUps' => $totalPendingFollowUps,
9145                'totalRequestAndVisit' => $totalRequestAndVisit,
9146                'totalError' => $totalError,
9147                'totalG3WError' => $totalG3WError,
9148                'totalSendToClient' => $totalSendToClient,
9149            ]);
9150
9151        } catch (\Exception $e) {
9152            report(AppException::fromException($e, 'GET_TOTAL_QUPTATIONS_BY_BUDGET_STATUS_EXCEPTION'));
9153
9154            return response(['message' => 'KO', 'error' => $e->getMessage()]);
9155        }
9156
9157    }
9158
9159    public function sendgrid_webhook_receiver(Request $request)
9160    {
9161
9162        try {
9163
9164            $data = $request->all();
9165
9166            $jsonBody = [];
9167            $order = [];
9168            $orderEmails = [];
9169
9170            Log::channel('email_log')->info('WEBHOOK: '.json_encode($data));
9171
9172            $quoteId = null;
9173
9174            // FIRE-864: SendGrid bounce/drop events should flag the quotation
9175            // as "Correo erróneo" (budget_status_id = 22) so the sales flow
9176            // shows the truth instead of defaulting to "Enviado".
9177            $errorEvents = ['bounce', 'dropped', 'blocked', 'spamreport'];
9178
9179            foreach ($data as $item) {
9180                $matches = explode('.', (string) $item['sg_message_id']);
9181                $messageId = $matches[0];
9182
9183                Log::channel('email_log')->info('MESSAGE-ID: '.$messageId);
9184
9185                $result = TblSendgridWebhook::where('x_message_id', $messageId)->first();
9186                if (! $result) {
9187                    Log::channel('email_log')->warning("No tbl_sendgrid_webhook row for x_message_id={$messageId}. Skipping event.");
9188
9189                    continue;
9190                }
9191                $quoteId = $result->quotation_id;
9192                Log::channel('email_log')->info('SENDGRID-BODY: '.json_encode($result));
9193
9194                if (empty($order)) {
9195                    $order = TblQuotations::where('x_message_id', $messageId)->first();
9196                    if ($order) {
9197                        $quoteId = $order->id;
9198                        $orderEmails = explode(',', (string) $order->email);
9199                    }
9200                }
9201
9202                if ($result->json_body == null) {
9203                    array_push($jsonBody, $item);
9204                } else {
9205                    $jsonBody = json_decode((string) $result->json_body);
9206                    array_push($jsonBody, $item);
9207                }
9208
9209                Log::channel('email_log')->info('JSON-BODY: '.json_encode($jsonBody));
9210
9211                TblSendgridWebhook::where('x_message_id', $messageId)->update(
9212                    [
9213                        'json_body' => json_encode($jsonBody),
9214                        'updated_at' => date('Y-m-d H:i:s'),
9215                    ]
9216                );
9217
9218                // FIRE-864: mark quotation as "correo erróneo" (22) on bounce/drop.
9219                // FIRE-1072: do not clobber rows the commercial has manually
9220                // moved off the send pipeline. Status 3 (Aceptado) and 7
9221                // (Rechazado) are terminal, status 12 (En curso) means the
9222                // rep is working it, and 22 is already-classified. A late
9223                // SendGrid event on any of these is logged but does not
9224                // change budget_status_id.
9225                if ($quoteId !== null && isset($item['event']) && in_array(strtolower((string) $item['event']), $errorEvents, true)) {
9226                    $current = $order ?: TblQuotations::where('id', $quoteId)->first();
9227                    $protectedStatuses = [3, 7, 12, 22];
9228                    if ($current && ! in_array((int) $current->budget_status_id, $protectedStatuses, true)) {
9229                        TblQuotations::where('id', $quoteId)->update(['budget_status_id' => 22]);
9230                        $this->addUpdateLog($quoteId, 'SendGrid', 'budget_status_id', $current->budget_status_id, 22, 4);
9231                        Log::channel('email_log')->info("FIRE-864: quotation {$quoteId} → budget_status_id=22 (correo erróneo) due to SendGrid event '{$item['event']}'.");
9232                    } elseif ($current) {
9233                        Log::channel('email_log')->info("FIRE-1072: skipped demoting quotation {$quoteId} (current status {$current->budget_status_id} is protected) for SendGrid event '{$item['event']}'.");
9234                    }
9235                }
9236
9237                if ($quoteId != null) {
9238                    $this->get_files($quoteId);
9239                }
9240
9241                // FIRE-1145: was Cache::flush() — sendgrid webhook updates email status visible in list_quotations.
9242                ResultCache::forgetDomain('quotations');
9243            }
9244
9245        } catch (\Exception $e) {
9246            report(AppException::fromException($e, 'SENDGRID_WEBHOOK_RECEIVER_EXCEPTION'));
9247
9248            return response(['message' => 'KO', 'error' => $e->getMessage()]);
9249        }
9250
9251        return response(['message' => 'OK']);
9252    }
9253
9254    public function isEmailValid($email): bool
9255    {
9256        // Regular expression pattern for email validation
9257        $pattern = '/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/';
9258
9259        // Check if the email matches the pattern
9260        if (preg_match($pattern, (string) $email)) {
9261            return true; // Valid email
9262        } else {
9263            return false; // Invalid email
9264        }
9265    }
9266
9267    public function list_email_status($companyId): ResponseFactory|HttpResponse
9268    {
9269        try {
9270            $params = [];
9271
9272            if ($companyId != 0) {
9273                $where = ' company_id = ? ';
9274                $params[] = $companyId;
9275            } else {
9276                $ids = is_string($this->companyId) ? explode(',', $this->companyId) : $this->companyId;
9277                $ids = array_filter($ids);
9278
9279                if (empty($ids)) {
9280                    return response(['message' => 'OK', 'data' => []]);
9281                }
9282
9283                $placeholders = implode(',', array_fill(0, count($ids), '?'));
9284                $where = " company_id IN ($placeholders";
9285                $params = array_values($ids);
9286            }
9287
9288            $query = "SELECT DISTINCT x_status FROM tbl_quotations WHERE {$where} AND x_status IS NOT NULL";
9289            $result = DB::select($query, $params);
9290
9291            return response([
9292                'message' => 'OK',
9293                'data' => $result,
9294            ]);
9295
9296        } catch (\Exception $e) {
9297            report($e);
9298
9299            return response(['message' => 'KO', 'error' => $e->getMessage()], 500);
9300        }
9301    }
9302
9303    public function list_quotation_analytics_commercial(Request $request): ResponseFactory|HttpResponse
9304    {
9305
9306        try {
9307
9308            $data = $request->all();
9309            $companyId = addslashes((string) $data['company_id']);
9310            $field = $data['field'];
9311
9312            $where = '';
9313            $whereYear = '';
9314            $dateLflArray = [];
9315            $companyIds = $this->companyIds;
9316            $whereQ = '';
9317
9318            $acc = '';
9319            if ($field == 'acceptance_date') {
9320                $acc = ' AND q.acceptance_date IS NOT NULL ';
9321                // $field = 'created_at';
9322
9323                if (@$data['data_to_display'] == 4) {
9324                    $field = 'created_at';
9325                    $where .= ' AND YEAR(q.created_at) = YEAR(q.issue_date)';
9326                    $whereQ .= ' AND YEAR(q.created_at) = YEAR(q.issue_date)';
9327                }
9328            } else {
9329                $field = 'created_at';
9330                $where .= ' AND YEAR(q.created_at) = YEAR(q.issue_date)';
9331                $whereQ .= ' AND YEAR(q.created_at) = YEAR(q.issue_date)';
9332            }
9333
9334            if (isset($data['years']) && $data['years'] != null) {
9335                $years = implode(',', $data['years']);
9336                if (count($data['years']) > 0) {
9337                    $whereYear = " AND YEAR(q.{$field}) IN ({$years})";
9338                }
9339            }
9340
9341            if ($companyId != 0) {
9342                $companyIds = [$companyId];
9343            }
9344
9345            foreach ($companyIds as $v) {
9346
9347                $lflWhere = " AND q.company_id = {$v} ";
9348
9349                $query = "SELECT
9350                            CONCAT(
9351                                DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field}),
9352                                ' - ',
9353                                DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field})
9354                            ) AS date_like,
9355                            YEAR(q.{$field}) 'year',
9356                            DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS min_date_like,
9357                            DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS max_date_like,
9358                            {$v} 'company_id'
9359                        FROM
9360                            tbl_quotations q
9361                        WHERE
9362                            q.{$field} IS NOT NULL
9363                            AND q.for_add = 0
9364                            {$lflWhere}
9365                            {$whereYear}
9366                        GROUP BY YEAR(q.{$field})
9367                        ORDER BY YEAR(q.{$field}) DESC";
9368
9369                $dateLike = DB::select($query);
9370
9371                $dateLflArray[$v] = $dateLike;
9372            }
9373
9374            $isFy = true;
9375
9376            if (isset($data['ytd']) && $data['ytd'] != null && $data['ytd'] == true) {
9377                $isFy = false;
9378                $ytdArray = [];
9379                $ytdAcceptanceArray = [];
9380                $lflCompanyIds = [];
9381                foreach ($dateLflArray as $k => $v) {
9382                    foreach ($dateLflArray[$k] as $item) {
9383                        $year = $item->year;
9384                        $now = date('m-d');
9385                        array_push($ytdArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN '{$year}-01-01' AND '{$year}-{$now}'");
9386                    }
9387
9388                    $ytdArray = implode(' OR ', $ytdArray);
9389                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$ytdArray})");
9390                    $ytdArray = [];
9391                }
9392
9393                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
9394                $where .= " AND ({$lflCompanyIds}";
9395                $whereQ .= " AND ({$lflCompanyIds}";
9396            }
9397
9398            if (isset($data['lfl']) && $data['lfl'] != null && $data['lfl'] == true) {
9399                $isFy = false;
9400                $lflArray = [];
9401                $ytdAcceptanceArray = [];
9402                $lflCompanyIds = [];
9403                foreach ($dateLflArray as $k => $v) {
9404                    foreach ($dateLflArray[$k] as $item) {
9405                        $year = $item->year;
9406                        $min_date_like = $item->min_date_like;
9407                        $max_date_like = $item->max_date_like;
9408                        array_push($lflArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN LEAST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND GREATEST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}')");
9409                    }
9410
9411                    $lflArray = implode(' OR ', $lflArray);
9412                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$lflArray})");
9413                    $lflArray = [];
9414                }
9415
9416                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
9417                $where .= " AND ({$lflCompanyIds}";
9418                $whereQ .= " AND ({$lflCompanyIds}";
9419            }
9420
9421            if ($isFy) {
9422                if ($companyId != 0) {
9423                    $where .= " AND q.company_id = {$companyId} ";
9424                    $whereQ .= " AND q.company_id = {$companyId} ";
9425                } else {
9426                    $where .= " AND q.company_id IN ({$this->companyId})";
9427                    $whereQ .= " AND q.company_id IN ({$this->companyId})";
9428                }
9429            }
9430
9431            if (isset($data['source']) && $data['source'] != null) {
9432                $where .= " AND s.name = '{$data['source']}'";
9433            }
9434
9435            if (isset($data['month']) && $data['month'] != null) {
9436                $where .= " AND MONTH(q.{$field}) = '{$data['month']}'";
9437            }
9438
9439            if (isset($data['week']) && $data['week'] != null) {
9440                $where .= " AND WEEK(q.{$field}) = '{$data['week']}'";
9441            }
9442
9443            if (isset($data['commercial']) && $data['commercial'] != null) {
9444                $where .= " AND q.commercial = '{$data['commercial']}'";
9445            }
9446
9447            if (isset($data['created_by']) && $data['created_by'] != null) {
9448                $where .= " AND q.created_by = '{$data['created_by']}'";
9449            }
9450
9451            if (isset($data['budget_type']) && $data['budget_type'] != null) {
9452                $where .= " AND bt.budget_type_id = {$data['budget_type']}";
9453            }
9454
9455            if (isset($data['budget_type_group']) && $data['budget_type_group'] != null) {
9456                $where .= " AND bt.budget_type_group_id = {$data['budget_type_group']}";
9457            }
9458
9459            if (isset($data['budget_status']) && $data['budget_status'] != null) {
9460                $where .= " AND bs.budget_status_id = {$data['budget_status']}";
9461            }
9462
9463            if (isset($data['client_type']) && $data['client_type'] != null) {
9464                $where .= " AND ct.customer_type_id = {$data['client_type']}";
9465            }
9466
9467            if (isset($data['segment_id']) && $data['segment_id'] != null) {
9468                $where .= " AND q.segment_id = {$data['segment_id']}";
9469            }
9470
9471            $col = '1';
9472
9473            if (isset($data['data_to_display']) && $data['data_to_display'] != null) {
9474                if ($data['data_to_display'] == 1) {
9475                    $col = '1';
9476                }
9477
9478                if ($data['data_to_display'] == 2) {
9479                    $col = 'q.amount';
9480                }
9481            }
9482
9483            $query = "SELECT
9484                        q.commercial 'name',
9485                        COUNT(q.commercial) totalCommercial
9486                    FROM tbl_quotations q
9487                    WHERE
9488                        q.for_add = 0
9489                        AND q.{$field} IS NOT NULL
9490                        {$whereQ}
9491                    GROUP BY q.commercial
9492                    HAVING COUNT(q.commercial) > 0
9493                    ORDER BY totalCommercial DESC";
9494
9495            $resultCommercials = DB::select($query);
9496
9497            $now = TblQuotations::whereIn('company_id', $this->companyIds)->orderBy($field, 'DESC')->pluck($field)->first();
9498            $weekNumber = date('W', strtotime((string) $now));
9499            $thisWeek = date('Y-m-d', strtotime($now.' - '.(date('N', strtotime((string) $now)) - 1).' days'));
9500
9501            $query = "SELECT
9502                        q.commercial 'name',
9503                        COUNT(q.commercial) totalCommercial
9504                    FROM tbl_quotations q
9505                    WHERE
9506                        q.for_add = 0
9507                        AND q.{$field} IS NOT NULL
9508                        AND YEARWEEK(q.{$field}, 1) = YEARWEEK(NOW(), 1)
9509                        {$whereQ}
9510                    GROUP BY q.commercial
9511                    HAVING COUNT(q.commercial) > 0
9512                    ORDER BY totalCommercial DESC";
9513
9514            $resultCommercialsOrder = DB::select($query);
9515
9516            $namesToRemove = [];
9517
9518            foreach ($resultCommercialsOrder as $item) {
9519                $namesToRemove[$item->name] = true;
9520            }
9521
9522            $resultArray = [];
9523            foreach ($resultCommercials as $item) {
9524                if (! isset($namesToRemove[$item->name])) {
9525                    $resultArray[] = $item;
9526                }
9527            }
9528
9529            $resultCommercials = array_merge($resultCommercialsOrder, $resultArray);
9530
9531            $cols = '';
9532
9533            $colsGroupConcatIds = '';
9534
9535            foreach ($resultCommercials as $item) {
9536                $cols .= ",COALESCE(
9537                    SUM(
9538                        CASE WHEN q.commercial = '{$item->name}{$acc} THEN {$col} ELSE 0 END
9539                    ), 0
9540                ) '{$item->name}'";
9541
9542                $colsGroupConcatIds .= ",GROUP_CONCAT(CASE WHEN q.commercial = '{$item->name}{$acc} THEN q.id END) 'groupConcatIds-{$item->name}'";
9543            }
9544
9545            $cols .= ",COALESCE(
9546                SUM(
9547                    CASE WHEN q.commercial IS NOT NULL {$acc} THEN {$col} ELSE 0 END
9548                ), 0
9549            ) total";
9550
9551            if (@$data['data_to_display'] == 3) {
9552
9553                $cols = '';
9554
9555                foreach ($resultCommercials as $item) {
9556                    $cols .= ",COALESCE(
9557                        (
9558                            SUM(CASE WHEN q.commercial = '{$item->name}{$acc} THEN q.amount END)
9559                        ) /
9560                        (
9561                            SUM(CASE WHEN q.commercial = '{$item->name}{$acc} THEN 1 END)
9562                        )
9563                    , 0) '{$item->name}'";
9564                }
9565
9566                $cols .= ",COALESCE(
9567                    (
9568                        SUM(CASE WHEN q.commercial IS NOT NULL {$acc} THEN q.amount END)
9569                    ) /
9570                    (
9571                        SUM(CASE WHEN q.commercial IS NOT NULL {$acc} THEN 1 END)
9572                    )
9573                , 0) total";
9574            }
9575
9576            if (@$data['data_to_display'] == 4) {
9577
9578                $cols = '';
9579
9580                foreach ($resultCommercials as $item) {
9581                    $cols .= ",COALESCE(
9582                        (
9583                            SUM(CASE WHEN q.commercial = '{$item->name}' AND q.acceptance_date IS NOT NULL THEN 1 END)
9584                        ) /
9585                        (
9586                            SUM(CASE WHEN q.commercial = '{$item->name}' AND q.created_at IS NOT NULL THEN 1 END)
9587                        ) * 100
9588                    , 0) '{$item->name}'";
9589                }
9590
9591                $cols .= ',COALESCE(
9592                    (
9593                        SUM(CASE WHEN q.commercial IS NOT NULL AND q.acceptance_date IS NOT NULL THEN 1 END)
9594                    ) /
9595                    (
9596                        SUM(CASE WHEN q.commercial IS NOT NULL AND q.created_at IS NOT NULL THEN 1 END)
9597                    ) * 100
9598                , 0) total';
9599            }
9600
9601            $query = "SELECT
9602                            YEAR(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)) AS 'year',
9603                            LPAD(MONTH(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)), 2, 0) AS 'month',
9604                            LPAD(WEEK(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)), 2, 0) AS 'week',
9605                            DATE_FORMAT(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY), '%W, %M %e') 'namedate',
9606                            GROUP_CONCAT(
9607                                CASE WHEN q.{$field} IS NOT NULL
9608                                THEN q.id END
9609                            ) AS groupConcatIds
9610                            {$colsGroupConcatIds}
9611                            {$cols}
9612                        FROM
9613                            tbl_quotations q
9614                            LEFT JOIN tbl_sources s ON s.source_id = q.source_id
9615                            LEFT JOIN tbl_budget_status bs ON bs.budget_status_id = q.budget_status_id
9616                            LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
9617                            LEFT JOIN tbl_budget_type_groups btg ON bt.budget_type_group_id = btg.budget_type_group_id
9618                            LEFT JOIN tbl_customer_types ct ON q.customer_type_id = ct.customer_type_id
9619                        WHERE
9620                            q.{$field} IS NOT NULL
9621                            AND q.for_add = 0
9622                            AND q.budget_type_id != 7
9623                            AND q.budget_type_id IS NOT NULL
9624                            AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
9625                            {$where}
9626                            {$whereYear}
9627                        GROUP BY
9628                            YEAR(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)),
9629                            MONTH(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)),
9630                            WEEK(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)) WITH ROLLUP
9631                        ORDER BY
9632                            YEAR DESC,
9633                            MONTH ASC,
9634                            WEEK ASC,
9635                            DATE_FORMAT(q.{$field}, '%e') ASC";
9636
9637            // FIRE-1145: domain-tagged cache (was Cache::get/put + Cache::flush()).
9638            $result = ResultCache::remember('quotations', $query, 600, fn () => DB::select($query));
9639
9640            return response([
9641                'message' => 'OK',
9642                'data' => $result,
9643                'commercials' => $resultCommercials,
9644            ]);
9645
9646        } catch (\Exception $e) {
9647            report(AppException::fromException($e, 'LIST_QUOTATION_ANALYTICS_COMMERCIAL_EXCEPTION'));
9648
9649            return response(['message' => 'KO', 'error' => $e->getMessage()]);
9650        }
9651    }
9652
9653    public function clear_open_data($companyId): ResponseFactory|HttpResponse
9654    {
9655
9656        try {
9657
9658            $companyIds = [$companyId];
9659            if ($companyId == 0) {
9660                $companyIds = $this->companyIds;
9661            }
9662
9663            $user = TblUsers::where('id', $this->userId)->first();
9664
9665            if (count($companyIds) > 0) {
9666
9667                foreach ($companyIds as $id) {
9668                    $startedAt = date('Y-m-d H:i:s');
9669                    $affectedRows = DB::delete("DELETE FROM tbl_quotations WHERE company_id = {$id} AND for_add = 1 AND DATE_FORMAT(created_at, '%Y-%m-%d') != DATE_FORMAT(NOW(), '%Y-%m-%d')");
9670
9671                    TblOrdersUpdateLogs::create(
9672                        [
9673                            'company_id' => $id,
9674                            'to_process' => 'Orders',
9675                            'status' => 'success',
9676                            'for_add_deleted_affected_rows' => $affectedRows,
9677                            'processed_by' => $user->name,
9678                            'started_at' => $startedAt,
9679                            'ended_at' => date('Y-m-d H:i:s'),
9680                        ]
9681                    );
9682                }
9683            }
9684
9685            return response([
9686                'message' => 'OK',
9687            ]);
9688
9689        } catch (\Exception $e) {
9690            report(AppException::fromException($e, 'CLEAR_OPEN_DATA_EXCEPTION'));
9691
9692            return response(['message' => 'KO', 'error' => $e->getMessage()]);
9693        }
9694
9695    }
9696
9697    public function list_quotation_analytics_order_size(Request $request): ResponseFactory|HttpResponse
9698    {
9699
9700        try {
9701
9702            $data = $request->all();
9703            $companyId = addslashes((string) $data['company_id']);
9704            $field = $data['field'];
9705
9706            $where = '';
9707            $dateLflArray = [];
9708            $whereYear = '';
9709            $companyIds = $this->companyIds;
9710
9711            if ($field == 'acceptance_date') {
9712                if (@$data['data_to_display'] == 4) {
9713                    $field = 'created_at';
9714                    $where .= ' AND YEAR(q.created_at) = YEAR(q.issue_date)';
9715                }
9716            } else {
9717                $field = 'created_at';
9718                $where .= ' AND YEAR(q.created_at) = YEAR(q.issue_date)';
9719            }
9720
9721            if (isset($data['years']) && $data['years'] != null) {
9722                $years = implode(',', $data['years']);
9723                if (count($data['years']) > 0) {
9724                    $whereYear = " AND YEAR(q.{$field}) IN ({$years})";
9725                }
9726            }
9727
9728            if ($companyId != 0) {
9729                $companyIds = [$companyId];
9730            }
9731
9732            foreach ($companyIds as $v) {
9733
9734                $lflWhere = " AND q.company_id = {$v} ";
9735
9736                $query = "SELECT
9737                            CONCAT(
9738                                DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field}),
9739                                ' - ',
9740                                DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field})
9741                            ) AS date_like,
9742                            YEAR(q.{$field}) 'year',
9743                            DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS min_date_like,
9744                            DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS max_date_like,
9745                            {$v} 'company_id'
9746                        FROM
9747                            tbl_quotations q
9748                        WHERE
9749                            q.{$field} IS NOT NULL
9750                            AND q.for_add = 0
9751                            {$lflWhere}
9752                            {$whereYear}
9753                        GROUP BY YEAR(q.{$field})
9754                        ORDER BY YEAR(q.{$field}) DESC";
9755
9756                $dateLike = DB::select($query);
9757
9758                $dateLflArray[$v] = $dateLike;
9759            }
9760
9761            $isFy = true;
9762
9763            if (isset($data['ytd']) && $data['ytd'] != null && $data['ytd'] == true) {
9764                $isFy = false;
9765                $ytdArray = [];
9766                $ytdAcceptanceArray = [];
9767                $lflCompanyIds = [];
9768                foreach ($dateLflArray as $k => $v) {
9769                    foreach ($dateLflArray[$k] as $item) {
9770                        $year = $item->year;
9771                        $now = date('m-d');
9772                        array_push($ytdArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN '{$year}-01-01' AND '{$year}-{$now}'");
9773                    }
9774
9775                    $ytdArray = implode(' OR ', $ytdArray);
9776                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$ytdArray})");
9777                    $ytdArray = [];
9778                }
9779
9780                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
9781                $where .= " AND ({$lflCompanyIds}";
9782            }
9783
9784            if (isset($data['lfl']) && $data['lfl'] != null && $data['lfl'] == true) {
9785                $isFy = false;
9786                $lflArray = [];
9787                $ytdAcceptanceArray = [];
9788                $lflCompanyIds = [];
9789                foreach ($dateLflArray as $k => $v) {
9790                    foreach ($dateLflArray[$k] as $item) {
9791                        $year = $item->year;
9792                        $min_date_like = $item->min_date_like;
9793                        $max_date_like = $item->max_date_like;
9794                        array_push($lflArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN LEAST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND GREATEST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}')");
9795                    }
9796
9797                    $lflArray = implode(' OR ', $lflArray);
9798                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$lflArray})");
9799                    $lflArray = [];
9800                }
9801
9802                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
9803                $where .= " AND ({$lflCompanyIds}";
9804            }
9805
9806            if ($isFy) {
9807                if ($companyId != 0) {
9808                    $where .= " AND q.company_id = {$companyId} ";
9809                } else {
9810                    $where .= " AND q.company_id IN ({$this->companyId})";
9811                }
9812            }
9813
9814            if (isset($data['source']) && $data['source'] != null) {
9815                $where .= " AND s.name = '{$data['source']}'";
9816            }
9817
9818            if (isset($data['source']) && $data['source'] != null) {
9819                $where .= " AND s.name = '{$data['source']}'";
9820            }
9821
9822            if (isset($data['commercial']) && $data['commercial'] != null) {
9823                $where .= " AND q.commercial = '{$data['commercial']}'";
9824            }
9825
9826            if (isset($data['created_by']) && $data['created_by'] != null) {
9827                $where .= " AND q.created_by = '{$data['created_by']}'";
9828            }
9829
9830            if (isset($data['budget_type']) && $data['budget_type'] != null) {
9831                $where .= " AND bt.budget_type_id = {$data['budget_type']}";
9832            }
9833
9834            if (isset($data['budget_type_group']) && $data['budget_type_group'] != null) {
9835                $where .= " AND bt.budget_type_group_id = {$data['budget_type_group']}";
9836            }
9837
9838            if (isset($data['budget_status']) && $data['budget_status'] != null) {
9839                $where .= " AND bs.budget_status_id = {$data['budget_status']}";
9840            }
9841
9842            if (isset($data['client_type']) && $data['client_type'] != null) {
9843                $where .= " AND ct.customer_type_id = {$data['client_type']}";
9844            }
9845
9846            if (isset($data['segment_id']) && $data['segment_id'] != null) {
9847                $where .= " AND q.segment_id = {$data['segment_id']}";
9848            }
9849
9850            $col = 'q.one';
9851
9852            if (isset($data['data_to_display']) && $data['data_to_display'] != null) {
9853                if ($data['data_to_display'] == 1) {
9854                    $col = 'q.one';
9855                }
9856
9857                if ($data['data_to_display'] == 2) {
9858                    $col = 'q.amount';
9859                }
9860            }
9861
9862            if ((isset($data['start_date']) && $data['start_date'] != null) && isset($data['end_date']) && $data['end_date'] != null) {
9863                $where .= " AND q.{$field} BETWEEN '{$data['start_date']}' AND '{$data['end_date']}";
9864            } elseif ((isset($data['start_date']) && $data['start_date'] == null || $data['start_date'] == '') && isset($data['end_date']) && $data['end_date'] != null) {
9865                $where .= " AND q.{$field} = '{$data['end_date']}";
9866            }
9867
9868            $whereQ = $where;
9869
9870            $sortBy = [
9871                0 => 'q.amount < 100',
9872                1 => 'q.amount BETWEEN 100 AND 500',
9873                2 => 'q.amount BETWEEN 500 AND 2000',
9874                3 => 'q.amount BETWEEN 2000 AND 10000',
9875                4 => 'q.amount BETWEEN 10000 AND 30000',
9876                5 => 'q.amount BETWEEN 30000 AND 100000',
9877                6 => 'q.amount BETWEEN 100000 AND 999999999999',
9878            ];
9879
9880            $query = "SELECT
9881                        q.commercial 'name',
9882                        COUNT(q.commercial) totalCommercial
9883                    FROM tbl_quotations q
9884                    LEFT JOIN tbl_sources s ON s.source_id = q.source_id
9885                    LEFT JOIN tbl_budget_status bs ON bs.budget_status_id = q.budget_status_id
9886                    LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
9887                    LEFT JOIN tbl_customer_types ct ON q.customer_type_id = ct.customer_type_id
9888                    WHERE
9889                        q.for_add = 0
9890                        AND q.{$field} IS NOT NULL                        
9891                        AND q.budget_type_id != 7
9892                        AND q.budget_type_id IS NOT NULL
9893                        {$whereQ}
9894                        {$whereYear}
9895                    GROUP BY q.commercial
9896                    HAVING COUNT(q.commercial) > 0
9897                    ORDER BY totalCommercial DESC";
9898
9899            $resultCommercials = DB::select($query);
9900
9901            if (isset($data['sort_by'])) {
9902                if ($data['sort_by'] != 7) {
9903                    $s = $sortBy[$data['sort_by']];
9904                    $whereQ .= " AND {$s} ";
9905                }
9906            }
9907
9908            $num = 'COUNT(1)';
9909
9910            if (@$data['data_to_display'] == 2) {
9911                $num = 'SUM(q.amount)';
9912            }
9913
9914            if (@$data['data_to_display'] == 3) {
9915                $num = 'SUM(q.amount) / COUNT(1)';
9916            }
9917
9918            if (@$data['data_to_display'] == 4) {
9919                $num = '(SUM(CASE WHEN q.acceptance_date IS NOT NULL THEN 1 ELSE 0 END) / COUNT(q.created_at) * 100)';
9920            }
9921
9922            $query = "SELECT
9923                    q.commercial 'name',
9924                    COUNT(q.commercial) totalCommercial,
9925                    {$num} num
9926                FROM tbl_quotations q
9927                LEFT JOIN tbl_sources s ON s.source_id = q.source_id
9928                LEFT JOIN tbl_budget_status bs ON bs.budget_status_id = q.budget_status_id
9929                LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
9930                LEFT JOIN tbl_customer_types ct ON q.customer_type_id = ct.customer_type_id
9931                WHERE
9932                    q.for_add = 0
9933                    AND q.{$field} IS NOT NULL                    
9934                    AND q.budget_type_id != 7
9935                    AND q.budget_type_id IS NOT NULL
9936                    {$whereQ}
9937                    {$whereYear}
9938                GROUP BY q.commercial
9939                HAVING COUNT(q.commercial) > 0
9940                ORDER BY num DESC";
9941
9942            $resultCommercialsOrder = DB::select($query);
9943
9944            foreach ($resultCommercialsOrder as $item) {
9945                $namesToRemove[$item->name] = true;
9946            }
9947
9948            $resultArray = [];
9949            foreach ($resultCommercials as $item) {
9950                if (! isset($namesToRemove[$item->name])) {
9951                    $resultArray[] = $item;
9952                }
9953            }
9954
9955            $resultCommercials = array_merge($resultCommercialsOrder, $resultArray);
9956
9957            $cols = '';
9958
9959            $colsGroupConcatIds = '';
9960
9961            foreach ($resultCommercials as $item) {
9962                $cols .= ",COALESCE(
9963                    SUM(
9964                        CASE WHEN q.commercial = '{$item->name}' THEN {$col} ELSE 0 END
9965                    ), 0
9966                ) '{$item->name}'";
9967
9968                $colsGroupConcatIds .= ",GROUP_CONCAT(CASE WHEN q.commercial = '{$item->name}' THEN q.id END) 'groupConcatIds-{$item->name}'";
9969            }
9970
9971            $cols .= ",COALESCE(
9972                SUM(
9973                    CASE WHEN q.commercial IS NOT NULL THEN {$col} ELSE 0 END
9974                ), 0
9975            ) total";
9976
9977            $range = "CASE
9978                        WHEN amount < 100 THEN '< 100€'
9979                        WHEN amount BETWEEN 100 AND 500 THEN '100€ - 500€'
9980                        WHEN amount BETWEEN 500 AND 2000 THEN '500€ - 2k€'
9981                        WHEN amount BETWEEN 2000 AND 10000 THEN '2k€ - 10k€'
9982                        WHEN amount BETWEEN 10000 AND 30000 THEN '10k€ - 30k€'
9983                        WHEN amount BETWEEN 30000 AND 100000 THEN '30k€ - 100k€'
9984                        WHEN amount BETWEEN 100000 AND 999999999999 THEN '> 100k€'
9985                    END AS amount_range";
9986
9987            if (@$data['data_to_display'] == 3) {
9988
9989                $range = "CASE
9990                            WHEN SUM(amount) / COUNT(1) < 100 THEN '< 100€'
9991                            WHEN SUM(amount) / COUNT(1) BETWEEN 100 AND 500 THEN '100€ - 500€'
9992                            WHEN SUM(amount) / COUNT(1) BETWEEN 500 AND 2000 THEN '500€ - 2k€'
9993                            WHEN SUM(amount) / COUNT(1) BETWEEN 2000 AND 10000 THEN '2k€ - 10k€'
9994                            WHEN SUM(amount) / COUNT(1) BETWEEN 10000 AND 30000 THEN '10k€ - 30k€'
9995                            WHEN SUM(amount) / COUNT(1) BETWEEN 30000 AND 100000 THEN '30k€ - 100k€'
9996                            WHEN SUM(amount) / COUNT(1) BETWEEN 100000 AND 999999999999 THEN '> 100k€'
9997                        END AS amount_range";
9998
9999                $cols = '';
10000
10001                foreach ($resultCommercials as $item) {
10002                    $cols .= ",COALESCE(
10003                        (
10004                            SUM(CASE WHEN q.commercial = '{$item->name}' THEN q.amount END)
10005                        ) /
10006                        (
10007                            SUM(CASE WHEN q.commercial = '{$item->name}' THEN q.one END)
10008                        )
10009                    , 0) '{$item->name}'";
10010                }
10011
10012                $cols .= ',COALESCE(
10013                    (
10014                        SUM(CASE WHEN q.commercial IS NOT NULL THEN q.amount END)
10015                    ) /
10016                    (
10017                        SUM(CASE WHEN q.commercial IS NOT NULL THEN q.one END)
10018                    )
10019                , 0) total';
10020            }
10021
10022            if (@$data['data_to_display'] == 4) {
10023
10024                $cols = '';
10025
10026                foreach ($resultCommercials as $item) {
10027                    $cols .= ",COALESCE(
10028                        (
10029                            SUM(CASE WHEN q.commercial = '{$item->name}' THEN q.acceptanceDate END)
10030                        ) /
10031                        (
10032                            SUM(CASE WHEN q.commercial = '{$item->name}' THEN q.createdAt END)
10033                        ) * 100
10034                    , 0) '{$item->name}'";
10035                }
10036
10037                $cols .= ',COALESCE(
10038                    (
10039                        SUM(CASE WHEN q.commercial IS NOT NULL THEN q.acceptanceDate END)
10040                    ) /
10041                    (
10042                        SUM(CASE WHEN q.commercial IS NOT NULL THEN q.createdAt END)
10043                    ) * 100
10044                , 0) total';
10045            }
10046
10047            $query = "WITH amount_ranges AS (
10048                            SELECT '< 100€' AS amount_range
10049                            UNION ALL SELECT '100€ - 500€'
10050                            UNION ALL SELECT '500€ - 2k€'
10051                            UNION ALL SELECT '2k€ - 10k€'
10052                            UNION ALL SELECT '10k€ - 30k€'
10053                            UNION ALL SELECT '30k€ - 100k€'
10054                            UNION ALL SELECT '> 100k€'
10055                        )
10056
10057                        SELECT
10058                            ar.amount_range,
10059                             GROUP_CONCAT(
10060                                q.id
10061                            ) AS groupConcatIds
10062                            {$colsGroupConcatIds}
10063                            {$cols}
10064                        FROM
10065                            amount_ranges ar
10066                        LEFT JOIN (
10067                            SELECT
10068                                commercial,
10069                                GROUP_CONCAT(
10070                                    CASE WHEN q.{$field} IS NOT NULL THEN id END
10071                                ) id,
10072                                amount AS amount,
10073                                COUNT(CASE WHEN q.created_at IS NOT NULL THEN 1 END) createdAt,
10074                                COUNT(CASE WHEN q.acceptance_date IS NOT NULL THEN 1 END) acceptanceDate,
10075                                COUNT(1) AS one,
10076                                {$range}
10077                            FROM
10078                                tbl_quotations q
10079                                LEFT JOIN tbl_sources s ON s.source_id = q.source_id
10080                                LEFT JOIN tbl_budget_status bs ON bs.budget_status_id = q.budget_status_id
10081                                LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
10082                                LEFT JOIN tbl_customer_types ct ON q.customer_type_id = ct.customer_type_id
10083                            WHERE
10084                                q.{$field} IS NOT NULL
10085                                AND q.for_add = 0                                
10086                                AND q.budget_type_id != 7
10087                                AND q.budget_type_id IS NOT NULL
10088                                AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
10089                                {$where}
10090                                {$whereYear}
10091                            GROUP BY
10092                                commercial,
10093                                id
10094                        ) AS q ON ar.amount_range = q.amount_range
10095                        GROUP BY
10096                            ar.amount_range WITH ROLLUP
10097                        ORDER BY
10098                            CASE
10099                                WHEN ar.amount_range = '< 100€' THEN 1
10100                                WHEN ar.amount_range = '100€ - 500€' THEN 2
10101                                WHEN ar.amount_range = '500€ - 2k€' THEN 3
10102                                WHEN ar.amount_range = '2k€ - 10k€' THEN 4
10103                                WHEN ar.amount_range = '10k€ - 30k€' THEN 5
10104                                WHEN ar.amount_range = '30k€ - 100k€' THEN 6
10105                                ELSE 7
10106                            END";
10107
10108            // FIRE-1145: domain-tagged cache (was Cache::get/put + Cache::flush()).
10109            $result = ResultCache::remember('quotations', $query, 600, fn () => DB::select($query));
10110
10111            return response([
10112                'message' => 'OK',
10113                'data' => $result,
10114                'commercials' => $resultCommercials,
10115            ]);
10116
10117        } catch (\Exception $e) {
10118            report(AppException::fromException($e, 'LIST_QUOTATION_ANALYTICS_COMMERCIAL_EXCEPTION'));
10119
10120            return response(['message' => 'KO', 'error' => $e->getMessage()]);
10121        }
10122
10123    }
10124
10125    public function send_email_template_preview($emailTemplateId): ResponseFactory|HttpResponse
10126    {
10127
10128        try {
10129
10130            $emailTemplateId = addslashes((string) $emailTemplateId);
10131
10132            $emailTemplate = TblEmailConfiguration::where('id', $emailTemplateId)->first();
10133
10134            $user = TblUsers::where('id', $this->userId)->first();
10135            $error = false;
10136
10137            $toEmail = $user->email;
10138
10139            $availableParameters = [
10140                'quote_id',
10141                'company_id',
10142                'client',
10143                'client_type',
10144                'phone_number',
10145                'email',
10146                'issue_date',
10147                'request_date',
10148                'duration',
10149                'invoice_number',
10150                'type',
10151                'acceptance_date',
10152                'status',
10153                'source',
10154                'amount',
10155                'reason_for_not_following_up',
10156                'last_follow_up_date',
10157                'last_follow_up_comment',
10158                'reason_for_rejection_id',
10159                'reason_for_rejection',
10160                'commercial',
10161                'created_at',
10162                'created_by',
10163                'updated_at',
10164                'updated_by',
10165            ];
10166
10167            $dateParameters = [
10168                'issue_date',
10169                'request_date',
10170                'acceptance_date',
10171                'last_follow_up_date',
10172                'created_at',
10173                'updated_at',
10174            ];
10175
10176            if ($this->locale == 'es') {
10177                setlocale(LC_ALL, 'es_ES', 'Spanish_Spain', 'Spanish');
10178                setlocale(LC_ALL, 'es_ES', 'Spanish_Spain', 'Spanish');
10179            }
10180
10181            $body = $emailTemplate->html;
10182            $subject = $emailTemplate->subject;
10183
10184            preg_match_all('/{{(.*?)}}/', (string) $body, $matches);
10185
10186            $parameters = $matches[1];
10187
10188            $result = TblQuotations::where('for_add', 0)->whereIn('company_id', $this->companyIds)->first();
10189
10190            foreach ($parameters as $parameter) {
10191
10192                if (in_array($parameter, $dateParameters)) {
10193                    if ($result->{$parameter}) {
10194                        $result->{$parameter} = iconv('ISO-8859-2', 'UTF-8', strftime('%A, %B %d, %Y', strtotime((string) $result->{$parameter})));
10195                    }
10196                }
10197
10198                if (in_array($parameter, $availableParameters)) {
10199                    $body = str_replace('{{'.$parameter.'}}', $result->{$parameter}, $body);
10200                }
10201            }
10202
10203            preg_match_all('/{{(.*?)}}/', (string) $subject, $matches);
10204
10205            $parameters = $matches[1];
10206
10207            foreach ($parameters as $parameter) {
10208
10209                if (in_array($parameter, $dateParameters)) {
10210                    if ($result->{$parameter}) {
10211                        $result->{$parameter} = iconv('ISO-8859-2', 'UTF-8', strftime('%A, %B %d, %Y', strtotime((string) $result->{$parameter})));
10212                    }
10213                }
10214
10215                if (in_array($parameter, $availableParameters)) {
10216                    $subject = str_replace('{{'.$parameter.'}}', $result->{$parameter}, $subject);
10217                }
10218            }
10219
10220            $email = new Mail;
10221
10222            $templateFiles = TblEmailFiles::where('email_template_id', $emailTemplateId)->orderBy('order', 'asc')->get();
10223
10224            foreach ($templateFiles as $item) {
10225                $f = storage_path('app/public/uploads/'.$item->filename);
10226                $imgpath = file_get_contents($f);
10227                $mimeType = mime_content_type($f);
10228
10229                $email->addAttachment(
10230                    $imgpath,
10231                    $mimeType,
10232                    str_replace(' ', '', $item->original_name),
10233                    'inline',
10234                    str_replace(' ', '', $item->original_name),
10235                );
10236
10237                $body .= "<img src='cid:{$item->original_name}' style='height: 45px; padding-right: 6px' />";
10238            }
10239
10240            $html = '<!DOCTYPE html>';
10241            $html .= '<html>';
10242            $html .= '<head>';
10243            $html .= '<meta charset="UTF-8">';
10244            $html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
10245            $html .= '</head>';
10246            $html .= '<body>';
10247            $html .= $body;
10248            $html .= '</body>';
10249            $html .= '</html>';
10250
10251            if ($toEmail != null) {
10252
10253                $companyEmail = null;
10254
10255                if ($emailTemplate->from_id != null) {
10256                    $companyEmail = TblCompanyEmails::where('id', $emailTemplate->from_id)->first();
10257                } else {
10258                    $companyEmail = TblCompanyEmails::where('is_active', 1)->where('verified', 1)->where('company_id', $result->company_id)->first();
10259                }
10260
10261                if (! $companyEmail) {
10262                    return response(['message' => 'KO', 'error' => __('language.no_active_verified_sender')]);
10263                }
10264
10265                $email->setFrom($companyEmail->from_email, $companyEmail->from_name);
10266                $email->setSubject($subject);
10267                $email->addTo($toEmail);
10268                $email->addContent('text/html', $html);
10269
10270                $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
10271
10272                try {
10273                    $response = $sendgrid->send($email);
10274                    SendgridLogger::log($email, $response);
10275                } catch (\Throwable $sendException) {
10276                    SendgridLogger::logException($email, $sendException);
10277                    throw $sendException;
10278                }
10279
10280                if ($response->statusCode() == 202) {
10281                    return response(['message' => 'OK']);
10282                }
10283            }
10284
10285            return response(['message' => 'KO']);
10286
10287        } catch (\Exception $e) {
10288            report(AppException::fromException($e, 'SEND_EMAIL_TEMPLATE_EXCEPTION'));
10289
10290            return response(['message' => 'KO', 'error' => $e->getMessage()]);
10291        }
10292
10293    }
10294
10295    public function list_quotation_analytics_by_types_of_budgets_company_per_week(Request $request): ResponseFactory|HttpResponse
10296    {
10297
10298        try {
10299
10300            $data = $request->all();
10301            $companyId = addslashes((string) $data['company_id']);
10302            $field = $data['field'];
10303
10304            $where = '';
10305            $whereYear = '';
10306            $dateLflArray = [];
10307            $companyIds = $this->companyIds;
10308
10309            $acc = '';
10310            if ($field == 'acceptance_date') {
10311                $acc = ' AND q.acceptance_date IS NOT NULL ';
10312                // $field = 'created_at';
10313
10314                if (@$data['data_to_display'] == 4) {
10315                    $field = 'created_at';
10316                    $where .= ' AND YEAR(q.created_at) = YEAR(q.issue_date)';
10317                }
10318            } else {
10319                $field = 'created_at';
10320                $where .= ' AND YEAR(q.created_at) = YEAR(q.issue_date)';
10321            }
10322
10323            if ($companyId != 0) {
10324                $companyIds = [$companyId];
10325            }
10326
10327            if (isset($data['years']) && $data['years'] != null) {
10328
10329                if (count($data['years']) > 0) {
10330                    foreach ($data['years'] as $year) {
10331                        if (isset($data['week']) && $data['week'] != null) {
10332                            $w = sprintf('%02d', $data['week']);
10333                            $whereYear .= " AND YEARWEEK(q.{$field}, 1) = '{$year}{$w}'";
10334                        } else {
10335                            $whereYear .= " AND YEAR(q.{$field}) = {$year}";
10336                        }
10337                    }
10338                }
10339            }
10340
10341            foreach ($companyIds as $v) {
10342
10343                $lflWhere = " AND q.company_id = {$v} ";
10344
10345                $query = "SELECT
10346                            CONCAT(
10347                                DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field}),
10348                                ' - ',
10349                                DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field})
10350                            ) AS date_like,
10351                            YEAR(q.{$field}) 'year',
10352                            DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS min_date_like,
10353                            DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS max_date_like,
10354                            {$v} 'company_id'
10355                        FROM
10356                            tbl_quotations q
10357                        WHERE
10358                            q.{$field} IS NOT NULL
10359                            AND q.for_add = 0
10360                            {$lflWhere}
10361                            {$whereYear}
10362                        GROUP BY YEAR(q.{$field})
10363                        ORDER BY YEAR(q.{$field}) DESC";
10364
10365                $dateLike = DB::select($query);
10366
10367                $dateLflArray[$v] = $dateLike;
10368            }
10369
10370            $isFy = true;
10371
10372            if (isset($data['ytd']) && $data['ytd'] != null && $data['ytd'] == true) {
10373                $isFy = false;
10374                $ytdArray = [];
10375                $ytdAcceptanceArray = [];
10376                $lflCompanyIds = [];
10377                foreach ($dateLflArray as $k => $v) {
10378                    foreach ($dateLflArray[$k] as $item) {
10379                        $year = $item->year;
10380                        $now = date('m-d');
10381                        array_push($ytdArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN '{$year}-01-01' AND '{$year}-{$now}'");
10382                    }
10383
10384                    $ytdArray = implode(' OR ', $ytdArray);
10385                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$ytdArray})");
10386                    $ytdArray = [];
10387                }
10388
10389                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
10390                $where .= " AND ({$lflCompanyIds}";
10391            }
10392
10393            if (isset($data['lfl']) && $data['lfl'] != null && $data['lfl'] == true) {
10394                $isFy = false;
10395                $lflArray = [];
10396                $ytdAcceptanceArray = [];
10397                $lflCompanyIds = [];
10398                foreach ($dateLflArray as $k => $v) {
10399                    foreach ($dateLflArray[$k] as $item) {
10400                        $year = $item->year;
10401                        $min_date_like = $item->min_date_like;
10402                        $max_date_like = $item->max_date_like;
10403                        array_push($lflArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN LEAST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND GREATEST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}')");
10404                    }
10405
10406                    $lflArray = implode(' OR ', $lflArray);
10407                    array_push($lflCompanyIds, "q.company_id = {$k} AND ({$lflArray})");
10408                    $lflArray = [];
10409                }
10410
10411                $lflCompanyIds = implode(' OR ', $lflCompanyIds);
10412                $where .= " AND ({$lflCompanyIds}";
10413            }
10414
10415            if ($isFy) {
10416                if ($companyId != 0) {
10417                    $where .= " AND q.company_id = {$companyId} ";
10418                } else {
10419                    $where .= " AND q.company_id IN ({$this->companyId})";
10420                }
10421            }
10422
10423            if (isset($data['source']) && $data['source'] != null) {
10424                $where .= " AND s.name = '{$data['source']}'";
10425            }
10426
10427            if (isset($data['month']) && $data['month'] != null) {
10428                $where .= " AND MONTH(q.{$field}) = '{$data['month']}'";
10429            }
10430
10431            if (isset($data['commercial']) && $data['commercial'] != null) {
10432                $where .= " AND q.commercial = '{$data['commercial']}'";
10433            }
10434
10435            if (isset($data['created_by']) && $data['created_by'] != null) {
10436                $where .= " AND q.created_by = '{$data['created_by']}'";
10437            }
10438
10439            if (isset($data['budget_type']) && $data['budget_type'] != null) {
10440                $where .= " AND bt.budget_type_id = {$data['budget_type']}";
10441            }
10442
10443            if (isset($data['budget_type_group']) && $data['budget_type_group'] != null) {
10444                $where .= " AND bt.budget_type_group_id = {$data['budget_type_group']}";
10445            }
10446
10447            if (isset($data['budget_status']) && $data['budget_status'] != null) {
10448                $where .= " AND bs.budget_status_id = {$data['budget_status']}";
10449            }
10450
10451            if (isset($data['client_type']) && $data['client_type'] != null) {
10452                $where .= " AND ct.customer_type_id = {$data['client_type']}";
10453            }
10454
10455            if (isset($data['segment_id']) && $data['segment_id'] != null) {
10456                $where .= " AND q.segment_id = {$data['segment_id']}";
10457            }
10458
10459            $col = '1';
10460
10461            if (isset($data['data_to_display']) && $data['data_to_display'] != null) {
10462                if ($data['data_to_display'] == 1) {
10463                    $col = '1';
10464                }
10465
10466                if ($data['data_to_display'] == 2) {
10467                    $col = 'q.amount';
10468                }
10469            }
10470
10471            $budgetTypes = TblBudgetTypes::orderByRaw('ISNULL(priority), priority ASC')->get();
10472            $cols = '';
10473            foreach ($budgetTypes as $item) {
10474                if ($item->name == '' || $item->name == null) {
10475                    $cols .= ",COALESCE(SUM(CASE WHEN bt.name IS NULL {$acc} THEN {$col} ELSE 0 END), 0) AS 'Otros'";
10476                } else {
10477                    $cols .= ",COALESCE(SUM(CASE WHEN bt.name = '{$item->name}{$acc} THEN {$col} ELSE 0 END), 0) AS '{$item->name}'";
10478                }
10479            }
10480
10481            $budgetTypeGroups = TblBudgetTypeGroups::orderByRaw('ISNULL(priority), priority ASC')->get();
10482
10483            $colsGroups = ",COALESCE(SUM(CASE WHEN bt.name IS NULL {$acc} THEN {$col} END), 0) AS Otros";
10484
10485            foreach ($budgetTypeGroups as $item) {
10486                $budgetTypeGroupName = str_replace(' ', '', $item->name).$item->budget_type_group_id;
10487                $colsGroups .= ",GROUP_CONCAT(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) {$acc} THEN q.id END) AS 'groupConcatIds{$budgetTypeGroupName}'";
10488                $colsGroups .= ",COALESCE(SUM(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) {$acc} THEN {$col} END), 0) AS '{$budgetTypeGroupName}'";
10489            }
10490
10491            $colsGroups .= ",COALESCE(SUM(CASE WHEN (bt.budget_type_group_id IS NOT NULL OR bt.name IS NULL) {$acc} THEN {$col} END), 0) AS total";
10492
10493            $col = $colsGroups.$cols;
10494
10495            if (@$data['data_to_display'] == 3) {
10496
10497                $cols = '';
10498                foreach ($budgetTypes as $item) {
10499                    if ($item->name == '' || $item->name == null) {
10500                        $cols .= ",COALESCE(
10501                                        SUM(CASE WHEN bt.name IS NULL {$acc} THEN q.amount ELSE 0 END) /
10502                                        SUM(CASE WHEN bt.name IS NULL {$acc} THEN 1 ELSE 0 END) * 100
10503                                    , 0) AS 'Otros'";
10504                    } else {
10505                        $cols .= ", COALESCE(
10506                                        SUM(CASE WHEN bt.name = '{$item->name}{$acc} THEN q.amount ELSE 0 END) /
10507                                        SUM(CASE WHEN bt.name = '{$item->name}{$acc} THEN 1 ELSE 0 END)
10508                                    , 0) AS '{$item->name}'";
10509                    }
10510                }
10511
10512                $colsGroups = ",COALESCE(
10513                                (SUM(CASE WHEN bt.name IS NULL {$acc} THEN q.amount END)) /
10514                                (SUM(CASE WHEN bt.name IS NULL {$acc} THEN 1 END))
10515                            , 0) Otros";
10516
10517                foreach ($budgetTypeGroups as $item) {
10518                    $budgetTypeGroupName = str_replace(' ', '', $item->name).$item->budget_type_group_id;
10519                    $colsGroups .= ",GROUP_CONCAT(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) {$acc} THEN q.id END) AS 'groupConcatIds{$budgetTypeGroupName}'";
10520                    $colsGroups .= ",COALESCE(
10521                                        (SUM(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) {$acc} THEN q.amount END)) /
10522                                        (SUM(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) {$acc} THEN 1 END))
10523                                    , 0) '{$budgetTypeGroupName}'";
10524                }
10525
10526                $colsGroups .= ",COALESCE(
10527                                    (SUM(CASE WHEN (bt.budget_type_group_id IS NOT NULL OR bt.name IS NULL) {$acc} THEN q.amount END)) /
10528                                    (SUM(CASE WHEN (bt.budget_type_group_id IS NOT NULL OR bt.name IS NULL) {$acc} THEN 1 END))
10529                                , 0) total";
10530
10531                $col = $colsGroups.$cols;
10532            }
10533
10534            if (@$data['data_to_display'] == 4) {
10535
10536                $cols = '';
10537
10538                foreach ($budgetTypes as $item) {
10539
10540                    if ($item->name == '' || $item->name == null) {
10541                        $cols .= ",COALESCE(
10542                                        SUM(CASE WHEN bt.name IS NULL AND q.acceptance_date IS NOT NULL THEN 1 ELSE 0 END) /
10543                                        SUM(CASE WHEN bt.name IS NULL AND q.created_at IS NOT NULL THEN 1 ELSE 0 END) * 100
10544                                    , 0) AS 'Otros'";
10545                    } else {
10546                        $cols .= ", COALESCE(
10547                                        SUM(CASE WHEN bt.name = '{$item->name}' AND q.acceptance_date IS NOT NULL THEN 1 END) /
10548                                        SUM(CASE WHEN bt.name = '{$item->name}' AND q.created_at IS NOT NULL THEN 1 END) * 100
10549                                    , 0) AS '{$item->name}'";
10550                    }
10551                }
10552
10553                $colsGroups = ',COALESCE(
10554                                    (SUM(CASE WHEN bt.name IS NULL AND q.acceptance_date IS NOT NULL THEN 1 END)) /
10555                                    (SUM(CASE WHEN bt.name IS NULL AND q.created_at IS NOT NULL THEN 1 END)) * 100
10556                                , 0) Otros';
10557
10558                foreach ($budgetTypeGroups as $item) {
10559                    $budgetTypeGroupName = str_replace(' ', '', $item->name).$item->budget_type_group_id;
10560                    $colsGroups .= ",GROUP_CONCAT(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) {$acc} THEN q.id END) AS 'groupConcatIds{$budgetTypeGroupName}'";
10561                    $colsGroups .= ",COALESCE(
10562                                        (SUM(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) AND q.acceptance_date IS NOT NULL THEN 1 END)) /
10563                                        (SUM(CASE WHEN (bt.budget_type_group_id = {$item->budget_type_group_id} OR bt.name IS NULL) AND q.created_at IS NOT NULL THEN 1 END)) * 100
10564                                    , 0) '{$budgetTypeGroupName}'";
10565                }
10566
10567                $colsGroups .= ',COALESCE(
10568                                    (SUM(CASE WHEN (bt.budget_type_group_id IS NOT NULL OR bt.name IS NULL) AND q.acceptance_date IS NOT NULL THEN 1 END)) /
10569                                    (SUM(CASE WHEN (bt.budget_type_group_id IS NOT NULL OR bt.name IS NULL) AND q.created_at IS NOT NULL THEN 1 END)) * 100
10570                                    , 0) total';
10571
10572                $col = $colsGroups.$cols;
10573            }
10574
10575            $query = "SELECT
10576                            YEAR(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)) AS 'year',
10577                            LPAD(MONTH(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)), 2, 0) AS 'month',
10578                            q.company_id,
10579                            c.name 'companyName',
10580                            GROUP_CONCAT(CASE WHEN (bt.budget_type_group_id IS NOT NULL OR bt.name IS NULL) {$acc} THEN q.id END) groupConcatIds
10581                            {$col}
10582                        FROM
10583                            tbl_quotations q
10584                            LEFT JOIN tbl_sources s ON s.source_id = q.source_id
10585                            LEFT JOIN tbl_budget_status bs ON bs.budget_status_id = q.budget_status_id
10586                            LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
10587                            LEFT JOIN tbl_budget_type_groups btg ON bt.budget_type_group_id = btg.budget_type_group_id
10588                            LEFT JOIN tbl_customer_types ct ON q.customer_type_id = ct.customer_type_id
10589                            LEFT JOIN tbl_companies c ON q.company_id = c.company_id
10590                        WHERE
10591                            q.{$field} IS NOT NULL
10592                            AND q.for_add = 0
10593                            AND q.budget_type_id != 7
10594                            AND q.budget_type_id IS NOT NULL
10595                            AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1                            
10596                            {$where}
10597                            {$whereYear}
10598                        GROUP BY
10599                            YEAR(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)),
10600                            MONTH(DATE_ADD(q.{$field}, INTERVAL - WEEKDAY(q.{$field}) DAY)),
10601                            q.company_id WITH ROLLUP
10602                        ORDER BY
10603                            YEAR DESC,
10604                            MONTH ASC,
10605                            q.company_id ASC,
10606                            DATE_FORMAT(q.{$field}, '%e') ASC";
10607
10608            $result = DB::select($query);
10609
10610            $query = "SELECT
10611                        btg.budget_type_group_id,
10612                        btg.name,
10613                        (
10614                            SELECT
10615                                GROUP_CONCAT(COALESCE(bt.name, '') ORDER BY ISNULL(bt.priority), bt.priority ASC SEPARATOR '|')
10616                            FROM
10617                                tbl_budget_types bt
10618                            WHERE
10619                                bt.budget_type_group_id = btg.budget_type_group_id
10620                        ) budget_types
10621                        FROM
10622                            tbl_budget_type_groups btg
10623                        ORDER BY
10624                            ISNULL(btg.priority),
10625                            btg.priority ASC";
10626
10627            $budgetTypeGroups = DB::select($query);
10628
10629            foreach ($budgetTypeGroups as $item) {
10630                $item->group_key_name = str_replace(' ', '', $item->name).$item->budget_type_group_id;
10631                $item->budget_types = explode('|', (string) $item->budget_types);
10632            }
10633
10634            return response([
10635                'message' => 'OK',
10636                'data' => $result,
10637                'budgetTypeGroups' => $budgetTypeGroups,
10638            ]);
10639
10640        } catch (\Exception $e) {
10641            report(AppException::fromException($e, 'LIST_QUOTATION_ANALYTICS_BY_TYPES_OF_BUDGETS_COMPANT_PER_WEEK_EXCEPTION'));
10642
10643            return response(['message' => 'KO', 'error' => $e->getMessage()]);
10644        }
10645    }
10646
10647    public function request_permission_commercial(Request $request): ResponseFactory|HttpResponse
10648    {
10649
10650        try {
10651
10652            $data = $request->all();
10653
10654            $id = addslashes((string) $data['id']);
10655            $requestedBy = $data['requested_by'];
10656            $body = '';
10657
10658            $result = TblQuotations::where('id', $id)->first();
10659
10660            $subject = __('language.request_permission_commercial.subject');
10661            $subject = str_replace('{{quote_id}}', $result->quote_id, $subject);
10662            $subject = str_replace('{{username}}', $requestedBy, $subject);
10663
10664            $email = new Mail;
10665
10666            $imgpath = file_get_contents(public_path('fireservicetitan.png'));
10667
10668            $email->addAttachment(
10669                $imgpath,
10670                'image/png',
10671                'fireservicetitan.png',
10672                'inline',
10673                'fireservicetitan'
10674            );
10675
10676            $url = config('app.frontend_url')."orders/{$id}?company_id={$result->company_id}";
10677            $href = "<a href='{$url}'>{$result->quote_id}</a>";
10678
10679            $user = TblUsers::where('name', $requestedBy)->first();
10680
10681            $urlClick = config('app.frontend_url')."update?confirm_request={$id}&requested_by={$user->id}&quote_id={$result->quote_id}";
10682            $company = TblCompanies::where('company_id', $result->company_id)->first();
10683
10684            $body .= __('language.request_permission_commercial.body_hello');
10685            $body .= __('language.request_permission_commercial.body_message');
10686
10687            $amount = $this->currency($result->amount, 1);
10688
10689            $body = str_replace('{{commercial}}', $result->commercial, $body);
10690            $body = str_replace('{{company}}', $company->name, $body);
10691            $body = str_replace('{{client}}', $result->client, $body);
10692            $body = str_replace('{{amount}}', $amount, $body);
10693            $body = str_replace('{{quote_id}}', $href, $body);
10694            $body = str_replace('{{click}}', $urlClick, $body);
10695            $body = str_replace('{{username}}', $requestedBy, $body);
10696
10697            $body .= '<p>Fire Service Titan</p>';
10698            $body .= "<img src='cid:fireservicetitan' style='height: 45px;' />";
10699
10700            $html = '<!DOCTYPE html>';
10701            $html .= '<html>';
10702            $html .= '<head>';
10703            $html .= '<meta charset="UTF-8">';
10704            $html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
10705            $html .= '</head>';
10706            $html .= '<body>';
10707            $html .= $body;
10708            $html .= '</body>';
10709            $html .= '</html>';
10710
10711            $user = TblUsers::where('id', $this->userId)->first();
10712
10713            if (config('services.sendgrid.staging')) {
10714                $email->addTo($user->email);
10715            } else {
10716                $email->addTo('luis.collar@fire.es');
10717
10718                $user = TblUsers::where('name', $result->created_by)->first();
10719                if ($user && $user->email != 'luis.collar@fire.es') {
10720                    $email->addTo($user->email);
10721                }
10722
10723                if ($result->created_by != $result->commercial) {
10724                    $user = TblUsers::where('name', $result->commercial)->first();
10725                    if ($user->email != 'luis.collar@fire.es') {
10726                        $email->addTo($user->email);
10727                    }
10728                }
10729            }
10730
10731            $email->setFrom('titan@fire.es');
10732            $email->setSubject($subject);
10733
10734            $email->addContent('text/html', $html);
10735
10736            $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
10737
10738            try {
10739                $response = $sendgrid->send($email);
10740                SendgridLogger::log($email, $response);
10741            } catch (\Throwable $sendException) {
10742                SendgridLogger::logException($email, $sendException);
10743                throw $sendException;
10744            }
10745
10746            if ($response->statusCode() == 202) {
10747                return response(['message' => 'OK']);
10748            }
10749
10750            $this->addUpdateLog($id, $requestedBy, 'request_permission_commercial', null, null, 4);
10751
10752            return response(['message' => 'KO']);
10753
10754        } catch (\Exception $e) {
10755            report(AppException::fromException($e, 'REQUEST_PERMISSION_EXCEPTION'));
10756
10757            return response(['message' => 'KO', 'error' => $e->getMessage()]);
10758        }
10759
10760    }
10761
10762    public function confirm_update_commercial(Request $request): ResponseFactory|HttpResponse
10763    {
10764
10765        try {
10766            sleep(3);
10767            $data = $request->all();
10768            $id = addslashes((string) $data['id']);
10769            $userId = addslashes((string) $data['requested_by']);
10770
10771            $user = TblUsers::where('id', $userId)->first();
10772
10773            if ($user) {
10774
10775                TblQuotations::where('id', $data['id'])->update(
10776                    [
10777                        'commercial' => $user->name,
10778                        'updated_at' => date('Y-m-d H:i:s'),
10779                    ]
10780                );
10781
10782            } else {
10783                return response(['message' => 'KO', 'error' => 'invalid_user']);
10784            }
10785
10786            return response(['message' => 'OK']);
10787
10788        } catch (\Exception $e) {
10789            report(AppException::fromException($e, 'CONFIRM_UPDATE_COMMERCIAL_EXCEPTION'));
10790
10791            return response(['message' => 'KO', 'error' => $e->getMessage()]);
10792        }
10793    }
10794
10795    public function calculateEmailRequestSize(Mail $email): int
10796    {
10797
10798        $size = 0;
10799
10800        // Add size of 'from', 'to', 'subject', 'content'
10801        $from = $email->getFrom();
10802        $size += strlen(json_encode([
10803            'from' => $from->getEmail().' '.$from->getName(),
10804            'subject' => $email->getSubject(),
10805        ]));
10806
10807        // Add size of 'to' (recipients)
10808        $personalizations = $email->getPersonalizations() ?? [];
10809        foreach ($personalizations as $personalization) {
10810            foreach ($personalization->getTos() as $to) {
10811                $size += strlen($to->getEmail().' '.$to->getName());
10812            }
10813        }
10814
10815        // Add size of content
10816        foreach ($email->getContents() as $content) {
10817            $size += strlen((string) $content->getValue());
10818        }
10819
10820        // Add size of attachments (if any)
10821
10822        if ($email->getAttachments() != null && $email->getAttachments() != '') {
10823            foreach ($email->getAttachments() as $attachment) {
10824                $size += strlen($attachment->getContent()); // Base64 encoded size
10825                $size += strlen($attachment->getFilename());
10826                $size += strlen($attachment->getType());
10827            }
10828        }
10829
10830        $sizeInMegabytes = $size / 1048576; // 1 MB = 1,048,576 bytes
10831
10832        return (int) ceil($sizeInMegabytes);
10833    }
10834
10835    public function list_quotation_analytics_commercial_productivity(Request $request): ResponseFactory|HttpResponse
10836    {
10837
10838        // try {
10839
10840        $data = $request->all();
10841        $companyId = addslashes((string) $data['company_id']);
10842
10843        $where = '';
10844        $whereYear = '';
10845        $whereVisit = '';
10846
10847        $dateLflArray = [];
10848        $companyIds = $this->companyIds;
10849
10850        if ($companyId != 0) {
10851            $companyIds = [$companyId];
10852        }
10853
10854        $field = 'issue_date';
10855
10856        if (isset($data['years']) && $data['years'] != null) {
10857            $years = implode(',', $data['years']);
10858            if (count($data['years']) > 0) {
10859                $whereYear = " AND YEAR(q.{$field}) IN ({$years})";
10860            }
10861        }
10862
10863        foreach ($companyIds as $v) {
10864
10865            $lflWhere = " AND q.company_id = {$v} ";
10866
10867            $query = "SELECT
10868                            CONCAT(
10869                                DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field}),
10870                                ' - ',
10871                                DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%c/%e/'), YEAR({$field})
10872                            ) AS date_like,
10873                            YEAR(q.{$field}) 'year',
10874                            DATE_FORMAT((SELECT MIN(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS min_date_like,
10875                            DATE_FORMAT((SELECT MAX(q.{$field}) FROM tbl_quotations q WHERE q.{$field} IS NOT NULL {$lflWhere}), '%m-%d') AS max_date_like,
10876                            {$v} 'company_id'
10877                        FROM
10878                            tbl_quotations q
10879                        WHERE
10880                            q.{$field} IS NOT NULL
10881                            AND q.for_add = 0
10882                            {$lflWhere}
10883                            {$whereYear}
10884                        GROUP BY YEAR(q.{$field})
10885                        ORDER BY YEAR(q.{$field}) DESC";
10886
10887            $dateLike = DB::select($query);
10888
10889            if (count($dateLike) > 0) {
10890                $dateLflArray[$v] = $dateLike;
10891            }
10892        }
10893
10894        $whereAcceptanceDate = '';
10895
10896        if (isset($data['source']) && $data['source'] != null) {
10897            $where .= " AND s.name = '{$data['source']}'";
10898        }
10899
10900        if (isset($data['month']) && $data['month'] != null) {
10901            $where .= " AND MONTH(q.created_at) = '{$data['month']}'";
10902        }
10903
10904        if (isset($data['week']) && $data['week'] != null) {
10905            $where .= " AND WEEK(q.created_at) = '{$data['week']}'";
10906        }
10907
10908        if (isset($data['commercial']) && $data['commercial'] != null) {
10909            $commercial = implode("','", $data['commercial']);
10910            if (count($data['commercial']) > 0) {
10911                $where .= " AND q.commercial IN ('{$commercial}') ";
10912                $whereVisit .= " AND q.commercial IN ('{$commercial}') ";
10913            }
10914        }
10915
10916        if (isset($data['created_by']) && $data['created_by'] != null) {
10917            $created_by = implode("','", $data['created_by']);
10918            if (count($data['created_by']) > 0) {
10919                $where .= " AND q.created_by IN ('{$created_by}')";
10920            }
10921        }
10922
10923        if (isset($data['budget_type']) && $data['budget_type'] != null) {
10924            $where .= " AND bt.budget_type_id = {$data['budget_type']}";
10925        }
10926
10927        if (isset($data['budget_type_group']) && $data['budget_type_group'] != null) {
10928            $budgetTypeGroupIds = implode(',', $data['budget_type_group']);
10929            if (count($data['budget_type_group']) > 0) {
10930                $where .= " AND bt.budget_type_group_id IN ({$budgetTypeGroupIds})";
10931            }
10932        }
10933
10934        if (isset($data['budget_status']) && $data['budget_status'] != null) {
10935            $where .= " AND bs.budget_status_id = {$data['budget_status']}";
10936        }
10937
10938        if (isset($data['client_type']) && $data['client_type'] != null) {
10939            $where .= " AND ct.customer_type_id = {$data['client_type']}";
10940        }
10941
10942        if (isset($data['segment_id']) && $data['segment_id'] != null) {
10943            $where .= " AND q.segment_id = {$data['segment_id']}";
10944        }
10945
10946        if (isset($data['role_id']) && $data['role_id'] != null) {
10947            $roleId = implode(',', $data['role_id']);
10948            if (count($data['role_id']) > 0) {
10949                $where .= " AND r.role_id IN ({$roleId})";
10950                $whereVisit .= " AND r.role_id IN ({$roleId}";
10951            }
10952        }
10953
10954        $groupByFilter = 2;
10955        if (isset($data['group_by']) && $data['group_by'] != null) {
10956            $groupByFilter = $data['group_by'];
10957        }
10958
10959        $groupBy = '1, 2, 3, q.commercial, budget_type';
10960
10961        if ($groupByFilter == 1) {
10962            $groupBy = '1, q.commercial, 2, 3, budget_type';
10963        }
10964
10965        if ($groupByFilter == 3) {
10966            $groupBy = '1, budget_type, q.commercial, 2, 3';
10967        }
10968
10969        $aggregatedBy = 1;
10970        $aggregatedCol = "LPAD(q.month, 2, 0) AS 'month', LPAD(q.week, 2, 0) AS 'week', ";
10971        $aggregatedByCalc = ' / 4';
10972        if (isset($data['aggregated_by']) && $data['aggregated_by'] != null) {
10973            $aggregatedBy = $data['aggregated_by'];
10974            if ($data['aggregated_by'] == 1) {
10975
10976                $groupBy = '1, 2, 3, q.commercial, budget_type';
10977
10978                if ($groupByFilter == 1) {
10979                    $groupBy = '1, q.commercial, 2, 3, budget_type';
10980                }
10981
10982                if ($groupByFilter == 3) {
10983                    $groupBy = '1, budget_type, q.commercial, 2, 3';
10984                }
10985
10986                $aggregatedCol = "LPAD(q.month, 2, 0) AS 'month', LPAD(q.week, 2, 0) AS 'week', ";
10987            } elseif ($data['aggregated_by'] == 2) {
10988                $groupBy = '1, 2, q.commercial, budget_type';
10989
10990                if ($groupByFilter == 1) {
10991                    $groupBy = '1, q.commercial, 2, budget_type';
10992                }
10993
10994                if ($groupByFilter == 3) {
10995                    $groupBy = '1, budget_type, q.commercial, 2';
10996                }
10997
10998                $aggregatedCol = "LPAD(q.month, 2, 0) AS 'month', NULL AS 'week',";
10999                $aggregatedByCalc = '';
11000            } elseif ($data['aggregated_by'] == 3) {
11001                $groupBy = '1, q.commercial, budget_type';
11002
11003                if ($groupByFilter == 3) {
11004                    $groupBy = '1, budget_type, q.commercial';
11005                }
11006
11007                $aggregatedCol = "NULL AS 'month', NULL AS 'week',";
11008                $aggregatedByCalc = ' * 12';
11009            }
11010        }
11011
11012        $whereAcceptanceDate = $where;
11013        $whereCreatedAt = $where;
11014
11015        $isFy = true;
11016
11017        if ($companyId != 0) {
11018            $where .= " AND q.company_id = {$companyId} ";
11019            $whereCreatedAt .= " AND q.company_id = {$companyId} ";
11020            $whereAcceptanceDate .= " AND q.company_id = {$companyId} ";
11021            $whereVisit .= " AND q.company_id = {$companyId} ";
11022        } else {
11023            $where .= " AND q.company_id IN ({$this->companyId}";
11024            $whereCreatedAt .= " AND q.company_id IN ({$this->companyId}";
11025            $whereAcceptanceDate .= " AND q.company_id IN ({$this->companyId}";
11026            $whereVisit .= " AND q.company_id IN ({$this->companyId}";
11027        }
11028
11029        if (isset($data['campaign']) && $data['campaign'] != null) {
11030            $campaign = implode("','", $data['campaign']);
11031            if (count($data['campaign']) > 0) {
11032                $whereVisit .= " AND q.campaign IN ('{$campaign}')";
11033            }
11034        }
11035
11036        if (isset($data['ytd']) && $data['ytd'] != null && $data['ytd'] == true) {
11037            $isFy = false;
11038            $now = date('m-d');
11039
11040            $where .= " AND q.{$field} BETWEEN DATE_FORMAT(q.{$field}, '%Y-01-01') AND DATE_FORMAT(q.{$field}, '%Y-{$now}') ";
11041            $whereCreatedAt .= " AND q.created_at BETWEEN DATE_FORMAT(q.created_at, '%Y-01-01') AND DATE_FORMAT(q.created_at, '%Y-{$now}') ";
11042            $whereAcceptanceDate .= " AND q.acceptance_date BETWEEN DATE_FORMAT(q.acceptance_date, '%Y-01-01') AND DATE_FORMAT(q.acceptance_date, '%Y-{$now}') ";
11043            $whereVisit .= " AND q.visit_date BETWEEN DATE_FORMAT(q.visit_date, '%Y-01-01') AND DATE_FORMAT(q.visit_date, '%Y-{$now}') ";
11044        }
11045
11046        if (isset($data['lfl']) && $data['lfl'] != null && $data['lfl'] == true) {
11047            $isFy = false;
11048            $lflArray = [];
11049            $ytdAcceptanceArray = [];
11050            $lflCompanyIds = [];
11051            $lflCompanyIdsAcc = [];
11052            foreach ($dateLflArray as $k => $v) {
11053                foreach ($dateLflArray[$k] as $item) {
11054                    $year = $item->year;
11055                    $min_date_like = $item->min_date_like;
11056                    $max_date_like = $item->max_date_like;
11057                    array_push($lflArray, "DATE_FORMAT(q.{$field}, '%Y-%m-%d') BETWEEN LEAST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND GREATEST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}')");
11058                    array_push($ytdAcceptanceArray, "DATE_FORMAT(q.acceptance_date, '%Y-%m-%d') BETWEEN LEAST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}') AND GREATEST('{$year}-{$min_date_like}', '{$year}-{$max_date_like}')");
11059                }
11060
11061                $lflArray = implode(' OR ', $lflArray);
11062                array_push($lflCompanyIds, "q.company_id = {$k} AND ({$lflArray})");
11063                $lflArray = [];
11064
11065                $ytdAcceptanceArray = implode(' OR ', $ytdAcceptanceArray);
11066                array_push($lflCompanyIdsAcc, "q.company_id = {$k} AND ({$ytdAcceptanceArray})");
11067                $ytdAcceptanceArray = [];
11068            }
11069
11070            $lflCompanyIds = implode(' OR ', $lflCompanyIds);
11071            $where .= " AND ({$lflCompanyIds}";
11072
11073            $lflCompanyIdsAcc = implode(' OR ', $lflCompanyIdsAcc);
11074            $whereAcceptanceDate .= " AND ({$lflCompanyIdsAcc}";
11075        }
11076
11077        $orderBy = 'CASE WHEN q.commercial IS NULL OR q.budget_type IS NULL THEN q.priority END DESC, q.priority ASC,';
11078
11079        if (isset($data['order_by'])) {
11080            $col = $data['order_by']['column'];
11081            $sort = $data['order_by']['sort'];
11082
11083            if (! empty($sort) || $sort != null) {
11084                $orderBy = "CASE WHEN q.commercial IS NULL OR q.budget_type IS NULL THEN {$col} END DESC, {$col} {$sort},";
11085            }
11086        }
11087
11088        $visitTypes = TblVisitTypeGroups::orderByRaw('ISNULL(priority), priority ASC')->get();
11089
11090        $visitCols = '';
11091        $visitMainTableCols = '';
11092        $visitSubMainTableCols = '';
11093        $visitSubTableCols = '';
11094        $visitMainCols = '';
11095
11096        $visitCall = ['Visita', 'Llamada'];
11097
11098        foreach ($visitCall as $value) {
11099            foreach ($visitTypes as $item) {
11100                $visitTypeNames = $value.$item->visit_type_group_id;
11101                $visitCols .= ",COUNT(CASE WHEN q.visit_date IS NOT NULL AND v.visit_type_group_id = {$item->visit_type_group_id} AND q.visit_call = '{$value}' THEN 1 END) AS 'total{$visitTypeNames}'";
11102                $visitCols .= ",GROUP_CONCAT(CASE WHEN q.visit_date IS NOT NULL AND v.visit_type_group_id = {$item->visit_type_group_id} AND q.visit_call = '{$value}' THEN q.id END) AS 'groupConcatIds{$visitTypeNames}'";
11103                $visitMainTableCols .= ",COALESCE(SUM(q.total{$visitTypeNames}), 0) AS 'total{$visitTypeNames}'";
11104                $visitMainTableCols .= ",GROUP_CONCAT(q.groupConcatIds{$visitTypeNames})  AS 'groupConcatIds{$visitTypeNames}'";
11105                $visitSubMainTableCols .= ",q.total{$visitTypeNames}";
11106                $visitSubMainTableCols .= ",q.groupConcatIds{$visitTypeNames}";
11107                $visitSubTableCols .= ",0 AS total{$visitTypeNames}";
11108                $visitSubTableCols .= ",NULL AS groupConcatIds{$visitTypeNames}";
11109                $visitMainCols .= ",COALESCE(SUM(q.total{$visitTypeNames}), 0) total{$visitTypeNames}";
11110                $visitMainCols .= ",GROUP_CONCAT(q.groupConcatIds{$visitTypeNames}) groupConcatIds{$visitTypeNames}";
11111            }
11112        }
11113
11114        $businessGoalsDefault = TblBusinessGoals::where('is_default', 1)->where('budget_type_group_id', 999999999)->first();
11115
11116        $businessGoalsDefault->issue_objective ??= 1;
11117        $businessGoalsDefault->acceptance_objective ??= 1;
11118        $businessGoalsDefault->new_objective ??= 1;
11119        $businessGoalsDefault->is_amount ??= 1;
11120
11121        $gO = '';
11122
11123        if ($groupByFilter != 3) {
11124            $gO = "ORDER BY
11125                        1 DESC,
11126                        2 ASC,
11127                        3 ASC,
11128                        q.commercial ASC,
11129                        {$orderBy}
11130                        DATE_FORMAT(q.namedate, '%e') ASC";
11131        } else {
11132            $gO = 'ORDER BY
11133                        1 DESC,
11134                        2 ASC,
11135                        3 ASC,
11136                        budget_type ASC,
11137                        q.commercial ASC';
11138        }
11139
11140        $query = "WITH business_goal_objective_users AS (
11141                        SELECT
11142                            bg.user_id,
11143                            bg.issue_objective,
11144                            bg.acceptance_objective,
11145                            bg.new_objective,
11146                            bg.is_amount
11147                        FROM tbl_business_goals bg
11148                        WHERE bg.budget_type_group_id = 999999999
11149                    ), business_goal_objective_roles AS (
11150                        SELECT
11151                            bg.role_id,
11152                            bg.issue_objective,
11153                            bg.acceptance_objective,
11154                            bg.new_objective,
11155                            bg.is_amount
11156                        FROM tbl_business_goals bg
11157                        WHERE bg.budget_type_group_id = 999999999
11158                    )
11159
11160                    SELECT
11161                        q.year,
11162                        {$aggregatedCol}
11163                        q.namedate created_at,
11164                        q.commercial,
11165                        q.budget_type,
11166                        COALESCE(SUM(q.issue_date), 0) totalIssue,
11167                        COALESCE(SUM(q.created_at), 0) totalCreatedAt,
11168                        GROUP_CONCAT(q.groupConcatIds) groupConcatIds,
11169                        CASE
11170                            WHEN SUM(q.is_amountIssue) > 0 THEN
11171                                (SUM(q.revenueIssue) / NULLIF(SUM(q.issueObjective), 0)) * 100
11172                            ELSE
11173                                (SUM(q.issue_date) / NULLIF(SUM(q.issueObjective), 0)) * 100
11174                        END AS totalIssueObjective,
11175                        CASE
11176                            WHEN (CASE
11177                                WHEN SUM(q.is_amountIssue) > 0 THEN
11178                                    (SUM(q.revenueIssue) / NULLIF(SUM(q.issueObjective), 0)) * 100
11179                                ELSE
11180                                    (SUM(q.issue_date) / NULLIF(SUM(q.issueObjective), 0)) * 100
11181                            END) BETWEEN 1 AND 70 THEN 'text-danger'
11182                            WHEN (CASE
11183                                WHEN SUM(q.is_amountIssue) > 0 THEN
11184                                    (SUM(q.revenueIssue) / NULLIF(SUM(q.issueObjective), 0)) * 100
11185                                ELSE
11186                                    (SUM(q.issue_date) / NULLIF(SUM(q.issueObjective), 0)) * 100
11187                            END) BETWEEN 70 AND 90 THEN 'text-warning'
11188                        END textIssueColor,
11189                        SUM(q.revenueIssue) revenueIssue,
11190                        CASE
11191                            WHEN SUM(q.is_amountIssue) > 0 THEN
11192                                (SUM(q.revenueIssue) / NULLIF(SUM(q.issueObjectiveMonthly), 0)) * 100
11193                            ELSE
11194                                (SUM(q.issue_date) / NULLIF(SUM(q.issueObjectiveMonthly), 0)) * 100
11195                        END AS totalIssueObjectiveMonthly,
11196                        CASE
11197                            WHEN SUM(q.is_amountIssue) > 0 THEN
11198                                (SUM(q.revenueIssue) / NULLIF(SUM(q.issueObjectiveYearly), 0)) * 100
11199                            ELSE
11200                                (SUM(q.issue_date) / NULLIF(SUM(q.issueObjectiveYearly), 0)) * 100
11201                        END AS totalIssueObjectiveYearly,
11202                        COALESCE(SUM(q.acceptance_date), 0) totalAcceptance,
11203                        GROUP_CONCAT(q.groupConcatCreatedAtIds) groupConcatCreatedAtIds,
11204                        SUM(q.totalIssueLessThan5) AS totalIssueLessThan5,
11205                        GROUP_CONCAT(q.groupConcatIdsIssueLessThan5) AS groupConcatIdsIssueLessThan5,
11206                        GROUP_CONCAT(q.groupConcatAcceptanceIds) groupConcatAcceptanceIds,
11207                        CASE
11208                            WHEN SUM(q.is_amountAcceptance) > 0 THEN
11209                                (SUM(q.revenueAcceptance) / NULLIF(SUM(q.acceptanceObjective), 0)) * 100
11210                            ELSE
11211                                (SUM(q.acceptance_date) / NULLIF(SUM(q.acceptanceObjective), 0)) * 100
11212                        END AS totalAcceptanceObjective,
11213                        CASE
11214                            WHEN SUM(q.is_amountAcceptance) > 0 THEN
11215                                (SUM(q.revenueAcceptance) / NULLIF(SUM(q.acceptanceObjectiveMonthly), 0)) * 100
11216                            ELSE
11217                                (SUM(q.acceptance_date) / NULLIF(SUM(q.acceptanceObjectiveMonthly), 0)) * 100
11218                        END AS totalAcceptanceObjectiveMonthly,
11219                        CASE
11220                            WHEN SUM(q.is_amountAcceptance) > 0 THEN
11221                                (SUM(q.revenueAcceptance) / NULLIF(SUM(q.acceptanceObjectiveYearly), 0)) * 100
11222                            ELSE
11223                                (SUM(q.acceptance_date) / NULLIF(SUM(q.acceptanceObjectiveYearly), 0)) * 100
11224                        END AS totalAcceptanceObjectiveYearly,
11225                        CASE
11226                            WHEN (CASE
11227                                WHEN SUM(q.is_amountAcceptance) > 0 THEN
11228                                    (SUM(q.revenueAcceptance) / NULLIF(SUM(q.acceptanceObjective), 0)) * 100
11229                                ELSE
11230                                    (SUM(q.acceptance_date) / NULLIF(SUM(q.acceptanceObjective), 0)) * 100
11231                            END) BETWEEN 1 AND 70 THEN 'text-danger'
11232                            WHEN (CASE
11233                                WHEN SUM(q.is_amountAcceptance) > 0 THEN
11234                                    (SUM(q.revenueAcceptance) / NULLIF(SUM(q.acceptanceObjective), 0)) * 100
11235                                ELSE
11236                                    (SUM(q.acceptance_date) / NULLIF(SUM(q.acceptanceObjective), 0)) * 100
11237                            END) BETWEEN 70 AND 90 THEN 'text-warning'
11238                        END textAcceptanceColor,
11239                        SUM(q.revenueAcceptance) revenueAcceptance,
11240                        COALESCE(SUM(q.totalRejected), 0) totalRejected,
11241                        GROUP_CONCAT(q.groupConcatRejectedIds) groupConcatRejectedIds,
11242                        SUM(q.revenueRejected) revenueRejected,
11243                        COALESCE(SUM(q.totalNew)) totalNew,
11244                        GROUP_CONCAT(q.groupConcatNewIds) groupConcatNewIds,
11245                        CASE
11246                            WHEN SUM(q.is_amountNew) > 0 THEN
11247                                (SUM(q.revenueNew) / NULLIF(SUM(q.newObjective), 0)) * 100
11248                            ELSE
11249                                (SUM(q.totalNew) / NULLIF(SUM(q.newObjective), 0)) * 100
11250                        END AS totalNewObjective,
11251                        CASE
11252                            WHEN SUM(q.is_amountNew) > 0 THEN
11253                                (SUM(q.revenueNew) / NULLIF(SUM(q.newObjectiveMonthly), 0)) * 100
11254                            ELSE
11255                                (SUM(q.totalNew) / NULLIF(SUM(q.newObjectiveMonthly), 0)) * 100
11256                        END AS totalNewObjectiveMonthly,
11257                        CASE
11258                            WHEN SUM(q.is_amountNew) > 0 THEN
11259                                (SUM(q.revenueNew) / NULLIF(SUM(q.newObjectiveYearly), 0)) * 100
11260                            ELSE
11261                                (SUM(q.totalNew) / NULLIF(SUM(q.newObjectiveYearly), 0)) * 100
11262                        END AS totalNewObjectiveYearly,
11263                        CASE
11264                            WHEN (CASE
11265                                WHEN SUM(q.is_amountNew) > 0 THEN
11266                                    (SUM(q.revenueNew) / NULLIF(SUM(q.newObjective), 0)) * 100
11267                                ELSE
11268                                    (SUM(q.totalNew) / NULLIF(SUM(q.newObjective), 0)) * 100
11269                            END) BETWEEN 1 AND 70 THEN 'text-danger'
11270                            WHEN (CASE
11271                                WHEN SUM(q.is_amountNew) > 0 THEN
11272                                    (SUM(q.revenueNew) / NULLIF(SUM(q.newObjective), 0)) * 100
11273                                ELSE
11274                                    (SUM(q.totalNew) / NULLIF(SUM(q.newObjective), 0)) * 100
11275                            END) BETWEEN 70 AND 90 THEN 'text-warning'
11276                        END textNewColor,
11277                        SUM(q.revenueNew) revenueNew,
11278                        COALESCE(SUM(q.totalVisit), 0) totalVisit,
11279                        GROUP_CONCAT(q.groupConcatVisitIds) groupConcatVisitIds,
11280                        COALESCE(SUM(q.totalCall), 0) totalCall,
11281                        GROUP_CONCAT(q.groupConcatCallIds) groupConcatCallIds,
11282                        SUM(q.is_amountIssue) AS is_amountIssue,
11283                        SUM(q.is_amountNew) AS is_amountNew,
11284                        SUM(q.is_amountAcceptance) AS is_amountAcceptance,
11285                        SUM(q.issueObjective) AS issueObjective,
11286                        SUM(q.issueObjectiveMonthly) AS issueObjectiveMonthly,
11287                        SUM(q.issueObjectiveYearly) AS issueObjectiveYearly,
11288                        SUM(q.newObjective) AS newObjective,
11289                        SUM(q.newObjectiveMonthly) AS newObjectiveMonthly,
11290                        SUM(q.newObjectiveYearly) AS newObjectiveYearly,
11291                        SUM(q.acceptanceObjective) AS acceptanceObjective,
11292                        SUM(q.acceptanceObjectiveMonthly) AS acceptanceObjectiveMonthly,
11293                        SUM(q.acceptanceObjectiveYearly) AS acceptanceObjectiveYearly
11294                        {$visitMainCols}
11295                    FROM
11296                    (
11297                        SELECT
11298                            q.year,
11299                            q.month,
11300                            q.week,
11301                            q.namedate,
11302                            SUM(q.issue_date) AS issue_date,
11303                            q.acceptance_date,
11304                            q.created_at,
11305                            q.commercial,
11306                            q.budget_type,
11307                            GROUP_CONCAT(q.groupConcatIds) AS groupConcatIds,
11308                            CASE
11309                                WHEN q.is_amountIssue > 0 THEN
11310                                    SUM(q.revenueIssue / q.issueObjective) * 100
11311                                ELSE
11312                                    SUM(q.issue_date / q.issueObjective) * 100
11313                                END
11314                            AS totalIssueObjective,
11315                            CASE
11316                                WHEN q.is_amountIssue > 0 THEN
11317                                    SUM(q.revenueIssue / q.issueObjectiveMonthly) * 100
11318                                ELSE
11319                                    SUM(q.issue_date / q.issueObjectiveMonthly) * 100
11320                                END
11321                            AS totalIssueObjectiveMonthly,
11322                            CASE
11323                                WHEN q.is_amountIssue > 0 THEN
11324                                    SUM(q.revenueIssue / q.issueObjectiveYearly) * 100
11325                                ELSE
11326                                    SUM(q.issue_date / q.issueObjectiveYearly) * 100
11327                                END
11328                            AS totalIssueObjectiveYearly,
11329                            SUM(q.revenueIssue) revenueIssue,
11330                            GROUP_CONCAT(q.groupConcatCreatedAtIds) AS groupConcatCreatedAtIds,
11331                            SUM(q.totalIssueLessThan5) totalIssueLessThan5,
11332                            GROUP_CONCAT(q.groupConcatIdsIssueLessThan5) groupConcatIdsIssueLessThan5,
11333                            GROUP_CONCAT(q.groupConcatAcceptanceIds) AS groupConcatAcceptanceIds,
11334                            SUM(q.acceptanceObjective) totalAcceptanceObjective,
11335                            SUM(q.acceptanceObjectiveMonthly) totalAcceptanceObjectiveMonthly,
11336                            SUM(q.acceptanceObjectiveYearly) totalAcceptanceObjectiveYearly,
11337                            q.revenueAcceptance,
11338                            SUM(q.totalRejected) AS totalRejected,
11339                            GROUP_CONCAT(q.groupConcatRejectedIds) AS groupConcatRejectedIds,
11340                            SUM(q.revenueRejected) revenueRejected,
11341                            SUM(q.totalNew) AS totalNew,
11342                            GROUP_CONCAT(q.groupConcatNewIds) AS groupConcatNewIds,
11343                            CASE
11344                                WHEN q.is_amountNew > 0 THEN
11345                                    SUM(q.revenueNew / q.newObjective) * 100
11346                                ELSE
11347                                    SUM(q.totalNew / q.newObjective) * 100
11348                                END
11349                            AS totalNewObjective,
11350                             CASE
11351                                WHEN q.is_amountNew > 0 THEN
11352                                    SUM(q.revenueNew / q.newObjectiveMonthly) * 100
11353                                ELSE
11354                                    SUM(q.totalNew / q.newObjectiveMonthly) * 100
11355                                END
11356                            AS totalNewObjectiveMonthly,
11357                             CASE
11358                                WHEN q.is_amountNew > 0 THEN
11359                                    SUM(q.revenueNew / q.newObjectiveYearly) * 100
11360                                ELSE
11361                                    SUM(q.totalNew / q.newObjectiveYearly) * 100
11362                                END
11363                            AS totalNewObjectiveYearly,
11364                            SUM(q.revenueNew) revenueNew,
11365                            q.totalVisit,
11366                            q.groupConcatVisitIds,
11367                            q.totalCall,
11368                            q.groupConcatCallIds,
11369                            q.priority,
11370                            SUM(q.is_amountIssue) AS is_amountIssue,
11371                            SUM(q.is_amountNew) AS is_amountNew,
11372                            SUM(q.is_amountAcceptance) AS is_amountAcceptance,
11373                            SUM(q.issueObjective) AS issueObjective,
11374                            SUM(q.issueObjectiveMonthly) AS issueObjectiveMonthly,
11375                            SUM(q.issueObjectiveYearly) AS issueObjectiveYearly,
11376                            SUM(q.newObjective) AS newObjective,
11377                            SUM(q.newObjectiveMonthly) AS newObjectiveMonthly,
11378                            SUM(q.newObjectiveYearly) AS newObjectiveYearly,
11379                            SUM(q.acceptanceObjective) AS acceptanceObjective,
11380                            SUM(q.acceptanceObjectiveMonthly) AS acceptanceObjectiveMonthly,
11381                            SUM(q.acceptanceObjectiveYearly) AS acceptanceObjectiveYearly
11382                            {$visitSubMainTableCols}
11383                        FROM (
11384                            SELECT
11385                                YEAR(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)) 'year',
11386                                MONTH(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)) 'month',
11387                                WEEK(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY)) 'week',
11388                                DATE_FORMAT(DATE_ADD(q.issue_date, INTERVAL - WEEKDAY(q.issue_date) DAY), '%W, %M %e') namedate,
11389                                COUNT(CASE WHEN q.issue_date IS NOT NULL THEN 1 END) issue_date,
11390                                0 acceptance_date,
11391                                0 created_at,
11392                                q.commercial,
11393                                btg.name budget_type,
11394                                GROUP_CONCAT(CASE WHEN q.issue_date IS NOT NULL THEN q.id END) AS groupConcatIds,
11395
11396                                SUM(CASE WHEN q.issue_date IS NOT NULL THEN q.amount END) AS revenueIssue,
11397                                NULL groupConcatCreatedAtIds,
11398                                0 totalIssueLessThan5,
11399                                NULL groupConcatIdsIssueLessThan5,
11400                                NULL groupConcatAcceptanceIds,
11401
11402                                0 revenueAcceptance,
11403                                COUNT(CASE WHEN bs.name = 'Rechazado' THEN 1 END) AS totalRejected,
11404                                GROUP_CONCAT(DISTINCT CASE WHEN bs.name = 'Rechazado' THEN q.id END) AS groupConcatRejectedIds,
11405                                COALESCE(SUM(CASE WHEN bs.name = 'Rechazado' THEN q.amount END), 0) AS revenueRejected,
11406                                COUNT(CASE WHEN ct.name = 'Nuevo' THEN 1 END) AS totalNew,
11407                                GROUP_CONCAT(DISTINCT CASE WHEN ct.name = 'Nuevo' THEN q.id END) AS groupConcatNewIds,
11408
11409                                COALESCE(SUM(CASE WHEN ct.name = 'Nuevo' THEN q.amount END), 0) revenueNew,
11410                                0 totalVisit,
11411                                NULL groupConcatVisitIds,
11412                                0 totalCall,
11413                                NULL groupConcatCallIds,
11414                                btg.priority,
11415                                CAST(
11416                                    CASE
11417                                        WHEN bg.issue_objective IS NOT NULL THEN bg.is_amount
11418                                        WHEN bgou.issue_objective IS NOT NULL THEN bgou.is_amount
11419                                        WHEN bg1.issue_objective IS NOT NULL THEN bg1.is_amount
11420                                        WHEN bgor.issue_objective IS NOT NULL THEN bgor.is_amount
11421                                        WHEN bgde.issue_objective IS NOT NULL THEN bgde.is_amount
11422                                        WHEN bg.issue_objective IS NULL AND bg1.issue_objective IS NULL THEN {$businessGoalsDefault->is_amount}
11423                                    END
11424                                AS DOUBLE) AS is_amountIssue,
11425                                CAST(
11426                                    CASE
11427                                        WHEN bg.new_objective IS NOT NULL THEN bg.is_amount
11428                                        WHEN bgou.new_objective IS NOT NULL THEN bgou.is_amount
11429                                        WHEN bg1.new_objective IS NOT NULL THEN bg1.is_amount
11430                                        WHEN bgor.new_objective IS NOT NULL THEN bgor.is_amount
11431                                        WHEN bgde.new_objective IS NOT NULL THEN bgde.is_amount
11432                                        WHEN bg.new_objective IS NULL AND bg1.new_objective IS NULL THEN {$businessGoalsDefault->is_amount}
11433                                    END
11434                                AS DOUBLE) AS is_amountNew,
11435                                0 is_amountAcceptance,
11436                                CAST(
11437                                    CASE
11438                                        WHEN bg.issue_objective IS NOT NULL THEN bg.issue_objective
11439                                        WHEN bgou.issue_objective IS NOT NULL THEN bgou.issue_objective
11440                                        WHEN bg1.issue_objective IS NOT NULL THEN bg1.issue_objective
11441                                        WHEN bgor.issue_objective IS NOT NULL THEN bgor.issue_objective
11442                                        WHEN bgde.issue_objective IS NOT NULL THEN bgde.issue_objective
11443                                        WHEN bg.issue_objective IS NULL AND bg1.issue_objective IS NULL THEN {$businessGoalsDefault->issue_objective}
11444                                    END {$aggregatedByCalc}
11445                                AS DOUBLE) AS issueObjective,
11446                                CAST(
11447                                    CASE
11448                                        WHEN bg.issue_objective IS NOT NULL THEN bg.issue_objective
11449                                        WHEN bgou.issue_objective IS NOT NULL THEN bgou.issue_objective
11450                                        WHEN bg1.issue_objective IS NOT NULL THEN bg1.issue_objective
11451                                        WHEN bgor.issue_objective IS NOT NULL THEN bgor.issue_objective
11452                                        WHEN bgde.issue_objective IS NOT NULL THEN bgde.issue_objective
11453                                        WHEN bg.issue_objective IS NULL AND bg1.issue_objective IS NULL THEN {$businessGoalsDefault->issue_objective}
11454                                    END
11455                                AS DOUBLE) AS issueObjectiveMonthly,
11456                                CAST(
11457                                    CASE
11458                                        WHEN bg.issue_objective IS NOT NULL THEN bg.issue_objective
11459                                        WHEN bgou.issue_objective IS NOT NULL THEN bgou.issue_objective
11460                                        WHEN bg1.issue_objective IS NOT NULL THEN bg1.issue_objective
11461                                        WHEN bgor.issue_objective IS NOT NULL THEN bgor.issue_objective
11462                                        WHEN bgde.issue_objective IS NOT NULL THEN bgde.issue_objective
11463                                        WHEN bg.issue_objective IS NULL AND bg1.issue_objective IS NULL THEN {$businessGoalsDefault->issue_objective}
11464                                    END * 12
11465                                AS DOUBLE) AS issueObjectiveYearly,
11466                                CAST(
11467                                    CASE
11468                                        WHEN bg.new_objective IS NOT NULL THEN bg.new_objective
11469                                        WHEN bgou.new_objective IS NOT NULL THEN bgou.new_objective
11470                                        WHEN bg1.new_objective IS NOT NULL THEN bg1.new_objective
11471                                        WHEN bgor.new_objective IS NOT NULL THEN bgor.new_objective
11472                                        WHEN bgde.new_objective IS NOT NULL THEN bgde.new_objective
11473                                        WHEN bg.new_objective IS NULL AND bg1.new_objective IS NULL THEN {$businessGoalsDefault->new_objective}
11474                                    END {$aggregatedByCalc}
11475                                AS DOUBLE) AS newObjective,
11476                                CAST(
11477                                    CASE
11478                                        WHEN bg.new_objective IS NOT NULL THEN bg.new_objective
11479                                        WHEN bgou.new_objective IS NOT NULL THEN bgou.new_objective
11480                                        WHEN bg1.new_objective IS NOT NULL THEN bg1.new_objective
11481                                        WHEN bgor.new_objective IS NOT NULL THEN bgor.new_objective
11482                                        WHEN bgde.new_objective IS NOT NULL THEN bgde.new_objective
11483                                        WHEN bg.new_objective IS NULL AND bg1.new_objective IS NULL THEN {$businessGoalsDefault->new_objective}
11484                                    END
11485                                AS DOUBLE) AS newObjectiveMonthly,
11486                                CAST(
11487                                    CASE
11488                                        WHEN bg.new_objective IS NOT NULL THEN bg.new_objective
11489                                        WHEN bgou.new_objective IS NOT NULL THEN bgou.new_objective
11490                                        WHEN bg1.new_objective IS NOT NULL THEN bg1.new_objective
11491                                        WHEN bgor.new_objective IS NOT NULL THEN bgor.new_objective
11492                                        WHEN bgde.new_objective IS NOT NULL THEN bgde.new_objective
11493                                        WHEN bg.new_objective IS NULL AND bg1.new_objective IS NULL THEN {$businessGoalsDefault->new_objective}
11494                                    END * 12
11495                                AS DOUBLE) AS newObjectiveYearly,
11496                                0 acceptanceObjective,
11497                                0 acceptanceObjectiveMonthly,
11498                                0 acceptanceObjectiveYearly
11499                                {$visitSubTableCols}
11500                            FROM
11501                            tbl_quotations q
11502                                LEFT JOIN tbl_sources s ON s.source_id = q.source_id
11503                                LEFT JOIN tbl_budget_status bs ON bs.budget_status_id = q.budget_status_id
11504                                LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
11505                                LEFT JOIN tbl_budget_type_groups btg ON btg.budget_type_group_id = bt.budget_type_group_id
11506                                LEFT JOIN tbl_customer_types ct ON q.customer_type_id = ct.customer_type_id
11507                                LEFT JOIN tbl_users u ON q.commercial = u.name
11508                                LEFT JOIN tbl_roles r ON u.role_id = r.role_id
11509                                LEFT JOIN tbl_business_goals bg ON bg.budget_type_group_id = btg.budget_type_group_id AND bg.user_id = u.id
11510                                LEFT JOIN tbl_business_goals bg1 ON bg1.budget_type_group_id = btg.budget_type_group_id AND bg1.role_id = r.role_id
11511                                LEFT JOIN tbl_business_goals bgde ON bgde.budget_type_group_id = btg.budget_type_group_id AND bgde.is_default = 1
11512                                LEFT JOIN business_goal_objective_users bgou ON bgou.user_id = u.id
11513                                LEFT JOIN business_goal_objective_roles bgor ON bgor.role_id = r.role_id
11514                            WHERE
11515                                q.budget_type_id != 7
11516                                AND q.budget_type_id IS NOT NULL
11517                                AND q.for_add = 0
11518                                AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
11519                                AND bt.include = 1
11520                                AND (q.commercial IS NOT NULL AND q.commercial != '')
11521                                {$where}
11522                                {$whereYear}
11523                            GROUP BY
11524                                {$groupBy}
11525                        ) q
11526                        GROUP BY
11527                            {$groupBy} WITH ROLLUP
11528
11529                        UNION ALL
11530
11531                        SELECT
11532                            q.year,
11533                            q.month,
11534                            q.week,
11535                            q.namedate,
11536                            q.issue_date,
11537                            SUM(q.acceptance_date) AS acceptance_date,
11538                            q.created_at,
11539                            q.commercial,
11540                            q.budget_type,
11541                            q.groupConcatIds,
11542                            q.issueObjective totalIssueObjective,
11543                            q.issueObjectiveMonthly totalIssueObjectiveMonthly,
11544                            q.issueObjectiveYearly totalIssueObjectiveYearly,
11545                            q.revenueIssue,
11546                            q.groupConcatCreatedAtIds,
11547                            q.totalIssueLessThan5,
11548                            q.groupConcatIdsIssueLessThan5,
11549                            GROUP_CONCAT(q.groupConcatAcceptanceIds) AS groupConcatAcceptanceIds,
11550                            CASE
11551                                WHEN q.is_amountAcceptance > 0 THEN
11552                                    SUM(q.revenueAcceptance / q.acceptanceObjective) * 100
11553                                ELSE
11554                                    SUM(q.acceptance_date / q.acceptanceObjective) * 100
11555                                END
11556                            AS totalAcceptanceObjective,
11557                            CASE
11558                                WHEN q.is_amountAcceptance > 0 THEN
11559                                    SUM(q.revenueAcceptance / q.acceptanceObjectiveMonthly) * 100
11560                                ELSE
11561                                    SUM(q.acceptance_date / q.acceptanceObjectiveMonthly) * 100
11562                                END
11563                            AS totalAcceptanceObjectiveMonthly,
11564                            CASE
11565                                WHEN q.is_amountAcceptance > 0 THEN
11566                                    SUM(q.revenueAcceptance / q.acceptanceObjectiveYearly) * 100
11567                                ELSE
11568                                    SUM(q.acceptance_date / q.acceptanceObjectiveYearly) * 100
11569                                END
11570                            AS totalAcceptanceObjectiveYearly,
11571                            SUM(q.revenueAcceptance) revenueAcceptance,
11572                            q.totalRejected,
11573                            q.groupConcatRejectedIds,
11574                            q.revenueRejected,
11575                            q.totalNew,
11576                            q.groupConcatNewIds,
11577                            q.newObjective totalNewObjective,
11578                            q.newObjectiveMonthly totalNewObjectiveMonthly,
11579                            q.newObjectiveYearly totalNewObjectiveYearly,
11580                            q.revenueNew,
11581                            q.totalVisit,
11582                            q.groupConcatVisitIds,
11583                            q.totalCall,
11584                            q.groupConcatCallIds,
11585                            q.priority,
11586                            SUM(q.is_amountIssue) AS is_amountIssue,
11587                            q.is_amountNew,
11588                            q.is_amountAcceptance,
11589                            SUM(q.issueObjective) AS issueObjective,
11590                            SUM(q.issueObjectiveMonthly) AS issueObjectiveMonthly,
11591                            SUM(q.issueObjectiveYearly) AS issueObjectiveYearly,
11592                            SUM(q.newObjective) AS newObjective,
11593                            SUM(q.newObjectiveMonthly) AS newObjectiveMonthly,
11594                            SUM(q.newObjectiveYearly) AS newObjectiveYearly,
11595                            SUM(q.acceptanceObjective) AS acceptanceObjective,
11596                            SUM(q.acceptanceObjectiveMonthly) AS acceptanceObjectiveMonthly,
11597                            SUM(q.acceptanceObjectiveYearly) AS acceptanceObjectiveYearly
11598                            {$visitSubMainTableCols}
11599                        FROM (
11600                            SELECT
11601                                YEAR(DATE_ADD(q.acceptance_date, INTERVAL - WEEKDAY(q.acceptance_date) DAY)) 'year',
11602                                MONTH(DATE_ADD(q.acceptance_date, INTERVAL - WEEKDAY(q.acceptance_date) DAY)) 'month',
11603                                WEEK(DATE_ADD(q.acceptance_date, INTERVAL - WEEKDAY(q.acceptance_date) DAY)) 'week',
11604                                DATE_FORMAT(DATE_ADD(q.acceptance_date, INTERVAL - WEEKDAY(q.acceptance_date) DAY), '%W, %M %e') namedate,
11605                                0 issue_date,
11606                                COUNT(CASE WHEN q.acceptance_date IS NOT NULL THEN 1 END) acceptance_date,
11607                                0 created_at,
11608                                q.commercial,
11609                                btg.name budget_type,
11610                                NULL groupConcatIds,
11611
11612                                0 revenueIssue,
11613                                NULL groupConcatCreatedAtIds,
11614                                0 totalIssueLessThan5,
11615                                NULL groupConcatIdsIssueLessThan5,
11616                                GROUP_CONCAT(CASE WHEN q.acceptance_date IS NOT NULL THEN q.id END) AS groupConcatAcceptanceIds,
11617
11618                                COALESCE(SUM(CASE WHEN q.acceptance_date IS NOT NULL THEN q.amount END), 0) AS revenueAcceptance,
11619                                0 totalRejected,
11620                                NULL groupConcatRejectedIds,
11621                                0 revenueRejected,
11622                                0 totalNew,
11623                                NULL groupConcatNewIds,
11624                                NULL totalNewObjective,
11625                                NULL totalNewObjectiveMonthly,
11626                                NULL totalNewObjectiveYearly,
11627                                0 revenueNew,
11628                                0 totalVisit,
11629                                NULL groupConcatVisitIds,
11630                                0 totalCall,
11631                                NULL groupConcatCallIds,
11632                                btg.priority,
11633                                0 is_amountIssue,
11634                                0 is_amountNew,
11635                                CAST(
11636                                    CASE
11637                                        WHEN bg.acceptance_objective IS NOT NULL THEN bg.is_amount
11638                                        WHEN bgou.acceptance_objective IS NOT NULL THEN bgou.is_amount
11639                                        WHEN bg1.acceptance_objective IS NOT NULL THEN bg1.is_amount
11640                                        WHEN bgor.acceptance_objective IS NOT NULL THEN bgor.is_amount
11641                                        WHEN bgde.acceptance_objective IS NOT NULL THEN bgde.is_amount
11642                                        WHEN bg.acceptance_objective IS NULL AND bg1.acceptance_objective IS NULL THEN {$businessGoalsDefault->is_amount}
11643                                    END
11644                                AS DOUBLE) AS is_amountAcceptance,
11645                                0 issueObjective,
11646                                0 issueObjectiveMonthly,
11647                                0 issueObjectiveYearly,
11648                                0 newObjective,
11649                                0 newObjectiveMonthly,
11650                                0 newObjectiveYearly,
11651                                CAST(
11652                                    CASE
11653                                        WHEN bg.acceptance_objective IS NOT NULL THEN bg.acceptance_objective
11654                                        WHEN bgou.acceptance_objective IS NOT NULL THEN bgou.acceptance_objective
11655                                        WHEN bg1.acceptance_objective IS NOT NULL THEN bg1.acceptance_objective
11656                                        WHEN bgor.acceptance_objective IS NOT NULL THEN bgor.acceptance_objective
11657                                        WHEN bgde.acceptance_objective IS NOT NULL THEN bgde.acceptance_objective
11658                                        WHEN bg.acceptance_objective IS NULL AND bg1.acceptance_objective IS NULL THEN {$businessGoalsDefault->acceptance_objective}
11659                                    END {$aggregatedByCalc}
11660                                AS DOUBLE) AS acceptanceObjective,
11661                                CAST(
11662                                    CASE
11663                                        WHEN bg.acceptance_objective IS NOT NULL THEN bg.acceptance_objective
11664                                        WHEN bgou.acceptance_objective IS NOT NULL THEN bgou.acceptance_objective
11665                                        WHEN bg1.acceptance_objective IS NOT NULL THEN bg1.acceptance_objective
11666                                        WHEN bgor.acceptance_objective IS NOT NULL THEN bgor.acceptance_objective
11667                                        WHEN bgde.acceptance_objective IS NOT NULL THEN bgde.acceptance_objective
11668                                        WHEN bg.acceptance_objective IS NULL AND bg1.acceptance_objective IS NULL THEN {$businessGoalsDefault->acceptance_objective}
11669                                    END
11670                                AS DOUBLE) AS acceptanceObjectiveMonthly,
11671                                CAST(
11672                                    CASE
11673                                        WHEN bg.acceptance_objective IS NOT NULL THEN bg.acceptance_objective
11674                                        WHEN bgou.acceptance_objective IS NOT NULL THEN bgou.acceptance_objective
11675                                        WHEN bg1.acceptance_objective IS NOT NULL THEN bg1.acceptance_objective
11676                                        WHEN bgor.acceptance_objective IS NOT NULL THEN bgor.acceptance_objective
11677                                        WHEN bgde.acceptance_objective IS NOT NULL THEN bgde.acceptance_objective
11678                                        WHEN bg.acceptance_objective IS NULL AND bg1.acceptance_objective IS NULL THEN {$businessGoalsDefault->acceptance_objective}
11679                                    END * 12
11680                                AS DOUBLE) AS acceptanceObjectiveYearly
11681                                {$visitSubTableCols}
11682                            FROM
11683                            tbl_quotations q
11684                                LEFT JOIN tbl_sources s ON s.source_id = q.source_id
11685                                LEFT JOIN tbl_budget_status bs ON bs.budget_status_id = q.budget_status_id
11686                                LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
11687                                LEFT JOIN tbl_budget_type_groups btg ON btg.budget_type_group_id = bt.budget_type_group_id
11688                                LEFT JOIN tbl_customer_types ct ON q.customer_type_id = ct.customer_type_id
11689                                LEFT JOIN tbl_users u ON q.commercial = u.name
11690                                LEFT JOIN tbl_roles r ON u.role_id = r.role_id
11691                                LEFT JOIN tbl_business_goals bg ON bg.budget_type_group_id = btg.budget_type_group_id AND bg.user_id = u.id
11692                                LEFT JOIN tbl_business_goals bg1 ON bg1.budget_type_group_id = btg.budget_type_group_id AND bg1.role_id = r.role_id
11693                                LEFT JOIN tbl_business_goals bgde ON bgde.budget_type_group_id = btg.budget_type_group_id AND bgde.is_default = 1
11694                                LEFT JOIN business_goal_objective_users bgou ON bgou.user_id = u.id
11695                                LEFT JOIN business_goal_objective_roles bgor ON bgor.role_id = r.role_id
11696                            WHERE
11697                                q.budget_type_id != 7
11698                                AND q.budget_type_id IS NOT NULL
11699                                AND q.for_add = 0
11700                                AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
11701                                AND bt.include = 1
11702                                AND (q.commercial IS NOT NULL AND q.commercial != '')
11703                                {$whereAcceptanceDate}
11704                                {$whereYear}
11705                            GROUP BY
11706                                {$groupBy}
11707                        ) q
11708                        GROUP BY
11709                            {$groupBy} WITH ROLLUP
11710
11711                        UNION ALL
11712
11713                        SELECT
11714                            q.year,
11715                            q.month,
11716                            q.week,
11717                            q.namedate,
11718                            q.issue_date,
11719                            q.acceptance_date,
11720                            SUM(q.created_at) AS created_at,
11721                            q.commercial,
11722                            q.budget_type,
11723                            q.groupConcatIds,
11724                            q.issueObjective totalIssueObjective,
11725                            q.issueObjectiveMonthly totalIssueObjectiveMonthly,
11726                            q.issueObjectiveYearly totalIssueObjectiveYearly,
11727                            q.revenueIssue,
11728                            GROUP_CONCAT(q.groupConcatCreatedAtIds) AS groupConcatCreatedAtIds,
11729                            SUM(q.totalIssueLessThan5) AS totalIssueLessThan5,
11730                            GROUP_CONCAT(q.groupConcatIdsIssueLessThan5) AS groupConcatIdsIssueLessThan5,
11731                            NULL groupConcatAcceptanceIds,
11732                            0 totalAcceptanceObjective,
11733                            0 totalAcceptanceObjectiveMonthly,
11734                            0 totalAcceptanceObjectiveYearly,
11735                            SUM(q.revenueAcceptance) revenueAcceptance,
11736                            q.totalRejected,
11737                            q.groupConcatRejectedIds,
11738                            q.revenueRejected,
11739                            q.totalNew,
11740                            q.groupConcatNewIds,
11741                            q.newObjective totalNewObjective,
11742                            q.newObjectiveMonthly totalNewObjectiveMonthly,
11743                            q.newObjectiveYearly totalNewObjectiveYearly,
11744                            q.revenueNew,
11745                            q.totalVisit,
11746                            q.groupConcatVisitIds,
11747                            q.totalCall,
11748                            q.groupConcatCallIds,
11749                            q.priority,
11750                            SUM(q.is_amountIssue) AS is_amountIssue,
11751                            q.is_amountNew,
11752                            q.is_amountAcceptance,
11753                            SUM(q.issueObjective) AS issueObjective,
11754                            SUM(q.issueObjectiveMonthly) AS issueObjectiveMonthly,
11755                            SUM(q.issueObjectiveYearly) AS issueObjectiveYearly,
11756                            SUM(q.newObjective) AS newObjective,
11757                            SUM(q.newObjectiveMonthly) AS newObjectiveMonthly,
11758                            SUM(q.newObjectiveYearly) AS newObjectiveYearly,
11759                            SUM(q.acceptanceObjective) AS acceptanceObjective,
11760                            SUM(q.acceptanceObjectiveMonthly) AS acceptanceObjectiveMonthly,
11761                            SUM(q.acceptanceObjectiveYearly) AS acceptanceObjectiveYearly
11762                            {$visitSubMainTableCols}
11763                        FROM (
11764                            SELECT
11765                                YEAR(DATE_ADD(q.created_at, INTERVAL - WEEKDAY(q.created_at) DAY)) 'year',
11766                                MONTH(DATE_ADD(q.created_at, INTERVAL - WEEKDAY(q.created_at) DAY)) 'month',
11767                                WEEK(DATE_ADD(q.created_at, INTERVAL - WEEKDAY(q.created_at) DAY)) 'week',
11768                                DATE_FORMAT(DATE_ADD(q.created_at, INTERVAL - WEEKDAY(q.created_at) DAY), '%W, %M %e') namedate,
11769                                0 issue_date,
11770                                0 acceptance_date,
11771                                COUNT(CASE WHEN q.created_at IS NOT NULL THEN 1 END) created_at,
11772                                q.commercial,
11773                                btg.name budget_type,
11774                                NULL groupConcatIds,
11775
11776                                0 revenueIssue,
11777                                GROUP_CONCAT(CASE WHEN q.created_at IS NOT NULL THEN q.id END) AS groupConcatCreatedAtIds,
11778                                COUNT(CASE WHEN ABS(DATEDIFF(q.created_at, q.issue_date)) < 5 THEN 1 END) AS totalIssueLessThan5,
11779                                GROUP_CONCAT(CASE WHEN ABS(DATEDIFF(q.created_at, q.issue_date)) < 5 THEN q.id END) AS groupConcatIdsIssueLessThan5,
11780                                NULL groupConcatAcceptanceIds,
11781
11782                                0 AS revenueAcceptance,
11783                                0 totalRejected,
11784                                NULL groupConcatRejectedIds,
11785                                0 revenueRejected,
11786                                0 totalNew,
11787                                NULL groupConcatNewIds,
11788                                NULL totalNewObjective,
11789                                NULL totalNewObjectiveMonthly,
11790                                NULL totalNewObjectiveYearly,
11791                                0 revenueNew,
11792                                0 totalVisit,
11793                                NULL groupConcatVisitIds,
11794                                0 totalCall,
11795                                NULL groupConcatCallIds,
11796                                btg.priority,
11797                                0 is_amountIssue,
11798                                0 is_amountNew,
11799                                0 is_amountAcceptance,
11800                                0 issueObjective,
11801                                0 issueObjectiveMonthly,
11802                                0 issueObjectiveYearly,
11803                                0 newObjective,
11804                                0 newObjectiveMonthly,
11805                                0 newObjectiveYearly,
11806                                0 acceptanceObjective,
11807                                0 acceptanceObjectiveMonthly,
11808                                0 acceptanceObjectiveYearly
11809                                {$visitSubTableCols}
11810                            FROM
11811                            tbl_quotations q
11812                                LEFT JOIN tbl_sources s ON s.source_id = q.source_id
11813                                LEFT JOIN tbl_budget_status bs ON bs.budget_status_id = q.budget_status_id
11814                                LEFT JOIN tbl_budget_types bt ON q.budget_type_id = bt.budget_type_id
11815                                LEFT JOIN tbl_budget_type_groups btg ON btg.budget_type_group_id = bt.budget_type_group_id
11816                                LEFT JOIN tbl_customer_types ct ON q.customer_type_id = ct.customer_type_id
11817                                LEFT JOIN tbl_users u ON q.commercial = u.name
11818                                LEFT JOIN tbl_roles r ON u.role_id = r.role_id
11819                            WHERE
11820                                q.budget_type_id != 7
11821                                AND q.budget_type_id IS NOT NULL
11822                                AND q.for_add = 0
11823                                AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
11824                                AND bt.include = 1
11825                                AND (q.commercial IS NOT NULL AND q.commercial != '')
11826                                {$whereCreatedAt}
11827                                {$whereYear}
11828                            GROUP BY
11829                                {$groupBy}
11830                        ) q
11831                        GROUP BY
11832                            {$groupBy} WITH ROLLUP
11833
11834                        UNION ALL
11835
11836                        SELECT
11837                            YEAR(DATE_ADD(q.visit_date, INTERVAL - WEEKDAY(q.visit_date) DAY)) YEAR,
11838                            MONTH(DATE_ADD(q.visit_date, INTERVAL - WEEKDAY(q.visit_date) DAY)) MONTH,
11839                            WEEK(DATE_ADD(q.visit_date, INTERVAL - WEEKDAY(q.visit_date) DAY)) WEEK,
11840                            DATE_FORMAT(DATE_ADD(visit_date, INTERVAL - WEEKDAY(q.visit_date) DAY), '%W, %M %e') namedate,
11841                            0 issue_date,
11842                            0 acceptance_date,
11843                            0 created_at,
11844                            commercial,
11845                            NULL budget_type,
11846                            NULL groupConcatIds,
11847                            NULL totalIssueObjective,
11848                            NULL totalIssueObjectiveMonthly,
11849                            NULL totalIssueObjectiveYearly,
11850                            0 revenueIssue,
11851                            NULL groupConcatCreatedAtIds,
11852                            0 totalIssueLessThan5,
11853                            NULL groupConcatIdsIssueLessThan5,
11854                            NULL groupConcatAcceptanceIds,
11855                            NULL totalAcceptanceObjective,NULL totalAcceptanceObjectiveMonthly,
11856                            NULL totalAcceptanceObjectiveYearly,
11857                            0 revenueAcceptance,
11858                            0 totalRejected,
11859                            NULL groupConcatRejectedIds,
11860                            0 revenueRejected,
11861                            0 totalNew,
11862                            NULL groupConcatNewIds,
11863                            NULL totalNewObjective,NULL totalNewObjectiveMonthly,
11864                            NULL totalNewObjectiveYearly,
11865                            0 revenueNew,
11866                            COUNT(CASE WHEN q.visit_date IS NOT NULL AND q.visit_call = 'Visita' THEN 1 END) totalVisit,
11867                            GROUP_CONCAT(CASE WHEN q.visit_date IS NOT NULL AND q.visit_call = 'Visita' THEN q.id END) AS groupConcatVisitIds,
11868                            COUNT(CASE WHEN q.visit_date IS NOT NULL AND q.visit_call = 'Llamada' THEN 1 END) totalCall,
11869                            GROUP_CONCAT(CASE WHEN q.visit_date IS NOT NULL AND q.visit_call = 'Llamada' THEN q.id END) AS groupConcatCallIds,
11870                            0 priority,
11871                            0 is_amountIssue,
11872                            0 is_amountNew,
11873                            0 is_amountAcceptance,
11874                            0 issueObjective,
11875                            0 issueObjectiveMonthly,
11876                            0 issueObjectiveYearly,
11877                            0 newObjective,
11878                            0 newObjectiveMontly,
11879                            0 newObjectiveYearly,
11880                            0 acceptanceObjective,
11881                            0 acceptanceObjectiveMonthly,
11882                            0 acceptanceObjectiveYearly
11883                            {$visitCols}
11884                        FROM
11885                            tbl_pipelines q
11886                        LEFT JOIN tbl_users u ON q.commercial = u.name
11887                        LEFT JOIN tbl_roles r ON u.role_id = r.role_id
11888                        LEFT JOIN tbl_visit_types v ON q.visit_type_id = v.visit_type_id
11889                        WHERE q.visit_date IS NOT NULL
11890                            {$whereVisit}
11891                        GROUP BY
11892                            {$groupBy}
11893                             WITH ROLLUP
11894                    ) q
11895                    WHERE
11896                        q.year NOT IN (2021, 2022)
11897                    GROUP BY
11898                        {$groupBy}
11899                    {$gO}";
11900
11901        // FIRE-1145: domain-tagged cache (was Cache::get/put + Cache::flush()).
11902        // Note: cache the post-processed (parsed) result, matching the prior behaviour.
11903        $result = ResultCache::remember('quotations', $query, 600, function () use ($query, $groupByFilter, $aggregatedBy) {
11904            $rows = DB::select($query);
11905            $structureData = new StructureData;
11906
11907            return $structureData->parse($rows, $groupByFilter, $aggregatedBy);
11908        });
11909
11910        return response([
11911            'message' => 'OK',
11912            'data' => $result,
11913        ]);
11914
11915        // } catch (\Exception $e) {
11916        //     return response(['message' => 'KO', 'error' => $e->getMessage()]);
11917        // }
11918    }
11919
11920    public function list_quotations_deleted($companyId): ResponseFactory|HttpResponse
11921    {
11922
11923        try {
11924
11925            $companyId = addslashes((string) $companyId);
11926            $where = '';
11927
11928            if ($companyId != 0) {
11929                $where = " a.company_id = {$companyId} ";
11930            } else {
11931                $where = " a.company_id IN ({$this->companyId})";
11932            }
11933
11934            $query = "SELECT
11935                        a.id,
11936                        a.quote_id,
11937                        a.internal_quote_id,
11938                        a.company_id,
11939                        b.name company_name,
11940                        a.client,
11941                        c.name client_type,
11942                        c.customer_type_id,
11943                        s.name segment,
11944                        s.segment_id,
11945                        a.request_date,
11946                        a.visit_date,
11947                        a.issue_date,
11948                        a.acceptance_date,
11949                        a.internal_quote_id,
11950                        DATE_FORMAT(a.request_date, '%d/%m/%Y') request_date_translate,
11951                        DATE_FORMAT(a.issue_date, '%d/%m/%Y') issue_date_translate,
11952                        DATE_FORMAT(a.acceptance_date, '%d/%m/%Y') acceptance_date_translate,
11953                        DATE_FORMAT(a.last_follow_up_date, '%d/%m/%Y') last_follow_up_date_translate,
11954                        DATE_FORMAT(a.created_at, '%d/%m/%Y') created_at_translate,
11955                        DATE_FORMAT(a.accepted_at, '%d/%m/%Y') accepted_at_translate,
11956                        a.phone_number,
11957                        a.email,
11958                        a.duration,
11959                        a.order_number,
11960                        d.name 'type',
11961                        d.budget_type_id,
11962                        e.name 'status',
11963                        e.budget_status_id,
11964                        f.name as source,
11965                        f.source_id,
11966                        a.amount,
11967                        g.name reason_for_not_following_up,
11968                        a.reason_for_not_following_up_id,
11969                        a.reason_for_rejection_id,
11970                        a.last_follow_up_date,
11971                        a.last_follow_up_comment,
11972                        CASE WHEN a.reason_for_rejection_id IS NULL THEN a.reason_for_rejection ELSE h.name END reason_for_rejection,
11973                        a.commercial,
11974                        a.user_commercial_by_g3w,
11975                        a.user_create_by_g3w,
11976                        a.created_by,
11977                        a.created_at,
11978                        a.updated_by,
11979                        a.updated_at,
11980                        a.total_sent,
11981                        a.has_attachment,
11982                        a.for_approval,
11983                        a.approval_type,
11984                        a.requires_technical_office,
11985                        a.box_work_g3w,
11986                        a.people_assigned_to_the_job,
11987                        a.duration_of_job_in_days,
11988                        a.estimated_cost_of_materials,
11989                        a.budget_margin_enabled,
11990                        a.question_enabled,
11991                        a.cost_of_labor,
11992                        a.total_cost_of_job,
11993                        CASE WHEN a.budget_margin_enabled > 0 THEN a.invoice_margin ELSE NULL END invoice_margin,
11994                        CASE WHEN a.budget_margin_enabled > 0 THEN a.margin_for_the_company ELSE NULL END margin_for_the_company,
11995                        a.margin_on_invoice_per_day_per_worker,
11996                        a.revenue_per_date_per_worked,
11997                        a.commission_cost,
11998                        a.commission_pct,
11999                        a.gross_margin,
12000                        a.labor_percentage,
12001                        a.question_ids,
12002                        a.question_ids_no,
12003                        a.approved_at,
12004                        a.approved_by,
12005                        a.rejected_at,
12006                        a.rejected_by,
12007                        a.approved_at_v2,
12008                        a.approved_by_v2,
12009                        a.rejected_at_v2,
12010                        a.rejected_by_v2,
12011                        a.accepted_at,
12012                        a.accepted_by,
12013                        a.is_validated,
12014                        a.resource_id,
12015                        a.x_status,
12016                        a.likehood,
12017                        a.sync_import,
12018                        a.sync_import_edited,
12019                        a.g3w_warning,
12020                        a.g3w_warning_fields
12021                    FROM tbl_quotations_deleted a                    
12022                    LEFT JOIN tbl_companies b ON a.company_id = b.company_id
12023                    LEFT JOIN tbl_customer_types c ON a.customer_type_id = c.customer_type_id
12024                    LEFT JOIN tbl_segments s ON a.segment_id = s.segment_id
12025                    LEFT JOIN tbl_budget_types d ON a.budget_type_id = d.budget_type_id
12026                    LEFT JOIN tbl_budget_status e ON a.budget_status_id = e.budget_status_id
12027                    LEFT JOIN tbl_sources f ON a.source_id = f.source_id
12028                    LEFT JOIN tbl_reason_for_not_following_up g ON a.reason_for_not_following_up_id = g.reason_for_not_following_up_id
12029                    LEFT JOIN tbl_reason_for_rejection h ON a.reason_for_rejection_id = h.reason_for_rejection_id
12030                    WHERE {$where}
12031                    ORDER BY a.updated_at DESC";
12032
12033            $result = DB::select($query);
12034
12035            return response([
12036                'message' => 'OK',
12037                'data' => $result,
12038            ]);
12039        } catch (\Exception $e) {
12040            report(AppException::fromException($e, 'LIST_QUOTATIONS_DELETED_EXCEPTION'));
12041
12042            return response(['message' => 'KO', 'error' => $e->getMessage()]);
12043        }
12044    }
12045
12046    public function delete_sengrid($id): ResponseFactory|HttpResponse
12047    {
12048
12049        try {
12050
12051            $id = addslashes((string) $id);
12052
12053            $order = TblQuotations::where('id', $id)->first();
12054
12055            if ($order) {
12056                if ($order->x_message_id != null) {
12057                    TblSendgridWebhook::where('quotation_id', $id)->where('x_message_id', $order->x_message_id)->delete();
12058
12059                    TblQuotations::where('id', $id)->update(
12060                        [
12061                            'x_message_id' => null,
12062                            'x_status' => null,
12063                        ]
12064                    );
12065
12066                    // FIRE-1145: was Cache::flush() — delete_sengrid clears email status shown in list_quotations.
12067                    ResultCache::forgetDomain('quotations');
12068                }
12069            }
12070
12071            return response([
12072                'message' => 'OK',
12073            ]);
12074
12075        } catch (\Exception $e) {
12076            report(AppException::fromException($e, 'DELETE_SENDGRID_EXCEPTION'));
12077
12078            return response(['message' => 'KO', 'error' => $e->getMessage()]);
12079        }
12080    }
12081
12082    public function download_productivity_commercial(Request $request): ResponseFactory|HttpResponse
12083    {
12084
12085        try {
12086
12087            ini_set('max_execution_time', 123456);
12088
12089            $data = $request->all();
12090
12091            $result = $this->list_quotation_analytics_commercial_productivity($request);
12092            $result = $result->original['data'];
12093
12094            $spreadsheet = new Spreadsheet;
12095            $worksheet = new Worksheet($spreadsheet, 'Inputs');
12096            $spreadsheet->addSheet($worksheet, 0);
12097            $col = range('A', 'Z');
12098
12099            for ($i = 0; $i < 26; $i++) {
12100                $worksheet->getColumnDimension($col[$i])->setAutoSize(true);
12101                if ($i != 1) {
12102                    $worksheet->getStyle($col[$i])
12103                        ->getAlignment()
12104                        ->setHorizontal(Alignment::HORIZONTAL_CENTER);
12105                }
12106            }
12107
12108            $l = 1;
12109            $worksheet->setCellValue('A'.$l, 'Años');
12110
12111            if ($data['group_by'] == 1) {
12112                $worksheet->setCellValue('B'.$l, 'Comercials');
12113                $worksheet->setCellValue('C'.$l, 'Meses');
12114                $worksheet->setCellValue('D'.$l, 'Semanas');
12115            } else {
12116                $worksheet->setCellValue('B'.$l, 'Meses');
12117                $worksheet->setCellValue('C'.$l, 'Semanas');
12118                $worksheet->setCellValue('D'.$l, 'Comercials');
12119            }
12120
12121            $worksheet->setCellValue('E'.$l, 'Categorías de presupuesto');
12122            $worksheet->setCellValue('F'.$l, 'Presupuestos emitidos (#)');
12123            $worksheet->setCellValue('G'.$l, 'Presupuestos emitidos (€)');
12124            $worksheet->setCellValue('H'.$l, 'Presupuestos emitidos (Objetivo)');
12125            $worksheet->setCellValue('I'.$l, 'Presupuestos aceptados (#)');
12126            $worksheet->setCellValue('J'.$l, 'Presupuestos aceptados (€)');
12127            $worksheet->setCellValue('K'.$l, 'Presupuestos aceptados (Objetivo)');
12128            $worksheet->setCellValue('L'.$l, 'Presupuestos rechazados (#)');
12129            $worksheet->setCellValue('M'.$l, 'Presupuestos rechazados (€)');
12130            $worksheet->setCellValue('N'.$l, 'Presupuestos emitidos a nuevos clientes (#)');
12131            $worksheet->setCellValue('O'.$l, 'Presupuestos emitidos a nuevos clientes (€)');
12132            $worksheet->setCellValue('P'.$l, 'Presupuestos emitidos a nuevos clientes (Objetivo)');
12133            $worksheet->setCellValue('Q'.$l, 'Venta (Llamada #)');
12134            $worksheet->setCellValue('R'.$l, 'Venta (Visita #)');
12135            $worksheet->setCellValue('S'.$l, 'Servicio (Llamada #)');
12136            $worksheet->setCellValue('T'.$l, 'Servicio (Visita #)');
12137
12138            $l++;
12139
12140            foreach ($result as $item) {
12141
12142                $worksheet->setCellValue('A'.$l, $item['year']);
12143                $worksheet->setCellValue('F'.$l, $item['totalIssue']);
12144                $worksheet->setCellValue('G'.$l, $item['revenueIssue']);
12145                $worksheet->setCellValue('H'.$l, '-');
12146
12147                $worksheet->setCellValue('I'.$l, $item['totalAcceptance']);
12148                $worksheet->setCellValue('J'.$l, $item['revenueAcceptance']);
12149                $worksheet->setCellValue('K'.$l, '-');
12150
12151                $worksheet->setCellValue('L'.$l, $item['totalRejected']);
12152                $worksheet->setCellValue('M'.$l, $item['revenueRejected']);
12153
12154                $worksheet->setCellValue('N'.$l, $item['totalNew']);
12155                $worksheet->setCellValue('O'.$l, $item['revenueNew']);
12156                $worksheet->setCellValue('P'.$l, '-');
12157
12158                $worksheet->setCellValue('Q'.$l, $item['totalLlamada1']);
12159                $worksheet->setCellValue('R'.$l, $item['totalVisita1']);
12160
12161                $worksheet->setCellValue('S'.$l, $item['totalLlamada2']);
12162                $worksheet->setCellValue('T'.$l, $item['totalVisita2']);
12163
12164                $l++;
12165
12166                if ($data['group_by'] == 1) {
12167
12168                    if (count($item['commercials']) > 0) {
12169
12170                        foreach ($item['commercials'] as $c) {
12171                            $worksheet->setCellValue('A'.$l, $item['year']);
12172
12173                            $worksheet->setCellValue('B'.$l, $c['commercial']);
12174                            $worksheet->setCellValue('F'.$l, $c['totalIssue']);
12175                            $worksheet->setCellValue('G'.$l, $c['revenueIssue']);
12176                            $worksheet->setCellValue('H'.$l, $c['totalIssueObjectiveYearly']);
12177
12178                            $worksheet->setCellValue('I'.$l, $c['totalAcceptance']);
12179                            $worksheet->setCellValue('J'.$l, $c['revenueAcceptance']);
12180                            $worksheet->setCellValue('K'.$l, $c['totalAcceptanceObjectiveYearly']);
12181
12182                            $worksheet->setCellValue('L'.$l, $c['totalRejected']);
12183                            $worksheet->setCellValue('M'.$l, $c['revenueRejected']);
12184
12185                            $worksheet->setCellValue('N'.$l, $c['totalNew']);
12186                            $worksheet->setCellValue('O'.$l, $c['revenueNew']);
12187                            $worksheet->setCellValue('P'.$l, $c['totalNewObjectiveYearly']);
12188
12189                            $worksheet->setCellValue('Q'.$l, $c['totalLlamada1']);
12190                            $worksheet->setCellValue('R'.$l, $c['totalVisita1']);
12191
12192                            $worksheet->setCellValue('S'.$l, $c['totalLlamada2']);
12193                            $worksheet->setCellValue('T'.$l, $c['totalVisita2']);
12194                            $l++;
12195
12196                            if (isset($c['budget_types']) && count($c['budget_types']) > 0) {
12197                                foreach ($c['budget_types'] as $b) {
12198                                    $worksheet->setCellValue('A'.$l, $item['year']);
12199                                    $worksheet->setCellValue('B'.$l, $c['commercial']);
12200
12201                                    $worksheet->setCellValue('E'.$l, $b['name']);
12202                                    $worksheet->setCellValue('F'.$l, $b['totalIssue']);
12203                                    $worksheet->setCellValue('G'.$l, $b['revenueIssue']);
12204                                    $worksheet->setCellValue('H'.$l, $b['totalIssueObjective']);
12205
12206                                    $worksheet->setCellValue('I'.$l, $b['totalAcceptance']);
12207                                    $worksheet->setCellValue('J'.$l, $b['revenueAcceptance']);
12208                                    $worksheet->setCellValue('K'.$l, $b['totalAcceptanceObjective']);
12209
12210                                    $worksheet->setCellValue('L'.$l, $b['totalRejected']);
12211                                    $worksheet->setCellValue('M'.$l, $b['revenueRejected']);
12212
12213                                    $worksheet->setCellValue('N'.$l, $b['totalNew']);
12214                                    $worksheet->setCellValue('O'.$l, $b['revenueNew']);
12215                                    $worksheet->setCellValue('P'.$l, $b['totalNewObjective']);
12216
12217                                    $worksheet->setCellValue('Q'.$l, $b['totalLlamada1']);
12218                                    $worksheet->setCellValue('R'.$l, $b['totalVisita1']);
12219
12220                                    $worksheet->setCellValue('S'.$l, $b['totalLlamada2']);
12221                                    $worksheet->setCellValue('T'.$l, $b['totalVisita2']);
12222                                    $l++;
12223                                }
12224                            }
12225
12226                            if (isset($c['months']) && count($c['months']) > 0) {
12227                                foreach ($c['months'] as $m) {
12228                                    $worksheet->setCellValue('A'.$l, $item['year']);
12229                                    $worksheet->setCellValue('B'.$l, $c['commercial']);
12230
12231                                    $worksheet->setCellValue('C'.$l, $m['month']);
12232                                    $worksheet->setCellValue('F'.$l, $m['totalIssue']);
12233                                    $worksheet->setCellValue('G'.$l, $m['revenueIssue']);
12234                                    $worksheet->setCellValue('H'.$l, $m['totalIssueObjectiveMonthly']);
12235
12236                                    $worksheet->setCellValue('I'.$l, $m['totalAcceptance']);
12237                                    $worksheet->setCellValue('J'.$l, $m['revenueAcceptance']);
12238                                    $worksheet->setCellValue('K'.$l, $m['totalAcceptanceObjectiveMonthly']);
12239
12240                                    $worksheet->setCellValue('L'.$l, $m['totalRejected']);
12241                                    $worksheet->setCellValue('M'.$l, $m['revenueRejected']);
12242
12243                                    $worksheet->setCellValue('N'.$l, $m['totalNew']);
12244                                    $worksheet->setCellValue('O'.$l, $m['revenueNew']);
12245                                    $worksheet->setCellValue('P'.$l, $m['totalNewObjectiveMonthly']);
12246
12247                                    $worksheet->setCellValue('Q'.$l, $m['totalLlamada1']);
12248                                    $worksheet->setCellValue('R'.$l, $m['totalVisita1']);
12249
12250                                    $worksheet->setCellValue('S'.$l, $m['totalLlamada2']);
12251                                    $worksheet->setCellValue('T'.$l, $m['totalVisita2']);
12252                                    $l++;
12253
12254                                    if (isset($m['weeks']) && count(@$m['weeks']) > 0 && count(@$m['weeks']) != 1) {
12255                                        foreach ($m['weeks'] as $w) {
12256                                            $worksheet->setCellValue('A'.$l, $item['year']);
12257                                            $worksheet->setCellValue('B'.$l, $c['commercial']);
12258                                            $worksheet->setCellValue('C'.$l, $m['month']);
12259
12260                                            $worksheet->setCellValue('D'.$l, $w['created_at']);
12261                                            $worksheet->setCellValue('F'.$l, $w['totalIssue']);
12262                                            $worksheet->setCellValue('G'.$l, $w['revenueIssue']);
12263                                            $worksheet->setCellValue('H'.$l, $w['totalIssueObjective']);
12264
12265                                            $worksheet->setCellValue('I'.$l, $w['totalAcceptance']);
12266                                            $worksheet->setCellValue('J'.$l, $w['revenueAcceptance']);
12267                                            $worksheet->setCellValue('K'.$l, $w['totalAcceptanceObjective']);
12268
12269                                            $worksheet->setCellValue('L'.$l, $w['totalRejected']);
12270                                            $worksheet->setCellValue('M'.$l, $w['revenueRejected']);
12271
12272                                            $worksheet->setCellValue('N'.$l, $w['totalNew']);
12273                                            $worksheet->setCellValue('O'.$l, $w['revenueNew']);
12274                                            $worksheet->setCellValue('P'.$l, $w['totalNewObjective']);
12275
12276                                            $worksheet->setCellValue('Q'.$l, $w['totalLlamada1']);
12277                                            $worksheet->setCellValue('R'.$l, $w['totalVisita1']);
12278
12279                                            $worksheet->setCellValue('S'.$l, $w['totalLlamada2']);
12280                                            $worksheet->setCellValue('T'.$l, $w['totalVisita2']);
12281                                            $l++;
12282
12283                                            if (count($w['budget_types']) > 0) {
12284                                                foreach ($w['budget_types'] as $b) {
12285                                                    $worksheet->setCellValue('A'.$l, $item['year']);
12286                                                    $worksheet->setCellValue('B'.$l, $c['commercial']);
12287                                                    $worksheet->setCellValue('C'.$l, $m['month']);
12288
12289                                                    $worksheet->setCellValue('E'.$l, $b['name']);
12290                                                    $worksheet->setCellValue('F'.$l, $b['totalIssue']);
12291                                                    $worksheet->setCellValue('G'.$l, $b['revenueIssue']);
12292                                                    $worksheet->setCellValue('H'.$l, $b['totalIssueObjective']);
12293
12294                                                    $worksheet->setCellValue('I'.$l, $b['totalAcceptance']);
12295                                                    $worksheet->setCellValue('J'.$l, $b['revenueAcceptance']);
12296                                                    $worksheet->setCellValue('K'.$l, $b['totalAcceptanceObjective']);
12297
12298                                                    $worksheet->setCellValue('L'.$l, $b['totalRejected']);
12299                                                    $worksheet->setCellValue('M'.$l, $b['revenueRejected']);
12300
12301                                                    $worksheet->setCellValue('N'.$l, $b['totalNew']);
12302                                                    $worksheet->setCellValue('O'.$l, $b['revenueNew']);
12303                                                    $worksheet->setCellValue('P'.$l, $b['totalNewObjective']);
12304
12305                                                    $worksheet->setCellValue('Q'.$l, $b['totalLlamada1']);
12306                                                    $worksheet->setCellValue('R'.$l, $b['totalVisita1']);
12307
12308                                                    $worksheet->setCellValue('S'.$l, $b['totalLlamada2']);
12309                                                    $worksheet->setCellValue('T'.$l, $b['totalVisita2']);
12310                                                    $l++;
12311                                                }
12312                                            }
12313                                        }
12314                                    } elseif (isset($m['weeks']) && count(@$m['weeks']) == 1) {
12315                                        foreach ($m['weeks'] as $w) {
12316                                            if (count($w['budget_types']) > 0) {
12317                                                foreach ($w['budget_types'] as $b) {
12318                                                    $worksheet->setCellValue('A'.$l, $item['year']);
12319                                                    $worksheet->setCellValue('B'.$l, $c['commercial']);
12320                                                    $worksheet->setCellValue('C'.$l, $m['month']);
12321
12322                                                    $worksheet->setCellValue('E'.$l, $b['name']);
12323                                                    $worksheet->setCellValue('F'.$l, $b['totalIssue']);
12324                                                    $worksheet->setCellValue('G'.$l, $b['revenueIssue']);
12325                                                    $worksheet->setCellValue('H'.$l, $b['totalIssueObjective']);
12326
12327                                                    $worksheet->setCellValue('I'.$l, $b['totalAcceptance']);
12328                                                    $worksheet->setCellValue('J'.$l, $b['revenueAcceptance']);
12329                                                    $worksheet->setCellValue('K'.$l, $b['totalAcceptanceObjective']);
12330
12331                                                    $worksheet->setCellValue('L'.$l, $b['totalRejected']);
12332                                                    $worksheet->setCellValue('M'.$l, $b['revenueRejected']);
12333
12334                                                    $worksheet->setCellValue('N'.$l, $b['totalNew']);
12335                                                    $worksheet->setCellValue('O'.$l, $b['revenueNew']);
12336                                                    $worksheet->setCellValue('P'.$l, $b['totalNewObjective']);
12337
12338                                                    $worksheet->setCellValue('Q'.$l, $b['totalLlamada1']);
12339                                                    $worksheet->setCellValue('R'.$l, $b['totalVisita1']);
12340
12341                                                    $worksheet->setCellValue('S'.$l, $b['totalLlamada2']);
12342                                                    $worksheet->setCellValue('T'.$l, $b['totalVisita2']);
12343                                                    $l++;
12344                                                }
12345                                            }
12346                                        }
12347
12348                                    }
12349                                }
12350                            }
12351                        }
12352                    }
12353                } else {
12354
12355                    if (isset($item['months']) && count($item['months']) > 0) {
12356                        foreach ($item['months'] as $m) {
12357                            $worksheet->setCellValue('A'.$l, $item['year']);
12358                            $worksheet->setCellValue('B'.$l, $m['month']);
12359                            $worksheet->setCellValue('F'.$l, $m['totalIssue']);
12360                            $worksheet->setCellValue('G'.$l, $m['revenueIssue']);
12361                            $worksheet->setCellValue('H'.$l, '-');
12362
12363                            $worksheet->setCellValue('I'.$l, $m['totalAcceptance']);
12364                            $worksheet->setCellValue('J'.$l, $m['revenueAcceptance']);
12365                            $worksheet->setCellValue('K'.$l, '-');
12366
12367                            $worksheet->setCellValue('L'.$l, $m['totalRejected']);
12368                            $worksheet->setCellValue('M'.$l, $m['revenueRejected']);
12369
12370                            $worksheet->setCellValue('N'.$l, $m['totalNew']);
12371                            $worksheet->setCellValue('O'.$l, $m['revenueNew']);
12372                            $worksheet->setCellValue('P'.$l, '-');
12373
12374                            $worksheet->setCellValue('Q'.$l, $m['totalLlamada1']);
12375                            $worksheet->setCellValue('R'.$l, $m['totalVisita1']);
12376
12377                            $worksheet->setCellValue('S'.$l, $m['totalLlamada2']);
12378                            $worksheet->setCellValue('T'.$l, $m['totalVisita2']);
12379                            $l++;
12380
12381                            if (isset($m['weeks']) && count(@$m['weeks']) > 0) {
12382                                foreach ($m['weeks'] as $w) {
12383                                    $worksheet->setCellValue('A'.$l, $item['year']);
12384                                    $worksheet->setCellValue('B'.$l, $m['month']);
12385
12386                                    $worksheet->setCellValue('C'.$l, $w['created_at']);
12387                                    $worksheet->setCellValue('F'.$l, $w['totalIssue']);
12388                                    $worksheet->setCellValue('G'.$l, $w['revenueIssue']);
12389                                    $worksheet->setCellValue('H'.$l, '-');
12390
12391                                    $worksheet->setCellValue('I'.$l, $w['totalAcceptance']);
12392                                    $worksheet->setCellValue('J'.$l, $w['revenueAcceptance']);
12393                                    $worksheet->setCellValue('K'.$l, '-');
12394
12395                                    $worksheet->setCellValue('L'.$l, $w['totalRejected']);
12396                                    $worksheet->setCellValue('M'.$l, $w['revenueRejected']);
12397
12398                                    $worksheet->setCellValue('N'.$l, $w['totalNew']);
12399                                    $worksheet->setCellValue('O'.$l, $w['revenueNew']);
12400                                    $worksheet->setCellValue('P'.$l, '-');
12401
12402                                    $worksheet->setCellValue('Q'.$l, $w['totalLlamada1']);
12403                                    $worksheet->setCellValue('R'.$l, $w['totalVisita1']);
12404
12405                                    $worksheet->setCellValue('S'.$l, $w['totalLlamada2']);
12406                                    $worksheet->setCellValue('T'.$l, $w['totalVisita2']);
12407                                    $l++;
12408
12409                                    if (count($w['commercials']) > 0) {
12410                                        foreach ($w['commercials'] as $c) {
12411                                            $worksheet->setCellValue('A'.$l, $item['year']);
12412                                            $worksheet->setCellValue('B'.$l, $m['month']);
12413
12414                                            $worksheet->setCellValue('D'.$l, $c['commercial']);
12415                                            $worksheet->setCellValue('F'.$l, $c['totalIssue']);
12416                                            $worksheet->setCellValue('G'.$l, $c['revenueIssue']);
12417                                            $worksheet->setCellValue('H'.$l, $c['totalIssueObjective']);
12418
12419                                            $worksheet->setCellValue('I'.$l, $c['totalAcceptance']);
12420                                            $worksheet->setCellValue('J'.$l, $c['revenueAcceptance']);
12421                                            $worksheet->setCellValue('K'.$l, $c['totalAcceptanceObjective']);
12422
12423                                            $worksheet->setCellValue('L'.$l, $c['totalRejected']);
12424                                            $worksheet->setCellValue('M'.$l, $c['revenueRejected']);
12425
12426                                            $worksheet->setCellValue('N'.$l, $c['totalNew']);
12427                                            $worksheet->setCellValue('O'.$l, $c['revenueNew']);
12428                                            $worksheet->setCellValue('P'.$l, $c['totalNewObjective']);
12429
12430                                            $worksheet->setCellValue('Q'.$l, $c['totalLlamada1']);
12431                                            $worksheet->setCellValue('R'.$l, $c['totalVisita1']);
12432
12433                                            $worksheet->setCellValue('S'.$l, $c['totalLlamada2']);
12434                                            $worksheet->setCellValue('T'.$l, $c['totalVisita2']);
12435                                            $l++;
12436
12437                                            if (count($c['budget_types']) > 0) {
12438                                                foreach ($c['budget_types'] as $b) {
12439                                                    $worksheet->setCellValue('A'.$l, $item['year']);
12440                                                    $worksheet->setCellValue('B'.$l, $m['month']);
12441                                                    $worksheet->setCellValue('D'.$l, $c['commercial']);
12442
12443                                                    $worksheet->setCellValue('E'.$l, $b['name']);
12444                                                    $worksheet->setCellValue('F'.$l, $b['totalIssue']);
12445                                                    $worksheet->setCellValue('G'.$l, $b['revenueIssue']);
12446                                                    $worksheet->setCellValue('H'.$l, $b['totalIssueObjective']);
12447
12448                                                    $worksheet->setCellValue('I'.$l, $b['totalAcceptance']);
12449                                                    $worksheet->setCellValue('J'.$l, $b['revenueAcceptance']);
12450                                                    $worksheet->setCellValue('K'.$l, $b['totalAcceptanceObjective']);
12451
12452                                                    $worksheet->setCellValue('L'.$l, $b['totalRejected']);
12453                                                    $worksheet->setCellValue('M'.$l, $b['revenueRejected']);
12454
12455                                                    $worksheet->setCellValue('N'.$l, $b['totalNew']);
12456                                                    $worksheet->setCellValue('O'.$l, $b['revenueNew']);
12457                                                    $worksheet->setCellValue('P'.$l, $b['totalNewObjective']);
12458
12459                                                    $worksheet->setCellValue('Q'.$l, $b['totalLlamada1']);
12460                                                    $worksheet->setCellValue('R'.$l, $b['totalVisita1']);
12461
12462                                                    $worksheet->setCellValue('S'.$l, $b['totalLlamada2']);
12463                                                    $worksheet->setCellValue('T'.$l, $b['totalVisita2']);
12464                                                    $l++;
12465                                                }
12466                                            }
12467                                        }
12468                                    }
12469                                }
12470                            }
12471
12472                            if (count($m['commercials']) > 0) {
12473                                foreach ($m['commercials'] as $c) {
12474                                    $worksheet->setCellValue('A'.$l, $item['year']);
12475                                    $worksheet->setCellValue('B'.$l, $m['month']);
12476
12477                                    $worksheet->setCellValue('D'.$l, $c['commercial']);
12478                                    $worksheet->setCellValue('F'.$l, $c['totalIssue']);
12479                                    $worksheet->setCellValue('G'.$l, $c['revenueIssue']);
12480                                    $worksheet->setCellValue('H'.$l, $c['totalIssueObjectiveYearly']);
12481
12482                                    $worksheet->setCellValue('I'.$l, $c['totalAcceptance']);
12483                                    $worksheet->setCellValue('J'.$l, $c['revenueAcceptance']);
12484                                    $worksheet->setCellValue('K'.$l, $c['totalAcceptanceObjectiveYearly']);
12485
12486                                    $worksheet->setCellValue('L'.$l, $c['totalRejected']);
12487                                    $worksheet->setCellValue('M'.$l, $c['revenueRejected']);
12488
12489                                    $worksheet->setCellValue('N'.$l, $c['totalNew']);
12490                                    $worksheet->setCellValue('O'.$l, $c['revenueNew']);
12491                                    $worksheet->setCellValue('P'.$l, $c['totalNewObjectiveYearly']);
12492
12493                                    $worksheet->setCellValue('Q'.$l, $c['totalLlamada1']);
12494                                    $worksheet->setCellValue('R'.$l, $c['totalVisita1']);
12495
12496                                    $worksheet->setCellValue('S'.$l, $c['totalLlamada2']);
12497                                    $worksheet->setCellValue('T'.$l, $c['totalVisita2']);
12498                                    $l++;
12499
12500                                    if (count($c['budget_types']) > 0) {
12501                                        foreach ($c['budget_types'] as $b) {
12502                                            $worksheet->setCellValue('A'.$l, $item['year']);
12503                                            $worksheet->setCellValue('B'.$l, $m['month']);
12504                                            $worksheet->setCellValue('D'.$l, $c['commercial']);
12505
12506                                            $worksheet->setCellValue('E'.$l, $b['name']);
12507                                            $worksheet->setCellValue('F'.$l, $b['totalIssue']);
12508                                            $worksheet->setCellValue('G'.$l, $b['revenueIssue']);
12509                                            $worksheet->setCellValue('H'.$l, $b['totalIssueObjective']);
12510
12511                                            $worksheet->setCellValue('I'.$l, $b['totalAcceptance']);
12512                                            $worksheet->setCellValue('J'.$l, $b['revenueAcceptance']);
12513                                            $worksheet->setCellValue('K'.$l, $b['totalAcceptanceObjective']);
12514
12515                                            $worksheet->setCellValue('L'.$l, $b['totalRejected']);
12516                                            $worksheet->setCellValue('M'.$l, $b['revenueRejected']);
12517
12518                                            $worksheet->setCellValue('N'.$l, $b['totalNew']);
12519                                            $worksheet->setCellValue('O'.$l, $b['revenueNew']);
12520                                            $worksheet->setCellValue('P'.$l, $b['totalNewObjective']);
12521
12522                                            $worksheet->setCellValue('Q'.$l, $b['totalLlamada1']);
12523                                            $worksheet->setCellValue('R'.$l, $b['totalVisita1']);
12524
12525                                            $worksheet->setCellValue('S'.$l, $b['totalLlamada2']);
12526                                            $worksheet->setCellValue('T'.$l, $b['totalVisita2']);
12527                                            $l++;
12528                                        }
12529                                    }
12530                                }
12531                            }
12532                        }
12533                    }
12534
12535                    if (count($item['commercials']) > 0) {
12536                        foreach ($item['commercials'] as $c) {
12537                            $worksheet->setCellValue('A'.$l, $item['year']);
12538
12539                            $worksheet->setCellValue('D'.$l, $c['commercial']);
12540                            $worksheet->setCellValue('F'.$l, $c['totalIssue']);
12541                            $worksheet->setCellValue('G'.$l, $c['revenueIssue']);
12542                            $worksheet->setCellValue('H'.$l, $c['totalIssueObjectiveYearly']);
12543
12544                            $worksheet->setCellValue('I'.$l, $c['totalAcceptance']);
12545                            $worksheet->setCellValue('J'.$l, $c['revenueAcceptance']);
12546                            $worksheet->setCellValue('K'.$l, $c['totalAcceptanceObjectiveYearly']);
12547
12548                            $worksheet->setCellValue('L'.$l, $c['totalRejected']);
12549                            $worksheet->setCellValue('M'.$l, $c['revenueRejected']);
12550
12551                            $worksheet->setCellValue('N'.$l, $c['totalNew']);
12552                            $worksheet->setCellValue('O'.$l, $c['revenueNew']);
12553                            $worksheet->setCellValue('P'.$l, $c['totalNewObjectiveYearly']);
12554
12555                            $worksheet->setCellValue('Q'.$l, $c['totalLlamada1']);
12556                            $worksheet->setCellValue('R'.$l, $c['totalVisita1']);
12557
12558                            $worksheet->setCellValue('S'.$l, $c['totalLlamada2']);
12559                            $worksheet->setCellValue('T'.$l, $c['totalVisita2']);
12560                            $l++;
12561
12562                            if (count($c['budget_types']) > 0) {
12563                                foreach ($c['budget_types'] as $b) {
12564                                    $worksheet->setCellValue('A'.$l, $item['year']);
12565                                    $worksheet->setCellValue('D'.$l, $c['commercial']);
12566
12567                                    $worksheet->setCellValue('E'.$l, $b['name']);
12568                                    $worksheet->setCellValue('F'.$l, $b['totalIssue']);
12569                                    $worksheet->setCellValue('G'.$l, $b['revenueIssue']);
12570                                    $worksheet->setCellValue('H'.$l, $b['totalIssueObjective']);
12571
12572                                    $worksheet->setCellValue('I'.$l, $b['totalAcceptance']);
12573                                    $worksheet->setCellValue('J'.$l, $b['revenueAcceptance']);
12574                                    $worksheet->setCellValue('K'.$l, $b['totalAcceptanceObjective']);
12575
12576                                    $worksheet->setCellValue('L'.$l, $b['totalRejected']);
12577                                    $worksheet->setCellValue('M'.$l, $b['revenueRejected']);
12578
12579                                    $worksheet->setCellValue('N'.$l, $b['totalNew']);
12580                                    $worksheet->setCellValue('O'.$l, $b['revenueNew']);
12581                                    $worksheet->setCellValue('P'.$l, $b['totalNewObjective']);
12582
12583                                    $worksheet->setCellValue('Q'.$l, $b['totalLlamada1']);
12584                                    $worksheet->setCellValue('R'.$l, $b['totalVisita1']);
12585
12586                                    $worksheet->setCellValue('S'.$l, $b['totalLlamada2']);
12587                                    $worksheet->setCellValue('T'.$l, $b['totalVisita2']);
12588                                    $l++;
12589                                }
12590                            }
12591                        }
12592                    }
12593                }
12594
12595                $l++;
12596            }
12597
12598            if ($data['group_by'] == 1) {
12599                if ($data['aggregated_by'] == 3) {
12600                    $worksheet->removeColumn('C');
12601                    $worksheet->removeColumn('C');
12602                } elseif ($data['aggregated_by'] == 2) {
12603                    $worksheet->removeColumn('D');
12604                }
12605            } else {
12606                if ($data['aggregated_by'] == 3) {
12607                    $worksheet->removeColumn('B');
12608                    $worksheet->removeColumn('B');
12609                } elseif ($data['aggregated_by'] == 2) {
12610                    $worksheet->removeColumn('C');
12611                }
12612            }
12613
12614            $writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
12615            ob_start();
12616            $writer->save('php://output');
12617            $file = ob_get_contents();
12618            ob_end_clean();
12619
12620            return response($file);
12621
12622        } catch (\Exception $e) {
12623            report(AppException::fromException($e, 'DOWNLOAD_PRODUCTIVITY_COMMERCIAL_EXCEPTION'));
12624
12625            return response(['message' => 'KO', 'error' => $e->getMessage()]);
12626        }
12627
12628    }
12629
12630    public function update_commercial_numbers($companyId): void
12631    {
12632        $phpBinary = '/usr/bin/php';
12633
12634        $artisanPath = escapeshellarg(base_path('artisan'));
12635
12636        $command = sprintf(
12637            '%s %s update:commercial-numbers %s > /dev/null 2>&1 &',
12638            $phpBinary,
12639            $artisanPath,
12640            $companyId
12641        );
12642
12643        exec($command, $output, $returnVar);
12644    }
12645
12646    public function list_quotation_analytics_by_service_type(Request $request): ResponseFactory|HttpResponse
12647    {
12648
12649        try {
12650
12651            $data = $request->all();
12652            $companyId = addslashes((string) $data['company_id']);
12653            $where = '';
12654
12655            if ($companyId != 0) {
12656                $where .= " AND c.company_id = {$companyId} ";
12657            } else {
12658                $where .= " AND c.company_id IN ({$this->companyId}";
12659            }
12660
12661            $col = '1';
12662
12663            if (isset($data['data_to_display']) && $data['data_to_display'] != null) {
12664                if ($data['data_to_display'] == 1) {
12665                    $col = '1';
12666                }
12667
12668                if ($data['data_to_display'] == 2) {
12669                    $col = 'q.amount';
12670                }
12671            }
12672
12673            if (isset($data['approvals'])) {
12674                $approvals = addslashes($data['approvals']);
12675
12676                if ($approvals == 2) {
12677                    $where .= ' AND q.for_approval != 1 ';
12678                }
12679
12680                if ($approvals == 3) {
12681                    $where .= ' AND q.for_approval > 0 ';
12682                }
12683
12684                if ($approvals == 4) {
12685                    $where .= ' AND q.approved_by IS NOT NULL';
12686                }
12687
12688                if ($approvals == 5) {
12689                    $where .= ' AND q.for_approval = 2 AND q.approved_by IS NULL';
12690                }
12691
12692                if ($approvals == 6) {
12693                    $where .= ' AND q.approved_by IS NOT NULL AND q.approved_by_v2 IS NOT NULL';
12694                }
12695
12696                if ($approvals == 7) {
12697                    $where .= ' AND q.for_approval = 3 AND q.approved_by IS NOT NULL AND q.approved_by_v2 IS NULL';
12698                }
12699
12700                if ($approvals == 8) {
12701                    $where .= ' AND q.for_approval = 3 AND q.approved_by IS NULL AND q.approved_by_v2 IS NOT NULL';
12702                }
12703
12704                if ($approvals == 9) {
12705                    $where .= ' AND q.for_approval = 3 AND q.approved_by IS NULL AND q.approved_by_v2 IS NULL';
12706                }
12707            }
12708
12709            if (isset($data['role_id']) && $data['role_id'] != null) {
12710                $roleIds = implode(',', $data['role_id']);
12711                if (count($data['role_id']) > 0) {
12712                    $where .= " AND u.role_id IN ({$roleIds}, 999999999)";
12713                }
12714            }
12715
12716            if (isset($data['client_type']) && $data['client_type'] != null) {
12717                $where .= " AND q.customer_type_id = {$data['client_type']}";
12718            }
12719
12720            $weekDay = '- WEEKDAY(q.date)';
12721
12722            if (isset($data['start_of_the_week']) && $data['start_of_the_week'] != null) {
12723                $weekDay = match ($data['start_of_the_week']) {
12724                    0 => '- WEEKDAY(q.date)',
12725                    1 => '(1 - WEEKDAY(q.date))',
12726                    2 => '(2 - WEEKDAY(q.date))',
12727                    3 => '(3 - WEEKDAY(q.date))',
12728                    4 => '(4 - WEEKDAY(q.date))',
12729                    default => '- WEEKDAY(q.date)',
12730                };
12731            }
12732
12733            $whereDates = '';
12734
12735            if ((isset($data['start_date']) && $data['start_date'] != null) && (isset($data['end_date']) && $data['end_date'] != null)) {
12736                $whereDates = " AND q.date BETWEEN '{$data['start_date']}' AND '{$data['end_date']}";
12737            }
12738
12739            $query = "SELECT
12740                        q.region,
12741                        YEAR(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)) 'year',
12742                        LPAD(MONTH(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)), 2, 0) 'month',
12743                        LPAD(WEEK(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)), 2, 0) 'week',
12744                        DATE_FORMAT(DATE_ADD(q.date, INTERVAL {$weekDay} DAY), '%W, %M %e') namedate,
12745
12746                        GROUP_CONCAT(CASE WHEN q.date_type = 'issue' THEN q.id END) AS groupConcatIdsTotalEnviado,
12747                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END), 0) AS totalIssueEnviado,
12748
12749                        GROUP_CONCAT(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 3 THEN q.id END) AS groupConcatIdsMantenimientoEnviado,
12750                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 3 THEN {$col} END), 0) totalMantenimientoEnviado,
12751                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 3 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END) * 100, 0) totalMantenimientoPercentageEnviado,
12752
12753                        GROUP_CONCAT(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 5 THEN q.id END) AS groupConcatIdsCorrectivosEnviado,
12754                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 5 THEN {$col} END), 0) totalCorrectivosEnviado,
12755                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 5 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END) * 100, 0) totalCorrectivosPercentageEnviado,
12756
12757                        GROUP_CONCAT(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 THEN q.id END) AS groupConcatIdsObrasEnviado,
12758                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 THEN {$col} END), 0) totalObrasEnviado,
12759                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END) * 100, 0) totalObrasPercentageEnviado,
12760
12761                        GROUP_CONCAT(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id IN (6, 7, 8) THEN q.id END) AS groupConcatIdsOtrosEnviado,
12762                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END), 0) totalOtrosEnviado,
12763                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END) * 100, 0) totalOtrosPercentageEnviado,
12764
12765                        GROUP_CONCAT(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id IN (6, 7, 8) THEN q.id END) AS groupConcatIdsOtrosEnviado,
12766                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END), 0) totalOtrosEnviado,
12767                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END) * 100, 0) totalOtrosPercentageEnviado,
12768
12769                        CASE
12770                            WHEN SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.margin_for_the_company <> 0 THEN q.amount ELSE 0 END) = 0 THEN 0
12771                            ELSE SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.margin_for_the_company <> 0 THEN q.s1 ELSE 0 END)
12772                                / SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.margin_for_the_company <> 0 THEN q.amount ELSE 0 END)
12773                        END AS weightedAverageMarginForTheCompanyEnviado,
12774                        CASE
12775                            WHEN SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.invoice_margin <> 0 THEN q.amount ELSE 0 END) = 0 THEN 0
12776                            ELSE SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.invoice_margin <> 0 THEN q.s2 ELSE 0 END)
12777                                / SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.invoice_margin <> 0 THEN q.amount ELSE 0 END)
12778                        END AS weightedAverageInvoiceEnviado,
12779
12780                        GROUP_CONCAT(CASE WHEN q.date_type = 'acceptance' THEN q.id END) AS groupConcatIdsTotalAceptado,
12781                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' THEN {$col} END), 0) AS totalIssueAceptado,
12782
12783                        GROUP_CONCAT(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 3 THEN q.id END) AS groupConcatIdsMantenimientoAceptado,
12784                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 3 THEN {$col} END), 0) totalMantenimientoAceptado,
12785                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 3 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'acceptance' THEN {$col} END) * 100, 0) totalMantenimientoPercentageAceptado,
12786
12787                        GROUP_CONCAT(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 5 THEN q.id END) AS groupConcatIdsCorrectivosAceptado,
12788                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 5 THEN {$col} END), 0) totalCorrectivosAceptado,
12789                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 5 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'acceptance' THEN {$col} END) * 100, 0) totalCorrectivosPercentageAceptado,
12790
12791                        GROUP_CONCAT(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 THEN q.id END) AS groupConcatIdsObrasAceptado,
12792                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 THEN {$col} END), 0) totalObrasAceptado,
12793                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'acceptance' THEN {$col} END) * 100, 0) totalObrasPercentageAceptado,
12794
12795                        GROUP_CONCAT(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id IN (6, 7, 8) THEN q.id END) AS groupConcatIdsOtrosAceptado,
12796                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END), 0) totalOtrosAceptado,
12797                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) / SUM(CASE WHEN q.date_type = 'acceptance' THEN {$col} END) * 100, 0) totalOtrosPercentageAceptado,
12798
12799                        CASE
12800                            WHEN SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.margin_for_the_company <> 0 THEN q.amount ELSE 0 END) = 0 THEN 0
12801                            ELSE SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.margin_for_the_company <> 0 THEN q.s1 ELSE 0 END)
12802                                / SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.margin_for_the_company <> 0 THEN q.amount ELSE 0 END)
12803                        END AS weightedAverageMarginForTheCompanyAceptado,
12804                        CASE
12805                            WHEN SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.invoice_margin <> 0 THEN q.amount ELSE 0 END) = 0 THEN 0
12806                            ELSE SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.invoice_margin <> 0 THEN q.s2 ELSE 0 END)
12807                                / SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.invoice_margin <> 0 THEN q.amount ELSE 0 END)
12808                        END AS weightedAverageInvoiceAceptado,
12809
12810                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' THEN 1 END) / SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END) * 100, 0) totalIssuePercentage,
12811                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 3 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 3 THEN {$col} END) * 100, 0) totalMantenimientoPercentage,
12812                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 5 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 5 THEN {$col} END) * 100, 0) totalCorrectivosPercentage,
12813                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 THEN {$col} END) * 100, 0) totalObrasPercentage,
12814                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) * 100 , 0) totalOtrosPercentage,
12815
12816                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.issue_date IS NOT NULL THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.acceptance_date IS NOT NULL THEN {$col} END) * 100, 0) totalIssuePercentageLead,
12817                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.issue_date IS NOT NULL AND q.budget_type_group_id = 3 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.acceptance_date IS NOT NULL AND q.budget_type_group_id = 3 THEN {$col} END) * 100, 0) totalMantenimientoPercentageLead,
12818                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.issue_date IS NOT NULL AND q.budget_type_group_id = 5 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.acceptance_date IS NOT NULL AND q.budget_type_group_id = 5 THEN {$col} END) * 100, 0) totalCorrectivosPercentageLead,
12819                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.issue_date IS NOT NULL AND q.budget_type_group_id = 4 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.acceptance_date IS NOT NULL AND q.budget_type_group_id = 4 THEN {$col} END) * 100, 0) totalObrasPercentageLead,
12820                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.issue_date IS NOT NULL AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.acceptance_date IS NOT NULL AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) * 100 , 0) totalOtrosPercentageLead
12821                    FROM
12822                    (
12823                        SELECT
12824                            c.region,
12825                            q.issue_date AS DATE,
12826                            'issue' AS date_type,
12827                            q.acceptance_date,
12828                            q.issue_date,
12829                            q.id,
12830                            q.margin_for_the_company,
12831                            CASE
12832                                WHEN bt.budget_type_group_id = 4
12833                                AND q.budget_margin_enabled > 0
12834                                AND q.budget_margin_enabled IS NOT NULL
12835                                AND q.margin_for_the_company <> 0
12836                            THEN q.margin_for_the_company * q.amount
12837                            END s1,
12838                            q.invoice_margin,
12839                            CASE
12840                                WHEN bt.budget_type_group_id = 4
12841                                AND q.budget_margin_enabled > 0
12842                                AND q.budget_margin_enabled IS NOT NULL
12843                                AND q.invoice_margin <> 0
12844                            THEN q.invoice_margin * q.amount
12845                            END s2,
12846                            q.budget_type_id,
12847                            q.budget_status_id,
12848                            q.amount,
12849                            bt.budget_type_group_id,
12850                            q.budget_margin_enabled
12851                        FROM
12852                            tbl_quotations q
12853                        JOIN tbl_companies c
12854                            ON c.company_id = q.company_id
12855                        LEFT JOIN tbl_budget_types bt
12856                            ON q.budget_type_id = bt.budget_type_id
12857                        LEFT JOIN tbl_users u
12858                            ON u.name = q.created_by
12859                        LEFT JOIN tbl_roles r
12860                            ON r.role_id = u.role_id
12861                        WHERE
12862                            q.issue_date IS NOT NULL
12863                            AND q.for_add = 0
12864                            AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
12865                            AND (q.commercial IS NOT NULL AND q.commercial != '')
12866                            AND q.budget_type_id != 7
12867                            AND q.budget_type_id IS NOT NULL
12868                            {$where}
12869
12870                        UNION ALL
12871
12872                        SELECT
12873                            c.region,
12874                            q.acceptance_date AS DATE,
12875                            'acceptance' AS date_type,
12876                            q.acceptance_date,
12877                            q.issue_date,
12878                            q.id,
12879                            q.margin_for_the_company,
12880                            CASE
12881                                WHEN bt.budget_type_group_id = 4
12882                                AND q.budget_margin_enabled > 0
12883                                AND q.budget_margin_enabled IS NOT NULL
12884                                AND q.margin_for_the_company > 0
12885                            THEN q.margin_for_the_company * q.amount
12886                            END s1,
12887                            q.invoice_margin,
12888                            CASE
12889                                WHEN bt.budget_type_group_id = 4
12890                                AND q.budget_margin_enabled <> 0
12891                                AND q.budget_margin_enabled IS NOT NULL
12892                                AND q.invoice_margin <> 0
12893                            THEN q.invoice_margin * q.amount
12894                            END s2,
12895                            q.budget_type_id,
12896                            q.budget_status_id,
12897                            q.amount,
12898                            bt.budget_type_group_id,
12899                            q.budget_margin_enabled
12900                        FROM
12901                            tbl_quotations q
12902                        JOIN tbl_companies c
12903                            ON c.company_id = q.company_id
12904                        LEFT JOIN tbl_budget_types bt
12905                            ON q.budget_type_id = bt.budget_type_id
12906                        LEFT JOIN tbl_users u
12907                            ON u.name = q.created_by
12908                        LEFT JOIN tbl_roles r
12909                            ON r.role_id = u.role_id
12910                        WHERE
12911                            q.acceptance_date IS NOT NULL
12912                            AND q.for_add = 0
12913                            AND q.budget_type_id IS NOT NULL
12914                            AND q.budget_type_id != 7
12915                            AND q.acceptance_date != '0000-00-00 00:00:00'
12916                            AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
12917                            {$where}
12918                    ) AS q
12919                    WHERE q.date != '0000-00-00 00:00:00' {$whereDates}
12920                    GROUP BY
12921                        q.region,
12922                        YEAR(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)),
12923                        MONTH(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)),
12924                        WEEK(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)) WITH ROLLUP
12925                    ORDER BY
12926                        q.region IS NULL,
12927                        q.region,
12928                        CASE WHEN q.region IS NOT NULL
12929                            AND YEAR(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)) IS NULL
12930                            AND MONTH(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)) IS NULL
12931                            AND WEEK(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)) IS NULL
12932                        THEN 0
12933                        ELSE 1 END,
12934                    YEAR DESC,
12935                    MONTH ASC,
12936                    WEEK ASC";
12937
12938            // FIRE-1145: domain-tagged cache (was Cache::get/put + Cache::flush()).
12939            $result = ResultCache::remember('quotations', $query, 600, fn () => DB::select($query));
12940
12941            $query = "SELECT
12942                        q.region,
12943                        YEAR(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)) 'year',
12944                        LPAD(MONTH(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)), 2, 0) 'month',
12945                        LPAD(WEEK(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)), 2, 0) 'week',
12946                        DATE_FORMAT(DATE_ADD(q.date, INTERVAL {$weekDay} DAY), '%W, %M %e') namedate,
12947
12948                        GROUP_CONCAT(CASE WHEN q.date_type = 'issue' THEN q.id END) AS groupConcatIdsTotalEnviado,
12949                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END), 0) AS totalIssueEnviado,
12950
12951                        GROUP_CONCAT(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 3 THEN q.id END) AS groupConcatIdsMantenimientoEnviado,
12952                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 3 THEN {$col} END), 0) totalMantenimientoEnviado,
12953                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 3 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END) * 100, 0) totalMantenimientoPercentageEnviado,
12954
12955                        GROUP_CONCAT(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 5 THEN q.id END) AS groupConcatIdsCorrectivosEnviado,
12956                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 5 THEN {$col} END), 0) totalCorrectivosEnviado,
12957                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 5 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END) * 100, 0) totalCorrectivosPercentageEnviado,
12958
12959                        GROUP_CONCAT(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 THEN q.id END) AS groupConcatIdsObrasEnviado,
12960                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 THEN {$col} END), 0) totalObrasEnviado,
12961                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END) * 100, 0) totalObrasPercentageEnviado,
12962
12963                        GROUP_CONCAT(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id IN (6, 7, 8) THEN q.id END) AS groupConcatIdsOtrosEnviado,
12964                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END), 0) totalOtrosEnviado,
12965                        COALESCE(SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END) * 100, 0) totalOtrosPercentageEnviado,
12966
12967                        CASE
12968                            WHEN SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.margin_for_the_company <> 0 THEN q.amount ELSE 0 END) = 0 THEN 0
12969                            ELSE SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.margin_for_the_company <> 0 THEN q.s1 ELSE 0 END)
12970                                / SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.margin_for_the_company <> 0 THEN q.amount ELSE 0 END)
12971                        END AS weightedAverageMarginForTheCompanyEnviado,
12972                        CASE
12973                            WHEN SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.invoice_margin <> 0 THEN q.amount ELSE 0 END) = 0 THEN 0
12974                            ELSE SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.invoice_margin <> 0 THEN q.s2 ELSE 0 END)
12975                                / SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.invoice_margin <> 0 THEN q.amount ELSE 0 END)
12976                        END AS weightedAverageInvoiceEnviado,
12977
12978                        GROUP_CONCAT(CASE WHEN q.date_type = 'acceptance' THEN q.id END) AS groupConcatIdsTotalAceptado,
12979                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' THEN {$col} END), 0) AS totalIssueAceptado,
12980
12981                        GROUP_CONCAT(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 3 THEN q.id END) AS groupConcatIdsMantenimientoAceptado,
12982                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 3 THEN {$col} END), 0) totalMantenimientoAceptado,
12983                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 3 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'acceptance' THEN {$col} END) * 100, 0) totalMantenimientoPercentageAceptado,
12984
12985                        GROUP_CONCAT(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 5 THEN q.id END) AS groupConcatIdsCorrectivosAceptado,
12986                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 5 THEN {$col} END), 0) totalCorrectivosAceptado,
12987                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 5 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'acceptance' THEN {$col} END) * 100, 0) totalCorrectivosPercentageAceptado,
12988
12989                        GROUP_CONCAT(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 THEN q.id END) AS groupConcatIdsObrasAceptado,
12990                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 THEN {$col} END), 0) totalObrasAceptado,
12991                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'acceptance' THEN {$col} END) * 100, 0) totalObrasPercentageAceptado,
12992
12993                        GROUP_CONCAT(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id IN (6, 7, 8) THEN q.id END) AS groupConcatIdsOtrosAceptado,
12994                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END), 0) totalOtrosAceptado,
12995                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) / SUM(CASE WHEN q.date_type = 'acceptance' THEN {$col} END) * 100, 0) totalOtrosPercentageAceptado,
12996
12997                        CASE
12998                            WHEN SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.margin_for_the_company <> 0 THEN q.amount ELSE 0 END) = 0 THEN 0
12999                            ELSE SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.margin_for_the_company <> 0 THEN q.s1 ELSE 0 END)
13000                                / SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.margin_for_the_company <> 0 THEN q.amount ELSE 0 END)
13001                        END AS weightedAverageMarginForTheCompanyAceptado,
13002                        CASE
13003                            WHEN SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.invoice_margin <> 0 THEN q.amount ELSE 0 END) = 0 THEN 0
13004                            ELSE SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.invoice_margin <> 0 THEN q.s2 ELSE 0 END)
13005                                / SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 AND q.budget_margin_enabled > 0 AND q.invoice_margin <> 0 THEN q.amount ELSE 0 END)
13006                        END AS weightedAverageInvoiceAceptado,
13007
13008                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' THEN {$col} END) * 100, 0) totalIssuePercentage,
13009                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 3 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 3 THEN {$col} END) * 100, 0) totalMantenimientoPercentage,
13010                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 5 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 5 THEN {$col} END) * 100, 0) totalCorrectivosPercentage,
13011                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id = 4 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id = 4 THEN {$col} END) * 100, 0) totalObrasPercentage,
13012                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) * 100 , 0) totalOtrosPercentage,
13013
13014                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.issue_date IS NOT NULL THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.acceptance_date IS NOT NULL THEN {$col} END) * 100, 0) totalIssuePercentageLead,
13015                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.issue_date IS NOT NULL AND q.budget_type_group_id = 3 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.acceptance_date IS NOT NULL AND q.budget_type_group_id = 3 THEN {$col} END) * 100, 0) totalMantenimientoPercentageLead,
13016                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.issue_date IS NOT NULL AND q.budget_type_group_id = 5 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.acceptance_date IS NOT NULL AND q.budget_type_group_id = 5 THEN {$col} END) * 100, 0) totalCorrectivosPercentageLead,
13017                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.issue_date IS NOT NULL AND q.budget_type_group_id = 4 THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.acceptance_date IS NOT NULL AND q.budget_type_group_id = 4 THEN {$col} END) * 100, 0) totalObrasPercentageLead,
13018                        COALESCE(SUM(CASE WHEN q.date_type = 'acceptance' AND q.issue_date IS NOT NULL AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) / SUM(CASE WHEN q.date_type = 'issue' AND q.acceptance_date IS NOT NULL AND q.budget_type_group_id IN (6, 7, 8) THEN {$col} END) * 100 , 0) totalOtrosPercentageLead
13019                    FROM
13020                    (
13021                        SELECT
13022                            'Total Grupo FIRE' region,
13023                            q.issue_date AS DATE,
13024                            'issue' AS date_type,
13025                            q.acceptance_date,
13026                            q.issue_date,
13027                            q.id,
13028                            q.margin_for_the_company,
13029                            CASE
13030                                WHEN bt.budget_type_group_id = 4
13031                                AND q.budget_margin_enabled > 0
13032                                AND q.budget_margin_enabled IS NOT NULL
13033                                AND q.margin_for_the_company <> 0
13034                            THEN q.margin_for_the_company * q.amount
13035                            END s1,
13036                            q.invoice_margin,
13037                            CASE
13038                                WHEN bt.budget_type_group_id = 4
13039                                AND q.budget_margin_enabled > 0
13040                                AND q.budget_margin_enabled IS NOT NULL
13041                                AND q.invoice_margin <> 0
13042                            THEN q.invoice_margin * q.amount
13043                            END s2,
13044                            q.budget_type_id,
13045                            q.budget_status_id,
13046                            q.amount,
13047                            bt.budget_type_group_id,
13048                            q.budget_margin_enabled
13049                        FROM
13050                            tbl_quotations q
13051                        JOIN tbl_companies c
13052                            ON c.company_id = q.company_id
13053                        LEFT JOIN tbl_budget_types bt
13054                            ON q.budget_type_id = bt.budget_type_id
13055                        LEFT JOIN tbl_users u
13056                            ON u.name = q.created_by
13057                        LEFT JOIN tbl_roles r
13058                            ON r.role_id = u.role_id
13059                        WHERE
13060                            q.issue_date IS NOT NULL
13061                            AND q.for_add = 0
13062                            AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
13063                            AND (q.commercial IS NOT NULL AND q.commercial != '')
13064                            AND q.budget_type_id != 7
13065                            AND q.budget_type_id IS NOT NULL
13066                            {$where}
13067
13068                        UNION ALL
13069
13070                        SELECT
13071                            'Total Grupo FIRE' region,
13072                            q.acceptance_date AS DATE,
13073                            'acceptance' AS date_type,
13074                            q.acceptance_date,
13075                            q.issue_date,
13076                            q.id,
13077                            q.margin_for_the_company,
13078                            CASE
13079                                WHEN bt.budget_type_group_id = 4
13080                                AND q.budget_margin_enabled > 0
13081                                AND q.budget_margin_enabled IS NOT NULL
13082                                AND q.margin_for_the_company <> 0
13083                            THEN q.margin_for_the_company * q.amount
13084                            END s1,
13085                            q.invoice_margin,
13086                            CASE
13087                                WHEN bt.budget_type_group_id = 4
13088                                AND q.budget_margin_enabled > 0
13089                                AND q.budget_margin_enabled IS NOT NULL
13090                                AND q.invoice_margin <> 0
13091                            THEN q.invoice_margin * q.amount
13092                            END s2,
13093                            q.budget_type_id,
13094                            q.budget_status_id,
13095                            q.amount,
13096                            bt.budget_type_group_id,
13097                            q.budget_margin_enabled
13098                        FROM
13099                            tbl_quotations q
13100                        JOIN tbl_companies c
13101                            ON c.company_id = q.company_id
13102                        LEFT JOIN tbl_budget_types bt
13103                            ON q.budget_type_id = bt.budget_type_id
13104                        LEFT JOIN tbl_users u
13105                            ON u.name = q.created_by
13106                        LEFT JOIN tbl_roles r
13107                            ON r.role_id = u.role_id
13108                        WHERE
13109                            q.acceptance_date IS NOT NULL
13110
13111                            AND q.for_add = 0
13112                            AND q.amount REGEXP '^[0-9]+\\.?[0-9]*$' = 1
13113                            AND (q.commercial IS NOT NULL AND q.commercial != '')
13114                            AND q.budget_type_id != 7
13115                            AND q.budget_type_id IS NOT NULL
13116                            {$where}
13117                    ) AS q
13118                    WHERE q.date != '0000-00-00 00:00:00' {$whereDates}
13119                    GROUP BY
13120                        q.region,
13121                        YEAR(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)),
13122                        MONTH(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)),
13123                        WEEK(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)) WITH ROLLUP
13124                    ORDER BY
13125                        q.region IS NULL,
13126                        q.region,
13127                        CASE WHEN q.region IS NOT NULL
13128                            AND YEAR(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)) IS NULL
13129                            AND MONTH(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)) IS NULL
13130                            AND WEEK(DATE_ADD(q.date, INTERVAL {$weekDay} DAY)) IS NULL
13131                        THEN 0
13132                        ELSE 1 END,
13133                    YEAR DESC,
13134                    MONTH ASC,
13135                    WEEK ASC";
13136
13137            // FIRE-1145: domain-tagged cache (was Cache::get/put + Cache::flush()).
13138            $totalGroup = ResultCache::remember('quotations', $query, 600, fn () => DB::select($query));
13139
13140            array_pop($result);
13141            $merged = array_merge($result, $totalGroup);
13142
13143            return response([
13144                'message' => 'OK',
13145                'data' => $merged,
13146            ]);
13147
13148        } catch (\Exception $e) {
13149            report(AppException::fromException($e, 'LIST_QUOTATIONS_ANALYTICS_BY_SERVICE_TYPE_EXCEPTION'));
13150
13151            return response(['message' => 'KO', 'error' => $e->getMessage()]);
13152        }
13153
13154    }
13155
13156    public function getIdsFromInternalQuoteIds($ids)
13157    {
13158        $idsArray = array_filter(explode(',', (string) $ids), is_numeric(...));
13159
13160        return TblQuotations::whereIn('internal_quote_id', $idsArray)
13161            ->pluck('id')
13162            ->toArray();
13163    }
13164
13165    public function checkQuotationExistByInternalQuoteId(Request $request): ResponseFactory|HttpResponse
13166    {
13167        try {
13168            $idsString = $request->all()['ids'];
13169            $ids = explode(',', (string) $idsString);
13170            $region = urldecode((string) request()->header('Region'));
13171            $company = TblCompanies::where('region', $region)->first();
13172
13173            if (! $company) {
13174                throw new \Exception('Region no encontrada');
13175            }
13176
13177            $companyId = $company->company_id;
13178
13179            $idsChecked = [];
13180
13181            foreach ($ids as $id) {
13182                $quote = TblQuotations::where('internal_quote_id', $id)->where('company_id', $companyId)->first();
13183                if (
13184                    ($companyId === 18 || $companyId === 22)
13185                    && ! $quote
13186                ) {
13187                    $quote = TblQuotations::where('internal_quote_id', $id)->whereIn('company_id', [18, 22])->first();
13188                }
13189                $idsChecked[$id] = $quote ? $quote->id : null;
13190            }
13191
13192            return response([
13193                'message' => 'OK',
13194                'data' => $idsChecked,
13195            ]);
13196
13197        } catch (\Exception $e) {
13198            report(AppException::fromException($e, 'CHECK_QUOTATION_EXIST_BY_INTERNAL_QUOTE_ID_EXCEPTION'));
13199
13200            return response(['message' => 'KO', 'error' => $e->getMessage()]);
13201        }
13202
13203    }
13204
13205    public function addUpdateLog($id, $userId, $field, $oldData, $newData, int $category = 4): void
13206    {
13207        $categoryOptions = [
13208            'Crear solicitud',
13209            'Modificacion de solicitud',
13210            'Creacion presupuesto',
13211            'Solicitud a presupuesto',
13212            'Modificacion de presupuesto',
13213            'Accion del presupuesto',
13214            'Eliminacion del presupuesto',
13215            'Convertir presupuesto a trabajo',
13216        ];
13217
13218        if ($field === 'amount') {
13219            $oldData = (float) $oldData;
13220            $newData = (float) $newData;
13221        }
13222
13223        if (
13224            ($oldData === $newData && ! is_null($oldData)) ||
13225            $field === 'updated_at' ||
13226            $field === 'created_at' ||
13227            $field === 'last_follow_up_comment' ||
13228            $field === 'reason_for_rejection_id' ||
13229            $field === 'for_add' ||
13230            $field === 'has_attachment' ||
13231            $field === 'question_ids' ||
13232            $field === 'question_ids_no' ||
13233            $field === 'x_message_id' ||
13234            $field === 'from_company_id' ||
13235            $field === 'type_by_g3w' ||
13236            $field === 'user_create_by_g3w' ||
13237            $field === 'user_commercial_by_g3w' ||
13238            $field === 'user_technical_by_g3w' ||
13239            $field === 'user_responsible_by_g3w' ||
13240            $field === 'resource_id' ||
13241            $field === 'y_message_id' ||
13242            $field === 'y_status' ||
13243            $field === 'sync_import_edited' ||
13244            $field === 'updated_by' ||
13245            $field === 'likehood' ||
13246            $field === 'g3w_warning' ||
13247            $field === 'gross_margin' ||
13248            $field === 'duration' ||
13249            ($field === 'budget_type_id' && is_null($oldData) && is_null($newData)) ||
13250            ($field === 'margin_on_invoice_per_day_per_worker' && $newData === 0)
13251        ) {
13252            return;
13253        }
13254
13255        $oldRegister = null;
13256        $newRegister = null;
13257
13258        if (! is_null($oldData) || ! is_null($newData)) {
13259            switch ($field) {
13260                case 'company_id':
13261                    $oldRegister = TblCompanies::where('company_id', $oldData)->first();
13262                    $newRegister = TblCompanies::where('company_id', $newData)->first();
13263                    break;
13264                case 'customer_type_id':
13265                    $oldRegister = TblCustomerTypes::where('customer_type_id', $oldData)->first();
13266                    $newRegister = TblCustomerTypes::where('customer_type_id', $newData)->first();
13267                    break;
13268                case 'segment_id':
13269                    $oldRegister = TblSegments::where('segment_id', $oldData)->first();
13270                    $newRegister = TblSegments::where('segment_id', $newData)->first();
13271                    break;
13272                case 'budget_type_id':
13273                    $oldRegister = TblBudgetTypes::where('budget_type_id', $oldData)->first();
13274                    $newRegister = TblBudgetTypes::where('budget_type_id', $newData)->first();
13275                    break;
13276                case 'budget_status_id':
13277                    $oldRegister = TblBudgetStatus::where('budget_status_id', $oldData)->first();
13278                    $newRegister = TblBudgetStatus::where('budget_status_id', $newData)->first();
13279                    break;
13280                case 'source_id':
13281                    $oldRegister = TblSources::where('source_id', $oldData)->first();
13282                    $newRegister = TblSources::where('source_id', $newData)->first();
13283                    break;
13284            }
13285        }
13286
13287        $finalOld = $oldRegister ? $oldRegister->name : ($oldData ?? 'N/A');
13288        $finalNew = $newRegister ? $newRegister->name : ($newData ?? 'N/A');
13289
13290        if (is_numeric($userId)) {
13291            $userObj = TblUsers::where('id', $userId)->first();
13292            $userName = $userObj ? $userObj->name : "Usuario desconocido ($userId)";
13293        } else {
13294            $userName = $userId;
13295        }
13296
13297        TblQuotationsLog::create([
13298            'category' => $categoryOptions[$category] ?? 'Otros',
13299            'quotation_id' => $id,
13300            'user' => $userName,
13301            'field' => $field,
13302            'old_value' => $finalOld,
13303            'new_value' => $finalNew,
13304        ]);
13305    }
13306
13307    public function setSolicitudDuplicity(Request $request): ResponseFactory|HttpResponse
13308    {
13309        try {
13310            $type = $request->all()['type'];
13311            $quoteId = $request->all()['quoteId'];
13312            $companyId = $request->all()['companyId'];
13313
13314            $quote = TblQuotations::where('quote_id', $quoteId)->where('company_id', $companyId)->first();
13315
13316            if (! $quote) {
13317                throw new \Exception('Quote no encontrada');
13318            }
13319
13320            $newIdSolicitudDuplicityValue = null;
13321
13322            if ($type === 'reject') {
13323                $quote->budget_status_id = 20;
13324                $newIdSolicitudDuplicityValue = 'R'.$quote->id_solicitud_duplicity;
13325            }
13326
13327            $this->addUpdateLog($quote->id, 'IA', 'id_solicitud_duplicity', $quote->id_solicitud_duplicity, $newIdSolicitudDuplicityValue);
13328
13329            $quote->id_solicitud_duplicity = $newIdSolicitudDuplicityValue;
13330
13331            $quote->save();
13332
13333            return response([
13334                'message' => 'OK',
13335                'data' => $quote,
13336            ]);
13337
13338        } catch (\Exception $e) {
13339            report(AppException::fromException($e, 'SET_SOLICITUD_DUPLICITY_EXCEPTION'));
13340
13341            return response(['message' => 'KO', 'error' => $e->getMessage()]);
13342        }
13343    }
13344
13345    public function getQuoteIdOfDuplicityById($id): ResponseFactory|HttpResponse
13346    {
13347        try {
13348            $quote = TblQuotations::where('id', $id)->first();
13349
13350            if (! $quote) {
13351                throw new \Exception('Quote no encontrada');
13352            }
13353
13354            return response([
13355                'message' => 'OK',
13356                'data' => $quote,
13357            ]);
13358
13359        } catch (\Exception $e) {
13360            report(AppException::fromException($e, 'GET_QUOTE_ID_OF_DUPLICITY_BY_QUOTE_ID_EXCEPTION'));
13361
13362            return response(['message' => 'KO', 'error' => $e->getMessage()]);
13363        }
13364    }
13365
13366    public function download_s3_files(Request $request)
13367    {
13368        ini_set('max_execution_time', 123456);
13369
13370        try {
13371
13372            $data = $request->all();
13373            $zipName = 'files_'.time().'.zip';
13374
13375            $r = new Request([
13376                'filterModel' => $data['filterModel'],
13377                'sortModel' => $data['sortModel'],
13378                'start' => 0,
13379                'end' => 999999999,
13380                'company_id' => $data['company_id'],
13381                'user_id' => $data['user_id'],
13382                'ids' => $data['ids'],
13383                'searchText' => $data['searchText'],
13384                'ids_not_in' => $data['ids_not_in'],
13385            ]);
13386
13387            $result = $this->list_orders_table($r);
13388            $result = $result->original['data'];
13389            $orderIds = array_column($result, 'id');
13390
13391            $files = TblFiles::whereIn('quotation_id', $orderIds)->where('is_internal', null)->get();
13392            if ($files->isEmpty()) {
13393                return response(['message' => 'KO', 'error' => 'No files found']);
13394            }
13395
13396            $tempDir = storage_path('app/temp');
13397
13398            if (! file_exists($tempDir)) {
13399                mkdir($tempDir, 0755, true);
13400            }
13401
13402            $zipPath = $tempDir.'/'.$zipName;
13403
13404            $zip = new ZipArchive;
13405            if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
13406                return response(['message' => 'KO', 'error' => 'Could not create ZIP file']);
13407            }
13408
13409            foreach ($files as $file) {
13410                $filePath = 'uploads/'.$file->filename;
13411
13412                if (Storage::disk('s3')->exists($filePath)) {
13413                    $contents = Storage::disk('s3')->get($filePath);
13414                    $zip->addFromString(basename($filePath), $contents);
13415                }
13416            }
13417
13418            $zip->close();
13419
13420            if (! file_exists($zipPath)) {
13421                return response(['message' => 'KO', 'error' => 'ZIP file creation failed'], 500);
13422            }
13423
13424            $content = file_get_contents($zipPath);
13425
13426            unlink($zipPath);
13427
13428            return response($content, 200, [
13429                'Content-Type' => 'application/zip',
13430                'Content-Disposition' => 'attachment; filename="'.$zipName.'"',
13431            ]);
13432        } catch (\Exception $e) {
13433            /** @disregard P1014 */
13434            $e->exceptionCode = 'DOWNLOAD_S3_FILES_EXCEPTION';
13435            report($e);
13436
13437            return response(['message' => 'KO', 'error' => $e->getMessage()]);
13438        }
13439    }
13440
13441    /**
13442     * FIRE-976: Return budget status and amount by Gestiona internal_quote_id + Region.
13443     * If the quotation doesn't exist locally, syncs it from Gestiona first.
13444     */
13445    public function getQuotationStatusByInternalId(Request $request, $id): ResponseFactory|HttpResponse
13446    {
13447        try {
13448            $region = urldecode((string) $request->header('Region'));
13449            if ($region === 'Catalunya') {
13450                $region = 'Cataluña';
13451            }
13452
13453            $company = TblCompanies::where('region', $region)->first();
13454            if (! $company) {
13455                return response(['message' => 'KO', 'error' => 'Region not found'], 404);
13456            }
13457
13458            $companyId = $company->company_id;
13459
13460            $quotation = $this->findQuotationByInternalId($id, $companyId);
13461
13462            // Found locally — return Titan data
13463            if ($quotation) {
13464                $statusName = null;
13465                if ($quotation->budget_status_id) {
13466                    $status = TblBudgetStatus::where('budget_status_id', $quotation->budget_status_id)->first();
13467                    $statusName = $status?->name;
13468                }
13469
13470                return response([
13471                    'message' => 'OK',
13472                    'data' => [
13473                        'internal_quote_id' => $quotation->internal_quote_id,
13474                        'amount' => $quotation->amount,
13475                        'budget_status_id' => $quotation->budget_status_id,
13476                        'budget_status' => $statusName,
13477                    ],
13478                ]);
13479            }
13480
13481            // Not found locally — try syncing from Gestiona
13482            $presupuestosService = app(PresupuestosService::class);
13483            $syncResult = $presupuestosService->syncById($id, $region);
13484
13485            if (! empty($syncResult['success'])) {
13486                // Sync succeeded — return freshly created Titan data
13487                $quotation = $this->findQuotationByInternalId($id, $companyId);
13488                if ($quotation) {
13489                    $statusName = null;
13490                    if ($quotation->budget_status_id) {
13491                        $status = TblBudgetStatus::where('budget_status_id', $quotation->budget_status_id)->first();
13492                        $statusName = $status?->name;
13493                    }
13494
13495                    return response([
13496                        'message' => 'OK',
13497                        'data' => [
13498                            'internal_quote_id' => $quotation->internal_quote_id,
13499                            'amount' => $quotation->amount,
13500                            'budget_status_id' => $quotation->budget_status_id,
13501                            'budget_status' => $statusName,
13502                        ],
13503                    ]);
13504                }
13505            }
13506
13507            // Sync failed — try to return raw Gestiona data with normalized status
13508            $gestionaResult = $presupuestosService->fetchFromGestiona($id, $region);
13509
13510            if (! empty($gestionaResult['success'])) {
13511                return response([
13512                    'message' => 'OK',
13513                    'data' => $gestionaResult['data'],
13514                    'source' => 'gestiona',
13515                ]);
13516            }
13517
13518            return response(['message' => 'KO', 'error' => 'Quotation not found'], 404);
13519        } catch (\Exception $e) {
13520            report(AppException::fromException($e, 'GET_QUOTATION_STATUS_BY_INTERNAL_ID_EXCEPTION'));
13521
13522            return response(['message' => 'KO', 'error' => $e->getMessage()], 500);
13523        }
13524    }
13525
13526    /**
13527     * FIRE-976: Find a quotation by internal_quote_id + company_id, with 18/22 fallback.
13528     */
13529    private function findQuotationByInternalId($id, int $companyId)
13530    {
13531        $quotation = TblQuotations::where('internal_quote_id', $id)
13532            ->where('company_id', $companyId)
13533            ->first();
13534
13535        if (! $quotation && ($companyId === 18 || $companyId === 22)) {
13536            $quotation = TblQuotations::where('internal_quote_id', $id)
13537                ->whereIn('company_id', [18, 22])
13538                ->first();
13539        }
13540
13541        return $quotation;
13542    }
13543}