Crvi/app/controllers/Notifications_Controller.php
2026-01-20 07:54:37 +01:00

562 lines
24 KiB
PHP

<?php
namespace ESI_CRVI_AGENDA\controllers;
use ESI_CRVI_AGENDA\controllers\CRVI_Plugin;
include_once plugin_dir_path(__FILE__) . '../config.php';
class CRVI_Notifications_Controller {
public function __construct() {
add_action('init', [$this, 'init']);
}
public function init() {
/* add_action('wp_ajax_crvi_notifications', [$this, 'crvi_notifications']); */
add_action('wp_ajax_crvi_send_sms_debug', [self::class, 'ajax_send_sms_debug']);
add_action('wp_ajax_nopriv_crvi_send_sms_debug', [self::class, 'ajax_send_sms_debug']);
}
/**
* Crée la table de log des SMS si elle n'existe pas
* Schéma basé sur DOC-2/wp_crvi_sms_log.sql
*/
public static function create_sms_log_table() {
global $wpdb;
$table_name = $wpdb->prefix . 'crvi_sms_log';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$table_name} (
id int(11) NOT NULL AUTO_INCREMENT,
numsms varchar(255) NOT NULL,
msgsms text NOT NULL,
status varchar(50) NOT NULL,
date_sms datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
sujet varchar(50) NOT NULL,
id_event int(11) NOT NULL,
full_log longtext NOT NULL,
PRIMARY KEY (id)
) {$charset_collate};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
}
/**
* AJAX: Envoi d'un SMS de debug via admin-ajax.php
* action: crvi_send_sms_debug
* champs attendus:
* - nonce: string
* - phone: string (optionnel, défaut 0485500723)
* - message: string (si présent => envoi direct via send_sms)
* - OU champs de datas pour send_sms_rdv_cloture (langue, type, date, time, name, gender, intervenant)
* - id_event: int (optionnel pour log)
* - sujet: string (optionnel pour log)
*/
public static function ajax_send_sms_debug() {
// Vérifier que c'est une requête AJAX valide
if (!defined('DOING_AJAX') || !DOING_AJAX) {
wp_send_json_error(['message' => 'Requête invalide'], 400);
return;
}
// Vérif nonce
$nonce = isset($_REQUEST['nonce']) ? sanitize_text_field(wp_unslash($_REQUEST['nonce'])) : '';
if (empty($nonce) || !wp_verify_nonce($nonce, 'crvi_sms')) {
wp_send_json_error(['message' => 'Nonce invalide'], 403);
return;
}
// Vérif permissions (réservé aux utilisateurs pouvant éditer)
if (!is_user_logged_in() || !current_user_can('edit_posts')) {
wp_send_json_error(['message' => 'Non autorisé'], 403);
return;
}
$phone = isset($_REQUEST['phone']) ? sanitize_text_field(wp_unslash($_REQUEST['phone'])) : '0485500723';
$message = isset($_REQUEST['message']) ? sanitize_text_field(wp_unslash($_REQUEST['message'])) : null;
$id_event = isset($_REQUEST['id_event']) ? (int) $_REQUEST['id_event'] : 0;
$sujet = isset($_REQUEST['sujet']) ? sanitize_text_field(wp_unslash($_REQUEST['sujet'])) : 'debug';
try {
if ($message) {
$result = self::sms_send($phone, $message, ['sujet' => $sujet, 'id_event' => $id_event]);
if ($result === true) {
wp_send_json_success(['sent' => true, 'via' => 'send_sms']);
} else {
// $result contient maintenant des détails sur l'erreur
$error_details = is_array($result) ? $result : ['message' => 'Échec d\'envoi (send_sms)', 'reason' => 'unknown'];
wp_send_json_error($error_details);
}
}
// Construction des datas pour le template de clôture par défaut
$datas = [
'phone' => $phone,
'langue' => isset($_REQUEST['langue']) ? sanitize_text_field(wp_unslash($_REQUEST['langue'])) : 'fr',
'type' => isset($_REQUEST['type']) ? sanitize_text_field(wp_unslash($_REQUEST['type'])) : '',
'date' => isset($_REQUEST['date']) ? sanitize_text_field(wp_unslash($_REQUEST['date'])) : '',
'time' => isset($_REQUEST['time']) ? sanitize_text_field(wp_unslash($_REQUEST['time'])) : '',
'name' => isset($_REQUEST['name']) ? sanitize_text_field(wp_unslash($_REQUEST['name'])) : '',
'gender' => isset($_REQUEST['gender']) ? sanitize_text_field(wp_unslash($_REQUEST['gender'])) : '',
'intervenant' => isset($_REQUEST['intervenant']) ? sanitize_text_field(wp_unslash($_REQUEST['intervenant'])) : '',
];
$result = self::send_sms_rdv_cloture($datas);
if ($result === true) {
wp_send_json_success(['sent' => true, 'via' => 'send_sms_rdv_cloture', 'datas' => $datas]);
} else {
// $result contient maintenant des détails sur l'erreur
$error_details = is_array($result) ? $result : ['message' => 'Échec d\'envoi (send_sms_rdv_cloture)', 'reason' => 'unknown', 'datas' => $datas];
wp_send_json_error($error_details);
}
} catch (\Throwable $e) {
wp_send_json_error(['message' => 'Exception: ' . $e->getMessage()], 500);
}
}
public static function send_rdv_cloture_automatique($datas) {
$template_path = plugin_dir_path(__FILE__) . '../../templates/email/rdv-cloture-email-template.php';
// Vérifier que le template existe
if (!file_exists($template_path)) {
error_log('[CRVI_AGENDA] Template email introuvable : ' . $template_path);
return false;
}
// Exécuter le template PHP avec output buffering pour capturer le HTML généré
ob_start();
// Passer les données au template (la variable $datas sera disponible dans le template)
include $template_path;
$body = ob_get_clean();
$subject = 'Rendez-vous clôturés';
$to = 'test@test.com';
$headers = ['Content-Type: text/html; charset=UTF-8'];
$result = wp_mail($to, $subject, $body, $headers);
if (!$result) {
error_log('[CRVI_AGENDA] Échec d\'envoi de l\'email de clôture automatique');
}
return $result;
}
/**
* SMS de clôture de rendez-vous
* Datas attendues : phone, name, gender, date, time, type, langue, intervenant
*/
public static function send_sms_rdv_cloture($datas) {
$langue = $datas['langue'] ?? 'fr';
$type = $datas['type'] ?? '';
$date = $datas['date'] ?? '';
$time = $datas['time'] ?? '';
$name = $datas['name'] ?? '';
$gender = $datas['gender'] ?? '';
$phone = $datas['phone'] ?? '';
$intervenant = $datas['intervenant'] ?? '';
// 1. Récupérer le texte du SMS depuis les options ACF,
// en fonction de la langue du rendez-vous.
$sms_text = '';
// Priorité au répéteur "texte_sms" (texte + langue)
$texte_sms_rows = get_field('texte_sms', 'option');
if (!empty($texte_sms_rows) && is_array($texte_sms_rows)) {
foreach ($texte_sms_rows as $row) {
$row_langue_id = $row['langue'] ?? null;
if ($row_langue_id) {
$term = get_term($row_langue_id);
if ($term && !is_wp_error($term) && $term->slug === $langue) {
$sms_text = $row['texte'] ?? '';
break;
}
}
}
}
// Fallback : texte_fr simple si aucun texte n'a été trouvé
if (!$sms_text) {
$sms_text = get_field('texte_fr', 'option');
}
// Fallback final: modèle par défaut si rien en ACF
if (!$sms_text) {
$sms_text = "Bonjour %titre% %nom%,\n\nVous avez rendez-vous le %date% à %heure%,\n\nLe crvi";
}
if (!$sms_text) {
// Rien de configuré : on ne tente pas d'envoyer
$error_msg = '[CRVI_AGENDA] Aucun texte SMS configuré pour la langue "' . $langue . '".';
error_log($error_msg);
return ['message' => 'Échec d\'envoi (send_sms_rdv_cloture)', 'reason' => 'no_sms_text', 'details' => 'Aucun texte SMS configuré pour la langue "' . $langue . '"'];
}
// 2. Déterminer le titre selon le genre (h/f/i) et la langue
$titres = [
'fr' => ['h' => 'Mr', 'f' => 'Mme', 'i' => ''],
'ar' => ['h' => 'السيد', 'f' => 'السيدة', 'i' => ''],
];
$titre = $titres[$langue][$gender] ?? ($titres['fr'][$gender] ?? '');
// 3. Remplacer les placeholders dans le texte
$replacements = [
'%titre%' => $titre,
'%nom%' => $name,
'%date%' => $date,
'%heure%' => $time,
'%intervenant%' => $intervenant,
];
$sms_text_final = strtr($sms_text, $replacements);
if (empty($phone)) {
$error_msg = '[CRVI_AGENDA] Numéro de téléphone vide, SMS non envoyé.';
error_log($error_msg);
// Log en base le skip pour traçabilité
self::log_sms([
'numsms' => '',
'msgsms' => $sms_text_final,
'status' => 'skipped_no_phone',
'sujet' => isset($datas['sujet']) ? (string) $datas['sujet'] : 'rdv_cloture',
'id_event' => isset($datas['id_event']) ? (int) $datas['id_event'] : 0,
'full_log' => 'No phone'
]);
return ['message' => 'Échec d\'envoi (send_sms_rdv_cloture)', 'reason' => 'empty_phone', 'details' => 'Numéro de téléphone vide'];
}
// 4. Envoi via OVH en utilisant login/mot de passe définis dans les options ACF
$options = [];
if (isset($datas['sujet'])) {
$options['sujet'] = (string) $datas['sujet'];
}
if (isset($datas['id_event'])) {
$options['id_event'] = (int) $datas['id_event'];
}
$result = self::sms_send($phone, $sms_text_final, $options);
// Si sms_send retourne true, retourner true, sinon retourner les détails de l'erreur
return ($result === true) ? true : $result;
}
/**
* Envoi générique d'un SMS via OVH HTTP2SMS
* Utilise les options ACF "option_sms" (login, mot_de_passe, quota_sms).
*/
public static function send_sms($tel, $message, $options = []) {
// Compatibilité ancien appel: si $options n'est pas un array, ignorer
if (!is_array($options)) {
$options = [];
}
$sujet = isset($options['sujet']) ? (string) $options['sujet'] : '';
$id_event = isset($options['id_event']) ? (int) $options['id_event'] : 0;
// Options SMS stockées dans ACF > Options > Option > Option sms
$option_sms = get_field('option_sms', 'option');
$login = $option_sms['login'] ?? '';
$password = $option_sms['mot_de_passe'] ?? '';
// $quota_sms = isset($option_sms['quota_sms']) ? (int) $option_sms['quota_sms'] : 0; // non utilisé ici, mais disponible
// smsAccount peut maintenant être configuré dans ACF (champ texte "compte_ovh" en options).
// Si le champ est vide ou non défini, on conserve le fallback sur la constante OVH_SMS_ACCOUNT.
$smsAccount_acf = '';
if (function_exists('get_field')) {
// Champ texte simple en options : compte_ovh
$smsAccount_acf = (string) get_field('compte_ovh', 'option');
// Si rien n'est défini à la racine, on tente éventuellement dans le groupe option_sms
if (!$smsAccount_acf && is_array($option_sms) && !empty($option_sms['compte_ovh'])) {
$smsAccount_acf = (string) $option_sms['compte_ovh'];
}
}
if (empty($login) || empty($password)) {
$error_msg = '[CRVI_AGENDA] SMS non envoyé : login ou mot de passe OVH non configuré dans ACF.';
error_log($error_msg);
return ['message' => 'Échec d\'envoi (send_sms)', 'reason' => 'missing_credentials', 'details' => 'Login ou mot de passe OVH non configuré dans ACF'];
}
// Normalisation du numéro (reprise de l'algorithme existant)
$tel = str_replace(['+', ' ', '-', '/'], '', $tel);
if (substr($tel, 0, 2) === '00') {
$to_sms = $tel; // pas de changement
} elseif (substr($tel, 0, 2) === '32' || substr($tel, 0, 2) === '33') {
$to_sms = '00' . $tel;
} else {
$to_sms = '0032' . substr($tel, 1);
}
$text_sms = urlencode(utf8_decode($message));
// Récupérer smsAccount et from depuis ACF / config.php, avec valeurs par défaut
// Priorité au champ texte ACF "compte_ovh" (options), puis fallback sur la constante.
$smsAccount = $smsAccount_acf ?: (\defined('OVH_SMS_ACCOUNT') ? \constant('OVH_SMS_ACCOUNT') : 'sms-xxxxxxx');
$from = \defined('OVH_SMS_FROM') ? \constant('OVH_SMS_FROM') : '0032496390437';
$url = "https://www.ovh.com/cgi-bin/sms/http2sms.cgi?smsAccount=" . rawurlencode($smsAccount)
. "&login=" . rawurlencode($login)
. "&password=" . rawurlencode($password)
. "&from=" . rawurlencode($from)
. "&to=" . rawurlencode($to_sms)
. "&contentType=text/xml"
. "&message={$text_sms}";
$ret = @file($url);
$raw_response = ($ret !== false && isset($ret[0])) ? $ret[0] : '';
$xml = $raw_response ? @simplexml_load_string($raw_response) : false;
$success = false;
$status_str = 'failed';
$error_details = [];
if ($xml && isset($xml->status)) {
$status_code = (int) $xml->status;
$success = ($status_code === 100);
$status_str = $success ? 'success' : ('failed(' . $status_code . ')');
if (!$success) {
$error_details = [
'message' => 'Échec d\'envoi (send_sms)',
'reason' => 'ovh_api_error',
'status_code' => $status_code,
'status_message' => (string) ($xml->message ?? 'Unknown error'),
'raw_response' => $raw_response
];
error_log('[CRVI_AGENDA] Réponse OVH HTTP2SMS: statut ' . $status_code . ' - ' . (string) ($xml->message ?? 'Unknown error'));
}
} else {
$error_details = [
'message' => 'Échec d\'envoi (send_sms)',
'reason' => 'invalid_response',
'details' => 'Réponse OVH HTTP2SMS invalide ou vide',
'raw_response' => $raw_response
];
error_log('[CRVI_AGENDA] Réponse OVH HTTP2SMS invalide ou vide. Raw response: ' . $raw_response);
}
// Mettre à jour le quota ACF si l'envoi a réussi, en se basant sur la réponse API (creditLeft)
if ($success && $xml && isset($xml->creditLeft) && function_exists('get_field') && function_exists('update_field')) {
$creditLeftVal = (int) $xml->creditLeft;
$option_sms = get_field('option_sms', 'option');
if (is_array($option_sms)) {
$option_sms['quota_sms'] = $creditLeftVal;
// Mettre à jour le groupe complet pour rester compatible ACF
update_field('option_sms', $option_sms, 'option');
}
}
// Log en base
self::log_sms([
'numsms' => $to_sms,
'msgsms' => $message,
'status' => $status_str,
'sujet' => $sujet,
'id_event' => $id_event,
'full_log' => $raw_response
]);
// Retourner true si succès, sinon retourner les détails de l'erreur
if ($success) {
return true;
} else {
return $error_details;
}
}
/**
* Alias demandé: utiliser sms_send comme point d'entrée standard.
* Garde la même signature et délègue à send_sms.
*/
public static function sms_send($tel, $message, $options = []) {
// Prise en charge d'un appel "cron" avec un array de données d'événement
// Exemple: sms_send(['phone'=>'...', 'name'=>'...', 'date'=>'...', 'time'=>'...', 'langue'=>'fr', 'sujet'=>'...', 'id_event'=>123], null);
if (is_array($tel)) {
$datas = $tel;
return self::send_sms_rdv_cloture($datas);
}
return self::send_sms($tel, $message, $options);
}
/**
* Insère une ligne de log dans la table wp_crvi_sms_log
*/
private static function log_sms(array $fields) {
global $wpdb;
$table = $wpdb->prefix . 'crvi_sms_log';
$data = [
'numsms' => isset($fields['numsms']) ? (string) $fields['numsms'] : '',
'msgsms' => isset($fields['msgsms']) ? (string) $fields['msgsms'] : '',
'status' => isset($fields['status']) ? (string) $fields['status'] : '',
'sujet' => isset($fields['sujet']) ? (string) $fields['sujet'] : '',
'id_event' => isset($fields['id_event']) ? (int) $fields['id_event'] : 0,
'full_log' => isset($fields['full_log']) ? (string) $fields['full_log'] : '',
];
// date_sms a une valeur par défaut CURRENT_TIMESTAMP côté DB
// Sécuriser l'insertion via $wpdb->insert (prépare sous le capot)
$wpdb->insert($table, $data, ['%s','%s','%s','%s','%d','%s']);
}
/**
* Envoie des SMS de rappel pour les événements individuels dans X jours
*
* @param int $days Nombre de jours avant l'événement (1, 2 ou 3)
* @return array Rapport détaillé avec statistiques d'envoi
*/
public static function send_sms_reminder($days = 3) {
global $wpdb;
// Valider le paramètre days
if (!in_array($days, [1, 2, 3])) {
return [
'error' => 'Le paramètre days doit être 1, 2 ou 3',
'received' => $days
];
}
$table_name = $wpdb->prefix . 'crvi_agenda';
// Calculer la date cible (aujourd'hui + X jours)
$target_date = date('Y-m-d', strtotime("+{$days} days"));
// Récupérer les événements individuels à la date cible avec statut 'prevu'
$sql = $wpdb->prepare(
"SELECT * FROM $table_name
WHERE date_rdv = %s
AND type = 'individuel'
AND statut = 'prevu'
AND is_deleted = 0
AND id_beneficiaire IS NOT NULL
ORDER BY heure_rdv ASC",
$target_date
);
$events = $wpdb->get_results($sql, ARRAY_A);
if (empty($events)) {
return [
'success' => true,
'count' => 0,
'date' => $target_date,
'days' => $days,
'message' => "Aucun événement individuel trouvé pour dans {$days} jour(s) ({$target_date})"
];
}
$results = [
'success' => true,
'date' => $target_date,
'days' => $days,
'total_events' => count($events),
'sms_sent' => 0,
'sms_failed' => 0,
'sms_skipped' => 0,
'details' => []
];
// Traiter chaque événement
foreach ($events as $event) {
$event_result = [
'event_id' => (int) $event['id'],
'date' => $event['date_rdv'],
'heure' => $event['heure_rdv'],
'status' => 'skipped',
'reason' => ''
];
// Charger le bénéficiaire
if (empty($event['id_beneficiaire'])) {
$event_result['reason'] = 'Pas de bénéficiaire associé';
$results['sms_skipped']++;
$results['details'][] = $event_result;
continue;
}
$beneficiaire = \ESI_CRVI_AGENDA\models\CRVI_Beneficiaire_Model::load($event['id_beneficiaire']);
if (!$beneficiaire) {
$event_result['reason'] = 'Bénéficiaire introuvable';
$results['sms_skipped']++;
$results['details'][] = $event_result;
continue;
}
// Vérifier que le bénéficiaire a un téléphone
$telephone = $beneficiaire->telephone ?? '';
if (empty($telephone)) {
$event_result['reason'] = 'Pas de numéro de téléphone pour le bénéficiaire';
$results['sms_skipped']++;
$results['details'][] = $event_result;
continue;
}
// Récupérer le genre du bénéficiaire depuis ACF
$genre = '';
if (function_exists('get_field')) {
$genre = get_field('genre', $beneficiaire->id) ?? '';
}
// Récupérer la langue de l'événement
$langue = $event['langue'] ?? 'fr';
// Si la langue est un ID de taxonomy, récupérer le slug
if (is_numeric($langue) && function_exists('get_term')) {
$langue_term = get_term((int) $langue, 'langue');
if ($langue_term && !is_wp_error($langue_term)) {
$langue = $langue_term->slug ?? 'fr';
}
}
// Récupérer le nom de l'intervenant
$intervenant_nom = '';
if (!empty($event['id_intervenant'])) {
$intervenant = \ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model::load($event['id_intervenant'], ['id', 'nom', 'prenom']);
if ($intervenant) {
$intervenant_nom = trim(($intervenant->nom ?? '') . ' ' . ($intervenant->prenom ?? ''));
}
}
// Formater la date et l'heure
$date_formatted = date('d/m/Y', strtotime($event['date_rdv']));
$heure_formatted = date('H:i', strtotime($event['heure_rdv']));
// Préparer les données pour l'envoi de SMS
$sms_data = [
'phone' => $telephone,
'name' => trim(($beneficiaire->nom ?? '') . ' ' . ($beneficiaire->prenom ?? '')),
'gender' => $genre, // h, f, ou i
'date' => $date_formatted,
'time' => $heure_formatted,
'type' => $event['type'],
'langue' => $langue,
'intervenant' => $intervenant_nom,
'sujet' => 'rappel_rdv',
'id_event' => (int) $event['id']
];
// Envoyer le SMS
$sms_result = self::send_sms_rdv_cloture($sms_data);
if ($sms_result === true) {
$event_result['status'] = 'sent';
$event_result['beneficiaire'] = $sms_data['name'];
$event_result['telephone'] = $telephone;
$results['sms_sent']++;
} else {
$event_result['status'] = 'failed';
$event_result['reason'] = is_array($sms_result) ? ($sms_result['message'] ?? 'Erreur inconnue') : 'Erreur lors de l\'envoi';
$event_result['beneficiaire'] = $sms_data['name'];
$event_result['telephone'] = $telephone;
$results['sms_failed']++;
}
$results['details'][] = $event_result;
}
return $results;
}
}