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 = '
';
+ $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) . '
+
+
+
+
+
+
+
+ | © ' . 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);
+?>
+
+
+
+
+
+
\ 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 @@