426 lines
18 KiB
PHP
426 lines
18 KiB
PHP
<?php
|
||
// /admin/gerer_praticiens.php
|
||
require_once __DIR__ . '/../../public/includes/init_admin.php';
|
||
|
||
// reauth (10 min)
|
||
$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;
|
||
}
|
||
|
||
// Session sûre
|
||
if (session_status() !== PHP_SESSION_ACTIVE) { @session_start(); }
|
||
|
||
|
||
if ($pdo && method_exists($pdo, 'setAttribute')) {
|
||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||
}
|
||
|
||
// ===== Accès admin praticien strict =====
|
||
const ADMIN_PRACTICIEN_ID = 88888888; // <-- adapte à ton ID
|
||
$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['admin_access_denied'] ?? 'Доступ запрещён.', ENT_QUOTES, 'UTF-8')
|
||
. '</div></div>';
|
||
exit;
|
||
}
|
||
|
||
// =====MAILER PHP =====
|
||
require_once __DIR__ . '/../../includes/mailer.php';
|
||
|
||
use PHPMailer\PHPMailer\PHPMailer;
|
||
use PHPMailer\PHPMailer\Exception;
|
||
|
||
// CSRF robuste
|
||
if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token']) || strlen($_SESSION['csrf_token']) < 32) {
|
||
try { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); }
|
||
catch (Throwable $e) { $_SESSION['csrf_token'] = bin2hex(openssl_random_pseudo_bytes(32)); }
|
||
}
|
||
$CSRF = $_SESSION['csrf_token'];
|
||
|
||
// ==================== Paramètres ====================
|
||
$DATE_FMT_TABLE = $translations['date_format_table'] ?? 'd.m.Y';
|
||
$DATE_FMT_MAIL = $translations['date_format_mail'] ?? 'd.m.Y';
|
||
$RENEW_URL = 'https://uslugi.fr/public/praticien/resiliation.php'; // <- adapte si besoin
|
||
|
||
// ==================== Helpers ====================
|
||
function e($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||
|
||
function latestPaymentFor(PDO $pdo, int $praticienId): ?array {
|
||
$sql = "
|
||
SELECT p.id, p.statut, p.methode, p.date_paiement
|
||
FROM paiement p
|
||
WHERE p.praticien_id = :pid
|
||
ORDER BY p.date_paiement DESC, p.id DESC
|
||
LIMIT 1
|
||
";
|
||
$st = $pdo->prepare($sql);
|
||
$st->execute([':pid'=>$praticienId]);
|
||
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||
return $row ?: null;
|
||
}
|
||
|
||
function computeAboState(?string $dateExpiration, ?string $lastPayStatus, int $warningDays = 10): string {
|
||
if (!$dateExpiration) return 'expired';
|
||
|
||
try {
|
||
$today = new DateTimeImmutable('today');
|
||
$exp = new DateTimeImmutable($dateExpiration);
|
||
} catch (Throwable $e) {
|
||
return 'expired';
|
||
}
|
||
|
||
if ($exp < $today) return 'expired';
|
||
|
||
$diff = $today->diff($exp);
|
||
$daysLeft = (int)$diff->days;
|
||
|
||
if ($daysLeft <= $warningDays) return 'expiring';
|
||
|
||
return 'active';
|
||
}
|
||
|
||
function buildMailHtml(array $t, string $headlineKey, string $leadKey, ?string $dateFinFmt, string $renewUrl, string $btnLabel): string {
|
||
$headline = htmlspecialchars($t[$headlineKey] ?? '', ENT_QUOTES, 'UTF-8');
|
||
|
||
$leadRaw = $t[$leadKey] ?? '';
|
||
if ($dateFinFmt) $leadRaw = str_replace('{DATE}', $dateFinFmt, $leadRaw);
|
||
$lead = nl2br(htmlspecialchars($leadRaw, ENT_QUOTES, 'UTF-8'));
|
||
|
||
$ctaLabel = htmlspecialchars($btnLabel, ENT_QUOTES, 'UTF-8');
|
||
$help = nl2br(htmlspecialchars($t['mail_footer_help'] ?? '', ENT_QUOTES, 'UTF-8'));
|
||
|
||
$labelAbo = htmlspecialchars($t['mail_label_abo'] ?? 'Абонемент', ENT_QUOTES, 'UTF-8');
|
||
$labelDate = htmlspecialchars($t['mail_label_date_fin'] ?? 'Дата окончания', ENT_QUOTES, 'UTF-8');
|
||
$labelState = htmlspecialchars($t['mail_label_statut'] ?? 'Статус', ENT_QUOTES, 'UTF-8');
|
||
|
||
$valueAbo = htmlspecialchars($t['mail_value_abo'] ?? '', ENT_QUOTES, 'UTF-8');
|
||
$valueDate = htmlspecialchars($dateFinFmt ?? '', ENT_QUOTES, 'UTF-8');
|
||
$valueState = htmlspecialchars($t['mail_value_statut'] ?? '', ENT_QUOTES, 'UTF-8');
|
||
|
||
$brand = '<img src="https://uslugi.fr/public/uploads/img/logo_mail.png" width="150" alt="USLUGI.FR">';
|
||
$brandBas = 'УСЛУГИ.ФР';
|
||
$merciLine = htmlspecialchars($t['merci_simple'] ?? 'Спасибо', ENT_QUOTES, 'UTF-8');
|
||
$renewUrl = htmlspecialchars($renewUrl, ENT_QUOTES, 'UTF-8');
|
||
|
||
$rowsHtml = '';
|
||
if ($valueAbo !== '') {
|
||
$rowsHtml .= '<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;border-bottom:1px solid #e5e7eb;">'.$labelAbo.'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.$valueAbo.'</td></tr>';
|
||
}
|
||
if ($valueDate !== '') {
|
||
$rowsHtml .= '<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;'.($valueState !== '' ? 'border-bottom:1px solid #e5e7eb;' : '').'">'.$labelDate.'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.$valueDate.'</td></tr>';
|
||
}
|
||
if ($valueState !== '') {
|
||
$rowsHtml .= '<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.$labelState.'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.$valueState.'</td></tr>';
|
||
}
|
||
|
||
$cardHtml = ($rowsHtml !== '')
|
||
? '<tr><td style="padding:16px 24px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:8px;">'.$rowsHtml.'</table></td></tr>'
|
||
: '';
|
||
|
||
return '<!DOCTYPE html><html lang="ru"><head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>'.$headline.'</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:18px;font-weight:600;line-height:1.4;">'.$headline.'</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px 24px;color:#111;font-size:16px;line-height:1.6;">'.$lead.'</td>
|
||
</tr>
|
||
'.$cardHtml.'
|
||
<tr>
|
||
<td align="center" style="padding:8px 24px 24px 24px;">
|
||
<a href="'.$renewUrl.'" target="_blank" style="display:inline-block;text-decoration:none;padding:12px 20px;border-radius:999px;background:#E03C3F;color:#fff;font-weight:600;">'.$ctaLabel.'</a>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px 24px 24px 24px;color:#111;font-size:14px;line-height:1.6;">'.$help.'</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.' — '.$merciLine.'</td>
|
||
</tr>
|
||
</table>
|
||
</td></tr>
|
||
</table>
|
||
</body></html>';
|
||
}
|
||
|
||
function sendReminderEmailPhpMailer(string $toEmail, string $toName, string $subject, string $html, string $text = ''): bool {
|
||
global $MAILCFG;
|
||
|
||
if (!filter_var($toEmail, FILTER_VALIDATE_EMAIL)) return false;
|
||
if (empty($MAILCFG['smtp']['host'])) return false;
|
||
|
||
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
|
||
|
||
try {
|
||
$mail->isSMTP();
|
||
$mail->Host = $MAILCFG['smtp']['host'];
|
||
$mail->Port = (int)($MAILCFG['smtp']['port'] ?? 587);
|
||
$mail->SMTPAuth = (bool)($MAILCFG['smtp']['auth'] ?? true);
|
||
|
||
$sec = $MAILCFG['smtp']['secure'] ?? 'tls';
|
||
if ($sec === 'ssl') $mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
|
||
else $mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
|
||
|
||
$mail->Username = $MAILCFG['smtp']['username'] ?? '';
|
||
$mail->Password = $MAILCFG['smtp']['password'] ?? '';
|
||
|
||
$mail->Timeout = 10;
|
||
$mail->SMTPKeepAlive = false;
|
||
$mail->SMTPAutoTLS = true;
|
||
$mail->SMTPDebug = 0;
|
||
|
||
$mail->CharSet = 'UTF-8';
|
||
$mail->Encoding = 'base64';
|
||
$mail->XMailer = '';
|
||
|
||
if (!empty($MAILCFG['bounce'])) $mail->Sender = $MAILCFG['bounce'];
|
||
|
||
$from = $MAILCFG['from'] ?? ['contact@uslugi.fr', 'USLUGI.FR'];
|
||
$rpto = $MAILCFG['reply_to'] ?? ['contact@uslugi.fr', 'USLUGI.FR'];
|
||
|
||
$mail->setFrom($from[0], $from[1] ?? '');
|
||
if (!empty($rpto[0])) $mail->addReplyTo($rpto[0], $rpto[1] ?? '');
|
||
|
||
$mail->addAddress($toEmail, $toName ?: '');
|
||
|
||
if (!empty($MAILCFG['suppress_auto'])) {
|
||
$mail->addCustomHeader('Auto-Submitted', 'auto-generated');
|
||
$mail->addCustomHeader('X-Auto-Response-Suppress', 'All');
|
||
}
|
||
|
||
$mail->isHTML(true);
|
||
$mail->Subject = $subject;
|
||
$mail->Body = $html;
|
||
|
||
if (trim($text) === '') {
|
||
$text = trim(strip_tags(str_replace(['<br>', '<br/>', '<br />'], "\n", $html)));
|
||
}
|
||
$mail->AltBody = $text;
|
||
|
||
return $mail->send();
|
||
|
||
} catch (\Throwable $e) {
|
||
error_log('PHPMailer reminder send exception: ' . $e->getMessage());
|
||
if (!empty($mail->ErrorInfo)) error_log('PHPMailer reminder ErrorInfo: ' . $mail->ErrorInfo);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ==================== Traitement POST: envoi mail ====================
|
||
$flash = null;
|
||
if (($_POST['do'] ?? '') === 'send_mail') {
|
||
try {
|
||
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) { throw new Exception('CSRF'); }
|
||
$pid = (int)($_POST['id'] ?? 0);
|
||
if ($pid <= 0) throw new Exception('ID');
|
||
|
||
$st = $pdo->prepare("
|
||
SELECT pr.id, pr.nom, pr.prenom, pr.email, pr.date_expiration, ab.nom AS abo_nom
|
||
FROM praticien pr
|
||
LEFT JOIN abonnement ab ON ab.id = pr.abonnement_id
|
||
WHERE pr.id = ?
|
||
LIMIT 1
|
||
");
|
||
$st->execute([$pid]);
|
||
$pr = $st->fetch(PDO::FETCH_ASSOC);
|
||
if (!$pr) throw new Exception('Not found');
|
||
|
||
$last = latestPaymentFor($pdo, $pid);
|
||
$state = computeAboState($pr['date_expiration'] ?? null, $last['statut'] ?? null);
|
||
|
||
if ($state === 'active') {
|
||
$flash = ['type'=>'warning','msg'=> e($translations['flash_pas_necessaire'] ?? 'Pas nécessaire.')];
|
||
} else {
|
||
if ($state === 'expired') {
|
||
$subjectKey='mail_subject_expired'; $headlineKey='mail_headline_expired'; $leadKey='mail_lead_expired';
|
||
} else {
|
||
$subjectKey='mail_subject_expiring'; $headlineKey='mail_headline_expiring'; $leadKey='mail_lead_expiring';
|
||
}
|
||
|
||
$subject = $translations[$subjectKey] ?? ($translations['mail_subject_default'] ?? 'Abonnement');
|
||
$btn = $translations['mail_btn_renew'] ?? 'Renouveler';
|
||
|
||
$dateFinFmt = '';
|
||
if (!empty($pr['date_expiration'])) {
|
||
try { $dateFinFmt = (new DateTimeImmutable($pr['date_expiration']))->format($DATE_FMT_MAIL); } catch(Throwable $e){}
|
||
}
|
||
|
||
$html = buildMailHtml($translations, $headlineKey, $leadKey, $dateFinFmt ?: null, $RENEW_URL, $btn);
|
||
|
||
$destName = trim(($pr['prenom'] ?? '').' '.($pr['nom'] ?? ''));
|
||
if ($destName === '') $destName = ($translations['mail_default_name'] ?? 'Collègue');
|
||
|
||
$toEmail = (string)$pr['email'];
|
||
$toName = $destName;
|
||
|
||
if (sendReminderEmailPhpMailer($toEmail, $toName, $subject, $html)) {
|
||
$msg = ($translations['flash_envoi_ok'] ?? 'Envoyé: {EMAIL}');
|
||
$msg = str_replace('{EMAIL}', e($pr['email']), $msg);
|
||
$flash = ['type'=>'success','msg'=>$msg];
|
||
} else {
|
||
$flash = ['type'=>'danger','msg'=> e($translations['flash_envoi_echec'] ?? 'Échec envoi.')];
|
||
}
|
||
}
|
||
} catch(Throwable $e) {
|
||
$flash = ['type'=>'danger','msg'=> e($translations['flash_erreur'] ?? 'Erreur')];
|
||
}
|
||
}
|
||
|
||
// ==================== Récupération liste praticiens ====================
|
||
$sql = "
|
||
SELECT
|
||
pr.id, pr.titre, pr.nom, pr.prenom, pr.email, pr.photo,
|
||
pr.date_expiration, ab.nom AS abo_nom,
|
||
(
|
||
SELECT p.statut FROM paiement p
|
||
WHERE p.praticien_id = pr.id
|
||
ORDER BY p.date_paiement DESC, p.id DESC
|
||
LIMIT 1
|
||
) AS pay_statut
|
||
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);
|
||
|
||
// ==================== Vue ====================
|
||
?>
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title><?= e($translations['titre_page_gerer_praticiens'] ?? 'Gestion praticiens') ?></title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<style>
|
||
.searchbar { display:flex; gap:.5rem; align-items:center; margin-bottom:1rem; }
|
||
.badge { display:inline-block; padding:.25rem .5rem; border-radius:9999px; font-size:.85rem; border:1px solid transparent; }
|
||
.badge-active { background:#198754; border-color:#27ae60; color: #2e07a3ff;}
|
||
.badge-expiring{ background:#fff8e6; border-color:#f39c12; color: #2e07a3ff; }
|
||
.badge-expired { background:#fdecea; border-color:#c0392b; color: #2e07a3ff; }
|
||
.minw-120{ min-width:120px; }
|
||
.photo { width:40px; height:40px; object-fit:cover; border-radius:8px; }
|
||
</style>
|
||
</head>
|
||
|
||
<div class="shadow p-4 rounded bg-light">
|
||
<h2 class="mb-3 text-center"><?= e($translations['h2_gerer_abos'] ?? '👩⚕️ Rappels d’abonnement') ?></h2>
|
||
|
||
<?php if ($flash): ?>
|
||
<div class="alert alert-<?= e($flash['type']) ?>"><?= $flash['msg'] ?></div>
|
||
<?php endif; ?>
|
||
|
||
<div class="mb-3">
|
||
<input type="text" class="form-control" id="filter"
|
||
placeholder="<?= e($translations['ph_recherche'] ?? 'Recherche (nom, prénom, e-mail, abonnement)') ?>">
|
||
</div>
|
||
|
||
<div class="table-responsive">
|
||
<table class="table table-bordered table-hover align-middle mobile-cards" id="tab">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th style="width:56px;"></th>
|
||
<th><?= e($translations['th_nom'] ?? 'Nom') ?></th>
|
||
<th><?= e($translations['th_prenom'] ?? 'Prénom') ?></th>
|
||
<th><?= e($translations['th_email'] ?? 'E-mail') ?></th>
|
||
<th class="minw-120"><?= e($translations['th_abonnement'] ?? 'Tarif') ?></th>
|
||
<th class="minw-120"><?= e($translations['th_statut_paiement'] ?? 'Statut paiement') ?></th>
|
||
<th class="minw-120"><?= e($translations['th_date_fin'] ?? 'Fin') ?></th>
|
||
<th class="minw-120"><?= e($translations['th_statut'] ?? 'Статус') ?></th>
|
||
<th class="text-center" style="width:160px;"><?= e($translations['th_action'] ?? 'Action') ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($rows as $r):
|
||
$name = trim(($r['titre'] ? $r['titre'].' ' : '').$r['prenom'].' '.$r['nom']);
|
||
$photo = !empty($r['photo']) ? '/public/uploads/'.e($r['photo']) : '/public/uploads/img/user_placeholder.png';
|
||
$state = computeAboState($r['date_expiration'] ?? null, $r['pay_statut'] ?? null);
|
||
|
||
$labelAboState = [
|
||
'active' => $translations['statut_active'] ?? 'Active',
|
||
'expiring' => $translations['statut_expiring'] ?? 'Bientôt',
|
||
'expired' => $translations['statut_expired'] ?? 'Expirée',
|
||
][$state] ?? ($translations['statut_unknown'] ?? '—');
|
||
|
||
$badgePay = (function($st,$t){
|
||
if ($st === 'payé') return '<span class="badge badge-active">'.e($t['pay_ok'] ?? 'Payé').'</span>';
|
||
if ($st === 'en_attente') return '<span class="badge badge-expiring">'.e($t['pay_wait']?? 'En attente').'</span>';
|
||
if ($st === 'échoué') return '<span class="badge badge-expired">'.e($t['pay_ko'] ?? 'Échoué').'</span>';
|
||
return '<span class="badge">'.e((string)$st).'</span>';
|
||
})($r['pay_statut'] ?? '', $translations);
|
||
|
||
$dateFinFmt = '';
|
||
if (!empty($r['date_expiration'])) {
|
||
try { $dateFinFmt = (new DateTimeImmutable($r['date_expiration']))->format($DATE_FMT_TABLE); } catch(Throwable $e){}
|
||
}
|
||
|
||
$hay = mb_strtolower($name.' '.($r['email']??'').' '.($r['abo_nom']??''), 'UTF-8');
|
||
?>
|
||
<tr data-hay="<?= e($hay) ?>">
|
||
<td><img src="<?= $photo ?>" alt="" class="photo"></td>
|
||
<td><?= e($r['nom']) ?></td>
|
||
<td><?= e($r['prenom']) ?></td>
|
||
<td><?= e($r['email']) ?></td>
|
||
<td><?php $k=trim((string)($r['abo_nom']??'')); echo e($translations[$k] ?? $k); ?></td>
|
||
|
||
<td><?= $badgePay ?></td>
|
||
<td><?= e($dateFinFmt) ?></td>
|
||
<td>
|
||
<?php if ($state === 'active'): ?>
|
||
<span class="badge badge-active"><?= e($labelAboState) ?></span>
|
||
<?php elseif ($state === 'expiring'): ?>
|
||
<span class="badge badge-expiring"><?= e($labelAboState) ?></span>
|
||
<?php else: ?>
|
||
<span class="badge badge-expired"><?= e($labelAboState) ?></span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td class="text-center">
|
||
<form method="post" class="d-inline">
|
||
<input type="hidden" name="csrf" value="<?= e($CSRF) ?>">
|
||
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
|
||
<button name="do" value="send_mail" class="btn btn-sm btn-outline-primary"
|
||
onclick="return confirm('<?= e($translations['confirm_envoyer_rappel'] ?? 'Envoyer un e-mail de rappel ?') ?>')">
|
||
<?= e($translations['btn_envoyer_rappel'] ?? 'Envoyer rappel') ?>
|
||
</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
|
||
<?php if (empty($rows)): ?>
|
||
<tr><td colspan="9" class="text-muted"><?= e($translations['aucun_praticien'] ?? 'Aucun praticien.') ?></td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Filtre client simple
|
||
document.getElementById('filter').addEventListener('input', function(){
|
||
const q = (this.value || '').trim().toLowerCase();
|
||
document.querySelectorAll('#tab tbody tr').forEach(tr=>{
|
||
const hay = (tr.dataset.hay || '').toLowerCase();
|
||
tr.style.display = !q || hay.includes(q) ? '' : 'none';
|
||
});
|
||
});
|
||
</script>
|