Files
2026-03-05 10:25:39 +01:00

880 lines
41 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
// /public/cabinet/valider_rdv.php
require_once __DIR__ . '/../includes/init_secretaire.php';
$ROOT = dirname(__DIR__, 2);
require_once APP_HTDOCS . '/push/push_endpoint.php';
// $MAILCFG
// (Facultatif) SMS/WhatsApp si tu l'utilises côté client
$notifyPath = APP_HTDOCS . '/sms/notify_rdv.php';
if (is_file($notifyPath)) require_once $notifyPath;
use PHPMailer\PHPMailer\PHPMailer;
/* ================== Const / helpers ================== */
$c_online = $c_online ?? 'online';
$c_offline = $c_offline ?? 'offline';
if (empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(16));
$CSRF = $_SESSION['csrf_token'];
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function email_norm(string $e): string { return strtolower(trim($e)); }
/* ------- Mail helpers ------- */
function send_mail_ics(
string $to, string $toName, string $subject, string $text, string $html,
?string $icsName=null, ?string $icsContent=null, ?string $icsMethod='REQUEST'
): bool {
global $MAILCFG;
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) return false;
$m = new PHPMailer(true);
try {
$m->isSMTP();
$m->Host = $MAILCFG['smtp']['host']; $m->Port = (int)$MAILCFG['smtp']['port'];
$m->SMTPSecure = $MAILCFG['smtp']['secure'] ?? PHPMailer::ENCRYPTION_STARTTLS;
$m->SMTPAuth = (bool)($MAILCFG['smtp']['auth'] ?? true);
$m->Username = $MAILCFG['smtp']['username'] ?? '';
$m->Password = $MAILCFG['smtp']['password'] ?? '';
$m->CharSet = 'UTF-8'; $m->Encoding = 'base64'; $m->XMailer = '';
if (!empty($MAILCFG['bounce'])) $m->Sender = $MAILCFG['bounce'];
$from = $MAILCFG['from'] ?? ['no-reply@uslugi.fr','USLUGI.FR'];
$rpto = $MAILCFG['reply_to'] ?? ['contact@uslugi.fr','USLUGI.FR'];
$m->setFrom($from[0], $from[1] ?? '');
if (!empty($rpto[0])) $m->addReplyTo($rpto[0], $rpto[1] ?? '');
$m->addAddress($to, $toName);
if (!empty($MAILCFG['suppress_auto'])) {
$m->addCustomHeader('Auto-Submitted','auto-generated');
$m->addCustomHeader('X-Auto-Response-Suppress','All');
}
$m->isHTML(true); $m->Subject = $subject; $m->Body = $html; $m->AltBody = $text;
if ($icsName && $icsContent !== null && $icsMethod) {
$m->addStringAttachment($icsContent, $icsName, 'base64', 'text/calendar; method='.$icsMethod.'; charset=UTF-8');
}
$m->send(); return true;
} catch(Throwable $e){ error_log('send_mail_ics: '.$e->getMessage()); return false; }
}
/* ====== Email template (même rendu pour client/praticien) ====== */
function email_tpl_html(array $d, 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:#fff;border-radius:8px;overflow:hidden;">
<tr><td style="background:#38378B;color:#fff;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($d['hello'])).'</td></tr>
<tr><td style="padding:8px 24px;color:#111;font-size:16px;line-height:1.6;">'.nl2br(htmlspecialchars($d['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($d['who_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($d['who_value']).'</td></tr>
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($t['date_label'] ?? 'Date').'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($d['when']).'</td></tr>'.
(!empty($d['format_line']) ? '<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($t['format_label'] ?? 'Формат').'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($d['format_line']).'</td></tr>' : '').
(!empty($d['address_line']) ? '<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($t['adresse_label'] ?? 'Adresse').'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($d['address_line']).'</td></tr>' : '').
'</table>
</td></tr>'.
(!empty($d['cta_url']) ? '<tr><td align="center" style="padding:8px 24px 24px 24px;"><a href="'.htmlspecialchars($d['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($d['cta_label']).'</a></td></tr>' : '').
'<tr><td style="padding:8px 24px 24px 24px;color:#111;font-size:16px;line-height:1.6;">'.nl2br(htmlspecialchars($d['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_tpl_text(array $d, array $t): string {
$L=[]; $L[]=$d['hello']; $L[]=''; $L[]=$d['intro']; $L[]='';
$L[]=($d['who_label']).': '.$d['who_value'];
$L[]=($t['date_label'] ?? 'Date').': '.$d['when'];
if (!empty($d['format_line'])) $L[]=($t['format_label'] ?? 'Формат').': '.$d['format_line'];
if (!empty($d['address_line'])) $L[]=($t['adresse_label'] ?? 'Adresse').': '.$d['address_line'];
if (!empty($d['cta_url'])) { $L[]=''; $L[]=($d['cta_label'] ?? 'Voir les détails').': '.$d['cta_url']; }
$L[]=''; $L[]=$d['footer']; return implode("\n",$L);
}
/* ------- ICS helpers ------- */
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 ics_build(
string $uid, DateTime $start, DateTime $end, string $tz, string $summary,
string $desc, string $loc, array $attendees, string $method='REQUEST', string $status='CONFIRMED'
): string {
$L=[];
$L[]="BEGIN:VCALENDAR";
$L[]="VERSION:2.0";
$L[]="PRODID:-//УСЛУГИ.ФР//RDV//FR";
$L[]="CALSCALE:GREGORIAN";
$L[]="METHOD:{$method}";
$L[]="BEGIN:VEVENT";
$L[]="UID:{$uid}";
$L[]="DTSTAMP:".gmdate('Ymd\THis\Z');
$L[]="DTSTART;TZID={$tz}:".$start->format('Ymd\THis');
$L[]="DTEND;TZID={$tz}:".$end->format('Ymd\THis');
if ($summary) $L[]="SUMMARY:".ics_escape($summary);
if ($desc) $L[]="DESCRIPTION:".ics_escape($desc);
if ($loc) $L[]="LOCATION:".ics_escape($loc);
$L[]="ORGANIZER;CN=УСЛУГИ.ФР:mailto:rdv@uslugi.fr";
foreach ($attendees as $a) {
if (empty($a['email'])) continue;
$cn = ics_escape($a['cn'] ?? '');
$L[]="ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN={$cn};RSVP=TRUE:mailto:".$a['email'];
}
$L[]="STATUS:{$status}";
$L[]="END:VEVENT";
$L[]="END:VCALENDAR";
return implode("\r\n",$L)."\r\n";
}
/* ================== Entrées ================== */
$secretaire_user_id = (int)($_SESSION['compte_id'] ?? 0);
$praticien_id = isset($_GET['praticien_id']) ? (int)$_GET['praticien_id'] : (int)($_POST['praticien_id'] ?? 0);
$startIso = $_GET['start'] ?? ($_POST['start'] ?? null);
$mode_rdv_in = $_GET['mode_rdv'] ?? ($_POST['mode_rdv'] ?? null);
$client_id = isset($_GET['client_id']) ? (int)$_GET['client_id'] : (isset($_POST['client_id']) ? (int)$_POST['client_id'] : 0);
/* ================== Vérif pivot secrétaire ↔ praticien ================== */
$chk = $pdo->prepare("
SELECT can_manage_rdv
FROM secretaire_praticien
WHERE secretaire_user_id = :u
AND praticien_id = :p
AND is_active = 1
LIMIT 1
");
$chk->execute([':u'=>$secretaire_user_id, ':p'=>$praticien_id]);
$pivot = $chk->fetch(PDO::FETCH_ASSOC);
if (!$pivot) { http_response_code(403); exit('<div style="padding:24px">Forbidden</div>'); }
if (((int)($pivot['can_manage_rdv'] ?? 0)) !== 1) {
http_response_code(403);
exit('<div style="padding:24px">'.h($translations['erreur_droits_reservation'] ?? 'Droits insuffisants pour réserver').'</div>');
}
/* ================== Récup praticien ================== */
$stmt = $pdo->prepare("SELECT nom, prenom, duree_rdv, adresse, ville, code_postal, email FROM praticien WHERE id=?");
$stmt->execute([$praticien_id]);
$praticien = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$praticien) { http_response_code(404); exit('Praticien introuvable'); }
$duree = (int)($praticien['duree_rdv'] ?? 60);
$has_address =
!empty(trim((string)($praticien['adresse'] ?? ''))) &&
!empty(trim((string)($praticien['ville'] ?? ''))) &&
!empty(trim((string)($praticien['code_postal'] ?? '')));
/* ================== Mode POST: AJAX (recherche / création / validation) ================== */
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
/* ----- Recherche dynamique de client plateforme non lié ----- */
// ----- Recherche dynamique de client plateforme non lié -----
if (($_POST['action'] ?? '') === 'search_client') {
header('Content-Type: application/json; charset=utf-8');
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) {
echo json_encode(['ok'=>false, 'items'=>[], 'msg'=>'CSRF']); exit;
}
$q = trim((string)($_POST['q'] ?? ''));
if ($q === '' || mb_strlen($q) < 2) { // évite spam / vide
echo json_encode(['ok'=>true, 'items'=>[]]); exit;
}
try {
$like = '%'.$q.'%';
// NOTE: si votre table client n'a PAS adresse/ville/code_postal,
// remplacez la ligne SELECT par: SELECT c.id, c.prenom, c.nom, c.email, c.telephone
$sql = "
SELECT c.id, c.prenom, c.nom, c.email, c.telephone, c.adresse, c.ville, c.code_postal
FROM client c
WHERE (
c.email LIKE :like_email
OR CONCAT(c.prenom,' ',c.nom) LIKE :like_np1
OR CONCAT(c.nom,' ',c.prenom) LIKE :like_np2
OR c.telephone LIKE :like_tel
)
AND NOT EXISTS (
SELECT 1 FROM client_dossiers d
WHERE d.client_id = c.id
AND d.praticien_id = :pid
)
ORDER BY c.prenom, c.nom
LIMIT 12
";
$s = $pdo->prepare($sql);
$s->execute([
':like_email' => $like,
':like_np1' => $like,
':like_np2' => $like,
':like_tel' => $like,
':pid' => $praticien_id,
]);
$items = $s->fetchAll(PDO::FETCH_ASSOC) ?: [];
echo json_encode(['ok'=>true, 'items'=>$items]); exit;
} catch (Throwable $e) {
error_log('search_client: '.$e->getMessage());
echo json_encode(['ok'=>false, 'items'=>[], 'msg'=>'Erreur serveur']); exit;
}
}
// création client rapide ?
if (($_POST['action'] ?? '') === 'create_client') {
header('Content-Type: application/json; charset=utf-8');
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) {
http_response_code(400);
exit(json_encode(['ok'=>false,'msg'=>'CSRF']));
}
$nom = trim((string)($_POST['nom'] ?? ''));
$prenom = trim((string)($_POST['prenom'] ?? ''));
$email = email_norm((string)($_POST['email'] ?? ''));
$tel = trim((string)($_POST['telephone'] ?? ''));
$dob = trim((string)($_POST['date_naissance'] ?? '')); // YYYY-MM-DD
if ($nom === '' || $prenom === '' || $email === '' || $tel === '') {
exit(json_encode(['ok'=>false,'msg'=>($translations['donnees_manquantes_reservation'] ?? 'Champs manquants')]));
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
exit(json_encode(['ok'=>false,'msg'=>($translations['email_invalide'] ?? 'Email invalide')]));
}
try {
$pdo->beginTransaction();
// Si un compte existe déjà pour cet email
$acc = $pdo->prepare("SELECT id, role, reference_id FROM compte WHERE email = ? LIMIT 1");
$acc->execute([$email]);
$exist = $acc->fetch(PDO::FETCH_ASSOC);
if ($exist) {
if (($exist['role'] ?? '') !== 'client') {
// Email déjà utilisé par praticien/admin/secretaire => refuser création
$pdo->rollBack();
exit(json_encode(['ok'=>false,'msg'=>($translations['email_deja_utilise'] ?? 'Cet email est déjà utilisé.')]));
}
// Récupérer le client lié via reference_id
$cid = (int)($exist['reference_id'] ?? 0);
if ($cid > 0) {
// Pas de re-création, on renvoie le client existant pour réserver directement
$pdo->commit();
exit(json_encode(['ok'=>true,'client_id'=>$cid,'msg'=>($translations['client_existant_utilise'] ?? 'Client existant sélectionné.')]));
}
// Compte client sans fiche client (rare) -> on crée la fiche et on lie le compte
$insC = $pdo->prepare("
INSERT INTO client (nom, prenom, email, telephone, date_naissance, accepted_cgu)
VALUES (?,?,?,?,?,1)
");
$insC->execute([$nom, $prenom, $email, $tel, ($dob ?: null)]);
$newClientId = (int)$pdo->lastInsertId();
$upd = $pdo->prepare("UPDATE compte SET reference_id = ? WHERE id = ? LIMIT 1");
$upd->execute([$newClientId, (int)$exist['id']]);
$pdo->commit();
exit(json_encode(['ok'=>true,'client_id'=>$newClientId,'msg'=>($translations['client_cree_ok'] ?? 'Client créé.')]));
}
// Sinon : créer la fiche client
$ins = $pdo->prepare("
INSERT INTO client (nom, prenom, email, telephone, date_naissance, accepted_cgu)
VALUES (?,?,?,?,?,1)
");
$ins->execute([$nom, $prenom, $email, $tel, ($dob ?: null)]);
$newClientId = (int)$pdo->lastInsertId();
// Créer le compte avec MDP provisoire hashé + token de validation email
$provisional = 'Uslugi_11992288';
$hash = password_hash($provisional, PASSWORD_DEFAULT);
$token = bin2hex(random_bytes(32)); // TTL 24h géré côté endpoint (via cree_le ou table dédiée)
$accIns = $pdo->prepare("
INSERT INTO compte (email, mot_de_passe, role, reference_id, token_validation, email_valide, cree_le, modifie_le)
VALUES (?,?,?,?,?,0,NOW(),NOW())
");
$accIns->execute([$email, $hash, 'client', $newClientId, $token]);
$pdo->commit();
// Envoyer l'email d'activation (sans ICS)
try {
$activationUrl = 'https://uslugi.fr/valider_email_cabinet.php?token='.urlencode($token).'&email='.urlencode($email);
$subj_a = $translations['activation_compte_sujet'] ?? 'Активируйте ваш аккаунт / Activez votre compte';
$helloName = trim($prenom.' '.$nom);
$dataA = [
'hello' => sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s', $helloName),
'intro' => $translations['activation_compte_intro'] ??
"Чтобы завершить создание аккаунта, пожалуйста подтвердите ваш e-mail и задайте новый пароль.\nPour finaliser la création de votre compte, confirmez votre e-mail et définissez votre mot de passe.",
'who_label' => $translations['email_label'] ?? 'E-mail',
'who_value' => $email,
'when' => date('d/m/Y H:i'),
'format_line' => '',
'address_line'=> '',
'cta_url' => $activationUrl,
'cta_label' => $translations['placeholder_nouveau_mdp'] ?? 'Définir le mot de passe',
'footer' => $translations['merci_simple'] ?? 'Merci.',
];
$text_a = email_tpl_text($dataA, $translations);
$html_a = email_tpl_html($dataA, $translations);
// Pas de pièce jointe ICS ici
send_mail_ics($email, $helloName, $subj_a, $text_a, $html_a, null, null, null);
} catch(Throwable $e) {
error_log('activation email send failed: '.$e->getMessage());
}
exit(json_encode([
'ok' => true,
'client_id' => $newClientId,
'msg' => ($translations['client_cree_ok'] ?? 'Client créé.')
]));
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
error_log('create_client: '.$e->getMessage());
exit(json_encode(['ok'=>false,'msg'=>'Erreur']));
}
}
// validation RDV
if (($_POST['action'] ?? '') === 'validate') {
header('Content-Type: application/json; charset=utf-8');
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) {
echo json_encode(['ok'=>false,'html'=>'<div class="msg"><h2>CSRF</h2></div>']); exit;
}
$client_id = (int)($_POST['client_id'] ?? 0);
$startIso = $_POST['start'] ?? null;
$mode_in = $_POST['mode_rdv'] ?? null;
if ($client_id <= 0 || !$startIso || $praticien_id <= 0) {
echo json_encode(['ok'=>false,'html'=>'<div class="msg"><h2>'.h($translations['donnees_manquantes_reservation'] ?? 'Données manquantes').'</h2></div>']); exit;
}
// vérifie que le client existe
$c = $pdo->prepare("SELECT id, email, nom, prenom, telephone FROM client WHERE id=?");
$c->execute([$client_id]);
$client = $c->fetch(PDO::FETCH_ASSOC);
if (!$client) {
echo json_encode(['ok'=>false,'html'=>'<div class="msg"><h2>Client introuvable</h2></div>']); exit;
}
// Optionnel: lier au praticien ici si tu veux (sinon, tu as dit que ça se fera à la confirmation)
// $pl = $pdo->prepare("INSERT IGNORE INTO client_dossiers (praticien_id, client_id, cree_le) VALUES (?,?,NOW())");
// $pl->execute([$praticien_id, $client_id]);
// créneau libre ?
$date_sql = (new DateTime($startIso))->format('Y-m-d H:i:s');
$dup = $pdo->prepare("SELECT COUNT(*) FROM rendez_vous WHERE praticien_id=? AND date_rdv=?");
$dup->execute([$praticien_id, $date_sql]);
if ((int)$dup->fetchColumn() > 0) {
echo json_encode(['ok'=>false,'html'=>'<div class="msg"><h2>'.h($translations['creneau_deja_reserve'] ?? 'Créneau déjà réservé').'</h2></div>']); exit;
}
// mode / location
$mode = ($mode_in === $GLOBALS['c_offline']) ? 'offline' : 'online';
if ($mode === 'offline' && !$GLOBALS['has_address']) $mode = 'online';
// insert RDV (statut 'prévu')
$uuid = bin2hex(random_bytes(16));
$ins = $pdo->prepare("
INSERT INTO rendez_vous (praticien_id, client_id, date_rdv, duree_minutes, lieu, statut, commentaire, uuid_ical, created_at)
VALUES (?, ?, ?, ?, ?, 'prévu', '', ?, NOW())
");
$ins->execute([$praticien_id, $client_id, $date_sql, $duree, $mode, $uuid]);
// =========================
// LOG RDV (rdv_actions_log) : create (secrétaire)
// =========================
$rdvIdCreated = (int)$pdo->lastInsertId();
// ✅ compte_id client + praticien (pour badge counters)
$clientCompteId = 0;
$praticienCompteId = 0;
$st = $pdo->prepare("SELECT id FROM compte WHERE role='client' AND reference_id=? LIMIT 1");
$st->execute([(int)$client_id]);
$clientCompteId = (int)($st->fetchColumn() ?: 0);
$st = $pdo->prepare("SELECT id FROM compte WHERE role='praticien' AND reference_id=? LIMIT 1");
$st->execute([(int)$praticien_id]);
$praticienCompteId = (int)($st->fetchColumn() ?: 0);
// ✅ Badge RDV : +1 pour le client (RDV créé par secrétaire)
if ($clientCompteId > 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' => $clientCompteId]);
}
// ✅ Badge RDV : +1 pour le praticien (nouveau RDV à confirmer/voir)
if ($praticienCompteId > 0) {
$pdo->prepare("
INSERT INTO push_badge_counters (compte_id, msg_unread, rdv_unread)
VALUES (:pid, 0, 1)
ON DUPLICATE KEY UPDATE
rdv_unread = LEAST(99, rdv_unread + 1),
updated_at = NOW()
")->execute([':pid' => $praticienCompteId]);
}
$afterJson = json_encode([
'statut' => 'prévu',
'date_rdv' => $date_sql,
'duree_minutes' => (int)$duree,
'lieu' => $mode, // 'online'/'offline'
'praticien_id' => (int)$praticien_id,
'client_id' => (int)$client_id,
], 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
(?, 'create', ?, 'secretaire', NULL, ?, NOW())
");
$logStmt->execute([$rdvIdCreated, $secretaire_user_id, $afterJson]);
// =========================
// =========================
// PUSH RDV — création (par secrétaire)
// =========================
try {
// 🔔 Client : nouveau RDV créé
push_rdv_to_client(
$pdo,
(int)$client_id,
$translations['push_rdv_created_title'] ?? 'Новая запись',
$translations['push_rdv_created_body'] ?? 'Для вас создана новая запись.',
'/public/client/mes_rendez_vous.php',
[
'rdv_id' => (int)$rdvIdCreated,
'action' => 'create',
'from_role' => 'secretaire',
'praticien_id' => (int)$praticien_id,
'client_id' => (int)$client_id,
'date_rdv' => $date_sql,
]
);
// 🔔 Praticien : nouveau RDV créé par secrétaire
push_rdv_to_praticien(
$pdo,
(int)$praticien_id,
$translations['push_rdv_created_praticien_title'] ?? 'Новая запись',
$translations['push_rdv_created_praticien_body'] ?? 'Секретарь создал(а) новую запись.',
'/public/praticien/rendez_vous.php',
[
'rdv_id' => (int)$rdvIdCreated,
'action' => 'create',
'from_role' => 'secretaire',
'secretaire_id' => (int)$secretaire_user_id,
'praticien_id' => (int)$praticien_id,
'client_id' => (int)$client_id,
'date_rdv' => $date_sql,
]
);
} catch (Throwable $e) {
error_log('push rdv create (secretaire) failed: '.$e->getMessage());
}
// Récup infos complètes pour e-mails
$q = $pdo->prepare("
SELECT rv.id AS rdv_id, rv.date_rdv, rv.duree_minutes, rv.lieu,
p.nom AS nom_praticien, p.prenom AS prenom_praticien, p.email AS email_praticien, p.telephone AS tel_praticien,
p.adresse AS p_adresse, p.ville AS p_ville, p.code_postal AS p_cp,
c.email AS email_client, c.nom AS nom_client, c.prenom AS prenom_client, c.telephone AS tel_client
FROM rendez_vous rv
JOIN praticien p ON rv.praticien_id = p.id
JOIN client c ON rv.client_id = c.id
WHERE rv.uuid_ical = ?
LIMIT 1
");
$q->execute([$uuid]);
$rdv = $q->fetch(PDO::FETCH_ASSOC);
if (!$rdv) {
echo json_encode(['ok'=>false,'html'=>'<div class="msg"><h2>RDV introuvable</h2></div>']); exit;
}
// Construire la réponse UI immédiatement
$tz='Europe/Paris'; $tzObj = new DateTimeZone($tz);
$start = new DateTime($rdv['date_rdv'], $tzObj);
$end = (clone $start)->modify('+'.(int)$rdv['duree_minutes'].' minutes');
$when_a = $start->format('d/m/Y') . ' ' . ($translations['a'] ?? 'à') . ' ' . $start->format('H:i');
$html = '<div class="card-notice" style="background:#fff;border-radius:22px;box-shadow:0 12px 40px rgba(0,0,0,.08);padding:22px;">
<h1 class="cn-title" style="margin:0 0 6px">'.h($translations['block_1'] ?? 'Спасибо за запись').'</h1>
<div class="cn-date" style="color:#60646c;margin-bottom:8px">'.h($when_a).'</div>
<p>'.h($translations['block_2'] ?? 'Ожидает подтверждения').'</p>
<div class="actions" style="margin-top:12px">
<a class="btn btn-outline" href="/public/cabinet/rendez_vous.php?praticien_id='.(int)$praticien_id.'">'.h($translations['retour_a_la_liste'] ?? 'Retour à la liste').'</a>
</div>
</div>';
// envoyer la réponse puis continuer (si FastCGI)
session_write_close();
echo json_encode(['ok'=>true,'html'=>$html]);
if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
ignore_user_abort(true);
// e-mails + ICS en arrière-plan
try {
$addr = trim(implode(', ', array_filter([
trim((string)$rdv['p_adresse']),
trim((string)$rdv['p_cp'].' '.$rdv['p_ville'])
])));
$is_offline = ($rdv['lieu'] === 'offline');
$loc = $is_offline ? ($addr ?: ($translations['option_cabinet'] ?? 'Очно')) : ($translations['option_online'] ?? 'Онлайн');
$uid = $uuid;
$ics_client = ics_build(
$uid,$start,$end,$tz,
($translations['rdv_avec'] ?? 'Rendez-vous avec').' '.($rdv['prenom_praticien'].' '.$rdv['nom_praticien']),
'', $loc,
[
['cn'=>$rdv['prenom_praticien'].' '.$rdv['nom_praticien'],'email'=>$rdv['email_praticien']],
['cn'=>$rdv['prenom_client'].' '.$rdv['nom_client'],'email'=>$rdv['email_client']]
],
'REQUEST','CONFIRMED'
);
$ics_prat = ics_build(
$uid,$start,$end,$tz,
($translations['rdv_avec'] ?? 'Rendez-vous avec').' '.($rdv['prenom_client'].' '.$rdv['nom_client']),
'', $loc,
[
['cn'=>$rdv['prenom_client'].' '.$rdv['nom_client'],'email'=>$rdv['email_client']],
['cn'=>$rdv['prenom_praticien'].' '.$rdv['nom_praticien'],'email'=>$rdv['email_praticien']]
],
'REQUEST','CONFIRMED'
);
$cta_client_url='/public/client/mes_rendez_vous.php';
$cta_prat_url ='/public/praticien/rendez_vous.php?id='.(int)$rdv['rdv_id'];
$cta_label = $translations['consultez_espace_pour_details'] ?? 'Voir les détails';
// Client — même template propre que pour le praticien
$subj_c = $translations['email_confirmation_rdv_sujet'] ?? 'Confirmation de rendez-vous';
$dataC = [
'hello' => sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s',
trim(($rdv['prenom_client'] ?? '').' '.($rdv['nom_client'] ?? ''))),
'intro' => ($translations['votre_rdv_attente'] ?? 'Votre rendez-vous est en attente de confirmation').' '.
($translations['email_avec'] ?? 'avec').' '.
trim(($rdv['prenom_praticien'] ?? '').' '.($rdv['nom_praticien'] ?? '')).'.',
'who_label' => $translations['praticien_label'] ?? 'Praticien',
'who_value' => trim(($rdv['prenom_praticien'] ?? '').' '.($rdv['nom_praticien'] ?? '')),
'when' => $when_a,
'format_line' => ($rdv['lieu'] === 'offline')
? ($translations['option_cabinet'] ?? 'В кабинете')
: ($translations['option_online'] ?? 'Онлайн'),
'address_line'=> ($rdv['lieu'] === 'offline') ? ($addr ?: '') : '',
'cta_url' => $cta_client_url,
'cta_label' => $translations['consultez_espace_pour_details'] ?? 'Voir les détails',
'footer' => $translations['merci_simple'] ?? 'Merci.',
];
$text_c = email_tpl_text($dataC, $translations);
$html_c = email_tpl_html($dataC, $translations);
send_mail_ics(
(string)$rdv['email_client'],
(string)trim(($rdv['prenom_client'] ?? '').' '.($rdv['nom_client'] ?? '')),
$subj_c, $text_c, $html_c,
'rdv_'.$uid.'_client.ics', $ics_client, 'REQUEST'
);
// Praticien
$subj_p = ($translations['nouveau_rdv_avec'] ?? 'Nouveau rendez-vous avec').' '.($rdv['prenom_client'].' '.$rdv['nom_client']);
$dataP = [
'hello' => ($translations['email_bonjour_dr'] ?? 'Bonjour').' '.($rdv['prenom_praticien'].' '.$rdv['nom_praticien']),
'intro' => ($translations['nouveau_rdv_pris_avec'] ?? 'Un nouveau rendez-vous a été pris.'),
'who_label' => $translations['client_label'] ?? 'Client',
'who_value' => ($rdv['prenom_client'].' '.$rdv['nom_client']),
'when' => $when_a,
'format_line' => ($rdv['lieu'] === 'offline')
? ($translations['option_cabinet'] ?? 'В кабинете')
: ($translations['option_online'] ?? 'Онлайн'),
'address_line'=> ($rdv['lieu'] === 'offline') ? ($addr ?: '') : '',
'cta_url' => $cta_prat_url,
'cta_label' => $cta_label,
'footer' => $translations['merci_simple'] ?? 'Merci.',
];
$text_p = email_tpl_text($dataP, $translations);
$html_p = email_tpl_html($dataP, $translations);
send_mail_ics(
(string)$rdv['email_praticien'],
(string)($rdv['prenom_praticien'].' '.$rdv['nom_praticien']),
$subj_p, $text_p, $html_p,
'rdv_'.$uid.'_praticien.ics', $ics_prat, 'REQUEST'
);
// WhatsApp côté praticien (optionnel)
if (function_exists('Sms\\notify_rdv_en_attente')) {
$waTo = (string)($rdv['tel_praticien'] ?? '');
if ($waTo) {
try { \Sms\notify_rdv_en_attente($waTo, trim($rdv['prenom_client'].' '.$rdv['nom_client']), $start->format('d/m/Y H:i'), (int)$rdv['rdv_id']); }
catch(Throwable $e){ error_log('WA notify error: '.$e->getMessage()); }
}
}
} catch(Throwable $e){
error_log('cabinet valider_rdv bg: '.$e->getMessage());
}
exit;
}
http_response_code(400);
exit('Bad request');
}
/* ================== Mode GET : UI ================== */
$title = $translations['titre_validation_secretaire'] ?? 'Validation du rendez-vous (secrétaire)';
?>
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title><?= h($title) ?></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script src="/js/sweetalert2@11.js"></script>
<style>
body{margin:0;background:#8CA9FF;font-family:system-ui,Segoe UI,Roboto,Arial}
.wrap{max-width:720px;margin:32px auto;padding:16px}
.card{background:#fff;border-radius:18px;box-shadow:0 12px 36px rgba(0,0,0,.08);padding:20px}
.row{display:grid;gap:12px}
label{font-weight:600}
input,select{width:97%;padding:10px;border:1px solid #e2e8f0;border-radius:10px}
.btn{display:inline-block;border-radius:999px;padding:10px 16px;text-decoration:none;font-weight:700}
.btn-primary{background:#E03C3F;color:#fff}
.btn-outline{background:#fff;border:1px solid #e2e8f0;color:#38378B}
.actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}
.muted{color:#6b7280}
.hint{font-size:12px;color:#6b7280}
.pill{display:inline-block;padding:2px 8px;border-radius:999px;border:1px solid #e5e7eb;font-size:12px}
.res-item{padding:8px 10px;border:1px solid #e5e7eb;border-radius:10px;margin-bottom:8px;background:#fafafa}
.res-item .meta{color:#6b7280;font-size:12px}
.err{color:#E03C3F;font-size:12px;display:none}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h2 style="margin:0 0 8px"><?= h($title) ?></h2>
<p class="muted" style="margin:0 0 18px">
<?= h(($praticien['prenom'] ?? '').' '.($praticien['nom'] ?? '')) ?> — <?= h($startIso ?: '') ?>
</p>
<?php if (!$praticien_id || !$startIso): ?>
<div class="row">
<p><?= h($translations['donnees_manquantes_reservation'] ?? 'Données manquantes pour la réservation.') ?></p>
<div class="actions">
<a class="btn btn-outline" href="/public/cabinet/rendez_vous.php?praticien_id=<?= (int)$praticien_id ?>"><?= h($translations['retour_a_la_liste'] ?? 'Retour à la liste') ?></a>
</div>
</div>
<?php elseif ($client_id > 0): ?>
<!-- On a déjà un client -> lance validation directe -->
<form id="f-validate" method="post">
<input type="hidden" name="csrf" value="<?= h($CSRF) ?>">
<input type="hidden" name="action" value="validate">
<input type="hidden" name="praticien_id" value="<?= (int)$praticien_id ?>">
<input type="hidden" name="start" value="<?= h($startIso) ?>">
<input type="hidden" name="client_id" value="<?= (int)$client_id ?>">
<input type="hidden" name="mode_rdv" value="<?= h($mode_rdv_in ?? $c_online) ?>">
<p class="muted"><?= h($translations['chargement_validation'] ?? 'Подождите, идёт подтверждение...') ?></p>
</form>
<script>
(function(){
const f = document.getElementById('f-validate');
fetch(location.pathname, {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'},
body: new URLSearchParams(new FormData(f))
}).then(r=>r.json()).then(d=>{
document.querySelector('.card').innerHTML = d.html || '<p>Erreur</p>';
}).catch(()=>{ document.querySelector('.card').innerHTML = '<p>Erreur réseau</p>'; });
})();
</script>
<?php else: ?>
<!-- Sélection / création client -->
<?php
// clients déjà liés à ce praticien
$cli = $pdo->prepare("
SELECT c.id, c.prenom, c.nom, c.email, c.telephone
FROM client_dossiers d
JOIN client c ON c.id = d.client_id
WHERE d.praticien_id = ?
ORDER BY c.prenom, c.nom
");
$cli->execute([$praticien_id]);
$clients = $cli->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="row" style="margin-bottom:16px">
<label><?= h($translations['selectionner_client'] ?? 'Sélectionner un client') ?></label>
<select id="clientSelect">
<option value="0"><?= h($translations['client_existant_placeholder'] ?? '— Choisir un client existant —') ?></option>
<?php foreach ($clients as $c): ?>
<option value="<?= (int)$c['id'] ?>">
<?= h(trim(($c['prenom'] ?? '').' '.($c['nom'] ?? ''))) ?>
<?php if(!empty($c['email'])) echo ' — '.h($c['email']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- Recherche globale des clients plateforme non liés à ce praticien -->
<div class="row" style="border-top:1px dashed #e2e8f0;padding-top:12px">
<label><?= h($translations['recherche_client_plateforme'] ?? 'Rechercher un client (plateforme)') ?></label>
<input type="text" id="searchClient" placeholder="<?= h($translations['placeholder_recherche_client'] ?? 'Email, Nom Prénom ou Téléphone') ?>">
<div class="hint"><?= h($translations['hint_clients_non_lies'] ?? 'Astuce : les résultats n’incluent pas les clients déjà liés à ce praticien.') ?></div>
<div id="searchResults"></div>
</div>
<div class="row" style="border-top:1px dashed #e2e8f0;padding-top:12px">
<label><?= h($translations['creer_client'] ?? 'Créer un nouveau client') ?></label>
<div class="row">
<input type="text" id="newPrenom" placeholder="<?= h($translations['prenom'] ?? 'Prénom') ?>">
<input type="text" id="newNom" placeholder="<?= h($translations['nom'] ?? 'Nom') ?>">
<input type="email" id="newEmail" placeholder="<?= h($translations['email'] ?? 'Email') ?>">
<small id="emailError" class="err"><?= h($translations['email_deja_utilise'] ?? 'Cet email est déjà utilisé.') ?></small>
<input type="text" id="newTel" placeholder="<?= h($translations['telephone'] ?? 'Téléphone') ?>">
<input type="date" id="newDob" placeholder="<?= h($translations['date_naissance'] ?? 'Date de naissance') ?>">
</div>
</div>
<div class="actions">
<a href="#" class="btn btn-primary" id="btn-continue"><?= h($translations['btn_continuer'] ?? 'Continuer') ?></a>
<a href="/public/cabinet/rendez_vous.php?praticien_id=<?= (int)$praticien_id ?>" class="btn btn-outline"><?= h($translations['btn_annuler'] ?? 'Annuler') ?></a>
</div>
<script>
(function(){
const csrf = <?= json_encode($CSRF) ?>;
const praticien_id = <?= (int)$praticien_id ?>;
const start = <?= json_encode($startIso) ?>;
const mode_rdv = <?= json_encode($mode_rdv_in ?? $c_online) ?>;
function validateWith(client_id){
const body = new URLSearchParams({ action:'validate', csrf, praticien_id, start, client_id, mode_rdv });
fetch(location.pathname, { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'}, body })
.then(r=>r.json()).then(d=>{
document.querySelector('.card').innerHTML = d.html || '<p>Erreur</p>';
}).catch(()=>{ Swal.fire('Erreur', 'Réseau', 'error'); });
}
// Recherche dynamique (clients plateforme non liés)
const search = document.getElementById('searchClient');
const results = document.getElementById('searchResults');
let tId = null;
function renderResults(items){
results.innerHTML = '';
if (!items || !items.length) {
results.innerHTML = '<div class="hint"><?= h($translations['cl_aucun_resultat'] ?? 'Aucun résultat') ?></div>';
return;
}
items.forEach(it=>{
const div = document.createElement('div');
div.className = 'res-item';
const name = [it.prenom||'', it.nom||''].join(' ').trim();
const adr = [it.adresse||'', [it.code_postal||'', it.ville||''].join(' ').trim()].filter(Boolean).join(', ');
div.innerHTML =
'<div><strong>'+ (name || '—') +'</strong> <span class="pill">'+ (it.email || '—') +'</span></div>'+
'<div class="meta"><?= h($translations['telephone'] ?? 'Téléphone') ?>: '+ (it.telephone || '—') + (adr? ' • '+adr : '') +'</div>'+
'<div style="margin-top:6px"><button class="btn btn-outline" data-id="'+ it.id +'"><?= h($translations['choisir'] ?? 'Choisir') ?></button></div>';
results.appendChild(div);
});
results.querySelectorAll('button[data-id]').forEach(btn=>{
btn.addEventListener('click', (e)=>{
e.preventDefault();
const id = parseInt(btn.getAttribute('data-id'), 10)||0;
if (id>0) validateWith(id);
});
});
}
function doSearch(q){
if (!q || q.length < 2) { results.innerHTML = ''; return; }
const body = new URLSearchParams({ action:'search_client', csrf, praticien_id, q });
fetch(location.pathname, { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'}, body })
.then(r => r.json())
.then(d => {
if (d.ok) { renderResults(d.items || []); }
else { results.innerHTML = '<div class="hint">'+ (d.msg || 'Erreur') +'</div>'; }
})
.catch(() => { results.innerHTML = '<div class="hint">Erreur réseau</div>'; });
}
search.addEventListener('input', function(){
if (tId) clearTimeout(tId);
tId = setTimeout(()=>doSearch(search.value.trim()), 250);
});
// Vérif e-mail live (utilise /verifier_email.php à la racine)
const emailInput = document.getElementById('newEmail');
const emailErr = document.getElementById('emailError');
let emailOk = true;
function checkEmail(){
const v = (emailInput.value||'').trim();
emailOk = true;
emailErr.style.display = 'none';
if (!v.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) return;
fetch('/verifier_email.php', {
method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'},
body: 'email='+encodeURIComponent(v)
}).then(r=>r.text()).then(tx=>{
if ((tx||'').trim()==='existe') {
emailOk = false;
emailErr.style.display = 'block';
}
}).catch(()=>{ /* silence */ });
}
emailInput.addEventListener('input', checkEmail);
document.getElementById('btn-continue').addEventListener('click', function(e){
e.preventDefault();
const selected = parseInt(document.getElementById('clientSelect').value, 10) || 0;
if (selected > 0) return validateWith(selected);
const prenom = document.getElementById('newPrenom').value.trim();
const nom = document.getElementById('newNom').value.trim();
const email = document.getElementById('newEmail').value.trim();
const tel = document.getElementById('newTel').value.trim();
const dob = document.getElementById('newDob').value.trim();
if (!prenom || !nom || !email || !tel) {
Swal.fire('<?= addslashes($translations['donnees_manquantes_reservation'] ?? 'Champs manquants') ?>','','warning');
return;
}
if (!emailOk) {
Swal.fire('<?= addslashes($translations['email_deja_utilise'] ?? 'Cet email est déjà utilisé.') ?>','','warning');
return;
}
const body = new URLSearchParams({ action:'create_client', csrf, praticien_id, nom, prenom, email, telephone:tel, date_naissance:dob });
fetch(location.pathname, { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'}, body })
.then(r=>r.json()).then(d=>{
if (!d.ok) throw new Error(d.msg || 'Erreur');
validateWith(d.client_id);
}).catch(err=>{
Swal.fire('Erreur', err.message || 'Erreur', 'error');
});
});
})();
</script>
<?php endif; ?>
</div>
</div>
</body>
</html>