PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.1.2
Fluent Support – Helpdesk & Customer Support Ticket System v2.1.2
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 / Models / Ticket.php

Ticket.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.1.2, at app/Models/Ticket.php

1,106 lines 35.1 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\Models;
4
5 use Exception;
6 use FluentSupport\App\Modules\PermissionManager;
7 use FluentSupport\App\Services\Helper;
8 use FluentSupport\App\Services\TicketHelper;
9 use FluentSupport\App\Services\TicketQueryService;
10 use FluentSupport\App\Services\Tickets\TicketService;
11 use FluentSupport\Framework\Support\Arr;
12
13 class Ticket extends Model
14 {
15 protected $table = 'fs_tickets';
16
17 protected $dates = ['waiting_since'];
18
19 /**
20 * The attributes that are mass assignable.
21 *
22 * @var array
23 */
24 protected $fillable = [
25 'customer_id',
26 'agent_id',
27 'product_id',
28 'mailbox_id',
29 'product_source',
30 'privacy',
31 'priority',
32 'client_priority',
33 'status',
34 'title',
35 'slug',
36 'hash',
37 'source',
38 'message_id',
39 'content',
40 'last_agent_response',
41 'last_customer_response',
42 'waiting_since',
43 'response_count',
44 'first_response_time',
45 'total_close_time',
46 'resolved_at',
47 'closed_by',
48 'created_by'
49 ];
50
51 public static function boot()
52 {
53 parent::boot();
54
55 static::creating(function ($model) {
56 if (empty($model->slug)) {
57 $model->slug = static::slugify($model->title);
58 }
59
60 $model->hash = bin2hex(random_bytes(16));
61 $model->content_hash = md5($model->content);
62
63 $model->last_customer_response = current_time('mysql');
64 $model->created_at = current_time('mysql');
65 $model->updated_at = current_time('mysql');
66 $model->waiting_since = current_time('mysql');
67
68 });
69
70 static::deleting(function ($model) {
71 //Delete the ticket meta
72 Meta::where('object_type', 'ticket_meta')->where('object_id', $model->id)->delete();
73 //Delete all cc info for the ticket
74 Meta::where('object_type', 'ticket')->where('object_id', $model->id)->delete();
75 //Delete draft info
76 Meta::where('object_type', '_fs_auto_draft')->where('object_id', $model->id)->delete();
77 //delete the responses first (their attachments are cleaned up by Conversation::deleting)
78 Conversation::deleteAll($model->id);
79 // Delete ticket-level attachments (conversation_id IS NULL) and remove the ticket upload directory
80 $class = __NAMESPACE__ . '\Attachment';
81 $ticketAttachments = $class::where('ticket_id', $model->id)->whereNull('conversation_id')->get();
82 $class::purgeAttachments($ticketAttachments, $model->id);
83 $class::where('ticket_id', $model->id)->whereNull('conversation_id')->delete();
84 });
85 }
86
87 /**
88 * $searchable Columns in table to search
89 * @var array
90 */
91 protected $searchable = [
92 'content',
93 'title',
94 'slug',
95 'id'
96 ];
97
98 /**
99 * Local scope to filter tickets by search/query string
100 * @param ModelQueryBuilder $query
101 * @param string $search
102 * @return ModelQueryBuilder
103 */
104 public function scopeSearchBy($query, $search)
105 {
106
107 if(!$search) {
108 return $query;
109 }
110
111 if (strpos($search, ':')) {
112 $array = explode(':', (string) $search);
113 $column = $array[0];
114 $value = $array[1];
115 $columns = $this->fillable;
116 $columns[] = 'id';
117
118 if (in_array($column, $columns) && $value) {
119 if (is_numeric($value)) {
120 $query->where($column, $value);
121 } else {
122 $query->where($column, 'LIKE', "%$value%");
123 }
124 return $query;
125 }
126 }
127
128 $fields = $this->searchable;
129 $query->where(function ($query) use ($fields, $search) {
130 $query->where(array_shift($fields), 'LIKE', "%$search%");
131 foreach ($fields as $field) {
132 $query->orWhere($field, 'LIKE', "%$search%");
133 }
134 });
135
136 return $query;
137 }
138
139 /**
140 * Local scope to filter tickets by different filtering condition
141 * @param ModelQueryBuilder $query
142 * @param mixed $search
143 * @return ModelQueryBuilder
144 */
145
146 public function doSearchForAdvancedFilter($query, $search)
147 {
148 foreach ($search as $s) {
149 $operator = $s['operator'];
150 //If selected item for ticket either title or content
151 if (in_array($s['property'], ['title', 'content'])) {
152 //If the selected condition is contains, query operator id LIKE
153 if ($operator == 'contains') {
154 $query = $query->where(function ($query) use ($s) {
155 $query->where($s['property'], 'LIKE', "%" . $s['value'] . "%");
156 });
157 } elseif ($operator == 'not_contains') {
158 //If the selected condition is not_contains, query operator id NOT LIKE
159 $query = $query->where(function ($query) use ($s) {
160 $query->where($s['property'], 'NOT LIKE', '%' . $s['value'] . '%');
161 });
162 }
163 }
164
165 //If selected item is Ticket Conversation Content
166 if ($s['property'] == 'conversation_content') {
167 $operator = $s['operator'];
168 if ($operator == 'contains') {
169 $query = $query->whereHas('responses', function ($q) use ($s) {
170 $q->where('content', 'LIKE', "%" . $s['value'] . "%");
171 });
172
173 } else if ($operator == 'not_contains') {
174 $query = $query->whereHas('responses', function ($q) use ($s) {
175 $q->where('content', 'NOT LIKE', "%" . $s['value'] . "%");
176 });
177 }
178 }
179
180 //If selected item is Ticket created or Last Response or Customer Waiting For, or Last Agent Response or Last Customer Response
181 if (in_array($s['property'], ['created_at', 'updated_at', 'waiting_since', 'last_agent_response', 'last_customer_response'])) {
182 $query = (new \FluentSupport\App\Models\Ticket())->buildDateBaseFilterQuery($query, $s);
183 }
184
185 //If selected item is Ticket Status or Client Priority or Agent Priority or Tags or Product or Waiting For Reply
186 if (in_array($s['property'], ['status', 'client_priority', 'priority', 'tags', 'product', 'waiting_for_reply', 'agent_id', 'mailbox_id'])) {
187 $query = (new \FluentSupport\App\Models\Ticket())->buildPropertiesFilterQuery($query, $s);
188 }
189 }
190 return $query;
191 }
192
193 /**
194 * Local scope to filter subscribers by search/query string
195 * @param ModelQueryBuilder $query
196 * @param array $statuses
197 * @return ModelQueryBuilder
198 */
199 public function scopeFilterByStatues($query, $statuses)
200 {
201 if ($statuses) {
202 $query->whereIn('status', $statuses);
203 }
204
205 return $query;
206 }
207
208 /**
209 * Local scope to filter tickets by not response by agent
210 * @param $query
211 * @return mixed
212 */
213 public function scopeWaitingOnly($query)
214 {
215 $query->where(function ($q) {
216 $q->whereColumn('last_agent_response', '<', 'last_customer_response')
217 ->orWhereNull('last_agent_response')
218 ->orWhere('status', 'new');
219 });
220 return $query;
221 }
222
223 /**
224 * scopeApplyFilters method will filet ticket based on the selected filters
225 * This method will get filter option as parameter, loop through and apply conditions in query
226 * @param $query
227 * @param $filters
228 * @return ModelQueryBuilder
229 */
230 public function scopeApplyFilters($query, $filters)
231 {
232 $supportedColumns = ['product_id', 'client_priority', 'priority', 'mailbox_id'];
233 foreach ($filters as $filterKey => $filterValue) {
234 if (!$filterValue && ($filterValue !== '0' && $filterValue !== 0)) {
235 continue;
236 }
237 //If filer using status
238 if ($filterKey == 'status_type') {
239 //Get list of ticket status
240 $statusArray = Helper::getTkStatusesByGroupName($filterValue);
241 if ($statusArray) {
242 //Apply filet where status in
243 $query->whereIn('status', $statusArray);
244 }
245 } else if (in_array($filterKey, $supportedColumns)) {
246 // Use whereIn for all supported columns (they all now support multi-select)
247 if (is_array($filterValue)) {
248 $query->whereIn($filterKey, $filterValue);
249 } else {
250 $query->where($filterKey, $filterValue);
251 }
252 } else if ($filterKey == 'waiting_for_reply') {
253 if ($filterValue != 'yes') {
254 continue;
255 }
256 //Apply filter where no response by agent
257 $query = $this->scopeWaitingOnly($query);
258 } else if ($filterKey == 'agent_id') {
259 // Handle array of agent IDs for multi-select
260 if (is_array($filterValue)) {
261 // Check if 'unassigned' is in the array
262 $hasUnassigned = in_array('unassigned', $filterValue);
263 $agentIds = array_filter($filterValue, function($v) {
264 return $v !== 'unassigned';
265 });
266
267 if ($hasUnassigned && !empty($agentIds)) {
268 // Include both unassigned and specific agents
269 $query->where(function($q) use ($agentIds) {
270 $q->whereNull('agent_id')
271 ->orWhereIn('agent_id', $agentIds);
272 });
273 } elseif ($hasUnassigned) {
274 // Only unassigned
275 $query->whereNull('agent_id');
276 } elseif (!empty($agentIds)) {
277 // Only specific agents
278 if (defined('FLUENTSUPPORTPRO')) {
279 if (isset($filters['watcher']) && $filters['watcher'] == 'watcher') {
280 $watcherTickets = [];
281 foreach ($agentIds as $agentId) {
282 $watcherTickets = array_merge($watcherTickets, TicketHelper::getWatcherTicketIds($agentId));
283 }
284 $query->whereIn('id', array_unique($watcherTickets));
285 } else {
286 $query->whereIn('agent_id', $agentIds);
287 }
288 } else {
289 $query->whereIn('agent_id', $agentIds);
290 }
291 }
292 } else {
293 // Single value (backward compatibility)
294 if ($filterValue == 'unassigned') {
295 $query->whereNull($filterKey);
296 } else {
297 if (defined('FLUENTSUPPORTPRO')) {
298 if (isset($filters['watcher']) && $filters['watcher'] == 'watcher') {
299 $watcherTickets = TicketHelper::getWatcherTicketIds($filterValue);
300 $query->whereIn('id', $watcherTickets);
301 } else {
302 //Apply filter, get only assigned ticket
303 $query->where($filterKey, $filterValue);
304 }
305 } else {
306 $query->where($filterKey, $filterValue);
307 }
308 }
309 }
310 } else if ($filterKey == 'agent_group') {
311 $groupIds = is_array($filterValue) ? $filterValue : [$filterValue];
312 $groupIds = array_filter(array_map('intval', $groupIds));
313 if (!empty($groupIds)) {
314 $agentIds = TagPivot::where('source_type', 'agent_group')
315 ->whereIn('tag_id', $groupIds)
316 ->pluck('source_id')
317 ->toArray();
318 if ($agentIds) {
319 $query->whereIn('agent_id', $agentIds);
320 } else {
321 $query->whereRaw('1 = 0');
322 }
323 }
324 } else if ($filterKey == 'ticket_tags') {
325 if (!$filterValue) {
326 continue;
327 }
328 //Apply filter where ticket only has this tag id
329 $query->whereHas('tags', function ($q) use ($filterValue) {
330 $q->whereIn('tag_id', $filterValue);
331 });
332 }
333 }
334
335 return $query;
336 }
337
338 /**
339 * Local scope to filter tickets by agent id
340 * @param ModelQueryBuilder $query
341 * @param int $agentId
342 * @return ModelQueryBuilder
343 */
344 public function scopeFilterByAgentId($query, $agentId)
345 {
346 if ($agentId) {
347 $query->where('agent_id', $agentId);
348 }
349
350 return $query;
351 }
352
353 /**
354 * Local scope to filter subscribers by search/query string
355 * @param ModelQueryBuilder $query
356 * @param int $customerId
357 * @return ModelQueryBuilder
358 */
359 public function scopeFilterByCustomerId($query, $customerId)
360 {
361 $query->where('customer_id', $customerId);
362
363 return $query;
364 }
365
366 /**
367 * Local scope to filter subscribers by search/query string
368 * @param ModelQueryBuilder $query
369 * @param int $productId
370 * @return ModelQueryBuilder
371 */
372 public function scopeFilterByProductId($query, $productId)
373 {
374 if ($productId) {
375 $query->where('product_id', $productId);
376 }
377
378 return $query;
379 }
380
381 /**
382 * Local scope to filter subscribers by search/query string
383 * @param ModelQueryBuilder $query
384 * @param array $priorities
385 * @return ModelQueryBuilder
386 */
387 public function scopeFilterByPriorities($query, $priorities)
388 {
389 if ($priorities) {
390 $query->whereIn('priority', $priorities);
391 }
392
393 return $query;
394 }
395
396 /**
397 * @param $filter
398 * @return string[]
399 */
400
401 public static function parseRelationalFilterQueryMethods($filter)
402 {
403 // default operator = in
404 $method = 'whereHas';
405 $subMethod = 'whereIn';
406
407 switch ($filter['operator']) {
408 case 'not_in':
409 $method = 'whereDoesntHave';
410 $subMethod = 'whereIn';
411
412 break;
413 case 'in_all':
414 $method = 'whereHas';
415 $subMethod = 'where';
416
417 break;
418 case 'not_in_all':
419 $method = 'whereDoesntHave';
420 $subMethod = 'where';
421
422 break;
423 }
424
425 return [$method, $subMethod];
426 }
427
428 /**
429 * Parse filter to set proper operator and value for the filter query.
430 *
431 * @param array $filter
432 * @return array
433 */
434 public static function filterParser($filter)
435 {
436 switch ($filter['operator']) {
437 case 'before':
438 $filter['operator'] = '<';
439 $filter['value'] = $filter['value'] . ' 23:59:59';
440 break;
441
442 case 'after':
443 $filter['operator'] = '>';
444 $filter['value'] = $filter['value'] . ' 23:59:59';
445 break;
446
447 case 'date_equal':
448 $filter['operator'] = 'LIKE';
449 $filter['value'] = '%' . $filter['value'] . '%';
450 break;
451
452 case 'days_before':
453 $filter['operator'] = '<';
454 $filter['value'] = gmdate('Y-m-d', time() - $filter['value'] * 24 * 60 * 60);
455 break;
456
457 case 'days_within':
458 $filter['operator'] = 'BETWEEN';
459 $filter['value'] = [
460 gmdate('Y-m-d', time() - $filter['value'] * 24 * 60 * 60),
461 gmdate('Y-m-d') . ' 23:59:59'
462 ];
463 break;
464 case 'date_range':
465 $filter['operator'] = 'BETWEEN';
466 if (isset($filter['value'][0]))
467 $filter['value'][0] .= ' 00:00:00';
468 if (isset($filter['value'][1]))
469 $filter['value'][1] .= ' 23:59:59';
470 break;
471 }
472
473 return $filter;
474 }
475
476 /**
477 * @param \FluentSupport\Framework\Database\Orm\Builder|\FluentSupport\Framework\Database\Query\Builder $query
478 * @param array $filters
479 * @return ModelQueryBuilder
480 */
481 public function buildDateBaseFilterQuery($query, $filters)
482 {
483 $filter = static::filterParser($filters);
484 $query->where(function ($dateQuery) use ($filter) {
485
486 if ($filter['operator'] == 'BETWEEN') {
487 $dateQuery->whereBetween($filter['property'], $filter['value']);
488 } else {
489 $dateQuery->where($filter['property'], $filter['operator'], $filter['value']);
490 }
491 });
492
493 return $query;
494 }
495
496 /**
497 * Relation builder
498 * @param $relation
499 * @param $query
500 * @param $method
501 * @param $subMethod
502 * @param $subField
503 * @param $filter
504 * @param false $provider
505 * @return ModelQueryBuilder
506 */
507
508 public static function buildRelationFilterQuery($relation, $query, $method, $subMethod, $subField, $filter, $provider = false)
509 {
510 if (in_array($filter['operator'], ['in_all', 'not_in_all']) && $filter['value']) {
511 foreach ($filter['value'] as $item) {
512 $query = static::buildRelationFilterQuery($relation, $query, $method, $subMethod, $subField, ['value' => $item, 'operator' => ''], $provider);
513 }
514 } else {
515 $query = $query->{$method}($relation, function ($relationQuery) use ($subMethod, $subField, $filter, $provider) {
516 $relationQuery = $relationQuery->{$subMethod}($subField, $filter['value']);
517
518 if ($provider) {
519 $relationQuery = $relationQuery->where('provider', $provider);
520 }
521
522 return $relationQuery;
523 });
524 }
525
526 return $query;
527 }
528
529 /**
530 * get tickets by advanced filter segment data
531 * @param $query
532 * @param $filter
533 * @return ModelQueryBuilder
534 */
535
536 public function buildPropertiesFilterQuery($query, $filter)
537 {
538 if (in_array($filter['property'], ['tags', 'product'])) {
539 $subField = $filter['property'] == 'tags' ? 'tag_id' : 'product_id';
540 list($method, $subMethod) = static::parseRelationalFilterQueryMethods($filter);
541 $query = static::buildRelationFilterQuery($filter['property'], $query, $method, $subMethod, $subField, $filter);
542 } elseif ($filter['property'] == 'waiting_for_reply') {
543 if (($filter['value'] == 'yes' && $filter['operator'] == 'in') || ($filter['value'] == 'no' && $filter['operator'] == 'not_in')) {
544 $query = $query->where(function ($q) {
545 $q->whereColumn('last_agent_response', '<', 'last_customer_response')
546 ->orWhereNull('last_agent_response')
547 ->orWhere('status', 'new');
548 });
549 } else {
550 $query = $query->where(function ($q) {
551 $q->whereColumn('last_customer_response', '<', 'last_agent_response');
552 });
553 }
554 } else {
555 $method = $filter['operator'] == 'in' ? 'whereIn' : 'whereNotIn';
556 $query = $query->{$method}($filter['property'], (array)$filter['value']);
557 }
558 return $query;
559 }
560
561 /**
562 * method to search by properties
563 * @param $provider
564 * @param $query
565 * @param $search
566 * @param string $operator
567 * @return ModelQueryBuilder
568 */
569 public function buildSearchableQuery($provider, $query, $search, $operator = 'LIKE')
570 {
571 switch ($provider) {
572 case 'customer':
573 $fields = (new Customer())->getSearchableFields();
574 break;
575 case 'agent':
576 $fields = (new Agent())->getSearchableFields();
577 break;
578 default:
579 $fields = $this->searchable;
580 break;
581 }
582
583 $query->whereHas($provider, function ($query) use ($fields, $search, $operator) {
584 $query->where(array_shift($fields), $operator, $search);
585
586 $nameArray = explode(' ', (string) $search);
587
588 if (count($nameArray) >= 2) {
589 $query->orWhere(function ($q) use ($nameArray, $operator) {
590 $firstName = array_shift($nameArray);
591 $lastName = implode(' ', $nameArray);
592
593 $q->where('first_name', $operator, $firstName);
594 $q->where('last_name', $operator, $lastName);
595 });
596 }
597
598 foreach ($fields as $field) {
599 $query->orWhere($field, $operator, $search);
600 }
601 });
602
603 return $query;
604 }
605
606 /**
607 * Filter by ticket general properties like customer name, agent name etc
608 * @param $provider
609 * @param $query
610 * @param $filters
611 * @return ModelQueryBuilder
612 */
613 public function filterTicketByUser($provider, $query, $filters)
614 {
615 foreach ($filters as $filter) {
616 if ($filter['operator'] == 'in' || $filter['operator'] == 'not_in') {
617 $method = $filter['operator'] == 'in' ? 'whereIn' : 'whereNotIn';
618 $query = $query->whereHas($provider, function ($q) use ($method, $filter) {
619 $q->{$method}($filter['property'], $filter['value']);
620 });
621 }
622
623 if ($filter['operator'] == 'contains' || $filter['operator'] == 'not_contains') {
624 $operator = $filter['operator'] == 'contains' ? 'LIKE' : 'NOT LIKE';
625 $query->whereHas($provider, function ($q) use ($operator, $filter) {
626 $q->where($filter['property'], $operator, '%' . $filter['value'] . '%');
627 });
628 }
629
630 if ($filter['operator'] == '=' || $filter['operator'] == '!=') {
631 $operator = $filter['operator'];
632 $query->whereHas($provider, function ($q) use ($operator, $filter) {
633 $q->where($filter['property'], $operator, $filter['value']);
634 });
635 }
636 }
637 return $query;
638 }
639
640 /**
641 * One2Many: Customer has to many Click Tickets
642 * @return Model Collection
643 */
644 public function responses()
645 {
646 $class = __NAMESPACE__ . '\Conversation';
647
648 return $this->hasMany(
649 $class, 'ticket_id', 'id'
650 )->orderBy('created_at', 'desc')
651 ->orderBy('id', 'desc');
652 }
653
654 public function preview_response()
655 {
656 $class = __NAMESPACE__ . '\Conversation';
657
658 return $this->hasOne(
659 $class, 'ticket_id', 'id'
660 );
661 }
662
663 public function tags()
664 {
665 $class = __NAMESPACE__ . '\TicketTag';
666
667 return $this->belongsToMany(
668 $class, 'fs_tag_pivot', 'source_id', 'tag_id'
669 )->wherePivot('source_type', 'ticket_tag');
670 }
671
672 public function watchers()
673 {
674 $class = __NAMESPACE__ . '\TagPivot';
675
676 return $this->hasMany($class, 'source_id', 'id')
677 ->where('source_type', 'ticket_watcher')
678 ->select(['tag_id']);
679 }
680
681 /**
682 * One2one: Customer has to many Click Tickets
683 * @return Model Collection
684 */
685 public function customer()
686 {
687 $class = __NAMESPACE__ . '\Customer';
688
689 return $this->belongsTo(
690 $class, 'customer_id', 'id'
691 );
692 }
693
694 /**
695 * One2one: Customer has to many Click Tickets
696 * @return Model Collection
697 */
698 public function agent()
699 {
700 $class = __NAMESPACE__ . '\Agent';
701
702 return $this->belongsTo(
703 $class, 'agent_id', 'id'
704 );
705 }
706
707 public function closed_by_person()
708 {
709 $class = __NAMESPACE__ . '\Person';
710
711 return $this->belongsTo(
712 $class, 'closed_by', 'id'
713 );
714 }
715
716 public function created_by_person()
717 {
718 $class = __NAMESPACE__ . '\Agent';
719
720 return $this->belongsTo(
721 $class, 'created_by', 'id'
722 );
723 }
724
725 public function product()
726 {
727 $class = __NAMESPACE__ . '\Product';
728
729 return $this->belongsTo(
730 $class, 'product_id', 'id'
731 );
732 }
733
734 public function mailbox()
735 {
736 $class = __NAMESPACE__ . '\MailBox';
737
738 return $this->belongsTo(
739 $class, 'mailbox_id', 'id'
740 );
741 }
742
743
744 public function deleteTicket()
745 {
746 /*
747 * Action on ticket deleting
748 *
749 * @since v1.0.0
750 * @param object $ticket
751 */
752 do_action('fluent_support/deleting_ticket', $this);
753 // Delete the ticket
754 $this->delete();
755 }
756
757 public static function slugify($title)
758 {
759 $slug = sanitize_title($title, 'support-ticket-' . time(), 'display');
760 if (Ticket::where('slug', $slug)->first()) {
761 $slug .= '-' . time();
762 }
763 return $slug;
764 }
765
766 public function hasTag($tagId)
767 {
768 $tags = $this->tags;
769 foreach ($tags as $tag) {
770 if ($tag->id == $tagId) {
771 return true;
772 }
773 }
774
775 return false;
776 }
777
778 public function attachments()
779 {
780 $class = __NAMESPACE__ . '\Attachment';
781 return $this->hasMany($class, 'ticket_id', 'id')->where('conversation_id', NULL);
782 }
783
784 public function customData($scope = 'admin', $rendered = false)
785 {
786 if (!defined('FLUENTSUPPORTPRO')) {
787 return [];
788 }
789
790 $fields = \FluentSupportPro\App\Services\CustomFieldsService::getFieldLabels($scope);
791
792 if (!$fields) {
793 return [];
794 }
795
796 $keys = array_keys($fields);
797
798 $customRows = Meta::where('object_type', 'ticket_meta')->where('object_id', $this->id)
799 ->whereIn('key', $keys)
800 ->get();
801
802 if (!$customRows) {
803 return [];
804 }
805
806 $formattedData = [];
807
808 $customRenderers = \FluentSupportPro\App\Services\CustomFieldsService::getCustomerRenderers();
809
810 foreach ($customRows as $row) {
811 $dataKey = $row->key;
812
813 $value = $row->value;
814
815 $fieldType = $fields[$dataKey]['type'];
816
817 if ($value) {
818 if (in_array($fieldType, $customRenderers) && $rendered) {
819 $value = apply_filters('fluent_support/custom_field_render_' . $fieldType, $value, $scope);
820 } else if ($fieldType == 'checkbox') {
821 $value = array_values(array_filter(explode('|', $value)));
822 }
823
824 if (!is_array($value) && !is_object($value)) {
825 $formattedData[$dataKey] = links_add_target(make_clickable($value));
826 } else {
827 $formattedData[$dataKey] = $value;
828 }
829 }
830 }
831
832 return $formattedData;
833 }
834
835 /**
836 * @param $data This is the data that will be saved to the ticket_meta for custom fields
837 * @return bool
838 */
839 public function syncCustomFields($data)
840 {
841 if (!is_array($data)) {
842 return false;
843 }
844
845 $fields = apply_filters('fluent_support/ticket_custom_fields', []);
846
847 if (!$fields) {
848 return false;
849 }
850
851 $keys = array_keys($fields);
852
853 $validData = Arr::only($data, $keys);
854
855 foreach ($validData as $dataKey => $validDatum) {
856 if (empty($validDatum)) {
857 Meta::where('object_type', 'ticket_meta')
858 ->where('object_id', $this->id)
859 ->where('key', $dataKey)
860 ->delete();
861 continue;
862 }
863
864 if ($fields[$dataKey]['type'] == 'checkbox' || is_array($validDatum)) {
865 $validDatum = implode('|', $validDatum);
866 $validDatum = '|' . $validDatum . '|';
867 }
868
869 $exist = Meta::where('object_type', 'ticket_meta')
870 ->where('object_id', $this->id)
871 ->where('key', $dataKey)
872 ->first();
873
874 if ($exist) {
875 $exist->value = $validDatum;
876 $exist->save();
877 } else {
878 Meta::insert([
879 'object_type' => 'ticket_meta',
880 'object_id' => $this->id,
881 'key' => $dataKey,
882 'value' => $validDatum
883 ]);
884 }
885 }
886
887 return true;
888 }
889
890 public function getLastAgentResponse()
891 {
892 $query = \FluentSupport\App\App::db()->table('fs_conversations')
893 ->select(['fs_conversations.*'])
894 ->where('fs_conversations.conversation_type', 'response')
895 ->where('fs_conversations.ticket_id', $this->id)
896 ->where('fs_persons.person_type', '=', 'agent')
897 ->join('fs_persons', 'fs_persons.id', '=', 'fs_conversations.person_id')
898 ->orderBy('fs_conversations.id', 'DESC');
899
900 return $query->first();
901 }
902
903 public function getLastResponse()
904 {
905 return \FluentSupport\App\App::db()->table('fs_conversations')
906 ->where('ticket_id', $this->id)
907 ->where('conversation_type', 'response')
908 ->latest('id')
909 ->first();
910 }
911
912 /**
913 * This method will assign tags to the ticket
914 * @param $tagIds This is the array of tag ids that will be assigned to the ticket
915 * @return \FluentSupport\App\Models\Ticket
916 */
917 public function applyTags($tagIds)
918 {
919 $result = false;
920
921 if (!is_array($tagIds)) {
922 $tagIds = array($tagIds);
923 }
924
925 foreach ($tagIds as $tagId) {
926 if (!$this->hasTag($tagId)) {
927 $this->tags()->attach($tagId, ['source_type' => 'ticket_tag']);
928 $result = true;
929
930 /*
931 * Action while tag added to ticket
932 *
933 * @since v1.0.0
934 * @param integer $tagId
935 * @param object $ticket
936 */
937 do_action('fluent_support/ticket_tag_added', $tagId, $this);
938 }
939 }
940 return $result;
941 }
942
943 /**
944 * This method will remove tags from ticket
945 * @param $tagIds This is the array of tag ids that will be removed from the ticket
946 * @return \FluentSupport\App\Models\Ticket
947 */
948 public function detachTags($tagIds)
949 {
950 $result = false;
951
952 if (!is_array($tagIds)) {
953 $tagIds = array($tagIds);
954 }
955
956 foreach ($tagIds as $tagId) {
957 if ($this->hasTag($tagId)) {
958 $this->tags()->detach($tagId);
959
960 /*
961 * Action while tag removed from ticket
962 *
963 * @since v1.0.0
964 * @param integer $tagId
965 * @param object $ticket
966 */
967 do_action('fluent_support/ticket_tag_removed', $tagId, $this);
968 $result = true;
969 }
970 }
971 return $result;
972 }
973
974 /**
975 * @deprecated Use TicketService::storeTicket() instead.
976 */
977 public function createTicket($ticketData, $maybeNewCustomer = false)
978 {
979 _deprecated_function(__METHOD__, '2.0.5', 'TicketService::storeTicket()');
980
981 if (empty($ticketData['customer_id']) && $maybeNewCustomer) {
982 $email = Arr::get($maybeNewCustomer, 'email');
983 if (!$email || !is_email($email)) {
984 return new \WP_Error('error', 'A valid email is required to create a ticket');
985 }
986
987 $existingCustomer = Customer::where('email', $email)->first();
988 if ($existingCustomer) {
989 $ticketData['customer_id'] = $existingCustomer->id;
990 } else {
991 $customerData = Arr::only($maybeNewCustomer, (new Customer())->getFillable());
992 $customerData = array_filter($customerData);
993 $createCustomer = Customer::create($customerData);
994 if (!$createCustomer) {
995 return new \WP_Error('error', 'Customer could not be created');
996 }
997 $ticketData['customer_id'] = $createCustomer->id;
998 }
999 }
1000
1001 if (empty($ticketData['customer_id'])) {
1002 return new \WP_Error('error', 'Ticket could not be created');
1003 }
1004
1005 $customer = Customer::findOrFail($ticketData['customer_id']);
1006
1007 return (new TicketService())->storeTicket($ticketData, $customer);
1008 }
1009
1010 /**
1011 * This `createResponse` will create a response for a ticket
1012 * @param array $data
1013 * @param int $ticketId
1014 * @return array
1015 * @throws Exception
1016 */
1017
1018 public static function countTicketByMailBoxId($mailbox_id)
1019 {
1020 return self::where('mailbox_id', $mailbox_id)->count();
1021 }
1022
1023 public static function syncMailBoxId($mailbox_id, $fallback_id)
1024 {
1025 return self::where('mailbox_id', $mailbox_id)
1026 ->update([
1027 'mailbox_id' => $fallback_id
1028 ]);
1029 }
1030
1031 public static function getTicketsQuery()
1032 {
1033 return self::with([
1034 'customer' => function ($query) {
1035 $query->select(['first_name', 'last_name', 'email', 'id', 'avatar']);
1036 }, 'agent' => function ($query) {
1037 $query->select(['first_name', 'last_name', 'id']);
1038 },
1039 'product',
1040 'tags',
1041 'preview_response' => function ($query) {
1042 $query->latest('id');
1043 }
1044 ]);
1045 }
1046
1047 public function getSettingsValue($valueKey = false, $default = false)
1048 {
1049 $exist = Meta::where('object_type', 'ticket')
1050 ->where('key', 'settings')
1051 ->where('object_id', $this->id)
1052 ->first();
1053
1054 if ($exist) {
1055 $value = Helper::safeUnserialize($exist->value);
1056 if ($valueKey) {
1057 if (!is_array($value)) {
1058 return $default;
1059 }
1060 return Arr::get($value, $valueKey, $default);
1061 }
1062 return $value;
1063 }
1064
1065 return $default;
1066 }
1067
1068 public function updateSettingsValue($valueKey, $value)
1069 {
1070 $exist = Meta::where('object_type', 'ticket')
1071 ->where('key', 'settings')
1072 ->where('object_id', $this->id)
1073 ->first();
1074
1075 if ($exist) {
1076 $existingValue = Helper::safeUnserialize($exist->value);
1077
1078 if (!is_array($existingValue)) {
1079 $existingValue = [];
1080 }
1081
1082 $existingValue[$valueKey] = $value;
1083
1084 $exist->value = maybe_serialize($existingValue);
1085 $exist->save();
1086 return $this;
1087 }
1088
1089 $settings = [
1090 'object_type' => 'ticket',
1091 'key' => 'settings',
1092 'object_id' => $this->id,
1093 'value' => maybe_serialize([
1094 $valueKey => $value
1095 ])
1096 ];
1097
1098 Meta::create($settings);
1099
1100 return $this;
1101
1102 }
1103
1104 }
1105
1106