Init Git Prod Mars 2026
This commit is contained in:
commit
d81efe93b1
362 files changed
+86627
No files matched your search
@@ -0,0 +1,421 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
require_once __DIR__ . '/../../push/push_endpoint.php';
|
||||
|
||||
|
||||
// PHPMailer (config centralisée)
|
||||
|
||||
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
// --------- Réponse JSON utilitaire ----------
|
||||
function json_out(bool $ok, string $msg, array $extra = []): never {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['ok'=>$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 = '<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:#111;font-size:16px;line-height:1.5;">'.nl2br(htmlspecialchars($data['hello_line'])).'</td></tr>
|
||||
<tr><td style="padding:8px 24px;color:#111;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($data['who_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($data['who_value']).'</td></tr>
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($data['date_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($data['when']).'</td></tr>
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($data['statut_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($data['statut_value']).'</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:#fff;font-weight:600;">'.htmlspecialchars($data['cta_label']).'</a></td></tr>' : '').
|
||||
'<tr><td style="padding:8px 24px 24px 24px;color:#111;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 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.');
|
||||
}
|
||||
Reference in new issue
Block a user