PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.2.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.2.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 1.5.6 All 67 releases
fluent-support / app / Http / Controllers / TicketController.php

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

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