Files

416 lines
17 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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>