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

289 lines
9.9 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
/**
* /visio/join.php?rdv=ID[&start=1]
* - Accès: client OU praticien liés au RDV
* - Fenêtre horaire respectée (config)
* - Création de salle SEULEMENT si praticien + start=1
* - Jitsi via external_api.js
*/
$config = require __DIR__ . '/visio_config.php';
// Sécurité & DB
require_once $config['paths']['securite'];
require_login_or_redirect(['client','praticien']);
require_once $config['paths']['db'];
// TZ cohérente
date_default_timezone_set('Europe/Paris');
$tz = new DateTimeZone('Europe/Paris');
// Paramètres
$rdv_id = (int)($_GET['rdv'] ?? 0);
if ($rdv_id <= 0) { http_response_code(400); exit('Paramètre manquant.'); }
// RDV
$stmt = $pdo->prepare("
SELECT rv.id, rv.praticien_id, rv.client_id, rv.date_rdv
FROM rendez_vous rv
WHERE rv.id = ?
LIMIT 1
");
$stmt->execute([$rdv_id]);
$rdv = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$rdv) { http_response_code(404); exit('Rendez-vous introuvable.'); }
// Rôle & appartenance
$role = $_SESSION['role'] ?? '';
$refId = (int)($_SESSION['reference_id'] ?? 0);
$ok = ($role === 'praticien' && $refId === (int)$rdv['praticien_id'])
|| ($role === 'client' && $refId === (int)$rdv['client_id']);
if (!$ok) { http_response_code(403); exit('Accès refusé.'); }
// Fenêtre d’accès temporelle (si ta BDD est en UTC, décommente la conversion)
// $start = (new DateTime($rdv['date_rdv'], new DateTimeZone('UTC')))->setTimezone($tz);
$start = new DateTime($rdv['date_rdv'], $tz);
$open = (clone $start)->modify('-' . (int)$config['open_before_minutes'] . ' minutes');
$close = (clone $start)->modify('+' . (int)$config['close_after_minutes'] . ' minutes');
$now = new DateTime('now', $tz);
if ($now < $open) { http_response_code(403); exit('La salle ouvre à '.$open->format('H:i').'.'); }
if ($now > $close){ http_response_code(403); exit('La salle a fermé à '.$close->format('H:i').'.'); }
// Création/obtention de salle
require_once __DIR__ . '/visio_create_or_get.php';
$allowCreate = ($role === 'praticien' && isset($_GET['start']) && $_GET['start'] == '1');
$roomSlug = visio_room_for_rdv($pdo, $rdv_id, $config['slug_secret'], $allowCreate);
if (!$roomSlug) {
// Pas de salle -> on affiche un message UX propre + code HTTP 403
header('Content-Type: text/html; charset=UTF-8', true, 403);
echo <<<HTML
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Ваша консультация скоро начнётся</title>
<style>
:root{
--brand-blue:#38378B;
--brand-red:#E03C3F;
--brand-mint:#06FFA8;
--text:#1d1d1f;
--muted:#6b7280;
--bg:#8CA9FF;
--card:#ffffff;
--radius:20px;
--shadow:0 10px 30px rgba(0,0,0,.08);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
background:var(--bg);
color:var(--text);
font-family: "Montserrat",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
display:flex; align-items:center; justify-content:center; padding:24px;
}
.msg{
width:min(720px, 100%);
background:var(--card);
border-radius:var(--radius);
box-shadow:var(--shadow);
padding:28px 24px;
text-align:center;
border:1px solid rgba(56,55,139,.08);
}
.msg__icon{
width:64px; height:64px; margin:4px auto 14px;
display:grid; place-items:center;
border-radius:50%;
background:rgba(56,55,139,.07);
}
.msg__icon svg{width:34px; height:34px; display:block; color:var(--brand-blue)}
.msg__title{
margin:0 0 10px;
font-weight:700;
font-size:clamp(18px,2.6vw,24px);
color:var(--brand-blue);
letter-spacing:.2px;
}
.msg__text{
margin:0 auto 16px;
max-width:46ch;
line-height:1.55;
color:#2b2b2b;
font-size:clamp(14px,2vw,16px);
}
.msg__hint{
color:var(--muted);
font-size:13px;
margin-top:10px;
}
.msg__actions{
margin-top:18px; display:flex; gap:10px; justify-content:center; flex-wrap:wrap;
}
.btn{
appearance:none; border:0; cursor:pointer;
padding:10px 18px; border-radius:999px; font-weight:600;
transition:.2s transform ease, .2s box-shadow ease, .2s background-color ease;
}
.btn:active{ transform:translateY(1px); }
.btn-primary{
background:var(--brand-red); color:#fff;
box-shadow:0 6px 16px rgba(56,55,139,.25);
}
.btn-primary:hover{ background:#E03C3F; }
.btn-ghost{
background:transparent; color:var(--brand-blue);
border:1px solid rgba(56,55,139,.25);
}
.badge{
display:inline-block; margin:auto; margin-bottom:8px;
padding:4px 10px; border-radius:999px;
background:rgba(6,255,168,.14); color:#0a7a5f; font-weight:600; font-size:12px;
border:1px solid rgba(6,255,168,.25);
}
a { text-decoration: none;}
</style>
</head>
<body>
<main class="msg" role="alert" aria-live="polite">
<div class="msg__icon" aria-hidden="true">
<!-- clock icon -->
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
<div class="badge">Ожидание подключения</div>
<h1 class="msg__title">Ваша консультация скоро начнётся</h1>
<p class="msg__text">
Пожалуйста, оставайтесь на этой странице — специалист подключится в ближайшее время.
Если окно не открылось автоматически, нажмите «Обновить».
</p>
<div class="msg__actions">
<button class="btn btn-primary" onclick="location.reload()">Обновить</button>
<a class="btn btn-ghost" href="/public/client/mes_rendez_vous.php">Мои консультации</a>
</div>
<p class="msg__hint">Подсказка: проверьте доступ к камере и микрофону заранее.</p>
</main>
</body>
</html>
HTML;
exit;
}
// Nom affiché
//$displayName = htmlspecialchars($_SESSION['display_name'] ?? ($role === 'praticien' ? 'Praticien' : 'Client'));
//$jitsiDomain = $config['jitsi_domain'];
require_once __DIR__ . '/nom_afficher.php';
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Visio — RDV #<?= (int)$rdv_id ?></title>
<style>
html, body { height: 100%; margin: 0; }
#jitsi { height: 100%; }
</style>
<script src="https://<?= htmlspecialchars($jitsiDomain) ?>/external_api.js"></script>
</head>
<body>
<div id="jitsi"></div>
<script>
(function () {
const domain = "<?= htmlspecialchars($jitsiDomain) ?>";
const REDIRECT_URL = "/visio/fin.php?rdv=<?= (int)$rdv_id ?>";
const isPraticien = <?= json_encode(($role ?? '') === 'praticien') ?>;
const options = {
roomName: "<?= htmlspecialchars($roomSlug) ?>",
parentNode: document.querySelector('#jitsi'),
userInfo: { displayName: <?= json_encode($displayName) ?> },
configOverwrite: {
prejoinConfig: { enabled: true },
startWithAudioMuted: false,
startWithVideoMuted: false,
disableDeepLinking: true,
enableClosePage: false
}
};
const api = new JitsiMeetExternalAPI(domain, options);
// Redirection quand l’utilisateur quitte
api.addListener('videoConferenceLeft', () => { window.location.assign(REDIRECT_URL); });
api.addListener('readyToClose', () => { window.location.assign(REDIRECT_URL); });
// ---- Bandeau “durée dépassée” (praticien uniquement) ----
if (isPraticien) {
// Charger Montserrat si absent
(function ensureMontserrat(){
const id = 'montserrat-font-link';
if (!document.getElementById(id)) {
const pre = document.createElement('link');
pre.rel = 'preconnect'; pre.href = 'https://fonts.gstatic.com'; pre.crossOrigin = 'anonymous';
const link = document.createElement('link');
link.id = id;
link.rel = 'stylesheet';
link.href = 'https://fonts.googleapis.com/css2?family=Montserrat:wght@500;600;700&display=swap';
document.head.appendChild(pre);
document.head.appendChild(link);
}
})();
const dureeMinutes = <?= (int)($rdv['duree_minutes'] ?? 60) ?>; // durée prévue (BDD)
const startTime = new Date("<?= (new DateTime($rdv['date_rdv'], new DateTimeZone('Europe/Paris')))->format('Y-m-d H:i:s') ?>");
const plannedEnd = new Date(startTime.getTime() + dureeMinutes * 60000);
function showOvertimeBanner() {
if (document.getElementById('overtime-banner')) return;
const banner = document.createElement('div');
banner.id = 'overtime-banner';
banner.textContent = "⚠️ Время консультации истекло. Пожалуйста, завершите встречу.";
Object.assign(banner.style, {
position: 'fixed',
bottom: '10px',
left: '50%',
transform: 'translateX(-50%)',
background: '#E03C3F',
color: '#fff',
padding: '10px 18px',
borderRadius: '8px',
fontSize: '14px',
fontWeight: '600',
zIndex: '9999',
boxShadow: '0 6px 16px rgba(0,0,0,.2)',
fontFamily: "Montserrat,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif"
});
document.body.appendChild(banner);
}
function checkOvertime() {
const now = new Date();
if (now > plannedEnd) showOvertimeBanner();
}
// Check à T0 (au cas où on lance en retard) puis toutes les 60s
checkOvertime();
setInterval(checkOvertime, 60000);
}
// Nettoyage
window.addEventListener('beforeunload', () => api.dispose());
})();
</script>
</body>
</html>