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 / Modules / Reporting / Reporting.php

Reporting.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.2.0, at app/Modules/Reporting/Reporting.php

902 lines 31.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App\Modules\Reporting;
4
5 use FluentSupport\App\Models\Agent;
6 use FluentSupport\App\Models\AgentGroup;
7 use FluentSupport\App\Models\Conversation;
8 use FluentSupport\App\Models\MailBox;
9 use FluentSupport\App\Models\Meta;
10 use FluentSupport\App\Models\Person;
11 use FluentSupport\App\Models\Product;
12 use FluentSupport\App\Models\TagPivot;
13 use FluentSupport\App\Models\Ticket;
14 use FluentSupport\App\Services\Helper;
15 use FluentSupport\Framework\Database\Orm\Builder;
16 use FluentSupport\Framework\Support\Arr;
17 use FluentSupport\Framework\Support\DateTime;
18
19 /**
20 * Reporting class is responsible for getting data related to report
21 * @package FluentSupport\App\Modules\Reporting
22 *
23 * @version 1.0.0
24 */
25 class Reporting
26 {
27 use ReportingHelperTrait;
28
29 /**
30 * getTicketsGrowth will generate tickets statistics and return
31 * @param false $from
32 * @param false $to
33 * @param array $filters
34 * @return array
35 */
36 public function getTicketsGrowth($from = false, $to = false, $filters = [])
37 {
38 //Generate report period
39 $period = $this->makeDatePeriod(
40 $from = $this->makeFromDate($from),//Date from
41 $to = $this->makeToDate($to),//Date to
42 $frequency = $this->getFrequency($from, $to)// frequency P1D, P1W, P1M
43 );
44
45 //Get group by and order by i.e date,week, month
46 list($groupBy, $orderBy) = $this->getGroupAndOrder($frequency);
47
48 //get all tickets statistics within the date range
49 $query = $this->db()->table('fs_tickets')
50 ->select($this->prepareSelect($frequency))
51 ->whereBetween('created_at', $this->prepareBetween($frequency, $from, $to))
52 ->groupBy($groupBy)
53 ->oldest($orderBy);
54
55 //If filter by product or agent or status selected
56 if ($filters) {
57 if (!empty($filters['statuses'])) {
58 $query->whereIn('status', $filters['statuses']);
59 }
60
61 if (!empty($filters['product_id'])) {
62 $query->where('product_id', $filters['product_id']);
63 }
64
65 if (!empty($filters['agent_id'])) {
66 $query->where('agent_id', $filters['agent_id']);
67 }
68
69 if (!empty($filters['mailbox_id'])) {
70 $query->where('mailbox_id', $filters['mailbox_id']);
71 }
72
73 if (!empty($filters['agent_ids'])) {
74 $query->whereIn('agent_id', $filters['agent_ids']);
75 }
76 }
77
78 $items = $query->get();
79
80 return $this->getResult($period, $items);
81 }
82
83 /**
84 * getTicketResolveGrowth method will get the statistics for resolved/closed tickets
85 * @param false $from
86 * @param false $to
87 * @param array $filters
88 * @return array
89 */
90 public function getTicketResolveGrowth($from = false, $to = false, $filters = [], $type = '')
91 {
92 $period = $this->makeDatePeriod(
93 $from = $this->makeFromDate($from),//Date from
94 $to = $this->makeToDate($to),//date to
95 $frequency = $this->getFrequency($from, $to)// frequency P1D, P1W, P1M
96 );
97
98 list($groupBy, $orderBy) = $this->getGroupAndOrder($frequency);//Get group by and order by i.e date,week, month
99
100 $filterColumn = (!empty($type)) ? $type.'_id' : 'id';
101
102 //get the closed ticket statistics within the date range
103 $query = $this->db()->table('fs_tickets')
104 ->select($this->prepareSelect($frequency, 'resolved_at'))
105 ->whereBetween('resolved_at', $this->prepareBetween($frequency, $from, $to))
106 ->where('status', 'closed')
107 ->where($filterColumn, '>', 0)
108 ->groupBy($groupBy)
109 ->oldest($orderBy);
110
111 //If filter by product or agent is selected
112 if ($filters) {
113 if (!empty($filters['product_id'])) {
114 $query->where('product_id', $filters['product_id']);
115 }
116
117 if (!empty($filters['agent_id'])) {
118 $query->where('agent_id', $filters['agent_id']);
119 }
120
121 if (!empty($filters['mailbox_id'])) {
122 $query->where('mailbox_id', $filters['mailbox_id']);
123 }
124
125 if (!empty($filters['agent_ids'])) {
126 $query->whereIn('agent_id', $filters['agent_ids']);
127 }
128 }
129
130 $items = $query->get();
131
132 return $this->getResult($period, $items);
133 }
134
135 /**
136 * getResponseGrowth method will generate the statistics for response
137 * @param false $from
138 * @param false $to
139 * @param array $filters
140 * @return array
141 */
142 public function getResponseGrowth($from = false, $to = false, $filters = [])
143 {
144 $period = $this->makeDatePeriod(
145 $from = $this->makeFromDate($from),
146 $to = $this->makeToDate($to),
147 $frequency = $this->getFrequency($from, $to)
148 );
149
150 list($groupBy, $orderBy) = $this->getGroupAndOrder($frequency);
151
152 $query = Conversation::query()
153 ->select($this->prepareSelect($frequency))
154 ->whereBetween('created_at', $this->prepareBetween($frequency, $from, $to))
155 ->where('conversation_type', 'response')
156 ->whereHas('person', function ($q) {
157 $q->where('person_type', 'agent');
158 })
159 ->groupBy($groupBy)
160 ->oldest($orderBy);
161
162 if ($filters) {
163 if (!empty($filters['person_id'])) {
164 $query->where('person_id', $filters['person_id']);
165 }
166
167 if (!empty($filters['person_ids'])) {
168 $query->whereIn('person_id', $filters['person_ids']);
169 }
170
171 if (!empty($filters['product_id'])) {
172 $query->where('product_id', $filters['product_id']);
173 }
174 }
175
176 $items = $query->get();
177
178 return $this->getResult($period, $items);
179 }
180
181 public function getResponseGrowthChart($from = false, $to = false, $filters = [], $type = ''): array
182 {
183 $period = $this->makeDatePeriod(
184 $from = $this->makeFromDate($from),
185 $to = $this->makeToDate($to),
186 $frequency = $this->getFrequency($from, $to)
187 );
188
189 list($groupBy, $orderBy) = $this->getGroupAndOrder($frequency);
190
191 $filterColumn = $type."_id";
192
193 $query = $this->db()->table('fs_tickets')
194 ->select($this->prepareSelect($frequency,'created_at','response_count'))
195 ->whereBetween('created_at', $this->prepareBetween($frequency, $from, $to))
196 ->havingRaw('COUNT(response_count)> 0')
197 ->where($filterColumn, '>', 0)
198 ->groupBy($groupBy)
199 ->oldest($orderBy);
200
201 if ($filters) {
202 if (!empty($filters['product_id'])) {
203 $query->where('product_id', $filters['product_id']);
204 }
205
206 if (!empty($filters['mailbox_id'])) {
207 $query->where('mailbox_id', $filters['mailbox_id']);
208 }
209 }
210
211 $items = $query->get();
212
213 return $this->getResult($period, $items);
214 }
215
216 /**
217 * agentSummary method will prepare ticket summary with responses by agent
218 * @param false $from
219 * @param false $to
220 * @param false $agent
221 * @return mixed
222 */
223 public function agentSummary($from = false, $to = false, $agent = false)
224 {
225 if(!$from) {
226 $from = current_time('Y-m-d');
227 }
228
229 if(!$to) {
230 $to = current_time('Y-m-d');
231 }
232
233 $from .= ' 00:00:00';
234 $to .= ' 23:59:59';
235 $reports = [];
236
237 //Get tickets statistics that are closed
238 $resolves = $this->db()->table('fs_tickets')
239 ->select([
240 $this->db()->raw('COUNT(id) AS count'),
241 'agent_id',
242 ])
243 ->groupBy('agent_id')
244 ->where('status', 'closed')
245 ->whereBetween('resolved_at', [$from, $to])
246 ->get();
247
248 $reports = $this->pushReportData('closed', $resolves, $reports, 'agent_id');
249
250 //get statistics for all except closed ticket
251 $openTickets = $this->db()->table('fs_tickets')
252 ->select([
253 $this->db()->raw('COUNT(id) AS count'),
254 'agent_id'
255 ])
256 ->groupBy('agent_id')
257 ->where('status', '!=', 'closed')
258 ->get();
259
260 $reports = $this->pushReportData('opens', $openTickets, $reports, 'agent_id');
261 //Get response by agent
262 $responses = Conversation::select([
263 $this->db()->raw('COUNT(id) AS count'),
264 $this->db()->raw('person_id as agent_id'),
265 $this->db()->raw('created_at')
266 ])
267 ->whereHas('person', function ($q) {
268 $q->where('person_type', '=', 'agent');
269 })
270 ->whereBetween('created_at', [$from, $to])
271 ->where('conversation_type', 'response')
272 ->groupBy('agent_id')
273 ->get();
274
275 $reports = $this->pushReportData('responses', $responses, $reports, 'agent_id');
276 //Get interactions/responses by individual agents
277 foreach ($responses as $response) {
278 $reports[$response->agent_id]['interactions'] = Conversation::where('person_id', $response->agent_id)
279 ->where('conversation_type', 'response')
280 ->whereBetween('created_at', [$from, $to])
281 ->groupBy('ticket_id')
282 ->get()
283 ->count();
284 }
285
286 $agentIds = array_keys($reports);
287
288 if ($agent) {
289 $agentIds = array_map('intval', explode(',', $agent));
290 }
291
292 // get agent feedback statistics
293 $agentFeedbackRatingEnabled = Helper::getBusinessSettings('agent_feedback_rating') === 'yes';
294 if (defined('FLUENTSUPPORTPRO_PLUGIN_VERSION') && $agentFeedbackRatingEnabled) {
295 $agentConversations = Conversation::select([
296 $this->db()->raw('person_id as agent_id'),
297 $this->db()->raw('GROUP_CONCAT(id) as conversation_ids')
298 ])
299 ->whereIn('person_id', $agentIds)
300 ->whereHas('person', function ($q) {
301 $q->where('person_type', '=', 'agent');
302 })
303 ->where('conversation_type', 'response')
304 ->groupBy('agent_id')
305 ->get();
306
307 foreach ($agentConversations as $conversation) {
308 $conversationIds = array_map('intval', explode(',', $conversation->conversation_ids));
309
310 $feedbackMeta = Meta::whereIn('object_id', $conversationIds)
311 ->where('key', 'agent_feedback_ratings')
312 ->whereBetween('created_at', [$from, $to])
313 ->get();
314
315 $likeCount = 0;
316 $dislikeCount = 0;
317
318 foreach ($feedbackMeta as $feedback) {
319 $feedbackStatus = $feedback->value;
320
321 if ($feedbackStatus === 'like') {
322 $likeCount++;
323 } elseif ($feedbackStatus === 'dislike') {
324 $dislikeCount++;
325 }
326 }
327
328 $agentId = $conversation->agent_id;
329 $reports[$agentId]['likes'] = $likeCount;
330 $reports[$agentId]['dislikes'] = $dislikeCount;
331 }
332 }
333
334 $agents = Agent::select(['id', 'first_name', 'last_name', 'email'])
335 ->whereIn('id', $agentIds)
336 ->get();
337
338 foreach ($agents as $agent) {
339 $report = NULL;
340 if(isset($reports[$agent->id])) {
341 $reportFields = [
342 'interactions' => 0,
343 'responses' => 0,
344 'opens' => 0,
345 'closed' => 0,
346 'waiting_tickets' => 0,
347 ];
348
349 $additionalFields = defined('FLUENTSUPPORTPRO_PLUGIN_VERSION') && $agentFeedbackRatingEnabled ? ['likes' => 0, 'dislikes' => 0] : [];
350 $reportFields = $reportFields + $additionalFields;
351
352 $report = wp_parse_args($reports[$agent->id], $reportFields);
353 }
354 $agent->stats = $report;
355 $agent->active_stat = $this->getActiveStatByAgent($agent->id);
356 }
357 return $agents;
358 }
359
360 public function getSummary($type, $from = null, $to = null)
361 {
362 global $wpdb;
363 $tablePrefix = $wpdb->prefix;
364
365 if (!$from) {
366 $from = current_time('Y-m-d');
367 }
368
369 if (!$to) {
370 $to = current_time('Y-m-d');
371 }
372
373 $from .= ' 00:00:00';
374 $to .= ' 23:59:59';
375 $reports = [];
376
377 $groupByField = $type == 'product' ? 'product_id' : 'mailbox_id';
378
379 $resolves = $this->db()->table('fs_tickets')
380 ->select([
381 $this->db()->raw('COUNT(id) AS count'),
382 $groupByField,
383 ])
384 ->groupBy($groupByField)
385 ->where('status', 'closed')
386 ->whereBetween('resolved_at', [$from, $to])
387 ->get();
388
389 $reports = $this->pushReportData('closed', $resolves, $reports, $groupByField);
390
391 $openTickets = $this->db()->table('fs_tickets')
392 ->select([
393 $this->db()->raw('COUNT(id) AS count'),
394 $groupByField
395 ])
396 ->groupBy($groupByField)
397 ->where('status', '!=', 'closed')
398 ->whereBetween('created_at', [$from, $to])
399 ->get();
400
401 $reports = $this->pushReportData('opens', $openTickets, $reports, $groupByField);
402
403 $responses = $this->db()->table('fs_conversations')
404 ->join('fs_tickets', 'fs_tickets.id', '=', 'fs_conversations.ticket_id')
405 ->select([
406 $this->db()->raw('COUNT(' . $tablePrefix . 'fs_conversations.id) AS count'),
407 'fs_tickets.' . $groupByField,
408 ])
409 ->groupBy('fs_tickets.' . $groupByField)
410 ->whereBetween('fs_conversations.created_at', [$from, $to])
411 ->get();
412
413 $reports = $this->pushReportData('responses', $responses, $reports, $groupByField);
414
415 $ticketIds = $this->db()->table('fs_tickets')
416 ->select([
417 $groupByField,
418 $this->db()->raw('GROUP_CONCAT(id) AS ticket_ids'),
419 ])
420 ->where($groupByField, '!=', 0)
421 ->groupBy($groupByField)
422 ->get();
423
424 $result = [];
425 foreach ($ticketIds as $item) {
426 $result[$item->{$groupByField}] = explode(',', $item->ticket_ids);
427 }
428
429 foreach ($result as $id => $ticketIds) {
430 $interactions = Conversation::whereIn('ticket_id', $ticketIds)
431 ->where('conversation_type', 'response')
432 ->whereBetween('created_at', [$from, $to])
433 ->groupBy('ticket_id')
434 ->get()
435 ->count();
436
437 $reports[$id]['interactions'] = $interactions;
438 }
439
440 $ids = array_keys($reports);
441
442 $types = [
443 'product' => [
444 'model' => Product::class,
445 'fields' => ['id', 'title'],
446 ],
447 'mailbox' => [
448 'model' => MailBox::class,
449 'fields' => ['id', 'name'],
450 ],
451 ];
452
453 $model = $types[$type]['model'];
454 $fields = $types[$type]['fields'];
455
456 $items = $model::select($fields)
457 ->whereIn('id', $ids)
458 ->get();
459
460 foreach ($items as $item) {
461 $report = isset($reports[$item->id]) ? $reports[$item->id] : [];
462
463 $report = wp_parse_args($report, [
464 'responses' => 0,
465 'opens' => 0,
466 'closed' => 0,
467 'interactions' => 0
468 ]);
469 $item->stats = $report;
470 $item->active_stat = '';
471 }
472
473 return $items;
474 }
475
476 /**
477 * agentGroupSummary method will prepare ticket summary aggregated by agent group
478 * @param false $from
479 * @param false $to
480 * @return mixed
481 */
482 public function agentGroupSummary($from = false, $to = false)
483 {
484 if (!$from) {
485 $from = current_time('Y-m-d');
486 }
487
488 if (!$to) {
489 $to = current_time('Y-m-d');
490 }
491
492 $from .= ' 00:00:00';
493 $to .= ' 23:59:59';
494
495 // Get per-agent stats using the same queries as agentSummary
496 $reports = [];
497
498 $resolves = $this->db()->table('fs_tickets')
499 ->select([
500 $this->db()->raw('COUNT(id) AS count'),
501 'agent_id',
502 ])
503 ->groupBy('agent_id')
504 ->where('status', 'closed')
505 ->whereBetween('resolved_at', [$from, $to])
506 ->get();
507
508 $reports = $this->pushReportData('closed', $resolves, $reports, 'agent_id');
509
510 $openTickets = $this->db()->table('fs_tickets')
511 ->select([
512 $this->db()->raw('COUNT(id) AS count'),
513 'agent_id'
514 ])
515 ->groupBy('agent_id')
516 ->where('status', '!=', 'closed')
517 ->get();
518
519 $reports = $this->pushReportData('opens', $openTickets, $reports, 'agent_id');
520
521 $responses = Conversation::select([
522 $this->db()->raw('COUNT(id) AS count'),
523 $this->db()->raw('person_id as agent_id'),
524 ])
525 ->whereHas('person', function ($q) {
526 $q->where('person_type', '=', 'agent');
527 })
528 ->whereBetween('created_at', [$from, $to])
529 ->where('conversation_type', 'response')
530 ->groupBy('agent_id')
531 ->get();
532
533 $reports = $this->pushReportData('responses', $responses, $reports, 'agent_id');
534
535 foreach ($responses as $response) {
536 $reports[$response->agent_id]['interactions'] = Conversation::where('person_id', $response->agent_id)
537 ->where('conversation_type', 'response')
538 ->whereBetween('created_at', [$from, $to])
539 ->groupBy('ticket_id')
540 ->get()
541 ->count();
542 }
543
544 // Get group -> agent_id mappings
545 $groupAgentMap = TagPivot::where('source_type', 'agent_group')
546 ->select(['tag_id', 'source_id'])
547 ->get()
548 ->groupBy('tag_id');
549
550 // Get all agent groups
551 $groups = AgentGroup::select(['id', 'title'])->get();
552
553 $defaultStats = [
554 'responses' => 0,
555 'interactions' => 0,
556 'opens' => 0,
557 'closed' => 0,
558 ];
559
560 foreach ($groups as $group) {
561 $agentIds = isset($groupAgentMap[$group->id])
562 ? array_map('intval', $groupAgentMap[$group->id]->pluck('source_id')->toArray())
563 : [];
564
565 $group->agents_count = count($agentIds);
566
567 $groupStats = $defaultStats;
568
569 foreach ($agentIds as $agentId) {
570 if (isset($reports[$agentId])) {
571 $agentStats = wp_parse_args($reports[$agentId], $defaultStats);
572 $groupStats['responses'] += (int) $agentStats['responses'];
573 $groupStats['interactions'] += (int) $agentStats['interactions'];
574 $groupStats['opens'] += (int) $agentStats['opens'];
575 $groupStats['closed'] += (int) $agentStats['closed'];
576 }
577 }
578
579 $group->stats = $groupStats;
580 }
581
582 return $groups;
583 }
584
585 /**
586 * pushReportData method will format the ticket summary report
587 * @param $type
588 * @param $tickets
589 * @param $reports
590 * @param $groupByField
591 * @return array
592 */
593 private function pushReportData($type, $tickets, $reports, $groupByField): array
594 {
595 foreach ($tickets as $ticket) {
596 $groupKey = $ticket->{$groupByField};
597
598 if (!$groupKey) {
599 continue;
600 }
601
602 if (!isset($reports[$groupKey])) {
603 $reports[$groupKey] = [];
604 }
605
606 $reports[$groupKey][$type] = $ticket->count;
607 }
608
609 return $reports;
610 }
611
612 /**
613 * getActiveStats method will return the statistics for active tickets
614 * This method will get the list of open tickets calculate the wait times and return results
615 * @return array|false
616 */
617 public function getActiveStats()
618 {
619 // We will calculate the wait times for open waiting tickets
620 $waitStat = Ticket::waitingOnly()
621 ->where('status', '!=', 'closed')
622 ->whereNotNull('waiting_since')
623 ->select([
624 $this->db()->raw('avg(UNIX_TIMESTAMP(waiting_since)) as avg_waiting'),
625 $this->db()->raw('MIN(UNIX_TIMESTAMP(waiting_since)) as max_waiting'),
626 $this->db()->raw('COUNT(*) as total_tickets')
627 ])
628 ->first();
629
630 if(!$waitStat) {
631 return false;
632 }
633
634 $waitStat->avg_waiting = intval($waitStat->avg_waiting);
635 if($waitStat->avg_waiting > 0) {
636 $waitSeconds = time() - $waitStat->avg_waiting;
637 if( $waitSeconds < 172800 && $waitSeconds > 7200) {
638 $avgWait = ceil($waitSeconds / 3600) . ' hours';
639 } else {
640 $avgWait = human_time_diff($waitStat->avg_waiting, time());
641 }
642 } else {
643 $avgWait = 0;
644 }
645
646 return [
647 'average_waiting' => $avgWait,
648 'max_waiting' => (intval($waitStat->max_waiting)) ? human_time_diff(intval($waitStat->max_waiting), time()) : 0,
649 'waiting_tickets' => $waitStat->total_tickets
650 ];
651 }
652
653 /**
654 * getActiveStatByAgent method will return the statistics of active tickets for an agent
655 * This method will get agent id as parameter, fetch the list of open tickets by agent id, calculate the wait times and return results
656 * @param $agentId
657 * @return array|false
658 */
659 public function getActiveStatByAgent($agentId)
660 {
661 $waitStat = Ticket::waitingOnly()
662 ->where('status', '!=', 'closed')
663 ->whereNotNull('waiting_since')
664 ->where('agent_id', $agentId)
665 ->select([
666 $this->db()->raw('avg(UNIX_TIMESTAMP(waiting_since)) as avg_waiting'),
667 $this->db()->raw('MIN(UNIX_TIMESTAMP(waiting_since)) as max_waiting'),
668 $this->db()->raw('COUNT(*) as total_tickets')
669 ])
670 ->first();
671
672 if(!$waitStat) {
673 return false;
674 }
675
676 $waitStat->avg_waiting = intval($waitStat->avg_waiting);
677 if($waitStat->avg_waiting > 0) {
678 $waitSeconds = time() - $waitStat->avg_waiting;
679 if( $waitSeconds < 172800 && $waitSeconds > 7200) {
680 $avgWait = ceil($waitSeconds / 3600) . ' hours';
681 } else {
682 $avgWait = human_time_diff($waitStat->avg_waiting, time());
683 }
684 } else {
685 $avgWait = 0;
686 }
687
688 return [
689 'average_waiting' => $avgWait,
690 'max_waiting' => (intval($waitStat->max_waiting)) ? human_time_diff(intval($waitStat->max_waiting), time()) : 0,
691 'waiting_tickets' => $waitStat->total_tickets
692 ];
693 }
694
695 public function getQueryResults($from, $to, $filter)
696 {
697 switch ($filter['report_type']) {
698 case 'ticket':
699 return $this->getTicketStats($from, $to);
700 case 'agent_response':
701 return $this->getResponseStats($from, $to, 'agent', $filter['agent_id'] ?? null);
702 case 'customer_response':
703 return $this->getResponseStats($from, $to, 'customer');
704 default:
705 return [];
706 }
707 }
708
709 public function getTicketStats($from, $to)
710 {
711 global $wpdb;
712
713 $whereClause = '';
714 $queryParams = [];
715
716 if ($from && $to) {
717 $start_date = gmdate('Y-m-d 00:00:00', strtotime($from));
718 $end_date = gmdate('Y-m-d 23:59:59', strtotime($to));
719 $whereClause = 'WHERE created_at BETWEEN %s AND %s';
720 $queryParams[] = $start_date;
721 $queryParams[] = $end_date;
722 }
723
724 // SQL query to count tickets by day of week and hour within the specified date range.
725 // $wpdb->prefix is WordPress-internal; $whereClause is built from hardcoded literals
726 // only — user-supplied date values are bound via %s placeholders in prepare() below.
727 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
728 $query = "SELECT DAYNAME(created_at) AS weekday, HOUR(created_at) AS hour, COUNT(*) AS count
729 FROM {$wpdb->prefix}fs_tickets
730 {$whereClause}
731 GROUP BY DAYNAME(created_at), HOUR(created_at)
732 ORDER BY FIELD(DAYNAME(created_at), 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'), HOUR(created_at)";
733
734 // Execute the query — prepared when date params are present, plain otherwise.
735 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $query structure is hardcoded; values are bound via $wpdb->prepare() when $queryParams is non-empty.
736 $results = $queryParams
737 ? $wpdb->get_results($wpdb->prepare($query, $queryParams)) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
738 : $wpdb->get_results($query); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
739
740 $fillData = array_fill(0, 24, 0);
741 // add :00 on the suffix
742 $fillData = array_combine(array_map(function ($hour) {
743 return sprintf('%1d:00', $hour);
744 }, array_keys($fillData)), $fillData);
745
746 // Prepare the report
747 // Prepare the report
748 $report = [
749 'Mon' => $fillData,
750 'Tue' => $fillData,
751 'Wed' => $fillData,
752 'Thu' => $fillData,
753 'Fri' => $fillData,
754 'Sat' => $fillData,
755 'Sun' => $fillData
756 ];
757
758 foreach ($results as $result) {
759 $weekdayName = substr($result->weekday, 0, 3);
760 if (!isset($report[$weekdayName])) {
761 $report[$weekdayName] = $fillData;
762 }
763 $report[$weekdayName][sprintf('%1d:00', $result->hour)] = (int)$result->count;
764 }
765
766 return $report;
767 }
768
769 public function getResponseStats($from, $to, $reportType, $agentId = null)
770 {
771 global $wpdb;
772
773 $whereClause = ' AND p.person_type = %s';
774 $queryParams = [$reportType];
775
776 if ($from && $to) {
777 $start_date = gmdate('Y-m-d 00:00:00', strtotime($from));
778 $end_date = gmdate('Y-m-d 23:59:59', strtotime($to));
779 $whereClause .= ' AND c.created_at BETWEEN %s AND %s';
780 $queryParams[] = $start_date;
781 $queryParams[] = $end_date;
782 }
783
784 if ($agentId) {
785 $whereClause .= ' AND c.person_id = %d';
786 $queryParams[] = intval($agentId);
787 }
788
789 // SQL query to count customer responses by day of week and hour within the specified date range
790 $query = $wpdb->prepare(
791 "SELECT DAYNAME(c.created_at) AS weekday, HOUR(c.created_at) AS hour, COUNT(*) AS count
792 FROM {$wpdb->prefix}fs_conversations AS c
793 JOIN {$wpdb->prefix}fs_persons AS p ON c.person_id = p.id
794 WHERE c.conversation_type = 'response'
795 {$whereClause}
796 GROUP BY DAYNAME(c.created_at), HOUR(c.created_at)
797 ORDER BY FIELD(DAYNAME(c.created_at), 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'), HOUR(c.created_at)",
798 $queryParams
799 );
800
801 // Execute the query
802 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.NotPrepared -- $query is the output of $wpdb->prepare() above; checker incorrectly traces back through the prepared parameters.
803 $results = $wpdb->get_results($query);
804
805 $fillData = array_fill(0, 24, 0);
806 // add :00 on the suffix
807 $fillData = array_combine(array_map(function ($hour) {
808 return sprintf('%1d:00', $hour);
809 }, array_keys($fillData)), $fillData);
810
811 // Prepare the report
812 $report = [
813 'Mon' => $fillData,
814 'Tue' => $fillData,
815 'Wed' => $fillData,
816 'Thu' => $fillData,
817 'Fri' => $fillData,
818 'Sat' => $fillData,
819 'Sun' => $fillData
820 ];
821
822 foreach ($results as $result) {
823 $weekdayName = substr($result->weekday, 0, 3);
824 if (!isset($report[$weekdayName])) {
825 $report[$weekdayName] = $fillData;
826 }
827 $report[$weekdayName][sprintf('%01d:00', $result->hour)] = (int)$result->count;
828 }
829
830 return $report;
831 }
832
833 public function getTicketResponseStats($from, $to, $filter)
834 {
835 $query = Conversation::query()
836 ->select('id', 'ticket_id', 'person_id', 'created_at', 'content')
837 ->addSelect([
838 'person_type' => Person::select('person_type')
839 ->whereColumn('id', 'fs_conversations.person_id'),
840 'full_name' => Person::selectRaw("CONCAT(first_name, ' ', last_name)")
841 ->whereColumn('id', 'fs_conversations.person_id')
842 ])
843 ->where('conversation_type', 'response')
844 ->when(Arr::get($filter, 'person_type'), function (Builder $q, $personType) {
845 return $q->whereHas('person', function ($q) use ($personType) {
846 return $q->where('person_type', $personType);
847 });
848 })
849 ->when(($from && $to), function ($q) use ($from, $to) {
850 return $q->whereBetween('created_at', [
851 DateTime::parse($from)->startOfDay(),
852 DateTime::parse($to)->endOfDay()
853 ]);
854 })
855 ->when(Arr::get($filter, 'person_id'), function ($q, $personId) {
856 return $q->where('person_id', $personId);
857 });
858
859 return $query->get();
860 }
861
862 public function applyDateFilter($query, $from, $to)
863 {
864 if ($from && $to) {
865 $from .= ' 00:00:00';
866 $to .= ' 23:59:59';
867
868 $query->whereBetween('created_at', [$from, $to]);
869 }
870 }
871
872 public function finalizeQuery($query)
873 {
874 return $query->groupByRaw('DAYNAME(created_at), HOUR(created_at)')
875 ->orderByRaw("FIELD(DAYNAME(created_at), 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'), HOUR(created_at)")
876 ->get();
877 }
878
879 public function formatResults($results)
880 {
881 $dataItems = [
882 'Mon' => [], 'Tue' => [], 'Wed' => [], 'Thu' => [], 'Fri' => [], 'Sat' => [], 'Sun' => []
883 ];
884
885 $hours = array_map(function ($hour) {
886 return $hour . ":00";
887 }, range(0, 23));
888
889 foreach ($dataItems as $day => $data) {
890 $dataItems[$day] = array_fill_keys($hours, 0);
891 }
892
893 foreach ($results as $row) {
894 $day = substr($row['day_of_week'], 0, 3);
895 $hour = $row['hour_of_day'] . ":00";
896 $dataItems[$day][$hour] = (int) $row['count'];
897 }
898
899 return $dataItems;
900 }
901 }
902