Mailing_client, indes/tools, les deux fichiers lang

This commit is contained in:
Oleg Barykin committed 2026-03-22 16:42:06 +01:00
1 parent f852c0516a
commit 865de8a976
5 files changed
+513 -7

No files matched your search

+4
View File
@@ -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 @@
<li onclick="location.href='<?php echo h(admin_url('mail_praticiens')) ?>'">
<?php echo h($translations['mail_praticiens'] ?? 'Рассылка практикующим') ?>
</li>
<li onclick="location.href='<?php echo h(admin_url('mailing_client')) ?>'">
<?php echo h($translations['mail_clients'] ?? 'Рассылка клиентам') ?>
</li>
<li onclick="location.href='<?php echo h(admin_url('connecter_en_admin')) ?>'">
<?php echo h($translations['connecter_en_admin'] ?? 'Войти от имени пользователя') ?>
+416
View File
@@ -0,0 +1,416 @@
<?php
require_once __DIR__ . '/../../public/includes/init_admin.php';
const ADMIN_PRACTICIEN_ID = 88888888;
$me = (int) ($_SESSION['reference_id'] ?? 0);
if ($me !== ADMIN_PRACTICIEN_ID) {
http_response_code(403);
echo '<div class="container py-5"><div class="alert alert-danger">'
. htmlspecialchars($translations['non_autorise'] ?? 'Доступ запрещён.', ENT_QUOTES, 'UTF-8')
. '</div></div>';
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 = '<img src="https://uslugi.fr/public/uploads/img/logo_mail.png" width="150" alt="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 '<!DOCTYPE html><html lang="ru"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>' . e($subject) . '</title>
</head>
<body style="margin:0;padding:0;background:#f4f6f8;font-family:Arial,Helvetica,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f4f6f8;padding:24px 0;">
<tr><td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:8px;overflow:hidden;">
<tr>
<td style="background:#38378B;color:#fff;padding:20px 24px;font-size:18px;font-weight:600;">' . $brand . '</td>
</tr>
<tr>
<td style="padding:24px 24px 8px 24px;color:#111;font-size:16px;line-height:1.6;">' . $bodyHtml . '</td>
</tr>
<tr>
<td align="center" style="padding:8px 24px 24px 24px;">
<a href="' . e($ctaUrl) . '" target="_blank"
style="display:inline-block;text-decoration:none;padding:12px 20px;border-radius:999px;background:#E03C3F;color:#fff;font-weight:600;">
' . e($ctaLabel) . '
</a>
</td>
</tr>
</table>
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="margin-top:12px;">
<tr>
<td style="text-align:center;color:#9ca3af;font-size:12px;padding:8px 0;">© ' . date('Y') . ' ' . $brandBas . ' — ' . e($thanks) . '</td>
</tr>
</table>
</td></tr>
</table>
</body></html>';
}
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);
?>
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-3"><?php echo e($translations['mailing_clients_title'] ?? 'Массовая рассылка клиентам') ?></h2>
<?php if ($flash): ?>
<div class="alert alert-<?php echo e($flash['type']) ?>"><?php echo e($flash['msg']) ?></div>
<?php endif; ?>
<form method="post" id="mailingForm">
<input type="hidden" name="csrf" value="<?php echo e($CSRF) ?>">
<input type="hidden" name="do" value="send_mailing">
<div class="mb-3">
<label class="form-label"><?php echo e($translations['mailing_clients_subject_label'] ?? 'Тема письма') ?></label>
<input type="text" name="subject" class="form-control" required>
</div>
<div class="mb-4">
<label class="form-label"><?php echo e($translations['mailing_clients_body_label'] ?? 'Текст письма') ?></label>
<textarea id="mailBody" name="body" class="form-control" rows="8"></textarea>
<div class="form-text">
<?php echo e($translations['mailing_clients_body_help'] ?? 'Текст будет отправлен как HTML-письмо с шапкой USLUGI.FR и красной кнопкой «Открыть сайт».') ?>
</div>
</div>
<hr class="my-4">
<h4 class="mb-3"><?php echo e($translations['mailing_clients_list_title'] ?? 'Список клиентов') ?></h4>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label"><?php echo e($translations['mailing_clients_filter_email'] ?? 'Фильтр по email') ?></label>
<select id="filter_email" class="form-select">
<option value="all"><?php echo e($translations['mailing_clients_email_all'] ?? 'Все клиенты') ?></option>
<option value="with_email"><?php echo e($translations['mailing_clients_email_with'] ?? 'С email') ?></option>
<option value="without_email"><?php echo e($translations['mailing_clients_email_without'] ?? 'Без email') ?></option>
</select>
</div>
<div class="col-md-4">
<label class="form-label"><?php echo e($translations['mailing_clients_filter_city'] ?? 'Фильтр по городу') ?></label>
<select id="filter_city" class="form-select">
<option value=""><?php echo e($translations['mailing_clients_city_all'] ?? 'Все города') ?></option>
<?php foreach ($cityOptions as $city): ?>
<option value="<?php echo e($city) ?>"><?php echo e($city) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label"><?php echo e($translations['mailing_clients_search_label'] ?? 'Поиск') ?></label>
<input
type="text"
id="filter_search"
class="form-control"
placeholder="<?php echo e($translations['mailing_clients_search_ph'] ?? 'Имя, фамилия, email, телефон...') ?>"
>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="form-check">
<input type="checkbox" id="chk_all" class="form-check-input">
<label for="chk_all" class="form-check-label">
<?php echo e($translations['mailing_clients_select_all_visible'] ?? 'Выбрать всех в текущем списке') ?>
</label>
</div>
<button type="submit" class="btn btn-danger" style="border-radius:999px;">
<?php echo e($translations['mailing_clients_send_btn'] ?? 'Отправить письмо выбранным') ?>
</button>
</div>
<div class="table-responsive">
<table class="table table-bordered table-hover align-middle" id="tabClients">
<thead class="table-light">
<tr>
<th style="width:40px;"></th>
<th><?php echo e($translations['mailing_clients_th_prenom'] ?? 'Имя') ?></th>
<th><?php echo e($translations['mailing_clients_th_nom'] ?? 'Фамилия') ?></th>
<th><?php echo e($translations['mailing_clients_th_email'] ?? 'Email') ?></th>
<th><?php echo e($translations['mailing_clients_th_phone'] ?? 'Телефон') ?></th>
<th><?php echo e($translations['mailing_clients_th_city'] ?? 'Город') ?></th>
<th><?php echo e($translations['mailing_clients_th_created'] ?? 'Дата регистрации') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r):
$email = trim((string) ($r['email'] ?? ''));
$emailStatus = ($email !== '') ? 'with_email' : 'without_email';
$createdFmt = '';
if (! empty($r['created_at'])) {
try {
$created = new DateTimeImmutable($r['created_at']);
$createdFmt = $created->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'
);
?>
<tr
data-email-status="<?php echo e($emailStatus) ?>"
data-city="<?php echo e($ville) ?>"
data-hay="<?php echo e($hay) ?>"
>
<td>
<input
type="checkbox"
class="form-check-input row-chk"
name="recipients[]"
value="<?php echo (int)$r['id'] ?>"
<?php echo $email === '' ? 'disabled' : '' ?>
>
</td>
<td><?php echo e($r['prenom']) ?></td>
<td><?php echo e($r['nom']) ?></td>
<td><?php echo e($r['email']) ?></td>
<td><?php echo e($r['telephone']) ?></td>
<td><?php echo e($r['ville']) ?></td>
<td><?php echo e($createdFmt) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?>
<tr>
<td colspan="7" class="text-muted text-center">
<?php echo e($translations['mailing_clients_none'] ?? 'Нет клиентов.') ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</form>
</div>
<!-- TinyMCE -->
<script src="https://cdn.tiny.cloud/1/l9t0aozxqrwrd6cv3gbopqgt7bi299cjtwwxvyhylwa1kjey/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<script>
tinymce.init({
selector: '#mailBody',
menubar: false,
height: 260,
language: 'ru',
plugins: 'link lists',
toolbar: 'undo redo | bold italic underline | bullist numlist | link removeformat',
branding: false
});
// Filtres JS
(function () {
const emailSel = document.getElementById('filter_email');
const citySel = document.getElementById('filter_city');
const searchInp = document.getElementById('filter_search');
const rows = Array.from(document.querySelectorAll('#tabClients tbody tr'));
const chkAll = document.getElementById('chk_all');
function applyFilters() {
const emailFilter = emailSel.value;
const cityFilter = (citySel.value || '').trim().toLowerCase();
const q = (searchInp.value || '').trim().toLowerCase();
rows.forEach(tr => {
let ok = true;
const trEmailStatus = tr.dataset.emailStatus || '';
const trCity = (tr.dataset.city || '').trim().toLowerCase();
const hay = (tr.dataset.hay || '').toLowerCase();
if (emailFilter !== 'all' && trEmailStatus !== emailFilter) ok = false;
if (cityFilter && trCity !== cityFilter) ok = false;
if (q && !hay.includes(q)) ok = false;
tr.style.display = ok ? '' : 'none';
});
}
emailSel.addEventListener('change', applyFilters);
citySel.addEventListener('change', applyFilters);
searchInp.addEventListener('input', applyFilters);
if (chkAll) {
chkAll.addEventListener('change', function () {
const checked = this.checked;
rows.forEach(tr => {
if (tr.style.display === 'none') return;
const cb = tr.querySelector('.row-chk');
if (cb && !cb.disabled) {
cb.checked = checked;
}
});
});
}
})();
</script>
+43
View File
@@ -2030,4 +2030,47 @@ Cliquez simplement sur le lien pour vous connecter.<br>',
'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',
];
+43
View File
@@ -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' => 'Маилинг клиентам',
];
+7 -7
View File
@@ -1,5 +1,5 @@
<?php
declare(strict_types=1);
declare (strict_types = 1);
header('Content-Type: text/plain; charset=UTF-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
@@ -12,23 +12,23 @@ ini_set('error_log', __DIR__ . '/webcron_error.log');
// ===== sécurité =====
const CRON_TOKEN = 'e3e6ba5785a447c3f763a4ca4b8c36480df63a19';
if (!hash_equals(CRON_TOKEN, $_GET['token'] ?? '')) {
if (! hash_equals(CRON_TOKEN, $_GET['token'] ?? '')) {
http_response_code(403);
exit("forbidden\n");
}
// ===== anti double exécution =====
$lockFile = __DIR__ . '/cron.lock';
$lock = fopen($lockFile, 'c');
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) {
$lock = fopen($lockFile, 'c');
if (! $lock || ! flock($lock, LOCK_EX | LOCK_NB)) {
exit("already running\n");
}
// ===== log minimal =====
file_put_contents(__DIR__ . '/webcron.log', date('Y-m-d H:i:s') . " run\n", FILE_APPEND);
file_put_contents(__DIR__ . '/webcron.log', date('Y-m-d H:i:s') . " before worker\n", FILE_APPEND);
// ===== lance ton worker =====
require __DIR__ . '/worker.php';
echo "OK\n";
file_put_contents(__DIR__ . '/webcron.log', date('Y-m-d H:i:s') . " after worker\n", FILE_APPEND);
echo "OK\n";