Files

451 lines
18 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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.');
}