$ok, 'message' => $msg] + $extra); exit; } // --------- Envoi e-mail via PHPMailer (avec ICS optionnel + fallback) ---------- function send_email_phpmailer( string $toEmail, string $subject, string $textBody, string $htmlBody, ?string $icsFilename = null, ?string $icsContent = null ): array { global $MAILCFG; $mail = new PHPMailer(true); try { // SMTP $mail->isSMTP(); $mail->Host = $MAILCFG['smtp']['host']; $mail->Port = $MAILCFG['smtp']['port']; $mail->SMTPSecure = $MAILCFG['smtp']['secure']; $mail->SMTPAuth = $MAILCFG['smtp']['auth']; $mail->Username = $MAILCFG['smtp']['username']; $mail->Password = $MAILCFG['smtp']['password']; $mail->CharSet = 'UTF-8'; $mail->Encoding = 'base64'; $mail->XMailer = ''; // Debug ponctuel : ?debug_mail=1 if (! empty($_GET['debug_mail'])) { $mail->SMTPDebug = 2; $mail->Debugoutput = function ($str, $lvl) {error_log("SMTP[$lvl] " . $str);}; } if (! empty($MAILCFG['bounce'])) { $mail->Sender = $MAILCFG['bounce']; } $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] ?? ''); } $mail->addAddress($toEmail); 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; if ($icsFilename && $icsContent !== null && empty($_GET['no_ics'])) { $mail->addStringAttachment( $icsContent, $icsFilename, 'base64', 'text/calendar; method=CANCEL; charset=UTF-8' ); } $mail->send(); return [true, null]; } catch (Throwable $e) { $err = 'PHPMailer error: ' . $e->getMessage(); if (! empty($mail->ErrorInfo)) { $err .= ' | ErrorInfo: ' . $mail->ErrorInfo; } error_log($err); // Fallback : réessaye SANS ICS try { if ($icsFilename && $icsContent !== null && empty($_GET['no_ics'])) { $mail->clearAttachments(); $mail->send(); return [true, 'sent_without_ics']; } } catch (Throwable $e2) { $err2 = 'PHPMailer fallback error: ' . $e2->getMessage(); if (! empty($mail->ErrorInfo)) { $err2 .= ' | ErrorInfo: ' . $mail->ErrorInfo; } error_log($err2); } return [false, $err]; } } // --------- Helpers e-mail (HTML/Texte) ---------- function email_html_cancel(array $data, array $t): string { $brand = 'USLUGI.FR'; $brand_bas = 'УСЛУГИ.ФР'; return 'USLUGI.FR
' . (! empty($data['cta_url']) ? '' : '') . '
' . $brand . '
' . nl2br(htmlspecialchars($data['hello_line'])) . '
' . nl2br(htmlspecialchars($data['intro'])) . '
' . htmlspecialchars($data['who_label']) . '' . htmlspecialchars($data['who_value']) . '
' . htmlspecialchars($data['date_label']) . '' . htmlspecialchars($data['when']) . '
' . htmlspecialchars($data['statut_label']) . '' . htmlspecialchars($data['statut_value']) . '
' . htmlspecialchars($data['cta_label']) . '
' . nl2br(htmlspecialchars($data['footer'])) . '
© ' . date('Y') . ' ' . $brand_bas . ' — ' . htmlspecialchars($t['merci_simple'] ?? 'Merci') . '
'; } function email_text_cancel(array $data, array $t): string { $lines = []; $lines[] = $data['hello_line']; $lines[] = ''; $lines[] = $data['intro']; $lines[] = ''; $lines[] = $data['who_label'] . ': ' . $data['who_value']; $lines[] = $data['date_label'] . ': ' . $data['when']; $lines[] = $data['statut_label'] . ': ' . $data['statut_value']; if (! empty($data['cta_url'])) {$lines[] = ''; $lines[] = $data['cta_label'] . ': ' . $data['cta_url'];} $lines[] = ''; $lines[] = $data['footer']; return implode("\n", $lines); } // --------- ICS CANCEL ---------- function ics_escape(string $s): string { $s = str_replace("\\", "\\\\", $s); $s = str_replace(";", "\;", $s); $s = str_replace(",", "\,", $s); return preg_replace("/\r\n|\r|\n/", "\\n", $s); } function build_ics_cancel( string $uid, DateTime $start, ?DateTime $end, string $timezone, string $summary, string $organizerEmail, array $attendees // [['cn'=>..., 'email'=>...]] ): string { $lines = []; $lines[] = "BEGIN:VCALENDAR"; $lines[] = "VERSION:2.0"; $lines[] = "PRODID:-//УСЛУГИ.ФР//RDV//FR"; $lines[] = "METHOD:CANCEL"; $lines[] = "BEGIN:VEVENT"; $lines[] = "UID:" . $uid; $lines[] = "DTSTAMP:" . gmdate('Ymd\THis\Z'); $lines[] = "STATUS:CANCELLED"; $lines[] = "SEQUENCE:2"; $lines[] = "SUMMARY:" . ics_escape($summary); if ($start) { $lines[] = "DTSTART;TZID={$timezone}:" . $start->format('Ymd\THis'); } if ($end) { $lines[] = "DTEND;TZID={$timezone}:" . $end->format('Ymd\THis'); } $lines[] = "ORGANIZER:mailto:" . $organizerEmail; foreach ($attendees as $a) { if (empty($a['email'])) { continue; } $cn = ics_escape($a['cn'] ?? ''); $lines[] = "ATTENDEE;ROLE=REQ-PARTICIPANT;CN={$cn}:mailto:" . $a['email']; } $lines[] = "END:VEVENT"; $lines[] = "END:VCALENDAR"; return implode("\r\n", $lines) . "\r\n"; } // =================== LOGIQUE =================== if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { json_out(false, 'Méthode invalide.'); } $client_id = $_SESSION['reference_id'] ?? null; $id = (int) ($_POST['id'] ?? 0); if (! $client_id || $id <= 0) { json_out(false, $translations['erreur_generique'] ?? 'Erreur.'); } try { // ========================= // SNAPSHOT AVANT ANNULATION // ========================= $beforeStmt = $pdo->prepare(" SELECT statut, date_rdv, duree_minutes, lieu, praticien_id, client_id FROM rendez_vous WHERE id = ? AND client_id = ? LIMIT 1 "); $beforeStmt->execute([$id, $client_id]); $beforeRow = $beforeStmt->fetch(PDO::FETCH_ASSOC); if (! $beforeRow) { json_out(false, $translations['erreur_generique'] ?? 'Rendez-vous introuvable.'); exit; } // ✅ compte_id praticien + client (pour badge counters) $praticienCompteId = 0; $clientCompteId = 0; if (! empty($beforeRow['praticien_id'])) { $st = $pdo->prepare("SELECT id FROM compte WHERE role='praticien' AND reference_id=? LIMIT 1"); $st->execute([(int) $beforeRow['praticien_id']]); $praticienCompteId = (int) ($st->fetchColumn() ?: 0); } if (! empty($beforeRow['client_id'])) { $st = $pdo->prepare("SELECT id FROM compte WHERE role='client' AND reference_id=? LIMIT 1"); $st->execute([(int) $beforeRow['client_id']]); $clientCompteId = (int) ($st->fetchColumn() ?: 0); } // 1) Annuler (contrainte client_id) $stmt = $pdo->prepare("UPDATE rendez_vous SET statut = 'annulé' WHERE id = ? AND client_id = ?"); $stmt->execute([$id, $client_id]); if ($stmt->rowCount() === 0) { // Rien modifié -> soit déjà annulé, soit pas à ce client, soit ID invalide json_out(false, $translations['aucune_modification'] ?? 'Aucune modification effectuée.'); } // ========================= // LOG RDV (rdv_actions_log) : cancel // ========================= $beforeJson = json_encode([ 'statut' => $beforeRow['statut'], 'date_rdv' => $beforeRow['date_rdv'], 'duree_minutes' => (int) $beforeRow['duree_minutes'], 'lieu' => $beforeRow['lieu'], ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $afterJson = json_encode([ 'statut' => 'annulé', 'date_rdv' => $beforeRow['date_rdv'], ], 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 (?, 'cancel', ?, 'client', ?, ?, NOW()) "); $logStmt->execute([(int) $id, (int) $client_id, $beforeJson, $afterJson]); // ========================= // PUSH RDV — annulation (client) // ========================= try { // ✅ Badge RDV : +1 pour le praticien (cancel par client) 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 : RDV annulé par le client push_rdv_to_praticien( $pdo, (int) $beforeRow['praticien_id'], $translations['push_rdv_cancel_praticien_title'] ?? 'Запись отменена', $translations['push_rdv_cancel_praticien_body'] ?? 'Клиент отменил запись.', '/public/praticien/rendez_vous.php?id=' . (int) $id, [ 'rdv_id' => (int) $id, 'action' => 'cancel', 'from_role' => 'client', 'client_id' => (int) $beforeRow['client_id'], 'praticien_id' => (int) $beforeRow['praticien_id'], 'date_rdv' => $beforeRow['date_rdv'], ] ); // 🔔 Client : confirmation annulation push_rdv_to_client( $pdo, (int) $beforeRow['client_id'], $translations['push_rdv_cancel_client_title'] ?? 'Запись отменена', $translations['push_rdv_cancel_client_body'] ?? 'Ваша запись была успешно отменена.', '/public/client/mes_rendez_vous.php', [ 'rdv_id' => (int) $id, 'action' => 'cancel', ] ); } catch (Throwable $e) { error_log('push rdv cancel (client) failed: ' . $e->getMessage()); } // 2) Infos RDV pour e-mails $info = $pdo->prepare(" SELECT rv.date_rdv, rv.duree_minutes, rv.uuid_ical, p.email AS praticien_email, p.nom AS praticien_nom, p.prenom AS praticien_prenom, c.prenom AS client_prenom, c.nom AS client_nom, c.email AS client_email FROM rendez_vous rv JOIN praticien p ON rv.praticien_id = p.id JOIN client c ON rv.client_id = c.id WHERE rv.id = ? AND rv.client_id = ? "); $info->execute([$id, $client_id]); $r = $info->fetch(PDO::FETCH_ASSOC); if (! $r) { json_out(false, $translations['erreur_generique'] ?? 'Erreur.'); } // 3) Prépare mails $tz = new DateTimeZone('Europe/Paris'); $start = new DateTime($r['date_rdv'], $tz); $end = (clone $start)->modify('+' . (int) ($r['duree_minutes'] ?? 60) . ' minutes'); $when = $start->format('d/m/Y H:i'); $pratFull = $r['praticien_prenom'] . ' ' . $r['praticien_nom']; $cliFull = $r['client_prenom'] . ' ' . $r['client_nom']; $cta_url_prat = 'https://uslugi.fr/public/praticien/agenda.php'; $cta_url_cli = 'https://uslugi.fr/public/client/mes_rendez_vous.php'; $cta_label = $translations['consultez_espace_pour_details'] ?? 'Voir les détails'; $statut_label = $translations['statut_label'] ?? 'Statut'; $statut_value = $translations['statut_annule'] ?? 'Annulé'; // ---- Mail praticien $subject_p = $translations['email_annulation_sujet'] ?? 'Rendez-vous annulé'; $hello_p = ($translations['email_bonjour_dr'] ?? 'Bonjour Dr') . ' ' . $pratFull . ','; $intro_p = ($translations['email_rdv_du'] ?? 'Le rendez-vous du') . ' ' . $when . ' ' . ($translations['email_avec'] ?? 'avec') . ' ' . $cliFull . ' ' . ($translations['email_annule_par_client'] ?? 'a été annulé par le client.'); $data_p = [ 'hello_line' => $hello_p, 'intro' => $intro_p, 'who_label' => $translations['client_label'] ?? 'Client', 'who_value' => $cliFull, 'date_label' => $translations['date_label'] ?? 'Date', 'when' => $when, 'statut_label' => $statut_label, 'statut_value' => $statut_value, 'cta_url' => $cta_url_prat, 'cta_label' => $cta_label, 'footer' => $translations['merci_simple'] ?? 'Merci.', ]; $text_p = email_text_cancel($data_p, $translations); $html_p = email_html_cancel($data_p, $translations); $ics_p = null; $ics_name_p = null; if (! empty($r['uuid_ical'])) { $ics_name_p = 'rdv_cancel_' . $r['uuid_ical'] . '_praticien.ics'; $ics_p = build_ics_cancel( $r['uuid_ical'], $start, $end, 'Europe/Paris', ($translations['rdv_avec'] ?? 'Rendez-vous avec') . ' ' . $cliFull, 'rdv@uslugi.fr', [ ['cn' => $cliFull, 'email' => $r['client_email']], ['cn' => $pratFull, 'email' => $r['praticien_email']], ] ); } [$ok_p, $err_p] = send_email_phpmailer($r['praticien_email'], $subject_p, $text_p, $html_p, $ics_name_p, $ics_p); if (! $ok_p) { error_log("Annulation RDV: mail praticien NON envoyé: " . $err_p); } // ---- Mail client $subject_c = $translations['email_annulation_client_sujet'] ?? 'Confirmation d’annulation de votre rendez-vous'; $hello_c = sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s', $cliFull); $intro_c = ($translations['email_votre_rdv_du'] ?? 'Votre rendez-vous du') . ' ' . $when . ' ' . ($translations['email_avec'] ?? 'avec') . ' ' . (($translations['titre_dr'] ?? 'Dr') . ' ' . $pratFull) . ' ' . ($translations['email_a_ete_annule'] ?? 'a été annulé.'); $who_label_cli = $translations['praticien_label'] ?? ($translations['titre_dr'] ?? 'Dr'); $who_value_cli = ($translations['titre_dr'] ?? 'Dr') . ' ' . $pratFull; $data_c = [ 'hello_line' => $hello_c, 'intro' => $intro_c, 'who_label' => $who_label_cli, 'who_value' => $who_value_cli, 'date_label' => $translations['date_label'] ?? 'Date', 'when' => $when, 'statut_label' => $statut_label, 'statut_value' => $statut_value, 'cta_url' => $cta_url_cli, 'cta_label' => $cta_label, 'footer' => $translations['merci_simple'] ?? 'Merci.', ]; $text_c = email_text_cancel($data_c, $translations); $html_c = email_html_cancel($data_c, $translations); $ics_c = null; $ics_name_c = null; if (! empty($r['uuid_ical'])) { $ics_name_c = 'rdv_cancel_' . $r['uuid_ical'] . '_client.ics'; $ics_c = build_ics_cancel( $r['uuid_ical'], $start, $end, 'Europe/Paris', ($translations['rdv_avec'] ?? 'Rendez-vous avec') . ' ' . (($translations['titre_dr'] ?? 'Dr') . ' ' . $pratFull), 'rdv@uslugi.fr', [ ['cn' => $cliFull, 'email' => $r['client_email']], ['cn' => $pratFull, 'email' => $r['praticien_email']], ] ); } [$ok_c, $err_c] = send_email_phpmailer($r['client_email'], $subject_c, $text_c, $html_c, $ics_name_c, $ics_c); if (! $ok_c) { error_log("Annulation RDV: mail client NON envoyé: " . $err_c); } json_out(true, $translations['rdv_annule_ok'] ?? 'Rendez-vous annulé.'); } catch (Throwable $e) { error_log('Annulation RDV error: ' . $e->getMessage()); json_out(false, $translations['erreur_generique'] ?? 'Erreur serveur.'); }