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 +205 -143 10.2.2 → 11.1.0 View file →
@@ -26,9 +26,8 @@
26 26
27 27 $this->prepareOpenTimeChart($data);
28 28 $this->preparecharts($data);
29 29 $this->prepareListReceivers($data);
30 - $this->prepareDefaultRoundCharts($data);
31 30 $this->prepareDefaultLineChart($data);
32 31 $this->prepareDefaultDevicesChart($data);
33 32 $this->prepareDefaultBrowsersChart($data);
34 33 if (count($this->selectedMailIds) == 1) {
@@ -72,36 +71,22 @@
72 71 if (!empty($this->selectedMailIds)) {
73 72 $attributes['data-selected'] = implode(',', $this->selectedMailIds);
74 73 }
75 74
76 - $data['mail_filter'] = acym_selectMultiple(
77 - [],
78 - 'mail_ids',
79 - [],
80 - $attributes
81 - );
75 + $data['mail_filter_options'] = $attributes;
76 + $data['emailVersionsFilters'] = [];
82 77
83 - $data['emailVersionsFilters'] = '';
84 -
85 78 if (!empty($data['emailVersions'])) {
86 - $data['emailVersionsFilters'] = acym_select(
87 - $data['emailVersions'],
88 - 'mail_id_version',
89 - $this->selectedMailIds[0],
90 - [
91 - 'class' => 'acym__select acym__stats__select__language',
92 - ]
93 - );
79 + $data['emailVersionsFilters'] = [
80 + 'options' => $data['emailVersions'],
81 + 'mailId' => $this->selectedMailIds[0],
82 + ];
94 83 }
95 84 if (!empty($data['emailTranslations'])) {
96 - $data['emailVersionsFilters'] = acym_select(
97 - $data['emailTranslations'],
98 - 'mail_id_version',
99 - $this->selectedMailIds[0],
100 - [
101 - 'class' => 'acym__select acym__stats__select__language',
102 - ]
103 - );
85 + $data['emailVersionsFilters'] = [
86 + 'options' => $data['emailTranslations'],
87 + 'mailId' => $this->selectedMailIds[0],
88 + ];
104 89 }
105 90 }
106 91
107 92 private function prepareClickStats(array &$data): void
@@ -156,76 +141,40 @@
156 141 $data['url_foundation_email'] = ACYM_CSS.'libraries/foundation_email.min.css?v='.filemtime(ACYM_MEDIA.'css'.DS.'libraries'.DS.'foundation_email.min.css');
157 142 $data['url_click_map_email'] = ACYM_CSS.'click_map.min.css?v='.filemtime(ACYM_MEDIA.'css'.DS.'click_map.min.css');
158 143 }
159 144
160 - public function prepareDefaultRoundCharts(array &$data): void
145 + public function prepareDefaultLineChart(array &$data): void
161 146 {
162 - $charts = [
163 - 'delivery' => [
164 - 'percentage' => 95,
165 - 'text' => 'ACYM_SUCCESSFULLY_SENT',
147 + $data['example_line_chart'] = [
148 + 'month' => [
149 + 'Jan 18' => ['open' => '150', 'click' => '40'],
166 150 ],
167 - 'open' => [
168 - 'percentage' => 25,
169 - 'text' => 'ACYM_OPEN_RATE',
151 + 'day' => [
152 + '23 Jan' => ['open' => '150', 'click' => '40'],
170 153 ],
171 - 'click' => [
172 - 'percentage' => 10,
173 - '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'],
174 160 ],
175 - 'fail' => [
176 - 'percentage' => 2,
177 - 'text' => 'ACYM_BOUNCE_RATE',
178 - ],
179 - 'unsub' => [
180 - 'percentage' => 3,
181 - 'text' => 'ACYM_UNSUBSCRIBE',
182 - ],
183 161 ];
184 -
185 - $data['example_round_chart'] = '';
186 - foreach ($charts as $type => $oneChart) {
187 - if ($type == 'unsub' && empty($this->selectedMailIds)) continue;
188 - $data['example_round_chart'] .= '<div class="cell acym__stats__donut__one-chart">';
189 - $data['example_round_chart'] .= acym_roundChart(
190 - $oneChart['percentage'],
191 - $type,
192 - '',
193 - acym_translation($oneChart['text'])
194 - );
195 - $data['example_round_chart'] .= '</div>';
196 - }
197 162 }
198 163
199 - public function prepareDefaultLineChart(array &$data): void
200 - {
201 - $dataMonth = [];
202 - $dataMonth['Jan 18'] = ['open' => '150', 'click' => '40'];
203 - $dataDay = [];
204 - $dataDay['23 Jan'] = ['open' => '150', 'click' => '40'];
205 - $dataHour = [];
206 - $dataHour['23 Jan 08:00'] = ['open' => '25', 'click' => '10'];
207 - $dataHour['23 Jan 09:00'] = ['open' => '50', 'click' => '10'];
208 - $dataHour['23 Jan 10:00'] = ['open' => '16', 'click' => '10'];
209 - $dataHour['23 Jan 11:00'] = ['open' => '59', 'click' => '10'];
210 - $data['example_line_chart'] = acym_lineChart($dataMonth, $dataDay, $dataHour);
211 - }
212 -
213 164 public function prepareDefaultDevicesChart(array &$data): void
214 165 {
215 - $defaultData = [
166 + $data['example_devices_chart'] = [
216 167 'ACYM_MOBILE' => 25662,
217 168 'ACYM_DESKTOP' => 12471,
218 169 'ACYM_OTHER' => 3548,
219 170 'ACYM_UNKNOWN' => 6213,
220 171 ];
221 -
222 - $data['example_devices_chart'] = acym_pieChart($defaultData, '', acym_translation('ACYM_DEVICES'));
223 172 }
224 173
225 174 public function prepareDefaultBrowsersChart(array &$data): void
226 175 {
227 - $exampleData = [
176 + $data['example_source_chart'] = [
228 177 'Apple Mail' => 15835,
229 178 'Google Chrome' => 13375,
230 179 'Safari' => 2667,
231 180 'Outlook' => 2458,
@@ -233,10 +182,8 @@
233 182 'Firefox' => 826,
234 183 'ACYM_OTHER' => 4775,
235 184 'ACYM_UNKNOWN' => 6123,
236 185 ];
237 -
238 - $data['example_source_chart'] = acym_pieChart($exampleData, '', acym_translation('ACYM_OPENED_WITH'));
239 186 }
240 187
241 188 public function setDataForChartLine(): void
242 189 {
@@ -264,9 +211,9 @@
264 211
265 212 $statsCampaignSelected = new \stdClass();
266 213 $this->prepareLineChart($statsCampaignSelected, $mailIds, $newStart, $newEnd);
267 214
268 - echo acym_lineChart($statsCampaignSelected->month, $statsCampaignSelected->day, $statsCampaignSelected->hour, true);
215 + acym_lineChart($statsCampaignSelected->month, $statsCampaignSelected->day, $statsCampaignSelected->hour, true);
269 216 exit;
270 217 }
271 218
272 219 private function getValues(string $modifier, string $intervalCode, array $campaignOpens, array $campaignClicks, string $dateCode, bool $hour = false): array
@@ -272,19 +219,55 @@
272 219 private function getValues(string $modifier, string $intervalCode, array $campaignOpens, array $campaignClicks, string $dateCode, bool $hour = false): array
273 220 {
274 221 $opens = [];
275 222 foreach ($campaignOpens as $one) {
276 - $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 + }
277 226 }
278 227
279 228 $clicks = [];
280 229 foreach ($campaignClicks as $one) {
281 - $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 + }
282 233 }
283 234
284 - $begin = new \DateTime(empty($campaignClicks) ? $campaignOpens[0]->open_date : min([$campaignOpens[0]->open_date, $campaignClicks[0]->date_click]));
285 - $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 + }
286 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 +
287 270 $end->modify('+1 '.$modifier);
288 271
289 272 $interval = new \DateInterval($intervalCode);
290 273 $daterange = new \DatePeriod($begin, $interval, $end);
@@ -305,19 +288,22 @@
305 288 }
306 289
307 290 public function prepareLineChart(object &$statsCampaignSelected, array $mailIdsOfCampaign, string $newStart = '', string $newEnd = ''): void
308 291 {
309 - $campaignClass = new CampaignClass();
310 292 $statsCampaignSelected->hasStats = true;
293 + $anonymousStats = !empty($this->config->get('anonymous_stats', 0));
311 294
312 - $campaignOpenByMonth = $campaignClass->getOpenByMonth($mailIdsOfCampaign, $newStart, $newEnd);
313 - $campaignOpenByDay = $campaignClass->getOpenByDay($mailIdsOfCampaign, $newStart, $newEnd);
314 - $campaignOpenByHour = $campaignClass->getOpenByHour($mailIdsOfCampaign, $newStart, $newEnd);
315 -
316 - if (empty($campaignOpenByMonth) || empty($campaignOpenByDay) || empty($campaignOpenByHour)) {
317 - $statsCampaignSelected->hasStats = false;
318 -
319 - 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);
320 306 }
321 307
322 308 $urlClickClass = new UrlClickClass();
323 309 $campaignClickByMonth = $urlClickClass->getAllClickByMailMonth($mailIdsOfCampaign, $newStart, $newEnd);
@@ -323,8 +309,17 @@
323 309 $campaignClickByMonth = $urlClickClass->getAllClickByMailMonth($mailIdsOfCampaign, $newStart, $newEnd);
324 310 $campaignClickByDay = $urlClickClass->getAllClickByMailDay($mailIdsOfCampaign, $newStart, $newEnd);
325 311 $campaignClickByHour = $urlClickClass->getAllClickByMailHour($mailIdsOfCampaign, $newStart, $newEnd);
326 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 +
327 322 $statsCampaignSelected->month = $this->getValues('day', 'P1M', $campaignOpenByMonth, $campaignClickByMonth, 'Y-m');
328 323 $statsCampaignSelected->day = $this->getValues('hour', 'P1D', $campaignOpenByDay, $campaignClickByDay, 'Y-m-d');
329 324 $statsCampaignSelected->hour = $this->getValues('min', 'PT1H', $campaignOpenByHour, $campaignClickByHour, 'Y-m-d H:i', true);
330 325
@@ -329,15 +324,20 @@
329 324 $statsCampaignSelected->hour = $this->getValues('min', 'PT1H', $campaignOpenByHour, $campaignClickByHour, 'Y-m-d H:i', true);
330 325
331 326 $allHour = array_keys($statsCampaignSelected->hour);
332 327
328 + //those are the dates when the first was open and the last was open
333 329 $statsCampaignSelected->startEndDateHour = [];
334 - $statsCampaignSelected->startEndDateHour['start'] = $allHour[0];
335 - $statsCampaignSelected->startEndDateHour['end'] = end($allHour);
330 + if (!empty($allHour)) {
331 + $statsCampaignSelected->startEndDateHour['start'] = $allHour[0];
332 + $statsCampaignSelected->startEndDateHour['end'] = end($allHour);
333 + }
336 334 }
337 335
338 336 public function exportGlobal(): void
339 337 {
338 + wp_verify_nonce(acym_getVar('cmd', '_wpnonce'), 'acymnonce') || die('Invalid Token');
339 +
340 340 $exportType = acym_getVar('string', 'export_type', 'charts');
341 341
342 342 $functionName = 'exportGlobal'.ucfirst($exportType);
343 343
@@ -353,9 +353,14 @@
353 353
354 354 public function prepareOpenTimeChart(array &$data): void
355 355 {
356 356 $userStatClass = new UserStatClass();
357 - $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 + }
358 363
359 364 if (empty($statsDB['total_open'])) {
360 365 $data['openTime'] = $userStatClass->getDefaultStat();
361 366 $data['empty_open'] = true;
@@ -361,16 +366,17 @@
361 366 $data['empty_open'] = true;
362 367
363 368 return;
364 369 }
370 +
365 371 $data['empty_open'] = false;
366 372
367 373 $stats = [];
368 374
369 - for ($day = 0 ; $day < 7 ; $day++) {
375 + for ($day = 0; $day < 7; $day++) {
370 376 $stats[$day] = [];
371 - for ($hour = 0 ; $hour < 8 ; $hour++) {
372 - if (empty($statsDB['stats'][$day.'_'.$hour]) || empty($statsDB['total_open'])) {
377 + for ($hour = 0; $hour < 8; $hour++) {
378 + if (empty($statsDB['stats'][$day.'_'.$hour])) {
373 379 $percentage = 0;
374 380 } else {
375 381 $percentage = ($statsDB['stats'][$day.'_'.$hour]->open_total * 100) / $statsDB['total_open'];
376 382 }
@@ -382,20 +388,17 @@
382 388 }
383 389
384 390 public function prepareStatByList(array &$data): void
385 391 {
386 - $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);
387 394
388 395 if (empty($mailid)) {
389 396 return;
390 397 }
391 398
392 - $mailSelected = $this->getVarFiltersListing('int', 'mail_id_language', $mailid);
399 + $mailSelected = (int)$this->getVarFiltersListing('int', 'mail_id_language', $mailid);
393 400
394 - $mailClass = new MailClass();
395 - $mail = $mailClass->getOneById($mailSelected);
396 - $mainMailId = empty($mail->parent_id) ? $mail->id : $mail->parent_id;
397 -
398 401 $data['lists'] = [];
399 402 $data['emailsSent'] = [];
400 403 $data['emailsOpen'] = [];
401 404 $data['bounces'] = [];
@@ -403,16 +406,32 @@
403 406 $data['click'] = [];
404 407 $data['listsStats'] = [];
405 408 $data['userPerList'] = [];
406 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
407 425 $query = 'SELECT l.id, l.name, l.color FROM #__acym_list l';
408 426 $query .= ' JOIN #__acym_mail_has_list ml on l.id = ml.list_id';
409 - $query .= ' WHERE ml.mail_id = '.$mainMailId;
427 + $query .= ' WHERE ml.mail_id = '.intval($mainMailId);
410 428
411 429 $data['lists'] = acym_loadObjectList($query, 'id');
412 430 $listIds = implode(',', array_keys($data['lists']));
413 431
414 432 if (!empty($listIds)) {
433 + //get nbSent, nbOpen, nbBounce, nbUnsubscribe
415 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';
416 435 $query .= ' JOIN #__acym_user_stat us on ul.user_id = us.user_id';
417 436 $query .= ' WHERE ul.list_id in ('.$listIds.') AND us.mail_id = '.$mailSelected;
418 437 $query .= ' GROUP BY ul.list_id;';
@@ -417,8 +436,9 @@
417 436 $query .= ' WHERE ul.list_id in ('.$listIds.') AND us.mail_id = '.$mailSelected;
418 437 $query .= ' GROUP BY ul.list_id;';
419 438 $data['listsStats'] = acym_loadObjectList($query, 'list_id');
420 439
440 + //get nbClick
421 441 $query = 'SELECT ul.list_id, COUNT(uc.click) as nbClick';
422 442 $query .= ' FROM #__acym_user_has_list ul ';
423 443 $query .= ' JOIN #__acym_url_click uc on ul.user_id = uc.user_id';
424 444 $query .= ' WHERE ul.list_id in ('.$listIds.') AND uc.mail_id = '.$mailSelected;
@@ -425,8 +445,9 @@
425 445 $query .= ' GROUP BY ul.list_id;';
426 446
427 447 $data['nbClick'] = acym_loadObjectList($query, 'list_id');
428 448
449 + //get nbUser per list
429 450 $query = 'SELECT ul.list_id, COUNT(ul.user_id) as nbUser';
430 451 $query .= ' FROM #__acym_user_has_list ul';
431 452 $query .= ' JOIN #__acym_user_stat us ON ul.user_id = us.user_id';
432 453 $query .= ' WHERE ul.list_id in ('.$listIds.') AND us.mail_id = '.$mailSelected;
@@ -434,9 +455,12 @@
434 455
435 456 $data['userPerList'] = acym_loadObjectList($query, 'list_id');
436 457 }
437 458
459 + //format data for barChart
438 460 foreach ($data['listsStats'] as $listId => $item) {
461 + $nbSent = empty($item->nbSent) ? 1 : $item->nbSent;
462 +
439 463 $data['emailsSent'][$listId] = [
440 464 'label' => $data['lists'][$listId]->name,
441 465 'value' => $item->nbSent,
442 466 'color' => $data['lists'][$listId]->color,
@@ -442,19 +466,19 @@
442 466 'color' => $data['lists'][$listId]->color,
443 467 ];
444 468 $data['emailsOpen'][$listId] = [
445 469 'label' => $data['lists'][$listId]->name,
446 - 'value' => round(($item->nbOpen * 100) / $item->nbSent),
470 + 'value' => round(($item->nbOpen * 100) / $nbSent),
447 471 'color' => $data['lists'][$listId]->color,
448 472 ];
449 473 $data['bounces'][$listId] = [
450 474 'label' => $data['lists'][$listId]->name,
451 - 'value' => round(($item->nbBounce * 100) / $item->nbSent),
475 + 'value' => round(($item->nbBounce * 100) / $nbSent),
452 476 'color' => $data['lists'][$listId]->color,
453 477 ];
454 478 $data['unsubscribed'][$listId] = [
455 479 'label' => $data['lists'][$listId]->name,
456 - 'value' => round(($item->nbUnsub * 100) / $item->nbSent),
480 + 'value' => round(($item->nbUnsub * 100) / $nbSent),
457 481 'color' => $data['lists'][$listId]->color,
458 482 ];
459 483 }
460 484
@@ -459,8 +483,10 @@
459 483 }
460 484
461 485 foreach ($data['userPerList'] as $listId => $item) {
462 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
463 489 $data['click'][$listId] = ['label' => $data['lists'][$listId]->name, 'value' => 0, 'color' => $data['lists'][$listId]->color];
464 490
465 491 if ($nbUser > 0 && !empty($data['nbClick'][$listId]->nbClick)) {
466 492 $data['click'][$listId]['value'] = round(($data['nbClick'][$listId]->nbClick * 100) / $nbUser);
@@ -519,10 +545,15 @@
519 545 }
520 546
521 547 private function prepareDevicesStats(array &$data): void
522 548 {
523 - $campaignClass = new CampaignClass();
524 - $devicesCampaign = $campaignClass->getDevicesWithCountByMailId($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 + }
525 556
526 557 $defaultDataDevices = [
527 558 'ACYM_MOBILE' => 0,
528 559 'ACYM_DESKTOP' => 0,
@@ -555,10 +586,15 @@
555 586 }
556 587
557 588 private function prepareOpenSourcesStats(array &$data): void
558 589 {
559 - $userStatClass = new UserStatClass();
560 - $openedFromStats = $userStatClass->getOpenSourcesStats($this->selectedMailIds);
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 + }
561 597
562 598 $data['openedWith'] = [];
563 599 foreach ($openedFromStats as $oneSource) {
564 600 if (empty($oneSource->number)) {
@@ -586,8 +622,9 @@
586 622
587 623 $campaignClass = new CampaignClass();
588 624 $urlClickClass = new UrlClickClass();
589 625
626 + //For the total opening, the doughnut chart
590 627 $data['mail']->totalMail = $data['mail']->sent + $data['mail']->fail;
591 628 $data['mail']->percentageSent = empty($data['mail']->totalMail) ? 0 : number_format(($data['mail']->sent * 100) / $data['mail']->totalMail, 2);
592 629 $data['mail']->allSent = empty($data['mail']->totalMail)
593 630 ? acym_translationSprintf('ACYM_X_MAIL_SUCCESSFULLY_SENT_OF_X', 0, 0)
@@ -596,28 +633,47 @@
596 633 $data['mail']->sent,
597 634 $data['mail']->totalMail
598 635 );
599 636
600 - $openRateCampaign = empty($this->selectedMailIds) ? $campaignClass->getOpenRateAllCampaign() : $campaignClass->getOpenRateCampaigns($this->selectedMailIds);
601 - $data['mail']->percentageOpen = empty($openRateCampaign->sent) ? 0 : number_format(($openRateCampaign->open_unique * 100) / $openRateCampaign->sent, 2);
602 - $data['mail']->allOpen = empty($openRateCampaign->sent)
603 - ? acym_translationSprintf('ACYM_X_MAIL_OPENED_OF_X', 0, 0)
604 - : acym_translationSprintf(
605 - 'ACYM_X_MAIL_OPENED_OF_X',
606 - $openRateCampaign->open_unique,
607 - $openRateCampaign->sent
608 - );
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;
609 640
610 - $clickRateCampaign = $urlClickClass->getNumberUsersClicked($this->selectedMailIds);
611 - $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);
612 671 $data['mail']->allClick = empty($data['mail']->sent)
613 - ? acym_translationSprintf('ACYM_X_MAIL_CLICKED_OF_X', 0, 0)
614 - : acym_translationSprintf(
615 - 'ACYM_X_MAIL_CLICKED_OF_X',
616 - $clickRateCampaign,
617 - $data['mail']->sent
618 - );
672 + ? acym_translationSprintf($clickLabelKey, 0, 0)
673 + : acym_translationSprintf($clickLabelKey, $clickCount, $data['mail']->sent);
619 674
675 + //bounce rate
620 676 $bounceRateCampaign = empty($this->selectedMailIds) ? $campaignClass->getBounceRateAllCampaign() : $campaignClass->getBounceRateCampaigns($this->selectedMailIds);
621 677 $data['mail']->percentageBounce = empty($data['mail']->sent) ? 0 : number_format(($bounceRateCampaign->bounce_unique * 100) / $data['mail']->sent, 2);
622 678 $data['mail']->allBounce = empty($data['mail']->sent)
623 679 ? acym_translationSprintf('ACYM_X_BOUNCE_OF_X', 0, 0)
@@ -627,17 +683,22 @@
627 683 $data['mail']->sent
628 684 );
629 685
630 686 if (!empty($this->selectedMailIds)) {
687 + //unsubscribe rate
631 688 $mailStat = $mailStatClass->getByMailIds($this->selectedMailIds);
632 - $data['mail']->percentageUnsub = empty($data['mail']->sent) ? 0 : number_format(($mailStat->unsubscribe_total * 100) / $data['mail']->sent, 2);
633 - $data['mail']->allUnsub = empty($data['mail']->sent)
634 - ? acym_translationSprintf('ACYM_X_USERS_UNSUBSCRIBED_OF_X', 0, 0)
635 - : acym_translationSprintf(
636 - 'ACYM_X_USERS_UNSUBSCRIBED_OF_X',
637 - $mailStat->unsubscribe_total,
638 - $data['mail']->sent
639 - );
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 + }
640 701 }
641 702
642 703 $this->prepareDevicesStats($data);
643 704 $this->prepareOpenSourcesStats($data);
@@ -658,10 +719,10 @@
658 719
659 720 private function prepareLinksDetailsListing(array &$data): void
660 721 {
661 722 $data['search'] = $this->getVarFiltersListing('string', 'links_details_search', '');
662 - $data['ordering'] = $this->getVarFiltersListing('string', 'links_details_ordering', 'id');
663 - $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');
664 725
665 726 if (empty($this->selectedMailIds)) return;
666 727
667 728 $pagination = new PaginationHelper();
@@ -682,8 +743,9 @@
682 743 );
683 744
684 745 $this->decode($urlClicks['links_details']);
685 746
747 + // Prepare the pagination
686 748 $pagination->setStatus((int)$urlClicks['total']->total, $page, $detailedStatsPerPage);
687 749
688 750 $data['pagination'] = $pagination;
689 751 $data['links_details'] = $urlClicks['links_details'];
@@ -697,9 +759,9 @@
697 759 $mailStatClass = new MailStatClass();
698 760 $mailClass = new MailClass();
699 761
700 762 $data['mailStat'] = $mailStatClass->getByMailIds($this->selectedMailIds);
701 - $data['lists'] = $mailClass->getAllListsByMailId($this->selectedMailIds);
763 + $data['lists'] = $mailClass->getAllListsByMailId($this->selectedMailIds[0]);
702 764 }
703 765
704 766 private function exportGlobalFormatted(): void
705 767 {
@@ -709,13 +771,11 @@
709 771 $data['show_date_filters'] = true;
710 772 $data['page_title'] = false;
711 773 $timeLinechart = acym_getVar('string', 'time_linechart', 'month');
712 774
713 -
714 775 $this->prepareMailFilter($data);
715 776 $this->prepareClickStats($data);
716 777 $this->preparecharts($data);
717 - $this->prepareDefaultRoundCharts($data);
718 778 $this->prepareDefaultLineChart($data);
719 779 $this->prepareDefaultDevicesChart($data);
720 780 $this->prepareDefaultBrowsersChart($data);
721 781
@@ -811,15 +871,15 @@
811 871 if (count($history) > 30) {
812 872 array_shift($history);
813 873 }
814 874
815 - $this->config->save([$key => json_encode($history)]);
875 + $this->config->saveConfig([$key => json_encode($history)]);
816 876
817 877 $evolutionKey = $this->getEvolutionKey($key);
818 878 $oldEvolutionData[$evolutionKey] = $this->calculateEvolution($history, $isPercentage);
819 879 }
820 880
821 - $this->config->save(['statsEvolution' => json_encode($oldEvolutionData)]);
881 + $this->config->saveConfig(['statsEvolution' => json_encode($oldEvolutionData)]);
822 882
823 883 if (!empty($data)) {
824 884 $this->updateDashboardData($data);
825 885 }
@@ -860,10 +920,12 @@
860 920 return null;
861 921 }
862 922
863 923 if ($isPercentage) {
924 + // For totalSubscribers: percentage variation
864 925 return round((($newValue - $oldValue) / $oldValue) * 100, 2);
865 926 } else {
927 + // For other stats: absolute difference
866 928 return round($newValue - $oldValue, 2);
867 929 }
868 930 }
869 931