Init Git Prod Mars 2026

This commit is contained in:
Oleg Barykin committed 2026-03-05 10:25:39 +01:00
commit d81efe93b1
362 files changed
+86627

No files matched your search

+879
View File
@@ -0,0 +1,879 @@
<?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>