$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 = '
';
$brand_bas ='УСЛУГИ.ФР';
return '
USLUGI.FR
| '.$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']).' |
| '.
(!empty($data['cta_url']) ? '| '.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.');
}