$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.'); }