Files

623 lines
21 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
$lang = $_SESSION['lang'] ?? 'fr';
$PAGE_TITLE = $PAGE_TITLE ?? ($translations['site_name'] ?? 'uslugi.fr');
$HEAD_EXTRA = $HEAD_EXTRA ?? '';
$initiales = $initiales ?? ($GLOBALS['initiales'] ?? 'U');
$avatarSvg = $avatar_svg ?? ($GLOBALS['avatar_svg'] ?? null);
$pushCfg = $pushCfg ?? ($GLOBALS['pushCfg'] ?? []);
$alerte_abonnement = $alerte_abonnement ?? ($GLOBALS['alerte_abonnement'] ?? null);
require_once __DIR__ . '/../contact_admin.php';
// =====================================================
// [RETOUR / REFERER SÉCURISÉ]
// - Autorise le retour seulement si le referer est sur le même host
// =====================================================
$ref = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
$safeRef = '';
if ($ref) {
$u = parse_url($ref);
if (! empty($u['host']) && $u['host'] === $host) {
$safeRef = $ref;
}
}
$fallbackBack = $safeRef ?: '/';
// =====================================================
// [DETECTION MOBILE]
// - Très simple détection UA pour le logo adaptatif
// =====================================================
$is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGENT']);
?>
<!doctype html>
<html lang="fr">
<head>
<title><?php echo htmlspecialchars($PAGE_TITLE, ENT_QUOTES, 'UTF-8') ?></title>
<?php echo $HEAD_EXTRA ?>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<!-- Police Montserrat -->
<!-- Bootstrap -->
<link href="/css/bootstrap.min.css" rel="stylesheet">
<script defer src="/js/bootstrap.bundle.min.js"></script>
<!-- Styles personnalisés -->
<link rel="stylesheet" href="<?php echo asset('/includes/site_style.css') ?>">
<link rel="stylesheet" href="<?php echo asset('/includes/custom_style.css') ?>"> <!-- ta home -->
<link rel="stylesheet" href="<?php echo asset('/includes/mobil_style.css') ?>">
<?php include_once __DIR__ . '/../../includes/tracking.php'; ?>
</head>
<body>
<?php
// Pour n'afficher qu'une fois par compte (PWA)
$pushOnboardKey = 'uslugi_push_onboard_v1_' . (int) ($_SESSION['compte_id'] ?? 0);
// helper traduction (si tu n'as pas déjà $t ici)
$t = function (string $k, string $fallback = '') use (&$translations): string {
$val = $translations[$k] ?? null;
return ($val === null || $val === '') ? $fallback : (string) $val;
};
?>
<div class="modal fade" id="pushOnboardModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<?php echo htmlspecialchars($t('push_onboard_title', 'Activer les notifications ?'), ENT_QUOTES, 'UTF-8') ?>
</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="<?php echo htmlspecialchars($t('push_onboard_close_aria', 'Fermer'), ENT_QUOTES, 'UTF-8') ?>">
</button>
</div>
<div class="modal-body">
<?php echo htmlspecialchars($t('push_onboard_body', 'Vous recevrez des alertes pour les nouveaux messages et rendez-vous.'), ENT_QUOTES, 'UTF-8') ?>
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-outline-secondary"
data-bs-dismiss="modal"
id="pushLaterBtn">
<?php echo htmlspecialchars($t('push_onboard_later', 'Plus tard'), ENT_QUOTES, 'UTF-8') ?>
</button>
<button type="button"
class="btn btn-primary"
id="pushEnableBtn">
<?php echo htmlspecialchars($t('push_onboard_allow', 'Autoriser'), ENT_QUOTES, 'UTF-8') ?>
</button>
</div>
</div>
</div>
</div>
<!-- ================================================
[HEADER / BARRE DE NAVIGATION]
- Bouton retour sécurisé vers $fallbackBack
- Logo adaptatif mobile/desktop
- Icône messages avec badge non lus
- Menu principal + sous-menu Admin
- Menu "?" compact (aide)
================================================ -->
<header class="site-header py-2 border-bottom">
<?php if (! empty($_SESSION['admin_impersonate'])): ?>
<div class="alert alert-warning text-center mb-0 rounded-0">
Вы вошли как другой пользователь.
<a href="/admin/tools/retour_admin.php"
class="btn btn-sm btn-danger ms-2"
style="border-radius:999px;">
Вернуться в режим администратора
</a>
</div>
<?php endif; ?>
<?php $alerte_abonnement = $GLOBALS['alerte_abonnement'] ?? null; ?>
<?php if (! empty($alerte_abonnement)): ?>
<div class="alert alert-warning m-0 py-1 px-2 rounded"
role="alert"
style="background:#F1F1F1; color:#E03C3F; font-size:14px; text-align:center;">
<?php echo htmlspecialchars((string) $alerte_abonnement, ENT_QUOTES, 'UTF-8') ?>
</div>
<?php endif; ?>
<div class="container d-flex justify-content-between align-items-center">
<!-- Marque + Bouton retour + Logo -->
<div class="header-left">
<div class="brand">
<!-- Bouton retour -->
<a
href="<?php echo htmlspecialchars($fallbackBack, ENT_QUOTES, 'UTF-8') ?>"
id="btnBack"
class="btn btn-light btn-circle me-2"
style="border-radius:5px; box-shadow:0 1px 2px rgba(0,0,0,.08);"
aria-label="<?php echo htmlspecialchars($translations['retour'] ?? 'Retour', ENT_QUOTES, 'UTF-8') ?>"
>
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true">
<path
d="M15 6l-6 6 6 6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</a>
<!-- Logo -->
<a href="/index.php">
<img src="/public/uploads/img/logo_u.png"
alt="uslugi.fr"
class="logo-mobile">
<img src="/public/uploads/img/logo.png"
alt="uslugi.fr"
class="logo-desktop">
</a>
</div>
<div class="lang-switch">
<?php require __DIR__ . '/../../includes/menu_langue.php'; ?>
</div>
</div>
<!-- Actions header (avatar, messages, menu, aide) -->
<nav class="header-actions d-flex align-items-center gap-2 position-relative">
<!-- RDV (+ badge non lus) -->
<a
id="btn-rdv"
href="<?php echo($_SESSION['role'] ?? '') === 'praticien'
? '/public/praticien/rendez_vous.php'
: '/public/client/mes_rendez_vous.php' ?>"
class="nav-rdv position-relative"
>
<img src="/public/uploads/img/bell-on.png" width="22" alt="Rendez-vous">
<span
id="rdv-badge"
style="position:absolute; top:-6px; right:-8px; background:#e11d48; color:#fff;
font-size:12px; min-width:18px; height:18px; line-height:18px;
text-align:center; border-radius:999px; padding:0 5px; display:none;"
>0</span>
</a>
<!-- Messages (+ badge non lus) -->
<a href="/messages/" class="nav-msg position-relative">
<img src="/public/uploads/img/message.png" width="20" alt="Messages">
<span
id="msg-badge"
style="position:absolute; top:-6px; right:-8px; background:#e11d48; color:#fff;
font-size:12px; min-width:18px; height:18px; line-height:18px;
text-align:center; border-radius:999px; padding:0 5px; display:none;"
>0</span>
</a>
<!-- Menu principal -->
<div class="menu-wrap">
<button type="button" id="btnMenu">
<div class="avatar"><?php echo $initiales ?></div>
</button>
<!-- Menu principal (liste) -->
<ul class="dropdown-menu-custom menu-panel" id="menu">
<?php if ($_SESSION['role'] === 'client'): ?>
<li onclick="location.href='/public/client/mes_rendez_vous.php'">
<?php echo htmlspecialchars($translations['menu_mes_rendez_vous']) ?>
</li>
<li onclick="location.href='/public/client/list_praticien.php'">
<?php echo htmlspecialchars($translations['menu_liste_specialistes']) ?>
</li>
<li onclick="location.href='/public/client/mes_favori.php'">
<?php echo htmlspecialchars($translations['menu_mes_favoris']) ?>
</li>
<?php else: ?>
<li onclick="location.href='/public/praticien/rendez_vous.php'">
<?php echo htmlspecialchars($translations['menu_mes_rendez_vous']) ?>
</li>
<li onclick="location.href='/public/praticien/agenda.php'">
<?php echo htmlspecialchars($translations['menu_mon_agenda']) ?>
</li>
<li onclick="location.href='/public/praticien/disponibilites.php'">
<?php echo htmlspecialchars($translations['menu_mes_disponibilites']) ?>
</li>
<li onclick="location.href='/public/praticien/synchronisation.php'">
<?php echo htmlspecialchars($translations['menu_sync_agendas']) ?>
</li>
<!--<li onclick="location.href='/public/praticien/clients/index.php'">
<?php echo htmlspecialchars($translations['menu_mes_clients'] ?? 'Mes clients') ?>
</li>-->
<?php endif; ?>
<!-- Sous-menu Admin -->
<li class="has-submenu">
<button
type="button"
class="submenu-toggle"
aria-expanded="false"
aria-controls="submenu-admin"
>
<?php echo htmlspecialchars($translations['menu_admin']) ?>
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<ul id="submenu-admin" class="submenu" hidden>
<?php if ($_SESSION['role'] === 'praticien'): ?>
<li onclick="location.href='/public/praticien/resiliation.php'">
<?php echo htmlspecialchars($translations['menu_gerer_abonnement']) ?>
</li>
<li onclick="location.href='/public/praticien/factures.php'">
<?php echo htmlspecialchars($translations['menu_mes_factures']) ?>
</li>
<li onclick="location.href='/public/cabinet/invitations.php'">
<?php echo htmlspecialchars($translations['secretariat_inviter_secretaire']) ?>
</li>
<li onclick="location.href='/public/praticien/clients/index.php'">
<?php echo htmlspecialchars($translations['menu_mes_clients'] ?? 'Mes clients') ?>
</li>
<?php endif; ?>
<li onclick="location.href='/public/<?php echo $_SESSION['role'] ?>/informations.php'">
<?php echo htmlspecialchars($translations['menu_mes_informations']) ?>
</li>
<li onclick="location.href='/public/<?php echo $_SESSION['role'] ?>/compte.php'">
<?php echo htmlspecialchars($translations['menu_infos_connexion']) ?>
</li>
<li onclick="location.href='/supprimer_compte.php'">
<?php echo htmlspecialchars($translations['menu_supprimer_compte']) ?>
</li>
</ul>
</li>
<!-- Déconnexion -->
<li onclick="closeMenu(); openContactAdmin()">
<?php echo htmlspecialchars($translations['contact_admin']) ?>
</li>
<li class="menu-item-form">
<form method="POST" action="/public/deconnexion.php" class="menu-logout-form">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8') ?>">
<button type="submit" class="menu-logout-btn">
<?php echo htmlspecialchars($translations['menu_deconnexion'] ?? 'Déconnexion') ?>
</button>
</form>
</li>
</ul>
</div>
</nav>
</div>
</header>
<!-- ================================================
[SCRIPTS / LOGIQUE CLIENT]
- Badge messages non lus (polling)
- Ouverture/fermeture du menu principal
- Gestion du sous-menu Admin (ARIA)
- Bootstrap JS bundle
================================================ -->
<script>
(function(){
function setBadge(el, n){
if (!el) return;
n = Number(n || 0);
if (n > 0){
el.style.display = 'inline-block';
el.textContent = String(n > 99 ? '99+' : n);
} else {
el.style.display = 'none';
el.textContent = '0';
}
}
async function tickBadges(){
try {
const r = await fetch('/push/badge_count.php', { credentials:'same-origin', cache:'no-store' });
if (!r.ok) return;
const j = await r.json();
if (!j || !j.ok) return;
// UI header
setBadge(document.getElementById('msg-badge'), j.msg_unread);
setBadge(document.getElementById('rdv-badge'), j.rdv_unread);
// Badge PWA (si supporté)
const n = Number(j.app_badge || 0);
if ('setAppBadge' in navigator) {
try {
if (n > 0) await navigator.setAppBadge(n);
else if ('clearAppBadge' in navigator) await navigator.clearAppBadge();
} catch(_) {}
}
} catch(e){}
}
// 1) au chargement
tickBadges();
// 2) refresh (mets 10s au lieu de 5s pour soulager)
setInterval(tickBadges, 10000);
// 3) quand on revient sur l’onglet
document.addEventListener('visibilitychange', () => {
if (!document.hidden) tickBadges();
});
// optionnel : dispo ailleurs
window.refreshBadges = tickBadges;
})();
// -------------------------------------------------
// [MENU PRINCIPAL] — Ouverture / Fermeture
// -------------------------------------------------
function toggleMenu() {
const menu = document.getElementById('menu');
if (!menu) return;
menu.style.display = (menu.style.display === 'block') ? 'none' : 'block';
}
function closeMenu() {
const menu = document.getElementById('menu');
if (menu) menu.style.display = 'none';
closeSubmenus();
}
document.getElementById('btnMenu').addEventListener('click', toggleMenu);
// Ferme le menu en cliquant à l'extérieur
document.addEventListener('click', (e) => {
const menu = document.getElementById('menu');
if (!menu) return;
const toggle = document.getElementById('btnMenu');
if (!toggle.contains(e.target) && !menu.contains(e.target)) {
closeMenu();
}
});
// Ferme avec la touche Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeMenu();
});
// -------------------------------------------------
// [SOUS-MENU ADMIN] — Toggle + ARIA
// -------------------------------------------------
document.addEventListener('click', function(e) {
const btn = e.target.closest('.submenu-toggle');
if (!btn) return;
const submenuId = btn.getAttribute('aria-controls');
const submenu = document.getElementById(submenuId);
if (!submenu) return;
const isOpen = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!isOpen));
submenu.hidden = isOpen;
e.stopPropagation();
});
function closeSubmenus() {
document
.querySelectorAll('.submenu-toggle[aria-expanded="true"]')
.forEach(btn => {
btn.setAttribute('aria-expanded', 'false');
const sub = document.getElementById(btn.getAttribute('aria-controls'));
if (sub) sub.hidden = true;
});
}
</script>
<script>
(async function registerSW(){
if (!('serviceWorker' in navigator)) return;
try {
// IMPORTANT : scope racine pour couvrir /push/, /messages/, /public/...
const reg = await navigator.serviceWorker.register('/sw.js', { scope: '/' });
// optionnel : log utile
// console.log('SW registered', reg.scope);
} catch (e) {
console.error('SW register failed', e);
}
})();
</script>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', async (e) => {
const msg = e.data || {};
if (msg.type === 'SET_BADGE') {
const n = Number(msg.badgeCount ?? 0);
// iOS/Safari: parfois pas supporté => silence
if ('setAppBadge' in navigator) {
try {
if (n > 0) await navigator.setAppBadge(n);
else if ('clearAppBadge' in navigator) await navigator.clearAppBadge();
} catch (_) {}
}
}
});
}
</script>
<script>
(async function pushOnboardingPWA(){
const isPWA =
window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true;
if (!isPWA) return;
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
if (!('Notification' in window)) return;
const VAPID_PUBLIC = "<?php echo htmlspecialchars($pushCfg['vapid_public'] ?? '', ENT_QUOTES) ?>";
if (!VAPID_PUBLIC) return;
const onboardKey = "<?php echo htmlspecialchars($pushOnboardKey, ENT_QUOTES) ?>";
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const raw = atob(base64);
const arr = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
return arr;
}
async function getSub(){
const reg = await navigator.serviceWorker.ready;
return await reg.pushManager.getSubscription();
}
async function syncToServer(sub){
const res = await fetch('/push/subscribe.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
cache: 'no-store',
body: JSON.stringify({
endpoint: sub.endpoint,
keys: sub.toJSON().keys,
userAgent: navigator.userAgent,
deviceLabel: 'PWA'
})
});
if (!res.ok) throw new Error('subscribe.php failed: ' + res.status);
return true;
}
async function subscribeNow(){
const reg = await navigator.serviceWorker.ready;
// 1) permission
const perm = await Notification.requestPermission();
if (perm !== 'granted') {
// on note qu'on a déjà proposé (évite spam)
try { localStorage.setItem(onboardKey, 'done'); } catch(_) {}
return false;
}
// 2) subscribe
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC)
});
// 3) sync DB
await syncToServer(sub);
// 4) mark done
try { localStorage.setItem(onboardKey, 'done'); } catch(_) {}
return true;
}
// --- Step 1: si sub existe déjà => sync DB silencieux ---
try {
const existing = await getSub();
if (existing) {
await syncToServer(existing);
// on marque done pour ne pas reproposer
try { localStorage.setItem(onboardKey, 'done'); } catch(_) {}
return;
}
} catch(e) {
// silencieux (pas bloquant)
}
// --- Step 2: pas de sub => afficher modal une seule fois ---
let alreadyDone = false;
try { alreadyDone = localStorage.getItem(onboardKey) === 'done'; } catch(_) {}
// Si l'utilisateur a déjà refusé au niveau OS => pas de modal (ou tu peux afficher un toast)
if (Notification.permission === 'denied') {
try { localStorage.setItem(onboardKey, 'done'); } catch(_) {}
return;
}
// Si déjà proposé => stop
if (alreadyDone) return;
// Affiche modal (Bootstrap)
const modalEl = document.getElementById('pushOnboardModal');
if (!modalEl || typeof bootstrap === 'undefined') return;
const modal = new bootstrap.Modal(modalEl, { backdrop: 'static' });
const laterBtn = document.getElementById('pushLaterBtn');
const enableBtn = document.getElementById('pushEnableBtn');
if (laterBtn) {
laterBtn.addEventListener('click', () => {
try { localStorage.setItem(onboardKey, 'done'); } catch(_) {}
modal.hide();
}, { once:true });
}
if (enableBtn) {
enableBtn.addEventListener('click', async () => {
enableBtn.disabled = true;
try {
await subscribeNow();
} catch(e) {
// si échec (réseau / serveur), on ne spam pas l'utilisateur
try { localStorage.setItem(onboardKey, 'done'); } catch(_) {}
} finally {
modal.hide();
}
}, { once:true });
}
modal.show();
})();
</script>
<script>
window.applyBadges = function (data) {
const msgUnread = Number(data?.msg_unread || 0);
const rdvUnread = Number(data?.rdv_unread || 0);
const appBadge = Number(data?.app_badge || (msgUnread + rdvUnread));
const msgSpan = document.getElementById('msg-badge');
const rdvSpan = document.getElementById('rdv-badge');
if (msgSpan){
msgSpan.style.display = msgUnread > 0 ? 'inline-block' : 'none';
msgSpan.textContent = msgUnread > 99 ? '99+' : String(msgUnread);
}
if (rdvSpan){
rdvSpan.style.display = rdvUnread > 0 ? 'inline-block' : 'none';
rdvSpan.textContent = rdvUnread > 99 ? '99+' : String(rdvUnread);
}
if ('setAppBadge' in navigator) {
try {
if (appBadge > 0) navigator.setAppBadge(appBadge);
else navigator.clearAppBadge?.();
} catch(_) {}
}
};
</script>