Files
maria_uslugi/public/client/list_praticien.php
T
2026-03-05 10:25:39 +01:00

491 lines
19 KiB
PHP

<?php
require_once __DIR__ . '/../includes/init_client.php';
// Support recherche ville en russe + latin
// Essaie d'abord /public/includes/, sinon /includes/ (selon ton projet)
$__paths = [
__DIR__ . '/../includes/search_helpers.php',
dirname(__DIR__, 2) . '/includes/search_helpers.php',
];
foreach ($__paths as $__p) { if (is_file($__p)) { require_once $__p; break; } }
unset($__paths, $__p);
/* =========================
Paramètres / Recherche
========================= */
$perPage = 12;
$page = max(1, (int)($_GET['page'] ?? 1));
$qRaw = trim((string)($_GET['q'] ?? ''));
$wRaw = trim((string)($_GET['where'] ?? ''));
$q = mb_strtolower($qRaw);
$w = mb_strtolower($wRaw);
$isSearch = ($q !== '' || $w !== '');
/* =========================
Helpers
========================= */
function video_thumb_info(?string $url): ?array {
if (!$url || !trim($url)) return null;
$u = trim($url);
// YouTube
if (preg_match('~(?:youtube\\.com/(?:watch\\?v=|embed/|shorts/)|youtu\\.be/)([A-Za-z0-9_-]{6,})~i', $u, $m)) {
$id = $m[1];
return ['provider'=>'youtube','id'=>$id, 'thumb'=>"https://i.ytimg.com/vi/$id/hqdefault.jpg"];
}
// Dailymotion
if (preg_match('~dailymotion\\.com/video/([A-Za-z0-9]+)|dai\\.ly/([A-Za-z0-9]+)~i', $u, $m)) {
$id = $m[1] ?: $m[2];
return ['provider'=>'dailymotion','id'=>$id,'thumb'=>"https://www.dailymotion.com/thumbnail/video/$id"];
}
// Fichiers directs
$path = parse_url($u, PHP_URL_PATH) ?? '';
if (preg_match('~\\.(mp4|webm|ogg)$~i', $path)) return ['provider'=>'file','id'=>null,'thumb'=>null];
if (strpos(strtolower($u), 'vimeo.com') !== false) return ['provider'=>'vimeo','id'=>null,'thumb'=>null];
return ['provider'=>'other','id'=>null,'thumb'=>null];
}
function fullName(array $p): string {
return trim(($p['prenom'] ?? '').' '.($p['nom'] ?? ''));
}
function excerpt_words(string $html, int $words = 60): string {
$plain = trim(preg_replace('/\\s+/', ' ', strip_tags($html)));
if ($plain === '') return '';
$parts = preg_split('/\\s+/', $plain);
if (count($parts) <= $words) return $plain;
return implode(' ', array_slice($parts, 0, $words)).'…';
}
/* =========================
Requêtes (COUNT + LIST)
========================= */
$whereSql = [];
$params = [];
/*==========================BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
$whereSql[] = "(date_expiration IS NOT NULL AND date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
/*=======================================================================================*/
if ($q !== '') {
$words = preg_split('/\s+/', $q, -1, PREG_SPLIT_NO_EMPTY);
$and = [];
foreach ($words as $i => $word) {
$k1 = ":q_name_$i";
$k2 = ":q_spec_$i";
$and[] = "(
LOWER(CONCAT(COALESCE(prenom,''),' ',COALESCE(nom,''))) LIKE $k1
OR LOWER(COALESCE(specialite,'')) LIKE $k2
)";
$params[$k1] = '%'.$word.'%';
$params[$k2] = '%'.$word.'%';
}
$whereSql[] = '(' . implode(' AND ', $and) . ')';
}
if ($w !== '') {
// Candidats latins à partir d'une saisie RU (alias + translittération)
$villeCands = city_query_candidates($w);
// Ajoute aussi la saisie brute (utile si déjà en latin)
$villeCands[] = mb_strtolower($w, 'UTF-8');
$villeCands = array_values(array_unique($villeCands));
$ors = [];
// Ville (LOWER(...)) : on cherche par préfixe
foreach ($villeCands as $i => $cand) {
$key = ":wv$i";
$ors[] = "LOWER(COALESCE(ville,'')) LIKE $key";
$params[$key] = $cand . '%'; // ← passe en '%'.$cand.'%' si tu veux "contient"
}
// Code postal : on garde un LIKE simple sur la saisie brute
$ors[] = "COALESCE(code_postal,'') LIKE :wcp";
$params[':wcp'] = '%' . $w . '%';
$whereSql[] = '(' . implode(' OR ', $ors) . ')';
}
$whereClause = $whereSql ? (' WHERE '.implode(' AND ', $whereSql)) : '';
/* Total pour pagination (utile seulement si !isSearch, mais on calcule une fois) */
$sqlCount = "SELECT COUNT(*) FROM praticien".$whereClause;
$stmt = $pdo->prepare($sqlCount);
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$totalPages = max(1, (int)ceil($total / $perPage));
/* Liste (paginée si pas de recherche, sinon tout) */
$sqlList = "
SELECT id, titre, nom, prenom, photo, specialite, ville, code_postal, presentation, video
FROM praticien
$whereClause
ORDER BY specialite ASC, nom ASC
";
if (!$isSearch) {
$sqlList .= " LIMIT :limit OFFSET :offset";
}
$stmt = $pdo->prepare($sqlList);
foreach ($params as $k=>$v) $stmt->bindValue($k, $v, PDO::PARAM_STR);
if (!$isSearch) {
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', ($page-1)*$perPage, PDO::PARAM_INT);
}
$stmt->execute();
$praticiens = $stmt->fetchAll(PDO::FETCH_ASSOC);
/* =========================
Rendu partiel AJAX (#results)
========================= */
function render_results(array $praticiens, array $translations, bool $isSearch, int $page, int $totalPages): void {
?>
<?php if ($isSearch): ?>
<h1 class="results-title"><?= htmlspecialchars($translations['res_poisk'] ?? 'Résultats', ENT_QUOTES, 'UTF-8') ?></h1>
<div id="resultCount" class="result-count">
<?php
$count = count($praticiens);
echo $count === 0
? htmlspecialchars($translations['ne_naideno'] ?? 'Aucun résultat', ENT_QUOTES, 'UTF-8')
: htmlspecialchars(($translations['naideno'] ?? 'Résultats')." : ".$count, ENT_QUOTES, 'UTF-8');
?>
</div>
<?php endif; ?>
<div class="pro-list" id="list">
<?php if (empty($praticiens)): ?>
<?php else: ?>
<?php foreach ($praticiens as $p):
$name = fullName($p);
$spec = $p['specialite'] ?? '';
$ville = $p['ville'] ?? '';
$cp = $p['code_postal'] ?? '';
$present= excerpt_words((string)($p['presentation'] ?? ''), 35);
$vinfo = !empty($p['video']) ? video_thumb_info($p['video']) : null;
$thumb = $vinfo['thumb'] ?? null;
$altVid = htmlspecialchars($translations['video_alt'] ?? 'Vidéo', ENT_QUOTES, 'UTF-8');
$href = !empty($p['video']) ? htmlspecialchars($p['video'], ENT_QUOTES, 'UTF-8') : '';
?>
<article class="pro-card praticien"
data-name="<?= htmlspecialchars(mb_strtolower($name), ENT_QUOTES, 'UTF-8') ?>"
data-spec="<?= htmlspecialchars(mb_strtolower($spec), ENT_QUOTES, 'UTF-8') ?>"
data-ville="<?= htmlspecialchars(mb_strtolower($ville), ENT_QUOTES, 'UTF-8') ?>"
data-cp="<?= htmlspecialchars(mb_strtolower($cp), ENT_QUOTES, 'UTF-8') ?>">
<!-- Photo -->
<div class="pro-photo">
<a href="/public/client/detail_praticien.php?id=<?= (int)$p['id'] ?>"><img
src="<?= !empty($p['photo']) ? '/public/uploads/'.htmlspecialchars($p['photo'], ENT_QUOTES, 'UTF-8') : '/public/uploads/img/user_placeholder.png' ?>"
alt="<?= htmlspecialchars($translations['alt_photo'] ?? 'Photo du praticien', ENT_QUOTES, 'UTF-8') ?>"
loading="lazy"></a>
</div>
<!-- Infos -->
<div class="pro-info">
<h2 class="pro-name"><a href="/public/client/detail_praticien.php?id=<?= (int)$p['id'] ?>"><?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></a></h2>
<?php if ($spec): ?><div class="pro-spec"><?= htmlspecialchars($spec, ENT_QUOTES, 'UTF-8') ?></div><?php endif; ?>
<?php if ($present): ?>
<p class="pro-desc"><?= nl2br(htmlspecialchars($present, ENT_QUOTES, 'UTF-8')) ?></p>
<?php endif; ?>
<a href="#" class="favori_lien" data-id="<?= (int)$p['id'] ?>" data-loading="0">
<?= htmlspecialchars($translations['plus_favorits'] ?? 'Ajouter aux favoris', ENT_QUOTES, 'UTF-8') ?>
</a>
<a href="/public/client/detail_praticien.php?id=<?= (int)$p['id'] ?>">
<?= htmlspecialchars($translations['plus_details'] ?? 'Détails', ENT_QUOTES, 'UTF-8') ?>
</a>
</div>
<!-- Vidéo -->
<?php if(!empty($p['video'])): ?>
<div class="pro-video-col">
<a class="pro-video"
href="#"
data-bs-toggle="modal"
data-bs-target="#videoModal"
data-yturl="<?= $href ?>"
aria-label="<?= $altVid ?>">
<?php if ($thumb): ?>
<img src="<?= htmlspecialchars($thumb, ENT_QUOTES, 'UTF-8') ?>" alt="<?= $altVid ?>" loading="lazy">
<?php else: ?>
<img src="/public/uploads/img/video_placeholder.jpg" alt="<?= $altVid ?>" loading="lazy">
<?php endif; ?>
</a>
</div>
<?php endif; ?>
<!-- CTA -->
<div class="pro-cta-col">
<a class="pro-cta" href="/public/client/reserver_rdv.php?praticien_id=<?= (int)$p['id'] ?>">
<?= htmlspecialchars($translations['den'] ?? 'Prendre RDV', ENT_QUOTES, 'UTF-8') ?>
</a>
</div>
</article>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php if (!$isSearch && $totalPages > 1): ?>
<nav class="pager" aria-label="Pagination">
<a class="pager-arrow <?= $page<=1?'is-disabled':'' ?>"
href="?page=<?= max(1,$page-1) ?>" aria-label="Page précédente">&laquo;&laquo;</a>
<?php
$start = max(1, $page-2);
$end = min($totalPages, $page+2);
for ($p=$start; $p<=$end; $p++):
?>
<a class="pager-page <?= $p===$page?'is-active':'' ?>" href="?page=<?= $p ?>"><?= $p ?></a>
<?php endfor; ?>
<a class="pager-arrow <?= $page>=$totalPages?'is-disabled':'' ?>"
href="?page=<?= min($totalPages,$page+1) ?>" aria-label="Page suivante">&raquo;&raquo;</a>
</nav>
<?php endif;
}
/* Si appel AJAX, ne renvoyer que le fragment #results */
if (isset($_GET['ajax']) && $_GET['ajax'] === '1') {
header('Content-Type: text/html; charset=utf-8');
ob_start();
render_results($praticiens, $translations, $isSearch, $page, $totalPages);
echo ob_get_clean();
exit;
}
require_once '../includes/header.php';
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title><?= htmlspecialchars($translations['menu_liste_specialistes'] ?? 'Liste des praticiens', ENT_QUOTES, 'UTF-8') ?></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Design -->
<link rel="stylesheet" href="../../includes/custom_style.css">
<link rel="stylesheet" href="../../includes/mobil_style.css">
<style>
/* Pagination pills (<< >> ovale + ronds numérotés) */
.pager{display:flex;justify-content:center;align-items:center;gap:10px;margin:22px 0;}
.pager a{text-decoration:none;font-weight:700}
.pager-arrow{
padding:10px 18px;border-radius:999px;border:1px solid #E0E3EB;background:#fff;color:#38378B;
box-shadow:0 6px 14px rgba(0,0,0,.06);transition:background .2s, transform .1s, opacity .2s;
}
.pager-arrow:hover{background:#f8f9ff;transform:translateY(-1px)}
.pager-arrow.is-disabled{opacity:.4;pointer-events:none}
.pager-page{
width:40px;height:40px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;
border:1px solid #E0E3EB;background:#fff;color:#38378B;box-shadow:0 6px 14px rgba(0,0,0,.06);
transition:background .2s, transform .1s, color .2s, border-color .2s;
}
.pager-page:hover{background:#f8f9ff;transform:translateY(-1px)}
.pager-page.is-active{background:#E03C3F;color:#fff;border-color:#E03C3F;transform:none}
</style>
</head>
<body>
<!-- HERO + BARRE DE RECHERCHE -->
<section class="hero">
<br>
<div class="searchbar" role="search">
<span class="search-icon" aria-hidden="true">🔎</span>
<div class="input-wrap">
<input id="q" type="text"
placeholder="<?= htmlspecialchars($translations['n_special'] ?? 'spécialité, nom…', ENT_QUOTES, 'UTF-8') ?>"
aria-label="spécialité, nom" value="<?= htmlspecialchars($qRaw, ENT_QUOTES, 'UTF-8') ?>">
</div>
<div class="divider" aria-hidden="true"></div>
<div class="input-wrap">
<input id="where" type="text"
placeholder="<?= htmlspecialchars($translations['gde'] ?? 'ville ou code postal', ENT_QUOTES, 'UTF-8') ?>"
aria-label="ville ou code postal" value="<?= htmlspecialchars($wRaw, ENT_QUOTES, 'UTF-8') ?>">
</div>
</div>
</section>
<!-- LISTE + PAGINATION (wrapper remplaçable en AJAX) -->
<div class="container">
<div id="results">
<?php render_results($praticiens, $translations, $isSearch, $page, $totalPages); ?>
</div>
</div>
<footer class="site-footer">
<div class="container">
<p><?= $translations['copyr'] ?></p>
</div>
</footer>
<!-- Modal vidéo -->
<div class="modal fade" id="videoModal" tabindex="-1" aria-labelledby="videoModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-0">
<h5 class="modal-title" id="videoModalLabel"><?= htmlspecialchars($translations['video_titre'] ?? 'Vidéo') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?= htmlspecialchars($translations['fermer'] ?? 'Fermer') ?>"></button>
</div>
<div class="modal-body pt-0">
<div class="ratio ratio-16x9 rounded-3 overflow-hidden">
<iframe id="ytPlayer"
src=""
title="YouTube video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
referrerpolicy="strict-origin-when-cross-origin"></iframe>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<?= htmlspecialchars($translations['fermer'] ?? 'Fermer') ?>
</button>
</div>
</div>
</div>
</div>
<!-- JS -->
<script>
(function(){
// Convertit n'importe quelle URL YouTube en URL d'embed nocookie
function toEmbed(url) {
if (!url) return '';
try {
const u = new URL(url), host = u.hostname;
let id = '';
if (host.includes('youtu.be')) {
id = u.pathname.split('/').filter(Boolean)[0] || '';
} else if (u.searchParams.get('v')) {
id = u.searchParams.get('v');
} else if (u.pathname.includes('/embed/')) {
id = u.pathname.split('/embed/')[1].split('/')[0];
} else if (u.pathname.includes('/shorts/')) {
id = u.pathname.split('/shorts/')[1].split('/')[0];
}
id = (id || '').replace(/[^a-zA-Z0-9_-]/g, '');
return id ? `https://www.youtube-nocookie.com/embed/${id}` : '';
} catch { return ''; }
}
const modal = document.getElementById('videoModal');
const iframe = document.getElementById('ytPlayer');
document.addEventListener('click', function(e) {
const a = e.target.closest('a.pro-video[data-yturl]');
if (!a) return;
e.preventDefault();
const embed = toEmbed(a.getAttribute('data-yturl'));
if (embed) iframe.src = embed + '?autoplay=1&rel=0&modestbranding=1';
}, false);
modal.addEventListener('hidden.bs.modal', function () { iframe.src = ''; });
// Recherche AJAX sur toute la table
const q = document.getElementById('q');
const where = document.getElementById('where');
const results = document.getElementById('results');
let t;
function reload(page=1) {
const qs = new URLSearchParams({
ajax: '1',
q: q.value.trim(),
where: where.value.trim(),
page: page
});
fetch(location.pathname + '?' + qs.toString(), { headers:{'X-Requested-With':'XMLHttpRequest'} })
.then(r => r.text())
.then(html => { results.innerHTML = html; })
.catch(()=>{ /* silencieux */ });
}
function onInput(){ clearTimeout(t); t = setTimeout(() => reload(1), 220); }
q.addEventListener('input', onInput);
where.addEventListener('input', onInput);
// Pagination progressive enhancement (charge dans #results)
document.addEventListener('click', function(e){
const a = e.target.closest('.pager a[href]');
if (!a) return;
e.preventDefault();
const url = new URL(a.getAttribute('href'), location.href);
// Conserve recherche si non vide
if (q.value.trim()) url.searchParams.set('q', q.value.trim());
if (where.value.trim()) url.searchParams.set('where', where.value.trim());
url.searchParams.set('ajax', '1');
fetch(url.toString(), { headers:{'X-Requested-With':'XMLHttpRequest'} })
.then(r=>r.text()).then(html=>{ results.innerHTML = html; });
}, false);
// Favoris (SweetAlert2)
function ajouterFavori(el, praticienId) {
if (el.dataset.loading === '1') return;
el.dataset.loading = '1';
fetch('/public/client/ajouter_favori.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({ praticien_id: praticienId }).toString()
})
.then(async (r) => {
const text = await r.text();
if (!r.ok) throw new Error(text || 'Erreur réseau');
const deja = /déjà|already/i.test(text);
el.classList.add('ajoute'); el.setAttribute('aria-pressed', 'true'); el.title = text;
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: deja ? 'info' : 'success',
title: <?= json_encode($translations['sw_favori_ajoute_title'] ?? 'Favori ajouté !', JSON_UNESCAPED_UNICODE) ?>,
text: text,
timer: 1800,
showConfirmButton: false
});
} else { alert(text); }
})
.catch((err) => {
if (typeof Swal !== 'undefined') {
Swal.fire({ icon:'error', title:'Oups', text: err.message || 'Erreur.' });
} else { console.error(err); alert('Erreur.'); }
})
.finally(() => { el.dataset.loading = '0'; });
}
document.addEventListener('click', (e)=>{
const a = e.target.closest('.favori_lien');
if (!a) return;
e.preventDefault();
const id = a.dataset.id;
const T = {
title: <?= json_encode($translations['confirmation'] ?? 'Confirmation') ?>,
text: <?= json_encode($translations['voulez_vous_ajouter_favori'] ?? 'Ajouter ce praticien à vos favoris ?') ?>,
ok: <?= json_encode($translations['btn_oui'] ?? 'Oui') ?>,
cancel:<?= json_encode($translations['btn_annuler'] ?? 'Annuler') ?>
};
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: 'question',
title: T.title,
text: T.text,
showCancelButton: true,
confirmButtonText: T.ok,
cancelButtonText: T.cancel,
}).then(res => { if (res.isConfirmed) ajouterFavori(a, id); });
} else {
if (confirm(T.text)) ajouterFavori(a, id);
}
});
})();
</script>
<script src="/js/sweetalert2@11.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
</body>
</html>