diff --git a/admin/index.php b/admin/index.php index ded3051..7a6066c 100644 --- a/admin/index.php +++ b/admin/index.php @@ -49,6 +49,7 @@ 'afficher_messages' => __DIR__ . '/tools/afficher_messages.php', 'new_inscrits' => __DIR__ . '/tools/new_clients.php', 'mail_praticiens' => __DIR__ . '/tools/mailing_praticiens.php', + 'mailing_client' => __DIR__ . '/tools/mailing_client.php', 'connecter_en_admin' => __DIR__ . '/tools/connecter_en_admin.php', 'clean_uploads' => __DIR__ . '/tools/clean_uploads.php', ]; @@ -125,6 +126,9 @@
  • +
  • + +
  • diff --git a/admin/tools/mailing_client.php b/admin/tools/mailing_client.php new file mode 100644 index 0000000..606eb4e --- /dev/null +++ b/admin/tools/mailing_client.php @@ -0,0 +1,416 @@ +
    ' + . htmlspecialchars($translations['non_autorise'] ?? 'Доступ запрещён.', ENT_QUOTES, 'UTF-8') + . '
    '; + exit; + } + + $mustReauth = function (int $ttl = 600) { + $t = (int) ($_SESSION['admin_reauth_at'] ?? 0); + return ($t === 0 || (time() - $t) > $ttl); + }; + + if ($mustReauth()) { + $qs = http_build_query([ + 'page' => 'reauth', + 'redir' => ($_SERVER['REQUEST_URI'] ?? 'index.php?page=maintenance'), + ]); + header('Location: ?' . $qs); + exit; + } + + if (! isset($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + $CSRF = $_SESSION['csrf_token']; + + function e($s) + { + return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); + } + + // --------- Template HTML du mail --------- + function buildAdminMailHtml(string $subject, string $bodyHtml): string + { + global $translations; + + $brand = 'USLUGI.FR'; + $brandBas = $translations['mailing_clients_brand_bas'] ?? 'УСЛУГИ.ФР'; + $ctaUrl = 'https://uslugi.fr/'; + $ctaLabel = $translations['mailing_clients_cta_open_site'] ?? 'Открыть сайт'; + $thanks = $translations['mailing_clients_footer_thanks'] ?? 'Спасибо'; + + return ' + + + ' . e($subject) . ' + + + + +
    + + + + + + + + + + +
    ' . $brand . '
    ' . $bodyHtml . '
    + + ' . e($ctaLabel) . ' + +
    + + + + +
    © ' . date('Y') . ' ' . $brandBas . ' — ' . e($thanks) . '
    +
    +'; + } + + function sendClientMail(string $to, string $subject, string $html): bool + { + [$ok, $err] = uslugi_mail($to, $subject, $html, null, [ + // 'from' => ['no-reply@uslugi.fr', 'USLUGI.FR'], + // 'reply_to' => ['support@uslugi.fr', 'USLUGI.FR'], + // 'headers' => ['X-USLUGI-Source' => 'admin_mailing_clients'], + ]); + + if (! $ok && $err) { + error_log('mailing_clients send error to ' . $to . ' : ' . $err); + } + + return $ok; + } + + // --------- Traitement POST --------- + $flash = null; + + if (($_POST['do'] ?? '') === 'send_mailing') { + try { + if (! hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) { + throw new Exception('CSRF'); + } + + $subject = trim($_POST['subject'] ?? ''); + $body = trim($_POST['body'] ?? ''); + $ids = $_POST['recipients'] ?? []; + + if ($subject === '' || $body === '') { + throw new Exception('EMPTY'); + } + + if (! is_array($ids) || count($ids) === 0) { + throw new Exception('NO_RECIPIENTS'); + } + + $ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v) => $v > 0))); + if (! $ids) { + throw new Exception('NO_RECIPIENTS'); + } + + // Récupérer les emails clients + $in = implode(',', array_fill(0, count($ids), '?')); + $sql = "SELECT id, prenom, nom, email FROM client WHERE id IN ($in) AND email IS NOT NULL AND email <> ''"; + $st = $pdo->prepare($sql); + $st->execute($ids); + $rowsMail = $st->fetchAll(PDO::FETCH_ASSOC); + + if (! $rowsMail) { + throw new Exception('NO_EMAILS'); + } + + $sent = 0; + $total = count($rowsMail); + + foreach ($rowsMail as $r) { + $email = trim($r['email'] ?? ''); + if ($email === '') { + continue; + } + + $html = buildAdminMailHtml($subject, $body); + if (sendClientMail($email, $subject, $html)) { + $sent++; + } + + // usleep(100000); + } + + $flash = [ + 'type' => 'success', + 'msg' => sprintf( + (string) ($translations['mailing_clients_sent_count'] ?? 'Отправлено писем: %d / %d'), + $sent, + $total + ), + ]; + } catch (Throwable $e) { + $code = $e->getMessage(); + $msg = $translations['mailing_clients_err_send'] ?? 'Ошибка при отправке.'; + + if ($code === 'EMPTY') { + $msg = $translations['mailing_clients_err_empty'] ?? 'Тема и текст письма обязательны.'; + } + if ($code === 'NO_RECIPIENTS') { + $msg = $translations['mailing_clients_err_no_recipients'] ?? 'Выберите хотя бы одного клиента.'; + } + if ($code === 'CSRF') { + $msg = $translations['mailing_clients_err_csrf'] ?? 'Ошибка безопасности. Обновите страницу.'; + } + if ($code === 'NO_EMAILS') { + $msg = $translations['mailing_clients_err_no_emails'] ?? 'У выбранных клиентов нет email.'; + } + + $flash = ['type' => 'danger', 'msg' => $msg]; + } + } + + // --------- Récupération des clients --------- + $sql = " + SELECT + c.id, + c.nom, + c.prenom, + c.email, + c.telephone, + c.ville, + c.created_at, + c.updated_at + FROM client c + ORDER BY c.nom ASC, c.prenom ASC +"; + $rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC); + + // Préparer options de filtre par ville + $cityOptions = []; + foreach ($rows as $r) { + $ville = trim((string) ($r['ville'] ?? '')); + if ($ville !== '') { + $cityOptions[mb_strtolower($ville, 'UTF-8')] = $ville; + } + } + natcasesort($cityOptions); +?> + +
    +

    + + +
    + + +
    + + + +
    + + +
    + +
    + + +
    + +
    +
    + +
    + +

    + +
    +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + +
    +
    + + +
    + + +
    + +
    + + + + + + + + + + + + + + format('Y-m-d H:i'); + } catch (Throwable $e) { + $createdFmt = ''; + } + } + + $ville = trim((string) ($r['ville'] ?? '')); + $hay = mb_strtolower( + trim( + ($r['prenom'] ?? '') . ' ' . + ($r['nom'] ?? '') . ' ' . + ($r['email'] ?? '') . ' ' . + ($r['telephone'] ?? '') . ' ' . + ($r['ville'] ?? '') + ), + 'UTF-8' + ); + ?> + + + + + + + + + + + + + + + + + +
    + + > +
    + +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/public/lang/fr.php b/public/lang/fr.php index 263f959..e4a8f26 100644 --- a/public/lang/fr.php +++ b/public/lang/fr.php @@ -2030,4 +2030,47 @@ Cliquez simplement sur le lien pour vous connecter.
    ', 'tvalidat_success_msg' => 'Un nouveau lien de validation vient d’être envoyé à votre adresse email.', 'tvalidat_err_lien_invalide' => 'Lien invalide.', 'tvalidat_err_lien_expire' => 'Le lien de validation a expiré.', + + 'mailing_clients_brand_bas' => 'USLUGI.FR', + 'mailing_clients_cta_open_site' => 'Ouvrir le site', + 'mailing_clients_footer_thanks' => 'Merci', + + 'mailing_clients_title' => 'Mailing clients', + 'mailing_clients_subject_label' => 'Sujet du message', + 'mailing_clients_body_label' => 'Contenu du message', + 'mailing_clients_body_help' => 'Le texte sera envoyé comme email HTML avec l’en-tête USLUGI.FR et un bouton rouge « Ouvrir le site ».', + + 'mailing_clients_list_title' => 'Liste des clients', + + 'mailing_clients_filter_email' => 'Filtre par email', + 'mailing_clients_email_all' => 'Tous les clients', + 'mailing_clients_email_with' => 'Avec email', + 'mailing_clients_email_without' => 'Sans email', + + 'mailing_clients_filter_city' => 'Filtre par ville', + 'mailing_clients_city_all' => 'Toutes les villes', + + 'mailing_clients_search_label' => 'Recherche', + 'mailing_clients_search_ph' => 'Prénom, nom, email, téléphone...', + + 'mailing_clients_select_all_visible' => 'Sélectionner tous les clients visibles', + 'mailing_clients_send_btn' => 'Envoyer un message aux clients sélectionnés', + + 'mailing_clients_th_prenom' => 'Prénom', + 'mailing_clients_th_nom' => 'Nom', + 'mailing_clients_th_email' => 'Email', + 'mailing_clients_th_phone' => 'Téléphone', + 'mailing_clients_th_city' => 'Ville', + 'mailing_clients_th_created' => 'Date d’inscription', + + 'mailing_clients_none' => 'Aucun client.', + + 'mailing_clients_sent_count' => 'Messages envoyés : %d / %d', + + 'mailing_clients_err_send' => 'Erreur lors de l’envoi.', + 'mailing_clients_err_empty' => 'Le sujet et le contenu du message sont obligatoires.', + 'mailing_clients_err_no_recipients' => 'Sélectionnez au moins un client.', + 'mailing_clients_err_csrf' => 'Erreur de sécurité. Rechargez la page.', + 'mailing_clients_err_no_emails' => 'Les clients sélectionnés n’ont pas d’adresse email.', + 'mail_clients' => 'Mailing clients', ]; diff --git a/public/lang/ru.php b/public/lang/ru.php index 3aa1488..87bf195 100644 --- a/public/lang/ru.php +++ b/public/lang/ru.php @@ -2040,4 +2040,47 @@ return [ 'tvalidat_success_msg' => 'Новая ссылка для подтверждения была отправлена на ваш email.', 'tvalidat_err_lien_invalide' => 'Недействительная ссылка.', 'tvalidat_err_lien_expire' => 'Срок действия ссылки подтверждения истёк.', + + 'mailing_clients_brand_bas' => 'USLUGI.FR', + 'mailing_clients_cta_open_site' => 'Открыть сайт', + 'mailing_clients_footer_thanks' => 'Спасибо', + + 'mailing_clients_title' => 'Маилинг клиентам', + 'mailing_clients_subject_label' => 'Тема письма', + 'mailing_clients_body_label' => 'Текст письма', + 'mailing_clients_body_help' => 'Текст будет отправлен как HTML-письмо с шапкой USLUGI.FR и красной кнопкой «Открыть сайт».', + + 'mailing_clients_list_title' => 'Список клиентов', + + 'mailing_clients_filter_email' => 'Фильтр по email', + 'mailing_clients_email_all' => 'Все клиенты', + 'mailing_clients_email_with' => 'С email', + 'mailing_clients_email_without' => 'Без email', + + 'mailing_clients_filter_city' => 'Фильтр по городу', + 'mailing_clients_city_all' => 'Все города', + + 'mailing_clients_search_label' => 'Поиск', + 'mailing_clients_search_ph' => 'Имя, фамилия, email, телефон...', + + 'mailing_clients_select_all_visible' => 'Выбрать всех в текущем списке', + 'mailing_clients_send_btn' => 'Отправить письмо выбранным клиентам', + + 'mailing_clients_th_prenom' => 'Имя', + 'mailing_clients_th_nom' => 'Фамилия', + 'mailing_clients_th_email' => 'Email', + 'mailing_clients_th_phone' => 'Телефон', + 'mailing_clients_th_city' => 'Город', + 'mailing_clients_th_created' => 'Дата регистрации', + + 'mailing_clients_none' => 'Нет клиентов.', + + 'mailing_clients_sent_count' => 'Отправлено писем: %d / %d', + + 'mailing_clients_err_send' => 'Ошибка при отправке.', + 'mailing_clients_err_empty' => 'Тема и текст письма обязательны.', + 'mailing_clients_err_no_recipients' => 'Выберите хотя бы одного клиента.', + 'mailing_clients_err_csrf' => 'Ошибка безопасности. Обновите страницу.', + 'mailing_clients_err_no_emails' => 'У выбранных клиентов нет email.', + 'mail_clients' => 'Маилинг клиентам', ]; diff --git a/push/cron.php b/push/cron.php index 09d04f9..aae0c27 100644 --- a/push/cron.php +++ b/push/cron.php @@ -1,5 +1,5 @@