Files
2026-03-05 10:25:39 +01:00

426 lines
18 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
// /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>