<?php
// +-------------------------------------------------+
// | 2002-2011 PMB Services / www.sigb.net pmb@sigb.net et contributeurs (voir www.sigb.net)
// +-------------------------------------------------+
// $Id: campaign_stats.class.php,v 1.6.12.1 2026/07/24 11:00:38 dgoron Exp $

if (stristr($_SERVER['REQUEST_URI'], ".class.php")) die("no access");

global $class_path;
require_once($class_path.'/campaigns/campaign_logs.class.php');
require_once($class_path.'/campaigns/campaign_recipients.class.php');
require_once($class_path.'/encoding_normalize.class.php');

class campaign_stats {
	
	protected $num_campaign;
	
	protected $data;
	
	protected $date;
	
	protected $opening_logs;
	
	protected $clicks_logs;
	
	protected $unsubscribes_logs;
	
	protected $recipients_number;

	protected $recipients_number_open_consent;

	protected $recipients_number_click_consent;

	public function __construct($num_campaign=0) {
		$this->num_campaign = intval($num_campaign);
		$this->fetch_data();
	}
	
	protected function fetch_data() {
		$query = "select * from campaigns_stats where campaign_stat_num_campaign = ".$this->num_campaign;
		$result = pmb_mysql_query($query);
		if(pmb_mysql_num_rows($result)) {
			$row = pmb_mysql_fetch_assoc($result);
			$this->data = encoding_normalize::json_decode($row['campaign_stat_data'], true);
			$this->date = $row['campaign_stat_date'];
		}
	}
	
	protected function init_data() {
		$this->data['opening_rate'] = array();
		$this->data['clicks_rate'] = array();
		$this->data['clicks_by_links'] = array();
		$this->data['clicks_by_links_type'] = array();
	}
	
	public function get_opening_logs() {
		if(!isset($this->opening_logs)) {
			$this->opening_logs = array();
			$campaign_logs = campaign_logs::get_instance($this->num_campaign);
			$logs = $campaign_logs->get_logs();
			foreach ($logs as $log) {
				if(empty($this->opening_logs[$log['num_recipient']])) {
					$this->opening_logs[$log['num_recipient']] = $log;
				}
				if($log['url']['code'] == 33) {
					$this->opening_logs[$log['num_recipient']] = $log;
				}
			}
		}
		return $this->opening_logs;
	}
	
	public function get_clicks_logs() {
		if(!isset($this->clicks_logs)) {
			$this->clicks_logs = array();
			$campaign_logs = campaign_logs::get_instance($this->num_campaign);
			$logs = $campaign_logs->get_logs();
			foreach ($logs as $log) {
				if($log['url']['code'] != 33) {
					$this->clicks_logs[] = $log;
				}
			}
		}
		return $this->clicks_logs;
	}
	
	public function get_opening($unique=false) {
		$opening = 0;
		foreach ($this->get_opening_logs() as $log) {
			if($unique) {
				$opening++;
			} else {
				$opening += count($log['url']['dates']);
			}
		}
		return $opening;
	}
	
	public function get_opening_rate() {
		$opening_rate = '0.00';
		// RGPD/CNIL : le taux ne porte que sur les destinataires ayant consenti au tracage des ouvertures
		$recipents_number = $this->get_recipients_number_open_consent();
		if($recipents_number) {
			$opening_rate = $this->get_opening(true)/$recipents_number*100;
		}
		return number_format($opening_rate, 2)."%";
	}
	
	public function get_clicks($unique=false) {
		$clicks = 0;
		$recipients = array();
		foreach ($this->get_clicks_logs() as $log) {
			if(!in_array($log['num_recipient'], $recipients)) {
				$recipients[] = $log['num_recipient'];
				if($unique) {
					$clicks++;
				} else {
					$clicks += count($log['dates']);
				}
			}
		}
		return $clicks;
	}
	
	public function get_clicks_rate() {
		$clicks_rate = '0.00';
		// RGPD/CNIL : le taux ne porte que sur les destinataires ayant consenti au tracage des clics
		$recipents_number = $this->get_recipients_number_click_consent();
		if($recipents_number) {
			$clicks_rate = $this->get_clicks(true)/$recipents_number*100;
		}
		return number_format($clicks_rate, 2)."%";
	}
	
	public function get_unsubscribes() {
		$campaign_logs = campaign_logs::get_instance($this->num_campaign);
		$logs = $campaign_logs->get_logs();
		$unsubscribes = 0;
		foreach ($logs as $log) {
// 			if($log['url']['code'] == !!code!!) {
				$unsubscribes++;
// 			}
		}
		return $unsubscribes;
	}
	
	protected function get_hour($timestamp) {
		return date('H', $timestamp);
	}
	
	protected function get_hours() {
		$hours = array();
		for($i = 0; $i < 24; $i++) {
			$hours[] = str_pad($i, 2, 0, STR_PAD_LEFT);
		}
		return $hours;
	}
	
	protected function get_day($timestamp) {
		return date('Y-m-d', $timestamp);
	}
	
	protected function get_days($start, $end) {
		$days = array();
		$day = $start;
		while($day <= $end) {
			$days[] = $day;
			$day = $this->get_day(strtotime($day. ' + 1 day'));
		}
		return $days;
	}

	/**
	 * Reduit une liste de logs (ouvertures ou clics) a un seul evenement par
	 * destinataire : le plus ancien, tous liens confondus pour les clics
	 * (un destinataire peut apparaitre dans plusieurs logs, un par lien
	 * clique). Utilise quand $first_only est actif, pour que le compteur ne
	 * soit incremente qu'une seule fois par lecteur -- sur le jour, l'heure
	 * ou le lien de cette premiere occurrence -- et non a chaque log ou
	 * chaque date ou ce lecteur apparait.
	 *
	 * @param array $logs Logs bruts (cf. campaign_logs::get_logs())
	 * @return array Un log de synthese par destinataire : ['num_recipient', 'dates' => [1 date], 'url' => ...]
	 */
	protected function first_only_logs($logs) {
		$first_by_recipient = array();
		foreach ($logs as $log) {
			foreach ($log['dates'] as $date) {
				$recipient = $log['num_recipient'];
				if (!isset($first_by_recipient[$recipient]) || strtotime($date) < strtotime($first_by_recipient[$recipient]['dates'][0])) {
					$first_by_recipient[$recipient] = array(
						'num_recipient' => $recipient,
						'dates' => array($date),
						'url' => $log['url'],
					);
				}
			}
		}
		return array_values($first_by_recipient);
	}

	public function get_opening_by_days($start, $end, $unique=false, $first_only=false) {
		$details = array();
		$days = $this->get_days($start, $end);
		foreach ($days as $day) {
			$details[$day] = 0;
		}
		$logs = $first_only ? $this->first_only_logs($this->get_opening_logs()) : $this->get_opening_logs();
		foreach ($logs as $log) {
			$counted_days = array();
			foreach($log['dates'] as $date) {
				$day = $this->get_day(strtotime($date));
				if($day >= $start && $day <= $end) {
					if($unique && isset($counted_days[$day])) {
						continue;
					}
					$counted_days[$day] = true;
					$details[$day]++;
				}
			}
		}
		return $details;
	}

	public function get_clicks_by_days($start, $end, $unique=false, $first_only=false) {
		$details = array();
		$days = $this->get_days($start, $end);
		foreach ($days as $day) {
			$details[$day] = 0;
		}
		$logs = $first_only ? $this->first_only_logs($this->get_clicks_logs()) : $this->get_clicks_logs();
		foreach ($logs as $log) {
			$counted_days = array();
			foreach($log['dates'] as $date) {
				$day = $this->get_day(strtotime($date));
				if($day >= $start && $day <= $end) {
					if($unique && isset($counted_days[$day])) {
						continue;
					}
					$counted_days[$day] = true;
					$details[$day]++;
				}
			}
		}
		return $details;
	}
	
	public function get_opening_by_hours($start, $end, $unique=false, $first_only=false) {
		$details = array();
		$days = $this->get_days($start, $end);
		foreach ($days as $day) {
			$hours = $this->get_hours($day);
			foreach ($hours as $hour) {
				$details[$day][$hour] = 0;
			}
		}
		$logs = $first_only ? $this->first_only_logs($this->get_opening_logs()) : $this->get_opening_logs();
		foreach ($logs as $log) {
			$counted_days = array();
			foreach($log['dates'] as $date) {
				$day = $this->get_day(strtotime($date));
				$hour = $this->get_hour(strtotime($date));
				if($unique) {
					if(isset($counted_days[$day])) {
						continue;
					}
					$counted_days[$day] = true;
				}
// 				if($hour >= $start && $hour <= $end) {
					$details[$day][$hour]++;
// 				}
			}
		}
		return $details;
	}

	public function get_clicks_by_hours($start, $end, $unique=false, $first_only=false) {
		$details = array();
		$days = $this->get_days($start, $end);
		foreach ($days as $day) {
			$hours = $this->get_hours($day);
			foreach ($hours as $hour) {
				$details[$day][$hour] = 0;
			}
		}
		$logs = $first_only ? $this->first_only_logs($this->get_clicks_logs()) : $this->get_clicks_logs();
		foreach ($logs as $log) {
			$counted_days = array();
			foreach($log['dates'] as $date) {
				$day = $this->get_day(strtotime($date));
				$hour = $this->get_hour(strtotime($date));
				if($unique) {
					if(isset($counted_days[$day])) {
						continue;
					}
					$counted_days[$day] = true;
				}
// 				if($hour >= $start && $hour <= $end) {
					$details[$day][$hour]++;
// 				}
			}
		}
		return $details;
	}
	
	public function get_clicks_by_links($unique=false, $first_only=false) {
		$clicks = array();
		$logs = $first_only ? $this->first_only_logs($this->get_clicks_logs()) : $this->get_clicks_logs();
		foreach ($logs as $log) {
			if(!isset($clicks[$log['url']['link']])) {
				$clicks[$log['url']['link']] = 0;
			}
			if($unique) {
				$clicks[$log['url']['link']]++;
			} else {
				$clicks[$log['url']['link']] += count($log['dates']);
			}
		}
		return $clicks;
	}
	
	public function get_clicks_by_links_type($unique=false, $first_only=false) {
		$clicks = array();
		$logs = $first_only ? $this->first_only_logs($this->get_clicks_logs()) : $this->get_clicks_logs();
		foreach ($logs as $log) {
			if(!isset($clicks[$log['url']['label']])) {
				$clicks[$log['url']['label']] = 0;
			}
			if($unique) {
				$clicks[$log['url']['label']]++;
			} else {
				$clicks[$log['url']['label']] += count($log['dates']);
			}
		}
		return $clicks;
	}

	public function get_opening_by_recipients($field_name, $unique=false, $first_only=false) {
		$opening = array();
		$campaign_recipients = new campaign_recipients($this->num_campaign);
		$logs = $first_only ? $this->first_only_logs($this->get_opening_logs()) : $this->get_opening_logs();
		foreach ($logs as $log) {
			$field_value = $campaign_recipients->get_recipient($log['num_recipient'])->get_detail_label($field_name);
			if(!isset($opening[$field_value])) {
				$opening[$field_value] = 0;
			}
			if($unique) {
				$opening[$field_value]++;
			} else {
				$opening[$field_value] += count($log['dates']);
			}
		}
		return $opening;
	}

	public function get_clicks_by_recipients($field_name, $unique=false, $first_only=false) {
		$clicks = array();
		$campaign_recipients = new campaign_recipients($this->num_campaign);
		$logs = $first_only ? $this->first_only_logs($this->get_clicks_logs()) : $this->get_clicks_logs();
		foreach ($logs as $log) {
			$field_value = $campaign_recipients->get_recipient($log['num_recipient'])->get_detail_label($field_name);
			if(!isset($clicks[$field_value])) {
				$clicks[$field_value] = 0;
			}
			if($unique) {
				$clicks[$field_value]++;
			} else {
				$clicks[$field_value] += count($log['dates']);
			}
		}
		return $clicks;
	}
	
	public function get_recipients_number() {
		if(!isset($this->recipients_number)) {
			$query = "select count(*) from campaigns_recipients where campaign_recipient_num_campaign = '".$this->num_campaign."'";
			$result = pmb_mysql_query($query);
			$this->recipients_number = pmb_mysql_result($result, 0, 0);
		}
		return $this->recipients_number;
	}

	/**
	 * Nombre de destinataires ayant consenti au tracage des ouvertures au moment de l'envoi
	 * @return int
	 */
	public function get_recipients_number_open_consent() {
		if(!isset($this->recipients_number_open_consent)) {
			$query = "select count(*) from campaigns_recipients where campaign_recipient_num_campaign = '".$this->num_campaign."' and campaign_recipient_empr_mail_tracking_open = 1";
			$result = pmb_mysql_query($query);
			$this->recipients_number_open_consent = pmb_mysql_result($result, 0, 0);
		}
		return $this->recipients_number_open_consent;
	}

	/**
	 * Nombre de destinataires ayant consenti au tracage des clics au moment de l'envoi
	 * @return int
	 */
	public function get_recipients_number_click_consent() {
		if(!isset($this->recipients_number_click_consent)) {
			$query = "select count(*) from campaigns_recipients where campaign_recipient_num_campaign = '".$this->num_campaign."' and campaign_recipient_empr_mail_tracking_click = 1";
			$result = pmb_mysql_query($query);
			$this->recipients_number_click_consent = pmb_mysql_result($result, 0, 0);
		}
		return $this->recipients_number_click_consent;
	}

	/**
	 * Nombre de destinataires n'ayant pas consenti au tracage des ouvertures
	 * @return int
	 */
	public function get_recipients_refused_open_number() {
		return $this->get_recipients_number() - $this->get_recipients_number_open_consent();
	}

	/**
	 * Nombre de destinataires n'ayant pas consenti au tracage des clics
	 * @return int
	 */
	public function get_recipients_refused_click_number() {
		return $this->get_recipients_number() - $this->get_recipients_number_click_consent();
	}

	public function get_no_email_sent_number() {
		//Compter le nombre de destinataires dans la file d'attente
		$no_email_sent = 0;
		return $no_email_sent." / ".$this->get_recipients_number();
	}

	/**
	 * Retourne la distribution des delais de reaction entre l'envoi de la campagne
	 * et la premiere ouverture de chaque destinataire, regroupee en tranches horaires.
	 *
	 * @return array Tableau indexe par tranche ('<1h', '1-6h', '6-24h', '24-48h', '>48h'),
	 *               valeurs = nombre de destinataires dans cette tranche
	 */
	public function get_reaction_delay_distribution() {
		$query = 'SELECT campaign_date FROM campaigns WHERE id_campaign = '.intval($this->num_campaign);
		$result = pmb_mysql_query($query);
		if (!$result || !pmb_mysql_num_rows($result)) {
			return [];
		}
		$row = pmb_mysql_fetch_assoc($result);
		$send_ts = strtotime($row['campaign_date']);
		if (!$send_ts) {
			return [];
		}

		$buckets = ['< 1h' => 0, '1-6h' => 0, '6-24h' => 0, '24-48h' => 0, '> 48h' => 0];
		foreach ($this->get_opening_logs() as $log) {
			if (empty($log['dates'])) {
				continue;
			}
			$first_ts = min(array_map('strtotime', $log['dates']));
			$hours = ($first_ts - $send_ts) / 3600;
			if ($hours < 1) {
				$buckets['< 1h']++;
			} elseif ($hours < 6) {
				$buckets['1-6h']++;
			} elseif ($hours < 24) {
				$buckets['6-24h']++;
			} elseif ($hours < 48) {
				$buckets['24-48h']++;
			} else {
				$buckets['> 48h']++;
			}
		}
		return $buckets;
	}

	/**
	 * Retourne le nombre cumule d'ouvreurs uniques par jour.
	 * Seule la premiere ouverture de chaque destinataire est comptabilisee.
	 *
	 * @param string $start Date de debut au format Y-m-d
	 * @param string $end   Date de fin au format Y-m-d
	 * @return array Tableau indexe par date Y-m-d, valeurs = total cumule d'ouvreurs uniques
	 */
	public function get_cumulative_openings_by_days($start, $end) {
		$first_by_day = [];
		foreach ($this->get_opening_logs() as $log) {
			if (empty($log['dates'])) {
				continue;
			}
			$first_day = $this->get_day(min(array_map('strtotime', $log['dates'])));
			$first_by_day[$first_day] = ($first_by_day[$first_day] ?? 0) + 1;
		}

		$cumulative = [];
		$running_total = 0;
		foreach ($this->get_days($start, $end) as $day) {
			$running_total += $first_by_day[$day] ?? 0;
			$cumulative[$day] = $running_total;
		}
		return $cumulative;
	}

	/**
	 * Retourne la plage de dates d'activite (premier et dernier evenement
	 * trace) de la campagne, au format Y-m-d. Chaines vides si aucun evenement.
	 *
	 * @return array ['min' => string, 'max' => string]
	 */
	public function get_activity_dates() {
		$query = "select date_format(min(campaign_log_date), '%Y-%m-%d') as min_date, date_format(max(campaign_log_date), '%Y-%m-%d') as max_date from campaigns_logs where campaign_log_num_campaign = ".$this->num_campaign;
		$result = pmb_mysql_query($query);
		$min = '';
		$max = '';
		if($result && pmb_mysql_num_rows($result)) {
			$row = pmb_mysql_fetch_assoc($result);
			$min = isset($row['min_date']) ? $row['min_date'] : '';
			$max = isset($row['max_date']) ? $row['max_date'] : '';
		}
		return ['min' => strval($min), 'max' => strval($max)];
	}

	/**
	 * @return array
	 */
	public function build_data() {
		$this->init_data();
		foreach($this->data as $property=>$data) {
			$method_name = 'get_'.$property;
			$this->data[$property] = $this->{$method_name}();
		}
		return $this->data;
	}
	
	/**
	 * @return string Données au format JSON
	 */
	public function get_json_data() {
		if(!isset($this->data)) {
			$this->build_data();
		}
		return encoding_normalize::json_encode($this->data);
	}
	
	public function save() {
		$query = "select count(*) from campaigns_stats where campaign_stat_num_campaign = ".$this->num_campaign;
		$result = pmb_mysql_query($query);
		if(pmb_mysql_result($result, 0, 0)) {
			$query = 'update campaigns_stats set ';
			$where = 'where campaign_stat_num_campaign= '.$this->num_campaign;
		} else {
			$query = 'insert into campaigns_stats set campaign_stat_num_campaign= '.$this->num_campaign.',';
			$where = '';
		}
		$query .= '
				campaign_stat_data = "'.addslashes(encoding_normalize::json_encode($this->data)).'",
				campaign_stat_date = "'.$this->date.'"
				'.$where;
		$result = pmb_mysql_query($query);
		if($result) {
			return true;
		} else {
			return false;
		}
	}
}