451 lines
18 KiB
PHP
451 lines
18 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../includes/init_client.php';
|
||
require_once __DIR__ . '/../../push/push_endpoint.php';
|
||
require_post_csrf();
|
||
|
||
// PHPMailer (config centralisée)
|
||
|
||
use PHPMailer\PHPMailer\PHPMailer;
|
||
|
||
// --------- 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.');
|
||
}
|