Init Git Prod Mars 2026
This commit is contained in:
commit
d81efe93b1
362 files changed
+86627
No files matched your search
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
session_start();
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
|
||||
$rdvId = (int)($_GET['rdv'] ?? 0);
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
if ($role === 'praticien') {
|
||||
$backUrl = '/public/praticien/rendez_vous.php';
|
||||
$backText = 'Мои консультации';
|
||||
} else {
|
||||
$backUrl = '/public/client/mes_rendez_vous.php';
|
||||
$backText = 'Мои консультации';
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Спасибо — uslugi.fr</title>
|
||||
<style>
|
||||
:root{
|
||||
--color-hero:#8CA9FF; --brand-blue:#38378B; --brand-red:#E03C3F;
|
||||
--card:#fff; --text:#1d1d1f; --muted:#6b7280;
|
||||
--radius:22px; --shadow:0 12px 30px rgba(0,0,0,.10);
|
||||
}
|
||||
*{box-sizing:border-box} html,body{height:100%}
|
||||
body{margin:0;background:var(--color-hero);font-family:Montserrat,system-ui,Arial,sans-serif;
|
||||
color:var(--text);display:flex;align-items:center;justify-content:center;padding:24px;}
|
||||
.card{width:min(760px,100%);background:var(--card);border-radius:var(--radius);
|
||||
box-shadow:var(--shadow);padding:28px 22px;text-align:center;border:1px solid rgba(56,55,139,.08);}
|
||||
h1{margin:6px 0 10px;font-size:clamp(20px,2.8vw,26px);color:var(--brand-blue)}
|
||||
p{margin:0 auto 12px;max-width:60ch;line-height:1.6}
|
||||
.muted{color:var(--muted);font-size:14px}
|
||||
.actions{margin-top:18px;display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
|
||||
.btn{appearance:none;border:0;cursor:pointer;padding:12px 18px;font-weight:700;border-radius:999px;
|
||||
text-decoration:none;display:inline-block;transition:.2s transform,.2s background-color,.2s box-shadow}
|
||||
.btn:active{transform:translateY(1px)}
|
||||
.btn-primary{background:var(--brand-red);color:#fff;box-shadow:0 8px 18px 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)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card" role="main">
|
||||
<h1>Спасибо за использование видеосвязи на uslugi.fr</h1>
|
||||
<p>Надеемся, что ваш опыт был комфортным и полезным. <br>Спасибо за доверие! <br>Команда <strong>uslugi.fr</strong>.</p>
|
||||
<?php if ($rdvId): ?>
|
||||
<p class="muted">ID консультации: #<?= $rdvId ?></p>
|
||||
<?php endif; ?>
|
||||
<div class="actions">
|
||||
<a class="btn btn-primary" href="https://uslugi.fr/">Вернуться на сайт</a>
|
||||
<a class="btn btn-ghost" href="<?= htmlspecialchars($backUrl) ?>"><?= htmlspecialchars($backText) ?></a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+288
@@ -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>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* /nom_afficher.php
|
||||
* Affiche le nom et prenom de la personne connecté lors de visio en Jitsi
|
||||
*/
|
||||
$displayName = 'Invité';
|
||||
|
||||
try {
|
||||
if ($refId > 0 && in_array($role, ['praticien','client'], true)) {
|
||||
// Adapte les noms de tables/colonnes si besoin
|
||||
if ($role === 'praticien') {
|
||||
$sql = "SELECT nom, prenom FROM praticien WHERE id = :id LIMIT 1";
|
||||
} else { // client
|
||||
$sql = "SELECT nom, prenom FROM client WHERE id = :id LIMIT 1";
|
||||
}
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute(['id' => $refId]);
|
||||
if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$pn = trim(($row['prenom'] ?? '') . ' ' . ($row['nom'] ?? ''));
|
||||
if ($pn !== '') {
|
||||
$displayName = $pn;
|
||||
} else {
|
||||
$displayName = ($role === 'praticien') ? 'Praticien' : 'Client';
|
||||
}
|
||||
} else {
|
||||
$displayName = ($role === 'praticien') ? 'Praticien' : 'Client';
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// En cas d’erreur, on garde un fallback propre
|
||||
$displayName = ($role === 'praticien') ? 'Praticien' : 'Client';
|
||||
}
|
||||
|
||||
// Optionnel: mémoriser pour d'autres pages
|
||||
$_SESSION['display_name'] = $displayName;
|
||||
|
||||
// Sécuriser pour l'injecter côté JS
|
||||
$displayName = htmlspecialchars($displayName, ENT_QUOTES, 'UTF-8');
|
||||
$jitsiDomain = $config['jitsi_domain'];
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* Configuration du module Visio
|
||||
* Adapte les chemins si tes includes sont ailleurs.
|
||||
*/
|
||||
require_once __DIR__ . '/../public/includes/init_pr_cl.php';
|
||||
return [
|
||||
// Domaine Jitsi public gratuit (pour MVP)
|
||||
'jitsi_domain' => 'meet.uslugi.fr',
|
||||
|
||||
// Ouvrir la salle X minutes AVANT l'heure du RDV
|
||||
'open_before_minutes' => 15,
|
||||
|
||||
// Fermer l'accès X minutes APRES l'heure du RDV
|
||||
'close_after_minutes' => 120,
|
||||
|
||||
// Secret pour générer un slug de salle non devinable (change-le !)
|
||||
'slug_secret' => getenv('JITSI_SLUG_SECRET') ?: '',
|
||||
|
||||
// Chemins vers tes includes du projet
|
||||
'paths' => [
|
||||
'db' => __DIR__ . '/../includes/db.php',
|
||||
'securite' => __DIR__ . '/../public/includes/securite.php',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Retourne le room_slug pour un RDV.
|
||||
* - Si existe en BD -> le retourne
|
||||
* - Sinon, ne crée QUE si $allowCreate = true (praticien + start=1)
|
||||
* Table attendue: visio_sessions (id, rendez_vous_id UNIQUE, room_slug, created_at)
|
||||
*/
|
||||
|
||||
function visio_room_for_rdv(PDO $pdo, int $rdvId, string $secret, bool $allowCreate): ?string
|
||||
{
|
||||
// 1) Déjà créée ?
|
||||
$sel = $pdo->prepare("SELECT room_slug FROM visio_sessions WHERE rendez_vous_id = ? LIMIT 1");
|
||||
$sel->execute([$rdvId]);
|
||||
$row = $sel->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row && !empty($row['room_slug'])) {
|
||||
return $row['room_slug'];
|
||||
}
|
||||
|
||||
if (!$allowCreate) {
|
||||
return null; // patient avant ouverture par praticien
|
||||
}
|
||||
|
||||
// 2) Créer un slug non prévisible
|
||||
// -> stable pour la séance, mais non devinable
|
||||
$raw = $rdvId . '|' . $secret . '|' . random_bytes(8);
|
||||
$slug = 'uslugi-' . substr(hash('sha256', $raw), 0, 20);
|
||||
|
||||
// 3) Insérer (idempotent via UNIQUE(rendez_vous_id))
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO visio_sessions (rendez_vous_id, room_slug)
|
||||
VALUES (:r, :s)
|
||||
ON DUPLICATE KEY UPDATE room_slug = VALUES(room_slug)
|
||||
");
|
||||
$ins->execute(['r' => $rdvId, 's' => $slug]);
|
||||
|
||||
// 4) Relire pour retourner la valeur
|
||||
$sel->execute([$rdvId]);
|
||||
$row = $sel->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $row['room_slug'] ?? null;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user