| 1 |
<?php |
| 2 |
|
| 3 |
namespace AcyMailing\Controllers\Stats; |
| 4 |
|
| 5 |
use AcyMailing\Classes\CampaignClass; |
| 6 |
use AcyMailing\Classes\MailClass; |
| 7 |
use AcyMailing\Classes\MailStatClass; |
| 8 |
use AcyMailing\Classes\UrlClickClass; |
| 9 |
use AcyMailing\Classes\UserStatClass; |
| 10 |
use AcyMailing\Helpers\ExportHelper; |
| 11 |
use AcyMailing\Helpers\MailerHelper; |
| 12 |
use AcyMailing\Helpers\PaginationHelper; |
| 13 |
use AcyMailing\Helpers\WorkflowHelper; |
| 14 |
use AcyMailing\Libraries\Browser\BrowserDetection; |
| 15 |
use AcyMailing\Classes\UserClass; |
| 16 |
|
| 17 |
trait GlobalStats |
| 18 |
{ |
| 19 |
public function globalStats(): void |
| 20 |
{ |
| 21 |
acym_setVar('layout', 'global_stats'); |
| 22 |
|
| 23 |
$data = []; |
| 24 |
|
| 25 |
$this->prepareDefaultPageInfo($data); |
| 26 |
|
| 27 |
$this->prepareOpenTimeChart($data); |
| 28 |
$this->preparecharts($data); |
| 29 |
$this->prepareListReceivers($data); |
| 30 |
$this->prepareDefaultLineChart($data); |
| 31 |
$this->prepareDefaultDevicesChart($data); |
| 32 |
$this->prepareDefaultBrowsersChart($data); |
| 33 |
if (count($this->selectedMailIds) == 1) { |
| 34 |
if ($data['isAbTest']) { |
| 35 |
$this->prepareAbTestMails($data); |
| 36 |
} elseif (acym_isMultilingual()) { |
| 37 |
$this->prepareMultilingualMails($data); |
| 38 |
} |
| 39 |
} |
| 40 |
$this->prepareMailFilter($data); |
| 41 |
|
| 42 |
parent::display($data); |
| 43 |
} |
| 44 |
|
| 45 |
private function prepareMailFilter(array &$data): void |
| 46 |
{ |
| 47 |
$data['input_mail_ids'] = ''; |
| 48 |
|
| 49 |
if (!empty($this->multiLanguageMailAdded) || !empty($this->generatedMailAdded)) { |
| 50 |
$this->selectedMailIds = array_filter( |
| 51 |
$this->selectedMailIds, |
| 52 |
function ($mailId) { |
| 53 |
return !in_array($mailId, $this->multiLanguageMailAdded) && !in_array($mailId, $this->generatedMailAdded); |
| 54 |
} |
| 55 |
); |
| 56 |
} |
| 57 |
|
| 58 |
if (count($this->selectedMailIds) > 1) { |
| 59 |
$data['input_mail_ids'] = '<input type="hidden" value="'.implode(',', $this->selectedMailIds).'" name="mail_ids">'; |
| 60 |
} |
| 61 |
|
| 62 |
$attributes = [ |
| 63 |
'class' => 'acym__select acym_select2_ajax acym__stats__select__mails', |
| 64 |
'data-placeholder' => acym_translation('ACYM_ALL_EMAILS'), |
| 65 |
'data-ctrl' => 'stats', |
| 66 |
'data-task' => 'searchSentMail', |
| 67 |
'data-min' => '0', |
| 68 |
'id' => 'mail_ids', |
| 69 |
]; |
| 70 |
|
| 71 |
if (!empty($this->selectedMailIds)) { |
| 72 |
$attributes['data-selected'] = implode(',', $this->selectedMailIds); |
| 73 |
} |
| 74 |
|
| 75 |
$data['mail_filter_options'] = $attributes; |
| 76 |
$data['emailVersionsFilters'] = []; |
| 77 |
|
| 78 |
if (!empty($data['emailVersions'])) { |
| 79 |
$data['emailVersionsFilters'] = [ |
| 80 |
'options' => $data['emailVersions'], |
| 81 |
'mailId' => $this->selectedMailIds[0], |
| 82 |
]; |
| 83 |
} |
| 84 |
if (!empty($data['emailTranslations'])) { |
| 85 |
$data['emailVersionsFilters'] = [ |
| 86 |
'options' => $data['emailTranslations'], |
| 87 |
'mailId' => $this->selectedMailIds[0], |
| 88 |
]; |
| 89 |
} |
| 90 |
} |
| 91 |
|
| 92 |
private function prepareClickStats(array &$data): void |
| 93 |
{ |
| 94 |
if (empty($data['selectedMailid'])) { |
| 95 |
return; |
| 96 |
} |
| 97 |
|
| 98 |
$urlClickClass = new UrlClickClass(); |
| 99 |
$allClickInfo = $urlClickClass->getAllLinkFromEmail($this->selectedMailIds[0]); |
| 100 |
|
| 101 |
$data['url_click'] = []; |
| 102 |
$data['url_click']['allClick'] = $allClickInfo['allClick']; |
| 103 |
|
| 104 |
$allPercentage = []; |
| 105 |
foreach ($allClickInfo['urls_click'] as $url) { |
| 106 |
$percentage = 0; |
| 107 |
if (empty($url->click)) { |
| 108 |
$data['url_click'][$url->name] = ['percentage' => $percentage, 'numberClick' => '0']; |
| 109 |
} else { |
| 110 |
$percentage = intval(($url->click * 100) / $allClickInfo['allClick']); |
| 111 |
$data['url_click'][$url->name] = ['percentage' => $percentage, 'numberClick' => $url->click]; |
| 112 |
} |
| 113 |
$allPercentage[] = $percentage; |
| 114 |
} |
| 115 |
|
| 116 |
$helperMailer = new MailerHelper(); |
| 117 |
if (!empty($data['mailInformation'])) { |
| 118 |
$helperMailer->body = $data['mailInformation']->body; |
| 119 |
$helperMailer->statClick(intval($data['mailInformation']->id), 0, true); |
| 120 |
$data['mailInformation']->body = preg_replace('#&(amp;)?autoSubId=[^"]+"#Uis', '"', $helperMailer->body); |
| 121 |
} |
| 122 |
|
| 123 |
if (!empty($allPercentage)) { |
| 124 |
$maxPercentage = max($allPercentage); |
| 125 |
|
| 126 |
foreach ($data['url_click'] as $name => $val) { |
| 127 |
if ($name === 'allClick') continue; |
| 128 |
$percentageRecalc = intval(($val['percentage'] * 100) / $maxPercentage); |
| 129 |
if ($percentageRecalc <= 33) { |
| 130 |
$data['url_click'][$name]['color'] = '0, 164, 255'; |
| 131 |
} elseif ($percentageRecalc <= 66) { |
| 132 |
$data['url_click'][$name]['color'] = '248, 31, 255'; |
| 133 |
} else { |
| 134 |
$data['url_click'][$name]['color'] = '255, 82, 89'; |
| 135 |
} |
| 136 |
} |
| 137 |
} |
| 138 |
|
| 139 |
$data['url_click'] = json_encode($data['url_click']); |
| 140 |
|
| 141 |
$data['url_foundation_email'] = ACYM_CSS.'libraries/foundation_email.min.css?v='.filemtime(ACYM_MEDIA.'css'.DS.'libraries'.DS.'foundation_email.min.css'); |
| 142 |
$data['url_click_map_email'] = ACYM_CSS.'click_map.min.css?v='.filemtime(ACYM_MEDIA.'css'.DS.'click_map.min.css'); |
| 143 |
} |
| 144 |
|
| 145 |
public function prepareDefaultLineChart(array &$data): void |
| 146 |
{ |
| 147 |
$data['example_line_chart'] = [ |
| 148 |
'month' => [ |
| 149 |
'Jan 18' => ['open' => '150', 'click' => '40'], |
| 150 |
], |
| 151 |
'day' => [ |
| 152 |
'23 Jan' => ['open' => '150', 'click' => '40'], |
| 153 |
], |
| 154 |
'hour' => [ |
| 155 |
'23 Jan' => ['open' => '150', 'click' => '40'], |
| 156 |
'23 Jan 08:00' => ['open' => '25', 'click' => '10'], |
| 157 |
'23 Jan 09:00' => ['open' => '50', 'click' => '10'], |
| 158 |
'23 Jan 10:00' => ['open' => '16', 'click' => '10'], |
| 159 |
'23 Jan 11:00' => ['open' => '59', 'click' => '10'], |
| 160 |
], |
| 161 |
]; |
| 162 |
} |
| 163 |
|
| 164 |
public function prepareDefaultDevicesChart(array &$data): void |
| 165 |
{ |
| 166 |
$data['example_devices_chart'] = [ |
| 167 |
'ACYM_MOBILE' => 25662, |
| 168 |
'ACYM_DESKTOP' => 12471, |
| 169 |
'ACYM_OTHER' => 3548, |
| 170 |
'ACYM_UNKNOWN' => 6213, |
| 171 |
]; |
| 172 |
} |
| 173 |
|
| 174 |
public function prepareDefaultBrowsersChart(array &$data): void |
| 175 |
{ |
| 176 |
$data['example_source_chart'] = [ |
| 177 |
'Apple Mail' => 15835, |
| 178 |
'Google Chrome' => 13375, |
| 179 |
'Safari' => 2667, |
| 180 |
'Outlook' => 2458, |
| 181 |
'Edge' => 1190, |
| 182 |
'Firefox' => 826, |
| 183 |
'ACYM_OTHER' => 4775, |
| 184 |
'ACYM_UNKNOWN' => 6123, |
| 185 |
]; |
| 186 |
} |
| 187 |
|
| 188 |
public function setDataForChartLine(): void |
| 189 |
{ |
| 190 |
$newStart = acym_date(acym_getVar('string', 'start'), 'Y-m-d H:i:s'); |
| 191 |
$newEnd = acym_date(acym_getVar('string', 'end'), 'Y-m-d H:i:s'); |
| 192 |
$mailIds = acym_getVar('int', 'id'); |
| 193 |
|
| 194 |
if (empty($mailIds)) { |
| 195 |
$mailIds = []; |
| 196 |
} |
| 197 |
|
| 198 |
$mailClass = new MailClass(); |
| 199 |
if (!empty($mailIds)) { |
| 200 |
$mailIds = $mailClass->getAutomaticMailIds($mailIds); |
| 201 |
} |
| 202 |
|
| 203 |
if (!empty($mailIds) && !is_array($mailIds)) { |
| 204 |
$mailIds = [$mailIds]; |
| 205 |
} |
| 206 |
|
| 207 |
if ($newStart >= $newEnd) { |
| 208 |
echo 'error'; |
| 209 |
exit; |
| 210 |
} |
| 211 |
|
| 212 |
$statsCampaignSelected = new \stdClass(); |
| 213 |
$this->prepareLineChart($statsCampaignSelected, $mailIds, $newStart, $newEnd); |
| 214 |
|
| 215 |
acym_lineChart($statsCampaignSelected->month, $statsCampaignSelected->day, $statsCampaignSelected->hour, true); |
| 216 |
exit; |
| 217 |
} |
| 218 |
|
| 219 |
private function getValues(string $modifier, string $intervalCode, array $campaignOpens, array $campaignClicks, string $dateCode, bool $hour = false): array |
| 220 |
{ |
| 221 |
$opens = []; |
| 222 |
foreach ($campaignOpens as $one) { |
| 223 |
if (!empty($one->open_date)) { |
| 224 |
$opens[acym_date(acym_getTimeFromUTCDate($one->open_date), $dateCode)] = $one->open; |
| 225 |
} |
| 226 |
} |
| 227 |
|
| 228 |
$clicks = []; |
| 229 |
foreach ($campaignClicks as $one) { |
| 230 |
if (!empty($one->date_click)) { |
| 231 |
$clicks[acym_date(acym_getTimeFromUTCDate($one->date_click), $dateCode)] = $one->click; |
| 232 |
} |
| 233 |
} |
| 234 |
|
| 235 |
$startDate = $campaignOpens[0]->open_date ?? null; |
| 236 |
if (!empty($campaignClicks) && !empty($campaignClicks[0]->date_click)) { |
| 237 |
$clickDate = $campaignClicks[0]->date_click; |
| 238 |
if (!empty($startDate)) { |
| 239 |
$startDate = min([$startDate, $clickDate]); |
| 240 |
} else { |
| 241 |
$startDate = $clickDate; |
| 242 |
} |
| 243 |
} |
| 244 |
|
| 245 |
$lastOpen = empty($campaignOpens) ? null : end($campaignOpens); |
| 246 |
$endDate = $lastOpen->open_date ?? null; |
| 247 |
if (!empty($campaignClicks)) { |
| 248 |
$lastClick = end($campaignClicks); |
| 249 |
$clickDate = $lastClick->date_click ?? null; |
| 250 |
if (!empty($endDate) && !empty($clickDate)) { |
| 251 |
$endDate = max([$endDate, $clickDate]); |
| 252 |
} elseif (!empty($clickDate)) { |
| 253 |
$endDate = $clickDate; |
| 254 |
} |
| 255 |
} |
| 256 |
|
| 257 |
if (empty($startDate) || empty($endDate)) { |
| 258 |
return []; |
| 259 |
} |
| 260 |
|
| 261 |
// Apply timezone for chart by hour |
| 262 |
if ($modifier === 'min') { |
| 263 |
$startDate = acym_date(acym_getTimeFromUTCDate($startDate), 'Y-m-d H:i:s'); |
| 264 |
$endDate = acym_date(acym_getTimeFromUTCDate($endDate), 'Y-m-d H:i:s'); |
| 265 |
} |
| 266 |
|
| 267 |
$begin = new \DateTime($startDate); |
| 268 |
$end = new \DateTime($endDate); |
| 269 |
|
| 270 |
$end->modify('+1 '.$modifier); |
| 271 |
|
| 272 |
$interval = new \DateInterval($intervalCode); |
| 273 |
$daterange = new \DatePeriod($begin, $interval, $end); |
| 274 |
|
| 275 |
$result = []; |
| 276 |
foreach ($daterange as $date) { |
| 277 |
$one = $date->format($dateCode); |
| 278 |
|
| 279 |
$current = []; |
| 280 |
$current['open'] = empty($opens[$one]) ? 0 : $opens[$one]; |
| 281 |
$current['click'] = empty($clicks[$one]) ? 0 : $clicks[$one]; |
| 282 |
|
| 283 |
$key = $hour ? $one.':00' : $one; |
| 284 |
$result[$key] = $current; |
| 285 |
} |
| 286 |
|
| 287 |
return $result; |
| 288 |
} |
| 289 |
|
| 290 |
public function prepareLineChart(object &$statsCampaignSelected, array $mailIdsOfCampaign, string $newStart = '', string $newEnd = ''): void |
| 291 |
{ |
| 292 |
$statsCampaignSelected->hasStats = true; |
| 293 |
$anonymousStats = !empty($this->config->get('anonymous_stats', 0)); |
| 294 |
|
| 295 |
//We get the opening by month, day, hour |
| 296 |
if ($anonymousStats) { |
| 297 |
$mailStatClass = new MailStatClass(); |
| 298 |
$campaignOpenByMonth = $mailStatClass->getAnonOpensByFormat($mailIdsOfCampaign, '%Y-%m', $newStart, $newEnd); |
| 299 |
$campaignOpenByDay = $mailStatClass->getAnonOpensByFormat($mailIdsOfCampaign, '%Y-%m-%d', $newStart, $newEnd); |
| 300 |
$campaignOpenByHour = $mailStatClass->getAnonOpensByFormat($mailIdsOfCampaign, '%Y-%m-%d %H:00:00', $newStart, $newEnd); |
| 301 |
} else { |
| 302 |
$campaignClass = new CampaignClass(); |
| 303 |
$campaignOpenByMonth = $campaignClass->getOpenByMonth($mailIdsOfCampaign, $newStart, $newEnd); |
| 304 |
$campaignOpenByDay = $campaignClass->getOpenByDay($mailIdsOfCampaign, $newStart, $newEnd); |
| 305 |
$campaignOpenByHour = $campaignClass->getOpenByHour($mailIdsOfCampaign, $newStart, $newEnd); |
| 306 |
} |
| 307 |
|
| 308 |
$urlClickClass = new UrlClickClass(); |
| 309 |
$campaignClickByMonth = $urlClickClass->getAllClickByMailMonth($mailIdsOfCampaign, $newStart, $newEnd); |
| 310 |
$campaignClickByDay = $urlClickClass->getAllClickByMailDay($mailIdsOfCampaign, $newStart, $newEnd); |
| 311 |
$campaignClickByHour = $urlClickClass->getAllClickByMailHour($mailIdsOfCampaign, $newStart, $newEnd); |
| 312 |
|
| 313 |
// In anonymous mode a click doesn't register an open, and in any mode a link can be clicked without the open pixel firing: gating on opens alone wrongly hid the whole chart. |
| 314 |
$hasOpens = !empty($campaignOpenByMonth) && !empty($campaignOpenByDay) && !empty($campaignOpenByHour); |
| 315 |
$hasClicks = !empty($campaignClickByMonth) && !empty($campaignClickByDay) && !empty($campaignClickByHour); |
| 316 |
if (!$hasOpens && !$hasClicks) { |
| 317 |
$statsCampaignSelected->hasStats = false; |
| 318 |
|
| 319 |
return; |
| 320 |
} |
| 321 |
|
| 322 |
$statsCampaignSelected->month = $this->getValues('day', 'P1M', $campaignOpenByMonth, $campaignClickByMonth, 'Y-m'); |
| 323 |
$statsCampaignSelected->day = $this->getValues('hour', 'P1D', $campaignOpenByDay, $campaignClickByDay, 'Y-m-d'); |
| 324 |
$statsCampaignSelected->hour = $this->getValues('min', 'PT1H', $campaignOpenByHour, $campaignClickByHour, 'Y-m-d H:i', true); |
| 325 |
|
| 326 |
$allHour = array_keys($statsCampaignSelected->hour); |
| 327 |
|
| 328 |
//those are the dates when the first was open and the last was open |
| 329 |
$statsCampaignSelected->startEndDateHour = []; |
| 330 |
if (!empty($allHour)) { |
| 331 |
$statsCampaignSelected->startEndDateHour['start'] = $allHour[0]; |
| 332 |
$statsCampaignSelected->startEndDateHour['end'] = end($allHour); |
| 333 |
} |
| 334 |
} |
| 335 |
|
| 336 |
public function exportGlobal(): void |
| 337 |
{ |
| 338 |
wp_verify_nonce(acym_getVar('cmd', '_wpnonce'), 'acymnonce') || die('Invalid Token'); |
| 339 |
|
| 340 |
$exportType = acym_getVar('string', 'export_type', 'charts'); |
| 341 |
|
| 342 |
$functionName = 'exportGlobal'.ucfirst($exportType); |
| 343 |
|
| 344 |
if (!method_exists($this, $functionName)) { |
| 345 |
acym_enqueueMessage(acym_translation('ACYM_EXPORT_METHOD_NOT_FOUND'), 'error'); |
| 346 |
$this->listing(); |
| 347 |
|
| 348 |
return; |
| 349 |
} |
| 350 |
|
| 351 |
$this->$functionName(); |
| 352 |
} |
| 353 |
|
| 354 |
public function prepareOpenTimeChart(array &$data): void |
| 355 |
{ |
| 356 |
$userStatClass = new UserStatClass(); |
| 357 |
if (!empty($this->config->get('anonymous_stats', 0))) { |
| 358 |
$mailStatClass = new MailStatClass(); |
| 359 |
$statsDB = $mailStatClass->getAnonOpenTimeStats($this->selectedMailIds); |
| 360 |
} else { |
| 361 |
$statsDB = $userStatClass->getOpenTimeStats($this->selectedMailIds); |
| 362 |
} |
| 363 |
|
| 364 |
if (empty($statsDB['total_open'])) { |
| 365 |
$data['openTime'] = $userStatClass->getDefaultStat(); |
| 366 |
$data['empty_open'] = true; |
| 367 |
|
| 368 |
return; |
| 369 |
} |
| 370 |
|
| 371 |
$data['empty_open'] = false; |
| 372 |
|
| 373 |
$stats = []; |
| 374 |
|
| 375 |
for ($day = 0; $day < 7; $day++) { |
| 376 |
$stats[$day] = []; |
| 377 |
for ($hour = 0; $hour < 8; $hour++) { |
| 378 |
if (empty($statsDB['stats'][$day.'_'.$hour])) { |
| 379 |
$percentage = 0; |
| 380 |
} else { |
| 381 |
$percentage = ($statsDB['stats'][$day.'_'.$hour]->open_total * 100) / $statsDB['total_open']; |
| 382 |
} |
| 383 |
$stats[$day][$hour] = round($percentage); |
| 384 |
} |
| 385 |
} |
| 386 |
|
| 387 |
$data['openTime'] = $stats; |
| 388 |
} |
| 389 |
|
| 390 |
public function prepareStatByList(array &$data): void |
| 391 |
{ |
| 392 |
$selectedMailIds = array_unique($data['selectedMailid']); |
| 393 |
$mailid = count($selectedMailIds) !== 1 ? 0 : (int)reset($selectedMailIds); |
| 394 |
|
| 395 |
if (empty($mailid)) { |
| 396 |
return; |
| 397 |
} |
| 398 |
|
| 399 |
$mailSelected = (int)$this->getVarFiltersListing('int', 'mail_id_language', $mailid); |
| 400 |
|
| 401 |
$data['lists'] = []; |
| 402 |
$data['emailsSent'] = []; |
| 403 |
$data['emailsOpen'] = []; |
| 404 |
$data['bounces'] = []; |
| 405 |
$data['unsubscribed'] = []; |
| 406 |
$data['click'] = []; |
| 407 |
$data['listsStats'] = []; |
| 408 |
$data['userPerList'] = []; |
| 409 |
|
| 410 |
$mailClass = new MailClass(); |
| 411 |
$mail = $mailClass->getOneById($mailSelected); |
| 412 |
|
| 413 |
if (empty($mail) || !in_array($mailid, [$mail->id, $mail->parent_id])) { |
| 414 |
$mailSelected = $mailid; |
| 415 |
$mail = $mailClass->getOneById($mailSelected); |
| 416 |
} |
| 417 |
|
| 418 |
if (empty($mail)) { |
| 419 |
return; |
| 420 |
} |
| 421 |
|
| 422 |
$mainMailId = empty($mail->parent_id) ? $mail->id : $mail->parent_id; |
| 423 |
|
| 424 |
// get lists from the selected campaign |
| 425 |
$query = 'SELECT l.id, l.name, l.color FROM #__acym_list l'; |
| 426 |
$query .= ' JOIN #__acym_mail_has_list ml on l.id = ml.list_id'; |
| 427 |
$query .= ' WHERE ml.mail_id = '.intval($mainMailId); |
| 428 |
|
| 429 |
$data['lists'] = acym_loadObjectList($query, 'id'); |
| 430 |
$listIds = implode(',', array_keys($data['lists'])); |
| 431 |
|
| 432 |
if (!empty($listIds)) { |
| 433 |
//get nbSent, nbOpen, nbBounce, nbUnsubscribe |
| 434 |
$query = 'SELECT ul.list_id, SUM(us.sent) as nbSent, SUM(IF(us.open >= 1, 1, 0)) as nbOpen, SUM(us.bounce) as nbBounce, SUM(us.unsubscribe) as nbUnsub from #__acym_user_has_list ul'; |
| 435 |
$query .= ' JOIN #__acym_user_stat us on ul.user_id = us.user_id'; |
| 436 |
$query .= ' WHERE ul.list_id in ('.$listIds.') AND us.mail_id = '.$mailSelected; |
| 437 |
$query .= ' GROUP BY ul.list_id;'; |
| 438 |
$data['listsStats'] = acym_loadObjectList($query, 'list_id'); |
| 439 |
|
| 440 |
//get nbClick |
| 441 |
$query = 'SELECT ul.list_id, COUNT(uc.click) as nbClick'; |
| 442 |
$query .= ' FROM #__acym_user_has_list ul '; |
| 443 |
$query .= ' JOIN #__acym_url_click uc on ul.user_id = uc.user_id'; |
| 444 |
$query .= ' WHERE ul.list_id in ('.$listIds.') AND uc.mail_id = '.$mailSelected; |
| 445 |
$query .= ' GROUP BY ul.list_id;'; |
| 446 |
|
| 447 |
$data['nbClick'] = acym_loadObjectList($query, 'list_id'); |
| 448 |
|
| 449 |
//get nbUser per list |
| 450 |
$query = 'SELECT ul.list_id, COUNT(ul.user_id) as nbUser'; |
| 451 |
$query .= ' FROM #__acym_user_has_list ul'; |
| 452 |
$query .= ' JOIN #__acym_user_stat us ON ul.user_id = us.user_id'; |
| 453 |
$query .= ' WHERE ul.list_id in ('.$listIds.') AND us.mail_id = '.$mailSelected; |
| 454 |
$query .= ' GROUP BY ul.list_id;'; |
| 455 |
|
| 456 |
$data['userPerList'] = acym_loadObjectList($query, 'list_id'); |
| 457 |
} |
| 458 |
|
| 459 |
//format data for barChart |
| 460 |
foreach ($data['listsStats'] as $listId => $item) { |
| 461 |
$nbSent = empty($item->nbSent) ? 1 : $item->nbSent; |
| 462 |
|
| 463 |
$data['emailsSent'][$listId] = [ |
| 464 |
'label' => $data['lists'][$listId]->name, |
| 465 |
'value' => $item->nbSent, |
| 466 |
'color' => $data['lists'][$listId]->color, |
| 467 |
]; |
| 468 |
$data['emailsOpen'][$listId] = [ |
| 469 |
'label' => $data['lists'][$listId]->name, |
| 470 |
'value' => round(($item->nbOpen * 100) / $nbSent), |
| 471 |
'color' => $data['lists'][$listId]->color, |
| 472 |
]; |
| 473 |
$data['bounces'][$listId] = [ |
| 474 |
'label' => $data['lists'][$listId]->name, |
| 475 |
'value' => round(($item->nbBounce * 100) / $nbSent), |
| 476 |
'color' => $data['lists'][$listId]->color, |
| 477 |
]; |
| 478 |
$data['unsubscribed'][$listId] = [ |
| 479 |
'label' => $data['lists'][$listId]->name, |
| 480 |
'value' => round(($item->nbUnsub * 100) / $nbSent), |
| 481 |
'color' => $data['lists'][$listId]->color, |
| 482 |
]; |
| 483 |
} |
| 484 |
|
| 485 |
foreach ($data['userPerList'] as $listId => $item) { |
| 486 |
$nbUser = $item->nbUser; |
| 487 |
// we can't provide lists and their colors if there is no click, |
| 488 |
// so we initialize the values with $data['lists'] to display lists with no click rather than nothing |
| 489 |
$data['click'][$listId] = ['label' => $data['lists'][$listId]->name, 'value' => 0, 'color' => $data['lists'][$listId]->color]; |
| 490 |
|
| 491 |
if ($nbUser > 0 && !empty($data['nbClick'][$listId]->nbClick)) { |
| 492 |
$data['click'][$listId]['value'] = round(($data['nbClick'][$listId]->nbClick * 100) / $nbUser); |
| 493 |
} |
| 494 |
} |
| 495 |
} |
| 496 |
|
| 497 |
private function prepareAbTestMails(array &$data): void |
| 498 |
{ |
| 499 |
if (empty($this->selectedMailIds)) return; |
| 500 |
|
| 501 |
$mailClass = new MailClass(); |
| 502 |
|
| 503 |
$mailVersions = []; |
| 504 |
|
| 505 |
if (empty($data['mailInformation']->parent_id)) { |
| 506 |
$mailVersions = $mailClass->getParentAndChildMails($data['mailInformation']->id); |
| 507 |
} elseif (!empty($data['mailInformation']->parent_id)) { |
| 508 |
$parentEmail = $mailClass->getOneById($data['mailInformation']->parent_id); |
| 509 |
if (empty($parentEmail)) return; |
| 510 |
$mailVersions = $mailClass->getParentAndChildMails($parentEmail->id); |
| 511 |
} |
| 512 |
|
| 513 |
$data['emailVersions'] = []; |
| 514 |
|
| 515 |
foreach ($mailVersions as $email) { |
| 516 |
$data['emailVersions'][$email->id] = $email->name.' - '.$email->subject; |
| 517 |
} |
| 518 |
} |
| 519 |
|
| 520 |
private function prepareMultilingualMails(array &$data): void |
| 521 |
{ |
| 522 |
if (empty($this->selectedMailIds)) return; |
| 523 |
|
| 524 |
$mailClass = new MailClass(); |
| 525 |
|
| 526 |
$translatedEmails = []; |
| 527 |
|
| 528 |
if (empty($data['mailInformation']->parent_id)) { |
| 529 |
$translatedEmails = $mailClass->getTranslationsById($data['mailInformation']->id, true, true); |
| 530 |
} elseif (!empty($data['mailInformation']->parent_id)) { |
| 531 |
$parentEmail = $mailClass->getOneById($data['mailInformation']->parent_id); |
| 532 |
if (empty($parentEmail)) return; |
| 533 |
$translatedEmails = $mailClass->getTranslationsById($parentEmail->id, true, true); |
| 534 |
} |
| 535 |
|
| 536 |
$data['emailTranslations'] = []; |
| 537 |
$allLanguages = acym_getLanguages(); |
| 538 |
|
| 539 |
foreach ($translatedEmails as $email) { |
| 540 |
if (!empty($email->language)) { |
| 541 |
$data['emailTranslations'][$email->id] = empty($allLanguages[$email->language]) ? $email->language |
| 542 |
: $allLanguages[$email->language]->name; |
| 543 |
} |
| 544 |
} |
| 545 |
} |
| 546 |
|
| 547 |
private function prepareDevicesStats(array &$data): void |
| 548 |
{ |
| 549 |
if (!empty($this->config->get('anonymous_stats', 0))) { |
| 550 |
$mailStatClass = new MailStatClass(); |
| 551 |
$devicesCampaign = $mailStatClass->getAnonDevices($this->selectedMailIds); |
| 552 |
} else { |
| 553 |
$campaignClass = new CampaignClass(); |
| 554 |
$devicesCampaign = $campaignClass->getDevicesWithCountByMailId($this->selectedMailIds); |
| 555 |
} |
| 556 |
|
| 557 |
$defaultDataDevices = [ |
| 558 |
'ACYM_MOBILE' => 0, |
| 559 |
'ACYM_DESKTOP' => 0, |
| 560 |
'ACYM_OTHER' => 0, |
| 561 |
'ACYM_UNKNOWN' => 0, |
| 562 |
]; |
| 563 |
$data['devices'] = $defaultDataDevices; |
| 564 |
|
| 565 |
foreach ($devicesCampaign as $oneDevice) { |
| 566 |
if (empty($oneDevice->number)) { |
| 567 |
continue; |
| 568 |
} |
| 569 |
|
| 570 |
if (empty($oneDevice->device)) { |
| 571 |
$device = 'ACYM_UNKNOWN'; |
| 572 |
} elseif (in_array($oneDevice->device, array_merge(BrowserDetection::PLATFORMS_MOBILE, BrowserDetection::LEGACY_PLATFORMS_MOBILE))) { |
| 573 |
$device = 'ACYM_MOBILE'; |
| 574 |
} elseif (in_array($oneDevice->device, array_merge(BrowserDetection::PLATFORMS_DESKTOP, BrowserDetection::LEGACY_PLATFORMS_DESKTOP))) { |
| 575 |
$device = 'ACYM_DESKTOP'; |
| 576 |
} else { |
| 577 |
$device = 'ACYM_OTHER'; |
| 578 |
} |
| 579 |
|
| 580 |
$data['devices'][$device] += $oneDevice->number; |
| 581 |
} |
| 582 |
|
| 583 |
if ($data['devices'] === $defaultDataDevices) { |
| 584 |
$data['devices'] = []; |
| 585 |
} |
| 586 |
} |
| 587 |
|
| 588 |
private function prepareOpenSourcesStats(array &$data): void |
| 589 |
{ |
| 590 |
if (!empty($this->config->get('anonymous_stats', 0))) { |
| 591 |
$mailStatClass = new MailStatClass(); |
| 592 |
$openedFromStats = $mailStatClass->getAnonOpenSources($this->selectedMailIds); |
| 593 |
} else { |
| 594 |
$userStatClass = new UserStatClass(); |
| 595 |
$openedFromStats = $userStatClass->getOpenSourcesStats($this->selectedMailIds); |
| 596 |
} |
| 597 |
|
| 598 |
$data['openedWith'] = []; |
| 599 |
foreach ($openedFromStats as $oneSource) { |
| 600 |
if (empty($oneSource->number)) { |
| 601 |
continue; |
| 602 |
} |
| 603 |
|
| 604 |
if (!empty($oneSource->opened_with) && isset(BrowserDetection::LEGACY_OPENED_WITH_MAP[$oneSource->opened_with])) { |
| 605 |
$oneSource->opened_with = BrowserDetection::LEGACY_OPENED_WITH_MAP[$oneSource->opened_with]; |
| 606 |
} |
| 607 |
|
| 608 |
if (empty($oneSource->opened_with)) { |
| 609 |
$oneSource->opened_with = 'ACYM_UNKNOWN'; |
| 610 |
} |
| 611 |
|
| 612 |
$data['openedWith'][$oneSource->opened_with] = $oneSource->number; |
| 613 |
} |
| 614 |
} |
| 615 |
|
| 616 |
public function preparecharts(array &$data): void |
| 617 |
{ |
| 618 |
$mailStatClass = new MailStatClass(); |
| 619 |
|
| 620 |
$data['mail'] = $mailStatClass->getSentFailByMailIds($this->selectedMailIds); |
| 621 |
if (empty($data['mail'])) return; |
| 622 |
|
| 623 |
$campaignClass = new CampaignClass(); |
| 624 |
$urlClickClass = new UrlClickClass(); |
| 625 |
|
| 626 |
//For the total opening, the doughnut chart |
| 627 |
$data['mail']->totalMail = $data['mail']->sent + $data['mail']->fail; |
| 628 |
$data['mail']->percentageSent = empty($data['mail']->totalMail) ? 0 : number_format(($data['mail']->sent * 100) / $data['mail']->totalMail, 2); |
| 629 |
$data['mail']->allSent = empty($data['mail']->totalMail) |
| 630 |
? acym_translationSprintf('ACYM_X_MAIL_SUCCESSFULLY_SENT_OF_X', 0, 0) |
| 631 |
: acym_translationSprintf( |
| 632 |
'ACYM_X_MAIL_SUCCESSFULLY_SENT_OF_X', |
| 633 |
$data['mail']->sent, |
| 634 |
$data['mail']->totalMail |
| 635 |
); |
| 636 |
|
| 637 |
// Anonymized mode has no per-subscriber data: show total opens/clicks instead of unique counts |
| 638 |
$anonymousStats = !empty($this->config->get('anonymous_stats', 0)); |
| 639 |
$data['anonymousStats'] = $anonymousStats; |
| 640 |
|
| 641 |
if ($anonymousStats) { |
| 642 |
$cumulatedTotals = $mailStatClass->getCumulatedStatsByMailIds($this->selectedMailIds); |
| 643 |
$openCount = (int)($cumulatedTotals->open ?? 0); |
| 644 |
$clickCount = (int)($cumulatedTotals->click_unique ?? 0); |
| 645 |
$openSent = (int)$data['mail']->sent; |
| 646 |
} else { |
| 647 |
$openRateCampaign = empty($this->selectedMailIds) ? $campaignClass->getOpenRateAllCampaign() : $campaignClass->getOpenRateCampaigns($this->selectedMailIds); |
| 648 |
$openCount = (int)$openRateCampaign->open_unique; |
| 649 |
$openSent = (int)$openRateCampaign->sent; |
| 650 |
$clickCount = $urlClickClass->getNumberUsersClicked($this->selectedMailIds); |
| 651 |
} |
| 652 |
|
| 653 |
//open rate |
| 654 |
// In anonymous mode $openCount is a total number of opens (not a unique-user count), so use a wording that reflects the total number of times mails were opened |
| 655 |
$openLabelKey = $anonymousStats ? 'ACYM_X_MAIL_OPENED_TIMES_OF_X' : 'ACYM_X_MAIL_OPENED_OF_X'; |
| 656 |
$data['mail']->openCount = $openCount; |
| 657 |
// In anonymous mode the count is a total (not a unique), so the rate can exceed 100%: cap it to keep a meaningful percentage |
| 658 |
$openRate = empty($openSent) ? 0 : ($openCount * 100) / $openSent; |
| 659 |
$data['mail']->percentageOpen = number_format($anonymousStats ? min($openRate, 100) : $openRate, 2); |
| 660 |
$data['mail']->allOpen = empty($openSent) |
| 661 |
? acym_translationSprintf($openLabelKey, 0, 0) |
| 662 |
: acym_translationSprintf($openLabelKey, $openCount, $openSent); |
| 663 |
|
| 664 |
//click rate |
| 665 |
// In anonymous mode $clickCount is a total (aggregated with user_id = 0), not a unique-user count, so use a wording that reflects total clicks |
| 666 |
$clickLabelKey = $anonymousStats ? 'ACYM_X_CLICKS_REGISTERED_OF_X' : 'ACYM_X_MAIL_CLICKED_OF_X'; |
| 667 |
$data['mail']->clickCount = $clickCount; |
| 668 |
// In anonymous mode the count is a total (not a unique), so the rate can exceed 100%: cap it to keep a meaningful percentage |
| 669 |
$clickRate = empty($data['mail']->sent) ? 0 : ($clickCount * 100) / $data['mail']->sent; |
| 670 |
$data['mail']->percentageClick = number_format($anonymousStats ? min($clickRate, 100) : $clickRate, 2); |
| 671 |
$data['mail']->allClick = empty($data['mail']->sent) |
| 672 |
? acym_translationSprintf($clickLabelKey, 0, 0) |
| 673 |
: acym_translationSprintf($clickLabelKey, $clickCount, $data['mail']->sent); |
| 674 |
|
| 675 |
//bounce rate |
| 676 |
$bounceRateCampaign = empty($this->selectedMailIds) ? $campaignClass->getBounceRateAllCampaign() : $campaignClass->getBounceRateCampaigns($this->selectedMailIds); |
| 677 |
$data['mail']->percentageBounce = empty($data['mail']->sent) ? 0 : number_format(($bounceRateCampaign->bounce_unique * 100) / $data['mail']->sent, 2); |
| 678 |
$data['mail']->allBounce = empty($data['mail']->sent) |
| 679 |
? acym_translationSprintf('ACYM_X_BOUNCE_OF_X', 0, 0) |
| 680 |
: acym_translationSprintf( |
| 681 |
'ACYM_X_BOUNCE_OF_X', |
| 682 |
$bounceRateCampaign->bounce_unique, |
| 683 |
$data['mail']->sent |
| 684 |
); |
| 685 |
|
| 686 |
if (!empty($this->selectedMailIds)) { |
| 687 |
//unsubscribe rate |
| 688 |
$mailStat = $mailStatClass->getByMailIds($this->selectedMailIds); |
| 689 |
$data['mail']->allUnsub = 0; |
| 690 |
$data['mail']->percentageUnsub = 0; |
| 691 |
if (!empty($mailStat)) { |
| 692 |
$data['mail']->percentageUnsub = empty($data['mail']->sent) ? 0 : number_format(($mailStat->unsubscribe_total * 100) / $data['mail']->sent, 2); |
| 693 |
$data['mail']->allUnsub = empty($data['mail']->sent) |
| 694 |
? acym_translationSprintf('ACYM_X_USERS_UNSUBSCRIBED_OF_X', 0, 0) |
| 695 |
: acym_translationSprintf( |
| 696 |
'ACYM_X_USERS_UNSUBSCRIBED_OF_X', |
| 697 |
$mailStat->unsubscribe_total, |
| 698 |
$data['mail']->sent |
| 699 |
); |
| 700 |
} |
| 701 |
} |
| 702 |
|
| 703 |
$this->prepareDevicesStats($data); |
| 704 |
$this->prepareOpenSourcesStats($data); |
| 705 |
$this->prepareLineChart($data['mail'], $this->selectedMailIds); |
| 706 |
$this->prepareComparedStats($data); |
| 707 |
} |
| 708 |
|
| 709 |
private function decode(array &$detailedStats, array $columnsToDecode = ['mail_subject', 'mail_name']): void |
| 710 |
{ |
| 711 |
foreach ($detailedStats as $oneDetailedStat) { |
| 712 |
foreach ($columnsToDecode as $column) { |
| 713 |
if (!empty($oneDetailedStat->$column)) { |
| 714 |
$oneDetailedStat->$column = acym_utf8Decode($oneDetailedStat->$column); |
| 715 |
} |
| 716 |
} |
| 717 |
} |
| 718 |
} |
| 719 |
|
| 720 |
private function prepareLinksDetailsListing(array &$data): void |
| 721 |
{ |
| 722 |
$data['search'] = $this->getVarFiltersListing('string', 'links_details_search', ''); |
| 723 |
$data['ordering'] = $this->getVarFiltersListing('cmd', 'links_details_ordering', 'id'); |
| 724 |
$data['orderingSortOrder'] = $this->getVarFiltersListing('cmd', 'links_details_ordering_sort_order', 'desc'); |
| 725 |
|
| 726 |
if (empty($this->selectedMailIds)) return; |
| 727 |
|
| 728 |
$pagination = new PaginationHelper(); |
| 729 |
$urlClickClass = new UrlClickClass(); |
| 730 |
|
| 731 |
$detailedStatsPerPage = $pagination->getListLimit(); |
| 732 |
$page = $this->getVarFiltersListing('int', 'links_details_pagination_page', 1); |
| 733 |
|
| 734 |
$urlClicks = $urlClickClass->getUrlsFromMailsWithDetails( |
| 735 |
[ |
| 736 |
'ordering' => $data['ordering'], |
| 737 |
'search' => $data['search'], |
| 738 |
'detailedStatsPerPage' => $detailedStatsPerPage, |
| 739 |
'offset' => ($page - 1) * $detailedStatsPerPage, |
| 740 |
'ordering_sort_order' => $data['orderingSortOrder'], |
| 741 |
'mail_ids' => $this->selectedMailIds, |
| 742 |
] |
| 743 |
); |
| 744 |
|
| 745 |
$this->decode($urlClicks['links_details']); |
| 746 |
|
| 747 |
// Prepare the pagination |
| 748 |
$pagination->setStatus((int)$urlClicks['total']->total, $page, $detailedStatsPerPage); |
| 749 |
|
| 750 |
$data['pagination'] = $pagination; |
| 751 |
$data['links_details'] = $urlClicks['links_details']; |
| 752 |
$data['query'] = $urlClicks['query']; |
| 753 |
} |
| 754 |
|
| 755 |
private function prepareListReceivers(array &$data): void |
| 756 |
{ |
| 757 |
if (empty($this->selectedMailIds)) return; |
| 758 |
|
| 759 |
$mailStatClass = new MailStatClass(); |
| 760 |
$mailClass = new MailClass(); |
| 761 |
|
| 762 |
$data['mailStat'] = $mailStatClass->getByMailIds($this->selectedMailIds); |
| 763 |
$data['lists'] = $mailClass->getAllListsByMailId($this->selectedMailIds[0]); |
| 764 |
} |
| 765 |
|
| 766 |
private function exportGlobalFormatted(): void |
| 767 |
{ |
| 768 |
$exportHelper = new ExportHelper(); |
| 769 |
$data = []; |
| 770 |
$this->prepareDefaultPageInfo($data); |
| 771 |
$data['show_date_filters'] = true; |
| 772 |
$data['page_title'] = false; |
| 773 |
$timeLinechart = acym_getVar('string', 'time_linechart', 'month'); |
| 774 |
|
| 775 |
$this->prepareMailFilter($data); |
| 776 |
$this->prepareClickStats($data); |
| 777 |
$this->preparecharts($data); |
| 778 |
$this->prepareDefaultLineChart($data); |
| 779 |
$this->prepareDefaultDevicesChart($data); |
| 780 |
$this->prepareDefaultBrowsersChart($data); |
| 781 |
|
| 782 |
$globalDonut = [ |
| 783 |
$data['mail']->percentageSent, |
| 784 |
$data['mail']->percentageOpen, |
| 785 |
$data['mail']->percentageClick, |
| 786 |
$data['mail']->percentageBounce, |
| 787 |
$data['mail']->percentageUnsub, |
| 788 |
]; |
| 789 |
$mailName = empty($this->selectedMailIds) ? acym_translation('ACYM_ALL_MAILS') : $data['mailInformation']->name; |
| 790 |
$globalLine = $data['mail']->$timeLinechart; |
| 791 |
|
| 792 |
$exportHelper->exportStatsFormattedCSV($mailName, $globalDonut, $globalLine, $timeLinechart); |
| 793 |
exit; |
| 794 |
} |
| 795 |
|
| 796 |
private function exportGlobalFull(): void |
| 797 |
{ |
| 798 |
$exportHelper = new ExportHelper(); |
| 799 |
$data = []; |
| 800 |
$this->prepareDefaultPageInfo($data); |
| 801 |
|
| 802 |
$where = ''; |
| 803 |
if (!empty($this->selectedMailIds)) { |
| 804 |
$where = 'WHERE mail_id IN ('.implode(',', $this->selectedMailIds).')'; |
| 805 |
} |
| 806 |
|
| 807 |
$columnsMailStat = acym_getColumns('mail_stat'); |
| 808 |
$columnsToExport = []; |
| 809 |
|
| 810 |
$columnsToExport['mail.subject'] = acym_translation('ACYM_EMAIL_SUBJECT'); |
| 811 |
foreach ($columnsMailStat as $column) { |
| 812 |
if ($column === 'mail_id') { |
| 813 |
continue; |
| 814 |
} |
| 815 |
|
| 816 |
$translation = acym_translation('ACYM_'.strtoupper($column).'_COLUMN_STAT'); |
| 817 |
if ($column === 'send_date') { |
| 818 |
$translation = acym_translation('ACYM_SEND_DATE'); |
| 819 |
} elseif ($column === 'click_total') { |
| 820 |
$translation = acym_translation('ACYM_TOTAL_CLICKS'); |
| 821 |
} elseif ($column === 'click_unique') { |
| 822 |
$translation = acym_translation('ACYM_UNIQUE_CLICKS'); |
| 823 |
} |
| 824 |
|
| 825 |
$columnsToExport['mailstat.'.$column] = $translation; |
| 826 |
} |
| 827 |
|
| 828 |
$query = 'SELECT '.implode(', ', array_keys($columnsToExport)).' FROM #__acym_mail_stat AS mailstat LEFT JOIN #__acym_mail AS mail ON mail.id = mailstat.mail_id '.$where; |
| 829 |
$exportHelper->exportStatsFullCSV($query, $columnsToExport); |
| 830 |
exit; |
| 831 |
} |
| 832 |
|
| 833 |
private function prepareComparedStats(array &$data): void |
| 834 |
{ |
| 835 |
if (empty($data['totalSubscribers']) || empty($data['mail']->percentageOpen) || empty($data['mail']->percentageClick) || empty($data['mail']->percentageBounce)) { |
| 836 |
return; |
| 837 |
} |
| 838 |
|
| 839 |
$currentDate = acym_date('now', 'Y-m-d'); |
| 840 |
|
| 841 |
$newStats = [ |
| 842 |
'date' => $currentDate, |
| 843 |
'value' => [ |
| 844 |
'totalSubscribers' => $data['totalSubscribers'], |
| 845 |
'openRate' => $data['mail']->percentageOpen, |
| 846 |
'clickRate' => $data['mail']->percentageClick, |
| 847 |
'bounceRate' => $data['mail']->percentageBounce, |
| 848 |
'totalNewSubscribers' => $data['totalSubscribers'], |
| 849 |
], |
| 850 |
]; |
| 851 |
|
| 852 |
$keys = [ |
| 853 |
'totalSubscribersHistory' => true, |
| 854 |
'percentageOpenHistory' => false, |
| 855 |
'percentageClickHistory' => false, |
| 856 |
'percentageBounceHistory' => false, |
| 857 |
'totalNewSubscribersHistory' => false, |
| 858 |
]; |
| 859 |
|
| 860 |
$oldEvolutionData = json_decode($this->config->get('statsEvolution', '{}'), true); |
| 861 |
|
| 862 |
foreach ($keys as $key => $isPercentage) { |
| 863 |
$history = json_decode($this->config->get($key, '[]'), true); |
| 864 |
|
| 865 |
if (!empty($history) && end($history)['date'] === $currentDate) { |
| 866 |
continue; |
| 867 |
} |
| 868 |
|
| 869 |
$history[] = ['date' => $currentDate, 'value' => $newStats['value'][$this->getStatKey($key)]]; |
| 870 |
|
| 871 |
if (count($history) > 30) { |
| 872 |
array_shift($history); |
| 873 |
} |
| 874 |
|
| 875 |
$this->config->saveConfig([$key => json_encode($history)]); |
| 876 |
|
| 877 |
$evolutionKey = $this->getEvolutionKey($key); |
| 878 |
$oldEvolutionData[$evolutionKey] = $this->calculateEvolution($history, $isPercentage); |
| 879 |
} |
| 880 |
|
| 881 |
$this->config->saveConfig(['statsEvolution' => json_encode($oldEvolutionData)]); |
| 882 |
|
| 883 |
if (!empty($data)) { |
| 884 |
$this->updateDashboardData($data); |
| 885 |
} |
| 886 |
} |
| 887 |
|
| 888 |
private function getStatKey(string $key): ?string |
| 889 |
{ |
| 890 |
return [ |
| 891 |
'totalSubscribersHistory' => 'totalSubscribers', |
| 892 |
'percentageOpenHistory' => 'openRate', |
| 893 |
'percentageClickHistory' => 'clickRate', |
| 894 |
'percentageBounceHistory' => 'bounceRate', |
| 895 |
'totalNewSubscribersHistory' => 'totalNewSubscribers', |
| 896 |
][$key] ?? null; |
| 897 |
} |
| 898 |
|
| 899 |
private function getEvolutionKey(string $key): ?string |
| 900 |
{ |
| 901 |
return [ |
| 902 |
'totalSubscribersHistory' => 'totalSubscribersEvolution', |
| 903 |
'percentageOpenHistory' => 'openRateEvolution', |
| 904 |
'percentageClickHistory' => 'clickRateEvolution', |
| 905 |
'percentageBounceHistory' => 'bounceRateEvolution', |
| 906 |
'totalNewSubscribersHistory' => 'totalNewSubscribersEvolution', |
| 907 |
][$key] ?? null; |
| 908 |
} |
| 909 |
|
| 910 |
private function calculateEvolution(array $history, bool $isPercentage = false): ?float |
| 911 |
{ |
| 912 |
if (empty($history)) { |
| 913 |
return null; |
| 914 |
} |
| 915 |
|
| 916 |
$oldValue = $history[0]['value'] ?? 0; |
| 917 |
$newValue = end($history)['value'] ?? 0; |
| 918 |
|
| 919 |
if ($oldValue === 0) { |
| 920 |
return null; |
| 921 |
} |
| 922 |
|
| 923 |
if ($isPercentage) { |
| 924 |
// For totalSubscribers: percentage variation |
| 925 |
return round((($newValue - $oldValue) / $oldValue) * 100, 2); |
| 926 |
} else { |
| 927 |
// For other stats: absolute difference |
| 928 |
return round($newValue - $oldValue, 2); |
| 929 |
} |
| 930 |
} |
| 931 |
|
| 932 |
private function updateDashboardData(array &$data): void |
| 933 |
{ |
| 934 |
$evolutionData = json_decode($this->config->get('statsEvolution', '{}'), true); |
| 935 |
|
| 936 |
if (!empty($evolutionData)) { |
| 937 |
$data['totalSubscribersEvolution'] = $evolutionData['totalSubscribersEvolution'] ?? null; |
| 938 |
$data['openRateEvolution'] = $evolutionData['openRateEvolution'] ?? null; |
| 939 |
$data['clickRateEvolution'] = $evolutionData['clickRateEvolution'] ?? null; |
| 940 |
$data['bounceRateEvolution'] = $evolutionData['bounceRateEvolution'] ?? null; |
| 941 |
$data['totalNewSubscribersEvolution'] = $evolutionData['totalNewSubscribersEvolution'] ?? null; |
| 942 |
} |
| 943 |
} |
| 944 |
} |
| 945 |
|