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

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