Init Git Prod Mars 2026
This commit is contained in:
commit
d81efe93b1
362 files changed
+86627
No files matched your search
@@ -0,0 +1,364 @@
|
||||
<?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_praticiens_brand_bas'] ?? 'УСЛУГИ.ФР';
|
||||
$ctaUrl = 'https://uslugi.fr/';
|
||||
$ctaLabel = $translations['mailing_praticiens_cta_open_site'] ?? 'Открыть сайт';
|
||||
$thanks = $translations['mailing_praticiens_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 sendPraticienMail(string $to, string $subject, string $html): bool {
|
||||
// Optionnel: définir From/Reply-To ici, sinon mail_config.php fait foi
|
||||
[$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_praticiens'],
|
||||
]);
|
||||
|
||||
if (!$ok && $err) {
|
||||
error_log('mailing_praticiens 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
|
||||
$in = implode(',', array_fill(0, count($ids), '?'));
|
||||
$sql = "SELECT id, prenom, nom, email FROM praticien WHERE id IN ($in) AND email <> ''";
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute($ids);
|
||||
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$rows) {
|
||||
throw new Exception('NO_EMAILS');
|
||||
}
|
||||
|
||||
$sent = 0;
|
||||
$total = count($rows);
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$email = trim($r['email'] ?? '');
|
||||
if ($email === '') continue;
|
||||
|
||||
// On laisse TinyMCE produire le HTML, on l’utilise tel quel
|
||||
$html = buildAdminMailHtml($subject, $body);
|
||||
if (sendPraticienMail($email, $subject, $html)) {
|
||||
$sent++;
|
||||
}
|
||||
// usleep(100000); // 0.1s
|
||||
}
|
||||
|
||||
$flash = [
|
||||
'type' => 'success',
|
||||
'msg' => ($translations['mailing_praticiens_sent_count'] ?? 'Отправлено писем: %d / %d')
|
||||
? sprintf((string)($translations['mailing_praticiens_sent_count'] ?? 'Отправлено писем: %d / %d'), $sent, $total)
|
||||
: ('Отправлено писем: '.$sent.' / '.$total)
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
$code = $e->getMessage();
|
||||
$msg = $translations['mailing_praticiens_err_send'] ?? 'Ошибка при отправке.';
|
||||
|
||||
if ($code === 'EMPTY') $msg = $translations['mailing_praticiens_err_empty'] ?? 'Тема и текст письма обязательны.';
|
||||
if ($code === 'NO_RECIPIENTS') $msg = $translations['mailing_praticiens_err_no_recipients'] ?? 'Выберите хотя бы одного практикующего.';
|
||||
if ($code === 'CSRF') $msg = $translations['mailing_praticiens_err_csrf'] ?? 'Ошибка безопасности. Обновите страницу.';
|
||||
|
||||
$flash = ['type'=>'danger','msg'=>$msg];
|
||||
}
|
||||
}
|
||||
|
||||
// --------- Récupération des praticiens ---------
|
||||
$sql = "
|
||||
SELECT
|
||||
pr.id,
|
||||
pr.nom,
|
||||
pr.prenom,
|
||||
pr.email,
|
||||
pr.abonnement_id,
|
||||
pr.date_expiration,
|
||||
ab.nom AS abo_nom
|
||||
FROM praticien pr
|
||||
LEFT JOIN abonnement ab ON ab.id = pr.abonnement_id
|
||||
ORDER BY pr.nom ASC, pr.prenom ASC
|
||||
";
|
||||
$rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Préparer options de filtre par abonnement
|
||||
$aboOptions = [];
|
||||
foreach ($rows as $r) {
|
||||
if (!empty($r['abonnement_id'])) {
|
||||
$aid = (int)$r['abonnement_id'];
|
||||
$aboOptions[$aid] = $r['abo_nom'] ?: (($translations['mailing_praticiens_tarif_id'] ?? 'ID %d') ? sprintf((string)($translations['mailing_praticiens_tarif_id'] ?? 'ID %d'), $aid) : ('ID '.$aid));
|
||||
}
|
||||
}
|
||||
ksort($aboOptions);
|
||||
|
||||
$today = new DateTimeImmutable('today');
|
||||
?>
|
||||
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-3"><?= e($translations['mailing_praticiens_title'] ?? 'Массовая рассылка практикующим') ?></h2>
|
||||
|
||||
<?php if ($flash): ?>
|
||||
<div class="alert alert-<?= e($flash['type']) ?>"><?= e($flash['msg']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" id="mailingForm">
|
||||
<input type="hidden" name="csrf" value="<?= e($CSRF) ?>">
|
||||
<input type="hidden" name="do" value="send_mailing">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= e($translations['mailing_praticiens_subject_label'] ?? 'Тема письма') ?></label>
|
||||
<input type="text" name="subject" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label"><?= e($translations['mailing_praticiens_body_label'] ?? 'Текст письма') ?></label>
|
||||
<textarea id="mailBody" name="body" class="form-control" rows="8"></textarea>
|
||||
<div class="form-text">
|
||||
<?= e($translations['mailing_praticiens_body_help'] ?? 'Текст будет отправлен как HTML-письмо с шапкой USLUGI.FR и красной кнопкой «Открыть сайт».') ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h4 class="mb-3"><?= e($translations['mailing_praticiens_list_title'] ?? 'Список практикующих') ?></h4>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><?= e($translations['mailing_praticiens_filter_status'] ?? 'Фильтр по статусу') ?></label>
|
||||
<select id="filter_status" class="form-select">
|
||||
<option value="all"><?= e($translations['mailing_praticiens_status_all'] ?? 'Все практикующие') ?></option>
|
||||
<option value="active"><?= e($translations['mailing_praticiens_status_active'] ?? 'С активной подпиской') ?></option>
|
||||
<option value="expired"><?= e($translations['mailing_praticiens_status_expired'] ?? 'С истекшей подпиской') ?></option>
|
||||
<option value="noabo"><?= e($translations['mailing_praticiens_status_noabo'] ?? 'Без подписки') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><?= e($translations['mailing_praticiens_filter_tarif'] ?? 'Фильтр по тарифу') ?></label>
|
||||
<select id="filter_abo" class="form-select">
|
||||
<option value=""><?= e($translations['mailing_praticiens_tarif_all'] ?? 'Все тарифы') ?></option>
|
||||
<?php foreach ($aboOptions as $aid => $name): ?>
|
||||
<option value="<?= (int)$aid ?>">
|
||||
<?= e($translations[$name] ?? $name) ?>
|
||||
(<?= e(sprintf(
|
||||
$translations['mailing_praticiens_tarif_id_label'] ?? 'ID %d',
|
||||
(int)$aid
|
||||
)) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><?= e($translations['mailing_praticiens_search_label'] ?? 'Поиск') ?></label>
|
||||
<input type="text" id="filter_search" class="form-control" placeholder="<?= e($translations['mailing_praticiens_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"><?= e($translations['mailing_praticiens_select_all_visible'] ?? 'Выбрать всех в текущем списке') ?></label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger" style="border-radius:999px;">
|
||||
<?= e($translations['mailing_praticiens_send_btn'] ?? 'Отправить письмо выбранным') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover align-middle" id="tabPraticiens">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:40px;"></th>
|
||||
<th><?= e($translations['mailing_praticiens_th_prenom'] ?? 'Имя') ?></th>
|
||||
<th><?= e($translations['mailing_praticiens_th_nom'] ?? 'Фамилия') ?></th>
|
||||
<th><?= e($translations['mailing_praticiens_th_email'] ?? 'Email') ?></th>
|
||||
<th><?= e($translations['mailing_praticiens_th_tarif'] ?? 'Тариф') ?></th>
|
||||
<th><?= e($translations['mailing_praticiens_th_exp'] ?? 'Окончание') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $r):
|
||||
$expRaw = $r['date_expiration'] ?? null;
|
||||
$status = 'noabo';
|
||||
$dateFmt = '';
|
||||
|
||||
if ($expRaw) {
|
||||
try {
|
||||
$exp = new DateTimeImmutable($expRaw);
|
||||
$dateFmt = $exp->format('Y-m-d H:i');
|
||||
$status = ($exp < $today) ? 'expired' : 'active';
|
||||
} catch (Throwable $e) {
|
||||
$status = 'noabo';
|
||||
}
|
||||
}
|
||||
|
||||
$hay = mb_strtolower(
|
||||
trim(($r['prenom'] ?? '').' '.($r['nom'] ?? '').' '.($r['email'] ?? '').' '.($r['abo_nom'] ?? '')),
|
||||
'UTF-8'
|
||||
);
|
||||
$aboId = (int)($r['abonnement_id'] ?? 0);
|
||||
?>
|
||||
<tr data-status="<?= e($status) ?>" data-abo="<?= $aboId ?: '' ?>" data-hay="<?= e($hay) ?>">
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
class="form-check-input row-chk"
|
||||
name="recipients[]"
|
||||
value="<?= (int)$r['id'] ?>">
|
||||
</td>
|
||||
<td><?= e($r['prenom']) ?></td>
|
||||
<td><?= e($r['nom']) ?></td>
|
||||
<td><?= e($r['email']) ?></td>
|
||||
<td><?php $k=trim((string)($r['abo_nom']??'')); echo e($translations[$k] ?? $k); ?></td>
|
||||
<td><?= e($dateFmt) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($rows)): ?>
|
||||
<tr><td colspan="6" class="text-muted text-center"><?= e($translations['mailing_praticiens_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 statusSel = document.getElementById('filter_status');
|
||||
const aboSel = document.getElementById('filter_abo');
|
||||
const searchInp = document.getElementById('filter_search');
|
||||
const rows = Array.from(document.querySelectorAll('#tabPraticiens tbody tr'));
|
||||
const chkAll = document.getElementById('chk_all');
|
||||
|
||||
function applyFilters(){
|
||||
const st = statusSel.value;
|
||||
const abo = aboSel.value;
|
||||
const q = (searchInp.value || '').trim().toLowerCase();
|
||||
|
||||
rows.forEach(tr => {
|
||||
let ok = true;
|
||||
const trStatus = tr.dataset.status || '';
|
||||
const trAbo = tr.dataset.abo || '';
|
||||
const hay = (tr.dataset.hay || '').toLowerCase();
|
||||
|
||||
if (st !== 'all' && trStatus !== st) ok = false;
|
||||
if (abo && trAbo !== abo) ok = false;
|
||||
if (q && !hay.includes(q)) ok = false;
|
||||
|
||||
tr.style.display = ok ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
statusSel.addEventListener('change', applyFilters);
|
||||
aboSel.addEventListener('change', applyFilters);
|
||||
searchInp.addEventListener('input', applyFilters);
|
||||
|
||||
// Sélectionner tous les visibles
|
||||
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.checked = checked;
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
Reference in new issue
Block a user