PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.1.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.1.0
2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 All 68 releases
fluent-support / app / Http / Controllers / TicketController.php

TicketController.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.1.0, at app/Http/Controllers/TicketController.php

1,408 lines 51.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App\Http\Controllers;
4
5 use FluentSupport\App\Models\Agent;
6 use FluentSupport\App\Models\Attachment;
7 use FluentSupport\App\Models\Meta;
8 use FluentSupport\App\Models\Customer;
9 use FluentSupport\Framework\Http\Request\Request;
10 use FluentSupport\Framework\Support\Arr;
11 use FluentSupport\App\Http\Requests\TicketRequest;
12 use FluentSupport\App\Http\Requests\TicketResponseRequest;
13 use FluentSupport\App\Models\Conversation;
14 use FluentSupport\App\Models\Ticket;
15 use FluentSupport\App\Services\FluentCRMServices;
16 use FluentSupport\App\Services\Helper;
17 use FluentSupport\App\Services\ProfileInfoService;
18 use FluentSupport\App\Services\TicketHelper;
19 use FluentSupport\App\Services\TicketQueryService;
20 use FluentSupport\App\Modules\PermissionManager;
21 use FluentSupport\App\Services\Tickets\ResponseService;
22 use FluentSupport\App\Models\AgentGroup;
23 use FluentSupport\App\Services\Tickets\TicketService;
24
25 /**
26 * TicketController class for REST API related to ticket
27 * This class is responsible for getting / inserting/ modifying data for all request related to ticket
28 * @package FluentSupport\App\Http\Controllers
29 *
30 * @version 1.0.0
31 */
32 class TicketController extends Controller
33 {
34 /**
35 * This `me` method will return the current user profile info
36 * @param Request $request
37 * @return array
38 */
39 public function me(Request $request)
40 {
41 $user = wp_get_current_user();
42 $requestData = $request->all();
43 $sanitizedRequest = [];
44 foreach ($requestData as $key => $value) {
45 if (is_array($value)) {
46 $sanitizedRequest[$key] = map_deep($value, 'sanitize_text_field');
47 } else {
48 $sanitizedRequest[$key] = sanitize_text_field($value);
49 }
50 }
51
52 $settings = [
53 'user_id' => $user->ID,
54 'email' => $user->user_email,
55 'person' => Helper::getAgentByUserId($user->ID),
56 'permissions' => PermissionManager::currentUserPermissions(),
57 'request' => $sanitizedRequest
58 ];
59
60 if ($request->getSafe('with_portal_settings', 'sanitize_text_field')) {
61 $mimeHeadings = Helper::getAcceptedMimeHeadings();
62 $businessSettings = (new \FluentSupport\App\Services\EmailNotification\Settings())->globalBusinessSettings();
63 $maxFileSize = absint($businessSettings['max_file_size']);
64
65 $portalSettings = [
66 'support_products' => \FluentSupport\App\Models\Product::select(['id', 'title'])->get(),
67 'customer_ticket_priorities' => Helper::customerTicketPriorities(),
68 'has_file_upload' => !!Helper::ticketAcceptedFileMiles(),
69 'has_rich_text_editor' => true,
70 'max_file_size' => $maxFileSize,
71 'mime_headings' => $mimeHeadings
72 ];
73
74 $portalSettings = apply_filters('fluent_support/customer_portal_vars', $portalSettings);
75 $settings['portal_settings'] = $portalSettings;
76 }
77
78 return $settings;
79 }
80
81 /**
82 * index method will return the list of ticket based on the selected filter
83 * @param Request $request
84 * @return array
85 */
86 public function index(Request $request)
87 {
88 //Selected filter type, either simple or Advanced
89 $filterType = $request->getSafe('filter_type', 'sanitize_text_field', 'simple');
90
91 /*Prepare Query Arguments*/
92 $queryArgs = [
93 'with' => [],
94 'filter_type' => $filterType,
95 'sort_by' => sanitize_sql_orderby($request->getSafe('order_by', 'sanitize_text_field', 'id')),
96 'sort_type' => $request->getSafe('order_type', 'sanitize_text_field', 'DESC') == 'DESC' ? 'DESC' : 'ASC',
97 ];
98
99 //If the selected filter type is advanced
100 if ($filterType == 'advanced') {
101 $advanced_filters = map_deep($request->get('advanced_filters', []), 'sanitize_text_field');
102 //Get the selected query params for advanced filter
103 $queryArgs['filters_groups_raw'] = json_decode($advanced_filters, true);
104 } else {
105 //Selected filter type is simple
106 $queryArgs['simple_filters'] = map_deep($request->get('filters', []), 'sanitize_text_field');
107 $queryArgs['search'] = trim($request->getSafe('search', 'sanitize_text_field', ''));
108
109 if ($customerId = $request->getSafe('customer_id', 'intval')) {
110 $queryArgs['customer_id'] = $customerId;
111 }
112 }
113 /*End Prepare Query Arguments*/
114
115 $ticketsModel = (new TicketQueryService($queryArgs))->getModel();
116
117 $ticketsModel = $ticketsModel->with([
118 'customer' => function ($query) {
119 $query->select(['first_name', 'last_name', 'email', 'id', 'avatar']);
120 }, 'agent' => function ($query) {
121 $query->select(['first_name', 'last_name', 'email', 'avatar', 'id']);
122 },
123 'mailbox',
124 'product',
125 'tags',
126 'preview_response' => function ($query) {
127 $query->latest('id');
128 }
129 ]);
130
131 // apply filters by access level
132 do_action_ref_array('fluent_support/tickets_query_by_permission_ref', [&$ticketsModel, false]);
133
134 $tickets = $ticketsModel->paginate();
135
136 $perPage = $request->getSafe('per_page', 'intval', 15);
137
138 // Load live activity for small page sizes (board/kanban view)
139 if ($perPage < 15) {
140 TicketHelper::loadBatchLiveActivities($tickets);
141 }
142
143 return [
144 'tickets' => $tickets
145 ];
146 }
147
148 /**
149 * createTicket method will create new ticket as well as customer or WP user
150 * @param TicketRequest $request
151 * @return array
152 */
153 public function createTicket(TicketRequest $request)
154 {
155 try {
156 //Sanitize and validate request data via TicketRequest
157 $data = $request->sanitize();
158 $ticketData = $data['ticket'];
159 $maybeNewCustomer = Arr::get($data, 'newCustomer', []);
160
161 //Include attachments if provided
162 if (!empty($data['attachments'])) {
163 $ticketData['attachments'] = $data['attachments'];
164 }
165
166 /*
167 * If customer_id is not provided, attempt to create a new customer
168 * This handles WP user creation and customer creation
169 */
170 if (empty($ticketData['customer_id'])) {
171 $createdUserId = false;
172
173 //If user selected create WP user during ticket creation
174 if (Arr::get($ticketData, 'create_wp_user') == 'yes' && !empty($maybeNewCustomer['username'])) {
175 //Check if username already in use, if not create new user
176 if (!username_exists($maybeNewCustomer['username'])) {
177 $authController = new AuthController();
178 $createdUserId = $authController->createUser($maybeNewCustomer);
179 $authController->maybeUpdateUser($createdUserId, $maybeNewCustomer);
180 }
181 }
182
183 $email = Arr::get($maybeNewCustomer, 'email');
184 if (!$email || !is_email($email)) {
185 return $this->sendError([
186 'message' => __('A valid email is required to create a ticket', 'fluent-support')
187 ]);
188 }
189
190 //Check if customer already exists by email
191 $existingCustomer = Customer::where('email', $email)->first();
192
193 if ($existingCustomer) {
194 $ticketData['customer_id'] = $existingCustomer->id;
195 } else {
196 //Create the customer now
197 $customerData = Arr::only($maybeNewCustomer, (new Customer())->getFillable());
198 $customerData['user_id'] = $createdUserId;
199 $customerData = array_filter($customerData);
200
201 $createCustomer = Customer::create($customerData);
202
203 do_action('fluent_support/customer_created', $createCustomer);
204
205 if (!$createCustomer) {
206 return $this->sendError([
207 'message' => __('Customer could not be created', 'fluent-support')
208 ]);
209 }
210
211 $ticketData['customer_id'] = $createCustomer->id;
212 }
213 }
214
215 //Get customer information from db
216 $customer = Customer::findOrFail($ticketData['customer_id']);
217
218 //Sanitize, store ticket, handle attachments, fire hooks
219 $createdTicket = (new TicketService())->storeTicket($ticketData, $customer);
220
221 return [
222 'message' => __('Ticket has been created successfully', 'fluent-support'),
223 'ticket' => $createdTicket
224 ];
225 } catch (\Exception $e) {
226 return $this->sendError([
227 'message' => Helper::getSafeErrorMessage($e)
228 ]);
229 }
230 }
231
232 /**
233 * getTicket method will return ticket information by ticket id
234 * @param Request $request
235 * @param $ticket_id
236 * @return array
237 */
238 public function getTicket(Request $request, $ticket_id)
239 {
240 try {
241 //Get logged in agent information
242 $agent = Helper::getAgentByUserId();
243
244 $ticketWith = $request->get('with');
245 $ticketWith = is_array($ticketWith) ? map_deep($ticketWith, 'sanitize_text_field') : null;
246
247 if (!$ticketWith) {
248 $ticketWith = ['customer', 'agent', 'product', 'mailbox', 'tags', 'attachments' => function ($q) {
249 $q->whereIn('status', ['active', 'inline']);
250 }];
251 }
252
253 //Get ticket by id
254 $ticket = Ticket::with($ticketWith)->findOrFail($ticket_id);
255
256 //Eager load responses with their nested relations to avoid N+1 queries
257 $ticket->load(['responses' => function ($q) {
258 $q->with('person', 'attachments', 'ccinfo');
259 }]);
260
261 //Check if ticket is in a restricted mailbox
262 $restrictedBusinessBoxes = PermissionManager::getRestrictedMailboxIds();
263
264 if (in_array($ticket->mailbox_id, $restrictedBusinessBoxes)) {
265 throw new \Exception(esc_html__('Ticket cannot be fetched due to restricted mailbox', 'fluent-support'));
266 }
267
268 $this->ensureCanAccessTicket($ticket);
269
270 //If ticket has customer, set custom fields and profile url
271 if ($ticket->customer) {
272 $customFieldsKey = apply_filters('fluent_support/custom_registration_form_fields_key', Helper::getBusinessSettings('custom_registration_form_field'));
273 $ticket->customer->custom_field_keys = $customFieldsKey;
274
275 if ($ticket->customer->user_id) {
276 $customFieldKeysUsingHook = apply_filters('fluent_support/custom_registration_form_fields_key', []);
277 if (!empty($customFieldKeysUsingHook)) {
278 $allUserMeta = get_user_meta($ticket->customer->user_id);
279 foreach ($customFieldKeysUsingHook as $key) {
280 if (isset($allUserMeta[$key][0]) && $allUserMeta[$key][0]) {
281 $ticket->customer->$key = $allUserMeta[$key][0];
282 }
283 }
284 }
285 }
286
287 $ticket->customer->profile_edit_url = $ticket->customer->getUserProfileEditUrl();
288 }
289
290 //If ticket is closed, load closed by person
291 if ($ticket->status == 'closed') {
292 $ticket->load('closed_by_person');
293 }
294
295 //Load agent feedback ratings if pro is active and feature is enabled
296 if (defined('FLUENTSUPPORTPRO_PLUGIN_VERSION') && Helper::isAgentFeedbackEnabled()) {
297 $responseIds = $ticket->responses->pluck('id')->toArray();
298 $feedbacks = Meta::where('object_type', 'conversation_meta')
299 ->where('key', 'agent_feedback_ratings')
300 ->whereIn('object_id', $responseIds)
301 ->get()
302 ->keyBy('object_id');
303
304 foreach ($ticket->responses as $response) {
305 if ($feedbacks->has($response->id)) {
306 $response->agent_feedback = $feedbacks->get($response->id)->value;
307 }
308 }
309 }
310
311 //Format response content
312 foreach ($ticket->responses as $response) {
313 $response->content = links_add_target(make_clickable(wpautop($response->content, false)));
314 if (!empty($response->ccinfo)) {
315 $val = Helper::safeUnserialize($response->ccinfo->value);
316 if (isset($val['cc_email']) && !empty($val['cc_email'])) {
317 $response->cc_info = $val['cc_email'];
318 } else {
319 $response->cc_info = '';
320 }
321 } else {
322 $response->cc_info = '';
323 }
324 }
325
326 $ticket->content = links_add_target(make_clickable(wpautop($ticket->content, false)));
327
328 //Get last activity by agent
329 $ticket->live_activity = TicketHelper::getActivity($ticket->id, $agent->id);
330
331 //Get all carbon copy customer
332 $ccInfo = $ticket->getSettingsValue('cc_email', []);
333 $ticket->carbon_copy = !empty($ccInfo) ? implode(', ', $ccInfo) : '';
334
335 if (defined('FLUENTSUPPORTPRO')) {
336 $ticket->custom_fields = $ticket->customData('admin', true);
337 }
338
339 // Load agent info if ticket was created on behalf of customer
340 if ($ticket->created_by) {
341 $ticket->load('created_by_person');
342 if ($ticket->created_by_person) {
343 $isAgentInitiated = strpos($ticket->content, __(' initialized this ticket', 'fluent-support')) !== false;
344 $ticket->created_by_agent = [
345 'id' => $ticket->created_by_person->id,
346 'full_name' => $ticket->created_by_person->full_name,
347 'agent_initiated' => $isAgentInitiated,
348 ];
349 }
350 }
351
352 $data = [
353 'ticket' => $ticket,
354 'responses' => $ticket->responses,
355 'agent_id' => $agent->id
356 ];
357
358 if (defined('FLUENTSUPPORTPRO') && $ticket->watchers) {
359 $data['watchers'] = TicketHelper::getWatchers($ticket->watchers);
360 }
361
362 $withData = $request->get('with_data', null);
363 $withDataArray = is_array($withData) ? map_deep($withData, 'sanitize_text_field') : [];
364
365 if (defined('FLUENTCRM') && in_array('fluentcrm_profile', $withDataArray)) {
366 $data['fluentcrm_profile'] = Helper::getFluentCrmContactData($ticket->customer);
367 }
368
369 return $data;
370 } catch (\Exception $e) {
371 return $this->sendError([
372 'message' => Helper::getSafeErrorMessage($e)
373 ]);
374 }
375 }
376
377 /**
378 * createResponse method will create response by agent for the ticket
379 * @param Request $request
380 * @param Ticket $ticket
381 * @param int $ticket_id
382 * @return array
383 * @throws \FluentSupport\Framework\Validator\ValidationException
384 */
385 public function createResponse(TicketResponseRequest $request, $ticket_id)
386 {
387 $data = $request->sanitize();
388
389 try {
390 $convoType = Arr::get($data, 'conversation_type', 'response');
391 $isDraft = $convoType === 'draft_response';
392
393 if (!$isDraft) {
394 $this->ensureCanManageTickets();
395 }
396
397 //Get logged-in agent information
398 $agent = Helper::getAgentByUserId();
399
400 if (!$agent) {
401 return $this->sendError([
402 'message' => __('Sorry, You do not have permission. Please add yourself as support agent first', 'fluent-support')
403 ]);
404 }
405
406 $ticket = Ticket::findOrFail($ticket_id);
407
408 $this->ensureCanAccessTicket($ticket);
409
410 $responseData = (new ResponseService())->createResponse($data, $agent, $ticket);
411
412 $responseData['response']->content = wp_specialchars_decode(wpautop($responseData['response']->content, false));
413
414 return [
415 'message' => __('Response has been added', 'fluent-support'),
416 'response' => $responseData['response'],
417 'ticket' => $responseData['ticket'],
418 'update_data' => $responseData['update_data']
419 ];
420 } catch (\Exception $e) {
421 return $this->sendError([
422 'message' => Helper::getSafeErrorMessage($e)
423 ]);
424 }
425 }
426
427 /**
428 * createDraft method will create draft by agent for the ticket
429 * @param Request $request
430 * @param Ticket $ticket
431 * @param int $ticket_id
432 * @return array
433 * @throws \FluentSupport\Framework\Validator\ValidationException
434 */
435 public function createOrUpdatDraft(TicketResponseRequest $request, $ticket_id)
436 {
437 $data = $request->sanitize();
438
439 try {
440 //Get logged-in agent information
441 $agent = Helper::getAgentByUserId();
442
443 if (!$agent) {
444 return $this->sendError([
445 'message' => __('Sorry, You do not have permission. Please add yourself as support agent first', 'fluent-support')
446 ]);
447 }
448
449 $ticket = Ticket::findOrFail($ticket_id);
450
451 $this->ensureCanAccessTicket($ticket);
452
453 $key = 'ticket_no_' . $ticket_id . '_agent_id_' . $agent->id . '_response_draft';
454 $previousDraft = Meta::where('key', $key)->first();
455
456 if ($data['draftID'] || $previousDraft) {
457 Meta::where('key', $key)->update([
458 'value' => maybe_serialize($data)
459 ]);
460
461 return [
462 'message' => __('Draft has been updated', 'fluent-support'),
463 'draftID' => $data['draftID']
464 ];
465 }
466
467 $draftID = Meta::insertGetId([
468 'object_type' => '_fs_auto_draft',
469 'object_id' => $ticket_id,
470 'key' => $key,
471 'value' => maybe_serialize($data)
472 ]);
473
474 return [
475 'message' => __('Draft has been added', 'fluent-support'),
476 'draftID' => $draftID
477 ];
478 } catch (\Exception $e) {
479 return $this->sendError([
480 'message' => Helper::getSafeErrorMessage($e)
481 ]);
482 }
483 }
484
485 public function getDraft($ticket_id)
486 {
487 try {
488 //Get logged-in agent information
489 $agent = Helper::getAgentByUserId();
490
491 if (!$agent) {
492 return $this->sendError([
493 'message' => __('Sorry, You do not have permission. Please add yourself as support agent first', 'fluent-support')
494 ]);
495 }
496
497 $ticket = Ticket::findOrFail($ticket_id);
498
499 $this->ensureCanAccessTicket($ticket);
500
501 $key = 'ticket_no_' . $ticket_id . '_agent_id_' . $agent->id . '_response_draft';
502
503 $draft = Meta::where([
504 'object_type' => '_fs_auto_draft',
505 'key' => $key,
506 ])->first();
507
508 if ($draft) {
509 $draft->value = Helper::safeUnserialize($draft->value);
510 }
511
512 return [
513 'draft' => $draft
514 ];
515 } catch (\Exception $e) {
516 return $this->sendError([
517 'message' => Helper::getSafeErrorMessage($e)
518 ]);
519 }
520 }
521
522 public function deleteDraft($draft_id)
523 {
524 $draft_id = intval($draft_id);
525
526 try {
527 $agent = Helper::getAgentByUserId();
528
529 if (!$agent) {
530 return $this->sendError([
531 'message' => __('You do not have permission to perform this action', 'fluent-support'),
532 ]);
533 }
534
535 $draft = Meta::where('id', $draft_id)
536 ->where('object_type', '_fs_auto_draft')
537 ->first();
538
539 if (!$draft) {
540 return $this->sendError([
541 'message' => __('Draft not found', 'fluent-support'),
542 ]);
543 }
544
545 // Verify ownership: draft key contains agent_id, only managers can delete others' drafts
546 $isOwnDraft = strpos($draft->key, '_agent_id_' . $agent->id . '_') !== false;
547
548 if (!$isOwnDraft && !PermissionManager::canManageTickets()) {
549 return $this->sendError([
550 'message' => __('You do not have permission to delete this draft', 'fluent-support'),
551 ]);
552 }
553
554 $draft->delete();
555
556 return [
557 'message' => __('Discard draft successfully', 'fluent-support'),
558 ];
559 } catch (\Exception $e) {
560 return $this->sendError([
561 'message' => Helper::getSafeErrorMessage($e)
562 ]);
563 }
564 }
565
566 /**
567 * getTicketWidgets method generate additional information for a ticket by customer
568 * @param Ticket $ticket
569 * @param $ticket_id
570 * @return array
571 */
572 public function getTicketWidgets($ticket_id)
573 {
574 try {
575 //Get ticket with customer by ticket id
576 $ticket = Ticket::with('customer')->findOrFail($ticket_id);
577
578 $this->ensureCanAccessTicket($ticket);
579
580 //Get last N tickets of this customer except this
581 $limit = apply_filters('fluent_support/previous_ticket_widgets_limit', 10);
582
583 $otherTickets = Ticket::where('id', '!=', $ticket_id)
584 ->select(['id', 'title', 'status', 'created_at'])
585 ->where('customer_id', $ticket->customer_id)
586 ->latest('id')
587 ->limit($limit)
588 ->get();
589
590 return [
591 'other_tickets' => $otherTickets,
592 'extra_widgets' => ProfileInfoService::getProfileExtraWidgets($ticket->customer)
593 ];
594 } catch (\Exception $e) {
595 return $this->sendError([
596 'message' => Helper::getSafeErrorMessage($e)
597 ]);
598 }
599 }
600
601 /**
602 * updateTicketProperty method will update ticket property
603 * @param Request $request
604 * @param Ticket $ticket
605 * @param $ticket_id
606 * @return array
607 */
608 public function updateTicketProperty(Request $request, $ticket_id)
609 {
610 try {
611 $assigner = Helper::getAgentByUserId();
612 $ticket = Ticket::findOrFail($ticket_id);
613
614 $this->ensureCanAccessTicket($ticket);
615
616 $propName = $request->getSafe('prop_name', 'sanitize_text_field');
617 $propValue = $request->getSafe('prop_value', 'sanitize_text_field');
618 $prevValue = $ticket->{$propName};
619
620 //Validate agent assignment restrictions
621 if ($propName === 'agent_id') {
622 if (!PermissionManager::currentUserCan('fst_assign_agents')) {
623 throw new \Exception(esc_html__('Permission denied to assign agent', 'fluent-support'), 403);
624 }
625
626 $agent = Agent::findOrFail($propValue);
627 $restrictions = $agent->getMeta('agent_restrictions', []);
628
629 if (!empty($restrictions['restrictedBusinessBoxes'])) {
630 $mailboxId = (int) $ticket->mailbox_id;
631 if (in_array($mailboxId, $restrictions['restrictedBusinessBoxes'], true)) {
632 throw new \Exception(esc_html__('Agent is restricted for this mailbox ticket', 'fluent-support'), 403);
633 }
634 }
635 }
636
637 if ($propName && $propValue && $prevValue != $propValue) {
638 $ticket->{$propName} = $propValue;
639 $ticket->save();
640 }
641
642 $updateData = [];
643
644 if ($propName == 'product_id') {
645 $ticket->load('product');
646 $updateData['product'] = $ticket->product;
647 } else if ($propName == 'agent_id') {
648 $ticket->load('agent');
649 $updateData['agent'] = $ticket->agent;
650 $updateData['assigner'] = (new TicketService())->onAgentChange($ticket, $assigner);
651 if ($prevValue != $ticket->{$propName}) {
652 do_action('fluent_support/agent_assigned_to_ticket', $ticket->agent, $ticket, $assigner);
653 }
654 }
655
656 $message = sprintf(
657 /* translators: %s: The name of the property that was updated */
658 __('%s has been updated', 'fluent-support'),
659 esc_html(str_replace('_', ' ', ucwords((string) $propName)))
660 );
661
662 return [
663 'message' => $message,
664 'update_data' => $updateData
665 ];
666 } catch (\Exception $e) {
667 return $this->sendError([
668 'message' => Helper::getSafeErrorMessage($e)
669 ]);
670 }
671 }
672
673 /**
674 * closeTicket method close the ticket by id
675 * @param Ticket $ticket
676 * @param int $ticket_id
677 * @return array
678 */
679 public function closeTicket(Request $request, $ticket_id)
680 {
681 try {
682 $agent = Helper::getAgentByUserId();
683 $ticket = Ticket::findOrFail($ticket_id);
684
685 $this->ensureCanAccessTicket($ticket);
686
687 $closeSilently = $request->getSafe('close_ticket_silently', 'sanitize_text_field');
688
689 return [
690 'message' => __('Ticket has been closed', 'fluent-support'),
691 'ticket' => (new TicketService())->close($ticket, $agent, '', $closeSilently)
692 ];
693 } catch (\Exception $e) {
694 return $this->sendError([
695 'message' => Helper::getSafeErrorMessage($e)
696 ]);
697 }
698 }
699
700 /**
701 * reOpenTicket method will reopen a closed ticket
702 * @param Request $request
703 * @param $ticket_id
704 * @return array
705 */
706 public function reOpenTicket($ticket_id)
707 {
708 try {
709 $agent = Helper::getAgentByUserId();
710 $ticket = Ticket::findOrFail($ticket_id);
711
712 $this->ensureCanAccessTicket($ticket);
713
714 return [
715 'message' => __('Ticket has been opened again', 'fluent-support'),
716 'ticket' => (new TicketService())->reopen($ticket, $agent)
717 ];
718 } catch (\Exception $e) {
719 return $this->sendError([
720 'message' => Helper::getSafeErrorMessage($e)
721 ]);
722 }
723 }
724
725 /**
726 * doBulkActions method is responsible for bulk action
727 * This function will get ticket ids and action as parameter and perform action based on the selection
728 * @param Request $request
729 * @param Ticket $ticket
730 * @return array|string[]|void
731 * @throws \Exception
732 */
733 public function doBulkActions(Request $request)
734 {
735 try {
736 $action = $request->getSafe('bulk_action', 'sanitize_text_field');
737 $ticketIds = array_map('intval', $request->get('ticket_ids', null, []));
738
739 $hasAllPermission = PermissionManager::currentUserCan('fst_manage_other_tickets');
740 $agent = Helper::getAgentByUserId();
741 $query = Ticket::whereIn('id', $ticketIds);
742
743 //If agent do not have permission to manage other tickets
744 if (!$hasAllPermission) {
745 $query->where('agent_id', $agent->id);
746 }
747
748 //If bulk action is close tickets
749 if ($action == 'close_tickets') {
750 $tickets = $query->get();
751 $tickets->each(function ($ticket) use ($agent) {
752 (new TicketService())->close($ticket, $agent);
753 });
754
755 return [
756 'message' => sprintf(
757 /* translators: %d represents the number of closed tickets. */
758 __('%d tickets have been closed.', 'fluent-support'),
759 count($tickets)
760 )
761 ];
762 } else if ($action == 'delete_tickets') {
763 $tickets = $query->get();
764 $ticketService = new TicketService();
765
766 foreach ($tickets as $ticket) {
767 $ticketService->deleteTicket($ticket, $agent);
768 }
769
770 return [
771 'message' => sprintf(
772 /* translators: %d is the number of tickets that were deleted */
773 __('%d tickets have been deleted', 'fluent-support'),
774 count($tickets)
775 )
776 ];
777 } else if ($action == 'assign_agent') {
778 if (!$request->has('agent_id')) {
779 throw new \Exception(esc_html__('agent_id param is required', 'fluent-support'));
780 }
781
782 $assignAgent = Agent::findOrFail($request->getSafe('agent_id', 'intval'));
783
784 $query->where(function ($q) use ($assignAgent) {
785 $q->where('agent_id', '!=', $assignAgent->id)
786 ->orWhereNull('agent_id');
787 });
788
789 $tickets = $query->get();
790 $assignedCount = 0;
791 $skippedCount = 0;
792
793 $tickets->each(function ($ticket) use ($assignAgent, $agent, &$assignedCount, &$skippedCount) {
794 $restrictions = $assignAgent->getMeta('agent_restrictions', []);
795
796 //Skip ticket if mailbox is restricted for the agent
797 if (!empty($restrictions) && in_array($ticket->mailbox_id, $restrictions['restrictedBusinessBoxes'])) {
798 $skippedCount++;
799 return;
800 }
801
802 $ticket->agent_id = $assignAgent->id;
803 $ticket->save();
804 $assignedCount++;
805
806 do_action('fluent_support/agent_assigned_to_ticket', $assignAgent, $ticket, $agent);
807 });
808
809 $assignedMessage = sprintf(
810 /* translators: %1$d is the number of tickets assigned, %2$s is the agent's name. */
811 __('%1$d tickets have been assigned to %2$s.', 'fluent-support'),
812 $assignedCount,
813 $assignAgent->full_name
814 );
815
816 $skippedMessage = $skippedCount > 0
817 ? sprintf(
818 /* translators: %1$d is the number of skipped tickets due to mailbox restrictions. */
819 __('%1$d tickets were skipped due to mailbox restrictions or already being assigned.', 'fluent-support'),
820 $skippedCount
821 )
822 : '';
823
824 return [
825 'message' => trim($assignedMessage . ' ' . $skippedMessage)
826 ];
827 } else if ($action == 'assign_agent_group') {
828 if (!$request->has('agent_group_id')) {
829 throw new \Exception(esc_html__('agent_group_id param is required', 'fluent-support'));
830 }
831
832 $groupId = $request->getSafe('agent_group_id', 'intval');
833 $group = AgentGroup::findOrFail($groupId);
834
835 if ($group->agents()->count() === 0) {
836 throw new \Exception(esc_html__('No agents found in this group', 'fluent-support'));
837 }
838
839 $tickets = $query->get();
840 $assignedCount = 0;
841 $skippedCount = 0;
842 $currentCounts = [];
843
844 foreach ($tickets as $ticket) {
845 $selectedAgent = $group->getLeastLoadedAgent(
846 $ticket->mailbox_id, $currentCounts
847 );
848
849 if (!$selectedAgent) {
850 $skippedCount++;
851 continue;
852 }
853
854 $ticket->agent_id = $selectedAgent->id;
855 $ticket->save();
856 $assignedCount++;
857 $currentCounts[$selectedAgent->id]++;
858
859 as_enqueue_async_action('fluent_support/async_agent_assigned_to_ticket', [
860 $selectedAgent->id, $ticket->id, $agent->id
861 ], 'fluent-support');
862 }
863
864 return [
865 'message' => sprintf(
866 /* translators: %1$d is tickets assigned, %2$d is tickets skipped. */
867 __('%1$d tickets assigned via agent group. %2$d skipped.', 'fluent-support'),
868 $assignedCount,
869 $skippedCount
870 )
871 ];
872 } else if ($action == 'assign_tags') {
873 $tagIds = $request->get('tag_ids', null);
874 if (!is_array($tagIds)) {
875 $tagIds = [];
876 }
877 $tags = array_filter(array_map('absint', $tagIds));
878
879 $query->get()->each(function ($ticket) use ($tags) {
880 $ticket->applyTags($tags);
881 });
882
883 return [
884 'message' => __('Selected tags has been added to tickets', 'fluent-support')
885 ];
886 }
887
888 throw new \Exception(esc_html__('Sorry no action found as available', 'fluent-support'));
889 } catch (\Exception $e) {
890 return $this->sendError([
891 'message' => Helper::getSafeErrorMessage($e)
892 ]);
893 }
894 }
895
896 /**
897 * deleteTicket method will delete a ticket
898 * @param int $ticket_id
899 * @return array
900 */
901 public function deleteTicket($ticket_id)
902 {
903 try {
904 $ticket = Ticket::findOrFail($ticket_id);
905
906 (new TicketService())->deleteTicket($ticket);
907
908 return [
909 'message' => __('Ticket has been deleted successfully', 'fluent-support')
910 ];
911 } catch (\Exception $e) {
912 return $this->sendError([
913 'message' => Helper::getSafeErrorMessage($e)
914 ]);
915 }
916 }
917
918 /**
919 * doBulkReplies method will create response for bulk tickets
920 * This function will get ticket ids, content, attachment etc and create response for tickets
921 * @param Request $request
922 * @param Conversation $conversation
923 * @return array
924 * @throws \Exception
925 */
926 public function doBulkReplies(Request $request)
927 {
928 try {
929 // Sanitize all request data before validation
930 $requestData = $request->all();
931 $data = [];
932 foreach ($requestData as $key => $value) {
933 if (is_array($value)) {
934 if ($key === 'ticket_ids') {
935 $data[$key] = array_map('intval', $value);
936 } elseif ($key === 'content') {
937 $data[$key] = wp_kses_post($value);
938 } else {
939 $data[$key] = map_deep($value, 'sanitize_text_field');
940 }
941 } else {
942 $data[$key] = sanitize_text_field($value);
943 }
944 }
945
946 $this->validate($data, [
947 'content' => 'required',
948 'ticket_ids' => 'required|array'
949 ]);
950
951 //Get logged in agent information
952 $agent = Helper::getAgentByUserId();
953 $ticketIds = array_filter($data['ticket_ids'], 'absint');
954
955 $hasAllPermission = PermissionManager::currentUserCan('fst_manage_other_tickets');
956 $query = Ticket::whereIn('id', $ticketIds)->where('status', '!=', 'closed');
957
958 //If the agent does not have permission
959 if (!$hasAllPermission) {
960 $query->where('agent_id', $agent->id);
961 }
962
963 $tickets = $query->get();
964
965 if ($tickets->isEmpty()) {
966 throw new \Exception(esc_html__('Sorry no tickets found based on your filter and bulk actions', 'fluent-support'));
967 }
968
969 $responseData = [
970 'content' => wp_kses_post(Arr::get($data, 'content', '')),
971 'conversation_type' => 'response',
972 'close_ticket' => Arr::get($data, 'close_ticket'),
973 ];
974
975 //If request with file attachments
976 $attachmentHashes = Arr::get($data, 'attachments', []);
977 $attachments = false;
978 if ($attachmentHashes) {
979 $attachments = Attachment::whereNull('ticket_id')
980 ->orderBy('id', 'asc')
981 ->whereIn('file_hash', $attachmentHashes)
982 ->get();
983 }
984
985 $responseService = new ResponseService();
986
987 foreach ($tickets as $ticket) {
988 if ($attachments) {
989 $responseData['attachments'] = [];
990 $attachmentRecords = [];
991 foreach ($attachments as $attachment) {
992 $fileHash = bin2hex(random_bytes(16));
993 $attachmentRecords[] = [
994 'ticket_id' => $ticket->id,
995 'file_path' => $attachment->file_path,
996 'full_url' => $attachment->full_url,
997 'title' => $attachment->title,
998 'driver' => $attachment->driver,
999 'file_size' => $attachment->file_size,
1000 'status' => $attachment->status,
1001 'file_hash' => $fileHash,
1002 ];
1003 $responseData['attachments'][] = $fileHash;
1004 }
1005 if ($attachmentRecords) {
1006 Attachment::insert($attachmentRecords);
1007 }
1008 }
1009
1010 $responseService->createResponse($responseData, $agent, $ticket);
1011 }
1012
1013 return [
1014 'message' => __('Response has been added to the selected tickets', 'fluent-support')
1015 ];
1016 } catch (\Exception $e) {
1017 return $this->sendError([
1018 'message' => Helper::getSafeErrorMessage($e)
1019 ]);
1020 }
1021 }
1022
1023 /**
1024 * deleteResponse method will remove a response from ticket by ticket id and response id
1025 * @param Request $request
1026 * @param Conversation $conversation
1027 * @param $ticket_id
1028 * @param $response_id
1029 * @return array
1030 */
1031 public function deleteResponse($ticket_id, $response_id)
1032 {
1033 try {
1034 $ticket = Ticket::findOrFail($ticket_id);
1035 $response = Conversation::findOrFail($response_id);
1036 $agent = Helper::getAgentByUserId();
1037
1038 if (!PermissionManager::currentUserCan('fst_delete_tickets') && $ticket->agent_id !== $agent->id) {
1039 throw new \Exception(
1040 esc_html__('Sorry, you do not have permission to delete this response.', 'fluent-support')
1041 );
1042 }
1043
1044 $response->delete();
1045 $response->ccinfo()->delete();
1046
1047 return [
1048 'message' => __('Selected response has been deleted', 'fluent-support')
1049 ];
1050 } catch (\Exception $e) {
1051 return $this->sendError([
1052 'message' => Helper::getSafeErrorMessage($e)
1053 ]);
1054 }
1055 }
1056
1057 /**
1058 * updateResponse method will update ticket response using ticket and response id
1059 * @param Request $request
1060 * @param int $ticket_id
1061 * @param int $response_id
1062 * @return array
1063 * @throws \Exception
1064 */
1065 public function updateResponse(TicketResponseRequest $request, $ticket_id, $response_id)
1066 {
1067 try {
1068 $ticket = Ticket::findOrFail($ticket_id);
1069 $response = Conversation::findOrFail($response_id);
1070 $agent = Helper::getAgentByUserId();
1071
1072 if (!PermissionManager::currentUserCan('fst_manage_other_tickets') && $ticket->agent_id !== $agent->id) {
1073 throw new \Exception(
1074 esc_html__('Sorry, you do not have permission to update this response.', 'fluent-support')
1075 );
1076 }
1077
1078 $response->content = wp_unslash(wp_kses_post($request->getSafe('content', 'wp_kses_post')));
1079
1080 //If updating a draft response by someone other than the author, check approval permission
1081 if ($response->conversation_type == 'draft_response' && $response->person_id != $agent->id) {
1082 if (!PermissionManager::currentUserCan('fst_approve_draft_reply')) {
1083 throw new \Exception(
1084 esc_html__('Sorry, You do not have permission to approve this draft response', 'fluent-support')
1085 );
1086 }
1087 $response->conversation_type = 'response';
1088 }
1089
1090 $response->save();
1091
1092 return [
1093 'message' => __('Selected response has been updated', 'fluent-support'),
1094 'response' => $response
1095 ];
1096 } catch (\Exception $e) {
1097 return $this->sendError([
1098 'message' => Helper::getSafeErrorMessage($e)
1099 ]);
1100 }
1101 }
1102
1103 public function approveDraftResponse(TicketResponseRequest $request, $ticket_id, $response_id)
1104 {
1105 try {
1106 if (!PermissionManager::currentUserCan('fst_approve_draft_reply')) {
1107 throw new \Exception(
1108 esc_html__('You do not have permission to approve draft responses.', 'fluent-support')
1109 );
1110 }
1111
1112 $ticket = Ticket::findOrFail($ticket_id);
1113
1114 $response = Conversation::where('id', $response_id)
1115 ->where('ticket_id', $ticket_id)
1116 ->where('conversation_type', 'draft_response')
1117 ->firstOrFail();
1118
1119 $person = Helper::getAgentByUserId();
1120
1121 $content = wp_unslash(wp_kses_post($request->getSafe('content', 'wp_kses_post')));
1122 $resetWaitingSince = apply_filters('fluent_support/reset_waiting_since', true, $content);
1123
1124 $response->conversation_type = 'response';
1125 $response->created_at = current_time('mysql');
1126 $response->save();
1127
1128 if ($person->person_type == 'agent' && $ticket->status == 'new') {
1129 $ticket->status = 'active';
1130 if ($ticket->created_at) {
1131 $ticket->first_response_time = strtotime(current_time('mysql')) - strtotime($ticket->created_at);
1132 } else {
1133 $ticket->first_response_time = 300;
1134 }
1135 }
1136
1137 if ($resetWaitingSince) {
1138 $ticket->last_agent_response = current_time('mysql');
1139 $ticket->waiting_since = current_time('mysql');
1140 }
1141
1142 $ticket->response_count += 1;
1143 $ticket->save();
1144
1145 do_action('fluent_support/response_added_by_' . $person->person_type, $response, $ticket, $person);
1146
1147 return [
1148 'message' => __('Draft response has been successfully approved.', 'fluent-support'),
1149 'response' => $response,
1150 ];
1151 } catch (\Exception $e) {
1152 return $this->sendError([
1153 'message' => Helper::getSafeErrorMessage($e)
1154 ]);
1155 }
1156 }
1157
1158 /**
1159 * getLiveActivity method will return the activity in a ticket by agents
1160 * @param Request $request
1161 * @param $ticket_id
1162 * @return array
1163 */
1164 public function getLiveActivity(Request $request, $ticket_id)
1165 {
1166 $agent = Helper::getAgentByUserId();
1167
1168 return [
1169 'live_activity' => TicketHelper::getActivity($ticket_id, $agent->id)
1170 ];
1171 }
1172
1173 /**
1174 * removeLiveActivity method will remove activities that
1175 * @param Request $request
1176 * @param $ticket_id
1177 * @return array
1178 */
1179 public function removeLiveActivity(Request $request, $ticket_id)
1180 {
1181 $agent = Helper::getAgentByUserId();
1182
1183 return [
1184 'result' => TicketHelper::removeFromActivities($ticket_id, $agent->id),
1185 'agent_id' => $agent->id
1186 ];
1187 }
1188
1189 /**
1190 * addTag method will add tag in ticket by ticket id
1191 * @param Request $request
1192 * @param $ticket_id
1193 * @return array
1194 */
1195 public function addTag(Request $request, $ticket_id)
1196 {
1197 try {
1198 $ticket = Ticket::findOrFail($ticket_id);
1199 $ticket->applyTags($request->getSafe('tag_id', 'intval'));
1200
1201 return [
1202 'message' => __('Tag has been added to this ticket', 'fluent-support'),
1203 'tags' => $ticket->tags
1204 ];
1205 } catch (\Exception $e) {
1206 return $this->sendError([
1207 'message' => Helper::getSafeErrorMessage($e)
1208 ]);
1209 }
1210 }
1211
1212 /**
1213 * detachTag method will remove all tags from tickets
1214 * @param $ticket_id
1215 * @param $tag_id
1216 * @return array
1217 */
1218 public function detachTag($ticket_id, $tag_id)
1219 {
1220 try {
1221 $ticket = Ticket::findOrFail($ticket_id);
1222 $ticket->detachTags($tag_id);
1223
1224 return [
1225 'message' => __('Tag has been removed from this ticket', 'fluent-support'),
1226 'tags' => $ticket->tags
1227 ];
1228 } catch (\Exception $e) {
1229 return $this->sendError([
1230 'message' => Helper::getSafeErrorMessage($e)
1231 ]);
1232 }
1233 }
1234
1235 /**
1236 * changeTicketCustomer method will update customer in a ticket
1237 * This method will get ticket id and customer id as parameter, it will replace existing customer id with new
1238 * @param Request $request
1239 * @return array
1240 */
1241 public function changeTicketCustomer(Request $request)
1242 {
1243 $ticketId = $request->getSafe('ticket_id', 'intval');
1244 $newCustomerId = $request->getSafe('customer', 'intval');
1245
1246 if (!$newCustomerId) {
1247 return $this->sendError(__('Invalid customer selected.', 'fluent-support'));
1248 }
1249
1250 try {
1251 $updated = Ticket::where('id', $ticketId)
1252 ->where('customer_id', '!=', $newCustomerId)
1253 ->update(['customer_id' => $newCustomerId]);
1254
1255 return $updated
1256 ? ['message' => __('Customer has been updated', 'fluent-support')]
1257 : $this->sendError(__('Ticket not found or customer already assigned.', 'fluent-support'));
1258
1259 } catch (\Exception $e) {
1260 return $this->sendError([
1261 'message' => Helper::getSafeErrorMessage($e)
1262 ]);
1263 }
1264 }
1265
1266 /**
1267 * getTicketCustomData method will return the custom data by ticket id
1268 * @param Request $request
1269 * @param $ticket_id
1270 * @return array|array[]
1271 */
1272 public function getTicketCustomData(Request $request, $ticket_id)
1273 {
1274 if (!defined('FLUENTSUPPORTPRO')) {
1275 return [
1276 'custom_data' => [],
1277 'rendered_fields' => []
1278 ];
1279 }
1280
1281 $ticket = Ticket::findOrFail($ticket_id);
1282
1283 return [
1284 'custom_data' => (object)$ticket->customData(),
1285 'rendered_fields' => \FluentSupportPro\App\Services\CustomFieldsService::getRenderedPublicFields($ticket->customer, 'admin')
1286 ];
1287 }
1288
1289 /**
1290 * syncFluentCrmTags method will synchronize the tags with Fluent CRM by contact id
1291 *This function will get contact id and tags as parameter, get existing tags from crm and updated added/removed tags
1292 * @param Request $request
1293 * @param FluentCRMServices $fluentCRMServices
1294 * @return array
1295 */
1296 public function syncFluentCrmTags(Request $request, FluentCRMServices $fluentCRMServices)
1297 {
1298 $data = [
1299 'contact_id' => $request->getSafe('contact_id', 'intval'),
1300 'tags' => $request->get('tags', null)
1301 ];
1302
1303 // Sanitize tags array if it's an array
1304 if (is_array($data['tags'])) {
1305 $data['tags'] = array_map('intval', $data['tags']);
1306 }
1307
1308 try {
1309 return $fluentCRMServices->syncCrmTags($data);
1310 } catch (\Exception $e) {
1311 return $this->sendError([
1312 'message' => Helper::getSafeErrorMessage($e)
1313 ]);
1314 }
1315 }
1316
1317 /**
1318 * This `syncFluentCrmLists` method will synchronize the lists with Fluent CRM by contact id
1319 * This method will get contact id and lists as parameter, get existing lists from crm and updated added/removed lists
1320 * @param Request $request
1321 * @param FluentCRMServices $fluentCRMServices
1322 * @return array
1323 */
1324
1325 public function syncFluentCrmLists(Request $request, FluentCRMServices $fluentCRMServices)
1326 {
1327 $data = [
1328 'contact_id' => $request->getSafe('contact_id', 'intval'),
1329 'lists' => $request->get('lists', null, [])
1330 ];
1331
1332 // Sanitize lists array if it's an array
1333 if (is_array($data['lists'])) {
1334 $data['lists'] = array_map('intval', $data['lists']);
1335 }
1336
1337 try {
1338 return $fluentCRMServices->syncCrmLists($data);
1339 } catch (\Exception $e) {
1340 return $this->sendError([
1341 'message' => Helper::getSafeErrorMessage($e)
1342 ]);
1343 }
1344 }
1345
1346 /**
1347 * Get ticket essentials data based on the provided types.
1348 *
1349 * @param \Illuminate\Http\Request $request
1350 * @return array The ticket essentials data.
1351 */
1352 public function getTicketEssentials(Request $request)
1353 {
1354 $type = $request->getSafe('type', 'sanitize_text_field');
1355
1356 return TicketHelper::getTicketEssentials($type);
1357 }
1358
1359 public function fetchLabelSearch()
1360 {
1361 try {
1362 $agent_id = get_current_user_id();
1363 return TicketHelper::getLabelSearch($agent_id);
1364 } catch (\Exception $e) {
1365 return $this->sendError([
1366 'message' => Helper::getSafeErrorMessage($e)
1367 ]);
1368 }
1369 }
1370
1371 public function storeOrUpdateLabelSearch(Request $request)
1372 {
1373 try {
1374 $agent_id = get_current_user_id();
1375 $searchData = $request->get('query', null, []);
1376 if (is_array($searchData)) {
1377 $searchData = map_deep($searchData, 'sanitize_text_field');
1378 }
1379 $filterType = Arr::get($searchData, 'filter_type', '');
1380 if ($filterType == 'advanced') {
1381 return TicketHelper::saveSearchLabel($agent_id, $searchData, $filterType);
1382 }
1383
1384 return [
1385 'message' => __('Invalid filter type.', 'fluent-support'),
1386 ];
1387
1388 } catch (\Exception $e) {
1389 return $this->sendError([
1390 'message' => Helper::getSafeErrorMessage($e)
1391 ]);
1392 }
1393 }
1394
1395 public function deleteLabelSearch(Request $request, $search_id)
1396 {
1397 try {
1398 $agent_id = get_current_user_id();
1399 return TicketHelper::deleteSavedSearch($search_id);
1400 } catch (\Exception $e) {
1401 return $this->sendError([
1402 'message' => Helper::getSafeErrorMessage($e)
1403 ]);
1404 }
1405 }
1406 }
1407
1408