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

564 lines
26 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__ . '/../../sms/notify_rdv.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
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
// garde-fous utiles
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 {
// SMTP
$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'] ?? '';
// Base
$mail->CharSet = 'UTF-8';
$mail->Encoding = 'base64';
$mail->XMailer = '';
// Envelope (Return-Path)
if (!empty($MAILCFG['bounce'])) $mail->Sender = $MAILCFG['bounce'];
// From / Reply-To
$from = $MAILCFG['from'] ?? ['no-reply@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] ?? '');
// Destinataire
$mail->addAddress($toEmail, $toName);
// Anti auto-replies (optionnel)
if (!empty($MAILCFG['suppress_auto'])) {
$mail->addCustomHeader('Auto-Submitted', 'auto-generated');
$mail->addCustomHeader('X-Auto-Response-Suppress', 'All');
}
// Corps
$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 (property_exists($mail, 'ErrorInfo') && $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 communs (HTML UI) ================== */
function ui_head(string $title): void {
$HEAD_EXTRA = <<<HTML
<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(640px, 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; }
.btn{ display:inline-block; text-decoration:none; font-weight:700; border-radius:999px; padding:10px 16px; }
.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>
HTML;
?>
<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 =================== */
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 (!isset($a['email']) || !$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) : traitement & réponse JSON ================== */
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);
// Normalisation du mode (défaut = online)
$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');
// Vérifier si le créneau est encore libre
$stmt = $pdo->prepare("SELECT COUNT(*) FROM rendez_vous WHERE praticien_id = ? AND date_rdv = ?");
$stmt->execute([$praticien_id, $date_rdv_sql]);
if ($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 complète ?
$adresse = trim((string)($p['adresse'] ?? ''));
$ville = trim((string)($p['ville'] ?? ''));
$code_postal = trim((string)($p['code_postal'] ?? ''));
$has_address = ($adresse !== '' && $ville !== '' && $code_postal !== '');
// Si offline choisi mais adresse manquante → bascule online
if ($mode_rdv === $c_offline && !$has_address) {
$mode_rdv = $c_online;
}
// Snapshot adresse (pour email/ICS)
$addrSnapshot = $has_address ? "{$adresse}, {$code_postal} {$ville}" : '';
// Insert RDV avec lieu = online/offline
$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]);
// Récupérer 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'];
$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');
// RU libellés
$ru_online = $translations['option_online'] ?? 'Онлайн';
$ru_offline = $translations['option_cabinet'] ?? 'В кабинете';
$ru_format = $translations['format_label'] ?? 'Формат';
$ru_addrLbl = $translations['adresse_label_ru'] ?? 'Адрес';
$is_offline = ($mode_rdv === $c_offline);
$mode_label_ru = $is_offline ? $ru_offline : $ru_online;
// ICS LOCATION
$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}";
// 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);
// E-mails (ajout format ou adresse)
$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,
'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,
'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.');
}
// WhatsApp (inchangé)
try {
$waTo = $rdv['tel_praticien'] ?? '';
$clientFullName = trim(($rdv['prenom_client'] ?? '') . ' ' . ($rdv['nom_client'] ?? ''));
$dateHeure = $when;
if ($waTo && $clientFullName && $dateHeure && $rdvId) {
$waRes = Sms\notify_rdv_en_attente($waTo, $clientFullName, $dateHeure, $rdvId);
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());
}
// Réponse succès (meta inclut le format)
$html = '
<div class="msg msg--ok">
<h2>'.($translations['rdv_confirme_pour_le'] ?? 'Rendez-vous enregistré pour le').' '.$when.' '.($translations['spasibo_za_zapis'] ?? '— спасибо за запись!').'</h2>
<div class="meta">'.htmlspecialchars($translations['votre_rdv_attente'] ?? 'Votre rendez-vous est en attente de confirmation').' — '.htmlspecialchars($ru_format).': '.htmlspecialchars($mode_label_ru).'</div>
<div class="actions">
<a class="btn btn-primary" href="'.htmlspecialchars('/public/client/reserver_rdv.php?praticien_id='.$praticien_id).'">'.htmlspecialchars($translations['retour_au_planning'] ?? 'Retour au planning').'</a>
<a class="btn btn-outline" href="/public/client/mes_rendez_vous.php">'.htmlspecialchars($translations['voir_mes_rdv'] ?? 'Voir mes rendez-vous').'</a>
</div>
</div>';
echo json_encode(['ok'=>true, 'html'=>$html]); 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 (déclenche l'AJAX) ================== */
$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(); ?>