422 lines
17 KiB
PHP
422 lines
17 KiB
PHP
<?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.');
|
||
}
|