Init Git Prod Mars 2026
This commit is contained in:
commit
d81efe93b1
362 files changed
+86627
No files matched your search
@@ -0,0 +1,12 @@
|
||||
# Interdit l'accès HTTP aux fichiers sensibles
|
||||
<FilesMatch "\.(php|inc|phtml|phar|env|ini|json|ya?ml|sql|bak|zip)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Interdit l'accès aux fichiers cachés (.env, .git, etc.)
|
||||
<FilesMatch "^\.">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Pas de listing de répertoire
|
||||
Options -Indexes
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
// Si quelqu'un essaie d'appeler directement /includes/db.php dans le navigateur, on coupe
|
||||
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME'])) {
|
||||
http_response_code(403);
|
||||
exit('
|
||||
<!doctype html>
|
||||
<html lang="ru"><head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>403 — Accès refusé</title>
|
||||
<style>
|
||||
html,body{height:100%;margin:0}
|
||||
body{display:flex;align-items:center;justify-content:center;background:#f7f7f9;font-family:system-ui,Segoe UI,Roboto,Arial}
|
||||
.card{max-width:560px;width:92%;background:#fff;border:1px solid #e5e7eb;border-radius:14px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:28px;text-align:center}
|
||||
.emoji{font-size:48px;margin-bottom:8px}
|
||||
h1{margin:6px 0 10px;font-size:22px}
|
||||
p{margin:0 0 18px;color:#6b7280}
|
||||
.row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
|
||||
.btn{padding:10px 14px;border-radius:999px;text-decoration:none;display:inline-block}
|
||||
.btn-primary{background:#E03C3F;color:#fff}
|
||||
.btn-outline{border:1px solid #d1d5db;color:#fff;background:#38378B}
|
||||
</style>
|
||||
</head><body>
|
||||
<div class="card">
|
||||
<div class="emoji"></div>
|
||||
<h1>Accès refusé</h1>
|
||||
<p>Vous n’êtes pas autorisé à consulter cette page.</p>
|
||||
<div class="row">
|
||||
<a class="btn btn-primary" href="/connexion.php">Se connecter</a>
|
||||
<a class="btn btn-outline" href="/">Retour à l’accueil</a>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>');
|
||||
}
|
||||
$host = getenv('APP_DB_HOST') ?: 'localhost';
|
||||
$dbname = getenv('APP_DB_NAME') ?: '';
|
||||
$user = getenv('APP_DB_USER') ?: '';
|
||||
$password = getenv('APP_DB_PASS') ?: '';
|
||||
$charset = getenv('APP_DB_CHARSET') ?: 'utf8mb4';
|
||||
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
"mysql:host=$host;dbname=$dbname;charset=$charset",
|
||||
$user,
|
||||
$password,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
error_log('DB connect error: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
exit('Erreur de connexion à la base de données public.');
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,616 @@
|
||||
<?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><?= htmlspecialchars($PAGE_TITLE, ENT_QUOTES, 'UTF-8') ?></title>
|
||||
<?= $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="<?= asset('/includes/site_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?= asset('/includes/custom_style.css') ?>"> <!-- ta home -->
|
||||
<link rel="stylesheet" href="<?= asset('/includes/mobil_style.css') ?>">
|
||||
</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">
|
||||
<?= 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="<?= htmlspecialchars($t('push_onboard_close_aria', 'Fermer'), ENT_QUOTES, 'UTF-8') ?>">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<?= 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">
|
||||
<?= htmlspecialchars($t('push_onboard_later', 'Plus tard'), ENT_QUOTES, 'UTF-8') ?>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-primary"
|
||||
id="pushEnableBtn">
|
||||
<?= 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;">
|
||||
<?= 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="<?= 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="<?= 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="<?= ($_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"><?= $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'">
|
||||
<?= htmlspecialchars($translations['menu_mes_rendez_vous']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/client/list_praticien.php'">
|
||||
<?= htmlspecialchars($translations['menu_liste_specialistes']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/client/mes_favori.php'">
|
||||
<?= htmlspecialchars($translations['menu_mes_favoris']) ?>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li onclick="location.href='/public/praticien/rendez_vous.php'">
|
||||
<?= htmlspecialchars($translations['menu_mes_rendez_vous']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/praticien/agenda.php'">
|
||||
<?= htmlspecialchars($translations['menu_mon_agenda']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/praticien/disponibilites.php'">
|
||||
<?= htmlspecialchars($translations['menu_mes_disponibilites']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/praticien/synchronisation.php'">
|
||||
<?= htmlspecialchars($translations['menu_sync_agendas']) ?>
|
||||
</li>
|
||||
<!--<li onclick="location.href='/public/praticien/clients/index.php'">
|
||||
<?= 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"
|
||||
>
|
||||
<?= 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'">
|
||||
<?= htmlspecialchars($translations['menu_gerer_abonnement']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/praticien/factures.php'">
|
||||
<?= htmlspecialchars($translations['menu_mes_factures']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/cabinet/invitations.php'">
|
||||
<?= htmlspecialchars($translations['secretariat_inviter_secretaire']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/praticien/clients/index.php'">
|
||||
<?= htmlspecialchars($translations['menu_mes_clients'] ?? 'Mes clients') ?>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<li onclick="location.href='/public/<?= $_SESSION['role'] ?>/informations.php'">
|
||||
<?= htmlspecialchars($translations['menu_mes_informations']) ?>
|
||||
</li>
|
||||
|
||||
<li onclick="location.href='/public/<?= $_SESSION['role'] ?>/compte.php'">
|
||||
<?= htmlspecialchars($translations['menu_infos_connexion']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/supprimer_compte.php'">
|
||||
<?= htmlspecialchars($translations['menu_supprimer_compte']) ?>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
<!-- Déconnexion -->
|
||||
<li onclick="closeMenu(); openContactAdmin()">
|
||||
<?= htmlspecialchars($translations['contact_admin']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/deconnexion.php'">
|
||||
<?= htmlspecialchars($translations['menu_deconnexion']) ?>
|
||||
</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 = "<?= htmlspecialchars($pushCfg['vapid_public'] ?? '', ENT_QUOTES) ?>";
|
||||
if (!VAPID_PUBLIC) return;
|
||||
|
||||
const onboardKey = "<?= 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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
// ⚠️ NE PAS inclure contact_admin.php ici (risque d'exécution à l'inclusion)
|
||||
require_once __DIR__ . '/../contact_admin.php';
|
||||
|
||||
|
||||
$sec_uid = (int)($_SESSION['compte_id'] ?? $_SESSION['user_id'] ?? 0);
|
||||
|
||||
// =====================================================
|
||||
// [CONTEXT UTILISATEUR] — Initiales avatar secrétaire
|
||||
// =====================================================
|
||||
$prenomUser = trim((string)($_SESSION['user_prenom'] ?? $_SESSION['prenom'] ?? ''));
|
||||
$nomUser = trim((string)($_SESSION['user_nom'] ?? $_SESSION['nom'] ?? ''));
|
||||
|
||||
if ($prenomUser === '' && $nomUser === '') {
|
||||
// petit fallback
|
||||
$prenomUser = 'Secr';
|
||||
$nomUser = 'Etaire';
|
||||
}
|
||||
$initiales = mb_strtoupper(
|
||||
mb_substr($prenomUser, 0, 1, 'UTF-8') .
|
||||
mb_substr($nomUser, 0, 1, 'UTF-8'),
|
||||
'UTF-8'
|
||||
);
|
||||
|
||||
// =====================================================
|
||||
// [PRATICIEN COURANT] — affichage nom/prénom dans le header
|
||||
// - on prend ?praticien_id si présent, sinon une valeur de session si dispo
|
||||
// =====================================================
|
||||
$currentPraticienId = null;
|
||||
if (isset($_GET['praticien_id'])) {
|
||||
$currentPraticienId = (int)$_GET['praticien_id'];
|
||||
$_SESSION['current_praticien_id'] = $currentPraticienId; // mémorise pour les autres pages
|
||||
} elseif (!empty($_SESSION['current_praticien_id'])) {
|
||||
$currentPraticienId = (int)$_SESSION['current_praticien_id'];
|
||||
}
|
||||
|
||||
$currentPraticienName = '';
|
||||
if ($currentPraticienId) {
|
||||
$stmt = $pdo->prepare("SELECT id, prenom, nom FROM praticien WHERE id = ? LIMIT 1");
|
||||
$stmt->execute([$currentPraticienId]);
|
||||
if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$p = trim((string)($row['prenom'] ?? ''));
|
||||
$n = trim((string)($row['nom'] ?? ''));
|
||||
$pid = trim((string)($row['id'] ?? ''));
|
||||
$currentPraticienName = trim($p.' '.$n);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// [Liste Praticien avec autorisations]
|
||||
// =====================================================
|
||||
$pid = (int)$currentPraticienId;
|
||||
$permRow = null;
|
||||
|
||||
if ($pid && $sec_uid) {
|
||||
$q = $pdo->prepare("
|
||||
SELECT sp.can_manage_rdv, sp.can_edit_slots
|
||||
FROM secretaire_praticien sp
|
||||
WHERE sp.secretaire_user_id = :u
|
||||
AND sp.praticien_id = :pid
|
||||
AND sp.is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$q->execute(['u' => $sec_uid, 'pid' => $pid]);
|
||||
$permRow = $q->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
}
|
||||
|
||||
|
||||
// =====================================================
|
||||
// [RETOUR / REFERER SÉCURISÉ]
|
||||
// =====================================================
|
||||
$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 ?: '/public/cabinet/dashboard.php';
|
||||
|
||||
// =====================================================
|
||||
// [DETECTION MOBILE]
|
||||
// =====================================================
|
||||
$is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGENT'] ?? '');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title><?= htmlspecialchars($PAGE_TITLE ?? 'Cabinet', ENT_QUOTES, 'UTF-8') ?></title>
|
||||
<?= $HEAD_EXTRA ?? '' ?>
|
||||
|
||||
<!-- Montserrat -->
|
||||
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Styles (chemins corrigés) -->
|
||||
<link rel="stylesheet" href="<?= asset('/includes/site_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?= asset('/includes/custom_style.css') ?>"> <!-- ta home -->
|
||||
|
||||
<link rel="stylesheet" href="<?= asset('/includes/admin_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?= asset('/includes/mobil_style.css') ?>">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="site-header py-2 border-bottom">
|
||||
<div class="container d-flex justify-content-between align-items-center">
|
||||
|
||||
<!-- Bouton retour + Logo -->
|
||||
<div class="header-left">
|
||||
<div class="brand">
|
||||
<a
|
||||
href="<?= 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="<?= 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>
|
||||
|
||||
<a href="/public/cabinet/dashboard.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 -->
|
||||
<nav class="header-actions d-flex align-items-center gap-3 position-relative">
|
||||
|
||||
<!-- ⚡ A LA PLACE DE L’ICÔNE MESSAGES : le praticien courant -->
|
||||
<div class="text-nowrap" style="font-weight:600;">
|
||||
<?php if ($currentPraticienName): ?>
|
||||
<?= htmlspecialchars($currentPraticienName, ENT_QUOTES, 'UTF-8') ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted"><?= htmlspecialchars($translations['aucun_praticien_selectionne'] ?? 'Praticien non sélectionné', ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Menu utilisateur (avatar) -->
|
||||
<div class="menu-wrap">
|
||||
<button type="button" id="btnMenu" class="btn p-0 border-0 bg-transparent">
|
||||
<div class="avatar d-flex align-items-center justify-content-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5m0 2c-5.33 0-8 2.67-8 4v2h16v-2c0-1.33-2.67-4-8-4"/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu-custom menu-panel" id="menu" style="display:none; min-width: 220px;">
|
||||
<?php if (($_SESSION['role'] ?? '') === 'secretaire'): ?>
|
||||
<li onclick="location.href='/public/cabinet/dashboard.php'">
|
||||
<?= htmlspecialchars($translations['home'] ?? 'Accueil') ?>
|
||||
</li>
|
||||
|
||||
<?php if ($pid && $permRow): ?>
|
||||
<?php if (!empty($permRow['can_manage_rdv'])): ?>
|
||||
<li onclick="location.href='/public/cabinet/agenda.php?praticien_id=<?= $pid ?>'">
|
||||
<?= htmlspecialchars($translations['sec_btn_agenda'] ?? 'Agenda') ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/cabinet/rendez_vous_du_jour.php?praticien_id=<?= $pid ?>'">
|
||||
<?= htmlspecialchars($translations['sec_btn_rdv'] ?? 'Rendez-vous') ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/cabinet/rendez_vous.php?praticien_id=<?= $pid ?>'">
|
||||
<?= htmlspecialchars($translations['sec_btn_rdv_jour'] ?? 'Les rendez-vous') ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/cabinet/reserver_rdv.php?praticien_id=<?= $pid ?>'">
|
||||
<?= htmlspecialchars($translations['sec_btn_prendre'] ?? 'Prendre un RDV') ?>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($permRow['can_edit_slots'])): ?>
|
||||
<li onclick="location.href='/public/cabinet/disponibilites.php?praticien_id=<?= $pid ?>'">
|
||||
<?= htmlspecialchars($translations['sec_btn_dispos'] ?? 'Disponibilités') ?>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<li onclick="closeMenu(); openContactAdmin()">
|
||||
<?= htmlspecialchars($translations['contact_admin'] ?? 'Contact Admin') ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/deconnexion.php'">
|
||||
<?= htmlspecialchars($translations['menu_deconnexion'] ?? 'Déconnexion') ?>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script>
|
||||
// --- Menu principal
|
||||
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';
|
||||
}
|
||||
document.getElementById('btnMenu')?.addEventListener('click', function(e){
|
||||
e.stopPropagation();
|
||||
toggleMenu();
|
||||
});
|
||||
document.addEventListener('click', function(e){
|
||||
const menu = document.getElementById('menu');
|
||||
const btn = document.getElementById('btnMenu');
|
||||
if (!menu || !btn) return;
|
||||
if (!btn.contains(e.target) && !menu.contains(e.target)) closeMenu();
|
||||
});
|
||||
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeMenu(); });
|
||||
</script>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/securite.php';
|
||||
require_once __DIR__ . '/db.php';
|
||||
require_once __DIR__ . '/verifier_abonnement.php';
|
||||
require_once __DIR__ . '/../contact_admin.php'; // ← depuis /public/includes/header.php
|
||||
require_once __DIR__ . '/../includes/lang.php';
|
||||
require_once __DIR__ . '/../../includes/pretty_urls.php';
|
||||
require_once __DIR__ . '/../../includes/mailer.php'; // uslugi_mail()
|
||||
|
||||
if (!isset($_SESSION)) session_start();
|
||||
|
||||
// Charger les données client ou praticien
|
||||
$nom = $prenom = '';
|
||||
if ($_SESSION['role'] === 'client') {
|
||||
$stmt = $pdo->prepare("SELECT nom, prenom FROM client WHERE id = ?");
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT nom, prenom FROM praticien WHERE id = ?");
|
||||
}
|
||||
$stmt->execute([$_SESSION['reference_id']]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user) {
|
||||
$nom = htmlspecialchars($user['nom']);
|
||||
$prenom = htmlspecialchars($user['prenom']);
|
||||
}
|
||||
|
||||
//Initiales pour Avatar
|
||||
|
||||
$initiales = mb_strtoupper(
|
||||
mb_substr((string)$prenom, 0, 1, 'UTF-8') .
|
||||
mb_substr((string)$nom, 0, 1, 'UTF-8'),
|
||||
'UTF-8'
|
||||
);
|
||||
//Function pour gerer hystorique de la navigation
|
||||
//*************************************************
|
||||
|
||||
$ref = $_SERVER['HTTP_REFERER'] ?? '';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? '';
|
||||
$safeRef = '';
|
||||
if ($ref) {
|
||||
$u = parse_url($ref);
|
||||
if (!empty($u['host']) && $u['host'] === $host) {
|
||||
$safeRef = $ref; // referer même origine uniquement
|
||||
}
|
||||
}
|
||||
// Mets ici ta route par défaut si pas d'historique / referer externe
|
||||
$fallbackBack = $safeRef ?: '/';
|
||||
//********************FIN********************************
|
||||
?>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
|
||||
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../../includes/site_style.css">
|
||||
<link rel="stylesheet" href="../../includes/mobil_style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<div class="brand">
|
||||
|
||||
<!--Bouton hystory qui fait retour en arriere-->
|
||||
<a href="<?= 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="<?= htmlspecialchars($translations['retour'] ?? 'Retour', ENT_QUOTES, 'UTF-8') ?>">
|
||||
<!-- inline SVG, compatible partout -->
|
||||
<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>
|
||||
<!-- *************************FIN*************************-->
|
||||
|
||||
<a href="https://uslugi.fr/index.php"><img src="/public/uploads/img/logo.png" width="130"></a>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if (isset($alerte_abonnement) && $alerte_abonnement): ?>
|
||||
<div class="alert alert-warning m-0 py-1 px-2 rounded text-dark" role="alert" style="background:#fff3cd; border: 1px solid #ffeeba; font-size: 14px;">
|
||||
<?= htmlspecialchars($alerte_abonnement) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<nav class="header-actions">
|
||||
<div class="avatar"><?= $initiales ?></div>
|
||||
|
||||
<!--**************************Bouton message conv*************************************************-->
|
||||
<a href="/messages/" class="nav-msg" style="position:relative; display:inline-block; margin-left:12px;">
|
||||
<span aria-hidden="true"><img src="/public/uploads/img/message.png" width="20px"></span>
|
||||
<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>
|
||||
<script>
|
||||
(function(){
|
||||
async function tick(){
|
||||
try{
|
||||
const r = await fetch('/messages/api/unread_count.php', { credentials:'same-origin' });
|
||||
if(!r.ok) throw 0;
|
||||
const j = await r.json();
|
||||
const b = document.getElementById('msg-badge');
|
||||
const n = Number(j.total||0);
|
||||
if(n>0){ b.style.display='inline-block'; b.textContent = String(n); }
|
||||
else { b.style.display='none'; }
|
||||
}catch(e){}
|
||||
}
|
||||
tick();
|
||||
setInterval(tick, 5000);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!--*************************************FIN BOUTON**********************************************
|
||||
KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK-->
|
||||
|
||||
|
||||
<div class="lang-switch">
|
||||
<?php require_once __DIR__ .'/../../includes/menu_langue.php'; ?>
|
||||
</div>
|
||||
<div class="menu-wrap">
|
||||
<button
|
||||
type="button"
|
||||
id="btnMenu"
|
||||
class="btn btn-light btn-circle me-2" style="border-radius: 5px; box-shadow: 0 1px 2px rgba(0,0,0,.08);"
|
||||
aria-label="<?= htmlspecialchars($translations['menu'] ?? 'Menu', ENT_QUOTES, 'UTF-8') ?>"
|
||||
onclick="toggleMenu()">
|
||||
<!-- icône hamburger 20x20, même style que la flèche -->
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true">
|
||||
<path d="M4 6h16M4 12h16M4 18h16"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
|
||||
<!-- Menu -->
|
||||
<ul class="dropdown-menu-custom" id="menu">
|
||||
<li onclick="location.href='/public/<?= $_SESSION['role'] ?>/informations.php'">
|
||||
<?= htmlspecialchars($translations['menu_mes_informations']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/<?= $_SESSION['role'] ?>/compte.php'">
|
||||
<?= htmlspecialchars($translations['menu_infos_connexion']) ?>
|
||||
</li>
|
||||
|
||||
<?php if ($_SESSION['role'] === 'client'): ?>
|
||||
<li onclick="location.href='/public/client/list_praticien.php'">
|
||||
<?= htmlspecialchars($translations['menu_liste_specialistes']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/client/mes_favori.php'">
|
||||
<?= htmlspecialchars($translations['menu_mes_favoris']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/client/mes_rendez_vous.php'">
|
||||
<?= htmlspecialchars($translations['menu_mes_rendez_vous']) ?>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li onclick="location.href='/public/praticien/disponibilites.php'">
|
||||
<?= htmlspecialchars($translations['menu_mes_disponibilites']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/praticien/rendez_vous.php'">
|
||||
<?= htmlspecialchars($translations['menu_mes_rendez_vous']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/praticien/agenda.php'">
|
||||
<?= htmlspecialchars($translations['menu_mon_agenda']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/praticien/synchronisation.php'">
|
||||
<?= htmlspecialchars($translations['menu_sync_agendas']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/praticien/resiliation.php'">
|
||||
<?= htmlspecialchars($translations['menu_gerer_abonnement']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/praticien/factures.php'">
|
||||
<?= htmlspecialchars($translations['menu_mes_factures']) ?>
|
||||
</li>
|
||||
|
||||
<?php endif; ?>
|
||||
<li onclick="closeMenu(); openContactAdmin()" class="text-primary">
|
||||
<?= htmlspecialchars($translations['contact_admin']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/deconnexion.php'" class="text-danger">
|
||||
<?= htmlspecialchars($translations['menu_deconnexion']) ?>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- JS -->
|
||||
<script>
|
||||
function toggleMenu() {
|
||||
const menu = document.getElementById('menu');
|
||||
if (!menu) return;
|
||||
menu.style.display = (menu.style.display === 'block') ? 'none' : 'block';
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
const toggle = document.getElementById('btnMenu');
|
||||
const menu = document.getElementById('menu');
|
||||
if (!menu) return;
|
||||
const clickOnToggle = toggle ? toggle.contains(e.target) : false;
|
||||
const clickInMenu = menu.contains(e.target);
|
||||
if (!clickOnToggle && !clickInMenu) {
|
||||
menu.style.display = 'none';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// --- déjà présent ---
|
||||
// function toggleMenu() { ... }
|
||||
// document.addEventListener('click', ...);
|
||||
|
||||
// 1) Petite utilitaire pour fermer le menu
|
||||
function closeMenu() {
|
||||
const menu = document.getElementById('menu');
|
||||
if (menu) menu.style.display = 'none';
|
||||
// (optionnel) retire un flag si tu l'ajoutes ailleurs
|
||||
document.body.classList.remove('menu-open');
|
||||
}
|
||||
|
||||
// 2) Si ta popup est ouverte par une fonction JS (ex: openContactAdmin)
|
||||
// on injecte la fermeture du menu AVANT l’ouverture
|
||||
// -> Si openContactAdmin existe, on la “wrap”
|
||||
if (typeof window.openContactAdmin === 'function') {
|
||||
const _origOpenContactAdmin = window.openContactAdmin;
|
||||
window.openContactAdmin = function(...args){
|
||||
closeMenu();
|
||||
return _origOpenContactAdmin.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
// 3) Bonus : touche Échap ferme le menu
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeMenu();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
//init_admin.php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/init_auth.php';
|
||||
init_auth_page(['admin']);
|
||||
|
||||
// ensuite, renfort strict admin (optionnel mais recommandé)
|
||||
require_login_or_redirect(['admin'], [
|
||||
'pdo' => $GLOBALS['pdo'] ?? null,
|
||||
'recheck_db_role' => true, // recheck DB comme secure_admin.php si besoin
|
||||
'admin_strict_reference' => 88888888 // ton super-admin
|
||||
]);
|
||||
|
||||
// CSRF ready
|
||||
ensure_csrf_token();
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
//init_auth.php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../../includes/bootstrap_global.php';
|
||||
require_once __DIR__ . '/securite.php';
|
||||
|
||||
function init_auth_page(?array $roles = null): void
|
||||
{
|
||||
require_login_or_redirect($roles);
|
||||
|
||||
// Push config (si utilisé dans header site)
|
||||
$pushCfgPath = __DIR__ . '/../../push/config.php'; // /public/push/config.php
|
||||
$GLOBALS['pushCfg'] = is_file($pushCfgPath) ? require $pushCfgPath : [];
|
||||
|
||||
// Abonnement (si tu l’utilises dans le header site praticien)
|
||||
$abonnementPath = __DIR__ . '/verifier_abonnement.php';
|
||||
if (is_file($abonnementPath)) {
|
||||
require $abonnementPath;
|
||||
$GLOBALS['alerte_abonnement'] = $alerte_abonnement ?? null;
|
||||
} else {
|
||||
$GLOBALS['alerte_abonnement'] = null;
|
||||
}
|
||||
|
||||
// Avatars
|
||||
$GLOBALS['initiales'] = 'U';
|
||||
$GLOBALS['avatar_svg'] = null;
|
||||
|
||||
$role = strtolower((string)($_SESSION['role'] ?? ''));
|
||||
|
||||
// Admin / Secrétaire => SVG
|
||||
if ($role === 'admin') {
|
||||
$GLOBALS['avatar_svg'] = '/public/uploads/img/avatar_admin.svg';
|
||||
return;
|
||||
}
|
||||
if ($role === 'secretaire') {
|
||||
$GLOBALS['avatar_svg'] = '/public/uploads/img/avatar_secretaire.svg';
|
||||
return;
|
||||
}
|
||||
|
||||
// Client / Praticien => initiales depuis tables
|
||||
$refId = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$pdo = $GLOBALS['pdo'] ?? null;
|
||||
if (!($pdo instanceof PDO) || $refId <= 0) return;
|
||||
|
||||
if ($role === 'client') {
|
||||
$stmt = $pdo->prepare("SELECT nom, prenom FROM client WHERE id = ?");
|
||||
} elseif ($role === 'praticien') {
|
||||
$stmt = $pdo->prepare("SELECT nom, prenom FROM praticien WHERE id = ?");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt->execute([$refId]);
|
||||
$u = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
$nom = trim((string)($u['nom'] ?? ''));
|
||||
$prenom = trim((string)($u['prenom'] ?? ''));
|
||||
|
||||
$ini = mb_strtoupper(
|
||||
mb_substr($prenom, 0, 1, 'UTF-8') . mb_substr($nom, 0, 1, 'UTF-8'),
|
||||
'UTF-8'
|
||||
);
|
||||
$GLOBALS['initiales'] = $ini !== '' ? $ini : 'U';
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/init_auth.php';
|
||||
init_auth_page(['client']);
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/init_auth.php';
|
||||
init_auth_page(['client', 'praticien']);
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/init_auth.php';
|
||||
init_auth_page(['praticien']);
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/init_auth.php';
|
||||
init_auth_page(['secretaire']);
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
// Définir la langue par défaut
|
||||
if (!isset($_SESSION['lang'])) {
|
||||
$_SESSION['lang'] = 'ru'; // par défaut russe
|
||||
}
|
||||
|
||||
// Charger le fichier de langue correspondant
|
||||
$lang = $_SESSION['lang'];
|
||||
$lang_file = __DIR__ . "/../lang/{$lang}.php";
|
||||
|
||||
// Si le fichier n'existe pas, on repasse en russe
|
||||
if (!file_exists($lang_file)) {
|
||||
$lang_file = __DIR__ . "/../lang/ru.php";
|
||||
}
|
||||
|
||||
// Charger les traductions
|
||||
$translations = require $lang_file;
|
||||
|
||||
/**
|
||||
* Asset helper: ajoute une version ?v= basée sur filemtime()
|
||||
* Usage: asset('/includes/custom_style.css')
|
||||
*/
|
||||
if (!function_exists('asset')) {
|
||||
function asset(string $webPath): string
|
||||
{
|
||||
// Normalise
|
||||
if ($webPath === '') return $webPath;
|
||||
if ($webPath[0] !== '/') $webPath = '/' . $webPath;
|
||||
|
||||
$docRoot = rtrim($_SERVER['DOCUMENT_ROOT'] ?? '', '/');
|
||||
$fsPath = $docRoot . $webPath;
|
||||
|
||||
$v = (is_file($fsPath) ? filemtime($fsPath) : time()); // fallback si non trouvé
|
||||
return $webPath . '?v=' . $v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
(function(){
|
||||
function initMobileCards(tbl){
|
||||
if (!tbl) return;
|
||||
const heads = Array.from(tbl.querySelectorAll('thead th')).map(th => th.textContent.trim());
|
||||
tbl.querySelectorAll('tbody tr').forEach(tr => {
|
||||
Array.from(tr.children).forEach((td, i) => {
|
||||
if (!td.hasAttribute('data-label')) {
|
||||
td.setAttribute('data-label', heads[i] || '');
|
||||
}
|
||||
});
|
||||
});
|
||||
// ✅ une seule source de vérité
|
||||
document.documentElement.classList.add('mc-ready');
|
||||
}
|
||||
|
||||
function ready(fn){
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
|
||||
else fn();
|
||||
}
|
||||
|
||||
ready(function(){
|
||||
const tbl = document.getElementById('table_rdv') || document.querySelector('table.mobile-cards');
|
||||
initMobileCards(tbl);
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// Interdit l’accès direct au fichier (pas de HTML ici)
|
||||
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME'])) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
/** Est-ce que l'utilisateur est connecté ? */
|
||||
function is_logged_in(): bool {
|
||||
return isset($_SESSION['compte_id'], $_SESSION['reference_id'])
|
||||
&& $_SESSION['compte_id'] !== ''
|
||||
&& $_SESSION['reference_id'] !== '';
|
||||
}
|
||||
|
||||
/** Timeout inactivité (2h) */
|
||||
function enforce_session_timeout(int $maxIdleSeconds = 7200): void {
|
||||
$now = time();
|
||||
$last = (int)($_SESSION['last_activity'] ?? 0);
|
||||
|
||||
if ($last > 0 && ($now - $last) > $maxIdleSeconds) {
|
||||
logout_and_redirect_to_login($_SERVER['REQUEST_URI'] ?? '/');
|
||||
}
|
||||
$_SESSION['last_activity'] = $now;
|
||||
}
|
||||
|
||||
/** Anti open-redirect : n’autorise que les chemins internes commençant par '/' */
|
||||
function next_safe(?string $u): ?string {
|
||||
if (!$u) return null;
|
||||
if (!preg_match('#^/[^\\r\\n]*$#', $u)) return null;
|
||||
if (strpos($u, '//') === 0) return null;
|
||||
return $u;
|
||||
}
|
||||
|
||||
function redirect_forbidden(?string $requestUri = null): void {
|
||||
$u = $requestUri ?: ($_SERVER['REQUEST_URI'] ?? '/');
|
||||
$u = next_safe($u) ?? '/';
|
||||
header('Location: /403.php?next=' . rawurlencode($u), true, 303);
|
||||
exit;
|
||||
}
|
||||
|
||||
function redirect_to_login_with_next(?string $requestUri = null): void {
|
||||
$u = $requestUri ?: ($_SERVER['REQUEST_URI'] ?? '/');
|
||||
$u = next_safe($u) ?? '/';
|
||||
$_SESSION['redirect_to'] = $u;
|
||||
header('Location: /connexion.php?next=' . rawurlencode($u), true, 303);
|
||||
exit;
|
||||
}
|
||||
|
||||
function logout_and_redirect_to_login(?string $requestUri = null): void {
|
||||
$_SESSION = [];
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(
|
||||
session_name(),
|
||||
'',
|
||||
time() - 42000,
|
||||
$params['path'] ?? '/',
|
||||
$params['domain'] ?? '',
|
||||
(bool)($params['secure'] ?? false),
|
||||
(bool)($params['httponly'] ?? true)
|
||||
);
|
||||
}
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_destroy();
|
||||
}
|
||||
redirect_to_login_with_next($requestUri ?? ($_SERVER['REQUEST_URI'] ?? '/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔒 Re-check rôle depuis DB (optionnel) + hydrate $_SESSION['role'] si absent.
|
||||
* Retourne le rôle (string) ou '' si introuvable.
|
||||
*/
|
||||
function refresh_role_from_db(PDO $pdo): string {
|
||||
$compteId = (int)($_SESSION['compte_id'] ?? 0);
|
||||
if ($compteId <= 0) return '';
|
||||
|
||||
$stmt = $pdo->prepare("SELECT role FROM compte WHERE id=? LIMIT 1");
|
||||
$stmt->execute([$compteId]);
|
||||
$role = (string)($stmt->fetchColumn() ?: '');
|
||||
if ($role !== '') {
|
||||
$_SESSION['role'] = $role;
|
||||
}
|
||||
return strtolower($role);
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ À appeler en haut des pages protégées.
|
||||
* - $allowedRoles = null => tous rôles
|
||||
* - options:
|
||||
* - 'pdo' => PDO (pour re-check DB si demandé)
|
||||
* - 'recheck_db_role' => true/false
|
||||
* - 'admin_strict_reference' => int|null (ex: 88888888)
|
||||
*/
|
||||
function require_login_or_redirect(?array $allowedRoles = null, array $options = []): void {
|
||||
if (!is_logged_in()) {
|
||||
redirect_to_login_with_next($_SERVER['REQUEST_URI'] ?? '/');
|
||||
}
|
||||
|
||||
enforce_session_timeout(7200);
|
||||
|
||||
$pdo = $options['pdo'] ?? ($GLOBALS['pdo'] ?? null);
|
||||
$recheck = (bool)($options['recheck_db_role'] ?? false);
|
||||
|
||||
// Rôle: session d'abord, DB si demandé (ou si role manquant)
|
||||
$userRole = strtolower((string)($_SESSION['role'] ?? ''));
|
||||
if (($userRole === '' || $recheck) && ($pdo instanceof PDO)) {
|
||||
$userRole = refresh_role_from_db($pdo);
|
||||
}
|
||||
|
||||
if ($allowedRoles) {
|
||||
$allowed = array_map(static fn($r) => strtolower((string)$r), $allowedRoles);
|
||||
|
||||
if (!in_array($userRole, $allowed, true)) {
|
||||
redirect_forbidden($_SERVER['REQUEST_URI'] ?? '/');
|
||||
}
|
||||
|
||||
// 🔥 Renfort admin optionnel : role + reference_id
|
||||
if (in_array('admin', $allowed, true)) {
|
||||
$strictRef = $options['admin_strict_reference'] ?? null;
|
||||
if ($strictRef !== null) {
|
||||
$refId = (int)($_SESSION['reference_id'] ?? 0);
|
||||
if ($refId !== (int)$strictRef) {
|
||||
redirect_forbidden($_SERVER['REQUEST_URI'] ?? '/');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Admin helpers (CSRF + logs)
|
||||
========================= */
|
||||
|
||||
/** Token CSRF session */
|
||||
function ensure_csrf_token(): string {
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return (string)$_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
/** Exige POST + CSRF (supporte aussi header X-CSRF-Token si tu veux fetch/AJAX) */
|
||||
function require_post_csrf(): void {
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
http_response_code(405);
|
||||
exit('Méthode non autorisée');
|
||||
}
|
||||
|
||||
$token = $_POST['csrf_token'] ?? '';
|
||||
if ($token === '' && !empty($_SERVER['HTTP_X_CSRF_TOKEN'])) {
|
||||
$token = (string)$_SERVER['HTTP_X_CSRF_TOKEN'];
|
||||
}
|
||||
|
||||
if ($token === '' || !hash_equals((string)($_SESSION['csrf_token'] ?? ''), (string)$token)) {
|
||||
http_response_code(419);
|
||||
exit('CSRF invalide');
|
||||
}
|
||||
}
|
||||
|
||||
/** Log admin minimal */
|
||||
function log_admin_action(PDO $pdo, int $userId, string $action, string $details = ''): void {
|
||||
// limite simple pour éviter d'exploser la table
|
||||
if (strlen($details) > 2000) $details = substr($details, 0, 2000);
|
||||
|
||||
$pdo->prepare(
|
||||
"INSERT INTO admin_actions_log (user_id, action, details, ip) VALUES (?,?,?,?)"
|
||||
)->execute([
|
||||
$userId,
|
||||
$action,
|
||||
$details,
|
||||
$_SERVER['REMOTE_ADDR'] ?? ''
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* /public/includes/verifier_abonnement.php
|
||||
*
|
||||
* Rôle: praticien uniquement
|
||||
* - Définit $alerte_abonnement (string|null) pour affichage dans header
|
||||
* - Peut rediriger si abonnement expiré (selon règles)
|
||||
*
|
||||
* Pré-requis: session démarrée, $pdo dispo (via bootstrap_global.php / init_auth.php)
|
||||
*/
|
||||
|
||||
$alerte_abonnement = null;
|
||||
|
||||
// --- Guard: uniquement praticien
|
||||
$role = (string)($_SESSION['role'] ?? '');
|
||||
if ($role !== 'praticien') {
|
||||
return;
|
||||
}
|
||||
|
||||
// --- PDO
|
||||
$pdo = $GLOBALS['pdo'] ?? null;
|
||||
if (!($pdo instanceof PDO)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// --- IDs
|
||||
$praticienId = (int)($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticienId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Traductions (fallbacks)
|
||||
$translations = $GLOBALS['translations'] ?? [];
|
||||
$t_expire = (string)($translations['alerte_abonnement_expire'] ?? "Votre abonnement est expiré.");
|
||||
$t_resilie = (string)($translations['alerte_abonnement_resilie'] ?? "Votre abonnement est résilié. Il se termine le {date}.");
|
||||
|
||||
// --- Récupère abonnement
|
||||
$stmt = $pdo->prepare("SELECT abonnement_id, date_expiration FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$praticienId]);
|
||||
$p = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
|
||||
$abonnement_id = $p['abonnement_id'] ?? null;
|
||||
|
||||
// date_expiration -> timestamp (0 si absent/invalide)
|
||||
$expiration_ts = 0;
|
||||
if (!empty($p['date_expiration'])) {
|
||||
$ts = strtotime((string)$p['date_expiration']);
|
||||
$expiration_ts = ($ts !== false) ? $ts : 0;
|
||||
}
|
||||
|
||||
$now_ts = time();
|
||||
|
||||
// --- Pages autorisées même si expiré
|
||||
$page_actuelle = basename((string)($_SERVER['PHP_SELF'] ?? ''));
|
||||
$pages_autorisees = [
|
||||
'choisir_abonnement.php',
|
||||
'souscrire_abonnement.php',
|
||||
'resiliation.php',
|
||||
'resiliation_action.php',
|
||||
'deconnexion.php',
|
||||
'paiement_simule.php',
|
||||
'create_checkout.php',
|
||||
];
|
||||
|
||||
// --- État abonnement
|
||||
$is_expired = ($expiration_ts > 0 && $expiration_ts < $now_ts);
|
||||
|
||||
// ⚠️ Cas "résilié mais encore actif": abonnement_id NULL mais expiration future
|
||||
$is_cancelled_but_active = (!$abonnement_id && $expiration_ts >= $now_ts && $expiration_ts > 0);
|
||||
|
||||
// --- Bannière (affichage)
|
||||
if ($is_cancelled_but_active) {
|
||||
$alerte_abonnement = strtr($t_resilie, [
|
||||
'{date}' => date('d/m/Y', $expiration_ts),
|
||||
]);
|
||||
} elseif ($is_expired) {
|
||||
$alerte_abonnement = $t_expire;
|
||||
}
|
||||
|
||||
// (Optionnel) expose aussi un état global utile
|
||||
$GLOBALS['abonnement_state'] = [
|
||||
'abonnement_id' => $abonnement_id,
|
||||
'expiration_ts' => $expiration_ts,
|
||||
'is_expired' => $is_expired,
|
||||
'is_cancelled_but_active' => $is_cancelled_but_active,
|
||||
];
|
||||
|
||||
// --- Redirection si expiré (uniquement si expiration connue)
|
||||
if ($is_expired && !in_array($page_actuelle, $pages_autorisees, true)) {
|
||||
|
||||
// Important: éviter "headers already sent"
|
||||
if (!headers_sent()) {
|
||||
header('Location: /public/choisir_abonnement.php', true, 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Si headers déjà envoyés, on ne peut pas header() -> fallback JS
|
||||
echo '<script>location.href="/public/choisir_abonnement.php";</script>';
|
||||
exit;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user