PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.1.0
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.1.0
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
← All changes | back/Controllers/Stats/GlobalStats.php +399 -202 10.0.1 → 11.1.0 View file →
@@ -10,12 +10,14 @@
10 10 use AcyMailing\Helpers\ExportHelper;
11 11 use AcyMailing\Helpers\MailerHelper;
12 12 use AcyMailing\Helpers\PaginationHelper;
13 13 use AcyMailing\Helpers\WorkflowHelper;
14 +use AcyMailing\Libraries\Browser\BrowserDetection;
15 +use AcyMailing\Classes\UserClass;
14 16
15 17 trait GlobalStats
16 18 {
17 - public function globalStats()
19 + public function globalStats(): void
18 20 {
19 21 acym_setVar('layout', 'global_stats');
20 22
21 23 $data = [];
@@ -24,9 +26,8 @@
24 26
25 27 $this->prepareOpenTimeChart($data);
26 28 $this->preparecharts($data);
27 29 $this->prepareListReceivers($data);
28 - $this->prepareDefaultRoundCharts($data);
29 30 $this->prepareDefaultLineChart($data);
30 31 $this->prepareDefaultDevicesChart($data);
31 32 $this->prepareDefaultBrowsersChart($data);
32 33 if (count($this->selectedMailIds) == 1) {
@@ -40,9 +41,9 @@
40 41
41 42 parent::display($data);
42 43 }
43 44
44 - private function prepareMailFilter(&$data)
45 + private function prepareMailFilter(array &$data): void
45 46 {
46 47 $data['input_mail_ids'] = '';
47 48
48 49 if (!empty($this->multiLanguageMailAdded) || !empty($this->generatedMailAdded)) {
@@ -66,44 +67,34 @@
66 67 'data-min' => '0',
67 68 'id' => 'mail_ids',
68 69 ];
69 70
70 - if (!empty($this->selectedMailIds)) $attributes['data-selected'] = implode(',', $this->selectedMailIds);
71 + if (!empty($this->selectedMailIds)) {
72 + $attributes['data-selected'] = implode(',', $this->selectedMailIds);
73 + }
71 74
72 - $data['mail_filter'] = acym_selectMultiple(
73 - [],
74 - 'mail_ids',
75 - [],
76 - $attributes
77 - );
75 + $data['mail_filter_options'] = $attributes;
76 + $data['emailVersionsFilters'] = [];
78 77
79 - $data['emailVersionsFilters'] = '';
80 -
81 78 if (!empty($data['emailVersions'])) {
82 - $data['emailVersionsFilters'] = acym_select(
83 - $data['emailVersions'],
84 - 'mail_id_version',
85 - $this->selectedMailIds[0],
86 - [
87 - 'class' => 'acym__select acym__stats__select__language',
88 - ]
89 - );
79 + $data['emailVersionsFilters'] = [
80 + 'options' => $data['emailVersions'],
81 + 'mailId' => $this->selectedMailIds[0],
82 + ];
90 83 }
91 84 if (!empty($data['emailTranslations'])) {
92 - $data['emailVersionsFilters'] = acym_select(
93 - $data['emailTranslations'],
94 - 'mail_id_version',
95 - $this->selectedMailIds[0],
96 - [
97 - 'class' => 'acym__select acym__stats__select__language',
98 - ]
99 - );
85 + $data['emailVersionsFilters'] = [
86 + 'options' => $data['emailTranslations'],
87 + 'mailId' => $this->selectedMailIds[0],
88 + ];
100 89 }
101 90 }
102 91
103 - private function prepareClickStats(&$data)
92 + private function prepareClickStats(array &$data): void
104 93 {
105 - if (empty($data['selectedMailid'])) return;
94 + if (empty($data['selectedMailid'])) {
95 + return;
96 + }
106 97
107 98 $urlClickClass = new UrlClickClass();
108 99 $allClickInfo = $urlClickClass->getAllLinkFromEmail($this->selectedMailIds[0]);
109 100
@@ -124,9 +115,9 @@
124 115
125 116 $helperMailer = new MailerHelper();
126 117 if (!empty($data['mailInformation'])) {
127 118 $helperMailer->body = $data['mailInformation']->body;
128 - $helperMailer->statClick($data['mailInformation']->id, 0, true);
119 + $helperMailer->statClick(intval($data['mailInformation']->id), 0, true);
129 120 $data['mailInformation']->body = preg_replace('#&(amp;)?autoSubId=[^"]+"#Uis', '"', $helperMailer->body);
130 121 }
131 122
132 123 if (!empty($allPercentage)) {
@@ -150,91 +141,52 @@
150 141 $data['url_foundation_email'] = ACYM_CSS.'libraries/foundation_email.min.css?v='.filemtime(ACYM_MEDIA.'css'.DS.'libraries'.DS.'foundation_email.min.css');
151 142 $data['url_click_map_email'] = ACYM_CSS.'click_map.min.css?v='.filemtime(ACYM_MEDIA.'css'.DS.'click_map.min.css');
152 143 }
153 144
154 - public function prepareDefaultRoundCharts(&$data)
145 + public function prepareDefaultLineChart(array &$data): void
155 146 {
156 - $charts = [
157 - 'delivery' => [
158 - 'percentage' => 95,
159 - 'text' => 'ACYM_SUCCESSFULLY_SENT',
147 + $data['example_line_chart'] = [
148 + 'month' => [
149 + 'Jan 18' => ['open' => '150', 'click' => '40'],
160 150 ],
161 - 'open' => [
162 - 'percentage' => 25,
163 - 'text' => 'ACYM_OPEN_RATE',
151 + 'day' => [
152 + '23 Jan' => ['open' => '150', 'click' => '40'],
164 153 ],
165 - 'click' => [
166 - 'percentage' => 10,
167 - 'text' => 'ACYM_CLICK_RATE',
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'],
168 160 ],
169 - 'fail' => [
170 - 'percentage' => 2,
171 - 'text' => 'ACYM_BOUNCE_RATE',
172 - ],
173 - 'unsub' => [
174 - 'percentage' => 3,
175 - 'text' => 'ACYM_UNSUBSCRIBE',
176 - ],
177 161 ];
178 -
179 - $data['example_round_chart'] = '';
180 - foreach ($charts as $type => $oneChart) {
181 - if ($type == 'unsub' && empty($this->selectedMailIds)) continue;
182 - $data['example_round_chart'] .= '<div class="cell acym__stats__donut__one-chart">';
183 - $data['example_round_chart'] .= acym_roundChart(
184 - '',
185 - $oneChart['percentage'],
186 - $type,
187 - '',
188 - acym_translation($oneChart['text'])
189 - );
190 - $data['example_round_chart'] .= '</div>';
191 - }
192 162 }
193 163
194 - public function prepareDefaultLineChart(&$data)
164 + public function prepareDefaultDevicesChart(array &$data): void
195 165 {
196 - $dataMonth = [];
197 - $dataMonth['Jan 18'] = ['open' => '150', 'click' => '40'];
198 - $dataDay = [];
199 - $dataDay['23 Jan'] = ['open' => '150', 'click' => '40'];
200 - $dataHour = [];
201 - $dataHour['23 Jan 08:00'] = ['open' => '25', 'click' => '10'];
202 - $dataHour['23 Jan 09:00'] = ['open' => '50', 'click' => '10'];
203 - $dataHour['23 Jan 10:00'] = ['open' => '16', 'click' => '10'];
204 - $dataHour['23 Jan 11:00'] = ['open' => '59', 'click' => '10'];
205 - $data['example_line_chart'] = acym_lineChart('', $dataMonth, $dataDay, $dataHour);
166 + $data['example_devices_chart'] = [
167 + 'ACYM_MOBILE' => 25662,
168 + 'ACYM_DESKTOP' => 12471,
169 + 'ACYM_OTHER' => 3548,
170 + 'ACYM_UNKNOWN' => 6213,
171 + ];
206 172 }
207 173
208 - public function prepareDefaultDevicesChart(&$data)
174 + public function prepareDefaultBrowsersChart(array &$data): void
209 175 {
210 - $allDevices = array_merge(UserStatClass::DESKTOP_DEVICES, UserStatClass::MOBILE_DEVICES);
211 - $defaultData = [];
212 -
213 - for ($i = 0 ; $i < 10 ; $i++) {
214 - $oneDevice = array_rand($allDevices);
215 - $defaultData[$allDevices[$oneDevice]] = rand(20, 10000);
216 - }
217 -
218 - $data['example_devices_chart'] = acym_pieChart('', $defaultData, '', acym_translation('ACYM_DEVICES'));
219 - }
220 -
221 - public function prepareDefaultBrowsersChart(&$data)
222 - {
223 - $exampleData = [
224 - 'Google Chrome' => rand(20, 10000),
225 - 'Firefox' => rand(20, 10000),
226 - 'Safari' => rand(20, 10000),
227 - 'Microsoft Edge' => rand(20, 10000),
228 - 'Outlook' => rand(20, 10000),
229 - 'Apple Mail' => rand(20, 10000),
230 - 'Thunderbird' => rand(20, 10000),
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,
231 185 ];
232 -
233 - $data['example_source_chart'] = acym_pieChart('', $exampleData, '', acym_translation('ACYM_OPENED_WITH'));
234 186 }
235 187
236 - public function setDataForChartLine()
188 + public function setDataForChartLine(): void
237 189 {
238 190 $newStart = acym_date(acym_getVar('string', 'start'), 'Y-m-d H:i:s');
239 191 $newEnd = acym_date(acym_getVar('string', 'end'), 'Y-m-d H:i:s');
240 192 $mailIds = acym_getVar('int', 'id');
@@ -259,27 +211,63 @@
259 211
260 212 $statsCampaignSelected = new \stdClass();
261 213 $this->prepareLineChart($statsCampaignSelected, $mailIds, $newStart, $newEnd);
262 214
263 - echo @acym_lineChart('', $statsCampaignSelected->month, $statsCampaignSelected->day, $statsCampaignSelected->hour, true);
215 + acym_lineChart($statsCampaignSelected->month, $statsCampaignSelected->day, $statsCampaignSelected->hour, true);
264 216 exit;
265 217 }
266 218
267 - private function getValues($modifier, $intervalCode, $campaignOpens, $campaignClicks, $dateCode, $hour = false)
219 + private function getValues(string $modifier, string $intervalCode, array $campaignOpens, array $campaignClicks, string $dateCode, bool $hour = false): array
268 220 {
269 221 $opens = [];
270 222 foreach ($campaignOpens as $one) {
271 - $opens[acym_date(acym_getTime($one->open_date), $dateCode)] = $one->open;
223 + if (!empty($one->open_date)) {
224 + $opens[acym_date(acym_getTimeFromUTCDate($one->open_date), $dateCode)] = $one->open;
225 + }
272 226 }
273 227
274 228 $clicks = [];
275 229 foreach ($campaignClicks as $one) {
276 - $clicks[acym_date(acym_getTime($one->date_click), $dateCode)] = $one->click;
230 + if (!empty($one->date_click)) {
231 + $clicks[acym_date(acym_getTimeFromUTCDate($one->date_click), $dateCode)] = $one->click;
232 + }
277 233 }
278 234
279 - $begin = new \DateTime(empty($campaignClicks) ? $campaignOpens[0]->open_date : min([$campaignOpens[0]->open_date, $campaignClicks[0]->date_click]));
280 - $end = new \DateTime(empty($campaignClicks) ? end($campaignOpens)->open_date : max([end($campaignOpens)->open_date, end($campaignClicks)->date_click]));
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 + }
281 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 +
282 270 $end->modify('+1 '.$modifier);
283 271
284 272 $interval = new \DateInterval($intervalCode);
285 273 $daterange = new \DatePeriod($begin, $interval, $end);
@@ -298,21 +286,24 @@
298 286
299 287 return $result;
300 288 }
301 289
302 - public function prepareLineChart(&$statsCampaignSelected, $mailIdsOfCampaign, $newStart = '', $newEnd = '')
290 + public function prepareLineChart(object &$statsCampaignSelected, array $mailIdsOfCampaign, string $newStart = '', string $newEnd = ''): void
303 291 {
304 - $campaignClass = new CampaignClass();
305 292 $statsCampaignSelected->hasStats = true;
293 + $anonymousStats = !empty($this->config->get('anonymous_stats', 0));
306 294
307 - $campaignOpenByMonth = $campaignClass->getOpenByMonth($mailIdsOfCampaign, $newStart, $newEnd);
308 - $campaignOpenByDay = $campaignClass->getOpenByDay($mailIdsOfCampaign, $newStart, $newEnd);
309 - $campaignOpenByHour = $campaignClass->getOpenByHour($mailIdsOfCampaign, $newStart, $newEnd);
310 -
311 - if (empty($campaignOpenByMonth) || empty($campaignOpenByDay) || empty($campaignOpenByHour)) {
312 - $statsCampaignSelected->hasStats = false;
313 -
314 - return;
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);
315 306 }
316 307
317 308 $urlClickClass = new UrlClickClass();
318 309 $campaignClickByMonth = $urlClickClass->getAllClickByMailMonth($mailIdsOfCampaign, $newStart, $newEnd);
@@ -318,8 +309,17 @@
318 309 $campaignClickByMonth = $urlClickClass->getAllClickByMailMonth($mailIdsOfCampaign, $newStart, $newEnd);
319 310 $campaignClickByDay = $urlClickClass->getAllClickByMailDay($mailIdsOfCampaign, $newStart, $newEnd);
320 311 $campaignClickByHour = $urlClickClass->getAllClickByMailHour($mailIdsOfCampaign, $newStart, $newEnd);
321 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 322 $statsCampaignSelected->month = $this->getValues('day', 'P1M', $campaignOpenByMonth, $campaignClickByMonth, 'Y-m');
323 323 $statsCampaignSelected->day = $this->getValues('hour', 'P1D', $campaignOpenByDay, $campaignClickByDay, 'Y-m-d');
324 324 $statsCampaignSelected->hour = $this->getValues('min', 'PT1H', $campaignOpenByHour, $campaignClickByHour, 'Y-m-d H:i', true);
325 325
@@ -324,15 +324,20 @@
324 324 $statsCampaignSelected->hour = $this->getValues('min', 'PT1H', $campaignOpenByHour, $campaignClickByHour, 'Y-m-d H:i', true);
325 325
326 326 $allHour = array_keys($statsCampaignSelected->hour);
327 327
328 + //those are the dates when the first was open and the last was open
328 329 $statsCampaignSelected->startEndDateHour = [];
329 - $statsCampaignSelected->startEndDateHour['start'] = $allHour[0];
330 - $statsCampaignSelected->startEndDateHour['end'] = end($allHour);
330 + if (!empty($allHour)) {
331 + $statsCampaignSelected->startEndDateHour['start'] = $allHour[0];
332 + $statsCampaignSelected->startEndDateHour['end'] = end($allHour);
333 + }
331 334 }
332 335
333 - public function exportGlobal()
336 + public function exportGlobal(): void
334 337 {
338 + wp_verify_nonce(acym_getVar('cmd', '_wpnonce'), 'acymnonce') || die('Invalid Token');
339 +
335 340 $exportType = acym_getVar('string', 'export_type', 'charts');
336 341
337 342 $functionName = 'exportGlobal'.ucfirst($exportType);
338 343
@@ -345,27 +350,33 @@
345 350
346 351 $this->$functionName();
347 352 }
348 353
349 - public function prepareOpenTimeChart(&$data)
354 + public function prepareOpenTimeChart(array &$data): void
350 355 {
351 356 $userStatClass = new UserStatClass();
352 - $statsDB = $userStatClass->getOpenTimeStats($this->selectedMailIds);
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 + }
353 363
354 364 if (empty($statsDB['total_open'])) {
355 365 $data['openTime'] = $userStatClass->getDefaultStat();
356 366 $data['empty_open'] = true;
357 367
358 - return true;
368 + return;
359 369 }
370 +
360 371 $data['empty_open'] = false;
361 372
362 373 $stats = [];
363 374
364 - for ($day = 0 ; $day < 7 ; $day++) {
375 + for ($day = 0; $day < 7; $day++) {
365 376 $stats[$day] = [];
366 - for ($hour = 0 ; $hour < 8 ; $hour++) {
367 - if (empty($statsDB['stats'][$day.'_'.$hour]) || empty($statsDB['total_open'])) {
377 + for ($hour = 0; $hour < 8; $hour++) {
378 + if (empty($statsDB['stats'][$day.'_'.$hour])) {
368 379 $percentage = 0;
369 380 } else {
370 381 $percentage = ($statsDB['stats'][$day.'_'.$hour]->open_total * 100) / $statsDB['total_open'];
371 382 }
@@ -373,26 +384,21 @@
373 384 }
374 385 }
375 386
376 387 $data['openTime'] = $stats;
377 -
378 - return true;
379 388 }
380 389
381 - public function prepareStatByList(&$data)
390 + public function prepareStatByList(array &$data): void
382 391 {
383 - $mailid = (count(array_unique($data['selectedMailid'])) != 1) ? 0 : $data['selectedMailid'][0];
392 + $selectedMailIds = array_unique($data['selectedMailid']);
393 + $mailid = count($selectedMailIds) !== 1 ? 0 : (int)reset($selectedMailIds);
384 394
385 395 if (empty($mailid)) {
386 396 return;
387 397 }
388 398
389 - $mailSelected = $this->getVarFiltersListing('int', 'mail_id_language', $mailid);
399 + $mailSelected = (int)$this->getVarFiltersListing('int', 'mail_id_language', $mailid);
390 400
391 - $mailClass = new MailClass();
392 - $mail = $mailClass->getOneById($mailSelected);
393 - $mainMailId = empty($mail->parent_id) ? $mail->id : $mail->parent_id;
394 -
395 401 $data['lists'] = [];
396 402 $data['emailsSent'] = [];
397 403 $data['emailsOpen'] = [];
398 404 $data['bounces'] = [];
@@ -400,16 +406,32 @@
400 406 $data['click'] = [];
401 407 $data['listsStats'] = [];
402 408 $data['userPerList'] = [];
403 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
404 425 $query = 'SELECT l.id, l.name, l.color FROM #__acym_list l';
405 426 $query .= ' JOIN #__acym_mail_has_list ml on l.id = ml.list_id';
406 - $query .= ' WHERE ml.mail_id = '.$mainMailId;
427 + $query .= ' WHERE ml.mail_id = '.intval($mainMailId);
407 428
408 429 $data['lists'] = acym_loadObjectList($query, 'id');
409 430 $listIds = implode(',', array_keys($data['lists']));
410 431
411 432 if (!empty($listIds)) {
433 + //get nbSent, nbOpen, nbBounce, nbUnsubscribe
412 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';
413 435 $query .= ' JOIN #__acym_user_stat us on ul.user_id = us.user_id';
414 436 $query .= ' WHERE ul.list_id in ('.$listIds.') AND us.mail_id = '.$mailSelected;
415 437 $query .= ' GROUP BY ul.list_id;';
@@ -414,8 +436,9 @@
414 436 $query .= ' WHERE ul.list_id in ('.$listIds.') AND us.mail_id = '.$mailSelected;
415 437 $query .= ' GROUP BY ul.list_id;';
416 438 $data['listsStats'] = acym_loadObjectList($query, 'list_id');
417 439
440 + //get nbClick
418 441 $query = 'SELECT ul.list_id, COUNT(uc.click) as nbClick';
419 442 $query .= ' FROM #__acym_user_has_list ul ';
420 443 $query .= ' JOIN #__acym_url_click uc on ul.user_id = uc.user_id';
421 444 $query .= ' WHERE ul.list_id in ('.$listIds.') AND uc.mail_id = '.$mailSelected;
@@ -422,8 +445,9 @@
422 445 $query .= ' GROUP BY ul.list_id;';
423 446
424 447 $data['nbClick'] = acym_loadObjectList($query, 'list_id');
425 448
449 + //get nbUser per list
426 450 $query = 'SELECT ul.list_id, COUNT(ul.user_id) as nbUser';
427 451 $query .= ' FROM #__acym_user_has_list ul';
428 452 $query .= ' JOIN #__acym_user_stat us ON ul.user_id = us.user_id';
429 453 $query .= ' WHERE ul.list_id in ('.$listIds.') AND us.mail_id = '.$mailSelected;
@@ -431,9 +455,12 @@
431 455
432 456 $data['userPerList'] = acym_loadObjectList($query, 'list_id');
433 457 }
434 458
459 + //format data for barChart
435 460 foreach ($data['listsStats'] as $listId => $item) {
461 + $nbSent = empty($item->nbSent) ? 1 : $item->nbSent;
462 +
436 463 $data['emailsSent'][$listId] = [
437 464 'label' => $data['lists'][$listId]->name,
438 465 'value' => $item->nbSent,
439 466 'color' => $data['lists'][$listId]->color,
@@ -439,19 +466,19 @@
439 466 'color' => $data['lists'][$listId]->color,
440 467 ];
441 468 $data['emailsOpen'][$listId] = [
442 469 'label' => $data['lists'][$listId]->name,
443 - 'value' => round(($item->nbOpen * 100) / $item->nbSent),
470 + 'value' => round(($item->nbOpen * 100) / $nbSent),
444 471 'color' => $data['lists'][$listId]->color,
445 472 ];
446 473 $data['bounces'][$listId] = [
447 474 'label' => $data['lists'][$listId]->name,
448 - 'value' => round(($item->nbBounce * 100) / $item->nbSent),
475 + 'value' => round(($item->nbBounce * 100) / $nbSent),
449 476 'color' => $data['lists'][$listId]->color,
450 477 ];
451 478 $data['unsubscribed'][$listId] = [
452 479 'label' => $data['lists'][$listId]->name,
453 - 'value' => round(($item->nbUnsub * 100) / $item->nbSent),
480 + 'value' => round(($item->nbUnsub * 100) / $nbSent),
454 481 'color' => $data['lists'][$listId]->color,
455 482 ];
456 483 }
457 484
@@ -456,8 +483,10 @@
456 483 }
457 484
458 485 foreach ($data['userPerList'] as $listId => $item) {
459 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
460 489 $data['click'][$listId] = ['label' => $data['lists'][$listId]->name, 'value' => 0, 'color' => $data['lists'][$listId]->color];
461 490
462 491 if ($nbUser > 0 && !empty($data['nbClick'][$listId]->nbClick)) {
463 492 $data['click'][$listId]['value'] = round(($data['nbClick'][$listId]->nbClick * 100) / $nbUser);
@@ -464,9 +493,9 @@
464 493 }
465 494 }
466 495 }
467 496
468 - private function prepareAbTestMails(&$data)
497 + private function prepareAbTestMails(array &$data): void
469 498 {
470 499 if (empty($this->selectedMailIds)) return;
471 500
472 501 $mailClass = new MailClass();
@@ -487,9 +516,9 @@
487 516 $data['emailVersions'][$email->id] = $email->name.' - '.$email->subject;
488 517 }
489 518 }
490 519
491 - private function prepareMultilingualMails(&$data)
520 + private function prepareMultilingualMails(array &$data): void
492 521 {
493 522 if (empty($this->selectedMailIds)) return;
494 523
495 524 $mailClass = new MailClass();
@@ -514,49 +543,78 @@
514 543 }
515 544 }
516 545 }
517 546
518 - private function prepareOpenSourcesStats(&$data)
547 + private function prepareDevicesStats(array &$data): void
519 548 {
520 - $userStatClass = new UserStatClass();
521 - $openedFromStats = $userStatClass->getOpenSourcesStats($this->selectedMailIds);
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 + }
522 556
523 - $formattedSources = [];
524 - foreach ($openedFromStats as $oneSource) {
525 - if (empty($oneSource->number)) continue;
557 + $defaultDataDevices = [
558 + 'ACYM_MOBILE' => 0,
559 + 'ACYM_DESKTOP' => 0,
560 + 'ACYM_OTHER' => 0,
561 + 'ACYM_UNKNOWN' => 0,
562 + ];
563 + $data['devices'] = $defaultDataDevices;
526 564
527 - if (empty($oneSource->opened_with)) $oneSource->opened_with = acym_translation('ACYM_UNKNOWN');
528 - $formattedSources[$oneSource->opened_with] = $oneSource->number;
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;
529 581 }
530 582
531 - $data['openedWith'] = $formattedSources;
583 + if ($data['devices'] === $defaultDataDevices) {
584 + $data['devices'] = [];
585 + }
532 586 }
533 587
534 - private function prepareDevicesStats(&$data)
588 + private function prepareOpenSourcesStats(array &$data): void
535 589 {
536 - $campaignClass = new CampaignClass();
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 + }
537 597
538 - $devicesCampaign = $campaignClass->getDevicesWithCountByMailId($this->selectedMailIds);
598 + $data['openedWith'] = [];
599 + foreach ($openedFromStats as $oneSource) {
600 + if (empty($oneSource->number)) {
601 + continue;
602 + }
539 603
540 - $formattedDevices = [];
541 - foreach ($devicesCampaign as $oneDevice) {
542 - if (empty($oneDevice->number)) continue;
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 + }
543 607
544 - if (in_array($oneDevice->device, array_keys(UserStatClass::MOBILE_DEVICES))) {
545 - $deviceName = UserStatClass::MOBILE_DEVICES[$oneDevice->device];
546 - } elseif (in_array($oneDevice->device, array_keys(UserStatClass::DESKTOP_DEVICES))) {
547 - $deviceName = UserStatClass::DESKTOP_DEVICES[$oneDevice->device];
548 - } else {
549 - $deviceName = acym_translation('ACYM_UNKNOWN');
608 + if (empty($oneSource->opened_with)) {
609 + $oneSource->opened_with = 'ACYM_UNKNOWN';
550 610 }
551 611
552 - $formattedDevices[$deviceName] = $oneDevice->number;
612 + $data['openedWith'][$oneSource->opened_with] = $oneSource->number;
553 613 }
554 -
555 - $data['devices'] = $formattedDevices;
556 614 }
557 615
558 - public function preparecharts(&$data)
616 + public function preparecharts(array &$data): void
559 617 {
560 618 $mailStatClass = new MailStatClass();
561 619
562 620 $data['mail'] = $mailStatClass->getSentFailByMailIds($this->selectedMailIds);
@@ -564,8 +622,9 @@
564 622
565 623 $campaignClass = new CampaignClass();
566 624 $urlClickClass = new UrlClickClass();
567 625
626 + //For the total opening, the doughnut chart
568 627 $data['mail']->totalMail = $data['mail']->sent + $data['mail']->fail;
569 628 $data['mail']->percentageSent = empty($data['mail']->totalMail) ? 0 : number_format(($data['mail']->sent * 100) / $data['mail']->totalMail, 2);
570 629 $data['mail']->allSent = empty($data['mail']->totalMail)
571 630 ? acym_translationSprintf('ACYM_X_MAIL_SUCCESSFULLY_SENT_OF_X', 0, 0)
@@ -574,28 +633,47 @@
574 633 $data['mail']->sent,
575 634 $data['mail']->totalMail
576 635 );
577 636
578 - $openRateCampaign = empty($this->selectedMailIds) ? $campaignClass->getOpenRateAllCampaign() : $campaignClass->getOpenRateCampaigns($this->selectedMailIds);
579 - $data['mail']->percentageOpen = empty($openRateCampaign->sent) ? 0 : number_format(($openRateCampaign->open_unique * 100) / $openRateCampaign->sent, 2);
580 - $data['mail']->allOpen = empty($openRateCampaign->sent)
581 - ? acym_translationSprintf('ACYM_X_MAIL_OPENED_OF_X', 0, 0)
582 - : acym_translationSprintf(
583 - 'ACYM_X_MAIL_OPENED_OF_X',
584 - $openRateCampaign->open_unique,
585 - $openRateCampaign->sent
586 - );
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;
587 640
588 - $clickRateCampaign = $urlClickClass->getNumberUsersClicked($this->selectedMailIds);
589 - $data['mail']->percentageClick = empty($data['mail']->sent) ? 0 : number_format(($clickRateCampaign * 100) / $data['mail']->sent, 2);
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);
590 671 $data['mail']->allClick = empty($data['mail']->sent)
591 - ? acym_translationSprintf('ACYM_X_MAIL_CLICKED_OF_X', 0, 0)
592 - : acym_translationSprintf(
593 - 'ACYM_X_MAIL_CLICKED_OF_X',
594 - $clickRateCampaign,
595 - $data['mail']->sent
596 - );
672 + ? acym_translationSprintf($clickLabelKey, 0, 0)
673 + : acym_translationSprintf($clickLabelKey, $clickCount, $data['mail']->sent);
597 674
675 + //bounce rate
598 676 $bounceRateCampaign = empty($this->selectedMailIds) ? $campaignClass->getBounceRateAllCampaign() : $campaignClass->getBounceRateCampaigns($this->selectedMailIds);
599 677 $data['mail']->percentageBounce = empty($data['mail']->sent) ? 0 : number_format(($bounceRateCampaign->bounce_unique * 100) / $data['mail']->sent, 2);
600 678 $data['mail']->allBounce = empty($data['mail']->sent)
601 679 ? acym_translationSprintf('ACYM_X_BOUNCE_OF_X', 0, 0)
@@ -605,25 +683,31 @@
605 683 $data['mail']->sent
606 684 );
607 685
608 686 if (!empty($this->selectedMailIds)) {
687 + //unsubscribe rate
609 688 $mailStat = $mailStatClass->getByMailIds($this->selectedMailIds);
610 - $data['mail']->percentageUnsub = empty($data['mail']->sent) ? 0 : number_format(($mailStat->unsubscribe_total * 100) / $data['mail']->sent, 2);
611 - $data['mail']->allUnsub = empty($data['mail']->sent)
612 - ? acym_translationSprintf('ACYM_X_USERS_UNSUBSCRIBED_OF_X', 0, 0)
613 - : acym_translationSprintf(
614 - 'ACYM_X_USERS_UNSUBSCRIBED_OF_X',
615 - $mailStat->unsubscribe_total,
616 - $data['mail']->sent
617 - );
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 + }
618 701 }
619 702
620 703 $this->prepareDevicesStats($data);
621 704 $this->prepareOpenSourcesStats($data);
622 705 $this->prepareLineChart($data['mail'], $this->selectedMailIds);
706 + $this->prepareComparedStats($data);
623 707 }
624 708
625 - private function decode(&$detailedStats, $columnsToDecode = ['mail_subject', 'mail_name'])
709 + private function decode(array &$detailedStats, array $columnsToDecode = ['mail_subject', 'mail_name']): void
626 710 {
627 711 foreach ($detailedStats as $oneDetailedStat) {
628 712 foreach ($columnsToDecode as $column) {
629 713 if (!empty($oneDetailedStat->$column)) {
@@ -632,13 +716,13 @@
632 716 }
633 717 }
634 718 }
635 719
636 - private function prepareLinksDetailsListing(&$data)
720 + private function prepareLinksDetailsListing(array &$data): void
637 721 {
638 722 $data['search'] = $this->getVarFiltersListing('string', 'links_details_search', '');
639 - $data['ordering'] = $this->getVarFiltersListing('string', 'links_details_ordering', 'id');
640 - $data['orderingSortOrder'] = $this->getVarFiltersListing('string', 'links_details_ordering_sort_order', 'desc');
723 + $data['ordering'] = $this->getVarFiltersListing('cmd', 'links_details_ordering', 'id');
724 + $data['orderingSortOrder'] = $this->getVarFiltersListing('cmd', 'links_details_ordering_sort_order', 'desc');
641 725
642 726 if (empty($this->selectedMailIds)) return;
643 727
644 728 $pagination = new PaginationHelper();
@@ -659,9 +743,10 @@
659 743 );
660 744
661 745 $this->decode($urlClicks['links_details']);
662 746
663 - $pagination->setStatus($urlClicks['total'], $page, $detailedStatsPerPage);
747 + // Prepare the pagination
748 + $pagination->setStatus((int)$urlClicks['total']->total, $page, $detailedStatsPerPage);
664 749
665 750 $data['pagination'] = $pagination;
666 751 $data['links_details'] = $urlClicks['links_details'];
667 752 $data['query'] = $urlClicks['query'];
@@ -666,9 +751,9 @@
666 751 $data['links_details'] = $urlClicks['links_details'];
667 752 $data['query'] = $urlClicks['query'];
668 753 }
669 754
670 - private function prepareListReceivers(&$data)
755 + private function prepareListReceivers(array &$data): void
671 756 {
672 757 if (empty($this->selectedMailIds)) return;
673 758
674 759 $mailStatClass = new MailStatClass();
@@ -674,12 +759,12 @@
674 759 $mailStatClass = new MailStatClass();
675 760 $mailClass = new MailClass();
676 761
677 762 $data['mailStat'] = $mailStatClass->getByMailIds($this->selectedMailIds);
678 - $data['lists'] = $mailClass->getAllListsByMailId($this->selectedMailIds);
763 + $data['lists'] = $mailClass->getAllListsByMailId($this->selectedMailIds[0]);
679 764 }
680 765
681 - private function exportGlobalFormatted()
766 + private function exportGlobalFormatted(): void
682 767 {
683 768 $exportHelper = new ExportHelper();
684 769 $data = [];
685 770 $this->prepareDefaultPageInfo($data);
@@ -686,13 +771,11 @@
686 771 $data['show_date_filters'] = true;
687 772 $data['page_title'] = false;
688 773 $timeLinechart = acym_getVar('string', 'time_linechart', 'month');
689 774
690 -
691 775 $this->prepareMailFilter($data);
692 776 $this->prepareClickStats($data);
693 777 $this->preparecharts($data);
694 - $this->prepareDefaultRoundCharts($data);
695 778 $this->prepareDefaultLineChart($data);
696 779 $this->prepareDefaultDevicesChart($data);
697 780 $this->prepareDefaultBrowsersChart($data);
698 781
@@ -709,9 +792,9 @@
709 792 $exportHelper->exportStatsFormattedCSV($mailName, $globalDonut, $globalLine, $timeLinechart);
710 793 exit;
711 794 }
712 795
713 - private function exportGlobalFull()
796 + private function exportGlobalFull(): void
714 797 {
715 798 $exportHelper = new ExportHelper();
716 799 $data = [];
717 800 $this->prepareDefaultPageInfo($data);
@@ -716,9 +799,11 @@
716 799 $data = [];
717 800 $this->prepareDefaultPageInfo($data);
718 801
719 802 $where = '';
720 - if (!empty($this->selectedMailIds)) $where = 'WHERE mail_id IN ('.implode(',', $this->selectedMailIds).')';
803 + if (!empty($this->selectedMailIds)) {
804 + $where = 'WHERE mail_id IN ('.implode(',', $this->selectedMailIds).')';
805 + }
721 806
722 807 $columnsMailStat = acym_getColumns('mail_stat');
723 808 $columnsToExport = [];
724 809
@@ -742,6 +827,118 @@
742 827
743 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;
744 829 $exportHelper->exportStatsFullCSV($query, $columnsToExport);
745 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 + }
746 943 }
747 944 }