Init Git Prod Mars 2026

This commit is contained in:
Oleg Barykin committed 2026-03-05 10:25:39 +01:00
commit d81efe93b1
362 files changed
+86627

No files matched your search

+425
View File
@@ -0,0 +1,425 @@
<?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>