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

647 lines
31 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__ . '/../includes/init_client.php';
require_once __DIR__ . '/../../push/push_endpoint.php';
require_once __DIR__ . '/../../sms/notify_rdv.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
/* -------- PHPMailer helper (timeouts ajoutés) -------- */
function send_rdv_email_with_ics_phpmailer(
string $toEmail, string $toName,
string $subject, string $textBody, string $htmlBody,
string $ical, string $filename
): bool {
global $MAILCFG; // vient de /includes/mailer.php
if (!filter_var($toEmail, FILTER_VALIDATE_EMAIL)) {
error_log('PHPMailer RDV: invalid recipient email: ' . var_export($toEmail, true));
return false;
}
if (empty($MAILCFG) || empty($MAILCFG['smtp']['host'])) {
error_log('PHPMailer RDV: MAILCFG missing/incomplete.');
return false;
}
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = $MAILCFG['smtp']['host'];
$mail->Port = (int)$MAILCFG['smtp']['port'];
$mail->SMTPSecure = $MAILCFG['smtp']['secure'] ?? PHPMailer::ENCRYPTION_STARTTLS;
$mail->SMTPAuth = (bool)$MAILCFG['smtp']['auth'];
$mail->Username = $MAILCFG['smtp']['username'] ?? '';
$mail->Password = $MAILCFG['smtp']['password'] ?? '';
// 🔒 robustesse
$mail->Timeout = 8; // seconds (SMTP)
$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 = $htmlBody;
$mail->AltBody = $textBody;
// 📎 Invitation iCalendar
$mail->addStringAttachment(
$ical,
$filename,
'base64',
'text/calendar; method=REQUEST; charset=UTF-8'
);
$mail->send();
return true;
} catch (Throwable $e) {
error_log('PHPMailer RDV send exception: '.$e->getMessage());
if (!empty($mail->ErrorInfo)) {
error_log('PHPMailer RDV ErrorInfo: '.$mail->ErrorInfo);
}
return false;
}
}
/* ================== Valeurs par défaut Online/Offline ================== */
if (!isset($c_online)) { $c_online = 'online'; }
if (!isset($c_offline)) { $c_offline = 'offline'; }
/* ================== CSRF pour l'appel AJAX ================== */
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(16));
}
$CSRF = $_SESSION['csrf_token'];
/* ================== Helpers UI (inchangés) ================== */
function ui_head(string $title): void { ?>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= htmlspecialchars($title) ?></title>
<style>
html,body{height:100%}
body{
margin:0; background:#8CA9FF;
font-family:'Montserrat',system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
display:flex; align-items:center; justify-content:center;
padding:32px 16px;
}
.page-loader{ position:fixed; inset:0; z-index:9999; background:#8CA9FF; display:flex; align-items:center; justify-content:center; transition:opacity .25s ease, visibility .25s ease; }
.hidden{opacity:0; visibility:hidden;}
.loader-card{ background:#fff; color:#38378B; border-radius:22px; padding:20px 22px; box-shadow:0 12px 40px rgba(0,0,0,.08); display:flex; align-items:center; gap:14px; }
.loader-spinner{ width:36px; height:36px; border-radius:50%; border:3px solid rgba(56,55,139,.15); border-top-color:#38378B; animation:spin 1s linear infinite; }
@keyframes spin{ to{ transform:rotate(360deg);} }
.loader-text{ font-weight:700; }
.wrap{ width:min(500px, 100%); margin:0 auto; }
.msg{ background:#fff; color:rgb(33,37,41); border-radius:22px; box-shadow:0 12px 40px rgba(0,0,0,.08); padding:22px; text-align:center; }
.msg h2{ margin:0 0 10px; font-size:16px; font-weight:400; }
.msg p{ margin:10px 0; line-height:1.5; }
.msg .meta{ margin:12px auto 0; padding:12px 14px; border:1px solid #f11e1eff; border-radius:16px; background:#F7F8FF; color:#f11e1eff; font-weight:600; display:inline-block; }
.msg .actions{ margin-top:16px; display:flex; gap:10px; justify-content:center; flex-wrap:wrap; }
.card-notice{ background:#fff; color:#111; border-radius:28px; box-shadow:0 20px 60px rgba(0,0,0,.08); padding:24px 22px; text-align:left; }
.cn-title{ margin:0 0 6px; font-size:clamp(22px,4.8vw,32px); font-weight:800; }
.cn-date{ color:#60646c; font-size:clamp(14px,3.6vw,18px); margin-bottom:12px; }
.notice-list{ list-style:none; padding:0; margin:10px 0 0; display:grid; gap:14px; }
.notice-list li{ display:grid; grid-template-columns:36px 1fr; align-items:flex-start; gap:10px; }
.txt{ line-height:1.5; }
.txt-warn{ color:#E03C3F; font-weight:700; }
.ico{ width:32px; height:32px; border-radius:8px; display:inline-grid; place-items:center; margin-top:2px; background:#fde9ea; flex:0 0 32px; }
.ico svg{ width:24px; height:24px; fill:#E03C3F; }
.ico-dark{ background:#eef0f3; }
.ico-dark svg{ fill:#333; }
.ico-info{ background:#e8f0ff; }
.ico-info svg{ color:#2b64d8; }
.btn{ display:inline-block; text-decoration:none; font-weight:700; border-radius:999px; padding:12px 18px; }
.btn-primary{ background:#E03C3F; color:#fff; box-shadow:0 8px 22px rgba(0,0,0,.12); }
.btn-primary:hover{ filter:brightness(1.05); }
.btn-outline{ background:#fff; color:#38378B; border:1px solid #E0E3EB; }
.btn-outline:hover{ background:#f8faff; }
.msg--ok h2::before{ content:" "; }
.msg--err h2{ color:#666666; }
.msg--err h2::before{ content:"❌ "; }
</style>
</head>
<body>
<div class="page-loader" id="loader" role="status" aria-live="polite">
<div class="loader-card">
<div class="loader-spinner" aria-hidden="true"></div>
<div class="loader-text"><?= htmlspecialchars($GLOBALS['translations']['chargement_validation'] ?? 'Подождите, идёт подтверждение...') ?></div>
</div>
</div>
<div class="wrap" id="result"></div>
<?php }
function ui_foot(): void { ?>
</body></html>
<?php }
/* =================== Helpers e-mail (HTML/Texte) =================== */
function build_email_html(array $data, array $t): string {
$brand = '<img src="https://uslugi.fr/public/uploads/img/logo_mail.png" width="150" alt="USLUGI.FR">';
$brand_bas ='УСЛУГИ.ФР';
return '
<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>USLUGI.FR</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:#ffffff;border-radius:8px;overflow:hidden;">
<tr><td style="background:#38378B;color:#ffffff;padding:20px 24px;font-size:18px;font-weight:600;">'.$brand.'</td></tr>
<tr><td style="padding:24px 24px 8px 24px;color:#111111;font-size:16px;line-height:1.5;">'.nl2br(htmlspecialchars($data['hello_line'])).'</td></tr>
<tr><td style="padding:8px 24px;color:#111111;font-size:16px;line-height:1.6;">'.nl2br(htmlspecialchars($data['intro'])).'</td></tr>
<tr><td style="padding:16px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:8px;">
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;border-bottom:1px solid #e5e7eb;">'.htmlspecialchars($t['email_avec'] ?? 'Avec').'</td><td style="padding:14px 16px;font-size:14px;color:#111111;">'.htmlspecialchars($data['who']).'</td></tr>
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($t['date_label'] ?? 'Date').'</td><td style="padding:14px 16px;font-size:14px;color:#111111;">'.htmlspecialchars($data['when']).'</td></tr>'.
(!empty($data['format_line']) ? '<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($t['format_label'] ?? 'Формат').'</td><td style="padding:14px 16px;font-size:14px;color:#111111;">'.htmlspecialchars($data['format_line']).'</td></tr>' : '').
(!empty($data['address_line']) ? '<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($t['adresse_label_ru'] ?? 'Адрес').'</td><td style="padding:14px 16px;font-size:14px;color:#111111;">'.htmlspecialchars($data['address_line']).'</td></tr>' : '').
'</table></td></tr>'.
(!empty($data['cta_url']) ? '<tr><td align="center" style="padding:8px 24px 24px 24px;"><a href="'.htmlspecialchars($data['cta_url']).'" target="_blank" style="display:inline-block;text-decoration:none;padding:12px 20px;border-radius:999px;background:#E03C3F;color:#ffffff;font-weight:600;">'.htmlspecialchars($data['cta_label']).'</a></td></tr>' : '').
'<tr><td style="padding:8px 24px 24px 24px;color:#111111;font-size:16px;line-height:1.6;">'.nl2br(htmlspecialchars($data['footer'])).'</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').' '.$brand_bas.' — '.htmlspecialchars($t['merci_simple'] ?? 'Merci').'</td></tr>
</table>
</td></tr>
</table>
</body></html>';
}
function build_email_text(array $data, array $t): string {
$lines = [];
$lines[] = $data['hello_line']; $lines[] = '';
$lines[] = $data['intro']; $lines[] = '';
$lines[] = ($t['email_avec'] ?? 'Avec').' '.$data['who'];
$lines[] = ($t['date_label'] ?? 'Date').' '.$data['when'];
if (!empty($data['format_line'])) $lines[] = ($t['format_label'] ?? 'Формат').': '.$data['format_line'];
if (!empty($data['address_line'])) $lines[] = ($t['adresse_label_ru'] ?? 'Адрес').': '.$data['address_line'];
if (!empty($data['cta_url'])) { $lines[] = ''; $lines[] = $data['cta_label'].': '.$data['cta_url']; }
$lines[] = ''; $lines[] = $data['footer'];
return implode("\n", $lines);
}
/* =================== Helpers ICS (inchangés) =================== */
function ics_escape(string $s): string {
$s = str_replace("\\", "\\\\", $s);
$s = str_replace(";", "\;", $s);
$s = str_replace(",", "\,", $s);
$s = preg_replace("/\r\n|\r|\n/", "\\n", $s);
return $s;
}
function build_ics_common_lines(
string $uid, DateTime $start, DateTime $end, string $timezone,
string $summary, string $description, string $location, array $attendees
): string {
$lines = [];
$lines[] = "BEGIN:VCALENDAR";
$lines[] = "VERSION:2.0";
$lines[] = "PRODID:-//УСЛУГИ.ФР//RDV//FR";
$lines[] = "CALSCALE:GREGORIAN";
$lines[] = "METHOD:REQUEST";
$lines[] = "BEGIN:VEVENT";
$lines[] = "UID:{$uid}";
$lines[] = "DTSTAMP:" . gmdate('Ymd\THis\Z');
$lines[] = "DTSTART;TZID={$timezone}:" . $start->format('Ymd\THis');
$lines[] = "DTEND;TZID={$timezone}:" . $end->format('Ymd\THis');
$lines[] = "SUMMARY:" . ics_escape($summary);
$lines[] = "DESCRIPTION:" . ics_escape($description);
$lines[] = "LOCATION:" . ics_escape($location);
$lines[] = "ORGANIZER;CN=УСЛУГИ.ФР:mailto:rdv@uslugi.fr";
foreach ($attendees as $a) {
if (empty($a['email'])) continue;
$cn = ics_escape($a['cn'] ?? '');
$em = $a['email'];
$lines[] = "ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN={$cn};RSVP=TRUE:mailto:{$em}";
}
$lines[] = "STATUS:CONFIRMED";
$lines[] = "END:VEVENT";
$lines[] = "END:VCALENDAR";
return implode("\r\n", $lines) . "\r\n";
}
function build_ics_client(array $rdv, DateTime $start, DateTime $end, string $timezone, string $location, string $ctaUrl, array $t, string $uid): string {
$summary = ($t['rdv_avec'] ?? 'Rendez-vous avec').' '.($t['titre_dr'] ?? 'Dr').' '.$rdv['prenom_praticien'].' '.$rdv['nom_praticien'];
$desc = ($t['votre_rdv_attente'] ?? 'Votre rendez-vous est prévu')."\n\n".
($t['email_avec'] ?? 'Avec').' '.($t['titre_dr'] ?? 'Dr').' '.$rdv['prenom_praticien'].' '.$rdv['nom_praticien']."\n".
'Email: '.$rdv['email_praticien']."\n".
(($rdv['tel_praticien'] ?? '') ? ('Téléphone: '.$rdv['tel_praticien']."\n") : '').
($ctaUrl ? ("\n".($t['consultez_espace_pour_details'] ?? 'Consultez votre espace pour les détails')."\n".$ctaUrl) : '');
$attendees = [
['cn' => $rdv['prenom_praticien'].' '.$rdv['nom_praticien'], 'email' => $rdv['email_praticien']],
['cn' => $rdv['prenom_client'].' '.$rdv['nom_client'], 'email' => $rdv['email_client']],
];
return build_ics_common_lines($uid, $start, $end, $timezone, $summary, $desc, $location, $attendees);
}
function build_ics_praticien(array $rdv, DateTime $start, DateTime $end, string $timezone, string $location, string $ctaUrl, array $t, string $uid): string {
$summary = ($t['rdv_avec'] ?? 'Rendez-vous avec').' '.$rdv['prenom_client'].' '.$rdv['nom_client'];
$desc = ($t['nouveau_rdv_pris_avec'] ?? 'Nouveau rendez-vous pris avec')."\n\n".
($t['client_label'] ?? 'Client').': '.$rdv['prenom_client'].' '.$rdv['nom_client']."\n".
'Email: '.$rdv['email_client']."\n".
(($rdv['tel_client'] ?? '') ? ('Téléphone: '.$rdv['tel_client']."\n") : '').
($ctaUrl ? ("\n".($t['consultez_espace_pour_details'] ?? 'Consultez votre espace pour les détails')."\n".$ctaUrl) : '');
$attendees = [
['cn' => $rdv['prenom_client'].' '.$rdv['nom_client'], 'email' => $rdv['email_client']],
['cn' => $rdv['prenom_praticien'].' '.$rdv['nom_praticien'], 'email' => $rdv['email_praticien']],
];
return build_ics_common_lines($uid, $start, $end, $timezone, $summary, $desc, $location, $attendees);
}
/* ================== Mode POST (AJAX) ================== */
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && ($_POST['action'] ?? '') === 'validate') {
header('Content-Type: application/json; charset=utf-8');
// CSRF
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) {
echo json_encode(['ok'=>false, 'html'=>'<div class="msg msg--err"><h2>Сессия истекла</h2><p>Обновите страницу и попробуйте ещё раз.</p></div>']); exit;
}
$client_id = $_SESSION['reference_id'] ?? null;
$praticien_id = isset($_POST['praticien_id']) ? (int)$_POST['praticien_id'] : null;
$startIso = $_POST['start'] ?? null;
$mode_rdv_in = $_POST['mode_rdv'] ?? ($_GET['mode_rdv'] ?? null);
$mode_rdv = ($mode_rdv_in === $GLOBALS['c_offline']) ? $GLOBALS['c_offline'] : $GLOBALS['c_online'];
$planningUrl = '/public/client/reserver_rdv.php?praticien_id='.(int)($praticien_id ?? 0);
if (!$praticien_id || !$startIso || !$client_id) {
$html = '
<div class="msg msg--err">
<h2>'.htmlspecialchars($translations['donnees_manquantes_reservation'] ?? 'Données manquantes pour la réservation').'</h2>
<p>'.htmlspecialchars($translations['retour_fiche_praticien_hint'] ?? 'Merci de revenir sur la fiche du praticien et de choisir un créneau.').'</p>
<div class="actions">
<a class="btn btn-outline" href="'.htmlspecialchars($planningUrl).'">'.htmlspecialchars($translations['retour_a_la_fiche'] ?? 'Retour à la fiche').'</a>
</div>
</div>';
echo json_encode(['ok'=>false, 'html'=>$html]); exit;
}
try {
$date_rdv_sql = (new DateTime($startIso))->format('Y-m-d H:i:s');
// Créneau libre ? (libre si seulement "annulé")
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM rendez_vous
WHERE praticien_id = ?
AND date_rdv = ?
AND statut <> 'annulé'
");
$stmt->execute([$praticien_id, $date_rdv_sql]);
if ((int)$stmt->fetchColumn() > 0) {
$html = '
<div class="msg msg--err">
<h2>'.htmlspecialchars($translations['creneau_deja_reserve'] ?? 'Créneau déjà réservé').'</h2>
<p>'.htmlspecialchars($translations['choisir_autre_creneau'] ?? 'Veuillez choisir un autre créneau.').'</p>
<div class="actions">
<a class="btn btn-outline" href="'.htmlspecialchars($planningUrl).'">'.htmlspecialchars($translations['retour_au_planning'] ?? 'Retour au planning').'</a>
</div>
</div>';
echo json_encode(['ok'=>false, 'html'=>$html]); exit;
}
// Récupérer durée + adresse praticien
$stmt = $pdo->prepare("SELECT duree_rdv, adresse, ville, code_postal FROM praticien WHERE id = ?");
$stmt->execute([$praticien_id]);
$p = $stmt->fetch(PDO::FETCH_ASSOC);
$duree = (int)($p['duree_rdv'] ?? 60);
$adresse = trim((string)($p['adresse'] ?? ''));
$ville = trim((string)($p['ville'] ?? ''));
$code_postal = trim((string)($p['code_postal'] ?? ''));
$has_address = ($adresse !== '' && $ville !== '' && $code_postal !== '');
if ($mode_rdv === $c_offline && !$has_address) $mode_rdv = $c_online;
$addrSnapshot = $has_address ? "{$adresse}, {$code_postal} {$ville}" : '';
// Insert RDV
$uuid = bin2hex(random_bytes(16));
$lieu_insert = ($mode_rdv === $c_offline) ? 'offline' : 'online';
$stmt = $pdo->prepare("
INSERT INTO rendez_vous (praticien_id, client_id, date_rdv, duree_minutes, lieu, statut, commentaire, uuid_ical, created_at)
VALUES (?, ?, ?, ?, ?, 'prévu', '', ?, NOW())
");
$stmt->execute([$praticien_id, $client_id, $date_rdv_sql, $duree, $lieu_insert, $uuid]);
// Infos pour e-mails & ICS
$stmt = $pdo->prepare("
SELECT rv.id AS rdv_id, rv.date_rdv, rv.duree_minutes, rv.lieu,
p.nom AS nom_praticien, p.prenom AS prenom_praticien, p.email AS email_praticien, p.telephone AS tel_praticien,
p.adresse AS p_adresse, p.ville AS p_ville, p.code_postal AS p_cp,
c.email AS email_client, c.nom AS nom_client, c.prenom AS prenom_client, c.telephone AS tel_client
FROM rendez_vous rv
JOIN praticien p ON rv.praticien_id = p.id
JOIN client c ON rv.client_id = c.id
WHERE rv.uuid_ical = ?
");
$stmt->execute([$uuid]);
$rdv = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$rdv) { throw new RuntimeException('Rendez-vous introuvable après insertion.'); }
$rdvId = (int)$rdv['rdv_id'];
// =========================
// LOG RDV (rdv_actions_log) : create
// =========================
$after = json_encode([
'statut' => 'prévu',
'date_rdv' => $rdv['date_rdv'], // datetime SQL
'duree_minutes'=> (int)$rdv['duree_minutes'],
'lieu' => $rdv['lieu'], // online/offline (chez toi)
'praticien_id' => (int)$praticien_id,
'client_id' => (int)$client_id,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$logStmt = $pdo->prepare("
INSERT INTO rdv_actions_log
(rdv_id, action, by_user_id, by_role, before_json, after_json, created_at)
VALUES
(?, 'create', ?, 'client', NULL, ?, NOW())
");
$logStmt->execute([$rdvId, (int)$client_id, $after]);
// =========================
// =========================
// PUSH RDV — création (client)
// =========================
try {
// ✅ Récupérer compte_id du praticien (pour compteur badge)
$st = $pdo->prepare("SELECT id FROM compte WHERE role='praticien' AND reference_id=? LIMIT 1");
$st->execute([(int)$praticien_id]);
$praticienCompteId = (int)($st->fetchColumn() ?: 0);
// ✅ Récupérer compte_id du client (utile plus tard pour d'autres events, pas obligatoire ici)
$st = $pdo->prepare("SELECT id FROM compte WHERE role='client' AND reference_id=? LIMIT 1");
$st->execute([(int)$client_id]);
$clientCompteId = (int)($st->fetchColumn() ?: 0);
// ✅ Incrémenter le compteur RDV (badge) pour le praticien
if ($praticienCompteId > 0) {
$pdo->prepare("
INSERT INTO push_badge_counters (compte_id, msg_unread, rdv_unread)
VALUES (:cid, 0, 1)
ON DUPLICATE KEY UPDATE
rdv_unread = LEAST(99, rdv_unread + 1),
updated_at = NOW()
")->execute([':cid' => $praticienCompteId]);
}
// 🔔 Praticien : nouvelle demande
push_rdv_to_praticien(
$pdo,
(int)$praticien_id,
$translations['push_rdv_new_title'] ?? 'Новая запись',
$translations['push_rdv_new_body'] ?? 'У вас новая запись на приём.',
'/public/praticien/rendez_vous.php?id='.(int)$rdvId,
[
'rdv_id' => (int)$rdvId,
'action' => 'create',
'from_role' => 'client',
'client_id' => (int)$client_id,
'praticien_id' => (int)$praticien_id,
'date_rdv' => $rdv['date_rdv'],
]
);
// 🔔 Client : confirmation
push_rdv_to_client(
$pdo,
(int)$client_id,
$translations['push_rdv_created_title'] ?? 'Запись отправлена',
$translations['push_rdv_created_body'] ?? 'Ожидает подтверждения специалиста.',
'/public/client/mes_rendez_vous.php',
[
'rdv_id' => (int)$rdvId,
'action' => 'create'
]
);
} catch (Throwable $e) {
error_log('push rdv create failed: '.$e->getMessage());
}
$tz = 'Europe/Paris';
$startDT = new DateTime($rdv['date_rdv'], new DateTimeZone($tz));
$endDT = (clone $startDT)->modify("+{$duree} minutes");
$when = $startDT->format('d/m/Y H:i');
$when_a = $startDT->format('d/m/Y') . ' ' . ($translations['a'] ?? 'в') . ' ' . $startDT->format('H:i');
$ru_online = $translations['option_online'] ?? 'Онлайн';
$ru_offline = $translations['option_cabinet'] ?? 'В кабинете';
$is_offline = ($mode_rdv === $c_offline);
$mode_label_ru = $is_offline ? $ru_offline : $ru_online;
$location = $is_offline && $has_address ? $addrSnapshot : $ru_online;
$cta_client_url = 'https://uslugi.fr/public/client/mes_rendez_vous.php';
$cta_prat_url = "https://uslugi.fr/public/praticien/rendez_vous.php?id={$rdvId}";
// ======= (1) RÉPONSE IMMÉDIATE AU CLIENT (évite le 503) =======
$html = '
<div class="card-notice">
<h1 class="cn-title">'.($translations['block_1'] ?? 'Спасибо за запись').'</h1>
<div class="cn-date">'.htmlspecialchars($when_a).'</div>
<ul class="notice-list">
<li>
<span class="ico">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 22a2.5 2.5 0 0 0 2.45-2h-4.9A2.5 2.5 0 0 0 12 22Zm7-6V11a7 7 0 1 0-14 0v5l-2 2v1h18v-1l-2-2Z"/></svg>
</span>
<span class="txt"><strong class="txt-warn">'.htmlspecialchars($translations['block_2'] ?? 'Ожидает подтверждения').'</strong></span>
</li>
<li>
<span class="ico ico-dark">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4h16v12H7l-3 3V4z"/></svg>
</span>
<span class="txt">'.($translations['block_3'] ?? 'Оплата производится напрямую со специалистом.').'</span>
</li>
<li>
<span class="ico ico-info">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="currentColor"/>
<rect x="11" y="6" width="2" height="8" fill="#fff"/>
<rect x="11" y="16" width="2" height="2" fill="#fff"/>
</svg>
</span>
<span class="txt">'.($translations['block_4'] ?? 'Ссылка появится за 15 минут до начала.').'</span>
</li>
<li>
<span> </span>
<span class="txt"><a class="btn btn-primary" href="/public/client/mes_rendez_vous.php">'.
htmlspecialchars($translations['ecrire_au_specialiste'] ?? ($translations['voir_mes_rdv'] ?? 'Написать специалисту'))
.'</a></span>
</li>
</ul>
</div>';
// ⚠️ libérer la session, envoyer la réponse, puis continuer en arrière-plan
session_write_close();
echo json_encode(['ok'=>true, 'html'=>$html]);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request(); // le client & Varnish ont reçu la réponse => pas de 503
}
ignore_user_abort(true);
// ======= (2) TÂCHES ARRIÈRE-PLAN : e-mails + WhatsApp =======
try {
// ICS
$ical_client = build_ics_client($rdv, $startDT, $endDT, $tz, $location, $cta_client_url, $translations, $uuid);
$ical_prat = build_ics_praticien($rdv, $startDT, $endDT, $tz, $location, $cta_prat_url, $translations, $uuid);
$whoClientSees = ($translations['titre_dr'] ?? 'Dr').' '.$rdv['prenom_praticien'].' '.$rdv['nom_praticien'];
$whoPratSees = ($translations['client_label'] ?? 'Client').' '.$rdv['prenom_client'].' '.$rdv['nom_client'];
$cta_label = $translations['consultez_espace_pour_details'] ?? 'Voir les détails';
// Client
$subject_c = $translations['email_confirmation_rdv_sujet'] ?? 'Confirmation de rendez-vous';
$data_c = [
'hello_line' => sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s', $rdv['prenom_client'].' '.$rdv['nom_client']),
'intro' => ($translations['votre_rdv_attente'] ?? 'Votre rendez-vous est en attente de confirmation').' '.
($translations['email_avec'] ?? 'avec').' '.$whoClientSees.".\n".
($translations['veuillez_trouver_invitation'] ?? "Veuillez trouver l'invitation en pièce jointe."),
'who' => $whoClientSees,
'when' => $when_a,
'format_line' => $mode_label_ru,
'address_line' => ($is_offline && $has_address) ? $addrSnapshot : '',
'cta_url' => $cta_client_url,
'cta_label' => $cta_label,
'footer' => $translations['merci_simple'] ?? 'Merci.',
];
$ok_c = send_rdv_email_with_ics_phpmailer(
$rdv['email_client'], $rdv['prenom_client'].' '.$rdv['nom_client'],
$subject_c,
build_email_text($data_c, $translations),
build_email_html($data_c, $translations),
$ical_client, "rdv_client_{$uuid}.ics"
);
// Praticien
$subject_p = ($translations['nouveau_rdv_avec'] ?? 'Nouveau rendez-vous avec').' '.$rdv['prenom_client'].' '.$rdv['nom_client'];
$data_p = [
'hello_line' => ($translations['email_bonjour_dr'] ?? 'Bonjour Dr').' '.$rdv['prenom_praticien'].' '.$rdv['nom_praticien'],
'intro' => ($translations['nouveau_rdv_pris_avec'] ?? 'Un nouveau rendez-vous a été pris avec vous.'),
'who' => $whoPratSees,
'when' => $when_a,
'format_line' => $mode_label_ru,
'address_line' => ($is_offline && $has_address) ? $addrSnapshot : '',
'cta_url' => $cta_prat_url,
'cta_label' => $cta_label,
'footer' => $translations['merci_simple'] ?? 'Merci.',
];
$ok_p = send_rdv_email_with_ics_phpmailer(
$rdv['email_praticien'], $rdv['prenom_praticien'].' '.$rdv['nom_praticien'],
$subject_p,
build_email_text($data_p, $translations),
build_email_html($data_p, $translations),
$ical_prat, "rdv_praticien_{$uuid}.ics"
);
if (!$ok_c || !$ok_p) {
error_log('RDV: un ou plusieurs e-mails ICS n’ont pas été envoyés.');
}
} catch (Throwable $e) {
error_log('RDV: background email error: '.$e->getMessage());
}
// WhatsApp (ne doit JAMAIS bloquer)
try {
$waTo = $rdv['tel_praticien'] ?? '';
$clientFullName = trim(($rdv['prenom_client'] ?? '') . ' ' . ($rdv['nom_client'] ?? ''));
$dateHeure = $when;
if ($waTo && $clientFullName && $dateHeure && $rdvId) {
// Si votre lib permet un timeout, utilisez-le. Sinon, laissez tel quel.
$waRes = Sms\notify_rdv_en_attente($waTo, $clientFullName, $dateHeure, $rdvId /*, ['timeout'=>5] */);
if ((int)($waRes['http_code'] ?? 0) !== 200) {
error_log('WA en_attente non-200: ' . json_encode($waRes, JSON_UNESCAPED_UNICODE));
}
} else {
error_log('WA en_attente: données incomplètes (tel praticien / nom client / dateHeure / rdvId).');
}
} catch (Throwable $e) {
error_log('WA en_attente_confirmation exception: ' . $e->getMessage());
}
// Fin du background
exit;
} catch (Throwable $e) {
$html = '
<div class="msg msg--err">
<h2>'.htmlspecialchars($translations['erreur_prefixe'] ?? 'Erreur').'</h2>
<p>'.htmlspecialchars($e->getMessage()).'</p>
<div class="actions">
<a class="btn btn-outline" href="'.htmlspecialchars('/public/client/reserver_rdv.php?praticien_id='.(int)($praticien_id ?? 0)).'">'.htmlspecialchars($translations['retour_a_la_fiche'] ?? 'Retour à la fiche').'</a>
</div>
</div>';
echo json_encode(['ok'=>false, 'html'=>$html]); exit;
}
}
/* ================== Mode GET : UI + loader + JS ================== */
$praticien_id = isset($_GET['praticien_id']) ? (int)$_GET['praticien_id'] : null;
$startIso = $_GET['start'] ?? null;
$modeGet = $_GET['mode_rdv'] ?? null;
ui_head($translations['validation_rdv_title'] ?? 'Validation du rendez-vous');
?>
<script>
(function(){
const loader = document.getElementById('loader');
const result = document.getElementById('result');
const praticien_id = <?= json_encode($praticien_id) ?>;
const start = <?= json_encode($startIso) ?>;
const csrf = <?= json_encode($CSRF) ?>;
const mode_rdv = <?= json_encode($modeGet) ?>;
if(!praticien_id || !start){
loader.classList.add('hidden');
const backUrl = praticien_id ? `/public/client/reserver_rdv.php?praticien_id=${praticien_id}` : `/public/client/reserver_rdv.php`;
result.innerHTML = `
<div class="msg msg--err">
<h2><?= htmlspecialchars($translations['donnees_manquantes_reservation'] ?? 'Données manquantes pour la réservation') ?></h2>
<p><?= htmlspecialchars($translations['retour_fiche_praticien_hint'] ?? 'Merci de revenir sur la fiche du praticien et de choisir un créneau.') ?></p>
<div class="actions">
<a class="btn btn-outline" href="${backUrl}"><?= htmlspecialchars($translations['retour_a_la_fiche'] ?? 'Retour à la fiche') ?></a>
</div>
</div>`;
return;
}
const body = new URLSearchParams({ action: 'validate', csrf, praticien_id, start });
if (mode_rdv) body.set('mode_rdv', mode_rdv);
fetch(location.pathname, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body
})
.then(r => r.json())
.then(data => {
result.innerHTML = data.html || '<div class="msg msg--err"><h2>Неизвестная ошибка</h2></div>';
})
.catch(() => {
result.innerHTML = '<div class="msg msg--err"><h2>Сетевая ошибка</h2><p>Попробуйте повторно.</p></div>';
})
.finally(() => {
loader.classList.add('hidden');
});
})();
</script>
<?php ui_foot(); ?>