416 lines
17 KiB
PHP
416 lines
17 KiB
PHP
<?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>
|