PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.3.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.3.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.3.0, at app/Http/Controllers/TicketController.php

1,709 lines 62.0 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 if ($response->conversation_type === 'note') {
342 $responseContent = wpautop($responseContent, false);
343 } else {
344 $responseContent = links_add_target(make_clickable(wpautop($responseContent, false)));
345 }
346
347
348 $response->content = apply_filters(
349 'fluent_support/response_content_after_render',
350 $responseContent,
351 $response,
352 $ticket
353 );
354
355 if (!empty($response->ccinfo)) {
356 $val = Helper::safeUnserialize($response->ccinfo->value);
357 if (isset($val['cc_email']) && !empty($val['cc_email'])) {
358 $response->cc_info = $val['cc_email'];
359 } else {
360 $response->cc_info = '';
361 }
362 } else {
363 $response->cc_info = '';
364 }
365 }
366
367 $ticketContent = apply_filters(
368 'fluent_support/ticket_content_before_render',
369 $ticket->content,
370 $ticket
371 );
372
373 $ticketContent = links_add_target(make_clickable(wpautop($ticketContent, false)));
374
375 $ticket->content = apply_filters(
376 'fluent_support/ticket_content_after_render',
377 $ticketContent,
378 $ticket
379 );
380
381 //Get last activity by agent
382 $ticket->live_activity = TicketHelper::getActivity($ticket->id, $agent->id);
383
384 //Get all carbon copy customer
385 $ccInfo = $ticket->getSettingsValue('cc_email', []);
386 $ticket->carbon_copy = !empty($ccInfo) ? implode(', ', $ccInfo) : '';
387
388 if (defined('FLUENTSUPPORTPRO')) {
389 $ticket->custom_fields = $ticket->customData('admin', true);
390 }
391
392 // Load agent info if ticket was created on behalf of customer
393 if ($ticket->created_by) {
394 $ticket->load('created_by_person');
395 if ($ticket->created_by_person) {
396 $isAgentInitiated = strpos($ticket->content, __(' initialized this ticket', 'fluent-support')) !== false;
397 $ticket->created_by_agent = [
398 'id' => $ticket->created_by_person->id,
399 'full_name' => $ticket->created_by_person->full_name,
400 'agent_initiated' => $isAgentInitiated,
401 ];
402 }
403 }
404
405 $data = [
406 'ticket' => $ticket,
407 'responses' => $ticket->responses,
408 'agent_id' => $agent->id
409 ];
410
411 if (defined('FLUENTSUPPORTPRO') && $ticket->watchers) {
412 $data['watchers'] = TicketHelper::getWatchers($ticket->watchers);
413 }
414
415 $withData = $request->get('with_data', null);
416 $withDataArray = is_array($withData) ? map_deep($withData, 'sanitize_text_field') : [];
417
418 if (defined('FLUENTCRM') && in_array('fluentcrm_profile', $withDataArray)) {
419 $data['fluentcrm_profile'] = Helper::getFluentCrmContactData($ticket->customer);
420 }
421
422 return $data;
423 } catch (\Exception $e) {
424 return $this->sendError([
425 'message' => Helper::getSafeErrorMessage($e)
426 ]);
427 }
428 }
429
430 public function getMentionableAgents(Request $request, $ticket_id)
431 {
432 try {
433 $ticket = Ticket::findOrFail($ticket_id);
434
435 if (in_array($ticket->mailbox_id, PermissionManager::getRestrictedMailboxIds())) {
436 throw new \Exception(esc_html__('Ticket cannot be fetched due to restricted mailbox', 'fluent-support'));
437 }
438
439 $this->ensureCanAccessTicket($ticket);
440
441 $search = trim($request->getSafe('search', 'sanitize_text_field', ''));
442 $limit = min(max(absint($request->getSafe('limit', 'intval', 20)), 1), 50);
443
444 return [
445 'agents' => $this->getMentionableAgentList($ticket, $search, $limit)
446 ];
447 } catch (\Exception $e) {
448 return $this->sendError([
449 'message' => Helper::getSafeErrorMessage($e)
450 ]);
451 }
452 }
453
454 protected function getMentionableAgentList($ticket, $search, $limit)
455 {
456 $allAgents = Agent::select(['id', 'first_name', 'last_name', 'email', 'user_id'])
457 ->mentionBy($search)
458 ->orderBy('first_name')
459 ->orderBy('last_name')
460 ->get();
461
462 if ($allAgents->isEmpty()) {
463 return [];
464 }
465
466 $restrictions = $this->getAgentRestrictionsMap($allAgents->pluck('id')->all());
467 $ticketAccess = new AgentTicketAccess();
468 $results = [];
469
470 foreach ($allAgents as $agent) {
471 if (!$ticketAccess->canAccess($agent, $ticket, $restrictions[$agent->id] ?? [])) {
472 continue;
473 }
474
475 $results[] = [
476 'id' => strval($agent->id),
477 'first_name' => $agent->first_name,
478 'last_name' => $agent->last_name,
479 'email' => $agent->email,
480 ];
481
482 if (count($results) >= $limit) {
483 break;
484 }
485 }
486
487 return $results;
488 }
489
490 protected function getAgentRestrictionsMap(array $agentIds)
491 {
492 if (!$agentIds) {
493 return [];
494 }
495
496 $metas = Meta::where('object_type', 'person_meta')
497 ->where('key', 'agent_restrictions')
498 ->whereIn('object_id', $agentIds)
499 ->get();
500
501 $restrictions = [];
502 foreach ($metas as $meta) {
503 $restrictions[$meta->object_id] = Helper::safeUnserialize($meta->value) ?: [];
504 }
505
506 return $restrictions;
507 }
508
509 /**
510 * createResponse method will create response by agent for the ticket
511 * @param Request $request
512 * @param Ticket $ticket
513 * @param int $ticket_id
514 * @return array
515 * @throws \FluentSupport\Framework\Validator\ValidationException
516 */
517 public function createResponse(TicketResponseRequest $request, $ticket_id)
518 {
519 $data = $request->sanitize();
520
521 try {
522 $convoType = Arr::get($data, 'conversation_type', 'response');
523 $isDraft = $convoType === 'draft_response';
524
525 if (!$isDraft) {
526 $this->ensureCanManageTickets();
527 }
528
529 //Get logged-in agent information
530 $agent = Helper::getAgentByUserId();
531
532 if (!$agent) {
533 return $this->sendError([
534 'message' => __('Sorry, You do not have permission. Please add yourself as support agent first', 'fluent-support')
535 ]);
536 }
537
538 $ticket = Ticket::findOrFail($ticket_id);
539
540 $this->ensureCanAccessTicket($ticket);
541
542 $responseData = (new ResponseService())->createResponse($data, $agent, $ticket);
543
544 $responseData['response']->content = Helper::refreshSignedAttachmentUrls($responseData['response']->content, $ticket->id);
545 $responseData['response']->load([
546 'attachments' => function ($q) {
547 $q->where('status', 'active');
548 }
549 ]);
550 $responseData['response']->content = wp_specialchars_decode(wpautop($responseData['response']->content, false));
551
552 return [
553 'message' => __('Response has been added', 'fluent-support'),
554 'response' => $responseData['response'],
555 'ticket' => $responseData['ticket'],
556 'update_data' => $responseData['update_data']
557 ];
558 } catch (\Exception $e) {
559 return $this->sendError([
560 'message' => Helper::getSafeErrorMessage($e)
561 ]);
562 }
563 }
564
565 public function getFluentBookingEventTypes()
566 {
567 try {
568 // All FluentBooking endpoints require manage permission; view-only agents cannot call a meeting.
569 $this->ensureCanManageTickets();
570
571 $service = new FluentBookingService();
572 $eventTypes = $service->getEventTypes();
573
574 return [
575 'status' => $service->getStatus($eventTypes),
576 'event_types' => $eventTypes
577 ];
578 } catch (\Exception $e) {
579 return $this->sendError([
580 'message' => Helper::getSafeErrorMessage($e)
581 ]);
582 }
583 }
584
585 public function createFluentBookingLink(Request $request, $ticket_id)
586 {
587 try {
588 // All FluentBooking endpoints require manage permission; view-only agents cannot call a meeting.
589 $this->ensureCanManageTickets();
590
591 $ticket = Ticket::with('customer')->findOrFail($ticket_id);
592
593 // Enforces per-ticket visibility (e.g. own-tickets-only agents cannot access unassigned tickets).
594 $this->ensureCanAccessTicket($ticket);
595
596 $eventId = $request->getSafe('event_type_id', 'intval');
597
598 if (!$eventId) {
599 throw new \Exception(esc_html__('Please select a FluentBooking event type.', 'fluent-support'));
600 }
601
602 return (new FluentBookingService())->createBookingLink(
603 $ticket,
604 $eventId,
605 $request->getSafe('message', 'wp_kses_post'),
606 $request->get('selected_slots', []),
607 $request->getSafe('timezone', 'sanitize_text_field', '')
608 );
609 } catch (\Exception $e) {
610 return $this->sendError([
611 'message' => Helper::getSafeErrorMessage($e)
612 ]);
613 }
614 }
615
616 public function getFluentBookingAvailability(Request $request, $ticket_id)
617 {
618 try {
619 // All FluentBooking endpoints require manage permission; view-only agents cannot call a meeting.
620 $this->ensureCanManageTickets();
621
622 $ticket = Ticket::with('customer')->findOrFail($ticket_id);
623
624 // Enforces per-ticket visibility (e.g. own-tickets-only agents cannot access unassigned tickets).
625 $this->ensureCanAccessTicket($ticket);
626
627 $eventId = $request->getSafe('event_type_id', 'intval');
628
629 if (!$eventId) {
630 throw new \Exception(esc_html__('Please select a FluentBooking event type.', 'fluent-support'));
631 }
632
633 return [
634 'availability' => (new FluentBookingService())->getAvailabilitySlots(
635 $eventId,
636 $request->getSafe('range', 'sanitize_key', 'next_3_days'),
637 $request->getSafe('timezone', 'sanitize_text_field'),
638 $request->getSafe('duration', 'intval'),
639 $ticket,
640 $request->get('selected_dates', []),
641 $request->getSafe('calendar_month', 'sanitize_text_field', '')
642 )
643 ];
644 } catch (\Exception $e) {
645 return $this->sendError([
646 'message' => Helper::getSafeErrorMessage($e)
647 ]);
648 }
649 }
650
651 public function getFluentBookingMeetings($ticket_id)
652 {
653 try {
654 // All FluentBooking endpoints require manage permission; view-only agents cannot call a meeting.
655 $this->ensureCanManageTickets();
656
657 $ticket = Ticket::with('customer')->findOrFail($ticket_id);
658
659 // Enforces per-ticket visibility (e.g. own-tickets-only agents cannot access unassigned tickets).
660 $this->ensureCanAccessTicket($ticket);
661
662 return [
663 'meetings' => (new FluentBookingService())->getTicketMeetings($ticket)
664 ];
665 } catch (\Exception $e) {
666 return $this->sendError([
667 'message' => Helper::getSafeErrorMessage($e)
668 ]);
669 }
670 }
671
672 /**
673 * createDraft method will create draft by agent for the ticket
674 * @param Request $request
675 * @param Ticket $ticket
676 * @param int $ticket_id
677 * @return array
678 * @throws \FluentSupport\Framework\Validator\ValidationException
679 */
680 public function createOrUpdatDraft(TicketResponseRequest $request, $ticket_id)
681 {
682 $data = $request->sanitize();
683
684 try {
685 //Get logged-in agent information
686 $agent = Helper::getAgentByUserId();
687
688 if (!$agent) {
689 return $this->sendError([
690 'message' => __('Sorry, You do not have permission. Please add yourself as support agent first', 'fluent-support')
691 ]);
692 }
693
694 $ticket = Ticket::findOrFail($ticket_id);
695
696 $this->ensureCanAccessTicket($ticket);
697
698 $key = 'ticket_no_' . $ticket_id . '_agent_id_' . $agent->id . '_response_draft';
699 $previousDraft = Meta::where('key', $key)->first();
700
701 if ($data['draftID'] || $previousDraft) {
702 Meta::where('key', $key)->update([
703 'value' => maybe_serialize($data)
704 ]);
705
706 return [
707 'message' => __('Draft has been updated', 'fluent-support'),
708 'draftID' => $data['draftID']
709 ];
710 }
711
712 $draftID = Meta::insertGetId([
713 'object_type' => '_fs_auto_draft',
714 'object_id' => $ticket_id,
715 'key' => $key,
716 'value' => maybe_serialize($data)
717 ]);
718
719 return [
720 'message' => __('Draft has been added', 'fluent-support'),
721 'draftID' => $draftID
722 ];
723 } catch (\Exception $e) {
724 return $this->sendError([
725 'message' => Helper::getSafeErrorMessage($e)
726 ]);
727 }
728 }
729
730 public function getDraft($ticket_id)
731 {
732 try {
733 //Get logged-in agent information
734 $agent = Helper::getAgentByUserId();
735
736 if (!$agent) {
737 return $this->sendError([
738 'message' => __('Sorry, You do not have permission. Please add yourself as support agent first', 'fluent-support')
739 ]);
740 }
741
742 $ticket = Ticket::findOrFail($ticket_id);
743
744 $this->ensureCanAccessTicket($ticket);
745
746 $key = 'ticket_no_' . $ticket_id . '_agent_id_' . $agent->id . '_response_draft';
747
748 $draft = Meta::where([
749 'object_type' => '_fs_auto_draft',
750 'key' => $key,
751 ])->first();
752
753 if ($draft) {
754 $draft->value = Helper::safeUnserialize($draft->value);
755 }
756
757 return [
758 'draft' => $draft
759 ];
760 } catch (\Exception $e) {
761 return $this->sendError([
762 'message' => Helper::getSafeErrorMessage($e)
763 ]);
764 }
765 }
766
767 public function deleteDraft($draft_id)
768 {
769 $draft_id = intval($draft_id);
770
771 try {
772 $agent = Helper::getAgentByUserId();
773
774 if (!$agent) {
775 return $this->sendError([
776 'message' => __('You do not have permission to perform this action', 'fluent-support'),
777 ]);
778 }
779
780 $draft = Meta::where('id', $draft_id)
781 ->where('object_type', '_fs_auto_draft')
782 ->first();
783
784 if (!$draft) {
785 return $this->sendError([
786 'message' => __('Draft not found', 'fluent-support'),
787 ]);
788 }
789
790 // Verify ownership: draft key contains agent_id, only managers can delete others' drafts
791 $isOwnDraft = strpos($draft->key, '_agent_id_' . $agent->id . '_') !== false;
792
793 if (!$isOwnDraft && !PermissionManager::canManageTickets()) {
794 return $this->sendError([
795 'message' => __('You do not have permission to delete this draft', 'fluent-support'),
796 ]);
797 }
798
799 $draft->delete();
800
801 return [
802 'message' => __('Discard draft successfully', 'fluent-support'),
803 ];
804 } catch (\Exception $e) {
805 return $this->sendError([
806 'message' => Helper::getSafeErrorMessage($e)
807 ]);
808 }
809 }
810
811 /**
812 * getTicketWidgets method generate additional information for a ticket by customer
813 * @param Ticket $ticket
814 * @param $ticket_id
815 * @return array
816 */
817 public function getTicketWidgets(Request $request, $ticket_id)
818 {
819 try {
820 //Get ticket with customer by ticket id
821 $ticket = Ticket::with('customer')->findOrFail($ticket_id);
822
823 $this->ensureCanAccessTicket($ticket);
824
825 $perPage = max(1, absint(apply_filters('fluent_support/previous_ticket_widgets_limit', 5)));
826 $page = max(1, absint($request->get('page', 1)));
827 $offset = ($page - 1) * $perPage;
828
829 $baseQuery = Ticket::where('id', '!=', $ticket_id)
830 ->where('customer_id', $ticket->customer_id);
831
832 (new AgentTicketAccess())->applyAccessScope($baseQuery);
833
834 $total = $baseQuery->count();
835
836 $otherTickets = (clone $baseQuery)
837 ->select(['id', 'title', 'status', 'created_at'])
838 ->latest('id')
839 ->limit($perPage)
840 ->offset($offset)
841 ->get();
842
843 $response = [
844 'other_tickets' => $otherTickets,
845 'other_tickets_total' => $total,
846 'other_tickets_more' => ($offset + $perPage) < $total,
847 ];
848
849 if (in_array('extra_widgets', $request->get('with', []))) {
850 $response['extra_widgets'] = ProfileInfoService::getProfileExtraWidgets($ticket->customer);
851 }
852
853 return $response;
854 } catch (\Exception $e) {
855 return $this->sendError([
856 'message' => Helper::getSafeErrorMessage($e)
857 ]);
858 }
859 }
860
861 /**
862 * updateTicketProperty method will update ticket property
863 * @param Request $request
864 * @param Ticket $ticket
865 * @param $ticket_id
866 * @return array
867 */
868 public function updateTicketProperty(Request $request, $ticket_id)
869 {
870 try {
871 $assigner = Helper::getAgentByUserId();
872 $ticket = Ticket::findOrFail($ticket_id);
873
874 $this->ensureCanAccessTicket($ticket);
875
876 $propName = $request->getSafe('prop_name', 'sanitize_text_field');
877 $propValue = $request->getSafe('prop_value', 'sanitize_text_field');
878 $prevValue = $ticket->{$propName};
879
880 //Validate agent assignment restrictions
881 if ($propName === 'agent_id') {
882 if (!PermissionManager::currentUserCan('fst_assign_agents')) {
883 throw new \Exception(esc_html__('Permission denied to assign agent', 'fluent-support'), 403);
884 }
885
886 $agent = Agent::findOrFail($propValue);
887 $restrictions = $agent->getMeta('agent_restrictions', []);
888
889 if (!empty($restrictions['restrictedBusinessBoxes'])) {
890 $mailboxId = (int) $ticket->mailbox_id;
891 if (in_array($mailboxId, $restrictions['restrictedBusinessBoxes'], true)) {
892 throw new \Exception(esc_html__('Agent is restricted for this mailbox ticket', 'fluent-support'), 403);
893 }
894 }
895 }
896
897 if ($propName && $propValue && $prevValue != $propValue) {
898 $ticket->{$propName} = $propValue;
899 $ticket->save();
900
901 // Log an internal note for status changes so the activity is
902 // traceable, mirroring the close/reopen flows.
903 if ($propName === 'status') {
904 $statuses = Helper::ticketStatuses();
905 $fromLabel = isset($statuses[$prevValue]) ? $statuses[$prevValue] : $prevValue;
906 $toLabel = isset($statuses[$propValue]) ? $statuses[$propValue] : $propValue;
907
908 $internalNote = sprintf(
909 /* translators: 1: previous status, 2: new status */
910 __('Ticket status changed from %1$s to %2$s', 'fluent-support'),
911 esc_html($fromLabel),
912 esc_html($toLabel)
913 );
914
915 Conversation::create([
916 'ticket_id' => $ticket->id,
917 'person_id' => $assigner->id,
918 'conversation_type' => 'internal_info',
919 'content' => $internalNote
920 ]);
921 }
922 }
923
924 $updateData = [];
925
926 if ($propName == 'product_id') {
927 $ticket->load('product');
928 $updateData['product'] = $ticket->product;
929 } else if ($propName == 'agent_id') {
930 $previousAgentId = (int) $prevValue;
931 $ticket->load('agent');
932 $updateData['agent'] = $ticket->agent;
933 $updateData['assigner'] = (new TicketService())->onAgentChange($ticket, $assigner);
934 if ($prevValue != $ticket->{$propName}) {
935 do_action('fluent_support/agent_assigned_to_ticket', $ticket->agent, $ticket, $assigner, $previousAgentId);
936 }
937 }
938
939 $message = sprintf(
940 /* translators: %s: The name of the property that was updated */
941 __('%s has been updated', 'fluent-support'),
942 esc_html(str_replace('_', ' ', ucwords((string) $propName)))
943 );
944
945 return [
946 'message' => $message,
947 'update_data' => $updateData
948 ];
949 } catch (\Exception $e) {
950 return $this->sendError([
951 'message' => Helper::getSafeErrorMessage($e)
952 ]);
953 }
954 }
955
956 /**
957 * closeTicket method close the ticket by id
958 * @param Ticket $ticket
959 * @param int $ticket_id
960 * @return array
961 */
962 public function closeTicket(Request $request, $ticket_id)
963 {
964 try {
965 $agent = Helper::getAgentByUserId();
966 $ticket = Ticket::findOrFail($ticket_id);
967
968 $this->ensureCanAccessTicket($ticket);
969
970 $closeSilently = $request->getSafe('close_ticket_silently', 'sanitize_text_field');
971
972 return [
973 'message' => __('Ticket has been closed', 'fluent-support'),
974 'ticket' => (new TicketService())->close($ticket, $agent, '', $closeSilently)
975 ];
976 } catch (\Exception $e) {
977 return $this->sendError([
978 'message' => Helper::getSafeErrorMessage($e)
979 ]);
980 }
981 }
982
983 /**
984 * reOpenTicket method will reopen a closed ticket
985 * @param Request $request
986 * @param $ticket_id
987 * @return array
988 */
989 public function reOpenTicket($ticket_id)
990 {
991 try {
992 $agent = Helper::getAgentByUserId();
993 $ticket = Ticket::findOrFail($ticket_id);
994
995 $this->ensureCanAccessTicket($ticket);
996
997 return [
998 'message' => __('Ticket has been opened again', 'fluent-support'),
999 'ticket' => (new TicketService())->reopen($ticket, $agent)
1000 ];
1001 } catch (\Exception $e) {
1002 return $this->sendError([
1003 'message' => Helper::getSafeErrorMessage($e)
1004 ]);
1005 }
1006 }
1007
1008 /**
1009 * doBulkActions method is responsible for bulk action
1010 * This function will get ticket ids and action as parameter and perform action based on the selection
1011 * @param Request $request
1012 * @param Ticket $ticket
1013 * @return array|string[]|void
1014 * @throws \Exception
1015 */
1016 public function doBulkActions(Request $request)
1017 {
1018 try {
1019 $action = $request->getSafe('bulk_action', 'sanitize_text_field');
1020 $ticketIds = array_map('intval', $request->get('ticket_ids', null, []));
1021
1022 $hasAllPermission = PermissionManager::currentUserCan('fst_manage_other_tickets');
1023 $agent = Helper::getAgentByUserId();
1024 $query = Ticket::whereIn('id', $ticketIds);
1025
1026 //If agent do not have permission to manage other tickets
1027 if (!$hasAllPermission) {
1028 $query->where('agent_id', $agent->id);
1029 }
1030
1031 //If bulk action is close tickets
1032 if ($action == 'close_tickets') {
1033 $tickets = $query->get();
1034 $tickets->each(function ($ticket) use ($agent) {
1035 (new TicketService())->close($ticket, $agent);
1036 });
1037
1038 return [
1039 'message' => sprintf(
1040 /* translators: %d represents the number of closed tickets. */
1041 __('%d tickets have been closed.', 'fluent-support'),
1042 count($tickets)
1043 )
1044 ];
1045 } else if ($action == 'delete_tickets') {
1046 $tickets = $query->get();
1047 $ticketService = new TicketService();
1048
1049 foreach ($tickets as $ticket) {
1050 $ticketService->deleteTicket($ticket, $agent);
1051 }
1052
1053 return [
1054 'message' => sprintf(
1055 /* translators: %d is the number of tickets that were deleted */
1056 __('%d tickets have been deleted', 'fluent-support'),
1057 count($tickets)
1058 )
1059 ];
1060 } else if ($action == 'assign_agent') {
1061 if (!$request->has('agent_id')) {
1062 throw new \Exception(esc_html__('agent_id param is required', 'fluent-support'));
1063 }
1064
1065 $assignAgent = Agent::findOrFail($request->getSafe('agent_id', 'intval'));
1066
1067 $query->where(function ($q) use ($assignAgent) {
1068 $q->where('agent_id', '!=', $assignAgent->id)
1069 ->orWhereNull('agent_id');
1070 });
1071
1072 $tickets = $query->get();
1073 $assignedCount = 0;
1074 $skippedCount = 0;
1075
1076 $tickets->each(function ($ticket) use ($assignAgent, $agent, &$assignedCount, &$skippedCount) {
1077 $previousAgentId = (int) $ticket->agent_id;
1078 $restrictions = $assignAgent->getMeta('agent_restrictions', []);
1079
1080 //Skip ticket if mailbox is restricted for the agent
1081 if (!empty($restrictions) && in_array($ticket->mailbox_id, $restrictions['restrictedBusinessBoxes'])) {
1082 $skippedCount++;
1083 return;
1084 }
1085
1086 $ticket->agent_id = $assignAgent->id;
1087 $ticket->save();
1088 $assignedCount++;
1089
1090 do_action('fluent_support/agent_assigned_to_ticket', $assignAgent, $ticket, $agent, $previousAgentId);
1091 });
1092
1093 $assignedMessage = sprintf(
1094 /* translators: %1$d is the number of tickets assigned, %2$s is the agent's name. */
1095 __('%1$d tickets have been assigned to %2$s.', 'fluent-support'),
1096 $assignedCount,
1097 $assignAgent->full_name
1098 );
1099
1100 $skippedMessage = $skippedCount > 0
1101 ? sprintf(
1102 /* translators: %1$d is the number of skipped tickets due to mailbox restrictions. */
1103 __('%1$d tickets were skipped due to mailbox restrictions or already being assigned.', 'fluent-support'),
1104 $skippedCount
1105 )
1106 : '';
1107
1108 return [
1109 'message' => trim($assignedMessage . ' ' . $skippedMessage)
1110 ];
1111 } else if ($action == 'assign_agent_group') {
1112 if (!$request->has('agent_group_id')) {
1113 throw new \Exception(esc_html__('agent_group_id param is required', 'fluent-support'));
1114 }
1115
1116 $groupId = $request->getSafe('agent_group_id', 'intval');
1117 $group = AgentGroup::findOrFail($groupId);
1118
1119 if ($group->agents()->count() === 0) {
1120 throw new \Exception(esc_html__('No agents found in this group', 'fluent-support'));
1121 }
1122
1123 $tickets = $query->get();
1124 $assignedCount = 0;
1125 $skippedCount = 0;
1126 $currentCounts = [];
1127
1128 foreach ($tickets as $ticket) {
1129 $previousAgentId = (int) $ticket->agent_id;
1130 $selectedAgent = $group->getLeastLoadedAgent(
1131 $ticket->mailbox_id, $currentCounts
1132 );
1133
1134 if (!$selectedAgent) {
1135 $skippedCount++;
1136 continue;
1137 }
1138
1139 $ticket->agent_id = $selectedAgent->id;
1140 $ticket->save();
1141 $assignedCount++;
1142 $currentCounts[$selectedAgent->id]++;
1143
1144 as_enqueue_async_action('fluent_support/async_agent_assigned_to_ticket', [
1145 $selectedAgent->id, $ticket->id, $agent->id, $previousAgentId
1146 ], 'fluent-support');
1147 }
1148
1149 return [
1150 'message' => sprintf(
1151 /* translators: %1$d is tickets assigned, %2$d is tickets skipped. */
1152 __('%1$d tickets assigned via agent group. %2$d skipped.', 'fluent-support'),
1153 $assignedCount,
1154 $skippedCount
1155 )
1156 ];
1157 } else if ($action == 'assign_tags') {
1158 $tagIds = $request->get('tag_ids', null);
1159 if (!is_array($tagIds)) {
1160 $tagIds = [];
1161 }
1162 $tags = array_filter(array_map('absint', $tagIds));
1163
1164 $query->get()->each(function ($ticket) use ($tags) {
1165 $ticket->applyTags($tags);
1166 });
1167
1168 return [
1169 'message' => __('Selected tags has been added to tickets', 'fluent-support')
1170 ];
1171 }
1172
1173 throw new \Exception(esc_html__('Sorry no action found as available', 'fluent-support'));
1174 } catch (\Exception $e) {
1175 return $this->sendError([
1176 'message' => Helper::getSafeErrorMessage($e)
1177 ]);
1178 }
1179 }
1180
1181 /**
1182 * deleteTicket method will delete a ticket
1183 * @param int $ticket_id
1184 * @return array
1185 */
1186 public function deleteTicket($ticket_id)
1187 {
1188 try {
1189 $ticket = Ticket::findOrFail($ticket_id);
1190
1191 (new TicketService())->deleteTicket($ticket);
1192
1193 return [
1194 'message' => __('Ticket has been deleted successfully', 'fluent-support')
1195 ];
1196 } catch (\Exception $e) {
1197 return $this->sendError([
1198 'message' => Helper::getSafeErrorMessage($e)
1199 ]);
1200 }
1201 }
1202
1203 /**
1204 * doBulkReplies method will create response for bulk tickets
1205 * This function will get ticket ids, content, attachment etc and create response for tickets
1206 * @param Request $request
1207 * @param Conversation $conversation
1208 * @return array
1209 * @throws \Exception
1210 */
1211 public function doBulkReplies(Request $request)
1212 {
1213 try {
1214 // Sanitize all request data before validation
1215 $requestData = $request->all();
1216 $data = [];
1217 foreach ($requestData as $key => $value) {
1218 if (is_array($value)) {
1219 if ($key === 'ticket_ids') {
1220 $data[$key] = array_map('intval', $value);
1221 } elseif ($key === 'content') {
1222 $data[$key] = wp_kses_post($value);
1223 } else {
1224 $data[$key] = map_deep($value, 'sanitize_text_field');
1225 }
1226 } else {
1227 $data[$key] = sanitize_text_field($value);
1228 }
1229 }
1230
1231 $this->validate($data, [
1232 'content' => 'required',
1233 'ticket_ids' => 'required|array'
1234 ]);
1235
1236 //Get logged in agent information
1237 $agent = Helper::getAgentByUserId();
1238 $ticketIds = array_filter($data['ticket_ids'], 'absint');
1239
1240 $hasAllPermission = PermissionManager::currentUserCan('fst_manage_other_tickets');
1241 $query = Ticket::whereIn('id', $ticketIds)->where('status', '!=', 'closed');
1242
1243 //If the agent does not have permission
1244 if (!$hasAllPermission) {
1245 $query->where('agent_id', $agent->id);
1246 }
1247
1248 $tickets = $query->get();
1249
1250 if ($tickets->isEmpty()) {
1251 throw new \Exception(esc_html__('Sorry no tickets found based on your filter and bulk actions', 'fluent-support'));
1252 }
1253
1254 $responseData = [
1255 'content' => wp_kses_post(Arr::get($data, 'content', '')),
1256 'conversation_type' => 'response',
1257 'close_ticket' => Arr::get($data, 'close_ticket'),
1258 ];
1259
1260 //If request with file attachments
1261 $attachmentHashes = Arr::get($data, 'attachments', []);
1262 $attachments = false;
1263 if ($attachmentHashes) {
1264 $attachments = Attachment::whereNull('ticket_id')
1265 ->orderBy('id', 'asc')
1266 ->whereIn('file_hash', $attachmentHashes)
1267 ->get();
1268 }
1269
1270 $responseService = new ResponseService();
1271
1272 foreach ($tickets as $ticket) {
1273 if ($attachments) {
1274 $responseData['attachments'] = [];
1275 $attachmentRecords = [];
1276 foreach ($attachments as $attachment) {
1277 $fileHash = bin2hex(random_bytes(16));
1278 $attachmentRecords[] = [
1279 'ticket_id' => $ticket->id,
1280 'file_path' => $attachment->file_path,
1281 'full_url' => $attachment->full_url,
1282 'title' => $attachment->title,
1283 'driver' => $attachment->driver,
1284 'file_size' => $attachment->file_size,
1285 'status' => $attachment->status,
1286 'file_hash' => $fileHash,
1287 ];
1288 $responseData['attachments'][] = $fileHash;
1289 }
1290 if ($attachmentRecords) {
1291 Attachment::insert($attachmentRecords);
1292 }
1293 }
1294
1295 $responseService->createResponse($responseData, $agent, $ticket);
1296 }
1297
1298 return [
1299 'message' => __('Response has been added to the selected tickets', 'fluent-support')
1300 ];
1301 } catch (\Exception $e) {
1302 return $this->sendError([
1303 'message' => Helper::getSafeErrorMessage($e)
1304 ]);
1305 }
1306 }
1307
1308 /**
1309 * deleteResponse method will remove a response from ticket by ticket id and response id
1310 * @param Request $request
1311 * @param Conversation $conversation
1312 * @param $ticket_id
1313 * @param $response_id
1314 * @return array
1315 */
1316 public function deleteResponse($ticket_id, $response_id)
1317 {
1318 try {
1319 $ticket = Ticket::findOrFail($ticket_id);
1320 $response = Conversation::where('id', $response_id)
1321 ->where('ticket_id', $ticket_id)
1322 ->firstOrFail();
1323 $agent = Helper::getAgentByUserId();
1324
1325 if (!PermissionManager::currentUserCan('fst_delete_tickets') && $ticket->agent_id !== $agent->id) {
1326 throw new \Exception(
1327 esc_html__('Sorry, you do not have permission to delete this response.', 'fluent-support')
1328 );
1329 }
1330
1331 $response->delete();
1332 $response->ccinfo()->delete();
1333
1334 return [
1335 'message' => __('Selected response has been deleted', 'fluent-support')
1336 ];
1337 } catch (\Exception $e) {
1338 return $this->sendError([
1339 'message' => Helper::getSafeErrorMessage($e)
1340 ]);
1341 }
1342 }
1343
1344 /**
1345 * updateResponse method will update ticket response using ticket and response id
1346 * @param Request $request
1347 * @param int $ticket_id
1348 * @param int $response_id
1349 * @return array
1350 * @throws \Exception
1351 */
1352 public function updateResponse(TicketResponseRequest $request, $ticket_id, $response_id)
1353 {
1354 try {
1355 $ticket = Ticket::findOrFail($ticket_id);
1356 $response = Conversation::where('id', $response_id)
1357 ->where('ticket_id', $ticket_id)
1358 ->firstOrFail();
1359 $agent = Helper::getAgentByUserId();
1360
1361 if (!PermissionManager::currentUserCan('fst_manage_other_tickets') && $ticket->agent_id !== $agent->id) {
1362 throw new \Exception(
1363 esc_html__('Sorry, you do not have permission to update this response.', 'fluent-support')
1364 );
1365 }
1366
1367 $content = wp_unslash(wp_kses_post($request->getSafe('content', 'wp_kses_post')));
1368 $response->content = $content;
1369
1370 if ($response->conversation_type == 'draft_response' && $response->person_id != $agent->id && PermissionManager::currentUserCan('fst_approve_draft_reply')) {
1371 $response = $this->approveDraftConversation($ticket, $response, $agent, $content);
1372 } else if ($response->conversation_type == 'draft_response' && $response->person_id != $agent->id) {
1373 if (!PermissionManager::currentUserCan('fst_approve_draft_reply')) {
1374 throw new \Exception(
1375 esc_html__('Sorry, You do not have permission to approve this draft response', 'fluent-support')
1376 );
1377 }
1378 } else {
1379 $response->save();
1380 }
1381
1382 return [
1383 'message' => __('Selected response has been updated', 'fluent-support'),
1384 'response' => $response
1385 ];
1386 } catch (\Exception $e) {
1387 return $this->sendError([
1388 'message' => Helper::getSafeErrorMessage($e)
1389 ]);
1390 }
1391 }
1392
1393 public function approveDraftResponse(TicketResponseRequest $request, $ticket_id, $response_id)
1394 {
1395 try {
1396 if (!PermissionManager::currentUserCan('fst_approve_draft_reply')) {
1397 throw new \Exception(
1398 esc_html__('You do not have permission to approve draft responses.', 'fluent-support')
1399 );
1400 }
1401
1402 $ticket = Ticket::findOrFail($ticket_id);
1403
1404 $response = Conversation::where('id', $response_id)
1405 ->where('ticket_id', $ticket_id)
1406 ->where('conversation_type', 'draft_response')
1407 ->firstOrFail();
1408
1409 $person = Helper::getAgentByUserId();
1410
1411 $response = $this->approveDraftConversation(
1412 $ticket,
1413 $response,
1414 $person,
1415 wp_unslash(wp_kses_post($request->getSafe('content', 'wp_kses_post')))
1416 );
1417
1418 return [
1419 'message' => __('Draft response has been successfully approved.', 'fluent-support'),
1420 'response' => $response,
1421 ];
1422 } catch (\Exception $e) {
1423 return $this->sendError([
1424 'message' => Helper::getSafeErrorMessage($e)
1425 ]);
1426 }
1427 }
1428
1429 protected function approveDraftConversation($ticket, $response, $person, $content)
1430 {
1431 $resetWaitingSince = apply_filters('fluent_support/reset_waiting_since', true, $content);
1432
1433 $response->content = $content;
1434 $response->conversation_type = 'response';
1435 $response->created_at = current_time('mysql');
1436 $response->save();
1437
1438 if ($person->person_type == 'agent' && $ticket->status == 'new') {
1439 $ticket->status = 'active';
1440 if ($ticket->created_at) {
1441 $ticket->first_response_time = strtotime(current_time('mysql')) - strtotime($ticket->created_at);
1442 } else {
1443 $ticket->first_response_time = 300;
1444 }
1445 }
1446
1447 if ($resetWaitingSince) {
1448 $ticket->last_agent_response = current_time('mysql');
1449 $ticket->waiting_since = current_time('mysql');
1450 }
1451
1452 $ticket->response_count += 1;
1453 $ticket->save();
1454
1455 do_action('fluent_support/response_added_by_' . $person->person_type, $response, $ticket, $person);
1456
1457 return $response;
1458 }
1459
1460 /**
1461 * getLiveActivity method will return the activity in a ticket by agents
1462 * @param Request $request
1463 * @param $ticket_id
1464 * @return array
1465 */
1466 public function getLiveActivity(Request $request, $ticket_id)
1467 {
1468 $agent = Helper::getAgentByUserId();
1469
1470 return [
1471 'live_activity' => TicketHelper::getActivity($ticket_id, $agent->id)
1472 ];
1473 }
1474
1475 /**
1476 * removeLiveActivity method will remove activities that
1477 * @param Request $request
1478 * @param $ticket_id
1479 * @return array
1480 */
1481 public function removeLiveActivity(Request $request, $ticket_id)
1482 {
1483 $agent = Helper::getAgentByUserId();
1484
1485 return [
1486 'result' => TicketHelper::removeFromActivities($ticket_id, $agent->id),
1487 'agent_id' => $agent->id
1488 ];
1489 }
1490
1491 /**
1492 * addTag method will add tag in ticket by ticket id
1493 * @param Request $request
1494 * @param $ticket_id
1495 * @return array
1496 */
1497 public function addTag(Request $request, $ticket_id)
1498 {
1499 try {
1500 $ticket = Ticket::findOrFail($ticket_id);
1501 $ticket->applyTags($request->getSafe('tag_id', 'intval'));
1502
1503 return [
1504 'message' => __('Tag has been added to this ticket', 'fluent-support'),
1505 'tags' => $ticket->tags
1506 ];
1507 } catch (\Exception $e) {
1508 return $this->sendError([
1509 'message' => Helper::getSafeErrorMessage($e)
1510 ]);
1511 }
1512 }
1513
1514 /**
1515 * detachTag method will remove all tags from tickets
1516 * @param $ticket_id
1517 * @param $tag_id
1518 * @return array
1519 */
1520 public function detachTag($ticket_id, $tag_id)
1521 {
1522 try {
1523 $ticket = Ticket::findOrFail($ticket_id);
1524 $ticket->detachTags($tag_id);
1525
1526 return [
1527 'message' => __('Tag has been removed from this ticket', 'fluent-support'),
1528 'tags' => $ticket->tags
1529 ];
1530 } catch (\Exception $e) {
1531 return $this->sendError([
1532 'message' => Helper::getSafeErrorMessage($e)
1533 ]);
1534 }
1535 }
1536
1537 /**
1538 * changeTicketCustomer method will update customer in a ticket
1539 * This method will get ticket id and customer id as parameter, it will replace existing customer id with new
1540 * @param Request $request
1541 * @return array
1542 */
1543 public function changeTicketCustomer(Request $request, $ticket_id)
1544 {
1545 $ticketId = (int) $ticket_id;
1546 $newCustomerId = $request->getSafe('customer', 'intval');
1547
1548 if (!$newCustomerId) {
1549 return $this->sendError(__('Invalid customer selected.', 'fluent-support'));
1550 }
1551
1552 try {
1553 $updated = Ticket::where('id', $ticketId)
1554 ->where('customer_id', '!=', $newCustomerId)
1555 ->update(['customer_id' => $newCustomerId]);
1556
1557 return $updated
1558 ? ['message' => __('Customer has been updated', 'fluent-support')]
1559 : $this->sendError(__('Ticket not found or customer already assigned.', 'fluent-support'));
1560
1561 } catch (\Exception $e) {
1562 return $this->sendError([
1563 'message' => Helper::getSafeErrorMessage($e)
1564 ]);
1565 }
1566 }
1567
1568 /**
1569 * getTicketCustomData method will return the custom data by ticket id
1570 * @param Request $request
1571 * @param $ticket_id
1572 * @return array|array[]
1573 */
1574 public function getTicketCustomData(Request $request, $ticket_id)
1575 {
1576 if (!defined('FLUENTSUPPORTPRO')) {
1577 return [
1578 'custom_data' => [],
1579 'rendered_fields' => []
1580 ];
1581 }
1582
1583 $ticket = Ticket::findOrFail($ticket_id);
1584
1585 return [
1586 'custom_data' => (object)$ticket->customData(),
1587 'rendered_fields' => \FluentSupportPro\App\Services\CustomFieldsService::getRenderedPublicFields($ticket->customer, 'admin')
1588 ];
1589 }
1590
1591 /**
1592 * syncFluentCrmTags method will synchronize the tags with Fluent CRM by contact id
1593 *This function will get contact id and tags as parameter, get existing tags from crm and updated added/removed tags
1594 * @param Request $request
1595 * @param FluentCRMServices $fluentCRMServices
1596 * @return array
1597 */
1598 public function syncFluentCrmTags(Request $request, FluentCRMServices $fluentCRMServices)
1599 {
1600 $data = [
1601 'contact_id' => $request->getSafe('contact_id', 'intval'),
1602 'tags' => $request->get('tags', null)
1603 ];
1604
1605 // Sanitize tags array if it's an array
1606 if (is_array($data['tags'])) {
1607 $data['tags'] = array_map('intval', $data['tags']);
1608 }
1609
1610 try {
1611 return $fluentCRMServices->syncCrmTags($data);
1612 } catch (\Exception $e) {
1613 return $this->sendError([
1614 'message' => Helper::getSafeErrorMessage($e)
1615 ]);
1616 }
1617 }
1618
1619 /**
1620 * This `syncFluentCrmLists` method will synchronize the lists with Fluent CRM by contact id
1621 * This method will get contact id and lists as parameter, get existing lists from crm and updated added/removed lists
1622 * @param Request $request
1623 * @param FluentCRMServices $fluentCRMServices
1624 * @return array
1625 */
1626
1627 public function syncFluentCrmLists(Request $request, FluentCRMServices $fluentCRMServices)
1628 {
1629 $data = [
1630 'contact_id' => $request->getSafe('contact_id', 'intval'),
1631 'lists' => $request->get('lists', null, [])
1632 ];
1633
1634 // Sanitize lists array if it's an array
1635 if (is_array($data['lists'])) {
1636 $data['lists'] = array_map('intval', $data['lists']);
1637 }
1638
1639 try {
1640 return $fluentCRMServices->syncCrmLists($data);
1641 } catch (\Exception $e) {
1642 return $this->sendError([
1643 'message' => Helper::getSafeErrorMessage($e)
1644 ]);
1645 }
1646 }
1647
1648 /**
1649 * Get ticket essentials data based on the provided types.
1650 *
1651 * @param \Illuminate\Http\Request $request
1652 * @return array The ticket essentials data.
1653 */
1654 public function getTicketEssentials(Request $request)
1655 {
1656 $type = $request->getSafe('type', 'sanitize_text_field');
1657
1658 return TicketHelper::getTicketEssentials($type);
1659 }
1660
1661 public function fetchLabelSearch()
1662 {
1663 try {
1664 $agent_id = get_current_user_id();
1665 return TicketHelper::getLabelSearch($agent_id);
1666 } catch (\Exception $e) {
1667 return $this->sendError([
1668 'message' => Helper::getSafeErrorMessage($e)
1669 ]);
1670 }
1671 }
1672
1673 public function storeOrUpdateLabelSearch(Request $request)
1674 {
1675 try {
1676 $agent_id = get_current_user_id();
1677 $searchData = $request->get('query', null, []);
1678 if (is_array($searchData)) {
1679 $searchData = map_deep($searchData, 'sanitize_text_field');
1680 }
1681 $filterType = Arr::get($searchData, 'filter_type', '');
1682 if ($filterType == 'advanced') {
1683 return TicketHelper::saveSearchLabel($agent_id, $searchData, $filterType);
1684 }
1685
1686 return [
1687 'message' => __('Invalid filter type.', 'fluent-support'),
1688 ];
1689
1690 } catch (\Exception $e) {
1691 return $this->sendError([
1692 'message' => Helper::getSafeErrorMessage($e)
1693 ]);
1694 }
1695 }
1696
1697 public function deleteLabelSearch(Request $request, $search_id)
1698 {
1699 try {
1700 $agent_id = get_current_user_id();
1701 return TicketHelper::deleteSavedSearch($search_id);
1702 } catch (\Exception $e) {
1703 return $this->sendError([
1704 'message' => Helper::getSafeErrorMessage($e)
1705 ]);
1706 }
1707 }
1708 }
1709